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.

409 lines
19 KiB

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