From a1a9ab4fe4cbcecf9c4a09d3c52c42ed06f97d71 Mon Sep 17 00:00:00 2001 From: Jeff Brown Date: Sat, 16 Mar 2024 14:14:21 -0700 Subject: [PATCH] Improve polymorphism, fully support cards, elements, and entity rows. Represent each type as an abstract thing instead of pretending they are all cards. Choose the correct editor for each type (when one is available). Decluttering cards can now be safely embedded within entities cards as entity row and within picture-elements cards as elements. --- src/decluttering-card.ts | 242 +++++++++++++++++++++++++++++---------- src/deep-replace.ts | 11 +- src/types.ts | 29 ++++- 3 files changed, 215 insertions(+), 67 deletions(-) diff --git a/src/decluttering-card.ts b/src/decluttering-card.ts index 961debb..cd3f39c 100644 --- a/src/decluttering-card.ts +++ b/src/decluttering-card.ts @@ -3,12 +3,19 @@ import { HomeAssistant, createThing, fireEvent, - LovelaceCardConfig, LovelaceCard, LovelaceCardEditor, LovelaceConfig, } from 'custom-card-helpers'; -import { DeclutteringCardConfig, DeclutteringTemplateConfig, TemplateConfig, VariablesConfig } from './types'; +import { + DeclutteringCardConfig, + DeclutteringTemplateConfig, + TemplateConfig, + VariablesConfig, + LovelaceThing, + LovelaceThingConfig, + LovelaceThingType, +} from './types'; import deepReplace from './deep-replace'; import { getLovelaceConfig } from './utils'; import { ResizeObserver } from 'resize-observer'; @@ -23,7 +30,7 @@ console.info( 'color: white; font-weight: bold; background: dimgray', ); -async function loadCardPicker(): Promise { +async function loadCardEditorPicker(): Promise { // Ensure hui-card-element-editor and hui-card-picker are loaded. // They happen to be used by the vertical-stack card editor but there must be a better way? let cls = customElements.get('hui-vertical-stack-card'); @@ -32,7 +39,22 @@ async function loadCardPicker(): Promise { await customElements.whenDefined('hui-vertical-stack-card'); cls = customElements.get('hui-vertical-stack-card'); } - if (cls) await cls.prototype.constructor.getConfigElement(); + if (cls) cls = cls.prototype.constructor; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (cls && (cls as any).getConfigElement) await (cls as any).getConfigElement(); +} + +async function loadRowEditor(): Promise { + // Ensure hui-row-element-editor are loaded. + // They happen to be used by the vertical-stack card editor but there must be a better way? + let cls = customElements.get('hui-entities-card'); + if (!cls) { + (await HELPERS).createCardElement({ type: 'entities', entities: [] }); + await customElements.whenDefined('hui-entities-card'); + cls = customElements.get('hui-entities-card'); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (cls && (cls as any).getConfigElement) await (cls as any).getConfigElement(); } function getTemplateConfig(ll: LovelaceConfig, template: string): TemplateConfig | null { @@ -102,18 +124,24 @@ function getTemplates(ll: LovelaceConfig): Record { return templates; } -class DeclutteringElement extends LitElement { +function getThingType(templateConfig: TemplateConfig): LovelaceThingType | undefined { + const thingTypes = Object.keys(templateConfig).filter(key => ['card', 'element', 'row'].includes(key)); + return thingTypes.length === 1 ? (thingTypes[0] as LovelaceThingType) : undefined; +} + +abstract class DeclutteringElement extends LitElement { @state() private _hass?: HomeAssistant; - @state() private _card?: LovelaceCard; + @state() private _thing?: LovelaceThing; - private _config?: LovelaceCardConfig; + private _thingConfig?: LovelaceThingConfig; + private _thingType?: LovelaceThingType; private _ro?: ResizeObserver; private _savedStyles?: Map; set hass(hass: HomeAssistant) { if (!hass) return; this._hass = hass; - if (this._card) this._card.hass = hass; + if (this._thing) this._thing.hass = hass; } static get styles(): CSSResult { @@ -135,7 +163,7 @@ class DeclutteringElement extends LitElement { } protected _displayHidden(): void { - if (this._card?.style.display === 'none') { + if (this._thing?.style.display === 'none') { this.classList.add('child-card-hidden'); } else if (this.classList.contains('child-card-hidden')) { this.classList.remove('child-card-hidden'); @@ -143,21 +171,22 @@ class DeclutteringElement extends LitElement { } protected _setTemplateConfig(templateConfig: TemplateConfig, variables: VariablesConfig[] | undefined): void { - if (!(templateConfig.card || templateConfig.element)) { - throw new Error('You should define either a card or an element in the template'); - } else if (templateConfig.card && templateConfig.element) { - throw new Error('You cannnot define both a card and an element in the template'); + const thingType = getThingType(templateConfig); + if (!thingType) { + throw new Error('You must define one card, element, or row in the template'); } + const thingConfig = deepReplace(variables, templateConfig); - const type = templateConfig.card ? 'card' : 'element'; - const config = deepReplace(variables, templateConfig); - this._config = config; - DeclutteringElement._createCard(config, type, (card: LovelaceCard) => { - if (this._config === config) this._setCard(card, templateConfig.element ? config.style : undefined); + this._thingConfig = thingConfig; + this._thingType = thingType; + DeclutteringElement._createThing(thingConfig, thingType, (thing: LovelaceThing) => { + if (this._thingConfig === thingConfig) { + this._setThing(thing, thingType === 'element' ? thingConfig.style : undefined); + } }); } - private _setCard(card: LovelaceCard, style?: Record): void { + private _setThing(thing: LovelaceThing, style?: Record): void { this._savedStyles?.forEach((v, k) => this.style.setProperty(k, v[0], v[1])); this._savedStyles = undefined; @@ -169,56 +198,58 @@ class DeclutteringElement extends LitElement { }); } - this._card = card; - if (this._hass) card.hass = this._hass; + this._thing = thing; + if (this._hass) thing.hass = this._hass; this._ro = new ResizeObserver(() => { this._displayHidden(); }); - this._ro.observe(card); + this._ro.observe(thing); } protected render(): TemplateResult | void { - if (!this._hass || !this._card) return html``; + if (!this._hass || !this._thing) return html``; return html` -
${this._card}
+
${this._thing}
`; } - private static async _createCard( - config: LovelaceCardConfig, - type: 'element' | 'card', - handler: (card: LovelaceCard) => void, + private static async _createThing( + thingConfig: LovelaceThingConfig, + thingType: LovelaceThingType, + handler: (thing: LovelaceThing) => void, ): Promise { - let element: LovelaceCard; + let thing: LovelaceThing; if (HELPERS) { - if (type === 'card') { - if (config.type === 'divider') element = (await HELPERS).createRowElement(config); - else element = (await HELPERS).createCardElement(config); - // fireEvent(element, 'll-rebuild'); + if (thingType === 'card') { + if (thingConfig.type === 'divider') thing = (await HELPERS).createRowElement(thingConfig); + else thing = (await HELPERS).createCardElement(thingConfig); + } else if (thingType === 'element') { + thing = (await HELPERS).createHuiElement(thingConfig); } else { - element = (await HELPERS).createHuiElement(config); + thing = (await HELPERS).createRowElement(thingConfig); } } else { - element = createThing(config); + thing = createThing(thingConfig, thingType === 'row'); } - element.addEventListener( + thing.addEventListener( 'll-rebuild', ev => { ev.stopPropagation(); - DeclutteringElement._createCard(config, type, (card: LovelaceCard) => { - element.replaceWith(card); - handler(card); + DeclutteringElement._createThing(thingConfig, thingType, (newThing: LovelaceThing) => { + thing.replaceWith(newThing); + handler(newThing); }); }, { once: true }, ); - element.id = 'declutter-child'; - handler(element); + thing.id = 'declutter-child'; + handler(thing); } + // for LovelaceCard public getCardSize(): Promise | number { - return this._card && typeof this._card.getCardSize === 'function' ? this._card.getCardSize() : 1; + return this._thing && this._thingType === 'card' ? (this._thing as LovelaceCard).getCardSize() : 1; } } @@ -275,7 +306,6 @@ class DeclutteringCardEditor extends LitElement implements LovelaceCardEditor { private _templates?: Record; // eslint-disable-next-line @typescript-eslint/no-explicit-any private _schema: any; - private _loadedElements = false; set lovelace(lovelace: LovelaceConfig) { this._lovelace = lovelace; @@ -288,7 +318,13 @@ class DeclutteringCardEditor extends LitElement implements LovelaceCardEditor { } protected render(): TemplateResult | void { - if (!this.hass || !this._lovelace || !this._config) return html``; + if (!this.hass || !this._config) return html``; + + if (!this._lovelace) { + // The lovelace property is not set when editing row elements so we retrieve it here + this._lovelace = getLovelaceConfig() ?? undefined; + if (!this._lovelace) return; + } if (!this._templates) this._templates = getTemplates(this._lovelace); if (!this._schema) { @@ -419,7 +455,7 @@ class DeclutteringTemplate extends DeclutteringElement { // eslint-disable-next-line @typescript-eslint/no-unused-vars class DeclutteringTemplateEditor extends LitElement implements LovelaceCardEditor { @state() private _config?: DeclutteringTemplateConfig; - @state() private _selectedTab = 0; + @state() private _selectedTab = 'settings'; @property() public lovelace?: LovelaceConfig; @property() public hass?: HomeAssistant; @@ -432,6 +468,20 @@ class DeclutteringTemplateEditor extends LitElement implements LovelaceCardEdito label: 'Template to define', selector: { text: {} }, }, + { + name: 'thingType', + label: 'Type of thing to template', + selector: { + select: { + mode: 'dropdown', + options: [ + { value: 'card', label: 'Card' }, + { value: 'element', label: 'Element' }, + { value: 'row', label: 'Row' }, + ], + }, + }, + }, { name: 'default', label: 'Variables', @@ -465,7 +515,8 @@ class DeclutteringTemplateEditor extends LitElement implements LovelaceCardEdito super.connectedCallback(); if (!this._loadedElements) { - await loadCardPicker(); + await loadCardEditorPicker(); + await loadRowEditor(); this._loadedElements = true; } } @@ -478,19 +529,39 @@ class DeclutteringTemplateEditor extends LitElement implements LovelaceCardEdito error.default = 'The list of variables must be an array of key and value pairs'; } + const data = { + template: this._config.template, + thingType: getThingType(this._config) ?? 'card', + default: this._config.default, + }; + return html`
- - Settings - Card - Change Card Type + + Settings + ${data.thingType === 'card' + ? html` + Card + Change Card Type + ` + : data.thingType === 'row' + ? html` + Row + ` + : html``}
- ${this._selectedTab === 0 + ${this._selectedTab === 'settings' ? html` s.label ?? s.name} @@ -498,7 +569,7 @@ class DeclutteringTemplateEditor extends LitElement implements LovelaceCardEdito @value-changed=${this._valueChanged} > ` - : this._selectedTab == 1 + : this._selectedTab === 'card' ? html` ` - : html` + : this._selectedTab === 'change_card' + ? html` - `} + ` + : this._selectedTab === 'row' + ? html` + + ` + : html``} `; } + private _activateTab(ev: CustomEvent): void { + this._selectedTab = ev.detail.selected; + } + private _valueChanged(ev: CustomEvent): void { - fireEvent(this, 'config-changed', { config: ev.detail.value }); + if (!this._config) return; + const data = ev.detail.value; + + this._config.template = data.template; + DeclutteringTemplateEditor.stubMember(data.thingType === 'card', this._config, 'card', { + type: 'entity', + entity: 'sun.sun', + }); + DeclutteringTemplateEditor.stubMember(data.thingType === 'element', this._config, 'element', { + type: 'icon', + icon: 'mdi:weather-sunny', + style: { + color: 'yellow', + }, + }); + DeclutteringTemplateEditor.stubMember(data.thingType === 'row', this._config, 'row', { + entity: 'sun.sun', + }); + this._config.default = data.default; + this._fireConfigChanged(); } private _cardChanged(ev: CustomEvent): void { @@ -526,15 +631,32 @@ class DeclutteringTemplateEditor extends LitElement implements LovelaceCardEdito if (!this._config) return; this._config.card = ev.detail.config; - fireEvent(this, 'config-changed', { config: this._config }); + this._fireConfigChanged(); } private _cardPicked(ev: CustomEvent): void { - this._selectedTab = 1; + this._selectedTab = 'card'; this._cardChanged(ev); } - private _activateTab(ev: CustomEvent): void { - this._selectedTab = parseInt(ev.detail.selected); + private _rowChanged(ev: CustomEvent): void { + ev.stopPropagation(); + if (!this._config) return; + + this._config.row = ev.detail.config; + this._fireConfigChanged(); + } + + private _fireConfigChanged(): void { + fireEvent(this, 'config-changed', { config: this._config }); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private static stubMember(include: boolean, dict: any, name: string, stub: any): void { + if (include) { + if (!(name in dict)) dict[name] = stub; + } else { + delete dict[name]; + } } } diff --git a/src/deep-replace.ts b/src/deep-replace.ts index 0f6bfeb..eb8a649 100644 --- a/src/deep-replace.ts +++ b/src/deep-replace.ts @@ -1,10 +1,9 @@ -import { VariablesConfig, TemplateConfig } from './types'; -import { LovelaceCardConfig } from 'custom-card-helpers'; +import { VariablesConfig, TemplateConfig, LovelaceThingConfig } from './types'; -export default (variables: VariablesConfig[] | undefined, templateConfig: TemplateConfig): LovelaceCardConfig => { - const cardOrElement = templateConfig.card ?? templateConfig.element; +export default (variables: VariablesConfig[] | undefined, templateConfig: TemplateConfig): LovelaceThingConfig => { + const content = templateConfig.card ?? templateConfig.element ?? templateConfig.row; if (!variables && !templateConfig.default) { - return cardOrElement; + return content; } let variableArray: VariablesConfig[] = []; if (variables) { @@ -13,7 +12,7 @@ export default (variables: VariablesConfig[] | undefined, templateConfig: Templa if (templateConfig.default) { variableArray = variableArray.concat(templateConfig.default); } - let jsonConfig = JSON.stringify(cardOrElement); + let jsonConfig = JSON.stringify(content); variableArray.forEach(variable => { const key = Object.keys(variable)[0]; const value = Object.values(variable)[0]; diff --git a/src/types.ts b/src/types.ts index 2428f7f..f3852df 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import { LovelaceCardConfig } from 'custom-card-helpers'; +import { HomeAssistant, LovelaceCard, LovelaceCardConfig } from 'custom-card-helpers'; /* eslint-disable @typescript-eslint/no-explicit-any */ export interface DeclutteringCardConfig extends LovelaceCardConfig { @@ -18,4 +18,31 @@ export interface TemplateConfig { default?: VariablesConfig[]; card?: any; element?: any; + row?: any; } + +export interface LovelaceElement extends HTMLElement { + hass?: HomeAssistant; + setConfig(config: LovelaceElementConfig): void; +} + +export interface LovelaceElementConfig { + type: string; + style: Record; + [key: string]: any; +} + +export interface LovelaceRow extends HTMLElement { + hass?: HomeAssistant; + editMode?: boolean; + setConfig(config: LovelaceRowConfig); +} + +export interface LovelaceRowConfig { + type?: string; + [key: string]: any; +} + +export type LovelaceThing = LovelaceCard | LovelaceElement | LovelaceRow; +export type LovelaceThingConfig = LovelaceCardConfig | LovelaceElementConfig | LovelaceRowConfig; +export type LovelaceThingType = 'card' | 'element' | 'row';