mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-04 05:28:59 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			528 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			528 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
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";
 | 
						|
 | 
						|
// Be consistent with `allowedSchemes` in `src\services\html_sanitizer.ts`
 | 
						|
// TODO: Deduplicate with server once we can.
 | 
						|
export const ALLOWED_PROTOCOLS = [
 | 
						|
    'http', 'https', 'ftp', 'ftps', 'mailto', 'data', 'evernote', 'file', 'facetime', 'gemini', 'git',
 | 
						|
    'gopher', 'imap', 'irc', 'irc6', 'jabber', 'jar', 'lastfm', 'ldap', 'ldaps', 'magnet', 'message',
 | 
						|
    'mumble', 'nfs', 'onenote', 'pop', 'rmi', 's3', 'sftp', 'skype', 'sms', 'spotify', 'steam', 'svn', 'udp',
 | 
						|
    'view-source', 'vlc', 'vnc', 'ws', 'wss', 'xmpp', 'jdbc', 'slack', 'tel', 'smb', 'zotero', 'geo',
 | 
						|
    'mid'
 | 
						|
];
 | 
						|
 | 
						|
function getNotePathFromUrl(url: string) {
 | 
						|
    const notePathMatch = /#(root[A-Za-z0-9_/]*)$/.exec(url);
 | 
						|
 | 
						|
    return notePathMatch === null ? null : notePathMatch[1];
 | 
						|
}
 | 
						|
 | 
						|
async function getLinkIcon(noteId: string, viewMode: ViewMode | undefined) {
 | 
						|
    let icon;
 | 
						|
 | 
						|
    if (!viewMode || viewMode === "default") {
 | 
						|
        const note = await froca.getNote(noteId);
 | 
						|
 | 
						|
        icon = note?.getIcon();
 | 
						|
    } else if (viewMode === "source") {
 | 
						|
        icon = "bx bx-code-curly";
 | 
						|
    } else if (viewMode === "attachments") {
 | 
						|
        icon = "bx bx-file";
 | 
						|
    }
 | 
						|
    return icon;
 | 
						|
}
 | 
						|
 | 
						|
// TODO: Remove `string` once all the view modes have been mapped.
 | 
						|
type ViewMode = "default" | "source" | "attachments" | "contextual-help" | string;
 | 
						|
 | 
						|
export interface ViewScope {
 | 
						|
    /**
 | 
						|
     * - "source", when viewing the source code of a note.
 | 
						|
     * - "attachments", when viewing the attachments of a note.
 | 
						|
     * - "contextual-help", if the current view represents a help window that was opened to the side of the main content.
 | 
						|
     * - "default", otherwise.
 | 
						|
     */
 | 
						|
    viewMode?: ViewMode;
 | 
						|
    attachmentId?: string;
 | 
						|
    readOnlyTemporarilyDisabled?: boolean;
 | 
						|
    /**
 | 
						|
     * If true, it indicates that the note in the view should be opened in read-only mode (for supported note types such as text or code).
 | 
						|
     *
 | 
						|
     * The reason why we store this information here is that a note can become read-only as the user types content in it, and we wouldn't want
 | 
						|
     * to immediately enter read-only mode.
 | 
						|
     */
 | 
						|
    isReadOnly?: boolean;
 | 
						|
    highlightsListPreviousVisible?: boolean;
 | 
						|
    highlightsListTemporarilyHidden?: boolean;
 | 
						|
    tocTemporarilyHidden?: boolean;
 | 
						|
    /*
 | 
						|
     * The reason for adding tocPreviousVisible is to record whether the previous state of the toc is hidden or displayed,
 | 
						|
     * and then let it be displayed/hidden at the initial time. If there is no such value,
 | 
						|
     * when the right panel needs to display highlighttext but not toc, every time the note content is changed,
 | 
						|
     * toc will appear and then close immediately, because getToc(html) function will consume time
 | 
						|
     */
 | 
						|
    tocPreviousVisible?: boolean;
 | 
						|
    tocCollapsedHeadings?:  Set<string>;
 | 
						|
}
 | 
						|
 | 
						|
