Convert custom panel to typescript (#2991)

* Convert custom panel to typescript

* Address comments
This commit is contained in:
Paulus Schoutsen 2019-03-23 11:41:36 -07:00 committed by GitHub
parent e2a9cf0d3c
commit 702c17d658
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 168 additions and 140 deletions

8
src/data/panel_custom.ts Normal file
View File

@ -0,0 +1,8 @@
export interface CustomPanelConfig {
name: string;
embed_iframe: boolean;
trust_external: boolean;
js_url?: string;
module_url?: string;
html_url?: string;
}

View File

@ -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 }
);

View File

@ -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<unknown>;
}
}
const webComponentsSupported =
"customElements" in window &&
"import" in document.createElement("link") &&
"content" in document.createElement("template");
let es5Loaded: Promise<unknown> | 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<unknown> = 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 }
);

View File

@ -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.
</style>
<iframe></iframe>
`.trim();
const iframeDoc = this.querySelector("iframe").contentWindow.document;
const iframeDoc = this.querySelector("iframe")!.contentWindow!.document;
iframeDoc.open();
iframeDoc.write(
`<!doctype html><script src='${window.customPanelJS}'></script>`
);
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);

View File

@ -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;

View File

@ -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);
}
};

View File

@ -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<unknown> => {
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.");
}
};

View File

@ -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];
});
}
}
};

View File

@ -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$/,
],