diff --git a/src/public/app/components/app_context.js b/src/public/app/components/app_context.js index 5cf1b35dd..4433322fa 100644 --- a/src/public/app/components/app_context.js +++ b/src/public/app/components/app_context.js @@ -9,6 +9,7 @@ import TabManager from "./tab_manager.js"; import treeService from "../services/tree.js"; import Component from "./component.js"; import keyboardActionsService from "../services/keyboard_actions.js"; +import linkService from "../services/link.js"; import MobileScreenSwitcherExecutor from "./mobile_screen_switcher.js"; import MainTreeExecutors from "./main_tree_executors.js"; import toast from "../services/toast.js"; @@ -158,14 +159,9 @@ $(window).on('beforeunload', () => { }); $(window).on('hashchange', function() { - if (treeService.isNotePathInAddress()) { - const {notePath, ntxId, viewScope} = treeService.parseNavigationStateFromAddress(); - - if (!notePath && !ntxId) { - console.log(`Invalid hash value "${document.location.hash}", ignoring.`); - return; - } + const {notePath, ntxId, viewScope} = linkService.parseNavigationStateFromUrl(window.location.href); + if (notePath || ntxId) { appContext.tabManager.switchToNoteContext(ntxId, notePath, viewScope); } }); diff --git a/src/public/app/components/tab_manager.js b/src/public/app/components/tab_manager.js index 60e583548..8f2a16942 100644 --- a/src/public/app/components/tab_manager.js +++ b/src/public/app/components/tab_manager.js @@ -52,14 +52,13 @@ export default class TabManager extends Component { async loadTabs() { try { - const noteContextsToOpen = appContext.isMainWindow - ? (options.getJson('openNoteContexts') || []) - : []; + const noteContextsToOpen = (appContext.isMainWindow && options.getJson('openNoteContexts')) || []; // preload all notes at once await froca.getNotes([ - ...noteContextsToOpen.map(tab => treeService.getNoteIdFromNotePath(tab.notePath)), - ...noteContextsToOpen.map(tab => tab.hoistedNoteId), + ...noteContextsToOpen.flatMap(tab => + [ treeService.getNoteIdFromNotePath(tab.notePath), tab.hoistedNoteId] + ), ], true); const filteredNoteContexts = noteContextsToOpen.filter(openTab => { @@ -81,7 +80,7 @@ export default class TabManager extends Component { }); // resolve before opened tabs can change this - const parsedFromUrl = treeService.parseNavigationStateFromAddress(); + const parsedFromUrl = linkService.parseNavigationStateFromUrl(window.location.href); if (filteredNoteContexts.length === 0) { parsedFromUrl.ntxId = parsedFromUrl.ntxId || NoteContext.generateNtxId(); // generate already here, so that we later know which one to activate @@ -109,8 +108,8 @@ export default class TabManager extends Component { } }); - // if there's notePath in the URL, make sure it's open and active - // (useful, for e.g. opening clipped notes from clipper or opening link in an extra window) + // if there's a notePath in the URL, make sure it's open and active + // (useful, for e.g., opening clipped notes from clipper or opening link in an extra window) if (parsedFromUrl.notePath) { await appContext.tabManager.switchToNoteContext( parsedFromUrl.ntxId, diff --git a/src/public/app/services/attribute_renderer.js b/src/public/app/services/attribute_renderer.js index fcbd7520b..e46671ffe 100644 --- a/src/public/app/services/attribute_renderer.js +++ b/src/public/app/services/attribute_renderer.js @@ -56,8 +56,7 @@ async function createNoteLink(noteId) { return $("", { href: `#root/${noteId}`, - class: 'reference-link', - 'data-note-path': noteId + class: 'reference-link' }) .text(note.title); } diff --git a/src/public/app/services/link.js b/src/public/app/services/link.js index 503cad97c..e077bfd1f 100644 --- a/src/public/app/services/link.js +++ b/src/public/app/services/link.js @@ -19,21 +19,17 @@ async function createNoteLink(notePath, options = {}) { if (!notePath.startsWith("root")) { // all note paths should start with "root/" (except for "root" itself) - // used e.g., to find internal links + // used, e.g., to find internal links notePath = `root/${notePath}`; } - let noteTitle = options.title; 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 {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(notePath); - - if (!noteTitle) { - noteTitle = await treeService.getNoteTitle(noteId, parentNoteId); - } + const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromNotePath(notePath); + const noteTitle = options.title || await treeService.getNoteTitle(noteId, parentNoteId); const $container = $(""); @@ -45,11 +41,15 @@ async function createNoteLink(notePath, options = {}) { .append(" "); } + const hash = calculateHash({ + notePath, + viewScope: options.viewScope + }); + const $noteLink = $("", { - href: `#${notePath}`, + href: hash, text: noteTitle - }).attr('data-action', 'note') - .attr('data-note-path', notePath); + }); if (!showTooltip) { $noteLink.addClass("no-tooltip-preview"); @@ -78,27 +78,6 @@ async function createNoteLink(notePath, options = {}) { return $container; } -function parseNotePathAndScope($link) { - let notePath = $link.attr("data-note-path"); - - if (!notePath) { - const url = $link.attr('href'); - - notePath = url ? getNotePathFromUrl(url) : null; - } - - const viewScope = { - viewMode: $link.attr('data-view-mode') || 'default', - attachmentId: $link.attr('data-attachment-id'), - }; - - return { - notePath, - noteId: treeService.getNoteIdFromNotePath(notePath), - viewScope - }; -} - function calculateHash({notePath, ntxId, hoistedNoteId, viewScope = {}}) { notePath = notePath || ""; const params = [ @@ -128,9 +107,50 @@ function calculateHash({notePath, ntxId, hoistedNoteId, viewScope = {}}) { return hash; } +function parseNavigationStateFromUrl(url) { + const hashIdx = url?.indexOf('#'); + if (hashIdx === -1) { + return {}; + } + + const hash = url?.substr(hashIdx + 1); // strip also the initial '#' + const [notePath, paramString] = hash.split("?"); + const viewScope = { + viewMode: 'default' + }; + let ntxId = null; + let hoistedNoteId = null; + + 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 (['viewMode', 'attachmentId'].includes(name)) { + viewScope[name] = value; + } else { + console.warn(`Unrecognized hash parameter '${name}'.`); + } + } + } + + return { + notePath, + noteId: treeService.getNoteIdFromNotePath(notePath), + ntxId, + hoistedNoteId, + viewScope + }; +} + function goToLink(evt) { const $link = $(evt.target).closest("a,.block-link"); - const hrefLink = $link.attr('href'); + const hrefLink = $link.attr('href') || $link.attr('data-href'); if (hrefLink?.startsWith("data:")) { return true; @@ -139,7 +159,7 @@ function goToLink(evt) { evt.preventDefault(); evt.stopPropagation(); - const { notePath, viewScope } = parseNotePathAndScope($link); + const { notePath, viewScope } = parseNavigationStateFromUrl(hrefLink); const ctrlKey = utils.isCtrlKey(evt); const isLeftClick = evt.which === 1; @@ -186,8 +206,9 @@ function goToLink(evt) { function linkContextMenu(e) { const $link = $(e.target).closest("a"); + const url = $link.attr("href") || $link.attr("data-href"); - const { notePath, viewScope } = parseNotePathAndScope($link); + const { notePath, viewScope } = parseNavigationStateFromUrl(url); if (!notePath) { return; @@ -252,6 +273,6 @@ export default { createNoteLink, goToLink, loadReferenceLinkTitle, - parseNotePathAndScope, - calculateHash + calculateHash, + parseNavigationStateFromUrl }; diff --git a/src/public/app/services/note_autocomplete.js b/src/public/app/services/note_autocomplete.js index f7b51038f..3eda517ef 100644 --- a/src/public/app/services/note_autocomplete.js +++ b/src/public/app/services/note_autocomplete.js @@ -114,8 +114,7 @@ function initNoteAutocomplete($el, options) { .prop("title", "Show recent notes"); const $goToSelectedNoteButton = $("") - .addClass("input-group-text go-to-selected-note-button bx bx-arrow-to-right") - .attr("data-action", "note"); + .addClass("input-group-text go-to-selected-note-button bx bx-arrow-to-right"); const $sideButtons = $("
") .addClass("input-group-append") diff --git a/src/public/app/services/note_content_renderer.js b/src/public/app/services/note_content_renderer.js index b86048f58..41639b7af 100644 --- a/src/public/app/services/note_content_renderer.js +++ b/src/public/app/services/note_content_renderer.js @@ -54,9 +54,9 @@ async function getRenderedContent(note, options = {}) { } } else if (type === 'code') { - const fullNote = await server.get(`notes/${note.noteId}`); + const blob = await note.getBlob({ preview: options.trim }); - $renderedContent.append($("
").text(trim(fullNote.content, options.trim)));
+        $renderedContent.append($("
").text(trim(blob.content, options.trim)));
     }
     else if (type === 'image') {
         const sanitizedTitle = note.title.replace(/[^a-z0-9-.]/gi, "");
diff --git a/src/public/app/services/note_list_renderer.js b/src/public/app/services/note_list_renderer.js
index b22fdc827..60040698c 100644
--- a/src/public/app/services/note_list_renderer.js
+++ b/src/public/app/services/note_list_renderer.js
@@ -268,7 +268,7 @@ class NoteListRenderer {
 
         const {$renderedAttributes} = await attributeRenderer.renderNormalAttributes(note);
         const notePath = this.parentNote.type === 'search'
-            ? note.noteId // for search note parent we want to display non-search path
+            ? note.noteId // for search note parent, we want to display a non-search path
             : `${this.parentNote.noteId}/${note.noteId}`;
 
         const $card = $('
') @@ -288,7 +288,7 @@ class NoteListRenderer { if (this.viewType === 'grid') { $card .addClass("block-link") - .attr("data-note-path", notePath) + .attr("data-href", `#${notePath}`) .on('click', e => linkService.goToLink(e)); } diff --git a/src/public/app/services/note_tooltip.js b/src/public/app/services/note_tooltip.js index 70f4a1c2e..bb7e9c897 100644 --- a/src/public/app/services/note_tooltip.js +++ b/src/public/app/services/note_tooltip.js @@ -32,7 +32,8 @@ async function mouseEnterHandler() { return; } - const { notePath, noteId, viewScope } = linkService.parseNotePathAndScope($link); + const url = $link.attr("href") || $link.attr("data-href"); + const { notePath, noteId, viewScope } = linkService.parseNavigationStateFromUrl(url); if (!notePath || viewScope.viewMode !== 'default') { return; diff --git a/src/public/app/services/tree.js b/src/public/app/services/tree.js index 82436fd23..47938aa88 100644 --- a/src/public/app/services/tree.js +++ b/src/public/app/services/tree.js @@ -279,50 +279,6 @@ async function getNoteTitleWithPathAsSuffix(notePath) { return $titleWithPath; } -function parseNavigationStateFromAddress() { - const str = document.location.hash?.substr(1) || ""; // strip initial # - - const [notePath, paramString] = str.split("?"); - const viewScope = { - viewMode: 'default' - }; - let ntxId = null; - let hoistedNoteId = null; - - 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 (['viewMode', 'attachmentId'].includes(name)) { - viewScope[name] = value; - } else { - console.warn(`Unrecognized hash parameter '${name}'.`); - } - } - } - - return { - notePath, - ntxId, - hoistedNoteId, - viewScope - }; -} - -function isNotePathInAddress() { - const {notePath, ntxId} = parseNavigationStateFromAddress(); - - return notePath.startsWith("root") - // empty string is for empty/uninitialized tab - || (notePath === '' && !!ntxId); -} - function isNotePathInHiddenSubtree(notePath) { return notePath?.includes("root/_hidden"); } @@ -338,7 +294,5 @@ export default { getNoteTitle, getNotePathTitle, getNoteTitleWithPathAsSuffix, - parseNavigationStateFromAddress, - isNotePathInAddress, isNotePathInHiddenSubtree }; diff --git a/src/public/app/widgets/attachment_detail.js b/src/public/app/widgets/attachment_detail.js index 8f4b9e43d..f149c10ad 100644 --- a/src/public/app/widgets/attachment_detail.js +++ b/src/public/app/widgets/attachment_detail.js @@ -4,6 +4,7 @@ import BasicWidget from "./basic_widget.js"; import server from "../services/server.js"; import options from "../services/options.js"; import imageService from "../services/image.js"; +import linkService from "../services/link.js"; const TPL = `
@@ -15,6 +16,7 @@ const TPL = ` .attachment-title-line { display: flex; align-items: baseline; + gap: 1em; } .attachment-details { @@ -54,10 +56,10 @@ const TPL = `
+

-
@@ -84,7 +86,7 @@ export default class AttachmentDetailWidget extends BasicWidget { super.doRender(); } - refresh() { + async refresh() { this.$widget.find('.attachment-detail-wrapper') .empty() .append( @@ -97,11 +99,13 @@ export default class AttachmentDetailWidget extends BasicWidget { if (!this.isFullDetail) { this.$wrapper.find('.attachment-title').append( - $('
') - .attr("data-note-path", this.attachment.parentId) - .attr("data-view-mode", "attachments") - .attr("data-attachment-id", this.attachment.attachmentId) - .text(this.attachment.title) + await linkService.createNoteLink(this.attachment.parentId, { + title: this.attachment.title, + viewScope: { + viewMode: 'attachments', + attachmentId: this.attachment.attachmentId + } + }) ); } else { this.$wrapper.find('.attachment-title') diff --git a/src/public/app/widgets/attribute_widgets/attribute_detail.js b/src/public/app/widgets/attribute_widgets/attribute_detail.js index dba64da70..eaf183c6e 100644 --- a/src/public/app/widgets/attribute_widgets/attribute_detail.js +++ b/src/public/app/widgets/attribute_widgets/attribute_detail.js @@ -701,9 +701,8 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { createNoteLink(noteId) { return $("", { - href: `#${noteId}`, - class: 'reference-link', - 'data-note-path': noteId + href: `#root/${noteId}`, + class: 'reference-link' }); } diff --git a/src/public/app/widgets/buttons/calendar.js b/src/public/app/widgets/buttons/calendar.js index f58b973d6..ec48491b7 100644 --- a/src/public/app/widgets/buttons/calendar.js +++ b/src/public/app/widgets/buttons/calendar.js @@ -105,7 +105,7 @@ export default class CalendarWidget extends RightDropdownButtonWidget { if (dateNoteId) { $newDay.addClass('calendar-date-exists'); - $newDay.attr("data-note-path", dateNoteId); + $newDay.attr("href", `#root/dateNoteId`); } if (this.isEqual(this.date, this.activeDate)) { diff --git a/src/public/app/widgets/buttons/history_navigation.js b/src/public/app/widgets/buttons/history_navigation.js index 64db2b8ae..8347c0db6 100644 --- a/src/public/app/widgets/buttons/history_navigation.js +++ b/src/public/app/widgets/buttons/history_navigation.js @@ -55,7 +55,7 @@ export default class HistoryNavigationButton extends ButtonFromNoteWidget { for (const idx in this.webContents.history) { const url = this.webContents.history[idx]; const [_, notePathWithTab] = url.split('#'); - // broken: use treeService.parseNavigationStateFromAddress(); + // broken: use linkService.parseNavigationStateFromUrl(); const [notePath, ntxId] = notePathWithTab.split('-'); const title = await treeService.getNotePathTitle(notePath); diff --git a/src/public/app/widgets/type_widgets/attachment_detail.js b/src/public/app/widgets/type_widgets/attachment_detail.js index b74b2cfa8..0e09e6cda 100644 --- a/src/public/app/widgets/type_widgets/attachment_detail.js +++ b/src/public/app/widgets/type_widgets/attachment_detail.js @@ -1,6 +1,7 @@ import TypeWidget from "./type_widget.js"; import server from "../../services/server.js"; import AttachmentDetailWidget from "../attachment_detail.js"; +import linkService from "../../services/link.js"; const TPL = `
@@ -10,6 +11,8 @@ const TPL = ` } + +
`; @@ -29,6 +32,8 @@ export default class AttachmentDetailTypeWidget extends TypeWidget { this.$wrapper.empty(); this.children = []; + linkService.createNoteLink(this.noteId, {}); + const attachment = await server.get(`attachments/${this.attachmentId}/?includeContent=true`); if (!attachment) { diff --git a/src/services/html_sanitizer.js b/src/services/html_sanitizer.js index 74f7aab15..db51349b5 100644 --- a/src/services/html_sanitizer.js +++ b/src/services/html_sanitizer.js @@ -33,7 +33,7 @@ function sanitize(dirtyHtml) { 'en-media' // for ENEX import ], allowedAttributes: { - 'a': [ 'href', 'class', 'data-note-path' ], + 'a': [ 'href', 'class' ], 'img': [ 'src' ], 'section': [ 'class', 'data-note-id' ], 'figure': [ 'class' ], diff --git a/src/services/import/zip.js b/src/services/import/zip.js index 1d61aa8de..802b4694f 100644 --- a/src/services/import/zip.js +++ b/src/services/import/zip.js @@ -376,20 +376,6 @@ async function importZip(taskContext, fileBuffer, importRootNote) { return `href="#root/${target.noteId}"`; }); - content = content.replace(/data-note-path="([^"]*)"/g, (match, notePath) => { - const noteId = notePath.split("/").pop(); - - let targetNoteId; - - if (noteId === 'root' || noteId.startsWith("_")) { // named noteIds stay identical across instances - targetNoteId = noteId; - } else { - targetNoteId = noteIdMap[noteId]; - } - - return `data-note-path="root/${targetNoteId}"`; - }); - if (noteMeta) { const includeNoteLinks = (noteMeta.attributes || []) .filter(attr => attr.type === 'relation' && attr.name === 'includeNoteLink');