From a759c1fbd212f9d481bff64b6fb201a4326470f1 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 21 Dec 2024 22:37:19 +0200 Subject: [PATCH] chore(client/ts): port services/link --- src/public/app/services/{link.js => link.ts} | 93 ++++++++++++++------ src/public/app/services/utils.ts | 2 +- 2 files changed, 68 insertions(+), 27 deletions(-) rename src/public/app/services/{link.js => link.ts} (83%) diff --git a/src/public/app/services/link.js b/src/public/app/services/link.ts similarity index 83% rename from src/public/app/services/link.js rename to src/public/app/services/link.ts index b7e236be5..a4d6a6d6e 100644 --- a/src/public/app/services/link.js +++ b/src/public/app/services/link.ts @@ -4,19 +4,19 @@ import appContext from "../components/app_context.js"; import froca from "./froca.js"; import utils from "./utils.js"; -function getNotePathFromUrl(url) { +function getNotePathFromUrl(url: string) { const notePathMatch = /#(root[A-Za-z0-9_/]*)$/.exec(url); return notePathMatch === null ? null : notePathMatch[1]; } -async function getLinkIcon(noteId, viewMode) { +async function getLinkIcon(noteId: string, viewMode: ViewMode | undefined) { let icon; - if (viewMode === 'default') { + if (!viewMode || viewMode === 'default') { const note = await froca.getNote(noteId); - icon = note.getIcon(); + icon = note?.getIcon(); } else if (viewMode === 'source') { icon = 'bx bx-code-curly'; } else if (viewMode === 'attachments') { @@ -25,7 +25,24 @@ async function getLinkIcon(noteId, viewMode) { return icon; } -async function createLink(notePath, options = {}) { +type ViewMode = "default" | "source" | "attachments" | string; + +interface ViewScope { + viewMode?: ViewMode; + attachmentId?: string; +} + +interface CreateLinkOptions { + title?: string; + showTooltip?: boolean; + showNotePath?: boolean; + showNoteIcon?: boolean; + referenceLink?: boolean; + autoConvertToImage?: boolean; + viewScope?: ViewScope; +} + +async function createLink(notePath: string, options: CreateLinkOptions = {}) { if (!notePath || !notePath.trim()) { logError("Missing note path"); @@ -45,6 +62,12 @@ async function createLink(notePath, options = {}) { const autoConvertToImage = options.autoConvertToImage === undefined ? false : options.autoConvertToImage; const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(notePath); + if (!noteId) { + logError("Missing note ID"); + + return $("").text("[missing note]"); + } + const viewScope = options.viewScope || {}; const viewMode = viewScope.viewMode || 'default'; let linkTitle = options.title; @@ -54,19 +77,19 @@ async function createLink(notePath, options = {}) { const attachment = await froca.getAttachment(viewScope.attachmentId); linkTitle = attachment ? attachment.title : '[missing attachment]'; - } else { + } else if (noteId) { linkTitle = await treeService.getNoteTitle(noteId, parentNoteId); } } const note = await froca.getNote(noteId); - if (autoConvertToImage && ['image', 'canvas', 'mermaid'].includes(note.type) && viewMode === 'default') { - const encodedTitle = encodeURIComponent(linkTitle); + if (autoConvertToImage && (note?.type && ['image', 'canvas', 'mermaid'].includes(note.type)) && viewMode === 'default') { + const encodedTitle = encodeURIComponent(linkTitle || ""); return $("") .attr("src", `api/images/${noteId}/${encodedTitle}?${Math.random()}`) - .attr("alt", linkTitle); + .attr("alt", linkTitle || ""); } const $container = $(""); @@ -102,7 +125,7 @@ async function createLink(notePath, options = {}) { $container.append($noteLink); if (showNotePath) { - const resolvedPathSegments = await treeService.resolveNotePathToSegments(notePath); + const resolvedPathSegments = await treeService.resolveNotePathToSegments(notePath) || []; resolvedPathSegments.pop(); // Remove last element const resolvedPath = resolvedPathSegments.join("/"); @@ -118,7 +141,14 @@ async function createLink(notePath, options = {}) { return $container; } -function calculateHash({notePath, ntxId, hoistedNoteId, viewScope = {}}) { +interface CalculateHashOpts { + notePath: string; + ntxId?: string; + hoistedNoteId?: string; + viewScope: ViewScope; +} + +function calculateHash({notePath, ntxId, hoistedNoteId, viewScope = {}}: CalculateHashOpts) { notePath = notePath || ""; const params = [ ntxId ? { ntxId: ntxId } : null, @@ -129,9 +159,9 @@ function calculateHash({notePath, ntxId, hoistedNoteId, viewScope = {}}) { const paramStr = params.map(pair => { const name = Object.keys(pair)[0]; - const value = pair[name]; + const value = (pair as Record)[name]; - return `${encodeURIComponent(name)}=${encodeURIComponent(value)}`; + return `${encodeURIComponent(name)}=${encodeURIComponent(value || "")}`; }).join("&"); if (!notePath && !paramStr) { @@ -147,7 +177,7 @@ function calculateHash({notePath, ntxId, hoistedNoteId, viewScope = {}}) { return hash; } -function parseNavigationStateFromUrl(url) { +function parseNavigationStateFromUrl(url: string | undefined) { if (!url) { return {}; } @@ -164,7 +194,7 @@ function parseNavigationStateFromUrl(url) { return {}; } - const viewScope = { + const viewScope: ViewScope = { viewMode: 'default' }; let ntxId = null; @@ -184,7 +214,7 @@ function parseNavigationStateFromUrl(url) { } else if (name === 'searchString') { searchString = value; // supports triggering search from URL, e.g. #?searchString=blabla } else if (['viewMode', 'attachmentId'].includes(name)) { - viewScope[name] = value; + (viewScope as any)[name] = value; } else { console.warn(`Unrecognized hash parameter '${name}'.`); } @@ -201,14 +231,14 @@ function parseNavigationStateFromUrl(url) { }; } -function goToLink(evt) { - const $link = $(evt.target).closest("a,.block-link"); +function goToLink(evt: MouseEvent) { + const $link = $(evt.target as any).closest("a,.block-link"); const hrefLink = $link.attr('href') || $link.attr('data-href'); return goToLinkExt(evt, hrefLink, $link); } -function goToLinkExt(evt, hrefLink, $link) { +function goToLinkExt(evt: MouseEvent, hrefLink: string | undefined, $link: JQuery) { if (hrefLink?.startsWith("data:")) { return true; } @@ -230,7 +260,7 @@ function goToLinkExt(evt, hrefLink, $link) { if (openInNewTab) { appContext.tabManager.openTabWithNoteWithHoisting(notePath, {viewScope}); } else if (isLeftClick) { - const ntxId = $(evt.target).closest("[data-ntx-id]").attr("data-ntx-id"); + const ntxId = $(evt.target as any).closest("[data-ntx-id]").attr("data-ntx-id"); const noteContext = ntxId ? appContext.tabManager.getNoteContextById(ntxId) @@ -275,8 +305,8 @@ function goToLinkExt(evt, hrefLink, $link) { return true; } -function linkContextMenu(e) { - const $link = $(e.target).closest("a"); +function linkContextMenu(e: Event) { + const $link = $(e.target as any).closest("a"); const url = $link.attr("href") || $link.attr("data-href"); const { notePath, viewScope } = parseNavigationStateFromUrl(url); @@ -290,7 +320,7 @@ function linkContextMenu(e) { linkContextMenuService.openContextMenu(notePath, e, viewScope, null); } -async function loadReferenceLinkTitle($el, href = null) { +async function loadReferenceLinkTitle($el: JQuery, href: string | null | undefined = null) { const $link = $el[0].tagName === 'A' ? $el : $el.find("a"); href = href || $link.attr("href"); @@ -300,6 +330,11 @@ async function loadReferenceLinkTitle($el, href = null) { } const {noteId, viewScope} = parseNavigationStateFromUrl(href); + if (!noteId) { + console.warn("Missing note ID."); + return; + } + const note = await froca.getNote(noteId, true); if (note) { @@ -312,11 +347,13 @@ async function loadReferenceLinkTitle($el, href = null) { if (note) { const icon = await getLinkIcon(noteId, viewScope.viewMode); - $el.prepend($("").addClass(icon)); + if (icon) { + $el.prepend($("").addClass(icon)); + } } } -async function getReferenceLinkTitle(href) { +async function getReferenceLinkTitle(href: string) { const {noteId, viewScope} = parseNavigationStateFromUrl(href); if (!noteId) { return "[missing note]"; @@ -336,7 +373,7 @@ async function getReferenceLinkTitle(href) { } } -function getReferenceLinkTitleSync(href) { +function getReferenceLinkTitleSync(href: string) { const {noteId, viewScope} = parseNavigationStateFromUrl(href); if (!noteId) { return "[missing note]"; @@ -360,7 +397,11 @@ function getReferenceLinkTitleSync(href) { } } +// 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 $(document).on('contextmenu', 'a', linkContextMenu); $(document).on('dblclick', "a", e => { diff --git a/src/public/app/services/utils.ts b/src/public/app/services/utils.ts index 47bc2c1e9..2a65e63c9 100644 --- a/src/public/app/services/utils.ts +++ b/src/public/app/services/utils.ts @@ -99,7 +99,7 @@ function isMac() { return navigator.platform.indexOf('Mac') > -1; } -function isCtrlKey(evt: KeyboardEvent) { +function isCtrlKey(evt: KeyboardEvent | MouseEvent) { return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey); }