panopoly_magic-8.x-2.x-dev/src/Plugin/views/display/MagicBlock.php
src/Plugin/views/display/MagicBlock.php
<?php
namespace Drupal\panopoly_magic\Plugin\views\display;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\TypedData\TypedDataManagerInterface;
use Drupal\ctools_views\Plugin\Display\Block;
use Drupal\views\Plugin\Block\ViewsBlock;
use Drupal\views\Plugin\views\filter\FilterPluginBase;
use Drupal\views\Plugin\views\sort\SortPluginBase;
use Drupal\views\Plugin\views\ViewsHandlerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides display type overrides for the Block display.
*
* phpcs:disable Drupal.NamingConventions.ValidVariableName.LowerCamelName
*/
final class MagicBlock extends Block {
/**
* The entity display repository.
*
* @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface
*/
protected $entityDisplayRepository;
/**
* The typed data manager.
*
* @var \Drupal\Core\TypedData\TypedDataManagerInterface
*/
protected $typedDataManager;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
/** @var self $instance */
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
$instance->setEntityDisplayRepository($container->get('entity_display.repository'));
$instance->setTypedDataManager($container->get('typed_data_manager'));
return $instance;
}
/**
* Set the entity display repository.
*
* @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
* The entity display repository.
*/
public function setEntityDisplayRepository(EntityDisplayRepositoryInterface $entity_display_repository) {
$this->entityDisplayRepository = $entity_display_repository;
}
/**
* Sets the typed data manager.
*
* @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager
* The typed data manager.
*/
public function setTypedDataManager(TypedDataManagerInterface $typed_data_manager) {
$this->typedDataManager = $typed_data_manager;
}
/**
* {@inheritdoc}
*/
public function optionsSummary(&$categories, &$options) {
parent::optionsSummary($categories, $options);
$filtered_allow = array_filter($this->getOption('allow'));
if (isset($filtered_allow['display_type'])) {
if ($options['allow']['value'] === $this->t('None')) {
$options['allow']['value'] = $this->t('Display type');
}
else {
$options['allow']['value'] .= ', ' . $this->t('Display type');
}
}
if (isset($filtered_allow['exposed_form'])) {
if ($options['allow']['value'] === $this->t('None')) {
$options['allow']['value'] = $this->t('Use exposed form as block configuration');
}
else {
$options['allow']['value'] .= ', ' . $this->t('Use exposed form as block configuration');
}
}
if (isset($filtered_allow['use_pager'])) {
if ($options['allow']['value'] === $this->t('None')) {
$options['allow']['value'] = $this->t('Use pager');
}
else {
$options['allow']['value'] .= ', ' . $this->t('Use pager');
}
if (isset($filtered_allow['no_pager_by_default'])) {
$options['allow']['value'] .= ', ' . $this->t('No pager by default');
}
}
$options['magic_arguments'] = [
'category' => 'block',
'title' => $this->t('Argument input'),
'value' => $this->t('Edit'),
];
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
switch ($form_state->get('section')) {
case 'allow':
$form['allow']['#options']['display_type'] = $this->t('Display type');
$form['allow']['#options']['exposed_form'] = $this->t('Use exposed form as block configuration');
$form['allow']['#options']['use_pager'] = $this->t('Use pager');
$form['allow']['#options']['no_pager_by_default'] = $this->t('No pager by default (only works if "Use pager" is enabled)');
break;
case 'magic_arguments':
$form['#title'] .= $this->t('Argument input');
$context_options = [];
foreach ($this->typedDataManager->getDefinitions() as $data_type_id => $data_type_definition) {
if (in_array($data_type_id, ['any'])) {
continue;
}
if (isset($data_type_definition['no_ui']) && $data_type_definition['no_ui']) {
continue;
}
if (strpos($data_type_id, 'field_item:') === 0) {
continue;
}
$context_options[$data_type_id] = t('@label (@machine_name)', [
'@label' => $data_type_definition['label'],
'@machine_name' => $data_type_id,
]);
}
asort($context_options);
$defaults = $this->getOption('magic_arguments') ?: [];
$form['magic_arguments'] = [
'#tree' => TRUE,
];
foreach ($this->view->getDisplay()->getHandlers('argument') as $id => $handler) {
$default = $defaults[$id] ?: [];
$form['magic_arguments'][$id] = [
'#title' => $handler->adminLabel(),
'#type' => 'fieldset',
];
$form['magic_arguments'][$id]['type'] = [
'#title' => $this->t('Type'),
'#type' => 'select',
'#options' => [
'none' => $this->t('No value'),
'context' => $this->t('From context'),
],
'#default_value' => $default['type'] ?: 'none',
];
$form['magic_arguments'][$id]['context'] = [
'#title' => $this->t('Required context'),
'#type' => 'select',
'#description' => $this->t('If "From context" is selected, which type of context to use.'),
'#options' => $context_options,
'#states' => [
'visible' => [
':input[name="magic_arguments[title][type]"]' => ['value' => 'context'],
],
],
'#default_value' => $default['context'] ?: '',
];
$form['magic_arguments'][$id]['context_token'] = [
'#title' => $this->t('Value token'),
'#type' => 'textfield',
'#description' => $this->t('Enter a token to get a sub-value from the context entity, using the entity type as a prefix, ex: <code>[node:title]</code> or <code>[node:field_name]</code>. Leave blank if the whole context value should be used.'),
'#states' => [
'visible' => [
':input[name="magic_arguments[title][type]"]' => ['value' => 'context'],
],
],
'#default_value' => $default['context_token'] ?: '',
];
$form['magic_arguments'][$id]['context_optional'] = [
'#title' => $this->t('Context is optional'),
'#type' => 'checkbox',
'#description' => $this->t('This context need not be present for the block to function. If you plan to use this, ensure that the argument handler can handle empty values gracefully.'),
'#states' => [
'visible' => [
':input[name="magic_arguments[title][type]"]' => ['value' => 'context'],
],
],
'#default_value' => (bool) $default['context_optional'] ?: FALSE,
];
}
break;
}
}
/**
* Perform any necessary changes to the form values prior to storage.
*
* There is no need for this function to actually store the data.
*/
public function submitOptionsForm(&$form, FormStateInterface $form_state) {
parent::submitOptionsForm($form, $form_state);
$section = $form_state->get('section');
if ($section === 'magic_arguments') {
$this->setOption($section, $form_state->getValue($section));
}
}
/**
* {@inheritdoc}
*/
public function getArgumentText() {
$text = parent::getArgumentText();
$text['description'] = $this->t("The contextual filter values are provided by the 'Argument input' configuration. If not configured, no contextual filter value will be available unless you select 'Provide default'.");
return $text;
}
/**
* {@inheritdoc}
*/
public function blockForm(ViewsBlock $block, array &$form, FormStateInterface $form_state) {
$form = parent::blockForm($block, $form, $form_state);
$form['override']['#type'] = 'container';
$form['override']['#weight'] = 50;
$allow_settings = array_filter($this->getOption('allow'));
$block_configuration = $block->getConfiguration();
if (!empty($allow_settings['use_pager'])) {
$form['override']['use_pager'] = [
'#type' => 'checkbox',
'#title' => $this->t('Use pager'),
'#default_value' => $block_configuration['use_pager'] ?? empty($allow_settings['no_pager_by_default']),
'#weight' => -10,
];
}
if (!empty($allow_settings['display_type']) && $this->view->getBaseEntityType() !== FALSE) {
$this->buildBlockFormDisplayType($form, $block_configuration);
}
if (!empty($allow_settings['exposed_form'])) {
$this->buildBlockFormExposedForm($form, $block_configuration);
}
return $form;
}
/**
* Builds the 'Display type' part of the block form.
*
* @param array $form
* The block form.
* @param array $block_configuration
* The block configuration.
*/
protected function buildBlockFormDisplayType(array &$form, array $block_configuration) {
$current_row_plugin = $this->getOption('row')['type'];
// Set to default view settings if there isn't one.
if (empty($block_configuration['view_settings'])) {
// Normalize the "entity" row plugin derivatives.
$block_configuration['view_settings'] = $this->convertViewSettings($current_row_plugin);
}
$block_configuration['view_settings'] = $this->convertViewSettings($block_configuration['view_settings']);
// Add information about the View Mode.
$form['override']['display_settings']['view_settings'] = [
'#type' => 'radios',
'#prefix' => '<div class="view-settings-wrapper">',
'#suffix' => '</div>',
'#title' => $this->t('Display Type'),
'#default_value' => $block_configuration['view_settings'],
'#weight' => 10,
'#options' => [
'fields' => $this->t('Fields'),
'rendered_entity' => $this->t('Content'),
'table' => $this->t('Table'),
],
];
// Add header column options for table views.
$form['override']['display_settings']['header_type'] = [
'#type' => 'select',
'#title' => $this->t('Column Header'),
'#options' => [
'none' => $this->t('None'),
'titles' => $this->t('Titles'),
],
'#default_value' => !empty($block_configuration['header_type']) ? $block_configuration['header_type'] : 'none',
'#states' => [
'visible' => [
':input[name="settings[override][display_settings][view_settings]"]' => ['value' => 'table'],
],
],
'#weight' => 11,
];
// Update field overrides to be dependent on the view settings selection.
if (!empty($form['override']['order_fields'])) {
$form['override']['order_fields']['#weight'] = 15;
// @note states target js-form-wrapper, which tables do not have by default.
$form['override']['order_fields']['#attributes']['class'][] = 'js-form-wrapper';
$form['override']['order_fields']['#attributes']['class'][] = 'form-wrapper';
$form['override']['order_fields']['#states'] = [
// The inverted logic here isn't optimal, and in the future may be
// better achieved via OR'd conditions.
// @link http://drupal.org/node/735528 @endlink
'invisible' => [
':input[name="settings[override][display_settings][view_settings]"]' => ['value' => 'rendered_entity'],
],
];
}
// Get view modes for entity.
$view_modes = $this->viewModeOptions($this->view->getBaseEntityType()->id());
$options_default_view_mode = ($current_row_plugin === 'fields') ? 'teaser' : 'full';
$row_options = $this->getOption('row');
if (!empty($row_options['view_mode'])) {
$options_default_view_mode = $this->getOption('row')['view_mode'];
}
if (!array_key_exists($options_default_view_mode, $view_modes)) {
$options_default_view_mode = key($view_modes);
}
// Add specific style options.
$form['override']['display_settings']['content_settings'] = [
'#type' => 'container',
'#states' => [
'visible' => [
':input[name="settings[override][display_settings][view_settings]"]' => ['value' => 'rendered_entity'],
],
],
'#weight' => 15,
];
// @todo finish porting visibility on lines 1177-1192.
$form['override']['display_settings']['content_settings']['view_mode'] = [
'#type' => 'radios',
'#title' => $this->t('View mode'),
'#options' => $view_modes,
'#default_value' => !empty($block_configuration['view_mode']) ? $block_configuration['view_mode'] : $options_default_view_mode,
];
}
/**
* Rewrites the '#states' on exposed Views forms to work on the block form.
*
* @param array $element
* The Form API element.
* @param string $parent_string
* The string to inject representing the parent of this form element.
*/
protected function rewriteFormElementStates(array &$element, $parent_string) {
if (!empty($element['#states'])) {
foreach ($element['#states'] as $state_name => $state) {
foreach ($state as $spec_index => $specs) {
$new_specs = [];
foreach ($specs as $selector => $value) {
$selector = preg_replace('/name="([a-zA-Z0-9_]+)/', "name=\"{$parent_string}[\\1]", $selector);
$new_specs[$selector] = $value;
}
$element['#states'][$state_name][$spec_index] = $new_specs;
}
}
}
foreach (Element::children($element) as $name) {
$this->rewriteFormElementStates($element[$name], $parent_string);
}
}
/**
* Builds the exposed form part of the block configuration.
*
* @param array $form
* The block form.
* @param array $block_configuration
* The block configuration.
*/
protected function buildBlockFormExposedForm(array &$form, array $block_configuration) {
// Get the exposed form values into their handlers.
$this->view->setExposedInput($block_configuration['exposed_form']['filters'] ?? []);
$this->view->build();
$form['override']['exposed_form'] = [
'#type' => 'container',
'#tree' => TRUE,
];
$filters = $this->getExposedFilters();
$form['override']['exposed_form']['filters'] = [
'#type' => 'container',
'#tree' => TRUE,
'#access' => count($filters) > 0,
];
$filter_form_state = $this->createFormStateForExposedFilters($this->view->getExposedInput());
foreach ($filters as $filter) {
assert($filter instanceof FilterPluginBase);
$filter->buildExposedForm($form['override']['exposed_form']['filters'], $filter_form_state);
if (!empty($filter->options['expose']['identifier'])) {
$identifier = $filter->options['expose']['identifier'];
$element_name = $identifier;
if (!empty($filter->options['expose']['use_operator']) && !empty($filter->options['expose']['operator_id'])) {
$element_name .= "_wrapper";
$form['override']['exposed_form']['filters'][$identifier . '_wrapper']['#parents'] = [
'settings',
'override',
'exposed_form',
'filters',
$identifier,
];
}
$form['override']['exposed_form']['filters'][$element_name]['#title'] = $filter->options['expose']['label'];
$this->rewriteFormElementStates($form['override']['exposed_form']['filters'][$element_name], "settings[override][exposed_form][filters][{$identifier}]");
}
}
// Modify the way ctools exposes sorts to match Panopoly 1.x functionality.
$sorts = array_filter($this->getHandlers('sort'), static function (SortPluginBase $plugin) {
return $plugin->isExposed();
});
$form['override']['exposed_form']['sort'] = [
'#type' => 'container',
'sort_order' => [
'#title' => $this->t('Sort order'),
'#type' => 'radios',
'#options' => [
'ASC' => $this->t('Sort ascending'),
'DESC' => $this->t('Sort descending'),
],
'#default_value' => $block_configuration['exposed_form']['sort']['sort_order'] ?? 'ASC',
],
'sort_by' => [
'#title' => $this->t('Sort by'),
'#type' => 'select',
'#options' => array_map(static function (ViewsHandlerInterface $plugin) {
// @todo exposed info label instead
return $plugin->adminLabel();
}, $sorts),
'#default_value' => $block_configuration['exposed_form']['sort']['sort_by'] ?? '',
],
'#access' => count($sorts) > 0,
];
}
/**
* Gets the exposed filter handlers.
*
* @return \Drupal\views\Plugin\views\filter\FilterPluginBase[]
* The exposed filter handlers.
*/
protected function getExposedFilters() {
return array_filter($this->getHandlers('filter'), static function (FilterPluginBase $plugin) {
return $plugin->isExposed();
});
}
/**
* {@inheritdoc}
*/
public function blockSubmit(ViewsBlock $block, $form, FormStateInterface $form_state) {
// Stash the 'items_per_page' so we can restore it later. This is removed
// from values in \Drupal\views\Plugin\views\display\Block::blockSubmit()
// which breaks the AJAX submit for preview.
$items_per_page = $form_state->getValue(['override', 'items_per_page']);
parent::blockSubmit($block, $form, $form_state);
// Restore this so AJAX works as it should!
if ($items_per_page) {
$form_state->setValue(['override', 'items_per_page'], $items_per_page);
}
$configuration = $block->getConfiguration();
$allow_settings = array_filter($this->getOption('allow'));
if (!empty($allow_settings['use_pager'])) {
$use_pager = $form_state->getValue(['override', 'use_pager']);
$configuration['use_pager'] = (bool) $use_pager;
}
if (!empty($allow_settings['display_type'])) {
$display_settings = $form_state->getValue(['override', 'display_settings']);
foreach ($display_settings as $setting => $value) {
// Flatten content_settings.
if ($setting === 'content_settings') {
foreach ($value as $k => $v) {
$configuration[$k] = $v;
}
}
else {
$configuration[$setting] = $value;
}
}
}
if (!empty($allow_settings['exposed_form'])) {
$sort = $form_state->getValue(['override', 'exposed_form', 'sort']);
foreach ($sort as $setting => $value) {
$configuration['exposed_form']['sort'][$setting] = $value;
}
$filter_values = $form_state->getValue([
'override',
'exposed_form',
'filters',
]);
foreach ($this->getExposedFilters() as $filter) {
if (empty($filter->options['expose']['identifier'])) {
continue;
}
$identifier = $filter->options['expose']['identifier'];
if (!empty($filter->options['expose']['use_operator']) && !empty($filter->options['expose']['operator_id'])) {
$operator = $filter->options['expose']['operator_id'];
$configuration['exposed_form']['filters'][$identifier] = $filter_values[$identifier][$identifier];
$configuration['exposed_form']['filters'][$operator] = $filter_values[$identifier][$operator];
}
else {
$configuration['exposed_form']['filters'][$identifier] = $filter_values[$identifier];
}
}
}
$block->setConfiguration($configuration);
}
/**
* {@inheritdoc}
*/
public function preBlockBuild(ViewsBlock $block) {
$config = $block->getConfiguration();
[, $display_id] = explode('-', $block->getDerivativeId(), 2);
// @see panopoly_magic_views_pre_view().
$allow_settings = array_filter($this->getOption('allow'));
if (!empty($allow_settings['display_type']) && !empty($config['view_settings'])) {
$view_settings = $this->convertViewSettings($config['view_settings']);
$view_entity_type = $this->view->getBaseEntityType();
// Set the style plugin to a table style.
// Determine that this was previously a field view, which has been
// overridden to a node view in the pane config.
if ($view_settings === 'rendered_entity' && $view_entity_type) {
$this->options['defaults']['row'] = FALSE;
$this->options['row']['type'] = 'entity:' . $view_entity_type->id();
if (!empty($config['view_mode'])) {
// Transfer over the row options from default if set to use.
if (!empty($this->options['defaults']['row_options'])) {
$this->options['defaults']['row_options'] = FALSE;
}
$this->options['row']['options']['view_mode'] = $config['view_mode'];
}
}
elseif ($view_settings === 'fields') {
$this->options['defaults']['row'] = FALSE;
$this->options['row']['type'] = 'fields';
}
elseif ($view_settings === 'table') {
// Find the currently active field definition, else break out as table
// needs fields.
if (!empty($this->options['fields'])) {
$fields = &$this->options['fields'];
}
elseif (empty($this->default_display->options['defaults']['fields']) && isset($this->view->display_handler->options['fields'])) {
$fields = &$this->default_display->options['fields'];
}
else {
// If no fields, don't try to display as table.
return;
}
$this->options['defaults']['style'] = FALSE;
$this->options['style']['type'] = 'table';
// Set or remove header labels depending on user selection.
$use_header_titles = !empty($config['header_type']) && $config['header_type'] === 'titles';
foreach ($fields as $field_key => &$field) {
if ($use_header_titles && !empty($field['admin_label']) && empty($field['label'])) {
$field['label'] = $field['admin_label'];
}
elseif (!$use_header_titles) {
$field['label'] = '';
}
// Hide empty columns.
if (!empty($this->options['row']['hide_empty'])) {
$this->options['style'][$field_key]['empty_column'] = TRUE;
}
}
unset($field);
}
}
// The ctools Block plugin invokes the style plugin and instantiates it
// first, so we run it after we've adjusted row and style options.
parent::preBlockBuild($block);
if (!empty($allow_settings['use_pager'])) {
// Disable pager if configured to not "Use pager".
$use_pager = $config['use_pager'] ?? empty($allow_settings['no_pager_by_default']);
if (!$use_pager) {
$pager = $this->view->display_handler->getOption('pager');
if (!empty($pager) && !in_array($pager['type'], ['none', 'some'])) {
$pager['type'] = 'some';
$this->view->display_handler->setOption('pager', $pager);
}
}
}
if (!empty($allow_settings['exposed_form'])) {
// Add exposed filter values.
$filter_values = $config['exposed_form']['filters'] ?? [];
foreach ($this->getExposedFilters() as $filter) {
if ($this->filterHasValue($filter, $filter_values) && $this->filterValidateExposed($filter, $filter_values)) {
// The values were accepted. Make it no longer exposed, in order to
// prevent it being rendered on the form.
$filter->options['exposed'] = FALSE;
}
elseif (empty($filter->options['expose']['required'])) {
// The values weren't accepted, so remove the filter entirely.
$filter_name = $filter->options['id'];
$this->view->removeHandler($display_id, 'filter', $filter_name);
unset($this->handlers['filter'][$filter_name]);
}
}
// Only use the selected exposed sort.
if (!empty($config['exposed_form']['sort'])) {
$sort_order = $config['exposed_form']['sort']['sort_order'];
$sort_by = $config['exposed_form']['sort']['sort_by'];
}
else {
$sort_order = $sort_by = [];
}
$sorts = $this->view->getHandlers('sort', $display_id);
foreach ($sorts as $sort_name => $sort) {
if (empty($sort['exposed'])) {
continue;
}
if ($sort_name !== $sort_by) {
$this->view->removeHandler($display_id, 'sort', $sort_name);
unset($this->handlers['sort'][$sort_name]);
}
else {
$sort['order'] = $sort_order;
$sort['exposed'] = FALSE;
$this->view->setHandler($display_id, 'sort', $sort_name, $sort);
}
}
}
}
/**
* Creates a form state for exposed filters.
*
* @param array $filter_values
* The filter values.
*
* @return \Drupal\Core\Form\FormState
* The form state.
*/
protected function createFormStateForExposedFilters(array $filter_values): FormState {
$filter_form_state = (new FormState())
->setStorage([
'view' => $this->view,
'display' => &$this->view->display_handler->display,
'rerender' => TRUE,
])
->setMethod('get')
->setAlwaysProcess()
->disableRedirect()
->setUserInput($filter_values)
->setValues($filter_values)
->set('exposed', TRUE);
return $filter_form_state;
}
/**
* Checks if the given filter has a value in the given array of values.
*
* @param \Drupal\views\Plugin\views\filter\FilterPluginBase $filter
* The filter.
* @param array $filter_values
* The values to check.
*
* @return bool
* Returns TRUE if a value is found; otherwise FALSE.
*/
protected function filterHasValue($filter, array $filter_values): bool {
$identifier = $filter->options['expose']['identifier'];
if (!isset($filter_values[$identifier])) {
return FALSE;
}
if (!empty($filter->options['expose']['use_operator']) && !empty($filter->options['expose']['operator_id'])) {
$operator = $filter->options['expose']['operator_id'];
if (!isset($filter_values[$operator])) {
return FALSE;
}
}
return TRUE;
}
/**
* Validates the given filter as if it were submitted on a form.
*
* @param \Drupal\views\Plugin\views\filter\FilterPluginBase $filter
* The filter.
* @param array $filter_values
* The values to check.
*
* @return bool
* Returns TRUE if a value is found; otherwise FALSE.
*/
protected function filterValidateExposed($filter, array $filter_values): bool {
$form = [];
$form_state = $this->createFormStateForExposedFilters($filter_values);
$filter->buildExposedForm($form, $form_state);
$filter->validateExposed($form, $form_state);
return $filter->acceptExposedInput($filter_values);
}
/**
* Convert with legacy 'nodes' and others (such as 'files') view settings.
*
* @todo copy and paste; used to help handle `entity:` derivatives.
*
* @param string $view_setting
* The view setting value.
*
* @return string
* The converted setting value.
*/
protected function convertViewSettings(string $view_setting): string {
// The 'fields' and 'table' view settings apply to any entity type.
if (in_array($view_setting, ['fields', 'table'])) {
return $view_setting;
}
// We convert other view settings to 'rendered_entity' (which could be
// 'entity:node' or 'entity:file' and others specific to an entity type).
return 'rendered_entity';
}
/**
* Get view mode options for an entity type.
*
* @param string $id
* The entity type ID.
*
* @return array
* The view mode options.
*/
protected function viewModeOptions(string $id): array {
$options = $this->entityDisplayRepository->getViewModeOptions($id);
// When selecting full, locally the site becomes unavailable with a 502
// gateway timeout. Default and full should be the same.
unset($options['full']);
return $options;
}
}
