mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-23 01:06:35 +00:00
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:
parent
8bf60d502a
commit
8cbd667286
@ -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:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
}
|
||||
}
|
||||
|
||||
|
140
src/panels/lovelace/common/directives/long-press-directive.ts
Normal file
140
src/panels/lovelace/common/directives/long-press-directive.ts
Normal 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);
|
||||
});
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user