dnd.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. /**
  2. * Copyright © Magento, Inc. All rights reserved.
  3. * See COPYING.txt for license details.
  4. */
  5. /**
  6. * @api
  7. */
  8. define([
  9. 'ko',
  10. 'jquery',
  11. 'underscore',
  12. 'uiElement',
  13. 'Magento_Ui/js/lib/view/utils/async'
  14. ], function (ko, $, _, Element) {
  15. 'use strict';
  16. var transformProp;
  17. /**
  18. * Get element context
  19. */
  20. function getContext(elem) {
  21. return ko.contextFor(elem);
  22. }
  23. /**
  24. * Defines supported css 'transform' property.
  25. *
  26. * @returns {String|Undefined}
  27. */
  28. transformProp = (function () {
  29. var style = document.createElement('div').style,
  30. base = 'Transform',
  31. vendors = ['webkit', 'moz', 'ms', 'o'],
  32. vi = vendors.length,
  33. property;
  34. if (typeof style.transform !== 'undefined') {
  35. return 'transform';
  36. }
  37. while (vi--) {
  38. property = vendors[vi] + base;
  39. if (typeof style[property] !== 'undefined') {
  40. return property;
  41. }
  42. }
  43. })();
  44. return Element.extend({
  45. defaults: {
  46. separatorsClass: {
  47. top: '_dragover-top',
  48. bottom: '_dragover-bottom'
  49. },
  50. step: 'auto',
  51. tableClass: 'table.admin__dynamic-rows',
  52. recordsCache: [],
  53. draggableElement: {},
  54. draggableElementClass: '_dragged',
  55. elemPositions: [],
  56. listens: {
  57. '${ $.recordsProvider }:elems': 'setCacheRecords'
  58. },
  59. modules: {
  60. parentComponent: '${ $.recordsProvider }'
  61. }
  62. },
  63. /**
  64. * Initialize component
  65. *
  66. * @returns {Object} Chainable.
  67. */
  68. initialize: function () {
  69. _.bindAll(
  70. this,
  71. 'mousemoveHandler',
  72. 'mouseupHandler'
  73. );
  74. this._super()
  75. .body = $('body');
  76. return this;
  77. },
  78. /**
  79. * Calls 'initObservable' of parent, initializes 'options' and 'initialOptions'
  80. * properties, calls 'setOptions' passing options to it
  81. *
  82. * @returns {Object} Chainable.
  83. */
  84. initObservable: function () {
  85. this._super()
  86. .observe([
  87. 'recordsCache'
  88. ]);
  89. return this;
  90. },
  91. /**
  92. * Init listens to start drag
  93. *
  94. * @param {Object} elem - DOM element
  95. * @param {Object} data - element data
  96. */
  97. initListeners: function (elem, data) {
  98. $(elem).on('mousedown touchstart', this.mousedownHandler.bind(this, data, elem));
  99. },
  100. /**
  101. * Mouse down handler
  102. *
  103. * @param {Object} data - element data
  104. * @param {Object} elem - element
  105. * @param {Object} event - key down event
  106. */
  107. mousedownHandler: function (data, elem, event) {
  108. var recordNode = this.getRecordNode(elem),
  109. originRecord = $(elem).parents('tr').eq(0),
  110. drEl = this.draggableElement,
  111. $table = $(elem).parents('table').eq(0),
  112. $tableWrapper = $table.parent();
  113. this.disableScroll();
  114. $(recordNode).addClass(this.draggableElementClass);
  115. $(originRecord).addClass(this.draggableElementClass);
  116. this.step = this.step === 'auto' ? originRecord.height() / 2 : this.step;
  117. drEl.originRow = originRecord;
  118. drEl.instance = recordNode = this.processingStyles(recordNode, elem);
  119. drEl.instanceCtx = this.getRecord(originRecord[0]);
  120. drEl.eventMousedownY = this.getPageY(event);
  121. drEl.minYpos =
  122. $table.offset().top - originRecord.offset().top + $table.children('thead').outerHeight();
  123. drEl.maxYpos = drEl.minYpos + $table.children('tbody').outerHeight() - originRecord.outerHeight();
  124. $tableWrapper.append(recordNode);
  125. this.body.bind('mousemove touchmove', this.mousemoveHandler);
  126. this.body.bind('mouseup touchend', this.mouseupHandler);
  127. },
  128. /**
  129. * Mouse move handler
  130. *
  131. * @param {Object} event - mouse move event
  132. */
  133. mousemoveHandler: function (event) {
  134. var depEl = this.draggableElement,
  135. pageY = this.getPageY(event),
  136. positionY = pageY - depEl.eventMousedownY,
  137. processingPositionY = positionY + 'px',
  138. processingMaxYpos = depEl.maxYpos + 'px',
  139. processingMinYpos = depEl.minYpos + 'px',
  140. depElement = this.getDepElement(depEl.instance, positionY, depEl.originRow);
  141. if (depElement) {
  142. depEl.depElement ? depEl.depElement.elem.removeClass(depEl.depElement.className) : false;
  143. depEl.depElement = depElement;
  144. depEl.depElement.insert !== 'none' ? depEl.depElement.elem.addClass(depElement.className) : false;
  145. } else if (depEl.depElement && depEl.depElement.insert !== 'none') {
  146. depEl.depElement.elem.removeClass(depEl.depElement.className);
  147. depEl.depElement.insert = 'none';
  148. }
  149. if (positionY > depEl.minYpos && positionY < depEl.maxYpos) {
  150. $(depEl.instance)[0].style[transformProp] = 'translateY(' + processingPositionY + ')';
  151. } else if (positionY < depEl.minYpos) {
  152. $(depEl.instance)[0].style[transformProp] = 'translateY(' + processingMinYpos + ')';
  153. } else if (positionY >= depEl.maxYpos) {
  154. $(depEl.instance)[0].style[transformProp] = 'translateY(' + processingMaxYpos + ')';
  155. }
  156. },
  157. /**
  158. * Mouse up handler
  159. */
  160. mouseupHandler: function (event) {
  161. var depElementCtx,
  162. drEl = this.draggableElement,
  163. pageY = this.getPageY(event),
  164. positionY = pageY - drEl.eventMousedownY;
  165. this.enableScroll();
  166. drEl.depElement = this.getDepElement(drEl.instance, positionY, this.draggableElement.originRow);
  167. drEl.instance.remove();
  168. if (drEl.depElement) {
  169. depElementCtx = this.getRecord(drEl.depElement.elem[0]);
  170. drEl.depElement.elem.removeClass(drEl.depElement.className);
  171. if (drEl.depElement.insert !== 'none') {
  172. this.setPosition(drEl.depElement.elem, depElementCtx, drEl);
  173. }
  174. }
  175. drEl.originRow.removeClass(this.draggableElementClass);
  176. this.body.unbind('mousemove touchmove', this.mousemoveHandler);
  177. this.body.unbind('mouseup touchend', this.mouseupHandler);
  178. this.draggableElement = {};
  179. },
  180. /**
  181. * Set position to element
  182. *
  183. * @param {Object} depElem - dep element
  184. * @param {Object} depElementCtx - dep element context
  185. * @param {Object} dragData - data draggable element
  186. */
  187. setPosition: function (depElem, depElementCtx, dragData) {
  188. var depElemPosition = ~~depElementCtx.position;
  189. if (dragData.depElement.insert === 'after') {
  190. dragData.instanceCtx.position = depElemPosition + 1;
  191. } else if (dragData.depElement.insert === 'before') {
  192. dragData.instanceCtx.position = depElemPosition;
  193. }
  194. },
  195. /**
  196. * Get dependency element
  197. *
  198. * @param {Object} curInstance - current element instance
  199. * @param {Number} position
  200. * @param {Object} row
  201. */
  202. getDepElement: function (curInstance, position, row) {
  203. var tableSelector = this.tableClass + ' tr',
  204. $table = $(row).parents('table').eq(0),
  205. $curInstance = $(curInstance),
  206. recordsCollection = $table.find('table').length ?
  207. $table.find('tbody > tr').filter(function (index, elem) {
  208. return !$(elem).parents(tableSelector).length;
  209. }) :
  210. $table.find('tbody > tr'),
  211. curInstancePositionTop = $curInstance.position().top,
  212. curInstancePositionBottom = curInstancePositionTop + $curInstance.height();
  213. if (position < 0) {
  214. return this._getDepElement(recordsCollection, 'before', curInstancePositionTop);
  215. } else if (position > 0) {
  216. return this._getDepElement(recordsCollection, 'after', curInstancePositionBottom);
  217. }
  218. },
  219. /**
  220. * Get dependency element private
  221. *
  222. * @param {Array} collection - record collection
  223. * @param {String} position - position to add
  224. * @param {Number} dragPosition - position drag element
  225. */
  226. _getDepElement: function (collection, position, dragPosition) {
  227. var rec,
  228. rangeEnd,
  229. rangeStart,
  230. result,
  231. className,
  232. i = 0,
  233. length = collection.length;
  234. for (i; i < length; i++) {
  235. rec = collection.eq(i);
  236. if (position === 'before') {
  237. rangeStart = collection.eq(i).position().top - this.step;
  238. rangeEnd = rangeStart + this.step * 2;
  239. className = this.separatorsClass.top;
  240. } else if (position === 'after') {
  241. rangeEnd = rec.position().top + rec.height() + this.step;
  242. rangeStart = rangeEnd - this.step * 2;
  243. className = this.separatorsClass.bottom;
  244. }
  245. if (dragPosition > rangeStart && dragPosition < rangeEnd) {
  246. result = {
  247. elem: rec,
  248. insert: rec[0] === this.draggableElement.originRow[0] ? 'none' : position,
  249. className: className
  250. };
  251. }
  252. }
  253. return result;
  254. },
  255. /**
  256. * Set default position of draggable element
  257. *
  258. * @param {Object} elem - current element instance
  259. * @param {Object} data - current element data
  260. */
  261. _setDefaultPosition: function (elem, data) {
  262. var originRecord = $(elem).parents('tr').eq(0),
  263. position = originRecord.position();
  264. ++position.top;
  265. $(data).css(position);
  266. },
  267. /**
  268. * Set records to cache
  269. *
  270. * @param {Object} records - record instance
  271. */
  272. setCacheRecords: function (records) {
  273. this.recordsCache(records);
  274. },
  275. /**
  276. * Set styles to draggable element
  277. *
  278. * @param {Object} data - data
  279. * @param {Object} elem - elem instance
  280. * @returns {Object} instance data.
  281. */
  282. processingStyles: function (data, elem) {
  283. var table = $(elem).parents('table').eq(0),
  284. columns = table.find('th'),
  285. recordColumns = $(data).find('td');
  286. this._setDefaultPosition(elem, $(data));
  287. this._setColumnsWidth(columns, recordColumns);
  288. this._setTableWidth(table, $(data));
  289. return data;
  290. },
  291. /**
  292. * Set table width.
  293. *
  294. * @param {Object} originalTable - original record instance
  295. * @param {Object} recordTable - draggable record instance
  296. */
  297. _setTableWidth: function (originalTable, recordTable) {
  298. recordTable.outerWidth(originalTable.outerWidth());
  299. },
  300. /**
  301. * Set columns width.
  302. *
  303. * @param {Object} originColumns - original record instance
  304. * @param {Object} recordColumns - draggable record instance
  305. */
  306. _setColumnsWidth: function (originColumns, recordColumns) {
  307. var i = 0,
  308. length = originColumns.length;
  309. for (i; i < length; i++) {
  310. recordColumns.eq(i).outerWidth(originColumns.eq(i).outerWidth());
  311. }
  312. },
  313. /**
  314. * Get copy original record
  315. *
  316. * @param {Object} record - original record instance
  317. * @returns {Object} draggable record instance
  318. */
  319. getRecordNode: function (record) {
  320. var $record = $(record),
  321. table = $record.parents('table')[0].cloneNode(true),
  322. $table = $(table);
  323. $table.find('tr').remove();
  324. $table.append($record.parents('tr')[0].cloneNode(true));
  325. return table;
  326. },
  327. /**
  328. * Get record context by element
  329. *
  330. * @param {Object} elem - original element
  331. * @returns {Object} draggable record context
  332. */
  333. getRecord: function (elem) {
  334. var ctx = getContext(elem),
  335. index = _.isFunction(ctx.$index) ? ctx.$index() : ctx.$index;
  336. return this.recordsCache()[index];
  337. },
  338. /**
  339. * Get correct page Y
  340. *
  341. * @param {Object} event - current event
  342. * @returns {integer}
  343. */
  344. getPageY: function (event) {
  345. var pageY;
  346. if (event.type.indexOf('touch') >= 0) {
  347. if (event.originalEvent.touches[0]) {
  348. pageY = event.originalEvent.touches[0].pageY;
  349. } else {
  350. pageY = event.originalEvent.changedTouches[0].pageY;
  351. }
  352. } else {
  353. pageY = event.pageY;
  354. }
  355. return pageY;
  356. },
  357. /**
  358. * Disable page scrolling
  359. */
  360. disableScroll: function () {
  361. document.body.addEventListener('touchmove', this.preventDefault, {
  362. passive: false
  363. });
  364. },
  365. /**
  366. * Enable page scrolling
  367. */
  368. enableScroll: function () {
  369. document.body.removeEventListener('touchmove', this.preventDefault, {
  370. passive: false
  371. });
  372. },
  373. /**
  374. * Prevent default function
  375. *
  376. * @param {Object} event - event object
  377. */
  378. preventDefault: function (event) {
  379. event.preventDefault();
  380. }
  381. });
  382. });