dragula.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. 'use strict';
  2. var emitter = require('contra/emitter');
  3. var crossvent = require('crossvent');
  4. var classes = require('./classes');
  5. var doc = document;
  6. var documentElement = doc.documentElement;
  7. var animateDuration = 300;
  8. function dragula (initialContainers, options) {
  9. var len = arguments.length;
  10. if (len === 1 && Array.isArray(initialContainers) === false) {
  11. options = initialContainers;
  12. initialContainers = [];
  13. }
  14. var _mirror; // mirror image
  15. var _source; // source container
  16. var _item; // item being dragged
  17. var _offsetX; // reference x
  18. var _offsetY; // reference y
  19. var _moveX; // reference move x
  20. var _moveY; // reference move y
  21. var _initialSibling; // reference sibling when grabbed
  22. var _currentSibling; // reference sibling now
  23. var _copy; // item used for copying
  24. var _renderTimer; // timer for setTimeout renderMirrorImage
  25. var _lastDropTarget = null; // last container item was over
  26. var _grabbed; // holds mousedown context until first mousemove
  27. var o = options || {};
  28. if (o.moves === void 0) { o.moves = always; }
  29. if (o.accepts === void 0) { o.accepts = always; }
  30. if (o.invalid === void 0) { o.invalid = invalidTarget; }
  31. if (o.containers === void 0) { o.containers = initialContainers || []; }
  32. if (o.isContainer === void 0) { o.isContainer = never; }
  33. if (o.copy === void 0) { o.copy = false; }
  34. if (o.copySortSource === void 0) { o.copySortSource = false; }
  35. if (o.revertOnSpill === void 0) { o.revertOnSpill = false; }
  36. if (o.removeOnSpill === void 0) { o.removeOnSpill = false; }
  37. if (o.direction === void 0) { o.direction = 'vertical'; }
  38. if (o.ignoreInputTextSelection === void 0) { o.ignoreInputTextSelection = true; }
  39. if (o.mirrorContainer === void 0) { o.mirrorContainer = doc.body; }
  40. if (o.staticClass === void 0) { o.staticClass = ''; }
  41. var drake = emitter({
  42. containers: o.containers,
  43. start: manualStart,
  44. end: end,
  45. cancel: cancel,
  46. remove: remove,
  47. destroy: destroy,
  48. canMove: canMove,
  49. dragging: false
  50. });
  51. if (o.removeOnSpill === true) {
  52. drake.on('over', spillOver).on('out', spillOut);
  53. }
  54. events();
  55. return drake;
  56. function isContainer (el) {
  57. return drake.containers.indexOf(el) !== -1 || o.isContainer(el);
  58. }
  59. function events (remove) {
  60. var op = remove ? 'remove' : 'add';
  61. touchy(documentElement, op, 'mousedown', grab);
  62. touchy(documentElement, op, 'mouseup', release);
  63. }
  64. function eventualMovements (remove) {
  65. var op = remove ? 'remove' : 'add';
  66. touchy(documentElement, op, 'mousemove', startBecauseMouseMoved);
  67. }
  68. function movements (remove) {
  69. var op = remove ? 'remove' : 'add';
  70. crossvent[op](documentElement, 'selectstart', preventGrabbed); // IE8
  71. crossvent[op](documentElement, 'click', preventGrabbed);
  72. }
  73. function destroy () {
  74. events(true);
  75. release({});
  76. }
  77. function preventGrabbed (e) {
  78. if (_grabbed) {
  79. e.preventDefault();
  80. }
  81. }
  82. function grab (e) {
  83. _moveX = e.clientX;
  84. _moveY = e.clientY;
  85. var ignore = whichMouseButton(e) !== 1 || e.metaKey || e.ctrlKey;
  86. if (ignore) {
  87. return; // we only care about honest-to-god left clicks and touch events
  88. }
  89. var item = e.target;
  90. var context = canStart(item);
  91. if (!context) {
  92. return;
  93. }
  94. _grabbed = context;
  95. eventualMovements();
  96. if (e.type === 'mousedown') {
  97. if (isInput(item)) { // see also: https://github.com/bevacqua/dragula/issues/208
  98. item.focus(); // fixes https://github.com/bevacqua/dragula/issues/176
  99. } else {
  100. e.preventDefault(); // fixes https://github.com/bevacqua/dragula/issues/155
  101. }
  102. }
  103. }
  104. function startBecauseMouseMoved (e) {
  105. if (!_grabbed) {
  106. return;
  107. }
  108. if (whichMouseButton(e) === 0) {
  109. release({});
  110. return; // when text is selected on an input and then dragged, mouseup doesn't fire. this is our only hope
  111. }
  112. // truthy check fixes #239, equality fixes #207
  113. if (e.clientX !== void 0 && e.clientX === _moveX && e.clientY !== void 0 && e.clientY === _moveY) {
  114. return;
  115. }
  116. if (o.ignoreInputTextSelection) {
  117. var clientX = getCoord('clientX', e);
  118. var clientY = getCoord('clientY', e);
  119. var elementBehindCursor = doc.elementFromPoint(clientX, clientY);
  120. if (isInput(elementBehindCursor)) {
  121. return;
  122. }
  123. }
  124. var grabbed = _grabbed; // call to end() unsets _grabbed
  125. eventualMovements(true);
  126. movements();
  127. end();
  128. start(grabbed);
  129. var offset = getOffset(_item);
  130. _offsetX = getCoord('pageX', e) - offset.left;
  131. _offsetY = getCoord('pageY', e) - offset.top;
  132. classes.add(_copy || _item, 'gu-transit');
  133. renderMirrorImage();
  134. drag(e);
  135. }
  136. function canStart (item) {
  137. if (drake.dragging && _mirror) {
  138. return;
  139. }
  140. if (isContainer(item)) {
  141. return; // don't drag container itself
  142. }
  143. var handle = item;
  144. while (getParent(item) && isContainer(getParent(item)) === false) {
  145. if (o.invalid(item, handle)) {
  146. return;
  147. }
  148. item = getParent(item); // drag target should be a top element
  149. if (!item) {
  150. return;
  151. }
  152. }
  153. var source = getParent(item);
  154. if (!source) {
  155. return;
  156. }
  157. if ((o.staticClass && item.classList.contains(o.staticClass))) {
  158. return;
  159. }
  160. if (o.invalid(item, handle)) {
  161. return;
  162. }
  163. var movable = o.moves(item, source, handle, nextEl(item));
  164. if (!movable) {
  165. return;
  166. }
  167. return {
  168. item: item,
  169. source: source
  170. };
  171. }
  172. function canMove (item) {
  173. return !!canStart(item);
  174. }
  175. function manualStart (item) {
  176. var context = canStart(item);
  177. if (context) {
  178. start(context);
  179. }
  180. }
  181. function start (context) {
  182. if (isCopy(context.item, context.source)) {
  183. _copy = context.item.cloneNode(true);
  184. drake.emit('cloned', _copy, context.item, 'copy');
  185. }
  186. _source = context.source;
  187. _item = context.item;
  188. _initialSibling = _currentSibling = nextEl(context.item);
  189. drake.dragging = true;
  190. drake.emit('drag', _item, _source);
  191. }
  192. function invalidTarget () {
  193. return false;
  194. }
  195. function end () {
  196. if (!drake.dragging) {
  197. return;
  198. }
  199. var item = _copy || _item;
  200. drop(item, getParent(item));
  201. }
  202. function ungrab () {
  203. _grabbed = false;
  204. eventualMovements(true);
  205. movements(true);
  206. }
  207. function release (e) {
  208. ungrab();
  209. if (!drake.dragging) {
  210. return;
  211. }
  212. var item = _copy || _item;
  213. var clientX = getCoord('clientX', e);
  214. var clientY = getCoord('clientY', e);
  215. var elementBehindCursor = getElementBehindPoint(_mirror, clientX, clientY);
  216. var dropTarget = findDropTarget(elementBehindCursor, clientX, clientY);
  217. if (dropTarget && ((_copy && o.copySortSource) || (!_copy || dropTarget !== _source))) {
  218. drop(item, dropTarget);
  219. } else if (o.removeOnSpill) {
  220. remove();
  221. } else {
  222. cancel();
  223. }
  224. }
  225. function drop (item, target) {
  226. var parent = getParent(item);
  227. if (_copy && o.copySortSource && target === _source) {
  228. parent.removeChild(_item);
  229. }
  230. if (isInitialPlacement(target)) {
  231. drake.emit('cancel', item, _source, _source);
  232. } else {
  233. drake.emit('drop', item, target, _source, _currentSibling);
  234. }
  235. cleanup();
  236. }
  237. function remove () {
  238. if (!drake.dragging) {
  239. return;
  240. }
  241. var item = _copy || _item;
  242. var parent = getParent(item);
  243. if (parent) {
  244. parent.removeChild(item);
  245. }
  246. drake.emit(_copy ? 'cancel' : 'remove', item, parent, _source);
  247. cleanup();
  248. }
  249. function cancel (revert) {
  250. if (!drake.dragging) {
  251. return;
  252. }
  253. var reverts = arguments.length > 0 ? revert : o.revertOnSpill;
  254. var item = _copy || _item;
  255. var parent = getParent(item);
  256. var initial = isInitialPlacement(parent);
  257. if (initial === false && reverts) {
  258. if (_copy) {
  259. if (parent) {
  260. parent.removeChild(_copy);
  261. }
  262. } else {
  263. _source.insertBefore(item, _initialSibling);
  264. }
  265. }
  266. if (initial || reverts) {
  267. drake.emit('cancel', item, _source, _source);
  268. } else {
  269. drake.emit('drop', item, parent, _source, _currentSibling);
  270. }
  271. cleanup();
  272. }
  273. function cleanup () {
  274. var item = _copy || _item;
  275. ungrab();
  276. removeMirrorImage();
  277. if (item) {
  278. classes.rm(item, 'gu-transit');
  279. }
  280. if (_renderTimer) {
  281. clearTimeout(_renderTimer);
  282. }
  283. drake.dragging = false;
  284. if (_lastDropTarget) {
  285. drake.emit('out', item, _lastDropTarget, _source);
  286. }
  287. drake.emit('dragend', item);
  288. _source = _item = _copy = _initialSibling = _currentSibling = _renderTimer = _lastDropTarget = null;
  289. }
  290. function isInitialPlacement (target, s) {
  291. var sibling;
  292. if (s !== void 0) {
  293. sibling = s;
  294. } else if (_mirror) {
  295. sibling = _currentSibling;
  296. } else {
  297. sibling = nextEl(_copy || _item);
  298. }
  299. return target === _source && sibling === _initialSibling;
  300. }
  301. function findDropTarget (elementBehindCursor, clientX, clientY) {
  302. var target = elementBehindCursor;
  303. while (target && !accepted()) {
  304. target = getParent(target);
  305. }
  306. return target;
  307. function accepted () {
  308. var droppable = isContainer(target);
  309. if (droppable === false) {
  310. return false;
  311. }
  312. var immediate = getImmediateChild(target, elementBehindCursor);
  313. var reference = getReference(target, immediate, clientX, clientY);
  314. var initial = isInitialPlacement(target, reference);
  315. if (initial) {
  316. return true; // should always be able to drop it right back where it was
  317. }
  318. return o.accepts(_item, target, _source, reference);
  319. }
  320. }
  321. function drag (e) {
  322. if (!_mirror) {
  323. return;
  324. }
  325. e.preventDefault();
  326. var clientX = getCoord('clientX', e);
  327. var clientY = getCoord('clientY', e);
  328. var x = clientX - _offsetX;
  329. var y = clientY - _offsetY;
  330. _mirror.style.left = x + 'px';
  331. _mirror.style.top = y + 'px';
  332. var item = _copy || _item;
  333. var elementBehindCursor = getElementBehindPoint(_mirror, clientX, clientY);
  334. var dropTarget = findDropTarget(elementBehindCursor, clientX, clientY);
  335. var changed = dropTarget !== null && dropTarget !== _lastDropTarget;
  336. if (changed || dropTarget === null) {
  337. out();
  338. _lastDropTarget = dropTarget;
  339. over();
  340. }
  341. var parent = getParent(item);
  342. if (dropTarget === _source && _copy && !o.copySortSource) {
  343. if (parent) {
  344. parent.removeChild(item);
  345. }
  346. return;
  347. }
  348. var reference;
  349. var immediate = getImmediateChild(dropTarget, elementBehindCursor);
  350. if (immediate !== null) {
  351. reference = getReference(dropTarget, immediate, clientX, clientY);
  352. } else if (o.revertOnSpill === true && !_copy) {
  353. reference = _initialSibling;
  354. dropTarget = _source;
  355. } else {
  356. if (_copy && parent) {
  357. parent.removeChild(item);
  358. }
  359. return;
  360. }
  361. if (
  362. (reference === null && changed) ||
  363. reference !== item &&
  364. reference !== nextEl(item)
  365. ) {
  366. _currentSibling = reference;
  367. var itemRect = item.getBoundingClientRect();
  368. var referenceRect = reference ? reference.getBoundingClientRect() : null;
  369. var direct = o.direction;
  370. // if isPositive is true, the direction is right or down
  371. var isPositive;
  372. if (referenceRect) {
  373. isPositive = direct === 'horizontal' ? (itemRect.x < referenceRect.x) : (itemRect.y < referenceRect.y);
  374. }else{
  375. isPositive = true;
  376. }
  377. // mover is the element to be exchange passively
  378. var mover;
  379. if (isPositive) {
  380. mover = reference ? (reference.previousElementSibling ? reference.previousElementSibling : reference) : dropTarget.lastElementChild;
  381. } else {
  382. mover = reference; //upward or right
  383. }
  384. if (!mover) {
  385. return;
  386. }
  387. if (o.staticClass && mover.classList.contains(o.staticClass)) {
  388. return;
  389. }
  390. var moverRect = mover && mover.getBoundingClientRect();
  391. dropTarget.insertBefore(item, reference);
  392. if (mover && moverRect) {
  393. animate(moverRect, mover);
  394. animate(itemRect, item);
  395. }
  396. drake.emit('shadow', item, dropTarget, _source);
  397. }
  398. function moved (type) { drake.emit(type, item, _lastDropTarget, _source); }
  399. function over () { if (changed) { moved('over'); } }
  400. function out () { if (_lastDropTarget) { moved('out'); } }
  401. }
  402. function spillOver (el) {
  403. classes.rm(el, 'gu-hide');
  404. }
  405. function spillOut (el) {
  406. if (drake.dragging) { classes.add(el, 'gu-hide'); }
  407. }
  408. function renderMirrorImage () {
  409. if (_mirror) {
  410. return;
  411. }
  412. var rect = _item.getBoundingClientRect();
  413. _mirror = _item.cloneNode(true);
  414. _mirror.style.width = getRectWidth(rect) + 'px';
  415. _mirror.style.height = getRectHeight(rect) + 'px';
  416. classes.rm(_mirror, 'gu-transit');
  417. classes.add(_mirror, 'gu-mirror');
  418. o.mirrorContainer.appendChild(_mirror);
  419. touchy(documentElement, 'add', 'mousemove', drag);
  420. classes.add(o.mirrorContainer, 'gu-unselectable');
  421. drake.emit('cloned', _mirror, _item, 'mirror');
  422. }
  423. function removeMirrorImage () {
  424. if (_mirror) {
  425. classes.rm(o.mirrorContainer, 'gu-unselectable');
  426. touchy(documentElement, 'remove', 'mousemove', drag);
  427. getParent(_mirror).removeChild(_mirror);
  428. _mirror = null;
  429. }
  430. }
  431. function getImmediateChild (dropTarget, target) {
  432. var immediate = target;
  433. while (immediate !== dropTarget && getParent(immediate) !== dropTarget) {
  434. immediate = getParent(immediate);
  435. }
  436. if (immediate === documentElement) {
  437. return null;
  438. }
  439. return immediate;
  440. }
  441. function getReference (dropTarget, target, x, y) {
  442. var horizontal = o.direction === 'horizontal';
  443. var reference = target !== dropTarget ? inside() : outside();
  444. return reference;
  445. function outside () { // slower, but able to figure out any position
  446. var len = dropTarget.children.length;
  447. var i;
  448. var el;
  449. var rect;
  450. for (i = 0; i < len; i++) {
  451. el = dropTarget.children[i];
  452. rect = el.getBoundingClientRect();
  453. if (horizontal && (rect.left + rect.width / 2) > x) { return el; }
  454. if (!horizontal && (rect.top + rect.height / 2) > y) { return el; }
  455. }
  456. return null;
  457. }
  458. function inside () { // faster, but only available if dropped inside a child element
  459. var rect = target.getBoundingClientRect();
  460. if (horizontal) {
  461. return resolve(x > rect.left + getRectWidth(rect) / 2);
  462. }
  463. return resolve(y > rect.top + getRectHeight(rect) / 2);
  464. }
  465. function resolve (after) {
  466. return after ? nextEl(target) : target;
  467. }
  468. }
  469. function isCopy (item, container) {
  470. return typeof o.copy === 'boolean' ? o.copy : o.copy(item, container);
  471. }
  472. }
  473. function touchy (el, op, type, fn) {
  474. var touch = {
  475. mouseup: 'touchend',
  476. mousedown: 'touchstart',
  477. mousemove: 'touchmove'
  478. };
  479. var pointers = {
  480. mouseup: 'pointerup',
  481. mousedown: 'pointerdown',
  482. mousemove: 'pointermove'
  483. };
  484. var microsoft = {
  485. mouseup: 'MSPointerUp',
  486. mousedown: 'MSPointerDown',
  487. mousemove: 'MSPointerMove'
  488. };
  489. if (global.navigator.pointerEnabled) {
  490. crossvent[op](el, pointers[type], fn);
  491. } else if (global.navigator.msPointerEnabled) {
  492. crossvent[op](el, microsoft[type], fn);
  493. } else {
  494. crossvent[op](el, touch[type], fn);
  495. crossvent[op](el, type, fn);
  496. }
  497. }
  498. function whichMouseButton (e) {
  499. if (e.touches !== void 0) { return e.touches.length; }
  500. if (e.which !== void 0 && e.which !== 0) { return e.which; } // see https://github.com/bevacqua/dragula/issues/261
  501. if (e.buttons !== void 0) { return e.buttons; }
  502. var button = e.button;
  503. if (button !== void 0) { // see https://github.com/jquery/jquery/blob/99e8ff1baa7ae341e94bb89c3e84570c7c3ad9ea/src/event.js#L573-L575
  504. return button & 1 ? 1 : button & 2 ? 3 : (button & 4 ? 2 : 0);
  505. }
  506. }
  507. function getOffset (el) {
  508. var rect = el.getBoundingClientRect();
  509. return {
  510. left: rect.left + getScroll('scrollLeft', 'pageXOffset'),
  511. top: rect.top + getScroll('scrollTop', 'pageYOffset')
  512. };
  513. }
  514. function getScroll (scrollProp, offsetProp) {
  515. if (typeof global[offsetProp] !== 'undefined') {
  516. return global[offsetProp];
  517. }
  518. if (documentElement.clientHeight) {
  519. return documentElement[scrollProp];
  520. }
  521. return doc.body[scrollProp];
  522. }
  523. function getElementBehindPoint (point, x, y) {
  524. var p = point || {};
  525. var state = p.className;
  526. var el;
  527. p.className += ' gu-hide';
  528. el = doc.elementFromPoint(x, y);
  529. p.className = state;
  530. return el;
  531. }
  532. function never () { return false; }
  533. function always () { return true; }
  534. function getRectWidth (rect) { return rect.width || (rect.right - rect.left); }
  535. function getRectHeight (rect) { return rect.height || (rect.bottom - rect.top); }
  536. function getParent (el) { return el.parentNode === doc ? null : el.parentNode; }
  537. function isInput (el) { return el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT' || isEditable(el); }
  538. function isEditable (el) {
  539. if (!el) { return false; } // no parents were editable
  540. if (el.contentEditable === 'false') { return false; } // stop the lookup
  541. if (el.contentEditable === 'true') { return true; } // found a contentEditable element in the chain
  542. return isEditable(getParent(el)); // contentEditable is set to 'inherit'
  543. }
  544. function nextEl (el) {
  545. return el.nextElementSibling || manually();
  546. function manually () {
  547. var sibling = el;
  548. do {
  549. sibling = sibling.nextSibling;
  550. } while (sibling && sibling.nodeType !== 1);
  551. return sibling;
  552. }
  553. }
  554. /**
  555. * Create an animation from position before sorting to present position
  556. * @param prevRect including element's position infomation before sorting
  557. * @param target element after sorting
  558. */
  559. function animate (prevRect, target) {
  560. if (!prevRect || !target) {
  561. return;
  562. }
  563. var currentRect = target.getBoundingClientRect();
  564. var originProps = {transition: target.style.transition, transform: target.style.transform};
  565. Object.assign(target.style, {
  566. transition: 'none',
  567. transform: 'translate(' + (prevRect.left - currentRect.left) + 'px,' + (prevRect.top - currentRect.top) + 'px)'
  568. });
  569. target.offsetWidth; // repaint
  570. Object.assign(target.style, {transition: 'all ' + animateDuration + 'ms', transform: 'translate(0,0)'});
  571. clearTimeout(target.animated);
  572. target.animated = setTimeout(function () {
  573. Object.assign(target.style, {originProps: originProps});
  574. target.animated = false;
  575. }, animateDuration);
  576. }
  577. function getEventHost (e) {
  578. // on touchend event, we have to use `e.changedTouches`
  579. // see http://stackoverflow.com/questions/7192563/touchend-event-properties
  580. // see https://github.com/bevacqua/dragula/issues/34
  581. if (e.targetTouches && e.targetTouches.length) {
  582. return e.targetTouches[0];
  583. }
  584. if (e.changedTouches && e.changedTouches.length) {
  585. return e.changedTouches[0];
  586. }
  587. return e;
  588. }
  589. function getCoord (coord, e) {
  590. var host = getEventHost(e);
  591. var missMap = {
  592. pageX: 'clientX', // IE8
  593. pageY: 'clientY' // IE8
  594. };
  595. if (coord in missMap && !(coord in host) && missMap[coord] in host) {
  596. coord = missMap[coord];
  597. }
  598. return host[coord];
  599. }
  600. module.exports = dragula;