2018-03-26 01:41:29 +08:00
|
|
|
import treeService from './tree.js';
|
2019-05-11 03:43:40 +08:00
|
|
|
import TabContext from './tab_context.js';
|
2018-03-25 23:09:17 +08:00
|
|
|
import server from './server.js';
|
2018-03-26 09:16:57 +08:00
|
|
|
import messagingService from "./messaging.js";
|
2018-03-26 09:29:35 +08:00
|
|
|
import infoService from "./info.js";
|
2018-03-26 11:25:17 +08:00
|
|
|
import treeCache from "./tree_cache.js";
|
|
|
|
import NoteFull from "../entities/note_full.js";
|
2018-07-29 22:06:13 +08:00
|
|
|
import bundleService from "./bundle.js";
|
2018-12-24 17:10:36 +08:00
|
|
|
import utils from "./utils.js";
|
2019-02-27 04:37:15 +08:00
|
|
|
import importDialog from "../dialogs/import.js";
|
2019-05-08 03:34:01 +08:00
|
|
|
import contextMenuService from "./context_menu.js";
|
2019-05-09 02:14:41 +08:00
|
|
|
import treeUtils from "./tree_utils.js";
|
2019-05-12 01:44:58 +08:00
|
|
|
import tabRow from "./tab_row.js";
|
2019-05-05 16:59:34 +08:00
|
|
|
|
2019-05-09 01:55:24 +08:00
|
|
|
const $tabContentsContainer = $("#note-tab-container");
|
2019-05-04 03:50:14 +08:00
|
|
|
const $savedIndicator = $(".saved-indicator");
|
2018-03-25 23:09:17 +08:00
|
|
|
|
2019-01-02 02:32:34 +08:00
|
|
|
let detailLoadedListeners = [];
|
|
|
|
|
2018-03-25 23:09:17 +08:00
|
|
|
async function reload() {
|
|
|
|
// no saving here
|
2017-11-05 05:54:27 +08:00
|
|
|
|
2019-05-12 03:27:27 +08:00
|
|
|
await loadNoteDetail(getActiveTabContext().notePath);
|
2018-03-25 23:09:17 +08:00
|
|
|
}
|
2017-11-05 05:54:27 +08:00
|
|
|
|
2019-05-21 04:25:04 +08:00
|
|
|
async function reloadTab(tabContext) {
|
|
|
|
if (tabContext.note) {
|
2019-05-09 01:55:24 +08:00
|
|
|
const note = await loadNote(tabContext.note.noteId);
|
2019-05-06 02:45:07 +08:00
|
|
|
|
2019-05-09 02:14:41 +08:00
|
|
|
await loadNoteDetailToContext(tabContext, note, tabContext.notePath);
|
2019-05-06 02:45:07 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-21 04:25:04 +08:00
|
|
|
async function reloadAllTabs() {
|
|
|
|
for (const tabContext of tabContexts) {
|
|
|
|
await reloadTab(tabContext);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-12 03:27:27 +08:00
|
|
|
async function openInTab(notePath) {
|
|
|
|
await loadNoteDetail(notePath, { newTab: true });
|
2019-05-03 04:24:43 +08:00
|
|
|
}
|
2019-05-02 05:06:18 +08:00
|
|
|
|
2019-05-09 02:14:41 +08:00
|
|
|
async function switchToNote(notePath) {
|
|
|
|
await saveNotesIfChanged();
|
2017-11-05 05:54:27 +08:00
|
|
|
|
2019-05-09 02:14:41 +08:00
|
|
|
await loadNoteDetail(notePath);
|
2019-05-12 01:27:33 +08:00
|
|
|
|
|
|
|
openTabsChanged();
|
2018-03-25 23:09:17 +08:00
|
|
|
}
|
2017-11-05 05:54:27 +08:00
|
|
|
|
2018-09-03 22:05:28 +08:00
|
|
|
function onNoteChange(func) {
|
2019-05-12 03:27:27 +08:00
|
|
|
return getActiveTabContext().getComponent().onNoteChange(func);
|
2018-09-03 22:05:28 +08:00
|
|
|
}
|
|
|
|
|
2019-05-02 04:19:29 +08:00
|
|
|
async function saveNotesIfChanged() {
|
2019-05-09 01:55:24 +08:00
|
|
|
for (const ctx of tabContexts) {
|
2019-05-02 04:19:29 +08:00
|
|
|
await ctx.saveNoteIfChanged();
|
2018-12-28 03:22:33 +08:00
|
|
|
}
|
|
|
|
|
2019-05-02 04:19:29 +08:00
|
|
|
// make sure indicator is visible in a case there was some race condition.
|
|
|
|
$savedIndicator.fadeIn();
|
|
|
|
}
|
2017-11-05 05:54:27 +08:00
|
|
|
|
2019-05-09 01:55:24 +08:00
|
|
|
/** @type {TabContext[]} */
|
|
|
|
let tabContexts = [];
|
2018-04-08 20:21:49 +08:00
|
|
|
|
2019-05-19 15:13:13 +08:00
|
|
|
function getActiveEditor() {
|
|
|
|
const activeTabContext = getActiveTabContext();
|
|
|
|
|
|
|
|
if (activeTabContext && activeTabContext.note && activeTabContext.note.type === 'text') {
|
|
|
|
return activeTabContext.getComponent().getEditor();
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-07 17:15:55 +08:00
|
|
|
async function activateOrOpenNote(noteId) {
|
|
|
|
for (const tabContext of tabContexts) {
|
|
|
|
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
|
|
|
|
|
|
|
|
await loadNoteDetail(noteId, {
|
|
|
|
newTab: true,
|
|
|
|
activate: true
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-05-12 03:27:27 +08:00
|
|
|
function getTabContexts() {
|
|
|
|
return tabContexts;
|
2019-05-08 04:33:53 +08:00
|
|
|
}
|
|
|
|
|
2019-05-09 01:55:24 +08:00
|
|
|
/** @returns {TabContext} */
|
2019-05-12 03:27:27 +08:00
|
|
|
function getActiveTabContext() {
|
2019-05-12 18:58:55 +08:00
|
|
|
const activeTabEl = tabRow.activeTabEl;
|
|
|
|
|
|
|
|
if (!activeTabEl) {
|
|
|
|
return null;
|
2019-05-03 04:24:43 +08:00
|
|
|
}
|
2019-05-12 18:58:55 +08:00
|
|
|
|
|
|
|
const tabId = activeTabEl.getAttribute('data-tab-id');
|
|
|
|
|
|
|
|
return tabContexts.find(tc => tc.tabId === tabId);
|
2018-03-25 23:09:17 +08:00
|
|
|
}
|
2017-11-05 05:54:27 +08:00
|
|
|
|
2019-05-22 02:24:40 +08:00
|
|
|
/** @return {NoteFull} */
|
|
|
|
function getActiveNote() {
|
|
|
|
const activeContext = getActiveTabContext();
|
|
|
|
return activeContext ? activeContext.note : null;
|
2019-05-15 04:29:47 +08:00
|
|
|
}
|
|
|
|
|
2019-05-22 02:24:40 +08:00
|
|
|
function getActiveNoteId() {
|
|
|
|
const activeNote = getActiveNote();
|
|
|
|
|
|
|
|
return activeNote ? activeNote.noteId : null;
|
2019-05-15 04:29:47 +08:00
|
|
|
}
|
|
|
|
|
2019-05-22 02:24:40 +08:00
|
|
|
function getActiveNoteType() {
|
|
|
|
const activeNote = getActiveNote();
|
|
|
|
|
|
|
|
return activeNote ? activeNote.type : null;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function switchToTab(tabId, notePath) {
|
|
|
|
const tabContext = tabContexts.find(tc => tc.tabId === tabId);
|
|
|
|
|
|
|
|
if (!tabContext) {
|
|
|
|
await loadNoteDetail(notePath, {
|
|
|
|
newTab: true,
|
|
|
|
tabId: tabId,
|
|
|
|
activate: true
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
await tabContext.activate();
|
|
|
|
|
|
|
|
if (notePath && tabContext.notePath !== notePath) {
|
2019-05-22 04:35:01 +08:00
|
|
|
await treeService.activateNote(notePath);
|
2019-05-22 02:24:40 +08:00
|
|
|
}
|
|
|
|
}
|
2019-05-15 04:29:47 +08:00
|
|
|
}
|
|
|
|
|
2019-05-10 03:30:08 +08:00
|
|
|
async function showTab(tabId) {
|
2019-05-09 01:55:24 +08:00
|
|
|
for (const ctx of tabContexts) {
|
2019-05-15 04:29:47 +08:00
|
|
|
if (ctx.tabId === tabId) {
|
|
|
|
ctx.show();
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
ctx.hide();
|
|
|
|
}
|
2018-03-27 12:22:02 +08:00
|
|
|
}
|
2019-05-10 03:30:08 +08:00
|
|
|
|
|
|
|
const oldActiveNode = treeService.getActiveNode();
|
|
|
|
|
|
|
|
if (oldActiveNode) {
|
|
|
|
oldActiveNode.setActive(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
treeService.clearSelectedNodes();
|
|
|
|
|
2019-05-12 03:27:27 +08:00
|
|
|
const newActiveTabContext = getActiveTabContext();
|
2019-05-10 03:30:08 +08:00
|
|
|
|
2019-05-12 18:58:55 +08:00
|
|
|
if (newActiveTabContext && newActiveTabContext.notePath) {
|
|
|
|
const newActiveNode = await treeService.getNodeFromPath(newActiveTabContext.notePath);
|
|
|
|
|
|
|
|
if (newActiveNode && newActiveNode.isVisible()) {
|
|
|
|
newActiveNode.setActive(true, {noEvents: true});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function renderComponent(ctx) {
|
|
|
|
for (const componentType in ctx.components) {
|
|
|
|
if (componentType !== ctx.note.type) {
|
|
|
|
ctx.components[componentType].cleanup();
|
|
|
|
}
|
2019-05-10 03:30:08 +08:00
|
|
|
}
|
2019-05-12 18:58:55 +08:00
|
|
|
|
|
|
|
ctx.$noteDetailComponents.hide();
|
|
|
|
|
|
|
|
ctx.$noteTitle.show(); // this can be hidden by empty detail
|
|
|
|
ctx.$noteTitle.removeAttr("readonly"); // this can be set by protected session service
|
|
|
|
|
|
|
|
await ctx.getComponent().render();
|
2018-03-27 12:22:02 +08:00
|
|
|
}
|
|
|
|
|
2019-05-06 02:45:07 +08:00
|
|
|
/**
|
2019-05-09 01:55:24 +08:00
|
|
|
* @param {TabContext} ctx
|
2019-05-06 02:45:07 +08:00
|
|
|
* @param {NoteFull} note
|
|
|
|
*/
|
2019-05-09 02:14:41 +08:00
|
|
|
async function loadNoteDetailToContext(ctx, note, notePath) {
|
|
|
|
ctx.setNote(note, notePath);
|
2018-11-01 02:08:31 +08:00
|
|
|
|
2019-05-12 18:58:55 +08:00
|
|
|
openTabsChanged();
|
|
|
|
|
2018-12-24 17:10:36 +08:00
|
|
|
if (utils.isDesktop()) {
|
2019-03-21 05:28:54 +08:00
|
|
|
// needs to happen after loading the note itself because it references active noteId
|
2019-05-06 00:24:59 +08:00
|
|
|
ctx.attributes.refreshAttributes();
|
2019-05-06 02:45:07 +08:00
|
|
|
} else {
|
2018-12-25 06:08:43 +08:00
|
|
|
// mobile usually doesn't need attributes so we just invalidate
|
2019-05-06 00:24:59 +08:00
|
|
|
ctx.attributes.invalidateAttributes();
|
2018-12-25 06:08:43 +08:00
|
|
|
}
|
2018-02-13 12:53:00 +08:00
|
|
|
|
2019-05-04 20:34:03 +08:00
|
|
|
ctx.noteChangeDisabled = true;
|
2017-11-05 05:54:27 +08:00
|
|
|
|
2018-03-27 12:22:02 +08:00
|
|
|
try {
|
2019-05-02 04:19:29 +08:00
|
|
|
ctx.$noteTitle.val(ctx.note.title);
|
2017-12-25 22:46:11 +08:00
|
|
|
|
2018-12-24 17:10:36 +08:00
|
|
|
if (utils.isDesktop()) {
|
2019-05-05 04:44:25 +08:00
|
|
|
ctx.noteType.type(ctx.note.type);
|
|
|
|
ctx.noteType.mime(ctx.note.mime);
|
2018-12-24 17:10:36 +08:00
|
|
|
}
|
2017-11-05 05:54:27 +08:00
|
|
|
|
2019-05-12 18:58:55 +08:00
|
|
|
await renderComponent(ctx);
|
2019-05-06 02:45:07 +08:00
|
|
|
} finally {
|
2019-05-04 20:34:03 +08:00
|
|
|
ctx.noteChangeDisabled = false;
|
2018-03-25 23:09:17 +08:00
|
|
|
}
|
2018-01-22 12:36:09 +08:00
|
|
|
|
2019-05-06 02:45:07 +08:00
|
|
|
treeService.setBranchBackgroundBasedOnProtectedStatus(note.noteId);
|
2018-01-24 12:41:22 +08:00
|
|
|
|
2018-03-25 23:09:17 +08:00
|
|
|
// after loading new note make sure editor is scrolled to the top
|
2019-05-06 01:48:30 +08:00
|
|
|
ctx.getComponent().scrollToTop();
|
2018-01-24 12:41:22 +08:00
|
|
|
|
2019-01-02 02:32:34 +08:00
|
|
|
fireDetailLoaded();
|
|
|
|
|
2019-05-02 04:19:29 +08:00
|
|
|
ctx.$scriptArea.empty();
|
2018-07-30 02:51:28 +08:00
|
|
|
|
2019-05-30 03:48:48 +08:00
|
|
|
await bundleService.executeRelationBundles(ctx.note, 'runOnNoteView', ctx);
|
2018-08-07 18:48:11 +08:00
|
|
|
|
2019-05-05 04:44:25 +08:00
|
|
|
if (utils.isDesktop()) {
|
|
|
|
await ctx.attributes.showAttributes();
|
|
|
|
|
|
|
|
await ctx.showChildrenOverview();
|
|
|
|
}
|
2018-04-09 10:38:52 +08:00
|
|
|
}
|
|
|
|
|
2019-05-12 18:58:55 +08:00
|
|
|
async function loadNoteDetail(origNotePath, options = {}) {
|
2019-05-11 03:43:40 +08:00
|
|
|
const newTab = !!options.newTab;
|
|
|
|
const activate = !!options.activate;
|
|
|
|
|
2019-05-12 18:58:55 +08:00
|
|
|
const notePath = await treeService.resolveNotePath(origNotePath);
|
|
|
|
|
|
|
|
if (!notePath) {
|
|
|
|
console.error(`Cannot resolve note path ${origNotePath}`);
|
2019-06-02 02:32:11 +08:00
|
|
|
|
|
|
|
// fallback to display something
|
|
|
|
if (tabContexts.length === 0) {
|
|
|
|
await openEmptyTab();
|
|
|
|
}
|
|
|
|
|
2019-05-12 18:58:55 +08:00
|
|
|
return;
|
|
|
|
}
|
2019-05-12 03:27:27 +08:00
|
|
|
|
2019-05-09 02:14:41 +08:00
|
|
|
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
|
2019-05-06 02:45:07 +08:00
|
|
|
const loadedNote = await loadNote(noteId);
|
|
|
|
let ctx;
|
|
|
|
|
2019-05-15 04:29:47 +08:00
|
|
|
if (!getActiveTabContext() || newTab) {
|
2019-05-06 02:45:07 +08:00
|
|
|
// if it's a new tab explicitly by user then it's in background
|
2019-05-15 04:29:47 +08:00
|
|
|
ctx = new TabContext(tabRow, options.tabId);
|
2019-05-09 01:55:24 +08:00
|
|
|
tabContexts.push(ctx);
|
2019-05-06 02:45:07 +08:00
|
|
|
}
|
|
|
|
else {
|
2019-05-12 03:27:27 +08:00
|
|
|
ctx = getActiveTabContext();
|
2019-05-06 02:45:07 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// we will try to render the new note only if it's still the active one in the tree
|
|
|
|
// this is useful when user quickly switches notes (by e.g. holding down arrow) so that we don't
|
|
|
|
// try to render all those loaded notes one after each other. This only guarantees that correct note
|
|
|
|
// will be displayed independent of timing
|
|
|
|
const currentTreeNode = treeService.getActiveNode();
|
|
|
|
if (!newTab && currentTreeNode && currentTreeNode.data.noteId !== loadedNote.noteId) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-05-09 02:14:41 +08:00
|
|
|
await loadNoteDetailToContext(ctx, loadedNote, notePath);
|
2019-05-10 03:30:08 +08:00
|
|
|
|
2019-05-11 03:43:40 +08:00
|
|
|
if (activate) {
|
2019-05-10 03:30:08 +08:00
|
|
|
// will also trigger showTab via event
|
2019-05-15 04:29:47 +08:00
|
|
|
await tabRow.activateTab(ctx.$tab[0]);
|
2019-05-10 03:30:08 +08:00
|
|
|
}
|
2019-05-06 02:45:07 +08:00
|
|
|
}
|
|
|
|
|
2018-08-30 04:28:58 +08:00
|
|
|
async function loadNote(noteId) {
|
|
|
|
const row = await server.get('notes/' + noteId);
|
|
|
|
|
|
|
|
return new NoteFull(treeCache, row);
|
|
|
|
}
|
|
|
|
|
2019-05-12 01:27:33 +08:00
|
|
|
async function filterTabs(noteId) {
|
|
|
|
for (const tc of tabContexts) {
|
2019-05-12 03:27:27 +08:00
|
|
|
if (tc.notePath && !tc.notePath.split("/").includes(noteId)) {
|
2019-05-21 03:50:01 +08:00
|
|
|
await tabRow.removeTab(tc.$tab[0]);
|
2019-05-12 03:27:27 +08:00
|
|
|
}
|
2019-05-12 01:27:33 +08:00
|
|
|
}
|
|
|
|
|
2019-05-12 03:27:27 +08:00
|
|
|
if (tabContexts.length === 0) {
|
|
|
|
await loadNoteDetail(noteId, {
|
|
|
|
newTab: true,
|
|
|
|
activate: true
|
|
|
|
});
|
|
|
|
}
|
2019-05-12 01:27:33 +08:00
|
|
|
|
|
|
|
await saveOpenTabs();
|
|
|
|
}
|
|
|
|
|
2019-05-21 03:50:01 +08:00
|
|
|
async function noteDeleted(noteId) {
|
|
|
|
for (const tc of tabContexts) {
|
|
|
|
if (tc.notePath && tc.notePath.split("/").includes(noteId)) {
|
|
|
|
await tabRow.removeTab(tc.$tab[0]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-21 04:25:04 +08:00
|
|
|
async function refreshTabs(sourceTabId, noteId) {
|
|
|
|
for (const tc of tabContexts) {
|
|
|
|
if (tc.noteId === noteId && tc.tabId !== sourceTabId) {
|
|
|
|
await reloadTab(tc);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-07 16:50:05 +08:00
|
|
|
function focusOnTitle() {
|
2019-05-12 03:27:27 +08:00
|
|
|
getActiveTabContext().$noteTitle.focus();
|
2018-08-30 04:28:58 +08:00
|
|
|
}
|
|
|
|
|
2019-01-11 05:46:08 +08:00
|
|
|
function focusAndSelectTitle() {
|
2019-05-12 03:27:27 +08:00
|
|
|
getActiveTabContext().$noteTitle.focus().select();
|
2019-01-11 05:46:08 +08:00
|
|
|
}
|
|
|
|
|
2019-01-02 02:32:34 +08:00
|
|
|
/**
|
|
|
|
* Since detail loading may take some time and user might just browse through the notes using UP-DOWN keys,
|
|
|
|
* we intentionally decouple activation of the note in the tree and full load of the note so just avaiting on
|
|
|
|
* fancytree's activate() won't wait for the full load.
|
|
|
|
*
|
|
|
|
* This causes an issue where in some cases you want to do some action after detail is loaded. For this reason
|
|
|
|
* we provide the listeners here which will be triggered after the detail is loaded and if the loaded note
|
|
|
|
* is the one registered in the listener.
|
|
|
|
*/
|
|
|
|
function addDetailLoadedListener(noteId, callback) {
|
|
|
|
detailLoadedListeners.push({ noteId, callback });
|
|
|
|
}
|
|
|
|
|
|
|
|
function fireDetailLoaded() {
|
|
|
|
for (const {noteId, callback} of detailLoadedListeners) {
|
2019-05-02 04:19:29 +08:00
|
|
|
if (noteId === getActiveNoteId()) {
|
2019-01-02 02:32:34 +08:00
|
|
|
callback();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// all the listeners are one time only
|
|
|
|
detailLoadedListeners = [];
|
|
|
|
}
|
|
|
|
|
2018-08-30 04:28:58 +08:00
|
|
|
messagingService.subscribeToSyncMessages(syncData => {
|
2019-06-01 18:14:09 +08:00
|
|
|
const noteIdsToRefresh = new Set();
|
|
|
|
|
|
|
|
syncData
|
|
|
|
.filter(sync => sync.entityName === 'notes')
|
|
|
|
.forEach(sync => noteIdsToRefresh.add(sync.entityId));
|
|
|
|
|
|
|
|
syncData
|
|
|
|
.filter(sync => sync.entityName === 'attributes')
|
|
|
|
.forEach(sync => noteIdsToRefresh.add(sync.noteId));
|
|
|
|
|
|
|
|
for (const noteId of noteIdsToRefresh) {
|
|
|
|
refreshTabs(null, noteId);
|
2018-08-30 04:28:58 +08:00
|
|
|
}
|
2018-08-06 20:43:42 +08:00
|
|
|
});
|
|
|
|
|
2019-05-09 01:55:24 +08:00
|
|
|
$tabContentsContainer.on("dragover", e => e.preventDefault());
|
2019-02-27 04:37:15 +08:00
|
|
|
|
2019-05-09 01:55:24 +08:00
|
|
|
$tabContentsContainer.on("dragleave", e => e.preventDefault());
|
2019-02-27 04:37:15 +08:00
|
|
|
|
2019-05-09 01:55:24 +08:00
|
|
|
$tabContentsContainer.on("drop", e => {
|
2019-05-22 02:24:40 +08:00
|
|
|
const activeNote = getActiveNote();
|
|
|
|
|
|
|
|
if (!activeNote) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
importDialog.uploadFiles(activeNote.noteId, e.originalEvent.dataTransfer.files, {
|
2019-02-27 04:37:15 +08:00
|
|
|
safeImport: true,
|
|
|
|
shrinkImages: true,
|
|
|
|
textImportedAsText: true,
|
|
|
|
codeImportedAsCode: true,
|
|
|
|
explodeArchives: true
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2019-05-19 16:46:19 +08:00
|
|
|
async function openEmptyTab(render = true) {
|
2019-05-12 18:58:55 +08:00
|
|
|
const ctx = new TabContext(tabRow);
|
|
|
|
tabContexts.push(ctx);
|
|
|
|
|
2019-05-19 16:46:19 +08:00
|
|
|
if (render) {
|
|
|
|
await renderComponent(ctx);
|
|
|
|
}
|
2019-05-12 18:58:55 +08:00
|
|
|
|
2019-05-15 04:29:47 +08:00
|
|
|
await tabRow.activateTab(ctx.$tab[0]);
|
2019-05-12 23:28:20 +08:00
|
|
|
}
|
|
|
|
|
2019-05-13 03:45:30 +08:00
|
|
|
tabRow.addListener('newTab', openEmptyTab);
|
2019-05-12 18:58:55 +08:00
|
|
|
|
2019-05-12 03:27:27 +08:00
|
|
|
tabRow.addListener('activeTabChange', async ({ detail }) => {
|
2019-05-05 16:59:34 +08:00
|
|
|
const tabId = detail.tabEl.getAttribute('data-tab-id');
|
|
|
|
|
2019-05-12 03:27:27 +08:00
|
|
|
await showTab(tabId);
|
2019-05-05 16:59:34 +08:00
|
|
|
|
|
|
|
console.log(`Activated tab ${tabId}`);
|
|
|
|
});
|
|
|
|
|
2019-05-12 03:27:27 +08:00
|
|
|
tabRow.addListener('tabRemove', async ({ detail }) => {
|
2019-05-12 18:58:55 +08:00
|
|
|
const tabId = detail.tabEl.getAttribute('data-tab-id');
|
2019-05-05 16:59:34 +08:00
|
|
|
|
2019-06-25 02:45:35 +08:00
|
|
|
const tabContextToDelete = tabContexts.find(nc => nc.tabId === tabId);
|
2019-05-12 01:27:33 +08:00
|
|
|
|
2019-06-25 02:45:35 +08:00
|
|
|
if (tabContextToDelete) {
|
|
|
|
// sometimes there are orphan autocompletes after closing the tab
|
2019-06-27 03:08:54 +08:00
|
|
|
tabContextToDelete.closeAutocomplete();
|
2019-06-25 02:45:35 +08:00
|
|
|
|
|
|
|
await tabContextToDelete.saveNoteIfChanged();
|
|
|
|
tabContextToDelete.$tabContent.remove();
|
2019-05-12 01:27:33 +08:00
|
|
|
}
|
|
|
|
|
2019-05-09 01:55:24 +08:00
|
|
|
tabContexts = tabContexts.filter(nc => nc.tabId !== tabId);
|
2019-05-05 16:59:34 +08:00
|
|
|
|
|
|
|
console.log(`Removed tab ${tabId}`);
|
2019-05-13 03:45:30 +08:00
|
|
|
|
|
|
|
if (tabContexts.length === 0) {
|
|
|
|
openEmptyTab();
|
|
|
|
}
|
2019-05-05 16:59:34 +08:00
|
|
|
});
|
|
|
|
|
2019-05-12 01:44:58 +08:00
|
|
|
$(tabRow.el).on('contextmenu', '.note-tab', e => {
|
|
|
|
const tab = $(e.target).closest(".note-tab");
|
2019-05-08 03:34:01 +08:00
|
|
|
|
|
|
|
contextMenuService.initContextMenu(e, {
|
|
|
|
getContextMenuItems: () => {
|
|
|
|
return [
|
2019-05-16 03:50:27 +08:00
|
|
|
{title: "Close all tabs", cmd: "removeAllTabs", uiIcon: "empty"},
|
2019-05-08 03:34:01 +08:00
|
|
|
{title: "Close all tabs except for this", cmd: "removeAllTabsExceptForThis", uiIcon: "empty"}
|
|
|
|
];
|
|
|
|
},
|
|
|
|
selectContextMenuItem: (e, cmd) => {
|
2019-05-16 03:50:27 +08:00
|
|
|
if (cmd === 'removeAllTabs') {
|
|
|
|
tabRow.removeAllTabs();
|
|
|
|
} else if (cmd === 'removeAllTabsExceptForThis') {
|
2019-05-12 01:44:58 +08:00
|
|
|
tabRow.removeAllTabsExceptForThis(tab[0]);
|
2019-05-08 03:34:01 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2019-05-05 16:59:34 +08:00
|
|
|
if (utils.isElectron()) {
|
2019-05-12 23:28:20 +08:00
|
|
|
utils.bindShortcut('ctrl+t', () => {
|
2019-05-13 03:45:30 +08:00
|
|
|
openEmptyTab();
|
2019-05-12 23:28:20 +08:00
|
|
|
});
|
|
|
|
|
2019-05-05 16:59:34 +08:00
|
|
|
utils.bindShortcut('ctrl+w', () => {
|
2019-05-12 23:28:20 +08:00
|
|
|
if (tabRow.activeTabEl) {
|
|
|
|
tabRow.removeTab(tabRow.activeTabEl);
|
2019-05-05 16:59:34 +08:00
|
|
|
}
|
|
|
|
});
|
2019-05-08 03:04:07 +08:00
|
|
|
|
|
|
|
utils.bindShortcut('ctrl+tab', () => {
|
2019-05-12 01:44:58 +08:00
|
|
|
const nextTab = tabRow.nextTabEl;
|
2019-05-08 03:04:07 +08:00
|
|
|
|
|
|
|
if (nextTab) {
|
2019-05-15 04:29:47 +08:00
|
|
|
tabRow.activateTab(nextTab);
|
2019-05-08 03:04:07 +08:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
utils.bindShortcut('ctrl+shift+tab', () => {
|
2019-05-12 01:44:58 +08:00
|
|
|
const prevTab = tabRow.previousTabEl;
|
2019-05-08 03:04:07 +08:00
|
|
|
|
|
|
|
if (prevTab) {
|
2019-05-15 04:29:47 +08:00
|
|
|
tabRow.activateTab(prevTab);
|
2019-05-08 03:04:07 +08:00
|
|
|
}
|
|
|
|
});
|
2019-05-05 16:59:34 +08:00
|
|
|
}
|
2019-05-04 03:50:14 +08:00
|
|
|
|
2019-05-12 03:27:27 +08:00
|
|
|
tabRow.addListener('activeTabChange', openTabsChanged);
|
|
|
|
tabRow.addListener('tabRemove', openTabsChanged);
|
|
|
|
tabRow.addListener('tabReorder', openTabsChanged);
|
2019-05-11 03:43:40 +08:00
|
|
|
|
|
|
|
let tabsChangedTaskId = null;
|
|
|
|
|
|
|
|
function clearOpenTabsTask() {
|
|
|
|
if (tabsChangedTaskId) {
|
|
|
|
clearTimeout(tabsChangedTaskId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function openTabsChanged() {
|
|
|
|
// we don't want to send too many requests with tab changes so we always schedule task to do this in 3 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
|
|
|
|
clearOpenTabsTask();
|
|
|
|
|
|
|
|
tabsChangedTaskId = setTimeout(saveOpenTabs, 3000);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function saveOpenTabs() {
|
2019-05-12 01:44:58 +08:00
|
|
|
const activeTabEl = tabRow.activeTabEl;
|
2019-05-11 03:43:40 +08:00
|
|
|
const openTabs = [];
|
|
|
|
|
2019-05-12 01:44:58 +08:00
|
|
|
for (const tabEl of tabRow.tabEls) {
|
2019-05-12 18:58:55 +08:00
|
|
|
const tabId = tabEl.getAttribute('data-tab-id');
|
2019-05-11 03:43:40 +08:00
|
|
|
const tabContext = tabContexts.find(tc => tc.tabId === tabId);
|
|
|
|
|
2019-05-12 23:28:20 +08:00
|
|
|
if (tabContext && tabContext.notePath) {
|
2019-05-11 03:43:40 +08:00
|
|
|
openTabs.push({
|
2019-05-15 04:29:47 +08:00
|
|
|
tabId: tabContext.tabId,
|
2019-05-11 03:43:40 +08:00
|
|
|
notePath: tabContext.notePath,
|
|
|
|
active: activeTabEl === tabEl
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
await server.put('options', {
|
|
|
|
openTabs: JSON.stringify(openTabs)
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-06-13 01:59:52 +08:00
|
|
|
function noteChanged() {
|
|
|
|
const activeTabContext = getActiveTabContext();
|
|
|
|
|
|
|
|
if (activeTabContext) {
|
|
|
|
activeTabContext.noteChanged();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-27 10:29:14 +08:00
|
|
|
// this makes sure that when user e.g. reloads the page or navigates away from the page, the note's content is saved
|
|
|
|
// this sends the request asynchronously and doesn't wait for result
|
2019-05-02 04:19:29 +08:00
|
|
|
$(window).on('beforeunload', () => { saveNotesIfChanged(); }); // don't convert to short form, handler doesn't like returned promise
|
2018-03-27 10:29:14 +08:00
|
|
|
|
2019-05-02 04:19:29 +08:00
|
|
|
setInterval(saveNotesIfChanged, 3000);
|
2018-03-25 23:09:17 +08:00
|
|
|
|
|
|
|
export default {
|
|
|
|
reload,
|
2019-05-06 02:45:07 +08:00
|
|
|
reloadAllTabs,
|
2019-05-03 04:24:43 +08:00
|
|
|
openInTab,
|
2018-03-25 23:09:17 +08:00
|
|
|
switchToNote,
|
|
|
|
loadNote,
|
2019-05-08 04:33:53 +08:00
|
|
|
loadNoteDetail,
|
2019-03-15 03:21:27 +08:00
|
|
|
getActiveNote,
|
|
|
|
getActiveNoteType,
|
|
|
|
getActiveNoteId,
|
2018-09-07 16:50:05 +08:00
|
|
|
focusOnTitle,
|
2019-01-11 05:46:08 +08:00
|
|
|
focusAndSelectTitle,
|
2019-05-02 04:19:29 +08:00
|
|
|
saveNotesIfChanged,
|
2019-01-02 02:32:34 +08:00
|
|
|
onNoteChange,
|
2019-05-05 04:44:25 +08:00
|
|
|
addDetailLoadedListener,
|
2019-05-22 02:24:40 +08:00
|
|
|
switchToTab,
|
2019-05-12 03:27:27 +08:00
|
|
|
getTabContexts,
|
|
|
|
getActiveTabContext,
|
2019-05-19 15:13:13 +08:00
|
|
|
getActiveEditor,
|
2019-07-07 17:15:55 +08:00
|
|
|
activateOrOpenNote,
|
2019-05-12 01:27:33 +08:00
|
|
|
clearOpenTabsTask,
|
2019-05-19 16:46:19 +08:00
|
|
|
filterTabs,
|
2019-05-21 03:50:01 +08:00
|
|
|
openEmptyTab,
|
2019-05-21 04:25:04 +08:00
|
|
|
noteDeleted,
|
2019-06-13 01:59:52 +08:00
|
|
|
refreshTabs,
|
|
|
|
noteChanged
|
2018-03-25 23:09:17 +08:00
|
|
|
};
|