Add visibility option to dashboard cards (#20840)

* Create hui card

* Add compatiblity with helpers

* Improve layout options

* Fix conditional card

* Add missing import

* Add visibility option in config

* Fix conditions

* Fix case with multiple conditions

* Remove useless set hass
This commit is contained in:
Paul Bottein 2024-05-29 17:50:16 +02:00 committed by GitHub
parent ce5bcf61f9
commit 13f01492b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 237 additions and 139 deletions

View File

@ -3,17 +3,13 @@ import {
getCollection,
HassEventBase,
} from "home-assistant-js-websocket";
import { HuiErrorCard } from "../panels/lovelace/cards/hui-error-card";
import {
Lovelace,
LovelaceBadge,
LovelaceCard,
} from "../panels/lovelace/types";
import type { HuiCard } from "../panels/lovelace/cards/hui-card";
import type { HuiSection } from "../panels/lovelace/sections/hui-section";
import { Lovelace, LovelaceBadge } from "../panels/lovelace/types";
import { HomeAssistant } from "../types";
import { LovelaceSectionConfig } from "./lovelace/config/section";
import { fetchConfig, LegacyLovelaceConfig } from "./lovelace/config/types";
import { LovelaceViewConfig } from "./lovelace/config/view";
import { HuiSection } from "../panels/lovelace/sections/hui-section";
export interface LovelacePanelConfig {
mode: "yaml" | "storage";
@ -24,7 +20,7 @@ export interface LovelaceViewElement extends HTMLElement {
lovelace?: Lovelace;
narrow?: boolean;
index?: number;
cards?: Array<LovelaceCard | HuiErrorCard>;
cards?: HuiCard[];
badges?: LovelaceBadge[];
sections?: HuiSection[];
isStrategy: boolean;
@ -36,7 +32,7 @@ export interface LovelaceSectionElement extends HTMLElement {
lovelace?: Lovelace;
viewIndex?: number;
index?: number;
cards?: Array<LovelaceCard | HuiErrorCard>;
cards?: HuiCard[];
isStrategy: boolean;
setConfig(config: LovelaceSectionConfig): void;
}

View File

@ -1,4 +1,5 @@
import { LovelaceLayoutOptions } from "../../../panels/lovelace/types";
import type { Condition } from "../../../panels/lovelace/common/validate-condition";
import type { LovelaceLayoutOptions } from "../../../panels/lovelace/types";
export interface LovelaceCardConfig {
index?: number;
@ -7,4 +8,5 @@ export interface LovelaceCardConfig {
layout_options?: LovelaceLayoutOptions;
type: string;
[key: string]: any;
visibility?: Condition[];
}

View File

@ -0,0 +1,141 @@
import { PropertyValues, ReactiveElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { MediaQueriesListener } from "../../../common/dom/media_query";
import "../../../components/ha-svg-icon";
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { HomeAssistant } from "../../../types";
import { computeCardSize } from "../common/compute-card-size";
import {
attachConditionMediaQueriesListeners,
checkConditionsMet,
} from "../common/validate-condition";
import { createCardElement } from "../create-element/create-card-element";
import type { Lovelace, LovelaceCard, LovelaceLayoutOptions } from "../types";
@customElement("hui-card")
export class HuiCard extends ReactiveElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public lovelace!: Lovelace;
@state() public _config?: LovelaceCardConfig;
private _element?: LovelaceCard;
private _listeners: MediaQueriesListener[] = [];
protected createRenderRoot() {
return this;
}
public disconnectedCallback() {
super.disconnectedCallback();
this._clearMediaQueries();
}
public connectedCallback() {
super.connectedCallback();
this._listenMediaQueries();
this._updateElement();
}
public getCardSize(): number | Promise<number> {
if (this._element) {
const size = computeCardSize(this._element);
return size;
}
return 1;
}
public getLayoutOptions(): LovelaceLayoutOptions {
const configOptions = this._config?.layout_options ?? {};
if (this._element) {
const cardOptions = this._element.getLayoutOptions?.() ?? {};
return {
...cardOptions,
...configOptions,
};
}
return configOptions;
}
public setConfig(config: LovelaceCardConfig): void {
if (this._config === config) {
return;
}
this._config = config;
this._element = createCardElement(config);
this._element.hass = this.hass;
this._element.editMode = this.lovelace.editMode;
while (this.lastChild) {
this.removeChild(this.lastChild);
}
this.appendChild(this._element!);
}
protected update(changedProperties: PropertyValues<typeof this>) {
super.update(changedProperties);
if (this._element) {
if (changedProperties.has("hass")) {
this._element.hass = this.hass;
}
if (changedProperties.has("lovelace")) {
this._element.editMode = this.lovelace.editMode;
}
if (changedProperties.has("hass") || changedProperties.has("lovelace")) {
this._updateElement();
}
}
}
private _clearMediaQueries() {
this._listeners.forEach((unsub) => unsub());
this._listeners = [];
}
private _listenMediaQueries() {
this._clearMediaQueries();
if (!this._config?.visibility) {
return;
}
const conditions = this._config.visibility;
const hasOnlyMediaQuery =
conditions.length === 1 &&
conditions[0].condition === "screen" &&
!!conditions[0].media_query;
this._listeners = attachConditionMediaQueriesListeners(
this._config.visibility,
(matches) => {
this._updateElement(hasOnlyMediaQuery && matches);
}
);
}
private _updateElement(forceVisible?: boolean) {
if (!this._element) {
return;
}
const visible =
forceVisible ||
this.lovelace.editMode ||
!this._config?.visibility ||
checkConditionsMet(this._config.visibility, this.hass);
this.style.setProperty("display", visible ? "" : "none");
this.toggleAttribute("hidden", !visible);
if (!visible && this._element.parentElement) {
this.removeChild(this._element);
} else if (visible && !this._element.parentElement) {
this.appendChild(this._element);
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-card": HuiCard;
}
}

View File

@ -1,8 +1,9 @@
import { promiseTimeout } from "../../../common/util/promise-timeout";
import { HuiCard } from "../cards/hui-card";
import { LovelaceCard, LovelaceHeaderFooter } from "../types";
export const computeCardSize = (
card: LovelaceCard | LovelaceHeaderFooter
card: LovelaceCard | LovelaceHeaderFooter | HuiCard
): number | Promise<number> => {
if (typeof card.getCardSize === "function") {
try {

View File

@ -327,27 +327,13 @@ export function extractMediaQueries(conditions: Condition[]): string[] {
export function attachConditionMediaQueriesListeners(
conditions: Condition[],
hass: HomeAssistant,
onChange: (visibility: boolean) => void
): MediaQueriesListener[] {
// For performance, if there is only one condition and it's a screen condition, set the visibility directly
if (
conditions.length === 1 &&
conditions[0].condition === "screen" &&
conditions[0].media_query
) {
const listener = listenMediaQuery(conditions[0].media_query, (matches) => {
onChange(matches);
});
return [listener];
}
const mediaQueries = extractMediaQueries(conditions);
const listeners = mediaQueries.map((query) => {
const listener = listenMediaQuery(query, () => {
const visibility = checkConditionsMet(conditions, hass);
onChange(visibility);
const listener = listenMediaQuery(query, (matches) => {
onChange(matches);
});
return listener;
});

View File

@ -84,11 +84,21 @@ export class HuiConditionalBase extends ReactiveElement {
this._clearMediaQueries();
const conditions = this._config.conditions;
const hasOnlyMediaQuery =
conditions.length === 1 &&
"condition" in conditions[0] &&
conditions[0].condition === "screen" &&
!!conditions[0].media_query;
this._listeners = attachConditionMediaQueriesListeners(
supportedConditions,
this.hass,
(visibility) => {
this._setVisibility(visibility);
(matches) => {
if (hasOnlyMediaQuery) {
this._setVisibility(matches);
return;
}
this._updateVisibility();
}
);
}
@ -99,7 +109,8 @@ export class HuiConditionalBase extends ReactiveElement {
if (
changed.has("_element") ||
changed.has("_config") ||
changed.has("hass")
changed.has("hass") ||
changed.has("editMode")
) {
this._listenMediaQueries();
this._updateVisibility();

View File

@ -3,13 +3,16 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
import { LovelaceConfig } from "../../../../data/lovelace/config/types";
import { isStrategyView } from "../../../../data/lovelace/config/view";
import { haStyleDialog } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
import "../../sections/hui-section";
import { addCards, addSection } from "../config-util";
import {
LovelaceContainerPath,
@ -18,7 +21,6 @@ import {
import "./hui-card-preview";
import { showCreateCardDialog } from "./show-create-card-dialog";
import { SuggestCardDialogParams } from "./show-suggest-card-dialog";
import { LovelaceConfig } from "../../../../data/lovelace/config/types";
@customElement("hui-dialog-suggest-card")
export class HuiDialogSuggestCard extends LitElement {

View File

@ -4,4 +4,5 @@ export const baseLovelaceCardConfig = object({
type: string(),
view_layout: any(),
layout_options: any(),
visibility: any(),
});

View File

@ -11,10 +11,10 @@ import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { HuiErrorCard } from "../cards/hui-error-card";
import "../components/hui-card-edit-mode";
import { moveCard } from "../editor/config-util";
import type { Lovelace, LovelaceCard, LovelaceLayoutOptions } from "../types";
import type { Lovelace } from "../types";
import { HuiCard } from "../cards/hui-card";
const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
delay: 100,
@ -34,9 +34,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
@property({ type: Boolean }) public isStrategy = false;
@property({ attribute: false }) public cards: Array<
LovelaceCard | HuiErrorCard
> = [];
@property({ attribute: false }) public cards: HuiCard[] = [];
@state() _config?: LovelaceSectionConfig;
@ -95,27 +93,16 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
(cardConfig) => this._getKey(cardConfig),
(_cardConfig, idx) => {
const card = this.cards![idx];
(card as any).editMode = editMode;
(card as any).lovelace = this.lovelace;
const configOptions = _cardConfig.layout_options;
const cardOptions = (card as any)?.getLayoutOptions?.() as
| LovelaceLayoutOptions
| undefined;
const options = {
...cardOptions,
...configOptions,
} as LovelaceLayoutOptions;
const layoutOptions = card.getLayoutOptions();
return html`
<div
style=${styleMap({
"--column-size": options.grid_columns,
"--row-size": options.grid_rows,
"--column-size": layoutOptions.grid_columns,
"--row-size": layoutOptions.grid_rows,
})}
class="card ${classMap({
"fit-rows": typeof options?.grid_rows === "number",
"fit-rows": typeof layoutOptions?.grid_rows === "number",
})}"
>
${editMode

View File

@ -10,16 +10,13 @@ import {
isStrategySection,
} from "../../../data/lovelace/config/section";
import type { HomeAssistant } from "../../../types";
import type { HuiErrorCard } from "../cards/hui-error-card";
import "../cards/hui-card";
import type { HuiCard } from "../cards/hui-card";
import {
checkConditionsMet,
attachConditionMediaQueriesListeners,
checkConditionsMet,
} from "../common/validate-condition";
import { createCardElement } from "../create-element/create-card-element";
import {
createErrorCardConfig,
createErrorCardElement,
} from "../create-element/create-element-base";
import { createErrorCardConfig } from "../create-element/create-element-base";
import { createSectionElement } from "../create-element/create-section-element";
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
@ -27,7 +24,7 @@ import { deleteCard } from "../editor/config-util";
import { confDeleteCard } from "../editor/delete-card";
import { parseLovelaceCardPath } from "../editor/lovelace-path";
import { generateLovelaceSectionStrategy } from "../strategies/get-strategy";
import type { Lovelace, LovelaceCard } from "../types";
import type { Lovelace } from "../types";
import { DEFAULT_SECTION_LAYOUT } from "./const";
@customElement("hui-section")
@ -42,7 +39,7 @@ export class HuiSection extends ReactiveElement {
@property({ type: Number }) public viewIndex!: number;
@state() private _cards: Array<LovelaceCard | HuiErrorCard> = [];
@state() private _cards: HuiCard[] = [];
private _layoutElementType?: string;
@ -52,14 +49,10 @@ export class HuiSection extends ReactiveElement {
// Public to make demo happy
public createCardElement(cardConfig: LovelaceCardConfig) {
const element = createCardElement(cardConfig) as LovelaceCard;
try {
element.hass = this.hass;
} catch (e: any) {
return createErrorCardElement(
createErrorCardConfig(e.message, cardConfig)
);
}
const element = document.createElement("hui-card");
element.hass = this.hass;
element.lovelace = this.lovelace;
element.setConfig(cardConfig);
element.addEventListener(
"ll-rebuild",
(ev: Event) => {
@ -131,6 +124,13 @@ export class HuiSection extends ReactiveElement {
}
if (changedProperties.has("lovelace")) {
this._layoutElement.lovelace = this.lovelace;
this._cards.forEach((element) => {
try {
element.lovelace = this.lovelace;
} catch (e: any) {
this._rebuildCard(element, createErrorCardConfig(e.message, null));
}
});
}
if (changedProperties.has("_cards")) {
this._layoutElement.cards = this._cards;
@ -147,16 +147,20 @@ export class HuiSection extends ReactiveElement {
}
private _listenMediaQueries() {
if (!this.config.visibility) {
this._clearMediaQueries();
if (!this.config?.visibility) {
return;
}
this._clearMediaQueries();
const conditions = this.config.visibility;
const hasOnlyMediaQuery =
conditions.length === 1 &&
conditions[0].condition === "screen" &&
conditions[0].media_query != null;
this._listeners = attachConditionMediaQueriesListeners(
this.config.visibility,
this.hass,
(visibility) => {
const visible = visibility || this.lovelace!.editMode;
this._updateElement(visible);
(matches) => {
this._updateElement(hasOnlyMediaQuery && matches);
}
);
}
@ -210,10 +214,10 @@ export class HuiSection extends ReactiveElement {
return;
}
const visible =
forceVisible ??
(this.lovelace.editMode ||
!this.config.visibility ||
checkConditionsMet(this.config.visibility, this.hass));
forceVisible ||
this.lovelace.editMode ||
!this.config.visibility ||
checkConditionsMet(this.config.visibility, this.hass);
this.style.setProperty("display", visible ? "" : "none");
this.toggleAttribute("hidden", !visible);
@ -267,29 +271,15 @@ export class HuiSection extends ReactiveElement {
this._cards = config.cards.map((cardConfig) => {
const element = this.createCardElement(cardConfig);
try {
element.hass = this.hass;
} catch (e: any) {
return createErrorCardElement(
createErrorCardConfig(e.message, cardConfig)
);
}
return element;
});
}
private _rebuildCard(
cardElToReplace: LovelaceCard,
cardElToReplace: HuiCard,
config: LovelaceCardConfig
): void {
let newCardEl = this.createCardElement(config);
try {
newCardEl.hass = this.hass;
} catch (e: any) {
newCardEl = createErrorCardElement(
createErrorCardConfig(e.message, config)
);
}
const newCardEl = this.createCardElement(config);
if (cardElToReplace.parentElement) {
cardElToReplace.parentElement!.replaceChild(newCardEl, cardElToReplace);
}

View File

@ -15,7 +15,7 @@ import "../../../components/ha-svg-icon";
import type { LovelaceViewElement } from "../../../data/lovelace";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import type { HuiErrorCard } from "../cards/hui-error-card";
import { HuiCard } from "../cards/hui-card";
import { computeCardSize } from "../common/compute-card-size";
import type { Lovelace, LovelaceBadge, LovelaceCard } from "../types";
@ -48,9 +48,7 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
@property({ type: Boolean }) public isStrategy = false;
@property({ attribute: false }) public cards: Array<
LovelaceCard | HuiErrorCard
> = [];
@property({ attribute: false }) public cards: HuiCard[] = [];
@property({ attribute: false }) public badges: LovelaceBadge[] = [];

View File

@ -14,7 +14,7 @@ import { computeRTL } from "../../../common/util/compute_rtl";
import type { LovelaceViewElement } from "../../../data/lovelace";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import { HuiErrorCard } from "../cards/hui-error-card";
import { HuiCard } from "../cards/hui-card";
import { HuiCardOptions } from "../components/hui-card-options";
import { HuiWarning } from "../components/hui-warning";
import type { Lovelace, LovelaceCard } from "../types";
@ -30,9 +30,7 @@ export class PanelView extends LitElement implements LovelaceViewElement {
@property({ type: Boolean }) public isStrategy = false;
@property({ attribute: false }) public cards: Array<
LovelaceCard | HuiErrorCard
> = [];
@property({ attribute: false }) public cards: HuiCard[] = [];
@state() private _card?: LovelaceCard | HuiWarning | HuiCardOptions;

View File

@ -12,7 +12,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
import type { LovelaceViewElement } from "../../../data/lovelace";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import { HuiErrorCard } from "../cards/hui-error-card";
import { HuiCard } from "../cards/hui-card";
import { HuiCardOptions } from "../components/hui-card-options";
import { replaceCard } from "../editor/config-util";
import type { Lovelace, LovelaceCard } from "../types";
@ -26,9 +26,7 @@ export class SideBarView extends LitElement implements LovelaceViewElement {
@property({ type: Boolean }) public isStrategy = false;
@property({ attribute: false }) public cards: Array<
LovelaceCard | HuiErrorCard
> = [];
@property({ attribute: false }) public cards: HuiCard[] = [];
@state() private _config?: LovelaceViewConfig;

View File

@ -17,14 +17,11 @@ import {
createErrorBadgeConfig,
createErrorBadgeElement,
} from "../badges/hui-error-badge";
import type { HuiErrorCard } from "../cards/hui-error-card";
import "../cards/hui-card";
import type { HuiCard } from "../cards/hui-card";
import { processConfigEntities } from "../common/process-config-entities";
import { createBadgeElement } from "../create-element/create-badge-element";
import { createCardElement } from "../create-element/create-card-element";
import {
createErrorCardConfig,
createErrorCardElement,
} from "../create-element/create-element-base";
import { createErrorCardConfig } from "../create-element/create-element-base";
import { createViewElement } from "../create-element/create-view-element";
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
@ -38,7 +35,7 @@ import { createErrorSectionConfig } from "../sections/hui-error-section";
import "../sections/hui-section";
import type { HuiSection } from "../sections/hui-section";
import { generateLovelaceViewStrategy } from "../strategies/get-strategy";
import type { Lovelace, LovelaceBadge, LovelaceCard } from "../types";
import type { Lovelace, LovelaceBadge } from "../types";
import { DEFAULT_VIEW_LAYOUT, PANEL_VIEW_LAYOUT } from "./const";
declare global {
@ -65,7 +62,7 @@ export class HUIView extends ReactiveElement {
@property({ type: Number }) public index!: number;
@state() private _cards: Array<LovelaceCard | HuiErrorCard> = [];
@state() private _cards: HuiCard[] = [];
@state() private _badges: LovelaceBadge[] = [];
@ -79,14 +76,10 @@ export class HUIView extends ReactiveElement {
// Public to make demo happy
public createCardElement(cardConfig: LovelaceCardConfig) {
const element = createCardElement(cardConfig) as LovelaceCard;
try {
element.hass = this.hass;
} catch (e: any) {
return createErrorCardElement(
createErrorCardConfig(e.message, cardConfig)
);
}
const element = document.createElement("hui-card");
element.hass = this.hass;
element.lovelace = this.lovelace;
element.setConfig(cardConfig);
element.addEventListener(
"ll-rebuild",
(ev: Event) => {
@ -233,6 +226,14 @@ export class HUIView extends ReactiveElement {
this._rebuildSection(element, createErrorSectionConfig(e.message));
}
});
this._cards.forEach((element) => {
try {
element.hass = this.hass;
element.lovelace = this.lovelace;
} catch (e: any) {
this._rebuildCard(element, createErrorCardConfig(e.message, null));
}
});
}
if (changedProperties.has("_cards")) {
this._layoutElement.cards = this._cards;
@ -371,13 +372,6 @@ export class HUIView extends ReactiveElement {
this._cards = config.cards.map((cardConfig) => {
const element = this.createCardElement(cardConfig);
try {
element.hass = this.hass;
} catch (e: any) {
return createErrorCardElement(
createErrorCardConfig(e.message, cardConfig)
);
}
return element;
});
}
@ -396,17 +390,10 @@ export class HUIView extends ReactiveElement {
}
private _rebuildCard(
cardElToReplace: LovelaceCard,
cardElToReplace: HuiCard,
config: LovelaceCardConfig
): void {
let newCardEl = this.createCardElement(config);
try {
newCardEl.hass = this.hass;
} catch (e: any) {
newCardEl = createErrorCardElement(
createErrorCardConfig(e.message, config)
);
}
const newCardEl = this.createCardElement(config);
if (cardElToReplace.parentElement) {
cardElToReplace.parentElement!.replaceChild(newCardEl, cardElToReplace);
}