Merge pull request #1091 from TriliumNext/feature/different_printing_mechanism

Export as PDF
This commit is contained in:
Elian Doran
2025-02-01 01:35:43 +02:00
committed by GitHub
20 changed files with 311 additions and 130 deletions
+4
View File
@@ -81,6 +81,10 @@ export type CommandMappings = {
showOptions: CommandData & {
section: string;
};
showExportDialog: CommandData & {
notePath: string;
defaultType: "single";
};
showDeleteNotesDialog: CommandData & {
branchIdsToDelete: string[];
callback: (value: ResolveOptions) => void;
@@ -51,10 +51,6 @@ const RELATION_MAP: Library = {
css: ["stylesheets/relation_map.css"]
};
const PRINT_THIS: Library = {
js: ["node_modules/print-this/printThis.js"]
};
const CALENDAR_WIDGET: Library = {
css: ["stylesheets/calendar.css"]
};
@@ -193,7 +189,6 @@ export default {
CODE_MIRROR,
ESLINT,
RELATION_MAP,
PRINT_THIS,
CALENDAR_WIDGET,
KATEX,
WHEEL_ZOOM,
@@ -5,8 +5,15 @@ import dialogService from "../../services/dialog.js";
import server from "../../services/server.js";
import toastService from "../../services/toast.js";
import ws from "../../services/ws.js";
import appContext from "../../components/app_context.js";
import appContext, { type EventData } from "../../components/app_context.js";
import { t } from "../../services/i18n.js";
import type FNote from "../../entities/fnote.js";
import type { FAttachmentRow } from "../../entities/fattachment.js";
// TODO: Deduplicate with server
interface ConvertToAttachmentResponse {
attachment: FAttachmentRow;
}
const TPL = `
<div class="dropdown note-actions">
@@ -52,8 +59,12 @@ const TPL = `
</li>
<li data-trigger-command="printActiveNote" class="dropdown-item print-active-note-button">
<span class="bx bx-printer"></span> ${t("note_actions.print_note")}<kbd data-command="printActiveNote"></kbd></li>
<span class="bx bx-printer"></span> ${t("note_actions.print_note")}<kbd data-command="printActiveNote"></kbd>
</li>
<li data-trigger-command="exportAsPdf" class="dropdown-item export-as-pdf-button">
<span class="bx bxs-file-pdf"></span> ${t("note_actions.print_pdf")}<kbd data-command="exportAsPdf"></kbd>
</li>
<div class="dropdown-divider"></div>
@@ -100,17 +111,37 @@ const TPL = `
</div>`;
export default class NoteActionsWidget extends NoteContextAwareWidget {
private $convertNoteIntoAttachmentButton!: JQuery<HTMLElement>;
private $findInTextButton!: JQuery<HTMLElement>;
private $printActiveNoteButton!: JQuery<HTMLElement>;
private $exportAsPdfButton!: JQuery<HTMLElement>;
private $showSourceButton!: JQuery<HTMLElement>;
private $showAttachmentsButton!: JQuery<HTMLElement>;
private $renderNoteButton!: JQuery<HTMLElement>;
private $saveRevisionButton!: JQuery<HTMLElement>;
private $exportNoteButton!: JQuery<HTMLElement>;
private $importNoteButton!: JQuery<HTMLElement>;
private $openNoteExternallyButton!: JQuery<HTMLElement>;
private $openNoteCustomButton!: JQuery<HTMLElement>;
private $deleteNoteButton!: JQuery<HTMLElement>;
isEnabled() {
return this.note?.type !== "launcher";
}
doRender() {
this.$widget = $(TPL);
this.$widget.on("show.bs.dropdown", () => this.refreshVisibility(this.note));
this.$widget.on("show.bs.dropdown", () => {
if (this.note) {
this.refreshVisibility(this.note);
}
});
this.$convertNoteIntoAttachmentButton = this.$widget.find("[data-trigger-command='convertNoteIntoAttachment']");
this.$findInTextButton = this.$widget.find(".find-in-text-button");
this.$printActiveNoteButton = this.$widget.find(".print-active-note-button");
this.$exportAsPdfButton = this.$widget.find(".export-as-pdf-button");
this.$showSourceButton = this.$widget.find(".show-source-button");
this.$showAttachmentsButton = this.$widget.find(".show-attachments-button");
this.$renderNoteButton = this.$widget.find(".render-note-button");
@@ -118,7 +149,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
this.$exportNoteButton = this.$widget.find(".export-note-button");
this.$exportNoteButton.on("click", () => {
if (this.$exportNoteButton.hasClass("disabled")) {
if (this.$exportNoteButton.hasClass("disabled") || !this.noteContext?.notePath) {
return;
}
@@ -129,7 +160,11 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
});
this.$importNoteButton = this.$widget.find(".import-files-button");
this.$importNoteButton.on("click", () => this.triggerCommand("showImportDialog", { noteId: this.noteId }));
this.$importNoteButton.on("click", () => {
if (this.noteId) {
this.triggerCommand("showImportDialog", { noteId: this.noteId });
}
});
this.$widget.on("click", ".dropdown-item", () => this.$widget.find("[data-bs-toggle='dropdown']").dropdown("toggle"));
@@ -138,7 +173,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
this.$deleteNoteButton = this.$widget.find(".delete-note-button");
this.$deleteNoteButton.on("click", () => {
if (this.note.noteId === "root") {
if (!this.note || this.note.noteId === "root") {
return;
}
@@ -146,7 +181,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
});
}
async refreshVisibility(note) {
async refreshVisibility(note: FNote) {
const isInOptions = note.noteId.startsWith("_options");
this.$convertNoteIntoAttachmentButton.toggle(note.isEligibleForConversionToAttachment());
@@ -156,7 +191,10 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
this.toggleDisabled(this.$showAttachmentsButton, !isInOptions);
this.toggleDisabled(this.$showSourceButton, ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "geoMap"].includes(note.type));
this.toggleDisabled(this.$printActiveNoteButton, ["text", "code"].includes(note.type));
const canPrint = ["text", "code"].includes(note.type);
this.toggleDisabled(this.$printActiveNoteButton, canPrint);
this.toggleDisabled(this.$exportAsPdfButton, canPrint);
this.$exportAsPdfButton.toggleClass("hidden-ext", !utils.isElectron());
this.$renderNoteButton.toggle(note.type === "render");
@@ -177,11 +215,11 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
}
async convertNoteIntoAttachmentCommand() {
if (!(await dialogService.confirm(t("note_actions.convert_into_attachment_prompt", { title: this.note.title })))) {
if (!this.note || !(await dialogService.confirm(t("note_actions.convert_into_attachment_prompt", { title: this.note.title })))) {
return;
}
const { attachment: newAttachment } = await server.post(`notes/${this.noteId}/convert-to-attachment`);
const { attachment: newAttachment } = await server.post<ConvertToAttachmentResponse>(`notes/${this.noteId}/convert-to-attachment`);
if (!newAttachment) {
toastService.showMessage(t("note_actions.convert_into_attachment_failed", { title: this.note.title }));
@@ -198,7 +236,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
});
}
toggleDisabled($el, enable) {
toggleDisabled($el: JQuery<HTMLElement>, enable: boolean) {
if (enable) {
$el.removeAttr("disabled");
} else {
@@ -206,7 +244,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
}
}
entitiesReloadedEvent({ loadResults }) {
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isNoteReloaded(this.noteId)) {
this.refresh();
}
+10 -36
View File
@@ -32,6 +32,7 @@ import AttachmentDetailTypeWidget from "./type_widgets/attachment_detail.js";
import MindMapWidget from "./type_widgets/mind_map.js";
import { getStylesheetUrl, isSyntaxHighlightEnabled } from "../services/syntax_highlight.js";
import GeoMapTypeWidget from "./type_widgets/geo_map.js";
import utils from "../services/utils.js";
const TPL = `
<div class="note-detail">
@@ -249,45 +250,18 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
return;
}
await libraryLoader.requireLibrary(libraryLoader.PRINT_THIS);
window.print();
}
let $promotedAttributes = $("");
if (this.note.getPromotedDefinitionAttributes().length > 0) {
$promotedAttributes = (await attributeRenderer.renderNormalAttributes(this.note)).$renderedAttributes;
async exportAsPdfEvent() {
if (!this.noteContext.isActive()) {
return;
}
const { assetPath } = window.glob;
const cssToLoad = [
`${assetPath}/node_modules/codemirror/lib/codemirror.css`,
`${assetPath}/libraries/ckeditor/ckeditor-content.css`,
`${assetPath}/node_modules/bootstrap/dist/css/bootstrap.min.css`,
`${assetPath}/node_modules/katex/dist/katex.min.css`,
`${assetPath}/stylesheets/print.css`,
`${assetPath}/stylesheets/relation_map.css`,
`${assetPath}/stylesheets/ckeditor-theme.css`
];
if (isSyntaxHighlightEnabled()) {
cssToLoad.push(getStylesheetUrl("default:vs"));
}
this.$widget.find(".note-detail-printable:visible").printThis({
header: $("<div>").append($("<h2>").text(this.note.title)).append($promotedAttributes).prop("outerHTML"),
footer: `
<script src="${assetPath}/node_modules/katex/dist/katex.min.js"></script>
<script src="${assetPath}/node_modules/katex/dist/contrib/mhchem.min.js"></script>
<script src="${assetPath}/node_modules/katex/dist/contrib/auto-render.min.js"></script>
<script>
document.body.className += ' ck-content printed-content';
renderMathInElement(document.body, {trust: true});
</script>
`,
importCSS: false,
loadCSS: cssToLoad,
debug: true
const { ipcRenderer } = utils.dynamicRequire("electron");
ipcRenderer.send("export-as-pdf", {
title: this.note.title,
landscape: this.note.hasAttribute("label", "printLandscape")
});
}
+161 -39
View File
@@ -1,40 +1,162 @@
@media print {
html body {
/* https://github.com/zadam/trilium/issues/3202 */
color: black;
}
.no-print,
.no-print * {
display: none !important;
}
.relation-map-wrapper {
height: 100vh !important;
}
.table thead th,
.table td,
.table th {
/* Fix center vertical alignment of table cells */
vertical-align: middle;
}
pre {
box-shadow: unset !important;
border: 0.75pt solid gray !important;
border-radius: 2pt !important;
}
span[style] {
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
/* Fix visibility of checkbox checkmarks
see https://github.com/TriliumNext/Notes/issues/901 */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable="false"] > input[checked]::after {
/* fallback to default ck-editor green */
border-color: hsl(126, 64%, 41%);
}
:root {
--main-background-color: white;
--root-background: var(--main-background-color);
--launcher-pane-background-color: var(--main-background-color);
--main-text-color: black;
--input-text-color: var(--main-text-color);
}
.no-print,
.no-print *,
.tab-row-container,
.tab-row-widget,
#launcher-pane,
#left-pane,
#right-pane,
.title-row .note-icon-widget,
.title-row .button-widget,
.ribbon-container,
.promoted-attributes-widget,
.scroll-padding-widget,
.note-list-widget,
.spacer {
display: none !important;
}
body.mobile #mobile-sidebar-wrapper,
body.mobile .classic-toolbar-widget,
body.mobile .action-button {
display: none !important;
}
body.mobile #detail-container {
max-height: unset;
}
body.mobile .note-title-widget {
padding: 0 !important;
}
body,
#root-widget,
#rest-pane > div.component:first-child,
.note-detail-printable,
.note-detail-editable-text-editor {
height: unset !important;
overflow: auto;
}
.note-title-widget input,
.note-detail-editable-text,
.note-detail-editable-text-editor {
padding: 0 !important;
}
html,
body {
width: unset !important;
height: unset !important;
overflow: visible;
position: unset;
/* https://github.com/zadam/trilium/issues/3202 */
color: black;
}
#root-widget,
#horizontal-main-container,
#rest-pane,
#vertical-main-container,
#center-pane,
.split-note-container-widget,
.note-split:not(.hidden-ext),
body.mobile #mobile-rest-container {
display: block !important;
overflow: auto;
border-radius: 0 !important;
}
#center-pane,
#rest-pane,
.note-split,
body.mobile #detail-container {
width: unset !important;
max-width: unset !important;
}
.component {
contain: none !important;
}
/* Respect page breaks */
.page-break {
page-break-after: always;
break-after: always;
}
.page-break > * {
display: none !important;
}
.relation-map-wrapper {
height: 100vh !important;
}
.table thead th,
.table td,
.table th {
/* Fix center vertical alignment of table cells */
vertical-align: middle;
}
pre {
box-shadow: unset !important;
border: 0.75pt solid gray !important;
border-radius: 2pt !important;
}
th,
span[style] {
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
/*
* Text note specific fixes
*/
.ck-widget {
outline: none !important;
}
.ck-placeholder,
.ck-widget__type-around,
.ck-widget__selection-handle {
display: none !important;
}
.ck-widget.table td.ck-editor__nested-editable.ck-editor__nested-editable_focused,
.ck-widget.table td.ck-editor__nested-editable:focus,
.ck-widget.table th.ck-editor__nested-editable.ck-editor__nested-editable_focused,
.ck-widget.table th.ck-editor__nested-editable:focus {
background: unset !important;
outline: unset !important;
}
/* Fix visibility of checkbox checkmarks
see https://github.com/TriliumNext/Notes/issues/901 */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable="false"] > input[checked]::after {
/* fallback to default ck-editor green */
border-color: hsl(126, 64%, 41%);
}
.include-note .include-note-content {
max-height: unset !important;
overflow: unset !important;
}
/*
* Code note specific fixes.
*/
.note-detail-code pre {
border: unset !important;
border-radius: unset !important;
}
+1 -1
View File
@@ -1604,4 +1604,4 @@ body.electron.platform-darwin:not(.native-titlebar) .tab-row-container {
border-color: var(--hover-item-border-color);
background: var(--hover-item-background-color);
color: var(--hover-item-text-color);
}
}
@@ -104,11 +104,13 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child {
opacity: 1;
}
.ck-content p code {
border: 1px solid var(--card-border-color);
box-shadow: var(--card-box-shadow);
border-radius: 6px;
background-color: var(--card-background-color);
@media (screen) {
.ck-content p code {
border: 1px solid var(--card-border-color);
box-shadow: var(--card-box-shadow);
border-radius: 6px;
background-color: var(--card-background-color);
}
}
.note-detail-printable:not(.word-wrap) pre code {
+2 -1
View File
@@ -672,7 +672,8 @@
"save_revision": "Save revision",
"convert_into_attachment_failed": "Converting note '{{title}}' failed.",
"convert_into_attachment_successful": "Note '{{title}}' has been converted to attachment.",
"convert_into_attachment_prompt": "Are you sure you want to convert note '{{title}}' into an attachment of the parent note?"
"convert_into_attachment_prompt": "Are you sure you want to convert note '{{title}}' into an attachment of the parent note?",
"print_pdf": "Export as PDF..."
},
"onclick_button": {
"no_click_handler": "Button widget '{{componentId}}' has no defined click handler"
+2 -1
View File
@@ -824,7 +824,8 @@
"search_in_note": "Caută în notiță",
"convert_into_attachment_failed": "Nu s-a putut converti notița „{{title}}”.",
"convert_into_attachment_successful": "Notița „{{title}}” a fost convertită în atașament.",
"convert_into_attachment_prompt": "Doriți convertirea notiței „{{title}}” într-un atașament al notiței părinte?"
"convert_into_attachment_prompt": "Doriți convertirea notiței „{{title}}” într-un atașament al notiței părinte?",
"print_pdf": "Exportare ca PDF..."
},
"note_erasure_timeout": {
"deleted_notes_erased": "Notițele șterse au fost eliminate permanent.",
-2
View File
@@ -66,8 +66,6 @@ async function register(app: express.Application) {
app.use(`/${assetPath}/node_modules/jquery-hotkeys/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/jquery-hotkeys/")));
app.use(`/${assetPath}/node_modules/print-this/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/print-this/")));
app.use(`/${assetPath}/node_modules/split.js/dist/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/split.js/dist/")));
app.use(`/${assetPath}/node_modules/panzoom/dist/`, persistentCacheStatic(path.join(srcRoot, "..", "node_modules/panzoom/dist/")));
+6
View File
@@ -503,6 +503,12 @@ function getDefaultKeyboardActions() {
description: t("keyboard_actions.print-active-note"),
scope: "window"
},
{
actionName: "exportAsPdf",
defaultShortcuts: [],
description: t("keyboard_actions.export-as-pdf"),
scope: "window"
},
{
actionName: "openNoteExternally",
defaultShortcuts: [],
@@ -75,6 +75,7 @@ const enum KeyboardActionNamesEnum {
toggleRibbonTabSimilarNotes,
toggleRightPane,
printActiveNote,
exportAsPdf,
openNoteExternally,
renderActiveNote,
runActiveNote,
+49 -3
View File
@@ -1,3 +1,4 @@
import fs from "fs/promises";
import path from "path";
import url from "url";
import port from "./port.js";
@@ -7,12 +8,13 @@ import sqlInit from "./sql_init.js";
import cls from "./cls.js";
import keyboardActionsService from "./keyboard_actions.js";
import remoteMain from "@electron/remote/main/index.js";
import type { App, BrowserWindow, BrowserWindowConstructorOptions, WebContents } from "electron";
import { ipcMain } from "electron";
import { isDev, isMac, isWindows } from "./utils.js";
import { BrowserWindow, shell, type App, type BrowserWindowConstructorOptions, type WebContents } from "electron";
import { dialog, ipcMain } from "electron";
import { formatDownloadTitle, isDev, isMac, isWindows } from "./utils.js";
import { fileURLToPath } from "url";
import { dirname } from "path";
import { t } from "i18next";
// Prevent the window being garbage collected
let mainWindow: BrowserWindow | null;
@@ -46,6 +48,50 @@ ipcMain.on("create-extra-window", (event, arg) => {
createExtraWindow(arg.extraWindowHash);
});
interface ExportAsPdfOpts {
title: string;
landscape: boolean;
}
ipcMain.on("export-as-pdf", async (e, opts: ExportAsPdfOpts) => {
const browserWindow = BrowserWindow.fromWebContents(e.sender);
if (!browserWindow) {
return;
}
const filePath = dialog.showSaveDialogSync(browserWindow, {
defaultPath: formatDownloadTitle(opts.title, "file", "application/pdf"),
filters: [
{
name: t("pdf.export_filter"),
extensions: [ "pdf" ]
}
]
});
if (!filePath) {
return;
}
let buffer: Buffer;
try {
buffer = await browserWindow.webContents.printToPDF({
landscape: opts.landscape
});
} catch (e) {
dialog.showErrorBox(t("pdf.unable-to-export-title"), t("pdf.unable-to-export-message"));
return;
}
try {
await fs.writeFile(filePath, buffer);
} catch (e) {
dialog.showErrorBox(t("pdf.unable-to-export-title"), t("pdf.unable-to-save-message"));
return;
}
shell.openPath(filePath);
});
async function createMainWindow(app: App) {
if ("setUserTasks" in app) {
app.setUserTasks([
+1
View File
@@ -62,6 +62,7 @@
<% } %>
<link href="<%= assetPath %>/stylesheets/style.css" rel="stylesheet">
<link href="<%= assetPath %>/stylesheets/print.css" rel="stylesheet" media="print">
<script>
$("body").show();
+1
View File
@@ -129,6 +129,7 @@
<link href="<%= themeCssUrl %>" rel="stylesheet">
<% } %>
<link href="<%= assetPath %>/stylesheets/style.css" rel="stylesheet">
<link href="<%= assetPath %>/stylesheets/print.css" rel="stylesheet" media="print">
<link rel="stylesheet" type="text/css" href="<%= assetPath %>/node_modules/boxicons/css/boxicons.min.css">