Refactored element creation out of picture-elements class. (#1446)

* Refactored element creation out of picture-element class.

* Fix navigation element listener.

* Cleanup. Change service-icon -> icon.

* Fixes.

* More fixes.

* More fixes.

* Feedback

* More fixes.
This commit is contained in:
Jerad Meisner 2018-07-18 00:48:27 -07:00 committed by Paulus Schoutsen
parent 3f0824bd39
commit 6db76c8ab4
9 changed files with 433 additions and 169 deletions

View File

@ -1,38 +1,9 @@
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import '../../../components/buttons/ha-call-service-button.js';
import '../../../components/entity/ha-state-label-badge.js';
import '../../../components/entity/state-badge.js';
import '../../../components/ha-icon.js';
import '../../../components/ha-card.js';
import '../components/hui-image.js';
import createHuiElement from '../common/create-hui-element.js';
import computeStateDisplay from '../../../common/entity/compute_state_display.js';
import computeStateName from '../../../common/entity/compute_state_name.js';
import computeDomain from '../../../common/entity/compute_domain';
import toggleEntity from '../common/entity/toggle-entity.js';
import EventsMixin from '../../../mixins/events-mixin.js';
import LocalizeMixin from '../../../mixins/localize-mixin.js';
import NavigateMixin from '../../../mixins/navigate-mixin.js';
const VALID_TYPES = new Set([
'image',
'navigation',
'service-button',
'service-icon',
'state-badge',
'state-icon',
'state-label',
]);
/*
* @appliesMixin EventsMixin
* @appliesMixin LocalizeMixin
* @appliesMixin NavigateMixin
*/
class HuiPictureElementsCard extends NavigateMixin(EventsMixin(LocalizeMixin(PolymerElement))) {
class HuiPictureElementsCard extends PolymerElement {
static get template() {
return html`
<style>
@ -51,18 +22,7 @@ class HuiPictureElementsCard extends NavigateMixin(EventsMixin(LocalizeMixin(Pol
position: absolute;
transform: translate(-50%, -50%);
}
.state-label {
padding: 8px;
white-space: nowrap;
}
.clickable {
cursor: pointer;
}
ha-call-service-button {
color: var(--primary-color);
white-space: nowrap;
}
hui-image {
hui-image-element {
overflow-y: hidden;
}
</style>
@ -85,10 +45,7 @@ class HuiPictureElementsCard extends NavigateMixin(EventsMixin(LocalizeMixin(Pol
constructor() {
super();
this._stateBadges = [];
this._stateIcons = [];
this._stateLabels = [];
this._images = [];
this._elements = [];
}
ready() {
@ -104,10 +61,6 @@ class HuiPictureElementsCard extends NavigateMixin(EventsMixin(LocalizeMixin(Pol
if (!config || !config.image || !Array.isArray(config.elements)) {
throw new Error('Invalid card configuration');
}
const invalidTypes = config.elements.map(el => el.type).filter(el => !VALID_TYPES.has(el));
if (invalidTypes.length) {
throw new Error(`Incorrect element types: ${invalidTypes.join(', ')}`);
}
this._config = config;
if (this.$) this._buildConfig();
@ -116,10 +69,7 @@ class HuiPictureElementsCard extends NavigateMixin(EventsMixin(LocalizeMixin(Pol
_buildConfig() {
const config = this._config;
const root = this.$.root;
this._stateBadges = [];
this._stateIcons = [];
this._stateLabels = [];
this._elements = [];
while (root.lastChild) {
root.removeChild(root.lastChild);
@ -130,65 +80,9 @@ class HuiPictureElementsCard extends NavigateMixin(EventsMixin(LocalizeMixin(Pol
root.appendChild(img);
config.elements.forEach((element) => {
const entityId = element.entity;
let el;
switch (element.type) {
case 'service-button':
el = document.createElement('ha-call-service-button');
[el.domain, el.service] = element.service.split('.', 2);
el.serviceData = element.service_data || {};
el.innerText = element.title;
el.hass = this.hass;
break;
case 'service-icon':
el = document.createElement('ha-icon');
el.icon = element.icon;
el.title = element.title || '';
el.addEventListener('click', () => this._handleClick(element));
el.classList.add('clickable');
break;
case 'state-badge':
el = document.createElement('ha-state-label-badge');
el.state = this.hass.states[entityId];
this._stateBadges.push({ el, entityId });
break;
case 'state-icon':
el = document.createElement('state-badge');
el.addEventListener('click', () => this._handleClick(element));
el.classList.add('clickable');
this._stateIcons.push({ el, entityId });
break;
case 'state-label':
el = document.createElement('div');
el.addEventListener('click', () => this._handleClick(element));
el.classList.add('clickable', 'state-label');
this._stateLabels.push({ el, entityId });
break;
case 'navigation':
el = document.createElement('ha-icon');
el.icon = element.icon || 'hass:image-filter-center-focus';
el.addEventListener('click', () => this.navigate(element.navigation_path));
el.title = element.navigation_path;
el.classList.add('clickable');
break;
case 'image':
el = document.createElement('hui-image');
el.hass = this.hass;
el.entity = element.entity;
el.image = element.image;
el.stateImage = element.state_image;
el.filter = element.filter;
el.stateFilter = element.state_filter;
if (!element.camera_image && computeDomain(element.entity) === 'camera') {
el.cameraImage = element.entity;
} else {
el.cameraImage = element.camera_image;
}
this._images.push(el);
if (element.tap_action === 'none') break;
el.addEventListener('click', () => this._handleClick(element));
el.classList.add('clickable');
}
const el = createHuiElement(element);
el.hass = this.hass;
this._elements.push(el);
el.classList.add('element');
Object.keys(element.style).forEach((prop) => {
@ -203,62 +97,9 @@ class HuiPictureElementsCard extends NavigateMixin(EventsMixin(LocalizeMixin(Pol
}
_hassChanged(hass) {
this._stateBadges.forEach((element) => {
const { el, entityId } = element;
el.state = hass.states[entityId];
el.hass = hass;
this._elements.forEach((element) => {
element.hass = hass;
});
this._stateIcons.forEach((element) => {
const { el, entityId } = element;
const stateObj = hass.states[entityId];
if (stateObj) {
el.stateObj = stateObj;
el.title = this._computeTooltip(stateObj);
}
});
this._stateLabels.forEach((element) => {
const { el, entityId } = element;
const stateObj = hass.states[entityId];
if (stateObj) {
el.innerText = computeStateDisplay(this.localize, stateObj);
el.title = this._computeTooltip(stateObj);
} else {
el.innerText = 'N/A';
el.title = '';
}
});
this._images.forEach((el) => {
el.hass = hass;
});
}
_computeTooltip(stateObj) {
return `${computeStateName(stateObj)}: ${computeStateDisplay(this.localize, stateObj)}`;
}
_handleClick(elementConfig) {
const tapAction = elementConfig.tap_action || (elementConfig.type === 'service-icon' ?
'call-service' : 'more-info');
switch (tapAction) {
case 'more-info':
this.fire('hass-more-info', { entityId: elementConfig.entity });
break;
case 'toggle':
toggleEntity(this.hass, elementConfig.entity);
break;
case 'call-service': {
const [domain, service] = elementConfig.service.split('.', 2);
const serviceData = Object.assign(
{}, { entity_id: elementConfig.entity },
elementConfig.service_data
);
this.hass.callService(domain, service, serviceData);
}
}
}
}

View File

@ -0,0 +1,46 @@
import '../elements/hui-icon-element.js';
import '../elements/hui-image-element.js';
import '../elements/hui-service-button-element.js';
import '../elements/hui-state-badge-element.js';
import '../elements/hui-state-icon-element.js';
import '../elements/hui-state-label-element.js';
import createErrorCardConfig from './create-error-card-config.js';
const ELEMENT_TYPES = new Set([
'icon',
'image',
'service-button',
'state-badge',
'state-icon',
'state-label',
]);
function _createElement(tag, config) {
const element = document.createElement(tag);
try {
element.setConfig(config);
} catch (err) {
// eslint-disable-next-line
console.error(tag, err);
// eslint-disable-next-line
return _createErrorElement(err.message, config);
}
return element;
}
function _createErrorElement(error, config) {
return _createElement('hui-error-card', createErrorCardConfig(error, config));
}
export default function createHuiElement(config) {
if (!config || typeof config !== 'object' || !config.type) {
return _createErrorElement('No element type configured.', config);
}
if (!ELEMENT_TYPES.has(config.type)) {
return _createErrorElement(`Unknown element type encountered: ${config.type}.`, config);
}
return _createElement(`hui-${config.type}-element`, config);
}

View File

@ -0,0 +1,46 @@
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import '../../../components/ha-icon.js';
import ElementClickMixin from '../mixins/element-click-mixin.js';
/*
* @appliesMixin ElementClickMixin
*/
class HuiIconElement extends ElementClickMixin(PolymerElement) {
static get template() {
return html`
<style>
:host {
cursor: pointer;
}
</style>
<ha-icon
icon="[[_config.icon]]"
title$="[[computeTooltip(hass, _config)]]"
></ha-icon>
`;
}
static get properties() {
return {
hass: Object,
_config: Object
};
}
ready() {
super.ready();
this.addEventListener('click', () => this.handleClick(this.hass, this._config));
}
setConfig(config) {
if (!config || !config.icon) {
throw Error('Error in element configuration');
}
this._config = config;
}
}
customElements.define('hui-icon-element', HuiIconElement);

View File

@ -0,0 +1,56 @@
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import '../components/hui-image.js';
import ElementClickMixin from '../mixins/element-click-mixin.js';
/*
* @appliesMixin ElementClickMixin
*/
class HuiImageElement extends ElementClickMixin(PolymerElement) {
static get template() {
return html`
<style>
:host(.clickable) {
cursor: pointer;
}
hui-image {
overflow-y: hidden;
}
</style>
<hui-image
hass="[[hass]]"
entity="[[_config.entity]]"
image="[[_config.image]]"
state-image="[[_config.state_image]]"
camera-image="[[_config.camera_image]]"
filter="[[_config.filter]]"
state-filter="[[_config.state_filter]]"
title$="[[computeTooltip(hass, _config)]]"
></hui-image>
`;
}
static get properties() {
return {
hass: Object,
_config: Object
};
}
ready() {
super.ready();
this.addEventListener('click', () => this.handleClick(this.hass, this._config));
}
setConfig(config) {
if (!config) {
throw Error('Error in element configuration');
}
this.classList.toggle('clickable', config.tap_action !== 'none');
this._config = config;
}
}
customElements.define('hui-image-element', HuiImageElement);

View File

@ -0,0 +1,46 @@
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import '../../../components/buttons/ha-call-service-button.js';
class HuiServiceButtonElement extends PolymerElement {
static get template() {
return html`
<style>
ha-call-service-button {
color: var(--primary-color);
white-space: nowrap;
}
</style>
<ha-call-service-button
hass="[[hass]]"
domain="[[_domain]]"
service="[[_service]]"
serviceData="[[_config.serviceData]]"
>[[_config.title]]</ha-call-service-button>
`;
}
static get properties() {
return {
hass: Object,
_config: Object,
_domain: String,
_service: String
};
}
setConfig(config) {
if (!config || !config.service) {
throw Error('Error in element configuration');
}
const [domain, service] = config.service.split('.', 2);
this.setProperties({
_config: config,
_domain: domain,
_service: service
});
}
}
customElements.define('hui-service-button-element', HuiServiceButtonElement);

View File

@ -0,0 +1,48 @@
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import '../../../components/entity/ha-state-label-badge.js';
import computeStateName from '../../../common/entity/compute_state_name';
class HuiStateBadgeElement extends PolymerElement {
static get properties() {
return {
hass: Object,
_config: Object,
};
}
static get observers() {
return [
'_updateBadge(hass, _config)'
];
}
_updateBadge(hass, config) {
if (!hass || !config || !(config.entity in hass.states)) return;
if (!this._badge) {
this._badge = document.createElement('ha-state-label-badge');
}
const stateObj = hass.states[config.entity];
this._badge.state = stateObj;
this._badge.setAttribute('title', computeStateName(stateObj));
if (!this.lastChild) {
this.appendChild(this._badge);
}
}
setConfig(config) {
if (!config || !config.entity) {
throw Error('Error in element configuration');
}
if (this.lastChild) {
this.removeChild(this.lastChild);
}
this._badge = null;
this._config = config;
}
}
customElements.define('hui-state-badge-element', HuiStateBadgeElement);

View File

@ -0,0 +1,54 @@
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import '../../../components/entity/state-badge.js';
import ElementClickMixin from '../mixins/element-click-mixin.js';
/*
* @appliesMixin ElementClickMixin
*/
class HuiStateIconElement extends ElementClickMixin(PolymerElement) {
static get template() {
return html`
<style>
:host {
cursor: pointer;
}
</style>
<state-badge
state-obj="[[_stateObj]]"
title$="[[computeTooltip(hass, _config)]]"
></state-badge>
`;
}
static get properties() {
return {
hass: {
type: Object,
observer: '_hassChanged'
},
_config: Object,
_stateObj: Object
};
}
ready() {
super.ready();
this.addEventListener('click', () => this.handleClick(this.hass, this._config));
}
setConfig(config) {
if (!config || !config.entity) {
throw Error('Error in element configuration');
}
this._config = config;
}
_hassChanged(hass) {
this._stateObj = hass.states[this._config.entity];
}
}
customElements.define('hui-state-icon-element', HuiStateIconElement);

View File

@ -0,0 +1,65 @@
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import computeStateDisplay from '../../../common/entity/compute_state_display.js';
import '../../../components/entity/ha-state-label-badge.js';
import LocalizeMixin from '../../../mixins/localize-mixin.js';
import ElementClickMixin from '../mixins/element-click-mixin.js';
/*
* @appliesMixin ElementClickMixin
* @appliesMixin LocalizeMixin
*/
class HuiStateLabelElement extends LocalizeMixin(ElementClickMixin(PolymerElement)) {
static get template() {
return html`
<style>
:host {
cursor: pointer;
}
.state-label {
padding: 8px;
white-space: nowrap;
}
</style>
<div class="state-label" title$="[[computeTooltip(hass, _config)]]">
[[_computeStateDisplay(_stateObj)]]
</div>
`;
}
static get properties() {
return {
hass: {
type: Object,
observer: '_hassChanged'
},
_config: Object,
_stateObj: Object
};
}
ready() {
super.ready();
this.addEventListener('click', () => this.handleClick(this.hass, this._config));
}
setConfig(config) {
if (!config || !config.entity) {
throw Error('Error in element configuration');
}
this._config = config;
}
_hassChanged(hass) {
this._stateObj = hass.states[this._config.entity];
}
_computeStateDisplay(stateObj) {
return stateObj && computeStateDisplay(this.localize, stateObj);
}
}
customElements.define('hui-state-label-element', HuiStateLabelElement);

View File

@ -0,0 +1,62 @@
import { dedupingMixin } from '@polymer/polymer/lib/utils/mixin.js';
import toggleEntity from '../common/entity/toggle-entity.js';
import NavigateMixin from '../../../mixins/navigate-mixin';
import EventsMixin from '../../../mixins/events-mixin.js';
import computeStateName from '../../../common/entity/compute_state_name';
/*
* @polymerMixin
* @appliesMixin EventsMixin
* @appliesMixin NavigateMixin
*/
export default dedupingMixin(superClass =>
class extends NavigateMixin(EventsMixin(superClass)) {
handleClick(hass, config) {
const tapAction = config.tap_action || 'more-info';
if (tapAction === 'none') return;
switch (tapAction) {
case 'more-info':
this.fire('hass-more-info', { entityId: config.entity });
break;
case 'navigate':
this.navigate(config.navigation_path);
break;
case 'toggle':
toggleEntity(hass, config.entity);
break;
case 'call-service': {
const [domain, service] = config.service.split('.', 2);
const serviceData = Object.assign(
{}, { entity_id: config.entity },
config.service_data
);
hass.callService(domain, service, serviceData);
}
}
}
computeTooltip(hass, config) {
if (config.title) return config.title;
const stateName = (config.entity in hass.states) ?
computeStateName(hass.states[config.entity]) : config.entity;
let tooltip;
switch (config.tap_action) {
case 'navigate':
tooltip = `Navigate to ${config.navigation_path}`;
break;
case 'toggle':
tooltip = `Toggle ${stateName}`;
break;
case 'call-service':
tooltip = `Call service ${config.service}`;
break;
default:
tooltip = `Show more-info: ${stateName}`;
}
return tooltip;
}
});