diff --git a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx
index 5d5a280a..1f401819 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx
@@ -113,6 +113,12 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
showCursorLabels: showCursorLabels as 'always' | 'activity',
},
dictionary: locales[lang as keyof typeof locales],
+ tables: {
+ splitCells: true,
+ cellBackgroundColor: true,
+ cellTextColor: true,
+ headers: true,
+ },
uploadFile,
schema: blockNoteSchema,
},
diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/tablePDF.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/tablePDF.tsx
index 61117925..f8a2b1dc 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/tablePDF.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/tablePDF.tsx
@@ -1,46 +1,120 @@
-import { TD, TH, TR, Table } from '@ag-media/react-pdf-table';
-import { View } from '@react-pdf/renderer';
+/**
+ * We use mainly the Blocknotes code, mixed with @ag-media/react-pdf-table
+ * to have a better Table support.
+ * See:
+ * https://github.com/TypeCellOS/BlockNote/blob/004c0bf720fe1415c497ad56449015c5f4dd7ba0/packages/xl-pdf-exporter/src/pdf/util/table/Table.tsx
+ *
+ * We succeded to manage the colspan, but rowspan is not supported yet.
+ */
+
+import { TD, TR, Table } from '@ag-media/react-pdf-table';
+import { mapTableCell } from '@blocknote/core';
+import { StyleSheet, Text } from '@react-pdf/renderer';
import { DocsExporterPDF } from '../types';
+const PIXELS_PER_POINT = 0.75;
+const styles = StyleSheet.create({
+ tableContainer: {
+ border: '1px solid #ddd',
+ },
+ row: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ display: 'flex',
+ },
+ cell: {
+ paddingHorizontal: 5 * PIXELS_PER_POINT,
+ paddingTop: 3 * PIXELS_PER_POINT,
+ wordWrap: 'break-word',
+ whiteSpace: 'pre-wrap',
+ },
+ headerCell: {
+ fontWeight: 'bold',
+ },
+});
export const blockMappingTablePDF: DocsExporterPDF['mappings']['blockMapping']['table'] =
(block, exporter) => {
+ const { options } = exporter;
+ const blockContent = block.content;
+
+ // If headerRows is 1, then the first row is a header row
+ const headerRows = new Array(blockContent.headerRows ?? 0).fill(
+ true,
+ ) as boolean[];
+ // If headerCols is 1, then the first column is a header column
+ const headerCols = new Array(blockContent.headerCols ?? 0).fill(
+ true,
+ ) as boolean[];
+
+ /**
+ * Calculate the table scale based on the column widths.
+ */
+ const columnWidths = blockContent.columnWidths.map((w) => w || 120);
+ const fullWidth = 730;
+ const totalWidth = Math.min(
+ columnWidths.reduce((sum, w) => sum + w, 0),
+ fullWidth,
+ );
+ const tableScale = (totalWidth * 100) / fullWidth;
+
return (
-
- {block.content.rows.map((row, index) => {
- if (index === 0) {
- return (
- |
- {row.cells.map((cell, index) => {
- // Make empty cells are rendered.
- if (cell.length === 0) {
- cell.push({
- styles: {},
- text: ' ',
- type: 'text',
- });
- }
- return (
- | {exporter.transformInlineContent(cell)} |
- );
- })}
-
- );
- }
+
+ {blockContent.rows.map((row, rowIndex) => {
+ const isHeaderRow = headerRows[rowIndex];
+
return (
-
- {row.cells.map((cell, index) => {
- // Make empty cells are rendered.
- if (cell.length === 0) {
+
+ {row.cells.map((c, colIndex) => {
+ const formatCell = mapTableCell(c);
+
+ const isHeaderCol = headerCols[colIndex];
+
+ const cell = formatCell.content;
+ const cellProps = formatCell.props;
+
+ // Make empty cells rendered.
+ if (Array.isArray(cell) && cell.length === 0) {
cell.push({
styles: {},
text: ' ',
type: 'text',
});
}
+
+ const weight = columnWidths
+ .slice(colIndex, colIndex + (cellProps.colspan || 1))
+ .reduce((sum, w) => sum + w, 0);
+
+ const flexCell = {
+ flex: `${weight} ${weight} 0%`,
+ };
+
+ const arrayStyle = [
+ isHeaderRow || isHeaderCol ? styles.headerCell : {},
+ flexCell,
+ {
+ color:
+ cellProps.textColor === 'default'
+ ? undefined
+ : options.colors[
+ cellProps.textColor as keyof typeof options.colors
+ ].text,
+ backgroundColor:
+ cellProps.backgroundColor === 'default'
+ ? undefined
+ : options.colors[
+ cellProps.backgroundColor as keyof typeof options.colors
+ ].background,
+ textAlign: cellProps.textAlignment,
+ },
+ ];
+
return (
- |
- {exporter.transformInlineContent(cell)}
+ |
+
+ {exporter.transformInlineContent(cell)}
+
|
);
})}
diff --git a/src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx b/src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx
index e576c8c6..b047960e 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx
@@ -9,7 +9,7 @@ import {
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
-import { pdf } from '@react-pdf/renderer';
+import { DocumentProps, pdf } from '@react-pdf/renderer';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
@@ -92,7 +92,10 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
const exporter = new PDFExporter(editor.schema, pdfDocsSchemaMappings, {
resolveFileUrl: async (url) => exportCorsResolveFileUrl(doc.id, url),
});
- const pdfDocument = await exporter.toReactPDFDocument(exportDocument);
+ const pdfDocument = (await exporter.toReactPDFDocument(
+ exportDocument,
+ )) as React.ReactElement;
+
blobExport = await pdf(pdfDocument).toBlob();
} else {
const exporter = new DOCXExporter(editor.schema, docxDocsSchemaMappings, {