WakaCKEditor

WakaCKEditor integrates with WakaPAC as a plugin, bridging CKEditor into the WakaPAC message model. Editor events — content changes, focus, blur, paste — all arrive in msgProc as messages, the same dispatch table as clicks, keypresses, and timers. Two separate plugin files are available: one for CKEditor 4 and one for CKEditor 5. Their public API is identical; the differences are in setup and a small number of behavioural details noted throughout this page.

explanation

Getting Started

Register WakaCKEditor with WakaPAC once, before creating any components. The plugin activates automatically for components whose root element is either a <waka-ckeditor> custom element, or a <textarea> carrying a data-ckeditor attribute.

CKEditor 4

Custom element

Use <waka-ckeditor> as the container element. Add a name attribute to participate in form submission:

<waka-ckeditor id="editor1" name="body"></waka-ckeditor>

<script>
    wakaPAC.use(WakaCKEditor);

    wakaPAC('#editor1', {
        msgProc(event) { }
    });
</script>

Attribute-based (classic)

<script src="https://cdn.jsdelivr.net/gh/quellabs/wakapac@main/wakapac.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/quellabs/wakapac@main/plugins/wakackeditor4.min.js"></script>

<script>
    wakaPAC.use(WakaCKEditor);

    wakaPAC('#editor1', {
        msgProc(event) { }
    });
}
</script>

<textarea id="editor1" data-ckeditor name="body"></textarea>

CKEditor 5

Custom element

Use <waka-ckeditor> as the container element. Add a name attribute to participate in form submission:

<waka-ckeditor data-pac-id="editor1" name="body"></waka-ckeditor>

<script>
    wakaPAC.use(WakaCKEditor);

    wakaPAC('#editor1', {
        msgProc(event) { }
    });
</script>

Attribute-based (classic)

<script src="https://cdn.jsdelivr.net/gh/quellabs/wakapac@main/wakapac.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/quellabs/wakapac@main/plugins/wakackeditor5.min.js"></script>

<script>
    wakaPAC.use(WakaCKEditor);
</script>

<textarea data-pac-id="editor1" data-ckeditor name="body"></textarea>

Loading CKEditor

Each plugin injects the CKEditor script automatically on first use and shares it across all instances. Components created before the script finishes loading are queued and initialized once it is available. You can also load CKEditor yourself before WakaPAC initializes any components — the plugin will detect the existing global and skip injection.

CKEditor 4 builds

Free build (default) — loads CKEditor 4.22.1 from the official CDN. This is the last version available under the open-source license:

wakaPAC.use(WakaCKEditor);

LTS build — loads CKEditor 4.25.1-lts, the commercial Long-Term Support track. Requires an Extended Support Model contract and a valid license key:

wakaPAC.use(WakaCKEditor, {
    license:    'lts',
    licenseKey: 'your-license-key'
});

Self-hosted — point the plugin at your own CKEditor 4 build:

wakaPAC.use(WakaCKEditor, { src: '/assets/ckeditor/ckeditor.js' });

Suppressing the version warning

The free build (4.22.1) logs a browser console warning recommending an upgrade to the LTS version. Pass suppressVersionCheck: true to silence it:

wakaPAC.use(WakaCKEditor, { suppressVersionCheck: true });

CKEditor 5 builds

CDN (default) — loads the official UMD bundle and its companion stylesheet from the CKEditor CDN. A license key is required:

wakaPAC.use(WakaCKEditor, { licenseKey: 'your-license-key' });

Self-hosted — point the plugin at your own UMD bundle. Pass css to load a companion stylesheet, or null if your build already includes styles:

// Self-hosted bundle with explicit CSS path
wakaPAC.use(WakaCKEditor, {
    src:        '/assets/ckeditor5/ckeditor5.umd.js',
    css:        '/assets/ckeditor5/ckeditor5.css',
    licenseKey: 'your-license-key'
});

