Work around issue with entering text around MathJax via IME (#2288)

* Add 'placement' property

* Extract logic for moving text node into instance method

... so that it can be used elsewhere.

* Add writable store to indicate whether composition session is active

* Work around issue with entering text around MathJax via IME

* Make get() called only once while composition session is active
This commit is contained in:
Hikaru Y 2022-12-30 12:32:41 +09:00 committed by GitHub
parent 7d22053730
commit be6d1dfb66
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 79 additions and 16 deletions

View file

@ -4,9 +4,12 @@
import { getSelection, isSelectionCollapsed } from "@tslib/cross-browser"; import { getSelection, isSelectionCollapsed } from "@tslib/cross-browser";
import { elementIsEmpty, nodeIsElement, nodeIsText } from "@tslib/dom"; import { elementIsEmpty, nodeIsElement, nodeIsText } from "@tslib/dom";
import { on } from "@tslib/events"; import { on } from "@tslib/events";
import type { Unsubscriber } from "svelte/store";
import { get } from "svelte/store";
import { moveChildOutOfElement } from "../domlib/move-nodes"; import { moveChildOutOfElement } from "../domlib/move-nodes";
import { placeCaretAfter } from "../domlib/place-caret"; import { placeCaretAfter } from "../domlib/place-caret";
import { isComposing } from "../sveltelib/composition";
import type { FrameElement } from "./frame-element"; import type { FrameElement } from "./frame-element";
/** /**
@ -53,7 +56,6 @@ function restoreHandleContent(mutations: MutationRecord[]): void {
} }
const handleElement = target; const handleElement = target;
const placement = handleElement instanceof FrameStart ? "beforebegin" : "afterend";
const frameElement = handleElement.parentElement as FrameElement; const frameElement = handleElement.parentElement as FrameElement;
for (const node of mutation.addedNodes) { for (const node of mutation.addedNodes) {
@ -75,7 +77,7 @@ function restoreHandleContent(mutations: MutationRecord[]): void {
referenceNode = moveChildOutOfElement( referenceNode = moveChildOutOfElement(
frameElement, frameElement,
node, node,
placement, handleElement.placement,
); );
} }
} }
@ -84,25 +86,16 @@ function restoreHandleContent(mutations: MutationRecord[]): void {
!nodeIsText(target) !nodeIsText(target)
|| !isFrameHandle(target.parentElement) || !isFrameHandle(target.parentElement)
|| skippableNode(target.parentElement, target) || skippableNode(target.parentElement, target)
|| target.parentElement.unsubscribe
) { ) {
continue; continue;
} }
if (get(isComposing)) {
const handleElement = target.parentElement; target.parentElement.subscribeToCompositionEvent();
const placement = handleElement instanceof FrameStart ? "beforebegin" : "afterend"; continue;
const frameElement = handleElement.parentElement! as FrameElement;
const cleaned = target.data.replace(spaceRegex, "");
const text = new Text(cleaned);
if (placement === "beforebegin") {
frameElement.before(text);
} else {
frameElement.after(text);
} }
handleElement.refreshSpace(); referenceNode = target.parentElement.moveTextOutOfFrame();
referenceNode = text;
} }
} }
@ -114,6 +107,8 @@ function restoreHandleContent(mutations: MutationRecord[]): void {
const handleObserver = new MutationObserver(restoreHandleContent); const handleObserver = new MutationObserver(restoreHandleContent);
const handles: Set<FrameHandle> = new Set(); const handles: Set<FrameHandle> = new Set();
type Placement = Extract<InsertPosition, "beforebegin" | "afterend">;
export abstract class FrameHandle extends HTMLElement { export abstract class FrameHandle extends HTMLElement {
static get observedAttributes(): string[] { static get observedAttributes(): string[] {
return ["data-frames"]; return ["data-frames"];
@ -128,6 +123,8 @@ export abstract class FrameHandle extends HTMLElement {
*/ */
partiallySelected = false; partiallySelected = false;
frames?: string; frames?: string;
abstract placement: Placement;
unsubscribe: Unsubscriber | null;
constructor() { constructor() {
super(); super();
@ -136,6 +133,7 @@ export abstract class FrameHandle extends HTMLElement {
subtree: true, subtree: true,
characterData: true, characterData: true,
}); });
this.unsubscribe = null;
} }
attributeChangedCallback(name: string, old: string, newValue: string): void { attributeChangedCallback(name: string, old: string, newValue: string): void {
@ -197,13 +195,54 @@ export abstract class FrameHandle extends HTMLElement {
this.removeMoveIn?.(); this.removeMoveIn?.();
this.removeMoveIn = undefined; this.removeMoveIn = undefined;
this.unsubscribeToCompositionEvent();
} }
abstract notifyMoveIn(offset: number): void; abstract notifyMoveIn(offset: number): void;
moveTextOutOfFrame(): Text {
const frameElement = this.parentElement! as FrameElement;
const cleaned = this.innerHTML.replace(spaceRegex, "");
const text = new Text(cleaned);
if (this.placement === "beforebegin") {
frameElement.before(text);
} else if (this.placement === "afterend") {
frameElement.after(text);
}
this.refreshSpace();
return text;
}
/**
* https://github.com/ankitects/anki/issues/2251
*
* Work around the issue by not moving the input string while an IME session
* is active, and moving the final output from IME only after the session ends.
*/
subscribeToCompositionEvent(): void {
this.unsubscribe = isComposing.subscribe((composing) => {
if (!composing) {
placeCaretAfter(this.moveTextOutOfFrame());
this.unsubscribeToCompositionEvent();
}
});
}
unsubscribeToCompositionEvent(): void {
this.unsubscribe?.();
this.unsubscribe = null;
}
} }
export class FrameStart extends FrameHandle { export class FrameStart extends FrameHandle {
static tagName = "frame-start"; static tagName = "frame-start";
placement: Placement;
constructor() {
super();
this.placement = "beforebegin";
}
getFrameRange(): Range { getFrameRange(): Range {
const range = new Range(); const range = new Range();
@ -245,6 +284,12 @@ export class FrameStart extends FrameHandle {
export class FrameEnd extends FrameHandle { export class FrameEnd extends FrameHandle {
static tagName = "frame-end"; static tagName = "frame-end";
placement: Placement;
constructor() {
super();
this.placement = "afterend";
}
getFrameRange(): Range { getFrameRange(): Range {
const range = new Range(); const range = new Range();

View file

@ -11,6 +11,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type CodeMirrorLib from "codemirror"; import type CodeMirrorLib from "codemirror";
import { tick } from "svelte"; import { tick } from "svelte";
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import { isComposing } from "sveltelib/composition";
import Popover from "../../components/Popover.svelte"; import Popover from "../../components/Popover.svelte";
import Shortcut from "../../components/Shortcut.svelte"; import Shortcut from "../../components/Shortcut.svelte";
@ -79,6 +80,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
const code = writable(""); const code = writable("");
function showOverlay(image: HTMLImageElement, pos?: CodeMirrorLib.Position) { function showOverlay(image: HTMLImageElement, pos?: CodeMirrorLib.Position) {
if ($isComposing) {
// Should be canceled while an IME composition session is active
return;
}
const [promise, allowResolve] = promiseWithResolver<void>(); const [promise, allowResolve] = promiseWithResolver<void>();
allowPromise = promise; allowPromise = promise;

View file

@ -0,0 +1,12 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { writable } from "svelte/store";
/**
* Indicates whether an IME composition session is currently active
*/
export const isComposing = writable(false);
window.addEventListener("compositionstart", () => isComposing.set(true));
window.addEventListener("compositionend", () => isComposing.set(false));