mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +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 { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import "../heading-badges/hui-heading-badge";
|
||||
import type {
|
||||
LovelaceCard,
|
||||
LovelaceCardEditor,
|
||||
LovelaceLayoutOptions,
|
||||
} from "../types";
|
||||
import "./heading/hui-heading-entity";
|
||||
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")
|
||||
export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
@ -45,7 +56,7 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
tap_action: {
|
||||
action: "none",
|
||||
},
|
||||
...config,
|
||||
...migrateHeadingCardConfig(config),
|
||||
};
|
||||
}
|
||||
|
||||
@ -73,6 +84,8 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
|
||||
const style = this._config.heading_style || "title";
|
||||
|
||||
const badges = this._config.badges;
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="container">
|
||||
@ -91,17 +104,17 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
: nothing}
|
||||
${actionable ? html`<ha-icon-next></ha-icon-next>` : nothing}
|
||||
</div>
|
||||
${this._config.entities?.length
|
||||
${badges?.length
|
||||
? html`
|
||||
<div class="entities">
|
||||
${this._config.entities.map(
|
||||
<div class="badges">
|
||||
${badges.map(
|
||||
(config) => html`
|
||||
<hui-heading-entity
|
||||
<hui-heading-badge
|
||||
.config=${config}
|
||||
.hass=${this.hass}
|
||||
.preview=${this.preview}
|
||||
>
|
||||
</hui-heading-entity>
|
||||
</hui-heading-badge>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
@ -150,7 +163,7 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
.container .content:not(:has(p)) {
|
||||
min-width: fit-content;
|
||||
}
|
||||
.container .entities {
|
||||
.container .badges {
|
||||
flex: 0 0;
|
||||
}
|
||||
.content {
|
||||
@ -186,7 +199,7 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
.entities {
|
||||
.badges {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
LovelaceRowConfig,
|
||||
} from "../entity-rows/types";
|
||||
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
||||
import { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
|
||||
|
||||
export type AlarmPanelCardConfigState =
|
||||
| "arm_away"
|
||||
@ -503,21 +504,12 @@ export interface TileCardConfig extends LovelaceCardConfig {
|
||||
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 {
|
||||
heading_style?: "title" | "subtitle";
|
||||
heading?: string;
|
||||
icon?: string;
|
||||
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 { LovelaceRow, LovelaceRowConfig } from "../entity-rows/types";
|
||||
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
||||
import { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
|
||||
import {
|
||||
LovelaceBadge,
|
||||
LovelaceBadgeConstructor,
|
||||
@ -26,6 +27,8 @@ import {
|
||||
LovelaceElementConstructor,
|
||||
LovelaceHeaderFooter,
|
||||
LovelaceHeaderFooterConstructor,
|
||||
LovelaceHeadingBadge,
|
||||
LovelaceHeadingBadgeConstructor,
|
||||
LovelaceRowConstructor,
|
||||
} from "../types";
|
||||
|
||||
@ -72,6 +75,11 @@ interface CreateElementConfigTypes {
|
||||
element: LovelaceSectionElement;
|
||||
constructor: unknown;
|
||||
};
|
||||
"heading-badge": {
|
||||
config: LovelaceHeadingBadgeConfig;
|
||||
element: LovelaceHeadingBadge;
|
||||
constructor: LovelaceHeadingBadgeConstructor;
|
||||
};
|
||||
}
|
||||
|
||||
export const createErrorCardElement = (config: ErrorCardConfig) => {
|
||||
@ -102,6 +110,20 @@ export const createErrorBadgeElement = (config: ErrorCardConfig) => {
|
||||
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) => ({
|
||||
type: "error",
|
||||
error,
|
||||
@ -114,6 +136,12 @@ export const createErrorBadgeConfig = (error, origConfig) => ({
|
||||
origConfig,
|
||||
});
|
||||
|
||||
export const createErrorHeadingBadgeConfig = (error, origConfig) => ({
|
||||
type: "error",
|
||||
error,
|
||||
origConfig,
|
||||
});
|
||||
|
||||
const _createElement = <T extends keyof CreateElementConfigTypes>(
|
||||
tag: string,
|
||||
config: CreateElementConfigTypes[T]["config"]
|
||||
@ -134,6 +162,11 @@ const _createErrorElement = <T extends keyof CreateElementConfigTypes>(
|
||||
if (tagSuffix === "badge") {
|
||||
return createErrorBadgeElement(createErrorBadgeConfig(error, config));
|
||||
}
|
||||
if (tagSuffix === "heading-badge") {
|
||||
return createErrorHeadingBadgeElement(
|
||||
createErrorHeadingBadgeConfig(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-svg-icon";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
|
||||
type EntityConfig = {
|
||||
entity: string;
|
||||
};
|
||||
import { LovelaceHeadingBadgeConfig } from "../../heading-badges/types";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"edit-entity": { index: number };
|
||||
"edit-heading-badge": { index: number };
|
||||
"heading-badges-changed": { badges: LovelaceHeadingBadgeConfig[] };
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("hui-entities-editor")
|
||||
export class HuiEntitiesEditor extends LitElement {
|
||||
@customElement("hui-heading-badges-editor")
|
||||
export class HuiHeadingBadgesEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public entities?: EntityConfig[];
|
||||
public badges?: LovelaceHeadingBadgeConfig[];
|
||||
|
||||
@query(".add-container", true) private _addContainer?: HTMLDivElement;
|
||||
|
||||
@ -40,14 +38,30 @@ export class HuiEntitiesEditor extends LitElement {
|
||||
|
||||
private _opened = false;
|
||||
|
||||
private _entitiesKeys = new WeakMap<EntityConfig, string>();
|
||||
private _badgesKeys = new WeakMap<LovelaceHeadingBadgeConfig, string>();
|
||||
|
||||
private _getKey(entity: EntityConfig) {
|
||||
if (!this._entitiesKeys.has(entity)) {
|
||||
this._entitiesKeys.set(entity, Math.random().toString());
|
||||
private _getKey(badge: LovelaceHeadingBadgeConfig) {
|
||||
if (!this._badgesKeys.has(badge)) {
|
||||
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() {
|
||||
@ -56,46 +70,35 @@ export class HuiEntitiesEditor extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
${this.entities
|
||||
${this.badges
|
||||
? html`
|
||||
<ha-sortable
|
||||
handle-selector=".handle"
|
||||
@item-moved=${this._entityMoved}
|
||||
@item-moved=${this._badgeMoved}
|
||||
>
|
||||
<div class="entities">
|
||||
${repeat(
|
||||
this.entities,
|
||||
(entityConf) => this._getKey(entityConf),
|
||||
(entityConf, index) => {
|
||||
const editable = true;
|
||||
|
||||
const entityId = entityConf.entity;
|
||||
const stateObj = this.hass.states[entityId];
|
||||
const name = stateObj
|
||||
? stateObj.attributes.friendly_name
|
||||
: undefined;
|
||||
this.badges,
|
||||
(badge) => this._getKey(badge),
|
||||
(badge, index) => {
|
||||
const label = this._computeBadgeLabel(badge);
|
||||
return html`
|
||||
<div class="entity">
|
||||
<div class="badge">
|
||||
<div class="handle">
|
||||
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
||||
</div>
|
||||
<div class="entity-content">
|
||||
<span>${name || entityId}</span>
|
||||
<div class="badge-content">
|
||||
<span>${label}</span>
|
||||
</div>
|
||||
${editable
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.entities.edit`
|
||||
)}
|
||||
.path=${mdiPencil}
|
||||
class="edit-icon"
|
||||
.index=${index}
|
||||
@click=${this._editEntity}
|
||||
.disabled=${!editable}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.entities.edit`
|
||||
)}
|
||||
.path=${mdiPencil}
|
||||
class="edit-icon"
|
||||
.index=${index}
|
||||
@click=${this._editBadge}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.label=${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.entities.remove`
|
||||
@ -186,34 +189,37 @@ export class HuiEntitiesEditor extends LitElement {
|
||||
if (!ev.detail.value) {
|
||||
return;
|
||||
}
|
||||
const newEntity: EntityConfig = { entity: ev.detail.value };
|
||||
const newEntities = (this.entities || []).concat(newEntity);
|
||||
fireEvent(this, "entities-changed", { entities: newEntities });
|
||||
const newEntity: LovelaceHeadingBadgeConfig = {
|
||||
type: "entity",
|
||||
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();
|
||||
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 {
|
||||
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;
|
||||
fireEvent(this, "edit-entity", {
|
||||
fireEvent(this, "edit-heading-badge", {
|
||||
index,
|
||||
});
|
||||
}
|
||||
@ -227,11 +233,11 @@ export class HuiEntitiesEditor extends LitElement {
|
||||
ha-button {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.entity {
|
||||
.badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.entity .handle {
|
||||
.badge .handle {
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
cursor: grab;
|
||||
padding-right: 8px;
|
||||
@ -239,11 +245,11 @@ export class HuiEntitiesEditor extends LitElement {
|
||||
padding-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.entity .handle > * {
|
||||
.badge .handle > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.entity-content {
|
||||
.badge-content {
|
||||
height: 60px;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
@ -252,7 +258,7 @@ export class HuiEntitiesEditor extends LitElement {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.entity-content div {
|
||||
.badge-content div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@ -291,6 +297,6 @@ export class HuiEntitiesEditor extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-entities-editor": HuiEntitiesEditor;
|
||||
"hui-heading-badges-editor": HuiHeadingBadgesEditor;
|
||||
}
|
||||
}
|
@ -22,15 +22,19 @@ import type {
|
||||
} from "../../../../components/ha-form/types";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
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 {
|
||||
EntityHeadingBadgeConfig,
|
||||
LovelaceHeadingBadgeConfig,
|
||||
} from "../../heading-badges/types";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import { processEditorEntities } from "../process-editor-entities";
|
||||
import { actionConfigStruct } from "../structs/action-struct";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
import "./hui-entities-editor";
|
||||
import { EditSubElementEvent } from "../types";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
import "./hui-heading-badges-editor";
|
||||
|
||||
const actions: UiAction[] = ["navigate", "url", "perform-action", "none"];
|
||||
|
||||
@ -41,7 +45,7 @@ const cardConfigStruct = assign(
|
||||
heading: optional(string()),
|
||||
icon: optional(string()),
|
||||
tap_action: optional(actionConfigStruct),
|
||||
entities: optional(array(any())),
|
||||
badges: optional(array(any())),
|
||||
})
|
||||
);
|
||||
|
||||
@ -55,8 +59,8 @@ export class HuiHeadingCardEditor
|
||||
@state() private _config?: HeadingCardConfig;
|
||||
|
||||
public setConfig(config: HeadingCardConfig): void {
|
||||
assert(config, cardConfigStruct);
|
||||
this._config = config;
|
||||
this._config = migrateHeadingCardConfig(config);
|
||||
assert(this._config, cardConfigStruct);
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
@ -103,8 +107,9 @@ export class HuiHeadingCardEditor
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
private _entities = memoizeOne((entities: HeadingCardConfig["entities"]) =>
|
||||
processEditorEntities(entities || [])
|
||||
private _badges = memoizeOne(
|
||||
(badges: HeadingCardConfig["badges"]): LovelaceHeadingBadgeConfig[] =>
|
||||
badges || []
|
||||
);
|
||||
|
||||
protected render() {
|
||||
@ -138,19 +143,19 @@ export class HuiHeadingCardEditor
|
||||
)}
|
||||
</h3>
|
||||
<div class="content">
|
||||
<hui-entities-editor
|
||||
<hui-heading-badges-editor
|
||||
.hass=${this.hass}
|
||||
.entities=${this._entities(this._config!.entities)}
|
||||
@entities-changed=${this._entitiesChanged}
|
||||
@edit-entity=${this._editEntity}
|
||||
.badges=${this._badges(this._config!.badges)}
|
||||
@heading-badges-changed=${this._badgesChanged}
|
||||
@edit-heading-badge=${this._editBadge}
|
||||
>
|
||||
</hui-entities-editor>
|
||||
</hui-heading-badges-editor>
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
private _entitiesChanged(ev: CustomEvent): void {
|
||||
private _badgesChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
@ -158,7 +163,7 @@ export class HuiHeadingCardEditor
|
||||
|
||||
const config = {
|
||||
...this._config,
|
||||
entities: ev.detail.entities as HeadingEntityConfig[],
|
||||
badges: ev.detail.badges as LovelaceHeadingBadgeConfig[],
|
||||
};
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
@ -175,22 +180,22 @@ export class HuiHeadingCardEditor
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _editEntity(ev: HASSDomEvent<{ index: number }>): void {
|
||||
private _editBadge(ev: HASSDomEvent<{ index: number }>): void {
|
||||
ev.stopPropagation();
|
||||
const index = ev.detail.index;
|
||||
const config = this._config!.entities![index!];
|
||||
const config = this._badges(this._config!.badges)[index];
|
||||
|
||||
fireEvent(this, "edit-sub-element", {
|
||||
config: config,
|
||||
saveConfig: (newConfig) => this._updateEntity(index, newConfig),
|
||||
type: "heading-entity",
|
||||
} as EditSubElementEvent<HeadingEntityConfig>);
|
||||
saveConfig: (newConfig) => this._updateBadge(index, newConfig),
|
||||
type: "heading-badge",
|
||||
} as EditSubElementEvent<EntityHeadingBadgeConfig>);
|
||||
}
|
||||
|
||||
private _updateEntity(index: number, entity: HeadingEntityConfig) {
|
||||
const entities = this._config!.entities!.concat();
|
||||
entities[index] = entity;
|
||||
const config = { ...this._config!, entities };
|
||||
private _updateBadge(index: number, entity: EntityHeadingBadgeConfig) {
|
||||
const badges = this._config!.badges!.concat();
|
||||
badges[index] = entity;
|
||||
const config = { ...this._config!, badges };
|
||||
fireEvent(this, "config-changed", {
|
||||
config: config,
|
||||
});
|
||||
|
@ -21,19 +21,21 @@ import type {
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { HeadingEntityConfig } from "../../cards/types";
|
||||
import { Condition } from "../../common/validate-condition";
|
||||
import { EntityHeadingBadgeConfig } from "../../heading-badges/types";
|
||||
import type { LovelaceGenericElementEditor } from "../../types";
|
||||
import "../conditions/ha-card-conditions-editor";
|
||||
import { configElementStyle } from "../config-elements/config-elements-style";
|
||||
import { actionConfigStruct } from "../structs/action-struct";
|
||||
|
||||
export const DEFAULT_CONFIG: Partial<HeadingEntityConfig> = {
|
||||
export const DEFAULT_CONFIG: Partial<EntityHeadingBadgeConfig> = {
|
||||
type: "entity",
|
||||
show_state: true,
|
||||
show_icon: true,
|
||||
};
|
||||
|
||||
const entityConfigStruct = object({
|
||||
type: optional(string()),
|
||||
entity: string(),
|
||||
icon: optional(string()),
|
||||
state_content: optional(union([string(), array(string())])),
|
||||
@ -44,7 +46,7 @@ const entityConfigStruct = object({
|
||||
visibility: optional(array(any())),
|
||||
});
|
||||
|
||||
type FormData = HeadingEntityConfig & {
|
||||
type FormData = EntityHeadingBadgeConfig & {
|
||||
displayed_elements?: string[];
|
||||
};
|
||||
|
||||
@ -57,9 +59,9 @@ export class HuiHeadingEntityEditor
|
||||
|
||||
@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);
|
||||
this._config = {
|
||||
...DEFAULT_CONFIG,
|
||||
@ -150,12 +152,14 @@ export class HuiHeadingEntityEditor
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
private _displayedElements = memoizeOne((config: HeadingEntityConfig) => {
|
||||
const elements: string[] = [];
|
||||
if (config.show_state) elements.push("state");
|
||||
if (config.show_icon) elements.push("icon");
|
||||
return elements;
|
||||
});
|
||||
private _displayedElements = memoizeOne(
|
||||
(config: EntityHeadingBadgeConfig) => {
|
||||
const elements: string[] = [];
|
||||
if (config.show_state) elements.push("state");
|
||||
if (config.show_icon) elements.push("icon");
|
||||
return elements;
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
@ -228,7 +232,7 @@ export class HuiHeadingEntityEditor
|
||||
|
||||
const conditions = ev.detail.value as Condition[];
|
||||
|
||||
const newConfig: HeadingEntityConfig = {
|
||||
const newConfig: EntityHeadingBadgeConfig = {
|
||||
...this._config,
|
||||
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 "./feature-editor/hui-card-feature-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 "./picture-element-editor/hui-picture-element-element-editor";
|
||||
import type { GUIModeChangedEvent, SubElementEditorConfig } from "./types";
|
||||
@ -132,16 +132,16 @@ export class HuiSubElementEditor extends LitElement {
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
></hui-card-feature-element-editor>
|
||||
`;
|
||||
case "heading-entity":
|
||||
case "heading-badge":
|
||||
return html`
|
||||
<hui-heading-entity-element-editor
|
||||
<hui-heading-badge-element-editor
|
||||
class="editor"
|
||||
.hass=${this.hass}
|
||||
.value=${this.config.elementConfig}
|
||||
.context=${this.config.context}
|
||||
@config-changed=${this._handleConfigChanged}
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
></hui-heading-entity-element-editor>
|
||||
></hui-heading-badge-element-editor>
|
||||
`;
|
||||
default:
|
||||
return nothing;
|
||||
|
@ -9,7 +9,7 @@ import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
||||
import { LovelaceCardFeatureConfig } from "../card-features/types";
|
||||
import { LovelaceElementConfig } from "../elements/types";
|
||||
import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||
import { HeadingEntityConfig } from "../cards/types";
|
||||
import { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
|
||||
|
||||
export interface YamlChangedEvent extends Event {
|
||||
detail: {
|
||||
@ -97,10 +97,10 @@ export interface SubElementEditorConfig {
|
||||
| LovelaceHeaderFooterConfig
|
||||
| LovelaceCardFeatureConfig
|
||||
| LovelaceElementConfig
|
||||
| HeadingEntityConfig;
|
||||
| LovelaceHeadingBadgeConfig;
|
||||
saveElementConfig?: (elementConfig: any) => void;
|
||||
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> {
|
||||
|
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 { LovelaceCardFeatureConfig } from "./card-features/types";
|
||||
import { LovelaceElement, LovelaceElementConfig } from "./elements/types";
|
||||
import { LovelaceHeadingBadgeConfig } from "./heading-badges/types";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line
|
||||
@ -178,3 +179,27 @@ export interface LovelaceCardFeatureEditor
|
||||
extends LovelaceGenericElementEditor {
|
||||
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",
|
||||
"feature": "Feature editor",
|
||||
"element": "Element editor",
|
||||
"heading-entity": "Entity editor",
|
||||
"heading-badge": "Heading badge editor",
|
||||
"element_type": "{type} element editor"
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user