CKEditor Model Element Reference 

Complete reference for the typo3image custom model element in CKEditor 5.

Overview 

The typo3image is a custom model element that represents TYPO3 FAL-integrated images in the CKEditor document model. It extends CKEditor's base $blockObject and includes TYPO3-specific attributes for FAL integration, image processing, and metadata management.

File: Resources/Public/JavaScript/Plugins/typo3image.js

Model vs View Architecture 

Understanding CKEditor 5 Architecture 

CKEditor 5 uses a Model-View-Controller (MVC) architecture:

┌─────────────────────────────────────────────────┐
│ Model Layer (Data)                              │
│ - Abstract representation of document           │
│ - Business logic and validation                 │
│ - typo3image element with attributes            │
└──────────────────┬──────────────────────────────┘
                   │
                   │ Conversions
                   │
┌──────────────────▼──────────────────────────────┐
│ View Layer (DOM)                                │
│ - Visual representation in editor               │
│ - <img> elements with HTML attributes           │
│ - User sees and interacts with                  │
└──────────────────┬──────────────────────────────┘
                   │
                   │ Rendering
                   │
┌──────────────────▼──────────────────────────────┐
│ DOM (Browser)                                   │
│ - Actual HTML in contenteditable               │
│ - <img src="..." data-htmlarea-file-uid="123"/> │
└─────────────────────────────────────────────────┘
Copied!

Why Separate Model and View? 

  • Data Integrity: Model maintains clean data structure regardless of DOM quirks
  • Cross-Platform: Same model can render differently on different platforms
  • Collaboration: Multiple users can edit same model with conflict resolution
  • Undo/Redo: Model changes tracked for history management
  • Validation: Business rules enforced in model layer

Model Elements 

The extension provides two model elements for different use cases:

  • typo3image: Block-level images wrapped in <figure> (with optional caption)
  • typo3imageInline: True inline images that flow with text

Block Image Schema (typo3image) 

editor.model.schema.register('typo3image', {
    inheritAllFrom: '$blockObject',
    allowIn: ['$text', '$block'],
    allowAttributes: [
        'src', 'fileUid', 'fileTable',
        'alt', 'altOverride', 'title', 'titleOverride',
        'class', 'enableZoom', 'width', 'height',
        'htmlA', 'imageLinkHref', 'imageLinkTarget', 'imageLinkTitle',
        'imageLinkClass', 'imageLinkParams'
    ],
});
Copied!

Inline Image Schema (typo3imageInline) 

New in version 13.6

True inline images with cursor positioning support.

editor.model.schema.register('typo3imageInline', {
    inheritAllFrom: '$inlineObject',
    allowIn: ['$block'],
    allowAttributes: [
        'src', 'fileUid', 'fileTable',
        'alt', 'altOverride', 'title', 'titleOverride',
        'class', 'enableZoom', 'width', 'height', 'noScale', 'quality',
        'imageLinkHref', 'imageLinkTarget', 'imageLinkTitle',
        'imageLinkClass', 'imageLinkParams'
    ],
});
Copied!

Key Differences 

Feature typo3image (Block) typo3imageInline (Inline)
Inheritance $blockObject $inlineObject
Caption Support ✅ Yes (typo3imageCaption child) ❌ No
Text Flow Breaks paragraph (block-level) Flows with text (inline)
Cursor Position Before/after figure Before/after on same line
Output HTML <figure><img></figure> <img class="image-inline">
Style Classes image-left, image-right, image-center, image-block image-inline

Usage Example 

Text can flow around inline images:

In the editor, users can type text before and after inline images on the same line, just like typing around any other inline element (bold text, links, etc.).

Toggle Command 

Users can convert between block and inline via the toggleImageType command:

// Toggle current image between block and inline
editor.execute('toggleImageType');

// Check current type
const isInline = editor.commands.get('toggleImageType').value === 'inline';
Copied!

Block → Inline Conversion:

  • Caption is removed (inline images cannot have captions)
  • Block style classes removed, image-inline added
  • Image becomes inline in text flow

Inline → Block Conversion:

  • Image wrapped in figure
  • image-block class added (or previous alignment class)
  • Image becomes block-level element

Schema Properties Explained 

inheritAllFrom: '$blockObject' 

Inherits all properties from CKEditor's base $blockObject:

  • Selectable: Can be selected like any block element
  • Object: Treated as atomic unit (not text content)
  • Focusable: Can receive focus for editing
  • Non-Breaking: Cannot be split by Enter key

