trilium/src/public/app/services/tab_manager.js
zadam 4ae965c5cb Merge remote-tracking branch 'origin/stable'
# Conflicts:
#	src/public/app/widgets/collapsible_widgets/note_info.js
2020-06-04 22:37:04 +02:00

352 lines
10 KiB
JavaScript

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";
import appContext from "./app_context.js";
export default class TabManager extends Component {
constructor() {
super();
this.activeTabId = null;
this.tabsUpdate = new SpacedUpdate(async () => {
if (!appContext.isMainWindow) {
return;
}
const openTabs = this.tabContexts
.map(tc => tc.getTabState())
.filter(t => !!t);
await server.put('options', {
openTabs: JSON.stringify(openTabs)
});
});
}
/** @type {TabContext[]} */
get tabContexts() {
return this.children;
}
async loadTabs() {
const tabsToOpen = appContext.isMainWindow
? (options.getJson('openTabs') || [])
: [];
// 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 tabsToOpen) {
tab.active = false;
}
const foundTab = tabsToOpen.find(tab => noteId === treeService.getNoteIdFromNotePath(tab.notePath));
if (foundTab) {
foundTab.active = true;
}
else {
tabsToOpen.push({
notePath: notePath,
active: true
});
}
}
}
let filteredTabs = [];
for (const openTab of tabsToOpen) {
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: this.isMainWindow ? 'root' : '',
active: true
});
}
if (!filteredTabs.find(tab => tab.active)) {
filteredTabs[0].active = true;
}
await this.tabsUpdate.allowUpdateWithoutChange(async () => {
for (const tab of filteredTabs) {
await this.openTabWithNote(tab.notePath, tab.active, tab.tabId);
}
});
}
tabNoteSwitchedEvent({tabContext}) {
if (tabContext.isActive()) {
this.setCurrentNotePathToHash();
}
this.tabsUpdate.scheduleUpdate();
}
setCurrentNotePathToHash() {
const activeTabContext = this.getActiveTabContext();
if (window.history.length === 0 // first history entry
|| (activeTabContext && activeTabContext.notePath !== treeService.getHashValueFromAddress()[0])) {
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);
document.title = "Trilium Notes";
if (activeTabContext.note) {
// it helps navigating in history if note title is included in the title
document.title += " - " + activeTabContext.note.title;
}
}
this.triggerEvent('activeNoteChanged'); // trigger this even in on popstate event
}
/** @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)
|| await this.openEmptyTab();
this.activateTab(tabContext.tabId);
await tabContext.setNote(notePath);
}
async openAndActivateEmptyTab() {
const tabContext = await this.openEmptyTab();
await this.activateTab(tabContext.tabId);
await tabContext.setEmpty();
}
async openEmptyTab(tabId) {
const tabContext = new TabContext(tabId);
this.child(tabContext);
await this.triggerEvent('newTabOpened', {tabContext});
return tabContext;
}
async openTabWithNote(notePath, activate, tabId = null) {
const tabContext = await this.openEmptyTab(tabId);
if (notePath) {
await tabContext.setNote(notePath, !activate); // if activate is false then send normal noteSwitched event
}
if (activate) {
this.activateTab(tabContext.tabId, false);
await this.triggerEvent('tabNoteSwitchedAndActivated', {
tabContext,
notePath: tabContext.notePath // resolved note path
});
}
return tabContext;
}
async activateOrOpenNote(noteId) {
for (const tabContext of this.getTabContexts()) {
if (tabContext.note && tabContext.note.noteId === noteId) {
this.activateTab(tabContext.tabId);
return;
}
}
// if no tab with this note has been found we'll create new tab
await this.openTabWithNote(noteId, true);
}
activateTab(tabId, triggerEvent = true) {
if (tabId === this.activeTabId) {
return;
}
this.activeTabId = tabId;
if (triggerEvent) {
this.triggerEvent('activeTabChanged', {
tabContext: this.getTabContextById(tabId)
});
}
this.tabsUpdate.scheduleUpdate();
this.setCurrentNotePathToHash();
}
async removeTab(tabId) {
const tabContextToRemove = this.getTabContextById(tabId);
if (!tabContextToRemove) {
return;
}
// close dangling autocompletes after closing the tab
$(".aa-input").autocomplete("close");
await this.triggerEvent('beforeTabRemove', {tabId});
if (this.tabContexts.length <= 1) {
this.openAndActivateEmptyTab();
}
else if (tabContextToRemove.isActive()) {
const idx = this.tabContexts.findIndex(tc => tc.tabId === tabId);
if (idx === this.tabContexts.length - 1) {
this.activatePreviousTabCommand();
}
else {
this.activateNextTabCommand();
}
}
this.children = this.children.filter(tc => tc.tabId !== tabId);
this.triggerEvent('tabRemoved', {tabId});
this.tabsUpdate.scheduleUpdate();
}
tabReorderEvent({tabIdsInOrder}) {
const order = {};
for (const i in tabIdsInOrder) {
order[tabIdsInOrder[i]] = i;
}
this.children.sort((a, b) => order[a.tabId] < order[b.tabId] ? -1 : 1);
this.tabsUpdate.scheduleUpdate();
}
activateNextTabCommand() {
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);
}
activatePreviousTabCommand() {
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);
}
closeActiveTabCommand() {
this.removeTab(this.activeTabId);
}
beforeUnloadEvent() {
this.tabsUpdate.updateNowIfNecessary();
}
openNewTabCommand() {
this.openAndActivateEmptyTab();
}
async removeAllTabsCommand() {
for (const tabIdToRemove of this.tabContexts.map(tc => tc.tabId)) {
await this.removeTab(tabIdToRemove);
}
}
async removeAllTabsExceptForThisCommand({tabId}) {
for (const tabIdToRemove of this.tabContexts.map(tc => tc.tabId)) {
if (tabIdToRemove !== tabId) {
await this.removeTab(tabIdToRemove);
}
}
}
moveTabToNewWindowCommand({tabId}) {
const notePath = this.getTabContextById(tabId).notePath;
this.removeTab(tabId);
this.triggerCommand('openInWindow', {notePath});
}
async hoistedNoteChangedEvent({hoistedNoteId}) {
if (hoistedNoteId === 'root') {
return;
}
for (const tc of this.tabContexts.splice()) {
if (tc.notePath && !tc.notePath.split("/").includes(hoistedNoteId)) {
await this.removeTab(tc.tabId);
}
}
}
}