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;
168 $this->setOptionDefaults($this->options, $this->defineOptions());
169 $this->display = &$display;
171 // Track changes that the user should know about.
174 if (!isset($options) && isset($display['display_options'])) {
175 $options = $display['display_options'];
178 if ($this->isDefaultDisplay() && isset($options['defaults'])) {
179 unset($options['defaults']);
182 $skip_cache = \Drupal::config('views.settings')->get('skip_cache');
184 if (empty($view->editing) || !$skip_cache) {
185 $cid = 'views:unpack_options:' . hash('sha256', serialize([$this->options, $options])) . ':' . \Drupal::languageManager()->getCurrentLanguage()->getId();
186 if (empty(static::$unpackOptions[$cid])) {
187 $cache = \Drupal::cache('data')->get($cid);
188 if (!empty($cache->data)) {
189 $this->options = $cache->data;
192 $this->unpackOptions($this->options, $options);
193 \Drupal::cache('data')->set($cid, $this->options, Cache::PERMANENT, $this->view->storage->getCacheTags());
195 static::$unpackOptions[$cid] = $this->options;
198 $this->options = static::$unpackOptions[$cid];
202 $this->unpackOptions($this->options, $options);
205 // Mark the view as changed so the user has a chance to save it.
207 $this->view->changed = TRUE;
214 public function destroy() {
217 foreach ($this->handlers as $type => $handlers) {
218 foreach ($handlers as $id => $handler) {
219 if (is_object($handler)) {
220 $this->handlers[$type][$id]->destroy();
225 if (isset($this->default_display)) {
226 unset($this->default_display);
229 foreach ($this->extenders as $extender) {
230 $extender->destroy();
237 public function isDefaultDisplay() { return FALSE; }
242 public function usesExposed() {
243 if (!isset($this->has_exposed)) {
244 foreach ($this->handlers as $type => $value) {
245 foreach ($this->view->$type as $handler) {
246 if ($handler->canExpose() && $handler->isExposed()) {
247 // One is all we need; if we find it, return TRUE.
248 $this->has_exposed = TRUE;
253 $pager = $this->getPlugin('pager');
254 if (isset($pager) && $pager->usesExposed()) {
255 $this->has_exposed = TRUE;
258 $this->has_exposed = FALSE;
261 return $this->has_exposed;
267 public function displaysExposed() {
274 public function usesAJAX() {
275 return $this->usesAJAX;
281 public function ajaxEnabled() {
282 if ($this->usesAJAX()) {
283 return $this->getOption('use_ajax');
291 public function isEnabled() {
292 return (bool) $this->getOption('enabled');
298 public function usesPager() {
299 return $this->usesPager;
305 public function isPagerEnabled() {
306 if ($this->usesPager()) {
307 $pager = $this->getPlugin('pager');
309 return $pager->usePager();
318 public function usesMore() {
319 return $this->usesMore;
325 public function isMoreEnabled() {
326 if ($this->usesMore()) {
327 return $this->getOption('use_more');
335 public function useGroupBy() {
336 return $this->getOption('group_by');
342 public function useMoreAlways() {
343 if ($this->usesMore()) {
344 return $this->getOption('use_more_always');
352 public function useMoreText() {
353 if ($this->usesMore()) {
354 return $this->getOption('use_more_text');
362 public function acceptAttachments() {
363 // To be able to accept attachments this display have to be able to use
364 // attachments but at the same time, you cannot attach a display to itself.
365 if (!$this->usesAttachments() || ($this->definition['id'] == $this->view->current_display)) {
369 if (!empty($this->view->argument) && $this->getOption('hide_attachment_summary')) {
370 foreach ($this->view->argument as $argument) {
371 if ($argument->needsStylePlugin() && empty($argument->argument_validated)) {
383 public function usesAttachments() {
384 return $this->usesAttachments;
390 public function usesAreas() {
391 return $this->usesAreas;
397 public function attachTo(ViewExecutable $view, $display_id, array &$build) { }
402 public function defaultableSections($section = NULL) {
404 'access' => ['access'],
405 'cache' => ['cache'],
406 'title' => ['title'],
407 'css_class' => ['css_class'],
408 'use_ajax' => ['use_ajax'],
409 'hide_attachment_summary' => ['hide_attachment_summary'],
410 'show_admin_links' => ['show_admin_links'],
411 'group_by' => ['group_by'],
412 'query' => ['query'],
413 'use_more' => ['use_more', 'use_more_always', 'use_more_text'],
414 'use_more_always' => ['use_more', 'use_more_always', 'use_more_text'],
415 'use_more_text' => ['use_more', 'use_more_always', 'use_more_text'],
416 'link_display' => ['link_display', 'link_url'],
418 // Force these to cascade properly.
419 'style' => ['style', 'row'],
420 'row' => ['style', 'row'],
422 'pager' => ['pager'],
424 'exposed_form' => ['exposed_form'],
426 // These sections are special.
427 'header' => ['header'],
428 'footer' => ['footer'],
429 'empty' => ['empty'],
430 'relationships' => ['relationships'],
431 'fields' => ['fields'],
432 'sorts' => ['sorts'],
433 'arguments' => ['arguments'],
434 'filters' => ['filters', 'filter_groups'],
435 'filter_groups' => ['filters', 'filter_groups'],
438 // If the display cannot use a pager, then we cannot default it.
439 if (!$this->usesPager()) {
440 unset($sections['pager']);
441 unset($sections['items_per_page']);
444 foreach ($this->extenders as $extender) {
445 $extender->defaultableSections($sections, $section);
449 if (!empty($sections[$section])) {
450 return $sections[$section];
458 protected function defineOptions() {
468 'display_description' => FALSE,
470 'hide_attachment_summary' => TRUE,
471 'show_admin_links' => TRUE,
474 'use_more_always' => TRUE,
475 'use_more_text' => TRUE,
476 'exposed_form' => TRUE,
478 'link_display' => TRUE,
489 'relationships' => TRUE,
494 'filter_groups' => TRUE,
504 'display_comment' => [
510 'display_description' => [
516 'hide_attachment_summary' => [
519 'show_admin_links' => [
525 'use_more_always' => [
540 'rendering_language' => [
541 'default' => '***LANGUAGE_entity_translation***',
544 // These types are all plugins that can have individual settings
545 // and therefore need special handling.
548 'type' => ['default' => 'none'],
549 'options' => ['default' => []],
551 'merge_defaults' => [$this, 'mergePlugin'],
555 'type' => ['default' => 'tag'],
556 'options' => ['default' => []],
558 'merge_defaults' => [$this, 'mergePlugin'],
562 'type' => ['default' => 'views_query'],
563 'options' => ['default' => []],
565 'merge_defaults' => [$this, 'mergePlugin'],
569 'type' => ['default' => 'basic'],
570 'options' => ['default' => []],
572 'merge_defaults' => [$this, 'mergePlugin'],
576 'type' => ['default' => 'mini'],
577 'options' => ['default' => []],
579 'merge_defaults' => [$this, 'mergePlugin'],
583 'type' => ['default' => 'default'],
584 'options' => ['default' => []],
586 'merge_defaults' => [$this, 'mergePlugin'],
590 'type' => ['default' => 'fields'],
591 'options' => ['default' => []],
593 'merge_defaults' => [$this, 'mergePlugin'],
602 'merge_defaults' => [$this, 'mergeHandler'],
606 'merge_defaults' => [$this, 'mergeHandler'],
610 'merge_defaults' => [$this, 'mergeHandler'],
613 // We want these to export last.
614 // These are the 5 handler types.
617 'merge_defaults' => [$this, 'mergeHandler'],
621 'merge_defaults' => [$this, 'mergeHandler'],
625 'merge_defaults' => [$this, 'mergeHandler'],
629 'merge_defaults' => [$this, 'mergeHandler'],
633 'operator' => ['default' => 'AND'],
634 'groups' => ['default' => [1 => 'AND']],
642 if (!$this->usesPager()) {
643 $options['defaults']['default']['pager'] = FALSE;
644 $options['pager']['contains']['type']['default'] = 'some';
647 if ($this->isDefaultDisplay()) {
648 unset($options['defaults']);
651 $options['display_extenders'] = ['default' => []];
652 // First allow display extenders to provide new options.
653 foreach ($this->extenders as $extender_id => $extender) {
654 $options['display_extenders']['contains'][$extender_id]['contains'] = $extender->defineOptions();
657 // Then allow display extenders to alter existing default values.
658 foreach ($this->extenders as $extender) {
659 $extender->defineOptionsAlter($options);
668 public function hasPath() { return FALSE; }
673 public function usesLinkDisplay() { return !$this->hasPath(); }
678 public function usesExposedFormInBlock() { return $this->hasPath(); }
683 public function getAttachedDisplays() {
684 $current_display_id = $this->display['id'];
685 $attached_displays = [];
687 // Go through all displays and search displays which link to this one.
688 foreach ($this->view->storage->get('display') as $display_id => $display) {
689 if (isset($display['display_options']['displays'])) {
690 $displays = $display['display_options']['displays'];
691 if (isset($displays[$current_display_id])) {
692 $attached_displays[] = $display_id;
697 return $attached_displays;
703 public function getLinkDisplay() {
704 $display_id = $this->getOption('link_display');
705 // If unknown, pick the first one.
706 if (empty($display_id) || !$this->view->displayHandlers->has($display_id)) {
707 foreach ($this->view->displayHandlers as $display_id => $display) {
708 if (!empty($display) && $display->hasPath()) {
716 // Fall-through returns NULL.
722 public function getPath() {
723 if ($this->hasPath()) {
724 return $this->getOption('path');
727 $display_id = $this->getLinkDisplay();
728 if ($display_id && $this->view->displayHandlers->has($display_id) && is_object($this->view->displayHandlers->get($display_id))) {
729 return $this->view->displayHandlers->get($display_id)->getPath();
736 public function getRoutedDisplay() {
737 // If this display has a route, return this display.
738 if ($this instanceof DisplayRouterInterface) {
742 // If the display does not have a route (e.g. a block display), get the
743 // route for the linked display.
744 $display_id = $this->getLinkDisplay();
745 if ($display_id && $this->view->displayHandlers->has($display_id) && is_object($this->view->displayHandlers->get($display_id))) {
746 return $this->view->displayHandlers->get($display_id)->getRoutedDisplay();
749 // No routed display exists, so return NULL
756 public function getUrl() {
757 return $this->view->getUrl(NULL, $this->display['id']);
763 public function isDefaulted($option) {
764 return !$this->isDefaultDisplay() && !empty($this->default_display) && !empty($this->options['defaults'][$option]);
770 public function getOption($option) {
771 if ($this->isDefaulted($option)) {
772 return $this->default_display->getOption($option);
775 if (array_key_exists($option, $this->options)) {
776 return $this->options[$option];
783 public function usesFields() {
784 return $this->getPlugin('style')->usesFields();
790 public function getPlugin($type) {
791 // Look up the plugin name to use for this instance.
792 $options = $this->getOption($type);
794 // Return now if no options have been loaded.
795 if (empty($options) || !isset($options['type'])) {
799 // Query plugins allow specifying a specific query class per base table.
800 if ($type == 'query') {
801 $views_data = Views::viewsData()->get($this->view->storage->get('base_table'));
802 $name = isset($views_data['table']['base']['query_id']) ? $views_data['table']['base']['query_id'] : 'views_query';
805 $name = $options['type'];
808 // Plugin instances are stored on the display for re-use.
809 if (!isset($this->plugins[$type][$name])) {
810 $plugin = Views::pluginManager($type)->createInstance($name);
812 // Initialize the plugin.
813 $plugin->init($this->view, $this, $options['options']);
815 $this->plugins[$type][$name] = $plugin;
818 return $this->plugins[$type][$name];
824 public function &getHandler($type, $id) {
825 if (!isset($this->handlers[$type])) {
826 $this->getHandlers($type);
829 if (isset($this->handlers[$type][$id])) {
830 return $this->handlers[$type][$id];
833 // So we can return a reference.
841 public function &getHandlers($type) {
842 if (!isset($this->handlers[$type])) {
843 $this->handlers[$type] = [];
844 $types = ViewExecutable::getHandlerTypes();
845 $plural = $types[$type]['plural'];
847 // Cast to an array so that if the display does not have any handlers of
848 // this type there is no PHP error.
849 foreach ((array) $this->getOption($plural) as $id => $info) {
850 // If this is during form submission and there are temporary options
851 // which can only appear if the view is in the edit cache, use those
852 // options instead. This is used for AJAX multi-step stuff.
853 if ($this->view->getRequest()->request->get('form_id') && isset($this->view->temporary_options[$type][$id])) {
854 $info = $this->view->temporary_options[$type][$id];
857 if ($info['id'] != $id) {
861 // If aggregation is on, the group type might override the actual
862 // handler that is in use. This piece of code checks that and,
863 // if necessary, sets the override handler.
865 if ($this->useGroupBy() && !empty($info['group_type'])) {
866 if (empty($this->view->query)) {
867 $this->view->initQuery();
869 $aggregate = $this->view->query->getAggregationInfo();
870 if (!empty($aggregate[$info['group_type']]['handler'][$type])) {
871 $override = $aggregate[$info['group_type']]['handler'][$type];
875 if (!empty($types[$type]['type'])) {
876 $handler_type = $types[$type]['type'];
879 $handler_type = $type;
882 if ($handler = Views::handlerManager($handler_type)->getHandler($info, $override)) {
883 // Special override for area types so they know where they come from.
884 if ($handler instanceof AreaPluginBase) {
885 $handler->areaType = $type;
888 $handler->init($this->view, $this, $info);
889 $this->handlers[$type][$id] = &$handler;
892 // Prevent reference problems.
897 return $this->handlers[$type];
901 * Gets all the handlers used by the display.
903 * @param bool $only_overrides
904 * Whether to include only overridden handlers.
906 * @return \Drupal\views\Plugin\views\ViewsHandlerInterface[]
908 protected function getAllHandlers($only_overrides = FALSE) {
909 $handler_types = Views::getHandlerTypes();
911 // Collect all dependencies of all handlers.
912 foreach ($handler_types as $handler_type => $handler_type_info) {
913 if ($only_overrides && $this->isDefaulted($handler_type_info['plural'])) {
916 $handlers = array_merge($handlers, array_values($this->getHandlers($handler_type)));
922 * Gets all the plugins used by the display.
924 * @param bool $only_overrides
925 * Whether to include only overridden plugins.
927 * @return \Drupal\views\Plugin\views\ViewsPluginInterface[]
929 protected function getAllPlugins($only_overrides = FALSE) {
931 // Collect all dependencies of plugins.
932 foreach (Views::getPluginTypes('plugin') as $plugin_type) {
933 $plugin = $this->getPlugin($plugin_type);
937 if ($only_overrides && $this->isDefaulted($plugin_type)) {
940 $plugins[] = $plugin;
948 public function calculateDependencies() {
949 $this->dependencies = parent::calculateDependencies();
950 // Collect all the dependencies of handlers and plugins. Only calculate
951 // their dependencies if they are configured by this display.
952 $plugins = array_merge($this->getAllHandlers(TRUE), $this->getAllPlugins(TRUE));
953 array_walk($plugins, [$this, 'calculatePluginDependencies']);
955 return $this->dependencies;
962 public function getFieldLabels($groupable_only = FALSE) {
964 foreach ($this->getHandlers('relationship') as $relationship => $handler) {
965 $relationships[$relationship] = $handler->adminLabel();
968 foreach ($this->getHandlers('field') as $id => $handler) {
969 if ($groupable_only && !$handler->useStringGroupBy()) {
970 // Continue to next handler if it's not groupable.
973 if ($label = $handler->label()) {
974 $options[$id] = $label;
977 $options[$id] = $handler->adminLabel();
979 if (!empty($handler->options['relationship']) && !empty($relationships[$handler->options['relationship']])) {
980 $options[$id] = '(' . $relationships[$handler->options['relationship']] . ') ' . $options[$id];
989 public function setOption($option, $value) {
990 if ($this->isDefaulted($option)) {
991 return $this->default_display->setOption($option, $value);
994 // Set this in two places: On the handler where we'll notice it
995 // but also on the display object so it gets saved. This should
996 // only be a temporary fix.
997 $this->display['display_options'][$option] = $value;
998 return $this->options[$option] = $value;
1004 public function overrideOption($option, $value) {
1005 $this->setOverride($option, FALSE);
1006 $this->setOption($option, $value);
1012 public function optionLink($text, $section, $class = '', $title = '') {
1014 $text = $this->t('Broken field');
1017 if (!empty($class)) {
1018 $text = SafeMarkup::format('<span>@text</span>', ['@text' => $text]);
1021 if (empty($title)) {
1025 return \Drupal::l($text, new Url('views_ui.form_display', [
1027 'view' => $this->view->storage->id(),
1028 'display_id' => $this->display['id'],
1032 'class' => ['views-ajax-link', $class],
1034 'id' => Html::getUniqueId('views-' . $this->display['id'] . '-' . $section)
1042 public function getArgumentsTokens() {
1044 if (!empty($this->view->build_info['substitutions'])) {
1045 $tokens = $this->view->build_info['substitutions'];
1054 public function optionsSummary(&$categories, &$options) {
1057 'title' => $this->t('Title'),
1058 'column' => 'first',
1061 'title' => $this->t('Format'),
1062 'column' => 'first',
1065 'title' => $this->t('Filters'),
1066 'column' => 'first',
1069 'title' => $this->t('Fields'),
1070 'column' => 'first',
1073 'title' => $this->t('Pager'),
1074 'column' => 'second',
1077 'title' => $this->t('Language'),
1078 'column' => 'second',
1081 'title' => $this->t('Exposed form'),
1082 'column' => 'third',
1089 'column' => 'second',
1095 'title' => $this->t('Other'),
1096 'column' => 'third',
1103 if ($this->display['id'] != 'default') {
1104 $options['display_id'] = [
1105 'category' => 'other',
1106 'title' => $this->t('Machine Name'),
1107 'value' => !empty($this->display['new_id']) ? $this->display['new_id'] : $this->display['id'],
1108 'desc' => $this->t('Change the machine name of this display.'),
1112 $display_comment = views_ui_truncate($this->getOption('display_comment'), 80);
1113 $options['display_comment'] = [
1114 'category' => 'other',
1115 'title' => $this->t('Administrative comment'),
1116 'value' => !empty($display_comment) ? $display_comment : $this->t('None'),
1117 'desc' => $this->t('Comment or document this display.'),
1120 $title = strip_tags($this->getOption('title'));
1122 $title = $this->t('None');
1125 $options['title'] = [
1126 'category' => 'title',
1127 'title' => $this->t('Title'),
1128 'value' => views_ui_truncate($title, 32),
1129 'desc' => $this->t('Change the title that this display will use.'),
1132 $style_plugin_instance = $this->getPlugin('style');
1133 $style_summary = empty($style_plugin_instance->definition['title']) ? $this->t('Missing style plugin') : $style_plugin_instance->summaryTitle();
1134 $style_title = empty($style_plugin_instance->definition['title']) ? $this->t('Missing style plugin') : $style_plugin_instance->pluginTitle();
1136 $options['style'] = [
1137 'category' => 'format',
1138 'title' => $this->t('Format'),
1139 'value' => $style_title,
1140 'setting' => $style_summary,
1141 'desc' => $this->t('Change the way content is formatted.'),
1144 // This adds a 'Settings' link to the style_options setting if the style has
1146 if ($style_plugin_instance->usesOptions()) {
1147 $options['style']['links']['style_options'] = $this->t('Change settings for this format');
1150 if ($style_plugin_instance->usesRowPlugin()) {
1151 $row_plugin_instance = $this->getPlugin('row');
1152 $row_summary = empty($row_plugin_instance->definition['title']) ? $this->t('Missing row plugin') : $row_plugin_instance->summaryTitle();
1153 $row_title = empty($row_plugin_instance->definition['title']) ? $this->t('Missing row plugin') : $row_plugin_instance->pluginTitle();
1156 'category' => 'format',
1157 'title' => $this->t('Show'),
1158 'value' => $row_title,
1159 'setting' => $row_summary,
1160 'desc' => $this->t('Change the way each row in the view is styled.'),
1162 // This adds a 'Settings' link to the row_options setting if the row style
1164 if ($row_plugin_instance->usesOptions()) {
1165 $options['row']['links']['row_options'] = $this->t('Change settings for this style');
1168 if ($this->usesAJAX()) {
1169 $options['use_ajax'] = [
1170 'category' => 'other',
1171 'title' => $this->t('Use AJAX'),
1172 'value' => $this->getOption('use_ajax') ? $this->t('Yes') : $this->t('No'),
1173 'desc' => $this->t('Change whether or not this display will use AJAX.'),
1176 if ($this->usesAttachments()) {
1177 $options['hide_attachment_summary'] = [
1178 'category' => 'other',
1179 'title' => $this->t('Hide attachments in summary'),
1180 'value' => $this->getOption('hide_attachment_summary') ? $this->t('Yes') : $this->t('No'),
1181 'desc' => $this->t('Change whether or not to display attachments when displaying a contextual filter summary.'),
1184 if (!isset($this->definition['contextual links locations']) || !empty($this->definition['contextual links locations'])) {
1185 $options['show_admin_links'] = [
1186 'category' => 'other',
1187 'title' => $this->t('Contextual links'),
1188 'value' => $this->getOption('show_admin_links') ? $this->t('Shown') : $this->t('Hidden'),
1189 'desc' => $this->t('Change whether or not to display contextual links for this view.'),
1193 $pager_plugin = $this->getPlugin('pager');
1194 if (!$pager_plugin) {
1195 // Default to the no pager plugin.
1196 $pager_plugin = Views::pluginManager('pager')->createInstance('none');
1199 $pager_str = $pager_plugin->summaryTitle();
1201 $options['pager'] = [
1202 'category' => 'pager',
1203 'title' => $this->t('Use pager'),
1204 'value' => $pager_plugin->pluginTitle(),
1205 'setting' => $pager_str,
1206 'desc' => $this->t("Change this display's pager setting."),
1209 // If pagers aren't allowed, change the text of the item.
1210 if (!$this->usesPager()) {
1211 $options['pager']['title'] = $this->t('Items to display');
1214 if ($pager_plugin->usesOptions()) {
1215 $options['pager']['links']['pager_options'] = $this->t('Change settings for this pager type.');
1218 if ($this->usesMore()) {
1219 $options['use_more'] = [
1220 'category' => 'pager',
1221 'title' => $this->t('More link'),
1222 'value' => $this->getOption('use_more') ? $this->t('Yes') : $this->t('No'),
1223 'desc' => $this->t('Specify whether this display will provide a "more" link.'),
1227 $this->view->initQuery();
1228 if ($this->view->query->getAggregationInfo()) {
1229 $options['group_by'] = [
1230 'category' => 'other',
1231 'title' => $this->t('Use aggregation'),
1232 'value' => $this->getOption('group_by') ? $this->t('Yes') : $this->t('No'),
1233 'desc' => $this->t('Allow grouping and aggregation (calculation) of fields.'),
1237 $options['query'] = [
1238 'category' => 'other',
1239 'title' => $this->t('Query settings'),
1240 'value' => $this->t('Settings'),
1241 'desc' => $this->t('Allow to set some advanced settings for the query plugin'),
1244 if (\Drupal::languageManager()->isMultilingual() && $this->isBaseTableTranslatable()) {
1245 $rendering_language_options = $this->buildRenderingLanguageOptions();
1246 $options['rendering_language'] = [
1247 'category' => 'language',
1248 'title' => $this->t('Rendering Language'),
1249 'value' => $rendering_language_options[$this->getOption('rendering_language')],
1250 'desc' => $this->t('All content that supports translations will be displayed in the selected language.'),
1254 $access_plugin = $this->getPlugin('access');
1255 if (!$access_plugin) {
1256 // Default to the no access control plugin.
1257 $access_plugin = Views::pluginManager('access')->createInstance('none');
1260 $access_str = $access_plugin->summaryTitle();
1262 $options['access'] = [
1263 'category' => 'access',
1264 'title' => $this->t('Access'),
1265 'value' => $access_plugin->pluginTitle(),
1266 'setting' => $access_str,
1267 'desc' => $this->t('Specify access control type for this display.'),
1270 if ($access_plugin->usesOptions()) {
1271 $options['access']['links']['access_options'] = $this->t('Change settings for this access type.');
1274 $cache_plugin = $this->getPlugin('cache');
1275 if (!$cache_plugin) {
1276 // Default to the no cache control plugin.
1277 $cache_plugin = Views::pluginManager('cache')->createInstance('none');
1280 $cache_str = $cache_plugin->summaryTitle();
1282 $options['cache'] = [
1283 'category' => 'other',
1284 'title' => $this->t('Caching'),
1285 'value' => $cache_plugin->pluginTitle(),
1286 'setting' => $cache_str,
1287 'desc' => $this->t('Specify caching type for this display.'),
1290 if ($cache_plugin->usesOptions()) {
1291 $options['cache']['links']['cache_options'] = $this->t('Change settings for this caching type.');
1294 if ($access_plugin->usesOptions()) {
1295 $options['access']['links']['access_options'] = $this->t('Change settings for this access type.');
1298 if ($this->usesLinkDisplay()) {
1299 $link_display_option = $this->getOption('link_display');
1300 $link_display = $this->t('None');
1302 if ($link_display_option == 'custom_url') {
1303 $link_display = $this->t('Custom URL');
1305 elseif (!empty($link_display_option)) {
1306 $display_id = $this->getLinkDisplay();
1307 $displays = $this->view->storage->get('display');
1308 if (!empty($displays[$display_id])) {
1309 $link_display = $displays[$display_id]['display_title'];
1313 $options['link_display'] = [
1314 'category' => 'pager',
1315 'title' => $this->t('Link display'),
1316 'value' => $link_display,
1317 'desc' => $this->t('Specify which display or custom URL this display will link to.'),
1321 if ($this->usesExposedFormInBlock()) {
1322 $options['exposed_block'] = [
1323 'category' => 'exposed',
1324 'title' => $this->t('Exposed form in block'),
1325 'value' => $this->getOption('exposed_block') ? $this->t('Yes') : $this->t('No'),
1326 'desc' => $this->t('Allow the exposed form to appear in a block instead of the view.'),
1330 /** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginInterface $exposed_form_plugin */
1331 $exposed_form_plugin = $this->getPlugin('exposed_form');
1332 if (!$exposed_form_plugin) {
1333 // Default to the no cache control plugin.
1334 $exposed_form_plugin = Views::pluginManager('exposed_form')->createInstance('basic');
1337 $exposed_form_str = $exposed_form_plugin->summaryTitle();
1339 $options['exposed_form'] = [
1340 'category' => 'exposed',
1341 'title' => $this->t('Exposed form style'),
1342 'value' => $exposed_form_plugin->pluginTitle(),
1343 'setting' => $exposed_form_str,
1344 'desc' => $this->t('Select the kind of exposed filter to use.'),
1347 if ($exposed_form_plugin->usesOptions()) {
1348 $options['exposed_form']['links']['exposed_form_options'] = $this->t('Exposed form settings for this exposed form style.');
1351 $css_class = trim($this->getOption('css_class'));
1353 $css_class = $this->t('None');
1356 $options['css_class'] = [
1357 'category' => 'other',
1358 'title' => $this->t('CSS class'),
1359 'value' => $css_class,
1360 'desc' => $this->t('Change the CSS class name(s) that will be added to this display.'),
1363 foreach ($this->extenders as $extender) {
1364 $extender->optionsSummary($categories, $options);
1371 public function buildOptionsForm(&$form, FormStateInterface $form_state) {
1372 parent::buildOptionsForm($form, $form_state);
1373 $section = $form_state->get('section');
1374 if ($this->defaultableSections($section)) {
1375 views_ui_standard_display_dropdown($form, $form_state, $section);
1377 $form['#title'] = $this->display['display_title'] . ': ';
1379 // Set the 'section' to highlight on the form.
1380 // If it's the item we're looking at is pulling from the default display,
1381 // reflect that. Don't use is_defaulted since we want it to show up even
1382 // on the default display.
1383 if (!empty($this->options['defaults'][$section])) {
1384 $form['#section'] = 'default-' . $section;
1387 $form['#section'] = $this->display['id'] . '-' . $section;
1392 $form['#title'] .= $this->t('The machine name of this display');
1393 $form['display_id'] = [
1394 '#type' => 'textfield',
1395 '#title' => $this->t('Machine name of the display'),
1396 '#default_value' => !empty($this->display['new_id']) ? $this->display['new_id'] : $this->display['id'],
1397 '#required' => TRUE,
1401 case 'display_title':
1402 $form['#title'] .= $this->t('The name and the description of this display');
1403 $form['display_title'] = [
1404 '#title' => $this->t('Administrative name'),
1405 '#type' => 'textfield',
1406 '#default_value' => $this->display['display_title'],
1408 $form['display_description'] = [
1409 '#title' => $this->t('Administrative description'),
1410 '#type' => 'textfield',
1411 '#default_value' => $this->getOption('display_description'),
1414 case 'display_comment':
1415 $form['#title'] .= $this->t('Administrative comment');
1416 $form['display_comment'] = [
1417 '#type' => 'textarea',
1418 '#title' => $this->t('Administrative comment'),
1419 '#description' => $this->t('This description will only be seen within the administrative interface and can be used to document this display.'),
1420 '#default_value' => $this->getOption('display_comment'),
1424 $form['#title'] .= $this->t('The title of this view');
1426 '#title' => $this->t('Title'),
1427 '#type' => 'textfield',
1428 '#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.'),
1429 '#default_value' => $this->getOption('title'),
1430 '#maxlength' => 255,
1434 $form['#title'] .= $this->t('CSS class');
1435 $form['css_class'] = [
1436 '#type' => 'textfield',
1437 '#title' => $this->t('CSS class name(s)'),
1438 '#description' => $this->t('Separate multiple classes by spaces.'),
1439 '#default_value' => $this->getOption('css_class'),
1443 $form['#title'] .= $this->t('AJAX');
1444 $form['use_ajax'] = [
1445 '#description' => $this->t('Options such as paging, table sorting, and exposed filters will not initiate a page refresh.'),
1446 '#type' => 'checkbox',
1447 '#title' => $this->t('Use AJAX'),
1448 '#default_value' => $this->getOption('use_ajax') ? 1 : 0,
1451 case 'hide_attachment_summary':
1452 $form['#title'] .= $this->t('Hide attachments when displaying a contextual filter summary');
1453 $form['hide_attachment_summary'] = [
1454 '#type' => 'checkbox',
1455 '#title' => $this->t('Hide attachments in summary'),
1456 '#default_value' => $this->getOption('hide_attachment_summary') ? 1 : 0,
1459 case 'show_admin_links':
1460 $form['#title'] .= $this->t('Show contextual links on this view.');
1461 $form['show_admin_links'] = [
1462 '#type' => 'checkbox',
1463 '#title' => $this->t('Show contextual links'),
1464 '#default_value' => $this->getOption('show_admin_links'),
1468 $form['#title'] .= $this->t('Add a more link to the bottom of the display.');
1469 $form['use_more'] = [
1470 '#type' => 'checkbox',
1471 '#title' => $this->t('Create more link'),
1472 '#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."),
1473 '#default_value' => $this->getOption('use_more'),
1475 $form['use_more_always'] = [
1476 '#type' => 'checkbox',
1477 '#title' => $this->t('Always display the more link'),
1478 '#description' => $this->t('Check this to display the more link even if there are no more items to display.'),
1479 '#default_value' => $this->getOption('use_more_always'),
1482 ':input[name="use_more"]' => ['checked' => TRUE],
1486 $form['use_more_text'] = [
1487 '#type' => 'textfield',
1488 '#title' => $this->t('More link text'),
1489 '#description' => $this->t('The text to display for the more link.'),
1490 '#default_value' => $this->getOption('use_more_text'),
1493 ':input[name="use_more"]' => ['checked' => TRUE],
1499 $form['#title'] .= $this->t('Allow grouping and aggregation (calculation) of fields.');
1500 $form['group_by'] = [
1501 '#type' => 'checkbox',
1502 '#title' => $this->t('Aggregate'),
1503 '#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.'),
1504 '#default_value' => $this->getOption('group_by'),
1508 $form['#title'] .= $this->t('Access restrictions');
1510 '#prefix' => '<div class="clearfix">',
1511 '#suffix' => '</div>',
1515 $access = $this->getOption('access');
1516 $form['access']['type'] = [
1517 '#title' => $this->t('Access'),
1518 '#title_display' => 'invisible',
1519 '#type' => 'radios',
1520 '#options' => Views::fetchPluginNames('access', $this->getType(), [$this->view->storage->get('base_table')]),
1521 '#default_value' => $access['type'],
1524 $access_plugin = $this->getPlugin('access');
1525 if ($access_plugin->usesOptions()) {
1527 '#prefix' => '<div class="js-form-item form-item description">',
1528 '#markup' => $this->t('You may also adjust the @settings for the currently selected access restriction.', ['@settings' => $this->optionLink($this->t('settings'), 'access_options')]),
1529 '#suffix' => '</div>',
1534 case 'access_options':
1535 $plugin = $this->getPlugin('access');
1536 $form['#title'] .= $this->t('Access options');
1538 $form['access_options'] = [
1541 $plugin->buildOptionsForm($form['access_options'], $form_state);
1545 $form['#title'] .= $this->t('Caching');
1547 '#prefix' => '<div class="clearfix">',
1548 '#suffix' => '</div>',
1552 $cache = $this->getOption('cache');
1553 $form['cache']['type'] = [
1554 '#title' => $this->t('Caching'),
1555 '#title_display' => 'invisible',
1556 '#type' => 'radios',
1557 '#options' => Views::fetchPluginNames('cache', $this->getType(), [$this->view->storage->get('base_table')]),
1558 '#default_value' => $cache['type'],
1561 $cache_plugin = $this->getPlugin('cache');
1562 if ($cache_plugin->usesOptions()) {
1564 '#prefix' => '<div class="js-form-item form-item description">',
1565 '#suffix' => '</div>',
1566 '#markup' => $this->t('You may also adjust the @settings for the currently selected cache mechanism.', ['@settings' => $this->optionLink($this->t('settings'), 'cache_options')]),
1570 case 'cache_options':
1571 $plugin = $this->getPlugin('cache');
1572 $form['#title'] .= $this->t('Caching options');
1574 $form['cache_options'] = [
1577 $plugin->buildOptionsForm($form['cache_options'], $form_state);
1581 $query_options = $this->getOption('query');
1582 $plugin_name = $query_options['type'];
1584 $form['#title'] .= $this->t('Query options');
1585 $this->view->initQuery();
1586 if ($this->view->query) {
1591 '#value' => $plugin_name,
1598 $this->view->query->buildOptionsForm($form['query']['options'], $form_state);
1601 case 'rendering_language':
1602 $form['#title'] .= $this->t('Rendering language');
1603 if (\Drupal::languageManager()->isMultilingual() && $this->isBaseTableTranslatable()) {
1604 $options = $this->buildRenderingLanguageOptions();
1605 $form['rendering_language'] = [
1606 '#type' => 'select',
1607 '#options' => $options,
1608 '#title' => $this->t('Rendering language'),
1609 '#description' => $this->t('All content that supports translations will be displayed in the selected language.'),
1610 '#default_value' => $this->getOption('rendering_language'),
1614 $form['rendering_language']['#markup'] = $this->t('The view is not based on a translatable entity type or the site is not multilingual.');
1618 $form['#title'] .= $this->t('How should this view be styled');
1619 $style_plugin = $this->getPlugin('style');
1621 '#prefix' => '<div class="clearfix">',
1622 '#suffix' => '</div>',
1625 $form['style']['type'] = [
1626 '#title' => $this->t('Style'),
1627 '#title_display' => 'invisible',
1628 '#type' => 'radios',
1629 '#options' => Views::fetchPluginNames('style', $this->getType(), [$this->view->storage->get('base_table')]),
1630 '#default_value' => $style_plugin->definition['id'],
1631 '#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.'),
1634 if ($style_plugin->usesOptions()) {
1636 '#prefix' => '<div class="js-form-item form-item description">',
1637 '#suffix' => '</div>',
1638 '#markup' => $this->t('You may also adjust the @settings for the currently selected style.', ['@settings' => $this->optionLink($this->t('settings'), 'style_options')]),
1643 case 'style_options':
1644 $form['#title'] .= $this->t('Style options');
1646 $style_plugin = $this->getOption('style');
1647 $name = $style_plugin['type'];
1650 if (!isset($name)) {
1651 $row_plugin = $this->getOption('row');
1652 $name = $row_plugin['type'];
1654 // If row, $style will be empty.
1655 if (empty($style)) {
1656 $form['#title'] .= $this->t('Row style options');
1658 $plugin = $this->getPlugin(empty($style) ? 'row' : 'style', $name);
1663 $plugin->buildOptionsForm($form[$section], $form_state);
1667 $form['#title'] .= $this->t('How should each row in this view be styled');
1668 $row_plugin_instance = $this->getPlugin('row');
1670 '#prefix' => '<div class="clearfix">',
1671 '#suffix' => '</div>',
1674 $form['row']['type'] = [
1675 '#title' => $this->t('Row'),
1676 '#title_display' => 'invisible',
1677 '#type' => 'radios',
1678 '#options' => Views::fetchPluginNames('row', $this->getType(), [$this->view->storage->get('base_table')]),
1679 '#default_value' => $row_plugin_instance->definition['id'],
1682 if ($row_plugin_instance->usesOptions()) {
1684 '#prefix' => '<div class="js-form-item form-item description">',
1685 '#suffix' => '</div>',
1686 '#markup' => $this->t('You may also adjust the @settings for the currently selected row style.', ['@settings' => $this->optionLink($this->t('settings'), 'row_options')]),
1691 case 'link_display':
1692 $form['#title'] .= $this->t('Which display to use for path');
1693 $options = [FALSE => $this->t('None'), 'custom_url' => $this->t('Custom URL')];
1695 foreach ($this->view->storage->get('display') as $display_id => $display) {
1696 if ($this->view->displayHandlers->get($display_id)->hasPath()) {
1697 $options[$display_id] = $display['display_title'];
1701 $form['link_display'] = [
1702 '#type' => 'radios',
1703 '#options' => $options,
1704 '#description' => $this->t("Which display to use to get this display's path for things like summary links, rss feed links, more links, etc."),
1705 '#default_value' => $this->getOption('link_display'),
1709 $optgroup_arguments = (string) t('Arguments');
1710 foreach ($this->view->display_handler->getHandlers('argument') as $arg => $handler) {
1711 $options[$optgroup_arguments]["{{ arguments.$arg }}"] = $this->t('@argument title', ['@argument' => $handler->adminLabel()]);
1712 $options[$optgroup_arguments]["{{ raw_arguments.$arg }}"] = $this->t('@argument input', ['@argument' => $handler->adminLabel()]);
1716 // We have some options, so make a list.
1719 '#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.'),
1721 if (!empty($options)) {
1724 '#markup' => $this->t('The following tokens are available for this link. You may use Twig syntax in this field.'),
1725 '#suffix' => '</p>',
1727 foreach (array_keys($options) as $type) {
1728 if (!empty($options[$type])) {
1730 foreach ($options[$type] as $key => $value) {
1731 $items[] = $key . ' == ' . $value;
1734 '#theme' => 'item_list',
1737 $description[] = $item_list;
1742 $form['link_url'] = [
1743 '#type' => 'textfield',
1744 '#title' => $this->t('Custom URL'),
1745 '#default_value' => $this->getOption('link_url'),
1746 '#description' => $description,
1749 ':input[name="link_display"]' => ['value' => 'custom_url'],
1754 case 'exposed_block':
1755 $form['#title'] .= $this->t('Put the exposed form in a block');
1756 $form['description'] = [
1757 '#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>',
1759 $form['exposed_block'] = [
1760 '#type' => 'radios',
1761 '#options' => [1 => $this->t('Yes'), 0 => $this->t('No')],
1762 '#default_value' => $this->getOption('exposed_block') ? 1 : 0,
1765 case 'exposed_form':
1766 $form['#title'] .= $this->t('Exposed Form');
1767 $form['exposed_form'] = [
1768 '#prefix' => '<div class="clearfix">',
1769 '#suffix' => '</div>',
1773 $exposed_form = $this->getOption('exposed_form');
1774 $form['exposed_form']['type'] = [
1775 '#title' => $this->t('Exposed form'),
1776 '#title_display' => 'invisible',
1777 '#type' => 'radios',
1778 '#options' => Views::fetchPluginNames('exposed_form', $this->getType(), [$this->view->storage->get('base_table')]),
1779 '#default_value' => $exposed_form['type'],
1782 $exposed_form_plugin = $this->getPlugin('exposed_form');
1783 if ($exposed_form_plugin->usesOptions()) {
1785 '#prefix' => '<div class="js-form-item form-item description">',
1786 '#suffix' => '</div>',
1787 '#markup' => $this->t('You may also adjust the @settings for the currently selected style.', ['@settings' => $this->optionLink($this->t('settings'), 'exposed_form_options')]),
1791 case 'exposed_form_options':
1792 $plugin = $this->getPlugin('exposed_form');
1793 $form['#title'] .= $this->t('Exposed form options');
1795 $form['exposed_form_options'] = [
1798 $plugin->buildOptionsForm($form['exposed_form_options'], $form_state);
1802 $form['#title'] .= $this->t('Select pager');
1804 '#prefix' => '<div class="clearfix">',
1805 '#suffix' => '</div>',
1809 $pager = $this->getOption('pager');
1810 $form['pager']['type'] = [
1811 '#title' => $this->t('Pager'),
1812 '#title_display' => 'invisible',
1813 '#type' => 'radios',
1814 '#options' => Views::fetchPluginNames('pager', !$this->usesPager() ? 'basic' : NULL, [$this->view->storage->get('base_table')]),
1815 '#default_value' => $pager['type'],
1818 $pager_plugin = $this->getPlugin('pager');
1819 if ($pager_plugin->usesOptions()) {
1821 '#prefix' => '<div class="js-form-item form-item description">',
1822 '#suffix' => '</div>',
1823 '#markup' => $this->t('You may also adjust the @settings for the currently selected pager.', ['@settings' => $this->optionLink($this->t('settings'), 'pager_options')]),
1828 case 'pager_options':
1829 $plugin = $this->getPlugin('pager');
1830 $form['#title'] .= $this->t('Pager options');
1832 $form['pager_options'] = [
1835 $plugin->buildOptionsForm($form['pager_options'], $form_state);
1840 foreach ($this->extenders as $extender) {
1841 $extender->buildOptionsForm($form, $form_state);
1848 public function validateOptionsForm(&$form, FormStateInterface $form_state) {
1849 $section = $form_state->get('section');
1851 case 'display_title':
1852 if ($form_state->isValueEmpty('display_title')) {
1853 $form_state->setError($form['display_title'], $this->t('Display title may not be empty.'));
1857 $css_class = $form_state->getValue('css_class');
1858 if (preg_match('/[^a-zA-Z0-9-_ ]/', $css_class)) {
1859 $form_state->setError($form['css_class'], $this->t('CSS classes must be alphanumeric or dashes only.'));
1863 if ($form_state->getValue('display_id')) {
1864 if (preg_match('/[^a-z0-9_]/', $form_state->getValue('display_id'))) {
1865 $form_state->setError($form['display_id'], $this->t('Display name must be letters, numbers, or underscores only.'));
1868 foreach ($this->view->displayHandlers as $id => $display) {
1869 if ($id != $this->view->current_display && ($form_state->getValue('display_id') == $id || (isset($display->new_id) && $form_state->getValue('display_id') == $display->new_id))) {
1870 $form_state->setError($form['display_id'], $this->t('Display id should be unique.'));
1876 if ($this->view->query) {
1877 $this->view->query->validateOptionsForm($form['query'], $form_state);
1882 // Validate plugin options. Every section with "_options" in it, belongs to
1883 // a plugin type, like "style_options".
1884 if (strpos($section, '_options') !== FALSE) {
1885 $plugin_type = str_replace('_options', '', $section);
1886 // Load the plugin and let it handle the validation.
1887 if ($plugin = $this->getPlugin($plugin_type)) {
1888 $plugin->validateOptionsForm($form[$section], $form_state);
1892 foreach ($this->extenders as $extender) {
1893 $extender->validateOptionsForm($form, $form_state);
1900 public function submitOptionsForm(&$form, FormStateInterface $form_state) {
1901 // Not sure I like this being here, but it seems (?) like a logical place.
1902 $cache_plugin = $this->getPlugin('cache');
1903 if ($cache_plugin) {
1904 $cache_plugin->cacheFlush();
1907 $section = $form_state->get('section');
1910 if ($form_state->hasValue('display_id')) {
1911 $this->display['new_id'] = $form_state->getValue('display_id');
1914 case 'display_title':
1915 $this->display['display_title'] = $form_state->getValue('display_title');
1916 $this->setOption('display_description', $form_state->getValue('display_description'));
1919 $plugin = $this->getPlugin('query');
1921 $plugin->submitOptionsForm($form['query']['options'], $form_state);
1922 $this->setOption('query', $form_state->getValue($section));
1926 case 'link_display':
1927 $this->setOption('link_url', $form_state->getValue('link_url'));
1930 case 'display_comment':
1933 $this->setOption($section, $form_state->getValue($section));
1935 case 'rendering_language':
1936 $this->setOption('rendering_language', $form_state->getValue('rendering_language'));
1939 case 'hide_attachment_summary':
1940 case 'show_admin_links':
1941 case 'exposed_block':
1942 $this->setOption($section, (bool) $form_state->getValue($section));
1945 $this->setOption($section, intval($form_state->getValue($section)));
1946 $this->setOption('use_more_always', intval($form_state->getValue('use_more_always')));
1947 $this->setOption('use_more_text', $form_state->getValue('use_more_text'));
1952 case 'exposed_form':
1956 $plugin_type = $section;
1957 $plugin_options = $this->getOption($plugin_type);
1958 $type = $form_state->getValue([$plugin_type, 'type']);
1959 if ($plugin_options['type'] != $type) {
1960 /** @var \Drupal\views\Plugin\views\ViewsPluginInterface $plugin */
1961 $plugin = Views::pluginManager($plugin_type)->createInstance($type);
1963 $plugin->init($this->view, $this, $plugin_options['options']);
1966 'options' => $plugin->options,
1968 $plugin->filterByDefinedOptions($plugin_options['options']);
1969 $this->setOption($plugin_type, $plugin_options);
1970 if ($plugin->usesOptions()) {
1971 $form_state->get('view')->addFormToStack('display', $this->display['id'], $plugin_type . '_options');
1977 case 'access_options':
1978 case 'cache_options':
1979 case 'exposed_form_options':
1980 case 'pager_options':
1982 case 'style_options':
1983 // Submit plugin options. Every section with "_options" in it, belongs to
1984 // a plugin type, like "style_options".
1985 $plugin_type = str_replace('_options', '', $section);
1986 if ($plugin = $this->getPlugin($plugin_type)) {
1987 $plugin_options = $this->getOption($plugin_type);
1988 $plugin->submitOptionsForm($form[$plugin_type . '_options'], $form_state);
1989 $plugin_options['options'] = $form_state->getValue($section);
1990 $this->setOption($plugin_type, $plugin_options);
1995 $extender_options = $this->getOption('display_extenders');
1996 foreach ($this->extenders as $extender) {
1997 $extender->submitOptionsForm($form, $form_state);
1999 $plugin_id = $extender->getPluginId();
2000 $extender_options[$plugin_id] = $extender->options;
2002 $this->setOption('display_extenders', $extender_options);
2008 public function optionsOverride($form, FormStateInterface $form_state) {
2009 $this->setOverride($form_state->get('section'));
2015 public function setOverride($section, $new_state = NULL) {
2016 $options = $this->defaultableSections($section);
2021 if (!isset($new_state)) {
2022 $new_state = empty($this->options['defaults'][$section]);
2025 // For each option that is part of this group, fix our settings.
2026 foreach ($options as $option) {
2028 // Revert to defaults.
2029 unset($this->options[$option]);
2030 unset($this->display['display_options'][$option]);
2033 // Copy existing values into our display.
2034 $this->options[$option] = $this->getOption($option);
2035 $this->display['display_options'][$option] = $this->options[$option];
2037 $this->options['defaults'][$option] = $new_state;
2038 $this->display['display_options']['defaults'][$option] = $new_state;
2045 public function query() {
2046 foreach ($this->extenders as $extender) {
2054 public function renderFilters() { }
2059 public function renderPager() {
2066 public function renderMoreLink() {
2067 if ($this->isMoreEnabled() && ($this->useMoreAlways() || (!empty($this->view->pager) && $this->view->pager->hasMoreRecords()))) {
2068 // If the user has supplied a custom "More" link path, replace any
2069 // argument tokens and use that for the URL.
2070 if ($this->getOption('link_display') == 'custom_url' && $override_path = $this->getOption('link_url')) {
2071 $tokens = $this->getArgumentsTokens();
2072 $path = $this->viewsTokenReplace($override_path, $tokens);
2073 // @todo Views should expect and store a leading /. See:
2074 // https://www.drupal.org/node/2423913
2075 $url = Url::fromUserInput('/' . $path);
2077 // Otherwise, use the URL for the display.
2079 $url = $this->view->getUrl(NULL, $this->display['id']);
2082 // If a URL is available (either from the display or a custom path),
2083 // render the "More" link.
2086 if (!empty($this->view->exposed_raw_input)) {
2087 $url_options['query'] = $this->view->exposed_raw_input;
2089 $url->setOptions($url_options);
2092 '#type' => 'more_link',
2094 '#title' => $this->useMoreText(),
2095 '#view' => $this->view,
2104 public function render() {
2105 $rows = (!empty($this->view->result) || $this->view->style_plugin->evenEmpty()) ? $this->view->style_plugin->render($this->view->result) : [];
2108 '#theme' => $this->themeFunctions(),
2109 '#view' => $this->view,
2110 '#pre_render' => [[$this, 'elementPreRender']],
2112 // Assigned by reference so anything added in $element['#attached'] will
2113 // be available on the view.
2114 '#attached' => &$this->view->element['#attached'],
2115 '#cache' => &$this->view->element['#cache'],
2118 $this->applyDisplayCachablityMetadata($this->view->element);
2124 * Applies the cacheability of the current display to the given render array.
2126 * @param array $element
2127 * The render array with updated cacheability metadata.
2129 protected function applyDisplayCachablityMetadata(array &$element) {
2130 /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache */
2131 $cache = $this->getPlugin('cache');
2133 (new CacheableMetadata())
2134 ->setCacheTags(Cache::mergeTags($this->view->getCacheTags(), isset($this->display['cache_metadata']['tags']) ? $this->display['cache_metadata']['tags'] : []))
2135 ->setCacheContexts(isset($this->display['cache_metadata']['contexts']) ? $this->display['cache_metadata']['contexts'] : [])
2136 ->setCacheMaxAge(Cache::mergeMaxAges($cache->getCacheMaxAge(), isset($this->display['cache_metadata']['max-age']) ? $this->display['cache_metadata']['max-age'] : Cache::PERMANENT))
2137 ->merge(CacheableMetadata::createFromRenderArray($element))
2138 ->applyTo($element);
2144 public function elementPreRender(array $element) {
2145 $view = $element['#view'];
2146 $empty = empty($view->result);
2148 // Force a render array so CSS/JS can be attached.
2149 if (!is_array($element['#rows'])) {
2150 $element['#rows'] = ['#markup' => $element['#rows']];
2153 $element['#header'] = $view->display_handler->renderArea('header', $empty);
2154 $element['#footer'] = $view->display_handler->renderArea('footer', $empty);
2155 $element['#empty'] = $empty ? $view->display_handler->renderArea('empty', $empty) : [];
2156 $element['#exposed'] = !empty($view->exposed_widgets) ? $view->exposed_widgets : [];
2157 $element['#more'] = $view->display_handler->renderMoreLink();
2158 $element['#feed_icons'] = !empty($view->feedIcons) ? $view->feedIcons : [];
2160 if ($view->display_handler->renderPager()) {
2161 $exposed_input = isset($view->exposed_raw_input) ? $view->exposed_raw_input : NULL;
2162 $element['#pager'] = $view->renderPager($exposed_input);
2165 if (!empty($view->attachment_before)) {
2166 $element['#attachment_before'] = $view->attachment_before;
2168 if (!empty($view->attachment_after)) {
2169 $element['#attachment_after'] = $view->attachment_after;
2172 // If form fields were found in the view, reformat the view output as a form.
2173 if ($view->hasFormElements()) {
2174 // Only render row output if there are rows. Otherwise, render the empty
2176 if (!empty($element['#rows'])) {
2177 $output = $element['#rows'];
2180 $output = $element['#empty'];
2183 $form_object = ViewsForm::create(\Drupal::getContainer(), $view->storage->id(), $view->current_display, $view->args);
2184 $form = \Drupal::formBuilder()->getForm($form_object, $view, $output);
2185 // The form is requesting that all non-essential views elements be hidden,
2186 // usually because the rendered step is not a view result.
2187 if ($form['show_view_elements']['#value'] == FALSE) {
2188 $element['#header'] = [];
2189 $element['#exposed'] = [];
2190 $element['#pager'] = [];
2191 $element['#footer'] = [];
2192 $element['#more'] = [];
2193 $element['#feed_icons'] = [];
2196 $element['#rows'] = $form;
2205 public function renderArea($area, $empty = FALSE) {
2207 foreach ($this->getHandlers($area) as $key => $area_handler) {
2208 if ($area_render = $area_handler->render($empty)) {
2209 if (isset($area_handler->position)) {
2210 // Fix weight of area.
2211 $area_render['#weight'] = $area_handler->position;
2213 $return[$key] = $area_render;
2222 public function access(AccountInterface $account = NULL) {
2223 if (!isset($account)) {
2224 $account = \Drupal::currentUser();
2227 $plugin = $this->getPlugin('access');
2228 /** @var \Drupal\views\Plugin\views\access\AccessPluginBase $plugin */
2230 return $plugin->access($account);
2233 // Fallback to all access if no plugin.
2240 public function preExecute() {
2241 $this->view->setAjaxEnabled($this->ajaxEnabled());
2242 if ($this->isMoreEnabled() && !$this->useMoreAlways()) {
2243 $this->view->get_total_rows = TRUE;
2245 $this->view->initHandlers();
2246 if ($this->usesExposed()) {
2247 /** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginInterface $exposed_form */
2248 $exposed_form = $this->getPlugin('exposed_form');
2249 $exposed_form->preExecute();
2252 foreach ($this->extenders as $extender) {
2253 $extender->preExecute();
2260 public function calculateCacheMetadata() {
2261 $cache_metadata = new CacheableMetadata();
2263 // Iterate over ordinary views plugins.
2264 foreach (Views::getPluginTypes('plugin') as $plugin_type) {
2265 $plugin = $this->getPlugin($plugin_type);
2266 if ($plugin instanceof CacheableDependencyInterface) {
2267 $cache_metadata = $cache_metadata->merge(CacheableMetadata::createFromObject($plugin));
2271 // Iterate over all handlers. Note that at least the argument handler will
2272 // need to ask all its subplugins.
2273 foreach (array_keys(Views::getHandlerTypes()) as $handler_type) {
2274 $handlers = $this->getHandlers($handler_type);
2275 foreach ($handlers as $handler) {
2276 if ($handler instanceof CacheableDependencyInterface) {
2277 $cache_metadata = $cache_metadata->merge(CacheableMetadata::createFromObject($handler));
2282 /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache_plugin */
2283 if ($cache_plugin = $this->getPlugin('cache')) {
2284 $cache_plugin->alterCacheMetadata($cache_metadata);
2287 return $cache_metadata;
2293 public function getCacheMetadata() {
2294 if (!isset($this->display['cache_metadata'])) {
2295 $cache_metadata = $this->calculateCacheMetadata();
2296 $this->display['cache_metadata']['max-age'] = $cache_metadata->getCacheMaxAge();
2297 $this->display['cache_metadata']['contexts'] = $cache_metadata->getCacheContexts();
2298 $this->display['cache_metadata']['tags'] = $cache_metadata->getCacheTags();
2301 $cache_metadata = (new CacheableMetadata())
2302 ->setCacheMaxAge($this->display['cache_metadata']['max-age'])
2303 ->setCacheContexts($this->display['cache_metadata']['contexts'])
2304 ->setCacheTags($this->display['cache_metadata']['tags']);
2306 return $cache_metadata;
2312 public function execute() { }
2317 public function buildRenderable(array $args = [], $cache = TRUE) {
2318 $this->view->element += [
2320 '#name' => $this->view->storage->id(),
2321 '#display_id' => $this->display['id'],
2322 '#arguments' => $args,
2324 '#view' => $this->view,
2325 '#cache_properties' => ['#view_id', '#view_display_show_admin_links', '#view_display_plugin_id'],
2328 // When something passes $cache = FALSE, they're asking us not to create our
2329 // own render cache for it. However, we still need to include certain pieces
2330 // of cacheability metadata (e.g.: cache contexts), so they can bubble up.
2331 // Thus, we add the cacheability metadata first, then modify / remove the
2332 // cache keys depending on the $cache argument.
2333 $this->applyDisplayCachablityMetadata($this->view->element);
2335 $this->view->element['#cache'] += ['keys' => []];
2336 // Places like \Drupal\views\ViewExecutable::setCurrentPage() set up an
2337 // additional cache context.
2338 $this->view->element['#cache']['keys'] = array_merge(['views', 'display', $this->view->element['#name'], $this->view->element['#display_id']], $this->view->element['#cache']['keys']);
2341 // Remove the cache keys, to ensure render caching is not triggered. We
2342 // don't unset the other #cache values, to allow cacheability metadata to
2343 // still be bubbled.
2344 unset($this->view->element['#cache']['keys']);
2347 return $this->view->element;
2353 public static function buildBasicRenderable($view_id, $display_id, array $args = []) {
2356 '#name' => $view_id,
2357 '#display_id' => $display_id,
2358 '#arguments' => $args,
2361 'keys' => ['view', $view_id, 'display', $display_id],
2366 $build['#cache']['keys'][] = 'args';
2367 $build['#cache']['keys'][] = implode(',', $args);
2370 $build['#cache_properties'] = ['#view_id', '#view_display_show_admin_links', '#view_display_plugin_id'];
2379 public function preview() {
2380 return $this->view->render();
2386 public function getType() {
2393 public function validate() {
2395 // Make sure displays that use fields HAVE fields.
2396 if ($this->usesFields()) {
2398 foreach ($this->getHandlers('field') as $field) {
2399 if (empty($field->options['exclude'])) {
2405 $errors[] = $this->t('Display "@display" uses fields but there are none defined for it or all are excluded.', ['@display' => $this->display['display_title']]);
2409 // Validate the more link.
2410 if ($this->isMoreEnabled() && $this->getOption('link_display') !== 'custom_url') {
2411 $routed_display = $this->getRoutedDisplay();
2412 if (!$routed_display || !$routed_display->isEnabled()) {
2413 $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']]);
2417 if ($this->hasPath() && !$this->getOption('path')) {
2418 $errors[] = $this->t('Display "@display" uses a path but the path is undefined.', ['@display' => $this->display['display_title']]);
2421 // Validate style plugin.
2422 $style = $this->getPlugin('style');
2423 if (empty($style)) {
2424 $errors[] = $this->t('Display "@display" has an invalid style plugin.', ['@display' => $this->display['display_title']]);
2427 $result = $style->validate();
2428 if (!empty($result) && is_array($result)) {
2429 $errors = array_merge($errors, $result);
2433 // Validate query plugin.
2434 $query = $this->getPlugin('query');
2435 $result = $query->validate();
2436 if (!empty($result) && is_array($result)) {
2437 $errors = array_merge($errors, $result);
2440 // Check for missing relationships.
2441 $relationships = array_keys($this->getHandlers('relationship'));
2442 foreach (ViewExecutable::getHandlerTypes() as $type => $handler_type_info) {
2443 foreach ($this->getHandlers($type) as $handler_id => $handler) {
2444 if (!empty($handler->options['relationship']) && $handler->options['relationship'] != 'none' && !in_array($handler->options['relationship'], $relationships)) {
2445 $errors[] = $this->t('The %handler_type %handler uses a relationship that has been removed.', ['%handler_type' => $handler_type_info['lstitle'], '%handler' => $handler->adminLabel()]);
2450 // Validate handlers.
2451 foreach (ViewExecutable::getHandlerTypes() as $type => $info) {
2452 foreach ($this->getHandlers($type) as $handler) {
2453 $result = $handler->validate();
2454 if (!empty($result) && is_array($result)) {
2455 $errors = array_merge($errors, $result);
2466 public function newDisplay() {
2472 public function isIdentifierUnique($id, $identifier) {
2473 foreach (ViewExecutable::getHandlerTypes() as $type => $info) {
2474 foreach ($this->getHandlers($type) as $key => $handler) {
2475 if ($handler->canExpose() && $handler->isExposed()) {
2476 if ($handler->isAGroup()) {
2477 if ($id != $key && $identifier == $handler->options['group_info']['identifier']) {
2482 if ($id != $key && $identifier == $handler->options['expose']['identifier']) {
2495 public function outputIsEmpty() {
2496 if (!empty($this->view->result)) {
2500 // Check whether all of the area handlers are empty.
2501 foreach (['empty', 'footer', 'header'] as $type) {
2502 $handlers = $this->getHandlers($type);
2503 foreach ($handlers as $handler) {
2504 // If one is not empty, return FALSE now.
2505 if (!$handler->isEmpty()) {
2517 public function getSpecialBlocks() {
2520 if ($this->usesExposedFormInBlock()) {
2521 $delta = '-exp-' . $this->view->storage->id() . '-' . $this->display['id'];
2522 $desc = $this->t('Exposed form: @view-@display_id', ['@view' => $this->view->storage->id(), '@display_id' => $this->display['id']]);
2535 public function viewExposedFormBlocks() {
2536 // Avoid interfering with the admin forms.
2537 $route_name = \Drupal::routeMatch()->getRouteName();
2538 if (strpos($route_name, 'views_ui.') === 0) {
2541 $this->view->initHandlers();
2543 if ($this->usesExposed() && $this->getOption('exposed_block')) {
2544 /** @var \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginInterface $exposed_form */
2545 $exposed_form = $this->getPlugin('exposed_form');
2546 return $exposed_form->renderExposedForm(TRUE);
2553 public function getArgumentText() {
2555 'filter value not present' => $this->t('When the filter value is <em>NOT</em> available'),
2556 'filter value present' => $this->t('When the filter value <em>IS</em> available or a default is provided'),
2557 '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'."),
2564 public function getPagerText() {
2566 'items per page title' => $this->t('Items to display'),
2567 'items per page description' => $this->t('Enter 0 for no limit.')
2574 public function mergeDefaults() {
2575 $defined_options = $this->defineOptions();
2577 // Build a map of plural => singular for handler types.
2579 foreach (ViewExecutable::getHandlerTypes() as $type => $info) {
2580 $type_map[$info['plural']] = $type;
2583 // Find all defined options, that have specified a merge_defaults callback.
2584 foreach ($defined_options as $type => $definition) {
2585 if (!isset($definition['merge_defaults']) || !is_callable($definition['merge_defaults'])) {
2588 // Switch the type to singular, if it's a plural handler.
2589 if (isset($type_map[$type])) {
2590 $type = $type_map[$type];
2593 call_user_func($definition['merge_defaults'], $type);
2600 public function remove() {
2605 * Merges plugins default values.
2607 * @param string $type
2608 * The name of the plugin type option.
2610 protected function mergePlugin($type) {
2611 if (($options = $this->getOption($type)) && isset($options['options'])) {
2612 $plugin = $this->getPlugin($type);
2613 $options['options'] = $options['options'] + $plugin->options;
2614 $this->setOption($type, $options);
2619 * Merges handlers default values.
2621 * @param string $type
2622 * The name of the handler type option.
2624 protected function mergeHandler($type) {
2625 $types = ViewExecutable::getHandlerTypes();
2627 $options = $this->getOption($types[$type]['plural']);
2628 foreach ($this->getHandlers($type) as $id => $handler) {
2629 if (isset($options[$id])) {
2630 $options[$id] = $options[$id] + $handler->options;
2634 $this->setOption($types[$type]['plural'], $options);
2640 public function getExtenders() {
2641 return $this->extenders;
2645 * Returns the available rendering strategies for language-aware entities.
2648 * An array of available entity row renderers keyed by renderer identifiers.
2650 protected function buildRenderingLanguageOptions() {
2651 // @todo Consider making these plugins. See
2652 // https://www.drupal.org/node/2173811.
2653 // Pass the current rendering language (in this case a one element array) so
2654 // is not lost when there are language configuration changes.
2655 return $this->listLanguages(LanguageInterface::STATE_CONFIGURABLE | LanguageInterface::STATE_SITE_DEFAULT | PluginBase::INCLUDE_NEGOTIATED | PluginBase::INCLUDE_ENTITY, [$this->getOption('rendering_language')]);
2659 * Returns whether the base table is of a translatable entity type.
2662 * TRUE if the base table is of a translatable entity type, FALSE otherwise.
2664 protected function isBaseTableTranslatable() {
2665 if ($entity_type = $this->view->getBaseEntityType()) {
2666 return $entity_type->isTranslatable();