diff --git a/example/index.html b/example/index.html index 4b74e8f..4252c80 100644 --- a/example/index.html +++ b/example/index.html @@ -483,6 +483,7 @@
+

trmnl:change event

Waiting for picker to load...
@@ -493,7 +494,7 @@ // Listen for changes (including initial state) // Attach listener BEFORE creating picker to catch initial event - document.getElementById('picker-form').addEventListener('trmnl:changed', (event) => { + document.getElementById('picker-form').addEventListener('trmnl:change', (event) => { // Display event data document.getElementById('event-output').textContent = JSON.stringify(event.detail, null, 2) diff --git a/example/main.css b/example/main.css index 52ae3d1..d3a5bda 100644 --- a/example/main.css +++ b/example/main.css @@ -137,7 +137,7 @@ pre { display: inline-block; } -.screen-preview h2 { +h2 { margin: 0 0 1.5rem 0; color: #333; font-size: 1.25rem; diff --git a/src/index.js b/src/index.js index c240df2..4283ea8 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,5 @@ +const DEFAULT_MODEL_NAME = 'og_plus' + /** * TRMNLPicker - Vanilla JS library for TRMNL device and palette selection * @@ -77,14 +79,6 @@ class TRMNLPicker { this.palettes = palettes this.localStorageKey = localStorageKey - // Initialize state - this.state = { - selectedModel: null, - selectedPalette: null, - isPortrait: false, - isDarkMode: false - } - // Only initialize if we have data if (this.models && this.palettes) { if (!Array.isArray(this.models) || this.models.length === 0) { @@ -182,7 +176,7 @@ class TRMNLPicker { paletteChange: this._handlePaletteChange.bind(this), orientationToggle: this._toggleOrientation.bind(this), darkModeToggle: this._toggleDarkMode.bind(this), - reset: this._resetToDefaults.bind(this) + reset: this._resetToModelDefaults.bind(this) } // Attach event listeners @@ -256,80 +250,30 @@ class TRMNLPicker { // Keep combined sorted list for default selection logic const sortedModels = [...sortTRMNL, ...sortBYOD] - // Load saved state from localStorage if available - const savedState = this._loadFromLocalStorage() + // Initialize state + this._state = {} - // Set model (from localStorage or default) - let selectedModel = null - if (savedState && savedState.modelName) { - selectedModel = this.models.find(m => m.name === savedState.modelName) + const savedParams = this._loadFromLocalStorage() + if (savedParams) { + this._setParams('constructor', savedParams) + } else { + const defaultModel = sortedModels.find(m => m.name === DEFAULT_MODEL_NAME) || sortedModels[0] + const defaultPaletteId = this._getFirstValidPaletteId(defaultModel) + + this._setParams('constructor', { + modelName: defaultModel.name, + paletteId: defaultPaletteId, + isPortrait: false, + isDarkMode: false + }) } - - if (!selectedModel) { - // Set default model (prefer TRMNL OG variants, otherwise first sorted) - selectedModel = - sortedModels.find(m => m.name === 'og' || (m.label && m.label.toLowerCase() === 'trmnl og')) || - sortedModels.find(m => m.name === 'og_plus') || - sortedModels.find(m => m.name === 'trmnl_original') || - sortedModels[0] - } - - this.elements.modelSelect.value = selectedModel.name - this.state.selectedModel = selectedModel - - // Populate palettes based on selected model - this._populatePalettes() - - // Set palette (from localStorage or default) - let paletteId = null - if (savedState && savedState.paletteId) { - // Check if saved palette is valid (has framework_class) and is in this model's palettes - const savedPalette = this.palettes.find(p => p.id === savedState.paletteId) - if (savedPalette && - savedPalette.framework_class && - savedPalette.framework_class.trim() !== '' && - selectedModel.palette_ids.includes(savedState.paletteId)) { - paletteId = savedState.paletteId - } - } - - if (!paletteId) { - // Get first valid palette (one with non-empty framework_class) - paletteId = this._getFirstValidPaletteId(selectedModel) - } - - this.elements.paletteSelect.value = paletteId - this.state.selectedPalette = this.palettes.find(p => p.id === paletteId) - - // Set orientation and dark mode from localStorage - if (savedState) { - if (typeof savedState.isPortrait === 'boolean') { - this.state.isPortrait = savedState.isPortrait - if (this.elements.orientationText) { - this.elements.orientationText.textContent = this.state.isPortrait ? 'Portrait' : 'Landscape' - } - } - - if (typeof savedState.isDarkMode === 'boolean') { - this.state.isDarkMode = savedState.isDarkMode - if (this.elements.darkModeText) { - this.elements.darkModeText.textContent = this.state.isDarkMode ? 'Dark Mode' : 'Light Mode' - } - } - } - - // Update UI - this._updateResetButton() - - // Emit initial changed event so consumers get the initial state (wait for DOM to load) - this._emitChangeEvent('initial') } /** * Populate palette dropdown based on selected model * @private */ - _populatePalettes() { + _populateModelPalettes() { const modelName = this.elements.modelSelect.value const model = this.models.find(m => m.name === modelName) @@ -351,85 +295,18 @@ class TRMNLPicker { } /** - * Calculate screen classes based on current state - * @private - * @returns {Array} Array of CSS class names - */ - _calculateScreenClasses() { - const model = this.state.selectedModel - const palette = this.state.selectedPalette - - if (!model) { - throw new Error('No model selected') - } - - const classes = [] - - // 0. Base screen class (always present) - classes.push('screen') - - // 1. Palette framework class - if (palette && palette.framework_class) { - classes.push(palette.framework_class) - } - - // 2. Model device class (from API) - if (model.css && model.css.classes && model.css.classes.device) { - classes.push(model.css.classes.device) - } - - // 3. Model size class (from API) - if (model.css && model.css.classes && model.css.classes.size) { - classes.push(model.css.classes.size) - } - - // 4. Orientation (UI state, only portrait - landscape is default) - if (this.state.isPortrait) { - classes.push('screen--portrait') - } - - // 5. Scale (UI state, always 1x) - classes.push('screen--1x') - - // 6. Dark mode (UI state, conditional) - if (this.state.isDarkMode) { - classes.push('screen--dark-mode') - } - - return classes - } - - /** - * Emit 'trmnl:changed' event with current state and screen classes + * Emit 'trmnl:change' event with current state and screen classes * @private */ - _emitChangeEvent(source) { - const model = this.state.selectedModel - const palette = this.state.selectedPalette - + _emitChangeEvent(origin) { // Save to localStorage if key is configured this._saveToLocalStorage() - const event = new CustomEvent('trmnl:changed', { + const event = new CustomEvent('trmnl:change', { detail: { - source, - screenClasses: this._calculateScreenClasses(), - state: { - model: model ? { - name: model.name, - label: model.label, - size: model.size, - width: model.width, - height: model.height - } : null, - palette: palette ? { - id: palette.id, - name: palette.name, - framework_class: palette.framework_class - } : null, - isPortrait: this.state.isPortrait, - isDarkMode: this.state.isDarkMode, - }, + origin, + ...this.state, + screenClasses: this.screenClasses }, bubbles: true }) @@ -465,14 +342,7 @@ class TRMNLPicker { if (!this.localStorageKey) return try { - const stateToSave = { - modelName: this.state.selectedModel?.name, - paletteId: this.state.selectedPalette?.id, - isPortrait: this.state.isPortrait, - isDarkMode: this.state.isDarkMode - } - - localStorage.setItem(this.localStorageKey, JSON.stringify(stateToSave)) + localStorage.setItem(this.localStorageKey, JSON.stringify(this.params)) } catch (error) { console.warn('TRMNLPicker: Failed to save to localStorage:', error) } @@ -483,27 +353,7 @@ class TRMNLPicker { * @private */ _handleModelChange(event) { - const modelName = event.target.value - const model = this.models.find(m => m.name === modelName) - - if (!model) return - - // Update state - this.state.selectedModel = model - - // Repopulate palettes for new model - this._populatePalettes() - - // Select first valid palette of new model - const firstPaletteId = this._getFirstValidPaletteId(model) - this.elements.paletteSelect.value = firstPaletteId - this.state.selectedPalette = this.palettes.find(p => p.id === firstPaletteId) - - // Update UI - this._updateResetButton() - - // Emit change event - this._emitChangeEvent('form') + this._setParams('form', { modelName: event.target.value }) } /** @@ -511,16 +361,7 @@ class TRMNLPicker { * @private */ _handlePaletteChange(event) { - const paletteId = event.target.value - const palette = this.palettes.find(p => p.id === paletteId) - - this.state.selectedPalette = palette - - // Update UI - this._updateResetButton() - - // Emit change event - this._emitChangeEvent('form') + this._setParams('form', { paletteId: event.target.value }) } /** @@ -528,18 +369,7 @@ class TRMNLPicker { * @private */ _toggleOrientation() { - this.state.isPortrait = !this.state.isPortrait - - // Update optional UI elements - if (this.elements.orientationText) { - this.elements.orientationText.textContent = this.state.isPortrait ? 'Portrait' : 'Landscape' - } - - // Update reset button state - this._updateResetButton() - - // Emit change event - this._emitChangeEvent('form') + this._setParams('form', { isPortrait: !this._state.isPortrait }) } /** @@ -547,50 +377,25 @@ class TRMNLPicker { * @private */ _toggleDarkMode() { - this.state.isDarkMode = !this.state.isDarkMode - - // Update optional UI elements - if (this.elements.darkModeText) { - this.elements.darkModeText.textContent = this.state.isDarkMode ? 'Dark Mode' : 'Light Mode' - } - - // Update reset button state - this._updateResetButton() - - // Emit change event - this._emitChangeEvent('form') + this._setParams('form', { isDarkMode: !this._state.isDarkMode }) } /** * Reset to defaults: first valid palette, landscape orientation, light mode * @private */ - _resetToDefaults() { - const model = this.state.selectedModel + _resetToModelDefaults() { + const model = this._state.model if (!model) return // Reset to first valid palette of current model const firstPaletteId = this._getFirstValidPaletteId(model) - this.elements.paletteSelect.value = firstPaletteId - this.state.selectedPalette = this.palettes.find(p => p.id === firstPaletteId) - // Reset to landscape orientation - this.state.isPortrait = false - if (this.elements.orientationText) { - this.elements.orientationText.textContent = 'Landscape' - } - - // Reset to light mode - this.state.isDarkMode = false - if (this.elements.darkModeText) { - this.elements.darkModeText.textContent = 'Light Mode' - } - - // Update UI - this._updateResetButton() - - // Emit change event - this._emitChangeEvent('form') + this._setParams('form', { + paletteId: firstPaletteId, + isPortrait: false, + isDarkMode: false + }) } /** @@ -601,13 +406,13 @@ class TRMNLPicker { _updateResetButton() { if (!this.elements.resetButton) return - const model = this.state.selectedModel + const model = this._state.model if (!model) return const firstValidPaletteId = this._getFirstValidPaletteId(model) const isPaletteDefault = this.elements.paletteSelect.value === String(firstValidPaletteId) - const isOrientationDefault = this.state.isPortrait === false - const isDarkModeDefault = this.state.isDarkMode === false + const isOrientationDefault = this._state.isPortrait === false + const isDarkModeDefault = this._state.isDarkMode === false const isAtDefaults = isPaletteDefault && isOrientationDefault && isDarkModeDefault @@ -622,89 +427,146 @@ class TRMNLPicker { } } + /** + * Get current screen classes + * @public + * @returns {Array} Array of CSS class names + */ + get screenClasses() { + const model = this._state.model + const palette = this._state.palette + + if (!model) { + throw new Error('No model selected') + } + + const classes = [] + + // 0. Base screen class (always present) + classes.push('screen') + + // 1. Palette framework class + if (palette && palette.framework_class) { + classes.push(palette.framework_class) + } + + // 2. Model device class (from API) + if (model.css && model.css.classes && model.css.classes.device) { + classes.push(model.css.classes.device) + } + + // 3. Model size class (from API) + if (model.css && model.css.classes && model.css.classes.size) { + classes.push(model.css.classes.size) + } + + // 4. Orientation (UI state, only portrait - landscape is default) + if (this._state.isPortrait) { + classes.push('screen--portrait') + } + + // 5. Scale (UI state, always 1x) + classes.push('screen--1x') + + // 6. Dark mode (UI state, conditional) + if (this._state.isDarkMode) { + classes.push('screen--dark-mode') + } + + return classes + } + + /** + * Get current picker parameters + * @public + * @returns {Object} Current parameters including model name, palette ID, and flags + */ + get params() { + return { + modelName: this._state.model?.name, + paletteId: this._state.palette?.id, + isPortrait: this._state.isPortrait, + isDarkMode: this._state.isDarkMode + } + } + /** * Update picker with new configuration * @public - * @param {Object} config - Configuration object - * @param {string} config.modelName - Model name to select - * @param {string} config.paletteId - Palette ID to select - * @param {boolean} config.isPortrait - Portrait orientation - * @param {boolean} config.isDarkMode - Dark mode enabled + * @param {Object} params - Configuration object + * @param {string} params.modelName - Model name to select + * @param {string} params.paletteId - Palette ID to select + * @param {boolean} params.isPortrait - Portrait orientation + * @param {boolean} params.isDarkMode - Dark mode enabled */ - update(config) { - if (!config || typeof config !== 'object') { - throw new Error('TRMNLPicker.update: config must be an object') + setParams(params) { + this._setParams('setParams', params) + } + + _setParams(origin, params) { + if (!params || typeof params !== 'object') { + throw new Error('params must be an object') } let changed = false // Update model if provided - if (config.modelName) { - const model = this.models.find(m => m.name === config.modelName) + if (params.modelName) { + const model = this.models.find(m => m.name === params.modelName) if (model) { this.elements.modelSelect.value = model.name - this.state.selectedModel = model - this._populatePalettes() + this._state.model = model + this._populateModelPalettes() + + // Select first valid palette of new model + const firstPaletteId = this._getFirstValidPaletteId(model) + this.elements.paletteSelect.value = firstPaletteId + this._state.palette = this.palettes.find(p => p.id === firstPaletteId) changed = true } } // Update palette if provided - if (config.paletteId) { - const palette = this.palettes.find(p => p.id === config.paletteId) + if (params.paletteId) { + const palette = this.palettes.find(p => p.id === params.paletteId) if (palette) { this.elements.paletteSelect.value = palette.id - this.state.selectedPalette = palette + this._state.palette = palette changed = true } } // Update orientation if provided - if (typeof config.isPortrait === 'boolean') { - this.state.isPortrait = config.isPortrait + if (typeof params.isPortrait === 'boolean') { + this._state.isPortrait = params.isPortrait if (this.elements.orientationText) { - this.elements.orientationText.textContent = this.state.isPortrait ? 'Portrait' : 'Landscape' + this.elements.orientationText.textContent = this._state.isPortrait ? 'Portrait' : 'Landscape' } changed = true } // Update dark mode if provided - if (typeof config.isDarkMode === 'boolean') { - this.state.isDarkMode = config.isDarkMode + if (typeof params.isDarkMode === 'boolean') { + this._state.isDarkMode = params.isDarkMode if (this.elements.darkModeText) { - this.elements.darkModeText.textContent = this.state.isDarkMode ? 'Dark Mode' : 'Light Mode' + this.elements.darkModeText.textContent = this._state.isDarkMode ? 'Dark Mode' : 'Light Mode' } changed = true } if (changed) { this._updateResetButton() - this._emitChangeEvent('update') } + + if (changed && origin) { + this._emitChangeEvent(origin) + } + + return changed } - /** - * Get current picker state - * @public - * @returns {Object} Current state including model, palette, flags, and screen classes - */ - getState() { - return { - model: this.state.selectedModel, - palette: this.state.selectedPalette, - isPortrait: this.state.isPortrait, - isDarkMode: this.state.isDarkMode, - screenClasses: this._calculateScreenClasses() - } - } - - /** - * Get current screen classes - * @public - * @returns {Array} Array of CSS class names - */ - getScreenClasses() { - return this._calculateScreenClasses() + get state() { + return { ...this._state, }; } /** @@ -734,7 +596,7 @@ class TRMNLPicker { this.handlers = null this.models = null this.palettes = null - this.state = null + this._state = null } }