mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-02 05:57:54 +00:00
Convert hui-picture-entity-card to TypeScript/LitElement (#2168)
* Convert hui-picture-entity-card to TypeScript/LitElement click/hold are not working on my Chrome dev env * typo * Address review comments Still having issues with clicks/holds on lots of cards on my system * Add explicit navigate option * Fixed after testing with touchevents * Simplify
This commit is contained in:
parent
5dc05129ef
commit
5fec881c39
1
src/data/entity.ts
Normal file
1
src/data/entity.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const UNAVAILABLE = "unavailable";
|
@ -1,197 +0,0 @@
|
|||||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
|
||||||
|
|
||||||
import "../../../components/ha-card";
|
|
||||||
import "../components/hui-image";
|
|
||||||
|
|
||||||
import computeDomain from "../../../common/entity/compute_domain";
|
|
||||||
import computeStateDisplay from "../../../common/entity/compute_state_display";
|
|
||||||
import computeStateName from "../../../common/entity/compute_state_name";
|
|
||||||
|
|
||||||
import EventsMixin from "../../../mixins/events-mixin";
|
|
||||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
|
||||||
import { toggleEntity } from "../common/entity/toggle-entity";
|
|
||||||
import { longPressBind } from "../common/directives/long-press-directive";
|
|
||||||
|
|
||||||
const UNAVAILABLE = "Unavailable";
|
|
||||||
|
|
||||||
/*
|
|
||||||
* @appliesMixin LocalizeMixin
|
|
||||||
* @appliesMixin EventsMixin
|
|
||||||
*/
|
|
||||||
class HuiPictureEntityCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
|
||||||
static get template() {
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
ha-card {
|
|
||||||
min-height: 75px;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
ha-card.canInteract {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.footer {
|
|
||||||
@apply --paper-font-common-nowrap;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
|
||||||
padding: 16px;
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 16px;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.both {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
.state {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<ha-card id="card">
|
|
||||||
<hui-image
|
|
||||||
hass="[[hass]]"
|
|
||||||
image="[[_config.image]]"
|
|
||||||
state-image="[[_config.state_image]]"
|
|
||||||
camera-image="[[_getCameraImage(_config)]]"
|
|
||||||
entity="[[_config.entity]]"
|
|
||||||
aspect-ratio="[[_config.aspect_ratio]]"
|
|
||||||
></hui-image>
|
|
||||||
<template is="dom-if" if="[[_showNameAndState(_config)]]">
|
|
||||||
<div class="footer both">
|
|
||||||
<div>[[_name]]</div>
|
|
||||||
<div>[[_state]]</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[_showName(_config)]]">
|
|
||||||
<div class="footer">[[_name]]</div>
|
|
||||||
</template>
|
|
||||||
<template is="dom-if" if="[[_showState(_config)]]">
|
|
||||||
<div class="footer state">[[_state]]</div>
|
|
||||||
</template>
|
|
||||||
</ha-card>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static get properties() {
|
|
||||||
return {
|
|
||||||
hass: {
|
|
||||||
type: Object,
|
|
||||||
observer: "_hassChanged",
|
|
||||||
},
|
|
||||||
_config: Object,
|
|
||||||
_name: String,
|
|
||||||
_state: String,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getCardSize() {
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
setConfig(config) {
|
|
||||||
if (!config || !config.entity) {
|
|
||||||
throw new Error("Error in card configuration.");
|
|
||||||
}
|
|
||||||
|
|
||||||
this._entityDomain = computeDomain(config.entity);
|
|
||||||
if (
|
|
||||||
this._entityDomain !== "camera" &&
|
|
||||||
(!config.image && !config.state_image && !config.camera_image)
|
|
||||||
) {
|
|
||||||
throw new Error("No image source configured.");
|
|
||||||
}
|
|
||||||
|
|
||||||
this._config = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
ready() {
|
|
||||||
super.ready();
|
|
||||||
const card = this.shadowRoot.querySelector("#card");
|
|
||||||
longPressBind(card);
|
|
||||||
card.addEventListener("ha-click", () => this._cardClicked(false));
|
|
||||||
card.addEventListener("ha-hold", () => this._cardClicked(true));
|
|
||||||
}
|
|
||||||
|
|
||||||
_hassChanged(hass) {
|
|
||||||
const config = this._config;
|
|
||||||
const entityId = config.entity;
|
|
||||||
const stateObj = hass.states[entityId];
|
|
||||||
|
|
||||||
// Nothing changed
|
|
||||||
if (
|
|
||||||
(!stateObj && this._oldState === UNAVAILABLE) ||
|
|
||||||
(stateObj && stateObj.state === this._oldState)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let name;
|
|
||||||
let state;
|
|
||||||
let stateLabel;
|
|
||||||
let available;
|
|
||||||
|
|
||||||
if (stateObj) {
|
|
||||||
name = config.name || computeStateName(stateObj);
|
|
||||||
state = stateObj.state;
|
|
||||||
stateLabel = computeStateDisplay(this.localize, stateObj);
|
|
||||||
available = true;
|
|
||||||
} else {
|
|
||||||
name = config.name || entityId;
|
|
||||||
state = UNAVAILABLE;
|
|
||||||
stateLabel = this.localize("state.default.unavailable");
|
|
||||||
available = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setProperties({
|
|
||||||
_name: name,
|
|
||||||
_state: stateLabel,
|
|
||||||
_oldState: state,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$.card.classList.toggle("canInteract", available);
|
|
||||||
}
|
|
||||||
|
|
||||||
_showNameAndState(config) {
|
|
||||||
return config.show_name !== false && config.show_state !== false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_showName(config) {
|
|
||||||
return config.show_name !== false && config.show_state === false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_showState(config) {
|
|
||||||
return config.show_name === false && config.show_state !== false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_cardClicked(hold) {
|
|
||||||
const config = this._config;
|
|
||||||
const entityId = config.entity;
|
|
||||||
|
|
||||||
if (!(entityId in this.hass.states)) return;
|
|
||||||
|
|
||||||
const action = hold ? config.hold_action : config.tap_action || "more-info";
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case "toggle":
|
|
||||||
toggleEntity(this.hass, entityId);
|
|
||||||
break;
|
|
||||||
case "more-info":
|
|
||||||
this.fire("hass-more-info", { entityId });
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_getCameraImage(config) {
|
|
||||||
return this._entityDomain === "camera"
|
|
||||||
? config.entity
|
|
||||||
: config.camera_image;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("hui-picture-entity-card", HuiPictureEntityCard);
|
|
175
src/panels/lovelace/cards/hui-picture-entity-card.ts
Normal file
175
src/panels/lovelace/cards/hui-picture-entity-card.ts
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element";
|
||||||
|
import { TemplateResult } from "lit-html/lib/shady-render";
|
||||||
|
import { classMap } from "lit-html/directives/classMap";
|
||||||
|
|
||||||
|
import "../../../components/ha-card";
|
||||||
|
import "../components/hui-image";
|
||||||
|
|
||||||
|
import computeDomain from "../../../common/entity/compute_domain";
|
||||||
|
import computeStateDisplay from "../../../common/entity/compute_state_display";
|
||||||
|
import computeStateName from "../../../common/entity/compute_state_name";
|
||||||
|
|
||||||
|
import { longPress } from "../common/directives/long-press-directive";
|
||||||
|
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import { LovelaceCardConfig } from "../../../data/lovelace";
|
||||||
|
import { LovelaceCard } from "../types";
|
||||||
|
import { handleClick } from "../common/handle-click";
|
||||||
|
import { UNAVAILABLE } from "../../../data/entity";
|
||||||
|
|
||||||
|
interface Config extends LovelaceCardConfig {
|
||||||
|
entity: string;
|
||||||
|
name?: string;
|
||||||
|
navigation_path?: string;
|
||||||
|
image?: string;
|
||||||
|
camera_image?: string;
|
||||||
|
state_image?: {};
|
||||||
|
aspect_ratio?: string;
|
||||||
|
tap_action?: "toggle" | "call-service" | "more-info" | "navigate";
|
||||||
|
hold_action?: "toggle" | "call-service" | "more-info" | "navigate";
|
||||||
|
service?: string;
|
||||||
|
service_data?: object;
|
||||||
|
show_name?: boolean;
|
||||||
|
show_state?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class HuiPictureEntityCard extends hassLocalizeLitMixin(LitElement)
|
||||||
|
implements LovelaceCard {
|
||||||
|
public hass?: HomeAssistant;
|
||||||
|
private _config?: Config;
|
||||||
|
|
||||||
|
static get properties(): PropertyDeclarations {
|
||||||
|
return {
|
||||||
|
hass: {},
|
||||||
|
_config: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCardSize(): number {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setConfig(config: Config): void {
|
||||||
|
if (!config || !config.entity) {
|
||||||
|
throw new Error("Invalid Configuration: 'entity' required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
computeDomain(config.entity) !== "camera" &&
|
||||||
|
(!config.image && !config.state_image && !config.camera_image)
|
||||||
|
) {
|
||||||
|
throw new Error("No image source configured.");
|
||||||
|
}
|
||||||
|
|
||||||
|
this._config = { show_name: true, show_state: true, ...config };
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
if (!this._config || !this.hass || !this.hass.states[this._config.entity]) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateObj = this.hass.states[this._config.entity];
|
||||||
|
const name = this._config.name || computeStateName(stateObj);
|
||||||
|
const state = computeStateDisplay(
|
||||||
|
this.localize,
|
||||||
|
stateObj,
|
||||||
|
this.hass.language
|
||||||
|
);
|
||||||
|
|
||||||
|
let footer: TemplateResult | string = "";
|
||||||
|
if (this._config.show_name && this._config.show_state) {
|
||||||
|
footer = html`
|
||||||
|
<div class="footer both">
|
||||||
|
<div>${name}</div>
|
||||||
|
<div>${state}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (this._config.show_name) {
|
||||||
|
footer = html`
|
||||||
|
<div class="footer">${name}</div>
|
||||||
|
`;
|
||||||
|
} else if (this._config.show_state) {
|
||||||
|
footer = html`
|
||||||
|
<div class="footer state">${state}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
${this.renderStyle()}
|
||||||
|
<ha-card>
|
||||||
|
<hui-image
|
||||||
|
.hass="${this.hass}"
|
||||||
|
.image="${this._config.image}"
|
||||||
|
.stateImage="${this._config.state_image}"
|
||||||
|
.cameraImage="${
|
||||||
|
computeDomain(this._config.entity) === "camera"
|
||||||
|
? this._config.entity
|
||||||
|
: this._config.camera_image
|
||||||
|
}"
|
||||||
|
.entity="${this._config.entity}"
|
||||||
|
.aspectRatio="${this._config.aspect_ratio}"
|
||||||
|
@ha-click="${this._handleClick}"
|
||||||
|
@ha-hold="${this._handleHold}"
|
||||||
|
.longPress="${longPress()}"
|
||||||
|
class="${
|
||||||
|
classMap({
|
||||||
|
clickable: stateObj.state !== UNAVAILABLE,
|
||||||
|
})
|
||||||
|
}"
|
||||||
|
></hui-image>
|
||||||
|
${footer}
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderStyle(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
ha-card {
|
||||||
|
min-height: 75px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
hui-image.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
@apply --paper-font-common-nowrap;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 16px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.both {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.state {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleClick() {
|
||||||
|
handleClick(this, this.hass!, this._config!, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleHold() {
|
||||||
|
handleClick(this, this.hass!, this._config!, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-picture-entity-card": HuiPictureEntityCard;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("hui-picture-entity-card", HuiPictureEntityCard);
|
@ -3,11 +3,12 @@ import { LovelaceElementConfig } from "../elements/types";
|
|||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import { navigate } from "../../../common/navigate";
|
import { navigate } from "../../../common/navigate";
|
||||||
import { toggleEntity } from "../../../../src/panels/lovelace/common/entity/toggle-entity";
|
import { toggleEntity } from "../../../../src/panels/lovelace/common/entity/toggle-entity";
|
||||||
|
import { LovelaceCardConfig } from "../../../data/lovelace";
|
||||||
|
|
||||||
export const handleClick = (
|
export const handleClick = (
|
||||||
node: HTMLElement,
|
node: HTMLElement,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config: LovelaceElementConfig,
|
config: LovelaceElementConfig | LovelaceCardConfig,
|
||||||
hold: boolean
|
hold: boolean
|
||||||
): void => {
|
): void => {
|
||||||
let action = config.tap_action || "more-info";
|
let action = config.tap_action || "more-info";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user