Release v2.6.19
This commit is contained in:
@@ -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);
|
||||
}());
|
||||
Reference in New Issue
Block a user