PageRenderTime 85ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/thirdparty/scriptaculous/dragdrop.js

http://github.com/silverstripe/sapphire
JavaScript | 581 lines | 469 code | 82 blank | 30 comment | 114 complexity | 3f5854a6341199f7dcccae7b3a8c8d52 MD5 | raw file
Possible License(s): BSD-3-Clause, MIT, CC-BY-3.0, GPL-2.0, AGPL-1.0, LGPL-2.1
  1. // Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
  2. //
  3. // See scriptaculous.js for full license.
  4. /*--------------------------------------------------------------------------*/
  5. var Droppables = {
  6. drops: [],
  7. remove: function(element) {
  8. this.drops = this.drops.reject(function(d) { return d.element==$(element) });
  9. },
  10. add: function(element) {
  11. element = $(element);
  12. var options = Object.extend({
  13. greedy: true,
  14. hoverclass: null
  15. }, arguments[1] || {});
  16. // cache containers
  17. if(options.containment) {
  18. options._containers = [];
  19. var containment = options.containment;
  20. if((typeof containment == 'object') &&
  21. (containment.constructor == Array)) {
  22. containment.each( function(c) { options._containers.push($(c)) });
  23. } else {
  24. options._containers.push($(containment));
  25. }
  26. }
  27. if(options.accept) options.accept = [options.accept].flatten();
  28. // Element.makePositioned(element); // fix IE
  29. options.element = element;
  30. this.drops.push(options);
  31. },
  32. isContained: function(element, drop) {
  33. var parentNode = element.parentNode;
  34. return drop._containers.detect(function(c) { return parentNode == c });
  35. },
  36. isAffected: function(pX, pY, element, drop) {
  37. return (
  38. (drop.element!=element) && (drop.element.parentNode) &&
  39. ((!drop._containers) ||
  40. this.isContained(element, drop)) &&
  41. ((!drop.accept) ||
  42. (Element.classNames(element).detect(
  43. function(v) { return drop.accept.include(v) } ) )) &&
  44. ((!drop.checkDroppableIsntContained) || !Element.contains(element, drop.element)) &&
  45. Position.within(drop.element, pX, pY) );
  46. },
  47. deactivate: function(drop) {
  48. if(drop.hoverclass)
  49. Element.removeClassName(drop.element, drop.hoverclass);
  50. this.last_active = null;
  51. },
  52. activate: function(drop) {
  53. if(this.last_active) this.deactivate(this.last_active);
  54. if(drop.hoverclass)
  55. Element.addClassName(drop.element, drop.hoverclass);
  56. this.last_active = drop;
  57. },
  58. show: function(event, element) {
  59. if(!this.drops.length) return;
  60. var pX = Event.pointerX(event);
  61. var pY = Event.pointerY(event);
  62. Position.prepare();
  63. var i = this.drops.length-1; do {
  64. var drop = this.drops[i];
  65. if(this.isAffected(pX, pY, element, drop)) {
  66. if(drop.onHover)
  67. drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
  68. if(drop.greedy) {
  69. this.activate(drop);
  70. return;
  71. }
  72. }
  73. } while (i--);
  74. if(this.last_active) this.deactivate(this.last_active);
  75. },
  76. fire: function(event, element) {
  77. if(!this.last_active) return;
  78. Position.prepare();
  79. if (this.isAffected(Event.pointerX(event), Event.pointerY(event), element, this.last_active))
  80. if (this.last_active.onDrop)
  81. this.last_active.onDrop(element, this.last_active.element, event);
  82. },
  83. reset: function() {
  84. if(this.last_active)
  85. this.deactivate(this.last_active);
  86. }
  87. }
  88. var Draggables = {
  89. observers: [],
  90. addObserver: function(observer) {
  91. this.observers.push(observer);
  92. this._cacheObserverCallbacks();
  93. },
  94. removeObserver: function(element) { // element instead of observer fixes mem leaks
  95. this.observers = this.observers.reject( function(o) { return o.element==element });
  96. this._cacheObserverCallbacks();
  97. },
  98. notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag'
  99. if(this[eventName+'Count'] > 0)
  100. this.observers.each( function(o) {
  101. if(o[eventName]) o[eventName](eventName, draggable, event);
  102. });
  103. },
  104. _cacheObserverCallbacks: function() {
  105. ['onStart','onEnd','onDrag'].each( function(eventName) {
  106. Draggables[eventName+'Count'] = Draggables.observers.select(
  107. function(o) { return o[eventName]; }
  108. ).length;
  109. });
  110. }
  111. }
  112. /*--------------------------------------------------------------------------*/
  113. var Draggable = Class.create();
  114. Draggable.prototype = {
  115. initialize: function(element) {
  116. var options = Object.extend({
  117. handle: false,
  118. starteffect: function(element) {
  119. new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7});
  120. },
  121. reverteffect: function(element, top_offset, left_offset) {
  122. var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
  123. new Effect.MoveBy(element, -top_offset, -left_offset, {duration:dur});
  124. },
  125. endeffect: function(element) {
  126. new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0});
  127. },
  128. zindex: 1000,
  129. revert: false,
  130. snap: false // false, or xy or [x,y] or function(x,y){ return [x,y] }
  131. }, arguments[1] || {});
  132. this.element = $(element);
  133. if(options.handle && (typeof options.handle == 'string'))
  134. this.handle = Element.childrenWithClassName(this.element, options.handle)[0];
  135. if(!this.handle) this.handle = $(options.handle);
  136. if(!this.handle) this.handle = this.element;
  137. this.offsetX = 0;
  138. this.offsetY = 0;
  139. this.originalLeft = this.currentLeft();
  140. this.originalTop = this.currentTop();
  141. this.originalX = this.element.offsetLeft;
  142. this.originalY = this.element.offsetTop;
  143. this.options = options;
  144. this.active = false;
  145. this.dragging = false;
  146. this.eventMouseDown = this.startDrag.bindAsEventListener(this);
  147. this.eventMouseUp = this.endDrag.bindAsEventListener(this);
  148. this.eventMouseMove = this.update.bindAsEventListener(this);
  149. this.eventKeypress = this.keyPress.bindAsEventListener(this);
  150. this.registerEvents();
  151. },
  152. destroy: function() {
  153. Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
  154. this.unregisterEvents();
  155. },
  156. registerEvents: function() {
  157. //if(!Draggable.eventsRegistered) {
  158. //Draggable.eventsRegistered = true;
  159. Event.observe(document, "mouseup", this.eventMouseUp);
  160. Event.observe(document, "mousemove", this.eventMouseMove);
  161. Event.observe(document, "keypress", this.eventKeypress);
  162. Event.observe(this.handle, "mousedown", this.eventMouseDown);
  163. //}
  164. },
  165. unregisterEvents: function() {
  166. //if(!Draggable.eventsRegistered) {
  167. //Draggable.eventsRegistered = false;
  168. //if(!this.active) return;
  169. Event.stopObserving(document, "mouseup", this.eventMouseUp);
  170. Event.stopObserving(document, "mousemove", this.eventMouseMove);
  171. Event.stopObserving(document, "keypress", this.eventKeypress);
  172. //}
  173. },
  174. currentLeft: function() {
  175. return parseInt(this.element.style.left || '0');
  176. },
  177. currentTop: function() {
  178. return parseInt(this.element.style.top || '0')
  179. },
  180. startDrag: function(event) {
  181. if(Event.isLeftClick(event)) {
  182. // abort on form elements, fixes a Firefox issue
  183. var src = Event.element(event);
  184. if(src.tagName && (
  185. src.tagName=='INPUT' ||
  186. src.tagName=='SELECT' ||
  187. src.tagName=='BUTTON' ||
  188. src.tagName=='TEXTAREA')) return;
  189. // this.registerEvents();
  190. Element.makePositioned(this.element); // fix IE
  191. this.element.wasDragged = false;
  192. this.active = true;
  193. var pointer = [Event.pointerX(event), Event.pointerY(event)];
  194. var offsets = Position.cumulativeOffset(this.element);
  195. this.offsetX = (pointer[0] - offsets[0]);
  196. this.offsetY = (pointer[1] - offsets[1]);
  197. Event.stop(event);
  198. }
  199. },
  200. finishDrag: function(event, success) {
  201. // this.unregisterEvents();
  202. this.active = false;
  203. this.dragging = false;
  204. this.element.wasDragged = true;
  205. if(this.options.ghosting) {
  206. Position.relativize(this.element);
  207. Element.remove(this._clone);
  208. this._clone = null;
  209. }
  210. if(success) Droppables.fire(event, this.element);
  211. Draggables.notify('onEnd', this, event);
  212. var revert = this.options.revert;
  213. if(revert && typeof revert == 'function') revert = revert(this.element);
  214. if(revert && this.options.reverteffect) {
  215. this.options.reverteffect(this.element,
  216. this.currentTop()-this.originalTop,
  217. this.currentLeft()-this.originalLeft);
  218. } else {
  219. this.originalLeft = this.currentLeft();
  220. this.originalTop = this.currentTop();
  221. }
  222. if(this.options.zindex)
  223. this.element.style.zIndex = this.originalZ;
  224. if(this.options.endeffect)
  225. this.options.endeffect(this.element);
  226. Droppables.reset();
  227. },
  228. keyPress: function(event) {
  229. if(this.active) {
  230. if(event.keyCode==Event.KEY_ESC) {
  231. Element.undoPositioned(this.element);
  232. this.finishDrag(event, false);
  233. Event.stop(event);
  234. }
  235. }
  236. },
  237. endDrag: function(event) {
  238. Element.undoPositioned(this.element);
  239. if(this.active && this.dragging) {
  240. this.finishDrag(event, true);
  241. Event.stop(event);
  242. }
  243. this.active = false;
  244. this.dragging = false;
  245. },
  246. draw: function(event) {
  247. var pointer = [Event.pointerX(event), Event.pointerY(event)];
  248. var offsets = Position.cumulativeOffset(this.element);
  249. offsets[0] -= this.currentLeft();
  250. offsets[1] -= this.currentTop();
  251. var style = this.element.style;
  252. var pos = [
  253. (pointer[0] - offsets[0] - this.offsetX),
  254. (pointer[1] - offsets[1] - this.offsetY)];
  255. if(this.options.snap) {
  256. if(typeof this.options.snap == 'function') {
  257. pos = this.options.snap(pos[0],pos[1]);
  258. } else {
  259. var draggable = this;
  260. if(this.options.snap instanceof Array) {
  261. pos = pos.collect( function(v, i) {
  262. return Math.round(v/draggable.options.snap[i])*draggable.options.snap[i] })
  263. } else {
  264. pos = pos.collect( function(v) {
  265. return Math.round(v/draggable.options.snap)*draggable.options.snap })
  266. }
  267. }}
  268. if((!this.options.constraint) || (this.options.constraint=='horizontal'))
  269. style.left = pos[0] + "px";
  270. if((!this.options.constraint) || (this.options.constraint=='vertical'))
  271. style.top = pos[1] + "px";
  272. if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
  273. },
  274. update: function(event) {
  275. if(this.active) {
  276. if(!this.dragging) {
  277. if(this.options.onStartDrag) this.options.onStartDrag(this.element);
  278. var style = this.element.style;
  279. this.dragging = true;
  280. if(Element.getStyle(this.element,'position')=='')
  281. style.position = "relative";
  282. if(this.options.zindex) {
  283. this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
  284. style.zIndex = this.options.zindex;
  285. }
  286. if(this.options.ghosting) {
  287. this._clone = this.element.cloneNode(true);
  288. Position.absolutize(this.element);
  289. this.element.parentNode.insertBefore(this._clone, this.element);
  290. }
  291. Draggables.notify('onStart', this, event);
  292. if(this.options.starteffect) this.options.starteffect(this.element);
  293. }
  294. Droppables.show(event, this.element);
  295. Draggables.notify('onDrag', this, event);
  296. this.draw(event);
  297. if(this.options.change) this.options.change(this);
  298. // fix AppleWebKit rendering
  299. if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
  300. Event.stop(event);
  301. }
  302. }
  303. }
  304. function rollingStatus(chr, maxLength) {
  305. var txt = window.status + chr;
  306. if(txt.length > maxLength) txt = txt.substr(txt.length - maxLength);
  307. window.status = txt;
  308. }
  309. /*--------------------------------------------------------------------------*/
  310. var SortableObserver = Class.create();
  311. SortableObserver.prototype = {
  312. initialize: function(element, observer) {
  313. this.element = $(element);
  314. this.observer = observer;
  315. this.lastValue = Sortable.serialize(this.element);
  316. },
  317. onStart: function() {
  318. this.lastValue = Sortable.serialize(this.element);
  319. },
  320. onEnd: function() {
  321. Sortable.unmark();
  322. if(this.lastValue != Sortable.serialize(this.element))
  323. this.observer(this.element)
  324. }
  325. }
  326. var Sortable = {
  327. sortables: new Array(),
  328. options: function(element){
  329. element = $(element);
  330. return this.sortables.detect(function(s) { return s.element == element });
  331. },
  332. destroy: function(element){
  333. element = $(element);
  334. this.sortables.findAll(function(s) { return s.element == element }).each(function(s){
  335. Draggables.removeObserver(s.element);
  336. s.droppables.each(function(d){ Droppables.remove(d) });
  337. s.draggables.invoke('destroy');
  338. });
  339. this.sortables = this.sortables.reject(function(s) { return s.element == element });
  340. },
  341. create: function(element) {
  342. element = $(element);
  343. var options = Object.extend({
  344. element: element,
  345. tag: 'li', // assumes li children, override with tag: 'tagname'
  346. dropOnEmpty: false,
  347. tree: false, // fixme: unimplemented
  348. overlap: 'vertical', // one of 'vertical', 'horizontal'
  349. constraint: 'vertical', // one of 'vertical', 'horizontal', false
  350. containment: element, // also takes array of elements (or id's); or false
  351. handle: false, // or a CSS class
  352. only: false,
  353. hoverclass: null,
  354. ghosting: false,
  355. format: null,
  356. onChange: Prototype.emptyFunction,
  357. onUpdate: Prototype.emptyFunction
  358. }, arguments[1] || {});
  359. // clear any old sortable with same element
  360. this.destroy(element);
  361. // build options for the draggables
  362. var options_for_draggable = {
  363. revert: true,
  364. ghosting: options.ghosting,
  365. constraint: options.constraint,
  366. handle: options.handle };
  367. if(options.starteffect)
  368. options_for_draggable.starteffect = options.starteffect;
  369. if(options.reverteffect)
  370. options_for_draggable.reverteffect = options.reverteffect;
  371. else
  372. if(options.ghosting) options_for_draggable.reverteffect = function(element) {
  373. element.style.top = 0;
  374. element.style.left = 0;
  375. };
  376. if(options.endeffect)
  377. options_for_draggable.endeffect = options.endeffect;
  378. if(options.zindex)
  379. options_for_draggable.zindex = options.zindex;
  380. // build options for the droppables
  381. var options_for_droppable = {
  382. overlap: options.overlap,
  383. containment: options.containment,
  384. hoverclass: options.hoverclass,
  385. onHover: Sortable.onHover,
  386. greedy: !options.dropOnEmpty
  387. }
  388. // fix for gecko engine
  389. Element.cleanWhitespace(element);
  390. options.draggables = [];
  391. options.droppables = [];
  392. // make it so
  393. // drop on empty handling
  394. if(options.dropOnEmpty) {
  395. Droppables.add(element,
  396. {containment: options.containment, onHover: Sortable.onEmptyHover, greedy: false});
  397. options.droppables.push(element);
  398. }
  399. (this.findElements(element, options) || []).each( function(e) {
  400. // handles are per-draggable
  401. var handle = options.handle ?
  402. Element.childrenWithClassName(e, options.handle)[0] : e;
  403. options.draggables.push(
  404. new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
  405. Droppables.add(e, options_for_droppable);
  406. options.droppables.push(e);
  407. });
  408. // keep reference
  409. this.sortables.push(options);
  410. // for onupdate
  411. Draggables.addObserver(new SortableObserver(element, options.onUpdate));
  412. },
  413. // return all suitable-for-sortable elements in a guaranteed order
  414. findElements: function(element, options) {
  415. if(!element.hasChildNodes()) return null;
  416. var elements = [];
  417. $A(element.childNodes).each( function(e) {
  418. if(e.tagName && e.tagName==options.tag.toUpperCase() &&
  419. (!options.only || (Element.hasClassName(e, options.only))))
  420. elements.push(e);
  421. if(options.tree) {
  422. var grandchildren = this.findElements(e, options);
  423. if(grandchildren) elements.push(grandchildren);
  424. }
  425. });
  426. return (elements.length>0 ? elements.flatten() : null);
  427. },
  428. onHover: function(element, dropon, overlap) {
  429. if(overlap>0.5) {
  430. Sortable.mark(dropon, 'before');
  431. if(dropon.previousSibling != element) {
  432. var oldParentNode = element.parentNode;
  433. element.style.visibility = "hidden"; // fix gecko rendering
  434. dropon.parentNode.insertBefore(element, dropon);
  435. if(dropon.parentNode!=oldParentNode)
  436. Sortable.options(oldParentNode).onChange(element);
  437. Sortable.options(dropon.parentNode).onChange(element);
  438. }
  439. } else {
  440. Sortable.mark(dropon, 'after');
  441. var nextElement = dropon.nextSibling || null;
  442. if(nextElement != element) {
  443. var oldParentNode = element.parentNode;
  444. element.style.visibility = "hidden"; // fix gecko rendering
  445. dropon.parentNode.insertBefore(element, nextElement);
  446. if(dropon.parentNode!=oldParentNode)
  447. Sortable.options(oldParentNode).onChange(element);
  448. Sortable.options(dropon.parentNode).onChange(element);
  449. }
  450. }
  451. },
  452. onEmptyHover: function(element, dropon) {
  453. if(element.parentNode!=dropon) {
  454. var oldParentNode = element.parentNode;
  455. dropon.appendChild(element);
  456. Sortable.options(oldParentNode).onChange(element);
  457. Sortable.options(dropon).onChange(element);
  458. }
  459. },
  460. unmark: function() {
  461. if(Sortable._marker) Element.hide(Sortable._marker);
  462. },
  463. mark: function(dropon, position) {
  464. // mark on ghosting only
  465. var sortable = Sortable.options(dropon.parentNode);
  466. if(sortable && !sortable.ghosting) return;
  467. if(!Sortable._marker) {
  468. Sortable._marker = $('dropmarker') || document.createElement('DIV');
  469. Element.hide(Sortable._marker);
  470. Element.addClassName(Sortable._marker, 'dropmarker');
  471. Sortable._marker.style.position = 'absolute';
  472. document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
  473. }
  474. var offsets = Position.cumulativeOffset(dropon);
  475. Sortable._marker.style.left = offsets[0] + 'px';
  476. Sortable._marker.style.top = offsets[1] + 'px';
  477. if(position=='after')
  478. if(sortable.overlap == 'horizontal')
  479. Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px';
  480. else
  481. Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px';
  482. Element.show(Sortable._marker);
  483. },
  484. serialize: function(element) {
  485. element = $(element);
  486. var sortableOptions = this.options(element);
  487. var options = Object.extend({
  488. tag: sortableOptions.tag,
  489. only: sortableOptions.only,
  490. name: element.id,
  491. format: sortableOptions.format || /^[^_]*_(.*)$/
  492. }, arguments[1] || {});
  493. var ret = $(this.findElements(element, options) || []).collect( function(item) {
  494. return (encodeURIComponent(options.name) + "[]=" +
  495. encodeURIComponent(item.id.match(options.format) ? item.id.match(options.format)[1] : ''));
  496. }).join("&");
  497. return ret;
  498. }
  499. }