Attempt to fix slowness on IOS14
This commit is contained in:
parent
32e646d586
commit
af2239cb9a
|
@ -3,6 +3,7 @@ import { directive, PropertyPart } from 'lit-html';
|
|||
// tslint:disable-next-line
|
||||
import { Ripple } from '@material/mwc-ripple';
|
||||
import { myFireEvent } from './my-fire-event';
|
||||
import { deepEqual } from './deep-equal';
|
||||
|
||||
const isTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
|
||||
|
||||
|
@ -10,24 +11,34 @@ interface ActionHandler extends HTMLElement {
|
|||
holdTime: number;
|
||||
bind(element: Element, options): void;
|
||||
}
|
||||
interface ActionHandlerElement extends HTMLElement {
|
||||
actionHandler?: boolean;
|
||||
|
||||
export interface ActionHandlerDetail {
|
||||
action: 'hold' | 'tap' | 'double_tap';
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
action: ActionHandlerDetail;
|
||||
}
|
||||
}
|
||||
|
||||
interface ActionHandlerOptions {
|
||||
export interface ActionHandlerOptions {
|
||||
hasHold?: boolean;
|
||||
hasDoubleClick?: boolean;
|
||||
disabled?: boolean;
|
||||
repeat?: number;
|
||||
}
|
||||
|
||||
interface ActionHandlerDetail {
|
||||
action: string;
|
||||
interface ActionHandlerElement extends HTMLElement {
|
||||
actionHandler?: {
|
||||
options: ActionHandlerOptions;
|
||||
start?: (ev: Event) => void;
|
||||
end?: (ev: Event) => void;
|
||||
handleEnter?: (ev: KeyboardEvent) => void;
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'action-handler': ActionHandler;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
action: ActionHandlerDetail;
|
||||
}
|
||||
}
|
||||
|
||||
class ActionHandler extends HTMLElement implements ActionHandler {
|
||||
|
@ -39,6 +50,8 @@ class ActionHandler extends HTMLElement implements ActionHandler {
|
|||
|
||||
protected held = false;
|
||||
|
||||
private cancelled = false;
|
||||
|
||||
private dblClickTimeout?: number;
|
||||
|
||||
private repeatTimeout: NodeJS.Timeout | undefined;
|
||||
|
@ -57,6 +70,7 @@ class ActionHandler extends HTMLElement implements ActionHandler {
|
|||
height: isTouch ? '100px' : '50px',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
pointerEvents: 'none',
|
||||
zIndex: '999',
|
||||
});
|
||||
|
||||
this.appendChild(this.ripple);
|
||||
|
@ -66,36 +80,59 @@ class ActionHandler extends HTMLElement implements ActionHandler {
|
|||
document.addEventListener(
|
||||
ev,
|
||||
() => {
|
||||
clearTimeout(this.timer);
|
||||
this.stopAnimation();
|
||||
this.timer = undefined;
|
||||
this.cancelled = true;
|
||||
if (this.timer) {
|
||||
this.stopAnimation();
|
||||
clearTimeout(this.timer);
|
||||
this.timer = undefined;
|
||||
if (this.isRepeating && this.repeatTimeout) {
|
||||
clearInterval(this.repeatTimeout);
|
||||
this.isRepeating = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public bind(element: ActionHandlerElement, options): void {
|
||||
if (element.actionHandler) {
|
||||
public bind(element: ActionHandlerElement, options: ActionHandlerOptions): void {
|
||||
if (element.actionHandler && deepEqual(options, element.actionHandler.options)) {
|
||||
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;
|
||||
});
|
||||
if (element.actionHandler) {
|
||||
element.removeEventListener('touchstart', element.actionHandler.start!);
|
||||
element.removeEventListener('touchend', element.actionHandler.end!);
|
||||
element.removeEventListener('touchcancel', element.actionHandler.end!);
|
||||
|
||||
const start = (ev: Event): void => {
|
||||
this.held = false;
|
||||
element.removeEventListener('mousedown', element.actionHandler.start!);
|
||||
element.removeEventListener('click', element.actionHandler.end!);
|
||||
|
||||
element.removeEventListener('keyup', element.actionHandler.handleEnter!);
|
||||
} else {
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
element.actionHandler = { options };
|
||||
|
||||
if (options.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
element.actionHandler.start = (ev: Event) => {
|
||||
this.cancelled = false;
|
||||
let x;
|
||||
let y;
|
||||
if ((ev as TouchEvent).touches) {
|
||||
|
@ -106,71 +143,83 @@ class ActionHandler extends HTMLElement implements ActionHandler {
|
|||
y = (ev as MouseEvent).pageY;
|
||||
}
|
||||
|
||||
this.timer = window.setTimeout(() => {
|
||||
this.startAnimation(x, y);
|
||||
this.held = true;
|
||||
if (options.repeat && !this.isRepeating) {
|
||||
this.isRepeating = true;
|
||||
this.repeatTimeout = setInterval(() => {
|
||||
myFireEvent(element, 'action', { action: 'hold' });
|
||||
}, options.repeat);
|
||||
}
|
||||
}, this.holdTime);
|
||||
};
|
||||
|
||||
const handleEnter = (ev: KeyboardEvent): void => {
|
||||
if (ev.keyCode !== 13) {
|
||||
return;
|
||||
if (options.hasHold) {
|
||||
this.held = false;
|
||||
this.timer = window.setTimeout(() => {
|
||||
this.startAnimation(x, y);
|
||||
this.held = true;
|
||||
if (options.repeat && !this.isRepeating) {
|
||||
this.isRepeating = true;
|
||||
this.repeatTimeout = setInterval(() => {
|
||||
myFireEvent(element, 'action', { action: 'hold' });
|
||||
}, options.repeat);
|
||||
}
|
||||
}, this.holdTime);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
end(ev);
|
||||
};
|
||||
|
||||
const end = (ev: Event): void => {
|
||||
// Prevent mouse event if touch event
|
||||
ev.preventDefault();
|
||||
if (['touchend', 'touchcancel'].includes(ev.type) && this.timer === undefined) {
|
||||
element.actionHandler.end = (ev: Event) => {
|
||||
// Don't respond when moved or scrolled while touch
|
||||
if (['touchend', 'touchcancel'].includes(ev.type) && this.cancelled) {
|
||||
if (this.isRepeating && this.repeatTimeout) {
|
||||
clearInterval(this.repeatTimeout);
|
||||
this.isRepeating = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
clearTimeout(this.timer);
|
||||
if (this.isRepeating && this.repeatTimeout) {
|
||||
clearInterval(this.repeatTimeout);
|
||||
const target = ev.target as HTMLElement;
|
||||
// Prevent mouse event if touch event
|
||||
if (ev.cancelable) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
this.isRepeating = false;
|
||||
this.stopAnimation();
|
||||
this.timer = undefined;
|
||||
if (this.held) {
|
||||
if (options.hasHold) {
|
||||
clearTimeout(this.timer);
|
||||
if (this.isRepeating && this.repeatTimeout) {
|
||||
clearInterval(this.repeatTimeout);
|
||||
}
|
||||
this.isRepeating = false;
|
||||
this.stopAnimation();
|
||||
this.timer = undefined;
|
||||
}
|
||||
if (options.hasHold && this.held) {
|
||||
if (!options.repeat) {
|
||||
myFireEvent(element, 'action', { action: 'hold' });
|
||||
myFireEvent(target, '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;
|
||||
myFireEvent(element, 'action', { action: 'tap' });
|
||||
myFireEvent(target, 'action', { action: 'tap' });
|
||||
}, 250);
|
||||
} else {
|
||||
clearTimeout(this.dblClickTimeout);
|
||||
this.dblClickTimeout = undefined;
|
||||
myFireEvent(element, 'action', { action: 'double_tap' });
|
||||
myFireEvent(target, 'action', { action: 'double_tap' });
|
||||
}
|
||||
} else {
|
||||
myFireEvent(element, 'action', { action: 'tap' });
|
||||
myFireEvent(target, 'action', { action: 'tap' });
|
||||
}
|
||||
};
|
||||
|
||||
element.addEventListener('touchstart', start, { passive: true });
|
||||
element.addEventListener('touchend', end);
|
||||
element.addEventListener('touchcancel', end);
|
||||
element.actionHandler.handleEnter = (ev: KeyboardEvent) => {
|
||||
if (ev.keyCode !== 13) {
|
||||
return;
|
||||
}
|
||||
(ev.currentTarget as ActionHandlerElement).actionHandler!.end!(ev);
|
||||
};
|
||||
|
||||
element.addEventListener('mousedown', start, { passive: true });
|
||||
element.addEventListener('click', end);
|
||||
element.addEventListener('touchstart', element.actionHandler.start, {
|
||||
passive: true,
|
||||
});
|
||||
element.addEventListener('touchend', element.actionHandler.end);
|
||||
element.addEventListener('touchcancel', element.actionHandler.end);
|
||||
|
||||
element.addEventListener('keyup', handleEnter);
|
||||
element.addEventListener('mousedown', element.actionHandler.start, {
|
||||
passive: true,
|
||||
});
|
||||
element.addEventListener('click', element.actionHandler.end);
|
||||
|
||||
element.addEventListener('keyup', element.actionHandler.handleEnter);
|
||||
}
|
||||
|
||||
private startAnimation(x: number, y: number): void {
|
||||
|
@ -180,16 +229,12 @@ class ActionHandler extends HTMLElement implements ActionHandler {
|
|||
display: null,
|
||||
});
|
||||
this.ripple.disabled = false;
|
||||
this.ripple.startPress ? this.ripple.startPress() : (((this.ripple as unknown) as any).active = true); // = true;
|
||||
this.ripple.startPress();
|
||||
this.ripple.unbounded = true;
|
||||
}
|
||||
|
||||
private stopAnimation(): void {
|
||||
if (this.ripple.endPress) {
|
||||
this.ripple.endPress();
|
||||
} else {
|
||||
((this.ripple as unknown) as any).active = false;
|
||||
}
|
||||
this.ripple.endPress();
|
||||
this.ripple.disabled = true;
|
||||
this.style.display = 'none';
|
||||
}
|
||||
|
|
|
@ -1110,13 +1110,13 @@ class ButtonCard extends LitElement {
|
|||
if (ev.detail && ev.detail.action) {
|
||||
switch (ev.detail.action) {
|
||||
case 'tap':
|
||||
this._handleTap(ev);
|
||||
this._handleTap();
|
||||
break;
|
||||
case 'hold':
|
||||
this._handleHold(ev);
|
||||
this._handleHold();
|
||||
break;
|
||||
case 'double_tap':
|
||||
this._handleDblTap(ev);
|
||||
this._handleDblTap();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@ -1124,26 +1124,26 @@ class ButtonCard extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
private _handleTap(ev): void {
|
||||
const config = ev.target.config;
|
||||
private _handleTap(): void {
|
||||
const config = this._config;
|
||||
if (!config) return;
|
||||
handleClick(this, this._hass!, this._evalActions(config, 'tap_action'), false, false);
|
||||
}
|
||||
|
||||
private _handleHold(ev): void {
|
||||
const config = ev.target.config;
|
||||
private _handleHold(): void {
|
||||
const config = this._config;
|
||||
if (!config) return;
|
||||
handleClick(this, this._hass!, this._evalActions(config, 'hold_action'), true, false);
|
||||
}
|
||||
|
||||
private _handleDblTap(ev): void {
|
||||
const config = ev.target.config;
|
||||
private _handleDblTap(): void {
|
||||
const config = this._config;
|
||||
if (!config) return;
|
||||
handleClick(this, this._hass!, this._evalActions(config, 'double_tap_action'), false, true);
|
||||
}
|
||||
|
||||
private _handleUnlockType(ev): void {
|
||||
const config = ev.target.config as ButtonCardConfig;
|
||||
const config = this._config as ButtonCardConfig;
|
||||
if (!config) return;
|
||||
if (config.lock.unlock === ev.detail.action) {
|
||||
this._handleLock();
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
// From https://github.com/epoberezkin/fast-deep-equal
|
||||
// MIT License - Copyright (c) 2017 Evgeny Poberezkin
|
||||
export const deepEqual = (a: any, b: any): boolean => {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a && b && typeof a === 'object' && typeof b === 'object') {
|
||||
if (a.constructor !== b.constructor) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let i: number | [any, any];
|
||||
let length: number;
|
||||
if (Array.isArray(a)) {
|
||||
length = a.length;
|
||||
if (length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
for (i = length; i-- !== 0; ) {
|
||||
if (!deepEqual(a[i], b[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a instanceof Map && b instanceof Map) {
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
for (i of a.entries()) {
|
||||
if (!b.has(i[0])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (i of a.entries()) {
|
||||
if (!deepEqual(i[1], b.get(i[0]))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a instanceof Set && b instanceof Set) {
|
||||
if (a.size !== b.size) {
|
||||
return false;
|
||||
}
|
||||
for (i of a.entries()) {
|
||||
if (!b.has(i[0])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) {
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
length = a.length;
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
if (length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
for (i = length; i-- !== 0; ) {
|
||||
if (a[i] !== b[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a.constructor === RegExp) {
|
||||
return a.source === b.source && a.flags === b.flags;
|
||||
}
|
||||
if (a.valueOf !== Object.prototype.valueOf) {
|
||||
return a.valueOf() === b.valueOf();
|
||||
}
|
||||
if (a.toString !== Object.prototype.toString) {
|
||||
return a.toString() === b.toString();
|
||||
}
|
||||
|
||||
const keys = Object.keys(a);
|
||||
length = keys.length;
|
||||
if (length !== Object.keys(b).length) {
|
||||
return false;
|
||||
}
|
||||
for (i = length; i-- !== 0; ) {
|
||||
if (!Object.prototype.hasOwnProperty.call(b, keys[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (i = length; i-- !== 0; ) {
|
||||
const key = keys[i];
|
||||
|
||||
if (!deepEqual(a[key], b[key])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// true if both NaN, false otherwise
|
||||
// eslint-disable-next-line no-self-compare
|
||||
return a !== a && b !== b;
|
||||
};
|
Loading…
Reference in New Issue