mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
backlinks WIP, #2349
This commit is contained in:
parent
89f117da5b
commit
bbceb6251a
10
.idea/runConfigurations.xml
generated
10
.idea/runConfigurations.xml
generated
@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
32
package-lock.json
generated
32
package-lock.json
generated
@ -1920,9 +1920,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001282",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001282.tgz",
|
||||
"integrity": "sha512-YhF/hG6nqBEllymSIjLtR2iWDDnChvhnVJqp+vloyt2tEHFG1yBR+ac2B/rOw0qOK0m0lEXU2dv4E/sMk5P9Kg==",
|
||||
"version": "1.0.30001283",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001283.tgz",
|
||||
"integrity": "sha512-9RoKo841j1GQFSJz/nCXOj0sD7tHBtlowjYlrqIUS812x9/emfBLBt6IyMz1zIaYc/eRL8Cs6HPUVi2Hzq4sIg==",
|
||||
"dev": true
|
||||
},
|
||||
"caseless": {
|
||||
@ -2903,9 +2903,9 @@
|
||||
}
|
||||
},
|
||||
"electron": {
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-16.0.1.tgz",
|
||||
"integrity": "sha512-6TSDBcoKGgmKL/+W+LyaXidRVeRl1V4I81ZOWcqsVksdTMfM4AlxTgfaoYdK/nUhqBrUtuPDcqOyJE6Bc4qMpw==",
|
||||
"version": "16.0.3",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-16.0.3.tgz",
|
||||
"integrity": "sha512-MzCYuEqrvyEtPSUWQwr88xWBrsbhmyOKp4wqP9WfAJTEDeUfBcrQYswHuYe17Gi00gRirQb9htoC/anYfaw20w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@electron/get": "^1.13.0",
|
||||
@ -3702,9 +3702,9 @@
|
||||
}
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.0.tgz",
|
||||
"integrity": "sha512-+oXCt6SaIu8EmFTPx8wNGSB0tHQ5biDscnlf6Uxuz17e9CjzMRtGk9B8705aMPnj0iWr3iC74WuIkngCsLElmA==",
|
||||
"version": "1.4.8",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.8.tgz",
|
||||
"integrity": "sha512-Cu5+dbg55+1E3ohlsa8HT0s4b8D0gBewXEGG8s5wBl8ynWv60VuvYW25GpsOeTVXpulhyU/U8JYZH+yxASSJBQ==",
|
||||
"dev": true
|
||||
},
|
||||
"electron-window-state": {
|
||||
@ -5100,9 +5100,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"jest-worker": {
|
||||
"version": "27.3.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.3.1.tgz",
|
||||
"integrity": "sha512-ks3WCzsiZaOPJl/oMsDjaf0TRiSv7ctNgs0FqRr2nARsovz6AWWy4oLElwcquGSz692DzgZQrCLScPNs5YlC4g==",
|
||||
"version": "27.4.2",
|
||||
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.4.2.tgz",
|
||||
"integrity": "sha512-0QMy/zPovLfUPyHuOuuU4E+kGACXXE84nRnq6lBVI9GJg5DCBiA97SATi+ZP8CpiJwEQy1oCPjRBf8AnLjN+Ag==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
@ -8118,9 +8118,9 @@
|
||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="
|
||||
},
|
||||
"webpack": {
|
||||
"version": "5.64.3",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.64.3.tgz",
|
||||
"integrity": "sha512-XF6/IL9Bw2PPQioiR1UYA8Bs4tX3QXJtSelezKECdLFeSFzWoe44zqTzPW5N+xI3fACaRl2/G3sNA4WYHD7Iww==",
|
||||
"version": "5.64.4",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.64.4.tgz",
|
||||
"integrity": "sha512-LWhqfKjCLoYJLKJY8wk2C3h77i8VyHowG3qYNZiIqD6D0ZS40439S/KVuc/PY48jp2yQmy0mhMknq8cys4jFMw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/eslint-scope": "^3.7.0",
|
||||
@ -8145,7 +8145,7 @@
|
||||
"schema-utils": "^3.1.0",
|
||||
"tapable": "^2.1.1",
|
||||
"terser-webpack-plugin": "^5.1.3",
|
||||
"watchpack": "^2.2.0",
|
||||
"watchpack": "^2.3.0",
|
||||
"webpack-sources": "^3.2.2"
|
||||
}
|
||||
},
|
||||
|
@ -82,7 +82,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "7.0.3",
|
||||
"electron": "16.0.1",
|
||||
"electron": "16.0.3",
|
||||
"@electron/remote": "2.0.1",
|
||||
"electron-builder": "22.14.5",
|
||||
"electron-packager": "15.4.0",
|
||||
@ -92,7 +92,7 @@
|
||||
"jsdoc": "3.6.7",
|
||||
"lorem-ipsum": "2.0.4",
|
||||
"rcedit": "3.0.1",
|
||||
"webpack": "5.64.3",
|
||||
"webpack": "5.64.4",
|
||||
"webpack-cli": "4.9.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
@ -46,6 +46,7 @@ import OpenNoteButtonWidget from "../widgets/buttons/open_note_button_widget.js"
|
||||
import MermaidWidget from "../widgets/mermaid.js";
|
||||
import BookmarkButtons from "../widgets/bookmark_buttons.js";
|
||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||
import BacklinksWidget from "../widgets/backlinks.js";
|
||||
|
||||
export default class DesktopLayout {
|
||||
constructor(customWidgets) {
|
||||
@ -147,6 +148,7 @@ export default class DesktopLayout {
|
||||
.button(new NoteActionsWidget())
|
||||
)
|
||||
.child(new NoteUpdateStatusWidget())
|
||||
.child(new BacklinksWidget())
|
||||
.child(new MermaidWidget())
|
||||
.child(
|
||||
new ScrollingContainer()
|
||||
|
@ -313,6 +313,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
|
||||
* @param {object} [params]
|
||||
* @param {boolean} [params.showTooltip=true] - enable/disable tooltip on the link
|
||||
* @param {boolean} [params.showNotePath=false] - show also whole note's path as part of the link
|
||||
* @param {boolean} [params.showNoteIcon=false] - show also note icon before the title
|
||||
* @param {string} [title=] - custom link tile with note's title as default
|
||||
*/
|
||||
this.createNoteLink = linkService.createNoteLink;
|
||||
|
143
src/public/app/widgets/backlinks.js
Normal file
143
src/public/app/widgets/backlinks.js
Normal file
@ -0,0 +1,143 @@
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import linkService from "../services/link.js";
|
||||
import server from "../services/server.js";
|
||||
import froca from "../services/froca.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="backlinks-widget">
|
||||
<style>
|
||||
.backlinks-widget {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.backlinks-ticker {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 130px;
|
||||
border-radius: 10px;
|
||||
border-color: var(--main-border-color);
|
||||
background-color: var(--more-accented-background-color);
|
||||
padding: 4px 10px 4px 10px;
|
||||
opacity: 70%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.backlinks-count {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.backlinks-close-ticker {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.backlinks-ticker:hover {
|
||||
opacity: 100%;
|
||||
}
|
||||
|
||||
.backlinks-items {
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
right: 10px;
|
||||
width: 400px;
|
||||
border-radius: 10px;
|
||||
background-color: #eeeeee;
|
||||
color: #444;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.backlink-excerpt {
|
||||
border-left: 2px solid var(--main-border-color);
|
||||
padding-left: 10px;
|
||||
opacity: 80%;
|
||||
font-size: 90%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="backlinks-ticker">
|
||||
<span class="backlinks-count"></span>
|
||||
|
||||
<span class="bx bx-x backlinks-close-ticker"></span>
|
||||
</div>
|
||||
|
||||
<div class="backlinks-items" style="display: none;"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class BacklinksWidget extends NoteContextAwareWidget {
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$count = this.$widget.find('.backlinks-count');
|
||||
this.$items = this.$widget.find('.backlinks-items');
|
||||
this.$ticker = this.$widget.find('.backlinks-ticker');
|
||||
|
||||
this.$count.on("click", () => {
|
||||
this.$items.toggle();
|
||||
this.$items.css("max-height", $(window).height() - this.$items.offset().top - 10);
|
||||
|
||||
if (this.$items.is(":visible")) {
|
||||
this.renderBacklinks();
|
||||
}
|
||||
});
|
||||
|
||||
this.$closeTickerButton = this.$widget.find('.backlinks-close-ticker');
|
||||
this.$closeTickerButton.on("click", () => {
|
||||
this.$ticker.hide();
|
||||
|
||||
this.clearItems();
|
||||
});
|
||||
|
||||
this.contentSized();
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
this.clearItems();
|
||||
|
||||
const targetRelationCount = note.getTargetRelations().length;
|
||||
if (targetRelationCount === 0) {
|
||||
this.$ticker.hide();
|
||||
}
|
||||
else {
|
||||
this.$ticker.show();
|
||||
this.$count.text(
|
||||
`${targetRelationCount} backlink`
|
||||
+ (targetRelationCount === 1 ? '' : 's')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
clearItems() {
|
||||
this.$items.empty().hide();
|
||||
}
|
||||
|
||||
async renderBacklinks() {
|
||||
if (!this.note) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$items.empty();
|
||||
|
||||
const backlinks = await server.get(`note-map/${this.noteId}/backlinks`);
|
||||
|
||||
if (!backlinks.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
await froca.getNotes(backlinks.map(bl => bl.noteId)); // prefetch all
|
||||
|
||||
for (const backlink of backlinks) {
|
||||
this.$items.append(await linkService.createNoteLink(backlink.noteId, {
|
||||
showNoteIcon: true,
|
||||
showNotePath: true,
|
||||
showTooltip: false
|
||||
}));
|
||||
|
||||
this.$items.append("<br/>");
|
||||
this.$items.append(...backlink.excerpts);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
const becca = require("../../becca/becca");
|
||||
const { JSDOM } = require("jsdom");
|
||||
|
||||
function buildDescendantCountMap() {
|
||||
const noteIdToCountMap = {};
|
||||
@ -174,7 +175,131 @@ function getTreeMap(req) {
|
||||
};
|
||||
}
|
||||
|
||||
function removeImages(document) {
|
||||
const images = document.getElementsByTagName('img');
|
||||
while (images.length > 0) {
|
||||
images[0].parentNode.removeChild(images[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function getBacklinks(req) {
|
||||
const {noteId} = req.params;
|
||||
const note = becca.getNote(noteId);
|
||||
|
||||
if (!note) {
|
||||
return [404, `Note ${noteId} was not found`];
|
||||
}
|
||||
|
||||
let backlinks = note.getTargetRelations();
|
||||
|
||||
if (backlinks.length > 50) {
|
||||
backlinks = backlinks.slice(0, 50);
|
||||
}
|
||||
|
||||
return backlinks.map(backlink => {
|
||||
const sourceNote = backlink.note;
|
||||
|
||||
const html = sourceNote.getContent();
|
||||
const dom = new JSDOM(html);
|
||||
|
||||
const excerpts = [];
|
||||
|
||||
const document = dom.window.document;
|
||||
|
||||
removeImages(document);
|
||||
|
||||
for (const linkEl of document.querySelectorAll("a")) {
|
||||
const href = linkEl.getAttribute("href");
|
||||
|
||||
if (!href || !href.includes(noteId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
linkEl.style.fontWeight = "bold";
|
||||
linkEl.style.backgroundColor = "yellow";
|
||||
|
||||
const LIMIT = 200;
|
||||
let centerEl = linkEl;
|
||||
|
||||
while (centerEl.tagName !== 'BODY' && centerEl.parentElement.textContent.length < LIMIT) {
|
||||
centerEl = centerEl.parentElement;
|
||||
}
|
||||
|
||||
const sub = [centerEl];
|
||||
let counter = centerEl.textContent.length;
|
||||
let left = centerEl;
|
||||
let right = centerEl;
|
||||
|
||||
while (true) {
|
||||
let added = false;
|
||||
|
||||
const prev = left.previousElementSibling;
|
||||
|
||||
if (prev) {
|
||||
const prevText = prev.textContent;
|
||||
|
||||
if (prevText.length + counter > LIMIT) {
|
||||
const prefix = prevText.substr(prevText.length - (LIMIT - counter));
|
||||
|
||||
const textNode = document.createTextNode("…" + prefix);
|
||||
sub.unshift(textNode);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
left = prev;
|
||||
sub.unshift(left);
|
||||
counter += prevText.length;
|
||||
added = true;
|
||||
}
|
||||
|
||||
const next = right.nextElementSibling;
|
||||
|
||||
if (next) {
|
||||
const nextText = next.textContent;
|
||||
|
||||
if (nextText.length + counter > LIMIT) {
|
||||
const suffix = nextText.substr(nextText.length - (LIMIT - counter));
|
||||
|
||||
const textNode = document.createTextNode(suffix + "…");
|
||||
sub.push(textNode);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
right = next;
|
||||
sub.push(right);
|
||||
counter += nextText.length;
|
||||
added = true;
|
||||
}
|
||||
|
||||
if (!added) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.classList.add("ck-content");
|
||||
div.classList.add("backlink-excerpt");
|
||||
|
||||
for (const childEl of sub) {
|
||||
div.appendChild(childEl);
|
||||
}
|
||||
|
||||
const subHtml = div.outerHTML;
|
||||
|
||||
excerpts.push(subHtml);
|
||||
}
|
||||
|
||||
return {
|
||||
noteId: sourceNote.noteId,
|
||||
excerpts
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getLinkMap,
|
||||
getTreeMap
|
||||
getTreeMap,
|
||||
getBacklinks
|
||||
};
|
||||
|
@ -260,6 +260,7 @@ function register(app) {
|
||||
|
||||
apiRoute(POST, '/api/note-map/:noteId/tree', noteMapRoute.getTreeMap);
|
||||
apiRoute(POST, '/api/note-map/:noteId/link', noteMapRoute.getLinkMap);
|
||||
apiRoute(GET, '/api/note-map/:noteId/backlinks', noteMapRoute.getBacklinks);
|
||||
|
||||
apiRoute(GET, '/api/special-notes/inbox/:date', specialNotesRoute.getInboxNote);
|
||||
apiRoute(GET, '/api/special-notes/date/:date', specialNotesRoute.getDateNote);
|
||||
|
Loading…
x
Reference in New Issue
Block a user