/** * LEDMatrix Schedule Picker Widget * * Composite widget combining enable toggle, mode switch (global/per-day), * and time range configurations. Composes day-selector and time-range widgets. * * Can be used standalone in schedule.html or by plugins via x-widget: "schedule-picker". * * Schema example: * { * "schedule": { * "type": "object", * "x-widget": "schedule-picker", * "x-options": { * "showModeToggle": true, // Allow switching global/per-day * "showEnableToggle": true, // Show enabled checkbox * "compactMode": false, // Compact layout for embedded use * "defaultMode": "global" // Default mode: "global" or "per_day" * } * } * } * * API-compatible output format: * { * enabled: boolean, * mode: "global" | "per_day", * start_time: "HH:MM", // if global mode * end_time: "HH:MM", // if global mode * days: { // if per_day mode * monday: { enabled: boolean, start_time: "HH:MM", end_time: "HH:MM" }, * ... * } * } * * @module SchedulePickerWidget */ (function() { 'use strict'; const DAYS = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']; const DAY_LABELS = { monday: 'Monday', tuesday: 'Tuesday', wednesday: 'Wednesday', thursday: 'Thursday', friday: 'Friday', saturday: 'Saturday', sunday: 'Sunday' }; // Use BaseWidget utilities if available const base = window.BaseWidget ? new window.BaseWidget('SchedulePicker', '1.0.0') : null; function escapeHtml(text) { if (base) return base.escapeHtml(text); const div = document.createElement('div'); div.textContent = String(text); return div.innerHTML.replace(/"/g, '"').replace(/'/g, '''); } function sanitizeId(id) { if (base) return base.sanitizeId(id); return String(id).replace(/[^a-zA-Z0-9_-]/g, '_'); } function triggerChange(fieldId, value) { if (base) { base.triggerChange(fieldId, value); } else { const event = new CustomEvent('widget-change', { detail: { fieldId, value }, bubbles: true, cancelable: true }); document.dispatchEvent(event); } } /** * Generate default schedule config */ function getDefaultSchedule() { const days = {}; DAYS.forEach(day => { days[day] = { enabled: true, start_time: '07:00', end_time: '23:00' }; }); return { enabled: false, mode: 'global', start_time: '07:00', end_time: '23:00', days: days }; } /** * Coerce a value to boolean, handling string 'true'/'false' values * that may come from config files or form submissions. */ function coerceToBoolean(value) { if (typeof value === 'boolean') { return value; } if (typeof value === 'string') { const trimmed = value.trim().toLowerCase(); return trimmed === 'true' || trimmed === '1' || trimmed === 'on'; } return Boolean(value); } /** * Merge user value with defaults */ function normalizeSchedule(value) { const defaults = getDefaultSchedule(); if (!value || typeof value !== 'object') { return defaults; } const schedule = { enabled: coerceToBoolean(value.enabled), mode: value.mode === 'per_day' ? 'per_day' : 'global', start_time: value.start_time || defaults.start_time, end_time: value.end_time || defaults.end_time, days: {} }; // Merge days DAYS.forEach(day => { const dayConfig = (value.days && value.days[day]) || defaults.days[day]; // Use coerceToBoolean but default to true if enabled is undefined const dayEnabled = dayConfig.enabled === undefined ? true : coerceToBoolean(dayConfig.enabled); schedule.days[day] = { enabled: dayEnabled, start_time: dayConfig.start_time || defaults.days[day].start_time, end_time: dayConfig.end_time || defaults.days[day].end_time }; }); return schedule; } window.LEDMatrixWidgets.register('schedule-picker', { name: 'Schedule Picker Widget', version: '1.0.0', /** * Render the schedule picker widget * @param {HTMLElement} container - Container element * @param {Object} config - Schema configuration * @param {Object} value - Schedule configuration object * @param {Object} options - Additional options (fieldId, pluginId) */ render: function(container, config, value, options) { const fieldId = sanitizeId(options.fieldId || container.id || 'schedule'); const xOptions = config['x-options'] || config['x_options'] || {}; const showModeToggle = xOptions.showModeToggle !== false; const showEnableToggle = xOptions.showEnableToggle !== false; const compactMode = xOptions.compactMode === true; const schedule = normalizeSchedule(value); let html = `
`; // Hidden inputs for API-compatible form submission html += this._renderHiddenInputs(fieldId, schedule); // Enable toggle if (showEnableToggle) { html += `

When enabled, the display will only operate during specified hours.

`; } // Mode selection if (showModeToggle) { html += `

Schedule Mode

Use the same start and end time for all days of the week

Set different times for each day of the week

`; } // Global schedule section const globalDisplay = schedule.mode === 'global' ? 'block' : 'none'; html += `

Global Times

When to start displaying content (HH:MM)

When to stop displaying content (HH:MM)

`; // Per-day schedule section const perDayDisplay = schedule.mode === 'per_day' ? 'block' : 'none'; html += `

Day-Specific Times

`; // Render each day row DAYS.forEach(day => { const dayConfig = schedule.days[day]; const disabled = !dayConfig.enabled; const disabledClass = disabled ? 'bg-gray-100' : ''; html += ` `; }); html += `
Day Enabled Start End
${escapeHtml(DAY_LABELS[day])}
`; html += '
'; container.innerHTML = html; }, /** * Render hidden inputs for form submission * These match the existing API format */ _renderHiddenInputs: function(fieldId, schedule) { let html = ''; // Enabled state (hidden input ensures value is always sent, even when checkbox is unchecked) html += ``; // Mode indicator (for the widget to track internally) html += ``; // Global times (used when mode is global) html += ``; html += ``; // Per-day values (used when mode is per_day) DAYS.forEach(day => { const dayConfig = schedule.days[day]; html += ``; html += ``; html += ``; }); return html; }, /** * Get current schedule value * @param {string} fieldId - Field ID * @returns {Object} Schedule configuration object */ getValue: function(fieldId) { const safeId = sanitizeId(fieldId); const widget = document.getElementById(`${safeId}_widget`); if (!widget) return getDefaultSchedule(); const enabledCheckbox = document.getElementById(`${safeId}_enabled`); const modeGlobal = document.getElementById(`${safeId}_mode_global`); const globalStart = document.getElementById(`${safeId}_global_start`); const globalEnd = document.getElementById(`${safeId}_global_end`); const schedule = { enabled: enabledCheckbox ? enabledCheckbox.checked : false, mode: (modeGlobal && modeGlobal.checked) ? 'global' : 'per_day', start_time: globalStart ? globalStart.value : '07:00', end_time: globalEnd ? globalEnd.value : '23:00', days: {} }; DAYS.forEach(day => { const dayEnabled = document.getElementById(`${safeId}_${day}_enabled`); const dayStart = document.getElementById(`${safeId}_${day}_start`); const dayEnd = document.getElementById(`${safeId}_${day}_end`); schedule.days[day] = { enabled: dayEnabled ? dayEnabled.checked : true, start_time: dayStart ? dayStart.value : '07:00', end_time: dayEnd ? dayEnd.value : '23:00' }; }); return schedule; }, /** * Set schedule value * @param {string} fieldId - Field ID * @param {Object} value - Schedule configuration object */ setValue: function(fieldId, value) { const safeId = sanitizeId(fieldId); const schedule = normalizeSchedule(value); // Set enabled const enabledCheckbox = document.getElementById(`${safeId}_enabled`); if (enabledCheckbox) enabledCheckbox.checked = schedule.enabled; // Set mode const modeGlobal = document.getElementById(`${safeId}_mode_global`); const modePerDay = document.getElementById(`${safeId}_mode_per_day`); if (modeGlobal) modeGlobal.checked = schedule.mode === 'global'; if (modePerDay) modePerDay.checked = schedule.mode === 'per_day'; // Set global times const globalStart = document.getElementById(`${safeId}_global_start`); const globalEnd = document.getElementById(`${safeId}_global_end`); if (globalStart) globalStart.value = schedule.start_time; if (globalEnd) globalEnd.value = schedule.end_time; // Set per-day values DAYS.forEach(day => { const dayConfig = schedule.days[day]; const dayEnabled = document.getElementById(`${safeId}_${day}_enabled`); const dayStart = document.getElementById(`${safeId}_${day}_start`); const dayEnd = document.getElementById(`${safeId}_${day}_end`); if (dayEnabled) dayEnabled.checked = dayConfig.enabled; if (dayStart) { dayStart.value = dayConfig.start_time; dayStart.disabled = !dayConfig.enabled; dayStart.classList.toggle('bg-gray-100', !dayConfig.enabled); } if (dayEnd) { dayEnd.value = dayConfig.end_time; dayEnd.disabled = !dayConfig.enabled; dayEnd.classList.toggle('bg-gray-100', !dayConfig.enabled); } }); // Update visibility this.handlers.onModeChange(fieldId, schedule.mode); // Update hidden inputs this._updateHiddenInputs(fieldId); }, /** * Update all hidden inputs to match current state */ _updateHiddenInputs: function(fieldId) { const safeId = sanitizeId(fieldId); const schedule = this.getValue(fieldId); // Enabled const enabledHidden = document.getElementById(`${safeId}_enabled_hidden`); if (enabledHidden) enabledHidden.value = schedule.enabled; // Mode const modeHidden = document.getElementById(`${safeId}_mode_value`); if (modeHidden) modeHidden.value = schedule.mode; // Global times const startHidden = document.getElementById(`${safeId}_start_time_hidden`); const endHidden = document.getElementById(`${safeId}_end_time_hidden`); if (startHidden) startHidden.value = schedule.start_time; if (endHidden) endHidden.value = schedule.end_time; // Per-day values DAYS.forEach(day => { const dayConfig = schedule.days[day]; const enabledHidden = document.getElementById(`${safeId}_${day}_enabled_hidden`); const startHidden = document.getElementById(`${safeId}_${day}_start_hidden`); const endHidden = document.getElementById(`${safeId}_${day}_end_hidden`); if (enabledHidden) enabledHidden.value = dayConfig.enabled; if (startHidden) startHidden.value = dayConfig.start_time; if (endHidden) endHidden.value = dayConfig.end_time; }); }, handlers: { /** * Handle enabled toggle change */ onEnabledChange: function(fieldId, enabled) { const widget = window.LEDMatrixWidgets.get('schedule-picker'); widget._updateHiddenInputs(fieldId); triggerChange(fieldId, widget.getValue(fieldId)); }, /** * Handle mode switch */ onModeChange: function(fieldId, mode) { const safeId = sanitizeId(fieldId); const globalSection = document.getElementById(`${safeId}_global_section`); const perDaySection = document.getElementById(`${safeId}_perday_section`); if (globalSection) globalSection.style.display = mode === 'global' ? 'block' : 'none'; if (perDaySection) perDaySection.style.display = mode === 'per_day' ? 'block' : 'none'; const widget = window.LEDMatrixWidgets.get('schedule-picker'); widget._updateHiddenInputs(fieldId); triggerChange(fieldId, widget.getValue(fieldId)); }, /** * Handle global time change */ onGlobalTimeChange: function(fieldId) { const widget = window.LEDMatrixWidgets.get('schedule-picker'); widget._updateHiddenInputs(fieldId); triggerChange(fieldId, widget.getValue(fieldId)); }, /** * Handle day enabled change */ onDayEnabledChange: function(fieldId, day, enabled) { const safeId = sanitizeId(fieldId); const dayStart = document.getElementById(`${safeId}_${day}_start`); const dayEnd = document.getElementById(`${safeId}_${day}_end`); if (dayStart) { dayStart.disabled = !enabled; dayStart.classList.toggle('bg-gray-100', !enabled); // Set default value only when enabling and input is empty if (enabled && !dayStart.value) { dayStart.value = '07:00'; } } if (dayEnd) { dayEnd.disabled = !enabled; dayEnd.classList.toggle('bg-gray-100', !enabled); // Set default value only when enabling and input is empty if (enabled && !dayEnd.value) { dayEnd.value = '23:00'; } } const widget = window.LEDMatrixWidgets.get('schedule-picker'); widget._updateHiddenInputs(fieldId); triggerChange(fieldId, widget.getValue(fieldId)); }, /** * Handle day time change */ onDayTimeChange: function(fieldId, day) { const widget = window.LEDMatrixWidgets.get('schedule-picker'); widget._updateHiddenInputs(fieldId); triggerChange(fieldId, widget.getValue(fieldId)); } } }); // Expose constants for external use window.LEDMatrixWidgets.get('schedule-picker').DAYS = DAYS; window.LEDMatrixWidgets.get('schedule-picker').DAY_LABELS = DAY_LABELS; window.LEDMatrixWidgets.get('schedule-picker').getDefaultSchedule = getDefaultSchedule; window.LEDMatrixWidgets.get('schedule-picker').normalizeSchedule = normalizeSchedule; console.log('[SchedulePickerWidget] Schedule picker widget registered'); })();