Skip to content

Commit

Permalink
Add visibility option to dashboard cards (#20840)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
piitaya authored May 29, 2024
1 parent ce5bcf6 commit 13f0149
Show file tree
Hide file tree
Showing 14 changed files with 237 additions and 139 deletions.
14 changes: 5 additions & 9 deletions src/data/lovelace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand All @@ -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;
}
Expand Down
4 changes: 3 additions & 1 deletion src/data/lovelace/config/card.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -7,4 +8,5 @@ export interface LovelaceCardConfig {
layout_options?: LovelaceLayoutOptions;
type: string;
[key: string]: any;
visibility?: Condition[];
}
141 changes: 141 additions & 0 deletions src/panels/lovelace/cards/hui-card.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
3 changes: 2 additions & 1 deletion src/panels/lovelace/common/compute-card-size.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
18 changes: 2 additions & 16 deletions src/panels/lovelace/common/validate-condition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
Expand Down
19 changes: 15 additions & 4 deletions src/panels/lovelace/components/hui-conditional-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
);
}
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions src/panels/lovelace/editor/structs/base-card-struct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export const baseLovelaceCardConfig = object({
type: string(),
view_layout: any(),
layout_options: any(),
visibility: any(),
});
27 changes: 7 additions & 20 deletions src/panels/lovelace/sections/hui-grid-section.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;

Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 13f0149

Please sign in to comment.