Implement schedule WYSIWYG and UTC dates
This commit is contained in:
@@ -725,6 +725,10 @@ td[data-label="Slides"] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.slide-editor-sidebar {
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.slide-form-stack {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
@@ -1734,6 +1738,11 @@ html[data-bs-theme='dark'] .template-field-card .tox .tox-collection {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.slide-editor-sidebar {
|
||||
position: static;
|
||||
top: auto;
|
||||
}
|
||||
|
||||
.slide-editor-shell {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// Minimal row editor for schedule groups.
|
||||
|
||||
(function () {
|
||||
var form = document.getElementById('schedule-group-form');
|
||||
var body = document.querySelector('[data-schedule-entries-body]');
|
||||
var addButton = document.querySelector('[data-add-schedule-entry]');
|
||||
var template = document.getElementById('schedule-entry-row-template');
|
||||
|
||||
if (!form || !body || !addButton || !template) {
|
||||
return;
|
||||
}
|
||||
|
||||
function bindRemove(row) {
|
||||
var button = row.querySelector('[data-remove-schedule-entry]');
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
button.addEventListener('click', function () {
|
||||
row.remove();
|
||||
});
|
||||
}
|
||||
|
||||
function addRow() {
|
||||
var fragment = template.content.cloneNode(true);
|
||||
var row = fragment.querySelector('[data-schedule-entry-row]');
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
bindRemove(row);
|
||||
body.appendChild(fragment);
|
||||
}
|
||||
|
||||
body.querySelectorAll('[data-schedule-entry-row]').forEach(bindRemove);
|
||||
addButton.addEventListener('click', addRow);
|
||||
}());
|
||||
@@ -0,0 +1,338 @@
|
||||
// Schedule region helpers for editor previews and defaults.
|
||||
|
||||
(function () {
|
||||
var registry = window.pulseRegionTypes;
|
||||
var utils = window.pulseRegionUtils || {};
|
||||
var placeholderChips = window.placeholderChips || {};
|
||||
var placeholderUtils = window.placeholderUtils || {};
|
||||
var DEFAULT_STYLE = {
|
||||
font_family: 'Arial',
|
||||
font_size: 28,
|
||||
font_color: '#000000'
|
||||
};
|
||||
|
||||
function escapeHtml(value) {
|
||||
return utils.escapeHtml ? utils.escapeHtml(value) : String(value === undefined || value === null ? '' : value);
|
||||
}
|
||||
|
||||
function sanitizeRichText(html) {
|
||||
return utils.sanitizeRichText ? utils.sanitizeRichText(html) : escapeHtml(html);
|
||||
}
|
||||
|
||||
function getDefaultStyle() {
|
||||
return {
|
||||
font_family: DEFAULT_STYLE.font_family,
|
||||
font_size: DEFAULT_STYLE.font_size,
|
||||
font_color: DEFAULT_STYLE.font_color
|
||||
};
|
||||
}
|
||||
|
||||
function getScheduleGroups() {
|
||||
return Array.isArray(window.initialData && window.initialData.scheduleGroups) ? window.initialData.scheduleGroups : [];
|
||||
}
|
||||
|
||||
function getGroupById(groupId, groups) {
|
||||
var normalizedId = Number(groupId || 0);
|
||||
return (Array.isArray(groups) ? groups : getScheduleGroups()).find(function (group) {
|
||||
return Number(group.id) === normalizedId;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function getEntries(groupId, groups) {
|
||||
var group = getGroupById(groupId, groups);
|
||||
return group && Array.isArray(group.entries) ? group.entries : [];
|
||||
}
|
||||
|
||||
function toDate(value) {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
var date = value instanceof Date ? value : new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? null : date;
|
||||
}
|
||||
|
||||
function isUpcoming(entry, now) {
|
||||
var start = toDate(entry && entry.start_datetime);
|
||||
return Boolean(start && now < start);
|
||||
}
|
||||
|
||||
function isLive(entry, now) {
|
||||
var start = toDate(entry && entry.start_datetime);
|
||||
var end = toDate(entry && entry.end_datetime);
|
||||
return Boolean(start && end && now >= start && now < end);
|
||||
}
|
||||
|
||||
function getVisibleEntries(groupId, displayMode, maxItems, groups) {
|
||||
var now = new Date();
|
||||
var entries = getEntries(groupId, groups).slice().sort(function (left, right) {
|
||||
var leftStart = toDate(left && left.start_datetime);
|
||||
var rightStart = toDate(right && right.start_datetime);
|
||||
return (leftStart ? leftStart.getTime() : 0) - (rightStart ? rightStart.getTime() : 0) || Number(left.id || 0) - Number(right.id || 0);
|
||||
});
|
||||
var mode = String(displayMode || 'upcoming').trim().toLowerCase();
|
||||
|
||||
entries = entries.filter(function (entry) {
|
||||
if (mode === 'current') {
|
||||
return isLive(entry, now);
|
||||
}
|
||||
if (mode === 'both') {
|
||||
return isUpcoming(entry, now) || isLive(entry, now);
|
||||
}
|
||||
return isUpcoming(entry, now);
|
||||
});
|
||||
|
||||
return entries.slice(0, Math.max(1, Number(maxItems || 5)));
|
||||
}
|
||||
|
||||
function renderTemplate(template, context) {
|
||||
var source = String(template || '');
|
||||
if (!source) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (placeholderUtils && typeof placeholderUtils.resolvePlaceholderExpression === 'function' && typeof placeholderUtils.formatPlaceholderValue === 'function') {
|
||||
return source.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, function (_match, expression) {
|
||||
return escapeHtml(placeholderUtils.formatPlaceholderValue(placeholderUtils.resolvePlaceholderExpression(context, expression)));
|
||||
});
|
||||
}
|
||||
|
||||
return source.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, function (_match, expression) {
|
||||
var value = context;
|
||||
String(expression || '').trim().split('.').forEach(function (segment) {
|
||||
if (value === undefined || value === null) {
|
||||
value = '';
|
||||
return;
|
||||
}
|
||||
value = value[segment];
|
||||
});
|
||||
return escapeHtml(value);
|
||||
});
|
||||
}
|
||||
|
||||
function getCurrentConfig(region, existingContent) {
|
||||
var current = existingContent[region.region_key] || {};
|
||||
return {
|
||||
schedule_group_id: current.schedule_group_id === undefined || current.schedule_group_id === null || current.schedule_group_id === '' ? '' : Number(current.schedule_group_id),
|
||||
display_mode: String(current.display_mode || 'upcoming').trim().toLowerCase() || 'upcoming',
|
||||
value: String(current.value !== undefined ? current.value : current.text !== undefined ? current.text : ''),
|
||||
max_items: Math.max(1, Number(current.max_items || 5)),
|
||||
font_family: current.font_family || region.font_family || getDefaultStyle().font_family,
|
||||
font_size: current.font_size || region.font_size || getDefaultStyle().font_size,
|
||||
font_color: current.font_color || region.font_color || getDefaultStyle().font_color
|
||||
};
|
||||
}
|
||||
|
||||
function getSchedulePlaceholderTokens() {
|
||||
return ['title', 'short_description', 'start', 'end'];
|
||||
}
|
||||
|
||||
function renderSchedulePlaceholderChips(group, entries) {
|
||||
var tokens = getSchedulePlaceholderTokens(group, entries);
|
||||
if (placeholderChips && typeof placeholderChips.renderChips === 'function') {
|
||||
return placeholderChips.renderChips(tokens);
|
||||
}
|
||||
|
||||
return tokens.map(function (token) {
|
||||
return '<span class="chip">{{' + escapeHtml(token) + '}}</span>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function getCurrentSelection(regionId, card, existingContent, scheduleGroups) {
|
||||
var current = existingContent[regionId] || {};
|
||||
var scheduleGroupInput = card && card.querySelector ? card.querySelector('select[name="region_schedule_group_id_' + regionId + '"]') : null;
|
||||
var displayModeInput = card && card.querySelector ? card.querySelector('select[name="region_schedule_display_mode_' + regionId + '"]') : null;
|
||||
var maxItemsInput = card && card.querySelector ? card.querySelector('input[name="region_schedule_max_items_' + regionId + '"]') : null;
|
||||
var groupId = scheduleGroupInput ? scheduleGroupInput.value : (current.schedule_group_id === undefined || current.schedule_group_id === null || current.schedule_group_id === '' ? '' : Number(current.schedule_group_id));
|
||||
var displayMode = displayModeInput ? displayModeInput.value : String(current.display_mode || 'upcoming').trim().toLowerCase() || 'upcoming';
|
||||
var maxItems = Math.max(1, Number(maxItemsInput ? maxItemsInput.value : current.max_items || 5));
|
||||
var group = getGroupById(groupId, scheduleGroups);
|
||||
var entries = getVisibleEntries(groupId, displayMode, maxItems, scheduleGroups);
|
||||
if (!entries.length) {
|
||||
entries = getEntries(groupId, scheduleGroups).slice(0, maxItems);
|
||||
}
|
||||
|
||||
return {
|
||||
group: group,
|
||||
entries: entries,
|
||||
displayMode: displayMode,
|
||||
maxItems: maxItems
|
||||
};
|
||||
}
|
||||
|
||||
function renderEditorCard(context) {
|
||||
var region = context.region;
|
||||
var current = context.current || {};
|
||||
var scheduleGroups = Array.isArray(context.scheduleGroups) ? context.scheduleGroups : [];
|
||||
var currentValue = String(current.value !== undefined ? current.value : current.text !== undefined ? current.text : '');
|
||||
var currentGroupId = current.schedule_group_id === undefined || current.schedule_group_id === null || current.schedule_group_id === '' ? '' : Number(current.schedule_group_id);
|
||||
var currentDisplayMode = String(current.display_mode || 'upcoming').trim().toLowerCase() || 'upcoming';
|
||||
var currentMaxItems = Math.max(1, Number(current.max_items || 5));
|
||||
var currentGroup = getGroupById(currentGroupId, scheduleGroups);
|
||||
var currentEntries = getVisibleEntries(currentGroupId, currentDisplayMode, currentMaxItems, scheduleGroups);
|
||||
if (!currentEntries.length) {
|
||||
currentEntries = getEntries(currentGroupId, scheduleGroups).slice(0, currentMaxItems);
|
||||
}
|
||||
var groupOptions = scheduleGroups.map(function (group) {
|
||||
var selected = Number(group.id) === Number(currentGroupId) ? ' selected' : '';
|
||||
return '<option value="' + escapeHtml(group.id) + '"' + selected + '>' + escapeHtml(group.name || ('Group ' + group.id)) + '</option>';
|
||||
}).join('');
|
||||
|
||||
return '' +
|
||||
'<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '" style="position:relative;overflow:visible !important;">' +
|
||||
'<div class="card-header template-field-head">' +
|
||||
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
||||
'<div class="template-field-actions"><span class="chip">Schedule</span></div>' +
|
||||
'</div>' +
|
||||
'<div class="card-body p-3 d-grid gap-3">' +
|
||||
'<div class="editor-holder" data-region-id="' + region.id + '">' +
|
||||
'<textarea class="editor-source" rows="10">' + escapeHtml(currentValue) + '</textarea>' +
|
||||
'<input type="hidden" name="region_text_' + region.id + '" value="' + escapeHtml(currentValue) + '" />' +
|
||||
'</div>' +
|
||||
'<div class="row g-3">' +
|
||||
'<div class="col-12 col-lg-6">' +
|
||||
'<label class="form-label" for="region_schedule_group_id_' + region.id + '">Schedule group</label>' +
|
||||
'<select id="region_schedule_group_id_' + region.id + '" name="region_schedule_group_id_' + region.id + '" class="form-select">' +
|
||||
'<option value="">Select a group</option>' +
|
||||
groupOptions +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<div class="col-6 col-lg-3">' +
|
||||
'<label class="form-label" for="region_schedule_display_mode_' + region.id + '">Display mode</label>' +
|
||||
'<select id="region_schedule_display_mode_' + region.id + '" name="region_schedule_display_mode_' + region.id + '" class="form-select">' +
|
||||
'<option value="upcoming"' + (currentDisplayMode === 'upcoming' ? ' selected' : '') + '>Upcoming</option>' +
|
||||
'<option value="current"' + (currentDisplayMode === 'current' ? ' selected' : '') + '>Current</option>' +
|
||||
'<option value="both"' + (currentDisplayMode === 'both' ? ' selected' : '') + '>Both</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="row g-3 align-items-end">' +
|
||||
'<div class="col-6 col-lg-3">' +
|
||||
'<label class="form-label" for="region_schedule_max_items_' + region.id + '">Max items</label>' +
|
||||
'<input id="region_schedule_max_items_' + region.id + '" type="number" min="1" step="1" name="region_schedule_max_items_' + region.id + '" class="form-control" value="' + escapeHtml(currentMaxItems || 5) + '" />' +
|
||||
'</div>' +
|
||||
'<div class="col-12 col-lg-9 text-body-secondary small">Use the selected group and display mode to choose which schedule entry fields are available.</div>' +
|
||||
'</div>' +
|
||||
'<div class="api-region-placeholder-section schedule-placeholder-section" data-schedule-placeholder-chips>' +
|
||||
'<div class="api-region-placeholder-title">Available placeholders</div>' +
|
||||
'<div class="d-flex flex-wrap gap-2">' + renderSchedulePlaceholderChips(currentGroup, currentEntries) + '</div>' +
|
||||
'<div class="text-body-secondary small mt-2">Format start and end values with expressions like <code>{{start.format("MMM D, YYYY h:mm A")}}</code> and <code>{{end.format("MMM D, YYYY h:mm A")}}</code>.</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function getTextStyle(region, current) {
|
||||
var defaultStyle = getDefaultStyle();
|
||||
return {
|
||||
font_family: String(current.font_family || region.font_family || defaultStyle.font_family || 'Arial').trim() || 'Arial',
|
||||
font_size: Math.max(8, Number(current.font_size || region.font_size || defaultStyle.font_size || 28)),
|
||||
font_color: String(current.font_color || region.font_color || defaultStyle.font_color || '#000000').trim() || '#000000'
|
||||
};
|
||||
}
|
||||
|
||||
function updateSchedulePlaceholderPanel(card) {
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
|
||||
var regionId = card.getAttribute('data-region-id');
|
||||
var panel = card.querySelector('[data-schedule-placeholder-chips] .d-flex.flex-wrap.gap-2');
|
||||
if (!regionId || !panel) {
|
||||
return;
|
||||
}
|
||||
|
||||
var groups = getScheduleGroups();
|
||||
var current = getCurrentSelection(regionId, card, {}, groups);
|
||||
panel.innerHTML = renderSchedulePlaceholderChips(current.group, current.entries);
|
||||
}
|
||||
|
||||
if (!window.__scheduleRegionPlaceholderRefreshInstalled) {
|
||||
window.__scheduleRegionPlaceholderRefreshInstalled = true;
|
||||
|
||||
document.addEventListener('change', function (event) {
|
||||
var target = event.target;
|
||||
if (!target || !target.closest) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target.closest('select[name^="region_schedule_group_id_"], select[name^="region_schedule_display_mode_"], input[name^="region_schedule_max_items_"]')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var card = target.closest('[data-region-id]');
|
||||
if (card) {
|
||||
updateSchedulePlaceholderPanel(card);
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
function buildEditorCardContext(context) {
|
||||
return {
|
||||
region: context.region,
|
||||
current: context.current || {},
|
||||
scheduleGroups: Array.isArray(context.scheduleGroups) ? context.scheduleGroups : []
|
||||
};
|
||||
}
|
||||
|
||||
function buildPreviewRenderContext(region, card, existingContent, scheduleGroups) {
|
||||
var current = getCurrentConfig(region, existingContent || {});
|
||||
var scheduleGroupInput = card && card.querySelector ? card.querySelector('select[name="region_schedule_group_id_' + region.id + '"]') : null;
|
||||
var displayModeInput = card && card.querySelector ? card.querySelector('select[name="region_schedule_display_mode_' + region.id + '"]') : null;
|
||||
var maxItemsInput = card && card.querySelector ? card.querySelector('input[name="region_schedule_max_items_' + region.id + '"]') : null;
|
||||
var textAreaInput = card && card.querySelector ? card.querySelector('textarea.editor-source') : null;
|
||||
var hiddenInput = card && card.querySelector ? card.querySelector('input[type="hidden"][name="region_text_' + region.id + '"]') : null;
|
||||
var editor = window.tinymce && typeof window.tinymce.get === 'function' ? window.tinymce.get('slide-editor-region-' + region.id) : null;
|
||||
var value = String(editor ? editor.getContent({ format: 'html' }) : (hiddenInput && hiddenInput.value !== undefined ? hiddenInput.value : (textAreaInput && textAreaInput.value !== undefined ? textAreaInput.value : current.value)));
|
||||
|
||||
return {
|
||||
value: value,
|
||||
style: getTextStyle(region, current),
|
||||
schedule_group_id: scheduleGroupInput ? scheduleGroupInput.value || current.schedule_group_id : current.schedule_group_id,
|
||||
display_mode: displayModeInput ? displayModeInput.value || current.display_mode : current.display_mode,
|
||||
max_items: maxItemsInput ? maxItemsInput.value || current.max_items : current.max_items,
|
||||
existingContent: existingContent || {},
|
||||
scheduleGroups: Array.isArray(scheduleGroups) ? scheduleGroups : []
|
||||
};
|
||||
}
|
||||
|
||||
function renderPreview(region, regionContent, context) {
|
||||
var groups = context && context.scheduleGroups ? context.scheduleGroups : [];
|
||||
var style = regionContent && regionContent.style ? regionContent.style : getTextStyle(region, regionContent || {});
|
||||
var value = String(regionContent && (regionContent.value !== undefined ? regionContent.value : regionContent.text !== undefined ? regionContent.text : '') || '').trim();
|
||||
var groupId = regionContent && regionContent.schedule_group_id !== undefined ? regionContent.schedule_group_id : '';
|
||||
var displayMode = regionContent && regionContent.display_mode ? regionContent.display_mode : 'upcoming';
|
||||
var maxItems = regionContent && regionContent.max_items !== undefined ? regionContent.max_items : 5;
|
||||
var group = getGroupById(groupId, groups);
|
||||
var entries = getVisibleEntries(groupId, displayMode, maxItems, groups);
|
||||
if (!entries.length) {
|
||||
entries = getEntries(groupId, groups).slice(0, Math.max(1, Number(maxItems || 5)));
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return '<div class="slide-preview-region schedule" style="width:100%;height:100%;overflow:hidden;' + (style.font_family ? 'font-family:' + escapeHtml(style.font_family) + ';' : '') + (style.font_size ? 'font-size:' + Math.max(1, Math.round(Number(style.font_size))) + 'px;' : '') + (style.font_color ? 'color:' + escapeHtml(style.font_color) + ';' : '') + '"></div>';
|
||||
}
|
||||
|
||||
return '<div class="slide-preview-region schedule" style="width:100%;height:100%;overflow:hidden;' + (style.font_family ? 'font-family:' + escapeHtml(style.font_family) + ';' : '') + (style.font_size ? 'font-size:' + Math.max(1, Math.round(Number(style.font_size))) + 'px;' : '') + (style.font_color ? 'color:' + escapeHtml(style.font_color) + ';' : '') + '">' + entries.map(function (entry, index) {
|
||||
return '<div class="schedule-region-entry" data-schedule-entry-index="' + index + '">' + sanitizeRichText(renderTemplate(value, Object.assign({}, entry || {}, {
|
||||
start: entry && entry.start_datetime !== undefined ? entry.start_datetime : '',
|
||||
end: entry && entry.end_datetime !== undefined ? entry.end_datetime : '',
|
||||
group: group || {},
|
||||
entries: entries,
|
||||
index: index + 1
|
||||
}))) + '</div>';
|
||||
}).join('') + '</div>';
|
||||
}
|
||||
|
||||
registry.register('schedule', {
|
||||
label: 'Schedule',
|
||||
getDefaultStyle: getDefaultStyle,
|
||||
getDefaultRegionSize: function () {
|
||||
return { width: 520, height: 280 };
|
||||
},
|
||||
getCurrentConfig: getCurrentConfig,
|
||||
renderPreview: renderPreview,
|
||||
renderEditorCard: renderEditorCard,
|
||||
buildEditorCardContext: buildEditorCardContext,
|
||||
buildPreviewRenderContext: buildPreviewRenderContext
|
||||
});
|
||||
}());
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
(function () {
|
||||
var root = window;
|
||||
var transformPattern = /^(upper|lower|title)\(\)$/i;
|
||||
var transformPattern = /^([a-z_][a-z0-9_]*)\((.*)\)$/i;
|
||||
|
||||
var monthNamesShort = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
var monthNamesLong = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
||||
var dayNamesShort = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
var dayNamesLong = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
|
||||
function resolvePath(value, path) {
|
||||
var current = value;
|
||||
@@ -29,11 +34,15 @@
|
||||
|
||||
while (segments.length) {
|
||||
var candidate = String(segments[segments.length - 1] || '').trim();
|
||||
if (!transformPattern.test(candidate)) {
|
||||
var match = candidate.match(transformPattern);
|
||||
if (!match) {
|
||||
break;
|
||||
}
|
||||
|
||||
transforms.unshift(candidate.replace(/\(\)$/g, '').toLowerCase());
|
||||
transforms.unshift({
|
||||
name: String(match[1] || '').trim().toLowerCase(),
|
||||
args: match[2] ? splitTransformArgs(match[2]) : []
|
||||
});
|
||||
segments.pop();
|
||||
}
|
||||
|
||||
@@ -43,23 +52,89 @@
|
||||
};
|
||||
}
|
||||
|
||||
function splitTransformArgs(value) {
|
||||
var source = String(value || '').trim();
|
||||
if (!source) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ((source[0] === '"' && source[source.length - 1] === '"') || (source[0] === '\'' && source[source.length - 1] === '\'')) {
|
||||
return [source.slice(1, -1)];
|
||||
}
|
||||
|
||||
return source.split(',').map(function (item) {
|
||||
return String(item || '').trim();
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
function padNumber(value, size) {
|
||||
var text = String(Math.abs(Number(value || 0)));
|
||||
while (text.length < size) {
|
||||
text = '0' + text;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function formatDateValue(value, pattern) {
|
||||
var date = value instanceof Date ? value : new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var format = String(pattern || 'YYYY-MM-DD HH:mm').trim() || 'YYYY-MM-DD HH:mm';
|
||||
var hours24 = date.getHours();
|
||||
var hours12 = hours24 % 12 || 12;
|
||||
var tokenMap = {
|
||||
YYYY: String(date.getFullYear()),
|
||||
YY: String(date.getFullYear()).slice(-2),
|
||||
MMMM: monthNamesLong[date.getMonth()],
|
||||
MMM: monthNamesShort[date.getMonth()],
|
||||
MM: padNumber(date.getMonth() + 1, 2),
|
||||
M: String(date.getMonth() + 1),
|
||||
DD: padNumber(date.getDate(), 2),
|
||||
D: String(date.getDate()),
|
||||
dddd: dayNamesLong[date.getDay()],
|
||||
ddd: dayNamesShort[date.getDay()],
|
||||
HH: padNumber(hours24, 2),
|
||||
H: String(hours24),
|
||||
hh: padNumber(hours12, 2),
|
||||
h: String(hours12),
|
||||
mm: padNumber(date.getMinutes(), 2),
|
||||
m: String(date.getMinutes()),
|
||||
ss: padNumber(date.getSeconds(), 2),
|
||||
s: String(date.getSeconds()),
|
||||
A: hours24 >= 12 ? 'PM' : 'AM',
|
||||
a: hours24 >= 12 ? 'pm' : 'am'
|
||||
};
|
||||
|
||||
return format.replace(/\[([^\]]+)\]|YYYY|YY|MMMM|MMM|MM|M|DD|D|dddd|ddd|HH|H|hh|h|mm|m|ss|s|A|a/g, function (match, literal) {
|
||||
return literal || tokenMap[match] || match;
|
||||
});
|
||||
}
|
||||
|
||||
function applyTransform(value, transform) {
|
||||
var text = String(value === undefined || value === null ? '' : value);
|
||||
var name = String(transform && transform.name || '').trim().toLowerCase();
|
||||
var args = Array.isArray(transform && transform.args) ? transform.args : [];
|
||||
|
||||
if (transform === 'lower') {
|
||||
if (name === 'lower') {
|
||||
return text.toLowerCase();
|
||||
}
|
||||
|
||||
if (transform === 'upper') {
|
||||
if (name === 'upper') {
|
||||
return text.toUpperCase();
|
||||
}
|
||||
|
||||
if (transform === 'title') {
|
||||
if (name === 'title') {
|
||||
return text.toLowerCase().replace(/\b([a-z])/g, function (match, letter) {
|
||||
return String(letter || '').toUpperCase();
|
||||
});
|
||||
}
|
||||
|
||||
if (name === 'format' || name === 'date' || name === 'datetime' || name === 'time') {
|
||||
return formatDateValue(value, args[0] || (name === 'time' ? 'h:mm A' : name === 'date' ? 'MMM D, YYYY' : 'MMM D, YYYY h:mm A'));
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,22 @@ export function createSlideFormEditorController(options) {
|
||||
return String(value === undefined || value === null ? '' : value).trim();
|
||||
}
|
||||
|
||||
function isEmptyRichTextValue(value) {
|
||||
var raw = String(value === undefined || value === null ? '' : value).trim();
|
||||
if (!raw) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var stripped = raw
|
||||
.replace(/<\s*br\s*\/?>/gi, '')
|
||||
.replace(/<p[^>]*>(?:\s| |<br\s*\/?>)*<\/p>/gi, '')
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.replace(/ /gi, '')
|
||||
.trim();
|
||||
|
||||
return !stripped;
|
||||
}
|
||||
|
||||
function normalizeFontSizeValue(value) {
|
||||
var raw = String(value || '').trim().toLowerCase();
|
||||
if (/^\d+(?:\.\d+)?px$/.test(raw)) {
|
||||
@@ -156,6 +172,7 @@ export function createSlideFormEditorController(options) {
|
||||
editor.save();
|
||||
}
|
||||
var content = editor.getContent({ format: 'html' });
|
||||
content = isEmptyRichTextValue(content) ? '' : content;
|
||||
if (hidden) {
|
||||
hidden.value = content;
|
||||
}
|
||||
@@ -170,6 +187,7 @@ export function createSlideFormEditorController(options) {
|
||||
|
||||
editor.on('init', function () {
|
||||
var content = editor.getContent({ format: 'html' });
|
||||
content = isEmptyRichTextValue(content) ? '' : content;
|
||||
if (hidden) {
|
||||
hidden.value = content;
|
||||
}
|
||||
@@ -215,20 +233,21 @@ export function createSlideFormEditorController(options) {
|
||||
promotion: false,
|
||||
statusbar: true,
|
||||
resize: true,
|
||||
plugins: 'lists link code advlist fullscreen',
|
||||
toolbar: 'undo redo | fontfamily fontsizeinput | forecolor backcolor bold italic underline strikethrough subscript superscript removeformat | align lineheight indent outdent bullist numlist | fullscreen',
|
||||
plugins: 'lists link code advlist fullscreen table',
|
||||
toolbar: 'undo redo | fontfamily fontsizeinput | forecolor backcolor bold italic underline strikethrough subscript superscript removeformat | align lineheight indent outdent bullist numlist table | fullscreen',
|
||||
toolbar_mode: 'sliding',
|
||||
license_key: 'gpl',
|
||||
skin: themeAssets.skinName,
|
||||
skin_url: themeAssets.skinUrl,
|
||||
content_css: getContentCss(),
|
||||
body_class: themeAssets.bodyClass,
|
||||
content_style: 'body { font-family: ' + defaultEditorFontFamily + '; font-size: 32px; line-height: 1.5; background-color: ' + getEditorBackgroundColorValue() + '; } p { margin: 1em 0; } p:first-child { margin-top: 0; } p:last-child { margin-bottom: 1em; }' + (editorContentStyle ? ' ' + editorContentStyle : ''),
|
||||
content_style: 'body { font-family: ' + defaultEditorFontFamily + '; font-size: 32px; line-height: 1.5; background-color: ' + getEditorBackgroundColorValue() + '; } p { margin: 1em 0; } p:first-child { margin-top: 0; } p:last-child { margin-bottom: 1em; } table { border-collapse: collapse; width: 100%; } td, th { border: 1px solid currentColor; padding: 0.35em 0.5em; vertical-align: top; } th { font-weight: 700; }' + (editorContentStyle ? ' ' + editorContentStyle : ''),
|
||||
font_family_formats: getFontFamilyFormats(),
|
||||
font_size_input_default_unit: 'px',
|
||||
forced_root_block: 'p',
|
||||
force_br_newlines: false,
|
||||
newline_behavior: 'default',
|
||||
placeholder: String(source.getAttribute('placeholder') || '').trim(),
|
||||
setup: function (editor) {
|
||||
editorInstances.set(regionId, editor);
|
||||
attachEditorEvents(regionId, editor);
|
||||
|
||||
@@ -8,6 +8,7 @@ export function createSlideFormRegionHelpers(options) {
|
||||
var existingContent = settings.existingContent || {};
|
||||
var rssFeeds = Array.isArray(settings.rssFeeds) ? settings.rssFeeds : [];
|
||||
var apiSources = Array.isArray(settings.apiSources) ? settings.apiSources : [];
|
||||
var scheduleGroups = Array.isArray(settings.scheduleGroups) ? settings.scheduleGroups : [];
|
||||
var getRegionTypeModule = typeof settings.getRegionTypeModule === 'function' ? settings.getRegionTypeModule : function () { return null; };
|
||||
var placeholderUtils = window.placeholderUtils || {};
|
||||
var placeholderChips = window.placeholderChips || {};
|
||||
@@ -459,6 +460,15 @@ export function createSlideFormRegionHelpers(options) {
|
||||
};
|
||||
}
|
||||
|
||||
function getCurrentScheduleConfig(region) {
|
||||
var current = existingContent[region.region_key] || {};
|
||||
return {
|
||||
schedule_group_id: current.schedule_group_id === undefined || current.schedule_group_id === null || current.schedule_group_id === '' ? '' : Number(current.schedule_group_id),
|
||||
display_mode: String(current.display_mode || 'upcoming').trim().toLowerCase() || 'upcoming',
|
||||
max_items: Math.max(1, Number(current.max_items || 5))
|
||||
};
|
||||
}
|
||||
|
||||
function getCurrentRegionVideoDuration(region) {
|
||||
var current = existingContent[region.region_key] || {};
|
||||
var duration = Math.round(Number(current.duration_seconds || 0) * 1000) / 1000;
|
||||
@@ -470,13 +480,13 @@ export function createSlideFormRegionHelpers(options) {
|
||||
if (module && typeof module.buildEditorCardContext === 'function') {
|
||||
return module.buildEditorCardContext({
|
||||
region: region,
|
||||
current: region.region_type === 'rss' || region.region_type === 'api' || region.region_type === 'time-date' ? (existingContent[region.region_key] || {}) : getCurrentRegionValue(region),
|
||||
current: region.region_type === 'rss' || region.region_type === 'api' || region.region_type === 'time-date' || region.region_type === 'schedule' ? (existingContent[region.region_key] || {}) : getCurrentRegionValue(region),
|
||||
currentDuration: getCurrentRegionVideoDuration(region),
|
||||
regionRatio: reduceAspectRatio(region.width, region.height),
|
||||
uploadMaxLabel: uploadMaxLabel,
|
||||
uploadVideoMaxLabel: uploadVideoMaxLabel,
|
||||
disableAudio: (existingContent[region.region_key] || {}).disable_audio,
|
||||
config: region.region_type === 'api' ? getCurrentApiConfig(region) : getCurrentRssConfig(region),
|
||||
config: region.region_type === 'api' ? getCurrentApiConfig(region) : region.region_type === 'schedule' ? getCurrentScheduleConfig(region) : getCurrentRssConfig(region),
|
||||
style: getCurrentTextStyle(region),
|
||||
fontSize: getCurrentTextStyle(region).font_size,
|
||||
feedOptions: rssFeeds.map(function (feed) {
|
||||
@@ -492,8 +502,9 @@ export function createSlideFormRegionHelpers(options) {
|
||||
return '<span class="chip">{{' + escapeHtml(field) + '}}</span>';
|
||||
}).join('')
|
||||
: buildLimitedPlaceholderChipMarkup(getApiFieldList(getCurrentApiConfig(region).source_id, getCurrentApiConfig(region).items_path), 'No JSON fields available.'),
|
||||
sampleDataPreview: buildApiSampleDataMarkup(getCurrentApiConfig(region).source_id, getCurrentApiConfig(region).item_number, getCurrentApiConfig(region).items_path)
|
||||
}, existingContent, region.region_type === 'rss' ? rssFeeds : apiSources);
|
||||
sampleDataPreview: buildApiSampleDataMarkup(getCurrentApiConfig(region).source_id, getCurrentApiConfig(region).item_number, getCurrentApiConfig(region).items_path),
|
||||
scheduleGroups: scheduleGroups
|
||||
}, existingContent, region.region_type === 'rss' ? rssFeeds : region.region_type === 'schedule' ? scheduleGroups : apiSources);
|
||||
}
|
||||
|
||||
var textStyle = getCurrentTextStyle(region);
|
||||
@@ -510,7 +521,7 @@ export function createSlideFormRegionHelpers(options) {
|
||||
uploadMaxLabel: uploadMaxLabel,
|
||||
uploadVideoMaxLabel: uploadVideoMaxLabel,
|
||||
disableAudio: currentContent.disable_audio,
|
||||
config: region.region_type === 'api' ? apiConfig : rssConfig,
|
||||
config: region.region_type === 'api' ? apiConfig : region.region_type === 'schedule' ? getCurrentScheduleConfig(region) : rssConfig,
|
||||
style: textStyle,
|
||||
fontSize: textStyle.font_size,
|
||||
feedOptions: rssFeeds.map(function (feed) {
|
||||
@@ -526,7 +537,8 @@ export function createSlideFormRegionHelpers(options) {
|
||||
return '<span class="chip">{{' + escapeHtml(field) + '}}</span>';
|
||||
}).join('')
|
||||
: buildLimitedPlaceholderChipMarkup(getApiFieldList(apiConfig.source_id, apiItemsPath), 'No JSON fields available.'),
|
||||
sampleDataPreview: buildApiSampleDataMarkup(apiConfig.source_id, apiConfig.item_number, apiItemsPath)
|
||||
sampleDataPreview: buildApiSampleDataMarkup(apiConfig.source_id, apiConfig.item_number, apiItemsPath),
|
||||
scheduleGroups: scheduleGroups
|
||||
};
|
||||
}
|
||||
|
||||
@@ -542,7 +554,7 @@ export function createSlideFormRegionHelpers(options) {
|
||||
function getPreviewRegionContent(card, region) {
|
||||
var module = getRegionTypeModule(region.region_type);
|
||||
if (module && typeof module.buildPreviewRenderContext === 'function') {
|
||||
return module.buildPreviewRenderContext(region, card, existingContent, region.region_type === 'rss' ? rssFeeds : apiSources);
|
||||
return module.buildPreviewRenderContext(region, card, existingContent, region.region_type === 'rss' ? rssFeeds : region.region_type === 'schedule' ? scheduleGroups : apiSources);
|
||||
}
|
||||
|
||||
var current = existingContent[region.region_key];
|
||||
@@ -592,6 +604,14 @@ export function createSlideFormRegionHelpers(options) {
|
||||
content.source_id = apiSourceInput ? apiSourceInput.value : content.source_id;
|
||||
content.item_number = apiItemInput ? apiItemInput.value : content.item_number;
|
||||
content.items_path = apiItemsPathInput ? apiItemsPathInput.value : content.items_path;
|
||||
} else if (region.region_type === 'schedule') {
|
||||
var scheduleGroupInput = card.querySelector('select[name="region_schedule_group_id_' + region.id + '"]');
|
||||
var scheduleDisplayModeInput = card.querySelector('select[name="region_schedule_display_mode_' + region.id + '"]');
|
||||
var scheduleMaxItemsInput = card.querySelector('input[name="region_schedule_max_items_' + region.id + '"]');
|
||||
var currentSchedule = getCurrentScheduleConfig(region);
|
||||
content.schedule_group_id = scheduleGroupInput ? scheduleGroupInput.value : currentSchedule.schedule_group_id;
|
||||
content.display_mode = scheduleDisplayModeInput ? scheduleDisplayModeInput.value : currentSchedule.display_mode;
|
||||
content.max_items = scheduleMaxItemsInput ? scheduleMaxItemsInput.value : currentSchedule.max_items;
|
||||
} else {
|
||||
content.value = hiddenInput ? hiddenInput.value : (content.value || '');
|
||||
}
|
||||
@@ -602,7 +622,7 @@ export function createSlideFormRegionHelpers(options) {
|
||||
function buildPreviewRenderContext(region, card) {
|
||||
var module = getRegionTypeModule(region.region_type);
|
||||
if (module && typeof module.buildPreviewRenderContext === 'function') {
|
||||
return module.buildPreviewRenderContext(region, card, existingContent, region.region_type === 'rss' ? rssFeeds : apiSources);
|
||||
return module.buildPreviewRenderContext(region, card, existingContent, region.region_type === 'rss' ? rssFeeds : region.region_type === 'schedule' ? scheduleGroups : apiSources);
|
||||
}
|
||||
|
||||
var textStyle = getCurrentTextStyle(region);
|
||||
@@ -619,6 +639,10 @@ export function createSlideFormRegionHelpers(options) {
|
||||
item_number: previewContent.item_number,
|
||||
source_id: previewContent.source_id,
|
||||
items_path: previewContent.items_path,
|
||||
schedule_group_id: previewContent.schedule_group_id,
|
||||
display_mode: previewContent.display_mode,
|
||||
max_items: previewContent.max_items,
|
||||
scheduleGroups: scheduleGroups,
|
||||
font_size: textStyle.font_size,
|
||||
font_color: textStyle.font_color,
|
||||
font_family: textStyle.font_family
|
||||
|
||||
@@ -21,6 +21,7 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev
|
||||
var templates = Array.isArray(slideEditorData.templates) ? slideEditorData.templates : [];
|
||||
var rssFeeds = Array.isArray(slideEditorData.rssFeeds) ? slideEditorData.rssFeeds : [];
|
||||
var apiSources = Array.isArray(slideEditorData.apiSources) ? slideEditorData.apiSources : [];
|
||||
var scheduleGroups = Array.isArray(slideEditorData.scheduleGroups) ? slideEditorData.scheduleGroups : [];
|
||||
var fontStylesheetHref = String(slideEditorData.fontStylesheetHref || '').trim();
|
||||
var existingTemplateId = slideEditorData.existingTemplateId !== undefined ? slideEditorData.existingTemplateId : null;
|
||||
var existingContent = slideEditorData.existingContent || {};
|
||||
@@ -49,7 +50,7 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev
|
||||
var currentPreviewCanvasWidth = 0;
|
||||
var currentPreviewCanvasHeight = 0;
|
||||
var sidebarSyncFrame = 0;
|
||||
var sidebarTopOffset = 16;
|
||||
var sidebarBaseOffset = 16;
|
||||
var templateSelectorLock = createTemplateSelectorLockController(templateSelect);
|
||||
var regionTypes = window.pulseRegionTypes || {};
|
||||
|
||||
@@ -119,6 +120,7 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev
|
||||
existingContent: existingContent,
|
||||
rssFeeds: rssFeeds,
|
||||
apiSources: apiSources,
|
||||
scheduleGroups: scheduleGroups,
|
||||
defaultFontSize: DEFAULT_FONT_SIZE,
|
||||
uploadMaxLabel: uploadMaxLabel,
|
||||
uploadVideoMaxLabel: uploadVideoMaxLabel,
|
||||
@@ -487,6 +489,9 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev
|
||||
return;
|
||||
}
|
||||
|
||||
var stickyHeader = document.querySelector('.app-header');
|
||||
var stickyHeaderHeight = stickyHeader ? Math.max(0, Math.round(stickyHeader.getBoundingClientRect().height || stickyHeader.offsetHeight || 0)) : 0;
|
||||
var sidebarTopOffset = sidebarBaseOffset + stickyHeaderHeight + 12;
|
||||
var shellRect = slideEditorShell.getBoundingClientRect();
|
||||
var sidebarHeight = slideEditorSidebar.offsetHeight;
|
||||
var shellTop = window.scrollY + shellRect.top;
|
||||
|
||||
Reference in New Issue
Block a user