Skip to content

Commit 26a009b

Browse files
authored
Printable list collection (#7812)
2 parents 165357f + be115c7 commit 26a009b

File tree

13 files changed

+260
-74
lines changed

13 files changed

+260
-74
lines changed

apps/client/src/services/link.ts

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -467,28 +467,30 @@ function getReferenceLinkTitleSync(href: string) {
467467
}
468468
}
469469

470-
// TODO: Check why the event is not supported.
471-
//@ts-ignore
472-
$(document).on("click", "a", goToLink);
473-
// TODO: Check why the event is not supported.
474-
//@ts-ignore
475-
$(document).on("auxclick", "a", goToLink); // to handle the middle button
476-
// TODO: Check why the event is not supported.
477-
//@ts-ignore
478-
$(document).on("contextmenu", "a", linkContextMenu);
479-
// TODO: Check why the event is not supported.
480-
//@ts-ignore
481-
$(document).on("dblclick", "a", goToLink);
482-
483-
$(document).on("mousedown", "a", (e) => {
484-
if (e.which === 2) {
485-
// prevent paste on middle click
486-
// https://github.com/zadam/trilium/issues/2995
487-
// https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event#preventing_default_actions
488-
e.preventDefault();
489-
return false;
490-
}
491-
});
470+
if (glob.device !== "print") {
471+
// TODO: Check why the event is not supported.
472+
//@ts-ignore
473+
$(document).on("click", "a", goToLink);
474+
// TODO: Check why the event is not supported.
475+
//@ts-ignore
476+
$(document).on("auxclick", "a", goToLink); // to handle the middle button
477+
// TODO: Check why the event is not supported.
478+
//@ts-ignore
479+
$(document).on("contextmenu", "a", linkContextMenu);
480+
// TODO: Check why the event is not supported.
481+
//@ts-ignore
482+
$(document).on("dblclick", "a", goToLink);
483+
484+
$(document).on("mousedown", "a", (e) => {
485+
if (e.which === 2) {
486+
// prevent paste on middle click
487+
// https://github.com/zadam/trilium/issues/2995
488+
// https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event#preventing_default_actions
489+
e.preventDefault();
490+
return false;
491+
}
492+
});
493+
}
492494

