Add responsive condition for conditional card

This commit is contained in:
Paul Bottein 2023-09-21 16:04:46 +02:00
parent 8e1e42cd50
commit 0a4401b417
No known key found for this signature in database
3 changed files with 156 additions and 25 deletions

View File

@ -1,10 +1,48 @@
import { UNAVAILABLE } from "../../../data/entity"; import { UNAVAILABLE } from "../../../data/entity";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
export interface Condition { export type Condition = StateCondition | ResponsiveCondition;
export type StateCondition = {
condition: "state";
entity: string; entity: string;
state?: string; state?: string;
state_not?: string; state_not?: string;
};
export type ResponsiveCondition = {
condition: "responsive";
min_width?: number;
max_width?: number;
};
function checkStateCondition(condition: StateCondition, hass: HomeAssistant) {
const state = hass.states[condition.entity]
? hass!.states[condition.entity].state
: UNAVAILABLE;
return condition.state != null
? state === condition.state
: state !== condition.state_not;
}
export function buildMediaQuery(condition: ResponsiveCondition) {
const queries: string[] = [];
if (condition.min_width != null) {
queries.push(`(min-width: ${condition.min_width}px)`);
}
if (condition.max_width != null) {
queries.push(`(max-width: ${condition.max_width}px)`);
}
return queries.join(" and ");
}
function checkResponsiveCondition(
condition: ResponsiveCondition,
_hass: HomeAssistant
) {
const query = buildMediaQuery(condition);
return matchMedia(query).matches;
} }
export function checkConditionsMet( export function checkConditionsMet(
@ -12,18 +50,30 @@ export function checkConditionsMet(
hass: HomeAssistant hass: HomeAssistant
): boolean { ): boolean {
return conditions.every((c) => { return conditions.every((c) => {
const state = hass.states[c.entity] if (c.condition === "responsive") {
? hass!.states[c.entity].state return checkResponsiveCondition(c, hass);
: UNAVAILABLE; }
return c.state != null ? state === c.state : state !== c.state_not; return checkStateCondition(c, hass);
}); });
} }
export function validateConditionalConfig(conditions: Condition[]): boolean { function valideStateCondition(condition: StateCondition) {
return conditions.every( return (condition.entity &&
(c) => (condition.state != null ||
(c.entity && condition.state_not != null)) as unknown as boolean;
(c.state != null || c.state_not != null)) as unknown as boolean }
);
function valideResponsiveCondition(condition: ResponsiveCondition) {
return (condition.min_width != null ||
condition.max_width != null) as unknown as boolean;
}
export function validateConditionalConfig(conditions: Condition[]): boolean {
return conditions.every((c) => {
if (c.condition === "responsive") {
return valideResponsiveCondition(c);
}
return valideStateCondition(c);
});
} }

View File

@ -3,11 +3,15 @@ import { customElement, property } from "lit/decorators";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { ConditionalCardConfig } from "../cards/types"; import { ConditionalCardConfig } from "../cards/types";
import { import {
ResponsiveCondition,
buildMediaQuery,
checkConditionsMet, checkConditionsMet,
validateConditionalConfig, validateConditionalConfig,
} from "../common/validate-condition"; } from "../common/validate-condition";
import { ConditionalRowConfig, LovelaceRow } from "../entity-rows/types"; import { ConditionalRowConfig, LovelaceRow } from "../entity-rows/types";
import { LovelaceCard } from "../types"; import { LovelaceCard } from "../types";
import { listenMediaQuery } from "../../../common/dom/media_query";
import { deepEqual } from "../../../common/util/deep-equal";
@customElement("hui-conditional-base") @customElement("hui-conditional-base")
export class HuiConditionalBase extends ReactiveElement { export class HuiConditionalBase extends ReactiveElement {
@ -21,6 +25,10 @@ export class HuiConditionalBase extends ReactiveElement {
protected _element?: LovelaceCard | LovelaceRow; protected _element?: LovelaceCard | LovelaceRow;
private _mediaQueriesListeners: Array<() => void> = [];
private _mediaQueries: string[] = [];
protected createRenderRoot() { protected createRenderRoot() {
return this; return this;
} }
@ -47,27 +55,82 @@ export class HuiConditionalBase extends ReactiveElement {
this._config = config; this._config = config;
} }
public disconnectedCallback() {
super.disconnectedCallback();
this._clearMediaQueries();
}
public connectedCallback() {
super.connectedCallback();
this._listenMediaQueries();
this._updateVisibility();
}
private _clearMediaQueries() {
this._mediaQueries = [];
while (this._mediaQueriesListeners.length) {
this._mediaQueriesListeners.pop()!();
}
}
private _listenMediaQueries() {
if (!this._config) {
return;
}
const conditions = this._config.conditions.filter(
(c) => c.condition === "responsive"
) as ResponsiveCondition[];
const mediaQueries = conditions.map((c) => buildMediaQuery(c));
if (deepEqual(mediaQueries, this._mediaQueries)) return;
this._mediaQueries = mediaQueries;
while (this._mediaQueriesListeners.length) {
this._mediaQueriesListeners.pop()!();
}
mediaQueries.forEach((query) => {
const listener = listenMediaQuery(query, () => {
this._updateVisibility();
});
this._mediaQueriesListeners.push(listener);
});
}
protected update(changed: PropertyValues): void { protected update(changed: PropertyValues): void {
super.update(changed); super.update(changed);
if (
changed.has("_element") ||
changed.has("_config") ||
changed.has("hass")
) {
this._listenMediaQueries();
this._updateVisibility();
}
}
private _updateVisibility() {
if (!this._element || !this.hass || !this._config) { if (!this._element || !this.hass || !this._config) {
return; return;
} }
this._element.editMode = this.editMode; this._element!.editMode = this.editMode;
const visible = const visible =
this.editMode || checkConditionsMet(this._config.conditions, this.hass); this.editMode || checkConditionsMet(this._config!.conditions, this.hass!);
this.hidden = !visible; this.hidden = !visible;
this.style.setProperty("display", visible ? "" : "none"); this.style.setProperty("display", visible ? "" : "none");
if (visible) { if (visible) {
this._element.hass = this.hass; this._element!.hass = this.hass;
if (!this._element.parentElement) { if (!this._element!.parentElement) {
this.appendChild(this._element); this.appendChild(this._element!);
} }
} else if (this._element.parentElement) { } else if (this._element!.parentElement) {
this.removeChild(this._element); this.removeChild(this._element!);
} }
} }
} }

View File

@ -11,9 +11,12 @@ import {
array, array,
assert, assert,
assign, assign,
literal,
number,
object, object,
optional, optional,
string, string,
union,
} from "superstruct"; } from "superstruct";
import { storage } from "../../../../common/decorators/storage"; import { storage } from "../../../../common/decorators/storage";
import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event"; import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event";
@ -36,17 +39,28 @@ import type { ConfigChangedEvent } from "../hui-element-editor";
import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import type { GUIModeChangedEvent } from "../types"; import type { GUIModeChangedEvent } from "../types";
import { configElementStyle } from "./config-elements-style"; import { configElementStyle } from "./config-elements-style";
import { StateCondition } from "../../common/validate-condition";
const conditionStruct = object({ const stateConditionStruct = object({
condition: optional(literal("state")),
entity: string(), entity: string(),
state: optional(string()), state: optional(string()),
state_not: optional(string()), state_not: optional(string()),
}); });
const responsiveConditionStruct = object({
condition: literal("responsive"),
max_width: optional(number()),
min_width: optional(number()),
});
const cardConfigStruct = assign( const cardConfigStruct = assign(
baseLovelaceCardConfig, baseLovelaceCardConfig,
object({ object({
card: any(), card: any(),
conditions: optional(array(conditionStruct)), conditions: optional(
array(union([stateConditionStruct, responsiveConditionStruct]))
),
}) })
); );
@ -163,8 +177,10 @@ export class HuiConditionalCardEditor
${this.hass!.localize( ${this.hass!.localize(
"ui.panel.lovelace.editor.card.conditional.condition_explanation" "ui.panel.lovelace.editor.card.conditional.condition_explanation"
)} )}
${this._config.conditions.map( ${this._config.conditions.map((cond, idx) => {
(cond, idx) => html` if (cond.condition && cond.condition !== "state")
return nothing;
return html`
<div class="condition"> <div class="condition">
<div class="entity"> <div class="entity">
<ha-entity-picker <ha-entity-picker
@ -214,8 +230,8 @@ export class HuiConditionalCardEditor
></ha-textfield> ></ha-textfield>
</div> </div>
</div> </div>
` `;
)} })}
<div class="condition"> <div class="condition">
<ha-entity-picker <ha-entity-picker
.hass=${this.hass} .hass=${this.hass}
@ -296,6 +312,7 @@ export class HuiConditionalCardEditor
} }
const conditions = [...this._config.conditions]; const conditions = [...this._config.conditions];
conditions.push({ conditions.push({
condition: "state",
entity: target.value, entity: target.value,
state: "", state: "",
}); });
@ -313,7 +330,8 @@ export class HuiConditionalCardEditor
if (target.configValue === "entity" && target.value === "") { if (target.configValue === "entity" && target.value === "") {
conditions.splice(target.idx, 1); conditions.splice(target.idx, 1);
} else { } else {
const condition = { ...conditions[target.idx] }; // Only state condition are supported by the editor
const condition = { ...conditions[target.idx] } as StateCondition;
if (target.configValue === "entity") { if (target.configValue === "entity") {
condition.entity = target.value; condition.entity = target.value;
} else if (target.configValue === "state") { } else if (target.configValue === "state") {