392 lines
10 KiB
JavaScript

import TabContext from "./tab_context.js";
import server from "./server.js";
import treeCache from "./tree_cache.js";
import bundleService from "./bundle.js";
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";
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)
});
});
}
async start() {
options.load(await server.get('options'));
this.showWidgets();
this.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);
$("body").append(rootContainer.render());
this.components = [
rootContainer,
new Entrypoints(),
new DialogEventComponent(this)
];
if (utils.isElectron()) {
this.components.push(new ZoomService(this));
import("./spell_check.js").then(spellCheckService => spellCheckService.initSpellCheck());
}
this.trigger('initialRenderComplete');
}
trigger(name, data, sync = false) {
this.eventReceived(name, data);
for (const component of this.components) {
component.eventReceived(name, data, sync);
}
}
async eventReceived(name, data, sync) {
const fun = this[name + 'Listener'];
if (typeof fun === 'function') {
await fun.call(this, data, sync);
}
}
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) {
if (tc.notePath && !tc.notePath.split("/").includes(hoistedNoteId)) {
this.removeTab(tc.tabId);
}
}
if (this.tabContexts.length === 0) {
this.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() {
await treeCache.loadInitialTree();
this.trigger('treeCacheReloaded');
}
}
const layout = new Layout();
const appContext = new AppContext(layout);
// we should save all outstanding changes before the page/app is closed
$(window).on('beforeunload', () => {
appContext.trigger('beforeUnload');
});
function isNotePathInAddress() {
const [notePath, tabId] = getHashValueFromAddress();
return notePath.startsWith("root")
// empty string is for empty/uninitialized tab
|| (notePath === '' && !!tabId);
}
function getHashValueFromAddress() {
const str = document.location.hash ? document.location.hash.substr(1) : ""; // strip initial #
return str.split("-");
}
$(window).on('hashchange', function() {
if (isNotePathInAddress()) {
const [notePath, tabId] = getHashValueFromAddress();
appContext.switchToTab(tabId, notePath);
}
});
export default appContext;