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 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);
}
});

View File

@ -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,

View File

@ -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);
}

View File

@ -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 = $("<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
};

View File

@ -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")

View File

@ -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, "");

View File

@ -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));
}

View File

@ -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;

View File

@ -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
};

View File

@ -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')

View File

@ -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'
});
}

View File

@ -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)) {

View File

@ -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);

View File

@ -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) {

View File

@ -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' ],

View File

@ -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');