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 = ( 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 = ( 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 = ( 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 = ( 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 = ( tagSuffix: T, config: CreateElementConfigTypes[T]["config"], alwaysLoadTypes?: Set, lazyLoadTypes?: { [domain: string]: () => Promise }, // 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, lazyLoadTypes?: { [domain: string]: () => Promise }, // 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, lazyLoadTypes?: { [domain: string]: () => Promise } ): Promise => { 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}`); };