Sortable.js (20054B)
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;