diff --git a/src/cards/chips-card/chips-card-chips-editor.ts b/src/cards/chips-card/chips-card-chips-editor.ts index ecf7115bd..646b65322 100644 --- a/src/cards/chips-card/chips-card-chips-editor.ts +++ b/src/cards/chips-card/chips-card-chips-editor.ts @@ -241,31 +241,23 @@ export class ChipsCardEditorChips extends MushroomBaseElement { private _renderChipLabel(chipConf: LovelaceChipConfig): string { const customLocalize = setupCustomlocalize(this.hass); - let label = customLocalize(`editor.chip.chip-picker.types.${chipConf.type}`); - if (chipConf.type === "conditional" && chipConf.conditions.length > 0) { - const condition = chipConf.conditions[0]; - const entity = this.getEntityName(condition.entity) ?? condition.entity; - label += ` - ${entity} ${ - condition.state - ? `= ${condition.state}` - : condition.state_not - ? `≠ ${condition.state_not}` - : null - }`; - } - return label; + return customLocalize(`editor.chip.chip-picker.types.${chipConf.type}`); } private _renderChipSecondary(chipConf: LovelaceChipConfig): string | undefined { const customLocalize = setupCustomlocalize(this.hass); if ("entity" in chipConf && chipConf.entity) { - return `${this.getEntityName(chipConf.entity) ?? chipConf.entity}`; + return `${this.getEntityName(chipConf.entity) ?? chipConf.entity ?? ""}`; } if ("chip" in chipConf && chipConf.chip) { const label = customLocalize(`editor.chip.chip-picker.types.${chipConf.chip.type}`); - return `${this._renderChipSecondary(chipConf.chip)} (via ${label})`; + const chipSecondary = this._renderChipSecondary(chipConf.chip); + if (chipSecondary) { + return `${this._renderChipSecondary(chipConf.chip)} (via ${label})`; + } + return label; } - return undefined; + return ""; } private getEntityName(entity_id: string): string | undefined { diff --git a/src/cards/chips-card/chips-card-editor.ts b/src/cards/chips-card/chips-card-editor.ts index f1c7c5569..2487f32c0 100644 --- a/src/cards/chips-card/chips-card-editor.ts +++ b/src/cards/chips-card/chips-card-editor.ts @@ -74,16 +74,10 @@ const weatherChipConfigStruct = object({ show_conditions: optional(boolean()), }); -const conditionStruct = object({ - entity: string(), - state: optional(string()), - state_not: optional(string()), -}); - const conditionChipConfigStruct = object({ type: literal("conditional"), chip: optional(any()), - conditions: optional(array(conditionStruct)), + conditions: optional(array(any())), }); const lightChipConfigStruct = object({ diff --git a/src/cards/chips-card/chips-card.ts b/src/cards/chips-card/chips-card.ts index 278221a8b..322d1451a 100644 --- a/src/cards/chips-card/chips-card.ts +++ b/src/cards/chips-card/chips-card.ts @@ -1,5 +1,5 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; -import { customElement, state } from "lit/decorators.js"; +import { customElement, property, state } from "lit/decorators.js"; import { computeRTL, HomeAssistant, @@ -13,7 +13,7 @@ import { registerCustomCard } from "../../utils/custom-cards"; import { createChipElement } from "../../utils/lovelace/chip/chip-element"; import { LovelaceChip, LovelaceChipConfig } from "../../utils/lovelace/chip/types"; import "./chips"; -import { EntityChip } from "./chips"; +import { EntityChip } from "./chips/entity-chip"; import { CHIPS_CARD_EDITOR_NAME, CHIPS_CARD_NAME } from "./const"; export interface ChipsCardConfig extends LovelaceCardConfig { @@ -42,6 +42,8 @@ export class ChipsCard extends LitElement implements LovelaceCard { }; } + @property() public editMode?: boolean; + @state() private _config?: ChipsCardConfig; private _hass?: HomeAssistant; @@ -94,6 +96,7 @@ export class ChipsCard extends LitElement implements LovelaceCard { } if (this._hass) { element.hass = this._hass; + element.editMode = this.editMode; } return html`${element}`; } diff --git a/src/cards/chips-card/chips/conditional-chip-editor-legacy.ts b/src/cards/chips-card/chips/conditional-chip-editor-legacy.ts new file mode 100644 index 000000000..bce8dd058 --- /dev/null +++ b/src/cards/chips-card/chips/conditional-chip-editor-legacy.ts @@ -0,0 +1,365 @@ +import type { MDCTabBarActivatedEvent } from "@material/tab-bar"; +import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { customElement, property, query, state } from "lit/decorators.js"; +import { computeRTL, fireEvent, HASSDomEvent, HomeAssistant, LovelaceConfig } from "../../../ha"; +import setupCustomlocalize from "../../../localize"; +import "../../../shared/form/mushroom-select"; +import "../../../shared/form/mushroom-textfield"; +import { getChipElementClass } from "../../../utils/lovelace/chip-element-editor"; +import { computeChipEditorComponentName } from "../../../utils/lovelace/chip/chip-element"; +import { + CHIP_LIST, + ConditionalChipConfig, + LovelaceChipConfig, +} from "../../../utils/lovelace/chip/types"; +import { GUIModeChangedEvent } from "../../../utils/lovelace/editor/types"; +import { ConfigChangedEvent } from "../../../utils/lovelace/element-editor"; +import { LovelaceChipEditor } from "../../../utils/lovelace/types"; + +@customElement(`${computeChipEditorComponentName("conditional")}-legacy`) +export class ConditionalChipEditor extends LitElement implements LovelaceChipEditor { + @property({ attribute: false }) public hass?: HomeAssistant; + + @property({ attribute: false }) public lovelace?: LovelaceConfig; + + @state() private _config?: ConditionalChipConfig; + + @state() private _GUImode = true; + + @state() private _guiModeAvailable? = true; + + @state() private _cardTab = false; + + @query("mushroom-chip-element-editor") + private _cardEditorEl?: any; + + public setConfig(config: ConditionalChipConfig): void { + this._config = config; + } + + public focusYamlEditor() { + this._cardEditorEl?.focusYamlEditor(); + } + + protected render() { + if (!this.hass || !this._config) { + return nothing; + } + + const customLocalize = setupCustomlocalize(this.hass); + + const rtl = computeRTL(this.hass); + + return html` + + + + + ${this._cardTab + ? html` +
+ ${this._config.chip?.type !== undefined + ? html` +
+ + ${this.hass!.localize( + !this._cardEditorEl || this._GUImode + ? "ui.panel.lovelace.editor.edit_card.show_code_editor" + : "ui.panel.lovelace.editor.edit_card.show_visual_editor" + )} + + ${this.hass!.localize( + "ui.panel.lovelace.editor.card.conditional.change_type" + )} +
+ + ` + : html` + e.stopPropagation()} + fixedMenuPosition + naturalMenuWidth + > + ${CHIP_LIST.map( + (chip) => + html` + + ${customLocalize( + `editor.chip.chip-picker.types.${chip}` + )} + + ` + )} + + `} +
+ ` + : html` +
+ ${this.hass!.localize( + "ui.panel.lovelace.editor.card.conditional.condition_explanation" + )} + ${this._config.conditions.map((cond, idx) => { + const stateObj = this.hass!.states[cond.entity]; + return html` +
+
+ +
+
+ e.stopPropagation()} + naturalMenuWidth + fixedMenuPosition + > + + ${this.hass!.localize( + "ui.panel.lovelace.editor.card.conditional.state_equal" + )} + + + ${this.hass!.localize( + "ui.panel.lovelace.editor.card.conditional.state_not_equal" + )} + + + + +
+
+ `; + })} +
+ +
+
+ `} + `; + } + + private _selectTab(ev: MDCTabBarActivatedEvent): void { + this._cardTab = ev.detail.index === 1; + } + + private _toggleMode(): void { + this._cardEditorEl?.toggleMode(); + } + + private _setMode(value: boolean): void { + this._GUImode = value; + if (this._cardEditorEl) { + this._cardEditorEl.GUImode = value; + } + } + + private _handleGUIModeChanged(ev: HASSDomEvent): void { + ev.stopPropagation(); + this._GUImode = ev.detail.guiMode; + this._guiModeAvailable = ev.detail.guiModeAvailable; + } + + private async _handleChipPicked(ev: CustomEvent): Promise { + const value = (ev.target as any).value; + + if (value === "") { + return; + } + + let newChip: LovelaceChipConfig; + + const elClass = getChipElementClass(value) as any; + + if (elClass && elClass.getStubConfig) { + newChip = (await elClass.getStubConfig(this.hass)) as LovelaceChipConfig; + } else { + newChip = { type: value }; + } + + (ev.target as any).value = ""; + + ev.stopPropagation(); + if (!this._config) { + return; + } + this._setMode(true); + this._guiModeAvailable = true; + this._config = { ...this._config, chip: newChip }; + fireEvent(this, "config-changed", { config: this._config }); + } + + private _handleChipChanged(ev: HASSDomEvent): void { + ev.stopPropagation(); + if (!this._config) { + return; + } + this._config = { + ...this._config, + chip: ev.detail.config as LovelaceChipConfig, + }; + this._guiModeAvailable = ev.detail.guiModeAvailable; + fireEvent(this, "config-changed", { config: this._config }); + } + + private _handleReplaceChip(): void { + if (!this._config) { + return; + } + // @ts-ignore + this._config = { ...this._config, chip: undefined }; + // @ts-ignore + fireEvent(this, "config-changed", { config: this._config }); + } + + private _addCondition(ev: Event): void { + const target = ev.target! as any; + if (target.value === "" || !this._config) { + return; + } + const conditions = [...this._config.conditions]; + conditions.push({ + entity: target.value, + state: "", + }); + 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); + } 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; + } + this._config = { ...this._config, conditions }; + fireEvent(this, "config-changed", { config: this._config }); + } + + static get styles(): CSSResultGroup { + return css` + mwc-tab-bar { + border-bottom: 1px solid var(--divider-color); + } + .conditions { + margin-top: 8px; + } + .condition { + margin-top: 8px; + border: 1px solid var(--divider-color); + padding: 12px; + } + .condition .state { + display: flex; + align-items: flex-end; + } + .condition .state mushroom-select { + margin-right: 16px; + } + .condition[rtl] .state mushroom-select { + margin-right: initial; + margin-left: 16px; + } + .card { + margin-top: 8px; + border: 1px solid var(--divider-color); + padding: 12px; + } + .card mushroom-select { + width: 100%; + margin-top: 0px; + } + @media (max-width: 450px) { + .card, + .condition { + margin: 8px -12px 0; + } + } + .card .card-options { + display: flex; + justify-content: flex-end; + width: 100%; + } + .gui-mode-button { + margin-right: auto; + } + `; + } +} diff --git a/src/cards/chips-card/chips/conditional-chip-editor.ts b/src/cards/chips-card/chips/conditional-chip-editor.ts index d129f7c38..c0957ca83 100644 --- a/src/cards/chips-card/chips/conditional-chip-editor.ts +++ b/src/cards/chips-card/chips/conditional-chip-editor.ts @@ -1,7 +1,7 @@ import type { MDCTabBarActivatedEvent } from "@material/tab-bar"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators.js"; -import { computeRTL, fireEvent, HASSDomEvent, HomeAssistant, LovelaceConfig } from "../../../ha"; +import { fireEvent, HASSDomEvent, HomeAssistant, LovelaceConfig } from "../../../ha"; import setupCustomlocalize from "../../../localize"; import "../../../shared/form/mushroom-select"; import "../../../shared/form/mushroom-textfield"; @@ -48,8 +48,6 @@ export class ConditionalChipEditor extends LitElement implements LovelaceChipEdi const customLocalize = setupCustomlocalize(this.hass); - const rtl = computeRTL(this.hass); - return html` @@ -116,76 +114,11 @@ export class ConditionalChipEditor extends LitElement implements LovelaceChipEdi ` : html` -
- ${this.hass!.localize( - "ui.panel.lovelace.editor.card.conditional.condition_explanation" - )} - ${this._config.conditions.map((cond, idx) => { - const stateObj = this.hass!.states[cond.entity]; - return html` -
-
- -
-
- e.stopPropagation()} - naturalMenuWidth - fixedMenuPosition - > - - ${this.hass!.localize( - "ui.panel.lovelace.editor.card.conditional.state_equal" - )} - - - ${this.hass!.localize( - "ui.panel.lovelace.editor.card.conditional.state_not_equal" - )} - - - - -
-
- `; - })} -
- -
-
+ `} `; } @@ -263,52 +196,12 @@ export class ConditionalChipEditor extends LitElement implements LovelaceChipEdi fireEvent(this, "config-changed", { config: this._config }); } - private _addCondition(ev: Event): void { - const target = ev.target! as any; - if (target.value === "" || !this._config) { + private _conditionChanged(ev: CustomEvent) { + ev.stopPropagation(); + if (!this._config) { return; } - const conditions = [...this._config.conditions]; - conditions.push({ - entity: target.value, - state: "", - }); - 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); - } 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; - } + const conditions = ev.detail.value; this._config = { ...this._config, conditions }; fireEvent(this, "config-changed", { config: this._config }); } @@ -318,25 +211,6 @@ export class ConditionalChipEditor extends LitElement implements LovelaceChipEdi mwc-tab-bar { border-bottom: 1px solid var(--divider-color); } - .conditions { - margin-top: 8px; - } - .condition { - margin-top: 8px; - border: 1px solid var(--divider-color); - padding: 12px; - } - .condition .state { - display: flex; - align-items: flex-end; - } - .condition .state mushroom-select { - margin-right: 16px; - } - .condition[rtl] .state mushroom-select { - margin-right: initial; - margin-left: 16px; - } .card { margin-top: 8px; border: 1px solid var(--divider-color); @@ -347,8 +221,7 @@ export class ConditionalChipEditor extends LitElement implements LovelaceChipEdi margin-top: 0px; } @media (max-width: 450px) { - .card, - .condition { + .card { margin: 8px -12px 0; } } diff --git a/src/cards/chips-card/chips/conditional-chip.ts b/src/cards/chips-card/chips/conditional-chip.ts index 728b4b537..4bfdd9069 100644 --- a/src/cards/chips-card/chips/conditional-chip.ts +++ b/src/cards/chips-card/chips/conditional-chip.ts @@ -1,5 +1,5 @@ -import { customElement } from "lit/decorators.js"; -import { ConditionalBase } from "../../../utils/conditional/conditional-base"; +import { atLeastHaVersion } from "../../../ha"; +import { loadConditionalCardComponents, loadCustomElement } from "../../../utils/loader"; import { computeChipComponentName, computeChipEditorComponentName, @@ -7,30 +7,42 @@ import { } from "../../../utils/lovelace/chip/chip-element"; import { ConditionalChipConfig, LovelaceChip } from "../../../utils/lovelace/chip/types"; import { LovelaceChipEditor } from "../../../utils/lovelace/types"; +import "./conditional-chip-editor"; +import "./conditional-chip-editor-legacy"; -@customElement(computeChipComponentName("conditional")) -export class ConditionalChip extends ConditionalBase implements LovelaceChip { - public static async getConfigElement(): Promise { - await import("./conditional-chip-editor"); - return document.createElement( - computeChipEditorComponentName("conditional") - ) as LovelaceChipEditor; - } +async function setupConditionChipComponent() { + const HuiConditionalBase = await loadCustomElement("hui-conditional-base"); + // @ts-ignore + class ConditionalChip extends HuiConditionalBase implements LovelaceChip { + public static async getConfigElement(): Promise { + const version = (document.querySelector("home-assistant")! as any).hass.connection + .haVersion; + const legacy = !atLeastHaVersion(version, 2023, 11); + const suffix = legacy ? "-legacy" : ""; + const tag = `${computeChipEditorComponentName("conditional")}${suffix}`; + return document.createElement(tag) as LovelaceChipEditor; + } - public static async getStubConfig(): Promise { - return { - type: `conditional`, - conditions: [], - }; - } + public static async getStubConfig(): Promise { + return { + type: `conditional`, + conditions: [], + }; + } - public setConfig(config: ConditionalChipConfig): void { - this.validateConfig(config); + public setConfig(config: ConditionalChipConfig): void { + this.validateConfig(config); - if (!config.chip) { - throw new Error("No row configured"); - } + if (!config.chip) { + throw new Error("No chip configured"); + } - this._element = createChipElement(config.chip) as LovelaceChip; + this._element = createChipElement(config.chip) as LovelaceChip; + } } + // @ts-ignore + customElements.define(computeChipComponentName("conditional"), ConditionalChip); } + +loadConditionalCardComponents(); +setupConditionChipComponent(); diff --git a/src/cards/chips-card/chips/index.ts b/src/cards/chips-card/chips/index.ts index 9bedc0ac3..5e2a90250 100644 --- a/src/cards/chips-card/chips/index.ts +++ b/src/cards/chips-card/chips/index.ts @@ -1,10 +1,10 @@ -export { EntityChip } from "./entity-chip"; -export { WeatherChip } from "./weather-chip"; -export { BackChip } from "./back-chip"; -export { ActionChip } from "./action-chip"; -export { MenuChip } from "./menu-chip"; -export { TemplateChip } from "./template-chip"; -export { ConditionalChip } from "./conditional-chip"; -export { LightChip } from "./light-chip"; -export { AlarmControlPanelChip } from "./alarm-control-panel-chip"; -export { SpacerChip } from "./spacer-chip"; +import "./entity-chip"; +import "./weather-chip"; +import "./back-chip"; +import "./action-chip"; +import "./menu-chip"; +import "./template-chip"; +import "./conditional-chip"; +import "./light-chip"; +import "./alarm-control-panel-chip"; +import "./spacer-chip"; diff --git a/src/mushroom.ts b/src/mushroom.ts index fd931d46f..0ddd7624b 100644 --- a/src/mushroom.ts +++ b/src/mushroom.ts @@ -5,22 +5,22 @@ import "./utils/form/custom/ha-selector-mushroom-icon-type"; import "./utils/form/custom/ha-selector-mushroom-info"; import "./utils/form/custom/ha-selector-mushroom-layout"; -export { AlarmControlPanelCard } from "./cards/alarm-control-panel-card/alarm-control-panel-card"; -export { ChipsCard } from "./cards/chips-card/chips-card"; -export { ClimateCard } from "./cards/climate-card/climate-card"; -export { CoverCard } from "./cards/cover-card/cover-card"; -export { EntityCard } from "./cards/entity-card/entity-card"; -export { FanCard } from "./cards/fan-card/fan-card"; -export { HumidifierCard } from "./cards/humidifier-card/humidifier-card"; -export { NumberCard } from "./cards/number-card/number-card"; -export { LightCard } from "./cards/light-card/light-card"; -export { LockCard } from "./cards/lock-card/lock-card"; -export { MediaPlayerCard } from "./cards/media-player-card/media-player-card"; -export { PersonCard } from "./cards/person-card/person-card"; -export { SelectCard } from "./cards/select-card/select-card"; -export { TemplateCard } from "./cards/template-card/template-card"; -export { TitleCard } from "./cards/title-card/title-card"; -export { UpdateCard } from "./cards/update-card/update-card"; -export { VacuumCard } from "./cards/vacuum-card/vacuum-card"; +import "./cards/alarm-control-panel-card/alarm-control-panel-card"; +import "./cards/chips-card/chips-card"; +import "./cards/climate-card/climate-card"; +import "./cards/cover-card/cover-card"; +import "./cards/entity-card/entity-card"; +import "./cards/fan-card/fan-card"; +import "./cards/humidifier-card/humidifier-card"; +import "./cards/number-card/number-card"; +import "./cards/light-card/light-card"; +import "./cards/lock-card/lock-card"; +import "./cards/media-player-card/media-player-card"; +import "./cards/person-card/person-card"; +import "./cards/select-card/select-card"; +import "./cards/template-card/template-card"; +import "./cards/title-card/title-card"; +import "./cards/update-card/update-card"; +import "./cards/vacuum-card/vacuum-card"; console.info(`%c🍄 Mushroom 🍄 - ${version}`, "color: #ef5350; font-weight: 700;"); diff --git a/src/utils/loader.ts b/src/utils/loader.ts index e2b8cdd86..0ffcc3baf 100644 --- a/src/utils/loader.ts +++ b/src/utils/loader.ts @@ -6,4 +6,29 @@ export const loadHaComponents = () => { if (!customElements.get("ha-entity-picker")) { (customElements.get("hui-entities-card") as any)?.getConfigElement(); } + if (!customElements.get("ha-card-conditions-editor")) { + (customElements.get("hui-conditional-card") as any)?.getConfigElement(); + } +}; + +export const loadConditionalCardComponents = async () => { + const HuiView = await loadCustomElement("hui-view"); + const view = new HuiView(); + view.lovelace = { + editMode: false, + }; + view.createCardElement({ + type: "conditional", + card: { type: "button" }, + conditions: [], + }); +}; + +export const loadCustomElement = async (name: string) => { + let Component = customElements.get(name) as T; + if (Component) { + return Component; + } + await customElements.whenDefined(name); + return customElements.get(name) as T; }; diff --git a/src/utils/lovelace/chip/chip-element.ts b/src/utils/lovelace/chip/chip-element.ts index 8a4654542..8fc6c055c 100644 --- a/src/utils/lovelace/chip/chip-element.ts +++ b/src/utils/lovelace/chip/chip-element.ts @@ -3,14 +3,26 @@ import { LovelaceChip, LovelaceChipConfig } from "./types"; export const createChipElement = (config: LovelaceChipConfig): LovelaceChip | undefined => { try { + const tag = computeChipComponentName(config.type); + if (customElements.get(tag)) { + // @ts-ignore + const element = document.createElement(tag, config) as LovelaceChip; + element.setConfig(config); + return element; + } // @ts-ignore - const element = document.createElement( - computeChipComponentName(config.type), - config - ) as LovelaceChip; - element.setConfig(config); + const element = document.createElement(tag) as LovelaceChip; + customElements.whenDefined(tag).then(() => { + try { + customElements.upgrade(element); + element.setConfig(config); + } catch (err: any) { + // Do nothing + } + }); return element; } catch (err) { + console.error(err); return undefined; } }; diff --git a/src/utils/lovelace/chip/types.ts b/src/utils/lovelace/chip/types.ts index 3cc3d3edc..cefc30914 100644 --- a/src/utils/lovelace/chip/types.ts +++ b/src/utils/lovelace/chip/types.ts @@ -1,4 +1,4 @@ -import { ActionConfig, Condition, HomeAssistant } from "../../../ha"; +import { ActionConfig, HomeAssistant } from "../../../ha"; import { Info } from "../../info"; export interface LovelaceChip extends HTMLElement { @@ -77,7 +77,7 @@ export type TemplateChipConfig = { export interface ConditionalChipConfig { type: "conditional"; chip?: LovelaceChipConfig; - conditions: Condition[]; + conditions: any[]; } export type LightChipConfig = {