Skip to content

Commit bafddd5

Browse files
committed
✨(frontend) Optionally include title in export
This commit introduces a checkbox in the export menu for inserting the document title Signed-off-by: Luc Haaijer <luc@haaijer.dev>
1 parent 6062d0e commit bafddd5

3 files changed

Lines changed: 63 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to
99
### Added
1010

1111
- ✨(helm) allow all keys in configMap as env var #1872
12+
- ✨(frontend) Optionally include title in export #1837
1213

1314
### Changed
1415

src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import { SpecificBlock } from '@blocknote/core';
12
import { DOCXExporter } from '@blocknote/xl-docx-exporter';
23
import { ODTExporter } from '@blocknote/xl-odt-exporter';
34
import { PDFExporter } from '@blocknote/xl-pdf-exporter';
45
import {
56
Button,
7+
Checkbox,
68
Loader,
79
Modal,
810
ModalSize,
@@ -20,9 +22,15 @@ import { css } from 'styled-components';
2022

2123
import { Box, ButtonCloseModal, Text } from '@/components';
2224
import { useMediaUrl } from '@/core';
23-
import { useEditorStore } from '@/docs/doc-editor';
25+
import {
26+
DocsBlockSchema,
27+
DocsInlineContentSchema,
28+
DocsStyleSchema,
29+
useEditorStore,
30+
} from '@/docs/doc-editor';
2431
import { Doc, useTrans } from '@/docs/doc-management';
2532
import { fallbackLng } from '@/i18n/config';
33+
import { safeLocalStorage } from '@/utils/storages';
2634

2735
import { exportCorsResolveFileUrl } from '../api/exportResolveFileUrl';
2836
import { docxDocsSchemaMappings } from '../mappingDocx';
@@ -57,6 +65,16 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
5765
const [format, setFormat] = useState<DocDownloadFormat>(
5866
DocDownloadFormat.PDF,
5967
);
68+
const documentHasH1 =
69+
editor?.document.some(
70+
(block) => block.type === 'heading' && block.props.level === 1,
71+
) ?? false;
72+
73+
const [withTitle, setWithTitle] = useState(() => {
74+
const stored = safeLocalStorage.getItem(`export-with-title-${doc.id}`);
75+
if (stored === null) return !documentHasH1;
76+
return stored !== 'false';
77+
});
6078
const { untitledDocument } = useTrans();
6179
const mediaUrl = useMediaUrl();
6280

@@ -84,7 +102,27 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
84102

85103
const documentTitle = doc.title || untitledDocument;
86104

87-
const exportDocument = editor.document;
105+
const titleBlock: SpecificBlock<
106+
DocsBlockSchema,
107+
'heading',
108+
DocsInlineContentSchema,
109+
DocsStyleSchema
110+
> = {
111+
id: crypto.randomUUID(),
112+
type: 'heading',
113+
props: {
114+
level: 1,
115+
textColor: 'default',
116+
backgroundColor: 'default',
117+
textAlignment: 'left',
118+
},
119+
content: [{ type: 'text', text: documentTitle, styles: {} }],
120+
children: [],
121+
};
122+
123+
const exportDocument = withTitle
124+
? [titleBlock, ...editor.document]
125+
: editor.document;
88126
let blobExport: Blob;
89127
if (format === DocDownloadFormat.PDF) {
90128
const exporter = new PDFExporter(editor.schema, pdfDocsSchemaMappings, {
@@ -135,15 +173,15 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
135173
blobExport = await exporter.toODTDocument(exportDocument);
136174
} else if (format === DocDownloadFormat.HTML) {
137175
// Use BlockNote "full HTML" export so that we stay closer to the editor rendering.
138-
const fullHtml = await editor.blocksToFullHTML();
176+
const fullHtml = await editor.blocksToFullHTML(exportDocument);
139177

140178
// Parse HTML and fetch media so that we can package a fully offline HTML document in a ZIP.
141179
const domParser = new DOMParser();
142180
const parsedDocument = domParser.parseFromString(fullHtml, 'text/html');
143181

144182
const zip = new JSZip();
145183

146-
improveHtmlAccessibility(parsedDocument, documentTitle);
184+
improveHtmlAccessibility(parsedDocument);
147185
await addMediaFilesToZip(parsedDocument, zip, mediaUrl);
148186

149187
const lang = i18next.language || fallbackLng;
@@ -273,6 +311,20 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
273311
}
274312
/>
275313

314+
{format !== DocDownloadFormat.PRINT && (
315+
<Checkbox
316+
label={t('Include document title')}
317+
checked={withTitle}
318+
onChange={(e) => {
319+
setWithTitle(e.target.checked);
320+
safeLocalStorage.setItem(
321+
`export-with-title-${doc.id}`,
322+
String(e.target.checked),
323+
);
324+
}}
325+
/>
326+
)}
327+
276328
{isExporting && (
277329
<Box
278330
$align="center"

src/frontend/apps/impress/src/features/docs/doc-export/utils_html.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,7 @@ export const generateHtmlDocument = (
128128
* - We work directly on the parsed Document so modifications are reflected before we zip files.
129129
* - We keep the editor inner structure but upgrade the key block types to native elements.
130130
*/
131-
export const improveHtmlAccessibility = (
132-
parsedDocument: Document,
133-
documentTitle: string,
134-
) => {
131+
export const improveHtmlAccessibility = (parsedDocument: Document) => {
135132
const body = parsedDocument.body;
136133
if (!body) {
137134
return;
@@ -362,19 +359,18 @@ export const improveHtmlAccessibility = (
362359

363360
// 8) Wrap content in an article with a title landmark if none exists
364361
const existingH1 = body.querySelector('h1');
365-
if (!existingH1) {
366-
const titleHeading = parsedDocument.createElement('h1');
367-
titleHeading.id = 'doc-title';
368-
titleHeading.textContent = documentTitle;
369-
body.insertBefore(titleHeading, body.firstChild);
362+
if (existingH1) {
363+
existingH1.id = 'doc-title';
370364
}
371365

372366
// If there is no article, group the body content inside one for better semantics.
373367
const hasArticle = body.querySelector('article');
374368
if (!hasArticle) {
375369
const article = parsedDocument.createElement('article');
376370
article.setAttribute('role', 'document');
377-
article.setAttribute('aria-labelledby', 'doc-title');
371+
if (existingH1) {
372+
article.setAttribute('aria-labelledby', 'doc-title');
373+
}
378374
while (body.firstChild) {
379375
article.appendChild(body.firstChild);
380376
}

0 commit comments

Comments
 (0)