nitro 0.25.0 → 0.26.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ }