nitro 0.25.0 → 0.26.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 (76) hide show
  1. data/CHANGELOG +531 -1
  2. data/ProjectInfo +29 -5
  3. data/README +1 -1
  4. data/doc/AUTHORS +12 -6
  5. data/doc/RELEASES +114 -0
  6. data/lib/glue/sweeper.rb +71 -0
  7. data/lib/nitro.rb +19 -12
  8. data/lib/nitro/adapter/cgi.rb +4 -0
  9. data/lib/nitro/adapter/webrick.rb +4 -2
  10. data/lib/nitro/caching.rb +1 -0
  11. data/lib/nitro/caching/fragments.rb +7 -1
  12. data/lib/nitro/caching/output.rb +6 -1
  13. data/lib/nitro/caching/stores.rb +13 -1
  14. data/lib/nitro/cgi.rb +9 -1
  15. data/lib/nitro/cgi/request.rb +11 -3
  16. data/lib/nitro/cgi/utils.rb +24 -2
  17. data/lib/nitro/compiler.rb +89 -63
  18. data/lib/nitro/compiler/cleanup.rb +16 -0
  19. data/lib/nitro/compiler/elements.rb +117 -0
  20. data/lib/nitro/compiler/markup.rb +3 -1
  21. data/lib/nitro/compiler/morphing.rb +203 -73
  22. data/lib/nitro/compiler/script_generator.rb +14 -0
  23. data/lib/nitro/compiler/shaders.rb +1 -1
  24. data/lib/nitro/context.rb +5 -6
  25. data/lib/nitro/controller.rb +43 -21
  26. data/lib/nitro/dispatcher.rb +86 -37
  27. data/lib/nitro/element.rb +3 -105
  28. data/lib/nitro/helper/benchmark.rb +3 -0
  29. data/lib/nitro/helper/dojo.rb +0 -0
  30. data/lib/nitro/helper/form.rb +85 -255
  31. data/lib/nitro/helper/form/controls.rb +274 -0
  32. data/lib/nitro/helper/javascript.rb +86 -6
  33. data/lib/nitro/helper/pager.rb +5 -0
  34. data/lib/nitro/helper/prototype.rb +49 -0
  35. data/lib/nitro/helper/scriptaculous.rb +0 -0
  36. data/lib/nitro/helper/xhtml.rb +11 -8
  37. data/lib/nitro/helper/xml.rb +1 -1
  38. data/lib/nitro/routing.rb +8 -1
  39. data/lib/nitro/scaffolding.rb +344 -0
  40. data/lib/nitro/server.rb +5 -1
  41. data/lib/nitro/server/runner.rb +19 -15
  42. data/lib/nitro/session.rb +32 -56
  43. data/lib/nitro/session/drbserver.rb +1 -1
  44. data/lib/nitro/session/file.rb +34 -15
  45. data/lib/nitro/session/memory.rb +13 -4
  46. data/lib/nitro/session/og.rb +56 -0
  47. data/proto/public/js/controls.js +30 -1
  48. data/proto/public/js/dragdrop.js +211 -146
  49. data/proto/public/js/effects.js +261 -399
  50. data/proto/public/js/prototype.js +131 -72
  51. data/proto/public/scaffold/edit.xhtml +10 -3
  52. data/proto/public/scaffold/form.xhtml +1 -7
  53. data/proto/public/scaffold/index.xhtml +20 -0
  54. data/proto/public/scaffold/list.xhtml +15 -8
  55. data/proto/public/scaffold/new.xhtml +10 -3
  56. data/proto/public/scaffold/search.xhtml +28 -0
  57. data/proto/public/scaffold/view.xhtml +8 -0
  58. data/proto/run.rb +93 -1
  59. data/src/part/admin.rb +4 -2
  60. data/src/part/admin/controller.rb +62 -28
  61. data/src/part/admin/skin.rb +8 -8
  62. data/src/part/admin/system.css +135 -0
  63. data/src/part/admin/template/index.xhtml +8 -12
  64. data/test/nitro/caching/tc_stores.rb +17 -0
  65. data/test/nitro/tc_caching.rb +1 -4
  66. data/test/nitro/tc_dispatcher.rb +22 -10
  67. data/test/nitro/tc_element.rb +1 -1
  68. data/test/nitro/tc_session.rb +23 -11
  69. data/test/public/blog/another/very_litle/index.xhtml +1 -0
  70. metadata +29 -15
  71. data/lib/nitro/dispatcher/general.rb +0 -62
  72. data/lib/nitro/dispatcher/nice.rb +0 -57
  73. data/lib/nitro/scaffold.rb +0 -171
  74. data/proto/public/index.xhtml +0 -83
  75. data/proto/public/js/scaffold.js +0 -74
  76. data/proto/public/settings.xhtml +0 -66
@@ -6,6 +6,8 @@ require 'mega/time_in_english'
6
6
 
