diff --git a/src/public/javascripts/desktop.js b/src/public/javascripts/desktop.js index 23640906f..954388f33 100644 --- a/src/public/javascripts/desktop.js +++ b/src/public/javascripts/desktop.js @@ -56,7 +56,7 @@ window.glob.loadIncludedNote = async (noteId, el) => { }; // this is required by CKEditor when uploading images window.glob.noteChanged = () => { - const activeTabContext = appContext.getActiveTabContext(); + const activeTabContext = appContext.tabManager.getActiveTabContext(); if (activeTabContext) { activeTabContext.noteChanged(); @@ -65,7 +65,7 @@ window.glob.noteChanged = () => { window.glob.refreshTree = treeService.reload; // required for ESLint plugin -window.glob.getActiveTabNote = () => appContext.getActiveTabNote(); +window.glob.getActiveTabNote = () => appContext.tabManager.getActiveTabNote(); window.glob.requireLibrary = libraryLoader.requireLibrary; window.glob.ESLINT = libraryLoader.ESLINT; diff --git a/src/public/javascripts/dialogs/attributes.js b/src/public/javascripts/dialogs/attributes.js index 4c80537ae..f9b5d9680 100644 --- a/src/public/javascripts/dialogs/attributes.js +++ b/src/public/javascripts/dialogs/attributes.js @@ -92,7 +92,7 @@ function AttributesModel() { } this.loadAttributes = async function() { - const noteId = appContext.getActiveTabNoteId(); + const noteId = appContext.tabManager.getActiveTabNoteId(); const attributes = await server.get('notes/' + noteId + '/attributes'); @@ -138,7 +138,7 @@ function AttributesModel() { self.updateAttributePositions(); - const noteId = appContext.getActiveTabNoteId(); + const noteId = appContext.tabManager.getActiveTabNoteId(); const attributesToSave = self.ownedAttributes() .map(attribute => attribute()) diff --git a/src/public/javascripts/dialogs/jump_to_note.js b/src/public/javascripts/dialogs/jump_to_note.js index 4bbf329c5..90b3e330f 100644 --- a/src/public/javascripts/dialogs/jump_to_note.js +++ b/src/public/javascripts/dialogs/jump_to_note.js @@ -22,7 +22,7 @@ export async function showDialog() { return false; } - appContext.getActiveTabContext().setNote(suggestion.path); + appContext.tabManager.getActiveTabContext().setNote(suggestion.path); }); noteAutocompleteService.showRecentNotes($autoComplete); diff --git a/src/public/javascripts/dialogs/link_map.js b/src/public/javascripts/dialogs/link_map.js index 3f99441d9..a3ebd4889 100644 --- a/src/public/javascripts/dialogs/link_map.js +++ b/src/public/javascripts/dialogs/link_map.js @@ -31,7 +31,7 @@ export async function showDialog() { } $dialog.on('shown.bs.modal', () => { - const note = appContext.getActiveTabNote(); + const note = appContext.tabManager.getActiveTabNote(); linkMapService = new LinkMapService(note, $linkMapContainer, getOptions()); linkMapService.render(); diff --git a/src/public/javascripts/dialogs/markdown_import.js b/src/public/javascripts/dialogs/markdown_import.js index ab58f930e..0e0c51de2 100644 --- a/src/public/javascripts/dialogs/markdown_import.js +++ b/src/public/javascripts/dialogs/markdown_import.js @@ -29,7 +29,7 @@ async function convertMarkdownToHtml(text) { } export async function importMarkdownInline() { - if (appContext.getActiveTabNoteType() !== 'text') { + if (appContext.tabManager.getActiveTabNoteType() !== 'text') { return; } diff --git a/src/public/javascripts/dialogs/note_info.js b/src/public/javascripts/dialogs/note_info.js index 7959d1f44..228d8094c 100644 --- a/src/public/javascripts/dialogs/note_info.js +++ b/src/public/javascripts/dialogs/note_info.js @@ -16,7 +16,7 @@ export async function showDialog() { $dialog.modal(); - const activeTabContext = appContext.getActiveTabContext(); + const activeTabContext = appContext.tabManager.getActiveTabContext(); const {note} = activeTabContext; const noteComplement = await activeTabContext.getNoteComplement(); diff --git a/src/public/javascripts/dialogs/note_revisions.js b/src/public/javascripts/dialogs/note_revisions.js index 4799a0374..a9d2afb81 100644 --- a/src/public/javascripts/dialogs/note_revisions.js +++ b/src/public/javascripts/dialogs/note_revisions.js @@ -25,7 +25,7 @@ let note; let noteRevisionId; export async function showCurrentNoteRevisions() { - await showNoteRevisionsDialog(appContext.getActiveTabNoteId()); + await showNoteRevisionsDialog(appContext.tabManager.getActiveTabNoteId()); } export async function showNoteRevisionsDialog(noteId, noteRevisionId) { @@ -42,7 +42,7 @@ async function loadNoteRevisions(noteId, noteRevId) { $list.empty(); $content.empty(); - note = appContext.getActiveTabNote(); + note = appContext.tabManager.getActiveTabNote(); revisionItems = await server.get(`notes/${noteId}/revisions`); for (const item of revisionItems) { diff --git a/src/public/javascripts/dialogs/note_source.js b/src/public/javascripts/dialogs/note_source.js index 1d2f90306..b881703ea 100644 --- a/src/public/javascripts/dialogs/note_source.js +++ b/src/public/javascripts/dialogs/note_source.js @@ -11,7 +11,7 @@ export function showDialog() { $dialog.modal(); - const noteText = appContext.getActiveTabNote().content; + const noteText = appContext.tabManager.getActiveTabNote().content; $noteSource.text(formatHtml(noteText)); } diff --git a/src/public/javascripts/dialogs/recent_changes.js b/src/public/javascripts/dialogs/recent_changes.js index 6f230c8d4..2b789545e 100644 --- a/src/public/javascripts/dialogs/recent_changes.js +++ b/src/public/javascripts/dialogs/recent_changes.js @@ -55,7 +55,7 @@ export async function showDialog() { await treeCache.reloadNotes([change.noteId]); - appContext.getActiveTabContext().setNote(change.noteId); + appContext.tabManager.getActiveTabContext().setNote(change.noteId); } }); diff --git a/src/public/javascripts/services/app_context.js b/src/public/javascripts/services/app_context.js index 0bbdbd695..5cb52bf2b 100644 --- a/src/public/javascripts/services/app_context.js +++ b/src/public/javascripts/services/app_context.js @@ -1,4 +1,3 @@ -import TabContext from "./tab_context.js"; import server from "./server.js"; import treeCache from "./tree_cache.js"; import bundleService from "./bundle.js"; @@ -6,28 +5,15 @@ import DialogEventComponent from "./dialog_events.js"; import Entrypoints from "./entrypoints.js"; import options from "./options.js"; import utils from "./utils.js"; -import treeService from "./tree.js"; import ZoomService from "./zoom.js"; import Layout from "../widgets/layout.js"; -import SpacedUpdate from "./spaced_update.js"; +import TabManager from "./tab_manager.js"; class AppContext { constructor(layout) { this.layout = layout; - this.components = []; - /** @type {TabContext[]} */ - this.tabContexts = []; - this.activeTabId = null; - - this.tabsUpdate = new SpacedUpdate(async () => { - const openTabs = this.tabContexts - .map(tc => tc.getTabState()) - .filter(t => !!t); - - await server.put('options', { - openTabs: JSON.stringify(openTabs) - }); - }); + this.tabManager = new TabManager(this); + this.components = [this.tabManager]; } async start() { @@ -35,80 +21,11 @@ class AppContext { this.showWidgets(); - this.loadTabs(); + this.tabManager.loadTabs(); bundleService.executeStartupBundles(); } - async loadTabs() { - const openTabs = options.getJson('openTabs') || []; - - await treeCache.initializedPromise; - - // if there's notePath in the URL, make sure it's open and active - // (useful, among others, for opening clipped notes from clipper) - if (window.location.hash) { - const notePath = window.location.hash.substr(1); - const noteId = treeService.getNoteIdFromNotePath(notePath); - - if (noteId && await treeCache.noteExists(noteId)) { - for (const tab of openTabs) { - tab.active = false; - } - - const foundTab = openTabs.find(tab => noteId === treeService.getNoteIdFromNotePath(tab.notePath)); - - if (foundTab) { - foundTab.active = true; - } - else { - openTabs.push({ - notePath: notePath, - active: true - }); - } - } - } - - let filteredTabs = []; - - for (const openTab of openTabs) { - const noteId = treeService.getNoteIdFromNotePath(openTab.notePath); - - if (await treeCache.noteExists(noteId)) { - // note doesn't exist so don't try to open tab for it - filteredTabs.push(openTab); - } - } - - if (utils.isMobile()) { - // mobile frontend doesn't have tabs so show only the active tab - filteredTabs = filteredTabs.filter(tab => tab.active); - } - - if (filteredTabs.length === 0) { - filteredTabs.push({ - notePath: 'root', - active: true - }); - } - - if (!filteredTabs.find(tab => tab.active)) { - filteredTabs[0].active = true; - } - - this.tabsUpdate.allowUpdateWithoutChange(() => { - for (const tab of filteredTabs) { - const tabContext = this.openEmptyTab(); - tabContext.setNote(tab.notePath); - - if (tab.active) { - this.activateTab(tabContext.tabId); - } - } - }); - } - showWidgets() { const rootContainer = this.layout.getRootWidget(this); @@ -145,210 +62,20 @@ class AppContext { } } - tabNoteSwitchedListener({tabId}) { - if (tabId === this.activeTabId) { - this._setCurrentNotePathToHash(); - } - } - - _setCurrentNotePathToHash() { - const activeTabContext = this.getActiveTabContext(); - - if (activeTabContext && activeTabContext.notePath) { - document.location.hash = (activeTabContext.notePath || "") + "-" + activeTabContext.tabId; - } - } - - /** @return {TabContext[]} */ - getTabContexts() { - return this.tabContexts; - } - - /** @returns {TabContext} */ - getTabContextById(tabId) { - return this.tabContexts.find(tc => tc.tabId === tabId); - } - - /** @returns {TabContext} */ - getActiveTabContext() { - return this.getTabContextById(this.activeTabId); - } - - /** @returns {string|null} */ - getActiveTabNotePath() { - const activeContext = this.getActiveTabContext(); - return activeContext ? activeContext.notePath : null; - } - - /** @return {NoteShort} */ - getActiveTabNote() { - const activeContext = this.getActiveTabContext(); - return activeContext ? activeContext.note : null; - } - - /** @return {string|null} */ - getActiveTabNoteId() { - const activeNote = this.getActiveTabNote(); - - return activeNote ? activeNote.noteId : null; - } - - /** @return {string|null} */ - getActiveTabNoteType() { - const activeNote = this.getActiveTabNote(); - - return activeNote ? activeNote.type : null; - } - - async switchToTab(tabId, notePath) { - const tabContext = this.tabContexts.find(tc => tc.tabId === tabId) - || this.openEmptyTab(); - - this.activateTab(tabContext.tabId); - await tabContext.setNote(notePath); - } - - 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, state); - this.tabContexts.push(ctx); - this.components.push(ctx); - - return ctx; - } else { - return this.getActiveTabContext(); - } - } - - async openAndActivateEmptyTab() { - const tabContext = this.openEmptyTab(); - - await this.activateTab(tabContext.tabId); - } - - openEmptyTab() { - const tabContext = new TabContext(this); - this.tabContexts.push(tabContext); - this.components.push(tabContext); - return tabContext; - } - - async activateOrOpenNote(noteId) { - for (const tabContext of this.getTabContexts()) { - if (tabContext.note && tabContext.note.noteId === noteId) { - await tabContext.activate(); - return; - } - } - - // if no tab with this note has been found we'll create new tab - - const tabContext = this.openEmptyTab(); - await tabContext.setNote(noteId); - } - hoistedNoteChangedListener({hoistedNoteId}) { if (hoistedNoteId === 'root') { return; } - for (const tc of this.tabContexts) { + for (const tc of this.tabManager.getTabContexts()) { if (tc.notePath && !tc.notePath.split("/").includes(hoistedNoteId)) { - this.removeTab(tc.tabId); + this.tabManager.removeTab(tc.tabId); } } - if (this.tabContexts.length === 0) { - this.openAndActivateEmptyTab(); + if (this.tabManager.getTabContexts().length === 0) { + this.tabManager.openAndActivateEmptyTab(); } - - this.saveOpenTabs(); - } - - openTabsChangedListener() { - this.tabsUpdate.scheduleUpdate(); - } - - activateTab(tabId) { - if (tabId === this.activeTabId) { - return; - } - - const oldActiveTabId = this.activeTabId; - - this.activeTabId = tabId; - - this.trigger('activeTabChanged', { oldActiveTabId, newActiveTabId: tabId }); - } - - newTabListener() { - this.openAndActivateEmptyTab(); - } - - async removeTab(tabId) { - const tabContextToRemove = this.tabContexts.find(tc => tc.tabId === tabId); - - if (!tabContextToRemove) { - return; - } - - await this.trigger('beforeTabRemove', {tabId}, true); - - if (this.tabContexts.length === 1) { - this.openAndActivateEmptyTab(); - } - else { - this.activateNextTabListener(); - } - - this.tabContexts = this.tabContexts.filter(tc => tc.tabId === tabId); - - this.trigger('tabRemoved', {tabId}); - - this.openTabsChangedListener(); - } - - tabReorderListener({tabIdsInOrder}) { - const order = {}; - - for (const i in tabIdsInOrder) { - order[tabIdsInOrder[i]] = i; - } - - this.tabContexts.sort((a, b) => order[a.tabId] < order[b.tabId] ? -1 : 1); - - this.openTabsChangedListener(); - } - - activateNextTabListener() { - const oldIdx = this.tabContexts.findIndex(tc => tc.tabId === this.activeTabId); - const newActiveTabId = this.tabContexts[oldIdx === this.tabContexts.length - 1 ? 0 : oldIdx + 1].tabId; - - this.activateTab(newActiveTabId); - } - - activatePreviousTabListener() { - const oldIdx = this.tabContexts.findIndex(tc => tc.tabId === this.activeTabId); - const newActiveTabId = this.tabContexts[oldIdx === 0 ? this.tabContexts.length - 1 : oldIdx - 1].tabId; - - this.activateTab(newActiveTabId); - } - - closeActiveTabListener() { - this.removeTab(this.activeTabId); - } - - openNewTabListener() { - this.openAndActivateEmptyTab(); - } - - removeAllTabsListener() { - // TODO - } - - removeAllTabsExceptForThis() { - // TODO } async protectedSessionStartedListener() { diff --git a/src/public/javascripts/services/entrypoints.js b/src/public/javascripts/services/entrypoints.js index c1f33f21f..892adf134 100644 --- a/src/public/javascripts/services/entrypoints.js +++ b/src/public/javascripts/services/entrypoints.js @@ -71,14 +71,14 @@ export default class Entrypoints extends Component { await treeService.expandToNote(note.noteId); const tabContext = appContext.openEmptyTab(); - appContext.activateTab(tabContext.tabId); + appContext.tabManager.activateTab(tabContext.tabId); await tabContext.setNote(note.noteId); appContext.trigger('focusAndSelectTitle'); } toggleNoteHoistingListener() { - const note = appContext.getActiveTabNote(); + const note = appContext.tabManager.getActiveTabNote(); hoistedNoteService.getHoistedNoteId().then(async hoistedNoteId => { if (note.noteId === hoistedNoteId) { diff --git a/src/public/javascripts/services/frontend_script_api.js b/src/public/javascripts/services/frontend_script_api.js index 6366fd5ac..d9f7386d4 100644 --- a/src/public/javascripts/services/frontend_script_api.js +++ b/src/public/javascripts/services/frontend_script_api.js @@ -48,7 +48,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte * @returns {Promise} */ this.activateNote = async notePath => { - await appContext.getActiveTabContext().setNote(notePath); + await appContext.tabManager.getActiveTabContext().setNote(notePath); }; /** @@ -60,7 +60,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte this.activateNewNote = async notePath => { await ws.waitForMaxKnownSyncId(); - await appContext.getActiveTabContext().setNote(notePath); + await appContext.tabManager.getActiveTabContext().setNote(notePath); appContext.trigger('focusAndSelectTitle'); }; @@ -285,7 +285,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte * @method * @returns {NoteShort} active note (loaded into right pane) */ - this.getActiveTabNote = appContext.getActiveTabNote; + this.getActiveTabNote = appContext.tabManager.getActiveTabNote; /** * See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html for a documentation on the returned instance. @@ -299,7 +299,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte * @method * @returns {Promise} returns note path of active note or null if there isn't active note */ - this.getActiveTabNotePath = appContext.getActiveTabNotePath; + this.getActiveTabNotePath = appContext.tabManager.getActiveTabNotePath; /** * This method checks whether user navigated away from the note from which the scripts has been started. diff --git a/src/public/javascripts/services/import.js b/src/public/javascripts/services/import.js index 9181bbfbf..8e3695ecb 100644 --- a/src/public/javascripts/services/import.js +++ b/src/public/javascripts/services/import.js @@ -65,7 +65,7 @@ ws.subscribeToMessages(async message => { toastService.showPersistent(toast); if (message.result.importedNoteId) { - await appContext.getActiveTabContext.setNote(message.result.importedNoteId); + await appContext.tabManager.getActiveTabContext.setNote(message.result.importedNoteId); } } }); diff --git a/src/public/javascripts/services/keyboard_actions.js b/src/public/javascripts/services/keyboard_actions.js index da798257a..95bb46072 100644 --- a/src/public/javascripts/services/keyboard_actions.js +++ b/src/public/javascripts/services/keyboard_actions.js @@ -26,7 +26,7 @@ server.get('keyboard-shortcuts-for-notes').then(shortcutForNotes => { utils.bindGlobalShortcut(shortcut, async () => { const treeService = (await import("./tree.js")).default; - appContext.getActiveTabContext().setNote(shortcutForNotes[shortcut]); + appContext.tabManager.getActiveTabContext().setNote(shortcutForNotes[shortcut]); }); } }); diff --git a/src/public/javascripts/services/link.js b/src/public/javascripts/services/link.js index bc4f75890..192d3b633 100644 --- a/src/public/javascripts/services/link.js +++ b/src/public/javascripts/services/link.js @@ -78,11 +78,11 @@ function goToLink(e) { if (notePath) { if ((e.which === 1 && e.ctrlKey) || e.which === 2) { const tabContext = appContext.openEmptyTab(); - appContext.activateTab(tabContext.tabId); + appContext.tabManager.activateTab(tabContext.tabId); tabContext.setNote(notePath); } else if (e.which === 1) { - const activeTabContext = appContext.getActiveTabContext(); + const activeTabContext = appContext.tabManager.getActiveTabContext(); activeTabContext.setNote(notePath) } else { @@ -121,7 +121,7 @@ function newTabContextMenu(e) { if (cmd === 'openNoteInNewTab') { const tabContext = appContext.openEmptyTab(); tabContext.setNote(notePath); - appContext.activateTab(tabContext.tabId); + appContext.tabManager.activateTab(tabContext.tabId); } } }); @@ -143,7 +143,7 @@ $(document).on('mousedown', '.note-detail-text a', function (e) { if (notePath) { const tabContext = appContext.openEmptyTab(); tabContext.setNote(notePath); - appContext.activateTab(tabContext.tabId); + appContext.tabManager.activateTab(tabContext.tabId); } else { const address = $link.attr('href'); diff --git a/src/public/javascripts/services/note_autocomplete.js b/src/public/javascripts/services/note_autocomplete.js index 25f87ffa9..4f6b6a575 100644 --- a/src/public/javascripts/services/note_autocomplete.js +++ b/src/public/javascripts/services/note_autocomplete.js @@ -8,7 +8,7 @@ const SELECTED_PATH_KEY = "data-note-path"; async function autocompleteSource(term, cb) { const result = await server.get('autocomplete' + '?query=' + encodeURIComponent(term) - + '&activeNoteId=' + appContext.getActiveTabNoteId()); + + '&activeNoteId=' + appContext.tabManager.getActiveTabNoteId()); if (result.length === 0) { result.push({ diff --git a/src/public/javascripts/services/note_create.js b/src/public/javascripts/services/note_create.js index 7715e0cc3..d5d45e278 100644 --- a/src/public/javascripts/services/note_create.js +++ b/src/public/javascripts/services/note_create.js @@ -25,7 +25,7 @@ async function createNote(parentNoteId, options = {}) { options.isProtected = false; } - if (appContext.getActiveTabNoteType() !== 'text') { + if (appContext.tabManager.getActiveTabNoteType() !== 'text') { options.saveSelection = false; } @@ -48,7 +48,7 @@ async function createNote(parentNoteId, options = {}) { } if (options.activate) { - const activeTabContext = appContext.getActiveTabContext(); + const activeTabContext = appContext.tabManager.getActiveTabContext(); activeTabContext.setNote(note.noteId); } @@ -76,7 +76,7 @@ async function duplicateNote(noteId, parentNoteId) { await ws.waitForMaxKnownSyncId(); - await appContext.activateOrOpenNote(note.noteId); + await appContext.tabManager.activateOrOpenNote(note.noteId); const origNote = await treeCache.getNote(noteId); toastService.showMessage(`Note "${origNote.title}" has been duplicated`); diff --git a/src/public/javascripts/services/protected_session.js b/src/public/javascripts/services/protected_session.js index 674a1f52c..a8c1c911e 100644 --- a/src/public/javascripts/services/protected_session.js +++ b/src/public/javascripts/services/protected_session.js @@ -69,20 +69,20 @@ async function enterProtectedSessionOnServer(password) { } async function protectNoteAndSendToServer() { - if (!appContext.getActiveTabNote() || appContext.getActiveTabNote().isProtected) { + if (!appContext.tabManager.getActiveTabNote() || appContext.tabManager.getActiveTabNote().isProtected) { return; } await enterProtectedSession(); - const note = appContext.getActiveTabNote(); + const note = appContext.tabManager.getActiveTabNote(); note.isProtected = true; - await appContext.getActiveTabContext().saveNote(); + await appContext.tabManager.getActiveTabContext().saveNote(); } async function unprotectNoteAndSendToServer() { - const activeNote = appContext.getActiveTabNote(); + const activeNote = appContext.tabManager.getActiveTabNote(); if (!activeNote.isProtected) { toastService.showAndLogError(`Note ${activeNote.noteId} is not protected`); @@ -101,7 +101,7 @@ async function unprotectNoteAndSendToServer() { activeNote.isProtected = false; - await appContext.getActiveTabContext().saveNote(); + await appContext.tabManager.getActiveTabContext().saveNote(); } async function protectSubtree(noteId, protect) { diff --git a/src/public/javascripts/services/tab_manager.js b/src/public/javascripts/services/tab_manager.js new file mode 100644 index 000000000..05819f8c2 --- /dev/null +++ b/src/public/javascripts/services/tab_manager.js @@ -0,0 +1,272 @@ +import Component from "../widgets/component.js"; +import SpacedUpdate from "./spaced_update.js"; +import server from "./server.js"; +import options from "./options.js"; +import treeCache from "./tree_cache.js"; +import treeService from "./tree.js"; +import utils from "./utils.js"; +import TabContext from "./tab_context.js"; + +export default class TabManager extends Component { + constructor(appContext) { + super(appContext); + + /** @type {TabContext[]} */ + this.tabContexts = []; + this.activeTabId = null; + + this.tabsUpdate = new SpacedUpdate(async () => { + const openTabs = this.tabContexts + .map(tc => tc.getTabState()) + .filter(t => !!t); + + await server.put('options', { + openTabs: JSON.stringify(openTabs) + }); + }); + } + + async loadTabs() { + const openTabs = options.getJson('openTabs') || []; + + await treeCache.initializedPromise; + + // if there's notePath in the URL, make sure it's open and active + // (useful, among others, for opening clipped notes from clipper) + if (window.location.hash) { + const notePath = window.location.hash.substr(1); + const noteId = treeService.getNoteIdFromNotePath(notePath); + + if (noteId && await treeCache.noteExists(noteId)) { + for (const tab of openTabs) { + tab.active = false; + } + + const foundTab = openTabs.find(tab => noteId === treeService.getNoteIdFromNotePath(tab.notePath)); + + if (foundTab) { + foundTab.active = true; + } + else { + openTabs.push({ + notePath: notePath, + active: true + }); + } + } + } + + let filteredTabs = []; + + for (const openTab of openTabs) { + const noteId = treeService.getNoteIdFromNotePath(openTab.notePath); + + if (await treeCache.noteExists(noteId)) { + // note doesn't exist so don't try to open tab for it + filteredTabs.push(openTab); + } + } + + if (utils.isMobile()) { + // mobile frontend doesn't have tabs so show only the active tab + filteredTabs = filteredTabs.filter(tab => tab.active); + } + + if (filteredTabs.length === 0) { + filteredTabs.push({ + notePath: 'root', + active: true + }); + } + + if (!filteredTabs.find(tab => tab.active)) { + filteredTabs[0].active = true; + } + + this.tabsUpdate.allowUpdateWithoutChange(() => { + for (const tab of filteredTabs) { + const tabContext = this.openEmptyTab(); + tabContext.setNote(tab.notePath); + + if (tab.active) { + this.activateTab(tabContext.tabId); + } + } + }); + } + + tabNoteSwitchedListener({tabId}) { + if (tabId === this.activeTabId) { + this._setCurrentNotePathToHash(); + } + } + + _setCurrentNotePathToHash() { + const activeTabContext = this.getActiveTabContext(); + + if (activeTabContext && activeTabContext.notePath) { + document.location.hash = (activeTabContext.notePath || "") + "-" + activeTabContext.tabId; + } + } + + /** @return {TabContext[]} */ + getTabContexts() { + return this.tabContexts; + } + + /** @returns {TabContext} */ + getTabContextById(tabId) { + return this.tabContexts.find(tc => tc.tabId === tabId); + } + + /** @returns {TabContext} */ + getActiveTabContext() { + return this.getTabContextById(this.activeTabId); + } + + /** @returns {string|null} */ + getActiveTabNotePath() { + const activeContext = this.getActiveTabContext(); + return activeContext ? activeContext.notePath : null; + } + + /** @return {NoteShort} */ + getActiveTabNote() { + const activeContext = this.getActiveTabContext(); + return activeContext ? activeContext.note : null; + } + + /** @return {string|null} */ + getActiveTabNoteId() { + const activeNote = this.getActiveTabNote(); + + return activeNote ? activeNote.noteId : null; + } + + /** @return {string|null} */ + getActiveTabNoteType() { + const activeNote = this.getActiveTabNote(); + + return activeNote ? activeNote.type : null; + } + + async switchToTab(tabId, notePath) { + const tabContext = this.tabContexts.find(tc => tc.tabId === tabId) + || this.openEmptyTab(); + + this.activateTab(tabContext.tabId); + await tabContext.setNote(notePath); + } + + async openAndActivateEmptyTab() { + const tabContext = this.openEmptyTab(); + + await this.activateTab(tabContext.tabId); + } + + openEmptyTab() { + const tabContext = new TabContext(this.appContext); + this.tabContexts.push(tabContext); + this.children.push(tabContext); + return tabContext; + } + + async activateOrOpenNote(noteId) { + for (const tabContext of this.getTabContexts()) { + if (tabContext.note && tabContext.note.noteId === noteId) { + await tabContext.activate(); + return; + } + } + + // if no tab with this note has been found we'll create new tab + + const tabContext = this.openEmptyTab(); + await tabContext.setNote(noteId); + } + + openTabsChangedListener() { + this.tabsUpdate.scheduleUpdate(); + } + + activateTab(tabId) { + if (tabId === this.activeTabId) { + return; + } + + const oldActiveTabId = this.activeTabId; + + this.activeTabId = tabId; + + this.trigger('activeTabChanged', { oldActiveTabId, newActiveTabId: tabId }); + } + + newTabListener() { + this.openAndActivateEmptyTab(); + } + + async removeTab(tabId) { + const tabContextToRemove = this.tabContexts.find(tc => tc.tabId === tabId); + + if (!tabContextToRemove) { + return; + } + + await this.trigger('beforeTabRemove', {tabId}, true); + + if (this.tabContexts.length === 1) { + this.openAndActivateEmptyTab(); + } + else { + this.activateNextTabListener(); + } + + this.children = this.tabContexts = this.tabContexts.filter(tc => tc.tabId === tabId); + + this.trigger('tabRemoved', {tabId}); + + this.openTabsChangedListener(); + } + + tabReorderListener({tabIdsInOrder}) { + const order = {}; + + for (const i in tabIdsInOrder) { + order[tabIdsInOrder[i]] = i; + } + + this.tabContexts.sort((a, b) => order[a.tabId] < order[b.tabId] ? -1 : 1); + + this.openTabsChangedListener(); + } + + activateNextTabListener() { + const oldIdx = this.tabContexts.findIndex(tc => tc.tabId === this.activeTabId); + const newActiveTabId = this.tabContexts[oldIdx === this.tabContexts.length - 1 ? 0 : oldIdx + 1].tabId; + + this.activateTab(newActiveTabId); + } + + activatePreviousTabListener() { + const oldIdx = this.tabContexts.findIndex(tc => tc.tabId === this.activeTabId); + const newActiveTabId = this.tabContexts[oldIdx === 0 ? this.tabContexts.length - 1 : oldIdx - 1].tabId; + + this.activateTab(newActiveTabId); + } + + closeActiveTabListener() { + this.removeTab(this.activeTabId); + } + + openNewTabListener() { + this.openAndActivateEmptyTab(); + } + + removeAllTabsListener() { + // TODO + } + + removeAllTabsExceptForThis() { + // TODO + } +} \ No newline at end of file diff --git a/src/public/javascripts/services/tree.js b/src/public/javascripts/services/tree.js index b5c9a4166..8271942f0 100644 --- a/src/public/javascripts/services/tree.js +++ b/src/public/javascripts/services/tree.js @@ -139,7 +139,7 @@ async function sortAlphabetically(noteId) { ws.subscribeToMessages(message => { if (message.type === 'open-note') { - appContext.activateOrOpenNote(message.noteId); + appContext.tabManager.activateOrOpenNote(message.noteId); if (utils.isElectron()) { const currentWindow = require("electron").remote.getCurrentWindow(); diff --git a/src/public/javascripts/services/tree_context_menu.js b/src/public/javascripts/services/tree_context_menu.js index f184051c4..56bd90d9c 100644 --- a/src/public/javascripts/services/tree_context_menu.js +++ b/src/public/javascripts/services/tree_context_menu.js @@ -103,7 +103,7 @@ class TreeContextMenu { if (cmd === 'openInTab') { const tabContext = appContext.openEmptyTab(); - appContext.activateTab(tabContext.tabId); + appContext.tabManager.activateTab(tabContext.tabId); tabContext.setNote(notePath); } else if (cmd.startsWith("insertNoteAfter")) { diff --git a/src/public/javascripts/widgets/calendar.js b/src/public/javascripts/widgets/calendar.js index 4299a84c0..f6e61eff7 100644 --- a/src/public/javascripts/widgets/calendar.js +++ b/src/public/javascripts/widgets/calendar.js @@ -58,7 +58,7 @@ export default class CalendarWidget extends CollapsibleWidget { const note = await dateNoteService.getDateNote(date); if (note) { - appContext.getActiveTabContext().setNote(note.noteId); + appContext.tabManager.getActiveTabContext().setNote(note.noteId); } else { alert("Cannot find day note"); diff --git a/src/public/javascripts/widgets/note_detail.js b/src/public/javascripts/widgets/note_detail.js index a28ec7b45..28230c0db 100644 --- a/src/public/javascripts/widgets/note_detail.js +++ b/src/public/javascripts/widgets/note_detail.js @@ -54,7 +54,7 @@ export default class NoteDetailWidget extends TabAwareWidget { this.$widget.on("dragleave", e => e.preventDefault()); this.$widget.on("drop", async e => { - const activeNote = this.appContext.getActiveTabNote(); + const activeNote = this.appContext.tabManager.getActiveTabNote(); if (!activeNote) { return; diff --git a/src/public/javascripts/widgets/note_tree.js b/src/public/javascripts/widgets/note_tree.js index 567eff5f6..1e73e65cb 100644 --- a/src/public/javascripts/widgets/note_tree.js +++ b/src/public/javascripts/widgets/note_tree.js @@ -89,7 +89,7 @@ export default class NoteTreeWidget extends TabAwareWidget { else if (event.ctrlKey) { const tabContext = appContext.openEmptyTab(); treeService.getNotePath(node).then(notePath => tabContext.setNote(notePath)); - appContext.activateTab(tabContext.tabId); + appContext.tabManager.activateTab(tabContext.tabId); } else { node.setActive(); @@ -106,7 +106,7 @@ export default class NoteTreeWidget extends TabAwareWidget { const notePath = await treeService.getNotePath(data.node); - const activeTabContext = this.appContext.getActiveTabContext(); + const activeTabContext = this.appContext.tabManager.getActiveTabContext(); await activeTabContext.setNote(notePath); }, expand: (event, data) => this.setExpandedToServer(data.node.data.branchId, true), @@ -286,7 +286,7 @@ export default class NoteTreeWidget extends TabAwareWidget { } async scrollToActiveNoteListener() { - const activeContext = appContext.getActiveTabContext(); + const activeContext = appContext.tabManager.getActiveTabContext(); if (activeContext && activeContext.notePath) { this.tree.setFocus(); @@ -466,7 +466,7 @@ export default class NoteTreeWidget extends TabAwareWidget { const notePath = await treeService.getNotePath(newActive); - appContext.getActiveTabContext().setNote(notePath); + appContext.tabManager.getActiveTabContext().setNote(notePath); } node.remove(); @@ -526,7 +526,7 @@ export default class NoteTreeWidget extends TabAwareWidget { } } - const activateNotePath = appContext.getActiveTabNotePath(); + const activateNotePath = appContext.tabManager.getActiveTabNotePath(); if (activateNotePath) { const node = await this.getNodeFromPath(activateNotePath); diff --git a/src/public/javascripts/widgets/search_box.js b/src/public/javascripts/widgets/search_box.js index eb35120e8..ef88b6e4e 100644 --- a/src/public/javascripts/widgets/search_box.js +++ b/src/public/javascripts/widgets/search_box.js @@ -168,7 +168,7 @@ export default class SearchBoxWidget extends BasicWidget { } searchInSubtreeListener({noteId}) { - noteId = noteId || appContext.getActiveTabNoteId(); + noteId = noteId || appContext.tabManager.getActiveTabNoteId(); this.toggle(true); diff --git a/src/public/javascripts/widgets/tab_aware_widget.js b/src/public/javascripts/widgets/tab_aware_widget.js index 0ec19ca0a..4e4c5ee1a 100644 --- a/src/public/javascripts/widgets/tab_aware_widget.js +++ b/src/public/javascripts/widgets/tab_aware_widget.js @@ -68,7 +68,7 @@ export default class TabAwareWidget extends BasicWidget { refreshWithNote(note, notePath) {} activeTabChangedListener() { - this.tabContext = this.appContext.getActiveTabContext(); + this.tabContext = this.appContext.tabManager.getActiveTabContext(); this.activeTabChanged(); } @@ -79,7 +79,7 @@ export default class TabAwareWidget extends BasicWidget { lazyLoadedListener() { if (!this.tabContext) { // has not been loaded yet - this.tabContext = this.appContext.getActiveTabContext(); + this.tabContext = this.appContext.tabManager.getActiveTabContext(); } this.refresh(); diff --git a/src/public/javascripts/widgets/tab_row.js b/src/public/javascripts/widgets/tab_row.js index 5814853ad..b4b447d62 100644 --- a/src/public/javascripts/widgets/tab_row.js +++ b/src/public/javascripts/widgets/tab_row.js @@ -490,7 +490,7 @@ export default class TabRowWidget extends BasicWidget { this.draggabillies.push(draggabilly); draggabilly.on('pointerDown', _ => { - this.appContext.activateTab(tabEl.getAttribute('data-tab-id')); + this.appContext.tabManager.activateTab(tabEl.getAttribute('data-tab-id')); }); draggabilly.on('dragStart', _ => { @@ -585,7 +585,7 @@ export default class TabRowWidget extends BasicWidget { tabNoteSwitchedListener({tabId}) { const $tab = this.getTabById(tabId); - const {note} = this.appContext.getTabContextById(tabId); + const {note} = this.appContext.tabManager.getTabContextById(tabId); this.updateTab($tab, note); } @@ -609,7 +609,7 @@ export default class TabRowWidget extends BasicWidget { } async entitiesReloadedListener({loadResults}) { - for (const tabContext of this.appContext.getTabContexts()) { + for (const tabContext of this.appContext.tabManager.getTabContexts()) { if (loadResults.isNoteReloaded(tabContext.noteId)) { const $tab = this.getTabById(tabContext.tabId); @@ -619,7 +619,7 @@ export default class TabRowWidget extends BasicWidget { } treeCacheReloadedListener() { - for (const tabContext of this.appContext.getTabContexts()) { + for (const tabContext of this.appContext.tabManager.getTabContexts()) { const $tab = this.getTabById(tabContext.tabId); this.updateTab($tab, tabContext.note); diff --git a/src/public/javascripts/widgets/type_widgets/empty.js b/src/public/javascripts/widgets/type_widgets/empty.js index 7be599b4a..7ab1b6f8b 100644 --- a/src/public/javascripts/widgets/type_widgets/empty.js +++ b/src/public/javascripts/widgets/type_widgets/empty.js @@ -28,7 +28,7 @@ export default class EmptyTypeWidget extends TypeWidget { return false; } - appContext.getActiveTabContext().setNote(suggestion.path); + appContext.tabManager.getActiveTabContext().setNote(suggestion.path); }); noteAutocompleteService.showRecentNotes(this.$autoComplete); diff --git a/src/public/javascripts/widgets/type_widgets/text.js b/src/public/javascripts/widgets/type_widgets/text.js index 1c60d1b71..b3de1207e 100644 --- a/src/public/javascripts/widgets/type_widgets/text.js +++ b/src/public/javascripts/widgets/type_widgets/text.js @@ -91,7 +91,7 @@ export default class TextTypeWidget extends TypeWidget { if (match) { const noteId = match[1]; - appContext.getActiveTabContext().setNote(noteId); + appContext.tabManager.getActiveTabContext().setNote(noteId); } else { window.open(src, '_blank');