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:
@@ -98,17 +98,20 @@ layout:
|
|||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
| Name | Type | Requirement | Description | Default |
|
| Name | Type | Requirement | Description | Default |
|
||||||
| ----------- | ------------ | ------------ | --------------------------------------------------------------------------------------------------------- | ------------------------ |
|
| ----------------- | ------------ | ------------ | --------------------------------------------------------------------------------------------------------- | ------------------------ |
|
||||||
| type | string | **Required** | `custom:time-picker-card` | |
|
| 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` | |
|
| 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` |
|
| name | string | **Optional** | Card name | Entity's `friendly_name` |
|
||||||
| link_values | boolean | **Optional** | If enabled, will change hour when minutes overflow. E.g. will go from 11:55 to 12:00, instead of 11:00 | `false` |
|
| link_values | boolean | **Optional** | If enabled, will change hour when minutes overflow. E.g. will go from 11:55 to 12:00, instead of 11:00 | `false` |
|
||||||
| hour_mode | `12` or `24` | **Optional** | Hour format. If `12`, card will show AM/PM picker | `24` |
|
| hour_mode | `12` or `24` | **Optional** | Hour format. If `12`, card will show AM/PM picker | `24` |
|
||||||
| hour_step | number | **Optional** | Hour change when clicking arrows | `1` |
|
| hour_step | number | **Optional** | Hour change when clicking arrows | `1` |
|
||||||
| minute_step | number | **Optional** | Minute change when clicking arrows | `5` |
|
| minute_step | number | **Optional** | Minute change when clicking arrows | `5` |
|
||||||
| layout | object | **Optional** | Card Layout configuration | `none` |
|
| layout | object | **Optional** | Card Layout configuration | `none` |
|
||||||
| hide | object | **Optional** | Hide object | `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
|
### Layout Object
|
||||||
|
|
||||||
@@ -121,11 +124,11 @@ layout:
|
|||||||
|
|
||||||
### Hide Object
|
### Hide Object
|
||||||
|
|
||||||
| Name | Type | Requirement | Description | Default |
|
| Name | Type | Requirement | Description | Default |
|
||||||
| ------- | ------- | ------------ | ------------------- | ------- |
|
| ------- | ------- | ------------ | ------------------------------------------------- | ------- |
|
||||||
| name | boolean | **Optional** | Hides the card name | `false` |
|
| name | boolean | **Optional** | Hides the card name | `false` |
|
||||||
| icon | boolean | **Optional** | Hides the card icon (only with controls "inside") | `false` |
|
| icon | boolean | **Optional** | Hides the card icon (only with controls "inside") | `false` |
|
||||||
| seconds | boolean | **Optional** | Hides seconds input | `true` |
|
| seconds | boolean | **Optional** | Hides seconds input | `true` |
|
||||||
|
|
||||||
### Theme Variables
|
### Theme Variables
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,24 @@ views:
|
|||||||
name: Alarm time
|
name: Alarm time
|
||||||
hour_mode: 24
|
hour_mode: 24
|
||||||
hour_step: 2
|
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
|
- type: custom:time-picker-card
|
||||||
entity: input_datetime.test_date_time
|
entity: input_datetime.test_date_time
|
||||||
hour_mode: 12
|
hour_mode: 12
|
||||||
@@ -50,6 +68,17 @@ views:
|
|||||||
hide:
|
hide:
|
||||||
name: false
|
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
|
- title: embedded
|
||||||
path: embedded
|
path: embedded
|
||||||
cards:
|
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 { LovelaceCard } from 'custom-card-helpers';
|
||||||
import { HassEntity } from 'home-assistant-js-websocket';
|
|
||||||
import { html, TemplateResult } from 'lit';
|
import { html, TemplateResult } from 'lit';
|
||||||
import { TimePickerCardConfig, TimePickerHideConfig } from './types';
|
import { TimePickerCardConfig } from './types';
|
||||||
|
|
||||||
export class Partial {
|
export class Partial {
|
||||||
static error(error: string, origConfig: TimePickerCardConfig): TemplateResult {
|
static error(error: string, origConfig: TimePickerCardConfig): TemplateResult {
|
||||||
@@ -14,24 +13,4 @@ export class Partial {
|
|||||||
|
|
||||||
return html`${errorCard}`;
|
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 { HassEntity } from 'home-assistant-js-websocket';
|
||||||
import { css, CSSResult, html, LitElement, TemplateResult } from 'lit';
|
import { css, CSSResult, html, LitElement, TemplateResult } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { ClassInfo, classMap } from 'lit/directives/class-map.js';
|
import { ClassInfo, classMap } from 'lit/directives/class-map.js';
|
||||||
|
import { actionHandler } from './action-handler-directive';
|
||||||
import './components/time-period.component';
|
import './components/time-period.component';
|
||||||
import './components/time-unit.component';
|
import './components/time-unit.component';
|
||||||
import {
|
import {
|
||||||
@@ -18,7 +26,7 @@ import { Minute } from './models/minute';
|
|||||||
import { Second } from './models/second';
|
import { Second } from './models/second';
|
||||||
import { Time } from './models/time';
|
import { Time } from './models/time';
|
||||||
import { Partial } from './partials';
|
import { Partial } from './partials';
|
||||||
import { Layout, Period, TimePickerCardConfig } from './types';
|
import { Layout, Period, TimePickerCardConfig, TimePickerHideConfig } from './types';
|
||||||
|
|
||||||
console.info(
|
console.info(
|
||||||
`%c TIME-PICKER-CARD \n%c Version ${CARD_VERSION} `,
|
`%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 {
|
render(): TemplateResult | null {
|
||||||
if (!this.entity) {
|
if (!this.entity) {
|
||||||
return Partial.error('Entity not found', this.config);
|
return Partial.error('Entity not found', this.config);
|
||||||
@@ -122,9 +182,11 @@ export class TimePickerCard extends LitElement implements LovelaceCard {
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-card class=${classMap(this.haCardClass)}>
|
<ha-card class=${classMap(this.haCardClass)}>
|
||||||
${this.hasNameInHeader ? Partial.headerName(this.name!) : ''}
|
${this.hasNameInHeader ? this.renderHeaderName(this.name!) : ''}
|
||||||
<div class=${classMap(this.rowClass)}>
|
<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)}>
|
<div class=${classMap(this.contentClass)}>
|
||||||
<time-unit
|
<time-unit
|
||||||
@@ -281,8 +343,13 @@ export class TimePickerCard extends LitElement implements LovelaceCard {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.entity-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.entity-name-inside {
|
.entity-name-inside {
|
||||||
margin-left: 16px;
|
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 {
|
export interface TimePickerCardConfig extends LovelaceCardConfig {
|
||||||
entity: string;
|
entity: string;
|
||||||
@@ -10,6 +10,9 @@ export interface TimePickerCardConfig extends LovelaceCardConfig {
|
|||||||
second_step?: number;
|
second_step?: number;
|
||||||
layout?: TimePickerLayoutConfig;
|
layout?: TimePickerLayoutConfig;
|
||||||
hide?: TimePickerHideConfig;
|
hide?: TimePickerHideConfig;
|
||||||
|
tap_action?: ActionConfig;
|
||||||
|
double_tap_action?: ActionConfig;
|
||||||
|
hold_action?: ActionConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HourMode = 12 | 24 | undefined;
|
export type HourMode = 12 | 24 | undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user