Attempt to fix slowness on IOS14

This commit is contained in:
Jérôme Wiedemann 2020-11-05 01:26:15 +00:00
parent 32e646d586
commit af2239cb9a
3 changed files with 239 additions and 85 deletions

View File

@ -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';
}

View File

@ -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();

109
src/deep-equal.ts Normal file
View File

@ -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;
};