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" ;
2019-05-22 20:53:59 +02:00
import utils from './utils.js' ;
2020-09-21 00:07:46 +02:00
import noteCreateService from './note_create.js' ;
2021-04-16 23:01:56 +02:00
import froca from "./froca.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" ;
2020-09-21 22:08:54 +02:00
async function autocompleteSourceForCKEditor ( queryText ) {
return await new Promise ( ( res , rej ) => {
autocompleteSource ( queryText , rows => {
res ( rows . map ( row => {
return {
action : row . action ,
noteTitle : row . noteTitle ,
2022-12-21 15:19:05 +01:00
id : ` @ ${ row . notePathTitle } ` ,
2020-09-21 22:08:54 +02:00
name : row . notePathTitle ,
2022-12-21 15:19:05 +01:00
link : ` # ${ row . notePath } ` ,
2020-09-21 22:08:54 +02:00
notePath : row . notePath ,
highlightedNotePathTitle : row . highlightedNotePathTitle
}
} ) ) ;
2021-01-18 22:52:07 +01:00
} , {
allowCreatingNotes : true
2020-09-21 22:08:54 +02:00
} ) ;
} ) ;
}
2024-11-26 15:41:18 +08:00
async function autocompleteSource ( term , cb , options = { } , fastSearch = true ) {
2021-05-22 12:35:41 +02:00
const activeNoteId = appContext . tabManager . getActiveContextNoteId ( ) ;
2020-09-21 00:07:46 +02:00
2024-11-26 15:41:18 +08:00
let results = await server . get ( ` 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 = [
{
2020-09-21 22:08:54 +02:00
action : 'create-note' ,
2020-09-21 00:07:46 +02:00
noteTitle : term ,
2020-09-21 22:08:54 +02:00
parentNoteId : activeNoteId || 'root' ,
2021-10-31 23:12:24 +01:00
highlightedNotePathTitle : ` Create and link child note " ${ utils . escapeHtml ( term ) } " `
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 ( [
{
action : 'search-notes' ,
noteTitle : term ,
highlightedNotePathTitle : ` Search for " ${ utils . escapeHtml ( term ) } " <kbd style='color: var(--muted-text-color); background-color: transparent; float: right;'>Ctrl+Enter</kbd> `
}
] ) ;
}
2021-01-08 21:44:43 +01:00
if ( term . match ( /^[a-z]+:\/\/.+/i ) && options . allowExternalLinks ) {
results = [
{
action : 'external-link' ,
externalLink : term ,
2021-10-31 23:12:24 +01:00
highlightedNotePathTitle : ` Insert external link to " ${ utils . escapeHtml ( term ) } " `
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
2018-11-13 23:16:26 +01:00
function clearText ( $el ) {
2019-05-22 20:53:59 +02:00
if ( utils . isMobile ( ) ) {
return ;
}
2020-05-16 22:11:09 +02:00
$el . setSelectedNotePath ( "" ) ;
2019-11-09 17:39:48 +01:00
$el . autocomplete ( "val" , "" ) . trigger ( 'change' ) ;
2018-11-13 23:16:26 +01:00
}
2021-01-05 14:22:11 +01:00
function setText ( $el , text ) {
if ( utils . isMobile ( ) ) {
return ;
}
$el . setSelectedNotePath ( "" ) ;
$el
. autocomplete ( "val" , text . trim ( ) )
. autocomplete ( "open" ) ;
}
2018-11-07 09:51:14 +01:00
function showRecentNotes ( $el ) {
2019-05-22 20:53:59 +02:00
if ( utils . isMobile ( ) ) {
return ;
}
2020-05-16 22:11:09 +02:00
$el . setSelectedNotePath ( "" ) ;
2018-11-07 09:51:14 +01:00
$el . autocomplete ( "val" , "" ) ;
2024-11-24 13:10:33 +08:00
$el . autocomplete ( 'open' ) ;
2019-11-09 17:45:22 +01:00
$el . trigger ( 'focus' ) ;
2018-11-07 09:51:14 +01:00
}
2024-11-26 15:41:18 +08:00
async function fullTextSearch ( $el , options ) {
if ( ! options . container ) {
// If no container is specified, the dropdown might remain closed. Calling `$el.autocomplete('open')` triggers a search by name and needs to wait for completion. Otherwise, if `$el.autocomplete('open')` executes too slowly, it will overwrite the full-text search results.
$el . autocomplete ( 'open' ) ;
await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
}
2024-11-24 13:10:33 +08:00
const searchString = $el . autocomplete ( 'val' ) ;
if ( searchString . trim ( ) . length >= 1 ) {
2024-11-26 15:41:18 +08:00
$el . setSelectedNotePath ( "" ) ;
autocompleteSource ( searchString , $el . data ( 'autocompleteCallback' ) , options , false ) ;
2024-11-24 13:10:33 +08:00
$el . trigger ( 'focus' ) ;
}
2024-11-23 20:51:51 +08:00
}
2018-11-14 11:28:52 +01:00
function initNoteAutocomplete ( $el , options ) {
2019-05-22 20:53:59 +02:00
if ( $el . hasClass ( "note-autocomplete-input" ) || utils . isMobile ( ) ) {
2020-10-02 21:44:21 +02:00
// clear any event listener added in previous invocation of this function
$el . off ( 'autocomplete:noteselected' ) ;
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
2024-09-03 17:08:07 +02:00
const $clearTextButton = $ ( "<button>" )
. addClass ( "input-group-text input-clearer-button bx bxs-tag-x" )
. prop ( "title" , "Clear text field" ) ;
2018-11-14 11:28:52 +01:00
2024-09-03 17:08:07 +02:00
const $showRecentNotesButton = $ ( "<button>" )
. addClass ( "input-group-text show-recent-notes-button bx bx-time" )
. prop ( "title" , "Show recent notes" ) ;
2018-11-14 11:28:52 +01:00
2024-11-23 20:51:51 +08:00
const $fullTextSearchButton = $ ( "<button>" )
. addClass ( "input-group-text full-text-search-button bx bx-search" )
2024-11-24 13:10:33 +08:00
. prop ( "title" , "Full text search (Shift+Enter)" ) ;
2024-11-23 20:51:51 +08:00
2024-09-03 17:08:07 +02:00
const $goToSelectedNoteButton = $ ( "<button>" )
2023-05-07 21:18:21 +02:00
. 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
2019-11-09 17:39:48 +01:00
$clearTextButton . on ( 'click' , ( ) => clearText ( $el ) ) ;
2019-05-22 20:53:59 +02:00
2019-11-09 17:39:48 +01: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
2024-11-23 20:51:51 +08:00
$fullTextSearchButton . on ( 'click' , e => {
fullTextSearch ( $el , options ) ;
2024-11-26 15:41:18 +08:00
return false ;
2024-11-23 20:51:51 +08:00
} ) ;
2023-08-26 17:03:09 +03:00
let autocompleteOptions = { } ;
if ( options . container ) {
autocompleteOptions . dropdownMenuContainer = options . container ;
2023-08-26 17:07:04 +03: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 ) {
2024-11-20 14:22:39 +08:00
$el . on ( 'keydown' , ( event ) => {
if ( event . ctrlKey && event . key === 'Enter' ) {
// Prevent Ctrl + Enter from triggering autoComplete.
event . stopImmediatePropagation ( ) ;
event . preventDefault ( ) ;
$el . trigger ( 'autocomplete:selected' , { action : 'search-notes' , noteTitle : $el . autocomplete ( "val" ) } ) ;
}
} ) ;
}
2024-11-23 20:51:51 +08:00
$el . on ( 'keydown' , async ( event ) => {
if ( event . shiftKey && event . key === 'Enter' ) {
// Prevent Enter from triggering autoComplete.
event . stopImmediatePropagation ( ) ;
event . preventDefault ( ) ;
fullTextSearch ( $el , options )
}
} ) ;
2019-05-22 20:53:59 +02:00
$el . autocomplete ( {
2023-08-26 17:03:09 +03:00
... autocompleteOptions ,
2023-08-26 17:48:12 +03:00
appendTo : document . querySelector ( 'body' ) ,
2019-05-22 20:53:59 +02:00
hint : false ,
autoselect : true ,
2022-06-03 08:07:27 +02:00
// openOnFocus has to be false, otherwise re-focus (after return from note type chooser dialog) forces
2023-05-05 23:41:11 +02:00
// re-querying of the autocomplete source which then changes the currently selected suggestion
2022-06-03 08:07:27 +02:00
openOnFocus : false ,
2019-05-22 20:53:59 +02:00
minLength : 0 ,
tabAutocomplete : false
} , [
{
2024-11-26 15:41:18 +08:00
source : ( term , cb ) => {
$el . data ( 'autocompleteCallback' , cb ) ;
autocompleteSource ( term , cb , options ) ;
} ,
2020-05-16 22:11:09 +02:00
displayKey : 'notePathTitle' ,
2019-05-22 20:53:59 +02:00
templates : {
2020-09-21 00:07:46 +02:00
suggestion : suggestion => suggestion . highlightedNotePathTitle
2019-05-22 20:53:59 +02:00
} ,
// 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
2020-09-21 00:07:46 +02:00
$el . on ( 'autocomplete:selected' , async ( event , suggestion ) => {
2021-01-08 21:44:43 +01:00
if ( suggestion . action === 'external-link' ) {
$el . setSelectedNotePath ( null ) ;
$el . setSelectedExternalLink ( suggestion . externalLink ) ;
$el . autocomplete ( "val" , suggestion . externalLink ) ;
$el . autocomplete ( "close" ) ;
$el . trigger ( 'autocomplete:externallinkselected' , [ suggestion ] ) ;
return ;
}
2020-09-21 22:08:54 +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 ;
suggestion . notePath = note . getBestNotePathString ( hoistedNoteId ) ;
2020-09-21 00:07:46 +02:00
}
2024-11-20 14:22:39 +08:00
if ( suggestion . action === 'search-notes' ) {
const searchString = suggestion . noteTitle ;
appContext . triggerCommand ( 'searchNotes' , { searchString } ) ;
return ;
}
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" ) ;
2020-09-21 22:08:54 +02:00
$el . trigger ( 'autocomplete:noteselected' , [ suggestion ] ) ;
2020-09-03 17:38:11 +02:00
} ) ;
2019-05-22 20:53:59 +02:00
$el . on ( 'autocomplete:closed' , ( ) => {
if ( ! $el . val ( ) . trim ( ) ) {
clearText ( $el ) ;
}
} ) ;
2018-11-07 09:51:14 +01:00
2020-08-11 22:52:17 +02:00
$el . on ( 'autocomplete:opened' , ( ) => {
if ( $el . attr ( "readonly" ) ) {
$el . autocomplete ( 'close' ) ;
}
} ) ;
2020-10-02 21:44:21 +02:00
// clear any event listener added in previous invocation of this function
$el . off ( 'autocomplete:noteselected' ) ;
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 ( ) {
2018-12-24 10:10:36 +01:00
if ( ! $ ( this ) . val ( ) . trim ( ) ) {
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 ( ) {
const notePath = $ ( this ) . getSelectedNotePath ( ) ;
2023-01-13 10:09:41 +01:00
if ( ! notePath ) {
return null ;
}
2020-08-12 23:39:05 +02:00
const chunks = notePath . split ( '/' ) ;
return chunks . length >= 1 ? chunks [ chunks . length - 1 ] : null ;
}
2020-05-16 22:11:09 +02:00
$ . fn . setSelectedNotePath = function ( notePath ) {
notePath = notePath || "" ;
2018-11-14 11:17:20 +01:00
2020-05-16 22:11:09 +02:00
$ ( this ) . attr ( SELECTED _NOTE _PATH _KEY , notePath ) ;
2018-11-14 11:17:20 +01:00
2018-12-24 10:10:36 +01:00
$ ( this )
. closest ( ".input-group" )
. find ( ".go-to-selected-note-button" )
2020-05-16 22:11:09 +02:00
. toggleClass ( "disabled" , ! notePath . trim ( ) )
2023-05-07 21:29:09 +02:00
. 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 ( ) {
if ( ! $ ( this ) . val ( ) . trim ( ) ) {
return "" ;
} else {
return $ ( this ) . attr ( SELECTED _EXTERNAL _LINK _KEY ) ;
}
} ;
$ . fn . setSelectedExternalLink = function ( externalLink ) {
2023-05-07 21:29:09 +02:00
if ( externalLink ) {
$ ( this )
. closest ( ".input-group" )
. find ( ".go-to-selected-note-button" )
. toggleClass ( "disabled" , true ) ;
}
2021-01-08 21:44:43 +01: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 ) ;
}
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
2020-05-16 22:11:09 +02:00
}