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.
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", []);
23 if (typeof(MochiKit.Base) == 'undefined' ||
24 typeof(MochiKit.DOM) == 'undefined' ||
25 typeof(MochiKit.Iter) == 'undefined') {
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 = [
54 MochiKit.Sortable.Sortable = {
57 Manage sortables. Mainly use the create function to add a sortable.
62 _findRootElement: function (element) {
63 while (element.tagName.toUpperCase() != "BODY") {
64 if (element.id && MochiKit.Sortable.Sortable.sortables[element.id]) {
67 element = element.parentNode;
71 /** @id MochiKit.Sortable.Sortable.options */
72 options: function (element) {
73 element = MochiKit.Sortable.Sortable._findRootElement(MochiKit.DOM.getElement(element));
77 return MochiKit.Sortable.Sortable.sortables[element.id];
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;
87 MochiKit.Signal.disconnect(s.startHandle);
88 MochiKit.Signal.disconnect(s.endHandle);
90 d.Droppables.remove(dr);
96 delete MochiKit.Sortable.Sortable.sortables[s.element.id];
100 /** @id MochiKit.Sortable.Sortable.create */
101 create: function (element, options) {
102 element = MochiKit.DOM.getElement(element);
103 var self = MochiKit.Sortable.Sortable;
105 /** @id MochiKit.Sortable.Sortable.options */
106 options = MochiKit.Base.update({
108 /** @id MochiKit.Sortable.Sortable.element */
111 /** @id MochiKit.Sortable.Sortable.tag */
112 tag: 'li', // assumes li children, override with tag: 'tagname'
114 /** @id MochiKit.Sortable.Sortable.dropOnEmpty */
117 /** @id MochiKit.Sortable.Sortable.tree */
120 /** @id MochiKit.Sortable.Sortable.treeTag */
123 /** @id MochiKit.Sortable.Sortable.overlap */
124 overlap: 'vertical', // one of 'vertical', 'horizontal'
126 /** @id MochiKit.Sortable.Sortable.constraint */
127 constraint: 'vertical', // one of 'vertical', 'horizontal', false
128 // also takes array of elements (or ids); or false
130 /** @id MochiKit.Sortable.Sortable.containment */
131 containment: [element],
133 /** @id MochiKit.Sortable.Sortable.handle */
134 handle: false, // or a CSS class
136 /** @id MochiKit.Sortable.Sortable.only */
139 /** @id MochiKit.Sortable.Sortable.hoverclass */
142 /** @id MochiKit.Sortable.Sortable.ghosting */
145 /** @id MochiKit.Sortable.Sortable.scroll */
148 /** @id MochiKit.Sortable.Sortable.scrollSensitivity */
149 scrollSensitivity: 20,
151 /** @id MochiKit.Sortable.Sortable.scrollSpeed */
154 /** @id MochiKit.Sortable.Sortable.format */
155 format: /^[^_]*_(.*)$/,
157 /** @id MochiKit.Sortable.Sortable.onChange */
158 onChange: MochiKit.Base.noop,
160 /** @id MochiKit.Sortable.Sortable.onUpdate */
161 onUpdate: MochiKit.Base.noop,
163 /** @id MochiKit.Sortable.Sortable.accept */
167 // clear any old sortable with same element
168 self.destroy(element);
170 // build options for the draggables
171 var options_for_draggable = {
173 ghosting: options.ghosting,
174 scroll: options.scroll,
175 scrollSensitivity: options.scrollSensitivity,
176 scrollSpeed: options.scrollSpeed,
177 constraint: options.constraint,
178 handle: options.handle
181 if (options.starteffect) {
182 options_for_draggable.starteffect = options.starteffect;
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;
194 if (options.endeffect) {
195 options_for_draggable.endeffect = options.endeffect;
198 if (options.zindex) {
199 options_for_draggable.zindex = options.zindex;
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,
209 accept: options.accept
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
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);
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,
240 new MochiKit.DragAndDrop.Droppable(e, options_for_droppable);
242 e.treeNode = element;
244 options.droppables.push(e);
245 }, (self.findElements(element, options) || []));
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) || []));
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));
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);
272 /** @id MochiKit.Sortable.Sortable.onEnd */
273 onEnd: function (element, draggable) {
274 var self = MochiKit.Sortable.Sortable;
276 var options = self.options(element);
277 if (options.lastValue != self.serialize(options.element)) {
278 options.onUpdate(options.element);
282 // return all suitable-for-sortable elements in a guaranteed order
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);
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);
296 /** @id MochiKit.Sortable.Sortable.findChildren */
297 findChildren: function (element, only, recursive, tagName) {
298 if (!element.hasChildNodes()) {
301 tagName = tagName.toUpperCase();
303 only = MochiKit.Base.flattenArray([only]);
306 MochiKit.Base.map(function (e) {
308 e.tagName.toUpperCase() == tagName &&
310 MochiKit.Iter.some(only, function (c) {
311 return MochiKit.DOM.hasElementClass(e, c);
316 var grandchildren = MochiKit.Sortable.Sortable.findChildren(e, only, recursive, tagName);
317 if (grandchildren && grandchildren.length > 0) {
318 elements = elements.concat(grandchildren);
321 }, element.childNodes);
325 /** @id MochiKit.Sortable.Sortable.onHover */
326 onHover: function (element, dropon, overlap) {
327 if (MochiKit.DOM.isParent(dropon, element)) {
330 var self = MochiKit.Sortable.Sortable;
332 if (overlap > .33 && overlap < .66 && self.options(dropon).tree) {
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);
343 self.options(dropon.parentNode).onChange(element);
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);
355 self.options(dropon.parentNode).onChange(element);
360 _offsetSize: function (element, type) {
361 if (type == 'vertical' || type == 'height') {
362 return element.offsetHeight;
364 return element.offsetWidth;
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)) {
377 var children = self.findElements(dropon, {tag: droponOptions.tag,
378 only: droponOptions.only});
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;
391 child = children[index];
397 dropon.insertBefore(element, child);
399 self.options(oldParentNode).onChange(element);
400 droponOptions.onChange(element);
404 /** @id MochiKit.Sortable.Sortable.unmark */
405 unmark: function () {
406 var m = MochiKit.Sortable.Sortable._marker;
408 MochiKit.Style.hideElement(m);
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) {
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);
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';
438 self._marker.style.top = (offsets.y + dropon.clientHeight) + 'px';
441 MochiKit.Style.showElement(self._marker);
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);
456 id: encodeURIComponent(match ? match[1] : null),
460 position: parent.children.length,
461 container: self._findChildrenElement(children[i], options.treeTag.toUpperCase())
464 /* Get the element containing the children and recurse over it */
465 if (child.container) {
466 self._tree(child.container, options, child)
469 parent.children.push (child);
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];
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,
498 format: sortableOptions.format
509 return MochiKit.Sortable.Sortable._tree(element, options, root);
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.
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 || {});
526 var m = n.id.match(options.format);
528 nodeMap[m[1]] = [n, n.parentNode];
530 n.parentNode.removeChild(n);
531 }, self.findElements(element, options));
533 b.map(function (ident) {
534 var n = nodeMap[ident];
536 n[1].appendChild(n[0]);
537 delete nodeMap[ident];
542 /* Construct a [i] index for a particular node */
543 _constructIndex: function (node) {
547 index = '[' + node.position + ']' + index;
549 } while ((node = node.parent) != null);
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) || []));
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.
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);
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('&');
582 return MochiKit.Base.map(function (item) {
583 return name + "[]=" + encodeURIComponent(item);
584 }, self.sequence(element, options)).join('&');