import { AbstractDisplayInteractionManager } from "./AbstractDisplayInteractionManager"; import { PointF2D } from "../Common/DataObjects/PointF2D"; import { Dictionary } from "typescript-collections"; export class WebDisplayInteractionManager extends AbstractDisplayInteractionManager { protected osmdSheetMusicContainer: HTMLElement; protected fullOffsetLeft: number = 0; protected fullOffsetTop: number = 0; protected fullScrollTop: number = 0; protected fullScrollLeft: number = 0; //Using map instead of collections dictionary because map supports using objects as keys properly protected parentScrollMap: Map = new Map(); protected scrollCallbackMap: Map any> = new Map any>(); constructor(osmdContainer: HTMLElement) { super(); this.osmdSheetMusicContainer = osmdContainer; this.listenForInteractions(); } public get FullOffsetTop(): number { return this.fullOffsetTop; } public get FullScrollTop(): number { return this.fullScrollTop; } public get FullOffsetLeft(): number { return this.fullOffsetLeft; } public get FullScrollLeft(): number { return this.fullScrollLeft; } protected timeout: NodeJS.Timeout = undefined; protected static resizeCallback(entries: ResizeObserverEntry[]|HTMLElement[], self: WebDisplayInteractionManager): void { //debounce resize callback clearTimeout(self.timeout); self.timeout = setTimeout(()=> { self.fullOffsetLeft = 0; self.fullOffsetTop = 0; let nextOffsetParent: HTMLElement = self.osmdSheetMusicContainer; while (nextOffsetParent) { self.fullOffsetLeft += nextOffsetParent.offsetLeft; self.fullOffsetTop += nextOffsetParent.offsetTop; nextOffsetParent = nextOffsetParent.offsetParent as HTMLElement; } self.resizeEventListener(); self.deregisterScrollOffsets(); self.registerScrollOffsets(); }, 500); } protected registerScrollOffsets(): void { let nextScrollParent: HTMLElement = this.osmdSheetMusicContainer; this.fullScrollTop = 0; this.fullScrollLeft = 0; const self: WebDisplayInteractionManager = this; while(nextScrollParent && nextScrollParent !== document.documentElement){ this.parentScrollMap.set(nextScrollParent, [nextScrollParent.scrollTop, nextScrollParent.scrollLeft]); this.fullScrollLeft += nextScrollParent.scrollLeft; this.fullScrollTop += nextScrollParent.scrollTop; if(nextScrollParent.scrollHeight > nextScrollParent.clientHeight){ const nextScrollCallback: (this: HTMLElement, ev: Event) => any = function(scrollEvent: Event): void{ //@ts-ignore const currentScroll: number[] = self.parentScrollMap.get(this); const currentScrollTop: number = currentScroll[0]; const currentScrollLeft: number = currentScroll[1]; //@ts-ignore self.fullScrollTop = self.fullScrollTop - currentScrollTop + this.scrollTop; //@ts-ignore self.fullScrollLeft = self.fullScrollLeft - currentScrollLeft + this.scrollLeft; //@ts-ignore self.parentScrollMap.set(this, [this.scrollTop, this.scrollLeft]); }; this.scrollCallbackMap.set(nextScrollParent, nextScrollCallback); nextScrollParent.addEventListener("scroll", nextScrollCallback); } nextScrollParent = nextScrollParent.parentElement; } } protected deregisterScrollOffsets(): void { for(const key of this.scrollCallbackMap.keys()){ key.removeEventListener("scroll", this.scrollCallbackMap.get(key)); } this.scrollCallbackMap.clear(); } protected disposeResizeListener: Function; protected resizeObserver: ResizeObserver = undefined; protected initialize(): void { this.fullOffsetLeft = 0; this.fullOffsetTop = 0; let nextOffsetParent: HTMLElement = this.osmdSheetMusicContainer; const entries: HTMLElement[] = []; const self: WebDisplayInteractionManager = this; if(ResizeObserver){ this.resizeObserver = new ResizeObserver((observedElements: ResizeObserverEntry[]) => { WebDisplayInteractionManager.resizeCallback(observedElements, self); }); } while (nextOffsetParent) { this.fullOffsetLeft += nextOffsetParent.offsetLeft; this.fullOffsetTop += nextOffsetParent.offsetTop; if(!ResizeObserver){ entries.push(nextOffsetParent); } else { this.resizeObserver.observe(nextOffsetParent); } nextOffsetParent = nextOffsetParent.offsetParent as HTMLElement; } if(!ResizeObserver){ let resizeListener: (this: Window, ev: UIEvent) => any = (): void => { WebDisplayInteractionManager.resizeCallback(entries, self); }; //Resize observer not avail. on this browser, default to window event window.addEventListener("resize", resizeListener); this.disposeResizeListener = (): void => { window.removeEventListener("resize", resizeListener); resizeListener = undefined; }; } else { this.disposeResizeListener = (): void => { self.resizeObserver.disconnect(); self.resizeObserver = undefined; }; } self.registerScrollOffsets(); } protected dispose(): void { this.disposeResizeListener(); for(const eventName of this.EventCallbackMap.keys()){ const result: [HTMLElement|Document, EventListener] = this.EventCallbackMap.getValue(eventName); result[0].removeEventListener(eventName, result[1]); } this.EventCallbackMap.clear(); this.deregisterScrollOffsets(); this.scrollCallbackMap.clear(); this.parentScrollMap.clear(); } //TODO: Much of this pulled from annotations code. Once we get the two branches together, combine common code private isTouch(): boolean { if (("ontouchstart" in window) || (window as any).DocumentTouch) { return true; } // include the 'heartz' as a way to have a non matching MQ to help terminate the join // https://git.io/vznFH const prefixes: string[] = ["-webkit-", "-moz-", "-o-", "-ms-"]; const query: string = ["(", prefixes.join("touch-enabled),("), "heartz", ")"].join(""); return window.matchMedia(query).matches; } protected get downEventName(): string { return this.isTouch() ? "touchstart" : "mousedown"; } protected get moveEventName(): string { return this.isTouch() ? "touchmove" : "mousemove"; } protected EventCallbackMap: Dictionary = new Dictionary(); private listenForInteractions(): void { const downEvent: (clickEvent: MouseEvent | TouchEvent) => void = this.downEventListener.bind(this); const endTouchEvent: (clickEvent: TouchEvent) => void = this.touchEndEventListener.bind(this); const moveEvent: (clickEvent: MouseEvent | TouchEvent) => void = this.moveEventListener.bind(this); this.osmdSheetMusicContainer.addEventListener("mousedown", downEvent); this.osmdSheetMusicContainer.addEventListener("touchend", endTouchEvent); document.addEventListener(this.moveEventName, moveEvent); this.EventCallbackMap.setValue("mousedown", [this.osmdSheetMusicContainer, downEvent]); this.EventCallbackMap.setValue("touchend", [this.osmdSheetMusicContainer, endTouchEvent]); this.EventCallbackMap.setValue(this.moveEventName, [document, moveEvent]); } //Millis of how long is valid for the next click of a double click private readonly DOUBLE_CLICK_WINDOW: number = 200; private clickTimeout: NodeJS.Timeout; private lastClick: number = 0; private downEventListener(clickEvent: MouseEvent | TouchEvent): void { //clickEvent.preventDefault(); const currentTime: number = new Date().getTime(); const clickLength: number = currentTime - this.lastClick; clearTimeout(this.clickTimeout); let x: number = 0; let y: number = 0; if (this.isTouch() && clickEvent instanceof TouchEvent) { x = clickEvent.touches[0].pageX; y = clickEvent.touches[0].pageY; } else if (clickEvent instanceof MouseEvent) { x = clickEvent.pageX; y = clickEvent.pageY; } const clickMinusOffset: PointF2D = this.getOffsetCoordinates(x, y); if (clickLength < this.DOUBLE_CLICK_WINDOW && clickLength > 0) { //double click this.doubleClick(clickMinusOffset.x, clickMinusOffset.y); } else { const self: WebDisplayInteractionManager = this; this.clickTimeout = setTimeout(function(): void { clearTimeout(this.clickTimeout); if (self.isTouch()) { self.touchDown(clickMinusOffset.x, clickMinusOffset.y, undefined); } else { self.click(clickMinusOffset.x, clickMinusOffset.y); } }, this.DOUBLE_CLICK_WINDOW); } this.lastClick = currentTime; } private moveEventListener(mouseMoveEvent: MouseEvent | TouchEvent): void { let x: number = 0; let y: number = 0; if (this.isTouch() && mouseMoveEvent instanceof TouchEvent) { let touch: Touch = undefined; if(mouseMoveEvent.touches && mouseMoveEvent.touches.length > 0){ touch = mouseMoveEvent.touches[0]; } else if(mouseMoveEvent.changedTouches && mouseMoveEvent.changedTouches.length > 0){ touch = mouseMoveEvent.changedTouches[0]; } x = touch?.clientX; y = touch?.clientY; } else if (mouseMoveEvent instanceof MouseEvent) { x = mouseMoveEvent.clientX; y = mouseMoveEvent.clientY; } const clickMinusOffset: PointF2D = this.getOffsetCoordinates(x, y); this.move(clickMinusOffset.x, clickMinusOffset.y); } private touchEndEventListener(clickEvent: TouchEvent): void { let touch: Touch = undefined; if(clickEvent.touches && clickEvent.touches.length > 0){ touch = clickEvent.touches[0]; } else if(clickEvent.changedTouches && clickEvent.changedTouches.length > 0){ touch = clickEvent.changedTouches[0]; } const touchMinusOffset: PointF2D = this.getOffsetCoordinates(touch?.pageX, touch?.pageY); this.touchUp(touchMinusOffset.x, touchMinusOffset.y); } private resizeEventListener(): void { this.displaySizeChanged(this.osmdSheetMusicContainer.clientWidth, this.osmdSheetMusicContainer.clientHeight); } private getOffsetCoordinates(clickX: number, clickY: number): PointF2D { const sheetX: number = clickX - this.fullOffsetLeft + this.fullScrollLeft; const sheetY: number = clickY - this.fullOffsetTop + this.fullScrollTop; return new PointF2D(sheetX, sheetY); } }