nitro 0.29.0 → 0.30.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. data/CHANGELOG +410 -0
  2. data/ProjectInfo +36 -44
  3. data/README +5 -5
  4. data/doc/AUTHORS +6 -0
  5. data/doc/RELEASES +159 -2
  6. data/lib/glue/sweeper.rb +2 -2
  7. data/lib/glue/webfile.rb +14 -1
  8. data/lib/nitro.rb +6 -9
  9. data/lib/nitro/adapter/mongrel.rb +36 -43
  10. data/lib/nitro/adapter/scgi.rb +1 -1
  11. data/lib/nitro/adapter/webrick.rb +96 -24
  12. data/lib/nitro/caching/actions.rb +2 -1
  13. data/lib/nitro/caching/fragments.rb +1 -8
  14. data/lib/nitro/caching/output.rb +14 -4
  15. data/lib/nitro/cgi.rb +19 -21
  16. data/lib/nitro/cgi/cookie.rb +5 -1
  17. data/lib/nitro/cgi/request.rb +20 -4
  18. data/lib/nitro/compiler.rb +74 -28
  19. data/lib/nitro/compiler/cleanup.rb +1 -1
  20. data/lib/nitro/compiler/elements.rb +1 -2
  21. data/lib/nitro/compiler/localization.rb +1 -1
  22. data/lib/nitro/compiler/markup.rb +1 -1
  23. data/lib/nitro/compiler/script.rb +52 -44
  24. data/lib/nitro/compiler/squeeze.rb +4 -3
  25. data/lib/nitro/compiler/xslt.rb +7 -6
  26. data/lib/nitro/context.rb +39 -20
  27. data/lib/nitro/controller.rb +24 -5
  28. data/lib/nitro/dispatcher.rb +13 -5
  29. data/lib/nitro/global.rb +63 -0
  30. data/lib/nitro/helper/feed.rb +432 -0
  31. data/lib/nitro/helper/form.rb +11 -3
  32. data/lib/nitro/helper/form/builder.rb +140 -0
  33. data/lib/nitro/helper/form/controls.rb +2 -1
  34. data/lib/nitro/helper/javascript.rb +6 -0
  35. data/lib/nitro/helper/javascript/morphing.rb +13 -6
  36. data/lib/nitro/helper/xhtml.rb +42 -6
  37. data/lib/nitro/helper/xml.rb +3 -0
  38. data/lib/nitro/part.rb +2 -2
  39. data/lib/nitro/render.rb +7 -2
  40. data/lib/nitro/router.rb +57 -16
  41. data/lib/nitro/scaffolding.rb +29 -20
  42. data/lib/nitro/server.rb +4 -10
  43. data/lib/nitro/server/drb.rb +1 -1
  44. data/lib/nitro/server/runner.rb +10 -0
  45. data/lib/nitro/session.rb +31 -12
  46. data/lib/nitro/session/drb.rb +13 -1
  47. data/lib/nitro/session/file.rb +1 -1
  48. data/lib/nitro/session/memcached.rb +1 -1
  49. data/lib/nitro/session/memory.rb +1 -1
  50. data/lib/nitro/session/og.rb +1 -1
  51. data/lib/nitro/test/testcase.rb +3 -0
  52. data/proto/public/error.xhtml +5 -5
  53. data/proto/public/js/controls.js +2 -2
  54. data/proto/public/js/dragdrop.js +320 -79
  55. data/proto/public/js/effects.js +200 -152
  56. data/proto/public/js/prototype.js +284 -63
  57. data/proto/public/js/scriptaculous.js +7 -5
  58. data/proto/public/js/unittest.js +11 -0
  59. data/proto/public/scaffold/advanced_search.xhtml +30 -0
  60. data/proto/public/scaffold/list.xhtml +8 -1
  61. data/proto/public/scaffold/search.xhtml +2 -1
  62. data/proto/script/scgi_service +1 -1
  63. data/src/part/admin/controller.rb +1 -1
  64. data/src/part/admin/skin.rb +1 -1
  65. data/test/nitro/CONFIG.rb +3 -0
  66. data/test/nitro/adapter/tc_webrick.rb +1 -1
  67. data/test/nitro/cgi/tc_cookie.rb +1 -1
  68. data/test/nitro/cgi/tc_request.rb +5 -5
  69. data/test/nitro/compiler/tc_client_morpher.rb +47 -0
  70. data/test/nitro/compiler/tc_compiler.rb +2 -0
  71. data/test/nitro/helper/tc_feed.rb +138 -0
  72. data/test/nitro/helper/tc_pager.rb +1 -1
  73. data/test/nitro/helper/tc_rss.rb +1 -1
  74. data/test/nitro/helper/tc_table.rb +1 -1
  75. data/test/nitro/helper/tc_xhtml.rb +1 -1
  76. data/test/nitro/tc_caching.rb +1 -1
  77. data/test/nitro/tc_cgi.rb +1 -1
  78. data/test/nitro/tc_context.rb +1 -1
  79. data/test/nitro/tc_controller.rb +31 -3
  80. data/test/nitro/tc_controller_aspect.rb +1 -1
  81. data/test/nitro/tc_dispatcher.rb +1 -1
  82. data/test/nitro/tc_element.rb +1 -1
  83. data/test/nitro/tc_flash.rb +1 -1
  84. data/test/nitro/tc_helper.rb +1 -1
  85. data/test/nitro/tc_render.rb +6 -6
  86. data/test/nitro/tc_router.rb +8 -4
  87. data/test/nitro/tc_server.rb +1 -3
  88. data/test/nitro/tc_session.rb +1 -3
  89. metadata +107 -104
  90. data/Rakefile +0 -232
  91. data/lib/nitro/adapter/acgi.rb +0 -237
  92. data/proto/public/Makefile.acgi +0 -40
  93. data/proto/public/acgi.c +0 -138
