---
title: "CKEditor Plugin Development"
manual: "RTE CKEditor Image"
version: "main"
permalink: "https://docs.typo3.org/permalink/netresearch/rte-ckeditor-image:ckeditor-plugin-development-guide@main"
source: "CKEditor/Plugin-Development.rst"
rendered: "2026-10-01T01:27:51+00:00"
---

# CKEditor Plugin Development {#ckeditor-plugin-development-guide}

Complete guide to the Typo3Image CKEditor 5 plugin architecture and development patterns.

## Plugin Overview {#plugin-overview}

**File**: `Resources/Public/JavaScript/Plugins/typo3image.js`

**Plugin Class**: `Typo3Image extends Core.Plugin`

**Required Dependencies**:

```javascript
static get requires() {
    return ['StyleUtils', 'GeneralHtmlSupport'];
}
```

> [!WARNING]
> Both `StyleUtils` and `GeneralHtmlSupport` are **mandatory**. Missing them causes style functionality to fail.

## Plugin Structure {#plugin-structure}

```javascript
export default class Typo3Image extends Core.Plugin {
    static pluginName = 'Typo3Image';

    static get requires() {
        return ['StyleUtils', 'GeneralHtmlSupport'];
    }

    init() {
        // Plugin initialization
        // - Define schema
        // - Register conversions
        // - Add UI components
        // - Register event listeners
    }
}
```

## Custom Model Element: typo3image {#custom-model-element-typo3image}

### Schema Definition {#schema-definition}

```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'
    ],
});
```

### Attribute Descriptions {#attribute-descriptions}

| Attribute | Type | Description |
| --- | --- | --- |
| src | string | Image source URL |
| fileUid | number | TYPO3 FAL file UID |
| fileTable | string | Database table (default: 'sys_file') |
| alt | string | Alternative text |
| altOverride | boolean | Alt text override flag |
| title | string | Advisory title |
| titleOverride | boolean | Title override flag |
| class | string | CSS classes (space-separated) |
| enableZoom | boolean | Zoom/clickenlarge functionality |
| width | string | Image width |
| height | string | Image height |
| htmlA | string | Link wrapper HTML |
| linkHref | string | Link URL |
| linkTarget | string | Link target |
| linkTitle | string | Link title |

## Conversion System {#conversion-system}

### Upcast: HTML → Model {#upcast-html-model}

Converts `<img>` elements with FAL attributes to `typo3image` model elements:

```javascript
editor.conversion.for('upcast').elementToElement({
    view: {
        name: 'img',
        attributes: ['data-htmlarea-file-uid', 'src']
    },
    model: (viewElement, { writer }) => {
        return writer.createElement('typo3image', {
            fileUid: viewElement.getAttribute('data-htmlarea-file-uid'),
            fileTable: viewElement.getAttribute('data-htmlarea-file-table') || 'sys_file',
            src: viewElement.getAttribute('src'),
            width: viewElement.getAttribute('width') || '',
            height: viewElement.getAttribute('height') || '',
            class: viewElement.getAttribute('class') || '',
            alt: viewElement.getAttribute('alt') || '',
            altOverride: viewElement.getAttribute('data-alt-override') || false,
            title: viewElement.getAttribute('title') || '',
            titleOverride: viewElement.getAttribute('data-title-override') || false,
            enableZoom: viewElement.getAttribute('data-htmlarea-zoom') || false,
        });
    }
});
```

### Downcast: Model → HTML {#downcast-model-html}

Converts `typo3image` model elements to `<img>` HTML:

```javascript
editor.conversion.for('downcast').elementToElement({
    model: {
        name: 'typo3image',
        attributes: ['fileUid', 'fileTable', 'src']
    },
    view: (modelElement, { writer }) => {
        const attributes = {
            'src': modelElement.getAttribute('src'),
            'data-htmlarea-file-uid': modelElement.getAttribute('fileUid'),
            'data-htmlarea-file-table': modelElement.getAttribute('fileTable'),
            'width': modelElement.getAttribute('width'),
            'height': modelElement.getAttribute('height'),
            'class': modelElement.getAttribute('class') || '',
            'title': modelElement.getAttribute('title') || '',
            'alt': modelElement.getAttribute('alt') || '',
        };

        if (modelElement.getAttribute('titleOverride')) {
            attributes['data-title-override'] = true;
        }
        if (modelElement.getAttribute('altOverride')) {
            attributes['data-alt-override'] = true;
        }
        if (modelElement.getAttribute('enableZoom')) {
            attributes['data-htmlarea-zoom'] = true;
        }

        return writer.createEmptyElement('img', attributes);
    },
});
```

### Class Attribute Converter {#class-attribute-converter}

Makes class changes immediately visible in the editor:

