Today I learned: Adding CKEditor5 to Voyager Laravel extension

Today I will show you how to integrate CKEditor5 in a custom Voyager form field. There are 3 basic steps we must follow to even have the new form field appear in the Voyager BREAD type selection.

  1. Create a controller handler which we will save in app\FormFields\CKEditorHandler.php
<?php

namespace App\FormFields;

use TCG\Voyager\FormFields\AbstractHandler;

class CKEditorHandler extends AbstractHandler
{
    protected $codename = "ckeditor";

    public function createContent($row, $dataType, $dataTypeContent, $options)
    {
        return view('formfields.ckeditor_formfield', compact('row', 'dataType', 'dataTypeContent', 'options'));
    }
}

2. Register the controller in app\Providers\AppServicesProvider.php

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use TCG\Voyager\Facades\Voyager;
use App\FormFields\CKEditorHandler;
use App\FormFields\GrapesJsHandler;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        Voyager::addFormField(CKEditorHandler::class);
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

3. Create a form field blade file in resources\views\formfields\ckeditor_formfield.blade

<br>
<input type="button" onclick="window.toggleEditor()" value="Edit">
<textarea style="display: none" class="form-control" name="{{ $row->field }}" id="myckeditor">
    {{ old($row->field, $dataTypeContent->{$row->field} ?? '') }}
</textarea>

@push('javascript')
    <script src="{{asset('assets/js/ckeditor5/build/ckeditor.js')}}"></script>
    <script>
        function hideTranslationButtons() {
            function toggleButton(elem) {
                if (elem.style.display === "none") {
                    elem.style.display = "block";
                } else {
                    elem.style.display = "none";
                }
            }
            let buttons = document.getElementsByName("i18n_selector");
            for (let el of buttons) {
                toggleButton(el.parentElement);
            }
        }

        function getElement() {
            return document.getElementById("myckeditor");
        }

        function startEditor() {
            ClassicEditor
                .create(getElement(), {
                    toolbar: {
                        items: [
                            'heading',
                            '|',
                            'bold',
                            'italic',
                            'link',
                            'bulletedList',
                            'numberedList',
                            '|',
                            'outdent',
                            'indent',
                            '|',
                            'imageUpload',
                            'blockQuote',
                            'insertTable',
                            'undo',
                            'redo'
                        ]
                    },
                    language: 'en',
                    image: {
                        toolbar: [
                            'imageTextAlternative',
                            'imageStyle:inline',
                            'imageStyle:block',
                            'imageStyle:side',
                            'linkImage'
                        ]
                    },
                    table: {
                        contentToolbar: [
                            'tableColumn',
                            'tableRow',
                            'mergeTableCells',
                            'tableCellProperties',
                            'tableProperties'
                        ]
                    },
                    licenseKey: '',


                })
                .then(editor => {
                    window.editor = editor;


                })
                .catch(error => {
                    console.error('Oops, something went wrong!');
                    console.error('Please, report the following error on https://github.com/ckeditor/ckeditor5/issues with the build id and the error stack trace:');
                    console.warn('Build id: xyb2mfluc2ki-unt8fr6ckh47');
                    console.error(error);
                });
        }
        function stopEditor() {
            window.editor.destroy();
            document.getElementById("myckeditor").style.display = "none";
        }
        window.toggleValue = false;
        function toggleEditor() {
            if (window.toggleValue === false) {
                window.toggleValue = true;
                hideTranslationButtons();
                startEditor();
            } else if (window.toggleValue === true) {
                window.toggleValue = false;
                hideTranslationButtons();
                stopEditor();
            }
        }
        window.toggleEditor = toggleEditor;
    </script>
@endpush

Now for a quick explanation of the Blade file. You need a CKEditor JS bundle. You may get it on the official site, ideally you can generate the bundle with whatever plugins you need. Build it here. You can find the CKEditor initialization snippet in the sample html file in the archive you will retrieve. The JS bundle is in the build folder of the same archive.

The form field files are integrated in a Voyager structure that uses the @stack directive, this is where the @push directive injects the scripts. We created a textarea that has the form-control class. This is essential. If you don’t add this class it will not save the data and you will get errors. Next, you must understand how CKEditor and Voyager work.

Let’s assume we enabled Voyager multilanguage support and have chosen English and Dutch. When we are inside a BREAD edit page there are two blue buttons in our Voyager app: English and Dutch. You may find them in the top right corner, you can’t miss them. When you press on a language button it changes the textarea data and stores internally the changes from the previous language. So when you press another language it actually changes the textarea child data.

Now for the CKEditor part. When we instantiate a new editor we inject it into a DOM element. In our case it is #myckeditor id. It creates an editor instance with the data from the textarea. Making language change work is a bit tricky though. When we press a Voyager language change button, the data in the textarea is replaced with another language while the previous language data is stored by Voyager. At the same time CKEditor holds in memory the data it is working with. When you submit the form it will inject whatever value it has back into the textarea. So if you edited Dutch and then switched to English, it will inject the Dutch data into the English section simply because the instance knows only what it holds at the moment. As a result Voyager will work, but whatever you edit will be injected equally in all languages. What we need to do is destroy the editor instance and recreate it when we change the language. When we destroy the editor instance it injects the internal data back into the textarea. Then when we press the Voyager button, it takes whatever we just edited, replaces it with another language and stores it internally until the form is submitted.

Ideally we would attach an event listener to the language change button so the editor will be destroyed and reinitialized. The problem I encountered is that we would have to do some pretty deep magic hacking to find the script responsible for the language change and introduce additional logic. I couldn’t do that as I had trouble finding whatever script is responsible for the language change. Therefore I created an edit button that hides the language change buttons and shows CKEditor. Once you finish entering your text and want to change to another language you would have to press this button so the editor instance is destroyed properly and the data re-injected back in the textarea. At the same time the translation buttons reappear and Voyager is happy to store your data in however many languages you want. Thank you for reading!

Leave a comment