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'];
}
Copied!

The Style System Problem 

Before v13.0.0 (Broken) 

// Missing GeneralHtmlSupport dependency
static get requires() {
    return ['StyleUtils'];  // Incomplete!
}
Copied!

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'];
}
Copied!

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          │
└─────────────────────────────────────────┘
Copied!

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
            }
        }
    }
});
Copied!

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.)
  2. Iterate Selection: for (const item of ...getFirstRange().getItems())

    • Get all items in current selection range
    • Check if any item is a typo3image
  3. Enable Style: event.return = true

    • Tell StyleUtils that img styles ARE applicable to typo3image
    • Without this, style drop-down would be disabled

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
                    }
                }
            }
        }
    }
});
Copied!

Logic Breakdown:

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

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

    • Convert string to array: ["float-left", "img-responsive"]
  5. 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
Copied!

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;
            }
        }
    }
});
Copied!

Logic Breakdown:

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

    • Return array with single typo3image element
    • StyleUtils will apply style changes to this element
  4. Break Loop: Once found, stop searching

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');
Copied!

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);
        });
    }
});
Copied!

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
  2. Join Classes: className.join(' ')

    • Convert array to space-separated string
    • Example: ['float-left', 'mr-3']'float-left mr-3'
  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" ... />
Copied!

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);
        });
    }
});
Copied!

Logic:

  1. Check Element: Only process typo3image
  2. Remove Attribute: writer.removeAttribute('class', selectable)

    • Completely removes class attribute
    • Note: Doesn't selectively remove classes, removes all

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);
            }
        });
    }
});
Copied!

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']
Copied!

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);
                });
            }
        });
    }
}
Copied!

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:

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

Solutions:

Verify Dependencies 

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

Verify Style Definitions 

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

Check Event Listeners 

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

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
  2. Model-to-view conversion missing class attribute
  3. CSS classes not defined in stylesheet

Solutions:

Verify GHS Listeners 

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

Check Class Attribute Conversion 

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

Verify CSS Loaded 

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

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);
Copied!

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']
Copied!

Application:

// Results in model:
class: 'img-fluid rounded shadow-sm d-block'

// View output:
<img class="img-fluid rounded shadow-sm d-block" src="..." />
Copied!

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;
            }
        }
    }
});
Copied!

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']
Copied!

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;
    }
});
Copied!