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

294 lines
8.4 KiB
JavaScript
Raw Normal View History

import treeService from './tree.js';
2019-05-02 04:19:29 +08:00
import NoteContext from './note_context.js';
import noteTypeService from './note_type.js';
import protectedSessionService from './protected_session.js';
import protectedSessionHolder from './protected_session_holder.js';
import server from './server.js';
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";
import bundleService from "./bundle.js";
import attributeService from "./attributes.js";
import utils from "./utils.js";
2019-02-27 04:37:15 +08:00
import importDialog from "../dialogs/import.js";
2019-05-03 04:24:43 +08:00
const $noteTabContentsContainer = $("#note-tab-container");
const $savedIndicator = $("#saved-indicator");
let noteChangeDisabled = false;
let detailLoadedListeners = [];
function getActiveNote() {
2019-05-02 04:19:29 +08:00
const activeContext = getActiveContext();
return activeContext ? activeContext.note : null;
}
function getActiveNoteId() {
2019-05-02 04:19:29 +08:00
const activeNote = getActiveNote();
return activeNote ? activeNote.noteId : null;
}
function getActiveNoteType() {
const activeNote = getActiveNote();
return activeNote ? activeNote.type : null;
}
async function reload() {
// no saving here
2017-11-05 05:54:27 +08:00
await loadNoteDetail(getActiveNoteId());
}
2017-11-05 05:54:27 +08:00
2019-05-03 04:24:43 +08:00
async function openInTab(noteId) {
await loadNoteDetail(noteId, true);
}
2019-05-02 05:06:18 +08:00
2019-05-03 04:24:43 +08:00
async function switchToNote(noteId) {
2019-05-02 05:06:18 +08:00
//if (getActiveNoteId() !== noteId) {
2019-05-02 04:19:29 +08:00
await saveNotesIfChanged();
2017-11-05 05:54:27 +08:00
await loadNoteDetail(noteId);
2019-05-02 05:06:18 +08:00
//}
}
2017-11-05 05:54:27 +08:00
function getActiveNoteContent() {
2019-05-02 04:19:29 +08:00
return getActiveContext().getComponent().getContent();
}
function onNoteChange(func) {
2019-05-02 04:19:29 +08:00
return getActiveContext().getComponent().onNoteChange(func);
}
2019-05-02 04:19:29 +08:00
async function saveNotesIfChanged() {
2019-05-03 04:24:43 +08:00
for (const ctx of noteContexts) {
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
2019-05-02 04:19:29 +08:00
async function handleProtectedSession() {
const newSessionCreated = await protectedSessionService.ensureProtectedSession(getActiveNote().isProtected, false);
2019-05-02 04:19:29 +08:00
if (getActiveNote().isProtected) {
2018-04-08 20:21:49 +08:00
protectedSessionHolder.touchProtectedSession();
}
2019-05-02 04:19:29 +08:00
// this might be important if we focused on protected note when not in protected note and we got a dialog
// to login, but we chose instead to come to another node - at that point the dialog is still visible and this will close it.
protectedSessionService.ensureDialogIsClosed();
2019-05-02 04:19:29 +08:00
return newSessionCreated;
}
2019-05-03 04:24:43 +08:00
/** @type {NoteContext[]} */
const noteContexts = [];
2018-04-08 20:21:49 +08:00
2019-05-02 04:19:29 +08:00
/** @returns {NoteContext} */
function getContext(noteId) {
2019-05-03 04:24:43 +08:00
const noteContext = noteContexts.find(nc => nc.noteId === noteId);
if (noteContext) {
return noteContext;
2019-05-02 04:19:29 +08:00
}
else {
throw new Error(`Can't find note context for ${noteId}`);
}
2018-04-08 20:21:49 +08:00
}
2019-05-02 04:19:29 +08:00
/** @returns {NoteContext} */
function getActiveContext() {
2019-05-03 04:24:43 +08:00
for (const ctx of noteContexts) {
if (ctx.$noteTabContent.is(":visible")) {
return ctx;
}
}
}
2017-11-05 05:54:27 +08:00
2019-05-02 04:19:29 +08:00
function showTab(noteId) {
2019-05-03 04:24:43 +08:00
for (const ctx of noteContexts) {
ctx.$noteTabContent.toggle(ctx.noteId === noteId);
}
}
2019-05-03 04:24:43 +08:00
async function loadNoteDetail(noteId, newTab = false) {
const loadedNote = await loadNote(noteId);
2019-05-03 04:24:43 +08:00
if (noteContexts.length === 0 || newTab) {
const tabContent = $("#note-tab-content-template").clone();
tabContent.removeAttr('id');
tabContent.attr('data-note-id', noteId);
$noteTabContentsContainer.append(tabContent);
noteContexts.push(new NoteContext(loadedNote));
}
const ctx = getActiveContext();
ctx.setNote(loadedNote);
// 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 (currentTreeNode && currentTreeNode.data.noteId !== loadedNote.noteId) {
return;
}
// only now that we're in sync with tree active node we will switch activeNote
2019-05-02 04:19:29 +08:00
ctx.note = loadedNote;
ctx.noteId = loadedNote.noteId;
if (utils.isDesktop()) {
// needs to happen after loading the note itself because it references active noteId
2019-05-02 04:19:29 +08:00
// FIXME
//attributeService.refreshAttributes();
}
2018-12-25 06:08:43 +08:00
else {
// mobile usually doesn't need attributes so we just invalidate
2019-05-02 04:19:29 +08:00
// FIXME
//attributeService.invalidateAttributes();
2018-12-25 06:08:43 +08:00
}
2019-05-02 04:19:29 +08:00
ctx.updateNoteView();
2019-05-02 04:19:29 +08:00
showTab(noteId);
2017-11-05 05:54:27 +08:00
noteChangeDisabled = true;
2017-11-05 05:54:27 +08:00
try {
2019-05-02 04:19:29 +08:00
ctx.$noteTitle.val(ctx.note.title);
if (utils.isDesktop()) {
2019-05-02 04:19:29 +08:00
noteTypeService.setNoteType(ctx.note.type);
noteTypeService.setNoteMime(ctx.note.mime);
}
2017-11-05 05:54:27 +08:00
2019-05-02 04:19:29 +08:00
for (const componentType in ctx.components) {
if (componentType !== ctx.note.type) {
ctx.components[componentType].cleanup();
}
}
2019-05-02 04:19:29 +08:00
ctx.$noteDetailComponents.hide();
const newSessionCreated = await handleProtectedSession();
if (newSessionCreated) {
// in such case we're reloading note anyway so no need to continue here.
return;
}
2019-05-02 04:19:29 +08:00
ctx.$noteTitle.removeAttr("readonly"); // this can be set by protected session service
2019-05-02 04:19:29 +08:00
await ctx.getComponent(ctx.note.type).show(ctx);
}
finally {
noteChangeDisabled = false;
}
treeService.setBranchBackgroundBasedOnProtectedStatus(noteId);
2018-01-24 12:41:22 +08:00
// after loading new note make sure editor is scrolled to the top
2019-05-02 04:19:29 +08:00
ctx.getComponent(ctx.note.type).scrollToTop();
2018-01-24 12:41:22 +08:00
fireDetailLoaded();
2019-05-02 04:19:29 +08:00
ctx.$scriptArea.empty();
await bundleService.executeRelationBundles(getActiveNote(), 'runOnNoteView');
if (utils.isDesktop()) {
await attributeService.showAttributes();
2019-05-02 04:19:29 +08:00
await ctx.showChildrenOverview();
}
}
async function loadNote(noteId) {
const row = await server.get('notes/' + noteId);
return new NoteFull(treeCache, row);
}
function focusOnTitle() {
2019-05-02 04:19:29 +08:00
getActiveContext().$noteTitle.focus();
}
function focusAndSelectTitle() {
2019-05-02 04:19:29 +08:00
getActiveContext().$noteTitle.focus().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) {
2019-05-02 04:19:29 +08:00
if (noteId === getActiveNoteId()) {
callback();
}
}
// all the listeners are one time only
detailLoadedListeners = [];
}
messagingService.subscribeToSyncMessages(syncData => {
if (syncData.some(sync => sync.entityName === 'notes' && sync.entityId === getActiveNoteId())) {
infoService.showMessage('Reloading note because of background changes');
reload();
}
});
2019-05-03 04:24:43 +08:00
$noteTabContentsContainer.on("dragover", e => e.preventDefault());
2019-02-27 04:37:15 +08:00
2019-05-03 04:24:43 +08:00
$noteTabContentsContainer.on("dragleave", e => e.preventDefault());
2019-02-27 04:37:15 +08:00
2019-05-03 04:24:43 +08:00
$noteTabContentsContainer.on("drop", e => {
importDialog.uploadFiles(getActiveNoteId(), e.originalEvent.dataTransfer.files, {
2019-02-27 04:37:15 +08:00
safeImport: true,
shrinkImages: true,
textImportedAsText: true,
codeImportedAsCode: true,
explodeArchives: true
});
});
// 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,
getActiveNote,
getActiveNoteContent,
getActiveNoteType,
getActiveNoteId,
focusOnTitle,
focusAndSelectTitle,
2019-05-02 04:19:29 +08:00
saveNotesIfChanged,
onNoteChange,
addDetailLoadedListener
};