2018-08-06 22:29:03 +02:00
import server from "./server.js" ;
2022-12-01 13:07:23 +01:00
import appContext from "../components/app_context.js" ;
2025-01-09 18:07:02 +02:00
import utils from "./utils.js" ;
import noteCreateService from "./note_create.js" ;
2021-04-16 23:01:56 +02:00
import froca from "./froca.js" ;
2024-12-14 01:48:56 +02:00
import { t } from "./i18n.js" ;
2018-08-16 21:02:42 +02:00
2022-08-09 21:49:37 +02:00
// this key needs to have this value, so it's hit by the tooltip
2020-05-16 22:11:09 +02:00
const SELECTED_NOTE_PATH_KEY = "data-note-path" ;
2018-11-14 00:05:09 +01:00
2021-01-08 21:44:43 +01:00
const SELECTED_EXTERNAL_LINK_KEY = "data-external-link" ;
2024-12-21 14:34:16 +02:00
export interface Suggestion {
noteTitle? : string ;
externalLink? : string ;
notePathTitle? : string ;
notePath? : string ;
highlightedNotePathTitle? : string ;
action? : string | "create-note" | "search-notes" | "external-link" ;
parentNoteId? : string ;
}
interface Options {
container? : HTMLElement ;
fastSearch? : boolean ;
allowCreatingNotes? : boolean ;
allowJumpToSearchNotes? : boolean ;
allowExternalLinks? : boolean ;
hideGoToSelectedNoteButton? : boolean ;
}
async function autocompleteSourceForCKEditor ( queryText : string ) {
2024-12-22 21:59:08 +02:00
return await new Promise < MentionItem [ ] > ( ( res , rej ) = > {
2025-01-09 18:07:02 +02:00
autocompleteSource (
queryText ,
( rows ) = > {
res (
rows . map ( ( row ) = > {
return {
action : row.action ,
noteTitle : row.noteTitle ,
id : ` @ ${ row . notePathTitle } ` ,
name : row.notePathTitle || "" ,
link : ` # ${ row . notePath } ` ,
notePath : row.notePath ,
highlightedNotePathTitle : row.highlightedNotePathTitle
} ;
} )
) ;
} ,
{
allowCreatingNotes : true
}
) ;
2020-09-21 22:08:54 +02:00
} ) ;
}
2024-12-21 14:34:16 +02:00
async function autocompleteSource ( term : string , cb : ( rows : Suggestion [ ] ) = > void , options : Options = { } ) {
2024-11-27 09:51:34 +08:00
const fastSearch = options . fastSearch === false ? false : true ;
2024-11-26 16:20:38 +08:00
if ( fastSearch === false ) {
2025-01-09 18:07:02 +02:00
if ( term . trim ( ) . length === 0 ) {
2024-11-27 09:51:34 +08:00
return ;
}
2025-01-09 18:07:02 +02:00
cb ( [
{
2024-11-26 16:20:38 +08:00
noteTitle : term ,
2024-12-14 01:48:56 +02:00
highlightedNotePathTitle : t ( "quick-search.searching" )
2025-01-09 18:07:02 +02:00
}
] ) ;
2024-11-26 16:20:38 +08:00
}
2024-12-22 21:59:08 +02:00
2021-05-22 12:35:41 +02:00
const activeNoteId = appContext . tabManager . getActiveContextNoteId ( ) ;
2020-09-21 00:07:46 +02:00
2024-12-21 14:34:16 +02:00
let results : Suggestion [ ] = await server . get < Suggestion [ ] > ( ` autocomplete?query= ${ encodeURIComponent ( term ) } &activeNoteId= ${ activeNoteId } &fastSearch= ${ fastSearch } ` ) ;
2021-01-18 22:52:07 +01:00
if ( term . trim ( ) . length >= 1 && options . allowCreatingNotes ) {
2020-09-21 00:07:46 +02:00
results = [
{
2025-01-09 18:07:02 +02:00
action : "create-note" ,
2020-09-21 00:07:46 +02:00
noteTitle : term ,
2025-01-09 18:07:02 +02:00
parentNoteId : activeNoteId || "root" ,
2024-12-14 01:48:56 +02:00
highlightedNotePathTitle : t ( "note_autocomplete.create-note" , { term } )
2024-12-21 14:34:16 +02:00
} as Suggestion
2020-09-21 00:07:46 +02:00
] . concat ( results ) ;
}
2018-08-16 21:02:42 +02:00
2024-11-24 13:11:57 +08:00
if ( term . trim ( ) . length >= 1 && options . allowJumpToSearchNotes ) {
2024-11-20 14:22:39 +08:00
results = results . concat ( [
{
2025-01-09 18:07:02 +02:00
action : "search-notes" ,
2024-11-20 14:22:39 +08:00
noteTitle : term ,
2024-12-14 01:48:56 +02:00
highlightedNotePathTitle : ` ${ t ( "note_autocomplete.search-for" , { term } )} <kbd style='color: var(--muted-text-color); background-color: transparent; float: right;'>Ctrl+Enter</kbd> `
2024-11-20 14:22:39 +08:00
}
] ) ;
}
2021-01-08 21:44:43 +01:00
if ( term . match ( /^[a-z]+:\/\/.+/i ) && options . allowExternalLinks ) {
results = [
{
2025-01-09 18:07:02 +02:00
action : "external-link" ,
2021-01-08 21:44:43 +01:00
externalLink : term ,
2024-12-14 01:48:56 +02:00
highlightedNotePathTitle : t ( "note_autocomplete.insert-external-link" , { term } )
2024-12-21 14:34:16 +02:00
} as Suggestion
2021-01-08 21:44:43 +01:00
] . concat ( results ) ;
}
2020-09-21 00:07:46 +02:00
cb ( results ) ;
2018-08-16 21:02:42 +02:00
}
2018-08-06 22:29:03 +02:00
2024-12-21 14:34:16 +02:00
function clearText ( $el : JQuery < HTMLElement > ) {
2020-05-16 22:11:09 +02:00
$el . setSelectedNotePath ( "" ) ;
2025-01-09 18:07:02 +02:00
$el . autocomplete ( "val" , "" ) . trigger ( "change" ) ;
2018-11-13 23:16:26 +01:00
}
2024-12-21 14:34:16 +02:00
function setText ( $el : JQuery < HTMLElement > , text : string ) {
2021-01-05 14:22:11 +01:00
$el . setSelectedNotePath ( "" ) ;
2025-01-09 18:07:02 +02:00
$el . autocomplete ( "val" , text . trim ( ) ) . autocomplete ( "open" ) ;
2021-01-05 14:22:11 +01:00
}
2025-01-09 18:07:02 +02:00
function showRecentNotes ( $el : JQuery < HTMLElement > ) {
2020-05-16 22:11:09 +02:00
$el . setSelectedNotePath ( "" ) ;
2018-11-07 09:51:14 +01:00
$el . autocomplete ( "val" , "" ) ;
2025-01-09 18:07:02 +02:00
$el . autocomplete ( "open" ) ;
$el . trigger ( "focus" ) ;
2018-11-07 09:51:14 +01:00
}
2025-01-09 18:07:02 +02:00
function fullTextSearch ( $el : JQuery < HTMLElement > , options : Options ) {
const searchString = $el . autocomplete ( "val" ) as unknown as string ;
2024-12-21 14:34:16 +02:00
if ( options . fastSearch === false || searchString ? . trim ( ) . length === 0 ) {
2024-11-27 09:51:34 +08:00
return ;
2024-12-22 21:59:08 +02:00
}
2025-01-09 18:07:02 +02:00
$el . trigger ( "focus" ) ;
2024-11-27 09:51:34 +08:00
options . fastSearch = false ;
2025-01-09 18:07:02 +02:00
$el . autocomplete ( "val" , "" ) ;
2024-11-27 09:51:34 +08:00
$el . setSelectedNotePath ( "" ) ;
2025-01-09 18:07:02 +02:00
$el . autocomplete ( "val" , searchString ) ;
2024-11-27 10:14:13 +08:00
// Set a delay to avoid resetting to true before full text search (await server.get) is called.
2025-01-09 18:07:02 +02:00
setTimeout ( ( ) = > {
options . fastSearch = true ;
} , 100 ) ;
2024-11-23 20:51:51 +08:00
}
2024-12-21 14:39:36 +02:00
function initNoteAutocomplete ( $el : JQuery < HTMLElement > , options? : Options ) {
2025-01-04 23:43:15 +02:00
if ( $el . hasClass ( "note-autocomplete-input" ) ) {
2020-10-02 21:44:21 +02:00
// clear any event listener added in previous invocation of this function
2025-01-09 18:07:02 +02:00
$el . off ( "autocomplete:noteselected" ) ;
2020-10-02 21:44:21 +02:00
2019-05-22 20:53:59 +02:00
return $el ;
}
2018-11-13 23:16:26 +01:00
2019-05-22 20:53:59 +02:00
options = options || { } ;
2018-11-13 23:16:26 +01:00
2019-05-22 20:53:59 +02:00
$el . addClass ( "note-autocomplete-input" ) ;
2018-11-14 11:17:20 +01:00
2025-02-13 22:09:08 +02:00
const $clearTextButton = $ ( "<a>" ) . addClass ( "input-group-text input-clearer-button bx bxs-tag-x" ) . prop ( "title" , t ( "note_autocomplete.clear-text-field" ) ) ;
2018-11-14 11:28:52 +01:00
2025-02-13 22:09:08 +02:00
const $showRecentNotesButton = $ ( "<a>" ) . addClass ( "input-group-text show-recent-notes-button bx bx-time" ) . prop ( "title" , t ( "note_autocomplete.show-recent-notes" ) ) ;
2018-11-14 11:28:52 +01:00
2025-02-13 22:09:08 +02:00
const $fullTextSearchButton = $ ( "<a>" )
2024-11-23 20:51:51 +08:00
. addClass ( "input-group-text full-text-search-button bx bx-search" )
2024-12-22 21:59:08 +02:00
. prop ( "title" , ` ${ t ( "note_autocomplete.full-text-search" ) } (Shift+Enter) ` ) ;
2024-11-23 20:51:51 +08:00
2025-01-09 18:07:02 +02:00
const $goToSelectedNoteButton = $ ( "<a>" ) . addClass ( "input-group-text go-to-selected-note-button bx bx-arrow-to-right" ) ;
2018-08-06 22:29:03 +02:00
2024-11-23 20:51:51 +08:00
$el . after ( $clearTextButton ) . after ( $showRecentNotesButton ) . after ( $fullTextSearchButton ) ;
2018-08-06 22:29:03 +02:00
2019-05-22 20:53:59 +02:00
if ( ! options . hideGoToSelectedNoteButton ) {
2024-09-03 17:08:07 +02:00
$el . after ( $goToSelectedNoteButton ) ;
2019-05-22 20:53:59 +02:00
}
2019-01-10 21:04:06 +01:00
2025-01-09 18:07:02 +02:00
$clearTextButton . on ( "click" , ( ) = > clearText ( $el ) ) ;
2019-05-22 20:53:59 +02:00
2025-01-09 18:07:02 +02:00
$showRecentNotesButton . on ( "click" , ( e ) = > {
2019-05-22 20:53:59 +02:00
showRecentNotes ( $el ) ;
// this will cause the click not give focus to the "show recent notes" button
2023-06-23 00:26:47 +08:00
// this is important because otherwise input will lose focus immediately and not show the results
2019-05-22 20:53:59 +02:00
return false ;
} ) ;
2024-11-17 12:18:05 +08:00
2025-01-09 18:07:02 +02:00
$fullTextSearchButton . on ( "click" , ( e ) = > {
2024-11-23 20:51:51 +08:00
fullTextSearch ( $el , options ) ;
2024-11-26 15:41:18 +08:00
return false ;
2024-11-23 20:51:51 +08:00
} ) ;
2024-12-21 14:34:16 +02:00
let autocompleteOptions : AutoCompleteConfig = { } ;
2023-08-26 17:03:09 +03:00
if ( options . container ) {
autocompleteOptions . dropdownMenuContainer = options . container ;
2025-01-09 18:07:02 +02:00
autocompleteOptions . debug = true ; // don't close on blur
2023-08-26 17:03:09 +03:00
}
2024-11-24 13:11:57 +08:00
if ( options . allowJumpToSearchNotes ) {
2025-01-09 18:07:02 +02:00
$el . on ( "keydown" , ( event ) = > {
if ( event . ctrlKey && event . key === "Enter" ) {
2024-11-20 14:22:39 +08:00
// Prevent Ctrl + Enter from triggering autoComplete.
event . stopImmediatePropagation ( ) ;
event . preventDefault ( ) ;
2025-01-09 18:07:02 +02:00
$el . trigger ( "autocomplete:selected" , { action : "search-notes" , noteTitle : $el.autocomplete ( "val" ) } ) ;
2024-11-20 14:22:39 +08:00
}
} ) ;
}
2025-01-09 18:07:02 +02:00
$el . on ( "keydown" , async ( event ) = > {
if ( event . shiftKey && event . key === "Enter" ) {
2024-11-23 20:51:51 +08:00
// Prevent Enter from triggering autoComplete.
event . stopImmediatePropagation ( ) ;
event . preventDefault ( ) ;
2025-01-09 18:07:02 +02:00
fullTextSearch ( $el , options ) ;
2024-11-23 20:51:51 +08:00
}
} ) ;
2024-12-22 21:59:08 +02:00
2025-01-09 18:07:02 +02:00
$el . autocomplete (
2019-05-22 20:53:59 +02:00
{
2025-01-09 18:07:02 +02:00
. . . autocompleteOptions ,
appendTo : document.querySelector ( "body" ) ,
hint : false ,
autoselect : true ,
// openOnFocus has to be false, otherwise re-focus (after return from note type chooser dialog) forces
// re-querying of the autocomplete source which then changes the currently selected suggestion
openOnFocus : false ,
minLength : 0 ,
tabAutocomplete : false
} ,
[
{
source : ( term , cb ) = > autocompleteSource ( term , cb , options ) ,
displayKey : "notePathTitle" ,
templates : {
suggestion : ( suggestion ) = > suggestion . highlightedNotePathTitle
} ,
// we can't cache identical searches because notes can be created / renamed, new recent notes can be added
cache : false
}
]
) ;
2018-11-07 09:51:14 +01:00
2024-12-21 14:34:16 +02:00
// TODO: Types fail due to "autocomplete:selected" not being registered in type definitions.
2025-01-09 18:07:02 +02:00
( $el as any ) . on ( "autocomplete:selected" , async ( event : Event , suggestion : Suggestion ) = > {
if ( suggestion . action === "external-link" ) {
2021-01-08 21:44:43 +01:00
$el . setSelectedNotePath ( null ) ;
$el . setSelectedExternalLink ( suggestion . externalLink ) ;
$el . autocomplete ( "val" , suggestion . externalLink ) ;
$el . autocomplete ( "close" ) ;
2025-01-09 18:07:02 +02:00
$el . trigger ( "autocomplete:externallinkselected" , [ suggestion ] ) ;
2021-01-08 21:44:43 +01:00
return ;
}
2025-01-09 18:07:02 +02:00
if ( suggestion . action === "create-note" ) {
2024-09-03 17:08:07 +02:00
const { success , noteType , templateNoteId } = await noteCreateService . chooseNoteType ( ) ;
2022-06-02 14:16:49 +02:00
if ( ! success ) {
return ;
}
2024-09-03 17:08:07 +02:00
const { note } = await noteCreateService . createNote ( suggestion . parentNoteId , {
2020-09-21 00:07:46 +02:00
title : suggestion.noteTitle ,
2022-06-02 14:16:49 +02:00
activate : false ,
type : noteType ,
templateNoteId : templateNoteId
2020-09-21 00:07:46 +02:00
} ) ;
2023-04-15 00:06:13 +02:00
const hoistedNoteId = appContext . tabManager . getActiveContext ( ) ? . hoistedNoteId ;
2024-12-21 14:34:16 +02:00
suggestion . notePath = note ? . getBestNotePathString ( hoistedNoteId ) ;
2020-09-21 00:07:46 +02:00
}
2025-01-09 18:07:02 +02:00
if ( suggestion . action === "search-notes" ) {
2024-11-20 14:22:39 +08:00
const searchString = suggestion . noteTitle ;
2025-01-09 18:07:02 +02:00
appContext . triggerCommand ( "searchNotes" , { searchString } ) ;
2024-11-20 14:22:39 +08:00
return ;
}
2024-12-22 21:59:08 +02:00
2020-09-03 17:38:11 +02:00
$el . setSelectedNotePath ( suggestion . notePath ) ;
2021-01-08 21:44:43 +01:00
$el . setSelectedExternalLink ( null ) ;
2020-09-03 17:38:11 +02:00
$el . autocomplete ( "val" , suggestion . noteTitle ) ;
2020-09-21 00:07:46 +02:00
$el . autocomplete ( "close" ) ;
2025-01-09 18:07:02 +02:00
$el . trigger ( "autocomplete:noteselected" , [ suggestion ] ) ;
2020-09-03 17:38:11 +02:00
} ) ;
2025-01-09 18:07:02 +02:00
$el . on ( "autocomplete:closed" , ( ) = > {
2024-12-21 14:34:16 +02:00
if ( ! String ( $el . val ( ) ) ? . trim ( ) ) {
2019-05-22 20:53:59 +02:00
clearText ( $el ) ;
}
} ) ;
2018-11-07 09:51:14 +01:00
2025-01-09 18:07:02 +02:00
$el . on ( "autocomplete:opened" , ( ) = > {
2020-08-11 22:52:17 +02:00
if ( $el . attr ( "readonly" ) ) {
2025-01-09 18:07:02 +02:00
$el . autocomplete ( "close" ) ;
2020-08-11 22:52:17 +02:00
}
} ) ;
2020-10-02 21:44:21 +02:00
// clear any event listener added in previous invocation of this function
2025-01-09 18:07:02 +02:00
$el . off ( "autocomplete:noteselected" ) ;
2020-10-02 21:44:21 +02:00
2018-11-07 09:51:14 +01:00
return $el ;
2018-08-06 22:29:03 +02:00
}
2018-12-24 10:10:36 +01:00
function init() {
2020-05-16 22:11:09 +02:00
$ . fn . getSelectedNotePath = function ( ) {
2024-12-21 14:34:16 +02:00
if ( ! String ( $ ( this ) . val ( ) ) ? . trim ( ) ) {
2018-12-24 10:10:36 +01:00
return "" ;
} else {
2020-05-16 22:11:09 +02:00
return $ ( this ) . attr ( SELECTED_NOTE_PATH_KEY ) ;
2018-12-24 10:10:36 +01:00
}
} ;
2018-11-12 23:34:22 +01:00
2020-08-12 23:39:05 +02:00
$ . fn . getSelectedNoteId = function ( ) {
2024-12-21 14:34:16 +02:00
const $el = $ ( this as unknown as HTMLElement ) ;
const notePath = $el . getSelectedNotePath ( ) ;
2023-01-13 10:09:41 +01:00
if ( ! notePath ) {
return null ;
}
2025-01-09 18:07:02 +02:00
const chunks = notePath . split ( "/" ) ;
2020-08-12 23:39:05 +02:00
return chunks . length >= 1 ? chunks [ chunks . length - 1 ] : null ;
2025-01-09 18:07:02 +02:00
} ;
2020-08-12 23:39:05 +02:00
2020-05-16 22:11:09 +02:00
$ . fn . setSelectedNotePath = function ( notePath ) {
notePath = notePath || "" ;
$ ( this ) . attr ( SELECTED_NOTE_PATH_KEY , notePath ) ;
2025-01-09 18:07:02 +02:00
$ ( this ) . closest ( ".input-group" ) . find ( ".go-to-selected-note-button" ) . toggleClass ( "disabled" , ! notePath . trim ( ) ) . attr ( "href" , ` # ${ notePath } ` ) ; // we also set href here so tooltip can be displayed
2018-12-24 10:10:36 +01:00
} ;
2021-01-08 21:44:43 +01:00
$ . fn . getSelectedExternalLink = function ( ) {
2024-12-21 14:34:16 +02:00
if ( ! String ( $ ( this ) . val ( ) ) ? . trim ( ) ) {
2021-01-08 21:44:43 +01:00
return "" ;
} else {
return $ ( this ) . attr ( SELECTED_EXTERNAL_LINK_KEY ) ;
}
} ;
2025-03-04 00:33:09 +01:00
$ . fn . setSelectedExternalLink = function ( externalLink : string | null ) {
$ ( this ) . attr ( SELECTED_EXTERNAL_LINK_KEY , externalLink ) ;
$ ( this ) . closest ( ".input-group" ) . find ( ".go-to-selected-note-button" ) . toggleClass ( "disabled" , true ) ;
2025-01-09 18:07:02 +02:00
} ;
2021-01-19 22:10:24 +01:00
$ . fn . setNote = async function ( noteId ) {
2021-04-16 22:57:37 +02:00
const note = noteId ? await froca . getNote ( noteId , true ) : null ;
2021-01-19 22:10:24 +01:00
$ ( this )
. val ( note ? note . title : "" )
. setSelectedNotePath ( noteId ) ;
2025-01-09 18:07:02 +02:00
} ;
2018-12-24 10:10:36 +01:00
}
2018-08-06 22:29:03 +02:00
export default {
2020-09-21 22:08:54 +02:00
autocompleteSourceForCKEditor ,
2018-08-16 21:02:42 +02:00
initNoteAutocomplete ,
2018-12-24 10:10:36 +01:00
showRecentNotes ,
2021-01-05 14:22:11 +01:00
setText ,
2018-12-24 10:10:36 +01:00
init
2025-01-09 18:07:02 +02:00
} ;