Verified Commit 41ab83b4 authored by Dave Long's avatar Dave Long
Browse files

task: #3507570 Allow recipe to add multiple buttons to CKEditor toolbar

By: mpotter
By: phenaproxima
By: thejimbirch
By: nicxvan
By: svendecabooter
By: robloach
By: dmundra
(cherry picked from commit c1ccef96)
parent ea8be0f9
Loading
Loading
Loading
Loading
Loading
+14 −0
Changes for core/core.api.php: 14 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -2453,6 +2453,20 @@ function hook_config_schema_info_alter(&$definitions) {
  $definitions['date_format']['form_element_class'] = '\Drupal\config_translation\FormElement\DateFormat';
}

/**
 * Alter config action plugin definitions.
 *
 * @param array[] $definitions
 *   The array of config action plugin definitions, keyed by plugin ID.
 *
 * @see \Drupal\Core\Config\Action\ConfigActionManager
 * @see \Drupal\Core\Config\Action\Attribute\ConfigAction
 */
function hook_config_action_alter(array &$definitions): void {
  // Create an alias for an existing config action.
  $definitions['editor:addItemsToToolbar'] = $definitions['editor:addItemToToolbar'];
}

/**
 * Alter validation constraint plugin definitions.
 *
+11 −0
Changes for core/modules/ckeditor5/src/Hook/Ckeditor5Hooks.php: 11 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\editor\EditorInterface;

@@ -381,6 +382,16 @@ public function configSchemaInfoAlter(&$definitions): void {
    $definitions['ckeditor5_valid_pair__format_and_editor']['mapping']['image_upload'] = $definitions['editor.editor.*']['mapping']['image_upload'];
  }

  /**
   * Implements hook_config_action_alter().
   */
  #[Hook('config_action_alter')]
  public function configActionAlter(array &$definitions): void {
    // Create a pluralized alias for the `addItemToToolbar` action.
    $definitions['editor:addItemsToToolbar'] = $definitions['editor:addItemToToolbar'];
    $definitions['editor:addItemsToToolbar']['admin_label'] = new TranslatableMarkup('Add multiple items to a CKEditor 5 toolbar');
  }

  /**
   * Implements hook_field_widget_single_element_form_alter().
   */
