Files
advanced-camera-card/src/action-handler-directive.ts
T

210 lines
6.2 KiB
TypeScript

import { noChange } from 'lit';
import {
AttributePart,
Directive,
DirectiveParameters,
directive,
} from 'lit/directive.js';
import { fireHASSEvent } from './ha/fire-hass-event.js';
import { ActionHandlerDetail, ActionHandlerOptions } from './ha/types.js';
import { stopEventFromActivatingCardWideActions } from './utils/action.js';
import { Timer } from './utils/timer.js';
export interface ActionHandlerInterface extends HTMLElement {
holdTime: number;
connectedCallback(): void;
bind(element: Element, options): void;
}
interface ActionHandlerElement extends HTMLElement {
actionHandlerOptions?: AdvancedCameraCardActionHandlerOptions;
}
interface AdvancedCameraCardActionHandlerOptions extends ActionHandlerOptions {
allowPropagation?: boolean;
}
class ActionHandler extends HTMLElement implements ActionHandlerInterface {
public holdTime = 0.4;
private holdTimer = new Timer();
private doubleClickTimer = new Timer();
private held = false;
private started = false;
public connectedCallback(): void {
['mouseup', 'mousewheel', 'scroll', 'touchcancel', 'wheel'].forEach((ev) => {
document.addEventListener(
ev,
() => {
this.holdTimer.stop();
},
{ passive: true },
);
});
}
public bind(
element: ActionHandlerElement,
options?: AdvancedCameraCardActionHandlerOptions,
): void {
if (element.actionHandlerOptions) {
// Reset the options on an existing actionHandler.
element.actionHandlerOptions = options;
return;
}
element.actionHandlerOptions = options;
element.addEventListener('contextmenu', (ev: Event) => {
ev.preventDefault();
ev.stopPropagation();
});
const start = (): void => {
this.held = false;
this.holdTimer.start(this.holdTime, () => {
this.held = true;
});
// Without this check we get double start_tap events from touchstart and
// mousedown events (on Android).
if (!this.started) {
this.started = true;
fireHASSEvent(element, 'action', { action: 'start_tap' });
}
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const endTap = (_ev: Event): void => {
this.holdTimer.stop();
if (this.started) {
this.started = false;
fireHASSEvent(element, 'action', { action: 'end_tap' });
}
};
const end = (ev: Event): void => {
const options = element.actionHandlerOptions;
if (!options?.allowPropagation) {
// This will ensure only 1 actionHandler is invoked for a given interaction.
stopEventFromActivatingCardWideActions(ev);
}
if (
['touchend', 'touchcancel'].includes(ev.type) &&
// This action handler by default relies on synthetic click events for
// touch devices, in order to ensure that embedded cards (e.g. WebRTC)
// can use stock click handlers. The exception is for hold events.
!this.held
) {
return;
}
endTap(ev);
if (options?.hasHold && this.held) {
fireHASSEvent(element, 'action', { action: 'hold' });
} else if (options?.hasDoubleClick) {
if (
(ev.type === 'click' && (ev as MouseEvent).detail < 2) ||
!this.doubleClickTimer.isRunning()
) {
this.doubleClickTimer.start(0.25, () =>
fireHASSEvent(element, 'action', { action: 'tap' }),
);
} else {
this.doubleClickTimer.stop();
fireHASSEvent(element, 'action', { action: 'double_tap' });
}
} else {
fireHASSEvent(element, 'action', { action: 'tap' });
}
};
const handleEnter = (ev: KeyboardEvent): void => {
if (ev.key === 'Enter') {
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);
// If the mouse leaves the element, this is considered the end of the interaction.
element.addEventListener('mouseleave', endTap);
}
}
customElements.define('action-handler-advanced-camera-card', ActionHandler);
const getActionHandler = (): ActionHandler => {
const body = document.body;
if (body.querySelector('action-handler-advanced-camera-card')) {
return body.querySelector('action-handler-advanced-camera-card') as ActionHandler;
}
const actionhandler = document.createElement('action-handler-advanced-camera-card');
body.appendChild(actionhandler);
return actionhandler as ActionHandler;
};
const actionHandlerBind = (
element: ActionHandlerElement,
options?: AdvancedCameraCardActionHandlerOptions,
): void => {
getActionHandler().bind(element, options);
};
export const actionHandler = directive(
class extends Directive {
update(part: AttributePart, [options]: DirectiveParameters<this>) {
actionHandlerBind(part.element as ActionHandlerElement, options);
return noChange;
}
// istanbul ignore next -- @preserve Required by Lit Directive API but never called (update() is used instead)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
render(_options?: AdvancedCameraCardActionHandlerOptions) {}
},
);
export interface ActionEventTarget extends EventTarget {
addEventListener(
event: '@action',
listener: (this: ActionEventTarget, ev: CustomEvent<ActionHandlerDetail>) => void,
options?: AddEventListenerOptions | boolean,
): void;
addEventListener(
type: string,
callback: EventListenerOrEventListenerObject,
options?: AddEventListenerOptions | boolean,
): void;
removeEventListener(
event: '@action',
listener: (this: ActionEventTarget, ev: CustomEvent<ActionHandlerDetail>) => void,
options?: boolean | EventListenerOptions,
): void;
removeEventListener(
type: string,
callback: EventListenerOrEventListenerObject,
options?: boolean | EventListenerOptions,
): void;
}
declare global {
interface HTMLElementTagNameMap {
'action-handler-advanced-camera-card': ActionHandler;
}
interface HASSDomEvents {
action: ActionHandlerDetail;
}
}