ckeditor5-1.0.x-dev/src/Plugin/Editor/CKEditor5.php

src/Plugin/Editor/CKEditor5.php
<?php

namespace Drupal\ckeditor5\Plugin\Editor;

use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\ckeditor5\Plugin\CKEditor5PluginManager;
use Drupal\editor\EditorInterface;
use Drupal\editor\Entity\Editor;
use Drupal\editor\Plugin\EditorBase;
use Drupal\filter\FilterFormatInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationListInterface;

/**
 * Defines a CKEditor 5-based text editor for Drupal.
 *
 * @Editor(
 *   id = "ckeditor5",
 *   label = @Translation("CKEditor 5"),
 *   supports_content_filtering = TRUE,
 *   supports_inline_editing = TRUE,
 *   is_xss_safe = TRUE,
 *   supported_element_types = {
 *     "textarea"
 *   }
 * )
 */
class CKEditor5 extends EditorBase implements ContainerFactoryPluginInterface {

  /**
   * The CKEditor plugin manager.
   *
   * @var \Drupal\ckeditor\Plugin\CKEditorPluginManager
   */
  protected $ckeditor5PluginManager;

  /**
   * The language manager.
   *
   * @var \Drupal\Core\Language\LanguageManagerInterface
   */
  protected $languageManager;

  /**
   * Constructs a CKEditor5 editor plugin.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Drupal\ckeditor5\Plugin\CKEditor5PluginManager $ckeditor5_plugin_manager
   *   The CKEditor5 plugin manager.
   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
   *   The language manager.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, CKEditor5PluginManager $ckeditor5_plugin_manager, LanguageManagerInterface $language_manager) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->ckeditor5PluginManager = $ckeditor5_plugin_manager;
    $this->languageManager = $language_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('plugin.manager.ckeditor5.plugin'),
      $container->get('language_manager')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getDefaultSettings() {
    // The default toolbar items will be accompanied by any plugin with an
    // isEnabled() method that returns TRUE.
    return [
      'toolbar' => [
        'items' => ['heading', 'bold', 'italic'],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function settingsForm(array $form, FormStateInterface $form_state, Editor $editor) {
  }

  /**
   * Validates a Text Editor + Text Format pair.
   *
   * Drupal is designed to only verify schema conformity (and validation) of
   * individual config entities. The Text Editor module layers a tightly coupled
   * Editor entity on top of the Filter module's FilterFormat config entity.
   * This inextricable coupling is clearly visible in EditorInterface:
   * \Drupal\editor\EditorInterface::getFilterFormat(). They are always paired.
   * Because not every text editor is guaranteed to be compatible with every
   * text format, the pair must be validated.
   *
   * @param \Drupal\editor\EditorInterface $text_editor
   *   The paired text editor to validate.
   * @param \Drupal\filter\FilterFormatInterface $text_format
   *   The paired text format to validate.
   * @param bool $all_compatibility_problems
   *   Get all compatibility problems (default) or only fundamental ones.
   *
   * @return \Symfony\Component\Validator\ConstraintViolationListInterface
   *   The validation constraint violations.
   *
   * @see \Drupal\editor\EditorInterface::getFilterFormat()
   * @see ckeditor5.pair.schema.yml
   */
  public static function validatePair(EditorInterface $text_editor, FilterFormatInterface $text_format, bool $all_compatibility_problems = TRUE) : ConstraintViolationListInterface {
    if ($text_editor->getEditor() !== 'ckeditor5') {
      throw new \DomainException('This text editor is not configured to use CKEditor 5.');
    }

    $typed_config_manager = \Drupal::getContainer()->get('config.typed');
    $typed_config = $typed_config_manager->createFromNameAndData(
      'ckeditor5_valid_pair__format_and_editor',
      [
        // A mix of:
        // - editor.editor.*.settings — note that "settings" is top-level in
        //   editor.editor.*, and so it is here, so all validation constraints
        //   will continue to work fine.
        'settings' => $text_editor->toArray()['settings'],
        // - filter.format.*.filters — note that "filters" is top-level in
        //   filter.format.*, and so it is here, so all validation constraints
        //   will continue to work fine.
        'filters' => $text_format->toArray()['filters'],
        // - editor.editor.*.image_upload — note that "image_upload" is
        //   top-level in editor.editor.*, and so it is here, so all validation
        //   constraints will continue to work fine.
        'image_upload' => $text_editor->toArray()['image_upload'],
      ]
    );
    $violations = $typed_config->validate();

    // Only consider validation constraint violations covering the pair, so not
    // irrelevant details such as a PrimitiveTypeConstraint being somewhere in
    // filter settings.
    foreach ($violations as $i => $violation) {
      assert($violation instanceof ConstraintViolation);
      if (strpos(get_class($violation->getConstraint()), 'Drupal\\ckeditor5\\') === 0) {
        continue;
      }
      $violations->remove($i);
    }

    if (!$all_compatibility_problems) {
      foreach ($violations as $i => $violation) {
        // Remove all violations that are not fundamental — these are at the
        // root (property path '').
        if ($violation->getPropertyPath() !== '') {
          $violations->remove($i);
        }
      }
    }

    return $violations;
  }

  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
    $editor = $form_state->get('editor');
    assert($editor instanceof Editor);
    $language = $this->languageManager->getCurrentLanguage();

