ckeditor5-1.0.x-dev/ckeditor5.module

ckeditor5.module
<?php

/**
 * @file
 * Implements hooks for the CKEditor 5 module.
 */

use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\PrependCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Symfony\Component\Validator\Constraints\Choice;

/**
 * Implements hook_theme().
 */
function ckeditor5_theme() {
  return [
    'ckeditor5_settings_toolbar' => [
      'render element' => 'form',
    ],
  ];
}

/**
 * Implements hook_module_implements_alter().
 */
function ckeditor5_module_implements_alter(&$implementations, $hook) {
  // This module's implementation of form_filter_format_form_alter() must happen
  // after the editor module's implementation, as that implementation adds the
  // active editor to $form_state. It must also happen after the media module's
  // implementation so media_filter_format_edit_form_validate can be removed
  // from the validation chain, as that validator is not needed with CKEditor 5
  // and will trigger a false error.
  if ($hook === 'form_alter' && isset($implementations['ckeditor5']) && isset($implementations['editor'])) {
    $group = $implementations['ckeditor5'];
    unset($implementations['ckeditor5']);

    $offset = array_search('editor', array_keys($implementations)) + 1;
    $media_offset = array_search('media', array_keys($implementations)) + 1;
    $max = max([$offset, $media_offset]);
    $implementations = array_slice($implementations, 0, $max, TRUE) +
      ['ckeditor5' => $group] +
      array_slice($implementations, $offset, NULL, TRUE);
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function ckeditor5_form_filter_format_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
  $form['#validate'][] = 'ckeditor5_form_filter_admin_format_validate';
  $editor = $form_state->get('editor');

  // CKEditor 5 plugin config determines the available HTML tags. If an HTML
  // restricting filter is enabled and the editor is CKEditor 5, the 'Allowed
  // HTML tags' field is made read only and automatically populated with the
  // values needed by CKEditor 5 plugins. If there is no fundamental
  // compatibility problem.
  // @see \Drupal\ckeditor5\Plugin\Editor\CKEditor5::buildConfigurationForm()
  if ($editor && $editor->getEditor() === 'ckeditor5' && $form_state->get('ckeditor5_fundamentally_compatible')) {
    if (isset($form['filters']['settings']['filter_html']['allowed_html'])) {
      $filter_allowed_html = &$form['filters']['settings']['filter_html']['allowed_html'];

      // Get a string of the HTML elements that must be allowed based on the
      // active CKEditor 5 plugins, and make that the value of the 'Allowed
      // HTML tags' field.
      $plugin_required_elements = \Drupal::service('ckeditor5.admin_ui')->getRequiredElements($form_state);
      $filter_allowed_html['#value'] = $plugin_required_elements;
      $filter_allowed_html['#default_value'] = $plugin_required_elements;

      // Set readonly and add the form-disabled wrapper class as using #disabled
      // or the disabled attribute will prevent the new values from being
      // validated.
      $filter_allowed_html['#attributes']['readonly'] = TRUE;
      $filter_allowed_html['#wrapper_attributes']['class'][] = 'form-disabled';

      $filter_allowed_html['#description'] = t('With CKEditor 5 this is a
          read-only field. The allowed HTML tags and attributes are determined
          by the CKEditor 5 configuration. Manually removing tags would break
          enabled functionality, and any manually added tags would be removed by
          CKEditor 5 on render.');

      // The media_filter_format_edit_form_validate validator is not needed
      // with CKEditor 5 as it exists to enforce the inclusion of specific
      // allowed tags that are added automatically by CKEditor 5. The
      // validator is removed so it does not conflict with the automatic
      // addition of those allowed tags.
      $key = array_search('media_filter_format_edit_form_validate', $form['#validate']);
      if ($key !== FALSE) {
        unset($form['#validate'][$key]);
      }
    }

    // Replace editor_form_filter_admin_format_validate with a validator that
    // still invokes editor_form_filter_admin_format_validate, but does not run
    // if the triggering element is 'editor_configure'.
    $editor_validate_offset = array_search('editor_form_filter_admin_format_validate', $form['#validate']);
    if ($editor_validate_offset !== FALSE) {
      $form['#validate'][$editor_validate_offset] = '_ckeditor5_editor_form_filter_admin_format_validate';
    }

    // Although validation errors should be limited on AJAX rebuilds, this must
    // be accomplished using something other than #limit_validation_errors. That
    // approach removes necessary values from $form_state, used for
    // functionality such as automatic syncing of settings between CKEditor 5
    // and the format's filter settings. The validation errors on AJAX are
    // suppressed in _ckeditor5_editor_form_filter_admin_format_validate()
    // instead.
    unset($form['editor']['configure']['#limit_validation_errors']);
  }

  // Override the AJAX callbacks for changing editors, so multiple areas of the
  // form can be updated on change.
  $form['editor']['editor']['#ajax'] = [
    'callback' => '_update_ckeditor5_html_filter',
    'trigger_as' => ['name' => 'editor_configure'],
  ];
  $form['editor']['configure']['#ajax'] = [
    'callback' => '_update_ckeditor5_html_filter',
  ];

  $form['editor']['settings']['subform']['toolbar']['items']['#ajax'] = [
    'callback' => '_update_ckeditor5_html_filter',
    'trigger_as' => ['name' => 'editor_configure'],
    'event' => 'change',
    'ckeditor5_only' => 'true',
  ];

  foreach (Element::children($form['filters']['status']) as $filter_type) {
    $form['filters']['status'][$filter_type]['#ajax'] = [
      'callback' => '_update_ckeditor5_html_filter',
      'trigger_as' => ['name' => 'editor_configure'],
      'event' => 'change',
      'ckeditor5_only' => 'true',
    ];
  }

  if (!function_exists('_add_ajax_listeners_to_plugin_inputs')) {

    /**
     * Recursively adds AJAX listeners to plugin settings elements.
     *
     * These are added so allowed tags and other fields that have values
     * dependent on plugin settings can be updated via AJAX when these settings
     * are changed in the editor form.
     *
     * @param array $plugins_config_form
     *   The plugins config subform render array.
     */
    function _add_ajax_listeners_to_plugin_inputs(array &$plugins_config_form) : void {
      $field_types = [
        'checkbox',
        'select',
        'radios',
      ];
      if (isset($plugins_config_form['#type']) && in_array($plugins_config_form['#type'], $field_types) && !isset($plugins_config_form['#ajax'])) {
        $plugins_config_form['#ajax'] = [
          'callback' => '_update_ckeditor5_html_filter',
          'trigger_as' => ['name' => 'editor_configure'],
          'event' => 'change',
          'ckeditor5_only' => 'true',
        ];
      }

      foreach ($plugins_config_form as $key => &$value) {
        if (is_array($value) && strpos($key, '#') === FALSE) {
          _add_ajax_listeners_to_plugin_inputs($value);
        }
      }
    }

  }

  if (isset($form['editor']['settings']['subform']['plugins'])) {
    _add_ajax_listeners_to_plugin_inputs($form['editor']['settings']['subform']['plugins']);
  }

  // Add an ID to the filter settings vertical tabs wrapper to facilitate AJAX
  // updates.
  // @todo consider moving this to editor.module when this module is moved to
  //   Drupal core.
  $form['filter_settings']['#wrapper_attributes']['id'] = 'filter-settings-wrapper';

  // Add an ID to the editor settings vertical tabs wrapper so it can be easily
  // targeted by JavaScript.
  // @todo consider moving this to editor.module when this module is moved to
  //   Drupal core.
  $form['editor']['settings']['subform']['plugin_settings']['#wrapper_attributes']['id'] = 'plugin-settings-wrapper';

  $editor_name = $editor ? $editor->getEditor() : NULL;
  $form_state->set('ckeditor5_configured_editor', $editor_name);

  $form['#validate'][] = 'ckeditor5_validate_switching_from_different_editor';
}

/**
 * Validator to inform the user of CKEditor 5 compatibility problems.
 */
function ckeditor5_validate_switching_from_different_editor(array $form, FormStateInterface $form_state) {
  $switching_from_a_different_editor = \Drupal::service('ckeditor5.admin_ui')->isSwitchingToCkeditor5($form_state);
  if ($switching_from_a_different_editor) {
    $submitted_editor = \Drupal::service('ckeditor5.admin_ui')->getSubmittedEditorAndFilterFormat($form_state);
    $fundamental_incompatibilities = CKEditor5::validatePair($submitted_editor, $submitted_editor->getFilterFormat(), FALSE);
    foreach ($fundamental_incompatibilities as $violation) {
      // @codingStandardsIgnoreLine
      $form_state->setErrorByName('editor][editor', t($violation->getMessageTemplate(), $violation->getParameters()));
    }
  }
}

/**
 * Validator that is called instead of editor_form_filter_admin_format_validate.
 *
 * In CKEditor 5, when the editor form is updated via AJAX submit using the
 * 'editor_configure' button, #limit_validation_errors' prevents validation for
 * all fields except 'editor'. The editor_form_filter_admin_format_validate()
 * is largely configured to bypass validation on AJAX submits triggered by
 * 'editor_configure', but it still validates the 'editor' field's subforms.
 * CKEditor 5's plugin subform has stricter validation that can fail unhelpfully
 * if triggered via an AJAX update. This validator immediately checks if the
 * form was triggered via 'editor_configure'. If not, then the originally set
 * validator is called: editor_form_filter_admin_format_validate().
 */
function _ckeditor5_editor_form_filter_admin_format_validate($form, FormStateInterface $form_state) {
  if ($form_state->getTriggeringElement()['#name'] === 'editor_configure') {
    // This validator is only added to $form when the configured editor is
    // CKEditor 5. It is still necessary to check the configured editor as this
    // validator will be called when switching a format's editor from CKEditor 5
    // to a different editor.
    if ($form_state->getValues()['editor']['editor'] === 'ckeditor5') {
      // Calling clearErrors() effectively disables validation on AJAX rebuilds.
      // This is used instead of #limit_validation_errors in order to preserve
      // the values in $form_state, as these values inform how the form is
      // rebuilt.
      $form_state->clearErrors();
    }
    else {
      // @todo this condition is only reached when switching from CKEditor 5
      //   to another editor. For editors other than CKEditor 5, the expectation
      //   is for all elements within $form['editor'] to be validated. It seems
      //   like a reasonable expectation for that validation to return as soon
      //   as the format switches to a non-CK5 editor. However, that results in
      //   applying CKEditor 5's strict validation criteria, and errors will
      //   prevent the editor from switching, even if it's a setting only
      //   relevant to CKEditor 5. This whole "else" should either be removed,
      //   or this should validate based on the criteria for the editor being
      //   switched to.
      // @see https://drupal.org/node/3218985
    }
    return;
  }
  editor_form_filter_admin_format_validate($form, $form_state);
}

/**
 * AJAX callback handler for filter_format_form().
 *
 * Used instead of editor_form_filter_admin_form_ajax from the editor module.
 */
function _update_ckeditor5_html_filter(array $form, FormStateInterface $form_state) {
  $switching_from_a_different_editor = \Drupal::service('ckeditor5.admin_ui')->isSwitchingToCkeditor5($form_state);
  $response = new AjaxResponse();
  $renderer = \Drupal::service('renderer');

  // Replace the editor settings with the settings for the currently selected
  // editor. This is the default behavior of editor.module.
  $renderedField = $renderer->render($form['editor']['settings']);
  $response->addCommand(new ReplaceCommand('#editor-settings-wrapper', $renderedField));

  // AJAX validation errors should appear visually close to the text editor
  // since this is a very long form: otherwise they would not be noticed.
  $response->addCommand(new PrependCommand('#editor-settings-wrapper', ['#type' => 'status_messages']));

  // Do not rebuild the filter settings if switching to CKEditor 5 from another
  // editor and there are validation errors.
  if (!$switching_from_a_different_editor && empty($form_state->getErrors())) {
    // Replace the filter settings with the settings for the currently selected
    // editor.
    $renderedSettings = $renderer->render($form['filter_settings']);
    $response->addCommand(new ReplaceCommand('#filter-settings-wrapper', $renderedSettings));
  }

  // If switching to CKEditor 5 from another editor and there are errors in that
  // switch, add an error class to the editor select, otherwise remove.
  $response->addCommand(new InvokeCommand('[data-drupal-selector="edit-editor-editor"]', $switching_from_a_different_editor && !empty($form_state->getErrors()) ? 'addClass' : 'removeClass', ['error']));

  if (!function_exists('_add_attachments_to_editor_update_response')) {

    /**
     * Recursively find #attach items in the form and add as attachments to the
     * AJAX response.
     *
     * @param array $form
     *   A form array.
     * @param \Drupal\Core\Ajax\AjaxResponse $response
     *   The AJAX response attachments will be added to.
     */
    function _add_attachments_to_editor_update_response(array $form, AjaxResponse &$response) : void {
      foreach ($form as $key => $value) {
        if ($key === "#attached") {
          $response->addAttachments(array_diff_key($value, ['placeholders' => '']));
        }
        elseif (is_array($value) && strpos($key, '#') === FALSE) {
          _add_attachments_to_editor_update_response($value, $response);
        }
      }
    }

  }

  _add_attachments_to_editor_update_response($form, $response);

  return $response;
}

/**
 * Validator to validate conditions for individual CKEditor 5 plugins.
 */
function ckeditor5_form_filter_admin_format_validate(array $form, FormStateInterface $form_state) {
  // @todo do we need to run after editor_form_filter_admin_format_validate()?
  if ($form_state->getTriggeringElement()['#name'] !== 'op') {
    return;
  }
  $editor = $form_state->getValue(['editor', 'editor']);
  if ($editor !== 'ckeditor5' || !$form_state->get('editor')) {
    return;
  }

  \Drupal::service('ckeditor5.admin_ui')->validateConditions($form_state);
}

/**
 * Implements hook_library_info_alter().
 */
function ckeditor5_library_info_alter(&$libraries, $extension) {
  if ($extension === 'filter') {
    $libraries['drupal.filter.admin']['dependencies'][] = 'ckeditor5/ie11.filter.warnings';
    $libraries['drupal.filter.admin']['dependencies'][] = 'ckeditor5/drupal.ckeditor5.filter.admin';
  }

  // If the 'ckeditor5/ie11.user.warnings' library is added as a dependency of
  // other Ckeditor 5 libraries, it won't reliably work as the CKEditor 5 assets
  // are loaded via AJAX, and the IE11-incompatible syntax in CKEditor 5 can
  // prevent the AJAX call from successfully loading the functionality in
  // 'ckeditor5/ie11.user.warnings'. Adding this as a dependency of
  // 'system/base', as excessive as it may seem, is the most reliable way to
  // assure it is loaded as part of the page request.
  if ($extension === 'system') {
    $libraries['base']['dependencies'][] = 'ckeditor5/ie11.user.warnings';
  }
}

/**
 * Implements hook_validation_constraint_alter().
 */
function ckeditor5_validation_constraint_alter(array &$definitions) {
  // Add the Symfony validation constraints that Drupal core does not add in
  // \Drupal\Core\Validation\ConstraintManager::registerDefinitions() for
  // unknown reasons. Do it defensively, to not break when this changes.
  if (!isset($definitions['Choice'])) {
    $definitions['Choice'] = [
      'label' => 'Choice',
      'class' => Choice::class,
      'type' => FALSE,
      'provider' => 'core',
      'id' => 'Choice',
    ];
  }
}

/**
 * Implements hook_config_schema_info_alter().
 */
function ckeditor5_config_schema_info_alter(&$definitions) {
  // @see filter.format.*.filters
  $definitions['ckeditor5_valid_pair__format_and_editor']['mapping']['filters'] = $definitions['filter.format.*']['mapping']['filters'];
  // @see @see editor.editor.*.image_upload
  $definitions['ckeditor5_valid_pair__format_and_editor']['mapping']['image_upload'] = $definitions['editor.editor.*']['mapping']['image_upload'];
}

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

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