diff --git a/apps/client/src/services/note_autocomplete.ts b/apps/client/src/services/note_autocomplete.ts
index f6ada1967..ea7e8cd8d 100644
--- a/apps/client/src/services/note_autocomplete.ts
+++ b/apps/client/src/services/note_autocomplete.ts
@@ -38,7 +38,7 @@ export interface Suggestion {
commandShortcut?: string;
}
-interface Options {
+export interface Options {
container?: HTMLElement;
fastSearch?: boolean;
allowCreatingNotes?: boolean;
@@ -82,12 +82,12 @@ async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void
// Check if we're in command mode
if (options.isCommandPalette && term.startsWith(">")) {
const commandQuery = term.substring(1).trim();
-
+
// Get commands (all if no query, filtered if query provided)
- const commands = commandQuery.length === 0
+ const commands = commandQuery.length === 0
? commandRegistry.getAllCommands()
: commandRegistry.searchCommands(commandQuery);
-
+
// Convert commands to suggestions
const commandSuggestions: Suggestion[] = commands.map(cmd => ({
action: "command",
@@ -99,7 +99,7 @@ async function autocompleteSource(term: string, cb: (rows: Suggestion[]) => void
commandShortcut: cmd.shortcut,
icon: cmd.icon
}));
-
+
cb(commandSuggestions);
return;
}
diff --git a/apps/client/src/translations/cn/translation.json b/apps/client/src/translations/cn/translation.json
index 99d0cea83..40cecaf6e 100644
--- a/apps/client/src/translations/cn/translation.json
+++ b/apps/client/src/translations/cn/translation.json
@@ -211,7 +211,7 @@
},
"jump_to_note": {
"close": "关闭",
- "search_button": "全文搜索 Ctrl+回车"
+ "search_button": "全文搜索"
},
"markdown_import": {
"dialog_title": "Markdown 导入",
diff --git a/apps/client/src/translations/de/translation.json b/apps/client/src/translations/de/translation.json
index 77024d63c..6d3136844 100644
--- a/apps/client/src/translations/de/translation.json
+++ b/apps/client/src/translations/de/translation.json
@@ -211,7 +211,7 @@
},
"jump_to_note": {
"close": "Schließen",
- "search_button": "Suche im Volltext: Strg+Eingabetaste"
+ "search_button": "Suche im Volltext"
},
"markdown_import": {
"dialog_title": "Markdown-Import",
diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json
index 2410c56b6..1d104739a 100644
--- a/apps/client/src/translations/en/translation.json
+++ b/apps/client/src/translations/en/translation.json
@@ -213,7 +213,7 @@
"jump_to_note": {
"search_placeholder": "Search for note by its name or type > for commands...",
"close": "Close",
- "search_button": "Search in full text Ctrl+Enter"
+ "search_button": "Search in full text"
},
"markdown_import": {
"dialog_title": "Markdown import",
diff --git a/apps/client/src/translations/es/translation.json b/apps/client/src/translations/es/translation.json
index 9df16d534..7aeae1655 100644
--- a/apps/client/src/translations/es/translation.json
+++ b/apps/client/src/translations/es/translation.json
@@ -212,7 +212,7 @@
},
"jump_to_note": {
"close": "Cerrar",
- "search_button": "Buscar en texto completo Ctrl+Enter"
+ "search_button": "Buscar en texto completo"
},
"markdown_import": {
"dialog_title": "Importación de Markdown",
diff --git a/apps/client/src/translations/fr/translation.json b/apps/client/src/translations/fr/translation.json
index f30ce130f..85e1bb0fc 100644
--- a/apps/client/src/translations/fr/translation.json
+++ b/apps/client/src/translations/fr/translation.json
@@ -211,7 +211,7 @@
},
"jump_to_note": {
"close": "Fermer",
- "search_button": "Rechercher dans le texte intégral Ctrl+Entrée"
+ "search_button": "Rechercher dans le texte intégral"
},
"markdown_import": {
"dialog_title": "Importation Markdown",
diff --git a/apps/client/src/translations/ro/translation.json b/apps/client/src/translations/ro/translation.json
index ee6369d85..101b9e779 100644
--- a/apps/client/src/translations/ro/translation.json
+++ b/apps/client/src/translations/ro/translation.json
@@ -767,7 +767,7 @@
"title": "Atribute moștenite"
},
"jump_to_note": {
- "search_button": "Caută în întregul conținut Ctrl+Enter",
+ "search_button": "Caută în întregul conținut",
"close": "Închide",
"search_placeholder": "Căutați notițe după nume sau tastați > pentru comenzi..."
},
diff --git a/apps/client/src/translations/tw/translation.json b/apps/client/src/translations/tw/translation.json
index f9cd5595f..5e10258d7 100644
--- a/apps/client/src/translations/tw/translation.json
+++ b/apps/client/src/translations/tw/translation.json
@@ -194,7 +194,7 @@
"okButton": "確定"
},
"jump_to_note": {
- "search_button": "全文搜尋 Ctrl+Enter"
+ "search_button": "全文搜尋"
},
"markdown_import": {
"dialog_title": "Markdown 匯入",
diff --git a/apps/client/src/widgets/dialogs/add_link.tsx b/apps/client/src/widgets/dialogs/add_link.tsx
index c0d344cd2..bca86460a 100644
--- a/apps/client/src/widgets/dialogs/add_link.tsx
+++ b/apps/client/src/widgets/dialogs/add_link.tsx
@@ -106,9 +106,11 @@ function AddLinkDialogComponent({ text: _text, textTypeWidget }: AddLinkDialogPr
element next to the search input - // this is important because the search input value is replaced with the suggestion note's title - .autocomplete("val", this.$autoComplete.next().text()) - .trigger("focus") - .trigger("select"); - - // Update command mode state based on the restored value - this.updateCommandModeState(); - - // If we restored a command mode value, manually trigger command display - if (this.isCommandMode) { - // Clear the value first, then set it to ">" to trigger a proper change - this.$autoComplete.autocomplete("val", ""); - noteAutocompleteService.showAllCommands(this.$autoComplete); - } - } - } - } - - showInFullText(e: JQuery.TriggeredEvent | KeyboardEvent) { - // stop from propagating upwards (dangerous, especially with ctrl+enter executable javascript notes) - e.preventDefault(); - e.stopPropagation(); - - // Don't perform full text search in command mode - if (this.isCommandMode) { - return; - } - - const searchString = String(this.$autoComplete.val()); - - this.triggerCommand("searchNotes", { searchString }); - this.modal.hide(); - } -} diff --git a/apps/client/src/widgets/dialogs/jump_to_note.tsx b/apps/client/src/widgets/dialogs/jump_to_note.tsx new file mode 100644 index 000000000..5ab28b8c5 --- /dev/null +++ b/apps/client/src/widgets/dialogs/jump_to_note.tsx @@ -0,0 +1,130 @@ +import { closeActiveDialog, openDialog } from "../../services/dialog"; +import ReactBasicWidget from "../react/ReactBasicWidget"; +import Modal from "../react/Modal"; +import Button from "../react/Button"; +import NoteAutocomplete from "../react/NoteAutocomplete"; +import { t } from "../../services/i18n"; +import { useEffect, useRef, useState } from "preact/hooks"; +import note_autocomplete, { Suggestion } from "../../services/note_autocomplete"; +import appContext from "../../components/app_context"; +import commandRegistry from "../../services/command_registry"; + +const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120; + +type Mode = "last-search" | "recent-notes" | "commands"; + +interface JumpToNoteDialogProps { + mode: Mode; +} + +function JumpToNoteDialogComponent({ mode }: JumpToNoteDialogProps) { + const containerRef = useRef(null); + const autocompleteRef = useRef (null); + const [ isCommandMode, setIsCommandMode ] = useState(mode === "commands"); + const [ text, setText ] = useState(isCommandMode ? "> " : ""); + + console.log(`Got text '${text}'`); + + console.log("Rendering with mode:", mode, "isCommandMode:", isCommandMode); + + useEffect(() => { + setIsCommandMode(text.startsWith(">")); + }, [ text ]); + + async function onItemSelected(suggestion: Suggestion) { + if (suggestion.notePath) { + appContext.tabManager.getActiveContext()?.setNote(suggestion.notePath); + } else if (suggestion.commandId) { + closeActiveDialog(); + await commandRegistry.executeCommand(suggestion.commandId); + } + } + + function onShown() { + const $autoComplete = $(autocompleteRef.current); + switch (mode) { + case "last-search": + break; + case "recent-notes": + note_autocomplete.showRecentNotes($autoComplete); + break; + case "commands": + note_autocomplete.showAllCommands($autoComplete); + break; + } + + $autoComplete + .trigger("focus") + .trigger("select"); + } + + return ( + } + onShown={onShown} + footer={!isCommandMode && } + > + + + ); +} + +export default class JumpToNoteDialog extends ReactBasicWidget { + + private lastOpenedTs: number; + private props: JumpToNoteDialogProps = { + mode: "last-search" + }; + + get component() { + return ; + } + + async openDialog(commandMode = false) { + this.lastOpenedTs = Date.now(); + + let newMode: Mode; + if (commandMode) { + newMode = "commands"; + } else if (Date.now() - this.lastOpenedTs > KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000) { + // if you open the Jump To dialog soon after using it previously, it can often mean that you + // actually want to search for the same thing (e.g., you opened the wrong note at first try) + // so we'll keep the content. + // if it's outside of this time limit, then we assume it's a completely new search and show recent notes instead. + newMode = "recent-notes"; + } else { + newMode = "last-search"; + } + + if (this.props.mode !== newMode) { + this.props.mode = newMode; + this.doRender(); + } + + openDialog(this.$widget); + } + + async jumpToNoteEvent() { + await this.openDialog(); + } + + async commandPaletteEvent() { + await this.openDialog(true); + } + +} \ No newline at end of file diff --git a/apps/client/src/widgets/react/Modal.tsx b/apps/client/src/widgets/react/Modal.tsx index 43e6b12ee..96b00af2e 100644 --- a/apps/client/src/widgets/react/Modal.tsx +++ b/apps/client/src/widgets/react/Modal.tsx @@ -5,7 +5,7 @@ import type { CSSProperties } from "preact/compat"; interface ModalProps { className: string; - title: string; + title: string | ComponentChildren; size: "lg" | "sm"; children: ComponentChildren; footer?: ComponentChildren; @@ -49,7 +49,11 @@ export default function Modal({ children, className, size, title, footer, onShow -); } \ No newline at end of file{title}
+ {typeof title === "string" ? ( +{title}
+ ) : ( + title + )} {helpPageId && ( )} diff --git a/apps/client/src/widgets/react/NoteAutocomplete.tsx b/apps/client/src/widgets/react/NoteAutocomplete.tsx index 23f2ccd25..f8e907e88 100644 --- a/apps/client/src/widgets/react/NoteAutocomplete.tsx +++ b/apps/client/src/widgets/react/NoteAutocomplete.tsx @@ -1,33 +1,46 @@ import { useRef } from "preact/hooks"; import { t } from "../../services/i18n"; import { useEffect } from "react"; -import note_autocomplete, { type Suggestion } from "../../services/note_autocomplete"; +import note_autocomplete, { Options, type Suggestion } from "../../services/note_autocomplete"; import type { RefObject } from "preact"; interface NoteAutocompleteProps { inputRef?: RefObject; text?: string; - allowExternalLinks?: boolean; - allowCreatingNotes?: boolean; + placeholder?: string; + container?: RefObject ; + opts?: Omit ; onChange?: (suggestion: Suggestion) => void; + onTextChange?: (text: string) => void; } -export default function NoteAutocomplete({ inputRef: _ref, text, allowCreatingNotes, allowExternalLinks, onChange }: NoteAutocompleteProps) { +export default function NoteAutocomplete({ inputRef: _ref, text, placeholder, onChange, onTextChange, container, opts }: NoteAutocompleteProps) { const ref = _ref ?? useRef (null); useEffect(() => { if (!ref.current) return; const $autoComplete = $(ref.current); + // clear any event listener added in previous invocation of this function + $autoComplete + .off("autocomplete:noteselected") + .off("autocomplete:commandselected") + note_autocomplete.initNoteAutocomplete($autoComplete, { - allowExternalLinks, - allowCreatingNotes + ...opts, + container: container?.current }); if (onChange) { - $autoComplete.on("autocomplete:noteselected", (_e, suggestion) => onChange(suggestion)); - $autoComplete.on("autocomplete:externallinkselected", (_e, suggestion) => onChange(suggestion)); - } - }, [allowExternalLinks, allowCreatingNotes]); + const listener = (_e, suggestion) => onChange(suggestion); + $autoComplete + .on("autocomplete:noteselected", listener) + .on("autocomplete:externallinkselected", listener) + .on("autocomplete:commandselected", listener); + } + if (onTextChange) { + $autoComplete.on("input", () => onTextChange($autoComplete[0].value)); + } + }, [opts, container?.current]); useEffect(() => { if (!ref.current) return; @@ -44,7 +57,7 @@ export default function NoteAutocomplete({ inputRef: _ref, text, allowCreatingNo + placeholder={placeholder ?? t("add_link.search_note")} />