| export class SettingsMenu { |
| constructor(containerSelector) { |
| this.container = document.querySelector(containerSelector); |
| this.eventListeners = []; |
| this.settingsConfig = this._getDefaultSettingsConfig(); |
| this.cookiePrefix = 'centichess_setting_'; |
| this.cookieExpireDays = 365; |
| |
| |
| this.cachedElements = new Map(); |
| |
| |
| this.throttledInputHandler = this._throttle(this._handleInputEvent.bind(this), 16); |
| } |
|
|
| init(chessboard) { |
| this.chessboard = chessboard; |
| this.render(); |
| this.bindEvents(); |
| this.loadSettingsFromCookies(); |
| } |
|
|
| |
| |
| |
| _getDefaultSettingsConfig() { |
| return { |
| 'Board Appearance': { |
| settings: { |
| 'boardTheme': { |
| type: 'visual-list', |
| label: 'Board Theme', |
| description: 'Choose your preferred board style', |
| defaultValue: 'green', |
| path: 'theme.boardColors', |
| options: [ |
| { |
| value: 'classic', |
| label: 'Classic', |
| preview: { type: 'board', light: '#f0d9b5', dark: '#b58863' }, |
| actions: [ |
| { path: 'theme.boardLightSquareColor', value: '#f0d9b5' }, |
| { path: 'theme.boardDarkSquareColor', value: '#b58863' } |
| ] |
| }, |
| { |
| value: 'green', |
| label: 'Green', |
| preview: { type: 'board', light: '#e0e0e0', dark: '#6ea176' }, |
| actions: [ |
| { path: 'theme.boardLightSquareColor', value: '#e0e0e0' }, |
| { path: 'theme.boardDarkSquareColor', value: '#6ea176' } |
| ] |
| }, |
| { |
| value: 'blue', |
| label: 'Blue', |
| preview: { type: 'board', light: '#dee3e6', dark: '#8ca2ad' }, |
| actions: [ |
| { path: 'theme.boardLightSquareColor', value: '#dee3e6' }, |
| { path: 'theme.boardDarkSquareColor', value: '#8ca2ad' } |
| ] |
| }, |
| { |
| value: 'purple', |
| label: 'Purple', |
| preview: { type: 'board', light: '#e8e4f0', dark: '#9b7aa0' }, |
| actions: [ |
| { path: 'theme.boardLightSquareColor', value: '#e8e4f0' }, |
| { path: 'theme.boardDarkSquareColor', value: '#9b7aa0' } |
| ] |
| }, |
| { |
| value: 'red', |
| label: 'Red', |
| preview: { type: 'board', light: '#ffe6e6', dark: '#cc5555' }, |
| actions: [ |
| { path: 'theme.boardLightSquareColor', value: '#ffe6e6' }, |
| { path: 'theme.boardDarkSquareColor', value: '#cc5555' } |
| ] |
| }, |
| { |
| value: 'orange', |
| label: 'Orange', |
| preview: { type: 'board', light: '#ffe6e6', dark: '#cd8042' }, |
| actions: [ |
| { path: 'theme.boardLightSquareColor', value: '#ffe6e6' }, |
| { path: 'theme.boardDarkSquareColor', value: '#cd8042' } |
| ] |
| } |
| ] |
| }, |
| 'pieceTheme': { |
| type: 'visual-list', |
| label: 'Piece Set', |
| description: 'Choose your preferred piece style', |
| defaultValue: 'cburnett', |
| path: 'theme.pieceFolderName', |
| options: [ |
| { |
| value: 'cburnett', |
| label: 'Classic', |
| preview: { type: 'pieces', folder: 'cburnett' } |
| }, |
| { |
| value: 'alpha', |
| label: 'Alpha', |
| preview: { type: 'pieces', folder: 'alpha' } |
| }, |
| { |
| value: 'anarcandy', |
| label: 'Anarcandy', |
| preview: { type: 'pieces', folder: 'anarcandy' } |
| }, |
| { |
| value: 'caliente', |
| label: 'Caliente', |
| preview: { type: 'pieces', folder: 'caliente' } |
| }, |
| { |
| value: 'california', |
| label: 'California', |
| preview: { type: 'pieces', folder: 'california' } |
| }, |
| { |
| value: 'cardinal', |
| label: 'Cardinal', |
| preview: { type: 'pieces', folder: 'cardinal' } |
| }, |
| { |
| value: 'celtic', |
| label: 'Celtic', |
| preview: { type: 'pieces', folder: 'celtic' } |
| }, |
| { |
| value: 'chess7', |
| label: 'Chess7', |
| preview: { type: 'pieces', folder: 'chess7' } |
| }, |
| { |
| value: 'chessnut', |
| label: 'Chessnut', |
| preview: { type: 'pieces', folder: 'chessnut' } |
| }, |
| { |
| value: 'companion', |
| label: 'Companion', |
| preview: { type: 'pieces', folder: 'companion' } |
| }, |
| { |
| value: 'cooke', |
| label: 'Cooke', |
| preview: { type: 'pieces', folder: 'cooke' } |
| }, |
| { |
| value: 'dubrovny', |
| label: 'Dubrovny', |
| preview: { type: 'pieces', folder: 'dubrovny' } |
| }, |
| { |
| value: 'fantasy', |
| label: 'Fantasy', |
| preview: { type: 'pieces', folder: 'fantasy' } |
| }, |
| { |
| value: 'firi', |
| label: 'Firi', |
| preview: { type: 'pieces', folder: 'firi' } |
| }, |
| { |
| value: 'fresca', |
| label: 'Fresca', |
| preview: { type: 'pieces', folder: 'fresca' } |
| }, |
| { |
| value: 'gioco', |
| label: 'Gioco', |
| preview: { type: 'pieces', folder: 'gioco' } |
| }, |
| { |
| value: 'governor', |
| label: 'Governor', |
| preview: { type: 'pieces', folder: 'governor' } |
| }, |
| { |
| value: 'horsey', |
| label: 'Horsey', |
| preview: { type: 'pieces', folder: 'horsey' } |
| }, |
| { |
| value: 'icpieces', |
| label: 'IC Pieces', |
| preview: { type: 'pieces', folder: 'icpieces' } |
| }, |
| { |
| value: 'kiwen-suwi', |
| label: 'Kiwen Suwi', |
| preview: { type: 'pieces', folder: 'kiwen-suwi' } |
| }, |
| { |
| value: 'kosal', |
| label: 'Kosal', |
| preview: { type: 'pieces', folder: 'kosal' } |
| }, |
| { |
| value: 'leipzig', |
| label: 'Leipzig', |
| preview: { type: 'pieces', folder: 'leipzig' } |
| }, |
| { |
| value: 'maestro', |
| label: 'Maestro', |
| preview: { type: 'pieces', folder: 'maestro' } |
| }, |
| { |
| value: 'merida', |
| label: 'Merida', |
| preview: { type: 'pieces', folder: 'merida' } |
| }, |
| { |
| value: 'monarchy', |
| label: 'Monarchy', |
| preview: { type: 'pieces', folder: 'monarchy' } |
| }, |
| { |
| value: 'mpchess', |
| label: 'MP Chess', |
| preview: { type: 'pieces', folder: 'mpchess' } |
| }, |
| { |
| value: 'pirouetti', |
| label: 'Pirouetti', |
| preview: { type: 'pieces', folder: 'pirouetti' } |
| }, |
| { |
| value: 'pixel', |
| label: 'Pixel', |
| preview: { type: 'pieces', folder: 'pixel' } |
| }, |
| { |
| value: 'reillycraig', |
| label: 'Reilly Craig', |
| preview: { type: 'pieces', folder: 'reillycraig' } |
| }, |
| { |
| value: 'rhosgfx', |
| label: 'Rhos GFX', |
| preview: { type: 'pieces', folder: 'rhosgfx' } |
| }, |
| { |
| value: 'riohacha', |
| label: 'Riohacha', |
| preview: { type: 'pieces', folder: 'riohacha' } |
| }, |
| { |
| value: 'shapes', |
| label: 'Shapes', |
| preview: { type: 'pieces', folder: 'shapes' } |
| }, |
| { |
| value: 'spatial', |
| label: 'Spatial', |
| preview: { type: 'pieces', folder: 'spatial' } |
| }, |
| { |
| value: 'staunty', |
| label: 'Staunty', |
| preview: { type: 'pieces', folder: 'staunty' } |
| }, |
| { |
| value: 'tatiana', |
| label: 'Tatiana', |
| preview: { type: 'pieces', folder: 'tatiana' } |
| }, |
| { |
| value: 'xkcd', |
| label: 'XKCD', |
| preview: { type: 'pieces', folder: 'xkcd' } |
| } |
| ] |
| }, |
| 'boardColors': { |
| type: 'group', |
| label: 'Custom Board Colors', |
| description: 'Customize light and dark square colors manually', |
| settings: [ |
| { |
| key: 'boardLightSquareColor', |
| type: 'color', |
| label: 'Light Squares', |
| defaultValue: '#f0d9b5', |
| path: 'theme.boardLightSquareColor', |
| preview: { target: '.color-preview-box' } |
| }, |
| { |
| key: 'boardDarkSquareColor', |
| type: 'color', |
| label: 'Dark Squares', |
| defaultValue: '#b58863', |
| path: 'theme.boardDarkSquareColor', |
| preview: { target: '.color-preview-box' } |
| } |
| ] |
| }, |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| } |
| }, |
| 'Engine & Quick Settings': { |
| settings: { |
| 'engineSettings': { |
| type: 'group', |
| label: 'Engine Settings', |
| description: 'Engine settings', |
| settings: [ |
| { |
| key: 'engineType', |
| type: 'dropdown', |
| label: 'Engine Type', |
| defaultValue: 'stockfish-17-lite', |
| options: [ |
| { |
| value: 'stockfish-17-lite', |
| label: 'Stockfish 17 Lite' |
| }, |
| { |
| value: 'stockfish-16-nnue', |
| label: 'Stockfish 16 NNUE ' |
| }, |
| { |
| value: 'stockfish-16-lite', |
| label: 'Stockfish 16 Lite' |
| }, |
| { |
| value: 'stockfish-11', |
| label: 'Stockfish 11' |
| } |
| ], |
| }, |
| { |
| key: 'engineDepth', |
| type: 'slider', |
| label: 'Engine Depth', |
| defaultValue: 14, |
| min: 2, |
| max: 24, |
| }, |
| { |
| key: 'variationEngineDepth', |
| type: 'slider', |
| label: 'Variation Engine Depth', |
| defaultValue: 14, |
| min: 2, |
| max: 24, |
| } |
| ] |
| }, |
| 'quickToggles': { |
| type: 'group', |
| label: 'Quick Toggles', |
| description: 'Quickly toggle settings', |
| settings: [ |
| { |
| key: 'showBoardLabels', |
| type: 'toggle', |
| label: 'Show Board Labels', |
| description: 'Display file and rank labels around the board', |
| defaultValue: true, |
| path: 'showBoardLabels' |
| }, |
| { |
| key: 'audioEnabled', |
| type: 'toggle', |
| label: 'Sound Effects', |
| description: 'Play sounds for moves and captures', |
| defaultValue: true, |
| path: 'audioEnabled' |
| } |
| ] |
| } |
| } |
| } |
| }; |
| } |
|
|
| |
| |
| |
| addSettingsGroup(groupName, groupConfig) { |
| this.settingsConfig[groupName] = groupConfig; |
| this.render(); |
| } |
|
|
| |
| |
| |
| addSetting(groupName, settingKey, settingConfig) { |
| if (this.settingsConfig[groupName]) { |
| this.settingsConfig[groupName].settings[settingKey] = settingConfig; |
| this.render(); |
| } |
| } |
|
|
| |
| |
| |
| render() { |
| this.container.innerHTML = ''; |
| this._clearElementCache(); |
| |
| Object.entries(this.settingsConfig).forEach(([groupName, groupConfig]) => { |
| const groupElement = this._createSettingsGroup(groupName, groupConfig); |
| this.container.appendChild(groupElement); |
| }); |
| } |
|
|
| |
| |
| |
| _createSettingsGroup(groupName, groupConfig) { |
| const groupDiv = document.createElement('div'); |
| groupDiv.className = 'settings-group'; |
| |
| const header = document.createElement('div'); |
| header.className = 'settings-group-header'; |
| header.innerHTML = `<h3 class="settings-group-title">${groupName}</h3>`; |
| |
| const content = document.createElement('div'); |
| content.className = 'settings-group-content'; |
| |
| Object.entries(groupConfig.settings).forEach(([settingKey, settingConfig]) => { |
| const settingElement = this._createSetting(settingKey, settingConfig); |
| content.appendChild(settingElement); |
| }); |
| |
| groupDiv.appendChild(header); |
| groupDiv.appendChild(content); |
| |
| return groupDiv; |
| } |
|
|
| |
| |
| |
| _createSetting(settingKey, config) { |
| if (config.type === 'group') { |
| return this._createGroupedSetting(settingKey, config); |
| } |
|
|
| const settingDiv = document.createElement('div'); |
| settingDiv.className = 'setting-item'; |
| settingDiv.setAttribute('data-setting', settingKey); |
| |
| const label = document.createElement('div'); |
| label.className = 'setting-label'; |
| label.innerHTML = ` |
| <span class="setting-name">${config.label}</span> |
| ${config.description ? `<span class="setting-description">${config.description}</span>` : ''} |
| `; |
| |
| const input = document.createElement('div'); |
| input.className = 'setting-input'; |
| |
| const inputElement = this._createInputByType(settingKey, config); |
| input.appendChild(inputElement); |
| |
| settingDiv.appendChild(label); |
| settingDiv.appendChild(input); |
| |
| return settingDiv; |
| } |
|
|
| |
| |
| |
| _createInputByType(settingKey, config) { |
| const inputCreators = { |
| 'color': () => this._createColorInput(settingKey, config), |
| 'slider': () => this._createSliderInput(settingKey, config), |
| 'dropdown': () => this._createDropdownInput(settingKey, config), |
| 'visual-list': () => this._createVisualListInput(settingKey, config), |
| 'toggle': () => this._createToggleInput(settingKey, config), |
| 'button': () => this._createButtonInput(settingKey, config) |
| }; |
|
|
| const creator = inputCreators[config.type]; |
| if (creator) { |
| return creator(); |
| } else { |
| const errorSpan = document.createElement('span'); |
| errorSpan.className = 'setting-error'; |
| errorSpan.textContent = `Unknown setting type: ${config.type}`; |
| return errorSpan; |
| } |
| } |
|
|
| |
| |
| |
| _createGroupedSetting(groupKey, config) { |
| const groupDiv = document.createElement('div'); |
| groupDiv.className = 'setting-item setting-group-item'; |
| groupDiv.setAttribute('data-setting', groupKey); |
| |
| if (config.label) { |
| const label = document.createElement('div'); |
| label.className = 'setting-group-label'; |
| label.innerHTML = ` |
| <span class="setting-name">${config.label}</span> |
| ${config.description ? `<span class="setting-description">${config.description}</span>` : ''} |
| `; |
| groupDiv.appendChild(label); |
| } |
| |
| const groupContent = document.createElement('div'); |
| groupContent.className = 'setting-group-content-inline'; |
| |
| config.settings.forEach(subConfig => { |
| const subSettingDiv = document.createElement('div'); |
| subSettingDiv.className = 'setting-sub-item'; |
| subSettingDiv.setAttribute('data-setting', subConfig.key); |
| |
| const subLabel = document.createElement('div'); |
| subLabel.className = 'setting-sub-label'; |
| subLabel.innerHTML = `<span class="setting-sub-name">${subConfig.label}</span>`; |
| |
| const subInput = document.createElement('div'); |
| subInput.className = 'setting-sub-input'; |
| |
| const inputElement = this._createInputByType(subConfig.key, subConfig); |
| subInput.appendChild(inputElement); |
| |
| subSettingDiv.appendChild(subLabel); |
| subSettingDiv.appendChild(subInput); |
| groupContent.appendChild(subSettingDiv); |
| }); |
| |
| groupDiv.appendChild(groupContent); |
| return groupDiv; |
| } |
|
|
| |
| |
| |
| _createColorInput(settingKey, config) { |
| const container = document.createElement('div'); |
| container.className = 'color-input-container'; |
| |
| const currentValue = this.getSettingValue(settingKey) || config.defaultValue; |
| |
| const colorBox = document.createElement('div'); |
| colorBox.className = 'color-preview-box'; |
| colorBox.style.backgroundColor = currentValue; |
| |
| const input = document.createElement('input'); |
| input.type = 'color'; |
| input.className = 'color-input'; |
| input.value = currentValue; |
| input.setAttribute('data-path', config.path); |
| input.setAttribute('data-setting-key', settingKey); |
| |
| container.appendChild(colorBox); |
| container.appendChild(input); |
| |
| return container; |
| } |
|
|
| |
| |
| |
| _createSliderInput(settingKey, config) { |
| const container = document.createElement('div'); |
| container.className = 'slider-input-container'; |
| |
| const currentValue = this.getSettingValue(settingKey) !== null ? this.getSettingValue(settingKey) : config.defaultValue; |
| |
| const slider = document.createElement('input'); |
| slider.type = 'range'; |
| slider.className = 'slider-input'; |
| slider.min = config.min; |
| slider.max = config.max; |
| slider.step = config.step; |
| slider.value = currentValue; |
| slider.setAttribute('data-path', config.path); |
| slider.setAttribute('data-setting-key', settingKey); |
| |
| |
| this._updateSliderProgress(slider, currentValue, config.min, config.max); |
| |
| const valueDisplay = document.createElement('span'); |
| valueDisplay.className = 'slider-value'; |
| valueDisplay.textContent = config.format ? config.format(currentValue) : currentValue; |
| |
| container.appendChild(slider); |
| container.appendChild(valueDisplay); |
| |
| return container; |
| } |
|
|
| |
| |
| |
| _createDropdownInput(settingKey, config) { |
| const select = document.createElement('select'); |
| select.className = 'dropdown-input'; |
| select.setAttribute('data-path', config.path); |
| select.setAttribute('data-setting-key', settingKey); |
| |
| const currentValue = this.getSettingValue(settingKey) || config.defaultValue; |
| |
| config.options.forEach(option => { |
| const optionElement = document.createElement('option'); |
| optionElement.value = option.value; |
| optionElement.textContent = option.label; |
| if (option.value === currentValue) { |
| optionElement.selected = true; |
| } |
| select.appendChild(optionElement); |
| }); |
| |
| return select; |
| } |
|
|
| |
| |
| |
| _createVisualListInput(settingKey, config) { |
| const container = document.createElement('div'); |
| container.className = 'visual-list-container'; |
| |
| const currentValue = this.getSettingValue(settingKey) || config.defaultValue; |
| |
| config.options.forEach(option => { |
| const optionDiv = document.createElement('div'); |
| optionDiv.className = 'visual-list-option'; |
| optionDiv.setAttribute('data-value', option.value); |
| optionDiv.setAttribute('data-path', config.path); |
| optionDiv.setAttribute('data-setting-key', settingKey); |
| |
| if (option.value === currentValue) { |
| optionDiv.classList.add('selected'); |
| } |
| |
| |
| const previewElement = this._createPreviewElement(option.preview); |
| const labelElement = document.createElement('span'); |
| labelElement.className = 'visual-list-label'; |
| labelElement.textContent = option.label; |
| |
| optionDiv.appendChild(previewElement); |
| optionDiv.appendChild(labelElement); |
| container.appendChild(optionDiv); |
| }); |
| |
| return container; |
| } |
|
|
| |
| |
| |
| _createPreviewElement(previewConfig) { |
| if (!previewConfig) { |
| return document.createElement('div'); |
| } |
|
|
| const previewCreators = { |
| 'board': (config) => { |
| const preview = document.createElement('div'); |
| preview.className = 'board-preview'; |
| preview.innerHTML = ` |
| <div class="board-preview-square light" style="background-color: ${config.light}"></div> |
| <div class="board-preview-square dark" style="background-color: ${config.dark}"></div> |
| <div class="board-preview-square dark" style="background-color: ${config.dark}"></div> |
| <div class="board-preview-square light" style="background-color: ${config.light}"></div> |
| `; |
| return preview; |
| }, |
| 'pieces': (config) => { |
| const preview = document.createElement('div'); |
| preview.className = 'piece-preview'; |
| preview.innerHTML = ` |
| <img src="assets/pieces/${config.folder}/wK.svg" alt="White King" class="piece-preview-icon"> |
| <img src="assets/pieces/${config.folder}/bQ.svg" alt="Black Queen" class="piece-preview-icon"> |
| `; |
| return preview; |
| } |
| }; |
|
|
| const creator = previewCreators[previewConfig.type]; |
| return creator ? creator(previewConfig) : document.createElement('div'); |
| } |
|
|
| |
| |
| |
| _createToggleInput(settingKey, config) { |
| const container = document.createElement('div'); |
| container.className = 'toggle-input-container'; |
| |
| const currentValue = this.getSettingValue(settingKey) !== null ? this.getSettingValue(settingKey) : config.defaultValue; |
| |
| const toggle = document.createElement('input'); |
| toggle.type = 'checkbox'; |
| toggle.className = 'toggle-input'; |
| toggle.checked = currentValue; |
| toggle.setAttribute('data-path', config.path); |
| toggle.setAttribute('data-setting-key', settingKey); |
| toggle.id = `toggle-${settingKey}`; |
| |
| const label = document.createElement('label'); |
| label.className = 'toggle-label'; |
| label.setAttribute('for', `toggle-${settingKey}`); |
| |
| const slider = document.createElement('span'); |
| slider.className = 'toggle-slider'; |
| |
| label.appendChild(slider); |
| container.appendChild(toggle); |
| container.appendChild(label); |
| |
| return container; |
| } |
|
|
| |
| |
| |
| _createButtonInput(settingKey, config) { |
| const button = document.createElement('button'); |
| button.className = 'setting-button'; |
| button.textContent = config.buttonText || config.label; |
| button.type = 'button'; |
| button.setAttribute('data-action', config.action || ''); |
| |
| return button; |
| } |
|
|
| |
| |
| |
| bindEvents() { |
| this._cleanupEventListeners(); |
|
|
| |
| const containerHandler = (e) => this._handleContainerEvent(e); |
| this.container.addEventListener('click', containerHandler); |
| this.container.addEventListener('change', containerHandler); |
| this.container.addEventListener('input', containerHandler); |
| |
| this.eventListeners.push({ |
| element: this.container, |
| event: 'click', |
| handler: containerHandler |
| }); |
| this.eventListeners.push({ |
| element: this.container, |
| event: 'change', |
| handler: containerHandler |
| }); |
| this.eventListeners.push({ |
| element: this.container, |
| event: 'input', |
| handler: containerHandler |
| }); |
| } |
|
|
| |
| |
| |
| _handleContainerEvent(e) { |
| const element = e.target; |
| const eventType = e.type; |
| |
| |
| if (!this._isSettingsControl(element)) { |
| return; |
| } |
|
|
| |
| if (eventType === 'input') { |
| |
| this.throttledInputHandler(e); |
| } else if (eventType === 'change') { |
| this._handleChangeEvent(e); |
| } else if (eventType === 'click') { |
| this._handleClickEvent(e); |
| } |
| } |
|
|
| |
| |
| |
| _isSettingsControl(element) { |
| return element.classList.contains('color-input') || |
| element.classList.contains('slider-input') || |
| element.classList.contains('dropdown-input') || |
| element.classList.contains('toggle-input') || |
| element.classList.contains('visual-list-option') || |
| element.classList.contains('setting-button') || |
| element.closest('.visual-list-option'); |
| } |
|
|
| |
| |
| |
| _handleInputEvent(e) { |
| const element = e.target; |
| const value = this._extractValueFromElement(element); |
| |
| |
| if (this._hasPreviewMode(element)) { |
| this._handlePreviewUpdate(element, value); |
| } |
| } |
|
|
| |
| |
| |
| _handleChangeEvent(e) { |
| const element = e.target; |
| const settingKey = element.getAttribute('data-setting-key'); |
| const path = element.getAttribute('data-path'); |
| const value = this._extractValueFromElement(element); |
| |
| if (settingKey && path) { |
| this._handleValueChange(settingKey, path, value, element); |
| } else { |
| console.warn('Missing settingKey or path for element:', element, { |
| settingKey: settingKey, |
| path: path |
| }); |
| } |
| } |
|
|
| |
| |
| |
| _handleClickEvent(e) { |
| const element = e.target; |
| const action = element.getAttribute('data-action'); |
| |
| |
| if (element.classList.contains('setting-button') && action) { |
| this._executeAction(action); |
| return; |
| } |
|
|
| |
| const visualListOption = element.classList.contains('visual-list-option') ? |
| element : element.closest('.visual-list-option'); |
| if (visualListOption) { |
| this._handleVisualListSelection(visualListOption); |
| } |
| } |
|
|
| |
| |
| |
| _handlePreviewUpdate(element, value) { |
| if (element.classList.contains('color-input')) { |
| |
| const container = element.closest('.color-input-container'); |
| const settingKey = element.getAttribute('data-setting-key'); |
| |
| |
| let preview = this._getCachedElement('.color-preview-box', container); |
| if (!preview) { |
| preview = container.querySelector('.color-preview-box'); |
| if (preview && settingKey) { |
| this._cacheElement(`preview_${settingKey}`, preview); |
| } |
| } |
| if (preview) { |
| preview.style.backgroundColor = value; |
| } |
| } else if (element.classList.contains('slider-input')) { |
| const container = element.closest('.slider-input-container'); |
| const settingKey = element.getAttribute('data-setting-key'); |
| |
| |
| let valueDisplay = this._getCachedElement('.slider-value', container); |
| if (!valueDisplay) { |
| valueDisplay = container.querySelector('.slider-value'); |
| if (valueDisplay && settingKey) { |
| this._cacheElement(`slider_${settingKey}`, valueDisplay); |
| } |
| } |
| |
| if (valueDisplay) { |
| const config = this._findSettingConfig(settingKey); |
| valueDisplay.textContent = config?.format ? config.format(value) : value; |
| |
| |
| if (config) { |
| this._updateSliderProgress(element, value, config.min, config.max); |
| } |
| } |
| } |
| } |
|
|
| |
| |
| |
| _updateSettingUI(settingKey, value) { |
| |
| let element = this._getCachedElement(`[data-setting-key="${settingKey}"]`); |
| if (!element) { |
| element = this.container.querySelector(`[data-setting-key="${settingKey}"]`); |
| if (element) { |
| this._cacheElement(`setting_${settingKey}`, element); |
| } |
| } |
| |
| if (!element) return; |
|
|
| if (element.type === 'color') { |
| element.value = value; |
| const container = element.closest('.color-input-container'); |
| |
| |
| let preview = this.cachedElements.get(`preview_${settingKey}`); |
| if (!preview) { |
| preview = container?.querySelector('.color-preview-box'); |
| if (preview) { |
| this._cacheElement(`preview_${settingKey}`, preview); |
| } |
| } |
| if (preview) { |
| preview.style.backgroundColor = value; |
| } |
| } else if (element.type === 'checkbox') { |
| element.checked = value; |
| } else if (element.type === 'range') { |
| element.value = value; |
| const container = element.closest('.slider-input-container'); |
| |
| |
| let valueDisplay = this.cachedElements.get(`slider_${settingKey}`); |
| if (!valueDisplay) { |
| valueDisplay = container?.querySelector('.slider-value'); |
| if (valueDisplay) { |
| this._cacheElement(`slider_${settingKey}`, valueDisplay); |
| } |
| } |
| const config = this._findSettingConfig(settingKey); |
| if (valueDisplay) { |
| valueDisplay.textContent = config?.format ? config.format(value) : value; |
| } |
| |
| if (config) { |
| this._updateSliderProgress(element, value, config.min, config.max); |
| } |
| } else if (element.tagName === 'SELECT') { |
| element.value = value; |
| } |
| } |
|
|
| |
| |
| |
| _handleVisualListSelection(option) { |
| const container = option.closest('.visual-list-container'); |
| const path = option.getAttribute('data-path'); |
| const value = option.getAttribute('data-value'); |
| const settingKey = option.getAttribute('data-setting-key'); |
| |
| |
| requestAnimationFrame(() => { |
| |
| const options = container.querySelectorAll('.visual-list-option'); |
| options.forEach(opt => opt.classList.remove('selected')); |
| option.classList.add('selected'); |
| }); |
| |
| this._handleValueChange(settingKey, path, value, option); |
| } |
|
|
| |
| |
| |
| _handleVisualListDeselection(changedPath, changedValue) { |
| |
| const visualListContainers = this.container.querySelectorAll('.visual-list-container'); |
| |
| |
| this._traverseSettings((settingKey, config) => { |
| if (config.type === 'visual-list' && config.options) { |
| |
| const hasMatchingAction = config.options.some(option => |
| option.actions && option.actions.some(action => action.path === changedPath) |
| ); |
| |
| if (hasMatchingAction) { |
| |
| const currentValue = this.getSettingValue(settingKey); |
| const currentOption = this._findOptionByValue(config, currentValue); |
| |
| if (currentOption && currentOption.actions) { |
| const stillMatches = currentOption.actions.every(action => { |
| if (action.path === changedPath) { |
| return action.value === changedValue; |
| } |
| |
| const relatedValue = this.getSettingValue(this._findSettingKeyByPath(action.path)); |
| return relatedValue === action.value; |
| }); |
| |
| if (!stillMatches) { |
| |
| let visualContainer = this._getCachedElement(`[data-setting-key="${settingKey}"] .visual-list-container`); |
| if (!visualContainer) { |
| const container = this.container.querySelector(`[data-setting-key="${settingKey}"]`); |
| visualContainer = container?.querySelector('.visual-list-container'); |
| } |
| |
| if (visualContainer) { |
| |
| requestAnimationFrame(() => { |
| visualContainer.querySelectorAll('.visual-list-option').forEach(opt => |
| opt.classList.remove('selected') |
| ); |
| }); |
| |
| this._deleteCookie(settingKey); |
| } |
| } |
| } |
| } |
| } |
| }); |
| } |
|
|
| |
| |
| |
| _cleanupEventListeners() { |
| this.eventListeners.forEach(({ element, event, handler }) => { |
| element.removeEventListener(event, handler); |
| }); |
| this.eventListeners = []; |
| this._clearElementCache(); |
| } |
|
|
| |
| |
| |
| destroy() { |
| this._cleanupEventListeners(); |
| this._clearElementCache(); |
| } |
|
|
| |
| |
| |
| _throttle(func, limit) { |
| let inThrottle; |
| return function() { |
| const args = arguments; |
| const context = this; |
| if (!inThrottle) { |
| func.apply(context, args); |
| inThrottle = true; |
| setTimeout(() => inThrottle = false, limit); |
| } |
| } |
| } |
|
|
| |
| |
| |
| _cacheElement(key, element) { |
| this.cachedElements.set(key, element); |
| return element; |
| } |
|
|
| |
| |
| |
| _getCachedElement(selector, parent = this.container) { |
| |
| let parentKey = 'container'; |
| if (parent !== this.container) { |
| |
| const settingKey = parent.closest('[data-setting]')?.getAttribute('data-setting') || |
| parent.querySelector('[data-setting-key]')?.getAttribute('data-setting-key') || |
| 'unknown'; |
| parentKey = `parent_${settingKey}`; |
| } |
| |
| const key = `${parentKey}_${selector}`; |
| if (this.cachedElements.has(key)) { |
| return this.cachedElements.get(key); |
| } |
| const element = parent.querySelector(selector); |
| if (element) { |
| this._cacheElement(key, element); |
| } |
| return element; |
| } |
|
|
| |
| |
| |
| _clearElementCache() { |
| this.cachedElements.clear(); |
| } |
|
|
| |
| |
| |
| _executeAction(actionName) { |
| const actions = { |
| 'clearAllSettings': () => this.clearAllSettings() |
| }; |
| |
| const action = actions[actionName]; |
| if (action) { |
| action(); |
| } |
| } |
|
|
| |
| |
| |
| _updateChessboardSetting(path, value) { |
| if (this.chessboard && this.chessboard.setOption) { |
| this.chessboard.setOption(path, value); |
| } |
| } |
|
|
| |
| |
| |
| _findSettingConfig(settingKey) { |
| for (const group of Object.values(this.settingsConfig)) { |
| if (group.settings[settingKey]) { |
| return group.settings[settingKey]; |
| } |
| |
| for (const setting of Object.values(group.settings)) { |
| if (setting.type === 'group' && setting.settings) { |
| const found = setting.settings.find(s => s.key === settingKey); |
| if (found) return found; |
| } |
| } |
| } |
| return null; |
| } |
|
|
| |
| |
| |
| _updateSliderProgress(sliderElement, value, min, max) { |
| const percentage = ((value - min) / (max - min)) * 100; |
| sliderElement.style.setProperty('--slider-progress', `${percentage}%`); |
| } |
|
|
| |
| |
| |
| _findSettingKeyByPath(path) { |
| let foundKey = null; |
| this._traverseSettings((settingKey, config) => { |
| if (config.path === path) { |
| foundKey = settingKey; |
| } |
| }); |
| return foundKey; |
| } |
|
|
| |
| |
| |
| _findOptionByValue(config, value) { |
| return config?.options?.find(option => option.value === value); |
| } |
|
|
| |
| |
| |
| loadSettingsFromCookies() { |
| this._traverseSettings((settingKey, config) => { |
| const savedValue = this._getCookie(settingKey); |
| if (savedValue !== null) { |
| const option = this._findOptionByValue(config, savedValue); |
| |
| if (option && option.actions) { |
| |
| option.actions.forEach(action => { |
| this._updateChessboardSetting(action.path, action.value); |
| }); |
| } else if (config.path) { |
| |
| this._updateChessboardSetting(config.path, savedValue); |
| } |
| |
| |
| config.defaultValue = savedValue; |
| } |
| }); |
| } |
|
|
| |
| |
| |
| saveSettingToCookie(settingKey, value) { |
| this._setCookie(settingKey, value); |
| } |
|
|
| |
| |
| |
| reapplyAllSettings() { |
| this.loadSettingsFromCookies(); |
| } |
|
|
| |
| |
| |
| clearAllSettings() { |
| this._traverseSettings((settingKey) => { |
| this._deleteCookie(settingKey); |
| }); |
| |
| if (confirm('This will reset all settings to defaults and reload the page. Continue?')) { |
| window.location.reload(); |
| } |
| } |
|
|
| |
| |
| |
| _traverseSettings(callback) { |
| Object.values(this.settingsConfig).forEach(groupConfig => { |
| Object.entries(groupConfig.settings).forEach(([settingKey, settingConfig]) => { |
| if (settingConfig.type === 'group') { |
| settingConfig.settings.forEach(subConfig => { |
| callback(subConfig.key, subConfig); |
| }); |
| } else { |
| callback(settingKey, settingConfig); |
| } |
| }); |
| }); |
| } |
|
|
| |
| |
| |
| getSettingValue(settingKey) { |
| const savedValue = this._getCookie(settingKey); |
| if (savedValue !== null) { |
| return savedValue; |
| } |
|
|
| const config = this._findSettingConfig(settingKey); |
| return config?.defaultValue || null; |
| } |
|
|
| |
| |
| |
| _setCookie(name, value, days = this.cookieExpireDays) { |
| const expires = new Date(); |
| expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000)); |
| document.cookie = `${this.cookiePrefix}${name}=${encodeURIComponent(JSON.stringify(value))};expires=${expires.toUTCString()};path=/`; |
| } |
|
|
| _getCookie(name) { |
| const cookieName = `${this.cookiePrefix}${name}=`; |
| const decodedCookie = decodeURIComponent(document.cookie); |
| const cookies = decodedCookie.split(';'); |
| |
| for (let cookie of cookies) { |
| cookie = cookie.trim(); |
| if (cookie.indexOf(cookieName) === 0) { |
| try { |
| return JSON.parse(cookie.substring(cookieName.length)); |
| } catch (e) { |
| console.warn(`Failed to parse cookie ${name}:`, e); |
| return null; |
| } |
| } |
| } |
| return null; |
| } |
|
|
| _deleteCookie(name) { |
| document.cookie = `${this.cookiePrefix}${name}=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/`; |
| } |
|
|
| |
| |
| |
| _extractValueFromElement(element) { |
| if (element.type === 'checkbox') { |
| return element.checked; |
| } else if (element.type === 'range') { |
| return parseFloat(element.value); |
| } else { |
| return element.value; |
| } |
| } |
|
|
| |
| |
| |
| _hasPreviewMode(element) { |
| return element.classList.contains('color-input') || element.classList.contains('slider-input'); |
| } |
|
|
| |
| |
| |
| _handleValueChange(settingKey, path, value, element) { |
| if (settingKey) { |
| this.saveSettingToCookie(settingKey, value); |
| } |
|
|
| |
| const config = this._findSettingConfig(settingKey); |
| const option = this._findOptionByValue(config, value); |
| |
| if (option && option.actions) { |
| |
| option.actions.forEach(action => { |
| this._updateChessboardSetting(action.path, action.value); |
| |
| |
| const relatedSettingKey = this._findSettingKeyByPath(action.path); |
| if (relatedSettingKey) { |
| this.saveSettingToCookie(relatedSettingKey, action.value); |
| } |
| }); |
| |
| this._syncRelatedSettings(option.actions.map(action => ({ path: action.path, value: action.value }))); |
| } else if (path) { |
| |
| this._updateChessboardSetting(path, value); |
| |
| this._syncRelatedSettings([{ path, value }]); |
| } |
| } |
|
|
| |
| |
| |
| _syncRelatedSettings(changedPaths) { |
| changedPaths.forEach(({ path, value }) => { |
| |
| this._traverseSettings((settingKey, config) => { |
| if (config.path === path) { |
| |
| this._updateSettingUI(settingKey, value); |
| |
| this.saveSettingToCookie(settingKey, value); |
| } |
| }); |
| |
| |
| this._handleVisualListDeselection(path, value); |
| }); |
| } |
| } |