Skip to content

Commit

Permalink
Add new badges design with UI editor (home-assistant#21401)
Browse files Browse the repository at this point in the history
* Add new entity badge

* Improve badge render

* Add edit mode

* Add editor

* Increase height

* Use hui-badge

* Add editor

* Add drag and drop

* Fix editor translations

* Fix icon

* Fix inactive color

* Add state content

* Add default config

* Fix types

* Add custom badge support to editor

* Fix custom badges

* Add new badges to masonry view

* fix lint

* Fix inactive color

* Fix entity filter card

* Add display type option

* Add support for picture

* Improve focus style

* Add visibility editor

* Fix visibility

* Fix add/delete card inside section

* Fix translations

* Add error badge

* Rename classes

* Fix badge type

* Remove badges from section type

* Add missing types
  • Loading branch information
piitaya authored Jul 19, 2024
1 parent ce43774 commit 729a12a
Show file tree
Hide file tree
Showing 32 changed files with 2,460 additions and 180 deletions.
5 changes: 3 additions & 2 deletions src/data/lovelace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import {
getCollection,
HassEventBase,
} from "home-assistant-js-websocket";
import { HuiBadge } from "../panels/lovelace/badges/hui-badge";
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 { Lovelace } from "../panels/lovelace/types";
import { HomeAssistant } from "../types";
import { LovelaceSectionConfig } from "./lovelace/config/section";
import { fetchConfig, LegacyLovelaceConfig } from "./lovelace/config/types";
Expand All @@ -21,7 +22,7 @@ export interface LovelaceViewElement extends HTMLElement {
narrow?: boolean;
index?: number;
cards?: HuiCard[];
badges?: LovelaceBadge[];
badges?: HuiBadge[];
sections?: HuiSection[];
isStrategy: boolean;
setConfig(config: LovelaceViewConfig): void;
Expand Down
8 changes: 8 additions & 0 deletions src/data/lovelace/config/badge.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { Condition } from "../../../panels/lovelace/common/validate-condition";

export interface LovelaceBadgeConfig {
type?: string;
[key: string]: any;
visibility?: Condition[];
}

export const defaultBadgeConfig = (entity_id: string): LovelaceBadgeConfig => ({
type: "entity",
entity: entity_id,
});
2 changes: 1 addition & 1 deletion src/data/lovelace/config/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export interface LovelaceBaseViewConfig {

export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
type?: string;
badges?: Array<string | LovelaceBadgeConfig>;
badges?: (string | LovelaceBadgeConfig)[]; // Badge can be just an entity_id
cards?: LovelaceCardConfig[];
sections?: LovelaceSectionRawConfig[];
}
Expand Down
16 changes: 16 additions & 0 deletions src/data/lovelace_custom_cards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ export interface CustomCardEntry {
documentationURL?: string;
}

export interface CustomBadgeEntry {
type: string;
name?: string;
description?: string;
preview?: boolean;
documentationURL?: string;
}

export interface CustomCardFeatureEntry {
type: string;
name?: string;
Expand All @@ -18,6 +26,7 @@ export interface CustomCardFeatureEntry {
export interface CustomCardsWindow {
customCards?: CustomCardEntry[];
customCardFeatures?: CustomCardFeatureEntry[];
customBadges?: CustomBadgeEntry[];
/**
* @deprecated Use customCardFeatures
*/
Expand All @@ -34,6 +43,9 @@ if (!("customCards" in customCardsWindow)) {
if (!("customCardFeatures" in customCardsWindow)) {
customCardsWindow.customCardFeatures = [];
}
if (!("customBadges" in customCardsWindow)) {
customCardsWindow.customBadges = [];
}
if (!("customTileFeatures" in customCardsWindow)) {
customCardsWindow.customTileFeatures = [];
}
Expand All @@ -43,10 +55,14 @@ export const getCustomCardFeatures = () => [
...customCardsWindow.customCardFeatures!,
...customCardsWindow.customTileFeatures!,
];
export const customBadges = customCardsWindow.customBadges!;

export const getCustomCardEntry = (type: string) =>
customCards.find((card) => card.type === type);

export const getCustomBadgeEntry = (type: string) =>
customBadges.find((badge) => badge.type === type);

export const isCustomType = (type: string) =>
type.startsWith(CUSTOM_TYPE_PREFIX);

Expand Down
200 changes: 200 additions & 0 deletions src/panels/lovelace/badges/hui-badge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { PropertyValues, ReactiveElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { MediaQueriesListener } from "../../../common/dom/media_query";
import "../../../components/ha-svg-icon";
import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
import type { HomeAssistant } from "../../../types";
import {
attachConditionMediaQueriesListeners,
checkConditionsMet,
} from "../common/validate-condition";
import { createBadgeElement } from "../create-element/create-badge-element";
import { createErrorBadgeConfig } from "../create-element/create-element-base";
import type { LovelaceBadge } from "../types";

declare global {
interface HASSDomEvents {
"badge-updated": undefined;
}
}

@customElement("hui-badge")
export class HuiBadge extends ReactiveElement {
@property({ type: Boolean }) public preview = false;

@property({ attribute: false }) public config?: LovelaceBadgeConfig;

@property({ attribute: false }) public hass?: HomeAssistant;

private _elementConfig?: LovelaceBadgeConfig;

public load() {
if (!this.config) {
throw new Error("Cannot build badge without config");
}
this._loadElement(this.config);
}

private _element?: LovelaceBadge;

private _listeners: MediaQueriesListener[] = [];

protected createRenderRoot() {
return this;
}

public disconnectedCallback() {
super.disconnectedCallback();
this._clearMediaQueries();
}

public connectedCallback() {
super.connectedCallback();
this._listenMediaQueries();
this._updateVisibility();
}

private _updateElement(config: LovelaceBadgeConfig) {
if (!this._element) {
return;
}
this._element.setConfig(config);
this._elementConfig = config;
fireEvent(this, "badge-updated");
}

private _loadElement(config: LovelaceBadgeConfig) {
this._element = createBadgeElement(config);
this._elementConfig = config;
if (this.hass) {
this._element.hass = this.hass;
}
this._element.addEventListener(
"ll-upgrade",
(ev: Event) => {
ev.stopPropagation();
if (this.hass) {
this._element!.hass = this.hass;
}
fireEvent(this, "badge-updated");
},
{ once: true }
);
this._element.addEventListener(
"ll-rebuild",
(ev: Event) => {
ev.stopPropagation();
this._loadElement(config);
fireEvent(this, "badge-updated");
},
{ once: true }
);
while (this.lastChild) {
this.removeChild(this.lastChild);
}
this._updateVisibility();
}

protected willUpdate(changedProps: PropertyValues<typeof this>): void {
super.willUpdate(changedProps);

if (!this._element) {
this.load();
}
}

protected update(changedProps: PropertyValues<typeof this>) {
super.update(changedProps);

if (this._element) {
if (changedProps.has("config")) {
const elementConfig = this._elementConfig;
if (this.config !== elementConfig && this.config) {
const typeChanged = this.config?.type !== elementConfig?.type;
if (typeChanged) {
this._loadElement(this.config);
} else {
this._updateElement(this.config);
}
}
}
if (changedProps.has("hass")) {
try {
if (this.hass) {
this._element.hass = this.hass;
}
} catch (e: any) {
this._loadElement(createErrorBadgeConfig(e.message, null));
}
}
}

if (changedProps.has("hass") || changedProps.has("preview")) {
this._updateVisibility();
}
}

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._updateVisibility(hasOnlyMediaQuery && matches);
}
);
}

private _updateVisibility(forceVisible?: boolean) {
if (!this._element || !this.hass) {
return;
}

if (this._element.hidden) {
this._setElementVisibility(false);
return;
}

const visible =
forceVisible ||
this.preview ||
!this.config?.visibility ||
checkConditionsMet(this.config.visibility, this.hass);
this._setElementVisibility(visible);
}

private _setElementVisibility(visible: boolean) {
if (!this._element) return;

if (this.hidden !== !visible) {
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-badge": HuiBadge;
}
}
Loading

0 comments on commit 729a12a

Please sign in to comment.