mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-11-04 00:19:47 +00:00 
			
		
		
		
	Compare commits
	
		
			9 Commits
		
	
	
		
			20251001.1
			...
			conditiona
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					67166be0d7 | ||
| 
						 | 
					4b386c0661 | ||
| 
						 | 
					3a03abbd3a | ||
| 
						 | 
					873ce2b405 | ||
| 
						 | 
					89b53a76f0 | ||
| 
						 | 
					15522d4926 | ||
| 
						 | 
					c6e6255d19 | ||
| 
						 | 
					8bcd730501 | ||
| 
						 | 
					689cd3d145 | 
							
								
								
									
										9
									
								
								src/common/array/combinations.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/common/array/combinations.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
export function getAllCombinations<T>(arr: T[]) {
 | 
			
		||||
  return arr.reduce<T[][]>(
 | 
			
		||||
    (combinations, element) =>
 | 
			
		||||
      combinations.concat(
 | 
			
		||||
        combinations.map((combination) => [...combination, element])
 | 
			
		||||
      ),
 | 
			
		||||
    [[]]
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -1,10 +1,44 @@
 | 
			
		||||
import { UNAVAILABLE } from "../../../data/entity";
 | 
			
		||||
import { HomeAssistant } from "../../../types";
 | 
			
		||||
 | 
			
		||||
export interface Condition {
 | 
			
		||||
  entity: string;
 | 
			
		||||
export type Condition = StateCondition | ResponsiveCondition;
 | 
			
		||||
 | 
			
		||||
export type LegacyCondition = {
 | 
			
		||||
  entity?: string;
 | 
			
		||||
  state?: string;
 | 
			
		||||
  state_not?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type StateCondition = {
 | 
			
		||||
  condition: "state";
 | 
			
		||||
  entity?: string;
 | 
			
		||||
  state?: string;
 | 
			
		||||
  state_not?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type ResponsiveCondition = {
 | 
			
		||||
  condition: "responsive";
 | 
			
		||||
  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 checkResponsiveCondition(
 | 
			
		||||
  condition: ResponsiveCondition,
 | 
			
		||||
  _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 === "responsive") {
 | 
			
		||||
      return checkResponsiveCondition(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 valideResponsiveCondition(condition: ResponsiveCondition) {
 | 
			
		||||
  return condition.media_query != null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function validateConditionalConfig(conditions: Condition[]): boolean {
 | 
			
		||||
  return conditions.every((c) => {
 | 
			
		||||
    if (c.condition === "responsive") {
 | 
			
		||||
      return valideResponsiveCondition(c);
 | 
			
		||||
    }
 | 
			
		||||
    return valideStateCondition(c);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,11 +3,14 @@ import { customElement, property } from "lit/decorators";
 | 
			
		||||
import { HomeAssistant } from "../../../types";
 | 
			
		||||
import { ConditionalCardConfig } from "../cards/types";
 | 
			
		||||
import {
 | 
			
		||||
  ResponsiveCondition,
 | 
			
		||||
  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 === "responsive"
 | 
			
		||||
    ) as ResponsiveCondition[];
 | 
			
		||||
 | 
			
		||||
    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!);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,128 @@
 | 
			
		||||
import { mdiCodeBraces, mdiDelete, mdiListBoxOutline } 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 "../../../../components/ha-icon-button";
 | 
			
		||||
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";
 | 
			
		||||
 | 
			
		||||
@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-icon-button
 | 
			
		||||
          @click=${this._toggleMode}
 | 
			
		||||
          .disabled=${!supported || !valid}
 | 
			
		||||
          .label=${this.hass!.localize(
 | 
			
		||||
            yamlMode
 | 
			
		||||
              ? "ui.panel.lovelace.editor.edit_card.show_visual_editor"
 | 
			
		||||
              : "ui.panel.lovelace.editor.edit_card.show_code_editor"
 | 
			
		||||
          )}
 | 
			
		||||
          .path=${yamlMode ? mdiListBoxOutline : mdiCodeBraces}
 | 
			
		||||
        ></ha-icon-button>
 | 
			
		||||
        <span class="title">
 | 
			
		||||
          ${this.hass.localize(
 | 
			
		||||
            `ui.panel.lovelace.editor.card.conditional.condition.${condition.condition}.label`
 | 
			
		||||
          ) || condition.condition}
 | 
			
		||||
        </span>
 | 
			
		||||
        <ha-icon-button
 | 
			
		||||
          .label=${this.hass!.localize("ui.common.delete")}
 | 
			
		||||
          .path=${mdiDelete}
 | 
			
		||||
          @click=${this._delete}
 | 
			
		||||
        >
 | 
			
		||||
        </ha-icon-button>
 | 
			
		||||
      </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 _toggleMode() {
 | 
			
		||||
    this._yamlMode = !this._yamlMode;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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;
 | 
			
		||||
      }
 | 
			
		||||
    `,
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface HTMLElementTagNameMap {
 | 
			
		||||
    "ha-card-condition-editor": HaCardConditionEditor;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										6
									
								
								src/panels/lovelace/editor/conditions/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/panels/lovelace/editor/conditions/types.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
import { Condition } from "../../common/validate-condition";
 | 
			
		||||
 | 
			
		||||
export interface LovelaceConditionEditorConstructor {
 | 
			
		||||
  defaultConfig?: Condition;
 | 
			
		||||
  validateUIConfig?: (condition: Condition) => boolean;
 | 
			
		||||
}
 | 
			
		||||
@@ -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 { ResponsiveCondition } 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 ResponsiveConditionData = {
 | 
			
		||||
  breakpoints: Breakpoint[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@customElement("ha-card-condition-responsive")
 | 
			
		||||
export class HaCardConditionResponsive extends LitElement {
 | 
			
		||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
			
		||||
 | 
			
		||||
  @property({ attribute: false }) public condition!: ResponsiveCondition;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean }) public disabled = false;
 | 
			
		||||
 | 
			
		||||
  public static get defaultConfig(): ResponsiveCondition {
 | 
			
		||||
    return { condition: "responsive", media_query: "" };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected static validateUIConfig(condition: ResponsiveCondition) {
 | 
			
		||||
    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.responsive.breakpoints_list.${b}`
 | 
			
		||||
                  )}${
 | 
			
		||||
                    value
 | 
			
		||||
                      ? ` (${localize(
 | 
			
		||||
                          `ui.panel.lovelace.editor.card.conditional.condition.responsive.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: ResponsiveConditionData = {
 | 
			
		||||
      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 ResponsiveConditionData;
 | 
			
		||||
 | 
			
		||||
    const { breakpoints } = data;
 | 
			
		||||
 | 
			
		||||
    const condition: ResponsiveCondition = {
 | 
			
		||||
      condition: "responsive",
 | 
			
		||||
      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.responsive.${schema.name}`
 | 
			
		||||
        );
 | 
			
		||||
      default:
 | 
			
		||||
        return "";
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface HTMLElementTagNameMap {
 | 
			
		||||
    "ha-card-condition-responsive": HaCardConditionResponsive;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,55 +1,66 @@
 | 
			
		||||
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,
 | 
			
		||||
  mdiResponsive,
 | 
			
		||||
  mdiStateMachine,
 | 
			
		||||
} 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 { 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-responsive";
 | 
			
		||||
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",
 | 
			
		||||
  "responsive",
 | 
			
		||||
] as const satisfies readonly Condition["condition"][];
 | 
			
		||||
 | 
			
		||||
type UiCondition = (typeof UI_CONDITION)[number];
 | 
			
		||||
 | 
			
		||||
const cardConfigStruct = assign(
 | 
			
		||||
  baseLovelaceCardConfig,
 | 
			
		||||
  object({
 | 
			
		||||
    card: any(),
 | 
			
		||||
    conditions: optional(array(conditionStruct)),
 | 
			
		||||
    conditions: optional(array(any())),
 | 
			
		||||
  })
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const ICONS: Record<UiCondition, string> = {
 | 
			
		||||
  state: mdiStateMachine,
 | 
			
		||||
  responsive: mdiResponsive,
 | 
			
		||||
};
 | 
			
		||||
@customElement("hui-conditional-card-editor")
 | 
			
		||||
export class HuiConditionalCardEditor
 | 
			
		||||
  extends LitElement
 | 
			
		||||
@@ -127,7 +138,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 +176,44 @@ export class HuiConditionalCardEditor
 | 
			
		||||
              ${this._config.conditions.map(
 | 
			
		||||
                (cond, idx) => html`
 | 
			
		||||
                  <div class="condition">
 | 
			
		||||
                    <div class="entity">
 | 
			
		||||
                      <ha-entity-picker
 | 
			
		||||
                        .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>
 | 
			
		||||
                    <ha-card-condition-editor
 | 
			
		||||
                      .index=${idx}
 | 
			
		||||
                      @value-changed=${this._conditionChanged}
 | 
			
		||||
                      .hass=${this.hass}
 | 
			
		||||
                      .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=${ICONS[condition]}
 | 
			
		||||
                        ></ha-svg-icon>
 | 
			
		||||
                      </ha-list-item>
 | 
			
		||||
                    `
 | 
			
		||||
                  )}
 | 
			
		||||
                </ha-button-menu>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          `}
 | 
			
		||||
@@ -289,53 +282,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 +332,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);
 | 
			
		||||
 
 | 
			
		||||
@@ -4768,7 +4768,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": {
 | 
			
		||||
                "responsive": {
 | 
			
		||||
                  "label": "Responsive",
 | 
			
		||||
                  "breakpoints": "Screen sizes",
 | 
			
		||||
                  "breakpoints_list": {
 | 
			
		||||
                    "mobile": "Mobile",
 | 
			
		||||
                    "tablet": "Tablet",
 | 
			
		||||
                    "desktop": "Desktop",
 | 
			
		||||
                    "wide": "Wide"
 | 
			
		||||
                  },
 | 
			
		||||
                  "min": "min: {size}px"
 | 
			
		||||
                },
 | 
			
		||||
                "state": {
 | 
			
		||||
                  "label": "Entity state"
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            },
 | 
			
		||||
            "config": {
 | 
			
		||||
              "required": "required",
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user