chore(nx): move all monorepo-style in subfolder for processing

This commit is contained in:
Elian Doran
2025-04-22 10:06:06 +03:00
parent 2e200eab39
commit 62dbcc0a2e
1469 changed files with 16 additions and 16 deletions

View File

@@ -1,93 +0,0 @@
import { t } from "../../services/i18n.js";
import server from "../../services/server.js";
import ws from "../../services/ws.js";
import appContext, { type EventData } from "../../components/app_context.js";
import toastService from "../../services/toast.js";
import treeService from "../../services/tree.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import keyboardActionService from "../../services/keyboard_actions.js";
import type FNote from "../../entities/fnote.js";
const TPL = /*html*/`
<div class="code-buttons-widget">
<style>
.code-buttons-widget {
display: flex;
gap: 10px;
}
</style>
<button data-trigger-command="runActiveNote" class="execute-button floating-button btn" title="${t("code_buttons.execute_button_title")}">
<span class="bx bx-play"></span>
</button>
<button class="trilium-api-docs-button floating-button btn" title="${t("code_buttons.trilium_api_docs_button_title")}">
<span class="bx bx-help-circle"></span>
</button>
<button class="save-to-note-button floating-button btn" title="${t("code_buttons.save_to_note_button_title")}">
<span class="bx bx-save"></span>
</button>
</div>`;
// TODO: Deduplicate with server.
interface SaveSqlConsoleResponse {
notePath: string;
}
export default class CodeButtonsWidget extends NoteContextAwareWidget {
private $openTriliumApiDocsButton!: JQuery<HTMLElement>;
private $executeButton!: JQuery<HTMLElement>;
private $saveToNoteButton!: JQuery<HTMLElement>;
isEnabled() {
return super.isEnabled() && this.note && (this.note.mime.startsWith("application/javascript") || this.note.mime === "text/x-sqlite;schema=trilium");
}
doRender() {
this.$widget = $(TPL);
this.$openTriliumApiDocsButton = this.$widget.find(".trilium-api-docs-button");
this.$openTriliumApiDocsButton.on("click", () => {
toastService.showMessage(t("code_buttons.opening_api_docs_message"));
if (this.note?.mime.endsWith("frontend")) {
window.open("https://zadam.github.io/trilium/frontend_api/FrontendScriptApi.html", "_blank");
} else {
window.open("https://zadam.github.io/trilium/backend_api/BackendScriptApi.html", "_blank");
}
});
this.$executeButton = this.$widget.find(".execute-button");
this.$saveToNoteButton = this.$widget.find(".save-to-note-button");
this.$saveToNoteButton.on("click", async () => {
const { notePath } = await server.post<SaveSqlConsoleResponse>("special-notes/save-sql-console", { sqlConsoleNoteId: this.noteId });
await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext()?.setNote(notePath);
toastService.showMessage(t("code_buttons.sql_console_saved_message", { notePath: await treeService.getNotePathTitle(notePath) }));
});
keyboardActionService.updateDisplayedShortcuts(this.$widget);
this.contentSized();
super.doRender();
}
async refreshWithNote(note: FNote) {
this.$executeButton.toggle(note.mime.startsWith("application/javascript") || note.mime === "text/x-sqlite;schema=trilium");
this.$saveToNoteButton.toggle(note.mime === "text/x-sqlite;schema=trilium" && note.isHiddenCompletely());
this.$openTriliumApiDocsButton.toggle(note.mime.startsWith("application/javascript;env="));
}
async noteTypeMimeChangedEvent({ noteId }: EventData<"noteTypeMimeChanged">) {
if (this.isNote(noteId)) {
await this.refresh();
}
}
}

View File

