(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; }());