allowIn: ['$text', '$block'] 

Defines where typo3image can exist:

  • $text: Inside text content (inline-like behavior)
  • $block: Inside block elements (paragraphs, divs, etc.)

Result: Images can be placed in any text flow or block context.

allowAttributes: [...] 

Lists all valid attributes for the model element. Attributes not listed are stripped.

Attribute Reference 

Core Attributes 

src 

type: String
required: true
Copied!

Purpose: Image source URL (absolute or relative)

Examples:

src: '/fileadmin/user_upload/image.jpg'
src: '/fileadmin/_processed_/a/b/csm_image_123.jpg'
src: 'https://example.com/external.jpg'
Copied!

Usage:

const src = modelElement.getAttribute('src');
writer.setAttribute('src', '/new/path.jpg', modelElement);
Copied!

fileUid 

type: Number
required: true (for TYPO3 FAL integration)
Copied!

Purpose: TYPO3 File Abstraction Layer file UID

Range: Positive integer matching sys_file.uid

Example:

fileUid: 123
Copied!

Usage:

const fileUid = modelElement.getAttribute('fileUid');
writer.setAttribute('fileUid', 456, modelElement);
Copied!

Backend Integration:

// Fetch file info from backend
const fileUid = modelElement.getAttribute('fileUid');
const fileInfo = await fetch(
    routeUrl + '&action=info&fileId=' + fileUid
).then(r => r.json());
Copied!

fileTable 

type: String
default: 'sys_file'
Copied!

Purpose: Database table name for file reference

Valid Values: 'sys_file' (default), 'sys_file_reference' (rarely used)

Example:

fileTable: 'sys_file'
Copied!

Usage:

const table = modelElement.getAttribute('fileTable') || 'sys_file';
Copied!

Metadata Attributes 

alt 

type: String
default: ''
Copied!

Purpose: Alternative text for accessibility (WCAG compliance)

Recommendations:

  • Describe image content concisely
  • Required for accessibility
  • Max  125 characters for optimal screen reader experience

Example:

alt: 'Product photo showing red widget from front angle'
Copied!

Usage:

writer.setAttribute('alt', 'New alt text', modelElement);
Copied!

altOverride 

type: Boolean
default: false
Copied!

Purpose: Flag indicating alt text was manually overridden by user

Behavior:

  • false: Use alt from FAL file metadata
  • true: Use custom alt text from alt attribute

Example:

altOverride: true  // Custom alt text takes precedence
Copied!

Usage Pattern:

// In image dialog
if (customAltCheckbox.checked) {
    writer.setAttribute('alt', customAltValue, modelElement);
    writer.setAttribute('altOverride', true, modelElement);
} else {
    writer.removeAttribute('alt', modelElement);
    writer.removeAttribute('altOverride', modelElement);
}
Copied!

title 

type: String
default: ''
Copied!

Purpose: Advisory title (tooltip text shown on hover)

Recommendations:

  • Optional supplementary information
  • Not a replacement for alt text
  • Brief contextual information

Example:

title: 'Click to view full size'
Copied!

Usage:

writer.setAttribute('title', 'Tooltip text', modelElement);
Copied!

titleOverride 

type: Boolean
default: false
Copied!

Purpose: Flag indicating title was manually overridden by user

Behavior:

  • false: Use title from FAL file metadata
  • true: Use custom title from title attribute

Example:

titleOverride: true
Copied!

Visual Attributes 

class 

type: String
default: ''
Copied!

Purpose: Space-separated CSS class names for styling

Style Integration: Modified by CKEditor style system via GeneralHtmlSupport

Examples:

class: 'float-left img-responsive'
class: 'img-thumbnail d-block mx-auto'
Copied!

Usage:

// Manual class setting
writer.setAttribute('class', 'my-class another-class', modelElement);

// Style system automatically updates this attribute
// when user selects a style from dropdown
Copied!

Style System Integration:

// CKEditor style definition
{
    name: 'Image Left',
    element: 'img',
    classes: ['float-left', 'mr-3']
}

// Results in:
class: 'float-left mr-3'
Copied!

width 

type: String (pixels without unit)
default: ''
Copied!

Purpose: Image display width in pixels

Format: Numeric string without 'px' unit

Examples:

width: '800'
width: '1200'
Copied!

