mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	start of the refactoring to widget system
This commit is contained in:
		| @@ -34,6 +34,7 @@ import keyboardActionService from "./services/keyboard_actions.js"; | ||||
| import splitService from "./services/split.js"; | ||||
| import optionService from "./services/options.js"; | ||||
| import noteContentRenderer from "./services/note_content_renderer.js"; | ||||
| import AppContext from "./services/app_context.js"; | ||||
|  | ||||
| window.glob.isDesktop = utils.isDesktop; | ||||
| window.glob.isMobile = utils.isMobile; | ||||
| @@ -117,10 +118,6 @@ $("#logout-button").on('click', () => { | ||||
|     $logoutForm.trigger('submit'); | ||||
| }); | ||||
|  | ||||
| $("#tree").on("click", ".unhoist-button", hoistedNoteService.unhoist); | ||||
|  | ||||
| $("#tree").on("click", ".refresh-search-button", searchNotesService.refreshSearch); | ||||
|  | ||||
| $("body").on("click", "a.external", function () { | ||||
|     window.open($(this).attr("href"), '_blank'); | ||||
| }); | ||||
| @@ -186,7 +183,8 @@ macInit.init(); | ||||
|  | ||||
| searchNotesService.init(); // should be in front of treeService since that one manipulates address bar hash | ||||
|  | ||||
| treeService.showTree(); | ||||
| const appContext = new AppContext(); | ||||
| appContext.showWidgets(); | ||||
|  | ||||
| entrypoints.registerEntrypoints(); | ||||
|  | ||||
|   | ||||
							
								
								
									
										33
									
								
								src/public/javascripts/services/app_context.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/public/javascripts/services/app_context.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| import GlobalButtonsWidget from "../widgets/global_buttons.js"; | ||||
