1

TL;DR

What is the correct/recommended way to implement a Redactor plugin within a Craft plugin?

My intention

I'd like to implement a Craft CMS 3 plugin that adds a new Reactor plugin.

Normally (when not implementing Craft plugins) I'd just add my Redactor plugin file, let's say redactorplugin.js to ./config/redactor/plugins/redactorplugin.js (while ./ is the project's root directory).

But for the sake of reusability I'd like to build a Craft plugin instead of manually putting the redactorplugin.js file to the Redactor plugin directory of each project.

My problem

I get an error because my Redactor plugin doesn't seem to be loaded correctly or maybe it's loaded in a wrong order (like too late for Redactor or something) or it's just not properly initialized.

Long story short, I get this error message:
Redactor plugin not found: <handle>

My current attempt

My Craft plugin which I am building has the following file structure (to be honest, I spared a few files, but they aren't important for now):

myplugin/
├── README.md
├── composer.json
└── src
    ├── MyPlugin.php
    ├── assetbundles
    │   └── redactor
    │       ├── RedactorBundle.php
    │       └── dist
    │           └── redactorplugin.js
    └── icon.svg

So, basically I have the Redactor plugin file redactorplugin.js and an assetbundle implementation which is the PHP class RedactorBundle.

The latter one looks as follows:

<?php

namespace myplugin\assetbundles\redactor;

use craft\redactor\assets\field\FieldAsset;
use craft\web\AssetBundle;

class RedactorBundle extends AssetBundle
{

    public function init()
    {
        $this->sourcePath = "@myplugin/assetbundles/redactor/dist";

        $this->depends = [
            FieldAsset::class,
        ];

        $this->js = [
            'redactorplugin.js',
        ];

        parent::init();
    }
}

What happens here so far, is that my assetbundle RedactorBundle is being loaded after loading the craft\redactor\assets\field\FieldAsset assetbundle, which happens for example when I head to an entry's edit page in the Control Panel of Craft.

While observing the network traffic in my browser dev-tools I can then see that redactorplugin.js is loaded right after some Redactor related JS files (after RedactorInput.min.js and RedactorOverrides.min.js, but before EntryTypeSwitcher.min.js if that even matters).

By the way, this is the redactorplugin.js file's content:

(function ($R) {
    $R.add('plugin', 'redactorplugin', {
        init: function (app) {
            console.log('init');
        },
        start: function () {
            console.log('start');
        },
    });
})(Redactor);
console.log(Redactor); //  will be logged as $R

In the Console tab of my browser's dev-tools I can see that $R is being logged (init and start aren't logged though, because I still need to add the plugin to my Redactor config). When expanding $R I can see a lot of stuff including the list of Redactor plugins where my redactorplugin is also listed.

So, apparently this already works.

But …

After adding redactorplugin to my Redactor config (e.g. in ./config/redactor/Simple.json) I get the above mentioned error.

Invalid Configuration – yii\base\InvalidConfigException
with the message:
Redactor plugin not found: redactorplugin

There seems to be going on some naming convention stuff. Craft/Redactor tries to load the redactorplugin.js on its own although it has already been loaded using the assetbundle.

Solution? – Nah…

When I just create an empty file called redactorplugin.js (literally empty, no single line of code) and put it in the ./config/redactor/plugins/ directory then everything works fine! Woohoo!
(Which is because two files of name redactorplugin.js will be loaded, the empty dummy file and the one inserted by the assetbundle.)

So, I would conclude that a solution (or at least a workaround) could be to create such a dummy file on installation of my Craft plugin.

Like:

namespace myplugin\assetbundles\redactor;

use craft\base\Plugin;

class MyPlugin extends Plugin
{
    // … init() and other methods here …

    protected function afterInstall()
    {
        //  TODO: create dummy file for Redactor plugin
    }

    protected function beforeUninstall(): bool
    {
        //  TODO: remove dummy file from ./config/redactor/plugins/

        return true;
    }
}

Unfortunately, this seems to be clumsy to me. First of all, creating dummy files to make something work feels really strange. Also, creating those files will result in "unknown files" in Git. This whole thing doesn't feel right.

Is using assetbundles the correct way at all?

Is there any other way?

What is the actually recommended way to achieve my goal? (Which is adding Redactor plugins from within a Craft plugin.)

1

I found the solution on my own. It's actually as simple as writing three lines of code.

First of all I

  1. removed the assetbundle class (RedactorBundle)
  2. moved my redactorplugin.js to another directory (to myplugin/src/resources)
    …which is not really necessary, but since I don't need the assetbundle any longer I wanted to flatten the directory structure

Then I just added this hook to my plugin class' init() method.


//  register new plugin source for Redactor using one of Redactor's events
Event::on(Field::class, Field::EVENT_REGISTER_PLUGIN_PATHS, function (RegisterPluginPathsEvent $event) {
    $event->paths[] = Craft::getAlias('@myplugin/resources/');
});

And that's it. Why didn't I came up to this a bit earlier…? However, this is the way to go.

In the end I only have these files left:

myplugin/
├── README.md
├── composer.json
└── src
    ├── MyPlugin.php
    ├── resources
    │   └── redactorplugin.js
    └── icon.svg

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.