Tables in document editor #8621
Replies: 4 comments
-
|
Hello! Did you happen to get anywhere with this? 😄 |
Beta Was this translation helpful? Give feedback.
-
|
Hey just bumping this as well, it's a large feature gap in the Rich Text editor. A simple example: |
Beta Was this translation helpful? Give feedback.
-
|
I had written Editor.js with keystone 5 here I may be able to get this done as sponsored work in K6. |
Beta Was this translation helpful? Give feedback.
-
|
I've implemented a custom table field type using CMS component block ( import React, { useEffect, useRef, useState } from 'react';
import { component, fields, NotEditable } from '@keystone-6/fields-document/component-blocks';
import {
ToolbarButton,
ToolbarGroup,
ToolbarSeparator,
} from '@keystone-6/fields-document/primitives';
import { FieldContainer, FieldLabel, TextArea } from '@keystone-ui/fields';
import { Edit3Icon, Trash2Icon } from '@keystone-ui/icons';
import { AlertDialog } from '@keystone-ui/modals';
import { Tooltip } from '@keystone-ui/tooltip';
import type { ActiveTable } from 'active-table-react';
type TTableData = (string | number)[][];
const DEFAULT_TABLE_DATA: TTableData = [];
function parseNumberIfPossible(raw: string): string | number {
const trimmed = raw.trim();
if (trimmed === '') {
return '';
}
const maybeNumber = Number(trimmed);
if (Number.isFinite(maybeNumber) && String(maybeNumber) === trimmed) {
return maybeNumber;
}
return trimmed;
}
function normalizeRowsLength(rows: TTableData): TTableData {
const maxCols = rows.reduce((max, row) => (row.length > max ? row.length : max), 0);
return rows.map(row => {
if (row.length === maxCols) {
return row;
}
const padded = row.slice();
while (padded.length < maxCols) {
padded.push('');
}
return padded;
});
}
function convertTableDataToMarkdown(rows: TTableData): string {
if (!rows || rows.length === 0) {
return '';
}
const normalized = normalizeRowsLength(rows);
const header = normalized[0].map(cell => String(cell));
const separator = header.map(() => '---');
const body = normalized.slice(1).map(r => r.map(cell => String(cell)));
const toLine = (cells: string[]) => `| ${cells.join(' | ')} |`;
const lines = [toLine(header), toLine(separator), ...body.map(r => toLine(r))];
return lines.join('\n');
}
function parseMarkdownTableToData(markdown: string): TTableData {
if (!markdown || markdown.trim() === '') {
return DEFAULT_TABLE_DATA;
}
const lines = markdown
.split(/\r?\n/)
.map(l => l.trim())
.filter(l => l.length > 0);
if (lines.length === 0) {
return DEFAULT_TABLE_DATA;
}
const parsedLines: string[][] = lines.map(line => {
let working = line;
if (working.startsWith('|')) {
working = working.slice(1);
}
if (working.endsWith('|')) {
working = working.slice(0, -1);
}
return working.split('|').map(c => c.trim());
});
// Remove markdown separator row if present (usually 2nd line)
const rowsNoSeparator = parsedLines.filter((cells, index) => {
if (index !== 1) {
return true;
}
const isSeparator = cells.every(cell => /^:?-{3,}:?$/.test(cell));
return !isSeparator;
});
const coerced: TTableData = rowsNoSeparator.map(row =>
row.map(cell => parseNumberIfPossible(cell)),
);
return normalizeRowsLength(coerced);
}
export const table = component({
label: 'Table',
schema: {
dataJson: fields.text({
label: 'Table Data',
defaultValue: JSON.stringify([]),
}),
},
toolbar: ({ props, onRemove }) => {
const [modalOpen, setModalOpen] = useState(false);
const [markdownValue, setMarkdownValue] = useState('');
useEffect(() => {
if (modalOpen) {
try {
const json = props.fields.dataJson.value || JSON.stringify(DEFAULT_TABLE_DATA);
const data = JSON.parse(json) as TTableData;
setMarkdownValue(convertTableDataToMarkdown(data));
} catch {
setMarkdownValue('');
}
}
}, [modalOpen]);
return (
<ToolbarGroup as={NotEditable} marginTop="small">
<Tooltip content="Edit as markdown" weight="subtle">
{attrs => (
<ToolbarButton
variant="default"
onClick={() => {
setModalOpen(true);
}}
{...attrs}
>
<Edit3Icon size="small" /> <span style={{ marginLeft: '0.5em' }}>Markdown</span>
</ToolbarButton>
)}
</Tooltip>
<ToolbarSeparator />
<Tooltip content="Remove" weight="subtle">
{attrs => (
<ToolbarButton
variant="destructive"
onClick={() => {
onRemove();
}}
{...attrs}
>
<Trash2Icon size="small" />
</ToolbarButton>
)}
</Tooltip>
<AlertDialog
title={`Set table data as markdown`}
actions={{
confirm: {
action: () => {
try {
const data = parseMarkdownTableToData(markdownValue);
props.fields.dataJson.onChange(JSON.stringify(data));
} catch {
// noop; invalid markdown keeps modal open for user to fix
return;
}
setModalOpen(false);
},
label: 'Save',
},
cancel: {
action: () => {
setModalOpen(false);
},
label: 'Cancel',
},
}}
isOpen={modalOpen}
>
{modalOpen && (
<FieldContainer>
<FieldLabel>Table Data (Markdown)</FieldLabel>
<TextArea
autoFocus
style={{ minHeight: 200 }}
value={markdownValue}
onChange={event => {
setMarkdownValue(event.target.value);
}}
/>
</FieldContainer>
)}
</AlertDialog>
</ToolbarGroup>
);
},
preview: function Table(props) {
const [TableComponent, setTableComponent] = React.useState<typeof ActiveTable | null>(null);
const [tableData, setTableData] = React.useState<TTableData>(() => {
try {
return JSON.parse(props.fields.dataJson.value || JSON.stringify(DEFAULT_TABLE_DATA));
} catch {
return DEFAULT_TABLE_DATA;
}
});
const tableComponentRef = useRef<{
getData: () => (number | string)[][];
updateData: (data: (number | string)[][]) => void;
} | null>(null);
const lastOwnDataJsonRef = useRef<string>(props.fields.dataJson.value);
React.useEffect(() => {
if (lastOwnDataJsonRef.current === props.fields.dataJson.value) {
// Don't trigger update when we set the data
return;
}
try {
const parsed = JSON.parse(
props.fields.dataJson.value || JSON.stringify(DEFAULT_TABLE_DATA),
) as TTableData;
if (tableComponentRef.current) {
tableComponentRef.current.updateData(parsed);
} else {
// NOTE: Fallback, this doesn't work all that great
setTableData(parsed);
}
} catch {
// ignore invalid json in field value
}
}, [props.fields.dataJson.value]);
React.useEffect(() => {
import('active-table-react').then(module => {
setTableComponent(() => module.ActiveTable);
});
}, []);
if (!TableComponent) {
return (
<NotEditable>
<div
style={{
margin: '16px 0',
padding: '16px',
border: '1px solid #ddd',
borderRadius: '4px',
}}
>
Loading table...
</div>
</NotEditable>
);
}
return (
<NotEditable>
<div style={{ display: 'flex', overflow: 'visible' }}>
<TableComponent
ref={el => {
tableComponentRef.current = el as any;
}}
data={tableData}
onDataUpdate={(updatedData: TTableData) => {
setTableData(updatedData);
lastOwnDataJsonRef.current = JSON.stringify(updatedData);
props.fields.dataJson.onChange(lastOwnDataJsonRef.current);
}}
availableDefaultColumnTypes={['Text', 'Number', 'Currency', 'Date d-m-y']}
allowDuplicateHeaders={true}
/>
</div>
</NotEditable>
);
},
});Table component in the client app (React): import React from 'react';
import { ActiveTable } from 'active-table-react';
import { Box } from '@mantine/core';
type TTableProps = {
dataJson: string;
};
export function Table({ dataJson }: TTableProps) {
let tableData: (string | number)[][] = [];
try {
tableData = JSON.parse(dataJson || '[]');
} catch (err) {
console.error('Failed to parse table data:', err);
}
if (!Array.isArray(tableData) || tableData.length === 0) {
return null;
}
return (
<ActiveTable
cellStyle={{
fontSize: '16px',
}}
data={tableData}
isCellTextEditable={false}
isHeaderTextEditable={false}
displayAddNewRow={false}
displayAddNewColumn={false}
dragRows={false}
columnDropdown={{ displaySettings: { isAvailable: false } }}
rowDropdown={{ displaySettings: { isAvailable: false } }}
headerStyles={{ hoverColors: { backgroundColor: 'white' } }}
frameComponentsStyles={{ styles: { hoverColors: { backgroundColor: 'white' } } }}
displayHeaderIcons={false}
/>
);
} |
Beta Was this translation helpful? Give feedback.

Uh oh!
There was an error while loading. Please reload this page.
-
As a user, I want to be able to add and edit HTML-like tables inside the WYSIWYG document editor. Adding rows and columns dynamically should be possible.
Beta Was this translation helpful? Give feedback.
All reactions