```javascript
editor.conversion.for('downcast').attributeToAttribute({
    model: { name: 'typo3image', key: 'class' },
    view: 'class'
});
```

## UI Components {#ui-components}

### Insert Image Button {#insert-image-button}

Registered in `editor.ui.componentFactory`:

```javascript
editor.ui.componentFactory.add('insertimage', () => {
    const button = new UI.ButtonView();

    button.set({
        label: 'Insert image',
        icon: '<svg>...</svg>',
        tooltip: true,
        withText: false,
    });

    button.on('execute', () => {
        const selectedElement = editor.model.document.selection.getSelectedElement();

        if (selectedElement && selectedElement.name === 'typo3image') {
            // Edit existing image
            edit(selectedElement, editor, attributes);
        } else {
            // Insert new image
            selectImage(editor).then(selectedImage => {
                edit(selectedImage, editor, {});
            });
        }
    });

    return button;
});
```

## Image Selection Flow {#image-selection-flow}

### selectImage() Function {#selectimage-function}

Opens TYPO3 Modal with file browser:

```javascript
function selectImage(editor) {
    let resolvePromise;
    const promise = new Promise((resolve) => { resolvePromise = resolve; });

    const bparams = ['', '', '', ''];
    const contentUrl = editor.config.get('style').typo3image.routeUrl
        + '&contentsLanguage=en&editorId=123&bparams=' + bparams.join('|');

    const modal = Modal.advanced({
        type: Modal.types.iframe,
        title: 'Select Image',
        content: contentUrl,
        size: Modal.sizes.large,
        callback: function (currentModal) {
            const iframe = currentModal.querySelector('iframe');
            iframe.addEventListener('load', () => {
                const doc = iframe.contentDocument;
                doc.addEventListener('click', (e) => {
                    const el = e.target.closest('[data-filelist-element]');
                    if (!el || el.dataset.filelistType !== 'file') return;

                    const selectedItem = {
                        uid: el.dataset.filelistUid,
                        table: 'sys_file',
                    };
                    currentModal.hideModal();
                    resolvePromise(selectedItem);
                });
            });
        }
    });

    return promise;
}
```

## Image Properties Dialog {#image-properties-dialog}

### getImageDialog() Function {#getimagedialog-function}

Creates image properties form:

```javascript
function getImageDialog(editor, img, attributes) {
    const d = {};
    const fields = [
        {
            width: { label: 'Width', type: 'number' },
            height: { label: 'Height', type: 'number' }
        },
        {
            title: { label: 'Advisory Title', type: 'text' },
            alt: { label: 'Alternative Text', type: 'text' }
        }
    ];

    // Create form elements using native DOM
    const container = document.createElement('div');
    container.className = 'rteckeditorimage';
    d.el = container;

    // ... form generation code ...

    // Aspect ratio preservation for width/height
    el.addEventListener('input', () => {
        const ratio = img.width / img.height;
        const newHeight = Math.ceil(newWidth / ratio);
        oppositeEl.value = newHeight;
    });

    // Override checkboxes for title/alt
    cboxLabel.addEventListener('click', () => {
        el.disabled = !cbox.checked;
        if (!cbox.checked) {
            el.value = '';  // Clear custom value
        }
    });

    d.get = function () {
        // Returns filtered attributes for allowed list
        return filteredAttributes;
    };

    return d;
}
```

### Dialog Features {#dialog-features}

-   **Width/Height**: Number inputs with aspect ratio preservation
-   **Title/Alt**: Text inputs with override checkboxes
-   **Zoom**: Checkbox for clickenlarge functionality
-   **CSS Class**: Text input for custom classes

## Style System Integration {#style-system-integration}

Critical for CKEditor style drop-down functionality.

### Event Listener: isStyleEnabledForBlock {#event-listener-isstyleenabledforblock}

Enables img styles when typo3image is selected:

```javascript
this.listenTo(styleUtils, 'isStyleEnabledForBlock', (event, [style, element]) => {
    if (style.element === 'img') {
        for (const item of editor.model.document.selection.getFirstRange().getItems()) {
            if (item.name === 'typo3image') {
                event.return = true;
            }
        }
    }
});
```

### Event Listener: isStyleActiveForBlock {#event-listener-isstyleactiveforblock}

Checks if style is currently applied:

```javascript
this.listenTo(styleUtils, 'isStyleActiveForBlock', (event, [style, element]) => {
    if (style.element === 'img') {
        for (const item of editor.model.document.selection.getFirstRange().getItems()) {
            if (item.name === 'typo3image') {
                const classAttribute = item.getAttribute('class');
                if (classAttribute && typeof classAttribute === 'string') {
                    const classlist = classAttribute.split(' ');
                    if (style.classes.filter(value => !classlist.includes(value)).length === 0) {
                        event.return = true;
                    }
                }
            }
        }
    }
});
```

