link map in sidebar POC

This commit is contained in:
zadam 2019-07-21 21:55:48 +02:00
parent eb28a3b0c9
commit 849fad0421
4 changed files with 248 additions and 9 deletions

View File

@ -1,4 +1,5 @@
import NoteInfoWidget from "../widgets/note_info.js";
import LinkMapWidget from "../widgets/link_map.js";
const WIDGET_TPL = `
<div class="card widget">
@ -46,7 +47,7 @@ class Sidebar {
this.$widgets.empty();
this.addNoteInfoWidget();
this.addNoteInfoWidget();
this.addLinkMapWidget();
}
async addNoteInfoWidget() {
@ -55,6 +56,15 @@ class Sidebar {
const noteInfoWidget = new NoteInfoWidget(this.ctx, $widget);
await noteInfoWidget.renderBody();
this.$widgets.append($widget);
}
async addLinkMapWidget() {
const $widget = this.createWidgetElement();
const linkMapWidget = new LinkMapWidget(this.ctx, $widget);
await linkMapWidget.renderBody();
console.log($widget);
this.$widgets.append($widget);

View File

@ -0,0 +1,220 @@
import libraryLoader from "../services/library_loader.js";
import server from "../services/server.js";
import treeCache from "../services/tree_cache.js";
import linkService from "../services/link.js";
let linkMapContainerIdCtr = 1;
const TPL = `
<div style="outline: none; overflow: hidden;">
<div class="link-map-container"></div>
</div>
`;
const linkOverlays = [
[ "Arrow", {
location: 1,
id: "arrow",
length: 10,
width: 10,
foldback: 0.7
} ]
];
class LinkMapWidget {
/**
* @param {TabContext} ctx
* @param {jQuery} $widget
*/
constructor(ctx, $widget) {
this.ctx = ctx;
this.$widget = $widget;
this.$title = this.$widget.find('.widget-title');
this.$title.text("Link map");
}
async renderBody() {
const $body = this.$widget.find('.card-body');
$body.html(TPL);
this.$linkMapContainer = $body.find('.link-map-container');
this.$linkMapContainer.attr("id", "link-map-container-" + linkMapContainerIdCtr++);
await libraryLoader.requireLibrary(libraryLoader.LINK_MAP);
jsPlumb.ready(() => {
this.initJsPlumbInstance();
this.initPanZoom();
this.loadNotesAndRelations();
});
}
async loadNotesAndRelations() {
this.cleanup();
const linkTypes = [ "hyper", "image", "relation", "relation-map" ];
const maxNotes = 50;
const noteId = this.ctx.note.noteId;
const links = await server.post(`notes/${noteId}/link-map`, {
linkTypes,
maxNotes
});
const noteIds = new Set(links.map(l => l.noteId).concat(links.map(l => l.targetNoteId)));
if (noteIds.size === 0) {
noteIds.add(noteId);
}
// preload all notes
const notes = await treeCache.getNotes(Array.from(noteIds));
const graph = new Springy.Graph();
graph.addNodes(...noteIds);
graph.addEdges(...links.map(l => [l.noteId, l.targetNoteId]));
const layout = new Springy.Layout.ForceDirected(
graph,
400.0, // Spring stiffness
400.0, // Node repulsion
0.5 // Damping
);
const getNoteBox = noteId => {
const noteBoxId = this.noteIdToId(noteId);
const $existingNoteBox = $("#" + noteBoxId);
if ($existingNoteBox.length > 0) {
return $existingNoteBox;
}
const note = notes.find(n => n.noteId === noteId);
const $noteBox = $("<div>")
.addClass("note-box")
.prop("id", noteBoxId);
linkService.createNoteLink(noteId, note.title).then($link => {
$noteBox.append($("<span>").addClass("title").append($link));
});
if (noteId === noteId) {
$noteBox.addClass("link-map-active-note");
}
this.$linkMapContainer.append($noteBox);
this.jsPlumbInstance.draggable($noteBox[0], {
start: params => {
renderer.stop();
},
drag: params => {},
stop: params => {}
});
return $noteBox;
};
this.renderer = new Springy.Renderer(
layout,
() => {},
(edge, p1, p2) => {
const connectionId = edge.source.id + '-' + edge.target.id;
if ($("#" + connectionId).length > 0) {
return;
}
getNoteBox(edge.source.id);
getNoteBox(edge.target.id);
const connection = this.jsPlumbInstance.connect({
source: this.noteIdToId(edge.source.id),
target: this.noteIdToId(edge.target.id),
type: 'link'
});
connection.canvas.id = connectionId;
},
(node, p) => {
const $noteBox = getNoteBox(node.id);
const middleW = this.$linkMapContainer.width() / 2;
const middleH = this.$linkMapContainer.height() / 2;
$noteBox
.css("left", (middleW + p.x * 100) + "px")
.css("top", (middleH + p.y * 100) + "px");
},
() => {},
() => {},
() => {
this.jsPlumbInstance.repaintEverything();
}
);
this.renderer.start();
}
initPanZoom() {
if (this.pzInstance) {
return;
}
this.pzInstance = panzoom(this.$linkMapContainer[0], {
maxZoom: 2,
minZoom: 0.3,
smoothScroll: false,
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;
}
});
}
cleanup() {
if (this.renderer) {
this.renderer.stop();
}
// 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.$linkMapContainer.empty();
// reset zoom/pan
this.pzInstance.zoomTo(0, 0, 0.5);
this.pzInstance.moveTo(0, 0);
}
initJsPlumbInstance() {
if (this.jsPlumbInstance) {
this.cleanup();
return;
}
this.jsPlumbInstance = jsPlumb.getInstance({
Endpoint: ["Blank", {}],
ConnectionOverlays: linkOverlays,
PaintStyle: { stroke: "var(--muted-text-color)", strokeWidth: 1 },
HoverPaintStyle: { stroke: "var(--main-text-color)", strokeWidth: 1 },
Container: this.$linkMapContainer.attr("id")
});
this.jsPlumbInstance.registerConnectionType("link", { anchor: "Continuous", connector: "Straight", overlays: linkOverlays });
}
noteIdToId(noteId) {
return "link-map-note-" + noteId;
}
}
export default LinkMapWidget;

View File

@ -1,10 +1,10 @@
#link-map-container {
.link-map-container {
position: relative;
height: calc(95vh - 130px);
height: 300px;
outline: none; /* remove dotted outline on click */
}
#link-map-container .note-box {
.link-map-container .note-box {
padding: 8px;
position: absolute !important;
background-color: var(--accented-background-color);
@ -23,16 +23,16 @@
overflow: hidden;
}
#link-map-container .note-box:hover {
.link-map-container .note-box:hover {
background-color: var(--more-accented-background-color);
}
#link-map-container .note-box .title {
.link-map-container .note-box .title {
font-size: larger;
font-weight: 600;
}
#link-map-container .floating-button {
.link-map-container .floating-button {
position: absolute !important;
z-index: 100;
}

View File

@ -116,10 +116,19 @@ ul.fancytree-container {
}
.note-detail-sidebar {
min-width: 300px;
min-width: 350px;
overflow: auto;
padding-top: 12px;
padding-left: 7px;
font-size: 90%;
}
.note-detail-sidebar .widget-title {
width: 100%;
border-radius: 0;
padding: 0;
border-left: 0;
border-right: 0;
}
.note-detail-sidebar .card {
@ -141,7 +150,7 @@ ul.fancytree-container {
}
.note-detail-sidebar .card-body {
max-width: 300px;
width: 100%;
padding: 8px;
border: 0;
}