This PR breaks the large web and player bootstrap files into smaller modules with clearer ownership.
Web changes: Split shared helpers, bootstrap logic, route groups, and upload-sync behavior out of web.js. Kept web.js focused on wiring and server startup. Fixed screen playlist reassignment so changing a screen’s playlist now triggers a refresh. Fixed single-slide playlist refresh behavior so updates do not get stuck behind the current slide. Player changes: Split websocket/runtime handling into runtime.js. Split playlist assembly and revision hashing into playlist.js. Split onboarding and player HTTP routes into dedicated modules. Split render utilities and template loading into render-helpers.js. Kept player.js mostly as startup/orchestration. Validation: Rebuilt both services with Docker Compose. Smoke-checked web and player routes after the refactor. Verified get_errors was clean on the touched modules.
This commit is contained in:
@@ -0,0 +1,657 @@
|
||||
(function () {
|
||||
var DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
|
||||
function formatDays(daysValue) {
|
||||
var days = [];
|
||||
if (Array.isArray(daysValue)) {
|
||||
days = daysValue;
|
||||
} else if (daysValue) {
|
||||
try {
|
||||
var parsedDays = JSON.parse(daysValue);
|
||||
days = Array.isArray(parsedDays) ? parsedDays : [];
|
||||
} catch (_error) {
|
||||
days = [];
|
||||
}
|
||||
}
|
||||
return days
|
||||
.map(function (day) {
|
||||
return DAY_NAMES[Number(day)] || '';
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
function initPlaylistScheduleModal() {
|
||||
var dialog = document.getElementById('slide-schedule-dialog');
|
||||
var content = document.getElementById('slide-schedule-content');
|
||||
var triggers = document.querySelectorAll('[data-schedule-config]');
|
||||
|
||||
if (!dialog || !content || !triggers.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
function initInjectedContent() {
|
||||
if (typeof window.initPlaylistScheduleForm === 'function') {
|
||||
window.initPlaylistScheduleForm(content);
|
||||
}
|
||||
}
|
||||
|
||||
function collectScheduleParams(row, rowKey) {
|
||||
var params = new URLSearchParams();
|
||||
var scheduleFields = [
|
||||
'schedule_mode[]',
|
||||
'schedule_start_datetime[]',
|
||||
'schedule_end_datetime[]',
|
||||
'schedule_start_time[]',
|
||||
'schedule_end_time[]',
|
||||
'schedule_days_json[]'
|
||||
];
|
||||
|
||||
params.set('row_key', String(rowKey || ''));
|
||||
scheduleFields.forEach(function (fieldName) {
|
||||
var field = row && row.querySelector ? row.querySelector('[name="' + fieldName + '"]') : null;
|
||||
var value = field && typeof field.value !== 'undefined' ? String(field.value || '') : '';
|
||||
if (value) {
|
||||
params.set(fieldName.replace(/\[\]$/, ''), value);
|
||||
}
|
||||
});
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
function openScheduleModal(url) {
|
||||
content.innerHTML = '<div class="card card-outline card-secondary mb-0"><div class="card-body py-4 text-center text-secondary">Loading schedule...</div></div>';
|
||||
if (typeof dialog.showModal === 'function') {
|
||||
dialog.showModal();
|
||||
} else {
|
||||
dialog.setAttribute('open', 'open');
|
||||
}
|
||||
|
||||
fetch(url, {
|
||||
credentials: 'same-origin'
|
||||
}).then(function (response) {
|
||||
if (!response.ok) {
|
||||
return response.text().then(function (text) {
|
||||
throw new Error(text || 'Unable to open schedule editor.');
|
||||
});
|
||||
}
|
||||
return response.text();
|
||||
}).then(function (html) {
|
||||
content.innerHTML = html;
|
||||
initInjectedContent();
|
||||
}).catch(function (error) {
|
||||
content.innerHTML = '<div class="card card-outline card-danger mb-0"><div class="card-body text-danger">' + String(error && error.message ? error.message : 'Unable to open schedule editor.') + '</div></div>';
|
||||
});
|
||||
}
|
||||
|
||||
function closeScheduleModal() {
|
||||
content.innerHTML = '';
|
||||
if (typeof dialog.close === 'function') {
|
||||
dialog.close();
|
||||
} else {
|
||||
dialog.removeAttribute('open');
|
||||
}
|
||||
}
|
||||
|
||||
window.openScheduleModal = openScheduleModal;
|
||||
window.closeScheduleModal = closeScheduleModal;
|
||||
|
||||
Array.prototype.forEach.call(triggers, function (trigger) {
|
||||
trigger.addEventListener('click', function () {
|
||||
var url = trigger.getAttribute('data-schedule-config');
|
||||
var rowKey = trigger.getAttribute('data-schedule-config-row') || '';
|
||||
var row = trigger.closest('tr[data-playlist-slide-row]');
|
||||
if (row) {
|
||||
var params = collectScheduleParams(row, rowKey || row.getAttribute('data-row-key') || '');
|
||||
url += (url.indexOf('?') === -1 ? '?' : '&') + params.toString();
|
||||
} else if (rowKey && url.indexOf('row_key=') === -1) {
|
||||
url += (url.indexOf('?') === -1 ? '?' : '&') + 'row_key=' + encodeURIComponent(rowKey);
|
||||
}
|
||||
openScheduleModal(url);
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function scheduleSummary(values) {
|
||||
var mode = String(values.schedule_mode || 'always');
|
||||
if (mode === 'dates') {
|
||||
if (values.schedule_start_datetime && values.schedule_end_datetime) {
|
||||
return 'Dates: ' + values.schedule_start_datetime.replace('T', ' ') + ' to ' + values.schedule_end_datetime.replace('T', ' ');
|
||||
}
|
||||
return 'Dates: not set';
|
||||
}
|
||||
if (mode === 'times') {
|
||||
var days = formatDays(values.schedule_days_json);
|
||||
if (days && values.schedule_start_time && values.schedule_end_time) {
|
||||
return 'Times: ' + days + ' ' + values.schedule_start_time.slice(0, 5) + '-' + values.schedule_end_time.slice(0, 5);
|
||||
}
|
||||
return 'Times: not set';
|
||||
}
|
||||
return 'Always visible';
|
||||
}
|
||||
|
||||
function initPlaylistScheduleForm(root) {
|
||||
var scope = root || document;
|
||||
var form = scope.querySelector('form[action*="/config"]');
|
||||
var select = scope.querySelector('#schedule-mode-select');
|
||||
if (!form || !select) {
|
||||
return;
|
||||
}
|
||||
|
||||
var DEFAULT_START_TIME = '00:00';
|
||||
var DEFAULT_END_TIME = '23:59';
|
||||
|
||||
var datesPanel = scope.querySelector('#schedule-dates-panel');
|
||||
var timesPanel = scope.querySelector('#schedule-times-panel');
|
||||
var startDateInput = form.querySelector('[name="schedule_start_datetime"]');
|
||||
var endDateInput = form.querySelector('[name="schedule_end_datetime"]');
|
||||
var startTimeInput = form.querySelector('[name="schedule_start_time"]');
|
||||
var endTimeInput = form.querySelector('[name="schedule_end_time"]');
|
||||
var rowKeyInput = form.querySelector('[name="row_key"]');
|
||||
var dayCheckboxes = form.querySelectorAll('[name="schedule_days"]');
|
||||
|
||||
function clearScheduleValidity() {
|
||||
[startDateInput, endDateInput, startTimeInput, endTimeInput].forEach(function (input) {
|
||||
if (input) {
|
||||
input.setCustomValidity('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setScheduleError(input, message) {
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
input.setCustomValidity(message);
|
||||
}
|
||||
|
||||
function validateScheduleForm() {
|
||||
var mode = select.value;
|
||||
var firstDayCheckbox = dayCheckboxes.length ? dayCheckboxes[0] : null;
|
||||
|
||||
clearScheduleValidity();
|
||||
|
||||
if (mode === 'dates') {
|
||||
if (!startDateInput.value) {
|
||||
setScheduleError(startDateInput, 'Start datetime is required for this schedule mode.');
|
||||
}
|
||||
if (!endDateInput.value) {
|
||||
setScheduleError(endDateInput, 'End datetime is required for this schedule mode.');
|
||||
}
|
||||
if (startDateInput.value && endDateInput.value && new Date(endDateInput.value) < new Date(startDateInput.value)) {
|
||||
setScheduleError(endDateInput, 'End datetime must be after start datetime.');
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === 'times') {
|
||||
if (!startTimeInput.value) {
|
||||
setScheduleError(startTimeInput, 'Start time is required for this schedule mode.');
|
||||
}
|
||||
if (!endTimeInput.value) {
|
||||
setScheduleError(endTimeInput, 'End time is required for this schedule mode.');
|
||||
}
|
||||
if (startTimeInput.value && endTimeInput.value && endTimeInput.value < startTimeInput.value) {
|
||||
setScheduleError(endTimeInput, 'End time must be after start time.');
|
||||
}
|
||||
if (!Array.prototype.some.call(dayCheckboxes, function (checkbox) {
|
||||
return checkbox.checked;
|
||||
})) {
|
||||
setScheduleError(firstDayCheckbox, 'Select at least one day.');
|
||||
}
|
||||
}
|
||||
|
||||
return form.checkValidity();
|
||||
}
|
||||
|
||||
[startDateInput, endDateInput, startTimeInput, endTimeInput].forEach(function (input) {
|
||||
if (input) {
|
||||
input.addEventListener('input', clearScheduleValidity);
|
||||
}
|
||||
});
|
||||
Array.prototype.forEach.call(dayCheckboxes, function (checkbox) {
|
||||
checkbox.addEventListener('change', clearScheduleValidity);
|
||||
});
|
||||
select.addEventListener('change', clearScheduleValidity);
|
||||
|
||||
function notifyParentResize() {
|
||||
return;
|
||||
}
|
||||
|
||||
function updateVisibility() {
|
||||
var mode = select.value;
|
||||
if (datesPanel) {
|
||||
datesPanel.classList.toggle('is-hidden', mode !== 'dates');
|
||||
}
|
||||
if (timesPanel) {
|
||||
timesPanel.classList.toggle('is-hidden', mode !== 'times');
|
||||
}
|
||||
if (mode === 'times') {
|
||||
if (!startTimeInput.value) {
|
||||
startTimeInput.value = DEFAULT_START_TIME;
|
||||
}
|
||||
if (!endTimeInput.value) {
|
||||
endTimeInput.value = DEFAULT_END_TIME;
|
||||
}
|
||||
}
|
||||
window.requestAnimationFrame(function () {
|
||||
notifyParentResize();
|
||||
});
|
||||
}
|
||||
|
||||
select.addEventListener('change', updateVisibility);
|
||||
updateVisibility();
|
||||
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (!validateScheduleForm()) {
|
||||
form.reportValidity();
|
||||
return;
|
||||
}
|
||||
|
||||
var mode = select.value;
|
||||
|
||||
var selectedDays = [];
|
||||
Array.prototype.forEach.call(dayCheckboxes, function (checkbox) {
|
||||
if (checkbox.checked) {
|
||||
selectedDays.push(Number(checkbox.value));
|
||||
}
|
||||
});
|
||||
|
||||
var values = {
|
||||
row_key: String(rowKeyInput && rowKeyInput.value ? rowKeyInput.value : ''),
|
||||
schedule_mode: mode,
|
||||
schedule_start_datetime: startDateInput.value || '',
|
||||
schedule_end_datetime: endDateInput.value || '',
|
||||
schedule_start_time: startTimeInput.value || '',
|
||||
schedule_end_time: endTimeInput.value || '',
|
||||
schedule_days_json: JSON.stringify(selectedDays.sort()),
|
||||
summary: ''
|
||||
};
|
||||
values.summary = scheduleSummary(values);
|
||||
|
||||
if (window.parent && typeof window.parent.applyPlaylistScheduleConfig === 'function') {
|
||||
window.parent.applyPlaylistScheduleConfig(values);
|
||||
if (typeof window.parent.closeScheduleModal === 'function') {
|
||||
window.parent.closeScheduleModal();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var payload = new URLSearchParams(new FormData(form));
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
||||
},
|
||||
body: payload.toString(),
|
||||
credentials: 'same-origin'
|
||||
}).then(function (response) {
|
||||
if (!response.ok) {
|
||||
return response.text().then(function (text) {
|
||||
throw new Error(text || 'Unable to save schedule.');
|
||||
});
|
||||
}
|
||||
if (window.parent && typeof window.parent.closeScheduleModal === 'function') {
|
||||
window.parent.closeScheduleModal();
|
||||
}
|
||||
}).catch(function (error) {
|
||||
alert(error.message || 'Unable to save schedule.');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initPlaylistEditStaging() {
|
||||
var tbody = document.getElementById('playlist-items-body');
|
||||
var form = document.getElementById('playlist-edit-form');
|
||||
var addSection = document.getElementById('playlist-add-section');
|
||||
var addSlideForm = document.getElementById('playlist-add-slide-form');
|
||||
var addSlideButton = document.getElementById('playlist-add-slide-button');
|
||||
var addSlideSelect = document.getElementById('playlist-add-slide-select');
|
||||
var addSlideEmpty = document.getElementById('playlist-add-slide-empty');
|
||||
|
||||
if (!tbody || !form) {
|
||||
return;
|
||||
}
|
||||
|
||||
function getRows() {
|
||||
return Array.prototype.slice.call(tbody.querySelectorAll('tr[data-playlist-slide-row]'));
|
||||
}
|
||||
|
||||
function findRowByKey(rowKey) {
|
||||
var rows = getRows();
|
||||
for (var i = 0; i < rows.length; i += 1) {
|
||||
if (rows[i].getAttribute('data-row-key') === rowKey) {
|
||||
return rows[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function updateEmptyState() {
|
||||
var rows = getRows();
|
||||
var placeholder = tbody.querySelector('.playlist-empty-row');
|
||||
if (rows.length) {
|
||||
if (placeholder) {
|
||||
placeholder.remove();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!placeholder) {
|
||||
tbody.innerHTML = '<tr class="playlist-empty-row"><td colspan="5" class="empty">No slides assigned yet.</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function markPlaylistDirty() {
|
||||
if (form && form.dataset) {
|
||||
form.dataset.dirty = 'true';
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleSummaryForRow(row) {
|
||||
var modeInput = row.querySelector('[name="schedule_mode[]"]');
|
||||
var startDatetime = row.querySelector('[name="schedule_start_datetime[]"]');
|
||||
var endDatetime = row.querySelector('[name="schedule_end_datetime[]"]');
|
||||
var startTime = row.querySelector('[name="schedule_start_time[]"]');
|
||||
var endTime = row.querySelector('[name="schedule_end_time[]"]');
|
||||
var daysJson = row.querySelector('[name="schedule_days_json[]"]');
|
||||
var mode = String(modeInput ? modeInput.value : 'always');
|
||||
var days = formatDays(daysJson && daysJson.value ? daysJson.value : '[]');
|
||||
|
||||
if (mode === 'dates') {
|
||||
if (startDatetime && endDatetime && startDatetime.value && endDatetime.value) {
|
||||
return 'Dates: ' + startDatetime.value.replace('T', ' ') + ' to ' + endDatetime.value.replace('T', ' ');
|
||||
}
|
||||
return 'Dates: not set';
|
||||
}
|
||||
if (mode === 'times') {
|
||||
if (days && startTime && endTime && startTime.value && endTime.value) {
|
||||
return 'Times: ' + days + ' ' + startTime.value.slice(0, 5) + '-' + endTime.value.slice(0, 5);
|
||||
}
|
||||
return 'Times: not set';
|
||||
}
|
||||
return 'Always visible';
|
||||
}
|
||||
|
||||
function syncAddSlideOptions() {
|
||||
if (!addSlideSelect) {
|
||||
return;
|
||||
}
|
||||
var activeSlideIds = {};
|
||||
var activeCanvasSignatures = {};
|
||||
getRows().forEach(function (row) {
|
||||
var slideId = String(row.getAttribute('data-slide-id') || '');
|
||||
var canvasSignature = String(row.getAttribute('data-canvas-signature') || '');
|
||||
if (slideId) {
|
||||
activeSlideIds[slideId] = true;
|
||||
}
|
||||
if (canvasSignature) {
|
||||
activeCanvasSignatures[canvasSignature] = true;
|
||||
}
|
||||
});
|
||||
|
||||
var allowedCanvasSignature = '';
|
||||
var canvasKeys = Object.keys(activeCanvasSignatures);
|
||||
if (canvasKeys.length === 1) {
|
||||
allowedCanvasSignature = canvasKeys[0];
|
||||
}
|
||||
|
||||
Array.prototype.forEach.call(addSlideSelect.options, function (option) {
|
||||
if (!option.value) {
|
||||
return;
|
||||
}
|
||||
var optionCanvasSignature = String(option.getAttribute('data-canvas-signature') || '');
|
||||
var isActive = Boolean(activeSlideIds[String(option.value)]);
|
||||
var isCanvasMismatch = Boolean(allowedCanvasSignature && optionCanvasSignature && optionCanvasSignature !== allowedCanvasSignature);
|
||||
option.disabled = isActive || isCanvasMismatch;
|
||||
option.hidden = isActive || isCanvasMismatch;
|
||||
});
|
||||
if (
|
||||
addSlideSelect.options[addSlideSelect.selectedIndex]
|
||||
&& (addSlideSelect.options[addSlideSelect.selectedIndex].disabled || addSlideSelect.options[addSlideSelect.selectedIndex].hidden)
|
||||
) {
|
||||
for (var i = 0; i < addSlideSelect.options.length; i += 1) {
|
||||
if (!addSlideSelect.options[i].disabled && !addSlideSelect.options[i].hidden) {
|
||||
addSlideSelect.selectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var availableCount = 0;
|
||||
Array.prototype.forEach.call(addSlideSelect.options, function (option) {
|
||||
if (!option.value) {
|
||||
return;
|
||||
}
|
||||
if (!option.disabled && !option.hidden) {
|
||||
availableCount += 1;
|
||||
}
|
||||
});
|
||||
|
||||
if (addSlideButton) {
|
||||
addSlideButton.disabled = availableCount === 0;
|
||||
}
|
||||
if (addSlideSelect) {
|
||||
addSlideSelect.disabled = availableCount === 0;
|
||||
}
|
||||
if (addSlideEmpty) {
|
||||
addSlideEmpty.classList.toggle('is-hidden', availableCount !== 0);
|
||||
}
|
||||
if (addSection && addSlideForm && addSlideSelect && addSlideButton) {
|
||||
var controlsVisible = availableCount > 0;
|
||||
addSlideForm.classList.toggle('is-hidden', !controlsVisible);
|
||||
}
|
||||
}
|
||||
|
||||
function updateRowOrder(markDirty) {
|
||||
var rows = getRows();
|
||||
rows.forEach(function (row, index) {
|
||||
var orderNumber = row.querySelector('.playlist-order-number');
|
||||
if (orderNumber) {
|
||||
orderNumber.textContent = String(index + 1);
|
||||
}
|
||||
});
|
||||
syncAddSlideOptions();
|
||||
updateEmptyState();
|
||||
if (markDirty) {
|
||||
markPlaylistDirty();
|
||||
}
|
||||
}
|
||||
|
||||
function lockDraggedRowWidths(row) {
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
|
||||
var rowRect = row.getBoundingClientRect();
|
||||
row.style.width = rowRect.width + 'px';
|
||||
row.style.height = rowRect.height + 'px';
|
||||
row.style.boxSizing = 'border-box';
|
||||
|
||||
Array.prototype.forEach.call(row.children, function (cell) {
|
||||
var cellRect = cell.getBoundingClientRect();
|
||||
cell.style.width = cellRect.width + 'px';
|
||||
cell.style.height = cellRect.height + 'px';
|
||||
cell.style.boxSizing = 'border-box';
|
||||
});
|
||||
}
|
||||
|
||||
function unlockDraggedRowWidths(row) {
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
|
||||
row.style.width = '';
|
||||
row.style.height = '';
|
||||
row.style.boxSizing = '';
|
||||
|
||||
Array.prototype.forEach.call(row.children, function (cell) {
|
||||
cell.style.width = '';
|
||||
cell.style.height = '';
|
||||
cell.style.boxSizing = '';
|
||||
});
|
||||
}
|
||||
|
||||
function setRowSchedule(row, values) {
|
||||
var modeInput = row.querySelector('[name="schedule_mode[]"]');
|
||||
var startDatetime = row.querySelector('[name="schedule_start_datetime[]"]');
|
||||
var endDatetime = row.querySelector('[name="schedule_end_datetime[]"]');
|
||||
var startTime = row.querySelector('[name="schedule_start_time[]"]');
|
||||
var endTime = row.querySelector('[name="schedule_end_time[]"]');
|
||||
var daysJson = row.querySelector('[name="schedule_days_json[]"]');
|
||||
var summary = row.querySelector('.playlist-schedule-summary');
|
||||
|
||||
if (modeInput) {
|
||||
modeInput.value = values.schedule_mode || 'always';
|
||||
}
|
||||
if (startDatetime) {
|
||||
startDatetime.value = values.schedule_start_datetime || '';
|
||||
}
|
||||
if (endDatetime) {
|
||||
endDatetime.value = values.schedule_end_datetime || '';
|
||||
}
|
||||
if (startTime) {
|
||||
startTime.value = values.schedule_start_time || '';
|
||||
}
|
||||
if (endTime) {
|
||||
endTime.value = values.schedule_end_time || '';
|
||||
}
|
||||
if (daysJson) {
|
||||
daysJson.value = values.schedule_days_json || '[]';
|
||||
}
|
||||
if (summary) {
|
||||
summary.textContent = values.summary || scheduleSummaryForRow(row);
|
||||
}
|
||||
markPlaylistDirty();
|
||||
}
|
||||
|
||||
function createRow(values) {
|
||||
var row = document.createElement('tr');
|
||||
var rowKey = values.row_key || ('new-' + Date.now() + '-' + Math.random().toString(36).slice(2));
|
||||
var playlistId = tbody.getAttribute('data-playlist-id') || '';
|
||||
row.setAttribute('data-playlist-slide-row', '');
|
||||
row.setAttribute('data-row-key', rowKey);
|
||||
row.setAttribute('data-slide-id', String(values.slide_id));
|
||||
row.setAttribute('data-canvas-signature', String(values.canvas_signature || ''));
|
||||
row.innerHTML = '' +
|
||||
'<td class="playlist-order-cell" data-label="Order">' +
|
||||
'<div class="playlist-order-cell-inner">' +
|
||||
'<button type="button" class="playlist-drag-handle btn btn-link p-0 text-body-secondary" data-playlist-drag-handle aria-label="Drag to reorder" title="Drag to reorder"><span class="playlist-drag-handle-icon" aria-hidden="true"><svg class="playlist-drag-handle-svg" viewBox="0 0 24 32" focusable="false" aria-hidden="true"><polygon points="12,2 20,9 4,9"></polygon><rect x="4" y="14" width="16" height="4" rx="2"></rect><polygon points="4,23 20,23 12,30"></polygon></svg></span></button>' +
|
||||
'<span class="playlist-order-number"></span>' +
|
||||
'</div>' +
|
||||
'</td>' +
|
||||
'<td>' + values.title + '<input type="hidden" name="slide_id[]" value="' + values.slide_id + '" form="playlist-edit-form" /></td>' +
|
||||
'<td>' +
|
||||
'<div class="playlist-schedule-summary">' + values.summary + '</div>' +
|
||||
'<input type="hidden" name="schedule_mode[]" value="' + values.schedule_mode + '" form="playlist-edit-form" />' +
|
||||
'<input type="hidden" name="schedule_start_datetime[]" value="' + values.schedule_start_datetime + '" form="playlist-edit-form" />' +
|
||||
'<input type="hidden" name="schedule_end_datetime[]" value="' + values.schedule_end_datetime + '" form="playlist-edit-form" />' +
|
||||
'<input type="hidden" name="schedule_start_time[]" value="' + values.schedule_start_time + '" form="playlist-edit-form" />' +
|
||||
'<input type="hidden" name="schedule_end_time[]" value="' + values.schedule_end_time + '" form="playlist-edit-form" />' +
|
||||
'<input type="hidden" name="schedule_days_json[]" value="' + values.schedule_days_json + '" form="playlist-edit-form" />' +
|
||||
'</td>' +
|
||||
'<td><input name="duration_seconds[]" type="number" min="1" value="' + values.duration_seconds + '" required form="playlist-edit-form" /></td>' +
|
||||
'<td><div class="actions playlist-item-actions">' +
|
||||
'<button type="button" class="btn btn-sm btn-primary" data-schedule-config="/admin/playlists/' + encodeURIComponent(playlistId) + '/slides/0/config" data-schedule-config-row="' + rowKey + '">Schedule</button>' +
|
||||
'<button type="button" class="btn btn-sm btn-danger" data-playlist-remove-row>Remove</button>' +
|
||||
'</div></td>';
|
||||
return row;
|
||||
}
|
||||
|
||||
tbody.addEventListener('click', function (event) {
|
||||
var removeButton = event.target.closest('[data-playlist-remove-row]');
|
||||
var scheduleButton = event.target.closest('[data-schedule-config]');
|
||||
var row = event.target.closest('tr[data-playlist-slide-row]');
|
||||
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (removeButton) {
|
||||
event.preventDefault();
|
||||
row.remove();
|
||||
updateRowOrder(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (scheduleButton) {
|
||||
event.preventDefault();
|
||||
if (typeof window.openScheduleModal === 'function') {
|
||||
window.openScheduleModal(scheduleButton.getAttribute('data-schedule-config') + '?row_key=' + encodeURIComponent(row.getAttribute('data-row-key') || ''));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (window.Sortable) {
|
||||
Sortable.create(tbody, {
|
||||
animation: 180,
|
||||
handle: '[data-playlist-drag-handle]',
|
||||
draggable: 'tr[data-playlist-slide-row]',
|
||||
ghostClass: 'sortable-ghost',
|
||||
chosenClass: 'sortable-chosen',
|
||||
dragClass: 'sortable-drag',
|
||||
forceFallback: true,
|
||||
fallbackOnBody: true,
|
||||
fallbackTolerance: 3,
|
||||
swapThreshold: 0.65,
|
||||
invertedSwapThreshold: 0.65,
|
||||
onChoose: function (event) {
|
||||
lockDraggedRowWidths(event && event.item);
|
||||
},
|
||||
onUnchoose: function (event) {
|
||||
unlockDraggedRowWidths(event && event.item);
|
||||
},
|
||||
onEnd: function () {
|
||||
unlockDraggedRowWidths(tbody.querySelector('.sortable-drag'));
|
||||
updateRowOrder(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (addSlideButton && addSlideSelect) {
|
||||
addSlideButton.addEventListener('click', function () {
|
||||
var selectedOption = addSlideSelect.options[addSlideSelect.selectedIndex];
|
||||
if (!selectedOption || selectedOption.disabled) {
|
||||
return;
|
||||
}
|
||||
var placeholder = tbody.querySelector('.playlist-empty-row');
|
||||
if (placeholder) {
|
||||
placeholder.remove();
|
||||
}
|
||||
var row = createRow({
|
||||
row_key: 'new-' + Date.now(),
|
||||
slide_id: String(selectedOption.value),
|
||||
canvas_signature: String(selectedOption.getAttribute('data-canvas-signature') || ''),
|
||||
title: selectedOption.textContent || 'Slide',
|
||||
duration_seconds: 10,
|
||||
schedule_mode: 'always',
|
||||
schedule_start_datetime: '',
|
||||
schedule_end_datetime: '',
|
||||
schedule_start_time: '',
|
||||
schedule_end_time: '',
|
||||
schedule_days_json: '[]',
|
||||
summary: 'Always visible'
|
||||
});
|
||||
tbody.appendChild(row);
|
||||
updateRowOrder(true);
|
||||
});
|
||||
}
|
||||
|
||||
window.applyPlaylistScheduleConfig = function (values) {
|
||||
var row = findRowByKey(String(values.row_key || ''));
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
setRowSchedule(row, values);
|
||||
};
|
||||
|
||||
updateRowOrder();
|
||||
}
|
||||
|
||||
window.initPlaylistScheduleForm = initPlaylistScheduleForm;
|
||||
|
||||
initPlaylistScheduleModal();
|
||||
initPlaylistScheduleForm();
|
||||
initPlaylistEditStaging();
|
||||
}());
|
||||
Reference in New Issue
Block a user