diff --git a/src/public/javascripts/services/link_map.js b/src/public/javascripts/services/link_map.js
new file mode 100644
index 000000000..6f1522516
--- /dev/null
+++ b/src/public/javascripts/services/link_map.js
@@ -0,0 +1,198 @@
+import libraryLoader from "./library_loader.js";
+import server from "./server.js";
+import treeCache from "./tree_cache.js";
+import linkService from "./link.js";
+
+const linkOverlays = [
+ [ "Arrow", {
+ location: 1,
+ id: "arrow",
+ length: 10,
+ width: 10,
+ foldback: 0.7
+ } ]
+];
+
+export default class LinkMap {
+ constructor(note, $linkMapContainer) {
+ this.note = note;
+ this.$linkMapContainer = $linkMapContainer;
+ this.linkMapContainerId = this.$linkMapContainer.attr("id");
+ }
+
+ async render() {
+ await libraryLoader.requireLibrary(libraryLoader.LINK_MAP);
+
+ jsPlumb.ready(() => {
+ this.initJsPlumbInstance();
+
+ this.initPanZoom();
+
+ this.loadNotesAndRelations();
+ });
+ }
+
+ async loadNotesAndRelations() {
+ this.cleanup();
+
+ const maxNotes = 50;
+
+ const currentNoteId = this.note.noteId;
+
+ const links = await server.post(`notes/${currentNoteId}/link-map`, {
+ maxNotes,
+ maxDepth: 1
+ });
+
+ const noteIds = new Set(links.map(l => l.noteId).concat(links.map(l => l.targetNoteId)));
+
+ if (noteIds.size === 0) {
+ noteIds.add(currentNoteId);
+ }
+
+ // 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 = $("
")
+ .addClass("note-box")
+ .prop("id", noteBoxId);
+
+ linkService.createNoteLink(noteId, note.title).then($link => {
+ $noteBox.append($("").addClass("title").append($link));
+ });
+
+ if (noteId === currentNoteId) {
+ $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.7);
+ 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 this.linkMapContainerId + "-note-" + noteId;
+ }
+}
\ No newline at end of file
diff --git a/src/public/javascripts/widgets/link_map.js b/src/public/javascripts/widgets/link_map.js
index 3bed76fe3..36cc84130 100644
--- a/src/public/javascripts/widgets/link_map.js
+++ b/src/public/javascripts/widgets/link_map.js
@@ -1,7 +1,3 @@
-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";
import StandardWidget from "./standard_widget.js";
let linkMapContainerIdCtr = 1;
@@ -12,16 +8,6 @@ const TPL = `
`;
-const linkOverlays = [
- [ "Arrow", {
- location: 1,
- id: "arrow",
- length: 10,
- width: 10,
- foldback: 0.7
- } ]
-];
-
class LinkMapWidget extends StandardWidget {
getWidgetTitle() { return "Link map"; }
@@ -38,182 +24,14 @@ class LinkMapWidget extends StandardWidget {
async doRenderBody() {
this.$body.html(TPL);
- this.$linkMapContainer = this.$body.find('.link-map-container');
- this.$linkMapContainer.attr("id", "link-map-container-" + linkMapContainerIdCtr++);
+ const $linkMapContainer = this.$body.find('.link-map-container');
+ $linkMapContainer.attr("id", "link-map-container-" + linkMapContainerIdCtr++);
- await libraryLoader.requireLibrary(libraryLoader.LINK_MAP);
+ const LinkMapServiceClass = (await import('../services/link_map.js')).default;
- jsPlumb.ready(() => {
- this.initJsPlumbInstance();
+ const linkMapService = new LinkMapServiceClass(this.ctx.note, $linkMapContainer);
- this.initPanZoom();
-
- this.loadNotesAndRelations();
- });
- }
-
- async loadNotesAndRelations() {
- this.cleanup();
-
- const maxNotes = 50;
-
- const currentNoteId = this.ctx.note.noteId;
-
- const links = await server.post(`notes/${currentNoteId}/link-map`, {
- maxNotes,
- maxDepth: 1
- });
-
- const noteIds = new Set(links.map(l => l.noteId).concat(links.map(l => l.targetNoteId)));
-
- if (noteIds.size === 0) {
- noteIds.add(currentNoteId);
- }
-
- // 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 = $("")
- .addClass("note-box")
- .prop("id", noteBoxId);
-
- linkService.createNoteLink(noteId, note.title).then($link => {
- $noteBox.append($("").addClass("title").append($link));
- });
-
- if (noteId === currentNoteId) {
- $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.7);
- 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;
+ await linkMapService.render();
}
}