7
7
  require 'glue/attribute'
8
8
  require 'glue/configuration'
9
+ require 'glue/logger'
10
+ require 'glue/expirable'
9
11
 
10
12
  require 'nitro/cgi/cookie'
11
13
 
@@ -21,8 +23,11 @@ module Nitro
21
23
  #
22
24
  # The session should be persistable to survive server
23
25
  # shutdowns.
26
+ #
27
+ # TODO rehash of the session cookie
24
28
 
25
29
  class Session < Hash
30
+ is Expirable
26
31
 
27
32
  # Session id salt.
28
33
 
@@ -32,16 +37,19 @@ class Session < Hash
32
37
 
33
38
  setting :cookie_name, :default => 'nsid', :doc => 'The name of the cookie that stores the session id'
34
39
 
40
+ # useful with persistents sessions stores
41
+
42
+ setting :cookie_expires, :default => false, :doc => 'Set expires parameter of session cookie equal to the keepalive setting?'
43
+
35
44
  # The session keepalive time. The session is eligable for
36
45
  # garbage collection after this time passes.
37
46
 
38
47
  setting :keepalive, :default => 30.minutes, :doc => 'The session keepalive time'
39
48
 
40
- # The sessions store. By default sessions are
41
- # stored in memory.
42
-
43
- cattr_accessor :store; @@store = SyncHash.new
49
+ # The sessions store.
44
50
 
51
+ cattr_accessor :store
52
+
45
53
  class << self
46
54
 
47
55
  # Set the session store. The following options are
@@ -49,16 +57,19 @@ class Session < Hash
49
57
  #
50
58
  # * :memory [default]
51
59
  # * :drb
60
+ # * :og
61
+ # * :file (not safe yet with multiple process as in fastcgi)
52
62
  # * :memcached (not available yet)
53
- # * :og (not available yet)
54
- # * :file (not available yet)
55
63
 
56
64
  def store_type=(store_type)
57
65
  # gmosx: RDoc complains about this, so lets use an
58
66
  # eval, AAAAAAAARGH!
59
67
  # require "nitro/session/#{store_type}"
68
+ Logger.debug "Using #{store_type} sessions."
69
+
60
70
  eval %{ require 'nitro/session/#{store_type}' }
61
71
  end
72
+ alias_method :set_store_type, :store_type=
62
73
 
63
74
  # Lookup the session in the store by using the session
64
75
  # cookie value as a key. If the session does not exist
@@ -74,6 +85,9 @@ class Session < Hash
74
85
  # Create new session.
75
86
  session = Session.new(context)
76
87
  cookie = Cookie.new(Session.cookie_name, session.session_id)
88
+ if Session.cookie_expires
89
+ cookie.expires = Time.now + Session.keepalive
90
+ end
77
91
  context.add_cookie(cookie)
78
92
  Session.store[session.session_id] = session
79
93
  else
@@ -84,54 +98,24 @@ class Session < Hash
84
98
  return session
85
99
  end
86
100
 
87
- # Perform session garbage collection. Typically this method
88
- # is called from a cron like mechanism (for example using
89
- # script/runner).
90
-
91
- def garbage_collection!
92
- Session.store.delete_if { |key, s| s.invalid? }
93
- end
94
- alias_method :gc!, :garbage_collection!
95
-
96
- # Helper method that returns all sessions.
97
-
98
- def all
99
- Session.store.values
100
- end
101
-
102
101
  end
102
+
103
+ # By default sessions are stored in memory.
104
+ #--
105
+ # gmosx: should be placed here.
106
+ #++
103
107
 
108
+ set_store_type(:memory)
109
+
104
110
  # The unique id of this session.
105
111
 
106
112
  attr_reader :session_id
107
113
 
108
- # The time this session was created.
109
-
110
- attr_reader :ctime
111
- alias_method :create_time, :ctime
112
-
113
- # The time this session was last modified.
114
-
115
- attr_accessor :mtime
116
- alias_method :modify_time, :mtime
117
-
118
- # The time this session was last accessed.
119
-
120
- attr_accessor :atime
121
- alias_method :access_time, :mtime
122
-
123
114
  # Create the session for the given context.
124
115
 
125
116
  def initialize(context = nil)
126
117
  @session_id = create_id
127
- @ctime = @mtime = @atime = Time.now
128
- end
129
-
130
- # Like the unix touch command, updates
131
- # the atime to now.
132
-
133
- def touch!
134
- @atime = Time.now
118
+ expires_after(Session.keepalive)
135
119
  end
136
120
 
137
121
  # Synchronize the session store, by
@@ -143,18 +127,9 @@ class Session < Hash
143
127
  end
144
128
  alias_method :restore, :sync
145
129
 
