3 namespace Drupal\views\Plugin\views\display;
5 use Drupal\Component\Plugin\DependentPluginInterface;
6 use Drupal\Component\Utility\Html;
7 use Drupal\Component\Utility\SafeMarkup;
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;
969 public function getFieldLabels($groupable_only = FALSE) {
971 foreach ($this->getHandlers('relationship') as $relationship => $handler) {
972 $relationships[$relationship] = $handler->adminLabel();
975 foreach ($this->getHandlers('field') as $id => $handler) {
976 if ($groupable_only && !$handler->useStringGroupBy()) {
977 // Continue to next handler if it's not groupable.
980 if ($label = $handler->label()) {
981 $options[$id] = $label;
984 $options[$id] = $handler->adminLabel();
986 if (!empty($handler->options['relationship']) && !empty($relationships[$handler->options['relationship']])) {
987 $options[$id] = '(' . $relationships[$handler->options['relationship']] . ') ' . $options[$id];
996 public function setOption($option, $value) {
997 if ($this->isDefaulted($option)) {
998 return $this->default_display->setOption($option, $value);
1001 // Set this in two places: On the handler where we'll notice it
1002 // but also on the display object so it gets saved. This should
1003 // only be a temporary fix.
1004 $this->display['display_options'][$option] = $value;
1005 return $this->options[$option] = $value;
1011 public function overrideOption($option, $value) {
1012 $this->setOverride($option, FALSE);
1013 $this->setOption($option, $value);
1019 public function optionLink($text, $section, $class = '', $title = '') {
1021 $text = $this->t('Broken field');
1024 if (!empty($class)) {
1025 $text = SafeMarkup::format('<span>@text</span>', ['@text' => $text]);
1028 if (empty($title)) {
1032 return \Drupal::l($text, new Url('views_ui.form_display', [
1034 'view' => $this->view->storage->id(),
1035 'display_id' => $this->display['id'],
1039 'class' => ['views-ajax-link', $class],
1041 'id' => Html::getUniqueId('views-' . $this->display['id'] . '-' . $section)
1049 public function getArgumentsTokens() {
1051 if (!empty($this->view->build_info['substitutions'])) {
1052 $tokens = $this->view->build_info['substitutions'];
1061 public function optionsSummary(&$categories, &$options) {
1064 'title' => $this->t('Title'),
1065 'column' => 'first',
1068 'title' => $this->t('Format'),
1069 'column' => 'first',
1072 'title' => $this->t('Filters'),
1073 'column' => 'first',
1076 'title' => $this->t('Fields'),
1077 'column' => 'first',
1080 'title' => $this->t('Pager'),
1081 'column' => 'second',
1084 'title' => $this->t('Language'),
1085 'column' => 'second',
1088 'title' => $this->t('Exposed form'),
1089 'column' => 'third',
1096 'column' => 'second',
1102 'title' => $this->t('Other'),
1103 'column' => 'third',
1110 if ($this->display['id'] != 'default') {
1111 $options['display_id'] = [
1112 'category' => 'other',
1113 'title' => $this->t('Machine Name'),
1114 'value' => !empty($this->display['new_id']) ? $this->display['new_id'] : $this->display['id'],
1115 'desc' => $this->t('Change the machine name of this display.'),
1119 $display_comment = views_ui_truncate($this->getOption('display_comment'), 80);
1120 $options['display_comment'] = [
1121 'category' => 'other',
1122 'title' => $this->t('Administrative comment'),
1123 'value' => !empty($display_comment) ? $display_comment : $this->t('None'),
1124 'desc' => $this->t('Comment or document this display.'),
1127 $title = strip_tags($this->getOption('title'));
1129 $title = $this->t('None');
1132 $options['title'] = [
1133 'category' => 'title',
1134 'title' => $this->t('Title'),
1135 'value' => views_ui_truncate($title, 32),
1136 'desc' => $this->t('Change the title that this display will use.'),
1139 $style_plugin_instance = $this->getPlugin('style');
1140 $style_summary = empty($style_plugin_instance->definition['title']) ? $this->t('Missing style plugin') : $style_plugin_instance->summaryTitle();
1141 $style_title = empty($style_plugin_instance->definition['title']) ? $this->t('Missing style plugin') : $style_plugin_instance->pluginTitle();
1143 $options['style'] = [
1144 'category' => 'format',
1145 'title' => $this->t('Format'),
1146 'value' => $style_title,
1147 'setting' => $style_summary,
1148 'desc' => $this->t('Change the way content is formatted.'),
1151 // This adds a 'Settings' link to the style_options setting if the style has
1153 if ($style_plugin_instance->usesOptions()) {
1154 $options['style']['links']['style_options'] = $this->t('Change settings for this format');
1157 if ($style_plugin_instance->usesRowPlugin()) {
1158 $row_plugin_instance = $this->getPlugin('row');
1159 $row_summary = empty($row_plugin_instance->definition['title']) ? $this->t('Missing row plugin') : $row_plugin_instance->summaryTitle();
1160 $row_title = empty($row_plugin_instance->definition['title']) ? $this->t('Missing row plugin') : $row_plugin_instance->pluginTitle();
1163 'category' => 'format',
1164 'title' => $this->t('Show'),
1165 'value' => $row_title,
1166 'setting' => $row_summary,
1167 'desc' => $this->t('Change the way each row in the view is styled.'),
1169 // This adds a 'Settings' link to the row_options setting if the row style
1171 if ($row_plugin_instance->usesOptions()) {
1172 $options['row']['links']['row_options'] = $this->t('Change settings for this style');
1175 if ($this->usesAJAX()) {
1176 $options['use_ajax'] = [
1177 'category' => 'other',
1178 'title' => $this->t('Use AJAX'),
1179 'value' => $this->getOption('use_ajax') ? $this->t('Yes') : $this->t('No'),
1180 'desc' => $this->t('Change whether or not this display will use AJAX.'),
1183 if ($this->usesAttachments()) {
1184 $options['hide_attachment_summary'] = [
1185 'category' => 'other',
1186 'title' => $this->t('Hide attachments in summary'),
1187 'value' => $this->getOption('hide_attachment_summary') ? $this->t('Yes') : $this->t('No'),
1188 'desc' => $this->t('Change whether or not to display attachments when displaying a contextual filter summary.'),
1191 if (!isset($this->definition['contextual links locations']) || !empty($this->definition['contextual links locations'])) {
1192 $options['show_admin_links'] = [
1193 'category' => 'other',
1194 'title' => $this->t('Contextual links'),
1195 'value' => $this->getOption('show_admin_links') ? $this->t('Shown') : $this->t('Hidden'),
1196 'desc' => $this->t('Change whether or not to display contextual links for this view.'),
1200 $pager_plugin = $this->getPlugin('pager');
1201 if (!$pager_plugin) {
1202 // Default to the no pager plugin.
1203 $pager_plugin = Views::pluginManager('pager')->createInstance('none');
1206 $pager_str = $pager_plugin->summaryTitle();
1208 $options['pager'] = [
1209 'category' => 'pager',
1210 'title' => $this->t('Use pager'),
1211 'value' => $pager_plugin->pluginTitle(),
1212 'setting' => $pager_str,
1213 'desc' => $this->t("Change this display's pager setting."),
1216 // If pagers aren't allowed, change the text of the item.
1217 if (!$this->usesPager()) {
1218 $options['pager']['title'] = $this->t('Items to display');
1221 if ($pager_plugin->usesOptions()) {
1222 $options['pager']['links']['pager_options'] = $this->t('Change settings for this pager type.');
1225 if ($this->usesMore()) {
1226 $options['use_more'] = [
1227 'category' => 'pager',
1228 'title' => $this->t('More link'),
1229 'value' => $this->getOption('use_more') ? $this->t('Yes') : $this->t('No'),
1230 'desc' => $this->t('Specify whether this display will provide a "more" link.'),
1234 $this->view->initQuery();
1235 if ($this->view->query->getAggregationInfo()) {
1236 $options['group_by'] = [
1237 'category' => 'other',
1238 'title' => $this->t('Use aggregation'),
1239 'value' => $this->getOption('group_by') ? $this->t('Yes') : $this->t('No'),
1240 'desc' => $this->t('Allow grouping and aggregation (calculation) of fields.'),
1244 $options['query'] = [
1245 'category' => 'other',
1246 'title' => $this->t('Query settings'),
1247 'value' => $this->t('Settings'),
1248 'desc' => $this->t('Allow to set some advanced settings for the query plugin'),
1251 if (\Drupal::languageManager()->isMultilingual() && $this->isBaseTableTranslatable()) {
1252 $rendering_language_options = $this->buildRenderingLanguageOptions();
1253 $options['rendering_language'] = [
1254 'category' => 'language',
1255 'title' => $this->t('Rendering Language'),
1256 'value' => $rendering_language_options[$this->getOption('rendering_language')],
1257 'desc' => $this->t('All content that supports translations will be displayed in the selected language.'),
1261 $access_plugin = $this->getPlugin('access');
1262 if (!$access_plugin) {
1263 // Default to the no access control plugin.
1264 $access_plugin = Views::pluginManager('access')->createInstance('none');
1267 $access_str = $access_plugin->summaryTitle();
1269 $options['access'] = [
1270 'category' => 'access',
1271 'title' => $this->t('Access'),
1272 'value' => $access_plugin->pluginTitle(),
1273 'setting' => $access_str,
1274 'desc' => $this->t('Specify access control type for this display.'),
1277 if ($access_plugin->usesOptions()) {
1278 $options['access']['links']['access_options'] = $this->t('Change settings for this access type.');
1281 $cache_plugin = $this->getPlugin('cache');
1282 if (!$cache_plugin) {
1283 // Default to the no cache control plugin.
1284 $cache_plugin = Views::pluginManager('cache')->createInstance('none');
1287 $cache_str = $cache_plugin->summaryTitle();
1289 $options['cache'] = [
1290 'category' => 'other',
1291 'title' => $this->t('Caching'),
1292 'value' => $cache_plugin->pluginTitle(),
1293 'setting' => $cache_str,
1294 'desc' => $this->t('Specify caching type for this display.'),
1297 if ($cache_plugin->usesOptions()) {
1298 $options['cache']['links']['cache_options'] = $this->t('Change settings for this caching type.');
1301 if ($access_plugin->usesOptions()) {
1302 $options['access']['links']['access_options'] = $this->t('Change settings for this access type.');
1305 if ($this->usesLinkDisplay()) {
1306 $link_display_option = $this->getOption('link_display');
1307 $link_display = $this->t('None');
1309 if ($link_display_option == 'custom_url') {
1310 $link_display = $this->t('Custom URL');
1312 elseif (!empty($link_display_option)) {
1313 $display_id = $this->getLinkDisplay();
1314 $displays = $this->view->storage->get('display');
1315 if (!empty($displays[$display_id])) {
1316 $link_display = $displays[$display_id]['display_title'];
1320 $options['link_display'] = [
1321 'category' => 'pager',
1322 'title' => $this->t('Link display'),
1323 'value' => $link_display,
1324 'desc' => $this->t('Specify which display or custom URL this display will link to.'),
1328 if ($this->usesExposedFormInBlock()) {
1329 $options['exposed_block'] = [
1330 'category' => 'exposed',
1331 'title' => $this->t('Exposed form in block'),
1332 'value' => $this->getOption('exposed_block') ? $this->t('Yes') : $this->t('No'),
1333 'desc' => $this->t('Allow the exposed form to appear in a block instead of the view.'),
1337 /** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginInterface $exposed_form_plugin */
1338 $exposed_form_plugin = $this->getPlugin('exposed_form');
1339 if (!$exposed_form_plugin) {
1340 // Default to the no cache control plugin.
1341 $exposed_form_plugin = Views::pluginManager('exposed_form')->createInstance('basic');
1344 $exposed_form_str = $exposed_form_plugin->summaryTitle();
1346 $options['exposed_form'] = [
1347 'category' => 'exposed',
1348 'title' => $this->t('Exposed form style'),
1349 'value' => $exposed_form_plugin->pluginTitle(),
1350 'setting' => $exposed_form_str,
1351 'desc' => $this->t('Select the kind of exposed filter to use.'),
1354 if ($exposed_form_plugin->usesOptions()) {
1355 $options['exposed_form']['links']['exposed_form_options'] = $this->t('Exposed form settings for this exposed form style.');
1358 $css_class = trim($this->getOption('css_class'));
1360 $css_class = $this->t('None');
1363 $options['css_class'] = [
1364 'category' => 'other',
1365 'title' => $this->t('CSS class'),
1366 'value' => $css_class,
1367 'desc' => $this->t('Change the CSS class name(s) that will be added to this display.'),
1370 foreach ($this->extenders as $extender) {
1371 $extender->optionsSummary($categories, $options);
1378 public function buildOptionsForm(&$form, FormStateInterface $form_state) {
1379 parent::buildOptionsForm($form, $form_state);
1380 $section = $form_state->get('section');
1381 if ($this->defaultableSections($section)) {
1382 views_ui_standard_display_dropdown($form, $form_state, $section);
1384 $form['#title'] = $this->display['display_title'] . ': ';
1386 // Set the 'section' to highlight on the form.
1387 // If it's the item we're looking at is pulling from the default display,
1388 // reflect that. Don't use is_defaulted since we want it to show up even
1389 // on the default display.
1390 if (!empty($this->options['defaults'][$section])) {
1391 $form['#section'] = 'default-' . $section;
1394 $form['#section'] = $this->display['id'] . '-' . $section;
1399 $form['#title'] .= $this->t('The machine name of this display');
1400 $form['display_id'] = [
1401 '#type' => 'textfield',
1402 '#title' => $this->t('Machine name of the display'),
1403 '#default_value' => !empty($this->display['new_id']) ? $this->display['new_id'] : $this->display['id'],
1404 '#required' => TRUE,
1408 case 'display_title':
1409 $form['#title'] .= $this->t('The name and the description of this display');
1410 $form['display_title'] = [
1411 '#title' => $this->t('Administrative name'),
1412 '#type' => 'textfield',
1413 '#default_value' => $this->display['display_title'],
1415 $form['display_description'] = [
1416 '#title' => $this->t('Administrative description'),
1417 '#type' => 'textfield',
1418 '#default_value' => $this->getOption('display_description'),
1421 case 'display_comment':
1422 $form['#title'] .= $this->t('Administrative comment');
1423 $form['display_comment'] = [
1424 '#type' => 'textarea',
1425 '#title' => $this->t('Administrative comment'),
1426 '#description' => $this->t('This description will only be seen within the administrative interface and can be used to document this display.'),
1427 '#default_value' => $this->getOption('display_comment'),
1431 $form['#title'] .= $this->t('The title of this view');
1433 '#title' => $this->t('Title'),
1434 '#type' => 'textfield',
1435 '#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.'),
1436 '#default_value' => $this->getOption('title'),
1437 '#maxlength' => 255,
1441 $form['#title'] .= $this->t('CSS class');
1442 $form['css_class'] = [
1443 '#type' => 'textfield',
1444 '#title' => $this->t('CSS class name(s)'),
1445 '#description' => $this->t('Separate multiple classes by spaces.'),
1446 '#default_value' => $this->getOption('css_class'),
1450 $form['#title'] .= $this->t('AJAX');
1451 $form['use_ajax'] = [
1452 '#description' => $this->t('Options such as paging, table sorting, and exposed filters will not initiate a page refresh.'),
1453 '#type' => 'checkbox',
1454 '#title' => $this->t('Use AJAX'),
1455 '#default_value' => $this->getOption('use_ajax') ? 1 : 0,
1458 case 'hide_attachment_summary':
1459 $form['#title'] .= $this->t('Hide attachments when displaying a contextual filter summary');
1460 $form['hide_attachment_summary'] = [
1461 '#type' => 'checkbox',
1462 '#title' => $this->t('Hide attachments in summary'),
1463 '#default_value' => $this->getOption('hide_attachment_summary') ? 1 : 0,
1466 case 'show_admin_links':
1467 $form['#title'] .= $this->t('Show contextual links on this view.');
1468 $form['show_admin_links'] = [
1469 '#type' => 'checkbox',
1470 '#title' => $this->t('Show contextual links'),
1471 '#default_value' => $this->getOption('show_admin_links'),
1475 $form['#title'] .= $this->t('Add a more link to the bottom of the display.');
1476 $form['use_more'] = [
1477 '#type' => 'checkbox',
1478 '#title' => $this->t('Create more link'),
1479 '#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."),
1480 '#default_value' => $this->getOption('use_more'),
1482 $form['use_more_always'] = [
1483 '#type' => 'checkbox',
1484 '#title' => $this->t('Always display the more link'),
1485 '#description' => $this->t('Check this to display the more link even if there are no more items to display.'),
1486 '#default_value' => $this->getOption('use_more_always'),
1489 ':input[name="use_more"]' => ['checked' => TRUE],
1493 $form['use_more_text'] = [
1494 '#type' => 'textfield',
1495 '#title' => $this->t('More link text'),
1496 '#description' => $this->t('The text to display for the more link.'),
1497 '#default_value' => $this->getOption('use_more_text'),
1500 ':input[name="use_more"]' => ['checked' => TRUE],
1506 $form['#title'] .= $this->t('Allow grouping and aggregation (calculation) of fields.');
1507 $form['group_by'] = [
1508 '#type' => 'checkbox',
1509 '#title' => $this->t('Aggregate'),
1510 '#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.'),
1511 '#default_value' => $this->getOption('group_by'),
1515 $form['#title'] .= $this->t('Access restrictions');
1517 '#prefix' => '<div class="clearfix">',
1518 '#suffix' => '</div>',
1522 $access = $this->getOption('access');
1523 $form['access']['type'] = [
1524 '#title' => $this->t('Access'),
1525 '#title_display' => 'invisible',
1526 '#type' => 'radios',
1527 '#options' => Views::fetchPluginNames('access', $this->getType(), [$this->view->storage->get('base_table')]),
1528 '#default_value' => $access['type'],
1531 $access_plugin = $this->getPlugin('access');
1532 if ($access_plugin->usesOptions()) {
1534 '#prefix' => '<div class="js-form-item form-item description">',
1535 '#markup' => $this->t('You may also adjust the @settings for the currently selected access restriction.', ['@settings' => $this->optionLink($this->t('settings'), 'access_options')]),
1536 '#suffix' => '</div>',
1541 case 'access_options':
1542 $plugin = $this->getPlugin('access');
1543 $form['#title'] .= $this->t('Access options');
1545 $form['access_options'] = [
1548 $plugin->buildOptionsForm($form['access_options'], $form_state);
1552 $form['#title'] .= $this->t('Caching');
1554 '#prefix' => '<div class="clearfix">',
1555 '#suffix' => '</div>',
1559 $cache = $this->getOption('cache');
1560 $form['cache']['type'] = [
1561 '#title' => $this->t('Caching'),
1562 '#title_display' => 'invisible',
1563 '#type' => 'radios',
1564 '#options' => Views::fetchPluginNames('cache', $this->getType(), [$this->view->storage->get('base_table')]),
1565 '#default_value' => $cache['type'],
1568 $cache_plugin = $this->getPlugin('cache');
1569 if ($cache_plugin->usesOptions()) {
1571 '#prefix' => '<div class="js-form-item form-item description">',
1572 '#suffix' => '</div>',
1573 '#markup' => $this->t('You may also adjust the @settings for the currently selected cache mechanism.', ['@settings' => $this->optionLink($this->t('settings'), 'cache_options')]),
1577 case 'cache_options':
1578 $plugin = $this->getPlugin('cache');
1579 $form['#title'] .= $this->t('Caching options');
1581 $form['cache_options'] = [
1584 $plugin->buildOptionsForm($form['cache_options'], $form_state);
1588 $query_options = $this->getOption('query');
1589 $plugin_name = $query_options['type'];
1591 $form['#title'] .= $this->t('Query options');
1592 $this->view->initQuery();
1593 if ($this->view->query) {
1598 '#value' => $plugin_name,
1605 $this->view->query->buildOptionsForm($form['query']['options'], $form_state);
1608 case 'rendering_language':
1609 $form['#title'] .= $this->t('Rendering language');
1610 if (\Drupal::languageManager()->isMultilingual() && $this->isBaseTableTranslatable()) {
1611 $options = $this->buildRenderingLanguageOptions();
1612 $form['rendering_language'] = [
1613 '#type' => 'select',
1614 '#options' => $options,
1615 '#title' => $this->t('Rendering language'),
1616 '#description' => $this->t('All content that supports translations will be displayed in the selected language.'),
1617 '#default_value' => $this->getOption('rendering_language'),
1621 $form['rendering_language']['#markup'] = $this->t('The view is not based on a translatable entity type or the site is not multilingual.');
1625 $form['#title'] .= $this->t('How should this view be styled');
1626 $style_plugin = $this->getPlugin('style');
1628 '#prefix' => '<div class="clearfix">',
1629 '#suffix' => '</div>',
1632 $form['style']['type'] = [
1633 '#title' => $this->t('Style'),
1634 '#title_display' => 'invisible',
1635 '#type' => 'radios',
1636 '#options' => Views::fetchPluginNames('style', $this->getType(), [$this->view->storage->get('base_table')]),
1637 '#default_value' => $style_plugin->definition['id'],
1638 '#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.'),
1641 if ($style_plugin->usesOptions()) {
1643 '#prefix' => '<div class="js-form-item form-item description">',
1644 '#suffix' => '</div>',
1645 '#markup' => $this->t('You may also adjust the @settings for the currently selected style.', ['@settings' => $this->optionLink($this->t('settings'), 'style_options')]),
1650 case 'style_options':
1651 $form['#title'] .= $this->t('Style options');
1653 $style_plugin = $this->getOption('style');
1654 $name = $style_plugin['type'];
1657 if (!isset($name)) {
1658 $row_plugin = $this->getOption('row');
1659 $name = $row_plugin['type'];
1661 // If row, $style will be empty.
1662 if (empty($style)) {
1663 $form['#title'] .= $this->t('Row style options');
1665 $plugin = $this->getPlugin(empty($style) ? 'row' : 'style', $name);
1670 $plugin->buildOptionsForm($form[$section], $form_state);
1674 $form['#title'] .= $this->t('How should each row in this view be styled');
1675 $row_plugin_instance = $this->getPlugin('row');
1677 '#prefix' => '<div class="clearfix">',
1678 '#suffix' => '</div>',
1681 $form['row']['type'] = [
1682 '#title' => $this->t('Row'),
1683 '#title_display' => 'invisible',
1684 '#type' => 'radios',
1685 '#options' => Views::fetchPluginNames('row', $this->getType(), [$this->view->storage->get('base_table')]),
1686 '#default_value' => $row_plugin_instance->definition['id'],
1689 if ($row_plugin_instance->usesOptions()) {
1691 '#prefix' => '<div class="js-form-item form-item description">',
1692 '#suffix' => '</div>',
1693 '#markup' => $this->t('You may also adjust the @settings for the currently selected row style.', ['@settings' => $this->optionLink($this->t('settings'), 'row_options')]),
1698 case 'link_display':
1699 $form['#title'] .= $this->t('Which display to use for path');
1700 $options = [FALSE => $this->t('None'), 'custom_url' => $this->t('Custom URL')];
1702 foreach ($this->view->storage->get('display') as $display_id => $display) {
1703 if ($this->view->displayHandlers->get($display_id)->hasPath()) {
1704 $options[$display_id] = $display['display_title'];
1708 $form['link_display'] = [
1709 '#type' => 'radios',
1710 '#options' => $options,
1711 '#description' => $this->t("Which display to use to get this display's path for things like summary links, rss feed links, more links, etc."),
1712 '#default_value' => $this->getOption('link_display'),
1716 $optgroup_arguments = (string) t('Arguments');
1717 foreach ($this->view->display_handler->getHandlers('argument') as $arg => $handler) {
1718 $options[$optgroup_arguments]["{{ arguments.$arg }}"] = $this->t('@argument title', ['@argument' => $handler->adminLabel()]);
1719 $options[$optgroup_arguments]["{{ raw_arguments.$arg }}"] = $this->t('@argument input', ['@argument' => $handler->adminLabel()]);
1723 // We have some options, so make a list.
1726 '#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.'),
1728 if (!empty($options)) {
1731 '#markup' => $this->t('The following tokens are available for this link. You may use Twig syntax in this field.'),
1732 '#suffix' => '</p>',
1734 foreach (array_keys($options) as $type) {
1735 if (!empty($options[$type])) {
1737 foreach ($options[$type] as $key => $value) {
1738 $items[] = $key . ' == ' . $value;
1741 '#theme' => 'item_list',
1744 $description[] = $item_list;
1749 $form['link_url'] = [
1750 '#type' => 'textfield',
1751 '#title' => $this->t('Custom URL'),
1752 '#default_value' => $this->getOption('link_url'),
1753 '#description' => $description,
1756 ':input[name="link_display"]' => ['value' => 'custom_url'],
1761 case 'exposed_block':
1762 $form['#title'] .= $this->t('Put the exposed form in a block');
1763 $form['description'] = [
1764 '#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>',
1766 $form['exposed_block'] = [
1767 '#type' => 'radios',
1768 '#options' => [1 => $this->t('Yes'), 0 => $this->t('No')],
1769 '#default_value' => $this->getOption('exposed_block') ? 1 : 0,
1772 case 'exposed_form':
1773 $form['#title'] .= $this->t('Exposed Form');
1774 $form['exposed_form'] = [
1775 '#prefix' => '<div class="clearfix">',
1776 '#suffix' => '</div>',
1780 $exposed_form = $this->getOption('exposed_form');
1781 $form['exposed_form']['type'] = [
1782 '#title' => $this->t('Exposed form'),
1783 '#title_display' => 'invisible',
1784 '#type' => 'radios',
1785 '#options' => Views::fetchPluginNames('exposed_form', $this->getType(), [$this->view->storage->get('base_table')]),
1786 '#default_value' => $exposed_form['type'],
1789 $exposed_form_plugin = $this->getPlugin('exposed_form');
1790 if ($exposed_form_plugin->usesOptions()) {
1792 '#prefix' => '<div class="js-form-item form-item description">',
1793 '#suffix' => '</div>',
1794 '#markup' => $this->t('You may also adjust the @settings for the currently selected style.', ['@settings' => $this->optionLink($this->t('settings'), 'exposed_form_options')]),
1798 case 'exposed_form_options':
1799 $plugin = $this->getPlugin('exposed_form');
1800 $form['#title'] .= $this->t('Exposed form options');
1802 $form['exposed_form_options'] = [
1805 $plugin->buildOptionsForm($form['exposed_form_options'], $form_state);
1809 $form['#title'] .= $this->t('Select pager');
1811 '#prefix' => '<div class="clearfix">',
1812 '#suffix' => '</div>',
1816 $pager = $this->getOption('pager');
1817 $form['pager']['type'] = [
1818 '#title' => $this->t('Pager'),
1819 '#title_display' => 'invisible',
1820 '#type' => 'radios',
1821 '#options' => Views::fetchPluginNames('pager', !$this->usesPager() ? 'basic' : NULL, [$this->view->storage->get('base_table')]),
1822 '#default_value' => $pager['type'],
1825 $pager_plugin = $this->getPlugin('pager');
1826 if ($pager_plugin->usesOptions()) {
1828 '#prefix' => '<div class="js-form-item form-item description">',
1829 '#suffix' => '</div>',
1830 '#markup' => $this->t('You may also adjust the @settings for the currently selected pager.', ['@settings' => $this->optionLink($this->t('settings'), 'pager_options')]),
1835 case 'pager_options':
1836 $plugin = $this->getPlugin('pager');
1837 $form['#title'] .= $this->t('Pager options');
1839 $form['pager_options'] = [
1842 $plugin->buildOptionsForm($form['pager_options'], $form_state);
1847 foreach ($this->extenders as $extender) {
1848 $extender->buildOptionsForm($form, $form_state);
1855 public function validateOptionsForm(&$form, FormStateInterface $form_state) {
1856 $section = $form_state->get('section');
1858 case 'display_title':
1859 if ($form_state->isValueEmpty('display_title')) {
1860 $form_state->setError($form['display_title'], $this->t('Display title may not be empty.'));
1864 $css_class = $form_state->getValue('css_class');
1865 if (preg_match('/[^a-zA-Z0-9-_ ]/', $css_class)) {
1866 $form_state->setError($form['css_class'], $this->t('CSS classes must be alphanumeric or dashes only.'));
1870 if ($form_state->getValue('display_id')) {
1871 if (preg_match('/[^a-z0-9_]/', $form_state->getValue('display_id'))) {
1872 $form_state->setError($form['display_id'], $this->t('Display name must be letters, numbers, or underscores only.'));
1875 foreach ($this->view->displayHandlers as $id => $display) {
1876 if ($id != $this->view->current_display && ($form_state->getValue('display_id') == $id || (isset($display->new_id) && $form_state->getValue('display_id') == $display->new_id))) {
1877 $form_state->setError($form['display_id'], $this->t('Display id should be unique.'));
1883 if ($this->view->query) {
1884 $this->view->query->validateOptionsForm($form['query'], $form_state);
1889 // Validate plugin options. Every section with "_options" in it, belongs to
1890 // a plugin type, like "style_options".
1891 if (strpos($section, '_options') !== FALSE) {
1892 $plugin_type = str_replace('_options', '', $section);
1893 // Load the plugin and let it handle the validation.
1894 if ($plugin = $this->getPlugin($plugin_type)) {
1895 $plugin->validateOptionsForm($form[$section], $form_state);
1899 foreach ($this->extenders as $extender) {
1900 $extender->validateOptionsForm($form, $form_state);
1907 public function submitOptionsForm(&$form, FormStateInterface $form_state) {
1908 // Not sure I like this being here, but it seems (?) like a logical place.
1909 $cache_plugin = $this->getPlugin('cache');
1910 if ($cache_plugin) {
1911 $cache_plugin->cacheFlush();
1914 $section = $form_state->get('section');
1917 if ($form_state->hasValue('display_id')) {
1918 $this->display['new_id'] = $form_state->getValue('display_id');
1921 case 'display_title':
1922 $this->display['display_title'] = $form_state->getValue('display_title');
1923 $this->setOption('display_description', $form_state->getValue('display_description'));
1926 $plugin = $this->getPlugin('query');
1928 $plugin->submitOptionsForm($form['query']['options'], $form_state);
1929 $this->setOption('query', $form_state->getValue($section));
1933 case 'link_display':
1934 $this->setOption('link_url', $form_state->getValue('link_url'));
1937 case 'display_comment':
1940 $this->setOption($section, $form_state->getValue($section));
1942 case 'rendering_language':
1943 $this->setOption('rendering_language', $form_state->getValue('rendering_language'));
1946 case 'hide_attachment_summary':
1947 case 'show_admin_links':
1948 case 'exposed_block':
1949 $this->setOption($section, (bool) $form_state->getValue($section));
1952 $this->setOption($section, intval($form_state->getValue($section)));
1953 $this->setOption('use_more_always', intval($form_state->getValue('use_more_always')));
1954 $this->setOption('use_more_text', $form_state->getValue('use_more_text'));
1959 case 'exposed_form':
1963 $plugin_type = $section;
1964 $plugin_options = $this->getOption($plugin_type);
1965 $type = $form_state->getValue([$plugin_type, 'type']);
1966 if ($plugin_options['type'] != $type) {
1967 /** @var \Drupal\views\Plugin\views\ViewsPluginInterface $plugin */
1968 $plugin = Views::pluginManager($plugin_type)->createInstance($type);
1970 $plugin->init($this->view, $this, $plugin_options['options']);
1973 'options' => $plugin->options,
1975 $plugin->filterByDefinedOptions($plugin_options['options']);
1976 $this->setOption($plugin_type, $plugin_options);
1977 if ($plugin->usesOptions()) {
1978 $form_state->get('view')->addFormToStack('display', $this->display['id'], $plugin_type . '_options');
1984 case 'access_options':
1985 case 'cache_options':
1986 case 'exposed_form_options':
1987 case 'pager_options':
1989 case 'style_options':
1990 // Submit plugin options. Every section with "_options" in it, belongs to
1991 // a plugin type, like "style_options".
1992 $plugin_type = str_replace('_options', '', $section);
1993 if ($plugin = $this->getPlugin($plugin_type)) {
1994 $plugin_options = $this->getOption($plugin_type);
1995 $plugin->submitOptionsForm($form[$plugin_type . '_options'], $form_state);
1996 $plugin_options['options'] = $form_state->getValue($section);
1997 $this->setOption($plugin_type, $plugin_options);
2002 $extender_options = $this->getOption('display_extenders');
2003 foreach ($this->extenders as $extender) {
2004 $extender->submitOptionsForm($form, $form_state);
2006 $plugin_id = $extender->getPluginId();
2007 $extender_options[$plugin_id] = $extender->options;
2009 $this->setOption('display_extenders', $extender_options);
2015 public function optionsOverride($form, FormStateInterface $form_state) {
2016 $this->setOverride($form_state->get('section'));
2022 public function setOverride($section, $new_state = NULL) {
2023 $options = $this->defaultableSections($section);
2028 if (!isset($new_state)) {
2029 $new_state = empty($this->options['defaults'][$section]);
2032 // For each option that is part of this group, fix our settings.
2033 foreach ($options as $option) {
2035 // Revert to defaults.
2036 unset($this->options[$option]);
2037 unset($this->display['display_options'][$option]);
2040 // Copy existing values into our display.
2041 $this->options[$option] = $this->getOption($option);
2042 $this->display['display_options'][$option] = $this->options[$option];
2044 $this->options['defaults'][$option] = $new_state;
2045 $this->display['display_options']['defaults'][$option] = $new_state;
2052 public function query() {
2053 foreach ($this->extenders as $extender) {
2061 public function renderFilters() {}
2066 public function renderPager() {
2073 public function renderMoreLink() {
2074 if ($this->isMoreEnabled() && ($this->useMoreAlways() || (!empty($this->view->pager) && $this->view->pager->hasMoreRecords()))) {
2075 // If the user has supplied a custom "More" link path, replace any
2076 // argument tokens and use that for the URL.
2077 if ($this->getOption('link_display') == 'custom_url' && $override_path = $this->getOption('link_url')) {
2078 $tokens = $this->getArgumentsTokens();
2079 $path = $this->viewsTokenReplace($override_path, $tokens);
2080 // @todo Views should expect and store a leading /. See:
2081 // https://www.drupal.org/node/2423913
2082 $url = Url::fromUserInput('/' . $path);
2084 // Otherwise, use the URL for the display.
2086 $url = $this->view->getUrl(NULL, $this->display['id']);
2089 // If a URL is available (either from the display or a custom path),
2090 // render the "More" link.
2093 if (!empty($this->view->exposed_raw_input)) {
2094 $url_options['query'] = $this->view->exposed_raw_input;
2096 $url->setOptions($url_options);
2099 '#type' => 'more_link',
2101 '#title' => $this->useMoreText(),
2102 '#view' => $this->view,
2111 public function render() {
2112 $rows = (!empty($this->view->result) || $this->view->style_plugin->evenEmpty()) ? $this->view->style_plugin->render($this->view->result) : [];
2115 '#theme' => $this->themeFunctions(),
2116 '#view' => $this->view,
2117 '#pre_render' => [[$this, 'elementPreRender']],
2119 // Assigned by reference so anything added in $element['#attached'] will
2120 // be available on the view.
2121 '#attached' => &$this->view->element['#attached'],
2122 '#cache' => &$this->view->element['#cache'],
2125 $this->applyDisplayCacheabilityMetadata($this->view->element);
2131 * Applies the cacheability of the current display to the given render array.
2133 * @param array $element
2134 * The render array with updated cacheability metadata.
2136 protected function applyDisplayCacheabilityMetadata(array &$element) {
2137 /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache */
2138 $cache = $this->getPlugin('cache');
2140 (new CacheableMetadata())
2141 ->setCacheTags(Cache::mergeTags($this->view->getCacheTags(), isset($this->display['cache_metadata']['tags']) ? $this->display['cache_metadata']['tags'] : []))
2142 ->setCacheContexts(isset($this->display['cache_metadata']['contexts']) ? $this->display['cache_metadata']['contexts'] : [])
2143 ->setCacheMaxAge(Cache::mergeMaxAges($cache->getCacheMaxAge(), isset($this->display['cache_metadata']['max-age']) ? $this->display['cache_metadata']['max-age'] : Cache::PERMANENT))
2144 ->merge(CacheableMetadata::createFromRenderArray($element))
2145 ->applyTo($element);
2149 * Applies the cacheability of the current display to the given render array.
2151 * @param array $element
2152 * The render array with updated cacheability metadata.
2154 * @deprecated in Drupal 8.4.0, will be removed before Drupal 9.0. Use
2155 * DisplayPluginBase::applyDisplayCacheabilityMetadata instead.
2157 * @see \Drupal\views\Plugin\views\display\DisplayPluginBase::applyDisplayCacheabilityMetadata()
2159 protected function applyDisplayCachablityMetadata(array &$element) {
2160 @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);
2161 $this->applyDisplayCacheabilityMetadata($element);
2167 public function elementPreRender(array $element) {
2168 $view = $element['#view'];
2169 $empty = empty($view->result);
2171 // Force a render array so CSS/JS can be attached.
2172 if (!is_array($element['#rows'])) {
2173 $element['#rows'] = ['#markup' => $element['#rows']];
2176 $element['#header'] = $view->display_handler->renderArea('header', $empty);
2177 $element['#footer'] = $view->display_handler->renderArea('footer', $empty);
2178 $element['#empty'] = $empty ? $view->display_handler->renderArea('empty', $empty) : [];
2179 $element['#exposed'] = !empty($view->exposed_widgets) ? $view->exposed_widgets : [];
2180 $element['#more'] = $view->display_handler->renderMoreLink();
2181 $element['#feed_icons'] = !empty($view->feedIcons) ? $view->feedIcons : [];
2183 if ($view->display_handler->renderPager()) {
2184 $exposed_input = isset($view->exposed_raw_input) ? $view->exposed_raw_input : NULL;
2185 $element['#pager'] = $view->renderPager($exposed_input);
2188 if (!empty($view->attachment_before)) {
2189 $element['#attachment_before'] = $view->attachment_before;
2191 if (!empty($view->attachment_after)) {
2192 $element['#attachment_after'] = $view->attachment_after;
2195 // If form fields were found in the view, reformat the view output as a form.
2196 if ($view->hasFormElements()) {
2197 // Only render row output if there are rows. Otherwise, render the empty
2199 if (!empty($element['#rows'])) {
2200 $output = $element['#rows'];
2203 $output = $element['#empty'];
2206 $form_object = ViewsForm::create(\Drupal::getContainer(), $view->storage->id(), $view->current_display, $view->args);
2207 $form = \Drupal::formBuilder()->getForm($form_object, $view, $output);
2208 // The form is requesting that all non-essential views elements be hidden,
2209 // usually because the rendered step is not a view result.
2210 if ($form['show_view_elements']['#value'] == FALSE) {
2211 $element['#header'] = [];
2212 $element['#exposed'] = [];
2213 $element['#pager'] = [];
2214 $element['#footer'] = [];
2215 $element['#more'] = [];
2216 $element['#feed_icons'] = [];
2219 $element['#rows'] = $form;
2228 public function renderArea($area, $empty = FALSE) {
2230 foreach ($this->getHandlers($area) as $key => $area_handler) {
2231 if ($area_render = $area_handler->render($empty)) {
2232 if (isset($area_handler->position)) {
2233 // Fix weight of area.
2234 $area_render['#weight'] = $area_handler->position;
2236 $return[$key] = $area_render;
2245 public function access(AccountInterface $account = NULL) {
2246 if (!isset($account)) {
2247 $account = \Drupal::currentUser();
2250 $plugin = $this->getPlugin('access');
2251 /** @var \Drupal\views\Plugin\views\access\AccessPluginBase $plugin */
2253 return $plugin->access($account);
2256 // Fallback to all access if no plugin.
2263 public function preExecute() {
2264 $this->view->setAjaxEnabled($this->ajaxEnabled());
2265 if ($this->isMoreEnabled() && !$this->useMoreAlways()) {
2266 $this->view->get_total_rows = TRUE;
2268 $this->view->initHandlers();
2269 if ($this->usesExposed()) {
2270 /** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginInterface $exposed_form */
2271 $exposed_form = $this->getPlugin('exposed_form');
2272 $exposed_form->preExecute();
2275 foreach ($this->extenders as $extender) {
2276 $extender->preExecute();
2283 public function calculateCacheMetadata() {
2284 $cache_metadata = new CacheableMetadata();
2286 // Iterate over ordinary views plugins.
2287 foreach (Views::getPluginTypes('plugin') as $plugin_type) {
2288 $plugin = $this->getPlugin($plugin_type);
2289 if ($plugin instanceof CacheableDependencyInterface) {
2290 $cache_metadata = $cache_metadata->merge(CacheableMetadata::createFromObject($plugin));
2294 // Iterate over all handlers. Note that at least the argument handler will
2295 // need to ask all its subplugins.
2296 foreach (array_keys(Views::getHandlerTypes()) as $handler_type) {
2297 $handlers = $this->getHandlers($handler_type);
2298 foreach ($handlers as $handler) {
2299 if ($handler instanceof CacheableDependencyInterface) {
2300 $cache_metadata = $cache_metadata->merge(CacheableMetadata::createFromObject($handler));
2305 /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache_plugin */
2306 if ($cache_plugin = $this->getPlugin('cache')) {
2307 $cache_plugin->alterCacheMetadata($cache_metadata);
2310 return $cache_metadata;
2316 public function getCacheMetadata() {
2317 if (!isset($this->display['cache_metadata'])) {
2318 $cache_metadata = $this->calculateCacheMetadata();
2319 $this->display['cache_metadata']['max-age'] = $cache_metadata->getCacheMaxAge();
2320 $this->display['cache_metadata']['contexts'] = $cache_metadata->getCacheContexts();
2321 $this->display['cache_metadata']['tags'] = $cache_metadata->getCacheTags();
2324 $cache_metadata = (new CacheableMetadata())
2325 ->setCacheMaxAge($this->display['cache_metadata']['max-age'])
2326 ->setCacheContexts($this->display['cache_metadata']['contexts'])
2327 ->setCacheTags($this->display['cache_metadata']['tags']);
2329 return $cache_metadata;
2335 public function execute() {}
2340 public function buildRenderable(array $args = [], $cache = TRUE) {
2341 $this->view->element += [
2343 '#name' => $this->view->storage->id(),
2344 '#display_id' => $this->display['id'],
2345 '#arguments' => $args,
2347 '#view' => $this->view,
2348 '#cache_properties' => ['#view_id', '#view_display_show_admin_links', '#view_display_plugin_id'],
2351 // When something passes $cache = FALSE, they're asking us not to create our
2352 // own render cache for it. However, we still need to include certain pieces
2353 // of cacheability metadata (e.g.: cache contexts), so they can bubble up.
2354 // Thus, we add the cacheability metadata first, then modify / remove the
2355 // cache keys depending on the $cache argument.
2356 $this->applyDisplayCacheabilityMetadata($this->view->element);
2358 $this->view->element['#cache'] += ['keys' => []];
2359 // Places like \Drupal\views\ViewExecutable::setCurrentPage() set up an
2360 // additional cache context.
2361 $this->view->element['#cache']['keys'] = array_merge(['views', 'display', $this->view->element['#name'], $this->view->element['#display_id']], $this->view->element['#cache']['keys']);
2364 // Remove the cache keys, to ensure render caching is not triggered. We
2365 // don't unset the other #cache values, to allow cacheability metadata to
2366 // still be bubbled.
2367 unset($this->view->element['#cache']['keys']);
2370 return $this->view->element;
2376 public static function buildBasicRenderable($view_id, $display_id, array $args = []) {
2379 '#name' => $view_id,
2380 '#display_id' => $display_id,
2381 '#arguments' => $args,
2384 'keys' => ['view', $view_id, 'display', $display_id],
2389 $build['#cache']['keys'][] = 'args';
2390 $build['#cache']['keys'][] = implode(',', $args);
2393 $build['#cache_properties'] = ['#view_id', '#view_display_show_admin_links', '#view_display_plugin_id'];
2402 public function preview() {
2403 return $this->view->render();
2409 public function getType() {
2416 public function validate() {
2418 // Make sure displays that use fields HAVE fields.
2419 if ($this->usesFields()) {
2421 foreach ($this->getHandlers('field') as $field) {
2422 if (empty($field->options['exclude'])) {
2428 $errors[] = $this->t('Display "@display" uses fields but there are none defined for it or all are excluded.', ['@display' => $this->display['display_title']]);
2432 // Validate the more link.
2433 if ($this->isMoreEnabled() && $this->getOption('link_display') !== 'custom_url') {
2434 $routed_display = $this->getRoutedDisplay();
2435 if (!$routed_display || !$routed_display->isEnabled()) {
2436 $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']]);
2440 if ($this->hasPath() && !$this->getOption('path')) {
2441 $errors[] = $this->t('Display "@display" uses a path but the path is undefined.', ['@display' => $this->display['display_title']]);
2444 // Validate style plugin.
2445 $style = $this->getPlugin('style');
2446 if (empty($style)) {
2447 $errors[] = $this->t('Display "@display" has an invalid style plugin.', ['@display' => $this->display['display_title']]);
2450 $result = $style->validate();
2451 if (!empty($result) && is_array($result)) {
2452 $errors = array_merge($errors, $result);
2456 // Validate query plugin.
2457 $query = $this->getPlugin('query');
2458 $result = $query->validate();
2459 if (!empty($result) && is_array($result)) {
2460 $errors = array_merge($errors, $result);
2463 // Check for missing relationships.
2464 $relationships = array_keys($this->getHandlers('relationship'));
2465 foreach (ViewExecutable::getHandlerTypes() as $type => $handler_type_info) {
2466 foreach ($this->getHandlers($type) as $handler_id => $handler) {
2467 if (!empty($handler->options['relationship']) && $handler->options['relationship'] != 'none' && !in_array($handler->options['relationship'], $relationships)) {
2468 $errors[] = $this->t('The %handler_type %handler uses a relationship that has been removed.', ['%handler_type' => $handler_type_info['lstitle'], '%handler' => $handler->adminLabel()]);
2473 // Validate handlers.
2474 foreach (ViewExecutable::getHandlerTypes() as $type => $info) {
2475 foreach ($this->getHandlers($type) as $handler) {
2476 $result = $handler->validate();
2477 if (!empty($result) && is_array($result)) {
2478 $errors = array_merge($errors, $result);
2489 public function newDisplay() {
2495 public function isIdentifierUnique($id, $identifier) {
2496 foreach (ViewExecutable::getHandlerTypes() as $type => $info) {
2497 foreach ($this->getHandlers($type) as $key => $handler) {
2498 if ($handler->canExpose() && $handler->isExposed()) {
2499 if ($handler->isAGroup()) {
2500 if ($id != $key && $identifier == $handler->options['group_info']['identifier']) {
2505 if ($id != $key && $identifier == $handler->options['expose']['identifier']) {
2518 public function outputIsEmpty() {
2519 if (!empty($this->view->result)) {
2523 // Check whether all of the area handlers are empty.
2524 foreach (['empty', 'footer', 'header'] as $type) {
2525 $handlers = $this->getHandlers($type);
2526 foreach ($handlers as $handler) {
2527 // If one is not empty, return FALSE now.
2528 if (!$handler->isEmpty()) {
2540 public function getSpecialBlocks() {
2543 if ($this->usesExposedFormInBlock()) {
2544 $delta = '-exp-' . $this->view->storage->id() . '-' . $this->display['id'];
2545 $desc = $this->t('Exposed form: @view-@display_id', ['@view' => $this->view->storage->id(), '@display_id' => $this->display['id']]);
2558 public function viewExposedFormBlocks() {
2559 // Avoid interfering with the admin forms.
2560 $route_name = \Drupal::routeMatch()->getRouteName();
2561 if (strpos($route_name, 'views_ui.') === 0) {
2564 $this->view->initHandlers();
2566 if ($this->usesExposed() && $this->getOption('exposed_block')) {
2567 /** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginInterface $exposed_form */
2568 $exposed_form = $this->getPlugin('exposed_form');
2569 return $exposed_form->renderExposedForm(TRUE);
2576 public function getArgumentText() {
2578 'filter value not present' => $this->t('When the filter value is <em>NOT</em> available'),
2579 'filter value present' => $this->t('When the filter value <em>IS</em> available or a default is provided'),
2580 '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'."),
2587 public function getPagerText() {
2589 'items per page title' => $this->t('Items to display'),
2590 'items per page description' => $this->t('Enter 0 for no limit.')
2597 public function mergeDefaults() {
2598 $defined_options = $this->defineOptions();
2600 // Build a map of plural => singular for handler types.
2602 foreach (ViewExecutable::getHandlerTypes() as $type => $info) {
2603 $type_map[$info['plural']] = $type;
2606 // Find all defined options, that have specified a merge_defaults callback.
2607 foreach ($defined_options as $type => $definition) {
2608 if (!isset($definition['merge_defaults']) || !is_callable($definition['merge_defaults'])) {
2611 // Switch the type to singular, if it's a plural handler.
2612 if (isset($type_map[$type])) {
2613 $type = $type_map[$type];
2616 call_user_func($definition['merge_defaults'], $type);
2623 public function remove() {
2628 * Merges plugins default values.
2630 * @param string $type
2631 * The name of the plugin type option.
2633 protected function mergePlugin($type) {
2634 if (($options = $this->getOption($type)) && isset($options['options'])) {
2635 $plugin = $this->getPlugin($type);
2636 $options['options'] = $options['options'] + $plugin->options;
2637 $this->setOption($type, $options);
2642 * Merges handlers default values.
2644 * @param string $type
2645 * The name of the handler type option.
2647 protected function mergeHandler($type) {
2648 $types = ViewExecutable::getHandlerTypes();
2650 $options = $this->getOption($types[$type]['plural']);
2651 foreach ($this->getHandlers($type) as $id => $handler) {
2652 if (isset($options[$id])) {
2653 $options[$id] = $options[$id] + $handler->options;
2657 $this->setOption($types[$type]['plural'], $options);
2663 public function getExtenders() {
2664 return $this->extenders;
2668 * Returns the available rendering strategies for language-aware entities.
2671 * An array of available entity row renderers keyed by renderer identifiers.
2673 protected function buildRenderingLanguageOptions() {
2674 // @todo Consider making these plugins. See
2675 // https://www.drupal.org/node/2173811.
2676 // Pass the current rendering language (in this case a one element array) so
2677 // is not lost when there are language configuration changes.
2678 return $this->listLanguages(LanguageInterface::STATE_CONFIGURABLE | LanguageInterface::STATE_SITE_DEFAULT | PluginBase::INCLUDE_NEGOTIATED | PluginBase::INCLUDE_ENTITY, [$this->getOption('rendering_language')]);
2682 * Returns whether the base table is of a translatable entity type.
2685 * TRUE if the base table is of a translatable entity type, FALSE otherwise.
2687 protected function isBaseTableTranslatable() {
2688 if ($entity_type = $this->view->getBaseEntityType()) {
2689 return $entity_type->isTranslatable();