// Self-hosted bundle whose styles are already on the page
wakaPAC.use(WakaCKEditor, {
    src:        '/assets/ckeditor5/ckeditor5.umd.js',
    css:        null,
    licenseKey: 'your-license-key'
});

The CKEditor 5 plugin expects a UMD bundle that exposes window.CKEDITOR. Module-only or ESM builds are not supported.

The Basics

Once registered, editor events arrive in msgProc. The current HTML content is always available through WakaCKEditor.getValue().

wakaPAC('#editor1', {
    msgProc(event) {
        switch (event.message) {
            case WakaCKEditor.MSG_EDITOR_READY:
                console.log('Editor ready, initial content:', event.detail.value);
                break;

            case wakaPAC.MSG_CHANGE:
                console.log('Content changed:', event.detail.value);
                break;

            case wakaPAC.MSG_INPUT_COMPLETE:
                // Validate or save on blur / after paste
                this.validate(event.detail.value);
                break;

            case wakaPAC.MSG_PASTE:
                // Return false to block the paste
                if (event.detail['text/html'].includes('<script')) {
                    return false;
                }
                break;

            case WakaCKEditor.MSG_EDITOR_ERROR:
                console.error('Editor failed to load:', event.detail.message);
                break;
        }
    }
});

Per-Instance Configuration

CKEditor config can be passed per-component as the third argument to wakaPAC(), under the ckeditor key. Any valid CKEditor config option for the version in use is accepted.

CKEditor 4

wakaPAC('#editor1', { msgProc }, {
    ckeditor: {
        toolbar:  'Basic',
        language: 'nl'
    }
});

CKEditor 5

CKEditor 5 requires an explicit plugins list and a toolbar array. The shorthand toolbar names from CKEditor 4 ('Full', 'Basic') do not exist in CKEditor 5.

wakaPAC('#editor1', { msgProc }, {
    ckeditor: {
        plugins:  [ Bold, Italic, Essentials, Paragraph, Heading ],
        toolbar:  { items: [ 'heading', '|', 'bold', 'italic' ] },
        language: 'nl'
    }
});

Messages

WakaCKEditor dispatches two plugin-specific message types. All other events use standard WakaPAC message constants available on the wakaPAC object. Unless noted, message behaviour is identical across CKEditor 4 and 5.

Editor Ready (MSG_EDITOR_READY)

Fired once when CKEditor has fully initialized and is ready to accept input. Do not call WakaCKEditor.setValue() before this message fires.

ParameterTypeDescription
wParamnumberAlways 0.
lParamnumberAlways 0.
detail.valuestringThe initial HTML content of the editor, sourced from the textarea's value at the time of initialization.

Editor Load Error (MSG_EDITOR_ERROR)

Fired when the CKEditor script fails to load (network error, Content Security Policy block, or a missing window.CKEDITOR global after load), or when ClassicEditor.create() rejects in CKEditor 5. Components waiting for the script are notified and will not initialize.

ParameterTypeDescription
wParamnumberAlways 0.
lParamnumberAlways 0.
detail.messagestringHuman-readable error description.

Content Changed (MSG_CHANGE)

Fired when the editor content changes. Use this as the primary signal that the document has changed.

CKEditor 4: fires for toolbar actions (bold, insert link, etc.) and committed edits, but not on every keystroke. MSG_INPUT precedes it for keystrokes.

CKEditor 5: fires on every model mutation — typing, toolbar actions, and programmatic setValue() calls — because change:data is the only mutation event CKEditor 5 exposes. MSG_INPUT also fires for every mutation, so both messages arrive together on each change.

ParameterTypeDescription
wParamnumberAlways 0.
lParamnumberAlways 0.
detail.valuestringThe current HTML content of the editor.

Keystroke / Mutation (MSG_INPUT)

Fired on per-keystroke or per-mutation updates, depending on the CKEditor version. Use this for live counters or previews that need to stay in sync with every edit.

