3 namespace Drupal\views\Plugin\views\display;
5 use Drupal\Component\Plugin\DependentPluginInterface;
6 use Drupal\Component\Utility\Html;
7 use Drupal\Component\Render\FormattableMarkup;
8 use Drupal\Core\Cache\Cache;
9 use Drupal\Core\Cache\CacheableMetadata;
10 use Drupal\Core\Cache\CacheableDependencyInterface;
11 use Drupal\Core\Form\FormStateInterface;
12 use Drupal\Core\Language\LanguageInterface;
13 use Drupal\Core\Plugin\PluginDependencyTrait;
14 use Drupal\Core\Session\AccountInterface;
16 use Drupal\views\Form\ViewsForm;
17 use Drupal\views\Plugin\views\area\AreaPluginBase;
18 use Drupal\views\ViewExecutable;
19 use Drupal\views\Plugin\views\PluginBase;
20 use Drupal\views\Views;
23 * Base class for views display plugins.
25 abstract class DisplayPluginBase extends PluginBase implements DisplayPluginInterface, DependentPluginInterface {
26 use PluginDependencyTrait;
29 * The top object of a view.
31 * @var \Drupal\views\ViewExecutable
36 * An array of instantiated handlers used in this display.
38 * @var \Drupal\views\Plugin\views\ViewsHandlerInterface[]
40 public $handlers = [];
43 * An array of instantiated plugins used in this display.
45 * @var \Drupal\views\Plugin\views\ViewsPluginInterface[]
47 protected $plugins = [];
50 * Stores all available display extenders.
52 * @var \Drupal\views\Plugin\views\display_extender\DisplayExtenderPluginBase[]
54 protected $extenders = [];
59 protected $usesOptions = TRUE;
62 * Stores the rendered output of the display.
67 public $output = NULL;
70 * Whether the display allows the use of AJAX or not.
74 protected $usesAJAX = TRUE;
77 * Whether the display allows the use of a pager or not.
81 protected $usesPager = TRUE;
84 * Whether the display allows the use of a 'more' link or not.
88 protected $usesMore = TRUE;
91 * Whether the display allows attachments.
94 * TRUE if the display can use attachments, or FALSE otherwise.
96 protected $usesAttachments = FALSE;
99 * Whether the display allows area plugins.
103 protected $usesAreas = TRUE;
106 * Static cache for unpackOptions, but not if we are in the UI.
110 protected static $unpackOptions = [];
113 * The display information coming directly from the view entity.
115 * @see \Drupal\views\Entity\View::getDisplay()
117 * @todo \Drupal\views\Entity\View::duplicateDisplayAsType directly access it.
124 * Constructs a new DisplayPluginBase object.
126 * Because DisplayPluginBase::initDisplay() takes the display configuration by
127 * reference and handles it differently than usual plugin configuration, pass
128 * an empty array of configuration to the parent. This prevents our
129 * configuration from being duplicated.
131 * @todo Replace DisplayPluginBase::$display with
132 * DisplayPluginBase::$configuration to standardize with other plugins.
134 * @param array $configuration
135 * A configuration array containing information about the plugin instance.
136 * @param string $plugin_id
137 * The plugin_id for the plugin instance.
138 * @param mixed $plugin_definition
139 * The plugin implementation definition.
141 public function __construct(array $configuration, $plugin_id, $plugin_definition) {
142 parent::__construct([], $plugin_id, $plugin_definition);
148 public function initDisplay(ViewExecutable $view, array &$display, array &$options = NULL) {
151 // Load extenders as soon as possible.
152 $display['display_options'] += ['display_extenders' => []];
153 $this->extenders = [];
154 if ($extenders = Views::getEnabledDisplayExtenders()) {
155 $manager = Views::pluginManager('display_extender');
156 $display_extender_options = $display['display_options']['display_extenders'];
157 foreach ($extenders as $extender) {
158 /** @var \Drupal\views\Plugin\views\display_extender\DisplayExtenderPluginBase $plugin */
159 if ($plugin = $manager->createInstance($extender)) {
160 $extender_options = isset($display_extender_options[$plugin->getPluginId()]) ? $display_extender_options[$plugin->getPluginId()] : [];
161 $plugin->init($this->view, $this, $extender_options);
162 $this->extenders[$extender] = $plugin;
167 $this->setOptionDefaults($this->options, $this->defineOptions());
168 $this->display = &$display;
170 // Track changes that the user should know about.
173 if (!isset($options) && isset($display['display_options'])) {
174 $options = $display['display_options'];
177 if ($this->isDefaultDisplay() && isset($options['defaults'])) {
178 unset($options['defaults']);
181 $skip_cache = \Drupal::config('views.settings')->get('skip_cache');
183 if (empty($view->editing) || !$skip_cache) {
184 $cid = 'views:unpack_options:' . hash('sha256', serialize([$this->options, $options])) . ':' . \Drupal::languageManager()->getCurrentLanguage()->getId();
185 if (empty(static::$unpackOptions[$cid])) {
186 $cache = \Drupal::cache('data')->get($cid);
187 if (!empty($cache->data)) {
188 $this->options = $cache->data;
191 $this->unpackOptions($this->options, $options);
192 \Drupal::cache('data')->set($cid, $this->options, Cache::PERMANENT, $this->view->storage->getCacheTags());
194 static::$unpackOptions[$cid] = $this->options;
197 $this->options = static::$unpackOptions[$cid];
201 $this->unpackOptions($this->options, $options);
204 // Mark the view as changed so the user has a chance to save it.
206 $this->view->changed = TRUE;
213 public function destroy() {
216 foreach ($this->handlers as $type => $handlers) {
217 foreach ($handlers as $id => $handler) {
218 if (is_object($handler)) {
219 $this->handlers[$type][$id]->destroy();
224 if (isset($this->default_display)) {
225 unset($this->default_display);
228 foreach ($this->extenders as $extender) {
229 $extender->destroy();
236 public function isDefaultDisplay() {
243 public function usesExposed() {
244 if (!isset($this->has_exposed)) {
245 foreach ($this->handlers as $type => $value) {
246 foreach ($this->view->$type as $handler) {
247 if ($handler->canExpose() && $handler->isExposed()) {
248 // One is all we need; if we find it, return TRUE.
249 $this->has_exposed = TRUE;
254 $pager = $this->getPlugin('pager');
255 if (isset($pager) && $pager->usesExposed()) {
256 $this->has_exposed = TRUE;
259 $this->has_exposed = FALSE;
262 return $this->has_exposed;
268 public function displaysExposed() {
275 public function usesAJAX() {
276 return $this->usesAJAX;
282 public function ajaxEnabled() {
283 if ($this->usesAJAX()) {
284 return $this->getOption('use_ajax');
292 public function isEnabled() {
293 return (bool) $this->getOption('enabled');
299 public function usesPager() {
300 return $this->usesPager;
306 public function isPagerEnabled() {
307 if ($this->usesPager()) {
308 $pager = $this->getPlugin('pager');
310 return $pager->usePager();
319 public function usesMore() {
320 return $this->usesMore;
326 public function isMoreEnabled() {
327 if ($this->usesMore()) {
328 return $this->getOption('use_more');
336 public function useGroupBy() {
337 return $this->getOption('group_by');
343 public function useMoreAlways() {
344 if ($this->usesMore()) {
345 return $this->getOption('use_more_always');
353 public function useMoreText() {
354 if ($this->usesMore()) {
355 return $this->getOption('use_more_text');
363 public function acceptAttachments() {
364 // To be able to accept attachments this display have to be able to use
365 // attachments but at the same time, you cannot attach a display to itself.
366 if (!$this->usesAttachments() || ($this->definition['id'] == $this->view->current_display)) {
370 if (!empty($this->view->argument) && $this->getOption('hide_attachment_summary')) {
371 foreach ($this->view->argument as $argument) {
372 if ($argument->needsStylePlugin() && empty($argument->argument_validated)) {
384 public function usesAttachments() {
385 return $this->usesAttachments;
391 public function usesAreas() {
392 return $this->usesAreas;
398 public function attachTo(ViewExecutable $view, $display_id, array &$build) {}
403 public function defaultableSections($section = NULL) {
405 'access' => ['access'],
406 'cache' => ['cache'],
407 'title' => ['title'],
408 'css_class' => ['css_class'],
409 'use_ajax' => ['use_ajax'],
410 'hide_attachment_summary' => ['hide_attachment_summary'],
411 'show_admin_links' => ['show_admin_links'],
412 'group_by' => ['group_by'],
413 'query' => ['query'],
414 'use_more' => ['use_more', 'use_more_always', 'use_more_text'],
415 'use_more_always' => ['use_more', 'use_more_always', 'use_more_text'],
416 'use_more_text' => ['use_more', 'use_more_always', 'use_more_text'],
417 'link_display' => ['link_display', 'link_url'],
419 // Force these to cascade properly.
420 'style' => ['style', 'row'],
421 'row' => ['style', 'row'],
423 'pager' => ['pager'],
425 'exposed_form' => ['exposed_form'],
427 // These sections are special.
428 'header' => ['header'],
429 'footer' => ['footer'],
430 'empty' => ['empty'],
431 'relationships' => ['relationships'],
432 'fields' => ['fields'],
433 'sorts' => ['sorts'],
434 'arguments' => ['arguments'],
435 'filters' => ['filters', 'filter_groups'],
436 'filter_groups' => ['filters', 'filter_groups'],
439 // If the display cannot use a pager, then we cannot default it.
440 if (!$this->usesPager()) {
441 unset($sections['pager']);
442 unset($sections['items_per_page']);
445 foreach ($this->extenders as $extender) {
446 $extender->defaultableSections($sections, $section);
450 if (!empty($sections[$section])) {
451 return $sections[$section];
459 protected function defineOptions() {
469 'display_description' => FALSE,
471 'hide_attachment_summary' => TRUE,
472 'show_admin_links' => TRUE,
475 'use_more_always' => TRUE,
476 'use_more_text' => TRUE,
477 'exposed_form' => TRUE,
479 'link_display' => TRUE,
490 'relationships' => TRUE,
495 'filter_groups' => TRUE,
505 'display_comment' => [
511 'display_description' => [
517 'hide_attachment_summary' => [
520 'show_admin_links' => [
526 'use_more_always' => [
541 'rendering_language' => [
542 'default' => '***LANGUAGE_entity_translation***',
545 // These types are all plugins that can have individual settings
546 // and therefore need special handling.
549 'type' => ['default' => 'none'],
550 'options' => ['default' => []],
552 'merge_defaults' => [$this, 'mergePlugin'],
556 'type' => ['default' => 'tag'],
557 'options' => ['default' => []],
559 'merge_defaults' => [$this, 'mergePlugin'],
563 'type' => ['default' => 'views_query'],
564 'options' => ['default' => []],
566 'merge_defaults' => [$this, 'mergePlugin'],
570 'type' => ['default' => 'basic'],
571 'options' => ['default' => []],
573 'merge_defaults' => [$this, 'mergePlugin'],
577 'type' => ['default' => 'mini'],
578 'options' => ['default' => []],
580 'merge_defaults' => [$this, 'mergePlugin'],
584 'type' => ['default' => 'default'],
585 'options' => ['default' => []],
587 'merge_defaults' => [$this, 'mergePlugin'],
591 'type' => ['default' => 'fields'],
592 'options' => ['default' => []],
594 'merge_defaults' => [$this, 'mergePlugin'],
603 'merge_defaults' => [$this, 'mergeHandler'],
607 'merge_defaults' => [$this, 'mergeHandler'],
611 'merge_defaults' => [$this, 'mergeHandler'],
614 // We want these to export last.
615 // These are the 5 handler types.
618 'merge_defaults' => [$this, 'mergeHandler'],
622 'merge_defaults' => [$this, 'mergeHandler'],
626 'merge_defaults' => [$this, 'mergeHandler'],
630 'merge_defaults' => [$this, 'mergeHandler'],
634 'operator' => ['default' => 'AND'],
635 'groups' => ['default' => [1 => 'AND']],
643 if (!$this->usesPager()) {
644 $options['defaults']['default']['pager'] = FALSE;
645 $options['pager']['contains']['type']['default'] = 'some';
648 if ($this->isDefaultDisplay()) {
649 unset($options['defaults']);
652 $options['display_extenders'] = ['default' => []];
653 // First allow display extenders to provide new options.
654 foreach ($this->extenders as $extender_id => $extender) {
655 $options['display_extenders']['contains'][$extender_id]['contains'] = $extender->defineOptions();
658 // Then allow display extenders to alter existing default values.
659 foreach ($this->extenders as $extender) {
660 $extender->defineOptionsAlter($options);
669 public function hasPath() {
676 public function usesLinkDisplay() {
677 return !$this->hasPath();
683 public function usesExposedFormInBlock() {
684 return $this->hasPath();
690 public function getAttachedDisplays() {
691 $current_display_id = $this->display['id'];
692 $attached_displays = [];
694 // Go through all displays and search displays which link to this one.
695 foreach ($this->view->storage->get('display') as $display_id => $display) {
696 if (isset($display['display_options']['displays'])) {
697 $displays = $display['display_options']['displays'];
698 if (isset($displays[$current_display_id])) {
699 $attached_displays[] = $display_id;
704 return $attached_displays;
710 public function getLinkDisplay() {
711 $display_id = $this->getOption('link_display');
712 // If unknown, pick the first one.
713 if (empty($display_id) || !$this->view->displayHandlers->has($display_id)) {
714 foreach ($this->view->displayHandlers as $display_id => $display) {
715 if (!empty($display) && $display->hasPath()) {
723 // Fall-through returns NULL.
729 public function getPath() {
730 if ($this->hasPath()) {
731 return $this->getOption('path');
734 $display_id = $this->getLinkDisplay();
735 if ($display_id && $this->view->displayHandlers->has($display_id) && is_object($this->view->displayHandlers->get($display_id))) {
736 return $this->view->displayHandlers->get($display_id)->getPath();
743 public function getRoutedDisplay() {
744 // If this display has a route, return this display.
745 if ($this instanceof DisplayRouterInterface) {
749 // If the display does not have a route (e.g. a block display), get the
750 // route for the linked display.
751 $display_id = $this->getLinkDisplay();
752 if ($display_id && $this->view->displayHandlers->has($display_id) && is_object($this->view->displayHandlers->get($display_id))) {
753 return $this->view->displayHandlers->get($display_id)->getRoutedDisplay();
756 // No routed display exists, so return NULL
763 public function getUrl() {
764 return $this->view->getUrl(NULL, $this->display['id']);
770 public function isDefaulted($option) {
771 return !$this->isDefaultDisplay() && !empty($this->default_display) && !empty($this->options['defaults'][$option]);
777 public function getOption($option) {
778 if ($this->isDefaulted($option)) {
779 return $this->default_display->getOption($option);
782 if (isset($this->options[$option]) || array_key_exists($option, $this->options)) {
783 return $this->options[$option];
790 public function usesFields() {
791 return $this->getPlugin('style')->usesFields();
797 public function getPlugin($type) {
798 // Look up the plugin name to use for this instance.
799 $options = $this->getOption($type);
801 // Return now if no options have been loaded.
802 if (empty($options) || !isset($options['type'])) {
806 // Query plugins allow specifying a specific query class per base table.
807 if ($type == 'query') {
808 $views_data = Views::viewsData()->get($this->view->storage->get('base_table'));
809 $name = isset($views_data['table']['base']['query_id']) ? $views_data['table']['base']['query_id'] : 'views_query';
812 $name = $options['type'];
815 // Plugin instances are stored on the display for re-use.
816 if (!isset($this->plugins[$type][$name])) {
817 $plugin = Views::pluginManager($type)->createInstance($name);
819 // Initialize the plugin.
820 $plugin->init($this->view, $this, $options['options']);
822 $this->plugins[$type][$name] = $plugin;
825 return $this->plugins[$type][$name];
831 public function &getHandler($type, $id) {
832 if (!isset($this->handlers[$type])) {
833 $this->getHandlers($type);
836 if (isset($this->handlers[$type][$id])) {
837 return $this->handlers[$type][$id];
840 // So we can return a reference.
848 public function &getHandlers($type) {
849 if (!isset($this->handlers[$type])) {
850 $this->handlers[$type] = [];
851 $types = ViewExecutable::getHandlerTypes();
852 $plural = $types[$type]['plural'];
854 // Cast to an array so that if the display does not have any handlers of
855 // this type there is no PHP error.
856 foreach ((array) $this->getOption($plural) as $id => $info) {
857 // If this is during form submission and there are temporary options
858 // which can only appear if the view is in the edit cache, use those
859 // options instead. This is used for AJAX multi-step stuff.
860 if ($this->view->getRequest()->request->get('form_id') && isset($this->view->temporary_options[$type][$id])) {
861 $info = $this->view->temporary_options[$type][$id];
864 if ($info['id'] != $id) {
868 // If aggregation is on, the group type might override the actual
869 // handler that is in use. This piece of code checks that and,
870 // if necessary, sets the override handler.
872 if ($this->useGroupBy() && !empty($info['group_type'])) {
873 if (empty($this->view->query)) {
874 $this->view->initQuery();
876 $aggregate = $this->view->query->getAggregationInfo();
877 if (!empty($aggregate[$info['group_type']]['handler'][$type])) {
878 $override = $aggregate[$info['group_type']]['handler'][$type];
882 if (!empty($types[$type]['type'])) {
883 $handler_type = $types[$type]['type'];
886 $handler_type = $type;
889 if ($handler = Views::handlerManager($handler_type)->getHandler($info, $override)) {
890 // Special override for area types so they know where they come from.
891 if ($handler instanceof AreaPluginBase) {
892 $handler->areaType = $type;
895 $handler->init($this->view, $this, $info);
896 $this->handlers[$type][$id] = &$handler;
899 // Prevent reference problems.
904 return $this->handlers[$type];
908 * Gets all the handlers used by the display.
910 * @param bool $only_overrides
911 * Whether to include only overridden handlers.
913 * @return \Drupal\views\Plugin\views\ViewsHandlerInterface[]
915 protected function getAllHandlers($only_overrides = FALSE) {
916 $handler_types = Views::getHandlerTypes();
918 // Collect all dependencies of all handlers.
919 foreach ($handler_types as $handler_type => $handler_type_info) {
920 if ($only_overrides && $this->isDefaulted($handler_type_info['plural'])) {
923 $handlers = array_merge($handlers, array_values($this->getHandlers($handler_type)));
929 * Gets all the plugins used by the display.
931 * @param bool $only_overrides
932 * Whether to include only overridden plugins.
934 * @return \Drupal\views\Plugin\views\ViewsPluginInterface[]
936 protected function getAllPlugins($only_overrides = FALSE) {
938 // Collect all dependencies of plugins.
939 foreach (Views::getPluginTypes('plugin') as $plugin_type) {
940 $plugin = $this->getPlugin($plugin_type);
944 if ($only_overrides && $this->isDefaulted($plugin_type)) {
947 $plugins[] = $plugin;
955 public function calculateDependencies() {
956 $this->dependencies = parent::calculateDependencies();
957 // Collect all the dependencies of handlers and plugins. Only calculate
958 // their dependencies if they are configured by this display.
959 $plugins = array_merge($this->getAllHandlers(TRUE), $this->getAllPlugins(TRUE));
960 array_walk($plugins, [$this, 'calculatePluginDependencies']);
962 return $this->dependencies;
968 public function getFieldLabels($groupable_only = FALSE) {
970 foreach ($this->getHandlers('relationship') as $relationship => $handler) {
971 $relationships[$relationship] = $handler->adminLabel();
974 foreach ($this->getHandlers('field') as $id => $handler) {
975 if ($groupable_only && !$handler->useStringGroupBy()) {
976 // Continue to next handler if it's not groupable.
979 if ($label = $handler->label()) {
980 $options[$id] = $label;
983 $options[$id] = $handler->adminLabel();
985 if (!empty($handler->options['relationship']) && !empty($relationships[$handler->options['relationship']])) {
986 $options[$id] = '(' . $relationships[$handler->options['relationship']] . ') ' . $options[$id];
995 public function setOption($option, $value) {
996 if ($this->isDefaulted($option)) {
997 return $this->default_display->setOption($option, $value);
1000 // Set this in two places: On the handler where we'll notice it
1001 // but also on the display object so it gets saved. This should
1002 // only be a temporary fix.
1003 $this->display['display_options'][$option] = $value;
1004 return $this->options[$option] = $value;
1010 public function overrideOption($option, $value) {
1011 $this->setOverride($option, FALSE);
1012 $this->setOption($option, $value);
1018 public function optionLink($text, $section, $class = '', $title = '') {
1020 $text = $this->t('Broken field');
1023 if (!empty($class)) {
1024 $text = new FormattableMarkup('<span>@text</span>', ['@text' => $text]);
1027 if (empty($title)) {
1031 return \Drupal::l($text, new Url('views_ui.form_display', [
1033 'view' => $this->view->storage->id(),
1034 'display_id' => $this->display['id'],
1038 'class' => ['views-ajax-link', $class],
1040 'id' => Html::getUniqueId('views-' . $this->display['id'] . '-' . $section),
1048 public function getArgumentsTokens() {
1050 if (!empty($this->view->build_info['substitutions'])) {
1051 $tokens = $this->view->build_info['substitutions'];
1060 public function optionsSummary(&$categories, &$options) {
1063 'title' => $this->t('Title'),
1064 'column' => 'first',
1067 'title' => $this->t('Format'),
1068 'column' => 'first',
1071 'title' => $this->t('Filters'),
1072 'column' => 'first',
1075 'title' => $this->t('Fields'),
1076 'column' => 'first',
1079 'title' => $this->t('Pager'),
1080 'column' => 'second',
1083 'title' => $this->t('Language'),
1084 'column' => 'second',
1087 'title' => $this->t('Exposed form'),
1088 'column' => 'third',
1095 'column' => 'second',
1101 'title' => $this->t('Other'),
1102 'column' => 'third',
1109 if ($this->display['id'] != 'default') {
1110 $options['display_id'] = [
1111 'category' => 'other',
1112 'title' => $this->t('Machine Name'),
1113 'value' => !empty($this->display['new_id']) ? $this->display['new_id'] : $this->display['id'],
1114 'desc' => $this->t('Change the machine name of this display.'),
1118 $display_comment = views_ui_truncate($this->getOption('display_comment'), 80);
1119 $options['display_comment'] = [
1120 'category' => 'other',
1121 'title' => $this->t('Administrative comment'),
1122 'value' => !empty($display_comment) ? $display_comment : $this->t('None'),
1123 'desc' => $this->t('Comment or document this display.'),
1126 $title = strip_tags($this->getOption('title'));
1128 $title = $this->t('None');
1131 $options['title'] = [
1132 'category' => 'title',
1133 'title' => $this->t('Title'),
1134 'value' => views_ui_truncate($title, 32),
1135 'desc' => $this->t('Change the title that this display will use.'),
1138 $style_plugin_instance = $this->getPlugin('style');
1139 $style_summary = empty($style_plugin_instance->definition['title']) ? $this->t('Missing style plugin') : $style_plugin_instance->summaryTitle();
1140 $style_title = empty($style_plugin_instance->definition['title']) ? $this->t('Missing style plugin') : $style_plugin_instance->pluginTitle();
1142 $options['style'] = [
1143 'category' => 'format',
1144 'title' => $this->t('Format'),
1145 'value' => $style_title,
1146 'setting' => $style_summary,
1147 'desc' => $this->t('Change the way content is formatted.'),
1150 // This adds a 'Settings' link to the style_options setting if the style has
1152 if ($style_plugin_instance->usesOptions()) {
1153 $options['style']['links']['style_options'] = $this->t('Change settings for this format');
1156 if ($style_plugin_instance->usesRowPlugin()) {
1157 $row_plugin_instance = $this->getPlugin('row');
1158 $row_summary = empty($row_plugin_instance->definition['title']) ? $this->t('Missing row plugin') : $row_plugin_instance->summaryTitle();
1159 $row_title = empty($row_plugin_instance->definition['title']) ? $this->t('Missing row plugin') : $row_plugin_instance->pluginTitle();
1162 'category' => 'format',
1163 'title' => $this->t('Show'),
1164 'value' => $row_title,
1165 'setting' => $row_summary,
1166 'desc' => $this->t('Change the way each row in the view is styled.'),
1168 // This adds a 'Settings' link to the row_options setting if the row style
1170 if ($row_plugin_instance->usesOptions()) {
1171 $options['row']['links']['row_options'] = $this->t('Change settings for this style');
1174 if ($this->usesAJAX()) {
1175 $options['use_ajax'] = [
1176 'category' => 'other',
1177 'title' => $this->t('Use AJAX'),
1178 'value' => $this->getOption('use_ajax') ? $this->t('Yes') : $this->t('No'),
1179 'desc' => $this->t('Change whether or not this display will use AJAX.'),
1182 if ($this->usesAttachments()) {
1183 $options['hide_attachment_summary'] = [
1184 'category' => 'other',
1185 'title' => $this->t('Hide attachments in summary'),
1186 'value' => $this->getOption('hide_attachment_summary') ? $this->t('Yes') : $this->t('No'),
1187 'desc' => $this->t('Change whether or not to display attachments when displaying a contextual filter summary.'),
1190 if (!isset($this->definition['contextual links locations']) || !empty($this->definition['contextual links locations'])) {
1191 $options['show_admin_links'] = [
1192 'category' => 'other',
1193 'title' => $this->t('Contextual links'),
1194 'value' => $this->getOption('show_admin_links') ? $this->t('Shown') : $this->t('Hidden'),
1195 'desc' => $this->t('Change whether or not to display contextual links for this view.'),
1199 $pager_plugin = $this->getPlugin('pager');
1200 if (!$pager_plugin) {
1201 // Default to the no pager plugin.
1202 $pager_plugin = Views::pluginManager('pager')->createInstance('none');
1205 $pager_str = $pager_plugin->summaryTitle();
1207 $options['pager'] = [
1208 'category' => 'pager',
1209 'title' => $this->t('Use pager'),
1210 'value' => $pager_plugin->pluginTitle(),
1211 'setting' => $pager_str,
1212 'desc' => $this->t("Change this display's pager setting."),
1215 // If pagers aren't allowed, change the text of the item.
1216 if (!$this->usesPager()) {
1217 $options['pager']['title'] = $this->t('Items to display');
1220 if ($pager_plugin->usesOptions()) {
1221 $options['pager']['links']['pager_options'] = $this->t('Change settings for this pager type.');
1224 if ($this->usesMore()) {
1225 $options['use_more'] = [
1226 'category' => 'pager',
1227 'title' => $this->t('More link'),
1228 'value' => $this->getOption('use_more') ? $this->t('Yes') : $this->t('No'),
1229 'desc' => $this->t('Specify whether this display will provide a "more" link.'),
1233 $this->view->initQuery();
1234 if ($this->view->query->getAggregationInfo()) {
1235 $options['group_by'] = [
1236 'category' => 'other',
1237 'title' => $this->t('Use aggregation'),
1238 'value' => $this->getOption('group_by') ? $this->t('Yes') : $this->t('No'),
1239 'desc' => $this->t('Allow grouping and aggregation (calculation) of fields.'),
1243 $options['query'] = [
1244 'category' => 'other',
1245 'title' => $this->t('Query settings'),
1246 'value' => $this->t('Settings'),
1247 'desc' => $this->t('Allow to set some advanced settings for the query plugin'),
1250 if (\Drupal::languageManager()->isMultilingual() && $this->isBaseTableTranslatable()) {
1251 $rendering_language_options = $this->buildRenderingLanguageOptions();
1252 $options['rendering_language'] = [
1253 'category' => 'language',
1254 'title' => $this->t('Rendering Language'),
1255 'value' => $rendering_language_options[$this->getOption('rendering_language')],
1256 'desc' => $this->t('All content that supports translations will be displayed in the selected language.'),
1260 $access_plugin = $this->getPlugin('access');
1261 if (!$access_plugin) {
1262 // Default to the no access control plugin.
1263 $access_plugin = Views::pluginManager('access')->createInstance('none');
1266 $access_str = $access_plugin->summaryTitle();
1268 $options['access'] = [
1269 'category' => 'access',
1270 'title' => $this->t('Access'),
1271 'value' => $access_plugin->pluginTitle(),
1272 'setting' => $access_str,
1273 'desc' => $this->t('Specify access control type for this display.'),
1276 if ($access_plugin->usesOptions()) {
1277 $options['access']['links']['access_options'] = $this->t('Change settings for this access type.');
1280 $cache_plugin = $this->getPlugin('cache');
1281 if (!$cache_plugin) {
1282 // Default to the no cache control plugin.
1283 $cache_plugin = Views::pluginManager('cache')->createInstance('none');
1286 $cache_str = $cache_plugin->summaryTitle();
1288 $options['cache'] = [
1289 'category' => 'other',
1290 'title' => $this->t('Caching'),
1291 'value' => $cache_plugin->pluginTitle(),
1292 'setting' => $cache_str,
1293 'desc' => $this->t('Specify caching type for this display.'),
1296 if ($cache_plugin->usesOptions()) {
1297 $options['cache']['links']['cache_options'] = $this->t('Change settings for this caching type.');
1300 if ($access_plugin->usesOptions()) {
1301 $options['access']['links']['access_options'] = $this->t('Change settings for this access type.');
1304 if ($this->usesLinkDisplay()) {
1305 $link_display_option = $this->getOption('link_display');
1306 $link_display = $this->t('None');
1308 if ($link_display_option == 'custom_url') {
1309 $link_display = $this->t('Custom URL');
1311 elseif (!empty($link_display_option)) {
1312 $display_id = $this->getLinkDisplay();
1313 $displays = $this->view->storage->get('display');
1314 if (!empty($displays[$display_id])) {
1315 $link_display = $displays[$display_id]['display_title'];
1319 $options['link_display'] = [
1320 'category' => 'pager',
1321 'title' => $this->t('Link display'),
1322 'value' => $link_display,
1323 'desc' => $this->t('Specify which display or custom URL this display will link to.'),
1327 if ($this->usesExposedFormInBlock()) {
1328 $options['exposed_block'] = [
1329 'category' => 'exposed',
1330 'title' => $this->t('Exposed form in block'),
1331 'value' => $this->getOption('exposed_block') ? $this->t('Yes') : $this->t('No'),
1332 'desc' => $this->t('Allow the exposed form to appear in a block instead of the view.'),
1336 /** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginInterface $exposed_form_plugin */
1337 $exposed_form_plugin = $this->getPlugin('exposed_form');
1338 if (!$exposed_form_plugin) {
1339 // Default to the no cache control plugin.
1340 $exposed_form_plugin = Views::pluginManager('exposed_form')->createInstance('basic');
1343 $exposed_form_str = $exposed_form_plugin->summaryTitle();
1345 $options['exposed_form'] = [
1346 'category' => 'exposed',
1347 'title' => $this->t('Exposed form style'),
1348 'value' => $exposed_form_plugin->pluginTitle(),
1349 'setting' => $exposed_form_str,
1350 'desc' => $this->t('Select the kind of exposed filter to use.'),
1353 if ($exposed_form_plugin->usesOptions()) {
1354 $options['exposed_form']['links']['exposed_form_options'] = $this->t('Exposed form settings for this exposed form style.');
1357 $css_class = trim($this->getOption('css_class'));
1359 $css_class = $this->t('None');
1362 $options['css_class'] = [
1363 'category' => 'other',
1364 'title' => $this->t('CSS class'),
1365 'value' => $css_class,
1366 'desc' => $this->t('Change the CSS class name(s) that will be added to this display.'),
1369 foreach ($this->extenders as $extender) {
1370 $extender->optionsSummary($categories, $options);
1377 public function buildOptionsForm(&$form, FormStateInterface $form_state) {
1378 parent::buildOptionsForm($form, $form_state);
1379 $section = $form_state->get('section');
1380 if ($this->defaultableSections($section)) {
1381 views_ui_standard_display_dropdown($form, $form_state, $section);
1383 $form['#title'] = $this->display['display_title'] . ': ';
1385 // Set the 'section' to highlight on the form.
1386 // If it's the item we're looking at is pulling from the default display,
1387 // reflect that. Don't use is_defaulted since we want it to show up even
1388 // on the default display.
1389 if (!empty($this->options['defaults'][$section])) {
1390 $form['#section'] = 'default-' . $section;
1393 $form['#section'] = $this->display['id'] . '-' . $section;
1398 $form['#title'] .= $this->t('The machine name of this display');
1399 $form['display_id'] = [
1400 '#type' => 'textfield',
1401 '#title' => $this->t('Machine name of the display'),
1402 '#default_value' => !empty($this->display['new_id']) ? $this->display['new_id'] : $this->display['id'],
1403 '#required' => TRUE,
1407 case 'display_title':
1408 $form['#title'] .= $this->t('The name and the description of this display');
1409 $form['display_title'] = [
1410 '#title' => $this->t('Administrative name'),
1411 '#type' => 'textfield',
1412 '#default_value' => $this->display['display_title'],
1414 $form['display_description'] = [
1415 '#title' => $this->t('Administrative description'),
1416 '#type' => 'textfield',
1417 '#default_value' => $this->getOption('display_description'),
1420 case 'display_comment':
1421 $form['#title'] .= $this->t('Administrative comment');
1422 $form['display_comment'] = [
1423 '#type' => 'textarea',
1424 '#title' => $this->t('Administrative comment'),
1425 '#description' => $this->t('This description will only be seen within the administrative interface and can be used to document this display.'),
1426 '#default_value' => $this->getOption('display_comment'),
1430 $form['#title'] .= $this->t('The title of this view');
1432 '#title' => $this->t('Title'),
1433 '#type' => 'textfield',
1434 '#description' => $this->t('This title will be displayed with the view, wherever titles are normally displayed; i.e, as the page title, block title, etc.'),
1435 '#default_value' => $this->getOption('title'),
1436 '#maxlength' => 255,
1440 $form['#title'] .= $this->t('CSS class');
1441 $form['css_class'] = [
1442 '#type' => 'textfield',
1443 '#title' => $this->t('CSS class name(s)'),
1444 '#description' => $this->t('Separate multiple classes by spaces.'),
1445 '#default_value' => $this->getOption('css_class'),
1449 $form['#title'] .= $this->t('AJAX');
1450 $form['use_ajax'] = [
1451 '#description' => $this->t('Options such as paging, table sorting, and exposed filters will not initiate a page refresh.'),
1452 '#type' => 'checkbox',
1453 '#title' => $this->t('Use AJAX'),
1454 '#default_value' => $this->getOption('use_ajax') ? 1 : 0,
1457 case 'hide_attachment_summary':
1458 $form['#title'] .= $this->t('Hide attachments when displaying a contextual filter summary');
1459 $form['hide_attachment_summary'] = [
1460 '#type' => 'checkbox',
1461 '#title' => $this->t('Hide attachments in summary'),
1462 '#default_value' => $this->getOption('hide_attachment_summary') ? 1 : 0,
1465 case 'show_admin_links':
1466 $form['#title'] .= $this->t('Show contextual links on this view.');
1467 $form['show_admin_links'] = [
1468 '#type' => 'checkbox',
1469 '#title' => $this->t('Show contextual links'),
1470 '#default_value' => $this->getOption('show_admin_links'),
1474 $form['#title'] .= $this->t('Add a more link to the bottom of the display.');
1475 $form['use_more'] = [
1476 '#type' => 'checkbox',
1477 '#title' => $this->t('Create more link'),
1478 '#description' => $this->t("This will add a more link to the bottom of this view, which will link to the page view. If you have more than one page view, the link will point to the display specified in 'Link display' section under pager. You can override the URL at the link display setting."),
1479 '#default_value' => $this->getOption('use_more'),
1481 $form['use_more_always'] = [
1482 '#type' => 'checkbox',
1483 '#title' => $this->t('Always display the more link'),
1484 '#description' => $this->t('Check this to display the more link even if there are no more items to display.'),
1485 '#default_value' => $this->getOption('use_more_always'),
1488 ':input[name="use_more"]' => ['checked' => TRUE],
1492 $form['use_more_text'] = [
1493 '#type' => 'textfield',
1494 '#title' => $this->t('More link text'),
1495 '#description' => $this->t('The text to display for the more link.'),
1496 '#default_value' => $this->getOption('use_more_text'),
1499 ':input[name="use_more"]' => ['checked' => TRUE],
1505 $form['#title'] .= $this->t('Allow grouping and aggregation (calculation) of fields.');
1506 $form['group_by'] = [
1507 '#type' => 'checkbox',
1508 '#title' => $this->t('Aggregate'),
1509 '#description' => $this->t('If enabled, some fields may become unavailable. All fields that are selected for grouping will be collapsed to one record per distinct value. Other fields which are selected for aggregation will have the function run on them. For example, you can group nodes on title and count the number of nids in order to get a list of duplicate titles.'),
1510 '#default_value' => $this->getOption('group_by'),
1514 $form['#title'] .= $this->t('Access restrictions');
1516 '#prefix' => '<div class="clearfix">',
1517 '#suffix' => '</div>',
1521 $access = $this->getOption('access');
1522 $form['access']['type'] = [
1523 '#title' => $this->t('Access'),
1524 '#title_display' => 'invisible',
1525 '#type' => 'radios',
1526 '#options' => Views::fetchPluginNames('access', $this->getType(), [$this->view->storage->get('base_table')]),
1527 '#default_value' => $access['type'],
1530 $access_plugin = $this->getPlugin('access');
1531 if ($access_plugin->usesOptions()) {
1533 '#prefix' => '<div class="js-form-item form-item description">',
1534 '#markup' => $this->t('You may also adjust the @settings for the currently selected access restriction.', ['@settings' => $this->optionLink($this->t('settings'), 'access_options')]),
1535 '#suffix' => '</div>',
1540 case 'access_options':
1541 $plugin = $this->getPlugin('access');
1542 $form['#title'] .= $this->t('Access options');
1544 $form['access_options'] = [
1547 $plugin->buildOptionsForm($form['access_options'], $form_state);
1551 $form['#title'] .= $this->t('Caching');
1553 '#prefix' => '<div class="clearfix">',
1554 '#suffix' => '</div>',
1558 $cache = $this->getOption('cache');
1559 $form['cache']['type'] = [
1560 '#title' => $this->t('Caching'),
1561 '#title_display' => 'invisible',
1562 '#type' => 'radios',
1563 '#options' => Views::fetchPluginNames('cache', $this->getType(), [$this->view->storage->get('base_table')]),
1564 '#default_value' => $cache['type'],
1567 $cache_plugin = $this->getPlugin('cache');
1568 if ($cache_plugin->usesOptions()) {
1570 '#prefix' => '<div class="js-form-item form-item description">',
1571 '#suffix' => '</div>',
1572 '#markup' => $this->t('You may also adjust the @settings for the currently selected cache mechanism.', ['@settings' => $this->optionLink($this->t('settings'), 'cache_options')]),
1576 case 'cache_options':
1577 $plugin = $this->getPlugin('cache');
1578 $form['#title'] .= $this->t('Caching options');
1580 $form['cache_options'] = [
1583 $plugin->buildOptionsForm($form['cache_options'], $form_state);
1587 $query_options = $this->getOption('query');
1588 $plugin_name = $query_options['type'];
1590 $form['#title'] .= $this->t('Query options');
1591 $this->view->initQuery();
1592 if ($this->view->query) {
1597 '#value' => $plugin_name,
1604 $this->view->query->buildOptionsForm($form['query']['options'], $form_state);
1607 case 'rendering_language':
1608 $form['#title'] .= $this->t('Rendering language');
1609 if (\Drupal::languageManager()->isMultilingual() && $this->isBaseTableTranslatable()) {
1610 $options = $this->buildRenderingLanguageOptions();
1611 $form['rendering_language'] = [
1612 '#type' => 'select',
1613 '#options' => $options,
1614 '#title' => $this->t('Rendering language'),
1615 '#description' => $this->t('All content that supports translations will be displayed in the selected language.'),
1616 '#default_value' => $this->getOption('rendering_language'),
1620 $form['rendering_language']['#markup'] = $this->t('The view is not based on a translatable entity type or the site is not multilingual.');
1624 $form['#title'] .= $this->t('How should this view be styled');
1625 $style_plugin = $this->getPlugin('style');
1627 '#prefix' => '<div class="clearfix">',
1628 '#suffix' => '</div>',
1631 $form['style']['type'] = [
1632 '#title' => $this->t('Style'),
1633 '#title_display' => 'invisible',
1634 '#type' => 'radios',
1635 '#options' => Views::fetchPluginNames('style', $this->getType(), [$this->view->storage->get('base_table')]),
1636 '#default_value' => $style_plugin->definition['id'],
1637 '#description' => $this->t('If the style you choose has settings, be sure to click the settings button that will appear next to it in the View summary.'),
1640 if ($style_plugin->usesOptions()) {
1642 '#prefix' => '<div class="js-form-item form-item description">',
1643 '#suffix' => '</div>',
1644 '#markup' => $this->t('You may also adjust the @settings for the currently selected style.', ['@settings' => $this->optionLink($this->t('settings'), 'style_options')]),
1649 case 'style_options':
1650 $form['#title'] .= $this->t('Style options');
1652 $style_plugin = $this->getOption('style');
1653 $name = $style_plugin['type'];
1656 if (!isset($name)) {
1657 $row_plugin = $this->getOption('row');
1658 $name = $row_plugin['type'];
1660 // If row, $style will be empty.
1661 if (empty($style)) {
1662 $form['#title'] .= $this->t('Row style options');
1664 $plugin = $this->getPlugin(empty($style) ? 'row' : 'style', $name);
1669 $plugin->buildOptionsForm($form[$section], $form_state);
1673 $form['#title'] .= $this->t('How should each row in this view be styled');
1674 $row_plugin_instance = $this->getPlugin('row');
1676 '#prefix' => '<div class="clearfix">',
1677 '#suffix' => '</div>',
1680 $form['row']['type'] = [
1681 '#title' => $this->t('Row'),
1682 '#title_display' => 'invisible',
1683 '#type' => 'radios',
1684 '#options' => Views::fetchPluginNames('row', $this->getType(), [$this->view->storage->get('base_table')]),
1685 '#default_value' => $row_plugin_instance->definition['id'],
1688 if ($row_plugin_instance->usesOptions()) {
1690 '#prefix' => '<div class="js-form-item form-item description">',
1691 '#suffix' => '</div>',
1692 '#markup' => $this->t('You may also adjust the @settings for the currently selected row style.', ['@settings' => $this->optionLink($this->t('settings'), 'row_options')]),
1697 case 'link_display':
1698 $form['#title'] .= $this->t('Which display to use for path');
1699 $options = [FALSE => $this->t('None'), 'custom_url' => $this->t('Custom URL')];
1701 foreach ($this->view->storage->get('display') as $display_id => $display) {
1702 if ($this->view->displayHandlers->get($display_id)->hasPath()) {
1703 $options[$display_id] = $display['display_title'];
1707 $form['link_display'] = [
1708 '#type' => 'radios',
1709 '#options' => $options,
1710 '#description' => $this->t("Which display to use to get this display's path for things like summary links, rss feed links, more links, etc."),
1711 '#default_value' => $this->getOption('link_display'),
1715 $optgroup_arguments = (string) t('Arguments');
1716 foreach ($this->view->display_handler->getHandlers('argument') as $arg => $handler) {
1717 $options[$optgroup_arguments]["{{ arguments.$arg }}"] = $this->t('@argument title', ['@argument' => $handler->adminLabel()]);
1718 $options[$optgroup_arguments]["{{ raw_arguments.$arg }}"] = $this->t('@argument input', ['@argument' => $handler->adminLabel()]);
1722 // We have some options, so make a list.
1725 '#markup' => $this->t('A Drupal path or external URL the more link will point to. Note that this will override the link display setting above.'),
1727 if (!empty($options)) {
1730 '#markup' => $this->t('The following tokens are available for this link. You may use Twig syntax in this field.'),
1731 '#suffix' => '</p>',
1733 foreach (array_keys($options) as $type) {
1734 if (!empty($options[$type])) {
1736 foreach ($options[$type] as $key => $value) {
1737 $items[] = $key . ' == ' . $value;
1740 '#theme' => 'item_list',
1743 $description[] = $item_list;
1748 $form['link_url'] = [
1749 '#type' => 'textfield',
1750 '#title' => $this->t('Custom URL'),
1751 '#default_value' => $this->getOption('link_url'),
1752 '#description' => $description,
1755 ':input[name="link_display"]' => ['value' => 'custom_url'],
1760 case 'exposed_block':
1761 $form['#title'] .= $this->t('Put the exposed form in a block');
1762 $form['description'] = [
1763 '#markup' => '<div class="js-form-item form-item description">' . $this->t('If set, any exposed widgets will not appear with this view. Instead, a block will be made available to the Drupal block administration system, and the exposed form will appear there. Note that this block must be enabled manually, Views will not enable it for you.') . '</div>',
1765 $form['exposed_block'] = [
1766 '#type' => 'radios',
1767 '#options' => [1 => $this->t('Yes'), 0 => $this->t('No')],
1768 '#default_value' => $this->getOption('exposed_block') ? 1 : 0,
1771 case 'exposed_form':
1772 $form['#title'] .= $this->t('Exposed Form');
1773 $form['exposed_form'] = [
1774 '#prefix' => '<div class="clearfix">',
1775 '#suffix' => '</div>',
1779 $exposed_form = $this->getOption('exposed_form');
1780 $form['exposed_form']['type'] = [
1781 '#title' => $this->t('Exposed form'),
1782 '#title_display' => 'invisible',
1783 '#type' => 'radios',
1784 '#options' => Views::fetchPluginNames('exposed_form', $this->getType(), [$this->view->storage->get('base_table')]),
1785 '#default_value' => $exposed_form['type'],
1788 $exposed_form_plugin = $this->getPlugin('exposed_form');
1789 if ($exposed_form_plugin->usesOptions()) {
1791 '#prefix' => '<div class="js-form-item form-item description">',
1792 '#suffix' => '</div>',
1793 '#markup' => $this->t('You may also adjust the @settings for the currently selected style.', ['@settings' => $this->optionLink($this->t('settings'), 'exposed_form_options')]),
1797 case 'exposed_form_options':
1798 $plugin = $this->getPlugin('exposed_form');
1799 $form['#title'] .= $this->t('Exposed form options');
1801 $form['exposed_form_options'] = [
1804 $plugin->buildOptionsForm($form['exposed_form_options'], $form_state);
1808 $form['#title'] .= $this->t('Select pager');
1810 '#prefix' => '<div class="clearfix">',
1811 '#suffix' => '</div>',
1815 $pager = $this->getOption('pager');
1816 $form['pager']['type'] = [
1817 '#title' => $this->t('Pager'),
1818 '#title_display' => 'invisible',
1819 '#type' => 'radios',
1820 '#options' => Views::fetchPluginNames('pager', !$this->usesPager() ? 'basic' : NULL, [$this->view->storage->get('base_table')]),
1821 '#default_value' => $pager['type'],
1824 $pager_plugin = $this->getPlugin('pager');
1825 if ($pager_plugin->usesOptions()) {
1827 '#prefix' => '<div class="js-form-item form-item description">',
1828 '#suffix' => '</div>',
1829 '#markup' => $this->t('You may also adjust the @settings for the currently selected pager.', ['@settings' => $this->optionLink($this->t('settings'), 'pager_options')]),
1834 case 'pager_options':
1835 $plugin = $this->getPlugin('pager');
1836 $form['#title'] .= $this->t('Pager options');
1838 $form['pager_options'] = [
1841 $plugin->buildOptionsForm($form['pager_options'], $form_state);
1846 foreach ($this->extenders as $extender) {
1847 $extender->buildOptionsForm($form, $form_state);
1854 public function validateOptionsForm(&$form, FormStateInterface $form_state) {
1855 $section = $form_state->get('section');
1857 case 'display_title':
1858 if ($form_state->isValueEmpty('display_title')) {
1859 $form_state->setError($form['display_title'], $this->t('Display title may not be empty.'));
1863 $css_class = $form_state->getValue('css_class');
1864 if (preg_match('/[^a-zA-Z0-9-_ ]/', $css_class)) {
1865 $form_state->setError($form['css_class'], $this->t('CSS classes must be alphanumeric or dashes only.'));
1869 if ($form_state->getValue('display_id')) {
1870 if (preg_match('/[^a-z0-9_]/', $form_state->getValue('display_id'))) {
1871 $form_state->setError($form['display_id'], $this->t('Display name must be letters, numbers, or underscores only.'));
1874 foreach ($this->view->displayHandlers as $id => $display) {
1875 if ($id != $this->view->current_display && ($form_state->getValue('display_id') == $id || (isset($display->new_id) && $form_state->getValue('display_id') == $display->new_id))) {
1876 $form_state->setError($form['display_id'], $this->t('Display id should be unique.'));
1882 if ($this->view->query) {
1883 $this->view->query->validateOptionsForm($form['query'], $form_state);
1888 // Validate plugin options. Every section with "_options" in it, belongs to
1889 // a plugin type, like "style_options".
1890 if (strpos($section, '_options') !== FALSE) {
1891 $plugin_type = str_replace('_options', '', $section);
1892 // Load the plugin and let it handle the validation.
1893 if ($plugin = $this->getPlugin($plugin_type)) {
1894 $plugin->validateOptionsForm($form[$section], $form_state);
1898 foreach ($this->extenders as $extender) {
1899 $extender->validateOptionsForm($form, $form_state);
1906 public function submitOptionsForm(&$form, FormStateInterface $form_state) {
1907 // Not sure I like this being here, but it seems (?) like a logical place.
1908 $cache_plugin = $this->getPlugin('cache');
1909 if ($cache_plugin) {
1910 $cache_plugin->cacheFlush();
1913 $section = $form_state->get('section');
1916 if ($form_state->hasValue('display_id')) {
1917 $this->display['new_id'] = $form_state->getValue('display_id');
1920 case 'display_title':
1921 $this->display['display_title'] = $form_state->getValue('display_title');
1922 $this->setOption('display_description', $form_state->getValue('display_description'));
1925 $plugin = $this->getPlugin('query');
1927 $plugin->submitOptionsForm($form['query']['options'], $form_state);
1928 $this->setOption('query', $form_state->getValue($section));
1932 case 'link_display':
1933 $this->setOption('link_url', $form_state->getValue('link_url'));
1936 case 'display_comment':
1939 $this->setOption($section, $form_state->getValue($section));
1941 case 'rendering_language':
1942 $this->setOption('rendering_language', $form_state->getValue('rendering_language'));
1945 case 'hide_attachment_summary':
1946 case 'show_admin_links':
1947 case 'exposed_block':
1948 $this->setOption($section, (bool) $form_state->getValue($section));
1951 $this->setOption($section, intval($form_state->getValue($section)));
1952 $this->setOption('use_more_always', intval($form_state->getValue('use_more_always')));
1953 $this->setOption('use_more_text', $form_state->getValue('use_more_text'));
1958 case 'exposed_form':
1962 $plugin_type = $section;
1963 $plugin_options = $this->getOption($plugin_type);
1964 $type = $form_state->getValue([$plugin_type, 'type']);
1965 if ($plugin_options['type'] != $type) {
1966 /** @var \Drupal\views\Plugin\views\ViewsPluginInterface $plugin */
1967 $plugin = Views::pluginManager($plugin_type)->createInstance($type);
1969 $plugin->init($this->view, $this, $plugin_options['options']);
1972 'options' => $plugin->options,
1974 $plugin->filterByDefinedOptions($plugin_options['options']);
1975 $this->setOption($plugin_type, $plugin_options);
1976 if ($plugin->usesOptions()) {
1977 $form_state->get('view')->addFormToStack('display', $this->display['id'], $plugin_type . '_options');
1983 case 'access_options':
1984 case 'cache_options':
1985 case 'exposed_form_options':
1986 case 'pager_options':
1988 case 'style_options':
1989 // Submit plugin options. Every section with "_options" in it, belongs to
1990 // a plugin type, like "style_options".
1991 $plugin_type = str_replace('_options', '', $section);
1992 if ($plugin = $this->getPlugin($plugin_type)) {
1993 $plugin_options = $this->getOption($plugin_type);
1994 $plugin->submitOptionsForm($form[$plugin_type . '_options'], $form_state);
1995 $plugin_options['options'] = $form_state->getValue($section);
1996 $this->setOption($plugin_type, $plugin_options);
2001 $extender_options = $this->getOption('display_extenders');
2002 foreach ($this->extenders as $extender) {
2003 $extender->submitOptionsForm($form, $form_state);
2005 $plugin_id = $extender->getPluginId();
2006 $extender_options[$plugin_id] = $extender->options;
2008 $this->setOption('display_extenders', $extender_options);
2014 public function optionsOverride($form, FormStateInterface $form_state) {
2015 $this->setOverride($form_state->get('section'));
2021 public function setOverride($section, $new_state = NULL) {
2022 $options = $this->defaultableSections($section);
2027 if (!isset($new_state)) {
2028 $new_state = empty($this->options['defaults'][$section]);
2031 // For each option that is part of this group, fix our settings.
2032 foreach ($options as $option) {
2034 // Revert to defaults.
2035 unset($this->options[$option]);
2036 unset($this->display['display_options'][$option]);
2039 // Copy existing values into our display.
2040 $this->options[$option] = $this->getOption($option);
2041 $this->display['display_options'][$option] = $this->options[$option];
2043 $this->options['defaults'][$option] = $new_state;
2044 $this->display['display_options']['defaults'][$option] = $new_state;
2051 public function query() {
2052 foreach ($this->extenders as $extender) {
2060 public function renderFilters() {}
2065 public function renderPager() {
2072 public function renderMoreLink() {
2073 if ($this->isMoreEnabled() && ($this->useMoreAlways() || (!empty($this->view->pager) && $this->view->pager->hasMoreRecords()))) {
2074 // If the user has supplied a custom "More" link path, replace any
2075 // argument tokens and use that for the URL.
2076 if ($this->getOption('link_display') == 'custom_url' && $override_path = $this->getOption('link_url')) {
2077 $tokens = $this->getArgumentsTokens();
2078 $path = $this->viewsTokenReplace($override_path, $tokens);
2079 // @todo Views should expect and store a leading /. See:
2080 // https://www.drupal.org/node/2423913
2081 $url = Url::fromUserInput('/' . $path);
2083 // Otherwise, use the URL for the display.
2085 $url = $this->view->getUrl(NULL, $this->display['id']);
2088 // If a URL is available (either from the display or a custom path),
2089 // render the "More" link.
2092 if (!empty($this->view->exposed_raw_input)) {
2093 $url_options['query'] = $this->view->exposed_raw_input;
2095 $url->setOptions($url_options);
2098 '#type' => 'more_link',
2100 '#title' => $this->useMoreText(),
2101 '#view' => $this->view,
2110 public function render() {
2111 $rows = (!empty($this->view->result) || $this->view->style_plugin->evenEmpty()) ? $this->view->style_plugin->render($this->view->result) : [];
2114 '#theme' => $this->themeFunctions(),
2115 '#view' => $this->view,
2116 '#pre_render' => [[$this, 'elementPreRender']],
2118 // Assigned by reference so anything added in $element['#attached'] will
2119 // be available on the view.
2120 '#attached' => &$this->view->element['#attached'],
2121 '#cache' => &$this->view->element['#cache'],
2124 $this->applyDisplayCacheabilityMetadata($this->view->element);
2130 * Applies the cacheability of the current display to the given render array.
2132 * @param array $element
2133 * The render array with updated cacheability metadata.
2135 protected function applyDisplayCacheabilityMetadata(array &$element) {
2136 /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache */
2137 $cache = $this->getPlugin('cache');
2139 (new CacheableMetadata())
2140 ->setCacheTags(Cache::mergeTags($this->view->getCacheTags(), isset($this->display['cache_metadata']['tags']) ? $this->display['cache_metadata']['tags'] : []))
2141 ->setCacheContexts(isset($this->display['cache_metadata']['contexts']) ? $this->display['cache_metadata']['contexts'] : [])
2142 ->setCacheMaxAge(Cache::mergeMaxAges($cache->getCacheMaxAge(), isset($this->display['cache_metadata']['max-age']) ? $this->display['cache_metadata']['max-age'] : Cache::PERMANENT))
2143 ->merge(CacheableMetadata::createFromRenderArray($element))
2144 ->applyTo($element);
2148 * Applies the cacheability of the current display to the given render array.
2150 * @param array $element
2151 * The render array with updated cacheability metadata.
2153 * @deprecated in Drupal 8.4.0, will be removed before Drupal 9.0. Use
2154 * DisplayPluginBase::applyDisplayCacheabilityMetadata instead.
2156 * @see \Drupal\views\Plugin\views\display\DisplayPluginBase::applyDisplayCacheabilityMetadata()
2158 protected function applyDisplayCachablityMetadata(array &$element) {
2159 @trigger_error('The DisplayPluginBase::applyDisplayCachablityMetadata method is deprecated since version 8.4 and will be removed in 9.0. Use DisplayPluginBase::applyDisplayCacheabilityMetadata instead.', E_USER_DEPRECATED);
2160 $this->applyDisplayCacheabilityMetadata($element);
2166 public function elementPreRender(array $element) {
2167 $view = $element['#view'];
2168 $empty = empty($view->result);
2170 // Force a render array so CSS/JS can be attached.
2171 if (!is_array($element['#rows'])) {
2172 $element['#rows'] = ['#markup' => $element['#rows']];
2175 $element['#header'] = $view->display_handler->renderArea('header', $empty);
2176 $element['#footer'] = $view->display_handler->renderArea('footer', $empty);
2177 $element['#empty'] = $empty ? $view->display_handler->renderArea('empty', $empty) : [];
2178 $element['#exposed'] = !empty($view->exposed_widgets) ? $view->exposed_widgets : [];
2179 $element['#more'] = $view->display_handler->renderMoreLink();
2180 $element['#feed_icons'] = !empty($view->feedIcons) ? $view->feedIcons : [];
2182 if ($view->display_handler->renderPager()) {
2183 $exposed_input = isset($view->exposed_raw_input) ? $view->exposed_raw_input : NULL;
2184 $element['#pager'] = $view->renderPager($exposed_input);
2187 if (!empty($view->attachment_before)) {
2188 $element['#attachment_before'] = $view->attachment_before;
2190 if (!empty($view->attachment_after)) {
2191 $element['#attachment_after'] = $view->attachment_after;
2194 // If form fields were found in the view, reformat the view output as a form.
2195 if ($view->hasFormElements()) {
2196 // Only render row output if there are rows. Otherwise, render the empty
2198 if (!empty($element['#rows'])) {
2199 $output = $element['#rows'];
2202 $output = $element['#empty'];
2205 $form_object = ViewsForm::create(\Drupal::getContainer(), $view->storage->id(), $view->current_display, $view->args);
2206 $form = \Drupal::formBuilder()->getForm($form_object, $view, $output);
2207 // The form is requesting that all non-essential views elements be hidden,
2208 // usually because the rendered step is not a view result.
2209 if ($form['show_view_elements']['#value'] == FALSE) {
2210 $element['#header'] = [];
2211 $element['#exposed'] = [];
2212 $element['#pager'] = [];
2213 $element['#footer'] = [];
2214 $element['#more'] = [];
2215 $element['#feed_icons'] = [];
2218 $element['#rows'] = $form;
2227 public function renderArea($area, $empty = FALSE) {
2229 foreach ($this->getHandlers($area) as $key => $area_handler) {
2230 if ($area_render = $area_handler->render($empty)) {
2231 if (isset($area_handler->position)) {
2232 // Fix weight of area.
2233 $area_render['#weight'] = $area_handler->position;
2235 $return[$key] = $area_render;
2244 public function access(AccountInterface $account = NULL) {
2245 if (!isset($account)) {
2246 $account = \Drupal::currentUser();
2249 $plugin = $this->getPlugin('access');
2250 /** @var \Drupal\views\Plugin\views\access\AccessPluginBase $plugin */
2252 return $plugin->access($account);
2255 // Fallback to all access if no plugin.
2262 public function preExecute() {
2263 $this->view->setAjaxEnabled($this->ajaxEnabled());
2264 if ($this->isMoreEnabled() && !$this->useMoreAlways()) {
2265 $this->view->get_total_rows = TRUE;
2267 $this->view->initHandlers();
2268 if ($this->usesExposed()) {
2269 /** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginInterface $exposed_form */
2270 $exposed_form = $this->getPlugin('exposed_form');
2271 $exposed_form->preExecute();
2274 foreach ($this->extenders as $extender) {
2275 $extender->preExecute();
2282 public function calculateCacheMetadata() {
2283 $cache_metadata = new CacheableMetadata();
2285 // Iterate over ordinary views plugins.
2286 foreach (Views::getPluginTypes('plugin') as $plugin_type) {
2287 $plugin = $this->getPlugin($plugin_type);
2288 if ($plugin instanceof CacheableDependencyInterface) {
2289 $cache_metadata = $cache_metadata->merge(CacheableMetadata::createFromObject($plugin));
2293 // Iterate over all handlers. Note that at least the argument handler will
2294 // need to ask all its subplugins.
2295 foreach (array_keys(Views::getHandlerTypes()) as $handler_type) {
2296 $handlers = $this->getHandlers($handler_type);
2297 foreach ($handlers as $handler) {
2298 if ($handler instanceof CacheableDependencyInterface) {
2299 $cache_metadata = $cache_metadata->merge(CacheableMetadata::createFromObject($handler));
2304 /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache_plugin */
2305 if ($cache_plugin = $this->getPlugin('cache')) {
2306 $cache_plugin->alterCacheMetadata($cache_metadata);
2309 return $cache_metadata;
2315 public function getCacheMetadata() {
2316 if (!isset($this->display['cache_metadata'])) {
2317 $cache_metadata = $this->calculateCacheMetadata();
2318 $this->display['cache_metadata']['max-age'] = $cache_metadata->getCacheMaxAge();
2319 $this->display['cache_metadata']['contexts'] = $cache_metadata->getCacheContexts();
2320 $this->display['cache_metadata']['tags'] = $cache_metadata->getCacheTags();
2323 $cache_metadata = (new CacheableMetadata())
2324 ->setCacheMaxAge($this->display['cache_metadata']['max-age'])
2325 ->setCacheContexts($this->display['cache_metadata']['contexts'])
2326 ->setCacheTags($this->display['cache_metadata']['tags']);
2328 return $cache_metadata;
2334 public function execute() {}
2339 public function buildRenderable(array $args = [], $cache = TRUE) {
2340 $this->view->element += [
2342 '#name' => $this->view->storage->id(),
2343 '#display_id' => $this->display['id'],
2344 '#arguments' => $args,
2346 '#view' => $this->view,
2347 '#cache_properties' => ['#view_id', '#view_display_show_admin_links', '#view_display_plugin_id'],
2350 // When something passes $cache = FALSE, they're asking us not to create our
2351 // own render cache for it. However, we still need to include certain pieces
2352 // of cacheability metadata (e.g.: cache contexts), so they can bubble up.
2353 // Thus, we add the cacheability metadata first, then modify / remove the
2354 // cache keys depending on the $cache argument.
2355 $this->applyDisplayCacheabilityMetadata($this->view->element);
2357 $this->view->element['#cache'] += ['keys' => []];
2358 // Places like \Drupal\views\ViewExecutable::setCurrentPage() set up an
2359 // additional cache context.
2360 $this->view->element['#cache']['keys'] = array_merge(['views', 'display', $this->view->element['#name'], $this->view->element['#display_id']], $this->view->element['#cache']['keys']);
2363 // Remove the cache keys, to ensure render caching is not triggered. We
2364 // don't unset the other #cache values, to allow cacheability metadata to
2365 // still be bubbled.
2366 unset($this->view->element['#cache']['keys']);
2369 return $this->view->element;
2375 public static function buildBasicRenderable($view_id, $display_id, array $args = []) {
2378 '#name' => $view_id,
2379 '#display_id' => $display_id,
2380 '#arguments' => $args,
2383 'keys' => ['view', $view_id, 'display', $display_id],
2388 $build['#cache']['keys'][] = 'args';
2389 $build['#cache']['keys'][] = implode(',', $args);
2392 $build['#cache_properties'] = ['#view_id', '#view_display_show_admin_links', '#view_display_plugin_id'];
2401 public function preview() {
2402 return $this->view->render();
2408 public function getType() {
2415 public function validate() {
2417 // Make sure displays that use fields HAVE fields.
2418 if ($this->usesFields()) {
2420 foreach ($this->getHandlers('field') as $field) {
2421 if (empty($field->options['exclude'])) {
2427 $errors[] = $this->t('Display "@display" uses fields but there are none defined for it or all are excluded.', ['@display' => $this->display['display_title']]);
2431 // Validate the more link.
2432 if ($this->isMoreEnabled() && $this->getOption('link_display') !== 'custom_url') {
2433 $routed_display = $this->getRoutedDisplay();
2434 if (!$routed_display || !$routed_display->isEnabled()) {
2435 $errors[] = $this->t('Display "@display" uses a "more" link but there are no displays it can link to. You need to specify a custom URL.', ['@display' => $this->display['display_title']]);
2439 if ($this->hasPath() && !$this->getOption('path')) {
2440 $errors[] = $this->t('Display "@display" uses a path but the path is undefined.', ['@display' => $this->display['display_title']]);
2443 // Validate style plugin.
2444 $style = $this->getPlugin('style');
2445 if (empty($style)) {
2446 $errors[] = $this->t('Display "@display" has an invalid style plugin.', ['@display' => $this->display['display_title']]);
2449 $result = $style->validate();
2450 if (!empty($result) && is_array($result)) {
2451 $errors = array_merge($errors, $result);
2455 // Validate query plugin.
2456 $query = $this->getPlugin('query');
2457 $result = $query->validate();
2458 if (!empty($result) && is_array($result)) {
2459 $errors = array_merge($errors, $result);
2462 // Check for missing relationships.
2463 $relationships = array_keys($this->getHandlers('relationship'));
2464 foreach (ViewExecutable::getHandlerTypes() as $type => $handler_type_info) {
2465 foreach ($this->getHandlers($type) as $handler_id => $handler) {
2466 if (!empty($handler->options['relationship']) && $handler->options['relationship'] != 'none' && !in_array($handler->options['relationship'], $relationships)) {
2467 $errors[] = $this->t('The %handler_type %handler uses a relationship that has been removed.', ['%handler_type' => $handler_type_info['lstitle'], '%handler' => $handler->adminLabel()]);
2472 // Validate handlers.
2473 foreach (ViewExecutable::getHandlerTypes() as $type => $info) {
2474 foreach ($this->getHandlers($type) as $handler) {
2475 $result = $handler->validate();
2476 if (!empty($result) && is_array($result)) {
2477 $errors = array_merge($errors, $result);
2488 public function newDisplay() {
2494 public function isIdentifierUnique($id, $identifier) {
2495 foreach (ViewExecutable::getHandlerTypes() as $type => $info) {
2496 foreach ($this->getHandlers($type) as $key => $handler) {
2497 if ($handler->canExpose() && $handler->isExposed()) {
2498 if ($handler->isAGroup()) {
2499 if ($id != $key && $identifier == $handler->options['group_info']['identifier']) {
2504 if ($id != $key && $identifier == $handler->options['expose']['identifier']) {
2517 public function outputIsEmpty() {
2518 if (!empty($this->view->result)) {
2522 // Check whether all of the area handlers are empty.
2523 foreach (['empty', 'footer', 'header'] as $type) {
2524 $handlers = $this->getHandlers($type);
2525 foreach ($handlers as $handler) {
2526 // If one is not empty, return FALSE now.
2527 if (!$handler->isEmpty()) {
2539 public function getSpecialBlocks() {
2542 if ($this->usesExposedFormInBlock()) {
2543 $delta = '-exp-' . $this->view->storage->id() . '-' . $this->display['id'];
2544 $desc = $this->t('Exposed form: @view-@display_id', ['@view' => $this->view->storage->id(), '@display_id' => $this->display['id']]);
2557 public function viewExposedFormBlocks() {
2558 // Avoid interfering with the admin forms.
2559 $route_name = \Drupal::routeMatch()->getRouteName();
2560 if (strpos($route_name, 'views_ui.') === 0) {
2563 $this->view->initHandlers();
2565 if ($this->usesExposed() && $this->getOption('exposed_block')) {
2566 /** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginInterface $exposed_form */
2567 $exposed_form = $this->getPlugin('exposed_form');
2568 return $exposed_form->renderExposedForm(TRUE);
2575 public function getArgumentText() {
2577 'filter value not present' => $this->t('When the filter value is <em>NOT</em> available'),
2578 'filter value present' => $this->t('When the filter value <em>IS</em> available or a default is provided'),
2579 'description' => $this->t("This display does not have a source for contextual filters, so no contextual filter value will be available unless you select 'Provide default'."),
2586 public function getPagerText() {
2588 'items per page title' => $this->t('Items to display'),
2589 'items per page description' => $this->t('Enter 0 for no limit.'),
2596 public function mergeDefaults() {
2597 $defined_options = $this->defineOptions();
2599 // Build a map of plural => singular for handler types.
2601 foreach (ViewExecutable::getHandlerTypes() as $type => $info) {
2602 $type_map[$info['plural']] = $type;
2605 // Find all defined options, that have specified a merge_defaults callback.
2606 foreach ($defined_options as $type => $definition) {
2607 if (!isset($definition['merge_defaults']) || !is_callable($definition['merge_defaults'])) {
2610 // Switch the type to singular, if it's a plural handler.
2611 if (isset($type_map[$type])) {
2612 $type = $type_map[$type];
2615 call_user_func($definition['merge_defaults'], $type);
2622 public function remove() {
2627 * Merges plugins default values.
2629 * @param string $type
2630 * The name of the plugin type option.
2632 protected function mergePlugin($type) {
2633 if (($options = $this->getOption($type)) && isset($options['options'])) {
2634 $plugin = $this->getPlugin($type);
2635 $options['options'] = $options['options'] + $plugin->options;
2636 $this->setOption($type, $options);
2641 * Merges handlers default values.
2643 * @param string $type
2644 * The name of the handler type option.
2646 protected function mergeHandler($type) {
2647 $types = ViewExecutable::getHandlerTypes();
2649 $options = $this->getOption($types[$type]['plural']);
2650 foreach ($this->getHandlers($type) as $id => $handler) {
2651 if (isset($options[$id])) {
2652 $options[$id] = $options[$id] + $handler->options;
2656 $this->setOption($types[$type]['plural'], $options);
2662 public function getExtenders() {
2663 return $this->extenders;
2667 * Returns the available rendering strategies for language-aware entities.
2670 * An array of available entity row renderers keyed by renderer identifiers.
2672 protected function buildRenderingLanguageOptions() {
2673 // @todo Consider making these plugins. See
2674 // https://www.drupal.org/node/2173811.
2675 // Pass the current rendering language (in this case a one element array) so
2676 // is not lost when there are language configuration changes.
2677 return $this->listLanguages(LanguageInterface::STATE_CONFIGURABLE | LanguageInterface::STATE_SITE_DEFAULT | PluginBase::INCLUDE_NEGOTIATED | PluginBase::INCLUDE_ENTITY, [$this->getOption('rendering_language')]);
2681 * Returns whether the base table is of a translatable entity type.
2684 * TRUE if the base table is of a translatable entity type, FALSE otherwise.
2686 protected function isBaseTableTranslatable() {
2687 if ($entity_type = $this->view->getBaseEntityType()) {
2688 return $entity_type->isTranslatable();