diff --git a/src/public/javascripts/services/protected_session.js b/src/public/javascripts/services/protected_session.js index 2f6fba0d9..46c98a02b 100644 --- a/src/public/javascripts/services/protected_session.js +++ b/src/public/javascripts/services/protected_session.js @@ -80,11 +80,10 @@ async function setupProtectedSession() { $noteDetailWrapper.show(); protectedSessionDeferred.resolve(); + protectedSessionDeferred = null; $protectedSessionOnButton.addClass('active'); $protectedSessionOffButton.removeClass('active'); - - protectedSessionDeferred = null; } } diff --git a/src/public/javascripts/services/search_tree.js b/src/public/javascripts/services/search_tree.js index 8769279d5..24521efa6 100644 --- a/src/public/javascripts/services/search_tree.js +++ b/src/public/javascripts/services/search_tree.js @@ -1,5 +1,6 @@ import treeService from './tree.js'; import server from './server.js'; +import treeUtils from "./tree_utils.js"; const $tree = $("#tree"); const $searchInput = $("input[name='search-text']"); @@ -7,6 +8,8 @@ const $resetSearchButton = $("#reset-search-button"); const $doSearchButton = $("#do-search-button"); const $saveSearchButton = $("#save-search-button"); const $searchBox = $("#search-box"); +const $searchResults = $("#search-results"); +const $searchResultsInner = $("#search-results-inner"); function toggleSearch() { if ($searchBox.is(":hidden")) { @@ -16,14 +19,13 @@ function toggleSearch() { else { resetSearch(); + $searchResults.hide(); $searchBox.hide(); } } function resetSearch() { $searchInput.val(""); - - getTree().clearFilter(); } function getTree() { @@ -33,14 +35,21 @@ function getTree() { async function doSearch() { const searchText = $searchInput.val(); - const noteIds = await server.get('search/' + encodeURIComponent(searchText)); + const results = await server.get('search/' + encodeURIComponent(searchText)); - for (const noteId of noteIds) { - await treeService.expandToNote(noteId, {noAnimation: true, noEvents: true}); + $searchResultsInner.empty(); + $searchResults.show(); + + for (const result of results) { + const link = $('', { + href: 'javascript:', + text: result.title + }).attr('action', 'note').attr('note-path', result.path); + + const $result = $('
  • ').append(link); + + $searchResultsInner.append($result); } - - // Pass a string to perform case insensitive matching - getTree().filterBranches(node => noteIds.includes(node.data.noteId)); } async function saveSearch() { diff --git a/src/public/stylesheets/style.css b/src/public/stylesheets/style.css index a18d68425..ee5f5e696 100644 --- a/src/public/stylesheets/style.css +++ b/src/public/stylesheets/style.css @@ -366,4 +366,12 @@ div.ui-tooltip { #note-path-list .current a { font-weight: bold; +} + +#search-results { + padding: 0 5px 5px 15px; +} + +#search-results ul { + padding: 5px 5px 5px 15px; } \ No newline at end of file diff --git a/src/routes/api/search.js b/src/routes/api/search.js index e03aa96ce..30b6b0229 100644 --- a/src/routes/api/search.js +++ b/src/routes/api/search.js @@ -2,15 +2,69 @@ const sql = require('../../services/sql'); const noteService = require('../../services/notes'); +const autocompleteService = require('../../services/autocomplete'); +const utils = require('../../services/utils'); const parseFilters = require('../../services/parse_filters'); const buildSearchQuery = require('../../services/build_search_query'); async function searchNotes(req) { const {labelFilters, searchText} = parseFilters(req.params.searchString); - const {query, params} = buildSearchQuery(labelFilters, searchText); + let labelFiltersNoteIds = null; - const noteIds = await sql.getColumn(query, params); + if (labelFilters.length > 0) { + const {query, params} = buildSearchQuery(labelFilters, searchText); + + labelFiltersNoteIds = await sql.getColumn(query, params); + } + + let searchTextResults = null; + + if (searchText.trim().length > 0) { + searchTextResults = autocompleteService.getResults(searchText); + + let fullTextNoteIds = await getFullTextResults(searchText); + + for (const noteId of fullTextNoteIds) { + if (!searchTextResults.some(item => item.noteId === noteId)) { + const result = autocompleteService.getResult(noteId); + + if (result) { + searchTextResults.push(result); + } + } + } + } + + let results; + + if (labelFiltersNoteIds && searchTextResults) { + results = labelFiltersNoteIds.filter(item => searchTextResults.includes(item.noteId)); + } + else if (labelFiltersNoteIds) { + results = labelFiltersNoteIds.map(autocompleteService.getResult).filter(res => !!res); + } + else { + results = searchTextResults; + } + + return results; +} + +async function getFullTextResults(searchText) { + const tokens = searchText.toLowerCase().split(" "); + const tokenSql = ["1=1"]; + + for (const token of tokens) { + tokenSql.push(`content LIKE '%${token}%'`); + } + + const noteIds = await sql.getColumn(` + SELECT DISTINCT noteId + FROM notes + WHERE isDeleted = 0 + AND isProtected = 0 + AND ${tokenSql.join(' AND ')}`); return noteIds; } diff --git a/src/services/autocomplete.js b/src/services/autocomplete.js index 95ef24eea..1d11ed5fe 100644 --- a/src/services/autocomplete.js +++ b/src/services/autocomplete.js @@ -63,7 +63,7 @@ function getResults(query) { continue; } - const title = getNoteTitle(noteId, parentNoteId).toLowerCase(); + const title = getNoteTitleForParent(noteId, parentNoteId).toLowerCase(); const foundTokens = []; for (const token of tokens) { @@ -93,6 +93,7 @@ function search(noteId, tokens, path, results) { const noteTitle = getNoteTitleForPath(retPath); results.push({ + noteId: noteId, title: noteTitle, path: retPath.join('/') }); @@ -115,7 +116,7 @@ function search(noteId, tokens, path, results) { continue; } - const title = getNoteTitle(noteId, parentNoteId); + const title = getNoteTitleForParent(noteId, parentNoteId); const foundTokens = []; for (const token of tokens) { @@ -135,7 +136,7 @@ function search(noteId, tokens, path, results) { } } -function getNoteTitle(noteId, parentNoteId) { +function getNoteTitleForParent(noteId, parentNoteId) { const prefix = prefixes[noteId + '-' + parentNoteId]; let title = noteTitles[noteId]; @@ -158,7 +159,7 @@ function getNoteTitleForPath(path) { let parentNoteId = 'root'; for (const noteId of path) { - const title = getNoteTitle(noteId, parentNoteId); + const title = getNoteTitleForParent(noteId, parentNoteId); titles.push(title); parentNoteId = noteId; @@ -180,6 +181,10 @@ function getSomePath(noteId, path) { } for (const parentNoteId of parents) { + if (hideInAutocomplete[parentNoteId]) { + continue; + } + const retPath = getSomePath(parentNoteId, path.concat([noteId])); if (retPath) { @@ -190,6 +195,28 @@ function getSomePath(noteId, path) { return false; } +function getNoteTitle(noteId) { + if (noteId in noteTitles) { + return noteTitles[noteId]; + } + + return protectedNoteTitles[noteId]; +} + +function getResult(noteId) { + const retPath = getSomePath(noteId, []); + + if (retPath) { + const noteTitle = getNoteTitleForPath(retPath); + + return { + noteId: noteId, + title: noteTitle, + path: retPath.join('/') + }; + } +} + eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entityId}) => { if (entityName === 'notes') { const note = await repository.getNote(entityId); @@ -250,5 +277,7 @@ eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, async () => { sqlInit.dbReady.then(() => utils.stopWatch("Autocomplete load", load)); module.exports = { - getResults + getResults, + getNoteTitle, + getResult }; \ No newline at end of file diff --git a/src/services/build_search_query.js b/src/services/build_search_query.js index d38a60bab..84aa89f8c 100644 --- a/src/services/build_search_query.js +++ b/src/services/build_search_query.js @@ -1,4 +1,4 @@ -module.exports = function(labelFilters, searchText) { +module.exports = function(labelFilters) { const joins = []; const joinParams = []; let where = '1'; @@ -44,16 +44,6 @@ module.exports = function(labelFilters, searchText) { let searchCondition = ''; const searchParams = []; - if (searchText.trim() !== '') { - // searching in protected notes is pointless because of encryption - searchCondition = ' AND (notes.isProtected = 0 AND (notes.title LIKE ? OR notes.content LIKE ?))'; - - searchText = '%' + searchText.trim() + '%'; - - searchParams.push(searchText); - searchParams.push(searchText); // two occurences in searchCondition - } - const query = `SELECT DISTINCT notes.noteId FROM notes ${joins.join('\r\n')} WHERE diff --git a/src/services/utils.js b/src/services/utils.js index d2739ef48..140a5d91f 100644 --- a/src/services/utils.js +++ b/src/services/utils.js @@ -79,6 +79,32 @@ function stripTags(text) { return text.replace(/<(?:.|\n)*?>/gm, ''); } +function intersection(a, b) { + return a.filter(value => b.indexOf(value) !== -1); +} + +function union(a, b) { + const obj = {}; + + for (let i = a.length-1; i >= 0; i--) { + obj[a[i]] = a[i]; + } + + for (let i = b.length-1; i >= 0; i--) { + obj[b[i]] = b[i]; + } + + const res = []; + + for (const k in obj) { + if (obj.hasOwnProperty(k)) { // <-- optional + res.push(obj[k]); + } + } + + return res; +} + module.exports = { randomSecureToken, randomString, @@ -93,5 +119,7 @@ module.exports = { stopWatch, unescapeHtml, toObject, - stripTags + stripTags, + intersection, + union }; \ No newline at end of file diff --git a/src/views/index.ejs b/src/views/index.ejs index e22f59013..49a187975 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -76,17 +76,22 @@ -
    + + +