2020-01-12 04:19:56 +08:00
|
|
|
import NoteTreeWidget from "../widgets/note_tree.js";
|
2020-01-12 19:30:30 +08:00
|
|
|
import TabContext from "./tab_context.js";
|
2020-01-12 19:48:17 +08:00
|
|
|
import server from "./server.js";
|
2020-01-15 04:52:18 +08:00
|
|
|
import TabRowWidget from "../widgets/tab_row.js";
|
2020-01-16 04:36:01 +08:00
|
|
|
import treeCache from "./tree_cache.js";
|
2020-01-20 04:12:53 +08:00
|
|
|
import bundleService from "./bundle.js";
|
2020-01-21 05:35:52 +08:00
|
|
|
import DialogEventComponent from "./dialog_events.js";
|
2020-01-22 05:54:16 +08:00
|
|
|
import Entrypoints from "./entrypoints.js";
|
2020-02-06 05:08:45 +08:00
|
|
|
import options from "./options.js";
|
2020-02-03 05:32:44 +08:00
|
|
|
import utils from "./utils.js";
|
|
|
|
import treeService from "./tree.js";
|
2020-02-06 05:08:45 +08:00
|
|
|
import ZoomService from "./zoom.js";
|
2020-02-07 04:47:31 +08:00
|
|
|
import Layout from "../widgets/layout.js";
|
2020-01-12 04:19:56 +08:00
|
|
|
|
2020-01-12 16:57:28 +08:00
|
|
|
class AppContext {
|
2020-02-07 04:47:31 +08:00
|
|
|
constructor(layout) {
|
|
|
|
this.layout = layout;
|
2020-01-21 05:35:52 +08:00
|
|
|
this.components = [];
|
2020-01-12 19:30:30 +08:00
|
|
|
/** @type {TabContext[]} */
|
|
|
|
this.tabContexts = [];
|
2020-01-12 19:48:17 +08:00
|
|
|
this.tabsChangedTaskId = null;
|
2020-01-13 02:05:09 +08:00
|
|
|
/** @type {TabRowWidget} */
|
|
|
|
this.tabRow = null;
|
2020-01-20 04:12:53 +08:00
|
|
|
this.activeTabId = null;
|
2020-01-13 02:05:09 +08:00
|
|
|
}
|
|
|
|
|
2020-02-06 05:08:45 +08:00
|
|
|
async start() {
|
|
|
|
options.load(await server.get('options'));
|
|
|
|
|
2020-02-03 05:04:28 +08:00
|
|
|
this.showWidgets();
|
|
|
|
|
2020-02-03 05:32:44 +08:00
|
|
|
this.loadTabs();
|
|
|
|
|
2020-02-03 05:04:28 +08:00
|
|
|
bundleService.executeStartupBundles();
|
|
|
|
}
|
2020-01-13 02:05:09 +08:00
|
|
|
|
2020-02-03 05:32:44 +08:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const tab of filteredTabs) {
|
|
|
|
const tabContext = this.openEmptyTab();
|
|
|
|
tabContext.setNote(tab.notePath);
|
|
|
|
|
|
|
|
if (tab.active) {
|
|
|
|
this.activateTab(tabContext.tabId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// previous opening triggered task to save tab changes but these are bogus changes (this is init)
|
|
|
|
// so we'll cancel it
|
|
|
|
this.clearOpenTabsTask();
|
|
|
|
}
|
|
|
|
|
2020-02-03 05:04:28 +08:00
|
|
|
showWidgets() {
|
2020-02-07 04:47:31 +08:00
|
|
|
const rootContainer = this.layout.getRootWidget(this);
|
2020-01-15 04:23:32 +08:00
|
|
|
|
2020-02-07 04:16:02 +08:00
|
|
|
$("body").append(rootContainer.render());
|
2020-02-06 05:08:45 +08:00
|
|
|
|
2020-01-21 05:35:52 +08:00
|
|
|
this.components = [
|
2020-02-07 04:16:02 +08:00
|
|
|
rootContainer,
|
2020-01-22 05:54:16 +08:00
|
|
|
new Entrypoints(),
|
2020-02-07 04:16:02 +08:00
|
|
|
new DialogEventComponent(this)
|
2020-01-14 04:48:44 +08:00
|
|
|
];
|
2020-02-06 05:08:45 +08:00
|
|
|
|
|
|
|
if (utils.isElectron()) {
|
|
|
|
this.components.push(new ZoomService(this));
|
|
|
|
|
|
|
|
import("./spell_check.js").then(spellCheckService => spellCheckService.initSpellCheck());
|
|
|
|
}
|
2020-02-07 04:16:02 +08:00
|
|
|
|
|
|
|
this.trigger('initialRenderComplete');
|
2020-01-12 04:19:56 +08:00
|
|
|
}
|
|
|
|
|
2020-01-20 03:18:02 +08:00
|
|
|
trigger(name, data, sync = false) {
|
2020-01-13 06:03:55 +08:00
|
|
|
this.eventReceived(name, data);
|
|
|
|
|
2020-01-22 05:54:16 +08:00
|
|
|
for (const component of this.components) {
|
|
|
|
component.eventReceived(name, data, sync);
|
2020-01-16 04:36:01 +08:00
|
|
|
}
|
2020-01-12 04:19:56 +08:00
|
|
|
}
|
|
|
|
|
2020-01-20 03:18:02 +08:00
|
|
|
async eventReceived(name, data, sync) {
|
2020-01-13 06:03:55 +08:00
|
|
|
const fun = this[name + 'Listener'];
|
|
|
|
|
|
|
|
if (typeof fun === 'function') {
|
2020-01-20 03:18:02 +08:00
|
|
|
await fun.call(this, data, sync);
|
2020-01-13 06:03:55 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-25 04:15:40 +08:00
|
|
|
tabNoteSwitchedListener({tabId}) {
|
|
|
|
if (tabId === this.activeTabId) {
|
|
|
|
this._setCurrentNotePathToHash();
|
|
|
|
}
|
2020-01-16 04:36:01 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
_setCurrentNotePathToHash() {
|
|
|
|
const activeTabContext = this.getActiveTabContext();
|
|
|
|
|
|
|
|
if (activeTabContext && activeTabContext.notePath) {
|
|
|
|
document.location.hash = (activeTabContext.notePath || "") + "-" + activeTabContext.tabId;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-12 19:30:30 +08:00
|
|
|
/** @return {TabContext[]} */
|
|
|
|
getTabContexts() {
|
|
|
|
return this.tabContexts;
|
|
|
|
}
|
|
|
|
|
2020-01-20 04:24:14 +08:00
|
|
|
/** @returns {TabContext} */
|
|
|
|
getTabContextById(tabId) {
|
|
|
|
return this.tabContexts.find(tc => tc.tabId === tabId);
|
|
|
|
}
|
|
|
|
|
2020-01-12 19:30:30 +08:00
|
|
|
/** @returns {TabContext} */
|
|
|
|
getActiveTabContext() {
|
2020-01-20 04:24:14 +08:00
|
|
|
return this.getTabContextById(this.activeTabId);
|
2020-01-12 19:30:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/** @returns {string|null} */
|
|
|
|
getActiveTabNotePath() {
|
|
|
|
const activeContext = this.getActiveTabContext();
|
|
|
|
return activeContext ? activeContext.notePath : null;
|
|
|
|
}
|
|
|
|
|
2020-02-01 18:15:58 +08:00
|
|
|
/** @return {NoteShort} */
|
2020-01-12 19:30:30 +08:00
|
|
|
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) {
|
2020-01-25 00:54:47 +08:00
|
|
|
const tabContext = this.tabContexts.find(tc => tc.tabId === tabId)
|
|
|
|
|| this.openEmptyTab();
|
2020-01-12 19:30:30 +08:00
|
|
|
|
2020-01-25 00:54:47 +08:00
|
|
|
this.activateTab(tabContext.tabId);
|
|
|
|
await tabContext.setNote(notePath);
|
2020-01-12 19:30:30 +08:00
|
|
|
}
|
|
|
|
|
2020-01-12 18:15:23 +08:00
|
|
|
/**
|
|
|
|
* @return {NoteTreeWidget}
|
|
|
|
*/
|
|
|
|
getMainNoteTree() {
|
|
|
|
return this.noteTreeWidget;
|
|
|
|
}
|
2020-01-12 19:30:30 +08:00
|
|
|
|
|
|
|
getTab(newTab, state) {
|
|
|
|
if (!this.getActiveTabContext() || newTab) {
|
|
|
|
// if it's a new tab explicitly by user then it's in background
|
2020-01-16 04:36:01 +08:00
|
|
|
const ctx = new TabContext(this, this.tabRow, state);
|
2020-01-12 19:30:30 +08:00
|
|
|
this.tabContexts.push(ctx);
|
2020-01-21 05:35:52 +08:00
|
|
|
this.components.push(ctx);
|
2020-01-12 19:30:30 +08:00
|
|
|
|
|
|
|
return ctx;
|
|
|
|
} else {
|
|
|
|
return this.getActiveTabContext();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-21 03:51:22 +08:00
|
|
|
async openAndActivateEmptyTab() {
|
|
|
|
const tabContext = this.openEmptyTab();
|
|
|
|
|
|
|
|
await this.activateTab(tabContext.tabId);
|
|
|
|
}
|
|
|
|
|
|
|
|
openEmptyTab() {
|
2020-01-20 04:12:53 +08:00
|
|
|
const tabContext = new TabContext(this, this.tabRow);
|
|
|
|
this.tabContexts.push(tabContext);
|
2020-01-21 05:35:52 +08:00
|
|
|
this.components.push(tabContext);
|
2020-01-21 03:51:22 +08:00
|
|
|
return tabContext;
|
2020-01-12 19:30:30 +08:00
|
|
|
}
|
2020-01-12 19:48:17 +08:00
|
|
|
|
2020-01-25 00:54:47 +08:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2020-02-03 05:04:28 +08:00
|
|
|
hoistedNoteChangedListener({hoistedNoteId}) {
|
|
|
|
if (hoistedNoteId === 'root') {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-01-12 19:48:17 +08:00
|
|
|
for (const tc of this.tabContexts) {
|
2020-02-03 05:04:28 +08:00
|
|
|
if (tc.notePath && !tc.notePath.split("/").includes(hoistedNoteId)) {
|
2020-01-16 05:27:52 +08:00
|
|
|
this.tabRow.removeTab(tc.tabId);
|
2020-01-12 19:48:17 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.tabContexts.length === 0) {
|
2020-02-03 05:04:28 +08:00
|
|
|
this.openAndActivateEmptyTab();
|
2020-01-12 19:48:17 +08:00
|
|
|
}
|
|
|
|
|
2020-02-03 05:04:28 +08:00
|
|
|
this.saveOpenTabs();
|
2020-01-12 19:48:17 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
async saveOpenTabs() {
|
|
|
|
const openTabs = [];
|
|
|
|
|
2020-01-20 04:12:53 +08:00
|
|
|
for (const tabId of this.tabRow.getTabIdsInOrder()) {
|
2020-01-12 19:48:17 +08:00
|
|
|
const tabContext = appContext.getTabContexts().find(tc => tc.tabId === tabId);
|
|
|
|
|
|
|
|
if (tabContext) {
|
|
|
|
const tabState = tabContext.getTabState();
|
|
|
|
|
|
|
|
if (tabState) {
|
|
|
|
openTabs.push(tabState);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
await server.put('options', {
|
|
|
|
openTabs: JSON.stringify(openTabs)
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
clearOpenTabsTask() {
|
|
|
|
if (this.tabsChangedTaskId) {
|
|
|
|
clearTimeout(this.tabsChangedTaskId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-25 00:54:47 +08:00
|
|
|
openTabsChangedListener() {
|
2020-01-12 19:48:17 +08:00
|
|
|
// we don't want to send too many requests with tab changes so we always schedule task to do this in 1 seconds,
|
|
|
|
// but if there's any change in between, we cancel the old one and schedule new one
|
|
|
|
// so effectively we kind of wait until user stopped e.g. quickly switching tabs
|
|
|
|
this.clearOpenTabsTask();
|
|
|
|
|
|
|
|
this.tabsChangedTaskId = setTimeout(() => this.saveOpenTabs(), 1000);
|
|
|
|
}
|
2020-01-12 16:57:28 +08:00
|
|
|
|
2020-01-25 00:54:47 +08:00
|
|
|
activateTab(tabId) {
|
2020-01-25 17:25:06 +08:00
|
|
|
if (tabId === this.activeTabId) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-01-25 03:15:53 +08:00
|
|
|
const oldActiveTabId = this.activeTabId;
|
|
|
|
|
2020-01-20 04:12:53 +08:00
|
|
|
this.activeTabId = tabId;
|
|
|
|
|
2020-01-25 05:30:17 +08:00
|
|
|
this.trigger('activeTabChanged', { oldActiveTabId, newActiveTabId: tabId });
|
2020-01-13 03:15:05 +08:00
|
|
|
}
|
|
|
|
|
2020-01-13 02:05:09 +08:00
|
|
|
newTabListener() {
|
2020-01-21 03:51:22 +08:00
|
|
|
this.openAndActivateEmptyTab();
|
2020-01-13 02:05:09 +08:00
|
|
|
}
|
2020-01-12 16:57:28 +08:00
|
|
|
|
2020-01-20 04:12:53 +08:00
|
|
|
async removeTab(tabId) {
|
|
|
|
const tabContextToRemove = this.tabContexts.find(tc => tc.tabId === tabId);
|
|
|
|
const tabIdsInOrder = this.tabRow.getTabIdsInOrder();
|
2020-01-12 19:30:30 +08:00
|
|
|
|
2020-01-20 04:12:53 +08:00
|
|
|
if (!tabContextToRemove) {
|
|
|
|
return;
|
|
|
|
}
|
2020-01-12 19:30:30 +08:00
|
|
|
|
2020-01-13 02:05:09 +08:00
|
|
|
if (this.tabContexts.length === 0) {
|
2020-01-21 03:51:22 +08:00
|
|
|
this.openAndActivateEmptyTab();
|
2020-01-13 02:05:09 +08:00
|
|
|
}
|
2020-01-20 04:12:53 +08:00
|
|
|
else {
|
|
|
|
const oldIdx = tabIdsInOrder.findIndex(tid => tid === tabId);
|
2020-01-21 03:51:22 +08:00
|
|
|
const newActiveTabId = tabIdsInOrder[oldIdx === tabIdsInOrder.length - 1 ? oldIdx - 1 : oldIdx + 1];
|
2020-01-20 04:12:53 +08:00
|
|
|
|
|
|
|
if (newActiveTabId) {
|
|
|
|
this.activateTab(newActiveTabId);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
console.log("Failed to find next tabcontext to activate");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
await tabContextToRemove.remove();
|
|
|
|
|
|
|
|
this.tabContexts = this.tabContexts.filter(tc => tc.tabId === tabId);
|
2020-01-12 19:30:30 +08:00
|
|
|
|
2020-01-25 00:54:47 +08:00
|
|
|
this.openTabsChangedListener();
|
2020-01-12 19:30:30 +08:00
|
|
|
}
|
|
|
|
|
2020-01-13 02:05:09 +08:00
|
|
|
tabReorderListener() {
|
2020-01-25 00:54:47 +08:00
|
|
|
this.openTabsChangedListener();
|
2020-01-13 02:05:09 +08:00
|
|
|
}
|
2020-01-20 03:18:02 +08:00
|
|
|
|
2020-01-21 03:51:22 +08:00
|
|
|
activateNextTabListener() {
|
|
|
|
const tabIdsInOrder = this.tabRow.getTabIdsInOrder();
|
|
|
|
const oldIdx = tabIdsInOrder.findIndex(tid => tid === this.activeTabId);
|
|
|
|
const newActiveTabId = tabIdsInOrder[oldIdx === tabIdsInOrder.length - 1 ? 0 : oldIdx + 1];
|
|
|
|
|
|
|
|
this.activateTab(newActiveTabId);
|
|
|
|
}
|
|
|
|
|
|
|
|
activatePreviousTabListener() {
|
|
|
|
const tabIdsInOrder = this.tabRow.getTabIdsInOrder();
|
|
|
|
const oldIdx = tabIdsInOrder.findIndex(tid => tid === this.activeTabId);
|
|
|
|
const newActiveTabId = tabIdsInOrder[oldIdx === 0 ? tabIdsInOrder.length - 1 : oldIdx - 1];
|
|
|
|
|
|
|
|
this.activateTab(newActiveTabId);
|
|
|
|
}
|
|
|
|
|
|
|
|
closeActiveTabListener() {
|
|
|
|
this.removeTab(this.activeTabId);
|
|
|
|
}
|
|
|
|
|
|
|
|
openNewTabListener() {
|
|
|
|
this.openAndActivateEmptyTab();
|
|
|
|
}
|
2020-01-21 05:35:52 +08:00
|
|
|
|
|
|
|
removeAllTabsListener() {
|
|
|
|
// TODO
|
|
|
|
}
|
|
|
|
|
|
|
|
removeAllTabsExceptForThis() {
|
|
|
|
// TODO
|
|
|
|
}
|
2020-02-02 05:29:32 +08:00
|
|
|
|
|
|
|
async protectedSessionStartedListener() {
|
|
|
|
await treeCache.loadInitialTree();
|
|
|
|
|
|
|
|
this.trigger('treeCacheReloaded');
|
|
|
|
}
|
2020-01-13 02:05:09 +08:00
|
|
|
}
|
|
|
|
|
2020-02-07 04:47:31 +08:00
|
|
|
const layout = new Layout();
|
|
|
|
|
|
|
|
const appContext = new AppContext(layout);
|
2020-01-12 19:48:17 +08:00
|
|
|
|
2020-02-02 17:41:43 +08:00
|
|
|
// we should save all outstanding changes before the page/app is closed
|
|
|
|
$(window).on('beforeunload', () => {
|
|
|
|
appContext.trigger('beforeUnload');
|
|
|
|
});
|
|
|
|
|
2020-02-04 03:07:34 +08:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-01-12 16:57:28 +08:00
|
|
|
export default appContext;
|