MultiDrag.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. import {
  2. toggleClass,
  3. getRect,
  4. index,
  5. closest,
  6. on,
  7. off,
  8. clone,
  9. css,
  10. setRect,
  11. unsetRect,
  12. matrix,
  13. expando
  14. } from '../../src/utils.js';
  15. import dispatchEvent from '../../src/EventDispatcher.js';
  16. let multiDragElements = [],
  17. multiDragClones = [],
  18. lastMultiDragSelect, // for selection with modifier key down (SHIFT)
  19. multiDragSortable,
  20. initialFolding = false, // Initial multi-drag fold when drag started
  21. folding = false, // Folding any other time
  22. dragStarted = false,
  23. dragEl,
  24. clonesFromRect,
  25. clonesHidden;
  26. function MultiDragPlugin() {
  27. function MultiDrag(sortable) {
  28. // Bind all private methods
  29. for (let fn in this) {
  30. if (fn.charAt(0) === '_' && typeof this[fn] === 'function') {
  31. this[fn] = this[fn].bind(this);
  32. }
  33. }
  34. if (sortable.options.supportPointer) {
  35. on(document, 'pointerup', this._deselectMultiDrag);
  36. } else {
  37. on(document, 'mouseup', this._deselectMultiDrag);
  38. on(document, 'touchend', this._deselectMultiDrag);
  39. }
  40. on(document, 'keydown', this._checkKeyDown);
  41. on(document, 'keyup', this._checkKeyUp);
  42. this.defaults = {
  43. selectedClass: 'sortable-selected',
  44. multiDragKey: null,
  45. setData(dataTransfer, dragEl) {
  46. let data = '';
  47. if (multiDragElements.length && multiDragSortable === sortable) {
  48. multiDragElements.forEach((multiDragElement, i) => {
  49. data += (!i ? '' : ', ') + multiDragElement.textContent;
  50. });
  51. } else {
  52. data = dragEl.textContent;
  53. }
  54. dataTransfer.setData('Text', data);
  55. }
  56. };
  57. }
  58. MultiDrag.prototype = {
  59. multiDragKeyDown: false,
  60. isMultiDrag: false,
  61. delayStartGlobal({ dragEl: dragged }) {
  62. dragEl = dragged;
  63. },
  64. delayEnded() {
  65. this.isMultiDrag = ~multiDragElements.indexOf(dragEl);
  66. },
  67. setupClone({ sortable, cancel }) {
  68. if (!this.isMultiDrag) return;
  69. for (let i = 0; i < multiDragElements.length; i++) {
  70. multiDragClones.push(clone(multiDragElements[i]));
  71. multiDragClones[i].sortableIndex = multiDragElements[i].sortableIndex;
  72. multiDragClones[i].draggable = false;
  73. multiDragClones[i].style['will-change'] = '';
  74. toggleClass(multiDragClones[i], this.options.selectedClass, false);
  75. multiDragElements[i] === dragEl && toggleClass(multiDragClones[i], this.options.chosenClass, false);
  76. }
  77. sortable._hideClone();
  78. cancel();
  79. },
  80. clone({ sortable, rootEl, dispatchSortableEvent, cancel }) {
  81. if (!this.isMultiDrag) return;
  82. if (!this.options.removeCloneOnHide) {
  83. if (multiDragElements.length && multiDragSortable === sortable) {
  84. insertMultiDragClones(true, rootEl);
  85. dispatchSortableEvent('clone');
  86. cancel();
  87. }
  88. }
  89. },
  90. showClone({ cloneNowShown, rootEl, cancel }) {
  91. if (!this.isMultiDrag) return;
  92. insertMultiDragClones(false, rootEl);
  93. multiDragClones.forEach(clone => {
  94. css(clone, 'display', '');
  95. });
  96. cloneNowShown();
  97. clonesHidden = false;
  98. cancel();
  99. },
  100. hideClone({ sortable, cloneNowHidden, cancel }) {
  101. if (!this.isMultiDrag) return;
  102. multiDragClones.forEach(clone => {
  103. css(clone, 'display', 'none');
  104. if (this.options.removeCloneOnHide && clone.parentNode) {
  105. clone.parentNode.removeChild(clone);
  106. }
  107. });
  108. cloneNowHidden();
  109. clonesHidden = true;
  110. cancel();
  111. },
  112. dragStartGlobal({ sortable }) {
  113. if (!this.isMultiDrag && multiDragSortable) {
  114. multiDragSortable.multiDrag._deselectMultiDrag();
  115. }
  116. multiDragElements.forEach(multiDragElement => {
  117. multiDragElement.sortableIndex = index(multiDragElement);
  118. });
  119. // Sort multi-drag elements
  120. multiDragElements = multiDragElements.sort(function(a, b) {
  121. return a.sortableIndex - b.sortableIndex;
  122. });
  123. dragStarted = true;
  124. },
  125. dragStarted({ sortable }) {
  126. if (!this.isMultiDrag) return;
  127. if (this.options.sort) {
  128. // Capture rects,
  129. // hide multi drag elements (by positioning them absolute),
  130. // set multi drag elements rects to dragRect,
  131. // show multi drag elements,
  132. // animate to rects,
  133. // unset rects & remove from DOM
  134. sortable.captureAnimationState();
  135. if (this.options.animation) {
  136. multiDragElements.forEach(multiDragElement => {
  137. if (multiDragElement === dragEl) return;
  138. css(multiDragElement, 'position', 'absolute');
  139. });
  140. let dragRect = getRect(dragEl, false, true, true);
  141. multiDragElements.forEach(multiDragElement => {
  142. if (multiDragElement === dragEl) return;
  143. setRect(multiDragElement, dragRect);
  144. });
  145. folding = true;
  146. initialFolding = true;
  147. }
  148. }
  149. sortable.animateAll(() => {
  150. folding = false;
  151. initialFolding = false;
  152. if (this.options.animation) {
  153. multiDragElements.forEach(multiDragElement => {
  154. unsetRect(multiDragElement);
  155. });
  156. }
  157. // Remove all auxiliary multidrag items from el, if sorting enabled
  158. if (this.options.sort) {
  159. removeMultiDragElements();
  160. }
  161. });
  162. },
  163. dragOver({ target, completed, cancel }) {
  164. if (folding && ~multiDragElements.indexOf(target)) {
  165. completed(false);
  166. cancel();
  167. }
  168. },
  169. revert({ fromSortable, rootEl, sortable, dragRect }) {
  170. if (multiDragElements.length > 1) {
  171. // Setup unfold animation
  172. multiDragElements.forEach(multiDragElement => {
  173. sortable.addAnimationState({
  174. target: multiDragElement,
  175. rect: folding ? getRect(multiDragElement) : dragRect
  176. });
  177. unsetRect(multiDragElement);
  178. multiDragElement.fromRect = dragRect;
  179. fromSortable.removeAnimationState(multiDragElement);
  180. });
  181. folding = false;
  182. insertMultiDragElements(!this.options.removeCloneOnHide, rootEl);
  183. }
  184. },
  185. dragOverCompleted({ sortable, isOwner, insertion, activeSortable, parentEl, putSortable }) {
  186. let options = this.options;
  187. if (insertion) {
  188. // Clones must be hidden before folding animation to capture dragRectAbsolute properly
  189. if (isOwner) {
  190. activeSortable._hideClone();
  191. }
  192. initialFolding = false;
  193. // If leaving sort:false root, or already folding - Fold to new location
  194. if (options.animation && multiDragElements.length > 1 && (folding || !isOwner && !activeSortable.options.sort && !putSortable)) {
  195. // Fold: Set all multi drag elements's rects to dragEl's rect when multi-drag elements are invisible
  196. let dragRectAbsolute = getRect(dragEl, false, true, true);
  197. multiDragElements.forEach(multiDragElement => {
  198. if (multiDragElement === dragEl) return;
  199. setRect(multiDragElement, dragRectAbsolute);
  200. // Move element(s) to end of parentEl so that it does not interfere with multi-drag clones insertion if they are inserted
  201. // while folding, and so that we can capture them again because old sortable will no longer be fromSortable
  202. parentEl.appendChild(multiDragElement);
  203. });
  204. folding = true;
  205. }
  206. // Clones must be shown (and check to remove multi drags) after folding when interfering multiDragElements are moved out
  207. if (!isOwner) {
  208. // Only remove if not folding (folding will remove them anyways)
  209. if (!folding) {
  210. removeMultiDragElements();
  211. }
  212. if (multiDragElements.length > 1) {
  213. let clonesHiddenBefore = clonesHidden;
  214. activeSortable._showClone(sortable);
  215. // Unfold animation for clones if showing from hidden
  216. if (activeSortable.options.animation && !clonesHidden && clonesHiddenBefore) {
  217. multiDragClones.forEach(clone => {
  218. activeSortable.addAnimationState({
  219. target: clone,
  220. rect: clonesFromRect
  221. });
  222. clone.fromRect = clonesFromRect;
  223. clone.thisAnimationDuration = null;
  224. });
  225. }
  226. } else {
  227. activeSortable._showClone(sortable);
  228. }
  229. }
  230. }
  231. },
  232. dragOverAnimationCapture({ dragRect, isOwner, activeSortable }) {
  233. multiDragElements.forEach(multiDragElement => {
  234. multiDragElement.thisAnimationDuration = null;
  235. });
  236. if (activeSortable.options.animation && !isOwner && activeSortable.multiDrag.isMultiDrag) {
  237. clonesFromRect = Object.assign({}, dragRect);
  238. let dragMatrix = matrix(dragEl, true);
  239. clonesFromRect.top -= dragMatrix.f;
  240. clonesFromRect.left -= dragMatrix.e;
  241. }
  242. },
  243. dragOverAnimationComplete() {
  244. if (folding) {
  245. folding = false;
  246. removeMultiDragElements();
  247. }
  248. },
  249. drop({ originalEvent: evt, rootEl, parentEl, sortable, dispatchSortableEvent, oldIndex, putSortable }) {
  250. let toSortable = (putSortable || this.sortable);
  251. if (!evt) return;
  252. let options = this.options,
  253. children = parentEl.children;
  254. // Multi-drag selection
  255. if (!dragStarted) {
  256. if (options.multiDragKey && !this.multiDragKeyDown) {
  257. this._deselectMultiDrag();
  258. }
  259. toggleClass(dragEl, options.selectedClass, !~multiDragElements.indexOf(dragEl));
  260. if (!~multiDragElements.indexOf(dragEl)) {
  261. multiDragElements.push(dragEl);
  262. dispatchEvent({
  263. sortable,
  264. rootEl,
  265. name: 'select',
  266. targetEl: dragEl,
  267. originalEvt: evt
  268. });
  269. // Modifier activated, select from last to dragEl
  270. if (evt.shiftKey && lastMultiDragSelect && sortable.el.contains(lastMultiDragSelect)) {
  271. let lastIndex = index(lastMultiDragSelect),
  272. currentIndex = index(dragEl);
  273. if (~lastIndex && ~currentIndex && lastIndex !== currentIndex) {
  274. // Must include lastMultiDragSelect (select it), in case modified selection from no selection
  275. // (but previous selection existed)
  276. let n, i;
  277. if (currentIndex > lastIndex) {
  278. i = lastIndex;
  279. n = currentIndex;
  280. } else {
  281. i = currentIndex;
  282. n = lastIndex + 1;
  283. }
  284. for (; i < n; i++) {
  285. if (~multiDragElements.indexOf(children[i])) continue;
  286. toggleClass(children[i], options.selectedClass, true);
  287. multiDragElements.push(children[i]);
  288. dispatchEvent({
  289. sortable,
  290. rootEl,
  291. name: 'select',
  292. targetEl: children[i],
  293. originalEvt: evt
  294. });
  295. }
  296. }
  297. } else {
  298. lastMultiDragSelect = dragEl;
  299. }
  300. multiDragSortable = toSortable;
  301. } else {
  302. multiDragElements.splice(multiDragElements.indexOf(dragEl), 1);
  303. lastMultiDragSelect = null;
  304. dispatchEvent({
  305. sortable,
  306. rootEl,
  307. name: 'deselect',
  308. targetEl: dragEl,
  309. originalEvt: evt
  310. });
  311. }
  312. }
  313. // Multi-drag drop
  314. if (dragStarted && this.isMultiDrag) {
  315. // Do not "unfold" after around dragEl if reverted
  316. if ((parentEl[expando].options.sort || parentEl !== rootEl) && multiDragElements.length > 1) {
  317. let dragRect = getRect(dragEl),
  318. multiDragIndex = index(dragEl, ':not(.' + this.options.selectedClass + ')');
  319. if (!initialFolding && options.animation) dragEl.thisAnimationDuration = null;
  320. toSortable.captureAnimationState();
  321. if (!initialFolding) {
  322. if (options.animation) {
  323. dragEl.fromRect = dragRect;
  324. multiDragElements.forEach(multiDragElement => {
  325. multiDragElement.thisAnimationDuration = null;
  326. if (multiDragElement !== dragEl) {
  327. let rect = folding ? getRect(multiDragElement) : dragRect;
  328. multiDragElement.fromRect = rect;
  329. // Prepare unfold animation
  330. toSortable.addAnimationState({
  331. target: multiDragElement,
  332. rect: rect
  333. });
  334. }
  335. });
  336. }
  337. // Multi drag elements are not necessarily removed from the DOM on drop, so to reinsert
  338. // properly they must all be removed
  339. removeMultiDragElements();
  340. multiDragElements.forEach(multiDragElement => {
  341. if (children[multiDragIndex]) {
  342. parentEl.insertBefore(multiDragElement, children[multiDragIndex]);
  343. } else {
  344. parentEl.appendChild(multiDragElement);
  345. }
  346. multiDragIndex++;
  347. });
  348. // If initial folding is done, the elements may have changed position because they are now
  349. // unfolding around dragEl, even though dragEl may not have his index changed, so update event
  350. // must be fired here as Sortable will not.
  351. if (oldIndex === index(dragEl)) {
  352. let update = false;
  353. multiDragElements.forEach(multiDragElement => {
  354. if (multiDragElement.sortableIndex !== index(multiDragElement)) {
  355. update = true;
  356. return;
  357. }
  358. });
  359. if (update) {
  360. dispatchSortableEvent('update');
  361. }
  362. }
  363. }
  364. // Must be done after capturing individual rects (scroll bar)
  365. multiDragElements.forEach(multiDragElement => {
  366. unsetRect(multiDragElement);
  367. });
  368. toSortable.animateAll();
  369. }
  370. multiDragSortable = toSortable;
  371. }
  372. // Remove clones if necessary
  373. if (rootEl === parentEl || (putSortable && putSortable.lastPutMode !== 'clone')) {
  374. multiDragClones.forEach(clone => {
  375. clone.parentNode && clone.parentNode.removeChild(clone);
  376. });
  377. }
  378. },
  379. nullingGlobal() {
  380. this.isMultiDrag =
  381. dragStarted = false;
  382. multiDragClones.length = 0;
  383. },
  384. destroyGlobal() {
  385. this._deselectMultiDrag();
  386. off(document, 'pointerup', this._deselectMultiDrag);
  387. off(document, 'mouseup', this._deselectMultiDrag);
  388. off(document, 'touchend', this._deselectMultiDrag);
  389. off(document, 'keydown', this._checkKeyDown);
  390. off(document, 'keyup', this._checkKeyUp);
  391. },
  392. _deselectMultiDrag(evt) {
  393. if (typeof dragStarted !== "undefined" && dragStarted) return;
  394. // Only deselect if selection is in this sortable
  395. if (multiDragSortable !== this.sortable) return;
  396. // Only deselect if target is not item in this sortable
  397. if (evt && closest(evt.target, this.options.draggable, this.sortable.el, false)) return;
  398. // Only deselect if left click
  399. if (evt && evt.button !== 0) return;
  400. while (multiDragElements.length) {
  401. let el = multiDragElements[0];
  402. toggleClass(el, this.options.selectedClass, false);
  403. multiDragElements.shift();
  404. dispatchEvent({
  405. sortable: this.sortable,
  406. rootEl: this.sortable.el,
  407. name: 'deselect',
  408. targetEl: el,
  409. originalEvt: evt
  410. });
  411. }
  412. },
  413. _checkKeyDown(evt) {
  414. if (evt.key === this.options.multiDragKey) {
  415. this.multiDragKeyDown = true;
  416. }
  417. },
  418. _checkKeyUp(evt) {
  419. if (evt.key === this.options.multiDragKey) {
  420. this.multiDragKeyDown = false;
  421. }
  422. }
  423. };
  424. return Object.assign(MultiDrag, {
  425. // Static methods & properties
  426. pluginName: 'multiDrag',
  427. utils: {
  428. /**
  429. * Selects the provided multi-drag item
  430. * @param {HTMLElement} el The element to be selected
  431. */
  432. select(el) {
  433. let sortable = el.parentNode[expando];
  434. if (!sortable || !sortable.options.multiDrag || ~multiDragElements.indexOf(el)) return;
  435. if (multiDragSortable && multiDragSortable !== sortable) {
  436. multiDragSortable.multiDrag._deselectMultiDrag();
  437. multiDragSortable = sortable;
  438. }
  439. toggleClass(el, sortable.options.selectedClass, true);
  440. multiDragElements.push(el);
  441. },
  442. /**
  443. * Deselects the provided multi-drag item
  444. * @param {HTMLElement} el The element to be deselected
  445. */
  446. deselect(el) {
  447. let sortable = el.parentNode[expando],
  448. index = multiDragElements.indexOf(el);
  449. if (!sortable || !sortable.options.multiDrag || !~index) return;
  450. toggleClass(el, sortable.options.selectedClass, false);
  451. multiDragElements.splice(index, 1);
  452. }
  453. },
  454. eventProperties() {
  455. const oldIndicies = [],
  456. newIndicies = [];
  457. multiDragElements.forEach(multiDragElement => {
  458. oldIndicies.push({
  459. multiDragElement,
  460. index: multiDragElement.sortableIndex
  461. });
  462. // multiDragElements will already be sorted if folding
  463. let newIndex;
  464. if (folding && multiDragElement !== dragEl) {
  465. newIndex = -1;
  466. } else if (folding) {
  467. newIndex = index(multiDragElement, ':not(.' + this.options.selectedClass + ')');
  468. } else {
  469. newIndex = index(multiDragElement);
  470. }
  471. newIndicies.push({
  472. multiDragElement,
  473. index: newIndex
  474. });
  475. });
  476. return {
  477. items: [...multiDragElements],
  478. clones: [...multiDragClones],
  479. oldIndicies,
  480. newIndicies
  481. };
  482. },
  483. optionListeners: {
  484. multiDragKey(key) {
  485. key = key.toLowerCase();
  486. if (key === 'ctrl') {
  487. key = 'Control';
  488. } else if (key.length > 1) {
  489. key = key.charAt(0).toUpperCase() + key.substr(1);
  490. }
  491. return key;
  492. }
  493. }
  494. });
  495. }
  496. function insertMultiDragElements(clonesInserted, rootEl) {
  497. multiDragElements.forEach((multiDragElement, i) => {
  498. let target = rootEl.children[multiDragElement.sortableIndex + (clonesInserted ? Number(i) : 0)];
  499. if (target) {
  500. rootEl.insertBefore(multiDragElement, target);
  501. } else {
  502. rootEl.appendChild(multiDragElement);
  503. }
  504. });
  505. }
  506. /**
  507. * Insert multi-drag clones
  508. * @param {[Boolean]} elementsInserted Whether the multi-drag elements are inserted
  509. * @param {HTMLElement} rootEl
  510. */
  511. function insertMultiDragClones(elementsInserted, rootEl) {
  512. multiDragClones.forEach((clone, i) => {
  513. let target = rootEl.children[clone.sortableIndex + (elementsInserted ? Number(i) : 0)];
  514. if (target) {
  515. rootEl.insertBefore(clone, target);
  516. } else {
  517. rootEl.appendChild(clone);
  518. }
  519. });
  520. }
  521. function removeMultiDragElements() {
  522. multiDragElements.forEach(multiDragElement => {
  523. if (multiDragElement === dragEl) return;
  524. multiDragElement.parentNode && multiDragElement.parentNode.removeChild(multiDragElement);
  525. });
  526. }
  527. export default MultiDragPlugin;