diff --git a/src/common/util/deep-equal.ts b/src/common/util/deep-equal.ts index 780aef6952..97c0f1e3e9 100644 --- a/src/common/util/deep-equal.ts +++ b/src/common/util/deep-equal.ts @@ -1,6 +1,16 @@ // From https://github.com/epoberezkin/fast-deep-equal // MIT License - Copyright (c) 2017 Evgeny Poberezkin -export const deepEqual = (a: any, b: any): boolean => { + +interface DeepEqualOptions { + /** Compare Symbol properties in addition to string keys */ + compareSymbols?: boolean; +} + +export const deepEqual = ( + a: any, + b: any, + options?: DeepEqualOptions +): boolean => { if (a === b) { return true; } @@ -18,7 +28,7 @@ export const deepEqual = (a: any, b: any): boolean => { return false; } for (i = length; i-- !== 0; ) { - if (!deepEqual(a[i], b[i])) { + if (!deepEqual(a[i], b[i], options)) { return false; } } @@ -35,7 +45,7 @@ export const deepEqual = (a: any, b: any): boolean => { } } for (i of a.entries()) { - if (!deepEqual(i[1], b.get(i[0]))) { + if (!deepEqual(i[1], b.get(i[0]), options)) { return false; } } @@ -93,11 +103,28 @@ export const deepEqual = (a: any, b: any): boolean => { for (i = length; i-- !== 0; ) { const key = keys[i]; - if (!deepEqual(a[key], b[key])) { + if (!deepEqual(a[key], b[key], options)) { return false; } } + // Compare Symbol properties if requested + if (options?.compareSymbols) { + const symbolsA = Object.getOwnPropertySymbols(a); + const symbolsB = Object.getOwnPropertySymbols(b); + if (symbolsA.length !== symbolsB.length) { + return false; + } + for (const sym of symbolsA) { + if (!Object.prototype.hasOwnProperty.call(b, sym)) { + return false; + } + if (!deepEqual(a[sym], b[sym], options)) { + return false; + } + } + } + return true; } diff --git a/src/panels/lovelace/cards/hui-picture-elements-card.ts b/src/panels/lovelace/cards/hui-picture-elements-card.ts index adbfc82f7d..f63b1e587b 100644 --- a/src/panels/lovelace/cards/hui-picture-elements-card.ts +++ b/src/panels/lovelace/cards/hui-picture-elements-card.ts @@ -11,7 +11,10 @@ import { findEntities } from "../common/find-entities"; import type { LovelaceElement, LovelaceElementConfig } from "../elements/types"; import type { LovelaceCard, LovelaceCardEditor } from "../types"; import { createStyledHuiElement } from "./picture-elements/create-styled-hui-element"; -import type { PictureElementsCardConfig } from "./types"; +import { + PREVIEW_CLICK_CALLBACK, + type PictureElementsCardConfig, +} from "./types"; import type { PersonEntity } from "../../../data/person"; @customElement("hui-picture-elements-card") @@ -166,6 +169,7 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard { .aspectRatio=${this._config.aspect_ratio} .darkModeFilter=${this._config.dark_mode_filter} .darkModeImage=${darkModeImage} + @click=${this._handleImageClick} > ${this._elements} @@ -221,6 +225,19 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard { curCardEl === elToReplace ? newCardEl : curCardEl ); } + + private _handleImageClick(ev: MouseEvent): void { + if (!this.preview || !this._config?.[PREVIEW_CLICK_CALLBACK]) { + return; + } + + const rect = (ev.currentTarget as HTMLElement).getBoundingClientRect(); + const x = ((ev.clientX - rect.left) / rect.width) * 100; + const y = ((ev.clientY - rect.top) / rect.height) * 100; + + // only the edited card has this callback + this._config[PREVIEW_CLICK_CALLBACK](x, y); + } } declare global { diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index bff6c75a51..6a3ad77c8a 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -487,6 +487,10 @@ export interface PictureCardConfig extends LovelaceCardConfig { alt_text?: string; } +// Symbol for preview click callback - preserved through spreads, not serialized +// This allows the editor to attach a callback that only exists on the edited card's config +export const PREVIEW_CLICK_CALLBACK = Symbol("previewClickCallback"); + export interface PictureElementsCardConfig extends LovelaceCardConfig { title?: string; image?: string | MediaSelectorValue; @@ -501,6 +505,7 @@ export interface PictureElementsCardConfig extends LovelaceCardConfig { theme?: string; dark_mode_image?: string | MediaSelectorValue; dark_mode_filter?: string; + [PREVIEW_CLICK_CALLBACK]?: (x: number, y: number) => void; } export interface PictureEntityCardConfig extends LovelaceCardConfig { 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 index 1abe7e16fa..ef38779acf 100644 --- 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 @@ -15,12 +15,16 @@ import { } from "superstruct"; import type { HASSDomEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-alert"; 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 { + PREVIEW_CLICK_CALLBACK, + type PictureElementsCardConfig, +} from "../../cards/types"; import type { LovelaceCardEditor } from "../../types"; import "../hui-sub-element-editor"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; @@ -28,7 +32,6 @@ import type { EditDetailElementEvent, SubElementEditorConfig } from "../types"; import { configElementStyle } from "./config-elements-style"; import "../hui-picture-elements-card-row-editor"; import type { LovelaceElementConfig } from "../../elements/types"; -import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card"; import type { LocalizeFunc } from "../../../../common/translations/localize"; const genericElementConfigStruct = type({ @@ -66,6 +69,44 @@ export class HuiPictureElementsCardEditor this._config = config; } + private _onPreviewClick = (x: number, y: number): void => { + if (this._subElementEditorConfig?.type === "element") { + this._handlePositionClick(x, y); + } + }; + + private _handlePositionClick(x: number, y: number): void { + if ( + !this._subElementEditorConfig?.elementConfig || + this._subElementEditorConfig.type !== "element" || + this._subElementEditorConfig.elementConfig.type === "conditional" + ) { + return; + } + + const elementConfig = this._subElementEditorConfig + .elementConfig as LovelaceElementConfig; + const currentPosition = (elementConfig.style as Record) + ?.position; + if (currentPosition && currentPosition !== "absolute") { + return; + } + + const newElement = { + ...elementConfig, + style: { + ...((elementConfig.style as Record) || {}), + left: `${Math.round(x)}%`, + top: `${Math.round(y)}%`, + }, + }; + + const updateEvent = new CustomEvent("config-changed", { + detail: { config: newElement }, + }); + this._handleSubElementChanged(updateEvent); + } + private _schema = memoizeOne( (localize: LocalizeFunc) => [ @@ -138,6 +179,16 @@ export class HuiPictureElementsCardEditor if (this._subElementEditorConfig) { return html` + ${this._subElementEditorConfig.type === "element" && + this._subElementEditorConfig.elementConfig?.type !== "conditional" + ? html` + + ${this.hass.localize( + "ui.panel.lovelace.editor.card.picture-elements.position_hint" + )} + + ` + : nothing} ): void { diff --git a/src/panels/lovelace/editor/get-element-stub-config.ts b/src/panels/lovelace/editor/get-element-stub-config.ts index 46516c7698..fe5bd784f4 100644 --- a/src/panels/lovelace/editor/get-element-stub-config.ts +++ b/src/panels/lovelace/editor/get-element-stub-config.ts @@ -10,7 +10,9 @@ export const getElementStubConfig = async ( ): Promise => { let elementConfig: LovelaceElementConfig = { type }; - if (type !== "conditional") { + if (type === "conditional") { + elementConfig = { type, conditions: [], elements: [] }; + } else { elementConfig.style = { left: "50%", top: "50%" }; } diff --git a/src/panels/lovelace/editor/hui-element-editor.ts b/src/panels/lovelace/editor/hui-element-editor.ts index 49fdd255f9..28b53e7924 100644 --- a/src/panels/lovelace/editor/hui-element-editor.ts +++ b/src/panels/lovelace/editor/hui-element-editor.ts @@ -89,7 +89,11 @@ export abstract class HuiElementEditor< } public set value(config: T | undefined) { - if (this._config && deepEqual(config, this._config)) { + // Compare symbols to detect callback changes (e.g., preview click handlers) + if ( + this._config && + deepEqual(config, this._config, { compareSymbols: true }) + ) { return; } this._config = config; diff --git a/src/translations/en.json b/src/translations/en.json index e106d58875..acd50ecfdc 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -8326,6 +8326,7 @@ "dark_mode_image": "Dark mode image path", "state_filter": "State filter", "dark_mode_filter": "Dark mode state filter", + "position_hint": "Click on the image preview to position this element", "element_types": { "state-badge": "State badge", "state-icon": "State icon",