Improve custom panel support (#1236)

* Add custom panel

* Lint

* Add reference to docs

* Use panel.config
This commit is contained in:
Paulus Schoutsen 2018-06-01 10:06:28 -04:00 committed by GitHub
parent 1a3966e55f
commit c3d67133c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 279 additions and 7 deletions

View File

@ -16,7 +16,7 @@
"__DEMO__": false,
"__BUILD__": false,
"__VERSION__": false,
"__ROOT__": false,
"__PUBLIC_PATH__": false,
"Polymer": true,
"webkitSpeechRecognition": false,
"ResizeObserver": false
@ -52,6 +52,7 @@
"import/no-unresolved": 0,
"import/extensions": [2, "ignorePackages"],
"object-curly-newline": 0,
"default-case": 0,
"react/jsx-no-bind": [2, { "ignoreRefs": true }],
"react/jsx-no-duplicate-props": 2,
"react/self-closing-comp": 2,

View File

@ -12,7 +12,7 @@ const buildReplaces = {
'/frontend_latest/authorize.js': 'authorize.js',
};
const es5Extra = "<script src='/frontend_es5/custom-elements-es5-adapter.js'></script>";
const es5Extra = "<script src='/static/custom-elements-es5-adapter.js'></script>";
async function buildAuth(es6) {
const targetPath = es6 ? config.output : config.output_es5;

View File

@ -30,7 +30,7 @@ function generateIndex(es6) {
const compatibilityPath = `/frontend_es5/compatibility-${md5(path.resolve(config.output_es5, 'compatibility.js'))}.js`;
const es5Extra = `
<script src='${compatibilityPath}'></script>
<script src='/frontend_es5/custom-elements-es5-adapter.js'></script>
<script src='/static/custom-elements-es5-adapter.js'></script>
`;
toReplace.push([

View File

@ -7,7 +7,7 @@ let index = fs.readFileSync('index.html', 'utf-8');
const toReplace = [
[
'<!--EXTRA_SCRIPTS-->',
"<script src='/frontend_es5/custom-elements-es5-adapter.js'></script>"
"<script src='/static/custom-elements-es5-adapter.js'></script>"
],
];

View File

@ -18,8 +18,6 @@ cp -r public/__init__.py $OUTPUT_DIR_ES5/
cp src/authorize.html $OUTPUT_DIR
# Manually copy over this file as we don't run the ES5 build
# The Hass.io panel depends on it.
cp node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js $OUTPUT_DIR_ES5
cp node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js.map $OUTPUT_DIR
./node_modules/.bin/webpack --watch --progress

View File

@ -0,0 +1,35 @@
// Load a resource and get a promise when loading done.
// From: https://davidwalsh.name/javascript-loader
function _load(tag, url) {
// This promise will be used by Promise.all to determine success or failure
return new Promise(function (resolve, reject) {
const element = document.createElement(tag);
let attr = 'src';
let parent = 'body';
// Important success and error for the promise
element.onload = () => resolve(url);
element.onerror = () => reject(url);
// Need to set different attributes depending on tag type
switch (tag) {
case 'script':
element.async = true;
break;
case 'link':
element.type = 'text/css';
element.rel = 'stylesheet';
attr = 'href';
parent = 'head';
}
// Inject into document to kick off loading
element[attr] = url;
document[parent].appendChild(element);
});
}
export const loadCSS = url => _load('link', url);
export const loadJS = url => _load('script', url);
export const loadImg = url => _load('img', url);

View File

@ -0,0 +1,71 @@
import { loadJS } from '../common/dom/load_resource.js';
import loadCustomPanel from '../util/custom-panel/load-custom-panel.js';
import createCustomPanelElement from '../util/custom-panel/create-custom-panel-element.js';
import setCustomPanelProperties from '../util/custom-panel/set-custom-panel-properties.js';
const webComponentsSupported = (
'customElements' in window &&
'import' in document.createElement('link') &&
'content' in document.createElement('template'));
let es5Loaded = null;
window.loadES5Adapter = () => {
if (!es5Loaded) {
es5Loaded = loadJS(`${__PUBLIC_PATH__}custom-elements-es5-adapter.js`).catch();
}
return es5Loaded;
};
let root = null;
function setProperties(properties) {
if (root === null) return;
setCustomPanelProperties(root, properties);
}
function initialize(panel, properties) {
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-open-menu', forwardEvent);
root.addEventListener('hass-close-menu', forwardEvent);
root.addEventListener(
'location-changed',
() => window.parent.customPanel.navigate(window.location.pathname)
);
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

@ -21,6 +21,10 @@ function ensureLoaded(panel) {
imported = import(/* webpackChunkName: "panel-config" */ '../panels/config/ha-panel-config.js');
break;
case 'custom':
imported = import(/* webpackChunkName: "panel-custom" */ '../panels/custom/ha-panel-custom.js');
break;
case 'dev-event':
imported = import(/* webpackChunkName: "panel-dev-event" */ '../panels/dev-event/ha-panel-dev-event.js');
break;

View File

@ -0,0 +1,123 @@
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import EventsMixin from '../../mixins/events-mixin.js';
import NavigateMixin from '../../mixins/navigate-mixin.js';
import loadCustomPanel from '../../util/custom-panel/load-custom-panel.js';
import createCustomPanelElement from '../../util/custom-panel/create-custom-panel-element.js';
import setCustomPanelProperties from '../../util/custom-panel/set-custom-panel-properties.js';
/*
* 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,
showMenu: Boolean,
route: Object,
panel: {
type: Object,
observer: '_panelChanged',
}
};
}
static get observers() {
return [
'_dataChanged(hass, narrow, showMenu, route)'
];
}
constructor() {
super();
this._setProperties = null;
}
_panelChanged(panel) {
// Clean up
delete window.customPanel;
this._setProperties = null;
while (this.lastChild) {
this.remove(this.lastChild);
}
const config = panel.config._panel_custom;
const tempA = document.createElement('a');
tempA.href = config.html_url || config.js_url;
if (!config.trust_external && !['localhost', '127.0.0.1', location.hostname].includes(tempA.hostname)) {
if (!confirm(`Do you trust the external panel "${config.name}" at "${tempA.href}"?
It will have access to all data in Home Assistant.
(Check docs for the panel_custom component to hide this message)`)) {
return;
}
}
if (!config.embed_iframe) {
loadCustomPanel(config)
.then(
() => {
const element = createCustomPanelElement(config);
this._setProperties = props => setCustomPanelProperties(element, props);
setCustomPanelProperties(element, {
panel,
hass: this.hass,
narrow: this.narrow,
showMenu: this.showMenu,
route: this.route,
});
this.appendChild(element);
},
() => {
alert(`Unable to load custom panel from ${tempA.href}`);
}
);
return;
}
window.customPanel = this;
this.innerHTML = `
<style>
iframe {
border: 0;
width: 100%;
height: 100%;
display: block;
}
</style>
<iframe></iframe>
`;
const iframeDoc = this.querySelector('iframe').contentWindow.document;
iframeDoc.open();
iframeDoc.write(`<script src='${__PUBLIC_PATH__}custom-panel.js'></script>`);
iframeDoc.close();
}
disconnectedCallback() {
super.disconnectedCallback();
delete window.customPanel;
}
_dataChanged(hass, narrow, showMenu, route) {
if (!this._setProperties) return;
this._setProperties({ hass, narrow, showMenu, route });
}
registerIframe(initialize, setProperties) {
initialize(this.panel, {
hass: this.hass,
narrow: this.narrow,
showMenu: this.showMenu,
route: this.route,
});
this._setProperties = setProperties;
}
}
customElements.define('ha-panel-custom', HaPanelCustom);

View File

@ -1,4 +1,5 @@
/* eslint-disable */
import './polyfill.js';
/**
@license
Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
@ -94,3 +95,6 @@ export const importHref = function (href, onload, onerror, optAsync) {
}
return link;
};
export const importHrefPromise = href =>
new Promise((resolve, reject) => importHref(href, resolve, reject));

View File

@ -0,0 +1,5 @@
export default function 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

@ -0,0 +1,20 @@
import { loadJS } from '../../common/dom/load_resource.js';
// Make sure we only import every JS-based panel once (HTML import has this built-in)
const JS_CACHE = {};
export default function loadCustomPanel(panelConfig) {
if ('html_url' in panelConfig) {
return Promise.all([
import('../legacy-support.js'),
import('../../resources/html-import/import-href.js'),
// eslint-disable-next-line
]).then(([{}, { importHrefPromise }]) => importHrefPromise(panelConfig.html_url));
} else if (panelConfig.js_url) {
if (!(panelConfig.js_url in JS_CACHE)) {
JS_CACHE[panelConfig.js_url] = loadJS(panelConfig.js_url);
}
return JS_CACHE[panelConfig.js_url];
}
return Promise.reject('No valid url found in panel config.');
}

View File

@ -0,0 +1,9 @@
export default function setCustomPanelProperties(root, properties) {
if ('setProperties' in root) {
root.setProperties(properties);
} else {
Object.keys(properties).forEach((key) => {
root[key] = properties[key];
});
}
}

View File

@ -18,6 +18,7 @@ function createConfig(isProdBuild, latestBuild) {
app: './src/entrypoints/app.js',
authorize: './src/entrypoints/authorize.js',
core: './src/entrypoints/core.js',
'custom-panel': './src/entrypoints/custom-panel.js',
};
const babelOptions = {
@ -42,6 +43,7 @@ function createConfig(isProdBuild, latestBuild) {
__DEV__: JSON.stringify(!isProdBuild),
__BUILD__: JSON.stringify(latestBuild ? 'latest' : 'es5'),
__VERSION__: JSON.stringify(VERSION),
__PUBLIC_PATH__: JSON.stringify(publicPath),
}),
new CopyWebpackPlugin(copyPluginOpts),
// Ignore moment.js locales
@ -64,9 +66,9 @@ function createConfig(isProdBuild, latestBuild) {
copyPluginOpts.push('node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js')
copyPluginOpts.push({ from: 'node_modules/leaflet/dist/leaflet.css', to: `images/leaflet/` });
copyPluginOpts.push({ from: 'node_modules/leaflet/dist/images', to: `images/leaflet/` });
copyPluginOpts.push('node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js');
entry['hass-icons'] = './src/entrypoints/hass-icons.js';
} else {
copyPluginOpts.push('node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js');
babelOptions.presets = [
['es2015', { modules: false }]
];