Schema Definition
Schema definition for the Heading node type:
{
heading: {
content: 'inline*',
group: 'block',
attrs: {
level: { default: 1 }
}
}
}Model Representation
Example model representation:
{
type: 'heading',
attrs: { level: 1 },
children: [
{ type: 'text', text: 'Main Title' }
]
}HTML Serialization
Converting model to HTML:
function serializeHeading(node) {
const level = node.attrs?.level || 1;
return '<h' + level + '>' +
serializeChildren(node.children) +
'</h' + level + '>';
}HTML Deserialization
Parsing HTML to model:
function parseHeading(domNode) {
const level = parseInt(domNode.tagName[1]) || 1;
return {
type: 'heading',
attrs: { level },
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 level = node.attrs?.level || 1;
const h = document.createElement('h' + level);
h.contentEditable = 'true';
node.children.forEach(child => {
h.appendChild(renderNode(child));
});
// Level change handling
function changeHeadingLevel(node, newLevel) {
if (newLevel < 1 || newLevel > 6) return;
return {
...node,
attrs: { ...node.attrs, level: newLevel }
};
}Common Issues
Common Pitfalls: These are issues frequently encountered when implementing this node type. Review carefully before implementation.
Common issues and solutions:
// Issue: Heading level validation
// Solution: Always validate level
if (node.attrs.level < 1 || node.attrs.level > 6) {
node.attrs.level = 1;
}
// Issue: Empty headings
// Solution: Prevent or handle empty headings
if (node.children.length === 0) {
return '<h' + level + '><br></h' + level + '>';
}
// Issue: Heading hierarchy (h1 after h3)
// Solution: Validate document structure
function validateHeadingHierarchy(doc) {
let lastLevel = 0;
// Check heading order
}Implementation
Complete implementation example:
class HeadingNode {
constructor(attrs, children) {
this.type = 'heading';
this.attrs = { level: attrs?.level || 1 };
this.children = children || [];
}
toDOM() {
const level = this.attrs.level;
const h = document.createElement('h' + level);
this.children.forEach(child => {
h.appendChild(child.toDOM());
});
return h;
}
static fromDOM(domNode) {
const level = parseInt(domNode.tagName[1]) || 1;
const children = Array.from(domNode.childNodes)
.map(node => parseNode(node))
.filter(Boolean);
return new HeadingNode({ level }, children);
}
}