interface CreateLinkOptions {
 | 
						|
    title?: string;
 | 
						|
    showTooltip?: boolean;
 | 
						|
    showNotePath?: boolean;
 | 
						|
    showNoteIcon?: boolean;
 | 
						|
    referenceLink?: boolean;
 | 
						|
    autoConvertToImage?: boolean;
 | 
						|
    viewScope?: ViewScope;
 | 
						|
}
 | 
						|
 | 
						|
async function createLink(notePath: string | undefined, options: CreateLinkOptions = {}) {
 | 
						|
    if (!notePath || !notePath.trim()) {
 | 
						|
        logError("Missing note path");
 | 
						|
 | 
						|
        return $("<span>").text("[missing note]");
 | 
						|
    }
 | 
						|
 | 
						|
    if (!notePath.startsWith("root")) {
 | 
						|
        // all note paths should start with "root/" (except for "root" itself)
 | 
						|
        // used, e.g., to find internal links
 | 
						|
        notePath = `root/${notePath}`;
 | 
						|
    }
 | 
						|
 | 
						|
    const showTooltip = options.showTooltip === undefined ? true : options.showTooltip;
 | 
						|
    const showNotePath = options.showNotePath === undefined ? false : options.showNotePath;
 | 
						|
    const showNoteIcon = options.showNoteIcon === undefined ? false : options.showNoteIcon;
 | 
						|
    const referenceLink = options.referenceLink === undefined ? false : options.referenceLink;
 | 
						|
    const autoConvertToImage = options.autoConvertToImage === undefined ? false : options.autoConvertToImage;
 | 
						|
 | 
						|
    const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath);
 | 
						|
    if (!noteId) {
 | 
						|
        logError("Missing note ID");
 | 
						|
 | 
						|
        return $("<span>").text("[missing note]");
 | 
						|
    }
 | 
						|
 | 
						|
    const viewScope = options.viewScope || {};
 | 
						|
    const viewMode = viewScope.viewMode || "default";
 | 
						|
    let linkTitle = options.title;
 | 
						|
 | 
						|
    if (!linkTitle) {
 | 
						|
        if (viewMode === "attachments" && viewScope.attachmentId) {
 | 
						|
            const attachment = await froca.getAttachment(viewScope.attachmentId);
 | 
						|
 | 
						|
            linkTitle = attachment ? attachment.title : "[missing attachment]";
 | 
						|
        } else if (noteId) {
 | 
						|
            linkTitle = await treeService.getNoteTitle(noteId, parentNoteId);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    const note = await froca.getNote(noteId);
 | 
						|
 | 
						|
    if (autoConvertToImage && note?.type && ["image", "canvas", "mermaid"].includes(note.type) && viewMode === "default") {
 | 
						|
        const encodedTitle = encodeURIComponent(linkTitle || "");
 | 
						|
 | 
						|
        return $("<img>")
 | 
						|
            .attr("src", `api/images/${noteId}/${encodedTitle}?${Math.random()}`)
 | 
						|
            .attr("alt", linkTitle || "");
 | 
						|
    }
 | 
						|
 | 
						|
    const $container = $("<span>");
 | 
						|
 | 
						|
    if (showNoteIcon) {
 | 
						|
        let icon = await getLinkIcon(noteId, viewMode);
 | 
						|
 | 
						|
        if (icon) {
 | 
						|
            $container.append($("<span>").addClass(`bx ${icon}`)).append(" ");
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    const hash = calculateHash({
 | 
						|
        notePath,
 | 
						|
        viewScope: viewScope
 | 
						|
    });
 | 
						|
 | 
						|
    const $noteLink = $("<a>", {
 | 
						|
        href: hash,
 | 
						|
        text: linkTitle
 | 
						|
    });
 | 
						|
 | 
						|
    if (!showTooltip) {
 | 
						|
        $noteLink.addClass("no-tooltip-preview");
 | 
						|
    }
 | 
						|
 | 
						|
    if (referenceLink) {
 | 
						|
        $noteLink.addClass("reference-link");
 | 
						|
    }
 | 
						|
 | 
						|
    $container.append($noteLink);
 | 
						|
 | 
						|
    if (showNotePath) {
 | 
						|
        const resolvedPathSegments = (await treeService.resolveNotePathToSegments(notePath)) || [];
 | 
						|
        resolvedPathSegments.pop(); // Remove last element
 | 
						|
 | 
						|
        const resolvedPath = resolvedPathSegments.join("/");
 | 
						|
        const pathSegments = await treeService.getNotePathTitleComponents(resolvedPath);
 | 
						|
 | 
						|
        if (pathSegments) {
 | 
						|
            if (pathSegments.length) {
 | 
						|
                $container.append($("<small>").append(treeService.formatNotePath(pathSegments)));
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    return $container;
 | 
						|
}
 | 
						|
 | 
						|
function calculateHash({ notePath, ntxId, hoistedNoteId, viewScope = {} }: NoteCommandData) {
 | 
						|
    notePath = notePath || "";
 | 
						|
    const params = [
 | 
						|
        ntxId ? { ntxId: ntxId } : null,
 | 
						|
        hoistedNoteId && hoistedNoteId !== "root" ? { hoistedNoteId: hoistedNoteId } : null,
 | 
						|
        viewScope.viewMode && viewScope.viewMode !== "default" ? { viewMode: viewScope.viewMode } : null,
 | 
						|
        viewScope.attachmentId ? { attachmentId: viewScope.attachmentId } : null
 | 
						|
    ].filter((p) => !!p);
 | 
						|
 | 
						|
    const paramStr = params
 | 
						|
        .map((pair) => {
 | 
						|
            const name = Object.keys(pair)[0];
 | 
						|
            const value = (pair as Record<string, string | undefined>)[name];
 | 
						|
 | 
						|
            return `${encodeURIComponent(name)}=${encodeURIComponent(value || "")}`;
 | 
						|
        })
 | 
						|
        .join("&");
 | 
						|
 | 
						|
    if (!notePath && !paramStr) {
 | 
						|
        return "";
 | 
						|
    }
 | 
						|
 | 
						|
    let hash = `#${notePath}`;
 | 
						|
 | 
						|
    if (paramStr) {
 | 
						|
        hash += `?${paramStr}`;
 | 
						|
    }
 | 
						|
 | 
						|
    return hash;
 | 
						|
}
 | 
						|
 | 
						|
export function parseNavigationStateFromUrl(url: string | undefined) {
 | 
						|
    if (!url) {
 | 
						|
        return {};
 | 
						|
    }
 | 
						|
 | 
						|
    url = url.trim();
 | 
						|
    const hashIdx = url.indexOf("#");
 | 
						|
    if (hashIdx === -1) {
 | 
						|
        return {};
 | 
						|
    }
 | 
						|
 | 
						|
    // Exclude external links that contain #
 | 
						|
    if (hashIdx !== 0 && !url.includes("/#root") && !url.includes("/#?searchString") && !url.includes("/?extraWindow")) {
 | 
						|
        return {};
 | 
						|
    }
 | 
						|
 | 
						|
    const hash = url.substr(hashIdx + 1); // strip also the initial '#'
 | 
						|
    let [notePath, paramString] = hash.split("?");
 | 
						|
 | 
						|
    const viewScope: ViewScope = {
 | 
						|
        viewMode: "default"
 | 
						|
    };
 | 
						|
    let ntxId: string | null = null;
 | 
						|
    let hoistedNoteId: string | null = null;
 | 
						|
    let searchString: string | null = null;
 | 
						|
    let openInPopup = false;
 | 
						|
 | 
						|
    if (paramString) {
 | 
						|
        for (const pair of paramString.split("&")) {
 | 
						|
            let [name, value] = pair.split("=");
 | 
						|
            name = decodeURIComponent(name);
 | 
						|
            value = decodeURIComponent(value);
 | 
						|
 | 
						|
            if (name === "ntxId") {
 | 
						|
                ntxId = value;
 | 
						|
            } else if (name === "hoistedNoteId") {
 | 
						|
                hoistedNoteId = value;
 | 
						|
            } else if (name === "searchString") {
 | 
						|
                searchString = value; // supports triggering search from URL, e.g. #?searchString=blabla
 | 
						|
            } else if (["viewMode", "attachmentId"].includes(name)) {
 | 
						|
                (viewScope as any)[name] = value;
 | 
						|
            } else if (name === "popup") {
 | 
						|
                openInPopup = true;
 | 
						|
            } else {
 | 
						|
                console.warn(`Unrecognized hash parameter '${name}'.`);
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    if (searchString) {
 | 
						|
        return { searchString }
 | 
						|
    }
 | 
						|
 | 
						|
    if (!notePath.match(/^[_a-z0-9]{4,}(\/[_a-z0-9]{4,})*$/i)) {
 | 
						|
        return {};
 | 
						|
    }
 | 
						|
 | 
						|
    return {
 | 
						|
        notePath,
 | 
						|
        noteId: treeService.getNoteIdFromUrl(notePath),
 | 
						|
        ntxId,
 | 
						|
        hoistedNoteId,
 | 
						|
        viewScope,
 | 
						|
        searchString,
 | 
						|
        openInPopup
 | 
						|
    };
 | 
						|
}
 | 
						|
 | 
						|
function goToLink(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent) {
 | 
						|
    const $link = $(evt.target as any).closest("a,.block-link");
 | 
						|
    const hrefLink = $link.attr("href") || $link.attr("data-href");
 | 
						|
 | 
						|
    return goToLinkExt(evt, hrefLink, $link);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Handles navigation to a link, which can be an internal note path (e.g., `#root/1234`) or an external URL (e.g., `https://example.com`).
 | 
						|
 *
 | 
						|
 * @param evt the event that triggered the link navigation, or `null` if the link was clicked programmatically. Used to determine if the link should be opened in a new tab/window, based on the button presses.
 | 
						|
 * @param hrefLink the link to navigate to, which can be a note path (e.g., `#root/1234`) or an external URL with any supported protocol (e.g., `https://example.com`).
 | 
						|
 * @param $link the jQuery element of the link that was clicked, used to determine if the link is an anchor link (e.g., `#fn1` or `#fnref1`) and to handle it accordingly.
 | 
						|
 * @returns `true` if the link was handled (i.e., the element was found and scrolled to), or a falsy value otherwise.
 | 
						|
 */
 | 
						|
function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement> | null, hrefLink: string | undefined, $link?: JQuery<HTMLElement> | null) {
 | 
						|
    if (hrefLink?.startsWith("data:")) {
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
 | 
						|
    evt?.preventDefault();
 | 
						|
    evt?.stopPropagation();
 | 
						|
 | 
						|
    if (hrefLink && hrefLink.startsWith("#") && !hrefLink.startsWith("#root/") && $link) {
 | 
						|
        if (handleAnchor(hrefLink, $link)) {
 | 
						|
            return true;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    const { notePath, viewScope, openInPopup } = parseNavigationStateFromUrl(hrefLink);
 | 
						|
 | 
						|
    const ctrlKey = evt && utils.isCtrlKey(evt);
 | 
						|
    const shiftKey = evt?.shiftKey;
 | 
						|
    const isLeftClick = !evt || ("which" in evt && evt.which === 1);
 | 
						|
    // Right click is handled separately.
 | 
						|
    const isMiddleClick = evt && "which" in evt && evt.which === 2;
 | 
						|
    const targetIsBlank = ($link?.attr("target") === "_blank");
 | 
						|
    const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick || targetIsBlank;
 | 
						|
    const activate = (isLeftClick && ctrlKey && shiftKey) || (isMiddleClick && shiftKey);
 | 
						|
    const openInNewWindow = isLeftClick && evt?.shiftKey && !ctrlKey;
 | 
						|
 | 
						|
    if (notePath) {
 | 
						|
        if (isLeftClick && openInPopup) {
 | 
						|
            appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath });
 | 
						|
        } else if (openInNewWindow) {
 | 
						|
            appContext.triggerCommand("openInWindow", { notePath, viewScope });
 | 
						|
        } else if (openInNewTab) {
 | 
						|
            appContext.tabManager.openTabWithNoteWithHoisting(notePath, {
 | 
						|
                activate: activate ? true : targetIsBlank,
 | 
						|
                viewScope
 | 
						|
            });
 | 
						|
        } else if (isLeftClick) {
 | 
						|
            const ntxId = $(evt?.target as any)
 | 
						|
                .closest("[data-ntx-id]")
 | 
						|
                .attr("data-ntx-id");
 | 
						|
 | 
						|
            const noteContext = ntxId ? appContext.tabManager.getNoteContextById(ntxId) : appContext.tabManager.getActiveContext();
 | 
						|
 | 
						|
            if (noteContext) {
 | 
						|
                noteContext.setNote(notePath, { viewScope }).then(() => {
 | 
						|
                    if (noteContext !== appContext.tabManager.getActiveContext()) {
 | 
						|
                        appContext.tabManager.activateNoteContext(noteContext.ntxId);
 | 
						|
                    }
 | 
						|
                });
 | 
						|
            } else {
 | 
						|
                appContext.tabManager.openContextWithNote(notePath, { viewScope, activate: true });
 | 
						|
            }
 | 
						|
        }
 | 
						|
    } else if (hrefLink) {
 | 
						|
        const withinEditLink = $link?.hasClass("ck-link-actions__preview");
 | 
						|
        const outsideOfCKEditor = !$link || $link.closest("[contenteditable]").length === 0;
 | 
						|
 | 
						|
        if (openInNewTab || (withinEditLink && (isLeftClick || isMiddleClick)) || (outsideOfCKEditor && (isLeftClick || isMiddleClick))) {
 | 
						|
            if (hrefLink.toLowerCase().startsWith("http") || hrefLink.startsWith("api/")) {
 | 
						|
                window.open(hrefLink, "_blank");
 | 
						|
            } else if ((hrefLink.toLowerCase().startsWith("file:") || hrefLink.toLowerCase().startsWith("geo:")) && utils.isElectron()) {
 | 
						|
                const electron = utils.dynamicRequire("electron");
 | 
						|
                electron.shell.openPath(hrefLink);
 | 
						|
            } else {
 | 
						|
                // Enable protocols supported by CKEditor 5 to be clickable.
 | 
						|
                if (ALLOWED_PROTOCOLS.some((protocol) => hrefLink.toLowerCase().startsWith(protocol + ":"))) {
 | 
						|
                    window.open(hrefLink, "_blank");
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    return true;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Scrolls to either the footnote (if clicking on a reference such as `[1]`), or to the reference of a footnote (if clicking on the footnote `^` arrow),
 | 
						|
 * or CKEditor bookmarks.
 | 
						|
 *
 | 
						|
 * @param hrefLink the URL of the link that was clicked (it should be in the form of `#fn` or `#fnref`).
 | 
						|
 * @param $link the element of the link that was clicked.
 | 
						|
 * @returns `true` if the link was handled (i.e., the element was found and scrolled to), `false` otherwise.
 | 
						|
 */
 | 
						|
function handleAnchor(hrefLink: string, $link: JQuery<HTMLElement>) {
 | 
						|
    const el = $link.closest(".ck-content").find(hrefLink)[0];
 | 
						|
    if (el) {
 | 
						|
        el.scrollIntoView({ behavior: "smooth", block: "center" });
 | 
						|
    }
 | 
						|
    return !!el;
 | 
						|
}
 | 
						|
 | 
						|
function linkContextMenu(e: PointerEvent) {
 | 
						|
    const $link = $(e.target as any).closest("a");
 | 
						|
    const url = $link.attr("href") || $link.attr("data-href");
 | 
						|
 | 
						|
    if ($link.attr("data-no-context-menu")) {
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    const { notePath, viewScope } = parseNavigationStateFromUrl(url);
 | 
						|
 | 
						|
    if (!notePath) {
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (utils.isCtrlKey(e) && e.button === 2) {
 | 
						|
        appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath });
 | 
						|
        e.preventDefault();
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    e.preventDefault();
 | 
						|
 | 
						|
    linkContextMenuService.openContextMenu(notePath, e, viewScope, null);
 | 
						|
}
 | 
						|
 | 
						|
async function loadReferenceLinkTitle($el: JQuery<HTMLElement>, href: string | null | undefined = null) {
 | 
						|
    const $link = $el[0].tagName === "A" ? $el : $el.find("a");
 | 
						|
 | 
						|
    href = href || $link.attr("href");
 | 
						|
    if (!href) {
 | 
						|
        console.warn("Empty URL for parsing: " + $el[0].outerHTML);
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    const { noteId, viewScope } = parseNavigationStateFromUrl(href);
 | 
						|
    if (!noteId) {
 | 
						|
        console.warn("Missing note ID.");
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    const note = await froca.getNote(noteId, true);
 | 
						|
 | 
						|
    if (note) {
 | 
						|
        $el.addClass(note.getColorClass());
 | 
						|
    }
 | 
						|
 | 
						|
    const title = await getReferenceLinkTitle(href);
 | 
						|
    $el.text(title);
 | 
						|
 | 
						|
    if (note) {
 | 
						|
        const icon = await getLinkIcon(noteId, viewScope.viewMode);
 | 
						|
 | 
						|
        if (icon) {
 | 
						|
            $el.prepend($("<span>").addClass(icon));
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
async function getReferenceLinkTitle(href: string) {
 | 
						|
    const { noteId, viewScope } = parseNavigationStateFromUrl(href);
 | 
						|
    if (!noteId) {
 | 
						|
        return "[missing note]";
 | 
						|
    }
 | 
						|
 | 
						|
    const note = await froca.getNote(noteId);
 | 
						|
    if (!note) {
 | 
						|
        return "[missing note]";
 | 
						|
    }
 | 
						|
 | 
						|
    if (viewScope?.viewMode === "attachments" && viewScope?.attachmentId) {
 | 
						|
        const attachment = await note.getAttachmentById(viewScope.attachmentId);
 | 
						|
 | 
						|
        return attachment ? attachment.title : "[missing attachment]";
 | 
						|
    } else {
 | 
						|
        return note.title;
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
function getReferenceLinkTitleSync(href: string) {
 | 
						|
    const { noteId, viewScope } = parseNavigationStateFromUrl(href);
 | 
						|
    if (!noteId) {
 | 
						|
        return "[missing note]";
 | 
						|
    }
 | 
						|
 | 
						|
    const note = froca.getNoteFromCache(noteId);
 | 
						|
    if (!note) {
 | 
						|
        return "[missing note]";
 | 
						|
    }
 | 
						|
 | 
						|
    if (viewScope?.viewMode === "attachments" && viewScope?.attachmentId) {
 | 
						|
        if (!note.attachments) {
 | 
						|
            return "[loading title...]";
 | 
						|
        }
 | 
						|
 | 
						|
        const attachment = note.attachments.find((att) => att.attachmentId === viewScope.attachmentId);
 | 
						|
 | 
						|
        return attachment ? attachment.title : "[missing attachment]";
 | 
						|
    } else {
 | 
						|
        return note.title;
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
// TODO: Check why the event is not supported.
 | 
						|
//@ts-ignore
 | 
						|
$(document).on("click", "a", goToLink);
 | 
						|
// TODO: Check why the event is not supported.
 | 
						|
//@ts-ignore
 | 
						|
$(document).on("auxclick", "a", goToLink); // to handle the middle button
 | 
						|
// TODO: Check why the event is not supported.
 | 
						|
//@ts-ignore
 | 
						|
$(document).on("contextmenu", "a", linkContextMenu);
 | 
						|
$(document).on("dblclick", "a", (e) => {
 | 
						|
    e.preventDefault();
 | 
						|
    e.stopPropagation();
 | 
						|
 | 
						|
    const $link = $(e.target).closest("a");
 | 
						|
 | 
						|
    const address = $link.attr("href");
 | 
						|
 | 
						|
    if (address && address.startsWith("http")) {
 | 
						|
        window.open(address, "_blank");
 | 
						|
    }
 | 
						|
});
 | 
						|
 | 
						|
$(document).on("mousedown", "a", (e) => {
 | 
						|
    if (e.which === 2) {
 | 
						|
        // prevent paste on middle click
 | 
						|
        // https://github.com/zadam/trilium/issues/2995
 | 
						|
        // https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event#preventing_default_actions
 | 
						|
        e.preventDefault();
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
});
 | 
						|
 | 
						|
export default {
 | 
						|
    getNotePathFromUrl,
 | 
						|
    createLink,
 | 
						|
    goToLink,
 | 
						|
    goToLinkExt,
 | 
						|
    loadReferenceLinkTitle,
 | 
						|
    getReferenceLinkTitle,
 | 
						|
    getReferenceLinkTitleSync,
 | 
						|
    calculateHash,
 | 
						|
    parseNavigationStateFromUrl
 | 
						|
};
 |