* @fileoverview Handle tap on 'CHROME_ANNOTATION' elements.
*/
import type {TextDecoration} from '//ios/web/annotations/resources/text_decoration.js';
import type {TaskTimer} from '//ios/web/annotations/resources/text_tasks.js';
import {LiveTaskTimer} from '//ios/web/annotations/resources/text_tasks.js';
export interface AnnotationsTapConsumer {
(element: HTMLElement, cancel: boolean): void;
}
export const DOM_MUTATION_DELAY_MS = 300;
class MutationsTracker {
hasMutations = false;
private mutationObserver: MutationObserver;
private mutationExtendId = 0;
private mutationCallback = (mutationList: MutationRecord[]) => {
for (const mutation of mutationList) {
if (this.strict ||
mutation.target.contains(this.initialEvent.target as Node)) {
this.hasMutations = true;
this.mutationObserver?.disconnect();
return;
}
}
};
constructor(
private readonly initialEvent: Event, root: Element,
private taskTimer: TaskTimer = new LiveTaskTimer(),
private strict = true) {
this.mutationObserver = new MutationObserver(this.mutationCallback);
this.mutationObserver.observe(
root, {attributes: false, childList: true, subtree: true});
}
hasPreventativeActivity(event: Event): boolean {
return event !== this.initialEvent || event.defaultPrevented ||
this.hasMutations;
}
extendObservation(then: Function, delayMs: number): void {
this.strict = false;
if (this.mutationExtendId) {
this.taskTimer.clear(this.mutationExtendId);
}
this.mutationExtendId = this.taskTimer.reset(then, delayMs);
}
stopObserving(): void {
if (this.mutationExtendId) {
this.taskTimer.clear(this.mutationExtendId);
}
this.mutationExtendId = 0;
this.mutationObserver?.disconnect();
}
updateForTesting(): void {
this.mutationCallback(this.mutationObserver.takeRecords());
}
}
export class TextClick {
private mutationObserver: MutationsTracker|null = null;
constructor(
private root: Element, private consumer: AnnotationsTapConsumer,
private decorationsProvider: () => Map<number, TextDecoration>| undefined,
private taskTimer: TaskTimer = new LiveTaskTimer(),
private mutationCheckDelay = DOM_MUTATION_DELAY_MS,
private annotationForTest: Element|null = null) {}
start(): void {
this.root.addEventListener('click', this.onClick, {capture: true});
this.root.addEventListener('click', this.onClick);
}
stop(): void {
this.root.removeEventListener('click', this.onClick, {capture: true});
this.root.removeEventListener('click', this.onClick);
this.cancelObserver();
}
updateForTesting(): void {
this.mutationObserver?.updateForTesting();
}
annotationForTesting(annotation: Element): void {
this.annotationForTest = annotation;
}
private onClick = (event: Event) => {
this.handleTopTap(event as PointerEvent);
};
private cancelObserver(): void {
this.mutationObserver?.stopObserving();
this.mutationObserver = null;
}
private toggleDecorationsPointerEvents(value: string): void {
this.decorationsProvider()?.forEach((decoration) => {
if (!decoration.live) {
return;
}
decoration.replacements.forEach((replacement) => {
if (replacement instanceof HTMLElement) {
replacement.style.pointerEvents = value;
}
});
});
}
private handleTopTap(event: PointerEvent): void {
let annotation = this.annotationForTest;
if (!annotation) {
this.toggleDecorationsPointerEvents('all');
annotation = document.elementFromPoint(event.clientX, event.clientY);
this.toggleDecorationsPointerEvents('none');
}
if (annotation instanceof HTMLElement &&
annotation.tagName === 'CHROME_ANNOTATION') {
if (event.eventPhase === Event.CAPTURING_PHASE) {
this.cancelObserver();
this.mutationObserver =
new MutationsTracker(event, this.root, this.taskTimer);
} else if (this.mutationObserver) {
if (this.mutationObserver.hasPreventativeActivity(event)) {
this.consumer(annotation, true);
this.cancelObserver();
} else {
this.mutationObserver.extendObservation(() => {
if (this.mutationObserver) {
this.consumer(annotation, this.mutationObserver.hasMutations);
this.cancelObserver();
}
}, this.mutationCheckDelay);
}
}
} else {
this.cancelObserver();
}
}
}