From c3ae0f3a25563a1ff10d26ce442a6c1735da2ee1 Mon Sep 17 00:00:00 2001 From: Indy Koning Date: Wed, 15 Jul 2020 21:01:09 +0200 Subject: [PATCH 1/2] Add pressed and released event to allow usage as momentary button --- README.md | 4 +- src/action-handler.ts | 4 ++ src/button-card.ts | 41 ++++++++++++++++++- src/handle-action.ts | 92 +++++++++++++++++++++++++++++++++++++++++++ src/types.ts | 2 + 5 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 src/handle-action.ts 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..146f80f 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); diff --git a/src/button-card.ts b/src/button-card.ts index a0525fe..f760f01 100644 --- a/src/button-card.ts +++ b/src/button-card.ts @@ -40,6 +40,7 @@ import { ButtonCardEmbeddedCardsConfig, } from './types'; import { actionHandler } from './action-handler'; +import { handleActionConfig } from './handle-action'; import { computeDomain, computeEntity, @@ -630,7 +631,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 +942,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 +1060,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 +1081,28 @@ class ButtonCard extends LitElement { } } + private _handlePress(ev): void { + const config = ev.target.config; + if (!config) return; + handleActionConfig( + this, + this._hass!, + this._evalActions(config, 'press_action'), + this._evalActions(config, 'press_action').press_action, + ); + } + + private _handleRelease(ev): void { + const config = ev.target.config; + if (!config) return; + handleActionConfig( + this, + this._hass!, + this._evalActions(config, 'release_action'), + this._evalActions(config, 'release_action').release_action, + ); + } + private _handleTap(ev): void { const config = ev.target.config; if (!config) return; diff --git a/src/handle-action.ts b/src/handle-action.ts new file mode 100644 index 0000000..60f59c6 --- /dev/null +++ b/src/handle-action.ts @@ -0,0 +1,92 @@ +import { HomeAssistant, ActionConfig, fireEvent, forwardHaptic, navigate, toggleEntity } from 'custom-card-helpers'; + +export const handleActionConfig = ( + node: HTMLElement, + hass: HomeAssistant, + config: { + entity?: string; + camera_image?: string; + hold_action?: ActionConfig; + tap_action?: ActionConfig; + double_tap_action?: ActionConfig; + }, + actionConfig: ActionConfig | undefined, +): void => { + 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 || config.camera_image) { + fireEvent(node, 'hass-more-info', { + entityId: config.entity ? config.entity : config.camera_image!, + }); + } + 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'); + } + } +}; + +export const handleAction = ( + node: HTMLElement, + hass: HomeAssistant, + config: { + entity?: string; + camera_image?: string; + hold_action?: ActionConfig; + tap_action?: ActionConfig; + double_tap_action?: ActionConfig; + }, + action: string, +): void => { + let actionConfig: ActionConfig | undefined; + + if (action === 'double_tap' && config.double_tap_action) { + actionConfig = config.double_tap_action; + } else if (action === 'hold' && config.hold_action) { + actionConfig = config.hold_action; + } else if (action === 'tap' && config.tap_action) { + actionConfig = config.tap_action; + } + + handleActionConfig(node, hass, config, actionConfig); +}; 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; From 736bcceea26db37fc952d98cc862b7124d3b9ed4 Mon Sep 17 00:00:00 2001 From: Indy Koning Date: Mon, 5 Apr 2021 14:50:39 +0200 Subject: [PATCH 2/2] Use card helpers --- src/action-handler.ts | 4 +- src/button-card.ts | 22 +++++------ src/handle-action.ts | 92 ------------------------------------------- 3 files changed, 10 insertions(+), 108 deletions(-) delete mode 100644 src/handle-action.ts diff --git a/src/action-handler.ts b/src/action-handler.ts index 3e03ef8..69a5637 100644 --- a/src/action-handler.ts +++ b/src/action-handler.ts @@ -3,8 +3,6 @@ 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'; import { deepEqual } from './deep-equal'; const isTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0; @@ -15,7 +13,7 @@ interface ActionHandler extends HTMLElement { } export interface ActionHandlerDetail { - action: 'hold' | 'tap' | 'double_tap'; + action: 'hold' | 'tap' | 'double_tap' | 'press' | 'release'; } export interface ActionHandlerOptions { diff --git a/src/button-card.ts b/src/button-card.ts index f4bc4d1..378d1e8 100644 --- a/src/button-card.ts +++ b/src/button-card.ts @@ -21,6 +21,7 @@ import { stateIcon, HomeAssistant, handleClick, + handleActionConfig, getLovelace, timerTimeRemaining, secondsToDuration, @@ -41,7 +42,6 @@ import { ButtonCardEmbeddedCardsConfig, } from './types'; import { actionHandler } from './action-handler'; -import { handleActionConfig } from './handle-action'; import { computeDomain, computeEntity, @@ -1152,23 +1152,19 @@ class ButtonCard extends LitElement { private _handlePress(): void { const config = this._config; if (!config) return; - handleActionConfig( - this, - this._hass!, - this._evalActions(config, 'press_action'), - this._evalActions(config, 'press_action').press_action, - ); + const actionConfig = this._evalActions(config, 'press_action'); + if (!actionConfig || !actionConfig.press_action) return; + + handleActionConfig(this, this._hass!, actionConfig, actionConfig.press_action); } private _handleRelease(): void { const config = this._config; if (!config) return; - handleActionConfig( - this, - this._hass!, - this._evalActions(config, 'release_action'), - this._evalActions(config, 'release_action').release_action, - ); + const actionConfig = this._evalActions(config, 'release_action'); + if (!actionConfig || !actionConfig.release_action) return; + + handleActionConfig(this, this._hass!, actionConfig, actionConfig.release_action); } private _handleTap(): void { diff --git a/src/handle-action.ts b/src/handle-action.ts deleted file mode 100644 index 60f59c6..0000000 --- a/src/handle-action.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { HomeAssistant, ActionConfig, fireEvent, forwardHaptic, navigate, toggleEntity } from 'custom-card-helpers'; - -export const handleActionConfig = ( - node: HTMLElement, - hass: HomeAssistant, - config: { - entity?: string; - camera_image?: string; - hold_action?: ActionConfig; - tap_action?: ActionConfig; - double_tap_action?: ActionConfig; - }, - actionConfig: ActionConfig | undefined, -): void => { - 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 || config.camera_image) { - fireEvent(node, 'hass-more-info', { - entityId: config.entity ? config.entity : config.camera_image!, - }); - } - 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'); - } - } -}; - -export const handleAction = ( - node: HTMLElement, - hass: HomeAssistant, - config: { - entity?: string; - camera_image?: string; - hold_action?: ActionConfig; - tap_action?: ActionConfig; - double_tap_action?: ActionConfig; - }, - action: string, -): void => { - let actionConfig: ActionConfig | undefined; - - if (action === 'double_tap' && config.double_tap_action) { - actionConfig = config.double_tap_action; - } else if (action === 'hold' && config.hold_action) { - actionConfig = config.hold_action; - } else if (action === 'tap' && config.tap_action) { - actionConfig = config.tap_action; - } - - handleActionConfig(node, hass, config, actionConfig); -};