146
- # Keep this session alive?
147
-
148
- def valid?
149
- Time.now < @atime + Session.keepalive
150
- end
151
- alias_method :alive?, :valid?
152
-
153
- # The reverse of keepalive?
154
-
155
- def invalid?
156
- Time.now > @atime + Session.keepalive
157
- end
130
+ def touch!
131
+ expires_after(Session.keepalive)
132
+ end
158
133
 
159
134
  protected
160
135
 
@@ -180,3 +155,4 @@ end
180
155
  end
181
156
 
182
157
  # * George Moschovitis <gm@navel.gr>
158
+ # * Guillaume Pierronnet <guillaume.pierronnet@gmail.com>
@@ -53,7 +53,7 @@ rescue OptionParser::InvalidOption
53
53
  exit
54
54
  end
55
55
 
56
- sessions = SyncHash.new
56
+ sessions = Nitro::MemorySessionStore.new
57
57
 
58
58
  if debug
59
59
 
@@ -1,40 +1,59 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'nitro/session'
4
+ require 'fileutils'
5
+ require 'tmpdir'
4
6
 
5
7
  module Nitro
6
- class FileSessionStore
7
- # The session keepalive time. The session is eligable for
8
- # garbage collection after this time passes.
9
8
 
10
- setting :path, :default => "/tmp/nitro_session", :doc => 'The directory to store file session'
9
+ # A Session manager that persists sessions on disk.
10
+ #
11
+ # TODO: safe locking of files, because Nitro can be multiprocess
12
+
13
+ class FileSessionStore
14
+ setting :path, :default => "#{Dir.tmpdir}/nitro_session_#{Session.cookie_name}", :doc => 'The directory to store file session'
11
15
 
12
16
  def initialize
13
17
  @path = FileSessionStore.path
14
- Dir.mkdir(@path) unless File.exists?(@path)
18
+ FileUtils.mkdir_p(@path)
15
19
  end
16
20
 
17
21
  def []=(k,v)
18
- File.open(File.join(@path, k), "w") { |f| f.write(Marshal.dump(v)) }
22
+ fn = File.join(@path, k.to_s)
23
+ encode_file(fn, v)
19
24
  end
20
25
 
21
26
  def [](k)
22
- fn = File.join(@path, k)
27
+ fn = File.join(@path, k.to_s)
23
28
  return nil unless File.exists?(fn)
29
+ decode_file(fn)
30
+ end
31
+
32
+ def gc!
33
+ now = Time.now
34
+ all.each do |fn|
35
+ expire_time = File.stat(fn).atime + Session.keepalive
36
+ File.delete(fn) if now > expire_time
37
+ end
38
+ end
39
+
40
+ def all
41
+ Dir.glob( File.join(@path, '*' ) )
42
+ end
43
+
44
+ private
45
+
46
+ def decode_file(fn)
24
47
  Marshal.load( File.read(fn) )
25
48
  end
26
49
 
27
- # def values
28
- # []
29
- # end
30
- #
31
- # def each
32
- # #yield
33
- # end
50
+ def encode_file(fn, value)
51
+ File.open(fn, "w") { |f| f.write(Marshal.dump(value)) }
52
+ end
34
53
 
35
54
  end
36
55
 
37
56
  Session.store = FileSessionStore.new
38
57
  end
39
58
 
40
- # * Guillaume Pierronnet <guillaume.pierronnet@gmail.com>
59
+ # * Guillaume Pierronnet <guillaume.pierronnet@gmail.com>
@@ -1,10 +1,19 @@
1
- require 'mega/synchash'
1
+ module Nitro
2
2
 
3
- Logger.debug 'Using In-Memory sessions.'
3
+ class MemorySessionStore < SyncHash
4
4
 
5
- module Nitro
5
+ # Perform session garbage collection. Typically this method
6
+ # is called from a cron like mechanism (for example using
7
+ # script/runner).
8
+
9
+ def gc!
10
+ delete_if { |key, s| s.expired? }
11
+ end
12
+
13
+ alias :all :values
14
+ end
6
15
 
7
- Session.store = SyncHash.new
16
+ Session.store = MemorySessionStore.new
8
17
 
9
18
  end
10
19
 
