html.sortable.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. ;(function(root, factory) {
  2. if (typeof define === 'function' && define.amd) {
  3. define(['jquery'], factory);
  4. } else if (typeof exports === 'object') {
  5. module.exports = factory(require('jquery'));
  6. } else {
  7. root.sortable = factory(root.jQuery);
  8. }
  9. }(this, function($) {
  10. /*
  11. * HTML5 Sortable jQuery Plugin
  12. * https://github.com/voidberg/html5sortable
  13. *
  14. * Original code copyright 2012 Ali Farhadi.
  15. * This version is mantained by Alexandru Badiu <andu@ctrlz.ro> & Lukas Oppermann <lukas@vea.re>
  16. *
  17. *
  18. * Released under the MIT license.
  19. */
  20. 'use strict';
  21. /*
  22. * variables global to the plugin
  23. */
  24. var dragging;
  25. var draggingHeight;
  26. var placeholders = $();
  27. var sortables = [];
  28. /*
  29. * remove event handlers from items
  30. * @param [jquery Collection] items
  31. * @info event.h5s (jquery way of namespacing events, to bind multiple handlers to the event)
  32. */
  33. var _removeItemEvents = function(items) {
  34. items.off('dragstart.h5s');
  35. items.off('dragend.h5s');
  36. items.off('selectstart.h5s');
  37. items.off('dragover.h5s');
  38. items.off('dragenter.h5s');
  39. items.off('drop.h5s');
  40. };
  41. /*
  42. * remove event handlers from sortable
  43. * @param [jquery Collection] sortable
  44. * @info event.h5s (jquery way of namespacing events, to bind multiple handlers to the event)
  45. */
  46. var _removeSortableEvents = function(sortable) {
  47. sortable.off('dragover.h5s');
  48. sortable.off('dragenter.h5s');
  49. sortable.off('drop.h5s');
  50. };
  51. /*
  52. * attache ghost to dataTransfer object
  53. * @param [event] original event
  54. * @param [object] ghost-object with item, x and y coordinates
  55. */
  56. var _attachGhost = function(event, ghost) {
  57. // this needs to be set for HTML5 drag & drop to work
  58. event.dataTransfer.effectAllowed = 'move';
  59. event.dataTransfer.setData('text', '');
  60. // check if setDragImage method is available
  61. if (event.dataTransfer.setDragImage) {
  62. event.dataTransfer.setDragImage(ghost.item, ghost.x, ghost.y);
  63. }
  64. };
  65. /**
  66. * _addGhostPos clones the dragged item and adds it as a Ghost item
  67. * @param [object] event - the event fired when dragstart is triggered
  68. * @param [object] ghost - .item = node, draggedItem = jQuery collection
  69. */
  70. var _addGhostPos = function(e, ghost) {
  71. if (!ghost.x) {
  72. ghost.x = parseInt(e.pageX - ghost.draggedItem.offset().left);
  73. }
  74. if (!ghost.y) {
  75. ghost.y = parseInt(e.pageY - ghost.draggedItem.offset().top);
  76. }
  77. return ghost;
  78. };
  79. /**
  80. * _makeGhost decides which way to make a ghost and passes it to attachGhost
  81. * @param [jQuery selection] $draggedItem - the item that the user drags
  82. */
  83. var _makeGhost = function($draggedItem) {
  84. return {
  85. item: $draggedItem[0],
  86. draggedItem: $draggedItem
  87. };
  88. };
  89. /**
  90. * _getGhost constructs ghost and attaches it to dataTransfer
  91. * @param [event] event - the original drag event object
  92. * @param [jQuery selection] $draggedItem - the item that the user drags
  93. * @param [object] ghostOpt - the ghost options
  94. */
  95. // TODO: could $draggedItem be replaced by event.target in all instances
  96. var _getGhost = function(event, $draggedItem) {
  97. // add ghost item & draggedItem to ghost object
  98. var ghost = _makeGhost($draggedItem);
  99. // attach ghost position
  100. ghost = _addGhostPos(event, ghost);
  101. // attach ghost to dataTransfer
  102. _attachGhost(event, ghost);
  103. };
  104. /*
  105. * return options if not set on sortable already
  106. * @param [object] soptions
  107. * @param [object] options
  108. */
  109. var _getOptions = function(soptions, options) {
  110. if (typeof soptions === 'undefined') {
  111. return options;
  112. }
  113. return soptions;
  114. };
  115. /*
  116. * remove data from sortable
  117. * @param [jquery Collection] a single sortable
  118. */
  119. var _removeSortableData = function(sortable) {
  120. sortable.removeData('opts');
  121. sortable.removeData('connectWith');
  122. sortable.removeData('items');
  123. sortable.removeAttr('aria-dropeffect');
  124. };
  125. /*
  126. * remove data from items
  127. * @param [jquery Collection] items
  128. */
  129. var _removeItemData = function(items) {
  130. items.removeAttr('aria-grabbed');
  131. items.removeAttr('draggable');
  132. items.removeAttr('role');
  133. };
  134. /*
  135. * check if two lists are connected
  136. * @param [jquery Collection] items
  137. */
  138. var _listsConnected = function(curList, destList) {
  139. if (curList[0] === destList[0]) {
  140. return true;
  141. }
  142. if (curList.data('connectWith') !== undefined) {
  143. return curList.data('connectWith') === destList.data('connectWith');
  144. }
  145. return false;
  146. };
  147. /*
  148. * destroy the sortable
  149. * @param [jquery Collection] a single sortable
  150. */
  151. var _destroySortable = function(sortable) {
  152. var opts = sortable.data('opts') || {};
  153. var items = sortable.children(opts.items);
  154. var handles = opts.handle ? items.find(opts.handle) : items;
  155. // remove event handlers & data from sortable
  156. _removeSortableEvents(sortable);
  157. _removeSortableData(sortable);
  158. // remove event handlers & data from items
  159. handles.off('mousedown.h5s');
  160. _removeItemEvents(items);
  161. _removeItemData(items);
  162. };
  163. /*
  164. * enable the sortable
  165. * @param [jquery Collection] a single sortable
  166. */
  167. var _enableSortable = function(sortable) {
  168. var opts = sortable.data('opts');
  169. var items = sortable.children(opts.items);
  170. var handles = opts.handle ? items.find(opts.handle) : items;
  171. sortable.attr('aria-dropeffect', 'move');
  172. handles.attr('draggable', 'true');
  173. // IE FIX for ghost
  174. // can be disabled as it has the side effect that other events
  175. // (e.g. click) will be ignored
  176. var spanEl = (document || window.document).createElement('span');
  177. if (typeof spanEl.dragDrop === 'function' && !opts.disableIEFix) {
  178. handles.on('mousedown.h5s', function() {
  179. if (items.index(this) !== -1) {
  180. this.dragDrop();
  181. } else {
  182. $(this).parents(opts.items)[0].dragDrop();
  183. }
  184. });
  185. }
  186. };
  187. /*
  188. * disable the sortable
  189. * @param [jquery Collection] a single sortable
  190. */
  191. var _disableSortable = function(sortable) {
  192. var opts = sortable.data('opts');
  193. var items = sortable.children(opts.items);
  194. var handles = opts.handle ? items.find(opts.handle) : items;
  195. sortable.attr('aria-dropeffect', 'none');
  196. handles.attr('draggable', false);
  197. handles.off('mousedown.h5s');
  198. };
  199. /*
  200. * reload the sortable
  201. * @param [jquery Collection] a single sortable
  202. * @description events need to be removed to not be double bound
  203. */
  204. var _reloadSortable = function(sortable) {
  205. var opts = sortable.data('opts');
  206. var items = sortable.children(opts.items);
  207. var handles = opts.handle ? items.find(opts.handle) : items;
  208. // remove event handlers from items
  209. _removeItemEvents(items);
  210. handles.off('mousedown.h5s');
  211. // remove event handlers from sortable
  212. _removeSortableEvents(sortable);
  213. };
  214. /*
  215. * public sortable object
  216. * @param [object|string] options|method
  217. */
  218. var sortable = function(selector, options) {
  219. var $sortables = $(selector);
  220. var method = String(options);
  221. options = $.extend({
  222. connectWith: false,
  223. placeholder: null,
  224. // dragImage can be null or a jQuery element
  225. dragImage: null,
  226. disableIEFix: false,
  227. placeholderClass: 'sortable-placeholder',
  228. draggingClass: 'sortable-dragging',
  229. hoverClass: false
  230. }, options);
  231. /* TODO: maxstatements should be 25, fix and remove line below */
  232. /*jshint maxstatements:false */
  233. return $sortables.each(function() {
  234. var $sortable = $(this);
  235. if (/enable|disable|destroy/.test(method)) {
  236. sortable[method]($sortable);
  237. return;
  238. }
  239. // get options & set options on sortable
  240. options = _getOptions($sortable.data('opts'), options);
  241. $sortable.data('opts', options);
  242. // reset sortable
  243. _reloadSortable($sortable);
  244. // initialize
  245. var items = $sortable.children(options.items);
  246. var index;
  247. var startParent;
  248. var newParent;
  249. var placeholder = (options.placeholder === null) ? $('<' + (/^ul|ol$/i.test(this.tagName) ? 'li' : 'div') + ' class="' + options.placeholderClass + '"/>') : $(options.placeholder).addClass(options.placeholderClass);
  250. // setup sortable ids
  251. if (!$sortable.attr('data-sortable-id')) {
  252. var id = sortables.length;
  253. sortables[id] = $sortable;
  254. $sortable.attr('data-sortable-id', id);
  255. items.attr('data-item-sortable-id', id);
  256. }
  257. $sortable.data('items', options.items);
  258. placeholders = placeholders.add(placeholder);
  259. if (options.connectWith) {
  260. $sortable.data('connectWith', options.connectWith);
  261. }
  262. _enableSortable($sortable);
  263. items.attr('role', 'option');
  264. items.attr('aria-grabbed', 'false');
  265. // Mouse over class
  266. if (options.hoverClass) {
  267. var hoverClass = 'sortable-over';
  268. if (typeof options.hoverClass === 'string') {
  269. hoverClass = options.hoverClass;
  270. }
  271. items.hover(function() {
  272. $(this).addClass(hoverClass);
  273. }, function() {
  274. $(this).removeClass(hoverClass);
  275. });
  276. }
  277. // Handle drag events on draggable items
  278. items.on('dragstart.h5s', function(e) {
  279. e.stopImmediatePropagation();
  280. if (options.dragImage) {
  281. _attachGhost(e.originalEvent, {
  282. item: options.dragImage,
  283. x: 0,
  284. y: 0
  285. });
  286. console.log('WARNING: dragImage option is deprecated' +
  287. ' and will be removed in the future!');
  288. } else {
  289. // add transparent clone or other ghost to cursor
  290. _getGhost(e.originalEvent, $(this), options.dragImage);
  291. }
  292. // cache selsection & add attr for dragging
  293. dragging = $(this);
  294. dragging.addClass(options.draggingClass);
  295. dragging.attr('aria-grabbed', 'true');
  296. // grab values
  297. index = dragging.index();
  298. draggingHeight = dragging.height();
  299. startParent = $(this).parent();
  300. // trigger sortstar update
  301. dragging.parent().triggerHandler('sortstart', {
  302. item: dragging,
  303. placeholder: placeholder,
  304. startparent: startParent
  305. });
  306. });
  307. // Handle drag events on draggable items
  308. items.on('dragend.h5s', function() {
  309. if (!dragging) {
  310. return;
  311. }
  312. // remove dragging attributes and show item
  313. dragging.removeClass(options.draggingClass);
  314. dragging.attr('aria-grabbed', 'false');
  315. dragging.show();
  316. placeholders.detach();
  317. newParent = $(this).parent();
  318. dragging.parent().triggerHandler('sortstop', {
  319. item: dragging,
  320. startparent: startParent,
  321. });
  322. if (index !== dragging.index() ||
  323. startParent.get(0) !== newParent.get(0)) {
  324. dragging.parent().triggerHandler('sortupdate', {
  325. item: dragging,
  326. index: newParent.children(newParent.data('items')).index(dragging),
  327. oldindex: items.index(dragging),
  328. elementIndex: dragging.index(),
  329. oldElementIndex: index,
  330. startparent: startParent,
  331. endparent: newParent
  332. });
  333. }
  334. dragging = null;
  335. draggingHeight = null;
  336. });
  337. // Handle drop event on sortable & placeholder
  338. // TODO: REMOVE placeholder?????
  339. $(this).add([placeholder]).on('drop.h5s', function(e) {
  340. if (!_listsConnected($sortable, $(dragging).parent())) {
  341. return;
  342. }
  343. e.stopPropagation();
  344. placeholders.filter(':visible').after(dragging);
  345. dragging.trigger('dragend.h5s');
  346. return false;
  347. });
  348. // Handle dragover and dragenter events on draggable items
  349. items.add([this]).on('dragover.h5s dragenter.h5s', function(e) {
  350. if (!_listsConnected($sortable, $(dragging).parent())) {
  351. return;
  352. }
  353. e.preventDefault();
  354. e.originalEvent.dataTransfer.dropEffect = 'move';
  355. if (items.is(this)) {
  356. var thisHeight = $(this).height();
  357. if (options.forcePlaceholderSize) {
  358. placeholder.height(draggingHeight);
  359. }
  360. // Check if $(this) is bigger than the draggable. If it is, we have to define a dead zone to prevent flickering
  361. if (thisHeight > draggingHeight) {
  362. // Dead zone?
  363. var deadZone = thisHeight - draggingHeight;
  364. var offsetTop = $(this).offset().top;
  365. if (placeholder.index() < $(this).index() &&
  366. e.originalEvent.pageY < offsetTop + deadZone) {
  367. return false;
  368. }
  369. if (placeholder.index() > $(this).index() &&
  370. e.originalEvent.pageY > offsetTop + thisHeight - deadZone) {
  371. return false;
  372. }
  373. }
  374. dragging.hide();
  375. if (placeholder.index() < $(this).index()) {
  376. $(this).after(placeholder);
  377. } else {
  378. $(this).before(placeholder);
  379. }
  380. placeholders.not(placeholder).detach();
  381. } else {
  382. if (!placeholders.is(this) && !$(this).children(options.items).length) {
  383. placeholders.detach();
  384. $(this).append(placeholder);
  385. }
  386. }
  387. return false;
  388. });
  389. });
  390. };
  391. sortable.destroy = function(sortable) {
  392. _destroySortable(sortable);
  393. };
  394. sortable.enable = function(sortable) {
  395. _enableSortable(sortable);
  396. };
  397. sortable.disable = function(sortable) {
  398. _disableSortable(sortable);
  399. };
  400. $.fn.sortable = function(options) {
  401. return sortable(this, options);
  402. };
  403. return sortable;
  404. }));