Schema Definition
Schema definition for the Table node type:
{
table: {
content: 'tableRow+',
group: 'block',
attrs: {
colCount: { default: 0 }
}
},
tableRow: {
content: '(tableCell | tableHeader)+',
},
tableCell: {
content: 'block+',
tableRole: 'cell',
isolating: true,
attrs: {
colspan: { default: 1 },
rowspan: { default: 1 }
}
},
tableHeader: {
content: 'block+',
attrs: {
colspan: { default: 1 },
rowspan: { default: 1 }
}
}
}Model Representation
Example model representation:
{
type: 'table',
attrs: { colCount: 3 },
children: [
{
type: 'tableRow',
children: [
{
type: 'tableHeader',
attrs: { colspan: 1, rowspan: 1 },
children: [
{
type: 'paragraph',
children: [{ type: 'text', text: 'Header' }]
}
]
}
]
}
]
}HTML Serialization
Converting model to HTML:
function serializeTable(node) {
let html = '<table><tbody>';
node.children.forEach(row => {
html += serializeTableRow(row);
});
html += '</tbody></table>';
return html;
}
function serializeTableRow(node) {
let html = '<tr>';
node.children.forEach(cell => {
html += serializeTableCell(cell);
});
html += '</tr>';
return html;
}
function serializeTableCell(node) {
const tag = node.type === 'tableHeader' ? 'th' : 'td';
const attrs = [];
if (node.attrs.colspan > 1) {
attrs.push('colspan="' + node.attrs.colspan + '"');
}
if (node.attrs.rowspan > 1) {
attrs.push('rowspan="' + node.attrs.rowspan + '"');
}
return '<' + tag + (attrs.length ? ' ' + attrs.join(' ') : '') + '>' +
serializeChildren(node.children) +
'</' + tag + '>';
}HTML Deserialization
Parsing HTML to model:
function parseTable(domNode) {
const tbody = domNode.querySelector('tbody') || domNode;
const rows = Array.from(tbody.querySelectorAll('tr'))
.map(row => parseTableRow(row));
// Calculate column count
const colCount = Math.max(...rows.map(row =>
row.children.reduce((sum, cell) => sum + (cell.attrs.colspan || 1), 0)
));
return {
type: 'table',
attrs: { colCount },
children: rows
};
}
function parseTableRow(domNode) {
const cells = Array.from(domNode.childNodes)
.filter(node => node.tagName === 'TD' || node.tagName === 'TH')
.map(cell => parseTableCell(cell));
return {
type: 'tableRow',
children: cells
};
}
function parseTableCell(domNode) {
const type = domNode.tagName === 'TH' ? 'tableHeader' : 'tableCell';
const colspan = parseInt(domNode.getAttribute('colspan')) || 1;
const rowspan = parseInt(domNode.getAttribute('rowspan')) || 1;
return {
type,
attrs: { colspan, rowspan },
children: parseChildren(domNode.childNodes)
};
}View Integration
View Integration Notes: Pay special attention to contenteditable behavior, selection handling, and event management when implementing this node type in your view layer.
View integration code:
// Rendering
const table = document.createElement('table');
const tbody = document.createElement('tbody');
node.children.forEach(row => {
const tr = renderTableRow(row);
tbody.appendChild(tr);
});
table.appendChild(tbody);
// Cell editing
function makeCellEditable(cell) {
cell.contentEditable = 'true';
cell.addEventListener('input', handleCellInput);
cell.addEventListener('blur', handleCellBlur);
}
// Table manipulation
function insertRow(table, index) {
const rowCount = table.children[0].children.length;
const newRow = createEmptyRow(rowCount);
// Insert at index
}
function insertColumn(table, index) {
// Add cell to each row at index
}Common Issues
Common Pitfalls: These are issues frequently encountered when implementing this node type. Review carefully before implementation.
Common issues and solutions:
// Issue: Table structure validation
// Solution: Ensure consistent column count
function validateTable(table) {
const colCount = table.attrs.colCount;
table.children.forEach(row => {
const actualCols = row.children.reduce((sum, cell) =>
sum + (cell.attrs.colspan || 1), 0
);
if (actualCols !== colCount) {
// Fix column count mismatch
}
});
}
// Issue: Empty cells
// Solution: Always have at least one block
if (cell.children.length === 0) {
cell.children.push({
type: 'paragraph',
children: []
});
}
// Issue: Colspan/rowspan calculations
// Solution: Track merged cells
function getCellAt(table, row, col) {
// Account for colspan/rowspan
}Implementation
Complete implementation example:
class TableNode {
constructor(attrs, children) {
this.type = 'table';
this.attrs = { colCount: attrs?.colCount || 0 };
this.children = children || [];
}
toDOM() {
const table = document.createElement('table');
const tbody = document.createElement('tbody');
this.children.forEach(row => {
tbody.appendChild(row.toDOM());
});
table.appendChild(tbody);
return table;
}
static fromDOM(domNode) {
const tbody = domNode.querySelector('tbody') || domNode;
const rows = Array.from(tbody.querySelectorAll('tr'))
.map(row => TableRowNode.fromDOM(row));
const colCount = Math.max(...rows.map(row =>
row.getColumnCount()
));
return new TableNode({ colCount }, rows);
}
}