diff --git a/src/public/javascripts/services/note_detail_relation_map.js b/src/public/javascripts/services/note_detail_relation_map.js index 1fc07405c..dfb2d7e80 100644 --- a/src/public/javascripts/services/note_detail_relation_map.js +++ b/src/public/javascripts/services/note_detail_relation_map.js @@ -3,11 +3,306 @@ import noteDetailService from "./note_detail.js"; import libraryLoader from "./library_loader.js"; const $noteDetailRelationMap = $("#note-detail-relation-map"); +const $relationMapCanvas = $("#relation-map-canvas"); async function show() { $noteDetailRelationMap.show(); await libraryLoader.requireLibrary(libraryLoader.RELATION_MAP); + + jsPlumb.ready(function () { + const instance = jsPlumb.getInstance({ + Endpoint: ["Dot", {radius: 2}], + Connector: "StateMachine", + HoverPaintStyle: {stroke: "#1e8151", strokeWidth: 2 }, + ConnectionOverlays: [ + [ "Arrow", { + location: 1, + id: "arrow", + length: 14, + foldback: 0.8 + } ], + [ "Label", { label: "", id: "label", cssClass: "aLabel" }] + ], + Container: "relation-map-canvas" + }); + + instance.registerConnectionType("basic", { anchor:"Continuous", connector:"StateMachine" }); + + window.jsp = instance; + + let data; + let initDone = false; + + instance.bind("connection", function (info) { + const connection = info.connection; + let type; + + if (initDone) { + type = prompt("Specify new connection label:"); + + data.relations.push({ + connectionId: connection.id, + source: connection.sourceId, + target: connection.targetId, + type: type + }); + + saveData(); + } + else { + type = "none"; + } + + connection.getOverlay("label").setLabel(type); + }); + + jsPlumb.on($relationMapCanvas[0], "dblclick", function(e) { + newNode(jsPlumbUtil.uuid(),"new", e.offsetX, e.offsetY, "auto", "auto"); + }); + + function saveData() { + localStorage.setItem('triliumData', JSON.stringify(data)); + } + + function initNode(el) { + instance.draggable(el, { + handle: ".handle", + start:function(params) { + }, + drag:function(params) { + + }, + stop:function(params) { + const note = data.notes.find(note => note.id === params.el.id); + + if (!note) { + console.error(`Note ${params.el.id} not found!`); + return; + } + + [note.x, note.y] = params.finalPos; + + saveData(); + } + }); + + instance.makeSource(el, { + filter: ".endpoint", + anchor: "Continuous", + connectorStyle: { stroke: "#5c96bc", strokeWidth: 2, outlineStroke: "transparent", outlineWidth: 4 }, + connectionType:"basic", + extract:{ + "action":"the-action" + } + }); + + instance.makeTarget(el, { + dropOptions: { hoverClass: "dragHover" }, + anchor: "Continuous", + allowLoopback: true + }); + + // this is not part of the core demo functionality; it is a means for the Toolkit edition's wrapped + // version of this demo to find out about new nodes being added. + // + instance.fire("jsPlumbDemoNodeAdded", el); + + $(el).resizable({ + resize: function(event, ui) { +// instance.repaint(ui.helper.prop("id")); + + instance.repaintEverything(); + }, + stop: function(event, ui) { + const note = data.notes.find(note => note.id === ui.helper.prop("id")); + + note.width = ui.helper.width(); + note.height = ui.helper.height(); + + saveData(); + }, + handles: "all" + }); + } + + function newNode(id, title, x, y, width, height) { + const $noteBox = $("
") + .addClass("note-box") + .prop("id", id) + .append($("
").addClass("handle")) + .append($("").addClass("title").text(title)) + .append($("
").addClass("endpoint")) + .css("left", x + "px") + .css("top", y + "px") + .css("width", width) + .css("height", height); + + instance.getContainer().appendChild($noteBox[0]); + + initNode($noteBox[0]); + } + + const notes = [ + { id: "eliI", title: "Queen Elizabeth", x: 100, y: 100 }, + { id: "kinggeorge", title: "King George VI.", x: 300, y: 100 }, + { id: "phillip", title: "Prince Phillip" }, + { id: "eliII", title: "Queen Elizabeth II.", x: 300, y: 300 }, + { id: "charles", title: "Prince Charles" }, + { id: "diana", title: "Princess Diana" }, + { id: "harry", title: "Prince Harry" }, + { id: "william", title: "Prince William "} + ]; + + const relations = [ + { source: "eliI", target: "kinggeorge", type: "isSpouse" }, + { source: "eliI", target: "eliII", type: "hasChild" }, + { source: "kinggeorge", target: "eliII", type: "hasChild" }, + { source: "eliII", target: "charles", type: "hasChild" }, + { source: "phillip", target: "charles", type: "hasChild" }, + { source: "phillip", target: "eliII", type: "isSpouse" }, + { source: "charles", target: "diana", type: "isSpouse" }, + { source: "charles", target: "william", type: "hasChild" }, + { source: "charles", target: "harry", type: "hasChild" }, + { source: "diana", target: "william", type: "hasChild" }, + { source: "diana", target: "harry", type: "hasChild" } + ]; + + data = localStorage.getItem('triliumData'); + + if (data) { + data = JSON.parse(data); + } + else { + data = { notes, relations }; + + saveData(); + } + + $relationMapCanvas.contextmenu({ + delegate: ".note-box", + menu: [ + {title: "Remove note", cmd: "remove", uiIcon: "ui-icon-trash"}, + {title: "Edit title", cmd: "edit-title", uiIcon: "ui-icon-pencil"}, + ], + select: function(event, ui) { + const $noteBox = ui.target.closest(".note-box"); + const noteId = $noteBox.prop("id"); + + if (ui.cmd === "remove") { + if (!confirm("Are you sure you want to remove the note?")) { + return; + } + + instance.remove(noteId); + + data.notes = data.notes.filter(note => note.id !== noteId); + data.relations = data.relations.filter(relation => relation.source !== noteId && relation.target !== noteId); + + saveData(); + } + else if (ui.cmd === "edit-title") { + const title = prompt("Enter new note title:"); + + if (!title) { + return; + } + + const note = data.notes.find(note => note.id === noteId); + note.title = title; + + $noteBox.find(".title").text(note.title); + + saveData(); + } + } + }); + + $.widget("moogle.contextmenuRelation", $.moogle.contextmenu, {}); + + $relationMapCanvas.contextmenuRelation({ + delegate: ".aLabel,.jtk-connector", + autoTrigger: false, // it doesn't open automatically, needs to be triggered explicitly by .open() call + menu: [ + {title: "Remove relation", cmd: "remove", uiIcon: "ui-icon-trash"}, + {title: "Edit relation name", cmd: "edit-name", uiIcon: "ui-icon-pencil"}, + ], + select: function(event, ui) { + const {connection} = ui.extraData; + + if (ui.cmd === 'remove') { + if (!confirm("Are you sure you want to remove the relation?")) { + return; + } + + instance.deleteConnection(connection); + + data.relations = data.relations.filter(relation => relation.connectionId !== connection.id); + saveData(); + } + else if (ui.cmd === 'edit-name') { + const relationName = prompt("Specify new relation name:"); + + connection.getOverlay("label").setLabel(relationName); + + const relation = data.relations.find(relation => relation.connectionId === connection.id); + relation.type = relationName; + + saveData(); + } + } + }); + + instance.bind("contextmenu", function (c, e) { + e.preventDefault(); + + $relationMapCanvas.contextmenuRelation("open", e, { connection: c }); + }); + + instance.batch(function () { + const maxY = notes.filter(note => !!note.y).map(note => note.y).reduce((a, b) => Math.max(a, b)); + let curX = 100; + let curY = maxY + 200; + + for (const note of data.notes) { + if (note.x && note.y) { + newNode(note.id, note.title, note.x, note.y, note.width + "px", note.height + "px"); + } + else { + note.width = "auto"; + note.height = "auto"; + + newNode(note.id, note.title, curX, curY, note.width, note.height); + + if (curX > 1000) { + curX = 100; + curY += 200; + } + else { + curX += 200; + } + } + } + + for (const relation of data.relations) { + const connection = instance.connect({ id: relation.id, source: relation.source, target: relation.target, type:"basic" }); + + relation.connectionId = connection.id; + + connection.getOverlay("label").setLabel(relation.type); + connection.canvas.setAttribute("data-connection-id", connection.id); + } + + initDone = true; + }); + + // so that canvas is not panned when clicking/dragging note box + $relationMapCanvas.on('mousedown touchstart', '.note-box, .aLabel', e => e.stopPropagation()); + + jsPlumb.fire("jsPlumbDemoLoaded", instance); + + panzoom($relationMapCanvas[0]); + }); } export default { diff --git a/src/public/stylesheets/relation-map.css b/src/public/stylesheets/relation-map.css index eabc7090f..c7050bf0a 100644 --- a/src/public/stylesheets/relation-map.css +++ b/src/public/stylesheets/relation-map.css @@ -1,6 +1,5 @@ -#canvas { - border: 1px dotted black; - height: 700px; +#relation-map-canvas { + height: 100vh; } .note-box {