CKEditor Style Integration
Complete guide to integrating the typo3image plugin with CKEditor's style system (StyleUtils and GeneralHtmlSupport).
Overview
New in version 13.0.0
Integration with GeneralHtmlSupport
is now required for style functionality.
Previous versions only required StyleUtils
, which caused the style dropdown
to be disabled for images.
Critical Dependencies:
static get requires() {
return ['StyleUtils', 'GeneralHtmlSupport'];
}
Warning
Both StyleUtils
and GeneralHtmlSupport
are mandatory for style functionality. Missing either plugin causes style drop-down to be disabled for images.
The Style System Problem
Before v13.0.0 (Broken)
// Missing GeneralHtmlSupport dependency
static get requires() {
return ['StyleUtils']; // Incomplete!
}
Issue: Style drop-down disabled when image selected
Symptoms:
- Styles grayed out when typo3image selected
- Class changes not applied to images
- No visual feedback when applying styles
After v13.0.0 (Fixed)
// Both dependencies required
static get requires() {
return ['StyleUtils', 'GeneralHtmlSupport'];
}
Result: Full style system integration working correctly
Style System Architecture
Three-Layer Integration
┌─────────────────────────────────────────┐
│ StyleUtils Plugin │
│ - Manages style definitions │
│ - Provides event system │
│ - Determines style applicability │
└───────────┬─────────────────────────────┘
│
│ Events
│
┌───────────▼─────────────────────────────┐
│ Typo3Image Plugin │
│ - Listens to StyleUtils events │
│ - Reports typo3image availability │
│ - Returns correct model elements │
└───────────┬─────────────────────────────┘
│
│ Operations
│
┌───────────▼─────────────────────────────┐
│ GeneralHtmlSupport Plugin │
│ - Applies class changes to model │
│ - Manages HTML attribute manipulation │
│ - Ensures class sync with view │
└─────────────────────────────────────────┘
StyleUtils Event System
Event: isStyleEnabledForBlock
Purpose: Determines if a style can be applied to the selected element
When Fired: User selects element, style drop-down needs update
Default Behavior: Only enable styles for elements matching style definition
typo3image Override:
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; // Enable img styles for typo3image
}
}
}
});
Logic Breakdown:
-
Check Style Element:
if (style.element === 'img')
- Only process styles defined for
<img>
elements - Ignore styles for other elements (p, h1, etc.)
- Only process styles defined for
-
Iterate Selection:
for (const item of ...getFirstRange().getItems())
- Get all items in current selection range
- Check if any item is a typo3image
-
Enable Style:
event.return = true
- Tell StyleUtils that img styles ARE applicable to typo3image
- Without this, style drop-down would be disabled
Note
CKEditor doesn't natively know that typo3image
(model element) corresponds to <img>
(view element). This listener bridges that gap.
Event: isStyleActiveForBlock
Purpose: Checks if a style is currently active (applied) on selected element
When Fired: User selects element, style drop-down shows active styles
Default Behavior: Check if element has required classes
typo3image Implementation:
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(' ');
// Check if ALL style classes are present
if (style.classes.filter(value => !classlist.includes(value)).length === 0) {
event.return = true; // Style is active
}
}
}
}
}
});
Logic Breakdown:
- Check Style Element: Only process img styles
- Find typo3image: Iterate selection to find typo3image element
-
Get Classes:
const classAttribute = item.getAttribute('class')
- Read current class attribute from model element
- Returns space-separated string (e.g., "float-left img-responsive")
-
Parse Classes:
const classlist = classAttribute.split(' ')
- Convert string to array:
["float-left", "img-responsive"]
- Convert string to array:
-
Check Match:
style.classes.filter(value => !classlist.includes(value)).length === 0
- Check if ALL style classes are present in element
- Example: Style has
['float-left', 'mr-3']
, check both exist - If any missing, style is NOT active
Example:
// Style definition
{
name: 'Image Left',
element: 'img',
classes: ['float-left', 'mr-3']
}
// Element class attribute
class: 'float-left mr-3 img-responsive'
// Check: Are 'float-left' AND 'mr-3' both present?
['float-left', 'mr-3'].filter(cls =>
!['float-left', 'mr-3', 'img-responsive'].includes(cls)
).length === 0 // true → style is active
Event: getAffectedBlocks
Purpose: Returns which model elements should be affected by style operation
When Fired: User applies/removes a style
Default Behavior: Return block elements from selection
typo3image Implementation:
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]; // Return typo3image element
break;
}
}
}
});
Logic Breakdown:
- Check Style Element: Only process img styles
- Find typo3image: Iterate to find typo3image in selection
-
Return Element:
event.return = [item]
- Return array with single typo3image element
- StyleUtils will apply style changes to this element
- Break Loop: Once found, stop searching
Note
StyleUtils can affect multiple blocks (e.g., multiple paragraphs selected). For images, typically only one image is selected.
GeneralHtmlSupport Integration
What is GeneralHtmlSupport?
Purpose: Manages HTML attributes that aren't core CKEditor features
Capabilities:
- Add/remove classes via style system
- Manage data-* attributes
- Handle custom HTML attributes
- Sync model attributes with view
Decoration Pattern
const ghs = editor.plugins.get('GeneralHtmlSupport');
ghs.decorate('addModelHtmlClass');
ghs.decorate('removeModelHtmlClass');
What decorate()
Does:
- Makes method observable via event system
- Allows plugins to intercept and customize behavior
- Enables event listeners to modify operations
Event: addModelHtmlClass
Purpose: Add CSS class to model element
When Fired: Style system applies a style (adds classes)
typo3image Implementation:
this.listenTo(ghs, 'addModelHtmlClass', (event, [viewElement, className, selectable]) => {
if (selectable && selectable.name === 'typo3image') {
editor.model.change(writer => {
writer.setAttribute('class', className.join(' '), selectable);
});
}
});
Parameters:
viewElement
: View layer element (not used for typo3image)className
: Array of class names to addselectable
: Model element to modify
Logic:
-
Check Element:
if (selectable && selectable.name === 'typo3image')
- Only process typo3image elements
-
Join Classes:
className.join(' ')
- Convert array to space-separated string
- Example:
['float-left', 'mr-3']
→'float-left mr-3'
-
Update Model:
writer.setAttribute('class', ..., selectable)
- Apply classes to model element
- Triggers view update automatically
Example Flow:
User clicks "Image Left" style
↓
StyleUtils determines style applies to typo3image
↓
GeneralHtmlSupport.addModelHtmlClass fired
↓
Event handler: className = ['float-left', 'mr-3']
↓
Model updated: class = 'float-left mr-3'
↓
View automatically updates: <img class="float-left mr-3" ... />
Event: removeModelHtmlClass
Purpose: Remove CSS class from model element
When Fired: Style system removes a style (removes classes)
typo3image Implementation:
this.listenTo(ghs, 'removeModelHtmlClass', (event, [viewElement, className, selectable]) => {
if (selectable && selectable.name === 'typo3image') {
editor.model.change(writer => {
writer.removeAttribute('class', selectable);
});
}
});
Logic:
- Check Element: Only process typo3image
-
Remove Attribute:
writer.removeAttribute('class', selectable)
- Completely removes class attribute
- Note: Doesn't selectively remove classes, removes all
Note
Limitation: Current implementation removes ALL classes when any style is removed. Could be enhanced to only remove specific classes.
Enhancement Pattern:
// Better implementation: remove only specific classes
this.listenTo(ghs, 'removeModelHtmlClass', (event, [viewElement, className, selectable]) => {
if (selectable && selectable.name === 'typo3image') {
editor.model.change(writer => {
const currentClass = selectable.getAttribute('class') || '';
const currentClasses = currentClass.split(' ').filter(Boolean);
const classesToRemove = className;
// Keep classes not being removed
const newClasses = currentClasses.filter(
cls => !classesToRemove.includes(cls)
);
if (newClasses.length > 0) {
writer.setAttribute('class', newClasses.join(' '), selectable);
} else {
writer.removeAttribute('class', selectable);
}
});
}
});
Complete Integration Example
Style Configuration (YAML)
# Configuration/RTE/Default.yaml
editor:
config:
style:
definitions:
- name: 'Image Left'
element: 'img'
classes: ['float-left', 'mr-3']
- name: 'Image Right'
element: 'img'
classes: ['float-right', 'ml-3']
- name: 'Image Center'
element: 'img'
classes: ['d-block', 'mx-auto']
- name: 'Full Width'
element: 'img'
classes: ['w-100']
Plugin Integration (JavaScript)
export default class Typo3Image extends Core.Plugin {
static get requires() {
return ['StyleUtils', 'GeneralHtmlSupport'];
}
init() {
const editor = this.editor;
const styleUtils = editor.plugins.get('StyleUtils');
const ghs = editor.plugins.get('GeneralHtmlSupport');
// Enable img styles for typo3image
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;
}
}
}
});
// Check if style is active
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;
}
}
}
}
}
});
// Return affected elements
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;
}
}
}
});
// Apply classes
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);
});
}
});
// Remove classes
ghs.decorate('removeModelHtmlClass');
this.listenTo(ghs, 'removeModelHtmlClass', (event, [viewElement, className, selectable]) => {
if (selectable && selectable.name === 'typo3image') {
editor.model.change(writer => {
writer.removeAttribute('class', selectable);
});
}
});
}
}
Troubleshooting Style Issues
Issue: Style Drop-down Disabled for Images
Symptoms:
- Select image → style drop-down grayed out
- No styles available when image selected
Causes:
- Missing
GeneralHtmlSupport
dependency - Missing
StyleUtils
dependency - Event listeners not registered
- Style definitions don't target 'img' element
Solutions:
Verify Dependencies
static get requires() {
return ['StyleUtils', 'GeneralHtmlSupport']; // Both required!
}
Verify Style Definitions
style:
definitions:
- name: 'My Style'
element: 'img' # Must be 'img', not 'image'
classes: ['my-class']
Check Event Listeners
// Debug in browser console
const styleUtils = editor.plugins.get('StyleUtils');
console.log(styleUtils.listenerCount('isStyleEnabledForBlock'));
// Should be > 0
Issue: Style Changes Not Applied
Symptoms:
- Style selected from drop-down
- No visual change to image
- Class attribute not updated
Causes:
- GeneralHtmlSupport event listeners not registered
- Model-to-view conversion missing class attribute
- CSS classes not defined in stylesheet
Solutions:
Verify GHS Listeners
const ghs = editor.plugins.get('GeneralHtmlSupport');
console.log(ghs.listenerCount('addModelHtmlClass'));
// Should be > 0
Check Class Attribute Conversion
editor.conversion.for('downcast').attributeToAttribute({
model: { name: 'typo3image', key: 'class' },
view: 'class'
});
Verify CSS Loaded
/* In your stylesheet */
.float-left { float: left; margin-right: 1rem; }
.float-right { float: right; margin-left: 1rem; }
Issue: Styles Not Shown as Active
Symptoms:
- Image has correct classes
- Style not checked/highlighted in drop-down
- Cannot tell which style is applied
Cause: isStyleActiveForBlock
listener not working correctly
Solution:
Debug Class Matching
// In isStyleActiveForBlock listener
console.log('Element classes:', item.getAttribute('class'));
console.log('Style classes:', style.classes);
const classlist = item.getAttribute('class').split(' ');
const missing = style.classes.filter(cls => !classlist.includes(cls));
console.log('Missing classes:', missing);
Advanced Style Patterns
Multiple Class Styles
# Complex styles with multiple classes
style:
definitions:
- name: 'Responsive Image Card'
element: 'img'
classes: ['img-fluid', 'rounded', 'shadow-sm', 'd-block']
Application:
// Results in model:
class: 'img-fluid rounded shadow-sm d-block'
// View output:
<img class="img-fluid rounded shadow-sm d-block" src="..." />
Conditional Style Availability
// Only enable certain styles for specific users
this.listenTo(styleUtils, 'isStyleEnabledForBlock', (event, [style, element]) => {
if (style.element === 'img' && style.name === 'Admin Only Style') {
// Check user permission
if (!userHasAdminPermission()) {
event.return = false; // Disable this style
event.stop(); // Prevent further processing
return;
}
}
// Default behavior for other styles
if (style.element === 'img') {
for (const item of editor.model.document.selection.getFirstRange().getItems()) {
if (item.name === 'typo3image') {
event.return = true;
}
}
}
});
Style Groups
# Organize styles into groups
style:
definitions:
- name: 'Left Align'
element: 'img'
classes: ['float-left']
- name: 'Right Align'
element: 'img'
classes: ['float-right']
- name: 'Center Align'
element: 'img'
classes: ['mx-auto', 'd-block']
groupDefinitions:
- name: 'Image Alignment'
styles: ['Left Align', 'Right Align', 'Center Align']
Performance Considerations
Event Listener Efficiency
// Inefficient: Iterates entire range multiple times
this.listenTo(styleUtils, 'isStyleEnabledForBlock', (event, [style]) => {
if (style.element === 'img') {
for (const item of editor.model.document.selection.getFirstRange().getItems()) {
if (item.name === 'typo3image') {
event.return = true;
}
}
}
});
// Efficient: Cache selection check
const isTypo3ImageSelected = () => {
const selection = editor.model.document.selection;
const element = selection.getSelectedElement();
return element && element.name === 'typo3image';
};
this.listenTo(styleUtils, 'isStyleEnabledForBlock', (event, [style]) => {
if (style.element === 'img' && isTypo3ImageSelected()) {
event.return = true;
}
});