@@ -0,0 +1,56 @@
1
+ require 'og'
2
+ require 'nitro/session'
3
+ require 'base64'
4
+
5
+ module Nitro
6
+
7
+ class OgSession < Session
8
+ prop_accessor :sessionid, String
9
+ prop_accessor :expires, Time
10
+ prop_accessor :content
11
+ end
12
+
13
+ # A Session manager that persists sessions on an Og store.
14
+
15
+ class OgSessionStore
16
+
17
+ def []=(k,v)
18
+ unless s = OgSession.find_by_sessionid(k.to_s)
19
+ s = OgSession.new
20
+ s.sessionid = k.to_s
21
+ end
22
+ #s.content = v.to_yaml
23
+ s.content = encode(v)
24
+ s.save
25
+ end
26
+
27
+ def [](k)
28
+ s = OgSession.find_by_sessionid(k.to_s) and decode(s.content)
29
+ end
30
+
31
+ def gc!
32
+ now = Og.manager.store.quote(Time.now)
33
+ OgSession.find(:condition => "expires < #{now}").each {|s| s.delete }
34
+ end
35
+
36
+ def all
37
+ OgSession.all
38
+ end
39
+
40
+ private
41
+
42
+ def encode(c)
43
+ Base64.encode64(Marshal.dump(c))
44
+ end
45
+
46
+ def decode(c)
47
+ Marshal::load(Base64.decode64(c))
48
+ #s.content = YAML::load(s.content)
49
+ end
50
+
51
+ end
52
+
53
+ Session.store = OgSessionStore.new
54
+ end
55
+
56
+ # * Guillaume Pierronnet <guillaume.pierronnet@gmail.com>
@@ -80,7 +80,10 @@ Autocompleter.Base.prototype = {
80
80
 
81
81
  show: function() {
82
82
  if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
83
- if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0) && (Element.getStyle(this.update, 'position')=='absolute')) {
83
+ if(!this.iefix &&
84
+ (navigator.appVersion.indexOf('MSIE')>0) &&
85
+ (navigator.userAgent.indexOf('Opera')<0) &&
86
+ (Element.getStyle(this.update, 'position')=='absolute')) {
84
87
  new Insertion.After(this.update,
85
88
  '<iframe id="' + this.update.id + '_iefix" '+
86
89
  'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
@@ -718,4 +721,30 @@ Ajax.InPlaceEditor.prototype = {
718
721
  Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
719
722
  }
720
723
  }
724
+ };
725
+
726
+ // Delayed observer, like Form.Element.Observer,
727
+ // but waits for delay after last key input
728
+ // Ideal for live-search fields
729
+
730
+ Form.Element.DelayedObserver = Class.create();
731
+ Form.Element.DelayedObserver.prototype = {
732
+ initialize: function(element, delay, callback) {
733
+ this.delay = delay || 0.5;
734
+ this.element = $(element);
735
+ this.callback = callback;
736
+ this.timer = null;
737
+ this.lastValue = $F(this.element);
738
+ Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
739
+ },
740
+ delayedListener: function(event) {
741
+ if(this.lastValue == $F(this.element)) return;
742
+ if(this.timer) clearTimeout(this.timer);
743
+ this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
744
+ this.lastValue = $F(this.element);
745
+ },
746
+ onTimerEvent: function() {
747
+ this.timer = null;
748
+ this.callback(this.element, $F(this.element));
749
+ }
721
750
  };
@@ -1,7 +1,5 @@
1
1
  // Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
2
2
  //
3
- // Element.Class part Copyright (c) 2005 by Rick Olson
4
- //
5
3
  // See scriptaculous.js for full license.
6
4
 
7
5
  /*--------------------------------------------------------------------------*/
@@ -10,7 +8,7 @@ var Droppables = {
10
8
  drops: [],
11
9
 
12
10
  remove: function(element) {
13
- this.drops = this.drops.reject(function(d) { return d.element==element });
11
+ this.drops = this.drops.reject(function(d) { return d.element==$(element) });
14
12
  },
15
13
 
16
14
  add: function(element) {
@@ -31,6 +29,8 @@ var Droppables = {
31
29
  options._containers.push($(containment));
32
30
  }
33
31
  }
32
+
33
+ if(options.accept) options.accept = [options.accept].flatten();
34
34
 
35
35
  Element.makePositioned(element); // fix IE
36
36
  options.element = element;
@@ -43,55 +43,50 @@ var Droppables = {
43
43
  return drop._containers.detect(function(c) { return parentNode == c });
44
44
  },
45
45
 
46
- isAffected: function(pX, pY, element, drop) {
46
+ isAffected: function(point, element, drop) {
47
47
  return (
48
48
  (drop.element!=element) &&
49
49
  ((!drop._containers) ||
50
50
  this.isContained(element, drop)) &&
51
51
  ((!drop.accept) ||
52
- (Element.Class.has_any(element, drop.accept))) &&
53
- Position.within(drop.element, pX, pY) );
52
+ (Element.classNames(element).detect(
53
+ function(v) { return drop.accept.include(v) } ) )) &&
54
+ Position.within(drop.element, point[0], point[1]) );
54
55
  },
55
56
 
56
57
  deactivate: function(drop) {
57
58
  if(drop.hoverclass)
58
- Element.Class.remove(drop.element, drop.hoverclass);
59
+ Element.removeClassName(drop.element, drop.hoverclass);
59
60
  this.last_active = null;
60
61
  },
61
62
 
62
63
  activate: function(drop) {
63
- if(this.last_active) this.deactivate(this.last_active);
64
64
  if(drop.hoverclass)
65
- Element.Class.add(drop.element, drop.hoverclass);
65
+ Element.addClassName(drop.element, drop.hoverclass);
66
66
  this.last_active = drop;
67
67
  },
68
68
 
69
- show: function(event, element) {
69
+ show: function(point, element) {
70
70
  if(!this.drops.length) return;
71
- var pX = Event.pointerX(event);
72
- var pY = Event.pointerY(event);
73
- Position.prepare();
74
-
75
- var i = this.drops.length-1; do {
76
- var drop = this.drops[i];
77
- if(this.isAffected(pX, pY, element, drop)) {
71
+
72
+ if(this.last_active) this.deactivate(this.last_active);
73
+ this.drops.each( function(drop) {
74
+ if(Droppables.isAffected(point, element, drop)) {
78
75
  if(drop.onHover)
79
76
  drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
80
77
  if(drop.greedy) {
81
- this.activate(drop);
82
- return;
78
+ Droppables.activate(drop);
79
+ throw $break;
83
80
  }
84
81
  }
85
- } while (i--);
86
-
87
- if(this.last_active) this.deactivate(this.last_active);
82
+ });
88
83
  },
89
84
 
90
85
  fire: function(event, element) {
91
86
  if(!this.last_active) return;
92
87
  Position.prepare();
93
88
 
94
- if (this.isAffected(Event.pointerX(event), Event.pointerY(event), element, this.last_active))
89
+ if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
95
90
  if (this.last_active.onDrop)
96
91
  this.last_active.onDrop(element, this.last_active.element, event);
97
92
  },
@@ -103,15 +98,84 @@ var Droppables = {
103
98
  }
104
99
 
105
100
  var Draggables = {
101
+ drags: [],
106
102
  observers: [],
103
+
104
+ register: function(draggable) {
105
+ if(this.drags.length == 0) {
106
+ this.eventMouseUp = this.endDrag.bindAsEventListener(this);
107
+ this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
108
+ this.eventKeypress = this.keyPress.bindAsEventListener(this);
109
+
110
+ Event.observe(document, "mouseup", this.eventMouseUp);
111
+ Event.observe(document, "mousemove", this.eventMouseMove);
112
+ Event.observe(document, "keypress", this.eventKeypress);
113
+ }
114
+ this.drags.push(draggable);
115
+ },
116
+
117
+ unregister: function(draggable) {
118
+ this.drags = this.drags.reject(function(d) { return d==draggable });
119
+ if(this.drags.length == 0) {
120
+ Event.stopObserving(document, "mouseup", this.eventMouseUp);
121
+ Event.stopObserving(document, "mousemove", this.eventMouseMove);
122
+ Event.stopObserving(document, "keypress", this.eventKeypress);
123
+ }
124
+ },
125
+
126
+ activate: function(draggable) {
127
+ window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
128
+ this.activeDraggable = draggable;
129
+ },
130
+
131
+ deactivate: function(draggbale) {
132
+ this.activeDraggable = null;
133
+ },
134
+
135
+ updateDrag: function(event) {
136
+ if(!this.activeDraggable) return;
137
+ var pointer = [Event.pointerX(event), Event.pointerY(event)];
138
+ // Mozilla-based browsers fire successive mousemove events with
139
+ // the same coordinates, prevent needless redrawing (moz bug?)
140
+ if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
141
+ this._lastPointer = pointer;
142
+ this.activeDraggable.updateDrag(event, pointer);
143
+ },
144
+
145
+ endDrag: function(event) {
146
+ if(!this.activeDraggable) return;
147
+ this._lastPointer = null;
148
+ this.activeDraggable.endDrag(event);
149
+ },
150
+
151
+ keyPress: function(event) {
152
+ if(this.activeDraggable)
153
+ this.activeDraggable.keyPress(event);
154
+ },
155
+
107
156
  addObserver: function(observer) {
108
- this.observers.push(observer);
157
+ this.observers.push(observer);
158
+ this._cacheObserverCallbacks();
109
159
  },
110
- removeObserver: function(element) { // element instead of obsever fixes mem leaks
160
+
161
+ removeObserver: function(element) { // element instead of observer fixes mem leaks
111
162
  this.observers = this.observers.reject( function(o) { return o.element==element });
163
+ this._cacheObserverCallbacks();
112
164
  },
113
- notify: function(eventName, draggable) { // 'onStart', 'onEnd'
114
- this.observers.invoke(eventName, draggable);
165
+
166
+ notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag'
167
+ if(this[eventName+'Count'] > 0)
168
+ this.observers.each( function(o) {
169
+ if(o[eventName]) o[eventName](eventName, draggable, event);
170
+ });
171
+ },
172
+
173
+ _cacheObserverCallbacks: function() {
174
+ ['onStart','onEnd','onDrag'].each( function(eventName) {
175
+ Draggables[eventName+'Count'] = Draggables.observers.select(
176
+ function(o) { return o[eventName]; }
177
+ ).length;
178
+ });
115
179
  }
116
180
  }
117
181
 
@@ -127,68 +191,48 @@ Draggable.prototype = {
127
191
  },
128
192
  reverteffect: function(element, top_offset, left_offset) {
129
193
  var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
130
- new Effect.MoveBy(element, -top_offset, -left_offset, {duration:dur});
194
+ element._revert = new Effect.MoveBy(element, -top_offset, -left_offset, {duration:dur});
131
195
  },
132
196
  endeffect: function(element) {
133
- new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0});
197
+ new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0});
134
198
  },