### Event Listener: getAffectedBlocks {#event-listener-getaffectedblocks}

Returns correct model element for style operations:

```javascript
this.listenTo(styleUtils, 'getAffectedBlocks', (event, [style, element]) => {
    if (style.element === 'img') {
        for (const item of editor.model.document.selection.getFirstRange().getItems()) {
            if (item.name === 'typo3image') {
                event.return = [item];
                break;
            }
        }
    }
});
```

## GeneralHtmlSupport Integration {#generalhtmlsupport-integration}

Manages class attribute updates from style system.

### addModelHtmlClass Listener {#addmodelhtmlclass-listener}

```javascript
const ghs = editor.plugins.get('GeneralHtmlSupport');
ghs.decorate('addModelHtmlClass');

this.listenTo(ghs, 'addModelHtmlClass', (event, [viewElement, className, selectable]) => {
    if (selectable && selectable.name === 'typo3image') {
        editor.model.change(writer => {
            writer.setAttribute('class', className.join(' '), selectable);
        });
    }
});
```

### removeModelHtmlClass Listener {#removemodelhtmlclass-listener}

```javascript
ghs.decorate('removeModelHtmlClass');

this.listenTo(ghs, 'removeModelHtmlClass', (event, [viewElement, className, selectable]) => {
    if (selectable && selectable.name === 'typo3image') {
        editor.model.change(writer => {
            writer.removeAttribute('class', selectable);
        });
    }
});
```

## Event Observers {#event-observers}

### DoubleClickObserver {#doubleclickobserver}

Custom observer for double-click detection:

```javascript
class DoubleClickObserver extends Engine.DomEventObserver {
    constructor(view) {
        super(view);
        this.domEventType = 'dblclick';
    }

    onDomEvent(domEvent) {
        this.fire(domEvent.type, domEvent);
    }
}

// Register observer
editor.editing.view.addObserver(DoubleClickObserver);

// Listen for double-click
editor.listenTo(editor.editing.view.document, 'dblclick', (event, data) => {
    const modelElement = editor.editing.mapper.toModelElement(data.target);
    if (modelElement && modelElement.name === 'typo3image') {
        // Open edit dialog
        edit({...}, editor, {...});
    }
});
```

### Click Handler {#click-handler}

Single-click selects image:

```javascript
editor.listenTo(editor.editing.view.document, 'click', (event, data) => {
    const modelElement = editor.editing.mapper.toModelElement(data.target);
    if (modelElement && modelElement.name === 'typo3image') {
        editor.model.change(writer => {
            writer.setSelection(modelElement, 'on');
        });
    }
});
```

## Backend API Integration {#backend-api-integration}

### getImageInfo() Function {#getimageinfo-function}

Fetches image data from backend:

```javascript
function getImageInfo(editor, table, uid, params) {
    let url = editor.config.get('style').typo3image.routeUrl
        + '&action=info&fileId=' + encodeURIComponent(uid)
        + '&table=' + encodeURIComponent(table)
        + '&contentsLanguage=en&editorId=123';

    if (params.width) {
        url += '&P[width]=' + params.width;
    }
    if (params.height) {
        url += '&P[height]=' + params.height;
    }

    const response = await fetch(url);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
}
```

## Plugin Configuration {#plugin-configuration}

### Registration (Configuration/RTE/Plugin.yaml) {#registration-configuration-rte-plugin-yaml}

```yaml
editor:
  config:
    importModules:
      - '@netresearch/rte-ckeditor-image/Plugins/typo3image.js'

  externalPlugins:
    typo3image: { route: "rteckeditorimage_wizard_select_image" }

processing:
  allowTagsOutside:
    - img
```

### JavaScript Module Registration {#javascript-module-registration}

```php
// Configuration/JavaScriptModules.php
return [
    'dependencies' => ['rte_ckeditor'],
    'tags' => ['backend.form'],
    'imports' => [
        '@netresearch/rte-ckeditor-image/' => 'EXT:rte_ckeditor_image/Resources/Public/JavaScript/',
    ],
];
```

## Development Tips {#development-tips}

1.  **Always test style integration** \- Verify StyleUtils and GeneralHtmlSupport work correctly
1.  **Use browser console** \- Monitor CKEditor model changes with `editor.model.document.on('change')`
1.  **Check conversions** \- Verify upcast/downcast produce expected results
1.  **Test attribute updates** \- Ensure class and other attributes update correctly
1.  **Debug with breakpoints** \- Use browser DevTools to step through plugin code

## Related Documentation {#related-documentation}

-   [CKEditor Model Element Reference](https://docs.typo3.org/permalink/netresearch/rte-ckeditor-image:ckeditor-model-element@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)
