diff --git a/src/components/ha-form/ha-form-optional_actions.ts b/src/components/ha-form/ha-form-optional_actions.ts new file mode 100644 index 0000000000..30f2fcec7f --- /dev/null +++ b/src/components/ha-form/ha-form-optional_actions.ts @@ -0,0 +1,166 @@ +import type { PropertyValues, TemplateResult } from "lit"; +import { css, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { stopPropagation } from "../../common/dom/stop_propagation"; +import type { LocalizeFunc } from "../../common/translations/localize"; +import type { HomeAssistant } from "../../types"; +import "./ha-form"; +import type { + HaFormOptionalActionsSchema, + HaFormDataContainer, + HaFormElement, + HaFormSchema, +} from "./types"; + +const NO_ACTIONS = []; + +@customElement("ha-form-optional_actions") +export class HaFormOptionalActions extends LitElement implements HaFormElement { + @property({ attribute: false }) public localize?: LocalizeFunc; + + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public data!: HaFormDataContainer; + + @property({ attribute: false }) public schema!: HaFormOptionalActionsSchema; + + @property({ type: Boolean }) public disabled = false; + + @property({ attribute: false }) public computeLabel?: ( + schema: HaFormSchema, + data?: HaFormDataContainer + ) => string; + + @property({ attribute: false }) public computeHelper?: ( + schema: HaFormSchema + ) => string; + + @property({ attribute: false }) public localizeValue?: ( + key: string + ) => string; + + @state() private _displayActions?: string[]; + + public async focus() { + await this.updateComplete; + this.renderRoot.querySelector("ha-form")?.focus(); + } + + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + if (changedProps.has("data")) { + const displayActions = this._displayActions ?? NO_ACTIONS; + const hiddenActions = this._hiddenActions( + this.schema.schema, + displayActions + ); + this._displayActions = [ + ...displayActions, + ...hiddenActions.filter((name) => name in this.data), + ]; + } + } + + private _hiddenActions = memoizeOne( + (schema: readonly HaFormSchema[], displayActions: string[]): string[] => + schema + .map((item) => item.name) + .filter((name) => !displayActions.includes(name)) + ); + + private _displaySchema = memoizeOne( + ( + schema: readonly HaFormSchema[], + displayActions: string[] + ): HaFormSchema[] => + schema.filter((item) => displayActions.includes(item.name)) + ); + + public render(): TemplateResult { + const displayActions = this._displayActions ?? NO_ACTIONS; + + const schema = this._displaySchema( + this.schema.schema, + this._displayActions ?? [] + ); + + const hiddenActions = this._hiddenActions( + this.schema.schema, + displayActions + ); + + const schemaMap = new Map( + this.computeLabel + ? this.schema.schema.map((item) => [item.name, item]) + : [] + ); + + return html` + ${schema.length > 0 + ? html` + + ` + : nothing} + ${hiddenActions.length > 0 + ? html` + + + ${this.localize?.("ui.components.form-optional-actions.add") || + "Add interaction"} + + ${hiddenActions.map((action) => { + const actionSchema = schemaMap.get(action); + return html` + + ${this.computeLabel && actionSchema + ? this.computeLabel(actionSchema) + : action} + + `; + })} + + ` + : nothing} + `; + } + + private _handleAddAction(ev: CustomEvent) { + const hiddenActions = this._hiddenActions( + this.schema.schema, + this._displayActions ?? NO_ACTIONS + ); + const index = ev.detail.index; + const action = hiddenActions[index]; + this._displayActions = [...(this._displayActions ?? []), action]; + } + + static styles = css` + :host { + display: flex !important; + flex-direction: column; + gap: 24px; + } + :host ha-form { + display: block; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-form-optional_actions": HaFormOptionalActions; + } +} diff --git a/src/components/ha-form/ha-form.ts b/src/components/ha-form/ha-form.ts index 258f5cc104..57c796312d 100644 --- a/src/components/ha-form/ha-form.ts +++ b/src/components/ha-form/ha-form.ts @@ -20,6 +20,7 @@ const LOAD_ELEMENTS = { import("./ha-form-positive_time_period_dict"), select: () => import("./ha-form-select"), string: () => import("./ha-form-string"), + optional_actions: () => import("./ha-form-optional_actions"), }; const getValue = (obj, item) => diff --git a/src/components/ha-form/types.ts b/src/components/ha-form/types.ts index cab1faa31c..550a533db9 100644 --- a/src/components/ha-form/types.ts +++ b/src/components/ha-form/types.ts @@ -13,7 +13,8 @@ export type HaFormSchema = | HaFormTimeSchema | HaFormSelector | HaFormGridSchema - | HaFormExpandableSchema; + | HaFormExpandableSchema + | HaFormOptionalActionsSchema; export interface HaFormBaseSchema { name: string; @@ -47,6 +48,12 @@ export interface HaFormExpandableSchema extends HaFormBaseSchema { schema: readonly HaFormSchema[]; } +export interface HaFormOptionalActionsSchema extends HaFormBaseSchema { + type: "optional_actions"; + flatten?: boolean; + schema: readonly HaFormSchema[]; +} + export interface HaFormSelector extends HaFormBaseSchema { type?: never; selector: Selector; @@ -100,7 +107,10 @@ export interface HaFormTimeSchema extends HaFormBaseSchema { export type SchemaUnion< SchemaArray extends readonly HaFormSchema[], Schema = SchemaArray[number], -> = Schema extends HaFormGridSchema | HaFormExpandableSchema +> = Schema extends + | HaFormGridSchema + | HaFormExpandableSchema + | HaFormOptionalActionsSchema ? SchemaUnion | Schema : Schema; diff --git a/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts b/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts index 6732fda5e5..f98e9dcbb1 100644 --- a/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts @@ -48,6 +48,8 @@ const badgeConfigStruct = assign( show_icon: optional(boolean()), show_entity_picture: optional(boolean()), tap_action: optional(actionConfigStruct), + hold_action: optional(actionConfigStruct), + double_tap_action: optional(actionConfigStruct), image: optional(string()), // For old badge config support }) ); @@ -169,6 +171,21 @@ export class HuiEntityBadgeEditor }, }, }, + { + name: "", + type: "optional_actions", + flatten: true, + schema: (["hold_action", "double_tap_action"] as const).map( + (action) => ({ + name: action, + selector: { + ui_action: { + default_action: "none" as const, + }, + }, + }) + ), + }, ], }, ] as const satisfies readonly HaFormSchema[] diff --git a/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts index ba834a00d7..3fe5cc938f 100644 --- a/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-tile-card-editor.ts @@ -1,4 +1,5 @@ import { mdiGestureTap, mdiListBox, mdiTextShort } from "@mdi/js"; +import type { HassEntity } from "home-assistant-js-websocket"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; @@ -14,7 +15,6 @@ import { string, union, } from "superstruct"; -import type { HassEntity } from "home-assistant-js-websocket"; import type { HASSDomEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event"; import type { LocalizeFunc } from "../../../../common/translations/localize"; @@ -61,15 +61,6 @@ const cardConfigStruct = assign( }) ); -const ADVANCED_ACTIONS = [ - "hold_action", - "icon_hold_action", - "double_tap_action", - "icon_double_tap_action", -] as const; - -type AdvancedActions = (typeof ADVANCED_ACTIONS)[number]; - @customElement("hui-tile-card-editor") export class HuiTileCardEditor extends LitElement @@ -79,44 +70,16 @@ export class HuiTileCardEditor @state() private _config?: TileCardConfig; - @state() private _displayActions?: AdvancedActions[]; - public setConfig(config: TileCardConfig): void { assert(config, cardConfigStruct); this._config = config; - - if (this._displayActions) return; - this._setDisplayActions(config); - } - - private _setDisplayActions(config: TileCardConfig) { - this._displayActions = ADVANCED_ACTIONS.filter( - (action) => action in config - ); - } - - private _resetConfiguredActions() { - this._displayActions = undefined; - } - - connectedCallback(): void { - super.connectedCallback(); - if (this._config) { - this._setDisplayActions(this._config); - } - } - - disconnectedCallback(): void { - super.disconnectedCallback(); - this._resetConfiguredActions(); } private _schema = memoizeOne( ( localize: LocalizeFunc, entityId: string | undefined, - hideState: boolean, - displayActions: AdvancedActions[] = [] + hideState: boolean ) => [ { name: "entity", selector: { entity: {} } }, @@ -220,14 +183,26 @@ export class HuiTileCardEditor }, }, }, - ...displayActions.map((action) => ({ - name: action, - selector: { - ui_action: { - default_action: "none" as const, + { + name: "", + type: "optional_actions", + flatten: true, + schema: ( + [ + "hold_action", + "icon_hold_action", + "double_tap_action", + "icon_double_tap_action", + ] as const + ).map((action) => ({ + name: action, + selector: { + ui_action: { + default_action: "none" as const, + }, }, - }, - })), + })), + }, ], }, ] as const satisfies readonly HaFormSchema[] @@ -278,8 +253,7 @@ export class HuiTileCardEditor const schema = this._schema( this.hass.localize, entityId, - this._config.hide_state ?? false, - this._displayActions + this._config.hide_state ?? false ); const featuresSchema = this._featuresSchema( diff --git a/src/panels/lovelace/editor/heading-badge-editor/hui-entity-heading-badge-editor.ts b/src/panels/lovelace/editor/heading-badge-editor/hui-entity-heading-badge-editor.ts index 3a0000c7c0..19c607d6f1 100644 --- a/src/panels/lovelace/editor/heading-badge-editor/hui-entity-heading-badge-editor.ts +++ b/src/panels/lovelace/editor/heading-badge-editor/hui-entity-heading-badge-editor.ts @@ -156,6 +156,21 @@ export class HuiHeadingEntityEditor }, }, }, + { + name: "", + type: "optional_actions", + flatten: true, + schema: (["hold_action", "double_tap_action"] as const).map( + (action) => ({ + name: action, + selector: { + ui_action: { + default_action: "none" as const, + }, + }, + }) + ), + }, ], }, ] as const satisfies readonly HaFormSchema[] diff --git a/src/panels/lovelace/heading-badges/hui-entity-heading-badge.ts b/src/panels/lovelace/heading-badges/hui-entity-heading-badge.ts index f4310de419..7779e3a6e2 100644 --- a/src/panels/lovelace/heading-badges/hui-entity-heading-badge.ts +++ b/src/panels/lovelace/heading-badges/hui-entity-heading-badge.ts @@ -25,6 +25,14 @@ import type { } from "../types"; import type { EntityHeadingBadgeConfig } from "./types"; +const DEFAULT_ACTIONS: Pick< + EntityHeadingBadgeConfig, + "tap_action" | "hold_action" | "double_tap_action" +> = { + tap_action: { action: "none" }, + hold_action: { action: "none" }, + double_tap_action: { action: "none" }, +}; @customElement("hui-entity-heading-badge") export class HuiEntityHeadingBadge extends LitElement @@ -46,33 +54,21 @@ export class HuiEntityHeadingBadge public setConfig(config): void { this._config = { ...DEFAULT_CONFIG, - tap_action: { - action: "none", - }, - hold_action: { - action: "none", - }, - double_tap_action: { - action: "none", - }, + ...DEFAULT_ACTIONS, ...config, }; } + get hasAction() { + return ( + hasAction(this._config?.tap_action) || + hasAction(this._config?.hold_action) || + hasAction(this._config?.double_tap_action) + ); + } + private _handleAction(ev: ActionHandlerEvent) { - const config: EntityHeadingBadgeConfig = { - tap_action: { - action: "none", - }, - hold_action: { - action: "none", - }, - double_tap_action: { - action: "none", - }, - ...this._config!, - }; - handleAction(this, this.hass!, config, ev.detail.action!); + handleAction(this, this.hass!, this._config!, ev.detail.action!); } private _computeStateColor = memoizeOne( @@ -145,7 +141,7 @@ export class HuiEntityHeadingBadge return html`