actionpack 1.9.0 → 1.9.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionpack might be problematic. Click here for more details.

@@ -1,4 +1,5 @@
1
1
  // Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
2
+ // (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
2
3
  //
3
4
  // Permission is hereby granted, free of charge, to any person obtaining
4
5
  // a copy of this software and associated documentation files (the
@@ -19,7 +20,6 @@
19
20
  // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
21
  // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22
 
22
-
23
23
  Element.collectTextNodesIgnoreClass = function(element, ignoreclass) {
24
24
  var children = $(element).childNodes;
25
25
  var text = "";
@@ -37,42 +37,70 @@ Element.collectTextNodesIgnoreClass = function(element, ignoreclass) {
37
37
  return text;
38
38
  }
39
39
 
40
- Ajax.Autocompleter = Class.create();
41
- Ajax.Autocompleter.prototype = (new Ajax.Base()).extend({
42
- initialize: function(element, update, url, options) {
40
+ // Autocompleter.Base handles all the autocompletion functionality
41
+ // that's independent of the data source for autocompletion. This
42
+ // includes drawing the autocompletion menu, observing keyboard
43
+ // and mouse events, and similar.
44
+ //
45
+ // Specific autocompleters need to provide, at the very least,
46
+ // a getUpdatedChoices function that will be invoked every time
47
+ // the text inside the monitored textbox changes. This method
48
+ // should get the text for which to provide autocompletion by
49
+ // invoking this.getEntry(), NOT by directly accessing
50
+ // this.element.value. This is to allow incremental tokenized
51
+ // autocompletion. Specific auto-completion logic (AJAX, etc)
52
+ // belongs in getUpdatedChoices.
53
+ //
54
+ // Tokenized incremental autocompletion is enabled automatically
55
+ // when an autocompleter is instantiated with the 'tokens' option
56
+ // in the options parameter, e.g.:
57
+ // new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
58
+ // will incrementally autocomplete with a comma as the token.
59
+ // Additionally, ',' in the above example can be replaced with
60
+ // a token array, e.g. { tokens: new Array (',', '\n') } which
61
+ // enables autocompletion on multiple tokens. This is most
62
+ // useful when one of the tokens is \n (a newline), as it
63
+ // allows smart autocompletion after linebreaks.
64
+
65
+ var Autocompleter = {}
66
+ Autocompleter.Base = function() {};
67
+ Autocompleter.Base.prototype = {
68
+ base_initialize: function(element, update, options) {
43
69
  this.element = $(element);
44
70
  this.update = $(update);
45
71
  this.has_focus = false;
46
72
  this.changed = false;
47
73
  this.active = false;
48
74
  this.index = 0;
49
- this.entry_count = 0;
50
- this.url = url;
75
+ this.entry_count = 0;
51
76
 
52
- this.setOptions(options);
53
- this.options.asynchronous = true;
54
- this.options.onComplete = this.onComplete.bind(this)
77
+ if (this.setOptions)
78
+ this.setOptions(options);
79
+ else
80
+ this.options = {}
81
+
82
+ this.options.tokens = this.options.tokens || new Array();
55
83
  this.options.frequency = this.options.frequency || 0.4;
56
84
  this.options.min_chars = this.options.min_chars || 1;
57
- this.options.method = 'post';
58
-
59
- this.options.onShow = this.options.onShow ||
60
- function(element, update){
61
- if(!update.style.position || update.style.position=='absolute') {
62
- update.style.position = 'absolute';
85
+ this.options.onShow = this.options.onShow ||
86
+ function(element, update){
87
+ if(!update.style.position || update.style.position=='absolute') {
88
+ update.style.position = 'absolute';
63
89
  var offsets = Position.cumulativeOffset(element);
64
90
  update.style.left = offsets[0] + 'px';
65
91
  update.style.top = (offsets[1] + element.offsetHeight) + 'px';
66
92
  update.style.width = element.offsetWidth + 'px';
67
- }
68
- new Effect.Appear(update,{duration:0.3});
69
- };
93
+ }
94
+ new Effect.Appear(update,{duration:0.15});
95
+ };
70
96
  this.options.onHide = this.options.onHide ||
71
- function(element, update){ new Effect.Fade(update,{duration:0.3}) };
72
-
97
+ function(element, update){ new Effect.Fade(update,{duration:0.15}) };
73
98
 
74
99
  if(this.options.indicator)
75
100
  this.indicator = $(this.options.indicator);
101
+
102
+ if (typeof(this.options.tokens) == 'string')
103
+ this.options.tokens = new Array(this.options.tokens);
76
104
 
77
105
  this.observer = null;
78
106
 
@@ -81,14 +109,14 @@ Ajax.Autocompleter.prototype = (new Ajax.Base()).extend({
81
109
  Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
82
110
  Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
83
111
  },
84
-
112
+
85
113
  show: function() {
86
114
  if(this.update.style.display=='none') this.options.onShow(this.element, this.update);
87
115
  if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0) && this.update.style.position=='absolute') {
88
116
  new Insertion.After(this.update,
89
117
  '<iframe id="' + this.update.id + '_iefix" '+
90
- 'style="display:none;filter:progid:DXImageTransform.Microsoft.Alpha(apacity=0);" ' +
91
- 'src="javascript:;" frameborder="0" scrolling="no"></iframe>');
118
+ 'style="display:none;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
119
+ 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
92
120
  this.iefix = $(this.update.id+'_iefix');
93
121
  }
94
122
  if(this.iefix) {
@@ -111,51 +139,7 @@ Ajax.Autocompleter.prototype = (new Ajax.Base()).extend({
111
139
  stopIndicator: function() {
112
140
  if(this.indicator) Element.hide(this.indicator);
113
141
  },
114
-
115
- onObserverEvent: function() {
116
- this.changed = false;
117
- if(this.element.value.length>=this.options.min_chars) {
118
- this.startIndicator();
119
- this.options.parameters = this.options.callback ?
120
- this.options.callback(this.element, Form.Element.getValue(this.element)) :
121
- Form.Element.serialize(this.element);
122
- new Ajax.Request(this.url, this.options);
123
- } else {
124
- this.active = false;
125
- this.hide();
126
- }
127
- },
128
-
129
- addObservers: function(element) {
130
- Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
131
- Event.observe(element, "click", this.onClick.bindAsEventListener(this));
132
- },
133
-
134
- onComplete: function(request) {
135
- if(!this.changed && this.has_focus) {
136
- this.update.innerHTML = request.responseText;
137
- Element.cleanWhitespace(this.update);
138
- Element.cleanWhitespace(this.update.firstChild);
139
142
 
140
- if(this.update.firstChild && this.update.firstChild.childNodes) {
141
- this.entry_count =
142
- this.update.firstChild.childNodes.length;
143
- for (var i = 0; i < this.entry_count; i++) {
144
- entry = this.get_entry(i);
145
- entry.autocompleteIndex = i;
146
- this.addObservers(entry);
147
- }
148
- } else {
149
- this.entry_count = 0;
150
- }
151
-
152
- this.stopIndicator();
153
-
154
- this.index = 0;
155
- this.render();
156
- }
157
- },
158
-
159
143
  onKeyPress: function(event) {
160
144
  if(this.active)
161
145
  switch(event.keyCode) {
@@ -255,7 +239,208 @@ Ajax.Autocompleter.prototype = (new Ajax.Base()).extend({
255
239
  select_entry: function() {
256
240
  this.active = false;
257
241
  value = Element.collectTextNodesIgnoreClass(this.get_current_entry(), 'informal').unescapeHTML();
258
- this.element.value = value;
242
+ this.updateElement(value);
259
243
  this.element.focus();
244
+ },
245
+
246
+ updateElement: function(value) {
247
+ var last_token_pos = this.findLastToken();
248
+ if (last_token_pos != -1) {
249
+ var new_value = this.element.value.substr(0, last_token_pos + 1);
250
+ var whitespace = this.element.value.substr(last_token_pos + 1).match(/^\s+/);
251
+ if (whitespace)
252
+ new_value += whitespace[0];
253
+ this.element.value = new_value + value;
254
+ } else {
255
+ this.element.value = value;
256
+ }
257
+ },
258
+
259
+ updateChoices: function(choices) {
260
+ if(!this.changed && this.has_focus) {
261
+ this.update.innerHTML = choices;
262
+ Element.cleanWhitespace(this.update);
263
+ Element.cleanWhitespace(this.update.firstChild);
264
+
265
+ if(this.update.firstChild && this.update.firstChild.childNodes) {
266
+ this.entry_count =
267
+ this.update.firstChild.childNodes.length;
268
+ for (var i = 0; i < this.entry_count; i++) {
269
+ entry = this.get_entry(i);
270
+ entry.autocompleteIndex = i;
271
+ this.addObservers(entry);
272
+ }
273
+ } else {
274
+ this.entry_count = 0;
275
+ }
276
+
277
+ this.stopIndicator();
278
+
279
+ this.index = 0;
280
+ this.render();
281
+ }
282
+ },
283
+
284
+ addObservers: function(element) {
285
+ Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
286
+ Event.observe(element, "click", this.onClick.bindAsEventListener(this));
287
+ },
288
+
289
+ onObserverEvent: function() {
290
+ this.changed = false;
291
+ if(this.getEntry().length>=this.options.min_chars) {
292
+ this.startIndicator();
293
+ this.getUpdatedChoices();
294
+ } else {
295
+ this.active = false;
296
+ this.hide();
297
+ }
298
+ },
299
+
300
+ getEntry: function() {
301
+ var token_pos = this.findLastToken();
302
+ if (token_pos != -1)
303
+ var ret = this.element.value.substr(token_pos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
304
+ else
305
+ var ret = this.element.value;
306
+
307
+ return /\n/.test(ret) ? '' : ret;
308
+ },
309
+
310
+ findLastToken: function() {
311
+ var last_token_pos = -1;
312
+
313
+ for (var i=0; i<this.options.tokens.length; i++) {
314
+ var this_token_pos = this.element.value.lastIndexOf(this.options.tokens[i]);
315
+ if (this_token_pos > last_token_pos)
316
+ last_token_pos = this_token_pos;
317
+ }
318
+ return last_token_pos;
319
+ }
320
+ }
321
+
322
+ Ajax.Autocompleter = Class.create();
323
+ Ajax.Autocompleter.prototype = Object.extend(new Autocompleter.Base(),
324
+ Object.extend(new Ajax.Base(), {
325
+ initialize: function(element, update, url, options) {
326
+ this.base_initialize(element, update, options);
327
+ this.options.asynchronous = true;
328
+ this.options.onComplete = this.onComplete.bind(this)
329
+ this.options.method = 'post';
330
+ this.options.defaultParams = this.options.parameters || null;
331
+ this.url = url;
332
+ },
333
+
334
+ getUpdatedChoices: function() {
335
+ entry = encodeURIComponent(this.element.name) + '=' +
336
+ encodeURIComponent(this.getEntry());
337
+
338
+ this.options.parameters = this.options.callback ?
339
+ this.options.callback(this.element, entry) : entry;
340
+
341
+ if(this.options.defaultParams)
342
+ this.options.parameters += '&' + this.options.defaultParams;
343
+
344
+ new Ajax.Request(this.url, this.options);
345
+ },
346
+
347
+ onComplete: function(request) {
348
+ this.updateChoices(request.responseText);
349
+ }
350
+
351
+ }));
352
+
353
+ // The local array autocompleter. Used when you'd prefer to
354
+ // inject an array of autocompletion options into the page, rather
355
+ // than sending out Ajax queries, which can be quite slow sometimes.
356
+ //
357
+ // The constructor takes four parameters. The first two are, as usual,
358
+ // the id of the monitored textbox, and id of the autocompletion menu.
359
+ // The third is the array you want to autocomplete from, and the fourth
360
+ // is the options block.
361
+ //
362
+ // Extra local autocompletion options:
363
+ // - choices - How many autocompletion choices to offer
364
+ //
365
+ // - partial_search - If false, the autocompleter will match entered
366
+ // text only at the beginning of strings in the
367
+ // autocomplete array. Defaults to true, which will
368
+ // match text at the beginning of any *word* in the
369
+ // strings in the autocomplete array. If you want to
370
+ // search anywhere in the string, additionally set
371
+ // the option full_search to true (default: off).
372
+ //
373
+ // - full_search - Search anywhere in autocomplete array strings.
374
+ //
375
+ // - partial_chars - How many characters to enter before triggering
376
+ // a partial match (unlike min_chars, which defines
377
+ // how many characters are required to do any match
378
+ // at all). Defaults to 2.
379
+ //
380
+ // - ignore_case - Whether to ignore case when autocompleting.
381
+ // Defaults to true.
382
+ //
383
+ // It's possible to pass in a custom function as the 'selector'
384
+ // option, if you prefer to write your own autocompletion logic.
385
+ // In that case, the other options above will not apply unless
386
+ // you support them.
387
+
388
+ Autocompleter.Local = Class.create();
389
+ Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
390
+ initialize: function(element, update, array, options) {
391
+ this.base_initialize(element, update, options);
392
+ this.options.array = array;
393
+ },
394
+
395
+ getUpdatedChoices: function() {
396
+ this.updateChoices(this.options.selector(this));
397
+ },
398
+
399
+ setOptions: function(options) {
400
+ this.options = Object.extend({
401
+ choices: 10,
402
+ partial_search: true,
403
+ partial_chars: 2,
404
+ ignore_case: true,
405
+ full_search: false,
406
+ selector: function(instance) {
407
+ var ret = new Array(); // Beginning matches
408
+ var partial = new Array(); // Inside matches
409
+ var entry = instance.getEntry();
410
+ var count = 0;
411
+
412
+ for (var i = 0; i < instance.options.array.length &&
413
+ ret.length < instance.options.choices ; i++) {
414
+ var elem = instance.options.array[i];
415
+ var found_pos = instance.options.ignore_case ?
416
+ elem.toLowerCase().indexOf(entry.toLowerCase()) :
417
+ elem.indexOf(entry);
418
+
419
+ while (found_pos != -1) {
420
+ if (found_pos == 0 && elem.length != entry.length) {
421
+ ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
422
+ elem.substr(entry.length) + "</li>");
423
+ break;
424
+ } else if (entry.length >= instance.options.partial_chars &&
425
+ instance.options.partial_search && found_pos != -1) {
426
+ if (instance.options.full_search || /\s/.test(elem.substr(found_pos-1,1))) {
427
+ partial.push("<li>" + elem.substr(0, found_pos) + "<strong>" +
428
+ elem.substr(found_pos, entry.length) + "</strong>" + elem.substr(
429
+ found_pos + entry.length) + "</li>");
430
+ break;
431
+ }
432
+ }
433
+
434
+ found_pos = instance.options.ignore_case ?
435
+ elem.toLowerCase().indexOf(entry.toLowerCase(), found_pos + 1) :
436
+ elem.indexOf(entry, found_pos + 1);
437
+
438
+ }
439
+ }
440
+ if (partial.length)
441
+ ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
442
+ return "<ul>" + ret.join('') + "</ul>";
443
+ }
444
+ }, options || {});
260
445
  }
261
- });
446
+ });
@@ -112,12 +112,18 @@ Element.Class = {
112
112
  var Droppables = {
113
113
  drops: false,
114
114
 
115
+ remove: function(element) {
116
+ for(var i = 0; i < this.drops.length; i++)
117
+ if(this.drops[i].element == element)
118
+ this.drops.splice(i,1);
119
+ },
120
+
115
121
  add: function(element) {
116
122
  var element = $(element);
117
- var options = {
123
+ var options = Object.extend({
118
124
  greedy: true,
119
125
  hoverclass: null
120
- }.extend(arguments[1] || {});
126
+ }, arguments[1] || {});
121
127
 
122
128
  // cache containers
123
129
  if(options.containment) {
@@ -134,43 +140,42 @@ var Droppables = {
134
140
  options._containers.length-1;
135
141
  }
136
142
 
137
- if(element.style.position=='') //fix IE
138
- element.style.position = 'relative';
143
+ Element.makePositioned(element); // fix IE
139
144
 
140
- // activate the droppable
141
- element.droppable = options;
145
+ options.element = element;
142
146
 
147
+ // activate the droppable
143
148
  if(!this.drops) this.drops = [];
144
- this.drops.push(element);
149
+ this.drops.push(options);
145
150
  },
146
151
 
147
152
  is_contained: function(element, drop) {
148
- var containers = drop.droppable._containers;
153
+ var containers = drop._containers;
149
154
  var parentNode = element.parentNode;
150
- var i = drop.droppable._containers_length;
155
+ var i = drop._containers_length;
151
156
  do { if(parentNode==containers[i]) return true; } while (i--);
152
157
  return false;
153
158
  },
154
159
 
155
160
  is_affected: function(pX, pY, element, drop) {
156
161
  return (
157
- (drop!=element) &&
158
- ((!drop.droppable._containers) ||
162
+ (drop.element!=element) &&
163
+ ((!drop._containers) ||
159
164
  this.is_contained(element, drop)) &&
160
- ((!drop.droppable.accept) ||
161
- (Element.Class.has_any(element, drop.droppable.accept))) &&
162
- Position.within(drop, pX, pY) );
165
+ ((!drop.accept) ||
166
+ (Element.Class.has_any(element, drop.accept))) &&
167
+ Position.within(drop.element, pX, pY) );
163
168
  },
164
169
 
165
170
  deactivate: function(drop) {
166
- Element.Class.remove(drop, drop.droppable.hoverclass);
171
+ Element.Class.remove(drop.element, drop.hoverclass);
167
172
  this.last_active = null;
168
173
  },
169
174
 
170
175
  activate: function(drop) {
171
176
  if(this.last_active) this.deactivate(this.last_active);
172
- if(drop.droppable.hoverclass) {
173
- Element.Class.add(drop, drop.droppable.hoverclass);
177
+ if(drop.hoverclass) {
178
+ Element.Class.add(drop.element, drop.hoverclass);
174
179
  this.last_active = drop;
175
180
  }
176
181
  },
@@ -184,10 +189,9 @@ var Droppables = {
184
189
  var i = this.drops.length-1; do {
185
190
  var drop = this.drops[i];
186
191
  if(this.is_affected(pX, pY, element, drop)) {
187
- if(drop.droppable.onHover)
188
- drop.droppable.onHover(
189
- element, drop, Position.overlap(drop.droppable.overlap, drop));
190
- if(drop.droppable.greedy) {
192
+ if(drop.onHover)
193
+ drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
194
+ if(drop.greedy) {
191
195
  this.activate(drop);
192
196
  return;
193
197
  }
@@ -196,17 +200,13 @@ var Droppables = {
196
200
  },
197
201
 
198
202
  fire: function(event, element) {
199
- if(!this.drops) return;
200
- var pX = Event.pointerX(event);
201
- var pY = Event.pointerY(event);
203
+ if(!this.last_active) return;
202
204
  Position.prepare();
203
205
 
204
- var i = this.drops.length-1; do {
205
- var drop = this.drops[i];
206
- if(this.is_affected(pX, pY, element, drop))
207
- if(drop.droppable.onDrop)
208
- drop.droppable.onDrop(element);
209
- } while (i--);
206
+ if (this.is_affected(Event.pointerX(event), Event.pointerY(event), element, this.last_active))
207
+ if (this.last_active.onDrop)
208
+ this.last_active.onDrop(element, this.last_active);
209
+
210
210
  },
211
211
 
212
212
  reset: function() {
@@ -220,6 +220,11 @@ Draggables = {
220
220
  addObserver: function(observer) {
221
221
  this.observers.push(observer);
222
222
  },
223
+ removeObserver: function(element) { // element instead of obsever fixes mem leaks
224
+ for(var i = 0; i < this.observers.length; i++)
225
+ if(this.observers[i].element && (this.observers[i].element == element))
226
+ this.observers.splice(i,1);
227
+ },
223
228
  notify: function(eventName, draggable) { // 'onStart', 'onEnd'
224
229
  for(var i = 0; i < this.observers.length; i++)
225
230
  this.observers[i][eventName](draggable);
@@ -231,7 +236,7 @@ Draggables = {
231
236
  Draggable = Class.create();
232
237
  Draggable.prototype = {
233
238
  initialize: function(element) {
234
- var options = {
239
+ var options = Object.extend({
235
240
  handle: false,
236
241
  starteffect: function(element) {
237
242
  new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7});
@@ -244,15 +249,12 @@ Draggable.prototype = {
244
249
  },
245
250
  zindex: 1000,
246
251
  revert: false
247
- }.extend(arguments[1] || {});
252
+ }, arguments[1] || {});
248
253
 
249
254
  this.element = $(element);
250
- this.element.drag = this;
251
255
  this.handle = options.handle ? $(options.handle) : this.element;
252
256
 
253
- // fix IE
254
- if(!this.element.style.position)
255
- this.element.style.position = 'relative';
257
+ Element.makePositioned(this.element); // fix IE
256
258
 
257
259
  this.offsetX = 0;
258
260
  this.offsetY = 0;
@@ -267,9 +269,21 @@ Draggable.prototype = {
267
269
  this.active = false;
268
270
  this.dragging = false;
269
271
 
270
- Event.observe(this.handle, "mousedown", this.startDrag.bindAsEventListener(this));
271
- Event.observe(document, "mouseup", this.endDrag.bindAsEventListener(this));
272
- Event.observe(document, "mousemove", this.update.bindAsEventListener(this));
272
+ this.eventMouseDown = this.startDrag.bindAsEventListener(this);
273
+ this.eventMouseUp = this.endDrag.bindAsEventListener(this);
274
+ this.eventMouseMove = this.update.bindAsEventListener(this);
275
+ this.eventKeypress = this.keyPress.bindAsEventListener(this);
276
+
277
+ Event.observe(this.handle, "mousedown", this.eventMouseDown);
278
+ Event.observe(document, "mouseup", this.eventMouseUp);
279
+ Event.observe(document, "mousemove", this.eventMouseMove);
280
+ Event.observe(document, "keypress", this.eventKeypress);
281
+ },
282
+ destroy: function() {
283
+ Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
284
+ Event.stopObserving(document, "mouseup", this.eventMouseUp);
285
+ Event.stopObserving(document, "mousemove", this.eventMouseMove);
286
+ Event.stopObserving(document, "keypress", this.eventKeypress);
273
287
  },
274
288
  currentLeft: function() {
275
289
  return parseInt(this.element.style.left || '0');
@@ -290,31 +304,43 @@ Draggable.prototype = {
290
304
  Event.stop(event);
291
305
  }
292
306
  },
293
- endDrag: function(event) {
294
- if(this.active && this.dragging) {
295
- this.active = false;
296
- this.dragging = false;
307
+ finishDrag: function(event, success) {
308
+ this.active = false;
309
+ this.dragging = false;
310
+
311
+ if(success) Droppables.fire(event, this.element);
312
+ Draggables.notify('onEnd', this);
313
+
314
+ var revert = this.options.revert;
315
+ if(revert && typeof revert == 'function') revert = revert(this.element);
297
316
 
298
- Droppables.fire(event, this.element);
299
- Draggables.notify('onEnd', this);
317
+ if(revert && this.options.reverteffect) {
318
+ this.options.reverteffect(this.element,
319
+ this.currentTop()-this.originalTop,
320
+ this.currentLeft()-this.originalLeft);
321
+ } else {
322
+ this.originalLeft = this.currentLeft();
323
+ this.originalTop = this.currentTop();
324
+ }
325
+
326
+ this.element.style.zIndex = this.originalZ;
300
327
 
301
- var revert = this.options.revert;
302
- if(revert && typeof revert == 'function') revert = revert(this.element);
328
+ if(this.options.endeffect)
329
+ this.options.endeffect(this.element);
303
330
 
304
- if(revert && this.options.reverteffect) {
305
- this.options.reverteffect(this.element,
306
- this.currentTop()-this.originalTop,
307
- this.currentLeft()-this.originalLeft);
308
- } else {
309
- this.originalLeft = this.currentLeft();
310
- this.originalTop = this.currentTop();
331
+ Droppables.reset();
332
+ },
333
+ keyPress: function(event) {
334
+ if(this.active) {
335
+ if(event.keyCode==Event.KEY_ESC) {
336
+ this.finishDrag(event, false);
337
+ Event.stop(event);
311
338
  }
312
- this.element.style.zIndex = this.originalZ;
313
-
314
- if(this.options.endeffect)
315
- this.options.endeffect(this.element);
316
-
317
- Droppables.reset();
339
+ }
340
+ },
341
+ endDrag: function(event) {
342
+ if(this.active && this.dragging) {
343
+ this.finishDrag(event, true);
318
344
  Event.stop(event);
319
345
  }
320
346
  this.active = false;
@@ -372,9 +398,32 @@ SortableObserver.prototype = {
372
398
  }
373
399
 
374
400
  Sortable = {
401
+ sortables: new Array(),
402
+ options: function(element){
403
+ var element = $(element);
404
+ for(var i=0;i<this.sortables.length;i++)
405
+ if(this.sortables[i].element == element)
406
+ return this.sortables[i];
407
+ return null;
408
+ },
409
+ destroy: function(element){
410
+ var element = $(element);
411
+ for(var i=0;i<this.sortables.length;i++) {
412
+ if(this.sortables[i].element == element) {
413
+ var s = this.sortables[i];
414
+ Draggables.removeObserver(s.element);
415
+ for(var j=0;j<s.droppables.length;j++)
416
+ Droppables.remove(s.droppables[j]);
417
+ for(var j=0;j<s.draggables.length;j++)
418
+ s.draggables[j].destroy();
419
+ this.sortables.splice(i,1);
420
+ }
421
+ }
422
+ },
375
423
  create: function(element) {
376
424
  var element = $(element);
377
- var options = {
425
+ var options = Object.extend({
426
+ element: element,
378
427
  tag: 'li', // assumes li children, override with tag: 'tagname'
379
428
  overlap: 'vertical', // one of 'vertical', 'horizontal'
380
429
  constraint: 'vertical', // one of 'vertical', 'horizontal', false
@@ -384,8 +433,10 @@ Sortable = {
384
433
  hoverclass: null,
385
434
  onChange: function() {},
386
435
  onUpdate: function() {}
387
- }.extend(arguments[1] || {});
388
- element.sortable = options;
436
+ }, arguments[1] || {});
437
+
438
+ // clear any old sortable with same element
439
+ this.destroy(element);
389
440
 
390
441
  // build options for the draggables
391
442
  var options_for_draggable = {
@@ -435,8 +486,8 @@ Sortable = {
435
486
  // fix for gecko engine
436
487
  Element.cleanWhitespace(element);
437
488
 
438
- // for onupdate
439
- Draggables.addObserver(new SortableObserver(element, options.onUpdate));
489
+ options.draggables = [];
490
+ options.droppables = [];
440
491
 
441
492
  // make it so
442
493
  var elements = element.childNodes;
@@ -448,18 +499,28 @@ Sortable = {
448
499
  var handle = options.handle ?
449
500
  Element.Class.childrenWith(elements[i], options.handle)[0] : elements[i];
450
501
 
451
- new Draggable(elements[i], options_for_draggable.extend({ handle: handle }));
502
+ options.draggables.push(new Draggable(elements[i], Object.extend(options_for_draggable, { handle: handle })));
503
+
452
504
  Droppables.add(elements[i], options_for_droppable);
505
+ options.droppables.push(elements[i]);
506
+
453
507
  }
454
508
 
509
+ // keep reference
510
+ this.sortables.push(options);
511
+
512
+ // for onupdate
513
+ Draggables.addObserver(new SortableObserver(element, options.onUpdate));
514
+
455
515
  },
456
516
  serialize: function(element) {
457
517
  var element = $(element);
458
- var options = {
459
- tag: element.sortable.tag,
460
- only: element.sortable.only,
518
+ var sortableOptions = this.options(element);
519
+ var options = Object.extend({
520
+ tag: sortableOptions.tag,
521
+ only: sortableOptions.only,
461
522
  name: element.id
462
- }.extend(arguments[1] || {});
523
+ }, arguments[1] || {});
463
524
 
464
525
  var items = $(element).childNodes;
465
526
  var queryComponents = new Array();