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
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'
],
});
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 fromalt
attribute
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 fromtitle
attribute
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);
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'
]
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());