mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +00:00
Allow different types of heading badges (#22109)
* Allow different type of heading item * Update editor * Migrate entities to items * Rename support for string entity * Refactor * Rename to badges and add error state * Update font weight * Feedback * Feedback
This commit is contained in:
parent
468660d235
commit
a92dab46c2
58
src/components/ha-heading-badge.ts
Normal file
58
src/components/ha-heading-badge.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
|
|
||||||
|
type HeadingBadgeType = "text" | "button";
|
||||||
|
|
||||||
|
@customElement("ha-heading-badge")
|
||||||
|
export class HaBadge extends LitElement {
|
||||||
|
@property() public type: HeadingBadgeType = "text";
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="heading-badge"
|
||||||
|
role=${ifDefined(this.type === "button" ? "button" : undefined)}
|
||||||
|
tabindex=${ifDefined(this.type === "button" ? "0" : undefined)}
|
||||||
|
>
|
||||||
|
<slot name="icon"></slot>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
[role="button"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.heading-badge {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
white-space: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
font-family: Roboto;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px;
|
||||||
|
letter-spacing: 0.1px;
|
||||||
|
--mdc-icon-size: 14px;
|
||||||
|
}
|
||||||
|
::slotted([slot="icon"]) {
|
||||||
|
--ha-icon-display: block;
|
||||||
|
color: var(--icon-color, inherit);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-heading-badge": HaBadge;
|
||||||
|
}
|
||||||
|
}
|
@ -1,248 +0,0 @@
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
|
||||||
import {
|
|
||||||
CSSResultGroup,
|
|
||||||
LitElement,
|
|
||||||
PropertyValues,
|
|
||||||
css,
|
|
||||||
html,
|
|
||||||
nothing,
|
|
||||||
} from "lit";
|
|
||||||
import { customElement, property } from "lit/decorators";
|
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
|
||||||
import { styleMap } from "lit/directives/style-map";
|
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { computeCssColor } from "../../../../common/color/compute-color";
|
|
||||||
import {
|
|
||||||
hsv2rgb,
|
|
||||||
rgb2hex,
|
|
||||||
rgb2hsv,
|
|
||||||
} from "../../../../common/color/convert-color";
|
|
||||||
import { MediaQueriesListener } from "../../../../common/dom/media_query";
|
|
||||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
|
||||||
import { stateActive } from "../../../../common/entity/state_active";
|
|
||||||
import { stateColorCss } from "../../../../common/entity/state_color";
|
|
||||||
import "../../../../components/ha-card";
|
|
||||||
import "../../../../components/ha-icon";
|
|
||||||
import "../../../../components/ha-icon-next";
|
|
||||||
import "../../../../components/ha-state-icon";
|
|
||||||
import { ActionHandlerEvent } from "../../../../data/lovelace/action_handler";
|
|
||||||
import "../../../../state-display/state-display";
|
|
||||||
import { HomeAssistant } from "../../../../types";
|
|
||||||
import { actionHandler } from "../../common/directives/action-handler-directive";
|
|
||||||
import { handleAction } from "../../common/handle-action";
|
|
||||||
import { hasAction } from "../../common/has-action";
|
|
||||||
import {
|
|
||||||
attachConditionMediaQueriesListeners,
|
|
||||||
checkConditionsMet,
|
|
||||||
} from "../../common/validate-condition";
|
|
||||||
import { DEFAULT_CONFIG } from "../../editor/heading-entity/hui-heading-entity-editor";
|
|
||||||
import type { HeadingEntityConfig } from "../types";
|
|
||||||
|
|
||||||
@customElement("hui-heading-entity")
|
|
||||||
export class HuiHeadingEntity extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public config!: HeadingEntityConfig | string;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public preview = false;
|
|
||||||
|
|
||||||
private _listeners: MediaQueriesListener[] = [];
|
|
||||||
|
|
||||||
private _handleAction(ev: ActionHandlerEvent) {
|
|
||||||
const config: HeadingEntityConfig = {
|
|
||||||
tap_action: {
|
|
||||||
action: "none",
|
|
||||||
},
|
|
||||||
...this._config(this.config),
|
|
||||||
};
|
|
||||||
handleAction(this, this.hass!, config, ev.detail.action!);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _config = memoizeOne(
|
|
||||||
(configOrString: HeadingEntityConfig | string): HeadingEntityConfig => {
|
|
||||||
const config =
|
|
||||||
typeof configOrString === "string"
|
|
||||||
? { entity: configOrString }
|
|
||||||
: configOrString;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...DEFAULT_CONFIG,
|
|
||||||
tap_action: {
|
|
||||||
action: "none",
|
|
||||||
},
|
|
||||||
...config,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
public disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
this._clearMediaQueries();
|
|
||||||
}
|
|
||||||
|
|
||||||
public connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
this._listenMediaQueries();
|
|
||||||
this._updateVisibility();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected update(changedProps: PropertyValues<typeof this>): void {
|
|
||||||
super.update(changedProps);
|
|
||||||
if (changedProps.has("hass") || changedProps.has("preview")) {
|
|
||||||
this._updateVisibility();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _updateVisibility(forceVisible?: boolean) {
|
|
||||||
const config = this._config(this.config);
|
|
||||||
const visible =
|
|
||||||
forceVisible ||
|
|
||||||
this.preview ||
|
|
||||||
!config.visibility ||
|
|
||||||
checkConditionsMet(config.visibility, this.hass);
|
|
||||||
this.toggleAttribute("hidden", !visible);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _clearMediaQueries() {
|
|
||||||
this._listeners.forEach((unsub) => unsub());
|
|
||||||
this._listeners = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
private _listenMediaQueries() {
|
|
||||||
const config = this._config(this.config);
|
|
||||||
if (!config?.visibility) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const conditions = config.visibility;
|
|
||||||
const hasOnlyMediaQuery =
|
|
||||||
conditions.length === 1 &&
|
|
||||||
conditions[0].condition === "screen" &&
|
|
||||||
!!conditions[0].media_query;
|
|
||||||
|
|
||||||
this._listeners = attachConditionMediaQueriesListeners(
|
|
||||||
config.visibility,
|
|
||||||
(matches) => {
|
|
||||||
this._updateVisibility(hasOnlyMediaQuery && matches);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _computeStateColor = memoizeOne(
|
|
||||||
(entity: HassEntity, color?: string) => {
|
|
||||||
if (!color || color === "none") {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (color === "state") {
|
|
||||||
// Use light color if the light support rgb
|
|
||||||
if (
|
|
||||||
computeDomain(entity.entity_id) === "light" &&
|
|
||||||
entity.attributes.rgb_color
|
|
||||||
) {
|
|
||||||
const hsvColor = rgb2hsv(entity.attributes.rgb_color);
|
|
||||||
|
|
||||||
// Modify the real rgb color for better contrast
|
|
||||||
if (hsvColor[1] < 0.4) {
|
|
||||||
// Special case for very light color (e.g: white)
|
|
||||||
if (hsvColor[1] < 0.1) {
|
|
||||||
hsvColor[2] = 225;
|
|
||||||
} else {
|
|
||||||
hsvColor[1] = 0.4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rgb2hex(hsv2rgb(hsvColor));
|
|
||||||
}
|
|
||||||
// Fallback to state color
|
|
||||||
return stateColorCss(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (color) {
|
|
||||||
// Use custom color if active
|
|
||||||
return stateActive(entity) ? computeCssColor(color) : undefined;
|
|
||||||
}
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
protected render() {
|
|
||||||
const config = this._config(this.config);
|
|
||||||
|
|
||||||
const stateObj = this.hass!.states[config.entity];
|
|
||||||
|
|
||||||
if (!stateObj) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
const color = this._computeStateColor(stateObj, config.color);
|
|
||||||
|
|
||||||
const actionable = hasAction(config.tap_action);
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
"--color": color,
|
|
||||||
};
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div
|
|
||||||
class="entity"
|
|
||||||
@action=${this._handleAction}
|
|
||||||
.actionHandler=${actionHandler()}
|
|
||||||
role=${ifDefined(actionable ? "button" : undefined)}
|
|
||||||
tabindex=${ifDefined(actionable ? "0" : undefined)}
|
|
||||||
style=${styleMap(style)}
|
|
||||||
>
|
|
||||||
${config.show_icon
|
|
||||||
? html`
|
|
||||||
<ha-state-icon
|
|
||||||
.hass=${this.hass}
|
|
||||||
.icon=${config.icon}
|
|
||||||
.stateObj=${stateObj}
|
|
||||||
></ha-state-icon>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
${config.show_state
|
|
||||||
? html`
|
|
||||||
<state-display
|
|
||||||
.hass=${this.hass}
|
|
||||||
.stateObj=${stateObj}
|
|
||||||
.content=${config.state_content}
|
|
||||||
></state-display>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return css`
|
|
||||||
[role="button"] {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.entity {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
white-space: nowrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 3px;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
font-family: Roboto;
|
|
||||||
font-size: 14px;
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 20px; /* 142.857% */
|
|
||||||
letter-spacing: 0.1px;
|
|
||||||
--mdc-icon-size: 14px;
|
|
||||||
--state-inactive-color: initial;
|
|
||||||
}
|
|
||||||
.entity ha-state-icon {
|
|
||||||
--ha-icon-display: block;
|
|
||||||
color: var(--color);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"hui-heading-entity": HuiHeadingEntity;
|
|
||||||
}
|
|
||||||
}
|
|
@ -11,14 +11,25 @@ import { HomeAssistant } from "../../../types";
|
|||||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||||
import { handleAction } from "../common/handle-action";
|
import { handleAction } from "../common/handle-action";
|
||||||
import { hasAction } from "../common/has-action";
|
import { hasAction } from "../common/has-action";
|
||||||
|
import "../heading-badges/hui-heading-badge";
|
||||||
import type {
|
import type {
|
||||||
LovelaceCard,
|
LovelaceCard,
|
||||||
LovelaceCardEditor,
|
LovelaceCardEditor,
|
||||||
LovelaceLayoutOptions,
|
LovelaceLayoutOptions,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import "./heading/hui-heading-entity";
|
|
||||||
import type { HeadingCardConfig } from "./types";
|
import type { HeadingCardConfig } from "./types";
|
||||||
|
|
||||||
|
export const migrateHeadingCardConfig = (
|
||||||
|
config: HeadingCardConfig
|
||||||
|
): HeadingCardConfig => {
|
||||||
|
const newConfig = { ...config };
|
||||||
|
if (newConfig.entities) {
|
||||||
|
newConfig.badges = [...(newConfig.badges || []), ...newConfig.entities];
|
||||||
|
delete newConfig.entities;
|
||||||
|
}
|
||||||
|
return newConfig;
|
||||||
|
};
|
||||||
|
|
||||||
@customElement("hui-heading-card")
|
@customElement("hui-heading-card")
|
||||||
export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||||
@ -45,7 +56,7 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
|||||||
tap_action: {
|
tap_action: {
|
||||||
action: "none",
|
action: "none",
|
||||||
},
|
},
|
||||||
...config,
|
...migrateHeadingCardConfig(config),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,6 +84,8 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
|||||||
|
|
||||||
const style = this._config.heading_style || "title";
|
const style = this._config.heading_style || "title";
|
||||||
|
|
||||||
|
const badges = this._config.badges;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-card>
|
<ha-card>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@ -91,17 +104,17 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
|||||||
: nothing}
|
: nothing}
|
||||||
${actionable ? html`<ha-icon-next></ha-icon-next>` : nothing}
|
${actionable ? html`<ha-icon-next></ha-icon-next>` : nothing}
|
||||||
</div>
|
</div>
|
||||||
${this._config.entities?.length
|
${badges?.length
|
||||||
? html`
|
? html`
|
||||||
<div class="entities">
|
<div class="badges">
|
||||||
${this._config.entities.map(
|
${badges.map(
|
||||||
(config) => html`
|
(config) => html`
|
||||||
<hui-heading-entity
|
<hui-heading-badge
|
||||||
.config=${config}
|
.config=${config}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.preview=${this.preview}
|
.preview=${this.preview}
|
||||||
>
|
>
|
||||||
</hui-heading-entity>
|
</hui-heading-badge>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -150,7 +163,7 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
|||||||
.container .content:not(:has(p)) {
|
.container .content:not(:has(p)) {
|
||||||
min-width: fit-content;
|
min-width: fit-content;
|
||||||
}
|
}
|
||||||
.container .entities {
|
.container .badges {
|
||||||
flex: 0 0;
|
flex: 0 0;
|
||||||
}
|
}
|
||||||
.content {
|
.content {
|
||||||
@ -186,7 +199,7 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
.entities {
|
.badges {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
LovelaceRowConfig,
|
LovelaceRowConfig,
|
||||||
} from "../entity-rows/types";
|
} from "../entity-rows/types";
|
||||||
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
||||||
|
import { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
|
||||||
|
|
||||||
export type AlarmPanelCardConfigState =
|
export type AlarmPanelCardConfigState =
|
||||||
| "arm_away"
|
| "arm_away"
|
||||||
@ -503,21 +504,12 @@ export interface TileCardConfig extends LovelaceCardConfig {
|
|||||||
features?: LovelaceCardFeatureConfig[];
|
features?: LovelaceCardFeatureConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HeadingEntityConfig {
|
|
||||||
entity: string;
|
|
||||||
state_content?: string | string[];
|
|
||||||
icon?: string;
|
|
||||||
show_state?: boolean;
|
|
||||||
show_icon?: boolean;
|
|
||||||
color?: string;
|
|
||||||
tap_action?: ActionConfig;
|
|
||||||
visibility?: Condition[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HeadingCardConfig extends LovelaceCardConfig {
|
export interface HeadingCardConfig extends LovelaceCardConfig {
|
||||||
heading_style?: "title" | "subtitle";
|
heading_style?: "title" | "subtitle";
|
||||||
heading?: string;
|
heading?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
tap_action?: ActionConfig;
|
tap_action?: ActionConfig;
|
||||||
entities?: (string | HeadingEntityConfig)[];
|
badges?: LovelaceHeadingBadgeConfig[];
|
||||||
|
/** @deprecated Use `badges` instead */
|
||||||
|
entities?: LovelaceHeadingBadgeConfig[];
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import type { ErrorCardConfig } from "../cards/types";
|
|||||||
import { LovelaceElement, LovelaceElementConfig } from "../elements/types";
|
import { LovelaceElement, LovelaceElementConfig } from "../elements/types";
|
||||||
import { LovelaceRow, LovelaceRowConfig } from "../entity-rows/types";
|
import { LovelaceRow, LovelaceRowConfig } from "../entity-rows/types";
|
||||||
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
||||||
|
import { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
|
||||||
import {
|
import {
|
||||||
LovelaceBadge,
|
LovelaceBadge,
|
||||||
LovelaceBadgeConstructor,
|
LovelaceBadgeConstructor,
|
||||||
@ -26,6 +27,8 @@ import {
|
|||||||
LovelaceElementConstructor,
|
LovelaceElementConstructor,
|
||||||
LovelaceHeaderFooter,
|
LovelaceHeaderFooter,
|
||||||
LovelaceHeaderFooterConstructor,
|
LovelaceHeaderFooterConstructor,
|
||||||
|
LovelaceHeadingBadge,
|
||||||
|
LovelaceHeadingBadgeConstructor,
|
||||||
LovelaceRowConstructor,
|
LovelaceRowConstructor,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
@ -72,6 +75,11 @@ interface CreateElementConfigTypes {
|
|||||||
element: LovelaceSectionElement;
|
element: LovelaceSectionElement;
|
||||||
constructor: unknown;
|
constructor: unknown;
|
||||||
};
|
};
|
||||||
|
"heading-badge": {
|
||||||
|
config: LovelaceHeadingBadgeConfig;
|
||||||
|
element: LovelaceHeadingBadge;
|
||||||
|
constructor: LovelaceHeadingBadgeConstructor;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createErrorCardElement = (config: ErrorCardConfig) => {
|
export const createErrorCardElement = (config: ErrorCardConfig) => {
|
||||||
@ -102,6 +110,20 @@ export const createErrorBadgeElement = (config: ErrorCardConfig) => {
|
|||||||
return el;
|
return el;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createErrorHeadingBadgeElement = (config: ErrorCardConfig) => {
|
||||||
|
const el = document.createElement("hui-error-heading-badge");
|
||||||
|
if (customElements.get("hui-error-heading-badge")) {
|
||||||
|
el.setConfig(config);
|
||||||
|
} else {
|
||||||
|
import("../heading-badges/hui-error-heading-badge");
|
||||||
|
customElements.whenDefined("hui-error-heading-badge").then(() => {
|
||||||
|
customElements.upgrade(el);
|
||||||
|
el.setConfig(config);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
|
||||||
export const createErrorCardConfig = (error, origConfig) => ({
|
export const createErrorCardConfig = (error, origConfig) => ({
|
||||||
type: "error",
|
type: "error",
|
||||||
error,
|
error,
|
||||||
@ -114,6 +136,12 @@ export const createErrorBadgeConfig = (error, origConfig) => ({
|
|||||||
origConfig,
|
origConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const createErrorHeadingBadgeConfig = (error, origConfig) => ({
|
||||||
|
type: "error",
|
||||||
|
error,
|
||||||
|
origConfig,
|
||||||
|
});
|
||||||
|
|
||||||
const _createElement = <T extends keyof CreateElementConfigTypes>(
|
const _createElement = <T extends keyof CreateElementConfigTypes>(
|
||||||
tag: string,
|
tag: string,
|
||||||
config: CreateElementConfigTypes[T]["config"]
|
config: CreateElementConfigTypes[T]["config"]
|
||||||
@ -134,6 +162,11 @@ const _createErrorElement = <T extends keyof CreateElementConfigTypes>(
|
|||||||
if (tagSuffix === "badge") {
|
if (tagSuffix === "badge") {
|
||||||
return createErrorBadgeElement(createErrorBadgeConfig(error, config));
|
return createErrorBadgeElement(createErrorBadgeConfig(error, config));
|
||||||
}
|
}
|
||||||
|
if (tagSuffix === "heading-badge") {
|
||||||
|
return createErrorHeadingBadgeElement(
|
||||||
|
createErrorHeadingBadgeConfig(error, config)
|
||||||
|
);
|
||||||
|
}
|
||||||
return createErrorCardElement(createErrorCardConfig(error, config));
|
return createErrorCardElement(createErrorCardConfig(error, config));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
import "../heading-badges/hui-entity-heading-badge";
|
||||||
|
|
||||||
|
import {
|
||||||
|
createLovelaceElement,
|
||||||
|
getLovelaceElementClass,
|
||||||
|
} from "./create-element-base";
|
||||||
|
import { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
|
||||||
|
|
||||||
|
const ALWAYS_LOADED_TYPES = new Set(["error", "entity"]);
|
||||||
|
|
||||||
|
export const createHeadingBadgeElement = (config: LovelaceHeadingBadgeConfig) =>
|
||||||
|
createLovelaceElement(
|
||||||
|
"heading-badge",
|
||||||
|
config,
|
||||||
|
ALWAYS_LOADED_TYPES,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
"entity"
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getHeadingBadgeElementClass = (type: string) =>
|
||||||
|
getLovelaceElementClass(type, "heading-badge", ALWAYS_LOADED_TYPES);
|
@ -14,23 +14,21 @@ import "../../../../components/ha-list-item";
|
|||||||
import "../../../../components/ha-sortable";
|
import "../../../../components/ha-sortable";
|
||||||
import "../../../../components/ha-svg-icon";
|
import "../../../../components/ha-svg-icon";
|
||||||
import { HomeAssistant } from "../../../../types";
|
import { HomeAssistant } from "../../../../types";
|
||||||
|
import { LovelaceHeadingBadgeConfig } from "../../heading-badges/types";
|
||||||
type EntityConfig = {
|
|
||||||
entity: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
"edit-entity": { index: number };
|
"edit-heading-badge": { index: number };
|
||||||
|
"heading-badges-changed": { badges: LovelaceHeadingBadgeConfig[] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@customElement("hui-entities-editor")
|
@customElement("hui-heading-badges-editor")
|
||||||
export class HuiEntitiesEditor extends LitElement {
|
export class HuiHeadingBadgesEditor extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public entities?: EntityConfig[];
|
public badges?: LovelaceHeadingBadgeConfig[];
|
||||||
|
|
||||||
@query(".add-container", true) private _addContainer?: HTMLDivElement;
|
@query(".add-container", true) private _addContainer?: HTMLDivElement;
|
||||||
|
|
||||||
@ -40,14 +38,30 @@ export class HuiEntitiesEditor extends LitElement {
|
|||||||
|
|
||||||
private _opened = false;
|
private _opened = false;
|
||||||
|
|
||||||
private _entitiesKeys = new WeakMap<EntityConfig, string>();
|
private _badgesKeys = new WeakMap<LovelaceHeadingBadgeConfig, string>();
|
||||||
|
|
||||||
private _getKey(entity: EntityConfig) {
|
private _getKey(badge: LovelaceHeadingBadgeConfig) {
|
||||||
if (!this._entitiesKeys.has(entity)) {
|
if (!this._badgesKeys.has(badge)) {
|
||||||
this._entitiesKeys.set(entity, Math.random().toString());
|
this._badgesKeys.set(badge, Math.random().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._entitiesKeys.get(entity)!;
|
return this._badgesKeys.get(badge)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _computeBadgeLabel(badge: LovelaceHeadingBadgeConfig) {
|
||||||
|
const type = badge.type ?? "entity";
|
||||||
|
|
||||||
|
if (type === "entity") {
|
||||||
|
const entityId = "entity" in badge ? (badge.entity as string) : undefined;
|
||||||
|
const stateObj = entityId ? this.hass.states[entityId] : undefined;
|
||||||
|
return (
|
||||||
|
(stateObj && stateObj.attributes.friendly_name) ||
|
||||||
|
entityId ||
|
||||||
|
type ||
|
||||||
|
"Unknown badge"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
@ -56,46 +70,35 @@ export class HuiEntitiesEditor extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${this.entities
|
${this.badges
|
||||||
? html`
|
? html`
|
||||||
<ha-sortable
|
<ha-sortable
|
||||||
handle-selector=".handle"
|
handle-selector=".handle"
|
||||||
@item-moved=${this._entityMoved}
|
@item-moved=${this._badgeMoved}
|
||||||
>
|
>
|
||||||
<div class="entities">
|
<div class="entities">
|
||||||
${repeat(
|
${repeat(
|
||||||
this.entities,
|
this.badges,
|
||||||
(entityConf) => this._getKey(entityConf),
|
(badge) => this._getKey(badge),
|
||||||
(entityConf, index) => {
|
(badge, index) => {
|
||||||
const editable = true;
|
const label = this._computeBadgeLabel(badge);
|
||||||
|
|
||||||
const entityId = entityConf.entity;
|
|
||||||
const stateObj = this.hass.states[entityId];
|
|
||||||
const name = stateObj
|
|
||||||
? stateObj.attributes.friendly_name
|
|
||||||
: undefined;
|
|
||||||
return html`
|
return html`
|
||||||
<div class="entity">
|
<div class="badge">
|
||||||
<div class="handle">
|
<div class="handle">
|
||||||
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
||||||
</div>
|
</div>
|
||||||
<div class="entity-content">
|
<div class="badge-content">
|
||||||
<span>${name || entityId}</span>
|
<span>${label}</span>
|
||||||
</div>
|
</div>
|
||||||
${editable
|
<ha-icon-button
|
||||||
? html`
|
.label=${this.hass!.localize(
|
||||||
<ha-icon-button
|
`ui.panel.lovelace.editor.entities.edit`
|
||||||
.label=${this.hass!.localize(
|
)}
|
||||||
`ui.panel.lovelace.editor.entities.edit`
|
.path=${mdiPencil}
|
||||||
)}
|
class="edit-icon"
|
||||||
.path=${mdiPencil}
|
.index=${index}
|
||||||
class="edit-icon"
|
@click=${this._editBadge}
|
||||||
.index=${index}
|
></ha-icon-button>
|
||||||
@click=${this._editEntity}
|
|
||||||
.disabled=${!editable}
|
|
||||||
></ha-icon-button>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
.label=${this.hass!.localize(
|
.label=${this.hass!.localize(
|
||||||
`ui.panel.lovelace.editor.entities.remove`
|
`ui.panel.lovelace.editor.entities.remove`
|
||||||
@ -186,34 +189,37 @@ export class HuiEntitiesEditor extends LitElement {
|
|||||||
if (!ev.detail.value) {
|
if (!ev.detail.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newEntity: EntityConfig = { entity: ev.detail.value };
|
const newEntity: LovelaceHeadingBadgeConfig = {
|
||||||
const newEntities = (this.entities || []).concat(newEntity);
|
type: "entity",
|
||||||
fireEvent(this, "entities-changed", { entities: newEntities });
|
entity: ev.detail.value,
|
||||||
|
};
|
||||||
|
const newBadges = (this.badges || []).concat(newEntity);
|
||||||
|
fireEvent(this, "heading-badges-changed", { badges: newBadges });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _entityMoved(ev: CustomEvent): void {
|
private _badgeMoved(ev: CustomEvent): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const { oldIndex, newIndex } = ev.detail;
|
const { oldIndex, newIndex } = ev.detail;
|
||||||
|
|
||||||
const newEntities = (this.entities || []).concat();
|
const newBadges = (this.badges || []).concat();
|
||||||
|
|
||||||
newEntities.splice(newIndex, 0, newEntities.splice(oldIndex, 1)[0]);
|
newBadges.splice(newIndex, 0, newBadges.splice(oldIndex, 1)[0]);
|
||||||
|
|
||||||
fireEvent(this, "entities-changed", { entities: newEntities });
|
fireEvent(this, "heading-badges-changed", { badges: newBadges });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _removeEntity(ev: CustomEvent): void {
|
private _removeEntity(ev: CustomEvent): void {
|
||||||
const index = (ev.currentTarget as any).index;
|
const index = (ev.currentTarget as any).index;
|
||||||
const newEntities = (this.entities || []).concat();
|
const newBadges = (this.badges || []).concat();
|
||||||
|
|
||||||
newEntities.splice(index, 1);
|
newBadges.splice(index, 1);
|
||||||
|
|
||||||
fireEvent(this, "entities-changed", { entities: newEntities });
|
fireEvent(this, "heading-badges-changed", { badges: newBadges });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _editEntity(ev: CustomEvent): void {
|
private _editBadge(ev: CustomEvent): void {
|
||||||
const index = (ev.currentTarget as any).index;
|
const index = (ev.currentTarget as any).index;
|
||||||
fireEvent(this, "edit-entity", {
|
fireEvent(this, "edit-heading-badge", {
|
||||||
index,
|
index,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -227,11 +233,11 @@ export class HuiEntitiesEditor extends LitElement {
|
|||||||
ha-button {
|
ha-button {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
.entity {
|
.badge {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.entity .handle {
|
.badge .handle {
|
||||||
cursor: move; /* fallback if grab cursor is unsupported */
|
cursor: move; /* fallback if grab cursor is unsupported */
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
@ -239,11 +245,11 @@ export class HuiEntitiesEditor extends LitElement {
|
|||||||
padding-inline-start: initial;
|
padding-inline-start: initial;
|
||||||
direction: var(--direction);
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
.entity .handle > * {
|
.badge .handle > * {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entity-content {
|
.badge-content {
|
||||||
height: 60px;
|
height: 60px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -252,7 +258,7 @@ export class HuiEntitiesEditor extends LitElement {
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entity-content div {
|
.badge-content div {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@ -291,6 +297,6 @@ export class HuiEntitiesEditor extends LitElement {
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"hui-entities-editor": HuiEntitiesEditor;
|
"hui-heading-badges-editor": HuiHeadingBadgesEditor;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -22,15 +22,19 @@ import type {
|
|||||||
} from "../../../../components/ha-form/types";
|
} from "../../../../components/ha-form/types";
|
||||||
import "../../../../components/ha-svg-icon";
|
import "../../../../components/ha-svg-icon";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import type { HeadingCardConfig, HeadingEntityConfig } from "../../cards/types";
|
import { migrateHeadingCardConfig } from "../../cards/hui-heading-card";
|
||||||
|
import type { HeadingCardConfig } from "../../cards/types";
|
||||||
import { UiAction } from "../../components/hui-action-editor";
|
import { UiAction } from "../../components/hui-action-editor";
|
||||||
|
import {
|
||||||
|
EntityHeadingBadgeConfig,
|
||||||
|
LovelaceHeadingBadgeConfig,
|
||||||
|
} from "../../heading-badges/types";
|
||||||
import type { LovelaceCardEditor } from "../../types";
|
import type { LovelaceCardEditor } from "../../types";
|
||||||
import { processEditorEntities } from "../process-editor-entities";
|
|
||||||
import { actionConfigStruct } from "../structs/action-struct";
|
import { actionConfigStruct } from "../structs/action-struct";
|
||||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||||
import { configElementStyle } from "./config-elements-style";
|
|
||||||
import "./hui-entities-editor";
|
|
||||||
import { EditSubElementEvent } from "../types";
|
import { EditSubElementEvent } from "../types";
|
||||||
|
import { configElementStyle } from "./config-elements-style";
|
||||||
|
import "./hui-heading-badges-editor";
|
||||||
|
|
||||||
const actions: UiAction[] = ["navigate", "url", "perform-action", "none"];
|
const actions: UiAction[] = ["navigate", "url", "perform-action", "none"];
|
||||||
|
|
||||||
@ -41,7 +45,7 @@ const cardConfigStruct = assign(
|
|||||||
heading: optional(string()),
|
heading: optional(string()),
|
||||||
icon: optional(string()),
|
icon: optional(string()),
|
||||||
tap_action: optional(actionConfigStruct),
|
tap_action: optional(actionConfigStruct),
|
||||||
entities: optional(array(any())),
|
badges: optional(array(any())),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -55,8 +59,8 @@ export class HuiHeadingCardEditor
|
|||||||
@state() private _config?: HeadingCardConfig;
|
@state() private _config?: HeadingCardConfig;
|
||||||
|
|
||||||
public setConfig(config: HeadingCardConfig): void {
|
public setConfig(config: HeadingCardConfig): void {
|
||||||
assert(config, cardConfigStruct);
|
this._config = migrateHeadingCardConfig(config);
|
||||||
this._config = config;
|
assert(this._config, cardConfigStruct);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _schema = memoizeOne(
|
private _schema = memoizeOne(
|
||||||
@ -103,8 +107,9 @@ export class HuiHeadingCardEditor
|
|||||||
] as const satisfies readonly HaFormSchema[]
|
] as const satisfies readonly HaFormSchema[]
|
||||||
);
|
);
|
||||||
|
|
||||||
private _entities = memoizeOne((entities: HeadingCardConfig["entities"]) =>
|
private _badges = memoizeOne(
|
||||||
processEditorEntities(entities || [])
|
(badges: HeadingCardConfig["badges"]): LovelaceHeadingBadgeConfig[] =>
|
||||||
|
badges || []
|
||||||
);
|
);
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
@ -138,19 +143,19 @@ export class HuiHeadingCardEditor
|
|||||||
)}
|
)}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<hui-entities-editor
|
<hui-heading-badges-editor
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.entities=${this._entities(this._config!.entities)}
|
.badges=${this._badges(this._config!.badges)}
|
||||||
@entities-changed=${this._entitiesChanged}
|
@heading-badges-changed=${this._badgesChanged}
|
||||||
@edit-entity=${this._editEntity}
|
@edit-heading-badge=${this._editBadge}
|
||||||
>
|
>
|
||||||
</hui-entities-editor>
|
</hui-heading-badges-editor>
|
||||||
</div>
|
</div>
|
||||||
</ha-expansion-panel>
|
</ha-expansion-panel>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _entitiesChanged(ev: CustomEvent): void {
|
private _badgesChanged(ev: CustomEvent): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
if (!this._config || !this.hass) {
|
if (!this._config || !this.hass) {
|
||||||
return;
|
return;
|
||||||
@ -158,7 +163,7 @@ export class HuiHeadingCardEditor
|
|||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
...this._config,
|
...this._config,
|
||||||
entities: ev.detail.entities as HeadingEntityConfig[],
|
badges: ev.detail.badges as LovelaceHeadingBadgeConfig[],
|
||||||
};
|
};
|
||||||
|
|
||||||
fireEvent(this, "config-changed", { config });
|
fireEvent(this, "config-changed", { config });
|
||||||
@ -175,22 +180,22 @@ export class HuiHeadingCardEditor
|
|||||||
fireEvent(this, "config-changed", { config });
|
fireEvent(this, "config-changed", { config });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _editEntity(ev: HASSDomEvent<{ index: number }>): void {
|
private _editBadge(ev: HASSDomEvent<{ index: number }>): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const index = ev.detail.index;
|
const index = ev.detail.index;
|
||||||
const config = this._config!.entities![index!];
|
const config = this._badges(this._config!.badges)[index];
|
||||||
|
|
||||||
fireEvent(this, "edit-sub-element", {
|
fireEvent(this, "edit-sub-element", {
|
||||||
config: config,
|
config: config,
|
||||||
saveConfig: (newConfig) => this._updateEntity(index, newConfig),
|
saveConfig: (newConfig) => this._updateBadge(index, newConfig),
|
||||||
type: "heading-entity",
|
type: "heading-badge",
|
||||||
} as EditSubElementEvent<HeadingEntityConfig>);
|
} as EditSubElementEvent<EntityHeadingBadgeConfig>);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _updateEntity(index: number, entity: HeadingEntityConfig) {
|
private _updateBadge(index: number, entity: EntityHeadingBadgeConfig) {
|
||||||
const entities = this._config!.entities!.concat();
|
const badges = this._config!.badges!.concat();
|
||||||
entities[index] = entity;
|
badges[index] = entity;
|
||||||
const config = { ...this._config!, entities };
|
const config = { ...this._config!, badges };
|
||||||
fireEvent(this, "config-changed", {
|
fireEvent(this, "config-changed", {
|
||||||
config: config,
|
config: config,
|
||||||
});
|
});
|
||||||
|
@ -21,19 +21,21 @@ import type {
|
|||||||
SchemaUnion,
|
SchemaUnion,
|
||||||
} from "../../../../components/ha-form/types";
|
} from "../../../../components/ha-form/types";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import type { HeadingEntityConfig } from "../../cards/types";
|
|
||||||
import { Condition } from "../../common/validate-condition";
|
import { Condition } from "../../common/validate-condition";
|
||||||
|
import { EntityHeadingBadgeConfig } from "../../heading-badges/types";
|
||||||
import type { LovelaceGenericElementEditor } from "../../types";
|
import type { LovelaceGenericElementEditor } from "../../types";
|
||||||
import "../conditions/ha-card-conditions-editor";
|
import "../conditions/ha-card-conditions-editor";
|
||||||
import { configElementStyle } from "../config-elements/config-elements-style";
|
import { configElementStyle } from "../config-elements/config-elements-style";
|
||||||
import { actionConfigStruct } from "../structs/action-struct";
|
import { actionConfigStruct } from "../structs/action-struct";
|
||||||
|
|
||||||
export const DEFAULT_CONFIG: Partial<HeadingEntityConfig> = {
|
export const DEFAULT_CONFIG: Partial<EntityHeadingBadgeConfig> = {
|
||||||
|
type: "entity",
|
||||||
show_state: true,
|
show_state: true,
|
||||||
show_icon: true,
|
show_icon: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const entityConfigStruct = object({
|
const entityConfigStruct = object({
|
||||||
|
type: optional(string()),
|
||||||
entity: string(),
|
entity: string(),
|
||||||
icon: optional(string()),
|
icon: optional(string()),
|
||||||
state_content: optional(union([string(), array(string())])),
|
state_content: optional(union([string(), array(string())])),
|
||||||
@ -44,7 +46,7 @@ const entityConfigStruct = object({
|
|||||||
visibility: optional(array(any())),
|
visibility: optional(array(any())),
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormData = HeadingEntityConfig & {
|
type FormData = EntityHeadingBadgeConfig & {
|
||||||
displayed_elements?: string[];
|
displayed_elements?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -57,9 +59,9 @@ export class HuiHeadingEntityEditor
|
|||||||
|
|
||||||
@property({ type: Boolean }) public preview = false;
|
@property({ type: Boolean }) public preview = false;
|
||||||
|
|
||||||
@state() private _config?: HeadingEntityConfig;
|
@state() private _config?: EntityHeadingBadgeConfig;
|
||||||
|
|
||||||
public setConfig(config: HeadingEntityConfig): void {
|
public setConfig(config: EntityHeadingBadgeConfig): void {
|
||||||
assert(config, entityConfigStruct);
|
assert(config, entityConfigStruct);
|
||||||
this._config = {
|
this._config = {
|
||||||
...DEFAULT_CONFIG,
|
...DEFAULT_CONFIG,
|
||||||
@ -150,12 +152,14 @@ export class HuiHeadingEntityEditor
|
|||||||
] as const satisfies readonly HaFormSchema[]
|
] as const satisfies readonly HaFormSchema[]
|
||||||
);
|
);
|
||||||
|
|
||||||
private _displayedElements = memoizeOne((config: HeadingEntityConfig) => {
|
private _displayedElements = memoizeOne(
|
||||||
const elements: string[] = [];
|
(config: EntityHeadingBadgeConfig) => {
|
||||||
if (config.show_state) elements.push("state");
|
const elements: string[] = [];
|
||||||
if (config.show_icon) elements.push("icon");
|
if (config.show_state) elements.push("state");
|
||||||
return elements;
|
if (config.show_icon) elements.push("icon");
|
||||||
});
|
return elements;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.hass || !this._config) {
|
if (!this.hass || !this._config) {
|
||||||
@ -228,7 +232,7 @@ export class HuiHeadingEntityEditor
|
|||||||
|
|
||||||
const conditions = ev.detail.value as Condition[];
|
const conditions = ev.detail.value as Condition[];
|
||||||
|
|
||||||
const newConfig: HeadingEntityConfig = {
|
const newConfig: EntityHeadingBadgeConfig = {
|
||||||
...this._config,
|
...this._config,
|
||||||
visibility: conditions,
|
visibility: conditions,
|
||||||
};
|
};
|
@ -0,0 +1,42 @@
|
|||||||
|
import { customElement } from "lit/decorators";
|
||||||
|
import { getHeadingBadgeElementClass } from "../../create-element/create-heading-badge-element";
|
||||||
|
import type { EntityHeadingBadgeConfig } from "../../heading-badges/types";
|
||||||
|
import { LovelaceConfigForm, LovelaceHeadingBadgeEditor } from "../../types";
|
||||||
|
import { HuiTypedElementEditor } from "../hui-typed-element-editor";
|
||||||
|
|
||||||
|
@customElement("hui-heading-badge-element-editor")
|
||||||
|
export class HuiHeadingEntityElementEditor extends HuiTypedElementEditor<EntityHeadingBadgeConfig> {
|
||||||
|
protected get configElementType(): string | undefined {
|
||||||
|
return this.value?.type || "entity";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getConfigElement(): Promise<
|
||||||
|
LovelaceHeadingBadgeEditor | undefined
|
||||||
|
> {
|
||||||
|
const elClass = await getHeadingBadgeElementClass(this.configElementType!);
|
||||||
|
|
||||||
|
// Check if a GUI editor exists
|
||||||
|
if (elClass && elClass.getConfigElement) {
|
||||||
|
return elClass.getConfigElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getConfigForm(): Promise<LovelaceConfigForm | undefined> {
|
||||||
|
const elClass = await getHeadingBadgeElementClass(this.configElementType!);
|
||||||
|
|
||||||
|
// Check if a schema exists
|
||||||
|
if (elClass && elClass.getConfigForm) {
|
||||||
|
return elClass.getConfigForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-heading-badge-element-editor": HuiHeadingEntityElementEditor;
|
||||||
|
}
|
||||||
|
}
|
@ -1,20 +0,0 @@
|
|||||||
import { customElement } from "lit/decorators";
|
|
||||||
import { HeadingEntityConfig } from "../../cards/types";
|
|
||||||
import { HuiElementEditor } from "../hui-element-editor";
|
|
||||||
import type { HuiHeadingEntityEditor } from "./hui-heading-entity-editor";
|
|
||||||
|
|
||||||
@customElement("hui-heading-entity-element-editor")
|
|
||||||
export class HuiHeadingEntityElementEditor extends HuiElementEditor<HeadingEntityConfig> {
|
|
||||||
protected async getConfigElement(): Promise<
|
|
||||||
HuiHeadingEntityEditor | undefined
|
|
||||||
> {
|
|
||||||
await import("./hui-heading-entity-editor");
|
|
||||||
return document.createElement("hui-heading-entity-editor");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"hui-heading-entity-element-editor": HuiHeadingEntityElementEditor;
|
|
||||||
}
|
|
||||||
}
|
|
@ -15,7 +15,7 @@ import type { HomeAssistant } from "../../../types";
|
|||||||
import "./entity-row-editor/hui-row-element-editor";
|
import "./entity-row-editor/hui-row-element-editor";
|
||||||
import "./feature-editor/hui-card-feature-element-editor";
|
import "./feature-editor/hui-card-feature-element-editor";
|
||||||
import "./header-footer-editor/hui-header-footer-element-editor";
|
import "./header-footer-editor/hui-header-footer-element-editor";
|
||||||
import "./heading-entity/hui-heading-entity-element-editor";
|
import "./heading-badge-editor/hui-heading-badge-element-editor";
|
||||||
import type { HuiElementEditor } from "./hui-element-editor";
|
import type { HuiElementEditor } from "./hui-element-editor";
|
||||||
import "./picture-element-editor/hui-picture-element-element-editor";
|
import "./picture-element-editor/hui-picture-element-element-editor";
|
||||||
import type { GUIModeChangedEvent, SubElementEditorConfig } from "./types";
|
import type { GUIModeChangedEvent, SubElementEditorConfig } from "./types";
|
||||||
@ -132,16 +132,16 @@ export class HuiSubElementEditor extends LitElement {
|
|||||||
@GUImode-changed=${this._handleGUIModeChanged}
|
@GUImode-changed=${this._handleGUIModeChanged}
|
||||||
></hui-card-feature-element-editor>
|
></hui-card-feature-element-editor>
|
||||||
`;
|
`;
|
||||||
case "heading-entity":
|
case "heading-badge":
|
||||||
return html`
|
return html`
|
||||||
<hui-heading-entity-element-editor
|
<hui-heading-badge-element-editor
|
||||||
class="editor"
|
class="editor"
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this.config.elementConfig}
|
.value=${this.config.elementConfig}
|
||||||
.context=${this.config.context}
|
.context=${this.config.context}
|
||||||
@config-changed=${this._handleConfigChanged}
|
@config-changed=${this._handleConfigChanged}
|
||||||
@GUImode-changed=${this._handleGUIModeChanged}
|
@GUImode-changed=${this._handleGUIModeChanged}
|
||||||
></hui-heading-entity-element-editor>
|
></hui-heading-badge-element-editor>
|
||||||
`;
|
`;
|
||||||
default:
|
default:
|
||||||
return nothing;
|
return nothing;
|
||||||
|
@ -9,7 +9,7 @@ import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
|||||||
import { LovelaceCardFeatureConfig } from "../card-features/types";
|
import { LovelaceCardFeatureConfig } from "../card-features/types";
|
||||||
import { LovelaceElementConfig } from "../elements/types";
|
import { LovelaceElementConfig } from "../elements/types";
|
||||||
import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||||
import { HeadingEntityConfig } from "../cards/types";
|
import { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
|
||||||
|
|
||||||
export interface YamlChangedEvent extends Event {
|
export interface YamlChangedEvent extends Event {
|
||||||
detail: {
|
detail: {
|
||||||
@ -97,10 +97,10 @@ export interface SubElementEditorConfig {
|
|||||||
| LovelaceHeaderFooterConfig
|
| LovelaceHeaderFooterConfig
|
||||||
| LovelaceCardFeatureConfig
|
| LovelaceCardFeatureConfig
|
||||||
| LovelaceElementConfig
|
| LovelaceElementConfig
|
||||||
| HeadingEntityConfig;
|
| LovelaceHeadingBadgeConfig;
|
||||||
saveElementConfig?: (elementConfig: any) => void;
|
saveElementConfig?: (elementConfig: any) => void;
|
||||||
context?: any;
|
context?: any;
|
||||||
type: "header" | "footer" | "row" | "feature" | "element" | "heading-entity";
|
type: "header" | "footer" | "row" | "feature" | "element" | "heading-badge";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditSubElementEvent<T = any, C = any> {
|
export interface EditSubElementEvent<T = any, C = any> {
|
||||||
|
177
src/panels/lovelace/heading-badges/hui-entity-heading-badge.ts
Normal file
177
src/panels/lovelace/heading-badges/hui-entity-heading-badge.ts
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import { mdiAlertCircle } from "@mdi/js";
|
||||||
|
import { HassEntity } from "home-assistant-js-websocket";
|
||||||
|
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { styleMap } from "lit/directives/style-map";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { computeCssColor } from "../../../common/color/compute-color";
|
||||||
|
import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color";
|
||||||
|
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||||
|
import { stateActive } from "../../../common/entity/state_active";
|
||||||
|
import { stateColorCss } from "../../../common/entity/state_color";
|
||||||
|
import "../../../components/ha-heading-badge";
|
||||||
|
import "../../../components/ha-state-icon";
|
||||||
|
import { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||||
|
import "../../../state-display/state-display";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||||
|
import { handleAction } from "../common/handle-action";
|
||||||
|
import { hasAction } from "../common/has-action";
|
||||||
|
import { DEFAULT_CONFIG } from "../editor/heading-badge-editor/hui-entity-heading-badge-editor";
|
||||||
|
import { LovelaceHeadingBadge, LovelaceHeadingBadgeEditor } from "../types";
|
||||||
|
import { EntityHeadingBadgeConfig } from "./types";
|
||||||
|
|
||||||
|
@customElement("hui-entity-heading-badge")
|
||||||
|
export class HuiEntityHeadingBadge
|
||||||
|
extends LitElement
|
||||||
|
implements LovelaceHeadingBadge
|
||||||
|
{
|
||||||
|
public static async getConfigElement(): Promise<LovelaceHeadingBadgeEditor> {
|
||||||
|
await import(
|
||||||
|
"../editor/heading-badge-editor/hui-entity-heading-badge-editor"
|
||||||
|
);
|
||||||
|
return document.createElement("hui-heading-entity-editor");
|
||||||
|
}
|
||||||
|
|
||||||
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _config?: EntityHeadingBadgeConfig;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public preview = false;
|
||||||
|
|
||||||
|
public setConfig(config): void {
|
||||||
|
this._config = {
|
||||||
|
...DEFAULT_CONFIG,
|
||||||
|
tap_action: {
|
||||||
|
action: "none",
|
||||||
|
},
|
||||||
|
...config,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleAction(ev: ActionHandlerEvent) {
|
||||||
|
const config: EntityHeadingBadgeConfig = {
|
||||||
|
tap_action: {
|
||||||
|
action: "none",
|
||||||
|
},
|
||||||
|
...this._config!,
|
||||||
|
};
|
||||||
|
handleAction(this, this.hass!, config, ev.detail.action!);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _computeStateColor = memoizeOne(
|
||||||
|
(entity: HassEntity, color?: string) => {
|
||||||
|
if (!color || color === "none") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (color === "state") {
|
||||||
|
// Use light color if the light support rgb
|
||||||
|
if (
|
||||||
|
computeDomain(entity.entity_id) === "light" &&
|
||||||
|
entity.attributes.rgb_color
|
||||||
|
) {
|
||||||
|
const hsvColor = rgb2hsv(entity.attributes.rgb_color);
|
||||||
|
|
||||||
|
// Modify the real rgb color for better contrast
|
||||||
|
if (hsvColor[1] < 0.4) {
|
||||||
|
// Special case for very light color (e.g: white)
|
||||||
|
if (hsvColor[1] < 0.1) {
|
||||||
|
hsvColor[2] = 225;
|
||||||
|
} else {
|
||||||
|
hsvColor[1] = 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rgb2hex(hsv2rgb(hsvColor));
|
||||||
|
}
|
||||||
|
// Fallback to state color
|
||||||
|
return stateColorCss(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (color) {
|
||||||
|
// Use custom color if active
|
||||||
|
return stateActive(entity) ? computeCssColor(color) : undefined;
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this.hass || !this._config) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = this._config;
|
||||||
|
|
||||||
|
const entityId = config.entity;
|
||||||
|
const stateObj = this.hass!.states[entityId];
|
||||||
|
|
||||||
|
if (!stateObj) {
|
||||||
|
return html`
|
||||||
|
<ha-heading-badge class="error" .title=${entityId}>
|
||||||
|
<ha-svg-icon
|
||||||
|
slot="icon"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.path=${mdiAlertCircle}
|
||||||
|
></ha-svg-icon>
|
||||||
|
-
|
||||||
|
</ha-heading-badge>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const color = this._computeStateColor(stateObj, config.color);
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
"--icon-color": color,
|
||||||
|
};
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-heading-badge
|
||||||
|
.type=${hasAction(config.tap_action) ? "button" : "text"}
|
||||||
|
@action=${this._handleAction}
|
||||||
|
.actionHandler=${actionHandler()}
|
||||||
|
style=${styleMap(style)}
|
||||||
|
>
|
||||||
|
${config.show_icon
|
||||||
|
? html`
|
||||||
|
<ha-state-icon
|
||||||
|
slot="icon"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.icon=${config.icon}
|
||||||
|
.stateObj=${stateObj}
|
||||||
|
></ha-state-icon>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
${config.show_state
|
||||||
|
? html`
|
||||||
|
<state-display
|
||||||
|
.hass=${this.hass}
|
||||||
|
.stateObj=${stateObj}
|
||||||
|
.content=${config.state_content}
|
||||||
|
></state-display>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</ha-heading-badge>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
[role="button"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
ha-heading-badge {
|
||||||
|
--state-inactive-color: initial;
|
||||||
|
}
|
||||||
|
ha-heading-badge.error {
|
||||||
|
--icon-color: var(--red-color);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-entity-heading-badge": HuiEntityHeadingBadge;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
import { mdiAlertCircle } from "@mdi/js";
|
||||||
|
import { dump } from "js-yaml";
|
||||||
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, state } from "lit/decorators";
|
||||||
|
import "../../../components/ha-badge";
|
||||||
|
import "../../../components/ha-svg-icon";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import { showAlertDialog } from "../custom-card-helpers";
|
||||||
|
import { LovelaceBadge } from "../types";
|
||||||
|
import { ErrorBadgeConfig } from "./types";
|
||||||
|
|
||||||
|
export const createErrorHeadingBadgeElement = (config) => {
|
||||||
|
const el = document.createElement("hui-error-heading-badge");
|
||||||
|
el.setConfig(config);
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createErrorHeadingBadgeConfig = (error) => ({
|
||||||
|
type: "error",
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
@customElement("hui-error-heading-badge")
|
||||||
|
export class HuiErrorHeadingBadge extends LitElement implements LovelaceBadge {
|
||||||
|
public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _config?: ErrorBadgeConfig;
|
||||||
|
|
||||||
|
public setConfig(config: ErrorBadgeConfig): void {
|
||||||
|
this._config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _viewDetail() {
|
||||||
|
let dumped: string | undefined;
|
||||||
|
|
||||||
|
if (this._config!.origConfig) {
|
||||||
|
try {
|
||||||
|
dumped = dump(this._config!.origConfig);
|
||||||
|
} catch (err: any) {
|
||||||
|
dumped = `[Error dumping ${this._config!.origConfig}]`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: this._config?.error,
|
||||||
|
warning: true,
|
||||||
|
text: dumped ? html`<pre>${dumped}</pre>` : "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this._config) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-heading-badge
|
||||||
|
class="error"
|
||||||
|
@click=${this._viewDetail}
|
||||||
|
type="button"
|
||||||
|
.title=${this._config.error}
|
||||||
|
>
|
||||||
|
<ha-svg-icon slot="icon" .path=${mdiAlertCircle}></ha-svg-icon>
|
||||||
|
<span class="content">${this._config.error}</span>
|
||||||
|
</ha-heading-badge>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
ha-heading-badge {
|
||||||
|
--icon-color: var(--error-color);
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
max-width: 70px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
font-family: var(--code-font-family, monospace);
|
||||||
|
white-space: break-spaces;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-error-heading-badge": HuiErrorHeadingBadge;
|
||||||
|
}
|
||||||
|
}
|
202
src/panels/lovelace/heading-badges/hui-heading-badge.ts
Normal file
202
src/panels/lovelace/heading-badges/hui-heading-badge.ts
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
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 type { HomeAssistant } from "../../../types";
|
||||||
|
import {
|
||||||
|
attachConditionMediaQueriesListeners,
|
||||||
|
checkConditionsMet,
|
||||||
|
} from "../common/validate-condition";
|
||||||
|
import { createHeadingBadgeElement } from "../create-element/create-heading-badge-element";
|
||||||
|
import type { LovelaceHeadingBadge } from "../types";
|
||||||
|
import { LovelaceHeadingBadgeConfig } from "./types";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HASSDomEvents {
|
||||||
|
"heading-badge-visibility-changed": { value: boolean };
|
||||||
|
"heading-badge-updated": undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement("hui-heading-badge")
|
||||||
|
export class HuiHeadingBadge extends ReactiveElement {
|
||||||
|
@property({ type: Boolean }) public preview = false;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public config?: LovelaceHeadingBadgeConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
private _elementConfig?: LovelaceHeadingBadgeConfig;
|
||||||
|
|
||||||
|
public load() {
|
||||||
|
if (!this.config) {
|
||||||
|
throw new Error("Cannot build heading badge without config");
|
||||||
|
}
|
||||||
|
this._loadElement(this.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _element?: LovelaceHeadingBadge;
|
||||||
|
|
||||||
|
private _listeners: MediaQueriesListener[] = [];
|
||||||
|
|
||||||
|
protected createRenderRoot() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._clearMediaQueries();
|
||||||
|
}
|
||||||
|
|
||||||
|
public connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._listenMediaQueries();
|
||||||
|
this._updateVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _updateElement(config: LovelaceHeadingBadgeConfig) {
|
||||||
|
if (!this._element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._element.setConfig(config);
|
||||||
|
this._elementConfig = config;
|
||||||
|
fireEvent(this, "heading-badge-updated");
|
||||||
|
}
|
||||||
|
|
||||||
|
private _loadElement(config: LovelaceHeadingBadgeConfig) {
|
||||||
|
this._element = createHeadingBadgeElement(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, "heading-badge-updated");
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
this._element.addEventListener(
|
||||||
|
"ll-rebuild",
|
||||||
|
(ev: Event) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._loadElement(config);
|
||||||
|
fireEvent(this, "heading-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._element = undefined;
|
||||||
|
this._elementConfig = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
fireEvent(this, "heading-badge-visibility-changed", { value: 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-heading-badge": HuiHeadingBadge;
|
||||||
|
}
|
||||||
|
}
|
25
src/panels/lovelace/heading-badges/types.ts
Normal file
25
src/panels/lovelace/heading-badges/types.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { ActionConfig } from "../../../data/lovelace/config/action";
|
||||||
|
import { Condition } from "../common/validate-condition";
|
||||||
|
|
||||||
|
export type LovelaceHeadingBadgeConfig = {
|
||||||
|
type?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
visibility?: Condition[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ErrorBadgeConfig extends LovelaceHeadingBadgeConfig {
|
||||||
|
type: string;
|
||||||
|
error: string;
|
||||||
|
origConfig: LovelaceHeadingBadgeConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EntityHeadingBadgeConfig extends LovelaceHeadingBadgeConfig {
|
||||||
|
type?: "entity";
|
||||||
|
entity: string;
|
||||||
|
state_content?: string | string[];
|
||||||
|
icon?: string;
|
||||||
|
show_state?: boolean;
|
||||||
|
show_icon?: boolean;
|
||||||
|
color?: string;
|
||||||
|
tap_action?: ActionConfig;
|
||||||
|
}
|
@ -13,6 +13,7 @@ import { LovelaceRow, LovelaceRowConfig } from "./entity-rows/types";
|
|||||||
import { LovelaceHeaderFooterConfig } from "./header-footer/types";
|
import { LovelaceHeaderFooterConfig } from "./header-footer/types";
|
||||||
import { LovelaceCardFeatureConfig } from "./card-features/types";
|
import { LovelaceCardFeatureConfig } from "./card-features/types";
|
||||||
import { LovelaceElement, LovelaceElementConfig } from "./elements/types";
|
import { LovelaceElement, LovelaceElementConfig } from "./elements/types";
|
||||||
|
import { LovelaceHeadingBadgeConfig } from "./heading-badges/types";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
@ -178,3 +179,27 @@ export interface LovelaceCardFeatureEditor
|
|||||||
extends LovelaceGenericElementEditor {
|
extends LovelaceGenericElementEditor {
|
||||||
setConfig(config: LovelaceCardFeatureConfig): void;
|
setConfig(config: LovelaceCardFeatureConfig): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LovelaceHeadingBadge extends HTMLElement {
|
||||||
|
hass?: HomeAssistant;
|
||||||
|
preview?: boolean;
|
||||||
|
setConfig(config: LovelaceHeadingBadgeConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LovelaceHeadingBadgeConstructor
|
||||||
|
extends Constructor<LovelaceHeadingBadge> {
|
||||||
|
getStubConfig?: (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
stateObj?: HassEntity
|
||||||
|
) => LovelaceHeadingBadgeConfig;
|
||||||
|
getConfigElement?: () => LovelaceHeadingBadgeEditor;
|
||||||
|
getConfigForm?: () => {
|
||||||
|
schema: HaFormSchema[];
|
||||||
|
assertConfig?: (config: LovelaceCardConfig) => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LovelaceHeadingBadgeEditor
|
||||||
|
extends LovelaceGenericElementEditor {
|
||||||
|
setConfig(config: LovelaceHeadingBadgeConfig): void;
|
||||||
|
}
|
||||||
|
@ -6403,7 +6403,7 @@
|
|||||||
"row": "Entity row editor",
|
"row": "Entity row editor",
|
||||||
"feature": "Feature editor",
|
"feature": "Feature editor",
|
||||||
"element": "Element editor",
|
"element": "Element editor",
|
||||||
"heading-entity": "Entity editor",
|
"heading-badge": "Heading badge editor",
|
||||||
"element_type": "{type} element editor"
|
"element_type": "{type} element editor"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user