1 // Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
3 // Element.Class part Copyright (c) 2005 by Rick Olson
5 // See scriptaculous.js for full license.
7 /*--------------------------------------------------------------------------*/
12 remove: function(element) {
13 this.drops = this.drops.reject(function(d) { return d.element==element });
16 add: function(element) {
18 var options = Object.extend({
21 }, arguments[1] || {});
24 if(options.containment) {
25 options._containers = [];
26 var containment = options.containment;
27 if((typeof containment == 'object') &&
28 (containment.constructor == Array)) {
29 containment.each( function(c) { options._containers.push($(c)) });
31 options._containers.push($(containment));
35 Element.makePositioned(element); // fix IE
36 options.element = element;
38 this.drops.push(options);
41 isContained: function(element, drop) {
42 var parentNode = element.parentNode;
43 return drop._containers.detect(function(c) { return parentNode == c });
46 isAffected: function(pX, pY, element, drop) {
48 (drop.element!=element) &&
49 ((!drop._containers) ||
50 this.isContained(element, drop)) &&
52 (Element.Class.has_any(element, drop.accept))) &&
53 Position.within(drop.element, pX, pY) );
56 deactivate: function(drop) {
58 Element.Class.remove(drop.element, drop.hoverclass);
59 this.last_active = null;
62 activate: function(drop) {
63 if(this.last_active) this.deactivate(this.last_active);
65 Element.Class.add(drop.element, drop.hoverclass);
66 this.last_active = drop;
69 show: function(event, element) {
70 if(!this.drops.length) return;
71 var pX = Event.pointerX(event);
72 var pY = Event.pointerY(event);
75 var i = this.drops.length-1; do {
76 var drop = this.drops[i];
77 if(this.isAffected(pX, pY, element, drop)) {
79 drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
87 if(this.last_active) this.deactivate(this.last_active);
90 fire: function(event, element) {
91 if(!this.last_active) return;
94 if (this.isAffected(Event.pointerX(event), Event.pointerY(event), element, this.last_active))
95 if (this.last_active.onDrop)
96 this.last_active.onDrop(element, this.last_active.element, event);
101 this.deactivate(this.last_active);
107 addObserver: function(observer) {
108 this.observers.push(observer);
110 removeObserver: function(element) { // element instead of obsever fixes mem leaks
111 this.observers = this.observers.reject( function(o) { return o.element==element });
113 notify: function(eventName, draggable) { // 'onStart', 'onEnd'
114 this.observers.invoke(eventName, draggable);
118 /*--------------------------------------------------------------------------*/
120 var Draggable = Class.create();
121 Draggable.prototype = {
122 initialize: function(element) {
123 var options = Object.extend({
125 starteffect: function(element) {
126 new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7});
128 reverteffect: function(element, top_offset, left_offset) {
129 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});
132 endeffect: function(element) {
133 new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0});
137 }, arguments[1] || {});
139 this.element = $(element);
140 if(options.handle && (typeof options.handle == 'string'))
141 this.handle = Element.Class.childrenWith(this.element, options.handle)[0];
143 if(!this.handle) this.handle = $(options.handle);
144 if(!this.handle) this.handle = this.element;
146 Element.makePositioned(this.element); // fix IE
150 this.originalLeft = this.currentLeft();
151 this.originalTop = this.currentTop();
152 this.originalX = this.element.offsetLeft;
153 this.originalY = this.element.offsetTop;
155 this.options = options;
158 this.dragging = false;
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);
165 this.registerEvents();
167 destroy: function() {
168 Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
169 this.unregisterEvents();
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);
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);
183 currentLeft: function() {
184 return parseInt(this.element.style.left || '0');
186 currentTop: function() {
187 return parseInt(this.element.style.top || '0')
189 startDrag: function(event) {
190 if(Event.isLeftClick(event)) {
192 // abort on form elements, fixes a Firefox issue
193 var src = Event.element(event);
195 src.tagName=='INPUT' ||
196 src.tagName=='SELECT' ||
197 src.tagName=='BUTTON' ||
198 src.tagName=='TEXTAREA')) return;
200 // this.registerEvents();
202 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]);
209 finishDrag: function(event, success) {
210 // this.unregisterEvents();
213 this.dragging = false;
215 if(this.options.ghosting) {
216 Position.relativize(this.element);
217 Element.remove(this._clone);
221 if(success) Droppables.fire(event, this.element);
222 Draggables.notify('onEnd', this);
224 var revert = this.options.revert;
225 if(revert && typeof revert == 'function') revert = revert(this.element);
227 if(revert && this.options.reverteffect) {
228 this.options.reverteffect(this.element,
229 this.currentTop()-this.originalTop,
230 this.currentLeft()-this.originalLeft);
232 this.originalLeft = this.currentLeft();
233 this.originalTop = this.currentTop();
236 if(this.options.zindex)
237 this.element.style.zIndex = this.originalZ;
239 if(this.options.endeffect)
240 this.options.endeffect(this.element);
245 keyPress: function(event) {
247 if(event.keyCode==Event.KEY_ESC) {
248 this.finishDrag(event, false);
253 endDrag: function(event) {
254 if(this.active && this.dragging) {
255 this.finishDrag(event, true);
259 this.dragging = false;
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();
266 var style = this.element.style;
267 if((!this.options.constraint) || (this.options.constraint=='horizontal'))
268 style.left = (pointer[0] - offsets[0] - this.offsetX) + "px";
269 if((!this.options.constraint) || (this.options.constraint=='vertical'))
270 style.top = (pointer[1] - offsets[1] - this.offsetY) + "px";
271 if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
273 update: function(event) {
276 var style = this.element.style;
277 this.dragging = true;
279 if(Element.getStyle(this.element,'position')=='')
280 style.position = "relative";
282 if(this.options.zindex) {
283 this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
284 style.zIndex = this.options.zindex;
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);
293 Draggables.notify('onStart', this);
294 if(this.options.starteffect) this.options.starteffect(this.element);
297 Droppables.show(event, this.element);
299 if(this.options.change) this.options.change(this);
301 // fix AppleWebKit rendering
302 if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
309 /*--------------------------------------------------------------------------*/
311 var SortableObserver = Class.create();
312 SortableObserver.prototype = {
313 initialize: function(element, observer) {
314 this.element = $(element);
315 this.observer = observer;
316 this.lastValue = Sortable.serialize(this.element);
318 onStart: function() {
319 this.lastValue = Sortable.serialize(this.element);
323 if(this.lastValue != Sortable.serialize(this.element))
324 this.observer(this.element)
329 sortables: new Array(),
330 options: function(element){
331 element = $(element);
332 return this.sortables.detect(function(s) { return s.element == element });
334 destroy: function(element){
335 element = $(element);
336 this.sortables.findAll(function(s) { return s.element == element }).each(function(s){
337 Draggables.removeObserver(s.element);
338 s.droppables.each(function(d){ Droppables.remove(d) });
339 s.draggables.invoke('destroy');
341 this.sortables = this.sortables.reject(function(s) { return s.element == element });
343 create: function(element) {
344 element = $(element);
345 var options = Object.extend({
347 tag: 'li', // assumes li children, override with tag: 'tagname'
349 tree: false, // fixme: unimplemented
350 overlap: 'vertical', // one of 'vertical', 'horizontal'
351 constraint: 'vertical', // one of 'vertical', 'horizontal', false
352 containment: element, // also takes array of elements (or id's); or false
353 handle: false, // or a CSS class
358 onChange: Prototype.emptyFunction,
359 onUpdate: Prototype.emptyFunction
360 }, arguments[1] || {});
362 // clear any old sortable with same element
363 this.destroy(element);
365 // build options for the draggables
366 var options_for_draggable = {
368 ghosting: options.ghosting,
369 constraint: options.constraint,
370 handle: options.handle };
372 if(options.starteffect)
373 options_for_draggable.starteffect = options.starteffect;
375 if(options.reverteffect)
376 options_for_draggable.reverteffect = options.reverteffect;
378 if(options.ghosting) options_for_draggable.reverteffect = function(element) {
379 element.style.top = 0;
380 element.style.left = 0;
383 if(options.endeffect)
384 options_for_draggable.endeffect = options.endeffect;
387 options_for_draggable.zindex = options.zindex;
389 // build options for the droppables
390 var options_for_droppable = {
391 overlap: options.overlap,
392 containment: options.containment,
393 hoverclass: options.hoverclass,
394 onHover: Sortable.onHover,
395 greedy: !options.dropOnEmpty
398 // fix for gecko engine
399 Element.cleanWhitespace(element);
401 options.draggables = [];
402 options.droppables = [];
406 // drop on empty handling
407 if(options.dropOnEmpty) {
408 Droppables.add(element,
409 {containment: options.containment, onHover: Sortable.onEmptyHover, greedy: false});
410 options.droppables.push(element);
413 (this.findElements(element, options) || []).each( function(e) {
414 // handles are per-draggable
415 var handle = options.handle ?
416 Element.Class.childrenWith(e, options.handle)[0] : e;
417 options.draggables.push(
418 new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
419 Droppables.add(e, options_for_droppable);
420 options.droppables.push(e);
424 this.sortables.push(options);
427 Draggables.addObserver(new SortableObserver(element, options.onUpdate));
431 // return all suitable-for-sortable elements in a guaranteed order
432 findElements: function(element, options) {
433 if(!element.hasChildNodes()) return null;
435 $A(element.childNodes).each( function(e) {
436 if(e.tagName && e.tagName==options.tag.toUpperCase() &&
437 (!options.only || (Element.Class.has(e, options.only))))
440 var grandchildren = this.findElements(e, options);
441 if(grandchildren) elements.push(grandchildren);
445 return (elements.length>0 ? elements.flatten() : null);
448 onHover: function(element, dropon, overlap) {
450 Sortable.mark(dropon, 'before');
451 if(dropon.previousSibling != element) {
452 var oldParentNode = element.parentNode;
453 element.style.visibility = "hidden"; // fix gecko rendering
454 dropon.parentNode.insertBefore(element, dropon);
455 if(dropon.parentNode!=oldParentNode)
456 Sortable.options(oldParentNode).onChange(element);
457 Sortable.options(dropon.parentNode).onChange(element);
460 Sortable.mark(dropon, 'after');
461 var nextElement = dropon.nextSibling || null;
462 if(nextElement != element) {
463 var oldParentNode = element.parentNode;
464 element.style.visibility = "hidden"; // fix gecko rendering
465 dropon.parentNode.insertBefore(element, nextElement);
466 if(dropon.parentNode!=oldParentNode)
467 Sortable.options(oldParentNode).onChange(element);
468 Sortable.options(dropon.parentNode).onChange(element);
473 onEmptyHover: function(element, dropon) {
474 if(element.parentNode!=dropon) {
475 var oldParentNode = element.parentNode;
476 dropon.appendChild(element);
477 Sortable.options(oldParentNode).onChange(element);
478 Sortable.options(dropon).onChange(element);
483 if(Sortable._marker) Element.hide(Sortable._marker);
486 mark: function(dropon, position) {
487 // mark on ghosting only
488 var sortable = Sortable.options(dropon.parentNode);
489 if(sortable && !sortable.ghosting) return;
491 if(!Sortable._marker) {
492 Sortable._marker = $('dropmarker') || document.createElement('DIV');
493 Element.hide(Sortable._marker);
494 Element.Class.add(Sortable._marker, 'dropmarker');
495 Sortable._marker.style.position = 'absolute';
496 document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
498 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 Sortable._marker.style.left = offsets[0] + 'px';
502 Element.show(Sortable._marker);
505 serialize: function(element) {
506 element = $(element);
507 var sortableOptions = this.options(element);
508 var options = Object.extend({
509 tag: sortableOptions.tag,
510 only: sortableOptions.only,
512 format: sortableOptions.format || /^[^_]*_(.*)$/
513 }, arguments[1] || {});
514 return $(this.findElements(element, options) || []).collect( function(item) {
515 return (encodeURIComponent(options.name) + "[]=" +
516 encodeURIComponent(item.id.match(options.format) ? item.id.match(options.format)[1] : ''));