diff --git a/developer/src/server/src/site/chargrid.js b/developer/src/server/src/site/chargrid.js index e3c78fd437a..1ce9206bae5 100644 --- a/developer/src/server/src/site/chargrid.js +++ b/developer/src/server/src/site/chargrid.js @@ -75,7 +75,7 @@ if(keyman.isPositionSynthesized()) { // this is an internal function // For touch devices, we need to ask KMW - selStart = ta1.kmw_ip ? ta1.kmw_ip.getTextBeforeCaret().length : 0; + selStart = 0; selLength = 0; selDirection = 'forward'; } else { diff --git a/web/source/dom/domDefaultOutput.ts b/web/source/dom/domDefaultOutput.ts index 694a4f2fb3b..9e66451da15 100644 --- a/web/source/dom/domDefaultOutput.ts +++ b/web/source/dom/domDefaultOutput.ts @@ -28,27 +28,14 @@ namespace com.keyman.dom { let code = DefaultOutput.codeForEvent(Lkc); let domManager = com.keyman.singleton.domManager; - let hideCaret: () => void; - if(outputTarget instanceof com.keyman.dom.targets.TouchAlias) { - hideCaret = function() { - let target = outputTarget as com.keyman.dom.targets.TouchAlias; - target.root.hideCaret(); - } - } else { - hideCaret = function() {}; - } - switch(code) { case Codes.keyCodes['K_TAB']: - hideCaret(); domManager.moveToNext((Lkc.Lmodifiers & text.Codes.modifierCodes['SHIFT']) != 0); break; case Codes.keyCodes['K_TABBACK']: - hideCaret(); domManager.moveToNext(true); break; case Codes.keyCodes['K_TABFWD']: - hideCaret(); domManager.moveToNext(false); break; } diff --git a/web/source/dom/domEventHandlers.ts b/web/source/dom/domEventHandlers.ts index 83b37a1bf9d..f692a2dc661 100644 --- a/web/source/dom/domEventHandlers.ts +++ b/web/source/dom/domEventHandlers.ts @@ -1,4 +1,3 @@ -/// /// namespace com.keyman.dom { @@ -51,20 +50,6 @@ namespace com.keyman.dom { this.keyman = keyman; } - /** - * Handle receiving focus by simulated input field - */ - setFocus: (e?: TouchEvent) => void = function(e?: TouchEvent): void { - // Touch-only handler. - }.bind(this); - - /** - * Handles touch-based loss of focus events. - */ - setBlur: (e: FocusEvent) => void = function(e: FocusEvent) { - // Touch-only handler. - }.bind(this); - // End of I3363 (Build 301) additions // Universal DOM event handlers (both desktop + touch) @@ -75,7 +60,6 @@ namespace com.keyman.dom { */ _ControlFocus: (e: FocusEvent) => boolean = function(this: DOMEventHandlers, e: FocusEvent): boolean { var Ltarg: HTMLElement; - var device = this.keyman.util.device; Ltarg = this.keyman.util.eventTarget(e) as HTMLElement; if (Ltarg == null) { @@ -86,23 +70,12 @@ namespace com.keyman.dom { Ltarg = Ltarg['body']; // Occurs in Firefox for design-mode iframes. } - // Prevent any action if a protected input field - if(device.touchable && (Ltarg.className == null || Ltarg.className.indexOf('keymanweb-input') < 0)) { - return true; - } - // Or if not a remappable input field - var en=Ltarg.nodeName.toLowerCase(); if(Ltarg.ownerDocument && Ltarg instanceof Ltarg.ownerDocument.defaultView.HTMLInputElement) { var et=Ltarg.type.toLowerCase(); if(!(et == 'text' || et == 'search')) { return true; } - } else if(Ltarg.ownerDocument && Ltarg.ownerDocument.designMode == 'on') { - // continue; don't block this one! - } else if((device.touchable || !Ltarg.isContentEditable) - && !(Ltarg.ownerDocument && Ltarg instanceof Ltarg.ownerDocument.defaultView.HTMLTextAreaElement)) { - return true; } // We condition on 'priorElement' below as a check to allow KMW to set a default active keyboard. @@ -114,13 +87,6 @@ namespace com.keyman.dom { var LfocusTarg = Ltarg; - // Ensure that focussed element is visible above the keyboard - if(Ltarg.className == null || Ltarg.className.indexOf('keymanweb-input') < 0) { - if(this instanceof DOMTouchHandlers) { - (this as DOMTouchHandlers).scrollBody(Ltarg); - } - } - if(Ltarg.ownerDocument && Ltarg instanceof Ltarg.ownerDocument.defaultView.HTMLIFrameElement) { //**TODO: check case reference this.keyman.domManager._AttachToIframe(Ltarg as HTMLIFrameElement); Ltarg=Ltarg.contentWindow.document.body; @@ -182,16 +148,6 @@ namespace com.keyman.dom { Ltarg = Ltarg['body']; // Occurs in Firefox for design-mode iframes. } - // Makes sure we properly detect the TouchAliasElement root, - // rather than one of its constituent children. - if(this.keyman.util.device.touchable) { - Ltarg = findTouchAliasTarget(Ltarg); - - if(!Ltarg) { - return true; - } - } - if(DOMEventHandlers.states._IgnoreNextSelChange) { // If a keyboard calls saveFocus() (KSF), then ignore the // next selection change @@ -208,12 +164,6 @@ namespace com.keyman.dom { return true; } - // Hide the touch device input caret, if applicable I3363 (Build 301) - if(dom.Utils.instanceof(this.keyman.domManager.activeElement, "TouchAliasElement")) { - let lastAlias = this.keyman.domManager.activeElement; - lastAlias.hideCaret(); - } - if (Ltarg.nodeType == 3) { // defeat Safari bug Ltarg = Ltarg.parentNode as HTMLElement; } @@ -337,6 +287,13 @@ namespace com.keyman.dom { const outputTarget = dom.Utils.getOutputTarget(target); + // Ensure that focused element is visible above the keyboard + if(keyman.util.device.touchable && outputTarget) { + if(this instanceof DOMTouchHandlers) { + (this as DOMTouchHandlers).scrollBody(target); + } + } + let activeKeyboard = keyman.core.activeKeyboard; if(!uiManager.justActivated) { if(target && outputTarget) { @@ -405,11 +362,6 @@ namespace com.keyman.dom { doChangeEvent(_target: HTMLElement) { if(DOMEventHandlers.states.changed) { let event = new Event('change', {"bubbles": true, "cancelable": false}); - - // Ensure that touch-aliased elements fire as if from the aliased element. - if(_target['base'] && _target['base']['kmw_ip']) { - _target = _target['base']; - } _target.dispatchEvent(event); } @@ -502,163 +454,6 @@ namespace com.keyman.dom { super(keyman); } - private static selectTouch(e: TouchEvent): Touch { - /* - * During multi-touch events, it's possible for one or more touches of said multi-touch - * to be against irrelevant parts of the page. We only want to consider touches against - * valid OutputTargets - against elements of the page that KMW can attach to. - * With touch active... that's a TouchAliasElement. - */ - let isValidTouch = function(touch: Touch, target: EventTarget): boolean { - return e.target == target && !!(findTouchAliasTarget(touch.target as HTMLElement)); - } - - // The event at least tells us the event's target, which can be used to help check - // whether or not individual `Touch`es may be related to this specific event for - // an ongoing multitouch scenario. - let target = e.target; - - // Find the first touch affected by this event that matches the current target. - for(let i=0; i < e.changedTouches.length; i++) { - if(isValidTouch(e.changedTouches[i], target)) { - return e.changedTouches[i]; - } - } - - // Shouldn't be possible. Just in case, we'd prefer a silent failure that allows - // callers to silently abort. - throw new Error("Could not select valid Touch for event."); - } - - /** - * Handle receiving focus by simulated input field - */ - setFocus: (e?: TouchEvent) => void = function(this: DOMTouchHandlers, e?: TouchEvent): void { - DOMEventHandlers.states.setFocusTimer(); - - var tEvent: { - pageX: number; - pageY: number; - target?: EventTarget; - }; - - if(e && dom.Utils.instanceof(e, "TouchEvent")) { - try { - tEvent=DOMTouchHandlers.selectTouch(e as TouchEvent); - } catch(err) { - console.warn(err); - return; - } - } else { // Allow external code to set focus and thus display the OSK on touch devices if required (KMEW-123) - tEvent={pageX:0, pageY:0} - - // Will usually be called from setActiveElement, which should define DOMEventHandlers.states.lastActiveElement - if(this.keyman.domManager.lastActiveElement) { - tEvent.target = this.keyman.domManager.lastActiveElement; - // Shouldn't happen, but... just in case. Implemented late in 14.0 beta, so - // this detail was kept, though it's likely safe to eliminate. - if(tEvent.target['kmw_ip']) { - tEvent.target = tEvent.target['kmw_ip']; - } - // but will default to first input or text area on page if DOMEventHandlers.states.lastActiveElement is null - } else { - tEvent.target = this.keyman.domManager.sortedInputs[0]['kmw_ip']; - } - } - - this.setFocusWithTouch(tEvent); - }.bind(this); - - // Also handles initial touch responses. - setFocusWithTouch(tEvent: {pageX: number, pageY: number, target?: EventTarget}) { - var touchX=tEvent.pageX,touchY=tEvent.pageY; - - // Some specifics rely upon which child of the TouchAliasElement received the actual event. - let tTarg=tEvent.target as HTMLElement; - - // Determines the actual TouchAliasElement - the part tied to an OutputTarget. - let target = findTouchAliasTarget(tTarg); - - if(!target) { - return; - } - - // Some parts rely upon the scroller element. - let scroller = target.firstChild as HTMLElement; - - // Move the caret and refocus if necessary - if(this.keyman.domManager.activeElement != target) { - // Hide the KMW caret - let prevTarget = this.keyman.domManager.activeElement; - - // We're not 100% sure whether or not the next line can occur, - // but it's a decent failsafe regardless. - if(prevTarget && prevTarget['kmw_ip']) { - prevTarget = prevTarget['kmw_ip'] as TouchAliasElement; - } - - // Make sure that we have the right type so that the expected method exists. - if(prevTarget && dom.Utils.instanceof(prevTarget, "TouchAliasElement")) { - prevTarget.hideCaret(); - } - - this.keyman.domManager.activeElement = target; - // The issue here is that touching a DIV does not actually set the focus for iOS, even when enabled to accept focus (by setting tabIndex=0) - // We must explicitly set the focus in order to remove focus from any non-KMW input - target.focus(); //Android native browsers may not like this, but it is needed for Chrome, Safari - } - - // Correct element directionality if required - this.keyman.domManager._SetTargDir(target); - - // If clicked on DIV on the main element, rather than any part of the text representation, - // set caret to end of text - if(tTarg && tTarg == target) { - let cp: number; - let x=dom.Utils.getAbsoluteX(scroller.firstChild as HTMLElement); - if(target.dir == 'rtl') { - x += (scroller.firstChild as HTMLElement).offsetWidth; - cp=(touchX > x ? 0 : 100000); - } else { - cp=(touchX void = function(this: DOMTouchHandlers, e: FocusEvent) { - // This works OK for iOS, but may need something else for other platforms - var elem: HTMLElement; - - if(('relatedTarget' in e) && e.relatedTarget) { - elem = e.relatedTarget as HTMLElement; - } - - this.executeBlur(elem); - }.bind(this); - - executeBlur(elem: HTMLElement) { - this.keyman['resetContext'](); - - if(elem) { - this.doChangeEvent(elem); - if(elem.nodeName != 'DIV' || elem.className.indexOf('keymanweb-input') == -1) { - this.cancelInput(); - return; - } - } - - //Hide the OSK - if(!DOMEventHandlers.states.focusing && !this.keyman.uiManager.justActivated) { - this.cancelInput(); - } - } - - /** - * Display and position a scrollbar in the input field if needed - * - * @param {Object} e input DIV element (copy of INPUT or TEXTAREA) - */ - setScrollBar(e: HTMLElement) { - // Display the scrollbar if necessary. Added TEXTAREA condition to correct rotation issue KMW-5. Fixed for 310 beta. - var scroller=e.childNodes[0], sbs=(e.childNodes[1]).style; - if((scroller.offsetWidth > e.offsetWidth || scroller.offsetLeft < 0) && (e.base.nodeName != 'TEXTAREA')) { - sbs.height='4px'; - sbs.width=100*(e.offsetWidth/scroller.offsetWidth)+'%'; - sbs.left=100*(-scroller.offsetLeft/scroller.offsetWidth)+'%'; - sbs.top='0'; - sbs.visibility='visible'; - } else if(scroller.offsetHeight > e.offsetHeight || scroller.offsetTop < 0) { - sbs.width='4px'; - sbs.height=100*(e.offsetHeight/scroller.offsetHeight)+'%'; - sbs.top=100*(-scroller.offsetTop/scroller.offsetHeight)+'%'; - sbs.left='0'; - sbs.visibility='visible'; - } else { - sbs.visibility='hidden'; - } - } - /** * Handle the touch end event for an input element */ @@ -732,101 +471,26 @@ namespace com.keyman.dom { this.firstTouch = null; }.bind(this); - /** - * Handle the touch move event for an input element - */ - dragInput: (e: TouchEvent) => void = function(this: DOMTouchHandlers, e: TouchEvent) { - // Prevent dragging window - if(e.cancelable) { - // If a touch-alias element is scrolling, this may be false. - // Tends to result in a spam of console errors when e.cancelable == false. - e.preventDefault(); - } - e.stopPropagation(); - - // Identify the target from the touch list or the event argument (IE 10 only) - var target: HTMLElement; - let touch: Touch; - - if(dom.Utils.instanceof(e, "TouchEvent")) { - try { - touch=DOMTouchHandlers.selectTouch(e as TouchEvent); - } catch(err) { - console.warn(err); - return; - } - target = touch.target as HTMLElement; - } else { - target = e.target as HTMLElement; - } - if(target == null) { - return; - } - - // Identify the input element from the touch event target (touched element may be contained by input) - target = findTouchAliasTarget(target); - - if(!target) { - return; - } - - const x = touch.screenX; - const y = touch.screenY; - - // Allow content of input elements to be dragged horizontally or vertically - if(typeof this.firstTouch == 'undefined' || this.firstTouch == null) { - this.firstTouch={x:x,y:y}; - } else { - var x0=this.firstTouch.x,y0=this.firstTouch.y, - scroller=target.firstChild as HTMLElement,dx,dy,x1; - - if(target.base.nodeName == 'TEXTAREA') { - var yOffset=parseInt(scroller.style.top,10); - if(isNaN(yOffset)) yOffset=0; - dy=y0-y; - if(dy < -4 || dy > 4) { - scroller.style.top=(yOffset 4) - { - // Limit dragging beyond the defined text (to avoid dragging the text completely out of view) - var xMin=0, xMax= dom.Utils.getAbsoluteX(target)+target.offsetWidth-scroller.offsetWidth-32; - if(target.base.dir == 'rtl')xMin=16; else xMax=xMax-24; - x1=xOffset-dx; - if(x1 > xMin) x1=xMin; - if(x1 < xMax) x1=xMax; - scroller.style.left=x1+'px'; - this.firstTouch.x=x; - } - } - } - // Should refactor to use TouchAliasElement's version; target is an instance of the class. - this.setScrollBar(target); - }.bind(this); - /** * Scroll the document body vertically to bring the active input into view * - * @param {Object} e simulated input field object being focussed + * @param {Object} e input field object being focussed */ scrollBody(e: HTMLElement): void { var osk = this.keyman.osk; - if(!e || e.className == null || e.className.indexOf('keymanweb-input') < 0 || !osk) { + if(!e || !osk) { return; } // Get the absolute position of the caret - var s2=e.firstChild.childNodes[1], y=dom.Utils.getAbsoluteY(s2), t=window.pageYOffset,dy=0; + const y = dom.Utils.getAbsoluteY(e); + const t = window.pageYOffset; + let dy = 0; if(y < t) { dy=y-t; } else { - dy=y-t-(window.innerHeight-osk._Box.offsetHeight-s2.offsetHeight-2); + dy=y-t-(window.innerHeight-osk._Box.offsetHeight-e.offsetHeight-2); if(dy < 0) dy=0; } // Hide OSK, then scroll, then re-anchor OSK with absolute position (on end of scroll event) diff --git a/web/source/dom/domManager.ts b/web/source/dom/domManager.ts index aeeb1b01307..2ecefe30c0a 100644 --- a/web/source/dom/domManager.ts +++ b/web/source/dom/domManager.ts @@ -8,8 +8,6 @@ /// // References other DOM-specific web-core overrides. /// -// Defines the touch-alias element structure used for mobile devices. -/// // Defines per-element-type OutputTarget element wrapping. /// // Defines cookie-based variable store serialization @@ -219,11 +217,6 @@ namespace com.keyman.dom { * Also ensures the element is registered on keymanweb's internal input list. */ enableTouchElement(Pelem: HTMLElement) { - // Touch doesn't worry about iframes. - if(Pelem.tagName.toLowerCase() == 'iframe') { - return false; - } - if(this.isKMWDisabled(Pelem)) { this.setupNonKMWTouchElement(Pelem); return false; @@ -238,34 +231,13 @@ namespace com.keyman.dom { // Remove any handlers for "NonKMWTouch" elements, since we're enabling it here. Pelem.removeEventListener('touchstart', this.nonKMWTouchHandler); - /* - * Does this element already have a simulated touch element established? If so, - * just reuse it - if it isn't still in the input list! - */ - if(Pelem['kmw_ip']) { - - if(this.inputList.indexOf(Pelem['kmw_ip']) != -1) { - return false; - } - - this.inputList.push(Pelem['kmw_ip']); - - console.log("Unexpected state - this element's simulated input DIV should have been removed from the page!"); - - return true; // May need setup elsewhere since it's just been re-added! - } - - // The simulated touch element doesn't already exist? Time to initialize it. - let x=dom.constructTouchAlias(Pelem); - if(this.isAttached(x)) { - x._kmwAttachment.interface = dom.targets.wrapElement(x); - } else { - this.setupElementAttachment(x); // The touch-alias should have its own wrapper. + if(!this.isAttached(Pelem)) { + this.setupElementAttachment(Pelem); + Pelem.inputMode = 'none'; } - Pelem._kmwAttachment = x._kmwAttachment; // It's an object reference we need to alias. // Set font for base element - this.enableInputElement(x, true); + this.enableInputElement(Pelem, false); // Superimpose custom input fields for each input or textarea, unless readonly or disabled @@ -275,14 +247,12 @@ namespace com.keyman.dom { // We know this to be the correct set of handlers because we're setting up a touch element. var touchHandlers = this.touchHandlers; - x.addEventListener('touchstart', touchHandlers.setFocus); - x.addEventListener('touchend', touchHandlers.dragEnd, false); - - // Disable internal scroll when input element in focus - x.addEventListener('touchmove', touchHandlers.dragInput, false); - - // Hide keyboard and caret when losing focus from simulated input field - x.onblur=touchHandlers.setBlur; + Pelem.addEventListener('touchmove', (event) => { + // Prevent the base-page `touchMoveActivationHandler` from triggering if we're + // "scrolling" within a valid input-element type. That can be used for selection. + event.stopPropagation(); + }, false); + Pelem.addEventListener('touchend', touchHandlers.dragEnd, false); // Note that touchend event propagates and is processed by body touchend handler // re-setting the first touch point for a drag @@ -299,34 +269,7 @@ namespace com.keyman.dom { */ disableTouchElement(Pelem: HTMLElement) { // Do not check for the element being officially disabled - it's also used for detachment. - - // Touch doesn't worry about iframes. - if(Pelem.tagName.toLowerCase() == 'iframe') { - return; // If/when we do support this, we'll need an iframe-level manager for it. - } - - if(Pelem['kmw_ip']) { - var index = this.inputList.indexOf(Pelem['kmw_ip']); - if(index != -1) { - this.inputList.splice(index, 1); - } - - Pelem.style.visibility='visible'; // hide by default: KMW-3 - Pelem.disabled = false; - Pelem.removeEventListener('resize', Pelem['kmw_ip']._kmwResizeHandler); - - // Disable touch-related handling code. - this.disableInputElement(Pelem['kmw_ip']); - Pelem._kmwAttachment.interface = dom.targets.wrapElement(Pelem); - - // We get weird repositioning errors if we don't remove our simulated input element - and permanently. - if(Pelem.parentNode) { - Pelem.parentNode.removeChild(Pelem['kmw_ip']); - } - delete Pelem['kmw_ip']; - } - - this.setupNonKMWTouchElement(Pelem); + Pelem.inputMode = 'text'; } /** @@ -404,15 +347,14 @@ namespace com.keyman.dom { return; } - var baseElement = isAlias ? Pelem['base'] : Pelem; // Do NOT test for pre-disabledness - we also use this to fully detach without officially 'disabling' via kmw-disabled. if((Pelem.ownerDocument.defaultView && Pelem instanceof Pelem.ownerDocument.defaultView.HTMLIFrameElement) || Pelem instanceof HTMLIFrameElement) { this._DetachFromIframe(Pelem); } else { - var cnIndex = baseElement.className.indexOf('keymanweb-font'); - if(cnIndex > 0 && !isAlias) { // See note about the alias below. - baseElement.className = baseElement.className.replace('keymanweb-font', '').trim(); + let cnIndex = Pelem.className.indexOf('keymanweb-font'); + if(cnIndex >= 0 && !isAlias) { // See note about the alias below. + Pelem.className = Pelem.className.replace('keymanweb-font', '').trim(); } // Remove the element from our internal input tracking. @@ -421,28 +363,17 @@ namespace com.keyman.dom { this.inputList.splice(index, 1); } - if(!isAlias) { // See note about the alias below. - this.keyman.util.detachDOMEvent(baseElement,'focus', this.getHandlers(Pelem)._ControlFocus); - this.keyman.util.detachDOMEvent(baseElement,'blur', this.getHandlers(Pelem)._ControlBlur); - this.keyman.util.detachDOMEvent(baseElement,'click', this.getHandlers(Pelem)._Click); - } - // These need to be on the actual input element, as otherwise the keyboard will disappear on touch. + this.keyman.util.detachDOMEvent(Pelem,'focus', this.getHandlers(Pelem)._ControlFocus); + this.keyman.util.detachDOMEvent(Pelem,'blur', this.getHandlers(Pelem)._ControlBlur); + this.keyman.util.detachDOMEvent(Pelem,'click', this.getHandlers(Pelem)._Click); + Pelem.onkeypress = null; Pelem.onkeydown = null; Pelem.onkeyup = null; } - // If we're disabling an alias, we should fully enable the base version. (Thinking ahead to toggleable-touch mode.) - if(isAlias) { - this.inputList.push(baseElement); - - baseElement.onkeypress = this.getHandlers(Pelem)._KeyPress; - baseElement.onkeydown = this.getHandlers(Pelem)._KeyDown; - baseElement.onkeyup = this.getHandlers(Pelem)._KeyUp; - } - var lastElem = this.lastActiveElement; - if(lastElem == Pelem || lastElem == Pelem['kmw_ip']) { + if(lastElem == Pelem) { if(this.activeElement == lastElem) { this.activeElement = null; } @@ -546,18 +477,21 @@ namespace com.keyman.dom { * Also filters elements not supported for touch devices when device.touchable == true. */ isKMWInput(x: HTMLElement): boolean { - var touchable = this.keyman.util.device.touchable; - if(x instanceof x.ownerDocument.defaultView.HTMLTextAreaElement) { return true; } else if(x instanceof x.ownerDocument.defaultView.HTMLInputElement) { if (x.type == 'text' || x.type == 'search') { return true; } - } else if(x instanceof x.ownerDocument.defaultView.HTMLIFrameElement && !touchable) { // Do not allow iframe attachment if in 'touch' mode. + } else if(x instanceof x.ownerDocument.defaultView.HTMLIFrameElement) { try { if(x.contentWindow) { - if(x.contentWindow.document) { // Only allow attachment if the iframe's internal document is valid. + const iframeDoc = x.contentWindow.document; + if(iframeDoc) { // Only allow attachment if the iframe's internal document is valid. + // Do not allow design-mode iframe attachment if in 'touch' mode. + if(this.keyman.util.device.touchable && iframeDoc.designMode.toLowerCase() == 'on') { + return false; + } return true; } } // else nothing? @@ -567,7 +501,7 @@ namespace com.keyman.dom { console.warn("Error during attachment to / detachment from iframe: "); console.warn(err); } - } else if(x.isContentEditable && !touchable) { // Only allow contentEditable attachment outside of 'touch' mode. + } else if(x.isContentEditable) { return true; } @@ -796,21 +730,13 @@ namespace com.keyman.dom { var elDir=(activeKeyboard && activeKeyboard.isRTL) ? 'rtl' : 'ltr'; if(Ptarg) { - if(this.keyman.util.device.touchable) { - let alias = Ptarg; - if(Ptarg.textContent.length == 0) { - alias.base.dir=alias.dir=elDir; - alias.setTextCaret(10000); - } - } else { - if(Ptarg instanceof Ptarg.ownerDocument.defaultView.HTMLInputElement - || Ptarg instanceof Ptarg.ownerDocument.defaultView.HTMLTextAreaElement) { - if((Ptarg as HTMLInputElement|HTMLTextAreaElement).value.length == 0) { - Ptarg.dir=elDir; - } - } else if(typeof Ptarg.textContent == "string" && Ptarg.textContent.length == 0) { // As with contenteditable DIVs, for example. + if(Ptarg instanceof Ptarg.ownerDocument.defaultView.HTMLInputElement + || Ptarg instanceof Ptarg.ownerDocument.defaultView.HTMLTextAreaElement) { + if((Ptarg as HTMLInputElement|HTMLTextAreaElement).value.length == 0) { Ptarg.dir=elDir; } + } else if(typeof Ptarg.textContent == "string" && Ptarg.textContent.length == 0) { // As with contenteditable DIVs, for example. + Ptarg.dir=elDir; } } } @@ -826,24 +752,9 @@ namespace com.keyman.dom { if(this.isAttached(Pelem) || Pelem instanceof Pelem.ownerDocument.defaultView.HTMLIFrameElement) { if(this.keyman.util.device.touchable) { this.disableTouchElement(Pelem); - this.setupNonKMWTouchElement(Pelem); - - var keyman = this.keyman; - - // If a touch alias was removed, chances are it's gonna mess up our touch-based layout scheme, so let's update the touch elements. - window.setTimeout(function() { - this.listInputs(); - - for(var k = 0; k < this.sortedInputs.length; k++) { - if(this.sortedInputs[k]['kmw_ip']) { - this.sortedInputs[k]['kmw_ip'].updateInput(this.sortedInputs[k]['kmw_ip']); - } - } - }.bind(this), 1); - } else { - this.listInputs(); // Fix up our internal input ordering scheme. } + this.listInputs(); // Fix up our internal input ordering scheme. this.disableInputElement(Pelem); } } @@ -865,12 +776,6 @@ namespace com.keyman.dom { // if we don't update the touch elements. window.setTimeout(function() { keyman.domManager.listInputs(); - - for(var k = 0; k < this.sortedInputs.length; k++) { - if(this.sortedInputs[k]['kmw_ip']) { - this.sortedInputs[k]['kmw_ip'].updateInput(this.sortedInputs[k]['kmw_ip']); - } - } }.bind(this), 1); } else { this.enableInputElement(Pelem); @@ -1002,12 +907,6 @@ namespace com.keyman.dom { var domManager = this; window.setTimeout(function() { domManager.listInputs(); - - for(var k = 0; k < this.sortedInputs.length; k++) { - if(this.sortedInputs[k]['kmw_ip']) { - this.sortedInputs[k]['kmw_ip'].updateInput(); - } - } }.bind(this), 1); } } @@ -1021,7 +920,7 @@ namespace com.keyman.dom { * */ _MutationAdditionObserved = function(Pelem: HTMLElement) { - if(Pelem instanceof Pelem.ownerDocument.defaultView.HTMLIFrameElement && !this.keyman.util.device.touchable) { + if(Pelem instanceof Pelem.ownerDocument.defaultView.HTMLIFrameElement) { //Problem: the iframe is loaded asynchronously, and we must wait for it to load fully before hooking in. var domManager = this; @@ -1146,6 +1045,10 @@ namespace com.keyman.dom { * Description Set default keyboard for the control */ setKeyboardForControl(Pelem: HTMLElement, Pkbd?: string, Plc?: string) { + if(!Pelem) { + return; + } + /* pass null for kbd to specify no default, or '' to specify the default system keyboard. */ if(Pkbd !== null && Pkbd !== undefined) { var index = Pkbd.indexOf("Keyboard_"); @@ -1169,10 +1072,9 @@ namespace com.keyman.dom { Pelem._kmwAttachment.languageCode = Plc; // If Pelem is the focused element/active control, we should set the keyboard in place now. - // 'kmw_ip' is the touch-alias for the original page's control. var lastElem = this.lastActiveElement; - if(lastElem && (lastElem == Pelem || lastElem == Pelem['kmw_ip'])) { + if(lastElem && (lastElem == Pelem)) { if(Pkbd != null && Plc != null) { // Second part necessary for Closure. this.keyman.keyboardManager.setActiveKeyboard(Pkbd, Plc); @@ -1262,13 +1164,6 @@ namespace com.keyman.dom { } set activeElement(Pelem: HTMLElement) { - // Ensure that a TouchAliasElement is hidden whenever it is deactivated for input. - if(this.activeElement) { - if(Utils.instanceof(this.keyman.domManager.activeElement, "TouchAliasElement")) { - (this.keyman.domManager.activeElement as TouchAliasElement).hideCaret(); - } - } - DOMEventHandlers.states._activeElement = Pelem; var isActivating = this.keyman.uiManager.isActivating; @@ -1308,10 +1203,6 @@ namespace com.keyman.dom { return; } - // As this is an API function, someone may pass in the base of a touch element. - // We need to respond appropriately. - e = (e['kmw_ip'] ? e['kmw_ip'] : e) as HTMLElement; - // If we're changing controls, don't forget to properly manage the keyboard settings! // It's only an issue on 'native' (non-embedded) code paths. if(!this.keyman.isEmbedded) { @@ -1330,18 +1221,7 @@ namespace com.keyman.dom { // Allow external focusing KMEW-123 if(arguments.length > 1 && setFocus) { - if(this.keyman.util.device.touchable) { - var tEvent = { - pageX: 0, - pageY: 0, - target: e as HTMLElement - }; - - // Kinda hacky, but gets the job done. - (this.keyman.touchAliasing as DOMTouchHandlers).setFocusWithTouch(tEvent); - } else { - this.focusLastActiveElement(); - } + this.focusLastActiveElement(); } // Let the keyboard do its initial group processing @@ -1393,24 +1273,7 @@ namespace com.keyman.dom { i = i < 0 ? i+t.length : i; // Move to the selected element - if(touchable) { - // Set focusing flag to prevent OSK disappearing - DOMEventHandlers.states.focusing=true; - var target=t[i]['kmw_ip']; - - // Focus if next element is non-mapped - if(typeof(target) == 'undefined') { - t[i].focus(); - } else { // Or reposition the caret on the input DIV if mapped - let alias = target; - this.keyman.domManager.setActiveElement(target); // Handles both `lastActive` + `active`. - alias.setTextCaret(10000); // Safe b/c touchable == true. - alias.scrollInput(); // mousedown check - target.focus(); - } - } else { // Behaviour for desktop browsers - t[i].focus(); - } + t[i].focus(); } /** @@ -1420,17 +1283,11 @@ namespace com.keyman.dom { * */ moveToElement(e:string|HTMLElement) { - var i; - if(typeof(e) == "string") { // Can't instanceof string, and String is a different type. e=document.getElementById(e); } - if(this.keyman.util.device.touchable && e['kmw_ip']) { - e['kmw_ip'].focus(); - } else { - e.focus(); - } + e.focus(); } /* ----------------------- Editable IFrame methods ------------------- */ @@ -1694,11 +1551,9 @@ namespace com.keyman.dom { // The following tests are needed to prevent the OSK from being hidden during normal input! let p=(e.target as HTMLElement).parentElement; if(typeof(p) != 'undefined' && p != null) { - if(p.className.indexOf('keymanweb-input') >= 0) return false; if(p.className.indexOf('kmw-key-') >= 0) return false; if(typeof(p.parentElement) != 'undefined' && p.parentElement != null) { p=p.parentElement; - if(p.className.indexOf('keymanweb-input') >= 0) return false; if(p.className.indexOf('kmw-key-') >= 0) return false; } } diff --git a/web/source/dom/targets/input.ts b/web/source/dom/targets/input.ts index bde5b52c5fc..a3b2514480c 100644 --- a/web/source/dom/targets/input.ts +++ b/web/source/dom/targets/input.ts @@ -155,7 +155,7 @@ namespace com.keyman.dom.targets { } static newlineHandler(inputEle: HTMLInputElement) { - // Can't occur for Mocks - just Input and TouchAlias types. + // Can't occur for Mocks - just Input types. if (inputEle && (inputEle.type == 'search' || inputEle.type == 'submit')) { inputEle.disabled=false; inputEle.form.submit(); diff --git a/web/source/dom/targets/readme.md b/web/source/dom/targets/readme.md index 8d62928033e..ee52947bc31 100644 --- a/web/source/dom/targets/readme.md +++ b/web/source/dom/targets/readme.md @@ -1,3 +1,3 @@ Please keep any code in this folder / namespace as free as possible from dependencies on other parts of KMW. Some of our unit tests wish to run against these types without requiring KMW to be active. -Note that with a little work, we _could_ completely spin this into its own separate module - this could be useful for development and testing purposes... at least, save for the TouchAlias type. \ No newline at end of file +Note that with a little work, we _could_ completely spin this into its own separate module - this could be useful for development and testing purposes, especially now that we've dropped the old `TouchAlias` type that was a bit entangled with main engine code. \ No newline at end of file diff --git a/web/source/dom/targets/touchAlias.ts b/web/source/dom/targets/touchAlias.ts deleted file mode 100644 index a152a4b4e63..00000000000 --- a/web/source/dom/targets/touchAlias.ts +++ /dev/null @@ -1,93 +0,0 @@ -// Defines the TouchAliasElement merged type. -/// - -namespace com.keyman.dom.targets { - export class TouchAlias extends OutputTarget { - root: TouchAliasElement; - - constructor(e: TouchAliasElement) { - super(); - this.root = e; - } - - getElement(): HTMLDivElement { - return this.root; - } - - clearSelection(): void { - // Touch-alias elements do not currently support selections. - return; - } - - invalidateSelection(): void { - // Touch-alias elements do not currently support selections. - return; - } - - isSelectionEmpty(): boolean { - // Touch-alias elements do not currently support selections. - return true; - } - - hasSelection(): boolean { - // Always has an internal caret position. - return true; - } - - getDeadkeyCaret(): number { - return this.root.getTextCaret(); - } - - getTextBeforeCaret(): string { - return this.root.getTextBeforeCaret(); - } - - getTextAfterCaret(): string { - return this.root.getTextAfterCaret(); - } - - getText(): string { - return this.root.getText(); - } - - deleteCharsBeforeCaret(dn: number): void { - if(dn > 0) { - let curText = this.getTextBeforeCaret(); - if(this.getDeadkeyCaret() < dn) { - dn = this.getDeadkeyCaret(); - } - this.adjustDeadkeys(-dn); - this.root.setTextBeforeCaret(curText.kmwSubstring(0, this.root.getTextCaret() - dn)); - } - } - - insertTextBeforeCaret(s: string): void { - this.adjustDeadkeys(s._kmwLength()); - this.root.setTextBeforeCaret(this.root.getTextBeforeCaret() + s); - } - - handleNewlineAtCaret(): void { - // Insert new line in text area fields - if(this.root.base.nodeName == 'TEXTAREA') { - // As the TouchAliasElement was implemented long before OutputTargets, it already has - // built-in newline handling. - this.insertTextBeforeCaret('\n'); - } else if(dom.Utils.instanceof(this.root.base, "HTMLInputElement")) { - // HTMLInputElements do not permit newlines; they instead have DOM-specific behaviors. - this.root.hideCaret(); - Input.newlineHandler(this.root.base as HTMLInputElement); - } else { - console.warn("TouchAlias OutputTarget cannot output newlines for unexpected base element types!"); - } - } - - protected setTextAfterCaret(s: string) { - this.root.setText(this.getTextBeforeCaret() + s, this.getTextBeforeCaret()._kmwLength()); - } - - doInputEvent() { - // Dispatch the event on the aliased element, not the TouchAliasElement itself. - this.dispatchInputEventOn(this.root.base); - } - } -} \ No newline at end of file diff --git a/web/source/dom/targets/wrapElement.ts b/web/source/dom/targets/wrapElement.ts index 5f13fbbeb4c..01b1a2a2c75 100644 --- a/web/source/dom/targets/wrapElement.ts +++ b/web/source/dom/targets/wrapElement.ts @@ -7,8 +7,6 @@ /// // Defines a basic design-mode IFrame wrapper. /// -// Defines a basic touch-alias element wrapper. -/// namespace com.keyman.dom.targets { export function wrapElement(e: HTMLElement): OutputTarget { @@ -18,8 +16,6 @@ namespace com.keyman.dom.targets { return new Input( e); } else if(Utils.instanceof(e, "HTMLTextAreaElement")) { return new TextArea( e); - } else if(Utils.instanceof(e, "TouchAliasElement")) { - return new TouchAlias( e); } else if(Utils.instanceof(e, "HTMLIFrameElement")) { let iframe = e; @@ -34,7 +30,7 @@ namespace com.keyman.dom.targets { } else if(e.isContentEditable) { return new ContentEditable(e); } - + return null; } } \ No newline at end of file diff --git a/web/source/dom/touchAliasElement.ts b/web/source/dom/touchAliasElement.ts deleted file mode 100644 index ee94e40e490..00000000000 --- a/web/source/dom/touchAliasElement.ts +++ /dev/null @@ -1,858 +0,0 @@ -// References extra HTML definitions not included by default in TS. -/// -// References device-specific code checks (separable module from KMW) -/// -// References common DOM utility functions (separate module from KMW) -/// - -namespace com.keyman.dom { - /** - * As our touch-alias elements have historically been based on
s, this - * defines the root element of touch-aliases as a merger type with HTMLDivElements. - * - */ - export type TouchAliasElement = HTMLDivElement & TouchAliasData; - - // Many thanks to https://www.typescriptlang.org/docs/handbook/advanced-types.html for this. - function link(elem: HTMLDivElement, data: TouchAliasData): TouchAliasElement { - let e = elem; - - // Merges all properties and methods of KeyData onto the underlying HTMLDivElement, creating a merged class. - for(let id in data) { - if(!e.hasOwnProperty(id)) { - (e)[id] = (data)[id]; - } - } - - return e; - } - - // If the specified HTMLElement is either a TouchAliasElement or one of its children elements, - // this method will return the root TouchAliasElement. - export function findTouchAliasTarget(target: HTMLElement): TouchAliasElement { - // The scrollable container element for the before & after text spans & the caret. - // Not to be confused with the simulated scrollbar. - let scroller: HTMLElement; - - // Identify the scroller element - if(target && dom.Utils.instanceof(target, "HTMLSpanElement")) { - scroller=target.parentNode as HTMLElement; - } else if(target && (target.className != null && target.className.indexOf('keymanweb-input') >= 0)) { - scroller=target.firstChild as HTMLElement; - } else if(target && dom.Utils.instanceof(target, "HTMLDivElement")) { - // Three possibilities: the scroller, the scrollbar, & the blinking DIV of the caret. - // A direct click CAN trigger events on the blinking element itself if well-timed. - scroller=target; - - // Ensures we land on the scroller, not the caret. - if(scroller.parentElement && scroller.parentElement.className.indexOf('keymanweb-input') < 0) { - scroller = scroller.parentElement; - } - - // scroller is now either the actual scroller or the scrollbar element. - // We don't return either of these, and they both have the same parent element. - } else if(target['kmw_ip']) { // In case it's called on a TouchAliasElement's base (aliased) element. - return target['kmw_ip'] as TouchAliasElement; - } else { - // If it's not in any way related to a TouchAliasElement, simply return null. - return null; - } - - // And the actual target element - let root = scroller.parentNode; - if(root['base'] !== undefined) { - return root as TouchAliasElement; - } else { - return null; - } - } - - export function constructTouchAlias(base?: HTMLElement): TouchAliasElement { - let div = document.createElement("div"); - let ele = link(div, new TouchAliasData()); - - if(base) { - ele.initWithBase(base); - } else { - ele.init(); - } - - return ele; - } - - /** - * The core definition for touch-alias 'subclassing' of HTMLDivElement. - * It's 'merged' with HTMLDivElement to avoid issues with DOM inheritance and DOM element creation. - */ - class TouchAliasData { - ['base']: HTMLElement = null; // NOT undefined; we can use this distinction for 'type-checking'. - __preCaret: HTMLSpanElement; - __postCaret: HTMLSpanElement; - __scrollDiv: HTMLDivElement; - __scrollBar: HTMLDivElement; - - __caretSpan: HTMLSpanElement; - __caretDiv: HTMLDivElement; - __caretTimerId: number; - __activeCaret: boolean = false; - - __executingCaretSearch: boolean = false; - - __resizeHandler: () => void; - - // The number of pixels a touch may lie within the 'next' character before we - // bump the caret AFTER that next character. - static readonly X_SNAP_LENIENCY_PIXELS: number = 5; - - private static device: Device; - - private static getDevice(): Device { - if(!TouchAliasData.device) { - let device = new com.keyman.Device(); - device.detect(); - - TouchAliasData.device = device; - } - - return TouchAliasData.device; - } - - private static getOS(): string { - return this.getDevice().OS; - } - - isMultiline(): boolean { - return this['base'] && this['base'].nodeName == "TEXTAREA"; - } - - initCaret(): void { - /** - * Create a caret to be appended to the scroller of the focussed input field. - * The caret is appended to the scroller so that it will automatically be clipped - * when the user manually scrolls it outside the element boundaries. - * It is positioned exactly over the hidden span (__caretSpan) that is inserted - * between the text spans before and after the insertion point. - */ - this.__caretDiv = document.createElement('DIV'); - var cs=this.__caretDiv.style; - cs.position='absolute'; - cs.height='16px'; // default height, actual height set from element properties - cs.width='2px'; - cs.backgroundColor='blue'; - cs.border='none'; - cs.left=cs.top='0px'; // actual position set relative to parent when displayed - cs.display='block'; - cs.visibility='hidden'; - cs.zIndex='9998'; // immediately below the OSK - - // Start the caret flash timer - this.__caretTimerId = window.setInterval(this.flashCaret.bind(this), 500); - } - - init() { - // Remember, this type exists to be merged into HTMLDivElements, so this will work. - // We have to trick TS a bit to make it happy, though. - let divThis = ( this); - divThis.className='keymanweb-input'; - - // Add a scrollable interior div - let d = this.__scrollDiv = document.createElement<'div'>('div'); - let xs = divThis.style; - xs.overflow='hidden'; - xs.position='absolute'; - //xs.border='1px solid gray'; - xs.border='hidden'; // hide when element empty - KMW-3 - xs.border='none'; - xs.borderRadius='5px'; - - // Add a scroll bar (horizontal for INPUT elements, vertical for TEXTAREA elements) - var sb = this.__scrollBar = document.createElement<'div'>('div'), sbs=sb.style; - sbs.position='absolute'; - sbs.height=sbs.width='4px'; - sbs.left=sbs.top='0'; - sbs.display='block'; - sbs.visibility='hidden'; - sbs.backgroundColor='#808080'; - sbs.borderRadius='2px'; - - // And add two spans for the text content before and after the caret, and a caret span - this.__preCaret=document.createElement<'span'>('span'); - this.__postCaret=document.createElement<'span'>('span'); - this.__caretSpan=document.createElement<'span'>('span'); - this.__preCaret.innerHTML = this.__postCaret.innerHTML = this.__caretSpan.innerHTML=''; - this.__preCaret.className = this.__postCaret.className = this.__caretSpan.className='keymanweb-font'; - - d.appendChild(this.__preCaret); - d.appendChild(this.__caretSpan); - d.appendChild(this.__postCaret); - divThis.appendChild(d); - divThis.appendChild(sb); - - let ds=d.style; - ds.position='relative'; - - let preCaretStyle = this.__preCaret.style; - let postCaretStyle = this.__postCaret.style; - let styleCaret = this.__caretSpan.style; - preCaretStyle.border=postCaretStyle.border='none'; - //preCaretStyle.backgroundColor='rgb(220,220,255)'; - //postCaretStyle.backgroundColor='rgb(220,255,220)'; //only for testing - preCaretStyle.height=postCaretStyle.height='100%'; - - // The invisible caret-positioning span must have a border to ensure that - // it remains in the layout, but colour doesn't matter, as it is never visible. - // Span margins are adjusted to compensate for the border and maintain text positioning. - styleCaret.border='1px solid red'; - styleCaret.visibility='hidden'; - styleCaret.marginLeft=styleCaret.marginRight='-1px'; - - ds.left='0px'; - ds.top='0px'; - ds.padding='0'; - ds.border='none'; - - // Set the outer element padding *after* appending the element, - // otherwise Firefox misaligns the two elements - xs.padding='8px'; - - // Set the tabindex to 0 to allow a DIV to accept focus and keyboard input - // c.f. http://www.w3.org/WAI/GL/WCAG20/WD-WCAG20-TECHS/SCR29.html - divThis.tabIndex=0; - - ds.minWidth=xs.width; - ds.height=xs.height; - - this.initCaret(); - } - - initWithBase(base: HTMLElement) { - this['base'] = base; - this.init(); - - let divThis = ( this); - - // There's quite a bit of setup for touch-alias elements that only occurs if it has an associated base. - this['base']['kmw_ip'] = divThis; - base.disabled = true; - - let baseStyle = window.getComputedStyle(base, null); - let scrollDivStyle = this.__scrollDiv.style; - let preCaretStyle = this.__preCaret.style; - let postCaretStyle = this.__postCaret.style; - - divThis.dir = base.dir; - - preCaretStyle.fontFamily = postCaretStyle.fontFamily = scrollDivStyle.fontFamily = baseStyle.fontFamily; - - // Set vertical centering for input elements - if(base.nodeName.toLowerCase() == 'input') { - if(!isNaN(parseInt(baseStyle.height,10))) { - preCaretStyle.lineHeight = postCaretStyle.lineHeight = baseStyle.height; - } - } - - if(TouchAliasData.getOS() == 'Android' && baseStyle.backgroundColor == 'transparent') { - scrollDivStyle.backgroundColor = '#fff'; - } else { - scrollDivStyle.backgroundColor = baseStyle.backgroundColor; - } - - if(divThis.base.nodeName.toLowerCase() == 'textarea') { - preCaretStyle.whiteSpace=postCaretStyle.whiteSpace='pre-wrap'; //scroll vertically - } else { - preCaretStyle.whiteSpace=postCaretStyle.whiteSpace='pre'; //scroll horizontally - } - - divThis.base.parentNode.appendChild(divThis); - divThis.updateInput(); - - let style = divThis.style; - style.color=baseStyle.color; - //style.backgroundColor=bs.backgroundColor; - style.fontFamily=baseStyle.fontFamily; - style.fontSize=baseStyle.fontSize; - style.fontWeight=baseStyle.fontWeight; - style.textDecoration=baseStyle.textDecoration; - style.padding=baseStyle.padding; - style.margin=baseStyle.margin; - style.border=baseStyle.border; - style.borderRadius=baseStyle.borderRadius; - - //xs.color='red'; //use only for checking alignment - - // Prevent highlighting of underlying element (Android) - if('webkitTapHighlightColor' in style) { - (style).webkitTapHighlightColor='rgba(0,0,0,0)'; - } - - if(base instanceof base.ownerDocument.defaultView.HTMLTextAreaElement) { - // Correct rows value if defaulted and box height set by CSS - // The rows value is used when setting the caret vertically - - if(base.rows == 2) { // 2 is default value - var h=parseFloat(baseStyle.height)-parseFloat(baseStyle.paddingTop)-parseFloat(baseStyle.paddingBottom), - dh=parseFloat(baseStyle.fontSize), calcRows=Math.round(h/dh); - if(calcRows > base.rows+1) { - base.rows=calcRows; - } - } - scrollDivStyle.width=style.width; - scrollDivStyle.minHeight=style.height; - } else { - scrollDivStyle.minWidth=style.width; - scrollDivStyle.height=style.height; - } - base.style.visibility='hidden'; // hide by default: KMW-3 - - // Add an explicit event listener to allow the duplicated input element - // to be adjusted for any changes in base element location or size - // This will be called for each element after any rotation, as well as after user-initiated changes - // It has to be wrapped in an anonymous function to preserve scope and be applied to each element. - (function(xx: TouchAliasElement){ - xx.__resizeHandler = function(){ - /* A timeout is needed to let the base element complete its resizing before our - * simulated element can properly resize itself. - * - * Not doing this causes errors if the input elements are resized for whatever reason, such as - * changing languages to a text with greater height. - */ - window.setTimeout(function (){ - xx.updateInput(); - }, 1); - }; - - xx.base.addEventListener('resize', xx.__resizeHandler, false); - xx.base.addEventListener('orientationchange', xx.__resizeHandler, false); - })(divThis); - - var textValue: string; - - if(base instanceof base.ownerDocument.defaultView.HTMLTextAreaElement - || base instanceof base.ownerDocument.defaultView.HTMLInputElement) { - textValue = base.value; - } else { - textValue = base.textContent; - } - - // And copy the text content - this.setText(textValue, null); - } - - setText(t?: string, cp?: number): void { - var tLen=0; - var t1: string, t2: string; - - // Read current text if null passed (for caret positioning) - if(t === null) { - t1 = this.__preCaret.textContent; - t2 = this.__postCaret.textContent; - t = t1 + t2; - } - - if(cp < 0) { - cp = 0; //if(typeof t._kmwLength == 'undefined') return; - } - - tLen=t._kmwLength(); - - if(cp === null || cp === undefined || cp > tLen) { - cp=tLen; - } - t1=t._kmwSubstr(0,cp); - t2=t._kmwSubstr(cp); - - this.__preCaret.textContent=t1; - this.__postCaret.textContent=t2; - - // Disable if a caret search is operational; no need to alter page layout or scroll just yet. - if(!this.__executingCaretSearch) { - this.updateBaseElement(); // KMW-3, KMW-29 - } - } - - getTextBeforeCaret() { - return this.__preCaret.textContent; - } - - getTextAfterCaret() { - return this.__postCaret.textContent; - } - - setTextBeforeCaret(t: string): void { - var tLen=0; - - // Collapse (trailing) whitespace to a single space for INPUT fields (also prevents wrapping) - if(!this.isMultiline()) { - t=t.replace(/\s+$/,' '); - } - this.__preCaret.textContent=t; - // Test total length in order to control base element visibility - tLen=t.length; - tLen=tLen+this.__postCaret.textContent.length; - - if(!this.__executingCaretSearch) { - this.finalizeCaret(); - } - } - - getTextCaret(): number { - return this.getTextBeforeCaret()._kmwLength(); - } - - setTextCaret(cp: number): void { - this.setText(null,cp); - this.showCaret(); - } - - /** - * An outer wrapper for the caret position update function. Disables unnecessary DOM-manipulation - * (& layout-triggering) operations during the search while guaranteeing that normal caret updates - * resume after the search, whether or not an error occurs. - * - * Some layout-triggering is still needed for the search to work, but the quantity is greatly reduced. - * @param touchPageX The target .pageX value for the caret - * @param touchPageY The target .pageY value for the caret - */ - executeCaretSearch(touchPageX: number, touchPageY: number) { - this.__executingCaretSearch = true; - try { - this.performCaretSearch(touchPageX, touchPageY); - } finally { - this.__executingCaretSearch = false; - this.finalizeCaret(); - } - } - - /** - * The core functionality for efficiently determining the intended caret placement within - * the text form of the context given the page coordinate of a `TouchEvent`. Takes great - * care to remain O(log N); some layout-triggering operations are required, making O(N) - * unacceptable. - * - * Even a few thousand characters is enough to become unacceptably laggy if O(N). - * @param touchPageX The target .pageX value for the caret - * @param touchPageY The target .pageY value for the caret - */ - private performCaretSearch(touchX: number, touchY: number) { - const dy=document.body.scrollTop; - const contextLength = this.getText()._kmwLength(); - let cpMin=0; - let cpMax=contextLength; - let cp=this.getTextCaret(); - - // Vertical scrolling - // instanceof this.base.[...] in case the element lies within an iframe. - if(this.base instanceof this.base.ownerDocument.defaultView.HTMLTextAreaElement) { - // Approximates the height of a row. - const yRow=Math.round(this.base.offsetHeight/(this.base as HTMLTextAreaElement).rows); - const maxY = touchY; - const minY = touchY - yRow; - - // Performs a binary search for a valid caret based on the y-position. - // cp: the previously-set caret position. - - // Break the binary search if our final search window is extremely small. - while(cpMax - cpMin > 1) { - - const y=dom.Utils.getAbsoluteY(this.__caretSpan)-dy; //top of caret - - if(y > maxY && cp > cpMin) { - // If caret's prior placement is below (after) the touch's y-pos... - cpMax=cp; // new max position - cp=Math.round((cp+cpMin)/2); // guess the halfway mark - } else if(y < minY && cp < cpMax) { - // If caret's prior placement is above (before) the touch's y-pos - 1 row height... - cpMin=cp; // new min posiiton - cp=Math.round((cp+cpMax)/2); // guess the halfway mark - } else { - // Great guess: the y-position lines up, so cp is within the target row. - // At this point, the only thing that matters is that we've found ONE caret - // position that matches the target line. - break; - } - // Actively set our caret to the determined matching y-position. - this.setTextCaret(cp); // mutates `caret`'s position - } - - // Tweak slightly if the caret position still falls out of bounds, but only enough to get in-bounds. - // Since our final window is small, we only need single-position shifts here (if needed at all). - if(dom.Utils.getAbsoluteY(this.__caretSpan)-dy > maxY && cp > cpMin) { - this.setTextCaret(--cp); // mutates `caret`'s position - } - - if(dom.Utils.getAbsoluteY(this.__caretSpan)-dy < minY && cp < cpMax) { - this.setTextCaret(++cp); // mutates `caret`'s position - } - - // Guarantees: cp is 'close' to both bounds, as it lies within the target row. - // Therefore, it caps the search interval for the x-coordinate versions of both cpMin and cpMax. - // - // Now to meet the pre-condition for the x-coord search later in the function: - // we need those bounds to be [cpMin, cpMax] === [start of row, end of row], both within the same line. - - // Determine minimum caret position on the target row; prep the x-coord cpMin. - // cpMin is guaranteed to lie strictly before the target row unless at the start of the first row. - // It is neither guaranteed to have changed since its initialization nor to be close to the target row. - let minCpMin = cpMin; - let maxCpMin = cp; - while(maxCpMin != minCpMin) { - // Our logic will auto-increment if too low, so favor the lower index. - const searchPt = Math.floor((maxCpMin+minCpMin)/2); - this.setTextCaret(searchPt); - - const y=dom.Utils.getAbsoluteY(this.__caretSpan)-dy; //top of caret - if(y < minY) { - // We already know it's not on this row, so the minimum possible index should be increased. - cpMin = minCpMin = searchPt+1; - } else { - // Still in the same row, so the boundary can only be at or before this point. - cpMin = maxCpMin = searchPt; - } - } - - // Determine maximum caret position on the target row; prep the x-coord cpMax. - // cpMax is guaranteed to lie strictly after the target row unless at the end of the final row. - // It is neither guaranteed to have changed since its initialization nor to be close to the target row. - let minCpMax = cp; - let maxCpMax = cpMax; - while(maxCpMax != minCpMax) { - // Our logic will auto-decrement if too high, so favor the higher index. - const searchPt = Math.round((maxCpMax+minCpMax)/2); - this.setTextCaret(searchPt); - - const y=dom.Utils.getAbsoluteY(this.__caretSpan)-dy; //top of caret - if(y > maxY) { - // We already know it's not on this row, so the maximum possible index should be decreased. - cpMax = maxCpMax = searchPt-1; - } else { - // Still in the same row, so the boundary can only be at or after this point. - cpMax = minCpMax = searchPt; - } - } - - // Set the potential caret in the middle of the range. - cp = Math.round((cpMin + cpMax)/2); - this.setTextCaret(cp); - } - - // Caret repositioning for horizontal scrolling of RTL text - - // snapOrder - 'snaps' the touch location in a manner corresponding to the 'ltr' vs 'rtl' orientation. - // Think of it as performing a floor() function, but the floor depends on the origin's direction. - const isRTL = (this as unknown as HTMLElement).dir == 'rtl'; - const snapOrder = isRTL ? (a, b) => a < b : (a, b) => a > b; - - // Used to signify a few pixels of leniency in the 'rtl'-appropriate direction for final - // caret placement. - const snapLeniency = isRTL ? -TouchAliasData.X_SNAP_LENIENCY_PIXELS : TouchAliasData.X_SNAP_LENIENCY_PIXELS; - - // Now to binary-search the x-coordinate. - // Pre-condition: [cpMin, cpMax] === [start of row, end of row], both within the same line. - // Automatically met for `-based instances. - // Break the binary search if our final search window is extremely small. - while(cpMax - cpMin > 1) { - const x=dom.Utils.getAbsoluteX(this.__caretSpan); //left of caret - - if(snapOrder(x, touchX) && cp > cpMin) { - cpMax=cp; - cp=Math.round((cp+cpMin)/2); - } else if(!snapOrder(x, touchX) && cp < cpMax) { - cpMin=cp; - cp=Math.round((cp+cpMax)/2); - } else { - break; - } - this.setTextCaret(cp); - } - - // LTR: if caret x-pos > touchPosition, push caret earlier. - if(snapOrder(dom.Utils.getAbsoluteX(this.__caretSpan), touchX) && cp > cpMin) { - this.setTextCaret(--cp); - } - - // LTR: if caret x-pos + leniency <= touchPosition, push caret later. - // Allows the touch to "bleed over" a couple pixels into the next char without - // bumping it later. - if(!snapOrder(dom.Utils.getAbsoluteX(this.__caretSpan) + snapLeniency, touchX) && cp < cpMax) { - this.setTextCaret(++cp); - } - } - - private finalizeCaret() { - // Update the base element then scroll into view if necessary - this.updateBaseElement(); //KMW-3, KMW-29 - this.scrollInput(); - } - - /** - * Set content, visibility, background and borders of input and base elements (KMW-3,KMW-29) - */ - updateBaseElement() { - let e = ( this); - - // Only proceed if we actually have a base element. - if(!e['base']) { - return; - } - - var Ldv = e.base.ownerDocument.defaultView; - if(e.base instanceof Ldv.HTMLInputElement || e.base instanceof Ldv.HTMLTextAreaElement) { - e.base.value = this.getText(); //KMW-29 - } else { - e.base.textContent = this.getText(); - } - - let n = this.getText()._kmwLength(); - - e.style.backgroundColor = (n==0 ? 'transparent' : window.getComputedStyle(e.base, null).backgroundColor); - - if(TouchAliasData.getOS() == 'iOS') { - e.base.style.visibility=(n==0?'visible':'hidden'); - } - } - - flashCaret(): void { - // Significant change - each element manages its own caret, and its activation is managed through show/hideCaret() - // without referencing core KMW code. (KMW must thus check if the active element is a TouchAliasElement, then use these - // methods as appropriate.) - if(this.__activeCaret) { - var cs=this.__caretDiv.style; - cs.visibility = cs.visibility != 'visible' ? 'visible' : 'hidden'; - } - }; - - /** - * Position the caret at the start of the second span within the scroller - */ - showCaret() { - var scroller=this.__scrollDiv, cs=this.__caretDiv.style, sp2=this.__caretSpan; - - // Attach the caret to this scroller and position it over the caret span - if(this.__caretDiv.parentNode != scroller) { - scroller.appendChild(this.__caretDiv); - } - - cs.left=sp2.offsetLeft+'px'; - cs.top=sp2.offsetTop+'px'; - cs.height=(sp2.offsetHeight-1)+'px'; - cs.visibility='hidden'; // best to wait for timer to display caret - this.__activeCaret = true; - - // Disable if a caret search is operational; no need to alter page layout or scroll just yet. - if(!this.__executingCaretSearch) { - // Scroll into view if required - this.scrollBody(); - - // Display and position the scrollbar if necessary - this.setScrollBar(); - } - } - - hideCaret() { - var e= ( this); - - // Always copy text back to underlying field on blur - if(e.base instanceof e.base.ownerDocument.defaultView.HTMLTextAreaElement - || e.base instanceof e.base.ownerDocument.defaultView.HTMLInputElement) { - e.base.value = this.getText(); - } - - // And set the scroller caret to the end of the element content (null preserves text) - this.setText(null, 100000); - - // Set the element scroll to zero (or max for RTL INPUT) - var ss=this.__scrollDiv.style; - if(e.isMultiline()) { - ss.top='0'; - } else { - if(e.base.dir == 'rtl') { - ss.left=(e.offsetWidth - this.__scrollDiv.offsetWidth-8)+'px'; - } else { - ss.left='0'; - } - } - - - // And hide the caret and scrollbar - if(this.__caretDiv.parentNode) { - this.__caretDiv.parentNode.removeChild(this.__caretDiv); - } - - this.__caretDiv.style.visibility='hidden'; - this.__scrollBar.style.visibility='hidden'; - - this.__activeCaret = false; - } - - getText(): string { - return ( ( this)).textContent; - } - - updateInput() { - if(this['base']) { - let divThis = ( ( this)); - - var xs=divThis.style, b=divThis.base, - s=window.getComputedStyle(b,null), - mLeft=parseFloat(s.marginLeft), - mTop=parseFloat(s.marginTop), - x1=Utils.getAbsoluteX(b), y1=Utils.getAbsoluteY(b); - - var p=divThis.offsetParent as HTMLElement; - if(p) { - x1=x1-Utils.getAbsoluteX(p); - y1=y1-Utils.getAbsoluteY(p); - } - - if(isNaN(mLeft)) { - mLeft=0; - } - if(isNaN(mTop)) { - mTop=0; - } - - xs.left=(x1-mLeft)+'px'; - xs.top=(y1-mTop)+'px'; - - var w=b.offsetWidth, h=b.offsetHeight, - pLeft=parseFloat(s.paddingLeft), pRight=parseFloat(s.paddingRight), - pTop=parseFloat(s.paddingTop), pBottom=parseFloat(s.paddingBottom), - bLeft=parseFloat(s.borderLeft), bRight=parseFloat(s.borderRight), - bTop=parseFloat(s.borderTop), bBottom=parseFloat(s.borderBottom); - - // If using content-box model, must subtract the padding and border, - // but *not* for border-box (as for WordPress PlugIn) - var boxSizing='undefined'; - if(typeof(s.boxSizing) != 'undefined') { - boxSizing=s.boxSizing; - } else if(typeof(s.MozBoxSizing) != 'undefined') { - boxSizing=s.MozBoxSizing; - } - - if(boxSizing == 'content-box') { - if(!isNaN(pLeft)) w -= pLeft; - if(!isNaN(pRight)) w -= pRight; - if(!isNaN(bLeft)) w -= bLeft; - if(!isNaN(bRight)) w -= bRight; - - if(!isNaN(pTop)) h -= pTop; - if(!isNaN(pBottom)) h -= pBottom; - if(!isNaN(bTop)) h -= bTop; - if(!isNaN(bBottom)) h -= bBottom; - } - - if(TouchAliasData.getOS() == 'Android') { - w++; - h++; - } - - xs.width=w+'px'; - xs.height=h+'px'; - } - } - - /** - * Scroll the input field horizontally (INPUT base element) or - * vertically (TEXTAREA base element) to bring the caret into view - * as text is entered or deleted form an element - */ - scrollInput() { - var scroller=this.__scrollDiv; - let divThis = ( this); - - // Get the actual absolute position of the caret and the element - var s2=this.__caretSpan, - cx=dom.Utils.getAbsoluteX(s2),cy=dom.Utils.getAbsoluteY(s2), - ex=dom.Utils.getAbsoluteX(divThis),ey=dom.Utils.getAbsoluteY(divThis), - x=parseInt(scroller.style.left,10), - y=parseInt(scroller.style.top,10), - dx=0,dy=0; - - // Scroller offsets must default to zero - if(isNaN(x)) x=0; if(isNaN(y)) y=0; - - // Scroll input field vertically if necessary - if(divThis.isMultiline()) { - var rowHeight=Math.round(divThis.offsetHeight/( divThis.base).rows); - if(cy < ey) { - dy=cy-ey; - } - if(cy > ey+divThis.offsetHeight-rowHeight) { - dy=cy-ey-divThis.offsetHeight+rowHeight; - } - if(dy != 0){ - scroller.style.top=(y ex+divThis.offsetWidth-12) { - dx=cx-ex-divThis.offsetWidth+12; - } - if(dx != 0) { - scroller.style.left=(x ( this); - - // Display the scrollbar if necessary. Added isMultiline condition to correct rotation issue KMW-5. Fixed for 310 beta. - var scroller=this.__scrollDiv, sbs=this.__scrollBar.style; - if((scroller.offsetWidth > e.offsetWidth || scroller.offsetLeft < 0) && !e.isMultiline()) { - sbs.height='4px'; - sbs.width=100*(e.offsetWidth/scroller.offsetWidth)+'%'; - sbs.left=100*(-scroller.offsetLeft/scroller.offsetWidth)+'%'; - sbs.top='0'; - sbs.visibility='visible'; - } else if(scroller.offsetHeight > e.offsetHeight || scroller.offsetTop < 0) { - sbs.width='4px'; - sbs.height=100*(e.offsetHeight/scroller.offsetHeight)+'%'; - sbs.top=100*(-scroller.offsetTop/scroller.offsetHeight)+'%'; - sbs.left='0'; - sbs.visibility='visible'; - } else { - sbs.visibility='hidden'; - } - } - } -} \ No newline at end of file diff --git a/web/source/dom/utils.ts b/web/source/dom/utils.ts index 2c16bede30e..8f7fc2673eb 100644 --- a/web/source/dom/utils.ts +++ b/web/source/dom/utils.ts @@ -13,7 +13,7 @@ namespace com.keyman.dom { if(keyman) { Lelem = keyman.domManager.lastActiveElement; } - + if(!Lelem) { // If we're trying to find an active target but one doesn't exist, just return null. return null; @@ -27,12 +27,12 @@ namespace com.keyman.dom { throw new Error("OSK could not find element output target data!"); } } - + /** * Function getAbsoluteX * Scope Public * @param {Object} Pobj HTML element - * @return {number} + * @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 @@ -41,7 +41,7 @@ namespace com.keyman.dom { if(!Pobj) { return 0; } - + var Lcurleft = Pobj.offsetLeft ? Pobj.offsetLeft : 0; Lobj = Pobj; // I2404 - Support for IFRAMEs @@ -73,9 +73,9 @@ namespace com.keyman.dom { * Function getAbsoluteY * Scope Public * @param {Object} Pobj HTML element - * @return {number} + * @return {number} * Description Returns y-coordinate of Pobj element absolute position with respect to page - */ + */ static getAbsoluteY(Pobj: HTMLElement): number { var Lobj: HTMLElement @@ -113,32 +113,20 @@ namespace com.keyman.dom { /** * 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. + // 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. @@ -151,7 +139,7 @@ namespace com.keyman.dom { var event = Pelem as Event; if(this.instanceof(event.target, 'Window')) { - scopedClass = event.target[className]; + 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')) { diff --git a/web/source/kmwbase.ts b/web/source/kmwbase.ts index d1ccd15c6ad..6cb0407ab2d 100644 --- a/web/source/kmwbase.ts +++ b/web/source/kmwbase.ts @@ -114,6 +114,9 @@ namespace com.keyman { KC_(n, ln, Pelem){return '';} handleRotationEvents(){} // Will serve as an API function for a workaround, in case of future touch-alignment issues. + /** + * This function is deprecated in 16.0, with plans for removal in 17.0. + */ ['alignInputs'](eleList?: HTMLElement[]){} hideInputs() {}; namespaceID(Pstub) {}; diff --git a/web/source/kmwnative.ts b/web/source/kmwnative.ts index a2d04d109f3..0af29d2ae28 100644 --- a/web/source/kmwnative.ts +++ b/web/source/kmwnative.ts @@ -6,10 +6,10 @@ Copyright 2019 SIL International ***/ -// If KMW is already initialized, the KMW script has been loaded more than once. We wish to prevent resetting the +// If KMW is already initialized, the KMW script has been loaded more than once. We wish to prevent resetting the // KMW system, so we use the fact that 'initialized' is only 1 / true after all scripts are loaded for the initial // load of KMW. -if(!window['keyman']['initialized']) { +if(!window['keyman']['initialized']) { /*****************************************/ /* */ /* On-Screen (Visual) Keyboard Code */ @@ -37,7 +37,7 @@ if(!window['keyman']['initialized']) { /** * Customized wait display - * + * * @param {string|boolean} s displayed text (or false) */ util.wait = function(s) { @@ -48,7 +48,7 @@ if(!window['keyman']['initialized']) { if(typeof(bg) == 'undefined' || bg == null || keymanweb.warned) { return; } - + var nn=bg.firstChild.childNodes; if(s) { bg.pending=true; @@ -68,7 +68,7 @@ if(!window['keyman']['initialized']) { } } } - + // Get default style sheet path keymanweb.getStyleSheetPath=function(ssName) { var ssPath = util['getOption']('resources')+'osk/'+ssName; @@ -80,86 +80,49 @@ if(!window['keyman']['initialized']) { * KeymanWeb 2 revised keyboard location specification: * (a) absolute URL (includes ':') - load from specified URL * (b) relative URL (starts with /, ./, ../) - load with respect to current page - * (c) filename only (anything else) - prepend keyboards option to URL + * (c) filename only (anything else) - prepend keyboards option to URL * (e.g. default keyboards option will be set by Cloud) - * - * @param {string} Lfilename keyboard file name with optional prefix - */ - keymanweb.getKeyboardPath=function(Lfilename) { - var rx=RegExp('^(([\\.]/)|([\\.][\\.]/)|(/))|(:)'); + * + * @param {string} Lfilename keyboard file name with optional prefix + */ + keymanweb.getKeyboardPath=function(Lfilename) { + var rx=RegExp('^(([\\.]/)|([\\.][\\.]/)|(/))|(:)'); return (rx.test(Lfilename) ? '' : keymanweb.options['keyboards']) + Lfilename; } /** * Align input fields (should not be needed with KMEI, KMEA), making them visible if previously hidden. - * + * * @param {object} eleList A list of specific elements to align. If nil, selects all elements. - * + * **/ keymanweb.alignInputs = function(eleList: HTMLElement[]) { - if(device.touchable) { - var domManager = keymanweb.domManager; - var processList: HTMLElement[] = []; - - if(eleList) { - // Did the user specify the actual element or the touch-alias? - eleList.forEach(function(element: HTMLElement){ - if(element.base) { - // It's a touch-alias element, which is what we wish to perform alignment on. - processList.push(element); - } else { - // This retrieves an element's touch-alias, should it exist. - let touchAlias = element['kmw_ip'] as HTMLDivElement; - if(touchAlias) { - processList.push(element['kmw_ip']); - } - } - }); - } else { - processList = domManager.inputList; - } - - processList.forEach(function(element: HTMLElement) { - if(dom.Utils.instanceof(element, "TouchAliasElement")) { - (element as com.keyman.dom.TouchAliasElement).updateInput(); - } - element.style.visibility = 'visible'; - if(element.base.textContent.length > 0) { - element.base.style.visibility = 'hidden'; - } - }) - } + // no-op } /** - * Programatically hides all input fields with underlying elements. Restore with .alignInputs. - * + * Programatically hides all input fields with underlying elements. Restore with .alignInputs. + * * @param {boolean} align align and make visible, else hide - * + * **/ keymanweb.hideInputs = function() { - var domManager = keymanweb.domManager; - if(device.touchable) { - for(var i=0; i= 0) { keyman.uiManager.setActivatingUI(false); oskManager.startHide(true); - let active = keyman.domManager.activeElement; - if (dom.Utils.instanceof(active, "TouchAliasElement")) { - (active as dom.TouchAliasElement).hideCaret(); - } keyman.domManager.lastActiveElement = null; } } diff --git a/web/source/tsconfig.dev_resources.json b/web/source/tsconfig.dev_resources.json index 164859fab1b..003689efd2f 100644 --- a/web/source/tsconfig.dev_resources.json +++ b/web/source/tsconfig.dev_resources.json @@ -16,9 +16,7 @@ "dom/targets/textarea.ts", "dom/targets/contentEditable.ts", "dom/targets/designIFrame.ts", - "dom/targets/touchAlias.ts", "dom/targets/outputTarget.ts", - "dom/touchAliasElement.ts", "kmwexthtml.ts", "kmwdevice.ts", "dom/utils.ts", diff --git a/web/testing/attachment-api/index.html b/web/testing/attachment-api/index.html index c02c66d3fd1..69fb497c152 100644 --- a/web/testing/attachment-api/index.html +++ b/web/testing/attachment-api/index.html @@ -93,9 +93,7 @@

KeymanWeb Sample Page - Attachment API Testing

Note: The iframe section should not actually attach/enable for touch devices and does not support setKeyboardForControl.

-

contenteditable Elements:

-

Note: This section should not actually attach/enable for touch devices.

-
+

contenteditable Elements:


Return to testing home page

diff --git a/web/tools/recorder/browserDriver.ts b/web/tools/recorder/browserDriver.ts index cb03ef1836c..55402f5afb6 100644 --- a/web/tools/recorder/browserDriver.ts +++ b/web/tools/recorder/browserDriver.ts @@ -52,7 +52,7 @@ namespace KMWRecorder { // To be safe, we replicate the MouseEvent similarly to the keystroke event. var downEvent; var upEvent; - if(target['base'] && target instanceof HTMLDivElement) { + if((window['keyman'] as any).util.device.touchable) { downEvent = new Event(BrowserDriver.oskDownTouchType); upEvent = new Event(BrowserDriver.oskUpTouchType); downEvent['touches'] = [{"target": oskKeyElement}]; diff --git a/web/tools/recorder/browserProctor.ts b/web/tools/recorder/browserProctor.ts index c68c770ffdd..74b02449b29 100644 --- a/web/tools/recorder/browserProctor.ts +++ b/web/tools/recorder/browserProctor.ts @@ -12,18 +12,13 @@ namespace KMWRecorder { ele.value = ""; } else { (window['keyman'] as any).resetContext(); - if(ele['base']) { - // Gotta be extra-careful with the simulated touch fields! - ( ele /*as com.keyman.dom.TouchAliasElement*/).setText("", 0); - } else { - ele.textContent = ""; - } + ele.textContent = ""; } } /** * Runs Recorder-generated tests in a browser. - * + * * Future notes: further abstraction needed; much of this code will be in common with Node. */ export class BrowserProctor extends Proctor { diff --git a/web/tools/recorder/recorder_ui_and_stubs.js b/web/tools/recorder/recorder_ui_and_stubs.js index a5d067f6886..b512f6424ce 100644 --- a/web/tools/recorder/recorder_ui_and_stubs.js +++ b/web/tools/recorder/recorder_ui_and_stubs.js @@ -7,11 +7,8 @@ var recorderScribe; function focusReceiver() { var receiver = document.getElementById('receiver'); - if(receiver['kmw_ip']) { - receiver = receiver['kmw_ip']; - } receiver.focus(); - + if(keyman.util.device.touchable) { // At present, touch doesn't 'focus' properly. keyman.setActiveElement(receiver); @@ -21,9 +18,6 @@ function focusReceiver() { setElementText = function(ele, text) { ele.value = text; - if(ele['kmw_ip']) { - ele['kmw_ip'].setTextBeforeCaret(ele.value); - } } onUpdateInputRecord = function(json) { @@ -42,15 +36,8 @@ resetInputRecord = function() { copyInputRecord = function() { try { - if(!ta_inputJSON['kmw_ip']) { - ta_inputJSON.select(); - } else { - var range = document.createRange(); - range.selectNode(ta_inputJSON['kmw_ip']); - window.getSelection().removeAllRanges(); - window.getSelection().addRange(range); - } - + ta_inputJSON.select(); + var res = document.execCommand('copy'); if(res) { in_output.focus(); @@ -97,7 +84,7 @@ copyTestDefinition = function() { try { masterJSON.select(); - + var res = document.execCommand('copy'); if(res) { in_output.focus(); @@ -164,18 +151,6 @@ window.addEventListener('load', function() { // Other setup. initDevice(); setupKeyboardPicker(); - - var errorInput = document.getElementById('errorText'); - if(errorInput['kmw_ip']) { - // Alias DIVs use subelements b/c caret simulation. - // Interestingly, 'childList' is the most important for noting textContent changes. - var config = { childList: true, subtree: true }; - var observer = new MutationObserver(function(mutations) { - errorUpdate(); - }); - - observer.observe(errorInput['kmw_ip'], config); - } }); //var p={'internalName':_internalName,'language':_language,'keyboardName':_keyboardName,'languageCode':_languageCode}; @@ -191,7 +166,7 @@ function keyboardAdded(properties) { function setupKeyboardPicker() { /* Make sure that Keyman is initialized (we can't guarantee initialization order) */ keyman.init(); - + var kbdControl = document.getElementById('KMW_Keyboard'); /* Retrieve the list of keyboards available from KeymanWeb and populate the selector using the DOM */ var kbds = keyman.getKeyboards(); @@ -199,9 +174,9 @@ function setupKeyboardPicker() { var opt = document.createElement('OPTION'); opt.value = kbds[kbd].InternalName + "$$" + kbds[kbd].LanguageCode; opt.innerHTML = kbds[kbd].Name; - kbdControl.appendChild(opt); + kbdControl.appendChild(opt); } - + // Ensures the default keyboard is active, to match our listbox's initial (default) option. keyman.setActiveKeyboard('', ''); keyman.addEventListener('keyboardregistered', keyboardAdded); @@ -332,7 +307,7 @@ function convertSet(testSet) { if(sequence.msg) { recorderScribe.errorUpdate(sequence.msg); } - + return new Promise(function(resolve) { let saveFunc = function() { // May need conversion in the future, but is fine for now - the 'Constraint' part of the spec didn't change. @@ -392,7 +367,7 @@ function deviceFromConstraint(constraint) { // ----------- END SPEC VERSION MIGRATION ------------- function loadExistingStubs(files) { - + var processStub = function(json, file) { try { // Load the stub @@ -429,7 +404,7 @@ function loadExistingStubs(files) { var reader = new XMLHttpRequest(); reader.open('GET', file); reader.responseType = 'text'; - + reader.onload = function() { processStub(reader.response, file); }; diff --git a/web/unit_tests/cases/attachmentAPI.js b/web/unit_tests/cases/attachmentAPI.js index 488761b932b..7cedb476b5a 100644 --- a/web/unit_tests/cases/attachmentAPI.js +++ b/web/unit_tests/cases/attachmentAPI.js @@ -51,8 +51,7 @@ describe('Attachment API', function() { keyman.attachToControl(ele); DynamicElements.assertAttached(ele); // Happens in-line, since we directly request the attachment. - // A keystroke must target the input-receiving element. For touch, that's the alias. - eventDriver = new KMWRecorder.BrowserDriver(ele['kmw_ip'] ? ele['kmw_ip'] : ele); + eventDriver = new KMWRecorder.BrowserDriver(ele); eventDriver.simulateEvent(DynamicElements.keyCommand); val = retrieveAndReset(ele); @@ -74,7 +73,7 @@ describe('Attachment API', function() { // for the change to take effect. window.setTimeout(function() { DynamicElements.assertAttached(ele); - let eventDriver = new KMWRecorder.BrowserDriver(ele['kmw_ip'] ? ele['kmw_ip'] : ele); + let eventDriver = new KMWRecorder.BrowserDriver(ele); eventDriver.simulateEvent(DynamicElements.keyCommand); val = retrieveAndReset(ele); assert.equal(val, DynamicElements.disabledOutput, "'Disabled' element performed keystroke processing!"); @@ -82,7 +81,7 @@ describe('Attachment API', function() { keyman.enableControl(ele); window.setTimeout(function() { DynamicElements.assertAttached(ele); // Happens in-line, since we directly request the attachment. - let eventDriver = new KMWRecorder.BrowserDriver(ele['kmw_ip'] ? ele['kmw_ip'] : ele); + let eventDriver = new KMWRecorder.BrowserDriver(ele); eventDriver.simulateEvent(DynamicElements.keyCommand); val = retrieveAndReset(ele); assert.equal(val, DynamicElements.enabledLaoOutput, "'Enabled' element did not perform keystroke processing!"); @@ -108,21 +107,21 @@ describe('Attachment API', function() { // Set control with independent keyboard. keyman.setKeyboardForControl(input, "khmer_angkor", "km"); - var eventDriver = new KMWRecorder.BrowserDriver(input['kmw_ip'] ? input['kmw_ip'] : input); + var eventDriver = new KMWRecorder.BrowserDriver(input); eventDriver.simulateEvent(DynamicElements.keyCommand); val = retrieveAndReset(input); assert.equal(val, DynamicElements.enabledKhmerOutput, "KMW did not use control's keyboard settings!"); // Swap to a global-linked control... keyman.setActiveElement(textarea); - eventDriver = new KMWRecorder.BrowserDriver(textarea['kmw_ip'] ? textarea['kmw_ip'] : textarea); + eventDriver = new KMWRecorder.BrowserDriver(textarea); eventDriver.simulateEvent(DynamicElements.keyCommand); val = retrieveAndReset(textarea); assert.equal(val, DynamicElements.enabledLaoOutput, "KMW did not use manage keyboard settings correctly for global-linked control!"); // Swap back and check that the settings persist. keyman.setActiveElement(input); - eventDriver = new KMWRecorder.BrowserDriver(input['kmw_ip'] ? input['kmw_ip'] : input); + eventDriver = new KMWRecorder.BrowserDriver(input); eventDriver.simulateEvent(DynamicElements.keyCommand); val = retrieveAndReset(input); assert.equal(val, DynamicElements.enabledKhmerOutput, "KMW forgot control's independent keyboard settings!"); @@ -154,14 +153,14 @@ describe('Attachment API', function() { keyman.setActiveElement(input); // Set textarea control with independent keyboard khmer_angkor. keyman.setKeyboardForControl(textarea, "khmer_angkor", "km"); - var eventDriver = new KMWRecorder.BrowserDriver(input['kmw_ip'] ? input['kmw_ip'] : input); + var eventDriver = new KMWRecorder.BrowserDriver(input); eventDriver.simulateEvent(DynamicElements.keyCommand); val = retrieveAndReset(input); assert.equal(val, DynamicElements.enabledLaoOutput, "KMW set independent keyboard for the incorrect control!"); // Swap to the textarea control with its overridden khmer_angkor keyboard... keyman.setActiveElement(textarea); - eventDriver = new KMWRecorder.BrowserDriver(textarea['kmw_ip'] ? textarea['kmw_ip'] : textarea); + eventDriver = new KMWRecorder.BrowserDriver(textarea); eventDriver.simulateEvent(DynamicElements.keyCommand); val = retrieveAndReset(textarea); assert.equal(val, DynamicElements.enabledKhmerOutput, "KMW did not properly store keyboard for the previously-inactive control!"); @@ -170,158 +169,89 @@ describe('Attachment API', function() { keyman.setActiveElement(input); keyman.setKeyboardForControl(textarea, null, null); - eventDriver = new KMWRecorder.BrowserDriver(input['kmw_ip'] ? input['kmw_ip'] : input); + eventDriver = new KMWRecorder.BrowserDriver(input); eventDriver.simulateEvent(DynamicElements.keyCommand); val = retrieveAndReset(input); assert.equal(val, DynamicElements.enabledLaoOutput, "KMW made a strange error when clearing an inactive control's keyboard setting!"); keyman.setActiveElement(textarea); // Finally, after clearing the independent setting, check that we are back to Lao output as expected for the textarea - eventDriver = new KMWRecorder.BrowserDriver(textarea['kmw_ip'] ? textarea['kmw_ip'] : textarea); + eventDriver = new KMWRecorder.BrowserDriver(textarea); eventDriver.simulateEvent(DynamicElements.keyCommand); val = retrieveAndReset(textarea); assert.equal(val, DynamicElements.enabledLaoOutput, "KMW did not properly clear control's independent keyboard settings!"); }); }); -Modernizr.on('touchevents', function(result) { - if(result) { - describe('Device-specific Attachment Checks (Touch, \'auto\')', function() { - this.timeout(testconfig.timeouts.standard); +describe('Attachment Checks (Desktop, \'auto\')', function() { - before(function() { - this.timeout(testconfig.timeouts.scriptLoad); - - fixture.setBase('fixtures'); - return setupKMW({ attachType:'auto' }, testconfig.timeouts.scriptLoad); - }); - - beforeEach(function() { - fixture.load("robustAttachment.html"); - }); - - after(function() { - teardownKMW(); - }); - - afterEach(function(done) { - fixture.cleanup(); - window.setTimeout(function(){ - done(); - }, testconfig.timeouts.eventDelay); - }); - - describe('Element Type', function() { - it('', function(done) { - var ID = DynamicElements.addInput(); - var ele = document.getElementById(ID); - - DynamicElements.assertAttached(ele, done); - }); - - it('