Sortable.js 20 KB

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