adding a semicolon... yay!
[cxgn-jslib.git] / MochiKit / Sortable.js
blob8976ec0b27fb3412355e91d997a969bda23293b4
1 /***
2 Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
3     Mochi-ized By Thomas Herve (_firstname_@nimail.org)
5 See scriptaculous.js for full license.
7 ***/
9 if (typeof(dojo) != 'undefined') {
10     dojo.provide('MochiKit.Sortable');
11     dojo.require('MochiKit.Base');
12     dojo.require('MochiKit.DOM');
13     dojo.require('MochiKit.Iter');
16 if (typeof(JSAN) != 'undefined') {
17     JSAN.use("MochiKit.Base", []);
18     JSAN.use("MochiKit.DOM", []);
19     JSAN.use("MochiKit.Iter", []);
22 try {
23     if (typeof(MochiKit.Base) == 'undefined' ||
24         typeof(MochiKit.DOM) == 'undefined' ||
25         typeof(MochiKit.Iter) == 'undefined') {
26         throw "";
27     }
28 } catch (e) {
29     throw "MochiKit.DragAndDrop depends on MochiKit.Base, MochiKit.DOM and MochiKit.Iter!";
32 if (typeof(MochiKit.Sortable) == 'undefined') {
33     MochiKit.Sortable = {};
36 MochiKit.Sortable.NAME = 'MochiKit.Sortable';
37 MochiKit.Sortable.VERSION = '1.4';
39 MochiKit.Sortable.__repr__ = function () {
40     return '[' + this.NAME + ' ' + this.VERSION + ']';
43 MochiKit.Sortable.toString = function () {
44     return this.__repr__();
47 MochiKit.Sortable.EXPORT = [
50 MochiKit.Sortable.EXPORT_OK = [
53 MochiKit.Base.update(MochiKit.Sortable, {
54     /***
56     Manage sortables. Mainly use the create function to add a sortable.
58     ***/
59     sortables: {},
61     _findRootElement: function (element) {
62         while (element.tagName.toUpperCase() != "BODY") {
63             if (element.id && MochiKit.Sortable.sortables[element.id]) {
64                 return element;
65             }
66             element = element.parentNode;
67         }
68     },
70     /** @id MochiKit.Sortable.options */
71     options: function (element) {
72         element = MochiKit.Sortable._findRootElement(MochiKit.DOM.getElement(element));
73         if (!element) {
74             return;
75         }
76         return MochiKit.Sortable.sortables[element.id];
77     },
79     /** @id MochiKit.Sortable.destroy */
80     destroy: function (element){
81         var s = MochiKit.Sortable.options(element);
82         var b = MochiKit.Base;
83         var d = MochiKit.DragAndDrop;
85         if (s) {
86             MochiKit.Signal.disconnect(s.startHandle);
87             MochiKit.Signal.disconnect(s.endHandle);
88             b.map(function (dr) {
89                 d.Droppables.remove(dr);
90             }, s.droppables);
91             b.map(function (dr) {
92                 dr.destroy();
93             }, s.draggables);
95             delete MochiKit.Sortable.sortables[s.element.id];
96         }
97     },
99     /** @id MochiKit.Sortable.create */
100     create: function (element, options) {
101         element = MochiKit.DOM.getElement(element);
102         var self = MochiKit.Sortable;
104         /** @id MochiKit.Sortable.options */
105         options = MochiKit.Base.update({
107             /** @id MochiKit.Sortable.element */
108             element: element,
110             /** @id MochiKit.Sortable.tag */
111             tag: 'li',  // assumes li children, override with tag: 'tagname'
113             /** @id MochiKit.Sortable.dropOnEmpty */
114             dropOnEmpty: false,
116             /** @id MochiKit.Sortable.tree */
117             tree: false,
119             /** @id MochiKit.Sortable.treeTag */
120             treeTag: 'ul',
122             /** @id MochiKit.Sortable.overlap */
123             overlap: 'vertical',  // one of 'vertical', 'horizontal'
125             /** @id MochiKit.Sortable.constraint */
126             constraint: 'vertical',  // one of 'vertical', 'horizontal', false
127             // also takes array of elements (or ids); or false
129             /** @id MochiKit.Sortable.containment */
130             containment: [element],
132             /** @id MochiKit.Sortable.handle */
133             handle: false,  // or a CSS class
135             /** @id MochiKit.Sortable.only */
136             only: false,
138             /** @id MochiKit.Sortable.hoverclass */
139             hoverclass: null,
141             /** @id MochiKit.Sortable.ghosting */
142             ghosting: false,
144             /** @id MochiKit.Sortable.scroll */
145             scroll: false,
147             /** @id MochiKit.Sortable.scrollSensitivity */
148             scrollSensitivity: 20,
150             /** @id MochiKit.Sortable.scrollSpeed */
151             scrollSpeed: 15,
153             /** @id MochiKit.Sortable.format */
154             format: /^[^_]*_(.*)$/,
156             /** @id MochiKit.Sortable.onChange */
157             onChange: MochiKit.Base.noop,
159             /** @id MochiKit.Sortable.onUpdate */
160             onUpdate: MochiKit.Base.noop,
162             /** @id MochiKit.Sortable.accept */
163             accept: null
164         }, options);
166         // clear any old sortable with same element
167         self.destroy(element);
169         // build options for the draggables
170         var options_for_draggable = {
171             revert: true,
172             ghosting: options.ghosting,
173             scroll: options.scroll,
174             scrollSensitivity: options.scrollSensitivity,
175             scrollSpeed: options.scrollSpeed,
176             constraint: options.constraint,
177             handle: options.handle
178         };
180         if (options.starteffect) {
181             options_for_draggable.starteffect = options.starteffect;
182         }
184         if (options.reverteffect) {
185             options_for_draggable.reverteffect = options.reverteffect;
186         } else if (options.ghosting) {
187             options_for_draggable.reverteffect = function (innerelement) {
188                 innerelement.style.top = 0;
189                 innerelement.style.left = 0;
190             };
191         }
193         if (options.endeffect) {
194             options_for_draggable.endeffect = options.endeffect;
195         }
197         if (options.zindex) {
198             options_for_draggable.zindex = options.zindex;
199         }
201         // build options for the droppables
202         var options_for_droppable = {
203             overlap: options.overlap,
204             containment: options.containment,
205             hoverclass: options.hoverclass,
206             onhover: self.onHover,
207             tree: options.tree,
208             accept: options.accept
209         }
211         var options_for_tree = {
212             onhover: self.onEmptyHover,
213             overlap: options.overlap,
214             containment: options.containment,
215             hoverclass: options.hoverclass,
216             accept: options.accept
217         }
219         // fix for gecko engine
220         MochiKit.DOM.removeEmptyTextNodes(element);
222         options.draggables = [];
223         options.droppables = [];
225         // drop on empty handling
226         if (options.dropOnEmpty || options.tree) {
227             new MochiKit.DragAndDrop.Droppable(element, options_for_tree);
228             options.droppables.push(element);
229         }
230         MochiKit.Base.map(function (e) {
231             // handles are per-draggable
232             var handle = options.handle ?
233                 MochiKit.DOM.getFirstElementByTagAndClassName(null,
234                     options.handle, e) : e;
235             options.draggables.push(
236                 new MochiKit.DragAndDrop.Draggable(e,
237                     MochiKit.Base.update(options_for_draggable,
238                                          {handle: handle})));
239             new MochiKit.DragAndDrop.Droppable(e, options_for_droppable);
240             if (options.tree) {
241                 e.treeNode = element;
242             }
243             options.droppables.push(e);
244         }, (self.findElements(element, options) || []));
246         if (options.tree) {
247             MochiKit.Base.map(function (e) {
248                 new MochiKit.DragAndDrop.Droppable(e, options_for_tree);
249                 e.treeNode = element;
250                 options.droppables.push(e);
251             }, (self.findTreeElements(element, options) || []));
252         }
254         // keep reference
255         self.sortables[element.id] = options;
257         options.lastValue = self.serialize(element);
258         options.startHandle = MochiKit.Signal.connect(MochiKit.DragAndDrop.Draggables, 'start',
259                                 MochiKit.Base.partial(self.onStart, element));
260         options.endHandle = MochiKit.Signal.connect(MochiKit.DragAndDrop.Draggables, 'end',
261                                 MochiKit.Base.partial(self.onEnd, element));
262     },
264     /** @id MochiKit.Sortable.onStart */
265     onStart: function (element, draggable) {
266         var self = MochiKit.Sortable;
267         var options = self.options(element);
268         options.lastValue = self.serialize(options.element);
269     },
271     /** @id MochiKit.Sortable.onEnd */
272     onEnd: function (element, draggable) {
273         var self = MochiKit.Sortable;
274         self.unmark();
275         var options = self.options(element);
276         if (options.lastValue != self.serialize(options.element)) {
277             options.onUpdate(options.element);
278         }
279     },
281     // return all suitable-for-sortable elements in a guaranteed order
283     /** @id MochiKit.Sortable.findElements */
284     findElements: function (element, options) {
285         return MochiKit.Sortable.findChildren(
286             element, options.only, options.tree ? true : false, options.tag);
287     },
289     /** @id MochiKit.Sortable.findTreeElements */
290     findTreeElements: function (element, options) {
291         return MochiKit.Sortable.findChildren(
292             element, options.only, options.tree ? true : false, options.treeTag);
293     },
295     /** @id MochiKit.Sortable.findChildren */
296     findChildren: function (element, only, recursive, tagName) {
297         if (!element.hasChildNodes()) {
298             return null;
299         }
300         tagName = tagName.toUpperCase();
301         if (only) {
302             only = MochiKit.Base.flattenArray([only]);
303         }
304         var elements = [];
305         MochiKit.Base.map(function (e) {
306             if (e.tagName &&
307                 e.tagName.toUpperCase() == tagName &&
308                (!only ||
309                 MochiKit.Iter.some(only, function (c) {
310                     return MochiKit.DOM.hasElementClass(e, c);
311                 }))) {
312                 elements.push(e);
313             }
314             if (recursive) {
315                 var grandchildren = MochiKit.Sortable.findChildren(e, only, recursive, tagName);
316                 if (grandchildren && grandchildren.length > 0) {
317                     elements = elements.concat(grandchildren);
318                 }
319             }
320         }, element.childNodes);
321         return elements;
322     },
324     /** @id MochiKit.Sortable.onHover */
325     onHover: function (element, dropon, overlap) {
326         if (MochiKit.DOM.isParent(dropon, element)) {
327             return;
328         }
329         var self = MochiKit.Sortable;
331         if (overlap > .33 && overlap < .66 && self.options(dropon).tree) {
332             return;
333         } else if (overlap > 0.5) {
334             self.mark(dropon, 'before');
335             if (dropon.previousSibling != element) {
336                 var oldParentNode = element.parentNode;
337                 element.style.visibility = 'hidden';  // fix gecko rendering
338                 dropon.parentNode.insertBefore(element, dropon);
339                 if (dropon.parentNode != oldParentNode) {
340                     self.options(oldParentNode).onChange(element);
341                 }
342                 self.options(dropon.parentNode).onChange(element);
343             }
344         } else {
345             self.mark(dropon, 'after');
346             var nextElement = dropon.nextSibling || null;
347             if (nextElement != element) {
348                 var oldParentNode = element.parentNode;
349                 element.style.visibility = 'hidden';  // fix gecko rendering
350                 dropon.parentNode.insertBefore(element, nextElement);
351                 if (dropon.parentNode != oldParentNode) {
352                     self.options(oldParentNode).onChange(element);
353                 }
354                 self.options(dropon.parentNode).onChange(element);
355             }
356         }
357     },
359     _offsetSize: function (element, type) {
360         if (type == 'vertical' || type == 'height') {
361             return element.offsetHeight;
362         } else {
363             return element.offsetWidth;
364         }
365     },
367     /** @id MochiKit.Sortable.onEmptyHover */
368     onEmptyHover: function (element, dropon, overlap) {
369         var oldParentNode = element.parentNode;
370         var self = MochiKit.Sortable;
371         var droponOptions = self.options(dropon);
373         if (!MochiKit.DOM.isParent(dropon, element)) {
374             var index;
376             var children = self.findElements(dropon, {tag: droponOptions.tag,
377                                                       only: droponOptions.only});
378             var child = null;
380             if (children) {
381                 var offset = self._offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
383                 for (index = 0; index < children.length; index += 1) {
384                     if (offset - self._offsetSize(children[index], droponOptions.overlap) >= 0) {
385                         offset -= self._offsetSize(children[index], droponOptions.overlap);
386                     } else if (offset - (self._offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
387                         child = index + 1 < children.length ? children[index + 1] : null;
388                         break;
389                     } else {
390                         child = children[index];
391                         break;
392                     }
393                 }
394             }
396             dropon.insertBefore(element, child);
398             self.options(oldParentNode).onChange(element);
399             droponOptions.onChange(element);
400         }
401     },
403     /** @id MochiKit.Sortable.unmark */
404     unmark: function () {
405         var m = MochiKit.Sortable._marker;
406         if (m) {
407             MochiKit.Style.hideElement(m);
408         }
409     },
411     /** @id MochiKit.Sortable.mark */
412     mark: function (dropon, position) {
413         // mark on ghosting only
414         var d = MochiKit.DOM;
415         var self = MochiKit.Sortable;
416         var sortable = self.options(dropon.parentNode);
417         if (sortable && !sortable.ghosting) {
418             return;
419         }
421         if (!self._marker) {
422             self._marker = d.getElement('dropmarker') ||
423                         document.createElement('DIV');
424             MochiKit.Style.hideElement(self._marker);
425             d.addElementClass(self._marker, 'dropmarker');
426             self._marker.style.position = 'absolute';
427             document.getElementsByTagName('body').item(0).appendChild(self._marker);
428         }
429         var offsets = MochiKit.Position.cumulativeOffset(dropon);
430         self._marker.style.left = offsets.x + 'px';
431         self._marker.style.top = offsets.y + 'px';
433         if (position == 'after') {
434             if (sortable.overlap == 'horizontal') {
435                 self._marker.style.left = (offsets.x + dropon.clientWidth) + 'px';
436             } else {
437                 self._marker.style.top = (offsets.y + dropon.clientHeight) + 'px';
438             }
439         }
440         MochiKit.Style.showElement(self._marker);
441     },
443     _tree: function (element, options, parent) {
444         var self = MochiKit.Sortable;
445         var children = self.findElements(element, options) || [];
447         for (var i = 0; i < children.length; ++i) {
448             var match = children[i].id.match(options.format);
450             if (!match) {
451                 continue;
452             }
454             var child = {
455                 id: encodeURIComponent(match ? match[1] : null),
456                 element: element,
457                 parent: parent,
458                 children: [],
459                 position: parent.children.length,
460                 container: self._findChildrenElement(children[i], options.treeTag.toUpperCase())
461             }
463             /* Get the element containing the children and recurse over it */
464             if (child.container) {
465                 self._tree(child.container, options, child)
466             }
468             parent.children.push (child);
469         }
471         return parent;
472     },
474     /* Finds the first element of the given tag type within a parent element.
475        Used for finding the first LI[ST] within a L[IST]I[TEM].*/
476     _findChildrenElement: function (element, containerTag) {
477         if (element && element.hasChildNodes) {
478             containerTag = containerTag.toUpperCase();
479             for (var i = 0; i < element.childNodes.length; ++i) {
480                 if (element.childNodes[i].tagName.toUpperCase() == containerTag) {
481                     return element.childNodes[i];
482                 }
483             }
484         }
485         return null;
486     },
488     /** @id MochiKit.Sortable.tree */
489     tree: function (element, options) {
490         element = MochiKit.DOM.getElement(element);
491         var sortableOptions = MochiKit.Sortable.options(element);
492         options = MochiKit.Base.update({
493             tag: sortableOptions.tag,
494             treeTag: sortableOptions.treeTag,
495             only: sortableOptions.only,
496             name: element.id,
497             format: sortableOptions.format
498         }, options || {});
500         var root = {
501             id: null,
502             parent: null,
503             children: new Array,
504             container: element,
505             position: 0
506         }
508         return MochiKit.Sortable._tree(element, options, root);
509     },
511     /**
512      * Specifies the sequence for the Sortable.
513      * @param {Node} element    Element to use as the Sortable.
514      * @param {Object} newSequence    New sequence to use.
515      * @param {Object} options    Options to use fro the Sortable.
516      */
517     setSequence: function (element, newSequence, options) {
518         var self = MochiKit.Sortable;
519         var b = MochiKit.Base;
520         element = MochiKit.DOM.getElement(element);
521         options = b.update(self.options(element), options || {});
523         var nodeMap = {};
524         b.map(function (n) {
525             var m = n.id.match(options.format);
526             if (m) {
527                 nodeMap[m[1]] = [n, n.parentNode];
528             }
529             n.parentNode.removeChild(n);
530         }, self.findElements(element, options));
532         b.map(function (ident) {
533             var n = nodeMap[ident];
534             if (n) {
535                 n[1].appendChild(n[0]);
536                 delete nodeMap[ident];
537             }
538         }, newSequence);
539     },
541     /* Construct a [i] index for a particular node */
542     _constructIndex: function (node) {
543         var index = '';
544         do {
545             if (node.id) {
546                 index = '[' + node.position + ']' + index;
547             }
548         } while ((node = node.parent) != null);
549         return index;
550     },
552     /** @id MochiKit.Sortable.sequence */
553     sequence: function (element, options) {
554         element = MochiKit.DOM.getElement(element);
555         var self = MochiKit.Sortable;
556         var options = MochiKit.Base.update(self.options(element), options || {});
558         return MochiKit.Base.map(function (item) {
559             return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
560         }, MochiKit.DOM.getElement(self.findElements(element, options) || []));
561     },
563     /**
564      * Serializes the content of a Sortable. Useful to send this content through a XMLHTTPRequest.
565      * These options override the Sortable options for the serialization only.
566      * @param {Node} element    Element to serialize.
567      * @param {Object} options    Serialization options.
568      */
569     serialize: function (element, options) {
570         element = MochiKit.DOM.getElement(element);
571         var self = MochiKit.Sortable;
572         options = MochiKit.Base.update(self.options(element), options || {});
573         var name = encodeURIComponent(options.name || element.id);
575         if (options.tree) {
576             return MochiKit.Base.flattenArray(MochiKit.Base.map(function (item) {
577                 return [name + self._constructIndex(item) + "[id]=" +
578                 encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
579             }, self.tree(element, options).children)).join('&');
580         } else {
581             return MochiKit.Base.map(function (item) {
582                 return name + "[]=" + encodeURIComponent(item);
583             }, self.sequence(element, options)).join('&');
584         }
585     }
588 // trunk compatibility
589 MochiKit.Sortable.Sortable = MochiKit.Sortable;