refactor(breadcrumb): use new component for rendering note links

This commit is contained in:
Elian Doran 2025-12-16 13:31:17 +02:00
parent 34343ce356
commit 2693b18ee6
No known key found for this signature in database
3 changed files with 61 additions and 23 deletions

View File

@ -1,10 +1,11 @@
import treeService from "./tree.js";
import linkContextMenuService from "../menus/link_context_menu.js";
import appContext, { type NoteCommandData } from "../components/app_context.js";
import froca from "./froca.js";
import utils from "./utils.js";
import { ALLOWED_PROTOCOLS } from "@triliumnext/commons"; import { ALLOWED_PROTOCOLS } from "@triliumnext/commons";
import appContext, { type NoteCommandData } from "../components/app_context.js";
import { openInCurrentNoteContext } from "../components/note_context.js"; import { openInCurrentNoteContext } from "../components/note_context.js";
import linkContextMenuService from "../menus/link_context_menu.js";
import froca from "./froca.js";
import treeService from "./tree.js";
import utils from "./utils.js";
function getNotePathFromUrl(url: string) { function getNotePathFromUrl(url: string) {
const notePathMatch = /#(root[A-Za-z0-9_/]*)$/.exec(url); const notePathMatch = /#(root[A-Za-z0-9_/]*)$/.exec(url);
@ -122,7 +123,7 @@ async function createLink(notePath: string | undefined, options: CreateLinkOptio
const $container = $("<span>"); const $container = $("<span>");
if (showNoteIcon) { if (showNoteIcon) {
let icon = await getLinkIcon(noteId, viewMode); const icon = await getLinkIcon(noteId, viewMode);
if (icon) { if (icon) {
$container.append($("<span>").addClass(`bx ${icon}`)).append(" "); $container.append($("<span>").addClass(`bx ${icon}`)).append(" ");
@ -131,7 +132,7 @@ async function createLink(notePath: string | undefined, options: CreateLinkOptio
const hash = calculateHash({ const hash = calculateHash({
notePath, notePath,
viewScope: viewScope viewScope
}); });
const $noteLink = $("<a>", { const $noteLink = $("<a>", {
@ -171,11 +172,11 @@ async function createLink(notePath: string | undefined, options: CreateLinkOptio
return $container; return $container;
} }
function calculateHash({ notePath, ntxId, hoistedNoteId, viewScope = {} }: NoteCommandData) { export function calculateHash({ notePath, ntxId, hoistedNoteId, viewScope = {} }: NoteCommandData) {
notePath = notePath || ""; notePath = notePath || "";
const params = [ const params = [
ntxId ? { ntxId: ntxId } : null, ntxId ? { ntxId } : null,
hoistedNoteId && hoistedNoteId !== "root" ? { hoistedNoteId: hoistedNoteId } : null, hoistedNoteId && hoistedNoteId !== "root" ? { hoistedNoteId } : null,
viewScope.viewMode && viewScope.viewMode !== "default" ? { viewMode: viewScope.viewMode } : null, viewScope.viewMode && viewScope.viewMode !== "default" ? { viewMode: viewScope.viewMode } : null,
viewScope.attachmentId ? { attachmentId: viewScope.attachmentId } : null viewScope.attachmentId ? { attachmentId: viewScope.attachmentId } : null
].filter((p) => !!p); ].filter((p) => !!p);
@ -219,7 +220,7 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
} }
const hash = url.substr(hashIdx + 1); // strip also the initial '#' const hash = url.substr(hashIdx + 1); // strip also the initial '#'
let [notePath, paramString] = hash.split("?"); const [notePath, paramString] = hash.split("?");
const viewScope: ViewScope = { const viewScope: ViewScope = {
viewMode: "default" viewMode: "default"
@ -252,7 +253,7 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
} }
if (searchString) { if (searchString) {
return { searchString } return { searchString };
} }
if (!notePath.match(/^[_a-z0-9]{4,}(\/[_a-z0-9]{4,})*$/i)) { if (!notePath.match(/^[_a-z0-9]{4,}(\/[_a-z0-9]{4,})*$/i)) {
@ -334,7 +335,7 @@ export function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDo
window.open(hrefLink, "_blank"); window.open(hrefLink, "_blank");
} else { } else {
// Enable protocols supported by CKEditor 5 to be clickable. // Enable protocols supported by CKEditor 5 to be clickable.
if (ALLOWED_PROTOCOLS.some((protocol) => hrefLink.toLowerCase().startsWith(protocol + ":"))) { if (ALLOWED_PROTOCOLS.some((protocol) => hrefLink.toLowerCase().startsWith(`${protocol }:`))) {
if ( utils.isElectron()) { if ( utils.isElectron()) {
const electron = utils.dynamicRequire("electron"); const electron = utils.dynamicRequire("electron");
electron.shell.openExternal(hrefLink); electron.shell.openExternal(hrefLink);
@ -395,7 +396,7 @@ async function loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string | n
href = href || $link.attr("href"); href = href || $link.attr("href");
if (!href) { if (!href) {
console.warn("Empty URL for parsing: " + $el[0].outerHTML); console.warn(`Empty URL for parsing: ${$el[0].outerHTML}`);
return; return;
} }
@ -438,9 +439,9 @@ async function getReferenceLinkTitle(href: string) {
const attachment = await note.getAttachmentById(viewScope.attachmentId); const attachment = await note.getAttachmentById(viewScope.attachmentId);
return attachment ? attachment.title : "[missing attachment]"; return attachment ? attachment.title : "[missing attachment]";
} else {
return note.title;
} }
return note.title;
} }
function getReferenceLinkTitleSync(href: string) { function getReferenceLinkTitleSync(href: string) {
@ -462,9 +463,9 @@ function getReferenceLinkTitleSync(href: string) {
const attachment = note.attachments.find((att) => att.attachmentId === viewScope.attachmentId); const attachment = note.attachments.find((att) => att.attachmentId === viewScope.attachmentId);
return attachment ? attachment.title : "[missing attachment]"; return attachment ? attachment.title : "[missing attachment]";
} else {
return note.title;
} }
return note.title;
} }
if (glob.device !== "print") { if (glob.device !== "print") {

View File

@ -25,7 +25,7 @@ import Dropdown from "../react/Dropdown";
import { FormListItem } from "../react/FormList"; import { FormListItem } from "../react/FormList";
import { useActiveNoteContext, useChildNotes, useNote, useNoteIcon, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useStaticTooltip } from "../react/hooks"; import { useActiveNoteContext, useChildNotes, useNote, useNoteIcon, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useStaticTooltip } from "../react/hooks";
import Icon from "../react/Icon"; import Icon from "../react/Icon";
import NoteLink from "../react/NoteLink"; import { NewNoteLink } from "../react/NoteLink";
import { ParentComponent } from "../react/react_utils"; import { ParentComponent } from "../react/react_utils";
const COLLAPSE_THRESHOLD = 5; const COLLAPSE_THRESHOLD = 5;
@ -114,7 +114,7 @@ function BreadcrumbHoistedNoteRoot({ noteId }: { noteId: string }) {
"color": getReadableTextColor(workspaceColor) "color": getReadableTextColor(workspaceColor)
} : undefined} } : undefined}
/> />
<NoteLink <NewNoteLink
notePath={noteId} notePath={noteId}
showNoteIcon showNoteIcon
noPreview noPreview
@ -161,7 +161,7 @@ function BreadcrumbItem({ index, notePath, noteContext, notePathLength, parentCo
</>; </>;
} }
return <NoteLink return <NewNoteLink
notePath={notePath} notePath={notePath}
noContextMenu noContextMenu
onContextMenu={buildContextMenu(notePath, parentComponent)} onContextMenu={buildContextMenu(notePath, parentComponent)}

View File

@ -1,7 +1,10 @@
import clsx from "clsx";
import { HTMLAttributes } from "preact";
import { useEffect, useRef, useState } from "preact/hooks"; import { useEffect, useRef, useState } from "preact/hooks";
import link, { ViewScope } from "../../services/link"; import link, { calculateHash, ViewScope } from "../../services/link";
import { useImperativeSearchHighlighlighting, useTriliumEvent } from "./hooks"; import { useImperativeSearchHighlighlighting, useNote, useNoteIcon, useNoteProperty, useTriliumEvent } from "./hooks";
import Icon from "./Icon";
interface NoteLinkOpts { interface NoteLinkOpts {
className?: string; className?: string;
@ -83,3 +86,37 @@ export default function NoteLink({ className, containerClassName, notePath, show
return <span className={containerClassName} ref={ref} />; return <span className={containerClassName} ref={ref} />;
} }
interface NewNoteLinkProps extends Pick<HTMLAttributes<HTMLAnchorElement>, "onContextMenu"> {
notePath: string;
viewScope?: ViewScope;
noContextMenu?: boolean;
showNoteIcon?: boolean;
noPreview?: boolean;
}
export function NewNoteLink({ notePath, viewScope, noContextMenu, showNoteIcon, noPreview, ...linkProps }: NewNoteLinkProps) {
const noteId = notePath.split("/").at(-1);
const note = useNote(noteId);
const title = useNoteProperty(note, "title");
const icon = useNoteIcon(showNoteIcon ? note : null);
return (
<span>
<span>
{icon && <Icon icon={icon} />}
<a
className={clsx("tn-link", {
"no-tooltip-preview": noPreview
})}
href={calculateHash({ notePath, viewScope })}
data-no-context-menu={noContextMenu}
{...linkProps}
>
{title}
</a>
</span>
</span>
);
}