@@ -1,42 +0,0 @@
import { t } from "../../services/i18n.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import utils from "../../services/utils.js";
import imageService from "../../services/image.js";
const TPL = /*html*/`
<button type="button"
class="copy-image-reference-button"
title="${t("copy_image_reference_button.button_title")}">
<span class="bx bx-copy"></span>
<div class="hidden-image-copy"></div>
</button>`;
export default class CopyImageReferenceButton extends NoteContextAwareWidget {
private $hiddenImageCopy!: JQuery<HTMLElement>;
isEnabled() {
return super.isEnabled() && ["mermaid", "canvas", "mindMap"].includes(this.note?.type ?? "") && this.note?.isContentAvailable() && this.noteContext?.viewScope?.viewMode === "default";
}
doRender() {
super.doRender();
this.$widget = $(TPL);
this.$hiddenImageCopy = this.$widget.find(".hidden-image-copy");
this.$widget.on("click", () => {
if (!this.note) {
return;
}
this.$hiddenImageCopy.empty().append($("<img>").attr("src", utils.createImageSrcUrl(this.note)));
imageService.copyImageReferenceToClipboard(this.$hiddenImageCopy);
this.$hiddenImageCopy.empty();
});
this.contentSized();
}
}

View File

@@ -1,79 +0,0 @@
import OnClickButtonWidget from "../buttons/onclick_button.js";
import appContext from "../../components/app_context.js";
import attributeService from "../../services/attributes.js";
import protectedSessionHolder from "../../services/protected_session_holder.js";
import { t } from "../../services/i18n.js";
import LoadResults from "../../services/load_results.js";
import type { AttributeRow } from "../../services/load_results.js";
import FNote from "../../entities/fnote.js";
export default class EditButton extends OnClickButtonWidget {
isEnabled(): boolean {
return Boolean(super.isEnabled() && this.note && this.noteContext?.viewScope?.viewMode === "default");
}
constructor() {
super();
this.icon("bx-pencil")
.title(t("edit_button.edit_this_note"))
.titlePlacement("bottom")
.onClick((widget) => {
if (this.noteContext?.viewScope) {
this.noteContext.viewScope.readOnlyTemporarilyDisabled = true;
appContext.triggerEvent("readOnlyTemporarilyDisabled", { noteContext: this.noteContext });
}
});
}
async refreshWithNote(note: FNote): Promise<void> {
if (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) {
this.toggleInt(false);
} else {
// prevent flickering by assuming hidden before async operation
this.toggleInt(false);
const wasVisible = this.isVisible();
// can't do this in isEnabled() since isReadOnly is async
const isReadOnly = await this.noteContext?.isReadOnly();
this.toggleInt(Boolean(isReadOnly));
// make the edit button stand out on the first display, otherwise
// it's difficult to notice that the note is readonly
if (this.isVisible() && !wasVisible && this.$widget) {
this.$widget.addClass("bx-tada bx-lg");
setTimeout(() => {
this.$widget?.removeClass("bx-tada bx-lg");
}, 1700);
}
}
await super.refreshWithNote(note);
}
entitiesReloadedEvent({ loadResults }: { loadResults: LoadResults }): void {
if (loadResults.getAttributeRows().find((attr: AttributeRow) =>
attr.type === "label" &&
attr.name?.toLowerCase().includes("readonly") &&
this.note &&
attributeService.isAffecting(attr, this.note)
)) {
if (this.noteContext?.viewScope) {
this.noteContext.viewScope.readOnlyTemporarilyDisabled = false;
}
this.refresh();
}
}
readOnlyTemporarilyDisabledEvent() {
this.refresh();
}
async noteTypeMimeChangedEvent({ noteId }: { noteId: string }): Promise<void> {
if (this.isNote(noteId)) {
await this.refresh();
}
}
}

View File

