Commit | Line | Data |
---|---|---|
6a1aa64f DV |
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; |