mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	feat(dialogs): port jump to note partially
This commit is contained in:
		| @@ -38,7 +38,7 @@ export interface Suggestion { | |||||||
|     commandShortcut?: string; |     commandShortcut?: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| interface Options { | export interface Options { | ||||||
|     container?: HTMLElement; |     container?: HTMLElement; | ||||||
|     fastSearch?: boolean; |     fastSearch?: boolean; | ||||||
|     allowCreatingNotes?: boolean; |     allowCreatingNotes?: boolean; | ||||||
|   | |||||||
| @@ -211,7 +211,7 @@ | |||||||
|   }, |   }, | ||||||
|   "jump_to_note": { |   "jump_to_note": { | ||||||
|     "close": "关闭", |     "close": "关闭", | ||||||
|     "search_button": "全文搜索 <kbd>Ctrl+回车</kbd>" |     "search_button": "全文搜索" | ||||||
|   }, |   }, | ||||||
|   "markdown_import": { |   "markdown_import": { | ||||||
|     "dialog_title": "Markdown 导入", |     "dialog_title": "Markdown 导入", | ||||||
|   | |||||||
| @@ -211,7 +211,7 @@ | |||||||
|   }, |   }, | ||||||
|   "jump_to_note": { |   "jump_to_note": { | ||||||
|     "close": "Schließen", |     "close": "Schließen", | ||||||
|     "search_button": "Suche im Volltext: <kbd>Strg+Eingabetaste</kbd>" |     "search_button": "Suche im Volltext" | ||||||
|   }, |   }, | ||||||
|   "markdown_import": { |   "markdown_import": { | ||||||
|     "dialog_title": "Markdown-Import", |     "dialog_title": "Markdown-Import", | ||||||
|   | |||||||
| @@ -213,7 +213,7 @@ | |||||||
|   "jump_to_note": { |   "jump_to_note": { | ||||||
|     "search_placeholder": "Search for note by its name or type > for commands...", |     "search_placeholder": "Search for note by its name or type > for commands...", | ||||||
|     "close": "Close", |     "close": "Close", | ||||||
|     "search_button": "Search in full text <kbd>Ctrl+Enter</kbd>" |     "search_button": "Search in full text" | ||||||
|   }, |   }, | ||||||
|   "markdown_import": { |   "markdown_import": { | ||||||
|     "dialog_title": "Markdown import", |     "dialog_title": "Markdown import", | ||||||
|   | |||||||
| @@ -212,7 +212,7 @@ | |||||||
|   }, |   }, | ||||||
|   "jump_to_note": { |   "jump_to_note": { | ||||||
|     "close": "Cerrar", |     "close": "Cerrar", | ||||||
|     "search_button": "Buscar en texto completo <kbd>Ctrl+Enter</kbd>" |     "search_button": "Buscar en texto completo" | ||||||
|   }, |   }, | ||||||
|   "markdown_import": { |   "markdown_import": { | ||||||
|     "dialog_title": "Importación de Markdown", |     "dialog_title": "Importación de Markdown", | ||||||
|   | |||||||
| @@ -211,7 +211,7 @@ | |||||||
|   }, |   }, | ||||||
|   "jump_to_note": { |   "jump_to_note": { | ||||||
|     "close": "Fermer", |     "close": "Fermer", | ||||||
|     "search_button": "Rechercher dans le texte intégral <kbd>Ctrl+Entrée</kbd>" |     "search_button": "Rechercher dans le texte intégral" | ||||||
|   }, |   }, | ||||||
|   "markdown_import": { |   "markdown_import": { | ||||||
|     "dialog_title": "Importation Markdown", |     "dialog_title": "Importation Markdown", | ||||||
|   | |||||||
| @@ -767,7 +767,7 @@ | |||||||
|     "title": "Atribute moștenite" |     "title": "Atribute moștenite" | ||||||
|   }, |   }, | ||||||
|   "jump_to_note": { |   "jump_to_note": { | ||||||
|     "search_button": "Caută în întregul conținut <kbd>Ctrl+Enter</kbd>", |     "search_button": "Caută în întregul conținut", | ||||||
|     "close": "Închide", |     "close": "Închide", | ||||||
|     "search_placeholder": "Căutați notițe după nume sau tastați > pentru comenzi..." |     "search_placeholder": "Căutați notițe după nume sau tastați > pentru comenzi..." | ||||||
|   }, |   }, | ||||||
|   | |||||||
| @@ -194,7 +194,7 @@ | |||||||
|     "okButton": "確定" |     "okButton": "確定" | ||||||
|   }, |   }, | ||||||
|   "jump_to_note": { |   "jump_to_note": { | ||||||
|     "search_button": "全文搜尋 <kbd>Ctrl+Enter</kbd>" |     "search_button": "全文搜尋" | ||||||
|   }, |   }, | ||||||
|   "markdown_import": { |   "markdown_import": { | ||||||
|     "dialog_title": "Markdown 匯入", |     "dialog_title": "Markdown 匯入", | ||||||
|   | |||||||
| @@ -106,9 +106,11 @@ function AddLinkDialogComponent({ text: _text, textTypeWidget }: AddLinkDialogPr | |||||||
|                 <NoteAutocomplete |                 <NoteAutocomplete | ||||||
|                     inputRef={autocompleteRef} |                     inputRef={autocompleteRef} | ||||||
|                     text={text} |                     text={text} | ||||||
|                     allowExternalLinks |  | ||||||
|                     allowCreatingNotes |  | ||||||
|                     onChange={setSuggestion} |                     onChange={setSuggestion} | ||||||
|  |                     opts={{ | ||||||
|  |                         allowExternalLinks: true, | ||||||
|  |                         allowCreatingNotes: true | ||||||
|  |                     }} | ||||||
|                 /> |                 /> | ||||||
|             </div> |             </div> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,205 +0,0 @@ | |||||||
| import { t } from "../../services/i18n.js"; |  | ||||||
| import noteAutocompleteService from "../../services/note_autocomplete.js"; |  | ||||||
| import utils from "../../services/utils.js"; |  | ||||||
| import appContext from "../../components/app_context.js"; |  | ||||||
| import BasicWidget from "../basic_widget.js"; |  | ||||||
| import shortcutService from "../../services/shortcuts.js"; |  | ||||||
| import { Modal } from "bootstrap"; |  | ||||||
| import { openDialog } from "../../services/dialog.js"; |  | ||||||
| import commandRegistry from "../../services/command_registry.js"; |  | ||||||
|  |  | ||||||
| const TPL = /*html*/`<div class="jump-to-note-dialog modal mx-auto" tabindex="-1" role="dialog"> |  | ||||||
|     <div class="modal-dialog modal-lg" role="document"> |  | ||||||
|         <div class="modal-content"> |  | ||||||
|             <div class="modal-header"> |  | ||||||
|                 <div class="input-group"> |  | ||||||
|                     <input class="jump-to-note-autocomplete form-control" placeholder="${t("jump_to_note.search_placeholder")}"> |  | ||||||
|                 </div> |  | ||||||
|                 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="${t("jump_to_note.close")}"></button> |  | ||||||
|             </div> |  | ||||||
|             <div class="modal-body"> |  | ||||||
|                 <div class="algolia-autocomplete-container jump-to-note-results"></div> |  | ||||||
|             </div> |  | ||||||
|             <div class="modal-footer"> |  | ||||||
|                 <button class="show-in-full-text-button btn btn-sm">${t("jump_to_note.search_button")}</button> |  | ||||||
|             </div> |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
| </div>`; |  | ||||||
|  |  | ||||||
| const KEEP_LAST_SEARCH_FOR_X_SECONDS = 120; |  | ||||||
|  |  | ||||||
| export default class JumpToNoteDialog extends BasicWidget { |  | ||||||
|  |  | ||||||
|     private lastOpenedTs: number; |  | ||||||
|     private modal!: bootstrap.Modal; |  | ||||||
|     private $autoComplete!: JQuery<HTMLElement>; |  | ||||||
|     private $results!: JQuery<HTMLElement>; |  | ||||||
|     private $modalFooter!: JQuery<HTMLElement>; |  | ||||||
|     private isCommandMode: boolean = false; |  | ||||||
|  |  | ||||||
|     constructor() { |  | ||||||
|         super(); |  | ||||||
|  |  | ||||||
|         this.lastOpenedTs = 0; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     doRender() { |  | ||||||
|         this.$widget = $(TPL); |  | ||||||
|         this.modal = Modal.getOrCreateInstance(this.$widget[0]); |  | ||||||
|  |  | ||||||
|         this.$autoComplete = this.$widget.find(".jump-to-note-autocomplete"); |  | ||||||
|         this.$results = this.$widget.find(".jump-to-note-results"); |  | ||||||
|         this.$modalFooter = this.$widget.find(".modal-footer"); |  | ||||||
|         this.$modalFooter.find(".show-in-full-text-button").on("click", (e) => this.showInFullText(e)); |  | ||||||
|  |  | ||||||
|         shortcutService.bindElShortcut(this.$widget, "ctrl+return", (e) => this.showInFullText(e)); |  | ||||||
|  |  | ||||||
|         // Monitor input changes to detect command mode switches |  | ||||||
|         this.$autoComplete.on("input", () => { |  | ||||||
|             this.updateCommandModeState(); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private updateCommandModeState() { |  | ||||||
|         const currentValue = String(this.$autoComplete.val() || ""); |  | ||||||
|         const newCommandMode = currentValue.startsWith(">"); |  | ||||||
|  |  | ||||||
|         if (newCommandMode !== this.isCommandMode) { |  | ||||||
|             this.isCommandMode = newCommandMode; |  | ||||||
|             this.updateButtonVisibility(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private updateButtonVisibility() { |  | ||||||
|         if (this.isCommandMode) { |  | ||||||
|             this.$modalFooter.hide(); |  | ||||||
|         } else { |  | ||||||
|             this.$modalFooter.show(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async jumpToNoteEvent() { |  | ||||||
|         await this.openDialog(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async commandPaletteEvent() { |  | ||||||
|         await this.openDialog(true); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private async openDialog(commandMode = false) { |  | ||||||
|         const dialogPromise = openDialog(this.$widget); |  | ||||||
|         if (utils.isMobile()) { |  | ||||||
|             dialogPromise.then(($dialog) => { |  | ||||||
|                 const el = $dialog.find(">.modal-dialog")[0]; |  | ||||||
|  |  | ||||||
|                 function reposition() { |  | ||||||
|                     const offset = 100; |  | ||||||
|                     const modalHeight = (window.visualViewport?.height ?? 0) - offset; |  | ||||||
|                     const safeAreaInsetBottom = (window.visualViewport?.height ?? 0) - window.innerHeight; |  | ||||||
|                     el.style.height = `${modalHeight}px`; |  | ||||||
|                     el.style.bottom = `${(window.visualViewport?.height ?? 0) - modalHeight - safeAreaInsetBottom - offset}px`; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 this.$autoComplete.on("focus", () => { |  | ||||||
|                     reposition(); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|                 window.visualViewport?.addEventListener("resize", () => { |  | ||||||
|                     reposition(); |  | ||||||
|                 }); |  | ||||||
|  |  | ||||||
|                 reposition(); |  | ||||||
|             }); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         // first open dialog, then refresh since refresh is doing focus which should be visible |  | ||||||
|         this.refresh(commandMode); |  | ||||||
|  |  | ||||||
|         this.lastOpenedTs = Date.now(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async refresh(commandMode = false) { |  | ||||||
|         noteAutocompleteService |  | ||||||
|             .initNoteAutocomplete(this.$autoComplete, { |  | ||||||
|                 allowCreatingNotes: true, |  | ||||||
|                 hideGoToSelectedNoteButton: true, |  | ||||||
|                 allowJumpToSearchNotes: true, |  | ||||||
|                 container: this.$results[0], |  | ||||||
|                 isCommandPalette: true |  | ||||||
|             }) |  | ||||||
|             // clear any event listener added in previous invocation of this function |  | ||||||
|             .off("autocomplete:noteselected") |  | ||||||
|             .off("autocomplete:commandselected") |  | ||||||
|             .on("autocomplete:noteselected", function (event, suggestion, dataset) { |  | ||||||
|                 if (!suggestion.notePath) { |  | ||||||
|                     return false; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 appContext.tabManager.getActiveContext()?.setNote(suggestion.notePath); |  | ||||||
|             }) |  | ||||||
|             .on("autocomplete:commandselected", async (event, suggestion, dataset) => { |  | ||||||
|                 if (!suggestion.commandId) { |  | ||||||
|                     return false; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 this.modal.hide(); |  | ||||||
|                 await commandRegistry.executeCommand(suggestion.commandId); |  | ||||||
|             }); |  | ||||||
|  |  | ||||||
|         if (commandMode) { |  | ||||||
|             // Start in command mode - manually trigger command search |  | ||||||
|             this.$autoComplete.autocomplete("val", ">"); |  | ||||||
|             this.isCommandMode = true; |  | ||||||
|             this.updateButtonVisibility(); |  | ||||||
|  |  | ||||||
|             // Manually populate with all commands immediately |  | ||||||
|             noteAutocompleteService.showAllCommands(this.$autoComplete); |  | ||||||
|  |  | ||||||
|             this.$autoComplete.trigger("focus"); |  | ||||||
|         } else { |  | ||||||
|             // 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. |  | ||||||
|             if (Date.now() - this.lastOpenedTs > KEEP_LAST_SEARCH_FOR_X_SECONDS * 1000) { |  | ||||||
|                 this.isCommandMode = false; |  | ||||||
|                 this.updateButtonVisibility(); |  | ||||||
|                 noteAutocompleteService.showRecentNotes(this.$autoComplete); |  | ||||||
|             } else { |  | ||||||
|                 this.$autoComplete |  | ||||||
|                     // hack, the actual search value is stored in <pre> 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(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										130
									
								
								apps/client/src/widgets/dialogs/jump_to_note.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								apps/client/src/widgets/dialogs/jump_to_note.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -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<HTMLDivElement>(null); | ||||||
|  |     const autocompleteRef = useRef<HTMLInputElement>(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 ( | ||||||
|  |         <Modal | ||||||
|  |             className="jump-to-note-dialog" | ||||||
|  |             size="lg" | ||||||
|  |             title={<NoteAutocomplete | ||||||
|  |                 placeholder={t("jump_to_note.search_placeholder")} | ||||||
|  |                 inputRef={autocompleteRef} | ||||||
|  |                 container={containerRef} | ||||||
|  |                 text={text} | ||||||
|  |                 opts={{ | ||||||
|  |                     allowCreatingNotes: true, | ||||||
|  |                     hideGoToSelectedNoteButton: true, | ||||||
|  |                     allowJumpToSearchNotes: true, | ||||||
|  |                     isCommandPalette: true | ||||||
|  |                 }} | ||||||
|  |                 onTextChange={setText} | ||||||
|  |                 onChange={onItemSelected} | ||||||
|  |                 />} | ||||||
|  |             onShown={onShown} | ||||||
|  |             footer={!isCommandMode && <Button className="show-in-full-text-button" text={t("jump_to_note.search_button")} keyboardShortcut="Ctrl+Enter" />} | ||||||
|  |         > | ||||||
|  |             <div className="algolia-autocomplete-container jump-to-note-results" ref={containerRef}></div> | ||||||
|  |         </Modal> | ||||||
|  |     ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default class JumpToNoteDialog extends ReactBasicWidget { | ||||||
|  |  | ||||||
|  |     private lastOpenedTs: number; | ||||||
|  |     private props: JumpToNoteDialogProps = { | ||||||
|  |         mode: "last-search" | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     get component() { | ||||||
|  |         return <JumpToNoteDialogComponent {...this.props} />; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     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); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -5,7 +5,7 @@ import type { CSSProperties } from "preact/compat"; | |||||||
|  |  | ||||||
| interface ModalProps { | interface ModalProps { | ||||||
|     className: string; |     className: string; | ||||||
|     title: string; |     title: string | ComponentChildren; | ||||||
|     size: "lg" | "sm"; |     size: "lg" | "sm"; | ||||||
|     children: ComponentChildren; |     children: ComponentChildren; | ||||||
|     footer?: ComponentChildren; |     footer?: ComponentChildren; | ||||||
| @@ -49,7 +49,11 @@ export default function Modal({ children, className, size, title, footer, onShow | |||||||
|             <div className={`modal-dialog modal-${size}`} style={style} role="document"> |             <div className={`modal-dialog modal-${size}`} style={style} role="document"> | ||||||
|                 <div className="modal-content"> |                 <div className="modal-content"> | ||||||
|                     <div className="modal-header"> |                     <div className="modal-header"> | ||||||
|  |                         {typeof title === "string" ? ( | ||||||
|                             <h5 className="modal-title">{title}</h5> |                             <h5 className="modal-title">{title}</h5> | ||||||
|  |                         ) : ( | ||||||
|  |                             title | ||||||
|  |                         )} | ||||||
|                         {helpPageId && ( |                         {helpPageId && ( | ||||||
|                             <button className="help-button" type="button" data-in-app-help={helpPageId} title={t("modal.help_title")}>?</button> |                             <button className="help-button" type="button" data-in-app-help={helpPageId} title={t("modal.help_title")}>?</button> | ||||||
|                         )} |                         )} | ||||||
|   | |||||||
| @@ -1,33 +1,46 @@ | |||||||
| import { useRef } from "preact/hooks"; | import { useRef } from "preact/hooks"; | ||||||
| import { t } from "../../services/i18n"; | import { t } from "../../services/i18n"; | ||||||
| import { useEffect } from "react"; | 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"; | import type { RefObject } from "preact"; | ||||||
|  |  | ||||||
| interface NoteAutocompleteProps {     | interface NoteAutocompleteProps {     | ||||||
|     inputRef?: RefObject<HTMLInputElement>; |     inputRef?: RefObject<HTMLInputElement>; | ||||||
|     text?: string; |     text?: string; | ||||||
|     allowExternalLinks?: boolean; |     placeholder?: string; | ||||||
|     allowCreatingNotes?: boolean; |     container?: RefObject<HTMLDivElement>; | ||||||
|  |     opts?: Omit<Options, "container">; | ||||||
|     onChange?: (suggestion: Suggestion) => void; |     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<HTMLInputElement>(null); |     const ref = _ref ?? useRef<HTMLInputElement>(null); | ||||||
|      |      | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|         if (!ref.current) return; |         if (!ref.current) return; | ||||||
|         const $autoComplete = $(ref.current); |         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, { |         note_autocomplete.initNoteAutocomplete($autoComplete, { | ||||||
|             allowExternalLinks, |             ...opts, | ||||||
|             allowCreatingNotes |             container: container?.current | ||||||
|         }); |         }); | ||||||
|         if (onChange) { |         if (onChange) { | ||||||
|             $autoComplete.on("autocomplete:noteselected", (_e, suggestion) => onChange(suggestion)); |             const listener = (_e, suggestion) => onChange(suggestion); | ||||||
|             $autoComplete.on("autocomplete:externallinkselected", (_e, suggestion) => onChange(suggestion)); |             $autoComplete | ||||||
|  |                 .on("autocomplete:noteselected", listener) | ||||||
|  |                 .on("autocomplete:externallinkselected", listener) | ||||||
|  |                 .on("autocomplete:commandselected", listener); | ||||||
|         } |         } | ||||||
|     }, [allowExternalLinks, allowCreatingNotes]); |         if (onTextChange) { | ||||||
|  |             $autoComplete.on("input", () => onTextChange($autoComplete[0].value)); | ||||||
|  |         } | ||||||
|  |     }, [opts, container?.current]); | ||||||
|  |  | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|         if (!ref.current) return; |         if (!ref.current) return; | ||||||
| @@ -44,7 +57,7 @@ export default function NoteAutocomplete({ inputRef: _ref, text, allowCreatingNo | |||||||
|             <input |             <input | ||||||
|                 ref={ref} |                 ref={ref} | ||||||
|                 className="note-autocomplete form-control" |                 className="note-autocomplete form-control" | ||||||
|                 placeholder={t("add_link.search_note")} /> |                 placeholder={placeholder ?? t("add_link.search_note")} /> | ||||||
|         </div> |         </div> | ||||||
|     ); |     ); | ||||||
| } | } | ||||||
		Reference in New Issue
	
	Block a user