michael@0: /*** michael@0: Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) michael@0: Mochi-ized By Thomas Herve (_firstname_@nimail.org) michael@0: michael@0: See scriptaculous.js for full license. michael@0: michael@0: ***/ michael@0: michael@0: if (typeof(dojo) != 'undefined') { michael@0: dojo.provide('MochiKit.DragAndDrop'); michael@0: dojo.require('MochiKit.Base'); michael@0: dojo.require('MochiKit.DOM'); michael@0: dojo.require('MochiKit.Iter'); michael@0: } michael@0: michael@0: if (typeof(JSAN) != 'undefined') { michael@0: JSAN.use("MochiKit.Base", []); michael@0: JSAN.use("MochiKit.DOM", []); michael@0: JSAN.use("MochiKit.Iter", []); michael@0: } michael@0: michael@0: try { michael@0: if (typeof(MochiKit.Base) == 'undefined' || michael@0: typeof(MochiKit.DOM) == 'undefined' || michael@0: typeof(MochiKit.Iter) == 'undefined') { michael@0: throw ""; michael@0: } michael@0: } catch (e) { michael@0: throw "MochiKit.DragAndDrop depends on MochiKit.Base, MochiKit.DOM and MochiKit.Iter!"; michael@0: } michael@0: michael@0: if (typeof(MochiKit.Sortable) == 'undefined') { michael@0: MochiKit.Sortable = {}; michael@0: } michael@0: michael@0: MochiKit.Sortable.NAME = 'MochiKit.Sortable'; michael@0: MochiKit.Sortable.VERSION = '1.4'; michael@0: michael@0: MochiKit.Sortable.__repr__ = function () { michael@0: return '[' + this.NAME + ' ' + this.VERSION + ']'; michael@0: }; michael@0: michael@0: MochiKit.Sortable.toString = function () { michael@0: return this.__repr__(); michael@0: }; michael@0: michael@0: MochiKit.Sortable.EXPORT = [ michael@0: ]; michael@0: michael@0: MochiKit.DragAndDrop.EXPORT_OK = [ michael@0: "Sortable" michael@0: ]; michael@0: michael@0: MochiKit.Sortable.Sortable = { michael@0: /*** michael@0: michael@0: Manage sortables. Mainly use the create function to add a sortable. michael@0: michael@0: ***/ michael@0: sortables: {}, michael@0: michael@0: _findRootElement: function (element) { michael@0: while (element.tagName.toUpperCase() != "BODY") { michael@0: if (element.id && MochiKit.Sortable.Sortable.sortables[element.id]) { michael@0: return element; michael@0: } michael@0: element = element.parentNode; michael@0: } michael@0: }, michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.options */ michael@0: options: function (element) { michael@0: element = MochiKit.Sortable.Sortable._findRootElement(MochiKit.DOM.getElement(element)); michael@0: if (!element) { michael@0: return; michael@0: } michael@0: return MochiKit.Sortable.Sortable.sortables[element.id]; michael@0: }, michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.destroy */ michael@0: destroy: function (element){ michael@0: var s = MochiKit.Sortable.Sortable.options(element); michael@0: var b = MochiKit.Base; michael@0: var d = MochiKit.DragAndDrop; michael@0: michael@0: if (s) { michael@0: MochiKit.Signal.disconnect(s.startHandle); michael@0: MochiKit.Signal.disconnect(s.endHandle); michael@0: b.map(function (dr) { michael@0: d.Droppables.remove(dr); michael@0: }, s.droppables); michael@0: b.map(function (dr) { michael@0: dr.destroy(); michael@0: }, s.draggables); michael@0: michael@0: delete MochiKit.Sortable.Sortable.sortables[s.element.id]; michael@0: } michael@0: }, michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.create */ michael@0: create: function (element, options) { michael@0: element = MochiKit.DOM.getElement(element); michael@0: var self = MochiKit.Sortable.Sortable; michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.options */ michael@0: options = MochiKit.Base.update({ michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.element */ michael@0: element: element, michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.tag */ michael@0: tag: 'li', // assumes li children, override with tag: 'tagname' michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.dropOnEmpty */ michael@0: dropOnEmpty: false, michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.tree */ michael@0: tree: false, michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.treeTag */ michael@0: treeTag: 'ul', michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.overlap */ michael@0: overlap: 'vertical', // one of 'vertical', 'horizontal' michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.constraint */ michael@0: constraint: 'vertical', // one of 'vertical', 'horizontal', false michael@0: // also takes array of elements (or ids); or false michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.containment */ michael@0: containment: [element], michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.handle */ michael@0: handle: false, // or a CSS class michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.only */ michael@0: only: false, michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.hoverclass */ michael@0: hoverclass: null, michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.ghosting */ michael@0: ghosting: false, michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.scroll */ michael@0: scroll: false, michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.scrollSensitivity */ michael@0: scrollSensitivity: 20, michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.scrollSpeed */ michael@0: scrollSpeed: 15, michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.format */ michael@0: format: /^[^_]*_(.*)$/, michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.onChange */ michael@0: onChange: MochiKit.Base.noop, michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.onUpdate */ michael@0: onUpdate: MochiKit.Base.noop, michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.accept */ michael@0: accept: null michael@0: }, options); michael@0: michael@0: // clear any old sortable with same element michael@0: self.destroy(element); michael@0: michael@0: // build options for the draggables michael@0: var options_for_draggable = { michael@0: revert: true, michael@0: ghosting: options.ghosting, michael@0: scroll: options.scroll, michael@0: scrollSensitivity: options.scrollSensitivity, michael@0: scrollSpeed: options.scrollSpeed, michael@0: constraint: options.constraint, michael@0: handle: options.handle michael@0: }; michael@0: michael@0: if (options.starteffect) { michael@0: options_for_draggable.starteffect = options.starteffect; michael@0: } michael@0: michael@0: if (options.reverteffect) { michael@0: options_for_draggable.reverteffect = options.reverteffect; michael@0: } else if (options.ghosting) { michael@0: options_for_draggable.reverteffect = function (innerelement) { michael@0: innerelement.style.top = 0; michael@0: innerelement.style.left = 0; michael@0: }; michael@0: } michael@0: michael@0: if (options.endeffect) { michael@0: options_for_draggable.endeffect = options.endeffect; michael@0: } michael@0: michael@0: if (options.zindex) { michael@0: options_for_draggable.zindex = options.zindex; michael@0: } michael@0: michael@0: // build options for the droppables michael@0: var options_for_droppable = { michael@0: overlap: options.overlap, michael@0: containment: options.containment, michael@0: hoverclass: options.hoverclass, michael@0: onhover: self.onHover, michael@0: tree: options.tree, michael@0: accept: options.accept michael@0: } michael@0: michael@0: var options_for_tree = { michael@0: onhover: self.onEmptyHover, michael@0: overlap: options.overlap, michael@0: containment: options.containment, michael@0: hoverclass: options.hoverclass, michael@0: accept: options.accept michael@0: } michael@0: michael@0: // fix for gecko engine michael@0: MochiKit.DOM.removeEmptyTextNodes(element); michael@0: michael@0: options.draggables = []; michael@0: options.droppables = []; michael@0: michael@0: // drop on empty handling michael@0: if (options.dropOnEmpty || options.tree) { michael@0: new MochiKit.DragAndDrop.Droppable(element, options_for_tree); michael@0: options.droppables.push(element); michael@0: } michael@0: MochiKit.Base.map(function (e) { michael@0: // handles are per-draggable michael@0: var handle = options.handle ? michael@0: MochiKit.DOM.getFirstElementByTagAndClassName(null, michael@0: options.handle, e) : e; michael@0: options.draggables.push( michael@0: new MochiKit.DragAndDrop.Draggable(e, michael@0: MochiKit.Base.update(options_for_draggable, michael@0: {handle: handle}))); michael@0: new MochiKit.DragAndDrop.Droppable(e, options_for_droppable); michael@0: if (options.tree) { michael@0: e.treeNode = element; michael@0: } michael@0: options.droppables.push(e); michael@0: }, (self.findElements(element, options) || [])); michael@0: michael@0: if (options.tree) { michael@0: MochiKit.Base.map(function (e) { michael@0: new MochiKit.DragAndDrop.Droppable(e, options_for_tree); michael@0: e.treeNode = element; michael@0: options.droppables.push(e); michael@0: }, (self.findTreeElements(element, options) || [])); michael@0: } michael@0: michael@0: // keep reference michael@0: self.sortables[element.id] = options; michael@0: michael@0: options.lastValue = self.serialize(element); michael@0: options.startHandle = MochiKit.Signal.connect(MochiKit.DragAndDrop.Draggables, 'start', michael@0: MochiKit.Base.partial(self.onStart, element)); michael@0: options.endHandle = MochiKit.Signal.connect(MochiKit.DragAndDrop.Draggables, 'end', michael@0: MochiKit.Base.partial(self.onEnd, element)); michael@0: }, michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.onStart */ michael@0: onStart: function (element, draggable) { michael@0: var self = MochiKit.Sortable.Sortable; michael@0: var options = self.options(element); michael@0: options.lastValue = self.serialize(options.element); michael@0: }, michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.onEnd */ michael@0: onEnd: function (element, draggable) { michael@0: var self = MochiKit.Sortable.Sortable; michael@0: self.unmark(); michael@0: var options = self.options(element); michael@0: if (options.lastValue != self.serialize(options.element)) { michael@0: options.onUpdate(options.element); michael@0: } michael@0: }, michael@0: michael@0: // return all suitable-for-sortable elements in a guaranteed order michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.findElements */ michael@0: findElements: function (element, options) { michael@0: return MochiKit.Sortable.Sortable.findChildren( michael@0: element, options.only, options.tree ? true : false, options.tag); michael@0: }, michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.findTreeElements */ michael@0: findTreeElements: function (element, options) { michael@0: return MochiKit.Sortable.Sortable.findChildren( michael@0: element, options.only, options.tree ? true : false, options.treeTag); michael@0: }, michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.findChildren */ michael@0: findChildren: function (element, only, recursive, tagName) { michael@0: if (!element.hasChildNodes()) { michael@0: return null; michael@0: } michael@0: tagName = tagName.toUpperCase(); michael@0: if (only) { michael@0: only = MochiKit.Base.flattenArray([only]); michael@0: } michael@0: var elements = []; michael@0: MochiKit.Base.map(function (e) { michael@0: if (e.tagName && michael@0: e.tagName.toUpperCase() == tagName && michael@0: (!only || michael@0: MochiKit.Iter.some(only, function (c) { michael@0: return MochiKit.DOM.hasElementClass(e, c); michael@0: }))) { michael@0: elements.push(e); michael@0: } michael@0: if (recursive) { michael@0: var grandchildren = MochiKit.Sortable.Sortable.findChildren(e, only, recursive, tagName); michael@0: if (grandchildren && grandchildren.length > 0) { michael@0: elements = elements.concat(grandchildren); michael@0: } michael@0: } michael@0: }, element.childNodes); michael@0: return elements; michael@0: }, michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.onHover */ michael@0: onHover: function (element, dropon, overlap) { michael@0: if (MochiKit.DOM.isParent(dropon, element)) { michael@0: return; michael@0: } michael@0: var self = MochiKit.Sortable.Sortable; michael@0: michael@0: if (overlap > .33 && overlap < .66 && self.options(dropon).tree) { michael@0: return; michael@0: } else if (overlap > 0.5) { michael@0: self.mark(dropon, 'before'); michael@0: if (dropon.previousSibling != element) { michael@0: var oldParentNode = element.parentNode; michael@0: element.style.visibility = 'hidden'; // fix gecko rendering michael@0: dropon.parentNode.insertBefore(element, dropon); michael@0: if (dropon.parentNode != oldParentNode) { michael@0: self.options(oldParentNode).onChange(element); michael@0: } michael@0: self.options(dropon.parentNode).onChange(element); michael@0: } michael@0: } else { michael@0: self.mark(dropon, 'after'); michael@0: var nextElement = dropon.nextSibling || null; michael@0: if (nextElement != element) { michael@0: var oldParentNode = element.parentNode; michael@0: element.style.visibility = 'hidden'; // fix gecko rendering michael@0: dropon.parentNode.insertBefore(element, nextElement); michael@0: if (dropon.parentNode != oldParentNode) { michael@0: self.options(oldParentNode).onChange(element); michael@0: } michael@0: self.options(dropon.parentNode).onChange(element); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: _offsetSize: function (element, type) { michael@0: if (type == 'vertical' || type == 'height') { michael@0: return element.offsetHeight; michael@0: } else { michael@0: return element.offsetWidth; michael@0: } michael@0: }, michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.onEmptyHover */ michael@0: onEmptyHover: function (element, dropon, overlap) { michael@0: var oldParentNode = element.parentNode; michael@0: var self = MochiKit.Sortable.Sortable; michael@0: var droponOptions = self.options(dropon); michael@0: michael@0: if (!MochiKit.DOM.isParent(dropon, element)) { michael@0: var index; michael@0: michael@0: var children = self.findElements(dropon, {tag: droponOptions.tag, michael@0: only: droponOptions.only}); michael@0: var child = null; michael@0: michael@0: if (children) { michael@0: var offset = self._offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap); michael@0: michael@0: for (index = 0; index < children.length; index += 1) { michael@0: if (offset - self._offsetSize(children[index], droponOptions.overlap) >= 0) { michael@0: offset -= self._offsetSize(children[index], droponOptions.overlap); michael@0: } else if (offset - (self._offsetSize (children[index], droponOptions.overlap) / 2) >= 0) { michael@0: child = index + 1 < children.length ? children[index + 1] : null; michael@0: break; michael@0: } else { michael@0: child = children[index]; michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: michael@0: dropon.insertBefore(element, child); michael@0: michael@0: self.options(oldParentNode).onChange(element); michael@0: droponOptions.onChange(element); michael@0: } michael@0: }, michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.unmark */ michael@0: unmark: function () { michael@0: var m = MochiKit.Sortable.Sortable._marker; michael@0: if (m) { michael@0: MochiKit.Style.hideElement(m); michael@0: } michael@0: }, michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.mark */ michael@0: mark: function (dropon, position) { michael@0: // mark on ghosting only michael@0: var d = MochiKit.DOM; michael@0: var self = MochiKit.Sortable.Sortable; michael@0: var sortable = self.options(dropon.parentNode); michael@0: if (sortable && !sortable.ghosting) { michael@0: return; michael@0: } michael@0: michael@0: if (!self._marker) { michael@0: self._marker = d.getElement('dropmarker') || michael@0: document.createElement('DIV'); michael@0: MochiKit.Style.hideElement(self._marker); michael@0: d.addElementClass(self._marker, 'dropmarker'); michael@0: self._marker.style.position = 'absolute'; michael@0: document.getElementsByTagName('body').item(0).appendChild(self._marker); michael@0: } michael@0: var offsets = MochiKit.Position.cumulativeOffset(dropon); michael@0: self._marker.style.left = offsets.x + 'px'; michael@0: self._marker.style.top = offsets.y + 'px'; michael@0: michael@0: if (position == 'after') { michael@0: if (sortable.overlap == 'horizontal') { michael@0: self._marker.style.left = (offsets.x + dropon.clientWidth) + 'px'; michael@0: } else { michael@0: self._marker.style.top = (offsets.y + dropon.clientHeight) + 'px'; michael@0: } michael@0: } michael@0: MochiKit.Style.showElement(self._marker); michael@0: }, michael@0: michael@0: _tree: function (element, options, parent) { michael@0: var self = MochiKit.Sortable.Sortable; michael@0: var children = self.findElements(element, options) || []; michael@0: michael@0: for (var i = 0; i < children.length; ++i) { michael@0: var match = children[i].id.match(options.format); michael@0: michael@0: if (!match) { michael@0: continue; michael@0: } michael@0: michael@0: var child = { michael@0: id: encodeURIComponent(match ? match[1] : null), michael@0: element: element, michael@0: parent: parent, michael@0: children: [], michael@0: position: parent.children.length, michael@0: container: self._findChildrenElement(children[i], options.treeTag.toUpperCase()) michael@0: } michael@0: michael@0: /* Get the element containing the children and recurse over it */ michael@0: if (child.container) { michael@0: self._tree(child.container, options, child) michael@0: } michael@0: michael@0: parent.children.push (child); michael@0: } michael@0: michael@0: return parent; michael@0: }, michael@0: michael@0: /* Finds the first element of the given tag type within a parent element. michael@0: Used for finding the first LI[ST] within a L[IST]I[TEM].*/ michael@0: _findChildrenElement: function (element, containerTag) { michael@0: if (element && element.hasChildNodes) { michael@0: containerTag = containerTag.toUpperCase(); michael@0: for (var i = 0; i < element.childNodes.length; ++i) { michael@0: if (element.childNodes[i].tagName.toUpperCase() == containerTag) { michael@0: return element.childNodes[i]; michael@0: } michael@0: } michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.tree */ michael@0: tree: function (element, options) { michael@0: element = MochiKit.DOM.getElement(element); michael@0: var sortableOptions = MochiKit.Sortable.Sortable.options(element); michael@0: options = MochiKit.Base.update({ michael@0: tag: sortableOptions.tag, michael@0: treeTag: sortableOptions.treeTag, michael@0: only: sortableOptions.only, michael@0: name: element.id, michael@0: format: sortableOptions.format michael@0: }, options || {}); michael@0: michael@0: var root = { michael@0: id: null, michael@0: parent: null, michael@0: children: new Array, michael@0: container: element, michael@0: position: 0 michael@0: } michael@0: michael@0: return MochiKit.Sortable.Sortable._tree(element, options, root); michael@0: }, michael@0: michael@0: /** michael@0: * Specifies the sequence for the Sortable. michael@0: * @param {Node} element Element to use as the Sortable. michael@0: * @param {Object} newSequence New sequence to use. michael@0: * @param {Object} options Options to use fro the Sortable. michael@0: */ michael@0: setSequence: function (element, newSequence, options) { michael@0: var self = MochiKit.Sortable.Sortable; michael@0: var b = MochiKit.Base; michael@0: element = MochiKit.DOM.getElement(element); michael@0: options = b.update(self.options(element), options || {}); michael@0: michael@0: var nodeMap = {}; michael@0: b.map(function (n) { michael@0: var m = n.id.match(options.format); michael@0: if (m) { michael@0: nodeMap[m[1]] = [n, n.parentNode]; michael@0: } michael@0: n.parentNode.removeChild(n); michael@0: }, self.findElements(element, options)); michael@0: michael@0: b.map(function (ident) { michael@0: var n = nodeMap[ident]; michael@0: if (n) { michael@0: n[1].appendChild(n[0]); michael@0: delete nodeMap[ident]; michael@0: } michael@0: }, newSequence); michael@0: }, michael@0: michael@0: /* Construct a [i] index for a particular node */ michael@0: _constructIndex: function (node) { michael@0: var index = ''; michael@0: do { michael@0: if (node.id) { michael@0: index = '[' + node.position + ']' + index; michael@0: } michael@0: } while ((node = node.parent) != null); michael@0: return index; michael@0: }, michael@0: michael@0: /** @id MochiKit.Sortable.Sortable.sequence */ michael@0: sequence: function (element, options) { michael@0: element = MochiKit.DOM.getElement(element); michael@0: var self = MochiKit.Sortable.Sortable; michael@0: var options = MochiKit.Base.update(self.options(element), options || {}); michael@0: michael@0: return MochiKit.Base.map(function (item) { michael@0: return item.id.match(options.format) ? item.id.match(options.format)[1] : ''; michael@0: }, MochiKit.DOM.getElement(self.findElements(element, options) || [])); michael@0: }, michael@0: michael@0: /** michael@0: * Serializes the content of a Sortable. Useful to send this content through a XMLHTTPRequest. michael@0: * These options override the Sortable options for the serialization only. michael@0: * @param {Node} element Element to serialize. michael@0: * @param {Object} options Serialization options. michael@0: */ michael@0: serialize: function (element, options) { michael@0: element = MochiKit.DOM.getElement(element); michael@0: var self = MochiKit.Sortable.Sortable; michael@0: options = MochiKit.Base.update(self.options(element), options || {}); michael@0: var name = encodeURIComponent(options.name || element.id); michael@0: michael@0: if (options.tree) { michael@0: return MochiKit.Base.flattenArray(MochiKit.Base.map(function (item) { michael@0: return [name + self._constructIndex(item) + "[id]=" + michael@0: encodeURIComponent(item.id)].concat(item.children.map(arguments.callee)); michael@0: }, self.tree(element, options).children)).join('&'); michael@0: } else { michael@0: return MochiKit.Base.map(function (item) { michael@0: return name + "[]=" + encodeURIComponent(item); michael@0: }, self.sequence(element, options)).join('&'); michael@0: } michael@0: } michael@0: }; michael@0: