diff --git a/src/decluttering-card.ts b/src/decluttering-card.ts index 961debb..e66a792 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', 'row', 'element'].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,60 @@ 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 === 'row') { + thing = (await HELPERS).createRowElement(thingConfig); + } else if (thingType === 'element') { + thing = (await HELPERS).createHuiElement(thingConfig); } else { - element = (await HELPERS).createHuiElement(config); + throw new Error(`Unsupported thing type '${thingType}'`); } } 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 +308,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 +320,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 +457,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 +470,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: 'row', label: 'Row' }, + { value: 'element', label: 'Element' }, + ], + }, + }, + }, { name: 'default', label: 'Variables', @@ -465,7 +517,8 @@ class DeclutteringTemplateEditor extends LitElement implements LovelaceCardEdito super.connectedCallback(); if (!this._loadedElements) { - await loadCardPicker(); + await loadCardEditorPicker(); + await loadRowEditor(); this._loadedElements = true; } } @@ -478,19 +531,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 +571,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 === 'row', this._config, 'row', { + entity: 'sun.sun', + }); + DeclutteringTemplateEditor.stubMember(data.thingType === 'element', this._config, 'element', { + type: 'icon', + icon: 'mdi:weather-sunny', + style: { + color: 'yellow', + }, + }); + this._config.default = data.default; + this._fireConfigChanged(); } private _cardChanged(ev: CustomEvent): void { @@ -526,15 +633,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..e76c9b9 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 { @@ -17,5 +17,32 @@ export interface VariablesConfig { export interface TemplateConfig { default?: VariablesConfig[]; card?: any; + row?: any; element?: 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' | 'row' | 'element';