135
199
  zindex: 1000,
136
- revert: false
200
+ revert: false,
201
+ snap: false // false, or xy or [x,y] or function(x,y){ return [x,y] }
137
202
  }, arguments[1] || {});
138
203
 
139
- this.element = $(element);
204
+ this.element = $(element);
205
+
140
206
  if(options.handle && (typeof options.handle == 'string'))
141
- this.handle = Element.Class.childrenWith(this.element, options.handle)[0];
142
-
207
+ this.handle = Element.childrenWithClassName(this.element, options.handle)[0];
143
208
  if(!this.handle) this.handle = $(options.handle);
144
209
  if(!this.handle) this.handle = this.element;
145
210
 
146
211
  Element.makePositioned(this.element); // fix IE
147
212
 
148
- this.offsetX = 0;
149
- this.offsetY = 0;
150
- this.originalLeft = this.currentLeft();
151
- this.originalTop = this.currentTop();
152
- this.originalX = this.element.offsetLeft;
153
- this.originalY = this.element.offsetTop;
154
-
155
- this.options = options;
213
+ this.delta = this.currentDelta();
214
+ this.options = options;
215
+ this.dragging = false;
156
216
 
157
- this.active = false;
158
- this.dragging = false;
159
-
160
- this.eventMouseDown = this.startDrag.bindAsEventListener(this);
161
- this.eventMouseUp = this.endDrag.bindAsEventListener(this);
162
- this.eventMouseMove = this.update.bindAsEventListener(this);
163
- this.eventKeypress = this.keyPress.bindAsEventListener(this);
217
+ this.eventMouseDown = this.initDrag.bindAsEventListener(this);
218
+ Event.observe(this.handle, "mousedown", this.eventMouseDown);
164
219
 
