ckeditor5-1.0.x-dev/src/AdminUi.php
src/AdminUi.php
<?php
namespace Drupal\ckeditor5;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\editor\Entity\Editor;
/**
* Provide CKEditor5 admin UI functionality.
*/
class AdminUi {
use StringTranslationTrait;
/**
* The CKEditor 5 plugin manager.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $pluginManager;
/**
* Constructs an AdminUi object.
*
* @param \Drupal\Component\Plugin\PluginManagerInterface $plugin_manager
* The CKEditor 5 plugin manager.
*/
public function __construct(PluginManagerInterface $plugin_manager) {
$this->pluginManager = $plugin_manager;
}
/**
* Gets submitted Editor and FilterFormat config entities from form state.
*
* This duplicates some form submission logic, but that is better than having
* every single CKEditor5Plugin plugin implementation hardcoding knowledge
* about this form — especially since it could be altered.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state of the filter format form.
*
* @return \Drupal\editor\Entity\Editor
* The submitted Editor config entity, based on the form values.
*
* @throws \ReflectionException
*/
public static function getSubmittedEditorAndFilterFormat(FormStateInterface $form_state) : Editor {
// Determine the form is currently switching to CKEditor 5 from another
// editor. If the form was previously set to a different editor (or no
// editor), there are two major differences that need to be accounted for:
// - If the editor isn't CKEditor 5, #limit_validation_errors is set in a
// way where the only values available are 'editor' and its children.
// - The editor may not be available in $form_state->get('editor') yet, but
// fortunately it's safe to use Editor::create to create a new CKEditor 5
// editor as the editor would be using its default configuration at the
// "switching to" stage.
$switching_to_ck5 = self::isSwitchingToCkeditor5($form_state);
if ($switching_to_ck5) {
// Create a hypothetical version of the current editor using CKEditor 5.
// This makes it possible to validate the editor's compatibility with
// CKEditor 5 before the editor set within $form_state actually changes.
$format = $form_state->getFormObject()->getEntity();
$submitted_editor = Editor::create([
'format' => $format->isNew() ? NULL : $format->id(),
'editor' => 'ckeditor5',
]);
}
else {
$submitted_editor = clone $form_state->get('editor');
}
$submitted_format = clone $form_state->getFormObject()->getEntity();
// Note: \Drupal\filter\FilterFormatFormBase::validateForm() already ran,
// so the form values are known to be valid.
// @see \Drupal\filter\FilterFormatFormBase::submitForm()
$values = $form_state->getFormObject()->getEntity()->isNew() || $switching_to_ck5
? $form_state->getUserInput()
: $form_state->getValues();
foreach ($values as $key => $value) {
if ($key !== 'filters') {
$submitted_format->set($key, $value);
}
else {
foreach ($value as $instance_id => $config) {
$submitted_format->setFilterConfig($instance_id, $config);
}
}
}
// @see \editor_form_filter_admin_format_submit()
if ($settings = $form_state->getValue(['editor', 'settings'])) {
// If the editor is new and no button config changes have been made in the
// UI, skip updating $settings['toolbar']['items'] so that setting can use
// the value returned by \CKEditor5::getDefaultSettings().
if (isset($settings['toolbar']['items']) && !is_null($settings['toolbar']['items'])) {
// Decode form value JSON blob to save as array.
$settings['toolbar']['items'] = Json::decode($settings['toolbar']['items']);
$submitted_editor->setSettings($settings);
}
}
// Overwrite the Editor config entity object's $filterFormat property, to
// prevent calls to Editor::hasAssociatedFilterFormat() and
// Editor::getFilterFormat() from loading the FilterFormat from storage.
// @todo Refactor the upstream implementations to remove the need for this.
$reflector = new \ReflectionObject($submitted_editor);
$property = $reflector->getProperty('filterFormat');
$property->setAccessible(TRUE);
$property->setValue($submitted_editor, $submitted_format);
return $submitted_editor;
}
/**
* Checks if the form is currently switching to CKEditor 5.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state of the filter format form.
*
* @return bool
* True if the form is currently switching to CKEditor 5, otherwise false.
*/
public static function isSwitchingToCkeditor5(FormStateInterface $form_state) {
$user_input = $form_state->getUserInput();
return $form_state->get('ckeditor5_configured_editor') !== 'ckeditor5' && (!empty($user_input['editor']['editor']) && $user_input['editor']['editor'] === 'ckeditor5') && $form_state->getTriggeringElement()['#name'] === 'editor_configure';
}
/**
* Returns the value required by the 'Allowed HTML tags' field.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The filter form state.
*
* @return string
* The string listing allowed elements.
*/
public function getRequiredElements(FormStateInterface $form_state) {
$submitted_editor = static::getSubmittedEditorAndFilterFormat($form_state);
$enabled_plugins = array_keys($this->pluginManager->getEnabledDefinitions($submitted_editor));
$provided = $this->pluginManager->getProvidedElements($enabled_plugins);
return $this->pluginManager->getReadableElements($provided);
}
/**
* Validate the conditions of each plugin.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Editor settings form state.
*/
public function validateConditions(FormStateInterface $form_state) {
$submitted_editor = static::getSubmittedEditorAndFilterFormat($form_state);
$plugins = $this->pluginManager->getEnabledDefinitions($submitted_editor);
$parents_base = ['editor', 'settings'];
foreach ($plugins as $definition) {
$conditions = isset($definition['conditions']) ? $definition['conditions'] : [];
foreach ($conditions as $condition_type => $required_value) {
switch ($condition_type) {
case 'toolbarItem':
if (!in_array($required_value, $submitted_editor->getSettings()['toolbar']['items'])) {
$parents = array_merge($parents_base, ['toolbar', 'items']);
$form_state->setErrorByName(implode('][', $parents), $this->t('The %plugin-id plugin requires the %plugin-button toolbar button.', [
'%plugin-id' => $definition['id'],
'%plugin-button' => $required_value,
]));
}
break;
case 'imageUploadStatus':
if ((bool) $submitted_editor->getImageUploadSettings()['status'] !== TRUE) {
$parents = array_merge($parents_base, [
'plugins',
'ckeditor5.imageUpload',
'image_upload',
'status',
]);
$form_state->setErrorByName(implode('][', $parents), $this->t('Image upload requires the %image-upload-button toolbar button.', [
'%image-upload-label' => 'Image upload',
'%image-upload-button' => 'uploadImage',
]));
}
break;
case 'filter':
$filters = $submitted_editor->getFilterFormat()->filters();
if (!$filters->has($required_value) || !$filters->get($required_value)->status) {
$parents = ['filters', $required_value, 'status'];
// If a button is required, tailor the error message to that.
if (in_array('toolbarItem', $conditions, TRUE)) {
$form_state->setErrorByName(implode('][', $parents), $this->t('The %required-filter-label filter must be enabled to use the %plugin-button button.', [
'%required-filter-label' => $filters->get($required_value)->getLabel(),
'%plugin-button' => $conditions['toolbarItem'],
]));
}
// Otherwise, fall back to the CKEditor 5 plugin name.
else {
$form_state->setErrorByName(implode('][', $parents), $this->t('The %required-filter-label filter must be enabled to use the %plugin-id plugin.', [
'%required-filter-label' => $filters->get($required_value)->getLabel(),
'%plugin-id' => $definition['id'],
]));
}
}
break;
}
}
}
}
}