@@ -88,16 +88,6 @@ class Server
88
88
  @map['/'] = options[:controller] if options[:controller]
89
89
  @dispatcher = options[:dispatcher] || Dispatcher.new(@map)
90
90
 
91
- # Create the actual store. Copy values already inserted
92
- # in the temporary cache.
93
- #--
94
- # FIXME: cleanup this code
95
- #++
96
-
97
- temp = $global
98
- $global = $application = Context.global_cache_class.new
99
- $global.update(temp)
100
-
101
91
  return self
102
92
  end
103
93
 
@@ -128,6 +118,10 @@ class Server
128
118
  runner.setup_mode
129
119
  runner.daemonize if runner.daemon
130
120
 
121
+ unless Session.cache
122
+ require 'nitro/session/memory'
123
+ end
124
+
131
125
  server = Server.new
132
126
  server.start(options)
133
127
 
@@ -68,7 +68,7 @@ class DrbServer
68
68
  # The default implementation only creates a session store.
69
69
 
70
70
  def setup_drb_objects
71
- require 'nitro/session'
71
+ require 'nitro/session/drb'
72
72
  @session_cache = SyncHash.new
73
73
  DRb.start_service("druby://#{Session.cache_address}:#{Session.cache_port}", @session_cache)
74
74
  puts "Drb session cache at druby://#{Session.cache_address}:#{Session.cache_port}."
@@ -189,6 +189,16 @@ class Runner
189
189
  @spider = :render
190
190
  end
191
191
 
192
+ opts.on('--record FILENAME', 'Record the application server session to the given file.') do |filename|
193
+ @server = :webrick
194
+ $record_session_filename = filename || 'vcrsession.yaml'
195
+ end
196
+
197
+ opts.on('--playback FILENAME', 'Playback a previously recorded session from the given file.') do |filename|
198
+ @server = :webrick
199
+ $playback_session_filename = filename || 'vcrsession.yaml'
200
+ end
201
+
192
202
  opts.on_tail('-v', '--version', 'Show version.') do
193
203
  puts "Nitro #{Nitro::Version}"
194
204
  exit
@@ -1,13 +1,13 @@
1
1
  require 'md5'
2
2
  require 'webrick'
3
3
 
4
- require 'facet/synchash'
5
- require 'facet/times'
4
+ require 'facets/more/synchash'
5
+ require 'facets/more/times'
6
+ require 'facets/more/expirable'
6
7
 
7
8
  require 'glue'
8
9
  require 'glue/attribute'
9
10
  require 'glue/configuration'
10
- require 'glue/expirable'
11
11
 
12
12
  require 'nitro/cgi/cookie'
13
13
 
@@ -24,13 +24,19 @@ module Nitro
24
24
  # The session should be persistable to survive server
25
25
  # shutdowns.
26
26
  #
27
+ # The session can be considered as a Hash where key-value
28
+ # pairs are stored. Typically symbols are used as keys. By
29
+ # convention uppercase symbols are used for internal Nitro
30
+ # session variables (ie :FLASH, :USER, etc). User applications
31
+ # typically use lowercase symbols (ie :cart, :history, etc).
32
+ #
27
33
  #--
28
34
  # TODO: rehash of the session cookie
29
35
  # TODO: store -> cache, reimplement helpers.
30
36
  #++
31
37
 
32
38
  class Session < Hash
33
- include Glue::Expirable
39
+ include Expirable
34
40
 
35
41
  # Session id salt.
36
42
 
@@ -99,15 +105,28 @@ class Session < Hash
99
105
  return session
100
106
  end
101
107
 
