diff --git a/src/public/javascripts/dialogs/jump_to_note.js b/src/public/javascripts/dialogs/jump_to_note.js index 0b8fcd046..906b929d7 100644 --- a/src/public/javascripts/dialogs/jump_to_note.js +++ b/src/public/javascripts/dialogs/jump_to_note.js @@ -1,7 +1,6 @@ import treeService from '../services/tree.js'; import linkService from '../services/link.js'; -import utils from '../services/utils.js'; -import autocompleteService from '../services/autocomplete.js'; +import server from '../services/server.js'; const $dialog = $("#jump-to-note-dialog"); const $autoComplete = $("#jump-to-note-autocomplete"); @@ -18,8 +17,12 @@ async function showDialog() { }); await $autoComplete.autocomplete({ - source: await utils.stopWatch("building autocomplete", autocompleteService.getAutocompleteItems), - minLength: 1 + source: async function(request, response) { + const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term)); + + response(result); + }, + minLength: 2 }); } diff --git a/src/routes/api/autocomplete.js b/src/routes/api/autocomplete.js new file mode 100644 index 000000000..039758cca --- /dev/null +++ b/src/routes/api/autocomplete.js @@ -0,0 +1,20 @@ +"use strict"; + +const autocompleteService = require('../../services/autocomplete'); + +async function getAutocomplete(req) { + const query = req.query.query; + + const results = autocompleteService.getResults(query); + + return results.map(res => { + return { + value: res.title + '(' + res.path + ')', + title: res.title + } + }); +} + +module.exports = { + getAutocomplete +}; \ No newline at end of file diff --git a/src/routes/routes.js b/src/routes/routes.js index 568e9fad4..a3f79be16 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -8,6 +8,7 @@ const multer = require('multer')(); const treeApiRoute = require('./api/tree'); const notesApiRoute = require('./api/notes'); const branchesApiRoute = require('./api/branches'); +const autocompleteApiRoute = require('./api/autocomplete'); const cloningApiRoute = require('./api/cloning'); const noteRevisionsApiRoute = require('./api/note_revisions'); const recentChangesApiRoute = require('./api/recent_changes'); @@ -108,6 +109,8 @@ function register(app) { apiRoute(PUT, '/api/branches/:branchId/expanded/:expanded', branchesApiRoute.setExpanded); apiRoute(DELETE, '/api/branches/:branchId', branchesApiRoute.deleteBranch); + apiRoute(GET, '/api/autocomplete', autocompleteApiRoute.getAutocomplete); + apiRoute(GET, '/api/notes/:noteId', notesApiRoute.getNote); apiRoute(PUT, '/api/notes/:noteId', notesApiRoute.updateNote); apiRoute(POST, '/api/notes/:parentNoteId/children', notesApiRoute.createNote); diff --git a/src/services/autocomplete.js b/src/services/autocomplete.js new file mode 100644 index 000000000..0175c74e0 --- /dev/null +++ b/src/services/autocomplete.js @@ -0,0 +1,102 @@ +const sql = require('./sql'); +const sqlInit = require('./sql_init'); + +let noteTitles; +let noteIds; +const childToParent = {}; + +async function load() { + noteTitles = await sql.getMap(`SELECT noteId, LOWER(title) FROM notes WHERE isDeleted = 0 AND isProtected = 0`); + noteIds = Object.keys(noteTitles); + + const relations = await sql.getRows(`SELECT noteId, parentNoteId FROM branches WHERE isDeleted = 0`); + + for (const rel of relations) { + childToParent[rel.noteId] = childToParent[rel.noteId] || []; + childToParent[rel.noteId].push(rel.parentNoteId); + } +} + +function getResults(query) { + if (!noteTitles || query.length <= 2) { + return []; + } + + const tokens = query.toLowerCase().split(" "); + const results = []; + + for (const noteId in noteTitles) { + const title = noteTitles[noteId]; + const foundTokens = []; + + for (const token of tokens) { + if (title.includes(token)) { + foundTokens.push(token); + } + } + + if (foundTokens.length > 0) { + const remainingTokens = tokens.filter(token => !foundTokens.includes(token)); + + search(childToParent[noteId], remainingTokens, [noteId], results); + } + } + + return results; +} + +function search(noteIds, tokens, path, results) { + if (!noteIds) { + return; + } + + for (const noteId of noteIds) { + if (noteId === 'root') { + if (tokens.length === 0) { + const reversedPath = path.slice(); + reversedPath.reverse(); + + const noteTitle = getNoteTitle(reversedPath); + + console.log(noteTitle); + + results.push({ + title: noteTitle, + path: reversedPath.join('/') + }); + } + + continue; + } + + const title = noteTitles[noteId]; + const foundTokens = []; + + for (const token of tokens) { + if (title.includes(token)) { + foundTokens.push(token); + } + } + + if (foundTokens.length > 0) { + const remainingTokens = tokens.filter(token => !foundTokens.includes(token)); + + search(childToParent[noteId], remainingTokens, path.concat([noteId]), results); + } + else { + search(childToParent[noteId], tokens, path.concat([noteId]), results); + } + } +} + +function getNoteTitle(path) { + const titles = path.map(noteId => noteTitles[noteId]); + + return titles.join(' / '); +} + +sqlInit.dbReady.then(load); + +module.exports = { + getResults +}; \ No newline at end of file