Skip to content

Instantly share code, notes, and snippets.

@sebkln
Created January 6, 2024 14:29
Show Gist options
  • Save sebkln/5919ef1fcf2008f3f667b0a04468e7c8 to your computer and use it in GitHub Desktop.
Save sebkln/5919ef1fcf2008f3f667b0a04468e7c8 to your computer and use it in GitHub Desktop.
TYPO3 v12+, CKEditor 5: simple plugin that allows to <div> elements as wrappers around paragraphs. Not yet editor-friendly, though.
# CKEditor 5 configuration in TYPO3 (excerpt)
imports:
- { resource: 'EXT:rte_ckeditor/Configuration/RTE/Processing.yaml' }
- { resource: 'EXT:rte_ckeditor/Configuration/RTE/Editor/Base.yaml' }
- { resource: 'EXT:rte_ckeditor/Configuration/RTE/Editor/Plugins.yaml' }
editor:
config:
importModules:
- '@yourvendor/ckeditor-div'
heading:
options:
- { model: 'paragraph', title: 'Paragraph' }
- { model: 'heading1', view: 'h1', title: 'Heading 1' }
- { model: 'heading2', view: 'h2', title: 'Heading 2' }
- { model: 'heading3', view: 'h3', title: 'Heading 3' }
- { model: 'heading4', view: 'h4', title: 'Heading 4' }
- { model: 'formatted', view: 'pre', title: 'Pre-Formatted Text' }
- { model: 'div', view: 'div', title: 'Div' }
style:
definitions:
# block level styles
- { name: 'Info alert (p)', element: 'p', classes: ['c-alert', 'c-alert--info'] }
- { name: 'Info alert (div)', element: 'div', classes: ['c-alert', 'c-alert--info'] }
- { name: 'Attention alert (p)', element: 'p', classes: ['c-alert', 'c-alert--attention'] }
- { name: 'Attention alert (div)', element: 'div', classes: ['c-alert', 'c-alert--attention'] }
- { name: 'Warning alert (p)', element: 'p', classes: ['c-alert', 'c-alert--warning'] }
- { name: 'Warning alert (div)', element: 'div', classes: ['c-alert', 'c-alert--warning'] }
import {Core, UI} from "@typo3/ckeditor5-bundle.js";
// This plugin allows to use <div> elements as wrappers around paragraphs.
export default class DivElement extends Core.Plugin {
static pluginName = 'DivElement';
init() {
const editor = this.editor;
const model = editor.model;
const view = editor.view;
model.schema.extend('div', {allowIn: '$root'});
model.schema.extend('paragraph', {allowIn: 'div'});
editor.conversion.for('upcast').elementToElement({model: 'div', view: 'div'});
}
}
<?php
return [
'dependencies' => ['backend'],
'tags' => [
'backend.form',
],
'imports' => [
'@yourvendor/ckeditor-div' => 'EXT:your_sitepackage/Resources/Public/JavaScript/CKEditor/div.js',
],
];
@artus70
Copy link

artus70 commented Aug 6, 2024

Thank you.

In the meantime I have found out that in principle it would need the same functionality as the blockQuote button, which wraps the blockquote tag around the selected blocks.

@klodeckl
Copy link

klodeckl commented Nov 6, 2024

@artus70 Did you find a solution? I need the same.

@artus70
Copy link

artus70 commented Nov 6, 2024

@klodeckl Unfortunately not. I just can insert the div tag in source code mode, but don't have an gui element for it.

As mentioned above, the structure of the needed JS code should be almost identical to the blockQuote element for which there exists a gui button. But my programming skills are not sufficient to port that functionality to the div tag by myself.

@klodeckl
Copy link

klodeckl commented Nov 8, 2024

I created a small plugin creating a block widget. The only thing is it creates two divs otherwise the content would not be editable. Feel free to use.

import { Plugin } from '@ckeditor/ckeditor5-core';
import { ButtonView } from '@ckeditor/ckeditor5-ui';
import { Command } from '@ckeditor/ckeditor5-core';
import { toWidget, toWidgetEditable } from '@ckeditor/ckeditor5-widget';
import { Widget } from '@ckeditor/ckeditor5-widget';

class InsertBoxCommand extends Command {
    constructor(editor, className = '') {
        super(editor);
        this.className = className; // Speichert die Klasse
    }

    execute() {
        const model = this.editor.model;

        model.change(writer => {
            // Erstelle das "box" Element
            const boxElement = writer.createElement('box');
            
            // Wenn eine Klasse angegeben wurde, setze sie auf das Element
            if (this.className) {
                writer.setAttribute('class', this.className, boxElement);
            }

            // Erstelle das "boxContent" Element
            const contentElement = writer.createElement('boxContent');

            // Beispielinhalt für das "boxContent" Element
            const paragraph = writer.createElement('paragraph');
            writer.append(writer.createText(''), paragraph);
            writer.append(paragraph, contentElement);

            // Füge contentElement ins boxElement ein
            writer.append(contentElement, boxElement);
            model.insertContent(boxElement);

            // Setze die Auswahl auf contentElement, damit es bearbeitbar ist
            writer.setSelection(contentElement, 'in');
        });
    }

