From 7e03f14e018fe8cd29a0a18883009696d758e5cd Mon Sep 17 00:00:00 2001 From: zadam Date: Wed, 1 May 2019 22:19:29 +0200 Subject: [PATCH] tabs wip --- .../javascripts/services/note_context.js | 153 +++++++++++ .../javascripts/services/note_detail.js | 243 +++++------------- .../javascripts/services/note_detail_code.js | 2 +- .../services/note_detail_search.js | 2 +- .../javascripts/services/note_detail_text.js | 163 ++++++------ src/public/javascripts/services/tree.js | 2 +- 6 files changed, 306 insertions(+), 259 deletions(-) create mode 100644 src/public/javascripts/services/note_context.js diff --git a/src/public/javascripts/services/note_context.js b/src/public/javascripts/services/note_context.js new file mode 100644 index 000000000..a7bad322d --- /dev/null +++ b/src/public/javascripts/services/note_context.js @@ -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 = $('', { + href: 'javascript:', + text: await treeUtils.getNoteTitle(childBranch.noteId, childBranch.parentNoteId) + }).attr('data-action', 'note').attr('data-note-path', notePath + '/' + childBranch.noteId); + + const childEl = $('
').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; \ No newline at end of file diff --git a/src/public/javascripts/services/note_detail.js b/src/public/javascripts/services/note_detail.js index 645b40b62..592df4271 100644 --- a/src/public/javascripts/services/note_detail.js +++ b/src/public/javascripts/services/note_detail.js @@ -1,5 +1,5 @@ import treeService from './tree.js'; -import treeUtils from './tree_utils.js'; +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'; @@ -8,67 +8,26 @@ import messagingService from "./messaging.js"; import infoService from "./info.js"; import treeCache from "./tree_cache.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 attributeService from "./attributes.js"; import utils from "./utils.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 $childrenOverview = $("#children-overview"); -const $scriptArea = $("#note-detail-script-area"); const $savedIndicator = $("#saved-indicator"); -const $body = $("body"); - -let activeNote = null; let noteChangeDisabled = false; -let isNoteChanged = false; - 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() { - return activeNote; + const activeContext = getActiveContext(); + return activeContext ? activeContext.note : null; } function getActiveNoteId() { + const activeNote = getActiveNote(); + return activeNote ? activeNote.noteId : null; } @@ -78,16 +37,6 @@ function getActiveNoteType() { return activeNote ? activeNote.type : null; } -function noteChanged() { - if (noteChangeDisabled) { - return; - } - - isNoteChanged = true; - - $savedIndicator.fadeOut(); -} - async function reload() { // no saving here @@ -96,78 +45,33 @@ async function reload() { async function switchToNote(noteId) { if (getActiveNoteId() !== noteId) { - await saveNoteIfChanged(); + await saveNotesIfChanged(); await loadNoteDetail(noteId); } } function getActiveNoteContent() { - return getComponent().getContent(); + return getActiveContext().getComponent().getContent(); } function onNoteChange(func) { - return getComponent().onNoteChange(func); + return getActiveContext().getComponent().onNoteChange(func); } -async function saveNote() { - const note = getActiveNote(); - - 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(); +async function saveNotesIfChanged() { + for (const ctx of noteContexts) { + await ctx.saveNoteIfChanged(); } // make sure indicator is visible in a case there was some race condition. $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() { - const newSessionCreated = await protectedSessionService.ensureProtectedSession(activeNote.isProtected, false); + const newSessionCreated = await protectedSessionService.ensureProtectedSession(getActiveNote().isProtected, false); - if (activeNote.isProtected) { + if (getActiveNote().isProtected) { protectedSessionHolder.touchProtectedSession(); } @@ -178,7 +82,34 @@ async function handleProtectedSession() { return newSessionCreated; } +/** @type {Object.} */ +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) { + const ctx = getContext(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 @@ -191,38 +122,41 @@ async function loadNoteDetail(noteId) { } // 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()) { // needs to happen after loading the note itself because it references active noteId - attributeService.refreshAttributes(); + // FIXME + //attributeService.refreshAttributes(); } else { // mobile usually doesn't need attributes so we just invalidate - attributeService.invalidateAttributes(); + // FIXME + //attributeService.invalidateAttributes(); } - updateNoteView(); + ctx.updateNoteView(); - $noteTabContent.show(); + showTab(noteId); noteChangeDisabled = true; try { - $noteTitle.val(activeNote.title); + ctx.$noteTitle.val(ctx.note.title); if (utils.isDesktop()) { - noteTypeService.setNoteType(activeNote.type); - noteTypeService.setNoteMime(activeNote.mime); + noteTypeService.setNoteType(ctx.note.type); + noteTypeService.setNoteMime(ctx.note.mime); } - for (const componentType in components) { - if (componentType !== activeNote.type) { - components[componentType].cleanup(); + for (const componentType in ctx.components) { + if (componentType !== ctx.note.type) { + ctx.components[componentType].cleanup(); } } - $noteDetailComponents.hide(); + ctx.$noteDetailComponents.hide(); const newSessionCreated = await handleProtectedSession(); if (newSessionCreated) { @@ -230,9 +164,9 @@ async function loadNoteDetail(noteId) { 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 { noteChangeDisabled = false; @@ -241,51 +175,21 @@ async function loadNoteDetail(noteId) { treeService.setBranchBackgroundBasedOnProtectedStatus(noteId); // after loading new note make sure editor is scrolled to the top - getComponent(activeNote.type).scrollToTop(); + ctx.getComponent(ctx.note.type).scrollToTop(); fireDetailLoaded(); - $scriptArea.empty(); + ctx.$scriptArea.empty(); await bundleService.executeRelationBundles(getActiveNote(), 'runOnNoteView'); if (utils.isDesktop()) { 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 = $('', { - href: 'javascript:', - text: await treeUtils.getNoteTitle(childBranch.noteId, childBranch.parentNoteId) - }).attr('data-action', 'note').attr('data-note-path', notePath + '/' + childBranch.noteId); - - const childEl = $('
').html(link); - $childrenOverview.append(childEl); - } - - $childrenOverview.show(); -} - async function loadNote(noteId) { const row = await server.get('notes/' + noteId); @@ -293,11 +197,11 @@ async function loadNote(noteId) { } function focusOnTitle() { - $noteTitle.focus(); + getActiveContext().$noteTitle.focus(); } function focusAndSelectTitle() { - $noteTitle.focus().select(); + getActiveContext().$noteTitle.focus().select(); } /** @@ -315,7 +219,7 @@ function addDetailLoadedListener(noteId, callback) { function fireDetailLoaded() { for (const {noteId, callback} of detailLoadedListeners) { - if (noteId === activeNote.noteId) { + if (noteId === getActiveNoteId()) { 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 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 { reload, switchToNote, - updateNoteView, loadNote, getActiveNote, getActiveNoteContent, @@ -375,9 +266,7 @@ export default { getActiveNoteId, focusOnTitle, focusAndSelectTitle, - saveNote, - saveNoteIfChanged, - noteChanged, + saveNotesIfChanged, onNoteChange, addDetailLoadedListener }; \ No newline at end of file diff --git a/src/public/javascripts/services/note_detail_code.js b/src/public/javascripts/services/note_detail_code.js index e4949d859..77269a0f1 100644 --- a/src/public/javascripts/services/note_detail_code.js +++ b/src/public/javascripts/services/note_detail_code.js @@ -76,7 +76,7 @@ async function executeCurrentNote() { } // make sure note is saved so we load latest changes - await noteDetailService.saveNoteIfChanged(); + await noteDetailService.saveNotesIfChanged(); const activeNote = noteDetailService.getActiveNote(); diff --git a/src/public/javascripts/services/note_detail_search.js b/src/public/javascripts/services/note_detail_search.js index 7f40095c5..c8130ca54 100644 --- a/src/public/javascripts/services/note_detail_search.js +++ b/src/public/javascripts/services/note_detail_search.js @@ -31,7 +31,7 @@ function getContent() { } $refreshButton.click(async () => { - await noteDetailService.saveNoteIfChanged(); + await noteDetailService.saveNotesIfChanged(); await searchNotesService.refreshSearch(); }); diff --git a/src/public/javascripts/services/note_detail_text.js b/src/public/javascripts/services/note_detail_text.js index d4da96df9..1cb9b49f3 100644 --- a/src/public/javascripts/services/note_detail_text.js +++ b/src/public/javascripts/services/note_detail_text.js @@ -3,93 +3,98 @@ import noteDetailService from './note_detail.js'; import treeService from './tree.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() { - if (!textEditor) { - await libraryLoader.requireLibrary(libraryLoader.CKEDITOR); + const match = src.match(/\/api\/images\/([A-Za-z0-9]+)\//); - // 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. - $component.show(); + if (match) { + const noteId = match[1]; - // textEditor might have been initialized during previous await so checking again - // looks like double initialization can freeze CKEditor pretty badly - if (!textEditor) { - textEditor = await BalloonEditor.create($component[0], { - placeholder: "Type the content of your note here ..." - }); + treeService.activateNote(noteId); + } + else { + window.open(src, '_blank'); + } + }) + } - 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

 

), then just make it empty + // this is important when setting new note to code + if (jQuery(content).text().trim() === '' && !content.includes(" 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(); - - $component.show(); - - textEditor.setData(noteDetailService.getActiveNote().content); -} - -function getContent() { - let content = textEditor.getData(); - - // if content is only tags/whitespace (typically

 

), then just make it empty - // this is important when setting new note to code - if (jQuery(content).text().trim() === '' && !content.includes(" 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) -} \ No newline at end of file +export default NoteDetailText \ No newline at end of file diff --git a/src/public/javascripts/services/tree.js b/src/public/javascripts/services/tree.js index 2f0988c6d..a314e56e4 100644 --- a/src/public/javascripts/services/tree.js +++ b/src/public/javascripts/services/tree.js @@ -624,7 +624,7 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) { window.cutToNote.removeSelection(); } - await noteDetailService.saveNoteIfChanged(); + await noteDetailService.saveNotesIfChanged(); noteDetailService.addDetailLoadedListener(note.noteId, noteDetailService.focusAndSelectTitle);