@@ -1,147 +0,0 @@
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import { t } from "../../services/i18n.js";
import type FNote from "../../entities/fnote.js";
import type BasicWidget from "../basic_widget.js";
/*
* Note:
*
* For floating button widgets that require content to overflow, the has-overflow CSS class should
* be applied to the root element of the widget. Additionally, this root element may need to
* properly handle rounded corners, as defined by the --border-radius CSS variable.
*/
const TPL = /*html*/`
<div class="floating-buttons no-print">
<style>
.floating-buttons {
position: relative;
}
.floating-buttons-children,
.show-floating-buttons {
position: absolute;
top: 10px;
right: 10px;
display: flex;
flex-direction: row;
z-index: 100;
}
.note-split.rtl .floating-buttons-children,
.note-split.rtl .show-floating-buttons {
right: unset;
left: 10px;
}
.note-split.rtl .close-floating-buttons {
order: -1;
}
.note-split.rtl .close-floating-buttons,
.note-split.rtl .show-floating-buttons {
transform: rotate(180deg);
}
.type-canvas .floating-buttons-children {
top: 70px;
}
.type-canvas .floating-buttons-children > * {
--border-radius: 0; /* Overridden by themes */
}
.floating-buttons-children > *:not(.hidden-int):not(.no-content-hidden) {
margin: 2px;
}
.floating-buttons-children > *:not(.has-overflow) {
overflow: hidden;
}
.floating-buttons-children > button, .floating-buttons-children .floating-button {
font-size: 150%;
padding: 5px 10px 4px 10px;
width: 40px;
cursor: pointer;
color: var(--button-text-color);
background: var(--button-background-color);
border-radius: var(--button-border-radius);
border: 1px solid transparent;
display: flex;
justify-content: space-around;
}
.floating-buttons-children > button:hover, .floating-buttons-children .floating-button:hover {
text-decoration: none;
border-color: var(--button-border-color);
}
.floating-buttons .floating-buttons-children.temporarily-hidden {
display: none;
}
</style>
<div class="floating-buttons-children"></div>
<!-- Show button that displays floating button after click on close button -->
<div class="show-floating-buttons">
<style>
.floating-buttons-children.temporarily-hidden+.show-floating-buttons {
display: block;
}
.floating-buttons-children:not(.temporarily-hidden)+.show-floating-buttons {
display: none;
}
.show-floating-buttons {
/* display: none;*/
margin-left: 5px !important;
}
.show-floating-buttons-button {
border: 1px solid transparent;
color: var(--button-text-color);
padding: 6px;
border-radius: 100px;
}
.show-floating-buttons-button:hover {
border: 1px solid var(--button-border-color);
}
</style>
<button type="button" class="show-floating-buttons-button btn bx bx-chevrons-left"
title="${t("show_floating_buttons_button.button_title")}"></button>
</div>
</div>`;
export default class FloatingButtons extends NoteContextAwareWidget {
private $children!: JQuery<HTMLElement>;
doRender() {
this.$widget = $(TPL);
this.$children = this.$widget.find(".floating-buttons-children");
for (const widget of this.children) {
if ("render" in widget) {
this.$children.append((widget as BasicWidget).render());
}
}
}
async refreshWithNote(note: FNote) {
this.toggle(true);
this.$widget.find(".show-floating-buttons-button").on("click", () => this.toggle(true));
}
toggle(show: boolean) {
this.$widget.find(".floating-buttons-children").toggleClass("temporarily-hidden", !show);
}
hideFloatingButtonsCommand() {
this.toggle(false);
}
}

View File

@@ -1,36 +0,0 @@
import { t } from "../../services/i18n.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
const TPL = /*html*/`\
<div class="geo-map-buttons">
<style>
.geo-map-buttons {
contain: none;
display: flex;
gap: 10px;
}
.leaflet-pane {
z-index: 50;
}
</style>
<button type="button"
class="geo-map-create-child-note floating-button btn bx bx-plus-circle"
title="${t("geo-map.create-child-note-title")}" />
</div>`;
export default class GeoMapButtons extends NoteContextAwareWidget {
isEnabled() {
return super.isEnabled() && this.note?.type === "geoMap";
}
doRender() {
super.doRender();
this.$widget = $(TPL);
this.$widget.find(".geo-map-create-child-note").on("click", () => this.triggerEvent("geoMapCreateChildNote", { ntxId: this.ntxId }));
}
}