CKEditor 4: fires on every keystroke after the editable content has been updated, before CKEditor internally commits the change. MSG_CHANGE follows once the edit is committed.

CKEditor 5: fires on every change:data event — meaning every model mutation, including toolbar actions and programmatic changes, not just keystrokes. MSG_CHANGE fires in the same dispatch cycle. If your msgProc handles both messages and you only want to react once per mutation, handle MSG_CHANGE and ignore MSG_INPUT, or vice versa.

case wakaPAC.MSG_INPUT:
    this.charCount = event.detail.value.replace(/<[^>]+>/g, '').length;
    break;
ParameterTypeDescription
wParamnumberAlways 0.
lParamnumberAlways 0.
detail.valuestringThe current HTML content of the editor.

Input Complete (MSG_INPUT_COMPLETE)

Fired when an editing interaction is considered complete — on blur, and immediately after a paste has been processed. Use this to trigger validation or autosave without reacting to every intermediate change.

case wakaPAC.MSG_INPUT_COMPLETE:
    this.save(event.detail.value);
    break;
ParameterTypeDescription
wParamnumberAlways 0.
lParamnumberAlways 0.
detail.valuestringThe current HTML content of the editor.

Paste (MSG_PASTE)

Fired before the pasted content is inserted into the editor, giving msgProc the opportunity to inspect or block it. Return false from msgProc to cancel the paste. If the paste proceeds, MSG_CHANGE and MSG_INPUT_COMPLETE fire afterward as normal.

CKEditor 4: intercepted via CKEditor's paste event, which fires after CKEditor's own sanitization pipeline has processed the content. The full clipboard payload is available in detail, but detail['text/html'] reflects the sanitized HTML rather than the raw clipboard HTML.

CKEditor 5: intercepted via the view document's native paste event at high priority, before CKEditor's paste pipeline runs. The full raw clipboard payload is available in detail.

case wakaPAC.MSG_PASTE:
    // Block pastes containing script tags
    if (event.detail['text/html'].includes('<script')) {
        return false;
    }
    break;
ParameterTypeDescription
wParamnumberModifier key bitmask: MK_CONTROL, MK_SHIFT, MK_ALT.
lParamnumberLength of the plain-text clipboard content in characters.
detail['text/plain']stringPlain-text clipboard content.
detail['text/html']stringHTML clipboard content. In CKEditor 4 this reflects the content after CKEditor's sanitization pipeline; in CKEditor 5 it is the raw clipboard HTML.
detail['text/rtf']stringRTF clipboard content, if available.
detail['text/uri-list']stringRaw URI-list clipboard content, if available.
detail.urisstring[]Parsed URIs from text/uri-list, comments stripped.
detail.filesobject[]File metadata objects { name, size, type } for any files on the clipboard.
detail.typesstring[]All MIME types present on the clipboard.

Focus Gained (MSG_SETFOCUS)

Fired when the editor gains focus.

ParameterTypeDescription
wParamnumberAlways 0.
lParamnumberAlways 0.

Focus Lost (MSG_KILLFOCUS)

Fired when the editor loses focus. Always fires after MSG_INPUT_COMPLETE on blur.

ParameterTypeDescription
wParamnumberAlways 0.
lParamnumberAlways 0.

API

All API methods take pacId as their first argument and are identical across CKEditor 4 and 5, except where noted. Methods targeting a pacId not registered as a CKEditor component are silently ignored.

WakaCKEditor.getValue(pacId)

Returns the current HTML content of the editor.

ParameterTypeDescription
pacIdstringThe data-pac-id of the target component.
Returns string | undefinedCurrent HTML content, or undefined if the component is not registered.

WakaCKEditor.setValue(pacId, html)

Sets the editor content programmatically. Triggers a change event internally, which dispatches MSG_CHANGE as normal. In CKEditor 5, MSG_INPUT also fires alongside MSG_CHANGE.

ParameterTypeDescription
pacIdstringThe data-pac-id of the target component.
htmlstringHTML string to set as the editor content.
Returns void

WakaCKEditor.focus(pacId)

Moves focus to the editor.

ParameterTypeDescription
pacIdstringThe data-pac-id of the target component.
Returns void

WakaCKEditor.setReadOnly(pacId, readOnly)

Toggles read-only mode. In read-only mode the editor content is visible but cannot be edited.

CKEditor 4: delegates to editor.setReadOnly(). The toolbar is hidden automatically by CKEditor.

CKEditor 5: delegates to editor.enableReadOnlyMode(lockId) / editor.disableReadOnlyMode(lockId) using a stable internal lock ID. If your application also manages read-only state through other means, keep in mind that CKEditor 5's lock-based API requires all locks to be released before editing is re-enabled — calling setReadOnly(pacId, false) only releases the lock held by this plugin.

ParameterTypeDescription
pacIdstringThe data-pac-id of the target component.
readOnlybooleantrue to enable read-only mode, false to restore editing.
Returns void

Form Submission

Both plugins ensure the textarea value is current when a form is submitted, but they achieve this differently.

CKEditor 4 hooks into the surrounding <form>'s submit event automatically when initialized via CKEDITOR.replace(). The original textarea retains its name attribute and receives the editor's HTML content before the form is posted. No extra handling is needed.

CKEditor 5 removes the original textarea from the DOM entirely and does not hook into the form submit event. The plugin inserts a hidden proxy textarea (same name) in its place and keeps it current on every change:data event. Native form posts work without any extra handling, but the textarea the form sees is the proxy, not the original element.

When using the <waka-ckeditor> custom element, both versions create a hidden <textarea> inside the custom element and initialize CKEditor on it. The proxy textarea inherits the name attribute from the custom element. CKEditor 5's additional submission proxy is then inserted relative to that inner textarea, exactly as it would be for a plain <textarea data-ckeditor>.

Best Practices

  • Register once: Call wakaPAC.use(WakaCKEditor) once before creating any components. The CKEditor script is shared across all instances on the page.
  • Choose your version carefully: CKEditor 4's free build (4.22.1) requires no license but will not receive further security updates. The LTS build (4.25.1-lts and beyond) is commercially licensed — using it without a valid licenseKey will display a warning in the browser console and editor UI. CKEditor 5 requires a license key for all builds, including the free tier.
  • Wait for MSG_EDITOR_READY: Do not call WakaCKEditor.setValue() programmatically before this message fires. This is especially important for CKEditor 5, where ClassicEditor.create() is asynchronous.
  • Use MSG_INPUT_COMPLETE for validation and autosave: Prefer MSG_INPUT_COMPLETE over MSG_CHANGE for expensive operations. It fires on blur and after paste — not on every mutation.
  • Handling MSG_INPUT and MSG_CHANGE in CKEditor 5: Both messages fire together on every mutation, including toolbar actions and setValue() calls. If your msgProc handles both, make sure the handlers are idempotent or pick one message to act on and ignore the other.
  • Use MSG_CHANGE for reactive UI: Use MSG_CHANGE to update character counters, word counts, or preview panels that should stay in sync with the document.
  • Blocking paste: Return false from msgProc when handling MSG_PASTE to cancel the paste. Use detail['text/html'] to inspect the HTML content and detail['text/plain'] for plain text. In CKEditor 4, detail['text/html'] reflects the content after CKEditor's sanitization pipeline; in CKEditor 5 it is the raw clipboard HTML intercepted before the paste pipeline runs.
  • No manual cleanup needed: The plugin destroys the CKEditor instance automatically when the component is destroyed. For CKEditor 5, the proxy textarea is also removed at this point.
  • File protocol limitation: CKEditor 5's CDN will not load when the page is opened directly from the filesystem (file:///) due to browser MIME-type enforcement. Serve your page over HTTP even during local development — for example with php -S localhost:8080 or npx serve ..