Improve custom panel support (#1236)
* Add custom panel * Lint * Add reference to docs * Use panel.config
This commit is contained in:
parent
1a3966e55f
commit
c3d67133c2
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -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>"
|
||||
],
|
||||
];
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
|
@ -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 }
|
||||
);
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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.');
|
||||
}
|
|
@ -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];
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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 }]
|
||||
];
|
||||
|
|
Loading…
Reference in New Issue