getting rid of attributes like data-note-path in favor of the whole nav state in URLs

This commit is contained in:
zadam 2023-05-07 21:18:21 +02:00
parent 291f0e79d9
commit f85209a72f
16 changed files with 96 additions and 133 deletions

View File

@ -9,6 +9,7 @@ import TabManager from "./tab_manager.js";
import treeService from "../services/tree.js"; import treeService from "../services/tree.js";
import Component from "./component.js"; import Component from "./component.js";
import keyboardActionsService from "../services/keyboard_actions.js"; import keyboardActionsService from "../services/keyboard_actions.js";
import linkService from "../services/link.js";
import MobileScreenSwitcherExecutor from "./mobile_screen_switcher.js"; import MobileScreenSwitcherExecutor from "./mobile_screen_switcher.js";
import MainTreeExecutors from "./main_tree_executors.js"; import MainTreeExecutors from "./main_tree_executors.js";
import toast from "../services/toast.js"; import toast from "../services/toast.js";
@ -158,14 +159,9 @@ $(window).on('beforeunload', () => {
}); });
$(window).on('hashchange', function() { $(window).on('hashchange', function() {
if (treeService.isNotePathInAddress()) { const {notePath, ntxId, viewScope} = linkService.parseNavigationStateFromUrl(window.location.href);
const {notePath, ntxId, viewScope} = treeService.parseNavigationStateFromAddress();
if (!notePath && !ntxId) {
console.log(`Invalid hash value "${document.location.hash}", ignoring.`);
return;
}
if (notePath || ntxId) {
appContext.tabManager.switchToNoteContext(ntxId, notePath, viewScope); appContext.tabManager.switchToNoteContext(ntxId, notePath, viewScope);
} }
}); });

View File

