In Your Plugin


Using the editor in your own plugin #

The editor isn’t tied to the field type. Install Wahlberg as a dependency and you can put it on any textarea in the control panel.

From a template. This wraps it in Craft’s own field chrome, so label, instructions, errors and the required marker all behave as they would for any other field:

Twig
{% import 'wahlberg/editor' as wahlberg %}

{{ wahlberg.field({
    label: 'Release Notes'|t('my-plugin'),
    instructions: 'Markdown, please.'|t('my-plugin'),
    name: 'notes',
    value: settings.notes,
    errors: settings.getErrors('notes'),
}) }}

wahlberg.input({ ... }) gives you the bare editor without the field chrome, and Editor::inputHtml([ ... ]) is the same thing from PHP:

PHP
use bensomething\wahlberg\Editor;

echo Editor::inputHtml([
    'name' => 'notes',
    'value' => $model->notes,
]);

Options: name, value, id, toolbar, floating, buttons, preview, highlight, stats, flavour, fontSize, minRows, maxRows, placeholder, charLimit, byteLimit, refTags, assetSources, assetCriteria, snippets, and inputAttributes (merged onto the <textarea>). Anything else in the config is passed through to Craft’s field macro.

snippets takes handles from config/wahlberg.php, or * for all of them. Pass a map of handle => {label, body} instead and the editor uses those directly, for a plugin shipping snippets of its own rather than borrowing the installation’s.

The ones you leave out fall back to the same defaults a Markdown field starts with: 14px text, a 2-row minimum, no maximum, and the same toolbar. An editor rendered from another plugin matches one in a field layout without having to be configured to.

buttons takes the command names Editor::commands() lists, in any order. The toolbar keeps its own, and drops a group nothing was picked from rather than leaving its divider hanging:

Twig
{{ wahlberg.field({
    label: 'Notes'|t('my-plugin'),
    name: 'notes',
    value: settings.notes,
    buttons: ['bold', 'italic', 'link'],
}) }}

charLimit and byteLimit only draw the counter, and only when stats is on. Enforcing them is the field type’s job, so validate the value yourself out here.

Turn highlight off and you get a plain textarea with the same chrome, sizing and toolbar included, but no Markdown colouring:

Twig
{{ wahlberg.field({
    label: 'Notes'|t('my-plugin'),
    name: 'notes',
    value: settings.notes,
    highlight: false,
    preview: false,
    toolbar: false,
}) }}

The Preview tab works without a field behind it, parsing with whatever flavour you pass and always purifying, since there are no field settings to consult. There’s no Preserve Line Breaks option out here: flavour takes a parser flavour directly, so pass gfm to turn line breaks off and gfm-comment (the default) to keep them.

For somewhere a value is shown rather than edited, Editor::staticHtml([ ... ]) renders the Markdown as it was written on the same surface, in the same type, with no textarea and nothing to run:

PHP
echo Editor::staticHtml([
    'value' => $model->notes,
]);

Options: value, fontSize and lineLength.

Reach for it anywhere the JavaScript won’t be there. Disabling the editor’s inputs isn’t enough on its own, because the textarea paints its own text transparent for the highlighted layer to show through, and that layer is filled by the JS. This is what a Markdown field renders in a revision, and in any other read-only form Craft builds.

What’s public API here is the four entry points and their options. The markup they generate, the CSS class names, and the data attributes the JS binds to are all internal and will change without a major version, so render through these rather than hand-rolling the HTML.

Hooking the Preview tab #

PreviewController::EVENT_MODIFY_PREVIEW hands you the preview’s HTML after parsing, reference tags and purification, so a listener can add markup the purifier would otherwise strip. Inline SVG is the case it exists for. ReferenceTags::outsideCode() is there if you want to leave tokens inside code fences as the author typed them.

Two things to be deliberate about, both because it runs after the sanitiser: what you add has to be safe on its own account, and the preview is now a step ahead of entry.body.html unless you ship a filter that puts it back on the template side. ModifyPreviewEvent has a worked example.