From f8a40cf8cc36168e65a61f5132916223ebe30962 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Wed, 30 Apr 2025 12:46:54 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20add=20advanced=20table=20?= =?UTF-8?q?features?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We added advanced table features to the table editor, including: - split / merge cells - cell background color - cell text color - header We adapted the export and brought some improvements compare to the previous version. The export PDF supports colspan (merge horizontally), but does not support the rowspan (merge vertically) for now. --- .../doc-editor/components/BlockNoteEditor.tsx | 6 + .../doc-export/blocks-mapping/tablePDF.tsx | 132 ++++++++++++++---- .../doc-export/components/ModalExport.tsx | 7 +- 3 files changed, 114 insertions(+), 31 deletions(-) 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)} +