diff --git a/src/panels/lovelace/create-element/create-element-base.ts b/src/panels/lovelace/create-element/create-element-base.ts index a857921f8e..5c35c44a0a 100644 --- a/src/panels/lovelace/create-element/create-element-base.ts +++ b/src/panels/lovelace/create-element/create-element-base.ts @@ -16,6 +16,7 @@ import { LovelaceCard, LovelaceCardConstructor, LovelaceHeaderFooter, + LovelaceHeaderFooterConstructor, LovelaceRowConstructor, } from "../types"; @@ -45,7 +46,7 @@ interface CreateElementConfigTypes { "header-footer": { config: LovelaceHeaderFooterConfig; element: LovelaceHeaderFooter; - constructor: unknown; + constructor: LovelaceHeaderFooterConstructor; }; view: { config: LovelaceViewConfig; diff --git a/src/panels/lovelace/create-element/create-header-footer-element.ts b/src/panels/lovelace/create-element/create-header-footer-element.ts index 4ec039f3f8..34ef90e9b6 100644 --- a/src/panels/lovelace/create-element/create-header-footer-element.ts +++ b/src/panels/lovelace/create-element/create-header-footer-element.ts @@ -1,5 +1,8 @@ import { LovelaceHeaderFooterConfig } from "../header-footer/types"; -import { createLovelaceElement } from "./create-element-base"; +import { + createLovelaceElement, + getLovelaceElementClass, +} from "./create-element-base"; const LAZY_LOAD_TYPES = { picture: () => import("../header-footer/hui-picture-header-footer"), @@ -16,3 +19,6 @@ export const createHeaderFooterElement = (config: LovelaceHeaderFooterConfig) => undefined, undefined ); + +export const getHeaderFooterElementClass = (type: string) => + getLovelaceElementClass(type, "header-footer", undefined, LAZY_LOAD_TYPES); diff --git a/src/panels/lovelace/editor/card-editor/hui-card-element-editor.ts b/src/panels/lovelace/editor/card-editor/hui-card-element-editor.ts new file mode 100644 index 0000000000..f2d99f3ad0 --- /dev/null +++ b/src/panels/lovelace/editor/card-editor/hui-card-element-editor.ts @@ -0,0 +1,25 @@ +import { customElement } from "lit-element"; +import type { LovelaceCardConfig } from "../../../../data/lovelace"; +import { getCardElementClass } from "../../create-element/create-card-element"; +import type { LovelaceCardEditor } from "../../types"; +import { HuiElementEditor } from "../hui-element-editor"; + +@customElement("hui-card-element-editor") +export class HuiCardElementEditor extends HuiElementEditor { + protected async getConfigElement(): Promise { + const elClass = await getCardElementClass(this.configElementType!); + + // Check if a GUI editor exists + if (elClass && elClass.getConfigElement) { + return elClass.getConfigElement(); + } + + return undefined; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-card-element-editor": HuiCardElementEditor; + } +} diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts index 4c0c1cb903..6c67151292 100755 --- a/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts +++ b/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts @@ -29,12 +29,10 @@ import type { HomeAssistant } from "../../../../types"; import { showSaveSuccessToast } from "../../../../util/toast-saved-success"; import { addCard, replaceCard } from "../config-util"; import { getCardDocumentationURL } from "../get-card-documentation-url"; -import "../hui-element-editor"; -import type { - ConfigChangedEvent, - HuiElementEditor, -} from "../hui-element-editor"; +import type { ConfigChangedEvent } from "../hui-element-editor"; import type { GUIModeChangedEvent } from "../types"; +import "./hui-card-element-editor"; +import type { HuiCardElementEditor } from "./hui-card-element-editor"; import "./hui-card-preview"; import type { EditCardDialogParams } from "./show-edit-card-dialog"; @@ -68,7 +66,8 @@ export class HuiDialogEditCard extends LitElement @internalProperty() private _guiModeAvailable? = true; - @query("hui-element-editor") private _cardEditorEl?: HuiElementEditor; + @query("hui-card-element-editor") + private _cardEditorEl?: HuiCardElementEditor; @internalProperty() private _GUImode = true; @@ -187,14 +186,14 @@ export class HuiDialogEditCard extends LitElement
- + >
- + > ` : html` - ${this.hass.localize( - "ui.panel.lovelace.editor.card.entities.entity_row_editor" - )} - - + `; } @@ -174,43 +143,75 @@ export class HuiEntitiesCardEditor extends LitElement >
+ + `; } - private _valueChanged(ev: EntitiesEditorEvent): void { + private _valueChanged(ev: CustomEvent): void { + ev.stopPropagation(); if (!this._config || !this.hass) { return; } const target = ev.target! as EditorTarget; + const configValue = + target.configValue || this._subElementEditorConfig?.type; + const value = + target.checked !== undefined + ? target.checked + : target.value || ev.detail.config || ev.detail.value; if ( - (target.configValue! === "title" && target.value === this._title) || - (target.configValue! === "theme" && target.value === this._theme) + (configValue! === "title" && target.value === this._title) || + (configValue! === "theme" && target.value === this._theme) ) { return; } - if (ev.detail && ev.detail.entities) { - this._config = { ...this._config, entities: ev.detail.entities }; + if (configValue === "row" || (ev.detail && ev.detail.entities)) { + const newConfigEntities = + ev.detail.entities || this._configEntities!.concat(); + if (configValue === "row") { + if (!value) { + newConfigEntities.splice(this._subElementEditorConfig!.index!, 1); + this._goBack(); + } else { + newConfigEntities[this._subElementEditorConfig!.index!] = value; + } - this._configEntities = processEditorEntities(this._config.entities); - } else if (target.configValue) { - if (target.value === "") { + this._subElementEditorConfig!.elementConfig = value; + } + + this._config = { ...this._config!, entities: newConfigEntities }; + this._configEntities = processEditorEntities(this._config!.entities); + } else if (configValue) { + if (value === "") { this._config = { ...this._config }; - delete this._config[target.configValue!]; + delete this._config[configValue!]; } else { this._config = { ...this._config, - [target.configValue]: - target.checked !== undefined ? target.checked : target.value, + [configValue]: value, }; } } @@ -218,49 +219,52 @@ export class HuiEntitiesCardEditor extends LitElement fireEvent(this, "config-changed", { config: this._config }); } - private _editRow(ev: HASSDomEvent): void { - this._editRowIndex = ev.detail.index; - this._editRowConfig = this._configEntities![this._editRowIndex]; + 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 === "row") { + const newConfigEntities = this._configEntities!.concat(); + if (!value) { + newConfigEntities.splice(this._subElementEditorConfig!.index!, 1); + this._goBack(); + } else { + newConfigEntities[this._subElementEditorConfig!.index!] = value; + } + + this._config = { ...this._config!, entities: newConfigEntities }; + this._configEntities = processEditorEntities(this._config!.entities); + } else if (configValue) { + if (value === "") { + this._config = { ...this._config }; + delete this._config[configValue!]; + } else { + this._config = { + ...this._config, + [configValue]: value, + }; + } + } + + 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._editRowIndex = undefined; - this._editRowConfig = undefined; - this._editRowGuiModeAvailable = true; - this._editRowGuiMode = true; - } - - private _toggleMode(): void { - this._cardEditorEl?.toggleMode(); - } - - private _handleEntityRowConfigChanged(ev: CustomEvent): void { - ev.stopPropagation(); - const value = ev.detail.config as LovelaceRowConfig; - this._editRowGuiModeAvailable = ev.detail.guiModeAvailable; - - const newConfigEntities = this._configEntities!.concat(); - - if (!value) { - newConfigEntities.splice(this._editRowIndex!, 1); - this._goBack(); - } else { - newConfigEntities[this._editRowIndex!] = value; - } - - this._editRowConfig = value; - - this._config = { ...this._config!, entities: newConfigEntities }; - - this._configEntities = processEditorEntities(this._config!.entities); - - fireEvent(this, "config-changed", { config: this._config! }); - } - - private _handleGUIModeChanged(ev: HASSDomEvent): void { - ev.stopPropagation(); - this._editRowGuiMode = ev.detail.guiMode; - this._editRowGuiModeAvailable = ev.detail.guiModeAvailable; + this._subElementEditorConfig = undefined; } static get styles(): CSSResultArray { @@ -273,6 +277,10 @@ export class HuiEntitiesCardEditor extends LitElement align-items: center; font-size: 18px; } + + hui-header-footer-editor { + padding-top: 4px; + } `, ]; } diff --git a/src/panels/lovelace/editor/config-elements/hui-graph-footer-editor.ts b/src/panels/lovelace/editor/config-elements/hui-graph-footer-editor.ts new file mode 100644 index 0000000000..3e9e3005d2 --- /dev/null +++ b/src/panels/lovelace/editor/config-elements/hui-graph-footer-editor.ts @@ -0,0 +1,156 @@ +import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; +import "@polymer/paper-input/paper-input"; +import { + CSSResult, + customElement, + html, + internalProperty, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import { assert } from "superstruct"; +import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/entity/ha-entity-picker"; +import "../../../../components/ha-formfield"; +import "../../../../components/ha-switch"; +import type { HomeAssistant } from "../../../../types"; +import { + GraphHeaderFooterConfig, + graphHeaderFooterConfigStruct, +} from "../../header-footer/types"; +import type { LovelaceCardEditor } from "../../types"; +import type { EditorTarget, EntitiesEditorEvent } from "../types"; +import { configElementStyle } from "./config-elements-style"; + +const includeDomains = ["sensor"]; + +@customElement("hui-graph-footer-editor") +export class HuiGraphFooterEditor extends LitElement + implements LovelaceCardEditor { + @property({ attribute: false }) public hass?: HomeAssistant; + + @internalProperty() private _config?: GraphHeaderFooterConfig; + + public setConfig(config: GraphHeaderFooterConfig): void { + assert(config, graphHeaderFooterConfigStruct); + this._config = config; + } + + get _entity(): string { + return this._config!.entity || ""; + } + + get _detail(): number { + return this._config!.detail ?? 1; + } + + get _hours_to_show(): number { + return this._config!.hours_to_show || 24; + } + + protected render(): TemplateResult { + if (!this.hass || !this._config) { + return html``; + } + + return html` +
+ +
+ + + + +
+
+ `; + } + + private _change(ev: Event) { + if (!this._config || !this.hass) { + return; + } + + const value = (ev.target! as EditorTarget).checked ? 2 : 1; + + if (this._detail === value) { + return; + } + + this._config = { + ...this._config, + detail: value, + }; + + fireEvent(this, "config-changed", { config: this._config }); + } + + private _valueChanged(ev: HASSDomEvent): void { + if (!this._config || !this.hass) { + return; + } + const target = ev.target! as EditorTarget; + + if (this[`_${target.configValue}`] === target.value) { + return; + } + if (target.configValue) { + if ( + target.value === "" || + (target.type === "number" && isNaN(Number(target.value))) + ) { + this._config = { ...this._config }; + delete this._config[target.configValue!]; + } else { + let value: any = target.value; + if (target.type === "number") { + value = Number(value); + } + this._config = { ...this._config, [target.configValue!]: value }; + } + } + + fireEvent(this, "config-changed", { config: this._config }); + } + + static get styles(): CSSResult { + return configElementStyle; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-graph-footer-editor": HuiGraphFooterEditor; + } +} diff --git a/src/panels/lovelace/editor/config-elements/hui-stack-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-stack-card-editor.ts index d2640e7cae..bdb6609b9b 100644 --- a/src/panels/lovelace/editor/config-elements/hui-stack-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-stack-card-editor.ts @@ -18,12 +18,10 @@ import { LovelaceCardConfig, LovelaceConfig } from "../../../../data/lovelace"; import { HomeAssistant } from "../../../../types"; import { StackCardConfig } from "../../cards/types"; import { LovelaceCardEditor } from "../../types"; +import "../card-editor/hui-card-element-editor"; +import type { HuiCardElementEditor } from "../card-editor/hui-card-element-editor"; import "../card-editor/hui-card-picker"; -import "../hui-element-editor"; -import type { - ConfigChangedEvent, - HuiElementEditor, -} from "../hui-element-editor"; +import type { ConfigChangedEvent } from "../hui-element-editor"; import { GUIModeChangedEvent } from "../types"; const cardConfigStruct = object({ @@ -47,7 +45,8 @@ export class HuiStackCardEditor extends LitElement @internalProperty() private _guiModeAvailable? = true; - @query("hui-element-editor") private _cardEditorEl?: HuiElementEditor; + @query("hui-card-element-editor") + private _cardEditorEl?: HuiCardElementEditor; public setConfig(config: Readonly): void { assert(config, cardConfigStruct); @@ -140,13 +139,13 @@ export class HuiStackCardEditor extends LitElement - + > ` : html` { + protected get configElementType(): string | undefined { + if (!this.value?.type && "entity" in this.value!) { + return GENERIC_ROW_TYPE; + } + + return this.value?.type; + } + + protected async getConfigElement(): Promise { + if (this.configElementType! === GENERIC_ROW_TYPE) { + return document.createElement("hui-generic-entity-row-editor"); + } + + const elClass = await getRowElementClass(this.configElementType!); + + // Check if a GUI editor exists + if (elClass && elClass.getConfigElement) { + return elClass.getConfigElement(); + } + + return undefined; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-row-element-editor": HuiRowElementEditor; + } +} diff --git a/src/panels/lovelace/editor/header-footer-editor/get-headerfooter-stub-config.ts b/src/panels/lovelace/editor/header-footer-editor/get-headerfooter-stub-config.ts new file mode 100644 index 0000000000..cda782b924 --- /dev/null +++ b/src/panels/lovelace/editor/header-footer-editor/get-headerfooter-stub-config.ts @@ -0,0 +1,26 @@ +import { HomeAssistant } from "../../../../types"; +import { getHeaderFooterElementClass } from "../../create-element/create-header-footer-element"; +import { LovelaceHeaderFooterConfig } from "../../header-footer/types"; + +export const getHeaderFooterStubConfig = async ( + hass: HomeAssistant, + type: string, + entities: string[], + entitiesFallback: string[] +): Promise => { + let config: LovelaceHeaderFooterConfig = { type }; + + const elClass = await getHeaderFooterElementClass(type); + + if (elClass && elClass.getStubConfig) { + const classStubConfig = elClass.getStubConfig( + hass, + entities, + entitiesFallback + ); + + config = { ...config, ...classStubConfig }; + } + + return config; +}; diff --git a/src/panels/lovelace/editor/header-footer-editor/hui-dialog-create-headerfooter.ts b/src/panels/lovelace/editor/header-footer-editor/hui-dialog-create-headerfooter.ts new file mode 100644 index 0000000000..2a1ccc928f --- /dev/null +++ b/src/panels/lovelace/editor/header-footer-editor/hui-dialog-create-headerfooter.ts @@ -0,0 +1,176 @@ +import "@material/mwc-button/mwc-button"; +import { + css, + CSSResultArray, + customElement, + html, + internalProperty, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import { createCloseHeading } from "../../../../components/ha-dialog"; +import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; +import { haStyleDialog } from "../../../../resources/styles"; +import type { HomeAssistant } from "../../../../types"; +import type { LovelaceHeaderFooterConfig } from "../../header-footer/types"; +import { headerFooterElements } from "../lovelace-headerfooters"; +import { getHeaderFooterStubConfig } from "./get-headerfooter-stub-config"; +import type { CreateHeaderFooterDialogParams } from "./show-create-headerfooter-dialog"; + +@customElement("hui-dialog-create-headerfooter") +export class HuiCreateDialogHeaderFooter extends LitElement + implements HassDialog { + @property({ attribute: false }) protected hass!: HomeAssistant; + + @internalProperty() private _params?: CreateHeaderFooterDialogParams; + + public async showDialog( + params: CreateHeaderFooterDialogParams + ): Promise { + this._params = params; + } + + public closeDialog(): boolean { + this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + return true; + } + + protected render(): TemplateResult { + if (!this._params) { + return html``; + } + + return html` + +
+ ${headerFooterElements.map( + (headerFooter) => + html` + + +
+ ${this.hass!.localize( + `ui.panel.lovelace.editor.header-footer.types.${headerFooter.type}.name` + )} +
+
+ ` + )} +
+
+ + ${this.hass!.localize("ui.common.cancel")} + +
+
+ `; + } + + private async _handleHeaderFooterPicked(ev: CustomEvent): Promise { + const type = (ev.currentTarget as any).type; + let config: LovelaceHeaderFooterConfig = { type }; + + if (this.hass) { + config = await getHeaderFooterStubConfig( + this.hass, + type, + this._params?.entities || [], + [] + ); + } + + this._params!.pickHeaderFooter(config); + this.closeDialog(); + } + + private _ignoreKeydown(ev: KeyboardEvent) { + ev.stopPropagation(); + } + + private _cancel(ev?: Event) { + if (ev) { + ev.stopPropagation(); + } + this.closeDialog(); + } + + static get styles(): CSSResultArray { + return [ + haStyleDialog, + css` + @media all and (max-width: 450px), all and (max-height: 500px) { + /* overrule the ha-style-dialog max-height on small screens */ + ha-dialog { + --mdc-dialog-max-height: 100%; + height: 100%; + } + } + + @media all and (min-width: 850px) { + ha-dialog { + --mdc-dialog-min-width: 550px; + } + } + + ha-dialog { + --mdc-dialog-max-width: 550px; + --dialog-content-padding: 2px 24px 20px 24px; + --dialog-z-index: 5; + } + + .elements { + display: flex; + flex-wrap: wrap; + } + + .spinner, + ha-card { + width: calc(50% - 8px); + text-align: center; + margin: 4px; + } + + ha-card { + box-sizing: border-box; + padding: 8px; + color: var(--secondary-text-color); + font-size: 16px; + cursor: pointer; + } + + ha-svg-icon { + padding-bottom: 4px; + --mdc-icon-size: 38px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-dialog-create-headerfooter": HuiCreateDialogHeaderFooter; + } +} diff --git a/src/panels/lovelace/editor/header-footer-editor/hui-header-footer-editor.ts b/src/panels/lovelace/editor/header-footer-editor/hui-header-footer-editor.ts new file mode 100644 index 0000000000..dc064f7ddd --- /dev/null +++ b/src/panels/lovelace/editor/header-footer-editor/hui-header-footer-editor.ts @@ -0,0 +1,146 @@ +import "@material/mwc-icon-button/mwc-icon-button"; +import { mdiClose, mdiPencil, mdiPlus } from "@mdi/js"; +import "@polymer/paper-item/paper-item"; +import "@polymer/paper-listbox/paper-listbox"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-paper-dropdown-menu"; +import "../../../../components/ha-svg-icon"; +import type { LovelaceConfig } from "../../../../data/lovelace"; +import type { HomeAssistant } from "../../../../types"; +import type { LovelaceHeaderFooterConfig } from "../../header-footer/types"; +import { showCreateHeaderFooterDialog } from "./show-create-headerfooter-dialog"; + +@customElement("hui-header-footer-editor") +export class HuiHeaderFooterEditor extends LitElement { + public hass!: HomeAssistant; + + public lovelaceConfig!: LovelaceConfig; + + @property({ attribute: false }) public config?: LovelaceHeaderFooterConfig; + + @property() public configValue!: "header" | "footer"; + + protected render(): TemplateResult { + return html` +
+ + ${this.hass.localize( + `ui.panel.lovelace.editor.header-footer.${this.configValue}` + )}: + ${!this.config?.type + ? this.hass!.localize("ui.panel.lovelace.editor.common.none") + : this.hass!.localize( + `ui.panel.lovelace.editor.header-footer.types.${this.config?.type}.name` + )} + +
+
+ ${!this.config?.type + ? html` + + + + ` + : html` + + + + + + + `} +
+ `; + } + + private _edit(): void { + fireEvent(this, "edit-detail-element", { + subElementConfig: { + elementConfig: this.config, + type: this.configValue, + }, + }); + } + + private _add(): void { + showCreateHeaderFooterDialog(this, { + pickHeaderFooter: (config) => this._elementPicked(config), + type: this.configValue, + }); + } + + private _elementPicked(config: LovelaceHeaderFooterConfig): void { + fireEvent(this, "value-changed", { value: config }); + fireEvent(this, "edit-detail-element", { + subElementConfig: { + elementConfig: config, + type: this.configValue, + }, + }); + } + + private _delete(): void { + fireEvent(this, "value-changed", { value: "" }); + } + + static get styles(): CSSResult { + return css` + :host { + font-size: 16px; + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + padding-bottom: 12px; + } + + :host > div { + display: flex; + align-items: center; + } + + mwc-icon-button, + .header-footer-icon { + --mdc-icon-button-size: 36px; + color: var(--secondary-text-color); + } + + .header-footer-icon { + padding-right: 8px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-header-footer-editor": HuiHeaderFooterEditor; + } +} diff --git a/src/panels/lovelace/editor/header-footer-editor/hui-header-footer-element-editor.ts b/src/panels/lovelace/editor/header-footer-editor/hui-header-footer-element-editor.ts new file mode 100644 index 0000000000..3ddd0fa1a5 --- /dev/null +++ b/src/panels/lovelace/editor/header-footer-editor/hui-header-footer-element-editor.ts @@ -0,0 +1,29 @@ +import { customElement } from "lit-element"; +import { getHeaderFooterElementClass } from "../../create-element/create-header-footer-element"; +import type { LovelaceHeaderFooterConfig } from "../../header-footer/types"; +import type { LovelaceHeaderFooterEditor } from "../../types"; +import { HuiElementEditor } from "../hui-element-editor"; + +@customElement("hui-headerfooter-element-editor") +export class HuiHeaderFooterElementEditor extends HuiElementEditor< + LovelaceHeaderFooterConfig +> { + protected async getConfigElement(): Promise< + LovelaceHeaderFooterEditor | undefined + > { + const elClass = await getHeaderFooterElementClass(this.configElementType!); + + // Check if a GUI editor exists + if (elClass && elClass.getConfigElement) { + return elClass.getConfigElement(); + } + + return undefined; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-headerfooter-element-editor": HuiHeaderFooterElementEditor; + } +} diff --git a/src/panels/lovelace/editor/header-footer-editor/show-create-headerfooter-dialog.ts b/src/panels/lovelace/editor/header-footer-editor/show-create-headerfooter-dialog.ts new file mode 100644 index 0000000000..8d5ce55686 --- /dev/null +++ b/src/panels/lovelace/editor/header-footer-editor/show-create-headerfooter-dialog.ts @@ -0,0 +1,24 @@ +import { fireEvent } from "../../../../common/dom/fire_event"; +import type { LovelaceHeaderFooterConfig } from "../../header-footer/types"; + +export interface CreateHeaderFooterDialogParams { + pickHeaderFooter: (config: LovelaceHeaderFooterConfig) => void; + type: "header" | "footer"; + entities?: string[]; // We can pass entity id's that will be added to the config when a header footer is picked +} + +const importCreateHeaderFooterDialog = () => + import( + /* webpackChunkName: "hui-dialog-create-headerfooter" */ "./hui-dialog-create-headerfooter" + ); + +export const showCreateHeaderFooterDialog = ( + element: HTMLElement, + createHeaderFooterDialogParams: CreateHeaderFooterDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "hui-dialog-create-headerfooter", + dialogImport: importCreateHeaderFooterDialog, + dialogParams: createHeaderFooterDialogParams, + }); +}; diff --git a/src/panels/lovelace/editor/hui-detail-editor-base.ts b/src/panels/lovelace/editor/hui-detail-editor-base.ts deleted file mode 100644 index 2dedee71f6..0000000000 --- a/src/panels/lovelace/editor/hui-detail-editor-base.ts +++ /dev/null @@ -1,86 +0,0 @@ -import "@material/mwc-button"; -import "@material/mwc-icon-button"; -import { mdiArrowLeft } from "@mdi/js"; -import { - css, - CSSResult, - customElement, - html, - LitElement, - property, - TemplateResult, -} from "lit-element"; -import { fireEvent } from "../../../common/dom/fire_event"; -import "../../../components/ha-svg-icon"; -import { HomeAssistant } from "../../../types"; - -declare global { - interface HASSDomEvents { - "go-back": undefined; - "toggle-gui-mode": undefined; - } -} - -@customElement("hui-detail-editor-base") -export class HuiDetailEditorBase extends LitElement { - public hass!: HomeAssistant; - - @property({ type: Boolean }) public guiModeAvailable? = true; - - @property({ type: Boolean }) public guiMode? = true; - - protected render(): TemplateResult { - return html` -
-
- - - - -
- - ${this.hass.localize( - this.guiMode - ? "ui.panel.lovelace.editor.edit_card.show_code_editor" - : "ui.panel.lovelace.editor.edit_card.show_visual_editor" - )} - -
- - `; - } - - private _goBack(): void { - fireEvent(this, "go-back"); - } - - private _toggleMode(): void { - fireEvent(this, "toggle-gui-mode"); - } - - static get styles(): CSSResult { - return css` - .header { - display: flex; - justify-content: space-between; - align-items: center; - } - .back-title { - display: flex; - align-items: center; - font-size: 18px; - } - `; - } -} - -declare global { - interface HTMLElementTagNameMap { - "hui-detail-editor-base": HuiDetailEditorBase; - } -} diff --git a/src/panels/lovelace/editor/hui-element-editor.ts b/src/panels/lovelace/editor/hui-element-editor.ts index c8f6078096..fa4ecddfb4 100644 --- a/src/panels/lovelace/editor/hui-element-editor.ts +++ b/src/panels/lovelace/editor/hui-element-editor.ts @@ -3,11 +3,11 @@ import { safeDump, safeLoad } from "js-yaml"; import { css, CSSResult, - customElement, html, internalProperty, LitElement, property, + PropertyValues, query, TemplateResult, } from "lit-element"; @@ -23,60 +23,45 @@ import type { } from "../../../data/lovelace"; import type { HomeAssistant } from "../../../types"; import { handleStructError } from "../common/structs/handle-errors"; -import { getCardElementClass } from "../create-element/create-card-element"; -import { getRowElementClass } from "../create-element/create-row-element"; import type { LovelaceRowConfig } from "../entity-rows/types"; -import type { - LovelaceCardConstructor, - LovelaceCardEditor, - LovelaceRowConstructor, - LovelaceRowEditor, -} from "../types"; +import { LovelaceHeaderFooterConfig } from "../header-footer/types"; +import type { LovelaceGenericElementEditor } from "../types"; import "./config-elements/hui-generic-entity-row-editor"; import { GUISupportError } from "./gui-support-error"; -import { GUIModeChangedEvent } from "./types"; +import { EditSubElementEvent, GUIModeChangedEvent } from "./types"; export interface ConfigChangedEvent { - config: LovelaceCardConfig | LovelaceRowConfig; + config: LovelaceCardConfig | LovelaceRowConfig | LovelaceHeaderFooterConfig; error?: string; guiModeAvailable?: boolean; } declare global { interface HASSDomEvents { - "entities-changed": { - entities: LovelaceRowConfig[]; - }; "config-changed": ConfigChangedEvent; "GUImode-changed": GUIModeChangedEvent; + "edit-detail-element": EditSubElementEvent; } } export interface UIConfigChangedEvent extends Event { detail: { - config: LovelaceCardConfig | LovelaceRowConfig; + config: LovelaceCardConfig | LovelaceRowConfig | LovelaceHeaderFooterConfig; }; } -const GENERIC_ROW_TYPE = "generic-row"; - -@customElement("hui-element-editor") -export class HuiElementEditor extends LitElement { +export abstract class HuiElementEditor extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public lovelace?: LovelaceConfig; - @property() public elementType: "row" | "card" = "card"; - @internalProperty() private _yaml?: string; - @internalProperty() private _config?: LovelaceCardConfig | LovelaceRowConfig; + @internalProperty() private _config?: T; - @internalProperty() private _configElement?: - | LovelaceCardEditor - | LovelaceRowEditor; + @internalProperty() private _configElement?: LovelaceGenericElementEditor; - @internalProperty() private _configElType?: string; + @internalProperty() private _configElementType?: string; @internalProperty() private _GUImode = true; @@ -108,11 +93,11 @@ export class HuiElementEditor extends LitElement { this._setConfig(); } - public get value(): LovelaceCardConfig | LovelaceRowConfig | undefined { + public get value(): T | undefined { return this._config; } - public set value(config: LovelaceCardConfig | LovelaceRowConfig | undefined) { + public set value(config: T | undefined) { if (this._config && deepEqual(config, this._config)) { return; } @@ -122,7 +107,7 @@ export class HuiElementEditor extends LitElement { this._setConfig(); } - private _setConfig() { + private _setConfig(): void { if (!this._error) { try { this._updateConfigElement(); @@ -131,8 +116,9 @@ export class HuiElementEditor extends LitElement { this._error = err.message; } } + fireEvent(this, "config-changed", { - config: this.value!, + config: this.value! as any, error: this._error, guiModeAvailable: !(this.hasWarning || this.hasError), }); @@ -175,6 +161,16 @@ export class HuiElementEditor extends LitElement { } } + protected async getConfigElement(): Promise< + LovelaceGenericElementEditor | undefined + > { + return undefined; + } + + protected get configElementType(): string | undefined { + return this.value ? (this.value as any).type : undefined; + } + protected render(): TemplateResult { return html`
@@ -228,8 +224,9 @@ export class HuiElementEditor extends LitElement { `; } - protected updated(changedProperties) { + protected updated(changedProperties: PropertyValues) { super.updated(changedProperties); + if (this._configElement && changedProperties.has("hass")) { this._configElement.hass = this.hass; } @@ -245,10 +242,10 @@ export class HuiElementEditor extends LitElement { private _handleUIConfigChanged(ev: UIConfigChangedEvent) { ev.stopPropagation(); const config = ev.detail.config; - this.value = config; + this.value = (config as unknown) as T; } - private _handleYAMLChanged(ev) { + private _handleYAMLChanged(ev: CustomEvent) { ev.stopPropagation(); const newYaml = ev.detail.value; if (newYaml !== this.yaml) { @@ -261,69 +258,43 @@ export class HuiElementEditor extends LitElement { return; } - let type: string; + let configElement: LovelaceGenericElementEditor | undefined; - if ( - this.elementType === "row" && - !this.value.type && - "entity" in this.value - ) { - type = GENERIC_ROW_TYPE; - } else { - type = this.value.type!; - } - - let configElement = this._configElement; try { this._error = undefined; this._warnings = undefined; - if (this._configElType !== type) { + if (this._configElementType !== this.configElementType) { // If the type has changed, we need to load a new GUI editor - if (!type) { - throw new Error(`No ${this.elementType} type defined`); + + if (!this.configElementType) { + throw new Error(`No type defined`); } - let elClass: - | LovelaceCardConstructor - | LovelaceRowConstructor - | undefined; - - if (this.elementType === "card") { - elClass = await getCardElementClass(type); - } else if (this.elementType === "row" && type !== GENERIC_ROW_TYPE) { - elClass = await getRowElementClass(type); - } + this._configElementType = this.configElementType; this._loading = true; - // Check if a GUI editor exists - if (elClass && elClass.getConfigElement) { - configElement = await elClass.getConfigElement(); - } else if (this.elementType === "row" && type === GENERIC_ROW_TYPE) { - configElement = document.createElement( - "hui-generic-entity-row-editor" + configElement = await this.getConfigElement(); + + if (!configElement) { + throw new Error( + `No visual editor available for: ${this.configElementType}` ); - } else { - configElement = undefined; - throw new GUISupportError(`No visual editor available for: ${type}`); } - this._configElement = configElement; - this._configElType = type; - - // Perform final setup - this._configElement.hass = this.hass; - if ("lovelace" in this._configElement) { - this._configElement.lovelace = this.lovelace; + configElement.hass = this.hass; + if ("lovelace" in configElement) { + configElement.lovelace = this.lovelace; } - this._configElement.addEventListener("config-changed", (ev) => + configElement.addEventListener("config-changed", (ev) => this._handleUIConfigChanged(ev as UIConfigChangedEvent) ); + + this._configElement = configElement; } // Setup GUI editor and check that it can handle the current config try { - // @ts-ignore this._configElement!.setConfig(this.value); } catch (err) { throw new GUISupportError( @@ -379,9 +350,3 @@ export class HuiElementEditor extends LitElement { `; } } - -declare global { - interface HTMLElementTagNameMap { - "hui-element-editor": HuiElementEditor; - } -} diff --git a/src/panels/lovelace/editor/hui-entities-card-row-editor.ts b/src/panels/lovelace/editor/hui-entities-card-row-editor.ts index 0e6a42bdb8..f24359cb42 100644 --- a/src/panels/lovelace/editor/hui-entities-card-row-editor.ts +++ b/src/panels/lovelace/editor/hui-entities-card-row-editor.ts @@ -25,6 +25,14 @@ import { sortableStyles } from "../../../resources/ha-sortable-style"; import { HomeAssistant } from "../../../types"; import { EntityConfig, LovelaceRowConfig } from "../entity-rows/types"; +declare global { + interface HASSDomEvents { + "entities-changed": { + entities: LovelaceRowConfig[]; + }; + } +} + @customElement("hui-entities-card-row-editor") export class HuiEntitiesCardRowEditor extends LitElement { @property({ attribute: false }) protected hass?: HomeAssistant; @@ -232,8 +240,13 @@ export class HuiEntitiesCardRowEditor extends LitElement { } private _editRow(ev: CustomEvent): void { - fireEvent(this, "edit-row", { - index: (ev.currentTarget as any).index, + const index = (ev.currentTarget as any).index; + fireEvent(this, "edit-detail-element", { + subElementConfig: { + index, + type: "row", + elementConfig: this.entities![index], + }, }); } diff --git a/src/panels/lovelace/editor/hui-sub-element-editor.ts b/src/panels/lovelace/editor/hui-sub-element-editor.ts new file mode 100644 index 0000000000..c382ed0570 --- /dev/null +++ b/src/panels/lovelace/editor/hui-sub-element-editor.ts @@ -0,0 +1,132 @@ +import "@material/mwc-button"; +import "@material/mwc-icon-button"; +import { mdiArrowLeft } from "@mdi/js"; +import { + css, + CSSResult, + customElement, + html, + internalProperty, + LitElement, + property, + query, + TemplateResult, +} from "lit-element"; +import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event"; +import "../../../components/ha-svg-icon"; +import type { HomeAssistant } from "../../../types"; +import type { LovelaceRowConfig } from "../entity-rows/types"; +import type { LovelaceHeaderFooterConfig } from "../header-footer/types"; +import "./entity-row-editor/hui-row-element-editor"; +import "./header-footer-editor/hui-header-footer-element-editor"; +import type { HuiElementEditor } from "./hui-element-editor"; +import type { GUIModeChangedEvent, SubElementEditorConfig } from "./types"; + +declare global { + interface HASSDomEvents { + "go-back": undefined; + } +} + +@customElement("hui-sub-element-editor") +export class HuiSubElementEditor extends LitElement { + public hass!: HomeAssistant; + + @property({ attribute: false }) public config!: SubElementEditorConfig; + + @internalProperty() private _guiModeAvailable = true; + + @internalProperty() private _guiMode = true; + + @query(".editor") private _editorElement?: HuiElementEditor< + LovelaceRowConfig | LovelaceHeaderFooterConfig + >; + + protected render(): TemplateResult { + return html` +
+
+ + + + ${this.hass.localize( + `ui.panel.lovelace.editor.sub-element-editor.types.${this.config?.type}` + )} +
+ + ${this.hass.localize( + this._guiMode + ? "ui.panel.lovelace.editor.edit_card.show_code_editor" + : "ui.panel.lovelace.editor.edit_card.show_visual_editor" + )} + +
+ ${this.config.type === "row" + ? html` + + ` + : this.config.type === "header" || this.config.type === "footer" + ? html` + + ` + : ""} + `; + } + + private _goBack(): void { + fireEvent(this, "go-back"); + } + + private _toggleMode(): void { + this._editorElement?.toggleMode(); + } + + private _handleGUIModeChanged(ev: HASSDomEvent): void { + ev.stopPropagation(); + this._guiMode = ev.detail.guiMode; + this._guiModeAvailable = ev.detail.guiModeAvailable; + } + + private _handleConfigChanged(ev: CustomEvent): void { + this._guiModeAvailable = ev.detail.guiModeAvailable; + } + + static get styles(): CSSResult { + return css` + .header { + display: flex; + justify-content: space-between; + align-items: center; + } + .back-title { + display: flex; + align-items: center; + font-size: 18px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-sub-element-editor": HuiSubElementEditor; + } +} diff --git a/src/panels/lovelace/editor/lovelace-headerfooters.ts b/src/panels/lovelace/editor/lovelace-headerfooters.ts new file mode 100644 index 0000000000..0aa0ccc8a1 --- /dev/null +++ b/src/panels/lovelace/editor/lovelace-headerfooters.ts @@ -0,0 +1,12 @@ +import { + mdiChartBellCurveCumulative, + mdiGestureTapButton, + mdiImageArea, +} from "@mdi/js"; +import { HeaderFooter } from "./types"; + +export const headerFooterElements: HeaderFooter[] = [ + { type: "graph", icon: mdiChartBellCurveCumulative }, + { type: "buttons", icon: mdiGestureTapButton }, + { type: "picture", icon: mdiImageArea }, +]; diff --git a/src/panels/lovelace/editor/types.ts b/src/panels/lovelace/editor/types.ts index 51edca3cfe..3f657062a6 100644 --- a/src/panels/lovelace/editor/types.ts +++ b/src/panels/lovelace/editor/types.ts @@ -14,7 +14,8 @@ import { LovelaceViewConfig, ShowViewConfig, } from "../../../data/lovelace"; -import { EntityConfig } from "../entity-rows/types"; +import { EntityConfig, LovelaceRowConfig } from "../entity-rows/types"; +import { LovelaceHeaderFooterConfig } from "../header-footer/types"; export interface YamlChangedEvent extends Event { detail: { @@ -72,10 +73,25 @@ export interface Card { isCustom?: boolean; } +export interface HeaderFooter { + type: string; + icon?: string; +} + export interface CardPickTarget extends EventTarget { config: LovelaceCardConfig; } +export interface SubElementEditorConfig { + index?: number; + elementConfig?: LovelaceRowConfig | LovelaceHeaderFooterConfig; + type: string; +} + +export interface EditSubElementEvent { + subElementConfig: SubElementEditorConfig; +} + export const actionConfigStruct = object({ action: string(), navigation_path: optional(string()), diff --git a/src/panels/lovelace/header-footer/hui-graph-header-footer.ts b/src/panels/lovelace/header-footer/hui-graph-header-footer.ts index 30fda681c4..9e9132c182 100644 --- a/src/panels/lovelace/header-footer/hui-graph-header-footer.ts +++ b/src/panels/lovelace/header-footer/hui-graph-header-footer.ts @@ -13,10 +13,11 @@ import { import "../../../components/ha-circular-progress"; import { fetchRecent } from "../../../data/history"; import { HomeAssistant } from "../../../types"; +import { findEntities } from "../common/find-entites"; import { coordinates } from "../common/graph/coordinates"; import { hasConfigOrEntityChanged } from "../common/has-changed"; import "../components/hui-graph-base"; -import { LovelaceHeaderFooter } from "../types"; +import { LovelaceHeaderFooter, LovelaceHeaderFooterEditor } from "../types"; import { GraphHeaderFooterConfig } from "./types"; const MINUTE = 60000; @@ -25,8 +26,40 @@ const DAY = 86400000; @customElement("hui-graph-header-footer") export class HuiGraphHeaderFooter extends LitElement implements LovelaceHeaderFooter { - public static getStubConfig(): Record { - return {}; + public static async getConfigElement(): Promise { + await import( + /* webpackChunkName: "hui-graph-footer-editor" */ "../editor/config-elements/hui-graph-footer-editor" + ); + return document.createElement("hui-graph-footer-editor"); + } + + public static getStubConfig( + hass: HomeAssistant, + entities: string[], + entitiesFallback: string[] + ): GraphHeaderFooterConfig { + const includeDomains = ["sensor"]; + const maxEntities = 1; + const entityFilter = (stateObj: HassEntity): boolean => { + return ( + !isNaN(Number(stateObj.state)) && + !!stateObj.attributes.unit_of_measurement + ); + }; + + const foundEntities = findEntities( + hass, + maxEntities, + entities, + entitiesFallback, + includeDomains, + entityFilter + ); + + return { + type: "graph", + entity: foundEntities[0] || "", + }; } @property({ attribute: false }) public hass?: HomeAssistant; diff --git a/src/panels/lovelace/types.ts b/src/panels/lovelace/types.ts index b6e52a3d1c..437fd58fa2 100644 --- a/src/panels/lovelace/types.ts +++ b/src/panels/lovelace/types.ts @@ -49,6 +49,16 @@ export interface LovelaceCardConstructor extends Constructor { getConfigElement?: () => LovelaceCardEditor; } +export interface LovelaceHeaderFooterConstructor + extends Constructor { + getStubConfig?: ( + hass: HomeAssistant, + entities: string[], + entitiesFallback: string[] + ) => LovelaceHeaderFooterConfig; + getConfigElement?: () => LovelaceHeaderFooterEditor; +} + export interface LovelaceRowConstructor extends Constructor { getConfigElement?: () => LovelaceRowEditor; } @@ -59,15 +69,22 @@ export interface LovelaceHeaderFooter extends HTMLElement { setConfig(config: LovelaceHeaderFooterConfig): void; } -export interface LovelaceCardEditor extends HTMLElement { - hass?: HomeAssistant; - lovelace?: LovelaceConfig; +export interface LovelaceCardEditor extends LovelaceGenericElementEditor { setConfig(config: LovelaceCardConfig): void; - refreshYamlEditor?: (focus: boolean) => void; } -export interface LovelaceRowEditor extends HTMLElement { - hass?: HomeAssistant; +export interface LovelaceHeaderFooterEditor + extends LovelaceGenericElementEditor { + setConfig(config: LovelaceHeaderFooterConfig): void; +} + +export interface LovelaceRowEditor extends LovelaceGenericElementEditor { setConfig(config: LovelaceRowConfig): void; +} + +export interface LovelaceGenericElementEditor extends HTMLElement { + hass?: HomeAssistant; + lovelace?: LovelaceConfig; + setConfig(config: any): void; refreshYamlEditor?: (focus: boolean) => void; } diff --git a/src/translations/en.json b/src/translations/en.json index ecc1c19390..240f4a4438 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2369,6 +2369,12 @@ "open": "Open Lovelace UI menu", "raw_editor": "Raw configuration editor" }, + "common": { + "edit": "Edit", + "clear": "Clear", + "add": "Add", + "none": "None" + }, "raw_editor": { "header": "Edit Configuration", "save": "Save", @@ -2420,6 +2426,7 @@ "show_code_editor": "Show Code Editor", "add": "Add Card", "edit": "Edit", + "clear": "Clear", "delete": "Delete card", "duplicate": "Duplicate card", "move": "Move to View", @@ -2696,6 +2703,29 @@ "entity": "Entity", "by_entity": "By Entity", "by_card": "By Card" + }, + "header-footer": { + "header": "Header", + "footer": "Footer", + "choose_header_footer": "Choose a {type}", + "types": { + "graph": { + "name": "Graph" + }, + "picture": { + "name": "Picture" + }, + "buttons": { + "name": "Buttons" + } + } + }, + "sub-element-editor": { + "types": { + "header": "Header Editor", + "footer": "Footer Editor", + "row": "Entity Row Editor" + } } }, "warning": {