From b8f3869600b5d990f2534a03da858dd7f3316536 Mon Sep 17 00:00:00 2001 From: Indy Koning Date: Wed, 15 Jul 2020 21:01:09 +0200 Subject: [PATCH] Add pressed and released event to allow usage as momentary button --- README.md | 4 ++- src/action-handler.ts | 67 +++++++++++++++++++++++++++++++++++++++++++ src/button-card.ts | 32 +++++++++++++++++++-- src/types.ts | 2 ++ 4 files changed, 102 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3585766..7410c4b 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Lovelace Button card for your entities. ## Features - works with any toggleable entity -- 6 available actions on **tap** and/or **hold** and/or **double click**: `none`, `toggle`, `more-info`, `navigate`, `url` and `call-service` +- 6 available actions on **tap** and/or **hold** and/or **double click** and/or **press**and/or **release**: `none`, `toggle`, `more-info`, `navigate`, `url` and `call-service` - state display (optional) - custom color (optional), or based on light rgb value/temperature - custom state definition with customizable color, icon and style (optional) @@ -98,6 +98,8 @@ Lovelace Button card for your entities. | `tap_action` | object | optional | See [Action](#Action) | Define the type of action on click, if undefined, toggle will be used. | | `hold_action` | object | optional | See [Action](#Action) | Define the type of action on hold, if undefined, nothing happens. | | `double_tap_action` | object | optional | See [Action](#Action) | Define the type of action on double click, if undefined, nothing happens. | +| `press_action` | object | optional | See [Action](#Action) | Define the type of action on press (triggers the moment your finger presses the button), if undefined, nothing happens. | +| `release_action` | object | optional | See [Action](#Action) | Define the type of action on releasing the button, if undefined, nothing happens. | | `name` | string | optional | `Air conditioner` | Define an optional text to show below the icon. Supports templates, see [templates](#javascript-templates) | | `state_display` | string | optional | `On` | Override the way the state is displayed. Supports templates, see [templates](#javascript-templates) | | `label` | string | optional | Any string that you want | Display a label below the card. See [Layouts](#layout) for more information. Supports templates, see [templates](#javascript-templates) | diff --git a/src/action-handler.ts b/src/action-handler.ts index 0e4a8f3..702c697 100644 --- a/src/action-handler.ts +++ b/src/action-handler.ts @@ -3,6 +3,8 @@ import { directive, PropertyPart } from 'lit-html'; // tslint:disable-next-line import { Ripple } from '@material/mwc-ripple'; import { myFireEvent } from './my-fire-event'; +import { ButtonCardConfig } from './types'; +import { HomeAssistant, ActionConfig, fireEvent, forwardHaptic, navigate, toggleEntity } from 'custom-card-helpers'; const isTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0; @@ -95,6 +97,7 @@ class ActionHandler extends HTMLElement implements ActionHandler { }); const start = (ev: Event): void => { + myFireEvent(element, 'action', { action: 'press' }); this.held = false; let x; let y; @@ -129,6 +132,7 @@ class ActionHandler extends HTMLElement implements ActionHandler { const end = (ev: Event): void => { // Prevent mouse event if touch event ev.preventDefault(); + myFireEvent(element, 'action', { action: 'release' }); if (['touchend', 'touchcancel'].includes(ev.type) && this.timer === undefined) { if (this.isRepeating && this.repeatTimeout) { clearInterval(this.repeatTimeout); @@ -220,3 +224,66 @@ export const actionHandlerBind = (element: ActionHandlerElement, options: Action export const actionHandler = directive((options: ActionHandlerOptions = {}) => (part: PropertyPart): void => { actionHandlerBind(part.committer.element as ActionHandlerElement, options); }); + +export const executeAction = ( + node: HTMLElement, + hass: HomeAssistant, + config: ButtonCardConfig, + action: string, +): void => { + let actionConfig: ActionConfig | undefined; + actionConfig = config[action]; + + if (!actionConfig) { + actionConfig = { + action: 'more-info', + }; + } + + if ( + actionConfig.confirmation && + (!actionConfig.confirmation.exemptions || + !actionConfig.confirmation.exemptions.some(e => e.user === hass!.user!.id)) + ) { + forwardHaptic('warning'); + + if (!confirm(actionConfig.confirmation.text || `Are you sure you want to ${actionConfig.action}?`)) { + return; + } + } + + switch (actionConfig.action) { + case 'more-info': + if (config.entity) { + fireEvent(node, 'hass-more-info', { + entityId: config.entity, + }); + } + break; + case 'navigate': + if (actionConfig.navigation_path) { + navigate(node, actionConfig.navigation_path); + } + break; + case 'url': + if (actionConfig.url_path) { + window.open(actionConfig.url_path); + } + break; + case 'toggle': + if (config.entity) { + toggleEntity(hass, config.entity!); + forwardHaptic('success'); + } + break; + case 'call-service': { + if (!actionConfig.service) { + forwardHaptic('failure'); + return; + } + const [domain, service] = actionConfig.service.split('.', 2); + hass.callService(domain, service, actionConfig.service_data); + forwardHaptic('success'); + } + } +}; diff --git a/src/button-card.ts b/src/button-card.ts index a0525fe..b4c7239 100644 --- a/src/button-card.ts +++ b/src/button-card.ts @@ -39,7 +39,7 @@ import { ButtonCardEmbeddedCards, ButtonCardEmbeddedCardsConfig, } from './types'; -import { actionHandler } from './action-handler'; +import { actionHandler, executeAction } from './action-handler'; import { computeDomain, computeEntity, @@ -630,7 +630,15 @@ class ButtonCard extends LitElement { const tap_action = this._getTemplateOrValue(state, this._config!.tap_action!.action); const hold_action = this._getTemplateOrValue(state, this._config!.hold_action!.action); const double_tap_action = this._getTemplateOrValue(state, this._config!.double_tap_action!.action); - if (tap_action != 'none' || hold_action != 'none' || double_tap_action != 'none') { + const press_action = this._getTemplateOrValue(state, this._config!.press_action!.action); + const release_action = this._getTemplateOrValue(state, this._config!.release_action!.action); + if ( + tap_action != 'none' || + hold_action != 'none' || + double_tap_action != 'none' || + press_action != 'none' || + release_action != 'none' + ) { clickable = true; } else { clickable = false; @@ -933,6 +941,8 @@ class ButtonCard extends LitElement { this._config = { hold_action: { action: 'none' }, double_tap_action: { action: 'none' }, + press_action: { action: 'none' }, + release_action: { action: 'none' }, layout: 'vertical', size: '40%', color_type: 'icon', @@ -1049,6 +1059,12 @@ class ButtonCard extends LitElement { private _handleAction(ev: any): void { if (ev.detail && ev.detail.action) { switch (ev.detail.action) { + case 'press': + this._handlePress(ev); + break; + case 'release': + this._handleRelease(ev); + break; case 'tap': this._handleTap(ev); break; @@ -1064,6 +1080,18 @@ class ButtonCard extends LitElement { } } + private _handlePress(ev): void { + const config = ev.target.config; + if (!config) return; + executeAction(this, this._hass!, this._evalActions(config, 'press_action'), 'press_action'); + } + + private _handleRelease(ev): void { + const config = ev.target.config; + if (!config) return; + executeAction(this, this._hass!, this._evalActions(config, 'release_action'), 'release_action'); + } + private _handleTap(ev): void { const config = ev.target.config; if (!config) return; diff --git a/src/types.ts b/src/types.ts index 2b2ccc0..15d5a22 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,6 +15,8 @@ export interface ButtonCardConfig { tap_action?: ActionConfig; hold_action?: ActionConfig; double_tap_action?: ActionConfig; + press_action?: ActionConfig; + release_action?: ActionConfig; show_name?: boolean; show_state?: boolean; show_icon?: boolean;