mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
tabs wip
This commit is contained in:
parent
c9183f8bd4
commit
7e03f14e01
153
src/public/javascripts/services/note_context.js
Normal file
153
src/public/javascripts/services/note_context.js
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import treeService from "./tree";
|
||||||
|
import protectedSessionHolder from "./protected_session_holder";
|
||||||
|
import server from "./server";
|
||||||
|
import bundleService from "./bundle";
|
||||||
|
import attributeService from "./attributes";
|
||||||
|
import treeUtils from "./tree_utils";
|
||||||
|
import utils from "./utils";
|
||||||
|
import noteDetailCode from "./note_detail_code";
|
||||||
|
import noteDetailText from "./note_detail_text";
|
||||||
|
import noteDetailFile from "./note_detail_file";
|
||||||
|
import noteDetailImage from "./note_detail_image";
|
||||||
|
import noteDetailSearch from "./note_detail_search";
|
||||||
|
import noteDetailRender from "./note_detail_render";
|
||||||
|
import noteDetailRelationMap from "./note_detail_relation_map";
|
||||||
|
|
||||||
|
const componentClasses = {
|
||||||
|
'code': noteDetailCode,
|
||||||
|
'text': noteDetailText,
|
||||||
|
'file': noteDetailFile,
|
||||||
|
'image': noteDetailImage,
|
||||||
|
'search': noteDetailSearch,
|
||||||
|
'render': noteDetailRender,
|
||||||
|
'relation-map': noteDetailRelationMap
|
||||||
|
};
|
||||||
|
|
||||||
|
class NoteContext {
|
||||||
|
constructor(noteId) {
|
||||||
|
/** @type {NoteFull} */
|
||||||
|
this.note = null;
|
||||||
|
this.noteId = noteId;
|
||||||
|
this.$noteTab = $noteTabsContainer.find(`[data-note-id="${noteId}"]`);
|
||||||
|
this.$noteTitle = this.$noteTab.find(".note-title");
|
||||||
|
this.$noteDetailComponents = this.$noteTab.find(".note-detail-component");
|
||||||
|
this.$protectButton = this.$noteTab.find(".protect-button");
|
||||||
|
this.$unprotectButton = this.$noteTab.find(".unprotect-button");
|
||||||
|
this.$childrenOverview = this.$noteTab.find(".children-overview");
|
||||||
|
this.$scriptArea = this.$noteTab.find(".note-detail-script-area");
|
||||||
|
this.isNoteChanged = false;
|
||||||
|
this.components = {};
|
||||||
|
|
||||||
|
this.$noteTitle.on('input', () => {
|
||||||
|
this.noteChanged();
|
||||||
|
|
||||||
|
const title = this.$noteTitle.val();
|
||||||
|
|
||||||
|
treeService.setNoteTitle(this.noteId, title);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getComponent(type) {
|
||||||
|
if (!type) {
|
||||||
|
type = this.note.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(type in this.components)) {
|
||||||
|
this.components[type] = new componentClasses[type](this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.components[type];
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveNote() {
|
||||||
|
if (this.note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.note.title = this.$noteTitle.val();
|
||||||
|
this.note.content = getActiveNoteContent(this.note);
|
||||||
|
|
||||||
|
// it's important to set the flag back to false immediatelly after retrieving title and content
|
||||||
|
// otherwise we might overwrite another change (especially async code)
|
||||||
|
this.isNoteChanged = false;
|
||||||
|
|
||||||
|
treeService.setNoteTitle(this.note.noteId, this.note.title);
|
||||||
|
|
||||||
|
await server.put('notes/' + this.note.noteId, this.note.dto);
|
||||||
|
|
||||||
|
if (this.note.isProtected) {
|
||||||
|
protectedSessionHolder.touchProtectedSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
$savedIndicator.fadeIn();
|
||||||
|
|
||||||
|
// run async
|
||||||
|
bundleService.executeRelationBundles(getActiveNote(), 'runOnNoteChange');
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveNoteIfChanged() {
|
||||||
|
if (this.isNoteChanged) {
|
||||||
|
await this.saveNote();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
noteChanged() {
|
||||||
|
if (noteChangeDisabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isNoteChanged = true;
|
||||||
|
|
||||||
|
$savedIndicator.fadeOut();
|
||||||
|
}
|
||||||
|
|
||||||
|
async showChildrenOverview() {
|
||||||
|
return; // FIXME
|
||||||
|
|
||||||
|
const attributes = await attributeService.getAttributes();
|
||||||
|
const hideChildrenOverview = attributes.some(attr => attr.type === 'label' && attr.name === 'hideChildrenOverview')
|
||||||
|
|| this.note.type === 'relation-map'
|
||||||
|
|| this.note.type === 'image'
|
||||||
|
|| this.note.type === 'file';
|
||||||
|
|
||||||
|
if (hideChildrenOverview) {
|
||||||
|
this.$childrenOverview.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$childrenOverview.empty();
|
||||||
|
|
||||||
|
const notePath = await treeService.getActiveNotePath();
|
||||||
|
|
||||||
|
for (const childBranch of await this.note.getChildBranches()) {
|
||||||
|
const link = $('<a>', {
|
||||||
|
href: 'javascript:',
|
||||||
|
text: await treeUtils.getNoteTitle(childBranch.noteId, childBranch.parentNoteId)
|
||||||
|
}).attr('data-action', 'note').attr('data-note-path', notePath + '/' + childBranch.noteId);
|
||||||
|
|
||||||
|
const childEl = $('<div class="child-overview-item">').html(link);
|
||||||
|
this.$childrenOverview.append(childEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$childrenOverview.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNoteView() {
|
||||||
|
this.$noteTab.toggleClass("protected", this.note.isProtected);
|
||||||
|
this.$protectButton.toggleClass("active", this.note.isProtected);
|
||||||
|
this.$protectButton.prop("disabled", this.note.isProtected);
|
||||||
|
this.$unprotectButton.toggleClass("active", !this.note.isProtected);
|
||||||
|
this.$unprotectButton.prop("disabled", !this.note.isProtected || !protectedSessionHolder.isProtectedSessionAvailable());
|
||||||
|
|
||||||
|
for (const clazz of Array.from(this.$noteTab[0].classList)) { // create copy to safely iterate over while removing classes
|
||||||
|
if (clazz.startsWith("type-") || clazz.startsWith("mime-")) {
|
||||||
|
this.$noteTab.removeClass(clazz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$noteTab.addClass(utils.getNoteTypeClass(this.note.type));
|
||||||
|
this.$noteTab.addClass(utils.getMimeTypeClass(this.note.mime));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NoteContext;
|
@ -1,5 +1,5 @@
|
|||||||
import treeService from './tree.js';
|
import treeService from './tree.js';
|
||||||
import treeUtils from './tree_utils.js';
|
import NoteContext from './note_context.js';
|
||||||
import noteTypeService from './note_type.js';
|
import noteTypeService from './note_type.js';
|
||||||
import protectedSessionService from './protected_session.js';
|
import protectedSessionService from './protected_session.js';
|
||||||
import protectedSessionHolder from './protected_session_holder.js';
|
import protectedSessionHolder from './protected_session_holder.js';
|
||||||
@ -8,67 +8,26 @@ import messagingService from "./messaging.js";
|
|||||||
import infoService from "./info.js";
|
import infoService from "./info.js";
|
||||||
import treeCache from "./tree_cache.js";
|
import treeCache from "./tree_cache.js";
|
||||||
import NoteFull from "../entities/note_full.js";
|
import NoteFull from "../entities/note_full.js";
|
||||||
import noteDetailCode from './note_detail_code.js';
|
|
||||||
import noteDetailText from './note_detail_text.js';
|
|
||||||
import noteDetailFile from './note_detail_file.js';
|
|
||||||
import noteDetailImage from './note_detail_image.js';
|
|
||||||
import noteDetailSearch from './note_detail_search.js';
|
|
||||||
import noteDetailRender from './note_detail_render.js';
|
|
||||||
import noteDetailRelationMap from './note_detail_relation_map.js';
|
|
||||||
import bundleService from "./bundle.js";
|
import bundleService from "./bundle.js";
|
||||||
import attributeService from "./attributes.js";
|
import attributeService from "./attributes.js";
|
||||||
import utils from "./utils.js";
|
import utils from "./utils.js";
|
||||||
import importDialog from "../dialogs/import.js";
|
import importDialog from "../dialogs/import.js";
|
||||||
|
|
||||||
const $noteTitle = $("#note-title");
|
|
||||||
|
|
||||||
const $noteDetailComponents = $(".note-detail-component");
|
|
||||||
|
|
||||||
const $protectButton = $("#protect-button");
|
|
||||||
const $unprotectButton = $("#unprotect-button");
|
|
||||||
const $noteTabContent = $(".note-tab-content");
|
|
||||||
const $noteTabsContainer = $("#note-tab-container");
|
const $noteTabsContainer = $("#note-tab-container");
|
||||||
const $childrenOverview = $("#children-overview");
|
|
||||||
const $scriptArea = $("#note-detail-script-area");
|
|
||||||
const $savedIndicator = $("#saved-indicator");
|
const $savedIndicator = $("#saved-indicator");
|
||||||
const $body = $("body");
|
|
||||||
|
|
||||||
let activeNote = null;
|
|
||||||
|
|
||||||
let noteChangeDisabled = false;
|
let noteChangeDisabled = false;
|
||||||
|
|
||||||
let isNoteChanged = false;
|
|
||||||
|
|
||||||
let detailLoadedListeners = [];
|
let detailLoadedListeners = [];
|
||||||
|
|
||||||
const components = {
|
|
||||||
'code': noteDetailCode,
|
|
||||||
'text': noteDetailText,
|
|
||||||
'file': noteDetailFile,
|
|
||||||
'image': noteDetailImage,
|
|
||||||
'search': noteDetailSearch,
|
|
||||||
'render': noteDetailRender,
|
|
||||||
'relation-map': noteDetailRelationMap
|
|
||||||
};
|
|
||||||
|
|
||||||
function getComponent(type) {
|
|
||||||
if (!type) {
|
|
||||||
type = getActiveNote().type;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (components[type]) {
|
|
||||||
return components[type];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
infoService.throwError("Unrecognized type: " + type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getActiveNote() {
|
function getActiveNote() {
|
||||||
return activeNote;
|
const activeContext = getActiveContext();
|
||||||
|
return activeContext ? activeContext.note : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActiveNoteId() {
|
function getActiveNoteId() {
|
||||||
|
const activeNote = getActiveNote();
|
||||||
|
|
||||||
return activeNote ? activeNote.noteId : null;
|
return activeNote ? activeNote.noteId : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,16 +37,6 @@ function getActiveNoteType() {
|
|||||||
return activeNote ? activeNote.type : null;
|
return activeNote ? activeNote.type : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function noteChanged() {
|
|
||||||
if (noteChangeDisabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isNoteChanged = true;
|
|
||||||
|
|
||||||
$savedIndicator.fadeOut();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function reload() {
|
async function reload() {
|
||||||
// no saving here
|
// no saving here
|
||||||
|
|
||||||
@ -96,78 +45,33 @@ async function reload() {
|
|||||||
|
|
||||||
async function switchToNote(noteId) {
|
async function switchToNote(noteId) {
|
||||||
if (getActiveNoteId() !== noteId) {
|
if (getActiveNoteId() !== noteId) {
|
||||||
await saveNoteIfChanged();
|
await saveNotesIfChanged();
|
||||||
|
|
||||||
await loadNoteDetail(noteId);
|
await loadNoteDetail(noteId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActiveNoteContent() {
|
function getActiveNoteContent() {
|
||||||
return getComponent().getContent();
|
return getActiveContext().getComponent().getContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onNoteChange(func) {
|
function onNoteChange(func) {
|
||||||
return getComponent().onNoteChange(func);
|
return getActiveContext().getComponent().onNoteChange(func);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveNote() {
|
async function saveNotesIfChanged() {
|
||||||
const note = getActiveNote();
|
for (const ctx of noteContexts) {
|
||||||
|
await ctx.saveNoteIfChanged();
|
||||||
if (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
note.title = $noteTitle.val();
|
|
||||||
note.content = getActiveNoteContent(note);
|
|
||||||
|
|
||||||
// it's important to set the flag back to false immediatelly after retrieving title and content
|
|
||||||
// otherwise we might overwrite another change (especially async code)
|
|
||||||
isNoteChanged = false;
|
|
||||||
|
|
||||||
treeService.setNoteTitle(note.noteId, note.title);
|
|
||||||
|
|
||||||
await server.put('notes/' + note.noteId, note.dto);
|
|
||||||
|
|
||||||
if (note.isProtected) {
|
|
||||||
protectedSessionHolder.touchProtectedSession();
|
|
||||||
}
|
|
||||||
|
|
||||||
$savedIndicator.fadeIn();
|
|
||||||
|
|
||||||
// run async
|
|
||||||
bundleService.executeRelationBundles(getActiveNote(), 'runOnNoteChange');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveNoteIfChanged() {
|
|
||||||
if (isNoteChanged) {
|
|
||||||
await saveNote();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure indicator is visible in a case there was some race condition.
|
// make sure indicator is visible in a case there was some race condition.
|
||||||
$savedIndicator.fadeIn();
|
$savedIndicator.fadeIn();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateNoteView() {
|
|
||||||
$noteTabContent.toggleClass("protected", activeNote.isProtected);
|
|
||||||
$protectButton.toggleClass("active", activeNote.isProtected);
|
|
||||||
$protectButton.prop("disabled", activeNote.isProtected);
|
|
||||||
$unprotectButton.toggleClass("active", !activeNote.isProtected);
|
|
||||||
$unprotectButton.prop("disabled", !activeNote.isProtected || !protectedSessionHolder.isProtectedSessionAvailable());
|
|
||||||
|
|
||||||
for (const clazz of Array.from($body[0].classList)) { // create copy to safely iterate over while removing classes
|
|
||||||
if (clazz.startsWith("type-") || clazz.startsWith("mime-")) {
|
|
||||||
$body.removeClass(clazz);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$body.addClass(utils.getNoteTypeClass(activeNote.type));
|
|
||||||
$body.addClass(utils.getMimeTypeClass(activeNote.mime));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleProtectedSession() {
|
async function handleProtectedSession() {
|
||||||
const newSessionCreated = await protectedSessionService.ensureProtectedSession(activeNote.isProtected, false);
|
const newSessionCreated = await protectedSessionService.ensureProtectedSession(getActiveNote().isProtected, false);
|
||||||
|
|
||||||
if (activeNote.isProtected) {
|
if (getActiveNote().isProtected) {
|
||||||
protectedSessionHolder.touchProtectedSession();
|
protectedSessionHolder.touchProtectedSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,7 +82,34 @@ async function handleProtectedSession() {
|
|||||||
return newSessionCreated;
|
return newSessionCreated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {Object.<string, NoteContext>} */
|
||||||
|
const noteContexts = {};
|
||||||
|
|
||||||
|
/** @returns {NoteContext} */
|
||||||
|
function getContext(noteId) {
|
||||||
|
if (noteId in noteContexts) {
|
||||||
|
return noteContexts[noteId];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error(`Can't find note context for ${noteId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns {NoteContext} */
|
||||||
|
function getActiveContext() {
|
||||||
|
const currentTreeNode = treeService.getActiveNode();
|
||||||
|
|
||||||
|
return getContext(currentTreeNode.data.noteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showTab(noteId) {
|
||||||
|
for (const ctx of noteContexts) {
|
||||||
|
ctx.$noteTab.toggle(ctx.noteId === noteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadNoteDetail(noteId) {
|
async function loadNoteDetail(noteId) {
|
||||||
|
const ctx = getContext(noteId);
|
||||||
const loadedNote = await loadNote(noteId);
|
const loadedNote = await loadNote(noteId);
|
||||||
|
|
||||||
// we will try to render the new note only if it's still the active one in the tree
|
// we will try to render the new note only if it's still the active one in the tree
|
||||||
@ -191,38 +122,41 @@ async function loadNoteDetail(noteId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// only now that we're in sync with tree active node we will switch activeNote
|
// only now that we're in sync with tree active node we will switch activeNote
|
||||||
activeNote = loadedNote;
|
ctx.note = loadedNote;
|
||||||
|
ctx.noteId = loadedNote.noteId;
|
||||||
|
|
||||||
if (utils.isDesktop()) {
|
if (utils.isDesktop()) {
|
||||||
// needs to happen after loading the note itself because it references active noteId
|
// needs to happen after loading the note itself because it references active noteId
|
||||||
attributeService.refreshAttributes();
|
// FIXME
|
||||||
|
//attributeService.refreshAttributes();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// mobile usually doesn't need attributes so we just invalidate
|
// mobile usually doesn't need attributes so we just invalidate
|
||||||
attributeService.invalidateAttributes();
|
// FIXME
|
||||||
|
//attributeService.invalidateAttributes();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateNoteView();
|
ctx.updateNoteView();
|
||||||
|
|
||||||
$noteTabContent.show();
|
showTab(noteId);
|
||||||
|
|
||||||
noteChangeDisabled = true;
|
noteChangeDisabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$noteTitle.val(activeNote.title);
|
ctx.$noteTitle.val(ctx.note.title);
|
||||||
|
|
||||||
if (utils.isDesktop()) {
|
if (utils.isDesktop()) {
|
||||||
noteTypeService.setNoteType(activeNote.type);
|
noteTypeService.setNoteType(ctx.note.type);
|
||||||
noteTypeService.setNoteMime(activeNote.mime);
|
noteTypeService.setNoteMime(ctx.note.mime);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const componentType in components) {
|
for (const componentType in ctx.components) {
|
||||||
if (componentType !== activeNote.type) {
|
if (componentType !== ctx.note.type) {
|
||||||
components[componentType].cleanup();
|
ctx.components[componentType].cleanup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$noteDetailComponents.hide();
|
ctx.$noteDetailComponents.hide();
|
||||||
|
|
||||||
const newSessionCreated = await handleProtectedSession();
|
const newSessionCreated = await handleProtectedSession();
|
||||||
if (newSessionCreated) {
|
if (newSessionCreated) {
|
||||||
@ -230,9 +164,9 @@ async function loadNoteDetail(noteId) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$noteTitle.removeAttr("readonly"); // this can be set by protected session service
|
ctx.$noteTitle.removeAttr("readonly"); // this can be set by protected session service
|
||||||
|
|
||||||
await getComponent(activeNote.type).show();
|
await ctx.getComponent(ctx.note.type).show(ctx);
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
noteChangeDisabled = false;
|
noteChangeDisabled = false;
|
||||||
@ -241,51 +175,21 @@ async function loadNoteDetail(noteId) {
|
|||||||
treeService.setBranchBackgroundBasedOnProtectedStatus(noteId);
|
treeService.setBranchBackgroundBasedOnProtectedStatus(noteId);
|
||||||
|
|
||||||
// after loading new note make sure editor is scrolled to the top
|
// after loading new note make sure editor is scrolled to the top
|
||||||
getComponent(activeNote.type).scrollToTop();
|
ctx.getComponent(ctx.note.type).scrollToTop();
|
||||||
|
|
||||||
fireDetailLoaded();
|
fireDetailLoaded();
|
||||||
|
|
||||||
$scriptArea.empty();
|
ctx.$scriptArea.empty();
|
||||||
|
|
||||||
await bundleService.executeRelationBundles(getActiveNote(), 'runOnNoteView');
|
await bundleService.executeRelationBundles(getActiveNote(), 'runOnNoteView');
|
||||||
|
|
||||||
if (utils.isDesktop()) {
|
if (utils.isDesktop()) {
|
||||||
await attributeService.showAttributes();
|
await attributeService.showAttributes();
|
||||||
|
|
||||||
await showChildrenOverview();
|
await ctx.showChildrenOverview();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showChildrenOverview() {
|
|
||||||
const note = getActiveNote();
|
|
||||||
const attributes = await attributeService.getAttributes();
|
|
||||||
const hideChildrenOverview = attributes.some(attr => attr.type === 'label' && attr.name === 'hideChildrenOverview')
|
|
||||||
|| note.type === 'relation-map'
|
|
||||||
|| note.type === 'image'
|
|
||||||
|| note.type === 'file';
|
|
||||||
|
|
||||||
if (hideChildrenOverview) {
|
|
||||||
$childrenOverview.hide();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$childrenOverview.empty();
|
|
||||||
|
|
||||||
const notePath = await treeService.getActiveNotePath();
|
|
||||||
|
|
||||||
for (const childBranch of await note.getChildBranches()) {
|
|
||||||
const link = $('<a>', {
|
|
||||||
href: 'javascript:',
|
|
||||||
text: await treeUtils.getNoteTitle(childBranch.noteId, childBranch.parentNoteId)
|
|
||||||
}).attr('data-action', 'note').attr('data-note-path', notePath + '/' + childBranch.noteId);
|
|
||||||
|
|
||||||
const childEl = $('<div class="child-overview-item">').html(link);
|
|
||||||
$childrenOverview.append(childEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
$childrenOverview.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadNote(noteId) {
|
async function loadNote(noteId) {
|
||||||
const row = await server.get('notes/' + noteId);
|
const row = await server.get('notes/' + noteId);
|
||||||
|
|
||||||
@ -293,11 +197,11 @@ async function loadNote(noteId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function focusOnTitle() {
|
function focusOnTitle() {
|
||||||
$noteTitle.focus();
|
getActiveContext().$noteTitle.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusAndSelectTitle() {
|
function focusAndSelectTitle() {
|
||||||
$noteTitle.focus().select();
|
getActiveContext().$noteTitle.focus().select();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -315,7 +219,7 @@ function addDetailLoadedListener(noteId, callback) {
|
|||||||
|
|
||||||
function fireDetailLoaded() {
|
function fireDetailLoaded() {
|
||||||
for (const {noteId, callback} of detailLoadedListeners) {
|
for (const {noteId, callback} of detailLoadedListeners) {
|
||||||
if (noteId === activeNote.noteId) {
|
if (noteId === getActiveNoteId()) {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -346,28 +250,15 @@ $noteTabsContainer.on("drop", e => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document).ready(() => {
|
|
||||||
$noteTitle.on('input', () => {
|
|
||||||
noteChanged();
|
|
||||||
|
|
||||||
const title = $noteTitle.val();
|
|
||||||
|
|
||||||
treeService.setNoteTitle(getActiveNoteId(), title);
|
|
||||||
});
|
|
||||||
|
|
||||||
noteDetailText.focus();
|
|
||||||
});
|
|
||||||
|
|
||||||
// this makes sure that when user e.g. reloads the page or navigates away from the page, the note's content is saved
|
// 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
|
// this sends the request asynchronously and doesn't wait for result
|
||||||
$(window).on('beforeunload', () => { saveNoteIfChanged(); }); // don't convert to short form, handler doesn't like returned promise
|
$(window).on('beforeunload', () => { saveNotesIfChanged(); }); // don't convert to short form, handler doesn't like returned promise
|
||||||
|
|
||||||
setInterval(saveNoteIfChanged, 3000);
|
setInterval(saveNotesIfChanged, 3000);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
reload,
|
reload,
|
||||||
switchToNote,
|
switchToNote,
|
||||||
updateNoteView,
|
|
||||||
loadNote,
|
loadNote,
|
||||||
getActiveNote,
|
getActiveNote,
|
||||||
getActiveNoteContent,
|
getActiveNoteContent,
|
||||||
@ -375,9 +266,7 @@ export default {
|
|||||||
getActiveNoteId,
|
getActiveNoteId,
|
||||||
focusOnTitle,
|
focusOnTitle,
|
||||||
focusAndSelectTitle,
|
focusAndSelectTitle,
|
||||||
saveNote,
|
saveNotesIfChanged,
|
||||||
saveNoteIfChanged,
|
|
||||||
noteChanged,
|
|
||||||
onNoteChange,
|
onNoteChange,
|
||||||
addDetailLoadedListener
|
addDetailLoadedListener
|
||||||
};
|
};
|
@ -76,7 +76,7 @@ async function executeCurrentNote() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// make sure note is saved so we load latest changes
|
// make sure note is saved so we load latest changes
|
||||||
await noteDetailService.saveNoteIfChanged();
|
await noteDetailService.saveNotesIfChanged();
|
||||||
|
|
||||||
const activeNote = noteDetailService.getActiveNote();
|
const activeNote = noteDetailService.getActiveNote();
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ function getContent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$refreshButton.click(async () => {
|
$refreshButton.click(async () => {
|
||||||
await noteDetailService.saveNoteIfChanged();
|
await noteDetailService.saveNotesIfChanged();
|
||||||
|
|
||||||
await searchNotesService.refreshSearch();
|
await searchNotesService.refreshSearch();
|
||||||
});
|
});
|
||||||
|
@ -3,93 +3,98 @@ import noteDetailService from './note_detail.js';
|
|||||||
import treeService from './tree.js';
|
import treeService from './tree.js';
|
||||||
import attributeService from "./attributes.js";
|
import attributeService from "./attributes.js";
|
||||||
|
|
||||||
const $component = $('#note-detail-text');
|
class NoteDetailText {
|
||||||
|
/**
|
||||||
|
* @param {NoteContext} ctx
|
||||||
|
*/
|
||||||
|
constructor(ctx) {
|
||||||
|
this.$component = ctx.$noteTab.find('.note-detail-text');
|
||||||
|
this.textEditor = null;
|
||||||
|
|
||||||
let textEditor = null;
|
this.$component.on("dblclick", "img", e => {
|
||||||
|
const $img = $(e.target);
|
||||||
|
const src = $img.prop("src");
|
||||||
|
|
||||||
async function show() {
|
const match = src.match(/\/api\/images\/([A-Za-z0-9]+)\//);
|
||||||
if (!textEditor) {
|
|
||||||
await libraryLoader.requireLibrary(libraryLoader.CKEDITOR);
|
|
||||||
|
|
||||||
// CKEditor since version 12 needs the element to be visible before initialization. At the same time
|
if (match) {
|
||||||
// we want to avoid flicker - i.e. show editor only once everything is ready. That's why we have separate
|
const noteId = match[1];
|
||||||
// display of $component in both branches.
|
|
||||||
$component.show();
|
|
||||||
|
|
||||||
// textEditor might have been initialized during previous await so checking again
|
treeService.activateNote(noteId);
|
||||||
// looks like double initialization can freeze CKEditor pretty badly
|
}
|
||||||
if (!textEditor) {
|
else {
|
||||||
textEditor = await BalloonEditor.create($component[0], {
|
window.open(src, '_blank');
|
||||||
placeholder: "Type the content of your note here ..."
|
}
|
||||||
});
|
})
|
||||||
|
}
|
||||||
|
|
||||||
onNoteChange(noteDetailService.noteChanged);
|
async show() {
|
||||||
|
if (!this.textEditor) {
|
||||||
|
await libraryLoader.requireLibrary(libraryLoader.CKEDITOR);
|
||||||
|
|
||||||
|
// CKEditor since version 12 needs the element to be visible before initialization. At the same time
|
||||||
|
// we want to avoid flicker - i.e. show editor only once everything is ready. That's why we have separate
|
||||||
|
// display of $component in both branches.
|
||||||
|
this.$component.show();
|
||||||
|
|
||||||
|
// textEditor might have been initialized during previous await so checking again
|
||||||
|
// looks like double initialization can freeze CKEditor pretty badly
|
||||||
|
if (!this.textEditor) {
|
||||||
|
this.textEditor = await BalloonEditor.create(this.$component[0], {
|
||||||
|
placeholder: "Type the content of your note here ..."
|
||||||
|
});
|
||||||
|
|
||||||
|
this.onNoteChange(noteDetailService.noteChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.textEditor.isReadOnly = await isReadOnly();
|
||||||
|
|
||||||
|
this.$component.show();
|
||||||
|
|
||||||
|
this.textEditor.setData(noteDetailService.getActiveNote().content);
|
||||||
|
}
|
||||||
|
|
||||||
|
getContent() {
|
||||||
|
let content = this.textEditor.getData();
|
||||||
|
|
||||||
|
// if content is only tags/whitespace (typically <p> </p>), then just make it empty
|
||||||
|
// this is important when setting new note to code
|
||||||
|
if (jQuery(content).text().trim() === '' && !content.includes("<img")) {
|
||||||
|
content = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
async isReadOnly() {
|
||||||
|
const attributes = await attributeService.getAttributes();
|
||||||
|
|
||||||
|
return attributes.some(attr => attr.type === 'label' && attr.name === 'readOnly');
|
||||||
|
}
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
this.$component.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
getEditor() {
|
||||||
|
return this.textEditor;
|
||||||
|
}
|
||||||
|
|
||||||
|
onNoteChange(func) {
|
||||||
|
this.textEditor.model.document.on('change:data', func);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if (this.textEditor) {
|
||||||
|
this.textEditor.setData('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
textEditor.isReadOnly = await isReadOnly();
|
scrollToTop() {
|
||||||
|
this.$component.scrollTop(0);
|
||||||
$component.show();
|
|
||||||
|
|
||||||
textEditor.setData(noteDetailService.getActiveNote().content);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getContent() {
|
|
||||||
let content = textEditor.getData();
|
|
||||||
|
|
||||||
// if content is only tags/whitespace (typically <p> </p>), then just make it empty
|
|
||||||
// this is important when setting new note to code
|
|
||||||
if (jQuery(content).text().trim() === '' && !content.includes("<img")) {
|
|
||||||
content = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function isReadOnly() {
|
export default NoteDetailText
|
||||||
const attributes = await attributeService.getAttributes();
|
|
||||||
|
|
||||||
return attributes.some(attr => attr.type === 'label' && attr.name === 'readOnly');
|
|
||||||
}
|
|
||||||
|
|
||||||
function focus() {
|
|
||||||
$component.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEditor() {
|
|
||||||
return textEditor;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onNoteChange(func) {
|
|
||||||
textEditor.model.document.on('change:data', func);
|
|
||||||
}
|
|
||||||
|
|
||||||
$component.on("dblclick", "img", e => {
|
|
||||||
const $img = $(e.target);
|
|
||||||
const src = $img.prop("src");
|
|
||||||
|
|
||||||
const match = src.match(/\/api\/images\/([A-Za-z0-9]+)\//);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
const noteId = match[1];
|
|
||||||
|
|
||||||
treeService.activateNote(noteId);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
window.open(src, '_blank');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default {
|
|
||||||
show,
|
|
||||||
getEditor,
|
|
||||||
getContent,
|
|
||||||
focus,
|
|
||||||
onNoteChange,
|
|
||||||
cleanup: () => {
|
|
||||||
if (textEditor) {
|
|
||||||
textEditor.setData('');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scrollToTop: () => $component.scrollTop(0)
|
|
||||||
}
|
|
@ -624,7 +624,7 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) {
|
|||||||
window.cutToNote.removeSelection();
|
window.cutToNote.removeSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
await noteDetailService.saveNoteIfChanged();
|
await noteDetailService.saveNotesIfChanged();
|
||||||
|
|
||||||
noteDetailService.addDetailLoadedListener(note.noteId, noteDetailService.focusAndSelectTitle);
|
noteDetailService.addDetailLoadedListener(note.noteId, noteDetailService.focusAndSelectTitle);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user