From 9e031dcd60755cf8fb2a4c17bb66f5e95b02f38e Mon Sep 17 00:00:00 2001 From: zadam Date: Sat, 11 Jan 2020 21:19:56 +0100 Subject: [PATCH] start of the refactoring to widget system --- src/public/javascripts/desktop.js | 8 +- .../javascripts/services/app_context.js | 33 ++++ .../javascripts/services/entrypoints.js | 1 - .../javascripts/services/search_notes.js | 158 +--------------- src/public/javascripts/services/tree.js | 34 +--- .../javascripts/widgets/basic_widget.js | 41 +++++ .../javascripts/widgets/global_buttons.js | 42 +++++ src/public/javascripts/widgets/note_tree.js | 65 +++++++ src/public/javascripts/widgets/search_box.js | 174 ++++++++++++++++++ .../javascripts/widgets/search_results.js | 68 +++++++ src/public/stylesheets/desktop.css | 24 --- src/public/stylesheets/style.css | 19 -- src/views/desktop.ejs | 46 +---- 13 files changed, 437 insertions(+), 276 deletions(-) create mode 100644 src/public/javascripts/services/app_context.js create mode 100644 src/public/javascripts/widgets/basic_widget.js create mode 100644 src/public/javascripts/widgets/global_buttons.js create mode 100644 src/public/javascripts/widgets/note_tree.js create mode 100644 src/public/javascripts/widgets/search_box.js create mode 100644 src/public/javascripts/widgets/search_results.js diff --git a/src/public/javascripts/desktop.js b/src/public/javascripts/desktop.js index c240275b0..b4eaee2b4 100644 --- a/src/public/javascripts/desktop.js +++ b/src/public/javascripts/desktop.js @@ -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(); diff --git a/src/public/javascripts/services/app_context.js b/src/public/javascripts/services/app_context.js new file mode 100644 index 000000000..4b0abb429 --- /dev/null +++ b/src/public/javascripts/services/app_context.js @@ -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); + } + } +} \ No newline at end of file diff --git a/src/public/javascripts/services/entrypoints.js b/src/public/javascripts/services/entrypoints.js index f37efbe37..1a2c7867c 100644 --- a/src/public/javascripts/services/entrypoints.js +++ b/src/public/javascripts/services/entrypoints.js @@ -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"); diff --git a/src/public/javascripts/services/search_notes.js b/src/public/javascripts/services/search_notes.js index a11cb29b8..1d31615be 100644 --- a/src/public/javascripts/services/search_notes.js +++ b/src/public/javascripts/services/search_notes.js @@ -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 = ` -Search tips - also see -

-

-

`; - -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 = $('', { - href: 'javascript:', - text: result.title - }).attr('data-action', 'note').attr('data-note-path', result.path); - - const $result = $('
  • ').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 diff --git a/src/public/javascripts/services/tree.js b/src/public/javascripts/services/tree.js index 6197e17b7..4c31c33ad 100644 --- a/src/public/javascripts/services/tree.js +++ b/src/public/javascripts/services/tree.js @@ -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 }; \ No newline at end of file diff --git a/src/public/javascripts/widgets/basic_widget.js b/src/public/javascripts/widgets/basic_widget.js new file mode 100644 index 000000000..cd0f2887d --- /dev/null +++ b/src/public/javascripts/widgets/basic_widget.js @@ -0,0 +1,41 @@ +class BasicWidget { + /** + * @param {AppContext} appContext + */ + constructor(appContext) { + this.appContext = appContext; + this.widgetId = `widget-${this.constructor.name}`; + } + + render() { + const $widget = $('
    ').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 = ` + + +
    + Search results: + +
      +
      +`; + +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 @@
      -
      -
      - - - - - - - -
      - - - -
      - Search results: - -
        -
        - -
        - - -
        +
        <% include center.ejs %> <% include sidebar.ejs %>
        + + <% include dialogs/about.ejs %> <% include dialogs/add_link.ejs %> <% include dialogs/attributes.ejs %>