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
MochiKit.Base._deps('Sortable', ['Base', 'Iter', 'DOM', 'Position', 'DragAndDrop']);
11
MochiKit.Sortable.NAME = 'MochiKit.Sortable';
12
MochiKit.Sortable.VERSION = '1.4.2';
14
MochiKit.Sortable.__repr__ = function () {
15
return '[' + this.NAME + ' ' + this.VERSION + ']';
18
MochiKit.Sortable.toString = function () {
19
return this.__repr__();
22
MochiKit.Sortable.EXPORT = [
25
MochiKit.Sortable.EXPORT_OK = [
28
MochiKit.Base.update(MochiKit.Sortable, {
31
Manage sortables. Mainly use the create function to add a sortable.
36
_findRootElement: function (element) {
37
while (element.tagName.toUpperCase() != "BODY") {
38
if (element.id && MochiKit.Sortable.sortables[element.id]) {
41
element = element.parentNode;
45
_createElementId: function(element) {
46
if (element.id == null || element.id == "") {
50
while (d.getElement(id = "sortable" + count) != null) {
53
d.setNodeAttribute(element, "id", id);
57
/** @id MochiKit.Sortable.options */
58
options: function (element) {
59
element = MochiKit.Sortable._findRootElement(MochiKit.DOM.getElement(element));
63
return MochiKit.Sortable.sortables[element.id];
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;
73
MochiKit.Signal.disconnect(s.startHandle);
74
MochiKit.Signal.disconnect(s.endHandle);
76
d.Droppables.remove(dr);
82
delete MochiKit.Sortable.sortables[s.element.id];
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);
92
/** @id MochiKit.Sortable.options */
93
options = MochiKit.Base.update({
95
/** @id MochiKit.Sortable.element */
98
/** @id MochiKit.Sortable.tag */
99
tag: 'li', // assumes li children, override with tag: 'tagname'
101
/** @id MochiKit.Sortable.dropOnEmpty */
104
/** @id MochiKit.Sortable.tree */
107
/** @id MochiKit.Sortable.treeTag */
110
/** @id MochiKit.Sortable.overlap */
111
overlap: 'vertical', // one of 'vertical', 'horizontal'
113
/** @id MochiKit.Sortable.constraint */
114
constraint: 'vertical', // one of 'vertical', 'horizontal', false
115
// also takes array of elements (or ids); or false
117
/** @id MochiKit.Sortable.containment */
118
containment: [element],
120
/** @id MochiKit.Sortable.handle */
121
handle: false, // or a CSS class
123
/** @id MochiKit.Sortable.only */
126
/** @id MochiKit.Sortable.hoverclass */
129
/** @id MochiKit.Sortable.ghosting */
132
/** @id MochiKit.Sortable.scroll */
135
/** @id MochiKit.Sortable.scrollSensitivity */
136
scrollSensitivity: 20,
138
/** @id MochiKit.Sortable.scrollSpeed */
141
/** @id MochiKit.Sortable.format */
142
format: /^[^_]*_(.*)$/,
144
/** @id MochiKit.Sortable.onChange */
145
onChange: MochiKit.Base.noop,
147
/** @id MochiKit.Sortable.onUpdate */
148
onUpdate: MochiKit.Base.noop,
150
/** @id MochiKit.Sortable.accept */
154
// clear any old sortable with same element
155
self.destroy(element);
157
// build options for the draggables
158
var options_for_draggable = {
160
ghosting: options.ghosting,
161
scroll: options.scroll,
162
scrollSensitivity: options.scrollSensitivity,
163
scrollSpeed: options.scrollSpeed,
164
constraint: options.constraint,
165
handle: options.handle
168
if (options.starteffect) {
169
options_for_draggable.starteffect = options.starteffect;
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;
181
if (options.endeffect) {
182
options_for_draggable.endeffect = options.endeffect;
185
if (options.zindex) {
186
options_for_draggable.zindex = options.zindex;
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,
196
accept: options.accept
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
207
// fix for gecko engine
208
MochiKit.DOM.removeEmptyTextNodes(element);
210
options.draggables = [];
211
options.droppables = [];
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);
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,
227
new MochiKit.DragAndDrop.Droppable(e, options_for_droppable);
229
e.treeNode = element;
231
options.droppables.push(e);
232
}, (self.findElements(element, options) || []));
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) || []));
243
self.sortables[element.id] = options;
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));
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);
259
/** @id MochiKit.Sortable.onEnd */
260
onEnd: function (element, draggable) {
261
var self = MochiKit.Sortable;
263
var options = self.options(element);
264
if (options.lastValue != self.serialize(options.element)) {
265
options.onUpdate(options.element);
269
// return all suitable-for-sortable elements in a guaranteed order
271
/** @id MochiKit.Sortable.findElements */
272
findElements: function (element, options) {
273
return MochiKit.Sortable.findChildren(element, options.only, options.tree, options.tag);
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);
282
/** @id MochiKit.Sortable.findChildren */
283
findChildren: function (element, only, recursive, tagName) {
284
if (!element.hasChildNodes()) {
287
tagName = tagName.toUpperCase();
289
only = MochiKit.Base.flattenArray([only]);
292
MochiKit.Base.map(function (e) {
294
e.tagName.toUpperCase() == tagName &&
296
MochiKit.Iter.some(only, function (c) {
297
return MochiKit.DOM.hasElementClass(e, c);
302
var grandchildren = MochiKit.Sortable.findChildren(e, only, recursive, tagName);
303
if (grandchildren && grandchildren.length > 0) {
304
elements = elements.concat(grandchildren);
307
}, element.childNodes);
311
/** @id MochiKit.Sortable.onHover */
312
onHover: function (element, dropon, overlap) {
313
if (MochiKit.DOM.isChildNode(dropon, element)) {
316
var self = MochiKit.Sortable;
318
if (overlap > .33 && overlap < .66 && self.options(dropon).tree) {
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);
329
self.options(dropon.parentNode).onChange(element);
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);
341
self.options(dropon.parentNode).onChange(element);
346
_offsetSize: function (element, type) {
347
if (type == 'vertical' || type == 'height') {
348
return element.offsetHeight;
350
return element.offsetWidth;
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);
360
if (!MochiKit.DOM.isChildNode(dropon, element)) {
363
var children = self.findElements(dropon, {tag: droponOptions.tag,
364
only: droponOptions.only});
368
var offset = self._offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
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;
377
child = children[index];
383
dropon.insertBefore(element, child);
385
self.options(oldParentNode).onChange(element);
386
droponOptions.onChange(element);
390
/** @id MochiKit.Sortable.unmark */
391
unmark: function () {
392
var m = MochiKit.Sortable._marker;
394
MochiKit.Style.hideElement(m);
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) {
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);
416
var offsets = MochiKit.Position.cumulativeOffset(dropon);
417
self._marker.style.left = offsets.x + 'px';
418
self._marker.style.top = offsets.y + 'px';
420
if (position == 'after') {
421
if (sortable.overlap == 'horizontal') {
422
self._marker.style.left = (offsets.x + dropon.clientWidth) + 'px';
424
self._marker.style.top = (offsets.y + dropon.clientHeight) + 'px';
427
MochiKit.Style.showElement(self._marker);
430
_tree: function (element, options, parent) {
431
var self = MochiKit.Sortable;
432
var children = self.findElements(element, options) || [];
434
for (var i = 0; i < children.length; ++i) {
435
var match = children[i].id.match(options.format);
442
id: encodeURIComponent(match ? match[1] : null),
446
position: parent.children.length,
447
container: self._findChildrenElement(children[i], options.treeTag.toUpperCase())
450
/* Get the element containing the children and recurse over it */
451
if (child.container) {
452
self._tree(child.container, options, child)
455
parent.children.push (child);
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];
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,
484
format: sortableOptions.format
495
return MochiKit.Sortable._tree(element, options, root);
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.
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 || {});
512
var m = n.id.match(options.format);
514
nodeMap[m[1]] = [n, n.parentNode];
516
n.parentNode.removeChild(n);
517
}, self.findElements(element, options));
519
b.map(function (ident) {
520
var n = nodeMap[ident];
522
n[1].appendChild(n[0]);
523
delete nodeMap[ident];
528
/* Construct a [i] index for a particular node */
529
_constructIndex: function (node) {
533
index = '[' + node.position + ']' + index;
535
} while ((node = node.parent) != null);
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 || {});
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) || []));
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.
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);
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('&');
568
return MochiKit.Base.map(function (item) {
569
return name + "[]=" + encodeURIComponent(item);
570
}, self.sequence(element, options)).join('&');
575
// trunk compatibility
576
MochiKit.Sortable.Sortable = MochiKit.Sortable;
578
MochiKit.Sortable.__new__ = function () {
579
MochiKit.Base.nameFunctions(this);
582
":common": this.EXPORT,
583
":all": MochiKit.Base.concat(this.EXPORT, this.EXPORT_OK)
587
MochiKit.Sortable.__new__();
589
MochiKit.Base._exportSymbols(this, MochiKit.Sortable);