Unverified Commit f6dbd06a authored by lauriii's avatar lauriii
Browse files

Issue #3224652 by Wim Leers, hooroomoo, vlyalko, lauriii, bnjmnm, huzooka:...

Issue #3224652 by Wim Leers, hooroomoo, vlyalko, lauriii, bnjmnm, huzooka: [drupalImage] Add ckeditor5-image's imageresize plugin to allow image resizing

(cherry picked from commit fa8929ec)
parent 52d0388b
......@@ -140,6 +140,8 @@
* - 'filter': a filter that must be enabled
* - 'imageUploadStatus': TRUE if image upload must be enabled, FALSE if it
* must not be enabled
* - 'requiresConfiguration': a subset of the configuration for this plugin
* that must match (exactly)
* - 'plugins': a list of CKEditor 5 Drupal plugin IDs that must be enabled
*
* All of these can be defined in YAML or annotations. A given plugin should
......
......@@ -430,6 +430,27 @@ ckeditor5_image:
toolbarItem: uploadImage
imageUploadStatus: true
ckeditor5_imageResize:
ckeditor5:
plugins:
- image.ImageResize
config:
image:
resizeUnit: '%'
resizeOptions:
-
name: 'resizeImage:original'
value: null
toolbar: [resizeImage]
drupal:
label: Image resize
class: \Drupal\ckeditor5\Plugin\CKEditor5Plugin\ImageResize
elements: false
conditions:
requiresConfiguration:
allow_resize: true
plugins: [ckeditor5_image]
ckeditor5_imageCaption:
ckeditor5:
plugins:
......
......@@ -62,6 +62,17 @@ ckeditor5.plugin.ckeditor5_heading:
Choice:
callback: \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Heading::validChoices
# Plugin \Drupal\ckeditor5\Plugin\CKEditor5Plugin\ImageResize
ckeditor5.plugin.ckeditor5_imageResize:
type: mapping
label: Image Resize
mapping:
allow_resize:
type: boolean
label: 'Allow resize'
constraints:
NotNull: []
ckeditor5.plugin.ckeditor5_sourceEditing:
type: mapping
label: Source Editing
......
......@@ -97,6 +97,10 @@
return Object.entries(config).reduce((processed, [key, value]) => {
if (typeof value === 'object') {
// Check for null values.
if (!value) {
return processed;
}
if (value.hasOwnProperty('func')) {
processed[key] = buildFunc(value);
} else if (value.hasOwnProperty('regexp')) {
......
......@@ -88,6 +88,10 @@ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len
value = _ref2[1];
if (_typeof(value) === 'object') {
if (!value) {
return processed;
}
if (value.hasOwnProperty('func')) {
processed[key] = buildFunc(value);
} else if (value.hasOwnProperty('regexp')) {
......
<?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\Core\Form\FormStateInterface;
/**
* CKEditor 5 ImageResize plugin.
*
* @internal
* Plugin classes are internal.
*/
class ImageResize extends CKEditor5PluginDefault implements CKEditor5PluginConfigurableInterface {
use CKEditor5PluginConfigurableTrait;
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return ['allow_resize' => TRUE];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['allow_resize'] = [
'#type' => 'checkbox',
'#title' => $this->t('Allow the user to resize images'),
'#default_value' => $this->configuration['allow_resize'],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
// Match the config schema structure at ckeditor5.plugin.ckeditor5_imageResize.
$form_value = $form_state->getValue('allow_resize');
$form_state->setValue('allow_resize', (bool) $form_value);
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configuration['allow_resize'] = $form_state->getValue('allow_resize');
}
}
......@@ -10,6 +10,7 @@
use Drupal\Component\Plugin\Definition\PluginDefinitionInterface;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Core\Config\Schema\SchemaCheckTrait;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
......@@ -160,33 +161,14 @@ private function validateDrupalAspects(string $id, array $definition): void {
$default_configuration = (new \ReflectionClass($definition['drupal']['class']))
->newInstanceWithoutConstructor()
->defaultConfiguration();
$typed_config = \Drupal::service('config.typed');
if (!empty($default_configuration)) {
$configuration_name = sprintf("ckeditor5.plugin.%s", $definition['id']);
if (!$typed_config->hasConfigSchema($configuration_name)) {
if (!$this->getTypedConfig()->hasConfigSchema($configuration_name)) {
throw new InvalidPluginDefinitionException($id, sprintf('The "%s" CKEditor 5 plugin definition is configurable, has non-empty default configuration but has no config schema. Config schema is required for validation.', $id));
}
// TRICKY: SchemaCheckTrait::checkConfigSchema() dynamically adds a
// 'langcode' key-value pair that is irrelevant here. Also,
// ::checkValue() may (counter to its docs) trigger an exception.
$this->configName = 'STRIP';
$this->schema = $typed_config->createFromNameAndData($configuration_name, $default_configuration);
$schema_errors = [];
foreach ($default_configuration as $key => $value) {
try {
$schema_error = $this->checkValue($key, $value);
}
catch (\InvalidArgumentException $e) {
$schema_error = [$key => $e->getMessage()];
}
$schema_errors = array_merge($schema_errors, $schema_error);
}
$formatted_schema_errors = [];
foreach ($schema_errors as $key => $value) {
$formatted_schema_errors[] = sprintf("[%s] %s", str_replace('STRIP:', '', $key), trim($value, '.'));
}
if (!empty($schema_errors)) {
throw new InvalidPluginDefinitionException($id, sprintf('The "%s" CKEditor 5 plugin definition is configurable, but its default configuration does not match its config schema. The following errors were found: %s.', $id, implode(', ', $formatted_schema_errors)));
$error_message = $this->validateConfiguration($default_configuration);
if ($error_message) {
throw new InvalidPluginDefinitionException($id, sprintf('The "%s" CKEditor 5 plugin definition is configurable, but its default configuration does not match its config schema. %s', $id, $error_message));
}
}
}
......@@ -204,6 +186,16 @@ private function validateDrupalAspects(string $id, array $definition): void {
'filter' => function ($value): ?string {
return is_string($value) ? NULL : 'A string corresponding to a filter plugin ID must be specified.';
},
'requiresConfiguration' => function ($required_configuration, array $definition): ?string {
if (!is_array($required_configuration)) {
return 'An array structure matching the required configuration for this plugin must be specified.';
}
if (!in_array(CKEditor5PluginConfigurableInterface::class, class_implements($definition['drupal']['class'], TRUE))) {
return 'This condition type is only available for CKEditor 5 plugins implementing CKEditor5PluginConfigurableInterface.';
}
$error_message = $this->validateConfiguration($required_configuration);
return is_string($error_message) ? sprintf('The required configuration does not match its config schema. %s', $error_message) : NULL;
},
'plugins' => function ($value): ?string {
return is_array($value) && Inspector::assertAllStrings($value) ? NULL : 'A list of strings, each corresponding to a CKEditor 5 plugin ID must be specified.';
},
......@@ -213,7 +205,7 @@ private function validateDrupalAspects(string $id, array $definition): void {
throw new InvalidPluginDefinitionException($id, sprintf('The "%s" CKEditor 5 plugin definition has a "drupal.conditions" value that contains some unsupported condition types: "%s". Only the following conditions types are supported: "%s".', $id, implode(', ', $unsupported_condition_types), implode('", "', array_keys($supported_condition_types))));
}
foreach ($definition['drupal']['conditions'] as $condition_type => $value) {
$assessment = $supported_condition_types[$condition_type]($value);
$assessment = $supported_condition_types[$condition_type]($value, $definition);
if (is_string($assessment)) {
throw new InvalidPluginDefinitionException($id, sprintf('The "%s" CKEditor 5 plugin definition has an invalid "drupal.conditions" item. "%s" is set to an invalid value. %s', $id, $condition_type, $assessment));
}
......@@ -228,6 +220,56 @@ private function validateDrupalAspects(string $id, array $definition): void {
}
}
/**
* Returns the typed configuration service.
*
* @return \Drupal\Core\Config\TypedConfigManagerInterface
* The typed configuration service.
*/
private function getTypedConfig(): TypedConfigManagerInterface {
return \Drupal::service('config.typed');
}
/**
* Validates the given configuration array.
*
* @param array $configuration
* The configuration to validate.
*
* @return string|null
* NULL if there are no validation errors, a string containing the schema
* violation error messages otherwise.
*/
private function validateConfiguration(array $configuration): ?string {
if (!isset($this->schema)) {
$configuration_name = sprintf("ckeditor5.plugin.%s", $this->id);
// TRICKY: SchemaCheckTrait::checkConfigSchema() dynamically adds a
// 'langcode' key-value pair that is irrelevant here. Also,
// ::checkValue() may (counter to its docs) trigger an exception.
$this->configName = 'STRIP';
$this->schema = $this->getTypedConfig()->createFromNameAndData($configuration_name, $configuration);
}
$schema_errors = [];
foreach ($configuration as $key => $value) {
try {
$schema_error = $this->checkValue($key, $value);
}
catch (\InvalidArgumentException $e) {
$schema_error = [$key => $e->getMessage()];
}
$schema_errors = array_merge($schema_errors, $schema_error);
}
$formatted_schema_errors = [];
foreach ($schema_errors as $key => $value) {
$formatted_schema_errors[] = sprintf("[%s] %s", str_replace('STRIP:', '', $key), trim($value, '.'));
}
if (!empty($formatted_schema_errors)) {
return sprintf('The following errors were found: %s.', implode(', ', $formatted_schema_errors));
}
return NULL;
}
/**
* {@inheritdoc}
*
......
......@@ -380,6 +380,10 @@ protected function isPluginDisabled(CKEditor5PluginInterface $plugin, EditorInte
}
break;
case 'requiresConfiguration':
$intersection = array_intersect($plugin->getConfiguration(), $required_value);
return $intersection !== $required_value;
case 'plugins':
// Tricky: this cannot yet be evaluated here. It will evaluated later.
// @see \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::getEnabledDefinitions()
......
......@@ -384,12 +384,19 @@ private function shouldHaveVisiblePluginSettingsForm(CKEditor5PluginDefinition $
// due to isEnabled() returning false, that should still have its config
// form provided:
// 1 - A conditionally enabled plugin that does not depend on a toolbar item
// to be active.
// to be active AND the plugins it depends on are enabled.
// 2 - A conditionally enabled plugin that does depend on a toolbar item,
// and that toolbar item is active.
if ($definition->hasConditions()) {
$conditions = $definition->getConditions();
if (!array_key_exists('toolbarItem', $conditions)) {
// The CKEditor 5 plugins this plugin depends on must be enabled.
if (array_key_exists('plugins', $conditions)) {
$all_plugins = $this->ckeditor5PluginManager->getDefinitions();
$dependencies = array_intersect_key($all_plugins, array_flip($conditions['plugins']));
$unmet_dependencies = array_diff_key($dependencies, $enabled_plugins);
return empty($unmet_dependencies);
}
return TRUE;
}
elseif (in_array($conditions['toolbarItem'], $editor->getSettings()['toolbar']['items'], TRUE)) {
......@@ -681,11 +688,19 @@ protected function getEventualEditorWithPrimedFilterFormat(SubformStateInterface
$pair = static::createEphemeralPairedEditor($submitted_editor, $submitted_filter_format);
// When CKEditor 5 plugins are disabled in the form-based admin UI, the
// associated settings (if any) should be omitted too.
// associated settings (if any) should be omitted too, except for plugins
// that are enabled using `requiresConfiguration` (because whether they are
// enabled or not depends on the associated settings).
$original_settings = $pair->getSettings();
$enabled_plugins = $this->ckeditor5PluginManager->getEnabledDefinitions($pair);
$config_enabled_plugins = [];
foreach ($this->ckeditor5PluginManager->getDefinitions() as $id => $definition) {
if ($definition->hasConditions() && isset($definition->getConditions()['requiresConfiguration'])) {
$config_enabled_plugins[$id] = TRUE;
}
}
$updated_settings = [
'plugins' => array_intersect_key($original_settings['plugins'], $enabled_plugins),
'plugins' => array_intersect_key($original_settings['plugins'], $enabled_plugins + $config_enabled_plugins),
] + $original_settings;
$pair->setSettings($updated_settings);
......
......@@ -85,7 +85,7 @@ public function testAttributeEncoding() {
'toolbar' => [
'items' => ['uploadImage'],
],
'plugins' => [],
'plugins' => ['ckeditor5_imageResize' => ['allow_resize' => FALSE]],
],
'image_upload' => [
'status' => TRUE,
......@@ -109,7 +109,7 @@ function (ConstraintViolation $v) {
$this->assertNotEmpty($image_upload_field = $page->find('css', '.ck-file-dialog-button input[type="file"]'));
$image = $this->getTestFiles('image')[0];
$image_upload_field->attachFile($this->container->get('file_system')->realpath($image->uri));
$assert_session->assertWaitOnAjaxRequest();
$assert_session->waitForElementVisible('css', '.ck-widget.image');
$this->click('.ck-widget.image');
$balloon_panel = $page->find('css', '.ck-balloon-panel');
......
......@@ -5,18 +5,19 @@
use Drupal\editor\Entity\Editor;
use Drupal\file\Entity\File;
use Drupal\filter\Entity\FilterFormat;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\TestFileCreationTrait;
use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait;
use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
use Symfony\Component\Validator\ConstraintViolation;
// cspell:ignore imageresize imageupload
/**
* @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\ImageUpload
* @group ckeditor5
* @internal
*/
class ImageTest extends WebDriverTestBase {
class ImageTest extends CKEditor5TestBase {
use CKEditor5TestTrait;
use TestFileCreationTrait;
......@@ -91,6 +92,9 @@ protected function setUp(): void {
'ckeditor5_sourceEditing' => [
'allowed_tags' => [],
],
'ckeditor5_imageResize' => [
'allow_resize' => TRUE,
],
],
],
'image_upload' => [
......@@ -113,6 +117,7 @@ function (ConstraintViolation $v) {
$this->adminUser = $this->drupalCreateUser([
'use text format test_format',
'bypass node access',
'administer filters',
]);
// Create a sample host entity to embed images in.
......@@ -120,9 +125,8 @@ function (ConstraintViolation $v) {
'uri' => $this->getTestFiles('image')[0]->uri,
]);
$this->file->save();
$this->drupalCreateContentType(['type' => 'blog']);
$this->host = $this->createNode([
'type' => 'blog',
'type' => 'page',
'title' => 'Animals with strange names',
'body' => [
'value' => '<p>The pirate is irate.</p>',
......@@ -327,7 +331,6 @@ public function testWidth(string $width): void {
$this->assertNotEmpty($image_upload_field = $page->find('css', '.ck-file-dialog-button input[type="file"]'));
$image = $this->getTestFiles('image')[0];
$image_upload_field->attachFile($this->container->get('file_system')->realpath($image->uri));
$assert_session->assertWaitOnAjaxRequest();
$this->assertNotEmpty($assert_session->waitForElementVisible('css', 'figure.image'));
// Edit the source of the image through the UI.
......@@ -370,4 +373,87 @@ public function providerWidth(): array {
];
}
/**
* Tests the image resize plugin.
*
* Confirms that enabling the resize plugin introduces the resize class to
* images within CKEditor 5.
*
* @param bool $is_resize_enabled
* Boolean flag to test enabled or disabled.
*
* @dataProvider providerResize
*/
public function testResize(bool $is_resize_enabled): void {
// Disable resize plugin because it is enabled by default.
if (!$is_resize_enabled) {
Editor::load('test_format')->setSettings([
'toolbar' => [
'items' => [
'uploadImage',
],
],
'plugins' => [
'ckeditor5_imageResize' => [
'allow_resize' => FALSE,
],
],
])->save();
}
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
$this->drupalGet('node/add');
$page->fillField('title[0][value]', 'My test content');
$this->assertNotEmpty($image_upload_field = $page->find('css', '.ck-file-dialog-button input[type="file"]'));
$image = $this->getTestFiles('image')[0];
$image_upload_field->attachFile($this->container->get('file_system')->realpath($image->uri));
$image_figure = $assert_session->waitForElementVisible('css', 'figure');
$this->assertSame($is_resize_enabled, $image_figure->hasClass('ck-widget_with-resizer'));
}
/**
* Data provider for ::testResize().
*
* @return array
* The test cases.
*/
public function providerResize(): array {
return [
'Image resize is enabled' => [
'is_resize_enabled' => TRUE,
],
'Image resize is disabled' => [
'is_resize_enabled' => FALSE,
],
];
}
/**
* Tests the ckeditor5_imageResize and ckeditor5_imageUpload settings forms.
*/
public function testImageSettingsForm() {
$assert_session = $this->assertSession();
$this->drupalGet('admin/config/content/formats/manage/test_format');
// The image resize and upload plugin settings forms should be present.
$assert_session->elementExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-imageresize"]');
$assert_session->elementExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-imageupload"]');
// Removing the imageUpload button from the toolbar must remove the plugin
// settings forms too.
$this->triggerKeyUp('.ckeditor5-toolbar-item-uploadImage', 'ArrowUp');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->elementNotExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-imageresize"]');
$assert_session->elementNotExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-imageupload"]');
// Re-adding the imageUpload button to the toolbar must re-add the plugin
// settings forms too.
$this->triggerKeyUp('.ckeditor5-toolbar-item-uploadImage', 'ArrowDown');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->elementExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-imageresize"]');
$assert_session->elementExists('css', '[data-drupal-selector="edit-editor-settings-plugins-ckeditor5-imageupload"]');
}
}
......@@ -352,7 +352,7 @@ public function providerTestInvalidPluginDefinitions(): \Generator {
conditions:
foo: bar
YAML,
'The "ckeditor5_invalid_plugin_foo_bar" CKEditor 5 plugin definition has a "drupal.conditions" value that contains some unsupported condition types: "foo". Only the following conditions types are supported: "toolbarItem", "imageUploadStatus", "filter", "plugins".',
'The "ckeditor5_invalid_plugin_foo_bar" CKEditor 5 plugin definition has a "drupal.conditions" value that contains some unsupported condition types: "foo". Only the following conditions types are supported: "toolbarItem", "imageUploadStatus", "filter", "requiresConfiguration", "plugins".',
];
yield 'invalid condition: toolbarItem' => [
<<<YAML
......@@ -833,6 +833,139 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {}
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {}
}
PHP,
],
],
],
],
];
yield 'invalid condition: requiresConfiguration not specifying a configuration array' => [
<<<YAML
ckeditor5_invalid_plugin_foo_bar:
ckeditor5:
plugins: {}
drupal:
label: "Foo bar"
elements: false
conditions:
requiresConfiguration: true
YAML,
'The "ckeditor5_invalid_plugin_foo_bar" CKEditor 5 plugin definition has an invalid "drupal.conditions" item. "requiresConfiguration" is set to an invalid value. An array structure matching the required configuration for this plugin must be specified.',
];
yield 'invalid condition: requiresConfiguration without configurable plugin' => [
<<<YAML
ckeditor5_invalid_plugin_foo_bar:
ckeditor5:
plugins: {}
drupal:
label: "Foo bar"
elements: false
conditions:
requiresConfiguration:
allow_resize: true
YAML,
'The "ckeditor5_invalid_plugin_foo_bar" CKEditor 5 plugin definition has an invalid "drupal.conditions" item. "requiresConfiguration" is set to an invalid value. This condition type is only available for CKEditor 5 plugins implementing CKEditor5PluginConfigurableInterface.',
];
yield 'invalid condition: requiresConfiguration with configurable plugin but required configuration does not match config schema' => [
<<<YAML
ckeditor5_invalid_plugin_foo_bar:
ckeditor5:
plugins: {}
drupal:
class: Drupal\ckeditor5_invalid_plugin\Plugin\CKEditor5Plugin\FooBar
label: "Foo bar"
elements: false
conditions:
requiresConfiguration:
allow_resize: true
YAML,
'The "ckeditor5_invalid_plugin_foo_bar" CKEditor 5 plugin definition has an invalid "drupal.conditions" item. "requiresConfiguration" is set to an invalid value. The required configuration does not match its config schema. The following errors were found: [allow_resize] The configuration property allow_resize doesn\'t exist.',
[
'config' => [
'schema' => [
'ckeditor5_invalid_plugin.schema.yml' => <<<YAML
ckeditor5.plugin.ckeditor5_invalid_plugin_foo_bar:
type: mapping
label: 'Foo Bar'
mapping:
foo:
type: boolean
label: 'Foo'
YAML,
],
],
'src' => [
'Plugin' => [
'CKEditor5Plugin' => [
'FooBar.php' => <<<'PHP'
<?php
namespace Drupal\ckeditor5_invalid_plugin\Plugin\CKEditor5Plugin;
use Drupal\ckeditor5\Plugin\CKEditor5PluginDefault;
use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableInterface;
use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableTrait;
use Drupal\Core\Form\FormStateInterface;
class FooBar extends CKEditor5PluginDefault implements CKEditor5PluginConfigurableInterface {
use CKEditor5PluginConfigurableTrait;
public function defaultConfiguration() { return ['foo' => FALSE]; }
public function buildConfigurationForm(array $form, FormStateInterface $form_state) { return []; }
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {}
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {}
}
PHP,
],
],
],
],
];
yield 'valid condition: requiresConfiguration' => [