Problem/Motivation

Currently, Drupal\ckeditor\Plugin\Editor\CKEditor::getJSSettings() uses a += operator to combine the configuration arrays provided by plugins. In the event there are identical keys, the first instance is honored, not the last. Drupal's Internal plugin, which loads first, sets some settings by default:

  /**
   * {@inheritdoc}
   */
  public function getConfig(Editor $editor) {
    // Reasonable defaults that provide expected basic behavior.
    $config = [
      // Don't load CKEditor's config.js file.
      'customConfig' => '',
      'pasteFromWordPromptCleanup' => TRUE,
      'resize_dir' => 'vertical',
      'justifyClasses' => ['text-align-left', 'text-align-center', 'text-align-right', 'text-align-justify'],
      'entities' => FALSE,
      'disableNativeSpellChecker' => FALSE,
    ];

    ...

    return $config;
  }

This means that another plugin cannot override these settings without implementing hook_editor_js_settings_alter().

This is being filed as a bug, because usinghook_editor_js_settings_alter() is a workaround. Per Drupal\ckeditor\Plugin\CKEditorPlugin\Internal::getConfig():

// Reasonable defaults that provide expected basic behavior.

The language shows a clear intent to provide defaults, not to hard-code these values.

Proposed resolution

Use array_merge() to combine the arrays instead.

Remaining tasks

Submit patch.

User interface changes

None.

API changes

None.

Data model changes

None.

Release notes snippet

In versions of Drupal prior to 8.8.0, it was not possible to override CKEditor default configuration provided by the Internal plugin. In the event multiple plugins provided configuration with the same key, the first instance was honored. Beginning in Drupal 8.8.0, the last instance is now honored.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Chris Burge created an issue. See original summary.

Chris Burge’s picture

Chris Burge’s picture

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Chris Burge’s picture

Tests still pass on 8.9. Leaving status at Needs Review.

TwoD’s picture

I do not actually agree with this approach for two reasons.

1, I'm not sure we can handle merging of settings correctly as there may be a need for for deep merges for complex cases. Unless only a single plugin wants to completely override an editor setting there's no generally "correct" way of merging them. Plugins usually only include settings which they have defined themselves.

2, The CKEditor custom config module could instead of [or in addition to] implementing its functionality as a CKEditorPlugin plugin, it could implement hook_editor_js_settings_alter() and modify any part of the settings tree.
It's not adding any buttons or actual CKEditor plugin as such so it seems to only use the plugin to inject itself into the settings form, which could also be accomplished with a form alteration (yes, storing the settings would be more complicated, so perhaps keep the plugin for that reason).

Chris Burge’s picture

I'm not sure we can handle merging of settings correctly as there may be a need for for deep merges for complex cases.

This issue does not seek to merge settings.

The patch simply allows for settings for pasteFromWordPromptCleanup, justifyClasses, etc. to be modified like all other settings. Core provides certain "defaults" that cannot be modified. This patch corrects that.

Unless only a single plugin wants to completely override an editor setting there's no generally "correct" way of merging them.

Agreed; however, that's not relevant to this issue. The patch simply allows pasteFromWordPromptCleanup, justifyClasses, etc. to be modified like all other settings. A module may desire to override settings or to modify/merge settings. That makes no difference here, however.

Plugins usually only include settings which they have defined themselves.

Usually, but not necessarily. By design, CKEditor config modifies all settings. It's a utility module that allows privileged users to manage CKEditor settings via a UI.

...[CKEditor config] could implement hook_editor_js_settings_alter() and modify any part of the settings tree.

Modifying granular parts of the settings tree is out of scope for CKEditor config. See #3062284-12: Move CKEditor Custom Config to a JSON approach...: "The second use case is merging configuration objects/arrays. That's outside the scope of this module as that is best accomplished programmatically (because we don't know how to resolve merge conflicts - it may vary from usage to usage)."

In conclusion, Merging settings is out of scope and is not suggested by this issue. Modifying any part of the settings tree is out of scope and is not suggested by this issue.

The clear intent from the existing code in core's CKEditor module is that the "default" settings are provided are defaults, not absolutes. The scope and intent of this issue to allow them to be modified like all other settings. That's it.

TwoD’s picture

This issue does not seek to merge settings.

The patch does open up for that possibility and potential side-effects array_merge() would introduce.
The description for ::getConfig() states

Returns the additions to CKEDITOR.config for a specific CKEditor instance.

