2020-02-07 21:08:55 +01:00
|
|
|
import Component from "../widgets/component.js";
|
|
|
|
import SpacedUpdate from "./spaced_update.js";
|
|
|
|
import server from "./server.js";
|
|
|
|
import options from "./options.js";
|
2021-04-16 23:01:56 +02:00
|
|
|
import froca from "./froca.js";
|
2020-02-07 21:08:55 +01:00
|
|
|
import treeService from "./tree.js";
|
|
|
|
import utils from "./utils.js";
|
|
|
|
import TabContext from "./tab_context.js";
|
2020-04-25 23:52:13 +02:00
|
|
|
import appContext from "./app_context.js";
|
2020-02-07 21:08:55 +01:00
|
|
|
|
|
|
|
export default class TabManager extends Component {
|
2020-02-27 10:03:14 +01:00
|
|
|
constructor() {
|
|
|
|
super();
|
2020-02-07 21:08:55 +01:00
|
|
|
|
|
|
|
this.activeTabId = null;
|
|
|
|
|
|
|
|
this.tabsUpdate = new SpacedUpdate(async () => {
|
2020-04-25 23:52:13 +02:00
|
|
|
if (!appContext.isMainWindow) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-02-07 21:08:55 +01:00
|
|
|
const openTabs = this.tabContexts
|
|
|
|
.map(tc => tc.getTabState())
|
|
|
|
.filter(t => !!t);
|
|
|
|
|
|
|
|
await server.put('options', {
|
|
|
|
openTabs: JSON.stringify(openTabs)
|
|
|
|
});
|
|
|
|
});
|
2021-02-27 23:39:02 +01:00
|
|
|
|
|
|
|
appContext.addBeforeUnloadListener(this);
|
2020-02-07 21:08:55 +01:00
|
|
|
}
|
|
|
|
|
2020-02-09 21:13:05 +01:00
|
|
|
/** @type {TabContext[]} */
|
|
|
|
get tabContexts() {
|
|
|
|
return this.children;
|
|
|
|
}
|
|
|
|
|
2021-05-20 23:13:34 +02:00
|
|
|
/** @type {TabContext[]} */
|
|
|
|
get mainTabContexts() {
|
|
|
|
return this.tabContexts.filter(tc => !tc.parentTabId)
|
|
|
|
}
|
|
|
|
|
2020-04-25 23:52:13 +02:00
|
|
|
async loadTabs() {
|
|
|
|
const tabsToOpen = appContext.isMainWindow
|
2020-04-23 23:08:15 +02:00
|
|
|
? (options.getJson('openTabs') || [])
|
|
|
|
: [];
|
2020-02-07 21:08:55 +01:00
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
2021-04-16 22:57:37 +02:00
|
|
|
if (noteId && await froca.noteExists(noteId)) {
|
2020-04-25 23:52:13 +02:00
|
|
|
for (const tab of tabsToOpen) {
|
2020-02-07 21:08:55 +01:00
|
|
|
tab.active = false;
|
|
|
|
}
|
|
|
|
|
2020-04-25 23:52:13 +02:00
|
|
|
const foundTab = tabsToOpen.find(tab => noteId === treeService.getNoteIdFromNotePath(tab.notePath));
|
2020-02-07 21:08:55 +01:00
|
|
|
|
|
|
|
if (foundTab) {
|
|
|
|
foundTab.active = true;
|
|
|
|
}
|
|
|
|
else {
|
2020-04-25 23:52:13 +02:00
|
|
|
tabsToOpen.push({
|
2020-02-07 21:08:55 +01:00
|
|
|
notePath: notePath,
|
2021-02-07 21:27:09 +01:00
|
|
|
active: true,
|
|
|
|
hoistedNoteId: glob.extraHoistedNoteId || 'root'
|
2020-02-07 21:08:55 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let filteredTabs = [];
|
|
|
|
|
2020-04-25 23:52:13 +02:00
|
|
|
for (const openTab of tabsToOpen) {
|
2020-02-07 21:08:55 +01:00
|
|
|
const noteId = treeService.getNoteIdFromNotePath(openTab.notePath);
|
|
|
|
|
2021-04-16 22:57:37 +02:00
|
|
|
if (await froca.noteExists(noteId)) {
|
2020-02-07 21:08:55 +01:00
|
|
|
// 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({
|
2020-05-05 19:30:03 +02:00
|
|
|
notePath: this.isMainWindow ? 'root' : '',
|
2021-02-07 21:27:09 +01:00
|
|
|
active: true,
|
|
|
|
extraHoistedNoteId: glob.extraHoistedNoteId || 'root'
|
2020-02-07 21:08:55 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!filteredTabs.find(tab => tab.active)) {
|
|
|
|
filteredTabs[0].active = true;
|
|
|
|
}
|
|
|
|
|
2021-05-21 22:34:40 +02:00
|
|
|
console.log("filteredTabs", filteredTabs);
|
|
|
|
|
2020-02-08 21:23:42 +01:00
|
|
|
await this.tabsUpdate.allowUpdateWithoutChange(async () => {
|
2020-02-07 21:08:55 +01:00
|
|
|
for (const tab of filteredTabs) {
|
2021-05-20 23:13:34 +02:00
|
|
|
await this.openTabWithNote(tab.notePath, tab.active, tab.tabId, tab.hoistedNoteId, tab.parentTabId);
|
2020-02-07 21:08:55 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-03-07 13:40:46 +01:00
|
|
|
tabNoteSwitchedEvent({tabContext}) {
|
|
|
|
if (tabContext.isActive()) {
|
2020-02-08 21:23:42 +01:00
|
|
|
this.setCurrentNotePathToHash();
|
2020-02-07 21:08:55 +01:00
|
|
|
}
|
2020-02-08 21:23:42 +01:00
|
|
|
|
|
|
|
this.tabsUpdate.scheduleUpdate();
|
2020-02-07 21:08:55 +01:00
|
|
|
}
|
|
|
|
|
2020-02-08 21:23:42 +01:00
|
|
|
setCurrentNotePathToHash() {
|
2020-02-07 21:08:55 +01:00
|
|
|
const activeTabContext = this.getActiveTabContext();
|
|
|
|
|
2020-03-08 17:17:18 +01:00
|
|
|
if (window.history.length === 0 // first history entry
|
|
|
|
|| (activeTabContext && activeTabContext.notePath !== treeService.getHashValueFromAddress()[0])) {
|
2020-02-09 21:13:05 +01:00
|
|
|
const url = '#' + (activeTabContext.notePath || "") + "-" + activeTabContext.tabId;
|
|
|
|
|
|
|
|
// using pushState instead of directly modifying document.location because it does not trigger hashchange
|
|
|
|
window.history.pushState(null, "", url);
|
2020-10-26 20:11:43 +01:00
|
|
|
}
|
2020-02-09 21:13:05 +01:00
|
|
|
|
2020-10-26 20:11:43 +01:00
|
|
|
document.title = "Trilium Notes";
|
2020-02-09 21:13:05 +01:00
|
|
|
|
2020-10-26 20:11:43 +01:00
|
|
|
if (activeTabContext.note) {
|
|
|
|
// it helps navigating in history if note title is included in the title
|
|
|
|
document.title += " - " + activeTabContext.note.title;
|
2020-02-07 21:08:55 +01:00
|
|
|
}
|
2020-03-08 17:17:18 +01:00
|
|
|
|
|
|
|
this.triggerEvent('activeNoteChanged'); // trigger this even in on popstate event
|
2020-02-07 21:08:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/** @return {TabContext[]} */
|
|
|
|
getTabContexts() {
|
|
|
|
return this.tabContexts;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @returns {TabContext} */
|
|
|
|
getTabContextById(tabId) {
|
2021-05-20 23:13:34 +02:00
|
|
|
const tabContext = this.tabContexts.find(tc => tc.tabId === tabId);
|
|
|
|
|
|
|
|
if (!tabContext) {
|
|
|
|
throw new Error(`Cannot find tabContext id='${tabId}'`);
|
|
|
|
}
|
|
|
|
|
|
|
|
return tabContext;
|
2020-02-07 21:08:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/** @returns {TabContext} */
|
|
|
|
getActiveTabContext() {
|
2021-05-20 23:13:34 +02:00
|
|
|
return this.activeTabId
|
|
|
|
? this.getTabContextById(this.activeTabId)
|
|
|
|
: null;
|
2020-02-07 21:08:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/** @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)
|
2020-02-29 16:26:46 +01:00
|
|
|
|| await this.openEmptyTab();
|
2020-02-07 21:08:55 +01:00
|
|
|
|
|
|
|
this.activateTab(tabContext.tabId);
|
|
|
|
await tabContext.setNote(notePath);
|
|
|
|
}
|
|
|
|
|
|
|
|
async openAndActivateEmptyTab() {
|
2020-02-29 16:26:46 +01:00
|
|
|
const tabContext = await this.openEmptyTab();
|
2020-02-07 21:08:55 +01:00
|
|
|
|
|
|
|
await this.activateTab(tabContext.tabId);
|
2020-02-28 11:46:35 +01:00
|
|
|
|
|
|
|
await tabContext.setEmpty();
|
2020-02-07 21:08:55 +01:00
|
|
|
}
|
|
|
|
|
2021-05-21 22:34:40 +02:00
|
|
|
async openEmptyTab(tabId, hoistedNoteId = 'root', parentTabId = null) {
|
2021-05-20 23:13:34 +02:00
|
|
|
const tabContext = new TabContext(tabId, hoistedNoteId, parentTabId);
|
2021-05-21 22:34:40 +02:00
|
|
|
|
|
|
|
const existingTabContext = this.children.find(tc => tc.tabId === tabContext.tabId);
|
|
|
|
|
|
|
|
if (existingTabContext) {
|
|
|
|
return existingTabContext;
|
|
|
|
}
|
|
|
|
|
2020-02-27 10:03:14 +01:00
|
|
|
this.child(tabContext);
|
|
|
|
|
2020-03-06 22:17:07 +01:00
|
|
|
await this.triggerEvent('newTabOpened', {tabContext});
|
2020-02-27 10:03:14 +01:00
|
|
|
|
2020-02-07 21:08:55 +01:00
|
|
|
return tabContext;
|
|
|
|
}
|
|
|
|
|
2020-11-24 23:24:05 +01:00
|
|
|
/**
|
|
|
|
* If the requested notePath is within current note hoisting scope then keep the note hoisting also for the new tab.
|
|
|
|
*/
|
|
|
|
async openTabWithNoteWithHoisting(notePath) {
|
|
|
|
const tabContext = this.getActiveTabContext();
|
|
|
|
let hoistedNoteId = 'root';
|
|
|
|
|
|
|
|
if (tabContext) {
|
2021-03-03 21:49:57 +01:00
|
|
|
const resolvedNotePath = await treeService.resolveNotePath(notePath, tabContext.hoistedNoteId);
|
2020-11-24 23:24:05 +01:00
|
|
|
|
|
|
|
if (resolvedNotePath.includes(tabContext.hoistedNoteId)) {
|
|
|
|
hoistedNoteId = tabContext.hoistedNoteId;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.openTabWithNote(notePath, false, null, hoistedNoteId);
|
|
|
|
}
|
|
|
|
|
2021-05-21 22:34:40 +02:00
|
|
|
async openTabWithNote(notePath, activate, tabId, hoistedNoteId = 'root', parentTabId = null) {
|
2021-05-20 23:13:34 +02:00
|
|
|
const tabContext = await this.openEmptyTab(tabId, hoistedNoteId, parentTabId);
|
2020-02-27 12:26:42 +01:00
|
|
|
|
2020-05-05 19:30:03 +02:00
|
|
|
if (notePath) {
|
|
|
|
await tabContext.setNote(notePath, !activate); // if activate is false then send normal noteSwitched event
|
|
|
|
}
|
2020-02-27 12:26:42 +01:00
|
|
|
|
|
|
|
if (activate) {
|
|
|
|
this.activateTab(tabContext.tabId, false);
|
|
|
|
|
2020-06-04 21:44:34 +02:00
|
|
|
await this.triggerEvent('tabNoteSwitchedAndActivated', {
|
2020-03-07 13:40:46 +01:00
|
|
|
tabContext,
|
2020-03-18 10:08:16 +01:00
|
|
|
notePath: tabContext.notePath // resolved note path
|
2020-02-28 00:31:12 +01:00
|
|
|
});
|
2020-02-27 12:26:42 +01:00
|
|
|
}
|
2020-05-08 23:39:46 +02:00
|
|
|
|
|
|
|
return tabContext;
|
2020-02-27 12:26:42 +01:00
|
|
|
}
|
|
|
|
|
2020-02-07 21:08:55 +01:00
|
|
|
async activateOrOpenNote(noteId) {
|
|
|
|
for (const tabContext of this.getTabContexts()) {
|
|
|
|
if (tabContext.note && tabContext.note.noteId === noteId) {
|
2020-04-05 15:35:01 +02:00
|
|
|
this.activateTab(tabContext.tabId);
|
|
|
|
|
2020-02-07 21:08:55 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// if no tab with this note has been found we'll create new tab
|
|
|
|
|
2020-03-18 10:08:16 +01:00
|
|
|
await this.openTabWithNote(noteId, true);
|
2020-02-07 21:08:55 +01:00
|
|
|
}
|
|
|
|
|
2020-02-27 12:26:42 +01:00
|
|
|
activateTab(tabId, triggerEvent = true) {
|
2020-02-07 21:08:55 +01:00
|
|
|
if (tabId === this.activeTabId) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.activeTabId = tabId;
|
|
|
|
|
2020-02-27 12:26:42 +01:00
|
|
|
if (triggerEvent) {
|
2020-03-07 13:40:46 +01:00
|
|
|
this.triggerEvent('activeTabChanged', {
|
|
|
|
tabContext: this.getTabContextById(tabId)
|
|
|
|
});
|
2020-02-27 12:26:42 +01:00
|
|
|
}
|
2020-02-08 21:23:42 +01:00
|
|
|
|
|
|
|
this.tabsUpdate.scheduleUpdate();
|
2020-05-22 19:30:21 +02:00
|
|
|
|
2020-02-08 21:23:42 +01:00
|
|
|
this.setCurrentNotePathToHash();
|
2020-02-07 21:08:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async removeTab(tabId) {
|
2021-05-21 22:34:40 +02:00
|
|
|
const mainTabContextToRemove = this.getTabContextById(tabId).getMainTabContext();
|
2021-05-20 23:13:34 +02:00
|
|
|
|
2020-05-22 19:30:21 +02:00
|
|
|
// close dangling autocompletes after closing the tab
|
|
|
|
$(".aa-input").autocomplete("close");
|
|
|
|
|
2021-05-20 23:13:34 +02:00
|
|
|
const tabIdsToRemove = mainTabContextToRemove.getAllSubTabContexts().map(tc => tc.tabId);
|
|
|
|
|
|
|
|
await this.triggerEvent('beforeTabRemove', { tabIds: tabIdsToRemove });
|
2020-02-07 21:08:55 +01:00
|
|
|
|
2021-05-20 23:13:34 +02:00
|
|
|
if (this.mainTabContexts.length <= 1) {
|
|
|
|
await this.openAndActivateEmptyTab();
|
2020-02-07 21:08:55 +01:00
|
|
|
}
|
2021-05-20 23:13:34 +02:00
|
|
|
else if (tabIdsToRemove.includes(this.activeTabId)) {
|
|
|
|
const idx = this.mainTabContexts.findIndex(tc => tc.tabId === mainTabContextToRemove.tabId);
|
2020-03-07 13:57:31 +01:00
|
|
|
|
2021-05-20 23:13:34 +02:00
|
|
|
if (idx === this.mainTabContexts.length - 1) {
|
2020-03-07 13:57:31 +01:00
|
|
|
this.activatePreviousTabCommand();
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
this.activateNextTabCommand();
|
|
|
|
}
|
2020-02-07 21:08:55 +01:00
|
|
|
}
|
|
|
|
|
2021-05-20 23:13:34 +02:00
|
|
|
this.children = this.children.filter(tc => !tabIdsToRemove.includes(tc.tabId));
|
2020-02-07 21:08:55 +01:00
|
|
|
|
2021-05-20 23:13:34 +02:00
|
|
|
this.triggerEvent('tabRemoved', {tabIds: tabIdsToRemove});
|
2020-02-07 21:08:55 +01:00
|
|
|
|
2020-02-08 21:23:42 +01:00
|
|
|
this.tabsUpdate.scheduleUpdate();
|
2020-02-07 21:08:55 +01:00
|
|
|
}
|
|
|
|
|
2020-02-16 19:23:49 +01:00
|
|
|
tabReorderEvent({tabIdsInOrder}) {
|
2020-02-07 21:08:55 +01:00
|
|
|
const order = {};
|
|
|
|
|
|
|
|
for (const i in tabIdsInOrder) {
|
|
|
|
order[tabIdsInOrder[i]] = i;
|
|
|
|
}
|
|
|
|
|
2020-02-09 21:13:05 +01:00
|
|
|
this.children.sort((a, b) => order[a.tabId] < order[b.tabId] ? -1 : 1);
|
2020-02-07 21:08:55 +01:00
|
|
|
|
2020-02-08 21:23:42 +01:00
|
|
|
this.tabsUpdate.scheduleUpdate();
|
2020-02-07 21:08:55 +01:00
|
|
|
}
|
|
|
|
|
2020-02-16 19:54:11 +01:00
|
|
|
activateNextTabCommand() {
|
2021-05-20 23:13:34 +02:00
|
|
|
const oldIdx = this.mainTabContexts.findIndex(tc => tc.tabId === this.activeTabId);
|
|
|
|
const newActiveTabId = this.mainTabContexts[oldIdx === this.tabContexts.length - 1 ? 0 : oldIdx + 1].tabId;
|
2020-02-07 21:08:55 +01:00
|
|
|
|
|
|
|
this.activateTab(newActiveTabId);
|
|
|
|
}
|
|
|
|
|
2020-02-16 19:54:11 +01:00
|
|
|
activatePreviousTabCommand() {
|
2021-05-20 23:13:34 +02:00
|
|
|
const oldIdx = this.mainTabContexts.findIndex(tc => tc.tabId === this.activeTabId);
|
|
|
|
const newActiveTabId = this.mainTabContexts[oldIdx === 0 ? this.tabContexts.length - 1 : oldIdx - 1].tabId;
|
2020-02-07 21:08:55 +01:00
|
|
|
|
|
|
|
this.activateTab(newActiveTabId);
|
|
|
|
}
|
|
|
|
|
2020-02-16 19:54:11 +01:00
|
|
|
closeActiveTabCommand() {
|
2020-02-07 21:08:55 +01:00
|
|
|
this.removeTab(this.activeTabId);
|
|
|
|
}
|
|
|
|
|
2020-02-16 19:23:49 +01:00
|
|
|
beforeUnloadEvent() {
|
2020-02-08 20:53:07 +01:00
|
|
|
this.tabsUpdate.updateNowIfNecessary();
|
2021-02-27 23:39:02 +01:00
|
|
|
|
|
|
|
return true; // don't block closing the tab, this metadata is not that important
|
2020-02-08 20:53:07 +01:00
|
|
|
}
|
|
|
|
|
2020-02-16 19:54:11 +01:00
|
|
|
openNewTabCommand() {
|
2020-02-07 21:08:55 +01:00
|
|
|
this.openAndActivateEmptyTab();
|
|
|
|
}
|
|
|
|
|
2020-02-16 19:54:11 +01:00
|
|
|
async removeAllTabsCommand() {
|
2020-02-09 21:13:05 +01:00
|
|
|
for (const tabIdToRemove of this.tabContexts.map(tc => tc.tabId)) {
|
|
|
|
await this.removeTab(tabIdToRemove);
|
|
|
|
}
|
2020-02-07 21:08:55 +01:00
|
|
|
}
|
|
|
|
|
2020-02-16 19:54:11 +01:00
|
|
|
async removeAllTabsExceptForThisCommand({tabId}) {
|
2020-02-09 21:13:05 +01:00
|
|
|
for (const tabIdToRemove of this.tabContexts.map(tc => tc.tabId)) {
|
|
|
|
if (tabIdToRemove !== tabId) {
|
|
|
|
await this.removeTab(tabIdToRemove);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-26 23:11:52 +02:00
|
|
|
moveTabToNewWindowCommand({tabId}) {
|
2021-02-07 21:27:09 +01:00
|
|
|
const {notePath, hoistedNoteId} = this.getTabContextById(tabId);
|
2020-04-26 23:11:52 +02:00
|
|
|
|
|
|
|
this.removeTab(tabId);
|
|
|
|
|
2021-02-07 21:27:09 +01:00
|
|
|
this.triggerCommand('openInWindow', {notePath, hoistedNoteId});
|
2020-04-26 23:11:52 +02:00
|
|
|
}
|
2020-11-24 22:32:22 +01:00
|
|
|
|
|
|
|
hoistedNoteChangedEvent() {
|
|
|
|
this.tabsUpdate.scheduleUpdate();
|
|
|
|
}
|
2020-05-22 19:30:21 +02:00
|
|
|
}
|