mirror of
https://github.com/GeorgeSG/lovelace-time-picker-card.git
synced 2026-11-03 03:38:45 +00:00
Add tap actions
This commit is contained in:
@@ -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` |
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user