    $form['toolbar'] = [
      '#type' => 'container',
      '#title' => $this->t('CKEditor 5 toolbar configuration'),
      '#theme' => 'ckeditor5_settings_toolbar',
      '#attached' => [
        'library' => $this->ckeditor5PluginManager->getAdminLibraries(),
        'drupalSettings' => [
          'ckeditor5' => [
            'language' => [
              'dir' => $language->getDirection(),
              'langcode' => $language->getId(),
            ],
          ],
        ],
      ],
    ];

    $form['available_items_description'] = [
      '#type' => 'container',
      '#markup' => $this->t('Press the down arrow key to add to the toolbar.'),
      '#id' => 'available-button-description',
      '#attributes' => [
        'class' => ['visually-hidden'],
      ],
    ];

    $form['active_items_description'] = [
      '#type' => 'container',
      '#markup' => $this->t('Move this button in the toolbar by pressing the left or right arrow keys. Press the up arrow key to remove from the toolbar.'),
      '#id' => 'active-button-description',
      '#attributes' => [
        'class' => ['visually-hidden'],
      ],
    ];

    // The items are encoded in markup to provide a no-JS fallback.
    // Although CKEditor 5 is useless without JS it would still be possible
    // to see all the available toolbar items provided by plugins in the format
    // that needs to be entered in the textarea. The UI app parses this list.
    $form['toolbar']['available'] = [
      '#type' => 'container',
      '#title' => 'Available items',
      '#id' => 'ckeditor5-toolbar-buttons-available',
      'available_items' => [
        '#markup' => Json::encode($this->ckeditor5PluginManager->getToolbarItems()),
      ],
    ];

    $editor_settings = $editor->getSettings();
    // This form field requires a JSON-style array of valid toolbar items.
    // e.g. ["bold","italic","|","uploadImage"].
    // CKEditor 5 config for toolbar items takes an array of strings which
    // correspond to the keys under toolbar_items in a plugin yml or annotation.
    // @see https://ckeditor.com/docs/ckeditor5/latest/features/toolbar/toolbar.html
    $form['toolbar']['items'] = [
      '#type' => 'textarea',
      '#title' => $this->t('Toolbar items'),
      '#rows' => 1,
      '#default_value' => Json::encode($editor_settings['toolbar']['items']),
      '#id' => 'ckeditor5-toolbar-buttons-selected',
      '#attributes' => [
        'tabindex' => '-1',
        'aria-hidden' => 'true',
      ],
    ];

    $form['plugin_settings'] = [
      '#type' => 'vertical_tabs',
      '#title' => $this->t('CKEditor5 plugin settings'),
      '#id' => 'ckeditor5-plugin-settings',
    ];
    $this->ckeditor5PluginManager->injectPluginSettingsForm($form, $form_state, \Drupal::service('ckeditor5.admin_ui')->getSubmittedEditorAndFilterFormat($form_state->getCompleteFormState()));

