|
1 /*** |
|
2 Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) |
|
3 Mochi-ized By Thomas Herve (_firstname_@nimail.org) |
|
4 |
|
5 See scriptaculous.js for full license. |
|
6 |
|
7 ***/ |
|
8 |
|
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'); |
|
14 } |
|
15 |
|
16 if (typeof(JSAN) != 'undefined') { |
|
17 JSAN.use("MochiKit.Base", []); |
|
18 JSAN.use("MochiKit.DOM", []); |
|
19 JSAN.use("MochiKit.Iter", []); |
|
20 } |
|
21 |
|
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!"; |
|
30 } |
|
31 |
|
32 if (typeof(MochiKit.Sortable) == 'undefined') { |
|
33 MochiKit.Sortable = {}; |
|
34 } |
|
35 |
|
36 MochiKit.Sortable.NAME = 'MochiKit.Sortable'; |
|
37 MochiKit.Sortable.VERSION = '1.4'; |
|
38 |
|
39 MochiKit.Sortable.__repr__ = function () { |
|
40 return '[' + this.NAME + ' ' + this.VERSION + ']'; |
|
41 }; |
|
42 |
|
43 MochiKit.Sortable.toString = function () { |
|
44 return this.__repr__(); |
|
45 }; |
|
46 |
|
47 MochiKit.Sortable.EXPORT = [ |
|
48 ]; |
|
49 |
|
50 MochiKit.Sortable.EXPORT_OK = [ |
|
51 ]; |
|
52 |
|
53 MochiKit.Base.update(MochiKit.Sortable, { |
|
54 /*** |
|
55 |
|
56 Manage sortables. Mainly use the create function to add a sortable. |
|
57 |
|
58 ***/ |
|
59 sortables: {}, |
|
60 |
|
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 }, |
|
69 |
|
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 }, |
|
78 |
|
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; |
|
84 |
|
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); |
|
94 |
|
95 delete MochiKit.Sortable.sortables[s.element.id]; |
|
96 } |
|
97 }, |
|
98 |
|
99 /** @id MochiKit.Sortable.create */ |
|
100 create: function (element, options) { |
|
101 element = MochiKit.DOM.getElement(element); |
|
102 var self = MochiKit.Sortable; |
|
103 |
|
104 /** @id MochiKit.Sortable.options */ |
|
105 options = MochiKit.Base.update({ |
|
106 |
|
107 /** @id MochiKit.Sortable.element */ |
|
108 element: element, |
|
109 |
|
110 /** @id MochiKit.Sortable.tag */ |
|
111 tag: 'li', // assumes li children, override with tag: 'tagname' |
|
112 |
|
113 /** @id MochiKit.Sortable.dropOnEmpty */ |
|
114 dropOnEmpty: false, |
|
115 |
|
116 /** @id MochiKit.Sortable.tree */ |
|
117 tree: false, |
|
118 |
|
119 /** @id MochiKit.Sortable.treeTag */ |
|
120 treeTag: 'ul', |
|
121 |
|
122 /** @id MochiKit.Sortable.overlap */ |
|
123 overlap: 'vertical', // one of 'vertical', 'horizontal' |
|
124 |
|
125 /** @id MochiKit.Sortable.constraint */ |
|
126 constraint: 'vertical', // one of 'vertical', 'horizontal', false |
|
127 // also takes array of elements (or ids); or false |
|
128 |
|
129 /** @id MochiKit.Sortable.containment */ |
|
130 containment: [element], |
|
131 |
|
132 /** @id MochiKit.Sortable.handle */ |
|
133 handle: false, // or a CSS class |
|
134 |
|
135 /** @id MochiKit.Sortable.only */ |
|
136 only: false, |
|
137 |
|
138 /** @id MochiKit.Sortable.hoverclass */ |
|
139 hoverclass: null, |
|
140 |
|
141 /** @id MochiKit.Sortable.ghosting */ |
|
142 ghosting: false, |
|
143 |
|
144 /** @id MochiKit.Sortable.scroll */ |
|
145 scroll: false, |
|
146 |
|
147 /** @id MochiKit.Sortable.scrollSensitivity */ |
|
148 scrollSensitivity: 20, |
|
149 |
|
150 /** @id MochiKit.Sortable.scrollSpeed */ |
|
151 scrollSpeed: 15, |
|
152 |
|
153 /** @id MochiKit.Sortable.format */ |
|
154 format: /^[^_]*_(.*)$/, |
|
155 |
|
156 /** @id MochiKit.Sortable.onChange */ |
|
157 onChange: MochiKit.Base.noop, |
|
158 |
|
159 /** @id MochiKit.Sortable.onUpdate */ |
|
160 onUpdate: MochiKit.Base.noop, |
|
161 |
|
162 /** @id MochiKit.Sortable.accept */ |
|
163 accept: null |
|
164 }, options); |
|
165 |
|
166 // clear any old sortable with same element |
|
167 self.destroy(element); |
|
168 |
|
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 }; |
|
179 |
|
180 if (options.starteffect) { |
|
181 options_for_draggable.starteffect = options.starteffect; |
|
182 } |
|
183 |
|
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 } |
|
192 |
|
193 if (options.endeffect) { |
|
194 options_for_draggable.endeffect = options.endeffect; |
|
195 } |
|
196 |
|
197 if (options.zindex) { |
|
198 options_for_draggable.zindex = options.zindex; |
|
199 } |
|
200 |
|
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 } |
|
210 |
|
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 } |
|
218 |
|
219 // fix for gecko engine |
|
220 MochiKit.DOM.removeEmptyTextNodes(element); |
|
221 |
|
222 options.draggables = []; |
|
223 options.droppables = []; |
|
224 |
|
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) || [])); |
|
245 |
|
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 } |
|
253 |
|
254 // keep reference |
|
255 self.sortables[element.id] = options; |
|
256 |
|
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 }, |
|
263 |
|
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 }, |
|
270 |
|
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 }, |
|
280 |
|
281 // return all suitable-for-sortable elements in a guaranteed order |
|
282 |
|
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 }, |
|
288 |
|
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 }, |
|
294 |
|
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 }, |
|
323 |
|
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; |
|
330 |
|
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 }, |
|
358 |
|
359 _offsetSize: function (element, type) { |
|
360 if (type == 'vertical' || type == 'height') { |
|
361 return element.offsetHeight; |
|
362 } else { |
|
363 return element.offsetWidth; |
|
364 } |
|
365 }, |
|
366 |
|
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); |
|
372 |
|
373 if (!MochiKit.DOM.isParent(dropon, element)) { |
|
374 var index; |
|
375 |
|
376 var children = self.findElements(dropon, {tag: droponOptions.tag, |
|
377 only: droponOptions.only}); |
|
378 var child = null; |
|
379 |
|
380 if (children) { |
|
381 var offset = self._offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); |
|
382 |
|
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 } |
|
395 |
|
396 dropon.insertBefore(element, child); |
|
397 |
|
398 self.options(oldParentNode).onChange(element); |
|
399 droponOptions.onChange(element); |
|
400 } |
|
401 }, |
|
402 |
|
403 /** @id MochiKit.Sortable.unmark */ |
|
404 unmark: function () { |
|
405 var m = MochiKit.Sortable._marker; |
|
406 if (m) { |
|
407 MochiKit.Style.hideElement(m); |
|
408 } |
|
409 }, |
|
410 |
|
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 } |
|
420 |
|
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'; |
|
432 |
|
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 }, |
|
442 |
|
443 _tree: function (element, options, parent) { |
|
444 var self = MochiKit.Sortable; |
|
445 var children = self.findElements(element, options) || []; |
|
446 |
|
447 for (var i = 0; i < children.length; ++i) { |
|
448 var match = children[i].id.match(options.format); |
|
449 |
|
450 if (!match) { |
|
451 continue; |
|
452 } |
|
453 |
|
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 } |
|
462 |
|
463 /* Get the element containing the children and recurse over it */ |
|
464 if (child.container) { |
|
465 self._tree(child.container, options, child) |
|
466 } |
|
467 |
|
468 parent.children.push (child); |
|
469 } |
|
470 |
|
471 return parent; |
|
472 }, |
|
473 |
|
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 }, |
|
487 |
|
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 || {}); |
|
499 |
|
500 var root = { |
|
501 id: null, |
|
502 parent: null, |
|
503 children: new Array, |
|
504 container: element, |
|
505 position: 0 |
|
506 } |
|
507 |
|
508 return MochiKit.Sortable._tree(element, options, root); |
|
509 }, |
|
510 |
|
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 || {}); |
|
522 |
|
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)); |
|
531 |
|
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 }, |
|
540 |
|
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 }, |
|
551 |
|
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 || {}); |
|
557 |
|
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 }, |
|
562 |
|
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); |
|
574 |
|
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 } |
|
586 }); |
|
587 |
|
588 // trunk compatibility |
|
589 MochiKit.Sortable.Sortable = MochiKit.Sortable; |