nitro 0.29.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ }