mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	tabs wip
This commit is contained in:
		
							
								
								
									
										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); | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user