Replacing an existing setting is an override, not an addition. So the + operator works as intended, allowing you to make additions to the default settings, allowing the to be modified/alter at a later stage (as in the alter hook).

The patch simply allows for settings for pasteFromWordPromptCleanup, justifyClasses, etc. to be modified like all other settings. Core provides certain "defaults" that cannot be modified. This patch corrects that.

The method is not intended to modify existing config, only extend it. The core defaults can be modified in hook_editor_js_settings_alter().

Usually, but not necessarily. By design, CKEditor config modifies all settings. It's a utility module that allows privileged users to manage CKEditor settings via a UI.

I meant native CKEditor plugins here, not Drupal modules. Drupal modules wrapping CKEditor plugins should likewise not touch settings for other plugins than those it implements, that can lead to all kinds of confusion. The CKEditor custom config module does not wrap any CKEditor plugin, and would be more suited to implement hook_editor_js_settings_alter() to add or override any editor/plugin with its custom config.

Modifying granular parts of the settings tree is out of scope for CKEditor config

Yeah, I get that, but I'm also trying to tell you that implementing Drupal\ckeditor\CKEditorPluginInterface is not the expected way to modify or override any setting at all, hook_editor_js_settings_alter() is that way.

In conclusion, Merging settings is out of scope and is not suggested by this issue. Modifying any part of the settings tree is out of scope and is not suggested by this issue.

Good, agreed. Then let's not use array_merge() on the return value of ::getConfig() and save us from a lot of future headaches. ;)

The clear intent from the existing code in core's CKEditor module is that the "default" settings are provided are defaults, not absolutes. The scope and intent of this issue to allow them to be modified like all other settings. That's it.

Is hook_editor_js_settings_alter() insufficient for this purpose?

Chris Burge’s picture

Issue summary: View changes

I've dug through the issue queue and Git history.

In the case of CKEditor config, I agree that implementing settings overrides with hook_editor_js_settings_alter() is preferable to implementing them with ::getConfig(). Primarily, we get settings after all other plugins have added their settings. The current method predates my co-maintainership with the CKEditor config module. Accordingly, I've opened an issue: #3090970: Implement settings with hook_editor_js_settings_alter()

That said, these changes to the CKEditor config module are tangential to this issue.

The central issue remains - CKEditor [Drupal] plugins are unable to set certain settings without implementing hook_editor_js_settings_alter().

Example: A developer writes a CKEditor Justify module that allows site builders to set the classes added to justified elements, which requires the justifyClasses setting. The correct method would be to set it with ::getConfig() in the plugin; however, because 1) the internal plugin always loads first and 2) += is used instead of array_merge() - this is impossible because the first instance of a key is used instead of the last.

As such, I'm leaving this issue open.

TwoD’s picture

The central issue remains - CKEditor [Drupal] plugins are unable to set certain settings without implementing hook_editor_js_settings_alter().

That is by design. Plugins provide defaults, alter hooks do alterations as needed.

Example: A developer writes a CKEditor Justify module that allows site builders to set the classes added to justified elements, which requires the justifyClasses setting.

The intended use case is; unless you are actually presenting a completely separate native CKEditor plugin to the Drupal CKEditor module, you should not implement Drupal\ckeditor\CKEditorPluginInterface or use the @CKEditorPlugin() annotation. CKEditor plugins are not meant to mess with each other's settings in this way, and if they need to do so, they can use the CKEditor JS API directly. I don't see that module using a CKEditor plugin and thus should not need a Drupal plugin. (As mentioned earlier, I do agree it's a bit easier to present the settings form that way vs a separate form alter hook, just don't return anything from ::getConfig().)

The correct method would be to set it with ::getConfig() in the plugin; however, because 1) the internal plugin always loads first and 2) += is used instead of array_merge() - this is impossible because the first instance of a key is used instead of the last.

1) When CKEditor is configured in standalone mode its plugins provide default settings which you can override for your use case when passing the settings object to a new instance. In Drupal the CKEditorPlugin should present overrides of those settings which makes it work better with Drupal in the general case. The Internal plugin does this for the plugins bundles with the CKEditor build used by core, including the justify plugin. Plugins are not supposed to be able to override each other's settings this way, doing so could cause conflicts you can not solve without also implementing a later alter hook and overriding conflicting values anyway.

