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[] = [];
$: 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> = {};
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 CollapseLabel from "./CollapseLabel.svelte";
import { refocusInput } from "./helpers";
import * as oldEditorAdapter from "./old-editor-adapter";
onMount(() => {
@ -457,21 +491,7 @@ the AddCards dialog) should be implemented in the user of this component.
<svelte:fragment slot="field-label">
<LabelContainer
collapsed={fieldsCollapsed[index]}
on:toggle={async () => {
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;
}
}}
on:toggle={() => toggleField(index)}
--icon-align="bottom"
>
<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] === $focusedField)}
bind:off={richTextsHidden[index]}
on:toggle={async () => {
richTextsHidden[index] =
!richTextsHidden[index];
if (!richTextsHidden[index]) {
refocusInput(richTextInputs[index].api);
}
}}
on:toggle={() => toggleRichTextInput(index)}
/>
{:else}
<PlainTextBadge
@ -504,14 +517,7 @@ the AddCards dialog) should be implemented in the user of this component.
(fields[index] === $hoveredField ||
fields[index] === $focusedField)}
bind:off={plainTextsHidden[index]}
on:toggle={async () => {
plainTextsHidden[index] =
!plainTextsHidden[index];
if (!plainTextsHidden[index]) {
refocusInput(plainTextInputs[index].api);
}
}}
on:toggle={() => togglePlainTextInput(index)}
/>
{/if}
<slot

View file

@ -1,9 +1,6 @@
// Copyright: Ankitects Pty Ltd and contributors
// 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 {
return element.tagName === "FONT";
}
@ -23,14 +20,21 @@ export function withFontColor(
return false;
}
/***
* Required for field inputs wrapped in Collapsible
*/
export async function refocusInput(
api: RichTextInputAPI | PlainTextInputAPI,
): Promise<void> {
do {
await new Promise(window.requestAnimationFrame);
} while (!api.focusable);
api.refocus();
export class Flag {
private flag: boolean;
constructor() {
this.flag = false;
}
setFlag(on: boolean): void {
this.flag = on;
}
/** 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 CodeMirror from "../CodeMirror.svelte";
import { context as editingAreaContext } from "../EditingArea.svelte";
import { Flag } from "../helpers";
import { context as noteEditorContext } from "../NoteEditor.svelte";
import removeProhibitedTags from "./remove-prohibited";
import { storedToUndecorated, undecoratedToStored } from "./transform";
export let hidden = false;
export const focusFlag = new Flag();
$: configuration = {
mode: htmlanki,
@ -115,7 +117,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
$: {
pushUpdate(!hidden);
tick().then(refresh);
tick().then(() => {
refresh();
if (focusFlag.checkAndReset()) {
refocus();
}
});
}
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 { promiseWithResolver } from "@tslib/promise";
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 { 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 { pageTheme } from "../../sveltelib/theme";
import { context as editingAreaContext } from "../EditingArea.svelte";
import { Flag } from "../helpers";
import { context as noteEditorContext } from "../NoteEditor.svelte";
import getNormalizingNodeStore from "./normalizing-node-store";
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";
export let hidden = false;
export const focusFlag = new Flag();
const { focusedInput } = noteEditorContext.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;
}
$: pushUpdate(!hidden);
$: {
pushUpdate(!hidden);
if (focusFlag.checkAndReset()) {
tick().then(refocus);
}
}
onMount(() => {
$editingInputs.push(api);