From e05c66444a796f52480ad42896cc5a335455efc1 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Wed, 31 Jul 2024 02:16:57 -0700 Subject: [PATCH] Picture Elements Visual editor (#19718) * preliminary edits * more functional prototype * all types implemented * mostly style and localization updates * fix empty conditional case * Move getConfigElement to elements themselves * drop unneeded imports * move struct validation to individual element editors * description for unknown types * Update src/panels/lovelace/editor/config-elements/elements/hui-service-button-element-editor.ts Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com> * Update src/panels/lovelace/editor/config-elements/elements/hui-state-icon-element-editor.ts Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com> * Update src/panels/lovelace/editor/config-elements/elements/hui-service-button-element-editor.ts Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com> * Update src/panels/lovelace/editor/config-elements/elements/hui-state-badge-element-editor.ts Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com> * Update hui-picture-elements-card-row-editor.ts * Fix merge mistake * Update src/panels/lovelace/create-element/create-picture-element.ts remove comment Co-authored-by: Bram Kragten --------- Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com> Co-authored-by: Bram Kragten --- .../ha-selector/ha-selector-boolean.ts | 4 +- .../cards/hui-picture-elements-card.ts | 7 +- .../create-element/create-element-base.ts | 3 +- .../create-element/create-picture-element.ts | 42 +++ .../hui-conditional-element-editor.ts | 167 ++++++++++++ .../elements/hui-icon-element-editor.ts | 88 ++++++ .../elements/hui-image-element-editor.ts | 103 +++++++ .../hui-service-button-element-editor.ts | 79 ++++++ .../hui-state-badge-element-editor.ts | 86 ++++++ .../elements/hui-state-icon-element-editor.ts | 98 +++++++ .../hui-state-label-element-editor.ts | 98 +++++++ .../hui-picture-elements-card-editor.ts | 214 +++++++++++++++ .../lovelace/editor/hui-element-editor.ts | 2 + .../hui-picture-elements-card-row-editor.ts | 255 ++++++++++++++++++ .../lovelace/editor/hui-sub-element-editor.ts | 23 +- .../hui-picture-element-element-editor.ts | 31 +++ src/panels/lovelace/editor/types.ts | 6 +- .../elements/hui-conditional-element.ts | 8 + .../lovelace/elements/hui-icon-element.ts | 6 + .../lovelace/elements/hui-image-element.ts | 6 + .../elements/hui-service-button-element.ts | 8 + .../elements/hui-state-badge-element.ts | 10 +- .../elements/hui-state-icon-element.ts | 10 +- .../elements/hui-state-label-element.ts | 10 +- src/panels/lovelace/elements/types.ts | 13 +- src/panels/lovelace/types.ts | 11 + src/translations/en.json | 28 +- 27 files changed, 1400 insertions(+), 16 deletions(-) create mode 100644 src/panels/lovelace/create-element/create-picture-element.ts create mode 100644 src/panels/lovelace/editor/config-elements/elements/hui-conditional-element-editor.ts create mode 100644 src/panels/lovelace/editor/config-elements/elements/hui-icon-element-editor.ts create mode 100644 src/panels/lovelace/editor/config-elements/elements/hui-image-element-editor.ts create mode 100644 src/panels/lovelace/editor/config-elements/elements/hui-service-button-element-editor.ts create mode 100644 src/panels/lovelace/editor/config-elements/elements/hui-state-badge-element-editor.ts create mode 100644 src/panels/lovelace/editor/config-elements/elements/hui-state-icon-element-editor.ts create mode 100644 src/panels/lovelace/editor/config-elements/elements/hui-state-label-element-editor.ts create mode 100644 src/panels/lovelace/editor/config-elements/hui-picture-elements-card-editor.ts create mode 100644 src/panels/lovelace/editor/hui-picture-elements-card-row-editor.ts create mode 100644 src/panels/lovelace/editor/picture-element-editor/hui-picture-element-element-editor.ts diff --git a/src/components/ha-selector/ha-selector-boolean.ts b/src/components/ha-selector/ha-selector-boolean.ts index a6e0584f07..18ce85b912 100644 --- a/src/components/ha-selector/ha-selector-boolean.ts +++ b/src/components/ha-selector/ha-selector-boolean.ts @@ -12,6 +12,8 @@ export class HaBooleanSelector extends LitElement { @property({ type: Boolean }) public value = false; + @property() public placeholder?: any; + @property() public label?: string; @property() public helper?: string; @@ -22,7 +24,7 @@ export class HaBooleanSelector extends LitElement { return html` diff --git a/src/panels/lovelace/cards/hui-picture-elements-card.ts b/src/panels/lovelace/cards/hui-picture-elements-card.ts index 143a2fde58..9ddf4d65e3 100644 --- a/src/panels/lovelace/cards/hui-picture-elements-card.ts +++ b/src/panels/lovelace/cards/hui-picture-elements-card.ts @@ -14,13 +14,18 @@ import { ImageEntity, computeImageUrl } from "../../../data/image"; import { HomeAssistant } from "../../../types"; import { findEntities } from "../common/find-entities"; import { LovelaceElement, LovelaceElementConfig } from "../elements/types"; -import { LovelaceCard } from "../types"; +import { LovelaceCard, LovelaceCardEditor } from "../types"; import { createStyledHuiElement } from "./picture-elements/create-styled-hui-element"; import { PictureElementsCardConfig } from "./types"; import { PersonEntity } from "../../../data/person"; @customElement("hui-picture-elements-card") class HuiPictureElementsCard extends LitElement implements LovelaceCard { + public static async getConfigElement(): Promise { + await import("../editor/config-elements/hui-picture-elements-card-editor"); + return document.createElement("hui-picture-elements-card-editor"); + } + @property({ attribute: false }) public hass?: HomeAssistant; @state() private _elements?: LovelaceElement[]; diff --git a/src/panels/lovelace/create-element/create-element-base.ts b/src/panels/lovelace/create-element/create-element-base.ts index 65f3ecbcc5..33aaecb0af 100644 --- a/src/panels/lovelace/create-element/create-element-base.ts +++ b/src/panels/lovelace/create-element/create-element-base.ts @@ -23,6 +23,7 @@ import { LovelaceCardConstructor, LovelaceCardFeature, LovelaceCardFeatureConstructor, + LovelaceElementConstructor, LovelaceHeaderFooter, LovelaceHeaderFooterConstructor, LovelaceRowConstructor, @@ -44,7 +45,7 @@ interface CreateElementConfigTypes { element: { config: LovelaceElementConfig; element: LovelaceElement; - constructor: unknown; + constructor: LovelaceElementConstructor; }; row: { config: LovelaceRowConfig; diff --git a/src/panels/lovelace/create-element/create-picture-element.ts b/src/panels/lovelace/create-element/create-picture-element.ts new file mode 100644 index 0000000000..ce848b3bd0 --- /dev/null +++ b/src/panels/lovelace/create-element/create-picture-element.ts @@ -0,0 +1,42 @@ +import "../elements/hui-conditional-element"; +import "../elements/hui-icon-element"; +import "../elements/hui-image-element"; +import "../elements/hui-service-button-element"; +import "../elements/hui-state-badge-element"; +import "../elements/hui-state-icon-element"; +import "../elements/hui-state-label-element"; +import { LovelaceElementConfig } from "../elements/types"; +import { + createLovelaceElement, + getLovelaceElementClass, +} from "./create-element-base"; + +const ALWAYS_LOADED_TYPES = new Set([ + "conditional", + "icon", + "image", + "service-button", + "state-badge", + "state-icon", + "state-label", +]); + +const LAZY_LOAD_TYPES = {}; + +export const createPictureElementElement = (config: LovelaceElementConfig) => + createLovelaceElement( + "element", + config, + ALWAYS_LOADED_TYPES, + LAZY_LOAD_TYPES, + undefined, + undefined + ); + +export const getPictureElementClass = (type: string) => + getLovelaceElementClass( + type, + "element", + ALWAYS_LOADED_TYPES, + LAZY_LOAD_TYPES + ); diff --git a/src/panels/lovelace/editor/config-elements/elements/hui-conditional-element-editor.ts b/src/panels/lovelace/editor/config-elements/elements/hui-conditional-element-editor.ts new file mode 100644 index 0000000000..94809079c6 --- /dev/null +++ b/src/panels/lovelace/editor/config-elements/elements/hui-conditional-element-editor.ts @@ -0,0 +1,167 @@ +import { html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { + any, + array, + assert, + literal, + object, + optional, + string, +} from "superstruct"; +import { HASSDomEvent, fireEvent } from "../../../../../common/dom/fire_event"; +import type { HomeAssistant } from "../../../../../types"; +import "../../../../../components/ha-form/ha-form"; +import { LovelacePictureElementEditor } from "../../../types"; +import { + ConditionalElementConfig, + LovelaceElementConfig, +} from "../../../elements/types"; +import "../../conditions/ha-card-conditions-editor"; +import "../../hui-picture-elements-card-row-editor"; +import { LovelaceCardConfig } from "../../../../../data/lovelace/config/card"; +import { EditSubElementEvent, SubElementEditorConfig } from "../../types"; +import "../../hui-sub-element-editor"; +import { SchemaUnion } from "../../../../../components/ha-form/types"; + +const conditionalElementConfigStruct = object({ + type: literal("conditional"), + conditions: optional(array(any())), + elements: optional(array(any())), + title: optional(string()), +}); + +const SCHEMA = [{ name: "title", selector: { text: {} } }] as const; + +@customElement("hui-conditional-element-editor") +export class HuiConditionalElementEditor + extends LitElement + implements LovelacePictureElementEditor +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: ConditionalElementConfig; + + @state() private _subElementEditorConfig?: SubElementEditorConfig; + + public setConfig(config: ConditionalElementConfig): void { + assert(config, conditionalElementConfigStruct); + this._config = config; + } + + protected render() { + if (!this.hass || !this._config) { + return nothing; + } + + if (this._subElementEditorConfig) { + return html` + + + `; + } + + return html` + + + + + `; + } + + private _formChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } + + private _conditionChanged(ev: CustomEvent) { + ev.stopPropagation(); + if (!this._config) { + return; + } + const conditions = ev.detail.value; + this._config = { ...this._config, conditions }; + fireEvent(this, "config-changed", { config: this._config }); + } + + private _elementsChanged(ev: CustomEvent): void { + ev.stopPropagation(); + + const config = { + ...this._config, + elements: ev.detail.elements as LovelaceElementConfig[], + } as LovelaceCardConfig; + + fireEvent(this, "config-changed", { config }); + } + + private _handleSubElementChanged(ev: CustomEvent): void { + ev.stopPropagation(); + if (!this._config || !this.hass) { + return; + } + + const configValue = this._subElementEditorConfig?.type; + const value = ev.detail.config; + + if (configValue === "element") { + const newConfigElements = this._config.elements!.concat(); + if (!value) { + newConfigElements.splice(this._subElementEditorConfig!.index!, 1); + this._goBack(); + } else { + newConfigElements[this._subElementEditorConfig!.index!] = value; + } + + this._config = { ...this._config!, elements: newConfigElements }; + } + + this._subElementEditorConfig = { + ...this._subElementEditorConfig!, + elementConfig: value, + }; + + fireEvent(this, "config-changed", { config: this._config }); + } + + private _editDetailElement(ev: HASSDomEvent): void { + this._subElementEditorConfig = ev.detail.subElementConfig; + } + + private _goBack(ev?): void { + ev?.stopPropagation(); + this._subElementEditorConfig = undefined; + } + + private _computeLabelCallback = (schema: SchemaUnion) => + this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ) || + this.hass!.localize(`ui.panel.lovelace.editor.elements.${schema.name}`) || + schema.name; +} + +declare global { + interface HTMLElementTagNameMap { + "hui-conditional-element-editor": HuiConditionalElementEditor; + } +} diff --git a/src/panels/lovelace/editor/config-elements/elements/hui-icon-element-editor.ts b/src/panels/lovelace/editor/config-elements/elements/hui-icon-element-editor.ts new file mode 100644 index 0000000000..bbf89f7477 --- /dev/null +++ b/src/panels/lovelace/editor/config-elements/elements/hui-icon-element-editor.ts @@ -0,0 +1,88 @@ +import { html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { any, assert, literal, object, optional, string } from "superstruct"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import type { SchemaUnion } from "../../../../../components/ha-form/types"; +import type { HomeAssistant } from "../../../../../types"; +import "../../../../../components/ha-form/ha-form"; +import { LovelacePictureElementEditor } from "../../../types"; +import { IconElementConfig } from "../../../elements/types"; +import { actionConfigStruct } from "../../structs/action-struct"; + +const iconElementConfigStruct = object({ + type: literal("icon"), + entity: optional(string()), + icon: optional(string()), + style: optional(any()), + title: optional(string()), + tap_action: optional(actionConfigStruct), + hold_action: optional(actionConfigStruct), + double_tap_action: optional(actionConfigStruct), +}); + +const SCHEMA = [ + { name: "icon", selector: { icon: {} } }, + { name: "title", selector: { text: {} } }, + { name: "entity", selector: { entity: {} } }, + { + name: "tap_action", + selector: { + ui_action: {}, + }, + }, + { + name: "hold_action", + selector: { + ui_action: {}, + }, + }, + { name: "style", selector: { object: {} } }, +] as const; + +@customElement("hui-icon-element-editor") +export class HuiIconElementEditor + extends LitElement + implements LovelacePictureElementEditor +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: IconElementConfig; + + public setConfig(config: IconElementConfig): void { + assert(config, iconElementConfigStruct); + this._config = config; + } + + protected render() { + if (!this.hass || !this._config) { + return nothing; + } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } + + private _computeLabelCallback = (schema: SchemaUnion) => + this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ) || + this.hass!.localize(`ui.panel.lovelace.editor.elements.${schema.name}`) || + schema.name; +} + +declare global { + interface HTMLElementTagNameMap { + "hui-icon-element-editor": HuiIconElementEditor; + } +} diff --git a/src/panels/lovelace/editor/config-elements/elements/hui-image-element-editor.ts b/src/panels/lovelace/editor/config-elements/elements/hui-image-element-editor.ts new file mode 100644 index 0000000000..06c9ee68d6 --- /dev/null +++ b/src/panels/lovelace/editor/config-elements/elements/hui-image-element-editor.ts @@ -0,0 +1,103 @@ +import { html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { any, assert, literal, object, optional, string } from "superstruct"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import type { SchemaUnion } from "../../../../../components/ha-form/types"; +import type { HomeAssistant } from "../../../../../types"; +import "../../../../../components/ha-form/ha-form"; +import { LovelacePictureElementEditor } from "../../../types"; +import { ImageElementConfig } from "../../../elements/types"; +import { actionConfigStruct } from "../../structs/action-struct"; + +const imageElementConfigStruct = object({ + type: literal("image"), + entity: optional(string()), + image: optional(string()), + style: optional(any()), + title: optional(string()), + tap_action: optional(actionConfigStruct), + hold_action: optional(actionConfigStruct), + double_tap_action: optional(actionConfigStruct), + camera_image: optional(string()), + camera_view: optional(string()), + state_image: optional(any()), + filter: optional(string()), + state_filter: optional(any()), + aspect_ratio: optional(string()), +}); + +const SCHEMA = [ + { name: "entity", selector: { entity: {} } }, + { name: "title", selector: { text: {} } }, + { + name: "tap_action", + selector: { + ui_action: {}, + }, + }, + { + name: "hold_action", + selector: { + ui_action: {}, + }, + }, + { name: "image", selector: { text: {} } }, + { name: "camera_image", selector: { entity: { domain: "camera" } } }, + { + name: "camera_view", + selector: { select: { options: ["auto", "live"] } }, + }, + { name: "state_image", selector: { object: {} } }, + { name: "filter", selector: { text: {} } }, + { name: "state_filter", selector: { object: {} } }, + { name: "aspect_ratio", selector: { text: {} } }, + { name: "style", selector: { object: {} } }, +] as const; + +@customElement("hui-image-element-editor") +export class HuiImageElementEditor + extends LitElement + implements LovelacePictureElementEditor +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: ImageElementConfig; + + public setConfig(config: ImageElementConfig): void { + assert(config, imageElementConfigStruct); + this._config = config; + } + + protected render() { + if (!this.hass || !this._config) { + return nothing; + } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } + + private _computeLabelCallback = (schema: SchemaUnion) => + this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ) || + this.hass!.localize(`ui.panel.lovelace.editor.elements.${schema.name}`) || + schema.name; +} + +declare global { + interface HTMLElementTagNameMap { + "hui-image-element-editor": HuiImageElementEditor; + } +} diff --git a/src/panels/lovelace/editor/config-elements/elements/hui-service-button-element-editor.ts b/src/panels/lovelace/editor/config-elements/elements/hui-service-button-element-editor.ts new file mode 100644 index 0000000000..7f6afd193e --- /dev/null +++ b/src/panels/lovelace/editor/config-elements/elements/hui-service-button-element-editor.ts @@ -0,0 +1,79 @@ +import { html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { any, assert, literal, object, optional, string } from "superstruct"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import type { SchemaUnion } from "../../../../../components/ha-form/types"; +import type { HomeAssistant } from "../../../../../types"; +import "../../../../../components/ha-form/ha-form"; +import { LovelacePictureElementEditor } from "../../../types"; +import { ServiceButtonElementConfig } from "../../../elements/types"; +// import { UiAction } from "../../components/hui-action-editor"; + +const serviceButtonElementConfigStruct = object({ + type: literal("service-button"), + style: optional(any()), + title: optional(string()), + service: optional(string()), + service_data: optional(any()), +}); + +const SCHEMA = [ + { name: "title", required: true, selector: { text: {} } }, + /* { + name: "service", + selector: { + ui_action: { actions: ["call-service"] as UiAction[] }, + }, + }, */ + { name: "service", required: true, selector: { text: {} } }, + { name: "service_data", selector: { object: {} } }, + { name: "style", selector: { object: {} } }, +] as const; + +@customElement("hui-service-button-element-editor") +export class HuiServiceButtonElementEditor + extends LitElement + implements LovelacePictureElementEditor +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: ServiceButtonElementConfig; + + public setConfig(config: ServiceButtonElementConfig): void { + assert(config, serviceButtonElementConfigStruct); + this._config = config; + } + + protected render() { + if (!this.hass || !this._config) { + return nothing; + } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } + + private _computeLabelCallback = (schema: SchemaUnion) => + this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ) || + this.hass!.localize(`ui.panel.lovelace.editor.elements.${schema.name}`) || + schema.name; +} + +declare global { + interface HTMLElementTagNameMap { + "hui-service-button-element-editor": HuiServiceButtonElementEditor; + } +} diff --git a/src/panels/lovelace/editor/config-elements/elements/hui-state-badge-element-editor.ts b/src/panels/lovelace/editor/config-elements/elements/hui-state-badge-element-editor.ts new file mode 100644 index 0000000000..f5cf03d0fb --- /dev/null +++ b/src/panels/lovelace/editor/config-elements/elements/hui-state-badge-element-editor.ts @@ -0,0 +1,86 @@ +import { html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { any, assert, literal, object, optional, string } from "superstruct"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import type { SchemaUnion } from "../../../../../components/ha-form/types"; +import type { HomeAssistant } from "../../../../../types"; +import "../../../../../components/ha-form/ha-form"; +import { LovelacePictureElementEditor } from "../../../types"; +import { StateBadgeElementConfig } from "../../../elements/types"; +import { actionConfigStruct } from "../../structs/action-struct"; + +const stateBadgeElementConfigStruct = object({ + type: literal("state-badge"), + entity: optional(string()), + style: optional(any()), + title: optional(string()), + tap_action: optional(actionConfigStruct), + hold_action: optional(actionConfigStruct), + double_tap_action: optional(actionConfigStruct), +}); + +const SCHEMA = [ + { name: "entity", required: true, selector: { entity: {} } }, + { name: "title", selector: { text: {} } }, + { + name: "tap_action", + selector: { + ui_action: {}, + }, + }, + { + name: "hold_action", + selector: { + ui_action: {}, + }, + }, + { name: "style", selector: { object: {} } }, +] as const; + +@customElement("hui-state-badge-element-editor") +export class HuiStateBadgeElementEditor + extends LitElement + implements LovelacePictureElementEditor +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: StateBadgeElementConfig; + + public setConfig(config: StateBadgeElementConfig): void { + assert(config, stateBadgeElementConfigStruct); + this._config = config; + } + + protected render() { + if (!this.hass || !this._config) { + return nothing; + } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } + + private _computeLabelCallback = (schema: SchemaUnion) => + this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ) || + this.hass!.localize(`ui.panel.lovelace.editor.elements.${schema.name}`) || + schema.name; +} + +declare global { + interface HTMLElementTagNameMap { + "hui-state-badge-element-editor": HuiStateBadgeElementEditor; + } +} diff --git a/src/panels/lovelace/editor/config-elements/elements/hui-state-icon-element-editor.ts b/src/panels/lovelace/editor/config-elements/elements/hui-state-icon-element-editor.ts new file mode 100644 index 0000000000..c5ebb70a19 --- /dev/null +++ b/src/panels/lovelace/editor/config-elements/elements/hui-state-icon-element-editor.ts @@ -0,0 +1,98 @@ +import { html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { + any, + assert, + boolean, + literal, + object, + optional, + string, +} from "superstruct"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import type { SchemaUnion } from "../../../../../components/ha-form/types"; +import type { HomeAssistant } from "../../../../../types"; +import "../../../../../components/ha-form/ha-form"; +import { LovelacePictureElementEditor } from "../../../types"; +import { StateIconElementConfig } from "../../../elements/types"; +import { actionConfigStruct } from "../../structs/action-struct"; + +const stateIconElementConfigStruct = object({ + type: literal("state-icon"), + entity: optional(string()), + icon: optional(string()), + state_color: optional(boolean()), + style: optional(any()), + title: optional(string()), + tap_action: optional(actionConfigStruct), + hold_action: optional(actionConfigStruct), + double_tap_action: optional(actionConfigStruct), +}); + +const SCHEMA = [ + { name: "entity", required: true, selector: { entity: {} } }, + { name: "icon", selector: { icon: {} } }, + { name: "title", selector: { text: {} } }, + { name: "state_color", default: true, selector: { boolean: {} } }, + { + name: "tap_action", + selector: { + ui_action: {}, + }, + }, + { + name: "hold_action", + selector: { + ui_action: {}, + }, + }, + { name: "style", selector: { object: {} } }, +] as const; + +@customElement("hui-state-icon-element-editor") +export class HuiStateIconElementEditor + extends LitElement + implements LovelacePictureElementEditor +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: StateIconElementConfig; + + public setConfig(config: StateIconElementConfig): void { + assert(config, stateIconElementConfigStruct); + this._config = config; + } + + protected render() { + if (!this.hass || !this._config) { + return nothing; + } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } + + private _computeLabelCallback = (schema: SchemaUnion) => + this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ) || + this.hass!.localize(`ui.panel.lovelace.editor.elements.${schema.name}`) || + schema.name; +} + +declare global { + interface HTMLElementTagNameMap { + "hui-state-icon-element-editor": HuiStateIconElementEditor; + } +} diff --git a/src/panels/lovelace/editor/config-elements/elements/hui-state-label-element-editor.ts b/src/panels/lovelace/editor/config-elements/elements/hui-state-label-element-editor.ts new file mode 100644 index 0000000000..af2043801d --- /dev/null +++ b/src/panels/lovelace/editor/config-elements/elements/hui-state-label-element-editor.ts @@ -0,0 +1,98 @@ +import { html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { any, assert, literal, object, optional, string } from "superstruct"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import type { SchemaUnion } from "../../../../../components/ha-form/types"; +import type { HomeAssistant } from "../../../../../types"; +import "../../../../../components/ha-form/ha-form"; +import { LovelacePictureElementEditor } from "../../../types"; +import { StateLabelElementConfig } from "../../../elements/types"; +import { actionConfigStruct } from "../../structs/action-struct"; + +const stateLabelElementConfigStruct = object({ + type: literal("state-label"), + entity: optional(string()), + attribute: optional(string()), + prefix: optional(string()), + suffix: optional(string()), + style: optional(any()), + title: optional(string()), + tap_action: optional(actionConfigStruct), + hold_action: optional(actionConfigStruct), + double_tap_action: optional(actionConfigStruct), +}); + +const SCHEMA = [ + { name: "entity", required: true, selector: { entity: {} } }, + { + name: "attribute", + selector: { attribute: {} }, + context: { + filter_entity: "entity", + }, + }, + { name: "prefix", selector: { text: {} } }, + { name: "suffix", selector: { text: {} } }, + { name: "title", selector: { text: {} } }, + { + name: "tap_action", + selector: { + ui_action: {}, + }, + }, + { + name: "hold_action", + selector: { + ui_action: {}, + }, + }, + { name: "style", selector: { object: {} } }, +] as const; + +@customElement("hui-state-label-element-editor") +export class HuiStateLabelElementEditor + extends LitElement + implements LovelacePictureElementEditor +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: StateLabelElementConfig; + + public setConfig(config: StateLabelElementConfig): void { + assert(config, stateLabelElementConfigStruct); + this._config = config; + } + + protected render() { + if (!this.hass || !this._config) { + return nothing; + } + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } + + private _computeLabelCallback = (schema: SchemaUnion) => + this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ) || + this.hass!.localize(`ui.panel.lovelace.editor.elements.${schema.name}`) || + schema.name; +} + +declare global { + interface HTMLElementTagNameMap { + "hui-state-label-element-editor": HuiStateLabelElementEditor; + } +} diff --git a/src/panels/lovelace/editor/config-elements/hui-picture-elements-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-picture-elements-card-editor.ts new file mode 100644 index 0000000000..329a19ab8e --- /dev/null +++ b/src/panels/lovelace/editor/config-elements/hui-picture-elements-card-editor.ts @@ -0,0 +1,214 @@ +import { CSSResultGroup, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { + any, + array, + assert, + assign, + object, + optional, + string, + type, +} from "superstruct"; +import memoizeOne from "memoize-one"; +import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-card"; +import "../../../../components/ha-form/ha-form"; +import "../../../../components/ha-icon"; +import "../../../../components/ha-switch"; +import type { HomeAssistant } from "../../../../types"; +import type { PictureElementsCardConfig } from "../../cards/types"; +import type { LovelaceCardEditor } from "../../types"; +import "../hui-sub-element-editor"; +import { baseLovelaceCardConfig } from "../structs/base-card-struct"; +import { EditSubElementEvent, SubElementEditorConfig } from "../types"; +import { configElementStyle } from "./config-elements-style"; +import "../hui-picture-elements-card-row-editor"; +import { LovelaceElementConfig } from "../../elements/types"; +import { LovelaceCardConfig } from "../../../../data/lovelace/config/card"; +import { LocalizeFunc } from "../../../../common/translations/localize"; + +const genericElementConfigStruct = type({ + type: string(), +}); + +const cardConfigStruct = assign( + baseLovelaceCardConfig, + object({ + image: optional(string()), + camera_image: optional(string()), + camera_view: optional(string()), + elements: array(genericElementConfigStruct), + title: optional(string()), + state_filter: optional(any()), + theme: optional(string()), + dark_mode_image: optional(string()), + dark_mode_filter: optional(any()), + }) +); + +@customElement("hui-picture-elements-card-editor") +export class HuiPictureElementsCardEditor + extends LitElement + implements LovelaceCardEditor +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: PictureElementsCardConfig; + + @state() private _subElementEditorConfig?: SubElementEditorConfig; + + public setConfig(config: PictureElementsCardConfig): void { + assert(config, cardConfigStruct); + this._config = config; + } + + private _schema = memoizeOne( + (localize: LocalizeFunc) => + [ + { + name: "", + type: "expandable", + title: localize( + "ui.panel.lovelace.editor.card.picture-elements.card_options" + ), + schema: [ + { name: "title", selector: { text: {} } }, + { name: "image", selector: { text: {} } }, + { name: "dark_mode_image", selector: { text: {} } }, + { + name: "camera_image", + selector: { entity: { domain: "camera" } }, + }, + { + name: "camera_view", + selector: { select: { options: ["auto", "live"] } }, + }, + { name: "theme", selector: { theme: {} } }, + { name: "state_filter", selector: { object: {} } }, + { name: "dark_mode_filter", selector: { object: {} } }, + ], + }, + ] as const + ); + + protected render() { + if (!this.hass || !this._config) { + return nothing; + } + + if (this._subElementEditorConfig) { + return html` + + + `; + } + + return html` + + + `; + } + + private _formChanged(ev: CustomEvent): void { + ev.stopPropagation(); + if (!this._config || !this.hass) { + return; + } + + fireEvent(this, "config-changed", { config: ev.detail.value }); + } + + private _elementsChanged(ev: CustomEvent): void { + ev.stopPropagation(); + + const config = { + ...this._config, + elements: ev.detail.elements as LovelaceElementConfig[], + } as LovelaceCardConfig; + + fireEvent(this, "config-changed", { config }); + } + + private _handleSubElementChanged(ev: CustomEvent): void { + ev.stopPropagation(); + if (!this._config || !this.hass) { + return; + } + + const configValue = this._subElementEditorConfig?.type; + const value = ev.detail.config; + + if (configValue === "element") { + const newConfigElements = this._config.elements!.concat(); + if (!value) { + newConfigElements.splice(this._subElementEditorConfig!.index!, 1); + this._goBack(); + } else { + newConfigElements[this._subElementEditorConfig!.index!] = value; + } + + this._config = { ...this._config!, elements: newConfigElements }; + } + + this._subElementEditorConfig = { + ...this._subElementEditorConfig!, + elementConfig: value, + }; + + fireEvent(this, "config-changed", { config: this._config }); + } + + private _editDetailElement(ev: HASSDomEvent): void { + this._subElementEditorConfig = ev.detail.subElementConfig; + } + + private _goBack(): void { + this._subElementEditorConfig = undefined; + } + + private _computeLabelCallback = (schema) => { + switch (schema.name) { + case "dark_mode_image": + case "state_filter": + case "dark_mode_filter": + return ( + this.hass!.localize( + `ui.panel.lovelace.editor.card.picture-elements.${schema.name}` + ) || schema.name + ); + default: + return ( + this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ) || schema.name + ); + } + }; + + static get styles(): CSSResultGroup { + return [configElementStyle]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-picture-elements-card-editor": HuiPictureElementsCardEditor; + } +} diff --git a/src/panels/lovelace/editor/hui-element-editor.ts b/src/panels/lovelace/editor/hui-element-editor.ts index 58c0337334..b3be3fb3bd 100644 --- a/src/panels/lovelace/editor/hui-element-editor.ts +++ b/src/panels/lovelace/editor/hui-element-editor.ts @@ -23,6 +23,7 @@ import type { HomeAssistant } from "../../../types"; import { LovelaceCardFeatureConfig } from "../card-features/types"; import type { LovelaceRowConfig } from "../entity-rows/types"; import { LovelaceHeaderFooterConfig } from "../header-footer/types"; +import { LovelaceElementConfig } from "../elements/types"; import type { LovelaceConfigForm, LovelaceGenericElementEditor, @@ -41,6 +42,7 @@ export interface ConfigChangedEvent { | LovelaceHeaderFooterConfig | LovelaceCardFeatureConfig | LovelaceStrategyConfig + | LovelaceElementConfig | LovelaceBadgeConfig; error?: string; guiModeAvailable?: boolean; diff --git a/src/panels/lovelace/editor/hui-picture-elements-card-row-editor.ts b/src/panels/lovelace/editor/hui-picture-elements-card-row-editor.ts new file mode 100644 index 0000000000..dfff3aea16 --- /dev/null +++ b/src/panels/lovelace/editor/hui-picture-elements-card-row-editor.ts @@ -0,0 +1,255 @@ +import { mdiClose, mdiPencil, mdiContentDuplicate } from "@mdi/js"; +import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; +import { customElement, property, query } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { stopPropagation } from "../../../common/dom/stop_propagation"; +import "../../../components/ha-icon-button"; +import "../../../components/ha-svg-icon"; +import { HomeAssistant } from "../../../types"; +import "../../../components/ha-select"; +import type { HaSelect } from "../../../components/ha-select"; +import { + ConditionalElementConfig, + IconElementConfig, + ImageElementConfig, + LovelaceElementConfig, + ServiceButtonElementConfig, + StateBadgeElementConfig, + StateIconElementConfig, + StateLabelElementConfig, +} from "../elements/types"; + +declare global { + interface HASSDomEvents { + "elements-changed": { + elements: any[]; + }; + } +} + +const elementTypes: string[] = [ + "state-badge", + "state-icon", + "state-label", + "service-button", + "icon", + "image", + "conditional", +]; + +@customElement("hui-picture-elements-card-row-editor") +export class HuiPictureElementsCardRowEditor extends LitElement { + @property({ attribute: false }) public hass?: HomeAssistant; + + @property({ attribute: false }) public elements?: LovelaceElementConfig[]; + + @query("ha-select") private _select!: HaSelect; + + protected render() { + if (!this.elements || !this.hass) { + return nothing; + } + + return html` +

+ ${this.hass.localize( + "ui.panel.lovelace.editor.card.picture-elements.elements" + )} +

+
+ ${this.elements.map( + (element, index) => html` +
+ ${element.type + ? html` +
+
+ + ${this.hass?.localize( + `ui.panel.lovelace.editor.card.picture-elements.element_types.${element.type}` + ) || element.type} + + ${this._getSecondaryDescription(element)} +
+
+ ` + : nothing} + + + +
+ ` + )} + + ${elementTypes.map( + (element) => html` + ${this.hass?.localize( + `ui.panel.lovelace.editor.card.picture-elements.element_types.${element}` + )} + ` + )} + +
+ `; + } + + private _getSecondaryDescription(element: LovelaceElementConfig): string { + switch (element.type) { + case "icon": + return element.title ?? (element as IconElementConfig).icon ?? ""; + case "state-badge": + case "state-icon": + case "state-label": + return ( + element.title ?? + ( + element as + | StateBadgeElementConfig + | StateIconElementConfig + | StateLabelElementConfig + ).entity ?? + "" + ); + case "service-button": + return ( + element.title ?? (element as ServiceButtonElementConfig).service ?? "" + ); + case "image": + return ( + element.title ?? + (element as ImageElementConfig).image ?? + (element as ImageElementConfig).camera_image ?? + "" + ); + case "conditional": + return ( + element.title ?? + `${((element as ConditionalElementConfig).elements || []).length.toString()} ${this.hass?.localize("ui.panel.lovelace.editor.card.picture-elements.elements")}` + ); + } + return "Unknown type"; + } + + private async _addElement(ev): Promise { + const value = ev.target!.value; + if (value === "") { + return; + } + const newElements = this.elements!.concat({ + type: value! as string, + ...(value !== "conditional" + ? { + style: { + top: "50%", + left: "50%", + }, + } + : {}), + } as LovelaceElementConfig); + fireEvent(this, "elements-changed", { elements: newElements }); + this._select.select(-1); + } + + private _removeRow(ev: CustomEvent): void { + const index = (ev.currentTarget as any).index; + const newElements = this.elements!.concat(); + + newElements.splice(index, 1); + + fireEvent(this, "elements-changed", { elements: newElements }); + } + + private _editRow(ev: CustomEvent): void { + const index = (ev.currentTarget as any).index; + fireEvent(this, "edit-detail-element", { + subElementConfig: { + index, + type: "element", + elementConfig: this.elements![index], + }, + }); + } + + private _duplicateRow(ev: CustomEvent): void { + const index = (ev.currentTarget as any).index; + const newElements = [...this.elements!, this.elements![index]]; + + fireEvent(this, "elements-changed", { elements: newElements }); + } + + static get styles(): CSSResultGroup { + return css` + .element { + display: flex; + align-items: center; + } + + .element-row { + height: 60px; + font-size: 16px; + display: flex; + align-items: center; + justify-content: space-between; + flex-grow: 1; + } + + .element-row div { + display: flex; + flex-direction: column; + } + + .remove-icon, + .edit-icon, + .duplicate-icon { + --mdc-icon-button-size: 36px; + color: var(--secondary-text-color); + } + + .secondary { + font-size: 12px; + color: var(--secondary-text-color); + } + + ha-select { + width: 100%; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-picture-elements-card-row-editor": HuiPictureElementsCardRowEditor; + } +} diff --git a/src/panels/lovelace/editor/hui-sub-element-editor.ts b/src/panels/lovelace/editor/hui-sub-element-editor.ts index 62848734dd..df6ed4308d 100644 --- a/src/panels/lovelace/editor/hui-sub-element-editor.ts +++ b/src/panels/lovelace/editor/hui-sub-element-editor.ts @@ -1,6 +1,13 @@ import "@material/mwc-button"; import { mdiCodeBraces, mdiListBoxOutline } from "@mdi/js"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { + css, + CSSResultGroup, + html, + LitElement, + nothing, + TemplateResult, +} from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-icon-button"; @@ -13,6 +20,7 @@ import "./header-footer-editor/hui-header-footer-element-editor"; import type { HuiElementEditor } from "./hui-element-editor"; import "./feature-editor/hui-card-feature-element-editor"; import type { GUIModeChangedEvent, SubElementEditorConfig } from "./types"; +import "./picture-element-editor/hui-picture-element-element-editor"; declare global { interface HASSDomEvents { @@ -95,7 +103,18 @@ export class HuiSubElementEditor extends LitElement { @GUImode-changed=${this._handleGUIModeChanged} > ` - : ""} + : this.config.type === "element" + ? html` + + ` + : nothing} `; } diff --git a/src/panels/lovelace/editor/picture-element-editor/hui-picture-element-element-editor.ts b/src/panels/lovelace/editor/picture-element-editor/hui-picture-element-element-editor.ts new file mode 100644 index 0000000000..79aa163318 --- /dev/null +++ b/src/panels/lovelace/editor/picture-element-editor/hui-picture-element-element-editor.ts @@ -0,0 +1,31 @@ +import { customElement } from "lit/decorators"; +import { LovelaceElementConfig } from "../../elements/types"; +import type { LovelacePictureElementEditor } from "../../types"; +import { HuiElementEditor } from "../hui-element-editor"; +import { getPictureElementClass } from "../../create-element/create-picture-element"; + +@customElement("hui-picture-element-element-editor") +export class HuiPictureElementElementEditor extends HuiElementEditor { + protected get configElementType(): string | undefined { + return this.value?.type; + } + + protected async getConfigElement(): Promise< + LovelacePictureElementEditor | undefined + > { + const elClass = await getPictureElementClass(this.configElementType!); + + // Check if a GUI editor exists + if (elClass && elClass.getConfigElement) { + return elClass.getConfigElement(); + } + + return undefined; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-picture-element-element-editor": HuiPictureElementElementEditor; + } +} diff --git a/src/panels/lovelace/editor/types.ts b/src/panels/lovelace/editor/types.ts index aaa8f4225f..0aacf3c406 100644 --- a/src/panels/lovelace/editor/types.ts +++ b/src/panels/lovelace/editor/types.ts @@ -7,6 +7,7 @@ import { import { EntityConfig, LovelaceRowConfig } from "../entity-rows/types"; import { LovelaceHeaderFooterConfig } from "../header-footer/types"; import { LovelaceCardFeatureConfig } from "../card-features/types"; +import { LovelaceElementConfig } from "../elements/types"; import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge"; export interface YamlChangedEvent extends Event { @@ -93,8 +94,9 @@ export interface SubElementEditorConfig { elementConfig?: | LovelaceRowConfig | LovelaceHeaderFooterConfig - | LovelaceCardFeatureConfig; - type: "header" | "footer" | "row" | "feature"; + | LovelaceCardFeatureConfig + | LovelaceElementConfig; + type: "header" | "footer" | "row" | "feature" | "element"; } export interface EditSubElementEvent { diff --git a/src/panels/lovelace/elements/hui-conditional-element.ts b/src/panels/lovelace/elements/hui-conditional-element.ts index 3f515e5870..838bb70874 100644 --- a/src/panels/lovelace/elements/hui-conditional-element.ts +++ b/src/panels/lovelace/elements/hui-conditional-element.ts @@ -4,6 +4,7 @@ import { checkConditionsMet, validateConditionalConfig, } from "../common/validate-condition"; +import { LovelacePictureElementEditor } from "../types"; import { ConditionalElementConfig, LovelaceElement, @@ -11,6 +12,13 @@ import { } from "./types"; class HuiConditionalElement extends HTMLElement implements LovelaceElement { + public static async getConfigElement(): Promise { + await import( + "../editor/config-elements/elements/hui-conditional-element-editor" + ); + return document.createElement("hui-conditional-element-editor"); + } + public _hass?: HomeAssistant; private _config?: ConditionalElementConfig; diff --git a/src/panels/lovelace/elements/hui-icon-element.ts b/src/panels/lovelace/elements/hui-icon-element.ts index 0dcd51fdd7..4daadc94ed 100644 --- a/src/panels/lovelace/elements/hui-icon-element.ts +++ b/src/panels/lovelace/elements/hui-icon-element.ts @@ -8,10 +8,16 @@ import { actionHandler } from "../common/directives/action-handler-directive"; import { handleAction } from "../common/handle-action"; import { hasAction } from "../common/has-action"; import { IconElementConfig, LovelaceElement } from "./types"; +import { LovelacePictureElementEditor } from "../types"; import { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; @customElement("hui-icon-element") export class HuiIconElement extends LitElement implements LovelaceElement { + public static async getConfigElement(): Promise { + await import("../editor/config-elements/elements/hui-icon-element-editor"); + return document.createElement("hui-icon-element-editor"); + } + public hass?: HomeAssistant; @state() private _config?: IconElementConfig; diff --git a/src/panels/lovelace/elements/hui-image-element.ts b/src/panels/lovelace/elements/hui-image-element.ts index 4fc9159c72..104461661a 100644 --- a/src/panels/lovelace/elements/hui-image-element.ts +++ b/src/panels/lovelace/elements/hui-image-element.ts @@ -10,9 +10,15 @@ import { handleAction } from "../common/handle-action"; import { hasAction } from "../common/has-action"; import "../components/hui-image"; import { ImageElementConfig, LovelaceElement } from "./types"; +import { LovelacePictureElementEditor } from "../types"; @customElement("hui-image-element") export class HuiImageElement extends LitElement implements LovelaceElement { + public static async getConfigElement(): Promise { + await import("../editor/config-elements/elements/hui-image-element-editor"); + return document.createElement("hui-image-element-editor"); + } + @property({ attribute: false }) public hass?: HomeAssistant; @state() private _config?: ImageElementConfig; diff --git a/src/panels/lovelace/elements/hui-service-button-element.ts b/src/panels/lovelace/elements/hui-service-button-element.ts index 418758540d..6c7d3508e1 100644 --- a/src/panels/lovelace/elements/hui-service-button-element.ts +++ b/src/panels/lovelace/elements/hui-service-button-element.ts @@ -3,12 +3,20 @@ import { customElement, state } from "lit/decorators"; import "../../../components/buttons/ha-call-service-button"; import { HomeAssistant } from "../../../types"; import { LovelaceElement, ServiceButtonElementConfig } from "./types"; +import { LovelacePictureElementEditor } from "../types"; @customElement("hui-service-button-element") export class HuiServiceButtonElement extends LitElement implements LovelaceElement { + public static async getConfigElement(): Promise { + await import( + "../editor/config-elements/elements/hui-service-button-element-editor" + ); + return document.createElement("hui-service-button-element-editor"); + } + public hass?: HomeAssistant; @state() private _config?: ServiceButtonElementConfig; diff --git a/src/panels/lovelace/elements/hui-state-badge-element.ts b/src/panels/lovelace/elements/hui-state-badge-element.ts index 446bae8cdc..78d69a31ca 100644 --- a/src/panels/lovelace/elements/hui-state-badge-element.ts +++ b/src/panels/lovelace/elements/hui-state-badge-element.ts @@ -12,12 +12,20 @@ import { hasConfigOrEntityChanged } from "../common/has-changed"; import { createEntityNotFoundWarning } from "../components/hui-warning"; import "../components/hui-warning-element"; import { LovelaceElement, StateBadgeElementConfig } from "./types"; +import { LovelacePictureElementEditor } from "../types"; @customElement("hui-state-badge-element") export class HuiStateBadgeElement extends LitElement implements LovelaceElement { + public static async getConfigElement(): Promise { + await import( + "../editor/config-elements/elements/hui-state-badge-element-editor" + ); + return document.createElement("hui-state-badge-element-editor"); + } + @property({ attribute: false }) public hass?: HomeAssistant; @state() private _config?: StateBadgeElementConfig; @@ -44,7 +52,7 @@ export class HuiStateBadgeElement if (!stateObj) { return html` `; } diff --git a/src/panels/lovelace/elements/hui-state-icon-element.ts b/src/panels/lovelace/elements/hui-state-icon-element.ts index 1f21100479..84e0a7add6 100644 --- a/src/panels/lovelace/elements/hui-state-icon-element.ts +++ b/src/panels/lovelace/elements/hui-state-icon-element.ts @@ -18,10 +18,18 @@ import { hasConfigOrEntityChanged } from "../common/has-changed"; import { createEntityNotFoundWarning } from "../components/hui-warning"; import "../components/hui-warning-element"; import { LovelaceElement, StateIconElementConfig } from "./types"; +import { LovelacePictureElementEditor } from "../types"; import { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; @customElement("hui-state-icon-element") export class HuiStateIconElement extends LitElement implements LovelaceElement { + public static async getConfigElement(): Promise { + await import( + "../editor/config-elements/elements/hui-state-icon-element-editor" + ); + return document.createElement("hui-state-icon-element-editor"); + } + @property({ attribute: false }) public hass?: HomeAssistant; @state() private _config?: StateIconElementConfig; @@ -52,7 +60,7 @@ export class HuiStateIconElement extends LitElement implements LovelaceElement { if (!stateObj) { return html` `; } diff --git a/src/panels/lovelace/elements/hui-state-label-element.ts b/src/panels/lovelace/elements/hui-state-label-element.ts index a8176fdb4a..ef699bf6a6 100644 --- a/src/panels/lovelace/elements/hui-state-label-element.ts +++ b/src/panels/lovelace/elements/hui-state-label-element.ts @@ -18,9 +18,17 @@ import { hasConfigOrEntityChanged } from "../common/has-changed"; import { createEntityNotFoundWarning } from "../components/hui-warning"; import "../components/hui-warning-element"; import { LovelaceElement, StateLabelElementConfig } from "./types"; +import { LovelacePictureElementEditor } from "../types"; @customElement("hui-state-label-element") class HuiStateLabelElement extends LitElement implements LovelaceElement { + public static async getConfigElement(): Promise { + await import( + "../editor/config-elements/elements/hui-state-label-element-editor" + ); + return document.createElement("hui-state-label-element-editor"); + } + @property({ attribute: false }) public hass?: HomeAssistant; @state() private _config?: StateLabelElementConfig; @@ -47,7 +55,7 @@ class HuiStateLabelElement extends LitElement implements LovelaceElement { if (!stateObj) { return html` `; } diff --git a/src/panels/lovelace/elements/types.ts b/src/panels/lovelace/elements/types.ts index 35b2c453a8..1b433b5bd9 100644 --- a/src/panels/lovelace/elements/types.ts +++ b/src/panels/lovelace/elements/types.ts @@ -26,6 +26,7 @@ export interface LovelaceElement extends HTMLElement { export interface ConditionalElementConfig extends LovelaceElementConfigBase { conditions: Condition[]; elements: LovelaceElementConfigBase[]; + title?: string; } export interface IconElementConfig extends LovelaceElementConfigBase { @@ -34,7 +35,8 @@ export interface IconElementConfig extends LovelaceElementConfigBase { tap_action?: ActionConfig; hold_action?: ActionConfig; double_tap_action?: ActionConfig; - icon: string; + icon?: string; + title?: string; } export interface ImageElementConfig extends LovelaceElementConfigBase { @@ -52,6 +54,7 @@ export interface ImageElementConfig extends LovelaceElementConfigBase { filter?: string; state_filter?: string; aspect_ratio?: string; + title?: string; } export interface ServiceButtonElementConfig extends LovelaceElementConfigBase { @@ -64,7 +67,7 @@ export interface ServiceButtonElementConfig extends LovelaceElementConfigBase { } export interface StateBadgeElementConfig extends LovelaceElementConfigBase { - entity: string; + entity?: string; title?: string; tap_action?: ActionConfig; hold_action?: ActionConfig; @@ -72,20 +75,22 @@ export interface StateBadgeElementConfig extends LovelaceElementConfigBase { } export interface StateIconElementConfig extends LovelaceElementConfigBase { - entity: string; + entity?: string; tap_action?: ActionConfig; hold_action?: ActionConfig; double_tap_action?: ActionConfig; icon?: string; state_color?: boolean; + title?: string; } export interface StateLabelElementConfig extends LovelaceElementConfigBase { - entity: string; + entity?: string; attribute?: string; prefix?: string; suffix?: string; tap_action?: ActionConfig; hold_action?: ActionConfig; double_tap_action?: ActionConfig; + title?: string; } diff --git a/src/panels/lovelace/types.ts b/src/panels/lovelace/types.ts index ab452b891f..d99f52112f 100644 --- a/src/panels/lovelace/types.ts +++ b/src/panels/lovelace/types.ts @@ -12,6 +12,7 @@ import { Constructor, HomeAssistant } from "../../types"; import { LovelaceRow, LovelaceRowConfig } from "./entity-rows/types"; import { LovelaceHeaderFooterConfig } from "./header-footer/types"; import { LovelaceCardFeatureConfig } from "./card-features/types"; +import { LovelaceElement, LovelaceElementConfig } from "./elements/types"; declare global { // eslint-disable-next-line @@ -105,6 +106,11 @@ export interface LovelaceRowConstructor extends Constructor { getConfigElement?: () => LovelaceRowEditor; } +export interface LovelaceElementConstructor + extends Constructor { + getConfigElement?: () => LovelacePictureElementEditor; +} + export interface LovelaceHeaderFooter extends HTMLElement { hass?: HomeAssistant; type: "header" | "footer"; @@ -129,6 +135,11 @@ export interface LovelaceRowEditor extends LovelaceGenericElementEditor { setConfig(config: LovelaceRowConfig): void; } +export interface LovelacePictureElementEditor + extends LovelaceGenericElementEditor { + setConfig(config: LovelaceElementConfig): void; +} + export interface LovelaceGenericElementEditor extends HTMLElement { hass?: HomeAssistant; lovelace?: LovelaceConfig; diff --git a/src/translations/en.json b/src/translations/en.json index 3cc624eac6..6d5434ab77 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5984,7 +5984,22 @@ }, "picture-elements": { "name": "Picture elements", - "description": "The Picture elements card is one of the most versatile types of cards. The cards allow you to position icons or text and even services! On an image based on coordinates." + "description": "The Picture elements card is one of the most versatile types of cards. The cards allow you to position icons or text and even services! On an image based on coordinates.", + "card_options": "Card Options", + "elements": "Elements", + "new_element": "Add new element", + "dark_mode_image": "Dark mode image path", + "state_filter": "State filter", + "dark_mode_filter": "Dark mode state filter", + "element_types": { + "state-badge": "State badge", + "state-icon": "State icon", + "state-label": "State label", + "service-button": "Service call button", + "icon": "Icon", + "image": "Image", + "conditional": "Conditional" + } }, "picture-entity": { "name": "Picture entity", @@ -6046,6 +6061,14 @@ "twice_daily": "Twice daily" } }, + "elements": { + "style": "Style", + "prefix": "Prefix", + "suffix": "Suffix", + "state_image": "State image", + "filter": "Filter", + "state_filter": "[%key:ui::panel::lovelace::editor::card::picture-elements::state_filter%]" + }, "badge": { "entity": { "name": "Entity", @@ -6290,7 +6313,8 @@ "header": "Header editor", "footer": "Footer editor", "row": "Entity row editor", - "feature": "Feature editor" + "feature": "Feature editor", + "element": "Element editor" } } },