108
+ # The number of active (online) sessions.
109
+ # DON'T use yet!
110
+
111
+ def count
112
+ Session.cache.size
113
+ end
114
+
115
+ # Perform Session garbage collection. You may call this
116
+ # method from a cron job.
117
+
118
+ def garbage_collect
119
+ expired = []
120
+ for s in Session.cache.all
121
+ expired << s.session_id if s.expired?
122
+ end
123
+ for sid in expired
124
+ Session.cache.delete(sid)
125
+ end
126
+ end
127
+ alias_method :gc!, :garbage_collect
102
128
  end
103
129
 
104
- # By default sessions are stored in memory.
105
- #--
106
- # gmosx: should be placed here.
107
- #++
108
-
109
- set_cache_type(:memory)
110
-
111
130
  # The unique id of this session.
112
131
 
113
132
  attr_reader :session_id
@@ -145,7 +164,7 @@ protected
145
164
  # Random may produce equal ids? add a prefix
146
165
  # (SALT) to stop hackers from creating session_ids.
147
166
  #--
148
- # THINK: Is MD5 slow???
167
+ # THINK: Is MD5 slow??? Allow for pluggable hashes.
149
168
  #++
150
169
 
151
170
  def create_id
@@ -3,7 +3,19 @@ require 'nitro/session'
3
3
 
4
4
  module Nitro
5
5
 
6
- Logger.debug "Using DRb sessions at #{Glue::DrbCache.address}:#{Glue::DrbCache.port}." if defined?(Logger)
6
+ Logger.info "Using DRb sessions at #{Glue::DrbCache.address}:#{Glue::DrbCache.port}." if defined?(Logger) && $DBG
7
+
8
+ class Session < Hash
9
+
10
+ # The address of the Drb store.
11
+
12
+ setting :cache_address, :default => '127.0.0.1', :doc => 'The address of the Drb store'
13
+
14
+ # The port of the Drb store.
15
+
16
+ setting :cache_port, :default => 9069, :doc => 'The port of the Drb store'
17
+
18
+ end
7
19
 
8
20
  Session.cache = Glue::DrbCache.new
9
21
 
@@ -5,7 +5,7 @@ module Nitro
5
5
 
6
6
  # A Session manager that persists sessions on disk.
7
7
 
8
- Logger.debug "Using File sessions." if defined?(Logger)
8
+ Logger.info "Using File sessions." if defined?(Logger) && $DBG
9
9
 
10
10
  Session.cache = Glue::FileCache.new("session_#{Session.cookie_name}", Session.keepalive)
11
11
 
@@ -5,7 +5,7 @@ module Nitro
5
5
 
6
6
  # A Session manager that persists sessions on disk.
7
7
 
8
- Logger.debug "Using MemCached sessions." if defined?(Logger)
8
+ Logger.debug "Using MemCached sessions." if defined?(Logger) && $DBG
9
9
 
10
10
  Session.cache = Glue::MemCached.new("session_#{Session.cookie_name}", Session.keepalive)
11
11
 
@@ -4,7 +4,7 @@ require 'glue/logger'
4
4
 
5
5
  module Nitro
6
6
 
7
- Logger.debug "Using Memory sessions." if defined?(Logger)
7
+ Logger.info "Using Memory sessions." if defined?(Logger) && $DBG
8
8
 
9
9
  Session.cache = Glue::MemoryCache.new
10
10
 
@@ -5,7 +5,7 @@ module Nitro
5
5
 
6
6
  # A Session manager that persists sessions on an Og store.
7
7
 
8
- Logger.debug "Using Og sessions." if defined?(Logger)
8
+ Logger.debug "Using Og sessions." if defined?(Logger) && $DBG
9
9
 
10
10
  Session.cache = OgCache.new("session_#{Session.cookie_name}", Session.keepalive)
11
11
 
@@ -44,6 +44,9 @@ class TestCase
44
44
  context.headers['REQUEST_URI'] = uri
45
45
  context.headers['REQUEST_METHOD'] = options[:method].to_s.upcase
46
46
  context.headers['REMOTE_ADDR'] ||= '127.0.0.1'
47
+ if ((:get == options[:method]) and (options[:params]))
48
+ context.headers['QUERY_STRING'] = options[:params].collect {|k,v| "#{k}=#{v}"}.join('&')
49
+ end
47
50
  context.cookies.merge! options[:cookies] if options[:cookies]
48
51
  context.session.merge! options[:session] if options[:session]
49
52
 
@@ -1,7 +1,7 @@
1
1
  <html>
2
2
  <head>
3
3
  <script lang="javascript" type="text/javascript">
