---
title: "CKEditor Style Integration"
manual: "RTE CKEditor Image"
version: "main"
permalink: "https://docs.typo3.org/permalink/netresearch/rte-ckeditor-image:ckeditor-style-integration@main"
source: "CKEditor/Style-Integration.rst"
rendered: "2026-10-01T01:27:51+00:00"
---

# CKEditor Style Integration {#ckeditor-style-integration}

Complete guide to integrating the typo3image plugin with CKEditor's style system (StyleUtils and GeneralHtmlSupport).

## Overview {#overview}

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

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

```javascript
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 {#the-style-system-problem}

### Before v13.0.0 (Broken) {#before-v13-0-0-broken}

```javascript
// 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) {#after-v13-0-0-fixed}

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

**Result**: Full style system integration working correctly

## Style System Architecture {#style-system-architecture}

### Three-Layer Integration {#three-layer-integration}

```php
┌─────────────────────────────────────────┐
│ 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 {#styleutils-event-system}

### Event: isStyleEnabledForBlock {#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**:

```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;  // Enable img styles for typo3image
            }
        }
    }
});
```

**Logic Breakdown**:

1.  **Check Style Element**: `if (style.element === 'img')`

    -   Only process styles defined for `<img>` elements
    -   Ignore styles for other elements (p, h1, etc.)
1.  **Iterate Selection**: `for (const item of ...getFirstRange().getItems())`

    -   Get all items in current selection range
    -   Check if any item is a typo3image
1.  **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 {#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**:

```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(' ');
                    // 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**:

1.  **Check Style Element**: Only process img styles
1.  **Find typo3image**: Iterate selection to find typo3image element
1.  **Get Classes**: `const classAttribute = item.getAttribute('class')`

    -   Read current class attribute from model element
    -   Returns space-separated string (e.g., "float-left img-responsive")
1.  **Parse Classes**: `const classlist = classAttribute.split(' ')`

    -   Convert string to array: `["float-left", "img-responsive"]`
1.  **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**:

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

```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];  // Return typo3image element
                break;
            }
        }
    }
});
```

**Logic Breakdown**:

1.  **Check Style Element**: Only process img styles
1.  **Find typo3image**: Iterate to find typo3image in selection
1.  **Return Element**: `event.return = [item]`

    -   Return array with single typo3image element
    -   StyleUtils will apply style changes to this element
1.  **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 {#generalhtmlsupport-integration}

### What is GeneralHtmlSupport? {#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 {#decoration-pattern}

```javascript
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 {#event-addmodelhtmlclass}

**Purpose**: Add CSS class to model element

**When Fired**: Style system applies a style (adds classes)

**typo3image Implementation**:

```javascript
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 add
-   `selectable`: Model element to modify

**Logic**:

1.  **Check Element**: `if (selectable && selectable.name === 'typo3image')`

    -   Only process typo3image elements
1.  **Join Classes**: `className.join(' ')`

    -   Convert array to space-separated string
    -   Example: `['float-left', 'mr-3']` → `'float-left mr-3'`
1.  **Update Model**: `writer.setAttribute('class', ..., selectable)`

    -   Apply classes to model element
    -   Triggers view update automatically

**Example Flow**:

```php
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 {#event-removemodelhtmlclass}

**Purpose**: Remove CSS class from model element

**When Fired**: Style system removes a style (removes classes)

**typo3image Implementation**:

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

**Logic**:

1.  **Check Element**: Only process typo3image
1.  **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**:

```javascript
// 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 {#complete-integration-example}

### Style Configuration (YAML) {#style-configuration-yaml}

```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) {#plugin-integration-javascript}

```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 {#troubleshooting-style-issues}

### Issue: Style Drop-down Disabled for Images {#issue-style-drop-down-disabled-for-images}

**Symptoms**:

-   Select image → style drop-down grayed out
-   No styles available when image selected

**Causes**:

1.  Missing `GeneralHtmlSupport` dependency
1.  Missing `StyleUtils` dependency
1.  Event listeners not registered
1.  Style definitions don't target 'img' element

**Solutions**:

#### Verify Dependencies {#verify-dependencies}

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

#### Verify Style Definitions {#verify-style-definitions}

```yaml
style:
  definitions:
    - name: 'My Style'
      element: 'img'  # Must be 'img', not 'image'
      classes: ['my-class']
```

#### Check Event Listeners {#check-event-listeners}

```javascript
// Debug in browser console
const styleUtils = editor.plugins.get('StyleUtils');
console.log(styleUtils.listenerCount('isStyleEnabledForBlock'));
// Should be > 0
```

### Issue: Style Changes Not Applied {#issue-style-changes-not-applied}

**Symptoms**:

-   Style selected from drop-down
-   No visual change to image
-   Class attribute not updated

**Causes**:

1.  GeneralHtmlSupport event listeners not registered
1.  Model-to-view conversion missing class attribute
1.  CSS classes not defined in stylesheet

**Solutions**:

#### Verify GHS Listeners {#verify-ghs-listeners}

```javascript
const ghs = editor.plugins.get('GeneralHtmlSupport');
console.log(ghs.listenerCount('addModelHtmlClass'));
// Should be > 0
```

#### Check Class Attribute Conversion {#check-class-attribute-conversion}

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

#### Verify CSS Loaded {#verify-css-loaded}

```css
/* In your stylesheet */
.float-left { float: left; margin-right: 1rem; }
.float-right { float: right; margin-left: 1rem; }
```

### Issue: Styles Not Shown as Active {#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 {#debug-class-matching}

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

### Multiple Class Styles {#multiple-class-styles}

```yaml
# Complex styles with multiple classes
style:
  definitions:
    - name: 'Responsive Image Card'
      element: 'img'
      classes: ['img-fluid', 'rounded', 'shadow-sm', 'd-block']
```

**Application**:

```javascript
// 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 {#conditional-style-availability}

```javascript
// 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 {#style-groups}

```yaml
# 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 {#performance-considerations}

### Event Listener Efficiency {#event-listener-efficiency}

```javascript
// 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;
    }
});
```

## Related Documentation {#related-documentation}

-   [CKEditor Plugin Development](https://docs.typo3.org/permalink/netresearch/rte-ckeditor-image:ckeditor-plugin-development-guide@main)
-   [CKEditor Model Element Reference](https://docs.typo3.org/permalink/netresearch/rte-ckeditor-image:ckeditor-model-element@main)
-   [CKEditor Conversion System](https://docs.typo3.org/permalink/netresearch/rte-ckeditor-image:ckeditor-conversions@main)
-   [Configuration](https://docs.typo3.org/permalink/netresearch/rte-ckeditor-image:configuration@main)
-   [Troubleshooting & Support](https://docs.typo3.org/permalink/netresearch/rte-ckeditor-image:troubleshooting-index@main)
