Heading.php

Same filename in other branches
  1. 10 core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/Heading.php
  2. 11.x core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/Heading.php

Namespace

Drupal\ckeditor5\Plugin\CKEditor5Plugin

File

core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/Heading.php

View source
<?php

declare (strict_types=1);
namespace Drupal\ckeditor5\Plugin\CKEditor5Plugin;

use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableTrait;
use Drupal\ckeditor5\Plugin\CKEditor5PluginDefault;
use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableInterface;
use Drupal\ckeditor5\Plugin\CKEditor5PluginElementsSubsetInterface;
use Drupal\ckeditor5\Plugin\CKEditor5PluginManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\editor\EditorInterface;

/**
 * CKEditor 5 Heading plugin.
 *
 * @internal
 *   Plugin classes are internal.
 */
class Heading extends CKEditor5PluginDefault implements CKEditor5PluginConfigurableInterface, CKEditor5PluginElementsSubsetInterface {
    use CKEditor5PluginConfigurableTrait;
    
    /**
     * The headings that cannot be disabled.
     *
     * @var string[]
     */
    const ALWAYS_ENABLED_HEADINGS = [
        'paragraph',
    ];
    
    /**
     * The default configuration for this plugin.
     *
     * @var string[][]
     */
    const DEFAULT_CONFIGURATION = [
        'enabled_headings' => [
            'heading2',
            'heading3',
            'heading4',
            'heading5',
            'heading6',
        ],
    ];
    
    /**
     * {@inheritdoc}
     */
    public function defaultConfiguration() {
        return static::DEFAULT_CONFIGURATION;
    }
    
    /**
     * Computes all valid choices for the "enabled_headings" setting.
     *
     * @see ckeditor5.schema.yml
     *
     * @return string[]
     *   All valid choices.
     */
    public static function validChoices() : array {
        $cke5_plugin_manager = \Drupal::service('plugin.manager.ckeditor5.plugin');
        assert($cke5_plugin_manager instanceof CKEditor5PluginManagerInterface);
        $plugin_definition = $cke5_plugin_manager->getDefinition('ckeditor5_heading');
        assert($plugin_definition->getClass() === static::class);
        return array_diff(array_column($plugin_definition->getCKEditor5Config()['heading']['options'], 'model'), static::ALWAYS_ENABLED_HEADINGS);
    }
    
    /**
     * Gets all enabled headings.
     *
     * @return string[]
     *   The values in the plugins.ckeditor5_heading.enabled_headings configuration
     *   plus the headings that are always enabled.
     */
    private function getEnabledHeadings() : array {
        return array_merge(self::ALWAYS_ENABLED_HEADINGS, $this->configuration['enabled_headings']);
    }
    
    /**
     * {@inheritdoc}
     *
     * Form for choosing which heading tags are available.
     */
    public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
        $form['enabled_headings'] = [
            '#type' => 'fieldset',
            '#title' => $this->t('Enabled Headings'),
            '#description' => $this->t('These are the headings that will appear in the headings dropdown. If a heading is not chosen here, it does not necessarily mean the corresponding tag is disallowed in the text format.'),
        ];
        foreach ($this->getPluginDefinition()
            ->getCKEditor5Config()['heading']['options'] as $heading_option) {
            $model = $heading_option['model'];
            if (in_array($model, self::ALWAYS_ENABLED_HEADINGS, TRUE)) {
                continue;
            }
            // It's safe to use $model as a key: listing the same model twice with
            // different properties triggers a schema error in CKEditor 5.
            // @see https://ckeditor.com/docs/ckeditor5/latest/framework/guides/support/error-codes.html#error-schema-cannot-register-item-twice
            // @see https://ckeditor.com/docs/ckeditor5/latest/features/headings.html#configuring-custom-heading-elements
            $form['enabled_headings'][$model] = self::generateCheckboxForHeadingOption($heading_option);
            $form['enabled_headings'][$model]['#default_value'] = in_array($model, $this->configuration['enabled_headings'], TRUE) ? $model : NULL;
        }
        return $form;
    }
    
    /**
     * {@inheritdoc}
     */
    public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
        // Match the config schema structure at ckeditor5.plugin.ckeditor5_heading.
        $form_value = $form_state->getValue('enabled_headings');
        $config_value = array_values(array_filter($form_value));
        $form_state->setValue('enabled_headings', $config_value);
    }
    
    /**
     * {@inheritdoc}
     */
    public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
        $this->configuration['enabled_headings'] = $form_state->getValue('enabled_headings');
    }
    
    /**
     * Generates checkbox for a CKEditor 5 heading plugin config option.
     *
     * @param array $heading_option
     *   A heading option configuration as the CKEditor 5 Heading plugin expects
     *   in its configuration.
     *
     * @return array
     *   The checkbox render array.
     *
     * @see https://ckeditor.com/docs/ckeditor5/latest/api/module_heading_heading-HeadingConfig.html#member-options
     */
    private static function generateCheckboxForHeadingOption(array $heading_option) : array {
        // This requires the `title` and `model` properties. The `class` property is
        // optional. The `view` property is not used.
        assert(array_key_exists('title', $heading_option));
        assert(array_key_exists('model', $heading_option));
        $checkbox = [
            '#type' => 'checkbox',
            '#title' => $heading_option['title'],
            '#return_value' => $heading_option['model'],
        ];
        if (isset($heading_option['class'])) {
            $checkbox['#label_attributes']['class'][] = $heading_option['class'];
            $checkbox['#label_attributes']['class'][] = 'ck';
        }
        return $checkbox;
    }
    
    /**
     * {@inheritdoc}
     *
     * Filters the header options to those chosen in editor config.
     */
    public function getDynamicPluginConfig(array $static_plugin_config, EditorInterface $editor) : array {
        $enabled_headings = $this->getEnabledHeadings();
        $all_heading_options = $static_plugin_config['heading']['options'];
        $configured_heading_options = array_filter($all_heading_options, function ($option) use ($enabled_headings) {
            return in_array($option['model'], $enabled_headings, TRUE);
        });
        return [
            'heading' => [
                'options' => array_values($configured_heading_options),
            ],
        ];
    }
    
    /**
     * {@inheritdoc}
     */
    public function getElementsSubset() : array {
        return $this->enabledHeadingsToTags($this->configuration['enabled_headings']);
    }
    
    /**
     * Returns an array of enabled tags based on the enabled headings.
     *
     * @param string[] $enabled_headings
     *   Array of the enabled headings.
     *
     * @return string[]
     *   List of tags provided by the enabled headings.
     */
    private function enabledHeadingsToTags(array $enabled_headings) : array {
        $plugin_definition = $this->getPluginDefinition();
        $elements = $plugin_definition->getElements();
        $heading_keyed_by_model = [];
        foreach ($plugin_definition->getCKEditor5Config()['heading']['options'] as $configured_heading) {
            if (isset($configured_heading['model'])) {
                $heading_keyed_by_model[$configured_heading['model']] = $configured_heading;
            }
        }
        $tags_to_return = [];
        foreach ($enabled_headings as $model) {
            if (isset($heading_keyed_by_model[$model]) && isset($heading_keyed_by_model[$model]['view'])) {
                $element_as_tag = "<{$heading_keyed_by_model[$model]['view']}>";
                if (in_array($element_as_tag, $elements, TRUE)) {
                    $tags_to_return[] = "<{$heading_keyed_by_model[$model]['view']}>";
                }
            }
        }
        return $tags_to_return;
    }

}

Classes

Title Deprecated Summary
Heading CKEditor 5 Heading plugin.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.