View File

@@ -1,36 +0,0 @@
import { describe, expect, it } from "vitest";
import { byBookType, byNoteType } from "./help_button.js";
import fs from "fs";
import type { HiddenSubtreeItem } from "@triliumnext/commons";
describe("Help button", () => {
it("All help notes are accessible", () => {
function getNoteIds(item: HiddenSubtreeItem | HiddenSubtreeItem[]): string[] {
const items: (string | string[])[] = [];
if ("id" in item && item.id) {
items.push(item.id);
}
const subitems = (Array.isArray(item) ? item : item.children);
for (const child of subitems ?? []) {
items.push(getNoteIds(child as (HiddenSubtreeItem | HiddenSubtreeItem[])));
}
return items.flat();
}
const allHelpNotes = [
...Object.values(byNoteType),
...Object.values(byBookType)
].filter((noteId) => noteId) as string[];
const meta: HiddenSubtreeItem[] = JSON.parse(fs.readFileSync("src/public/app/doc_notes/en/User Guide/!!!meta.json", "utf-8"));
const allNoteIds = new Set(getNoteIds(meta));
for (const helpNote of allHelpNotes) {
if (!allNoteIds.has(`_help_${helpNote}`)) {
expect.fail(`Help note with ID ${helpNote} does not exist in the in-app help.`);
}
}
});
});

View File

@@ -1,75 +0,0 @@
import appContext, { type EventData } from "../../components/app_context.js";
import type FNote from "../../entities/fnote.js";
import type { NoteType } from "../../entities/fnote.js";
import { t } from "../../services/i18n.js";
import type { ViewScope } from "../../services/link.js";
import type { ViewTypeOptions } from "../../services/note_list_renderer.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
const TPL = /*html*/`
<button class="open-contextual-help-button" title="${t("help-button.title")}">
<span class="bx bx-help-circle"></span>
</button>
`;
export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
canvas: null,
code: null,
contentWidget: null,
doc: null,
file: null,
geoMap: "81SGnPGMk7Xc",
image: null,
launcher: null,
mermaid: null,
mindMap: null,
noteMap: null,
relationMap: null,
render: null,
search: null,
text: null,
webView: null,
aiChat: null
};
export const byBookType: Record<ViewTypeOptions, string | null> = {
list: null,
grid: null,
calendar: "xWbu3jpNWapp"
};
export default class ContextualHelpButton extends NoteContextAwareWidget {
isEnabled() {
if (!super.isEnabled()) {
return false;
}
return !!ContextualHelpButton.#getUrlToOpen(this.note);
}
doRender() {
this.$widget = $(TPL);
}
static #getUrlToOpen(note: FNote | null | undefined) {
if (note && note.type !== "book" && byNoteType[note.type]) {
return byNoteType[note.type];
} else if (note?.hasLabel("calendarRoot")) {
return "l0tKav7yLHGF";
} else if (note && note.type === "book") {
return byBookType[note.getAttributeValue("label", "viewType") as ViewTypeOptions ?? ""]
}
}
async refreshWithNote(note: FNote | null | undefined): Promise<void> {
this.$widget.attr("data-in-app-help", ContextualHelpButton.#getUrlToOpen(this.note) ?? "");
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (this.note?.type === "book" && loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId && attr.name === "viewType")) {
this.refresh();
}
}
}

View File

