diff --git a/core/core.services.yml b/core/core.services.yml index 9c60943d25..84ca680fba 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1720,3 +1720,6 @@ services: arguments: ['@keyvalue.expirable', '@lock', '@request_stack', '%tempstore.expire%'] tags: - { name: backend_overridable } + table_sort: + class: Drupal\Core\Utility\TableSort + arguments: ['@renderer', '@request_stack', '@string_translation'] diff --git a/core/includes/tablesort.inc b/core/includes/tablesort.inc index ee50869006..de64187aa7 100644 --- a/core/includes/tablesort.inc +++ b/core/includes/tablesort.inc @@ -9,18 +9,20 @@ * column. */ -use Drupal\Component\Render\FormattableMarkup; -use Drupal\Core\Url; -use Drupal\Component\Utility\UrlHelper; - /** * Initializes the table sort context. + * + * @deprecated as of Drupal 8.7.x and will be removed before Drupal 9.0.0. + * Instead, get a TableSort utility injected into your service from the + * container and call init() on it. For example, + * $table_sort->init($header); + * + * @see \Drupal\Core\Utility\TableSortInterface::init() + * @see https://www.drupal.org/node/3009182 */ function tablesort_init($header) { - $ts = tablesort_get_order($header); - $ts['sort'] = tablesort_get_sort($header); - $ts['query'] = tablesort_get_query_parameters(); - return $ts; + @trigger_error(__FUNCTION__ . ' is deprecated in Drupal 8.7.x and will be removed before Drupal 9.0.0. Instead, get a TableSort utility injected into your service from the container and call init() on it. For example, $table_sort->init($header). See https://www.drupal.org/node/3009182', E_USER_DEPRECATED); + return \Drupal::service('table_sort')->init($header); } /** @@ -37,39 +39,18 @@ function tablesort_init($header) { * An array of column headers in the format described in '#type' => 'table'. * @param array $ts * The current table sort context as returned from tablesort_init(). + * + * @deprecated as of Drupal 8.7.x and will be removed before Drupal 9.0.0. + * Instead, get a TableSort utility injected into your service from the + * container and call header() on it. For example, + * $table_sort->header($cell_content, $cell_attributes, $header, $ts); + * + * @see \Drupal\Core\Utility\TableSortInterface::header() + * @see https://www.drupal.org/node/3009182 */ function tablesort_header(&$cell_content, array &$cell_attributes, array $header, array $ts) { - // Special formatting for the currently sorted column header. - if (isset($cell_attributes['field'])) { - $title = t('sort by @s', ['@s' => $cell_content]); - if ($cell_content == $ts['name']) { - // aria-sort is a WAI-ARIA property that indicates if items in a table - // or grid are sorted in ascending or descending order. See - // http://www.w3.org/TR/wai-aria/states_and_properties#aria-sort - $cell_attributes['aria-sort'] = ($ts['sort'] == 'asc') ? 'ascending' : 'descending'; - $ts['sort'] = (($ts['sort'] == 'asc') ? 'desc' : 'asc'); - $cell_attributes['class'][] = 'is-active'; - $tablesort_indicator = [ - '#theme' => 'tablesort_indicator', - '#style' => $ts['sort'], - ]; - $image = \Drupal::service('renderer')->render($tablesort_indicator); - } - else { - // If the user clicks a different header, we want to sort ascending initially. - $ts['sort'] = 'asc'; - $image = ''; - } - $cell_content = \Drupal::l(new FormattableMarkup('@cell_content@image', ['@cell_content' => $cell_content, '@image' => $image]), new Url('', [], [ - 'attributes' => ['title' => $title], - 'query' => array_merge($ts['query'], [ - 'sort' => $ts['sort'], - 'order' => $cell_content, - ]), - ])); - - unset($cell_attributes['field'], $cell_attributes['sort']); - } + @trigger_error(__FUNCTION__ . ' is deprecated in Drupal 8.7.x and will be removed before Drupal 9.0.0. Instead, get a TableSort utility injected into your service from the container and call header() on it. For example, $table_sort->header($cell_content, $cell_attributes, $header, $ts). See https://www.drupal.org/node/3009182', E_USER_DEPRECATED); + \Drupal::service('table_sort')->header($cell_content, $cell_attributes, $header, $ts); } /** @@ -78,9 +59,18 @@ function tablesort_header(&$cell_content, array &$cell_attributes, array $header * @return * A URL query parameter array that consists of all components of the current * page request except for those pertaining to table sorting. + * + * @deprecated as of Drupal 8.7.x and will be removed before Drupal 9.0.0. + * Instead, get a TableSort utility injected into your service from the + * container and call getQueryParameters() on it. For example, + * $table_sort->getQueryParameters(); + * + * @see \Drupal\Core\Utility\TableSortInterface::getQueryParameters() + * @see https://www.drupal.org/node/3009182 */ function tablesort_get_query_parameters() { - return UrlHelper::filterQueryParameters(\Drupal::request()->query->all(), ['sort', 'order']); + @trigger_error(__FUNCTION__ . ' is deprecated in Drupal 8.7.x and will be removed before Drupal 9.0.0. Instead, get a TableSort utility injected into your service from the container and call getQueryParameters() on it. For example, $table_sort->getQueryParameters(). See https://www.drupal.org/node/3009182', E_USER_DEPRECATED); + return \Drupal::service('table_sort')->getQueryParameters(); } /** @@ -93,31 +83,18 @@ function tablesort_get_query_parameters() { * An associative array describing the criterion, containing the keys: * - "name": The localized title of the table column. * - "sql": The name of the database field to sort on. + * + * @deprecated as of Drupal 8.7.x and will be removed before Drupal 9.0.0. + * Instead, get a TableSort utility injected into your service from the + * container and call getOrder() on it. For example, + * $table_sort->getOrder($headers); + * + * @see \Drupal\Core\Utility\TableSortInterface::getOrder() + * @see https://www.drupal.org/node/3009182 */ function tablesort_get_order($headers) { - $order = \Drupal::request()->query->get('order', ''); - foreach ($headers as $header) { - if (is_array($header)) { - if (isset($header['data']) && $order == $header['data']) { - $default = $header; - break; - } - - if (empty($default) && isset($header['sort']) && ($header['sort'] == 'asc' || $header['sort'] == 'desc')) { - $default = $header; - } - } - } - - if (!isset($default)) { - $default = reset($headers); - if (!is_array($default)) { - $default = ['data' => $default]; - } - } - - $default += ['data' => NULL, 'field' => NULL]; - return ['name' => $default['data'], 'sql' => $default['field']]; + @trigger_error(__FUNCTION__ . ' is deprecated in Drupal 8.7.x and will be removed before Drupal 9.0.0. Instead, get a TableSort utility injected into your service from the container and call getOrder() on it. For example, $table_sort->getOrder($headers). See https://www.drupal.org/node/3009182', E_USER_DEPRECATED); + return \Drupal::service('table_sort')->getOrder($headers); } /** @@ -128,22 +105,16 @@ function tablesort_get_order($headers) { * * @return * The current sort direction ("asc" or "desc"). + * + * @deprecated as of Drupal 8.7.x and will be removed before Drupal 9.0.0. + * Instead, get a TableSort utility injected into your service from the + * container and call getSort() on it. For example, + * $table_sort->getSort($headers); + * + * @see \Drupal\Core\Utility\TableSortInterface::getSort() + * @see https://www.drupal.org/node/3009182 */ function tablesort_get_sort($headers) { - $query = \Drupal::request()->query; - if ($query->has('sort')) { - return (strtolower($query->get('sort')) == 'desc') ? 'desc' : 'asc'; - } - // The user has not specified a sort. Use the default for the currently sorted - // header if specified; otherwise use "asc". - else { - // Find out which header is currently being sorted. - $ts = tablesort_get_order($headers); - foreach ($headers as $header) { - if (is_array($header) && isset($header['data']) && $header['data'] == $ts['name'] && isset($header['sort'])) { - return $header['sort']; - } - } - } - return 'asc'; + @trigger_error(__FUNCTION__ . ' is deprecated in Drupal 8.7.x and will be removed before Drupal 9.0.0. Instead, get a TableSort utility injected into your service from the container and call getSort() on it. For example, $table_sort->getSort($headers). See https://www.drupal.org/node/3009182', E_USER_DEPRECATED); + return \Drupal::service('table_sort')->getSort($headers); } diff --git a/core/includes/theme.inc b/core/includes/theme.inc index de21bfe911..6f6da5c866 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -944,7 +944,10 @@ function template_preprocess_table(&$variables) { $ts = []; $header_columns = 0; if (!empty($variables['header'])) { - $ts = tablesort_init($variables['header']); + /** @var \Drupal\Core\Utility\TableSortInterface $table_sort */ + $table_sort = \Drupal::service('table_sort'); + + $ts = $table_sort->init($variables['header']); // Use a separate index with responsive classes as headers // may be associative. @@ -988,9 +991,9 @@ function template_preprocess_table(&$variables) { } } - tablesort_header($cell_content, $cell, $variables['header'], $ts); + $table_sort->header($cell_content, $cell, $variables['header'], $ts); - // tablesort_header() removes the 'sort' and 'field' keys. + // TableSort::header() removes the 'sort' and 'field' keys. $cell_attributes = new Attribute($cell); } $variables['header'][$col_key] = []; diff --git a/core/lib/Drupal/Core/Database/Query/TableSortExtender.php b/core/lib/Drupal/Core/Database/Query/TableSortExtender.php index e0172000fb..9c8b67e862 100644 --- a/core/lib/Drupal/Core/Database/Query/TableSortExtender.php +++ b/core/lib/Drupal/Core/Database/Query/TableSortExtender.php @@ -10,12 +10,18 @@ class TableSortExtender extends SelectExtender { /** - * The array of fields that can be sorted by. + * The table sorting service. + * + * @var \Drupal\Core\Utility\TableSortInterface */ - protected $header = []; + protected $tableSort; + /** + * {@inheritdoc} + */ public function __construct(SelectInterface $query, Connection $connection) { parent::__construct($query, $connection); + $this->tableSort = \Drupal::service('table_sort'); // Add convenience tag to mark that this is an extended query. We have to // do this in the constructor to ensure that it is set before preExecute() @@ -35,67 +41,17 @@ public function __construct(SelectInterface $query, Connection $connection) { * @see table.html.twig */ public function orderByHeader(array $header) { - $this->header = $header; - $ts = $this->init(); - if (!empty($ts['sql'])) { + $context = $this->tableSort->init($header); + if (!empty($context['sql'])) { // Based on code from \Drupal\Core\Database\Connection::escapeTable(), // but this can also contain a dot. - $field = preg_replace('/[^A-Za-z0-9_.]+/', '', $ts['sql']); + $field = preg_replace('/[^A-Za-z0-9_.]+/', '', $context['sql']); // orderBy() will ensure that only ASC/DESC values are accepted, so we // don't need to sanitize that here. - $this->orderBy($field, $ts['sort']); + $this->orderBy($field, $context['sort']); } return $this; } - /** - * Initialize the table sort context. - */ - protected function init() { - $ts = $this->order(); - $ts['sort'] = $this->getSort(); - $ts['query'] = $this->getQueryParameters(); - return $ts; - } - - /** - * Determine the current sort direction. - * - * @return - * The current sort direction ("asc" or "desc"). - * - * @see tablesort_get_sort() - */ - protected function getSort() { - return tablesort_get_sort($this->header); - } - - /** - * Compose a URL query parameter array to append to table sorting requests. - * - * @return - * A URL query parameter array that consists of all components of the current - * page request except for those pertaining to table sorting. - * - * @see tablesort_get_query_parameters() - */ - protected function getQueryParameters() { - return tablesort_get_query_parameters(); - } - - /** - * Determine the current sort criterion. - * - * @return - * An associative array describing the criterion, containing the keys: - * - "name": The localized title of the table column. - * - "sql": The name of the database field to sort on. - * - * @see tablesort_get_order() - */ - protected function order() { - return tablesort_get_order($this->header); - } - } diff --git a/core/lib/Drupal/Core/Entity/Query/QueryBase.php b/core/lib/Drupal/Core/Entity/Query/QueryBase.php index 06dfb8f20f..6bfe500a34 100644 --- a/core/lib/Drupal/Core/Entity/Query/QueryBase.php +++ b/core/lib/Drupal/Core/Entity/Query/QueryBase.php @@ -329,8 +329,9 @@ public function tableSort(&$headers) { } } - $order = tablesort_get_order($headers); - $direction = tablesort_get_sort($headers); + $table_sort = \Drupal::service('table_sort'); + $order = $table_sort->getOrder($headers); + $direction = $table_sort->getSort($headers); foreach ($headers as $header) { if (is_array($header) && ($header['data'] == $order['name'])) { $this->sort($header['specifier'], $direction, isset($header['langcode']) ? $header['langcode'] : NULL); diff --git a/core/lib/Drupal/Core/Utility/TableSort.php b/core/lib/Drupal/Core/Utility/TableSort.php new file mode 100644 index 0000000000..f59cd39f68 --- /dev/null +++ b/core/lib/Drupal/Core/Utility/TableSort.php @@ -0,0 +1,173 @@ +renderer = $renderer; + $this->requestStack = $request_stack; + $this->stringTranslation = $translation; + } + + /** + * Gets current request. + * + * @param \Symfony\Component\HttpFoundation\Request|null $request + * A current request (optional). + * + * @return null|\Symfony\Component\HttpFoundation\Request + * The current request object. + */ + protected function request(Request $request = NULL) { + if ($request) { + return $request; + } + return $this->requestStack->getCurrentRequest(); + } + + /** + * {@inheritdoc} + */ + public function init(array $header, Request $request = NULL) { + $request = $this->request($request); + $context = $this->getOrder($header, $request); + $context['sort'] = $this->getSort($header, $request); + $context['query'] = $this->getQueryParameters($request); + return $context; + } + + /** + * {@inheritdoc} + */ + public function header(&$cell_content, array &$cell_attributes, array $header, array $context) { + // Special formatting for the currently sorted column header. + if (isset($cell_attributes['field'])) { + $title = $this->t('sort by @s', ['@s' => $cell_content]); + if ($cell_content == $context['name']) { + // aria-sort is a WAI-ARIA property that indicates if items in a table + // or grid are sorted in ascending or descending order. See + // http://www.w3.org/TR/wai-aria/states_and_properties#aria-sort + $cell_attributes['aria-sort'] = ($context['sort'] == self::ASC) ? 'ascending' : 'descending'; + $context['sort'] = (($context['sort'] == self::ASC) ? self::DESC : self::ASC); + $cell_attributes['class'][] = 'is-active'; + $tablesort_indicator = [ + '#theme' => 'tablesort_indicator', + '#style' => $context['sort'], + ]; + $image = $this->renderer->render($tablesort_indicator); + } + else { + // If the user clicks a different header, we want to sort ascending + // initially. + $context['sort'] = self::ASC; + $image = ''; + } + $cell_content = Link::createFromRoute(new FormattableMarkup('@cell_content@image', ['@cell_content' => $cell_content, '@image' => $image]), '', [], [ + 'attributes' => ['title' => $title], + 'query' => array_merge($context['query'], [ + 'sort' => $context['sort'], + 'order' => $cell_content, + ]), + ]); + + unset($cell_attributes['field'], $cell_attributes['sort']); + } + } + + /** + * {@inheritdoc} + */ + public function getQueryParameters(Request $request = NULL) { + return UrlHelper::filterQueryParameters($this->request($request)->query->all(), ['sort', 'order']); + } + + /** + * {@inheritdoc} + */ + public function getOrder(array $headers, Request $request = NULL) { + $order = $this->request($request)->query->get('order', ''); + foreach ($headers as $header) { + if (is_array($header)) { + if (isset($header['data']) && $order == $header['data']) { + $default = $header; + break; + } + + if (empty($default) && isset($header['sort']) && in_array($header['sort'], [self::ASC, self::DESC])) { + $default = $header; + } + } + } + + if (!isset($default)) { + $default = reset($headers); + if (!is_array($default)) { + $default = ['data' => $default]; + } + } + + $default += ['data' => NULL, 'field' => NULL]; + return ['name' => $default['data'], 'sql' => $default['field']]; + } + + /** + * {@inheritdoc} + */ + public function getSort(array $headers, Request $request = NULL) { + $request = $this->request($request); + $query = $request->query; + if ($query->has('sort')) { + return (strtolower($query->get('sort')) == self::DESC) ? self::DESC : self::ASC; + } + // The user has not specified a sort. Use the default for the currently + // sorted header if specified; otherwise use "asc". + // Find out which header is currently being sorted. + $order = $this->getOrder($headers, $request); + foreach ($headers as $header) { + if (is_array($header) && isset($header['data']) && $header['data'] == $order['name'] && isset($header['sort'])) { + return $header['sort']; + } + } + return self::ASC; + } + +} diff --git a/core/lib/Drupal/Core/Utility/TableSortInterface.php b/core/lib/Drupal/Core/Utility/TableSortInterface.php new file mode 100644 index 0000000000..26f6e935c5 --- /dev/null +++ b/core/lib/Drupal/Core/Utility/TableSortInterface.php @@ -0,0 +1,90 @@ + 'table'. + * @param \Symfony\Component\HttpFoundation\Request|null $request + * A current request (optional). + * + * @return array + * The current table sort context. + */ + public function init(array $header, Request $request = NULL); + + /** + * Formats a column header. + * + * If the cell in question is the column header for the current sort + * criterion, it gets special formatting. All possible sort criteria become + * links. + * + * @param string $cell_content + * The cell content to format. Passed by reference. + * @param array $cell_attributes + * The cell attributes. Passed by reference. + * @param array $header + * An array of column headers in the format described in '#type' => 'table'. + * @param array $context + * The current table sort context as returned from init() method. + * + * @throws \Exception + * + * @see init() + */ + public function header(&$cell_content, array &$cell_attributes, array $header, array $context); + + /** + * Composes a URL query parameter array for table sorting links. + * + * @param \Symfony\Component\HttpFoundation\Request|null $request + * A current request (optional). + * + * @return array + * A URL query parameter array that consists of all components of the + * current page request except for those pertaining to table sorting. + */ + public function getQueryParameters(Request $request = NULL); + + /** + * Determines the current sort criterion. + * + * @param array $headers + * An array of column headers in the format described in '#type' => 'table'. + * @param \Symfony\Component\HttpFoundation\Request|null $request + * A current request (optional). + * + * @return array + * An associative array describing the criterion, containing the keys: + * - "name": The localized title of the table column. + * - "sql": The name of the database field to sort on. + */ + public function getOrder(array $headers, Request $request = NULL); + + /** + * Determines the current sort direction. + * + * @param array $headers + * An array of column headers in the format described in '#type' => 'table'. + * @param \Symfony\Component\HttpFoundation\Request|null $request + * A current request (optional). + * + * @return string + * The current sort direction ("asc" or "desc"). + */ + public function getSort(array $headers, Request $request = NULL); + +} diff --git a/core/modules/views/views.theme.inc b/core/modules/views/views.theme.inc index cb9aee7ba7..1243c53336 100644 --- a/core/modules/views/views.theme.inc +++ b/core/modules/views/views.theme.inc @@ -449,7 +449,7 @@ function template_preprocess_views_view_table(&$variables) { // working URLs. $route_name = !empty($view->live_preview) ? '' : ''; - $query = tablesort_get_query_parameters(); + $query = \Drupal::service('table_sort')->getQueryParameters(); if (isset($view->exposed_raw_input)) { $query += $view->exposed_raw_input; } diff --git a/core/tests/Drupal/KernelTests/Core/Render/Element/TableSortExtenderTest.php b/core/tests/Drupal/KernelTests/Core/Render/Element/TableSortExtenderTest.php index 4291179f40..636feebacd 100644 --- a/core/tests/Drupal/KernelTests/Core/Render/Element/TableSortExtenderTest.php +++ b/core/tests/Drupal/KernelTests/Core/Render/Element/TableSortExtenderTest.php @@ -14,7 +14,7 @@ class TableSortExtenderTest extends KernelTestBase { /** - * Tests tablesort_init(). + * Tests \Drupal\Core\Utility\TableSort::init(). */ public function testTableSortInit() { @@ -32,7 +32,7 @@ public function testTableSortInit() { $request = Request::createFromGlobals(); $request->query->replace([]); \Drupal::getContainer()->get('request_stack')->push($request); - $ts = tablesort_init($headers); + $ts = \Drupal::getContainer()->get('table_sort')->init($headers, $request); $this->verbose(strtr('$ts:
!ts
', ['!ts' => Html::escape(var_export($ts, TRUE))])); $this->assertEqual($ts, $expected_ts, 'Simple table headers sorted correctly.'); @@ -45,7 +45,7 @@ public function testTableSortInit() { 'order' => 'bar', ]); \Drupal::getContainer()->get('request_stack')->push($request); - $ts = tablesort_init($headers); + $ts = \Drupal::getContainer()->get('table_sort')->init($headers, $request); $this->verbose(strtr('$ts:
!ts
', ['!ts' => Html::escape(var_export($ts, TRUE))])); $this->assertEqual($ts, $expected_ts, 'Simple table headers plus non-overriding $_GET parameters sorted correctly.'); @@ -61,7 +61,7 @@ public function testTableSortInit() { \Drupal::getContainer()->get('request_stack')->push($request); $expected_ts['sort'] = 'desc'; $expected_ts['query'] = ['alpha' => 'beta']; - $ts = tablesort_init($headers); + $ts = \Drupal::getContainer()->get('table_sort')->init($headers, $request); $this->verbose(strtr('$ts:
!ts
', ['!ts' => Html::escape(var_export($ts, TRUE))])); $this->assertEqual($ts, $expected_ts, 'Simple table headers plus $_GET parameters sorted correctly.'); @@ -87,7 +87,7 @@ public function testTableSortInit() { 'order' => '2', ]); \Drupal::getContainer()->get('request_stack')->push($request); - $ts = tablesort_init($headers); + $ts = \Drupal::getContainer()->get('table_sort')->init($headers, $request); $expected_ts = [ 'name' => '2', 'sql' => 'two', @@ -106,7 +106,7 @@ public function testTableSortInit() { 'order' => 'bar', ]); \Drupal::getContainer()->get('request_stack')->push($request); - $ts = tablesort_init($headers); + $ts = \Drupal::getContainer()->get('table_sort')->init($headers, $request); $expected_ts = [ 'name' => '1', 'sql' => 'one', @@ -133,7 +133,7 @@ public function testTableSortInit() { 'sort' => 'asc', 'query' => ['alpha' => 'beta'], ]; - $ts = tablesort_init($headers); + $ts = \Drupal::getContainer()->get('table_sort')->init($headers, $request); $this->verbose(strtr('$ts:
!ts
', ['!ts' => Html::escape(var_export($ts, TRUE))])); $this->assertEqual($ts, $expected_ts, 'Complex table headers plus $_GET parameters sorted correctly.'); }