|
- /* kanban-board.js - grid rendering and drag-drop between cells */
- (function () {
- 'use strict';
-
- var boardId = KANBAN.boardId;
- var dragState = {
- active: false,
- x: 0,
- y: 0,
- rafId: 0
- };
-
- function post(url, data, cb) {
- var params = new URLSearchParams();
- Object.keys(data).forEach(function (k) { params.append(k, data[k]); });
- fetch(url, {
- method: 'POST',
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
- body: params.toString()
- })
- .then(function (r) { return r.json(); })
- .then(cb)
- .catch(function (e) { console.error(url, e); });
- }
-
- function esc(s) {
- return String(s)
- .replace(/&/g, '&')
- .replace(/</g, '<')
- .replace(/>/g, '>')
- .replace(/"/g, '"');
- }
-
- function applyGridTemplate() {
- var grid = document.getElementById('kanban-grid');
- var colHs = grid.querySelectorAll('.kanban-col-header');
- var cols = '240px';
- colHs.forEach(function () { cols += ' 220px'; });
- grid.style.gridTemplateColumns = cols;
- }
-
- function buildCardEl(card) {
- var div = document.createElement('div');
- div.className = 'kanban-card';
- div.dataset.id = card.id;
- div.dataset.columnId = card.column_id;
- div.dataset.laneId = card.swim_lane_id;
- div.innerHTML =
- '<div class="card-job-number">' + esc(card.job_number || '') + '</div>' +
- '<div class="card-job-name">' + esc(card.job_name || '') + '</div>';
- div.addEventListener('click', function () {
- window.KanbanModal.openEdit(
- card.id,
- card.column_id,
- card.swim_lane_id,
- card.job_number,
- card.job_name
- );
- });
- return div;
- }
-
- function renderCards() {
- KANBAN.cards.forEach(function (card) {
- var cell = document.querySelector(
- '.kanban-cell[data-col-id="' + card.column_id + '"][data-lane-id="' + card.swim_lane_id + '"]'
- );
- if (cell) {
- cell.appendChild(buildCardEl(card));
- }
- });
- }
-
- function handleDragEnd(evt) {
- var cardId = evt.item.dataset.id;
- var newColId = evt.to.dataset.colId;
- var newLaneId = evt.to.dataset.laneId;
- var newPos = evt.newIndex;
-
- var siblings = [];
- evt.to.querySelectorAll('.kanban-card').forEach(function (el) {
- siblings.push(el.dataset.id);
- });
-
- var card = KANBAN.cards.find(function (c) { return String(c.id) === String(cardId); });
- if (card) {
- card.column_id = parseInt(newColId, 10);
- card.swim_lane_id = parseInt(newLaneId, 10);
- card.position = newPos;
- }
-
- evt.item.dataset.columnId = newColId;
- evt.item.dataset.laneId = newLaneId;
-
- post('/cards/' + cardId + '/move', {
- column_id: newColId,
- swim_lane_id: newLaneId,
- position: newPos,
- sibling_ids: siblings.join(',')
- }, function (res) {
- if (!res.ok) console.error('Move failed', res);
- });
- }
-
- function createCellSortable(cell) {
- Sortable.create(cell, {
- group: 'cards',
- animation: 150,
- ghostClass: 'sortable-ghost',
- chosenClass: 'sortable-chosen',
- handle: '.kanban-card',
- delayOnTouchOnly: true,
- delay: 120,
- touchStartThreshold: 3,
- fallbackTolerance: 4,
- scroll: true,
- bubbleScroll: true,
- scrollSensitivity: 140,
- scrollSpeed: 32,
- onStart: function () { startEdgeAutoScroll(); },
- onEnd: function (evt) {
- stopEdgeAutoScroll();
- handleDragEnd(evt);
- }
- });
- }
-
- function updatePointerFromEvent(evt) {
- if (!evt) return;
- if (evt.touches && evt.touches.length > 0) {
- dragState.x = evt.touches[0].clientX;
- dragState.y = evt.touches[0].clientY;
- return;
- }
- if (evt.clientX !== undefined && evt.clientY !== undefined) {
- dragState.x = evt.clientX;
- dragState.y = evt.clientY;
- }
- }
-
- function edgeScrollStep() {
- if (!dragState.active) return;
-
- var wrapper = document.querySelector('.kanban-wrapper');
- if (wrapper) {
- var rect = wrapper.getBoundingClientRect();
- var edge = 110;
- var maxStep = 40;
- var dx = 0;
- var dy = 0;
-
- if (dragState.x > 0 && dragState.x < rect.left + edge) {
- dx = -Math.min(maxStep, Math.ceil((rect.left + edge - dragState.x) / 3));
- } else if (dragState.x > rect.right - edge && dragState.x < rect.right + edge) {
- dx = Math.min(maxStep, Math.ceil((dragState.x - (rect.right - edge)) / 3));
- }
-
- if (dragState.y > 0 && dragState.y < rect.top + edge) {
- dy = -Math.min(maxStep, Math.ceil((rect.top + edge - dragState.y) / 3));
- } else if (dragState.y > rect.bottom - edge && dragState.y < rect.bottom + edge) {
- dy = Math.min(maxStep, Math.ceil((dragState.y - (rect.bottom - edge)) / 3));
- }
-
- if (dx !== 0) wrapper.scrollLeft += dx;
- if (dy !== 0) wrapper.scrollTop += dy;
- }
-
- dragState.rafId = window.requestAnimationFrame(edgeScrollStep);
- }
-
- function startEdgeAutoScroll() {
- if (dragState.active) return;
- dragState.active = true;
- document.addEventListener('pointermove', updatePointerFromEvent, { passive: true });
- document.addEventListener('touchmove', updatePointerFromEvent, { passive: true });
- document.addEventListener('dragover', updatePointerFromEvent, { passive: true });
- dragState.rafId = window.requestAnimationFrame(edgeScrollStep);
- }
-
- function stopEdgeAutoScroll() {
- if (!dragState.active) return;
- dragState.active = false;
- if (dragState.rafId) {
- window.cancelAnimationFrame(dragState.rafId);
- dragState.rafId = 0;
- }
- document.removeEventListener('pointermove', updatePointerFromEvent);
- document.removeEventListener('touchmove', updatePointerFromEvent);
- document.removeEventListener('dragover', updatePointerFromEvent);
- }
-
- function initSortables() {
- document.querySelectorAll('.kanban-cell').forEach(createCellSortable);
- }
-
- document.getElementById('btn-add-card').addEventListener('click', function () {
- window.KanbanModal.openCreate(boardId, null, null);
- });
-
- window.KanbanBoard = {
- onCardCreated: function (card) {
- KANBAN.cards.push(card);
- var cell = document.querySelector(
- '.kanban-cell[data-col-id="' + card.column_id + '"][data-lane-id="' + card.swim_lane_id + '"]'
- );
- if (cell) {
- cell.appendChild(buildCardEl(card));
- }
- },
- onCardUpdated: function (id, jobNumber, jobName) {
- var card = KANBAN.cards.find(function (c) { return String(c.id) === String(id); });
- if (card) {
- card.job_number = jobNumber;
- card.job_name = jobName;
- }
- var el = document.querySelector('.kanban-card[data-id="' + id + '"]');
- if (el) {
- el.querySelector('.card-job-number').textContent = jobNumber;
- el.querySelector('.card-job-name').textContent = jobName;
- }
- },
- onCardDeleted: function (id) {
- KANBAN.cards = KANBAN.cards.filter(function (c) { return String(c.id) !== String(id); });
- var el = document.querySelector('.kanban-card[data-id="' + id + '"]');
- if (el) el.remove();
- },
- addColumn: function (col) {
- var grid = document.getElementById('kanban-grid');
-
- var headers = grid.querySelectorAll('.kanban-col-header');
- var refNode = headers.length ? headers[headers.length - 1].nextSibling : null;
-
- var hdr = document.createElement('div');
- hdr.className = 'kanban-col-header';
- hdr.dataset.colId = col.id;
- hdr.innerHTML = '<span class="col-label">' + esc(col.name) + '</span>';
- grid.insertBefore(hdr, refNode);
-
- var laneHeaders = grid.querySelectorAll('.kanban-lane-header');
- laneHeaders.forEach(function (lh) {
- var laneId = lh.dataset.laneId;
- var cell = document.createElement('div');
- cell.className = 'kanban-cell';
- cell.dataset.colId = col.id;
- cell.dataset.laneId = laneId;
- var row = lh.parentNode;
- row.appendChild(cell);
- createCellSortable(cell);
- });
-
- applyGridTemplate();
- },
- removeColumn: function (colId) {
- document.querySelector('.kanban-col-header[data-col-id="' + colId + '"]').remove();
- document.querySelectorAll('.kanban-cell[data-col-id="' + colId + '"]').forEach(function (el) { el.remove(); });
- KANBAN.cards = KANBAN.cards.filter(function (c) { return String(c.column_id) !== String(colId); });
- applyGridTemplate();
- },
- addLane: function (lane) {
- var grid = document.getElementById('kanban-grid');
- var colHeaders = grid.querySelectorAll('.kanban-col-header');
-
- var lh = document.createElement('div');
- lh.className = 'kanban-lane-header';
- lh.dataset.laneId = lane.id;
- lh.innerHTML = '<span class="lane-label">' + esc(lane.name) + '</span>';
- grid.appendChild(lh);
-
- colHeaders.forEach(function (ch) {
- var cell = document.createElement('div');
- cell.className = 'kanban-cell';
- cell.dataset.colId = ch.dataset.colId;
- cell.dataset.laneId = lane.id;
- grid.appendChild(cell);
- createCellSortable(cell);
- });
-
- applyGridTemplate();
- },
- removeLane: function (laneId) {
- document.querySelector('.kanban-lane-header[data-lane-id="' + laneId + '"]').remove();
- document.querySelectorAll('.kanban-cell[data-lane-id="' + laneId + '"]').forEach(function (el) { el.remove(); });
- KANBAN.cards = KANBAN.cards.filter(function (c) { return String(c.swim_lane_id) !== String(laneId); });
- },
- renameColumn: function (colId, name) {
- var hdr = document.querySelector('.kanban-col-header[data-col-id="' + colId + '"] .col-label');
- if (hdr) hdr.textContent = name;
- },
- renameLane: function (laneId, name) {
- var hdr = document.querySelector('.kanban-lane-header[data-lane-id="' + laneId + '"] .lane-label');
- if (hdr) hdr.textContent = name;
- }
- };
-
- applyGridTemplate();
- renderCards();
- initSortables();
- })();
|