Licitator 1.0
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

418 lines
20 KiB

5 years ago
  1. (function (global, factory) {
  2. typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  3. typeof define === 'function' && define.amd ? define(factory) :
  4. (global = global || self, global.ItemMovement = factory());
  5. }(this, (function () { 'use strict';
  6. /**
  7. * ItemMovement plugin
  8. *
  9. * @copyright Rafal Pospiech <https://neuronet.io>
  10. * @author Rafal Pospiech <neuronet.io@gmail.com>
  11. * @package gantt-schedule-timeline-calendar
  12. * @license AGPL-3.0 (https://github.com/neuronetio/gantt-schedule-timeline-calendar/blob/master/LICENSE)
  13. * @link https://github.com/neuronetio/gantt-schedule-timeline-calendar
  14. */
  15. const pointerEventsExists = typeof PointerEvent !== 'undefined';
  16. function ItemMovement(options = {}) {
  17. const defaultOptions = {
  18. moveable: true,
  19. resizable: true,
  20. resizerContent: '',
  21. collisionDetection: true,
  22. outOfBorders: false,
  23. snapStart(timeStart, startDiff) {
  24. return timeStart + startDiff;
  25. },
  26. snapEnd(timeEnd, endDiff) {
  27. return timeEnd + endDiff;
  28. },
  29. ghostNode: true,
  30. wait: 0
  31. };
  32. options = Object.assign(Object.assign({}, defaultOptions), options);
  33. const movementState = {};
  34. /**
  35. * Add moving functionality to items as action
  36. *
  37. * @param {HTMLElement} element DOM Node
  38. * @param {Object} data
  39. */
  40. function ItemAction(element, data) {
  41. if (!options.moveable && !options.resizable) {
  42. return;
  43. }
  44. const state = data.state;
  45. const api = data.api;
  46. function isMoveable(data) {
  47. let moveable = options.moveable;
  48. if (data.item.hasOwnProperty('moveable') && moveable) {
  49. moveable = data.item.moveable;
  50. }
  51. if (data.row.hasOwnProperty('moveable') && moveable) {
  52. moveable = data.row.moveable;
  53. }
  54. return moveable;
  55. }
  56. function isResizable(data) {
  57. let resizable = options.resizable && (!data.item.hasOwnProperty('resizable') || data.item.resizable === true);
  58. if (data.row.hasOwnProperty('resizable') && resizable) {
  59. resizable = data.row.resizable;
  60. }
  61. return resizable;
  62. }
  63. function getMovement(data) {
  64. const itemId = data.item.id;
  65. if (typeof movementState[itemId] === 'undefined') {
  66. movementState[itemId] = { moving: false, resizing: false, waiting: false };
  67. }
  68. return movementState[itemId];
  69. }
  70. function saveMovement(itemId, movement) {
  71. state.update(`config.plugin.ItemMovement.item`, Object.assign({ id: itemId }, movement));
  72. state.update('config.plugin.ItemMovement.movement', (current) => {
  73. if (!current) {
  74. current = { moving: false, waiting: false, resizing: false };
  75. }
  76. current.moving = movement.moving;
  77. current.waiting = movement.waiting;
  78. current.resizing = movement.resizing;
  79. return current;
  80. });
  81. }
  82. function createGhost(data, normalized, ganttLeft, ganttTop) {
  83. const movement = getMovement(data);
  84. if (!options.ghostNode || typeof movement.ghost !== 'undefined') {
  85. return;
  86. }
  87. const ghost = element.cloneNode(true);
  88. const style = getComputedStyle(element);
  89. const compensationY = state.get('config.scroll.compensation.y');
  90. ghost.style.position = 'absolute';
  91. ghost.style.left = normalized.clientX - ganttLeft - movement.itemLeftCompensation + 'px';
  92. const itemTop = normalized.clientY - ganttTop - element.offsetTop - compensationY + parseInt(style['margin-top']);
  93. movement.itemTop = itemTop;
  94. ghost.style.top = normalized.clientY - ganttTop - itemTop + 'px';
  95. ghost.style.width = style.width;
  96. ghost.style['box-shadow'] = '10px 10px 6px #00000020';
  97. const height = element.clientHeight + 'px';
  98. ghost.style.height = height;
  99. ghost.style['line-height'] = element.clientHeight - 18 + 'px';
  100. ghost.style.opacity = '0.6';
  101. ghost.style.transform = 'scale(1.05, 1.05)';
  102. state.get('_internal.elements.chart-timeline').appendChild(ghost);
  103. movement.ghost = ghost;
  104. saveMovement(data.item.id, movement);
  105. return ghost;
  106. }
  107. function moveGhost(data, normalized) {
  108. if (options.ghostNode) {
  109. const movement = getMovement(data);
  110. const left = normalized.clientX - movement.ganttLeft - movement.itemLeftCompensation;
  111. movement.ghost.style.left = left + 'px';
  112. movement.ghost.style.top =
  113. normalized.clientY -
  114. movement.ganttTop -
  115. movement.itemTop +
  116. parseInt(getComputedStyle(element)['margin-top']) +
  117. 'px';
  118. saveMovement(data.item.id, movement);
  119. }
  120. }
  121. function destroyGhost(itemId) {
  122. if (!options.ghostNode) {
  123. return;
  124. }
  125. if (typeof movementState[itemId] !== 'undefined' && typeof movementState[itemId].ghost !== 'undefined') {
  126. state.get('_internal.elements.chart-timeline').removeChild(movementState[itemId].ghost);
  127. delete movementState[itemId].ghost;
  128. saveMovement(data.item.id, movementState[itemId]);
  129. }
  130. }
  131. function getSnapStart(data) {
  132. let snapStart = options.snapStart;
  133. if (typeof data.item.snapStart === 'function') {
  134. snapStart = data.item.snapStart;
  135. }
  136. return snapStart;
  137. }
  138. function getSnapEnd(data) {
  139. let snapEnd = options.snapEnd;
  140. if (typeof data.item.snapEnd === 'function') {
  141. snapEnd = data.item.snapEnd;
  142. }
  143. return snapEnd;
  144. }
  145. const resizerHTML = `<div class="${api.getClass('chart-timeline-items-row-item-resizer')}">${options.resizerContent}</div>`;
  146. // @ts-ignore
  147. element.insertAdjacentHTML('beforeend', resizerHTML);
  148. const resizerEl = element.querySelector('.gantt-schedule-timeline-calendar__chart-timeline-items-row-item-resizer');
  149. if (!isResizable(data)) {
  150. resizerEl.style.visibility = 'hidden';
  151. }
  152. else {
  153. resizerEl.style.visibility = 'visible';
  154. }
  155. function labelDown(ev) {
  156. const normalized = api.normalizePointerEvent(ev);
  157. if ((ev.type === 'pointerdown' || ev.type === 'mousedown') && ev.button !== 0) {
  158. return;
  159. }
  160. const movement = getMovement(data);
  161. movement.waiting = true;
  162. saveMovement(data.item.id, movement);
  163. setTimeout(() => {
  164. ev.stopPropagation();
  165. ev.preventDefault();
  166. if (!movement.waiting)
  167. return;
  168. movement.moving = true;
  169. const item = state.get(`config.chart.items.${data.item.id}`);
  170. const chartLeftTime = state.get('_internal.chart.time.leftGlobal');
  171. const timePerPixel = state.get('_internal.chart.time.timePerPixel');
  172. const ganttRect = state.get('_internal.elements.chart-timeline').getBoundingClientRect();
  173. movement.ganttTop = ganttRect.top;
  174. movement.ganttLeft = ganttRect.left;
  175. movement.itemX = Math.round((item.time.start - chartLeftTime) / timePerPixel);
  176. movement.itemLeftCompensation = normalized.clientX - movement.ganttLeft - movement.itemX;
  177. saveMovement(data.item.id, movement);
  178. createGhost(data, normalized, ganttRect.left, ganttRect.top);
  179. }, options.wait);
  180. }
  181. function resizerDown(ev) {
  182. ev.stopPropagation();
  183. ev.preventDefault();
  184. if ((ev.type === 'pointerdown' || ev.type === 'mousedown') && ev.button !== 0) {
  185. return;
  186. }
  187. const normalized = api.normalizePointerEvent(ev);
  188. const movement = getMovement(data);
  189. movement.resizing = true;
  190. const item = state.get(`config.chart.items.${data.item.id}`);
  191. const chartLeftTime = state.get('_internal.chart.time.leftGlobal');
  192. const timePerPixel = state.get('_internal.chart.time.timePerPixel');
  193. const ganttRect = state.get('_internal.elements.chart-timeline').getBoundingClientRect();
  194. movement.ganttTop = ganttRect.top;
  195. movement.ganttLeft = ganttRect.left;
  196. movement.itemX = (item.time.end - chartLeftTime) / timePerPixel;
  197. movement.itemLeftCompensation = normalized.clientX - movement.ganttLeft - movement.itemX;
  198. saveMovement(data.item.id, movement);
  199. }
  200. function isCollision(rowId, itemId, start, end) {
  201. if (!options.collisionDetection) {
  202. return false;
  203. }
  204. const time = state.get('_internal.chart.time');
  205. if (options.outOfBorders && (start < time.from || end > time.to)) {
  206. return true;
  207. }
  208. let diff = api.time.date(end).diff(start, 'milliseconds');
  209. if (Math.sign(diff) === -1) {
  210. diff = -diff;
  211. }
  212. if (diff <= 1) {
  213. return true;
  214. }
  215. const row = state.get('config.list.rows.' + rowId);
  216. for (const rowItem of row._internal.items) {
  217. if (rowItem.id !== itemId) {
  218. if (start >= rowItem.time.start && start <= rowItem.time.end) {
  219. return true;
  220. }
  221. if (end >= rowItem.time.start && end <= rowItem.time.end) {
  222. return true;
  223. }
  224. if (start <= rowItem.time.start && end >= rowItem.time.end) {
  225. return true;
  226. }
  227. }
  228. }
  229. return false;
  230. }
  231. function movementX(normalized, row, item, zoom, timePerPixel) {
  232. const movement = getMovement(data);
  233. const left = normalized.clientX - movement.ganttLeft - movement.itemLeftCompensation;
  234. moveGhost(data, normalized);
  235. const leftMs = state.get('_internal.chart.time.leftGlobal') + left * timePerPixel;
  236. const add = leftMs - item.time.start;
  237. const originalStart = item.time.start;
  238. const finalStartTime = getSnapStart(data)(item.time.start, add, item);
  239. const finalAdd = finalStartTime - originalStart;
  240. const collision = isCollision(row.id, item.id, item.time.start + finalAdd, item.time.end + finalAdd);
  241. if (finalAdd && !collision) {
  242. state.update(`config.chart.items.${data.item.id}.time`, function moveItem(time) {
  243. time.start += finalAdd;
  244. time.end = getSnapEnd(data)(time.end, finalAdd, item) - 1;
  245. return time;
  246. });
  247. }
  248. }
  249. function resizeX(normalized, row, item, zoom, timePerPixel) {
  250. if (!isResizable(data)) {
  251. return;
  252. }
  253. const time = state.get('_internal.chart.time');
  254. const movement = getMovement(data);
  255. const left = normalized.clientX - movement.ganttLeft - movement.itemLeftCompensation;
  256. const leftMs = time.leftGlobal + left * timePerPixel;
  257. const add = leftMs - item.time.end;
  258. if (item.time.end + add < item.time.start) {
  259. return;
  260. }
  261. const originalEnd = item.time.end;
  262. const finalEndTime = getSnapEnd(data)(item.time.end, add, item) - 1;
  263. const finalAdd = finalEndTime - originalEnd;
  264. const collision = isCollision(row.id, item.id, item.time.start, item.time.end + finalAdd);
  265. if (finalAdd && !collision) {
  266. state.update(`config.chart.items.${data.item.id}.time`, time => {
  267. time.start = getSnapStart(data)(time.start, 0, item);
  268. time.end = getSnapEnd(data)(time.end, finalAdd, item) - 1;
  269. return time;
  270. });
  271. }
  272. }
  273. function movementY(normalized, row, item, zoom, timePerPixel) {
  274. moveGhost(data, normalized);
  275. const movement = getMovement(data);
  276. const top = normalized.clientY - movement.ganttTop;
  277. const visibleRows = state.get('_internal.list.visibleRows');
  278. const compensationY = state.get('config.scroll.compensation.y');
  279. let index = 0;
  280. for (const currentRow of visibleRows) {
  281. if (currentRow.top + compensationY > top) {
  282. if (index > 0) {
  283. return index - 1;
  284. }
  285. return 0;
  286. }
  287. index++;
  288. }
  289. return index;
  290. }
  291. function documentMove(ev) {
  292. const movement = getMovement(data);
  293. const normalized = api.normalizePointerEvent(ev);
  294. let item, rowId, row, zoom, timePerPixel;
  295. if (movement.moving || movement.resizing) {
  296. ev.stopPropagation();
  297. ev.preventDefault();
  298. item = state.get(`config.chart.items.${data.item.id}`);
  299. rowId = state.get(`config.chart.items.${data.item.id}.rowId`);
  300. row = state.get(`config.list.rows.${rowId}`);
  301. zoom = state.get('_internal.chart.time.zoom');
  302. timePerPixel = state.get('_internal.chart.time.timePerPixel');
  303. }
  304. const moveable = isMoveable(data);
  305. if (movement.moving) {
  306. if (moveable === true || moveable === 'x' || (Array.isArray(moveable) && moveable.includes(rowId))) {
  307. movementX(normalized, row, item, zoom, timePerPixel);
  308. }
  309. if (!moveable || moveable === 'x') {
  310. return;
  311. }
  312. let visibleRowsIndex = movementY(normalized);
  313. const visibleRows = state.get('_internal.list.visibleRows');
  314. if (typeof visibleRows[visibleRowsIndex] === 'undefined') {
  315. if (visibleRowsIndex > 0) {
  316. visibleRowsIndex = visibleRows.length - 1;
  317. }
  318. else if (visibleRowsIndex < 0) {
  319. visibleRowsIndex = 0;
  320. }
  321. }
  322. const newRow = visibleRows[visibleRowsIndex];
  323. const newRowId = newRow.id;
  324. const collision = isCollision(newRowId, item.id, item.time.start, item.time.end);
  325. if (newRowId !== item.rowId && !collision) {
  326. if (!Array.isArray(moveable) || moveable.includes(newRowId)) {
  327. if (!newRow.hasOwnProperty('moveable') || newRow.moveable) {
  328. state.update(`config.chart.items.${item.id}.rowId`, newRowId);
  329. }
  330. }
  331. }
  332. }
  333. else if (movement.resizing && (typeof item.resizable === 'undefined' || item.resizable === true)) {
  334. resizeX(normalized, row, item, zoom, timePerPixel);
  335. }
  336. }
  337. function documentUp(ev) {
  338. const movement = getMovement(data);
  339. if (movement.moving || movement.resizing || movement.waiting) {
  340. ev.stopPropagation();
  341. ev.preventDefault();
  342. }
  343. else {
  344. return;
  345. }
  346. movement.moving = false;
  347. movement.waiting = false;
  348. movement.resizing = false;
  349. saveMovement(data.item.id, movement);
  350. for (const itemId in movementState) {
  351. movementState[itemId].moving = false;
  352. movementState[itemId].resizing = false;
  353. movementState[itemId].waiting = false;
  354. destroyGhost(itemId);
  355. }
  356. }
  357. if (pointerEventsExists) {
  358. element.addEventListener('pointerdown', labelDown);
  359. resizerEl.addEventListener('pointerdown', resizerDown);
  360. document.addEventListener('pointermove', documentMove);
  361. document.addEventListener('pointerup', documentUp);
  362. }
  363. else {
  364. element.addEventListener('touchstart', labelDown);
  365. resizerEl.addEventListener('touchstart', resizerDown);
  366. document.addEventListener('touchmove', documentMove);
  367. document.addEventListener('touchend', documentUp);
  368. document.addEventListener('touchcancel', documentUp);
  369. element.addEventListener('mousedown', labelDown);
  370. resizerEl.addEventListener('mousedown', resizerDown);
  371. document.addEventListener('mousemove', documentMove);
  372. document.addEventListener('mouseup', documentUp);
  373. }
  374. return {
  375. update(node, changedData) {
  376. if (!isResizable(changedData) && resizerEl.style.visibility === 'visible') {
  377. resizerEl.style.visibility = 'hidden';
  378. }
  379. else if (isResizable(changedData) && resizerEl.style.visibility === 'hidden') {
  380. resizerEl.style.visibility = 'visible';
  381. }
  382. data = changedData;
  383. },
  384. destroy(node, data) {
  385. if (pointerEventsExists) {
  386. element.removeEventListener('pointerdown', labelDown);
  387. resizerEl.removeEventListener('pointerdown', resizerDown);
  388. document.removeEventListener('pointermove', documentMove);
  389. document.removeEventListener('pointerup', documentUp);
  390. }
  391. else {
  392. element.removeEventListener('mousedown', labelDown);
  393. resizerEl.removeEventListener('mousedown', resizerDown);
  394. document.removeEventListener('mousemove', documentMove);
  395. document.removeEventListener('mouseup', documentUp);
  396. element.removeEventListener('touchstart', labelDown);
  397. resizerEl.removeEventListener('touchstart', resizerDown);
  398. document.removeEventListener('touchmove', documentMove);
  399. document.removeEventListener('touchend', documentUp);
  400. document.removeEventListener('touchcancel', documentUp);
  401. }
  402. resizerEl.remove();
  403. }
  404. };
  405. }
  406. return function initialize(vido) {
  407. vido.state.update('config.actions.chart-timeline-items-row-item', actions => {
  408. actions.push(ItemAction);
  409. return actions;
  410. });
  411. };
  412. }
  413. return ItemMovement;
  414. })));
  415. //# sourceMappingURL=ItemMovement.plugin.js.map