.. _ckeditor-style-integration: ============================= CKEditor Style Integration ============================= Complete guide to integrating the typo3image plugin with CKEditor's style system (StyleUtils and GeneralHtmlSupport). Overview ======== .. versionadded:: 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**: .. code-block:: 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 ========================= Before v13.0.0 (Broken) ----------------------- .. code-block:: 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) --------------------- .. code-block:: javascript // 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**: .. code-block:: 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 ```` 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 .. note:: CKEditor doesn't natively know that ``typo3image`` (model element) corresponds to ```` (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**: .. code-block:: 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 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**: .. code-block:: 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 ------------------------- **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**: .. code-block:: 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 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 .. 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 ------------------ .. code-block:: 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 ------------------------- **Purpose**: Add CSS class to model element **When Fired**: Style system applies a style (adds classes) **typo3image Implementation**: .. code-block:: 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 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: Event: removeModelHtmlClass ---------------------------- **Purpose**: Remove CSS class from model element **When Fired**: Style system removes a style (removes classes) **typo3image Implementation**: .. code-block:: 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 2. **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**: .. code-block:: 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 ============================= Style Configuration (YAML) --------------------------- .. code-block:: 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) -------------------------------- .. code-block:: 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**: 1. Missing ``GeneralHtmlSupport`` dependency 2. Missing ``StyleUtils`` dependency 3. Event listeners not registered 4. Style definitions don't target 'img' element **Solutions**: Verify Dependencies ^^^^^^^^^^^^^^^^^^^ .. code-block:: javascript static get requires() { return ['StyleUtils', 'GeneralHtmlSupport']; // Both required! } Verify Style Definitions ^^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: yaml style: definitions: - name: 'My Style' element: 'img' # Must be 'img', not 'image' classes: ['my-class'] Check Event Listeners ^^^^^^^^^^^^^^^^^^^^^ .. code-block:: javascript // 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**: 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 ^^^^^^^^^^^^^^^^^^^^ .. code-block:: javascript const ghs = editor.plugins.get('GeneralHtmlSupport'); console.log(ghs.listenerCount('addModelHtmlClass')); // Should be > 0 Check Class Attribute Conversion ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: javascript editor.conversion.for('downcast').attributeToAttribute({ model: { name: 'typo3image', key: 'class' }, view: 'class' }); Verify CSS Loaded ^^^^^^^^^^^^^^^^^ .. code-block:: css /* 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 ^^^^^^^^^^^^^^^^^^^^ .. code-block:: 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 ======================== Multiple Class Styles ---------------------- .. code-block:: yaml # Complex styles with multiple classes style: definitions: - name: 'Responsive Image Card' element: 'img' classes: ['img-fluid', 'rounded', 'shadow-sm', 'd-block'] **Application**: .. code-block:: javascript // Results in model: class: 'img-fluid rounded shadow-sm d-block' // View output: Conditional Style Availability ------------------------------- .. code-block:: 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 ------------ .. code-block:: 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 ========================== Event Listener Efficiency -------------------------- .. code-block:: 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 ===================== - :ref:`ckeditor-plugin-development-guide` - :ref:`ckeditor-model-element` - :ref:`ckeditor-conversions` - :ref:`configuration` - :ref:`troubleshooting-index`