CKEditor Plugin Development
Complete guide to the Typo3Image CKEditor 5 plugin architecture and development patterns.
Plugin Overview
File: Resources/Public/JavaScript/Plugins/typo3image.js
Plugin Class: Typo3Image extends Core.Plugin
Required Dependencies:
static get requires() {
return ['StyleUtils', 'GeneralHtmlSupport'];
}
Warning
Both StyleUtils
and GeneralHtmlSupport
are mandatory. Missing them causes style functionality to fail.
Plugin Structure
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
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'
],
});
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
Upcast: HTML → Model
Converts <img>
elements with FAL attributes to typo3image
model elements:
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
Converts typo3image
model elements to <img>
HTML:
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
Makes class changes immediately visible in the editor:
editor.conversion.for('downcast').attributeToAttribute({
model: { name: 'typo3image', key: 'class' },
view: 'class'
});
UI Components
Insert Image Button
Registered in editor.ui.componentFactory
:
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
selectImage() Function
Opens TYPO3 Modal with file browser:
function selectImage(editor) {
const deferred = $.Deferred();
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) {
$(currentModal).find('iframe').on('load', function (e) {
$(this).contents().on('click', '[data-filelist-element]', function (e) {
if ($(this).data('filelist-type') !== 'file') {
return;
}
const selectedItem = {
uid: $(this).data('filelist-uid'),
table: 'sys_file',
};
currentModal.hideModal();
deferred.resolve(selectedItem);
});
});
}
});
return deferred;
}
Image Properties Dialog
getImageDialog() Function
Creates image properties form:
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
d.$el = $('<div class="rteckeditorimage">');
// ... form generation code ...
// Aspect ratio preservation for width/height
$el.on('input', function () {
const ratio = img.width / img.height;
const newHeight = Math.ceil(newWidth / ratio);
$opposite.val(newHeight);
});
// Override checkboxes for title/alt
cbox.on('click', function () {
$el.prop('disabled', !cbox.prop('checked'));
if (!cbox.prop('checked')) {
$el.val(''); // Clear custom value
}
});
d.get = function () {
// Returns filtered attributes for allowed list
return filteredAttributes;
};
return d;
}
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
Critical for CKEditor style drop-down functionality.
Event Listener: isStyleEnabledForBlock
Enables img styles when typo3image is selected:
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
Checks if style is currently applied:
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
Returns correct model element for style operations:
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
Manages class attribute updates from style system.
addModelHtmlClass Listener
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
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
DoubleClickObserver
Custom observer for double-click detection:
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
Single-click selects image:
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
getImageInfo() Function
Fetches image data from backend:
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;
}
return $.getJSON(url);
}
Plugin Configuration
Registration (Configuration/RTE/Plugin.yaml)
editor:
config:
importModules:
- '@netresearch/rte-ckeditor-image/Plugins/typo3image.js'
externalPlugins:
typo3image: { route: "rteckeditorimage_wizard_select_image" }
processing:
allowTagsOutside:
- img
JavaScript Module Registration
// Configuration/JavaScriptModules.php
return [
'dependencies' => ['rte_ckeditor'],
'tags' => ['backend.form'],
'imports' => [
'@netresearch/rte-ckeditor-image/' => 'EXT:rte_ckeditor_image/Resources/Public/JavaScript/',
],
];
Development Tips
- Always test style integration - Verify StyleUtils and GeneralHtmlSupport work correctly
- Use browser console - Monitor CKEditor model changes with
editor.model.document.on('change')
- Check conversions - Verify upcast/downcast produce expected results
- Test attribute updates - Ensure class and other attributes update correctly
- Debug with breakpoints - Use browser DevTools to step through plugin code