    // Immediately warn the user about fundamental incompatibilities, long
    // before the user has spent time reconfiguring their text format.
    // @see ckeditor5_form_filter_admin_format_validate()
    $submitted_editor = \Drupal::service('ckeditor5.admin_ui')->getSubmittedEditorAndFilterFormat($form_state->getCompleteFormState());
    $fundamental_incompatibilities = static::validatePair($submitted_editor, $submitted_editor->getFilterFormat(), FALSE);
    $form_state->set('ckeditor5_fundamentally_compatible', $fundamental_incompatibilities->count() === 0);
    foreach ($fundamental_incompatibilities as $violation) {
      // @see \Drupal\Core\Entity\Entity\EntityFormDisplay::movePropertyPathViolationsRelativeToField()
      // @see \Drupal\link\Plugin\Field\FieldWidget\LinkWidget::flagErrors()
      // Note: we cannot use $violation->getMessage(): it contains double-
      // escaped translatable markup parameters.
      // @codingStandardsIgnoreLine
      $this->messenger()->addError($this->t($violation->getMessageTemplate(), $violation->getParameters()));
    }

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
    $json = $form_state->getValue(['toolbar', 'items']);
    $toolbar_items = Json::decode($json);

    // This basic validation must live in the form logic because it can only
    // occur in a form context.
    if (!$toolbar_items) {
      $form_state->setErrorByName('toolbar][items', $this->t('Invalid toolbar value.'));
      return;
    }

    $submitted_editor = \Drupal::service('ckeditor5.admin_ui')->getSubmittedEditorAndFilterFormat($form_state->getCompleteFormState());
    assert($submitted_editor instanceof EditorInterface);

    // @codingStandardsIgnoreStart
    // @todo After https://www.drupal.org/project/drupal/issues/2164373 lands,
    // we should be able to simplify this to
    // $violations = $submitted_editor->getTypedData()->validate();
    // @codingStandardsIgnoreEnd

    $typed_config_manager = \Drupal::getContainer()->get('config.typed');

    // Validate the text editor config entity prior to saving.
    $typed_config = $typed_config_manager->createFromNameAndData(
      $submitted_editor->getConfigDependencyName(),
      $submitted_editor->toArray(),
    );
    $violations = $typed_config->validate();
    foreach ($violations as $violation) {
      $form_item_name = static::mapViolationPropertyPathsToFormNames($violation->getPropertyPath());
      $form_state->setError(NestedArray::getValue($form, explode('][', $form_item_name)), $violation->getMessage());
      // @todo Remove the above line in favor of the below line when https://www.drupal.org/project/drupal/issues/3217124 is fixed.
      // $form_state->setErrorByName($form_item_name, $violation->getMessage());
    }

    // Validate the text editor + text format pair.
    $violations = CKEditor5::validatePair($submitted_editor, $submitted_editor->getFilterFormat());
    foreach ($violations as $violation) {
      $form_item_name = static::mapPairViolationPropertyPathsToFormNames($violation->getPropertyPath());
      $form_state->getCompleteFormState()->setErrorByName($form_item_name, $violation->getMessage());
    }
  }

  /**
   * Maps Text Editor config object property paths to form names.
   *
   * @param string $property_path
   *   A config object property path.
   *
   * @return string
   *   The corresponding form name in the subform.
   */
  protected static function mapViolationPropertyPathsToFormNames(string $property_path) : string {
    // All of the toolbar item configuration is powered by a small JavaScript
    // application which writes its results as a JSON blob in a text area at the
    // location toolbar][items.
    if (preg_match('/^settings\.toolbar\.items.*/', $property_path)) {
      return 'toolbar][items';
    }

    return implode('][', array_slice(explode('.', $property_path), 1));
  }

  /**
   * Maps Text Editor + Text Format pair property paths to form names.
   *
   * @param string $property_path
   *   A config object property path.
   *
   * @return string
   *   The corresponding form name in the complete form.
   */
  protected static function mapPairViolationPropertyPathsToFormNames(string $property_path) : string {
    // Filters are top-level.
    if (preg_match('/^filters\..*/', $property_path)) {
      return implode('][', array_merge(explode('.', $property_path), ['settings']));
    }

    // Everything else is in the subform.
    return 'editor][' . static::mapViolationPropertyPathsToFormNames($property_path);
  }

  /**
   * {@inheritdoc}
   */
  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
    $form_state->setValue(
      ['toolbar', 'items'],
      Json::decode($form_state->getValue(['toolbar', 'items']))
    );

    // Remove the plugin vertical tab state.
    $form_state->unsetValue('plugin_settings');

    parent::submitConfigurationForm($form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
  public function getJSSettings(Editor $editor) {
    return $this->ckeditor5PluginManager->getCKEditorPluginSettings($editor);
  }

  /**
   * {@inheritdoc}
   */
  public function getLibraries(Editor $editor) {
    return $this->ckeditor5PluginManager->getEnabledLibraries($editor);
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc