Skip to content

Commit f8a40cf

Browse files
committed
✨(frontend) add advanced table features
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.
1 parent c32fdb6 commit f8a40cf

File tree

3 files changed

+114
-31
lines changed

3 files changed

+114
-31
lines changed

src/frontend/apps/impress/src/features/docs/doc-editor/components/BlockNoteEditor.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
113113
showCursorLabels: showCursorLabels as 'always' | 'activity',
114114
},
115115
dictionary: locales[lang as keyof typeof locales],
116+
tables: {
117+
splitCells: true,
118+
cellBackgroundColor: true,
119+
cellTextColor: true,
120+
headers: true,
121+
},
116122
uploadFile,
117123
schema: blockNoteSchema,
118124
},

src/frontend/apps/impress/src/features/docs/doc-export/blocks-mapping/tablePDF.tsx

Lines changed: 103 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,120 @@
1-
import { TD, TH, TR, Table } from '@ag-media/react-pdf-table';
2-
import { View } from '@react-pdf/renderer';
1+
/**
2+
* We use mainly the Blocknotes code, mixed with @ag-media/react-pdf-table
3+
* to have a better Table support.
4+
* See:
5+
* https://github.com/TypeCellOS/BlockNote/blob/004c0bf720fe1415c497ad56449015c5f4dd7ba0/packages/xl-pdf-exporter/src/pdf/util/table/Table.tsx
6+
*
7+
* We succeded to manage the colspan, but rowspan is not supported yet.
8+
*/
9+
10+
import { TD, TR, Table } from '@ag-media/react-pdf-table';
11+
import { mapTableCell } from '@blocknote/core';
12+
import { StyleSheet, Text } from '@react-pdf/renderer';
313

414
import { DocsExporterPDF } from '../types';
15+
const PIXELS_PER_POINT = 0.75;
16+
const styles = StyleSheet.create({
17+
tableContainer: {
18+
border: '1px solid #ddd',
19+
},
20+
row: {
21+
flexDirection: 'row',
22+
flexWrap: 'wrap',
23+
display: 'flex',
24+
},
25+
cell: {
26+
paddingHorizontal: 5 * PIXELS_PER_POINT,
27+
paddingTop: 3 * PIXELS_PER_POINT,
28+
wordWrap: 'break-word',
29+
whiteSpace: 'pre-wrap',
30+
},
31+
headerCell: {
32+
fontWeight: 'bold',
33+
},
34+
});
535

636
export const blockMappingTablePDF: DocsExporterPDF['mappings']['blockMapping']['table'] =
737
(block, exporter) => {
38+
const { options } = exporter;
39+
const blockContent = block.content;
40+
41+
// If headerRows is 1, then the first row is a header row
42+
const headerRows = new Array(blockContent.headerRows ?? 0).fill(
43+
true,
44+
) as boolean[];
45+
// If headerCols is 1, then the first column is a header column
46+
const headerCols = new Array(blockContent.headerCols ?? 0).fill(
47+
true,
48+
) as boolean[];
49+
50+
/**
51+
* Calculate the table scale based on the column widths.
52+
*/
53+
const columnWidths = blockContent.columnWidths.map((w) => w || 120);
54+
const fullWidth = 730;
55+
const totalWidth = Math.min(
56+
columnWidths.reduce((sum, w) => sum + w, 0),
57+
fullWidth,
58+
);
59+
const tableScale = (totalWidth * 100) / fullWidth;
60+
861
return (
9-
<Table>
10-
{block.content.rows.map((row, index) => {
11-
if (index === 0) {
12-
return (
13-
<TH key={index}>
14-
{row.cells.map((cell, index) => {
15-
// Make empty cells are rendered.
16-
if (cell.length === 0) {
17-
cell.push({
18-
styles: {},
19-
text: ' ',
20-
type: 'text',
21-
});
22-
}
23-
return (
24-
<TD key={index}>{exporter.transformInlineContent(cell)}</TD>
25-
);
26-
})}
27-
</TH>
28-
);
29-
}
62+
<Table style={[styles.tableContainer, { width: `${tableScale}%` }]}>
63+
{blockContent.rows.map((row, rowIndex) => {
64+
const isHeaderRow = headerRows[rowIndex];
65+
3066
return (
31-
<TR key={index}>
32-
{row.cells.map((cell, index) => {
33-
// Make empty cells are rendered.
34-
if (cell.length === 0) {
67+
<TR key={rowIndex}>
68+
{row.cells.map((c, colIndex) => {
69+
const formatCell = mapTableCell(c);
70+
71+
const isHeaderCol = headerCols[colIndex];
72+
73+
const cell = formatCell.content;
74+
const cellProps = formatCell.props;
75+
76+
// Make empty cells rendered.
77+
if (Array.isArray(cell) && cell.length === 0) {
3578
cell.push({
3679
styles: {},
3780
text: ' ',
3881
type: 'text',
3982
});
4083
}
84+
85+
const weight = columnWidths
86+
.slice(colIndex, colIndex + (cellProps.colspan || 1))
87+
.reduce((sum, w) => sum + w, 0);
88+
89+
const flexCell = {
90+
flex: `${weight} ${weight} 0%`,
91+
};
92+
93+
const arrayStyle = [
94+
isHeaderRow || isHeaderCol ? styles.headerCell : {},
95+
flexCell,
96+
{
97+
color:
98+
cellProps.textColor === 'default'
99+
? undefined
100+
: options.colors[
101+
cellProps.textColor as keyof typeof options.colors
102+
].text,
103+
backgroundColor:
104+
cellProps.backgroundColor === 'default'
105+
? undefined
106+
: options.colors[
107+
cellProps.backgroundColor as keyof typeof options.colors
108+
].background,
109+
textAlign: cellProps.textAlignment,
110+
},
111+
];
112+
41113
return (
42-
<TD key={index}>
43-
<View>{exporter.transformInlineContent(cell)}</View>
114+
<TD key={colIndex} style={arrayStyle}>
115+
<Text style={styles.cell}>
116+
{exporter.transformInlineContent(cell)}
117+
</Text>
44118
</TD>
45119
);
46120
})}

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
VariantType,
1010
useToastProvider,
1111
} from '@openfun/cunningham-react';
12-
import { pdf } from '@react-pdf/renderer';
12+
import { DocumentProps, pdf } from '@react-pdf/renderer';
1313
import { useMemo, useState } from 'react';
1414
import { useTranslation } from 'react-i18next';
1515
import { css } from 'styled-components';
@@ -92,7 +92,10 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
9292
const exporter = new PDFExporter(editor.schema, pdfDocsSchemaMappings, {
9393
resolveFileUrl: async (url) => exportCorsResolveFileUrl(doc.id, url),
9494
});
95-
const pdfDocument = await exporter.toReactPDFDocument(exportDocument);
95+
const pdfDocument = (await exporter.toReactPDFDocument(
96+
exportDocument,
97+
)) as React.ReactElement<DocumentProps>;
98+
9699
blobExport = await pdf(pdfDocument).toBlob();
97100
} else {
98101
const exporter = new DOCXExporter(editor.schema, docxDocsSchemaMappings, {

0 commit comments

Comments
 (0)