165
- this.registerEvents();
220
+ Draggables.register(this);
166
221
  },
222
+
167
223
  destroy: function() {
168
224
  Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
169
- this.unregisterEvents();
225
+ Draggables.unregister(this);
170
226
  },
171
- registerEvents: function() {
172
- Event.observe(document, "mouseup", this.eventMouseUp);
173
- Event.observe(document, "mousemove", this.eventMouseMove);
174
- Event.observe(document, "keypress", this.eventKeypress);
175
- Event.observe(this.handle, "mousedown", this.eventMouseDown);
176
- },
177
- unregisterEvents: function() {
178
- //if(!this.active) return;
179
- //Event.stopObserving(document, "mouseup", this.eventMouseUp);
180
- //Event.stopObserving(document, "mousemove", this.eventMouseMove);
181
- //Event.stopObserving(document, "keypress", this.eventKeypress);
182
- },
183
- currentLeft: function() {
184
- return parseInt(this.element.style.left || '0');
227
+
228
+ currentDelta: function() {
229
+ return([
230
+ parseInt(this.element.style.left || '0'),
231
+ parseInt(this.element.style.top || '0')]);
185
232
  },
186
- currentTop: function() {
187
- return parseInt(this.element.style.top || '0')
188
- },
189
- startDrag: function(event) {
190
- if(Event.isLeftClick(event)) {
191
-
233
+
234
+ initDrag: function(event) {
235
+ if(Event.isLeftClick(event)) {
192
236
  // abort on form elements, fixes a Firefox issue
193
237
  var src = Event.element(event);
194
238
  if(src.tagName && (
@@ -196,20 +240,53 @@ Draggable.prototype = {
196
240
  src.tagName=='SELECT' ||
197
241
  src.tagName=='BUTTON' ||
198
242
  src.tagName=='TEXTAREA')) return;
243
+
244
+ if(this.element._revert) {
245
+ this.element._revert.cancel();
246
+ this.element._revert = null;
247
+ }
199
248
 
200
- // this.registerEvents();
201
- this.active = true;
202
249
  var pointer = [Event.pointerX(event), Event.pointerY(event)];
203
- var offsets = Position.cumulativeOffset(this.element);
204
- this.offsetX = (pointer[0] - offsets[0]);
205
- this.offsetY = (pointer[1] - offsets[1]);
250
+ var pos = Position.cumulativeOffset(this.element);
251
+ this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
252
+
253
+ Draggables.activate(this);
206
254
  Event.stop(event);
207
255
  }
208
256
  },
257
+
258
+ startDrag: function(event) {
259
+ this.dragging = true;
260
+
261
+ if(this.options.zindex) {
262
+ this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
263
+ this.element.style.zIndex = this.options.zindex;
264
+ }
265
+
266
+ if(this.options.ghosting) {
267
+ this._clone = this.element.cloneNode(true);
268
+ Position.absolutize(this.element);
269
+ this.element.parentNode.insertBefore(this._clone, this.element);
270
+ }
271
+
272
+ Draggables.notify('onStart', this, event);
273
+ if(this.options.starteffect) this.options.starteffect(this.element);
274
+ },
275
+
276
+ updateDrag: function(event, pointer) {
277
+ if(!this.dragging) this.startDrag(event);
278
+ Position.prepare();
279
+ Droppables.show(pointer, this.element);
280
+ Draggables.notify('onDrag', this, event);
281
+ this.draw(pointer);
282
+ if(this.options.change) this.options.change(this);
283
+
284
+ // fix AppleWebKit rendering
285
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
286
+ Event.stop(event);
287
+ },
288
+
209
289
  finishDrag: function(event, success) {
210
- // this.unregisterEvents();
211
-
212
- this.active = false;
213
290
  this.dragging = false;
214
291
 
215
292
  if(this.options.ghosting) {
@@ -219,18 +296,17 @@ Draggable.prototype = {
219
296
  }
220
297
 
221
298
  if(success) Droppables.fire(event, this.element);
222
- Draggables.notify('onEnd', this);
299
+ Draggables.notify('onEnd', this, event);
223
300
 
224
301
  var revert = this.options.revert;
225
302
  if(revert && typeof revert == 'function') revert = revert(this.element);
226
-
303
+
304
+ var d = this.currentDelta();
227
305
  if(revert && this.options.reverteffect) {
228
306
  this.options.reverteffect(this.element,
229
- this.currentTop()-this.originalTop,
230
- this.currentLeft()-this.originalLeft);
307
+ d[1]-this.delta[1], d[0]-this.delta[0]);
231
308
  } else {
232
- this.originalLeft = this.currentLeft();
233
- this.originalTop = this.currentTop();
309
+ this.delta = d;
234
310
  }
235
311
 
236
312
  if(this.options.zindex)
@@ -239,70 +315,48 @@ Draggable.prototype = {
239
315
  if(this.options.endeffect)
240
316
  this.options.endeffect(this.element);
241
317
 
242
-
318
+ Draggables.deactivate(this);
243
319
  Droppables.reset();
244
320
  },
321
+
245
322
  keyPress: function(event) {
246
- if(this.active) {
247
- if(event.keyCode==Event.KEY_ESC) {
248
- this.finishDrag(event, false);
249
- Event.stop(event);
250
- }
251
- }
323
+ if(!event.keyCode==Event.KEY_ESC) return;
324
+ this.finishDrag(event, false);
325
+ Event.stop(event);
252
326
  },
327
+
253
328
  endDrag: function(event) {
254
- if(this.active && this.dragging) {
255
- this.finishDrag(event, true);
256
- Event.stop(event);
257
- }
258
- this.active = false;
259
- this.dragging = false;
329
+ if(!this.dragging) return;
330
+ this.finishDrag(event, true);
331
+ Event.stop(event);
260
332
  },
261
- draw: function(event) {
262
- var pointer = [Event.pointerX(event), Event.pointerY(event)];
263
- var offsets = Position.cumulativeOffset(this.element);
264
- offsets[0] -= this.currentLeft();
265
- offsets[1] -= this.currentTop();
333
+
334
+ draw: function(point) {
335
+ var pos = Position.cumulativeOffset(this.element);
336
+ var d = this.currentDelta();
337
+ pos[0] -= d[0]; pos[1] -= d[1];
338
+
339
+ var p = [0,1].map(function(i){ return (point[i]-pos[i]-this.offset[i]) }.bind(this));
340
+
341
+ if(this.options.snap) {
342
+ if(typeof this.options.snap == 'function') {
343
+ p = this.options.snap(p[0],p[1]);
344
+ } else {
345
+ if(this.options.snap instanceof Array) {
346
+ p = p.map( function(v, i) {
347
+ return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this))
348
+ } else {
349
+ p = p.map( function(v) {
350
+ return Math.round(v/this.options.snap)*this.options.snap }.bind(this))
351
+ }
352
+ }}
353
+
266
354
  var style = this.element.style;
267
355
  if((!this.options.constraint) || (this.options.constraint=='horizontal'))
268
- style.left = (pointer[0] - offsets[0] - this.offsetX) + "px";
356
+ style.left = p[0] + "px";
269
357
  if((!this.options.constraint) || (this.options.constraint=='vertical'))
270
- style.top = (pointer[1] - offsets[1] - this.offsetY) + "px";
358
+ style.top = p[1] + "px";
271
359
  if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
272
- },
273
- update: function(event) {
274
- if(this.active) {
275
- if(!this.dragging) {
276
- var style = this.element.style;
277
- this.dragging = true;
278
-
279
- if(Element.getStyle(this.element,'position')=='')
280
- style.position = "relative";
281
-
282
- if(this.options.zindex) {
283
- this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
284
- style.zIndex = this.options.zindex;
285
- }
286
-
287
- if(this.options.ghosting) {
288
- this._clone = this.element.cloneNode(true);
289
- Position.absolutize(this.element);
290
- this.element.parentNode.insertBefore(this._clone, this.element);
291
- }
292
-
293
- Draggables.notify('onStart', this);
294
- if(this.options.starteffect) this.options.starteffect(this.element);
295
- }
296
-
297
- Droppables.show(event, this.element);
298
- this.draw(event);
299
- if(this.options.change) this.options.change(this);
300
-
301
- // fix AppleWebKit rendering
302
- if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
303
-
304
- Event.stop(event);
305
- }
306
360
  }
307
361
  }
308
362
 
@@ -315,9 +369,11 @@ SortableObserver.prototype = {
315
369
  this.observer = observer;
316
370
  this.lastValue = Sortable.serialize(this.element);
317
371
  },
372
+
318
373
  onStart: function() {
319
374
  this.lastValue = Sortable.serialize(this.element);
320
375
  },
376
+
321
377
  onEnd: function() {
322
378
  Sortable.unmark();
323
379
  if(this.lastValue != Sortable.serialize(this.element))
@@ -327,10 +383,12 @@ SortableObserver.prototype = {
327
383
 
328
384
  var Sortable = {
329
385
  sortables: new Array(),
386
+
330
387
  options: function(element){
331
388
  element = $(element);
332
389
  return this.sortables.detect(function(s) { return s.element == element });
333
390
  },
391
+
334
392
  destroy: function(element){
335
393
  element = $(element);
336
394
  this.sortables.findAll(function(s) { return s.element == element }).each(function(s){
@@ -340,6 +398,7 @@ var Sortable = {
340
398
  });
341
399
  this.sortables = this.sortables.reject(function(s) { return s.element == element });
342
400
  },
401
+
343
402
  create: function(element) {
344
403
  element = $(element);
345
404
  var options = Object.extend({
@@ -413,7 +472,7 @@ var Sortable = {
413
472
  (this.findElements(element, options) || []).each( function(e) {
414
473
  // handles are per-draggable
415
474
  var handle = options.handle ?
416
- Element.Class.childrenWith(e, options.handle)[0] : e;
475
+ Element.childrenWithClassName(e, options.handle)[0] : e;
417
476
  options.draggables.push(
418
477
  new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
419
478
  Droppables.add(e, options_for_droppable);
@@ -433,8 +492,8 @@ var Sortable = {
433
492
  if(!element.hasChildNodes()) return null;
434
493
  var elements = [];
435
494
  $A(element.childNodes).each( function(e) {
436
- if(e.tagName && e.tagName==options.tag.toUpperCase() &&
437
- (!options.only || (Element.Class.has(e, options.only))))
495
+ if(e.tagName && e.tagName.toUpperCase()==options.tag.toUpperCase() &&
496
+ (!options.only || (Element.hasClassName(e, options.only))))
438
497
  elements.push(e);
439
498
  if(options.tree) {
440
499
  var grandchildren = this.findElements(e, options);
@@ -491,14 +550,20 @@ var Sortable = {
491
550
  if(!Sortable._marker) {
492
551
  Sortable._marker = $('dropmarker') || document.createElement('DIV');
493
552
  Element.hide(Sortable._marker);
494
- Element.Class.add(Sortable._marker, 'dropmarker');
553
+ Element.addClassName(Sortable._marker, 'dropmarker');
495
554
  Sortable._marker.style.position = 'absolute';
496
555
  document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
497
556
  }
498
557
  var offsets = Position.cumulativeOffset(dropon);
499
- Sortable._marker.style.top = offsets[1] + 'px';
500
- if(position=='after') Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px';
501
558
  Sortable._marker.style.left = offsets[0] + 'px';
559
+ Sortable._marker.style.top = offsets[1] + 'px';
560
+
561
+ if(position=='after')
562
+ if(sortable.overlap == 'horizontal')
563
+ Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px';
564
+ else
565
+ Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px';
566
+
502
567
  Element.show(Sortable._marker);
503
568
  },
504
569
 
@@ -511,9 +576,9 @@ var Sortable = {
511
576
  name: element.id,
512
577
  format: sortableOptions.format || /^[^_]*_(.*)$/
513
578
  }, arguments[1] || {});
514
- return $(this.findElements(element, options) || []).collect( function(item) {
579
+ return $(this.findElements(element, options) || []).map( function(item) {
515
580
  return (encodeURIComponent(options.name) + "[]=" +
516
581
  encodeURIComponent(item.id.match(options.format) ? item.id.match(options.format)[1] : ''));
517
582
  }).join("&");
518
583
  }
519
- }
584
+ }