Add tap actions

This commit is contained in:
Georgi Gardev
2022-12-01 15:58:00 +02:00
parent b33fd03c11
commit f7daf893b3
6 changed files with 327 additions and 42 deletions
+5 -2
View File
@@ -99,7 +99,7 @@ layout:
## Options
| Name | Type | Requirement | Description | Default |
| ----------- | ------------ | ------------ | --------------------------------------------------------------------------------------------------------- | ------------------------ |
| ----------------- | ------------ | ------------ | --------------------------------------------------------------------------------------------------------- | ------------------------ |
| type | string | **Required** | `custom:time-picker-card` | |
| entity | string | **Required** | [Input Datetime](https://www.home-assistant.io/integrations/input_datetime/) entity with `has_time: true` | |
| name | string | **Optional** | Card name | Entity's `friendly_name` |
@@ -109,6 +109,9 @@ layout:
| minute_step | number | **Optional** | Minute change when clicking arrows | `5` |
| layout | object | **Optional** | Card Layout configuration | `none` |
| hide | object | **Optional** | Hide object | `none` |
| tap_action | action | **Optional** | Home assistant action to perform on tap | `more-info` |
| double_tap_action | action | **Optional** | Home assistant action to perform on tap | `more-info` |
| hold_action | action | **Optional** | Home assistant action to perform on tap | `more-info` |
### Layout Object
@@ -122,7 +125,7 @@ layout:
### Hide Object
| Name | Type | Requirement | Description | Default |
| ------- | ------- | ------------ | ------------------- | ------- |
| ------- | ------- | ------------ | ------------------------------------------------- | ------- |
| name | boolean | **Optional** | Hides the card name | `false` |
| icon | boolean | **Optional** | Hides the card icon (only with controls "inside") | `false` |
| seconds | boolean | **Optional** | Hides seconds input | `true` |
+29
View File
@@ -17,6 +17,24 @@ views:
name: Alarm time
hour_mode: 24
hour_step: 2
tap_action:
action: call-service
service: logbook.log
service_data:
name: "Time picker card · Tap Action"
message: "Debug"
double_tap_action:
action: call-service
service: logbook.log
service_data:
name: "Time picker card · Double Tap Action"
message: "Debug"
hold_action:
action: call-service
service: logbook.log
service_data:
name: "Time picker card · Hold action"
message: "Debug"
- type: custom:time-picker-card
entity: input_datetime.test_date_time
hour_mode: 12
@@ -50,6 +68,17 @@ views:
hide:
name: false
- type: custom:time-picker-card
entity: input_datetime.test_date
link_values: true
hour_mode: 12
hour_step: 2
minute_step: 3
layout:
hour_mode: single
hide:
name: false
- title: embedded
path: embedded
cards:
+204
View File
@@ -0,0 +1,204 @@
import { noChange } from 'lit';
import { AttributePart, directive, Directive, DirectiveParameters } from 'lit/directive.js';
import { fireEvent } from 'custom-card-helpers';
import { ActionHandlerDetail, ActionHandlerOptions } from 'custom-card-helpers/dist/types';
const isTouch =
'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.maxTouchPoints > 0;
interface ActionHandler extends HTMLElement {
holdTime: number;
bind(element: Element, options): void;
}
interface ActionHandlerElement extends HTMLElement {
actionHandler?: boolean;
}
declare global {
interface HASSDomEvents {
action: ActionHandlerDetail;
}
}
class ActionHandler extends HTMLElement implements ActionHandler {
public holdTime = 500;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public ripple: any;
protected timer?: number;
protected held = false;
private dblClickTimeout?: number;
constructor() {
super();
this.ripple = document.createElement('mwc-ripple');
}
public connectedCallback(): void {
Object.assign(this.style, {
position: 'absolute',
width: isTouch ? '100px' : '50px',
height: isTouch ? '100px' : '50px',
transform: 'translate(-50%, -50%)',
pointerEvents: 'none',
zIndex: '999',
});
this.appendChild(this.ripple);
this.ripple.primary = true;
['touchcancel', 'mouseout', 'mouseup', 'touchmove', 'mousewheel', 'wheel', 'scroll'].forEach(
(ev) => {
document.addEventListener(
ev,
() => {
clearTimeout(this.timer);
this.stopAnimation();
this.timer = undefined;
},
{ passive: true }
);
}
);
}
public bind(element: ActionHandlerElement, options): void {
if (element.actionHandler) {
return;
}
element.actionHandler = true;
element.addEventListener('contextmenu', (ev: Event) => {
const e = ev || window.event;
if (e.preventDefault) {
e.preventDefault();
}
if (e.stopPropagation) {
e.stopPropagation();
}
e.cancelBubble = true;
e.returnValue = false;
return false;
});
const start = (ev: Event): void => {
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);
};
const end = (ev: Event): void => {
// Prevent mouse event if touch event
ev.preventDefault();
if (['touchend', 'touchcancel'].includes(ev.type) && this.timer === undefined) {
return;
}
clearTimeout(this.timer);
this.stopAnimation();
this.timer = undefined;
if (this.held) {
fireEvent(element, 'action', { action: 'hold' });
} else if (options.hasDoubleClick) {
if ((ev.type === 'click' && (ev as MouseEvent).detail < 2) || !this.dblClickTimeout) {
this.dblClickTimeout = window.setTimeout(() => {
this.dblClickTimeout = undefined;
fireEvent(element, 'action', { action: 'tap' });
}, 250);
} else {
clearTimeout(this.dblClickTimeout);
this.dblClickTimeout = undefined;
fireEvent(element, 'action', { action: 'double_tap' });
}
} else {
fireEvent(element, 'action', { action: 'tap' });
}
};
const handleEnter = (ev: KeyboardEvent): void => {
if (ev.keyCode !== 13) {
return;
}
end(ev);
};
element.addEventListener('touchstart', start, { passive: true });
element.addEventListener('touchend', end);
element.addEventListener('touchcancel', end);
element.addEventListener('mousedown', start, { passive: true });
element.addEventListener('click', end);
element.addEventListener('keyup', handleEnter);
}
private startAnimation(x: number, y: number): void {
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(): void {
this.ripple.active = false;
this.ripple.disabled = true;
this.style.display = 'none';
}
}
// TODO You need to replace all instances of "action-handler-boilerplate" with "action-handler-<your card name>"
customElements.define('action-handler-boilerplate', ActionHandler);
const getActionHandler = (): ActionHandler => {
const body = document.body;
if (body.querySelector('action-handler-boilerplate')) {
return body.querySelector('action-handler-boilerplate') as ActionHandler;
}
const actionhandler = document.createElement('action-handler-boilerplate');
body.appendChild(actionhandler);
return actionhandler as ActionHandler;
};
export const actionHandlerBind = (
element: ActionHandlerElement,
options?: ActionHandlerOptions
): void => {
const actionhandler: ActionHandler = getActionHandler();
if (!actionhandler) {
return;
}
actionhandler.bind(element, options);
};
export const actionHandler = directive(
class extends Directive {
update(part: AttributePart, [options]: DirectiveParameters<this>) {
actionHandlerBind(part.element as ActionHandlerElement, options);
return noChange;
}
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
render(_options?: ActionHandlerOptions) {}
}
);
+1 -22
View File
@@ -1,7 +1,6 @@
import { LovelaceCard } from 'custom-card-helpers';
import { HassEntity } from 'home-assistant-js-websocket';
import { html, TemplateResult } from 'lit';
import { TimePickerCardConfig, TimePickerHideConfig } from './types';
import { TimePickerCardConfig } from './types';
export class Partial {
static error(error: string, origConfig: TimePickerCardConfig): TemplateResult {
@@ -14,24 +13,4 @@ export class Partial {
return html`${errorCard}`;
}
static headerName(title: string): TemplateResult {
return html`<div class="time-picker-header">${title}</div>`;
}
static nestedName(name: string, entity: HassEntity, hide?: TimePickerHideConfig): TemplateResult {
const icon = html`<state-badge .stateObj=${entity}></state-badge>`;
const label = html`<div class="entity-name-inside">${name}</div>`;
const visibleElements = [
{ show: !hide?.icon, value: icon },
{ show: !hide?.name, value: label },
]
.filter(({ show }) => show)
.map(({ value }) => value);
return html`${visibleElements}`;
// return hideIcon ? label : html`${icon}${label}`;
}
}
+71 -4
View File
@@ -1,8 +1,16 @@
import { computeDomain, HomeAssistant, LovelaceCard } from 'custom-card-helpers';
import {
ActionHandlerEvent,
computeDomain,
handleAction,
hasAction,
HomeAssistant,
LovelaceCard,
} from 'custom-card-helpers';
import { HassEntity } from 'home-assistant-js-websocket';
import { css, CSSResult, html, LitElement, TemplateResult } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { ClassInfo, classMap } from 'lit/directives/class-map.js';
import { actionHandler } from './action-handler-directive';
import './components/time-period.component';
import './components/time-unit.component';
import {
@@ -18,7 +26,7 @@ import { Minute } from './models/minute';
import { Second } from './models/second';
import { Time } from './models/time';
import { Partial } from './partials';
import { Layout, Period, TimePickerCardConfig } from './types';
import { Layout, Period, TimePickerCardConfig, TimePickerHideConfig } from './types';
console.info(
`%c TIME-PICKER-CARD \n%c Version ${CARD_VERSION} `,
@@ -97,6 +105,58 @@ export class TimePickerCard extends LitElement implements LovelaceCard {
};
}
private handleAction(ev: ActionHandlerEvent) {
handleAction(this, this.hass, this.config, ev.detail.action!);
}
private renderHeaderName(title: string): TemplateResult {
return html`<div
class="time-picker-header"
@action=${this.handleAction}
.actionHandler="${actionHandler({
hasHold: hasAction(this.config.hold_action),
hasDoubleClick: hasAction(this.config.double_tap_action),
})}"
>
${title}
</div>`;
}
private renderNestedName(
name: string,
entity: HassEntity,
hide?: TimePickerHideConfig
): TemplateResult {
const icon = html`<state-badge
class="entity-icon"
.stateObj=${entity}
@action=${this.handleAction}
.actionHandler="${actionHandler({
hasHold: hasAction(this.config.hold_action),
hasDoubleClick: hasAction(this.config.double_tap_action),
})}"
></state-badge>`;
const label = html`<div
class="entity-name-inside"
@action=${this.handleAction}
.actionHandler="${actionHandler({
hasHold: hasAction(this.config!.hold_action),
hasDoubleClick: hasAction(this.config.double_tap_action),
})}"
>
${name}
</div>`;
const visibleElements = [
{ element: icon, visible: !hide?.icon },
{ element: label, visible: !hide?.name },
]
.filter(({ visible }) => visible)
.map(({ element }) => element);
return html`${visibleElements}`;
}
render(): TemplateResult | null {
if (!this.entity) {
return Partial.error('Entity not found', this.config);
@@ -122,9 +182,11 @@ export class TimePickerCard extends LitElement implements LovelaceCard {
return html`
<ha-card class=${classMap(this.haCardClass)}>
${this.hasNameInHeader ? Partial.headerName(this.name!) : ''}
${this.hasNameInHeader ? this.renderHeaderName(this.name!) : ''}
<div class=${classMap(this.rowClass)}>
${this.hasNameInside ? Partial.nestedName(this.name!, this.entity, this.config.hide) : ''}
${this.hasNameInside
? this.renderNestedName(this.name!, this.entity, this.config.hide)
: ''}
<div class=${classMap(this.contentClass)}>
<time-unit
@@ -281,8 +343,13 @@ export class TimePickerCard extends LitElement implements LovelaceCard {
justify-content: flex-end;
}
.entity-icon {
cursor: pointer;
}
.entity-name-inside {
margin-left: 16px;
cursor: pointer;
}
`;
}
+4 -1
View File
@@ -1,4 +1,4 @@
import { LovelaceCardConfig } from 'custom-card-helpers';
import { ActionConfig, LovelaceCardConfig } from 'custom-card-helpers';
export interface TimePickerCardConfig extends LovelaceCardConfig {
entity: string;
@@ -10,6 +10,9 @@ export interface TimePickerCardConfig extends LovelaceCardConfig {
second_step?: number;
layout?: TimePickerLayoutConfig;
hide?: TimePickerHideConfig;
tap_action?: ActionConfig;
double_tap_action?: ActionConfig;
hold_action?: ActionConfig;
}
export type HourMode = 12 | 24 | undefined;