From 1a13132a696eee38834c610a466cb54bd11920f0 Mon Sep 17 00:00:00 2001 From: zadam Date: Thu, 20 May 2021 23:13:34 +0200 Subject: [PATCH] split window WIP --- src/public/app/layouts/desktop_layout.js | 76 +++++++------- src/public/app/services/tab_context.js | 18 +++- src/public/app/services/tab_manager.js | 61 ++++++++---- .../app/widgets/containers/pane_container.js | 99 +++++++++++++------ src/public/app/widgets/note_detail.js | 4 +- src/public/app/widgets/note_title.js | 4 +- src/public/app/widgets/tab_aware_widget.js | 7 +- src/public/app/widgets/tab_caching_widget.js | 14 +-- src/public/app/widgets/tab_row.js | 8 +- 9 files changed, 186 insertions(+), 105 deletions(-) diff --git a/src/public/app/layouts/desktop_layout.js b/src/public/app/layouts/desktop_layout.js index 8d4746a5a..c65f04b76 100644 --- a/src/public/app/layouts/desktop_layout.js +++ b/src/public/app/layouts/desktop_layout.js @@ -182,46 +182,44 @@ export default class DesktopLayout { .child(new TitleBarButtonsWidget()) .css('height', '36px') ) - .child(new TabCachingWidget(() => - new PaneContainer(() => - new FlexContainer('column') - .css("flex-grow", "1") - .child(new FlexContainer('row').class('title-row') - .css('align-items: center;') - .cssBlock('.title-row > * { margin: 5px; }') - .overflowing() - .child(new NoteIconWidget()) - .child(new NoteTitleWidget()) - .child(new NotePathsWidget().hideInZenMode()) - .child(new NoteTypeWidget().hideInZenMode()) - .child(new NoteActionsWidget().hideInZenMode()) - .child(new ButtonWidget() - .icon("bx-window-open bx-rotate-90") - .title("Create new pane") - .command("openNewPane")) - ) - .child( - new CollapsibleSectionContainer() - .child(new SearchDefinitionWidget()) - .child(new NotePropertiesWidget()) - .child(new FilePropertiesWidget()) - .child(new ImagePropertiesWidget()) - .child(new PromotedAttributesWidget()) - .child(new OwnedAttributeListWidget()) - .child(new InheritedAttributesWidget()) - ) - .child(new NoteUpdateStatusWidget()) - .child( - new ScrollingContainer() - .child(new SqlTableSchemasWidget()) - .child(new NoteDetailWidget()) - .child(new NoteListWidget()) - .child(new SearchResultWidget()) - .child(new SqlResultWidget()) - ) - .child(new SimilarNotesWidget()) - .child(...this.customWidgets.get('center-pane')) + .child(new PaneContainer(() => + new FlexContainer('column') + .css("flex-grow", "1") + .child(new FlexContainer('row').class('title-row') + .css('align-items: center;') + .cssBlock('.title-row > * { margin: 5px; }') + .overflowing() + .child(new NoteIconWidget()) + .child(new NoteTitleWidget()) + .child(new NotePathsWidget().hideInZenMode()) + .child(new NoteTypeWidget().hideInZenMode()) + .child(new NoteActionsWidget().hideInZenMode()) + .child(new ButtonWidget() + .icon("bx-window-open bx-rotate-90") + .title("Create new pane") + .command("openNewPane")) ) + .child( + new CollapsibleSectionContainer() + .child(new SearchDefinitionWidget()) + .child(new NotePropertiesWidget()) + .child(new FilePropertiesWidget()) + .child(new ImagePropertiesWidget()) + .child(new PromotedAttributesWidget()) + .child(new OwnedAttributeListWidget()) + .child(new InheritedAttributesWidget()) + ) + .child(new NoteUpdateStatusWidget()) + .child( + new ScrollingContainer() + .child(new SqlTableSchemasWidget()) + .child(new NoteDetailWidget()) + .child(new NoteListWidget()) + .child(new SearchResultWidget()) + .child(new SqlResultWidget()) + ) + .child(new SimilarNotesWidget()) + .child(...this.customWidgets.get('center-pane')) ) ) ); diff --git a/src/public/app/services/tab_context.js b/src/public/app/services/tab_context.js index 6310a3fd1..fb73d3579 100644 --- a/src/public/app/services/tab_context.js +++ b/src/public/app/services/tab_context.js @@ -11,12 +11,12 @@ class TabContext extends Component { /** * @param {string|null} tabId */ - constructor(tabId = null, hoistedNoteId = 'root') { + constructor(tabId = null, hoistedNoteId = 'root', parentTabId = null) { super(); this.tabId = tabId || utils.randomString(4); this.hoistedNoteId = hoistedNoteId; - this.parentTabId = null; + this.parentTabId = parentTabId; } setEmpty() { @@ -60,6 +60,19 @@ class TabContext extends Component { } } + getAllSubTabContexts() { + return appContext.tabManager.tabContexts.filter(tc => tc.tabId === this.tabId || tc.parentTabId === this.tabId); + } + + getMainTabContext() { + if (this.parentTabId) { + return appContext.tabManager.getTabContextById(this.parentTabId); + } + else { + return this; + } + } + saveToRecentNotes(resolvedNotePath) { setTimeout(async () => { // we include the note into recent list only if the user stayed on the note at least 5 seconds @@ -135,6 +148,7 @@ class TabContext extends Component { return { tabId: this.tabId, + parentTabId: this.parentTabId, notePath: this.notePath, hoistedNoteId: this.hoistedNoteId, active: this.isActive() diff --git a/src/public/app/services/tab_manager.js b/src/public/app/services/tab_manager.js index b4116416f..c95d6b323 100644 --- a/src/public/app/services/tab_manager.js +++ b/src/public/app/services/tab_manager.js @@ -36,6 +36,11 @@ export default class TabManager extends Component { return this.children; } + /** @type {TabContext[]} */ + get mainTabContexts() { + return this.tabContexts.filter(tc => !tc.parentTabId) + } + async loadTabs() { const tabsToOpen = appContext.isMainWindow ? (options.getJson('openTabs') || []) @@ -97,7 +102,7 @@ export default class TabManager extends Component { await this.tabsUpdate.allowUpdateWithoutChange(async () => { for (const tab of filteredTabs) { - await this.openTabWithNote(tab.notePath, tab.active, tab.tabId, tab.hoistedNoteId); + await this.openTabWithNote(tab.notePath, tab.active, tab.tabId, tab.hoistedNoteId, tab.parentTabId); } }); } @@ -138,12 +143,20 @@ export default class TabManager extends Component { /** @returns {TabContext} */ getTabContextById(tabId) { - return this.tabContexts.find(tc => tc.tabId === tabId); + const tabContext = this.tabContexts.find(tc => tc.tabId === tabId); + + if (!tabContext) { + throw new Error(`Cannot find tabContext id='${tabId}'`); + } + + return tabContext; } /** @returns {TabContext} */ getActiveTabContext() { - return this.getTabContextById(this.activeTabId); + return this.activeTabId + ? this.getTabContextById(this.activeTabId) + : null; } /** @returns {string|null} */ @@ -188,8 +201,8 @@ export default class TabManager extends Component { await tabContext.setEmpty(); } - async openEmptyTab(tabId, hoistedNoteId) { - const tabContext = new TabContext(tabId, hoistedNoteId); + async openEmptyTab(tabId, hoistedNoteId, parentTabId = null) { + const tabContext = new TabContext(tabId, hoistedNoteId, parentTabId); this.child(tabContext); await this.triggerEvent('newTabOpened', {tabContext}); @@ -215,8 +228,8 @@ export default class TabManager extends Component { return this.openTabWithNote(notePath, false, null, hoistedNoteId); } - async openTabWithNote(notePath, activate, tabId, hoistedNoteId) { - const tabContext = await this.openEmptyTab(tabId, hoistedNoteId); + async openTabWithNote(notePath, activate, tabId, hoistedNoteId, parentTabId = null) { + const tabContext = await this.openEmptyTab(tabId, hoistedNoteId, parentTabId); if (notePath) { await tabContext.setNote(notePath, !activate); // if activate is false then send normal noteSwitched event @@ -267,24 +280,30 @@ export default class TabManager extends Component { } async removeTab(tabId) { - const tabContextToRemove = this.getTabContextById(tabId); + let mainTabContextToRemove = this.getTabContextById(tabId); - if (!tabContextToRemove) { + if (!mainTabContextToRemove) { return; } + if (mainTabContextToRemove.parentTabId) { + mainTabContextToRemove = this.getTabContextById(mainTabContextToRemove.parentTabId); + } + // close dangling autocompletes after closing the tab $(".aa-input").autocomplete("close"); - await this.triggerEvent('beforeTabRemove', {tabId}); + const tabIdsToRemove = mainTabContextToRemove.getAllSubTabContexts().map(tc => tc.tabId); - if (this.tabContexts.length <= 1) { - this.openAndActivateEmptyTab(); + await this.triggerEvent('beforeTabRemove', { tabIds: tabIdsToRemove }); + + if (this.mainTabContexts.length <= 1) { + await this.openAndActivateEmptyTab(); } - else if (tabContextToRemove.isActive()) { - const idx = this.tabContexts.findIndex(tc => tc.tabId === tabId); + else if (tabIdsToRemove.includes(this.activeTabId)) { + const idx = this.mainTabContexts.findIndex(tc => tc.tabId === mainTabContextToRemove.tabId); - if (idx === this.tabContexts.length - 1) { + if (idx === this.mainTabContexts.length - 1) { this.activatePreviousTabCommand(); } else { @@ -292,9 +311,9 @@ export default class TabManager extends Component { } } - this.children = this.children.filter(tc => tc.tabId !== tabId); + this.children = this.children.filter(tc => !tabIdsToRemove.includes(tc.tabId)); - this.triggerEvent('tabRemoved', {tabId}); + this.triggerEvent('tabRemoved', {tabIds: tabIdsToRemove}); this.tabsUpdate.scheduleUpdate(); } @@ -312,15 +331,15 @@ export default class TabManager extends Component { } activateNextTabCommand() { - const oldIdx = this.tabContexts.findIndex(tc => tc.tabId === this.activeTabId); - const newActiveTabId = this.tabContexts[oldIdx === this.tabContexts.length - 1 ? 0 : oldIdx + 1].tabId; + const oldIdx = this.mainTabContexts.findIndex(tc => tc.tabId === this.activeTabId); + const newActiveTabId = this.mainTabContexts[oldIdx === this.tabContexts.length - 1 ? 0 : oldIdx + 1].tabId; this.activateTab(newActiveTabId); } activatePreviousTabCommand() { - const oldIdx = this.tabContexts.findIndex(tc => tc.tabId === this.activeTabId); - const newActiveTabId = this.tabContexts[oldIdx === 0 ? this.tabContexts.length - 1 : oldIdx - 1].tabId; + const oldIdx = this.mainTabContexts.findIndex(tc => tc.tabId === this.activeTabId); + const newActiveTabId = this.mainTabContexts[oldIdx === 0 ? this.tabContexts.length - 1 : oldIdx - 1].tabId; this.activateTab(newActiveTabId); } diff --git a/src/public/app/widgets/containers/pane_container.js b/src/public/app/widgets/containers/pane_container.js index f2ee3d037..ea2828e1e 100644 --- a/src/public/app/widgets/containers/pane_container.js +++ b/src/public/app/widgets/containers/pane_container.js @@ -1,6 +1,5 @@ import FlexContainer from "./flex_container.js"; import appContext from "../../services/app_context.js"; -import TabContext from "../../services/tab_context.js"; export default class PaneContainer extends FlexContainer { constructor(widgetFactory) { @@ -9,48 +8,90 @@ export default class PaneContainer extends FlexContainer { this.counter = 0; this.widgetFactory = widgetFactory; + this.widgets = {}; - this.child(this.widgetFactory()); - - this.id('pane-container-widget'); + this.class('pane-container-widget'); this.css('flex-grow', '1'); } - doRender() { - super.doRender(); - - this.$widget.find("div").on("click", () => { - const activeTabContext = appContext.tabManager.getActiveTabContext(); - - const tabId = activeTabContext.parentTabId || activeTabContext.tabId; - - appContext.tabManager.activateTab(tabId); - }); + setTabContextEvent({tabContext}) { + /** @var {TabContext} */ + this.tabContext = tabContext; } - async openNewPaneCommand() { - const newWidget = this.widgetFactory(); + async newTabOpenedEvent({tabContext}) { + const widget = this.widgetFactory(); - const $rendered = newWidget.render(); + const $renderedWidget = widget.render(); - this.$widget.append($rendered); - - const tabContext = new TabContext(); - appContext.tabManager.tabContexts.push(tabContext); - appContext.tabManager.child(tabContext); - - $rendered.on('click', () => { + $renderedWidget.on('click', () => { appContext.tabManager.activateTab(tabContext.tabId); }); - tabContext.parentTabId = appContext.tabManager.getActiveTabContext().tabId; + let $parent; - await newWidget.handleEvent('setTabContext', { tabContext }); + if (!tabContext.parentTabId) { + $parent = $("
") + .attr("data-main-tab-id", tabContext.tabId) + .css("display", "flex") + .css("flex-grow", "1"); - this.child(newWidget); + this.$widget.append($parent); + } + else { + $parent = this.$widget.find(`[data-main-tab-id="${tabContext.parentTabId}"]`); + } - tabContext.setEmpty(); + $parent.append($renderedWidget); - appContext.tabManager.activateTab(tabContext.tabId); + this.widgets[tabContext.tabId] = widget; + + await widget.handleEvent('setTabContext', { tabContext }); + + this.child(widget); + } + + async openNewPaneCommand() { + const tabContext = await appContext.tabManager.openEmptyTab(null, null, appContext.tabManager.getActiveTabContext().tabId); + + await appContext.tabManager.activateTab(tabContext.tabId); + + await tabContext.setEmpty(); + } + + /** + * widget.hasBeenAlreadyShown is intended for lazy loading of cached tabs - initial note switches of new tabs + * are not executed, we're waiting for the first tab activation and then we update the tab. After this initial + * activation further note switches are always propagated to the tabs. + */ + handleEventInChildren(name, data) { + if (['tabNoteSwitched', 'tabNoteSwitchedAndActivated'].includes(name)) { + // this event is propagated only to the widgets of a particular tab + const widget = this.widgets[data.tabContext.tabId]; + + if (widget && (widget.hasBeenAlreadyShown || name === 'tabNoteSwitchedAndActivated')) { + widget.hasBeenAlreadyShown = true; + + return widget.handleEvent('tabNoteSwitched', data); + } + else { + return Promise.resolve(); + } + } + + if (name === 'activeTabChanged') { + const widget = this.widgets[data.tabContext.tabId]; + + if (widget.hasBeenAlreadyShown) { + return Promise.resolve(); + } + else { + widget.hasBeenAlreadyShown = true; + + return widget.handleEvent(name, data); + } + } else { + return super.handleEventInChildren(name, data); + } } } diff --git a/src/public/app/widgets/note_detail.js b/src/public/app/widgets/note_detail.js index 958e24ded..4e4219035 100644 --- a/src/public/app/widgets/note_detail.js +++ b/src/public/app/widgets/note_detail.js @@ -202,8 +202,8 @@ export default class NoteDetailWidget extends TabAwareWidget { } } - async beforeTabRemoveEvent({tabId}) { - if (this.isTab(tabId)) { + async beforeTabRemoveEvent({tabIds}) { + if (this.isTab(tabIds)) { await this.spacedUpdate.updateNowIfNecessary(); } } diff --git a/src/public/app/widgets/note_title.js b/src/public/app/widgets/note_title.js index e630deefc..3650bda92 100644 --- a/src/public/app/widgets/note_title.js +++ b/src/public/app/widgets/note_title.js @@ -72,8 +72,8 @@ export default class NoteTitleWidget extends TabAwareWidget { } } - async beforeTabRemoveEvent({tabId}) { - if (this.isTab(tabId)) { + async beforeTabRemoveEvent({tabIds}) { + if (this.isTab(tabIds)) { await this.spacedUpdate.updateNowIfNecessary(); } } diff --git a/src/public/app/widgets/tab_aware_widget.js b/src/public/app/widgets/tab_aware_widget.js index b54c68df6..d5ecdd5cf 100644 --- a/src/public/app/widgets/tab_aware_widget.js +++ b/src/public/app/widgets/tab_aware_widget.js @@ -3,7 +3,12 @@ import appContext from "../services/app_context.js"; export default class TabAwareWidget extends BasicWidget { isTab(tabId) { - return this.tabContext && this.tabContext.tabId === tabId; + if (Array.isArray(tabId)) { + return this.tabContext && tabId.includes(this.tabContext.tabId); + } + else { + return this.tabContext && this.tabContext.tabId === tabId; + } } isTabOrParent(tabId) { diff --git a/src/public/app/widgets/tab_caching_widget.js b/src/public/app/widgets/tab_caching_widget.js index c8e59a694..5438dd8a3 100644 --- a/src/public/app/widgets/tab_caching_widget.js +++ b/src/public/app/widgets/tab_caching_widget.js @@ -34,14 +34,16 @@ export default class TabCachingWidget extends TabAwareWidget { this.child(this.widgets[tabId]); // add as child only once it is ready (rendered with tabContext) } - tabRemovedEvent({tabId}) { - const widget = this.widgets[tabId]; + tabRemovedEvent({tabIds}) { + for (const tabId of tabIds) { + const widget = this.widgets[tabId]; - if (widget) { - widget.remove(); - delete this.widgets[tabId]; + if (widget) { + widget.remove(); + delete this.widgets[tabId]; - this.children = this.children.filter(ch => ch !== widget); + this.children = this.children.filter(ch => ch !== widget); + } } } diff --git a/src/public/app/widgets/tab_row.js b/src/public/app/widgets/tab_row.js index dfb008690..10a73c05c 100644 --- a/src/public/app/widgets/tab_row.js +++ b/src/public/app/widgets/tab_row.js @@ -445,7 +445,7 @@ export default class TabRowWidget extends BasicWidget { } newTabOpenedEvent({tabContext}) { - if (!tabContext.parentId) { + if (!tabContext.parentTabId) { this.addTab(tabContext.tabId); } } @@ -478,8 +478,10 @@ export default class TabRowWidget extends BasicWidget { return $tab.attr('data-tab-id'); } - tabRemovedEvent({tabId}) { - this.removeTab(tabId); + tabRemovedEvent({tabIds}) { + for (const tabId of tabIds) { + this.removeTab(tabId); + } } cleanUpPreviouslyDraggedTabs() {