4
- // <!--
4
+ // <!--
5
5
  function toggleVisible(element) {
6
6
  if (element.style.display == 'block') {
7
7
  element.style.display = 'none';
@@ -98,26 +98,26 @@
98
98
  </div>
99
99
  <?r end ?>
100
100
 
101
- <h2><a href="#" onclick="document.getElementById('request').style.display = 'block'; return false">Request</a></h2>
101
+ <h2><a href="#" onclick="return toggleVisible(document.getElementById('request'));">Request</a></h2>
102
102
  <div id="request" style="display: none">
103
103
  <p><strong>Parameters:</strong> #{request.params.reject{ |k,v| k == :__RELOADED__ }.inspect}</p>
104
104
  <p><strong>Cookies:</strong> #{request.cookies.inspect}</p>
105
105
  <p><strong>Headers:</strong><br />#{request.headers.collect { |k, v| "#{k} => #{v}" }.join('<br />')}</p>
106
106
  </div>
107
107
 
108
- <h2><a href="#" onclick="document.getElementById('response').style.display = 'block'; return false">Response</a></h2>
108
+ <h2><a href="#" onclick="return toggleVisible(document.getElementById('response'));">Response</a></h2>
109
109
  <div id="response" style="display: none">
110
110
  <p><strong>Headers:</strong> #{request.response_headers.inspect}</p>
111
111
  <p><strong>Cookies:</strong> #{request.response_cookies.inspect}</p>
112
112
  </div>
113
113
 
114
- <h2><a href="#" onclick="document.getElementById('session').style.display = 'block'; return false">Session</a></h2>
114
+ <h2><a href="#" onclick="return toggleVisible(document.getElementById('session'));">Session</a></h2>
115
115
  <div id="session" style="display: none">
116
116
  <p><strong>Values:</strong> #{session.inspect}</p>
117
117
  </div>
118
118
 
119
119
  <br /><br />
120
- Powered by <a href="http://www.nitrohq.com">Nitro</a> version #{Nitro::Version}
120
+ Powered by <a href="http://www.nitroproject.org">Nitro</a> version #{Nitro::Version}
121
121
  <?r end ?>
122
122
  </body>
123
123
  </html>
@@ -141,8 +141,8 @@ Autocompleter.Base.prototype = {
141
141
  return;
142
142
  }
143
143
  else
144
- if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN)
145
- return;
144
+ if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
145
+ (navigator.appVersion.indexOf('AppleWebKit') > 0 && event.keyCode == 0)) return;
146
146
 
147
147
  this.changed = true;
148
148
  this.hasFocus = true;
@@ -1,4 +1,5 @@
1
1
  // Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
2
+ // (c) 2005 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
2
3
  //
3
4
  // See scriptaculous.js for full license.
4
5
 
