WebDisplayInteractionManager.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. import { AbstractDisplayInteractionManager } from "./AbstractDisplayInteractionManager";
  2. import { PointF2D } from "../Common/DataObjects/PointF2D";
  3. import { Dictionary } from "typescript-collections";
  4. export class WebDisplayInteractionManager extends AbstractDisplayInteractionManager {
  5. protected osmdSheetMusicContainer: HTMLElement;
  6. protected fullOffsetLeft: number = 0;
  7. protected fullOffsetTop: number = 0;
  8. protected fullScrollTop: number = 0;
  9. protected fullScrollLeft: number = 0;
  10. //Using map instead of collections dictionary because map supports using objects as keys properly
  11. protected parentScrollMap: Map<HTMLElement, number[]> = new Map<HTMLElement, number[]>();
  12. protected scrollCallbackMap: Map<HTMLElement, (this: HTMLElement, ev: Event) => any> =
  13. new Map<HTMLElement, (this: HTMLElement, ev: Event) => any>();
  14. constructor(osmdContainer: HTMLElement) {
  15. super();
  16. this.osmdSheetMusicContainer = osmdContainer;
  17. this.listenForInteractions();
  18. }
  19. public get FullOffsetTop(): number {
  20. return this.fullOffsetTop;
  21. }
  22. public get FullScrollTop(): number {
  23. return this.fullScrollTop;
  24. }
  25. public get FullOffsetLeft(): number {
  26. return this.fullOffsetLeft;
  27. }
  28. public get FullScrollLeft(): number {
  29. return this.fullScrollLeft;
  30. }
  31. protected timeout: NodeJS.Timeout = undefined;
  32. protected static resizeCallback(entries: ResizeObserverEntry[]|HTMLElement[], self: WebDisplayInteractionManager): void {
  33. //debounce resize callback
  34. clearTimeout(self.timeout);
  35. self.timeout = setTimeout(()=> {
  36. self.fullOffsetLeft = 0;
  37. self.fullOffsetTop = 0;
  38. let nextOffsetParent: HTMLElement = self.osmdSheetMusicContainer;
  39. while (nextOffsetParent) {
  40. self.fullOffsetLeft += nextOffsetParent.offsetLeft;
  41. self.fullOffsetTop += nextOffsetParent.offsetTop;
  42. nextOffsetParent = nextOffsetParent.offsetParent as HTMLElement;
  43. }
  44. self.resizeEventListener();
  45. self.deregisterScrollOffsets();
  46. self.registerScrollOffsets();
  47. }, 500);
  48. }
  49. protected registerScrollOffsets(): void {
  50. let nextScrollParent: HTMLElement = this.osmdSheetMusicContainer;
  51. this.fullScrollTop = 0;
  52. this.fullScrollLeft = 0;
  53. const self: WebDisplayInteractionManager = this;
  54. while(nextScrollParent && nextScrollParent !== document.documentElement){
  55. this.parentScrollMap.set(nextScrollParent, [nextScrollParent.scrollTop, nextScrollParent.scrollLeft]);
  56. this.fullScrollLeft += nextScrollParent.scrollLeft;
  57. this.fullScrollTop += nextScrollParent.scrollTop;
  58. if(nextScrollParent.scrollHeight > nextScrollParent.clientHeight){
  59. const nextScrollCallback: (this: HTMLElement, ev: Event) => any = function(scrollEvent: Event): void{
  60. //@ts-ignore
  61. const currentScroll: number[] = self.parentScrollMap.get(this);
  62. const currentScrollTop: number = currentScroll[0];
  63. const currentScrollLeft: number = currentScroll[1];
  64. //@ts-ignore
  65. self.fullScrollTop = self.fullScrollTop - currentScrollTop + this.scrollTop;
  66. //@ts-ignore
  67. self.fullScrollLeft = self.fullScrollLeft - currentScrollLeft + this.scrollLeft;
  68. //@ts-ignore
  69. self.parentScrollMap.set(this, [this.scrollTop, this.scrollLeft]);
  70. };
  71. this.scrollCallbackMap.set(nextScrollParent, nextScrollCallback);
  72. nextScrollParent.addEventListener("scroll", nextScrollCallback);
  73. }
  74. nextScrollParent = nextScrollParent.parentElement;
  75. }
  76. }
  77. protected deregisterScrollOffsets(): void {
  78. for(const key of this.scrollCallbackMap.keys()){
  79. key.removeEventListener("scroll", this.scrollCallbackMap.get(key));
  80. }
  81. this.scrollCallbackMap.clear();
  82. }
  83. protected disposeResizeListener: Function;
  84. protected resizeObserver: ResizeObserver = undefined;
  85. protected initialize(): void {
  86. this.fullOffsetLeft = 0;
  87. this.fullOffsetTop = 0;
  88. let nextOffsetParent: HTMLElement = this.osmdSheetMusicContainer;
  89. const entries: HTMLElement[] = [];
  90. const self: WebDisplayInteractionManager = this;
  91. if(ResizeObserver){
  92. this.resizeObserver = new ResizeObserver((observedElements: ResizeObserverEntry[]) => {
  93. WebDisplayInteractionManager.resizeCallback(observedElements, self);
  94. });
  95. }
  96. while (nextOffsetParent) {
  97. this.fullOffsetLeft += nextOffsetParent.offsetLeft;
  98. this.fullOffsetTop += nextOffsetParent.offsetTop;
  99. if(!ResizeObserver){
  100. entries.push(nextOffsetParent);
  101. } else {
  102. this.resizeObserver.observe(nextOffsetParent);
  103. }
  104. nextOffsetParent = nextOffsetParent.offsetParent as HTMLElement;
  105. }
  106. if(!ResizeObserver){
  107. let resizeListener: (this: Window, ev: UIEvent) => any = (): void => {
  108. WebDisplayInteractionManager.resizeCallback(entries, self);
  109. };
  110. //Resize observer not avail. on this browser, default to window event
  111. window.addEventListener("resize", resizeListener);
  112. this.disposeResizeListener = (): void => {
  113. window.removeEventListener("resize", resizeListener);
  114. resizeListener = undefined;
  115. };
  116. } else {
  117. this.disposeResizeListener = (): void => {
  118. self.resizeObserver.disconnect();
  119. self.resizeObserver = undefined;
  120. };
  121. }
  122. self.registerScrollOffsets();
  123. }
  124. protected dispose(): void {
  125. this.disposeResizeListener();
  126. for(const eventName of this.EventCallbackMap.keys()){
  127. const result: [HTMLElement|Document, EventListener] = this.EventCallbackMap.getValue(eventName);
  128. result[0].removeEventListener(eventName, result[1]);
  129. }
  130. this.EventCallbackMap.clear();
  131. this.deregisterScrollOffsets();
  132. this.scrollCallbackMap.clear();
  133. this.parentScrollMap.clear();
  134. }
  135. //TODO: Much of this pulled from annotations code. Once we get the two branches together, combine common code
  136. private isTouch(): boolean {
  137. if (("ontouchstart" in window) || (window as any).DocumentTouch) {
  138. return true;
  139. }
  140. // include the 'heartz' as a way to have a non matching MQ to help terminate the join
  141. // https://git.io/vznFH
  142. const prefixes: string[] = ["-webkit-", "-moz-", "-o-", "-ms-"];
  143. const query: string = ["(", prefixes.join("touch-enabled),("), "heartz", ")"].join("");
  144. return window.matchMedia(query).matches;
  145. }
  146. protected get downEventName(): string {
  147. return this.isTouch() ? "touchstart" : "mousedown";
  148. }
  149. protected get moveEventName(): string {
  150. return this.isTouch() ? "touchmove" : "mousemove";
  151. }
  152. protected EventCallbackMap: Dictionary<string, [HTMLElement|Document, EventListener]> =
  153. new Dictionary<string, [HTMLElement|Document, EventListener]>();
  154. private listenForInteractions(): void {
  155. const downEvent: (clickEvent: MouseEvent | TouchEvent) => void = this.downEventListener.bind(this);
  156. const endTouchEvent: (clickEvent: TouchEvent) => void = this.touchEndEventListener.bind(this);
  157. const moveEvent: (clickEvent: MouseEvent | TouchEvent) => void = this.moveEventListener.bind(this);
  158. this.osmdSheetMusicContainer.addEventListener("mousedown", downEvent);
  159. this.osmdSheetMusicContainer.addEventListener("touchend", endTouchEvent);
  160. document.addEventListener(this.moveEventName, moveEvent);
  161. this.EventCallbackMap.setValue("mousedown", [this.osmdSheetMusicContainer, downEvent]);
  162. this.EventCallbackMap.setValue("touchend", [this.osmdSheetMusicContainer, endTouchEvent]);
  163. this.EventCallbackMap.setValue(this.moveEventName, [document, moveEvent]);
  164. }
  165. //Millis of how long is valid for the next click of a double click
  166. private readonly DOUBLE_CLICK_WINDOW: number = 200;
  167. private clickTimeout: NodeJS.Timeout;
  168. private lastClick: number = 0;
  169. private downEventListener(clickEvent: MouseEvent | TouchEvent): void {
  170. //clickEvent.preventDefault();
  171. const currentTime: number = new Date().getTime();
  172. const clickLength: number = currentTime - this.lastClick;
  173. clearTimeout(this.clickTimeout);
  174. let x: number = 0;
  175. let y: number = 0;
  176. if (this.isTouch() && clickEvent instanceof TouchEvent) {
  177. x = clickEvent.touches[0].pageX;
  178. y = clickEvent.touches[0].pageY;
  179. } else if (clickEvent instanceof MouseEvent) {
  180. x = clickEvent.pageX;
  181. y = clickEvent.pageY;
  182. }
  183. const clickMinusOffset: PointF2D = this.getOffsetCoordinates(x, y);
  184. if (clickLength < this.DOUBLE_CLICK_WINDOW && clickLength > 0) {
  185. //double click
  186. this.doubleClick(clickMinusOffset.x, clickMinusOffset.y);
  187. } else {
  188. const self: WebDisplayInteractionManager = this;
  189. this.clickTimeout = setTimeout(function(): void {
  190. clearTimeout(this.clickTimeout);
  191. if (self.isTouch()) {
  192. self.touchDown(clickMinusOffset.x, clickMinusOffset.y, undefined);
  193. } else {
  194. self.click(clickMinusOffset.x, clickMinusOffset.y);
  195. }
  196. }, this.DOUBLE_CLICK_WINDOW);
  197. }
  198. this.lastClick = currentTime;
  199. }
  200. private moveEventListener(mouseMoveEvent: MouseEvent | TouchEvent): void {
  201. let x: number = 0;
  202. let y: number = 0;
  203. if (this.isTouch() && mouseMoveEvent instanceof TouchEvent) {
  204. let touch: Touch = undefined;
  205. if(mouseMoveEvent.touches && mouseMoveEvent.touches.length > 0){
  206. touch = mouseMoveEvent.touches[0];
  207. } else if(mouseMoveEvent.changedTouches && mouseMoveEvent.changedTouches.length > 0){
  208. touch = mouseMoveEvent.changedTouches[0];
  209. }
  210. x = touch?.clientX;
  211. y = touch?.clientY;
  212. } else if (mouseMoveEvent instanceof MouseEvent) {
  213. x = mouseMoveEvent.clientX;
  214. y = mouseMoveEvent.clientY;
  215. }
  216. const clickMinusOffset: PointF2D = this.getOffsetCoordinates(x, y);
  217. this.move(clickMinusOffset.x, clickMinusOffset.y);
  218. }
  219. private touchEndEventListener(clickEvent: TouchEvent): void {
  220. let touch: Touch = undefined;
  221. if(clickEvent.touches && clickEvent.touches.length > 0){
  222. touch = clickEvent.touches[0];
  223. } else if(clickEvent.changedTouches && clickEvent.changedTouches.length > 0){
  224. touch = clickEvent.changedTouches[0];
  225. }
  226. const touchMinusOffset: PointF2D = this.getOffsetCoordinates(touch?.pageX, touch?.pageY);
  227. this.touchUp(touchMinusOffset.x, touchMinusOffset.y);
  228. }
  229. private resizeEventListener(): void {
  230. this.displaySizeChanged(this.osmdSheetMusicContainer.clientWidth, this.osmdSheetMusicContainer.clientHeight);
  231. }
  232. private getOffsetCoordinates(clickX: number, clickY: number): PointF2D {
  233. const sheetX: number = clickX - this.fullOffsetLeft + this.fullScrollLeft;
  234. const sheetY: number = clickY - this.fullOffsetTop + this.fullScrollTop;
  235. return new PointF2D(sheetX, sheetY);
  236. }
  237. }