Sortable.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  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. See scriptaculous.js for full license.
  5. ***/
  6. if (typeof(dojo) != 'undefined') {
  7. dojo.provide('MochiKit.DragAndDrop');
  8. dojo.require('MochiKit.Base');
  9. dojo.require('MochiKit.DOM');
  10. dojo.require('MochiKit.Iter');
  11. }
  12. if (typeof(JSAN) != 'undefined') {
  13. JSAN.use("MochiKit.Base", []);
  14. JSAN.use("MochiKit.DOM", []);
  15. JSAN.use("MochiKit.Iter", []);
  16. }
  17. try {
  18. if (typeof(MochiKit.Base) == 'undefined' ||
  19. typeof(MochiKit.DOM) == 'undefined' ||
  20. typeof(MochiKit.Iter) == 'undefined') {
  21. throw "";
  22. }
  23. } catch (e) {
  24. throw "MochiKit.DragAndDrop depends on MochiKit.Base, MochiKit.DOM and MochiKit.Iter!";
  25. }
  26. if (typeof(MochiKit.Sortable) == 'undefined') {
  27. MochiKit.Sortable = {};
  28. }
  29. MochiKit.Sortable.NAME = 'MochiKit.Sortable';
  30. MochiKit.Sortable.VERSION = '1.4';
  31. MochiKit.Sortable.__repr__ = function () {
  32. return '[' + this.NAME + ' ' + this.VERSION + ']';
  33. };
  34. MochiKit.Sortable.toString = function () {
  35. return this.__repr__();
  36. };
  37. MochiKit.Sortable.EXPORT = [
  38. "SortableObserver"
  39. ];
  40. MochiKit.DragAndDrop.EXPORT_OK = [
  41. "Sortable"
  42. ];
  43. MochiKit.Sortable.SortableObserver = function (element, observer) {
  44. this.__init__(element, observer);
  45. };
  46. MochiKit.Sortable.SortableObserver.prototype = {
  47. /***
  48. Observe events of drag and drop sortables.
  49. ***/
  50. __init__: function (element, observer) {
  51. this.element = MochiKit.DOM.getElement(element);
  52. this.observer = observer;
  53. this.lastValue = MochiKit.Sortable.Sortable.serialize(this.element);
  54. },
  55. onStart: function () {
  56. this.lastValue = MochiKit.Sortable.Sortable.serialize(this.element);
  57. },
  58. onEnd: function () {
  59. MochiKit.Sortable.Sortable.unmark();
  60. if (this.lastValue != MochiKit.Sortable.Sortable.serialize(this.element)) {
  61. this.observer(this.element)
  62. }
  63. }
  64. };
  65. MochiKit.Sortable.Sortable = {
  66. /***
  67. Manage sortables. Mainly use the create function to add a sortable.
  68. ***/
  69. sortables: {},
  70. _findRootElement: function (element) {
  71. while (element.tagName != "BODY") {
  72. if (element.id && MochiKit.Sortable.Sortable.sortables[element.id]) {
  73. return element;
  74. }
  75. element = element.parentNode;
  76. }
  77. },
  78. options: function (element) {
  79. element = MochiKit.Sortable.Sortable._findRootElement(MochiKit.DOM.getElement(element));
  80. if (!element) {
  81. return;
  82. }
  83. return MochiKit.Sortable.Sortable.sortables[element.id];
  84. },
  85. destroy: function (element){
  86. var s = MochiKit.Sortable.Sortable.options(element);
  87. var b = MochiKit.Base;
  88. var d = MochiKit.DragAndDrop;
  89. if (s) {
  90. d.Draggables.removeObserver(s.element);
  91. b.map(function (dr) {
  92. d.Droppables.remove(dr);
  93. }, s.droppables);
  94. b.map(function (dr) {
  95. dr.destroy();
  96. }, s.draggables);
  97. delete MochiKit.Sortable.Sortable.sortables[s.element.id];
  98. }
  99. },
  100. create: function (element, options) {
  101. element = MochiKit.DOM.getElement(element);
  102. var self = MochiKit.Sortable.Sortable;
  103. options = MochiKit.Base.update({
  104. element: element,
  105. tag: 'li', // assumes li children, override with tag: 'tagname'
  106. dropOnEmpty: false,
  107. tree: false,
  108. treeTag: 'ul',
  109. overlap: 'vertical', // one of 'vertical', 'horizontal'
  110. constraint: 'vertical', // one of 'vertical', 'horizontal', false
  111. // also takes array of elements (or ids); or false
  112. containment: [element],
  113. handle: false, // or a CSS class
  114. only: false,
  115. hoverclass: null,
  116. ghosting: false,
  117. scroll: false,
  118. scrollSensitivity: 20,
  119. scrollSpeed: 15,
  120. format: /^[^_]*_(.*)$/,
  121. onChange: MochiKit.Base.noop,
  122. onUpdate: MochiKit.Base.noop,
  123. accept: null
  124. }, options);
  125. // clear any old sortable with same element
  126. self.destroy(element);
  127. // build options for the draggables
  128. var options_for_draggable = {
  129. revert: true,
  130. ghosting: options.ghosting,
  131. scroll: options.scroll,
  132. scrollSensitivity: options.scrollSensitivity,
  133. scrollSpeed: options.scrollSpeed,
  134. constraint: options.constraint,
  135. handle: options.handle
  136. };
  137. if (options.starteffect) {
  138. options_for_draggable.starteffect = options.starteffect;
  139. }
  140. if (options.reverteffect) {
  141. options_for_draggable.reverteffect = options.reverteffect;
  142. } else if (options.ghosting) {
  143. options_for_draggable.reverteffect = function (innerelement) {
  144. innerelement.style.top = 0;
  145. innerelement.style.left = 0;
  146. };
  147. }
  148. if (options.endeffect) {
  149. options_for_draggable.endeffect = options.endeffect;
  150. }
  151. if (options.zindex) {
  152. options_for_draggable.zindex = options.zindex;
  153. }
  154. // build options for the droppables
  155. var options_for_droppable = {
  156. overlap: options.overlap,
  157. containment: options.containment,
  158. hoverclass: options.hoverclass,
  159. onhover: self.onHover,
  160. tree: options.tree,
  161. accept: options.accept
  162. }
  163. var options_for_tree = {
  164. onhover: self.onEmptyHover,
  165. overlap: options.overlap,
  166. containment: options.containment,
  167. hoverclass: options.hoverclass,
  168. accept: options.accept
  169. }
  170. // fix for gecko engine
  171. MochiKit.DOM.removeEmptyTextNodes(element);
  172. options.draggables = [];
  173. options.droppables = [];
  174. // drop on empty handling
  175. if (options.dropOnEmpty || options.tree) {
  176. new MochiKit.DragAndDrop.Droppable(element, options_for_tree);
  177. options.droppables.push(element);
  178. }
  179. MochiKit.Base.map(function (e) {
  180. // handles are per-draggable
  181. var handle = options.handle ?
  182. MochiKit.DOM.getFirstElementByTagAndClassName(null,
  183. options.handle, e) : e;
  184. options.draggables.push(
  185. new MochiKit.DragAndDrop.Draggable(e,
  186. MochiKit.Base.update(options_for_draggable,
  187. {handle: handle})));
  188. new MochiKit.DragAndDrop.Droppable(e, options_for_droppable);
  189. if (options.tree) {
  190. e.treeNode = element;
  191. }
  192. options.droppables.push(e);
  193. }, (self.findElements(element, options) || []));
  194. if (options.tree) {
  195. MochiKit.Base.map(function (e) {
  196. new MochiKit.DragAndDrop.Droppable(e, options_for_tree);
  197. e.treeNode = element;
  198. options.droppables.push(e);
  199. }, (self.findTreeElements(element, options) || []));
  200. }
  201. // keep reference
  202. self.sortables[element.id] = options;
  203. // for onupdate
  204. MochiKit.DragAndDrop.Draggables.addObserver(
  205. new MochiKit.Sortable.SortableObserver(element, options.onUpdate));
  206. },
  207. // return all suitable-for-sortable elements in a guaranteed order
  208. findElements: function (element, options) {
  209. return MochiKit.Sortable.Sortable.findChildren(
  210. element, options.only, options.tree ? true : false, options.tag);
  211. },
  212. findTreeElements: function (element, options) {
  213. return MochiKit.Sortable.Sortable.findChildren(
  214. element, options.only, options.tree ? true : false, options.treeTag);
  215. },
  216. findChildren: function (element, only, recursive, tagName) {
  217. if (!element.hasChildNodes()) {
  218. return null;
  219. }
  220. tagName = tagName.toUpperCase();
  221. if (only) {
  222. only = MochiKit.Base.flattenArray([only]);
  223. }
  224. var elements = [];
  225. MochiKit.Base.map(function (e) {
  226. if (e.tagName &&
  227. e.tagName.toUpperCase() == tagName &&
  228. (!only ||
  229. MochiKit.Iter.some(only, function (c) {
  230. return MochiKit.DOM.hasElementClass(e, c);
  231. }))) {
  232. elements.push(e);
  233. }
  234. if (recursive) {
  235. var grandchildren = MochiKit.Sortable.Sortable.findChildren(e, only, recursive, tagName);
  236. if (grandchildren && grandchildren.length > 0) {
  237. elements = elements.concat(grandchildren);
  238. }
  239. }
  240. }, element.childNodes);
  241. return elements;
  242. },
  243. onHover: function (element, dropon, overlap) {
  244. if (MochiKit.DOM.isParent(dropon, element)) {
  245. return;
  246. }
  247. var self = MochiKit.Sortable.Sortable;
  248. if (overlap > .33 && overlap < .66 && self.options(dropon).tree) {
  249. return;
  250. } else if (overlap > 0.5) {
  251. self.mark(dropon, 'before');
  252. if (dropon.previousSibling != element) {
  253. var oldParentNode = element.parentNode;
  254. element.style.visibility = 'hidden'; // fix gecko rendering
  255. dropon.parentNode.insertBefore(element, dropon);
  256. if (dropon.parentNode != oldParentNode) {
  257. self.options(oldParentNode).onChange(element);
  258. }
  259. self.options(dropon.parentNode).onChange(element);
  260. }
  261. } else {
  262. self.mark(dropon, 'after');
  263. var nextElement = dropon.nextSibling || null;
  264. if (nextElement != element) {
  265. var oldParentNode = element.parentNode;
  266. element.style.visibility = 'hidden'; // fix gecko rendering
  267. dropon.parentNode.insertBefore(element, nextElement);
  268. if (dropon.parentNode != oldParentNode) {
  269. self.options(oldParentNode).onChange(element);
  270. }
  271. self.options(dropon.parentNode).onChange(element);
  272. }
  273. }
  274. },
  275. _offsetSize: function (element, type) {
  276. if (type == 'vertical' || type == 'height') {
  277. return element.offsetHeight;
  278. } else {
  279. return element.offsetWidth;
  280. }
  281. },
  282. onEmptyHover: function (element, dropon, overlap) {
  283. var oldParentNode = element.parentNode;
  284. var self = MochiKit.Sortable.Sortable;
  285. var droponOptions = self.options(dropon);
  286. if (!MochiKit.DOM.isParent(dropon, element)) {
  287. var index;
  288. var children = self.findElements(dropon, {tag: droponOptions.tag,
  289. only: droponOptions.only});
  290. var child = null;
  291. if (children) {
  292. var offset = self._offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
  293. for (index = 0; index < children.length; index += 1) {
  294. if (offset - self._offsetSize(children[index], droponOptions.overlap) >= 0) {
  295. offset -= self._offsetSize(children[index], droponOptions.overlap);
  296. } else if (offset - (self._offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
  297. child = index + 1 < children.length ? children[index + 1] : null;
  298. break;
  299. } else {
  300. child = children[index];
  301. break;
  302. }
  303. }
  304. }
  305. dropon.insertBefore(element, child);
  306. self.options(oldParentNode).onChange(element);
  307. droponOptions.onChange(element);
  308. }
  309. },
  310. unmark: function () {
  311. var m = MochiKit.Sortable.Sortable._marker;
  312. if (m) {
  313. MochiKit.Style.hideElement(m);
  314. }
  315. },
  316. mark: function (dropon, position) {
  317. // mark on ghosting only
  318. var d = MochiKit.DOM;
  319. var self = MochiKit.Sortable.Sortable;
  320. var sortable = self.options(dropon.parentNode);
  321. if (sortable && !sortable.ghosting) {
  322. return;
  323. }
  324. if (!self._marker) {
  325. self._marker = d.getElement('dropmarker') ||
  326. document.createElement('DIV');
  327. MochiKit.Style.hideElement(self._marker);
  328. d.addElementClass(self._marker, 'dropmarker');
  329. self._marker.style.position = 'absolute';
  330. document.getElementsByTagName('body').item(0).appendChild(self._marker);
  331. }
  332. var offsets = MochiKit.Position.cumulativeOffset(dropon);
  333. self._marker.style.left = offsets.x + 'px';
  334. self._marker.style.top = offsets.y + 'px';
  335. if (position == 'after') {
  336. if (sortable.overlap == 'horizontal') {
  337. self._marker.style.left = (offsets.x + dropon.clientWidth) + 'px';
  338. } else {
  339. self._marker.style.top = (offsets.y + dropon.clientHeight) + 'px';
  340. }
  341. }
  342. MochiKit.Style.showElement(self._marker);
  343. },
  344. _tree: function (element, options, parent) {
  345. var self = MochiKit.Sortable.Sortable;
  346. var children = self.findElements(element, options) || [];
  347. for (var i = 0; i < children.length; ++i) {
  348. var match = children[i].id.match(options.format);
  349. if (!match) {
  350. continue;
  351. }
  352. var child = {
  353. id: encodeURIComponent(match ? match[1] : null),
  354. element: element,
  355. parent: parent,
  356. children: [],
  357. position: parent.children.length,
  358. container: self._findChildrenElement(children[i], options.treeTag.toUpperCase())
  359. }
  360. /* Get the element containing the children and recurse over it */
  361. if (child.container) {
  362. self._tree(child.container, options, child)
  363. }
  364. parent.children.push (child);
  365. }
  366. return parent;
  367. },
  368. /* Finds the first element of the given tag type within a parent element.
  369. Used for finding the first LI[ST] within a L[IST]I[TEM].*/
  370. _findChildrenElement: function (element, containerTag) {
  371. if (element && element.hasChildNodes) {
  372. for (var i = 0; i < element.childNodes.length; ++i) {
  373. if (element.childNodes[i].tagName == containerTag) {
  374. return element.childNodes[i];
  375. }
  376. }
  377. }
  378. return null;
  379. },
  380. tree: function (element, options) {
  381. element = MochiKit.DOM.getElement(element);
  382. var sortableOptions = MochiKit.Sortable.Sortable.options(element);
  383. options = MochiKit.Base.update({
  384. tag: sortableOptions.tag,
  385. treeTag: sortableOptions.treeTag,
  386. only: sortableOptions.only,
  387. name: element.id,
  388. format: sortableOptions.format
  389. }, options || {});
  390. var root = {
  391. id: null,
  392. parent: null,
  393. children: new Array,
  394. container: element,
  395. position: 0
  396. }
  397. return MochiKit.Sortable.Sortable._tree(element, options, root);
  398. },
  399. setSequence: function (element, newSequence, options) {
  400. var self = MochiKit.Sortable.Sortable;
  401. var b = MochiKit.Base;
  402. element = MochiKit.DOM.getElement(element);
  403. options = b.update(self.options(element), options || {});
  404. var nodeMap = {};
  405. b.map(function (n) {
  406. var m = n.id.match(options.format);
  407. if (m) {
  408. nodeMap[m[1]] = [n, n.parentNode];
  409. }
  410. n.parentNode.removeChild(n);
  411. }, self.findElements(element, options));
  412. b.map(function (ident) {
  413. var n = nodeMap[ident];
  414. if (n) {
  415. n[1].appendChild(n[0]);
  416. delete nodeMap[ident];
  417. }
  418. }, newSequence);
  419. },
  420. /* Construct a [i] index for a particular node */
  421. _constructIndex: function (node) {
  422. var index = '';
  423. do {
  424. if (node.id) {
  425. index = '[' + node.position + ']' + index;
  426. }
  427. } while ((node = node.parent) != null);
  428. return index;
  429. },
  430. sequence: function (element, options) {
  431. element = MochiKit.DOM.getElement(element);
  432. var self = MochiKit.Sortable.Sortable;
  433. var options = MochiKit.Base.update(self.options(element), options || {});
  434. return MochiKit.Base.map(function (item) {
  435. return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
  436. }, MochiKit.DOM.getElement(self.findElements(element, options) || []));
  437. },
  438. serialize: function (element, options) {
  439. element = MochiKit.DOM.getElement(element);
  440. var self = MochiKit.Sortable.Sortable;
  441. options = MochiKit.Base.update(self.options(element), options || {});
  442. var name = encodeURIComponent(options.name || element.id);
  443. if (options.tree) {
  444. return MochiKit.Base.flattenArray(MochiKit.Base.map(function (item) {
  445. return [name + self._constructIndex(item) + "[id]=" +
  446. encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
  447. }, self.tree(element, options).children)).join('&');
  448. } else {
  449. return MochiKit.Base.map(function (item) {
  450. return name + "[]=" + encodeURIComponent(item);
  451. }, self.sequence(element, options)).join('&');
  452. }
  453. }
  454. };