.. _ckeditor-model-element:
==================================
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 │
│ -
elements with HTML attributes │
│ - User sees and interacts with │
└──────────────────┬──────────────────────────────┘
│
│ Rendering
│
┌──────────────────▼──────────────────────────────┐
│ DOM (Browser) │
│ - Actual HTML in contenteditable │
│ -
│
└─────────────────────────────────────────────────┘
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
=================
.. code-block:: javascript
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
^^^
.. code-block:: javascript
type: String
required: true
**Purpose**: Image source URL (absolute or relative)
**Examples**:
.. code-block:: javascript
src: '/fileadmin/user_upload/image.jpg'
src: '/fileadmin/_processed_/a/b/csm_image_123.jpg'
src: 'https://example.com/external.jpg'
**Usage**:
.. code-block:: javascript
const src = modelElement.getAttribute('src');
writer.setAttribute('src', '/new/path.jpg', modelElement);
fileUid
^^^^^^^
.. code-block:: javascript
type: Number
required: true (for TYPO3 FAL integration)
**Purpose**: TYPO3 File Abstraction Layer file UID
**Range**: Positive integer matching ``sys_file.uid``
**Example**:
.. code-block:: javascript
fileUid: 123
**Usage**:
.. code-block:: javascript
const fileUid = modelElement.getAttribute('fileUid');
writer.setAttribute('fileUid', 456, modelElement);
**Backend Integration**:
.. code-block:: javascript
// Fetch file info from backend
const fileUid = modelElement.getAttribute('fileUid');
const fileInfo = await fetch(
routeUrl + '&action=info&fileId=' + fileUid
).then(r => r.json());
fileTable
^^^^^^^^^
.. code-block:: javascript
type: String
default: 'sys_file'
**Purpose**: Database table name for file reference
**Valid Values**: ``'sys_file'`` (default), ``'sys_file_reference'`` (rarely used)
**Example**:
.. code-block:: javascript
fileTable: 'sys_file'
**Usage**:
.. code-block:: javascript
const table = modelElement.getAttribute('fileTable') || 'sys_file';
Metadata Attributes
-------------------
alt
^^^
.. code-block:: javascript
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**:
.. code-block:: javascript
alt: 'Product photo showing red widget from front angle'
**Usage**:
.. code-block:: javascript
writer.setAttribute('alt', 'New alt text', modelElement);
altOverride
^^^^^^^^^^^
.. code-block:: javascript
type: Boolean
default: false
**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**:
.. code-block:: javascript
altOverride: true // Custom alt text takes precedence
**Usage Pattern**:
.. code-block:: javascript
// 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
^^^^^
.. code-block:: javascript
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**:
.. code-block:: javascript
title: 'Click to view full size'
**Usage**:
.. code-block:: javascript
writer.setAttribute('title', 'Tooltip text', modelElement);
titleOverride
^^^^^^^^^^^^^
.. code-block:: javascript
type: Boolean
default: false
**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**:
.. code-block:: javascript
titleOverride: true
Visual Attributes
-----------------
class
^^^^^
.. code-block:: javascript
type: String
default: ''
**Purpose**: Space-separated CSS class names for styling
**Style Integration**: Modified by CKEditor style system via ``GeneralHtmlSupport``
**Examples**:
.. code-block:: javascript
class: 'float-left img-responsive'
class: 'img-thumbnail d-block mx-auto'
**Usage**:
.. code-block:: javascript
// 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**:
.. code-block:: javascript
// CKEditor style definition
{
name: 'Image Left',
element: 'img',
classes: ['float-left', 'mr-3']
}
// Results in:
class: 'float-left mr-3'
width
^^^^^
.. code-block:: javascript
type: String (pixels without unit)
default: ''
**Purpose**: Image display width in pixels
**Format**: Numeric string without 'px' unit
**Examples**:
.. code-block:: javascript
width: '800'
width: '1200'
**Usage**:
.. code-block:: javascript
writer.setAttribute('width', '800', modelElement);
**Aspect Ratio Preservation**:
.. code-block:: javascript
// 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
^^^^^^
.. code-block:: javascript
type: String (pixels without unit)
default: ''
**Purpose**: Image display height in pixels
**Format**: Numeric string without 'px' unit
**Examples**:
.. code-block:: javascript
height: '600'
height: '900'
**Usage**:
.. code-block:: javascript
writer.setAttribute('height', '600', modelElement);
enableZoom
^^^^^^^^^^
.. code-block:: javascript
type: Boolean
default: false
**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**:
.. code-block:: javascript
enableZoom: true
**Frontend Rendering**:
.. code-block:: html
**Usage**:
.. code-block:: javascript
writer.setAttribute('enableZoom', true, modelElement);
Link Attributes
---------------
htmlA
^^^^^
.. code-block:: javascript
type: String
default: undefined
**Purpose**: Complete HTML anchor tag wrapping the image (legacy attribute)
**Format**: Full HTML string
**Example**:
.. code-block:: javascript
htmlA: '...'
.. note::
This is a legacy attribute. Modern approach uses separate link attributes.
linkHref
^^^^^^^^
.. code-block:: javascript
type: String
default: undefined
**Purpose**: Link URL when image is wrapped in anchor tag
**Examples**:
.. code-block:: javascript
linkHref: '/page/detail'
linkHref: 'https://example.com'
linkHref: 'mailto:info@example.com'
**Usage**:
.. code-block:: javascript
writer.setAttribute('linkHref', '/page/123', modelElement);
linkTarget
^^^^^^^^^^
.. code-block:: javascript
type: String
default: undefined
**Purpose**: Link target attribute (_blank, _self, _parent, _top)
**Valid Values**: ``'_blank'``, ``'_self'``, ``'_parent'``, ``'_top'``, or named frame
**Example**:
.. code-block:: javascript
linkTarget: '_blank'
**Usage**:
.. code-block:: javascript
writer.setAttribute('linkTarget', '_blank', modelElement);
linkTitle
^^^^^^^^^
.. code-block:: javascript
type: String
default: undefined
**Purpose**: Title attribute for the wrapping anchor tag
**Example**:
.. code-block:: javascript
linkTitle: 'Click to view product details'
**Usage**:
.. code-block:: javascript
writer.setAttribute('linkTitle', 'Link description', modelElement);
Working with Model Elements
============================
Creating Model Elements
-----------------------
.. code-block:: javascript
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
-------------------
.. code-block:: javascript
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
------------------
.. code-block:: javascript
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
-------------------
.. code-block:: javascript
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
------------------
.. code-block:: javascript
// Select element programmatically
editor.model.change(writer => {
const element = /* get element reference */;
writer.setSelection(element, 'on'); // 'on' = select element itself
});
Getting Selected Element
-------------------------
.. code-block:: javascript
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
--------------------------
.. code-block:: javascript
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
-----------------------
.. code-block:: javascript
const element = /* typo3image element */;
const parent = element.parent;
console.log(parent.name); // e.g., 'paragraph', '$root'
Finding Previous/Next Siblings
-------------------------------
.. code-block:: javascript
const element = /* typo3image element */;
const previousSibling = element.previousSibling;
const nextSibling = element.nextSibling;
Walking the Model Tree
----------------------
.. code-block:: javascript
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:
.. code-block:: javascript
// 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
-----------------
.. code-block:: javascript
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
-------------------------------
.. code-block:: javascript
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
-------------------------------
.. code-block:: javascript
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
----------------
.. code-block:: javascript
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
-----------------------
.. code-block:: javascript
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
------------------------------
.. code-block:: javascript
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
---------------------------
.. code-block:: javascript
// 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
---------------------
.. code-block:: javascript
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
--------------------------
.. code-block:: javascript
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());
Related Documentation
=====================
- :ref:`ckeditor-plugin-development-guide`
- :ref:`ckeditor-style-integration`
- :ref:`ckeditor-conversions`
- :ref:`architecture-overview`