2) Then you are free to implement hook_editor_js_settings_alter() as the primary method of overriding any of the defaults - perhaps using a helper module like CKEditor custom config or in a custom per-site module. Here you can do whatever you want with justifyClasses or any other setting as very few modules should need to use this hook (at least in conflicting ways).

Chris Burge’s picture

Most of #6 through #10 is off-topic (although, thank you for your suggestion to improve the architecture of the CKEditor config module). This issue is about the order in which CKEditor Drupal plugin settings are honored.

> unless you are actually presenting a completely separate native CKEditor plugin to the Drupal CKEditor module, you should not implement Drupal\ckeditor\CKEditorPluginInterface or use the @CKEditorPlugin() annotation.

Feel free to open a new issue to address this assertion. I can't find anything (documentation, code comments, comments in issues, Git commit messages) supporting this assertion.

Returning to the scope of this issue:

I have seen no evidence, either here or elsewhere, that the default settings provided by the Internal plugin should be within the absolute domain of the Internal plugin within Drupal's plugin architecture. Again, this issue is about the order in which CKEditor Drupal plugin settings are honored. Feel free to open new issues (and link back) for issues tangential to this.

TwoD’s picture

What I've said so far is very relevant because the CKEditor custom config module is trying to coerce an API into doing something it's not meant to do while there is already a more suited separate API. I'm trying to explain to you why the plugin API is (mostly) the wrong choice for CKEditor custom config module and that the alter hook would perfectly fit what it's trying to accomplish, without any Core changes being necessary.

This issue is about the order in which CKEditor Drupal plugin settings are honored.

Invocation order is not honored because it should be irrelevant. What is returned from ::getConfig() should be unique to a single plugin because we have no way of correctly resolving any potentially conflicting defaults. If each plugin sticks to only setting defaults for itself, the outcome is deterministic, reliable and a source of any values can be easily traced.

I've already quoted part of the method description from the interface method you are implementing; Here it is again with the relevant part highlighted:

* Returns the additions to CKEDITOR.config for a specific CKEditor instance.

The description of the interface Drupal\ckeditor\CKEditorPluginInterface itself also makes a point about it being centered around CKEditor plugins:

/**
* Defines an interface for (loading of) CKEditor plugins.
*

The CKEditor custom config module does not use a plugin, which are isolated pieces of functionality unless they have explicit dependencies on some other plugins. Even in that case they avoid changing defaults as those are normally defined inside the plugins itself. In the case of Drupal plugins wrapping CKEditor plugins, we provide a convenience method for making common tweaks to those internal defaults to better work with Drupal instead of having to always implement the alter hook. The method can do that precisely because the alter hook always has the last say in what gets passed on to the editor.

The CKEditor custom config module implements a plugin wrapper for a plugin that does not exist just to be able to override the default settings for other plugins, something which there is already a specific and supported hook for, which runs after all plugins have been processed.

Opening up for merging of default settings would lead to plugins stepping on each others toes because the order is by design irrelevant. CKEditor plugins try hard to avoid naming their settings the same because having to settings with the same name is not supported, and I don't see how it can be for all the cases that would come up.

What the CKEditor custom config module currently does is the equivalent of hacking the plugin files to modify default settings, instead of overriding them just before instantiating the editor instance, via the alter hook.

Yes, the internal plugin runs first in Drupal, but that's just an implementation detail and not something which should be relied on to change it's default settings at that point in the code flow. The defaults are defaults, not something that needs to change at that point in the config setup.

Chris Burge’s picture

Continued comments regarding CKEditor config are irrelevant and out of scope. Please move this discussion elsewhere.

TwoD’s picture

The CKEditor Custom config module is relevant because it is the only case I know of that has tried to use the Editor plugin API (or the equivalent preceeding Wysiwyg module API) in the way the posted patch would allow.

If this module can demonstrate a use case where this change is required (vs implementing hook_editor_js_settings_alter() it would mean we have found something that was not considered during the design phase.

If so, I would like to bring this to the attention of the other module co-maintainers so we can all have a deeper discussion about the implications and consequences - such as how to handle multiple modules wanting to change the defaults of multiple other modules in a deterministic and explainable way.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

quietone’s picture

Project: Drupal core » CKEditor 4 - WYSIWYG HTML editor
Version: 9.5.x-dev » 1.0.x-dev
Component: ckeditor.module » Code

CKEditor has been removed from core, CKEditor 4 is removed from Drupal Core in 10.0.0

apaderno’s picture