mirror of
https://github.com/zadam/trilium.git
synced 2026-05-06 15:45:47 +02:00
feat(text): basic insert link with anchors
This commit is contained in:
@@ -60,6 +60,8 @@ export interface ViewScope {
|
||||
*/
|
||||
tocPreviousVisible?: boolean;
|
||||
tocCollapsedHeadings?: Set<string>;
|
||||
/** When set, scrolls to a bookmark anchor within the note after navigation. */
|
||||
bookmark?: string;
|
||||
}
|
||||
|
||||
interface CreateLinkOptions {
|
||||
@@ -244,7 +246,7 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
|
||||
hoistedNoteId = value;
|
||||
} else if (name === "searchString") {
|
||||
searchString = value; // supports triggering search from URL, e.g. #?searchString=blabla
|
||||
} else if (["viewMode", "attachmentId"].includes(name)) {
|
||||
} else if (["viewMode", "attachmentId", "bookmark"].includes(name)) {
|
||||
(viewScope as any)[name] = value;
|
||||
} else if (name === "popup") {
|
||||
openInPopup = true;
|
||||
|
||||
@@ -41,6 +41,8 @@
|
||||
"link_title_mirrors": "link title mirrors the note's current title",
|
||||
"link_title_arbitrary": "link title can be changed arbitrarily",
|
||||
"link_title": "Link title",
|
||||
"bookmark": "Bookmark (optional)",
|
||||
"bookmark_none": "None (link to note)",
|
||||
"button_add_link": "Add link"
|
||||
},
|
||||
"branch_prefix": {
|
||||
@@ -861,7 +863,7 @@
|
||||
"none": "none"
|
||||
},
|
||||
"auto_link_attribute_list": {
|
||||
"title": "System Links"
|
||||
"title": "System Attributes"
|
||||
},
|
||||
"note_info_widget": {
|
||||
"note_id": "Note ID",
|
||||
|
||||
@@ -5,6 +5,7 @@ import FormRadioGroup from "../react/FormRadioGroup";
|
||||
import NoteAutocomplete from "../react/NoteAutocomplete";
|
||||
import { useRef, useState, useEffect } from "preact/hooks";
|
||||
import tree from "../../services/tree";
|
||||
import froca from "../../services/froca";
|
||||
import note_autocomplete, { Suggestion } from "../../services/note_autocomplete";
|
||||
import { logError } from "../../services/ws";
|
||||
import FormGroup from "../react/FormGroup.js";
|
||||
@@ -24,6 +25,8 @@ export default function AddLinkDialog() {
|
||||
const [ linkTitle, setLinkTitle ] = useState("");
|
||||
const [ linkType, setLinkType ] = useState<LinkType>();
|
||||
const [ suggestion, setSuggestion ] = useState<Suggestion | null>(null);
|
||||
const [ bookmarks, setBookmarks ] = useState<string[]>([]);
|
||||
const [ selectedBookmark, setSelectedBookmark ] = useState("");
|
||||
const [ shown, setShown ] = useState(false);
|
||||
const hasSubmittedRef = useRef(false);
|
||||
|
||||
@@ -61,6 +64,11 @@ export default function AddLinkDialog() {
|
||||
const noteId = tree.getNoteIdFromUrl(suggestion.notePath);
|
||||
if (noteId) {
|
||||
setDefaultLinkTitle(noteId);
|
||||
froca.getNote(noteId).then((note) => {
|
||||
const bkms = note?.getLabels("internalBookmark").map((l) => l.value) ?? [];
|
||||
setBookmarks(bkms);
|
||||
setSelectedBookmark("");
|
||||
});
|
||||
}
|
||||
resetExternalLink();
|
||||
}
|
||||
@@ -114,8 +122,11 @@ export default function AddLinkDialog() {
|
||||
hasSubmittedRef.current = false;
|
||||
|
||||
if (suggestion.notePath) {
|
||||
// Handle note link
|
||||
opts.addLink(suggestion.notePath, linkType === "reference-link" ? null : linkTitle);
|
||||
// Handle note link, optionally with a bookmark anchor
|
||||
const path = selectedBookmark
|
||||
? `${suggestion.notePath}?bookmark=${encodeURIComponent(selectedBookmark)}`
|
||||
: suggestion.notePath;
|
||||
opts.addLink(path, linkType === "reference-link" ? null : linkTitle);
|
||||
} else if (suggestion.externalLink) {
|
||||
// Handle external link
|
||||
opts.addLink(suggestion.externalLink, linkTitle, true);
|
||||
@@ -123,6 +134,8 @@ export default function AddLinkDialog() {
|
||||
}
|
||||
|
||||
setSuggestion(null);
|
||||
setBookmarks([]);
|
||||
setSelectedBookmark("");
|
||||
setShown(false);
|
||||
}}
|
||||
show={shown}
|
||||
@@ -138,6 +151,21 @@ export default function AddLinkDialog() {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{bookmarks.length > 0 && (
|
||||
<FormGroup label={t("add_link.bookmark")} name="bookmark">
|
||||
<select
|
||||
className="form-select"
|
||||
value={selectedBookmark}
|
||||
onChange={(e) => setSelectedBookmark((e.target as HTMLSelectElement).value)}
|
||||
>
|
||||
<option value="">{t("add_link.bookmark_none")}</option>
|
||||
{bookmarks.map((bk) => (
|
||||
<option key={bk} value={bk}>{bk}</option>
|
||||
))}
|
||||
</select>
|
||||
</FormGroup>
|
||||
)}
|
||||
|
||||
{!opts?.hasSelection && (
|
||||
<div className="add-link-title-settings">
|
||||
{(linkType !== "external-link") && (
|
||||
|
||||
@@ -263,6 +263,14 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
|
||||
// We are not using CKEditor's built-in watch dog content, instead we are using the data we store regularly in the spaced update (see `dataSaved`).
|
||||
editor.setData(contentRef.current);
|
||||
parentComponent?.triggerEvent("textEditorRefreshed", { ntxId, editor });
|
||||
|
||||
// Scroll to bookmark anchor if navigated with ?bookmark=...
|
||||
const viewScope = noteContext?.viewScope;
|
||||
if (viewScope?.bookmark) {
|
||||
const el = editor.editing.view.getDomRoot()?.querySelector(`[id="${CSS.escape(viewScope.bookmark)}"]`);
|
||||
el?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
viewScope.bookmark = undefined;
|
||||
}
|
||||
}}
|
||||
/>}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import "@triliumnext/ckeditor5";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { Ref } from "preact";
|
||||
import { useEffect, useLayoutEffect, useMemo } from "preact/hooks";
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef as usePreactRef } from "preact/hooks";
|
||||
|
||||
import appContext from "../../../components/app_context";
|
||||
import FNote from "../../../entities/fnote";
|
||||
@@ -24,6 +24,17 @@ import { loadIncludedNote, refreshIncludedNote, setupImageOpening } from "./util
|
||||
export default function ReadOnlyText({ note, noteContext, ntxId }: TypeWidgetProps) {
|
||||
const blob = useNoteBlob(note);
|
||||
const { isRtl } = useNoteLanguage(note);
|
||||
const readOnlyContentRef = usePreactRef<HTMLDivElement>(null);
|
||||
|
||||
// Scroll to bookmark anchor if navigated with ?bookmark=...
|
||||
useEffect(() => {
|
||||
const viewScope = noteContext?.viewScope;
|
||||
if (!viewScope?.bookmark || !readOnlyContentRef.current) return;
|
||||
|
||||
const el = readOnlyContentRef.current.querySelector(`[id="${CSS.escape(viewScope.bookmark)}"]`);
|
||||
el?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
viewScope.bookmark = undefined;
|
||||
}, [blob]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -31,6 +42,7 @@ export default function ReadOnlyText({ note, noteContext, ntxId }: TypeWidgetPro
|
||||
html={blob?.content ?? ""}
|
||||
ntxId={ntxId}
|
||||
dir={isRtl ? "rtl" : "ltr"}
|
||||
contentRef={readOnlyContentRef}
|
||||
/>
|
||||
|
||||
<TouchBar>
|
||||
|
||||
@@ -119,7 +119,15 @@ class BAttribute extends AbstractBeccaEntity<BAttribute> {
|
||||
}
|
||||
|
||||
isAutoLink() {
|
||||
return this.type === "relation" && ["internalLink", "imageLink", "relationMapLink", "includeNoteLink"].includes(this.name);
|
||||
if (this.type === "relation") {
|
||||
return ["internalLink", "imageLink", "relationMapLink", "includeNoteLink"].includes(this.name);
|
||||
}
|
||||
|
||||
if (this.type === "label") {
|
||||
return this.name === "internalBookmark";
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
get note() {
|
||||
|
||||
@@ -91,6 +91,7 @@ export default [
|
||||
{ type: "label", name: "printPageSize" },
|
||||
{ type: "label", name: "printScale" },
|
||||
{ type: "label", name: "printMargins" },
|
||||
{ type: "label", name: "internalBookmark" },
|
||||
|
||||
// relation names
|
||||
{ type: "relation", name: "internalLink" },
|
||||
|
||||
Reference in New Issue
Block a user