frontend/src/panels/lovelace/create-element/create-element-base.ts
2025-01-06 12:10:34 +01:00

365 lines
10 KiB
TypeScript

import { fireEvent } from "../../../common/dom/fire_event";
import type {
LovelaceSectionElement,
LovelaceViewElement,
} from "../../../data/lovelace";
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import {
isCustomType,
stripCustomPrefix,
} from "../../../data/lovelace_custom_cards";
import type { LovelaceCardFeatureConfig } from "../card-features/types";
import type { ErrorCardConfig } from "../cards/types";
import type { LovelaceElement, LovelaceElementConfig } from "../elements/types";
import type { LovelaceRow, LovelaceRowConfig } from "../entity-rows/types";
import type { LovelaceHeaderFooterConfig } from "../header-footer/types";
import type { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
import type {
LovelaceBadge,
LovelaceBadgeConstructor,
LovelaceCard,
LovelaceCardConstructor,
LovelaceCardFeature,
LovelaceCardFeatureConstructor,
LovelaceElementConstructor,
LovelaceHeaderFooter,
LovelaceHeaderFooterConstructor,
LovelaceHeadingBadge,
LovelaceHeadingBadgeConstructor,
LovelaceRowConstructor,
} from "../types";
const TIMEOUT = 2000;
interface CreateElementConfigTypes {
card: {
config: LovelaceCardConfig;
element: LovelaceCard;
constructor: LovelaceCardConstructor;
};
badge: {
config: LovelaceBadgeConfig;
element: LovelaceBadge;
constructor: LovelaceBadgeConstructor;
};
element: {
config: LovelaceElementConfig;
element: LovelaceElement;
constructor: LovelaceElementConstructor;
};
row: {
config: LovelaceRowConfig;
element: LovelaceRow;
constructor: LovelaceRowConstructor;
};
"header-footer": {
config: LovelaceHeaderFooterConfig;
element: LovelaceHeaderFooter;
constructor: LovelaceHeaderFooterConstructor;
};
view: {
config: LovelaceViewConfig;
element: LovelaceViewElement;
constructor: unknown;
};
"card-feature": {
config: LovelaceCardFeatureConfig;
element: LovelaceCardFeature;
constructor: LovelaceCardFeatureConstructor;
};
section: {
config: LovelaceSectionConfig;
element: LovelaceSectionElement;
constructor: unknown;
};
"heading-badge": {
config: LovelaceHeadingBadgeConfig;
element: LovelaceHeadingBadge;
constructor: LovelaceHeadingBadgeConstructor;
};
}
export const createErrorCardElement = (config: ErrorCardConfig) => {
const el = document.createElement("hui-error-card");
if (customElements.get("hui-error-card")) {
el.setConfig(config);
} else {
import("../cards/hui-error-card");
customElements.whenDefined("hui-error-card").then(() => {
customElements.upgrade(el);
el.setConfig(config);
});
}
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 createErrorHeadingBadgeElement = (config: ErrorCardConfig) => {
const el = document.createElement("hui-error-heading-badge");
if (customElements.get("hui-error-heading-badge")) {
el.setConfig(config);
} else {
import("../heading-badges/hui-error-heading-badge");
customElements.whenDefined("hui-error-heading-badge").then(() => {
customElements.upgrade(el);
el.setConfig(config);
});
}
return el;
};
export const createErrorCardConfig = (error, origConfig) => ({
type: "error",
error,
origConfig,
});
export const createErrorBadgeConfig = (error, origConfig) => ({
type: "error",
error,
origConfig,
});
export const createErrorHeadingBadgeConfig = (error, origConfig) => ({
type: "error",
error,
origConfig,
});
const _createElement = <T extends keyof CreateElementConfigTypes>(
tag: string,
config: CreateElementConfigTypes[T]["config"]
): CreateElementConfigTypes[T]["element"] => {
const element = document.createElement(
tag
) as CreateElementConfigTypes[T]["element"];
// @ts-ignore
element.setConfig(config);
return element;
};
const _createErrorElement = <T extends keyof CreateElementConfigTypes>(
tagSuffix: T,
error: string,
config: CreateElementConfigTypes[T]["config"]
): CreateElementConfigTypes[T]["element"] => {
if (tagSuffix === "badge") {
return createErrorBadgeElement(createErrorBadgeConfig(error, config));
}
if (tagSuffix === "heading-badge") {
return createErrorHeadingBadgeElement(
createErrorHeadingBadgeConfig(error, config)
);
}
return createErrorCardElement(createErrorCardConfig(error, config));
};
const _customCreate = <T extends keyof CreateElementConfigTypes>(
tagSuffix: T,
tag: string,
config: CreateElementConfigTypes[T]["config"]
) => {
if (customElements.get(tag)) {
return _createElement(tag, config);
}
const element = _createErrorElement(
tagSuffix,
`Custom element doesn't exist: ${tag}.`,
config
);
// Custom elements are required to have a - in the name
if (!tag.includes("-")) {
return element;
}
element.style.display = "None";
const timer = window.setTimeout(() => {
element.style.display = "";
}, TIMEOUT);
customElements.whenDefined(tag).then(() => {
clearTimeout(timer);
fireEvent(element, "ll-rebuild");
});
return element;
};
const _lazyCreate = <T extends keyof CreateElementConfigTypes>(
tag: string,
config: CreateElementConfigTypes[T]["config"]
) => {
if (customElements.get(tag)) {
return _createElement(tag, config);
}
const element = document.createElement(
tag
) as CreateElementConfigTypes[T]["element"];
customElements.whenDefined(tag).then(() => {
try {
customElements.upgrade(element);
fireEvent(element, "ll-upgrade");
// @ts-ignore
element.setConfig(config);
} catch (err: any) {
// We let it rebuild and the error will be handled by _createElement
fireEvent(element, "ll-rebuild");
}
});
return element;
};
const _getCustomTag = (type: string) =>
isCustomType(type) ? stripCustomPrefix(type) : undefined;
export const createLovelaceElement = <T extends keyof CreateElementConfigTypes>(
tagSuffix: T,
config: CreateElementConfigTypes[T]["config"],
alwaysLoadTypes?: Set<string>,
lazyLoadTypes?: { [domain: string]: () => Promise<unknown> },
// Allow looking at "entity" in config and mapping that to a type
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"] => {
try {
return tryCreateLovelaceElement(
tagSuffix,
config,
alwaysLoadTypes,
lazyLoadTypes,
domainTypes,
defaultType
);
} catch (err: any) {
// eslint-disable-next-line
console.error(tagSuffix, config.type, err);
return _createErrorElement(tagSuffix, err.message, config);
}
};
export const tryCreateLovelaceElement = <
T extends keyof CreateElementConfigTypes,
>(
tagSuffix: T,
config: CreateElementConfigTypes[T]["config"],
alwaysLoadTypes?: Set<string>,
lazyLoadTypes?: { [domain: string]: () => Promise<unknown> },
// Allow looking at "entity" in config and mapping that to a type
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"] => {
if (!config || typeof config !== "object") {
throw new Error("Config is not an object");
}
if (
!config.type &&
!defaultType &&
// If domain types is given, we can derive a type from "entity"
(!domainTypes || !("entity" in config))
) {
throw new Error("No card type configured");
}
const customTag = config.type ? _getCustomTag(config.type) : undefined;
if (customTag) {
return _customCreate(tagSuffix, customTag, config);
}
let type: string | undefined;
// config.type has priority over config.entity, but defaultType has not.
// @ts-ignore
if (domainTypes && !config.type && config.entity) {
// @ts-ignore
const domain = config.entity.split(".", 1)[0];
type = `${domainTypes![domain] || domainTypes!._domain_not_found}-entity`;
} else {
type = config.type || defaultType;
}
if (type === undefined) {
throw new Error("No type specified");
}
const tag = `hui-${type}-${tagSuffix}`;
if (lazyLoadTypes && type in lazyLoadTypes) {
lazyLoadTypes[type]();
return _lazyCreate(tag, config);
}
if (alwaysLoadTypes && alwaysLoadTypes.has(type)) {
return _createElement(tag, config);
}
throw new Error(`Unknown type encountered: ${type}`);
};
export const getLovelaceElementClass = async <
T extends keyof CreateElementConfigTypes,
>(
type: string,
tagSuffix: T,
alwaysLoadTypes?: Set<string>,
lazyLoadTypes?: { [domain: string]: () => Promise<unknown> }
): Promise<CreateElementConfigTypes[T]["constructor"]> => {
const customTag = _getCustomTag(type);
if (customTag) {
const customCls = customElements.get(customTag);
if (customCls) {
return customCls;
}
// Custom elements are required to have a - in the name
if (!customTag.includes("-")) {
throw new Error(`Custom element not found: ${customTag}`);
}
return new Promise((resolve, reject) => {
// We will give custom components up to TIMEOUT seconds to get defined
setTimeout(
() => reject(new Error(`Custom element not found: ${customTag}`)),
TIMEOUT
);
customElements
.whenDefined(customTag)
.then(() => resolve(customElements.get(customTag)));
});
}
const tag = `hui-${type}-${tagSuffix}`;
const cls = customElements.get(tag);
if (alwaysLoadTypes && alwaysLoadTypes.has(type)) {
return cls;
}
if (lazyLoadTypes && type in lazyLoadTypes) {
return cls || lazyLoadTypes[type]().then(() => customElements.get(tag));
}
throw new Error(`Unknown type: ${type}`);
};