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 ( - ); })} 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, {
- {exporter.transformInlineContent(cell)} + + + {exporter.transformInlineContent(cell)} +