').attr('id', this.widgetId);
+
+ // actual rendering is async
+ this.doRender($widget);
+
+ return $widget;
+ }
+
+ /**
+ * for overriding
+ *
+ * @param {JQuery} $widget
+ */
+ async doRender($widget) {}
+
+ eventReceived(name, data) {
+ const fun = this[name + 'Listener'];
+
+ if (typeof fun === 'function') {
+ fun.call(this, data);
+ }
+ }
+
+ trigger(name, data) {
+ this.appContext.trigger(name, data);
+ }
+
+ cleanup() {}
+}
+
+export default BasicWidget;
\ No newline at end of file
diff --git a/src/public/javascripts/widgets/global_buttons.js b/src/public/javascripts/widgets/global_buttons.js
new file mode 100644
index 000000000..9da161d80
--- /dev/null
+++ b/src/public/javascripts/widgets/global_buttons.js
@@ -0,0 +1,42 @@
+import BasicWidget from "./basic_widget.js";
+
+const WIDGET_TPL = `
+
+
+
+`;
+
+class GlobalButtonsWidget extends BasicWidget {
+ async doRender($widget) {
+ $widget.append($(WIDGET_TPL));
+
+ const $createTopLevelNoteButton = $widget.find(".create-top-level-note-button");
+ const $collapseTreeButton = $widget.find(".collapse-tree-button");
+ const $scrollToActiveNoteButton = $widget.find(".scroll-to-active-note-button");
+ const $toggleSearchButton = $widget.find(".toggle-search-button");
+
+ $createTopLevelNoteButton.on('click', () => this.trigger('createTopLevelNote'));
+ $collapseTreeButton.on('click', () => this.trigger('collapseTree'));
+ $scrollToActiveNoteButton.on('click', () => this.trigger('scrollToActiveNote'));
+ $toggleSearchButton.on('click', () => this.trigger('toggleSearch'));
+ }
+}
+
+export default GlobalButtonsWidget;
\ No newline at end of file
diff --git a/src/public/javascripts/widgets/note_tree.js b/src/public/javascripts/widgets/note_tree.js
new file mode 100644
index 000000000..c2f4da4e7
--- /dev/null
+++ b/src/public/javascripts/widgets/note_tree.js
@@ -0,0 +1,65 @@
+import BasicWidget from "./basic_widget.js";
+import hoistedNoteService from "../services/hoisted_note.js";
+import searchNotesService from "../services/search_notes.js";
+import keyboardActionService from "../services/keyboard_actions.js";
+import treeService from "../services/tree.js";
+import treeUtils from "../services/tree_utils.js";
+import noteDetailService from "../services/note_detail.js";
+
+const TPL = `
+
+
+
+`;
+
+export default class NoteTreeWidget extends BasicWidget {
+ async doRender($widget) {
+ $widget.append($(TPL));
+
+ const $tree = $widget.find('#tree');
+
+ await treeService.showTree($tree);
+
+ $tree.on("click", ".unhoist-button", hoistedNoteService.unhoist);
+ $tree.on("click", ".refresh-search-button", searchNotesService.refreshSearch);
+
+ keyboardActionService.setGlobalActionHandler('CollapseTree', () => treeService.collapseTree()); // don't use shortened form since collapseTree() accepts argument
+
+ // fancytree doesn't support middle click so this is a way to support it
+ $widget.on('mousedown', '.fancytree-title', e => {
+ if (e.which === 2) {
+ const node = $.ui.fancytree.getNode(e);
+
+ treeUtils.getNotePath(node).then(notePath => {
+ if (notePath) {
+ noteDetailService.openInTab(notePath, false);
+ }
+ });
+
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ });
+ }
+
+ createTopLevelNoteListener() {
+ treeService.createNewTopLevelNote();
+ }
+
+ collapseTreeListener() {
+ treeService.collapseTree();
+ }
+
+ scrollToActiveNoteListener() {
+ treeService.scrollToActiveNote();
+ }
+}
\ No newline at end of file
diff --git a/src/public/javascripts/widgets/search_box.js b/src/public/javascripts/widgets/search_box.js
new file mode 100644
index 000000000..fae8316e1
--- /dev/null
+++ b/src/public/javascripts/widgets/search_box.js
@@ -0,0 +1,174 @@
+import BasicWidget from "./basic_widget.js";
+import treeService from "../services/tree.js";
+import treeCache from "../services/tree_cache.js";
+import toastService from "../services/toast.js";
+
+const helpText = `
+
Search tips - also see
+
+
+ - Just enter any text for full text search
+ @abc
- returns notes with label abc
+ @year=2019
- matches notes with label year
having value 2019
+ @rock @pop
- matches notes which have both rock
and pop
labels
+ @rock or @pop
- only one of the labels must be present
+ @year<=2000
- numerical comparison (also >, >=, <).
+ @dateCreated>=MONTH-1
- notes created in the last month
+ =handler
- will execute script defined in handler
relation to get results
+
+`;
+
+const TPL = `
+
+
+
+
+
+
+
+
+
+
+
`;
+
+export default class SearchBoxWidget extends BasicWidget {
+ async doRender($widget) {
+ $widget.append($(TPL));
+
+ this.$searchBox = $widget.find(".search-box");
+ this.$closeSearchButton = $widget.find(".close-search-button");
+ this.$searchInput = $widget.find("input[name='search-text']");
+ this.$resetSearchButton = $widget.find(".reset-search-button");
+ this.$doSearchButton = $widget.find(".do-search-button");
+ this.$saveSearchButton = $widget.find(".save-search-button");
+
+ this.$searchInput.on('keyup',e => {
+ const searchText = this.$searchInput.val();
+
+ if (e && e.which === $.ui.keyCode.ESCAPE || $.trim(searchText) === "") {
+ this.$resetSearchButton.trigger('click');
+ return;
+ }
+
+ if (e && e.which === $.ui.keyCode.ENTER) {
+ this.doSearch();
+ }
+ });
+
+ this.$doSearchButton.on('click', () => this.doSearch()); // keep long form because of argument
+ this.$resetSearchButton.on('click', () => this.resetSearchListener());
+
+ this.$saveSearchButton.on('click', () => this.saveSearch());
+
+ this.$closeSearchButton.on('click', () => this.hideSearchListener());
+ }
+
+ doSearch(searchText) {
+ if (searchText) {
+ this.$searchInput.val(searchText);
+ }
+ else {
+ searchText = this.$searchInput.val();
+ }
+
+ if (searchText.trim().length === 0) {
+ toastService.showMessage("Please enter search criteria first.");
+
+ this.$searchInput.trigger('focus');
+
+ return;
+ }
+
+ this.trigger('searchForResults', {
+ searchText: this.$searchInput.val()
+ });
+
+ this.$searchBox.tooltip("hide");
+ }
+
+ async saveSearch() {
+ const searchString = this.$searchInput.val().trim();
+
+ if (searchString.length === 0) {
+ alert("Write some search criteria first so there is something to save.");
+ return;
+ }
+
+ let activeNode = treeService.getActiveNode();
+ const parentNote = await treeCache.getNote(activeNode.data.noteId);
+
+ if (parentNote.type === 'search') {
+ activeNode = activeNode.getParent();
+ }
+
+ await treeService.createNote(activeNode, activeNode.data.noteId, 'into', {
+ type: "search",
+ mime: "application/json",
+ title: searchString,
+ content: JSON.stringify({ searchString: searchString })
+ });
+
+ this.resetSearchListener();
+ }
+
+ showSearchListener() {
+ this.$searchBox.slideDown();
+
+ this.$searchBox.tooltip({
+ trigger: 'focus',
+ html: true,
+ title: helpText,
+ placement: 'right',
+ delay: {
+ show: 500, // necessary because sliding out may cause wrong position
+ hide: 200
+ }
+ });
+
+ this.$searchInput.trigger('focus');
+ }
+
+ hideSearchListener() {
+ this.resetSearchListener();
+
+ this.$searchBox.slideUp();
+ }
+
+ toggleSearchListener() {
+ if (this.$searchBox.is(":hidden")) {
+ this.showSearchListener();
+ }
+ else {
+ this.hideSearchListener();
+ this.trigger('hideSearchResults');
+ }
+ }
+
+ resetSearchListener() {
+ this.$searchInput.val("");
+ }
+}
\ No newline at end of file
diff --git a/src/public/javascripts/widgets/search_results.js b/src/public/javascripts/widgets/search_results.js
new file mode 100644
index 000000000..16b412249
--- /dev/null
+++ b/src/public/javascripts/widgets/search_results.js
@@ -0,0 +1,68 @@
+import BasicWidget from "./basic_widget.js";
+import toastService from "../services/toast.js";
+import server from "../services/server.js";
+
+const TPL = `
+
+
+
+`;
+
+export default class SearchResultsWidget extends BasicWidget {
+ async doRender($widget) {
+ $widget.append($(TPL));
+
+ this.$searchResults = $widget.find(".search-results");
+ this.$searchResultsInner = $widget.find(".search-results-inner");
+ }
+
+ async searchForResultsListener({searchText}) {
+ const response = await server.get('search/' + encodeURIComponent(searchText));
+
+ if (!response.success) {
+ toastService.showError("Search failed.", 3000);
+ return;
+ }
+
+ this.$searchResultsInner.empty();
+ this.$searchResults.show();
+
+ for (const result of response.results) {
+ const link = $('
', {
+ href: 'javascript:',
+ text: result.title
+ }).attr('data-action', 'note').attr('data-note-path', result.path);
+
+ const $result = $('').append(link);
+
+ this.$searchResultsInner.append($result);
+ }
+
+ // have at least some feedback which is good especially in situations
+ // when the result list does not change with a query
+ toastService.showMessage("Search finished successfully.");
+ }
+
+ hideSearchResultsListener() {
+ this.$searchResults.hide();
+ }
+}
\ No newline at end of file
diff --git a/src/public/stylesheets/desktop.css b/src/public/stylesheets/desktop.css
index d7349ded1..0545cb310 100644
--- a/src/public/stylesheets/desktop.css
+++ b/src/public/stylesheets/desktop.css
@@ -45,21 +45,6 @@ body {
width: 100%;
}
-#search-box {
- display: none;
- padding: 10px;
- margin-top: 10px;
-}
-
-#tree {
- overflow: auto;
- flex-grow: 1;
- flex-shrink: 1;
- flex-basis: 60%;
- font-family: var(--tree-font-family);
- font-size: var(--tree-font-size);
-}
-
#left-pane {
height: 100%;
display: flex;
@@ -95,15 +80,6 @@ body {
margin: 0 15px 0 5px;
}
-#global-buttons {
- display: flex;
- justify-content: space-around;
- padding: 3px 0 3px 0;
- border: 1px solid var(--main-border-color);
- border-radius: 7px;
- margin: 3px 5px 5px 5px;
-}
-
.dropdown-menu {
font-size: inherit;
}
diff --git a/src/public/stylesheets/style.css b/src/public/stylesheets/style.css
index 3deaeebf6..44db06758 100644
--- a/src/public/stylesheets/style.css
+++ b/src/public/stylesheets/style.css
@@ -264,25 +264,6 @@ div.ui-tooltip {
width: auto;
}
-#search-results {
- padding: 0 5px 5px 15px;
- flex-basis: 40%;
- flex-grow: 1;
- flex-shrink: 1;
- margin-top: 10px;
- display: none;
- overflow: auto;
- border-bottom: 2px solid var(--main-border-color);
-}
-
-#search-results ul {
- padding: 5px 5px 5px 15px;
-}
-
-#search-text {
- border: 1px solid var(--main-border-color);
-}
-
/*
* .search-inactive is added to search window when the window
* is inactive.
diff --git a/src/views/desktop.ejs b/src/views/desktop.ejs
index a91093747..fdf8014aa 100644
--- a/src/views/desktop.ejs
+++ b/src/views/desktop.ejs
@@ -138,55 +138,15 @@