No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.

299 líneas
9.8KB

  1. /* kanban-board.js - grid rendering and drag-drop between cells */
  2. (function () {
  3. 'use strict';
  4. var boardId = KANBAN.boardId;
  5. var dragState = {
  6. active: false,
  7. x: 0,
  8. y: 0,
  9. rafId: 0
  10. };
  11. function post(url, data, cb) {
  12. var params = new URLSearchParams();
  13. Object.keys(data).forEach(function (k) { params.append(k, data[k]); });
  14. fetch(url, {
  15. method: 'POST',
  16. headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  17. body: params.toString()
  18. })
  19. .then(function (r) { return r.json(); })
  20. .then(cb)
  21. .catch(function (e) { console.error(url, e); });
  22. }
  23. function esc(s) {
  24. return String(s)
  25. .replace(/&/g, '&')
  26. .replace(/</g, '&lt;')
  27. .replace(/>/g, '&gt;')
  28. .replace(/"/g, '&quot;');
  29. }
  30. function applyGridTemplate() {
  31. var grid = document.getElementById('kanban-grid');
  32. var colHs = grid.querySelectorAll('.kanban-col-header');
  33. var cols = '240px';
  34. colHs.forEach(function () { cols += ' 220px'; });
  35. grid.style.gridTemplateColumns = cols;
  36. }
  37. function buildCardEl(card) {
  38. var div = document.createElement('div');
  39. div.className = 'kanban-card';
  40. div.dataset.id = card.id;
  41. div.dataset.columnId = card.column_id;
  42. div.dataset.laneId = card.swim_lane_id;
  43. div.innerHTML =
  44. '<div class="card-job-number">' + esc(card.job_number || '') + '</div>' +
  45. '<div class="card-job-name">' + esc(card.job_name || '') + '</div>';
  46. div.addEventListener('click', function () {
  47. window.KanbanModal.openEdit(
  48. card.id,
  49. card.column_id,
  50. card.swim_lane_id,
  51. card.job_number,
  52. card.job_name
  53. );
  54. });
  55. return div;
  56. }
  57. function renderCards() {
  58. KANBAN.cards.forEach(function (card) {
  59. var cell = document.querySelector(
  60. '.kanban-cell[data-col-id="' + card.column_id + '"][data-lane-id="' + card.swim_lane_id + '"]'
  61. );
  62. if (cell) {
  63. cell.appendChild(buildCardEl(card));
  64. }
  65. });
  66. }
  67. function handleDragEnd(evt) {
  68. var cardId = evt.item.dataset.id;
  69. var newColId = evt.to.dataset.colId;
  70. var newLaneId = evt.to.dataset.laneId;
  71. var newPos = evt.newIndex;
  72. var siblings = [];
  73. evt.to.querySelectorAll('.kanban-card').forEach(function (el) {
  74. siblings.push(el.dataset.id);
  75. });
  76. var card = KANBAN.cards.find(function (c) { return String(c.id) === String(cardId); });
  77. if (card) {
  78. card.column_id = parseInt(newColId, 10);
  79. card.swim_lane_id = parseInt(newLaneId, 10);
  80. card.position = newPos;
  81. }
  82. evt.item.dataset.columnId = newColId;
  83. evt.item.dataset.laneId = newLaneId;
  84. post('/cards/' + cardId + '/move', {
  85. column_id: newColId,
  86. swim_lane_id: newLaneId,
  87. position: newPos,
  88. sibling_ids: siblings.join(',')
  89. }, function (res) {
  90. if (!res.ok) console.error('Move failed', res);
  91. });
  92. }
  93. function createCellSortable(cell) {
  94. Sortable.create(cell, {
  95. group: 'cards',
  96. animation: 150,
  97. ghostClass: 'sortable-ghost',
  98. chosenClass: 'sortable-chosen',
  99. handle: '.kanban-card',
  100. delayOnTouchOnly: true,
  101. delay: 120,
  102. touchStartThreshold: 3,
  103. fallbackTolerance: 4,
  104. scroll: true,
  105. bubbleScroll: true,
  106. scrollSensitivity: 140,
  107. scrollSpeed: 32,
  108. onStart: function () { startEdgeAutoScroll(); },
  109. onEnd: function (evt) {
  110. stopEdgeAutoScroll();
  111. handleDragEnd(evt);
  112. }
  113. });
  114. }
  115. function updatePointerFromEvent(evt) {
  116. if (!evt) return;
  117. if (evt.touches && evt.touches.length > 0) {
  118. dragState.x = evt.touches[0].clientX;
  119. dragState.y = evt.touches[0].clientY;
  120. return;
  121. }
  122. if (evt.clientX !== undefined && evt.clientY !== undefined) {
  123. dragState.x = evt.clientX;
  124. dragState.y = evt.clientY;
  125. }
  126. }
  127. function edgeScrollStep() {
  128. if (!dragState.active) return;
  129. var wrapper = document.querySelector('.kanban-wrapper');
  130. if (wrapper) {
  131. var rect = wrapper.getBoundingClientRect();
  132. var edge = 110;
  133. var maxStep = 40;
  134. var dx = 0;
  135. var dy = 0;
  136. if (dragState.x > 0 && dragState.x < rect.left + edge) {
  137. dx = -Math.min(maxStep, Math.ceil((rect.left + edge - dragState.x) / 3));
  138. } else if (dragState.x > rect.right - edge && dragState.x < rect.right + edge) {
  139. dx = Math.min(maxStep, Math.ceil((dragState.x - (rect.right - edge)) / 3));
  140. }
  141. if (dragState.y > 0 && dragState.y < rect.top + edge) {
  142. dy = -Math.min(maxStep, Math.ceil((rect.top + edge - dragState.y) / 3));
  143. } else if (dragState.y > rect.bottom - edge && dragState.y < rect.bottom + edge) {
  144. dy = Math.min(maxStep, Math.ceil((dragState.y - (rect.bottom - edge)) / 3));
  145. }
  146. if (dx !== 0) wrapper.scrollLeft += dx;
  147. if (dy !== 0) wrapper.scrollTop += dy;
  148. }
  149. dragState.rafId = window.requestAnimationFrame(edgeScrollStep);
  150. }
  151. function startEdgeAutoScroll() {
  152. if (dragState.active) return;
  153. dragState.active = true;
  154. document.addEventListener('pointermove', updatePointerFromEvent, { passive: true });
  155. document.addEventListener('touchmove', updatePointerFromEvent, { passive: true });
  156. document.addEventListener('dragover', updatePointerFromEvent, { passive: true });
  157. dragState.rafId = window.requestAnimationFrame(edgeScrollStep);
  158. }
  159. function stopEdgeAutoScroll() {
  160. if (!dragState.active) return;
  161. dragState.active = false;
  162. if (dragState.rafId) {
  163. window.cancelAnimationFrame(dragState.rafId);
  164. dragState.rafId = 0;
  165. }
  166. document.removeEventListener('pointermove', updatePointerFromEvent);
  167. document.removeEventListener('touchmove', updatePointerFromEvent);
  168. document.removeEventListener('dragover', updatePointerFromEvent);
  169. }
  170. function initSortables() {
  171. document.querySelectorAll('.kanban-cell').forEach(createCellSortable);
  172. }
  173. document.getElementById('btn-add-card').addEventListener('click', function () {
  174. window.KanbanModal.openCreate(boardId, null, null);
  175. });
  176. window.KanbanBoard = {
  177. onCardCreated: function (card) {
  178. KANBAN.cards.push(card);
  179. var cell = document.querySelector(
  180. '.kanban-cell[data-col-id="' + card.column_id + '"][data-lane-id="' + card.swim_lane_id + '"]'
  181. );
  182. if (cell) {
  183. cell.appendChild(buildCardEl(card));
  184. }
  185. },
  186. onCardUpdated: function (id, jobNumber, jobName) {
  187. var card = KANBAN.cards.find(function (c) { return String(c.id) === String(id); });
  188. if (card) {
  189. card.job_number = jobNumber;
  190. card.job_name = jobName;
  191. }
  192. var el = document.querySelector('.kanban-card[data-id="' + id + '"]');
  193. if (el) {
  194. el.querySelector('.card-job-number').textContent = jobNumber;
  195. el.querySelector('.card-job-name').textContent = jobName;
  196. }
  197. },
  198. onCardDeleted: function (id) {
  199. KANBAN.cards = KANBAN.cards.filter(function (c) { return String(c.id) !== String(id); });
  200. var el = document.querySelector('.kanban-card[data-id="' + id + '"]');
  201. if (el) el.remove();
  202. },
  203. addColumn: function (col) {
  204. var grid = document.getElementById('kanban-grid');
  205. var headers = grid.querySelectorAll('.kanban-col-header');
  206. var refNode = headers.length ? headers[headers.length - 1].nextSibling : null;
  207. var hdr = document.createElement('div');
  208. hdr.className = 'kanban-col-header';
  209. hdr.dataset.colId = col.id;
  210. hdr.innerHTML = '<span class="col-label">' + esc(col.name) + '</span>';
  211. grid.insertBefore(hdr, refNode);
  212. var laneHeaders = grid.querySelectorAll('.kanban-lane-header');
  213. laneHeaders.forEach(function (lh) {
  214. var laneId = lh.dataset.laneId;
  215. var cell = document.createElement('div');
  216. cell.className = 'kanban-cell';
  217. cell.dataset.colId = col.id;
  218. cell.dataset.laneId = laneId;
  219. var row = lh.parentNode;
  220. row.appendChild(cell);
  221. createCellSortable(cell);
  222. });
  223. applyGridTemplate();
  224. },
  225. removeColumn: function (colId) {
  226. document.querySelector('.kanban-col-header[data-col-id="' + colId + '"]').remove();
  227. document.querySelectorAll('.kanban-cell[data-col-id="' + colId + '"]').forEach(function (el) { el.remove(); });
  228. KANBAN.cards = KANBAN.cards.filter(function (c) { return String(c.column_id) !== String(colId); });
  229. applyGridTemplate();
  230. },
  231. addLane: function (lane) {
  232. var grid = document.getElementById('kanban-grid');
  233. var colHeaders = grid.querySelectorAll('.kanban-col-header');
  234. var lh = document.createElement('div');
  235. lh.className = 'kanban-lane-header';
  236. lh.dataset.laneId = lane.id;
  237. lh.innerHTML = '<span class="lane-label">' + esc(lane.name) + '</span>';
  238. grid.appendChild(lh);
  239. colHeaders.forEach(function (ch) {
  240. var cell = document.createElement('div');
  241. cell.className = 'kanban-cell';
  242. cell.dataset.colId = ch.dataset.colId;
  243. cell.dataset.laneId = lane.id;
  244. grid.appendChild(cell);
  245. createCellSortable(cell);
  246. });
  247. applyGridTemplate();
  248. },
  249. removeLane: function (laneId) {
  250. document.querySelector('.kanban-lane-header[data-lane-id="' + laneId + '"]').remove();
  251. document.querySelectorAll('.kanban-cell[data-lane-id="' + laneId + '"]').forEach(function (el) { el.remove(); });
  252. KANBAN.cards = KANBAN.cards.filter(function (c) { return String(c.swim_lane_id) !== String(laneId); });
  253. },
  254. renameColumn: function (colId, name) {
  255. var hdr = document.querySelector('.kanban-col-header[data-col-id="' + colId + '"] .col-label');
  256. if (hdr) hdr.textContent = name;
  257. },
  258. renameLane: function (laneId, name) {
  259. var hdr = document.querySelector('.kanban-lane-header[data-lane-id="' + laneId + '"] .lane-label');
  260. if (hdr) hdr.textContent = name;
  261. }
  262. };
  263. applyGridTemplate();
  264. renderCards();
  265. initSortables();
  266. })();

Powered by TurnKey Linux.