Files
pulse-signage/src/web/public/js/playlists/playlist-table-drag.js
T
2026-08-03 12:45:27 +01:00

410 lines
13 KiB
JavaScript

(function () {
// Playlist table drag sorting is isolated here so the playlist editor script
// can stay focused on slide picking, schedule editing, and row updates.
function getDragEventPoint(event) {
var touch = event.touches && event.touches[0] ? event.touches[0] : event.changedTouches && event.changedTouches[0] ? event.changedTouches[0] : null;
return {
x: touch ? touch.clientX : event.clientX,
y: touch ? touch.clientY : event.clientY
};
}
function measureDraggedRowWidths(row) {
var metrics = {
rowRect: row.getBoundingClientRect(),
cellRects: []
};
Array.prototype.forEach.call(row.children, function (cell) {
metrics.cellRects.push(cell.getBoundingClientRect());
});
return metrics;
}
function lockDraggedRowWidths(row, metrics) {
if (!row) {
return;
}
var rowRect = metrics && metrics.rowRect ? metrics.rowRect : row.getBoundingClientRect();
row.style.width = rowRect.width + 'px';
row.style.height = rowRect.height + 'px';
row.style.boxSizing = 'border-box';
Array.prototype.forEach.call(row.children, function (cell, index) {
var cellRect = metrics && metrics.cellRects && metrics.cellRects[index] ? metrics.cellRects[index] : cell.getBoundingClientRect();
cell.style.width = cellRect.width + 'px';
cell.style.height = cellRect.height + 'px';
cell.style.boxSizing = 'border-box';
});
}
function syncDraggedRowCellPadding(row, sourceRow) {
if (!row || !sourceRow) {
return;
}
Array.prototype.forEach.call(row.children, function (cell, index) {
var sourceCell = sourceRow.children[index];
if (!sourceCell) {
return;
}
var sourceStyle = window.getComputedStyle(sourceCell);
cell.style.paddingLeft = sourceStyle.paddingLeft;
cell.style.paddingRight = sourceStyle.paddingRight;
cell.style.paddingTop = sourceStyle.paddingTop;
cell.style.paddingBottom = sourceStyle.paddingBottom;
});
}
function unlockDraggedRowWidths(row) {
if (!row) {
return;
}
row.style.width = '';
row.style.height = '';
row.style.boxSizing = '';
Array.prototype.forEach.call(row.children, function (cell) {
cell.style.width = '';
cell.style.height = '';
cell.style.boxSizing = '';
});
}
function animateRowReorder(previousRects, excludedRows) {
var excluded = Array.isArray(excludedRows) ? excludedRows : [];
if (!previousRects || typeof previousRects.forEach !== 'function') {
return;
}
window.requestAnimationFrame(function () {
previousRects.forEach(function (previousRect, row) {
var nextRect;
var deltaY;
var previousTransition;
if (!row || excluded.indexOf(row) !== -1 || !row.parentNode) {
return;
}
nextRect = row.getBoundingClientRect();
deltaY = previousRect.top - nextRect.top;
if (!deltaY) {
return;
}
previousTransition = row.style.transition;
row.style.transition = 'none';
row.style.transform = 'translate3d(0, ' + deltaY + 'px, 0)';
row.offsetHeight;
row.style.transition = 'transform 180ms ease';
window.requestAnimationFrame(function () {
if (!row.parentNode) {
return;
}
row.style.transform = '';
var cleanup = function () {
row.style.transition = previousTransition;
row.removeEventListener('transitionend', cleanup);
row.removeEventListener('transitioncancel', cleanup);
};
row.addEventListener('transitionend', cleanup, { once: true });
row.addEventListener('transitioncancel', cleanup, { once: true });
});
});
});
}
function initPlaylistTableDrag(options) {
var tbody = options && options.tbody ? options.tbody : null;
var onOrderChanged = options && typeof options.onOrderChanged === 'function' ? options.onOrderChanged : function () {};
var rowDragState = null;
if (!tbody) {
return;
}
if (tbody.dataset && tbody.dataset.playlistTableDragBound === 'true') {
return;
}
if (tbody.dataset) {
tbody.dataset.playlistTableDragBound = 'true';
}
function getRows() {
return Array.prototype.slice.call(tbody.querySelectorAll('tr[data-playlist-slide-row]:not([data-playlist-ghost-row])'));
}
function getDragHandle(target) {
return target && target.closest ? target.closest('[data-playlist-drag-handle]') : null;
}
function prepareGhostRow(row) {
if (!row) {
return;
}
row.setAttribute('data-playlist-ghost-row', 'true');
row.setAttribute('aria-hidden', 'true');
row.classList.add('playlist-row-ghost');
row.style.pointerEvents = 'none';
Array.prototype.forEach.call(row.querySelectorAll('[id]'), function (node) {
node.removeAttribute('id');
});
Array.prototype.forEach.call(row.querySelectorAll('input, select, textarea, button'), function (control) {
control.disabled = true;
control.setAttribute('tabindex', '-1');
if (control.tagName === 'BUTTON') {
control.setAttribute('type', 'button');
}
});
}
function createDragLayer(row, width) {
var table = row.ownerDocument.createElement('table');
var tbodyElement = row.ownerDocument.createElement('tbody');
table.className = 'playlist-row-drag-layer';
table.style.position = 'fixed';
table.style.top = '0';
table.style.left = '0';
table.style.margin = '0';
table.style.borderCollapse = 'collapse';
table.style.borderSpacing = '0';
table.style.tableLayout = 'fixed';
table.style.pointerEvents = 'none';
table.style.zIndex = '1080';
table.style.width = width + 'px';
tbodyElement.appendChild(row);
table.appendChild(tbodyElement);
row.ownerDocument.body.appendChild(table);
return table;
}
function positionDragLayer(layer, metrics, point) {
if (!layer || !metrics || !point) {
return;
}
layer.style.transform = 'translate3d(' + (point.x - metrics.offsetX) + 'px, ' + (point.y - metrics.offsetY) + 'px, 0)';
}
function beginRowDrag() {
if (!rowDragState || rowDragState.active) {
return;
}
var row = rowDragState.row;
var metrics = rowDragState.metrics;
var placeholder = row.cloneNode(true);
var rowRect = metrics.rowRect;
prepareGhostRow(placeholder);
lockDraggedRowWidths(placeholder, metrics);
syncDraggedRowCellPadding(placeholder, row);
placeholder.classList.add('playlist-row-ghost');
tbody.insertBefore(placeholder, row);
rowDragState.placeholder = placeholder;
rowDragState.dragLayer = createDragLayer(row, metrics.rowRect.width);
rowDragState.active = true;
rowDragState.metrics.offsetX = rowDragState.startX - rowRect.left;
rowDragState.metrics.offsetY = rowDragState.startY - rowRect.top;
lockDraggedRowWidths(row, metrics);
syncDraggedRowCellPadding(row, placeholder);
row.classList.add('playlist-row-dragging');
row.style.pointerEvents = 'none';
row.setAttribute('aria-grabbed', 'true');
positionDragLayer(rowDragState.dragLayer, metrics, rowDragState.currentPoint || {
x: rowDragState.startX,
y: rowDragState.startY
});
}
function repositionGhostRow(clientY) {
var rows = getRows();
var insertBeforeRow = null;
var previousRects = new Map();
var moved = false;
rows.forEach(function (row) {
if (row !== rowDragState.placeholder) {
previousRects.set(row, row.getBoundingClientRect());
}
});
for (var index = 0; index < rows.length; index += 1) {
var candidateRow = rows[index];
var rect = candidateRow.getBoundingClientRect();
if (clientY < rect.top + rect.height / 2) {
insertBeforeRow = candidateRow;
break;
}
}
if (insertBeforeRow) {
if (rowDragState.placeholder !== insertBeforeRow && rowDragState.placeholder.nextSibling !== insertBeforeRow) {
tbody.insertBefore(rowDragState.placeholder, insertBeforeRow);
moved = true;
}
} else if (rowDragState.placeholder.parentNode !== tbody || rowDragState.placeholder.nextSibling) {
tbody.appendChild(rowDragState.placeholder);
moved = true;
}
if (moved) {
animateRowReorder(previousRects, [rowDragState.row, rowDragState.placeholder]);
}
}
function teardownRowDrag() {
if (!rowDragState) {
return;
}
if (rowDragState.supportsPointerEvents) {
document.removeEventListener('pointermove', handleRowDragMove);
document.removeEventListener('pointerup', handleRowDragEnd);
document.removeEventListener('pointercancel', handleRowDragEnd);
} else {
document.removeEventListener('mousemove', handleRowDragMove);
document.removeEventListener('mouseup', handleRowDragEnd);
document.removeEventListener('touchmove', handleRowDragMove);
document.removeEventListener('touchend', handleRowDragEnd);
document.removeEventListener('touchcancel', handleRowDragEnd);
}
if (rowDragState.active) {
if (rowDragState.placeholder && rowDragState.placeholder.parentNode) {
rowDragState.placeholder.parentNode.insertBefore(rowDragState.row, rowDragState.placeholder);
rowDragState.placeholder.parentNode.removeChild(rowDragState.placeholder);
} else if (rowDragState.row.parentNode !== tbody) {
tbody.appendChild(rowDragState.row);
}
if (rowDragState.dragLayer && rowDragState.dragLayer.parentNode) {
rowDragState.dragLayer.parentNode.removeChild(rowDragState.dragLayer);
}
rowDragState.row.classList.remove('playlist-row-dragging');
rowDragState.row.style.pointerEvents = rowDragState.previousPointerEvents;
rowDragState.row.removeAttribute('aria-grabbed');
unlockDraggedRowWidths(rowDragState.row);
onOrderChanged();
}
rowDragState = null;
}
function handleRowDragMove(event) {
if (!rowDragState) {
return;
}
var point = getDragEventPoint(event);
if (!point) {
return;
}
rowDragState.currentPoint = point;
if (!rowDragState.active) {
var threshold = typeof rowDragState.threshold === 'number' ? rowDragState.threshold : 3;
if (Math.abs(point.x - rowDragState.startX) < threshold && Math.abs(point.y - rowDragState.startY) < threshold) {
return;
}
beginRowDrag();
}
event.preventDefault();
positionDragLayer(rowDragState.dragLayer, rowDragState.metrics, point);
repositionGhostRow(point.y);
}
function handleRowDragEnd() {
teardownRowDrag();
}
function handleRowDragStart(event) {
var handle = getDragHandle(event.target);
var row = handle ? handle.closest('tr[data-playlist-slide-row]') : null;
var point = getDragEventPoint(event);
if (!row || rowDragState) {
return;
}
if (event.type === 'mousedown' && event.button !== 0) {
return;
}
if (!point) {
return;
}
event.preventDefault();
event.stopPropagation();
rowDragState = {
row: row,
metrics: measureDraggedRowWidths(row),
startX: point.x,
startY: point.y,
currentPoint: point,
threshold: 3,
active: false,
previousPointerEvents: row.style.pointerEvents || '',
placeholder: null,
dragLayer: null,
supportsPointerEvents: typeof window !== 'undefined' && typeof window.PointerEvent === 'function'
};
if (rowDragState.supportsPointerEvents) {
document.addEventListener('pointermove', handleRowDragMove, { passive: false });
document.addEventListener('pointerup', handleRowDragEnd);
document.addEventListener('pointercancel', handleRowDragEnd);
} else {
document.addEventListener('mousemove', handleRowDragMove);
document.addEventListener('mouseup', handleRowDragEnd);
document.addEventListener('touchmove', handleRowDragMove, { passive: false });
document.addEventListener('touchend', handleRowDragEnd);
document.addEventListener('touchcancel', handleRowDragEnd);
}
}
// The drag handle is intentionally isolated from the rest of the playlist
// editor so it can be swapped or removed without touching schedule logic.
if (typeof window !== 'undefined' && window.PointerEvent) {
tbody.addEventListener('pointerdown', handleRowDragStart);
} else {
tbody.addEventListener('mousedown', handleRowDragStart);
tbody.addEventListener('touchstart', handleRowDragStart, { passive: false });
}
}
window.initPlaylistTableDrag = initPlaylistTableDrag;
}());