@@ -1,43 +0,0 @@
import { t } from "../../services/i18n.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
const TPL = /*html*/`
<div class="close-floating-buttons">
<style>
.close-floating-buttons {
display: none;
margin-left: 5px !important;
}
/* conditionally display close button if there's some other button visible */
.floating-buttons *:not(.hidden-int):not(.hidden-no-content) ~ .close-floating-buttons {
display: block;
}
.close-floating-buttons-button {
border: 1px solid transparent;
color: var(--button-text-color);
padding: 6px;
border-radius: 100px;
}
.close-floating-buttons-button:hover {
border: 1px solid var(--button-border-color);
}
</style>
<button type="button"
class="close-floating-buttons-button btn bx bx-chevrons-right"
title="${t("hide_floating_buttons_button.button_title")}"></button>
</div>
`;
export default class HideFloatingButtonsButton extends NoteContextAwareWidget {
doRender() {
super.doRender();
this.$widget = $(TPL);
this.$widget.on("click", () => this.triggerCommand("hideFloatingButtons"));
this.contentSized();
}
}

View File

@@ -1,24 +0,0 @@
import { t } from "../../services/i18n.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
const TPL = /*html*/`
<button type="button"
class="export-svg-button"
title="${t("png_export_button.button_title")}">
<span class="bx bxs-file-png"></span>
</button>
`;
export default class PngExportButton extends NoteContextAwareWidget {
isEnabled() {
return super.isEnabled() && ["mermaid", "mindMap"].includes(this.note?.type ?? "") && this.note?.isContentAvailable() && this.noteContext?.viewScope?.viewMode === "default";
}
doRender() {
super.doRender();
this.$widget = $(TPL);
this.$widget.on("click", () => this.triggerEvent("exportPng", { ntxId: this.ntxId }));
this.contentSized();
}
}

View File

@@ -1,21 +0,0 @@
import appContext from "../../components/app_context.js";
import { t } from "../../services/i18n.js";
import OnClickButtonWidget from "../buttons/onclick_button.js";
export default class RefreshButton extends OnClickButtonWidget {
constructor() {
super();
this
.title(t("backend_log.refresh"))
.icon("bx-refresh")
.onClick(() => this.triggerEvent("refreshData", { ntxId: this.noteContext?.ntxId }))
}
isEnabled(): boolean | null | undefined {
return super.isEnabled()
&& this.note?.noteId === "_backendLog"
&& this.noteContext?.viewScope?.viewMode === "default";
}
}

View File

@@ -1,60 +0,0 @@
import { t } from "../../services/i18n.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
const TPL = /*html*/`
<div class="relation-map-buttons">
<style>
.relation-map-buttons {
display: flex;
gap: 10px;
}
</style>
<button type="button"
class="relation-map-create-child-note floating-button btn bx bx-folder-plus"
title="${t("relation_map_buttons.create_child_note_title")}"></button>
<button type="button"
class="relation-map-reset-pan-zoom floating-button btn bx bx-crop"
title="${t("relation_map_buttons.reset_pan_zoom_title")}"></button>
<div class="btn-group">
<button type="button"
class="relation-map-zoom-in floating-button btn bx bx-zoom-in"
title="${t("relation_map_buttons.zoom_in_title")}"></button>
<button type="button"
class="relation-map-zoom-out floating-button btn bx bx-zoom-out"
title="${t("relation_map_buttons.zoom_out_title")}"></button>
</div>
</div>`;
export default class RelationMapButtons extends NoteContextAwareWidget {
private $createChildNote!: JQuery<HTMLElement>;
private $zoomInButton!: JQuery<HTMLElement>;
private $zoomOutButton!: JQuery<HTMLElement>;
private $resetPanZoomButton!: JQuery<HTMLElement>;
isEnabled() {
return super.isEnabled() && this.note?.type === "relationMap";
}
doRender() {
super.doRender();
this.$widget = $(TPL);
this.$createChildNote = this.$widget.find(".relation-map-create-child-note");
this.$zoomInButton = this.$widget.find(".relation-map-zoom-in");
this.$zoomOutButton = this.$widget.find(".relation-map-zoom-out");
this.$resetPanZoomButton = this.$widget.find(".relation-map-reset-pan-zoom");
// TODO: Deduplicate object creation here.
this.$createChildNote.on("click", () => this.triggerEvent("relationMapCreateChildNote", { ntxId: this.ntxId }));
this.$resetPanZoomButton.on("click", () => this.triggerEvent("relationMapResetPanZoom", { ntxId: this.ntxId }));
this.$zoomInButton.on("click", () => this.triggerEvent("relationMapResetZoomIn", { ntxId: this.ntxId }));
this.$zoomOutButton.on("click", () => this.triggerEvent("relationMapResetZoomOut", { ntxId: this.ntxId }));
this.contentSized();
}
}

