/public/dragdrop.js

http://github.com/mtravers/wuwei · JavaScript · 972 lines · 786 code · 161 blank · 25 comment · 219 complexity · b2a7b9d3d5c10565fd9ff7c72702603e MD5 · raw file

  1. // Copyright (c) 2005-2010 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
  2. //
  3. // script.aculo.us is freely distributable under the terms of an MIT-style license.
  4. // For details, see the script.aculo.us web site: http://script.aculo.us/
  5. if(Object.isUndefined(Effect))
  6. throw("dragdrop.js requires including script.aculo.us' effects.js library");
  7. var Droppables = {
  8. drops: [],
  9. remove: function(element) {
  10. this.drops = this.drops.reject(function(d) { return d.element==$(element) });
  11. },
  12. add: function(element) {
  13. element = $(element);
  14. var options = Object.extend({
  15. greedy: true,
  16. hoverclass: null,
  17. tree: false
  18. }, arguments[1] || { });
  19. // cache containers
  20. if(options.containment) {
  21. options._containers = [];
  22. var containment = options.containment;
  23. if(Object.isArray(containment)) {
  24. containment.each( function(c) { options._containers.push($(c)) });
  25. } else {
  26. options._containers.push($(containment));
  27. }
  28. }
  29. if(options.accept) options.accept = [options.accept].flatten();
  30. Element.makePositioned(element); // fix IE
  31. options.element = element;
  32. this.drops.push(options);
  33. },
  34. findDeepestChild: function(drops) {
  35. deepest = drops[0];
  36. for (i = 1; i < drops.length; ++i)
  37. if (Element.isParent(drops[i].element, deepest.element))
  38. deepest = drops[i];
  39. return deepest;
  40. },
  41. isContained: function(element, drop) {
  42. var containmentNode;
  43. if(drop.tree) {
  44. containmentNode = element.treeNode;
  45. } else {
  46. containmentNode = element.parentNode;
  47. }
  48. return drop._containers.detect(function(c) { return containmentNode == c });
  49. },
  50. isAffected: function(point, element, drop) {
  51. return (
  52. (drop.element!=element) &&
  53. ((!drop._containers) ||
  54. this.isContained(element, drop)) &&
  55. ((!drop.accept) ||
  56. (Element.classNames(element).detect(
  57. function(v) { return drop.accept.include(v) } ) )) &&
  58. Position.within(drop.element, point[0], point[1]) );
  59. },
  60. deactivate: function(drop) {
  61. if(drop.hoverclass)
  62. Element.removeClassName(drop.element, drop.hoverclass);
  63. this.last_active = null;
  64. },
  65. activate: function(drop) {
  66. if(drop.hoverclass)
  67. Element.addClassName(drop.element, drop.hoverclass);
  68. this.last_active = drop;
  69. },
  70. show: function(point, element) {
  71. if(!this.drops.length) return;
  72. var drop, affected = [];
  73. this.drops.each( function(drop) {
  74. if(Droppables.isAffected(point, element, drop))
  75. affected.push(drop);
  76. });
  77. if(affected.length>0)
  78. drop = Droppables.findDeepestChild(affected);
  79. if(this.last_active && this.last_active != drop) this.deactivate(this.last_active);
  80. if (drop) {
  81. Position.within(drop.element, point[0], point[1]);
  82. if(drop.onHover)
  83. drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
  84. if (drop != this.last_active) Droppables.activate(drop);
  85. }
  86. },
  87. fire: function(event, element) {
  88. if(!this.last_active) return;
  89. Position.prepare();
  90. if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
  91. if (this.last_active.onDrop) {
  92. this.last_active.onDrop(element, this.last_active.element, event);
  93. return true;
  94. }
  95. },
  96. reset: function() {
  97. if(this.last_active)
  98. this.deactivate(this.last_active);
  99. }
  100. };
  101. var Draggables = {
  102. drags: [],
  103. observers: [],
  104. register: function(draggable) {
  105. if(this.drags.length == 0) {
  106. this.eventMouseUp = this.endDrag.bindAsEventListener(this);
  107. this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
  108. this.eventKeypress = this.keyPress.bindAsEventListener(this);
  109. Event.observe(document, "mouseup", this.eventMouseUp);
  110. Event.observe(document, "mousemove", this.eventMouseMove);
  111. Event.observe(document, "keypress", this.eventKeypress);
  112. }
  113. this.drags.push(draggable);
  114. },
  115. unregister: function(draggable) {
  116. this.drags = this.drags.reject(function(d) { return d==draggable });
  117. if(this.drags.length == 0) {
  118. Event.stopObserving(document, "mouseup", this.eventMouseUp);
  119. Event.stopObserving(document, "mousemove", this.eventMouseMove);
  120. Event.stopObserving(document, "keypress", this.eventKeypress);
  121. }
  122. },
  123. activate: function(draggable) {
  124. if(draggable.options.delay) {
  125. this._timeout = setTimeout(function() {
  126. Draggables._timeout = null;
  127. window.focus();
  128. Draggables.activeDraggable = draggable;
  129. }.bind(this), draggable.options.delay);
  130. } else {
  131. window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
  132. this.activeDraggable = draggable;
  133. }
  134. },
  135. deactivate: function() {
  136. this.activeDraggable = null;
  137. },
  138. updateDrag: function(event) {
  139. if(!this.activeDraggable) return;
  140. var pointer = [Event.pointerX(event), Event.pointerY(event)];
  141. // Mozilla-based browsers fire successive mousemove events with
  142. // the same coordinates, prevent needless redrawing (moz bug?)
  143. if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
  144. this._lastPointer = pointer;
  145. this.activeDraggable.updateDrag(event, pointer);
  146. },
  147. endDrag: function(event) {
  148. if(this._timeout) {
  149. clearTimeout(this._timeout);
  150. this._timeout = null;
  151. }
  152. if(!this.activeDraggable) return;
  153. this._lastPointer = null;
  154. this.activeDraggable.endDrag(event);
  155. this.activeDraggable = null;
  156. },
  157. keyPress: function(event) {
  158. if(this.activeDraggable)
  159. this.activeDraggable.keyPress(event);
  160. },
  161. addObserver: function(observer) {
  162. this.observers.push(observer);
  163. this._cacheObserverCallbacks();
  164. },
  165. removeObserver: function(element) { // element instead of observer fixes mem leaks
  166. this.observers = this.observers.reject( function(o) { return o.element==element });
  167. this._cacheObserverCallbacks();
  168. },
  169. notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag'
  170. if(this[eventName+'Count'] > 0)
  171. this.observers.each( function(o) {
  172. if(o[eventName]) o[eventName](eventName, draggable, event);
  173. });
  174. if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
  175. },
  176. _cacheObserverCallbacks: function() {
  177. ['onStart','onEnd','onDrag'].each( function(eventName) {
  178. Draggables[eventName+'Count'] = Draggables.observers.select(
  179. function(o) { return o[eventName]; }
  180. ).length;
  181. });
  182. }
  183. };
  184. /*--------------------------------------------------------------------------*/
  185. var Draggable = Class.create({
  186. initialize: function(element) {
  187. var defaults = {
  188. handle: false,
  189. reverteffect: function(element, top_offset, left_offset) {
  190. var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
  191. new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
  192. queue: {scope:'_draggable', position:'end'}
  193. });
  194. },
  195. endeffect: function(element) {
  196. var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0;
  197. new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity,
  198. queue: {scope:'_draggable', position:'end'},
  199. afterFinish: function(){
  200. Draggable._dragging[element] = false
  201. }
  202. });
  203. },
  204. zindex: 1000,
  205. revert: false,
  206. quiet: false,
  207. scroll: false,
  208. scrollSensitivity: 20,
  209. scrollSpeed: 15,
  210. snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] }
  211. delay: 0
  212. };
  213. if(!arguments[1] || Object.isUndefined(arguments[1].endeffect))
  214. Object.extend(defaults, {
  215. starteffect: function(element) {
  216. element._opacity = Element.getOpacity(element);
  217. Draggable._dragging[element] = true;
  218. new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7});
  219. }
  220. });
  221. var options = Object.extend(defaults, arguments[1] || { });
  222. this.element = $(element);
  223. if(options.handle && Object.isString(options.handle))
  224. this.handle = this.element.down('.'+options.handle, 0);
  225. if(!this.handle) this.handle = $(options.handle);
  226. if(!this.handle) this.handle = this.element;
  227. if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
  228. options.scroll = $(options.scroll);
  229. this._isScrollChild = Element.childOf(this.element, options.scroll);
  230. }
  231. Element.makePositioned(this.element); // fix IE
  232. this.options = options;
  233. this.dragging = false;
  234. this.eventMouseDown = this.initDrag.bindAsEventListener(this);
  235. Event.observe(this.handle, "mousedown", this.eventMouseDown);
  236. Draggables.register(this);
  237. },
  238. destroy: function() {
  239. Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
  240. Draggables.unregister(this);
  241. },
  242. currentDelta: function() {
  243. return([
  244. parseInt(Element.getStyle(this.element,'left') || '0'),
  245. parseInt(Element.getStyle(this.element,'top') || '0')]);
  246. },
  247. initDrag: function(event) {
  248. if(!Object.isUndefined(Draggable._dragging[this.element]) &&
  249. Draggable._dragging[this.element]) return;
  250. if(Event.isLeftClick(event)) {
  251. // abort on form elements, fixes a Firefox issue
  252. var src = Event.element(event);
  253. if((tag_name = src.tagName.toUpperCase()) && (
  254. tag_name=='INPUT' ||
  255. tag_name=='SELECT' ||
  256. tag_name=='OPTION' ||
  257. tag_name=='BUTTON' ||
  258. tag_name=='TEXTAREA')) return;
  259. var pointer = [Event.pointerX(event), Event.pointerY(event)];
  260. var pos = this.element.cumulativeOffset();
  261. this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
  262. Draggables.activate(this);
  263. Event.stop(event);
  264. }
  265. },
  266. startDrag: function(event) {
  267. this.dragging = true;
  268. if(!this.delta)
  269. this.delta = this.currentDelta();
  270. if(this.options.zindex) {
  271. this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
  272. this.element.style.zIndex = this.options.zindex;
  273. }
  274. if(this.options.ghosting) {
  275. this._clone = this.element.cloneNode(true);
  276. this._originallyAbsolute = (this.element.getStyle('position') == 'absolute');
  277. if (!this._originallyAbsolute)
  278. Position.absolutize(this.element);
  279. this.element.parentNode.insertBefore(this._clone, this.element);
  280. }
  281. if(this.options.scroll) {
  282. if (this.options.scroll == window) {
  283. var where = this._getWindowScroll(this.options.scroll);
  284. this.originalScrollLeft = where.left;
  285. this.originalScrollTop = where.top;
  286. } else {
  287. this.originalScrollLeft = this.options.scroll.scrollLeft;
  288. this.originalScrollTop = this.options.scroll.scrollTop;
  289. }
  290. }
  291. Draggables.notify('onStart', this, event);
  292. if(this.options.starteffect) this.options.starteffect(this.element);
  293. },
  294. updateDrag: function(event, pointer) {
  295. if(!this.dragging) this.startDrag(event);
  296. if(!this.options.quiet){
  297. Position.prepare();
  298. Droppables.show(pointer, this.element);
  299. }
  300. Draggables.notify('onDrag', this, event);
  301. this.draw(pointer);
  302. if(this.options.change) this.options.change(this);
  303. if(this.options.scroll) {
  304. this.stopScrolling();
  305. var p;
  306. if (this.options.scroll == window) {
  307. with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
  308. } else {
  309. p = Position.page(this.options.scroll).toArray();
  310. p[0] += this.options.scroll.scrollLeft + Position.deltaX;
  311. p[1] += this.options.scroll.scrollTop + Position.deltaY;
  312. p.push(p[0]+this.options.scroll.offsetWidth);
  313. p.push(p[1]+this.options.scroll.offsetHeight);
  314. }
  315. var speed = [0,0];
  316. if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
  317. if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
  318. if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
  319. if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
  320. this.startScrolling(speed);
  321. }
  322. // fix AppleWebKit rendering
  323. if(Prototype.Browser.WebKit) window.scrollBy(0,0);
  324. Event.stop(event);
  325. },
  326. finishDrag: function(event, success) {
  327. this.dragging = false;
  328. if(this.options.quiet){
  329. Position.prepare();
  330. var pointer = [Event.pointerX(event), Event.pointerY(event)];
  331. Droppables.show(pointer, this.element);
  332. }
  333. if(this.options.ghosting) {
  334. if (!this._originallyAbsolute)
  335. Position.relativize(this.element);
  336. delete this._originallyAbsolute;
  337. Element.remove(this._clone);
  338. this._clone = null;
  339. }
  340. var dropped = false;
  341. if(success) {
  342. dropped = Droppables.fire(event, this.element);
  343. if (!dropped) dropped = false;
  344. }
  345. if(dropped && this.options.onDropped) this.options.onDropped(this.element);
  346. Draggables.notify('onEnd', this, event);
  347. var revert = this.options.revert;
  348. if(revert && Object.isFunction(revert)) revert = revert(this.element);
  349. var d = this.currentDelta();
  350. if(revert && this.options.reverteffect) {
  351. if (dropped == 0 || revert != 'failure')
  352. this.options.reverteffect(this.element,
  353. d[1]-this.delta[1], d[0]-this.delta[0]);
  354. } else {
  355. this.delta = d;
  356. }
  357. if(this.options.zindex)
  358. this.element.style.zIndex = this.originalZ;
  359. if(this.options.endeffect)
  360. this.options.endeffect(this.element);
  361. Draggables.deactivate(this);
  362. Droppables.reset();
  363. },
  364. keyPress: function(event) {
  365. if(event.keyCode!=Event.KEY_ESC) return;
  366. this.finishDrag(event, false);
  367. Event.stop(event);
  368. },
  369. endDrag: function(event) {
  370. if(!this.dragging) return;
  371. this.stopScrolling();
  372. this.finishDrag(event, true);
  373. Event.stop(event);
  374. },
  375. draw: function(point) {
  376. var pos = this.element.cumulativeOffset();
  377. if(this.options.ghosting) {
  378. var r = Position.realOffset(this.element);
  379. pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
  380. }
  381. var d = this.currentDelta();
  382. pos[0] -= d[0]; pos[1] -= d[1];
  383. if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
  384. pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
  385. pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
  386. }
  387. var p = [0,1].map(function(i){
  388. return (point[i]-pos[i]-this.offset[i])
  389. }.bind(this));
  390. if(this.options.snap) {
  391. if(Object.isFunction(this.options.snap)) {
  392. p = this.options.snap(p[0],p[1],this);
  393. } else {
  394. if(Object.isArray(this.options.snap)) {
  395. p = p.map( function(v, i) {
  396. return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this));
  397. } else {
  398. p = p.map( function(v) {
  399. return (v/this.options.snap).round()*this.options.snap }.bind(this));
  400. }
  401. }}
  402. var style = this.element.style;
  403. if((!this.options.constraint) || (this.options.constraint=='horizontal'))
  404. style.left = p[0] + "px";
  405. if((!this.options.constraint) || (this.options.constraint=='vertical'))
  406. style.top = p[1] + "px";
  407. if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
  408. },
  409. stopScrolling: function() {
  410. if(this.scrollInterval) {
  411. clearInterval(this.scrollInterval);
  412. this.scrollInterval = null;
  413. Draggables._lastScrollPointer = null;
  414. }
  415. },
  416. startScrolling: function(speed) {
  417. if(!(speed[0] || speed[1])) return;
  418. this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
  419. this.lastScrolled = new Date();
  420. this.scrollInterval = setInterval(this.scroll.bind(this), 10);
  421. },
  422. scroll: function() {
  423. var current = new Date();
  424. var delta = current - this.lastScrolled;
  425. this.lastScrolled = current;
  426. if(this.options.scroll == window) {
  427. with (this._getWindowScroll(this.options.scroll)) {
  428. if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
  429. var d = delta / 1000;
  430. this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
  431. }
  432. }
  433. } else {
  434. this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
  435. this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000;
  436. }
  437. Position.prepare();
  438. Droppables.show(Draggables._lastPointer, this.element);
  439. Draggables.notify('onDrag', this);
  440. if (this._isScrollChild) {
  441. Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
  442. Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
  443. Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
  444. if (Draggables._lastScrollPointer[0] < 0)
  445. Draggables._lastScrollPointer[0] = 0;
  446. if (Draggables._lastScrollPointer[1] < 0)
  447. Draggables._lastScrollPointer[1] = 0;
  448. this.draw(Draggables._lastScrollPointer);
  449. }
  450. if(this.options.change) this.options.change(this);
  451. },
  452. _getWindowScroll: function(w) {
  453. var T, L, W, H;
  454. with (w.document) {
  455. if (w.document.documentElement && documentElement.scrollTop) {
  456. T = documentElement.scrollTop;
  457. L = documentElement.scrollLeft;
  458. } else if (w.document.body) {
  459. T = body.scrollTop;
  460. L = body.scrollLeft;
  461. }
  462. if (w.innerWidth) {
  463. W = w.innerWidth;
  464. H = w.innerHeight;
  465. } else if (w.document.documentElement && documentElement.clientWidth) {
  466. W = documentElement.clientWidth;
  467. H = documentElement.clientHeight;
  468. } else {
  469. W = body.offsetWidth;
  470. H = body.offsetHeight;
  471. }
  472. }
  473. return { top: T, left: L, width: W, height: H };
  474. }
  475. });
  476. Draggable._dragging = { };
  477. /*--------------------------------------------------------------------------*/
  478. var SortableObserver = Class.create({
  479. initialize: function(element, observer) {
  480. this.element = $(element);
  481. this.observer = observer;
  482. this.lastValue = Sortable.serialize(this.element);
  483. },
  484. onStart: function() {
  485. this.lastValue = Sortable.serialize(this.element);
  486. },
  487. onEnd: function() {
  488. Sortable.unmark();
  489. if(this.lastValue != Sortable.serialize(this.element))
  490. this.observer(this.element)
  491. }
  492. });
  493. var Sortable = {
  494. SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,
  495. sortables: { },
  496. _findRootElement: function(element) {
  497. while (element.tagName.toUpperCase() != "BODY") {
  498. if(element.id && Sortable.sortables[element.id]) return element;
  499. element = element.parentNode;
  500. }
  501. },
  502. options: function(element) {
  503. element = Sortable._findRootElement($(element));
  504. if(!element) return;
  505. return Sortable.sortables[element.id];
  506. },
  507. destroy: function(element){
  508. element = $(element);
  509. var s = Sortable.sortables[element.id];
  510. if(s) {
  511. Draggables.removeObserver(s.element);
  512. s.droppables.each(function(d){ Droppables.remove(d) });
  513. s.draggables.invoke('destroy');
  514. delete Sortable.sortables[s.element.id];
  515. }
  516. },
  517. create: function(element) {
  518. element = $(element);
  519. var options = Object.extend({
  520. element: element,
  521. tag: 'li', // assumes li children, override with tag: 'tagname'
  522. dropOnEmpty: false,
  523. tree: false,
  524. treeTag: 'ul',
  525. overlap: 'vertical', // one of 'vertical', 'horizontal'
  526. constraint: 'vertical', // one of 'vertical', 'horizontal', false
  527. containment: element, // also takes array of elements (or id's); or false
  528. handle: false, // or a CSS class
  529. only: false,
  530. delay: 0,
  531. hoverclass: null,
  532. ghosting: false,
  533. quiet: false,
  534. scroll: false,
  535. scrollSensitivity: 20,
  536. scrollSpeed: 15,
  537. format: this.SERIALIZE_RULE,
  538. // these take arrays of elements or ids and can be
  539. // used for better initialization performance
  540. elements: false,
  541. handles: false,
  542. onChange: Prototype.emptyFunction,
  543. onUpdate: Prototype.emptyFunction
  544. }, arguments[1] || { });
  545. // clear any old sortable with same element
  546. this.destroy(element);
  547. // build options for the draggables
  548. var options_for_draggable = {
  549. revert: true,
  550. quiet: options.quiet,
  551. scroll: options.scroll,
  552. scrollSpeed: options.scrollSpeed,
  553. scrollSensitivity: options.scrollSensitivity,
  554. delay: options.delay,
  555. ghosting: options.ghosting,
  556. constraint: options.constraint,
  557. handle: options.handle };
  558. if(options.starteffect)
  559. options_for_draggable.starteffect = options.starteffect;
  560. if(options.reverteffect)
  561. options_for_draggable.reverteffect = options.reverteffect;
  562. else
  563. if(options.ghosting) options_for_draggable.reverteffect = function(element) {
  564. element.style.top = 0;
  565. element.style.left = 0;
  566. };
  567. if(options.endeffect)
  568. options_for_draggable.endeffect = options.endeffect;
  569. if(options.zindex)
  570. options_for_draggable.zindex = options.zindex;
  571. // build options for the droppables
  572. var options_for_droppable = {
  573. overlap: options.overlap,
  574. containment: options.containment,
  575. tree: options.tree,
  576. hoverclass: options.hoverclass,
  577. onHover: Sortable.onHover
  578. };
  579. var options_for_tree = {
  580. onHover: Sortable.onEmptyHover,
  581. overlap: options.overlap,
  582. containment: options.containment,
  583. hoverclass: options.hoverclass
  584. };
  585. // fix for gecko engine
  586. Element.cleanWhitespace(element);
  587. options.draggables = [];
  588. options.droppables = [];
  589. // drop on empty handling
  590. if(options.dropOnEmpty || options.tree) {
  591. Droppables.add(element, options_for_tree);
  592. options.droppables.push(element);
  593. }
  594. (options.elements || this.findElements(element, options) || []).each( function(e,i) {
  595. var handle = options.handles ? $(options.handles[i]) :
  596. (options.handle ? $(e).select('.' + options.handle)[0] : e);
  597. options.draggables.push(
  598. new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
  599. Droppables.add(e, options_for_droppable);
  600. if(options.tree) e.treeNode = element;
  601. options.droppables.push(e);
  602. });
  603. if(options.tree) {
  604. (Sortable.findTreeElements(element, options) || []).each( function(e) {
  605. Droppables.add(e, options_for_tree);
  606. e.treeNode = element;
  607. options.droppables.push(e);
  608. });
  609. }
  610. // keep reference
  611. this.sortables[element.identify()] = options;
  612. // for onupdate
  613. Draggables.addObserver(new SortableObserver(element, options.onUpdate));
  614. },
  615. // return all suitable-for-sortable elements in a guaranteed order
  616. findElements: function(element, options) {
  617. return Element.findChildren(
  618. element, options.only, options.tree ? true : false, options.tag);
  619. },
  620. findTreeElements: function(element, options) {
  621. return Element.findChildren(
  622. element, options.only, options.tree ? true : false, options.treeTag);
  623. },
  624. onHover: function(element, dropon, overlap) {
  625. if(Element.isParent(dropon, element)) return;
  626. if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
  627. return;
  628. } else if(overlap>0.5) {
  629. Sortable.mark(dropon, 'before');
  630. if(dropon.previousSibling != element) {
  631. var oldParentNode = element.parentNode;
  632. element.style.visibility = "hidden"; // fix gecko rendering
  633. dropon.parentNode.insertBefore(element, dropon);
  634. if(dropon.parentNode!=oldParentNode)
  635. Sortable.options(oldParentNode).onChange(element);
  636. Sortable.options(dropon.parentNode).onChange(element);
  637. }
  638. } else {
  639. Sortable.mark(dropon, 'after');
  640. var nextElement = dropon.nextSibling || null;
  641. if(nextElement != element) {
  642. var oldParentNode = element.parentNode;
  643. element.style.visibility = "hidden"; // fix gecko rendering
  644. dropon.parentNode.insertBefore(element, nextElement);
  645. if(dropon.parentNode!=oldParentNode)
  646. Sortable.options(oldParentNode).onChange(element);
  647. Sortable.options(dropon.parentNode).onChange(element);
  648. }
  649. }
  650. },
  651. onEmptyHover: function(element, dropon, overlap) {
  652. var oldParentNode = element.parentNode;
  653. var droponOptions = Sortable.options(dropon);
  654. if(!Element.isParent(dropon, element)) {
  655. var index;
  656. var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
  657. var child = null;
  658. if(children) {
  659. var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
  660. for (index = 0; index < children.length; index += 1) {
  661. if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
  662. offset -= Element.offsetSize (children[index], droponOptions.overlap);
  663. } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
  664. child = index + 1 < children.length ? children[index + 1] : null;
  665. break;
  666. } else {
  667. child = children[index];
  668. break;
  669. }
  670. }
  671. }
  672. dropon.insertBefore(element, child);
  673. Sortable.options(oldParentNode).onChange(element);
  674. droponOptions.onChange(element);
  675. }
  676. },
  677. unmark: function() {
  678. if(Sortable._marker) Sortable._marker.hide();
  679. },
  680. mark: function(dropon, position) {
  681. // mark on ghosting only
  682. var sortable = Sortable.options(dropon.parentNode);
  683. if(sortable && !sortable.ghosting) return;
  684. if(!Sortable._marker) {
  685. Sortable._marker =
  686. ($('dropmarker') || Element.extend(document.createElement('DIV'))).
  687. hide().addClassName('dropmarker').setStyle({position:'absolute'});
  688. document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
  689. }
  690. var offsets = dropon.cumulativeOffset();
  691. Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});
  692. if(position=='after')
  693. if(sortable.overlap == 'horizontal')
  694. Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
  695. else
  696. Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});
  697. Sortable._marker.show();
  698. },
  699. _tree: function(element, options, parent) {
  700. var children = Sortable.findElements(element, options) || [];
  701. for (var i = 0; i < children.length; ++i) {
  702. var match = children[i].id.match(options.format);
  703. if (!match) continue;
  704. var child = {
  705. id: encodeURIComponent(match ? match[1] : null),
  706. element: element,
  707. parent: parent,
  708. children: [],
  709. position: parent.children.length,
  710. container: $(children[i]).down(options.treeTag)
  711. };
  712. /* Get the element containing the children and recurse over it */
  713. if (child.container)
  714. this._tree(child.container, options, child);
  715. parent.children.push (child);
  716. }
  717. return parent;
  718. },
  719. tree: function(element) {
  720. element = $(element);
  721. var sortableOptions = this.options(element);
  722. var options = Object.extend({
  723. tag: sortableOptions.tag,
  724. treeTag: sortableOptions.treeTag,
  725. only: sortableOptions.only,
  726. name: element.id,
  727. format: sortableOptions.format
  728. }, arguments[1] || { });
  729. var root = {
  730. id: null,
  731. parent: null,
  732. children: [],
  733. container: element,
  734. position: 0
  735. };
  736. return Sortable._tree(element, options, root);
  737. },
  738. /* Construct a [i] index for a particular node */
  739. _constructIndex: function(node) {
  740. var index = '';
  741. do {
  742. if (node.id) index = '[' + node.position + ']' + index;
  743. } while ((node = node.parent) != null);
  744. return index;
  745. },
  746. sequence: function(element) {
  747. element = $(element);
  748. var options = Object.extend(this.options(element), arguments[1] || { });
  749. return $(this.findElements(element, options) || []).map( function(item) {
  750. return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
  751. });
  752. },
  753. setSequence: function(element, new_sequence) {
  754. element = $(element);
  755. var options = Object.extend(this.options(element), arguments[2] || { });
  756. var nodeMap = { };
  757. this.findElements(element, options).each( function(n) {
  758. if (n.id.match(options.format))
  759. nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
  760. n.parentNode.removeChild(n);
  761. });
  762. new_sequence.each(function(ident) {
  763. var n = nodeMap[ident];
  764. if (n) {
  765. n[1].appendChild(n[0]);
  766. delete nodeMap[ident];
  767. }
  768. });
  769. },
  770. serialize: function(element) {
  771. element = $(element);
  772. var options = Object.extend(Sortable.options(element), arguments[1] || { });
  773. var name = encodeURIComponent(
  774. (arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
  775. if (options.tree) {
  776. return Sortable.tree(element, arguments[1]).children.map( function (item) {
  777. return [name + Sortable._constructIndex(item) + "[id]=" +
  778. encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
  779. }).flatten().join('&');
  780. } else {
  781. return Sortable.sequence(element, arguments[1]).map( function(item) {
  782. return name + "[]=" + encodeURIComponent(item);
  783. }).join('&');
  784. }
  785. }
  786. };
  787. // Returns true if child is contained within element
  788. Element.isParent = function(child, element) {
  789. if (!child.parentNode || child == element) return false;
  790. if (child.parentNode == element) return true;
  791. return Element.isParent(child.parentNode, element);
  792. };
  793. Element.findChildren = function(element, only, recursive, tagName) {
  794. if(!element.hasChildNodes()) return null;
  795. tagName = tagName.toUpperCase();
  796. if(only) only = [only].flatten();
  797. var elements = [];
  798. $A(element.childNodes).each( function(e) {
  799. if(e.tagName && e.tagName.toUpperCase()==tagName &&
  800. (!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
  801. elements.push(e);
  802. if(recursive) {
  803. var grandchildren = Element.findChildren(e, only, recursive, tagName);
  804. if(grandchildren) elements.push(grandchildren);
  805. }
  806. });
  807. return (elements.length>0 ? elements.flatten() : []);
  808. };
  809. Element.offsetSize = function (element, type) {
  810. return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
  811. };