---
title: "CKEditor Model Element Reference"
manual: "RTE CKEditor Image"
version: "main"
permalink: "https://docs.typo3.org/permalink/netresearch/rte-ckeditor-image:ckeditor-model-element@main"
source: "CKEditor/Model-Element.rst"
rendered: "2026-10-01T01:27:51+00:00"
---

# CKEditor Model Element Reference {#ckeditor-model-element}

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

## Overview {#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 {#model-vs-view-architecture}

### Understanding CKEditor 5 Architecture {#understanding-ckeditor-5-architecture}

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

```php
┌─────────────────────────────────────────────────┐
│ 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? {#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 {#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) {#block-image-schema-typo3image}

```javascript
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) {#inline-image-schema-typo3imageinline}

<!-- TODO: no Markdown rendering for "versionadded" -->

True inline images with cursor positioning support.

```javascript
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 {#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 {#usage-example}

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 {#toggle-command}

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

```javascript
// 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-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 {#schema-properties-explained}

#### inheritAllFrom: '$blockObject' {#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'\] {#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: \[...\] {#allowattributes}

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

## Attribute Reference {#attribute-reference}

### Core Attributes {#core-attributes}

#### src {#src}

```javascript
type: String
required: true
```

**Purpose**: Image source URL (absolute or relative)

**Examples**:

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

**Usage**:

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

#### fileUid {#fileuid}

```javascript
type: Number
required: true (for TYPO3 FAL integration)
```

**Purpose**: TYPO3 File Abstraction Layer file UID

**Range**: Positive integer matching `sys_file.uid`

**Example**:

```javascript
fileUid: 123
```

**Usage**:

```javascript
const fileUid = modelElement.getAttribute('fileUid');
writer.setAttribute('fileUid', 456, modelElement);
```

**Backend Integration**:

```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 {#filetable}

```javascript
type: String
default: 'sys_file'
```

**Purpose**: Database table name for file reference

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

**Example**:

```javascript
fileTable: 'sys_file'
```

**Usage**:

```javascript
const table = modelElement.getAttribute('fileTable') || 'sys_file';
```

### Metadata Attributes {#metadata-attributes}

#### alt {#alt}

```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**:

```javascript
alt: 'Product photo showing red widget from front angle'
```

**Usage**:

```javascript
writer.setAttribute('alt', 'New alt text', modelElement);
```

#### altOverride {#altoverride}

```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**:

```javascript
altOverride: true  // Custom alt text takes precedence
```

**Usage Pattern**:

```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 {#title}

```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**:

```javascript
title: 'Click to view full size'
```

**Usage**:

```javascript
writer.setAttribute('title', 'Tooltip text', modelElement);
```

#### titleOverride {#titleoverride}

```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**:

```javascript
titleOverride: true
```

### Visual Attributes {#visual-attributes}

#### class {#class}

```javascript
type: String
default: ''
```

**Purpose**: Space-separated CSS class names for styling

**Style Integration**: Modified by CKEditor style system via `GeneralHtmlSupport`

**Examples**:

```javascript
class: 'float-left img-responsive'
class: 'img-thumbnail d-block mx-auto'
```

**Usage**:

```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**:

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

// Results in:
class: 'float-left mr-3'
```

#### width {#width}

```javascript
type: String (pixels without unit)
default: ''
```

**Purpose**: Image display width in pixels

**Format**: Numeric string without 'px' unit

**Examples**:

```javascript
width: '800'
width: '1200'
```

**Usage**:

```javascript
writer.setAttribute('width', '800', modelElement);
```

**Aspect Ratio Preservation**:

```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 {#height}

```javascript
type: String (pixels without unit)
default: ''
```

**Purpose**: Image display height in pixels

**Format**: Numeric string without 'px' unit

**Examples**:

```javascript
height: '600'
height: '900'
```

**Usage**:

```javascript
writer.setAttribute('height', '600', modelElement);
```

#### enableZoom {#enablezoom}

```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**:

```javascript
enableZoom: true
```

**Frontend Rendering**:

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

**Usage**:

```javascript
writer.setAttribute('enableZoom', true, modelElement);
```

### Link Attributes {#link-attributes}

#### htmlA {#htmla}

```javascript
type: String
default: undefined
```

**Purpose**: Complete HTML anchor tag wrapping the image (legacy attribute)

**Format**: Full HTML string

**Example**:

```javascript
htmlA: '<a href="/page/123" target="_blank" title="Link title">...</a>'
```

> [!NOTE]
> This is a legacy attribute. Modern approach uses separate link attributes.

#### linkHref {#linkhref}

```javascript
type: String
default: undefined
```

**Purpose**: Link URL when image is wrapped in anchor tag

**Examples**:

```javascript
linkHref: '/page/detail'
linkHref: 'https://example.com'
linkHref: 'mailto:info@example.com'
```

**Usage**:

```javascript
writer.setAttribute('linkHref', '/page/123', modelElement);
```

#### linkTarget {#linktarget}

```javascript
type: String
default: undefined
```

**Purpose**: Link target attribute (\_blank, \_self, \_parent, \_top)

**Valid Values**: `'_blank'`, `'_self'`, `'_parent'`, `'_top'`, or named frame

**Example**:

```javascript
linkTarget: '_blank'
```

**Usage**:

```javascript
writer.setAttribute('linkTarget', '_blank', modelElement);
```

#### linkTitle {#linktitle}

```javascript
type: String
default: undefined
```

**Purpose**: Title attribute for the wrapping anchor tag

**Example**:

```javascript
linkTitle: 'Click to view product details'
```

**Usage**:

```javascript
writer.setAttribute('linkTitle', 'Link description', modelElement);
```

#### linkClass {#linkclass}

<!-- TODO: no Markdown rendering for "versionadded" -->

```javascript
type: String
default: undefined
```

**Purpose**: CSS class for the wrapping anchor tag

**Example**:

```javascript
linkClass: 'external-link btn-primary'
```

**Usage**:

```javascript
writer.setAttribute('linkClass', 'my-link-class', modelElement);
```

#### linkParams {#linkparams}

<!-- TODO: no Markdown rendering for "versionadded" -->

```javascript
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**:

```javascript
linkParams: '&L=1&type=123'
```

**Usage**:

```javascript
writer.setAttribute('linkParams', '&cHash=abc123', modelElement);
```

**TypoLink Integration**:

This corresponds to the fifth parameter in TYPO3's TypoLink format:

```php
url target class "title" additionalParams
↓                         ↓
linkHref                 linkParams

```

## Working with Model Elements {#working-with-model-elements}

### Creating Model Elements {#creating-model-elements}

```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 {#updating-attributes}

```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 {#reading-attributes}

```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 {#removing-attributes}

```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 {#model-selection}

### Selecting Elements {#selecting-elements}

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

### Getting Selected Element {#getting-selected-element}

```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 {#iterating-selection-range}

```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 {#model-traversal}

### Finding Parent Elements {#finding-parent-elements}

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

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

### Finding Previous/Next Siblings {#finding-previous-next-siblings}

```javascript
const element = /* typo3image element */;
const previousSibling = element.previousSibling;
const nextSibling = element.nextSibling;
```

### Walking the Model Tree {#walking-the-model-tree}

```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 {#attribute-validation}

### Allowed Attributes Enforcement {#allowed-attributes-enforcement}

CKEditor automatically strips attributes not in `allowAttributes` list:

```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',
    'linkClass', 'linkParams'
]
```

### Custom Validation {#custom-validation}

```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 {#model-change-listeners}

### Listening to Attribute Changes {#listening-to-attribute-changes}

```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 {#listening-to-element-insertion}

```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 {#advanced-patterns}

### Cloning Elements {#cloning-elements}

```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 {#batch-attribute-updates}

```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 {#conditional-attribute-setting}

```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 {#debugging-model-elements}

### Inspect Element in Console {#inspect-element-in-console}

```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 {#monitor-model-changes}

```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 {#visualize-model-structure}

```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 {#related-documentation}

-   [CKEditor Plugin Development](https://docs.typo3.org/permalink/netresearch/rte-ckeditor-image:ckeditor-plugin-development-guide@main)
-   [CKEditor Style Integration](https://docs.typo3.org/permalink/netresearch/rte-ckeditor-image:ckeditor-style-integration@main)
-   [CKEditor Conversion System](https://docs.typo3.org/permalink/netresearch/rte-ckeditor-image:ckeditor-conversions@main)
-   [System Architecture](https://docs.typo3.org/permalink/netresearch/rte-ckeditor-image:architecture-overview@main)
