410 lines
13 KiB
JavaScript
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;
|
|
}()); |