Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions src/common/util/deep-equal.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Expand All @@ -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;
}
}
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -93,9 +103,26 @@ 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;
Expand Down
19 changes: 18 additions & 1 deletion src/panels/lovelace/cards/hui-picture-elements-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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}
></hui-image>
${this._elements}
</div>
Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions src/panels/lovelace/cards/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,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;
Expand All @@ -497,6 +501,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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,23 @@ 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";
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({
Expand Down Expand Up @@ -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<string, string>)
?.position;
if (currentPosition && currentPosition !== "absolute") {
return;
}

const newElement = {
...elementConfig,
style: {
...((elementConfig.style as Record<string, string>) || {}),
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) =>
[
Expand Down Expand Up @@ -138,6 +179,16 @@ export class HuiPictureElementsCardEditor

if (this._subElementEditorConfig) {
return html`
${this._subElementEditorConfig.type === "element" &&
this._subElementEditorConfig.elementConfig?.type !== "conditional"
? html`
<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.lovelace.editor.card.picture-elements.position_hint"
)}
</ha-alert>
`
: nothing}
<hui-sub-element-editor
.hass=${this.hass}
.config=${this._subElementEditorConfig}
Expand Down Expand Up @@ -181,6 +232,7 @@ export class HuiPictureElementsCardEditor
return;
}

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

Expand All @@ -191,7 +243,8 @@ export class HuiPictureElementsCardEditor
const config = {
...this._config,
elements: ev.detail.elements as LovelaceElementConfig[],
} as LovelaceCardConfig;
[PREVIEW_CLICK_CALLBACK]: this._onPreviewClick,
} as PictureElementsCardConfig;

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

Expand Down Expand Up @@ -232,7 +285,12 @@ export class HuiPictureElementsCardEditor
elementConfig: value,
};

fireEvent(this, "config-changed", { config: this._config });
fireEvent(this, "config-changed", {
config: {
...this._config,
[PREVIEW_CLICK_CALLBACK]: this._onPreviewClick,
},
});
}

private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {
Expand Down
4 changes: 3 additions & 1 deletion src/panels/lovelace/editor/get-element-stub-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ export const getElementStubConfig = async (
): Promise<LovelaceElementConfig> => {
let elementConfig: LovelaceElementConfig = { type };

if (type !== "conditional") {
if (type === "conditional") {
elementConfig = { type, conditions: [], elements: [] };
} else {
elementConfig.style = { left: "50%", top: "50%" };
}

Expand Down
6 changes: 5 additions & 1 deletion src/panels/lovelace/editor/hui-element-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -8217,6 +8217,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",
Expand Down
Loading