    refresh() {
        const model = this.editor.model;
        const selection = model.document.selection;
        const allowedIn = model.schema.findAllowedParent(selection.getFirstPosition(), 'box');

        this.isEnabled = allowedIn !== null;
    }
}

class BoxWidgetPlugin extends Plugin {
    static get requires() {
        return [Widget]; // Nur das Widget-Plugin wird benötigt
    }

    init() {
        const editor = this.editor;

        // Registriere die Elemente 'box' und 'boxContent' im Schema
        editor.model.schema.register('box', {
            isObject: true,
            allowWhere: '$block',
        });

        editor.model.schema.register('boxContent', {
            allowIn: 'box',
            allowContentOf: '$root', // Erlaube Block-Elemente wie p, ul, ol, etc.
            allowAttributes: ['class', 'id', 'style'], // Erlaube Attribute
        });

        // Downcast-Konvertierung für 'box' (umhüllen in einem div)
        editor.conversion.for('editingDowncast').elementToElement({
            model: 'box',
            view: (modelElement, { writer: viewWriter }) => {
                const div = viewWriter.createContainerElement('div', { class: 'box' });
                return toWidget(div, viewWriter, { label: 'Box widget' });
            }
        });

        // Downcast-Konvertierung für 'boxContent' (editable Bereich)
        editor.conversion.for('editingDowncast').elementToElement({
            model: 'boxContent',
            view: (modelElement, { writer: viewWriter }) => {
                const editable = viewWriter.createEditableElement('div', { class: 'box-content' });
                return toWidgetEditable(editable, viewWriter); // Sicherstellen, dass dieser Bereich bearbeitbar ist
            }
        });

        // Downcast-Konvertierung für die 'box' und 'boxContent' Elemente in Daten (beim Exportieren)
        editor.conversion.for('dataDowncast').elementToElement({
            model: 'box',
            view: {
                name: 'div',
                classes: 'box'
            }
        });

        editor.conversion.for('dataDowncast').elementToElement({
            model: 'boxContent',
            view: {
                name: 'div',
                classes: 'box-content'
            }
        });

        // Upcast-Konvertierung für 'box' und 'boxContent' (beim Importieren)
        editor.conversion.for('upcast').elementToElement({
            view: {
                name: 'div',
                classes: 'box'
            },
            model: 'box'
        });

        editor.conversion.for('upcast').elementToElement({
            view: {
                name: 'div',
                classes: 'box-content'
            },
            model: 'boxContent'
        });

        // Registriere den InsertBox-Befehl
        editor.commands.add('insertBox', new InsertBoxCommand(editor));

        // Füge den Button in die Toolbar ein
        editor.ui.componentFactory.add('insertBox', locale => {
            const button = new ButtonView(locale);

            button.set({
                label: 'Box einfügen',
                tooltip: true,
                withText: false,
                icon: this._getBoxIcon() // Hier fügen wir das SVG-Icon hinzu
            });

            button.bind('isEnabled').to(editor.commands.get('insertBox'));

            this.listenTo(button, 'execute', () => {
                editor.execute('insertBox');
            });

            return button;
        });
    }

    // Methode, um das SVG-Icon für den Button zu erhalten
    _getBoxIcon() {
        return `
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
                <rect x="4" y="4" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none"/>
            </svg>
        `;
    }
}

export default BoxWidgetPlugin;

@artus70
Copy link

artus70 commented Nov 8, 2024

@klodeckl What does "two divs" mean? One div around the selected elements, and the second ...???

@klodeckl
Copy link

klodeckl commented Nov 8, 2024

The output will be:

<div class="box">
<div class="box-content">
Content from RTE here
</div>
</div>

@artus70
Copy link

artus70 commented Nov 8, 2024

Ah, oh, okay - thanks. 😄

@wini2
Copy link

wini2 commented Nov 11, 2024

@klodeckl: Thank you so much for sharing this long awaited code!
Maybe I'm wrong, but is it true, that other classes, defined in my editor.yaml don't appear in the styles-dropdown and it's not possible to add them manually in source-code view?

@klodeckl
Copy link

Where do you want to put additional classes? If in one of the two divs you have to adjust the plugin.

@wini2
Copy link

wini2 commented Nov 11, 2024

Clicking on the edge of the div, it seems that the div is selected, but classes for divs are not present in the style-dropdown. Switching to cource-code-view, manually added classes are removed.

In my editor.yaml I allow div with classes and styles
{ name: 'div', attributes: true, classes: true, styles: { pattern: '/^style.*$/' } },
so I can add the div-tag in source-code-view.
For Editors it would be much easier if they could choose/append a class for the div, added with your button and code (like they can do for other tags like p).

How can I adjust your plugin in order to make this possible?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment