mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 14:32:22 -04:00
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:
parent
4e4d12a62a
commit
489eadb352
9 changed files with 90 additions and 77 deletions
|
@ -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";
|
||||
|
|
|
@ -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={{}}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue