스키마 정의
테이블 노드 타입의 스키마 정의:
{
table: {
content: 'tableRow+',
group: 'block',
attrs: {
colCount: { default: 0 }
}
},
tableRow: {
content: '(tableCell | tableHeader)+'
},
tableCell: {
content: 'block+',
attrs: {
colspan: { default: 1 },
rowspan: { default: 1 }
}
},
tableHeader: {
content: 'block+',
attrs: {
colspan: { default: 1 },
rowspan: { default: 1 }
}
}
}모델 표현
모델 표현 예제:
{
type: 'table',
attrs: { colCount: 3 },
children: [
{
type: 'tableRow',
children: [
{
type: 'tableHeader',
attrs: { colspan: 1, rowspan: 1 },
children: [
{
type: 'paragraph',
children: [{ type: 'text', text: '헤더' }]
}
]
}
]
}
]
}HTML 직렬화
모델을 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 역직렬화
HTML을 모델로 파싱:
function parseTable(domNode) {
const tbody = domNode.querySelector('tbody') || domNode;
const rows = Array.from(tbody.querySelectorAll('tr'))
.map(row => parseTableRow(row));
// 열 개수 계산
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)
};
}뷰 연동
뷰 연동 노트: 이 노드 타입을 뷰 레이어에서 구현할 때 contenteditable 동작, 선택 처리, 이벤트 관리에 특히 주의하세요.
뷰 연동 코드:
// 렌더링
const table = document.createElement('table');
const tbody = document.createElement('tbody');
node.children.forEach(row => {
const tr = renderTableRow(row);
tbody.appendChild(tr);
});
table.appendChild(tbody);
// 셀 편집
function makeCellEditable(cell) {
cell.contentEditable = 'true';
cell.addEventListener('input', handleCellInput);
cell.addEventListener('blur', handleCellBlur);
}
// 테이블 조작
function insertRow(table, index) {
const rowCount = table.children[0].children.length;
const newRow = createEmptyRow(rowCount);
// 인덱스에 삽입
}
function insertColumn(table, index) {
// 각 행의 인덱스에 셀 추가
}일반적인 문제
일반적인 함정: 이 노드 타입을 구현할 때 자주 발생하는 문제들입니다. 구현 전에 주의 깊게 검토하세요.
일반적인 문제 및 해결 방법:
// 문제: 테이블 구조 검증
// 해결: 일관된 열 개수 보장
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) {
// 열 개수 불일치 수정
}
});
}
// 문제: 빈 셀
// 해결: 항상 최소 하나의 블록 유지
if (cell.children.length === 0) {
cell.children.push({
type: 'paragraph',
children: []
});
}
// 문제: Colspan/rowspan 계산
// 해결: 병합된 셀 추적
function getCellAt(table, row, col) {
// colspan/rowspan 고려
}구현
완전한 구현 예제:
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);
}
}