(function () { // Shared constants used across the playlist schedule editor. var DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; var PICKER_THUMB_EMPTY_LABEL = 'No thumbnail'; var PICKER_ASSIGNED_BADGE_LABEL = 'Already in playlist'; var PICKER_CARD_CLASS = 'playlist-slide-picker-card'; var PICKER_MEDIA_CLASS = 'playlist-slide-picker-media'; var PICKER_TITLE_CLASS = 'playlist-slide-picker-title'; var PICKER_CHECK_CLASS = 'playlist-slide-picker-check bi bi-check2-circle'; var PICKER_BADGE_CLASS = 'playlist-slide-picker-badge badge text-bg-primary'; var SCHEDULE_ROW_FIELD_NAMES = { rulesContainer: '[data-schedule-rules-container]', summaryNode: '.playlist-schedule-summary' }; // Schedule helpers. 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 normalizeScheduleRuleValue(rule) { var input = rule && typeof rule === 'object' ? rule : {}; var startDatetime = String(input.start_datetime || input.startDateTime || input.startDateTimeValue || '').trim(); var endDatetime = String(input.end_datetime || input.endDateTime || input.endDateTimeValue || '').trim(); var startTime = String(input.start_time || input.startTime || input.startTimeValue || '').trim(); var endTime = String(input.end_time || input.endTime || input.endTimeValue || '').trim(); var days = []; if (Array.isArray(input.days)) { days = input.days; } else if (Array.isArray(input.dayOptions)) { days = input.dayOptions.filter(function (dayOption) { return Boolean(dayOption && dayOption.checked); }).map(function (dayOption) { return dayOption.value; }); } else if (input.daysCsv !== undefined && input.daysCsv !== null && String(input.daysCsv).trim()) { days = String(input.daysCsv).split(','); } else if (input.days !== undefined && input.days !== null && String(input.days).trim()) { try { var parsedDays = JSON.parse(String(input.days)); days = Array.isArray(parsedDays) ? parsedDays : []; } catch (_error) { days = String(input.days).split(','); } } days = Array.from(new Set(days.map(function (day) { return Number(day); }).filter(function (day) { return Number.isInteger(day) && day >= 0 && day <= 6; }))).sort(function (left, right) { return left - right; }); if (startDatetime || endDatetime) { if (!startDatetime || !endDatetime) { return null; } } if (startTime || endTime) { if (!startTime || !endTime) { return null; } } if (!startDatetime && !endDatetime && !startTime && !endTime && !days.length) { return null; } var normalized = {}; if (startDatetime) { normalized.start_datetime = startDatetime; } if (endDatetime) { normalized.end_datetime = endDatetime; } if (startTime) { normalized.start_time = startTime; } if (endTime) { normalized.end_time = endTime; } if (days.length) { normalized.days = days; } return normalized; } function parseScheduleRulesValue(value) { var rawRules = []; if (Array.isArray(value)) { rawRules = value; } else { var raw = String(value || '').trim(); if (!raw) { return []; } try { var parsed = JSON.parse(raw); rawRules = Array.isArray(parsed) ? parsed : []; } catch (_error) { return []; } } return rawRules.map(function (rule) { return normalizeScheduleRuleValue(rule); }).filter(Boolean); } function formatScheduleRuleSummary(rule) { var normalized = normalizeScheduleRuleValue(rule); var left = []; var right = []; if (!normalized) { return 'Always visible'; } if (normalized.start_datetime && normalized.end_datetime) { left.push('Dates ' + normalized.start_datetime.replace('T', ' ').slice(0, 16) + ' to ' + normalized.end_datetime.replace('T', ' ').slice(0, 16)); } else { left.push('Any date'); } var days = formatDays(normalized.days || []); if (days) { right.push(days); } if (normalized.start_time && normalized.end_time) { right.push(normalized.start_time.slice(0, 5) + '-' + normalized.end_time.slice(0, 5)); } if (right.length) { return left[0] + ': ' + right.join(' '); } return left[0]; } function formatScheduleRulesSummary(value) { var rules = parseScheduleRulesValue(value); if (!rules.length) { return 'Always visible'; } return rules.length === 1 ? '1 rule' : String(rules.length) + ' rules'; } function parseScheduleBoundary(value) { if (!value) return null; var date = value instanceof Date ? value : new Date(value); return Number.isNaN(date.getTime()) ? null : date; } function startOfScheduleDay(date) { return new Date(date.getFullYear(), date.getMonth(), date.getDate()); } function getScheduleBoundaryMinute(date) { return date.getHours() * 60 + date.getMinutes() + date.getSeconds() / 60 + date.getMilliseconds() / 60000; } function getScheduleTimeIntervals(rule, day) { var dayStart = startOfScheduleDay(day); var dayEnd = new Date(dayStart.getTime()); dayEnd.setDate(dayEnd.getDate() + 1); dayEnd = new Date(dayEnd.getTime() - 1); if (rule.start_datetime) { var ruleStart = parseScheduleBoundary(rule.start_datetime); if (!ruleStart || dayEnd < ruleStart) return []; } if (rule.end_datetime) { var ruleEnd = parseScheduleBoundary(rule.end_datetime); if (!ruleEnd || dayStart > ruleEnd) return []; } if (Array.isArray(rule.days) && rule.days.length && rule.days.indexOf(day.getDay()) === -1) { return []; } var lowerMinute = 0; var upperMinute = 1440; if (rule.start_datetime) { var startDate = parseScheduleBoundary(rule.start_datetime); if (startDate && startOfScheduleDay(startDate).getTime() === dayStart.getTime()) { lowerMinute = Math.max(lowerMinute, getScheduleBoundaryMinute(startDate)); } } if (rule.end_datetime) { var endDate = parseScheduleBoundary(rule.end_datetime); if (endDate && startOfScheduleDay(endDate).getTime() === dayStart.getTime()) { upperMinute = Math.min(upperMinute, getScheduleBoundaryMinute(endDate)); } } if (lowerMinute > upperMinute) return []; var intervals = []; if (!rule.start_time || !rule.end_time) { intervals.push([0, 1440]); } else { var startTime = normalizeScheduleRuleValue({ start_time: rule.start_time, end_time: rule.end_time }); if (!startTime) return []; var startMinute = Number(String(rule.start_time).slice(0, 2)) * 60 + Number(String(rule.start_time).slice(3, 5)); var endMinute = Number(String(rule.end_time).slice(0, 2)) * 60 + Number(String(rule.end_time).slice(3, 5)); if (startMinute <= endMinute) { intervals.push([startMinute, endMinute]); } else { intervals.push([0, endMinute], [startMinute, 1440]); } } return intervals.map(function (interval) { return [Math.max(interval[0], lowerMinute), Math.min(interval[1], upperMinute)]; }).filter(function (interval) { return interval[0] <= interval[1]; }); } function scheduleRulePairCanOverlap(leftRule, rightRule) { var startDates = [parseScheduleBoundary(leftRule.start_datetime), parseScheduleBoundary(rightRule.start_datetime)].filter(Boolean); var endDates = [parseScheduleBoundary(leftRule.end_datetime), parseScheduleBoundary(rightRule.end_datetime)].filter(Boolean); var startDate = startDates.reduce(function (latest, date) { return !latest || date > latest ? date : latest; }, null); var endDate = endDates.reduce(function (earliest, date) { return !earliest || date < earliest ? date : earliest; }, null); if (startDate && endDate && startDate > endDate) { return false; } var firstDay = startDate ? startOfScheduleDay(startDate) : endDate ? startOfScheduleDay(endDate) : startOfScheduleDay(new Date()); if (!startDate && endDate) { firstDay.setDate(firstDay.getDate() - 7); } var lastDay = endDate ? startOfScheduleDay(endDate) : startOfScheduleDay(firstDay); if (!endDate) { lastDay.setDate(lastDay.getDate() + 7); } var spanDays = Math.floor((Date.UTC(lastDay.getFullYear(), lastDay.getMonth(), lastDay.getDate()) - Date.UTC(firstDay.getFullYear(), firstDay.getMonth(), firstDay.getDate())) / 86400000); var candidateOffsets = []; var lastOffset = Math.max(0, spanDays); if (lastOffset <= 16) { for (var offset = 0; offset <= lastOffset; offset += 1) candidateOffsets.push(offset); } else { for (var startOffset = 0; startOffset <= 7; startOffset += 1) candidateOffsets.push(startOffset); for (var endOffset = lastOffset - 7; endOffset <= lastOffset; endOffset += 1) candidateOffsets.push(endOffset); } var checkedDays = new Set(); for (var index = 0; index < candidateOffsets.length; index += 1) { var day = new Date(firstDay.getTime()); day.setDate(day.getDate() + candidateOffsets[index]); var dayKey = [day.getFullYear(), day.getMonth(), day.getDate()].join('-'); if (checkedDays.has(dayKey)) continue; checkedDays.add(dayKey); var leftIntervals = getScheduleTimeIntervals(leftRule, day); var rightIntervals = getScheduleTimeIntervals(rightRule, day); for (var leftIndex = 0; leftIndex < leftIntervals.length; leftIndex += 1) { for (var rightIndex = 0; rightIndex < rightIntervals.length; rightIndex += 1) { if (Math.max(leftIntervals[leftIndex][0], rightIntervals[rightIndex][0]) <= Math.min(leftIntervals[leftIndex][1], rightIntervals[rightIndex][1])) { return true; } } } } return false; } function scheduleRuleSetsCanOverlap(slideRulesValue, groupRulesValue) { var slideRules = parseScheduleRulesValue(slideRulesValue); var groupRules = parseScheduleRulesValue(groupRulesValue); if (!slideRules.length || !groupRules.length) { return true; } return slideRules.some(function (slideRule) { return groupRules.some(function (groupRule) { return scheduleRulePairCanOverlap(slideRule, groupRule); }); }); } function getScheduleRowFields(row) { return { rulesContainer: row ? row.querySelector(SCHEDULE_ROW_FIELD_NAMES.rulesContainer) : null, summaryNode: row ? row.querySelector(SCHEDULE_ROW_FIELD_NAMES.summaryNode) : null }; } function buildScheduleRuleHiddenInputs(rule, rowKey, position, formId) { var normalizedRule = normalizeScheduleRuleValue(rule) || {}; var daysCsv = Array.isArray(normalizedRule.days) ? normalizedRule.days.join(',') : ''; var formAttr = formId ? ' form="' + formId + '"' : ''; return '' + '' + '' + '' + '' + '' + '' + ''; } function buildScheduleRulesMarkup(values, rowKey, formId) { var rules = Array.isArray(values) ? values : []; return rules.map(function (rule, index) { return buildScheduleRuleHiddenInputs(rule, rowKey, index, formId); }).join(''); } function parseLocalDate(value) { var date = new Date(String(value || '').trim()); if (Number.isNaN(date.getTime())) { return null; } return new Date(date.getFullYear(), date.getMonth(), date.getDate()); } function isDayInDateRange(startDatetime, endDatetime, dayIndex) { var startDate = parseLocalDate(startDatetime); var endDate = parseLocalDate(endDatetime); var currentDate; if (!startDate || !endDate) { return false; } for (currentDate = new Date(startDate.getTime()); currentDate.getTime() <= endDate.getTime(); currentDate.setDate(currentDate.getDate() + 1)) { if (currentDate.getDay() === Number(dayIndex)) { return true; } } return false; } function getTimeParts(value) { var match = String(value || '').trim().match(/^(\d{2}):(\d{2})(?::\d{2})?$/); if (!match) { return null; } return { hours: Number(match[1]), minutes: Number(match[2]) }; } function combineDateAndTime(baseDate, timeValue) { var timeParts = getTimeParts(timeValue); if (!baseDate || !timeParts) { return null; } return new Date( baseDate.getFullYear(), baseDate.getMonth(), baseDate.getDate(), timeParts.hours, timeParts.minutes, 0, 0 ); } function hasScheduleRuleOverlap(startDatetime, endDatetime, startTime, endTime, selectedDays) { var startDateTime = new Date(String(startDatetime || '').trim()); var endDateTime = new Date(String(endDatetime || '').trim()); var startDate = parseLocalDate(startDatetime); var endDate = parseLocalDate(endDatetime); var allowedDays = Array.isArray(selectedDays) && selectedDays.length ? selectedDays : [0, 1, 2, 3, 4, 5, 6]; var currentDate; if (!startDate || !endDate || Number.isNaN(startDateTime.getTime()) || Number.isNaN(endDateTime.getTime())) { return false; } for (currentDate = new Date(startDate.getTime()); currentDate.getTime() <= endDate.getTime(); currentDate.setDate(currentDate.getDate() + 1)) { if (allowedDays.indexOf(currentDate.getDay()) === -1) { continue; } var dayStart = combineDateAndTime(currentDate, startTime); var dayEnd = combineDateAndTime(currentDate, endTime); if (!dayStart || !dayEnd) { continue; } if (dayEnd >= startDateTime && dayStart <= endDateTime) { return true; } } return false; } function getDateRangeDayIndices(startDatetime, endDatetime) { var startDate = parseLocalDate(startDatetime); var endDate = parseLocalDate(endDatetime); var dayIndices = []; var currentDate; if (!startDate || !endDate) { return dayIndices; } for (currentDate = new Date(startDate.getTime()); currentDate.getTime() <= endDate.getTime(); currentDate.setDate(currentDate.getDate() + 1)) { if (dayIndices.indexOf(currentDate.getDay()) === -1) { dayIndices.push(currentDate.getDay()); } } return dayIndices; } function setDayInputError(card, dayInput, message) { var dayInputs = card ? Array.prototype.slice.call(card.querySelectorAll('[data-schedule-rule-day]')) : []; var dayLabels = card ? Array.prototype.slice.call(card.querySelectorAll('.schedule-day-button')) : []; var feedbackNode = card ? card.querySelector('[data-schedule-rule-feedback]') : null; var dayIndex = dayInputs.indexOf(dayInput); if (dayInput) { dayInput.setCustomValidity(message); dayInput.classList.add('is-invalid'); dayInput.setAttribute('aria-invalid', 'true'); } if (dayIndex !== -1 && dayLabels[dayIndex]) { dayLabels[dayIndex].classList.add('is-invalid'); dayLabels[dayIndex].setAttribute('aria-invalid', 'true'); } if (feedbackNode) { var messageNode = document.createElement('div'); messageNode.textContent = message; feedbackNode.appendChild(messageNode); feedbackNode.hidden = false; } } function appendRuleFeedbackMessage(card, message) { var feedbackNode = card ? card.querySelector('[data-schedule-rule-feedback]') : null; if (!feedbackNode) { return; } var messageNode = document.createElement('div'); messageNode.textContent = message; feedbackNode.appendChild(messageNode); feedbackNode.hidden = false; } function validateScheduleRuleCard(card) { var fields = getRuleFields(card); var dayInputs = Array.prototype.slice.call(fields.days || []); var dayLabels = getRuleDayLabels(card); var checkedDayInputs = dayInputs.filter(function (input) { return Boolean(input && input.checked); }); var checkedDayIndices = checkedDayInputs.map(function (input) { return Number(input && input.value); }); var startDatetime = String(fields.startDatetime && fields.startDatetime.value || '').trim(); var endDatetime = String(fields.endDatetime && fields.endDatetime.value || '').trim(); var startTime = String(fields.startTime && fields.startTime.value || '').trim(); var endTime = String(fields.endTime && fields.endTime.value || '').trim(); clearRuleValidity(card); if ((startDatetime || endDatetime) && (!startDatetime || !endDatetime)) { setRuleError(!startDatetime ? fields.startDatetime : fields.endDatetime, 'Start and end datetimes must both be set for this rule.'); return false; } if ((startTime || endTime) && (!startTime || !endTime)) { setRuleError(!startTime ? fields.startTime : fields.endTime, 'Start and end times must both be set for this rule.'); return false; } if (startDatetime && endDatetime && new Date(endDatetime) <= new Date(startDatetime)) { setRuleError(fields.endDatetime, 'End datetime must be after start datetime.'); return false; } if (startTime && endTime && endTime <= startTime) { setRuleError(fields.endTime, 'End time must be after start time.'); if (fields.startTime) { fields.startTime.classList.add('is-invalid'); fields.startTime.setAttribute('aria-invalid', 'true'); } return false; } if (startDatetime && endDatetime) { var dateRangeDayIndices = getDateRangeDayIndices(startDatetime, endDatetime); var dayIndicesToCheck = checkedDayIndices.length ? checkedDayIndices : [0, 1, 2, 3, 4, 5, 6]; var invalidDayInputs = checkedDayInputs.filter(function (input) { return dateRangeDayIndices.indexOf(Number(input && input.value)) === -1; }); invalidDayInputs.forEach(function (input) { var dayIndex = dayInputs.indexOf(input); if (dayIndex !== -1 && dayLabels[dayIndex]) { dayLabels[dayIndex].classList.add('is-invalid'); dayLabels[dayIndex].setAttribute('aria-invalid', 'true'); } }); if (checkedDayInputs.length && invalidDayInputs.length === checkedDayInputs.length) { setDayInputError(card, invalidDayInputs[0] || checkedDayInputs[0] || dayInputs[0], 'Selected days must overlap the date range.'); return false; } if (startTime && endTime && !hasScheduleRuleOverlap(startDatetime, endDatetime, startTime, endTime, dayIndicesToCheck)) { setRuleError(fields.endTime, 'Start and end times must overlap the date range.'); if (fields.startTime) { fields.startTime.classList.add('is-invalid'); fields.startTime.setAttribute('aria-invalid', 'true'); } return false; } } return true; } function getScheduleRuleValidationIssue(card) { var fields = getRuleFields(card); var dayInputs = Array.prototype.slice.call(fields.days || []); var checkedDayInputs = dayInputs.filter(function (input) { return Boolean(input && input.checked); }); var checkedDayIndices = checkedDayInputs.map(function (input) { return Number(input && input.value); }); var startDatetime = String(fields.startDatetime && fields.startDatetime.value || '').trim(); var endDatetime = String(fields.endDatetime && fields.endDatetime.value || '').trim(); var startTime = String(fields.startTime && fields.startTime.value || '').trim(); var endTime = String(fields.endTime && fields.endTime.value || '').trim(); if (startDatetime || endDatetime) { if (!startDatetime || !endDatetime) { return { type: 'datetime-pair', target: !startDatetime ? fields.startDatetime : fields.endDatetime, message: 'Start and end datetimes must both be set for this rule.' }; } } if (startTime || endTime) { if (!startTime || !endTime) { return { type: 'time-pair', target: !startTime ? fields.startTime : fields.endTime, message: 'Start and end times must both be set for this rule.' }; } } if (startDatetime && endDatetime && new Date(endDatetime) <= new Date(startDatetime)) { return { type: 'datetime-order', target: fields.endDatetime, message: 'End datetime must be after start datetime.' }; } if (startTime && endTime && endTime <= startTime) { return { type: 'time-order', target: fields.endTime, message: 'End time must be after start time.' }; } if (startDatetime && endDatetime) { var dateRangeDayIndices = getDateRangeDayIndices(startDatetime, endDatetime); var dayIndicesToCheck = checkedDayIndices.length ? checkedDayIndices : [0, 1, 2, 3, 4, 5, 6]; var invalidDayInputs = checkedDayInputs.filter(function (input) { return dateRangeDayIndices.indexOf(Number(input && input.value)) === -1; }); if (checkedDayInputs.length && invalidDayInputs.length === checkedDayInputs.length) { return { type: 'day-overlap', target: checkedDayInputs[0] || dayInputs[0] || fields.endDatetime, message: 'Selected days must overlap the date range.' }; } if (startTime && endTime && !hasScheduleRuleOverlap(startDatetime, endDatetime, startTime, endTime, dayIndicesToCheck)) { return { type: 'time-overlap', target: fields.endTime, secondaryTarget: fields.startTime, message: 'Start and end times must overlap the date range.' }; } } return null; } function applyScheduleRuleValidationIssue(card, issue) { var fields = getRuleFields(card); var dayLabels = getRuleDayLabels(card); clearRuleValidity(card); if (!issue) { return true; } if (issue.type === 'time-overlap') { setRuleError(issue.target || fields.endTime, issue.message); if (issue.secondaryTarget) { issue.secondaryTarget.classList.add('is-invalid'); issue.secondaryTarget.setAttribute('aria-invalid', 'true'); } return false; } if (issue.type === 'day-overlap') { var dayInputs = Array.prototype.slice.call(fields.days || []); var dayIndex = dayInputs.indexOf(issue.target); setRuleError(issue.target, issue.message); if (dayIndex !== -1 && dayLabels[dayIndex]) { dayLabels[dayIndex].classList.add('is-invalid'); dayLabels[dayIndex].setAttribute('aria-invalid', 'true'); } return false; } setRuleError(issue.target, issue.message); return false; } // Video duration helpers. function getVideoDurationButtonLabel(isPressed) { return isPressed ? 'Disable use video duration' : 'Use video duration'; } function buildVideoDurationButtonContent(isPressed) { return '' + '' + '' + getVideoDurationButtonLabel(isPressed) + ''; } function buildVideoDurationButtonMarkup(isPressed, enabled) { if (!enabled) { return ''; } return ''; } function getDisableAudioButtonLabel(isPressed) { return isPressed ? 'Enable audio' : 'Mute slide'; } function buildDisableAudioButtonContent(isPressed) { return '' + '' + '' + getDisableAudioButtonLabel(isPressed) + ''; } function buildDisableAudioButtonMarkup(isPressed, enabled) { if (!enabled) { return ''; } return ''; } function syncDisableAudioButtonState(button, isPressed) { if (!button) { return; } button.classList.toggle('btn-outline-secondary', Boolean(isPressed)); button.classList.toggle('btn-secondary', !Boolean(isPressed)); button.setAttribute('aria-pressed', isPressed ? 'true' : 'false'); button.setAttribute('aria-label', getDisableAudioButtonLabel(isPressed)); button.setAttribute('title', getDisableAudioButtonLabel(isPressed)); button.innerHTML = buildDisableAudioButtonContent(isPressed); } function syncVideoDurationButtonState(button, isPressed) { if (!button) { return; } button.classList.toggle('btn-outline-info', !Boolean(isPressed)); button.classList.toggle('btn-info', Boolean(isPressed)); button.setAttribute('aria-pressed', isPressed ? 'true' : 'false'); button.setAttribute('aria-label', getVideoDurationButtonLabel(isPressed)); button.setAttribute('title', getVideoDurationButtonLabel(isPressed)); button.innerHTML = buildVideoDurationButtonContent(isPressed); } // Slide picker helpers. function getSlidePickerEmptyMessage(query, showAssignedSlides, assignedCount, visibleCount) { if (!showAssignedSlides && assignedCount && visibleCount === 0) { return query ? 'No available slides match your search.' : 'No available slides are currently left for this playlist.'; } return query ? 'No slides match your search.' : 'No slides are currently available for this playlist.'; } function updateSlidePickerCardState(card, isAssigned, isCanvasMismatch) { if (!card) { return; } card.setAttribute('data-is-assigned', isAssigned ? 'true' : 'false'); card.classList.toggle('is-assigned', isAssigned); card.classList.toggle('is-mismatch', isCanvasMismatch); card.classList.toggle('is-hidden', false); } function createElementWithClass(tagName, className, textContent) { var element = document.createElement(tagName); if (className) { element.className = className; } if (textContent !== undefined) { element.textContent = textContent; } return element; } function escapeHtmlAttribute(value) { return String(value === undefined || value === null ? '' : value) .replace(/&/g, '&') .replace(/"/g, '"') .replace(/'/g, ''') .replace(//g, '>'); } function buildScheduleSummaryChipMarkup(summary) { return '' + escapeHtmlAttribute(String(summary || 'Always visible')) + ''; } function buildSlidePickerThumbPlaceholder() { var placeholder = createElementWithClass('div', 'playlist-slide-picker-thumb-placeholder'); var icon = createElementWithClass('i', 'bi bi-image playlist-slide-picker-thumb-placeholder-icon'); var label = createElementWithClass('span', 'playlist-slide-picker-thumb-placeholder-label', PICKER_THUMB_EMPTY_LABEL); icon.setAttribute('aria-hidden', 'true'); placeholder.appendChild(icon); placeholder.appendChild(label); return placeholder; } function buildSlidePickerThumbImage(slide) { var image = createElementWithClass('img', '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(buildSlidePickerThumbPlaceholder(), image); } }); return image; } function buildSlidePickerMedia(slide) { var media = createElementWithClass('div', PICKER_MEDIA_CLASS); var check = createElementWithClass('span', PICKER_CHECK_CLASS); var badge = createElementWithClass('span', PICKER_BADGE_CLASS, PICKER_ASSIGNED_BADGE_LABEL); check.setAttribute('aria-hidden', 'true'); badge.setAttribute('aria-hidden', 'true'); media.appendChild(slide && slide.thumbnail_path ? buildSlidePickerThumbImage(slide) : buildSlidePickerThumbPlaceholder()); media.appendChild(check); media.appendChild(badge); return media; } // Playlist row helpers. function getPlaylistSlideRow(target) { return target && target.closest ? target.closest('tr[data-playlist-slide-row]') : null; } function getPlaylistSlideRowKey(row) { return String(row && row.getAttribute('data-row-key') || ''); } function isDurationControl(target) { return Boolean(target && target.closest && target.closest('.playlist-use-video-duration')); } function isAudioControl(target) { return Boolean(target && target.closest && target.closest('.playlist-disable-audio')); } function getDurationPointerState(target, type) { var row = getPlaylistSlideRow(target); var pointerType = type === 'button' ? 'button' : 'input'; if (!row) { return null; } return { type: pointerType, rowKey: getPlaylistSlideRowKey(row) }; } function buildPlaylistOrderCellMarkup() { return '' + '' + '
' + '
' + '' + '' + '' + '
' + '' + '
' + ''; } function buildPlaylistSlideCellMarkup(values, thumbnailStyle, thumbnailMarkup) { var groupKey = String(values.groupKey || ''); var groupName = escapeHtmlAttribute(String(values.groupName || '')); var groupPickCount = Math.max(1, Number(values.groupPickCount) || 1); var groupScheduleRules = Array.isArray(values.groupScheduleRules) ? values.groupScheduleRules : []; return '' + '' + '
' + '' + '
' + values.title + '
' + '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + ''; } function buildPlaylistScheduleCellMarkup(values, rowKey) { return '' + '' + '
' + buildScheduleSummaryChipMarkup(values.summary || 'Always visible') + '
' + '
' + buildScheduleRulesMarkup(values.scheduleRules, rowKey, 'playlist-edit-form') + '
' + ''; } function buildPlaylistDurationCellMarkup(values, durationActionMarkup) { return '' + '
' + '
' + '' + '' + '
' + '' + '' + (values.useVideoDuration ? '' : '') + durationActionMarkup + '
'; } function buildPlaylistActionsCellMarkup(playlistId, rowKey) { var scheduleUrl = '/playlists/new/slides/0/config'; var scheduleButtonMarkup = ''; return '' + '
' + scheduleButtonMarkup + '' + '
'; } function collectScheduleParams(row, rowKey, isGroupSchedule) { var params = new URLSearchParams(); var fields = !isGroupSchedule && row ? Array.prototype.slice.call(row.querySelectorAll('[name^="schedule_rule_"]')) : []; params.set('row_key', String(rowKey || '')); fields.forEach(function (field) { if (!field || !field.name) { return; } params.append(field.name, String(field.value || '')); }); if (isGroupSchedule && row) { var rulesInput = row.querySelector('[data-playlist-group-schedule-rules]'); var rules = []; try { rules = JSON.parse(rulesInput && rulesInput.value || '[]'); } catch (_error) { rules = []; } (Array.isArray(rules) ? rules : []).forEach(function (rule, index) { params.append('schedule_rule_row_key[]', String(rowKey || '')); params.append('schedule_rule_position[]', String(index)); params.append('schedule_rule_start_datetime[]', String(rule && rule.start_datetime || '')); params.append('schedule_rule_end_datetime[]', String(rule && rule.end_datetime || '')); params.append('schedule_rule_start_time[]', String(rule && rule.start_time || '')); params.append('schedule_rule_end_time[]', String(rule && rule.end_time || '')); params.append('schedule_rule_days_csv[]', Array.isArray(rule && rule.days) ? rule.days.join(',') : ''); }); } return params; } function openScheduleModal(url) { var dialog = document.getElementById('slide-schedule-dialog'); var content = document.getElementById('slide-schedule-content'); if (!dialog || !content) { return; } 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; if (typeof window.initPlaylistScheduleForm === 'function') { window.initPlaylistScheduleForm(content); } }).catch(function (error) { content.innerHTML = '
' + escapeHtmlAttribute(String(error && error.message ? error.message : 'Unable to open schedule editor.')) + '
'; }); } function closeScheduleModal(options) { var dialog = document.getElementById('slide-schedule-dialog'); var content = document.getElementById('slide-schedule-content'); var shouldDiscardDraft = Boolean(options && options.discardDraft); var shouldPreserveChanges = Boolean(options && options.preserveChanges); if (shouldDiscardDraft && !shouldPreserveChanges && typeof window.restorePlaylistScheduleDraft === 'function') { window.restorePlaylistScheduleDraft(); } if (shouldDiscardDraft && typeof window.clearPlaylistScheduleDraft === 'function') { window.clearPlaylistScheduleDraft(); } if (content) { content.innerHTML = ''; } if (!dialog) { return; } if (typeof dialog.close === 'function') { dialog.close(); } else { dialog.removeAttribute('open'); } } function initPlaylistScheduleModal() { window.openScheduleModal = openScheduleModal; window.closeScheduleModal = closeScheduleModal; } // Keep schedule rule markup in sync with each rendered playlist row. function hydrateScheduleRowContainers(root) { var scope = root || document; var rows = Array.prototype.slice.call(scope.querySelectorAll('[data-playlist-slide-row]')); rows.forEach(function (row) { var fields = getScheduleRowFields(row); var data = fields.rulesContainer ? fields.rulesContainer.getAttribute('data-schedule-rules') : ''; var rules = []; if (!fields.rulesContainer) { return; } if (!data && fields.rulesContainer.children.length) { return; } if (data) { try { rules = JSON.parse(data); } catch (_error) { rules = []; } } if (!fields.rulesContainer.children.length) { fields.rulesContainer.innerHTML = buildScheduleRulesMarkup(rules, row.getAttribute('data-row-key') || '', 'playlist-edit-form'); } }); } function setRowSchedule(row, values) { var rowKey = String(values && values.row_key ? values.row_key : row && row.getAttribute('data-row-key') || ''); var fields = getScheduleRowFields(row); var rules = Array.isArray(values && values.scheduleRules) ? values.scheduleRules : []; var summary = String(values && values.summary ? values.summary : formatScheduleRulesSummary(rules) || 'Always visible'); if (!row) { return; } if (fields.summaryNode) { fields.summaryNode.innerHTML = buildScheduleSummaryChipMarkup(summary || 'Always visible'); } if (fields.rulesContainer) { fields.rulesContainer.setAttribute('data-schedule-rules', JSON.stringify(rules).replace(/' + '
' + '
' + '

Rule ' + String(ruleIndex) + '

' + '
' + escapeHtmlAttribute(formatScheduleRuleSummary(normalizedRule)) + '
' + '
' + '
' + '' + '' + '
' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '' + '' + '
' + '
' + '
' + DAY_NAMES.map(function (label, dayIndex) { return '' + '' + '' + ''; }).join('') + '
' + '
' + '
' + '' + '
' + ''; } // Modal form state, validation, and submit handling. function initPlaylistScheduleForm(root) { var scope = root || document; var form = scope.querySelector('form[action*="/config"], form[data-schedule-draft="true"]'); var scheduleRuleList = scope.querySelector('#schedule-rule-list'); var effectiveSummary = scope.querySelector('#schedule-effective-summary'); var addRuleButton = scope.querySelector('#schedule-add-rule'); var scheduleRuleTemplate = scope.querySelector('#schedule-rule-card-template'); if (!form || !scheduleRuleList) { return; } var isDraftMode = Boolean(form.getAttribute('data-schedule-draft') === 'true'); var rowKeyInput = form.querySelector('[name="row_key"]'); var draftStore = window.__playlistScheduleDraftStore = window.__playlistScheduleDraftStore || {}; var draftSnapshotStore = window.__playlistScheduleDraftSnapshotStore = window.__playlistScheduleDraftSnapshotStore || {}; var cancelButton = scope.querySelector('[data-schedule-cancel]'); // Draft state is keyed by the row being edited so cancel/save can restore it later. function getScheduleDraftKey() { return String(rowKeyInput && rowKeyInput.value ? rowKeyInput.value : '').trim(); } function readRuleDraftFromCard(card) { var fields = getRuleFields(card); var days = []; Array.prototype.forEach.call(fields.days || [], function (checkbox) { if (checkbox.checked) { days.push(Number(checkbox.value)); } }); days = Array.from(new Set(days)).sort(function (left, right) { return left - right; }); return { start_datetime: String(fields.startDatetime && fields.startDatetime.value || ''), end_datetime: String(fields.endDatetime && fields.endDatetime.value || ''), start_time: String(fields.startTime && fields.startTime.value || ''), end_time: String(fields.endTime && fields.endTime.value || ''), days: days }; } function captureScheduleDraftState() { return getRuleCards().map(function (card) { return { rule: readRuleDraftFromCard(card), isCollapsed: Boolean(card && card.classList && card.classList.contains('collapsed-card')) }; }); } function renderScheduleDraftState(ruleStates) { var states = Array.isArray(ruleStates) ? ruleStates : []; scheduleRuleList.innerHTML = ''; states.forEach(function (ruleState, index) { var card = createRuleCardElement(ruleState && ruleState.rule ? ruleState.rule : {}, index + 1, { isCollapsed: Boolean(ruleState && ruleState.isCollapsed) }); if (card) { scheduleRuleList.appendChild(card); } }); refreshRuleNumbers(); syncScheduleRulesField(); } function restoreScheduleDraft() { var draftKey = getScheduleDraftKey(); var snapshot = draftKey && Object.prototype.hasOwnProperty.call(draftSnapshotStore, draftKey) ? draftSnapshotStore[draftKey] : null; if (!draftKey || !Array.isArray(snapshot)) { return; } renderScheduleDraftState(snapshot); } function getScheduleDraftRules() { var draftKey = getScheduleDraftKey(); if (!draftKey || !Object.prototype.hasOwnProperty.call(draftStore, draftKey)) { return null; } return Array.isArray(draftStore[draftKey]) ? draftStore[draftKey] : []; } function persistScheduleDraft() { var draftKey = getScheduleDraftKey(); if (!draftKey) { return; } draftStore[draftKey] = getRuleCards().map(function (card) { return { rule: readRuleDraftFromCard(card), isCollapsed: Boolean(card && card.classList && card.classList.contains('collapsed-card')) }; }); } function clearScheduleDraft() { var draftKey = getScheduleDraftKey(); if (draftKey && Object.prototype.hasOwnProperty.call(draftStore, draftKey)) { delete draftStore[draftKey]; } if (draftKey && Object.prototype.hasOwnProperty.call(draftSnapshotStore, draftKey)) { delete draftSnapshotStore[draftKey]; } } window.clearPlaylistScheduleDraft = clearScheduleDraft; window.restorePlaylistScheduleDraft = restoreScheduleDraft; var draftKey = getScheduleDraftKey(); if (draftKey && !Object.prototype.hasOwnProperty.call(draftSnapshotStore, draftKey)) { draftSnapshotStore[draftKey] = captureScheduleDraftState(); } function getRuleCards() { return Array.prototype.slice.call(scheduleRuleList.querySelectorAll('[data-schedule-rule-card]')); } function getRuleFields(card) { return { startDatetime: card ? card.querySelector('[data-schedule-rule-start-datetime]') : null, endDatetime: card ? card.querySelector('[data-schedule-rule-end-datetime]') : null, startTime: card ? card.querySelector('[data-schedule-rule-start-time]') : null, endTime: card ? card.querySelector('[data-schedule-rule-end-time]') : null, days: card ? card.querySelectorAll('[data-schedule-rule-day]') : [] }; } function getRuleSummaryNode(card) { return card ? card.querySelector('[data-schedule-rule-summary]') : null; } function getRuleNumberNode(card) { return card ? card.querySelector('[data-schedule-rule-number]') : null; } function getRuleRowKeyNode(card) { return card ? card.querySelector('[data-schedule-rule-row-key]') : null; } function getRulePositionNode(card) { return card ? card.querySelector('[data-schedule-rule-position]') : null; } function getRemoveRuleButton(card) { return card ? card.querySelector('[data-remove-schedule-rule]') : null; } function getRuleDayLabels(card) { return card ? Array.prototype.slice.call(card.querySelectorAll('.schedule-day-button')) : []; } function getCollapseRuleButton(card) { return card ? card.querySelector('[data-schedule-rule-collapse], [data-lte-toggle="card-collapse"]') : null; } function getCollapseRuleIcon(card) { return card ? card.querySelector('[data-schedule-rule-collapse-icon]') : null; } function syncRuleCollapseButtonState(card) { var button = getCollapseRuleButton(card); var icon = getCollapseRuleIcon(card); var numberNode = getRuleNumberNode(card); var ruleIndex = numberNode ? String(numberNode.textContent || '') : ''; var isCollapsed = Boolean(card && card.classList && card.classList.contains('collapsed-card')); var label = (isCollapsed ? 'Expand' : 'Collapse') + ' rule ' + ruleIndex; if (!button) { return; } button.setAttribute('aria-expanded', isCollapsed ? 'false' : 'true'); button.setAttribute('aria-label', label); button.setAttribute('title', label); if (icon) { icon.classList.remove('bi-dash-lg', 'bi-plus-lg'); icon.classList.add(isCollapsed ? 'bi-plus-lg' : 'bi-dash-lg'); } var visibleLabel = button.querySelector('.visually-hidden'); if (visibleLabel) { visibleLabel.textContent = label; } } function clearRuleValidity(card) { var fields = getRuleFields(card); var dayInputs = Array.prototype.slice.call(fields.days || []); var dayLabels = getRuleDayLabels(card); var feedbackNode = card ? card.querySelector('[data-schedule-rule-feedback]') : null; [fields.startDatetime, fields.endDatetime, fields.startTime, fields.endTime].forEach(function (input) { if (input) { input.setCustomValidity(''); input.classList.remove('is-invalid'); input.removeAttribute('aria-invalid'); } }); dayInputs.forEach(function (input) { input.setCustomValidity(''); input.classList.remove('is-invalid'); input.removeAttribute('aria-invalid'); }); dayLabels.forEach(function (label) { label.classList.remove('is-invalid'); label.removeAttribute('aria-invalid'); }); if (feedbackNode) { feedbackNode.innerHTML = ''; feedbackNode.hidden = true; } } function setRuleError(input, message) { if (!input) { return; } input.setCustomValidity(message); if (String(input.value || '').trim()) { input.classList.add('is-invalid'); input.setAttribute('aria-invalid', 'true'); } appendRuleFeedbackMessage(input.closest ? input.closest('[data-schedule-rule-card]') : null, message); } function validateScheduleRuleCard(card) { var fields = getRuleFields(card); var dayInputs = Array.prototype.slice.call(fields.days || []); var dayLabels = getRuleDayLabels(card); var checkedDayInputs = dayInputs.filter(function (input) { return Boolean(input && input.checked); }); var checkedDayIndices = checkedDayInputs.map(function (input) { return Number(input && input.value); }); var startDatetime = String(fields.startDatetime && fields.startDatetime.value || '').trim(); var endDatetime = String(fields.endDatetime && fields.endDatetime.value || '').trim(); var startTime = String(fields.startTime && fields.startTime.value || '').trim(); var endTime = String(fields.endTime && fields.endTime.value || '').trim(); var isValid = true; clearRuleValidity(card); if ((startDatetime || endDatetime) && (!startDatetime || !endDatetime)) { setRuleError(!startDatetime ? fields.startDatetime : fields.endDatetime, 'Start and end datetimes must both be set for this rule.'); isValid = false; } if ((startTime || endTime) && (!startTime || !endTime)) { setRuleError(!startTime ? fields.startTime : fields.endTime, 'Start and end times must both be set for this rule.'); isValid = false; } if (startDatetime && endDatetime && new Date(endDatetime) <= new Date(startDatetime)) { setRuleError(fields.endDatetime, 'End datetime must be after start datetime.'); isValid = false; } if (startTime && endTime && endTime <= startTime) { setRuleError(fields.endTime, 'End time must be after start time.'); if (fields.startTime) { fields.startTime.classList.add('is-invalid'); fields.startTime.setAttribute('aria-invalid', 'true'); } isValid = false; } if (startDatetime && endDatetime) { var dateRangeDayIndices = getDateRangeDayIndices(startDatetime, endDatetime); var dayIndicesToCheck = checkedDayIndices.length ? checkedDayIndices : [0, 1, 2, 3, 4, 5, 6]; var invalidDayInputs = checkedDayInputs.filter(function (input) { return dateRangeDayIndices.indexOf(Number(input && input.value)) === -1; }); invalidDayInputs.forEach(function (input) { var dayIndex = dayInputs.indexOf(input); if (dayIndex !== -1 && dayLabels[dayIndex]) { dayLabels[dayIndex].classList.add('is-invalid'); dayLabels[dayIndex].setAttribute('aria-invalid', 'true'); } }); if (checkedDayInputs.length && invalidDayInputs.length === checkedDayInputs.length) { setDayInputError(card, checkedDayInputs[0] || dayInputs[0], 'Selected days must overlap the date range.'); isValid = false; } if (startTime && endTime && !hasScheduleRuleOverlap(startDatetime, endDatetime, startTime, endTime, dayIndicesToCheck)) { setRuleError(fields.endTime, 'Start and end times must overlap the date range.'); if (fields.startTime) { fields.startTime.classList.add('is-invalid'); fields.startTime.setAttribute('aria-invalid', 'true'); } isValid = false; } } return isValid; } function updateLiveRuleValidity(card) { validateScheduleRuleCard(card); } function validateRuleCard(card) { return validateScheduleRuleCard(card); } function readRuleFromCard(card) { var fields = getRuleFields(card); var days = []; Array.prototype.forEach.call(fields.days || [], function (checkbox) { if (checkbox.checked) { days.push(Number(checkbox.value)); } }); days = Array.from(new Set(days)).sort(function (left, right) { return left - right; }); return normalizeScheduleRuleValue({ start_datetime: fields.startDatetime && fields.startDatetime.value || '', end_datetime: fields.endDatetime && fields.endDatetime.value || '', start_time: fields.startTime && fields.startTime.value || '', end_time: fields.endTime && fields.endTime.value || '', days: days }); } function updateRuleCardSummary(card) { var summaryNode = getRuleSummaryNode(card); var rule = readRuleFromCard(card); if (summaryNode) { summaryNode.textContent = formatScheduleRuleSummary(rule); } return rule; } function refreshRuleNumbers() { getRuleCards().forEach(function (card, index) { var numberNode = getRuleNumberNode(card); var rowKeyNode = getRuleRowKeyNode(card); var positionNode = getRulePositionNode(card); var removeButton = getRemoveRuleButton(card); var ruleIndex = index + 1; if (numberNode) { numberNode.textContent = String(ruleIndex); } if (rowKeyNode) { rowKeyNode.value = String(getScheduleDraftKey() || ''); } if (positionNode) { positionNode.value = String(index); } if (removeButton) { removeButton.disabled = false; } card.setAttribute('data-schedule-rule-key', 'schedule-rule-' + ruleIndex); }); } function renderEffectiveSummary(rules) { if (!effectiveSummary) { return; } effectiveSummary.innerHTML = ''; if (!rules.length) { effectiveSummary.textContent = 'Always visible'; return; } var list = document.createElement('ul'); list.className = 'mb-0'; rules.forEach(function (rule, index) { var item = document.createElement('li'); item.textContent = 'Rule ' + (index + 1) + ': ' + formatScheduleRuleSummary(rule); list.appendChild(item); }); effectiveSummary.appendChild(list); } // Rebuild the hidden form fields and keep the live summary chip current. function syncScheduleRulesField() { var rules = []; getRuleCards().forEach(function (card) { var rule = readRuleFromCard(card); if (rule) { rules.push(rule); } }); renderEffectiveSummary(rules); persistScheduleDraft(); if (window.parent && typeof window.parent.applyPlaylistScheduleConfig === 'function') { window.parent.applyPlaylistScheduleConfig({ row_key: String(rowKeyInput && rowKeyInput.value ? rowKeyInput.value : ''), scheduleRules: rules, summary: formatScheduleRulesSummary(rules) }); } return rules; } function getRuleCardTemplate() { if (!scheduleRuleTemplate || !scheduleRuleTemplate.content) { return null; } return scheduleRuleTemplate.content.firstElementChild; } function updateRuleCardDayOptions(card, ruleKey, normalizedRule) { var dayInputs = card ? Array.prototype.slice.call(card.querySelectorAll('[data-schedule-rule-day]')) : []; var dayLabels = card ? Array.prototype.slice.call(card.querySelectorAll('.schedule-day-button')) : []; dayInputs.forEach(function (input, dayIndex) { var label = dayLabels[dayIndex]; var isChecked = Array.isArray(normalizedRule.days) && normalizedRule.days.indexOf(dayIndex) !== -1; var inputId = ruleKey + '-day-' + dayIndex; input.id = inputId; input.value = String(dayIndex); input.checked = isChecked; input.autocomplete = 'off'; input.setAttribute('data-schedule-rule-day', ''); if (label) { label.setAttribute('for', inputId); } }); } function createRuleCardElement(rule, ruleIndex, options) { var ruleKey = 'schedule-rule-' + ruleIndex + '-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8); var normalizedRule = normalizeScheduleRuleValue(rule) || {}; var cardTemplate = getRuleCardTemplate(); var isCollapsed = Boolean(options && options.isCollapsed); var card; if (!cardTemplate) { return null; } card = cardTemplate.cloneNode(true); card.setAttribute('data-schedule-rule-card', ''); card.setAttribute('data-schedule-rule-key', ruleKey); card.querySelector('[data-schedule-rule-number]').textContent = String(ruleIndex); card.querySelector('[data-schedule-rule-summary]').textContent = formatScheduleRuleSummary(normalizedRule); card.querySelector('[data-schedule-rule-row-key]').value = String(getScheduleDraftKey() || ''); card.querySelector('[data-schedule-rule-position]').value = String(Math.max(0, ruleIndex - 1)); card.querySelector('[data-schedule-rule-start-datetime]').value = String(normalizedRule.start_datetime || ''); card.querySelector('[data-schedule-rule-end-datetime]').value = String(normalizedRule.end_datetime || ''); card.querySelector('[data-schedule-rule-start-time]').value = String(normalizedRule.start_time || ''); card.querySelector('[data-schedule-rule-end-time]').value = String(normalizedRule.end_time || ''); card.querySelector('[data-remove-schedule-rule]').setAttribute('data-confirm-message', 'Remove this rule?'); updateRuleCardDayOptions(card, ruleKey, normalizedRule); if (isCollapsed) { card.classList.add('collapsed-card'); } else { card.classList.remove('collapsed-card'); } try { updateLiveRuleValidity(card); } catch (_error) { clearRuleValidity(card); } syncRuleCollapseButtonState(card); return card; } function addRule(rule, options) { var ruleCount = getRuleCards().length + 1; var card = createRuleCardElement(rule || {}, ruleCount, options); if (!card) { scheduleRuleList.insertAdjacentHTML('beforeend', buildScheduleRuleCardMarkup(rule || {}, ruleCount, Boolean(options && options.isCollapsed))); card = scheduleRuleList.lastElementChild; } if (!card) { return null; } if (card.parentNode !== scheduleRuleList) { scheduleRuleList.appendChild(card); } refreshRuleNumbers(); syncScheduleRulesField(); return card; } function handleCardChange(card) { updateLiveRuleValidity(card); updateRuleCardSummary(card); syncScheduleRulesField(); } function handleRuleRemoval(card) { if (!card) { return; } card.remove(); refreshRuleNumbers(); syncScheduleRulesField(); } // Block saves when any rule is incomplete or out of order. function validateScheduleForm() { var isValid = true; getRuleCards().forEach(function (card) { if (!validateRuleCard(card)) { isValid = false; } }); if (isValid) { syncScheduleRulesField(); } return isValid && form.checkValidity(); } // Persist the modal form back to the server for non-draft rows. function saveScheduleForm() { return fetch(form.action, { method: 'POST', credentials: 'same-origin', body: new URLSearchParams(new FormData(form)) }).then(function (response) { if (!response.ok) { return response.text().then(function (text) { throw new Error(text || 'Unable to save schedule.'); }); } return response; }); } scheduleRuleList.addEventListener('input', function (event) { var card = event.target && event.target.closest ? event.target.closest('[data-schedule-rule-card]') : null; if (!card) { return; } handleCardChange(card); }); scheduleRuleList.addEventListener('change', function (event) { var card = event.target && event.target.closest ? event.target.closest('[data-schedule-rule-card]') : null; if (!card) { return; } handleCardChange(card); }); scheduleRuleList.addEventListener('click', function (event) { var removeButton = event.target && event.target.closest ? event.target.closest('[data-remove-schedule-rule]') : null; var collapseButton = event.target && event.target.closest ? event.target.closest('[data-schedule-rule-collapse], [data-lte-toggle="card-collapse"]') : null; if (collapseButton) { window.setTimeout(function () { syncRuleCollapseButtonState(collapseButton.closest('[data-schedule-rule-card]')); persistScheduleDraft(); }, 0); return; } if (!removeButton) { return; } event.preventDefault(); var confirmMessage = removeButton.getAttribute('data-confirm-message'); if (confirmMessage && !window.confirm(confirmMessage)) { return; } handleRuleRemoval(removeButton.closest('[data-schedule-rule-card]')); }); if (addRuleButton) { addRuleButton.addEventListener('click', function () { addRule({}, { isCollapsed: false }); }); } if (cancelButton) { cancelButton.addEventListener('click', function () { if (typeof window.closeScheduleModal === 'function') { window.closeScheduleModal({ discardDraft: true }); } }); } var draftRules = getScheduleDraftRules(); if (draftRules !== null) { renderScheduleDraftState(draftRules); } else if (!getRuleCards().length) { syncScheduleRulesField(); } else { refreshRuleNumbers(); getRuleCards().forEach(function (card) { syncRuleCollapseButtonState(card); updateLiveRuleValidity(card); updateRuleCardSummary(card); }); syncScheduleRulesField(); } form.addEventListener('submit', function (event) { event.preventDefault(); if (!validateScheduleForm()) { form.reportValidity(); return; } var rules = syncScheduleRulesField(); var values = { row_key: String(rowKeyInput && rowKeyInput.value ? rowKeyInput.value : ''), scheduleRules: rules, summary: '' }; values.summary = formatScheduleRulesSummary(values.scheduleRules); if (isDraftMode) { clearScheduleDraft(); if (window.parent && typeof window.parent.applyPlaylistScheduleConfig === 'function') { window.parent.applyPlaylistScheduleConfig(values); } if (typeof window.closeScheduleModal === 'function') { window.closeScheduleModal({ discardDraft: true, preserveChanges: true }); } return; } saveScheduleForm().then(function () { clearScheduleDraft(); if (window.parent && typeof window.parent.applyPlaylistScheduleConfig === 'function') { window.parent.applyPlaylistScheduleConfig(values); if (typeof window.parent.closeScheduleModal === 'function') { window.parent.closeScheduleModal({ discardDraft: true, preserveChanges: true }); } return; } if (typeof window.closeScheduleModal === 'function') { window.closeScheduleModal({ discardDraft: true, preserveChanges: true }); } }).catch(function (error) { window.alert(String(error && error.message ? error.message : 'Unable to save schedule.')); }); }); } // Playlist editor behavior. // Playlist table editing, picker modal, and row sync logic. 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 addSlideShowAssigned = document.getElementById('playlist-slide-picker-show-assigned'); 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 createGroupButton = document.getElementById('playlist-create-group'); var canvasSizeSelect = document.getElementById('playlist-canvas-size'); var canvasSizeLockInput = document.getElementById('playlist-canvas-size-lock'); var slidePickerCards = []; var slidePickerSelection = new Set(); var slidePickerSlides = []; var lastDurationPointerDown = null; var lastDurationPointerUp = null; var defaultSlideDuration = Number(tbody.getAttribute('data-default-slide-duration')) || 10; if (!tbody || !form) { return; } if (addSlideData) { try { var parsedSlides = JSON.parse(addSlideData.textContent || '[]'); slidePickerSlides = Array.isArray(parsedSlides) ? parsedSlides : []; } catch (_error) { slidePickerSlides = []; } } // Selection state helpers for the picker modal. 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 getCanvasSizeEditable() { return Boolean(canvasSizeSelect && String(canvasSizeSelect.getAttribute('data-playlist-canvas-editable') || '') === 'true'); } function getLockedCanvasId() { var canvasId = Number(canvasSizeSelect && canvasSizeSelect.value ? canvasSizeSelect.value : 0) || null; if (canvasId) { return canvasId; } var canvasIds = {}; getRows().forEach(function (row) { var rowCanvasId = Number(row.getAttribute('data-canvas-id') || 0) || null; if (rowCanvasId) { canvasIds[rowCanvasId] = true; } }); var canvasKeys = Object.keys(canvasIds); if (canvasKeys.length === 1) { return Number(canvasKeys[0]) || null; } return null; } function syncPlaylistCanvasLock() { if (!canvasSizeSelect) { return; } var shouldLock = !getCanvasSizeEditable() || getRows().length > 0; var lockedCanvasId = shouldLock ? getLockedCanvasId() : null; if (shouldLock && !canvasSizeSelect.value && lockedCanvasId) { canvasSizeSelect.value = String(lockedCanvasId); } canvasSizeSelect.disabled = shouldLock; if (canvasSizeLockInput) { canvasSizeLockInput.disabled = !shouldLock; canvasSizeLockInput.value = shouldLock ? String(canvasSizeSelect.value || lockedCanvasId || '') : ''; } if (tbody && tbody.setAttribute) { tbody.setAttribute('data-playlist-canvas-id', String(shouldLock ? (canvasSizeSelect.value || lockedCanvasId || '') : getPlaylistCanvasId() || '')); } } function syncAddSlideButtonState() { if (!addSlideOpenButton) { return; } addSlideOpenButton.disabled = !Boolean(getPlaylistCanvasId()); } 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); } } // Picker card rendering. function createPickerCard(slide) { var button = document.createElement('button'); var searchText = String(slide && slide.title ? slide.title : '').toLowerCase(); button.type = 'button'; button.className = PICKER_CARD_CLASS; button.setAttribute('aria-pressed', 'false'); button.setAttribute('data-slide-id', String(slide.id || '')); button.setAttribute('data-canvas-id', String(slide && slide.canvasSizeId ? slide.canvasSizeId : '')); button.setAttribute('data-search-text', searchText); button.setAttribute('data-is-assigned', slide && slide.isAssigned ? 'true' : 'false'); if (slide && slide.isAssigned) { button.classList.add('is-assigned'); } var media = buildSlidePickerMedia(slide); var title = document.createElement('div'); title.className = PICKER_TITLE_CLASS; title.textContent = String(slide && slide.title ? slide.title : 'Slide'); button.appendChild(media); button.appendChild(title); return button; } // Picker filtering and modal controls. function setShowAssignedState(showAssignedSlides) { if (!addSlideShowAssigned) { return; } addSlideShowAssigned.setAttribute('aria-pressed', showAssignedSlides ? 'true' : 'false'); addSlideShowAssigned.classList.toggle('btn-secondary', showAssignedSlides); addSlideShowAssigned.classList.toggle('btn-outline-secondary', !showAssignedSlides); } function renderSlidePicker() { if (!addSlideGrid) { return; } addSlideGrid.innerHTML = ''; slidePickerCards = slidePickerSlides.map(function (slide) { var card = createPickerCard(slide); addSlideGrid.appendChild(card); return card; }); syncSlidePickerState(); } function getPlaylistCanvasId() { if (canvasSizeSelect && canvasSizeSelect.value) { return Number(canvasSizeSelect.value) || null; } if (canvasSizeSelect && (!canvasSizeSelect.disabled || !canvasSizeSelect.value)) { return canvasSizeLockInput && canvasSizeLockInput.value ? Number(canvasSizeLockInput.value) || null : null; } if (canvasSizeLockInput && canvasSizeLockInput.value) { return Number(canvasSizeLockInput.value) || null; } return Number(tbody && tbody.getAttribute ? tbody.getAttribute('data-playlist-canvas-id') : 0) || null; } function syncPlaylistCanvasId() { if (tbody && tbody.setAttribute) { tbody.setAttribute('data-playlist-canvas-id', String(getPlaylistCanvasId() || '')); } syncAddSlideOptions(); syncSlidePickerState(); syncPlaylistCanvasLock(); syncAddSlideButtonState(); } function syncSlidePickerState() { var query = String(addSlideSearch && addSlideSearch.value ? addSlideSearch.value : '').trim().toLowerCase(); var showAssignedSlides = Boolean(addSlideShowAssigned && addSlideShowAssigned.getAttribute('aria-pressed') === 'true'); var visibleCount = 0; var assignedCount = 0; var allowedCanvasId = getPlaylistCanvasId(); var activeCanvasIds = {}; if (!allowedCanvasId) { getRows().forEach(function (row) { var canvasId = Number(row.getAttribute('data-canvas-id') || 0) || null; if (canvasId) { activeCanvasIds[canvasId] = true; } }); if (!Object.keys(activeCanvasIds).length) { slidePickerCards.forEach(function (card) { if (slidePickerSelection.has(String(card.getAttribute('data-slide-id') || ''))) { var selectedCanvasId = Number(card.getAttribute('data-canvas-id') || 0) || null; if (selectedCanvasId) { activeCanvasIds[selectedCanvasId] = true; } } }); } var canvasKeys = Object.keys(activeCanvasIds); if (canvasKeys.length === 1) { allowedCanvasId = Number(canvasKeys[0]) || null; } } slidePickerCards.forEach(function (card) { var searchText = String(card.getAttribute('data-search-text') || ''); var isAssigned = card.getAttribute('data-is-assigned') === 'true'; var canvasId = Number(card.getAttribute('data-canvas-id') || 0) || null; var isCanvasMismatch = Boolean(allowedCanvasId && canvasId && canvasId !== allowedCanvasId); var matchesSearch = !query || searchText.indexOf(query) !== -1; var matchesAssignment = showAssignedSlides || !isAssigned; var matches = matchesSearch && matchesAssignment && !isCanvasMismatch; updateSlidePickerCardState(card, isAssigned, isCanvasMismatch); setCardVisible(card, matches); if (isAssigned) { assignedCount += 1; } if (matches) { visibleCount += 1; } }); if (addSlideEmpty) { addSlideEmpty.textContent = getSlidePickerEmptyMessage(query, showAssignedSlides, assignedCount, visibleCount); addSlideEmpty.classList.toggle('is-hidden', visibleCount !== 0); } if (addSlideCount) { addSlideCount.textContent = String(slidePickerSelection.size); } if (addSlideConfirm) { addSlideConfirm.disabled = slidePickerSelection.size === 0; } setShowAssignedState(showAssignedSlides); } function toggleCardSelection(card) { var slideId = String(card && card.getAttribute('data-slide-id') ? card.getAttribute('data-slide-id') : ''); var selected; if (!slideId || String(card && card.getAttribute('data-is-assigned') || '') === 'true') { return; } selected = !slidePickerSelection.has(slideId); if (selected) { slidePickerSelection.add(slideId); } else { slidePickerSelection.delete(slideId); } setCardSelected(card, selected); syncSlidePickerState(); } function addSelectedSlides() { var currentTbody = document.getElementById('playlist-items-body') || tbody; var selectedSlides = slidePickerSlides.filter(function (slide) { return slidePickerSelection.has(String(slide.id || '')) && !slide.isAssigned; }); if (!selectedSlides.length) { return; } if (currentTbody && currentTbody.querySelector('.playlist-empty-row')) { currentTbody.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_width: slide.canvas_width, canvas_height: slide.canvas_height, canvas_id: slide.canvasSizeId, showVideoDurationButton: slide.showVideoDurationButton, showMuteButton: slide.showMuteButton, videoSourcePath: slide.videoSourcePath, videoDurationSeconds: slide.videoDurationSeconds, disableAudio: slide.disableAudio, title: slide.title || 'Slide', duration_seconds: defaultSlideDuration, durationSeconds: defaultSlideDuration, scheduleRules: [], summary: 'Always visible' }); if (currentTbody) { currentTbody.appendChild(row); } }); attachSlideThumbFallbacks(currentTbody || tbody); updateRowOrder(true); if (!window.pulseModal || !window.pulseModal.hide(addSlideModal)) { addSlideModal.classList.remove('show'); addSlideModal.setAttribute('aria-hidden', 'true'); } } attachSlideThumbFallbacks(tbody); function getRows() { var currentTbody = document.getElementById('playlist-items-body') || tbody; return currentTbody ? Array.prototype.slice.call(currentTbody.querySelectorAll('tr[data-playlist-slide-row]:not([data-playlist-ghost-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 getGroupHeader(groupKey) { return Array.prototype.slice.call((document.getElementById('playlist-items-body') || tbody).querySelectorAll('[data-playlist-group-row]')).find(function (header) { return String(header.getAttribute('data-group-key') || '') === String(groupKey || ''); }) || null; } function getGroupRows(groupKey) { return getRows().filter(function (row) { var keyInput = row.querySelector('[data-playlist-group-key]'); return keyInput && String(keyInput.value || '') === String(groupKey || ''); }); } function updateGroupScheduleWarnings() { getRows().forEach(function (row) { var groupKeyInput = row.querySelector('[data-playlist-group-key]'); var groupKey = String(groupKeyInput && groupKeyInput.value || ''); var scheduleContainer = row.querySelector('[data-schedule-rules-container]'); var scheduleSummary = row.querySelector('.playlist-schedule-summary'); var groupHeader = groupKey ? getGroupHeader(groupKey) : null; var groupRulesInput = groupHeader && groupHeader.querySelector('[data-playlist-group-schedule-rules]'); var slideRules = parseScheduleRulesValue(scheduleContainer && scheduleContainer.getAttribute('data-schedule-rules') || '[]'); var groupRules = parseScheduleRulesValue(groupRulesInput && groupRulesInput.value || '[]'); var hasConflict = Boolean(groupKey && slideRules.length && groupRules.length && !scheduleRuleSetsCanOverlap(slideRules, groupRules)); var indicator = scheduleSummary && scheduleSummary.querySelector('[data-group-schedule-conflict]'); row.classList.toggle('playlist-schedule-conflict-row', hasConflict); if (!scheduleSummary) { return; } if (!hasConflict) { if (indicator) indicator.remove(); return; } if (!indicator) { indicator = document.createElement('span'); indicator.className = 'chip playlist-schedule-conflict-indicator'; indicator.setAttribute('data-group-schedule-conflict', ''); indicator.setAttribute('role', 'img'); scheduleSummary.appendChild(indicator); } indicator.setAttribute('aria-label', 'Never shown: slide and group schedules do not overlap'); indicator.title = 'This slide and its group have no overlapping schedule, so it will never display.'; indicator.innerHTML = ' Never shown'; }); } window.refreshPlaylistGroupScheduleWarnings = updateGroupScheduleWarnings; function getGroupHeaders() { var currentTbody = document.getElementById('playlist-items-body') || tbody; return currentTbody ? Array.prototype.slice.call(currentTbody.querySelectorAll('[data-playlist-group-row]')) : []; } function createGroupHeader(values) { var group = values || {}; var groupKey = String(group.groupKey || ''); var groupName = String(group.groupName || 'New group'); var pickCount = Math.max(1, Number(group.groupPickCount) || 1); var rules = Array.isArray(group.groupScheduleRules) ? group.groupScheduleRules : []; var header = document.createElement('tr'); header.className = 'playlist-group-block-row'; header.setAttribute('data-playlist-group-row', ''); header.setAttribute('data-group-key', groupKey); header.setAttribute('data-group-mode', group.groupMode === 'repeat' ? 'repeat' : 'select'); header.innerHTML = '' + '
' + '
' + '' + '' + '' + '
' + '
' + '
' + '' + '' + '
' + '
' + '
' + '
Pick
' + '
' + '' + '' + '
'; header.querySelector('[data-playlist-group-name]').value = groupName; header.querySelector('[data-playlist-group-pick-count]').value = String(pickCount); header.querySelector('[data-playlist-group-repeat-count]').value = String(Math.min(100, Math.max(1, Number(group.groupRepeatCount) || 1))); syncPlaylistGroupModeControls(header); var shuffleButton = header.querySelector('[data-playlist-group-shuffle]'); var shuffleEnabled = Boolean(group.groupShuffle); shuffleButton.setAttribute('aria-pressed', shuffleEnabled ? 'true' : 'false'); shuffleButton.setAttribute('aria-label', shuffleEnabled ? 'Disable shuffle' : 'Enable shuffle'); shuffleButton.title = shuffleEnabled ? 'Disable shuffle' : 'Enable shuffle'; shuffleButton.classList.toggle('btn-secondary', shuffleEnabled); shuffleButton.classList.toggle('btn-outline-secondary', !shuffleEnabled); shuffleButton.querySelector('i').className = shuffleEnabled ? 'bi bi-shuffle' : 'bi bi-chevron-double-right'; header.querySelector('[data-playlist-group-schedule-rules]').value = JSON.stringify(rules); header.querySelector('[data-group-schedule-summary]').textContent = formatScheduleRulesSummary(rules) || 'Always visible'; return header; } function syncPlaylistGroupModeControls(header) { var mode = header.getAttribute('data-group-mode') === 'repeat' ? 'repeat' : 'select'; Array.prototype.forEach.call(header.querySelectorAll('[data-playlist-group-mode]'), function (button) { var active = button.getAttribute('data-playlist-group-mode') === mode; button.setAttribute('aria-pressed', active ? 'true' : 'false'); button.classList.toggle('btn-secondary', active); button.classList.toggle('btn-outline-secondary', !active); }); header.querySelector('[data-playlist-group-select-controls]').hidden = mode === 'repeat'; header.querySelector('[data-playlist-group-repeat-controls]').hidden = mode !== 'repeat'; } function syncGroupRowMetadata(header) { if (!header) { return; } var groupKey = String(header.getAttribute('data-group-key') || ''); var groupMode = header.getAttribute('data-group-mode') === 'repeat' ? 'repeat' : 'select'; var repeatInput = header.querySelector('[data-playlist-group-repeat-count]'); var repeatCount = Math.min(100, Math.max(1, Math.floor(Number(repeatInput && repeatInput.value) || 1))); if (repeatInput) repeatInput.value = String(repeatCount); syncPlaylistGroupModeControls(header); var nameInput = header.querySelector('[data-playlist-group-name]'); var pickInput = header.querySelector('[data-playlist-group-pick-count]'); var shuffleButton = header.querySelector('[data-playlist-group-shuffle]'); var shuffleEnabled = shuffleButton && shuffleButton.getAttribute('aria-pressed') === 'true'; var rulesInput = header.querySelector('[data-playlist-group-schedule-rules]'); var groupName = String(nameInput && nameInput.value || '').trim() || 'Group'; var members = getGroupRows(groupKey); var maximum = Math.max(1, members.length); var pickCount = Math.min(maximum, Math.max(1, Math.floor(Number(pickInput && pickInput.value) || 1))); if (pickInput) { pickInput.max = String(maximum); pickInput.disabled = members.length === 0; pickInput.value = String(pickCount); } var scheduleRules = []; try { scheduleRules = JSON.parse(rulesInput && rulesInput.value || '[]'); } catch (_error) { scheduleRules = []; } if (!Array.isArray(scheduleRules)) { scheduleRules = []; } getGroupRows(groupKey).forEach(function (row) { var keyField = row.querySelector('[data-playlist-group-key]'); var nameField = row.querySelector('[data-playlist-group-name-value]'); var pickField = row.querySelector('[data-playlist-group-pick-count-value]'); var shuffleField = row.querySelector('[data-playlist-group-shuffle-value]'); var modeField = row.querySelector('[data-playlist-group-mode-value]'); var repeatField = row.querySelector('[data-playlist-group-repeat-count-value]'); var scheduleField = row.querySelector('[data-playlist-group-schedule-rules]'); row.classList.add('playlist-group-child'); row.setAttribute('data-group-key', groupKey); if (keyField) keyField.value = groupKey; if (nameField) nameField.value = groupName; if (pickField) pickField.value = String(pickCount); if (shuffleField) shuffleField.value = shuffleEnabled ? '1' : '0'; if (modeField) modeField.value = groupMode; if (repeatField) repeatField.value = String(repeatCount); if (scheduleField) scheduleField.value = JSON.stringify(scheduleRules); }); } function initializePlaylistGroups(currentTbody) { var target = currentTbody || document.getElementById('playlist-items-body') || tbody; var rows = target ? Array.prototype.slice.call(target.querySelectorAll('tr[data-playlist-slide-row]:not([data-playlist-ghost-row])')) : []; var groupValues = new Map(); var renderedKeys = new Set(); var orderedNodes = []; rows.forEach(function (row) { var keyField = row.querySelector('[data-playlist-group-key]'); var groupKey = String(keyField && keyField.value || ''); if (!groupKey || groupValues.has(groupKey)) { return; } var nameField = row.querySelector('[data-playlist-group-name-value]'); var pickField = row.querySelector('[data-playlist-group-pick-count-value]'); var rulesField = row.querySelector('[data-playlist-group-schedule-rules]'); var rules = []; try { rules = JSON.parse(rulesField && rulesField.value || '[]'); } catch (_error) { rules = []; } groupValues.set(groupKey, { groupKey: groupKey, groupName: String(nameField && nameField.value || 'Group'), groupPickCount: Number(pickField && pickField.value) || 1, groupShuffle: String((row.querySelector('[data-playlist-group-shuffle-value]') || {}).value || '0') === '1', groupMode: String((row.querySelector('[data-playlist-group-mode-value]') || {}).value || 'select'), groupRepeatCount: Number((row.querySelector('[data-playlist-group-repeat-count-value]') || {}).value) || 1, groupScheduleRules: Array.isArray(rules) ? rules : [] }); }); rows.forEach(function (row) { var keyField = row.querySelector('[data-playlist-group-key]'); var groupKey = String(keyField && keyField.value || ''); if (!groupKey) { row.classList.remove('playlist-group-child'); orderedNodes.push(row); return; } if (renderedKeys.has(groupKey)) { return; } renderedKeys.add(groupKey); orderedNodes.push(createGroupHeader(groupValues.get(groupKey))); rows.filter(function (member) { var memberKey = member.querySelector('[data-playlist-group-key]'); return String(memberKey && memberKey.value || '') === groupKey; }).forEach(function (member) { member.classList.add('playlist-group-child'); orderedNodes.push(member); }); }); var emptyRow = target && target.querySelector('.playlist-empty-row'); if (emptyRow) { orderedNodes.push(emptyRow); } if (target) { var groupBlockFragment = document.createDocumentFragment(); orderedNodes.forEach(function (node) { groupBlockFragment.appendChild(node); }); target.replaceChildren(groupBlockFragment); } getGroupHeaders().forEach(syncGroupRowMetadata); updateGroupScheduleWarnings(); } function getTopLevelPlaylistBlocks() { var currentTbody = document.getElementById('playlist-items-body') || tbody; var children = currentTbody ? Array.prototype.slice.call(currentTbody.children) : []; var blocks = []; for (var index = 0; index < children.length; index += 1) { var node = children[index]; if (node.classList.contains('playlist-empty-row')) { continue; } if (!node.matches('[data-playlist-group-row]')) { blocks.push({ key: String(node.getAttribute('data-group-key') || ''), nodes: [node] }); continue; } var groupKey = String(node.getAttribute('data-group-key') || ''); var groupNodes = [node]; var nextIndex = index + 1; while (nextIndex < children.length && children[nextIndex].matches('[data-playlist-slide-row], [data-playlist-group-empty]') && String(children[nextIndex].getAttribute('data-group-key') || '') === groupKey) { groupNodes.push(children[nextIndex]); nextIndex += 1; } blocks.push({ key: groupKey, nodes: groupNodes }); index = nextIndex - 1; } return blocks; } function moveTopLevelBlock(blockNode, offset) { var currentTbody = document.getElementById('playlist-items-body') || tbody; var blocks = getTopLevelPlaylistBlocks(); var currentIndex = blocks.findIndex(function (block) { return block.nodes[0] === blockNode; }); var targetIndex = currentIndex + Number(offset || 0); if (!currentTbody || currentIndex < 0 || targetIndex < 0 || targetIndex >= blocks.length) { return false; } var movingNodes = blocks[currentIndex].nodes; var referenceNode = offset < 0 ? blocks[targetIndex].nodes[0] : blocks[targetIndex].nodes[blocks[targetIndex].nodes.length - 1].nextSibling; var movingBlock = document.createDocumentFragment(); movingNodes.forEach(function (node) { movingBlock.appendChild(node); }); if (referenceNode) currentTbody.insertBefore(movingBlock, referenceNode); else currentTbody.appendChild(movingBlock); updateRowOrder(true); return true; } function moveGroupBlock(header, offset) { return moveTopLevelBlock(header, offset); } function moveGroupBlockRelative(header, targetBlock, placeAfter, markDirty) { var currentTbody = document.getElementById('playlist-items-body') || tbody; var blocks = getTopLevelPlaylistBlocks(); var sourceBlock = blocks.find(function (block) { return block.nodes[0] === header; }); if (!currentTbody || !sourceBlock || !targetBlock || sourceBlock === targetBlock) { return false; } var sourceNextSibling = sourceBlock.nodes[sourceBlock.nodes.length - 1].nextSibling; var targetNextSibling = targetBlock.nodes[targetBlock.nodes.length - 1].nextSibling; if ((!placeAfter && sourceNextSibling === targetBlock.nodes[0]) || (placeAfter && targetNextSibling === sourceBlock.nodes[0])) { return false; } var previousRects = new Map(); Array.prototype.forEach.call(currentTbody.children, function (node) { if (node.matches('[data-playlist-group-row], [data-playlist-slide-row]')) { previousRects.set(node, node.getBoundingClientRect()); } }); var referenceNode = placeAfter ? targetNextSibling : targetBlock.nodes[0]; var movingBlock = document.createDocumentFragment(); sourceBlock.nodes.forEach(function (node) { movingBlock.appendChild(node); }); if (referenceNode) currentTbody.insertBefore(movingBlock, referenceNode); else currentTbody.appendChild(movingBlock); updateRowOrder(markDirty !== false); if (typeof window.animatePlaylistRowReorder === 'function') { window.animatePlaylistRowReorder(previousRects, sourceBlock.nodes); } return true; } function bindGroupBlockDrag(targetTbody) { if (!targetTbody || targetTbody.dataset.groupBlockDragBound === 'true') { return; } targetTbody.dataset.groupBlockDragBound = 'true'; var draggedHeader = null; var dragPreview = null; var draggedMembers = []; var originalNodes = []; var placeholderNodes = []; var placeholderHeader = null; var groupDragLayer = null; var originalNextSibling = null; var groupDragMoved = false; var groupDropCompleted = false; var groupDragCleanupTimer = null; var groupPointerState = null; Array.prototype.forEach.call(document.querySelectorAll('.playlist-group-drag-preview'), function (preview) { preview.remove(); }); Array.prototype.forEach.call(document.querySelectorAll('.playlist-group-original-drag-layer'), function (layer) { layer.remove(); }); function createGroupDragPreview(header, point) { var groupKey = String(header.getAttribute('data-group-key') || ''); var preview = document.createElement('div'); var groupRows = getGroupRows(groupKey).slice(0, 3); var heading = document.createElement('div'); var icon = document.createElement('i'); var name = document.createElement('strong'); var memberList = document.createElement('div'); var groupNameInput = header.querySelector('[data-playlist-group-name]'); preview.className = 'playlist-group-drag-preview'; preview.style.setProperty('--playlist-group-accent', header.style.getPropertyValue('--playlist-group-accent')); preview.style.setProperty('--playlist-group-bg', header.style.getPropertyValue('--playlist-group-bg')); preview.setAttribute('aria-hidden', 'true'); preview.style.position = 'fixed'; preview.style.top = '0'; preview.style.left = '0'; preview.style.width = Math.min(460, Math.max(300, window.innerWidth * 0.4)) + 'px'; preview.style.pointerEvents = 'none'; preview.style.zIndex = '1200'; preview.style.transform = 'translate3d(' + Math.max(0, Number(point && point.x || 0) + 14) + 'px, ' + Math.max(0, Number(point && point.y || 0) + 14) + 'px, 0)'; heading.className = 'playlist-group-drag-preview-heading'; icon.className = 'bi bi-collection-fill playlist-group-icon'; icon.setAttribute('aria-hidden', 'true'); name.textContent = String(groupNameInput && groupNameInput.value || 'Group'); heading.appendChild(icon); heading.appendChild(name); memberList.className = 'playlist-group-drag-preview-members'; groupRows.forEach(function (row) { var member = document.createElement('div'); var thumbnail = row.querySelector('[data-playlist-slide-thumb-image]'); var title = row.querySelector('[data-playlist-slide-title]'); member.className = 'playlist-group-drag-preview-member'; if (thumbnail && thumbnail.getAttribute('src')) { var image = document.createElement('img'); image.alt = ''; image.src = thumbnail.getAttribute('src'); member.appendChild(image); } else { var placeholder = document.createElement('span'); placeholder.className = 'playlist-group-drag-preview-placeholder'; placeholder.innerHTML = ''; member.appendChild(placeholder); } var memberTitle = document.createElement('span'); memberTitle.textContent = String(title && title.textContent || 'Slide'); member.appendChild(memberTitle); memberList.appendChild(member); }); if (getGroupRows(groupKey).length > groupRows.length) { var remaining = document.createElement('div'); remaining.className = 'playlist-group-drag-preview-more'; remaining.textContent = '+ ' + String(getGroupRows(groupKey).length - groupRows.length) + ' more'; memberList.appendChild(remaining); } preview.appendChild(heading); preview.appendChild(memberList); document.body.appendChild(preview); return preview; } function cloneGroupPlaceholderNode(node) { var clone = node.cloneNode(true); clone.classList.add('playlist-group-block-placeholder'); clone.setAttribute('aria-hidden', 'true'); clone.style.pointerEvents = 'none'; if (clone.matches('[data-playlist-slide-row]')) { clone.setAttribute('data-playlist-ghost-row', 'true'); } Array.prototype.forEach.call(clone.querySelectorAll('[id]'), function (element) { element.removeAttribute('id'); }); Array.prototype.forEach.call(clone.querySelectorAll('input, select, textarea, button'), function (control) { control.disabled = true; control.removeAttribute('name'); control.removeAttribute('form'); control.setAttribute('tabindex', '-1'); }); return clone; } function createGroupDragLayer(nodes) { var layer = document.createElement('table'); var layerBody = document.createElement('tbody'); layer.className = 'playlist-group-original-drag-layer'; layer.setAttribute('aria-hidden', 'true'); layer.style.position = 'fixed'; layer.style.top = '0'; layer.style.left = '-10000px'; layer.style.width = String(targetTbody.getBoundingClientRect().width) + 'px'; layer.style.pointerEvents = 'none'; layer.style.opacity = '0'; layer.style.tableLayout = 'fixed'; layer.style.zIndex = '-1'; nodes.forEach(function (node) { layerBody.appendChild(node); }); layer.appendChild(layerBody); document.body.appendChild(layer); return layer; } function getGroupDragPoint(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 beginGroupPointerDrag(point) { if (!groupPointerState || groupPointerState.active) { return false; } draggedHeader = groupPointerState.header; var sourceBlock = getTopLevelPlaylistBlocks().find(function (block) { return block.nodes[0] === draggedHeader; }); if (!sourceBlock) { draggedHeader = null; return false; } originalNodes = sourceBlock.nodes.slice(); originalNextSibling = originalNodes[originalNodes.length - 1].nextSibling; dragPreview = createGroupDragPreview(draggedHeader, point); placeholderNodes = originalNodes.map(cloneGroupPlaceholderNode); placeholderHeader = placeholderNodes[0]; var placeholderBlock = document.createDocumentFragment(); placeholderNodes.forEach(function (node) { placeholderBlock.appendChild(node); }); targetTbody.insertBefore(placeholderBlock, draggedHeader); draggedMembers = originalNodes.filter(function (node) { return node.matches('[data-playlist-slide-row]'); }); originalNodes.forEach(function (node) { node.classList.add('playlist-group-block-dragging'); }); groupDragLayer = createGroupDragLayer(originalNodes); groupPointerState.active = true; groupDragMoved = false; groupDropCompleted = false; groupDragCleanupTimer = window.setTimeout(finishGroupDrag, 120000); return true; } function moveGroupPlaceholder(clientY) { var blocks = getTopLevelPlaylistBlocks(); var sourceBlock = blocks.find(function (block) { return block.nodes[0] === placeholderHeader; }); if (!sourceBlock) { return; } var destination = null; var placeAfter = false; for (var index = 0; index < blocks.length; index += 1) { var block = blocks[index]; if (block === sourceBlock) { continue; } var firstRect = block.nodes[0].getBoundingClientRect(); var lastRect = block.nodes[block.nodes.length - 1].getBoundingClientRect(); var midpoint = (firstRect.top + lastRect.bottom) / 2; if (clientY < midpoint) { destination = block; placeAfter = false; break; } destination = block; placeAfter = true; } if (destination && moveGroupBlockRelative(placeholderHeader, destination, placeAfter, false)) { groupDragMoved = true; } } function handleGroupPointerMove(event) { if (!groupPointerState) { return; } var point = getGroupDragPoint(event); if (!groupPointerState.active) { var threshold = 4; if (Math.abs(point.x - groupPointerState.startPoint.x) < threshold && Math.abs(point.y - groupPointerState.startPoint.y) < threshold) { return; } if (!beginGroupPointerDrag(groupPointerState.currentPoint || point)) { return; } } event.preventDefault(); if (dragPreview) { dragPreview.style.transform = 'translate3d(' + Math.max(0, point.x + 14) + 'px, ' + Math.max(0, point.y + 14) + 'px, 0)'; } moveGroupPlaceholder(point.y); } function handleGroupPointerEnd(event, shouldCancel) { if (!groupPointerState) { return; } if (groupPointerState.active) { groupDropCompleted = !shouldCancel && groupDragMoved; finishGroupDrag(); } else { groupPointerState = null; } } function finishGroupDrag() { var hasGroupDrag = Boolean(originalNodes.length || placeholderNodes.length || groupDragLayer); var shouldCommit = groupDropCompleted && groupDragMoved; if (groupDragCleanupTimer !== null) { window.clearTimeout(groupDragCleanupTimer); groupDragCleanupTimer = null; } if (placeholderNodes.length && placeholderHeader && placeholderHeader.parentNode && !shouldCommit) { var resetPlaceholder = document.createDocumentFragment(); placeholderNodes.forEach(function (node) { resetPlaceholder.appendChild(node); }); if (originalNextSibling && originalNextSibling.parentNode === targetTbody) { targetTbody.insertBefore(resetPlaceholder, originalNextSibling); } else { targetTbody.appendChild(resetPlaceholder); } } if (originalNodes.length && placeholderHeader && placeholderHeader.parentNode) { var restoredBlock = document.createDocumentFragment(); originalNodes.forEach(function (node) { node.classList.remove('playlist-group-block-dragging'); restoredBlock.appendChild(node); }); placeholderHeader.parentNode.insertBefore(restoredBlock, placeholderHeader); } placeholderNodes.forEach(function (node) { if (node.parentNode) node.parentNode.removeChild(node); }); if (groupDragLayer && groupDragLayer.parentNode) groupDragLayer.parentNode.removeChild(groupDragLayer); if (draggedHeader) draggedHeader.classList.remove('playlist-group-block-dragging'); draggedMembers.forEach(function (row) { row.classList.remove('playlist-group-block-dragging'); }); draggedMembers = []; Array.prototype.forEach.call(document.querySelectorAll('.playlist-group-drag-preview'), function (preview) { preview.remove(); }); Array.prototype.forEach.call(document.querySelectorAll('.playlist-group-drop-target'), function (target) { target.classList.remove('playlist-group-drop-target'); }); dragPreview = null; draggedHeader = null; originalNodes = []; placeholderNodes = []; placeholderHeader = null; groupDragLayer = null; originalNextSibling = null; groupDragMoved = false; groupDropCompleted = false; groupPointerState = null; if (hasGroupDrag) updateRowOrder(shouldCommit); } function handleGroupPointerStart(event) { var handle = event.target && event.target.closest ? event.target.closest('[data-playlist-group-drag-handle]') : null; var header = handle ? handle.closest('[data-playlist-group-row]') : null; var point = getGroupDragPoint(event); if (!header || groupPointerState || !point) { return; } if (event.type === 'mousedown' && event.button !== 0) { return; } event.preventDefault(); event.stopPropagation(); groupPointerState = { header: header, startPoint: point, currentPoint: point, pointerId: event.pointerId, active: false }; } function handleGroupPointerMove(event) { if (!groupPointerState) { return; } if (groupPointerState.pointerId !== undefined && event.pointerId !== undefined && groupPointerState.pointerId !== event.pointerId) { return; } var point = getGroupDragPoint(event); groupPointerState.currentPoint = point; if (!groupPointerState.active) { var threshold = 4; if (Math.abs(point.x - groupPointerState.startPoint.x) < threshold && Math.abs(point.y - groupPointerState.startPoint.y) < threshold) { return; } if (!beginGroupPointerDrag(point)) { return; } } event.preventDefault(); if (dragPreview) { dragPreview.style.transform = 'translate3d(' + Math.max(0, point.x + 14) + 'px, ' + Math.max(0, point.y + 14) + 'px, 0)'; } moveGroupPlaceholder(point.y); } function handleGroupPointerEnd(event, shouldCancel) { if (!groupPointerState) { return; } if (groupPointerState.pointerId !== undefined && event && event.pointerId !== undefined && groupPointerState.pointerId !== event.pointerId) { return; } if (groupPointerState.active) { groupDropCompleted = !shouldCancel && groupDragMoved; finishGroupDrag(); return; } groupPointerState = null; } if (typeof window !== 'undefined' && window.PointerEvent) { targetTbody.addEventListener('pointerdown', handleGroupPointerStart); document.addEventListener('pointermove', handleGroupPointerMove, { passive: false }); document.addEventListener('pointerup', function (event) { handleGroupPointerEnd(event, false); }); document.addEventListener('pointercancel', function (event) { handleGroupPointerEnd(event, true); }); } else { targetTbody.addEventListener('mousedown', handleGroupPointerStart); document.addEventListener('mousemove', handleGroupPointerMove); document.addEventListener('mouseup', function (event) { handleGroupPointerEnd(event, false); }); targetTbody.addEventListener('touchstart', handleGroupPointerStart, { passive: false }); document.addEventListener('touchmove', handleGroupPointerMove, { passive: false }); document.addEventListener('touchend', function (event) { handleGroupPointerEnd(event, false); }); document.addEventListener('touchcancel', function (event) { handleGroupPointerEnd(event, true); }); } document.addEventListener('keydown', function (event) { if (event.key === 'Escape') handleGroupPointerEnd(event, true); }); document.addEventListener('visibilitychange', function () { if (document.hidden) handleGroupPointerEnd(null, true); }); window.addEventListener('blur', function () { handleGroupPointerEnd(null, true); }); } function removePlaylistGroup(header) { if (!header) { return; } var groupKey = String(header.getAttribute('data-group-key') || ''); getGroupRows(groupKey).forEach(function (row) { var keyField = row.querySelector('[data-playlist-group-key]'); var nameField = row.querySelector('[data-playlist-group-name-value]'); var pickField = row.querySelector('[data-playlist-group-pick-count-value]'); var rulesField = row.querySelector('[data-playlist-group-schedule-rules]'); if (keyField) keyField.value = ''; if (nameField) nameField.value = ''; if (pickField) pickField.value = '1'; var shuffleField = row.querySelector('[data-playlist-group-shuffle-value]'); if (shuffleField) shuffleField.value = '0'; var modeField = row.querySelector('[data-playlist-group-mode-value]'); var repeatField = row.querySelector('[data-playlist-group-repeat-count-value]'); if (modeField) modeField.value = 'select'; if (repeatField) repeatField.value = '1'; if (rulesField) rulesField.value = '[]'; row.setAttribute('data-group-key', ''); row.classList.remove('playlist-group-child'); }); header.remove(); updateRowOrder(true); } function setSlideGroupFields(row, targetGroupKey) { var groupKey = String(targetGroupKey || ''); var keyField = row && row.querySelector('[data-playlist-group-key]'); var nameField = row && row.querySelector('[data-playlist-group-name-value]'); var pickField = row && row.querySelector('[data-playlist-group-pick-count-value]'); var shuffleField = row && row.querySelector('[data-playlist-group-shuffle-value]'); var modeField = row && row.querySelector('[data-playlist-group-mode-value]'); var repeatField = row && row.querySelector('[data-playlist-group-repeat-count-value]'); var scheduleField = row && row.querySelector('[data-playlist-group-schedule-rules]'); var header = groupKey ? getGroupHeader(groupKey) : null; if (groupKey && !header) { return false; } if (keyField) keyField.value = groupKey; row.setAttribute('data-group-key', groupKey); row.classList.toggle('playlist-group-child', Boolean(groupKey)); if (groupKey) { var groupNameInput = header.querySelector('[data-playlist-group-name]'); var groupPickInput = header.querySelector('[data-playlist-group-pick-count]'); var groupScheduleInput = header.querySelector('[data-playlist-group-schedule-rules]'); if (nameField) nameField.value = String(groupNameInput && groupNameInput.value || '').trim() || 'Group'; if (pickField) pickField.value = String(Math.max(1, Number(groupPickInput && groupPickInput.value) || 1)); var groupShuffleButton = header.querySelector('[data-playlist-group-shuffle]'); if (shuffleField) shuffleField.value = groupShuffleButton && groupShuffleButton.getAttribute('aria-pressed') === 'true' ? '1' : '0'; if (modeField) modeField.value = header.getAttribute('data-group-mode') === 'repeat' ? 'repeat' : 'select'; var groupRepeatInput = header.querySelector('[data-playlist-group-repeat-count]'); if (repeatField) repeatField.value = String(Number(groupRepeatInput && groupRepeatInput.value) || 1); if (scheduleField) scheduleField.value = String(groupScheduleInput && groupScheduleInput.value || '[]'); } else { if (nameField) nameField.value = ''; if (pickField) pickField.value = '1'; if (shuffleField) shuffleField.value = '0'; if (modeField) modeField.value = 'select'; if (repeatField) repeatField.value = '1'; if (scheduleField) scheduleField.value = '[]'; } return true; } function updateEmptyState() { var currentTbody = document.getElementById('playlist-items-body') || tbody; var rows = getRows(); var placeholder = currentTbody ? currentTbody.querySelector('.playlist-empty-row') : null; if (rows.length || getGroupHeaders().length) { if (placeholder) { placeholder.remove(); } return; } if (!placeholder) { if (currentTbody) { currentTbody.innerHTML = 'No slides assigned yet.'; } } } function markPlaylistDirty() { if (form && form.dataset) { form.dataset.dirty = 'true'; } } // Rebuild hidden schedule inputs after the async save swaps the playlist tbody. function syncScheduleRowInputs(row) { var fields = getScheduleRowFields(row); var rowKey = String(row && row.getAttribute('data-row-key') || '').trim(); var serializedRules = fields.rulesContainer ? fields.rulesContainer.getAttribute('data-schedule-rules') : ''; var rules = []; if (!fields.rulesContainer || !rowKey) { return; } if (serializedRules) { try { rules = JSON.parse(serializedRules); } catch (_error) { rules = []; } } else { rules = []; } if (!Array.isArray(rules)) { rules = []; } fields.rulesContainer.innerHTML = buildScheduleRulesMarkup(rules, rowKey, 'playlist-edit-form'); if (fields.summaryNode) { fields.summaryNode.innerHTML = buildScheduleSummaryChipMarkup(formatScheduleRulesSummary(rules) || 'Always visible'); } } function syncScheduleDraftRows() { getRows().forEach(function (row) { syncScheduleRowInputs(row); }); } // Filter the slide picker against the current playlist rows and canvas lock. function syncAddSlideOptions() { var activeSlideIds = {}; var activeCanvasIds = {}; var allowedCanvasId = getPlaylistCanvasId(); getRows().forEach(function (row) { var slideId = String(row.getAttribute('data-slide-id') || ''); var canvasId = Number(row.getAttribute('data-canvas-id') || 0) || null; if (slideId) { activeSlideIds[slideId] = true; } if (!allowedCanvasId && canvasId) { activeCanvasIds[canvasId] = true; } }); if (!allowedCanvasId) { slidePickerCards.forEach(function (card) { if (slidePickerSelection.has(String(card.getAttribute('data-slide-id') || ''))) { var selectedCanvasId = Number(card.getAttribute('data-canvas-id') || 0) || null; if (selectedCanvasId) { activeCanvasIds[selectedCanvasId] = true; } } }); var canvasKeys = Object.keys(activeCanvasIds); if (canvasKeys.length === 1) { allowedCanvasId = Number(canvasKeys[0]) || null; } } slidePickerSlides.forEach(function (slide) { var slideId = String(slide.id || ''); var card = slidePickerCards.find(function (item) { return String(item.getAttribute('data-slide-id') || '') === slideId; }); var canvasId = Number(slide.canvasSizeId || 0) || null; var isAssigned = Boolean(activeSlideIds[slideId]); var isCanvasMismatch = Boolean(allowedCanvasId && canvasId && canvasId !== allowedCanvasId); slide.isAssigned = isAssigned; updateSlidePickerCardState(card, isAssigned, isCanvasMismatch); if (isAssigned) { slidePickerSelection.delete(slideId); if (card) { setCardSelected(card, false); } } }); syncSlidePickerState(); } function updatePlaylistGroupEmptyRows() { var currentTbody = document.getElementById('playlist-items-body') || tbody; if (!currentTbody) return; Array.prototype.forEach.call(currentTbody.querySelectorAll('[data-playlist-group-empty]'), function (row) { row.remove(); }); getGroupHeaders().forEach(function (header) { var groupKey = String(header.getAttribute('data-group-key') || ''); if (getGroupRows(groupKey).length) return; var emptyRow = document.createElement('tr'); emptyRow.className = 'playlist-group-child playlist-group-empty'; emptyRow.setAttribute('data-playlist-group-empty', ''); emptyRow.setAttribute('data-group-key', groupKey); emptyRow.innerHTML = ''; header.after(emptyRow); }); } function updatePlaylistGroupColours(rows, blocks) { var palette = ['success', 'warning', 'info', 'danger']; var usedColours = new Set(); blocks.forEach(function (block) { var colour = block.nodes[0].getAttribute('data-group-colour'); if (palette.indexOf(colour) !== -1) usedColours.add(colour); }); rows.forEach(function (row) { row.style.removeProperty('--playlist-group-accent'); row.style.removeProperty('--playlist-group-bg'); }); var groupIndex = 0; blocks.forEach(function (block) { var header = block.nodes[0]; if (!header.matches('[data-playlist-group-row]')) return; var colour = header.getAttribute('data-group-colour'); if (palette.indexOf(colour) === -1) { colour = palette.find(function (candidate) { return !usedColours.has(candidate); }) || palette[groupIndex % palette.length]; header.setAttribute('data-group-colour', colour); } usedColours.add(colour); groupIndex += 1; block.nodes.forEach(function (row) { row.style.setProperty('--playlist-group-accent', 'var(--bs-' + colour + ')'); row.style.setProperty('--playlist-group-bg', 'var(--bs-' + colour + '-bg-subtle)'); }); }); } // Recompute row numbers and derived picker state after drag/drop or adds. function updateRowOrder(markDirty) { var rows = getRows(); updatePlaylistGroupEmptyRows(); getGroupHeaders().forEach(syncGroupRowMetadata); var blocks = getTopLevelPlaylistBlocks(); updatePlaylistGroupColours(rows, blocks); rows.forEach(function (row) { var rowGroupKey = String((row.querySelector('[data-playlist-group-key]') || {}).value || ''); var siblingRows = rows.filter(function (candidate) { return String((candidate.querySelector('[data-playlist-group-key]') || {}).value || '') === rowGroupKey; }); var siblingIndex = siblingRows.indexOf(row); var blockIndex = blocks.findIndex(function (block) { return rowGroupKey ? block.key === rowGroupKey : block.nodes[0] === row; }); var orderNumber = row.querySelector('.playlist-order-number'); var moveUpButton = row.querySelector('[data-playlist-row-move="up"]'); var moveDownButton = row.querySelector('[data-playlist-row-move="down"]'); var moveUpIcon = row.querySelector('[data-playlist-row-move-icon="up"]'); var moveDownIcon = row.querySelector('[data-playlist-row-move-icon="down"]'); var isFirst = rowGroupKey ? siblingIndex === 0 : blockIndex === 0; var isLast = rowGroupKey ? siblingIndex === siblingRows.length - 1 : blockIndex === blocks.length - 1; if (orderNumber) { orderNumber.textContent = String(blockIndex + 1) + (rowGroupKey ? '.' + String(siblingIndex + 1) : ''); } if (moveUpButton) { moveUpButton.disabled = isFirst; moveUpButton.setAttribute('aria-disabled', isFirst ? 'true' : 'false'); } if (moveUpIcon) { moveUpIcon.classList.toggle('bi-caret-up-fill', !isFirst); moveUpIcon.classList.toggle('bi-caret-up', isFirst); } if (moveDownButton) { moveDownButton.disabled = isLast; moveDownButton.setAttribute('aria-disabled', isLast ? 'true' : 'false'); } if (moveDownIcon) { moveDownIcon.classList.toggle('bi-caret-down-fill', !isLast); moveDownIcon.classList.toggle('bi-caret-down', isLast); } }); blocks.forEach(function (block, index) { var header = block.nodes[0]; if (!header || !header.matches('[data-playlist-group-row]')) { return; } var orderNumber = header.querySelector('[data-playlist-group-order]'); if (orderNumber) orderNumber.textContent = String(index + 1); var moveUpButton = header.querySelector('[data-playlist-group-move="up"]'); var moveDownButton = header.querySelector('[data-playlist-group-move="down"]'); var moveUpIcon = header.querySelector('[data-playlist-group-move-icon="up"]'); var moveDownIcon = header.querySelector('[data-playlist-group-move-icon="down"]'); if (moveUpButton) moveUpButton.disabled = index === 0; if (moveDownButton) moveDownButton.disabled = index === blocks.length - 1; if (moveUpIcon) { moveUpIcon.classList.toggle('bi-caret-up-fill', index !== 0); moveUpIcon.classList.toggle('bi-caret-up', index === 0); } if (moveDownIcon) { moveDownIcon.classList.toggle('bi-caret-down-fill', index !== blocks.length - 1); moveDownIcon.classList.toggle('bi-caret-down', index === blocks.length - 1); } }); syncAddSlideOptions(); updateGroupScheduleWarnings(); syncPlaylistCanvasLock(); syncAddSlideButtonState(); updateEmptyState(); if (markDirty) { markPlaylistDirty(); } } form.addEventListener('submit', function () { getGroupHeaders().forEach(syncGroupRowMetadata); syncScheduleDraftRows(); }, true); function bindPlaylistTableDrag(tbodyElement) { if (!tbodyElement || typeof window.initPlaylistTableDrag !== 'function') { return; } window.initPlaylistTableDrag({ tbody: tbodyElement, onOrderChanged: updateRowOrder, onRowDrop: function (row, targetGroupKey) { if (setSlideGroupFields(row, targetGroupKey)) { var groupHeader = targetGroupKey ? getGroupHeader(targetGroupKey) : null; if (groupHeader) { var previousMember = groupHeader; getGroupRows(targetGroupKey).forEach(function (member) { previousMember.after(member); previousMember = member; }); } markPlaylistDirty(); } } }); } function movePlaylistRowByOffset(row, offset) { var currentTbody = document.getElementById('playlist-items-body') || tbody; var groupKey = String((row && row.querySelector('[data-playlist-group-key]') || {}).value || ''); if (!groupKey) { return moveTopLevelBlock(row, offset); } var rows = getRows().filter(function (candidate) { return String((candidate.querySelector('[data-playlist-group-key]') || {}).value || '') === groupKey; }); var currentIndex = rows.indexOf(row); var targetIndex; var referenceRow; if (!currentTbody || !row || currentIndex === -1) { return false; } targetIndex = currentIndex + Number(offset || 0); if (targetIndex < 0 || targetIndex >= rows.length) { return false; } referenceRow = rows[targetIndex]; if (targetIndex > currentIndex) { referenceRow = referenceRow ? referenceRow.nextSibling : null; } if (referenceRow) { currentTbody.insertBefore(row, referenceRow); } else { currentTbody.appendChild(row); } updateRowOrder(true); return true; } function populatePlaylistSlideRow(row, values, rowKey) { var canvasWidth = Number(values.canvas_width); var canvasHeight = Number(values.canvas_height); var thumbnailPath = String(values.thumbnail_path || ''); var scheduleRules = Array.isArray(values.scheduleRules) ? values.scheduleRules : []; var useVideoDuration = Boolean(values.useVideoDuration); var showVideoDurationButton = Boolean(values.showVideoDurationButton); var disableAudio = values.disableAudio === undefined || values.disableAudio === null ? true : Boolean(values.disableAudio); var showMuteButton = Boolean(values.showMuteButton); var durationSource = values.durationSeconds !== undefined && values.durationSeconds !== null ? values.durationSeconds : values.duration_seconds; var durationSeconds = String(durationSource !== undefined && durationSource !== null ? durationSource : 10); var summary = String(values.summary || formatScheduleRulesSummary(scheduleRules) || 'Always visible'); var durationButton = row ? row.querySelector('[data-use-video-duration-button]') : null; var muteButton = row ? row.querySelector('[data-disable-audio-button]') : null; var durationInput = row ? row.querySelector('[data-playlist-duration-input]') : null; var useVideoDurationInput = row ? row.querySelector('[data-playlist-use-video-duration-input]') : null; var disableAudioInput = row ? row.querySelector('[data-playlist-disable-audio-input]') : null; var durationMirror = row ? row.querySelector('[data-playlist-duration-mirror]') : null; var thumb = row ? row.querySelector('[data-playlist-slide-thumb]') : null; var titleNode = row ? row.querySelector('[data-playlist-slide-title]') : null; var slideIdInput = row ? row.querySelector('[data-playlist-slide-id-input]') : null; var rowKeyInput = row ? row.querySelector('[data-playlist-row-key-input]') : null; var groupKeyInput = row ? row.querySelector('[data-playlist-group-key]') : null; var groupNameInput = row ? row.querySelector('[data-playlist-group-name-value]') : null; var groupPickCountInput = row ? row.querySelector('[data-playlist-group-pick-count-value]') : null; var groupShuffleInput = row ? row.querySelector('[data-playlist-group-shuffle-value]') : null; var groupModeInput = row ? row.querySelector('[data-playlist-group-mode-value]') : null; var groupRepeatInput = row ? row.querySelector('[data-playlist-group-repeat-count-value]') : null; var groupScheduleInput = row ? row.querySelector('[data-playlist-group-schedule-rules]') : null; var groupKey = String(values.groupKey || ''); var groupScheduleRules = Array.isArray(values.groupScheduleRules) ? values.groupScheduleRules : []; var scheduleSummaryChip = row ? row.querySelector('[data-schedule-summary-chip]') : null; var rulesContainer = row ? row.querySelector('[data-schedule-rules-container]') : null; if (!row) { return; } row.setAttribute('data-row-key', rowKey); row.setAttribute('data-slide-id', String(values.slide_id || '')); row.setAttribute('data-group-key', groupKey); row.setAttribute('data-canvas-id', String(values.canvas_id || '')); row.setAttribute('data-video-source-path', String(values.videoSourcePath || '')); row.setAttribute('data-video-duration-seconds', String(values.videoDurationSeconds || '')); row.setAttribute('data-use-video-duration-state', useVideoDuration ? 'true' : 'false'); row.setAttribute('data-disable-audio-state', disableAudio ? 'true' : 'false'); if (Number.isFinite(canvasWidth) && Number.isFinite(canvasHeight) && canvasWidth > 0 && canvasHeight > 0 && thumb) { thumb.style.setProperty('--playlist-slide-thumb-aspect-ratio', canvasWidth + ' / ' + canvasHeight); } if (thumb) { thumb.innerHTML = thumbnailPath ? '' : ''; } if (titleNode) { titleNode.textContent = String(values.title || 'Slide'); } if (slideIdInput) { slideIdInput.value = String(values.slide_id || ''); } if (rowKeyInput) { rowKeyInput.value = String(rowKey || ''); } if (groupKeyInput) { groupKeyInput.value = groupKey; } if (groupNameInput) { groupNameInput.value = String(values.groupName || ''); } if (groupPickCountInput) { groupPickCountInput.value = String(Math.max(1, Number(values.groupPickCount) || 1)); } if (groupShuffleInput) { groupShuffleInput.value = values.groupShuffle ? '1' : '0'; } if (groupModeInput) groupModeInput.value = values.groupMode === 'repeat' ? 'repeat' : 'select'; if (groupRepeatInput) groupRepeatInput.value = String(Math.min(100, Math.max(1, Number(values.groupRepeatCount) || 1))); if (groupScheduleInput) { groupScheduleInput.value = JSON.stringify(groupScheduleRules); } if (scheduleSummaryChip) { scheduleSummaryChip.textContent = summary; } if (rulesContainer) { rulesContainer.setAttribute('data-schedule-rules', JSON.stringify(scheduleRules).replace(/ 0) { input.value = String(storedDuration); } input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); } function setDisableAudioToggleState(row, button, isPressed) { var flag = row ? row.querySelector('[name="disable_audio[]"]') : null; syncDisableAudioButtonState(button, isPressed); if (flag) { flag.value = isPressed ? '1' : '0'; } if (row) { row.setAttribute('data-disable-audio-state', isPressed ? 'true' : 'false'); } } function applyDisableAudioToRow(row, button) { if (!row) { return; } setDisableAudioToggleState(row, button, !(button && button.getAttribute('aria-pressed') === 'true')); markPlaylistDirty(); } // Central click handler for row actions so drag, remove, schedule, and duration toggle stay coordinated. document.addEventListener('click', function (event) { if (event.target && event.target.matches('[name="duration_seconds[]"]')) { event.stopPropagation(); return; } if (createGroupButton && event.target.closest('#playlist-create-group')) { event.preventDefault(); var nextGroupKey = 'group-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8); var emptyPlaylistRow = tbody.querySelector('.playlist-empty-row'); if (emptyPlaylistRow) emptyPlaylistRow.remove(); var newGroupHeader = createGroupHeader({ groupKey: nextGroupKey, groupName: 'New group', groupPickCount: 1, groupScheduleRules: [] }); tbody.appendChild(newGroupHeader); updateRowOrder(true); var newGroupName = newGroupHeader.querySelector('[data-playlist-group-name]'); if (newGroupName) { newGroupName.focus(); newGroupName.select(); } return; } var groupHeader = event.target.closest('tr[data-playlist-group-row]'); var groupModeButton = event.target.closest('[data-playlist-group-mode]'); if (groupHeader && groupModeButton) { event.preventDefault(); groupHeader.setAttribute('data-group-mode', groupModeButton.getAttribute('data-playlist-group-mode') === 'repeat' ? 'repeat' : 'select'); syncGroupRowMetadata(groupHeader); markPlaylistDirty(); return; } var groupShuffleButton = event.target.closest('[data-playlist-group-shuffle]'); if (groupHeader && groupShuffleButton) { event.preventDefault(); var shuffleEnabled = groupShuffleButton.getAttribute('aria-pressed') !== 'true'; groupShuffleButton.setAttribute('aria-pressed', shuffleEnabled ? 'true' : 'false'); groupShuffleButton.setAttribute('aria-label', shuffleEnabled ? 'Disable shuffle' : 'Enable shuffle'); groupShuffleButton.title = shuffleEnabled ? 'Disable shuffle' : 'Enable shuffle'; groupShuffleButton.classList.toggle('btn-secondary', shuffleEnabled); groupShuffleButton.classList.toggle('btn-outline-secondary', !shuffleEnabled); groupShuffleButton.querySelector('i').className = shuffleEnabled ? 'bi bi-shuffle' : 'bi bi-chevron-double-right'; syncGroupRowMetadata(groupHeader); markPlaylistDirty(); return; } var groupMoveButton = event.target.closest('[data-playlist-group-move]'); var groupRemoveButton = event.target.closest('[data-playlist-group-remove]'); var groupScheduleButton = event.target.closest('[data-group-schedule-config]'); if (groupHeader && groupMoveButton) { event.preventDefault(); moveGroupBlock(groupHeader, groupMoveButton.getAttribute('data-playlist-group-move') === 'up' ? -1 : 1); return; } if (groupHeader && groupRemoveButton) { event.preventDefault(); removePlaylistGroup(groupHeader); return; } if (groupHeader && groupScheduleButton && !groupScheduleButton.disabled) { event.preventDefault(); if (typeof window.openScheduleModal === 'function') { var groupKey = String(groupHeader.getAttribute('data-group-key') || ''); var groupScheduleParams = collectScheduleParams(groupHeader, 'group:' + groupKey, true); window.openScheduleModal(groupScheduleButton.getAttribute('data-group-schedule-config') + '?' + groupScheduleParams.toString()); } return; } var moveButton = event.target.closest('[data-playlist-row-move]'); var removeButton = event.target.closest('[data-playlist-remove-row]'); var durationButton = event.target.closest('.playlist-use-video-duration'); var muteButton = event.target.closest('.playlist-disable-audio'); var row = getPlaylistSlideRow(event.target); if (!row) { return; } if (moveButton) { event.preventDefault(); event.stopPropagation(); movePlaylistRowByOffset(row, moveButton.getAttribute('data-playlist-row-move') === 'up' ? -1 : 1); return; } if (removeButton) { event.preventDefault(); row.remove(); updateRowOrder(true); return; } if (durationButton) { var rowKey = getPlaylistSlideRowKey(row); var pressedOnInput = lastDurationPointerDown && lastDurationPointerDown.type === 'input' && lastDurationPointerDown.rowKey === rowKey; var releasedOnButton = lastDurationPointerUp && lastDurationPointerUp.type === 'button' && lastDurationPointerUp.rowKey === rowKey; if (event.detail > 0 && (pressedOnInput || !releasedOnButton)) { lastDurationPointerDown = null; lastDurationPointerUp = null; event.preventDefault(); return; } event.preventDefault(); void applyVideoDurationToRow(row, durationButton); lastDurationPointerDown = null; lastDurationPointerUp = null; return; } if (muteButton) { event.preventDefault(); void applyDisableAudioToRow(row, muteButton); return; } var scheduleButton = event.target.closest('[data-schedule-config]'); if (scheduleButton) { event.preventDefault(); if (typeof window.openScheduleModal === 'function') { var scheduleParams = collectScheduleParams(row, scheduleButton.getAttribute('data-schedule-config-row') || row.getAttribute('data-row-key') || ''); window.openScheduleModal(scheduleButton.getAttribute('data-schedule-config') + '?' + scheduleParams.toString()); } } lastDurationPointerDown = null; lastDurationPointerUp = null; }); // Track the pointer source so clicks on the duration toggle do not fight text selection. document.addEventListener('mousedown', function (event) { var row = getPlaylistSlideRow(event.target); var dragHandle = event.target && event.target.closest ? event.target.closest('[data-playlist-drag-handle]') : null; var durationInput = event.target && event.target.matches('[name="duration_seconds[]"]') ? event.target : null; var durationToggle = isDurationControl(event.target) ? event.target.closest('.playlist-use-video-duration') : null; var muteToggle = isAudioControl(event.target) ? event.target.closest('.playlist-disable-audio') : null; if (row && !dragHandle) { event.stopPropagation(); } if (durationInput) { lastDurationPointerDown = getDurationPointerState(event.target, 'input'); event.stopPropagation(); return; } if (durationToggle) { lastDurationPointerDown = getDurationPointerState(durationToggle, 'button'); return; } if (muteToggle) { event.stopPropagation(); return; } lastDurationPointerDown = null; }); // Mirror the pointer-up side of the duration toggle click detection. document.addEventListener('mouseup', function (event) { var durationInput = event.target && event.target.matches('[name="duration_seconds[]"]') ? event.target : null; var durationToggle = isDurationControl(event.target) ? event.target.closest('.playlist-use-video-duration') : null; var muteToggle = isAudioControl(event.target) ? event.target.closest('.playlist-disable-audio') : null; var row = getPlaylistSlideRow(event.target); if (durationInput) { lastDurationPointerUp = getDurationPointerState(row, 'input'); return; } if (durationToggle) { lastDurationPointerUp = getDurationPointerState(row, 'button'); return; } if (muteToggle) { lastDurationPointerUp = null; return; } lastDurationPointerUp = null; }); // Keep the row dirty state and mirrored duration input in sync while the user edits. document.addEventListener('input', function (event) { if (event.target && event.target.matches('[name="duration_seconds[]"]')) { var editedRow = getPlaylistSlideRow(event.target); var editedButton = editedRow ? editedRow.querySelector('.playlist-use-video-duration') : null; var editedMuteButton = editedRow ? editedRow.querySelector('.playlist-disable-audio') : null; var editedMirror = editedRow ? editedRow.querySelector('[data-video-duration-mirror]') : null; var useVideoDuration = Boolean(editedButton && editedButton.getAttribute('aria-pressed') === 'true'); var disableAudio = Boolean(editedMuteButton && editedMuteButton.getAttribute('aria-pressed') === 'true'); if (editedRow) { editedRow.setAttribute('data-use-video-duration-state', useVideoDuration ? 'true' : 'false'); editedRow.setAttribute('data-disable-audio-state', disableAudio ? 'true' : 'false'); } if (useVideoDuration && editedMirror) { editedMirror.value = event.target.value; } if (editedMirror && !useVideoDuration) { editedMirror.parentNode.removeChild(editedMirror); } markPlaylistDirty(); return; } if (event.target && event.target.matches('[data-playlist-group-name]')) { var editedGroupHeader = event.target.closest('[data-playlist-group-row]'); if (!editedGroupHeader) { return; } syncGroupRowMetadata(editedGroupHeader); markPlaylistDirty(); return; } if (event.target && event.target.matches('[data-playlist-group-pick-count], [data-playlist-group-repeat-count]')) { var quotaGroupHeader = event.target.closest('[data-playlist-group-row]'); if (quotaGroupHeader) { syncGroupRowMetadata(quotaGroupHeader); markPlaylistDirty(); } } }); document.addEventListener('change', function (event) { if (!event.target) { return; } if (event.target.matches('[data-playlist-group-name]')) { var groupNameHeader = event.target.closest('[data-playlist-group-row]'); if (groupNameHeader) { if (!String(event.target.value || '').trim()) event.target.value = 'Group'; syncGroupRowMetadata(groupNameHeader); markPlaylistDirty(); } return; } if (event.target.matches('[data-playlist-group-pick-count], [data-playlist-group-repeat-count]')) { var groupQuotaHeader = event.target.closest('[data-playlist-group-row]'); if (groupQuotaHeader) { event.target.value = String(Math.max(1, Math.floor(Number(event.target.value) || 1))); syncGroupRowMetadata(groupQuotaHeader); markPlaylistDirty(); } return; } }); initializePlaylistGroups(tbody); bindGroupBlockDrag(tbody); bindPlaylistTableDrag(tbody); hydrateScheduleRowContainers(tbody); initPlaylistScheduleModal(); 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 (addSlideShowAssigned) { addSlideShowAssigned.addEventListener('click', function () { var nextState = addSlideShowAssigned.getAttribute('aria-pressed') !== 'true'; setShowAssignedState(nextState); syncSlidePickerState(); }); } if (addSlideConfirm) { addSlideConfirm.addEventListener('click', function () { addSelectedSlides(); }); } if (canvasSizeSelect) { canvasSizeSelect.addEventListener('change', function () { syncPlaylistCanvasId(); }); } addSlideModal.addEventListener('shown.bs.modal', function () { syncSlidePickerState(); if (addSlideSearch) { addSlideSearch.focus(); } }); addSlideModal.addEventListener('hidden.bs.modal', function () { clearModalSelection(); }); } window.rebindPlaylistTableDrag = function (tbodyElement) { initializePlaylistGroups(tbodyElement); bindGroupBlockDrag(tbodyElement); bindPlaylistTableDrag(tbodyElement); hydrateScheduleRowContainers(tbodyElement); updateRowOrder(); }; updateRowOrder(); syncPlaylistCanvasLock(); syncAddSlideButtonState(); } window.initPlaylistScheduleForm = initPlaylistScheduleForm; initPlaylistScheduleForm(); initPlaylistEditStaging(); }());