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