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.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", []);
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
.Sortable
.EXPORT_OK
= [
53 MochiKit
.Base
.update(MochiKit
.Sortable
, {
56 Manage sortables. Mainly use the create function to add a sortable.
61 _findRootElement
: function (element
) {
62 while (element
.tagName
.toUpperCase() != "BODY") {
63 if (element
.id
&& MochiKit
.Sortable
.sortables
[element
.id
]) {
66 element
= element
.parentNode
;
70 /** @id MochiKit.Sortable.options */
71 options
: function (element
) {
72 element
= MochiKit
.Sortable
._findRootElement(MochiKit
.DOM
.getElement(element
));
76 return MochiKit
.Sortable
.sortables
[element
.id
];
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
;
86 MochiKit
.Signal
.disconnect(s
.startHandle
);
87 MochiKit
.Signal
.disconnect(s
.endHandle
);
89 d
.Droppables
.remove(dr
);
95 delete MochiKit
.Sortable
.sortables
[s
.element
.id
];
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 */
110 /** @id MochiKit.Sortable.tag */
111 tag
: 'li', // assumes li children, override with tag: 'tagname'
113 /** @id MochiKit.Sortable.dropOnEmpty */
116 /** @id MochiKit.Sortable.tree */
119 /** @id MochiKit.Sortable.treeTag */
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 */
138 /** @id MochiKit.Sortable.hoverclass */
141 /** @id MochiKit.Sortable.ghosting */
144 /** @id MochiKit.Sortable.scroll */
147 /** @id MochiKit.Sortable.scrollSensitivity */
148 scrollSensitivity
: 20,
150 /** @id MochiKit.Sortable.scrollSpeed */
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 */
166 // clear any old sortable with same element
167 self
.destroy(element
);
169 // build options for the draggables
170 var options_for_draggable
= {
172 ghosting
: options
.ghosting
,
173 scroll
: options
.scroll
,
174 scrollSensitivity
: options
.scrollSensitivity
,
175 scrollSpeed
: options
.scrollSpeed
,
176 constraint
: options
.constraint
,
177 handle
: options
.handle
180 if (options
.starteffect
) {
181 options_for_draggable
.starteffect
= options
.starteffect
;
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;
193 if (options
.endeffect
) {
194 options_for_draggable
.endeffect
= options
.endeffect
;
197 if (options
.zindex
) {
198 options_for_draggable
.zindex
= options
.zindex
;
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
,
208 accept
: options
.accept
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
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
);
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
,
239 new MochiKit
.DragAndDrop
.Droppable(e
, options_for_droppable
);
241 e
.treeNode
= element
;
243 options
.droppables
.push(e
);
244 }, (self
.findElements(element
, options
) || []));
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
) || []));
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
));
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
);
271 /** @id MochiKit.Sortable.onEnd */
272 onEnd
: function (element
, draggable
) {
273 var self
= MochiKit
.Sortable
;
275 var options
= self
.options(element
);
276 if (options
.lastValue
!= self
.serialize(options
.element
)) {
277 options
.onUpdate(options
.element
);
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
);
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
);
295 /** @id MochiKit.Sortable.findChildren */
296 findChildren
: function (element
, only
, recursive
, tagName
) {
297 if (!element
.hasChildNodes()) {
300 tagName
= tagName
.toUpperCase();
302 only
= MochiKit
.Base
.flattenArray([only
]);
305 MochiKit
.Base
.map(function (e
) {
307 e
.tagName
.toUpperCase() == tagName
&&
309 MochiKit
.Iter
.some(only
, function (c
) {
310 return MochiKit
.DOM
.hasElementClass(e
, c
);
315 var grandchildren
= MochiKit
.Sortable
.findChildren(e
, only
, recursive
, tagName
);
316 if (grandchildren
&& grandchildren
.length
> 0) {
317 elements
= elements
.concat(grandchildren
);
320 }, element
.childNodes
);
324 /** @id MochiKit.Sortable.onHover */
325 onHover
: function (element
, dropon
, overlap
) {
326 if (MochiKit
.DOM
.isParent(dropon
, element
)) {
329 var self
= MochiKit
.Sortable
;
331 if (overlap
> .33 && overlap
< .66 && self
.options(dropon
).tree
) {
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
);
342 self
.options(dropon
.parentNode
).onChange(element
);
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
);
354 self
.options(dropon
.parentNode
).onChange(element
);
359 _offsetSize
: function (element
, type
) {
360 if (type
== 'vertical' || type
== 'height') {
361 return element
.offsetHeight
;
363 return element
.offsetWidth
;
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
)) {
376 var children
= self
.findElements(dropon
, {tag
: droponOptions
.tag
,
377 only
: droponOptions
.only
});
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;
390 child
= children
[index
];
396 dropon
.insertBefore(element
, child
);
398 self
.options(oldParentNode
).onChange(element
);
399 droponOptions
.onChange(element
);
403 /** @id MochiKit.Sortable.unmark */
404 unmark
: function () {
405 var m
= MochiKit
.Sortable
._marker
;
407 MochiKit
.Style
.hideElement(m
);
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
) {
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
);
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';
437 self
._marker
.style
.top
= (offsets
.y
+ dropon
.clientHeight
) + 'px';
440 MochiKit
.Style
.showElement(self
._marker
);
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
);
455 id
: encodeURIComponent(match
? match
[1] : null),
459 position
: parent
.children
.length
,
460 container
: self
._findChildrenElement(children
[i
], options
.treeTag
.toUpperCase())
463 /* Get the element containing the children and recurse over it */
464 if (child
.container
) {
465 self
._tree(child
.container
, options
, child
)
468 parent
.children
.push (child
);
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
];
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
,
497 format
: sortableOptions
.format
508 return MochiKit
.Sortable
._tree(element
, options
, root
);
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.
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
|| {});
525 var m
= n
.id
.match(options
.format
);
527 nodeMap
[m
[1]] = [n
, n
.parentNode
];
529 n
.parentNode
.removeChild(n
);
530 }, self
.findElements(element
, options
));
532 b
.map(function (ident
) {
533 var n
= nodeMap
[ident
];
535 n
[1].appendChild(n
[0]);
536 delete nodeMap
[ident
];
541 /* Construct a [i] index for a particular node */
542 _constructIndex
: function (node
) {
546 index
= '[' + node
.position
+ ']' + index
;
548 } while ((node
= node
.parent
) != null);
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
) || []));
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.
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
);
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('&');
581 return MochiKit
.Base
.map(function (item
) {
582 return name
+ "[]=" + encodeURIComponent(item
);
583 }, self
.sequence(element
, options
)).join('&');
588 // trunk compatibility
589 MochiKit
.Sortable
.Sortable
= MochiKit
.Sortable
;