Usage:

writer.setAttribute('width', '800', modelElement);
Copied!

Aspect Ratio Preservation:

// When width changes, height should be recalculated
const newWidth = 800;
const originalWidth = img.width;
const originalHeight = img.height;
const ratio = originalWidth / originalHeight;
const newHeight = Math.ceil(newWidth / ratio);

writer.setAttribute('width', String(newWidth), modelElement);
writer.setAttribute('height', String(newHeight), modelElement);
Copied!

height 

type: String (pixels without unit)
default: ''
Copied!

Purpose: Image display height in pixels

Format: Numeric string without 'px' unit

Examples:

height: '600'
height: '900'
Copied!

Usage:

writer.setAttribute('height', '600', modelElement);
Copied!

enableZoom 

type: Boolean
default: false
Copied!

Purpose: Enable zoom/click-to-enlarge functionality (TYPO3-specific feature)

Behavior:

  • true: Image becomes clickable, opens larger version
  • false: Image is static, no click interaction

Example:

enableZoom: true
Copied!

Frontend Rendering:

<!-- When enableZoom is true -->
<a href="large-image.jpg" data-lightbox="gallery">
    <img src="thumb.jpg" data-htmlarea-zoom="true" />
</a>
Copied!

Usage:

writer.setAttribute('enableZoom', true, modelElement);
Copied!

Working with Model Elements 

Creating Model Elements 

editor.model.change(writer => {
    const typo3image = writer.createElement('typo3image', {
        src: '/fileadmin/image.jpg',
        fileUid: 123,
        fileTable: 'sys_file',
        width: '800',
        height: '600',
        alt: 'Description',
        class: 'img-fluid'
    });

    // Insert at current selection
    const insertPosition = editor.model.document.selection.getFirstPosition();
    editor.model.insertContent(typo3image, insertPosition);
});
Copied!

Updating Attributes 

editor.model.change(writer => {
    const selectedElement = editor.model.document.selection.getSelectedElement();

    if (selectedElement && selectedElement.name === 'typo3image') {
        // Update single attribute
        writer.setAttribute('width', '1200', selectedElement);

        // Update multiple attributes
        writer.setAttributes({
            width: '1200',
            height: '800',
            class: 'img-large'
        }, selectedElement);
    }
});
Copied!

Reading Attributes 

const selectedElement = editor.model.document.selection.getSelectedElement();

if (selectedElement && selectedElement.name === 'typo3image') {
    // Read single attribute
    const src = selectedElement.getAttribute('src');
    const fileUid = selectedElement.getAttribute('fileUid');

    // Read with default fallback
    const alt = selectedElement.getAttribute('alt') || '';
    const width = selectedElement.getAttribute('width') || '0';

    // Check if attribute exists
    const hasClass = selectedElement.hasAttribute('class');

    // Get all attributes
    const allAttrs = Array.from(selectedElement.getAttributes());
    console.log(allAttrs);  // [['src', '...'], ['fileUid', 123], ...]
}
Copied!

Removing Attributes 

editor.model.change(writer => {
    const selectedElement = editor.model.document.selection.getSelectedElement();

    if (selectedElement && selectedElement.name === 'typo3image') {
        // Remove single attribute
        writer.removeAttribute('class', selectedElement);

        // Remove multiple attributes
        writer.removeAttribute('title', selectedElement);
        writer.removeAttribute('titleOverride', selectedElement);
    }
});
Copied!

Model Selection 

Selecting Elements 

// Select element programmatically
editor.model.change(writer => {
    const element = /* get element reference */;
    writer.setSelection(element, 'on');  // 'on' = select element itself
});
Copied!

Getting Selected Element 

const selection = editor.model.document.selection;
const selectedElement = selection.getSelectedElement();

if (selectedElement && selectedElement.name === 'typo3image') {
    // Image is selected
    console.log('Selected typo3image element');
}
Copied!

Iterating Selection Range 

const selection = editor.model.document.selection;
const range = selection.getFirstRange();

for (const item of range.getItems()) {
    if (item.is('element', 'typo3image')) {
        console.log('Found typo3image:', item.getAttribute('src'));
    }
}
Copied!

Model Traversal 

Finding Parent Elements 

const element = /* typo3image element */;
const parent = element.parent;

console.log(parent.name);  // e.g., 'paragraph', '$root'
Copied!

Finding Previous/Next Siblings 

