diff --git a/src/public/javascripts/services/app_context.js b/src/public/javascripts/services/app_context.js index 53e366076..fd1dd3e6b 100644 --- a/src/public/javascripts/services/app_context.js +++ b/src/public/javascripts/services/app_context.js @@ -20,8 +20,10 @@ import WhatLinksHereWidget from "../widgets/what_links_here.js"; import AttributesWidget from "../widgets/attributes.js"; import TitleBarButtonsWidget from "../widgets/title_bar_buttons.js"; import GlobalMenuWidget from "../widgets/global_menu.js"; -import HorizontalFlexContainer from "../widgets/horizontal_flex_container.js"; +import RowFlexContainer from "../widgets/row_flex_container.js"; import StandardTopWidget from "../widgets/standard_top_widget.js"; +import treeCache from "./tree_cache.js"; +import treeUtils from "./tree_utils.js"; class AppContext { constructor() { @@ -38,7 +40,7 @@ class AppContext { this.tabRow = new TabRowWidget(this); const topPaneWidgets = [ - new HorizontalFlexContainer(this, [ + new RowFlexContainer(this, [ new GlobalMenuWidget(this), this.tabRow, new TitleBarButtonsWidget(this) @@ -105,6 +107,10 @@ class AppContext { trigger(name, data) { this.eventReceived(name, data); + for (const tabContext of this.tabContexts) { + tabContext.eventReceived(name, data); + } + for (const widget of this.widgets) { widget.eventReceived(name, data); } @@ -118,6 +124,36 @@ class AppContext { } } + activateNote(notePath) { + const activeTabContext = this.getActiveTabContext(); + + activeTabContext.setNote(notePath); + + this._setTitleBar(); + this._setCurrentNotePathToHash(); + } + + _setCurrentNotePathToHash() { + const activeTabContext = this.getActiveTabContext(); + + if (activeTabContext && activeTabContext.notePath) { + document.location.hash = (activeTabContext.notePath || "") + "-" + activeTabContext.tabId; + } + } + + async _setTitleBar() { + document.title = "Trilium Notes"; + + const activeTabContext = this.getActiveTabContext(); + + if (activeTabContext && activeTabContext.notePath) { + const note = await treeCache.getNote(treeUtils.getNoteIdFromNotePath(activeTabContext.notePath)); + + // it helps navigating in history if note title is included in the title + document.title += " - " + note.title; + } + } + /** @return {TabContext[]} */ getTabContexts() { return this.tabContexts; @@ -219,7 +255,7 @@ class AppContext { getTab(newTab, state) { if (!this.getActiveTabContext() || newTab) { // if it's a new tab explicitly by user then it's in background - const ctx = new TabContext(this.tabRow, state); + const ctx = new TabContext(this, this.tabRow, state); this.tabContexts.push(ctx); return ctx; @@ -249,7 +285,7 @@ class AppContext { } async openEmptyTab() { - const ctx = new TabContext(this.tabRow); + const ctx = new TabContext(this, this.tabRow); this.tabContexts.push(ctx); await this.tabRow.activateTab(ctx.$tab[0]); diff --git a/src/public/javascripts/services/attributes.js b/src/public/javascripts/services/attributes.js index 805093ce7..156445f5e 100644 --- a/src/public/javascripts/services/attributes.js +++ b/src/public/javascripts/services/attributes.js @@ -2,13 +2,17 @@ import server from "./server.js"; import ws from "./ws.js"; import treeUtils from "./tree_utils.js"; import noteAutocompleteService from "./note_autocomplete.js"; +import Component from "../widgets/component.js"; +import utils from "./utils.js"; -class Attributes { +class Attributes extends Component { /** - * @param {TabContext} ctx + * @param {AppContext} appContext + * @param {TabContext} tabContext */ - constructor(ctx) { - this.ctx = ctx; + constructor(appContext, tabContext) { + super(appContext); + this.tabContext = tabContext; this.attributePromise = null; } @@ -17,7 +21,7 @@ class Attributes { } reloadAttributes() { - this.attributePromise = server.get(`notes/${this.ctx.note.noteId}/attributes`); + this.attributePromise = server.get(`notes/${this.tabContext.note.noteId}/attributes`); } async refreshAttributes() { @@ -32,15 +36,18 @@ class Attributes { return this.attributePromise; } - eventReceived(name, data) { - if (!this.ctx.note) { - return; + syncDataListener({data}) { + if (this.tabContext.note && data.find(sd => sd.entityName === 'attributes' && sd.noteId === this.tabContext.note.noteId)) { + this.reloadAttributes(); } + } - if (name === 'syncData') { - if (data.find(sd => sd.entityName === 'attributes' && sd.noteId === this.ctx.note.noteId)) { - this.reloadAttributes(); - } + activeNoteChangedListener() { + if (utils.isDesktop()) { + this.attributes.refreshAttributes(); + } else { + // mobile usually doesn't need attributes so we just invalidate + this.attributes.invalidateAttributes(); } } } diff --git a/src/public/javascripts/services/note_detail.js b/src/public/javascripts/services/note_detail.js index 05a7ed247..77b2f6ad2 100644 --- a/src/public/javascripts/services/note_detail.js +++ b/src/public/javascripts/services/note_detail.js @@ -20,9 +20,7 @@ async function reload() { } async function reloadNote(tabContext) { - const note = await loadNote(tabContext.note.noteId); - - await loadNoteDetailToContext(tabContext, note, tabContext.notePath); + await loadNoteDetailToContext(tabContext, tabContext.notePath); } async function openInTab(notePath, activate) { @@ -79,11 +77,10 @@ async function activateOrOpenNote(noteId) { /** * @param {TabContext} ctx - * @param {NoteFull} note * @param {string} notePath */ -async function loadNoteDetailToContext(ctx, note, notePath) { - await ctx.setNote(note, notePath); +async function loadNoteDetailToContext(ctx, notePath) { + await ctx.setNote(notePath); appContext.openTabsChanged(); @@ -104,7 +101,6 @@ async function loadNoteDetail(origNotePath, options = {}) { } const noteId = treeUtils.getNoteIdFromNotePath(notePath); - const loadedNote = await loadNote(noteId); const ctx = appContext.getTab(newTab, options.state); // we will try to render the new note only if it's still the active one in the tree @@ -112,11 +108,11 @@ async function loadNoteDetail(origNotePath, options = {}) { // try to render all those loaded notes one after each other. This only guarantees that correct note // will be displayed independent of timing const currentTreeNode = appContext.getMainNoteTree().getActiveNode(); - if (!newTab && currentTreeNode && currentTreeNode.data.noteId !== loadedNote.noteId) { + if (!newTab && currentTreeNode && currentTreeNode.data.noteId !== noteId) { return; } - const loadPromise = loadNoteDetailToContext(ctx, loadedNote, notePath).then(() => { + const loadPromise = loadNoteDetailToContext(ctx, notePath).then(() => { if (activate) { return appContext.activateTab(ctx); } @@ -201,9 +197,7 @@ ws.subscribeToOutsideSyncMessages(syncData => { }); ws.subscribeToAllSyncMessages(syncData => { - for (const tc of appContext.getTabContexts()) { - tc.eventReceived('syncData', syncData); - } + appContext.trigger('syncData', {data: syncData}); }); $tabContentsContainer.on("dragover", e => e.preventDefault()); diff --git a/src/public/javascripts/services/note_tooltip.js b/src/public/javascripts/services/note_tooltip.js index 9edb33b44..f998c5f02 100644 --- a/src/public/javascripts/services/note_tooltip.js +++ b/src/public/javascripts/services/note_tooltip.js @@ -43,7 +43,7 @@ async function mouseEnterHandler() { const noteId = treeUtils.getNoteIdFromNotePath(notePath); const notePromise = noteDetailService.loadNote(noteId); - const attributePromise = server.get('notes/' + noteId + '/attributes'); + const attributePromise = server.get(`notes/${noteId}/attributes`); const [note, attributes] = await Promise.all([notePromise, attributePromise]); diff --git a/src/public/javascripts/services/tab_context.js b/src/public/javascripts/services/tab_context.js index d67a83160..1280f0a0b 100644 --- a/src/public/javascripts/services/tab_context.js +++ b/src/public/javascripts/services/tab_context.js @@ -6,6 +6,9 @@ import Attributes from "./attributes.js"; import utils from "./utils.js"; import optionsService from "./options.js"; import appContext from "./app_context.js"; +import treeUtils from "./tree_utils.js"; +import noteDetailService from "./note_detail.js"; +import Component from "../widgets/component.js"; let showSidebarInNewTab = true; @@ -13,12 +16,15 @@ optionsService.addLoadListener(options => { showSidebarInNewTab = options.is('showSidebarInNewTab'); }); -class TabContext { +class TabContext extends Component { /** + * @param {AppContext} appContext * @param {TabRowWidget} tabRow * @param {object} state */ - constructor(tabRow, state = {}) { + constructor(appContext, tabRow, state = {}) { + super(appContext); + this.tabRow = tabRow; this.tabId = state.tabId || utils.randomString(4); this.$tab = $(this.tabRow.addTab(this.tabId)); @@ -37,40 +43,26 @@ class TabContext { this.noteChangeDisabled = false; this.isNoteChanged = false; - this.attributes = new Attributes(this); + this.attributes = new Attributes(this.appContext, this); + + this.children.push(this.attributes); } - async setNote(note, notePath) { - /** @property {NoteFull} */ - this.note = note; + async setNote(notePath) { this.notePath = notePath; + const noteId = treeUtils.getNoteIdFromNotePath(notePath); + + /** @property {NoteFull} */ + this.note = await noteDetailService.loadNote(noteId); + this.tabRow.updateTab(this.$tab[0], {title: this.note.title}); if (!this.initialized) { return; } - if (utils.isDesktop()) { - this.attributes.refreshAttributes(); - } else { - // mobile usually doesn't need attributes so we just invalidate - this.attributes.invalidateAttributes(); - } - this.setupClasses(); - this.setCurrentNotePathToHash(); - - this.noteChangeDisabled = true; - - try { - - } finally { - this.noteChangeDisabled = false; - } - - this.setTitleBar(); - this.cleanup(); // esp. on windows autocomplete is not getting closed automatically setTimeout(async () => { @@ -96,8 +88,8 @@ class TabContext { if (!this.initialized) { await this.initTabContent(); - if (this.note) { - await this.setNote(this.note, this.notePath); + if (this.notePath) { + await this.setNote(this.notePath); } else { // FIXME @@ -106,38 +98,18 @@ class TabContext { } this.setCurrentNotePathToHash(); - this.setTitleBar(); } async renderComponent(disableAutoBook = false) { // FIXME } - setTitleBar() { - if (!this.$tabContent.is(":visible")) { - return; - } - - document.title = "Trilium Notes"; - - if (this.note) { - // it helps navigating in history if note title is included in the title - document.title += " - " + this.note.title; - } - } - hide() { if (this.initialized) { this.$tabContent.hide(); } } - setCurrentNotePathToHash() { - if (this.isActive()) { - document.location.hash = (this.notePath || "") + "-" + this.tabId; - } - } - isActive() { return this.$tab[0] === this.tabRow.activeTabEl; } @@ -149,21 +121,9 @@ class TabContext { } } - for (const clazz of Array.from(this.$tabContent[0].classList)) { // create copy to safely iterate over while removing classes - if (clazz !== 'note-tab-content') { - this.$tabContent.removeClass(clazz); - } - } - this.$tab.addClass(this.note.cssClass); this.$tab.addClass(utils.getNoteTypeClass(this.note.type)); this.$tab.addClass(utils.getMimeTypeClass(this.note.mime)); - - this.$tabContent.addClass(this.note.cssClass); - this.$tabContent.addClass(utils.getNoteTypeClass(this.note.type)); - this.$tabContent.addClass(utils.getMimeTypeClass(this.note.mime)); - - this.$tabContent.toggleClass("protected", this.note.isProtected); } getComponent() { @@ -247,14 +207,6 @@ class TabContext { } } - eventReceived(name, data) { - if (!this.initialized) { - return; - } - - this.attributes.eventReceived(name, data); - } - getTabState() { if (!this.notePath) { return null; diff --git a/src/public/javascripts/services/tree_cache.js b/src/public/javascripts/services/tree_cache.js index 9d6257cf8..0b7eef957 100644 --- a/src/public/javascripts/services/tree_cache.js +++ b/src/public/javascripts/services/tree_cache.js @@ -172,6 +172,10 @@ class TreeCache { return (await this.getNotes([noteId], silentNotFoundError))[0]; } + getNoteFromCache(noteId) { + return this.notes[noteId]; + } + getBranches(branchIds) { return branchIds .map(branchId => this.getBranch(branchId)) diff --git a/src/public/javascripts/widgets/basic_widget.js b/src/public/javascripts/widgets/basic_widget.js index fd94d9cf2..b62bf9112 100644 --- a/src/public/javascripts/widgets/basic_widget.js +++ b/src/public/javascripts/widgets/basic_widget.js @@ -1,9 +1,8 @@ -class BasicWidget { - /** - * @param {AppContext} appContext - */ +import Component from "./component.js"; + +class BasicWidget extends Component { constructor(appContext) { - this.appContext = appContext; + super(appContext); this.widgetId = `widget-${this.constructor.name}`; } @@ -25,20 +24,6 @@ class BasicWidget { */ doRender() {} - eventReceived(name, data) { - console.log("received", name, "to", this.widgetId); - - const fun = this[name + 'Listener']; - - if (typeof fun === 'function') { - fun.call(this, data); - } - } - - trigger(name, data) { - this.appContext.trigger(name, data); - } - toggle(show) { this.$widget.toggle(show); } diff --git a/src/public/javascripts/widgets/component.js b/src/public/javascripts/widgets/component.js new file mode 100644 index 000000000..312033740 --- /dev/null +++ b/src/public/javascripts/widgets/component.js @@ -0,0 +1,24 @@ +export default class Component { + /** @param {AppContext} appContext */ + constructor(appContext) { + this.appContext = appContext; + /** @type Component[] */ + this.children = []; + } + + eventReceived(name, data) { + const fun = this[name + 'Listener']; + + if (typeof fun === 'function') { + fun.call(this, data); + } + + for (const child of this.children) { + child.eventReceived(name, data); + } + } + + trigger(name, data) { + this.appContext.trigger(name, data); + } +} \ No newline at end of file diff --git a/src/public/javascripts/widgets/note_detail.js b/src/public/javascripts/widgets/note_detail.js index a63a8cd56..d13fb4b84 100644 --- a/src/public/javascripts/widgets/note_detail.js +++ b/src/public/javascripts/widgets/note_detail.js @@ -58,6 +58,24 @@ export default class NoteDetailWidget extends TabAwareWidget { this.getComponent().show(); await this.getComponent().render(); + + this.setupClasses(); + } + + setupClasses() { + const note = this.tabContext.note; + + for (const clazz of Array.from(this.$widget[0].classList)) { // create copy to safely iterate over while removing classes + if (clazz !== 'note-detail') { + this.$widget.removeClass(clazz); + } + } + + this.$widget.addClass(note.cssClass); + this.$widget.addClass(utils.getNoteTypeClass(note.type)); + this.$widget.addClass(utils.getMimeTypeClass(note.mime)); + + this.$widget.toggleClass("protected", note.isProtected); } getComponent() { diff --git a/src/public/javascripts/widgets/note_tree.js b/src/public/javascripts/widgets/note_tree.js index a32e8c190..b0d5c4dce 100644 --- a/src/public/javascripts/widgets/note_tree.js +++ b/src/public/javascripts/widgets/note_tree.js @@ -101,7 +101,7 @@ export default class NoteTreeWidget extends BasicWidget { const notePath = await treeUtils.getNotePath(data.node); - noteDetailService.switchToNote(notePath); + this.appContext.activateNote(notePath); }, expand: (event, data) => treeService.setExpandedToServer(data.node.data.branchId, true), collapse: (event, data) => treeService.setExpandedToServer(data.node.data.branchId, false), diff --git a/src/public/javascripts/widgets/horizontal_flex_container.js b/src/public/javascripts/widgets/row_flex_container.js similarity index 85% rename from src/public/javascripts/widgets/horizontal_flex_container.js rename to src/public/javascripts/widgets/row_flex_container.js index 3914a9a61..bae3b0eb6 100644 --- a/src/public/javascripts/widgets/horizontal_flex_container.js +++ b/src/public/javascripts/widgets/row_flex_container.js @@ -1,6 +1,6 @@ import BasicWidget from "./basic_widget.js"; -export default class HorizontalFlexContainer extends BasicWidget { +export default class RowFlexContainer extends BasicWidget { constructor(appContext, widgets) { super(appContext); diff --git a/src/views/desktop.ejs b/src/views/desktop.ejs index d8b11b7b1..73208d541 100644 --- a/src/views/desktop.ejs +++ b/src/views/desktop.ejs @@ -11,7 +11,7 @@