DragDropTouch.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. var DragDropTouch;
  2. (function (DragDropTouch_1) {
  3. 'use strict';
  4. /**
  5. * Object used to hold the data that is being dragged during drag and drop operations.
  6. *
  7. * It may hold one or more data items of different types. For more information about
  8. * drag and drop operations and data transfer objects, see
  9. * <a href="https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer">HTML Drag and Drop API</a>.
  10. *
  11. * This object is created automatically by the @see:DragDropTouch singleton and is
  12. * accessible through the @see:dataTransfer property of all drag events.
  13. */
  14. var DataTransfer = (function () {
  15. function DataTransfer() {
  16. this._dropEffect = 'move';
  17. this._effectAllowed = 'all';
  18. this._data = {};
  19. }
  20. Object.defineProperty(DataTransfer.prototype, "dropEffect", {
  21. /**
  22. * Gets or sets the type of drag-and-drop operation currently selected.
  23. * The value must be 'none', 'copy', 'link', or 'move'.
  24. */
  25. get: function () {
  26. return this._dropEffect;
  27. },
  28. set: function (value) {
  29. this._dropEffect = value;
  30. },
  31. enumerable: true,
  32. configurable: true
  33. });
  34. Object.defineProperty(DataTransfer.prototype, "effectAllowed", {
  35. /**
  36. * Gets or sets the types of operations that are possible.
  37. * Must be one of 'none', 'copy', 'copyLink', 'copyMove', 'link',
  38. * 'linkMove', 'move', 'all' or 'uninitialized'.
  39. */
  40. get: function () {
  41. return this._effectAllowed;
  42. },
  43. set: function (value) {
  44. this._effectAllowed = value;
  45. },
  46. enumerable: true,
  47. configurable: true
  48. });
  49. Object.defineProperty(DataTransfer.prototype, "types", {
  50. /**
  51. * Gets an array of strings giving the formats that were set in the @see:dragstart event.
  52. */
  53. get: function () {
  54. return Object.keys(this._data);
  55. },
  56. enumerable: true,
  57. configurable: true
  58. });
  59. /**
  60. * Removes the data associated with a given type.
  61. *
  62. * The type argument is optional. If the type is empty or not specified, the data
  63. * associated with all types is removed. If data for the specified type does not exist,
  64. * or the data transfer contains no data, this method will have no effect.
  65. *
  66. * @param type Type of data to remove.
  67. */
  68. DataTransfer.prototype.clearData = function (type) {
  69. if (type != null) {
  70. delete this._data[type];
  71. }
  72. else {
  73. this._data = null;
  74. }
  75. };
  76. /**
  77. * Retrieves the data for a given type, or an empty string if data for that type does
  78. * not exist or the data transfer contains no data.
  79. *
  80. * @param type Type of data to retrieve.
  81. */
  82. DataTransfer.prototype.getData = function (type) {
  83. return this._data[type] || '';
  84. };
  85. /**
  86. * Set the data for a given type.
  87. *
  88. * For a list of recommended drag types, please see
  89. * https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Recommended_Drag_Types.
  90. *
  91. * @param type Type of data to add.
  92. * @param value Data to add.
  93. */
  94. DataTransfer.prototype.setData = function (type, value) {
  95. this._data[type] = value;
  96. };
  97. /**
  98. * Set the image to be used for dragging if a custom one is desired.
  99. *
  100. * @param img An image element to use as the drag feedback image.
  101. * @param offsetX The horizontal offset within the image.
  102. * @param offsetY The vertical offset within the image.
  103. */
  104. DataTransfer.prototype.setDragImage = function (img, offsetX, offsetY) {
  105. var ddt = DragDropTouch._instance;
  106. ddt._imgCustom = img;
  107. ddt._imgOffset = { x: offsetX, y: offsetY };
  108. };
  109. return DataTransfer;
  110. }());
  111. DragDropTouch_1.DataTransfer = DataTransfer;
  112. /**
  113. * Defines a class that adds support for touch-based HTML5 drag/drop operations.
  114. *
  115. * The @see:DragDropTouch class listens to touch events and raises the
  116. * appropriate HTML5 drag/drop events as if the events had been caused
  117. * by mouse actions.
  118. *
  119. * The purpose of this class is to enable using existing, standard HTML5
  120. * drag/drop code on mobile devices running IOS or Android.
  121. *
  122. * To use, include the DragDropTouch.js file on the page. The class will
  123. * automatically start monitoring touch events and will raise the HTML5
  124. * drag drop events (dragstart, dragenter, dragleave, drop, dragend) which
  125. * should be handled by the application.
  126. *
  127. * For details and examples on HTML drag and drop, see
  128. * https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Drag_operations.
  129. */
  130. var DragDropTouch = (function () {
  131. /**
  132. * Initializes the single instance of the @see:DragDropTouch class.
  133. */
  134. function DragDropTouch() {
  135. this._lastClick = 0;
  136. // enforce singleton pattern
  137. if (DragDropTouch._instance) {
  138. throw 'DragDropTouch instance already created.';
  139. }
  140. // detect passive event support
  141. // https://github.com/Modernizr/Modernizr/issues/1894
  142. var supportsPassive = false;
  143. document.addEventListener('test', function () { }, {
  144. get passive() {
  145. supportsPassive = true;
  146. return true;
  147. }
  148. });
  149. // listen to touch events
  150. if ('ontouchstart' in document) {
  151. var d = document, ts = this._touchstart.bind(this), tm = this._touchmove.bind(this), te = this._touchend.bind(this), opt = supportsPassive ? { passive: false, capture: false } : false;
  152. d.addEventListener('touchstart', ts, opt);
  153. d.addEventListener('touchmove', tm, opt);
  154. d.addEventListener('touchend', te);
  155. d.addEventListener('touchcancel', te);
  156. }
  157. }
  158. /**
  159. * Gets a reference to the @see:DragDropTouch singleton.
  160. */
  161. DragDropTouch.getInstance = function () {
  162. return DragDropTouch._instance;
  163. };
  164. // ** event handlers
  165. DragDropTouch.prototype._touchstart = function (e) {
  166. var _this = this;
  167. if (this._shouldHandle(e)) {
  168. // raise double-click and prevent zooming
  169. if (Date.now() - this._lastClick < DragDropTouch._DBLCLICK) {
  170. if (this._dispatchEvent(e, 'dblclick', e.target)) {
  171. e.preventDefault();
  172. this._reset();
  173. return;
  174. }
  175. }
  176. // clear all variables
  177. this._reset();
  178. // get nearest draggable element
  179. var src = this._closestDraggable(e.target);
  180. if (src) {
  181. // give caller a chance to handle the hover/move events
  182. if (!this._dispatchEvent(e, 'mousemove', e.target) &&
  183. !this._dispatchEvent(e, 'mousedown', e.target)) {
  184. // get ready to start dragging
  185. this._dragSource = src;
  186. this._ptDown = this._getPoint(e);
  187. this._lastTouch = e;
  188. e.preventDefault();
  189. // show context menu if the user hasn't started dragging after a while
  190. setTimeout(function () {
  191. if (_this._dragSource == src && _this._img == null) {
  192. if (_this._dispatchEvent(e, 'contextmenu', src)) {
  193. _this._reset();
  194. }
  195. }
  196. }, DragDropTouch._CTXMENU);
  197. if (DragDropTouch._ISPRESSHOLDMODE) {
  198. this._pressHoldInterval = setTimeout(function () {
  199. _this._isDragEnabled = true;
  200. _this._touchmove(e);
  201. }, DragDropTouch._PRESSHOLDAWAIT);
  202. }
  203. }
  204. }
  205. }
  206. };
  207. DragDropTouch.prototype._touchmove = function (e) {
  208. if (this._shouldCancelPressHoldMove(e)) {
  209. this._reset();
  210. return;
  211. }
  212. if (this._shouldHandleMove(e) || this._shouldHandlePressHoldMove(e)) {
  213. // see if target wants to handle move
  214. var target = this._getTarget(e);
  215. if (this._dispatchEvent(e, 'mousemove', target)) {
  216. this._lastTouch = e;
  217. e.preventDefault();
  218. return;
  219. }
  220. // start dragging
  221. if (this._dragSource && !this._img && this._shouldStartDragging(e)) {
  222. this._dispatchEvent(e, 'dragstart', this._dragSource);
  223. this._createImage(e);
  224. this._dispatchEvent(e, 'dragenter', target);
  225. }
  226. // continue dragging
  227. if (this._img) {
  228. this._lastTouch = e;
  229. e.preventDefault(); // prevent scrolling
  230. if (target != this._lastTarget) {
  231. this._dispatchEvent(this._lastTouch, 'dragleave', this._lastTarget);
  232. this._dispatchEvent(e, 'dragenter', target);
  233. this._lastTarget = target;
  234. }
  235. this._moveImage(e);
  236. this._isDropZone = this._dispatchEvent(e, 'dragover', target);
  237. }
  238. }
  239. };
  240. DragDropTouch.prototype._touchend = function (e) {
  241. if (this._shouldHandle(e)) {
  242. // see if target wants to handle up
  243. if (this._dispatchEvent(this._lastTouch, 'mouseup', e.target)) {
  244. e.preventDefault();
  245. return;
  246. }
  247. // user clicked the element but didn't drag, so clear the source and simulate a click
  248. if (!this._img) {
  249. this._dragSource = null;
  250. this._dispatchEvent(this._lastTouch, 'click', e.target);
  251. this._lastClick = Date.now();
  252. }
  253. // finish dragging
  254. this._destroyImage();
  255. if (this._dragSource) {
  256. if (e.type.indexOf('cancel') < 0 && this._isDropZone) {
  257. this._dispatchEvent(this._lastTouch, 'drop', this._lastTarget);
  258. }
  259. this._dispatchEvent(this._lastTouch, 'dragend', this._dragSource);
  260. this._reset();
  261. }
  262. }
  263. };
  264. // ** utilities
  265. // ignore events that have been handled or that involve more than one touch
  266. DragDropTouch.prototype._shouldHandle = function (e) {
  267. return e &&
  268. !e.defaultPrevented &&
  269. e.touches && e.touches.length < 2;
  270. };
  271. // use regular condition outside of press & hold mode
  272. DragDropTouch.prototype._shouldHandleMove = function (e) {
  273. return !DragDropTouch._ISPRESSHOLDMODE && this._shouldHandle(e);
  274. };
  275. // allow to handle moves that involve many touches for press & hold
  276. DragDropTouch.prototype._shouldHandlePressHoldMove = function (e) {
  277. return DragDropTouch._ISPRESSHOLDMODE &&
  278. this._isDragEnabled && e && e.touches && e.touches.length;
  279. };
  280. // reset data if user drags without pressing & holding
  281. DragDropTouch.prototype._shouldCancelPressHoldMove = function (e) {
  282. return DragDropTouch._ISPRESSHOLDMODE && !this._isDragEnabled &&
  283. this._getDelta(e) > DragDropTouch._PRESSHOLDMARGIN;
  284. };
  285. // start dragging when specified delta is detected
  286. DragDropTouch.prototype._shouldStartDragging = function (e) {
  287. var delta = this._getDelta(e);
  288. return delta > DragDropTouch._THRESHOLD ||
  289. (DragDropTouch._ISPRESSHOLDMODE && delta >= DragDropTouch._PRESSHOLDTHRESHOLD);
  290. }
  291. // clear all members
  292. DragDropTouch.prototype._reset = function () {
  293. this._destroyImage();
  294. this._dragSource = null;
  295. this._lastTouch = null;
  296. this._lastTarget = null;
  297. this._ptDown = null;
  298. this._isDragEnabled = false;
  299. this._isDropZone = false;
  300. this._dataTransfer = new DataTransfer();
  301. clearInterval(this._pressHoldInterval);
  302. };
  303. // get point for a touch event
  304. DragDropTouch.prototype._getPoint = function (e, page) {
  305. if (e && e.touches) {
  306. e = e.touches[0];
  307. }
  308. return { x: page ? e.pageX : e.clientX, y: page ? e.pageY : e.clientY };
  309. };
  310. // get distance between the current touch event and the first one
  311. DragDropTouch.prototype._getDelta = function (e) {
  312. if (DragDropTouch._ISPRESSHOLDMODE && !this._ptDown) { return 0; }
  313. var p = this._getPoint(e);
  314. return Math.abs(p.x - this._ptDown.x) + Math.abs(p.y - this._ptDown.y);
  315. };
  316. // get the element at a given touch event
  317. DragDropTouch.prototype._getTarget = function (e) {
  318. var pt = this._getPoint(e), el = document.elementFromPoint(pt.x, pt.y);
  319. while (el && getComputedStyle(el).pointerEvents == 'none') {
  320. el = el.parentElement;
  321. }
  322. return el;
  323. };
  324. // create drag image from source element
  325. DragDropTouch.prototype._createImage = function (e) {
  326. // just in case...
  327. if (this._img) {
  328. this._destroyImage();
  329. }
  330. // create drag image from custom element or drag source
  331. var src = this._imgCustom || this._dragSource;
  332. this._img = src.cloneNode(true);
  333. this._copyStyle(src, this._img);
  334. this._img.style.top = this._img.style.left = '-9999px';
  335. // if creating from drag source, apply offset and opacity
  336. if (!this._imgCustom) {
  337. var rc = src.getBoundingClientRect(), pt = this._getPoint(e);
  338. this._imgOffset = { x: pt.x - rc.left, y: pt.y - rc.top };
  339. this._img.style.opacity = DragDropTouch._OPACITY.toString();
  340. }
  341. // add image to document
  342. this._moveImage(e);
  343. document.body.appendChild(this._img);
  344. };
  345. // dispose of drag image element
  346. DragDropTouch.prototype._destroyImage = function () {
  347. if (this._img && this._img.parentElement) {
  348. this._img.parentElement.removeChild(this._img);
  349. }
  350. this._img = null;
  351. this._imgCustom = null;
  352. };
  353. // move the drag image element
  354. DragDropTouch.prototype._moveImage = function (e) {
  355. var _this = this;
  356. requestAnimationFrame(function () {
  357. if (_this._img) {
  358. var pt = _this._getPoint(e, true), s = _this._img.style;
  359. s.position = 'absolute';
  360. s.pointerEvents = 'none';
  361. s.zIndex = '999999';
  362. s.left = Math.round(pt.x - _this._imgOffset.x) + 'px';
  363. s.top = Math.round(pt.y - _this._imgOffset.y) + 'px';
  364. }
  365. });
  366. };
  367. // copy properties from an object to another
  368. DragDropTouch.prototype._copyProps = function (dst, src, props) {
  369. for (var i = 0; i < props.length; i++) {
  370. var p = props[i];
  371. dst[p] = src[p];
  372. }
  373. };
  374. DragDropTouch.prototype._copyStyle = function (src, dst) {
  375. // remove potentially troublesome attributes
  376. DragDropTouch._rmvAtts.forEach(function (att) {
  377. dst.removeAttribute(att);
  378. });
  379. // copy canvas content
  380. if (src instanceof HTMLCanvasElement) {
  381. var cSrc = src, cDst = dst;
  382. cDst.width = cSrc.width;
  383. cDst.height = cSrc.height;
  384. cDst.getContext('2d').drawImage(cSrc, 0, 0);
  385. }
  386. // copy style (without transitions)
  387. var cs = getComputedStyle(src);
  388. for (var i = 0; i < cs.length; i++) {
  389. var key = cs[i];
  390. if (key.indexOf('transition') < 0) {
  391. dst.style[key] = cs[key];
  392. }
  393. }
  394. dst.style.pointerEvents = 'none';
  395. // and repeat for all children
  396. for (var i = 0; i < src.children.length; i++) {
  397. this._copyStyle(src.children[i], dst.children[i]);
  398. }
  399. };
  400. DragDropTouch.prototype._dispatchEvent = function (e, type, target) {
  401. if (e && target) {
  402. var evt = document.createEvent('Event'), t = e.touches ? e.touches[0] : e;
  403. evt.initEvent(type, true, true);
  404. evt.button = 0;
  405. evt.which = evt.buttons = 1;
  406. this._copyProps(evt, e, DragDropTouch._kbdProps);
  407. this._copyProps(evt, t, DragDropTouch._ptProps);
  408. evt.dataTransfer = this._dataTransfer;
  409. target.dispatchEvent(evt);
  410. return evt.defaultPrevented;
  411. }
  412. return false;
  413. };
  414. // gets an element's closest draggable ancestor
  415. DragDropTouch.prototype._closestDraggable = function (e) {
  416. for (; e; e = e.parentElement) {
  417. if (e.hasAttribute('draggable') && e.draggable) {
  418. return e;
  419. }
  420. }
  421. return null;
  422. };
  423. return DragDropTouch;
  424. }());
  425. /*private*/ DragDropTouch._instance = new DragDropTouch(); // singleton
  426. // constants
  427. DragDropTouch._THRESHOLD = 5; // pixels to move before drag starts
  428. DragDropTouch._OPACITY = 0.5; // drag image opacity
  429. DragDropTouch._DBLCLICK = 500; // max ms between clicks in a double click
  430. DragDropTouch._CTXMENU = 900; // ms to hold before raising 'contextmenu' event
  431. DragDropTouch._ISPRESSHOLDMODE = false; // decides of press & hold mode presence
  432. DragDropTouch._PRESSHOLDAWAIT = 400; // ms to wait before press & hold is detected
  433. DragDropTouch._PRESSHOLDMARGIN = 25; // pixels that finger might shiver while pressing
  434. DragDropTouch._PRESSHOLDTHRESHOLD = 0; // pixels to move before drag starts
  435. // copy styles/attributes from drag source to drag image element
  436. DragDropTouch._rmvAtts = 'id,class,style,draggable'.split(',');
  437. // synthesize and dispatch an event
  438. // returns true if the event has been handled (e.preventDefault == true)
  439. DragDropTouch._kbdProps = 'altKey,ctrlKey,metaKey,shiftKey'.split(',');
  440. DragDropTouch._ptProps = 'pageX,pageY,clientX,clientY,screenX,screenY'.split(',');
  441. DragDropTouch_1.DragDropTouch = DragDropTouch;
  442. })(DragDropTouch || (DragDropTouch = {}));