mirror of
https://github.com/keymanapp/keyman.git
synced 2026-10-08 06:57:29 +00:00
fix(web): restores element text scrolling
This commit is contained in:
parent
21c780f66b
commit
39f30e4385
6 changed files with 101 additions and 114 deletions
|
|
@ -41,6 +41,18 @@ interface EventMap {
|
|||
export class FocusAssistant extends EventEmitter<EventMap> {
|
||||
private _maintainingFocus: boolean = false; // ActivatingKeymanWebUI - Does the OSK have active focus / an active interaction?
|
||||
|
||||
/**
|
||||
* Returns `true` only when the active target has an active `forceScroll` method/state, which deliberately
|
||||
* blurs and then refocuses the same element in order to force a browser-default page scroll to keep the
|
||||
* element and text-caret visible.
|
||||
*/
|
||||
readonly isTargetForcingScroll: () => boolean;
|
||||
|
||||
constructor(isTargetForcingScroll: () => boolean) {
|
||||
super();
|
||||
this.isTargetForcingScroll = isTargetForcingScroll;
|
||||
}
|
||||
|
||||
/*
|
||||
* Long-term idea here: about all of the relevant OSK events that would interact with this have "enter" and
|
||||
* "leave" variants - we could take a stack of `Promise`s. On a `Promise` fulfillment, remove it from the
|
||||
|
|
@ -109,16 +121,6 @@ export class FocusAssistant extends EventEmitter<EventMap> {
|
|||
*/
|
||||
_IgnoreNextSelChange = 0;
|
||||
|
||||
/**
|
||||
* JH (2023-04-24): Set only by the OutputTarget `forceScroll` method, which deliberately blurs and
|
||||
* then refocuses the same element in order to force a browser-default page scroll to keep the element
|
||||
* visible.
|
||||
*
|
||||
* While it feels like this should be possible to merge with the other class fields in some form... it
|
||||
* doesn't seem as safe to do on first glance.
|
||||
*/
|
||||
_IgnoreBlurFocus: boolean = false;
|
||||
|
||||
/**
|
||||
* Is used as a time-delayed async `restoringFocus` or `maintainingFocus` - could be modeled decently as a Promise.
|
||||
* Probably more the latter, as it's a touch-OSK interaction like the other `maintainingFocus` cases.
|
||||
|
|
|
|||
|
|
@ -67,8 +67,10 @@ export class PageIntegrationHandlers {
|
|||
}
|
||||
|
||||
private suppressFocusCheck: (e: FocusEvent) => boolean = (e) => {
|
||||
if(this.focusAssistant._IgnoreBlurFocus) {
|
||||
// Prevent triggering other blur-handling events (as possible)
|
||||
if(this.focusAssistant.isTargetForcingScroll()) {
|
||||
// Prevent triggering other blur-handling events (as possible) - this blur
|
||||
// is programmatic in order to force a browser scroll-position update.
|
||||
// All focus changes should be prevented at this time.
|
||||
e.stopPropagation();
|
||||
e.cancelBubble = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ function _SetTargDir(Ptarg: HTMLElement, activeKeyboard: Keyboard) {
|
|||
export default class ContextManager extends ContextManagerBase<BrowserConfiguration> {
|
||||
private _activeKeyboard: {keyboard: Keyboard, metadata: KeyboardStub};
|
||||
private cookieManager = new CookieSerializer<KeyboardCookie>('KeymanWeb_Keyboard');
|
||||
readonly focusAssistant = new FocusAssistant();
|
||||
readonly focusAssistant = new FocusAssistant(() => this.activeTarget?.isForcingScroll());
|
||||
readonly page: PageContextAttachment;
|
||||
private mostRecentTarget: OutputTarget<any>;
|
||||
private currentTarget: OutputTarget<any>;
|
||||
|
|
@ -231,6 +231,11 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
|
|||
* still trigger a focus event upon it... which can cascade here if uncaught
|
||||
* and trigger a contextReset DURING keyboard rule processing without this
|
||||
* guard.
|
||||
*
|
||||
* The #2 reason: the `forceScroll` method used within the Input and Textarea
|
||||
* types whenever the selection must be programatically updated. The blur
|
||||
* is 'swallowed', preventing it from being dropped as 'active'. However, the
|
||||
* corresponding focus is not swallowed... until this if-condition's check.
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
|
@ -598,9 +603,6 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
|
|||
// Ltarg=Ltarg.contentWindow.document.body; // And we only care about Ltarg b/c of finding the OutputTarget.
|
||||
// }
|
||||
|
||||
// Save it for the event in step 3... but now, before we mutate the field's value!
|
||||
const previousTarget = this.lastActiveTarget;
|
||||
|
||||
// Step 2: Make the newly-focused control the active control, and thus the active context.
|
||||
this.setActiveTarget(target, true);
|
||||
|
||||
|
|
@ -622,7 +624,7 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat
|
|||
return true;
|
||||
}
|
||||
|
||||
if(this.focusAssistant._IgnoreBlurFocus) {
|
||||
if(this.focusAssistant.isTargetForcingScroll()) {
|
||||
// Prevent triggering other blur-handling events (as possible)
|
||||
e.cancelBubble = true;
|
||||
e.stopPropagation();
|
||||
|
|
|
|||
|
|
@ -1,39 +1,6 @@
|
|||
import OutputTarget from './outputTarget.js';
|
||||
|
||||
interface EventMap {
|
||||
/**
|
||||
* Used to facilitate a pre-modularization utility method we wish to maintain:
|
||||
```
|
||||
export function forceScroll(element: HTMLInputElement | HTMLTextAreaElement) {
|
||||
// Only executes when com.keyman.DOMEventHandlers is defined.
|
||||
//
|
||||
// We bypass this whenever operating in the embedded format.
|
||||
if(com && com.keyman && com.keyman['DOMEventHandlers'] && !com.keyman['singleton']['isEmbedded']) {
|
||||
let DOMEventHandlers = com.keyman['DOMEventHandlers'];
|
||||
|
||||
let selectionStart = element.selectionStart;
|
||||
let selectionEnd = element.selectionEnd;
|
||||
|
||||
DOMEventHandlers.states._IgnoreBlurFocus = true;
|
||||
//Forces scrolling; the re-focus triggers the scroll, at least.
|
||||
element.blur();
|
||||
element.focus();
|
||||
DOMEventHandlers.states._IgnoreBlurFocus = false;
|
||||
|
||||
// On Edge, it appears that the blur/focus combination will reset the caret position
|
||||
// under certain scenarios during unit tests. So, we re-set it afterward.
|
||||
element.selectionStart = selectionStart;
|
||||
element.selectionEnd = selectionEnd;
|
||||
}
|
||||
}
|
||||
```
|
||||
* References to the event-handlers & related states objects are not available within this submodule.
|
||||
*
|
||||
* It is the parts between and including the _IgnoreBlurFocus references that must be
|
||||
* implemented externally.
|
||||
*/
|
||||
'scrollfocusrequest': (element: HTMLInputElement) => void,
|
||||
|
||||
/**
|
||||
* This event will be raised when a newline is received by wrapped elements not of
|
||||
* the 'search' or 'submit' types.
|
||||
|
|
@ -74,23 +41,19 @@ export default class Input extends OutputTarget<EventMap> {
|
|||
*/
|
||||
private processedSelectionEnd: number;
|
||||
|
||||
/**
|
||||
* Set, then unset within the `forceScroll` method in order to facilitate the
|
||||
* `isForcingScroll` flag.
|
||||
*/
|
||||
private _activeForcedScroll: boolean;
|
||||
|
||||
constructor(ele: HTMLInputElement) {
|
||||
super();
|
||||
|
||||
this.root = ele;
|
||||
this._cachedSelectionStart = -1;
|
||||
|
||||
// Intended to facilitate reimplmentation of the old `forceScroll` as an event handler
|
||||
// defined externally, but automatically set on class construction.
|
||||
Input.constructorExtensions(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* This may be set to define additional construction behaviors to perform, such as
|
||||
* automatically setting handlers for defined events.
|
||||
*/
|
||||
public static constructorExtensions: (constructingInstance: Input) => void = () => {};
|
||||
|
||||
get isSynthetic(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -146,11 +109,39 @@ export default class Input extends OutputTarget<EventMap> {
|
|||
this.processedSelectionStart = start;
|
||||
this.processedSelectionEnd = end;
|
||||
|
||||
this.events.emit('scrollfocusrequest', this.root);
|
||||
this.forceScroll();
|
||||
|
||||
this.root.setSelectionRange(domStart, domEnd, direction);
|
||||
}
|
||||
|
||||
forceScroll() {
|
||||
// Only executes when com.keyman.DOMEventHandlers is defined.
|
||||
//
|
||||
// We bypass this whenever operating in the embedded format.
|
||||
const element = this.getElement();
|
||||
|
||||
let selectionStart = element.selectionStart;
|
||||
let selectionEnd = element.selectionEnd;
|
||||
|
||||
this._activeForcedScroll = true;
|
||||
|
||||
try {
|
||||
//Forces scrolling; the re-focus triggers the scroll, at least.
|
||||
element.blur();
|
||||
element.focus();
|
||||
} finally {
|
||||
// On Edge, it appears that the blur/focus combination will reset the caret position
|
||||
// under certain scenarios during unit tests. So, we re-set it afterward.
|
||||
element.selectionStart = selectionStart;
|
||||
element.selectionEnd = selectionEnd;
|
||||
this._activeForcedScroll = false;
|
||||
}
|
||||
}
|
||||
|
||||
isForcingScroll(): boolean {
|
||||
return this._activeForcedScroll;
|
||||
}
|
||||
|
||||
getSelectionDirection(): "forward" | "backward" | "none" {
|
||||
return this.root.selectionDirection;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,16 @@ export default abstract class OutputTarget<EventMap extends EventEmitter.ValidEv
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Denotes when the represented element is forcing a text scroll via focus manipulation.
|
||||
* As the intent is not to change the focused element, but just to have the browser update
|
||||
* the scroll location, standard focus handlers (for updating the active context) should
|
||||
* not deactivate the element while this state is active.
|
||||
*/
|
||||
isForcingScroll(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper method for doInputEvent; creates a simple common event and default dispatching.
|
||||
* @param elem
|
||||
|
|
|
|||
|
|
@ -1,41 +1,6 @@
|
|||
import OutputTarget from './outputTarget.js';
|
||||
|
||||
interface EventMap {
|
||||
/**
|
||||
* Used to facilitate a pre-modularization utility method we wish to maintain:
|
||||
```
|
||||
export function forceScroll(element: HTMLInputElement | HTMLTextAreaElement) {
|
||||
// Only executes when com.keyman.DOMEventHandlers is defined.
|
||||
//
|
||||
// We bypass this whenever operating in the embedded format.
|
||||
if(com && com.keyman && com.keyman['DOMEventHandlers'] && !com.keyman['singleton']['isEmbedded']) {
|
||||
let DOMEventHandlers = com.keyman['DOMEventHandlers'];
|
||||
|
||||
let selectionStart = element.selectionStart;
|
||||
let selectionEnd = element.selectionEnd;
|
||||
|
||||
DOMEventHandlers.states._IgnoreBlurFocus = true;
|
||||
//Forces scrolling; the re-focus triggers the scroll, at least.
|
||||
element.blur();
|
||||
element.focus();
|
||||
DOMEventHandlers.states._IgnoreBlurFocus = false;
|
||||
|
||||
// On Edge, it appears that the blur/focus combination will reset the caret position
|
||||
// under certain scenarios during unit tests. So, we re-set it afterward.
|
||||
element.selectionStart = selectionStart;
|
||||
element.selectionEnd = selectionEnd;
|
||||
}
|
||||
}
|
||||
```
|
||||
* References to the event-handlers & related states objects are not available within this submodule.
|
||||
*
|
||||
* It is the parts between and including the _IgnoreBlurFocus references that must be
|
||||
* implemented externally.
|
||||
*/
|
||||
'scrollfocusrequest': (element: HTMLTextAreaElement) => void,
|
||||
}
|
||||
|
||||
export default class TextArea extends OutputTarget<EventMap> {
|
||||
export default class TextArea extends OutputTarget<{}> {
|
||||
root: HTMLTextAreaElement;
|
||||
|
||||
/**
|
||||
|
|
@ -56,31 +21,18 @@ export default class TextArea extends OutputTarget<EventMap> {
|
|||
private processedSelectionEnd: number;
|
||||
|
||||
/**
|
||||
* Used to temporarily store the y-axis scroll coordinate.
|
||||
* Set, then unset within the `forceScroll` method in order to facilitate the
|
||||
* `isForcingScroll` flag.
|
||||
*/
|
||||
private scrollTop?: number;
|
||||
|
||||
/**
|
||||
* Used to temporarily store the x-axis scroll coordinate.
|
||||
*/
|
||||
private scrollLeft?: number;
|
||||
private _activeForcedScroll: boolean;
|
||||
|
||||
constructor(ele: HTMLTextAreaElement) {
|
||||
super();
|
||||
|
||||
this.root = ele;
|
||||
this._cachedSelectionStart = -1;
|
||||
// Intended to facilitate reimplmentation of the old `forceScroll` as an event handler
|
||||
// defined externally, but automatically set on class construction.
|
||||
TextArea.constructorExtensions(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* This may be set to define additional construction behaviors to perform, such as
|
||||
* automatically setting handlers for defined events.
|
||||
*/
|
||||
public static constructorExtensions: (constructingInstance: TextArea) => void = () => {};
|
||||
|
||||
get isSynthetic(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -136,11 +88,39 @@ export default class TextArea extends OutputTarget<EventMap> {
|
|||
this.processedSelectionStart = start;
|
||||
this.processedSelectionEnd = end;
|
||||
|
||||
this.events.emit('scrollfocusrequest', this.root);
|
||||
this.forceScroll();
|
||||
|
||||
this.root.setSelectionRange(domStart, domEnd, direction);
|
||||
}
|
||||
|
||||
forceScroll() {
|
||||
// Only executes when com.keyman.DOMEventHandlers is defined.
|
||||
//
|
||||
// We bypass this whenever operating in the embedded format.
|
||||
const element = this.getElement();
|
||||
|
||||
let selectionStart = element.selectionStart;
|
||||
let selectionEnd = element.selectionEnd;
|
||||
|
||||
this._activeForcedScroll = true;
|
||||
|
||||
try {
|
||||
//Forces scrolling; the re-focus triggers the scroll, at least.
|
||||
element.blur();
|
||||
element.focus();
|
||||
} finally {
|
||||
// On Edge, it appears that the blur/focus combination will reset the caret position
|
||||
// under certain scenarios during unit tests. So, we re-set it afterward.
|
||||
element.selectionStart = selectionStart;
|
||||
element.selectionEnd = selectionEnd;
|
||||
this._activeForcedScroll = false;
|
||||
}
|
||||
}
|
||||
|
||||
isForcingScroll(): boolean {
|
||||
return this._activeForcedScroll;
|
||||
}
|
||||
|
||||
getSelectionDirection(): "forward" | "backward" | "none" {
|
||||
return this.root.selectionDirection;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue