/** * LEDMatrix Number Input Widget * * Enhanced number input with min/max/step, formatting, and increment buttons. * * Schema example: * { * "brightness": { * "type": "number", * "x-widget": "number-input", * "minimum": 0, * "maximum": 100, * "x-options": { * "step": 5, * "prefix": null, * "suffix": "%", * "showButtons": true, * "format": "integer" // "integer", "decimal", "percent" * } * } * } * * @module NumberInputWidget */ (function() { 'use strict'; const base = window.BaseWidget ? new window.BaseWidget('NumberInput', '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); } } window.LEDMatrixWidgets.register('number-input', { name: 'Number Input Widget', version: '1.0.0', render: function(container, config, value, options) { // Guard against undefined options options = options || {}; const fieldId = sanitizeId(options.fieldId || container.id || 'number_input'); const xOptions = config['x-options'] || config['x_options'] || {}; // Sanitize min/max as valid numbers or null const rawMin = config.minimum !== undefined ? config.minimum : (xOptions.min !== undefined ? xOptions.min : null); const rawMax = config.maximum !== undefined ? config.maximum : (xOptions.max !== undefined ? xOptions.max : null); const min = (rawMin !== null && Number.isFinite(Number(rawMin))) ? Number(rawMin) : null; const max = (rawMax !== null && Number.isFinite(Number(rawMax))) ? Number(rawMax) : null; // Sanitize step - must be a positive number or 'any' const rawStep = xOptions.step || (config.type === 'integer' ? 1 : 'any'); const step = (rawStep === 'any' || (Number.isFinite(Number(rawStep)) && Number(rawStep) > 0)) ? (rawStep === 'any' ? 'any' : Number(rawStep)) : 1; const prefix = xOptions.prefix || ''; const suffix = xOptions.suffix || ''; const showButtons = xOptions.showButtons !== false; const disabled = xOptions.disabled === true; const placeholder = xOptions.placeholder || ''; // Sanitize currentValue - ensure it's a safe numeric string or empty const rawValue = value !== null && value !== undefined ? value : ''; const currentValue = rawValue === '' ? '' : (isNaN(Number(rawValue)) ? '' : String(Number(rawValue))); // Escape values for safe HTML attribute interpolation const safeMin = min !== null ? escapeHtml(String(min)) : ''; const safeMax = max !== null ? escapeHtml(String(max)) : ''; const safeStep = escapeHtml(String(step)); let html = `
`; html += '
'; if (prefix) { html += `${escapeHtml(prefix)}`; } if (showButtons && !disabled) { html += ` `; } const inputRoundedClass = showButtons || prefix || suffix ? '' : 'rounded-md'; html += ` `; if (showButtons && !disabled) { html += ` `; } if (suffix) { html += `${escapeHtml(suffix)}`; } html += '
'; // Range indicator if min/max specified if (min !== null || max !== null) { const rangeText = min !== null && max !== null ? `${min} - ${max}` : (min !== null ? `Min: ${min}` : `Max: ${max}`); html += `
${escapeHtml(rangeText)}
`; } // Error message area html += ``; html += '
'; container.innerHTML = html; }, getValue: function(fieldId) { const safeId = sanitizeId(fieldId); const input = document.getElementById(`${safeId}_input`); if (!input || input.value === '') return null; const num = parseFloat(input.value); return isNaN(num) ? null : num; }, setValue: function(fieldId, value) { const safeId = sanitizeId(fieldId); const input = document.getElementById(`${safeId}_input`); if (input) { input.value = value !== null && value !== undefined ? value : ''; } }, validate: function(fieldId) { const safeId = sanitizeId(fieldId); const input = document.getElementById(`${safeId}_input`); const errorEl = document.getElementById(`${safeId}_error`); if (!input) return { valid: true, errors: [] }; const isValid = input.checkValidity(); if (errorEl) { if (!isValid) { errorEl.textContent = input.validationMessage; errorEl.classList.remove('hidden'); input.classList.add('border-red-500'); } else { errorEl.classList.add('hidden'); input.classList.remove('border-red-500'); } } return { valid: isValid, errors: isValid ? [] : [input.validationMessage] }; }, handlers: { onChange: function(fieldId) { const widget = window.LEDMatrixWidgets.get('number-input'); widget.validate(fieldId); triggerChange(fieldId, widget.getValue(fieldId)); }, onInput: function(fieldId) { // Real-time input handling if needed }, onIncrement: function(fieldId) { const safeId = sanitizeId(fieldId); const widget = document.getElementById(`${safeId}_widget`); const input = document.getElementById(`${safeId}_input`); if (!input || !widget) return; const step = parseFloat(widget.dataset.step) || 1; const max = widget.dataset.max !== '' ? parseFloat(widget.dataset.max) : Infinity; const current = parseFloat(input.value) || 0; const newValue = Math.min(current + step, max); input.value = newValue; this.onChange(fieldId); }, onDecrement: function(fieldId) { const safeId = sanitizeId(fieldId); const widget = document.getElementById(`${safeId}_widget`); const input = document.getElementById(`${safeId}_input`); if (!input || !widget) return; const step = parseFloat(widget.dataset.step) || 1; const min = widget.dataset.min !== '' ? parseFloat(widget.dataset.min) : -Infinity; const current = parseFloat(input.value) || 0; const newValue = Math.max(current - step, min); input.value = newValue; this.onChange(fieldId); } } }); console.log('[NumberInputWidget] Number input widget registered'); })();