View File

@@ -1,24 +0,0 @@
import { t } from "../../services/i18n.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
const TPL = /*html*/`
<button type="button"
class="export-svg-button"
title="${t("svg_export_button.button_title")}">
<span class="bx bxs-file-image"></span>
</button>
`;
export default class SvgExportButton extends NoteContextAwareWidget {
isEnabled() {
return super.isEnabled() && ["mermaid", "mindMap"].includes(this.note?.type ?? "") && this.note?.isContentAvailable() && this.noteContext?.viewScope?.viewMode === "default";
}
doRender() {
super.doRender();
this.$widget = $(TPL);
this.$widget.on("click", () => this.triggerEvent("exportSvg", { ntxId: this.ntxId }));
this.contentSized();
}
}

View File

@@ -1,62 +0,0 @@
import type { EventData } from "../../components/app_context.js";
import { t } from "../../services/i18n.js";
import options from "../../services/options.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
const TPL = /*html*/`
<button type="button"
class="switch-layout-button">
<span class="bx"></span>
</button>
`;
export default class SwitchSplitOrientationButton extends NoteContextAwareWidget {
isEnabled() {
return super.isEnabled()
&& ["mermaid"].includes(this.note?.type ?? "")
&& this.note?.isContentAvailable()
&& !this.note?.hasLabel("readOnly")
&& this.noteContext?.viewScope?.viewMode === "default";
}
doRender(): void {
super.doRender();
this.$widget = $(TPL);
this.$widget.on("click", () => {
const currentOrientation = options.get("splitEditorOrientation");
options.save("splitEditorOrientation", toggleOrientation(currentOrientation));
});
this.#adjustIcon();
this.contentSized();
}
#adjustIcon() {
const currentOrientation = options.get("splitEditorOrientation");
const upcomingOrientation = toggleOrientation(currentOrientation);
const $icon = this.$widget.find("span.bx");
$icon
.toggleClass("bxs-dock-bottom", upcomingOrientation === "vertical")
.toggleClass("bxs-dock-left", upcomingOrientation === "horizontal");
if (upcomingOrientation === "vertical") {
this.$widget.attr("title", t("switch_layout_button.title_vertical"));
} else {
this.$widget.attr("title", t("switch_layout_button.title_horizontal"));
}
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isOptionReloaded("splitEditorOrientation")) {
this.#adjustIcon();
}
}
}
function toggleOrientation(orientation: string) {
if (orientation === "horizontal") {
return "vertical";
} else {
return "horizontal";
}
}

View File

@@ -1,48 +0,0 @@
import type FNote from "../../entities/fnote.js";
import attributes from "../../services/attributes.js";
import { t } from "../../services/i18n.js";
import OnClickButtonWidget from "../buttons/onclick_button.js";
export default class ToggleReadOnlyButton extends OnClickButtonWidget {
private isReadOnly?: boolean;
constructor() {
super();
this
.title(() => this.isReadOnly ? t("toggle_read_only_button.unlock-editing") : t("toggle_read_only_button.lock-editing"))
.titlePlacement("bottom")
.icon(() => this.isReadOnly ? "bx-lock-open-alt" : "bx-lock-alt")
.onClick(() => this.#toggleReadOnly());
}
#toggleReadOnly() {
if (!this.noteId || !this.note) {
return;
}
if (this.isReadOnly) {
attributes.removeOwnedLabelByName(this.note, "readOnly");
} else {
attributes.setLabel(this.noteId, "readOnly");
}
}
async refreshWithNote(note: FNote | null | undefined) {
const isReadOnly = !!note?.hasLabel("readOnly");
if (isReadOnly !== this.isReadOnly) {
this.isReadOnly = isReadOnly;
this.refreshIcon();
}
}
isEnabled() {
return super.isEnabled()
&& this.note?.type === "mermaid"
&& this.note?.isContentAvailable()
&& this.noteContext?.viewScope?.viewMode === "default";
}
}

