Release v2.5.6
This commit is contained in:
@@ -197,6 +197,341 @@
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function parseLocalDate(value) {
|
||||
var date = new Date(String(value || '').trim());
|
||||
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||||
}
|
||||
|
||||
function isDayInDateRange(startDatetime, endDatetime, dayIndex) {
|
||||
var startDate = parseLocalDate(startDatetime);
|
||||
var endDate = parseLocalDate(endDatetime);
|
||||
var currentDate;
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (currentDate = new Date(startDate.getTime()); currentDate.getTime() <= endDate.getTime(); currentDate.setDate(currentDate.getDate() + 1)) {
|
||||
if (currentDate.getDay() === Number(dayIndex)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getTimeParts(value) {
|
||||
var match = String(value || '').trim().match(/^(\d{2}):(\d{2})(?::\d{2})?$/);
|
||||
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
hours: Number(match[1]),
|
||||
minutes: Number(match[2])
|
||||
};
|
||||
}
|
||||
|
||||
function combineDateAndTime(baseDate, timeValue) {
|
||||
var timeParts = getTimeParts(timeValue);
|
||||
|
||||
if (!baseDate || !timeParts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Date(
|
||||
baseDate.getFullYear(),
|
||||
baseDate.getMonth(),
|
||||
baseDate.getDate(),
|
||||
timeParts.hours,
|
||||
timeParts.minutes,
|
||||
0,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
function hasScheduleRuleOverlap(startDatetime, endDatetime, startTime, endTime, selectedDays) {
|
||||
var startDateTime = new Date(String(startDatetime || '').trim());
|
||||
var endDateTime = new Date(String(endDatetime || '').trim());
|
||||
var startDate = parseLocalDate(startDatetime);
|
||||
var endDate = parseLocalDate(endDatetime);
|
||||
var allowedDays = Array.isArray(selectedDays) && selectedDays.length ? selectedDays : [0, 1, 2, 3, 4, 5, 6];
|
||||
var currentDate;
|
||||
|
||||
if (!startDate || !endDate || Number.isNaN(startDateTime.getTime()) || Number.isNaN(endDateTime.getTime())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (currentDate = new Date(startDate.getTime()); currentDate.getTime() <= endDate.getTime(); currentDate.setDate(currentDate.getDate() + 1)) {
|
||||
if (allowedDays.indexOf(currentDate.getDay()) === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var dayStart = combineDateAndTime(currentDate, startTime);
|
||||
var dayEnd = combineDateAndTime(currentDate, endTime);
|
||||
|
||||
if (!dayStart || !dayEnd) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dayEnd >= startDateTime && dayStart <= endDateTime) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getDateRangeDayIndices(startDatetime, endDatetime) {
|
||||
var startDate = parseLocalDate(startDatetime);
|
||||
var endDate = parseLocalDate(endDatetime);
|
||||
var dayIndices = [];
|
||||
var currentDate;
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
return dayIndices;
|
||||
}
|
||||
|
||||
for (currentDate = new Date(startDate.getTime()); currentDate.getTime() <= endDate.getTime(); currentDate.setDate(currentDate.getDate() + 1)) {
|
||||
if (dayIndices.indexOf(currentDate.getDay()) === -1) {
|
||||
dayIndices.push(currentDate.getDay());
|
||||
}
|
||||
}
|
||||
|
||||
return dayIndices;
|
||||
}
|
||||
|
||||
function setDayInputError(card, dayInput, message) {
|
||||
var dayInputs = card ? Array.prototype.slice.call(card.querySelectorAll('[data-schedule-rule-day]')) : [];
|
||||
var dayLabels = card ? Array.prototype.slice.call(card.querySelectorAll('.schedule-day-button')) : [];
|
||||
var feedbackNode = card ? card.querySelector('[data-schedule-rule-feedback]') : null;
|
||||
var dayIndex = dayInputs.indexOf(dayInput);
|
||||
|
||||
if (dayInput) {
|
||||
dayInput.setCustomValidity(message);
|
||||
dayInput.classList.add('is-invalid');
|
||||
dayInput.setAttribute('aria-invalid', 'true');
|
||||
}
|
||||
|
||||
if (dayIndex !== -1 && dayLabels[dayIndex]) {
|
||||
dayLabels[dayIndex].classList.add('is-invalid');
|
||||
dayLabels[dayIndex].setAttribute('aria-invalid', 'true');
|
||||
}
|
||||
|
||||
if (feedbackNode) {
|
||||
var messageNode = document.createElement('div');
|
||||
messageNode.textContent = message;
|
||||
feedbackNode.appendChild(messageNode);
|
||||
feedbackNode.hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
function appendRuleFeedbackMessage(card, message) {
|
||||
var feedbackNode = card ? card.querySelector('[data-schedule-rule-feedback]') : null;
|
||||
|
||||
if (!feedbackNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
var messageNode = document.createElement('div');
|
||||
messageNode.textContent = message;
|
||||
feedbackNode.appendChild(messageNode);
|
||||
feedbackNode.hidden = false;
|
||||
}
|
||||
|
||||
function validateScheduleRuleCard(card) {
|
||||
var fields = getRuleFields(card);
|
||||
var dayInputs = Array.prototype.slice.call(fields.days || []);
|
||||
var dayLabels = getRuleDayLabels(card);
|
||||
var checkedDayInputs = dayInputs.filter(function (input) {
|
||||
return Boolean(input && input.checked);
|
||||
});
|
||||
var checkedDayIndices = checkedDayInputs.map(function (input) {
|
||||
return Number(input && input.value);
|
||||
});
|
||||
var startDatetime = String(fields.startDatetime && fields.startDatetime.value || '').trim();
|
||||
var endDatetime = String(fields.endDatetime && fields.endDatetime.value || '').trim();
|
||||
var startTime = String(fields.startTime && fields.startTime.value || '').trim();
|
||||
var endTime = String(fields.endTime && fields.endTime.value || '').trim();
|
||||
clearRuleValidity(card);
|
||||
|
||||
if ((startDatetime || endDatetime) && (!startDatetime || !endDatetime)) {
|
||||
setRuleError(!startDatetime ? fields.startDatetime : fields.endDatetime, 'Start and end datetimes must both be set for this rule.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((startTime || endTime) && (!startTime || !endTime)) {
|
||||
setRuleError(!startTime ? fields.startTime : fields.endTime, 'Start and end times must both be set for this rule.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (startDatetime && endDatetime && new Date(endDatetime) <= new Date(startDatetime)) {
|
||||
setRuleError(fields.endDatetime, 'End datetime must be after start datetime.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (startTime && endTime && endTime <= startTime) {
|
||||
setRuleError(fields.endTime, 'End time must be after start time.');
|
||||
if (fields.startTime) {
|
||||
fields.startTime.classList.add('is-invalid');
|
||||
fields.startTime.setAttribute('aria-invalid', 'true');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (startDatetime && endDatetime) {
|
||||
var dateRangeDayIndices = getDateRangeDayIndices(startDatetime, endDatetime);
|
||||
var dayIndicesToCheck = checkedDayIndices.length ? checkedDayIndices : [0, 1, 2, 3, 4, 5, 6];
|
||||
var invalidDayInputs = checkedDayInputs.filter(function (input) {
|
||||
return dateRangeDayIndices.indexOf(Number(input && input.value)) === -1;
|
||||
});
|
||||
|
||||
invalidDayInputs.forEach(function (input) {
|
||||
var dayIndex = dayInputs.indexOf(input);
|
||||
|
||||
if (dayIndex !== -1 && dayLabels[dayIndex]) {
|
||||
dayLabels[dayIndex].classList.add('is-invalid');
|
||||
dayLabels[dayIndex].setAttribute('aria-invalid', 'true');
|
||||
}
|
||||
});
|
||||
|
||||
if (checkedDayInputs.length && invalidDayInputs.length === checkedDayInputs.length) {
|
||||
setDayInputError(card, invalidDayInputs[0] || checkedDayInputs[0] || dayInputs[0], 'Selected days must overlap the date range.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (startTime && endTime && !hasScheduleRuleOverlap(startDatetime, endDatetime, startTime, endTime, dayIndicesToCheck)) {
|
||||
setRuleError(fields.endTime, 'Start and end times must overlap the date range.');
|
||||
if (fields.startTime) {
|
||||
fields.startTime.classList.add('is-invalid');
|
||||
fields.startTime.setAttribute('aria-invalid', 'true');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getScheduleRuleValidationIssue(card) {
|
||||
var fields = getRuleFields(card);
|
||||
var dayInputs = Array.prototype.slice.call(fields.days || []);
|
||||
var checkedDayInputs = dayInputs.filter(function (input) {
|
||||
return Boolean(input && input.checked);
|
||||
});
|
||||
var checkedDayIndices = checkedDayInputs.map(function (input) {
|
||||
return Number(input && input.value);
|
||||
});
|
||||
var startDatetime = String(fields.startDatetime && fields.startDatetime.value || '').trim();
|
||||
var endDatetime = String(fields.endDatetime && fields.endDatetime.value || '').trim();
|
||||
var startTime = String(fields.startTime && fields.startTime.value || '').trim();
|
||||
var endTime = String(fields.endTime && fields.endTime.value || '').trim();
|
||||
|
||||
if (startDatetime || endDatetime) {
|
||||
if (!startDatetime || !endDatetime) {
|
||||
return {
|
||||
type: 'datetime-pair',
|
||||
target: !startDatetime ? fields.startDatetime : fields.endDatetime,
|
||||
message: 'Start and end datetimes must both be set for this rule.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (startTime || endTime) {
|
||||
if (!startTime || !endTime) {
|
||||
return {
|
||||
type: 'time-pair',
|
||||
target: !startTime ? fields.startTime : fields.endTime,
|
||||
message: 'Start and end times must both be set for this rule.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (startDatetime && endDatetime && new Date(endDatetime) <= new Date(startDatetime)) {
|
||||
return {
|
||||
type: 'datetime-order',
|
||||
target: fields.endDatetime,
|
||||
message: 'End datetime must be after start datetime.'
|
||||
};
|
||||
}
|
||||
|
||||
if (startTime && endTime && endTime <= startTime) {
|
||||
return {
|
||||
type: 'time-order',
|
||||
target: fields.endTime,
|
||||
message: 'End time must be after start time.'
|
||||
};
|
||||
}
|
||||
|
||||
if (startDatetime && endDatetime) {
|
||||
var dateRangeDayIndices = getDateRangeDayIndices(startDatetime, endDatetime);
|
||||
var dayIndicesToCheck = checkedDayIndices.length ? checkedDayIndices : [0, 1, 2, 3, 4, 5, 6];
|
||||
var invalidDayInputs = checkedDayInputs.filter(function (input) {
|
||||
return dateRangeDayIndices.indexOf(Number(input && input.value)) === -1;
|
||||
});
|
||||
|
||||
if (checkedDayInputs.length && invalidDayInputs.length === checkedDayInputs.length) {
|
||||
return {
|
||||
type: 'day-overlap',
|
||||
target: checkedDayInputs[0] || dayInputs[0] || fields.endDatetime,
|
||||
message: 'Selected days must overlap the date range.'
|
||||
};
|
||||
}
|
||||
|
||||
if (startTime && endTime && !hasScheduleRuleOverlap(startDatetime, endDatetime, startTime, endTime, dayIndicesToCheck)) {
|
||||
return {
|
||||
type: 'time-overlap',
|
||||
target: fields.endTime,
|
||||
secondaryTarget: fields.startTime,
|
||||
message: 'Start and end times must overlap the date range.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function applyScheduleRuleValidationIssue(card, issue) {
|
||||
var fields = getRuleFields(card);
|
||||
var dayLabels = getRuleDayLabels(card);
|
||||
|
||||
clearRuleValidity(card);
|
||||
|
||||
if (!issue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (issue.type === 'time-overlap') {
|
||||
setRuleError(issue.target || fields.endTime, issue.message);
|
||||
if (issue.secondaryTarget) {
|
||||
issue.secondaryTarget.classList.add('is-invalid');
|
||||
issue.secondaryTarget.setAttribute('aria-invalid', 'true');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (issue.type === 'day-overlap') {
|
||||
var dayInputs = Array.prototype.slice.call(fields.days || []);
|
||||
var dayIndex = dayInputs.indexOf(issue.target);
|
||||
|
||||
setRuleError(issue.target, issue.message);
|
||||
if (dayIndex !== -1 && dayLabels[dayIndex]) {
|
||||
dayLabels[dayIndex].classList.add('is-invalid');
|
||||
dayLabels[dayIndex].setAttribute('aria-invalid', 'true');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
setRuleError(issue.target, issue.message);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Video duration helpers.
|
||||
function getVideoDurationButtonLabel(isPressed) {
|
||||
return isPressed ? 'Disable use video duration' : 'Use video duration';
|
||||
@@ -213,7 +548,7 @@
|
||||
return '';
|
||||
}
|
||||
|
||||
return '<button type="button" class="btn btn-outline-info btn-sm playlist-use-video-duration' + (isPressed ? ' active' : '') + '" data-use-video-duration-button aria-pressed="' + (isPressed ? 'true' : 'false') + '" aria-label="' + getVideoDurationButtonLabel(isPressed) + '" title="' + getVideoDurationButtonLabel(isPressed) + '">' + buildVideoDurationButtonContent(isPressed) + '</button>';
|
||||
return '<button type="button" class="btn ' + (isPressed ? 'btn-info' : 'btn-outline-info') + ' btn-sm playlist-use-video-duration" data-use-video-duration-button aria-pressed="' + (isPressed ? 'true' : 'false') + '" aria-label="' + getVideoDurationButtonLabel(isPressed) + '" title="' + getVideoDurationButtonLabel(isPressed) + '">' + buildVideoDurationButtonContent(isPressed) + '</button>';
|
||||
}
|
||||
|
||||
function getDisableAudioButtonLabel(isPressed) {
|
||||
@@ -241,7 +576,6 @@
|
||||
|
||||
button.classList.toggle('btn-outline-secondary', Boolean(isPressed));
|
||||
button.classList.toggle('btn-secondary', !Boolean(isPressed));
|
||||
button.classList.toggle('active', !Boolean(isPressed));
|
||||
button.setAttribute('aria-pressed', isPressed ? 'true' : 'false');
|
||||
button.setAttribute('aria-label', getDisableAudioButtonLabel(isPressed));
|
||||
button.setAttribute('title', getDisableAudioButtonLabel(isPressed));
|
||||
@@ -253,7 +587,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
button.classList.toggle('active', Boolean(isPressed));
|
||||
button.classList.toggle('btn-outline-info', !Boolean(isPressed));
|
||||
button.classList.toggle('btn-info', Boolean(isPressed));
|
||||
button.setAttribute('aria-pressed', isPressed ? 'true' : 'false');
|
||||
button.setAttribute('aria-label', getVideoDurationButtonLabel(isPressed));
|
||||
button.setAttribute('title', getVideoDurationButtonLabel(isPressed));
|
||||
@@ -382,7 +717,11 @@
|
||||
return '' +
|
||||
'<td class="playlist-order-cell" data-label="Order">' +
|
||||
'<div class="playlist-order-cell-inner">' +
|
||||
'<button type="button" class="playlist-drag-handle btn btn-link p-0 text-body-secondary" data-playlist-drag-handle aria-label="Drag to reorder" title="Drag to reorder"><span class="playlist-drag-handle-icon" aria-hidden="true"><svg class="playlist-drag-handle-svg" viewBox="0 0 24 32" focusable="false" aria-hidden="true"><polygon points="12,2 20,9 4,9"></polygon><rect x="4" y="14" width="16" height="4" rx="2"></rect><polygon points="4,23 20,23 12,30"></polygon></svg></span></button>' +
|
||||
'<div class="playlist-order-stepper" aria-label="Reorder slide controls">' +
|
||||
'<button type="button" class="playlist-order-move playlist-order-move-up btn btn-link p-0 text-body-secondary" data-playlist-row-move="up" aria-label="Move slide up" title="Move slide up"><i class="bi bi-caret-up-fill" aria-hidden="true" data-playlist-row-move-icon="up"></i></button>' +
|
||||
'<button type="button" class="playlist-drag-handle btn btn-link p-0 text-body-secondary" data-playlist-drag-handle aria-label="Drag to reorder" title="Drag to reorder"><span class="playlist-drag-handle-icon" aria-hidden="true"><i class="bi bi-grip-horizontal"></i></span></button>' +
|
||||
'<button type="button" class="playlist-order-move playlist-order-move-down btn btn-link p-0 text-body-secondary" data-playlist-row-move="down" aria-label="Move slide down" title="Move slide down"><i class="bi bi-caret-down-fill" aria-hidden="true" data-playlist-row-move-icon="down"></i></button>' +
|
||||
'</div>' +
|
||||
'<span class="playlist-order-number"></span>' +
|
||||
'</div>' +
|
||||
'</td>';
|
||||
@@ -412,8 +751,11 @@
|
||||
|
||||
function buildPlaylistDurationCellMarkup(values, durationActionMarkup) {
|
||||
return '' +
|
||||
'<td data-label="Config"><div class="playlist-duration-field">' +
|
||||
'<input name="duration_seconds[]" type="text" inputmode="decimal" value="' + values.duration_seconds + '" required form="playlist-edit-form" class="form-control form-control-sm playlist-duration-input text-end"' + (values.useVideoDuration ? ' disabled' : '') + ' />' +
|
||||
'<td data-label="Playback"><div class="playlist-duration-field">' +
|
||||
'<div class="input-group input-group-sm flex-nowrap playlist-duration-input-group">' +
|
||||
'<span class="input-group-text playlist-duration-prefix text-body-secondary" aria-hidden="true"><i class="bi bi-clock"></i></span>' +
|
||||
'<input name="duration_seconds[]" type="text" inputmode="decimal" min="1" value="' + values.duration_seconds + '" required form="playlist-edit-form" class="form-control form-control-sm playlist-duration-input text-end" data-limit-input-value' + (values.useVideoDuration ? ' disabled' : '') + ' />' +
|
||||
'</div>' +
|
||||
'<input type="hidden" name="use_video_duration[]" value="' + (values.useVideoDuration ? '1' : '0') + '" form="playlist-edit-form" />' +
|
||||
'<input type="hidden" name="disable_audio[]" value="' + (values.disableAudio ? '1' : '0') + '" form="playlist-edit-form" data-playlist-disable-audio-input />' +
|
||||
(values.useVideoDuration ? '<input type="hidden" name="duration_seconds[]" value="' + values.duration_seconds + '" form="playlist-edit-form" data-video-duration-mirror />' : '') +
|
||||
@@ -586,8 +928,7 @@
|
||||
'<div class="d-flex align-items-center gap-4 ms-auto flex-shrink-0">' +
|
||||
'<button type="button" class="btn btn-sm btn-outline-danger" data-remove-schedule-rule data-confirm-message="Remove this rule?">Remove</button>' +
|
||||
'<button type="button" class="btn btn-tool p-0 text-body-secondary" data-lte-toggle="card-collapse" aria-label="Collapse rule ' + String(ruleIndex) + '" title="Collapse rule ' + String(ruleIndex) + '">' +
|
||||
'<i class="bi bi-dash-lg" aria-hidden="true" data-lte-icon="collapse"></i>' +
|
||||
'<i class="bi bi-plus-lg" aria-hidden="true" data-lte-icon="expand"></i>' +
|
||||
'<i class="bi ' + (isCollapsed ? 'bi-plus-lg' : 'bi-dash-lg') + '" aria-hidden="true" data-schedule-rule-collapse-icon></i>' +
|
||||
'<span class="visually-hidden">Collapse rule ' + String(ruleIndex) + '</span>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
@@ -614,11 +955,14 @@
|
||||
'</div>' +
|
||||
'<div class="col-12">' +
|
||||
'<div class="btn-group btn-group-sm w-100 schedule-day-grid" role="group" aria-label="Schedule days">' + DAY_NAMES.map(function (label, dayIndex) {
|
||||
return '<input class="btn-check" type="checkbox" id="schedule-rule-' + String(ruleIndex) + '-day-' + String(dayIndex) + '" value="' + String(dayIndex) + '" autocomplete="off"' + (selectedDays.has(dayIndex) ? ' checked' : '') + ' data-schedule-rule-day />' +
|
||||
'<label class="btn btn-outline-primary flex-fill schedule-day-button" for="schedule-rule-' + String(ruleIndex) + '-day-' + String(dayIndex) + '">' + label + '</label>';
|
||||
return '<span class="schedule-day-option">' +
|
||||
'<input class="btn-check" type="checkbox" id="schedule-rule-' + String(ruleIndex) + '-day-' + String(dayIndex) + '" value="' + String(dayIndex) + '" autocomplete="off"' + (selectedDays.has(dayIndex) ? ' checked' : '') + ' data-schedule-rule-day />' +
|
||||
'<label class="btn btn-outline-primary flex-fill schedule-day-button" for="schedule-rule-' + String(ruleIndex) + '-day-' + String(dayIndex) + '">' + label + '</label>' +
|
||||
'</span>';
|
||||
}).join('') + '</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="invalid-feedback d-block mt-2" data-schedule-rule-feedback aria-live="polite" hidden></div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
@@ -779,13 +1123,71 @@
|
||||
return card ? card.querySelector('[data-remove-schedule-rule]') : null;
|
||||
}
|
||||
|
||||
function getRuleDayLabels(card) {
|
||||
return card ? Array.prototype.slice.call(card.querySelectorAll('.schedule-day-button')) : [];
|
||||
}
|
||||
|
||||
function getCollapseRuleButton(card) {
|
||||
return card ? card.querySelector('[data-schedule-rule-collapse], [data-lte-toggle="card-collapse"]') : null;
|
||||
}
|
||||
|
||||
function getCollapseRuleIcon(card) {
|
||||
return card ? card.querySelector('[data-schedule-rule-collapse-icon]') : null;
|
||||
}
|
||||
|
||||
function syncRuleCollapseButtonState(card) {
|
||||
var button = getCollapseRuleButton(card);
|
||||
var icon = getCollapseRuleIcon(card);
|
||||
var numberNode = getRuleNumberNode(card);
|
||||
var ruleIndex = numberNode ? String(numberNode.textContent || '') : '';
|
||||
var isCollapsed = Boolean(card && card.classList && card.classList.contains('collapsed-card'));
|
||||
var label = (isCollapsed ? 'Expand' : 'Collapse') + ' rule ' + ruleIndex;
|
||||
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
button.setAttribute('aria-expanded', isCollapsed ? 'false' : 'true');
|
||||
button.setAttribute('aria-label', label);
|
||||
button.setAttribute('title', label);
|
||||
|
||||
if (icon) {
|
||||
icon.classList.remove('bi-dash-lg', 'bi-plus-lg');
|
||||
icon.classList.add(isCollapsed ? 'bi-plus-lg' : 'bi-dash-lg');
|
||||
}
|
||||
|
||||
var visibleLabel = button.querySelector('.visually-hidden');
|
||||
if (visibleLabel) {
|
||||
visibleLabel.textContent = label;
|
||||
}
|
||||
}
|
||||
|
||||
function clearRuleValidity(card) {
|
||||
var fields = getRuleFields(card);
|
||||
var dayInputs = Array.prototype.slice.call(fields.days || []);
|
||||
var dayLabels = getRuleDayLabels(card);
|
||||
var feedbackNode = card ? card.querySelector('[data-schedule-rule-feedback]') : null;
|
||||
[fields.startDatetime, fields.endDatetime, fields.startTime, fields.endTime].forEach(function (input) {
|
||||
if (input) {
|
||||
input.setCustomValidity('');
|
||||
input.classList.remove('is-invalid');
|
||||
input.removeAttribute('aria-invalid');
|
||||
}
|
||||
});
|
||||
dayInputs.forEach(function (input) {
|
||||
input.setCustomValidity('');
|
||||
input.classList.remove('is-invalid');
|
||||
input.removeAttribute('aria-invalid');
|
||||
});
|
||||
dayLabels.forEach(function (label) {
|
||||
label.classList.remove('is-invalid');
|
||||
label.removeAttribute('aria-invalid');
|
||||
});
|
||||
|
||||
if (feedbackNode) {
|
||||
feedbackNode.innerHTML = '';
|
||||
feedbackNode.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
function setRuleError(input, message) {
|
||||
@@ -793,37 +1195,96 @@
|
||||
return;
|
||||
}
|
||||
input.setCustomValidity(message);
|
||||
if (String(input.value || '').trim()) {
|
||||
input.classList.add('is-invalid');
|
||||
input.setAttribute('aria-invalid', 'true');
|
||||
}
|
||||
|
||||
appendRuleFeedbackMessage(input.closest ? input.closest('[data-schedule-rule-card]') : null, message);
|
||||
}
|
||||
|
||||
function validateRuleCard(card) {
|
||||
function validateScheduleRuleCard(card) {
|
||||
var fields = getRuleFields(card);
|
||||
var dayInputs = Array.prototype.slice.call(fields.days || []);
|
||||
var dayLabels = getRuleDayLabels(card);
|
||||
var checkedDayInputs = dayInputs.filter(function (input) {
|
||||
return Boolean(input && input.checked);
|
||||
});
|
||||
var checkedDayIndices = checkedDayInputs.map(function (input) {
|
||||
return Number(input && input.value);
|
||||
});
|
||||
var startDatetime = String(fields.startDatetime && fields.startDatetime.value || '').trim();
|
||||
var endDatetime = String(fields.endDatetime && fields.endDatetime.value || '').trim();
|
||||
var startTime = String(fields.startTime && fields.startTime.value || '').trim();
|
||||
var endTime = String(fields.endTime && fields.endTime.value || '').trim();
|
||||
var hasDateInput = Boolean(startDatetime || endDatetime);
|
||||
var hasTimeInput = Boolean(startTime || endTime);
|
||||
var isValid = true;
|
||||
|
||||
clearRuleValidity(card);
|
||||
|
||||
if (hasDateInput && (!startDatetime || !endDatetime)) {
|
||||
setRuleError(fields.startDatetime || fields.endDatetime, 'Start and end datetimes must both be set for this rule.');
|
||||
return false;
|
||||
}
|
||||
if (hasTimeInput && (!startTime || !endTime)) {
|
||||
setRuleError(fields.startTime || fields.endTime, 'Start and end times must both be set for this rule.');
|
||||
return false;
|
||||
}
|
||||
if (startDatetime && endDatetime && new Date(endDatetime) < new Date(startDatetime)) {
|
||||
setRuleError(fields.endDatetime, 'End datetime must be after start datetime.');
|
||||
return false;
|
||||
}
|
||||
if (startTime && endTime && endTime < startTime) {
|
||||
setRuleError(fields.endTime, 'End time must be after start time.');
|
||||
return false;
|
||||
if ((startDatetime || endDatetime) && (!startDatetime || !endDatetime)) {
|
||||
setRuleError(!startDatetime ? fields.startDatetime : fields.endDatetime, 'Start and end datetimes must both be set for this rule.');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return true;
|
||||
if ((startTime || endTime) && (!startTime || !endTime)) {
|
||||
setRuleError(!startTime ? fields.startTime : fields.endTime, 'Start and end times must both be set for this rule.');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (startDatetime && endDatetime && new Date(endDatetime) <= new Date(startDatetime)) {
|
||||
setRuleError(fields.endDatetime, 'End datetime must be after start datetime.');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (startTime && endTime && endTime <= startTime) {
|
||||
setRuleError(fields.endTime, 'End time must be after start time.');
|
||||
if (fields.startTime) {
|
||||
fields.startTime.classList.add('is-invalid');
|
||||
fields.startTime.setAttribute('aria-invalid', 'true');
|
||||
}
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (startDatetime && endDatetime) {
|
||||
var dateRangeDayIndices = getDateRangeDayIndices(startDatetime, endDatetime);
|
||||
var dayIndicesToCheck = checkedDayIndices.length ? checkedDayIndices : [0, 1, 2, 3, 4, 5, 6];
|
||||
var invalidDayInputs = checkedDayInputs.filter(function (input) {
|
||||
return dateRangeDayIndices.indexOf(Number(input && input.value)) === -1;
|
||||
});
|
||||
|
||||
invalidDayInputs.forEach(function (input) {
|
||||
var dayIndex = dayInputs.indexOf(input);
|
||||
|
||||
if (dayIndex !== -1 && dayLabels[dayIndex]) {
|
||||
dayLabels[dayIndex].classList.add('is-invalid');
|
||||
dayLabels[dayIndex].setAttribute('aria-invalid', 'true');
|
||||
}
|
||||
});
|
||||
|
||||
if (checkedDayInputs.length && invalidDayInputs.length === checkedDayInputs.length) {
|
||||
setDayInputError(card, checkedDayInputs[0] || dayInputs[0], 'Selected days must overlap the date range.');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (startTime && endTime && !hasScheduleRuleOverlap(startDatetime, endDatetime, startTime, endTime, dayIndicesToCheck)) {
|
||||
setRuleError(fields.endTime, 'Start and end times must overlap the date range.');
|
||||
if (fields.startTime) {
|
||||
fields.startTime.classList.add('is-invalid');
|
||||
fields.startTime.setAttribute('aria-invalid', 'true');
|
||||
}
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
function updateLiveRuleValidity(card) {
|
||||
validateScheduleRuleCard(card);
|
||||
}
|
||||
|
||||
function validateRuleCard(card) {
|
||||
return validateScheduleRuleCard(card);
|
||||
}
|
||||
|
||||
function readRuleFromCard(card) {
|
||||
@@ -978,37 +1439,47 @@
|
||||
card.querySelector('[data-schedule-rule-start-time]').value = String(normalizedRule.start_time || '');
|
||||
card.querySelector('[data-schedule-rule-end-time]').value = String(normalizedRule.end_time || '');
|
||||
card.querySelector('[data-remove-schedule-rule]').setAttribute('data-confirm-message', 'Remove this rule?');
|
||||
card.querySelector('[data-lte-toggle="card-collapse"]').setAttribute('aria-label', 'Collapse rule ' + String(ruleIndex));
|
||||
card.querySelector('[data-lte-toggle="card-collapse"]').setAttribute('title', 'Collapse rule ' + String(ruleIndex));
|
||||
card.querySelector('[data-lte-toggle="card-collapse"] .visually-hidden').textContent = 'Collapse rule ' + String(ruleIndex);
|
||||
updateRuleCardDayOptions(card, ruleKey, normalizedRule);
|
||||
|
||||
card.querySelector('[data-lte-toggle="card-collapse"]').addEventListener('click', function (event) {
|
||||
event.preventDefault();
|
||||
card.classList.toggle('collapsed-card');
|
||||
persistScheduleDraft();
|
||||
});
|
||||
|
||||
if (isCollapsed) {
|
||||
card.classList.add('collapsed-card');
|
||||
} else {
|
||||
card.classList.remove('collapsed-card');
|
||||
}
|
||||
|
||||
try {
|
||||
updateLiveRuleValidity(card);
|
||||
} catch (_error) {
|
||||
clearRuleValidity(card);
|
||||
}
|
||||
syncRuleCollapseButtonState(card);
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
function addRule(rule, options) {
|
||||
var ruleCount = getRuleCards().length + 1;
|
||||
var card = createRuleCardElement(rule || {}, ruleCount, options);
|
||||
scheduleRuleList.appendChild(card);
|
||||
|
||||
if (!card) {
|
||||
scheduleRuleList.insertAdjacentHTML('beforeend', buildScheduleRuleCardMarkup(rule || {}, ruleCount, Boolean(options && options.isCollapsed)));
|
||||
card = scheduleRuleList.lastElementChild;
|
||||
}
|
||||
|
||||
if (!card) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (card.parentNode !== scheduleRuleList) {
|
||||
scheduleRuleList.appendChild(card);
|
||||
}
|
||||
refreshRuleNumbers();
|
||||
syncScheduleRulesField();
|
||||
return card;
|
||||
}
|
||||
|
||||
function handleCardChange(card) {
|
||||
clearRuleValidity(card);
|
||||
updateLiveRuleValidity(card);
|
||||
updateRuleCardSummary(card);
|
||||
syncScheduleRulesField();
|
||||
}
|
||||
@@ -1074,6 +1545,16 @@
|
||||
|
||||
scheduleRuleList.addEventListener('click', function (event) {
|
||||
var removeButton = event.target && event.target.closest ? event.target.closest('[data-remove-schedule-rule]') : null;
|
||||
var collapseButton = event.target && event.target.closest ? event.target.closest('[data-schedule-rule-collapse], [data-lte-toggle="card-collapse"]') : null;
|
||||
|
||||
if (collapseButton) {
|
||||
window.setTimeout(function () {
|
||||
syncRuleCollapseButtonState(collapseButton.closest('[data-schedule-rule-card]'));
|
||||
persistScheduleDraft();
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!removeButton) {
|
||||
return;
|
||||
}
|
||||
@@ -1107,6 +1588,8 @@
|
||||
} else {
|
||||
refreshRuleNumbers();
|
||||
getRuleCards().forEach(function (card) {
|
||||
syncRuleCollapseButtonState(card);
|
||||
updateLiveRuleValidity(card);
|
||||
updateRuleCardSummary(card);
|
||||
});
|
||||
syncScheduleRulesField();
|
||||
@@ -1637,9 +2120,36 @@
|
||||
var rows = getRows();
|
||||
rows.forEach(function (row, index) {
|
||||
var orderNumber = row.querySelector('.playlist-order-number');
|
||||
var moveUpButton = row.querySelector('[data-playlist-row-move="up"]');
|
||||
var moveDownButton = row.querySelector('[data-playlist-row-move="down"]');
|
||||
var moveUpIcon = row.querySelector('[data-playlist-row-move-icon="up"]');
|
||||
var moveDownIcon = row.querySelector('[data-playlist-row-move-icon="down"]');
|
||||
var isFirst = index === 0;
|
||||
var isLast = index === rows.length - 1;
|
||||
|
||||
if (orderNumber) {
|
||||
orderNumber.textContent = String(index + 1);
|
||||
}
|
||||
|
||||
if (moveUpButton) {
|
||||
moveUpButton.disabled = isFirst;
|
||||
moveUpButton.setAttribute('aria-disabled', isFirst ? 'true' : 'false');
|
||||
}
|
||||
|
||||
if (moveUpIcon) {
|
||||
moveUpIcon.classList.toggle('bi-caret-up-fill', !isFirst);
|
||||
moveUpIcon.classList.toggle('bi-caret-up', isFirst);
|
||||
}
|
||||
|
||||
if (moveDownButton) {
|
||||
moveDownButton.disabled = isLast;
|
||||
moveDownButton.setAttribute('aria-disabled', isLast ? 'true' : 'false');
|
||||
}
|
||||
|
||||
if (moveDownIcon) {
|
||||
moveDownIcon.classList.toggle('bi-caret-down-fill', !isLast);
|
||||
moveDownIcon.classList.toggle('bi-caret-down', isLast);
|
||||
}
|
||||
});
|
||||
syncAddSlideOptions();
|
||||
syncPlaylistCanvasLock();
|
||||
@@ -1665,6 +2175,38 @@
|
||||
});
|
||||
}
|
||||
|
||||
function movePlaylistRowByOffset(row, offset) {
|
||||
var currentTbody = document.getElementById('playlist-items-body') || tbody;
|
||||
var rows = getRows();
|
||||
var currentIndex = rows.indexOf(row);
|
||||
var targetIndex;
|
||||
var referenceRow;
|
||||
|
||||
if (!currentTbody || !row || currentIndex === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
targetIndex = currentIndex + Number(offset || 0);
|
||||
if (targetIndex < 0 || targetIndex >= rows.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
referenceRow = rows[targetIndex];
|
||||
|
||||
if (targetIndex > currentIndex) {
|
||||
referenceRow = referenceRow ? referenceRow.nextSibling : null;
|
||||
}
|
||||
|
||||
if (referenceRow) {
|
||||
currentTbody.insertBefore(row, referenceRow);
|
||||
} else {
|
||||
currentTbody.appendChild(row);
|
||||
}
|
||||
|
||||
updateRowOrder(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
function populatePlaylistSlideRow(row, values, rowKey) {
|
||||
var canvasWidth = Number(values.canvas_width);
|
||||
var canvasHeight = Number(values.canvas_height);
|
||||
@@ -1898,6 +2440,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var moveButton = event.target.closest('[data-playlist-row-move]');
|
||||
var removeButton = event.target.closest('[data-playlist-remove-row]');
|
||||
var durationButton = event.target.closest('.playlist-use-video-duration');
|
||||
var muteButton = event.target.closest('.playlist-disable-audio');
|
||||
@@ -1907,6 +2450,13 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (moveButton) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
movePlaylistRowByOffset(row, moveButton.getAttribute('data-playlist-row-move') === 'up' ? -1 : 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (removeButton) {
|
||||
event.preventDefault();
|
||||
row.remove();
|
||||
|
||||
Reference in New Issue
Block a user