trilium/src/public/javascripts/services/note_detail.js

376 lines
11 KiB
JavaScript
Raw Normal View History

import treeService from './tree.js';
2019-05-11 03:43:40 +08:00
import TabContext from './tab_context.js';
import server from './server.js';
2019-08-27 02:21:43 +08:00
import ws from "./ws.js";
2018-03-26 11:25:17 +08:00
import treeCache from "./tree_cache.js";
import NoteFull from "../entities/note_full.js";
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-11-24 16:50:19 +08:00
import keyboardActionService from "./keyboard_actions.js";
2020-01-12 18:15:23 +08:00
import appContext from "./app_context.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");
let detailLoadedListeners = [];
async function reload() {
// no saving here
2017-11-05 05:54:27 +08:00
2020-01-12 19:30:30 +08:00
await loadNoteDetail(appContext.getActiveTabNotePath());
}
2017-11-05 05:54:27 +08:00
2020-01-12 19:30:30 +08:00
async function reloadNote(tabContext) {
const note = await loadNote(tabContext.note.noteId);
2020-01-12 19:30:30 +08:00
await loadNoteDetailToContext(tabContext, note, tabContext.notePath);
2019-05-21 04:25:04 +08:00
}
async function openInTab(notePath, activate) {
await loadNoteDetail(notePath, { newTab: true, activate });
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();
}
2017-11-05 05:54:27 +08:00
function onNoteChange(func) {
2020-01-12 19:37:44 +08:00
return appContext.getActiveTabContext().getComponent().onNoteChange(func);
}
2019-05-02 04:19:29 +08:00
async function saveNotesIfChanged() {
2020-01-12 19:30:30 +08:00
for (const ctx of appContext.getTabContexts()) {
2019-05-02 04:19:29 +08:00
await ctx.saveNoteIfChanged();
}
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
function getActiveEditor() {
2020-01-12 19:37:44 +08:00
const activeTabContext = appContext.getActiveTabContext();
if (activeTabContext && activeTabContext.note && activeTabContext.note.type === 'text') {
return activeTabContext.getComponent().getEditor();
}
else {
return null;
}
}
async function activateOrOpenNote(noteId) {
2020-01-12 19:37:44 +08:00
for (const tabContext of appContext.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
await loadNoteDetail(noteId, {
newTab: true,
activate: true
});
}
/**
2019-05-09 01:55:24 +08:00
* @param {TabContext} ctx
* @param {NoteFull} note
2019-09-05 04:13:22 +08:00
* @param {string} notePath
*/
2019-05-09 02:14:41 +08:00
async function loadNoteDetailToContext(ctx, note, notePath) {
2019-08-27 01:49:19 +08:00
await ctx.setNote(note, notePath);
2019-05-12 18:58:55 +08:00
openTabsChanged();
fireDetailLoaded();
}
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;
let notePath = await treeService.resolveNotePath(origNotePath);
2019-05-12 18:58:55 +08:00
if (!notePath) {
console.error(`Cannot resolve note path ${origNotePath}`);
// fallback to display something
notePath = 'root';
2019-05-12 18:58:55 +08:00
}
2019-05-09 02:14:41 +08:00
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
const loadedNote = await loadNote(noteId);
2020-01-12 19:30:30 +08:00
const ctx = appContext.getTab(newTab, options.state);
// 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
2020-01-12 18:15:23 +08:00
const currentTreeNode = appContext.getMainNoteTree().getActiveNode();
if (!newTab && currentTreeNode && currentTreeNode.data.noteId !== loadedNote.noteId) {
return;
}
const loadPromise = loadNoteDetailToContext(ctx, loadedNote, notePath).then(() => {
if (activate) {
// will also trigger showTab via event
return tabRow.activateTab(ctx.$tab[0]);
}
else {
return Promise.resolve();
}
});
if (!options.async) {
await loadPromise;
}
}
async function loadNote(noteId) {
const row = await server.get('notes/' + noteId);
2019-10-26 15:51:08 +08:00
const noteShort = await treeCache.getNote(noteId);
return new NoteFull(treeCache, row, noteShort);
}
2019-05-12 01:27:33 +08:00
async function filterTabs(noteId) {
2020-01-12 19:37:44 +08:00
for (const tc of appContext.getTabContexts()) {
if (tc.notePath && !tc.notePath.split("/").includes(noteId)) {
await tabRow.removeTab(tc.$tab[0]);
}
2019-05-12 01:27:33 +08:00
}
2020-01-12 19:37:44 +08:00
if (appContext.getTabContexts().length === 0) {
await loadNoteDetail(noteId, {
newTab: true,
activate: true
});
}
2019-05-12 01:27:33 +08:00
await saveOpenTabs();
}
async function noteDeleted(noteId) {
2020-01-12 19:37:44 +08:00
for (const tc of appContext.getTabContexts()) {
// not removing active even if it contains deleted note since that one will move to another note (handled by deletion logic)
// and we would lose tab context state (e.g. sidebar visibility)
if (!tc.isActive() && tc.notePath && tc.notePath.split("/").includes(noteId)) {
await tabRow.removeTab(tc.$tab[0]);
}
}
}
function focusOnTitle() {
2020-01-12 19:37:44 +08:00
appContext.getActiveTabContext().$noteTitle.trigger('focus');
}
function focusAndSelectTitle() {
2020-01-12 19:37:44 +08:00
appContext.getActiveTabContext()
2019-11-10 00:45:22 +08:00
.$noteTitle
.trigger('focus')
.trigger('select');
}
/**
* 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) {
2020-01-12 19:37:44 +08:00
if (noteId === appContext.getActiveTabNoteId()) {
callback();
}
}
// all the listeners are one time only
detailLoadedListeners = [];
}
2019-08-27 02:21:43 +08:00
ws.subscribeToOutsideSyncMessages(syncData => {
const noteIdsToRefresh = new Set();
syncData
.filter(sync => sync.entityName === 'notes')
.forEach(sync => noteIdsToRefresh.add(sync.entityId));
// we need to reload because of promoted attributes
syncData
.filter(sync => sync.entityName === 'attributes')
.forEach(sync => noteIdsToRefresh.add(sync.noteId));
for (const noteId of noteIdsToRefresh) {
2020-01-12 19:30:30 +08:00
appContext.refreshTabs(null, noteId);
}
});
2019-08-27 02:21:43 +08:00
ws.subscribeToAllSyncMessages(syncData => {
2020-01-12 19:37:44 +08:00
for (const tc of appContext.getTabContexts()) {
tc.eventReceived('syncData', syncData);
}
});
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
$tabContentsContainer.on("drop", async e => {
2020-01-12 19:37:44 +08:00
const activeNote = appContext.getActiveTabNote();
2019-05-22 02:24:40 +08:00
if (!activeNote) {
return;
}
const files = [...e.originalEvent.dataTransfer.files]; // chrome has issue that dataTransfer.files empties after async operation
const importService = await import("./import.js");
importService.uploadFiles(activeNote.noteId, files, {
2019-02-27 04:37:15 +08:00
safeImport: true,
shrinkImages: true,
textImportedAsText: true,
codeImportedAsCode: true,
explodeArchives: true
});
});
2019-05-12 01:44:58 +08:00
$(tabRow.el).on('contextmenu', '.note-tab', e => {
e.preventDefault();
2019-05-12 01:44:58 +08:00
const tab = $(e.target).closest(".note-tab");
contextMenuService.initContextMenu(e, {
getContextMenuItems: () => {
return [
{title: "Close all tabs", cmd: "removeAllTabs", uiIcon: "empty"},
{title: "Close all tabs except for this", cmd: "removeAllTabsExceptForThis", uiIcon: "empty"}
];
},
selectContextMenuItem: (e, cmd) => {
if (cmd === 'removeAllTabs') {
tabRow.removeAllTabs();
} else if (cmd === 'removeAllTabsExceptForThis') {
2019-05-12 01:44:58 +08:00
tabRow.removeAllTabsExceptForThis(tab[0]);
}
}
});
});
keyboardActionService.setGlobalActionHandler('OpenNewTab', () => {
2019-11-24 16:50:19 +08:00
openEmptyTab();
});
2019-05-12 23:28:20 +08:00
keyboardActionService.setGlobalActionHandler('CloseActiveTab', () => {
2019-11-24 16:50:19 +08:00
if (tabRow.activeTabEl) {
tabRow.removeTab(tabRow.activeTabEl);
}
});
keyboardActionService.setGlobalActionHandler('ActivateNextTab', () => {
2019-11-24 16:50:19 +08:00
const nextTab = tabRow.nextTabEl;
2019-11-24 16:50:19 +08:00
if (nextTab) {
tabRow.activateTab(nextTab);
}
});
keyboardActionService.setGlobalActionHandler('ActivatePreviousTab', () => {
2019-11-24 16:50:19 +08:00
const prevTab = tabRow.previousTabEl;
2019-11-24 16:50:19 +08:00
if (prevTab) {
tabRow.activateTab(prevTab);
}
});
2019-05-04 03:50:14 +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() {
2019-08-17 03:52:36 +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,
2019-05-11 03:43:40 +08:00
// 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();
2019-08-17 03:52:36 +08:00
tabsChangedTaskId = setTimeout(saveOpenTabs, 1000);
2019-05-11 03:43:40 +08:00
}
async function saveOpenTabs() {
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');
2020-01-12 19:30:30 +08:00
const tabContext = appContext.getTabContexts().find(tc => tc.tabId === tabId);
2019-05-11 03:43:40 +08:00
2019-08-15 16:04:03 +08:00
if (tabContext) {
const tabState = tabContext.getTabState();
if (tabState) {
openTabs.push(tabState);
}
2019-05-11 03:43:40 +08:00
}
}
await server.put('options', {
openTabs: JSON.stringify(openTabs)
});
}
2019-06-13 01:59:52 +08:00
function noteChanged() {
2020-01-12 19:37:44 +08:00
const activeTabContext = appContext.getActiveTabContext();
2019-06-13 01:59:52 +08:00
if (activeTabContext) {
activeTabContext.noteChanged();
}
}
// 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
2019-05-02 04:19:29 +08:00
setInterval(saveNotesIfChanged, 3000);
export default {
reload,
2019-05-03 04:24:43 +08:00
openInTab,
switchToNote,
loadNote,
loadNoteDetail,
focusOnTitle,
focusAndSelectTitle,
2019-05-02 04:19:29 +08:00
saveNotesIfChanged,
onNoteChange,
addDetailLoadedListener,
getActiveEditor,
activateOrOpenNote,
2019-05-12 01:27:33 +08:00
clearOpenTabsTask,
filterTabs,
2019-05-21 04:25:04 +08:00
noteDeleted,
2019-08-15 16:04:03 +08:00
noteChanged,
2020-01-12 19:30:30 +08:00
openTabsChanged,
reloadNote
};