mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
Add new badges design with UI editor (#21401)
* Add new entity badge * Improve badge render * Add edit mode * Add editor * Increase height * Use hui-badge * Add editor * Add drag and drop * Fix editor translations * Fix icon * Fix inactive color * Add state content * Add default config * Fix types * Add custom badge support to editor * Fix custom badges * Add new badges to masonry view * fix lint * Fix inactive color * Fix entity filter card * Add display type option * Add support for picture * Improve focus style * Add visibility editor * Fix visibility * Fix add/delete card inside section * Fix translations * Add error badge * Rename classes * Fix badge type * Remove badges from section type * Add missing types
This commit is contained in:
parent
ce43774b5f
commit
729a12af0c
@ -3,9 +3,10 @@ import {
|
||||
getCollection,
|
||||
HassEventBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { HuiBadge } from "../panels/lovelace/badges/hui-badge";
|
||||
import type { HuiCard } from "../panels/lovelace/cards/hui-card";
|
||||
import type { HuiSection } from "../panels/lovelace/sections/hui-section";
|
||||
import { Lovelace, LovelaceBadge } from "../panels/lovelace/types";
|
||||
import { Lovelace } from "../panels/lovelace/types";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { LovelaceSectionConfig } from "./lovelace/config/section";
|
||||
import { fetchConfig, LegacyLovelaceConfig } from "./lovelace/config/types";
|
||||
@ -21,7 +22,7 @@ export interface LovelaceViewElement extends HTMLElement {
|
||||
narrow?: boolean;
|
||||
index?: number;
|
||||
cards?: HuiCard[];
|
||||
badges?: LovelaceBadge[];
|
||||
badges?: HuiBadge[];
|
||||
sections?: HuiSection[];
|
||||
isStrategy: boolean;
|
||||
setConfig(config: LovelaceViewConfig): void;
|
||||
|
@ -1,4 +1,12 @@
|
||||
import { Condition } from "../../../panels/lovelace/common/validate-condition";
|
||||
|
||||
export interface LovelaceBadgeConfig {
|
||||
type?: string;
|
||||
[key: string]: any;
|
||||
visibility?: Condition[];
|
||||
}
|
||||
|
||||
export const defaultBadgeConfig = (entity_id: string): LovelaceBadgeConfig => ({
|
||||
type: "entity",
|
||||
entity: entity_id,
|
||||
});
|
||||
|
@ -27,7 +27,7 @@ export interface LovelaceBaseViewConfig {
|
||||
|
||||
export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
|
||||
type?: string;
|
||||
badges?: Array<string | LovelaceBadgeConfig>;
|
||||
badges?: (string | LovelaceBadgeConfig)[]; // Badge can be just an entity_id
|
||||
cards?: LovelaceCardConfig[];
|
||||
sections?: LovelaceSectionRawConfig[];
|
||||
}
|
||||
|
@ -8,6 +8,14 @@ export interface CustomCardEntry {
|
||||
documentationURL?: string;
|
||||
}
|
||||
|
||||
export interface CustomBadgeEntry {
|
||||
type: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
preview?: boolean;
|
||||
documentationURL?: string;
|
||||
}
|
||||
|
||||
export interface CustomCardFeatureEntry {
|
||||
type: string;
|
||||
name?: string;
|
||||
@ -18,6 +26,7 @@ export interface CustomCardFeatureEntry {
|
||||
export interface CustomCardsWindow {
|
||||
customCards?: CustomCardEntry[];
|
||||
customCardFeatures?: CustomCardFeatureEntry[];
|
||||
customBadges?: CustomBadgeEntry[];
|
||||
/**
|
||||
* @deprecated Use customCardFeatures
|
||||
*/
|
||||
@ -34,6 +43,9 @@ if (!("customCards" in customCardsWindow)) {
|
||||
if (!("customCardFeatures" in customCardsWindow)) {
|
||||
customCardsWindow.customCardFeatures = [];
|
||||
}
|
||||
if (!("customBadges" in customCardsWindow)) {
|
||||
customCardsWindow.customBadges = [];
|
||||
}
|
||||
if (!("customTileFeatures" in customCardsWindow)) {
|
||||
customCardsWindow.customTileFeatures = [];
|
||||
}
|
||||
@ -43,10 +55,14 @@ export const getCustomCardFeatures = () => [
|
||||
...customCardsWindow.customCardFeatures!,
|
||||
...customCardsWindow.customTileFeatures!,
|
||||
];
|
||||
export const customBadges = customCardsWindow.customBadges!;
|
||||
|
||||
export const getCustomCardEntry = (type: string) =>
|
||||
customCards.find((card) => card.type === type);
|
||||
|
||||
export const getCustomBadgeEntry = (type: string) =>
|
||||
customBadges.find((badge) => badge.type === type);
|
||||
|
||||
export const isCustomType = (type: string) =>
|
||||
type.startsWith(CUSTOM_TYPE_PREFIX);
|
||||
|
||||
|
200
src/panels/lovelace/badges/hui-badge.ts
Normal file
200
src/panels/lovelace/badges/hui-badge.ts
Normal file
@ -0,0 +1,200 @@
|
||||
import { PropertyValues, ReactiveElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { MediaQueriesListener } from "../../../common/dom/media_query";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import {
|
||||
attachConditionMediaQueriesListeners,
|
||||
checkConditionsMet,
|
||||
} from "../common/validate-condition";
|
||||
import { createBadgeElement } from "../create-element/create-badge-element";
|
||||
import { createErrorBadgeConfig } from "../create-element/create-element-base";
|
||||
import type { LovelaceBadge } from "../types";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"badge-updated": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("hui-badge")
|
||||
export class HuiBadge extends ReactiveElement {
|
||||
@property({ type: Boolean }) public preview = false;
|
||||
|
||||
@property({ attribute: false }) public config?: LovelaceBadgeConfig;
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
private _elementConfig?: LovelaceBadgeConfig;
|
||||
|
||||
public load() {
|
||||
if (!this.config) {
|
||||
throw new Error("Cannot build badge without config");
|
||||
}
|
||||
this._loadElement(this.config);
|
||||
}
|
||||
|
||||
private _element?: LovelaceBadge;
|
||||
|
||||
private _listeners: MediaQueriesListener[] = [];
|
||||
|
||||
protected createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._clearMediaQueries();
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._listenMediaQueries();
|
||||
this._updateVisibility();
|
||||
}
|
||||
|
||||
private _updateElement(config: LovelaceBadgeConfig) {
|
||||
if (!this._element) {
|
||||
return;
|
||||
}
|
||||
this._element.setConfig(config);
|
||||
this._elementConfig = config;
|
||||
fireEvent(this, "badge-updated");
|
||||
}
|
||||
|
||||
private _loadElement(config: LovelaceBadgeConfig) {
|
||||
this._element = createBadgeElement(config);
|
||||
this._elementConfig = config;
|
||||
if (this.hass) {
|
||||
this._element.hass = this.hass;
|
||||
}
|
||||
this._element.addEventListener(
|
||||
"ll-upgrade",
|
||||
(ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
if (this.hass) {
|
||||
this._element!.hass = this.hass;
|
||||
}
|
||||
fireEvent(this, "badge-updated");
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
this._element.addEventListener(
|
||||
"ll-rebuild",
|
||||
(ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
this._loadElement(config);
|
||||
fireEvent(this, "badge-updated");
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
while (this.lastChild) {
|
||||
this.removeChild(this.lastChild);
|
||||
}
|
||||
this._updateVisibility();
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues<typeof this>): void {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!this._element) {
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
protected update(changedProps: PropertyValues<typeof this>) {
|
||||
super.update(changedProps);
|
||||
|
||||
if (this._element) {
|
||||
if (changedProps.has("config")) {
|
||||
const elementConfig = this._elementConfig;
|
||||
if (this.config !== elementConfig && this.config) {
|
||||
const typeChanged = this.config?.type !== elementConfig?.type;
|
||||
if (typeChanged) {
|
||||
this._loadElement(this.config);
|
||||
} else {
|
||||
this._updateElement(this.config);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (changedProps.has("hass")) {
|
||||
try {
|
||||
if (this.hass) {
|
||||
this._element.hass = this.hass;
|
||||
}
|
||||
} catch (e: any) {
|
||||
this._loadElement(createErrorBadgeConfig(e.message, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has("hass") || changedProps.has("preview")) {
|
||||
this._updateVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
private _clearMediaQueries() {
|
||||
this._listeners.forEach((unsub) => unsub());
|
||||
this._listeners = [];
|
||||
}
|
||||
|
||||
private _listenMediaQueries() {
|
||||
this._clearMediaQueries();
|
||||
if (!this.config?.visibility) {
|
||||
return;
|
||||
}
|
||||
const conditions = this.config.visibility;
|
||||
const hasOnlyMediaQuery =
|
||||
conditions.length === 1 &&
|
||||
conditions[0].condition === "screen" &&
|
||||
!!conditions[0].media_query;
|
||||
|
||||
this._listeners = attachConditionMediaQueriesListeners(
|
||||
this.config.visibility,
|
||||
(matches) => {
|
||||
this._updateVisibility(hasOnlyMediaQuery && matches);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _updateVisibility(forceVisible?: boolean) {
|
||||
if (!this._element || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._element.hidden) {
|
||||
this._setElementVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const visible =
|
||||
forceVisible ||
|
||||
this.preview ||
|
||||
!this.config?.visibility ||
|
||||
checkConditionsMet(this.config.visibility, this.hass);
|
||||
this._setElementVisibility(visible);
|
||||
}
|
||||
|
||||
private _setElementVisibility(visible: boolean) {
|
||||
if (!this._element) return;
|
||||
|
||||
if (this.hidden !== !visible) {
|
||||
this.style.setProperty("display", visible ? "" : "none");
|
||||
this.toggleAttribute("hidden", !visible);
|
||||
}
|
||||
|
||||
if (!visible && this._element.parentElement) {
|
||||
this.removeChild(this._element);
|
||||
} else if (visible && !this._element.parentElement) {
|
||||
this.appendChild(this._element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-badge": HuiBadge;
|
||||
}
|
||||
}
|
306
src/panels/lovelace/badges/hui-entity-badge.ts
Normal file
306
src/panels/lovelace/badges/hui-entity-badge.ts
Normal file
@ -0,0 +1,306 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
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 { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { stateActive } from "../../../common/entity/state_active";
|
||||
import { stateColorCss } from "../../../common/entity/state_color";
|
||||
import "../../../components/ha-ripple";
|
||||
import "../../../components/ha-state-icon";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import { LovelaceBadge, LovelaceBadgeEditor } from "../types";
|
||||
import { EntityBadgeConfig } from "./types";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { cameraUrlWithWidthHeight } from "../../../data/camera";
|
||||
|
||||
export const DISPLAY_TYPES = ["minimal", "standard", "complete"] as const;
|
||||
|
||||
export type DisplayType = (typeof DISPLAY_TYPES)[number];
|
||||
|
||||
export const DEFAULT_DISPLAY_TYPE: DisplayType = "standard";
|
||||
|
||||
@customElement("hui-entity-badge")
|
||||
export class HuiEntityBadge extends LitElement implements LovelaceBadge {
|
||||
public static async getConfigElement(): Promise<LovelaceBadgeEditor> {
|
||||
await import("../editor/config-elements/hui-entity-badge-editor");
|
||||
return document.createElement("hui-entity-badge-editor");
|
||||
}
|
||||
|
||||
public static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
entities: string[],
|
||||
entitiesFallback: string[]
|
||||
): EntityBadgeConfig {
|
||||
const includeDomains = ["sensor", "light", "switch"];
|
||||
const maxEntities = 1;
|
||||
const foundEntities = findEntities(
|
||||
hass,
|
||||
maxEntities,
|
||||
entities,
|
||||
entitiesFallback,
|
||||
includeDomains
|
||||
);
|
||||
|
||||
return {
|
||||
type: "entity",
|
||||
entity: foundEntities[0] || "",
|
||||
};
|
||||
}
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() protected _config?: EntityBadgeConfig;
|
||||
|
||||
public setConfig(config: EntityBadgeConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
get hasAction() {
|
||||
return (
|
||||
!this._config?.tap_action ||
|
||||
hasAction(this._config?.tap_action) ||
|
||||
hasAction(this._config?.hold_action) ||
|
||||
hasAction(this._config?.double_tap_action)
|
||||
);
|
||||
}
|
||||
|
||||
private _computeStateColor = memoizeOne(
|
||||
(stateObj: HassEntity, color?: string) => {
|
||||
// Use custom color if active
|
||||
if (color) {
|
||||
return stateActive(stateObj) ? computeCssColor(color) : undefined;
|
||||
}
|
||||
|
||||
// Use light color if the light support rgb
|
||||
if (
|
||||
computeDomain(stateObj.entity_id) === "light" &&
|
||||
stateObj.attributes.rgb_color
|
||||
) {
|
||||
const hsvColor = rgb2hsv(stateObj.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(stateObj);
|
||||
}
|
||||
);
|
||||
|
||||
private _getImageUrl(stateObj: HassEntity): string | undefined {
|
||||
const entityPicture =
|
||||
stateObj.attributes.entity_picture_local ||
|
||||
stateObj.attributes.entity_picture;
|
||||
|
||||
if (!entityPicture) return undefined;
|
||||
|
||||
let imageUrl = this.hass!.hassUrl(entityPicture);
|
||||
if (computeStateDomain(stateObj) === "camera") {
|
||||
imageUrl = cameraUrlWithWidthHeight(imageUrl, 32, 32);
|
||||
}
|
||||
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._config || !this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const entityId = this._config.entity;
|
||||
const stateObj = entityId ? this.hass.states[entityId] : undefined;
|
||||
|
||||
if (!stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const active = stateActive(stateObj);
|
||||
const color = this._computeStateColor(stateObj, this._config.color);
|
||||
|
||||
const style = {
|
||||
"--badge-color": color,
|
||||
};
|
||||
|
||||
const stateDisplay = html`
|
||||
<state-display
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
.content=${this._config.state_content}
|
||||
>
|
||||
</state-display>
|
||||
`;
|
||||
|
||||
const name = this._config.name || stateObj.attributes.friendly_name;
|
||||
|
||||
const displayType = this._config.display_type || DEFAULT_DISPLAY_TYPE;
|
||||
|
||||
const imageUrl = this._config.show_entity_picture
|
||||
? this._getImageUrl(stateObj)
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<div
|
||||
style=${styleMap(style)}
|
||||
class="badge ${classMap({
|
||||
active,
|
||||
[displayType]: true,
|
||||
})}"
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(this._config!.hold_action),
|
||||
hasDoubleClick: hasAction(this._config!.double_tap_action),
|
||||
})}
|
||||
role=${ifDefined(this.hasAction ? "button" : undefined)}
|
||||
tabindex=${ifDefined(this.hasAction ? "0" : undefined)}
|
||||
>
|
||||
<ha-ripple .disabled=${!this.hasAction}></ha-ripple>
|
||||
${imageUrl
|
||||
? html`<img src=${imageUrl} aria-hidden />`
|
||||
: html`
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
.icon=${this._config.icon}
|
||||
></ha-state-icon>
|
||||
`}
|
||||
${displayType !== "minimal"
|
||||
? html`
|
||||
<span class="content">
|
||||
${displayType === "complete"
|
||||
? html`<span class="name">${name}</span>`
|
||||
: nothing}
|
||||
<span class="state">${stateDisplay}</span>
|
||||
</span>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAction(ev: ActionHandlerEvent) {
|
||||
handleAction(this, this.hass!, this._config!, ev.detail.action!);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
--badge-color: var(--state-inactive-color);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.badge {
|
||||
position: relative;
|
||||
--ha-ripple-color: var(--badge-color);
|
||||
--ha-ripple-hover-opacity: 0.04;
|
||||
--ha-ripple-pressed-opacity: 0.12;
|
||||
transition:
|
||||
box-shadow 180ms ease-in-out,
|
||||
border-color 180ms ease-in-out;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
height: 36px;
|
||||
min-width: 36px;
|
||||
padding: 0px 8px;
|
||||
box-sizing: border-box;
|
||||
width: auto;
|
||||
border-radius: 18px;
|
||||
background-color: var(--card-background-color, white);
|
||||
border-width: var(--ha-card-border-width, 1px);
|
||||
border-style: solid;
|
||||
border-color: var(
|
||||
--ha-card-border-color,
|
||||
var(--divider-color, #e0e0e0)
|
||||
);
|
||||
--mdc-icon-size: 18px;
|
||||
text-align: center;
|
||||
font-family: Roboto;
|
||||
}
|
||||
.badge:focus-visible {
|
||||
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
|
||||
--shadow-focus: 0 0 0 1px var(--badge-color);
|
||||
border-color: var(--badge-color);
|
||||
box-shadow: var(--shadow-default), var(--shadow-focus);
|
||||
}
|
||||
button,
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
button:focus,
|
||||
[role="button"]:focus {
|
||||
outline: none;
|
||||
}
|
||||
.badge.active {
|
||||
--badge-color: var(--primary-color);
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding-right: 4px;
|
||||
padding-inline-end: 4px;
|
||||
padding-inline-start: initial;
|
||||
}
|
||||
.name {
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 10px;
|
||||
letter-spacing: 0.1px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.state {
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.1px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
ha-state-icon {
|
||||
color: var(--badge-color);
|
||||
line-height: 0;
|
||||
}
|
||||
img {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
overflow: hidden;
|
||||
}
|
||||
.badge.minimal {
|
||||
padding: 0;
|
||||
}
|
||||
.badge:not(.minimal) img {
|
||||
margin-left: -6px;
|
||||
margin-inline-start: -6px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-entity-badge": HuiEntityBadge;
|
||||
}
|
||||
}
|
@ -8,9 +8,10 @@ import {
|
||||
checkConditionsMet,
|
||||
extractConditionEntityIds,
|
||||
} from "../common/validate-condition";
|
||||
import { createBadgeElement } from "../create-element/create-badge-element";
|
||||
import { EntityFilterEntityConfig } from "../entity-rows/types";
|
||||
import { LovelaceBadge } from "../types";
|
||||
import "./hui-badge";
|
||||
import type { HuiBadge } from "./hui-badge";
|
||||
import { EntityFilterBadgeConfig } from "./types";
|
||||
|
||||
@customElement("hui-entity-filter-badge")
|
||||
@ -18,11 +19,13 @@ export class HuiEntityFilterBadge
|
||||
extends ReactiveElement
|
||||
implements LovelaceBadge
|
||||
{
|
||||
@property({ attribute: false }) public preview = false;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _config?: EntityFilterBadgeConfig;
|
||||
|
||||
private _elements?: LovelaceBadge[];
|
||||
private _elements?: HuiBadge[];
|
||||
|
||||
private _configEntities?: EntityFilterEntityConfig[];
|
||||
|
||||
@ -121,8 +124,11 @@ export class HuiEntityFilterBadge
|
||||
if (!isSame) {
|
||||
this._elements = [];
|
||||
for (const badgeConfig of entitiesList) {
|
||||
const element = createBadgeElement(badgeConfig);
|
||||
const element = document.createElement("hui-badge");
|
||||
element.hass = this.hass;
|
||||
element.preview = this.preview;
|
||||
element.config = badgeConfig;
|
||||
element.load();
|
||||
this._elements.push(element);
|
||||
}
|
||||
this._oldEntities = entitiesList;
|
||||
@ -140,7 +146,10 @@ export class HuiEntityFilterBadge
|
||||
this.appendChild(element);
|
||||
}
|
||||
|
||||
this.style.display = "inline";
|
||||
this.style.display = "flex";
|
||||
this.style.flexWrap = "wrap";
|
||||
this.style.justifyContent = "center";
|
||||
this.style.gap = "8px";
|
||||
}
|
||||
|
||||
private haveEntitiesChanged(oldHass?: HomeAssistant): boolean {
|
||||
|
@ -1,10 +1,13 @@
|
||||
import { mdiAlert } from "@mdi/js";
|
||||
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-label-badge";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { showAlertDialog } from "../custom-card-helpers";
|
||||
import { LovelaceBadge } from "../types";
|
||||
import { HuiEntityBadge } from "./hui-entity-badge";
|
||||
import { ErrorBadgeConfig } from "./types";
|
||||
|
||||
export const createErrorBadgeElement = (config) => {
|
||||
@ -28,24 +31,65 @@ export class HuiErrorBadge extends LitElement implements LovelaceBadge {
|
||||
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-label-badge label="Error" description=${this._config.error}>
|
||||
<ha-svg-icon .path=${mdiAlert}></ha-svg-icon>
|
||||
</ha-label-badge>
|
||||
<button class="badge error" @click=${this._viewDetail}>
|
||||
<ha-svg-icon .hass=${this.hass} .path=${mdiAlertCircle}></ha-svg-icon>
|
||||
<ha-ripple></ha-ripple>
|
||||
<span class="content">
|
||||
<span class="name">Error</span>
|
||||
<span class="state">${this._config.error}</span>
|
||||
</span>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
--ha-label-badge-color: var(--label-badge-red, #fce588);
|
||||
}
|
||||
`;
|
||||
return [
|
||||
HuiEntityBadge.styles,
|
||||
css`
|
||||
.badge.error {
|
||||
--badge-color: var(--error-color);
|
||||
border-color: var(--badge-color);
|
||||
}
|
||||
ha-svg-icon {
|
||||
color: var(--badge-color);
|
||||
}
|
||||
.state {
|
||||
max-width: 100px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
pre {
|
||||
font-family: var(--code-font-family, monospace);
|
||||
white-space: break-spaces;
|
||||
user-select: text;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
177
src/panels/lovelace/badges/hui-view-badges.ts
Normal file
177
src/panels/lovelace/badges/hui-view-badges.ts
Normal file
@ -0,0 +1,177 @@
|
||||
import { mdiPlus } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-sortable";
|
||||
import type { HaSortableOptions } from "../../../components/ha-sortable";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "../components/hui-badge-edit-mode";
|
||||
import { moveBadge } from "../editor/config-util";
|
||||
import { Lovelace } from "../types";
|
||||
import { HuiBadge } from "./hui-badge";
|
||||
|
||||
const BADGE_SORTABLE_OPTIONS: HaSortableOptions = {
|
||||
delay: 100,
|
||||
delayOnTouchOnly: true,
|
||||
direction: "horizontal",
|
||||
invertedSwapThreshold: 0.7,
|
||||
} as HaSortableOptions;
|
||||
|
||||
@customElement("hui-view-badges")
|
||||
export class HuiViewBadges extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public lovelace!: Lovelace;
|
||||
|
||||
@property({ attribute: false }) public badges: HuiBadge[] = [];
|
||||
|
||||
@property({ attribute: false }) public viewIndex!: number;
|
||||
|
||||
@state() _dragging = false;
|
||||
|
||||
private _badgeConfigKeys = new WeakMap<HuiBadge, string>();
|
||||
|
||||
private _getBadgeKey(badge: HuiBadge) {
|
||||
if (!this._badgeConfigKeys.has(badge)) {
|
||||
this._badgeConfigKeys.set(badge, Math.random().toString());
|
||||
}
|
||||
return this._badgeConfigKeys.get(badge)!;
|
||||
}
|
||||
|
||||
private _badgeMoved(ev) {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex, oldPath, newPath } = ev.detail;
|
||||
const newConfig = moveBadge(
|
||||
this.lovelace!.config,
|
||||
[...oldPath, oldIndex] as [number, number, number],
|
||||
[...newPath, newIndex] as [number, number, number]
|
||||
);
|
||||
this.lovelace!.saveConfig(newConfig);
|
||||
}
|
||||
|
||||
private _dragStart() {
|
||||
this._dragging = true;
|
||||
}
|
||||
|
||||
private _dragEnd() {
|
||||
this._dragging = false;
|
||||
}
|
||||
|
||||
private _addBadge() {
|
||||
fireEvent(this, "ll-create-badge");
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.lovelace) return nothing;
|
||||
|
||||
const editMode = this.lovelace.editMode;
|
||||
|
||||
const badges = this.badges;
|
||||
|
||||
return html`
|
||||
${badges?.length > 0 || editMode
|
||||
? html`
|
||||
<ha-sortable
|
||||
.disabled=${!editMode}
|
||||
@item-moved=${this._badgeMoved}
|
||||
@drag-start=${this._dragStart}
|
||||
@drag-end=${this._dragEnd}
|
||||
group="badge"
|
||||
draggable-selector="[data-sortable]"
|
||||
.path=${[this.viewIndex]}
|
||||
.rollback=${false}
|
||||
.options=${BADGE_SORTABLE_OPTIONS}
|
||||
invert-swap
|
||||
>
|
||||
<div class="badges">
|
||||
${repeat(
|
||||
badges,
|
||||
(badge) => this._getBadgeKey(badge),
|
||||
(badge, idx) => html`
|
||||
${editMode
|
||||
? html`
|
||||
<hui-badge-edit-mode
|
||||
data-sortable
|
||||
.hass=${this.hass}
|
||||
.lovelace=${this.lovelace}
|
||||
.path=${[this.viewIndex, idx]}
|
||||
.hiddenOverlay=${this._dragging}
|
||||
>
|
||||
${badge}
|
||||
</hui-badge-edit-mode>
|
||||
`
|
||||
: badge}
|
||||
`
|
||||
)}
|
||||
${editMode
|
||||
? html`
|
||||
<button
|
||||
class="add"
|
||||
@click=${this._addBadge}
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.section.add_card"
|
||||
)}
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.section.add_card"
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlus}></ha-svg-icon>
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-sortable>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
.badges {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
hui-badge-edit-mode {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.add {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 36px;
|
||||
padding: 6px 20px 6px 20px;
|
||||
box-sizing: border-box;
|
||||
width: auto;
|
||||
border-radius: 18px;
|
||||
background-color: transparent;
|
||||
border-width: 2px;
|
||||
border-style: dashed;
|
||||
border-color: var(--primary-color);
|
||||
--mdc-icon-size: 18px;
|
||||
cursor: pointer;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.add:focus {
|
||||
border-style: solid;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-view-badges": HuiViewBadges;
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ export interface EntityFilterBadgeConfig extends LovelaceBadgeConfig {
|
||||
|
||||
export interface ErrorBadgeConfig extends LovelaceBadgeConfig {
|
||||
error: string;
|
||||
origConfig: LovelaceBadgeConfig;
|
||||
}
|
||||
|
||||
export interface StateLabelBadgeConfig extends LovelaceBadgeConfig {
|
||||
@ -25,3 +26,17 @@ export interface StateLabelBadgeConfig extends LovelaceBadgeConfig {
|
||||
hold_action?: ActionConfig;
|
||||
double_tap_action?: ActionConfig;
|
||||
}
|
||||
|
||||
export interface EntityBadgeConfig extends LovelaceBadgeConfig {
|
||||
type: "entity";
|
||||
entity?: string;
|
||||
name?: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
show_entity_picture?: boolean;
|
||||
display_type?: "minimal" | "standard" | "complete";
|
||||
state_content?: string | string[];
|
||||
tap_action?: ActionConfig;
|
||||
hold_action?: ActionConfig;
|
||||
double_tap_action?: ActionConfig;
|
||||
}
|
||||
|
@ -244,7 +244,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
const color = this._computeStateColor(stateObj, this._config.color);
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
|
||||
const localizedState = this._config.hide_state
|
||||
const stateDisplay = this._config.hide_state
|
||||
? nothing
|
||||
: html`
|
||||
<state-display
|
||||
@ -311,7 +311,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
<ha-tile-info
|
||||
id="info"
|
||||
.primary=${name}
|
||||
.secondary=${localizedState}
|
||||
.secondary=${stateDisplay}
|
||||
></ha-tile-info>
|
||||
</div>
|
||||
${this._config.features
|
||||
|
275
src/panels/lovelace/components/hui-badge-edit-mode.ts
Normal file
275
src/panels/lovelace/components/hui-badge-edit-mode.ts
Normal file
@ -0,0 +1,275 @@
|
||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import {
|
||||
mdiContentDuplicate,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiPencil,
|
||||
} from "@mdi/js";
|
||||
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { showEditBadgeDialog } from "../editor/badge-editor/show-edit-badge-dialog";
|
||||
import {
|
||||
LovelaceCardPath,
|
||||
findLovelaceItems,
|
||||
getLovelaceContainerPath,
|
||||
parseLovelaceCardPath,
|
||||
} from "../editor/lovelace-path";
|
||||
import { Lovelace } from "../types";
|
||||
|
||||
@customElement("hui-badge-edit-mode")
|
||||
export class HuiBadgeEditMode extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public lovelace!: Lovelace;
|
||||
|
||||
@property({ type: Array }) public path!: LovelaceCardPath;
|
||||
|
||||
@property({ type: Boolean }) public hiddenOverlay = false;
|
||||
|
||||
@state()
|
||||
public _menuOpened: boolean = false;
|
||||
|
||||
@state()
|
||||
public _hover: boolean = false;
|
||||
|
||||
@state()
|
||||
public _focused: boolean = false;
|
||||
|
||||
@storage({
|
||||
key: "lovelaceClipboard",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
storage: "sessionStorage",
|
||||
})
|
||||
protected _clipboard?: LovelaceCardConfig;
|
||||
|
||||
private get _badges() {
|
||||
const containerPath = getLovelaceContainerPath(this.path!);
|
||||
return findLovelaceItems("badges", this.lovelace!.config, containerPath)!;
|
||||
}
|
||||
|
||||
private _touchStarted = false;
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this.addEventListener("focus", () => {
|
||||
this._focused = true;
|
||||
});
|
||||
this.addEventListener("blur", () => {
|
||||
this._focused = false;
|
||||
});
|
||||
this.addEventListener("touchstart", () => {
|
||||
this._touchStarted = true;
|
||||
});
|
||||
this.addEventListener("touchend", () => {
|
||||
setTimeout(() => {
|
||||
this._touchStarted = false;
|
||||
}, 10);
|
||||
});
|
||||
this.addEventListener("mouseenter", () => {
|
||||
if (this._touchStarted) return;
|
||||
this._hover = true;
|
||||
});
|
||||
this.addEventListener("mouseout", () => {
|
||||
this._hover = false;
|
||||
});
|
||||
this.addEventListener("click", () => {
|
||||
this._hover = true;
|
||||
document.addEventListener("click", this._documentClicked);
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
document.removeEventListener("click", this._documentClicked);
|
||||
}
|
||||
|
||||
_documentClicked = (ev) => {
|
||||
this._hover = ev.composedPath().includes(this);
|
||||
document.removeEventListener("click", this._documentClicked);
|
||||
};
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const showOverlay =
|
||||
(this._hover || this._menuOpened || this._focused) && !this.hiddenOverlay;
|
||||
|
||||
return html`
|
||||
<div class="badge-wrapper" inert><slot></slot></div>
|
||||
<div class="badge-overlay ${classMap({ visible: showOverlay })}">
|
||||
<div
|
||||
class="edit"
|
||||
@click=${this._editBadge}
|
||||
@keydown=${this._editBadge}
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="edit-overlay"></div>
|
||||
<ha-svg-icon class="edit" .path=${mdiPencil}> </ha-svg-icon>
|
||||
</div>
|
||||
<ha-button-menu
|
||||
class="more"
|
||||
corner="BOTTOM_END"
|
||||
menuCorner="END"
|
||||
.path=${[this.path!]}
|
||||
@action=${this._handleAction}
|
||||
@opened=${this._handleOpened}
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<ha-icon-button slot="trigger" .path=${mdiDotsVertical}>
|
||||
</ha-icon-button>
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.duplicate"
|
||||
)}
|
||||
</ha-list-item>
|
||||
<li divider role="separator"></li>
|
||||
<ha-list-item graphic="icon" class="warning">
|
||||
${this.hass.localize("ui.panel.lovelace.editor.edit_card.delete")}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="graphic"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleOpened() {
|
||||
this._menuOpened = true;
|
||||
}
|
||||
|
||||
private _handleClosed() {
|
||||
this._menuOpened = false;
|
||||
}
|
||||
|
||||
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._duplicateCard();
|
||||
break;
|
||||
case 1:
|
||||
this._deleteCard();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _duplicateCard(): void {
|
||||
const { cardIndex } = parseLovelaceCardPath(this.path!);
|
||||
const containerPath = getLovelaceContainerPath(this.path!);
|
||||
const badgeConfig = this._badges![cardIndex];
|
||||
showEditBadgeDialog(this, {
|
||||
lovelaceConfig: this.lovelace!.config,
|
||||
saveConfig: this.lovelace!.saveConfig,
|
||||
path: containerPath,
|
||||
badgeConfig,
|
||||
});
|
||||
}
|
||||
|
||||
private _editBadge(ev): void {
|
||||
if (ev.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "ll-edit-badge", { path: this.path! });
|
||||
}
|
||||
|
||||
private _deleteCard(): void {
|
||||
fireEvent(this, "ll-delete-badge", { path: this.path! });
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.badge-overlay {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: opacity 180ms ease-in-out;
|
||||
}
|
||||
|
||||
.badge-overlay.visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.badge-wrapper {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.edit {
|
||||
outline: none !important;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
z-index: 0;
|
||||
}
|
||||
.edit-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0.8;
|
||||
background-color: var(--primary-background-color);
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
z-index: 0;
|
||||
}
|
||||
.edit ha-svg-icon {
|
||||
display: flex;
|
||||
position: relative;
|
||||
color: var(--primary-text-color);
|
||||
border-radius: 50%;
|
||||
padding: 4px;
|
||||
background: var(--secondary-background-color);
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
.more {
|
||||
position: absolute;
|
||||
right: -8px;
|
||||
top: -8px;
|
||||
inset-inline-end: -10px;
|
||||
inset-inline-start: initial;
|
||||
}
|
||||
.more ha-icon-button {
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
background: var(--secondary-background-color);
|
||||
--mdc-icon-button-size: 24px;
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-badge-edit-mode": HuiBadgeEditMode;
|
||||
}
|
||||
}
|
@ -24,7 +24,7 @@ import { HomeAssistant } from "../../../types";
|
||||
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||
import {
|
||||
LovelaceCardPath,
|
||||
findLovelaceCards,
|
||||
findLovelaceItems,
|
||||
getLovelaceContainerPath,
|
||||
parseLovelaceCardPath,
|
||||
} from "../editor/lovelace-path";
|
||||
@ -59,7 +59,7 @@ export class HuiCardEditMode extends LitElement {
|
||||
|
||||
private get _cards() {
|
||||
const containerPath = getLovelaceContainerPath(this.path!);
|
||||
return findLovelaceCards(this.lovelace!.config, containerPath)!;
|
||||
return findLovelaceItems("cards", this.lovelace!.config, containerPath)!;
|
||||
}
|
||||
|
||||
private _touchStarted = false;
|
||||
|
@ -46,7 +46,7 @@ import {
|
||||
} from "../editor/config-util";
|
||||
import {
|
||||
LovelaceCardPath,
|
||||
findLovelaceCards,
|
||||
findLovelaceItems,
|
||||
getLovelaceContainerPath,
|
||||
parseLovelaceCardPath,
|
||||
} from "../editor/lovelace-path";
|
||||
@ -91,7 +91,7 @@ export class HuiCardOptions extends LitElement {
|
||||
|
||||
private get _cards() {
|
||||
const containerPath = getLovelaceContainerPath(this.path!);
|
||||
return findLovelaceCards(this.lovelace!.config, containerPath)!;
|
||||
return findLovelaceItems("cards", this.lovelace!.config, containerPath)!;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
|
@ -1,8 +1,12 @@
|
||||
import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||
import "../badges/hui-entity-badge";
|
||||
import "../badges/hui-state-label-badge";
|
||||
import { createLovelaceElement } from "./create-element-base";
|
||||
import {
|
||||
createLovelaceElement,
|
||||
getLovelaceElementClass,
|
||||
} from "./create-element-base";
|
||||
|
||||
const ALWAYS_LOADED_TYPES = new Set(["error", "state-label"]);
|
||||
const ALWAYS_LOADED_TYPES = new Set(["error", "state-label", "entity"]);
|
||||
const LAZY_LOAD_TYPES = {
|
||||
"entity-filter": () => import("../badges/hui-entity-filter-badge"),
|
||||
};
|
||||
@ -14,5 +18,8 @@ export const createBadgeElement = (config: LovelaceBadgeConfig) =>
|
||||
ALWAYS_LOADED_TYPES,
|
||||
LAZY_LOAD_TYPES,
|
||||
undefined,
|
||||
"state-label"
|
||||
"entity"
|
||||
);
|
||||
|
||||
export const getBadgeElementClass = (type: string) =>
|
||||
getLovelaceElementClass(type, "badge", ALWAYS_LOADED_TYPES, LAZY_LOAD_TYPES);
|
||||
|
@ -12,13 +12,13 @@ import {
|
||||
stripCustomPrefix,
|
||||
} from "../../../data/lovelace_custom_cards";
|
||||
import { LovelaceCardFeatureConfig } from "../card-features/types";
|
||||
import type { HuiErrorCard } from "../cards/hui-error-card";
|
||||
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 {
|
||||
LovelaceBadge,
|
||||
LovelaceBadgeConstructor,
|
||||
LovelaceCard,
|
||||
LovelaceCardConstructor,
|
||||
LovelaceCardFeature,
|
||||
@ -39,7 +39,7 @@ interface CreateElementConfigTypes {
|
||||
badge: {
|
||||
config: LovelaceBadgeConfig;
|
||||
element: LovelaceBadge;
|
||||
constructor: unknown;
|
||||
constructor: LovelaceBadgeConstructor;
|
||||
};
|
||||
element: {
|
||||
config: LovelaceElementConfig;
|
||||
@ -87,16 +87,36 @@ export const createErrorCardElement = (config: ErrorCardConfig) => {
|
||||
return el;
|
||||
};
|
||||
|
||||
export const createErrorBadgeElement = (config: ErrorCardConfig) => {
|
||||
const el = document.createElement("hui-error-badge");
|
||||
if (customElements.get("hui-error-badge")) {
|
||||
el.setConfig(config);
|
||||
} else {
|
||||
import("../badges/hui-error-badge");
|
||||
customElements.whenDefined("hui-error-badge").then(() => {
|
||||
customElements.upgrade(el);
|
||||
el.setConfig(config);
|
||||
});
|
||||
}
|
||||
return el;
|
||||
};
|
||||
|
||||
export const createErrorCardConfig = (error, origConfig) => ({
|
||||
type: "error",
|
||||
error,
|
||||
origConfig,
|
||||
});
|
||||
|
||||
export const createErrorBadgeConfig = (error, origConfig) => ({
|
||||
type: "error",
|
||||
error,
|
||||
origConfig,
|
||||
});
|
||||
|
||||
const _createElement = <T extends keyof CreateElementConfigTypes>(
|
||||
tag: string,
|
||||
config: CreateElementConfigTypes[T]["config"]
|
||||
): CreateElementConfigTypes[T]["element"] | HuiErrorCard => {
|
||||
): CreateElementConfigTypes[T]["element"] => {
|
||||
const element = document.createElement(
|
||||
tag
|
||||
) as CreateElementConfigTypes[T]["element"];
|
||||
@ -106,11 +126,18 @@ const _createElement = <T extends keyof CreateElementConfigTypes>(
|
||||
};
|
||||
|
||||
const _createErrorElement = <T extends keyof CreateElementConfigTypes>(
|
||||
tagSuffix: T,
|
||||
error: string,
|
||||
config: CreateElementConfigTypes[T]["config"]
|
||||
): HuiErrorCard => createErrorCardElement(createErrorCardConfig(error, config));
|
||||
): CreateElementConfigTypes[T]["element"] => {
|
||||
if (tagSuffix === "badge") {
|
||||
return createErrorBadgeElement(createErrorBadgeConfig(error, config));
|
||||
}
|
||||
return createErrorCardElement(createErrorCardConfig(error, config));
|
||||
};
|
||||
|
||||
const _customCreate = <T extends keyof CreateElementConfigTypes>(
|
||||
tagSuffix: T,
|
||||
tag: string,
|
||||
config: CreateElementConfigTypes[T]["config"]
|
||||
) => {
|
||||
@ -119,6 +146,7 @@ const _customCreate = <T extends keyof CreateElementConfigTypes>(
|
||||
}
|
||||
|
||||
const element = _createErrorElement(
|
||||
tagSuffix,
|
||||
`Custom element doesn't exist: ${tag}.`,
|
||||
config
|
||||
);
|
||||
@ -175,7 +203,7 @@ export const createLovelaceElement = <T extends keyof CreateElementConfigTypes>(
|
||||
domainTypes?: { _domain_not_found: string; [domain: string]: string },
|
||||
// Default type if no type given. If given, entity types will not work.
|
||||
defaultType?: string
|
||||
): CreateElementConfigTypes[T]["element"] | HuiErrorCard => {
|
||||
): CreateElementConfigTypes[T]["element"] => {
|
||||
try {
|
||||
return tryCreateLovelaceElement(
|
||||
tagSuffix,
|
||||
@ -188,7 +216,7 @@ export const createLovelaceElement = <T extends keyof CreateElementConfigTypes>(
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line
|
||||
console.error(tagSuffix, config.type, err);
|
||||
return _createErrorElement(err.message, config);
|
||||
return _createErrorElement(tagSuffix, err.message, config);
|
||||
}
|
||||
};
|
||||
|
||||
@ -203,7 +231,7 @@ export const tryCreateLovelaceElement = <
|
||||
domainTypes?: { _domain_not_found: string; [domain: string]: string },
|
||||
// Default type if no type given. If given, entity types will not work.
|
||||
defaultType?: string
|
||||
): CreateElementConfigTypes[T]["element"] | HuiErrorCard => {
|
||||
): CreateElementConfigTypes[T]["element"] => {
|
||||
if (!config || typeof config !== "object") {
|
||||
throw new Error("Config is not an object");
|
||||
}
|
||||
@ -220,7 +248,7 @@ export const tryCreateLovelaceElement = <
|
||||
const customTag = config.type ? _getCustomTag(config.type) : undefined;
|
||||
|
||||
if (customTag) {
|
||||
return _customCreate(customTag, config);
|
||||
return _customCreate(tagSuffix, customTag, config);
|
||||
}
|
||||
|
||||
let type: string | undefined;
|
||||
|
@ -0,0 +1,109 @@
|
||||
import { css, CSSResultGroup, html, nothing, TemplateResult } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
|
||||
import { getBadgeElementClass } from "../../create-element/create-badge-element";
|
||||
import type { LovelaceCardEditor, LovelaceConfigForm } from "../../types";
|
||||
import { HuiElementEditor } from "../hui-element-editor";
|
||||
import "./hui-badge-visibility-editor";
|
||||
|
||||
type Tab = "config" | "visibility";
|
||||
|
||||
@customElement("hui-badge-element-editor")
|
||||
export class HuiBadgeElementEditor extends HuiElementEditor<LovelaceBadgeConfig> {
|
||||
@state() private _curTab: Tab = "config";
|
||||
|
||||
protected async getConfigElement(): Promise<LovelaceCardEditor | undefined> {
|
||||
const elClass = await getBadgeElementClass(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 getBadgeElementClass(this.configElementType!);
|
||||
|
||||
// Check if a schema exists
|
||||
if (elClass && elClass.getConfigForm) {
|
||||
return elClass.getConfigForm();
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _handleTabSelected(ev: CustomEvent): void {
|
||||
if (!ev.detail.value) {
|
||||
return;
|
||||
}
|
||||
this._curTab = ev.detail.value.id;
|
||||
}
|
||||
|
||||
private _configChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
this.value = ev.detail.value;
|
||||
}
|
||||
|
||||
protected renderConfigElement(): TemplateResult {
|
||||
const displayedTabs: Tab[] = ["config", "visibility"];
|
||||
|
||||
let content: TemplateResult<1> | typeof nothing = nothing;
|
||||
|
||||
switch (this._curTab) {
|
||||
case "config":
|
||||
content = html`${super.renderConfigElement()}`;
|
||||
break;
|
||||
case "visibility":
|
||||
content = html`
|
||||
<hui-badge-visibility-editor
|
||||
.hass=${this.hass}
|
||||
.config=${this.value}
|
||||
@value-changed=${this._configChanged}
|
||||
></hui-badge-visibility-editor>
|
||||
`;
|
||||
break;
|
||||
}
|
||||
return html`
|
||||
<paper-tabs
|
||||
scrollable
|
||||
hide-scroll-buttons
|
||||
.selected=${displayedTabs.indexOf(this._curTab)}
|
||||
@selected-item-changed=${this._handleTabSelected}
|
||||
>
|
||||
${displayedTabs.map(
|
||||
(tab, index) => html`
|
||||
<paper-tab id=${tab} .dialogInitialFocus=${index === 0}>
|
||||
${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_badge.tab_${tab}`
|
||||
)}
|
||||
</paper-tab>
|
||||
`
|
||||
)}
|
||||
</paper-tabs>
|
||||
${content}
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
HuiElementEditor.styles,
|
||||
css`
|
||||
paper-tabs {
|
||||
--paper-tabs-selection-bar-color: var(--primary-color);
|
||||
color: var(--primary-text-color);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-badge-element-editor": HuiBadgeElementEditor;
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { Condition } from "../../common/validate-condition";
|
||||
import "../conditions/ha-card-conditions-editor";
|
||||
|
||||
@customElement("hui-badge-visibility-editor")
|
||||
export class HuiBadgeVisibilityEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public config!: LovelaceCardConfig;
|
||||
|
||||
render() {
|
||||
const conditions = this.config.visibility ?? [];
|
||||
return html`
|
||||
<p class="intro">
|
||||
${this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_badge.visibility.explanation`
|
||||
)}
|
||||
</p>
|
||||
<ha-card-conditions-editor
|
||||
.hass=${this.hass}
|
||||
.conditions=${conditions}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-card-conditions-editor>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const conditions = ev.detail.value as Condition[];
|
||||
const newConfig: LovelaceCardConfig = {
|
||||
...this.config,
|
||||
visibility: conditions,
|
||||
};
|
||||
if (newConfig.visibility?.length === 0) {
|
||||
delete newConfig.visibility;
|
||||
}
|
||||
fireEvent(this, "value-changed", { value: newConfig });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.intro {
|
||||
margin: 0;
|
||||
color: var(--secondary-text-color);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-badge-visibility-editor": HuiBadgeVisibilityEditor;
|
||||
}
|
||||
}
|
521
src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts
Normal file
521
src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts
Normal file
@ -0,0 +1,521 @@
|
||||
import { mdiClose, mdiHelpCircle } from "@mdi/js";
|
||||
import deepFreeze from "deep-freeze";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
|
||||
import "../../../../components/ha-circular-progress";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import {
|
||||
defaultBadgeConfig,
|
||||
LovelaceBadgeConfig,
|
||||
} from "../../../../data/lovelace/config/badge";
|
||||
import { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import {
|
||||
getCustomBadgeEntry,
|
||||
isCustomType,
|
||||
stripCustomPrefix,
|
||||
} from "../../../../data/lovelace_custom_cards";
|
||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||
import "../../badges/hui-badge";
|
||||
import "../../sections/hui-section";
|
||||
import { addBadge, replaceBadge } from "../config-util";
|
||||
import { getBadgeDocumentationURL } from "../get-dashboard-documentation-url";
|
||||
import type { ConfigChangedEvent } from "../hui-element-editor";
|
||||
import { findLovelaceContainer } from "../lovelace-path";
|
||||
import type { GUIModeChangedEvent } from "../types";
|
||||
import "./hui-badge-element-editor";
|
||||
import type { HuiBadgeElementEditor } from "./hui-badge-element-editor";
|
||||
import type { EditBadgeDialogParams } from "./show-edit-badge-dialog";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"reload-lovelace": undefined;
|
||||
}
|
||||
// for add event listener
|
||||
interface HTMLElementEventMap {
|
||||
"reload-lovelace": HASSDomEvent<undefined>;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("hui-dialog-edit-badge")
|
||||
export class HuiDialogEditBadge
|
||||
extends LitElement
|
||||
implements HassDialog<EditBadgeDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public large = false;
|
||||
|
||||
@state() private _params?: EditBadgeDialogParams;
|
||||
|
||||
@state() private _badgeConfig?: LovelaceBadgeConfig;
|
||||
|
||||
@state() private _containerConfig!: LovelaceViewConfig;
|
||||
|
||||
@state() private _saving = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _guiModeAvailable? = true;
|
||||
|
||||
@query("hui-badge-element-editor")
|
||||
private _badgeEditorEl?: HuiBadgeElementEditor;
|
||||
|
||||
@state() private _GUImode = true;
|
||||
|
||||
@state() private _documentationURL?: string;
|
||||
|
||||
@state() private _dirty = false;
|
||||
|
||||
@state() private _isEscapeEnabled = true;
|
||||
|
||||
public async showDialog(params: EditBadgeDialogParams): Promise<void> {
|
||||
this._params = params;
|
||||
this._GUImode = true;
|
||||
this._guiModeAvailable = true;
|
||||
|
||||
const containerConfig = findLovelaceContainer(
|
||||
params.lovelaceConfig,
|
||||
params.path
|
||||
);
|
||||
|
||||
if ("strategy" in containerConfig) {
|
||||
throw new Error("Can't edit strategy");
|
||||
}
|
||||
|
||||
this._containerConfig = containerConfig;
|
||||
|
||||
if ("badgeConfig" in params) {
|
||||
this._badgeConfig = params.badgeConfig;
|
||||
this._dirty = true;
|
||||
} else {
|
||||
const badge = this._containerConfig.badges?.[params.badgeIndex];
|
||||
this._badgeConfig =
|
||||
typeof badge === "string" ? defaultBadgeConfig(badge) : badge;
|
||||
}
|
||||
|
||||
this.large = false;
|
||||
if (this._badgeConfig && !Object.isFrozen(this._badgeConfig)) {
|
||||
this._badgeConfig = deepFreeze(this._badgeConfig);
|
||||
}
|
||||
}
|
||||
|
||||
public closeDialog(): boolean {
|
||||
this._isEscapeEnabled = true;
|
||||
window.removeEventListener("dialog-closed", this._enableEscapeKeyClose);
|
||||
window.removeEventListener("hass-more-info", this._disableEscapeKeyClose);
|
||||
if (this._dirty) {
|
||||
this._confirmCancel();
|
||||
return false;
|
||||
}
|
||||
this._params = undefined;
|
||||
this._badgeConfig = undefined;
|
||||
this._error = undefined;
|
||||
this._documentationURL = undefined;
|
||||
this._dirty = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
return true;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
if (
|
||||
!this._badgeConfig ||
|
||||
this._documentationURL !== undefined ||
|
||||
!changedProps.has("_badgeConfig")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldConfig = changedProps.get("_badgeConfig") as LovelaceBadgeConfig;
|
||||
|
||||
if (oldConfig?.type !== this._badgeConfig!.type) {
|
||||
this._documentationURL = this._badgeConfig!.type
|
||||
? getBadgeDocumentationURL(this.hass, this._badgeConfig!.type)
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _enableEscapeKeyClose = (ev: any) => {
|
||||
if (ev.detail.dialog === "ha-more-info-dialog") {
|
||||
this._isEscapeEnabled = true;
|
||||
}
|
||||
};
|
||||
|
||||
private _disableEscapeKeyClose = () => {
|
||||
this._isEscapeEnabled = false;
|
||||
};
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
let heading: string;
|
||||
if (this._badgeConfig && this._badgeConfig.type) {
|
||||
let badgeName: string | undefined;
|
||||
if (isCustomType(this._badgeConfig.type)) {
|
||||
// prettier-ignore
|
||||
badgeName = getCustomBadgeEntry(
|
||||
stripCustomPrefix(this._badgeConfig.type)
|
||||
)?.name;
|
||||
// Trim names that end in " Card" so as not to redundantly duplicate it
|
||||
if (badgeName?.toLowerCase().endsWith(" badge")) {
|
||||
badgeName = badgeName.substring(0, badgeName.length - 6);
|
||||
}
|
||||
} else {
|
||||
badgeName = this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.badge.${this._badgeConfig.type}.name`
|
||||
);
|
||||
}
|
||||
heading = this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_badge.typed_header",
|
||||
{ type: badgeName }
|
||||
);
|
||||
} else if (!this._badgeConfig) {
|
||||
heading = this._containerConfig.title
|
||||
? this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_badge.pick_badge_view_title",
|
||||
{ name: this._containerConfig.title }
|
||||
)
|
||||
: this.hass!.localize("ui.panel.lovelace.editor.edit_badge.pick_badge");
|
||||
} else {
|
||||
heading = this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_badge.header"
|
||||
);
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
.escapeKeyAction=${this._isEscapeEnabled ? undefined : ""}
|
||||
@keydown=${this._ignoreKeydown}
|
||||
@closed=${this._cancel}
|
||||
@opened=${this._opened}
|
||||
.heading=${heading}
|
||||
>
|
||||
<ha-dialog-header slot="heading">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
dialogAction="cancel"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<span slot="title" @click=${this._enlarge}>${heading}</span>
|
||||
${this._documentationURL !== undefined
|
||||
? html`
|
||||
<a
|
||||
slot="actionItems"
|
||||
href=${this._documentationURL}
|
||||
title=${this.hass!.localize("ui.panel.lovelace.menu.help")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
dir=${computeRTLDirection(this.hass)}
|
||||
>
|
||||
<ha-icon-button .path=${mdiHelpCircle}></ha-icon-button>
|
||||
</a>
|
||||
`
|
||||
: nothing}
|
||||
</ha-dialog-header>
|
||||
<div class="content">
|
||||
<div class="element-editor">
|
||||
<hui-badge-element-editor
|
||||
.hass=${this.hass}
|
||||
.lovelace=${this._params.lovelaceConfig}
|
||||
.value=${this._badgeConfig}
|
||||
@config-changed=${this._handleConfigChanged}
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
@editor-save=${this._save}
|
||||
dialogInitialFocus
|
||||
></hui-badge-element-editor>
|
||||
</div>
|
||||
<div class="element-preview">
|
||||
<hui-badge
|
||||
.hass=${this.hass}
|
||||
.config=${this._badgeConfig}
|
||||
preview
|
||||
class=${this._error ? "blur" : ""}
|
||||
></hui-badge>
|
||||
${this._error
|
||||
? html`
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
aria-label="Can't update badge"
|
||||
></ha-circular-progress>
|
||||
`
|
||||
: ``}
|
||||
</div>
|
||||
</div>
|
||||
${this._badgeConfig !== undefined
|
||||
? html`
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._toggleMode}
|
||||
.disabled=${!this._guiModeAvailable}
|
||||
class="gui-mode-button"
|
||||
>
|
||||
${this.hass!.localize(
|
||||
!this._badgeEditorEl || this._GUImode
|
||||
? "ui.panel.lovelace.editor.edit_badge.show_code_editor"
|
||||
: "ui.panel.lovelace.editor.edit_badge.show_visual_editor"
|
||||
)}
|
||||
</mwc-button>
|
||||
`
|
||||
: ""}
|
||||
<div slot="primaryAction" @click=${this._save}>
|
||||
<mwc-button @click=${this._cancel} dialogInitialFocus>
|
||||
${this.hass!.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
${this._badgeConfig !== undefined && this._dirty
|
||||
? html`
|
||||
<mwc-button
|
||||
?disabled=${!this._canSave || this._saving}
|
||||
@click=${this._save}
|
||||
>
|
||||
${this._saving
|
||||
? html`
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
aria-label="Saving"
|
||||
size="small"
|
||||
></ha-circular-progress>
|
||||
`
|
||||
: this.hass!.localize("ui.common.save")}
|
||||
</mwc-button>
|
||||
`
|
||||
: ``}
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _enlarge() {
|
||||
this.large = !this.large;
|
||||
}
|
||||
|
||||
private _ignoreKeydown(ev: KeyboardEvent) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
private _handleConfigChanged(ev: HASSDomEvent<ConfigChangedEvent>) {
|
||||
this._badgeConfig = deepFreeze(ev.detail.config);
|
||||
this._error = ev.detail.error;
|
||||
this._guiModeAvailable = ev.detail.guiModeAvailable;
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
private _handleGUIModeChanged(ev: HASSDomEvent<GUIModeChangedEvent>): void {
|
||||
ev.stopPropagation();
|
||||
this._GUImode = ev.detail.guiMode;
|
||||
this._guiModeAvailable = ev.detail.guiModeAvailable;
|
||||
}
|
||||
|
||||
private _toggleMode(): void {
|
||||
this._badgeEditorEl?.toggleMode();
|
||||
}
|
||||
|
||||
private _opened() {
|
||||
window.addEventListener("dialog-closed", this._enableEscapeKeyClose);
|
||||
window.addEventListener("hass-more-info", this._disableEscapeKeyClose);
|
||||
this._badgeEditorEl?.focusYamlEditor();
|
||||
}
|
||||
|
||||
private get _canSave(): boolean {
|
||||
if (this._saving) {
|
||||
return false;
|
||||
}
|
||||
if (this._badgeConfig === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (this._badgeEditorEl && this._badgeEditorEl.hasError) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private async _confirmCancel() {
|
||||
// Make sure the open state of this dialog is handled before the open state of confirm dialog
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_badge.unsaved_changes"
|
||||
),
|
||||
text: this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_badge.confirm_cancel"
|
||||
),
|
||||
dismissText: this.hass!.localize("ui.common.stay"),
|
||||
confirmText: this.hass!.localize("ui.common.leave"),
|
||||
});
|
||||
if (confirm) {
|
||||
this._cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private _cancel(ev?: Event) {
|
||||
if (ev) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
this._dirty = false;
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private async _save(): Promise<void> {
|
||||
if (!this._canSave) {
|
||||
return;
|
||||
}
|
||||
if (!this._dirty) {
|
||||
this.closeDialog();
|
||||
return;
|
||||
}
|
||||
this._saving = true;
|
||||
const path = this._params!.path;
|
||||
await this._params!.saveConfig(
|
||||
"badgeConfig" in this._params!
|
||||
? addBadge(this._params!.lovelaceConfig, path, this._badgeConfig!)
|
||||
: replaceBadge(
|
||||
this._params!.lovelaceConfig,
|
||||
[...path, this._params!.badgeIndex],
|
||||
this._badgeConfig!
|
||||
)
|
||||
);
|
||||
this._saving = false;
|
||||
this._dirty = false;
|
||||
showSaveSuccessToast(this, this.hass);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
:host {
|
||||
--code-mirror-max-height: calc(100vh - 176px);
|
||||
}
|
||||
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 100px;
|
||||
--dialog-z-index: 6;
|
||||
--dialog-surface-position: fixed;
|
||||
--dialog-surface-top: 40px;
|
||||
--mdc-dialog-max-width: 90vw;
|
||||
--dialog-content-padding: 24px 12px;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: calc(90vw - 48px);
|
||||
max-width: 1000px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
/* overrule the ha-style-dialog max-height on small screens */
|
||||
ha-dialog {
|
||||
height: 100%;
|
||||
--mdc-dialog-max-height: 100%;
|
||||
--dialog-surface-top: 0px;
|
||||
--mdc-dialog-max-width: 100vw;
|
||||
}
|
||||
.content {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 451px) and (min-height: 501px) {
|
||||
:host([large]) .content {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content .element-editor {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
.content {
|
||||
flex-direction: row;
|
||||
}
|
||||
.content > * {
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.element-editor {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.blur {
|
||||
filter: blur(2px) grayscale(100%);
|
||||
}
|
||||
.element-preview {
|
||||
position: relative;
|
||||
height: max-content;
|
||||
background: var(--primary-background-color);
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.element-preview ha-circular-progress {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
.gui-mode-button {
|
||||
margin-right: auto;
|
||||
margin-inline-end: auto;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
ha-dialog-header a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-dialog-edit-badge": HuiDialogEditBadge;
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import { LovelaceContainerPath } from "../lovelace-path";
|
||||
|
||||
export type EditBadgeDialogParams = {
|
||||
lovelaceConfig: LovelaceConfig;
|
||||
saveConfig: (config: LovelaceConfig) => void;
|
||||
path: LovelaceContainerPath;
|
||||
} & (
|
||||
| {
|
||||
badgeIndex: number;
|
||||
}
|
||||
| {
|
||||
badgeConfig: LovelaceBadgeConfig;
|
||||
}
|
||||
);
|
||||
|
||||
export const importEditBadgeDialog = () => import("./hui-dialog-edit-badge");
|
||||
|
||||
export const showEditBadgeDialog = (
|
||||
element: HTMLElement,
|
||||
editBadgeDialogParams: EditBadgeDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "hui-dialog-edit-badge",
|
||||
dialogImport: importEditBadgeDialog,
|
||||
dialogParams: editBadgeDialogParams,
|
||||
});
|
||||
};
|
@ -30,15 +30,15 @@ import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||
import "../../cards/hui-card";
|
||||
import "../../sections/hui-section";
|
||||
import { addCard, replaceCard } from "../config-util";
|
||||
import { getCardDocumentationURL } from "../get-card-documentation-url";
|
||||
import { getCardDocumentationURL } from "../get-dashboard-documentation-url";
|
||||
import type { ConfigChangedEvent } from "../hui-element-editor";
|
||||
import { findLovelaceContainer } from "../lovelace-path";
|
||||
import type { GUIModeChangedEvent } from "../types";
|
||||
import "./hui-card-element-editor";
|
||||
import type { HuiCardElementEditor } from "./hui-card-element-editor";
|
||||
import "../../cards/hui-card";
|
||||
import type { EditCardDialogParams } from "./show-edit-card-dialog";
|
||||
|
||||
declare global {
|
||||
|
@ -0,0 +1,236 @@
|
||||
import { mdiGestureTap, mdiPalette } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
array,
|
||||
assert,
|
||||
assign,
|
||||
boolean,
|
||||
enums,
|
||||
object,
|
||||
optional,
|
||||
string,
|
||||
union,
|
||||
} from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
DEFAULT_DISPLAY_TYPE,
|
||||
DISPLAY_TYPES,
|
||||
} from "../../badges/hui-entity-badge";
|
||||
import { EntityBadgeConfig } from "../../badges/types";
|
||||
import type { LovelaceBadgeEditor } from "../../types";
|
||||
import "../hui-sub-element-editor";
|
||||
import { actionConfigStruct } from "../structs/action-struct";
|
||||
import { baseLovelaceBadgeConfig } from "../structs/base-badge-struct";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
import "./hui-card-features-editor";
|
||||
|
||||
const badgeConfigStruct = assign(
|
||||
baseLovelaceBadgeConfig,
|
||||
object({
|
||||
entity: optional(string()),
|
||||
display_type: optional(enums(DISPLAY_TYPES)),
|
||||
name: optional(string()),
|
||||
icon: optional(string()),
|
||||
state_content: optional(union([string(), array(string())])),
|
||||
color: optional(string()),
|
||||
show_entity_picture: optional(boolean()),
|
||||
tap_action: optional(actionConfigStruct),
|
||||
})
|
||||
);
|
||||
|
||||
@customElement("hui-entity-badge-editor")
|
||||
export class HuiEntityBadgeEditor
|
||||
extends LitElement
|
||||
implements LovelaceBadgeEditor
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _config?: EntityBadgeConfig;
|
||||
|
||||
public setConfig(config: EntityBadgeConfig): void {
|
||||
assert(config, badgeConfigStruct);
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc) =>
|
||||
[
|
||||
{ name: "entity", selector: { entity: {} } },
|
||||
{
|
||||
name: "",
|
||||
type: "expandable",
|
||||
iconPath: mdiPalette,
|
||||
title: localize(`ui.panel.lovelace.editor.badge.entity.appearance`),
|
||||
schema: [
|
||||
{
|
||||
name: "display_type",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
options: DISPLAY_TYPES.map((type) => ({
|
||||
value: type,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.badge.entity.display_type_options.${type}`
|
||||
),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "name",
|
||||
selector: {
|
||||
text: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "icon",
|
||||
selector: {
|
||||
icon: {},
|
||||
},
|
||||
context: { icon_entity: "entity" },
|
||||
},
|
||||
{
|
||||
name: "color",
|
||||
selector: {
|
||||
ui_color: { default_color: true },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "show_entity_picture",
|
||||
selector: {
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: "state_content",
|
||||
selector: {
|
||||
ui_state_content: {},
|
||||
},
|
||||
context: {
|
||||
filter_entity: "entity",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "expandable",
|
||||
title: localize(`ui.panel.lovelace.editor.badge.entity.actions`),
|
||||
iconPath: mdiGestureTap,
|
||||
schema: [
|
||||
{
|
||||
name: "tap_action",
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "more-info",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const schema = this._schema(this.hass!.localize);
|
||||
|
||||
const data = { ...this._config };
|
||||
|
||||
if (!data.display_type) {
|
||||
data.display_type = DEFAULT_DISPLAY_TYPE;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newConfig = ev.detail.value as EntityBadgeConfig;
|
||||
|
||||
const config: EntityBadgeConfig = {
|
||||
...newConfig,
|
||||
};
|
||||
|
||||
if (!config.state_content) {
|
||||
delete config.state_content;
|
||||
}
|
||||
|
||||
if (config.display_type === "standard") {
|
||||
delete config.display_type;
|
||||
}
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "color":
|
||||
case "state_content":
|
||||
case "display_type":
|
||||
case "show_entity_picture":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.badge.entity.${schema.name}`
|
||||
);
|
||||
default:
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
configElementStyle,
|
||||
css`
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
ha-form {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-entity-badge-editor": HuiEntityBadgeEditor;
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section";
|
||||
import { LovelaceConfig } from "../../../data/lovelace/config/types";
|
||||
@ -9,13 +10,13 @@ import type { HomeAssistant } from "../../../types";
|
||||
import {
|
||||
LovelaceCardPath,
|
||||
LovelaceContainerPath,
|
||||
findLovelaceCards,
|
||||
findLovelaceContainer,
|
||||
findLovelaceItems,
|
||||
getLovelaceContainerPath,
|
||||
parseLovelaceCardPath,
|
||||
parseLovelaceContainerPath,
|
||||
updateLovelaceCards,
|
||||
updateLovelaceContainer,
|
||||
updateLovelaceItems,
|
||||
} from "./lovelace-path";
|
||||
|
||||
export const addCard = (
|
||||
@ -23,9 +24,9 @@ export const addCard = (
|
||||
path: LovelaceContainerPath,
|
||||
cardConfig: LovelaceCardConfig
|
||||
): LovelaceConfig => {
|
||||
const cards = findLovelaceCards(config, path);
|
||||
const cards = findLovelaceItems("cards", config, path);
|
||||
const newCards = cards ? [...cards, cardConfig] : [cardConfig];
|
||||
const newConfig = updateLovelaceCards(config, path, newCards);
|
||||
const newConfig = updateLovelaceItems("cards", config, path, newCards);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
@ -34,9 +35,9 @@ export const addCards = (
|
||||
path: LovelaceContainerPath,
|
||||
cardConfigs: LovelaceCardConfig[]
|
||||
): LovelaceConfig => {
|
||||
const cards = findLovelaceCards(config, path);
|
||||
const cards = findLovelaceItems("cards", config, path);
|
||||
const newCards = cards ? [...cards, ...cardConfigs] : [...cardConfigs];
|
||||
const newConfig = updateLovelaceCards(config, path, newCards);
|
||||
const newConfig = updateLovelaceItems("cards", config, path, newCards);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
@ -48,13 +49,18 @@ export const replaceCard = (
|
||||
const { cardIndex } = parseLovelaceCardPath(path);
|
||||
const containerPath = getLovelaceContainerPath(path);
|
||||
|
||||
const cards = findLovelaceCards(config, containerPath);
|
||||
const cards = findLovelaceItems("cards", config, containerPath);
|
||||
|
||||
const newCards = (cards ?? []).map((origConf, ind) =>
|
||||
ind === cardIndex ? cardConfig : origConf
|
||||
);
|
||||
|
||||
const newConfig = updateLovelaceCards(config, containerPath, newCards);
|
||||
const newConfig = updateLovelaceItems(
|
||||
"cards",
|
||||
config,
|
||||
containerPath,
|
||||
newCards
|
||||
);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
@ -65,11 +71,16 @@ export const deleteCard = (
|
||||
const { cardIndex } = parseLovelaceCardPath(path);
|
||||
const containerPath = getLovelaceContainerPath(path);
|
||||
|
||||
const cards = findLovelaceCards(config, containerPath);
|
||||
const cards = findLovelaceItems("cards", config, containerPath);
|
||||
|
||||
const newCards = (cards ?? []).filter((_origConf, ind) => ind !== cardIndex);
|
||||
|
||||
const newConfig = updateLovelaceCards(config, containerPath, newCards);
|
||||
const newConfig = updateLovelaceItems(
|
||||
"cards",
|
||||
config,
|
||||
containerPath,
|
||||
newCards
|
||||
);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
@ -81,13 +92,18 @@ export const insertCard = (
|
||||
const { cardIndex } = parseLovelaceCardPath(path);
|
||||
const containerPath = getLovelaceContainerPath(path);
|
||||
|
||||
const cards = findLovelaceCards(config, containerPath);
|
||||
const cards = findLovelaceItems("cards", config, containerPath);
|
||||
|
||||
const newCards = cards
|
||||
? [...cards.slice(0, cardIndex), cardConfig, ...cards.slice(cardIndex)]
|
||||
: [cardConfig];
|
||||
|
||||
const newConfig = updateLovelaceCards(config, containerPath, newCards);
|
||||
const newConfig = updateLovelaceItems(
|
||||
"cards",
|
||||
config,
|
||||
containerPath,
|
||||
newCards
|
||||
);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
@ -99,7 +115,7 @@ export const moveCardToIndex = (
|
||||
const { cardIndex } = parseLovelaceCardPath(path);
|
||||
const containerPath = getLovelaceContainerPath(path);
|
||||
|
||||
const cards = findLovelaceCards(config, containerPath);
|
||||
const cards = findLovelaceItems("cards", config, containerPath);
|
||||
|
||||
const newCards = cards ? [...cards] : [];
|
||||
|
||||
@ -110,7 +126,12 @@ export const moveCardToIndex = (
|
||||
newCards.splice(oldIndex, 1);
|
||||
newCards.splice(newIndex, 0, card);
|
||||
|
||||
const newConfig = updateLovelaceCards(config, containerPath, newCards);
|
||||
const newConfig = updateLovelaceItems(
|
||||
"cards",
|
||||
config,
|
||||
containerPath,
|
||||
newCards
|
||||
);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
@ -132,7 +153,7 @@ export const moveCardToContainer = (
|
||||
}
|
||||
|
||||
const fromContainerPath = getLovelaceContainerPath(fromPath);
|
||||
const cards = findLovelaceCards(config, fromContainerPath);
|
||||
const cards = findLovelaceItems("cards", config, fromContainerPath);
|
||||
const card = cards![fromCardIndex];
|
||||
|
||||
let newConfig = addCard(config, toPath, card);
|
||||
@ -148,7 +169,7 @@ export const moveCard = (
|
||||
): LovelaceConfig => {
|
||||
const { cardIndex: fromCardIndex } = parseLovelaceCardPath(fromPath);
|
||||
const fromContainerPath = getLovelaceContainerPath(fromPath);
|
||||
const cards = findLovelaceCards(config, fromContainerPath);
|
||||
const cards = findLovelaceItems("cards", config, fromContainerPath);
|
||||
const card = cards![fromCardIndex];
|
||||
|
||||
let newConfig = deleteCard(config, fromPath);
|
||||
@ -298,3 +319,98 @@ export const moveSection = (
|
||||
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
export const addBadge = (
|
||||
config: LovelaceConfig,
|
||||
path: LovelaceContainerPath,
|
||||
badgeConfig: LovelaceBadgeConfig
|
||||
): LovelaceConfig => {
|
||||
const badges = findLovelaceItems("badges", config, path);
|
||||
const newBadges = badges ? [...badges, badgeConfig] : [badgeConfig];
|
||||
const newConfig = updateLovelaceItems("badges", config, path, newBadges);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
export const replaceBadge = (
|
||||
config: LovelaceConfig,
|
||||
path: LovelaceCardPath,
|
||||
cardConfig: LovelaceBadgeConfig
|
||||
): LovelaceConfig => {
|
||||
const { cardIndex } = parseLovelaceCardPath(path);
|
||||
const containerPath = getLovelaceContainerPath(path);
|
||||
|
||||
const badges = findLovelaceItems("badges", config, containerPath);
|
||||
|
||||
const newBadges = (badges ?? []).map((origConf, ind) =>
|
||||
ind === cardIndex ? cardConfig : origConf
|
||||
);
|
||||
|
||||
const newConfig = updateLovelaceItems(
|
||||
"badges",
|
||||
config,
|
||||
containerPath,
|
||||
newBadges
|
||||
);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
export const deleteBadge = (
|
||||
config: LovelaceConfig,
|
||||
path: LovelaceCardPath
|
||||
): LovelaceConfig => {
|
||||
const { cardIndex } = parseLovelaceCardPath(path);
|
||||
const containerPath = getLovelaceContainerPath(path);
|
||||
|
||||
const badges = findLovelaceItems("badges", config, containerPath);
|
||||
|
||||
const newBadges = (badges ?? []).filter(
|
||||
(_origConf, ind) => ind !== cardIndex
|
||||
);
|
||||
|
||||
const newConfig = updateLovelaceItems(
|
||||
"badges",
|
||||
config,
|
||||
containerPath,
|
||||
newBadges
|
||||
);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
export const insertBadge = (
|
||||
config: LovelaceConfig,
|
||||
path: LovelaceCardPath,
|
||||
badgeConfig: LovelaceBadgeConfig
|
||||
) => {
|
||||
const { cardIndex } = parseLovelaceCardPath(path);
|
||||
const containerPath = getLovelaceContainerPath(path);
|
||||
|
||||
const badges = findLovelaceItems("badges", config, containerPath);
|
||||
|
||||
const newBadges = badges
|
||||
? [...badges.slice(0, cardIndex), badgeConfig, ...badges.slice(cardIndex)]
|
||||
: [badgeConfig];
|
||||
|
||||
const newConfig = updateLovelaceItems(
|
||||
"badges",
|
||||
config,
|
||||
containerPath,
|
||||
newBadges
|
||||
);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
export const moveBadge = (
|
||||
config: LovelaceConfig,
|
||||
fromPath: LovelaceCardPath,
|
||||
toPath: LovelaceCardPath
|
||||
): LovelaceConfig => {
|
||||
const { cardIndex: fromCardIndex } = parseLovelaceCardPath(fromPath);
|
||||
const fromContainerPath = getLovelaceContainerPath(fromPath);
|
||||
const badges = findLovelaceItems("badges", config, fromContainerPath);
|
||||
const badge = badges![fromCardIndex];
|
||||
|
||||
let newConfig = deleteBadge(config, fromPath);
|
||||
newConfig = insertBadge(newConfig, toPath, badge);
|
||||
|
||||
return newConfig;
|
||||
};
|
||||
|
26
src/panels/lovelace/editor/get-badge-stub-config.ts
Normal file
26
src/panels/lovelace/editor/get-badge-stub-config.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { getBadgeElementClass } from "../create-element/create-badge-element";
|
||||
|
||||
export const getBadgeStubConfig = async (
|
||||
hass: HomeAssistant,
|
||||
type: string,
|
||||
entities: string[],
|
||||
entitiesFallback: string[]
|
||||
): Promise<LovelaceCardConfig> => {
|
||||
let badgeConfig: LovelaceCardConfig = { type };
|
||||
|
||||
const elClass = await getBadgeElementClass(type);
|
||||
|
||||
if (elClass && elClass.getStubConfig) {
|
||||
const classStubConfig = await elClass.getStubConfig(
|
||||
hass,
|
||||
entities,
|
||||
entitiesFallback
|
||||
);
|
||||
|
||||
badgeConfig = { ...badgeConfig, ...classStubConfig };
|
||||
}
|
||||
|
||||
return badgeConfig;
|
||||
};
|
@ -1,4 +1,5 @@
|
||||
import {
|
||||
getCustomBadgeEntry,
|
||||
getCustomCardEntry,
|
||||
isCustomType,
|
||||
stripCustomPrefix,
|
||||
@ -14,5 +15,16 @@ export const getCardDocumentationURL = (
|
||||
return getCustomCardEntry(stripCustomPrefix(type))?.documentationURL;
|
||||
}
|
||||
|
||||
return `${documentationUrl(hass, "/lovelace/")}${type}`;
|
||||
return `${documentationUrl(hass, "/dashboards/")}${type}`;
|
||||
};
|
||||
|
||||
export const getBadgeDocumentationURL = (
|
||||
hass: HomeAssistant,
|
||||
type: string
|
||||
): string | undefined => {
|
||||
if (isCustomType(type)) {
|
||||
return getCustomBadgeEntry(stripCustomPrefix(type))?.documentationURL;
|
||||
}
|
||||
|
||||
return `${documentationUrl(hass, "/dashboards/badges")}`;
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import {
|
||||
LovelaceSectionRawConfig,
|
||||
@ -80,35 +81,6 @@ export const findLovelaceContainer: FindLovelaceContainer = (
|
||||
return section;
|
||||
};
|
||||
|
||||
export const findLovelaceCards = (
|
||||
config: LovelaceConfig,
|
||||
path: LovelaceContainerPath
|
||||
): LovelaceCardConfig[] | undefined => {
|
||||
const { viewIndex, sectionIndex } = parseLovelaceContainerPath(path);
|
||||
|
||||
const view = config.views[viewIndex];
|
||||
|
||||
if (!view) {
|
||||
throw new Error("View does not exist");
|
||||
}
|
||||
if (isStrategyView(view)) {
|
||||
throw new Error("Can not find cards in a strategy view");
|
||||
}
|
||||
if (sectionIndex === undefined) {
|
||||
return view.cards;
|
||||
}
|
||||
|
||||
const section = view.sections?.[sectionIndex];
|
||||
|
||||
if (!section) {
|
||||
throw new Error("Section does not exist");
|
||||
}
|
||||
if (isStrategySection(section)) {
|
||||
throw new Error("Can not find cards in a strategy section");
|
||||
}
|
||||
return section.cards;
|
||||
};
|
||||
|
||||
export const updateLovelaceContainer = (
|
||||
config: LovelaceConfig,
|
||||
path: LovelaceContainerPath,
|
||||
@ -153,10 +125,16 @@ export const updateLovelaceContainer = (
|
||||
};
|
||||
};
|
||||
|
||||
export const updateLovelaceCards = (
|
||||
type LovelaceItemKeys = {
|
||||
cards: LovelaceCardConfig[];
|
||||
badges: LovelaceBadgeConfig[];
|
||||
};
|
||||
|
||||
export const updateLovelaceItems = <T extends keyof LovelaceItemKeys>(
|
||||
key: T,
|
||||
config: LovelaceConfig,
|
||||
path: LovelaceContainerPath,
|
||||
cards: LovelaceCardConfig[]
|
||||
items: LovelaceItemKeys[T]
|
||||
): LovelaceConfig => {
|
||||
const { viewIndex, sectionIndex } = parseLovelaceContainerPath(path);
|
||||
|
||||
@ -164,13 +142,13 @@ export const updateLovelaceCards = (
|
||||
const newViews = config.views.map((view, vIndex) => {
|
||||
if (vIndex !== viewIndex) return view;
|
||||
if (isStrategyView(view)) {
|
||||
throw new Error("Can not update cards in a strategy view");
|
||||
throw new Error(`Can not update ${key} in a strategy view`);
|
||||
}
|
||||
if (sectionIndex === undefined) {
|
||||
updated = true;
|
||||
return {
|
||||
...view,
|
||||
cards,
|
||||
[key]: items,
|
||||
};
|
||||
}
|
||||
|
||||
@ -181,12 +159,12 @@ export const updateLovelaceCards = (
|
||||
const newSections = view.sections.map((section, sIndex) => {
|
||||
if (sIndex !== sectionIndex) return section;
|
||||
if (isStrategySection(section)) {
|
||||
throw new Error("Can not update cards in a strategy section");
|
||||
throw new Error(`Can not update ${key} in a strategy section`);
|
||||
}
|
||||
updated = true;
|
||||
return {
|
||||
...section,
|
||||
cards,
|
||||
[key]: items,
|
||||
};
|
||||
});
|
||||
return {
|
||||
@ -196,10 +174,43 @@ export const updateLovelaceCards = (
|
||||
});
|
||||
|
||||
if (!updated) {
|
||||
throw new Error("Can not update cards in a non-existing view/section");
|
||||
throw new Error(`Can not update ${key} in a non-existing view/section`);
|
||||
}
|
||||
return {
|
||||
...config,
|
||||
views: newViews,
|
||||
};
|
||||
};
|
||||
|
||||
export const findLovelaceItems = <T extends keyof LovelaceItemKeys>(
|
||||
key: T,
|
||||
config: LovelaceConfig,
|
||||
path: LovelaceContainerPath
|
||||
): LovelaceItemKeys[T] | undefined => {
|
||||
const { viewIndex, sectionIndex } = parseLovelaceContainerPath(path);
|
||||
|
||||
const view = config.views[viewIndex];
|
||||
|
||||
if (!view) {
|
||||
throw new Error("View does not exist");
|
||||
}
|
||||
if (isStrategyView(view)) {
|
||||
throw new Error("Can not find cards in a strategy view");
|
||||
}
|
||||
if (sectionIndex === undefined) {
|
||||
return view[key] as LovelaceItemKeys[T] | undefined;
|
||||
}
|
||||
|
||||
const section = view.sections?.[sectionIndex];
|
||||
|
||||
if (!section) {
|
||||
throw new Error("Section does not exist");
|
||||
}
|
||||
if (isStrategySection(section)) {
|
||||
throw new Error("Can not find cards in a strategy section");
|
||||
}
|
||||
if (key === "cards") {
|
||||
return section[key as "cards"] as LovelaceItemKeys[T] | undefined;
|
||||
}
|
||||
throw new Error(`${key} is not supported in section`);
|
||||
};
|
||||
|
6
src/panels/lovelace/editor/structs/base-badge-struct.ts
Normal file
6
src/panels/lovelace/editor/structs/base-badge-struct.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { object, string, any } from "superstruct";
|
||||
|
||||
export const baseLovelaceBadgeConfig = object({
|
||||
type: string(),
|
||||
visibility: any(),
|
||||
});
|
@ -82,6 +82,16 @@ export interface LovelaceCardConstructor extends Constructor<LovelaceCard> {
|
||||
getConfigForm?: () => LovelaceConfigForm;
|
||||
}
|
||||
|
||||
export interface LovelaceBadgeConstructor extends Constructor<LovelaceBadge> {
|
||||
getStubConfig?: (
|
||||
hass: HomeAssistant,
|
||||
entities: string[],
|
||||
entitiesFallback: string[]
|
||||
) => LovelaceBadgeConfig;
|
||||
getConfigElement?: () => LovelaceBadgeEditor;
|
||||
getConfigForm?: () => LovelaceConfigForm;
|
||||
}
|
||||
|
||||
export interface LovelaceHeaderFooterConstructor
|
||||
extends Constructor<LovelaceHeaderFooter> {
|
||||
getStubConfig?: (
|
||||
@ -107,6 +117,10 @@ export interface LovelaceCardEditor extends LovelaceGenericElementEditor {
|
||||
setConfig(config: LovelaceCardConfig): void;
|
||||
}
|
||||
|
||||
export interface LovelaceBadgeEditor extends LovelaceGenericElementEditor {
|
||||
setConfig(config: LovelaceBadgeConfig): void;
|
||||
}
|
||||
|
||||
export interface LovelaceHeaderFooterEditor
|
||||
extends LovelaceGenericElementEditor {
|
||||
setConfig(config: LovelaceHeaderFooterConfig): void;
|
||||
|
@ -15,9 +15,11 @@ import "../../../components/ha-svg-icon";
|
||||
import type { LovelaceViewElement } from "../../../data/lovelace";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { HuiBadge } from "../badges/hui-badge";
|
||||
import "../badges/hui-view-badges";
|
||||
import { HuiCard } from "../cards/hui-card";
|
||||
import { computeCardSize } from "../common/compute-card-size";
|
||||
import type { Lovelace, LovelaceBadge } from "../types";
|
||||
import type { Lovelace } from "../types";
|
||||
|
||||
// Find column with < 5 size, else smallest column
|
||||
const getColumnIndex = (columnSizes: number[], size: number) => {
|
||||
@ -50,7 +52,7 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
@property({ attribute: false }) public cards: HuiCard[] = [];
|
||||
|
||||
@property({ attribute: false }) public badges: LovelaceBadge[] = [];
|
||||
@property({ attribute: false }) public badges: HuiBadge[] = [];
|
||||
|
||||
@state() private _columns?: number;
|
||||
|
||||
@ -78,9 +80,12 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this.badges.length > 0
|
||||
? html`<div class="badges">${this.badges}</div>`
|
||||
: ""}
|
||||
<hui-view-badges
|
||||
.hass=${this.hass}
|
||||
.badges=${this.badges}
|
||||
.lovelace=${this.lovelace}
|
||||
.viewIndex=${this.index}
|
||||
></hui-view-badges>
|
||||
<div
|
||||
id="columns"
|
||||
class=${this.lovelace?.editMode ? "edit-mode" : ""}
|
||||
@ -289,10 +294,10 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.badges {
|
||||
margin: 8px 16px;
|
||||
hui-view-badges {
|
||||
display: block;
|
||||
margin: 12px 8px 20px 8px;
|
||||
font-size: 85%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#columns {
|
||||
|
@ -17,11 +17,14 @@ import type { LovelaceViewElement } from "../../../data/lovelace";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { HuiBadge } from "../badges/hui-badge";
|
||||
import "../badges/hui-view-badges";
|
||||
import "../components/hui-badge-edit-mode";
|
||||
import { addSection, deleteSection, moveSection } from "../editor/config-util";
|
||||
import { findLovelaceContainer } from "../editor/lovelace-path";
|
||||
import { showEditSectionDialog } from "../editor/section-editor/show-edit-section-dialog";
|
||||
import { HuiSection } from "../sections/hui-section";
|
||||
import type { Lovelace, LovelaceBadge } from "../types";
|
||||
import type { Lovelace } from "../types";
|
||||
|
||||
@customElement("hui-sections-view")
|
||||
export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
@ -35,23 +38,25 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
@property({ attribute: false }) public sections: HuiSection[] = [];
|
||||
|
||||
@property({ attribute: false }) public badges: LovelaceBadge[] = [];
|
||||
@property({ attribute: false }) public badges: HuiBadge[] = [];
|
||||
|
||||
@state() private _config?: LovelaceViewConfig;
|
||||
|
||||
@state() private _sectionCount = 0;
|
||||
|
||||
@state() _dragging = false;
|
||||
|
||||
public setConfig(config: LovelaceViewConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _sectionConfigKeys = new WeakMap<HuiSection, string>();
|
||||
|
||||
private _getKey(sectionConfig: HuiSection) {
|
||||
if (!this._sectionConfigKeys.has(sectionConfig)) {
|
||||
this._sectionConfigKeys.set(sectionConfig, Math.random().toString());
|
||||
private _getSectionKey(section: HuiSection) {
|
||||
if (!this._sectionConfigKeys.has(section)) {
|
||||
this._sectionConfigKeys.set(section, Math.random().toString());
|
||||
}
|
||||
return this._sectionConfigKeys.get(sectionConfig)!;
|
||||
return this._sectionConfigKeys.get(section)!;
|
||||
}
|
||||
|
||||
private _computeSectionsCount() {
|
||||
@ -83,9 +88,12 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
const maxColumnsCount = this._config?.max_columns;
|
||||
|
||||
return html`
|
||||
${this.badges.length > 0
|
||||
? html`<div class="badges">${this.badges}</div>`
|
||||
: ""}
|
||||
<hui-view-badges
|
||||
.hass=${this.hass}
|
||||
.badges=${this.badges}
|
||||
.lovelace=${this.lovelace}
|
||||
.viewIndex=${this.index}
|
||||
></hui-view-badges>
|
||||
<ha-sortable
|
||||
.disabled=${!editMode}
|
||||
@item-moved=${this._sectionMoved}
|
||||
@ -103,7 +111,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
>
|
||||
${repeat(
|
||||
sections,
|
||||
(section) => this._getKey(section),
|
||||
(section) => this._getSectionKey(section),
|
||||
(section, idx) => {
|
||||
(section as any).itemPath = [idx];
|
||||
return html`
|
||||
@ -141,7 +149,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
${editMode
|
||||
? html`
|
||||
<button
|
||||
class="create"
|
||||
class="create-section"
|
||||
@click=${this._createSection}
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.section.create_section"
|
||||
@ -235,14 +243,6 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.badges {
|
||||
margin: 4px 0;
|
||||
padding: var(--row-gap) var(--column-gap);
|
||||
padding-bottom: 0;
|
||||
font-size: 85%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container > * {
|
||||
position: relative;
|
||||
max-width: var(--column-max-width);
|
||||
@ -313,7 +313,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.create {
|
||||
.create-section {
|
||||
margin-top: calc(66px + var(--row-gap));
|
||||
outline: none;
|
||||
background: none;
|
||||
@ -326,13 +326,19 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.create:focus {
|
||||
.create-section:focus {
|
||||
border: 2px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.sortable-ghost {
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
}
|
||||
|
||||
hui-view-badges {
|
||||
display: block;
|
||||
margin: 16px 8px;
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -5,27 +5,28 @@ import { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/entity/ha-state-label-badge";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { LovelaceViewElement } from "../../../data/lovelace";
|
||||
import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||
import {
|
||||
defaultBadgeConfig,
|
||||
LovelaceBadgeConfig,
|
||||
} from "../../../data/lovelace/config/badge";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import {
|
||||
LovelaceViewConfig,
|
||||
isStrategyView,
|
||||
LovelaceViewConfig,
|
||||
} from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import {
|
||||
createErrorBadgeConfig,
|
||||
createErrorBadgeElement,
|
||||
} from "../badges/hui-error-badge";
|
||||
import "../badges/hui-badge";
|
||||
import type { HuiBadge } from "../badges/hui-badge";
|
||||
import "../cards/hui-card";
|
||||
import type { HuiCard } from "../cards/hui-card";
|
||||
import { processConfigEntities } from "../common/process-config-entities";
|
||||
import { createBadgeElement } from "../create-element/create-badge-element";
|
||||
import { createViewElement } from "../create-element/create-view-element";
|
||||
import { showEditBadgeDialog } from "../editor/badge-editor/show-edit-badge-dialog";
|
||||
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
||||
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||
import { deleteCard } from "../editor/config-util";
|
||||
import { deleteBadge, deleteCard } from "../editor/config-util";
|
||||
import { confDeleteCard } from "../editor/delete-card";
|
||||
import { getBadgeStubConfig } from "../editor/get-badge-stub-config";
|
||||
import {
|
||||
LovelaceCardPath,
|
||||
parseLovelaceCardPath,
|
||||
@ -34,7 +35,7 @@ import { createErrorSectionConfig } from "../sections/hui-error-section";
|
||||
import "../sections/hui-section";
|
||||
import type { HuiSection } from "../sections/hui-section";
|
||||
import { generateLovelaceViewStrategy } from "../strategies/get-strategy";
|
||||
import type { Lovelace, LovelaceBadge } from "../types";
|
||||
import type { Lovelace } from "../types";
|
||||
import { DEFAULT_VIEW_LAYOUT, PANEL_VIEW_LAYOUT } from "./const";
|
||||
|
||||
declare global {
|
||||
@ -43,11 +44,17 @@ declare global {
|
||||
"ll-create-card": { suggested?: string[] } | undefined;
|
||||
"ll-edit-card": { path: LovelaceCardPath };
|
||||
"ll-delete-card": { path: LovelaceCardPath; confirm: boolean };
|
||||
"ll-create-badge": undefined;
|
||||
"ll-edit-badge": { path: LovelaceCardPath };
|
||||
"ll-delete-badge": { path: LovelaceCardPath };
|
||||
}
|
||||
interface HTMLElementEventMap {
|
||||
"ll-create-card": HASSDomEvent<HASSDomEvents["ll-create-card"]>;
|
||||
"ll-edit-card": HASSDomEvent<HASSDomEvents["ll-edit-card"]>;
|
||||
"ll-delete-card": HASSDomEvent<HASSDomEvents["ll-delete-card"]>;
|
||||
"ll-create-badge": HASSDomEvent<HASSDomEvents["ll-create-badge"]>;
|
||||
"ll-edit-badge": HASSDomEvent<HASSDomEvents["ll-edit-badge"]>;
|
||||
"ll-delete-badge": HASSDomEvent<HASSDomEvents["ll-delete-badge"]>;
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,7 +70,7 @@ export class HUIView extends ReactiveElement {
|
||||
|
||||
@state() private _cards: HuiCard[] = [];
|
||||
|
||||
@state() private _badges: LovelaceBadge[] = [];
|
||||
@state() private _badges: HuiBadge[] = [];
|
||||
|
||||
@state() private _sections: HuiSection[] = [];
|
||||
|
||||
@ -86,20 +93,16 @@ export class HUIView extends ReactiveElement {
|
||||
return element;
|
||||
}
|
||||
|
||||
public createBadgeElement(badgeConfig: LovelaceBadgeConfig) {
|
||||
const element = createBadgeElement(badgeConfig) as LovelaceBadge;
|
||||
try {
|
||||
element.hass = this.hass;
|
||||
} catch (e: any) {
|
||||
return createErrorBadgeElement(createErrorBadgeConfig(e.message));
|
||||
}
|
||||
element.addEventListener(
|
||||
"ll-badge-rebuild",
|
||||
() => {
|
||||
this._rebuildBadge(element, badgeConfig);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
public _createBadgeElement(badgeConfig: LovelaceBadgeConfig) {
|
||||
const element = document.createElement("hui-badge");
|
||||
element.hass = this.hass;
|
||||
element.preview = this.lovelace.editMode;
|
||||
element.config = badgeConfig;
|
||||
element.addEventListener("badge-updated", (ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
this._badges = [...this._badges];
|
||||
});
|
||||
element.load();
|
||||
return element;
|
||||
}
|
||||
|
||||
@ -169,11 +172,7 @@ export class HUIView extends ReactiveElement {
|
||||
// Config has not changed. Just props
|
||||
if (changedProperties.has("hass")) {
|
||||
this._badges.forEach((badge) => {
|
||||
try {
|
||||
badge.hass = this.hass;
|
||||
} catch (e: any) {
|
||||
this._rebuildBadge(badge, createErrorBadgeConfig(e.message));
|
||||
}
|
||||
badge.hass = this.hass;
|
||||
});
|
||||
|
||||
this._cards.forEach((element) => {
|
||||
@ -219,6 +218,9 @@ export class HUIView extends ReactiveElement {
|
||||
this._cards.forEach((element) => {
|
||||
element.preview = this.lovelace.editMode;
|
||||
});
|
||||
this._badges.forEach((element) => {
|
||||
element.preview = this.lovelace.editMode;
|
||||
});
|
||||
}
|
||||
if (changedProperties.has("_cards")) {
|
||||
this._layoutElement.cards = this._cards;
|
||||
@ -329,6 +331,34 @@ export class HUIView extends ReactiveElement {
|
||||
this.lovelace.saveConfig(newLovelace);
|
||||
}
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-create-badge", async () => {
|
||||
const defaultConfig = await getBadgeStubConfig(
|
||||
this.hass,
|
||||
"entity",
|
||||
Object.keys(this.hass.entities),
|
||||
[]
|
||||
);
|
||||
|
||||
showEditBadgeDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path: [this.index],
|
||||
badgeConfig: defaultConfig,
|
||||
});
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-edit-badge", (ev) => {
|
||||
const { cardIndex } = parseLovelaceCardPath(ev.detail.path);
|
||||
showEditBadgeDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path: [this.index],
|
||||
badgeIndex: cardIndex,
|
||||
});
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-delete-badge", (ev) => {
|
||||
const newLovelace = deleteBadge(this.lovelace!.config, ev.detail.path);
|
||||
this.lovelace.saveConfig(newLovelace);
|
||||
});
|
||||
}
|
||||
|
||||
private _createBadges(config: LovelaceViewConfig): void {
|
||||
@ -337,14 +367,10 @@ export class HUIView extends ReactiveElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const badges = processConfigEntities(config.badges as any);
|
||||
this._badges = badges.map((badge) => {
|
||||
const element = createBadgeElement(badge);
|
||||
try {
|
||||
element.hass = this.hass;
|
||||
} catch (e: any) {
|
||||
return createErrorBadgeElement(createErrorBadgeConfig(e.message));
|
||||
}
|
||||
this._badges = config.badges.map((badge) => {
|
||||
const badgeConfig =
|
||||
typeof badge === "string" ? defaultBadgeConfig(badge) : badge;
|
||||
const element = this._createBadgeElement(badgeConfig);
|
||||
return element;
|
||||
});
|
||||
}
|
||||
@ -374,27 +400,6 @@ export class HUIView extends ReactiveElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _rebuildBadge(
|
||||
badgeElToReplace: LovelaceBadge,
|
||||
config: LovelaceBadgeConfig
|
||||
): void {
|
||||
let newBadgeEl = this.createBadgeElement(config);
|
||||
try {
|
||||
newBadgeEl.hass = this.hass;
|
||||
} catch (e: any) {
|
||||
newBadgeEl = createErrorBadgeElement(createErrorBadgeConfig(e.message));
|
||||
}
|
||||
if (badgeElToReplace.parentElement) {
|
||||
badgeElToReplace.parentElement!.replaceChild(
|
||||
newBadgeEl,
|
||||
badgeElToReplace
|
||||
);
|
||||
}
|
||||
this._badges = this._badges!.map((curBadgeEl) =>
|
||||
curBadgeEl === badgeElToReplace ? newBadgeEl : curBadgeEl
|
||||
);
|
||||
}
|
||||
|
||||
private _rebuildSection(
|
||||
sectionElToReplace: HuiSection,
|
||||
config: LovelaceSectionConfig
|
||||
|
@ -5560,9 +5560,31 @@
|
||||
"tab_layout": "Layout",
|
||||
"visibility": {
|
||||
"explanation": "The card will be shown when ALL conditions below are fulfilled. If no conditions are set, the card will always be shown."
|
||||
},
|
||||
"layout": {
|
||||
"explanation": "Configure how the card will appear on the dashboard. This settings will override the default size and position of the card."
|
||||
}
|
||||
},
|
||||
"edit_badge": {
|
||||
"header": "Badge configuration",
|
||||
"typed_header": "{type} Badge configuration",
|
||||
"pick_badge": "Which badge would you like to add?",
|
||||
"pick_badge_title": "Which badge would you like to add to {name}",
|
||||
"toggle_editor": "[%key:ui::panel::lovelace::editor::edit_card::toggle_editor%]",
|
||||
"unsaved_changes": "[%key:ui::panel::lovelace::editor::edit_card::unsaved_changes%]",
|
||||
"confirm_cancel": "[%key:ui::panel::lovelace::editor::edit_card::confirm_cancel%]",
|
||||
"show_visual_editor": "[%key:ui::panel::lovelace::editor::edit_card::show_visual_editor%]",
|
||||
"show_code_editor": "[%key:ui::panel::lovelace::editor::edit_card::show_code_editor%]",
|
||||
"edit_ui": "[%key:ui::panel::config::automation::editor::edit_ui%]",
|
||||
"edit_yaml": "[%key:ui::panel::config::automation::editor::edit_yaml%]",
|
||||
"add": "Add badge",
|
||||
"edit": "[%key:ui::panel::lovelace::editor::edit_card::edit%]",
|
||||
"clear": "[%key:ui::panel::lovelace::editor::edit_card::clear%]",
|
||||
"delete": "[%key:ui::panel::lovelace::editor::edit_card::delete%]",
|
||||
"copy": "[%key:ui::panel::lovelace::editor::edit_card::copy%]",
|
||||
"cut": "[%key:ui::panel::lovelace::editor::edit_card::cut%]",
|
||||
"duplicate": "[%key:ui::panel::lovelace::editor::edit_card::duplicate%]",
|
||||
"tab_config": "[%key:ui::panel::lovelace::editor::edit_card::tab_config%]",
|
||||
"tab_visibility": "[%key:ui::panel::lovelace::editor::edit_card::tab_visibility%]",
|
||||
"visibility": {
|
||||
"explanation": "The badge will be shown when ALL conditions below are fulfilled. If no conditions are set, the badge will always be shown."
|
||||
}
|
||||
},
|
||||
"move_card": {
|
||||
@ -5987,7 +6009,6 @@
|
||||
"icon_tap_action": "Icon tap behavior",
|
||||
"actions": "Actions",
|
||||
"appearance": "Appearance",
|
||||
"default_color": "Default color (state)",
|
||||
"show_entity_picture": "Show entity picture",
|
||||
"vertical": "Vertical",
|
||||
"hide_state": "Hide state",
|
||||
@ -6012,6 +6033,23 @@
|
||||
"twice_daily": "Twice daily"
|
||||
}
|
||||
},
|
||||
"badge": {
|
||||
"entity": {
|
||||
"name": "Entity",
|
||||
"description": "The Entity badge gives you a quick overview of your entity.",
|
||||
"color": "Color",
|
||||
"actions": "Actions",
|
||||
"appearance": "Appearance",
|
||||
"show_entity_picture": "Show entity picture",
|
||||
"state_content": "State content",
|
||||
"display_type": "Display type",
|
||||
"display_type_options": {
|
||||
"minimal": "Minimal (icon only)",
|
||||
"standard": "Standard (icon and state)",
|
||||
"complete": "Complete (icon, name and state)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"name": "Features",
|
||||
"not_compatible": "Not compatible",
|
||||
|
Loading…
x
Reference in New Issue
Block a user