View File

@@ -1,167 +0,0 @@
/**
* !!! Filename is intentionally mangled, because some adblockers don't like the word "backlinks".
*/
import { t } from "../../services/i18n.js";
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";
import type FNote from "../../entities/fnote.js";
const TPL = /*html*/`
<div class="backlinks-widget has-overflow">
<style>
.backlinks-widget {
position: relative;
}
.backlinks-ticker {
border-radius: 10px;
border-color: var(--main-border-color);
background-color: var(--more-accented-background-color);
padding: 4px 10px 4px 10px;
opacity: 90%;
display: flex;
justify-content: space-between;
align-items: center;
}
.backlinks-count {
cursor: pointer;
}
.backlinks-items {
z-index: 10;
position: absolute;
top: 50px;
right: 10px;
width: 400px;
border-radius: 10px;
background-color: var(--accented-background-color);
color: var(--main-text-color);
padding: 20px;
overflow-y: auto;
}
.backlink-excerpt {
border-left: 2px solid var(--main-border-color);
padding-left: 10px;
opacity: 80%;
font-size: 90%;
}
.backlink-excerpt .backlink-link { /* the actual backlink */
font-weight: bold;
background-color: yellow;
}
</style>
<div class="backlinks-ticker">
<span class="backlinks-count"></span>
</div>
<div class="backlinks-items dropdown-menu" style="display: none;"></div>
</div>
`;
// TODO: Deduplicate with server
interface Backlink {
noteId: string;
relationName?: string;
excerpts?: string[];
}
export default class BacklinksWidget extends NoteContextAwareWidget {
private $count!: JQuery<HTMLElement>;
private $items!: JQuery<HTMLElement>;
private $ticker!: JQuery<HTMLElement>;
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() ?? 0) - (this.$items.offset()?.top ?? 0) - 10);
if (this.$items.is(":visible")) {
this.renderBacklinks();
}
});
this.contentSized();
}
async refreshWithNote(note: FNote) {
this.clearItems();
if (this.noteContext?.viewScope?.viewMode !== "default") {
this.toggle(false);
return;
}
// can't use froca since that would count only relations from loaded notes
// TODO: Deduplicate response type
const resp = await server.get<{ count: number }>(`note-map/${this.noteId}/backlink-count`);
if (!resp || !resp.count) {
this.toggle(false);
return;
}
this.toggle(true);
this.$count.text(
// i18next plural
`${t("zpetne_odkazy.backlink", { count: resp.count })}`
);
}
toggle(show: boolean) {
this.$widget.toggleClass("hidden-no-content", !show)
.toggleClass("visible", !!show);
}
clearItems() {
this.$items.empty().hide();
}
async renderBacklinks() {
if (!this.note) {
return;
}
this.$items.empty();
const backlinks = await server.get<Backlink[]>(`note-map/${this.noteId}/backlinks`);
if (!backlinks.length) {
return;
}
await froca.getNotes(backlinks.map((bl) => bl.noteId)); // prefetch all
for (const backlink of backlinks) {
const $item = $("<div>");
$item.append(
await linkService.createLink(backlink.noteId, {
showNoteIcon: true,
showNotePath: true,
showTooltip: false
})
);
if (backlink.relationName) {
$item.append($("<p>").text(`${t("zpetne_odkazy.relation")}: ${backlink.relationName}`));
} else {
$item.append(...(backlink.excerpts ?? []));
}
this.$items.append($item);
}
}
}