+47 −13
Changes for core/modules/ckeditor5/src/Plugin/ConfigAction/AddItemToToolbar.php: 47 added lines, 13 removed lines.
Original line number Diff line number Diff line
@@ -46,6 +46,18 @@ public static function create(ContainerInterface $container, array $configuratio
   * {@inheritdoc}
   */
  public function apply(string $configName, mixed $value): void {
    // Normalize $value, which could be one of three things:
    // - A string (add one item to the toolbar, no additional options)
    // - An associative array (add one item to the toolbar, with options)
    // - An indexed array (add multiple items to the toolbar, each of which
    //   could be one of the previous two forms)
    if (is_string($value) || (is_array($value) && !array_is_list($value))) {
      $value = [$value];
    }
    if (!is_array($value) || $value === []) {
      throw new ConfigActionException(sprintf('The %s config action requires one or more toolbar items.', $this->pluginId));
    }

    $editor = $this->configManager->loadConfigEntityByName($configName);
    assert($editor instanceof EditorInterface);

@@ -53,13 +65,37 @@ public function apply(string $configName, mixed $value): void {
      throw new ConfigActionException(sprintf('The %s config action only works with editors that use CKEditor 5.', $this->pluginId));
    }

    $changed = FALSE;
    foreach ($value as $item) {
      if (!is_string($item) && !is_array($item)) {
        throw new ConfigActionException(sprintf('The %s config action requires each toolbar item to be a string or an array of options.', $this->pluginId));
      }
      $changed = $this->applySingle($editor, $item) || $changed;
    }
    if ($changed) {
      $editor->save();
    }
  }

  /**
   * Adds an item to the toolbar.
   *
   * @param \Drupal\editor\EditorInterface $editor
   *   The editor to which the item should be added.
   * @param string|array $value
   *   The name of the item, or an array of options for the item.
   *
   * @return bool
   *   TRUE if the editor settings were changed, FALSE otherwise.
   */
  private function applySingle(EditorInterface $editor, string|array $value): bool {
    if (is_string($value)) {
      $value = ['item_name' => $value];
    }
    assert(is_array($value));

    $item_name = $value['item_name'];
    assert(is_string($item_name));
    $item_name = $value['item_name'] ?? NULL;
    if (!is_string($item_name)) {
      throw new ConfigActionException(sprintf("The %s config action requires each toolbar item to define an 'item_name' string.", $this->pluginId));
    }

    $replace = $value['replace'] ?? FALSE;
    assert(is_bool($replace));
@@ -74,7 +110,7 @@ public function apply(string $configName, mixed $value): void {
    // If the item is already in the toolbar and we're not allowing duplicate
    // items, we're done.
    if (in_array($item_name, $editor_settings['toolbar']['items'], TRUE) && $allow_duplicate === FALSE && $item_name !== '|') {
      return;
      return FALSE;
    }

    if (is_int($position)) {
@@ -87,14 +123,10 @@ public function apply(string $configName, mixed $value): void {
      $editor_settings['toolbar']['items'][] = $item_name;
    }

    // If we're just adding a vertical separator, there's nothing else we need
    // to do at this point.
    if ($item_name === '|') {
      return;
    }

    // If this item is associated with a plugin, ensure that it's configured
    // at the editor level, if necessary.
    // at the editor level, if necessary. A vertical separator is not
    // associated with any plugin.
    if ($item_name !== '|') {
      /** @var \Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition $definition */
      foreach ($this->pluginManager->getDefinitions() as $id => $definition) {
        if (array_key_exists($item_name, $definition->getToolbarItems())) {
@@ -111,8 +143,10 @@ public function apply(string $configName, mixed $value): void {
          break;
        }
      }
    }

    $editor->setSettings($editor_settings)->save();
    $editor->setSettings($editor_settings);
    return TRUE;
  }

}
+62 −0
Changes for core/modules/ckeditor5/tests/src/Kernel/ConfigAction/AddItemToToolbarConfigActionTest.php: 62 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -106,6 +106,68 @@ public function testAddItemToToolbar(string|array $action, array $expected_toolb
    }
  }

  /**
   * Tests adding multiple items to an editor toolbar.
   */
  public function testAddMultipleItemsToToolbar(): void {
    $recipe = $this->createRecipe([
      'name' => 'CKEditor 5 multiple toolbar items test',
      'config' => [
        'actions' => [
          'editor.editor.filter_test' => [
            'addItemsToToolbar' => [
              'sourceEditing',
              [
                'item_name' => 'italic',
                'allow_duplicate' => TRUE,
                'position' => 0,
              ],
            ],
          ],
        ],
      ],
    ]);
    RecipeRunner::processRecipe($recipe);

    /** @var array{toolbar: array{items: string[]}, plugins: array<string, array<mixed>>} $settings */
    $settings = Editor::load('filter_test')?->getSettings();
    // The `sourceEditing` item should have been appended to the initial
    // toolbar, and a duplicate `italic` item inserted at the beginning.
    $this->assertSame(['italic', 'heading', 'bold', 'italic', 'sourceEditing'], $settings['toolbar']['items']);
    // The plugin's default settings should have been added.
    $this->assertSame([], $settings['plugins']['ckeditor5_sourceEditing']['allowed_tags']);
  }

  /**
   * Tests that an empty list of toolbar items is rejected.
   */
  public function testEmptyItemListIsRejected(): void {
    $this->expectException(ConfigActionException::class);
    $this->expectExceptionMessageIs('The editor:addItemsToToolbar config action requires one or more toolbar items.');
    $this->container->get('plugin.manager.config_action')
      ->applyAction('editor:addItemsToToolbar', 'editor.editor.filter_test', []);
  }

  /**
   * Tests that a toolbar item which is not a string or array is rejected.
   */
  public function testInvalidItemTypeIsRejected(): void {
    $this->expectException(ConfigActionException::class);
    $this->expectExceptionMessageIs('The editor:addItemsToToolbar config action requires each toolbar item to be a string or an array of options.');
    $this->container->get('plugin.manager.config_action')
      ->applyAction('editor:addItemsToToolbar', 'editor.editor.filter_test', [123]);
  }

  /**
   * Tests that a toolbar item without an item name is rejected.
   */
  public function testMissingItemNameIsRejected(): void {
    $this->expectException(ConfigActionException::class);
    $this->expectExceptionMessageIs("The editor:addItemsToToolbar config action requires each toolbar item to define an 'item_name' string.");
    $this->container->get('plugin.manager.config_action')
      ->applyAction('editor:addItemsToToolbar', 'editor.editor.filter_test', [['position' => 0]]);
  }

  /**
   * Tests that adding non-existent toolbar item to CKEditor triggers an error.
   */