mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-30 18:05:55 +01:00 
			
		
		
		
	convert more note details to new style
This commit is contained in:
		| @@ -121,12 +121,12 @@ async function loadNoteDetail(noteId, newTab = false) { | ||||
|     let ctx; | ||||
|  | ||||
|     if (noteContexts.length === 0 || newTab) { | ||||
|         const tabContent = $("#note-tab-content-template").clone(); | ||||
|         const $tabContent = $(".note-tab-content-template").clone(); | ||||
|  | ||||
|         tabContent.removeAttr('id'); | ||||
|         tabContent.attr('data-note-id', noteId); | ||||
|         $tabContent.removeClass('note-tab-content-template'); | ||||
|         $tabContent.attr('data-note-id', noteId); | ||||
|  | ||||
|         $noteTabContentsContainer.append(tabContent); | ||||
|         $noteTabContentsContainer.append($tabContent); | ||||
|  | ||||
|         // if it's a new tab explicitly by user then it's in background | ||||
|         ctx = new NoteContext(loadedNote, newTab); | ||||
|   | ||||
| @@ -5,15 +5,26 @@ import server from "./server.js"; | ||||
| import noteDetailService from "./note_detail.js"; | ||||
| import utils from "./utils.js"; | ||||
|  | ||||
| let codeEditor = null; | ||||
| class NoteDetailCode { | ||||
|  | ||||
| const $component = $('#note-detail-code'); | ||||
| const $executeScriptButton = $("#execute-script-button"); | ||||
|     /** | ||||
|      * @param {NoteContext} ctx | ||||
|      */ | ||||
|     constructor(ctx) { | ||||
|         this.ctx = ctx; | ||||
|         this.codeEditor = null; | ||||
|         this.$component = ctx.$noteTabContent.find('.note-detail-code'); | ||||
|         this.$executeScriptButton = ctx.$noteTabContent.find(".execute-script-button"); | ||||
|  | ||||
| async function show() { | ||||
|         utils.bindShortcut("ctrl+return", this.executeCurrentNote); | ||||
|  | ||||
|         this.$executeScriptButton.click(this.executeCurrentNote); | ||||
|     } | ||||
|  | ||||
|     async show() { | ||||
|         await libraryLoader.requireLibrary(libraryLoader.CODE_MIRROR); | ||||
|  | ||||
|     if (!codeEditor) { | ||||
|         if (!this.codeEditor) { | ||||
|             CodeMirror.keyMap.default["Shift-Tab"] = "indentLess"; | ||||
|             CodeMirror.keyMap.default["Tab"] = "indentMore"; | ||||
|  | ||||
| @@ -23,7 +34,7 @@ async function show() { | ||||
|  | ||||
|             CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js'; | ||||
|  | ||||
|         codeEditor = CodeMirror($component[0], { | ||||
|             this.codeEditor = CodeMirror(this.$component[0], { | ||||
|                 value: "", | ||||
|                 viewportMargin: Infinity, | ||||
|                 indentUnit: 4, | ||||
| @@ -39,75 +50,67 @@ async function show() { | ||||
|                 lineWrapping: true | ||||
|             }); | ||||
|  | ||||
|         onNoteChange(noteDetailService.noteChanged); | ||||
|             this.onNoteChange(noteDetailService.noteChanged); | ||||
|         } | ||||
|  | ||||
|     $component.show(); | ||||
|  | ||||
|     const activeNote = noteDetailService.getActiveNote(); | ||||
|         this.$component.show(); | ||||
|  | ||||
|         // this needs to happen after the element is shown, otherwise the editor won't be refreshed | ||||
|         // CodeMirror breaks pretty badly on null so even though it shouldn't happen (guarded by consistency check) | ||||
|         // we provide fallback | ||||
|     codeEditor.setValue(activeNote.content || ""); | ||||
|         this.codeEditor.setValue(this.ctx.note.content || ""); | ||||
|  | ||||
|     const info = CodeMirror.findModeByMIME(activeNote.mime); | ||||
|         const info = CodeMirror.findModeByMIME(this.ctx.note.mime); | ||||
|  | ||||
|         if (info) { | ||||
|         codeEditor.setOption("mode", info.mime); | ||||
|         CodeMirror.autoLoadMode(codeEditor, info.mode); | ||||
|             this.codeEditor.setOption("mode", info.mime); | ||||
|             CodeMirror.autoLoadMode(this.codeEditor, info.mode); | ||||
|         } | ||||
|  | ||||
|     codeEditor.refresh(); | ||||
| } | ||||
|         this.codeEditor.refresh(); | ||||
|     } | ||||
|  | ||||
| function getContent() { | ||||
|     return codeEditor.getValue(); | ||||
| } | ||||
|     getContent() { | ||||
|         return this.codeEditor.getValue(); | ||||
|     } | ||||
|  | ||||
| function focus() { | ||||
|     codeEditor.focus(); | ||||
| } | ||||
|     focus() { | ||||
|         this.codeEditor.focus(); | ||||
|     } | ||||
|  | ||||
| async function executeCurrentNote() { | ||||
|     async executeCurrentNote() { | ||||
|         // ctrl+enter is also used elsewhere so make sure we're running only when appropriate | ||||
|     if (noteDetailService.getActiveNoteType() !== 'code') { | ||||
|         if (this.ctx.note.type !== 'code') { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // make sure note is saved so we load latest changes | ||||
|         await noteDetailService.saveNotesIfChanged(); | ||||
|  | ||||
|     const activeNote = noteDetailService.getActiveNote(); | ||||
|  | ||||
|     if (activeNote.mime.endsWith("env=frontend")) { | ||||
|         await bundleService.getAndExecuteBundle(noteDetailService.getActiveNoteId()); | ||||
|         if (this.ctx.note.mime.endsWith("env=frontend")) { | ||||
|             await bundleService.getAndExecuteBundle(this.ctx.note.noteId); | ||||
|         } | ||||
|  | ||||
|     if (activeNote.mime.endsWith("env=backend")) { | ||||
|         await server.post('script/run/' + noteDetailService.getActiveNoteId()); | ||||
|         if (this.ctx.note.mime.endsWith("env=backend")) { | ||||
|             await server.post('script/run/' + this.ctx.note.noteId); | ||||
|         } | ||||
|  | ||||
|         infoService.showMessage("Note executed"); | ||||
| } | ||||
|  | ||||
| function onNoteChange(func) { | ||||
|     codeEditor.on('change', func); | ||||
| } | ||||
|  | ||||
| utils.bindShortcut("ctrl+return", executeCurrentNote); | ||||
|  | ||||
| $executeScriptButton.click(executeCurrentNote); | ||||
|  | ||||
| export default { | ||||
|     show, | ||||
|     getContent, | ||||
|     focus, | ||||
|     onNoteChange, | ||||
|     cleanup: () => { | ||||
|         if (codeEditor) { | ||||
|             codeEditor.setValue(''); | ||||
|     } | ||||
|     }, | ||||
|     scrollToTop: () => $component.scrollTop(0) | ||||
|  | ||||
|     onNoteChange(func) { | ||||
|         this.codeEditor.on('change', func); | ||||
|     } | ||||
|  | ||||
|     cleanup() { | ||||
|         if (this.codeEditor) { | ||||
|             this.codeEditor.setValue(''); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     scrollToTop() { | ||||
|         this.$component.scrollTop(0); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default NoteDetailCode; | ||||
| @@ -10,19 +10,6 @@ import promptDialog from "../dialogs/prompt.js"; | ||||
| import infoDialog from "../dialogs/info.js"; | ||||
| import confirmDialog from "../dialogs/confirm.js"; | ||||
|  | ||||
| const $component = $("#note-detail-relation-map"); | ||||
| const $relationMapContainer = $("#relation-map-container"); | ||||
| const $createChildNote = $("#relation-map-create-child-note"); | ||||
| const $zoomInButton = $("#relation-map-zoom-in"); | ||||
| const $zoomOutButton = $("#relation-map-zoom-out"); | ||||
| const $resetPanZoomButton = $("#relation-map-reset-pan-zoom"); | ||||
|  | ||||
| let mapData; | ||||
| let jsPlumbInstance; | ||||
| // outside of mapData because they are not persisted in the note content | ||||
| let relations; | ||||
| let pzInstance; | ||||
|  | ||||
| const uniDirectionalOverlays = [ | ||||
|     [ "Arrow", { | ||||
|         location: 1, | ||||
| @@ -77,9 +64,120 @@ const linkOverlays = [ | ||||
|     } ] | ||||
| ]; | ||||
|  | ||||
| function loadMapData() { | ||||
|     const activeNote = noteDetailService.getActiveNote(); | ||||
|     mapData = { | ||||
| let containerCounter = 1; | ||||
|  | ||||
| class NoteDetailRelationMap { | ||||
|     /** | ||||
|      * @param {NoteContext} ctx | ||||
|      */ | ||||
|     constructor(ctx) { | ||||
|         this.ctx = ctx; | ||||
|         this.$component = ctx.$noteTabContent.find(".note-detail-relation-map"); | ||||
|         this.$relationMapContainer = ctx.$noteTabContent.find(".relation-map-container"); | ||||
|         this.$createChildNote = ctx.$noteTabContent.find(".relation-map-create-child-note"); | ||||
|         this.$zoomInButton = ctx.$noteTabContent.find(".relation-map-zoom-in"); | ||||
|         this.$zoomOutButton = ctx.$noteTabContent.find(".relation-map-zoom-out"); | ||||
|         this.$resetPanZoomButton = ctx.$noteTabContent.find(".relation-map-reset-pan-zoom"); | ||||
|  | ||||
|         this.mapData = null; | ||||
|         this.jsPlumbInstance = null; | ||||
|         // outside of mapData because they are not persisted in the note content | ||||
|         this.relations = null; | ||||
|         this.pzInstance = null; | ||||
|  | ||||
|         this.$relationMapContainer.attr("id", "relation-map-container-" + (containerCounter++)); | ||||
|         this.$relationMapContainer.on("contextmenu", ".note-box", e => { | ||||
|             contextMenuWidget.initContextMenu(e, { | ||||
|                 getContextMenuItems: () => { | ||||
|                     return [ | ||||
|                         {title: "Remove note", cmd: "remove", uiIcon: "trash"}, | ||||
|                         {title: "Edit title", cmd: "edit-title", uiIcon: "pencil"}, | ||||
|                     ]; | ||||
|                 }, | ||||
|                 selectContextMenuItem: (event, cmd) => this.noteContextMenuHandler(event, cmd) | ||||
|             }); | ||||
|  | ||||
|             return false; | ||||
|         }); | ||||
|  | ||||
|         this.clipboard = null; | ||||
|  | ||||
|         this.$createChildNote.click(async () => { | ||||
|             const title = await promptDialog.ask({ message: "Enter title of new note",  defaultValue: "new note" }); | ||||
|  | ||||
|             if (!title.trim()) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const {note} = await server.post(`notes/${this.ctx.note.noteId}/children`, { | ||||
|                 title, | ||||
|                 target: 'into' | ||||
|             }); | ||||
|  | ||||
|             infoService.showMessage("Click on canvas to place new note"); | ||||
|  | ||||
|             // reloading tree so that the new note appears there | ||||
|             // no need to wait for it to finish | ||||
|             treeService.reload(); | ||||
|  | ||||
|             this.clipboard = { noteId: note.noteId, title }; | ||||
|         }); | ||||
|  | ||||
|         this.$resetPanZoomButton.click(() => { | ||||
|             // reset to initial pan & zoom state | ||||
|             this.pzInstance.zoomTo(0, 0, 1 / getZoom()); | ||||
|             this.pzInstance.moveTo(0, 0); | ||||
|         }); | ||||
|  | ||||
|         this.$component.on("drop", this.dropNoteOntoRelationMapHandler); | ||||
|         this.$component.on("dragover", ev => ev.preventDefault()); | ||||
|     } | ||||
|  | ||||
|     async noteContextMenuHandler(event, cmd) { | ||||
|         const $noteBox = $(event.originalTarget).closest(".note-box"); | ||||
|         const $title = $noteBox.find(".title a"); | ||||
|         const noteId = this.idToNoteId($noteBox.prop("id")); | ||||
|  | ||||
|         if (cmd === "remove") { | ||||
|             if (!await confirmDialog.confirmDeleteNoteBoxWithNote($title.text())) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.jsPlumbInstance.remove(this.noteIdToId(noteId)); | ||||
|  | ||||
|             if (confirmDialog.isDeleteNoteChecked()) { | ||||
|                 await server.remove("notes/" + noteId); | ||||
|  | ||||
|                 // to force it to disappear from the tree | ||||
|                 treeService.reload(); | ||||
|             } | ||||
|  | ||||
|             this.mapData.notes = this.mapData.notes.filter(note => note.noteId !== noteId); | ||||
|  | ||||
|             this.relations = this.relations.filter(relation => relation.sourceNoteId !== noteId && relation.targetNoteId !== noteId); | ||||
|  | ||||
|             this.saveData(); | ||||
|         } | ||||
|         else if (cmd === "edit-title") { | ||||
|             const title = await promptDialog.ask({ | ||||
|                 message: "Enter new note title:", | ||||
|                 defaultValue: $title.text() | ||||
|             }); | ||||
|  | ||||
|             if (!title) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             await server.put(`notes/${noteId}/change-title`, { title }); | ||||
|  | ||||
|             treeService.setNoteTitle(noteId, title); | ||||
|  | ||||
|             $title.text(title); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     loadMapData() { | ||||
|         this.mapData = { | ||||
|             notes: [], | ||||
|             // it is important to have this exact value here so that initial transform is same as this | ||||
|             // which will guarantee note won't be saved on first conversion to relation map note type | ||||
| @@ -92,57 +190,57 @@ function loadMapData() { | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|     if (activeNote.content) { | ||||
|         if (this.ctx.note.content) { | ||||
|             try { | ||||
|             mapData = JSON.parse(activeNote.content); | ||||
|                 this.mapData = JSON.parse(this.ctx.note.content); | ||||
|             } catch (e) { | ||||
|                 console.log("Could not parse content: ", e); | ||||
|             } | ||||
|         } | ||||
| } | ||||
|     } | ||||
|  | ||||
| function noteIdToId(noteId) { | ||||
|     noteIdToId(noteId) { | ||||
|         return "rel-map-note-" + noteId; | ||||
| } | ||||
|     } | ||||
|  | ||||
| function idToNoteId(id) { | ||||
|     idToNoteId(id) { | ||||
|         return id.substr(13); | ||||
| } | ||||
|     } | ||||
|  | ||||
| async function show() { | ||||
|     $component.show(); | ||||
|     async show() { | ||||
|         this.$component.show(); | ||||
|  | ||||
|         await libraryLoader.requireLibrary(libraryLoader.RELATION_MAP); | ||||
|  | ||||
|     loadMapData(); | ||||
|         this.loadMapData(); | ||||
|  | ||||
|         jsPlumb.ready(() => { | ||||
|         initJsPlumbInstance(); | ||||
|             this.initJsPlumbInstance(); | ||||
|  | ||||
|         initPanZoom(); | ||||
|             this.initPanZoom(); | ||||
|  | ||||
|         loadNotesAndRelations(); | ||||
|             this.loadNotesAndRelations(); | ||||
|         }); | ||||
|  | ||||
| } | ||||
|     } | ||||
|  | ||||
| function clearMap() { | ||||
|     clearMap() { | ||||
|         // delete all endpoints and connections | ||||
|         // this is done at this point (after async operations) to reduce flicker to the minimum | ||||
|     jsPlumbInstance.deleteEveryEndpoint(); | ||||
|         this.jsPlumbInstance.deleteEveryEndpoint(); | ||||
|  | ||||
|         // without this we still end up with note boxes remaining in the canvas | ||||
|     $relationMapContainer.empty(); | ||||
| } | ||||
|         this.$relationMapContainer.empty(); | ||||
|     } | ||||
|  | ||||
| async function loadNotesAndRelations() { | ||||
|     const noteIds = mapData.notes.map(note => note.noteId); | ||||
|     async loadNotesAndRelations() { | ||||
|         const noteIds = this.mapData.notes.map(note => note.noteId); | ||||
|         const data = await server.post("notes/relation-map", {noteIds}); | ||||
|  | ||||
|     relations = []; | ||||
|         this.relations = []; | ||||
|  | ||||
|         for (const relation of data.relations) { | ||||
|         const match = relations.find(rel => | ||||
|             const match = this.relations.find(rel => | ||||
|                 rel.name === data.inverseRelations[relation.name] | ||||
|                 && ((rel.sourceNoteId === relation.sourceNoteId && rel.targetNoteId === relation.targetNoteId) | ||||
|                 || (rel.sourceNoteId === relation.targetNoteId && rel.targetNoteId === relation.sourceNoteId))); | ||||
| @@ -155,28 +253,28 @@ async function loadNotesAndRelations() { | ||||
|                 relation.render = true; | ||||
|             } | ||||
|  | ||||
|         relations.push(relation); | ||||
|             this.relations.push(relation); | ||||
|         } | ||||
|  | ||||
|     mapData.notes = mapData.notes.filter(note => note.noteId in data.noteTitles); | ||||
|         this.mapData.notes = this.mapData.notes.filter(note => note.noteId in data.noteTitles); | ||||
|  | ||||
|     jsPlumbInstance.batch(async function () { | ||||
|         clearMap(); | ||||
|         this.jsPlumbInstance.batch(async () => { | ||||
|             this.clearMap(); | ||||
|  | ||||
|         for (const note of mapData.notes) { | ||||
|             for (const note of this.mapData.notes) { | ||||
|                 const title = data.noteTitles[note.noteId]; | ||||
|  | ||||
|             await createNoteBox(note.noteId, title, note.x, note.y); | ||||
|                 await this.createNoteBox(note.noteId, title, note.x, note.y); | ||||
|             } | ||||
|  | ||||
|         for (const relation of relations) { | ||||
|             for (const relation of this.relations) { | ||||
|                 if (!relation.render) { | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|             const connection = jsPlumbInstance.connect({ | ||||
|                 source: noteIdToId(relation.sourceNoteId), | ||||
|                 target: noteIdToId(relation.targetNoteId), | ||||
|                 const connection = this.jsPlumbInstance.connect({ | ||||
|                     source: this.noteIdToId(relation.sourceNoteId), | ||||
|                     target: this.noteIdToId(relation.targetNoteId), | ||||
|                     type: relation.type | ||||
|                 }); | ||||
|  | ||||
| @@ -194,39 +292,39 @@ async function loadNotesAndRelations() { | ||||
|             } | ||||
|  | ||||
|             for (const link of data.links) { | ||||
|             jsPlumbInstance.connect({ | ||||
|                 source: noteIdToId(link.sourceNoteId), | ||||
|                 target: noteIdToId(link.targetNoteId), | ||||
|                 this.jsPlumbInstance.connect({ | ||||
|                     source: this.noteIdToId(link.sourceNoteId), | ||||
|                     target: this.noteIdToId(link.targetNoteId), | ||||
|                     type: 'link' | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
| } | ||||
|     } | ||||
|  | ||||
| function initPanZoom() { | ||||
|     if (pzInstance) { | ||||
|     initPanZoom() { | ||||
|         if (this.pzInstance) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|     pzInstance = panzoom($relationMapContainer[0], { | ||||
|         this.pzInstance = panzoom(this.$relationMapContainer[0], { | ||||
|             maxZoom: 2, | ||||
|             minZoom: 0.3, | ||||
|             smoothScroll: false, | ||||
|             onMouseDown: function(event) { | ||||
|             if (clipboard) { | ||||
|                 let {x, y} = getMousePosition(event); | ||||
|                 if (this.clipboard) { | ||||
|                     let {x, y} = this.getMousePosition(event); | ||||
|  | ||||
|                     // modifying position so that cursor is on the top-center of the box | ||||
|                     x -= 80; | ||||
|                     y -= 15; | ||||
|  | ||||
|                 createNoteBox(clipboard.noteId, clipboard.title, x, y); | ||||
|                     this.createNoteBox(this.clipboard.noteId, this.clipboard.title, x, y); | ||||
|  | ||||
|                 mapData.notes.push({ noteId: clipboard.noteId, x, y }); | ||||
|                     this.mapData.notes.push({ noteId: this.clipboard.noteId, x, y }); | ||||
|  | ||||
|                 saveData(); | ||||
|                     this.saveData(); | ||||
|  | ||||
|                 clipboard = null; | ||||
|                     this.clipboard = null; | ||||
|                 } | ||||
|  | ||||
|                 return true; | ||||
| @@ -238,101 +336,78 @@ function initPanZoom() { | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|     pzInstance.on('transform', () => { // gets triggered on any transform change | ||||
|         jsPlumbInstance.setZoom(getZoom()); | ||||
|         this.pzInstance.on('transform', () => { // gets triggered on any transform change | ||||
|             this.jsPlumbInstance.setZoom(this.getZoom()); | ||||
|  | ||||
|         saveCurrentTransform(); | ||||
|             this.saveCurrentTransform(); | ||||
|         }); | ||||
|  | ||||
|     if (mapData.transform) { | ||||
|         pzInstance.zoomTo(0, 0, mapData.transform.scale); | ||||
|         if (this.mapData.transform) { | ||||
|             this.pzInstance.zoomTo(0, 0, this.mapData.transform.scale); | ||||
|  | ||||
|         pzInstance.moveTo(mapData.transform.x, mapData.transform.y); | ||||
|             this.pzInstance.moveTo(this.mapData.transform.x, this.mapData.transform.y); | ||||
|         } | ||||
|         else { | ||||
|             // set to initial coordinates | ||||
|         pzInstance.moveTo(0, 0); | ||||
|             this.pzInstance.moveTo(0, 0); | ||||
|         } | ||||
|  | ||||
|     $zoomInButton.click(() => pzInstance.zoomTo(0, 0, 1.2)); | ||||
|     $zoomOutButton.click(() => pzInstance.zoomTo(0, 0, 0.8)); | ||||
| } | ||||
|         this.$zoomInButton.click(() => this.pzInstance.zoomTo(0, 0, 1.2)); | ||||
|         this.$zoomOutButton.click(() => this.pzInstance.zoomTo(0, 0, 0.8)); | ||||
|     } | ||||
|  | ||||
| function saveCurrentTransform() { | ||||
|     const newTransform = pzInstance.getTransform(); | ||||
|     saveCurrentTransform() { | ||||
|         const newTransform = this.pzInstance.getTransform(); | ||||
|  | ||||
|     if (JSON.stringify(newTransform) !== JSON.stringify(mapData.transform)) { | ||||
|         if (JSON.stringify(newTransform) !== JSON.stringify(this.mapData.transform)) { | ||||
|             // clone transform object | ||||
|         mapData.transform = JSON.parse(JSON.stringify(newTransform)); | ||||
|             this.mapData.transform = JSON.parse(JSON.stringify(newTransform)); | ||||
|  | ||||
|         saveData(); | ||||
|             this.saveData(); | ||||
|         } | ||||
| } | ||||
|  | ||||
| function cleanup() { | ||||
|     if (jsPlumbInstance) { | ||||
|         clearMap(); | ||||
|     } | ||||
|  | ||||
|     if (pzInstance) { | ||||
|         pzInstance.dispose(); | ||||
|         pzInstance = null; | ||||
|     cleanup() { | ||||
|         if (this.jsPlumbInstance) { | ||||
|             this.clearMap(); | ||||
|         } | ||||
| } | ||||
|  | ||||
| function initJsPlumbInstance () { | ||||
|     if (jsPlumbInstance) { | ||||
|         cleanup(); | ||||
|         if (this.pzInstance) { | ||||
|             this.pzInstance.dispose(); | ||||
|             this.pzInstance = null; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     initJsPlumbInstance () { | ||||
|         if (this.jsPlumbInstance) { | ||||
|             this.cleanup(); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|     jsPlumbInstance = jsPlumb.getInstance({ | ||||
|         this.jsPlumbInstance = jsPlumb.getInstance({ | ||||
|             Endpoint: ["Dot", {radius: 2}], | ||||
|             Connector: "StateMachine", | ||||
|             ConnectionOverlays: uniDirectionalOverlays, | ||||
|             HoverPaintStyle: { stroke: "#777", strokeWidth: 1 }, | ||||
|         Container: "relation-map-container" | ||||
|             Container: this.$relationMapContainer.attr("id") | ||||
|         }); | ||||
|  | ||||
|     jsPlumbInstance.registerConnectionType("uniDirectional", { anchor:"Continuous", connector:"StateMachine", overlays: uniDirectionalOverlays }); | ||||
|         this.jsPlumbInstance.registerConnectionType("uniDirectional", { anchor:"Continuous", connector:"StateMachine", overlays: uniDirectionalOverlays }); | ||||
|  | ||||
|     jsPlumbInstance.registerConnectionType("biDirectional", { anchor:"Continuous", connector:"StateMachine", overlays: biDirectionalOverlays }); | ||||
|         this.jsPlumbInstance.registerConnectionType("biDirectional", { anchor:"Continuous", connector:"StateMachine", overlays: biDirectionalOverlays }); | ||||
|  | ||||
|     jsPlumbInstance.registerConnectionType("inverse", { anchor:"Continuous", connector:"StateMachine", overlays: inverseRelationsOverlays }); | ||||
|         this.jsPlumbInstance.registerConnectionType("inverse", { anchor:"Continuous", connector:"StateMachine", overlays: inverseRelationsOverlays }); | ||||
|  | ||||
|     jsPlumbInstance.registerConnectionType("link", { anchor:"Continuous", connector:"StateMachine", overlays: linkOverlays }); | ||||
|         this.jsPlumbInstance.registerConnectionType("link", { anchor:"Continuous", connector:"StateMachine", overlays: linkOverlays }); | ||||
|  | ||||
|     jsPlumbInstance.bind("connection", connectionCreatedHandler); | ||||
|         this.jsPlumbInstance.bind("connection", (info, originalEvent) => this.connectionCreatedHandler(info, originalEvent)); | ||||
|  | ||||
|         // so that canvas is not panned when clicking/dragging note box | ||||
|     $relationMapContainer.on('mousedown touchstart', '.note-box, .connection-label', e => e.stopPropagation()); | ||||
| } | ||||
|  | ||||
| function connectionContextMenuHandler(connection, event) { | ||||
|     event.preventDefault(); | ||||
|     event.stopPropagation(); | ||||
|  | ||||
|     const items = [ {title: "Remove relation", cmd: "remove", uiIcon: "trash"} ]; | ||||
|  | ||||
|     contextMenuWidget.initContextMenu(event, items, async (event, cmd) => { | ||||
|         if (cmd === 'remove') { | ||||
|             if (!await confirmDialog.confirm("Are you sure you want to remove the relation?")) { | ||||
|                 return; | ||||
|         this.$relationMapContainer.on('mousedown touchstart', '.note-box, .connection-label', e => e.stopPropagation()); | ||||
|     } | ||||
|  | ||||
|             const relation = relations.find(rel => rel.attributeId === connection.id); | ||||
|  | ||||
|             await server.remove(`notes/${relation.sourceNoteId}/relations/${relation.name}/to/${relation.targetNoteId}`); | ||||
|  | ||||
|             jsPlumbInstance.deleteConnection(connection); | ||||
|  | ||||
|             relations = relations.filter(relation => relation.attributeId !== connection.id); | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
| async function connectionCreatedHandler(info, originalEvent) { | ||||
|     async connectionCreatedHandler(info, originalEvent) { | ||||
|         const connection = info.connection; | ||||
|  | ||||
|         connection.bind("contextmenu", (obj, event) => { | ||||
| @@ -342,7 +417,29 @@ async function connectionCreatedHandler(info, originalEvent) { | ||||
|                 event.preventDefault(); | ||||
|             } | ||||
|             else { | ||||
|             connectionContextMenuHandler(connection, event); | ||||
|                 event.preventDefault(); | ||||
|                 event.stopPropagation(); | ||||
|  | ||||
|                 contextMenuWidget.initContextMenu(event, { | ||||
|                     getContextMenuItems: () => { | ||||
|                         return [ {title: "Remove relation", cmd: "remove", uiIcon: "trash"} ]; | ||||
|                     }, | ||||
|                     selectContextMenuItem: async (event, cmd) => { | ||||
|                         if (cmd === 'remove') { | ||||
|                             if (!await confirmDialog.confirm("Are you sure you want to remove the relation?")) { | ||||
|                                 return; | ||||
|                             } | ||||
|  | ||||
|                             const relation = this.relations.find(rel => rel.attributeId === connection.id); | ||||
|  | ||||
|                             await server.remove(`notes/${relation.sourceNoteId}/relations/${relation.name}/to/${relation.targetNoteId}`); | ||||
|  | ||||
|                             this.jsPlumbInstance.deleteConnection(connection); | ||||
|  | ||||
|                             this.relations = this.relations.filter(relation => relation.attributeId !== connection.id); | ||||
|                         } | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
| @@ -362,15 +459,15 @@ async function connectionCreatedHandler(info, originalEvent) { | ||||
|         }); | ||||
|  | ||||
|         if (!name || !name.trim()) { | ||||
|         jsPlumbInstance.deleteConnection(connection); | ||||
|             this.jsPlumbInstance.deleteConnection(connection); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|     const targetNoteId = idToNoteId(connection.target.id); | ||||
|     const sourceNoteId = idToNoteId(connection.source.id); | ||||
|         const targetNoteId = this.idToNoteId(connection.target.id); | ||||
|         const sourceNoteId = this.idToNoteId(connection.source.id); | ||||
|  | ||||
|     const relationExists = relations.some(rel => | ||||
|         const relationExists = this.relations.some(rel => | ||||
|             rel.targetNoteId === targetNoteId | ||||
|             && rel.sourceNoteId === sourceNoteId | ||||
|             && rel.name === name); | ||||
| @@ -378,92 +475,38 @@ async function connectionCreatedHandler(info, originalEvent) { | ||||
|         if (relationExists) { | ||||
|             await infoDialog.info("Connection '" + name + "' between these notes already exists."); | ||||
|  | ||||
|         jsPlumbInstance.deleteConnection(connection); | ||||
|             this.jsPlumbInstance.deleteConnection(connection); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         await server.put(`notes/${sourceNoteId}/relations/${name}/to/${targetNoteId}`); | ||||
|  | ||||
|     await refresh(); | ||||
| } | ||||
|  | ||||
| $relationMapContainer.on("contextmenu", ".note-box", e => { | ||||
|     const items = [ | ||||
|         {title: "Remove note", cmd: "remove", uiIcon: "trash"}, | ||||
|         {title: "Edit title", cmd: "edit-title", uiIcon: "pencil"}, | ||||
|     ]; | ||||
|  | ||||
|     contextMenuWidget.initContextMenu(e, items, noteContextMenuHandler); | ||||
|  | ||||
|     return false; | ||||
| }); | ||||
|  | ||||
| async function noteContextMenuHandler(event, cmd) { | ||||
|     const $noteBox = $(event.originalTarget).closest(".note-box"); | ||||
|     const $title = $noteBox.find(".title a"); | ||||
|     const noteId = idToNoteId($noteBox.prop("id")); | ||||
|  | ||||
|     if (cmd === "remove") { | ||||
|         if (!await confirmDialog.confirmDeleteNoteBoxWithNote($title.text())) { | ||||
|             return; | ||||
|         await this.refresh(); | ||||
|     } | ||||
|  | ||||
|         jsPlumbInstance.remove(noteIdToId(noteId)); | ||||
|  | ||||
|         if (confirmDialog.isDeleteNoteChecked()) { | ||||
|             await server.remove("notes/" + noteId); | ||||
|  | ||||
|             // to force it to disappear from the tree | ||||
|             treeService.reload(); | ||||
|     saveData() { | ||||
|         this.ctx.noteChanged(); | ||||
|     } | ||||
|  | ||||
|         mapData.notes = mapData.notes.filter(note => note.noteId !== noteId); | ||||
|  | ||||
|         relations = relations.filter(relation => relation.sourceNoteId !== noteId && relation.targetNoteId !== noteId); | ||||
|  | ||||
|         saveData(); | ||||
|     } | ||||
|     else if (cmd === "edit-title") { | ||||
|         const title = await promptDialog.ask({ | ||||
|             message: "Enter new note title:", | ||||
|             defaultValue: $title.text() | ||||
|         }); | ||||
|  | ||||
|         if (!title) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         await server.put(`notes/${noteId}/change-title`, { title }); | ||||
|  | ||||
|         treeService.setNoteTitle(noteId, title); | ||||
|  | ||||
|         $title.text(title); | ||||
|     } | ||||
| } | ||||
|  | ||||
| function saveData() { | ||||
|     noteDetailService.noteChanged(); | ||||
| } | ||||
|  | ||||
| async function createNoteBox(noteId, title, x, y) { | ||||
|     async createNoteBox(noteId, title, x, y) { | ||||
|         const $noteBox = $("<div>") | ||||
|             .addClass("note-box") | ||||
|         .prop("id", noteIdToId(noteId)) | ||||
|             .prop("id", this.noteIdToId(noteId)) | ||||
|             .append($("<span>").addClass("title").html(await linkService.createNoteLink(noteId, title))) | ||||
|             .append($("<div>").addClass("endpoint").attr("title", "Start dragging relations from here and drop them on another note.")) | ||||
|             .css("left", x + "px") | ||||
|             .css("top", y + "px"); | ||||
|  | ||||
|     jsPlumbInstance.getContainer().appendChild($noteBox[0]); | ||||
|         this.jsPlumbInstance.getContainer().appendChild($noteBox[0]); | ||||
|  | ||||
|     jsPlumbInstance.draggable($noteBox[0], { | ||||
|         this.jsPlumbInstance.draggable($noteBox[0], { | ||||
|             start: params => {}, | ||||
|             drag: params => {}, | ||||
|             stop: params => { | ||||
|             const noteId = idToNoteId(params.el.id); | ||||
|                 const noteId = this.idToNoteId(params.el.id); | ||||
|  | ||||
|             const note = mapData.notes.find(note => note.noteId === noteId); | ||||
|                 const note = this.mapData.notes.find(note => note.noteId === noteId); | ||||
|  | ||||
|                 if (!note) { | ||||
|                     console.error(`Note ${noteId} not found!`); | ||||
| @@ -472,11 +515,11 @@ async function createNoteBox(noteId, title, x, y) { | ||||
|  | ||||
|                 [note.x, note.y] = params.finalPos; | ||||
|  | ||||
|             saveData(); | ||||
|                 this.saveData(); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|     jsPlumbInstance.makeSource($noteBox[0], { | ||||
|         this.jsPlumbInstance.makeSource($noteBox[0], { | ||||
|             filter: ".endpoint", | ||||
|             anchor: "Continuous", | ||||
|             connectorStyle: { stroke: "#000", strokeWidth: 1 }, | ||||
| @@ -486,56 +529,33 @@ async function createNoteBox(noteId, title, x, y) { | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|     jsPlumbInstance.makeTarget($noteBox[0], { | ||||
|         this.jsPlumbInstance.makeTarget($noteBox[0], { | ||||
|             dropOptions: { hoverClass: "dragHover" }, | ||||
|             anchor: "Continuous", | ||||
|             allowLoopback: true | ||||
|         }); | ||||
| } | ||||
|  | ||||
| async function refresh() { | ||||
|     await loadNotesAndRelations(); | ||||
| } | ||||
|  | ||||
| let clipboard = null; | ||||
|  | ||||
| $createChildNote.click(async () => { | ||||
|     const title = await promptDialog.ask({ message: "Enter title of new note",  defaultValue: "new note" }); | ||||
|  | ||||
|     if (!title.trim()) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const {note} = await server.post(`notes/${noteDetailService.getActiveNoteId()}/children`, { | ||||
|         title, | ||||
|         target: 'into' | ||||
|     }); | ||||
|     async refresh() { | ||||
|         await this.loadNotesAndRelations(); | ||||
|     } | ||||
|  | ||||
|     infoService.showMessage("Click on canvas to place new note"); | ||||
|  | ||||
|     // reloading tree so that the new note appears there | ||||
|     // no need to wait for it to finish | ||||
|     treeService.reload(); | ||||
|  | ||||
|     clipboard = { noteId: note.noteId, title }; | ||||
| }); | ||||
|  | ||||
| function getZoom() { | ||||
|     getZoom() { | ||||
|         const matrixRegex = /matrix\((-?\d*\.?\d+),\s*0,\s*0,\s*-?\d*\.?\d+,\s*-?\d*\.?\d+,\s*-?\d*\.?\d+\)/; | ||||
|  | ||||
|     const matches = $relationMapContainer.css('transform').match(matrixRegex); | ||||
|         const matches = this.$relationMapContainer.css('transform').match(matrixRegex); | ||||
|  | ||||
|         return matches[1]; | ||||
| } | ||||
|     } | ||||
|  | ||||
| async function dropNoteOntoRelationMapHandler(ev) { | ||||
|     async dropNoteOntoRelationMapHandler(ev) { | ||||
|         ev.preventDefault(); | ||||
|  | ||||
|         const note = JSON.parse(ev.originalEvent.dataTransfer.getData("text")); | ||||
|  | ||||
|     let {x, y} = getMousePosition(ev); | ||||
|         let {x, y} = this.getMousePosition(ev); | ||||
|  | ||||
|     const exists = mapData.notes.some(n => n.noteId === note.noteId); | ||||
|         const exists = this.mapData.notes.some(n => n.noteId === note.noteId); | ||||
|  | ||||
|         if (exists) { | ||||
|             await infoDialog.info(`Note "${note.title}" is already placed into the diagram`); | ||||
| @@ -543,38 +563,33 @@ async function dropNoteOntoRelationMapHandler(ev) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|     mapData.notes.push({noteId: note.noteId, x, y}); | ||||
|         this.mapData.notes.push({noteId: note.noteId, x, y}); | ||||
|  | ||||
|     saveData(); | ||||
|         this.saveData(); | ||||
|  | ||||
|     await refresh(); | ||||
| } | ||||
|         await this.refresh(); | ||||
|     } | ||||
|  | ||||
| function getMousePosition(evt) { | ||||
|     const rect = $relationMapContainer[0].getBoundingClientRect(); | ||||
|     getMousePosition(evt) { | ||||
|         const rect = this.$relationMapContainer[0].getBoundingClientRect(); | ||||
|  | ||||
|     const zoom = getZoom(); | ||||
|         const zoom = this.getZoom(); | ||||
|  | ||||
|         return { | ||||
|             x: (evt.clientX - rect.left) / zoom, | ||||
|             y: (evt.clientY - rect.top) / zoom | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     getContent() { | ||||
|         return JSON.stringify(this.mapData); | ||||
|     } | ||||
|  | ||||
|     focus() {} | ||||
|  | ||||
|     onNoteChange() {} | ||||
|  | ||||
|     scrollToTop() {} | ||||
| } | ||||
|  | ||||
| $resetPanZoomButton.click(() => { | ||||
|     // reset to initial pan & zoom state | ||||
|     pzInstance.zoomTo(0, 0, 1 / getZoom()); | ||||
|     pzInstance.moveTo(0, 0); | ||||
| }); | ||||
|  | ||||
| $component.on("drop", dropNoteOntoRelationMapHandler); | ||||
| $component.on("dragover", ev => ev.preventDefault()); | ||||
|  | ||||
| export default { | ||||
|     show, | ||||
|     getContent: () => JSON.stringify(mapData), | ||||
|     focus: () => null, | ||||
|     onNoteChange: () => null, | ||||
|     cleanup, | ||||
|     scrollToTop: () => null | ||||
| } | ||||
| export default NoteDetailRelationMap; | ||||
| @@ -3,40 +3,55 @@ import server from "./server.js"; | ||||
| import noteDetailService from "./note_detail.js"; | ||||
| import attributeService from "./attributes.js"; | ||||
|  | ||||
| const $component = $('#note-detail-render'); | ||||
| const $noteDetailRenderHelp = $('#note-detail-render-help'); | ||||
| const $noteDetailRenderContent = $('#note-detail-render-content'); | ||||
| const $renderButton = $('#render-button'); | ||||
| class NoteDetailRender { | ||||
|     /** | ||||
|      * @param {NoteContext} ctx | ||||
|      */ | ||||
|     constructor(ctx) { | ||||
|         this.ctx = ctx; | ||||
|         this.$component = ctx.$noteTabContent.find('.note-detail-render'); | ||||
|         this.$noteDetailRenderHelp = ctx.$noteTabContent.find('.note-detail-render-help'); | ||||
|         this.$noteDetailRenderContent = ctx.$noteTabContent.find('.note-detail-render-content'); | ||||
|         this.$renderButton = ctx.$noteTabContent.find('.render-button'); | ||||
|  | ||||
| async function render() { | ||||
|         this.$renderButton.click(this.show); | ||||
|     } | ||||
|  | ||||
|     async show() { | ||||
|         const attributes = await attributeService.getAttributes(); | ||||
|         const renderNotes = attributes.filter(attr => | ||||
|             attr.type === 'relation' | ||||
|             && attr.name === 'renderNote' | ||||
|             && !!attr.value); | ||||
|  | ||||
|     $component.show(); | ||||
|         this.$component.show(); | ||||
|  | ||||
|     $noteDetailRenderContent.empty(); | ||||
|     $noteDetailRenderContent.toggle(renderNotes.length > 0); | ||||
|     $noteDetailRenderHelp.toggle(renderNotes.length === 0); | ||||
|         this.$noteDetailRenderContent.empty(); | ||||
|         this.$noteDetailRenderContent.toggle(renderNotes.length > 0); | ||||
|         this.$noteDetailRenderHelp.toggle(renderNotes.length === 0); | ||||
|  | ||||
|         for (const renderNote of renderNotes) { | ||||
|             const bundle = await server.get('script/bundle/' + renderNote.value); | ||||
|  | ||||
|         $noteDetailRenderContent.append(bundle.html); | ||||
|             this.$noteDetailRenderContent.append(bundle.html); | ||||
|  | ||||
|             await bundleService.executeBundle(bundle, noteDetailService.getActiveNote()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     getContent() {} | ||||
|  | ||||
|     focus() {} | ||||
|  | ||||
|     onNoteChange() {} | ||||
|  | ||||
|     cleanup() { | ||||
|         this.$noteDetailRenderContent.empty(); | ||||
|     } | ||||
|  | ||||
|     scrollToTop() { | ||||
|         this.$component.scrollTop(0); | ||||
|     } | ||||
| } | ||||
|  | ||||
| $renderButton.click(render); | ||||
|  | ||||
| export default { | ||||
|     show: render, | ||||
|     getContent: () => "", | ||||
|     focus: () => null, | ||||
|     onNoteChange: () => null, | ||||
|     cleanup: () => $noteDetailRenderContent.empty(), | ||||
|     scrollToTop: () => $component.scrollTop(0) | ||||
| } | ||||
| export default NoteDetailRender; | ||||
| @@ -1,46 +1,55 @@ | ||||
| import noteDetailService from "./note_detail.js"; | ||||
| import searchNotesService from "./search_notes.js"; | ||||
|  | ||||
| const $searchString = $("#search-string"); | ||||
| const $component = $('#note-detail-search'); | ||||
| const $refreshButton = $('#note-detail-search-refresh-results-button'); | ||||
| const $help = $("#note-detail-search-help"); | ||||
| class NoteDetailSearch { | ||||
|     /** | ||||
|      * @param {NoteContext} ctx | ||||
|      */ | ||||
|     constructor(ctx) { | ||||
|         this.ctx = ctx; | ||||
|         this.$searchString = ctx.$noteTabContent.find(".search-string"); | ||||
|         this.$component = ctx.$noteTabContent.find('.note-detail-search'); | ||||
|         this.$help = ctx.$noteTabContent.find(".note-detail-search-help"); | ||||
|         this.$refreshButton = ctx.$noteTabContent.find('.note-detail-search-refresh-results-button'); | ||||
|  | ||||
| function show() { | ||||
|     $help.html(searchNotesService.getHelpText()); | ||||
|  | ||||
|     $component.show(); | ||||
|  | ||||
|     try { | ||||
|         const json = JSON.parse(noteDetailService.getActiveNote().content); | ||||
|  | ||||
|         $searchString.val(json.searchString); | ||||
|     } | ||||
|     catch (e) { | ||||
|         console.log(e); | ||||
|         $searchString.val(''); | ||||
|     } | ||||
|  | ||||
|     $searchString.on('input', noteDetailService.noteChanged); | ||||
| } | ||||
|  | ||||
| function getContent() { | ||||
|     return JSON.stringify({ | ||||
|         searchString: $searchString.val() | ||||
|     }); | ||||
| } | ||||
|  | ||||
| $refreshButton.click(async () => { | ||||
|         this.$refreshButton.click(async () => { | ||||
|             await noteDetailService.saveNotesIfChanged(); | ||||
|  | ||||
|             await searchNotesService.refreshSearch(); | ||||
| }); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
| export default { | ||||
|     getContent, | ||||
|     show, | ||||
|     focus: () => null, | ||||
|     onNoteChange: () => null, | ||||
|     cleanup: () => null, | ||||
|     scrollToTop: () => null | ||||
|     show() { | ||||
|         this.$help.html(searchNotesService.getHelpText()); | ||||
|  | ||||
|         this.$component.show(); | ||||
|  | ||||
|         try { | ||||
|             const json = JSON.parse(this.ctx.note.content); | ||||
|  | ||||
|             this.$searchString.val(json.searchString); | ||||
|         } | ||||
|         catch (e) { | ||||
|             console.log(e); | ||||
|             this.$searchString.val(''); | ||||
|         } | ||||
|  | ||||
|         this.$searchString.on('input', noteDetailService.noteChanged); | ||||
|     } | ||||
|  | ||||
|     getContent() { | ||||
|         return JSON.stringify({ | ||||
|             searchString: this.$searchString.val() | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     focus() {} | ||||
|  | ||||
|     onNoteChange() {} | ||||
|  | ||||
|     cleanup() {} | ||||
|  | ||||
|     scrollToTop() {} | ||||
| } | ||||
|  | ||||
| export default NoteDetailSearch; | ||||
| @@ -53,7 +53,7 @@ class NoteDetailText { | ||||
|  | ||||
|         this.$component.show(); | ||||
|  | ||||
| //        this.textEditor.setData(this.ctx.note.content); | ||||
|         this.textEditor.setData(this.ctx.note.content); | ||||
|     } | ||||
|  | ||||
|     getContent() { | ||||
|   | ||||
| @@ -103,8 +103,8 @@ ul.fancytree-container { | ||||
|     display: none; | ||||
| } | ||||
|  | ||||
| #note-tab-content-template { | ||||
|     display: none; | ||||
| .note-tab-content-template { | ||||
|     display: none !important; | ||||
| } | ||||
|  | ||||
| .note-tab-content { | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
| </div> | ||||
|  | ||||
| <div id="note-tab-container"> | ||||
|     <div id="note-tab-content-template" class="note-tab-content"> | ||||
|     <div class="note-tab-content note-tab-content-template"> | ||||
|         <% include title.ejs %> | ||||
|  | ||||
|         <div class="note-detail-script-area"></div> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user