Files
pulse-signage/src/web/public/js/data-sources/timetable-group-form.js
T
2026-08-09 11:57:40 +01:00

356 lines
9.7 KiB
JavaScript

// Minimal row editor for timetable groups.
(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 || !timezoneInput || !body || !addButton || !template) {
return;
}
var DEFAULT_TIME_ZONE = 'Europe/London';
function markDirty() {
form.dataset.dirty = 'true';
}
function pad(value) {
return String(value).padStart(2, '0');
}
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(parts.year || '').padStart(4, '0'),
'-',
pad(parts.month),
'-',
pad(parts.day),
'T',
pad(parts.hour),
':',
pad(parts.minute)
].join('');
}
function getTimeZoneOffsetMillis(date, timeZone) {
var parts = getDateTimeParts(date, timeZone);
if (!parts) {
return 0;
}
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 parseDateTimeLocalAsUtc(value, timeZone) {
var raw = String(value || '').trim();
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 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) {
if (!input) {
return;
}
input.setCustomValidity('');
input.classList.remove('is-invalid');
if (input.removeAttribute) {
input.removeAttribute('aria-invalid');
}
}
function setFieldValidity(input, message) {
if (!input) {
return;
}
input.setCustomValidity(message);
input.classList.add('is-invalid');
if (input.setAttribute) {
input.setAttribute('aria-invalid', 'true');
}
}
function validateEntryRow(row) {
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) {
return;
}
startValue = String(startInput.value || '').trim();
endValue = String(endInput.value || '').trim();
if (!startValue || !endValue) {
return;
}
startDate = parseDateTimeLocalAsUtc(startValue, timezone);
endDate = parseDateTimeLocalAsUtc(endValue, timezone);
if (!startDate || !endDate) {
return;
}
if (endDate.getTime() < startDate.getTime() + 60000) {
setFieldValidity(endInput, 'End time must be at least 1 minute after the start time.');
}
}
function bindRowValidation(row) {
var startInput;
var endInput;
if (!row) {
return;
}
startInput = row.querySelector('[name="entry_start_datetime[]"]');
endInput = row.querySelector('[name="entry_end_datetime[]"]');
if (!startInput || !endInput) {
return;
}
function handleValidation() {
validateEntryRow(row);
}
startInput.addEventListener('input', handleValidation);
startInput.addEventListener('change', handleValidation);
endInput.addEventListener('input', handleValidation);
endInput.addEventListener('change', handleValidation);
validateEntryRow(row);
}
function syncRowValuesToTimeZone(row, sourceTimeZone, targetTimeZone) {
if (!row) {
return;
}
['entry_start_datetime[]', 'entry_end_datetime[]'].forEach(function (name) {
var input = row.querySelector('[name="' + name + '"]');
var convertedValue;
if (!input) {
return;
}
convertedValue = convertDateTimeLocalValue(input.value, sourceTimeZone, targetTimeZone);
if (convertedValue) {
input.value = convertedValue;
}
});
}
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);
});
rows.forEach(function (row) {
var idInput = row.querySelector('[name="entry_id[]"]');
var titleInput = row.querySelector('[name="entry_title[]"]');
var descriptionInput = row.querySelector('[name="entry_short_description[]"]');
var startInput = row.querySelector('[name="entry_start_datetime[]"]');
var endInput = row.querySelector('[name="entry_end_datetime[]"]');
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 ? convertDateTimeLocalValue(startInput.value, currentTimeZone, 'UTC') : '');
formData.append('entry_end_datetime[]', endInput ? convertDateTimeLocalValue(endInput.value, currentTimeZone, 'UTC') : '');
});
}
function bindRemove(row) {
var button = row.querySelector('[data-remove-timetable-entry]');
if (!button) {
return;
}
button.addEventListener('click', function () {
row.remove();
markDirty();
});
}
function addRow() {
var fragment = template.content.cloneNode(true);
var row = fragment.querySelector('[data-timetable-entry-row]');
if (!row) {
return;
}
bindRemove(row);
bindRowValidation(row);
body.appendChild(fragment);
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);
bindRowValidation(row);
});
form.addEventListener('formdata', function (event) {
syncFormDataToUtc(event.formData);
});
timezoneInput.addEventListener('change', handleTimezoneChange);
timezoneInput.addEventListener('input', handleTimezoneChange);
addButton.addEventListener('click', addRow);
}());