diff --git a/src/common/array/combinations.ts b/src/common/array/combinations.ts new file mode 100644 index 0000000000..adb6e6476d --- /dev/null +++ b/src/common/array/combinations.ts @@ -0,0 +1,9 @@ +export function getAllCombinations(arr: T[]) { + return arr.reduce( + (combinations, element) => + combinations.concat( + combinations.map((combination) => [...combination, element]) + ), + [[]] + ); +} diff --git a/src/panels/lovelace/common/icon-condition.ts b/src/panels/lovelace/common/icon-condition.ts new file mode 100644 index 0000000000..11a530f7fa --- /dev/null +++ b/src/panels/lovelace/common/icon-condition.ts @@ -0,0 +1,7 @@ +import { mdiResponsive, mdiStateMachine } from "@mdi/js"; +import { Condition } from "./validate-condition"; + +export const ICON_CONDITION: Record = { + state: mdiStateMachine, + screen: mdiResponsive, +}; diff --git a/src/panels/lovelace/common/validate-condition.ts b/src/panels/lovelace/common/validate-condition.ts index 452d061eb0..c689505090 100644 --- a/src/panels/lovelace/common/validate-condition.ts +++ b/src/panels/lovelace/common/validate-condition.ts @@ -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); + }); +} diff --git a/src/panels/lovelace/components/hui-conditional-base.ts b/src/panels/lovelace/components/hui-conditional-base.ts index d5d6da4e28..eaec714cd3 100644 --- a/src/panels/lovelace/components/hui-conditional-base.ts +++ b/src/panels/lovelace/components/hui-conditional-base.ts @@ -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!); } } } diff --git a/src/panels/lovelace/editor/conditions/ha-card-condition-editor.ts b/src/panels/lovelace/editor/conditions/ha-card-condition-editor.ts new file mode 100644 index 0000000000..7d7f322723 --- /dev/null +++ b/src/panels/lovelace/editor/conditions/ha-card-condition-editor.ts @@ -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` +
+ + + ${this.hass.localize( + `ui.panel.lovelace.editor.card.conditional.condition.${condition.condition}.label` + ) || condition.condition} + + + + + + + ${this.hass.localize("ui.panel.lovelace.editor.edit_card.edit_ui")} + ${!yamlMode + ? html` + + ` + : ``} + + + + ${this.hass.localize( + "ui.panel.lovelace.editor.edit_card.edit_yaml" + )} + ${yamlMode + ? html` + + ` + : ``} + + +
  • + + + ${this.hass!.localize("ui.common.delete")} + + +
    +
    + ${!valid + ? html` + + ${this.hass.localize("ui.errors.config.editor_not_supported")} + + ` + : nothing} +
    + ${yamlMode + ? html` + + ` + : html` + ${dynamicElement(`ha-card-condition-${condition.condition}`, { + hass: this.hass, + condition: condition, + })} + `} +
    + `; + } + + private _handleAction(ev: CustomEvent) { + 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; + } +} diff --git a/src/panels/lovelace/editor/conditions/types.ts b/src/panels/lovelace/editor/conditions/types.ts new file mode 100644 index 0000000000..cdfe8a6c8f --- /dev/null +++ b/src/panels/lovelace/editor/conditions/types.ts @@ -0,0 +1,6 @@ +import { Condition } from "../../common/validate-condition"; + +export interface LovelaceConditionEditorConstructor { + defaultConfig?: Condition; + validateUIConfig?: (condition: Condition) => boolean; +} diff --git a/src/panels/lovelace/editor/conditions/types/ha-card-condition-screen.ts b/src/panels/lovelace/editor/conditions/types/ha-card-condition-screen.ts new file mode 100644 index 0000000000..90d9ff583a --- /dev/null +++ b/src/panels/lovelace/editor/conditions/types/ha-card-condition-screen.ts @@ -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((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` + + `; + } + + 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> + ): 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; + } +} diff --git a/src/panels/lovelace/editor/conditions/types/ha-card-condition-state.ts b/src/panels/lovelace/editor/conditions/types/ha-card-condition-state.ts new file mode 100644 index 0000000000..f23cd3eb09 --- /dev/null +++ b/src/panels/lovelace/editor/conditions/types/ha-card-condition-state.ts @@ -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` + + `; + } + + 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> + ): 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; + } +} diff --git a/src/panels/lovelace/editor/config-elements/hui-conditional-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-conditional-card-editor.ts index b63913d45b..4c67d934c5 100644 --- a/src/panels/lovelace/editor/config-elements/hui-conditional-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-conditional-card-editor.ts @@ -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} > - html`
    -
    - -
    -
    - - - ${this.hass!.localize( - "ui.panel.lovelace.editor.card.conditional.state_equal" - )} - - - ${this.hass!.localize( - "ui.panel.lovelace.editor.card.conditional.state_not_equal" - )} - - - -
    +
    ` )} -
    - +
    + + + + + ${UI_CONDITION.map( + (condition) => html` + + ${this.hass!.localize( + `ui.panel.lovelace.editor.card.conditional.condition.${condition}.label` + ) || condition} + + + ` + )} +
    `} @@ -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; - } - } 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; + conditions[index] = newValue; } - 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); diff --git a/src/translations/en.json b/src/translations/en.json index df4e78187e..8275526d4d 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -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",