Bug 1876888 - Try estimate percentiles as "shortest 5%" instead of "top 95%". r=ahal...
[gecko.git] / testing / mochitest / MochiKit / Sortable.js
blob2bee90b5d82ab9135de6648c3af78a0bab439fe3
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.DragAndDrop');
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.DragAndDrop.EXPORT_OK = [
51     "Sortable"
54 MochiKit.Sortable.Sortable = {
55     /***
57     Manage sortables. Mainly use the create function to add a sortable.
59     ***/
60     sortables: {},
62     _findRootElement: function (element) {
63         while (element.tagName.toUpperCase() != "BODY") {
64             if (element.id && MochiKit.Sortable.Sortable.sortables[element.id]) {
65                 return element;
66             }
67             element = element.parentNode;
68         }
69     },
71     /** @id MochiKit.Sortable.Sortable.options */
72     options: function (element) {
73         element = MochiKit.Sortable.Sortable._findRootElement(MochiKit.DOM.getElement(element));
74         if (!element) {
75             return;
76         }
77         return MochiKit.Sortable.Sortable.sortables[element.id];
78     },
80     /** @id MochiKit.Sortable.Sortable.destroy */
81     destroy: function (element){
82         var s = MochiKit.Sortable.Sortable.options(element);
83         var b = MochiKit.Base;
84         var d = MochiKit.DragAndDrop;
86         if (s) {
87             MochiKit.Signal.disconnect(s.startHandle);
88             MochiKit.Signal.disconnect(s.endHandle);
89             b.map(function (dr) {
90                 d.Droppables.remove(dr);
91             }, s.droppables);
92             b.map(function (dr) {
93                 dr.destroy();
94             }, s.draggables);
96             delete MochiKit.Sortable.Sortable.sortables[s.element.id];
97         }
98     },
100     /** @id MochiKit.Sortable.Sortable.create */
101     create: function (element, options) {
102         element = MochiKit.DOM.getElement(element);
103         var self = MochiKit.Sortable.Sortable;
104         
105         /** @id MochiKit.Sortable.Sortable.options */
106         options = MochiKit.Base.update({
107             
108             /** @id MochiKit.Sortable.Sortable.element */
109             element: element,
110             
111             /** @id MochiKit.Sortable.Sortable.tag */
112             tag: 'li',  // assumes li children, override with tag: 'tagname'
113             
114             /** @id MochiKit.Sortable.Sortable.dropOnEmpty */
115             dropOnEmpty: false,
116             
117             /** @id MochiKit.Sortable.Sortable.tree */
118             tree: false,
119             
120             /** @id MochiKit.Sortable.Sortable.treeTag */
121             treeTag: 'ul',
122             
123             /** @id MochiKit.Sortable.Sortable.overlap */
124             overlap: 'vertical',  // one of 'vertical', 'horizontal'
125             
126             /** @id MochiKit.Sortable.Sortable.constraint */
127             constraint: 'vertical',  // one of 'vertical', 'horizontal', false
128             // also takes array of elements (or ids); or false
129             
130             /** @id MochiKit.Sortable.Sortable.containment */
131             containment: [element],
132             
133             /** @id MochiKit.Sortable.Sortable.handle */
134             handle: false,  // or a CSS class
135             
136             /** @id MochiKit.Sortable.Sortable.only */
137             only: false,
138             
139             /** @id MochiKit.Sortable.Sortable.hoverclass */
140             hoverclass: null,
141             
142             /** @id MochiKit.Sortable.Sortable.ghosting */
143             ghosting: false,
144             
145             /** @id MochiKit.Sortable.Sortable.scroll */
146             scroll: false,
147             
148             /** @id MochiKit.Sortable.Sortable.scrollSensitivity */
149             scrollSensitivity: 20,
150             
151             /** @id MochiKit.Sortable.Sortable.scrollSpeed */
152             scrollSpeed: 15,
153             
154             /** @id MochiKit.Sortable.Sortable.format */
155             format: /^[^_]*_(.*)$/,
156             
157             /** @id MochiKit.Sortable.Sortable.onChange */
158             onChange: MochiKit.Base.noop,
159             
160             /** @id MochiKit.Sortable.Sortable.onUpdate */
161             onUpdate: MochiKit.Base.noop,
162             
163             /** @id MochiKit.Sortable.Sortable.accept */
164             accept: null
165         }, options);
167         // clear any old sortable with same element
168         self.destroy(element);
170         // build options for the draggables
171         var options_for_draggable = {
172             revert: true,
173             ghosting: options.ghosting,
174             scroll: options.scroll,
175             scrollSensitivity: options.scrollSensitivity,
176             scrollSpeed: options.scrollSpeed,
177             constraint: options.constraint,
178             handle: options.handle
179         };
181         if (options.starteffect) {
182             options_for_draggable.starteffect = options.starteffect;
183         }
185         if (options.reverteffect) {
186             options_for_draggable.reverteffect = options.reverteffect;
187         } else if (options.ghosting) {
188             options_for_draggable.reverteffect = function (innerelement) {
189                 innerelement.style.top = 0;
190                 innerelement.style.left = 0;
191             };
192         }
194         if (options.endeffect) {
195             options_for_draggable.endeffect = options.endeffect;
196         }
198         if (options.zindex) {
199             options_for_draggable.zindex = options.zindex;
200         }
202         // build options for the droppables
203         var options_for_droppable = {
204             overlap: options.overlap,
205             containment: options.containment,
206             hoverclass: options.hoverclass,
207             onhover: self.onHover,
208             tree: options.tree,
209             accept: options.accept
210         }
212         var options_for_tree = {
213             onhover: self.onEmptyHover,
214             overlap: options.overlap,
215             containment: options.containment,
216             hoverclass: options.hoverclass,
217             accept: options.accept
218         }
220         // fix for gecko engine
221         MochiKit.DOM.removeEmptyTextNodes(element);
223         options.draggables = [];
224         options.droppables = [];
226         // drop on empty handling
227         if (options.dropOnEmpty || options.tree) {
228             new MochiKit.DragAndDrop.Droppable(element, options_for_tree);
229             options.droppables.push(element);
230         }
231         MochiKit.Base.map(function (e) {
232             // handles are per-draggable
233             var handle = options.handle ?
234                 MochiKit.DOM.getFirstElementByTagAndClassName(null,
235                     options.handle, e) : e;
236             options.draggables.push(
237                 new MochiKit.DragAndDrop.Draggable(e,
238                     MochiKit.Base.update(options_for_draggable,
239                                          {handle: handle})));
240             new MochiKit.DragAndDrop.Droppable(e, options_for_droppable);
241             if (options.tree) {
242                 e.treeNode = element;
243             }
244             options.droppables.push(e);
245         }, (self.findElements(element, options) || []));
247         if (options.tree) {
248             MochiKit.Base.map(function (e) {
249                 new MochiKit.DragAndDrop.Droppable(e, options_for_tree);
250                 e.treeNode = element;
251                 options.droppables.push(e);
252             }, (self.findTreeElements(element, options) || []));
253         }
255         // keep reference
256         self.sortables[element.id] = options;
258         options.lastValue = self.serialize(element);
259         options.startHandle = MochiKit.Signal.connect(MochiKit.DragAndDrop.Draggables, 'start',
260                                 MochiKit.Base.partial(self.onStart, element));
261         options.endHandle = MochiKit.Signal.connect(MochiKit.DragAndDrop.Draggables, 'end',
262                                 MochiKit.Base.partial(self.onEnd, element));
263     },
265     /** @id MochiKit.Sortable.Sortable.onStart */
266     onStart: function (element, draggable) {
267         var self = MochiKit.Sortable.Sortable;
268         var options = self.options(element);
269         options.lastValue = self.serialize(options.element);
270     },
272     /** @id MochiKit.Sortable.Sortable.onEnd */
273     onEnd: function (element, draggable) {
274         var self = MochiKit.Sortable.Sortable;
275         self.unmark();
276         var options = self.options(element);
277         if (options.lastValue != self.serialize(options.element)) {
278             options.onUpdate(options.element);
279         }
280     },
282     // return all suitable-for-sortable elements in a guaranteed order
283     
284     /** @id MochiKit.Sortable.Sortable.findElements */
285     findElements: function (element, options) {
286         return MochiKit.Sortable.Sortable.findChildren(
287             element, options.only, options.tree ? true : false, options.tag);
288     },
290     /** @id MochiKit.Sortable.Sortable.findTreeElements */
291     findTreeElements: function (element, options) {
292         return MochiKit.Sortable.Sortable.findChildren(
293             element, options.only, options.tree ? true : false, options.treeTag);
294     },
296     /** @id MochiKit.Sortable.Sortable.findChildren */
297     findChildren: function (element, only, recursive, tagName) {
298         if (!element.hasChildNodes()) {
299             return null;
300         }
301         tagName = tagName.toUpperCase();
302         if (only) {
303             only = MochiKit.Base.flattenArray([only]);
304         }
305         var elements = [];
306         MochiKit.Base.map(function (e) {
307             if (e.tagName &&
308                 e.tagName.toUpperCase() == tagName &&
309                (!only ||
310                 MochiKit.Iter.some(only, function (c) {
311                     return MochiKit.DOM.hasElementClass(e, c);
312                 }))) {
313                 elements.push(e);
314             }
315             if (recursive) {
316                 var grandchildren = MochiKit.Sortable.Sortable.findChildren(e, only, recursive, tagName);
317                 if (grandchildren && grandchildren.length > 0) {
318                     elements = elements.concat(grandchildren);
319                 }
320             }
321         }, element.childNodes);
322         return elements;
323     },
325     /** @id MochiKit.Sortable.Sortable.onHover */
326     onHover: function (element, dropon, overlap) {
327         if (MochiKit.DOM.isParent(dropon, element)) {
328             return;
329         }
330         var self = MochiKit.Sortable.Sortable;
332         if (overlap > .33 && overlap < .66 && self.options(dropon).tree) {
333             return;
334         } else if (overlap > 0.5) {
335             self.mark(dropon, 'before');
336             if (dropon.previousSibling != element) {
337                 var oldParentNode = element.parentNode;
338                 element.style.visibility = 'hidden';  // fix gecko rendering
339                 dropon.parentNode.insertBefore(element, dropon);
340                 if (dropon.parentNode != oldParentNode) {
341                     self.options(oldParentNode).onChange(element);
342                 }
343                 self.options(dropon.parentNode).onChange(element);
344             }
345         } else {
346             self.mark(dropon, 'after');
347             var nextElement = dropon.nextSibling || null;
348             if (nextElement != element) {
349                 var oldParentNode = element.parentNode;
350                 element.style.visibility = 'hidden';  // fix gecko rendering
351                 dropon.parentNode.insertBefore(element, nextElement);
352                 if (dropon.parentNode != oldParentNode) {
353                     self.options(oldParentNode).onChange(element);
354                 }
355                 self.options(dropon.parentNode).onChange(element);
356             }
357         }
358     },
360     _offsetSize: function (element, type) {
361         if (type == 'vertical' || type == 'height') {
362             return element.offsetHeight;
363         } else {
364             return element.offsetWidth;
365         }
366     },
368     /** @id MochiKit.Sortable.Sortable.onEmptyHover */
369     onEmptyHover: function (element, dropon, overlap) {
370         var oldParentNode = element.parentNode;
371         var self = MochiKit.Sortable.Sortable;
372         var droponOptions = self.options(dropon);
374         if (!MochiKit.DOM.isParent(dropon, element)) {
375             var index;
377             var children = self.findElements(dropon, {tag: droponOptions.tag,
378                                                       only: droponOptions.only});
379             var child = null;
381             if (children) {
382                 var offset = self._offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
384                 for (index = 0; index < children.length; index += 1) {
385                     if (offset - self._offsetSize(children[index], droponOptions.overlap) >= 0) {
386                         offset -= self._offsetSize(children[index], droponOptions.overlap);
387                     } else if (offset - (self._offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
388                         child = index + 1 < children.length ? children[index + 1] : null;
389                         break;
390                     } else {
391                         child = children[index];
392                         break;
393                     }
394                 }
395             }
397             dropon.insertBefore(element, child);
399             self.options(oldParentNode).onChange(element);
400             droponOptions.onChange(element);
401         }
402     },
404     /** @id MochiKit.Sortable.Sortable.unmark */
405     unmark: function () {
406         var m = MochiKit.Sortable.Sortable._marker;
407         if (m) {
408             MochiKit.Style.hideElement(m);
409         }
410     },
412     /** @id MochiKit.Sortable.Sortable.mark */
413     mark: function (dropon, position) {
414         // mark on ghosting only
415         var d = MochiKit.DOM;
416         var self = MochiKit.Sortable.Sortable;
417         var sortable = self.options(dropon.parentNode);
418         if (sortable && !sortable.ghosting) {
419             return;
420         }
422         if (!self._marker) {
423             self._marker = d.getElement('dropmarker') ||
424                         document.createElement('DIV');
425             MochiKit.Style.hideElement(self._marker);
426             d.addElementClass(self._marker, 'dropmarker');
427             self._marker.style.position = 'absolute';
428             document.getElementsByTagName('body').item(0).appendChild(self._marker);
429         }
430         var offsets = MochiKit.Position.cumulativeOffset(dropon);
431         self._marker.style.left = offsets.x + 'px';
432         self._marker.style.top = offsets.y + 'px';
434         if (position == 'after') {
435             if (sortable.overlap == 'horizontal') {
436                 self._marker.style.left = (offsets.x + dropon.clientWidth) + 'px';
437             } else {
438                 self._marker.style.top = (offsets.y + dropon.clientHeight) + 'px';
439             }
440         }
441         MochiKit.Style.showElement(self._marker);
442     },
444     _tree: function (element, options, parent) {
445         var self = MochiKit.Sortable.Sortable;
446         var children = self.findElements(element, options) || [];
448         for (var i = 0; i < children.length; ++i) {
449             var match = children[i].id.match(options.format);
451             if (!match) {
452                 continue;
453             }
455             var child = {
456                 id: encodeURIComponent(match ? match[1] : null),
457                 element: element,
458                 parent: parent,
459                 children: [],
460                 position: parent.children.length,
461                 container: self._findChildrenElement(children[i], options.treeTag.toUpperCase())
462             }
464             /* Get the element containing the children and recurse over it */
465             if (child.container) {
466                 self._tree(child.container, options, child)
467             }
469             parent.children.push (child);
470         }
472         return parent;
473     },
475     /* Finds the first element of the given tag type within a parent element.
476        Used for finding the first LI[ST] within a L[IST]I[TEM].*/
477     _findChildrenElement: function (element, containerTag) {
478         if (element && element.hasChildNodes) {
479             containerTag = containerTag.toUpperCase();
480             for (var i = 0; i < element.childNodes.length; ++i) {
481                 if (element.childNodes[i].tagName.toUpperCase() == containerTag) {
482                     return element.childNodes[i];
483                 }
484             }
485         }
486         return null;
487     },
489     /** @id MochiKit.Sortable.Sortable.tree */
490     tree: function (element, options) {
491         element = MochiKit.DOM.getElement(element);
492         var sortableOptions = MochiKit.Sortable.Sortable.options(element);
493         options = MochiKit.Base.update({
494             tag: sortableOptions.tag,
495             treeTag: sortableOptions.treeTag,
496             only: sortableOptions.only,
497             name: element.id,
498             format: sortableOptions.format
499         }, options || {});
501         var root = {
502             id: null,
503             parent: null,
504             children: new Array,
505             container: element,
506             position: 0
507         }
509         return MochiKit.Sortable.Sortable._tree(element, options, root);
510     },
512     /**
513      * Specifies the sequence for the Sortable.
514      * @param {Node} element    Element to use as the Sortable.
515      * @param {Object} newSequence    New sequence to use.
516      * @param {Object} options    Options to use fro the Sortable.
517      */
518     setSequence: function (element, newSequence, options) {
519         var self = MochiKit.Sortable.Sortable;
520         var b = MochiKit.Base;
521         element = MochiKit.DOM.getElement(element);
522         options = b.update(self.options(element), options || {});
524         var nodeMap = {};
525         b.map(function (n) {
526             var m = n.id.match(options.format);
527             if (m) {
528                 nodeMap[m[1]] = [n, n.parentNode];
529             }
530             n.parentNode.removeChild(n);
531         }, self.findElements(element, options));
533         b.map(function (ident) {
534             var n = nodeMap[ident];
535             if (n) {
536                 n[1].appendChild(n[0]);
537                 delete nodeMap[ident];
538             }
539         }, newSequence);
540     },
542     /* Construct a [i] index for a particular node */
543     _constructIndex: function (node) {
544         var index = '';
545         do {
546             if (node.id) {
547                 index = '[' + node.position + ']' + index;
548             }
549         } while ((node = node.parent) != null);
550         return index;
551     },
553     /** @id MochiKit.Sortable.Sortable.sequence */
554     sequence: function (element, options) {
555         element = MochiKit.DOM.getElement(element);
556         var self = MochiKit.Sortable.Sortable;
557         var options = MochiKit.Base.update(self.options(element), options || {});
559         return MochiKit.Base.map(function (item) {
560             return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
561         }, MochiKit.DOM.getElement(self.findElements(element, options) || []));
562     },
564     /**
565      * Serializes the content of a Sortable. Useful to send this content through a XMLHTTPRequest. 
566      * These options override the Sortable options for the serialization only.
567      * @param {Node} element    Element to serialize.
568      * @param {Object} options    Serialization options.
569      */
570     serialize: function (element, options) {
571         element = MochiKit.DOM.getElement(element);
572         var self = MochiKit.Sortable.Sortable;
573         options = MochiKit.Base.update(self.options(element), options || {});
574         var name = encodeURIComponent(options.name || element.id);
576         if (options.tree) {
577             return MochiKit.Base.flattenArray(MochiKit.Base.map(function (item) {
578                 return [name + self._constructIndex(item) + "[id]=" +
579                 encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
580             }, self.tree(element, options).children)).join('&');
581         } else {
582             return MochiKit.Base.map(function (item) {
583                 return name + "[]=" + encodeURIComponent(item);
584             }, self.sequence(element, options)).join('&');
585         }
586     }