493495
export default {
494496
getNotePathFromUrl,

apps/client/src/widgets/collections/NoteList.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { subscribeToMessages, unsubscribeToMessage as unsubscribeFromMessage } f
1313
import { WebSocketMessage } from "@triliumnext/commons";
1414
import froca from "../../services/froca";
1515
import PresentationView from "./presentation";
16+
import { ListPrintView } from "./legacy/ListPrintView";
1617

1718
interface NoteListProps {
1819
note: FNote | null | undefined;
@@ -103,7 +104,11 @@ export function CustomNoteList({ note, viewType, isEnabled: shouldEnable, notePa
103104
function getComponentByViewType(viewType: ViewTypeOptions, props: ViewModeProps<any>) {
104105
switch (viewType) {
105106
case "list":
106-
return <ListView {...props} />;
107+
if (props.media !== "print") {
108+
return <ListView {...props} />;
109+
} else {
110+
return <ListPrintView {...props} />;
111+
}
107112
case "grid":
108113
return <GridView {...props} />;
109114
case "geoMap":

apps/client/src/widgets/collections/legacy/ListOrGridView.tsx

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
1+
import { useEffect, useRef, useState } from "preact/hooks";
22
import FNote from "../../../entities/fnote";
33
import Icon from "../../react/Icon";
44
import { ViewModeProps } from "../interface";
@@ -11,6 +11,7 @@ import tree from "../../../services/tree";
1111
import link from "../../../services/link";
1212
import { t } from "../../../services/i18n";
1313
import attribute_renderer from "../../../services/attribute_renderer";
14+
import { filterChildNotes, useFilteredNoteIds } from "./utils";
1415

1516
export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }: ViewModeProps<{}>) {
1617
const [ isExpanded ] = useNoteLabelBoolean(note, "expanded");
@@ -160,30 +161,15 @@ function NoteContent({ note, trim, noChildrenList, highlightedTokens }: { note:
160161
}
161162

162163
function NoteChildren({ note, parentNote, highlightedTokens }: { note: FNote, parentNote: FNote, highlightedTokens: string[] | null | undefined }) {
163-
const imageLinks = note.getRelations("imageLink");
164164
const [ childNotes, setChildNotes ] = useState<FNote[]>();
165165

166166
useEffect(() => {
167-
note.getChildNotes().then(childNotes => {
168-
const filteredChildNotes = childNotes.filter((childNote) => !imageLinks.find((rel) => rel.value === childNote.noteId));
169-
setChildNotes(filteredChildNotes);
170-
});
167+
filterChildNotes(note).then(setChildNotes);
171168
}, [ note ]);
172169

173170
return childNotes?.map(childNote => <ListNoteCard note={childNote} parentNote={parentNote} highlightedTokens={highlightedTokens} />)
174171
}
175172

176-
/**
177-
* 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.
178-
*/
179-
function useFilteredNoteIds(note: FNote, noteIds: string[]) {
180-
return useMemo(() => {
181-
const includedLinks = note ? note.getRelations().filter((rel) => rel.name === "imageLink" || rel.name === "includeNoteLink") : [];
182-
const includedNoteIds = new Set(includedLinks.map((rel) => rel.value));
183-
return noteIds.filter((noteId) => !includedNoteIds.has(noteId) && noteId !== "_hidden");
184-
}, noteIds);
185-
}
186-
187173
function getNotePath(parentNote: FNote, childNote: FNote) {
188174
if (parentNote.type === "search") {
189175
// for search note parent, we want to display a non-search path
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { useEffect, useLayoutEffect, useState } from "preact/hooks";
2+
import froca from "../../../services/froca";
3+
import type FNote from "../../../entities/fnote";
4+
import content_renderer from "../../../services/content_renderer";
5+
import type { ViewModeProps } from "../interface";
6+
import { filterChildNotes, useFilteredNoteIds } from "./utils";
7+
8+
interface NotesWithContent {
9+
note: FNote;
10+
contentEl: HTMLElement;
11+
}
12+
13+
export function ListPrintView({ note, noteIds: unfilteredNoteIds, onReady }: ViewModeProps<{}>) {
14+
const noteIds = useFilteredNoteIds(note, unfilteredNoteIds);
15+
const [ notesWithContent, setNotesWithContent ] = useState<NotesWithContent[]>();
16+
17+
useLayoutEffect(() => {
18+
const noteIdsSet = new Set<string>();
19+
20+
froca.getNotes(noteIds).then(async (notes) => {
21+
const notesWithContent: NotesWithContent[] = [];
22+
23+
async function processNote(note: FNote, depth: number) {
24+
const content = await content_renderer.getRenderedContent(note, {
25+
trim: false,
26+
noChildrenList: true
27+
});
28+
29+
const contentEl = content.$renderedContent[0];
30+
31+
insertPageTitle(contentEl, note.title);
32+
rewriteHeadings(contentEl, depth);
33+
noteIdsSet.add(note.noteId);
34+
notesWithContent.push({ note, contentEl });
35+
36+
if (note.hasChildren()) {
37+
const filteredChildNotes = await filterChildNotes(note);
38+
for (const childNote of filteredChildNotes) {
39+
await processNote(childNote, depth + 1);
40+
}
41+
}
42+
}
43+
44+
for (const note of notes) {
45+
await processNote(note, 1);
46+
}
47+
48+
// After all notes are processed, rewrite links
49+
for (const { contentEl } of notesWithContent) {
50+
rewriteLinks(contentEl, noteIdsSet);
51+
}
52+
53+
setNotesWithContent(notesWithContent);
54+
});
55+
}, [noteIds]);
56+
57+
useEffect(() => {
58+
if (notesWithContent && onReady) {
59+
onReady();
60+
}
61+
}, [ notesWithContent, onReady ]);
62+
63+
return (
64+
<div class="note-list list-print-view">
65+
<div class="note-list-container use-tn-links">
66+
<h1>{note.title}</h1>
67+
68+
{notesWithContent?.map(({ note: childNote, contentEl }) => (
69+
<section id={`note-${childNote.noteId}`} class="note" dangerouslySetInnerHTML={{ __html: contentEl.innerHTML }} />
70+
))}
71+
</div>
72+
</div>
73+
);
74+
}
75+
76+
function insertPageTitle(contentEl: HTMLElement, title: string) {
77+
const pageTitleEl = document.createElement("h1");
78+
pageTitleEl.textContent = title;
79+
contentEl.prepend(pageTitleEl);
80+
}
81+
82+
function rewriteHeadings(contentEl: HTMLElement, depth: number) {
83+
const headings = contentEl.querySelectorAll("h1, h2, h3, h4, h5, h6");
84+
for (const headingEl of headings) {
85+
const currentLevel = parseInt(headingEl.tagName.substring(1), 10);
86+
const newLevel = Math.min(currentLevel + depth, 6);
87+
const newHeadingEl = document.createElement(`h${newLevel}`);
88+
newHeadingEl.innerHTML = headingEl.innerHTML;
89+
headingEl.replaceWith(newHeadingEl);
90+
}
91+
}
92+
93+
function rewriteLinks(contentEl: HTMLElement, noteIdsSet: Set<string>) {
94+
const linkEls = contentEl.querySelectorAll("a");
95+
for (const linkEl of linkEls) {
96+
const href = linkEl.getAttribute("href");
97+
if (href && href.startsWith("#root/")) {
98+
const noteId = href.split("/").at(-1);
99+
100+
if (noteId && noteIdsSet.has(noteId)) {
101+
linkEl.setAttribute("href", `#note-${noteId}`);
102+
} else {
103+
// Link to note not in the print view, remove link but keep text
104+
const spanEl = document.createElement("span");
105+
spanEl.innerHTML = linkEl.innerHTML;
106+
linkEl.replaceWith(spanEl);
107+
}
108+
}
109+
}
110+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { useMemo } from "preact/hooks";
2+
import FNote from "../../../entities/fnote";
3+
4+
/**
5+
* 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.
6+
*/
7+
export function useFilteredNoteIds(note: FNote, noteIds: string[]) {
8+
return useMemo(() => {
9+
const includedLinks = note ? note.getRelations().filter((rel) => rel.name === "imageLink" || rel.name === "includeNoteLink") : [];
10+
const includedNoteIds = new Set(includedLinks.map((rel) => rel.value));
11+
return noteIds.filter((noteId) => !includedNoteIds.has(noteId) && noteId !== "_hidden");
12+
}, [ note, noteIds ]);
13+
}
14+
15+
export async function filterChildNotes(note: FNote) {
16+
const imageLinks = note.getRelations("imageLink");
17+
const imageLinkNoteIds = new Set(imageLinks.map(rel => rel.value));
18+
const childNotes = await note.getChildNotes();
19+
return childNotes.filter((childNote) => !imageLinkNoteIds.has(childNote.noteId));
20+
}

apps/client/src/widgets/ribbon/NoteActions.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
4949
const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment();
5050
const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(note.type);
5151
const isInOptionsOrHelp = note?.noteId.startsWith("_options") || note?.noteId.startsWith("_help");
52-
const isPrintable = ["text", "code"].includes(note.type) || (note.type === "book" && note.getLabelValue("viewType") === "presentation");
52+
const isPrintable = ["text", "code"].includes(note.type) || (note.type === "book" && ["presentation", "list"].includes(note.getLabelValue("viewType") ?? ""));
5353
const isElectron = getIsElectron();
5454
const isMac = getIsMac();
5555
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "aiChat"].includes(note.type);

apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/Notes/Printing & Exporting as PDF.html

Lines changed: 37 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)