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
- {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")} />
);
}
\ No newline at end of file