From 7f81b839550d822d48e23fae3574acc19afd8776 Mon Sep 17 00:00:00 2001
From: Elian Doran
Date: Thu, 20 Nov 2025 20:13:20 +0200
Subject: [PATCH 01/16] chore(print/list): get note titles to render
---
.../src/widgets/collections/NoteList.tsx | 8 ++++++--
.../collections/legacy/ListOrGridView.tsx | 20 +++++++++++++++++++
2 files changed, 26 insertions(+), 2 deletions(-)
diff --git a/apps/client/src/widgets/collections/NoteList.tsx b/apps/client/src/widgets/collections/NoteList.tsx
index 1d5a96810..860886a0f 100644
--- a/apps/client/src/widgets/collections/NoteList.tsx
+++ b/apps/client/src/widgets/collections/NoteList.tsx
@@ -2,7 +2,7 @@ import { allViewTypes, ViewModeMedia, ViewModeProps, ViewTypeOptions } from "./i
import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useTriliumEvent } from "../react/hooks";
import FNote from "../../entities/fnote";
import "./NoteList.css";
-import { ListView, GridView } from "./legacy/ListOrGridView";
+import { ListView, GridView, ListPrintView } from "./legacy/ListOrGridView";
import { useEffect, useRef, useState } from "preact/hooks";
import GeoView from "./geomap";
import ViewModeStorage from "./view_mode_storage";
@@ -103,7 +103,11 @@ export function CustomNoteList({ note, viewType, isEnabled: shouldEnable, notePa
function getComponentByViewType(viewType: ViewTypeOptions, props: ViewModeProps) {
switch (viewType) {
case "list":
- return ;
+ if (props.media !== "print") {
+ return ;
+ } else {
+ return ;
+ }
case "grid":
return ;
case "geoMap":
diff --git a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
index ef37b6685..db5a88593 100644
--- a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
+++ b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
@@ -11,6 +11,7 @@ import tree from "../../../services/tree";
import link from "../../../services/link";
import { t } from "../../../services/i18n";
import attribute_renderer from "../../../services/attribute_renderer";
+import froca from "../../../services/froca";
export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }: ViewModeProps<{}>) {
const [ isExpanded ] = useNoteLabelBoolean(note, "expanded");
@@ -34,6 +35,25 @@ export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
);
}
+export function ListPrintView({ note, noteIds: unfilteredNoteIds, highlightedTokens }: ViewModeProps<{}>) {
+ const noteIds = useFilteredNoteIds(note, unfilteredNoteIds);
+ const [ notes, setNotes ] = useState();
+
+ useEffect(() => {
+ froca.getNotes(noteIds).then(setNotes);
+ }, [noteIds]);
+
+ return (
+
+
+ {notes?.map(childNote => (
+
{childNote.title}
+ ))}
+
+
+ );
+}
+
export function GridView({ note, noteIds: unfilteredNoteIds, highlightedTokens }: ViewModeProps<{}>) {
const noteIds = useFilteredNoteIds(note, unfilteredNoteIds);
const { pageNotes, ...pagination } = usePagination(note, noteIds);
From 73e7fa0f85ede19c2ec8626f7c4bb7a6194b4793 Mon Sep 17 00:00:00 2001
From: Elian Doran
Date: Thu, 20 Nov 2025 20:15:44 +0200
Subject: [PATCH 02/16] chore(print/list): get note content to render
---
.../src/widgets/collections/legacy/ListOrGridView.tsx | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
index db5a88593..55415e7c1 100644
--- a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
+++ b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
@@ -47,7 +47,13 @@ export function ListPrintView({ note, noteIds: unfilteredNoteIds, highlightedTok
{notes?.map(childNote => (
-
{childNote.title}
+ <>
+ {childNote.title}
+
+ >
))}
From c95cb79672e7c1a49c51013bd50bf39aa2738b6d Mon Sep 17 00:00:00 2001
From: Elian Doran
Date: Thu, 20 Nov 2025 20:17:20 +0200
Subject: [PATCH 03/16] chore(print/list): enable print dialog
---
.../src/widgets/collections/legacy/ListOrGridView.tsx | 8 +++++++-
apps/client/src/widgets/ribbon/NoteActions.tsx | 2 +-
2 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
index 55415e7c1..80087f584 100644
--- a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
+++ b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
@@ -35,7 +35,7 @@ export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
);
}
-export function ListPrintView({ note, noteIds: unfilteredNoteIds, highlightedTokens }: ViewModeProps<{}>) {
+export function ListPrintView({ note, noteIds: unfilteredNoteIds, highlightedTokens, onReady }: ViewModeProps<{}>) {
const noteIds = useFilteredNoteIds(note, unfilteredNoteIds);
const [ notes, setNotes ] = useState();
@@ -43,6 +43,12 @@ export function ListPrintView({ note, noteIds: unfilteredNoteIds, highlightedTok
froca.getNotes(noteIds).then(setNotes);
}, [noteIds]);
+ useEffect(() => {
+ if (notes && onReady) {
+ onReady();
+ }
+ }, [ notes, onReady ]);
+
return (
diff --git a/apps/client/src/widgets/ribbon/NoteActions.tsx b/apps/client/src/widgets/ribbon/NoteActions.tsx
index cbd3bf406..d7e344dd8 100644
--- a/apps/client/src/widgets/ribbon/NoteActions.tsx
+++ b/apps/client/src/widgets/ribbon/NoteActions.tsx
@@ -49,7 +49,7 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment();
const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(note.type);
const isInOptionsOrHelp = note?.noteId.startsWith("_options") || note?.noteId.startsWith("_help");
- const isPrintable = ["text", "code"].includes(note.type) || (note.type === "book" && note.getLabelValue("viewType") === "presentation");
+ const isPrintable = ["text", "code"].includes(note.type) || (note.type === "book" && ["presentation", "list"].includes(note.getLabelValue("viewType") ?? ""));
const isElectron = getIsElectron();
const isMac = getIsMac();
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "aiChat"].includes(note.type);
From a59d407f120dfd54e62cb05648162a32d8d8a3d2 Mon Sep 17 00:00:00 2001
From: Elian Doran
Date: Thu, 20 Nov 2025 20:24:19 +0200
Subject: [PATCH 04/16] fix(print/list): note content not shown due to race
condition
---
.../collections/legacy/ListOrGridView.tsx | 39 ++++++++++++-------
1 file changed, 26 insertions(+), 13 deletions(-)
diff --git a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
index 80087f584..811932128 100644
--- a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
+++ b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useRef, useState } from "preact/hooks";
+import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
import FNote from "../../../entities/fnote";
import Icon from "../../react/Icon";
import { ViewModeProps } from "../interface";
@@ -12,6 +12,7 @@ import link from "../../../services/link";
import { t } from "../../../services/i18n";
import attribute_renderer from "../../../services/attribute_renderer";
import froca from "../../../services/froca";
+import { RawHtmlBlock } from "../../react/RawHtml";
export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }: ViewModeProps<{}>) {
const [ isExpanded ] = useNoteLabelBoolean(note, "expanded");
@@ -35,31 +36,43 @@ export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
);
}
+interface NotesWithContent {
+ note: FNote;
+ content: string;
+}
+
export function ListPrintView({ note, noteIds: unfilteredNoteIds, highlightedTokens, onReady }: ViewModeProps<{}>) {
const noteIds = useFilteredNoteIds(note, unfilteredNoteIds);
- const [ notes, setNotes ] = useState();
+ const [ notesWithContent, setNotesWithContent ] = useState();
- useEffect(() => {
- froca.getNotes(noteIds).then(setNotes);
+ useLayoutEffect(() => {
+ froca.getNotes(noteIds).then(async (notes) => {
+ const notesWithContent: NotesWithContent[] = [];
+ for (const note of notes) {
+ const content = await content_renderer.getRenderedContent(note, {
+ trim: false,
+ noChildrenList: true
+ });
+ notesWithContent.push({ note, content: content.$renderedContent[0].innerHTML });
+ }
+ setNotesWithContent(notesWithContent);
+ });
}, [noteIds]);
useEffect(() => {
- if (notes && onReady) {
+ if (notesWithContent && onReady) {
onReady();
}
- }, [ notes, onReady ]);
+ }, [ notesWithContent, onReady ]);
return (
- {notes?.map(childNote => (
- <>
+ {notesWithContent?.map(({ note: childNote, content }) => (
+
{childNote.title}
-
- >
+
+
))}
From f4b6e9c25a2a238818d6762fb11a63dd49428132 Mon Sep 17 00:00:00 2001
From: Elian Doran
Date: Thu, 20 Nov 2025 20:27:01 +0200
Subject: [PATCH 05/16] feat(print/list): display parent note title
---
apps/client/src/widgets/collections/legacy/ListOrGridView.tsx | 2 ++
1 file changed, 2 insertions(+)
diff --git a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
index 811932128..050aa24e3 100644
--- a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
+++ b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
@@ -68,6 +68,8 @@ export function ListPrintView({ note, noteIds: unfilteredNoteIds, highlightedTok
return (
+
{note.title}
+
{notesWithContent?.map(({ note: childNote, content }) => (
{childNote.title}
From 4958b8963677bb9ab35eab53f814ea3fa9561c9c Mon Sep 17 00:00:00 2001
From: Elian Doran
Date: Thu, 20 Nov 2025 20:31:45 +0200
Subject: [PATCH 06/16] feat(print/list): process notes recursively
---
.../collections/legacy/ListOrGridView.tsx | 17 ++++++++++++++++-
1 file changed, 16 insertions(+), 1 deletion(-)
diff --git a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
index 050aa24e3..2970e4e38 100644
--- a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
+++ b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
@@ -48,12 +48,27 @@ export function ListPrintView({ note, noteIds: unfilteredNoteIds, highlightedTok
useLayoutEffect(() => {
froca.getNotes(noteIds).then(async (notes) => {
const notesWithContent: NotesWithContent[] = [];
- for (const note of notes) {
+
+ async function processNote(note: FNote) {
const content = await content_renderer.getRenderedContent(note, {
trim: false,
noChildrenList: true
});
+
notesWithContent.push({ note, content: content.$renderedContent[0].innerHTML });
+
+ if (note.hasChildren()) {
+ const imageLinks = note.getRelations("imageLink");
+ const childNotes = await note.getChildNotes();
+ const filteredChildNotes = childNotes.filter((childNote) => !imageLinks.find((rel) => rel.value === childNote.noteId));
+ for (const childNote of filteredChildNotes) {
+ await processNote(childNote);
+ }
+ }
+ }
+
+ for (const note of notes) {
+ await processNote(note);
}
setNotesWithContent(notesWithContent);
});
From 5e63d9015f2e41cd2fd46ffc12c42126f6ebd4d4 Mon Sep 17 00:00:00 2001
From: Elian Doran
Date: Thu, 20 Nov 2025 20:48:39 +0200
Subject: [PATCH 07/16] feat(print/list): start rewriting headings
---
.../collections/legacy/ListOrGridView.tsx | 23 ++++++++++++++++---
1 file changed, 20 insertions(+), 3 deletions(-)
diff --git a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
index 2970e4e38..de81264d2 100644
--- a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
+++ b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
@@ -13,6 +13,7 @@ import { t } from "../../../services/i18n";
import attribute_renderer from "../../../services/attribute_renderer";
import froca from "../../../services/froca";
import { RawHtmlBlock } from "../../react/RawHtml";
+import { escapeHtml } from "../../../services/utils";
export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }: ViewModeProps<{}>) {
const [ isExpanded ] = useNoteLabelBoolean(note, "expanded");
@@ -41,7 +42,7 @@ interface NotesWithContent {
content: string;
}
-export function ListPrintView({ note, noteIds: unfilteredNoteIds, highlightedTokens, onReady }: ViewModeProps<{}>) {
+export function ListPrintView({ note, noteIds: unfilteredNoteIds, onReady }: ViewModeProps<{}>) {
const noteIds = useFilteredNoteIds(note, unfilteredNoteIds);
const [ notesWithContent, setNotesWithContent ] = useState();
@@ -55,7 +56,24 @@ export function ListPrintView({ note, noteIds: unfilteredNoteIds, highlightedTok
noChildrenList: true
});
- notesWithContent.push({ note, content: content.$renderedContent[0].innerHTML });
+ const contentEl = content.$renderedContent[0];
+
+ // Create page title element
+ const pageTitleEl = document.createElement("h1");
+ pageTitleEl.textContent = note.title;
+ contentEl.prepend(pageTitleEl);
+
+ // Rewrite heading tags to ensure proper hierarchy in print view.
+ const headings = contentEl.querySelectorAll("h1, h2, h3, h4, h5, h6")
+ for (const headingEl of headings) {
+ const currentLevel = parseInt(headingEl.tagName.substring(1), 10);
+ const newLevel = Math.min(currentLevel + 1, 6); // Shift down by 1, max to h6
+ const newHeadingEl = document.createElement(`h${newLevel}`);
+ newHeadingEl.innerHTML = headingEl.innerHTML;
+ headingEl.replaceWith(newHeadingEl);
+ }
+
+ notesWithContent.push({ note, content: contentEl.innerHTML });
if (note.hasChildren()) {
const imageLinks = note.getRelations("imageLink");
@@ -87,7 +105,6 @@ export function ListPrintView({ note, noteIds: unfilteredNoteIds, highlightedTok
{notesWithContent?.map(({ note: childNote, content }) => (
))}
From bbcc2f4be45b6bc4f04d27d6b4d22f07bf88c4c1 Mon Sep 17 00:00:00 2001
From: Elian Doran
Date: Thu, 20 Nov 2025 20:51:41 +0200
Subject: [PATCH 08/16] feat(print/list): rewrite headings while preserving
depth
---
.../src/widgets/collections/legacy/ListOrGridView.tsx | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
index de81264d2..1f42c1890 100644
--- a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
+++ b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
@@ -50,7 +50,7 @@ export function ListPrintView({ note, noteIds: unfilteredNoteIds, onReady }: Vie
froca.getNotes(noteIds).then(async (notes) => {
const notesWithContent: NotesWithContent[] = [];
- async function processNote(note: FNote) {
+ async function processNote(note: FNote, depth: number) {
const content = await content_renderer.getRenderedContent(note, {
trim: false,
noChildrenList: true
@@ -67,7 +67,7 @@ export function ListPrintView({ note, noteIds: unfilteredNoteIds, onReady }: Vie
const headings = contentEl.querySelectorAll("h1, h2, h3, h4, h5, h6")
for (const headingEl of headings) {
const currentLevel = parseInt(headingEl.tagName.substring(1), 10);
- const newLevel = Math.min(currentLevel + 1, 6); // Shift down by 1, max to h6
+ const newLevel = Math.min(currentLevel + depth, 6);
const newHeadingEl = document.createElement(`h${newLevel}`);
newHeadingEl.innerHTML = headingEl.innerHTML;
headingEl.replaceWith(newHeadingEl);
@@ -80,13 +80,13 @@ export function ListPrintView({ note, noteIds: unfilteredNoteIds, onReady }: Vie
const childNotes = await note.getChildNotes();
const filteredChildNotes = childNotes.filter((childNote) => !imageLinks.find((rel) => rel.value === childNote.noteId));
for (const childNote of filteredChildNotes) {
- await processNote(childNote);
+ await processNote(childNote, depth + 1);
}
}
}
for (const note of notes) {
- await processNote(note);
+ await processNote(note, 1);
}
setNotesWithContent(notesWithContent);
});
From c17df24a19fb9bf562ae79abac1129f0e7ac9970 Mon Sep 17 00:00:00 2001
From: Elian Doran
Date: Thu, 20 Nov 2025 20:56:55 +0200
Subject: [PATCH 09/16] refactor(print/list): use separate file
---
.../src/widgets/collections/NoteList.tsx | 3 +-
.../collections/legacy/ListOrGridView.tsx | 93 +------------------
.../collections/legacy/ListPrintView.tsx | 83 +++++++++++++++++
.../src/widgets/collections/legacy/utils.ts | 13 +++
4 files changed, 100 insertions(+), 92 deletions(-)
create mode 100644 apps/client/src/widgets/collections/legacy/ListPrintView.tsx
create mode 100644 apps/client/src/widgets/collections/legacy/utils.ts
diff --git a/apps/client/src/widgets/collections/NoteList.tsx b/apps/client/src/widgets/collections/NoteList.tsx
index 860886a0f..04c03b0e8 100644
--- a/apps/client/src/widgets/collections/NoteList.tsx
+++ b/apps/client/src/widgets/collections/NoteList.tsx
@@ -2,7 +2,7 @@ import { allViewTypes, ViewModeMedia, ViewModeProps, ViewTypeOptions } from "./i
import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useTriliumEvent } from "../react/hooks";
import FNote from "../../entities/fnote";
import "./NoteList.css";
-import { ListView, GridView, ListPrintView } from "./legacy/ListOrGridView";
+import { ListView, GridView } from "./legacy/ListOrGridView";
import { useEffect, useRef, useState } from "preact/hooks";
import GeoView from "./geomap";
import ViewModeStorage from "./view_mode_storage";
@@ -13,6 +13,7 @@ import { subscribeToMessages, unsubscribeToMessage as unsubscribeFromMessage } f
import { WebSocketMessage } from "@triliumnext/commons";
import froca from "../../services/froca";
import PresentationView from "./presentation";
+import { ListPrintView } from "./legacy/ListPrintView";
interface NoteListProps {
note: FNote | null | undefined;
diff --git a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
index 1f42c1890..9017c99ba 100644
--- a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
+++ b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
+import { useEffect, useRef, useState } from "preact/hooks";
import FNote from "../../../entities/fnote";
import Icon from "../../react/Icon";
import { ViewModeProps } from "../interface";
@@ -11,9 +11,7 @@ import tree from "../../../services/tree";
import link from "../../../services/link";
import { t } from "../../../services/i18n";
import attribute_renderer from "../../../services/attribute_renderer";
-import froca from "../../../services/froca";
-import { RawHtmlBlock } from "../../react/RawHtml";
-import { escapeHtml } from "../../../services/utils";
+import { useFilteredNoteIds } from "./utils";
export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }: ViewModeProps<{}>) {
const [ isExpanded ] = useNoteLabelBoolean(note, "expanded");
@@ -37,82 +35,6 @@ export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
);
}
-interface NotesWithContent {
- note: FNote;
- content: string;
-}
-
-export function ListPrintView({ note, noteIds: unfilteredNoteIds, onReady }: ViewModeProps<{}>) {
- const noteIds = useFilteredNoteIds(note, unfilteredNoteIds);
- const [ notesWithContent, setNotesWithContent ] = useState();
-
- useLayoutEffect(() => {
- froca.getNotes(noteIds).then(async (notes) => {
- const notesWithContent: NotesWithContent[] = [];
-
- async function processNote(note: FNote, depth: number) {
- const content = await content_renderer.getRenderedContent(note, {
- trim: false,
- noChildrenList: true
- });
-
- const contentEl = content.$renderedContent[0];
-
- // Create page title element
- const pageTitleEl = document.createElement("h1");
- pageTitleEl.textContent = note.title;
- contentEl.prepend(pageTitleEl);
-
- // Rewrite heading tags to ensure proper hierarchy in print view.
- const headings = contentEl.querySelectorAll("h1, h2, h3, h4, h5, h6")
- for (const headingEl of headings) {
- const currentLevel = parseInt(headingEl.tagName.substring(1), 10);
- const newLevel = Math.min(currentLevel + depth, 6);
- const newHeadingEl = document.createElement(`h${newLevel}`);
- newHeadingEl.innerHTML = headingEl.innerHTML;
- headingEl.replaceWith(newHeadingEl);
- }
-
- notesWithContent.push({ note, content: contentEl.innerHTML });
-
- if (note.hasChildren()) {
- const imageLinks = note.getRelations("imageLink");
- const childNotes = await note.getChildNotes();
- const filteredChildNotes = childNotes.filter((childNote) => !imageLinks.find((rel) => rel.value === childNote.noteId));
- for (const childNote of filteredChildNotes) {
- await processNote(childNote, depth + 1);
- }
- }
- }
-
- for (const note of notes) {
- await processNote(note, 1);
- }
- setNotesWithContent(notesWithContent);
- });
- }, [noteIds]);
-
- useEffect(() => {
- if (notesWithContent && onReady) {
- onReady();
- }
- }, [ notesWithContent, onReady ]);
-
- return (
-
-
-
{note.title}
-
- {notesWithContent?.map(({ note: childNote, content }) => (
-
- ))}
-
-
- );
-}
-
export function GridView({ note, noteIds: unfilteredNoteIds, highlightedTokens }: ViewModeProps<{}>) {
const noteIds = useFilteredNoteIds(note, unfilteredNoteIds);
const { pageNotes, ...pagination } = usePagination(note, noteIds);
@@ -252,17 +174,6 @@ function NoteChildren({ note, parentNote, highlightedTokens }: { note: FNote, pa
return childNotes?.map(childNote => )
}
-/**
- * Filters the note IDs for the legacy view to filter out subnotes that are already included in the note content such as images, included notes.
- */
-function useFilteredNoteIds(note: FNote, noteIds: string[]) {
- return useMemo(() => {
- const includedLinks = note ? note.getRelations().filter((rel) => rel.name === "imageLink" || rel.name === "includeNoteLink") : [];
- const includedNoteIds = new Set(includedLinks.map((rel) => rel.value));
- return noteIds.filter((noteId) => !includedNoteIds.has(noteId) && noteId !== "_hidden");
- }, noteIds);
-}
-
function getNotePath(parentNote: FNote, childNote: FNote) {
if (parentNote.type === "search") {
// for search note parent, we want to display a non-search path
diff --git a/apps/client/src/widgets/collections/legacy/ListPrintView.tsx b/apps/client/src/widgets/collections/legacy/ListPrintView.tsx
new file mode 100644
index 000000000..ab6be78ab
--- /dev/null
+++ b/apps/client/src/widgets/collections/legacy/ListPrintView.tsx
@@ -0,0 +1,83 @@
+import { useEffect, useLayoutEffect, useState } from "preact/hooks";
+import { RawHtmlBlock } from "../../react/RawHtml";
+import froca from "../../../services/froca";
+import type FNote from "../../../entities/fnote";
+import content_renderer from "../../../services/content_renderer";
+import type { ViewModeProps } from "../interface";
+import { useFilteredNoteIds } from "./utils";
+
+interface NotesWithContent {
+ note: FNote;
+ content: string;
+}
+
+export function ListPrintView({ note, noteIds: unfilteredNoteIds, onReady }: ViewModeProps<{}>) {
+ const noteIds = useFilteredNoteIds(note, unfilteredNoteIds);
+ const [ notesWithContent, setNotesWithContent ] = useState();
+
+ useLayoutEffect(() => {
+ froca.getNotes(noteIds).then(async (notes) => {
+ const notesWithContent: NotesWithContent[] = [];
+
+ async function processNote(note: FNote, depth: number) {
+ const content = await content_renderer.getRenderedContent(note, {
+ trim: false,
+ noChildrenList: true
+ });
+
+ const contentEl = content.$renderedContent[0];
+
+ // Create page title element
+ const pageTitleEl = document.createElement("h1");
+ pageTitleEl.textContent = note.title;
+ contentEl.prepend(pageTitleEl);
+
+ // Rewrite heading tags to ensure proper hierarchy in print view.
+ const headings = contentEl.querySelectorAll("h1, h2, h3, h4, h5, h6")
+ for (const headingEl of headings) {
+ const currentLevel = parseInt(headingEl.tagName.substring(1), 10);
+ const newLevel = Math.min(currentLevel + depth, 6);
+ const newHeadingEl = document.createElement(`h${newLevel}`);
+ newHeadingEl.innerHTML = headingEl.innerHTML;
+ headingEl.replaceWith(newHeadingEl);
+ }
+
+ notesWithContent.push({ note, content: contentEl.innerHTML });
+
+ if (note.hasChildren()) {
+ const imageLinks = note.getRelations("imageLink");
+ const childNotes = await note.getChildNotes();
+ const filteredChildNotes = childNotes.filter((childNote) => !imageLinks.find((rel) => rel.value === childNote.noteId));
+ for (const childNote of filteredChildNotes) {
+ await processNote(childNote, depth + 1);
+ }
+ }
+ }
+
+ for (const note of notes) {
+ await processNote(note, 1);
+ }
+ setNotesWithContent(notesWithContent);
+ });
+ }, [noteIds]);
+
+ useEffect(() => {
+ if (notesWithContent && onReady) {
+ onReady();
+ }
+ }, [ notesWithContent, onReady ]);
+
+ return (
+
+
+
{note.title}
+
+ {notesWithContent?.map(({ note: childNote, content }) => (
+
+ ))}
+
+
+ );
+}
diff --git a/apps/client/src/widgets/collections/legacy/utils.ts b/apps/client/src/widgets/collections/legacy/utils.ts
new file mode 100644
index 000000000..6592c9cd9
--- /dev/null
+++ b/apps/client/src/widgets/collections/legacy/utils.ts
@@ -0,0 +1,13 @@
+import { useMemo } from "preact/hooks";
+import FNote from "../../../entities/fnote";
+
+/**
+ * Filters the note IDs for the legacy view to filter out subnotes that are already included in the note content such as images, included notes.
+ */
+export function useFilteredNoteIds(note: FNote, noteIds: string[]) {
+ return useMemo(() => {
+ const includedLinks = note ? note.getRelations().filter((rel) => rel.name === "imageLink" || rel.name === "includeNoteLink") : [];
+ const includedNoteIds = new Set(includedLinks.map((rel) => rel.value));
+ return noteIds.filter((noteId) => !includedNoteIds.has(noteId) && noteId !== "_hidden");
+ }, noteIds);
+}
From 89a83a625b8d01395a1d4c0b7e482bfdf2434d29 Mon Sep 17 00:00:00 2001
From: Elian Doran
Date: Thu, 20 Nov 2025 20:58:50 +0200
Subject: [PATCH 10/16] refactor(print/list): extract into functions
---
.../collections/legacy/ListPrintView.tsx | 33 +++++++++++--------
1 file changed, 19 insertions(+), 14 deletions(-)
diff --git a/apps/client/src/widgets/collections/legacy/ListPrintView.tsx b/apps/client/src/widgets/collections/legacy/ListPrintView.tsx
index ab6be78ab..01a8259c2 100644
--- a/apps/client/src/widgets/collections/legacy/ListPrintView.tsx
+++ b/apps/client/src/widgets/collections/legacy/ListPrintView.tsx
@@ -27,20 +27,8 @@ export function ListPrintView({ note, noteIds: unfilteredNoteIds, onReady }: Vie
const contentEl = content.$renderedContent[0];
- // Create page title element
- const pageTitleEl = document.createElement("h1");
- pageTitleEl.textContent = note.title;
- contentEl.prepend(pageTitleEl);
-
- // Rewrite heading tags to ensure proper hierarchy in print view.
- const headings = contentEl.querySelectorAll("h1, h2, h3, h4, h5, h6")
- for (const headingEl of headings) {
- const currentLevel = parseInt(headingEl.tagName.substring(1), 10);
- const newLevel = Math.min(currentLevel + depth, 6);
- const newHeadingEl = document.createElement(`h${newLevel}`);
- newHeadingEl.innerHTML = headingEl.innerHTML;
- headingEl.replaceWith(newHeadingEl);
- }
+ insertPageTitle(contentEl, note.title);
+ rewriteHeadings(contentEl, depth);
notesWithContent.push({ note, content: contentEl.innerHTML });
@@ -81,3 +69,20 @@ export function ListPrintView({ note, noteIds: unfilteredNoteIds, onReady }: Vie
);
}
+
+function insertPageTitle(contentEl: HTMLElement, title: string) {
+ const pageTitleEl = document.createElement("h1");
+ pageTitleEl.textContent = title;
+ contentEl.prepend(pageTitleEl);
+}
+
+function rewriteHeadings(contentEl: HTMLElement, depth: number) {
+ const headings = contentEl.querySelectorAll("h1, h2, h3, h4, h5, h6")
+ for (const headingEl of headings) {
+ const currentLevel = parseInt(headingEl.tagName.substring(1), 10);
+ const newLevel = Math.min(currentLevel + depth, 6);
+ const newHeadingEl = document.createElement(`h${newLevel}`);
+ newHeadingEl.innerHTML = headingEl.innerHTML;
+ headingEl.replaceWith(newHeadingEl);
+ }
+}
From eee496a050d63d3dcf2af60bb4ed25c29b333ca3 Mon Sep 17 00:00:00 2001
From: Elian Doran
Date: Thu, 20 Nov 2025 20:59:34 +0200
Subject: [PATCH 11/16] chore(print/list): get rid of inner div
---
.../src/widgets/collections/legacy/ListPrintView.tsx | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/apps/client/src/widgets/collections/legacy/ListPrintView.tsx b/apps/client/src/widgets/collections/legacy/ListPrintView.tsx
index 01a8259c2..07750a87b 100644
--- a/apps/client/src/widgets/collections/legacy/ListPrintView.tsx
+++ b/apps/client/src/widgets/collections/legacy/ListPrintView.tsx
@@ -8,7 +8,7 @@ import { useFilteredNoteIds } from "./utils";
interface NotesWithContent {
note: FNote;
- content: string;
+ content: { __html: string };
}
export function ListPrintView({ note, noteIds: unfilteredNoteIds, onReady }: ViewModeProps<{}>) {
@@ -30,7 +30,7 @@ export function ListPrintView({ note, noteIds: unfilteredNoteIds, onReady }: Vie
insertPageTitle(contentEl, note.title);
rewriteHeadings(contentEl, depth);
- notesWithContent.push({ note, content: contentEl.innerHTML });
+ notesWithContent.push({ note, content: { __html: contentEl.innerHTML } });
if (note.hasChildren()) {
const imageLinks = note.getRelations("imageLink");
@@ -61,9 +61,7 @@ export function ListPrintView({ note, noteIds: unfilteredNoteIds, onReady }: Vie
{note.title}
{notesWithContent?.map(({ note: childNote, content }) => (
-
+
))}
From f4d6e98d61127038662bf6941b83900e788ef656 Mon Sep 17 00:00:00 2001
From: Elian Doran
Date: Thu, 20 Nov 2025 21:06:25 +0200
Subject: [PATCH 12/16] feat(print/list): rewrite links
---
apps/client/src/services/link.ts | 44 ++++++++++---------
.../collections/legacy/ListPrintView.tsx | 12 +++++
2 files changed, 35 insertions(+), 21 deletions(-)
diff --git a/apps/client/src/services/link.ts b/apps/client/src/services/link.ts
index 9af93b313..a596e7136 100644
--- a/apps/client/src/services/link.ts
+++ b/apps/client/src/services/link.ts
@@ -467,28 +467,30 @@ function getReferenceLinkTitleSync(href: string) {
}
}
-// TODO: Check why the event is not supported.
-//@ts-ignore
-$(document).on("click", "a", goToLink);
-// TODO: Check why the event is not supported.
-//@ts-ignore
-$(document).on("auxclick", "a", goToLink); // to handle the middle button
-// TODO: Check why the event is not supported.
-//@ts-ignore
-$(document).on("contextmenu", "a", linkContextMenu);
-// TODO: Check why the event is not supported.
-//@ts-ignore
-$(document).on("dblclick", "a", goToLink);
+if (glob.device !== "print") {
+ // TODO: Check why the event is not supported.
+ //@ts-ignore
+ $(document).on("click", "a", goToLink);
+ // TODO: Check why the event is not supported.
+ //@ts-ignore
+ $(document).on("auxclick", "a", goToLink); // to handle the middle button
+ // TODO: Check why the event is not supported.
+ //@ts-ignore
+ $(document).on("contextmenu", "a", linkContextMenu);
+ // TODO: Check why the event is not supported.
+ //@ts-ignore
+ $(document).on("dblclick", "a", goToLink);
-$(document).on("mousedown", "a", (e) => {
- if (e.which === 2) {
- // prevent paste on middle click
- // https://github.com/zadam/trilium/issues/2995
- // https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event#preventing_default_actions
- e.preventDefault();
- return false;
- }
-});
+ $(document).on("mousedown", "a", (e) => {
+ if (e.which === 2) {
+ // prevent paste on middle click
+ // https://github.com/zadam/trilium/issues/2995
+ // https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event#preventing_default_actions
+ e.preventDefault();
+ return false;
+ }
+ });
+}
export default {
getNotePathFromUrl,
diff --git a/apps/client/src/widgets/collections/legacy/ListPrintView.tsx b/apps/client/src/widgets/collections/legacy/ListPrintView.tsx
index 07750a87b..d4fa36133 100644
--- a/apps/client/src/widgets/collections/legacy/ListPrintView.tsx
+++ b/apps/client/src/widgets/collections/legacy/ListPrintView.tsx
@@ -29,6 +29,7 @@ export function ListPrintView({ note, noteIds: unfilteredNoteIds, onReady }: Vie
insertPageTitle(contentEl, note.title);
rewriteHeadings(contentEl, depth);
+ rewriteLinks(contentEl);
notesWithContent.push({ note, content: { __html: contentEl.innerHTML } });
@@ -84,3 +85,14 @@ function rewriteHeadings(contentEl: HTMLElement, depth: number) {
headingEl.replaceWith(newHeadingEl);
}
}
+
+function rewriteLinks(contentEl: HTMLElement) {
+ const linkEls = contentEl.querySelectorAll("a");
+ for (const linkEl of linkEls) {
+ const href = linkEl.getAttribute("href");
+ if (href && href.startsWith("#root/")) {
+ const noteId = href.split("/").at(-1);
+ linkEl.setAttribute("href", `#note-${noteId}`);
+ }
+ }
+}
From 25a51a71a0df0ca9666576c0a12709630ae0cb10 Mon Sep 17 00:00:00 2001
From: Elian Doran
Date: Thu, 20 Nov 2025 21:20:24 +0200
Subject: [PATCH 13/16] feat(print/list): unlink references to notes that are
not printed
---
.../collections/legacy/ListPrintView.tsx | 30 ++++++++++++++-----
1 file changed, 22 insertions(+), 8 deletions(-)
diff --git a/apps/client/src/widgets/collections/legacy/ListPrintView.tsx b/apps/client/src/widgets/collections/legacy/ListPrintView.tsx
index d4fa36133..a2b9d5319 100644
--- a/apps/client/src/widgets/collections/legacy/ListPrintView.tsx
+++ b/apps/client/src/widgets/collections/legacy/ListPrintView.tsx
@@ -8,11 +8,12 @@ import { useFilteredNoteIds } from "./utils";
interface NotesWithContent {
note: FNote;
- content: { __html: string };
+ contentEl: HTMLElement;
}
export function ListPrintView({ note, noteIds: unfilteredNoteIds, onReady }: ViewModeProps<{}>) {
const noteIds = useFilteredNoteIds(note, unfilteredNoteIds);
+ const noteIdsSet = new Set();
const [ notesWithContent, setNotesWithContent ] = useState();
useLayoutEffect(() => {
@@ -29,9 +30,8 @@ export function ListPrintView({ note, noteIds: unfilteredNoteIds, onReady }: Vie
insertPageTitle(contentEl, note.title);
rewriteHeadings(contentEl, depth);
- rewriteLinks(contentEl);
-
- notesWithContent.push({ note, content: { __html: contentEl.innerHTML } });
+ noteIdsSet.add(note.noteId);
+ notesWithContent.push({ note, contentEl });
if (note.hasChildren()) {
const imageLinks = note.getRelations("imageLink");
@@ -46,6 +46,12 @@ export function ListPrintView({ note, noteIds: unfilteredNoteIds, onReady }: Vie
for (const note of notes) {
await processNote(note, 1);
}
+
+ // After all notes are processed, rewrite links
+ for (const { contentEl } of notesWithContent) {
+ rewriteLinks(contentEl, noteIdsSet);
+ }
+
setNotesWithContent(notesWithContent);
});
}, [noteIds]);
@@ -61,8 +67,8 @@ export function ListPrintView({ note, noteIds: unfilteredNoteIds, onReady }: Vie
{note.title}
- {notesWithContent?.map(({ note: childNote, content }) => (
-
+ {notesWithContent?.map(({ note: childNote, contentEl }) => (
+
))}
@@ -86,13 +92,21 @@ function rewriteHeadings(contentEl: HTMLElement, depth: number) {
}
}
-function rewriteLinks(contentEl: HTMLElement) {
+function rewriteLinks(contentEl: HTMLElement, noteIdsSet: Set) {
const linkEls = contentEl.querySelectorAll("a");
for (const linkEl of linkEls) {
const href = linkEl.getAttribute("href");
if (href && href.startsWith("#root/")) {
const noteId = href.split("/").at(-1);
- linkEl.setAttribute("href", `#note-${noteId}`);
+
+ if (noteId && noteIdsSet.has(noteId)) {
+ linkEl.setAttribute("href", `#note-${noteId}`);
+ } else {
+ // Link to note not in the print view, remove link but keep text
+ const spanEl = document.createElement("span");
+ spanEl.innerHTML = linkEl.innerHTML;
+ linkEl.replaceWith(spanEl);
+ }
}
}
}
From 8b4e76832f3bb33ee705b0e409f899c0e5a32e3f Mon Sep 17 00:00:00 2001
From: Elian Doran
Date: Thu, 20 Nov 2025 21:28:55 +0200
Subject: [PATCH 14/16] docs(user): update documentation regarding printing
multiple notes
---
.../Notes/Printing & Exporting as PDF.html | 53 +++++++++++--------
.../User Guide/Collections/List View.html | 17 +++++-
.../Custom Widgets/Right pane widget.html | 9 ++--
.../Developer Guide/Documentation.md | 2 +-
docs/User Guide/!!!meta.json | 14 +++++
.../Notes/Printing & Exporting as PDF.md | 10 ++++
.../User Guide/Collections/List View.md | 13 ++++-
7 files changed, 88 insertions(+), 30 deletions(-)
diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Printing & Exporting as PDF.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Printing & Exporting as PDF.html
index c0cf16ba9..f0c704785 100644
--- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Printing & Exporting as PDF.html
+++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Printing & Exporting as PDF.html
@@ -4,7 +4,6 @@
Screenshot of the note contextual menu indicating the “Export as PDF”
option.
-
Printing
This feature allows printing of notes. It works on both the desktop client,
but also on the web.
@@ -60,9 +59,9 @@ class="admonition note">
orientation, size. However, there are a few Attributes to adjust some of the settings:
- - To print in landscape mode instead of portrait (useful for big diagrams
+
- To print in landscape mode instead of portrait (useful for big diagrams
or slides), add
#printLandscape.
- - By default, the resulting PDF will be in Letter format. It is possible
+
- By default, the resulting PDF will be in Letter format. It is possible
to adjust it to another page size via the
#printPageSize attribute,
with one of the following values: A0, A1, A2, A3, A4, A5, A6, Legal, Letter, Tabloid, Ledger.
@@ -70,15 +69,26 @@ class="admonition note">
These options have no effect when used with the printing feature, since
the user-defined settings are used instead.
+ Printing multiple notes
+ Since v0.100.0, it is possible to print more than one note at the time
+ by using Collections:
+
+ - First create a collection.
+ - Configure it to use List View.
+ - Print the collection note normally.
+
+ The resulting collection will contain all the children of the collection,
+ while maintaining the hierarchy.
Keyboard shortcut
It's possible to trigger both printing and export as PDF from the keyboard
by going to Keyboard shortcuts in Options and assigning a key combination
for:
- - Print Active Note
+
- Print Active Note
- - Export Active Note as PDF
+
- Export Active Note as PDF
Constraints & limitations
@@ -86,24 +96,24 @@ class="admonition note">
supported when printing, in which case the Print and Export as PDF options
will be disabled.
- - For Code notes:
+
- For Code notes:
- - Line numbers are not printed.
- - Syntax highlighting is enabled, however a default theme (Visual Studio)
+
- Line numbers are not printed.
+ - Syntax highlighting is enabled, however a default theme (Visual Studio)
is enforced.
- - For Collections:
+
- For Collections:
- - Only Presentation is
+
- Only Presentation is
currently supported.
- - We plan to add support for all the collection types at some point.
+ - We plan to add support for all the collection types at some point.
- - Using Custom app-wide CSS for
+
- Using Custom app-wide CSS for
printing is not longer supported, due to a more stable but isolated mechanism.
- - We plan to introduce a new mechanism specifically for a print CSS.
+ - We plan to introduce a new mechanism specifically for a print CSS.
@@ -114,10 +124,10 @@ class="admonition note">
printing.
To do so:
- - Create a CSS code note.
- - On the note being printed, apply the
~printCss relation to
+ - Create a CSS code note.
+ - On the note being printed, apply the
~printCss relation to
point to the newly created CSS code note.
- - To apply the CSS to multiple notes, consider using inheritable attributes or
+
- To apply the CSS to multiple notes, consider using inheritable attributes or
Templates.
@@ -128,12 +138,13 @@ class="admonition note">
}
To remark:
- - Multiple CSS notes can be add by using multiple
~printCss relations.
- - If the note pointing to the
printCss doesn't have the right
+ - Multiple CSS notes can be add by using multiple
~printCss relations.
+ - If the note pointing to the
printCss doesn't have the right
note type or mime type, it will be ignored.
- - If migrating from a previous version where Custom app-wide CSS, there's no need for
@media print { since
- the style-sheet is used only for printing.
+ - If migrating from a previous version where Custom app-wide CSS, there's no need for
@media print { since
+ the style-sheet is used only for printing.
Under the hood
Both printing and exporting as PDF use the same mechanism: a note is rendered
diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Collections/List View.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Collections/List View.html
index f3e4926b4..64c09e024 100644
--- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Collections/List View.html
+++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Collections/List View.html
@@ -12,9 +12,22 @@
as a single continuous document.
Interaction
- - Each note can be expanded or collapsed by clicking on the arrow to the
+
- Each note can be expanded or collapsed by clicking on the arrow to the
left of the title.
- - In the Ribbon,
+
- In the Ribbon,
in the Collection tab there are options to expand and to collapse
all notes easily.
+
+Printing and exporting to PDF
+Since v0.100.0, list collections can be printed or exported to PDF.
+A printed list collection will print all the notes in the collection,
+ in the right order and preserving the full hierarchy.
+If exported to PDF within the desktop application, there is additional
+ functionality:
+
+ - The table of contents of the PDF will reflect the structure of the notes.
+ - Reference and inline links to other notes within the same hierarchy will
+ be functional (will jump to the corresponding page). If a link refers to
+ a note that is not in the printed hierarchy, it will be unlinked.
\ No newline at end of file
diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Frontend Basics/Custom Widgets/Right pane widget.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Frontend Basics/Custom Widgets/Right pane widget.html
index 393a9a60a..27437786f 100644
--- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Frontend Basics/Custom Widgets/Right pane widget.html
+++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Scripting/Frontend Basics/Custom Widgets/Right pane widget.html
@@ -1,13 +1,12 @@
- doRender must not be overridden, instead doRenderBody() has
+ doRender must not be overridden, instead doRenderBody() has
to be overridden.
- doRenderBody can optionally be async.
+ doRenderBody can optionally be async.
- parentWidget() must be set to “rightPane”.
- widgetTitle() getter can optionally be overriden, otherwise
+ parentWidget() must be set to “rightPane”.
+ widgetTitle() getter can optionally be overriden, otherwise
the widget will be displayed as “Untitled widget”.
const template = `<div>Hi</div>`;
diff --git a/docs/Developer Guide/Developer Guide/Documentation.md b/docs/Developer Guide/Developer Guide/Documentation.md
index 93ee33d7a..b8eca125b 100644
--- a/docs/Developer Guide/Developer Guide/Documentation.md
+++ b/docs/Developer Guide/Developer Guide/Documentation.md
@@ -1,5 +1,5 @@
# Documentation
-There are multiple types of documentation for Trilium:
+There are multiple types of documentation for Trilium:
* The _User Guide_ represents the user-facing documentation. This documentation can be browsed by users directly from within Trilium, by pressing F1.
* The _Developer's Guide_ represents a set of Markdown documents that present the internals of Trilium, for developers.
diff --git a/docs/User Guide/!!!meta.json b/docs/User Guide/!!!meta.json
index 91af437e1..7b77cb46a 100644
--- a/docs/User Guide/!!!meta.json
+++ b/docs/User Guide/!!!meta.json
@@ -4135,6 +4135,13 @@
"value": "printing-and-pdf-export",
"isInheritable": false,
"position": 110
+ },
+ {
+ "type": "relation",
+ "name": "internalLink",
+ "value": "mULW0Q3VojwY",
+ "isInheritable": false,
+ "position": 130
}
],
"format": "markdown",
@@ -10478,6 +10485,13 @@
"value": "list",
"isInheritable": false,
"position": 30
+ },
+ {
+ "type": "relation",
+ "name": "internalLink",
+ "value": "NRnIZmSMc5sj",
+ "isInheritable": false,
+ "position": 40
}
],
"format": "markdown",
diff --git a/docs/User Guide/User Guide/Basic Concepts and Features/Notes/Printing & Exporting as PDF.md b/docs/User Guide/User Guide/Basic Concepts and Features/Notes/Printing & Exporting as PDF.md
index 21fbe12e6..d8cbe4bfa 100644
--- a/docs/User Guide/User Guide/Basic Concepts and Features/Notes/Printing & Exporting as PDF.md
+++ b/docs/User Guide/User Guide/Basic Concepts and Features/Notes/Printing & Exporting as PDF.md
@@ -49,6 +49,16 @@ When exporting to PDF, there are no customizable settings such as page orientati
> [!NOTE]
> These options have no effect when used with the printing feature, since the user-defined settings are used instead.
+## Printing multiple notes
+
+Since v0.100.0, it is possible to print more than one note at the time by using Collections:
+
+1. First create a collection.
+2. Configure it to use List View.
+3. Print the collection note normally.
+
+The resulting collection will contain all the children of the collection, while maintaining the hierarchy.
+
## Keyboard shortcut
It's possible to trigger both printing and export as PDF from the keyboard by going to _Keyboard shortcuts_ in Options and assigning a key combination for:
diff --git a/docs/User Guide/User Guide/Collections/List View.md b/docs/User Guide/User Guide/Collections/List View.md
index 76fd15820..86cb59806 100644
--- a/docs/User Guide/User Guide/Collections/List View.md
+++ b/docs/User Guide/User Guide/Collections/List View.md
@@ -8,4 +8,15 @@ In the example above, the "Node.js" note on the left panel contains several chil
## Interaction
* Each note can be expanded or collapsed by clicking on the arrow to the left of the title.
-* In the Ribbon, in the _Collection_ tab there are options to expand and to collapse all notes easily.
\ No newline at end of file
+* In the Ribbon, in the _Collection_ tab there are options to expand and to collapse all notes easily.
+
+## Printing and exporting to PDF
+
+Since v0.100.0, list collections can be [printed or exported to PDF](../Basic%20Concepts%20and%20Features/Notes/Printing%20%26%20Exporting%20as%20PDF.md).
+
+A printed list collection will print all the notes in the collection, in the right order and preserving the full hierarchy.
+
+If exported to PDF within the desktop application, there is additional functionality:
+
+* The table of contents of the PDF will reflect the structure of the notes.
+* Reference and inline links to other notes within the same hierarchy will be functional (will jump to the corresponding page). If a link refers to a note that is not in the printed hierarchy, it will be unlinked.
\ No newline at end of file
From 049721bbfe5c877f4082acc58218529272e8dfeb Mon Sep 17 00:00:00 2001
From: Elian Doran
Date: Thu, 20 Nov 2025 21:32:06 +0200
Subject: [PATCH 15/16] docs(user): update limitations for printing/exporting
---
.../Notes/Printing & Exporting as PDF.html | 10 +++++++---
.../Notes/Printing & Exporting as PDF.md | 5 +++--
2 files changed, 10 insertions(+), 5 deletions(-)
diff --git a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Printing & Exporting as PDF.html b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Printing & Exporting as PDF.html
index f0c704785..35ae5862b 100644
--- a/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Printing & Exporting as PDF.html
+++ b/apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Printing & Exporting as PDF.html
@@ -105,9 +105,13 @@ class="admonition note">
For Collections:
- - Only Presentation is
- currently supported.
- - We plan to add support for all the collection types at some point.
+ - List View is
+ supported, allowing to print multiple notes at once while preserving hierarchy
+ (similar to a book).
+ - Presentation is
+ also supported, where each slide/subnote is displayed.
+ - The rest of the collections are not supported, but we plan to add support
+ for all the collection types at some point.
Using Custom app-wide CSS for
diff --git a/docs/User Guide/User Guide/Basic Concepts and Features/Notes/Printing & Exporting as PDF.md b/docs/User Guide/User Guide/Basic Concepts and Features/Notes/Printing & Exporting as PDF.md
index d8cbe4bfa..083ad6ec7 100644
--- a/docs/User Guide/User Guide/Basic Concepts and Features/Notes/Printing & Exporting as PDF.md
+++ b/docs/User Guide/User Guide/Basic Concepts and Features/Notes/Printing & Exporting as PDF.md
@@ -74,8 +74,9 @@ Not all Note Types
* Line numbers are not printed.
* Syntax highlighting is enabled, however a default theme (Visual Studio) is enforced.
* For Collections:
- * Only Presentation is currently supported.
- * We plan to add support for all the collection types at some point.
+ * List View is supported, allowing to print multiple notes at once while preserving hierarchy (similar to a book).
+ * Presentation is also supported, where each slide/subnote is displayed.
+ * The rest of the collections are not supported, but we plan to add support for all the collection types at some point.
* Using Custom app-wide CSS for printing is not longer supported, due to a more stable but isolated mechanism.
* We plan to introduce a new mechanism specifically for a print CSS.
From be115c74c3626fd4d47d56fbb84166813d98ad4c Mon Sep 17 00:00:00 2001
From: Elian Doran
Date: Thu, 20 Nov 2025 21:42:50 +0200
Subject: [PATCH 16/16] chore(print/list): address review
---
.../widgets/collections/legacy/ListOrGridView.tsx | 8 ++------
.../src/widgets/collections/legacy/ListPrintView.tsx | 12 +++++-------
apps/client/src/widgets/collections/legacy/utils.ts | 9 ++++++++-
3 files changed, 15 insertions(+), 14 deletions(-)
diff --git a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
index 9017c99ba..749036598 100644
--- a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
+++ b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx
@@ -11,7 +11,7 @@ import tree from "../../../services/tree";
import link from "../../../services/link";
import { t } from "../../../services/i18n";
import attribute_renderer from "../../../services/attribute_renderer";
-import { useFilteredNoteIds } from "./utils";
+import { filterChildNotes, useFilteredNoteIds } from "./utils";
export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }: ViewModeProps<{}>) {
const [ isExpanded ] = useNoteLabelBoolean(note, "expanded");
@@ -161,14 +161,10 @@ function NoteContent({ note, trim, noChildrenList, highlightedTokens }: { note:
}
function NoteChildren({ note, parentNote, highlightedTokens }: { note: FNote, parentNote: FNote, highlightedTokens: string[] | null | undefined }) {
- const imageLinks = note.getRelations("imageLink");
const [ childNotes, setChildNotes ] = useState();
useEffect(() => {
- note.getChildNotes().then(childNotes => {
- const filteredChildNotes = childNotes.filter((childNote) => !imageLinks.find((rel) => rel.value === childNote.noteId));
- setChildNotes(filteredChildNotes);
- });
+ filterChildNotes(note).then(setChildNotes);
}, [ note ]);
return childNotes?.map(childNote => )
diff --git a/apps/client/src/widgets/collections/legacy/ListPrintView.tsx b/apps/client/src/widgets/collections/legacy/ListPrintView.tsx
index a2b9d5319..77a354d0d 100644
--- a/apps/client/src/widgets/collections/legacy/ListPrintView.tsx
+++ b/apps/client/src/widgets/collections/legacy/ListPrintView.tsx
@@ -1,10 +1,9 @@
import { useEffect, useLayoutEffect, useState } from "preact/hooks";
-import { RawHtmlBlock } from "../../react/RawHtml";
import froca from "../../../services/froca";
import type FNote from "../../../entities/fnote";
import content_renderer from "../../../services/content_renderer";
import type { ViewModeProps } from "../interface";
-import { useFilteredNoteIds } from "./utils";
+import { filterChildNotes, useFilteredNoteIds } from "./utils";
interface NotesWithContent {
note: FNote;
@@ -13,10 +12,11 @@ interface NotesWithContent {
export function ListPrintView({ note, noteIds: unfilteredNoteIds, onReady }: ViewModeProps<{}>) {
const noteIds = useFilteredNoteIds(note, unfilteredNoteIds);
- const noteIdsSet = new Set();
const [ notesWithContent, setNotesWithContent ] = useState();
useLayoutEffect(() => {
+ const noteIdsSet = new Set();
+
froca.getNotes(noteIds).then(async (notes) => {
const notesWithContent: NotesWithContent[] = [];
@@ -34,9 +34,7 @@ export function ListPrintView({ note, noteIds: unfilteredNoteIds, onReady }: Vie
notesWithContent.push({ note, contentEl });
if (note.hasChildren()) {
- const imageLinks = note.getRelations("imageLink");
- const childNotes = await note.getChildNotes();
- const filteredChildNotes = childNotes.filter((childNote) => !imageLinks.find((rel) => rel.value === childNote.noteId));
+ const filteredChildNotes = await filterChildNotes(note);
for (const childNote of filteredChildNotes) {
await processNote(childNote, depth + 1);
}
@@ -82,7 +80,7 @@ function insertPageTitle(contentEl: HTMLElement, title: string) {
}
function rewriteHeadings(contentEl: HTMLElement, depth: number) {
- const headings = contentEl.querySelectorAll("h1, h2, h3, h4, h5, h6")
+ const headings = contentEl.querySelectorAll("h1, h2, h3, h4, h5, h6");
for (const headingEl of headings) {
const currentLevel = parseInt(headingEl.tagName.substring(1), 10);
const newLevel = Math.min(currentLevel + depth, 6);
diff --git a/apps/client/src/widgets/collections/legacy/utils.ts b/apps/client/src/widgets/collections/legacy/utils.ts
index 6592c9cd9..6432ce1d2 100644
--- a/apps/client/src/widgets/collections/legacy/utils.ts
+++ b/apps/client/src/widgets/collections/legacy/utils.ts
@@ -9,5 +9,12 @@ export function useFilteredNoteIds(note: FNote, noteIds: string[]) {
const includedLinks = note ? note.getRelations().filter((rel) => rel.name === "imageLink" || rel.name === "includeNoteLink") : [];
const includedNoteIds = new Set(includedLinks.map((rel) => rel.value));
return noteIds.filter((noteId) => !includedNoteIds.has(noteId) && noteId !== "_hidden");
- }, noteIds);
+ }, [ note, noteIds ]);
+}
+
+export async function filterChildNotes(note: FNote) {
+ const imageLinks = note.getRelations("imageLink");
+ const imageLinkNoteIds = new Set(imageLinks.map(rel => rel.value));
+ const childNotes = await note.getChildNotes();
+ return childNotes.filter((childNote) => !imageLinkNoteIds.has(childNote.noteId));
}