Paragraph Node Type

Category: Basic • Detailed implementation guide with view integration notes

Schema Definition

Schema definition for the Paragraph node type:

{
  paragraph: {
    content: 'inline*',
    group: 'block'
  }
}

Model Representation

Example model representation:

{
  type: 'paragraph',
  children: [
    { type: 'text', text: 'This is a paragraph.' }
  ]
}

HTML Serialization

Converting model to HTML:

function serializeParagraph(node) {
  return '<p>' + serializeChildren(node.children) + '</p>';
}

HTML Deserialization

Parsing HTML to model:

function parseParagraph(domNode) {
  return {
    type: 'paragraph',
    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 p = document.createElement('p');
p.contentEditable = 'true';
node.children.forEach(child => {
  p.appendChild(renderNode(child));
});

// Input handling
p.addEventListener('input', (e) => {
  const newModel = parseDOMToModel(p);
  updateModel(newModel);
});

// Selection mapping
function getParagraphPosition(selection) {
  const range = selection.getRangeAt(0);
  const offset = getTextOffset(range.startContainer, range.startOffset);
  return { path: [0], offset };
}

Common Issues

Common Pitfalls: These are issues frequently encountered when implementing this node type. Review carefully before implementation.

Common issues and solutions:

// Issue: Empty paragraphs collapse
// Solution: Always render at least <br> or &nbsp;
if (node.children.length === 0) {
  return '<p><br></p>';
}

// Issue: Browser adds extra <br> tags
// Solution: Normalize on deserialization
function normalizeParagraph(domNode) {
  const brs = domNode.querySelectorAll('br');
  if (brs.length > 1) {
    // Remove extra <br> tags
  }
}

// Issue: Paste creates nested paragraphs
// Solution: Unwrap on paste
function handlePaste(e) {
  const pasted = e.clipboardData.getData('text/html');
  const parsed = parseHTML(pasted);
  // Unwrap nested paragraphs
  return unwrapNestedParagraphs(parsed);
}

Implementation

Complete implementation example:

class ParagraphNode {
  constructor(attrs, children) {
    this.type = 'paragraph';
    this.attrs = attrs || {};
    this.children = children || [];
  }
  
  toDOM() {
    const p = document.createElement('p');
    this.children.forEach(child => {
      p.appendChild(child.toDOM());
    });
    return p;
  }
  
  static fromDOM(domNode) {
    const children = Array.from(domNode.childNodes)
      .map(node => parseNode(node))
      .filter(Boolean);
    return new ParagraphNode({}, children);
  }
}