Refactor plain/rich text input toggling code; fix focus loss (#2479)

* Refactor plain/rich text input toggling code; fix focus loss

Fix:
- Issue where field loses focus when plain/rich text input is closed

Refactoring:
- Call refocus() inside the reactive statement in
  Plain/RichTextInput.svelte to eliminate the need for polling
  with requestAnimationFrame
- Introduce 'Flag' class
- Move 'on:toggle' handlers from inline to functions defined in
  the <script> section for better readability

* Improve code clarity based on feedback from code review

- Rename method and add comment to it
- Add 'private' access modifier to property
This commit is contained in:
Hikaru Y 2023-04-22 15:08:25 +09:00 committed by GitHub
parent 7c225fb5cd
commit 9123821131
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 72 additions and 48 deletions

View file

@ -317,6 +317,41 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let plainTextInputs: PlainTextInput[] = []; let plainTextInputs: PlainTextInput[] = [];
$: plainTextInputs = plainTextInputs.filter(Boolean); $: plainTextInputs = plainTextInputs.filter(Boolean);
function toggleRichTextInput(index: number): void {
const hidden = !richTextsHidden[index];
richTextInputs[index].focusFlag.setFlag(!hidden);
richTextsHidden[index] = hidden;
if (hidden) {
plainTextInputs[index].api.refocus();
}
}
function togglePlainTextInput(index: number): void {
const hidden = !plainTextsHidden[index];
plainTextInputs[index].focusFlag.setFlag(!hidden);
plainTextsHidden[index] = hidden;
if (hidden) {
richTextInputs[index].api.refocus();
}
}
function toggleField(index: number): void {
const collapsed = !fieldsCollapsed[index];
fieldsCollapsed[index] = collapsed;
const defaultInput = !plainTextDefaults[index]
? richTextInputs[index]
: plainTextInputs[index];
if (!collapsed) {
defaultInput.api.refocus();
} else if (!plainTextDefaults[index]) {
plainTextsHidden[index] = true;
} else {
richTextsHidden[index] = true;
}
}
const toolbar: Partial<EditorToolbarAPI> = {}; const toolbar: Partial<EditorToolbarAPI> = {};
function setShrinkImages(shrinkByDefault: boolean) { function setShrinkImages(shrinkByDefault: boolean) {
@ -332,7 +367,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { mathjaxConfig } from "../editable/mathjax-element"; import { mathjaxConfig } from "../editable/mathjax-element";
import CollapseLabel from "./CollapseLabel.svelte"; import CollapseLabel from "./CollapseLabel.svelte";
import { refocusInput } from "./helpers";
import * as oldEditorAdapter from "./old-editor-adapter"; import * as oldEditorAdapter from "./old-editor-adapter";
onMount(() => { onMount(() => {
@ -457,21 +491,7 @@ the AddCards dialog) should be implemented in the user of this component.
<svelte:fragment slot="field-label"> <svelte:fragment slot="field-label">
<LabelContainer <LabelContainer
collapsed={fieldsCollapsed[index]} collapsed={fieldsCollapsed[index]}
on:toggle={async () => { on:toggle={() => toggleField(index)}
fieldsCollapsed[index] = !fieldsCollapsed[index];
const defaultInput = !plainTextDefaults[index]
? richTextInputs[index]
: plainTextInputs[index];
if (!fieldsCollapsed[index]) {
refocusInput(defaultInput.api);
} else if (!plainTextDefaults[index]) {
plainTextsHidden[index] = true;
} else {
richTextsHidden[index] = true;
}
}}
--icon-align="bottom" --icon-align="bottom"
> >
<svelte:fragment slot="field-name"> <svelte:fragment slot="field-name">
@ -489,14 +509,7 @@ the AddCards dialog) should be implemented in the user of this component.
(fields[index] === $hoveredField || (fields[index] === $hoveredField ||
fields[index] === $focusedField)} fields[index] === $focusedField)}
bind:off={richTextsHidden[index]} bind:off={richTextsHidden[index]}
on:toggle={async () => { on:toggle={() => toggleRichTextInput(index)}
richTextsHidden[index] =
!richTextsHidden[index];
if (!richTextsHidden[index]) {
refocusInput(richTextInputs[index].api);
}
}}
/> />
{:else} {:else}
<PlainTextBadge <PlainTextBadge
@ -504,14 +517,7 @@ the AddCards dialog) should be implemented in the user of this component.
(fields[index] === $hoveredField || (fields[index] === $hoveredField ||
fields[index] === $focusedField)} fields[index] === $focusedField)}
bind:off={plainTextsHidden[index]} bind:off={plainTextsHidden[index]}
on:toggle={async () => { on:toggle={() => togglePlainTextInput(index)}
plainTextsHidden[index] =
!plainTextsHidden[index];
if (!plainTextsHidden[index]) {
refocusInput(plainTextInputs[index].api);
}
}}
/> />
{/if} {/if}
<slot <slot

