diff --git a/apps/client/src/widgets/note_title.tsx b/apps/client/src/widgets/note_title.tsx index f886d9f7db..76a86104c2 100644 --- a/apps/client/src/widgets/note_title.tsx +++ b/apps/client/src/widgets/note_title.tsx @@ -1,15 +1,17 @@ -import { useEffect, useRef, useState } from "preact/hooks"; -import { t } from "../services/i18n"; -import FormTextBox from "./react/FormTextBox"; -import { useNoteContext, useNoteProperty, useSpacedUpdate, useTriliumEvent, useTriliumEvents } from "./react/hooks"; -import protected_session_holder from "../services/protected_session_holder"; -import server from "../services/server"; import "./note_title.css"; -import { isLaunchBarConfig } from "../services/utils"; + +import clsx from "clsx"; +import { useEffect, useRef, useState } from "preact/hooks"; + import appContext from "../components/app_context"; import branches from "../services/branches"; +import { t } from "../services/i18n"; +import protected_session_holder from "../services/protected_session_holder"; +import server from "../services/server"; import { isIMEComposing } from "../services/shortcuts"; -import clsx from "clsx"; +import { isLaunchBarConfig } from "../services/utils"; +import FormTextBox from "./react/FormTextBox"; +import { useNoteContext, useNoteProperty, useSpacedUpdate, useTriliumEvent, useTriliumEvents } from "./react/hooks"; export default function NoteTitleWidget(props: {className?: string}) { const { note, noteId, componentId, viewScope, noteContext, parentComponent } = useNoteContext(); @@ -58,11 +60,28 @@ export default function NoteTitleWidget(props: {className?: string}) { // Manage focus. const textBoxRef = useRef(null); const isNewNote = useRef(); + const pendingSelect = useRef(false); + + // Re-apply selection when title changes if we have a pending select. + // This handles the case where the server sends back entity changes after we've + // already called select(), which causes the controlled input to re-render and lose selection. + useEffect(() => { + if (pendingSelect.current && textBoxRef.current && document.activeElement === textBoxRef.current) { + textBoxRef.current.select(); + } + }, [title]); + useTriliumEvents([ "focusOnTitle", "focusAndSelectTitle" ], (e, eventName) => { if (noteContext?.isActive() && textBoxRef.current) { + // In the new layout, there are two NoteTitleWidget instances. Only handle if visible. + if (!textBoxRef.current.checkVisibility({ checkOpacity: true })) { + return; + } + textBoxRef.current.focus(); if (eventName === "focusAndSelectTitle") { textBoxRef.current.select(); + pendingSelect.current = true; } isNewNote.current = ("isNewNote" in e ? e.isNewNote : false); } @@ -83,6 +102,9 @@ export default function NoteTitleWidget(props: {className?: string}) { spacedUpdate.scheduleUpdate(); }} onKeyDown={(e) => { + // User started typing, stop re-applying selection + pendingSelect.current = false; + // Skip processing if IME is composing to prevent interference // with text input in CJK languages if (isIMEComposing(e)) { @@ -101,6 +123,7 @@ export default function NoteTitleWidget(props: {className?: string}) { } }} onBlur={() => { + pendingSelect.current = false; spacedUpdate.updateNowIfNecessary(); isNewNote.current = false; }}