diff --git a/src/common/dom/fire_event.js b/src/common/dom/fire_event.js new file mode 100644 index 0000000000..4b345c0b75 --- /dev/null +++ b/src/common/dom/fire_event.js @@ -0,0 +1,57 @@ +// Polymer legacy event helpers used courtesy of the Polymer project. +// +// Copyright (c) 2017 The Polymer Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/** + * Dispatches a custom event with an optional detail value. + * + * @param {string} type Name of event type. + * @param {*=} detail Detail value containing event-specific + * payload. + * @param {{ bubbles: (boolean|undefined), + cancelable: (boolean|undefined), + composed: (boolean|undefined) }=} + * options Object specifying options. These may include: + * `bubbles` (boolean, defaults to `true`), + * `cancelable` (boolean, defaults to false), and + * `node` on which to fire the event (HTMLElement, defaults to `this`). + * @return {Event} The new event that was fired. + */ +export default function fire(node, type, detail, options) { + options = options || {}; + detail = (detail === null || detail === undefined) ? {} : detail; + const event = new Event(type, { + bubbles: options.bubbles === undefined ? true : options.bubbles, + cancelable: Boolean(options.cancelable), + composed: options.composed === undefined ? true : options.composed + }); + event.detail = detail; + node.dispatchEvent(event); + return event; +} diff --git a/src/common/util/debounce.js b/src/common/util/debounce.js new file mode 100644 index 0000000000..f14a1d672f --- /dev/null +++ b/src/common/util/debounce.js @@ -0,0 +1,20 @@ +// From: https://davidwalsh.name/javascript-debounce-function + +// Returns a function, that, as long as it continues to be invoked, will not +// be triggered. The function will be called after it stops being called for +// N milliseconds. If `immediate` is passed, trigger the function on the +// leading edge, instead of the trailing. +export default function debounce(func, wait, immediate) { + let timeout; + return function (...args) { + const context = this; + const later = () => { + timeout = null; + if (!immediate) func.apply(context, args); + }; + const callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) func.apply(context, args); + }; +} diff --git a/src/mixins/events-mixin.js b/src/mixins/events-mixin.js index 6140f901f4..7f208a6aa7 100644 --- a/src/mixins/events-mixin.js +++ b/src/mixins/events-mixin.js @@ -1,5 +1,7 @@ import { dedupingMixin } from '@polymer/polymer/lib/utils/mixin.js'; +import fireEvent from '../common/dom/fire_event.js'; + // Polymer legacy event helpers used courtesy of the Polymer project. // // Copyright (c) 2017 The Polymer Authors. All rights reserved. @@ -33,31 +35,22 @@ import { dedupingMixin } from '@polymer/polymer/lib/utils/mixin.js'; /* @polymerMixin */ export default dedupingMixin(superClass => class extends superClass { /** - * Dispatches a custom event with an optional detail value. - * - * @param {string} type Name of event type. - * @param {*=} detail Detail value containing event-specific - * payload. - * @param {{ bubbles: (boolean|undefined), - cancelable: (boolean|undefined), - composed: (boolean|undefined) }=} - * options Object specifying options. These may include: - * `bubbles` (boolean, defaults to `true`), - * `cancelable` (boolean, defaults to false), and - * `node` on which to fire the event (HTMLElement, defaults to `this`). - * @return {Event} The new event that was fired. - */ + * Dispatches a custom event with an optional detail value. + * + * @param {string} type Name of event type. + * @param {*=} detail Detail value containing event-specific + * payload. + * @param {{ bubbles: (boolean|undefined), + cancelable: (boolean|undefined), + composed: (boolean|undefined) }=} + * options Object specifying options. These may include: + * `bubbles` (boolean, defaults to `true`), + * `cancelable` (boolean, defaults to false), and + * `node` on which to fire the event (HTMLElement, defaults to `this`). + * @return {Event} The new event that was fired. + */ fire(type, detail, options) { options = options || {}; - detail = (detail === null || detail === undefined) ? {} : detail; - const event = new Event(type, { - bubbles: options.bubbles === undefined ? true : options.bubbles, - cancelable: Boolean(options.cancelable), - composed: options.composed === undefined ? true : options.composed - }); - event.detail = detail; - const node = options.node || this; - node.dispatchEvent(event); - return event; + return fireEvent(options.node || this, type, detail, options); } }); diff --git a/src/panels/lovelace/cards/hui-entity-filter-card.js b/src/panels/lovelace/cards/hui-entity-filter-card.js index b0f1ee5926..7a0c69353e 100644 --- a/src/panels/lovelace/cards/hui-entity-filter-card.js +++ b/src/panels/lovelace/cards/hui-entity-filter-card.js @@ -1,7 +1,8 @@ import { PolymerElement } from '@polymer/polymer/polymer-element.js'; import computeStateDomain from '../../../common/entity/compute_state_domain.js'; -import createCardElement from '../common/create-card-element'; +import createCardElement from '../common/create-card-element.js'; +import createErrorCardConfig from '../common/create-error-card-config.js'; class HuiEntitiesCard extends PolymerElement { static get properties() { @@ -17,12 +18,6 @@ class HuiEntitiesCard extends PolymerElement { }; } - constructor() { - super(); - this._whenDefined = {}; - this.elementNotDefinedCallback = this.elementNotDefinedCallback.bind(this); - } - getCardSize() { return this.lastChild ? this.lastChild.getCardSize() : 1; } @@ -63,23 +58,29 @@ class HuiEntitiesCard extends PolymerElement { if (this.lastChild) { this.removeChild(this.lastChild); } + let error; - let element; if (!config.filter || !Array.isArray(config.filter)) { error = 'Incorrect filter config.'; } else if (!config.card) { - config.card = { type: 'entities' }; + config = Object.assign({}, config, { + card: { type: 'entities' } + }); } else if (!config.card.type) { - config.card.type = 'entities'; + config = Object.assign({}, config, { + card: Object.assign({}, config.card, { type: 'entities' }) + }); } + let element; if (error) { - element = createCardElement(config, this.elementNotDefinedCallback, error); + element = createCardElement(createErrorCardConfig(error, config.card)); } else { - element = createCardElement(config.card, this.elementNotDefinedCallback, null); - element.config = this._computeCardConfig(config); + element = createCardElement(config.card); + element._filterRawConfig = config.card; + this._updateCardConfig(element); element.hass = this.hass; } this.appendChild(element); @@ -87,25 +88,17 @@ class HuiEntitiesCard extends PolymerElement { _hassChanged(hass) { const element = this.lastChild; - if (!element || element.tagName === 'HUI-ERROR-CARD') return; - + this._updateCardConfig(element); element.hass = hass; - element.config = this._computeCardConfig(this.config); } - _computeCardConfig(config) { - return Object.assign( + _updateCardConfig(element) { + if (!element || element.tagName === 'HUI-ERROR-CARD') return; + element.config = Object.assign( {}, - config.card, - { entities: this._getEntities(this.hass, config.filter) } + element._filterRawConfig, + { entities: this._getEntities(this.hass, this.config.filter) } ); } - - elementNotDefinedCallback(tag) { - if (!(tag in this._whenDefined)) { - this._whenDefined[tag] = customElements.whenDefined(tag) - .then(() => this._configChanged(this.config)); - } - } } customElements.define('hui-entity-filter-card', HuiEntitiesCard); diff --git a/src/panels/lovelace/cards/hui-error-card.js b/src/panels/lovelace/cards/hui-error-card.js index 60dfecac56..6a0f8b934e 100644 --- a/src/panels/lovelace/cards/hui-error-card.js +++ b/src/panels/lovelace/cards/hui-error-card.js @@ -12,15 +12,14 @@ class HuiErrorCard extends PolymerElement { padding: 8px; } - [[error]] -
[[_toStr(config)]]
+ [[config.error]] +
[[_toStr(config.origConfig)]]
`; } static get properties() { return { config: Object, - error: String }; } diff --git a/src/panels/lovelace/cards/hui-picture-elements-card.js b/src/panels/lovelace/cards/hui-picture-elements-card.js index 7d8690992a..daddb09712 100644 --- a/src/panels/lovelace/cards/hui-picture-elements-card.js +++ b/src/panels/lovelace/cards/hui-picture-elements-card.js @@ -27,13 +27,12 @@ class HuiPictureElementsCard extends LocalizeMixin(EventsMixin(PolymerElement)) #root { position: relative; overflow: hidden; - line-height: 0; } #root img { + display: block; width: 100%; } .element { - line-height: initial; white-space: nowrap; position: absolute; transform: translate(-50%, -50%); diff --git a/src/panels/lovelace/common/create-card-element.js b/src/panels/lovelace/common/create-card-element.js index c2e57258a8..e5501046a6 100644 --- a/src/panels/lovelace/common/create-card-element.js +++ b/src/panels/lovelace/common/create-card-element.js @@ -1,3 +1,5 @@ +import fireEvent from '../../../common/dom/fire_event.js'; + import '../cards/hui-camera-preview-card.js'; import '../cards/hui-entities-card.js'; import '../cards/hui-entity-filter-card.js'; @@ -13,11 +15,14 @@ import '../cards/hui-plant-status-card.js'; import '../cards/hui-weather-forecast-card'; import '../cards/hui-error-card.js'; +import createErrorCardConfig from './create-error-card-config.js'; + const CARD_TYPES = [ 'camera-preview', 'entities', 'entity-filter', 'entity-picture', + 'error', 'glance', 'history-graph', 'iframe', @@ -31,33 +36,41 @@ const CARD_TYPES = [ const CUSTOM_TYPE_PREFIX = 'custom:'; -export default function -createCardElement(config, elementNotDefinedCallback = null, invalidConfig = null) { - let error = invalidConfig; - let tag; - - if (!error && config && typeof config === 'object' && config.type) { - if (CARD_TYPES.includes(config.type)) { - tag = `hui-${config.type}-card`; - } else if (config.type.startsWith(CUSTOM_TYPE_PREFIX)) { - tag = config.type.substr(CUSTOM_TYPE_PREFIX.length); - } - - if (tag) { - if (!customElements.get(tag)) { - error = 'Custom element doesn\'t exist.'; - if (elementNotDefinedCallback) elementNotDefinedCallback(tag); - } - } else { - error = 'Unknown card type encountered.'; - } - } else { - error = 'No card type configured.'; - } - - if (error) tag = 'hui-error-card'; +function _createElement(tag, config) { const element = document.createElement(tag); - if (error) element.error = error; element.config = config; return element; } + +function _createErrorElement(error, config) { + return _createElement('hui-error-card', createErrorCardConfig(error, config)); +} + +export default function createCardElement(config) { + let tag; + + if (!config || typeof config !== 'object' || !config.type) { + return _createErrorElement('No card type configured.', config); + } + + if (config.type.startsWith(CUSTOM_TYPE_PREFIX)) { + tag = config.type.substr(CUSTOM_TYPE_PREFIX.length); + + if (customElements.get(tag)) { + return _createElement(tag, config); + } + + const element = _createErrorElement(`Custom element doesn't exist: ${tag}.`, config); + + customElements.whenDefined(tag) + .then(() => fireEvent(element, 'rebuild-view')); + + return element; + } + + if (!CARD_TYPES.includes(config.type)) { + return _createErrorElement(`Unknown card type encountered: ${config.type}.`, config); + } + + return _createElement(`hui-${config.type}-card`, config); +} diff --git a/src/panels/lovelace/common/create-error-card-config.js b/src/panels/lovelace/common/create-error-card-config.js new file mode 100644 index 0000000000..31b62ca9ef --- /dev/null +++ b/src/panels/lovelace/common/create-error-card-config.js @@ -0,0 +1,7 @@ +export default function createErrorConfig(error, origConfig) { + return { + type: 'error', + error, + origConfig, + }; +} diff --git a/src/panels/lovelace/hui-view.js b/src/panels/lovelace/hui-view.js index 8e943f32ff..08ca649ed5 100644 --- a/src/panels/lovelace/hui-view.js +++ b/src/panels/lovelace/hui-view.js @@ -2,6 +2,8 @@ import { html } from '@polymer/polymer/lib/utils/html-tag.js'; import { PolymerElement } from '@polymer/polymer/polymer-element.js'; import applyThemesOnElement from '../../common/dom/apply_themes_on_element.js'; +import debounce from '../../common/util/debounce.js'; + import createCardElement from './common/create-card-element'; class HUIView extends PolymerElement { @@ -51,7 +53,7 @@ class HUIView extends PolymerElement { } } -
+
`; } static get properties() { @@ -76,20 +78,7 @@ class HUIView extends PolymerElement { constructor() { super(); this._elements = []; - this._whenDefined = {}; - this.elementNotDefinedCallback = this.elementNotDefinedCallback.bind(this); - } - - _getElements(cards) { - const elements = []; - - for (let i = 0; i < cards.length; i++) { - const element = createCardElement(cards[i], this.elementNotDefinedCallback, null); - element.hass = this.hass; - elements.push(element); - } - - return elements; + this._debouncedConfigChanged = debounce(this._configChanged, 100); } _configChanged() { @@ -105,7 +94,11 @@ class HUIView extends PolymerElement { return; } - const elements = this._getElements(config.cards); + const elements = config.cards.map((cardConfig) => { + const element = createCardElement(cardConfig); + element.hass = this.hass; + return element; + }); let columns = []; const columnEntityCount = []; @@ -159,13 +152,6 @@ class HUIView extends PolymerElement { this._elements[i].hass = hass; } } - - elementNotDefinedCallback(tag) { - if (!(tag in this._whenDefined)) { - this._whenDefined[tag] = customElements.whenDefined(tag) - .then(() => this._configChanged()); - } - } } customElements.define('hui-view', HUIView);