View file

@ -1,9 +1,6 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { PlainTextInputAPI } from "./plain-text-input";
import type { RichTextInputAPI } from "./rich-text-input";
function isFontElement(element: Element): element is HTMLFontElement { function isFontElement(element: Element): element is HTMLFontElement {
return element.tagName === "FONT"; return element.tagName === "FONT";
} }
@ -23,14 +20,21 @@ export function withFontColor(
return false; return false;
} }
/*** export class Flag {
* Required for field inputs wrapped in Collapsible private flag: boolean;
*/
export async function refocusInput( constructor() {
api: RichTextInputAPI | PlainTextInputAPI, this.flag = false;
): Promise<void> { }
do {
await new Promise(window.requestAnimationFrame); setFlag(on: boolean): void {
} while (!api.focusable); this.flag = on;
api.refocus(); }
/** Resets the flag to false and returns the previous value. */
checkAndReset(): boolean {
const val = this.flag;
this.flag = false;
return val;
}
} }

View file

@ -37,11 +37,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { baseOptions, gutterOptions, htmlanki } from "../code-mirror"; import { baseOptions, gutterOptions, htmlanki } from "../code-mirror";
import CodeMirror from "../CodeMirror.svelte"; import CodeMirror from "../CodeMirror.svelte";
import { context as editingAreaContext } from "../EditingArea.svelte"; import { context as editingAreaContext } from "../EditingArea.svelte";
import { Flag } from "../helpers";
import { context as noteEditorContext } from "../NoteEditor.svelte"; import { context as noteEditorContext } from "../NoteEditor.svelte";
import removeProhibitedTags from "./remove-prohibited"; import removeProhibitedTags from "./remove-prohibited";
import { storedToUndecorated, undecoratedToStored } from "./transform"; import { storedToUndecorated, undecoratedToStored } from "./transform";
export let hidden = false; export let hidden = false;
export const focusFlag = new Flag();
$: configuration = { $: configuration = {
mode: htmlanki, mode: htmlanki,
@ -115,7 +117,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
$: { $: {
pushUpdate(!hidden); pushUpdate(!hidden);
tick().then(refresh); tick().then(() => {
refresh();
if (focusFlag.checkAndReset()) {
refocus();
}
});
} }
function onChange({ detail: html }: CustomEvent<string>): void { function onChange({ detail: html }: CustomEvent<string>): void {

View file

@ -64,7 +64,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { directionKey, fontFamilyKey, fontSizeKey } from "@tslib/context-keys"; import { directionKey, fontFamilyKey, fontSizeKey } from "@tslib/context-keys";
import { promiseWithResolver } from "@tslib/promise"; import { promiseWithResolver } from "@tslib/promise";
import { singleCallback } from "@tslib/typing"; import { singleCallback } from "@tslib/typing";
import { getAllContexts, getContext, onMount } from "svelte"; import { getAllContexts, getContext, onMount, tick } from "svelte";
import type { Readable } from "svelte/store"; import type { Readable } from "svelte/store";
import { placeCaretAfterContent } from "../../domlib/place-caret"; import { placeCaretAfterContent } from "../../domlib/place-caret";
@ -73,6 +73,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import useInputHandler from "../../sveltelib/input-handler"; import useInputHandler from "../../sveltelib/input-handler";
import { pageTheme } from "../../sveltelib/theme"; import { pageTheme } from "../../sveltelib/theme";
import { context as editingAreaContext } from "../EditingArea.svelte"; import { context as editingAreaContext } from "../EditingArea.svelte";
import { Flag } from "../helpers";
import { context as noteEditorContext } from "../NoteEditor.svelte"; import { context as noteEditorContext } from "../NoteEditor.svelte";
import getNormalizingNodeStore from "./normalizing-node-store"; import getNormalizingNodeStore from "./normalizing-node-store";
import useRichTextResolve from "./rich-text-resolve"; import useRichTextResolve from "./rich-text-resolve";
@ -80,6 +81,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { fragmentToStored, storedToFragment } from "./transform"; import { fragmentToStored, storedToFragment } from "./transform";
export let hidden = false; export let hidden = false;
export const focusFlag = new Flag();
const { focusedInput } = noteEditorContext.get(); const { focusedInput } = noteEditorContext.get();
const { content, editingInputs } = editingAreaContext.get(); const { content, editingInputs } = editingAreaContext.get();
@ -192,7 +194,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
$apiStore = null; $apiStore = null;
} }
$: pushUpdate(!hidden); $: {
pushUpdate(!hidden);
if (focusFlag.checkAndReset()) {
tick().then(refocus);
}
}
onMount(() => { onMount(() => {
$editingInputs.push(api); $editingInputs.push(api);