mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	getting rid of attributes like data-note-path in favor of the whole nav state in URLs
This commit is contained in:
		| @@ -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); | ||||
|     } | ||||
| }); | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -56,8 +56,7 @@ async function createNoteLink(noteId) { | ||||
|  | ||||
|     return $("<a>", { | ||||
|         href: `#root/${noteId}`, | ||||
|         class: 'reference-link', | ||||
|         'data-note-path': noteId | ||||
|         class: 'reference-link' | ||||
|     }) | ||||
|         .text(note.title); | ||||
| } | ||||
|   | ||||
| @@ -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 noteTitle = options.title || await treeService.getNoteTitle(noteId, parentNoteId); | ||||
|  | ||||
|     const $container = $("<span>"); | ||||
|  | ||||
| @@ -45,11 +41,15 @@ async function createNoteLink(notePath, options = {}) { | ||||
|             .append(" "); | ||||
|     } | ||||
|  | ||||
|     const hash = calculateHash({ | ||||
|         notePath, | ||||
|         viewScope: options.viewScope | ||||
|     }); | ||||
|  | ||||
|     const $noteLink = $("<a>", { | ||||
|         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 | ||||
| }; | ||||
|   | ||||
| @@ -114,8 +114,7 @@ function initNoteAutocomplete($el, options) { | ||||
|             .prop("title", "Show recent notes"); | ||||
|  | ||||
|     const $goToSelectedNoteButton = $("<a>") | ||||
|         .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 = $("<div>") | ||||
|         .addClass("input-group-append") | ||||
|   | ||||
| @@ -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($("<pre>").text(trim(fullNote.content, options.trim))); | ||||
|         $renderedContent.append($("<pre>").text(trim(blob.content, options.trim))); | ||||
|     } | ||||
|     else if (type === 'image') { | ||||
|         const sanitizedTitle = note.title.replace(/[^a-z0-9-.]/gi, ""); | ||||
|   | ||||
| @@ -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 = $('<div class="note-book-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)); | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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 | ||||
| }; | ||||
|   | ||||
| @@ -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 = ` | ||||
| <div class="attachment-detail"> | ||||
| @@ -15,6 +16,7 @@ const TPL = ` | ||||
|         .attachment-title-line { | ||||
|             display: flex; | ||||
|             align-items: baseline; | ||||
|             gap: 1em; | ||||
|         } | ||||
|          | ||||
|         .attachment-details { | ||||
| @@ -54,10 +56,10 @@ const TPL = ` | ||||
|  | ||||
|     <div class="attachment-detail-wrapper"> | ||||
|         <div class="attachment-title-line"> | ||||
|             <div class="attachment-actions-container"></div> | ||||
|             <h4 class="attachment-title"></h4>                 | ||||
|             <div class="attachment-details"></div> | ||||
|             <div style="flex: 1 1;"></div> | ||||
|             <div class="attachment-actions-container"></div> | ||||
|         </div> | ||||
|          | ||||
|         <div class="attachment-deletion-warning alert alert-info"></div> | ||||
| @@ -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( | ||||
|                 $('<a href="javascript:">') | ||||
|                     .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') | ||||
|   | ||||
| @@ -701,9 +701,8 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { | ||||
|  | ||||
|     createNoteLink(noteId) { | ||||
|         return $("<a>", { | ||||
|             href: `#${noteId}`, | ||||
|             class: 'reference-link', | ||||
|             'data-note-path': noteId | ||||
|             href: `#root/${noteId}`, | ||||
|             class: 'reference-link' | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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)) { | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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 = ` | ||||
| <div class="attachment-detail note-detail-printable"> | ||||
| @@ -10,6 +11,8 @@ const TPL = ` | ||||
|         } | ||||
|     </style> | ||||
|  | ||||
|     <div class="links-wrapper"></div> | ||||
|  | ||||
|     <div class="attachment-wrapper"></div> | ||||
| </div>`; | ||||
|  | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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' ], | ||||
|   | ||||
| @@ -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'); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user