Release v2.6.19

This commit is contained in:
2026-08-09 11:57:40 +01:00
parent 49c72923b4
commit 459f84ed94
34 changed files with 1650 additions and 256 deletions
@@ -2,14 +2,17 @@
(function () {
var form = document.getElementById('timetable-group-form');
var timezoneInput = document.getElementById('timetable-group-timezone');
var body = document.querySelector('[data-timetable-entries-body]');
var addButton = document.querySelector('[data-add-timetable-entry]');
var template = document.getElementById('timetable-entry-row-template');
if (!form || !body || !addButton || !template) {
if (!form || !timezoneInput || !body || !addButton || !template) {
return;
}
var DEFAULT_TIME_ZONE = 'Europe/London';
function markDirty() {
form.dataset.dirty = 'true';
}
@@ -18,43 +21,150 @@
return String(value).padStart(2, '0');
}
function formatDateTimeLocalValue(date) {
function getDefaultTimeZone() {
try {
return Intl.DateTimeFormat().resolvedOptions().timeZone || DEFAULT_TIME_ZONE;
} catch (_error) {
return DEFAULT_TIME_ZONE;
}
}
function resolveTimeZone(value) {
var raw = String(value || '').trim();
if (!raw) {
return getDefaultTimeZone();
}
try {
new Intl.DateTimeFormat('en-GB', { timeZone: raw }).format(new Date());
return raw;
} catch (_error) {
return getDefaultTimeZone();
}
}
function parseDateTimeLocalParts(value) {
var raw = String(value || '').trim();
var match = raw.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/);
if (!match) {
return null;
}
return {
year: Number(match[1]),
month: Number(match[2]),
day: Number(match[3]),
hour: Number(match[4]),
minute: Number(match[5]),
second: Number(match[6] || '0')
};
}
function getDateTimeParts(date, timeZone) {
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
return null;
}
var resolvedTimeZone = resolveTimeZone(timeZone);
var parts = new Intl.DateTimeFormat('en-GB', {
timeZone: resolvedTimeZone,
hour12: false,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).formatToParts(date);
var mapped = Object.create(null);
parts.forEach(function (part) {
if (part && part.type && part.type !== 'literal') {
mapped[part.type] = part.value;
}
});
return mapped;
}
function formatDateTimeLocalValue(date, timeZone) {
var parts = getDateTimeParts(date, timeZone);
if (!parts) {
return '';
}
return [
String(date.getFullYear()).padStart(4, '0'),
String(parts.year || '').padStart(4, '0'),
'-',
pad(date.getMonth() + 1),
pad(parts.month),
'-',
pad(date.getDate()),
pad(parts.day),
'T',
pad(date.getHours()),
pad(parts.hour),
':',
pad(date.getMinutes())
pad(parts.minute)
].join('');
}
function parseUtcDateTimeLocalValue(value) {
var raw = String(value || '').trim();
if (!raw) {
return null;
function getTimeZoneOffsetMillis(date, timeZone) {
var parts = getDateTimeParts(date, timeZone);
if (!parts) {
return 0;
}
var normalized = /(?:[zZ]|[+-]\d\d(?::?\d\d)?)$/.test(raw) ? raw : raw + 'Z';
var date = new Date(normalized);
return Number.isNaN(date.getTime()) ? null : date;
var localAsUtc = Date.UTC(
Number(parts.year) || 0,
(Number(parts.month) || 1) - 1,
Number(parts.day) || 1,
Number(parts.hour) || 0,
Number(parts.minute) || 0,
Number(parts.second) || 0,
0
);
return localAsUtc - date.getTime();
}
function parseDateTimeLocalValue(value) {
function parseDateTimeLocalAsUtc(value, timeZone) {
var raw = String(value || '').trim();
if (!raw) {
return null;
var isoDate;
var parts = parseDateTimeLocalParts(value);
if (!parts) {
isoDate = /(?:[zZ]|[+-]\d\d(?::?\d\d)?)$/.test(raw) ? new Date(raw) : null;
return isoDate && !Number.isNaN(isoDate.getTime()) ? isoDate : null;
}
var date = new Date(raw);
return Number.isNaN(date.getTime()) ? null : date;
var resolvedTimeZone = resolveTimeZone(timeZone);
var utcMillis = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second, 0);
var date = new Date(utcMillis);
var offset = getTimeZoneOffsetMillis(date, resolvedTimeZone);
var adjusted = new Date(utcMillis - offset);
var adjustedOffset = getTimeZoneOffsetMillis(adjusted, resolvedTimeZone);
if (adjustedOffset !== offset) {
adjusted = new Date(utcMillis - adjustedOffset);
}
return Number.isNaN(adjusted.getTime()) ? null : adjusted;
}
function convertDateTimeLocalValue(value, sourceTimeZone, targetTimeZone) {
var raw = String(value || '').trim();
if (!raw) {
return '';
}
var utcDate = parseDateTimeLocalAsUtc(raw, sourceTimeZone);
if (!utcDate) {
return '';
}
var resolvedTargetTimeZone = resolveTimeZone(targetTimeZone);
if (resolvedTargetTimeZone === 'UTC') {
return utcDate.toISOString();
}
return formatDateTimeLocalValue(utcDate, resolvedTargetTimeZone);
}
function clearFieldValidity(input) {
@@ -82,17 +192,21 @@
}
function validateEntryRow(row) {
if (!row) {
return;
}
var startInput = row.querySelector('[name="entry_start_datetime[]"]');
var endInput = row.querySelector('[name="entry_end_datetime[]"]');
var timezone = resolveTimeZone(timezoneInput.value);
var startInput;
var endInput;
var startValue;
var endValue;
var startDate;
var endDate;
if (!row) {
return;
}
startInput = row.querySelector('[name="entry_start_datetime[]"]');
endInput = row.querySelector('[name="entry_end_datetime[]"]');
clearFieldValidity(endInput);
if (!startInput || !endInput) {
@@ -106,8 +220,8 @@
return;
}
startDate = parseDateTimeLocalValue(startValue);
endDate = parseDateTimeLocalValue(endValue);
startDate = parseDateTimeLocalAsUtc(startValue, timezone);
endDate = parseDateTimeLocalAsUtc(endValue, timezone);
if (!startDate || !endDate) {
return;
@@ -119,12 +233,15 @@
}
function bindRowValidation(row) {
var startInput;
var endInput;
if (!row) {
return;
}
var startInput = row.querySelector('[name="entry_start_datetime[]"]');
var endInput = row.querySelector('[name="entry_end_datetime[]"]');
startInput = row.querySelector('[name="entry_start_datetime[]"]');
endInput = row.querySelector('[name="entry_end_datetime[]"]');
if (!startInput || !endInput) {
return;
@@ -142,32 +259,37 @@
validateEntryRow(row);
}
function toUtcDateTimeLocalValue(value) {
var localDate = new Date(String(value || '').trim());
return Number.isNaN(localDate.getTime()) ? '' : localDate.toISOString();
}
function syncRowValuesToLocal(row) {
function syncRowValuesToTimeZone(row, sourceTimeZone, targetTimeZone) {
if (!row) {
return;
}
['entry_start_datetime[]', 'entry_end_datetime[]'].forEach(function (name) {
var input = row.querySelector('[name="' + name + '"]');
if (!input || input.dataset.timetableTimezoneSynced === 'true') {
var convertedValue;
if (!input) {
return;
}
var localValue = formatDateTimeLocalValue(parseUtcDateTimeLocalValue(input.value));
if (localValue) {
input.value = localValue;
convertedValue = convertDateTimeLocalValue(input.value, sourceTimeZone, targetTimeZone);
if (convertedValue) {
input.value = convertedValue;
}
input.dataset.timetableTimezoneSynced = 'true';
});
}
function syncAllRowsToTimeZone(sourceTimeZone, targetTimeZone) {
body.querySelectorAll('[data-timetable-entry-row]').forEach(function (row) {
syncRowValuesToTimeZone(row, sourceTimeZone, targetTimeZone);
validateEntryRow(row);
});
}
function syncFormDataToUtc(formData) {
var rows = body.querySelectorAll('[data-timetable-entry-row]');
var currentTimeZone = resolveTimeZone(timezoneInput.value);
['entry_id[]', 'entry_title[]', 'entry_short_description[]', 'entry_start_datetime[]', 'entry_end_datetime[]'].forEach(function (name) {
formData.delete(name);
});
@@ -182,8 +304,8 @@
formData.append('entry_id[]', idInput ? idInput.value : '');
formData.append('entry_title[]', titleInput ? titleInput.value : '');
formData.append('entry_short_description[]', descriptionInput ? descriptionInput.value : '');
formData.append('entry_start_datetime[]', startInput ? toUtcDateTimeLocalValue(startInput.value) : '');
formData.append('entry_end_datetime[]', endInput ? toUtcDateTimeLocalValue(endInput.value) : '');
formData.append('entry_start_datetime[]', startInput ? convertDateTimeLocalValue(startInput.value, currentTimeZone, 'UTC') : '');
formData.append('entry_end_datetime[]', endInput ? convertDateTimeLocalValue(endInput.value, currentTimeZone, 'UTC') : '');
});
}
@@ -210,9 +332,16 @@
markDirty();
}
function handleTimezoneChange() {
var nextTimeZone = resolveTimeZone(timezoneInput.value || DEFAULT_TIME_ZONE);
form.dataset.timetableTimezone = nextTimeZone;
markDirty();
}
form.dataset.timetableTimezone = resolveTimeZone(timezoneInput.value || DEFAULT_TIME_ZONE);
body.querySelectorAll('[data-timetable-entry-row]').forEach(function (row) {
bindRemove(row);
syncRowValuesToLocal(row);
bindRowValidation(row);
});
@@ -220,5 +349,8 @@
syncFormDataToUtc(event.formData);
});
timezoneInput.addEventListener('change', handleTimezoneChange);
timezoneInput.addEventListener('input', handleTimezoneChange);
addButton.addEventListener('click', addRow);
}());
+11 -1
View File
@@ -272,8 +272,18 @@
function buildPreviewRenderContext(region, card, existingContent, sources) {
var current = getCurrentConfig(region, existingContent || {}, sources || []);
var editor = window.tinymce && typeof window.tinymce.get === 'function' ? window.tinymce.get('slide-editor-region-' + region.id) : null;
var fallbackValue = card && card.querySelector ? ((card.querySelector('textarea[name="region_text_' + region.id + '"]') || {}).value || current.value || '') : current.value;
var content = card && card.querySelector ? {
value: String(editor ? editor.getContent({ format: 'html' }) : ((card.querySelector('textarea[name="region_text_' + region.id + '"]') || {}).value || current.value || '')),
value: String((function () {
if (!editor || typeof editor.getContent !== 'function') {
return fallbackValue;
}
try {
return editor.getContent({ format: 'html' });
} catch (_error) {
return fallbackValue;
}
}())),
source_id: (card.querySelector('select[name="region_api_source_id_' + region.id + '"]') || {}).value || current.source_id,
item_number: (card.querySelector('input[name="region_api_item_number_' + region.id + '"]') || {}).value || current.item_number,
items_path: card.querySelector('input[name="region_api_items_path_' + region.id + '"]') ? (card.querySelector('input[name="region_api_items_path_' + region.id + '"]') || {}).value : current.items_path
+11 -1
View File
@@ -277,8 +277,18 @@
function buildPreviewRenderContext(region, card, existingContent, feeds) {
var current = getCurrentConfig(region, existingContent || {});
var editor = window.tinymce && typeof window.tinymce.get === 'function' ? window.tinymce.get('slide-editor-region-' + region.id) : null;
var fallbackValue = card && card.querySelector ? ((card.querySelector('textarea[name="region_text_' + region.id + '"]') || {}).value || current.value || '') : current.value;
var content = card && card.querySelector ? {
value: String(editor ? editor.getContent({ format: 'html' }) : ((card.querySelector('textarea[name="region_text_' + region.id + '"]') || {}).value || current.value || '')),
value: String((function () {
if (!editor || typeof editor.getContent !== 'function') {
return fallbackValue;
}
try {
return editor.getContent({ format: 'html' });
} catch (_error) {
return fallbackValue;
}
}())),
feed_id: (card.querySelector('select[name="region_rss_feed_id_' + region.id + '"]') || {}).value || current.feed_id,
item_number: (card.querySelector('input[name="region_rss_item_number_' + region.id + '"]') || {}).value || current.item_number
} : current;
+116 -11
View File
@@ -43,6 +43,52 @@
return group && Array.isArray(group.entries) ? group.entries : [];
}
function getDefaultTimeZone() {
try {
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
} catch (_error) {
return 'UTC';
}
}
function resolveTimeZone(value) {
var raw = String(value || '').trim();
if (!raw) {
return getDefaultTimeZone();
}
try {
new Intl.DateTimeFormat('en-GB', { timeZone: raw }).format(new Date());
return raw;
} catch (_error) {
return getDefaultTimeZone();
}
}
function getTimezoneValues(group, dateValue) {
var timeZone = resolveTimeZone(group && (group.timezone || group.time_zone || ''));
var shortName = timeZone;
var targetDate = dateValue instanceof Date ? dateValue : new Date(dateValue || Date.now());
try {
var parts = new Intl.DateTimeFormat('en-GB', {
timeZone: timeZone,
timeZoneName: 'short'
}).formatToParts(Number.isNaN(targetDate.getTime()) ? new Date() : targetDate);
var timeZonePart = parts.find(function (part) {
return part && part.type === 'timeZoneName';
});
shortName = timeZonePart && timeZonePart.value ? String(timeZonePart.value) : timeZone;
} catch (_error) {
shortName = timeZone;
}
return {
tz: timeZone,
tz_short: shortName
};
}
function toDate(value) {
if (!value) {
return null;
@@ -119,9 +165,9 @@
var current = existingContent[region.region_key] || {};
return {
timetable_group_id: current.timetable_group_id === undefined || current.timetable_group_id === null || current.timetable_group_id === '' ? '' : Number(current.timetable_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)),
display_mode: String(current.display_mode || current.timetable_display_mode || 'upcoming').trim().toLowerCase() || 'upcoming',
value: String(current.text !== undefined ? current.text : current.value !== undefined ? current.value : ''),
max_items: Math.max(1, Number(current.max_items || current.timetable_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
@@ -143,14 +189,47 @@
}).join('');
}
function renderScheduleFormatTokenTable() {
if (window.timeDatePlaceholders && typeof window.timeDatePlaceholders.renderTable === 'function') {
return window.timeDatePlaceholders.renderTable();
}
return '' +
'<div class="table-responsive">' +
'<table class="table table-sm align-middle mb-0">' +
'<thead><tr><th scope="col">Format</th><th scope="col">Output</th><th scope="col">Description</th></tr></thead>' +
'<tbody>' +
'<tr><td><code>h</code></td><td>1-12</td><td>The hour, 12-hour clock</td></tr>' +
'<tr><td><code>hh</code></td><td>01-12</td><td>The hour, 12-hour clock, 2-digits</td></tr>' +
'<tr><td><code>m</code></td><td>0-59</td><td>The minute</td></tr>' +
'<tr><td><code>mm</code></td><td>00-59</td><td>The minute, 2-digits</td></tr>' +
'<tr><td><code>s</code></td><td>0-59</td><td>The second</td></tr>' +
'<tr><td><code>ss</code></td><td>00-59</td><td>The second, 2-digits</td></tr>' +
'<tr><td><code>A</code></td><td>AM/PM</td><td>Uppercase day period</td></tr>' +
'<tr><td><code>a</code></td><td>am/pm</td><td>Lowercase day period</td></tr>' +
'<tr><td><code>D</code></td><td>1-31</td><td>The day of the month</td></tr>' +
'<tr><td><code>DD</code></td><td>01-31</td><td>The day of the month, 2-digits</td></tr>' +
'<tr><td><code>ddd</code></td><td>Mon</td><td>The abbreviated weekday name</td></tr>' +
'<tr><td><code>dddd</code></td><td>Monday</td><td>The full weekday name</td></tr>' +
'<tr><td><code>M</code></td><td>1-12</td><td>The month, beginning at 1</td></tr>' +
'<tr><td><code>MM</code></td><td>01-12</td><td>The month, 2-digits</td></tr>' +
'<tr><td><code>MMM</code></td><td>Jan-Dec</td><td>The abbreviated month name</td></tr>' +
'<tr><td><code>MMMM</code></td><td>January-December</td><td>The full month name</td></tr>' +
'<tr><td><code>YY</code></td><td>18</td><td>The two-digit year</td></tr>' +
'<tr><td><code>YYYY</code></td><td>2018</td><td>The four-digit year</td></tr>' +
'</tbody>' +
'</table>' +
'</div>';
}
function getCurrentSelection(regionId, card, existingContent, timetableGroups) {
var current = existingContent[regionId] || {};
var timetableGroupInput = card && card.querySelector ? card.querySelector('select[name="region_timetable_group_id_' + regionId + '"]') : null;
var timetableDisplayModeInput = card && card.querySelector ? card.querySelector('select[name="region_timetable_display_mode_' + regionId + '"]') : null;
var timetableMaxItemsInput = card && card.querySelector ? card.querySelector('input[name="region_timetable_max_items_' + regionId + '"]') : null;
var groupId = timetableGroupInput ? timetableGroupInput.value : (current.timetable_group_id === undefined || current.timetable_group_id === null || current.timetable_group_id === '' ? '' : Number(current.timetable_group_id));
var displayMode = timetableDisplayModeInput ? timetableDisplayModeInput.value : String(current.display_mode || 'upcoming').trim().toLowerCase() || 'upcoming';
var maxItems = Math.max(1, Number(timetableMaxItemsInput ? timetableMaxItemsInput.value : current.max_items || 5));
var displayMode = timetableDisplayModeInput ? timetableDisplayModeInput.value : String(current.display_mode || current.timetable_display_mode || 'upcoming').trim().toLowerCase() || 'upcoming';
var maxItems = Math.max(1, Number(timetableMaxItemsInput ? timetableMaxItemsInput.value : current.max_items || current.timetable_max_items || 5));
var group = getGroupById(groupId, timetableGroups);
var entries = getVisibleEntries(groupId, displayMode, maxItems, timetableGroups);
if (!entries.length) {
@@ -169,10 +248,10 @@
var region = context.region;
var current = context.current || {};
var timetableGroups = Array.isArray(context.timetableGroups) ? context.timetableGroups : [];
var currentValue = String(current.value !== undefined ? current.value : current.text !== undefined ? current.text : '');
var currentValue = String(current.text !== undefined ? current.text : current.value !== undefined ? current.value : '');
var currentGroupId = current.timetable_group_id === undefined || current.timetable_group_id === null || current.timetable_group_id === '' ? '' : Number(current.timetable_group_id);
var currentDisplayMode = String(current.display_mode || 'upcoming').trim().toLowerCase() || 'upcoming';
var currentMaxItems = Math.max(1, Number(current.max_items || 5));
var currentDisplayMode = String(current.display_mode || current.timetable_display_mode || 'upcoming').trim().toLowerCase() || 'upcoming';
var currentMaxItems = Math.max(1, Number(current.max_items || current.timetable_max_items || 5));
var currentGroup = getGroupById(currentGroupId, timetableGroups);
var currentEntries = getVisibleEntries(currentGroupId, currentDisplayMode, currentMaxItems, timetableGroups);
if (!currentEntries.length) {
@@ -219,7 +298,12 @@
'<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-1">Placeholder values support transforms, for example <code>{{title.upper()}}</code>, <code>{{title.title()}}</code>, <code>{{title.lower()}}</code> or <code>{{start.format("MMM D, YYYY h:mm A")}}</code>.</div>' +
'<div class="text-body-secondary small mt-1">Placeholder values support transforms, for example <code>{{title.upper()}}</code>, <code>{{title.title()}}</code>, <code>{{title.lower()}}</code>, <code>{{start.format("MMM D, YYYY h:mm A")}}</code>, <code>{{start.tz()}}</code> or <code>{{start.tz_short()}}</code>.</div>' +
'<details class="mt-2">' +
'<summary class="small text-body-secondary">Supported date format tokens</summary>' +
'<div class="mt-2">' + renderScheduleFormatTokenTable() + '</div>' +
'<div class="text-body-secondary small mt-1">Use these tokens inside <code>.format(...)</code>; for example <code>{{start.format("MMM D, YYYY h:mm A")}}</code>.</div>' +
'</details>' +
'</div>' +
'</div>' +
'</div>' +
@@ -287,13 +371,29 @@
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)));
var fallbackValue = hiddenInput && hiddenInput.value !== undefined ? hiddenInput.value : (textAreaInput && textAreaInput.value !== undefined ? textAreaInput.value : current.value);
var currentGroup = getGroupById(timetableGroupInput ? timetableGroupInput.value : current.timetable_group_id, timetableGroups);
var timezoneValues = getTimezoneValues(currentGroup);
var value = String((function () {
if (!editor || typeof editor.getContent !== 'function') {
return fallbackValue;
}
try {
return editor.getContent({ format: 'html' });
} catch (_error) {
return fallbackValue;
}
}()));
return {
value: value,
style: getTextStyle(region, current),
timetable_group_id: timetableGroupInput ? timetableGroupInput.value || current.timetable_group_id : current.timetable_group_id,
display_mode: timetableDisplayModeInput ? timetableDisplayModeInput.value || current.display_mode : current.display_mode,
tz: timezoneValues.tz,
tz_short: timezoneValues.tz_short,
timeZone: timezoneValues.tz,
max_items: timetableMaxItemsInput ? timetableMaxItemsInput.value || current.max_items : current.max_items,
existingContent: existingContent || {},
timetableGroups: Array.isArray(timetableGroups) ? timetableGroups : []
@@ -303,7 +403,7 @@
function renderPreview(region, regionContent, context) {
var groups = context && context.timetableGroups ? context.timetableGroups : [];
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 value = String(regionContent && (regionContent.text !== undefined ? regionContent.text : regionContent.value !== undefined ? regionContent.value : '') || '').trim();
var groupId = regionContent && regionContent.timetable_group_id !== undefined ? regionContent.timetable_group_id : '';
var displayMode = regionContent && regionContent.display_mode ? regionContent.display_mode : 'upcoming';
var maxItems = regionContent && regionContent.max_items !== undefined ? regionContent.max_items : 5;
@@ -315,9 +415,14 @@
}
return '<div class="slide-preview-region timetable" 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) {
var entryDate = entry && (entry.start_datetime || entry.end_datetime || entry.date || entry.time || '');
var timezoneValues = getTimezoneValues(group, entryDate);
return '<div class="timetable-region-entry" data-timetable-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 : '',
tz: timezoneValues.tz,
tz_short: timezoneValues.tz_short,
timeZone: timezoneValues.tz,
group: group || {},
entries: entries,
index: index + 1
+11 -1
View File
@@ -84,7 +84,17 @@
} : {};
return {
value: String(editor ? editor.getContent({ format: 'html' }) : (hidden && hidden.value !== undefined ? hidden.value : (textarea && textarea.value !== undefined ? textarea.value : (current && current.value !== undefined ? current.value : '')))),
value: String((function () {
var fallback = hidden && hidden.value !== undefined ? hidden.value : (textarea && textarea.value !== undefined ? textarea.value : (current && current.value !== undefined ? current.value : ''));
if (!editor || typeof editor.getContent !== 'function') {
return fallback;
}
try {
return editor.getContent({ format: 'html' });
} catch (_error) {
return fallback;
}
}())),
style: style,
existingContent: existingContent || {}
};
+3 -3
View File
@@ -166,9 +166,9 @@
};
}
function resolveTimeDatePlaceholder(values, expression) {
function resolveTimeDatePlaceholder(values, expression, options) {
if (typeof placeholderUtils.resolvePlaceholderExpression === 'function' && typeof placeholderUtils.formatPlaceholderValue === 'function') {
return placeholderUtils.formatPlaceholderValue(placeholderUtils.resolvePlaceholderExpression(values, expression));
return placeholderUtils.formatPlaceholderValue(placeholderUtils.resolvePlaceholderExpression(values, expression, options || {}));
}
var parsed = String(expression || '').trim();
@@ -220,7 +220,7 @@
var template = String(format || '').trim();
var values = getFormattedParts(timeZone, date);
return template.replace(/\{\{\s*([a-zA-Z0-9_.()\-]+)\s*\}\}/g, function (_match, key) {
return String(resolveTimeDatePlaceholder(values, key) || '');
return String(resolveTimeDatePlaceholder(values, key, { timeZone: timeZone }) || '');
});
}
+173 -28
View File
@@ -75,36 +75,148 @@
return text;
}
function formatDateValue(value, pattern) {
var date = value instanceof Date ? value : new Date(value);
function getDefaultTimeZone() {
try {
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
} catch (_error) {
return 'UTC';
}
}
function getDateValue(value) {
return value instanceof Date ? value : new Date(value);
}
function resolveTimeZone(value) {
var raw = String(value || '').trim();
if (!raw) {
return getDefaultTimeZone();
}
try {
new Intl.DateTimeFormat('en-GB', { timeZone: raw }).format(new Date());
return raw;
} catch (_error) {
return getDefaultTimeZone();
}
}
function getDatePartsFromLocalTime(date) {
var dayPeriod = date.getHours() >= 12 ? 'PM' : 'AM';
return {
year: String(date.getFullYear()),
month: padNumber(date.getMonth() + 1, 2),
day: padNumber(date.getDate(), 2),
hour24: padNumber(date.getHours(), 2),
hour12: padNumber(date.getHours() % 12 || 12, 2),
minute: padNumber(date.getMinutes(), 2),
second: padNumber(date.getSeconds(), 2),
weekdayLong: dayNamesLong[date.getDay()],
weekdayShort: dayNamesShort[date.getDay()],
monthLong: monthNamesLong[date.getMonth()],
monthShort: monthNamesShort[date.getMonth()],
dayPeriod: dayPeriod
};
}
function getDatePartsFromTimeZone(date, timeZone) {
var resolvedTimeZone = resolveTimeZone(timeZone);
var baseFormatter = new Intl.DateTimeFormat('en-GB', {
timeZone: resolvedTimeZone,
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
var weekdayLongFormatter = new Intl.DateTimeFormat('en-GB', {
timeZone: resolvedTimeZone,
weekday: 'long'
});
var weekdayShortFormatter = new Intl.DateTimeFormat('en-GB', {
timeZone: resolvedTimeZone,
weekday: 'short'
});
var monthLongFormatter = new Intl.DateTimeFormat('en-GB', {
timeZone: resolvedTimeZone,
month: 'long'
});
var monthShortFormatter = new Intl.DateTimeFormat('en-GB', {
timeZone: resolvedTimeZone,
month: 'short'
});
var ampmFormatter = new Intl.DateTimeFormat('en-GB', {
timeZone: resolvedTimeZone,
hour12: true,
hour: '2-digit',
minute: '2-digit'
});
var numericParts = baseFormatter.formatToParts(date);
var weekdayLong = weekdayLongFormatter.formatToParts(date);
var weekdayShort = weekdayShortFormatter.formatToParts(date);
var monthLong = monthLongFormatter.formatToParts(date);
var monthShort = monthShortFormatter.formatToParts(date);
var ampm = ampmFormatter.formatToParts(date);
function getPart(parts, type) {
var match = parts.find(function (part) {
return part && part.type === type;
});
return match ? String(match.value || '') : '';
}
var dayPeriod = getPart(ampm, 'dayPeriod');
return {
year: getPart(numericParts, 'year'),
month: getPart(numericParts, 'month'),
day: getPart(numericParts, 'day'),
hour24: getPart(numericParts, 'hour'),
hour12: getPart(ampm, 'hour'),
minute: getPart(numericParts, 'minute'),
second: getPart(numericParts, 'second'),
weekdayLong: getPart(weekdayLong, 'weekday'),
weekdayShort: getPart(weekdayShort, 'weekday'),
monthLong: getPart(monthLong, 'month'),
monthShort: getPart(monthShort, 'month'),
dayPeriod: String(dayPeriod || '').toUpperCase(),
timeZone: resolvedTimeZone
};
}
function formatDateValue(value, pattern, timeZone) {
var date = getDateValue(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 parts = timeZone ? getDatePartsFromTimeZone(date, timeZone) : getDatePartsFromLocalTime(date);
var hours24 = parts.hour24;
var hours12 = parts.hour12;
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'
YYYY: parts.year,
YY: parts.year.slice(-2),
MMMM: parts.monthLong,
MMM: parts.monthShort,
MM: parts.month,
M: String(Number(parts.month) || 0),
DD: parts.day,
D: String(Number(parts.day) || 0),
dddd: parts.weekdayLong,
ddd: parts.weekdayShort,
HH: hours24,
H: String(Number(hours24) || 0),
hh: hours12,
h: String(Number(hours12) || 0),
mm: parts.minute,
m: String(Number(parts.minute) || 0),
ss: parts.second,
s: String(Number(parts.second) || 0),
A: parts.dayPeriod,
a: String(parts.dayPeriod || '').toLowerCase()
};
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) {
@@ -112,7 +224,32 @@
});
}
function applyTransform(value, transform) {
function getTimeZoneShortName(value, timeZone) {
var date = getDateValue(value);
if (Number.isNaN(date.getTime())) {
return '';
}
var resolvedTimeZone = resolveTimeZone(timeZone);
try {
var parts = new Intl.DateTimeFormat('en-GB', {
timeZone: resolvedTimeZone,
timeZoneName: 'short'
}).formatToParts(date);
var match = parts.find(function (part) {
return part && part.type === 'timeZoneName';
});
return match ? String(match.value || '') : resolvedTimeZone;
} catch (_error) {
return resolvedTimeZone;
}
}
function getTransformTimeZone(args, options) {
return resolveTimeZone((args && args[0]) || (options && options.timeZone) || '');
}
function applyTransform(value, transform, options) {
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 : [];
@@ -132,18 +269,26 @@
}
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 formatDateValue(value, args[0] || (name === 'time' ? 'h:mm A' : name === 'date' ? 'MMM D, YYYY' : 'MMM D, YYYY h:mm A'), getTransformTimeZone(args.slice(1), options));
}
if (name === 'tz') {
return getTransformTimeZone(args, options);
}
if (name === 'tz_short') {
return getTimeZoneShortName(value, getTransformTimeZone(args, options));
}
return text;
}
function resolvePlaceholderExpression(value, expression) {
function resolvePlaceholderExpression(value, expression, options) {
var parsed = parsePlaceholderExpression(expression);
var resolved = resolvePath(value, parsed.path);
parsed.transforms.forEach(function (transform) {
resolved = applyTransform(resolved, transform);
resolved = applyTransform(resolved, transform, options || {});
});
return resolved;
@@ -2,47 +2,79 @@
(function () {
var root = window;
var PLACEHOLDERS = [
{ token: 'h', label: 'h' },
{ token: 'hh', label: 'hh' },
{ token: 'm', label: 'm' },
{ token: 'mm', label: 'mm' },
{ token: 's', label: 's' },
{ token: 'ss', label: 'ss' },
{ token: 'd', label: 'd' },
{ token: 'dd', label: 'dd' },
{ token: 'M', label: 'M' },
{ token: 'MM', label: 'MM' },
{ token: 'y', label: 'y' },
{ token: 'yyyy', label: 'yyyy' },
{ token: 'yy', label: 'yy' },
{ token: 'ddd', label: 'ddd' },
{ token: 'dddd', label: 'dddd' },
{ token: 'MMM', label: 'MMM' },
{ token: 'MMMM', label: 'MMMM' },
{ token: 'a', label: 'a' },
{ token: 'tz', label: 'tz' },
{ token: 'tz_short', label: 'tz_short' }
var FORMAT_PLACEHOLDERS = [
{ token: 'h', output: '1-12', description: 'The hour, 12-hour clock' },
{ token: 'hh', output: '01-12', description: 'The hour, 12-hour clock, 2-digits' },
{ token: 'm', output: '0-59', description: 'The minute' },
{ token: 'mm', output: '00-59', description: 'The minute, 2-digits' },
{ token: 's', output: '0-59', description: 'The second' },
{ token: 'ss', output: '00-59', description: 'The second, 2-digits' },
{ token: 'A', output: 'AM/PM', description: 'Uppercase day period' },
{ token: 'a', output: 'am/pm', description: 'Lowercase day period' },
{ token: 'D', output: '1-31', description: 'The day of the month' },
{ token: 'DD', output: '01-31', description: 'The day of the month, 2-digits' },
{ token: 'ddd', output: 'Mon', description: 'The abbreviated weekday name' },
{ token: 'dddd', output: 'Monday', description: 'The full weekday name' },
{ token: 'M', output: '1-12', description: 'The month, beginning at 1' },
{ token: 'MM', output: '01-12', description: 'The month, 2-digits' },
{ token: 'MMM', output: 'Jan-Dec', description: 'The abbreviated month name' },
{ token: 'MMMM', output: 'January-December', description: 'The full month name' },
{ token: 'YY', output: '18', description: 'The two-digit year' },
{ token: 'YYYY', output: '2018', description: 'The four-digit year' }
];
var HELPER_PLACEHOLDERS = [
{ token: 'tz', output: 'Europe/London', description: 'The resolved time zone name' },
{ token: 'tz_short', output: 'BST/GMT', description: 'The time zone abbreviation for the selected date' }
];
var PLACEHOLDERS = FORMAT_PLACEHOLDERS.concat(HELPER_PLACEHOLDERS);
function getTokens() {
return PLACEHOLDERS.slice();
}
function renderChips() {
if (root.placeholderChips && typeof root.placeholderChips.renderChips === 'function') {
return root.placeholderChips.renderChips(PLACEHOLDERS.map(function (item) {
return item.token;
}));
}
function getFormatTokens() {
return FORMAT_PLACEHOLDERS.slice();
}
return PLACEHOLDERS.map(function (item) {
return '<span class="chip">{{' + item.token + '}}</span>';
}).join('');
function renderTable() {
return '' +
'<div class="table-responsive">' +
'<table class="table table-sm align-middle mb-0">' +
'<thead>' +
'<tr>' +
'<th scope="col">Format</th>' +
'<th scope="col">Output</th>' +
'<th scope="col">Description</th>' +
'</tr>' +
'</thead>' +
'<tbody>' +
FORMAT_PLACEHOLDERS.map(function (item) {
return '' +
'<tr>' +
'<td><code>' + item.token + '</code></td>' +
'<td>' + escapeHtml(item.output || '') + '</td>' +
'<td>' + escapeHtml(item.description || '') + '</td>' +
'</tr>';
}).join('') +
'</tbody>' +
'</table>' +
'</div>';
}
function escapeHtml(value) {
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
root.timeDatePlaceholders = {
getTokens: getTokens,
renderChips: renderChips
getFormatTokens: getFormatTokens,
renderTable: renderTable
};
}());
+23 -8
View File
@@ -72,6 +72,18 @@ export function createSlideFormEditorController(options) {
return window.tinymce || null;
}
function getEditorContentSafely(editor, fallbackValue) {
if (!editor || typeof editor.getContent !== 'function') {
return String(fallbackValue || '');
}
try {
return String(editor.getContent({ format: 'html' }));
} catch (_error) {
return String(fallbackValue || '');
}
}
function getThemeName() {
var theme = String(document.documentElement && document.documentElement.dataset && document.documentElement.dataset.bsTheme || 'light').trim().toLowerCase();
if (theme === 'auto') {
@@ -95,14 +107,19 @@ export function createSlideFormEditorController(options) {
}
function syncEditorState(regionId, editor, shouldLock, hydrated) {
var hidden = getEditorHiddenInput(regionId);
var sourceElm = editor && editor.targetElm ? editor.targetElm : null;
var fallbackContent = hidden && hidden.value !== undefined ? hidden.value : (sourceElm && sourceElm.value !== undefined ? sourceElm.value : '');
if (editor && typeof editor.save === 'function') {
if (typeof editor.blur === 'function') {
editor.blur();
}
editor.save();
}
var content = editor.getContent({ format: 'html' });
var content = sourceElm && sourceElm.value !== undefined ? String(sourceElm.value || '') : getEditorContentSafely(editor, fallbackContent);
content = isEmptyRichTextValue(content) ? '' : content;
var hidden = getEditorHiddenInput(regionId);
var sourceElm = editor && editor.targetElm ? editor.targetElm : null;
if (hidden) {
hidden.value = content;
@@ -211,7 +228,7 @@ export function createSlideFormEditorController(options) {
}
editor.on('init', function () {
var content = editor.getContent({ format: 'html' });
var content = getEditorContentSafely(editor, hidden && hidden.value !== undefined ? hidden.value : (source && source.value !== undefined ? source.value : ''));
content = isEmptyRichTextValue(content) ? '' : content;
if (hidden) {
hidden.value = content;
@@ -405,14 +422,12 @@ export function createSlideFormEditorController(options) {
var saves = Array.prototype.map.call(templateFields.querySelectorAll('.editor-holder'), function (holder) {
var regionId = holder.getAttribute('data-region-id');
var editor = editorInstances.get(regionId);
var hidden = getEditorHiddenInput(regionId);
if (!editor || !hidden) {
if (!editor) {
return Promise.resolve();
}
hidden.value = editor.getContent({ format: 'html' });
syncEditorFontSizeHidden(regionId);
syncEditorState(regionId, editor, false, true);
return Promise.resolve();
});
+8
View File
@@ -840,6 +840,14 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev
throw new Error('Wait for the image upload to finish before saving.');
}
await slideFormEditorController.syncEditors();
await new Promise(function (resolve) {
window.requestAnimationFrame(function () {
resolve();
});
});
await new Promise(function (resolve) {
window.setTimeout(resolve, 0);
});
await syncRegionCards();
},
fallbackSuccessMessage: 'Saved slide.',