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

Schema Definition 

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

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'
]
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!