Skip to content

Commit 9ce9d25

Browse files
authored
Picture elements position by click (#28597)
1 parent 1beca4b commit 9ce9d25

File tree

7 files changed

+125
-11
lines changed

7 files changed

+125
-11
lines changed

src/common/util/deep-equal.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
// From https://github.com/epoberezkin/fast-deep-equal
22
// MIT License - Copyright (c) 2017 Evgeny Poberezkin
3-
export const deepEqual = (a: any, b: any): boolean => {
3+
4+
interface DeepEqualOptions {
5+
/** Compare Symbol properties in addition to string keys */
6+
compareSymbols?: boolean;
7+
}
8+
9+
export const deepEqual = (
10+
a: any,
11+
b: any,
12+
options?: DeepEqualOptions
13+
): boolean => {
414
if (a === b) {
515
return true;
616
}
@@ -18,7 +28,7 @@ export const deepEqual = (a: any, b: any): boolean => {
1828
return false;
1929
}
2030
for (i = length; i-- !== 0; ) {
21-
if (!deepEqual(a[i], b[i])) {
31+
if (!deepEqual(a[i], b[i], options)) {
2232
return false;
2333
}
2434
}
@@ -35,7 +45,7 @@ export const deepEqual = (a: any, b: any): boolean => {
3545
}
3646
}
3747
for (i of a.entries()) {
38-
if (!deepEqual(i[1], b.get(i[0]))) {
48+
if (!deepEqual(i[1], b.get(i[0]), options)) {
3949
return false;
4050
}
4151
}
@@ -93,9 +103,26 @@ export const deepEqual = (a: any, b: any): boolean => {
93103
for (i = length; i-- !== 0; ) {
94104
const key = keys[i];
95105

96-
if (!deepEqual(a[key], b[key])) {
106+
if (!deepEqual(a[key], b[key], options)) {
107+
return false;
108+
}
109+
}
110+
111+
// Compare Symbol properties if requested
112+
if (options?.compareSymbols) {
113+
const symbolsA = Object.getOwnPropertySymbols(a);
114+
const symbolsB = Object.getOwnPropertySymbols(b);
115+
if (symbolsA.length !== symbolsB.length) {
97116
return false;
98117
}
118+
for (const sym of symbolsA) {
119+
if (!Object.prototype.hasOwnProperty.call(b, sym)) {
120+
return false;
121+
}
122+
if (!deepEqual(a[sym], b[sym], options)) {
123+
return false;
124+
}
125+
}
99126
}
100127

101128
return true;

src/panels/lovelace/cards/hui-picture-elements-card.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import { findEntities } from "../common/find-entities";
1111
import type { LovelaceElement, LovelaceElementConfig } from "../elements/types";
1212
import type { LovelaceCard, LovelaceCardEditor } from "../types";
1313
import { createStyledHuiElement } from "./picture-elements/create-styled-hui-element";
14-
import type { PictureElementsCardConfig } from "./types";
14+
import {
15+
PREVIEW_CLICK_CALLBACK,
16+
type PictureElementsCardConfig,
17+
} from "./types";
1518
import type { PersonEntity } from "../../../data/person";
1619

1720
@customElement("hui-picture-elements-card")
@@ -166,6 +169,7 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
166169
.aspectRatio=${this._config.aspect_ratio}
167170
.darkModeFilter=${this._config.dark_mode_filter}
168171
.darkModeImage=${darkModeImage}
172+
@click=${this._handleImageClick}
169173
></hui-image>
170174
${this._elements}
171175
</div>
@@ -221,6 +225,19 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
221225
curCardEl === elToReplace ? newCardEl : curCardEl
222226
);
223227
}
228+
229+
private _handleImageClick(ev: MouseEvent): void {
230+
if (!this.preview || !this._config?.[PREVIEW_CLICK_CALLBACK]) {
231+
return;
232+
}
233+
234+
const rect = (ev.currentTarget as HTMLElement).getBoundingClientRect();
235+
const x = ((ev.clientX - rect.left) / rect.width) * 100;
236+
const y = ((ev.clientY - rect.top) / rect.height) * 100;
237+
238+
// only the edited card has this callback
239+
this._config[PREVIEW_CLICK_CALLBACK](x, y);
240+
}
224241
}
225242

226243
declare global {

src/panels/lovelace/cards/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,10 @@ export interface PictureCardConfig extends LovelaceCardConfig {
487487
alt_text?: string;
488488
}
489489

490+
// Symbol for preview click callback - preserved through spreads, not serialized
491+
// This allows the editor to attach a callback that only exists on the edited card's config
492+
export const PREVIEW_CLICK_CALLBACK = Symbol("previewClickCallback");
493+
490494
export interface PictureElementsCardConfig extends LovelaceCardConfig {
491495
title?: string;
492496
image?: string | MediaSelectorValue;
@@ -501,6 +505,7 @@ export interface PictureElementsCardConfig extends LovelaceCardConfig {
501505
theme?: string;
502506
dark_mode_image?: string | MediaSelectorValue;
503507
dark_mode_filter?: string;
508+
[PREVIEW_CLICK_CALLBACK]?: (x: number, y: number) => void;
504509
}
505510

506511
export interface PictureEntityCardConfig extends LovelaceCardConfig {

src/panels/lovelace/editor/config-elements/hui-picture-elements-card-editor.ts

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,23 @@ import {
1515
} from "superstruct";
1616
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
1717
import { fireEvent } from "../../../../common/dom/fire_event";
18+
import "../../../../components/ha-alert";
1819
import "../../../../components/ha-card";
1920
import "../../../../components/ha-form/ha-form";
2021
import "../../../../components/ha-icon";
2122
import "../../../../components/ha-switch";
2223
import type { HomeAssistant } from "../../../../types";
23-
import type { PictureElementsCardConfig } from "../../cards/types";
24+
import {
25+
PREVIEW_CLICK_CALLBACK,
26+
type PictureElementsCardConfig,
27+
} from "../../cards/types";
2428
import type { LovelaceCardEditor } from "../../types";
2529
import "../hui-sub-element-editor";
2630
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
2731
import type { EditDetailElementEvent, SubElementEditorConfig } from "../types";
2832
import { configElementStyle } from "./config-elements-style";
2933
import "../hui-picture-elements-card-row-editor";
3034
import type { LovelaceElementConfig } from "../../elements/types";
31-
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
3235
import type { LocalizeFunc } from "../../../../common/translations/localize";
3336

3437
const genericElementConfigStruct = type({
@@ -66,6 +69,44 @@ export class HuiPictureElementsCardEditor
6669
this._config = config;
6770
}
6871

72+
private _onPreviewClick = (x: number, y: number): void => {
73+
if (this._subElementEditorConfig?.type === "element") {
74+
this._handlePositionClick(x, y);
75+
}
76+
};
77+
78+
private _handlePositionClick(x: number, y: number): void {
79+
if (
80+
!this._subElementEditorConfig?.elementConfig ||
81+
this._subElementEditorConfig.type !== "element" ||
82+
this._subElementEditorConfig.elementConfig.type === "conditional"
83+
) {
84+
return;
85+
}
86+
87+
const elementConfig = this._subElementEditorConfig
88+
.elementConfig as LovelaceElementConfig;
89+
const currentPosition = (elementConfig.style as Record<string, string>)
90+
?.position;
91+
if (currentPosition && currentPosition !== "absolute") {
92+
return;
93+
}
94+
95+
const newElement = {
96+
...elementConfig,
97+
style: {
98+
...((elementConfig.style as Record<string, string>) || {}),
99+
left: `${Math.round(x)}%`,
100+
top: `${Math.round(y)}%`,
101+
},
102+
};
103+
104+
const updateEvent = new CustomEvent("config-changed", {
105+
detail: { config: newElement },
106+
});
107+
this._handleSubElementChanged(updateEvent);
108+
}
109+
69110
private _schema = memoizeOne(
70111
(localize: LocalizeFunc) =>
71112
[
@@ -138,6 +179,16 @@ export class HuiPictureElementsCardEditor
138179

139180
if (this._subElementEditorConfig) {
140181
return html`
182+
${this._subElementEditorConfig.type === "element" &&
183+
this._subElementEditorConfig.elementConfig?.type !== "conditional"
184+
? html`
185+
<ha-alert alert-type="info">
186+
${this.hass.localize(
187+
"ui.panel.lovelace.editor.card.picture-elements.position_hint"
188+
)}
189+
</ha-alert>
190+
`
191+
: nothing}
141192
<hui-sub-element-editor
142193
.hass=${this.hass}
143194
.config=${this._subElementEditorConfig}
@@ -181,6 +232,7 @@ export class HuiPictureElementsCardEditor
181232
return;
182233
}
183234

235+
// no need to attach the preview click callback here, no element is being edited
184236
fireEvent(this, "config-changed", { config: ev.detail.value });
185237
}
186238

@@ -191,7 +243,8 @@ export class HuiPictureElementsCardEditor
191243
const config = {
192244
...this._config,
193245
elements: ev.detail.elements as LovelaceElementConfig[],
194-
} as LovelaceCardConfig;
246+
[PREVIEW_CLICK_CALLBACK]: this._onPreviewClick,
247+
} as PictureElementsCardConfig;
195248

196249
fireEvent(this, "config-changed", { config });
197250

@@ -232,7 +285,12 @@ export class HuiPictureElementsCardEditor
232285
elementConfig: value,
233286
};
234287

235-
fireEvent(this, "config-changed", { config: this._config });
288+
fireEvent(this, "config-changed", {
289+
config: {
290+
...this._config,
291+
[PREVIEW_CLICK_CALLBACK]: this._onPreviewClick,
292+
},
293+
});
236294
}
237295

238296
private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {

src/panels/lovelace/editor/get-element-stub-config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ export const getElementStubConfig = async (
1010
): Promise<LovelaceElementConfig> => {
1111
let elementConfig: LovelaceElementConfig = { type };
1212

13-
if (type !== "conditional") {
13+
if (type === "conditional") {
14+
elementConfig = { type, conditions: [], elements: [] };
15+
} else {
1416
elementConfig.style = { left: "50%", top: "50%" };
1517
}
1618

src/panels/lovelace/editor/hui-element-editor.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,11 @@ export abstract class HuiElementEditor<
8989
}
9090

9191
public set value(config: T | undefined) {
92-
if (this._config && deepEqual(config, this._config)) {
92+
// Compare symbols to detect callback changes (e.g., preview click handlers)
93+
if (
94+
this._config &&
95+
deepEqual(config, this._config, { compareSymbols: true })
96+
) {
9397
return;
9498
}
9599
this._config = config;

src/translations/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8326,6 +8326,7 @@
83268326
"dark_mode_image": "Dark mode image path",
83278327
"state_filter": "State filter",
83288328
"dark_mode_filter": "Dark mode state filter",
8329+
"position_hint": "Click on the image preview to position this element",
83298330
"element_types": {
83308331
"state-badge": "State badge",
83318332
"state-icon": "State icon",

0 commit comments

Comments
 (0)