✨(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.
This commit is contained in:
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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 (
|
||||
<Table>
|
||||
{block.content.rows.map((row, index) => {
|
||||
if (index === 0) {
|
||||
return (
|
||||
<TH key={index}>
|
||||
{row.cells.map((cell, index) => {
|
||||
// Make empty cells are rendered.
|
||||
if (cell.length === 0) {
|
||||
cell.push({
|
||||
styles: {},
|
||||
text: ' ',
|
||||
type: 'text',
|
||||
});
|
||||
}
|
||||
return (
|
||||
<TD key={index}>{exporter.transformInlineContent(cell)}</TD>
|
||||
);
|
||||
})}
|
||||
</TH>
|
||||
);
|
||||
}
|
||||
<Table style={[styles.tableContainer, { width: `${tableScale}%` }]}>
|
||||
{blockContent.rows.map((row, rowIndex) => {
|
||||
const isHeaderRow = headerRows[rowIndex];
|
||||
|
||||
return (
|
||||
<TR key={index}>
|
||||
{row.cells.map((cell, index) => {
|
||||
// Make empty cells are rendered.
|
||||
if (cell.length === 0) {
|
||||
<TR key={rowIndex}>
|
||||
{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 (
|
||||
<TD key={index}>
|
||||
<View>{exporter.transformInlineContent(cell)}</View>
|
||||
<TD key={colIndex} style={arrayStyle}>
|
||||
<Text style={styles.cell}>
|
||||
{exporter.transformInlineContent(cell)}
|
||||
</Text>
|
||||
</TD>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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<DocumentProps>;
|
||||
|
||||
blobExport = await pdf(pdfDocument).toBlob();
|
||||
} else {
|
||||
const exporter = new DOCXExporter(editor.schema, docxDocsSchemaMappings, {
|
||||
|
||||
Reference in New Issue
Block a user