From e1fc76a1d2d9ff925bdbb7d4440f15911c03c915 Mon Sep 17 00:00:00 2001 From: Lauri Eskola <lauri.eskola@acquia.com> Date: Thu, 28 Sep 2023 12:00:06 +0300 Subject: [PATCH] Issue #3273986 by ifrik, xurizaemon, rpayanm, Wim Leers, Charles Belov, jonathan_hunt: Third option for the CKEditor 5 "Language" button: site_configured (in addition to un and all) --- .../config/schema/ckeditor5.schema.yml | 6 +- .../src/Plugin/CKEditor5Plugin/Language.php | 98 ++++++++++++++++--- .../FunctionalJavascript/CKEditor5Test.php | 86 +++++++++++----- .../tests/src/Unit/LanguagePluginTest.php | 43 +++++++- 4 files changed, 187 insertions(+), 46 deletions(-) diff --git a/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml b/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml index 20c31399c4af..52a9e1afbddf 100644 --- a/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml +++ b/core/modules/ckeditor5/config/schema/ckeditor5.schema.yml @@ -41,10 +41,14 @@ ckeditor5.plugin.ckeditor5_language: # Configuring this does not make sense without the corresponding button. CKEditor5ToolbarItemDependencyConstraint: toolbarItem: textPartLanguage - # Only two possible values are accepted. + # Only the following values are accepted. Choice: + # United Nations "official languages". - un + # Drupal's predefined language list. - all + # Languages configured at /admin/config/regional/language for the site. + - site_configured # Plugin \Drupal\ckeditor5\Plugin\CKEditor5Plugin\Heading ckeditor5.plugin.ckeditor5_heading: diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/Language.php b/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/Language.php index ccbeb9884f91..3e0ac2a968aa 100644 --- a/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/Language.php +++ b/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/Language.php @@ -4,13 +4,19 @@ namespace Drupal\ckeditor5\Plugin\CKEditor5Plugin; +use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableInterface; use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableTrait; use Drupal\ckeditor5\Plugin\CKEditor5PluginDefault; -use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableInterface; +use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Language\LanguageManager; use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Language\LanguageManager; +use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Routing\RouteProviderInterface; +use Drupal\Core\Url; use Drupal\editor\EditorInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * CKEditor 5 Language plugin. @@ -18,23 +24,72 @@ * @internal * Plugin classes are internal. */ -class Language extends CKEditor5PluginDefault implements CKEditor5PluginConfigurableInterface { +class Language extends CKEditor5PluginDefault implements CKEditor5PluginConfigurableInterface, ContainerFactoryPluginInterface { use CKEditor5PluginConfigurableTrait; + /** + * Language constructor. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param \Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Language\LanguageManagerInterface $languageManager + * The language manager. + * @param \Drupal\Core\Routing\RouteProviderInterface $routeProvider + * The route provider. + */ + public function __construct(array $configuration, string $plugin_id, CKEditor5PluginDefinition $plugin_definition, protected LanguageManagerInterface $languageManager, protected RouteProviderInterface $routeProvider) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('language_manager'), + $container->get('router.route_provider'), + ); + } + /** * {@inheritdoc} */ public function getDynamicPluginConfig(array $static_plugin_config, EditorInterface $editor): array { - $predefined_languages = $this->configuration['language_list'] === 'all' ? - LanguageManager::getStandardLanguageList() : - LanguageManager::getUnitedNationsLanguageList(); + $languages = NULL; + switch ($this->configuration['language_list']) { + case 'site_configured': + $configured_languages = $this->languageManager->getLanguages(); + $languages = []; + foreach ($configured_languages as $language) { + $languages[$language->getId()] = [ + $language->getName(), + '', + $language->getDirection(), + ]; + } + break; + + case 'all': + $languages = LanguageManager::getStandardLanguageList(); + break; + + case 'un': + $languages = LanguageManager::getUnitedNationsLanguageList(); + } // Generate the language_list setting as expected by the CKEditor Language // plugin, but key the values by the full language name so that we can sort // them later on. $language_list = []; - foreach ($predefined_languages as $langcode => $language) { + foreach ($languages as $langcode => $language) { $english_name = $language[0]; $direction = empty($language[2]) ? NULL : $language[2]; $language_list[$english_name] = [ @@ -60,20 +115,35 @@ public function getDynamicPluginConfig(array $static_plugin_config, EditorInterf * @see editor_image_upload_settings_form() */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { - $predefined_languages = LanguageManager::getStandardLanguageList(); + $configured = count($this->languageManager->getLanguages()); + $predefined = count(LanguageManager::getStandardLanguageList()); + $united_nations = count(LanguageManager::getUnitedNationsLanguageList()); + + $language_list_description_args = [ + ':united-nations-official' => 'https://www.un.org/en/sections/about-un/official-languages', + '@count_predefined' => $predefined, + '@count_united_nations' => $united_nations, + '@count_configured' => $configured, + ]; + // If Language is enabled, link to the configuration route. + if ($this->routeProvider->getRoutesByNames(['entity.configurable_language.collection'])) { + $language_list_description = $this->t('The list of languages in the CKEditor "Language" dropdown can present the <a href=":united-nations-official">@count_united_nations official languages of the UN</a>, all @count_predefined languages predefined in Drupal, or the <a href=":admin-configure-languages">@count_configured languages configured for this site</a>.', $language_list_description_args + [':admin-configure-languages' => Url::fromRoute('entity.configurable_language.collection')->toString()]); + } + else { + $language_list_description = $this->t('The list of languages in the CKEditor "Language" dropdown can present the <a href=":united-nations-official">@count_united_nations official languages of the UN</a>, all @count_predefined languages predefined in Drupal, or the languages configured for this site.', $language_list_description_args); + } + $form['language_list'] = [ '#title' => $this->t('Language list'), '#title_display' => 'invisible', '#type' => 'select', '#options' => [ - 'un' => $this->t("United Nations' official languages"), - 'all' => $this->t('All @count languages', ['@count' => count($predefined_languages)]), + 'un' => $this->t("United Nations' official languages (@count)", ['@count' => $united_nations]), + 'all' => $this->t('Drupal predefined languages (@count)', ['@count' => $predefined]), + 'site_configured' => $this->t("Site-configured languages (@count)", ['@count' => $configured]), ], '#default_value' => $this->configuration['language_list'], - '#description' => $this->t('The list of languages to show in the language dropdown. The basic list will only show the <a href=":url">six official languages of the UN</a>. The extended list will show all @count languages that are available in Drupal.', [ - ':url' => 'https://www.un.org/en/sections/about-un/official-languages', - '@count' => count($predefined_languages), - ]), + '#description' => $language_list_description, ]; return $form; diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php index 61b337f1fb6a..aa85bc1766c1 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php @@ -7,13 +7,14 @@ use Drupal\editor\Entity\Editor; use Drupal\file\Entity\File; use Drupal\filter\Entity\FilterFormat; +use Drupal\language\Entity\ConfigurableLanguage; use Drupal\node\Entity\Node; use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait; use Drupal\Tests\TestFileCreationTrait; use Drupal\user\RoleInterface; use Symfony\Component\Validator\ConstraintViolation; -// cspell:ignore esque splitbutton upcasted sourceediting +// cspell:ignore esque mÄori sourceediting splitbutton upcasted /** * Tests for CKEditor 5. @@ -31,6 +32,7 @@ class CKEditor5Test extends CKEditor5TestBase { */ protected static $modules = [ 'media_library', + 'language', ]; /** @@ -41,7 +43,7 @@ public function testExistingContent() { $assert_session = $this->assertSession(); // Add a node with text rendered via the Plain Text format. - $this->drupalGet('node/add'); + $this->drupalGet('node/add/page'); $page->fillField('title[0][value]', 'My test content'); $page->fillField('body[0][value]', '<p>This is test content</p>'); $page->pressButton('Save'); @@ -103,7 +105,7 @@ function (ConstraintViolation $v) { )) )); - $this->drupalGet('node/add'); + $this->drupalGet('node/add/page'); $this->waitForEditor(); $page->fillField('title[0][value]', 'My test content'); @@ -147,7 +149,7 @@ public function testHeadingsPlugin() { $this->drupalGet('admin/config/content/formats/manage/ckeditor5'); $this->assertHtmlEsqueFieldValueEquals('filters[filter_html][settings][allowed_html]', '<br> <p> <h2> <h3> <h4> <h5> <h6> <strong> <em>'); - $this->drupalGet('node/add'); + $this->drupalGet('node/add/page'); $this->assertNotEmpty($assert_session->waitForElement('css', '.ck-heading-dropdown button')); $page->find('css', '.ck-heading-dropdown button')->click(); @@ -188,7 +190,7 @@ public function testHeadingsPlugin() { $page->pressButton('Save configuration'); - $this->drupalGet('node/add'); + $this->drupalGet('node/add/page'); $this->assertNotEmpty($assert_session->waitForElement('css', '.ck-heading-dropdown button')); $page->find('css', '.ck-heading-dropdown button')->click(); @@ -212,12 +214,43 @@ public function testHeadingsPlugin() { } /** - * Test for plugin Language of parts. + * Test for Language of Parts plugin. */ public function testLanguageOfPartsPlugin() { $page = $this->getSession()->getPage(); $assert_session = $this->assertSession(); + $this->languageOfPartsPluginInitialConfigurationHelper($page, $assert_session); + + // Test for "United Nations' official languages" option. + $languages = LanguageManager::getUnitedNationsLanguageList(); + $this->languageOfPartsPluginConfigureLanguageListHelper($page, $assert_session, 'un'); + $this->languageOfPartsPluginTestHelper($page, $assert_session, $languages); + + // Test for "Drupal predefined languages" option. + $languages = LanguageManager::getStandardLanguageList(); + $this->languageOfPartsPluginConfigureLanguageListHelper($page, $assert_session, 'all'); + $this->languageOfPartsPluginTestHelper($page, $assert_session, $languages); + + // Test for "Site-configured languages" option. + ConfigurableLanguage::createFromLangcode('ar')->save(); + ConfigurableLanguage::createFromLangcode('fr')->save(); + ConfigurableLanguage::createFromLangcode('mi')->setName('MÄori')->save(); + $configured_languages = \Drupal::languageManager()->getLanguages(); + $languages = []; + foreach ($configured_languages as $language) { + $language_name = $language->getName(); + $language_code = $language->getId(); + $languages[$language_code] = [$language_name]; + } + $this->languageOfPartsPluginConfigureLanguageListHelper($page, $assert_session, 'site_configured'); + $this->languageOfPartsPluginTestHelper($page, $assert_session, $languages); + } + + /** + * Helper to configure CKEditor5 with Language plugin. + */ + public function languageOfPartsPluginInitialConfigurationHelper($page, $assert_session) { $this->createNewTextFormat($page, $assert_session); // Press arrow down key to add the button to the active toolbar. $this->assertNotEmpty($assert_session->waitForElement('css', '.ckeditor5-toolbar-item-textPartLanguage')); @@ -248,21 +281,15 @@ public function testLanguageOfPartsPlugin() { // Confirm there are no longer any warnings. $assert_session->waitForElementRemoved('css', '[data-drupal-messages] [role="alert"]'); - - // Test for "United Nations' official languages" option. - $languages = LanguageManager::getUnitedNationsLanguageList(); - $this->languageOfPartsPluginTestHelper($page, $assert_session, $languages, "un"); - - // Test for "All 95 languages" option. - $this->drupalGet('admin/config/content/formats/manage/ckeditor5'); - $languages = LanguageManager::getStandardLanguageList(); - $this->languageOfPartsPluginTestHelper($page, $assert_session, $languages, "all"); + $page->pressButton('Save configuration'); + $assert_session->responseContains('Added text format <em class="placeholder">ckeditor5</em>.'); } /** - * Validate the available languages on the basis of selected language option. + * Helper to set language list option for CKEditor. */ - public function languageOfPartsPluginTestHelper($page, $assert_session, $predefined_languages, $option) { + public function languageOfPartsPluginConfigureLanguageListHelper($page, $assert_session, $option) { + $this->drupalGet('admin/config/content/formats/manage/ckeditor5'); $this->assertNotEmpty($assert_session->waitForElement('css', 'a[href^="#edit-editor-settings-plugins-ckeditor5-language"]')); // Set correct value. @@ -271,9 +298,14 @@ public function languageOfPartsPluginTestHelper($page, $assert_session, $predefi $page->selectFieldOption('editor[settings][plugins][ckeditor5_language][language_list]', $option); $assert_session->assertWaitOnAjaxRequest(); $page->pressButton('Save configuration'); + $assert_session->responseContains('The text format <em class="placeholder">ckeditor5</em> has been updated.'); + } - // Validate plugin on node add page. - $this->drupalGet('node/add'); + /** + * Validate expected languages available in editor. + */ + public function languageOfPartsPluginTestHelper($page, $assert_session, $configured_languages) { + $this->drupalGet('node/add/page'); $this->assertNotEmpty($assert_session->waitForText('Choose language')); // Click on the dropdown button. @@ -290,13 +322,13 @@ public function languageOfPartsPluginTestHelper($page, $assert_session, $predefi foreach ($current_languages as $item) { $languages[] = $item->getText(); } + // Return the values from a single column. - $predefined_languages = array_column($predefined_languages, 0); + $configured_languages = array_column($configured_languages, 0); // Sort on full language name. - asort($predefined_languages); - - $this->assertSame(array_values($predefined_languages), $languages); + asort($configured_languages); + $this->assertSame(array_values($configured_languages), $languages); } /** @@ -518,7 +550,7 @@ public function testEditorFileReferenceIntegration() { $assert_session->assertWaitOnAjaxRequest(); $this->saveNewTextFormat($page, $assert_session); - $this->drupalGet('node/add'); + $this->drupalGet('node/add/page'); $page->fillField('title[0][value]', 'My test content'); // Ensure that CKEditor 5 is focused. @@ -566,7 +598,7 @@ public function testEmphasis() { $assert_session = $this->assertSession(); // Add a node with text rendered via the Plain Text format. - $this->drupalGet('node/add'); + $this->drupalGet('node/add/page'); $page->fillField('title[0][value]', 'My test content'); $page->fillField('body[0][value]', '<p>This is a <em>test!</em></p>'); $page->pressButton('Save'); @@ -624,7 +656,7 @@ function (ConstraintViolation $v) { $ordered_list_html = '<ol><li>apple</li><li>banana</li><li>cantaloupe</li></ol>'; $page = $this->getSession()->getPage(); $assert_session = $this->assertSession(); - $this->drupalGet('node/add'); + $this->drupalGet('node/add/page'); $page->fillField('title[0][value]', 'My test content'); $this->pressEditorButton('Source'); $source_text_area = $assert_session->waitForElement('css', '.ck-source-editing-area textarea'); @@ -690,7 +722,7 @@ public function testFilterHtmlAllowedGlobalAttributes(): void { $assert_session = $this->assertSession(); // Add a node with text rendered via the Plain Text format. - $this->drupalGet('node/add'); + $this->drupalGet('node/add/page'); $page->fillField('title[0][value]', 'Multilingual Hello World'); // cSpell:disable-next-line $page->fillField('body[0][value]', '<p dir="ltr" lang="en">Hello World</p><p dir="rtl" lang="ar">Ù…Ø±ØØ¨Ø§ بالعالم</p>'); diff --git a/core/modules/ckeditor5/tests/src/Unit/LanguagePluginTest.php b/core/modules/ckeditor5/tests/src/Unit/LanguagePluginTest.php index 395004ecec5d..de73ca706f09 100644 --- a/core/modules/ckeditor5/tests/src/Unit/LanguagePluginTest.php +++ b/core/modules/ckeditor5/tests/src/Unit/LanguagePluginTest.php @@ -5,7 +5,11 @@ namespace Drupal\Tests\ckeditor5\Unit; use Drupal\ckeditor5\Plugin\CKEditor5Plugin\Language; +use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition; +use Drupal\Core\Language\Language as LanguageLanguage; use Drupal\Core\Language\LanguageManager; +use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\Routing\RouteProviderInterface; use Drupal\editor\EditorInterface; use Drupal\Tests\UnitTestCase; @@ -20,7 +24,7 @@ class LanguagePluginTest extends UnitTestCase { * Provides a list of configs to test. */ public static function providerGetDynamicPluginConfig(): array { - $un_expected_output = [ + $united_nations_expected_output = [ 'language' => [ 'textPartLanguage' => [ [ @@ -54,7 +58,25 @@ public static function providerGetDynamicPluginConfig(): array { return [ 'un' => [ ['language_list' => 'un'], - $un_expected_output, + $united_nations_expected_output, + ], + 'site_configured' => [ + ['language_list' => 'site_configured'], + [ + 'language' => [ + 'textPartLanguage' => [ + [ + 'title' => 'Arabic', + 'languageCode' => 'ar', + 'textDirection' => 'rtl', + ], + [ + 'title' => 'German', + 'languageCode' => 'de', + ], + ], + ], + ], ], 'all' => [ ['language_list' => 'all'], @@ -66,7 +88,7 @@ public static function providerGetDynamicPluginConfig(): array { ], 'default configuration' => [ [], - $un_expected_output, + $united_nations_expected_output, ], ]; } @@ -101,7 +123,20 @@ protected static function buildExpectedDynamicConfig(array $language_list) { * @dataProvider providerGetDynamicPluginConfig */ public function testGetDynamicPluginConfig(array $configuration, array $expected_dynamic_config): void { - $plugin = new Language($configuration, 'ckeditor5_language', NULL); + $route_provider = $this->prophesize(RouteProviderInterface::class); + $language_manager = $this->prophesize(LanguageManagerInterface::class); + $language_manager->getLanguages()->willReturn([ + new LanguageLanguage([ + 'id' => 'de', + 'name' => 'German', + ]), + new LanguageLanguage([ + 'id' => 'ar', + 'name' => 'Arabic', + 'direction' => 'rtl', + ]), + ]); + $plugin = new Language($configuration, 'ckeditor5_language', new CKEditor5PluginDefinition(['id' => 'IRRELEVANT-FOR-A-UNIT-TEST']), $language_manager->reveal(), $route_provider->reveal()); $dynamic_config = $plugin->getDynamicPluginConfig([], $this->prophesize(EditorInterface::class) ->reveal()); $this->assertSame($expected_dynamic_config, $dynamic_config); -- GitLab