Fix IME input after tab (#1584)

* Avoid initial call of mirror-dom

* Disable updateFocus from OldEditorAdapter

* fixes IME input duplication bug

* Fix saving of latestLocation for ContentEditable

* Fix IME after calling placeCaretAfterContent

* Export other libraries from domlib/index.ts

* Remove dead code

+ Uncomment code which was mistakenly left commmented
This commit is contained in:
Henrik Giesel 2022-01-11 23:39:41 +01:00 committed by GitHub
parent 4e4d12a62a
commit 489eadb352
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 90 additions and 77 deletions

View file

@ -1,5 +1,7 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export * as location from "./location";
export * as surround from "./surround";
export * from "./location";
export * from "./surround";
export * from "./move-nodes";
export * from "./place-caret";

View file

@ -12,7 +12,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import actionList from "../sveltelib/action-list";
import contentEditableAPI, {
saveLocation,
prepareFocusHandling,
initialFocusHandling,
preventBuiltinContentEditableShortcuts,
} from "./content-editable";
import type { ContentEditableAPI } from "./content-editable";
@ -40,7 +40,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
contenteditable="true"
use:resolve
use:saveLocation
use:prepareFocusHandling
use:initialFocusHandling
use:preventBuiltinContentEditableShortcuts
use:mirrorAction={mirrorOptions}
use:managerAction={{}}

View file

@ -17,30 +17,42 @@ function flushLocation(): void {
}
}
let latestLocation: SelectionLocation | null = null;
function safePlaceCaretAfterContent(editable: HTMLElement): void {
/**
* Workaround: If you try to invoke an IME after calling
* `placeCaretAfterContent` on a cE element, the IME will immediately
* end and the input character will be duplicated
*/
placeCaretAfterContent(editable);
restoreSelection(editable, saveSelection(editable)!);
}
function onFocus(this: HTMLElement): void {
if (!latestLocation) {
placeCaretAfterContent(this);
return;
}
function onFocus(location: SelectionLocation | null): () => void {
return function (this: HTMLElement): void {
if (!location) {
safePlaceCaretAfterContent(this);
return;
}
try {
restoreSelection(this, latestLocation);
} catch {
placeCaretAfterContent(this);
}
try {
restoreSelection(this, location);
} catch {
safePlaceCaretAfterContent(this);
}
};
}
function onBlur(this: HTMLElement): void {
prepareFocusHandling(this);
latestLocation = saveSelection(this);
prepareFocusHandling(this, saveSelection(this));
}
let removeOnFocus: () => void;
export function prepareFocusHandling(editable: HTMLElement): void {
removeOnFocus = on(editable, "focus", onFocus, { once: true });
function prepareFocusHandling(
editable: HTMLElement,
latestLocation: SelectionLocation | null = null,
): void {
const removeOnFocus = on(editable, "focus", onFocus(latestLocation), {
once: true,
});
locationEvents.push(
removeOnFocus,
@ -48,6 +60,10 @@ export function prepareFocusHandling(editable: HTMLElement): void {
);
}
export function initialFocusHandling(editable: HTMLElement): void {
prepareFocusHandling(editable);
}
/* Must execute before DOMMirror */
export function saveLocation(editable: HTMLElement): { destroy(): void } {
const removeOnBlur = on(editable, "blur", onBlur);

View file

@ -8,7 +8,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { PlainTextInputAPI } from "./PlainTextInput.svelte";
import type { EditorToolbarAPI } from "./EditorToolbar.svelte";
import { registerShortcut } from "../lib/shortcuts";
import contextProperty from "../sveltelib/context-property";
import { writable, get } from "svelte/store";
@ -27,15 +26,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
const activeInput = writable<RichTextInputAPI | PlainTextInputAPI | null>(null);
const currentField = writable<EditorFieldAPI | null>(null);
function updateFocus() {
get(activeInput)?.moveCaretToEnd();
}
registerShortcut(
() => document.addEventListener("focusin", updateFocus, { once: true }),
"Shift?+Tab",
);
</script>
<script lang="ts">
@ -67,6 +57,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { onMount, onDestroy } from "svelte";
import type { Writable } from "svelte/store";
import { bridgeCommand } from "../lib/bridgecommand";
import { registerShortcut } from "../lib/shortcuts";
import { isApplePlatform } from "../lib/platform";
import { ChangeTimer } from "./change-timer";
import { alertIcon } from "./icons";

View file

@ -110,7 +110,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
/* we need createContextualFragment so that customElements are initialized */
const fragment = range.createContextualFragment(adjustInputHTML(html));
adjustInputFragment(fragment);
nodes.setUnprocessed(fragment);
}

View file

@ -40,6 +40,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let selectAll = false;
function placeHandle(after: boolean): void {
flushLocation();
if (after) {
(mathjaxElement as any).placeCaretAfter();
} else {
@ -48,7 +50,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
async function resetHandle(): Promise<void> {
flushLocation();
selectAll = false;
if (activeImage && mathjaxElement) {

View file

@ -4,7 +4,7 @@
import { writable } from "svelte/store";
import type { Writable } from "svelte/store";
import storeSubscribe from "./store-subscribe";
// import { getSelection } from "../lib/cross-browser";
import { on } from "../lib/events";
const config = {
childList: true,
@ -14,7 +14,7 @@ const config = {
};
export type MirrorAction = (
element: Element,
element: HTMLElement,
params: { store: Writable<DocumentFragment> },
) => { destroy(): void };
@ -23,10 +23,22 @@ interface DOMMirrorAPI {
preventResubscription(): () => void;
}
function cloneNode(node: Node): DocumentFragment {
/**
* Creates a deep clone
* This seems to be less buggy than node.cloneNode(true)
*/
const range = document.createRange();
range.selectNodeContents(node);
return range.cloneContents();
}
/**
* Allows you to keep an element's inner HTML bidirectionally
* in sync with a store containing a DocumentFragment.
* While the element has focus, this connection is tethered.
* In practice, this will sync changes from PlainTextInput to RichTextInput.
*/
function getDOMMirror(): DOMMirrorAPI {
const allowResubscription = writable(true);
@ -40,68 +52,58 @@ function getDOMMirror(): DOMMirrorAPI {
}
function mirror(
element: Element,
element: HTMLElement,
{ store }: { store: Writable<DocumentFragment> },
): { destroy(): void } {
function saveHTMLToStore(): void {
const range = document.createRange();
range.selectNodeContents(element);
const contents = range.cloneContents();
store.set(contents);
store.set(cloneNode(element));
}
const observer = new MutationObserver(saveHTMLToStore);
observer.observe(element, config);
function mirrorToNode(node: Node): void {
function mirrorToElement(node: Node): void {
observer.disconnect();
const clone = node.cloneNode(true);
/* TODO use Element.replaceChildren */
// element.replaceChildren(...node.childNodes); // TODO use once available
while (element.firstChild) {
element.firstChild.remove();
}
while (clone.firstChild) {
element.appendChild(clone.firstChild);
while (node.firstChild) {
element.appendChild(node.firstChild);
}
observer.observe(element, config);
}
const { subscribe, unsubscribe } = storeSubscribe(store, mirrorToNode);
// const selection = getSelection(element)!;
function doSubscribe(): void {
// Might not be needed after all:
// /**
// * Focused element and caret are two independent things in the browser.
// * When the ContentEditable calls blur, it will still have the selection inside of it.
// * Some elements (e.g. FrameElement) need to figure whether the intended focus is still
// * in the contenteditable or elsewhere because they might change the selection.
// */
// selection.removeAllRanges();
subscribe();
function mirrorFromFragment(fragment: DocumentFragment): void {
mirrorToElement(cloneNode(fragment));
}
const { subscribe, unsubscribe } = storeSubscribe(
store,
mirrorFromFragment,
false,
);
/* do not update when focused as it will reset caret */
element.addEventListener("focus", unsubscribe);
const removeFocus = on(element, "focus", unsubscribe);
let removeBlur: (() => void) | undefined;
const unsubResubscription = allowResubscription.subscribe(
(allow: boolean): void => {
if (allow) {
element.addEventListener("blur", doSubscribe);
if (!removeBlur) {
removeBlur = on(element, "blur", subscribe);
}
const root = element.getRootNode() as Document | ShadowRoot;
if (root.activeElement !== element) {
doSubscribe();
subscribe();
}
} else {
element.removeEventListener("blur", doSubscribe);
} else if (removeBlur) {
removeBlur();
removeBlur = undefined;
}
},
);
@ -110,9 +112,8 @@ function getDOMMirror(): DOMMirrorAPI {
destroy() {
observer.disconnect();
// removes blur event listener
allowResubscription.set(false);
element.removeEventListener("focus", unsubscribe);
removeFocus();
removeBlur?.();
unsubscribe();
unsubResubscription();

View file

@ -15,12 +15,13 @@ export function nodeStore<T extends Node>(
const subscribers: Set<Subscriber<T>> = new Set();
function setUnprocessed(newNode: T): void {
if (!node || !node.isEqualNode(newNode)) {
node = newNode;
if (node && node.isEqualNode(newNode)) {
return;
}
for (const subscriber of subscribers) {
subscriber(node);
}
node = newNode;
for (const subscriber of subscribers) {
subscriber(node);
}
}

View file

@ -8,7 +8,9 @@ interface StoreAccessors {
unsubscribe: () => void;
}
// Prevent double subscriptions / unsubscriptions
/**
* Helper function to prevent double (un)subscriptions
*/
function storeSubscribe<T>(
store: Readable<T>,
callback: (value: T) => void,