| import SearchBoxWidget from "../widgets/search_box.js"; | ||||
| import SearchResultsWidget from "../widgets/search_results.js"; | ||||
| import NoteTreeWidget from "../widgets/note_tree.js"; | ||||
|  | ||||
| export default class AppContext { | ||||
|     constructor() { | ||||
|         this.widgets = []; | ||||
|     } | ||||
|  | ||||
|     trigger(name, data) { | ||||
|         for (const widget of this.widgets) { | ||||
|             widget.eventReceived(name, data); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     showWidgets() { | ||||
|         const $leftPane = $("#left-pane"); | ||||
|  | ||||
|         this.widgets = [ | ||||
|             new GlobalButtonsWidget(this), | ||||
|             new SearchBoxWidget(this), | ||||
|             new SearchResultsWidget(this), | ||||
|             new NoteTreeWidget(this) | ||||
|         ]; | ||||
|  | ||||
|         for (const widget of this.widgets) { | ||||
|             const $widget = widget.render(); | ||||
|  | ||||
|             $leftPane.append($widget); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -46,7 +46,6 @@ function registerEntrypoints() { | ||||
|     $("#enter-protected-session-button").on('click', protectedSessionService.enterProtectedSession); | ||||
|     $("#leave-protected-session-button").on('click', protectedSessionService.leaveProtectedSession); | ||||
|  | ||||
|     $("#toggle-search-button").on('click', searchNotesService.toggleSearch); | ||||
|     keyboardActionService.setGlobalActionHandler('SearchNotes', searchNotesService.toggleSearch); | ||||
|  | ||||
|     const $noteTabContainer = $("#note-tab-container"); | ||||
|   | ||||
| @@ -3,136 +3,6 @@ import treeCache from "./tree_cache.js"; | ||||
| import server from './server.js'; | ||||
| import toastService from "./toast.js"; | ||||
|  | ||||
| const $searchInput = $("input[name='search-text']"); | ||||
| 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"); | ||||
| const $closeSearchButton = $("#close-search-button"); | ||||
|  | ||||
| const helpText = ` | ||||
| <strong>Search tips</strong> - also see <button class="btn btn-sm" type="button" data-help-page="Search">complete help on search</button> | ||||
| <p> | ||||
| <ul> | ||||
|     <li>Just enter any text for full text search</li> | ||||
|     <li><code>@abc</code> - returns notes with label abc</li> | ||||
|     <li><code>@year=2019</code> - matches notes with label <code>year</code> having value <code>2019</code></li> | ||||
|     <li><code>@rock @pop</code> - matches notes which have both <code>rock</code> and <code>pop</code> labels</li> | ||||
|     <li><code>@rock or @pop</code> - only one of the labels must be present</li> | ||||
|     <li><code>@year<=2000</code> - numerical comparison (also >, >=, <).</li> | ||||
|     <li><code>@dateCreated>=MONTH-1</code> - notes created in the last month</li> | ||||
|     <li><code>=handler</code> - will execute script defined in <code>handler</code> relation to get results</li> | ||||
| </ul> | ||||
| </p>`; | ||||
|  | ||||
| function showSearch() { | ||||
|     $searchBox.slideDown(); | ||||
|  | ||||
|     $searchBox.tooltip({ | ||||
|         trigger: 'focus', | ||||
|         html: true, | ||||
|         title: helpText, | ||||
|         placement: 'right', | ||||
|         delay: { | ||||
|             show: 500, // necessary because sliding out may cause wrong position | ||||
|             hide: 200 | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     $searchInput.trigger('focus'); | ||||
| } | ||||
|  | ||||
| function hideSearch() { | ||||
|     resetSearch(); | ||||
|  | ||||
|     $searchResults.hide(); | ||||
|     $searchBox.slideUp(); | ||||
| } | ||||
|  | ||||
| function toggleSearch() { | ||||
|     if ($searchBox.is(":hidden")) { | ||||
|         showSearch(); | ||||
|     } | ||||
|     else { | ||||
|         hideSearch(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| function resetSearch() { | ||||
|     $searchInput.val(""); | ||||
| } | ||||
|  | ||||
| async function doSearch(searchText) { | ||||
|     if (searchText) { | ||||
|         $searchInput.val(searchText); | ||||
|     } | ||||
|     else { | ||||
|         searchText = $searchInput.val(); | ||||
|     } | ||||
|  | ||||
|     if (searchText.trim().length === 0) { | ||||
|         toastService.showMessage("Please enter search criteria first."); | ||||
|  | ||||
|         $searchInput.trigger('focus'); | ||||
|  | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     $searchBox.tooltip("hide"); | ||||
|  | ||||
|     const response = await server.get('search/' + encodeURIComponent(searchText)); | ||||
|  | ||||
|     if (!response.success) { | ||||
|         toastService.showError("Search failed.", 3000); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     $searchResultsInner.empty(); | ||||
|     $searchResults.show(); | ||||
|  | ||||
|     for (const result of response.results) { | ||||
|         const link = $('<a>', { | ||||
|             href: 'javascript:', | ||||
|             text: result.title | ||||
|         }).attr('data-action', 'note').attr('data-note-path', result.path); | ||||
|  | ||||
|         const $result = $('<li>').append(link); | ||||
|  | ||||
|         $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."); | ||||
| } | ||||
|  | ||||
| async function saveSearch() { | ||||
|     const searchString = $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 }) | ||||
|     }); | ||||
|  | ||||
|     resetSearch(); | ||||
| } | ||||
|  | ||||
| async function refreshSearch() { | ||||
|     const activeNode = treeService.getActiveNode(); | ||||
|  | ||||
| @@ -157,32 +27,12 @@ function init() { | ||||
|     } | ||||
| } | ||||
|  | ||||
| $searchInput.on('keyup',e => { | ||||
|     const searchText = $searchInput.val(); | ||||
|  | ||||
|     if (e && e.which === $.ui.keyCode.ESCAPE || $.trim(searchText) === "") { | ||||
|         $resetSearchButton.trigger('click'); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     if (e && e.which === $.ui.keyCode.ENTER) { | ||||
|         doSearch(); | ||||
|     } | ||||
| }); | ||||
|  | ||||
| $doSearchButton.on('click', () => doSearch()); // keep long form because of argument | ||||
| $resetSearchButton.on('click', resetSearch); | ||||
|  | ||||
| $saveSearchButton.on('click', saveSearch); | ||||
|  | ||||
| $closeSearchButton.on('click', hideSearch); | ||||
|  | ||||
| export default { | ||||
|     toggleSearch, | ||||
|     resetSearch, | ||||
|     showSearch, | ||||
|     // toggleSearch, | ||||
|     // resetSearch, | ||||
|     // showSearch, | ||||
|     // doSearch, | ||||
|     refreshSearch, | ||||
|     doSearch, | ||||
|     init, | ||||
|     searchInSubtree, | ||||
|     getHelpText: () => helpText | ||||
|   | ||||
| @@ -18,11 +18,6 @@ import keyboardActionService from "./keyboard_actions.js"; | ||||
|  | ||||
| let tree; | ||||
|  | ||||
| const $tree = $("#tree"); | ||||
| const $createTopLevelNoteButton = $("#create-top-level-note-button"); | ||||
| const $collapseTreeButton = $("#collapse-tree-button"); | ||||
| const $scrollToActiveNoteButton = $("#scroll-to-active-note-button"); | ||||
|  | ||||
| let setFrontendAsLoaded; | ||||
| const frontendLoaded = new Promise(resolve => { setFrontendAsLoaded = resolve; }); | ||||
|  | ||||
| @@ -429,7 +424,7 @@ async function treeInitialized() { | ||||
|     setFrontendAsLoaded(); | ||||
| } | ||||
|  | ||||
| async function initFancyTree(treeData) { | ||||
| async function initFancyTree($tree, treeData) { | ||||
|     utils.assertArguments(treeData); | ||||
|  | ||||
|     $tree.fancytree({ | ||||
| @@ -750,10 +745,10 @@ async function sortAlphabetically(noteId) { | ||||
|     await reload(); | ||||
| } | ||||
|  | ||||
| async function showTree() { | ||||
| async function showTree($tree) { | ||||
|     const treeData = await loadTreeData(); | ||||
|  | ||||
|     await initFancyTree(treeData); | ||||
|     await initFancyTree($tree, treeData); | ||||
| } | ||||
|  | ||||
| ws.subscribeToMessages(message => { | ||||
| @@ -882,22 +877,6 @@ $(window).bind('hashchange', async function() { | ||||
|     } | ||||
| }); | ||||
|  | ||||
| // fancytree doesn't support middle click so this is a way to support it | ||||
| $tree.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(); | ||||
|     } | ||||
| }); | ||||
|  | ||||
| async function duplicateNote(noteId, parentNoteId) { | ||||
|     const {note} = await server.post(`notes/${noteId}/duplicate/${parentNoteId}`); | ||||
|  | ||||
| @@ -913,12 +892,6 @@ function getNodeByKey(key) { | ||||
|     return tree.getNodeByKey(key); | ||||
| } | ||||
|  | ||||
| keyboardActionService.setGlobalActionHandler('CollapseTree', () => collapseTree()); // don't use shortened form since collapseTree() accepts argument | ||||
| $collapseTreeButton.on('click', () => collapseTree()); | ||||
|  | ||||
| $createTopLevelNoteButton.on('click', createNewTopLevelNote); | ||||
| $scrollToActiveNoteButton.on('click', scrollToActiveNote); | ||||
|  | ||||
| frontendLoaded.then(bundle.executeStartupBundles); | ||||
|  | ||||
| export default { | ||||
| @@ -948,6 +921,7 @@ export default { | ||||
|     getSomeNotePath, | ||||
|     focusTree, | ||||
|     scrollToActiveNote, | ||||
|     createNewTopLevelNote, | ||||
|     duplicateNote, | ||||
|     getNodeByKey | ||||
| }; | ||||
							
								
								
									
										41
									
								
								src/public/javascripts/widgets/basic_widget.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/public/javascripts/widgets/basic_widget.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| class BasicWidget { | ||||
|     /** | ||||
|      * @param {AppContext} appContext | ||||
|      */ | ||||
|     constructor(appContext) { | ||||
|         this.appContext = appContext; | ||||
|         this.widgetId = `widget-${this.constructor.name}`; | ||||
|     } | ||||
|  | ||||
|     render() { | ||||
|         const $widget = $('<div>').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; | ||||
							
								
								
									
										42
									
								
								src/public/javascripts/widgets/global_buttons.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/public/javascripts/widgets/global_buttons.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| import BasicWidget from "./basic_widget.js"; | ||||
|  | ||||
| const WIDGET_TPL = ` | ||||
| <style> | ||||
| .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; | ||||
| } | ||||
| </style> | ||||
|  | ||||
| <div class="global-buttons"> | ||||
|     <a title="Create new top level note" class="create-top-level-note-button icon-action bx bx-folder-plus"></a> | ||||
|  | ||||
|     <a title="Collapse note tree" data-kb-action="CollapseTree" class="collapse-tree-button icon-action bx bx-layer-minus"></a> | ||||
|  | ||||
|     <a title="Scroll to active note" data-kb-action="ScrollToActiveNote" class="scroll-to-active-note-button icon-action bx bx-crosshair"></a> | ||||
|  | ||||
|     <a title="Search in notes" data-kb-action="SearchNotes" class="toggle-search-button icon-action bx bx-search"></a> | ||||
| </div> | ||||
| `; | ||||
|  | ||||
| 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; | ||||
							
								
								
									
										65
									
								
								src/public/javascripts/widgets/note_tree.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/public/javascripts/widgets/note_tree.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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 = ` | ||||
| <style> | ||||
| #tree { | ||||
|     overflow: auto; | ||||
|     flex-grow: 1; | ||||
|     flex-shrink: 1; | ||||
|     flex-basis: 60%; | ||||
|     font-family: var(--tree-font-family); | ||||
|     font-size: var(--tree-font-size); | ||||
| } | ||||
| </style> | ||||
|  | ||||
| <div id="tree"></div> | ||||
| `; | ||||
|  | ||||
| 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(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										174
									
								
								src/public/javascripts/widgets/search_box.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								src/public/javascripts/widgets/search_box.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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 = ` | ||||
| <strong>Search tips</strong> - also see <button class="btn btn-sm" type="button" data-help-page="Search">complete help on search</button> | ||||
| <p> | ||||
| <ul> | ||||
|     <li>Just enter any text for full text search</li> | ||||
|     <li><code>@abc</code> - returns notes with label abc</li> | ||||
|     <li><code>@year=2019</code> - matches notes with label <code>year</code> having value <code>2019</code></li> | ||||
|     <li><code>@rock @pop</code> - matches notes which have both <code>rock</code> and <code>pop</code> labels</li> | ||||
|     <li><code>@rock or @pop</code> - only one of the labels must be present</li> | ||||
|     <li><code>@year<=2000</code> - numerical comparison (also >, >=, <).</li> | ||||
|     <li><code>@dateCreated>=MONTH-1</code> - notes created in the last month</li> | ||||
|     <li><code>=handler</code> - will execute script defined in <code>handler</code> relation to get results</li> | ||||
| </ul> | ||||
| </p>`; | ||||
|  | ||||
| const TPL = ` | ||||
| <style> | ||||
| .search-box { | ||||
|     display: none; | ||||
|     padding: 10px; | ||||
|     margin-top: 10px; | ||||
| } | ||||
|  | ||||
| .search-text { | ||||
|     border: 1px solid var(--main-border-color); | ||||
| } | ||||
| </style> | ||||
|  | ||||
| <div class="search-box"> | ||||
|     <div class="form-group"> | ||||
|         <div class="input-group"> | ||||
|             <input name="search-text" class="search-text form-control" | ||||
|                    placeholder="Search text, labels" autocomplete="off"> | ||||
|  | ||||
|             <div class="input-group-append"> | ||||
|                 <button class="do-search-button btn btn-sm icon-button bx bx-search" title="Search (enter)"></button> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <div style="display: flex; align-items: center; justify-content: space-evenly; flex-wrap: wrap;"> | ||||
|         <button class="save-search-button btn btn-sm" | ||||
|             title="This will create new saved search note under active note."> | ||||
|             <span class="bx bx-save"></span> Save search | ||||
|         </button> | ||||
|  | ||||
|         <button class="close-search-button btn btn-sm"> | ||||
|             <span class="bx bx-x"></span> Close search | ||||
|         </button> | ||||
|     </div> | ||||
| </div>`; | ||||
|  | ||||
| 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(""); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										68
									
								
								src/public/javascripts/widgets/search_results.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/public/javascripts/widgets/search_results.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| import BasicWidget from "./basic_widget.js"; | ||||
| import toastService from "../services/toast.js"; | ||||
| import server from "../services/server.js"; | ||||
|  | ||||
| const TPL = ` | ||||
| <style> | ||||
| .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; | ||||
| } | ||||
| </style> | ||||
|  | ||||
| <div class="search-results"> | ||||
|     <strong>Search results:</strong> | ||||
|  | ||||
|     <ul class="search-results-inner"></ul> | ||||
| </div> | ||||
| `; | ||||
|  | ||||
| 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 = $('<a>', { | ||||
|                 href: 'javascript:', | ||||
|                 text: result.title | ||||
|             }).attr('data-action', 'note').attr('data-note-path', result.path); | ||||
|  | ||||
|             const $result = $('<li>').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(); | ||||
|     } | ||||
| } | ||||
| @@ -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; | ||||
| } | ||||
|   | ||||
| @@ -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 <webview> when the window | ||||
| * is inactive. | ||||
|   | ||||
| @@ -138,55 +138,15 @@ | ||||
|     </div> | ||||
|  | ||||
|     <div style="display: flex; flex-grow: 1; flex-shrink: 1; min-height: 0;"> | ||||
|         <div id="left-pane" class="hide-in-zen-mode"> | ||||
|             <div id="global-buttons"> | ||||
|                 <a id="create-top-level-note-button" title="Create new top level note" class="icon-action bx bx-folder-plus"></a> | ||||
|  | ||||
|                 <a id="collapse-tree-button" title="Collapse note tree" data-kb-action="CollapseTree" class="icon-action bx bx-layer-minus"></a> | ||||
|  | ||||
|                 <a id="scroll-to-active-note-button" title="Scroll to active note" data-kb-action="ScrollToActiveNote" class="icon-action bx bx-crosshair"></a> | ||||
|  | ||||
|                 <a id="toggle-search-button" title="Search in notes" data-kb-action="SearchNotes" class="icon-action bx bx-search"></a> | ||||
|             </div> | ||||
|  | ||||
|             <div id="search-box"> | ||||
|                 <div class="form-group"> | ||||
|                     <div class="input-group"> | ||||
|                         <input name="search-text" id="search-text" class="form-control" | ||||
|                                placeholder="Search text, labels" autocomplete="off"> | ||||
|  | ||||
|                         <div class="input-group-append"> | ||||
|                             <button id="do-search-button" class="btn btn-sm icon-button bx bx-search" title="Search (enter)"></button> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|  | ||||
|  | ||||
|                 <div style="display: flex; align-items: center; justify-content: space-evenly; flex-wrap: wrap;"> | ||||
|                     <button id="save-search-button" class="btn btn-sm" | ||||
|                         title="This will create new saved search note under active note."> | ||||
|                         <span class="bx bx-save"></span> Save search</button> | ||||
|  | ||||
|                     <button id="close-search-button" class="btn btn-sm"><span class="bx bx-x"></span> Close search</button> | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|             <div id="search-results"> | ||||
|                 <strong>Search results:</strong> | ||||
|  | ||||
|                 <ul id="search-results-inner"></ul> | ||||
|             </div> | ||||
|  | ||||
|             <div id="tree"></div> | ||||
|  | ||||
|             <div class="dropdown-menu dropdown-menu-sm" id="context-menu-container"></div> | ||||
|         </div> | ||||
|         <div id="left-pane" class="hide-in-zen-mode"></div> | ||||
|  | ||||
|         <% include center.ejs %> | ||||
|  | ||||
|         <% include sidebar.ejs %> | ||||
|     </div> | ||||
|  | ||||
|     <div class="dropdown-menu dropdown-menu-sm" id="context-menu-container"></div> | ||||
|  | ||||
|     <% include dialogs/about.ejs %> | ||||
|     <% include dialogs/add_link.ejs %> | ||||
|     <% include dialogs/attributes.ejs %> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user