const element = /* typo3image element */;
const previousSibling = element.previousSibling;
const nextSibling = element.nextSibling;
Copied!

Walking the Model Tree 

function findAllImages(root) {
    const images = [];
    const walker = editor.model.createRangeIn(root).getWalker();

    for (const {item} of walker) {
        if (item.is('element', 'typo3image')) {
            images.push(item);
        }
    }

    return images;
}

// Find all images in document
const allImages = findAllImages(editor.model.document.getRoot());
Copied!

Attribute Validation 

Allowed Attributes Enforcement 

CKEditor automatically strips attributes not in allowAttributes list:

// This attribute will be stripped
writer.setAttribute('invalidAttr', 'value', modelElement);

// Only these attributes are preserved
allowAttributes: [
    'src', 'fileUid', 'fileTable',
    'alt', 'altOverride', 'title', 'titleOverride',
    'class', 'enableZoom', 'width', 'height',
    'htmlA', 'linkHref', 'linkTarget', 'linkTitle',
    'linkClass', 'linkParams'
]
Copied!

Custom Validation 

editor.model.schema.addAttributeCheck((context, attributeName) => {
    // Only allow width/height with valid numeric values
    if (attributeName === 'width' || attributeName === 'height') {
        if (context.endsWith('typo3image')) {
            const value = context.getAttribute(attributeName);
            return /^\d+$/.test(value);  // Must be numeric
        }
    }
    return true;
});
Copied!

Model Change Listeners 

Listening to Attribute Changes 

editor.model.document.on('change:data', () => {
    const changes = editor.model.document.differ.getChanges();

    for (const change of changes) {
        if (change.type === 'attribute' && change.attributeKey === 'class') {
            console.log('Class changed:', {
                element: change.range.start.parent.name,
                oldValue: change.attributeOldValue,
                newValue: change.attributeNewValue
            });
        }
    }
});
Copied!

Listening to Element Insertion 

editor.model.document.on('change:data', () => {
    const changes = editor.model.document.differ.getChanges();

    for (const change of changes) {
        if (change.type === 'insert' && change.name === 'typo3image') {
            console.log('typo3image inserted:', change.position.path);
        }
    }
});
Copied!

Advanced Patterns 

Cloning Elements 

editor.model.change(writer => {
    const original = /* get typo3image element */;

    // Clone with all attributes
    const clone = writer.cloneElement(original);

    // Insert clone
    const insertPosition = /* target position */;
    writer.insert(clone, insertPosition);
});
Copied!

Batch Attribute Updates 

editor.model.change(writer => {
    const images = /* array of typo3image elements */;

    // Apply same class to all images
    images.forEach(img => {
        const currentClass = img.getAttribute('class') || '';
        const newClass = currentClass + ' batch-processed';
        writer.setAttribute('class', newClass.trim(), img);
    });
});
Copied!

Conditional Attribute Setting 

editor.model.change(writer => {
    const element = /* typo3image element */;

    // Only set width if not already set
    if (!element.hasAttribute('width')) {
        writer.setAttribute('width', '800', element);
    }

    // Update alt only if override is enabled
    if (element.getAttribute('altOverride')) {
        writer.setAttribute('alt', customAltText, element);
    }
});
Copied!

Debugging Model Elements 

Inspect Element in Console 

// Get selected element
const element = editor.model.document.selection.getSelectedElement();

// Log all attributes
console.log('Element:', element.name);
console.log('Attributes:', Array.from(element.getAttributes()));

// Log specific attributes
console.log('src:', element.getAttribute('src'));
console.log('fileUid:', element.getAttribute('fileUid'));
console.log('class:', element.getAttribute('class'));
Copied!

Monitor Model Changes 

editor.model.document.on('change', (evt, batch) => {
    console.log('Model changed, batch type:', batch.type);
    console.log('Is undoable:', batch.isUndoable);

    const changes = editor.model.document.differ.getChanges();
    console.log('Changes:', changes);
});
Copied!

Visualize Model Structure 

function logModelTree(element, indent = 0) {
    const prefix = ' '.repeat(indent);
    if (element.is('$text')) {
        console.log(prefix + 'TEXT:', element.data);
    } else {
        console.log(prefix + element.name);
        for (const child of element.getChildren()) {
            logModelTree(child, indent + 2);
        }
    }
}

// Log entire document structure
logModelTree(editor.model.document.getRoot());
Copied!