Refactor web server and release workflow
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,485 @@
|
||||
(function () {
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function formatDashboardDate(value) {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
var date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return '';
|
||||
}
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
function getClientRowKey(client) {
|
||||
return String(client && client.id ? client.id : client && client.clientId ? client.clientId : '').trim();
|
||||
}
|
||||
|
||||
function renderClientActionCell(client) {
|
||||
var paused = Boolean(client.paused);
|
||||
var pauseButtonClass = 'button-link list-action' + (paused ? ' is-paused' : '');
|
||||
var pauseButtonLabel = paused ? 'Resume' : 'Pause';
|
||||
var blackout = Boolean(client.blackout);
|
||||
var blackoutButtonClass = 'button-link list-action' + (blackout ? ' is-blackout' : '');
|
||||
var blackoutButtonLabel = blackout ? 'Restore' : 'Blackout';
|
||||
var reloadConfirmMessage = 'Reloading will restart the player page. Continue?';
|
||||
var blackoutCommandValue = blackout ? 'false' : 'true';
|
||||
|
||||
return '<div class="actions"><form method="post" action="/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands" class="inline-form" data-confirm-message="' + escapeHtml(reloadConfirmMessage) + '" data-async-command><input type="hidden" name="command" value="reload" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="button-link list-action is-danger" data-action="reload">Reload</button></form><form method="post" action="/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands" class="inline-form" data-async-command><input type="hidden" name="command" value="previous" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="button-link list-action list-action--nav" data-action="previous" aria-label="Previous slide">◀</button></form><form method="post" action="/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands" class="inline-form" data-async-command><input type="hidden" name="command" value="next" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="button-link list-action list-action--nav" data-action="next" aria-label="Next slide">▶</button></form><form method="post" action="/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands" class="inline-form" data-async-command><input type="hidden" name="command" value="pause" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="' + pauseButtonClass + '" data-action="pause">' + pauseButtonLabel + '</button></form><form method="post" action="/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands" class="inline-form" data-async-command><input type="hidden" name="command" value="blackout" /><input type="hidden" name="blackout" value="' + blackoutCommandValue + '" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="' + blackoutButtonClass + '" data-action="blackout">' + blackoutButtonLabel + '</button></form></div>';
|
||||
}
|
||||
|
||||
function updateClientActionCell(cell, client) {
|
||||
if (!cell) {
|
||||
return;
|
||||
}
|
||||
|
||||
var pauseButton = cell.querySelector('button[data-action="pause"]');
|
||||
if (!pauseButton) {
|
||||
cell.innerHTML = renderClientActionCell(client);
|
||||
return;
|
||||
}
|
||||
|
||||
var paused = Boolean(client.paused);
|
||||
pauseButton.className = 'button-link list-action' + (paused ? ' is-paused' : '');
|
||||
pauseButton.textContent = paused ? 'Resume' : 'Pause';
|
||||
|
||||
var pauseForm = pauseButton.form;
|
||||
if (pauseForm) {
|
||||
var commandInput = pauseForm.querySelector('input[name="command"]');
|
||||
if (commandInput) {
|
||||
commandInput.value = 'pause';
|
||||
}
|
||||
var connectionInput = pauseForm.querySelector('input[name="connectionId"]');
|
||||
if (connectionInput) {
|
||||
connectionInput.value = client.id || '';
|
||||
}
|
||||
pauseForm.action = '/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands';
|
||||
}
|
||||
|
||||
var reloadButton = cell.querySelector('button[data-action="reload"]');
|
||||
if (reloadButton) {
|
||||
reloadButton.textContent = 'Reload';
|
||||
reloadButton.className = 'button-link list-action is-danger';
|
||||
var reloadForm = reloadButton.form;
|
||||
if (reloadForm) {
|
||||
var reloadInput = reloadForm.querySelector('input[name="connectionId"]');
|
||||
if (reloadInput) {
|
||||
reloadInput.value = client.id || '';
|
||||
}
|
||||
reloadForm.action = '/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands';
|
||||
reloadForm.setAttribute('data-confirm-message', 'Reloading will restart the player page. Continue?');
|
||||
}
|
||||
}
|
||||
|
||||
var blackoutButton = cell.querySelector('button[data-action="blackout"]');
|
||||
if (!blackoutButton) {
|
||||
cell.innerHTML = renderClientActionCell(client);
|
||||
return;
|
||||
}
|
||||
|
||||
var blackout = Boolean(client.blackout);
|
||||
blackoutButton.className = 'button-link list-action' + (blackout ? ' is-blackout' : '');
|
||||
blackoutButton.textContent = blackout ? 'Restore' : 'Blackout';
|
||||
|
||||
var blackoutForm = blackoutButton.form;
|
||||
if (blackoutForm) {
|
||||
var blackoutCommandInput = blackoutForm.querySelector('input[name="command"]');
|
||||
if (blackoutCommandInput) {
|
||||
blackoutCommandInput.value = 'blackout';
|
||||
}
|
||||
var blackoutStateInput = blackoutForm.querySelector('input[name="blackout"]');
|
||||
if (blackoutStateInput) {
|
||||
blackoutStateInput.value = blackout ? 'false' : 'true';
|
||||
}
|
||||
var blackoutConnectionInput = blackoutForm.querySelector('input[name="connectionId"]');
|
||||
if (blackoutConnectionInput) {
|
||||
blackoutConnectionInput.value = client.id || '';
|
||||
}
|
||||
blackoutForm.action = '/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands';
|
||||
}
|
||||
|
||||
var previousButton = cell.querySelector('button[data-action="previous"]');
|
||||
if (!previousButton) {
|
||||
cell.innerHTML = renderClientActionCell(client);
|
||||
return;
|
||||
}
|
||||
previousButton.className = 'button-link list-action list-action--nav';
|
||||
previousButton.setAttribute('aria-label', 'Previous slide');
|
||||
var previousForm = previousButton.form;
|
||||
if (previousForm) {
|
||||
var previousCommandInput = previousForm.querySelector('input[name="command"]');
|
||||
if (previousCommandInput) {
|
||||
previousCommandInput.value = 'previous';
|
||||
}
|
||||
var previousConnectionInput = previousForm.querySelector('input[name="connectionId"]');
|
||||
if (previousConnectionInput) {
|
||||
previousConnectionInput.value = client.id || '';
|
||||
}
|
||||
previousForm.action = '/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands';
|
||||
}
|
||||
|
||||
var nextButton = cell.querySelector('button[data-action="next"]');
|
||||
if (!nextButton) {
|
||||
cell.innerHTML = renderClientActionCell(client);
|
||||
return;
|
||||
}
|
||||
nextButton.className = 'button-link list-action list-action--nav';
|
||||
nextButton.setAttribute('aria-label', 'Next slide');
|
||||
var nextForm = nextButton.form;
|
||||
if (nextForm) {
|
||||
var nextCommandInput = nextForm.querySelector('input[name="command"]');
|
||||
if (nextCommandInput) {
|
||||
nextCommandInput.value = 'next';
|
||||
}
|
||||
var nextConnectionInput = nextForm.querySelector('input[name="connectionId"]');
|
||||
if (nextConnectionInput) {
|
||||
nextConnectionInput.value = client.id || '';
|
||||
}
|
||||
nextForm.action = '/admin/screens/' + encodeURIComponent(client.screen_slug) + '/commands';
|
||||
}
|
||||
}
|
||||
|
||||
function renderClientRow(client) {
|
||||
var connectedAt = client.connectedAt ? '<div>' + escapeHtml(client.connectedAtLabel || formatDashboardDate(client.connectedAt) || client.connectedAt) + '</div>' + (client.lastSeenAt ? '<div class="subtle">' + escapeHtml(client.lastSeenAtLabel || formatDashboardDate(client.lastSeenAt) || client.lastSeenAt) + '</div>' : '') : '<span class="empty">Unknown</span>';
|
||||
var clientIp = client.clientIp ? escapeHtml(client.clientIp) : (client.remoteAddress ? escapeHtml(client.remoteAddress) : '<span class="empty">Unknown</span>');
|
||||
var viewport = client.viewport && client.viewport.width && client.viewport.height ? escapeHtml(client.viewport.width + 'x' + client.viewport.height) : '<span class="empty">Unknown</span>';
|
||||
var clientId = client.clientId ? escapeHtml(client.clientId) : '<span class="empty">Unknown</span>';
|
||||
var connectionId = client.id ? escapeHtml(client.id) : '<span class="empty">Unknown</span>';
|
||||
var currentSlide = client.currentSlideTitle ? escapeHtml(client.currentSlideTitle) : 'No slide currently showing';
|
||||
|
||||
return [
|
||||
'<tr data-client-key="' + escapeHtml(getClientRowKey(client)) + '">',
|
||||
'<td data-label="Client/Connection ID"><div class="connection-count">' + clientId + '</div><div class="connection-id">' + connectionId + '</div></td>',
|
||||
'<td data-label="IP">' + clientIp + '</td>',
|
||||
'<td data-label="Viewport">' + viewport + '</td>',
|
||||
'<td data-label="Connected">' + connectedAt + '</td>',
|
||||
'<td data-label="Screen"><div>' + escapeHtml(client.screen_name) + '</div><div class="subtle">Showing: ' + currentSlide + '</div></td>',
|
||||
'<td data-label="Actions">' + renderClientActionCell(client) + '</td>',
|
||||
'</tr>'
|
||||
].join('');
|
||||
}
|
||||
|
||||
function renderScreenRow(screen) {
|
||||
var clientCount = Number(screen.player_connection_count || 0);
|
||||
return [
|
||||
'<tr>',
|
||||
'<td data-label="Name">' + escapeHtml(screen.name) + '</td>',
|
||||
'<td data-label="Player URL"><a href="' + escapeHtml(screen.player_url || '') + '" target="_blank">' + escapeHtml(screen.player_url || '') + '</a></td>',
|
||||
'<td data-label="Playlist">' + escapeHtml(screen.playlist_name || '') + '</td>',
|
||||
'<td data-label="Connected clients">' + (clientCount ? '<div class="connection-count" data-screen-connection-count="' + escapeHtml(screen.slug) + '">' + clientCount + ' connected</div>' : '<span class="empty">No clients connected.</span>') + '</td>',
|
||||
'</tr>'
|
||||
].join('');
|
||||
}
|
||||
|
||||
function updateStats(state) {
|
||||
var clientCount = document.getElementById('dashboard-client-count');
|
||||
var screenCount = document.getElementById('dashboard-screen-count');
|
||||
var slideCount = document.getElementById('dashboard-slide-count');
|
||||
var playlistCount = document.getElementById('dashboard-playlist-count');
|
||||
|
||||
if (playlistCount && Array.isArray(state.playlists)) {
|
||||
playlistCount.textContent = String(state.playlists.length);
|
||||
}
|
||||
if (slideCount && Array.isArray(state.slides)) {
|
||||
slideCount.textContent = String(state.slides.length);
|
||||
}
|
||||
if (screenCount && Array.isArray(state.screens)) {
|
||||
screenCount.textContent = String(state.screens.length);
|
||||
}
|
||||
if (clientCount) {
|
||||
clientCount.textContent = String(Number(state.connectedClientsCount || 0));
|
||||
}
|
||||
}
|
||||
|
||||
function updateClientTable(state) {
|
||||
var tbody = document.getElementById('dashboard-clients-table-body');
|
||||
if (!tbody || !Array.isArray(state.clients)) {
|
||||
return;
|
||||
}
|
||||
if (!state.clients.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="empty">No connected clients yet.</td></tr>';
|
||||
return;
|
||||
}
|
||||
var existingRows = {};
|
||||
Array.prototype.slice.call(tbody.querySelectorAll('tr[data-client-key]')).forEach(function (row) {
|
||||
existingRows[row.getAttribute('data-client-key')] = row;
|
||||
});
|
||||
|
||||
Array.prototype.slice.call(tbody.querySelectorAll('tr')).forEach(function (row) {
|
||||
if (!row.hasAttribute('data-client-key')) {
|
||||
row.parentNode.removeChild(row);
|
||||
}
|
||||
});
|
||||
|
||||
state.clients.forEach(function (client, index) {
|
||||
var rowKey = getClientRowKey(client);
|
||||
var row = existingRows[rowKey];
|
||||
if (!row) {
|
||||
var tempBody = document.createElement('tbody');
|
||||
tempBody.innerHTML = renderClientRow(client);
|
||||
row = tempBody.firstElementChild;
|
||||
}
|
||||
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
|
||||
row.setAttribute('data-client-key', rowKey);
|
||||
if (row.cells && row.cells.length >= 6) {
|
||||
var connectedAt = client.connectedAt ? '<div>' + escapeHtml(client.connectedAtLabel || formatDashboardDate(client.connectedAt) || client.connectedAt) + '</div>' + (client.lastSeenAt ? '<div class="subtle">' + escapeHtml(client.lastSeenAtLabel || formatDashboardDate(client.lastSeenAt) || client.lastSeenAt) + '</div>' : '') : '<span class="empty">Unknown</span>';
|
||||
var clientIp = client.clientIp ? escapeHtml(client.clientIp) : (client.remoteAddress ? escapeHtml(client.remoteAddress) : '<span class="empty">Unknown</span>');
|
||||
var viewport = client.viewport && client.viewport.width && client.viewport.height ? escapeHtml(client.viewport.width + 'x' + client.viewport.height) : '<span class="empty">Unknown</span>';
|
||||
var clientId = client.clientId ? escapeHtml(client.clientId) : '<span class="empty">Unknown</span>';
|
||||
var connectionId = client.id ? escapeHtml(client.id) : '<span class="empty">Unknown</span>';
|
||||
var currentSlide = client.currentSlideTitle ? escapeHtml(client.currentSlideTitle) : 'No slide currently showing';
|
||||
|
||||
row.cells[0].innerHTML = '<div class="connection-count">' + clientId + '</div><div class="connection-id">' + connectionId + '</div>';
|
||||
row.cells[1].innerHTML = clientIp;
|
||||
row.cells[2].innerHTML = viewport;
|
||||
row.cells[3].innerHTML = connectedAt;
|
||||
row.cells[4].innerHTML = '<div>' + escapeHtml(client.screen_name) + '</div><div class="subtle">Showing: ' + currentSlide + '</div>';
|
||||
updateClientActionCell(row.cells[5], client);
|
||||
}
|
||||
|
||||
var referenceNode = tbody.children[index] || null;
|
||||
if (referenceNode !== row) {
|
||||
tbody.insertBefore(row, referenceNode);
|
||||
}
|
||||
});
|
||||
|
||||
while (tbody.children.length > state.clients.length) {
|
||||
tbody.removeChild(tbody.lastElementChild);
|
||||
}
|
||||
|
||||
window.applyTableSort(document.getElementById('dashboard-clients-table'));
|
||||
}
|
||||
|
||||
function updateScreenTable(state) {
|
||||
var table = document.getElementById('dashboard-screens-table');
|
||||
if (!table || !Array.isArray(state.screens)) {
|
||||
return;
|
||||
}
|
||||
var tbody = table.tBodies && table.tBodies[0] ? table.tBodies[0] : null;
|
||||
if (!tbody) {
|
||||
return;
|
||||
}
|
||||
if (!state.screens.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="4" class="empty">No screens yet.</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = state.screens.map(renderScreenRow).join('');
|
||||
window.applyTableSort(table);
|
||||
}
|
||||
|
||||
function updateDashboardQuickActions(state) {
|
||||
var blackoutButton = document.getElementById('dashboard-blackout-all-button');
|
||||
if (!blackoutButton || !state || !Array.isArray(state.clients)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var hasClients = state.clients.length > 0;
|
||||
var allBlackout = hasClients && state.clients.every(function (client) {
|
||||
return Boolean(client && client.blackout);
|
||||
});
|
||||
var label = allBlackout ? 'Restore all clients' : 'Blackout all clients';
|
||||
var blackoutForm = blackoutButton.form;
|
||||
var blackoutInput = blackoutForm ? blackoutForm.querySelector('input[name="blackout"]') : null;
|
||||
|
||||
blackoutButton.textContent = label;
|
||||
blackoutButton.className = 'button-link list-action' + (allBlackout ? ' is-blackout' : '');
|
||||
if (blackoutInput) {
|
||||
blackoutInput.value = allBlackout ? 'false' : 'true';
|
||||
}
|
||||
if (blackoutForm) {
|
||||
blackoutForm.setAttribute('data-confirm-message', allBlackout ? 'Restore all connected clients?' : 'Blackout all connected clients?');
|
||||
}
|
||||
blackoutButton.setAttribute('aria-label', label);
|
||||
}
|
||||
|
||||
function handleDashboardState(state) {
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
updateStats(state);
|
||||
updateScreenTable(state);
|
||||
updateClientTable(state);
|
||||
updateDashboardQuickActions(state);
|
||||
}
|
||||
|
||||
window.webHandleDashboardState = handleDashboardState;
|
||||
|
||||
function initConfirmForms() {
|
||||
document.addEventListener('submit', function (event) {
|
||||
var form = event.target;
|
||||
if (!form || !form.getAttribute) {
|
||||
return;
|
||||
}
|
||||
if (form.hasAttribute && form.hasAttribute('data-async-command')) {
|
||||
return;
|
||||
}
|
||||
var message = form.getAttribute('data-confirm-message');
|
||||
if (message && !window.confirm(message)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initAsyncCommandForms() {
|
||||
document.addEventListener('submit', function (event) {
|
||||
var form = event.target;
|
||||
if (!form || !form.hasAttribute || !form.hasAttribute('data-async-command')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (form.dataset && form.dataset.busy === 'true') {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
var message = form.getAttribute('data-confirm-message');
|
||||
if (message && !window.confirm(message)) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
form.dataset.busy = 'true';
|
||||
|
||||
var formData = new FormData(form);
|
||||
var body = new URLSearchParams();
|
||||
formData.forEach(function (value, key) {
|
||||
body.append(key, value);
|
||||
});
|
||||
|
||||
fetch(form.action, {
|
||||
method: (form.method || 'POST').toUpperCase(),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'application/json, text/plain, */*'
|
||||
},
|
||||
body: body.toString(),
|
||||
credentials: 'same-origin'
|
||||
}).finally(function () {
|
||||
delete form.dataset.busy;
|
||||
});
|
||||
}, true);
|
||||
}
|
||||
|
||||
function initAsyncSaveForms() {
|
||||
document.addEventListener('submit', function (event) {
|
||||
var form = event.target;
|
||||
if (!form || !form.hasAttribute || !form.hasAttribute('data-async-save')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (form.dataset && form.dataset.busy === 'true') {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
var message = form.getAttribute('data-confirm-message');
|
||||
if (message && !window.confirm(message)) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
form.dataset.busy = 'true';
|
||||
|
||||
var formData = new FormData(form);
|
||||
var hasFileValue = false;
|
||||
formData.forEach(function (value) {
|
||||
if (value && typeof value === 'object' && typeof value.name === 'string') {
|
||||
hasFileValue = true;
|
||||
}
|
||||
});
|
||||
|
||||
var isMultipart = hasFileValue || String(form.enctype || '').toLowerCase() === 'multipart/form-data';
|
||||
var body = isMultipart ? formData : new URLSearchParams();
|
||||
|
||||
if (!isMultipart) {
|
||||
formData.forEach(function (value, key) {
|
||||
body.append(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
fetch(form.action, {
|
||||
method: (form.method || 'POST').toUpperCase(),
|
||||
headers: Object.assign({
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'text/html, application/json, text/plain, */*'
|
||||
}, isMultipart ? {} : {
|
||||
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
|
||||
}),
|
||||
body: isMultipart ? body : body.toString(),
|
||||
credentials: 'same-origin'
|
||||
}).then(function (response) {
|
||||
if (!response.ok) {
|
||||
return response.text().then(function (text) {
|
||||
throw new Error(text || 'Unable to save changes.');
|
||||
});
|
||||
}
|
||||
if (form.hasAttribute && form.hasAttribute('data-async-save-reload')) {
|
||||
window.location.replace(response.url || window.location.href);
|
||||
return;
|
||||
}
|
||||
return response.text().then(function (text) {
|
||||
var savedMessage = '';
|
||||
try {
|
||||
var doc = new DOMParser().parseFromString(text || '', 'text/html');
|
||||
var toastBody = doc.querySelector('.toast-body');
|
||||
if (toastBody && toastBody.textContent) {
|
||||
savedMessage = toastBody.textContent.trim();
|
||||
}
|
||||
} catch (_error) {
|
||||
savedMessage = '';
|
||||
}
|
||||
showToast(savedMessage || 'Saved.');
|
||||
});
|
||||
}).catch(function (error) {
|
||||
window.alert(error.message || 'Unable to save changes.');
|
||||
}).finally(function () {
|
||||
delete form.dataset.busy;
|
||||
});
|
||||
}, true);
|
||||
}
|
||||
|
||||
function initSubmitOnChange() {
|
||||
var fields = document.querySelectorAll('[data-submit-on-change]');
|
||||
Array.prototype.forEach.call(fields, function (field) {
|
||||
field.addEventListener('change', function () {
|
||||
var form = field.form || field.closest('form');
|
||||
if (form) {
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (window.initSortableTables) {
|
||||
window.initSortableTables();
|
||||
}
|
||||
|
||||
initConfirmForms();
|
||||
initAsyncCommandForms();
|
||||
initAsyncSaveForms();
|
||||
initSubmitOnChange();
|
||||
}());
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Skipped minification because the original files appears to be already minified.
|
||||
* Original file: /npm/@editorjs/header@2.8.9/dist/header.umd.js
|
||||
*
|
||||
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||
*/
|
||||
(function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode(".ce-header{padding:.6em 0 3px;margin:0;line-height:1.25em;outline:none}.ce-header p,.ce-header div{padding:0!important;margin:0!important}")),document.head.appendChild(e)}}catch(n){console.error("vite-plugin-css-injected-by-js",n)}})();
|
||||
(function(o,s){typeof exports=="object"&&typeof module<"u"?module.exports=s():typeof define=="function"&&define.amd?define(s):(o=typeof globalThis<"u"?globalThis:o||self,o.Header=s())})(this,function(){"use strict";const o="",s='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M6 7L6 12M6 17L6 12M6 12L12 12M12 7V12M12 17L12 12"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M19 17V10.2135C19 10.1287 18.9011 10.0824 18.836 10.1367L16 12.5"/></svg>',d='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M6 7L6 12M6 17L6 12M6 12L12 12M12 7V12M12 17L12 12"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M16 11C16 10 19 9.5 19 12C19 13.9771 16.0684 13.9997 16.0012 16.8981C15.9999 16.9533 16.0448 17 16.1 17L19.3 17"/></svg>',u='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M6 7L6 12M6 17L6 12M6 12L12 12M12 7V12M12 17L12 12"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M16 11C16 10.5 16.8323 10 17.6 10C18.3677 10 19.5 10.311 19.5 11.5C19.5 12.5315 18.7474 12.9022 18.548 12.9823C18.5378 12.9864 18.5395 13.0047 18.5503 13.0063C18.8115 13.0456 20 13.3065 20 14.8C20 16 19.5 17 17.8 17C17.8 17 16 17 16 16.3"/></svg>',c='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M6 7L6 12M6 17L6 12M6 12L12 12M12 7V12M12 17L12 12"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M18 10L15.2834 14.8511C15.246 14.9178 15.294 15 15.3704 15C16.8489 15 18.7561 15 20.2 15M19 17C19 15.7187 19 14.8813 19 13.6"/></svg>',g='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M6 7L6 12M6 17L6 12M6 12L12 12M12 7V12M12 17L12 12"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M16 15.9C16 15.9 16.3768 17 17.8 17C19.5 17 20 15.6199 20 14.7C20 12.7323 17.6745 12.0486 16.1635 12.9894C16.094 13.0327 16 12.9846 16 12.9027V10.1C16 10.0448 16.0448 10 16.1 10H19.8"/></svg>',v='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M6 7L6 12M6 17L6 12M6 12L12 12M12 7V12M12 17L12 12"/><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M19.5 10C16.5 10.5 16 13.3285 16 15M16 15V15C16 16.1046 16.8954 17 18 17H18.3246C19.3251 17 20.3191 16.3492 20.2522 15.3509C20.0612 12.4958 16 12.6611 16 15Z"/></svg>',w='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M9 7L9 12M9 17V12M9 12L15 12M15 7V12M15 17L15 12"/></svg>';/**
|
||||
* Header block for the Editor.js.
|
||||
*
|
||||
* @author CodeX (team@ifmo.su)
|
||||
* @copyright CodeX 2018
|
||||
* @license MIT
|
||||
* @version 2.0.0
|
||||
*/class p{constructor({data:e,config:t,api:i,readOnly:r}){this.api=i,this.readOnly=r,this._config=t??null,this._data=this.normalizeData(e),this._element=this.getTag()}get _CSS(){return{block:this.api.styles.block,wrapper:"ce-header"}}isHeaderData(e){return e.text!==void 0}normalizeData(e){const t={text:"",level:this.defaultLevel.number};return this.isHeaderData(e)&&(t.text=e.text||"",e.level!==void 0&&!isNaN(parseInt(e.level.toString()))&&(t.level=parseInt(e.level.toString()))),t}render(){return this._element}renderSettings(){return this.levels.map(e=>({icon:e.svg,label:this.api.i18n.t(`Heading ${e.number}`),onActivate:()=>this.setLevel(e.number),closeOnActivate:!0,isActive:this.currentLevel.number===e.number,render:()=>document.createElement("div")}))}setLevel(e){this.data={level:e,text:this.data.text}}merge(e){this._element.insertAdjacentHTML("beforeend",e.text)}validate(e){return e.text.trim()!==""}save(e){return{text:e.innerHTML,level:this.currentLevel.number}}static get conversionConfig(){return{export:"text",import:"text"}}static get sanitize(){return{level:!1,text:{}}}static get isReadOnlySupported(){return!0}get data(){return this._data.text=this._element.innerHTML,this._data.level=this.currentLevel.number,this._data}set data(e){if(this._data=this.normalizeData(e),e.level!==void 0&&this._element.parentNode){const t=this.getTag();t.innerHTML=this._element.innerHTML,this._element.parentNode.replaceChild(t,this._element),this._element=t}e.text!==void 0&&(this._element.innerHTML=this._data.text||"")}getTag(){var t;const e=document.createElement(this.currentLevel.tag);return e.innerHTML=this._data.text||"",e.classList.add(this._CSS.wrapper),e.contentEditable=this.readOnly?"false":"true",e.dataset.placeholder=this.api.i18n.t(((t=this._config)==null?void 0:t.placeholder)||""),e}get currentLevel(){let e=this.levels.find(t=>t.number===this._data.level);return e||(e=this.defaultLevel),e}get defaultLevel(){var e;if((e=this._config)!=null&&e.defaultLevel){const t=this.levels.find(i=>{var r;return i.number===((r=this._config)==null?void 0:r.defaultLevel)});if(t)return t;console.warn("(ง'̀-'́)ง Heading Tool: the default level specified was not found in available levels")}return this.levels[1]}get levels(){var t;const e=[{number:1,tag:"H1",svg:s},{number:2,tag:"H2",svg:d},{number:3,tag:"H3",svg:u},{number:4,tag:"H4",svg:c},{number:5,tag:"H5",svg:g},{number:6,tag:"H6",svg:v}];return(t=this._config)!=null&&t.levels?e.filter(i=>{var r;return(r=this._config)==null?void 0:r.levels.includes(i.number)}):e}onPaste(e){var i,r;const t=e.detail;if("data"in t){const l=t.data;let n=this.defaultLevel.number;switch(l.tagName){case"H1":n=1;break;case"H2":n=2;break;case"H3":n=3;break;case"H4":n=4;break;case"H5":n=5;break;case"H6":n=6;break}(i=this._config)!=null&&i.levels&&(n=(r=this._config)==null?void 0:r.levels.reduce((a,h)=>Math.abs(h-n)<Math.abs(a-n)?h:a)),this.data={level:n,text:l.innerHTML}}}static get pasteConfig(){return{tags:["H1","H2","H3","H4","H5","H6"]}}static get toolbox(){return{icon:w,title:"Heading"}}}return p});
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Skipped minification because the original files appears to be already minified.
|
||||
* Original file: /npm/@editorjs/marker@1.4.0/dist/marker.umd.js
|
||||
*
|
||||
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||
*/
|
||||
(function(){"use strict";try{if(typeof document<"u"){var e=document.createElement("style");e.appendChild(document.createTextNode(".cdx-marker{background:rgba(245,235,111,.29);padding:3px 0}")),document.head.appendChild(e)}}catch(d){console.error("vite-plugin-css-injected-by-js",d)}})();
|
||||
(function(i,s){typeof exports=="object"&&typeof module<"u"?module.exports=s():typeof define=="function"&&define.amd?define(s):(i=typeof globalThis<"u"?globalThis:i||self,i.Marker=s())})(this,function(){"use strict";const i="",s='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path stroke="currentColor" stroke-width="2" d="M11.3536 9.31802L12.7678 7.90381C13.5488 7.12276 14.8151 7.12276 15.5962 7.90381C16.3772 8.68486 16.3772 9.95119 15.5962 10.7322L14.182 12.1464M11.3536 9.31802L7.96729 12.7043C7.40889 13.2627 7.02827 13.9739 6.8734 14.7482L6.69798 15.6253C6.55804 16.325 7.17496 16.942 7.87468 16.802L8.75176 16.6266C9.52612 16.4717 10.2373 16.0911 10.7957 15.5327L14.182 12.1464M11.3536 9.31802L14.182 12.1464"/><line x1="15" x2="19" y1="17" y2="17" stroke="currentColor" stroke-linecap="round" stroke-width="2"/></svg>';class n{static get CSS(){return"cdx-marker"}constructor({api:t}){this.api=t,this.button=null,this.tag="MARK",this.iconClasses={base:this.api.styles.inlineToolButton,active:this.api.styles.inlineToolButtonActive}}static get isInline(){return!0}render(){return this.button=document.createElement("button"),this.button.type="button",this.button.classList.add(this.iconClasses.base),this.button.innerHTML=this.toolboxIcon,this.button}surround(t){if(!t)return;let e=this.api.selection.findParentTag(this.tag,n.CSS);e?this.unwrap(e):this.wrap(t)}wrap(t){let e=document.createElement(this.tag);e.classList.add(n.CSS),e.appendChild(t.extractContents()),t.insertNode(e),this.api.selection.expandToTag(e)}unwrap(t){this.api.selection.expandToTag(t);let e=window.getSelection(),o=e.getRangeAt(0),a=o.extractContents();t.parentNode.removeChild(t),o.insertNode(a),e.removeAllRanges(),e.addRange(o)}checkState(){const t=this.api.selection.findParentTag(this.tag,n.CSS);this.button.classList.toggle(this.iconClasses.active,!!t)}get toolboxIcon(){return s}static get sanitize(){return{mark:{class:n.CSS}}}}return n});
|
||||
+8
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,527 @@
|
||||
(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 frame = document.getElementById('slide-schedule-frame');
|
||||
var closeButton = document.getElementById('slide-schedule-close');
|
||||
var triggers = document.querySelectorAll('[data-schedule-config]');
|
||||
|
||||
if (!dialog || !frame || !closeButton || !triggers.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
function openScheduleModal(url) {
|
||||
frame.src = url;
|
||||
if (typeof dialog.showModal === 'function') {
|
||||
dialog.showModal();
|
||||
} else {
|
||||
dialog.setAttribute('open', 'open');
|
||||
}
|
||||
}
|
||||
|
||||
function resizeScheduleFrame() {
|
||||
try {
|
||||
if (!frame.contentWindow || !frame.contentWindow.document) {
|
||||
return;
|
||||
}
|
||||
var doc = frame.contentWindow.document;
|
||||
var height = Math.max(
|
||||
doc.body.scrollHeight,
|
||||
doc.documentElement.scrollHeight,
|
||||
doc.body.offsetHeight,
|
||||
doc.documentElement.offsetHeight
|
||||
);
|
||||
frame.style.height = height + 'px';
|
||||
} catch (_error) {
|
||||
frame.style.height = '70vh';
|
||||
}
|
||||
}
|
||||
|
||||
function closeScheduleModal() {
|
||||
frame.src = 'about:blank';
|
||||
if (typeof dialog.close === 'function') {
|
||||
dialog.close();
|
||||
} else {
|
||||
dialog.removeAttribute('open');
|
||||
}
|
||||
}
|
||||
|
||||
window.openScheduleModal = openScheduleModal;
|
||||
window.resizeScheduleFrame = resizeScheduleFrame;
|
||||
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') || '';
|
||||
if (rowKey && url.indexOf('row_key=') === -1) {
|
||||
url += (url.indexOf('?') === -1 ? '?' : '&') + 'row_key=' + encodeURIComponent(rowKey);
|
||||
}
|
||||
openScheduleModal(url);
|
||||
});
|
||||
});
|
||||
|
||||
frame.addEventListener('load', resizeScheduleFrame);
|
||||
closeButton.addEventListener('click', closeScheduleModal);
|
||||
}
|
||||
|
||||
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() {
|
||||
var form = document.querySelector('form[action*="/config"]');
|
||||
var select = document.getElementById('schedule-mode-select');
|
||||
if (!form || !select) {
|
||||
return;
|
||||
}
|
||||
|
||||
var datesPanel = document.getElementById('schedule-dates-panel');
|
||||
var timesPanel = document.getElementById('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 notifyParentResize() {
|
||||
if (window.parent && typeof window.parent.resizeScheduleFrame === 'function') {
|
||||
window.parent.resizeScheduleFrame();
|
||||
}
|
||||
}
|
||||
|
||||
function updateVisibility() {
|
||||
var mode = select.value;
|
||||
datesPanel.style.display = mode === 'dates' ? '' : 'none';
|
||||
timesPanel.style.display = mode === 'times' ? '' : 'none';
|
||||
window.requestAnimationFrame(function () {
|
||||
notifyParentResize();
|
||||
});
|
||||
}
|
||||
|
||||
select.addEventListener('change', updateVisibility);
|
||||
updateVisibility();
|
||||
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
var mode = select.value;
|
||||
var datesIncomplete = mode === 'dates' && (!startDateInput.value || !endDateInput.value);
|
||||
var timesIncomplete = mode === 'times' && (!startTimeInput.value || !endTimeInput.value || !Array.prototype.some.call(dayCheckboxes, function (checkbox) {
|
||||
return checkbox.checked;
|
||||
}));
|
||||
|
||||
if (mode === 'dates' && !datesIncomplete) {
|
||||
if (new Date(endDateInput.value) < new Date(startDateInput.value)) {
|
||||
alert('End datetime must be after start datetime.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (mode === 'times' && !timesIncomplete) {
|
||||
if (endTimeInput.value < startTimeInput.value) {
|
||||
alert('End time must be after start time.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (datesIncomplete || timesIncomplete) {
|
||||
mode = 'always';
|
||||
select.value = 'always';
|
||||
startDateInput.value = '';
|
||||
endDateInput.value = '';
|
||||
startTimeInput.value = '';
|
||||
endTimeInput.value = '';
|
||||
Array.prototype.forEach.call(dayCheckboxes, function (checkbox) {
|
||||
checkbox.checked = false;
|
||||
});
|
||||
updateVisibility();
|
||||
}
|
||||
|
||||
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 addDurationInput = document.getElementById('playlist-add-duration');
|
||||
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 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 (addDurationInput) {
|
||||
addDurationInput.disabled = availableCount === 0;
|
||||
}
|
||||
if (addSlideEmpty) {
|
||||
addSlideEmpty.style.display = availableCount === 0 ? '' : 'none';
|
||||
}
|
||||
if (addSection && addSlideForm && addSlideSelect && addSlideButton) {
|
||||
var controlsVisible = availableCount > 0;
|
||||
addSlideForm.style.display = controlsVisible ? '' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function updateRowOrder() {
|
||||
var rows = getRows();
|
||||
rows.forEach(function (row, index) {
|
||||
var orderCell = row.querySelector('.playlist-order-cell');
|
||||
var moveUp = row.querySelector('[data-playlist-move="up"]');
|
||||
var moveDown = row.querySelector('[data-playlist-move="down"]');
|
||||
if (orderCell) {
|
||||
orderCell.textContent = String(index + 1);
|
||||
}
|
||||
if (moveUp) {
|
||||
moveUp.disabled = index === 0;
|
||||
}
|
||||
if (moveDown) {
|
||||
moveDown.disabled = index === rows.length - 1;
|
||||
}
|
||||
});
|
||||
syncAddSlideOptions();
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function createRow(values) {
|
||||
var row = document.createElement('tr');
|
||||
var rowKey = values.row_key || ('new-' + Date.now() + '-' + Math.random().toString(36).slice(2));
|
||||
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"></td>' +
|
||||
'<td>' + values.title + '<input type="hidden" name="slide_id[]" value="' + values.slide_id + '" /></td>' +
|
||||
'<td><input name="duration_seconds[]" type="number" min="1" value="' + values.duration_seconds + '" required /></td>' +
|
||||
'<td>' +
|
||||
'<div class="playlist-schedule-summary">' + values.summary + '</div>' +
|
||||
'<input type="hidden" name="schedule_mode[]" value="' + values.schedule_mode + '" />' +
|
||||
'<input type="hidden" name="schedule_start_datetime[]" value="' + values.schedule_start_datetime + '" />' +
|
||||
'<input type="hidden" name="schedule_end_datetime[]" value="' + values.schedule_end_datetime + '" />' +
|
||||
'<input type="hidden" name="schedule_start_time[]" value="' + values.schedule_start_time + '" />' +
|
||||
'<input type="hidden" name="schedule_end_time[]" value="' + values.schedule_end_time + '" />' +
|
||||
'<input type="hidden" name="schedule_days_json[]" value="' + values.schedule_days_json + '" />' +
|
||||
'</td>' +
|
||||
'<td><div class="actions playlist-item-actions">' +
|
||||
'<button type="button" class="secondary" data-playlist-move="up">Up</button>' +
|
||||
'<button type="button" class="secondary" data-playlist-move="down">Down</button>' +
|
||||
'<button type="button" class="schedule-config-button" disabled>Schedule</button>' +
|
||||
'<button type="button" class="danger" data-playlist-remove-row>Remove</button>' +
|
||||
'</div></td>';
|
||||
return row;
|
||||
}
|
||||
|
||||
tbody.addEventListener('click', function (event) {
|
||||
var moveButton = event.target.closest('[data-playlist-move]');
|
||||
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 (moveButton) {
|
||||
event.preventDefault();
|
||||
var direction = moveButton.getAttribute('data-playlist-move');
|
||||
if (direction === 'up' && row.previousElementSibling && row.previousElementSibling.matches('[data-playlist-slide-row]')) {
|
||||
tbody.insertBefore(row, row.previousElementSibling);
|
||||
} else if (direction === 'down' && row.nextElementSibling && row.nextElementSibling.matches('[data-playlist-slide-row]')) {
|
||||
tbody.insertBefore(row.nextElementSibling, row);
|
||||
}
|
||||
updateRowOrder();
|
||||
return;
|
||||
}
|
||||
|
||||
if (removeButton) {
|
||||
event.preventDefault();
|
||||
row.remove();
|
||||
updateRowOrder();
|
||||
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 (addSlideButton && addSlideSelect && addDurationInput) {
|
||||
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: Math.max(1, Number(addDurationInput.value || 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();
|
||||
});
|
||||
}
|
||||
|
||||
window.applyPlaylistScheduleConfig = function (values) {
|
||||
var row = findRowByKey(String(values.row_key || ''));
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
setRowSchedule(row, values);
|
||||
};
|
||||
|
||||
updateRowOrder();
|
||||
}
|
||||
|
||||
initPlaylistScheduleModal();
|
||||
initPlaylistScheduleForm();
|
||||
initPlaylistEditStaging();
|
||||
}());
|
||||
@@ -0,0 +1,555 @@
|
||||
(function () {
|
||||
var dataElement = document.getElementById('slide-editor-data');
|
||||
if (!dataElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
var slideEditorData = {};
|
||||
try {
|
||||
slideEditorData = JSON.parse(dataElement.getAttribute('data-json') || dataElement.textContent || '{}') || {};
|
||||
} catch (_error) {
|
||||
slideEditorData = {};
|
||||
}
|
||||
|
||||
var templates = Array.isArray(slideEditorData.templates) ? slideEditorData.templates : [];
|
||||
var existingTemplateId = slideEditorData.existingTemplateId !== undefined ? slideEditorData.existingTemplateId : null;
|
||||
var existingContent = slideEditorData.existingContent || {};
|
||||
var templateSelect = document.getElementById('template-select');
|
||||
var templateFields = document.getElementById('template-fields');
|
||||
var slideForm = document.getElementById('slide-form');
|
||||
var slidePreviewMeta = document.getElementById('slide-preview-meta');
|
||||
var slidePreviewStage = document.getElementById('slide-preview-stage');
|
||||
var slidePreviewBackground = document.getElementById('slide-preview-background');
|
||||
var slidePreviewEmpty = document.getElementById('slide-preview-empty');
|
||||
var slidePreviewOverlay = document.getElementById('slide-preview-overlay');
|
||||
var editorInstances = new Map();
|
||||
var submitting = false;
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function getTemplateById(id) {
|
||||
return templates.find(function (item) { return Number(item.id) === Number(id); }) || null;
|
||||
}
|
||||
|
||||
function getCurrentRegionValue(region) {
|
||||
var current = existingContent[region.region_key];
|
||||
return current && current.value !== undefined ? current.value : '';
|
||||
}
|
||||
|
||||
function normalizeEditorData(value) {
|
||||
var raw = String(value || '');
|
||||
if (!raw) {
|
||||
return { time: Date.now(), blocks: [] };
|
||||
}
|
||||
try {
|
||||
var parsed = JSON.parse(raw);
|
||||
if (parsed && Array.isArray(parsed.blocks)) {
|
||||
return parsed;
|
||||
}
|
||||
} catch (_error) {
|
||||
// fall through to legacy HTML/plain text
|
||||
}
|
||||
return {
|
||||
time: Date.now(),
|
||||
blocks: [{
|
||||
type: 'paragraph',
|
||||
data: { text: raw }
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizePreviewHtml(html) {
|
||||
var output = String(html || '');
|
||||
output = output.replace(/<script[\s\S]*?<\/script>/gi, '');
|
||||
output = output.replace(/<style[\s\S]*?<\/style>/gi, '');
|
||||
return output.replace(/<[^>]+>/g, function (tag) {
|
||||
var match = tag.match(/^<\s*(\/?)\s*([a-z0-9]+)(?:\s[^>]*)?>$/i);
|
||||
if (!match) {
|
||||
return '';
|
||||
}
|
||||
var closing = Boolean(match[1]);
|
||||
var name = String(match[2] || '').toLowerCase();
|
||||
var allowed = ['b', 'strong', 'i', 'em', 'u', 'br', 'p', 'div', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre', 'code', 'table', 'thead', 'tbody', 'tr', 'th', 'td'];
|
||||
if (allowed.indexOf(name) === -1) {
|
||||
return '';
|
||||
}
|
||||
if (name === 'br') {
|
||||
return '<br>';
|
||||
}
|
||||
return closing ? '</' + name + '>' : '<' + name + '>';
|
||||
});
|
||||
}
|
||||
|
||||
function renderPreviewListItem(item, tag) {
|
||||
if (item && typeof item === 'object') {
|
||||
var content = item.content !== undefined ? item.content : (item.text !== undefined ? item.text : item.value !== undefined ? item.value : '');
|
||||
var children = Array.isArray(item.items) ? item.items : Array.isArray(item.subItems) ? item.subItems : Array.isArray(item.children) ? item.children : [];
|
||||
var nested = children.length ? '<' + tag + ' style="list-style-type:' + (tag === 'ol' ? 'decimal' : 'disc') + ';padding-left:1.4em;">' + children.map(function (child) { return renderPreviewListItem(child, tag); }).join('') + '</' + tag + '>' : '';
|
||||
return '<li>' + sanitizePreviewHtml(content || '') + nested + '</li>';
|
||||
}
|
||||
return '<li>' + sanitizePreviewHtml(item || '') + '</li>';
|
||||
}
|
||||
|
||||
function renderPreviewTable(data) {
|
||||
var rows = Array.isArray(data.content) ? data.content : Array.isArray(data.rows) ? data.rows : [];
|
||||
if (!rows.length) {
|
||||
return '';
|
||||
}
|
||||
var hasHeadings = Boolean(data.withHeadings);
|
||||
var body = rows.map(function (row, rowIndex) {
|
||||
var cells = Array.isArray(row) ? row : [];
|
||||
var cellTag = hasHeadings && rowIndex === 0 ? 'th' : 'td';
|
||||
var cellAttrs = cellTag === 'th' ? ' scope="col"' : '';
|
||||
return '<tr>' + cells.map(function (cell) {
|
||||
return '<' + cellTag + cellAttrs + '>' + sanitizePreviewHtml(cell || '') + '</' + cellTag + '>';
|
||||
}).join('') + '</tr>';
|
||||
}).join('');
|
||||
return '<table class="slide-preview-table">' + body + '</table>';
|
||||
}
|
||||
|
||||
function renderPreviewBlock(block) {
|
||||
if (!block || !block.type || !block.data) {
|
||||
return '';
|
||||
}
|
||||
if (block.type === 'header') {
|
||||
var level = Math.max(1, Math.min(6, Number(block.data.level || 2)));
|
||||
return '<h' + level + '>' + sanitizePreviewHtml(block.data.text || '') + '</h' + level + '>';
|
||||
}
|
||||
if (block.type === 'list') {
|
||||
var tag = block.data.style === 'ordered' ? 'ol' : 'ul';
|
||||
var items = Array.isArray(block.data.items) ? block.data.items : [];
|
||||
return '<' + tag + ' style="list-style-type:' + (tag === 'ol' ? 'decimal' : 'disc') + ';padding-left:1.4em;">' + items.map(function (item) { return renderPreviewListItem(item, tag); }).join('') + '</' + tag + '>';
|
||||
}
|
||||
if (block.type === 'delimiter') {
|
||||
return '<hr />';
|
||||
}
|
||||
if (block.type === 'code') {
|
||||
return '<pre><code>' + sanitizePreviewHtml(block.data.code || '') + '</code></pre>';
|
||||
}
|
||||
if (block.type === 'table') {
|
||||
return renderPreviewTable(block.data);
|
||||
}
|
||||
if (block.type === 'paragraph') {
|
||||
return '<p>' + sanitizePreviewHtml(block.data.text || '') + '</p>';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function renderPreviewText(value) {
|
||||
var raw = String(value || '');
|
||||
if (!raw) {
|
||||
return '<div class="slide-preview-placeholder">Empty text</div>';
|
||||
}
|
||||
try {
|
||||
var parsed = JSON.parse(raw);
|
||||
if (parsed && Array.isArray(parsed.blocks)) {
|
||||
return parsed.blocks.map(renderPreviewBlock).join('');
|
||||
}
|
||||
} catch (_error) {
|
||||
// fall through to raw text
|
||||
}
|
||||
return '<p>' + sanitizePreviewHtml(raw) + '</p>';
|
||||
}
|
||||
|
||||
function renderPreviewTextRegion(region, value, style, scale) {
|
||||
var fontFamily = style.font_family ? 'font-family:' + escapeHtml(style.font_family) + ';' : '';
|
||||
var fontSize = style.font_size ? 'font-size:' + Math.max(1, Math.round(Number(style.font_size) * scale)) + 'px;' : '';
|
||||
var color = style.font_color ? 'color:' + escapeHtml(style.font_color) + ';' : '';
|
||||
return '<div class="slide-preview-text-content" style="' + fontFamily + fontSize + color + '">' + renderPreviewText(value) + '</div>';
|
||||
}
|
||||
|
||||
function renderPreviewWebpageRegion(value) {
|
||||
var src = String(value || '').trim();
|
||||
if (!src) {
|
||||
return '<div class="slide-preview-placeholder">Webpage</div>';
|
||||
}
|
||||
return '<iframe class="slide-preview-webpage-frame" src="' + escapeHtml(src) + '" title="Webpage preview" loading="eager" referrerpolicy="no-referrer" scrolling="no"></iframe>';
|
||||
}
|
||||
|
||||
function renderPreviewHtmlRegion(value) {
|
||||
var html = String(value || '').trim();
|
||||
if (!html) {
|
||||
return '<div class="slide-preview-placeholder">HTML</div>';
|
||||
}
|
||||
return '<iframe class="slide-preview-html-frame" sandbox="" scrolling="no" srcdoc="<!doctype html><html><head><style>html,body{margin:0;width:100%;height:100%;overflow:hidden;background:transparent;}</style></head><body>' + escapeHtml(html) + '</body></html>" title="HTML preview" loading="eager"></iframe>';
|
||||
}
|
||||
|
||||
function destroyEditors() {
|
||||
editorInstances.forEach(function (editor) {
|
||||
if (editor && typeof editor.destroy === 'function') {
|
||||
editor.destroy();
|
||||
}
|
||||
});
|
||||
editorInstances.clear();
|
||||
}
|
||||
|
||||
function getEditorHiddenInput(regionId) {
|
||||
var card = templateFields.querySelector('[data-region-id="' + regionId + '"]');
|
||||
return card ? card.querySelector('input[type="hidden"][name="region_text_' + regionId + '"]') : null;
|
||||
}
|
||||
|
||||
function getCurrentTextStyle(region) {
|
||||
var current = existingContent[region.region_key] || {};
|
||||
return {
|
||||
font_family: String(current.font_family || region.font_family || 'Arial').trim() || 'Arial',
|
||||
font_size: Math.max(8, Number(current.font_size || 24)),
|
||||
font_color: String(current.font_color || region.font_color || '#000000').trim() || '#000000'
|
||||
};
|
||||
}
|
||||
|
||||
function getTextStyleFromCard(card, region) {
|
||||
var familyInput = card.querySelector('[name="region_font_family_' + region.id + '"]');
|
||||
var sizeInput = card.querySelector('[name="region_font_size_' + region.id + '"]');
|
||||
var colorInput = card.querySelector('[name="region_font_color_' + region.id + '"]');
|
||||
var current = getCurrentTextStyle(region);
|
||||
return {
|
||||
font_family: String(familyInput ? familyInput.value : current.font_family).trim() || current.font_family,
|
||||
font_size: Math.max(8, Number(sizeInput ? sizeInput.value : current.font_size)),
|
||||
font_color: String(colorInput ? colorInput.value : current.font_color).trim() || current.font_color
|
||||
};
|
||||
}
|
||||
|
||||
function getSelectedTemplate() {
|
||||
return templates.find(function (item) { return Number(item.id) === Number(templateSelect.value); }) || null;
|
||||
}
|
||||
|
||||
function clearImagePreviewUrls() {
|
||||
templateFields.querySelectorAll('input[type="file"][name^="region_image_"]').forEach(function (input) {
|
||||
if (input.dataset.previewUrl) {
|
||||
URL.revokeObjectURL(input.dataset.previewUrl);
|
||||
delete input.dataset.previewUrl;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getRegionBox(region, stageRect, canvasWidth, canvasHeight) {
|
||||
return {
|
||||
left: (Number(region.x || 0) / canvasWidth) * stageRect.width,
|
||||
top: (Number(region.y || 0) / canvasHeight) * stageRect.height,
|
||||
width: (Number(region.width || 0) / canvasWidth) * stageRect.width,
|
||||
height: (Number(region.height || 0) / canvasHeight) * stageRect.height
|
||||
};
|
||||
}
|
||||
|
||||
function renderPreview() {
|
||||
var template = getSelectedTemplate();
|
||||
if (!template) {
|
||||
slidePreviewMeta.textContent = 'Select a template to preview the slide.';
|
||||
slidePreviewBackground.style.display = 'none';
|
||||
slidePreviewEmpty.style.display = 'flex';
|
||||
slidePreviewEmpty.textContent = 'Select a template to preview the slide.';
|
||||
slidePreviewOverlay.innerHTML = '';
|
||||
slidePreviewStage.style.aspectRatio = '16 / 9';
|
||||
return;
|
||||
}
|
||||
|
||||
var canvasWidth = Math.max(1, Number(template.canvas_size_width || 1920));
|
||||
var canvasHeight = Math.max(1, Number(template.canvas_size_height || 1080));
|
||||
slidePreviewStage.style.aspectRatio = canvasWidth + ' / ' + canvasHeight;
|
||||
slidePreviewMeta.textContent = template.name + ' • ' + canvasWidth + 'x' + canvasHeight;
|
||||
if (!(template.regions || []).length) {
|
||||
slidePreviewEmpty.style.display = 'flex';
|
||||
slidePreviewEmpty.textContent = 'This template has no regions.';
|
||||
slidePreviewBackground.style.display = template.background_image_path ? 'block' : 'none';
|
||||
slidePreviewBackground.src = template.background_image_path || '';
|
||||
slidePreviewOverlay.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
slidePreviewEmpty.style.display = 'none';
|
||||
slidePreviewBackground.style.display = template.background_image_path ? 'block' : 'none';
|
||||
slidePreviewBackground.src = template.background_image_path || '';
|
||||
|
||||
var stageRect = slidePreviewStage.getBoundingClientRect();
|
||||
var scale = Math.min(stageRect.width / canvasWidth, stageRect.height / canvasHeight) || 1;
|
||||
var regions = template.regions || [];
|
||||
slidePreviewOverlay.innerHTML = regions.map(function (region) {
|
||||
var box = getRegionBox(region, stageRect, canvasWidth, canvasHeight);
|
||||
var card = templateFields.querySelector('[data-region-id="' + region.id + '"]');
|
||||
var hiddenInput = card && card.querySelector('input[type="hidden"][name="region_text_' + region.id + '"]');
|
||||
var htmlInput = card && card.querySelector('textarea[name="region_html_' + region.id + '"]');
|
||||
var imageHidden = card && card.querySelector('input[type="hidden"][name="existing_region_image_' + region.id + '"]');
|
||||
var imageFileInput = card && card.querySelector('input[type="file"][name="region_image_' + region.id + '"]');
|
||||
var webpageInput = card && card.querySelector('input[type="url"][name="region_webpage_' + region.id + '"]');
|
||||
var value = region.region_type === 'image'
|
||||
? ((imageFileInput && imageFileInput.dataset.previewUrl) ? imageFileInput.dataset.previewUrl : (imageHidden ? imageHidden.value : ''))
|
||||
: region.region_type === 'webpage'
|
||||
? (webpageInput ? webpageInput.value : '')
|
||||
: region.region_type === 'html'
|
||||
? (htmlInput ? htmlInput.value : '')
|
||||
: (hiddenInput ? hiddenInput.value : '');
|
||||
var style = region.region_type === 'image'
|
||||
? null
|
||||
: region.region_type === 'webpage'
|
||||
? null
|
||||
: region.region_type === 'html'
|
||||
? null
|
||||
: getTextStyleFromCard(card, region);
|
||||
var content = region.region_type === 'image'
|
||||
? (value ? '<img class="slide-preview-image" src="' + escapeHtml(value) + '" alt="" />' : '<div class="slide-preview-placeholder">Image</div>')
|
||||
: region.region_type === 'webpage'
|
||||
? renderPreviewWebpageRegion(value)
|
||||
: region.region_type === 'html'
|
||||
? renderPreviewHtmlRegion(value)
|
||||
: renderPreviewTextRegion(region, value, style, scale);
|
||||
var selected = region.region_type === 'image'
|
||||
? ' slide-preview-image-region'
|
||||
: region.region_type === 'webpage'
|
||||
? ' slide-preview-webpage-region'
|
||||
: region.region_type === 'html'
|
||||
? ' slide-preview-html-region'
|
||||
: ' slide-preview-text-region';
|
||||
return '<div class="slide-preview-region' + selected + '" style="left:' + box.left + 'px;top:' + box.top + 'px;width:' + box.width + 'px;height:' + box.height + 'px;">' + content + '</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderTextRegion(region) {
|
||||
var current = getCurrentRegionValue(region);
|
||||
var style = getCurrentTextStyle(region);
|
||||
var fontChoices = [
|
||||
'Arial',
|
||||
'Courier New',
|
||||
'Garamond',
|
||||
'Georgia',
|
||||
'Helvetica',
|
||||
'Impact',
|
||||
'Lucida Console',
|
||||
'Palatino',
|
||||
'Tahoma',
|
||||
'Times New Roman',
|
||||
'Trebuchet MS',
|
||||
'Verdana'
|
||||
];
|
||||
return '' +
|
||||
'<div class="card template-field-card" data-region-id="' + region.id + '">' +
|
||||
'<div class="template-field-head">' +
|
||||
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
||||
'<span class="chip">Text</span>' +
|
||||
'</div>' +
|
||||
'<div class="row template-text-style-row">' +
|
||||
'<label style="flex: 1;">Font style' +
|
||||
'<select name="region_font_family_' + region.id + '">' + fontChoices.map(function (font) {
|
||||
return '<option value="' + escapeHtml(font) + '"' + (style.font_family === font ? ' selected' : '') + '>' + escapeHtml(font) + '</option>';
|
||||
}).join('') + '</select>' +
|
||||
'</label>' +
|
||||
'<label style="width: 120px;">Size' +
|
||||
'<input type="number" name="region_font_size_' + region.id + '" min="8" max="180" step="1" value="' + escapeHtml(style.font_size) + '" />' +
|
||||
'</label>' +
|
||||
'<label style="width: 140px;">Color' +
|
||||
'<input type="color" name="region_font_color_' + region.id + '" value="' + escapeHtml(style.font_color) + '" />' +
|
||||
'</label>' +
|
||||
'</div>' +
|
||||
'<div class="editorjs-holder" id="editorjs-' + region.id + '" data-region-id="' + region.id + '"></div>' +
|
||||
'<input type="hidden" name="region_text_' + region.id + '" value="' + escapeHtml(current) + '" />' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function renderWebpageRegion(region) {
|
||||
var current = getCurrentRegionValue(region);
|
||||
return '' +
|
||||
'<div class="card template-field-card" data-region-id="' + region.id + '">' +
|
||||
'<div class="template-field-head">' +
|
||||
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
||||
'<span class="chip">Webpage</span>' +
|
||||
'</div>' +
|
||||
'<label style="display:block; margin-top:12px;">Webpage URL' +
|
||||
'<input type="url" name="region_webpage_' + region.id + '" value="' + escapeHtml(current) + '" placeholder="https://example.com" />' +
|
||||
'</label>' +
|
||||
'<div class="muted slide-image-file">The webpage will be loaded in an iframe.</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function renderHtmlRegion(region) {
|
||||
var current = getCurrentRegionValue(region);
|
||||
return '' +
|
||||
'<div class="card template-field-card" data-region-id="' + region.id + '">' +
|
||||
'<div class="template-field-head">' +
|
||||
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
||||
'<span class="chip">HTML</span>' +
|
||||
'</div>' +
|
||||
'<label style="display:block; margin-top:12px;">HTML content' +
|
||||
'<textarea name="region_html_' + region.id + '" rows="10" placeholder="<div>Hello</div>">' + escapeHtml(current) + '</textarea>' +
|
||||
'</label>' +
|
||||
'<div class="muted slide-image-file">HTML is rendered in a sandboxed iframe preview.</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function renderImageRegion(region) {
|
||||
var current = getCurrentRegionValue(region);
|
||||
return '' +
|
||||
'<div class="card template-field-card" data-region-id="' + region.id + '">' +
|
||||
'<div class="template-field-head">' +
|
||||
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
||||
'<span class="chip">Image</span>' +
|
||||
'</div>' +
|
||||
'<label style="display:block; margin-top:12px;">Image file' +
|
||||
'<input type="file" name="region_image_' + region.id + '" accept="image/*" />' +
|
||||
'</label>' +
|
||||
'<input type="hidden" name="existing_region_image_' + region.id + '" value="' + escapeHtml(current) + '" />' +
|
||||
(current
|
||||
? '<div class="muted slide-image-file">Current: <a href="' + escapeHtml(current) + '" target="_blank" rel="noreferrer">' + escapeHtml(current) + '</a></div>'
|
||||
: '<div class="muted slide-image-file">No image selected.</div>') +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function renderTemplate() {
|
||||
clearImagePreviewUrls();
|
||||
destroyEditors();
|
||||
var template = getTemplateById(templateSelect.value);
|
||||
if (!template) {
|
||||
templateFields.innerHTML = '';
|
||||
renderPreview();
|
||||
return;
|
||||
}
|
||||
|
||||
templateFields.innerHTML = template.regions.length ? template.regions.map(function (region) {
|
||||
if (region.region_type === 'image') {
|
||||
return renderImageRegion(region);
|
||||
}
|
||||
if (region.region_type === 'webpage') {
|
||||
return renderWebpageRegion(region);
|
||||
}
|
||||
if (region.region_type === 'html') {
|
||||
return renderHtmlRegion(region);
|
||||
}
|
||||
return renderTextRegion(region);
|
||||
}).join('') : '<div class="muted">This template has no regions.</div>';
|
||||
|
||||
templateFields.querySelectorAll('.editorjs-holder').forEach(function (holder) {
|
||||
var regionId = holder.getAttribute('data-region-id');
|
||||
var hidden = getEditorHiddenInput(regionId);
|
||||
var editor = new EditorJS({
|
||||
holder: holder.id,
|
||||
minHeight: 120,
|
||||
placeholder: 'Write content...',
|
||||
inlineToolbar: true,
|
||||
tools: {
|
||||
header: {
|
||||
class: Header,
|
||||
config: {
|
||||
placeholder: 'Enter a header',
|
||||
levels: [1, 2, 3, 4],
|
||||
defaultLevel: 3
|
||||
}
|
||||
},
|
||||
list: {
|
||||
class: EditorjsList,
|
||||
inlineToolbar: false,
|
||||
config: {
|
||||
defaultStyle: 'unordered'
|
||||
}
|
||||
},
|
||||
table: {
|
||||
class: Table,
|
||||
inlineToolbar: true,
|
||||
config: {
|
||||
rows: 2,
|
||||
cols: 3,
|
||||
maxRows: 5,
|
||||
maxCols: 5
|
||||
}
|
||||
},
|
||||
marker: {
|
||||
class: Marker
|
||||
}
|
||||
},
|
||||
data: normalizeEditorData(hidden ? hidden.value : ''),
|
||||
onChange: function () {
|
||||
var instance = editorInstances.get(regionId);
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
instance.save().then(function (output) {
|
||||
if (hidden) {
|
||||
hidden.value = JSON.stringify(output);
|
||||
}
|
||||
renderPreview();
|
||||
});
|
||||
}
|
||||
});
|
||||
editorInstances.set(regionId, editor);
|
||||
});
|
||||
|
||||
templateFields.querySelectorAll('input[type="file"][name^="region_image_"]').forEach(function (input) {
|
||||
input.addEventListener('change', function () {
|
||||
if (input.dataset.previewUrl) {
|
||||
URL.revokeObjectURL(input.dataset.previewUrl);
|
||||
delete input.dataset.previewUrl;
|
||||
}
|
||||
if (input.files && input.files.length) {
|
||||
input.dataset.previewUrl = URL.createObjectURL(input.files[0]);
|
||||
}
|
||||
renderPreview();
|
||||
});
|
||||
});
|
||||
renderPreview();
|
||||
}
|
||||
|
||||
slideForm.addEventListener('submit', async function (event) {
|
||||
if (submitting) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
var saves = Array.prototype.map.call(templateFields.querySelectorAll('.editorjs-holder'), function (holder) {
|
||||
var regionId = holder.getAttribute('data-region-id');
|
||||
var editor = editorInstances.get(regionId);
|
||||
var hidden = getEditorHiddenInput(regionId);
|
||||
if (!editor || !hidden) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return editor.save().then(function (output) {
|
||||
hidden.value = JSON.stringify(output);
|
||||
});
|
||||
});
|
||||
submitting = true;
|
||||
try {
|
||||
await Promise.all(saves);
|
||||
var response = await fetch(slideForm.action, {
|
||||
method: (slideForm.method || 'POST').toUpperCase(),
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'text/html, application/json, text/plain, */*'
|
||||
},
|
||||
body: new FormData(slideForm),
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await response.text() || 'Unable to save slide.');
|
||||
}
|
||||
|
||||
var responseText = await response.text();
|
||||
var savedMessage = '';
|
||||
try {
|
||||
var doc = new DOMParser().parseFromString(responseText || '', 'text/html');
|
||||
var toastBody = doc.querySelector('.toast-body');
|
||||
if (toastBody && toastBody.textContent) {
|
||||
savedMessage = toastBody.textContent.trim();
|
||||
}
|
||||
} catch (_error) {
|
||||
savedMessage = '';
|
||||
}
|
||||
|
||||
showToast(savedMessage || 'Saved slide.');
|
||||
window.location.href = response.url || slideForm.action;
|
||||
} finally {
|
||||
submitting = false;
|
||||
}
|
||||
});
|
||||
|
||||
templateSelect.addEventListener('change', renderTemplate);
|
||||
templateFields.addEventListener('change', renderPreview);
|
||||
templateFields.addEventListener('input', renderPreview);
|
||||
if (existingTemplateId) {
|
||||
templateSelect.value = String(existingTemplateId);
|
||||
}
|
||||
renderTemplate();
|
||||
window.addEventListener('resize', renderPreview);
|
||||
})();
|
||||
@@ -0,0 +1,99 @@
|
||||
(function () {
|
||||
function updateSidebarStatus(status, label) {
|
||||
var dot = document.getElementById('sidebar-status-dot');
|
||||
var text = document.getElementById('sidebar-status-text');
|
||||
var pill = document.getElementById('sidebar-status-pill');
|
||||
if (!dot && !text && !pill) {
|
||||
return;
|
||||
}
|
||||
|
||||
var normalizedStatus = status === 'online' || status === 'offline' || status === 'unknown'
|
||||
? status
|
||||
: (status ? 'online' : 'offline');
|
||||
var statusLabel = normalizedStatus === 'online'
|
||||
? (label || 'Connected to player feed')
|
||||
: normalizedStatus === 'offline'
|
||||
? 'Disconnected from player feed'
|
||||
: (label || 'Connecting to player feed');
|
||||
var pillLabel = normalizedStatus === 'online' ? 'Connected' : normalizedStatus === 'offline' ? 'Disconnected' : 'Connecting';
|
||||
|
||||
dot.classList.remove('status-dot--unknown', 'status-dot--online', 'status-dot--offline');
|
||||
dot.classList.add(normalizedStatus === 'online' ? 'status-dot--online' : normalizedStatus === 'offline' ? 'status-dot--offline' : 'status-dot--unknown');
|
||||
dot.setAttribute('aria-label', statusLabel);
|
||||
dot.setAttribute('title', statusLabel);
|
||||
|
||||
if (text) {
|
||||
text.textContent = statusLabel;
|
||||
}
|
||||
|
||||
if (pill) {
|
||||
pill.classList.remove('status-pill--unknown', 'status-pill--online', 'status-pill--offline');
|
||||
pill.classList.add(normalizedStatus === 'online' ? 'status-pill--online' : normalizedStatus === 'offline' ? 'status-pill--offline' : 'status-pill--unknown');
|
||||
pill.textContent = pillLabel;
|
||||
}
|
||||
}
|
||||
|
||||
function connectDashboardSocket() {
|
||||
if (!window.WebSocket) {
|
||||
return;
|
||||
}
|
||||
|
||||
var hasStatusIndicators = document.getElementById('sidebar-status-dot') || document.getElementById('sidebar-status-text') || document.getElementById('sidebar-status-pill');
|
||||
if (!hasStatusIndicators) {
|
||||
return;
|
||||
}
|
||||
|
||||
var socket = null;
|
||||
var reconnectTimer = null;
|
||||
|
||||
function scheduleReconnect() {
|
||||
if (reconnectTimer) {
|
||||
return;
|
||||
}
|
||||
reconnectTimer = window.setTimeout(function () {
|
||||
reconnectTimer = null;
|
||||
connect();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function connect() {
|
||||
var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
socket = new WebSocket(protocol + '//' + window.location.host + '/ws/admin/dashboard');
|
||||
updateSidebarStatus('unknown', 'Connecting to player feed');
|
||||
|
||||
socket.onmessage = function (event) {
|
||||
try {
|
||||
var payload = JSON.parse(String(event.data || '{}'));
|
||||
if (payload && payload.type === 'dashboard-state' && typeof window.webHandleDashboardState === 'function') {
|
||||
window.webHandleDashboardState(payload.state);
|
||||
updateSidebarStatus(Boolean(payload.state && payload.state.playerServiceConnected), 'Live player feed');
|
||||
}
|
||||
} catch (_error) {
|
||||
// Ignore malformed dashboard payloads.
|
||||
}
|
||||
};
|
||||
|
||||
socket.onclose = function () {
|
||||
updateSidebarStatus('offline');
|
||||
scheduleReconnect();
|
||||
};
|
||||
|
||||
socket.onerror = function () {
|
||||
updateSidebarStatus('offline');
|
||||
try {
|
||||
socket.close();
|
||||
} catch (_error) {
|
||||
// ignore close errors
|
||||
}
|
||||
};
|
||||
|
||||
socket.onopen = function () {
|
||||
updateSidebarStatus('unknown', 'Connecting to player feed');
|
||||
};
|
||||
}
|
||||
|
||||
connect();
|
||||
}
|
||||
|
||||
connectDashboardSocket();
|
||||
}());
|
||||
@@ -0,0 +1,194 @@
|
||||
(function () {
|
||||
function getTableHeaderText(headerCell) {
|
||||
return String(headerCell && headerCell.textContent ? headerCell.textContent : '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function cellHasButtonContent(cell) {
|
||||
return Boolean(cell && cell.querySelector && cell.querySelector('button, form, input, select, textarea'));
|
||||
}
|
||||
|
||||
function getCellSortValue(cell) {
|
||||
if (!cell) {
|
||||
return '';
|
||||
}
|
||||
var sortValue = cell.getAttribute && cell.getAttribute('data-sort-value');
|
||||
if (sortValue !== null && sortValue !== undefined && sortValue !== '') {
|
||||
return String(sortValue).trim();
|
||||
}
|
||||
return String(cell.textContent || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function getComparableSortValue(rawValue) {
|
||||
var value = String(rawValue || '').trim();
|
||||
if (!value) {
|
||||
return { type: 'empty', value: '' };
|
||||
}
|
||||
|
||||
var numericValue = Number(value.replace(/,/g, ''));
|
||||
if (!Number.isNaN(numericValue) && value !== '') {
|
||||
return { type: 'number', value: numericValue };
|
||||
}
|
||||
|
||||
var dateValue = Date.parse(value);
|
||||
if (!Number.isNaN(dateValue)) {
|
||||
return { type: 'date', value: dateValue };
|
||||
}
|
||||
|
||||
return { type: 'string', value: value.toLowerCase() };
|
||||
}
|
||||
|
||||
function compareSortValues(leftValue, rightValue) {
|
||||
if (leftValue.type === 'empty' && rightValue.type === 'empty') {
|
||||
return 0;
|
||||
}
|
||||
if (leftValue.type === 'empty') {
|
||||
return 1;
|
||||
}
|
||||
if (rightValue.type === 'empty') {
|
||||
return -1;
|
||||
}
|
||||
if (leftValue.type === rightValue.type) {
|
||||
if (leftValue.value < rightValue.value) {
|
||||
return -1;
|
||||
}
|
||||
if (leftValue.value > rightValue.value) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return String(leftValue.value).localeCompare(String(rightValue.value), undefined, { numeric: true, sensitivity: 'base' });
|
||||
}
|
||||
|
||||
function getSortableTableState(table) {
|
||||
if (!table._sortableState) {
|
||||
table._sortableState = {
|
||||
columnIndex: null,
|
||||
direction: 'asc'
|
||||
};
|
||||
}
|
||||
return table._sortableState;
|
||||
}
|
||||
|
||||
function isSortableTableColumn(table, columnIndex) {
|
||||
var headerCell = table.tHead && table.tHead.rows && table.tHead.rows.length ? table.tHead.rows[0].cells[columnIndex] : null;
|
||||
if (!headerCell) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (/actions?|buttons?/i.test(getTableHeaderText(headerCell))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var bodies = table.tBodies ? Array.prototype.slice.call(table.tBodies) : [];
|
||||
for (var i = 0; i < bodies.length; i += 1) {
|
||||
var rows = Array.prototype.slice.call(bodies[i].rows || []);
|
||||
for (var j = 0; j < rows.length; j += 1) {
|
||||
var cell = rows[j].cells ? rows[j].cells[columnIndex] : null;
|
||||
if (cell && cellHasButtonContent(cell)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateSortableHeaderState(table) {
|
||||
var state = getSortableTableState(table);
|
||||
var headerCells = table.tHead && table.tHead.rows && table.tHead.rows.length ? Array.prototype.slice.call(table.tHead.rows[0].cells || []) : [];
|
||||
headerCells.forEach(function (headerCell, index) {
|
||||
if (!headerCell) {
|
||||
return;
|
||||
}
|
||||
headerCell.classList.remove('sort-asc', 'sort-desc', 'sortable', 'unsortable');
|
||||
headerCell.removeAttribute('aria-sort');
|
||||
headerCell.removeAttribute('role');
|
||||
headerCell.removeAttribute('tabindex');
|
||||
|
||||
if (isSortableTableColumn(table, index)) {
|
||||
headerCell.classList.add('sortable');
|
||||
headerCell.setAttribute('role', 'button');
|
||||
headerCell.setAttribute('tabindex', '0');
|
||||
if (state.columnIndex === index) {
|
||||
headerCell.classList.add(state.direction === 'desc' ? 'sort-desc' : 'sort-asc');
|
||||
headerCell.setAttribute('aria-sort', state.direction === 'desc' ? 'descending' : 'ascending');
|
||||
} else {
|
||||
headerCell.setAttribute('aria-sort', 'none');
|
||||
}
|
||||
} else {
|
||||
headerCell.classList.add('unsortable');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function sortTable(table, columnIndex, direction) {
|
||||
var tbody = table.tBodies && table.tBodies[0] ? table.tBodies[0] : null;
|
||||
if (!tbody) {
|
||||
return;
|
||||
}
|
||||
|
||||
var rows = Array.prototype.slice.call(tbody.rows || []);
|
||||
if (!rows.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var multiplier = direction === 'desc' ? -1 : 1;
|
||||
rows.sort(function (leftRow, rightRow) {
|
||||
var leftCell = leftRow.cells ? leftRow.cells[columnIndex] : null;
|
||||
var rightCell = rightRow.cells ? rightRow.cells[columnIndex] : null;
|
||||
var leftComparable = getComparableSortValue(getCellSortValue(leftCell));
|
||||
var rightComparable = getComparableSortValue(getCellSortValue(rightCell));
|
||||
return compareSortValues(leftComparable, rightComparable) * multiplier;
|
||||
});
|
||||
|
||||
rows.forEach(function (row) {
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function applyTableSort(table) {
|
||||
var state = getSortableTableState(table);
|
||||
if (state.columnIndex === null || state.columnIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
sortTable(table, state.columnIndex, state.direction);
|
||||
updateSortableHeaderState(table);
|
||||
}
|
||||
|
||||
function initSortableTables() {
|
||||
var tables = Array.prototype.slice.call(document.querySelectorAll('table'));
|
||||
tables.forEach(function (table) {
|
||||
var headerRow = table.tHead && table.tHead.rows && table.tHead.rows.length ? table.tHead.rows[0] : null;
|
||||
if (!headerRow) {
|
||||
return;
|
||||
}
|
||||
|
||||
Array.prototype.forEach.call(headerRow.cells, function (headerCell, index) {
|
||||
if (!isSortableTableColumn(table, index)) {
|
||||
return;
|
||||
}
|
||||
|
||||
headerCell.addEventListener('click', function () {
|
||||
var state = getSortableTableState(table);
|
||||
var nextDirection = state.columnIndex === index && state.direction === 'asc' ? 'desc' : 'asc';
|
||||
state.columnIndex = index;
|
||||
state.direction = nextDirection;
|
||||
sortTable(table, index, nextDirection);
|
||||
updateSortableHeaderState(table);
|
||||
});
|
||||
|
||||
headerCell.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
headerCell.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
updateSortableHeaderState(table);
|
||||
});
|
||||
}
|
||||
|
||||
window.applyTableSort = applyTableSort;
|
||||
window.initSortableTables = initSortableTables;
|
||||
}());
|
||||
@@ -0,0 +1,481 @@
|
||||
(function () {
|
||||
var dataElement = document.getElementById('template-editor-data');
|
||||
if (!dataElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
var templateData = {};
|
||||
try {
|
||||
templateData = JSON.parse(dataElement.getAttribute('data-json') || dataElement.textContent || '{}') || {};
|
||||
} catch (_error) {
|
||||
templateData = {};
|
||||
}
|
||||
|
||||
var existingRegions = Array.isArray(templateData.regions)
|
||||
? templateData.regions
|
||||
: (Array.isArray(templateData) ? templateData : []);
|
||||
var stage = document.getElementById('designer-stage');
|
||||
var overlay = document.getElementById('designer-overlay');
|
||||
var regionList = document.getElementById('region-list');
|
||||
var regionSelect = document.getElementById('region-select');
|
||||
var canvasSizeSelect = document.getElementById('canvas-size-select');
|
||||
var canvasSizeIdInput = document.getElementById('canvas-size-id');
|
||||
var canvasSizeSummary = document.getElementById('canvas-size-summary');
|
||||
var canvasWidthInput = document.getElementById('canvas-width');
|
||||
var canvasHeightInput = document.getElementById('canvas-height');
|
||||
var backgroundInput = document.getElementById('background-image');
|
||||
var backgroundPreview = document.getElementById('background-preview');
|
||||
var backgroundEmpty = document.getElementById('background-empty');
|
||||
var removeBackgroundButton = document.getElementById('remove-background-image');
|
||||
var removeBackgroundFlag = document.getElementById('remove-background-image-flag');
|
||||
var addTextRegionButton = document.getElementById('add-text-region');
|
||||
var addImageRegionButton = document.getElementById('add-image-region');
|
||||
var regionsJsonInput = document.getElementById('regions-json');
|
||||
var draft = null;
|
||||
var selectedIndex = -1;
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value === undefined || value === null ? '' : value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function valueOrDefault(value, fallback) {
|
||||
return value === undefined || value === null || value === '' ? fallback : value;
|
||||
}
|
||||
|
||||
function getCanvasSize() {
|
||||
return {
|
||||
width: Math.max(1, Number(canvasWidthInput.value || 1920)),
|
||||
height: Math.max(1, Number(canvasHeightInput.value || 1080))
|
||||
};
|
||||
}
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
function getCards() {
|
||||
return Array.prototype.slice.call(regionList.querySelectorAll('.region-item'));
|
||||
}
|
||||
|
||||
function cardAt(index) {
|
||||
return getCards()[index] || null;
|
||||
}
|
||||
|
||||
function getRegionName(card) {
|
||||
return String(card.querySelector('[name="region_name[]"]').value || '').trim();
|
||||
}
|
||||
|
||||
function syncRegionIdentity(card, value) {
|
||||
var next = String(value || '').trim();
|
||||
card.querySelector('[name="region_name[]"]').value = next;
|
||||
card.querySelector('[name="region_key[]"]').value = next;
|
||||
card.querySelector('[name="region_label[]"]').value = next;
|
||||
}
|
||||
|
||||
function readCard(card) {
|
||||
var name = getRegionName(card);
|
||||
return {
|
||||
region_key: name,
|
||||
label: name,
|
||||
region_type: card.querySelector('[name="region_type[]"]').value,
|
||||
font_family: card.querySelector('[name="font_family[]"]').value,
|
||||
x: Number(card.querySelector('[name="region_x[]"]').value || 0),
|
||||
y: Number(card.querySelector('[name="region_y[]"]').value || 0),
|
||||
width: Number(card.querySelector('[name="region_width[]"]').value || 0),
|
||||
height: Number(card.querySelector('[name="region_height[]"]').value || 0),
|
||||
z_index: Number(card.querySelector('[name="region_z[]"]').value || 0)
|
||||
};
|
||||
}
|
||||
|
||||
function writeCard(card, values) {
|
||||
if (values.region_name !== undefined) {
|
||||
syncRegionIdentity(card, values.region_name);
|
||||
} else if (values.region_key !== undefined) {
|
||||
syncRegionIdentity(card, values.region_key);
|
||||
} else if (values.label !== undefined) {
|
||||
syncRegionIdentity(card, values.label);
|
||||
}
|
||||
if (values.region_type !== undefined) { card.querySelector('[name="region_type[]"]').value = values.region_type; }
|
||||
if (values.font_family !== undefined) { card.querySelector('[name="font_family[]"]').value = values.font_family; updateRegionFieldVisibility(card); }
|
||||
if (values.x !== undefined) { card.querySelector('[name="region_x[]"]').value = Math.round(values.x); }
|
||||
if (values.y !== undefined) { card.querySelector('[name="region_y[]"]').value = Math.round(values.y); }
|
||||
if (values.width !== undefined) { card.querySelector('[name="region_width[]"]').value = Math.round(values.width); }
|
||||
if (values.height !== undefined) { card.querySelector('[name="region_height[]"]').value = Math.round(values.height); }
|
||||
if (values.z_index !== undefined) { card.querySelector('[name="region_z[]"]').value = Math.round(values.z_index); }
|
||||
}
|
||||
|
||||
function getOverlayRect() {
|
||||
return overlay.getBoundingClientRect();
|
||||
}
|
||||
|
||||
function toCanvasPoint(event) {
|
||||
var rect = getOverlayRect();
|
||||
var size = getCanvasSize();
|
||||
var x = clamp(event.clientX - rect.left, 0, rect.width);
|
||||
var y = clamp(event.clientY - rect.top, 0, rect.height);
|
||||
return {
|
||||
x: Math.round((x / Math.max(rect.width, 1)) * size.width),
|
||||
y: Math.round((y / Math.max(rect.height, 1)) * size.height)
|
||||
};
|
||||
}
|
||||
|
||||
function canvasRectToPixels(region) {
|
||||
var rect = getOverlayRect();
|
||||
var size = getCanvasSize();
|
||||
return {
|
||||
left: (region.x / size.width) * rect.width,
|
||||
top: (region.y / size.height) * rect.height,
|
||||
width: (region.width / size.width) * rect.width,
|
||||
height: (region.height / size.height) * rect.height
|
||||
};
|
||||
}
|
||||
|
||||
function updateAspectRatio() {
|
||||
var size = getCanvasSize();
|
||||
stage.style.aspectRatio = size.width + ' / ' + size.height;
|
||||
}
|
||||
|
||||
function updateCanvasSizeSummary() {
|
||||
var option = canvasSizeSelect.options[canvasSizeSelect.selectedIndex];
|
||||
canvasSizeSummary.textContent = option ? option.textContent : '';
|
||||
}
|
||||
|
||||
function updateCanvasSizeLock() {
|
||||
var lockOnExistingTemplate = canvasSizeSelect.dataset.lockOnExistingTemplate === 'true';
|
||||
var locked = lockOnExistingTemplate && getCards().length > 0;
|
||||
canvasSizeSelect.disabled = locked;
|
||||
canvasSizeSummary.classList.toggle('is-locked', locked);
|
||||
}
|
||||
|
||||
function syncCanvasSizeSelection() {
|
||||
var option = canvasSizeSelect.options[canvasSizeSelect.selectedIndex];
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
if (canvasSizeIdInput) {
|
||||
canvasSizeIdInput.value = option.value;
|
||||
}
|
||||
canvasWidthInput.value = Math.max(1, Number(option.dataset.width || canvasWidthInput.value || 1920));
|
||||
canvasHeightInput.value = Math.max(1, Number(option.dataset.height || canvasHeightInput.value || 1080));
|
||||
updateAspectRatio();
|
||||
updateCanvasSizeSummary();
|
||||
}
|
||||
|
||||
function updateBackgroundPreview(file) {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
if (removeBackgroundFlag) {
|
||||
removeBackgroundFlag.checked = false;
|
||||
}
|
||||
var reader = new FileReader();
|
||||
reader.onload = function () {
|
||||
backgroundPreview.src = reader.result;
|
||||
backgroundPreview.style.display = 'block';
|
||||
backgroundEmpty.style.display = 'none';
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
function updateRegionFieldVisibility(card) {
|
||||
var typeSelect = card.querySelector('[name="region_type[]"]');
|
||||
var fontField = card.querySelector('[name="font_family[]"]');
|
||||
if (!typeSelect || !fontField) {
|
||||
return;
|
||||
}
|
||||
fontField.value = typeSelect.value === 'text' || typeSelect.value === 'html' ? 'Arial' : '';
|
||||
}
|
||||
|
||||
function makeRegionCard(region) {
|
||||
var card = document.createElement('div');
|
||||
card.className = 'region-item';
|
||||
var chipLabel = region.region_type === 'image' ? 'Image' : region.region_type === 'webpage' ? 'Webpage' : region.region_type === 'html' ? 'HTML' : 'Text';
|
||||
card.innerHTML = '' +
|
||||
'<label>Region<input name="region_name[]" value="' + escapeHtml(region.region_key || region.label || '') + '" placeholder="region_1" required /></label>' +
|
||||
'<label>Type<select name="region_type[]"><option value="text"' + (region.region_type === 'image' || region.region_type === 'webpage' || region.region_type === 'html' ? '' : ' selected') + '>Text</option><option value="html"' + (region.region_type === 'html' ? ' selected' : '') + '>HTML</option><option value="image"' + (region.region_type === 'image' ? ' selected' : '') + '>Image</option><option value="webpage"' + (region.region_type === 'webpage' ? ' selected' : '') + '>Webpage</option></select></label>' +
|
||||
'<input type="hidden" name="font_family[]" value="' + escapeHtml(region.region_type === 'image' ? '' : (region.font_family || 'Arial')) + '" />' +
|
||||
'<input type="hidden" name="region_key[]" value="' + escapeHtml(region.region_key || region.label || '') + '" />' +
|
||||
'<input type="hidden" name="region_label[]" value="' + escapeHtml(region.label || region.region_key || '') + '" />' +
|
||||
'<div class="template-field-head"><strong>' + escapeHtml(region.label || region.region_key || '') + '</strong><span class="chip">' + escapeHtml(chipLabel) + '</span></div>' +
|
||||
'<div class="row"><label style="flex:1">X<input type="number" name="region_x[]" value="' + escapeHtml(valueOrDefault(region.x, 80)) + '" required /></label><label style="flex:1">Y<input type="number" name="region_y[]" value="' + escapeHtml(valueOrDefault(region.y, 80)) + '" required /></label></div>' +
|
||||
'<div class="row"><label style="flex:1">Width<input type="number" name="region_width[]" value="' + escapeHtml(valueOrDefault(region.width, 300)) + '" required /></label><label style="flex:1">Height<input type="number" name="region_height[]" value="' + escapeHtml(valueOrDefault(region.height, 120)) + '" required /></label><label style="flex:1">Z-Index<input type="number" name="region_z[]" value="' + escapeHtml(valueOrDefault(region.z_index, 1)) + '" required /></label></div>' +
|
||||
'<div class="row"><button type="button" class="danger remove-region">Remove</button></div>';
|
||||
var nameInput = card.querySelector('[name="region_name[]"]');
|
||||
var typeSelect = card.querySelector('[name="region_type[]"]');
|
||||
nameInput.addEventListener('input', function () {
|
||||
syncRegionIdentity(card, nameInput.value);
|
||||
renderRegionSidebar();
|
||||
renderOverlay();
|
||||
});
|
||||
typeSelect.addEventListener('change', function () {
|
||||
updateRegionFieldVisibility(card);
|
||||
renderOverlay();
|
||||
});
|
||||
card.addEventListener('click', function (event) {
|
||||
if (event.target && event.target.classList && event.target.classList.contains('remove-region')) {
|
||||
return;
|
||||
}
|
||||
setSelected(getCards().indexOf(card));
|
||||
});
|
||||
card.querySelector('.remove-region').addEventListener('click', function (event) {
|
||||
event.preventDefault();
|
||||
card.remove();
|
||||
if (!getCards().length) {
|
||||
selectedIndex = -1;
|
||||
} else if (selectedIndex >= getCards().length) {
|
||||
selectedIndex = getCards().length - 1;
|
||||
}
|
||||
renderRegionSidebar();
|
||||
renderOverlay();
|
||||
});
|
||||
updateRegionFieldVisibility(card);
|
||||
return card;
|
||||
}
|
||||
|
||||
function renderRegionList(initialRegions) {
|
||||
regionList.innerHTML = '';
|
||||
initialRegions.forEach(function (region) {
|
||||
regionList.appendChild(makeRegionCard(region));
|
||||
});
|
||||
}
|
||||
|
||||
function renderRegionSidebar() {
|
||||
var cards = getCards();
|
||||
regionSelect.innerHTML = '';
|
||||
if (!cards.length) {
|
||||
regionSelect.disabled = true;
|
||||
regionList.innerHTML = '';
|
||||
updateCanvasSizeLock();
|
||||
return;
|
||||
}
|
||||
regionSelect.disabled = false;
|
||||
if (selectedIndex < 0 || selectedIndex >= cards.length) {
|
||||
selectedIndex = 0;
|
||||
}
|
||||
cards.forEach(function (card, index) {
|
||||
var option = document.createElement('option');
|
||||
option.value = String(index);
|
||||
option.textContent = getRegionName(card) || ('Region ' + (index + 1));
|
||||
if (index === selectedIndex) {
|
||||
option.selected = true;
|
||||
}
|
||||
regionSelect.appendChild(option);
|
||||
card.hidden = index !== selectedIndex;
|
||||
});
|
||||
regionSelect.value = String(selectedIndex);
|
||||
updateCanvasSizeLock();
|
||||
}
|
||||
|
||||
function renderOverlay() {
|
||||
var cards = getCards();
|
||||
var selectedCard = selectedIndex >= 0 ? cards[selectedIndex] : null;
|
||||
var selectedNow = selectedCard ? cards.indexOf(selectedCard) : -1;
|
||||
var regions = cards.map(readCard);
|
||||
regionsJsonInput.value = JSON.stringify(regions);
|
||||
overlay.innerHTML = regions.map(function (region, index) {
|
||||
var box = canvasRectToPixels(region);
|
||||
var selected = index === selectedNow ? ' selected' : '';
|
||||
return '<div class="designer-rect' + selected + '" data-index="' + index + '" style="left:' + box.left + 'px;top:' + box.top + 'px;width:' + box.width + 'px;height:' + box.height + 'px;"><div class="designer-rect-label">' + escapeHtml(region.label || region.region_key || 'Region') + '</div><span class="resize-handle nw" data-dir="nw"></span><span class="resize-handle ne" data-dir="ne"></span><span class="resize-handle sw" data-dir="sw"></span><span class="resize-handle se" data-dir="se"></span></div>';
|
||||
}).join('');
|
||||
if (draft) {
|
||||
var rect = getOverlayRect();
|
||||
var size = getCanvasSize();
|
||||
var draftBox = { x: Math.min(draft.start.x, draft.end.x), y: Math.min(draft.start.y, draft.end.y), width: Math.abs(draft.end.x - draft.start.x), height: Math.abs(draft.end.y - draft.start.y) };
|
||||
overlay.innerHTML += '<div class="designer-rect designer-draft" style="left:' + ((draftBox.x / size.width) * rect.width) + 'px;top:' + ((draftBox.y / size.height) * rect.height) + 'px;width:' + ((draftBox.width / size.width) * rect.width) + 'px;height:' + ((draftBox.height / size.height) * rect.height) + 'px;"></div>';
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
updateAspectRatio();
|
||||
renderRegionSidebar();
|
||||
renderOverlay();
|
||||
}
|
||||
|
||||
function setSelected(index) {
|
||||
var cards = getCards();
|
||||
if (!cards.length) {
|
||||
selectedIndex = -1;
|
||||
} else if (index < 0) {
|
||||
selectedIndex = 0;
|
||||
} else {
|
||||
selectedIndex = clamp(index, 0, cards.length - 1);
|
||||
}
|
||||
renderRegionSidebar();
|
||||
renderOverlay();
|
||||
}
|
||||
|
||||
function addRegion(region) {
|
||||
var hint = regionList.querySelector('.muted');
|
||||
if (hint) {
|
||||
hint.remove();
|
||||
}
|
||||
regionList.appendChild(makeRegionCard(region));
|
||||
setSelected(getCards().length - 1);
|
||||
}
|
||||
|
||||
function createDefaultRegion(type) {
|
||||
var count = getCards().length + 1;
|
||||
var name = 'region_' + count;
|
||||
return {
|
||||
region_key: name,
|
||||
label: name,
|
||||
region_type: type,
|
||||
font_family: type === 'text' || type === 'html' ? 'Arial' : '',
|
||||
x: 80,
|
||||
y: 80,
|
||||
width: type === 'image' || type === 'webpage' || type === 'html' ? 420 : 300,
|
||||
height: type === 'image' || type === 'webpage' || type === 'html' ? 240 : 120,
|
||||
z_index: 1
|
||||
};
|
||||
}
|
||||
|
||||
function clampRegion(region) {
|
||||
var size = getCanvasSize();
|
||||
var minSize = 12;
|
||||
var x = clamp(region.x, 0, size.width - minSize);
|
||||
var y = clamp(region.y, 0, size.height - minSize);
|
||||
var width = Math.max(minSize, region.width);
|
||||
var height = Math.max(minSize, region.height);
|
||||
if (x + width > size.width) {
|
||||
width = size.width - x;
|
||||
}
|
||||
if (y + height > size.height) {
|
||||
height = size.height - y;
|
||||
}
|
||||
return { x: Math.round(x), y: Math.round(y), width: Math.round(Math.max(minSize, width)), height: Math.round(Math.max(minSize, height)) };
|
||||
}
|
||||
|
||||
function startDraw(event) {
|
||||
var start = toCanvasPoint(event);
|
||||
draft = { start: start, end: start };
|
||||
renderOverlay();
|
||||
function moveHandler(moveEvent) {
|
||||
draft.end = toCanvasPoint(moveEvent);
|
||||
renderOverlay();
|
||||
}
|
||||
function upHandler(upEvent) {
|
||||
draft.end = toCanvasPoint(upEvent);
|
||||
var width = Math.abs(draft.end.x - draft.start.x);
|
||||
var height = Math.abs(draft.end.y - draft.start.y);
|
||||
if (width >= 8 && height >= 8) {
|
||||
var region = clampRegion({ x: Math.min(draft.start.x, draft.end.x), y: Math.min(draft.start.y, draft.end.y), width: width, height: height });
|
||||
addRegion({ region_key: 'region_' + (getCards().length + 1), label: 'Region ' + (getCards().length + 1), region_type: 'text', x: region.x, y: region.y, width: region.width, height: region.height, z_index: 1 });
|
||||
}
|
||||
draft = null;
|
||||
renderOverlay();
|
||||
document.removeEventListener('mousemove', moveHandler);
|
||||
document.removeEventListener('mouseup', upHandler);
|
||||
}
|
||||
document.addEventListener('mousemove', moveHandler);
|
||||
document.addEventListener('mouseup', upHandler);
|
||||
}
|
||||
|
||||
function startMove(index, event) {
|
||||
var startPoint = toCanvasPoint(event);
|
||||
var startRegion = readCard(cardAt(index));
|
||||
function moveHandler(moveEvent) {
|
||||
var currentPoint = toCanvasPoint(moveEvent);
|
||||
var dx = currentPoint.x - startPoint.x;
|
||||
var dy = currentPoint.y - startPoint.y;
|
||||
var next = clampRegion({ x: startRegion.x + dx, y: startRegion.y + dy, width: startRegion.width, height: startRegion.height });
|
||||
writeCard(cardAt(index), { x: next.x, y: next.y });
|
||||
renderOverlay();
|
||||
}
|
||||
function upHandler() {
|
||||
document.removeEventListener('mousemove', moveHandler);
|
||||
document.removeEventListener('mouseup', upHandler);
|
||||
}
|
||||
document.addEventListener('mousemove', moveHandler);
|
||||
document.addEventListener('mouseup', upHandler);
|
||||
}
|
||||
|
||||
function resizeFromHandle(index, dir, event) {
|
||||
var startPoint = toCanvasPoint(event);
|
||||
var startRegion = readCard(cardAt(index));
|
||||
function moveHandler(moveEvent) {
|
||||
var currentPoint = toCanvasPoint(moveEvent);
|
||||
var dx = currentPoint.x - startPoint.x;
|
||||
var dy = currentPoint.y - startPoint.y;
|
||||
var next = { x: startRegion.x, y: startRegion.y, width: startRegion.width, height: startRegion.height };
|
||||
if (dir.indexOf('w') !== -1) { next.x = startRegion.x + dx; next.width = startRegion.width - dx; }
|
||||
if (dir.indexOf('e') !== -1) { next.width = startRegion.width + dx; }
|
||||
if (dir.indexOf('n') !== -1) { next.y = startRegion.y + dy; next.height = startRegion.height - dy; }
|
||||
if (dir.indexOf('s') !== -1) { next.height = startRegion.height + dy; }
|
||||
if (next.width < 12) { if (dir.indexOf('w') !== -1) { next.x -= 12 - next.width; } next.width = 12; }
|
||||
if (next.height < 12) { if (dir.indexOf('n') !== -1) { next.y -= 12 - next.height; } next.height = 12; }
|
||||
next = clampRegion(next);
|
||||
writeCard(cardAt(index), { x: next.x, y: next.y, width: next.width, height: next.height });
|
||||
renderOverlay();
|
||||
}
|
||||
function upHandler() {
|
||||
document.removeEventListener('mousemove', moveHandler);
|
||||
document.removeEventListener('mouseup', upHandler);
|
||||
}
|
||||
document.addEventListener('mousemove', moveHandler);
|
||||
document.addEventListener('mouseup', upHandler);
|
||||
}
|
||||
|
||||
addTextRegionButton.addEventListener('click', function () { addRegion(createDefaultRegion('text')); });
|
||||
var addHtmlRegionButton = document.getElementById('add-html-region');
|
||||
if (addHtmlRegionButton) {
|
||||
addHtmlRegionButton.addEventListener('click', function () { addRegion(createDefaultRegion('html')); });
|
||||
}
|
||||
addImageRegionButton.addEventListener('click', function () { addRegion(createDefaultRegion('image')); });
|
||||
var addWebpageRegionButton = document.getElementById('add-webpage-region');
|
||||
if (addWebpageRegionButton) {
|
||||
addWebpageRegionButton.addEventListener('click', function () { addRegion(createDefaultRegion('webpage')); });
|
||||
}
|
||||
backgroundInput.addEventListener('change', function () {
|
||||
var file = backgroundInput.files && backgroundInput.files[0];
|
||||
if (file) {
|
||||
updateBackgroundPreview(file);
|
||||
}
|
||||
});
|
||||
if (removeBackgroundButton && removeBackgroundFlag) {
|
||||
removeBackgroundButton.addEventListener('click', function () {
|
||||
removeBackgroundFlag.checked = true;
|
||||
backgroundInput.value = '';
|
||||
backgroundPreview.removeAttribute('src');
|
||||
backgroundPreview.style.display = 'none';
|
||||
backgroundEmpty.style.display = 'block';
|
||||
});
|
||||
}
|
||||
canvasSizeSelect.addEventListener('change', function () { syncCanvasSizeSelection(); render(); });
|
||||
canvasWidthInput.addEventListener('input', render);
|
||||
canvasHeightInput.addEventListener('input', render);
|
||||
regionSelect.addEventListener('change', function () { setSelected(Number(regionSelect.value || 0)); });
|
||||
overlay.addEventListener('mousedown', function (event) {
|
||||
var rect = event.target.closest('.designer-rect');
|
||||
if (rect) {
|
||||
var index = Number(rect.getAttribute('data-index'));
|
||||
var handle = event.target.closest('.resize-handle');
|
||||
event.preventDefault();
|
||||
setSelected(index);
|
||||
if (handle) {
|
||||
resizeFromHandle(index, handle.getAttribute('data-dir'), event);
|
||||
} else {
|
||||
startMove(index, event);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.target !== overlay && !event.target.classList.contains('designer-overlay')) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
setSelected(-1);
|
||||
startDraw(event);
|
||||
});
|
||||
document.getElementById('template-form').addEventListener('submit', function () {
|
||||
syncCanvasSizeSelection();
|
||||
regionsJsonInput.value = JSON.stringify(getCards().map(readCard));
|
||||
});
|
||||
|
||||
renderRegionList(existingRegions);
|
||||
syncCanvasSizeSelection();
|
||||
render();
|
||||
})();
|
||||
@@ -0,0 +1,20 @@
|
||||
(function () {
|
||||
var storageKey = 'web-theme';
|
||||
var theme = 'light';
|
||||
|
||||
try {
|
||||
var storedTheme = window.localStorage.getItem(storageKey);
|
||||
if (storedTheme === 'dark' || storedTheme === 'light') {
|
||||
theme = storedTheme;
|
||||
} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
theme = 'dark';
|
||||
}
|
||||
} catch (error) {
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
theme = 'dark';
|
||||
}
|
||||
}
|
||||
|
||||
document.documentElement.dataset.theme = theme;
|
||||
document.documentElement.style.colorScheme = theme;
|
||||
}());
|
||||
@@ -0,0 +1,118 @@
|
||||
(function () {
|
||||
var THEME_STORAGE_KEY = 'web-theme';
|
||||
|
||||
function getPreferredTheme() {
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
return 'dark';
|
||||
}
|
||||
return 'light';
|
||||
}
|
||||
|
||||
function getStoredTheme() {
|
||||
try {
|
||||
var storedTheme = window.localStorage.getItem(THEME_STORAGE_KEY);
|
||||
if (storedTheme === 'dark' || storedTheme === 'light') {
|
||||
return storedTheme;
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setStoredTheme(theme) {
|
||||
try {
|
||||
window.localStorage.setItem(THEME_STORAGE_KEY, theme);
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function applyTheme(theme) {
|
||||
var normalizedTheme = theme === 'dark' ? 'dark' : 'light';
|
||||
var nextTheme = normalizedTheme === 'dark' ? 'light' : 'dark';
|
||||
var nextThemeLabel = nextTheme === 'dark' ? 'Dark mode' : 'Light mode';
|
||||
|
||||
document.documentElement.dataset.theme = normalizedTheme;
|
||||
document.documentElement.style.colorScheme = normalizedTheme;
|
||||
|
||||
Array.prototype.forEach.call(document.querySelectorAll('[data-theme-toggle]'), function (toggleButton) {
|
||||
var icon = toggleButton.querySelector('.theme-toggle__icon');
|
||||
toggleButton.setAttribute('aria-pressed', normalizedTheme === 'dark' ? 'true' : 'false');
|
||||
toggleButton.setAttribute('aria-label', 'Switch to ' + nextThemeLabel.toLowerCase());
|
||||
if (icon) {
|
||||
icon.classList.remove('theme-toggle__icon--moon', 'theme-toggle__icon--sun');
|
||||
icon.classList.add(normalizedTheme === 'dark' ? 'theme-toggle__icon--sun' : 'theme-toggle__icon--moon');
|
||||
}
|
||||
});
|
||||
|
||||
return normalizedTheme;
|
||||
}
|
||||
|
||||
function initThemeToggle() {
|
||||
var toggleButtons = Array.prototype.slice.call(document.querySelectorAll('[data-theme-toggle]'));
|
||||
var storedTheme = getStoredTheme();
|
||||
var theme = storedTheme || getPreferredTheme();
|
||||
|
||||
applyTheme(theme);
|
||||
|
||||
if (!toggleButtons.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
Array.prototype.forEach.call(toggleButtons, function (toggleButton) {
|
||||
toggleButton.addEventListener('click', function () {
|
||||
var nextTheme = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
|
||||
setStoredTheme(nextTheme);
|
||||
applyTheme(nextTheme);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initSidebarToggle() {
|
||||
var body = document.body;
|
||||
var toggleButtons = Array.prototype.slice.call(document.querySelectorAll('[data-sidebar-toggle]'));
|
||||
var backdrop = document.querySelector('[data-sidebar-backdrop]');
|
||||
|
||||
if (!toggleButtons.length || !backdrop) {
|
||||
return;
|
||||
}
|
||||
|
||||
function setSidebarOpen(isOpen) {
|
||||
body.classList.toggle('is-sidebar-open', isOpen);
|
||||
Array.prototype.forEach.call(toggleButtons, function (toggleButton) {
|
||||
toggleButton.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSidebar() {
|
||||
setSidebarOpen(!body.classList.contains('is-sidebar-open'));
|
||||
}
|
||||
|
||||
Array.prototype.forEach.call(toggleButtons, function (toggleButton) {
|
||||
toggleButton.addEventListener('click', function () {
|
||||
toggleSidebar();
|
||||
});
|
||||
});
|
||||
|
||||
backdrop.addEventListener('click', function () {
|
||||
setSidebarOpen(false);
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Escape') {
|
||||
setSidebarOpen(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.getPreferredTheme = getPreferredTheme;
|
||||
window.getStoredTheme = getStoredTheme;
|
||||
window.setStoredTheme = setStoredTheme;
|
||||
window.applyTheme = applyTheme;
|
||||
window.initThemeToggle = initThemeToggle;
|
||||
window.initSidebarToggle = initSidebarToggle;
|
||||
|
||||
initThemeToggle();
|
||||
initSidebarToggle();
|
||||
}());
|
||||
@@ -0,0 +1,82 @@
|
||||
(function () {
|
||||
function dismissToast(toast) {
|
||||
if (!toast) {
|
||||
return;
|
||||
}
|
||||
|
||||
toast.classList.add('toast-hide');
|
||||
window.setTimeout(function () {
|
||||
if (toast && toast.parentNode) {
|
||||
toast.parentNode.removeChild(toast);
|
||||
}
|
||||
}, 220);
|
||||
}
|
||||
|
||||
function showToast(message) {
|
||||
var text = String(message || '').trim();
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
|
||||
var existingToast = document.getElementById('app-toast');
|
||||
var existingBody = existingToast ? existingToast.querySelector('.toast-body') : null;
|
||||
if (existingToast && existingBody) {
|
||||
existingBody.textContent = text;
|
||||
existingToast.classList.remove('toast-hide');
|
||||
window.clearTimeout(existingToast._dismissTimer);
|
||||
existingToast._dismissTimer = window.setTimeout(function () {
|
||||
dismissToast(existingToast);
|
||||
}, 4000);
|
||||
return;
|
||||
}
|
||||
|
||||
var toast = document.createElement('div');
|
||||
toast.className = 'toast';
|
||||
toast.setAttribute('role', 'status');
|
||||
toast.setAttribute('aria-live', 'polite');
|
||||
toast.innerHTML = '<div class="toast-body"></div><button type="button" class="toast-close" aria-label="Dismiss notification">×</button>';
|
||||
toast.querySelector('.toast-body').textContent = text;
|
||||
|
||||
var closeButton = toast.querySelector('.toast-close');
|
||||
closeButton.addEventListener('click', function () {
|
||||
dismissToast(toast);
|
||||
});
|
||||
|
||||
document.body.appendChild(toast);
|
||||
toast._dismissTimer = window.setTimeout(function () {
|
||||
dismissToast(toast);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
function initToast() {
|
||||
var toast = document.getElementById('app-toast');
|
||||
var closeButton = document.getElementById('app-toast-close');
|
||||
if (!toast || !closeButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var url = new URL(window.location.href);
|
||||
if (url.searchParams.has('message')) {
|
||||
url.searchParams.delete('message');
|
||||
window.history.replaceState({}, document.title, url.pathname + url.search + url.hash);
|
||||
}
|
||||
} catch (_error) {
|
||||
// ignore URL cleanup failures
|
||||
}
|
||||
|
||||
closeButton.addEventListener('click', function () {
|
||||
dismissToast(toast);
|
||||
});
|
||||
|
||||
window.setTimeout(function () {
|
||||
dismissToast(toast);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
window.dismissToast = dismissToast;
|
||||
window.showToast = showToast;
|
||||
window.initToast = initToast;
|
||||
|
||||
initToast();
|
||||
}());
|
||||
Reference in New Issue
Block a user