From 05c8d734f8b87fd5be72c848bbcc40fd40ed8d30 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 4 Apr 2019 10:09:32 +0700 Subject: [PATCH] Enhances 'nearest target' checks and auto-cancels. --- common/predictive-text/package-lock.json | 41 +++----- web/source/dom/uiTouchHandlerBase.ts | 116 ++++++++++++++--------- web/source/osk/banner.ts | 6 +- web/source/osk/visualKeyboard.ts | 9 +- 4 files changed, 90 insertions(+), 82 deletions(-) diff --git a/common/predictive-text/package-lock.json b/common/predictive-text/package-lock.json index 4c369f1ec9..49a9963a70 100644 --- a/common/predictive-text/package-lock.json +++ b/common/predictive-text/package-lock.json @@ -1177,8 +1177,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -1199,14 +1198,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1221,20 +1218,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -1351,8 +1345,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -1364,7 +1357,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -1379,7 +1371,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -1387,14 +1378,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -1413,7 +1402,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -1494,8 +1482,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -1507,7 +1494,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -1593,8 +1579,7 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -1630,7 +1615,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -1650,7 +1634,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -1694,14 +1677,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, diff --git a/web/source/dom/uiTouchHandlerBase.ts b/web/source/dom/uiTouchHandlerBase.ts index 46e426f4f8..49cd4fa7e6 100644 --- a/web/source/dom/uiTouchHandlerBase.ts +++ b/web/source/dom/uiTouchHandlerBase.ts @@ -87,17 +87,20 @@ namespace com.keyman.dom { * * @param {Event} e touch event * @param {Object} t HTML object at touch point + * @param {boolean} omitCurrent Omits any target directly under the touch point. * @return {Object} nearest key to touch point * **/ - findNearestTarget(e: TouchEvent, t: HTMLElement): Target { - if((!e) || (typeof e.changedTouches == 'undefined') - || (e.changedTouches.length == 0)) { + private findTargetFromTouch(e: TouchEvent, t: HTMLElement, forMove: boolean): Target { + let touchList = forMove ? e.touches : e.changedTouches; + + if((!e) || (typeof touchList == 'undefined') + || (touchList.length == 0)) { return null; } // Get touch point on screen - var x = e.changedTouches[0].pageX; + var x = touchList[0].pageX; // Get the UI row beneath touch point (SuggestionBanner div, 'kmw-key-row' if OSK, ...) while(t && t.className !== undefined && t.className.indexOf(this.rowClassMatch) < 0) { @@ -108,24 +111,35 @@ namespace com.keyman.dom { } // Find minimum distance from any key - var k: number, bestMatch=0, dx: number, dxMax=24, dxMin=100000, x1: number, x2: number; + var k: number, bestMatch=0, dxMax=24, dxMin=100000, x1: number, x2: number; for(k = 0; k < t.childNodes.length; k++) { let childNode = t.childNodes[k] as HTMLElement; - /* // If integrating with the OSK, replace with an 'isInvalidTarget' check. - if(childNode.className !== undefined && childNode.className.indexOf('key-hidden') >= 0) { + + if(this.isInvalidTarget(this.findTargetFrom(childNode))) { continue; - }*/ + } + x1 = childNode.offsetLeft; x2 = x1 + childNode.offsetWidth; - dx = x1 - x; - if(dx >= 0 && dx < dxMin) { + + // If it lies completely to the right and is the closest so far + let dxRight = x1 - x; + if(dxRight >= 0 && dxRight < dxMin) { bestMatch = k; - dxMin = dx; + dxMin = dxRight; } - dx = x - x2; - if(dx >= 0 && dx < dxMin) { + + // If it lies completely to the left and is the closest so far + let dxLeft = x - x2; + if(dxLeft >= 0 && dxLeft < dxMin) { bestMatch = k; - dxMin = dx; + dxMin = dxLeft; + } + + // If it is neither completely to the left nor completely to the right, + // it's under the cursor. Stop the search! + if(dxLeft < 0 && dxRight < 0) { + return this.findTargetFrom(childNode); } } @@ -140,17 +154,46 @@ namespace com.keyman.dom { } if(((x1 - x) >= 0 && (x1 - x) < dxMax) || ((x - x2) >= 0 && (x - x2) < dxMax)) { - // If integrating with the OSK, should probably use `findTargetFrom`. - // The OSK would want this (key-square) element's child. - return t; + return this.findTargetFrom(t); } } return null; } + findBestTarget(e: TouchEvent, forMove?: boolean) { + var eventTarget: HTMLElement; + + if(forMove) { + eventTarget = document.elementFromPoint(e.touches[0].clientX, e.touches[0].clientY) as HTMLElement; + } else { + eventTarget = e.changedTouches[0].target as HTMLElement; + } + + let target = this.findTargetFrom(eventTarget); + + // Should refactor this multi-check a bit for more overall reliability. + if(!target) { + // We didn't find a direct target, so we should look for the closest possible one. + // Filters out invalid targets. + target = this.findTargetFromTouch(e, eventTarget, forMove); + } + + return target; + } + + /** + * Reports whether or not a `Target` should be considered invalid. Needed by the OSK for + * hidden keys. + * @param target A `Target` element to be validated. + */ + protected isInvalidTarget(target: Target): boolean { + return false; + } + touchStart(e: TouchEvent) { // Determine the selected Target, manage state. - this.currentTarget = this.findTargetFrom(e.changedTouches[0].target as HTMLElement); + //this.currentTarget = this.findTargetFrom(e.changedTouches[0].target as HTMLElement); + this.currentTarget = this.findBestTarget(e); this.touchX = e.changedTouches[0].pageX; this.touchY = e.changedTouches[0].pageY; @@ -158,16 +201,6 @@ namespace com.keyman.dom { this.touchCount = e.touches.length; - // If option should not be selectable, how do we re-target? - // (If/when the OSK is refactored to use this, we'll need appropriate code here.) - // Probably a 'isInvalidTarget()' and 'nearestTarget()'. - - if(!this.currentTarget) { - // Find nearest target. - this.currentTarget = this.findNearestTarget(e, e.changedTouches[0].target) - } - - // Still no appropriate target? Reject the touch event. if(!this.currentTarget) { return; } @@ -234,12 +267,7 @@ namespace com.keyman.dom { this.pendingTarget = null; // Always clear highlighting of current target on release (multi-touch) } else { - var tt = e.changedTouches[0]; - t = this.findTargetFrom(tt.target as HTMLElement); - if(!t) { - var t1 = document.elementFromPoint(tt.clientX,tt.clientY); - t = this.findNearestTarget(e, t1); - } + t = this.findBestTarget(e); if(t) { this.highlight(t,false); @@ -272,22 +300,14 @@ namespace com.keyman.dom { } // Get touch position - var x=typeof e.touches == 'object' ? e.touches[0].clientX : e.clientX, - y=typeof e.touches == 'object' ? e.touches[0].clientY : e.clientY; + var y=typeof e.touches == 'object' ? e.touches[0].clientY : e.clientY; // Move target key and highlighting - var t1 = document.elementFromPoint(x,y), - key0 = this.pendingTarget, - key1 = this.findTargetFrom(t1); // For the OSK, this ALSO gets subkeys. + var key0 = this.pendingTarget, + key1 = this.findBestTarget(e, true); // For the OSK, this ALSO gets subkeys. // If option should not be selectable, how do we re-target? - // (If/when the OSK is refactored to use this, we'll need appropriate code here.) - // Probably a 'isInvalidTarget()' and 'nearestTarget()'. - // Find the nearest key to the touch point if not on a visible key - if(!key1) { - key1 = this.findNearestTarget(e,t1); - } // Do not move over keys if device popup visible if(this.hasModalPopup()) { @@ -349,7 +369,11 @@ namespace com.keyman.dom { // Cancel touch if moved up and off keyboard, unless popup keys visible } else { let base = this.baseElement; - if(key0 && e.touches[0].pageY < Math.max(5, base.offsetTop - 0.25 * base.offsetHeight)) { + let top = (base.offsetParent as HTMLElement).offsetTop + base.offsetTop; + let height = base.offsetHeight; + let yMin = Math.max(5, top - 0.25 * height); + let yMax = (top + height) + 0.25 * height; + if(key0 && (e.touches[0].pageY < yMin || e.touches[0].pageY > yMax)) { this.highlight(key0,false); this.clearHolds(); this.pendingTarget = null; diff --git a/web/source/osk/banner.ts b/web/source/osk/banner.ts index a001cb8821..b5144911db 100644 --- a/web/source/osk/banner.ts +++ b/web/source/osk/banner.ts @@ -291,9 +291,9 @@ namespace com.keyman.osk { if(e.parentNode && util.hasClass( e.parentNode,'kmw-suggest-option')) { return e.parentNode as HTMLDivElement; } - if(e.firstChild && util.hasClass( e.firstChild,'kmw-suggest-option')) { - return e.firstChild as HTMLDivElement; - } + // if(e.firstChild && util.hasClass( e.firstChild,'kmw-suggest-option')) { + // return e.firstChild as HTMLDivElement; + // } } } catch(ex) {} return null; diff --git a/web/source/osk/visualKeyboard.ts b/web/source/osk/visualKeyboard.ts index 05ae0a9c75..bb8be6aea2 100644 --- a/web/source/osk/visualKeyboard.ts +++ b/web/source/osk/visualKeyboard.ts @@ -1062,9 +1062,12 @@ namespace com.keyman.osk { } // Cancel touch if moved up and off keyboard, unless popup keys visible } else { - let _Box = com.keyman.singleton.osk._Box; - var yMin = Math.max(5, _Box.offsetTop - 0.25*_Box.offsetHeight); - if(key0 && e.touches[0].pageY < Math.max(5,_Box.offsetTop - 0.25*_Box.offsetHeight)) { + // _Box has (most of) the useful client values. + let _Box = this.kbdDiv.offsetParent as HTMLElement; // == osk._Box + let height = (this.kbdDiv.firstChild as HTMLElement).offsetHeight; // firstChild == layer-group, has height info. + // We need to adjust the offset properties by any offsets related to the active banner. + var yMin = Math.max(5, this.kbdDiv.offsetTop + _Box.offsetTop - 0.25*height); + if(key0 && e.touches[0].pageY < yMin) { this.highlightKey(key0,false); this.showKeyTip(null,false); this.keyPending = null;