From 702c17d658672a649329084cd7c781fba24d663f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 23 Mar 2019 11:41:36 -0700 Subject: [PATCH] Convert custom panel to typescript (#2991) * Convert custom panel to typescript * Address comments --- src/data/panel_custom.ts | 8 ++ src/entrypoints/custom-panel.js | 80 -------------- src/entrypoints/custom-panel.ts | 97 ++++++++++++++++ ...{ha-panel-custom.js => ha-panel-custom.ts} | 104 +++++++++--------- src/types.ts | 3 + ...ment.js => create-custom-panel-element.ts} | 4 +- ...d-custom-panel.js => load-custom-panel.ts} | 4 +- ...ties.js => set-custom-panel-properties.ts} | 4 +- webpack.config.js | 4 +- 9 files changed, 168 insertions(+), 140 deletions(-) create mode 100644 src/data/panel_custom.ts delete mode 100644 src/entrypoints/custom-panel.js create mode 100644 src/entrypoints/custom-panel.ts rename src/panels/custom/{ha-panel-custom.js => ha-panel-custom.ts} (52%) rename src/util/custom-panel/{create-custom-panel-element.js => create-custom-panel-element.ts} (78%) rename src/util/custom-panel/{load-custom-panel.js => load-custom-panel.ts} (93%) rename src/util/custom-panel/{set-custom-panel-properties.js => set-custom-panel-properties.ts} (71%) diff --git a/src/data/panel_custom.ts b/src/data/panel_custom.ts new file mode 100644 index 0000000000..7f8ad4a29e --- /dev/null +++ b/src/data/panel_custom.ts @@ -0,0 +1,8 @@ +export interface CustomPanelConfig { + name: string; + embed_iframe: boolean; + trust_external: boolean; + js_url?: string; + module_url?: string; + html_url?: string; +} diff --git a/src/entrypoints/custom-panel.js b/src/entrypoints/custom-panel.js deleted file mode 100644 index 1d064943ce..0000000000 --- a/src/entrypoints/custom-panel.js +++ /dev/null @@ -1,80 +0,0 @@ -import { loadJS } from "../common/dom/load_resource"; -import loadCustomPanel from "../util/custom-panel/load-custom-panel"; -import createCustomPanelElement from "../util/custom-panel/create-custom-panel-element"; -import setCustomPanelProperties from "../util/custom-panel/set-custom-panel-properties"; - -const webComponentsSupported = - "customElements" in window && - "import" in document.createElement("link") && - "content" in document.createElement("template"); - -let es5Loaded = null; - -window.loadES5Adapter = () => { - if (!es5Loaded) { - es5Loaded = Promise.all([ - loadJS(`${__STATIC_PATH__}custom-elements-es5-adapter.js`).catch(), - import(/* webpackChunkName: "compat" */ "./compatibility"), - ]); - } - return es5Loaded; -}; - -let root = null; - -function setProperties(properties) { - if (root === null) return; - setCustomPanelProperties(root, properties); -} - -function initialize(panel, properties) { - const style = document.createElement("style"); - style.innerHTML = "body{margin:0}"; - document.head.appendChild(style); - - const config = panel.config._panel_custom; - let start = Promise.resolve(); - - if (!webComponentsSupported) { - start = start.then(() => loadJS("/static/webcomponents-bundle.js")); - } - - if (__BUILD__ === "es5") { - // Load ES5 adapter. Swallow errors as it raises errors on old browsers. - start = start.then(() => window.loadES5Adapter()); - } - - start - .then(() => loadCustomPanel(config)) - // If our element is using es5, let it finish loading that and define element - // This avoids elements getting upgraded after being added to the DOM - .then(() => es5Loaded || Promise.resolve()) - .then( - () => { - root = createCustomPanelElement(config); - - const forwardEvent = (ev) => - window.parent.customPanel.fire(ev.type, ev.detail); - root.addEventListener("hass-toggle-menu", forwardEvent); - window.addEventListener("location-changed", (ev) => - window.parent.customPanel.navigate( - window.location.pathname, - ev.detail ? ev.detail.replace : false - ) - ); - setProperties(Object.assign({ panel }, properties)); - document.body.appendChild(root); - }, - (err) => { - // eslint-disable-next-line - console.error(err, panel); - alert(`Unable to load the panel source: ${err}.`); - } - ); -} - -document.addEventListener( - "DOMContentLoaded", - () => window.parent.customPanel.registerIframe(initialize, setProperties), - { once: true } -); diff --git a/src/entrypoints/custom-panel.ts b/src/entrypoints/custom-panel.ts new file mode 100644 index 0000000000..f5c9c463e5 --- /dev/null +++ b/src/entrypoints/custom-panel.ts @@ -0,0 +1,97 @@ +import { loadJS } from "../common/dom/load_resource"; +import { loadCustomPanel } from "../util/custom-panel/load-custom-panel"; +import { createCustomPanelElement } from "../util/custom-panel/create-custom-panel-element"; +import { setCustomPanelProperties } from "../util/custom-panel/set-custom-panel-properties"; +import { fireEvent } from "../common/dom/fire_event"; +import { navigate } from "../common/navigate"; +import { PolymerElement } from "@polymer/polymer"; +import { Panel } from "../types"; +import { CustomPanelConfig } from "../data/panel_custom"; + +declare global { + interface Window { + loadES5Adapter: () => Promise; + } +} + +const webComponentsSupported = + "customElements" in window && + "import" in document.createElement("link") && + "content" in document.createElement("template"); + +let es5Loaded: Promise | undefined; + +window.loadES5Adapter = () => { + if (!es5Loaded) { + es5Loaded = Promise.all([ + loadJS(`${__STATIC_PATH__}custom-elements-es5-adapter.js`).catch(), + import(/* webpackChunkName: "compat" */ "./compatibility"), + ]); + } + return es5Loaded; +}; + +let panelEl: HTMLElement | PolymerElement | undefined; + +function setProperties(properties) { + if (!panelEl) { + return; + } + setCustomPanelProperties(panelEl, properties); +} + +function initialize(panel: Panel, properties: {}) { + const style = document.createElement("style"); + style.innerHTML = "body{margin:0}"; + document.head.appendChild(style); + + const config = panel.config!._panel_custom as CustomPanelConfig; + let start: Promise = Promise.resolve(); + + if (!webComponentsSupported) { + start = start.then(() => loadJS("/static/webcomponents-bundle.js")); + } + + if (__BUILD__ === "es5") { + // Load ES5 adapter. Swallow errors as it raises errors on old browsers. + start = start.then(() => window.loadES5Adapter()); + } + + start + .then(() => loadCustomPanel(config)) + // If our element is using es5, let it finish loading that and define element + // This avoids elements getting upgraded after being added to the DOM + .then(() => es5Loaded || Promise.resolve()) + .then( + () => { + panelEl = createCustomPanelElement(config); + + const forwardEvent = (ev) => { + if (window.parent.customPanel) { + fireEvent(window.parent.customPanel, ev.type, ev.detail); + } + }; + panelEl!.addEventListener("hass-toggle-menu", forwardEvent); + window.addEventListener("location-changed", (ev: any) => + navigate( + window.parent.customPanel, + window.location.pathname, + ev.detail ? ev.detail.replace : false + ) + ); + setProperties({ panel, ...properties }); + document.body.appendChild(panelEl!); + }, + (err) => { + // tslint:disable-next-line + console.error(err, panel); + alert(`Unable to load the panel source: ${err}.`); + } + ); +} + +document.addEventListener( + "DOMContentLoaded", + () => window.parent.customPanel!.registerIframe(initialize, setProperties), + { once: true } +); diff --git a/src/panels/custom/ha-panel-custom.js b/src/panels/custom/ha-panel-custom.ts similarity index 52% rename from src/panels/custom/ha-panel-custom.js rename to src/panels/custom/ha-panel-custom.ts index 7571d697c5..55bb613ad6 100644 --- a/src/panels/custom/ha-panel-custom.js +++ b/src/panels/custom/ha-panel-custom.ts @@ -1,50 +1,69 @@ -import { PolymerElement } from "@polymer/polymer/polymer-element"; +import { property, PropertyValues, UpdatingElement } from "lit-element"; +import { loadCustomPanel } from "../../util/custom-panel/load-custom-panel"; +import { createCustomPanelElement } from "../../util/custom-panel/create-custom-panel-element"; +import { setCustomPanelProperties } from "../../util/custom-panel/set-custom-panel-properties"; +import { HomeAssistant, Route, Panel } from "../../types"; +import { CustomPanelConfig } from "../../data/panel_custom"; -import EventsMixin from "../../mixins/events-mixin"; -import NavigateMixin from "../../mixins/navigate-mixin"; -import loadCustomPanel from "../../util/custom-panel/load-custom-panel"; -import createCustomPanelElement from "../../util/custom-panel/create-custom-panel-element"; -import setCustomPanelProperties from "../../util/custom-panel/set-custom-panel-properties"; +declare global { + interface Window { + customPanel: HaPanelCustom | undefined; + } +} -/* - * Mixins are used by ifram to communicate with main frontend. - * @appliesMixin EventsMixin - * @appliesMixin NavigateMixin - */ -class HaPanelCustom extends NavigateMixin(EventsMixin(PolymerElement)) { - static get properties() { - return { - hass: Object, - narrow: Boolean, - route: Object, - panel: { - type: Object, - observer: "_panelChanged", - }, - }; +export class HaPanelCustom extends UpdatingElement { + @property() public hass!: HomeAssistant; + @property() public narrow!: boolean; + @property() public route!: Route; + @property() public panel!: Panel; + private _setProperties?: (props: {}) => void | undefined; + + public registerIframe(initialize, setProperties) { + initialize(this.panel, { + hass: this.hass, + narrow: this.narrow, + route: this.route, + }); + this._setProperties = setProperties; } - static get observers() { - return ["_dataChanged(hass, narrow, route)"]; + public disconnectedCallback() { + super.disconnectedCallback(); + this._cleanupPanel(); } - constructor() { - super(); - this._setProperties = null; + protected updated(changedProps: PropertyValues) { + if (changedProps.has("panel")) { + // Clean up old things if we had a panel + if (changedProps.get("panel")) { + this._cleanupPanel(); + } + this._createPanel(this.panel); + return; + } + if (!this._setProperties) { + return; + } + const props = {}; + for (const key of changedProps.keys()) { + props[key] = this[key]; + } + this._setProperties(props); } - _panelChanged(panel) { - // Clean up + private _cleanupPanel() { delete window.customPanel; - this._setProperties = null; + this._setProperties = undefined; while (this.lastChild) { this.removeChild(this.lastChild); } + } - const config = panel.config._panel_custom; + private _createPanel(panel: Panel) { + const config = panel.config!._panel_custom as CustomPanelConfig; const tempA = document.createElement("a"); - tempA.href = config.html_url || config.js_url || config.module_url; + tempA.href = config.html_url || config.js_url || config.module_url || ""; if ( !config.trust_external && @@ -96,32 +115,13 @@ It will have access to all data in Home Assistant. `.trim(); - const iframeDoc = this.querySelector("iframe").contentWindow.document; + const iframeDoc = this.querySelector("iframe")!.contentWindow!.document; iframeDoc.open(); iframeDoc.write( `` ); iframeDoc.close(); } - - disconnectedCallback() { - super.disconnectedCallback(); - delete window.customPanel; - } - - _dataChanged(hass, narrow, route) { - if (!this._setProperties) return; - this._setProperties({ hass, narrow, route }); - } - - registerIframe(initialize, setProperties) { - initialize(this.panel, { - hass: this.hass, - narrow: this.narrow, - route: this.route, - }); - this._setProperties = setProperties; - } } customElements.define("ha-panel-custom", HaPanelCustom); diff --git a/src/types.ts b/src/types.ts index 099fe66574..33665a4594 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,10 +15,13 @@ declare global { var __DEMO__: boolean; var __BUILD__: "latest" | "es5"; var __VERSION__: string; + var __STATIC_PATH__: string; } declare global { interface Window { + // Custom panel entry point url + customPanelJS: string; ShadyCSS: { nativeCss: boolean; nativeShadow: boolean; diff --git a/src/util/custom-panel/create-custom-panel-element.js b/src/util/custom-panel/create-custom-panel-element.ts similarity index 78% rename from src/util/custom-panel/create-custom-panel-element.js rename to src/util/custom-panel/create-custom-panel-element.ts index 4969c179de..d4a23699b4 100644 --- a/src/util/custom-panel/create-custom-panel-element.js +++ b/src/util/custom-panel/create-custom-panel-element.ts @@ -1,8 +1,8 @@ -export default function createCustomPanelElement(panelConfig) { +export const createCustomPanelElement = (panelConfig) => { // Legacy support. Custom panels used to have to define element ha-panel-{name} const tagName = "html_url" in panelConfig ? `ha-panel-${panelConfig.name}` : panelConfig.name; return document.createElement(tagName); -} +}; diff --git a/src/util/custom-panel/load-custom-panel.js b/src/util/custom-panel/load-custom-panel.ts similarity index 93% rename from src/util/custom-panel/load-custom-panel.js rename to src/util/custom-panel/load-custom-panel.ts index 54614810aa..08b399d591 100644 --- a/src/util/custom-panel/load-custom-panel.js +++ b/src/util/custom-panel/load-custom-panel.ts @@ -3,7 +3,7 @@ import { loadJS, loadModule } from "../../common/dom/load_resource"; // Make sure we only import every JS-based panel once (HTML import has this built-in) const JS_CACHE = {}; -export default function loadCustomPanel(panelConfig) { +export const loadCustomPanel = (panelConfig): Promise => { if (panelConfig.html_url) { const toLoad = [ import(/* webpackChunkName: "import-href-polyfill" */ "../../resources/html-import/import-href"), @@ -29,4 +29,4 @@ export default function loadCustomPanel(panelConfig) { return loadModule(panelConfig.module_url); } return Promise.reject("No valid url found in panel config."); -} +}; diff --git a/src/util/custom-panel/set-custom-panel-properties.js b/src/util/custom-panel/set-custom-panel-properties.ts similarity index 71% rename from src/util/custom-panel/set-custom-panel-properties.js rename to src/util/custom-panel/set-custom-panel-properties.ts index f54996cd5d..3aa8271ef8 100644 --- a/src/util/custom-panel/set-custom-panel-properties.js +++ b/src/util/custom-panel/set-custom-panel-properties.ts @@ -1,4 +1,4 @@ -export default function setCustomPanelProperties(root, properties) { +export const setCustomPanelProperties = (root, properties) => { if ("setProperties" in root) { root.setProperties(properties); } else { @@ -6,4 +6,4 @@ export default function setCustomPanelProperties(root, properties) { root[key] = properties[key]; }); } -} +}; diff --git a/webpack.config.js b/webpack.config.js index e723bd58d7..7e61e7f290 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -44,7 +44,7 @@ function createConfig(isProdBuild, latestBuild) { onboarding: "./src/entrypoints/onboarding.ts", core: "./src/entrypoints/core.ts", compatibility: "./src/entrypoints/compatibility.js", - "custom-panel": "./src/entrypoints/custom-panel.js", + "custom-panel": "./src/entrypoints/custom-panel.ts", "hass-icons": "./src/entrypoints/hass-icons.js", }; @@ -156,7 +156,7 @@ function createConfig(isProdBuild, latestBuild) { include: [ /core.ts$/, /app.js$/, - /custom-panel.js$/, + /custom-panel.ts$/, /hass-icons.js$/, /\.chunk\.js$/, ],