mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +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; |     let ctx; | ||||||
|  |  | ||||||
|     if (noteContexts.length === 0 || newTab) { |     if (noteContexts.length === 0 || newTab) { | ||||||
|         const tabContent = $("#note-tab-content-template").clone(); |         const $tabContent = $(".note-tab-content-template").clone(); | ||||||
|  |  | ||||||
|         tabContent.removeAttr('id'); |         $tabContent.removeClass('note-tab-content-template'); | ||||||
|         tabContent.attr('data-note-id', noteId); |         $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 |         // if it's a new tab explicitly by user then it's in background | ||||||
|         ctx = new NoteContext(loadedNote, newTab); |         ctx = new NoteContext(loadedNote, newTab); | ||||||
|   | |||||||
| @@ -5,15 +5,26 @@ import server from "./server.js"; | |||||||
| import noteDetailService from "./note_detail.js"; | import noteDetailService from "./note_detail.js"; | ||||||
| import utils from "./utils.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); |         await libraryLoader.requireLibrary(libraryLoader.CODE_MIRROR); | ||||||
|  |  | ||||||
|     if (!codeEditor) { |         if (!this.codeEditor) { | ||||||
|             CodeMirror.keyMap.default["Shift-Tab"] = "indentLess"; |             CodeMirror.keyMap.default["Shift-Tab"] = "indentLess"; | ||||||
|             CodeMirror.keyMap.default["Tab"] = "indentMore"; |             CodeMirror.keyMap.default["Tab"] = "indentMore"; | ||||||
|  |  | ||||||
| @@ -23,7 +34,7 @@ async function show() { | |||||||
|  |  | ||||||
|             CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js'; |             CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js'; | ||||||
|  |  | ||||||
|         codeEditor = CodeMirror($component[0], { |             this.codeEditor = CodeMirror(this.$component[0], { | ||||||
|                 value: "", |                 value: "", | ||||||
|                 viewportMargin: Infinity, |                 viewportMargin: Infinity, | ||||||
|                 indentUnit: 4, |                 indentUnit: 4, | ||||||
| @@ -39,75 +50,67 @@ async function show() { | |||||||
|                 lineWrapping: true |                 lineWrapping: true | ||||||
|             }); |             }); | ||||||
|  |  | ||||||
|         onNoteChange(noteDetailService.noteChanged); |             this.onNoteChange(noteDetailService.noteChanged); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|     $component.show(); |         this.$component.show(); | ||||||
|  |  | ||||||
|     const activeNote = noteDetailService.getActiveNote(); |  | ||||||
|  |  | ||||||
|         // this needs to happen after the element is shown, otherwise the editor won't be refreshed |         // 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) |         // CodeMirror breaks pretty badly on null so even though it shouldn't happen (guarded by consistency check) | ||||||
|         // we provide fallback |         // 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) { |         if (info) { | ||||||
|         codeEditor.setOption("mode", info.mime); |             this.codeEditor.setOption("mode", info.mime); | ||||||
|         CodeMirror.autoLoadMode(codeEditor, info.mode); |             CodeMirror.autoLoadMode(this.codeEditor, info.mode); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|     codeEditor.refresh(); |         this.codeEditor.refresh(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| function getContent() { |     getContent() { | ||||||
|     return codeEditor.getValue(); |         return this.codeEditor.getValue(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| function focus() { |     focus() { | ||||||
|     codeEditor.focus(); |         this.codeEditor.focus(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| async function executeCurrentNote() { |     async executeCurrentNote() { | ||||||
|         // ctrl+enter is also used elsewhere so make sure we're running only when appropriate |         // 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; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         // make sure note is saved so we load latest changes |         // make sure note is saved so we load latest changes | ||||||
|         await noteDetailService.saveNotesIfChanged(); |         await noteDetailService.saveNotesIfChanged(); | ||||||
|  |  | ||||||
|     const activeNote = noteDetailService.getActiveNote(); |         if (this.ctx.note.mime.endsWith("env=frontend")) { | ||||||
|  |             await bundleService.getAndExecuteBundle(this.ctx.note.noteId); | ||||||
|     if (activeNote.mime.endsWith("env=frontend")) { |  | ||||||
|         await bundleService.getAndExecuteBundle(noteDetailService.getActiveNoteId()); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|     if (activeNote.mime.endsWith("env=backend")) { |         if (this.ctx.note.mime.endsWith("env=backend")) { | ||||||
|         await server.post('script/run/' + noteDetailService.getActiveNoteId()); |             await server.post('script/run/' + this.ctx.note.noteId); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         infoService.showMessage("Note executed"); |         infoService.showMessage("Note executed"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| function onNoteChange(func) { |     onNoteChange(func) { | ||||||
|     codeEditor.on('change', func); |         this.codeEditor.on('change', func); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| utils.bindShortcut("ctrl+return", executeCurrentNote); |     cleanup() { | ||||||
|  |         if (this.codeEditor) { | ||||||
| $executeScriptButton.click(executeCurrentNote); |             this.codeEditor.setValue(''); | ||||||
|  |  | ||||||
| export default { |  | ||||||
|     show, |  | ||||||
|     getContent, |  | ||||||
|     focus, |  | ||||||
|     onNoteChange, |  | ||||||
|     cleanup: () => { |  | ||||||
|         if (codeEditor) { |  | ||||||
|             codeEditor.setValue(''); |  | ||||||
|         } |         } | ||||||
|     }, |  | ||||||
|     scrollToTop: () => $component.scrollTop(0) |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     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 infoDialog from "../dialogs/info.js"; | ||||||
| import confirmDialog from "../dialogs/confirm.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 = [ | const uniDirectionalOverlays = [ | ||||||
|     [ "Arrow", { |     [ "Arrow", { | ||||||
|         location: 1, |         location: 1, | ||||||
| @@ -77,339 +64,86 @@ const linkOverlays = [ | |||||||
|     } ] |     } ] | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| function loadMapData() { | let containerCounter = 1; | ||||||
|     const activeNote = noteDetailService.getActiveNote(); |  | ||||||
|     mapData = { | class NoteDetailRelationMap { | ||||||
|         notes: [], |     /** | ||||||
|         // it is important to have this exact value here so that initial transform is same as this |      * @param {NoteContext} ctx | ||||||
|         // which will guarantee note won't be saved on first conversion to relation map note type |      */ | ||||||
|         // this keeps the principle that note type change doesn't destroy note content unless user |     constructor(ctx) { | ||||||
|         // does some actual change |         this.ctx = ctx; | ||||||
|         transform: { |         this.$component = ctx.$noteTabContent.find(".note-detail-relation-map"); | ||||||
|             x: 0, |         this.$relationMapContainer = ctx.$noteTabContent.find(".relation-map-container"); | ||||||
|             y: 0, |         this.$createChildNote = ctx.$noteTabContent.find(".relation-map-create-child-note"); | ||||||
|             scale: 1 |         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"); | ||||||
|  |  | ||||||
|     if (activeNote.content) { |         this.mapData = null; | ||||||
|         try { |         this.jsPlumbInstance = null; | ||||||
|             mapData = JSON.parse(activeNote.content); |         // outside of mapData because they are not persisted in the note content | ||||||
|         } catch (e) { |         this.relations = null; | ||||||
|             console.log("Could not parse content: ", e); |         this.pzInstance = null; | ||||||
|         } |  | ||||||
|     } |         this.$relationMapContainer.attr("id", "relation-map-container-" + (containerCounter++)); | ||||||
| } |         this.$relationMapContainer.on("contextmenu", ".note-box", e => { | ||||||
|  |             contextMenuWidget.initContextMenu(e, { | ||||||
| function noteIdToId(noteId) { |                 getContextMenuItems: () => { | ||||||
|     return "rel-map-note-" + noteId; |                     return [ | ||||||
| } |  | ||||||
|  |  | ||||||
| function idToNoteId(id) { |  | ||||||
|     return id.substr(13); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function show() { |  | ||||||
|     $component.show(); |  | ||||||
|  |  | ||||||
|     await libraryLoader.requireLibrary(libraryLoader.RELATION_MAP); |  | ||||||
|  |  | ||||||
|     loadMapData(); |  | ||||||
|  |  | ||||||
|     jsPlumb.ready(() => { |  | ||||||
|         initJsPlumbInstance(); |  | ||||||
|  |  | ||||||
|         initPanZoom(); |  | ||||||
|  |  | ||||||
|         loadNotesAndRelations(); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function clearMap() { |  | ||||||
|     // delete all endpoints and connections |  | ||||||
|     // this is done at this point (after async operations) to reduce flicker to the minimum |  | ||||||
|     jsPlumbInstance.deleteEveryEndpoint(); |  | ||||||
|  |  | ||||||
|     // without this we still end up with note boxes remaining in the canvas |  | ||||||
|     $relationMapContainer.empty(); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function loadNotesAndRelations() { |  | ||||||
|     const noteIds = mapData.notes.map(note => note.noteId); |  | ||||||
|     const data = await server.post("notes/relation-map", {noteIds}); |  | ||||||
|  |  | ||||||
|     relations = []; |  | ||||||
|  |  | ||||||
|     for (const relation of data.relations) { |  | ||||||
|         const match = 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))); |  | ||||||
|  |  | ||||||
|         if (match) { |  | ||||||
|             match.type = relation.type = relation.name === data.inverseRelations[relation.name] ? 'biDirectional' : 'inverse'; |  | ||||||
|             relation.render = false; // don't render second relation |  | ||||||
|         } else { |  | ||||||
|             relation.type = 'uniDirectional'; |  | ||||||
|             relation.render = true; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         relations.push(relation); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     mapData.notes = mapData.notes.filter(note => note.noteId in data.noteTitles); |  | ||||||
|  |  | ||||||
|     jsPlumbInstance.batch(async function () { |  | ||||||
|         clearMap(); |  | ||||||
|  |  | ||||||
|         for (const note of mapData.notes) { |  | ||||||
|             const title = data.noteTitles[note.noteId]; |  | ||||||
|  |  | ||||||
|             await createNoteBox(note.noteId, title, note.x, note.y); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         for (const relation of relations) { |  | ||||||
|             if (!relation.render) { |  | ||||||
|                 continue; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             const connection = jsPlumbInstance.connect({ |  | ||||||
|                 source: noteIdToId(relation.sourceNoteId), |  | ||||||
|                 target: noteIdToId(relation.targetNoteId), |  | ||||||
|                 type: relation.type |  | ||||||
|             }); |  | ||||||
|  |  | ||||||
|             connection.id = relation.attributeId; |  | ||||||
|  |  | ||||||
|             if (relation.type === 'inverse') { |  | ||||||
|                 connection.getOverlay("label-source").setLabel(relation.name); |  | ||||||
|                 connection.getOverlay("label-target").setLabel(data.inverseRelations[relation.name]); |  | ||||||
|             } |  | ||||||
|             else { |  | ||||||
|                 connection.getOverlay("label").setLabel(relation.name); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             connection.canvas.setAttribute("data-connection-id", connection.id); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         for (const link of data.links) { |  | ||||||
|             jsPlumbInstance.connect({ |  | ||||||
|                 source: noteIdToId(link.sourceNoteId), |  | ||||||
|                 target: noteIdToId(link.targetNoteId), |  | ||||||
|                 type: 'link' |  | ||||||
|             }); |  | ||||||
|         } |  | ||||||
|     }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function initPanZoom() { |  | ||||||
|     if (pzInstance) { |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     pzInstance = panzoom($relationMapContainer[0], { |  | ||||||
|         maxZoom: 2, |  | ||||||
|         minZoom: 0.3, |  | ||||||
|         smoothScroll: false, |  | ||||||
|         onMouseDown: function(event) { |  | ||||||
|             if (clipboard) { |  | ||||||
|                 let {x, y} = 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); |  | ||||||
|  |  | ||||||
|                 mapData.notes.push({ noteId: clipboard.noteId, x, y }); |  | ||||||
|  |  | ||||||
|                 saveData(); |  | ||||||
|  |  | ||||||
|                 clipboard = null; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             return true; |  | ||||||
|         }, |  | ||||||
|         filterKey: function(e, dx, dy, dz) { |  | ||||||
|             // if ALT is pressed then panzoom should bubble the event up |  | ||||||
|             // this is to preserve ALT-LEFT, ALT-RIGHT navigation working |  | ||||||
|             return e.altKey; |  | ||||||
|         } |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     pzInstance.on('transform', () => { // gets triggered on any transform change |  | ||||||
|         jsPlumbInstance.setZoom(getZoom()); |  | ||||||
|  |  | ||||||
|         saveCurrentTransform(); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     if (mapData.transform) { |  | ||||||
|         pzInstance.zoomTo(0, 0, mapData.transform.scale); |  | ||||||
|  |  | ||||||
|         pzInstance.moveTo(mapData.transform.x, mapData.transform.y); |  | ||||||
|     } |  | ||||||
|     else { |  | ||||||
|         // set to initial coordinates |  | ||||||
|         pzInstance.moveTo(0, 0); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     $zoomInButton.click(() => pzInstance.zoomTo(0, 0, 1.2)); |  | ||||||
|     $zoomOutButton.click(() => pzInstance.zoomTo(0, 0, 0.8)); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function saveCurrentTransform() { |  | ||||||
|     const newTransform = pzInstance.getTransform(); |  | ||||||
|  |  | ||||||
|     if (JSON.stringify(newTransform) !== JSON.stringify(mapData.transform)) { |  | ||||||
|         // clone transform object |  | ||||||
|         mapData.transform = JSON.parse(JSON.stringify(newTransform)); |  | ||||||
|  |  | ||||||
|         saveData(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function cleanup() { |  | ||||||
|     if (jsPlumbInstance) { |  | ||||||
|         clearMap(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (pzInstance) { |  | ||||||
|         pzInstance.dispose(); |  | ||||||
|         pzInstance = null; |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function initJsPlumbInstance () { |  | ||||||
|     if (jsPlumbInstance) { |  | ||||||
|         cleanup(); |  | ||||||
|  |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     jsPlumbInstance = jsPlumb.getInstance({ |  | ||||||
|         Endpoint: ["Dot", {radius: 2}], |  | ||||||
|         Connector: "StateMachine", |  | ||||||
|         ConnectionOverlays: uniDirectionalOverlays, |  | ||||||
|         HoverPaintStyle: { stroke: "#777", strokeWidth: 1 }, |  | ||||||
|         Container: "relation-map-container" |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     jsPlumbInstance.registerConnectionType("uniDirectional", { anchor:"Continuous", connector:"StateMachine", overlays: uniDirectionalOverlays }); |  | ||||||
|  |  | ||||||
|     jsPlumbInstance.registerConnectionType("biDirectional", { anchor:"Continuous", connector:"StateMachine", overlays: biDirectionalOverlays }); |  | ||||||
|  |  | ||||||
|     jsPlumbInstance.registerConnectionType("inverse", { anchor:"Continuous", connector:"StateMachine", overlays: inverseRelationsOverlays }); |  | ||||||
|  |  | ||||||
|     jsPlumbInstance.registerConnectionType("link", { anchor:"Continuous", connector:"StateMachine", overlays: linkOverlays }); |  | ||||||
|  |  | ||||||
|     jsPlumbInstance.bind("connection", connectionCreatedHandler); |  | ||||||
|  |  | ||||||
|     // 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; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             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) { |  | ||||||
|     const connection = info.connection; |  | ||||||
|  |  | ||||||
|     connection.bind("contextmenu", (obj, event) => { |  | ||||||
|         if (connection.getType().includes("link")) { |  | ||||||
|             // don't create context menu if it's a link since there's nothing to do with link from relation map |  | ||||||
|             // (don't open browser menu either) |  | ||||||
|             event.preventDefault(); |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             connectionContextMenuHandler(connection, event); |  | ||||||
|         } |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     // if there's no event, then this has been triggered programatically |  | ||||||
|     if (!originalEvent) { |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const name = await promptDialog.ask({ |  | ||||||
|         message: "Specify new relation name:", |  | ||||||
|         shown: ({ $answer }) => |  | ||||||
|             attributeAutocompleteService.initAttributeNameAutocomplete({ |  | ||||||
|                 $el: $answer, |  | ||||||
|                 attributeType: "relation", |  | ||||||
|                 open: true |  | ||||||
|             }) |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     if (!name || !name.trim()) { |  | ||||||
|         jsPlumbInstance.deleteConnection(connection); |  | ||||||
|  |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const targetNoteId = idToNoteId(connection.target.id); |  | ||||||
|     const sourceNoteId = idToNoteId(connection.source.id); |  | ||||||
|  |  | ||||||
|     const relationExists = relations.some(rel => |  | ||||||
|         rel.targetNoteId === targetNoteId |  | ||||||
|         && rel.sourceNoteId === sourceNoteId |  | ||||||
|         && rel.name === name); |  | ||||||
|  |  | ||||||
|     if (relationExists) { |  | ||||||
|         await infoDialog.info("Connection '" + name + "' between these notes already exists."); |  | ||||||
|  |  | ||||||
|         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: "Remove note", cmd: "remove", uiIcon: "trash"}, | ||||||
|                         {title: "Edit title", cmd: "edit-title", uiIcon: "pencil"}, |                         {title: "Edit title", cmd: "edit-title", uiIcon: "pencil"}, | ||||||
|                     ]; |                     ]; | ||||||
|  |                 }, | ||||||
|     contextMenuWidget.initContextMenu(e, items, noteContextMenuHandler); |                 selectContextMenuItem: (event, cmd) => this.noteContextMenuHandler(event, cmd) | ||||||
|  |             }); | ||||||
|  |  | ||||||
|             return false; |             return false; | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
| async function noteContextMenuHandler(event, cmd) { |         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 $noteBox = $(event.originalTarget).closest(".note-box"); | ||||||
|         const $title = $noteBox.find(".title a"); |         const $title = $noteBox.find(".title a"); | ||||||
|     const noteId = idToNoteId($noteBox.prop("id")); |         const noteId = this.idToNoteId($noteBox.prop("id")); | ||||||
|  |  | ||||||
|         if (cmd === "remove") { |         if (cmd === "remove") { | ||||||
|             if (!await confirmDialog.confirmDeleteNoteBoxWithNote($title.text())) { |             if (!await confirmDialog.confirmDeleteNoteBoxWithNote($title.text())) { | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|         jsPlumbInstance.remove(noteIdToId(noteId)); |             this.jsPlumbInstance.remove(this.noteIdToId(noteId)); | ||||||
|  |  | ||||||
|             if (confirmDialog.isDeleteNoteChecked()) { |             if (confirmDialog.isDeleteNoteChecked()) { | ||||||
|                 await server.remove("notes/" + noteId); |                 await server.remove("notes/" + noteId); | ||||||
| @@ -418,11 +152,11 @@ async function noteContextMenuHandler(event, cmd) { | |||||||
|                 treeService.reload(); |                 treeService.reload(); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|         mapData.notes = mapData.notes.filter(note => note.noteId !== noteId); |             this.mapData.notes = this.mapData.notes.filter(note => note.noteId !== noteId); | ||||||
|  |  | ||||||
|         relations = relations.filter(relation => relation.sourceNoteId !== noteId && relation.targetNoteId !== noteId); |             this.relations = this.relations.filter(relation => relation.sourceNoteId !== noteId && relation.targetNoteId !== noteId); | ||||||
|  |  | ||||||
|         saveData(); |             this.saveData(); | ||||||
|         } |         } | ||||||
|         else if (cmd === "edit-title") { |         else if (cmd === "edit-title") { | ||||||
|             const title = await promptDialog.ask({ |             const title = await promptDialog.ask({ | ||||||
| @@ -442,28 +176,337 @@ async function noteContextMenuHandler(event, cmd) { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| function saveData() { |     loadMapData() { | ||||||
|     noteDetailService.noteChanged(); |         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 | ||||||
|  |             // this keeps the principle that note type change doesn't destroy note content unless user | ||||||
|  |             // does some actual change | ||||||
|  |             transform: { | ||||||
|  |                 x: 0, | ||||||
|  |                 y: 0, | ||||||
|  |                 scale: 1 | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         if (this.ctx.note.content) { | ||||||
|  |             try { | ||||||
|  |                 this.mapData = JSON.parse(this.ctx.note.content); | ||||||
|  |             } catch (e) { | ||||||
|  |                 console.log("Could not parse content: ", e); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| async function createNoteBox(noteId, title, x, y) { |     noteIdToId(noteId) { | ||||||
|  |         return "rel-map-note-" + noteId; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     idToNoteId(id) { | ||||||
|  |         return id.substr(13); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async show() { | ||||||
|  |         this.$component.show(); | ||||||
|  |  | ||||||
|  |         await libraryLoader.requireLibrary(libraryLoader.RELATION_MAP); | ||||||
|  |  | ||||||
|  |         this.loadMapData(); | ||||||
|  |  | ||||||
|  |         jsPlumb.ready(() => { | ||||||
|  |             this.initJsPlumbInstance(); | ||||||
|  |  | ||||||
|  |             this.initPanZoom(); | ||||||
|  |  | ||||||
|  |             this.loadNotesAndRelations(); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     clearMap() { | ||||||
|  |         // delete all endpoints and connections | ||||||
|  |         // this is done at this point (after async operations) to reduce flicker to the minimum | ||||||
|  |         this.jsPlumbInstance.deleteEveryEndpoint(); | ||||||
|  |  | ||||||
|  |         // without this we still end up with note boxes remaining in the canvas | ||||||
|  |         this.$relationMapContainer.empty(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async loadNotesAndRelations() { | ||||||
|  |         const noteIds = this.mapData.notes.map(note => note.noteId); | ||||||
|  |         const data = await server.post("notes/relation-map", {noteIds}); | ||||||
|  |  | ||||||
|  |         this.relations = []; | ||||||
|  |  | ||||||
|  |         for (const relation of data.relations) { | ||||||
|  |             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))); | ||||||
|  |  | ||||||
|  |             if (match) { | ||||||
|  |                 match.type = relation.type = relation.name === data.inverseRelations[relation.name] ? 'biDirectional' : 'inverse'; | ||||||
|  |                 relation.render = false; // don't render second relation | ||||||
|  |             } else { | ||||||
|  |                 relation.type = 'uniDirectional'; | ||||||
|  |                 relation.render = true; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             this.relations.push(relation); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         this.mapData.notes = this.mapData.notes.filter(note => note.noteId in data.noteTitles); | ||||||
|  |  | ||||||
|  |         this.jsPlumbInstance.batch(async () => { | ||||||
|  |             this.clearMap(); | ||||||
|  |  | ||||||
|  |             for (const note of this.mapData.notes) { | ||||||
|  |                 const title = data.noteTitles[note.noteId]; | ||||||
|  |  | ||||||
|  |                 await this.createNoteBox(note.noteId, title, note.x, note.y); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             for (const relation of this.relations) { | ||||||
|  |                 if (!relation.render) { | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 const connection = this.jsPlumbInstance.connect({ | ||||||
|  |                     source: this.noteIdToId(relation.sourceNoteId), | ||||||
|  |                     target: this.noteIdToId(relation.targetNoteId), | ||||||
|  |                     type: relation.type | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |                 connection.id = relation.attributeId; | ||||||
|  |  | ||||||
|  |                 if (relation.type === 'inverse') { | ||||||
|  |                     connection.getOverlay("label-source").setLabel(relation.name); | ||||||
|  |                     connection.getOverlay("label-target").setLabel(data.inverseRelations[relation.name]); | ||||||
|  |                 } | ||||||
|  |                 else { | ||||||
|  |                     connection.getOverlay("label").setLabel(relation.name); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 connection.canvas.setAttribute("data-connection-id", connection.id); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             for (const link of data.links) { | ||||||
|  |                 this.jsPlumbInstance.connect({ | ||||||
|  |                     source: this.noteIdToId(link.sourceNoteId), | ||||||
|  |                     target: this.noteIdToId(link.targetNoteId), | ||||||
|  |                     type: 'link' | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     initPanZoom() { | ||||||
|  |         if (this.pzInstance) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         this.pzInstance = panzoom(this.$relationMapContainer[0], { | ||||||
|  |             maxZoom: 2, | ||||||
|  |             minZoom: 0.3, | ||||||
|  |             smoothScroll: false, | ||||||
|  |             onMouseDown: function(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; | ||||||
|  |  | ||||||
|  |                     this.createNoteBox(this.clipboard.noteId, this.clipboard.title, x, y); | ||||||
|  |  | ||||||
|  |                     this.mapData.notes.push({ noteId: this.clipboard.noteId, x, y }); | ||||||
|  |  | ||||||
|  |                     this.saveData(); | ||||||
|  |  | ||||||
|  |                     this.clipboard = null; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 return true; | ||||||
|  |             }, | ||||||
|  |             filterKey: function(e, dx, dy, dz) { | ||||||
|  |                 // if ALT is pressed then panzoom should bubble the event up | ||||||
|  |                 // this is to preserve ALT-LEFT, ALT-RIGHT navigation working | ||||||
|  |                 return e.altKey; | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         this.pzInstance.on('transform', () => { // gets triggered on any transform change | ||||||
|  |             this.jsPlumbInstance.setZoom(this.getZoom()); | ||||||
|  |  | ||||||
|  |             this.saveCurrentTransform(); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         if (this.mapData.transform) { | ||||||
|  |             this.pzInstance.zoomTo(0, 0, this.mapData.transform.scale); | ||||||
|  |  | ||||||
|  |             this.pzInstance.moveTo(this.mapData.transform.x, this.mapData.transform.y); | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             // set to initial coordinates | ||||||
|  |             this.pzInstance.moveTo(0, 0); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         this.$zoomInButton.click(() => this.pzInstance.zoomTo(0, 0, 1.2)); | ||||||
|  |         this.$zoomOutButton.click(() => this.pzInstance.zoomTo(0, 0, 0.8)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     saveCurrentTransform() { | ||||||
|  |         const newTransform = this.pzInstance.getTransform(); | ||||||
|  |  | ||||||
|  |         if (JSON.stringify(newTransform) !== JSON.stringify(this.mapData.transform)) { | ||||||
|  |             // clone transform object | ||||||
|  |             this.mapData.transform = JSON.parse(JSON.stringify(newTransform)); | ||||||
|  |  | ||||||
|  |             this.saveData(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     cleanup() { | ||||||
|  |         if (this.jsPlumbInstance) { | ||||||
|  |             this.clearMap(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (this.pzInstance) { | ||||||
|  |             this.pzInstance.dispose(); | ||||||
|  |             this.pzInstance = null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     initJsPlumbInstance () { | ||||||
|  |         if (this.jsPlumbInstance) { | ||||||
|  |             this.cleanup(); | ||||||
|  |  | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         this.jsPlumbInstance = jsPlumb.getInstance({ | ||||||
|  |             Endpoint: ["Dot", {radius: 2}], | ||||||
|  |             Connector: "StateMachine", | ||||||
|  |             ConnectionOverlays: uniDirectionalOverlays, | ||||||
|  |             HoverPaintStyle: { stroke: "#777", strokeWidth: 1 }, | ||||||
|  |             Container: this.$relationMapContainer.attr("id") | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         this.jsPlumbInstance.registerConnectionType("uniDirectional", { anchor:"Continuous", connector:"StateMachine", overlays: uniDirectionalOverlays }); | ||||||
|  |  | ||||||
|  |         this.jsPlumbInstance.registerConnectionType("biDirectional", { anchor:"Continuous", connector:"StateMachine", overlays: biDirectionalOverlays }); | ||||||
|  |  | ||||||
|  |         this.jsPlumbInstance.registerConnectionType("inverse", { anchor:"Continuous", connector:"StateMachine", overlays: inverseRelationsOverlays }); | ||||||
|  |  | ||||||
|  |         this.jsPlumbInstance.registerConnectionType("link", { anchor:"Continuous", connector:"StateMachine", overlays: linkOverlays }); | ||||||
|  |  | ||||||
|  |         this.jsPlumbInstance.bind("connection", (info, originalEvent) => this.connectionCreatedHandler(info, originalEvent)); | ||||||
|  |  | ||||||
|  |         // so that canvas is not panned when clicking/dragging note box | ||||||
|  |         this.$relationMapContainer.on('mousedown touchstart', '.note-box, .connection-label', e => e.stopPropagation()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async connectionCreatedHandler(info, originalEvent) { | ||||||
|  |         const connection = info.connection; | ||||||
|  |  | ||||||
|  |         connection.bind("contextmenu", (obj, event) => { | ||||||
|  |             if (connection.getType().includes("link")) { | ||||||
|  |                 // don't create context menu if it's a link since there's nothing to do with link from relation map | ||||||
|  |                 // (don't open browser menu either) | ||||||
|  |                 event.preventDefault(); | ||||||
|  |             } | ||||||
|  |             else { | ||||||
|  |                 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); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         // if there's no event, then this has been triggered programatically | ||||||
|  |         if (!originalEvent) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const name = await promptDialog.ask({ | ||||||
|  |             message: "Specify new relation name:", | ||||||
|  |             shown: ({ $answer }) => | ||||||
|  |                 attributeAutocompleteService.initAttributeNameAutocomplete({ | ||||||
|  |                     $el: $answer, | ||||||
|  |                     attributeType: "relation", | ||||||
|  |                     open: true | ||||||
|  |                 }) | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         if (!name || !name.trim()) { | ||||||
|  |             this.jsPlumbInstance.deleteConnection(connection); | ||||||
|  |  | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const targetNoteId = this.idToNoteId(connection.target.id); | ||||||
|  |         const sourceNoteId = this.idToNoteId(connection.source.id); | ||||||
|  |  | ||||||
|  |         const relationExists = this.relations.some(rel => | ||||||
|  |             rel.targetNoteId === targetNoteId | ||||||
|  |             && rel.sourceNoteId === sourceNoteId | ||||||
|  |             && rel.name === name); | ||||||
|  |  | ||||||
|  |         if (relationExists) { | ||||||
|  |             await infoDialog.info("Connection '" + name + "' between these notes already exists."); | ||||||
|  |  | ||||||
|  |             this.jsPlumbInstance.deleteConnection(connection); | ||||||
|  |  | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         await server.put(`notes/${sourceNoteId}/relations/${name}/to/${targetNoteId}`); | ||||||
|  |  | ||||||
|  |         await this.refresh(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     saveData() { | ||||||
|  |         this.ctx.noteChanged(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async createNoteBox(noteId, title, x, y) { | ||||||
|         const $noteBox = $("<div>") |         const $noteBox = $("<div>") | ||||||
|             .addClass("note-box") |             .addClass("note-box") | ||||||
|         .prop("id", noteIdToId(noteId)) |             .prop("id", this.noteIdToId(noteId)) | ||||||
|             .append($("<span>").addClass("title").html(await linkService.createNoteLink(noteId, title))) |             .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.")) |             .append($("<div>").addClass("endpoint").attr("title", "Start dragging relations from here and drop them on another note.")) | ||||||
|             .css("left", x + "px") |             .css("left", x + "px") | ||||||
|             .css("top", y + "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 => {}, |             start: params => {}, | ||||||
|             drag: params => {}, |             drag: params => {}, | ||||||
|             stop: 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) { |                 if (!note) { | ||||||
|                     console.error(`Note ${noteId} not found!`); |                     console.error(`Note ${noteId} not found!`); | ||||||
| @@ -472,11 +515,11 @@ async function createNoteBox(noteId, title, x, y) { | |||||||
|  |  | ||||||
|                 [note.x, note.y] = params.finalPos; |                 [note.x, note.y] = params.finalPos; | ||||||
|  |  | ||||||
|             saveData(); |                 this.saveData(); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|     jsPlumbInstance.makeSource($noteBox[0], { |         this.jsPlumbInstance.makeSource($noteBox[0], { | ||||||
|             filter: ".endpoint", |             filter: ".endpoint", | ||||||
|             anchor: "Continuous", |             anchor: "Continuous", | ||||||
|             connectorStyle: { stroke: "#000", strokeWidth: 1 }, |             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" }, |             dropOptions: { hoverClass: "dragHover" }, | ||||||
|             anchor: "Continuous", |             anchor: "Continuous", | ||||||
|             allowLoopback: true |             allowLoopback: true | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| async function refresh() { |     async refresh() { | ||||||
|     await loadNotesAndRelations(); |         await this.loadNotesAndRelations(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| let clipboard = null; |     getZoom() { | ||||||
|  |  | ||||||
| $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' |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     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() { |  | ||||||
|         const matrixRegex = /matrix\((-?\d*\.?\d+),\s*0,\s*0,\s*-?\d*\.?\d+,\s*-?\d*\.?\d+,\s*-?\d*\.?\d+\)/; |         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]; |         return matches[1]; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| async function dropNoteOntoRelationMapHandler(ev) { |     async dropNoteOntoRelationMapHandler(ev) { | ||||||
|         ev.preventDefault(); |         ev.preventDefault(); | ||||||
|  |  | ||||||
|         const note = JSON.parse(ev.originalEvent.dataTransfer.getData("text")); |         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) { |         if (exists) { | ||||||
|             await infoDialog.info(`Note "${note.title}" is already placed into the diagram`); |             await infoDialog.info(`Note "${note.title}" is already placed into the diagram`); | ||||||
| @@ -543,17 +563,17 @@ async function dropNoteOntoRelationMapHandler(ev) { | |||||||
|             return; |             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) { |     getMousePosition(evt) { | ||||||
|     const rect = $relationMapContainer[0].getBoundingClientRect(); |         const rect = this.$relationMapContainer[0].getBoundingClientRect(); | ||||||
|  |  | ||||||
|     const zoom = getZoom(); |         const zoom = this.getZoom(); | ||||||
|  |  | ||||||
|         return { |         return { | ||||||
|             x: (evt.clientX - rect.left) / zoom, |             x: (evt.clientX - rect.left) / zoom, | ||||||
| @@ -561,20 +581,15 @@ function getMousePosition(evt) { | |||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| $resetPanZoomButton.click(() => { |     getContent() { | ||||||
|     // reset to initial pan & zoom state |         return JSON.stringify(this.mapData); | ||||||
|     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 |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     focus() {} | ||||||
|  |  | ||||||
|  |     onNoteChange() {} | ||||||
|  |  | ||||||
|  |     scrollToTop() {} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default NoteDetailRelationMap; | ||||||
| @@ -3,40 +3,55 @@ import server from "./server.js"; | |||||||
| import noteDetailService from "./note_detail.js"; | import noteDetailService from "./note_detail.js"; | ||||||
| import attributeService from "./attributes.js"; | import attributeService from "./attributes.js"; | ||||||
|  |  | ||||||
| const $component = $('#note-detail-render'); | class NoteDetailRender { | ||||||
| const $noteDetailRenderHelp = $('#note-detail-render-help'); |     /** | ||||||
| const $noteDetailRenderContent = $('#note-detail-render-content'); |      * @param {NoteContext} ctx | ||||||
| const $renderButton = $('#render-button'); |      */ | ||||||
|  |     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 attributes = await attributeService.getAttributes(); | ||||||
|         const renderNotes = attributes.filter(attr => |         const renderNotes = attributes.filter(attr => | ||||||
|             attr.type === 'relation' |             attr.type === 'relation' | ||||||
|             && attr.name === 'renderNote' |             && attr.name === 'renderNote' | ||||||
|             && !!attr.value); |             && !!attr.value); | ||||||
|  |  | ||||||
|     $component.show(); |         this.$component.show(); | ||||||
|  |  | ||||||
|     $noteDetailRenderContent.empty(); |         this.$noteDetailRenderContent.empty(); | ||||||
|     $noteDetailRenderContent.toggle(renderNotes.length > 0); |         this.$noteDetailRenderContent.toggle(renderNotes.length > 0); | ||||||
|     $noteDetailRenderHelp.toggle(renderNotes.length === 0); |         this.$noteDetailRenderHelp.toggle(renderNotes.length === 0); | ||||||
|  |  | ||||||
|         for (const renderNote of renderNotes) { |         for (const renderNote of renderNotes) { | ||||||
|             const bundle = await server.get('script/bundle/' + renderNote.value); |             const bundle = await server.get('script/bundle/' + renderNote.value); | ||||||
|  |  | ||||||
|         $noteDetailRenderContent.append(bundle.html); |             this.$noteDetailRenderContent.append(bundle.html); | ||||||
|  |  | ||||||
|             await bundleService.executeBundle(bundle, noteDetailService.getActiveNote()); |             await bundleService.executeBundle(bundle, noteDetailService.getActiveNote()); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| $renderButton.click(render); |     getContent() {} | ||||||
|  |  | ||||||
| export default { |     focus() {} | ||||||
|     show: render, |  | ||||||
|     getContent: () => "", |     onNoteChange() {} | ||||||
|     focus: () => null, |  | ||||||
|     onNoteChange: () => null, |     cleanup() { | ||||||
|     cleanup: () => $noteDetailRenderContent.empty(), |         this.$noteDetailRenderContent.empty(); | ||||||
|     scrollToTop: () => $component.scrollTop(0) |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     scrollToTop() { | ||||||
|  |         this.$component.scrollTop(0); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default NoteDetailRender; | ||||||
| @@ -1,46 +1,55 @@ | |||||||
| import noteDetailService from "./note_detail.js"; | import noteDetailService from "./note_detail.js"; | ||||||
| import searchNotesService from "./search_notes.js"; | import searchNotesService from "./search_notes.js"; | ||||||
|  |  | ||||||
| const $searchString = $("#search-string"); | class NoteDetailSearch { | ||||||
| const $component = $('#note-detail-search'); |     /** | ||||||
| const $refreshButton = $('#note-detail-search-refresh-results-button'); |      * @param {NoteContext} ctx | ||||||
| const $help = $("#note-detail-search-help"); |      */ | ||||||
|  |     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() { |         this.$refreshButton.click(async () => { | ||||||
|     $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 () => { |  | ||||||
|             await noteDetailService.saveNotesIfChanged(); |             await noteDetailService.saveNotesIfChanged(); | ||||||
|  |  | ||||||
|             await searchNotesService.refreshSearch(); |             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.$component.show(); | ||||||
|  |  | ||||||
| //        this.textEditor.setData(this.ctx.note.content); |         this.textEditor.setData(this.ctx.note.content); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     getContent() { |     getContent() { | ||||||
|   | |||||||
| @@ -103,8 +103,8 @@ ul.fancytree-container { | |||||||
|     display: none; |     display: none; | ||||||
| } | } | ||||||
|  |  | ||||||
| #note-tab-content-template { | .note-tab-content-template { | ||||||
|     display: none; |     display: none !important; | ||||||
| } | } | ||||||
|  |  | ||||||
| .note-tab-content { | .note-tab-content { | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ | |||||||
| </div> | </div> | ||||||
|  |  | ||||||
| <div id="note-tab-container"> | <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 %> |         <% include title.ejs %> | ||||||
|  |  | ||||||
|         <div class="note-detail-script-area"></div> |         <div class="note-detail-script-area"></div> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user