Lovelace - Long Press for everything (#1848)

* Long-press controller and lit-directive

* Enable long-press for glance card

* Enable long-press for entity-button

* Use new long-press for picture-elements

* Enable long-press for picture-entity card
This commit is contained in:
Thomas Lovén 2018-10-26 09:27:39 +02:00 committed by Paulus Schoutsen
parent 8bf60d502a
commit 8cbd667286
9 changed files with 231 additions and 109 deletions

View File

@ -13,6 +13,7 @@ import { styleMap } from "lit-html/directives/styleMap.js";
import { HomeAssistant } from "../../../types.js";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { LovelaceCard, LovelaceConfig } from "../types.js";
import { longPress } from "../common/directives/long-press-directive";
interface Config extends LovelaceConfig {
entity: string;
@ -20,6 +21,7 @@ interface Config extends LovelaceConfig {
icon?: string;
theme?: string;
tap_action?: "toggle" | "call-service" | "more-info";
hold_action?: "toggle" | "call-service" | "more-info";
service?: string;
service_data?: object;
}
@ -62,7 +64,11 @@ class HuiEntityButtonCard extends hassLocalizeLitMixin(LitElement)
return html`
${this.renderStyle()}
<ha-card @click="${this.handleClick}">
<ha-card
@ha-click="${() => this.handleClick(false)}"
@ha-hold="${() => this.handleClick(true)}"
.longPress="${longPress()}"
>
${
!stateObj
? html`<div class="not-found">Entity not available: ${
@ -157,7 +163,7 @@ class HuiEntityButtonCard extends hassLocalizeLitMixin(LitElement)
return `hsl(${hue}, 100%, ${100 - sat / 2}%)`;
}
private handleClick() {
private handleClick(hold) {
const config = this.config;
if (!config) {
return;
@ -167,11 +173,12 @@ class HuiEntityButtonCard extends hassLocalizeLitMixin(LitElement)
return;
}
const entityId = stateObj.entity_id;
switch (config.tap_action) {
const action = hold ? config.hold_action : config.tap_action || "more-info";
switch (action) {
case "toggle":
toggleEntity(this.hass, entityId);
break;
case "call-service": {
case "call-service":
if (!config.service) {
return;
}
@ -179,9 +186,10 @@ class HuiEntityButtonCard extends hassLocalizeLitMixin(LitElement)
const serviceData = { entity_id: entityId, ...config.service_data };
this.hass!.callService(domain, service, serviceData);
break;
}
default:
case "more-info":
fireEvent(this, "hass-more-info", { entityId });
break;
default:
}
}
}

View File

@ -16,12 +16,14 @@ import { fireEvent } from "../../../common/dom/fire_event.js";
import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin";
import { HomeAssistant } from "../../../types.js";
import { LovelaceCard, LovelaceConfig } from "../types.js";
import { longPress } from "../common/directives/long-press-directive";
interface EntityConfig {
name: string;
icon: string;
entity: string;
tap_action: "toggle" | "call-service" | "more-info";
hold_action?: "toggle" | "call-service" | "more-info";
service?: string;
service_data?: object;
}
@ -59,9 +61,13 @@ export class HuiGlanceCard extends hassLocalizeLitMixin(LitElement)
const entities = processConfigEntities(config.entities);
for (const entity of entities) {
if (entity.tap_action === "call-service" && !entity.service) {
if (
(entity.tap_action === "call-service" ||
entity.hold_action === "call-service") &&
!entity.service
) {
throw new Error(
'Missing required property "service" when tap_action is call-service'
'Missing required property "service" when tap_action or hold_action is call-service'
);
}
}
@ -151,7 +157,9 @@ export class HuiGlanceCard extends hassLocalizeLitMixin(LitElement)
<div
class="entity"
.entityConf="${entityConf}"
@click="${this.handleClick}"
@ha-click="${(ev) => this.handleClick(ev, false)}"
@ha-hold="${(ev) => this.handleClick(ev, true)}"
.longPress="${longPress()}"
>
${
this.config!.show_name !== false
@ -175,21 +183,23 @@ export class HuiGlanceCard extends hassLocalizeLitMixin(LitElement)
`;
}
private handleClick(ev: MouseEvent) {
private handleClick(ev: MouseEvent, hold) {
const config = (ev.currentTarget as any).entityConf as EntityConfig;
const entityId = config.entity;
switch (config.tap_action) {
const action = hold ? config.hold_action : config.tap_action || "more-info";
switch (action) {
case "toggle":
toggleEntity(this.hass, entityId);
break;
case "call-service": {
case "call-service":
const [domain, service] = config.service!.split(".", 2);
const serviceData = { entity_id: entityId, ...config.service_data };
this.hass!.callService(domain, service, serviceData);
break;
}
default:
case "more-info":
fireEvent(this, "hass-more-info", { entityId });
break;
default:
}
}
}

View File

@ -11,6 +11,7 @@ import toggleEntity from "../common/entity/toggle-entity.js";
import EventsMixin from "../../../mixins/events-mixin.js";
import LocalizeMixin from "../../../mixins/localize-mixin.js";
import { longPressBind } from "../common/directives/long-press-directive";
const UNAVAILABLE = "Unavailable";
@ -51,7 +52,7 @@ class HuiPictureEntityCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
}
</style>
<ha-card id='card' on-click="_cardClicked">
<ha-card id='card'>
<hui-image
hass="[[hass]]"
image="[[_config.image]]"
@ -112,6 +113,14 @@ class HuiPictureEntityCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
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;
@ -163,16 +172,22 @@ class HuiPictureEntityCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
return config.show_name === false && config.show_state !== false;
}
_cardClicked() {
_cardClicked(hold) {
const config = this._config;
const entityId = config.entity;
if (!(entityId in this.hass.states)) return;
if (config.tap_action === "toggle") {
toggleEntity(this.hass, entityId);
} else {
this.fire("hass-more-info", { entityId });
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:
}
}

View File

@ -0,0 +1,140 @@
import { directive, PropertyPart } from "lit-html";
import "@material/mwc-ripple";
const isTouch =
"ontouchstart" in window ||
navigator.maxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0;
interface LongPress extends HTMLElement {
holdTime: number;
bind(element: Element): void;
}
interface LongPressElement extends Element {
longPress?: boolean;
}
class LongPress extends HTMLElement implements LongPress {
public holdTime: number;
protected ripple: any;
protected timer: number | undefined;
protected held: boolean;
constructor() {
super();
this.holdTime = 500;
this.ripple = document.createElement("mwc-ripple");
this.timer = undefined;
this.held = false;
}
public connectedCallback() {
Object.assign(this.style, {
position: "absolute",
width: isTouch ? "100px" : "50px",
height: isTouch ? "100px" : "50px",
transform: "translate(-50%, -50%)",
pointerEvents: "none",
});
this.appendChild(this.ripple);
this.ripple.primary = true;
[
isTouch ? "touchcancel" : "mouseout",
"mouseup",
"touchmove",
"mousewheel",
"wheel",
"scroll",
].forEach((ev) => {
document.addEventListener(
ev,
() => {
clearTimeout(this.timer);
this.stopAnimation();
},
{ passive: true }
);
});
}
public bind(element: LongPressElement) {
if (element.longPress) {
return;
}
element.longPress = true;
element.addEventListener(
isTouch ? "touchstart" : "mousedown",
(ev: Event) => {
this.held = false;
let x;
let y;
if ((ev as TouchEvent).touches) {
x = (ev as TouchEvent).touches[0].pageX;
y = (ev as TouchEvent).touches[0].pageY;
} else {
x = (ev as MouseEvent).pageX;
y = (ev as MouseEvent).pageY;
}
this.timer = window.setTimeout(() => {
this.startAnimation(x, y);
this.held = true;
}, this.holdTime);
},
{ passive: true }
);
element.addEventListener("click", () => {
this.stopAnimation();
if (this.held) {
element.dispatchEvent(new Event("ha-hold"));
} else {
element.dispatchEvent(new Event("ha-click"));
}
});
}
private startAnimation(x: number, y: number) {
Object.assign(this.style, {
left: `${x}px`,
top: `${y}px`,
display: null,
});
this.ripple.disabled = false;
this.ripple.active = true;
this.ripple.unbounded = true;
}
private stopAnimation() {
this.ripple.active = false;
this.ripple.disabled = true;
this.style.display = "none";
}
}
customElements.define("long-press", LongPress);
const getLongPress = (): LongPress => {
const body = document.body;
if (body.querySelector("long-press")) {
return body.querySelector("long-press") as LongPress;
}
const longpress = document.createElement("long-press");
body.appendChild(longpress);
return longpress as LongPress;
};
export const longPressBind = (element: LongPressElement) => {
const longpress: LongPress = getLongPress();
if (!longpress) {
return;
}
longpress.bind(element);
};
export const longPress = () =>
directive((part: PropertyPart) => {
longPressBind(part.committer.element);
});

View File

@ -4,6 +4,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element.js";
import "../../../components/ha-icon.js";
import ElementClickMixin from "../mixins/element-click-mixin.js";
import { longPressBind } from "../common/directives/long-press-directive";
/*
* @appliesMixin ElementClickMixin
@ -32,7 +33,13 @@ class HuiIconElement extends ElementClickMixin(PolymerElement) {
ready() {
super.ready();
this.registerMouse(this._config);
longPressBind(this);
this.addEventListener("ha-click", () =>
this.handleClick(this.hass, this._config, false)
);
this.addEventListener("ha-hold", () =>
this.handleClick(this.hass, this._config, true)
);
}
setConfig(config) {

View File

@ -4,6 +4,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element.js";
import "../components/hui-image.js";
import ElementClickMixin from "../mixins/element-click-mixin.js";
import { longPressBind } from "../common/directives/long-press-directive";
/*
* @appliesMixin ElementClickMixin
@ -44,7 +45,13 @@ class HuiImageElement extends ElementClickMixin(PolymerElement) {
ready() {
super.ready();
this.registerMouse(this._config);
longPressBind(this);
this.addEventListener("ha-click", () =>
this.handleClick(this.hass, this._config, false)
);
this.addEventListener("ha-hold", () =>
this.handleClick(this.hass, this._config, true)
);
}
setConfig(config) {

View File

@ -4,6 +4,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element.js";
import "../../../components/entity/state-badge.js";
import ElementClickMixin from "../mixins/element-click-mixin.js";
import { longPressBind } from "../common/directives/long-press-directive";
/*
* @appliesMixin ElementClickMixin
@ -36,7 +37,13 @@ class HuiStateIconElement extends ElementClickMixin(PolymerElement) {
ready() {
super.ready();
this.registerMouse(this._config);
longPressBind(this);
this.addEventListener("ha-click", () =>
this.handleClick(this.hass, this._config, false)
);
this.addEventListener("ha-hold", () =>
this.handleClick(this.hass, this._config, true)
);
}
setConfig(config) {

View File

@ -7,6 +7,7 @@ import "../../../components/entity/ha-state-label-badge.js";
import LocalizeMixin from "../../../mixins/localize-mixin.js";
import ElementClickMixin from "../mixins/element-click-mixin.js";
import { longPressBind } from "../common/directives/long-press-directive";
/*
* @appliesMixin ElementClickMixin
@ -45,7 +46,13 @@ class HuiStateLabelElement extends LocalizeMixin(
ready() {
super.ready();
this.registerMouse(this._config);
longPressBind(this);
this.addEventListener("ha-click", () =>
this.handleClick(this.hass, this._config, false)
);
this.addEventListener("ha-hold", () =>
this.handleClick(this.hass, this._config, true)
);
}
setConfig(config) {

View File

@ -3,7 +3,6 @@ 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";
import "@material/mwc-ripple";
/*
* @polymerMixin
@ -13,92 +12,14 @@ import "@material/mwc-ripple";
export default dedupingMixin(
(superClass) =>
class extends NavigateMixin(EventsMixin(superClass)) {
registerMouse(config) {
var isTouch =
"ontouchstart" in window ||
navigator.MaxTouchPoints > 0 ||
navigator.msMaxTouchPoints > 0;
let ripple = null;
const rippleWrapper = document.createElement("div");
this.parentElement.appendChild(rippleWrapper);
Object.assign(rippleWrapper.style, {
position: this.style.position || "absolute",
width: isTouch ? "100px" : "50px",
height: isTouch ? "100px" : "50px",
top: this.style.top,
left: this.style.left,
bottom: this.style.bottom,
right: this.style.right,
transform: "translate(-50%, -50%)",
pointerEvents: "none",
});
const loadRipple = () => {
if (ripple) return;
ripple = document.createElement("mwc-ripple");
rippleWrapper.appendChild(ripple);
ripple.unbounded = true;
ripple.primary = true;
};
const startAnimation = () => {
ripple.style.visibility = "visible";
ripple.disabled = false;
ripple.active = true;
};
const stopAnimation = () => {
if (ripple) {
ripple.active = false;
ripple.disabled = true;
ripple.style.visibility = "hidden";
}
};
var mouseDown = isTouch ? "touchstart" : "mousedown";
var mouseOut = isTouch ? "touchcancel" : "mouseout";
var click = isTouch ? "touchend" : "click";
var timer = null;
var held = false;
var holdTime = config.hold_time || 500;
this.addEventListener(mouseDown, () => {
held = false;
loadRipple();
timer = setTimeout(() => {
startAnimation();
held = true;
}, holdTime);
});
this.addEventListener(click, () => {
stopAnimation();
this.handleClick(this.hass, config, held);
});
[
mouseOut,
"mouseup",
"touchmove",
"mousewheel",
"wheel",
"scroll",
].forEach((ev) => {
document.addEventListener(ev, () => {
clearTimeout(timer);
stopAnimation();
});
});
}
handleClick(hass, config, held = false) {
let tapAction = config.tap_action || "more-info";
if (held) {
tapAction = config.hold_action || "none";
handleClick(hass, config, hold) {
let action = config.tap_action || "more-info";
if (hold) {
action = config.hold_action;
}
if (tapAction === "none") return;
if (action === "none") return;
switch (tapAction) {
switch (action) {
case "more-info":
this.fire("hass-more-info", { entityId: config.entity });
break;