@ -52,14 +52,13 @@ export default class TabManager extends Component {
async loadTabs() { async loadTabs() {
try { try {
const noteContextsToOpen = appContext.isMainWindow const noteContextsToOpen = (appContext.isMainWindow && options.getJson('openNoteContexts')) || [];
? (options.getJson('openNoteContexts') || [])
: [];
// preload all notes at once // preload all notes at once
await froca.getNotes([ await froca.getNotes([
...noteContextsToOpen.map(tab => treeService.getNoteIdFromNotePath(tab.notePath)), ...noteContextsToOpen.flatMap(tab =>
...noteContextsToOpen.map(tab => tab.hoistedNoteId), [ treeService.getNoteIdFromNotePath(tab.notePath), tab.hoistedNoteId]
),
], true); ], true);
const filteredNoteContexts = noteContextsToOpen.filter(openTab => { const filteredNoteContexts = noteContextsToOpen.filter(openTab => {
@ -81,7 +80,7 @@ export default class TabManager extends Component {
}); });
// resolve before opened tabs can change this // resolve before opened tabs can change this
const parsedFromUrl = treeService.parseNavigationStateFromAddress(); const parsedFromUrl = linkService.parseNavigationStateFromUrl(window.location.href);
if (filteredNoteContexts.length === 0) { if (filteredNoteContexts.length === 0) {
parsedFromUrl.ntxId = parsedFromUrl.ntxId || NoteContext.generateNtxId(); // generate already here, so that we later know which one to activate 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 // 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) // (useful, for e.g., opening clipped notes from clipper or opening link in an extra window)
if (parsedFromUrl.notePath) { if (parsedFromUrl.notePath) {
await appContext.tabManager.switchToNoteContext( await appContext.tabManager.switchToNoteContext(
parsedFromUrl.ntxId, parsedFromUrl.ntxId,

View File

@ -56,8 +56,7 @@ async function createNoteLink(noteId) {
return $("<a>", { return $("<a>", {
href: `#root/${noteId}`, href: `#root/${noteId}`,
class: 'reference-link', class: 'reference-link'
'data-note-path': noteId
}) })
.text(note.title); .text(note.title);
} }

View File

@ -19,21 +19,17 @@ async function createNoteLink(notePath, options = {}) {
if (!notePath.startsWith("root")) { if (!notePath.startsWith("root")) {
// all note paths should start with "root/" (except for "root" itself) // 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}`; notePath = `root/${notePath}`;
} }
let noteTitle = options.title;
const showTooltip = options.showTooltip === undefined ? true : options.showTooltip; const showTooltip = options.showTooltip === undefined ? true : options.showTooltip;
const showNotePath = options.showNotePath === undefined ? false : options.showNotePath; const showNotePath = options.showNotePath === undefined ? false : options.showNotePath;
const showNoteIcon = options.showNoteIcon === undefined ? false : options.showNoteIcon; const showNoteIcon = options.showNoteIcon === undefined ? false : options.showNoteIcon;
const referenceLink = options.referenceLink === undefined ? false : options.referenceLink; const referenceLink = options.referenceLink === undefined ? false : options.referenceLink;
const {noteId, parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(notePath); const { noteId, parentNoteId } = treeService.getNoteIdAndParentIdFromNotePath(notePath);
const noteTitle = options.title || await treeService.getNoteTitle(noteId, parentNoteId);
if (!noteTitle) {
noteTitle = await treeService.getNoteTitle(noteId, parentNoteId);
}
const $container = $("<span>"); const $container = $("<span>");
@ -45,11 +41,15 @@ async function createNoteLink(notePath, options = {}) {
.append(" "); .append(" ");
} }
const hash = calculateHash({
notePath,
viewScope: options.viewScope
});
const $noteLink = $("<a>", { const $noteLink = $("<a>", {
href: `#${notePath}`, href: hash,
text: noteTitle text: noteTitle
}).attr('data-action', 'note') });
.attr('data-note-path', notePath);
if (!showTooltip) { if (!showTooltip) {
$noteLink.addClass("no-tooltip-preview"); $noteLink.addClass("no-tooltip-preview");
@ -78,27 +78,6 @@ async function createNoteLink(notePath, options = {}) {
return $container; 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 = {}}) { function calculateHash({notePath, ntxId, hoistedNoteId, viewScope = {}}) {
notePath = notePath || ""; notePath = notePath || "";
const params = [ const params = [
@ -128,9 +107,50 @@ function calculateHash({notePath, ntxId, hoistedNoteId, viewScope = {}}) {
return hash; 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) { function goToLink(evt) {
const $link = $(evt.target).closest("a,.block-link"); 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:")) { if (hrefLink?.startsWith("data:")) {
return true; return true;
@ -139,7 +159,7 @@ function goToLink(evt) {
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation(); evt.stopPropagation();
const { notePath, viewScope } = parseNotePathAndScope($link); const { notePath, viewScope } = parseNavigationStateFromUrl(hrefLink);
const ctrlKey = utils.isCtrlKey(evt); const ctrlKey = utils.isCtrlKey(evt);
const isLeftClick = evt.which === 1; const isLeftClick = evt.which === 1;
@ -186,8 +206,9 @@ function goToLink(evt) {
function linkContextMenu(e) { function linkContextMenu(e) {
const $link = $(e.target).closest("a"); 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) { if (!notePath) {
return; return;
@ -252,6 +273,6 @@ export default {
createNoteLink, createNoteLink,
goToLink, goToLink,
loadReferenceLinkTitle, loadReferenceLinkTitle,
parseNotePathAndScope, calculateHash,
calculateHash parseNavigationStateFromUrl
}; };

View File

@ -114,8 +114,7 @@ function initNoteAutocomplete($el, options) {
.prop("title", "Show recent notes"); .prop("title", "Show recent notes");
const $goToSelectedNoteButton = $("<a>") const $goToSelectedNoteButton = $("<a>")
.addClass("input-group-text go-to-selected-note-button bx bx-arrow-to-right") .addClass("input-group-text go-to-selected-note-button bx bx-arrow-to-right");
.attr("data-action", "note");
const $sideButtons = $("<div>") const $sideButtons = $("<div>")
.addClass("input-group-append") .addClass("input-group-append")

View File

@ -54,9 +54,9 @@ async function getRenderedContent(note, options = {}) {
} }
} }
else if (type === 'code') { 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') { else if (type === 'image') {
const sanitizedTitle = note.title.replace(/[^a-z0-9-.]/gi, ""); const sanitizedTitle = note.title.replace(/[^a-z0-9-.]/gi, "");

View File

@ -268,7 +268,7 @@ class NoteListRenderer {
const {$renderedAttributes} = await attributeRenderer.renderNormalAttributes(note); const {$renderedAttributes} = await attributeRenderer.renderNormalAttributes(note);
const notePath = this.parentNote.type === 'search' 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}`; : `${this.parentNote.noteId}/${note.noteId}`;
const $card = $('<div class="note-book-card">') const $card = $('<div class="note-book-card">')
@ -288,7 +288,7 @@ class NoteListRenderer {
if (this.viewType === 'grid') { if (this.viewType === 'grid') {
$card $card
.addClass("block-link") .addClass("block-link")
.attr("data-note-path", notePath) .attr("data-href", `#${notePath}`)
.on('click', e => linkService.goToLink(e)); .on('click', e => linkService.goToLink(e));
} }

View File

@ -32,7 +32,8 @@ async function mouseEnterHandler() {
return; 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') { if (!notePath || viewScope.viewMode !== 'default') {
return; return;

View File

@ -279,50 +279,6 @@ async function getNoteTitleWithPathAsSuffix(notePath) {
return $titleWithPath; 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) { function isNotePathInHiddenSubtree(notePath) {
return notePath?.includes("root/_hidden"); return notePath?.includes("root/_hidden");
} }
@ -338,7 +294,5 @@ export default {
getNoteTitle, getNoteTitle,
getNotePathTitle, getNotePathTitle,
getNoteTitleWithPathAsSuffix, getNoteTitleWithPathAsSuffix,
parseNavigationStateFromAddress,
isNotePathInAddress,
isNotePathInHiddenSubtree isNotePathInHiddenSubtree
}; };

View File

@ -4,6 +4,7 @@ import BasicWidget from "./basic_widget.js";
import server from "../services/server.js"; import server from "../services/server.js";
import options from "../services/options.js"; import options from "../services/options.js";
import imageService from "../services/image.js"; import imageService from "../services/image.js";
import linkService from "../services/link.js";
const TPL = ` const TPL = `
<div class="attachment-detail"> <div class="attachment-detail">
@ -15,6 +16,7 @@ const TPL = `
.attachment-title-line { .attachment-title-line {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
gap: 1em;
} }
.attachment-details { .attachment-details {
@ -54,10 +56,10 @@ const TPL = `
<div class="attachment-detail-wrapper"> <div class="attachment-detail-wrapper">
<div class="attachment-title-line"> <div class="attachment-title-line">
<div class="attachment-actions-container"></div>
<h4 class="attachment-title"></h4> <h4 class="attachment-title"></h4>
<div class="attachment-details"></div> <div class="attachment-details"></div>
<div style="flex: 1 1;"></div> <div style="flex: 1 1;"></div>
<div class="attachment-actions-container"></div>
</div> </div>
<div class="attachment-deletion-warning alert alert-info"></div> <div class="attachment-deletion-warning alert alert-info"></div>
@ -84,7 +86,7 @@ export default class AttachmentDetailWidget extends BasicWidget {
super.doRender(); super.doRender();
} }
refresh() { async refresh() {
this.$widget.find('.attachment-detail-wrapper') this.$widget.find('.attachment-detail-wrapper')
.empty() .empty()
.append( .append(
@ -97,11 +99,13 @@ export default class AttachmentDetailWidget extends BasicWidget {
if (!this.isFullDetail) { if (!this.isFullDetail) {
this.$wrapper.find('.attachment-title').append( this.$wrapper.find('.attachment-title').append(
$('<a href="javascript:">') await linkService.createNoteLink(this.attachment.parentId, {
.attr("data-note-path", this.attachment.parentId) title: this.attachment.title,
.attr("data-view-mode", "attachments") viewScope: {
.attr("data-attachment-id", this.attachment.attachmentId) viewMode: 'attachments',
.text(this.attachment.title) attachmentId: this.attachment.attachmentId
}
})
); );
} else { } else {
this.$wrapper.find('.attachment-title') this.$wrapper.find('.attachment-title')

View File

@ -701,9 +701,8 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
createNoteLink(noteId) { createNoteLink(noteId) {
return $("<a>", { return $("<a>", {
href: `#${noteId}`, href: `#root/${noteId}`,
class: 'reference-link', class: 'reference-link'
'data-note-path': noteId
}); });
} }

View File

@ -105,7 +105,7 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
if (dateNoteId) { if (dateNoteId) {
$newDay.addClass('calendar-date-exists'); $newDay.addClass('calendar-date-exists');
$newDay.attr("data-note-path", dateNoteId); $newDay.attr("href", `#root/dateNoteId`);
} }
if (this.isEqual(this.date, this.activeDate)) { if (this.isEqual(this.date, this.activeDate)) {

View File

@ -55,7 +55,7 @@ export default class HistoryNavigationButton extends ButtonFromNoteWidget {
for (const idx in this.webContents.history) { for (const idx in this.webContents.history) {
const url = this.webContents.history[idx]; const url = this.webContents.history[idx];
const [_, notePathWithTab] = url.split('#'); const [_, notePathWithTab] = url.split('#');
// broken: use treeService.parseNavigationStateFromAddress(); // broken: use linkService.parseNavigationStateFromUrl();
const [notePath, ntxId] = notePathWithTab.split('-'); const [notePath, ntxId] = notePathWithTab.split('-');
const title = await treeService.getNotePathTitle(notePath); const title = await treeService.getNotePathTitle(notePath);

View File

@ -1,6 +1,7 @@
import TypeWidget from "./type_widget.js"; import TypeWidget from "./type_widget.js";
import server from "../../services/server.js"; import server from "../../services/server.js";
import AttachmentDetailWidget from "../attachment_detail.js"; import AttachmentDetailWidget from "../attachment_detail.js";
import linkService from "../../services/link.js";
const TPL = ` const TPL = `
<div class="attachment-detail note-detail-printable"> <div class="attachment-detail note-detail-printable">
@ -10,6 +11,8 @@ const TPL = `
} }
</style> </style>
<div class="links-wrapper"></div>
<div class="attachment-wrapper"></div> <div class="attachment-wrapper"></div>
</div>`; </div>`;
@ -29,6 +32,8 @@ export default class AttachmentDetailTypeWidget extends TypeWidget {
this.$wrapper.empty(); this.$wrapper.empty();
this.children = []; this.children = [];
linkService.createNoteLink(this.noteId, {});
const attachment = await server.get(`attachments/${this.attachmentId}/?includeContent=true`); const attachment = await server.get(`attachments/${this.attachmentId}/?includeContent=true`);
if (!attachment) { if (!attachment) {

View File

@ -33,7 +33,7 @@ function sanitize(dirtyHtml) {
'en-media' // for ENEX import 'en-media' // for ENEX import
], ],
allowedAttributes: { allowedAttributes: {
'a': [ 'href', 'class', 'data-note-path' ], 'a': [ 'href', 'class' ],
'img': [ 'src' ], 'img': [ 'src' ],
'section': [ 'class', 'data-note-id' ], 'section': [ 'class', 'data-note-id' ],
'figure': [ 'class' ], 'figure': [ 'class' ],

View File

@ -376,20 +376,6 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
return `href="#root/${target.noteId}"`; 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) { if (noteMeta) {
const includeNoteLinks = (noteMeta.attributes || []) const includeNoteLinks = (noteMeta.attributes || [])
.filter(attr => attr.type === 'relation' && attr.name === 'includeNoteLink'); .filter(attr => attr.type === 'relation' && attr.name === 'includeNoteLink');