mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-12 04:20:28 +00:00
Compare commits
2 Commits
renovate/n
...
refactor-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c95409ebd | ||
|
|
01a86b313a |
16
src/common/condition/extract.ts
Normal file
16
src/common/condition/extract.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { Condition } from "../../panels/lovelace/common/validate-condition";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract media queries from conditions recursively
|
||||||
|
*/
|
||||||
|
export function extractMediaQueries(conditions: Condition[]): string[] {
|
||||||
|
return conditions.reduce<string[]>((array, c) => {
|
||||||
|
if ("conditions" in c && c.conditions) {
|
||||||
|
array.push(...extractMediaQueries(c.conditions));
|
||||||
|
}
|
||||||
|
if (c.condition === "screen" && c.media_query) {
|
||||||
|
array.push(c.media_query);
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
37
src/common/condition/listeners.ts
Normal file
37
src/common/condition/listeners.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { listenMediaQuery } from "../dom/media_query";
|
||||||
|
import type { HomeAssistant } from "../../types";
|
||||||
|
import type { Condition } from "../../panels/lovelace/common/validate-condition";
|
||||||
|
import { checkConditionsMet } from "../../panels/lovelace/common/validate-condition";
|
||||||
|
import { extractMediaQueries } from "./extract";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to setup media query listeners for conditional visibility
|
||||||
|
*/
|
||||||
|
export function setupMediaQueryListeners(
|
||||||
|
conditions: Condition[],
|
||||||
|
hass: HomeAssistant,
|
||||||
|
addListener: (unsub: () => void) => void,
|
||||||
|
onUpdate: (conditionsMet: boolean) => void
|
||||||
|
): void {
|
||||||
|
const mediaQueries = extractMediaQueries(conditions);
|
||||||
|
|
||||||
|
if (mediaQueries.length === 0) return;
|
||||||
|
|
||||||
|
// Optimization for single media query
|
||||||
|
const hasOnlyMediaQuery =
|
||||||
|
conditions.length === 1 &&
|
||||||
|
conditions[0].condition === "screen" &&
|
||||||
|
!!conditions[0].media_query;
|
||||||
|
|
||||||
|
mediaQueries.forEach((mediaQuery) => {
|
||||||
|
const unsub = listenMediaQuery(mediaQuery, (matches) => {
|
||||||
|
if (hasOnlyMediaQuery) {
|
||||||
|
onUpdate(matches);
|
||||||
|
} else {
|
||||||
|
const conditionsMet = checkConditionsMet(conditions, hass);
|
||||||
|
onUpdate(conditionsMet);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
addListener(unsub);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,73 +1,25 @@
|
|||||||
import type { ReactiveElement } from "lit";
|
import type { ReactiveElement } from "lit";
|
||||||
import { listenMediaQuery } from "../common/dom/media_query";
|
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import type { Condition } from "../panels/lovelace/common/validate-condition";
|
import { setupMediaQueryListeners } from "../common/condition/listeners";
|
||||||
import { checkConditionsMet } from "../panels/lovelace/common/validate-condition";
|
|
||||||
|
|
||||||
type Constructor<T> = abstract new (...args: any[]) => T;
|
type Constructor<T> = abstract new (...args: any[]) => T;
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract media queries from conditions recursively
|
|
||||||
*/
|
|
||||||
export function extractMediaQueries(conditions: Condition[]): string[] {
|
|
||||||
return conditions.reduce<string[]>((array, c) => {
|
|
||||||
if ("conditions" in c && c.conditions) {
|
|
||||||
array.push(...extractMediaQueries(c.conditions));
|
|
||||||
}
|
|
||||||
if (c.condition === "screen" && c.media_query) {
|
|
||||||
array.push(c.media_query);
|
|
||||||
}
|
|
||||||
return array;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to setup media query listeners for conditional visibility
|
|
||||||
*/
|
|
||||||
export function setupMediaQueryListeners(
|
|
||||||
conditions: Condition[],
|
|
||||||
hass: HomeAssistant,
|
|
||||||
addListener: (unsub: () => void) => void,
|
|
||||||
onUpdate: (conditionsMet: boolean) => void
|
|
||||||
): void {
|
|
||||||
const mediaQueries = extractMediaQueries(conditions);
|
|
||||||
|
|
||||||
if (mediaQueries.length === 0) return;
|
|
||||||
|
|
||||||
// Optimization for single media query
|
|
||||||
const hasOnlyMediaQuery =
|
|
||||||
conditions.length === 1 &&
|
|
||||||
conditions[0].condition === "screen" &&
|
|
||||||
!!conditions[0].media_query;
|
|
||||||
|
|
||||||
mediaQueries.forEach((mediaQuery) => {
|
|
||||||
const unsub = listenMediaQuery(mediaQuery, (matches) => {
|
|
||||||
if (hasOnlyMediaQuery) {
|
|
||||||
onUpdate(matches);
|
|
||||||
} else {
|
|
||||||
const conditionsMet = checkConditionsMet(conditions, hass);
|
|
||||||
onUpdate(conditionsMet);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
addListener(unsub);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mixin to handle conditional listeners for visibility control
|
* Mixin to handle conditional listeners for visibility control
|
||||||
*
|
*
|
||||||
* Provides lifecycle management for listeners (media queries, time-based, state changes, etc.)
|
* Provides lifecycle management for listeners that control conditional
|
||||||
* that control conditional visibility of components.
|
* visibility of components.
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* 1. Extend your component with ConditionalListenerMixin(ReactiveElement)
|
* 1. Extend your component with ConditionalListenerMixin(ReactiveElement)
|
||||||
* 2. Override setupConditionalListeners() to setup your listeners
|
* 2. Ensure component has config.visibility or _config.visibility property with conditions
|
||||||
* 3. Use addConditionalListener() to register unsubscribe functions
|
* 3. Ensure component has _updateVisibility() or _updateElement() method
|
||||||
* 4. Call clearConditionalListeners() and setupConditionalListeners() when config changes
|
* 4. Override setupConditionalListeners() if custom behavior needed (e.g., filter conditions)
|
||||||
*
|
*
|
||||||
* The mixin automatically:
|
* The mixin automatically:
|
||||||
* - Sets up listeners when component connects to DOM
|
* - Sets up listeners when component connects to DOM
|
||||||
* - Cleans up listeners when component disconnects from DOM
|
* - Cleans up listeners when component disconnects from DOM
|
||||||
|
* - Handles conditional visibility based on defined conditions
|
||||||
*/
|
*/
|
||||||
export const ConditionalListenerMixin = <
|
export const ConditionalListenerMixin = <
|
||||||
T extends Constructor<ReactiveElement>,
|
T extends Constructor<ReactiveElement>,
|
||||||
@@ -77,6 +29,9 @@ export const ConditionalListenerMixin = <
|
|||||||
abstract class ConditionalListenerClass extends superClass {
|
abstract class ConditionalListenerClass extends superClass {
|
||||||
private __listeners: (() => void)[] = [];
|
private __listeners: (() => void)[] = [];
|
||||||
|
|
||||||
|
// Type hint for hass property (should be provided by subclass)
|
||||||
|
abstract hass?: HomeAssistant;
|
||||||
|
|
||||||
public connectedCallback() {
|
public connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
this.setupConditionalListeners();
|
this.setupConditionalListeners();
|
||||||
@@ -96,8 +51,44 @@ export const ConditionalListenerMixin = <
|
|||||||
this.__listeners.push(unsubscribe);
|
this.__listeners.push(unsubscribe);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected setupConditionalListeners(): void {
|
/**
|
||||||
// Override in subclass
|
* Setup conditional listeners for visibility control
|
||||||
|
*
|
||||||
|
* Default implementation:
|
||||||
|
* - Checks config.visibility or _config.visibility for conditions (if not provided)
|
||||||
|
* - Sets up appropriate listeners based on condition types
|
||||||
|
* - Calls _updateVisibility() or _updateElement() when conditions change
|
||||||
|
*
|
||||||
|
* Override this method to customize behavior (e.g., filter conditions first)
|
||||||
|
* and call super.setupConditionalListeners(customConditions) to reuse the base implementation
|
||||||
|
*
|
||||||
|
* @param conditions - Optional conditions array. If not provided, will check config.visibility or _config.visibility
|
||||||
|
*/
|
||||||
|
protected setupConditionalListeners(conditions?: any[]): void {
|
||||||
|
const component = this as any;
|
||||||
|
const finalConditions =
|
||||||
|
conditions ||
|
||||||
|
component.config?.visibility ||
|
||||||
|
component._config?.visibility;
|
||||||
|
|
||||||
|
if (!finalConditions || !this.hass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onUpdate = (conditionsMet: boolean) => {
|
||||||
|
if (component._updateVisibility) {
|
||||||
|
component._updateVisibility(conditionsMet);
|
||||||
|
} else if (component._updateElement) {
|
||||||
|
component._updateElement(conditionsMet);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setupMediaQueryListeners(
|
||||||
|
finalConditions,
|
||||||
|
this.hass,
|
||||||
|
(unsub) => this.addConditionalListener(unsub),
|
||||||
|
onUpdate
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ConditionalListenerClass;
|
return ConditionalListenerClass;
|
||||||
|
|||||||
@@ -5,10 +5,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
|||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import {
|
import { ConditionalListenerMixin } from "../../../mixins/conditional-listener-mixin";
|
||||||
ConditionalListenerMixin,
|
|
||||||
setupMediaQueryListeners,
|
|
||||||
} from "../../../mixins/conditional-listener-mixin";
|
|
||||||
import { checkConditionsMet } from "../common/validate-condition";
|
import { checkConditionsMet } from "../common/validate-condition";
|
||||||
import { createBadgeElement } from "../create-element/create-badge-element";
|
import { createBadgeElement } from "../create-element/create-badge-element";
|
||||||
import { createErrorBadgeConfig } from "../create-element/create-element-base";
|
import { createErrorBadgeConfig } from "../create-element/create-element-base";
|
||||||
@@ -133,21 +130,6 @@ export class HuiBadge extends ConditionalListenerMixin(ReactiveElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected setupConditionalListeners() {
|
|
||||||
if (!this.config?.visibility || !this.hass) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setupMediaQueryListeners(
|
|
||||||
this.config.visibility,
|
|
||||||
this.hass,
|
|
||||||
(unsub) => this.addConditionalListener(unsub),
|
|
||||||
(conditionsMet) => {
|
|
||||||
this._updateVisibility(conditionsMet);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _updateVisibility(ignoreConditions?: boolean) {
|
private _updateVisibility(ignoreConditions?: boolean) {
|
||||||
if (!this._element || !this.hass) {
|
if (!this._element || !this.hass) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -5,10 +5,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
|||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import {
|
import { ConditionalListenerMixin } from "../../../mixins/conditional-listener-mixin";
|
||||||
ConditionalListenerMixin,
|
|
||||||
setupMediaQueryListeners,
|
|
||||||
} from "../../../mixins/conditional-listener-mixin";
|
|
||||||
import { migrateLayoutToGridOptions } from "../common/compute-card-grid-size";
|
import { migrateLayoutToGridOptions } from "../common/compute-card-grid-size";
|
||||||
import { computeCardSize } from "../common/compute-card-size";
|
import { computeCardSize } from "../common/compute-card-size";
|
||||||
import { checkConditionsMet } from "../common/validate-condition";
|
import { checkConditionsMet } from "../common/validate-condition";
|
||||||
@@ -247,21 +244,6 @@ export class HuiCard extends ConditionalListenerMixin(ReactiveElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected setupConditionalListeners() {
|
|
||||||
if (!this.config?.visibility || !this.hass) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setupMediaQueryListeners(
|
|
||||||
this.config.visibility,
|
|
||||||
this.hass,
|
|
||||||
(unsub) => this.addConditionalListener(unsub),
|
|
||||||
(conditionsMet) => {
|
|
||||||
this._updateVisibility(conditionsMet);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _updateVisibility(ignoreConditions?: boolean) {
|
private _updateVisibility(ignoreConditions?: boolean) {
|
||||||
if (!this._element || !this.hass) {
|
if (!this._element || !this.hass) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { ensureArray } from "../../../common/array/ensure-array";
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
|
||||||
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
|
||||||
import { UNKNOWN } from "../../../data/entity";
|
import { UNKNOWN } from "../../../data/entity";
|
||||||
import { getUserPerson } from "../../../data/person";
|
import { getUserPerson } from "../../../data/person";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import { ensureArray } from "../../../common/array/ensure-array";
|
||||||
|
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
||||||
|
|
||||||
export type Condition =
|
export type Condition =
|
||||||
| LocationCondition
|
| LocationCondition
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ import type { PropertyValues } from "lit";
|
|||||||
import { ReactiveElement } from "lit";
|
import { ReactiveElement } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import {
|
import { ConditionalListenerMixin } from "../../../mixins/conditional-listener-mixin";
|
||||||
ConditionalListenerMixin,
|
|
||||||
setupMediaQueryListeners,
|
|
||||||
} from "../../../mixins/conditional-listener-mixin";
|
|
||||||
import type { HuiCard } from "../cards/hui-card";
|
import type { HuiCard } from "../cards/hui-card";
|
||||||
import type { ConditionalCardConfig } from "../cards/types";
|
import type { ConditionalCardConfig } from "../cards/types";
|
||||||
import type { Condition } from "../common/validate-condition";
|
import type { Condition } from "../common/validate-condition";
|
||||||
@@ -73,18 +70,13 @@ export class HuiConditionalBase extends ConditionalListenerMixin(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter to supported conditions (those with 'condition' property)
|
||||||
const supportedConditions = this._config.conditions.filter(
|
const supportedConditions = this._config.conditions.filter(
|
||||||
(c) => "condition" in c
|
(c) => "condition" in c
|
||||||
) as Condition[];
|
) as Condition[];
|
||||||
|
|
||||||
setupMediaQueryListeners(
|
// Pass filtered conditions to parent implementation
|
||||||
supportedConditions,
|
super.setupConditionalListeners(supportedConditions);
|
||||||
this.hass,
|
|
||||||
(unsub) => this.addConditionalListener(unsub),
|
|
||||||
(conditionsMet) => {
|
|
||||||
this.setVisibility(conditionsMet);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected update(changed: PropertyValues): void {
|
protected update(changed: PropertyValues): void {
|
||||||
@@ -102,17 +94,15 @@ export class HuiConditionalBase extends ConditionalListenerMixin(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _updateVisibility() {
|
private _updateVisibility(conditionsMet?: boolean) {
|
||||||
if (!this._element || !this.hass || !this._config) {
|
if (!this._element || !this.hass || !this._config) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._element.preview = this.preview;
|
this._element.preview = this.preview;
|
||||||
|
|
||||||
const conditionMet = checkConditionsMet(
|
const conditionMet =
|
||||||
this._config!.conditions,
|
conditionsMet ?? checkConditionsMet(this._config.conditions, this.hass);
|
||||||
this.hass!
|
|
||||||
);
|
|
||||||
|
|
||||||
this.setVisibility(conditionMet);
|
this.setVisibility(conditionMet);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ import { customElement, property } from "lit/decorators";
|
|||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import {
|
import { ConditionalListenerMixin } from "../../../mixins/conditional-listener-mixin";
|
||||||
ConditionalListenerMixin,
|
|
||||||
setupMediaQueryListeners,
|
|
||||||
} from "../../../mixins/conditional-listener-mixin";
|
|
||||||
import { checkConditionsMet } from "../common/validate-condition";
|
import { checkConditionsMet } from "../common/validate-condition";
|
||||||
import { createHeadingBadgeElement } from "../create-element/create-heading-badge-element";
|
import { createHeadingBadgeElement } from "../create-element/create-heading-badge-element";
|
||||||
import type { LovelaceHeadingBadge } from "../types";
|
import type { LovelaceHeadingBadge } from "../types";
|
||||||
@@ -133,21 +130,6 @@ export class HuiHeadingBadge extends ConditionalListenerMixin(ReactiveElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected setupConditionalListeners() {
|
|
||||||
if (!this.config?.visibility || !this.hass) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setupMediaQueryListeners(
|
|
||||||
this.config.visibility,
|
|
||||||
this.hass,
|
|
||||||
(unsub) => this.addConditionalListener(unsub),
|
|
||||||
(conditionsMet) => {
|
|
||||||
this._updateVisibility(conditionsMet);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _updateVisibility(forceVisible?: boolean) {
|
private _updateVisibility(forceVisible?: boolean) {
|
||||||
if (!this._element || !this.hass) {
|
if (!this._element || !this.hass) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -13,10 +13,7 @@ import type {
|
|||||||
} from "../../../data/lovelace/config/section";
|
} from "../../../data/lovelace/config/section";
|
||||||
import { isStrategySection } from "../../../data/lovelace/config/section";
|
import { isStrategySection } from "../../../data/lovelace/config/section";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import {
|
import { ConditionalListenerMixin } from "../../../mixins/conditional-listener-mixin";
|
||||||
ConditionalListenerMixin,
|
|
||||||
setupMediaQueryListeners,
|
|
||||||
} from "../../../mixins/conditional-listener-mixin";
|
|
||||||
import "../cards/hui-card";
|
import "../cards/hui-card";
|
||||||
import type { HuiCard } from "../cards/hui-card";
|
import type { HuiCard } from "../cards/hui-card";
|
||||||
import { checkConditionsMet } from "../common/validate-condition";
|
import { checkConditionsMet } from "../common/validate-condition";
|
||||||
@@ -152,21 +149,6 @@ export class HuiSection extends ConditionalListenerMixin(ReactiveElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected setupConditionalListeners() {
|
|
||||||
if (!this._config?.visibility || !this.hass) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setupMediaQueryListeners(
|
|
||||||
this._config.visibility,
|
|
||||||
this.hass,
|
|
||||||
(unsub) => this.addConditionalListener(unsub),
|
|
||||||
(conditionsMet) => {
|
|
||||||
this._updateElement(conditionsMet);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _initializeConfig() {
|
private async _initializeConfig() {
|
||||||
let sectionConfig = { ...this.config };
|
let sectionConfig = { ...this.config };
|
||||||
let isStrategy = false;
|
let isStrategy = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user