diff --git a/core/misc/tabledrag.es6.js b/core/misc/tabledrag.es6.js index 76643c3a478c70fd3ea61e6d421296b9c75fb7dc..67fc2b2ee71cfd71e6f2b5df2349780f5c27aef7 100644 --- a/core/misc/tabledrag.es6.js +++ b/core/misc/tabledrag.es6.js @@ -558,10 +558,14 @@ case 38: // Safari up arrow. case 63232: { - let $previousRow = $(self.rowObject.element).prev('tr:first-of-type'); + let $previousRow = $(self.rowObject.element) + .prev('tr') + .eq(0); let previousRow = $previousRow.get(0); while (previousRow && $previousRow.is(':hidden')) { - $previousRow = $(previousRow).prev('tr:first-of-type'); + $previousRow = $(previousRow) + .prev('tr') + .eq(0); previousRow = $previousRow.get(0); } if (previousRow) { @@ -577,7 +581,9 @@ previousRow && $previousRow.find('.js-indentation').length ) { - $previousRow = $(previousRow).prev('tr:first-of-type'); + $previousRow = $(previousRow) + .prev('tr') + .eq(0); previousRow = $previousRow.get(0); groupHeight += $previousRow.is(':hidden') ? 0 @@ -618,10 +624,13 @@ case 63233: { let $nextRow = $(self.rowObject.group) .eq(-1) - .next('tr:first-of-type'); + .next('tr') + .eq(0); let nextRow = $nextRow.get(0); while (nextRow && $nextRow.is(':hidden')) { - $nextRow = $(nextRow).next('tr:first-of-type'); + $nextRow = $(nextRow) + .next('tr') + .eq(0); nextRow = $nextRow.get(0); } if (nextRow) { diff --git a/core/misc/tabledrag.js b/core/misc/tabledrag.js index 97ead1c69ab475afa70a20c33c0fb741897fc681..db926660930337c23b4ba53483f074fa82c8089f 100644 --- a/core/misc/tabledrag.js +++ b/core/misc/tabledrag.js @@ -285,10 +285,10 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol case 38: case 63232: { - var $previousRow = $(self.rowObject.element).prev('tr:first-of-type'); + var $previousRow = $(self.rowObject.element).prev('tr').eq(0); var previousRow = $previousRow.get(0); while (previousRow && $previousRow.is(':hidden')) { - $previousRow = $(previousRow).prev('tr:first-of-type'); + $previousRow = $(previousRow).prev('tr').eq(0); previousRow = $previousRow.get(0); } if (previousRow) { @@ -299,7 +299,7 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol if ($(item).is('.tabledrag-root')) { groupHeight = 0; while (previousRow && $previousRow.find('.js-indentation').length) { - $previousRow = $(previousRow).prev('tr:first-of-type'); + $previousRow = $(previousRow).prev('tr').eq(0); previousRow = $previousRow.get(0); groupHeight += $previousRow.is(':hidden') ? 0 : previousRow.offsetHeight; } @@ -329,10 +329,10 @@ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol case 40: case 63233: { - var $nextRow = $(self.rowObject.group).eq(-1).next('tr:first-of-type'); + var $nextRow = $(self.rowObject.group).eq(-1).next('tr').eq(0); var nextRow = $nextRow.get(0); while (nextRow && $nextRow.is(':hidden')) { - $nextRow = $(nextRow).next('tr:first-of-type'); + $nextRow = $(nextRow).next('tr').eq(0); nextRow = $nextRow.get(0); } if (nextRow) { diff --git a/core/modules/system/tests/modules/tabledrag_test/js/tabledrag_test.es6.js b/core/modules/system/tests/modules/tabledrag_test/js/tabledrag_test.es6.js new file mode 100644 index 0000000000000000000000000000000000000000..b3a3af7db91516338c00f9894c1b1e331a94da2c --- /dev/null +++ b/core/modules/system/tests/modules/tabledrag_test/js/tabledrag_test.es6.js @@ -0,0 +1,22 @@ +/** + * @file + * Testing behaviors for tabledrag library. + */ +(function($, Drupal) { + /** + * @type {Drupal~behavior} + * + * @prop {Drupal~behaviorAttach} attach + * Removes a test class from the handle elements to allow verifying that + * dragging operations have been executed. + */ + Drupal.behaviors.tableDragTest = { + attach(context) { + $('.tabledrag-handle', context) + .once('tabledrag-test') + .on('keydown.tabledrag-test', event => { + $(event.currentTarget).removeClass('tabledrag-test-dragging'); + }); + }, + }; +})(jQuery, Drupal); diff --git a/core/modules/system/tests/modules/tabledrag_test/js/tabledrag_test.js b/core/modules/system/tests/modules/tabledrag_test/js/tabledrag_test.js new file mode 100644 index 0000000000000000000000000000000000000000..8e0d8108fe35a32ad7ca9ffb5723d443110886c6 --- /dev/null +++ b/core/modules/system/tests/modules/tabledrag_test/js/tabledrag_test.js @@ -0,0 +1,16 @@ +/** +* DO NOT EDIT THIS FILE. +* See the following change record for more information, +* https://www.drupal.org/node/2815083 +* @preserve +**/ + +(function ($, Drupal) { + Drupal.behaviors.tableDragTest = { + attach: function attach(context) { + $('.tabledrag-handle', context).once('tabledrag-test').on('keydown.tabledrag-test', function (event) { + $(event.currentTarget).removeClass('tabledrag-test-dragging'); + }); + } + }; +})(jQuery, Drupal); \ No newline at end of file diff --git a/core/modules/system/tests/modules/tabledrag_test/src/Form/TableDragTestForm.php b/core/modules/system/tests/modules/tabledrag_test/src/Form/TableDragTestForm.php new file mode 100644 index 0000000000000000000000000000000000000000..a5f1f4630a2d4acb3b8a88cfd1aeb693f63327a7 --- /dev/null +++ b/core/modules/system/tests/modules/tabledrag_test/src/Form/TableDragTestForm.php @@ -0,0 +1,157 @@ +<?php + +namespace Drupal\tabledrag_test\Form; + +use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\State\StateInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Provides a form for draggable table testing. + */ +class TableDragTestForm extends FormBase { + + /** + * The state service. + * + * @var \Drupal\Core\State\StateInterface + */ + protected $state; + + /** + * Constructs a TableDragTestForm object. + * + * @param \Drupal\Core\State\StateInterface $state + * The state service. + */ + public function __construct(StateInterface $state) { + $this->state = $state; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static($container->get('state')); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'tabledrag_test_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form['table'] = [ + '#type' => 'table', + '#header' => [ + [ + 'data' => $this->t('Text'), + 'colspan' => 4, + ], + $this->t('Weight'), + ], + '#tabledrag' => [ + [ + 'action' => 'order', + 'relationship' => 'sibling', + 'group' => 'tabledrag-test-weight', + ], + [ + 'action' => 'match', + 'relationship' => 'parent', + 'group' => 'tabledrag-test-parent', + 'subgroup' => 'tabledrag-test-parent', + 'source' => 'tabledrag-test-id', + 'hidden' => TRUE, + 'limit' => 2, + ], + [ + 'action' => 'depth', + 'relationship' => 'group', + 'group' => 'tabledrag-test-depth', + 'hidden' => TRUE, + ], + ], + '#attributes' => ['id' => 'tabledrag-test-table'], + '#attached' => ['library' => ['tabledrag_test/tabledrag']], + ]; + + // Provide a default set of five rows. + $rows = $this->state->get('tabledrag_test_table', array_flip(range(1, 5))); + foreach ($rows as $id => $row) { + if (!is_array($row)) { + $row = []; + } + + $row += [ + 'parent' => '', + 'weight' => 0, + 'depth' => 0, + 'classes' => [], + 'draggable' => TRUE, + ]; + + if (!empty($row['draggable'])) { + $row['classes'][] = 'draggable'; + } + + $form['table'][$id] = [ + 'title' => [ + 'indentation' => [ + '#theme' => 'indentation', + '#size' => $row['depth'], + ], + '#plain_text' => "Row with id $id", + ], + 'id' => [ + '#type' => 'hidden', + '#value' => $id, + '#attributes' => ['class' => ['tabledrag-test-id']], + ], + 'parent' => [ + '#type' => 'hidden', + '#default_value' => $row['parent'], + '#parents' => ['table', $id, 'parent'], + '#attributes' => ['class' => ['tabledrag-test-parent']], + ], + 'depth' => [ + '#type' => 'hidden', + '#default_value' => $row['depth'], + '#attributes' => ['class' => ['tabledrag-test-depth']], + ], + 'weight' => [ + '#type' => 'weight', + '#default_value' => $row['weight'], + '#attributes' => ['class' => ['tabledrag-test-weight']], + ], + '#attributes' => ['class' => $row['classes']], + ]; + } + + $form['save'] = [ + '#type' => 'submit', + '#value' => $this->t('Save'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $test_table = []; + foreach ($form_state->getValue('table') as $row) { + $test_table[$row['id']] = $row; + } + + $this->state->set('tabledrag_test_table', $test_table); + } + +} diff --git a/core/modules/system/tests/modules/tabledrag_test/tabledrag_test.info.yml b/core/modules/system/tests/modules/tabledrag_test/tabledrag_test.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..98d6bb4b96ba09d8f1024052c94753fd499d6af5 --- /dev/null +++ b/core/modules/system/tests/modules/tabledrag_test/tabledrag_test.info.yml @@ -0,0 +1,6 @@ +type: module +name: 'TableDrag test' +description: 'Draggable table test module.' +core: 8.x +package: Testing +version: VERSION diff --git a/core/modules/system/tests/modules/tabledrag_test/tabledrag_test.libraries.yml b/core/modules/system/tests/modules/tabledrag_test/tabledrag_test.libraries.yml new file mode 100644 index 0000000000000000000000000000000000000000..87876c3c2b8cf79d103f0dbfa2960beaa6175bd0 --- /dev/null +++ b/core/modules/system/tests/modules/tabledrag_test/tabledrag_test.libraries.yml @@ -0,0 +1,6 @@ +tabledrag: + version: VERSION + js: + js/tabledrag_test.js: {} + dependencies: + - core/drupal.tabledrag diff --git a/core/modules/system/tests/modules/tabledrag_test/tabledrag_test.routing.yml b/core/modules/system/tests/modules/tabledrag_test/tabledrag_test.routing.yml new file mode 100644 index 0000000000000000000000000000000000000000..1bb88ff12bc629d3ae20ecd3221aab4340b83eb9 --- /dev/null +++ b/core/modules/system/tests/modules/tabledrag_test/tabledrag_test.routing.yml @@ -0,0 +1,7 @@ +tabledrag_test.test_form: + path: '/tabledrag_test' + defaults: + _form: '\Drupal\tabledrag_test\Form\TableDragTestForm' + _title: 'Draggable table test' + requirements: + _access: 'TRUE' diff --git a/core/tests/Drupal/FunctionalJavascriptTests/TableDrag/TableDragTest.php b/core/tests/Drupal/FunctionalJavascriptTests/TableDrag/TableDragTest.php new file mode 100644 index 0000000000000000000000000000000000000000..ee7fe96c2ba37a4f4bf10ee03d2f0091c5ce11c3 --- /dev/null +++ b/core/tests/Drupal/FunctionalJavascriptTests/TableDrag/TableDragTest.php @@ -0,0 +1,345 @@ +<?php + +namespace Drupal\FunctionalJavascriptTests\TableDrag; + +use Behat\Mink\Element\NodeElement; +use Drupal\FunctionalJavascriptTests\WebDriverTestBase; + +/** + * Tests draggable table. + * + * @group javascript + */ +class TableDragTest extends WebDriverTestBase { + + /** + * Class used to verify that dragging operations are in execution. + */ + const DRAGGING_CSS_CLASS = 'tabledrag-test-dragging'; + + /** + * {@inheritdoc} + */ + protected static $modules = ['tabledrag_test']; + + /** + * The state service. + * + * @var \Drupal\Core\State\StateInterface + */ + protected $state; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->state = $this->container->get('state'); + } + + /** + * Tests accessibility through keyboard of the tabledrag functionality. + */ + public function testKeyboardAccessibility() { + $this->state->set('tabledrag_test_table', array_flip(range(1, 5))); + + $expected_table = [ + ['id' => 1, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 2, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 3, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 4, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 5, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ]; + $this->drupalGet('tabledrag_test'); + $this->assertDraggableTable($expected_table); + + // Nest the row with id 2 as child of row 1. + $this->moveRowWithKeyboard($this->findRowById(2), 'right'); + $expected_table[1] = ['id' => 2, 'weight' => -10, 'parent' => 1, 'indentation' => 1, 'changed' => TRUE]; + $this->assertDraggableTable($expected_table); + + // Nest the row with id 3 as child of row 1. + $this->moveRowWithKeyboard($this->findRowById(3), 'right'); + $expected_table[2] = ['id' => 3, 'weight' => -9, 'parent' => 1, 'indentation' => 1, 'changed' => TRUE]; + $this->assertDraggableTable($expected_table); + + // Nest the row with id 3 as child of row 2. + $this->moveRowWithKeyboard($this->findRowById(3), 'right'); + $expected_table[2] = ['id' => 3, 'weight' => -10, 'parent' => 2, 'indentation' => 2, 'changed' => TRUE]; + $this->assertDraggableTable($expected_table); + + // Nesting should be allowed to maximum level 2. + $this->moveRowWithKeyboard($this->findRowById(4), 'right', 4); + $expected_table[3] = ['id' => 4, 'weight' => -9, 'parent' => 2, 'indentation' => 2, 'changed' => TRUE]; + $this->assertDraggableTable($expected_table); + + // Re-order children of row 1. + $this->moveRowWithKeyboard($this->findRowById(4), 'up'); + $expected_table[2] = ['id' => 4, 'weight' => -10, 'parent' => 2, 'indentation' => 2, 'changed' => TRUE]; + $expected_table[3] = ['id' => 3, 'weight' => -9, 'parent' => 2, 'indentation' => 2, 'changed' => TRUE]; + $this->assertDraggableTable($expected_table); + + // Move back the row 3 to the 1st level. + $this->moveRowWithKeyboard($this->findRowById(3), 'left'); + $expected_table[3] = ['id' => 3, 'weight' => -9, 'parent' => 1, 'indentation' => 1, 'changed' => TRUE]; + $this->assertDraggableTable($expected_table); + + $this->moveRowWithKeyboard($this->findRowById(3), 'left'); + $expected_table[0] = ['id' => 1, 'weight' => -10, 'parent' => '', 'indentation' => 0, 'changed' => FALSE]; + $expected_table[3] = ['id' => 3, 'weight' => -9, 'parent' => '', 'indentation' => 0, 'changed' => TRUE]; + $expected_table[4] = ['id' => 5, 'weight' => -8, 'parent' => '', 'indentation' => 0, 'changed' => FALSE]; + $this->assertDraggableTable($expected_table); + + // Move row 3 to the last position. + $this->moveRowWithKeyboard($this->findRowById(3), 'down'); + $expected_table[3] = ['id' => 5, 'weight' => -9, 'parent' => '', 'indentation' => 0, 'changed' => FALSE]; + $expected_table[4] = ['id' => 3, 'weight' => -8, 'parent' => '', 'indentation' => 0, 'changed' => TRUE]; + $this->assertDraggableTable($expected_table); + + // Nothing happens when trying to move the last row further down. + $this->moveRowWithKeyboard($this->findRowById(3), 'down'); + $this->assertDraggableTable($expected_table); + + // Nest row 3 under 5. The max depth allowed should be 1. + $this->moveRowWithKeyboard($this->findRowById(3), 'right', 3); + $expected_table[4] = ['id' => 3, 'weight' => -10, 'parent' => 5, 'indentation' => 1, 'changed' => TRUE]; + $this->assertDraggableTable($expected_table); + + // The first row of the table cannot be nested. + $this->moveRowWithKeyboard($this->findRowById(1), 'right'); + $this->assertDraggableTable($expected_table); + + // Move a row which has nested children. The children should move with it, + // with nesting preserved. Swap the order of the top-level rows by moving + // row 1 to after row 3. + $this->moveRowWithKeyboard($this->findRowById(1), 'down', 2); + $expected_table[0] = ['id' => 5, 'weight' => -10, 'parent' => '', 'indentation' => 0, 'changed' => FALSE]; + $expected_table[3] = $expected_table[1]; + $expected_table[1] = $expected_table[4]; + $expected_table[4] = $expected_table[2]; + $expected_table[2] = ['id' => 1, 'weight' => -9, 'parent' => '', 'indentation' => 0, 'changed' => TRUE]; + $this->assertDraggableTable($expected_table); + } + + /** + * Tests the root and leaf behaviors for rows. + */ + public function testRootLeafDraggableRowsWithKeyboard() { + $this->state->set('tabledrag_test_table', [ + 1 => [], + 2 => ['parent' => 1, 'depth' => 1, 'classes' => ['tabledrag-leaf']], + 3 => ['parent' => 1, 'depth' => 1], + 4 => [], + 5 => ['classes' => ['tabledrag-root']], + ]); + + $this->drupalGet('tabledrag_test'); + $expected_table = [ + ['id' => 1, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 2, 'weight' => 0, 'parent' => 1, 'indentation' => 1, 'changed' => FALSE], + ['id' => 3, 'weight' => 0, 'parent' => 1, 'indentation' => 1, 'changed' => FALSE], + ['id' => 4, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ['id' => 5, 'weight' => 0, 'parent' => '', 'indentation' => 0, 'changed' => FALSE], + ]; + $this->assertDraggableTable($expected_table); + + // Rows marked as root cannot be moved as children of another row. + $this->moveRowWithKeyboard($this->findRowById(5), 'right'); + $this->assertDraggableTable($expected_table); + + // Rows marked as leaf cannot have children. Trying to move the row #3 + // as child of #2 should have no results. + $this->moveRowWithKeyboard($this->findRowById(3), 'right'); + $this->assertDraggableTable($expected_table); + + // Leaf can be still swapped and moved to first level. + $this->moveRowWithKeyboard($this->findRowById(2), 'down'); + $this->moveRowWithKeyboard($this->findRowById(2), 'left'); + $expected_table[0]['weight'] = -10; + $expected_table[1]['id'] = 3; + $expected_table[1]['weight'] = -10; + $expected_table[2] = ['id' => 2, 'weight' => -9, 'parent' => '', 'indentation' => 0, 'changed' => TRUE]; + $expected_table[3]['weight'] = -8; + $expected_table[4]['weight'] = -7; + $this->assertDraggableTable($expected_table); + + // Root rows can have children. + $this->moveRowWithKeyboard($this->findRowById(4), 'down'); + $this->moveRowWithKeyboard($this->findRowById(4), 'right'); + $expected_table[3]['id'] = 5; + $expected_table[4] = ['id' => 4, 'weight' => -10, 'parent' => 5, 'indentation' => 1, 'changed' => TRUE]; + $this->assertDraggableTable($expected_table); + } + + /** + * Tests the warning that appears upon making changes to a tabledrag table. + */ + public function testTableDragChangedWarning() { + $this->drupalGet('tabledrag_test'); + + // By default no text is visible. + $this->assertSession()->pageTextNotContains('You have unsaved changes.'); + // Try to make a non-allowed action, like moving further down the last row. + // No changes happen, so no message should be shown. + $this->moveRowWithKeyboard($this->findRowById(5), 'down'); + $this->assertSession()->pageTextNotContains('You have unsaved changes.'); + + // Make a change. The message will appear. + $this->moveRowWithKeyboard($this->findRowById(5), 'right'); + $this->assertSession()->pageTextContainsOnce('You have unsaved changes.'); + + // Make another change, the text will stay visible and appear only once. + $this->moveRowWithKeyboard($this->findRowById(2), 'up'); + $this->assertSession()->pageTextContainsOnce('You have unsaved changes.'); + } + + /** + * Asserts the whole structure of the draggable test table. + * + * @param array $structure + * The table structure. Each entry represents a row and consists of: + * - id: the expected value for the ID hidden field. + * - weight: the expected row weight. + * - parent: the expected parent ID for the row. + * - indentation: how many indents the row should have. + * - changed: whether or not the row should have been marked as changed. + */ + protected function assertDraggableTable(array $structure) { + $rows = $this->getSession()->getPage()->findAll('xpath', '//table[@id="tabledrag-test-table"]/tbody/tr'); + $this->assertSession()->elementsCount('xpath', '//table[@id="tabledrag-test-table"]/tbody/tr', count($structure)); + + foreach ($structure as $delta => $expected) { + $this->assertTableRow($rows[$delta], $expected['id'], $expected['weight'], $expected['parent'], $expected['indentation'], $expected['changed']); + } + } + + /** + * Asserts the values of a draggable row. + * + * @param \Behat\Mink\Element\NodeElement $row + * The row element to assert. + * @param string $id + * The expected value for the ID hidden input of the row. + * @param int $weight + * The expected weight of the row. + * @param string $parent + * The expected parent ID. + * @param int $indentation + * The expected indentation of the row. + * @param bool $changed + * Whether or not the row should have been marked as changed. + */ + protected function assertTableRow(NodeElement $row, $id, $weight, $parent = '', $indentation = 0, $changed = FALSE) { + // Assert that the row position is correct by checking that the id + // corresponds. + $this->assertSession()->hiddenFieldValueEquals("table[$id][id]", $id, $row); + $this->assertSession()->hiddenFieldValueEquals("table[$id][parent]", $parent, $row); + $this->assertSession()->fieldValueEquals("table[$id][weight]", $weight, $row); + $this->assertSession()->elementsCount('css', '.js-indentation.indentation', $indentation, $row); + // A row is marked as changed when the related markup is present. + $this->assertSession()->elementsCount('css', 'abbr.tabledrag-changed', (int) $changed, $row); + } + + /** + * Finds a row in the test table by the row ID. + * + * @param string $id + * The ID of the row. + * + * @return \Behat\Mink\Element\NodeElement + * The row element. + */ + protected function findRowById($id) { + $xpath = "//table[@id='tabledrag-test-table']/tbody/tr[.//input[@name='table[$id][id]']]"; + $row = $this->getSession()->getPage()->find('xpath', $xpath); + $this->assertNotEmpty($row); + return $row; + } + + /** + * Moves a row through the keyboard. + * + * @param \Behat\Mink\Element\NodeElement $row + * The row to move. + * @param string $arrow + * The arrow button to use to move the row. Either one of 'left', 'right', + * 'up' or 'down'. + * @param int $repeat + * (optional) How many times to press the arrow button. Defaults to 1. + */ + protected function moveRowWithKeyboard(NodeElement $row, $arrow, $repeat = 1) { + $keys = [ + 'left' => 37, + 'right' => 39, + 'up' => 38, + 'down' => 40, + ]; + if (!isset($keys[$arrow])) { + throw new \InvalidArgumentException('The arrow parameter must be one of "left", "right", "up" or "down".'); + } + + $key = $keys[$arrow]; + + $handle = $row->find('css', 'a.tabledrag-handle'); + $handle->focus(); + + for ($i = 0; $i < $repeat; $i++) { + $this->markRowHandleForDragging($handle); + $handle->keyDown($key); + $handle->keyUp($key); + $this->waitUntilDraggingCompleted($handle); + } + + $handle->blur(); + } + + /** + * Marks a row handle for dragging. + * + * The handle is marked by adding a css class that is removed by an helper + * js library once the dragging is over. + * + * @param \Behat\Mink\Element\NodeElement $handle + * The draggable row handle element. + * + * @throws \Exception + * Thrown when the class is not added successfully to the handle. + */ + protected function markRowHandleForDragging(NodeElement $handle) { + $class = self::DRAGGING_CSS_CLASS; + $script = <<<JS +document.evaluate("{$handle->getXpath()}", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null) + .singleNodeValue.classList.add('{$class}'); +JS; + + $this->getSession()->executeScript($script); + $has_class = $this->getSession()->getPage()->waitFor(1, function () use ($handle, $class) { + return $handle->hasClass($class); + }); + + if (!$has_class) { + throw new \Exception(sprintf('Dragging css class was not added on handle "%s".', $handle->getXpath())); + } + } + + /** + * Waits until the dragging operations are finished on a row handle. + * + * @param \Behat\Mink\Element\NodeElement $handle + * The draggable row handle element. + * + * @throws \Exception + * Thrown when the dragging operations are not completed on time. + */ + protected function waitUntilDraggingCompleted(NodeElement $handle) { + $class_removed = $this->getSession()->getPage()->waitFor(1, function () use ($handle) { + return !$handle->hasClass($this::DRAGGING_CSS_CLASS); + }); + + if (!$class_removed) { + throw new \Exception(sprintf('Dragging operations did not complete on time on handle %s', $handle->getXpath())); + } + } + +}