diff --git a/css/overview.css b/css/overview.css new file mode 100644 index 0000000..f53c610 --- /dev/null +++ b/css/overview.css @@ -0,0 +1,10 @@ +.overview-details { + border: 0; + margin: 0; +} + +.overview-details summary { + font-weight: normal; + text-transform: none; + padding: 0; +} diff --git a/js/overview.js b/js/overview.js new file mode 100644 index 0000000..a11712f --- /dev/null +++ b/js/overview.js @@ -0,0 +1,67 @@ +/** + * @file + * Paragraphs Collection overview behaviors. + */ + +(function ($, Drupal) { + + "use strict"; + + /** + * Filters the overview table by input search filters. + * + * Target table: .table-filter[data-table] + * Text search input: input.table-filter-text + * Group search select: select.table-filter-group-select + * Source text: .table-filter-text-source + * Source group: .table-filter-group-source + * + * @type {Drupal~behavior} + */ + Drupal.behaviors.tableFilterByText = { + attach: function (context, settings) { + var $filters = $('.table-filter').once('table-filter'); + var $table = $($filters.attr('data-table')); + // var $selects = $ + var $text_input = $('input.table-filter-text').once('table-filter-text'); + var $group_select = $('select.table-filter-group-select').once('table-filter-group-select'); + var $rows; + + function filterItemList() { + var group_value; + if (typeof $group_select.val() !== 'undefined') { + group_value = $group_select.val().toLowerCase(); + } + var text_value = $text_input.val().toLowerCase(); + + function showItemRow(index, row) { + var $row = $(row); + var $group_sources = $row.find('.table-filter-group-source'); + var $text_sources = $row.find('.table-filter-text-source'); + var group_array = $group_sources.map(function() { + return $(this).text().toLowerCase(); + }).get(); + + if (group_value && group_array.indexOf(group_value) == -1) { + $row.hide(); + return; + } + if (text_value && $text_sources.text().toLowerCase().indexOf(text_value) == -1) { + $row.hide(); + return; + } + $row.show(); + } + + $rows.each(showItemRow); + } + + if ($table.length) { + $rows = $table.find('tbody tr'); + $text_input.on('keyup', filterItemList); + $group_select.on('change', filterItemList); + } + } + }; + +}(jQuery, Drupal)); diff --git a/paragraphs_collection.libraries.yml b/paragraphs_collection.libraries.yml index bddd5ea..298bdec 100644 --- a/paragraphs_collection.libraries.yml +++ b/paragraphs_collection.libraries.yml @@ -1,3 +1,14 @@ +overview: + css: + component: + css/overview.css: {} + js: + js/overview.js: {} + dependencies: + - core/drupal + - core/jquery + - core/jquery.once + grid_layout: css: theme: diff --git a/paragraphs_collection.links.menu.yml b/paragraphs_collection.links.menu.yml new file mode 100644 index 0000000..70aeea3 --- /dev/null +++ b/paragraphs_collection.links.menu.yml @@ -0,0 +1,6 @@ +paragraphs_collection.layouts: + title: 'Paragraphs Collection' + parent: system.admin_reports + description: 'Overviews of items discoverable by behavior plugins.' + route_name: paragraphs_collection.layouts + menu_name: admin diff --git a/paragraphs_collection.links.task.yml b/paragraphs_collection.links.task.yml new file mode 100644 index 0000000..a7742b7 --- /dev/null +++ b/paragraphs_collection.links.task.yml @@ -0,0 +1,10 @@ +paragraphs_collection.layouts: + title: Layouts + route_name: paragraphs_collection.layouts + base_route: paragraphs_collection.layouts + +paragraphs_collection.styles: + title: Styles + route_name: paragraphs_collection.styles + base_route: paragraphs_collection.layouts + diff --git a/paragraphs_collection.routing.yml b/paragraphs_collection.routing.yml new file mode 100644 index 0000000..6a87acf --- /dev/null +++ b/paragraphs_collection.routing.yml @@ -0,0 +1,15 @@ +paragraphs_collection.layouts: + path: '/admin/reports/paragraphs_collection/layouts' + defaults: + _controller: '\Drupal\paragraphs_collection\Controller\OverviewController::layouts' + _title: 'Available grid layouts' + requirements: + _permission: 'administer paragraphs types' + +paragraphs_collection.styles: + path: '/admin/reports/paragraphs_collection/styles' + defaults: + _controller: '\Drupal\paragraphs_collection\Controller\OverviewController::styles' + _title: 'Available styles' + requirements: + _permission: 'administer paragraphs types' diff --git a/src/Controller/OverviewController.php b/src/Controller/OverviewController.php new file mode 100644 index 0000000..80a7a43 --- /dev/null +++ b/src/Controller/OverviewController.php @@ -0,0 +1,371 @@ +' => ['' => ]] + * @endcode + * + * Items: + * + * For the grid layout plugin, each Paragraphs Type has the machine names of + * all enabled layouts in an array. Exception: An empty array means that all + * layouts are enabled for that Paragraphs Type. + * + * For the style plugin, each Paragraphs Type has the machine name of the + * style group is uses as its value. Exception: An empty string means that all + * style groups are enabled for that Paragraphs Type. + * + * @var array + */ + protected $itemsPerParagraphsTypesPerPlugins; + + /** + * Constructs a \Drupal\paragraphs_collection\Controller\OverviewController object. + * + * @param \Drupal\paragraphs_collection\GridLayoutDiscoveryInterface $grid_discovery + * The discovery service for grid layout files. + * @param \Drupal\paragraphs_collection\StyleDiscoveryInterface $style_discovery + * The discovery service for style files. + */ + public function __construct(GridLayoutDiscoveryInterface $grid_discovery, StyleDiscoveryInterface $style_discovery) { + $this->gridDiscovery = $grid_discovery; + $this->styleDiscovery = $style_discovery; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('paragraphs_collection.grid_layout_discovery'), + $container->get('paragraphs_collection.style_discovery') + ); + } + + /** + * Lists Paragraphs Types with discoverable items they allow for a plugin. + * + * @param string $plugin_id + * The ID of the behaviour plugin that the discoverable items belong to. + * Only "grid_layout" and "style" are currently supported. + * + * @return array + * Discoverable items keyed by by Paragraphs Type IDs. The form of the items + * depends on the behaviour plugin. + * For the grid layout plugin, these are arrays of grid layout machine names + * keyed by the IDs of Paragraph Types which have them enabled. A Paragraphs + * Type ID with an empty array as its value means that all grid layouts are + * enabled for that Paragraphs Type. + * For the style plugin, the items are style groups machine names keyed by + * the IDs of Paragraph Types which use them. A Paragraphs Type ID with an + * empty sting as its value means that all styles are enabled for that + * Paragraphs Type. + * For an an unsupported plugin, an empty array is returned. + * + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + * If the behaviour plugin ID is invalid. + */ + public function getItemsPerParagraphsTypesPerPlugin($plugin_id) { + if (isset($this->itemsPerParagraphsTypesPerPlugins[$plugin_id])) { + return $this->itemsPerParagraphsTypesPerPlugins[$plugin_id]; + } + + $paragraph_type_ids = \Drupal::entityQuery('paragraphs_type')->execute(); + $paragraphs_types_enabled = []; + foreach ($paragraph_type_ids as $paragraph_type_id) { + /** @var ParagraphsType $paragraphs_type */ + $paragraphs_type = ParagraphsType::load($paragraph_type_id); + $configuration = $paragraphs_type->getBehaviorPlugin($plugin_id)->getConfiguration(); + if (isset($configuration['enabled']) && $configuration['enabled']) { + switch ($plugin_id) { + case 'grid_layout': + $paragraphs_types_enabled[$paragraph_type_id] = []; + foreach ($configuration['available_grid_layouts'] as $key => $value) { + if ($value) { + $paragraphs_types_enabled[$paragraph_type_id][] = $key; + } + } + break; + case 'style': + $paragraphs_types_enabled[$paragraph_type_id] = $configuration['group']; + break; + default: + return $this->itemsPerParagraphsTypesPerPlugins[$plugin_id] = []; + } + } + } + return $this->itemsPerParagraphsTypesPerPlugins[$plugin_id] = $paragraphs_types_enabled; + } + + /** + * Finds all Paragraphs Types which allow a particular grid layout behaviour. + * + * @param string $layout + * The machine name of the grid layout. + * + * @return array + * Array of IDs of Paragraphs Types that use the grid layout. + */ + public function getParagraphsTypesPerLayout($layout) { + $paragraphs_types = []; + foreach ($this->getItemsPerParagraphsTypesPerPlugin('grid_layout') as $paragraphs_type => $enabled_layouts) { + if ($enabled_layouts == [] || in_array($layout, $enabled_layouts)) { + $paragraphs_types[] = $paragraphs_type; + } + } + + return $paragraphs_types; + } + + /** + * Finds all Paragraphs Types which allow a particular style behaviour. + * + * @param string $style + * The machine name of the style. + * + * @return array + * Array of IDs of Paragraphs Types that use the style. + */ + public function getParagraphsTypesPerStyle($style) { + $paragraphs_types = []; + foreach ($this->getItemsPerParagraphsTypesPerPlugin('style') as $paragraphs_type => $used_style_group) { + $enabled_styles = array_keys($this->styleDiscovery->getStyleOptions($used_style_group)); + if (in_array($style, $enabled_styles)) { + $paragraphs_types[] = $paragraphs_type; + } + } + + return $paragraphs_types; + } + + /** + * Generates an overview page of available layouts for the grid layout plugin. + * + * @return array + * The output render array. + */ + public function layouts() { + return self::content('grid_layout'); + } + + /** + * Generates an overview page of available styles for the styles plugin. + * + * @return array + * The output render array. + */ + public function styles() { + return self::content('style'); + } + + /** + * Generates an overview page of available discoverable items for a plugin. + * + * Discoverable items are styles for the style plugin and layouts for the + * grid layout plugin. + * + * @param string $plugin_id + * The ID of the behaviour plugin that the discoverable items belong to. + * Only "grid_layout" and "style" are currently supported. + * + * @return array|null + * The output render array. NULL for invalid plugin IDs and unsupported + * behavior plugins. + */ + public function content($plugin_id) { + $header = [ + 'label' => $this->t('Style'), + 'details' => $this->t('Details'), + 'use' => $this->t('Used in'), + ]; + + switch ($plugin_id) { + case 'grid_layout': + $items = $this->gridDiscovery->getGridLayouts(); + break; + case 'style': + $items = $this->styleDiscovery->getStyles(); + break; + default: + return NULL; + } + uasort($items, function ($item1, $item2) { + return strcasecmp($item1['title'], $item2['title']); + }); + + $rows =[]; + foreach ($items as $item_id => $item) { + switch ($plugin_id) { + case 'grid_layout': + $paragraphs_type_ids = $this->getParagraphsTypesPerLayout($item_id); + break; + case 'style': + $paragraphs_type_ids = $this->getParagraphsTypesPerStyle($item_id); + break; + default: + return NULL; + } + + $paragraphs_type_link_list = []; + foreach ($paragraphs_type_ids as $paragraphs_type_id) { + $paragraphs_type = ParagraphsType::load($paragraphs_type_id); + + if($paragraphs_type_link_list != []) { + $paragraphs_type_link_list[] = ['#plain_text' => ', ']; + } + + $paragraphs_type_link_list[] = [ + '#type' => 'link', + '#title' => $paragraphs_type->label(), + '#url' => $paragraphs_type->toUrl(), + '#attributes' => [ + 'class' => ['table-filter-paragraphs-type-source'], + ], + ]; + } + + $row['label'] = [ + '#type' => 'markup', + '#markup' => Html::escape($item['title']), + '#prefix' => '', + '#suffix' => '', + ]; + $row['details'] = [ + '#type' => 'details', + '#title' => $this->t($item['description']) ?: $this->t('Details'), + '#open' => FALSE, + '#attributes' => ['class' => ['overview-details']], + ]; + $row['details']['id'] = [ + '#type' => 'item', + '#title' => $this->t('ID'), + '#markup' => ''. Html::escape($item_id) . '', + '#prefix' => '
', + '#suffix' => '
', + ]; + switch ($plugin_id) { + case 'style': + $group_list = []; + foreach ($item['groups'] as $group) { + if ($group_list != []) { + $group_list[] = ['#plain_text' => ', ']; + } + $group_list[] = [ + '#type' => 'markup', + '#markup' => Html::escape($group), + '#prefix' => '', + '#suffix' => '', + ]; + } + $row['details']['groups'] = [ + '#type' => 'item', + '#title' => $this->t('Groups'), + 'item' => $group_list, + '#prefix' => '
', + '#suffix' => '
', + ]; + break; + default: + } + $row['use'] = $paragraphs_type_link_list; + + $rows[] = $row; + } + + $table = [ + '#type' => 'table', + '#header' => $header, + '#sticky' => TRUE, + '#attributes' => [ + 'class' => ['paragraphs-collection-overview-table'], + ], + ]; + $table += $rows; + + $filters = [ + '#type' => 'fieldset', + '#attributes' => [ + 'class' => ['table-filter', 'js-show', 'form--inline'], + 'data-table' => '.paragraphs-collection-overview-table', + ], + '#weight' => -10, + '#title' => $this->t('Filter'), + ]; + switch ($plugin_id) { + case 'style': + $group_options = $this->styleDiscovery->getStyleGroups(); + asort($group_options); + $empty_option = ['' => '- All -']; + + $filters['group'] = [ + '#type' => 'select', + '#title' => $this->t('Group'), + '#options' => $empty_option + $group_options, + '#attributes' => [ + 'class' => ['table-filter-group-select'], + ], + ]; + break; + default: + } + $filters['text'] = [ + '#type' => 'search', + '#title' => $this->t('Style label or ID'), + '#size' => 40, + '#attributes' => [ + 'class' => ['table-filter-text'], + 'autocomplete' => 'off', + 'title' => $this->t('Enter a part of the style label or ID to filter by.'), + ], + ]; + + $build['table'] = $table; + $build['filters'] = $filters; + $build['#attached']['library'] = ['paragraphs_collection/overview']; + + return $build; + } + +}