@@ -15,7 +16,8 @@ var Droppables = {
15
16
  element = $(element);
16
17
  var options = Object.extend({
17
18
  greedy: true,
18
- hoverclass: null
19
+ hoverclass: null,
20
+ tree: false
19
21
  }, arguments[1] || {});
20
22
 
21
23
  // cache containers
@@ -37,12 +39,27 @@ var Droppables = {
37
39
 
38
40
  this.drops.push(options);
39
41
  },
42
+
43
+ findDeepestChild: function(drops) {
44
+ deepest = drops[0];
45
+
46
+ for (i = 1; i < drops.length; ++i)
47
+ if (Element.isParent(drops[i].element, deepest.element))
48
+ deepest = drops[i];
49
+
50
+ return deepest;
51
+ },
40
52
 
41
53
  isContained: function(element, drop) {
42
- var parentNode = element.parentNode;
43
- return drop._containers.detect(function(c) { return parentNode == c });
54
+ var containmentNode;
55
+ if(drop.tree) {
56
+ containmentNode = element.treeNode;
57
+ } else {
58
+ containmentNode = element.parentNode;
59
+ }
60
+ return drop._containers.detect(function(c) { return containmentNode == c });
44
61
  },
45
-
62
+
46
63
  isAffected: function(point, element, drop) {
47
64
  return (
48
65
  (drop.element!=element) &&
@@ -68,18 +85,22 @@ var Droppables = {
68
85
 
69
86
  show: function(point, element) {
70
87
  if(!this.drops.length) return;
88
+ var affected = [];
71
89
 
72
90
  if(this.last_active) this.deactivate(this.last_active);
73
91
  this.drops.each( function(drop) {
74
- if(Droppables.isAffected(point, element, drop)) {
75
- if(drop.onHover)
76
- drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
77
- if(drop.greedy) {
78
- Droppables.activate(drop);
79
- throw $break;
80
- }
81
- }
92
+ if(Droppables.isAffected(point, element, drop))
93
+ affected.push(drop);
82
94
  });
95
+
96
+ if(affected.length>0) {
97
+ drop = Droppables.findDeepestChild(affected);
98
+ Position.within(drop.element, point[0], point[1]);
99
+ if(drop.onHover)
100
+ drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
101
+
102
+ Droppables.activate(drop);
103
+ }
83
104
  },
84
105
 
85
106
  fire: function(event, element) {
@@ -187,15 +208,17 @@ Draggable.prototype = {
187
208
  initialize: function(element) {
188
209
  var options = Object.extend({
189
210
  handle: false,
190
- starteffect: function(element) {
191
- new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7});
211
+ starteffect: function(element) {
212
+ element._opacity = Element.getOpacity(element);
213
+ new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7});
192
214
  },
193
215
  reverteffect: function(element, top_offset, left_offset) {
194
216
  var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
195
217
  element._revert = new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur});
196
218
  },
197
- endeffect: function(element) {
198
- new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0});
219
+ endeffect: function(element) {
220
+ var toOpacity = typeof element._opacity == 'number' ? element._opacity : 1.0
221
+ new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity});
199
222
  },
200
223
  zindex: 1000,
201
224
  revert: false,
@@ -207,12 +230,15 @@ Draggable.prototype = {
207
230
 
208
231
  this.element = $(element);
209
232
 
210
- if(options.handle && (typeof options.handle == 'string'))
211
- this.handle = Element.childrenWithClassName(this.element, options.handle)[0];
233
+ if(options.handle && (typeof options.handle == 'string')) {
234
+ var h = Element.childrenWithClassName(this.element, options.handle, true);
235
+ if(h.length>0) this.handle = h[0];
236
+ }
212
237
  if(!this.handle) this.handle = $(options.handle);
213
238
  if(!this.handle) this.handle = this.element;
214
239
 
215
- if(options.scroll) options.scroll = $(options.scroll);
240
+ if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML)
241
+ options.scroll = $(options.scroll);
216
242
 
217
243
  Element.makePositioned(this.element); // fix IE
218
244
 
@@ -277,8 +303,14 @@ Draggable.prototype = {
277
303
  }
278
304
 
279
305
  if(this.options.scroll) {
280
- this.originalScrollLeft = this.options.scroll.scrollLeft;
281
- this.originalScrollTop = this.options.scroll.scrollTop;
306
+ if (this.options.scroll == window) {
307
+ var where = this._getWindowScroll(this.options.scroll);
308
+ this.originalScrollLeft = where.left;
309
+ this.originalScrollTop = where.top;
310
+ } else {
311
+ this.originalScrollLeft = this.options.scroll.scrollLeft;
312
+ this.originalScrollTop = this.options.scroll.scrollTop;
313
+ }
282
314
  }
283
315
 
284
316
  Draggables.notify('onStart', this, event);
@@ -294,13 +326,18 @@ Draggable.prototype = {
294
326
  if(this.options.change) this.options.change(this);
295
327
 
296
328
  if(this.options.scroll) {
297
- //if(this.scrollInterval) this.scroll();
298
329
  this.stopScrolling();
299
- var p = Position.page(this.options.scroll);
300
- p[0] += this.options.scroll.scrollLeft;
301
- p[1] += this.options.scroll.scrollTop;
302
- p.push(p[0]+this.options.scroll.offsetWidth);
303
- p.push(p[1]+this.options.scroll.offsetHeight);
330
+
331
+ var p;
332
+ if (this.options.scroll == window) {
333
+ with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
334
+ } else {
335
+ p = Position.page(this.options.scroll);
336
+ p[0] += this.options.scroll.scrollLeft;
337
+ p[1] += this.options.scroll.scrollTop;
338
+ p.push(p[0]+this.options.scroll.offsetWidth);
339
+ p.push(p[1]+this.options.scroll.offsetHeight);
340
+ }
304
341
  var speed = [0,0];
305
342
  if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
306
343
  if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
@@ -366,7 +403,7 @@ Draggable.prototype = {
366
403
  var d = this.currentDelta();
367
404
  pos[0] -= d[0]; pos[1] -= d[1];
368
405
 
369
- if(this.options.scroll) {
406
+ if(this.options.scroll && (this.options.scroll != window)) {
370
407
  pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
371
408
  pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
372
409
  }
@@ -377,7 +414,7 @@ Draggable.prototype = {
377
414
 
378
415
  if(this.options.snap) {
379
416
  if(typeof this.options.snap == 'function') {
380
- p = this.options.snap(p[0],p[1]);
417
+ p = this.options.snap(p[0],p[1],this);
381
418
  } else {
382
419
  if(this.options.snap instanceof Array) {
383
420
  p = p.map( function(v, i) {
@@ -400,6 +437,7 @@ Draggable.prototype = {
400
437
  if(this.scrollInterval) {
401
438
  clearInterval(this.scrollInterval);
402
439
  this.scrollInterval = null;
440
+ Draggables._lastScrollPointer = null;
403
441
  }
404
442
  },
405
443
 
@@ -413,15 +451,55 @@ Draggable.prototype = {
413
451
  var current = new Date();
414
452
  var delta = current - this.lastScrolled;
415
453
  this.lastScrolled = current;
416
- this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
417
- this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000;
454
+ if(this.options.scroll == window) {
455
+ with (this._getWindowScroll(this.options.scroll)) {
456
+ if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
457
+ var d = delta / 1000;
458
+ this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
459
+ }
460
+ }
461
+ } else {
462
+ this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
463
+ this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000;
464
+ }
418
465
 
419
466
  Position.prepare();
420
467
  Droppables.show(Draggables._lastPointer, this.element);
421
468
  Draggables.notify('onDrag', this);
422
- this.draw(Draggables._lastPointer);
469
+ Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
470
+ Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
471
+ Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
472
+ if (Draggables._lastScrollPointer[0] < 0)
473
+ Draggables._lastScrollPointer[0] = 0;
474
+ if (Draggables._lastScrollPointer[1] < 0)
475
+ Draggables._lastScrollPointer[1] = 0;
476
+ this.draw(Draggables._lastScrollPointer);
423
477
 
424
478
  if(this.options.change) this.options.change(this);
479
+ },
480
+
481
+ _getWindowScroll: function(w) {
482
+ var T, L, W, H;
483
+ with (w.document) {
484
+ if (w.document.documentElement && documentElement.scrollTop) {
485
+ T = documentElement.scrollTop;
486
+ L = documentElement.scrollLeft;
487
+ } else if (w.document.body) {
488
+ T = body.scrollTop;
489
+ L = body.scrollLeft;
490
+ }
491
+ if (w.innerWidth) {
492
+ W = w.innerWidth;
493
+ H = w.innerHeight;
494
+ } else if (w.document.documentElement && documentElement.clientWidth) {
495
+ W = documentElement.clientWidth;
496
+ H = documentElement.clientHeight;
497
+ } else {
498
+ W = body.offsetWidth;
499
+ H = body.offsetHeight
500
+ }
501
+ }
502
+ return { top: T, left: L, width: W, height: H };
425
503
  }
426
504
  }
427
505
 
@@ -447,30 +525,41 @@ SortableObserver.prototype = {
447
525
  }
448
526
 
449
527
  var Sortable = {
450
- sortables: new Array(),
528
+ sortables: {},
451
529
 
452
- options: function(element){
453
- element = $(element);
454
- return this.sortables.detect(function(s) { return s.element == element });
530
+ _findRootElement: function(element) {
531
+ while (element.tagName != "BODY") {
532
+ if(element.id && Sortable.sortables[element.id]) return element;
533
+ element = element.parentNode;
534
+ }
535
+ },
536
+
537
+ options: function(element) {
538
+ element = Sortable._findRootElement($(element));
539
+ if(!element) return;
540
+ return Sortable.sortables[element.id];
455
541
  },
456
542
 
457
543
  destroy: function(element){
458
- element = $(element);
459
- this.sortables.findAll(function(s) { return s.element == element }).each(function(s){
544
+ var s = Sortable.options(element);
545
+
546
+ if(s) {
460
547
  Draggables.removeObserver(s.element);
461
548
  s.droppables.each(function(d){ Droppables.remove(d) });
462
549
  s.draggables.invoke('destroy');
463
- });
464
- this.sortables = this.sortables.reject(function(s) { return s.element == element });
550
+
551
+ delete Sortable.sortables[s.element.id];
552
+ }
465
553
  },
466
-
554
+
467
555
  create: function(element) {
468
556
  element = $(element);
469
557
  var options = Object.extend({
470
558
  element: element,
471
559
  tag: 'li', // assumes li children, override with tag: 'tagname'
472
560
  dropOnEmpty: false,
473
- tree: false, // fixme: unimplemented
561
+ tree: false,
562
+ treeTag: 'ul',
474
563
  overlap: 'vertical', // one of 'vertical', 'horizontal'
475
564
  constraint: 'vertical', // one of 'vertical', 'horizontal', false
476
565
  containment: element, // also takes array of elements (or id's); or false
@@ -479,6 +568,8 @@ var Sortable = {
479
568
  hoverclass: null,
480
569
  ghosting: false,
481
570
  scroll: false,
571
+ scrollSensitivity: 20,
572
+ scrollSpeed: 15,
482
573
  format: /^[^_]*_(.*)$/,
483
574
  onChange: Prototype.emptyFunction,
484
575
  onUpdate: Prototype.emptyFunction
@@ -491,6 +582,8 @@ var Sortable = {
491
582
  var options_for_draggable = {
492
583
  revert: true,
493
584
  scroll: options.scroll,
585
+ scrollSpeed: options.scrollSpeed,
586
+ scrollSensitivity: options.scrollSensitivity,
494
587
  ghosting: options.ghosting,
495
588
  constraint: options.constraint,
496
589
  handle: options.handle };
@@ -516,9 +609,17 @@ var Sortable = {
516
609
  var options_for_droppable = {
517
610
  overlap: options.overlap,
518
611
  containment: options.containment,
612
+ tree: options.tree,
519
613
  hoverclass: options.hoverclass,
520
- onHover: Sortable.onHover,
521
- greedy: !options.dropOnEmpty
614
+ onHover: Sortable.onHover
615
+ //greedy: !options.dropOnEmpty
616
+ }
617
+
618
+ var options_for_tree = {
619
+ onHover: Sortable.onEmptyHover,
620
+ overlap: options.overlap,
621
+ containment: options.containment,
622
+ hoverclass: options.hoverclass
522
623
  }
523
624
 
524
625
  // fix for gecko engine
@@ -527,12 +628,9 @@ var Sortable = {
527
628
  options.draggables = [];
528
629
  options.droppables = [];
529
630
 
530
- // make it so
531
-
532
631
  // drop on empty handling
533
- if(options.dropOnEmpty) {
534
- Droppables.add(element,
535
- {containment: options.containment, onHover: Sortable.onEmptyHover, greedy: false});
632
+ if(options.dropOnEmpty || options.tree) {
633
+ Droppables.add(element, options_for_tree);
536
634
  options.droppables.push(element);
537
635
  }
538
636
 
@@ -543,11 +641,20 @@ var Sortable = {
543
641
  options.draggables.push(
544
642
  new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
545
643
  Droppables.add(e, options_for_droppable);
644
+ if(options.tree) e.treeNode = element;
546
645
  options.droppables.push(e);
547
646
  });
647
+
648
+ if(options.tree) {
649
+ (Sortable.findTreeElements(element, options) || []).each( function(e) {
650
+ Droppables.add(e, options_for_tree);
651
+ e.treeNode = element;
652
+ options.droppables.push(e);
653
+ });
654
+ }
548
655
 
549
656
  // keep reference
550
- this.sortables.push(options);
657
+ this.sortables[element.id] = options;
551
658
 
552
659
  // for onupdate
553
660
  Draggables.addObserver(new SortableObserver(element, options.onUpdate));
@@ -556,23 +663,21 @@ var Sortable = {
556
663
 
557
664
  // return all suitable-for-sortable elements in a guaranteed order
558
665
  findElements: function(element, options) {
559
- if(!element.hasChildNodes()) return null;
560
- var elements = [];
561
- $A(element.childNodes).each( function(e) {
562
- if(e.tagName && e.tagName.toUpperCase()==options.tag.toUpperCase() &&
563
- (!options.only || (Element.hasClassName(e, options.only))))
564
- elements.push(e);
565
- if(options.tree) {
566
- var grandchildren = this.findElements(e, options);
567
- if(grandchildren) elements.push(grandchildren);
568
- }
569
- });
570
-
571
- return (elements.length>0 ? elements.flatten() : null);
666
+ return Element.findChildren(
667
+ element, options.only, options.tree ? true : false, options.tag);
668
+ },
669
+
670
+ findTreeElements: function(element, options) {
671
+ return Element.findChildren(
672
+ element, options.only, options.tree ? true : false, options.treeTag);
572
673
  },
573
674
 
574
675
  onHover: function(element, dropon, overlap) {
575
- if(overlap>0.5) {
676
+ if(Element.isParent(dropon, element)) return;
677
+
678
+ if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
679
+ return;
680
+ } else if(overlap>0.5) {
576
681
  Sortable.mark(dropon, 'before');
577
682
  if(dropon.previousSibling != element) {
578
683
  var oldParentNode = element.parentNode;
@@ -595,13 +700,37 @@ var Sortable = {
595
700
  }
596
701
  }
597
702
  },
598
-
599
- onEmptyHover: function(element, dropon) {
600
- if(element.parentNode!=dropon) {
601
- var oldParentNode = element.parentNode;
602
- dropon.appendChild(element);
703
+
704
+ onEmptyHover: function(element, dropon, overlap) {
705
+ var oldParentNode = element.parentNode;
706
+ var droponOptions = Sortable.options(dropon);
707
+
708
+ if(!Element.isParent(dropon, element)) {
709
+ var index;
710
+
711
+ var children = Sortable.findElements(dropon, {tag: droponOptions.tag});
712
+ var child = null;
713
+
714
+ if(children) {
715
+ var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
716
+
717
+ for (index = 0; index < children.length; index += 1) {
718
+ if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
719
+ offset -= Element.offsetSize (children[index], droponOptions.overlap);
720
+ } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
721
+ child = index + 1 < children.length ? children[index + 1] : null;
722
+ break;
723
+ } else {
724
+ child = children[index];
725
+ break;
726
+ }
727
+ }
728
+ }
729
+
730
+ dropon.insertBefore(element, child);
731
+
603
732
  Sortable.options(oldParentNode).onChange(element);
604
- Sortable.options(dropon).onChange(element);
733
+ droponOptions.onChange(element);
605
734
  }
606
735
  },
607
736
 
@@ -633,6 +762,75 @@ var Sortable = {
633
762
 
634
763
  Element.show(Sortable._marker);
635
764
  },
765
+
766
+ _tree: function(element, options, parent) {
767
+ var children = Sortable.findElements(element, options) || [];
768
+
769
+ for (var i = 0; i < children.length; ++i) {
770
+ var match = children[i].id.match(options.format);
771
+
772
+ if (!match) continue;
773
+
774
+ var child = {
775
+ id: encodeURIComponent(match ? match[1] : null),
776
+ element: element,
777
+ parent: parent,
778
+ children: new Array,
779
+ position: parent.children.length,
780
+ container: Sortable._findChildrenElement(children[i], options.treeTag.toUpperCase())
781
+ }
782
+
783
+ /* Get the element containing the children and recurse over it */
784
+ if (child.container)
785
+ this._tree(child.container, options, child)
786
+
787
+ parent.children.push (child);
788
+ }
789
+
790
+ return parent;
791
+ },
792
+
793
+ /* Finds the first element of the given tag type within a parent element.
794
+ Used for finding the first LI[ST] within a L[IST]I[TEM].*/
795
+ _findChildrenElement: function (element, containerTag) {
796
+ if (element && element.hasChildNodes)
797
+ for (var i = 0; i < element.childNodes.length; ++i)
798
+ if (element.childNodes[i].tagName == containerTag)
799
+ return element.childNodes[i];
800
+
801
+ return null;
802
+ },
803
+
804
+ tree: function(element) {
805
+ element = $(element);
806
+ var sortableOptions = this.options(element);
807
+ var options = Object.extend({
808
+ tag: sortableOptions.tag,
809
+ treeTag: sortableOptions.treeTag,
810
+ only: sortableOptions.only,
811
+ name: element.id,
812
+ format: sortableOptions.format
813
+ }, arguments[1] || {});
814
+
815
+ var root = {
816
+ id: null,
817
+ parent: null,
818
+ children: new Array,
819
+ container: element,
820
+ position: 0
821
+ }
822
+
823
+ return Sortable._tree (element, options, root);
824
+ },
825
+
826
+ /* Construct a [i] index for a particular node */
827
+ _constructIndex: function(node) {
828
+ var index = '';
829
+ do {
830
+ if (node.id) index = '[' + node.position + ']' + index;
831
+ } while ((node = node.parent) != null);
832
+ return index;
833
+ },
636
834
 
637
835
  sequence: function(element) {
638
836
  element = $(element);
@@ -655,20 +853,63 @@ var Sortable = {
655
853
  });
656
854
 
657
855
  new_sequence.each(function(ident) {
658
- var n = nodeMap[ident];
659
- if (n) {
660
- n[1].appendChild(n[0]);
661
- delete nodeMap[ident];
662
- }
856
+ var n = nodeMap[ident];
857
+ if (n) {
858
+ n[1].appendChild(n[0]);
859
+ delete nodeMap[ident];
860
+ }
663
861
  });
664
862
  },
665
-
863
+
666
864
  serialize: function(element) {
667
865
  element = $(element);
866
+ var options = Object.extend(Sortable.options(element), arguments[1] || {});
668
867
  var name = encodeURIComponent(
669
868
  (arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
670
- return Sortable.sequence(element, arguments[1]).map( function(item) {
671
- return name + "[]=" + encodeURIComponent(item);
672
- }).join('&');
869
+
870
+ if (options.tree) {
871
+ return Sortable.tree(element, arguments[1]).children.map( function (item) {
872
+ return [name + Sortable._constructIndex(item) + "=" +
873
+ encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
874
+ }).flatten().join('&');
875
+ } else {
876
+ return Sortable.sequence(element, arguments[1]).map( function(item) {
877
+ return name + "[]=" + encodeURIComponent(item);
878
+ }).join('&');
879
+ }
673
880
  }
674
881
  }
882
+
883
+ /* Returns true if child is contained within element */
884
+ Element.isParent = function(child, element) {
885
+ if (!child.parentNode || child == element) return false;
886
+
887
+ if (child.parentNode == element) return true;
888
+
889
+ return Element.isParent(child.parentNode, element);
890
+ }
891
+
892
+ Element.findChildren = function(element, only, recursive, tagName) {
893
+ if(!element.hasChildNodes()) return null;
894
+ tagName = tagName.toUpperCase();
895
+ if(only) only = [only].flatten();
896
+ var elements = [];
897
+ $A(element.childNodes).each( function(e) {
898
+ if(e.tagName && e.tagName.toUpperCase()==tagName &&
899
+ (!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
900
+ elements.push(e);
901
+ if(recursive) {
902
+ var grandchildren = Element.findChildren(e, only, recursive, tagName);
903
+ if(grandchildren) elements.push(grandchildren);
904
+ }
905
+ });
906
+
907
+ return (elements.length>0 ? elements.flatten() : []);
908
+ }
909
+
910
+ Element.offsetSize = function (element, type) {
911
+ if (type == 'vertical' || type == 'height')
912
+ return element.offsetHeight;
913
+ else
914
+ return element.offsetWidth;
915
+ }