From dc9bd62e088ab2ba1e79a20923dbcfe0e230a93e Mon Sep 17 00:00:00 2001 From: Viet Ngoc Date: Tue, 26 Nov 2024 06:52:42 +0100 Subject: [PATCH] refactor: add 'yaml-editor' action to IMAGE_CONFIG_ACTIONS enum --- src/editor/components/panel-images-editor.ts | 50 ++- src/editor/components/panel-indicator.ts | 59 ++++ src/editor/components/panel-range-info.ts | 330 +++++++++++-------- src/editor/editor-const.ts | 2 +- 4 files changed, 299 insertions(+), 142 deletions(-) diff --git a/src/editor/components/panel-images-editor.ts b/src/editor/components/panel-images-editor.ts index 2c50862..49b8167 100644 --- a/src/editor/components/panel-images-editor.ts +++ b/src/editor/components/panel-images-editor.ts @@ -5,6 +5,7 @@ import { customElement, property, state } from 'lit/decorators'; import { repeat } from 'lit/directives/repeat.js'; import Sortable from 'sortablejs'; +import './sub-panel-yaml'; import { ICON } from '../../const/const'; import editorcss from '../../css/editor.css'; import { VehicleStatusCardConfig, ImageConfig, HA as HomeAssistant } from '../../types'; @@ -20,6 +21,7 @@ export class PanelImagesEditor extends LitElement { @property({ type: Array }) _images!: ImageConfig[]; @property({ type: Boolean }) isDragging = false; + @state() _yamlEditorActive = false; @state() _newImage: string = ''; @state() _sortable: Sortable | null = null; @state() _reindexImages: boolean = false; @@ -155,7 +157,7 @@ export class PanelImagesEditor extends LitElement { ` : html``; const dropArea = this._renderDropArea(); - + const yamlEditor = this._renderYamlEditor(); const actionMap = [ { title: 'Show Image', icon: 'mdi:eye', action: IMAGE_ACTIONS.SHOW_IMAGE }, { title: 'Delete Image', icon: 'mdi:delete', action: IMAGE_ACTIONS.DELETE }, @@ -207,8 +209,29 @@ export class PanelImagesEditor extends LitElement { const actionFooter = html` `; - return html` ${infoText} ${dropArea} ${imageList} ${actionFooter} `; + return html` ${infoText} ${dropArea} ${imageList} ${yamlEditor} ${actionFooter} `; + } + private _renderYamlEditor(): TemplateResult { + return html` + + `; + } + + private _yamlChanged(ev: CustomEvent): void { + ev.stopPropagation(); + const { isValid, value } = ev.detail; + if (!isValid || !this.config) return; + this.config = { ...this.config, images: value }; + fireEvent(this, 'config-changed', { config: this.config }); } private _renderDropArea(): TemplateResult { @@ -402,6 +425,7 @@ export class PanelImagesEditor extends LitElement { const dropArea = this.shadowRoot?.getElementById('drop-area') as HTMLElement; const imageList = this.shadowRoot?.getElementById('images-list') as HTMLElement; const addImageBtn = this.shadowRoot?.getElementById('upload-btn') as HTMLElement; + const isHidden = dropArea?.style.display === 'none'; if (isHidden) { dropArea.style.display = 'block'; @@ -414,6 +438,25 @@ export class PanelImagesEditor extends LitElement { } }; + const showYamlEditor = () => { + const yamlEditor = this.shadowRoot?.getElementById('yaml-editor') as HTMLElement; + const imageList = this.shadowRoot?.getElementById('images-list') as HTMLElement; + const addImageBtn = this.shadowRoot?.getElementById('upload-btn') as HTMLElement; + const yamlBtn = this.shadowRoot?.getElementById('yaml-btn') as HTMLElement; + const yamlEditorActive = yamlEditor?.style.display === 'block'; + if (!yamlEditorActive) { + yamlEditor.style.display = 'block'; + imageList.style.display = 'none'; + addImageBtn.style.display = 'none'; + yamlBtn.innerHTML = 'Close YAML Editor'; + } else { + yamlEditor.style.display = 'none'; + imageList.style.display = 'block'; + addImageBtn.style.display = 'block'; + yamlBtn.innerHTML = 'Edit YAML'; + } + }; + const handleImageAction = () => { switch (action) { case 'delete': @@ -446,6 +489,9 @@ export class PanelImagesEditor extends LitElement { case 'show-image': this.editor?._dispatchEvent('show-image', { index: idx }); break; + case 'yaml-editor': + showYamlEditor(); + break; } }; handleImageAction(); diff --git a/src/editor/components/panel-indicator.ts b/src/editor/components/panel-indicator.ts index 6de703d..24aa29d 100644 --- a/src/editor/components/panel-indicator.ts +++ b/src/editor/components/panel-indicator.ts @@ -32,6 +32,8 @@ export class PanelIndicator extends LitElement { @state() _reindexItems: boolean = false; @state() _addFormVisible: boolean = false; + @state() _yamlEditorVisible: boolean = false; + constructor() { super(); this._closeSubPanel = this._closeSubPanel.bind(this); @@ -279,6 +281,55 @@ export class PanelIndicator extends LitElement { /* ----------------------------- TEMPLATE UI ----------------------------- */ + private _renderYamlEditor(): TemplateResult { + if (!this._yamlEditorVisible) { + return html``; + } + const singleIndicators = this.config.indicators?.single || []; + const groupIndicators = this.config.indicators?.group || []; + const yamlConfig = { + single: singleIndicators, + group: groupIndicators, + }; + const header = html` +
+
(this._yamlEditorVisible = false)}> + + Close editor +
+
YAML editor
+
+ `; + return html` ${header} +
+ +
`; + } + + private _handleYamlConfigChange(ev: CustomEvent): void { + ev.stopPropagation(); + const detail = ev.detail; + const { key, value, isValid } = detail; + if (!isValid || !this.config) { + return; + } + if (key === 'single') { + this.config = { ...this.config, indicators: { ...this.config.indicators, single: value } }; + } else if (key === 'group') { + this.config = { ...this.config, indicators: { ...this.config.indicators, group: value } }; + } + fireEvent(this, 'config-changed', { config: this.config }); + console.log('YAML config changed:', key, value); + } + private _renderAddTemplate(type: string): TemplateResult { if (!this._addFormVisible) { return html``; @@ -398,6 +449,9 @@ export class PanelIndicator extends LitElement { if (type === 'single') { const singleIndicators: IndicatorConfig[] = this.config.indicators?.single || []; + if (this._yamlEditorVisible) { + return this._renderYamlEditor(); + } return this._renderIndicatorContent( singleIndicators, (single: IndicatorConfig) => single.entity, @@ -416,6 +470,9 @@ export class PanelIndicator extends LitElement { ); } else if (type === 'group') { const groupIndicators: IndicatorGroupConfig[] = this.config.indicators?.group || []; + if (this._yamlEditorVisible) { + return this._renderYamlEditor(); + } return this._renderIndicatorContent( groupIndicators, (group: IndicatorGroupConfig) => group.name, @@ -494,6 +551,8 @@ export class PanelIndicator extends LitElement { (this._addFormVisible = !this._addFormVisible)} >${this._addFormVisible ? 'Cancel' : `Add new ${this.type}`} + (this._yamlEditorVisible = !this._yamlEditorVisible)}>Edit YAML + ${this._renderAddTemplate(this.type)} `; diff --git a/src/editor/components/panel-range-info.ts b/src/editor/components/panel-range-info.ts index b11737b..e9a2d8f 100644 --- a/src/editor/components/panel-range-info.ts +++ b/src/editor/components/panel-range-info.ts @@ -3,12 +3,15 @@ import { fireEvent } from 'custom-card-helpers'; import { LitElement, html, TemplateResult, CSSResultGroup, nothing } from 'lit'; import { customElement, property, state } from 'lit/decorators'; import { repeat } from 'lit/directives/repeat.js'; +// utils +import tinycolor from 'tinycolor2'; import { ICON } from '../../const/const'; import editorcss from '../../css/editor.css'; import { HA as HomeAssistant, VehicleStatusCardConfig, RangeInfoConfig, RangeItemConfig } from '../../types'; import * as Create from '../../utils/create'; import { RANGE_ACTIONS } from '../editor-const'; +import './sub-panel-yaml'; @customElement('panel-range-info') export class PanelRangeInfo extends LitElement { @@ -21,6 +24,13 @@ export class PanelRangeInfo extends LitElement { @state() private _newColor: string = ''; @state() private _picker!: iro.ColorPicker; + @state() private _yamlEditorActive = false; + @state() _colorChangeTimeout?: number = undefined; + + constructor() { + super(); + } + static get styles(): CSSResultGroup { return [editorcss]; } @@ -36,20 +46,10 @@ export class PanelRangeInfo extends LitElement { console.log('Picker element not found'); return; } - let displayFormat = 'hex'; - - const testFormat = (colorValue: string): string => { - if (colorValue.includes('#')) { - return 'hex'; - } - if (colorValue.includes('rgb')) { - return 'rgb'; - } - if (colorValue.includes('hsl')) { - return 'hsl'; - } - return 'hex'; - }; + const inputColor = tinycolor(currentColor); + const format = inputColor.getFormat(); + console.log('Current color', currentColor, 'input color', inputColor, 'format', format); + const displayFormat = format === 'hex' ? 'hex' : format === 'rgb' ? 'rgb' : 'hsl'; // Initialize the iro color picker this._picker = iro.ColorPicker(pickerElement, { @@ -64,29 +64,35 @@ export class PanelRangeInfo extends LitElement { const hexInput = this.shadowRoot?.getElementById('hexInput') as HTMLInputElement; const values = this.shadowRoot?.getElementById('values') as HTMLElement; - // Determine the display format based on the current color - if (hexInput) { - const trimmedValue = hexInput.value.trim(); - displayFormat = testFormat(trimmedValue); - } - // Add an event listener to listen to color changes this._picker.on(['color:init', 'color:change'], (color: any) => { values.innerHTML = ['hex: ' + color.hexString, 'rgb: ' + color.rgbString, 'hsl: ' + color.hslString].join('
'); // Set the new color value if (hexInput) { - if (displayFormat === 'hex') { - hexInput.value = color.hexString; - } - if (displayFormat === 'rgb') { - hexInput.value = color.rgbString; - } - if (displayFormat === 'hsl') { - hexInput.value = color.hslString; + switch (displayFormat) { + case 'hex': + hexInput.value = color.hexString; + break; + case 'rgb': + hexInput.value = color.rgbString; + break; + case 'hsl': + hexInput.value = color.hslString; + break; } this._newColor = hexInput.value; } + + // Clear the color change timeout + if (this._colorChangeTimeout !== undefined) { + clearTimeout(this._colorChangeTimeout); + } + + // Set a new color change timeout + this._colorChangeTimeout = window.setTimeout(() => { + this._handleColorOnChange(this._newColor); + }, 1000); }); // Add an event listener to the input field to allow manual changes @@ -94,12 +100,27 @@ export class PanelRangeInfo extends LitElement { console.log('Input changed', hexInput.value, 'display format', displayFormat); const trimmedValue = hexInput.value.trim(); console.log('Input changed', trimmedValue); - displayFormat = testFormat(trimmedValue); // Set the new color value this._picker.color.set(trimmedValue); }); } + + private _handleColorOnChange(color: string): void { + if (!color) { + return; + } + const index = this._activeColorPicker; + if (index === null) { + return; + } + const rangeInfo = [...(this.config.range_info || [])]; + const rangeInfoItem = { ...rangeInfo[index] }; + rangeInfoItem.progress_color = color; + rangeInfo[index] = rangeInfoItem; + fireEvent(this, 'config-changed', { config: { ...this.config, range_info: rangeInfo } }); + } + protected render(): TemplateResult { return html`
@@ -108,40 +129,50 @@ export class PanelRangeInfo extends LitElement { `; } - private _toggleAction(action?: 'add' | 'delete-item' | 'edit-item', index?: number): () => void { + private _toggleAction(action: 'add' | 'delete-item' | 'edit-item', index?: number): () => void { return () => { const updateChanged = (update: any) => { fireEvent(this, 'config-changed', { config: { ...this.config, range_info: update } }); }; + const hideAllDeleteButtons = () => { const deleteButtons = this.shadowRoot?.querySelectorAll('.card-actions'); deleteButtons?.forEach((button) => { button.classList.add('hidden'); }); }; - - if (action === 'add') { - hideAllDeleteButtons(); - const rangeInfo = [...(this.config.range_info || [])]; - const newRangeInfo = { - energy_level: [{ entity: '', attribute: '', icon: '' }], - range_level: [{ entity: '', attribute: '' }], - progress_color: '', - }; - rangeInfo.push(newRangeInfo); - updateChanged(rangeInfo); - } - if (action === 'delete-item' && index !== undefined) { - const rangeInfo = [...(this.config.range_info || [])]; - rangeInfo.splice(index, 1); - updateChanged(rangeInfo); - } - - if (action === 'edit-item' && index !== undefined) { - console.log('Edit item', index); - this._activeIndexItem = index; - this.requestUpdate(); - } + console.log('Action', action, 'Index', index); + const handleAction = () => { + switch (action) { + case 'add': + hideAllDeleteButtons(); + const rangeInfo = [...(this.config.range_info || [])]; + const newRangeInfo = { + energy_level: [{ entity: '', attribute: '', icon: '' }], + range_level: [{ entity: '', attribute: '' }], + progress_color: '', + }; + rangeInfo.push(newRangeInfo); + updateChanged(rangeInfo); + break; + case 'delete-item': + if (index !== undefined) { + const rangeInfo = [...(this.config.range_info || [])]; + rangeInfo.splice(index, 1); + updateChanged(rangeInfo); + } + break; + + case 'edit-item': + if (index !== undefined) { + console.log('Edit item', index); + this._activeIndexItem = index; + this.requestUpdate(); + } + break; + } + }; + handleAction(); }; } @@ -213,62 +244,100 @@ export class PanelRangeInfo extends LitElement { color: 'var(--error-color)', }, ]; - return html` -
- ${repeat(this.config.range_info, (rangeItem: RangeInfoConfig, index: number) => { - const entity = rangeItem.energy_level.map((item) => item.entity).join(', '); - const icon = rangeItem.energy_level.map((item) => item.icon).join(', '); - const progressColor = rangeItem.progress_color; - return html` -
-
- -
-
-
Range Info #${index + 1}
-
${entity}
-
-
- ev.stopPropagation()} - > - - ${actionMap.map( - (action) => html` - + ${repeat(this.config.range_info, (rangeItem: RangeInfoConfig, index: number) => { + const entity = rangeItem.energy_level.map((item) => item.entity).join(', '); + const icon = rangeItem.energy_level.map((item) => item.icon).join(', '); + const progressColor = rangeItem.progress_color; + return html` +
+
+ +
+
+
Range Info #${index + 1}
+
${entity}
+
+
+ ev.stopPropagation()} > - - ${action.title} - - ` - )} - -
+ + ${actionMap.map( + (action) => html` + + + ${action.title} + + ` + )} + +
+
+ `; + })}
- `; - })} -
+ ` + : this._renderYamlEditor() + }
`; } + private _renderYamlEditor(): TemplateResult | typeof nothing { + if (!this._yamlEditorActive) { + return nothing; + } + + return html` +
+ +
+ `; + } + + private _onYamlConfigChanged(ev: CustomEvent): void { + ev.stopPropagation(); + const detail = ev.detail; + const { isValid, value } = detail; + if (!isValid) { + return; + } + const rangeInfo = [...value]; + const updates: Partial = { range_info: rangeInfo }; + fireEvent(this, 'config-changed', { config: { ...this.config, ...updates } }); + } + private _toggleHelp(ev: Event): void { const target = ev.target as HTMLElement; const alert = target.parentElement?.querySelector('.info-alert') as HTMLElement; @@ -283,9 +352,10 @@ export class PanelRangeInfo extends LitElement { }; const energyEntry = this.config.range_info[index].energy_level[0] as RangeItemConfig; - const energyEntity = energyEntry?.entity; - const energyAttribute = energyEntry?.attribute; - const energyIcon = energyEntry?.icon || ''; + const energyEntity = energyEntry.entity || ''; + const energyIcon = energyEntry.icon || ''; + + const energyAttribute = energyEntry.attribute || ''; const entityAttrs = energyEntity ? Object.keys(this.hass.states[energyEntity].attributes) : []; const attrOpts = [...entityAttrs.map((attr) => ({ value: attr, label: attr }))]; @@ -359,9 +429,9 @@ export class PanelRangeInfo extends LitElement { this._valueChanged(ev)} + @change=${this._valueChanged} >
= {}; // Fetch the current range_info array - const rangeInfo = [...(this.config.range_info || [])]; - const rangeInfoItem = { ...rangeInfo[index] }; // Clone the item at the specific index + let rangeInfo = [...(this.config.range_info || [])]; + let rangeInfoItem = { ...rangeInfo[index] }; // Clone the item at the specific index if (configType === 'progress_color') { // Directly update progress_color since it's a simple string value @@ -450,37 +522,17 @@ export class PanelRangeInfo extends LitElement { updates.range_info = rangeInfo; // Apply the updates console.log(`Range info [${index}] progress_color changed to`, newValue); console.log('Updates', updates); - - // Trigger an update or fire an event to apply the changes - fireEvent(this, 'config-changed', { config: { ...this.config, ...updates } }); - return; // No need to proceed further for progress_color + } else if (configType === 'energy_level' || configType === 'range_level') { + // Update the entity or attribute value + const rangeLevel = rangeInfoItem[configType][0]; + rangeLevel[configValue] = newValue; + rangeInfoItem[configType][0] = rangeLevel; + rangeInfo[index] = rangeInfoItem; + + updates.range_info = rangeInfo; } - // Clone the nested configType object (e.g., energy_level or range_level) if it's not progress_color - const rangeInfoConfig = [...(rangeInfoItem[configType] || [])]; - - // Check if the value actually changed - if (rangeInfoConfig[0][configValue] === newValue) { - // console.log('Value not changed'); - return; - } - - // Update the nested config type (e.g., energy_level or range_level) - rangeInfoConfig[0] = { ...rangeInfoConfig[0], [configValue]: newValue }; - - // Update the specific config type (e.g., energy_level or range_level) in the item - rangeInfoItem[configType] = rangeInfoConfig; - - // Replace the modified item in the range_info array - rangeInfo[index] = rangeInfoItem; - - // Apply the updates - updates.range_info = rangeInfo; - - console.log(`Range info [${index}] ${configType} ${configValue} changed to`, newValue); - console.log('Updates', updates); - - // Trigger an update or fire an event to apply the changes + // Send the updated config to the main card fireEvent(this, 'config-changed', { config: { ...this.config, ...updates } }); } } diff --git a/src/editor/editor-const.ts b/src/editor/editor-const.ts index a9d7d74..66c32f0 100644 --- a/src/editor/editor-const.ts +++ b/src/editor/editor-const.ts @@ -137,7 +137,7 @@ export enum ACTIONS { UNHIDE_BUTTON = 'unhide-button', } -type IMAGE_CONFIG_ACTIONS = 'add' | 'showDelete' | 'delete' | 'upload' | 'add-new-url' | 'show-image'; +type IMAGE_CONFIG_ACTIONS = 'add' | 'showDelete' | 'delete' | 'upload' | 'add-new-url' | 'show-image' | 'yaml-editor'; export enum IMAGE_ACTIONS { ADD = 'add',