Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Export as PDF #1091

Merged
merged 37 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
d5d7c15
feat(print): implement stylesheet for global note printing
eliandoran Jan 31, 2025
c06642b
fix(print): remove extra spacing
eliandoran Jan 31, 2025
7dfba9f
feat(print): respect page breaks
eliandoran Jan 31, 2025
d201fe7
fix(print): left padding of text notes
eliandoran Jan 31, 2025
4a5bf28
fix(print): remove title & reduce paddings
eliandoran Jan 31, 2025
561c507
feat(print): carry over original print stylesheet
eliandoran Jan 31, 2025
298634c
refactor(print): use dedicated print CSS
eliandoran Jan 31, 2025
e3dbe21
chore(client/ts): port note_actions
eliandoran Jan 31, 2025
dbf004d
feat(print): add option to print as PDF (not yet implemented)
eliandoran Jan 31, 2025
0f7826d
feat(print): implement basic export as PDF support
eliandoran Jan 31, 2025
414a4d7
feat(print): add filter for PDF
eliandoran Jan 31, 2025
6152185
feat(pdf): suggest proper file name
eliandoran Jan 31, 2025
e029a39
fix(pdf): not disabled for unprintable note types
eliandoran Jan 31, 2025
edaf030
chore(pdf: change icon
eliandoran Jan 31, 2025
8ac1bea
chore(i18n): translate one message
eliandoran Jan 31, 2025
508ce4c
feat(pdf): open the exported file
eliandoran Jan 31, 2025
7813c89
feat(pdf): hide the option if not electron
eliandoran Jan 31, 2025
d4965e8
feat(pdf): improve save filter on Windows
eliandoran Jan 31, 2025
84532d4
feat(pdf): error handling
eliandoran Jan 31, 2025
143217b
fix(print): background color
eliandoran Jan 31, 2025
30b1da0
fix(print): tabs sometimes visible
eliandoran Jan 31, 2025
cd03133
fix(print): disable rounded corners on some containers
eliandoran Jan 31, 2025
3720ca6
fix(print): hide promoted attributes, note list
eliandoran Jan 31, 2025
ae5a9df
fix(print): sidebar reduces width
eliandoran Jan 31, 2025
3927548
fix(print): width is not always 100%
eliandoran Jan 31, 2025
d1a5d31
feat(print): improve rendering of tables
eliandoran Jan 31, 2025
bfadd36
style(next): don't print box shadow for code
eliandoran Jan 31, 2025
658ce10
feat(pdf): maintain table heading background
eliandoran Jan 31, 2025
f3a3906
feat(pdf): support landscape mode at note level
eliandoran Jan 31, 2025
719046e
feat(pdf): add keyboard shortcut
eliandoran Jan 31, 2025
e201800
chore(i18n): translate new messages to Romanian
eliandoran Jan 31, 2025
ac5f911
fix(print): include note not always full-height
eliandoran Jan 31, 2025
2cc7113
refactor(deps): get rid of print-this
eliandoran Jan 31, 2025
8e88645
feat(print): hide placeholder if note is empty
eliandoran Jan 31, 2025
6ad1919
feat(print): improve on mobile
eliandoran Jan 31, 2025
81755d8
feat(print): improve code notes
eliandoran Jan 31, 2025
bd363da
chore(review): use async
eliandoran Jan 31, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion bin/copy-dist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ const copy = async () => {
"node_modules/mermaid/dist/",
"node_modules/jquery/dist/",
"node_modules/jquery-hotkeys/",
"node_modules/print-this/",
"node_modules/split.js/dist/",
"node_modules/panzoom/dist/",
"node_modules/i18next/",
Expand Down
21 changes: 0 additions & 21 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,6 @@
"normalize-strings": "1.1.1",
"normalize.css": "8.0.1",
"panzoom": "9.4.3",
"print-this": "2.0.0",
"rand-token": "1.0.1",
"react": "18.3.1",
"react-dom": "18.3.1",
Expand Down
4 changes: 4 additions & 0 deletions src/public/app/components/app_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 0 additions & 5 deletions src/public/app/services/library_loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
};
Expand Down Expand Up @@ -193,7 +189,6 @@ export default {
CODE_MIRROR,
ESLINT,
RELATION_MAP,
PRINT_THIS,
CALENDAR_WIDGET,
KATEX,
WHEEL_ZOOM,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down Expand Up @@ -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>

Expand Down Expand Up @@ -100,25 +111,45 @@ 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");
this.$saveRevisionButton = this.$widget.find(".save-revision-button");

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

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

Expand All @@ -138,15 +173,15 @@ 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;
}

branchService.deleteNotes([this.note.getParentBranches()[0].branchId], true);
});
}

async refreshVisibility(note) {
async refreshVisibility(note: FNote) {
const isInOptions = note.noteId.startsWith("_options");

this.$convertNoteIntoAttachmentButton.toggle(note.isEligibleForConversionToAttachment());
Expand All @@ -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");

Expand All @@ -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 }));
Expand All @@ -198,15 +236,15 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
});
}

toggleDisabled($el, enable) {
toggleDisabled($el: JQuery<HTMLElement>, enable: boolean) {
if (enable) {
$el.removeAttr("disabled");
} else {
$el.attr("disabled", "disabled");
}
}

entitiesReloadedEvent({ loadResults }) {
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isNoteReloaded(this.noteId)) {
this.refresh();
}
Expand Down
46 changes: 10 additions & 36 deletions src/public/app/widgets/note_detail.js
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down Expand Up @@ -249,45 +250,18 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
return;
}

await libraryLoader.requireLibrary(libraryLoader.PRINT_THIS);

let $promotedAttributes = $("");

if (this.note.getPromotedDefinitionAttributes().length > 0) {
$promotedAttributes = (await attributeRenderer.renderNormalAttributes(this.note)).$renderedAttributes;
}
window.print();
}

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"));
async exportAsPdfEvent() {
if (!this.noteContext.isActive()) {
return;
}

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

Expand Down
Loading