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"/> │
└─────────────────────────────────────────────────┘
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'
],
});
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'
],
});
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';
Block → Inline Conversion:
- Caption is removed (inline images cannot have captions)
- Block style classes removed,
image-inlineadded - Image becomes inline in text flow
Inline → Block Conversion:
- Image wrapped in figure
image-blockclass 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
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'
Usage:
const src = modelElement.getAttribute('src');
writer.setAttribute('src', '/new/path.jpg', modelElement);
fileUid
type: Number
required: true (for TYPO3 FAL integration)
Purpose: TYPO3 File Abstraction Layer file UID
Range: Positive integer matching sys_file.uid
Example:
fileUid: 123
Usage:
const fileUid = modelElement.getAttribute('fileUid');
writer.setAttribute('fileUid', 456, modelElement);
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());
fileTable
type: String
default: 'sys_file'
Purpose: Database table name for file reference
Valid Values: 'sys_file' (default), 'sys_file_reference' (rarely used)
Example:
fileTable: 'sys_file'
Usage:
const table = modelElement.getAttribute('fileTable') || 'sys_file';
Metadata Attributes
alt
type: String
default: ''
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'
Usage:
writer.setAttribute('alt', 'New alt text', modelElement);
altOverride
type: Boolean
default: false
Purpose: Flag indicating alt text was manually overridden by user
Behavior:
false: Use alt from FAL file metadatatrue: Use custom alt text fromaltattribute
Example:
altOverride: true // Custom alt text takes precedence
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);
}
title
type: String
default: ''
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'
Usage:
writer.setAttribute('title', 'Tooltip text', modelElement);
titleOverride
type: Boolean
default: false
Purpose: Flag indicating title was manually overridden by user
Behavior:
false: Use title from FAL file metadatatrue: Use custom title fromtitleattribute
Example:
titleOverride: true
Visual Attributes
class
type: String
default: ''
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'
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
Style System Integration:
// CKEditor style definition
{
name: 'Image Left',
element: 'img',
classes: ['float-left', 'mr-3']
}
// Results in:
class: 'float-left mr-3'
width
type: String (pixels without unit)
default: ''
Purpose: Image display width in pixels
Format: Numeric string without 'px' unit
Examples:
width: '800'
width: '1200'
Usage:
writer.setAttribute('width', '800', modelElement);
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);
height
type: String (pixels without unit)
default: ''
Purpose: Image display height in pixels
Format: Numeric string without 'px' unit
Examples:
height: '600'
height: '900'
Usage:
writer.setAttribute('height', '600', modelElement);
enableZoom
type: Boolean
default: false
Purpose: Enable zoom/click-to-enlarge functionality (TYPO3-specific feature)
Behavior:
true: Image becomes clickable, opens larger versionfalse: Image is static, no click interaction
Example:
enableZoom: true
Frontend Rendering:
<!-- When enableZoom is true -->
<a href="large-image.jpg" data-lightbox="gallery">
<img src="thumb.jpg" data-htmlarea-zoom="true" />
</a>
Usage:
writer.setAttribute('enableZoom', true, modelElement);
Link Attributes
htmlA
type: String
default: undefined
Purpose: Complete HTML anchor tag wrapping the image (legacy attribute)
Format: Full HTML string
Example:
htmlA: '<a href="/page/123" target="_blank" title="Link title">...</a>'
Note
This is a legacy attribute. Modern approach uses separate link attributes.
linkHref
type: String
default: undefined
Purpose: Link URL when image is wrapped in anchor tag
Examples:
linkHref: '/page/detail'
linkHref: 'https://example.com'
linkHref: 'mailto:info@example.com'
Usage:
writer.setAttribute('linkHref', '/page/123', modelElement);
linkTarget
type: String
default: undefined
Purpose: Link target attribute (_blank, _self, _parent, _top)
Valid Values: '_blank', '_self', '_parent', '_top', or named frame
Example:
linkTarget: '_blank'
Usage:
writer.setAttribute('linkTarget', '_blank', modelElement);
linkTitle
type: String
default: undefined
Purpose: Title attribute for the wrapping anchor tag
Example:
linkTitle: 'Click to view product details'
Usage:
writer.setAttribute('linkTitle', 'Link description', modelElement);
linkClass
New in version 13.5.0
type: String
default: undefined
Purpose: CSS class for the wrapping anchor tag
Example:
linkClass: 'external-link btn-primary'
Usage:
writer.setAttribute('linkClass', 'my-link-class', modelElement);
linkParams
New in version 13.5.0
type: String
default: undefined
Purpose: Additional URL parameters for the link (TypoLink additionalParams)
Format: Query string starting with & (e.g., &L=1&type=123)
Example:
linkParams: '&L=1&type=123'
Usage:
writer.setAttribute('linkParams', '&cHash=abc123', modelElement);
TypoLink Integration:
This corresponds to the fifth parameter in TYPO3's TypoLink format:
url target class "title" additionalParams
↓ ↓
linkHref linkParams
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);
});
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);
}
});
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], ...]
}
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);
}
});
Model Selection
Selecting Elements
// Select element programmatically
editor.model.change(writer => {
const element = /* get element reference */;
writer.setSelection(element, 'on'); // 'on' = select element itself
});
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');
}
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'));
}
}
Model Traversal
Finding Parent Elements
const element = /* typo3image element */;
const parent = element.parent;
console.log(parent.name); // e.g., 'paragraph', '$root'
Finding Previous/Next Siblings
const element = /* typo3image element */;
const previousSibling = element.previousSibling;
const nextSibling = element.nextSibling;
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());
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'
]
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;
});
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
});
}
}
});
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);
}
}
});
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);
});
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);
});
});
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);
}
});
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'));
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);
});
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());