From 15357ee8fb4cf18dcbab75377e77c542bc7e1b49 Mon Sep 17 00:00:00 2001 From: Sergii Pavlenko Date: Fri, 23 Sep 2022 10:49:10 +0300 Subject: [PATCH 01/10] EWPP-2557: Fix phpunit yml config for selenium 4. --- phpunit.xml.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 580fdc812..77f5faceb 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -7,7 +7,7 @@ - + From ac0f12ae2a46fc3a725d1932271209221b46776b Mon Sep 17 00:00:00 2001 From: Sergii Pavlenko Date: Fri, 23 Sep 2022 10:52:37 +0300 Subject: [PATCH 02/10] EWPP-2557: Add new ckeditor plugin for zebra striping option. --- .../js/table_zebra_striping.js | 45 ++++++++++++ .../CKEditorPlugin/TableZebraStriping.php | 70 +++++++++++++++++++ .../src/Plugin/Filter/FilterEclTable.php | 7 ++ 3 files changed, 122 insertions(+) create mode 100644 modules/oe_theme_helper/js/table_zebra_striping.js create mode 100755 modules/oe_theme_helper/src/Plugin/CKEditorPlugin/TableZebraStriping.php diff --git a/modules/oe_theme_helper/js/table_zebra_striping.js b/modules/oe_theme_helper/js/table_zebra_striping.js new file mode 100644 index 000000000..9762f5da3 --- /dev/null +++ b/modules/oe_theme_helper/js/table_zebra_striping.js @@ -0,0 +1,45 @@ +/** + * @file + * Plugin for adding zebra striping of table. + */ + +(function ($, CKEDITOR) { + "use strict"; + + CKEDITOR.plugins.add('table_zebra_striping', { + afterInit: function afterInit (editor) { + CKEDITOR.on('dialogDefinition', function(event) { + const dialog_name = event.data.name; + + if (dialog_name !== 'table' && dialog_name !== 'tableProperties') { + return; + } + + const dialog_definition = event.data.definition; + const info_tab = dialog_definition.getContents('info'); + const zebra_attribute = 'data-striped'; + // Avoid multiple checkbox adding. + if (!info_tab.get('zebraStriping')) { + info_tab.add({ + type: 'checkbox', + label: event.editor.config.zebra_striping__checkboxLabel, + id: 'zebraStriping', + labelStyle: 'display: inline;', + requiredContent: 'table[' + zebra_attribute + ']', + setup: function (selectedTable) { + this.setValue(selectedTable.getAttribute(zebra_attribute)); + }, + commit: function (data, selectedTable) { + if (this.getValue()) { + selectedTable.setAttribute(zebra_attribute, true); + } else { + selectedTable.removeAttribute(zebra_attribute); + } + } + }); + } + }); + } + }); + +})(jQuery, CKEDITOR); diff --git a/modules/oe_theme_helper/src/Plugin/CKEditorPlugin/TableZebraStriping.php b/modules/oe_theme_helper/src/Plugin/CKEditorPlugin/TableZebraStriping.php new file mode 100755 index 000000000..ab10b5b9a --- /dev/null +++ b/modules/oe_theme_helper/src/Plugin/CKEditorPlugin/TableZebraStriping.php @@ -0,0 +1,70 @@ +getModulePath('oe_theme_helper') . '/js/table_zebra_striping.js'; + } + + /** + * {@inheritdoc} + */ + public function getConfig(Editor $editor) { + return [ + 'zebra_striping__checkboxLabel' => $this->t('Zebra striping'), + ]; + } + + /** + * {@inheritdoc} + */ + public function getButtons() { + return []; + } + + /** + * {@inheritdoc} + */ + public function isEnabled(Editor $editor) { + if (!$editor->hasAssociatedFilterFormat()) { + return FALSE; + } + + $enabled = FALSE; + $format = $editor->getFilterFormat(); + if ($format->filters('filter_ecl_table')->status) { + $settings = $editor->getSettings(); + foreach ($settings['toolbar']['rows'] as $row) { + foreach ($row as $group) { + foreach ($group['items'] as $button) { + if ($button === 'Table') { + $enabled = TRUE; + } + } + } + } + } + + return $enabled; + } + +} diff --git a/modules/oe_theme_helper/src/Plugin/Filter/FilterEclTable.php b/modules/oe_theme_helper/src/Plugin/Filter/FilterEclTable.php index 472dbe9aa..a7806dc7a 100644 --- a/modules/oe_theme_helper/src/Plugin/Filter/FilterEclTable.php +++ b/modules/oe_theme_helper/src/Plugin/Filter/FilterEclTable.php @@ -24,6 +24,7 @@ class FilterEclTable extends FilterBase { * {@inheritdoc} * * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function process($text, $langcode) { $result = new FilterProcessResult($text); @@ -76,6 +77,12 @@ public function process($text, $langcode) { } } + foreach ($xpath->query('//table[@data-striped="true"]') as $table) { + $classes = $table->getAttribute('class'); + $table->setAttribute('class', ltrim($classes . ' ecl-table ecl-table--zebra')); + $table->removeAttribute('data-striped'); + } + $result->setProcessedText(Html::serialize($dom)); return $result; From c0b241af9f0a189db4753a171bff28fa8fb9904a Mon Sep 17 00:00:00 2001 From: Sergii Pavlenko Date: Fri, 23 Sep 2022 10:53:40 +0300 Subject: [PATCH 03/10] EWPP-2557: Add test coverage for zebra striping plugin. --- .../FunctionalJavascript/WysiwygTableTest.php | 190 ++++++++++++++++++ .../tests/src/Unit/FilterEclTableTest.php | 4 + 2 files changed, 194 insertions(+) create mode 100644 modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php diff --git a/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php b/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php new file mode 100644 index 000000000..0c6d4979c --- /dev/null +++ b/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php @@ -0,0 +1,190 @@ +container->get('theme_installer')->install(['oe_theme', 'seven']); + $this->config('system.theme')->set('default', 'oe_theme')->save(); + $this->config('system.theme')->set('admin', 'seven')->save(); + $this->config('node.settings')->set('use_admin_theme', TRUE)->save(); + $this->container->get('router.builder')->rebuild(); + + // Rebuild the ui_pattern definitions to collect the ones provided by + // oe_theme itself. + \Drupal::service('plugin.manager.ui_patterns')->clearCachedDefinitions(); + + // Create a text format and associate this with CKEditor. + FilterFormat::create([ + 'format' => 'full_html', + 'name' => 'Full HTML', + 'weight' => 1, + 'roles' => [RoleInterface::AUTHENTICATED_ID], + 'filters' => [ + 'filter_ecl_table' => [ + 'status' => 1, + ], + ], + ])->save(); + + Editor::create([ + 'format' => 'full_html', + 'editor' => 'ckeditor', + 'settings' => [ + 'toolbar' => [ + 'rows' => [ + 0 => [ + 0 => [ + 'name' => 'Group with table', + 'items' => [ + 'Table', + 'Source', + ], + ], + ], + ], + ], + ], + ])->save(); + + $this->webUser = $this->drupalCreateUser([ + 'access administration pages', + 'create oe_page content', + 'edit own oe_page content', + 'view the administration theme', + ]); + } + + /** + * Test table widget in WYSIWYG. + */ + public function testWysiwygTable(): void { + $web_assert = $this->assertSession(); + $page = $this->getSession()->getPage(); + $this->drupalLogin($this->webUser); + $this->drupalGet('node/add/oe_page'); + $this->waitOnCkeditorInstance('edit-body-0-value'); + $this->pressEditorButton('table'); + $this->assertNotEmpty($web_assert->waitForElement('css', '.cke_editor_edit-body-0-value_dialog')); + $web_assert->elementContains('css', '.cke_editor_edit-body-0-value_dialog .cke_dialog_ui_checkbox', 'Zebra striping'); + + FilterFormat::load('full_html') + ->setFilterConfig('filter_ecl_table', ['status' => FALSE]) + ->save(); + $this->getSession()->reload(); + $this->waitOnCkeditorInstance('edit-body-0-value'); + $this->pressEditorButton('table'); + $this->assertNotEmpty($web_assert->waitForElement('css', '.cke_editor_edit-body-0-value_dialog')); + $web_assert->elementNotExists('css', '.cke_editor_edit-body-0-value_dialog .cke_dialog_ui_checkbox'); + + FilterFormat::load('full_html') + ->setFilterConfig('filter_ecl_table', ['status' => TRUE]) + ->save(); + $this->getSession()->reload(); + $this->waitOnCkeditorInstance('edit-body-0-value'); + $this->pressEditorButton('table'); + $this->assertNotEmpty($web_assert->waitForElement('css', '.cke_editor_edit-body-0-value_dialog')); + $this->click('.cke_editor_edit-body-0-value_dialog .cke_dialog_ui_checkbox'); + $page->pressButton('OK'); + + $this->pressEditorButton('source'); + $this->assertStringContainsString('find('css', 'textarea.cke_source') + ->getValue()); + $this->pressEditorButton('source'); + + $this->assignNameToCkeditorIframe('edit-body-0-value-instance-id'); + $this->getSession()->switchToIFrame('edit-body-0-value-instance-id'); + $page->find('css', 'table > tbody > tr > td')->rightClick(); + $this->getSession()->switchToIFrame(); + $this->getSession()->switchToIFrame($page->find('css', '.cke_menu_panel iframe')->getAttribute('id')); + $this->clickLink('Table Properties'); + $this->getSession()->switchToIFrame(); + $this->assertNotEmpty($web_assert->waitForElement('css', '.cke_editor_edit-body-0-value_dialog')); + $this->assertTrue($page->findField('Zebra striping')->isChecked()); + $page->find('css', '.cke_editor_edit-body-0-value_dialog[style~="flex;"] .cke_dialog_ui_button_ok')->click(); + + $page->fillField('Page title', 'test'); + $this->click('#cke_edit-oe-teaser-0-value .cke_button__source'); + $page->find('css', 'textarea.cke_source')->setValue('test'); + $page->fillField('Content owner (value 1)', 'Audit Board of the European Communities (http://publications.europa.eu/resource/authority/corporate-body/ABEC)'); + $page->pressButton('Save'); + $web_assert->elementExists('css', 'article .ecl table.ecl-table.ecl-table--zebra'); + + $this->drupalGet('node/1/edit'); + $this->assignNameToCkeditorIframe('edit-body-0-value-instance-id'); + $this->getSession()->switchToIFrame('edit-body-0-value-instance-id'); + $page->find('css', 'table > tbody > tr > td')->rightClick(); + $this->getSession()->switchToIFrame(); + $this->getSession()->switchToIFrame($page->find('css', '.cke_menu_panel iframe')->getAttribute('id')); + $this->clickLink('Table Properties'); + $this->getSession()->switchToIFrame(); + $this->assertNotEmpty($web_assert->waitForElement('css', '.cke_editor_edit-body-0-value_dialog')); + $this->click('.cke_editor_edit-body-0-value_dialog .cke_dialog_ui_checkbox'); + $page->find('css', '.cke_editor_edit-body-0-value_dialog[style~="flex;"] .cke_dialog_ui_button_ok')->click(); + $page->pressButton('Save'); + $web_assert->elementNotExists('css', 'article .ecl table.ecl-table.ecl-table--zebra'); + $web_assert->elementExists('css', 'article .ecl table'); + } + + /** + * Wait for a CKEditor instance to finish loading and initializing. + * + * @param string $instance_id + * The CKEditor instance ID. + * @param int $timeout + * (optional) Timeout in milliseconds, defaults to 10000. + */ + protected function waitOnCkeditorInstance($instance_id, $timeout = 10000) { + $condition = <<getSession()->wait($timeout, $condition); + } + +} diff --git a/modules/oe_theme_helper/tests/src/Unit/FilterEclTableTest.php b/modules/oe_theme_helper/tests/src/Unit/FilterEclTableTest.php index 9e7bbf434..d819c18dd 100644 --- a/modules/oe_theme_helper/tests/src/Unit/FilterEclTableTest.php +++ b/modules/oe_theme_helper/tests/src/Unit/FilterEclTableTest.php @@ -59,6 +59,10 @@ public function processDataProvider(): array { '
Caption
Column 1Column 2
1-11-2
2-12-2
', '
Caption
Column 1Column 2
1-11-2
2-12-2
', ], + 'Table with data-striped' => [ + '
Caption
Column 1Column 2
1-11-2
2-12-2
', + '
Caption
Column 1Column 2
1-11-2
2-12-2
', + ], 'Multiple tables' => [ '
Column A1Column A2
A1-1A1-2
A2-1A2-2
B1-1B1-2
B2-1B2-2
Column C1Column C2Column C3
C1-1C1-2C1-3
C2-1C2-2C2-3
', '
Column A1Column A2
A1-1A1-2
A2-1A2-2
B1-1B1-2
B2-1B2-2
Column C1Column C2Column C3
C1-1C1-2C1-3
C2-1C2-2C2-3
', From 094e8cc0cedff24bb0050a6a2cde3f850f86344a Mon Sep 17 00:00:00 2001 From: Sergii Pavlenko Date: Fri, 23 Sep 2022 12:37:25 +0300 Subject: [PATCH 04/10] EWPP-2557: Update comments in code. --- .../Plugin/CKEditorPlugin/TableZebraStriping.php | 2 ++ .../src/Plugin/Filter/FilterEclTable.php | 1 + .../FunctionalJavascript/WysiwygTableTest.php | 16 ++++++++++------ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/modules/oe_theme_helper/src/Plugin/CKEditorPlugin/TableZebraStriping.php b/modules/oe_theme_helper/src/Plugin/CKEditorPlugin/TableZebraStriping.php index ab10b5b9a..c2eaf522b 100755 --- a/modules/oe_theme_helper/src/Plugin/CKEditorPlugin/TableZebraStriping.php +++ b/modules/oe_theme_helper/src/Plugin/CKEditorPlugin/TableZebraStriping.php @@ -49,6 +49,8 @@ public function isEnabled(Editor $editor) { return FALSE; } + // "Zebra striping" option should be available only when the ECL table + // filter is enabled and the table button is present in the WYSIWYG toolbar. $enabled = FALSE; $format = $editor->getFilterFormat(); if ($format->filters('filter_ecl_table')->status) { diff --git a/modules/oe_theme_helper/src/Plugin/Filter/FilterEclTable.php b/modules/oe_theme_helper/src/Plugin/Filter/FilterEclTable.php index a7806dc7a..1959e96e1 100644 --- a/modules/oe_theme_helper/src/Plugin/Filter/FilterEclTable.php +++ b/modules/oe_theme_helper/src/Plugin/Filter/FilterEclTable.php @@ -77,6 +77,7 @@ public function process($text, $langcode) { } } + // Add related to "Zebra striping" classes. foreach ($xpath->query('//table[@data-striped="true"]') as $table) { $classes = $table->getAttribute('class'); $table->setAttribute('class', ltrim($classes . ' ecl-table ecl-table--zebra')); diff --git a/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php b/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php index 0c6d4979c..81a701f92 100644 --- a/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php +++ b/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php @@ -101,12 +101,14 @@ public function testWysiwygTable(): void { $web_assert = $this->assertSession(); $page = $this->getSession()->getPage(); $this->drupalLogin($this->webUser); + // "Zebra striping" checkbox should be visible in Table properties dialog. $this->drupalGet('node/add/oe_page'); $this->waitOnCkeditorInstance('edit-body-0-value'); $this->pressEditorButton('table'); $this->assertNotEmpty($web_assert->waitForElement('css', '.cke_editor_edit-body-0-value_dialog')); $web_assert->elementContains('css', '.cke_editor_edit-body-0-value_dialog .cke_dialog_ui_checkbox', 'Zebra striping'); - + // "Zebra striping" checkbox should not be visible in Table properties + // dialog with disabled ECL table filter. FilterFormat::load('full_html') ->setFilterConfig('filter_ecl_table', ['status' => FALSE]) ->save(); @@ -115,7 +117,8 @@ public function testWysiwygTable(): void { $this->pressEditorButton('table'); $this->assertNotEmpty($web_assert->waitForElement('css', '.cke_editor_edit-body-0-value_dialog')); $web_assert->elementNotExists('css', '.cke_editor_edit-body-0-value_dialog .cke_dialog_ui_checkbox'); - + // "Zebra striping" checkbox should be visible in Table properties dialog + // after enabling ECL table filter. FilterFormat::load('full_html') ->setFilterConfig('filter_ecl_table', ['status' => TRUE]) ->save(); @@ -125,12 +128,12 @@ public function testWysiwygTable(): void { $this->assertNotEmpty($web_assert->waitForElement('css', '.cke_editor_edit-body-0-value_dialog')); $this->click('.cke_editor_edit-body-0-value_dialog .cke_dialog_ui_checkbox'); $page->pressButton('OK'); - + // Data attribute should be present after enabling "Zebra striping" option. $this->pressEditorButton('source'); $this->assertStringContainsString('find('css', 'textarea.cke_source') ->getValue()); $this->pressEditorButton('source'); - + // Assert enabled "Zebra striping" checkbox in Table properties dialog. $this->assignNameToCkeditorIframe('edit-body-0-value-instance-id'); $this->getSession()->switchToIFrame('edit-body-0-value-instance-id'); $page->find('css', 'table > tbody > tr > td')->rightClick(); @@ -141,14 +144,15 @@ public function testWysiwygTable(): void { $this->assertNotEmpty($web_assert->waitForElement('css', '.cke_editor_edit-body-0-value_dialog')); $this->assertTrue($page->findField('Zebra striping')->isChecked()); $page->find('css', '.cke_editor_edit-body-0-value_dialog[style~="flex;"] .cke_dialog_ui_button_ok')->click(); - + // Assert presence of classes in table related to "Zebra striping" option. $page->fillField('Page title', 'test'); $this->click('#cke_edit-oe-teaser-0-value .cke_button__source'); $page->find('css', 'textarea.cke_source')->setValue('test'); $page->fillField('Content owner (value 1)', 'Audit Board of the European Communities (http://publications.europa.eu/resource/authority/corporate-body/ABEC)'); $page->pressButton('Save'); $web_assert->elementExists('css', 'article .ecl table.ecl-table.ecl-table--zebra'); - + // Assert absence of classes in table related to "Zebra striping" with + // disabled option. $this->drupalGet('node/1/edit'); $this->assignNameToCkeditorIframe('edit-body-0-value-instance-id'); $this->getSession()->switchToIFrame('edit-body-0-value-instance-id'); From f3949997b69cd9232d439f87fd57218e076f6407 Mon Sep 17 00:00:00 2001 From: Sergii Pavlenko Date: Mon, 26 Sep 2022 12:39:14 +0300 Subject: [PATCH 05/10] EWPP-2557: Fix function javascript test. --- .../tests/src/FunctionalJavascript/WysiwygTableTest.php | 5 +---- ...er+3.4.1.patch => twig-component-site-footer+3.4.2.patch} | 0 ...nt-table+3.4.1.patch => twig-component-table+3.4.2.patch} | 0 3 files changed, 1 insertion(+), 4 deletions(-) rename patches/@ecl/{twig-component-site-footer+3.4.1.patch => twig-component-site-footer+3.4.2.patch} (100%) rename patches/@ecl/{twig-component-table+3.4.1.patch => twig-component-table+3.4.2.patch} (100%) diff --git a/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php b/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php index 81a701f92..2a3d07eed 100644 --- a/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php +++ b/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php @@ -1,6 +1,6 @@ container->get('theme_installer')->install(['oe_theme', 'seven']); $this->config('system.theme')->set('default', 'oe_theme')->save(); - $this->config('system.theme')->set('admin', 'seven')->save(); - $this->config('node.settings')->set('use_admin_theme', TRUE)->save(); - $this->container->get('router.builder')->rebuild(); // Rebuild the ui_pattern definitions to collect the ones provided by // oe_theme itself. diff --git a/patches/@ecl/twig-component-site-footer+3.4.1.patch b/patches/@ecl/twig-component-site-footer+3.4.2.patch similarity index 100% rename from patches/@ecl/twig-component-site-footer+3.4.1.patch rename to patches/@ecl/twig-component-site-footer+3.4.2.patch diff --git a/patches/@ecl/twig-component-table+3.4.1.patch b/patches/@ecl/twig-component-table+3.4.2.patch similarity index 100% rename from patches/@ecl/twig-component-table+3.4.1.patch rename to patches/@ecl/twig-component-table+3.4.2.patch From c0cec1c8f62e0310f73dec618d1135e529916f12 Mon Sep 17 00:00:00 2001 From: Sergii Pavlenko Date: Fri, 30 Sep 2022 11:56:34 +0300 Subject: [PATCH 06/10] EWPP-2558: Add Sort plugin with test coverage. --- modules/oe_theme_helper/js/table_sort.js | 94 ++++++++ .../src/Plugin/CKEditorPlugin/TableSort.php | 70 ++++++ .../src/Plugin/Filter/FilterEclTable.php | 6 + .../FunctionalJavascript/WysiwygTableTest.php | 203 +++++++++++++++++- .../tests/src/Unit/FilterEclTableTest.php | 4 + 5 files changed, 375 insertions(+), 2 deletions(-) create mode 100644 modules/oe_theme_helper/js/table_sort.js create mode 100755 modules/oe_theme_helper/src/Plugin/CKEditorPlugin/TableSort.php diff --git a/modules/oe_theme_helper/js/table_sort.js b/modules/oe_theme_helper/js/table_sort.js new file mode 100644 index 000000000..461edf13d --- /dev/null +++ b/modules/oe_theme_helper/js/table_sort.js @@ -0,0 +1,94 @@ +/** + * @file + * Plugin for adding sorting of table. + */ + +(function ($, CKEDITOR) { + "use strict"; + + CKEDITOR.plugins.add('table_sort', { + afterInit: function afterInit (editor) { + CKEDITOR.on('dialogDefinition', function(event) { + const dialog_name = event.data.name; + const sort_attribute = 'data-sortable'; + + if (dialog_name !== 'cellProperties') { + return; + } + + const dialog_definition = event.data.definition; + const info_tab = dialog_definition.getContents('info'); + const langCell = event.editor.lang.table.cell; + // Avoid multiple selectbox adding. + if (!info_tab.get('columnSortable')) { + info_tab.add({ + type: 'select', + label: 'Sortable', + id: 'columnSortable', + requiredContent: 'th[' + sort_attribute + ']', + items: [ + [langCell.yes, 'yes'], + [langCell.no, 'no'], + ], + + /** + * @param {CKEDITOR.dom.element[]} selectedCells + */ + setup: function (selectedCells) { + // Disable the element if any of the selected cells is not a header cell. + for (let i = 0; i < selectedCells.length; i++) { + if (selectedCells[i].getName() !== 'th') { + this.disable(); + this.setValue(null); + return; + } + } + + // This method receives an array of selected cells. + // We use the value of the first one as driver for the remaining cells. + // @see setupCells() in /plugins/tabletools/dialogs/tableCell.js + let value = selectedCells[0].hasAttribute(sort_attribute); + + for (let i = 1; i < selectedCells.length; i++) { + if (selectedCells[i].hasAttribute(sort_attribute) !== value) { + // If any of the cells has a different sortable value, set the value + // to undetermined. + value = null; + break; + } + } + + // Convert the value to the matching option. + if (value !== null) { + value = value ? 'yes' : 'no'; + } + + this.setValue(value); + + // The only way to have an empty select value in Firefox is + // to set a negative selectedIndex. + if (value === null && CKEDITOR.env.gecko) { + this.getInputElement().$.selectedIndex = -1; + } + }, + + /** + * @param {CKEDITOR.dom.element} selectedCell + */ + commit: function (selectedCell) { + const value = this.getValue(); + + // Handle only supported values. + if (value === 'yes') { + selectedCell.setAttribute(sort_attribute, true); + } else if (value === 'no') { + selectedCell.removeAttribute(sort_attribute); + } + } + }); + } + }); + } + }); + +})(jQuery, CKEDITOR); diff --git a/modules/oe_theme_helper/src/Plugin/CKEditorPlugin/TableSort.php b/modules/oe_theme_helper/src/Plugin/CKEditorPlugin/TableSort.php new file mode 100755 index 000000000..c9b9e2703 --- /dev/null +++ b/modules/oe_theme_helper/src/Plugin/CKEditorPlugin/TableSort.php @@ -0,0 +1,70 @@ +getModulePath('oe_theme_helper') . '/js/table_sort.js'; + } + + /** + * {@inheritdoc} + */ + public function getConfig(Editor $editor) { + return []; + } + + /** + * {@inheritdoc} + */ + public function getButtons() { + return []; + } + + /** + * {@inheritdoc} + */ + public function isEnabled(Editor $editor) { + if (!$editor->hasAssociatedFilterFormat()) { + return FALSE; + } + + // "Sortable" option should be available only when the ECL table + // filter is enabled and the table button is present in the WYSIWYG toolbar. + $enabled = FALSE; + $format = $editor->getFilterFormat(); + if ($format->filters('filter_ecl_table')->status) { + $settings = $editor->getSettings(); + foreach ($settings['toolbar']['rows'] as $row) { + foreach ($row as $group) { + foreach ($group['items'] as $button) { + if ($button === 'Table') { + $enabled = TRUE; + } + } + } + } + } + + return $enabled; + } + +} diff --git a/modules/oe_theme_helper/src/Plugin/Filter/FilterEclTable.php b/modules/oe_theme_helper/src/Plugin/Filter/FilterEclTable.php index 1959e96e1..d0401fc1b 100644 --- a/modules/oe_theme_helper/src/Plugin/Filter/FilterEclTable.php +++ b/modules/oe_theme_helper/src/Plugin/Filter/FilterEclTable.php @@ -84,6 +84,12 @@ public function process($text, $langcode) { $table->removeAttribute('data-striped'); } + // Add related to "Sort" data attribute. + foreach ($xpath->query('//table/thead/tr/th[@data-sortable="true"]') as $column) { + $column->setAttribute('data-ecl-table-sort-toggle', ''); + $column->removeAttribute('data-sortable'); + } + $result->setProcessedText(Html::serialize($dom)); return $result; diff --git a/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php b/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php index 2a3d07eed..3d5da7a0e 100644 --- a/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php +++ b/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php @@ -4,6 +4,7 @@ namespace Drupal\Tests\oe_theme_helper\FunctionalJavascript; +use Behat\Mink\Element\NodeElement; use Drupal\editor\Entity\Editor; use Drupal\filter\Entity\FilterFormat; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; @@ -100,6 +101,7 @@ public function testWysiwygTable(): void { $this->drupalLogin($this->webUser); // "Zebra striping" checkbox should be visible in Table properties dialog. $this->drupalGet('node/add/oe_page'); + $this->waitForEditor(); $this->waitOnCkeditorInstance('edit-body-0-value'); $this->pressEditorButton('table'); $this->assertNotEmpty($web_assert->waitForElement('css', '.cke_editor_edit-body-0-value_dialog')); @@ -110,6 +112,7 @@ public function testWysiwygTable(): void { ->setFilterConfig('filter_ecl_table', ['status' => FALSE]) ->save(); $this->getSession()->reload(); + $this->waitForEditor(); $this->waitOnCkeditorInstance('edit-body-0-value'); $this->pressEditorButton('table'); $this->assertNotEmpty($web_assert->waitForElement('css', '.cke_editor_edit-body-0-value_dialog')); @@ -120,6 +123,7 @@ public function testWysiwygTable(): void { ->setFilterConfig('filter_ecl_table', ['status' => TRUE]) ->save(); $this->getSession()->reload(); + $this->waitForEditor(); $this->waitOnCkeditorInstance('edit-body-0-value'); $this->pressEditorButton('table'); $this->assertNotEmpty($web_assert->waitForElement('css', '.cke_editor_edit-body-0-value_dialog')); @@ -140,7 +144,7 @@ public function testWysiwygTable(): void { $this->getSession()->switchToIFrame(); $this->assertNotEmpty($web_assert->waitForElement('css', '.cke_editor_edit-body-0-value_dialog')); $this->assertTrue($page->findField('Zebra striping')->isChecked()); - $page->find('css', '.cke_editor_edit-body-0-value_dialog[style~="flex;"] .cke_dialog_ui_button_ok')->click(); + $this->getOpenedDialogElement()->getParent()->getParent()->pressButton('OK'); // Assert presence of classes in table related to "Zebra striping" option. $page->fillField('Page title', 'test'); $this->click('#cke_edit-oe-teaser-0-value .cke_button__source'); @@ -151,6 +155,7 @@ public function testWysiwygTable(): void { // Assert absence of classes in table related to "Zebra striping" with // disabled option. $this->drupalGet('node/1/edit'); + $this->waitForEditor(); $this->assignNameToCkeditorIframe('edit-body-0-value-instance-id'); $this->getSession()->switchToIFrame('edit-body-0-value-instance-id'); $page->find('css', 'table > tbody > tr > td')->rightClick(); @@ -160,10 +165,204 @@ public function testWysiwygTable(): void { $this->getSession()->switchToIFrame(); $this->assertNotEmpty($web_assert->waitForElement('css', '.cke_editor_edit-body-0-value_dialog')); $this->click('.cke_editor_edit-body-0-value_dialog .cke_dialog_ui_checkbox'); - $page->find('css', '.cke_editor_edit-body-0-value_dialog[style~="flex;"] .cke_dialog_ui_button_ok')->click(); + $this->getOpenedDialogElement()->getParent()->getParent()->pressButton('OK'); $page->pressButton('Save'); $web_assert->elementNotExists('css', 'article .ecl table.ecl-table.ecl-table--zebra'); $web_assert->elementExists('css', 'article .ecl table'); + + // If the filter is not enabled in an editor, + // the "Sort" plugin is not active. + FilterFormat::load('full_html') + ->setFilterConfig('filter_ecl_table', ['status' => FALSE]) + ->save(); + $this->drupalGet('node/1/edit'); + $this->waitForEditor(); + $this->assignNameToCkeditorIframe('edit-body-0-value-instance-id'); + $this->getSession()->switchToIFrame('edit-body-0-value-instance-id'); + // Enable first row headers for table. + $page->find('css', 'table > tbody > tr > td')->rightClick(); + $this->getSession()->switchToIFrame(); + $this->getSession()->switchToIFrame($page->find('css', '.cke_menu_panel iframe')->getAttribute('id')); + $this->clickLink('Table Properties'); + $this->getSession()->switchToIFrame(); + $this->assertNotEmpty($web_assert->waitForElement('css', '.cke_editor_edit-body-0-value_dialog[style~="flex;"]')); + $this->getOpenedDialogElement()->selectFieldOption('Headers', 'First Row'); + $this->getOpenedDialogElement()->getParent()->getParent()->pressButton('OK'); + $this->getSession()->switchToIFrame(); + // Put content for table headers. + $javascript = << thead > tr > th'); + header_cols.getItem(0).setHtml('Header text 1'); + header_cols.getItem(1).setHtml('Header text 2'); +})() +JS; + $this->getSession()->evaluateScript($javascript); + $page->pressButton('Save'); + // Sortable field select box is not available as + // "ECL Table" plugin is disabled. + $this->drupalGet('node/1/edit'); + $this->waitForEditor(); + $this->assignNameToCkeditorIframe('edit-body-0-value-instance-id'); + $this->openCellPropertiesDialog('table > thead > tr > th'); + $this->assertFalse($page->hasField('Sortable')); + $this->getOpenedDialogElement()->getParent()->getParent()->pressButton('OK'); + + // If the filter is enabled in an editor, + // the "Sort" plugin is active. + FilterFormat::load('full_html') + ->setFilterConfig('filter_ecl_table', ['status' => TRUE]) + ->save(); + $this->getSession()->reload(); + $this->waitForEditor(); + $this->assignNameToCkeditorIframe('edit-body-0-value-instance-id'); + // The select is shown when right clicking a
. + $this->openCellPropertiesDialog('table > thead > tr > th'); + $this->assertFalse((bool) $this->getOpenedDialogElement()->findField('Sortable')->getAttribute('disabled')); + $this->getOpenedDialogElement()->getParent()->getParent()->pressButton('OK'); + + // The select is disabled when right clicking a . + $this->openCellPropertiesDialog('table > tbody > tr > td'); + $this->assertTrue((bool) $this->getOpenedDialogElement()->findField('Sortable')->getAttribute('disabled')); + $this->getOpenedDialogElement()->getParent()->getParent()->pressButton('OK'); + + // The select is shown when right clicking after selecting multiple . + $this->selectElementBySelector('table > thead > tr'); + $this->openCellPropertiesDialog('table > thead > tr > th'); + $this->assertFalse((bool) $this->getOpenedDialogElement()->findField('Sortable')->getAttribute('disabled')); + $this->getOpenedDialogElement()->getParent()->getParent()->pressButton('OK'); + + // The select is disabled when right clicking + // after selecting a mix of and . + $this->selectElementBySelector('table'); + $this->openCellPropertiesDialog('table > thead > tr > th'); + $this->assertTrue((bool) $this->getOpenedDialogElement()->findField('Sortable')->getAttribute('disabled')); + $this->getOpenedDialogElement()->getParent()->getParent()->pressButton('OK'); + + // Make sortable particular header column. + $this->selectElementBySelector('table > thead > tr > th'); + $this->openCellPropertiesDialog('table > thead > tr > th'); + $this->getOpenedDialogElement()->selectFieldOption('Sortable', 'yes'); + $this->getOpenedDialogElement()->getParent()->getParent()->pressButton('OK'); + // Assert correct saving of header column sort state. + $this->selectElementBySelector('table > thead > tr > th'); + $this->openCellPropertiesDialog('table > thead > tr > th'); + $this->assertEquals('yes', $this->getOpenedDialogElement()->findField('Sortable')->getValue()); + $this->getOpenedDialogElement()->getParent()->getParent()->pressButton('OK'); + + // Make not sortable particular header column. + $this->selectElementBySelector('table > thead > tr > th'); + $this->openCellPropertiesDialog('table > thead > tr > th'); + $this->getOpenedDialogElement()->selectFieldOption('Sortable', 'no'); + $this->getOpenedDialogElement()->getParent()->getParent()->pressButton('OK'); + // Assert correct saving of header column sort state. + $this->selectElementBySelector('table > thead > tr > th'); + $this->openCellPropertiesDialog('table > thead > tr > th'); + $this->assertEquals('no', $this->getOpenedDialogElement()->findField('Sortable')->getValue()); + $this->getOpenedDialogElement()->getParent()->getParent()->pressButton('OK'); + + // The select has no selection when the selection contains a mix of + // with and without the attribute. + $this->selectElementBySelector('table > thead > tr > th'); + $this->openCellPropertiesDialog('table > thead > tr > th'); + $this->getOpenedDialogElement()->selectFieldOption('Sortable', 'yes'); + $this->getOpenedDialogElement()->getParent()->getParent()->pressButton('OK'); + $this->selectElementBySelector('table > thead > tr'); + $this->openCellPropertiesDialog('table > thead > tr > th'); + $this->assertTrue(empty($this->getOpenedDialogElement()->findField('Sortable')->getValue())); + $this->getOpenedDialogElement()->getParent()->getParent()->pressButton('OK'); + // The attributes are untouched when if the dialog is submitted with the + // "no selection" option at the step above. + $this->selectElementBySelector('table > thead > tr > th'); + $this->openCellPropertiesDialog('table > thead > tr > th'); + $this->assertEquals('yes', $this->getOpenedDialogElement()->findField('Sortable')->getValue()); + $this->getOpenedDialogElement()->getParent()->getParent()->pressButton('OK'); + + // The attribute is added/removed on single/multiple selections + // based on the chosen value. + $this->selectElementBySelector('table > thead > tr'); + $this->openCellPropertiesDialog('table > thead > tr > th'); + $this->getOpenedDialogElement()->selectFieldOption('Sortable', 'no'); + $this->getOpenedDialogElement()->getParent()->getParent()->pressButton('OK'); + $this->selectElementBySelector('table > thead > tr'); + $this->openCellPropertiesDialog('table > thead > tr > th'); + $this->assertEquals('no', $this->getOpenedDialogElement()->findField('Sortable')->getValue()); + $this->getOpenedDialogElement()->getParent()->getParent()->pressButton('OK'); + + // Make sure that the selected column's sortability is correctly + // reflected on frontend. + $this->selectElementBySelector('table > thead > tr'); + $this->openCellPropertiesDialog('table > thead > tr > th'); + $this->getOpenedDialogElement()->selectFieldOption('Sortable', 'yes'); + $this->getOpenedDialogElement()->getParent()->getParent()->pressButton('OK'); + $this->selectElementBySelector('table > thead > tr'); + $this->openCellPropertiesDialog('table > thead > tr > th'); + $this->assertEquals('yes', $this->getOpenedDialogElement()->findField('Sortable')->getValue()); + $this->getOpenedDialogElement()->getParent()->getParent()->pressButton('OK'); + + $page->pressButton('Save'); + $headers = $this->getSession()->getPage()->findAll('css', 'table thead tr th[data-ecl-table-sort-toggle]'); + $this->assertCount(2, $headers); + } + + /** + * Open Cell Property dialog based on provided selector for a table cell. + * + * @param string $right_click_selector + * Right click selector. + */ + protected function openCellPropertiesDialog(string $right_click_selector): void { + $web_assert = $this->assertSession(); + $page = $this->getSession()->getPage(); + $this->getSession()->switchToIFrame('edit-body-0-value-instance-id'); + $page->find('css', $right_click_selector)->rightClick(); + $this->getSession()->switchToIFrame(); + $web_assert->waitForElementVisible('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' cke_menu_panel ')]"); + $this->getSession()->switchToIFrame($page->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' cke_menu_panel ') and not(@hidden)]/iframe")->getAttribute('id')); + $this->clickLink('Cell'); + $this->getSession()->switchToIFrame(); + $web_assert->waitForElementVisible('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' cke_menu_panel ')][last()]"); + $this->getSession()->switchToIFrame($page->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' cke_menu_panel ') and not(@hidden)][last()]/iframe")->getAttribute('id')); + $this->clickLink('Cell Properties'); + $this->getSession()->switchToIFrame(); + $web_assert->waitForElementVisible('css', '#' . $this->getOpenedDialogElement()->getAttribute('id')); + } + + /** + * Get current dialog element related to the WYSIWYG. + */ + protected function getOpenedDialogElement(): ?NodeElement { + $javascript = <<getSession()->evaluateScript($javascript); + return $this->getSession()->getPage()->findById($dialogId); + } + + /** + * Make selection inside the WYSIWYG content. + * + * @param string $selector + * Selector of element. + * @param string $instance_id + * (optional) The CKEditor instance ID. Defaults to 'edit-body-0-value'. + */ + protected function selectElementBySelector(string $selector, string $instance_id = 'edit-body-0-value'): void { + $javascript = <<getSession()->evaluateScript($javascript); } /** diff --git a/modules/oe_theme_helper/tests/src/Unit/FilterEclTableTest.php b/modules/oe_theme_helper/tests/src/Unit/FilterEclTableTest.php index d819c18dd..b2d98b712 100644 --- a/modules/oe_theme_helper/tests/src/Unit/FilterEclTableTest.php +++ b/modules/oe_theme_helper/tests/src/Unit/FilterEclTableTest.php @@ -63,6 +63,10 @@ public function processDataProvider(): array { '
Caption
Column 1Column 2
1-11-2
2-12-2
', '
Caption
Column 1Column 2
1-11-2
2-12-2
', ], + 'Table with sort' => [ + '
Caption
Column 1Column 2
1-11-2
2-12-2
', + '
Caption
Column 1Column 2
1-11-2
2-12-2
', + ], 'Multiple tables' => [ '
Column A1Column A2
A1-1A1-2
A2-1A2-2
B1-1B1-2
B2-1B2-2
Column C1Column C2Column C3
C1-1C1-2C1-3
C2-1C2-2C2-3
', '
Column A1Column A2
A1-1A1-2
A2-1A2-2
B1-1B1-2
B2-1B2-2
Column C1Column C2Column C3
C1-1C1-2C1-3
C2-1C2-2C2-3
', From f204ab82331347f5330ca7e39633fd62de9612f7 Mon Sep 17 00:00:00 2001 From: Sergii Pavlenko Date: Mon, 3 Oct 2022 18:54:08 +0300 Subject: [PATCH 07/10] EWPP-2559: Adjust ECL table filter. --- .../src/Plugin/Filter/FilterEclTable.php | 56 ++++++++++++------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/modules/oe_theme_helper/src/Plugin/Filter/FilterEclTable.php b/modules/oe_theme_helper/src/Plugin/Filter/FilterEclTable.php index d0401fc1b..21b470cb6 100644 --- a/modules/oe_theme_helper/src/Plugin/Filter/FilterEclTable.php +++ b/modules/oe_theme_helper/src/Plugin/Filter/FilterEclTable.php @@ -51,14 +51,48 @@ public function process($text, $langcode) { continue; } + // Put ECL related classes for table tag. + $table_classes = ltrim($table->getAttribute('class') . ' ecl-table'); + // Add related to "Zebra striping" classes. + if ($table->getAttribute('data-striped') === 'true') { + $table_classes .= ' ecl-table--zebra'; + $table->removeAttribute('data-striped'); + } + $table->setAttribute('class', $table_classes); + + // Put ECL related classes for thead tag. + foreach ($xpath->query('./thead', $table) as $thead) { + $thead->setAttribute('class', ltrim($thead->getAttribute('class') . ' ecl-table__head')); + } + + // Put ECL related classes for tbody tag. + foreach ($xpath->query('./tbody', $table) as $tbody) { + $tbody->setAttribute('class', ltrim($tbody->getAttribute('class') . ' ecl-table__body')); + } + + // Put ECL related classes for tr tags. + foreach ($xpath->query('.//tr', $table) as $trow) { + $trow->setAttribute('class', ltrim($trow->getAttribute('class') . ' ecl-table__row')); + } + $headers = []; // Collect the first header row, validating that is composed only of // th elements. $has_header_row = $xpath->query('(./thead/tr[1])[count(./*[not(self::th)]) = 0]', $table); if ($has_header_row->count()) { $header_row = $has_header_row[0]; - foreach ($xpath->query('./th', $header_row) as $cell) { - $headers[] = $cell->nodeValue; + foreach ($xpath->query('./th', $header_row) as $thead_cell) { + $thead_cell->setAttribute('class', ltrim($thead_cell->getAttribute('class') . ' ecl-table__header')); + // Add related to "Sort" data attribute. + if ($thead_cell->getAttribute('data-sortable') === 'true') { + $thead_cell->setAttribute('data-ecl-table-sort-toggle', ''); + $thead_cell->removeAttribute('data-sortable'); + + // Add additional attributes to enable sorting for table. + $table->setAttribute('data-ecl-table', ''); + $table->setAttribute('data-ecl-auto-init', 'Table'); + } + $headers[] = $thead_cell->nodeValue; } } @@ -66,10 +100,7 @@ public function process($text, $langcode) { foreach ($xpath->query('.//tr[not(parent::thead)]', $table) as $row) { // Fetch all the cells inside the row. foreach ($xpath->query('./*[self::th or self::td]', $row) as $cell_index => $cell) { - $existing_class = $cell->getAttribute('class'); - $new_class = $existing_class ? "$existing_class ecl-table__cell" : 'ecl-table__cell'; - $cell->setAttribute('class', $new_class); - + $cell->setAttribute('class', ltrim($cell->getAttribute('class') . ' ecl-table__cell')); if (array_key_exists($cell_index, $headers)) { $cell->setAttribute('data-ecl-table-header', $headers[$cell_index]); } @@ -77,19 +108,6 @@ public function process($text, $langcode) { } } - // Add related to "Zebra striping" classes. - foreach ($xpath->query('//table[@data-striped="true"]') as $table) { - $classes = $table->getAttribute('class'); - $table->setAttribute('class', ltrim($classes . ' ecl-table ecl-table--zebra')); - $table->removeAttribute('data-striped'); - } - - // Add related to "Sort" data attribute. - foreach ($xpath->query('//table/thead/tr/th[@data-sortable="true"]') as $column) { - $column->setAttribute('data-ecl-table-sort-toggle', ''); - $column->removeAttribute('data-sortable'); - } - $result->setProcessedText(Html::serialize($dom)); return $result; From 2aea4cb11de288a0aa31b48181a71d41bea88285 Mon Sep 17 00:00:00 2001 From: Sergii Pavlenko Date: Mon, 3 Oct 2022 18:55:03 +0300 Subject: [PATCH 08/10] EWPP-2559: Update test coverage for ECL Filter. --- .../src/Plugin/Filter/FilterEclTable.php | 75 +++++++++++++------ .../FunctionalJavascript/WysiwygTableTest.php | 37 +++------ .../tests/src/Unit/FilterEclTableTest.php | 58 +++++++------- 3 files changed, 94 insertions(+), 76 deletions(-) diff --git a/modules/oe_theme_helper/src/Plugin/Filter/FilterEclTable.php b/modules/oe_theme_helper/src/Plugin/Filter/FilterEclTable.php index 21b470cb6..2f012d6ca 100644 --- a/modules/oe_theme_helper/src/Plugin/Filter/FilterEclTable.php +++ b/modules/oe_theme_helper/src/Plugin/Filter/FilterEclTable.php @@ -37,42 +37,46 @@ public function process($text, $langcode) { $dom = Html::load($text); $xpath = new \DOMXPath($dom); - foreach ($xpath->query('//table[.//th]') as $table) { - // Skip the table if any cell spans over multiple columns or rows. - $span_cells = $xpath->query('.//*[self::th or self::td][(@colspan and @colspan > 1) or (@rowspan and @rowspan > 1)]', $table); - if ($span_cells->count() !== 0) { - continue; - } - - // Do not process tables that use th cells anywhere but in the first - // column. - $ths_in_body = $xpath->query('.//tr[not(parent::thead)]/*[position()>1 and self::th]', $table); - if ($ths_in_body->count() !== 0) { - continue; - } - + foreach ($xpath->query('//table') as $table) { // Put ECL related classes for table tag. - $table_classes = ltrim($table->getAttribute('class') . ' ecl-table'); - // Add related to "Zebra striping" classes. + $this->elementAddClass($table, 'ecl-table'); + // Add classes related to "Zebra striping". if ($table->getAttribute('data-striped') === 'true') { - $table_classes .= ' ecl-table--zebra'; + $this->elementAddClass($table, 'ecl-table--zebra'); $table->removeAttribute('data-striped'); } - $table->setAttribute('class', $table_classes); // Put ECL related classes for thead tag. foreach ($xpath->query('./thead', $table) as $thead) { - $thead->setAttribute('class', ltrim($thead->getAttribute('class') . ' ecl-table__head')); + $this->elementAddClass($thead, 'ecl-table__head'); } // Put ECL related classes for tbody tag. foreach ($xpath->query('./tbody', $table) as $tbody) { - $tbody->setAttribute('class', ltrim($tbody->getAttribute('class') . ' ecl-table__body')); + $this->elementAddClass($tbody, 'ecl-table__body'); } // Put ECL related classes for tr tags. foreach ($xpath->query('.//tr', $table) as $trow) { - $trow->setAttribute('class', ltrim($trow->getAttribute('class') . ' ecl-table__row')); + $this->elementAddClass($trow, 'ecl-table__row'); + } + + // Skip further processing of the table if table without headers. + $ths = $xpath->query('.//th', $table); + if ($ths->count() === 0) { + continue; + } + else { + foreach ($xpath->query('.//thead/tr[1]/th', $table) as $th) { + $this->elementAddClass($th, 'ecl-table__header'); + } + } + + // Do not process tables that use th cells anywhere but in the first + // column. + $ths_in_body = $xpath->query('.//tr[not(parent::thead)]/*[position()>1 and self::th]', $table); + if ($ths_in_body->count() !== 0) { + continue; } $headers = []; @@ -82,8 +86,8 @@ public function process($text, $langcode) { if ($has_header_row->count()) { $header_row = $has_header_row[0]; foreach ($xpath->query('./th', $header_row) as $thead_cell) { - $thead_cell->setAttribute('class', ltrim($thead_cell->getAttribute('class') . ' ecl-table__header')); - // Add related to "Sort" data attribute. + $this->elementAddClass($thead_cell, 'ecl-table__header'); + // Add data attribute related to "Sort". if ($thead_cell->getAttribute('data-sortable') === 'true') { $thead_cell->setAttribute('data-ecl-table-sort-toggle', ''); $thead_cell->removeAttribute('data-sortable'); @@ -96,11 +100,18 @@ public function process($text, $langcode) { } } + // Skip further processing of the table if any cell spans + // over multiple columns or rows. + $span_cells = $xpath->query('.//*[self::th or self::td][(@colspan and @colspan > 1) or (@rowspan and @rowspan > 1)]', $table); + if ($span_cells->count() !== 0) { + continue; + } + // Loop through all the table rows, aside from header ones. foreach ($xpath->query('.//tr[not(parent::thead)]', $table) as $row) { // Fetch all the cells inside the row. foreach ($xpath->query('./*[self::th or self::td]', $row) as $cell_index => $cell) { - $cell->setAttribute('class', ltrim($cell->getAttribute('class') . ' ecl-table__cell')); + $this->elementAddClass($cell, 'ecl-table__cell'); if (array_key_exists($cell_index, $headers)) { $cell->setAttribute('data-ecl-table-header', $headers[$cell_index]); } @@ -113,4 +124,20 @@ public function process($text, $langcode) { return $result; } + /** + * Adds class to element. + * + * @param \DOMNode $element + * Element. + * @param string $class + * Class that should be added. + */ + public function elementAddClass(\DOMNode $element, string $class): void { + $classes = $element->getAttribute('class'); + $classes_array = array_filter(array_map('trim', explode(' ', $classes))); + $classes_array[] = $class; + $classes = implode(' ', array_unique($classes_array)); + $element->setAttribute('class', $classes); + } + } diff --git a/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php b/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php index 3d5da7a0e..4c7fb339c 100644 --- a/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php +++ b/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php @@ -144,7 +144,19 @@ public function testWysiwygTable(): void { $this->getSession()->switchToIFrame(); $this->assertNotEmpty($web_assert->waitForElement('css', '.cke_editor_edit-body-0-value_dialog')); $this->assertTrue($page->findField('Zebra striping')->isChecked()); + // Enable first row headers for table. + $this->getOpenedDialogElement()->selectFieldOption('Headers', 'First Row'); $this->getOpenedDialogElement()->getParent()->getParent()->pressButton('OK'); + // Put content for table headers. + $javascript = << thead > tr > th'); + header_cols.getItem(0).setHtml('Header text 1'); + header_cols.getItem(1).setHtml('Header text 2'); +})() +JS; + $this->getSession()->evaluateScript($javascript); // Assert presence of classes in table related to "Zebra striping" option. $page->fillField('Page title', 'test'); $this->click('#cke_edit-oe-teaser-0-value .cke_button__source'); @@ -175,31 +187,6 @@ public function testWysiwygTable(): void { FilterFormat::load('full_html') ->setFilterConfig('filter_ecl_table', ['status' => FALSE]) ->save(); - $this->drupalGet('node/1/edit'); - $this->waitForEditor(); - $this->assignNameToCkeditorIframe('edit-body-0-value-instance-id'); - $this->getSession()->switchToIFrame('edit-body-0-value-instance-id'); - // Enable first row headers for table. - $page->find('css', 'table > tbody > tr > td')->rightClick(); - $this->getSession()->switchToIFrame(); - $this->getSession()->switchToIFrame($page->find('css', '.cke_menu_panel iframe')->getAttribute('id')); - $this->clickLink('Table Properties'); - $this->getSession()->switchToIFrame(); - $this->assertNotEmpty($web_assert->waitForElement('css', '.cke_editor_edit-body-0-value_dialog[style~="flex;"]')); - $this->getOpenedDialogElement()->selectFieldOption('Headers', 'First Row'); - $this->getOpenedDialogElement()->getParent()->getParent()->pressButton('OK'); - $this->getSession()->switchToIFrame(); - // Put content for table headers. - $javascript = << thead > tr > th'); - header_cols.getItem(0).setHtml('Header text 1'); - header_cols.getItem(1).setHtml('Header text 2'); -})() -JS; - $this->getSession()->evaluateScript($javascript); - $page->pressButton('Save'); // Sortable field select box is not available as // "ECL Table" plugin is disabled. $this->drupalGet('node/1/edit'); diff --git a/modules/oe_theme_helper/tests/src/Unit/FilterEclTableTest.php b/modules/oe_theme_helper/tests/src/Unit/FilterEclTableTest.php index b2d98b712..34291877d 100644 --- a/modules/oe_theme_helper/tests/src/Unit/FilterEclTableTest.php +++ b/modules/oe_theme_helper/tests/src/Unit/FilterEclTableTest.php @@ -45,63 +45,67 @@ public function processDataProvider(): array { return [ 'Full table with thead and tfoot' => [ '

Some text before table

Caption
Column 1Column 2
1-11-2
2-12-2
Footer 1Footer 2

Some text after table

', - '

Some text before table

Caption
Column 1Column 2
1-11-2
2-12-2
Footer 1Footer 2

Some text after table

', + '

Some text before table

Caption
Column 1Column 2
1-11-2
2-12-2
Footer 1Footer 2

Some text after table

', + ], + 'Table with zebra striping' => [ + '

Some text before table

Caption
Column 1Column 2
1-11-2
2-12-2
Footer 1Footer 2

Some text after table

', + '

Some text before table

Caption
Column 1Column 2
1-11-2
2-12-2
Footer 1Footer 2

Some text after table

', + ], + 'Table with zebra striping and sort' => [ + '

Some text before table

Caption
Column 1Column 2
1-11-2
2-12-2
Footer 1Footer 2

Some text after table

', + '

Some text before table

Caption
Column 1Column 2
1-11-2
2-12-2
Footer 1Footer 2

Some text after table

', + ], + 'Table with sort' => [ + '

Some text before table

Caption
Column 1Column 2
1-11-2
2-12-2
Footer 1Footer 2

Some text after table

', + '

Some text before table

Caption
Column 1Column 2
1-11-2
2-12-2
Footer 1Footer 2

Some text after table

', ], 'Table with vertical header - only class is added' => [ '
Row 11-11-2
Row 22-12-2
Row 33-13-2
', - '
Row 11-11-2
Row 22-12-2
Row 33-13-2
', + '
Row 11-11-2
Row 22-12-2
Row 33-13-2
', ], 'Table with horizontal and vertical headers' => [ '
Column 1Column 2Column 3
Row 11-21-3
Row 22-22-3
', - '
Column 1Column 2Column 3
Row 11-21-3
Row 22-22-3
', + '
Column 1Column 2Column 3
Row 11-21-3
Row 22-22-3
', ], 'Table without tbody' => [ '
Caption
Column 1Column 2
1-11-2
2-12-2
', - '
Caption
Column 1Column 2
1-11-2
2-12-2
', - ], - 'Table with data-striped' => [ - '
Caption
Column 1Column 2
1-11-2
2-12-2
', - '
Caption
Column 1Column 2
1-11-2
2-12-2
', - ], - 'Table with sort' => [ - '
Caption
Column 1Column 2
1-11-2
2-12-2
', - '
Caption
Column 1Column 2
1-11-2
2-12-2
', + '
Caption
Column 1Column 2
1-11-2
2-12-2
', ], 'Multiple tables' => [ '
Column A1Column A2
A1-1A1-2
A2-1A2-2
B1-1B1-2
B2-1B2-2
Column C1Column C2Column C3
C1-1C1-2C1-3
C2-1C2-2C2-3
', - '
Column A1Column A2
A1-1A1-2
A2-1A2-2
B1-1B1-2
B2-1B2-2
Column C1Column C2Column C3
C1-1C1-2C1-3
C2-1C2-2C2-3
', + '
Column A1Column A2
A1-1A1-2
A2-1A2-2
B1-1B1-2
B2-1B2-2
Column C1Column C2Column C3
C1-1C1-2C1-3
C2-1C2-2C2-3
', ], - 'Table without any th - not processed' => [ - '
1-11-2
2-12-2
3-13-2
', + 'Table without any th - only general classes added' => [ '
1-11-2
2-12-2
3-13-2
', + '
1-11-2
2-12-2
3-13-2
', ], - 'Table with cells spanning multiple rows - not processed' => [ - '
Column 1Column 2Column 3
1-11-21-3
2-22-3
', + 'Table with cells spanning multiple rows - only general classes added' => [ '
Column 1Column 2Column 3
1-11-21-3
2-22-3
', + '
Column 1Column 2Column 3
1-11-21-3
2-22-3
', ], - 'Table with cells spanning multiple columns - not processed' => [ - '
Column 1Column 3
1-11-21-
2-12-22-3
', + 'Table with cells spanning multiple columns - only general classes added' => [ '
Column 1Column 3
1-11-21-
2-12-22-3
', + '
Column 1Column 3
1-11-21-
2-12-22-3
', ], - 'Table with header cells spanning multiple rows - not processed' => [ - '
Column 1Column 2
Column 4
1-11-2
2-12-2
', + 'Table with header cells spanning multiple rows - only general classes added' => [ '
Column 1Column 2
Column 4
1-11-2
2-12-2
', + '
Column 1Column 2
Column 4
1-11-2
2-12-2
', ], - 'Table with header cells spanning multiple columns - not processed' => [ - '
Column 1Column 2Column 3
1-11-3
2-12-22-3
', + 'Table with header cells spanning multiple columns - only general classes added' => [ '
Column 1Column 2Column 3
1-11-3
2-12-22-3
', + '
Column 1Column 2Column 3
1-11-3
2-12-22-3
', ], - 'Table with th cell in body - not processed' => [ - '
Column 1Column 2
1-11-2
2-12-2
', + 'Table with th cell in body - only general classes added' => [ '
Column 1Column 2
1-11-2
2-12-2
', + '
Column 1Column 2
1-11-2
2-12-2
', ], 'Table with multiple header rows - first row is used' => [ '
Column 1Column 2Column 3
Column 4Column 5Column 6
1-11-21-3
', - '
Column 1Column 2Column 3
Column 4Column 5Column 6
1-11-21-3
', + '
Column 1Column 2Column 3
Column 4Column 5Column 6
1-11-21-3
', ], 'Table header with invalid header - only class is added' => [ '
Column 1Column 2
1-11-2
', - '
Column 1Column 2
1-11-2
', + '
Column 1Column 2
1-11-2
', ], 'Table wrapped in HTML comment - not processed' => [ '', From 3ea05a4c95e190970f1d3c7b3542c1d2f22f171f Mon Sep 17 00:00:00 2001 From: Sergii Pavlenko Date: Wed, 26 Oct 2022 12:54:30 +0300 Subject: [PATCH 09/10] EWPP-2556: Fix phpunit test. --- .../tests/src/FunctionalJavascript/WysiwygTableTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php b/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php index 4c7fb339c..bd77784ac 100644 --- a/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php +++ b/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php @@ -309,7 +309,7 @@ protected function openCellPropertiesDialog(string $right_click_selector): void $this->getSession()->switchToIFrame($page->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' cke_menu_panel ') and not(@hidden)]/iframe")->getAttribute('id')); $this->clickLink('Cell'); $this->getSession()->switchToIFrame(); - $web_assert->waitForElementVisible('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' cke_menu_panel ')][last()]"); + $web_assert->waitForElementVisible('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' cke_menu_panel ')][last()]/iframe"); $this->getSession()->switchToIFrame($page->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' cke_menu_panel ') and not(@hidden)][last()]/iframe")->getAttribute('id')); $this->clickLink('Cell Properties'); $this->getSession()->switchToIFrame(); From ce6e5d189797910749bb249a1d66c8e9ec3fa7ae Mon Sep 17 00:00:00 2001 From: Sergii Pavlenko Date: Wed, 26 Oct 2022 14:49:55 +0300 Subject: [PATCH 10/10] EWPP-2556: Add workaround for context submenu appearance in wysiwyg. --- .../tests/src/FunctionalJavascript/WysiwygTableTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php b/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php index bd77784ac..2df2999a8 100644 --- a/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php +++ b/modules/oe_theme_helper/tests/src/FunctionalJavascript/WysiwygTableTest.php @@ -309,7 +309,9 @@ protected function openCellPropertiesDialog(string $right_click_selector): void $this->getSession()->switchToIFrame($page->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' cke_menu_panel ') and not(@hidden)]/iframe")->getAttribute('id')); $this->clickLink('Cell'); $this->getSession()->switchToIFrame(); + // @todo Fix in EWPP-2734 the way for waiting for the showing of the context submenu. $web_assert->waitForElementVisible('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' cke_menu_panel ')][last()]/iframe"); + $this->getSession()->wait(1000); $this->getSession()->switchToIFrame($page->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' cke_menu_panel ') and not(@hidden)][last()]/iframe")->getAttribute('id')); $this->clickLink('Cell Properties'); $this->getSession()->switchToIFrame();