Add screen condition to conditional card. (#18041)

This commit is contained in:
Paul Bottein 2023-10-09 15:06:58 +02:00 committed by GitHub
parent 5a6d6dc7d3
commit 86c014b677
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 831 additions and 145 deletions

View File

@ -0,0 +1,9 @@
export function getAllCombinations<T>(arr: T[]) {
return arr.reduce<T[][]>(
(combinations, element) =>
combinations.concat(
combinations.map((combination) => [...combination, element])
),
[[]]
);
}

View File

@ -0,0 +1,7 @@
import { mdiResponsive, mdiStateMachine } from "@mdi/js";
import { Condition } from "./validate-condition";
export const ICON_CONDITION: Record<Condition["condition"], string> = {
state: mdiStateMachine,
screen: mdiResponsive,
};

View File

@ -1,10 +1,44 @@
import { UNAVAILABLE } from "../../../data/entity";
import { HomeAssistant } from "../../../types";
export interface Condition {
entity: string;
export type Condition = StateCondition | ScreenCondition;
export type LegacyCondition = {
entity?: string;
state?: string;
state_not?: string;
};
export type StateCondition = {
condition: "state";
entity?: string;
state?: string;
state_not?: string;
};
export type ScreenCondition = {
condition: "screen";
media_query?: string;
};
function checkStateCondition(condition: StateCondition, hass: HomeAssistant) {
const state =
condition.entity && hass.states[condition.entity]
? hass.states[condition.entity].state
: UNAVAILABLE;
return condition.state != null
? state === condition.state
: state !== condition.state_not;
}
function checkScreenCondition(
condition: ScreenCondition,
_hass: HomeAssistant
) {
return condition.media_query
? matchMedia(condition.media_query).matches
: false;
}
export function checkConditionsMet(
@ -12,18 +46,30 @@ export function checkConditionsMet(
hass: HomeAssistant
): boolean {
return conditions.every((c) => {
const state = hass.states[c.entity]
? hass!.states[c.entity].state
: UNAVAILABLE;
if (c.condition === "screen") {
return checkScreenCondition(c, hass);
}
return c.state != null ? state === c.state : state !== c.state_not;
return checkStateCondition(c, hass);
});
}
export function validateConditionalConfig(conditions: Condition[]): boolean {
return conditions.every(
(c) =>
(c.entity &&
(c.state != null || c.state_not != null)) as unknown as boolean
function valideStateCondition(condition: StateCondition) {
return (
condition.entity != null &&
(condition.state != null || condition.state_not != null)
);
}
function validateScreenCondition(condition: ScreenCondition) {
return condition.media_query != null;
}
export function validateConditionalConfig(conditions: Condition[]): boolean {
return conditions.every((c) => {
if (c.condition === "screen") {
return validateScreenCondition(c);
}
return valideStateCondition(c);
});
}

View File

@ -3,11 +3,14 @@ import { customElement, property } from "lit/decorators";
import { HomeAssistant } from "../../../types";
import { ConditionalCardConfig } from "../cards/types";
import {
ScreenCondition,
checkConditionsMet,
validateConditionalConfig,
} from "../common/validate-condition";
import { ConditionalRowConfig, LovelaceRow } from "../entity-rows/types";
import { LovelaceCard } from "../types";
import { listenMediaQuery } from "../../../common/dom/media_query";
import { deepEqual } from "../../../common/util/deep-equal";
@customElement("hui-conditional-base")
export class HuiConditionalBase extends ReactiveElement {
@ -21,6 +24,10 @@ export class HuiConditionalBase extends ReactiveElement {
protected _element?: LovelaceCard | LovelaceRow;
private _mediaQueriesListeners: Array<() => void> = [];
private _mediaQueries: string[] = [];
protected createRenderRoot() {
return this;
}
@ -47,27 +54,98 @@ export class HuiConditionalBase extends ReactiveElement {
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 === "screen"
) as ScreenCondition[];
const mediaQueries = conditions
.filter((c) => c.media_query)
.map((c) => c.media_query as string);
if (deepEqual(mediaQueries, this._mediaQueries)) return;
this._mediaQueries = mediaQueries;
while (this._mediaQueriesListeners.length) {
this._mediaQueriesListeners.pop()!();
}
mediaQueries.forEach((query) => {
const listener = listenMediaQuery(query, (matches) => {
// For performance, if there is only one condition, set the visibility directly
if (this._config!.conditions.length === 1) {
this._setVisibility(matches);
return;
}
this._updateVisibility();
});
this._mediaQueriesListeners.push(listener);
});
}
protected update(changed: PropertyValues): void {
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) {
return;
}
this._element.editMode = this.editMode;
const visible =
this.editMode || checkConditionsMet(this._config.conditions, this.hass);
this.hidden = !visible;
const conditionMet = checkConditionsMet(
this._config!.conditions,
this.hass!
);
this._setVisibility(conditionMet);
}
private _setVisibility(conditionMet: boolean) {
if (!this._element || !this.hass) {
return;
}
const visible = this.editMode || conditionMet;
this.hidden = !visible;
this.style.setProperty("display", visible ? "" : "none");
if (visible) {
this._element.hass = this.hass;
if (!this._element.parentElement) {
this.appendChild(this._element);
if (!this._element!.parentElement) {
this.appendChild(this._element!);
}
} else if (this._element.parentElement) {
this.removeChild(this._element);
this.removeChild(this._element!);
}
}
}

View File

@ -0,0 +1,197 @@
import { preventDefault } from "@fullcalendar/core/internal";
import { ActionDetail } from "@material/mwc-list";
import { mdiCheck, mdiDelete, mdiDotsVertical } from "@mdi/js";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-list-item";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-yaml-editor";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { Condition, LegacyCondition } from "../../common/validate-condition";
import type { LovelaceConditionEditorConstructor } from "./types";
import { ICON_CONDITION } from "../../common/icon-condition";
@customElement("ha-card-condition-editor")
export default class HaCardConditionEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) condition!: Condition | LegacyCondition;
@state() public _yamlMode = false;
protected render() {
const condition: Condition = {
condition: "state",
...this.condition,
};
const element = customElements.get(
`ha-card-condition-${condition.condition}`
) as LovelaceConditionEditorConstructor | undefined;
const supported = element !== undefined;
const valid =
element &&
(!element.validateUIConfig || element.validateUIConfig(condition));
const yamlMode = this._yamlMode || !supported || !valid;
return html`
<div class="header">
<ha-svg-icon
class="icon"
.path=${ICON_CONDITION[condition.condition]}
></ha-svg-icon>
<span class="title">
${this.hass.localize(
`ui.panel.lovelace.editor.card.conditional.condition.${condition.condition}.label`
) || condition.condition}
</span>
<ha-button-menu
slot="icons"
@action=${this._handleAction}
@click=${preventDefault}
@closed=${stopPropagation}
fixed
.corner=${"BOTTOM_END"}
.menuCorner=${"END"}
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
>
</ha-icon-button>
<ha-list-item graphic="icon" .disabled=${!supported || !valid}>
${this.hass.localize("ui.panel.lovelace.editor.edit_card.edit_ui")}
${!yamlMode
? html`
<ha-svg-icon
class="selected_menu_item"
slot="graphic"
.path=${mdiCheck}
></ha-svg-icon>
`
: ``}
</ha-list-item>
<ha-list-item graphic="icon">
${this.hass.localize(
"ui.panel.lovelace.editor.edit_card.edit_yaml"
)}
${yamlMode
? html`
<ha-svg-icon
class="selected_menu_item"
slot="graphic"
.path=${mdiCheck}
></ha-svg-icon>
`
: ``}
</ha-list-item>
<li divider role="separator"></li>
<ha-list-item class="warning" graphic="icon">
${this.hass!.localize("ui.common.delete")}
<ha-svg-icon
class="warning"
slot="graphic"
.path=${mdiDelete}
></ha-svg-icon>
</ha-list-item>
</ha-button-menu>
</div>
${!valid
? html`
<ha-alert alert-type="warning">
${this.hass.localize("ui.errors.config.editor_not_supported")}
</ha-alert>
`
: nothing}
<div class="content">
${yamlMode
? html`
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.condition}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>
`
: html`
${dynamicElement(`ha-card-condition-${condition.condition}`, {
hass: this.hass,
condition: condition,
})}
`}
</div>
`;
}
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this._yamlMode = false;
break;
case 1:
this._yamlMode = true;
break;
case 2:
this._delete();
break;
}
}
private _delete() {
fireEvent(this, "value-changed", { value: null });
}
private _onYamlChange(ev: CustomEvent) {
ev.stopPropagation();
if (!ev.detail.isValid) {
return;
}
// @ts-ignore
fireEvent(this, "value-changed", { value: ev.detail.value });
}
static styles = [
haStyle,
css`
.header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.header span {
flex: 1;
font-size: 16px;
}
.content {
padding: 12px;
}
.header .icon {
padding: 12px;
}
.selected_menu_item {
color: var(--primary-color);
}
li[role="separator"] {
border-bottom-color: var(--divider-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-card-condition-editor": HaCardConditionEditor;
}
}

View File

@ -0,0 +1,6 @@
import { Condition } from "../../common/validate-condition";
export interface LovelaceConditionEditorConstructor {
defaultConfig?: Condition;
validateUIConfig?: (condition: Condition) => boolean;
}

View File

@ -0,0 +1,196 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { getAllCombinations } from "../../../../../common/array/combinations";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { LocalizeFunc } from "../../../../../common/translations/localize";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import { HaFormSchema } from "../../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../../types";
import { ScreenCondition } from "../../../common/validate-condition";
const BREAKPOINT_VALUES = [0, 768, 1024, 1280, Infinity];
const BREAKPOINTS = ["mobile", "tablet", "desktop", "wide"] as const;
type BreakpointSize = [number, number];
type Breakpoint = (typeof BREAKPOINTS)[number];
function mergeConsecutiveRanges(arr: [number, number][]): [number, number][] {
if (arr.length === 0) {
return [];
}
[...arr].sort((a, b) => a[0] - b[0]);
const mergedRanges = [arr[0]];
for (let i = 1; i < arr.length; i++) {
const currentRange = arr[i];
const previousRange = mergedRanges[mergedRanges.length - 1];
if (currentRange[0] <= previousRange[1] + 1) {
previousRange[1] = currentRange[1];
} else {
mergedRanges.push(currentRange);
}
}
return mergedRanges;
}
function buildMediaQuery(size: BreakpointSize) {
const [min, max] = size;
const query: string[] = [];
if (min != null) {
query.push(`(min-width: ${min}px)`);
}
if (max != null && max !== Infinity) {
query.push(`(max-width: ${max - 1}px)`);
}
return query.join(" and ");
}
function computeBreakpointsSize(breakpoints: Breakpoint[]) {
const sizes = breakpoints.map<BreakpointSize>((breakpoint) => {
const index = BREAKPOINTS.indexOf(breakpoint);
return [BREAKPOINT_VALUES[index], BREAKPOINT_VALUES[index + 1] || Infinity];
});
const mergedSizes = mergeConsecutiveRanges(sizes);
const queries = mergedSizes
.map((size) => buildMediaQuery(size))
.filter((size) => size);
return queries.join(", ");
}
function computeBreakpointsKey(breakpoints) {
return [...breakpoints].sort().join("_");
}
// Compute all possible media queries from each breakpoints combination (2 ^ breakpoints = 16)
const queries = getAllCombinations(BREAKPOINTS as unknown as Breakpoint[])
.filter((arr) => arr.length !== 0)
.map(
(breakpoints) =>
[breakpoints, computeBreakpointsSize(breakpoints)] as [
Breakpoint[],
string,
]
);
// Store them in maps to avoid recomputing them
const mediaQueryMap = new Map(
queries.map(([b, m]) => [computeBreakpointsKey(b), m])
);
const mediaQueryReverseMap = new Map(queries.map(([b, m]) => [m, b]));
type ScreenConditionData = {
breakpoints: Breakpoint[];
};
@customElement("ha-card-condition-screen")
export class HaCardConditionScreen extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public condition!: ScreenCondition;
@property({ type: Boolean }) public disabled = false;
public static get defaultConfig(): ScreenCondition {
return { condition: "screen", media_query: "" };
}
protected static validateUIConfig(condition: ScreenCondition) {
return (
!condition.media_query || mediaQueryReverseMap.get(condition.media_query)
);
}
private _schema = memoizeOne(
(localize: LocalizeFunc) =>
[
{
name: "breakpoints",
selector: {
select: {
mode: "list",
options: BREAKPOINTS.map((b) => {
const value = BREAKPOINT_VALUES[BREAKPOINTS.indexOf(b)];
return {
value: b,
label: `${localize(
`ui.panel.lovelace.editor.card.conditional.condition.screen.breakpoints_list.${b}`
)}${
value
? ` (${localize(
`ui.panel.lovelace.editor.card.conditional.condition.screen.min`,
{ size: value }
)})`
: ""
}`,
};
}),
multiple: true,
},
},
},
] as const satisfies readonly HaFormSchema[]
);
protected render() {
const breakpoints = this.condition.media_query
? mediaQueryReverseMap.get(this.condition.media_query)
: undefined;
const data: ScreenConditionData = {
breakpoints: breakpoints ?? [],
};
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${this._schema(this.hass.localize)}
.disabled=${this.disabled}
@value-changed=${this._valueChanged}
.computeLabel=${this._computeLabelCallback}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const data = ev.detail.value as ScreenConditionData;
const { breakpoints } = data;
const condition: ScreenCondition = {
condition: "screen",
media_query: mediaQueryMap.get(computeBreakpointsKey(breakpoints)) ?? "",
};
fireEvent(this, "value-changed", { value: condition });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string => {
switch (schema.name) {
case "breakpoints":
return this.hass.localize(
`ui.panel.lovelace.editor.card.conditional.condition.screen.${schema.name}`
);
default:
return "";
}
};
}
declare global {
interface HTMLElementTagNameMap {
"ha-card-condition-screen": HaCardConditionScreen;
}
}

View File

@ -0,0 +1,164 @@
import { html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { assert, literal, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { LocalizeFunc } from "../../../../../common/translations/localize";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import { HaFormSchema } from "../../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../../types";
import { StateCondition } from "../../../common/validate-condition";
const stateConditionStruct = object({
condition: literal("state"),
entity: string(),
state: optional(string()),
state_not: optional(string()),
});
type StateConditionData = {
condition: "state";
entity: string;
invert: "true" | "false";
state?: string;
};
@customElement("ha-card-condition-state")
export class HaCardConditionState extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public condition!: StateCondition;
@property({ type: Boolean }) public disabled = false;
public static get defaultConfig(): StateCondition {
return { condition: "state", entity: "", state: "" };
}
protected willUpdate(changedProperties: PropertyValues): void {
if (!changedProperties.has("condition")) {
return;
}
try {
assert(this.condition, stateConditionStruct);
} catch (err: any) {
fireEvent(this, "ui-mode-not-available", err);
}
}
private _schema = memoizeOne(
(localize: LocalizeFunc) =>
[
{ name: "entity", selector: { entity: {} } },
{
name: "",
type: "grid",
schema: [
{
name: "invert",
selector: {
select: {
mode: "dropdown",
options: [
{
label: localize(
"ui.panel.lovelace.editor.card.conditional.state_equal"
),
value: "false",
},
{
label: localize(
"ui.panel.lovelace.editor.card.conditional.state_not_equal"
),
value: "true",
},
],
},
},
},
{
name: "state",
selector: {
state: {},
},
context: {
filter_entity: "entity",
},
},
],
},
] as const satisfies readonly HaFormSchema[]
);
protected render() {
const { state, state_not, ...content } = this.condition;
const data: StateConditionData = {
...content,
entity: this.condition.entity ?? "",
invert: this.condition.state_not ? "true" : "false",
state: this.condition.state_not ?? this.condition.state ?? "",
};
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${this._schema(this.hass.localize)}
.disabled=${this.disabled}
@value-changed=${this._valueChanged}
.computeLabel=${this._computeLabelCallback}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const data = ev.detail.value as StateConditionData;
const { invert, state, entity, condition: _, ...content } = data;
const condition: StateCondition = {
condition: "state",
...content,
entity: entity ?? "",
state: invert === "false" ? state ?? "" : undefined,
state_not: invert === "true" ? state ?? "" : undefined,
};
fireEvent(this, "value-changed", { value: condition });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string => {
const entity = this.condition.entity
? this.hass.states[this.condition.entity]
: undefined;
switch (schema.name) {
case "entity":
return this.hass.localize("ui.components.entity.entity-picker.entity");
case "state":
if (entity) {
return `${this.hass.localize(
"ui.components.entity.entity-state-picker.state"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.conditional.current_state"
)}: ${this.hass.formatEntityState(entity)})`;
}
return `${this.hass.localize(
"ui.components.entity.entity-state-picker.state"
)}`;
default:
return "";
}
};
}
declare global {
interface HTMLElementTagNameMap {
"ha-card-condition-state": HaCardConditionState;
}
}

View File

@ -1,52 +1,56 @@
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-tab-bar/mwc-tab-bar";
import "@material/mwc-tab/mwc-tab";
import { mdiCodeBraces, mdiContentCopy, mdiListBoxOutline } from "@mdi/js";
import deepClone from "deep-clone-simple";
import type { MDCTabBarActivatedEvent } from "@material/tab-bar";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import {
any,
array,
assert,
assign,
object,
optional,
string,
} from "superstruct";
mdiCodeBraces,
mdiContentCopy,
mdiListBoxOutline,
mdiPlus,
} from "@mdi/js";
import deepClone from "deep-clone-simple";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { any, array, assert, assign, object, optional } from "superstruct";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event";
import { HASSDomEvent, fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/ha-select";
import "../../../../components/ha-textfield";
import "../../../../components/ha-button";
import "../../../../components/ha-list-item";
import "../../../../components/ha-menu-button";
import type { HaSelect } from "../../../../components/ha-select";
import "../../../../components/ha-svg-icon";
import type {
LovelaceCardConfig,
LovelaceConfig,
} from "../../../../data/lovelace";
import type { HomeAssistant } from "../../../../types";
import type { ConditionalCardConfig } from "../../cards/types";
import { ICON_CONDITION } from "../../common/icon-condition";
import { Condition } from "../../common/validate-condition";
import type { LovelaceCardEditor } from "../../types";
import "../card-editor/hui-card-element-editor";
import type { HuiCardElementEditor } from "../card-editor/hui-card-element-editor";
import "../card-editor/hui-card-picker";
import "../conditions/ha-card-condition-editor";
import { LovelaceConditionEditorConstructor } from "../conditions/types";
import "../conditions/types/ha-card-condition-screen";
import "../conditions/types/ha-card-condition-state";
import "../hui-element-editor";
import type { ConfigChangedEvent } from "../hui-element-editor";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import type { GUIModeChangedEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
const conditionStruct = object({
entity: string(),
state: optional(string()),
state_not: optional(string()),
});
const UI_CONDITION = [
"state",
"screen",
] as const satisfies readonly Condition["condition"][];
const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
card: any(),
conditions: optional(array(conditionStruct)),
conditions: optional(array(any())),
})
);
@ -127,7 +131,6 @@ export class HuiConditionalCardEditor
)}
.path=${isGuiMode ? mdiCodeBraces : mdiListBoxOutline}
></ha-icon-button>
<ha-icon-button
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.copy"
@ -166,61 +169,44 @@ export class HuiConditionalCardEditor
${this._config.conditions.map(
(cond, idx) => html`
<div class="condition">
<div class="entity">
<ha-entity-picker
<ha-card-condition-editor
.index=${idx}
@value-changed=${this._conditionChanged}
.hass=${this.hass}
.value=${cond.entity}
.idx=${idx}
.configValue=${"entity"}
@change=${this._changeCondition}
allow-custom-entity
></ha-entity-picker>
</div>
<div class="state">
<ha-select
.value=${cond.state_not !== undefined
? "true"
: "false"}
.idx=${idx}
.configValue=${"invert"}
@selected=${this._changeCondition}
@closed=${stopPropagation}
naturalMenuWidth
fixedMenuPosition
>
<mwc-list-item value="false">
${this.hass!.localize(
"ui.panel.lovelace.editor.card.conditional.state_equal"
)}
</mwc-list-item>
<mwc-list-item value="true">
${this.hass!.localize(
"ui.panel.lovelace.editor.card.conditional.state_not_equal"
)}
</mwc-list-item>
</ha-select>
<ha-textfield
.label="${this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.state"
)} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.conditional.current_state"
)}: ${this.hass?.states[cond.entity].state})"
.value=${cond.state_not !== undefined
? cond.state_not
: cond.state}
.idx=${idx}
.configValue=${"state"}
@input=${this._changeCondition}
></ha-textfield>
</div>
.condition=${cond}
></ha-card-condition-editor>
</div>
`
)}
<div class="condition">
<ha-entity-picker
.hass=${this.hass}
@change=${this._addCondition}
></ha-entity-picker>
<div>
<ha-button-menu
@action=${this._addCondition}
fixed
@closed=${stopPropagation}
>
<ha-button
slot="trigger"
outlined
.label=${this.hass.localize(
"ui.panel.lovelace.editor.card.conditional.add_condition"
)}
>
<ha-svg-icon .path=${mdiPlus} slot="icon"></ha-svg-icon>
</ha-button>
${UI_CONDITION.map(
(condition) => html`
<ha-list-item .value=${condition} graphic="icon">
${this.hass!.localize(
`ui.panel.lovelace.editor.card.conditional.condition.${condition}.label`
) || condition}
<ha-svg-icon
slot="graphic"
.path=${ICON_CONDITION[condition]}
></ha-svg-icon>
</ha-list-item>
`
)}
</ha-button-menu>
</div>
</div>
`}
@ -289,53 +275,40 @@ export class HuiConditionalCardEditor
fireEvent(this, "config-changed", { config: this._config });
}
private _addCondition(ev: Event): void {
const target = ev.target! as any;
if (target.value === "" || !this._config) {
private _addCondition(ev: CustomEvent): void {
const condition = (ev.currentTarget as HaSelect).items[ev.detail.index]
.value as Condition["condition"];
if (!this._config) {
return;
}
const conditions = [...this._config.conditions];
conditions.push({
entity: target.value,
state: "",
});
const elClass = customElements.get(`ha-card-condition-${condition}`) as
| LovelaceConditionEditorConstructor
| undefined;
conditions.push(
elClass?.defaultConfig
? { ...elClass.defaultConfig }
: { condition: condition }
);
this._config = { ...this._config, conditions };
target.value = "";
fireEvent(this, "config-changed", { config: this._config });
}
private _changeCondition(ev: Event): void {
const target = ev.target as any;
if (!this._config || !target) {
return;
}
const conditions = [...this._config.conditions];
if (target.configValue === "entity" && target.value === "") {
conditions.splice(target.idx, 1);
private _conditionChanged(ev: CustomEvent) {
ev.stopPropagation();
const conditions = [...this._config!.conditions];
const newValue = ev.detail.value;
const index = (ev.target as any).index;
if (newValue === null) {
conditions.splice(index, 1);
} else {
const condition = { ...conditions[target.idx] };
if (target.configValue === "entity") {
condition.entity = target.value;
} else if (target.configValue === "state") {
if (condition.state_not !== undefined) {
condition.state_not = target.value;
} else {
condition.state = target.value;
conditions[index] = newValue;
}
} else if (target.configValue === "invert") {
if (target.value === "true") {
if (condition.state) {
condition.state_not = condition.state;
delete condition.state;
}
} else if (condition.state_not) {
condition.state = condition.state_not;
delete condition.state_not;
}
}
conditions[target.idx] = condition;
}
this._config = { ...this._config, conditions };
this._config = { ...this._config!, conditions };
fireEvent(this, "config-changed", { config: this._config });
}
@ -352,22 +325,13 @@ export class HuiConditionalCardEditor
.condition {
margin-top: 8px;
border: 1px solid var(--divider-color);
}
.condition .content {
padding: 12px;
}
.condition .state {
display: flex;
align-items: flex-end;
ha-button-menu {
margin-top: 12px;
}
.condition .state ha-select {
margin-right: 16px;
margin-inline-end: 16px;
margin-inline-start: initial;
direction: var(--direction);
}
.condition .state ha-textfield {
flex-grow: 1;
}
.card {
margin-top: 8px;
border: 1px solid var(--divider-color);

View File

@ -4682,6 +4682,8 @@
"confirm_cancel": "Are you sure you want to cancel?",
"show_visual_editor": "Show visual editor",
"show_code_editor": "Show code editor",
"edit_ui": "[%key:ui::panel::config::automation::editor::edit_ui%]",
"edit_yaml": "[%key:ui::panel::config::automation::editor::edit_yaml%]",
"add": "Add card",
"edit": "Edit",
"clear": "Clear",
@ -4780,7 +4782,24 @@
"state_not_equal": "State is not equal to",
"current_state": "current",
"condition_explanation": "The card will be shown when ALL conditions below are fulfilled.",
"change_type": "Change type"
"change_type": "Change type",
"add_condition": "Add condition",
"condition": {
"screen": {
"label": "Screen",
"breakpoints": "Screen sizes",
"breakpoints_list": {
"mobile": "Mobile",
"tablet": "Tablet",
"desktop": "Desktop",
"wide": "Wide"
},
"min": "min: {size}px"
},
"state": {
"label": "Entity state"
}
}
},
"config": {
"required": "required",