Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to
### Added

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

### Changed

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { SpecificBlock } from '@blocknote/core';
import { DOCXExporter } from '@blocknote/xl-docx-exporter';
import { ODTExporter } from '@blocknote/xl-odt-exporter';
import { PDFExporter } from '@blocknote/xl-pdf-exporter';
import {
Button,
Checkbox,
Loader,
Modal,
ModalSize,
Expand All @@ -20,9 +22,15 @@ import { css } from 'styled-components';

import { Box, ButtonCloseModal, Text } from '@/components';
import { useMediaUrl } from '@/core';
import { useEditorStore } from '@/docs/doc-editor';
import {
DocsBlockSchema,
DocsInlineContentSchema,
DocsStyleSchema,
useEditorStore,
} from '@/docs/doc-editor';
import { Doc, useTrans } from '@/docs/doc-management';
import { fallbackLng } from '@/i18n/config';
import { safeLocalStorage } from '@/utils/storages';

import { exportCorsResolveFileUrl } from '../api/exportResolveFileUrl';
import { docxDocsSchemaMappings } from '../mappingDocx';
Expand Down Expand Up @@ -57,6 +65,18 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
const [format, setFormat] = useState<DocDownloadFormat>(
DocDownloadFormat.PDF,
);
const documentHasH1 =
editor?.document.some(
(block) => block.type === 'heading' && block.props.level === 1,
) ?? false;

const [withTitle, setWithTitle] = useState(() => {
const stored = safeLocalStorage.getItem(`export-with-title-${doc.id}`);
if (stored === null) {
return !documentHasH1;
}
return stored !== 'false';
});
const { untitledDocument } = useTrans();
const mediaUrl = useMediaUrl();

Expand Down Expand Up @@ -84,7 +104,27 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {

const documentTitle = doc.title || untitledDocument;

const exportDocument = editor.document;
const titleBlock: SpecificBlock<
DocsBlockSchema,
'heading',
DocsInlineContentSchema,
DocsStyleSchema
> = {
id: crypto.randomUUID(),
type: 'heading',
props: {
level: 1,
textColor: 'default',
backgroundColor: 'default',
textAlignment: 'left',
},
content: [{ type: 'text', text: documentTitle, styles: {} }],
children: [],
};

const exportDocument = withTitle
? [titleBlock, ...editor.document]
: editor.document;
let blobExport: Blob;
if (format === DocDownloadFormat.PDF) {
const exporter = new PDFExporter(editor.schema, pdfDocsSchemaMappings, {
Expand Down Expand Up @@ -135,15 +175,15 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
blobExport = await exporter.toODTDocument(exportDocument);
} else if (format === DocDownloadFormat.HTML) {
// Use BlockNote "full HTML" export so that we stay closer to the editor rendering.
const fullHtml = await editor.blocksToFullHTML();
const fullHtml = await editor.blocksToFullHTML(exportDocument);

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

const zip = new JSZip();

improveHtmlAccessibility(parsedDocument, documentTitle);
improveHtmlAccessibility(parsedDocument);
await addMediaFilesToZip(parsedDocument, zip, mediaUrl);

const lang = i18next.language || fallbackLng;
Expand Down Expand Up @@ -273,6 +313,20 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
}
/>

{format !== DocDownloadFormat.PRINT && (
<Checkbox
label={t('Include document title')}
checked={withTitle}
onChange={(e) => {
setWithTitle(e.target.checked);
safeLocalStorage.setItem(
`export-with-title-${doc.id}`,
String(e.target.checked),
);
}}
/>
)}

{isExporting && (
<Box
$align="center"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,7 @@ export const generateHtmlDocument = (
* - We work directly on the parsed Document so modifications are reflected before we zip files.
* - We keep the editor inner structure but upgrade the key block types to native elements.
*/
export const improveHtmlAccessibility = (
parsedDocument: Document,
documentTitle: string,
) => {
export const improveHtmlAccessibility = (parsedDocument: Document) => {
const body = parsedDocument.body;
if (!body) {
return;
Expand Down Expand Up @@ -362,19 +359,18 @@ export const improveHtmlAccessibility = (

// 8) Wrap content in an article with a title landmark if none exists
const existingH1 = body.querySelector('h1');
if (!existingH1) {
const titleHeading = parsedDocument.createElement('h1');
titleHeading.id = 'doc-title';
titleHeading.textContent = documentTitle;
body.insertBefore(titleHeading, body.firstChild);
if (existingH1) {
existingH1.id = 'doc-title';
}

// If there is no article, group the body content inside one for better semantics.
const hasArticle = body.querySelector('article');
if (!hasArticle) {
const article = parsedDocument.createElement('article');
article.setAttribute('role', 'document');
article.setAttribute('aria-labelledby', 'doc-title');
if (existingH1) {
article.setAttribute('aria-labelledby', 'doc-title');
}
while (body.firstChild) {
article.appendChild(body.firstChild);
}
Expand Down