mirror of https://github.com/TriliumNext/Notes
Merge remote-tracking branch 'origin/t35'
# Conflicts: # src/views/tabs.ejspull/255/head
commit
385b79ad34
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
File diff suppressed because one or more lines are too long
@ -0,0 +1,93 @@
|
||||
import NoteInfoWidget from "../widgets/note_info.js";
|
||||
import LinkMapWidget from "../widgets/link_map.js";
|
||||
import NoteRevisionsWidget from "../widgets/note_revisions.js";
|
||||
|
||||
const WIDGET_TPL = `
|
||||
<div class="card widget">
|
||||
<div class="card-header">
|
||||
<button class="btn btn-sm widget-title" data-toggle="collapse" data-target="#collapseOne">
|
||||
Collapsible Group Item
|
||||
</button>
|
||||
|
||||
<div class="widget-header-actions"></div>
|
||||
</div>
|
||||
|
||||
<div id="collapseOne" class="collapse show body-wrapper">
|
||||
<div class="card-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
let widgetIdCtr = 1;
|
||||
|
||||
class Sidebar {
|
||||
/**
|
||||
* @param {TabContext} ctx
|
||||
*/
|
||||
constructor(ctx) {
|
||||
this.ctx = ctx;
|
||||
this.$sidebar = ctx.$tabContent.find(".note-detail-sidebar");
|
||||
this.$widgets = this.$sidebar.find(".note-detail-widgets");
|
||||
this.$showSideBarButton = this.ctx.$tabContent.find(".show-sidebar-button");
|
||||
this.$showSideBarButton.hide();
|
||||
|
||||
this.$hideSidebarButton = this.$sidebar.find(".hide-sidebar-button");
|
||||
|
||||
this.$hideSidebarButton.click(() => {
|
||||
this.$sidebar.hide();
|
||||
this.$showSideBarButton.show();
|
||||
});
|
||||
|
||||
this.$showSideBarButton.click(() => {
|
||||
this.$sidebar.show();
|
||||
this.$showSideBarButton.hide();
|
||||
});
|
||||
}
|
||||
|
||||
async noteLoaded() {
|
||||
this.$widgets.empty();
|
||||
|
||||
this.addNoteInfoWidget();
|
||||
this.addLinkMapWidget();
|
||||
this.addNoteRevisionsWidget();
|
||||
}
|
||||
|
||||
async addNoteInfoWidget() {
|
||||
const $widget = this.createWidgetElement();
|
||||
|
||||
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();
|
||||
|
||||
this.$widgets.append($widget);
|
||||
}
|
||||
|
||||
async addNoteRevisionsWidget() {
|
||||
const $widget = this.createWidgetElement();
|
||||
|
||||
const noteRevisionsWidget = new NoteRevisionsWidget(this.ctx, $widget);
|
||||
await noteRevisionsWidget.renderBody();
|
||||
|
||||
this.$widgets.append($widget);
|
||||
}
|
||||
|
||||
createWidgetElement() {
|
||||
const widgetId = 'widget-' + widgetIdCtr++;
|
||||
|
||||
const $widget = $(WIDGET_TPL);
|
||||
$widget.find('[data-target]').attr('data-target', "#" + widgetId);
|
||||
$widget.find('.body-wrapper').attr('id', widgetId);
|
||||
|
||||
return $widget;
|
||||
}
|
||||
}
|
||||
|
||||
export default Sidebar;
|
||||
@ -0,0 +1,230 @@
|
||||
import libraryLoader from "../services/library_loader.js";
|
||||
import linkMapDialog from "../dialogs/link_map.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");
|
||||
this.$headerActions = this.$widget.find('.widget-header-actions');
|
||||
|
||||
const $showFullButton = $("<a>").append("show full").addClass('widget-header-action');
|
||||
$showFullButton.click(() => {
|
||||
linkMapDialog.showDialog();
|
||||
});
|
||||
|
||||
this.$headerActions.append($showFullButton);
|
||||
}
|
||||
|
||||
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,
|
||||
maxDepth: 1
|
||||
});
|
||||
|
||||
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.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;
|
||||
}
|
||||
}
|
||||
|
||||
export default LinkMapWidget;
|
||||
@ -0,0 +1,59 @@
|
||||
const TPL = `
|
||||
<table class="note-info-table">
|
||||
<tr>
|
||||
<th>Note ID</th>
|
||||
<td class="note-info-note-id"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Created</th>
|
||||
<td class="note-info-date-created"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Modified</th>
|
||||
<td class="note-info-date-modified"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<td class="note-info-type"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>MIME</th>
|
||||
<td class="note-info-mime"></td>
|
||||
</tr>
|
||||
</table>
|
||||
`;
|
||||
|
||||
class NoteInfoWidget {
|
||||
/**
|
||||
* @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("Note info");
|
||||
}
|
||||
|
||||
async renderBody() {
|
||||
const $body = this.$widget.find('.card-body');
|
||||
|
||||
$body.html(TPL);
|
||||
|
||||
const $noteId = $body.find(".note-info-note-id");
|
||||
const $dateCreated = $body.find(".note-info-date-created");
|
||||
const $dateModified = $body.find(".note-info-date-modified");
|
||||
const $type = $body.find(".note-info-type");
|
||||
const $mime = $body.find(".note-info-mime");
|
||||
|
||||
const note = this.ctx.note;
|
||||
|
||||
$noteId.text(note.noteId);
|
||||
$dateCreated.text(note.dateCreated);
|
||||
$dateModified.text(note.dateModified);
|
||||
$type.text(note.type);
|
||||
$mime.text(note.mime);
|
||||
}
|
||||
}
|
||||
|
||||
export default NoteInfoWidget;
|
||||
@ -0,0 +1,44 @@
|
||||
import server from "../services/server.js";
|
||||
|
||||
const TPL = `
|
||||
<ul class="note-revision-list" style="max-height: 150px; overflow: auto;">
|
||||
</ul>
|
||||
`;
|
||||
|
||||
class NoteRevisionsWidget {
|
||||
/**
|
||||
* @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("Note revisions");
|
||||
}
|
||||
|
||||
async renderBody() {
|
||||
const $body = this.$widget.find('.card-body');
|
||||
const revisionItems = await server.get(`notes/${this.ctx.note.noteId}/revisions`);
|
||||
|
||||
if (revisionItems.length === 0) {
|
||||
$body.text("No revisions yet...");
|
||||
return;
|
||||
}
|
||||
|
||||
$body.html(TPL);
|
||||
|
||||
const $list = $body.find('.note-revision-list');
|
||||
|
||||
for (const item of revisionItems) {
|
||||
$list.append($('<li>').append($("<a>", {
|
||||
'data-action': 'note-revision',
|
||||
'data-note-path': this.ctx.note.noteId,
|
||||
'data-note-revision-id': item.noteRevisionId,
|
||||
href: 'javascript:'
|
||||
}).text(item.dateModifiedFrom)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default NoteRevisionsWidget;
|
||||
@ -0,0 +1,7 @@
|
||||
<div class="note-detail-sidebar">
|
||||
<div style="text-align: center; margin-bottom: 10px;">
|
||||
<button class="hide-sidebar-button" style="background: none; border: none;">hide sidebar <span class="jam jam-chevron-right"></span></button>
|
||||
</div>
|
||||
|
||||
<div class="note-detail-widgets"></div>
|
||||
</div>
|
||||
Loading…
Reference in New Issue