(function () { var DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; function formatDays(daysValue) { var days = []; if (Array.isArray(daysValue)) { days = daysValue; } else if (daysValue) { try { var parsedDays = JSON.parse(daysValue); days = Array.isArray(parsedDays) ? parsedDays : []; } catch (_error) { days = []; } } return days .map(function (day) { return DAY_NAMES[Number(day)] || ''; }) .filter(Boolean) .join(', '); } function initPlaylistScheduleModal() { var dialog = document.getElementById('slide-schedule-dialog'); var content = document.getElementById('slide-schedule-content'); var triggers = document.querySelectorAll('[data-schedule-config]'); if (!dialog || !content || !triggers.length) { return; } function initInjectedContent() { if (typeof window.initPlaylistScheduleForm === 'function') { window.initPlaylistScheduleForm(content); } } function collectScheduleParams(row, rowKey) { var params = new URLSearchParams(); var scheduleFields = [ 'schedule_mode[]', 'schedule_start_datetime[]', 'schedule_end_datetime[]', 'schedule_start_time[]', 'schedule_end_time[]', 'schedule_days_json[]' ]; params.set('row_key', String(rowKey || '')); scheduleFields.forEach(function (fieldName) { var field = row && row.querySelector ? row.querySelector('[name="' + fieldName + '"]') : null; var value = field && typeof field.value !== 'undefined' ? String(field.value || '') : ''; if (value) { params.set(fieldName.replace(/\[\]$/, ''), value); } }); return params; } function openScheduleModal(url) { content.innerHTML = '
Loading schedule...
'; if (typeof dialog.showModal === 'function') { dialog.showModal(); } else { dialog.setAttribute('open', 'open'); } fetch(url, { credentials: 'same-origin' }).then(function (response) { if (!response.ok) { return response.text().then(function (text) { throw new Error(text || 'Unable to open schedule editor.'); }); } return response.text(); }).then(function (html) { content.innerHTML = html; initInjectedContent(); }).catch(function (error) { content.innerHTML = '
' + String(error && error.message ? error.message : 'Unable to open schedule editor.') + '
'; }); } function closeScheduleModal() { content.innerHTML = ''; if (typeof dialog.close === 'function') { dialog.close(); } else { dialog.removeAttribute('open'); } } window.openScheduleModal = openScheduleModal; window.closeScheduleModal = closeScheduleModal; Array.prototype.forEach.call(triggers, function (trigger) { trigger.addEventListener('click', function () { var url = trigger.getAttribute('data-schedule-config'); var rowKey = trigger.getAttribute('data-schedule-config-row') || ''; var row = trigger.closest('tr[data-playlist-slide-row]'); if (row) { var params = collectScheduleParams(row, rowKey || row.getAttribute('data-row-key') || ''); url += (url.indexOf('?') === -1 ? '?' : '&') + params.toString(); } else if (rowKey && url.indexOf('row_key=') === -1) { url += (url.indexOf('?') === -1 ? '?' : '&') + 'row_key=' + encodeURIComponent(rowKey); } openScheduleModal(url); }); }); } function scheduleSummary(values) { var mode = String(values.schedule_mode || 'always'); if (mode === 'dates') { if (values.schedule_start_datetime && values.schedule_end_datetime) { return 'Dates: ' + values.schedule_start_datetime.replace('T', ' ') + ' to ' + values.schedule_end_datetime.replace('T', ' '); } return 'Dates: not set'; } if (mode === 'times') { var days = formatDays(values.schedule_days_json); if (days && values.schedule_start_time && values.schedule_end_time) { return 'Times: ' + days + ' ' + values.schedule_start_time.slice(0, 5) + '-' + values.schedule_end_time.slice(0, 5); } return 'Times: not set'; } return 'Always visible'; } function initPlaylistScheduleForm(root) { var scope = root || document; var form = scope.querySelector('form[action*="/config"]'); var select = scope.querySelector('#schedule-mode-select'); if (!form || !select) { return; } var DEFAULT_START_TIME = '00:00'; var DEFAULT_END_TIME = '23:59'; var datesPanel = scope.querySelector('#schedule-dates-panel'); var timesPanel = scope.querySelector('#schedule-times-panel'); var startDateInput = form.querySelector('[name="schedule_start_datetime"]'); var endDateInput = form.querySelector('[name="schedule_end_datetime"]'); var startTimeInput = form.querySelector('[name="schedule_start_time"]'); var endTimeInput = form.querySelector('[name="schedule_end_time"]'); var rowKeyInput = form.querySelector('[name="row_key"]'); var dayCheckboxes = form.querySelectorAll('[name="schedule_days"]'); function clearScheduleValidity() { [startDateInput, endDateInput, startTimeInput, endTimeInput].forEach(function (input) { if (input) { input.setCustomValidity(''); } }); } function setScheduleError(input, message) { if (!input) { return; } input.setCustomValidity(message); } function validateScheduleForm() { var mode = select.value; var firstDayCheckbox = dayCheckboxes.length ? dayCheckboxes[0] : null; clearScheduleValidity(); if (mode === 'dates') { if (!startDateInput.value) { setScheduleError(startDateInput, 'Start datetime is required for this schedule mode.'); } if (!endDateInput.value) { setScheduleError(endDateInput, 'End datetime is required for this schedule mode.'); } if (startDateInput.value && endDateInput.value && new Date(endDateInput.value) < new Date(startDateInput.value)) { setScheduleError(endDateInput, 'End datetime must be after start datetime.'); } } if (mode === 'times') { if (!startTimeInput.value) { setScheduleError(startTimeInput, 'Start time is required for this schedule mode.'); } if (!endTimeInput.value) { setScheduleError(endTimeInput, 'End time is required for this schedule mode.'); } if (startTimeInput.value && endTimeInput.value && endTimeInput.value < startTimeInput.value) { setScheduleError(endTimeInput, 'End time must be after start time.'); } if (!Array.prototype.some.call(dayCheckboxes, function (checkbox) { return checkbox.checked; })) { setScheduleError(firstDayCheckbox, 'Select at least one day.'); } } return form.checkValidity(); } [startDateInput, endDateInput, startTimeInput, endTimeInput].forEach(function (input) { if (input) { input.addEventListener('input', clearScheduleValidity); } }); Array.prototype.forEach.call(dayCheckboxes, function (checkbox) { checkbox.addEventListener('change', clearScheduleValidity); }); select.addEventListener('change', clearScheduleValidity); function notifyParentResize() { return; } function updateVisibility() { var mode = select.value; if (datesPanel) { datesPanel.classList.toggle('is-hidden', mode !== 'dates'); } if (timesPanel) { timesPanel.classList.toggle('is-hidden', mode !== 'times'); } if (mode === 'times') { if (!startTimeInput.value) { startTimeInput.value = DEFAULT_START_TIME; } if (!endTimeInput.value) { endTimeInput.value = DEFAULT_END_TIME; } } window.requestAnimationFrame(function () { notifyParentResize(); }); } select.addEventListener('change', updateVisibility); updateVisibility(); form.addEventListener('submit', function (event) { event.preventDefault(); if (!validateScheduleForm()) { form.reportValidity(); return; } var mode = select.value; var selectedDays = []; Array.prototype.forEach.call(dayCheckboxes, function (checkbox) { if (checkbox.checked) { selectedDays.push(Number(checkbox.value)); } }); var values = { row_key: String(rowKeyInput && rowKeyInput.value ? rowKeyInput.value : ''), schedule_mode: mode, schedule_start_datetime: startDateInput.value || '', schedule_end_datetime: endDateInput.value || '', schedule_start_time: startTimeInput.value || '', schedule_end_time: endTimeInput.value || '', schedule_days_json: JSON.stringify(selectedDays.sort()), summary: '' }; values.summary = scheduleSummary(values); if (window.parent && typeof window.parent.applyPlaylistScheduleConfig === 'function') { window.parent.applyPlaylistScheduleConfig(values); if (typeof window.parent.closeScheduleModal === 'function') { window.parent.closeScheduleModal(); } return; } var payload = new URLSearchParams(new FormData(form)); fetch(form.action, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, body: payload.toString(), credentials: 'same-origin' }).then(function (response) { if (!response.ok) { return response.text().then(function (text) { throw new Error(text || 'Unable to save schedule.'); }); } if (window.parent && typeof window.parent.closeScheduleModal === 'function') { window.parent.closeScheduleModal(); } }).catch(function (error) { alert(error.message || 'Unable to save schedule.'); }); }); } function initPlaylistEditStaging() { var tbody = document.getElementById('playlist-items-body'); var form = document.getElementById('playlist-edit-form'); var addSlideModal = document.getElementById('playlist-add-slide-modal'); var addSlideGrid = document.getElementById('playlist-slide-picker-grid'); var addSlideSearch = document.getElementById('playlist-slide-picker-search'); var addSlideEmpty = document.getElementById('playlist-slide-picker-empty'); var addSlideConfirm = document.getElementById('playlist-confirm-add-slides'); var addSlideCount = document.getElementById('playlist-slide-picker-selected-count'); var addSlideData = document.getElementById('playlist-add-slide-data'); var addSlideOpenButton = document.getElementById('playlist-open-slide-modal'); var slidePickerCards = []; var slidePickerSelection = new Set(); var slidePickerSlides = []; if (!tbody || !form) { return; } if (addSlideData) { try { var parsedSlides = JSON.parse(addSlideData.textContent || '[]'); slidePickerSlides = Array.isArray(parsedSlides) ? parsedSlides : []; } catch (_error) { slidePickerSlides = []; } } function getModalInstance() { if (!window.pulseModal) { return null; } return window.pulseModal.getOrCreate(addSlideModal); } function setCardSelected(card, selected) { if (!card) { return; } card.classList.toggle('is-selected', Boolean(selected)); card.setAttribute('aria-pressed', Boolean(selected) ? 'true' : 'false'); } function setCardVisible(card, visible) { if (!card) { return; } card.classList.toggle('is-hidden', !visible); } function clearModalSelection() { slidePickerSelection.clear(); slidePickerCards.forEach(function (card) { setCardSelected(card, false); }); if (addSlideSearch) { addSlideSearch.value = ''; } syncSlidePickerState(); } function attachSlideThumbFallbacks(root) { if (typeof window.attachSlideThumbFallbacks === 'function') { window.attachSlideThumbFallbacks(root || tbody); } } function createPickerThumb(slide) { function createPlaceholder() { var placeholder = document.createElement('div'); var icon = document.createElement('i'); var label = document.createElement('span'); placeholder.className = 'playlist-slide-picker-thumb-placeholder'; icon.className = 'bi bi-image playlist-slide-picker-thumb-placeholder-icon'; icon.setAttribute('aria-hidden', 'true'); label.className = 'playlist-slide-picker-thumb-placeholder-label'; label.textContent = 'No thumbnail'; placeholder.appendChild(icon); placeholder.appendChild(label); return placeholder; } if (slide && slide.thumbnail_path) { var image = document.createElement('img'); image.className = 'playlist-slide-picker-thumb-image'; image.loading = 'lazy'; image.alt = String(slide.title || 'Slide'); image.src = String(slide.thumbnail_path || ''); image.addEventListener('error', function () { if (image.parentNode) { image.parentNode.replaceChild(createPlaceholder(), image); } }); return image; } return createPlaceholder(); } function createPickerCard(slide) { var button = document.createElement('button'); var media = document.createElement('div'); var title = document.createElement('div'); var check = document.createElement('span'); var searchText = String(slide && slide.title ? slide.title : '').toLowerCase(); button.type = 'button'; button.className = 'playlist-slide-picker-card'; button.setAttribute('aria-pressed', 'false'); button.setAttribute('data-slide-id', String(slide.id || '')); button.setAttribute('data-search-text', searchText); if (slide && slide.isAssigned) { button.disabled = true; button.classList.add('is-assigned'); } media.className = 'playlist-slide-picker-media'; media.appendChild(createPickerThumb(slide)); check.className = 'playlist-slide-picker-check bi bi-check2-circle'; check.setAttribute('aria-hidden', 'true'); media.appendChild(check); title.className = 'playlist-slide-picker-title'; title.textContent = String(slide && slide.title ? slide.title : 'Slide'); button.appendChild(media); button.appendChild(title); return button; } function renderSlidePicker() { if (!addSlideGrid) { return; } addSlideGrid.innerHTML = ''; slidePickerCards = slidePickerSlides.map(function (slide) { var card = createPickerCard(slide); addSlideGrid.appendChild(card); return card; }); syncSlidePickerState(); } function syncSlidePickerState() { var query = String(addSlideSearch && addSlideSearch.value ? addSlideSearch.value : '').trim().toLowerCase(); var visibleCount = 0; slidePickerCards.forEach(function (card) { var searchText = String(card.getAttribute('data-search-text') || ''); var matches = !query || searchText.indexOf(query) !== -1; setCardVisible(card, matches); if (matches) { visibleCount += 1; } }); if (addSlideEmpty) { addSlideEmpty.textContent = query ? 'No slides match your search.' : 'No slides are currently available for this playlist.'; addSlideEmpty.classList.toggle('is-hidden', visibleCount !== 0); } if (addSlideCount) { addSlideCount.textContent = String(slidePickerSelection.size); } if (addSlideConfirm) { addSlideConfirm.disabled = slidePickerSelection.size === 0; } } function toggleCardSelection(card) { var slideId = String(card && card.getAttribute('data-slide-id') ? card.getAttribute('data-slide-id') : ''); var selected; if (!slideId || card.disabled) { return; } selected = !slidePickerSelection.has(slideId); if (selected) { slidePickerSelection.add(slideId); } else { slidePickerSelection.delete(slideId); } setCardSelected(card, selected); syncSlidePickerState(); } function addSelectedSlides() { var selectedSlides = slidePickerSlides.filter(function (slide) { return slidePickerSelection.has(String(slide.id || '')) && !slide.isAssigned; }); if (!selectedSlides.length) { return; } if (tbody.querySelector('.playlist-empty-row')) { tbody.querySelector('.playlist-empty-row').remove(); } selectedSlides.forEach(function (slide) { var row = createRow({ row_key: 'new-' + Date.now() + '-' + slide.id, slide_id: String(slide.id), thumbnail_path: String(slide.thumbnail_path || ''), canvas_signature: String(slide.canvasSignature || ''), title: slide.title || 'Slide', duration_seconds: 10, schedule_mode: 'always', schedule_start_datetime: '', schedule_end_datetime: '', schedule_start_time: '', schedule_end_time: '', schedule_days_json: '[]', summary: 'Always visible' }); tbody.appendChild(row); }); attachSlideThumbFallbacks(tbody); updateRowOrder(true); if (!window.pulseModal || !window.pulseModal.hide(addSlideModal)) { addSlideModal.classList.remove('show'); addSlideModal.setAttribute('aria-hidden', 'true'); } } attachSlideThumbFallbacks(tbody); function getRows() { return Array.prototype.slice.call(tbody.querySelectorAll('tr[data-playlist-slide-row]')); } function findRowByKey(rowKey) { var rows = getRows(); for (var i = 0; i < rows.length; i += 1) { if (rows[i].getAttribute('data-row-key') === rowKey) { return rows[i]; } } return null; } function updateEmptyState() { var rows = getRows(); var placeholder = tbody.querySelector('.playlist-empty-row'); if (rows.length) { if (placeholder) { placeholder.remove(); } return; } if (!placeholder) { tbody.innerHTML = 'No slides assigned yet.'; } } function markPlaylistDirty() { if (form && form.dataset) { form.dataset.dirty = 'true'; } } function scheduleSummaryForRow(row) { var modeInput = row.querySelector('[name="schedule_mode[]"]'); var startDatetime = row.querySelector('[name="schedule_start_datetime[]"]'); var endDatetime = row.querySelector('[name="schedule_end_datetime[]"]'); var startTime = row.querySelector('[name="schedule_start_time[]"]'); var endTime = row.querySelector('[name="schedule_end_time[]"]'); var daysJson = row.querySelector('[name="schedule_days_json[]"]'); var mode = String(modeInput ? modeInput.value : 'always'); var days = formatDays(daysJson && daysJson.value ? daysJson.value : '[]'); if (mode === 'dates') { if (startDatetime && endDatetime && startDatetime.value && endDatetime.value) { return 'Dates: ' + startDatetime.value.replace('T', ' ') + ' to ' + endDatetime.value.replace('T', ' '); } return 'Dates: not set'; } if (mode === 'times') { if (days && startTime && endTime && startTime.value && endTime.value) { return 'Times: ' + days + ' ' + startTime.value.slice(0, 5) + '-' + endTime.value.slice(0, 5); } return 'Times: not set'; } return 'Always visible'; } function syncAddSlideOptions() { var activeSlideIds = {}; var activeCanvasSignatures = {}; getRows().forEach(function (row) { var slideId = String(row.getAttribute('data-slide-id') || ''); var canvasSignature = String(row.getAttribute('data-canvas-signature') || ''); if (slideId) { activeSlideIds[slideId] = true; } if (canvasSignature) { activeCanvasSignatures[canvasSignature] = true; } }); var allowedCanvasSignature = ''; var canvasKeys = Object.keys(activeCanvasSignatures); if (canvasKeys.length === 1) { allowedCanvasSignature = canvasKeys[0]; } slidePickerSlides.forEach(function (slide) { var slideId = String(slide.id || ''); var card = slidePickerCards.find(function (item) { return String(item.getAttribute('data-slide-id') || '') === slideId; }); var canvasSignature = String(slide.canvasSignature || ''); var isAssigned = Boolean(activeSlideIds[slideId]); var isCanvasMismatch = Boolean(allowedCanvasSignature && canvasSignature && canvasSignature !== allowedCanvasSignature); if (card) { card.disabled = isAssigned || isCanvasMismatch; card.classList.toggle('is-assigned', isAssigned); card.classList.toggle('is-mismatch', isCanvasMismatch); card.classList.toggle('is-hidden', false); card.setAttribute('aria-disabled', card.disabled ? 'true' : 'false'); } if (isAssigned) { slidePickerSelection.delete(slideId); if (card) { setCardSelected(card, false); } } }); if (addSlideOpenButton) { addSlideOpenButton.disabled = slidePickerCards.filter(function (card) { return !card.disabled; }).length === 0; } syncSlidePickerState(); } function updateRowOrder(markDirty) { var rows = getRows(); rows.forEach(function (row, index) { var orderNumber = row.querySelector('.playlist-order-number'); if (orderNumber) { orderNumber.textContent = String(index + 1); } }); syncAddSlideOptions(); updateEmptyState(); if (markDirty) { markPlaylistDirty(); } } function lockDraggedRowWidths(row) { if (!row) { return; } var 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) { var cellRect = cell.getBoundingClientRect(); cell.style.width = cellRect.width + 'px'; cell.style.height = cellRect.height + 'px'; cell.style.boxSizing = 'border-box'; }); } 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 setRowSchedule(row, values) { var modeInput = row.querySelector('[name="schedule_mode[]"]'); var startDatetime = row.querySelector('[name="schedule_start_datetime[]"]'); var endDatetime = row.querySelector('[name="schedule_end_datetime[]"]'); var startTime = row.querySelector('[name="schedule_start_time[]"]'); var endTime = row.querySelector('[name="schedule_end_time[]"]'); var daysJson = row.querySelector('[name="schedule_days_json[]"]'); var summary = row.querySelector('.playlist-schedule-summary'); if (modeInput) { modeInput.value = values.schedule_mode || 'always'; } if (startDatetime) { startDatetime.value = values.schedule_start_datetime || ''; } if (endDatetime) { endDatetime.value = values.schedule_end_datetime || ''; } if (startTime) { startTime.value = values.schedule_start_time || ''; } if (endTime) { endTime.value = values.schedule_end_time || ''; } if (daysJson) { daysJson.value = values.schedule_days_json || '[]'; } if (summary) { summary.textContent = values.summary || scheduleSummaryForRow(row); } markPlaylistDirty(); } function createRow(values) { var row = document.createElement('tr'); var rowKey = values.row_key || ('new-' + Date.now() + '-' + Math.random().toString(36).slice(2)); var playlistId = tbody.getAttribute('data-playlist-id') || ''; var thumbnailMarkup = values.thumbnail_path ? '' : ''; row.setAttribute('data-playlist-slide-row', ''); row.setAttribute('data-row-key', rowKey); row.setAttribute('data-slide-id', String(values.slide_id)); row.setAttribute('data-canvas-signature', String(values.canvas_signature || '')); row.innerHTML = '' + '' + '
' + '' + '' + '
' + '' + '' + '
' + '' + '
' + values.title + '
' + '
' + '' + '' + '
' + values.summary + '
' + '' + '' + '' + '' + '' + '' + '' + '' + '
' + '' + '' + '
'; return row; } tbody.addEventListener('click', function (event) { var removeButton = event.target.closest('[data-playlist-remove-row]'); var scheduleButton = event.target.closest('[data-schedule-config]'); var row = event.target.closest('tr[data-playlist-slide-row]'); if (!row) { return; } if (removeButton) { event.preventDefault(); row.remove(); updateRowOrder(true); return; } if (scheduleButton) { event.preventDefault(); if (typeof window.openScheduleModal === 'function') { window.openScheduleModal(scheduleButton.getAttribute('data-schedule-config') + '?row_key=' + encodeURIComponent(row.getAttribute('data-row-key') || '')); } } }); if (window.Sortable) { Sortable.create(tbody, { animation: 180, handle: '[data-playlist-drag-handle]', draggable: 'tr[data-playlist-slide-row]', ghostClass: 'sortable-ghost', chosenClass: 'sortable-chosen', dragClass: 'sortable-drag', forceFallback: true, fallbackOnBody: true, fallbackTolerance: 3, swapThreshold: 0.65, invertedSwapThreshold: 0.65, onChoose: function (event) { lockDraggedRowWidths(event && event.item); }, onUnchoose: function (event) { unlockDraggedRowWidths(event && event.item); }, onEnd: function () { unlockDraggedRowWidths(tbody.querySelector('.sortable-drag')); updateRowOrder(true); } }); } if (addSlideModal && addSlideGrid) { renderSlidePicker(); addSlideGrid.addEventListener('click', function (event) { var card = event.target.closest('.playlist-slide-picker-card'); if (!card) { return; } event.preventDefault(); toggleCardSelection(card); }); if (addSlideSearch) { addSlideSearch.addEventListener('input', syncSlidePickerState); } if (addSlideConfirm) { addSlideConfirm.addEventListener('click', function () { addSelectedSlides(); }); } addSlideModal.addEventListener('shown.bs.modal', function () { syncSlidePickerState(); if (addSlideSearch) { addSlideSearch.focus(); } }); addSlideModal.addEventListener('hidden.bs.modal', function () { clearModalSelection(); }); } window.applyPlaylistScheduleConfig = function (values) { var row = findRowByKey(String(values.row_key || '')); if (!row) { return; } setRowSchedule(row, values); }; updateRowOrder(); } window.initPlaylistScheduleForm = initPlaylistScheduleForm; initPlaylistScheduleModal(); initPlaylistScheduleForm(); initPlaylistEditStaging(); }());