mirror of
https://github.com/home-assistant/frontend.git
synced 2025-06-21 17:46:34 +00:00
365 lines
10 KiB
TypeScript
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}`);
|
|
};
|