diff --git a/src/public/app/widgets/quick_search.js b/src/public/app/widgets/quick_search.js
new file mode 100644
index 000000000..d979e04e4
--- /dev/null
+++ b/src/public/app/widgets/quick_search.js
@@ -0,0 +1,87 @@
+import BasicWidget from "./basic_widget.js";
+import server from "../services/server.js";
+import linkService from "../services/link.js";
+import treeCache from "../services/tree_cache.js";
+import utils from "../services/utils.js";
+
+const TPL = `
+
+`;
+
+const MAX_DISPLAYED_NOTES = 20;
+
+export default class QuickSearchWidget extends BasicWidget {
+ doRender() {
+ this.$widget = $(TPL);
+ this.overflowing();
+
+ this.$searchString = this.$widget.find('.search-string');
+ this.$dropdownMenu = this.$widget.find('.dropdown-menu');
+ this.$dropdownToggle = this.$widget.find('.search-button');
+
+ this.$widget.find('.input-group-append').on('show.bs.dropdown', () => this.search());
+
+ utils.bindElShortcut(this.$searchString, 'return', () => {
+ this.$dropdownToggle.dropdown('show');
+
+ this.$searchString.focus();
+ });
+
+ return this.$widget;
+ }
+
+ async search() {
+ this.$dropdownMenu.empty();
+ this.$dropdownMenu.append(' Searching ...');
+
+ const resultNoteIds = await server.get('quick-search/' + encodeURIComponent(this.$searchString.val()));
+
+ const displayedNoteIds = resultNoteIds.slice(0, Math.min(MAX_DISPLAYED_NOTES, resultNoteIds.length));
+
+ this.$dropdownMenu.empty();
+
+ this.$dropdownMenu.append('');
+
+ if (displayedNoteIds.length === 0) {
+ this.$dropdownMenu.append('No results found');
+ }
+
+ for (const note of await treeCache.getNotes(displayedNoteIds)) {
+ const $link = await linkService.createNoteLink(note.noteId, {showNotePath: true});
+ $link.addClass('dropdown-item');
+
+ this.$dropdownMenu.append($link);
+ }
+
+ if (resultNoteIds.length > MAX_DISPLAYED_NOTES) {
+ this.$dropdownMenu.append(`... and ${resultNoteIds.length - MAX_DISPLAYED_NOTES} more results.`);
+ }
+
+ this.$dropdownToggle.dropdown('update');
+ }
+}
diff --git a/src/public/app/widgets/standard_top_widget.js b/src/public/app/widgets/standard_top_widget.js
index ee18e22f9..90055ea7c 100644
--- a/src/public/app/widgets/standard_top_widget.js
+++ b/src/public/app/widgets/standard_top_widget.js
@@ -1,6 +1,7 @@
import BasicWidget from "./basic_widget.js";
import HistoryNavigationWidget from "./history_navigation.js";
import protectedSessionService from "../services/protected_session.js";
+import QuickSearchWidget from "./quick_search.js";
const TPL = `
@@ -13,7 +14,7 @@ const TPL = `
height: 35px;
}
- .standard-top-widget button {
+ .standard-top-widget button:not(.search-button) {
padding: 1px 5px 1px 5px;
font-size: 90%;
margin-bottom: 2px;
@@ -84,6 +85,11 @@ export default class StandardTopWidget extends BasicWidget {
this.$widget.prepend(historyNavigationWidget.render());
+ const quickSearchWidget = new QuickSearchWidget();
+ this.child(quickSearchWidget);
+
+ this.$widget.append(quickSearchWidget.render());
+
this.$enterProtectedSessionButton = this.$widget.find(".enter-protected-session-button");
this.$enterProtectedSessionButton.on('click', protectedSessionService.enterProtectedSession);
diff --git a/src/routes/api/search.js b/src/routes/api/search.js
index d15916c10..d5969afac 100644
--- a/src/routes/api/search.js
+++ b/src/routes/api/search.js
@@ -200,6 +200,19 @@ async function searchFromRelation(note, relationName) {
return typeof result[0] === 'string' ? result : result.map(item => item.noteId);
}
+function quickSearch(req) {
+ const {searchString} = req.params;
+
+ const searchContext = new SearchContext({
+ fastSearch: false,
+ includeArchivedNotes: false,
+ fuzzyAttributeSearch: false
+ });
+
+ return searchService.findNotesWithQuery(searchString, searchContext)
+ .map(sr => sr.noteId);
+}
+
function getRelatedNotes(req) {
const attr = req.body;
@@ -276,5 +289,6 @@ function formatValue(val) {
module.exports = {
searchFromNote,
searchAndExecute,
- getRelatedNotes
+ getRelatedNotes,
+ quickSearch
};
diff --git a/src/routes/routes.js b/src/routes/routes.js
index 0bf7b25cb..d8b4c6b9e 100644
--- a/src/routes/routes.js
+++ b/src/routes/routes.js
@@ -262,6 +262,7 @@ function register(app) {
route(POST, '/api/sender/image', [auth.checkToken, uploadMiddleware], senderRoute.uploadImage, apiResultHandler);
route(POST, '/api/sender/note', [auth.checkToken], senderRoute.saveNote, apiResultHandler);
+ apiRoute(GET, '/api/quick-search/:searchString', searchRoute.quickSearch);
apiRoute(GET, '/api/search-note/:noteId', searchRoute.searchFromNote);
apiRoute(POST, '/api/search-and-execute-note/:noteId', searchRoute.searchAndExecute);
apiRoute(POST, '/api/search-related', searchRoute.getRelatedNotes);
diff --git a/src/services/search/search_context.js b/src/services/search/search_context.js
index 01691e472..864878bae 100644
--- a/src/services/search/search_context.js
+++ b/src/services/search/search_context.js
@@ -1,9 +1,11 @@
"use strict";
+const cls = require('../cls');
+
class SearchContext {
constructor(params = {}) {
this.fastSearch = !!params.fastSearch;
- this.ancestorNoteId = params.ancestorNoteId;
+ this.ancestorNoteId = params.ancestorNoteId || cls.getHoistedNoteId();
this.ancestorDepth = params.ancestorDepth;
this.includeArchivedNotes = !!params.includeArchivedNotes;
this.orderBy = params.orderBy;