namespace com.keyman.dom { // Defines DOM-related utility functions that are not reliant on KMW's internal state. export class Utils { /** * Function getAbsoluteX * Scope Public * @param {Object} Pobj HTML element * @return {number} * Description Returns x-coordinate of Pobj element absolute position with respect to page */ static getAbsoluteX(Pobj: HTMLElement): number { // I1476 - Handle SELECT overlapping END var Lobj: HTMLElement if(!Pobj) { return 0; } var Lcurleft = Pobj.offsetLeft ? Pobj.offsetLeft : 0; Lobj = Pobj; // I2404 - Support for IFRAMEs if (Lobj.offsetParent) { while (Lobj.offsetParent) { Lobj = Lobj.offsetParent as HTMLElement; Lcurleft += Lobj.offsetLeft; } // On mobile devices, the OSK uses 'fixed' - this requires some extra offset work to handle. let Ldoc = Lobj.ownerDocument; if(Lobj.style.position == 'fixed' && Ldoc && Ldoc.scrollingElement) { Lcurleft += Ldoc.scrollingElement.scrollLeft; } } // Correct position if element is within a frame (but not if the controller is in document within that frame) // We used to reference a KMW state variable `this.keyman._MasterDocument`, but it was only ever set to `window.document`. if(Lobj && Lobj.ownerDocument && (Pobj.ownerDocument != window.document)) { var Ldoc=Lobj.ownerDocument; // I2404 - Support for IFRAMEs if(Ldoc && Ldoc.defaultView && Ldoc.defaultView.frameElement) { return Lcurleft + Utils.getAbsoluteX(Ldoc.defaultView.frameElement) - Ldoc.documentElement.scrollLeft; } } return Lcurleft; } /** * Function getAbsoluteY * Scope Public * @param {Object} Pobj HTML element * @return {number} * Description Returns y-coordinate of Pobj element absolute position with respect to page */ static getAbsoluteY(Pobj: HTMLElement): number { var Lobj: HTMLElement if(!Pobj) { return 0; } var Lcurtop = Pobj.offsetTop ? Pobj.offsetTop : 0; Lobj = Pobj; // I2404 - Support for IFRAMEs if (Lobj.ownerDocument && Lobj instanceof Lobj.ownerDocument.defaultView.HTMLElement) { while (Lobj.offsetParent) { Lobj = Lobj.offsetParent as HTMLElement; Lcurtop += Lobj.offsetTop; } // On mobile devices, the OSK uses 'fixed' - this requires some extra offset work to handle. let Ldoc = Lobj.ownerDocument; if(Lobj.style.position == 'fixed' && Ldoc && Ldoc.scrollingElement) { Lcurtop += Ldoc.scrollingElement.scrollTop; } } // Correct position if element is within a frame (but not if the controller is in document within that frame) // We used to reference a KMW state variable `this.keyman._MasterDocument`, but it was only ever set to `window.document`. if(Lobj && Lobj.ownerDocument && (Pobj.ownerDocument != window.document)) { var Ldoc=Lobj.ownerDocument; // I2404 - Support for IFRAMEs if(Ldoc && Ldoc.defaultView && Ldoc.defaultView.frameElement) { return Lcurtop + Utils.getAbsoluteY(Ldoc.defaultView.frameElement) - Ldoc.documentElement.scrollTop; } } return Lcurtop; } /** * Checks the type of an input DOM-related object while ensuring that it is checked against the correct prototype, * as class prototypes are (by specification) scoped upon the owning Window. * * See https://stackoverflow.com/questions/43587286/why-does-instanceof-return-false-on-chrome-safari-and-edge-and-true-on-firefox * for more details. * * @param {Element|Event} Pelem An element of the web page or one of its IFrame-based subdocuments. * @param {string} className The plain-text name of the expected Element type. * @return {boolean} */ static instanceof(Pelem: Event|EventTarget, className: string): boolean { // We must write special checks for our custom-defined element types! if(className == "TouchAliasElement") { if(this.instanceof(Pelem, "HTMLDivElement")) { let div = Pelem; // We should probably implement a slightly more robust check, but this should get us started well enough. return div['base'] !== undefined; } else { return false; } } var scopedClass; if(!Pelem) { // If we're bothering to check something's type, null references don't match // what we're looking for. return false; } if (Pelem['Window']) { // Window objects contain the class definitions for types held within them. So, we can check for those. return className == 'Window'; } else if (Pelem['defaultView']) { // Covers Document. scopedClass = Pelem['defaultView'][className]; } else if(Pelem['ownerDocument']) { scopedClass = (Pelem as Node).ownerDocument.defaultView[className]; } else if(Pelem['target']) { var event = Pelem as Event; if(this.instanceof(event.target, 'Window')) { scopedClass = event.target[className]; } else if(this.instanceof(event.target, 'Document')) { scopedClass = (event.target as Document).defaultView[className]; } else if(this.instanceof(event.target, 'HTMLElement')) { scopedClass = (event.target as HTMLElement).ownerDocument.defaultView[className]; } } if(scopedClass) { return Pelem instanceof scopedClass; } else { return false; } } static forceScroll(element: HTMLInputElement | HTMLTextAreaElement) { // Needed to allow ./build_dev_resources.sh to complete; // only executes when com.keyman.DOMEventHandlers is defined. // // We also bypass this whenever operating in the embedded format. if(com && com.keyman && com.keyman['DOMEventHandlers'] && !com.keyman['singleton']['isEmbedded']) { let DOMEventHandlers = com.keyman['DOMEventHandlers']; let selectionStart = element.selectionStart; let selectionEnd = element.selectionEnd; DOMEventHandlers.states._IgnoreBlurFocus = true; //Forces scrolling; the re-focus triggers the scroll, at least. element.blur(); element.focus(); DOMEventHandlers.states._IgnoreBlurFocus = false; // On Edge, it appears that the blur/focus combination will reset the caret position // under certain scenarios during unit tests. So, we re-set it afterward. element.selectionStart = selectionStart; element.selectionEnd = selectionEnd; } } } }