Loading core/modules/views/src/Plugin/views/display/DisplayPluginBase.php +3 −1 Original line number Diff line number Diff line Loading @@ -21,12 +21,14 @@ use Drupal\views\ViewExecutable; use Drupal\views\Plugin\views\PluginBase; use Drupal\views\Views; use Drupal\views\ViewsFormHelperTrait; /** * Base class for views display plugins. */ abstract class DisplayPluginBase extends PluginBase implements DisplayPluginInterface, DependentPluginInterface { use PluginDependencyTrait; use ViewsFormHelperTrait; /** * The top object of a view. Loading Loading @@ -1384,7 +1386,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { parent::buildOptionsForm($form, $form_state); $section = $form_state->get('section'); if ($this->defaultableSections($section)) { views_ui_standard_display_dropdown($form, $form_state, $section); $this->standardDisplayDropdown($form, $form_state, $section); } $form['#title'] = $this->display['display_title'] . ': '; Loading core/modules/views/src/Plugin/views/field/EntityField.php +3 −1 Original line number Diff line number Diff line Loading @@ -30,6 +30,7 @@ use Drupal\views\Plugin\DependentWithRemovalPluginInterface; use Drupal\views\ResultRow; use Drupal\views\ViewExecutable; use Drupal\views\ViewsFormHelperTrait; use Symfony\Component\DependencyInjection\Attribute\Autowire; /** Loading @@ -42,6 +43,7 @@ class EntityField extends FieldPluginBase implements CacheableDependencyInterfac use FieldAPIHandlerTrait; use PluginDependencyTrait; use ViewsFormHelperTrait; /** * An array to store field renderable arrays for use by renderItems(). Loading Loading @@ -506,7 +508,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { '#options' => $formatters, '#default_value' => $this->options['type'], '#ajax' => [ 'url' => views_ui_build_form_url($form_state), 'url' => $this->buildFormUrl($form_state), ], '#submit' => [[$this, 'submitTemporaryForm']], '#executes_submit_callback' => TRUE, Loading core/modules/views/src/Plugin/views/wizard/WizardPluginBase.php +7 −7 Original line number Diff line number Diff line Loading @@ -10,6 +10,7 @@ use Drupal\Core\Url; use Drupal\views\Entity\View; use Drupal\views\Views; use Drupal\views\ViewsFormAjaxHelperTrait; use Drupal\views_ui\ViewUI; use Drupal\views\Plugin\views\display\DisplayPluginBase; use Drupal\views\Plugin\views\PluginBase; Loading @@ -36,6 +37,8 @@ */ abstract class WizardPluginBase extends PluginBase implements WizardInterface { use ViewsFormAjaxHelperTrait; /** * The base table connected with the wizard. * Loading Loading @@ -285,7 +288,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { $style_form['style_plugin']['#default_value'] = static::getSelected($form_state, ['page', 'style', 'style_plugin'], 'default', $style_form['style_plugin']); // Changing this dropdown updates $form['displays']['page']['options'] via // AJAX. views_ui_add_ajax_trigger($style_form, 'style_plugin', ['displays', 'page', 'options']); $this->addAjaxTrigger($style_form, 'style_plugin', ['displays', 'page', 'options']); $this->buildFormStyle($form, $form_state, 'page'); $form['displays']['page']['options']['items_per_page'] = [ Loading Loading @@ -419,7 +422,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { ], 'default', $style_form['style_plugin']); // Changing this dropdown updates $form['displays']['block']['options'] // via AJAX. views_ui_add_ajax_trigger($style_form, 'style_plugin', ['displays', 'block', 'options']); $this->addAjaxTrigger($style_form, 'style_plugin', ['displays', 'block', 'options']); $this->buildFormStyle($form, $form_state, 'block'); $form['displays']['block']['options']['items_per_page'] = [ Loading Loading @@ -589,7 +592,7 @@ protected function buildFormStyle(array &$form, FormStateInterface $form_state, $default_value = $block_with_linked_titles_available ? 'titles_linked' : key($options); $style_form['row_plugin']['#default_value'] = static::getSelected($form_state, [$type, 'style', 'row_plugin'], $default_value, $style_form['row_plugin']); // Changing this dropdown updates the individual row options via AJAX. views_ui_add_ajax_trigger($style_form, 'row_plugin', ['displays', $type, 'options', 'style', 'row_options']); $this->addAjaxTrigger($style_form, 'row_plugin', ['displays', $type, 'options', 'style', 'row_options']); // This is the region that can be updated by AJAX. The base class doesn't // add anything here, but child classes can. Loading Loading @@ -621,8 +624,6 @@ protected function rowStyleOptions() { * available). */ protected function buildFilters(&$form, FormStateInterface $form_state) { \Drupal::moduleHandler()->loadInclude('views_ui', 'inc', 'admin'); $bundles = isset($this->entityTypeId) ? $this->bundleInfoService->getBundleInfo($this->entityTypeId) : []; // If the current base table support bundles and has more than one (like // user). Loading @@ -642,7 +643,7 @@ protected function buildFilters(&$form, FormStateInterface $form_state) { // Changing this dropdown updates the entire content of $form['displays'] // via AJAX, since each bundle might have entirely different fields // attached to it, etc. views_ui_add_ajax_trigger($form['displays']['show'], 'type', ['displays']); $this->addAjaxTrigger($form['displays']['show'], 'type', ['displays']); } } Loading Loading @@ -927,7 +928,6 @@ protected function defaultDisplayFiltersUser(array $form, FormStateInterface $fo // Figure out the table where $bundle_key lives. It may not be the same as // the base table for the view; the taxonomy vocabulary machine_name, for // example, is stored in taxonomy_vocabulary, not taxonomy_term_data. \Drupal::moduleHandler()->loadInclude('views_ui', 'inc', 'admin'); $fields = Views::viewsDataHelper()->fetchFields($this->base_table, 'filter'); $table = FALSE; if (isset($fields[$this->base_table . '.' . $bundle_key])) { Loading core/modules/views/src/ViewsFormAjaxHelperTrait.php 0 → 100644 +255 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\views; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; /** * Provides reusable code to be shared by Views Ajax forms. */ trait ViewsFormAjaxHelperTrait { use StringTranslationTrait; /** * Memory cache for processed buttons. * * @var array<string, int> */ protected static array $ajaxTriggerButtons = []; /** * Converts a form element in the new view wizard to be AJAX-enabled. * * This function takes a form element and adds AJAX behaviors to it such that * changing it triggers another part of the form to update automatically. It * also adds a submit-button to the form that appears next to the triggering * element, and that duplicates its functionality for users who do not have * JavaScript enabled (the button is automatically hidden for users who do * have JavaScript). * * To use this function, call it directly from your form builder function * immediately after you have defined the form element that will serve as the * JavaScript trigger. Calling it elsewhere (such as in hook_form_alter()) may * mean that the non-JavaScript fallback button does not appear in the correct * place in the form. * * @param array $wrappingElement * The element whose child will server as the AJAX trigger. For example, if * $form['some_wrapper']['triggering_element'] represents the element which * will trigger the AJAX behavior, you would pass $form['some_wrapper'] for * this parameter. * @param string $triggerKey * The key within the wrapping element that identifies which of its children * serves as the AJAX trigger. In the above example, you would pass * 'triggering_element' for this parameter. * @param string[] $refreshParents * An array of parent keys that point to the part of the form that AJAX will * refresh. For example, if triggering the AJAX behavior should cause * $form['dynamic_content']['section'] to be refreshed, you would pass * ['dynamic_content', 'section'] for this parameter. */ protected function addAjaxTrigger(array &$wrappingElement, string $triggerKey, array $refreshParents): void { // Add the AJAX behavior to the triggering element. $triggeringElement = &$wrappingElement[$triggerKey]; $triggeringElement['#ajax']['callback'] = static::class . ':ajaxUpdateForm'; // We do not use \Drupal\Component\Utility\Html::getUniqueId() to get an ID // for the AJAX wrapper. It remembers IDs across AJAX requests (and won't // reuse them). But in our case we need to use the same ID from request to // request so that the wrapper can be recognized by the AJAX system and its // content can be dynamically updated. So instead, we will keep track of // duplicate IDs (within a single request) on our own, later in this method. $triggeringElement['#ajax']['wrapper'] = 'edit-view-' . implode('-', $refreshParents) . '-wrapper'; // Add a submit-button for users who do not have JavaScript enabled. It // should be displayed next to the triggering element on the form. $buttonKey = $triggerKey . '_trigger_update'; $wrappingElement[$buttonKey] = [ '#type' => 'submit', // Hide this button when JavaScript is enabled. '#attributes' => ['class' => ['js-hide']], '#submit' => [[static::class, 'noJsSubmit']], // Add a process function to limit this button's validation errors to the // triggering element only. We have to do this in #process since until the // form API has added the #parents property to the triggering element for // us, we don't have any (easy) way to find out where its submitted values // will eventually appear in FormStateInterface->getValues(). '#process' => [ [static::class, 'addLimitedValidation'], ...$this->getElementInfo()->getInfoProperty('submit', '#process', []), ], // Add an after-build function that inserts a wrapper around the region of // the form that needs to be refreshed by AJAX (so that the AJAX system // can detect and dynamically update it). This is done in #after_build // because it's a convenient place where we have automatic access to the // complete form array, but also to minimize the chance that the HTML we // add will get clobbered by code that runs after we have added it. '#after_build' => [ ...$this->getElementInfo()->getInfoProperty('submit', '#after_build', []), [static::class, 'addAjaxWrapper'], ], ]; // Copy #weight and #access from the triggering element to the button so // that the two elements will be displayed together. foreach (['#weight', '#access'] as $property) { if (isset($triggeringElement[$property])) { $wrappingElement[$buttonKey][$property] = $triggeringElement[$property]; } } // For the easiest integration with the form API and the testing framework, // we always give the button a unique #value, rather than playing around // with #name. We also cast the #title to string as we will use it as an // array key, and it may be a TranslatableMarkup. $buttonTitle = !empty($triggeringElement['#title']) ? (string) $triggeringElement['#title'] : $triggerKey; if (empty(static::$ajaxTriggerButtons[$buttonTitle])) { $wrappingElement[$buttonKey]['#value'] = $this->t('Update "@title" choice', [ '@title' => $buttonTitle, ]); static::$ajaxTriggerButtons[$buttonTitle] = 1; } else { $wrappingElement[$buttonKey]['#value'] = $this->t('Update "@title" choice (@number)', [ '@title' => $buttonTitle, '@number' => ++static::$ajaxTriggerButtons[$buttonTitle], ]); } // Attach custom data to the triggering element and submit button, so we can // use it in both the process function and AJAX callback. $ajaxData = [ 'wrapper' => $triggeringElement['#ajax']['wrapper'], 'trigger_key' => $triggerKey, 'refresh_parents' => $refreshParents, ]; $triggeringElement['#views_ui_ajax_data'] = $ajaxData; $wrappingElement[$buttonKey]['#views_ui_ajax_data'] = $ajaxData; } /** * Limits validation errors for a non-JavaScript fallback submit button. * * @param array $element * The form element render array. * @param \Drupal\Core\Form\FormStateInterface $formState * The current state of the form. * * @return array * Render array. */ public static function addLimitedValidation(array $element, FormStateInterface $formState): array { // Retrieve the AJAX triggering element so we can determine its parents. (We // know it's at the same level of the complete form array as the // submit-button, so all we have to do to find it is swap out the // submit-button's last array parent.) $arrayParents = $element['#array_parents']; array_pop($arrayParents); $arrayParents[] = $element['#views_ui_ajax_data']['trigger_key']; $ajaxTriggeringElement = NestedArray::getValue($formState->getCompleteForm(), $arrayParents); // Limit this button's validation to the AJAX triggering element, so it can // update the form for that change without requiring that the rest of the // form be already filled out properly. $element['#limit_validation_errors'] = [$ajaxTriggeringElement['#parents']]; // If we are in the process of a form submission and this is the button that // was clicked, the form API workflow in // \Drupal::formBuilder()->doBuildForm() will have already copied it to // $formState->getTriggeringElement() before our #process function is run. // So we need to make the same modifications in $formState as we did to the // element itself to ensure that #limit_validation_errors will actually be // set in the correct place. $clickedButton = &$formState->getTriggeringElement(); if ($clickedButton && $clickedButton['#name'] == $element['#name'] && $clickedButton['#value'] == $element['#value']) { $clickedButton['#limit_validation_errors'] = $element['#limit_validation_errors']; } return $element; } /** * Adds a wrapper to a form region (for AJAX refreshes) after the build. * * This function inserts a wrapper around the region of the form that needs to * be refreshed by AJAX, based on information stored in the corresponding * submit-button form element. * * @param array $element * The form element render array. * @param \Drupal\Core\Form\FormStateInterface $formState * The current state of the form. * * @return array * Render array. */ public static function addAjaxWrapper(array $element, FormStateInterface $formState): array { // Find the region of the complete form that needs to be refreshed by AJAX. // This was earlier stored in a property on the element. $completeForm = &$formState->getCompleteForm(); $refreshParents = $element['#views_ui_ajax_data']['refresh_parents']; $refreshElement = NestedArray::getValue($completeForm, $refreshParents); // The HTML ID that AJAX expects was also stored in a property on the // element, so use that information to insert the wrapper <div> here. $id = $element['#views_ui_ajax_data']['wrapper']; $refreshElement += [ '#prefix' => '', '#suffix' => '', ]; $refreshElement['#prefix'] = '<div id="' . $id . '" class="views-ui-ajax-wrapper">' . $refreshElement['#prefix']; $refreshElement['#suffix'] .= '</div>'; // Copy the element that needs to be refreshed back into the form, with our // modifications to it. NestedArray::setValue($completeForm, $refreshParents, $refreshElement); return $element; } /** * Provides a triggering element Ajax callback. * * @param array $form * The form render array. * @param \Drupal\Core\Form\FormStateInterface $formState * The current state of the form. * * @return array * Render array. */ public function ajaxUpdateForm(array $form, FormStateInterface $formState): array { // The region that needs to be updated was stored in a property of the // triggering element by self::addAjaxTrigger(), so all we have to do is // retrieve that here. // @see \Drupal\views\ViewsFormAjaxHelperTrait::addAjaxTrigger() return NestedArray::getValue($form, $formState->getTriggeringElement()['#views_ui_ajax_data']['refresh_parents']); } /** * Provides a callback for non-JavaScript submit. * * @param array $form * The form render array. * @param \Drupal\Core\Form\FormStateInterface $formState * The current state of the form. */ public static function noJsSubmit(array $form, FormStateInterface $formState): void { $formState->setRebuild(); } /** * Returns the element info plugin manager. * * @return \Drupal\Core\Render\ElementInfoManagerInterface * The element info plugin manager. */ protected function getElementInfo(): ElementInfoManagerInterface { return \Drupal::service('plugin.manager.element_info'); } } core/modules/views/src/ViewsFormHelperTrait.php 0 → 100644 +167 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\views; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Url; /** * Provides reusable code to be shared by Views forms. */ trait ViewsFormHelperTrait { use StringTranslationTrait; /** * Adds an element to select either the default or the current display. * * @param array $form * The form render array to be altered. * @param \Drupal\Core\Form\FormStateInterface $formState * The current state of the form. * @param string $section * The section to which the display dropdown belongs. */ protected function standardDisplayDropdown(array &$form, FormStateInterface $formState, string $section): void { $view = $formState->get('view'); $displayId = $formState->get('display_id'); $executable = $view->getExecutable(); $displays = $executable->displayHandlers; $currentDisplay = $executable->display_handler; // @todo Move this to a separate function if it's needed on any forms that // don't have the display dropdown. $form['override'] = [ '#prefix' => '<div class="views-override clearfix form--inline views-offset-top" data-drupal-views-offset="top">', '#suffix' => '</div>', '#weight' => -1000, '#tree' => TRUE, ]; // Add the "2 of 3" progress indicator. if ($formProgress = $view->getFormProgress()) { $arguments = $form['#title']->getArguments() + [ '@current' => $formProgress['current'], '@total' => $formProgress['total'], ]; $form['#title'] = $this->t('Configure @type @current of @total: @item', $arguments); } // The dropdown should not be added when any of the following is true: // - this is the default display. // - there is no default shown and just one additional display (mostly // page), and the current display is defaulted. if ( $currentDisplay->isDefaultDisplay() || ( $currentDisplay->isDefaulted($section) && !$this->getConfigFactory()->get('views.settings')->get('ui.show.default_display') && count($displays) <= 2 ) ) { return; } // Determine whether any other displays have overrides for this section. $sectionOverrides = FALSE; $sectionDefaulted = $currentDisplay->isDefaulted($section); foreach ($displays as $id => $display) { if ($id === 'default' || $id === $displayId) { continue; } if ($display && !$display->isDefaulted($section)) { $sectionOverrides = TRUE; } } $displayDropdown['default'] = ($sectionOverrides ? $this->t('All displays (except overridden)') : $this->t('All displays')); $displayDropdown[$displayId] = $this->t('This @display_type (override)', ['@display_type' => $currentDisplay->getPluginId()]); // Only display the revert option if we are in an overridden section. if (!$sectionDefaulted) { $displayDropdown['default_revert'] = $this->t('Revert to default'); } $form['override']['dropdown'] = [ '#type' => 'select', // @todo Translators may need more context than this. '#title' => $this->t('For'), '#options' => $displayDropdown, ]; if ($currentDisplay->isDefaulted($section)) { $form['override']['dropdown']['#default_value'] = 'defaults'; } else { $form['override']['dropdown']['#default_value'] = $displayId; } } /** * Creates the menu path for a standard AJAX form given the form state. * * @return \Drupal\Core\Url * The URL object pointing to the form URL. */ protected function buildFormUrl(FormStateInterface $formState): Url { $ajax = !$formState->get('ajax') ? 'nojs' : 'ajax'; $name = $formState->get('view')->id(); $formKey = $formState->get('form_key'); $displayId = $formState->get('display_id'); $formKey = str_replace('-', '_', $formKey); $routeName = "views_ui.form_$formKey"; $routeParameters = [ 'js' => $ajax, 'view' => $name, 'display_id' => $displayId, ]; $url = Url::fromRoute($routeName, $routeParameters); if ($type = $formState->get('type')) { $url->setRouteParameter('type', $type); } if ($id = $formState->get('id')) { $url->setRouteParameter('id', $id); } return $url; } /** * The #process callback for a button. * * Determines if a button is the form's triggering element. * * The Form API has logic to determine the form's triggering element based on * the data in POST. However, it only checks buttons based on a single #value * per button. This function may be added to a button's #process callbacks to * extend button click detection to support multiple #values per button. If * the data in POST matches any value in the button's #values array, then the * button is detected as having been clicked. This can be used when the value * (label) of the same logical button may be different based on context (e.g., * "Apply" vs. "Apply and continue"). * * @see \Drupal\Core\Form\FormBuilder::handleInputElement() * @see \Drupal\Core\Form\FormBuilder::buttonWasClicked() */ public static function formButtonWasClicked(array $element, FormStateInterface $formState): array { $userInput = $formState->getUserInput(); $processInput = empty($element['#disabled']) && ($formState->isProgrammed() || ($formState->isProcessingInput() && (!isset($element['#access']) || $element['#access']))); if ($processInput && !$formState->getTriggeringElement() && !empty($element['#is_button']) && isset($userInput[$element['#name']]) && isset($element['#values']) && in_array($userInput[$element['#name']], array_map('strval', $element['#values']), TRUE)) { $formState->setTriggeringElement($element); } return $element; } /** * Returns the config factory service. * * @return \Drupal\Core\Config\ConfigFactoryInterface * The config factory service. */ protected function getConfigFactory(): ConfigFactoryInterface { return \Drupal::service('config.factory'); } } Loading
core/modules/views/src/Plugin/views/display/DisplayPluginBase.php +3 −1 Original line number Diff line number Diff line Loading @@ -21,12 +21,14 @@ use Drupal\views\ViewExecutable; use Drupal\views\Plugin\views\PluginBase; use Drupal\views\Views; use Drupal\views\ViewsFormHelperTrait; /** * Base class for views display plugins. */ abstract class DisplayPluginBase extends PluginBase implements DisplayPluginInterface, DependentPluginInterface { use PluginDependencyTrait; use ViewsFormHelperTrait; /** * The top object of a view. Loading Loading @@ -1384,7 +1386,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { parent::buildOptionsForm($form, $form_state); $section = $form_state->get('section'); if ($this->defaultableSections($section)) { views_ui_standard_display_dropdown($form, $form_state, $section); $this->standardDisplayDropdown($form, $form_state, $section); } $form['#title'] = $this->display['display_title'] . ': '; Loading
core/modules/views/src/Plugin/views/field/EntityField.php +3 −1 Original line number Diff line number Diff line Loading @@ -30,6 +30,7 @@ use Drupal\views\Plugin\DependentWithRemovalPluginInterface; use Drupal\views\ResultRow; use Drupal\views\ViewExecutable; use Drupal\views\ViewsFormHelperTrait; use Symfony\Component\DependencyInjection\Attribute\Autowire; /** Loading @@ -42,6 +43,7 @@ class EntityField extends FieldPluginBase implements CacheableDependencyInterfac use FieldAPIHandlerTrait; use PluginDependencyTrait; use ViewsFormHelperTrait; /** * An array to store field renderable arrays for use by renderItems(). Loading Loading @@ -506,7 +508,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { '#options' => $formatters, '#default_value' => $this->options['type'], '#ajax' => [ 'url' => views_ui_build_form_url($form_state), 'url' => $this->buildFormUrl($form_state), ], '#submit' => [[$this, 'submitTemporaryForm']], '#executes_submit_callback' => TRUE, Loading
core/modules/views/src/Plugin/views/wizard/WizardPluginBase.php +7 −7 Original line number Diff line number Diff line Loading @@ -10,6 +10,7 @@ use Drupal\Core\Url; use Drupal\views\Entity\View; use Drupal\views\Views; use Drupal\views\ViewsFormAjaxHelperTrait; use Drupal\views_ui\ViewUI; use Drupal\views\Plugin\views\display\DisplayPluginBase; use Drupal\views\Plugin\views\PluginBase; Loading @@ -36,6 +37,8 @@ */ abstract class WizardPluginBase extends PluginBase implements WizardInterface { use ViewsFormAjaxHelperTrait; /** * The base table connected with the wizard. * Loading Loading @@ -285,7 +288,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { $style_form['style_plugin']['#default_value'] = static::getSelected($form_state, ['page', 'style', 'style_plugin'], 'default', $style_form['style_plugin']); // Changing this dropdown updates $form['displays']['page']['options'] via // AJAX. views_ui_add_ajax_trigger($style_form, 'style_plugin', ['displays', 'page', 'options']); $this->addAjaxTrigger($style_form, 'style_plugin', ['displays', 'page', 'options']); $this->buildFormStyle($form, $form_state, 'page'); $form['displays']['page']['options']['items_per_page'] = [ Loading Loading @@ -419,7 +422,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { ], 'default', $style_form['style_plugin']); // Changing this dropdown updates $form['displays']['block']['options'] // via AJAX. views_ui_add_ajax_trigger($style_form, 'style_plugin', ['displays', 'block', 'options']); $this->addAjaxTrigger($style_form, 'style_plugin', ['displays', 'block', 'options']); $this->buildFormStyle($form, $form_state, 'block'); $form['displays']['block']['options']['items_per_page'] = [ Loading Loading @@ -589,7 +592,7 @@ protected function buildFormStyle(array &$form, FormStateInterface $form_state, $default_value = $block_with_linked_titles_available ? 'titles_linked' : key($options); $style_form['row_plugin']['#default_value'] = static::getSelected($form_state, [$type, 'style', 'row_plugin'], $default_value, $style_form['row_plugin']); // Changing this dropdown updates the individual row options via AJAX. views_ui_add_ajax_trigger($style_form, 'row_plugin', ['displays', $type, 'options', 'style', 'row_options']); $this->addAjaxTrigger($style_form, 'row_plugin', ['displays', $type, 'options', 'style', 'row_options']); // This is the region that can be updated by AJAX. The base class doesn't // add anything here, but child classes can. Loading Loading @@ -621,8 +624,6 @@ protected function rowStyleOptions() { * available). */ protected function buildFilters(&$form, FormStateInterface $form_state) { \Drupal::moduleHandler()->loadInclude('views_ui', 'inc', 'admin'); $bundles = isset($this->entityTypeId) ? $this->bundleInfoService->getBundleInfo($this->entityTypeId) : []; // If the current base table support bundles and has more than one (like // user). Loading @@ -642,7 +643,7 @@ protected function buildFilters(&$form, FormStateInterface $form_state) { // Changing this dropdown updates the entire content of $form['displays'] // via AJAX, since each bundle might have entirely different fields // attached to it, etc. views_ui_add_ajax_trigger($form['displays']['show'], 'type', ['displays']); $this->addAjaxTrigger($form['displays']['show'], 'type', ['displays']); } } Loading Loading @@ -927,7 +928,6 @@ protected function defaultDisplayFiltersUser(array $form, FormStateInterface $fo // Figure out the table where $bundle_key lives. It may not be the same as // the base table for the view; the taxonomy vocabulary machine_name, for // example, is stored in taxonomy_vocabulary, not taxonomy_term_data. \Drupal::moduleHandler()->loadInclude('views_ui', 'inc', 'admin'); $fields = Views::viewsDataHelper()->fetchFields($this->base_table, 'filter'); $table = FALSE; if (isset($fields[$this->base_table . '.' . $bundle_key])) { Loading
core/modules/views/src/ViewsFormAjaxHelperTrait.php 0 → 100644 +255 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\views; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; /** * Provides reusable code to be shared by Views Ajax forms. */ trait ViewsFormAjaxHelperTrait { use StringTranslationTrait; /** * Memory cache for processed buttons. * * @var array<string, int> */ protected static array $ajaxTriggerButtons = []; /** * Converts a form element in the new view wizard to be AJAX-enabled. * * This function takes a form element and adds AJAX behaviors to it such that * changing it triggers another part of the form to update automatically. It * also adds a submit-button to the form that appears next to the triggering * element, and that duplicates its functionality for users who do not have * JavaScript enabled (the button is automatically hidden for users who do * have JavaScript). * * To use this function, call it directly from your form builder function * immediately after you have defined the form element that will serve as the * JavaScript trigger. Calling it elsewhere (such as in hook_form_alter()) may * mean that the non-JavaScript fallback button does not appear in the correct * place in the form. * * @param array $wrappingElement * The element whose child will server as the AJAX trigger. For example, if * $form['some_wrapper']['triggering_element'] represents the element which * will trigger the AJAX behavior, you would pass $form['some_wrapper'] for * this parameter. * @param string $triggerKey * The key within the wrapping element that identifies which of its children * serves as the AJAX trigger. In the above example, you would pass * 'triggering_element' for this parameter. * @param string[] $refreshParents * An array of parent keys that point to the part of the form that AJAX will * refresh. For example, if triggering the AJAX behavior should cause * $form['dynamic_content']['section'] to be refreshed, you would pass * ['dynamic_content', 'section'] for this parameter. */ protected function addAjaxTrigger(array &$wrappingElement, string $triggerKey, array $refreshParents): void { // Add the AJAX behavior to the triggering element. $triggeringElement = &$wrappingElement[$triggerKey]; $triggeringElement['#ajax']['callback'] = static::class . ':ajaxUpdateForm'; // We do not use \Drupal\Component\Utility\Html::getUniqueId() to get an ID // for the AJAX wrapper. It remembers IDs across AJAX requests (and won't // reuse them). But in our case we need to use the same ID from request to // request so that the wrapper can be recognized by the AJAX system and its // content can be dynamically updated. So instead, we will keep track of // duplicate IDs (within a single request) on our own, later in this method. $triggeringElement['#ajax']['wrapper'] = 'edit-view-' . implode('-', $refreshParents) . '-wrapper'; // Add a submit-button for users who do not have JavaScript enabled. It // should be displayed next to the triggering element on the form. $buttonKey = $triggerKey . '_trigger_update'; $wrappingElement[$buttonKey] = [ '#type' => 'submit', // Hide this button when JavaScript is enabled. '#attributes' => ['class' => ['js-hide']], '#submit' => [[static::class, 'noJsSubmit']], // Add a process function to limit this button's validation errors to the // triggering element only. We have to do this in #process since until the // form API has added the #parents property to the triggering element for // us, we don't have any (easy) way to find out where its submitted values // will eventually appear in FormStateInterface->getValues(). '#process' => [ [static::class, 'addLimitedValidation'], ...$this->getElementInfo()->getInfoProperty('submit', '#process', []), ], // Add an after-build function that inserts a wrapper around the region of // the form that needs to be refreshed by AJAX (so that the AJAX system // can detect and dynamically update it). This is done in #after_build // because it's a convenient place where we have automatic access to the // complete form array, but also to minimize the chance that the HTML we // add will get clobbered by code that runs after we have added it. '#after_build' => [ ...$this->getElementInfo()->getInfoProperty('submit', '#after_build', []), [static::class, 'addAjaxWrapper'], ], ]; // Copy #weight and #access from the triggering element to the button so // that the two elements will be displayed together. foreach (['#weight', '#access'] as $property) { if (isset($triggeringElement[$property])) { $wrappingElement[$buttonKey][$property] = $triggeringElement[$property]; } } // For the easiest integration with the form API and the testing framework, // we always give the button a unique #value, rather than playing around // with #name. We also cast the #title to string as we will use it as an // array key, and it may be a TranslatableMarkup. $buttonTitle = !empty($triggeringElement['#title']) ? (string) $triggeringElement['#title'] : $triggerKey; if (empty(static::$ajaxTriggerButtons[$buttonTitle])) { $wrappingElement[$buttonKey]['#value'] = $this->t('Update "@title" choice', [ '@title' => $buttonTitle, ]); static::$ajaxTriggerButtons[$buttonTitle] = 1; } else { $wrappingElement[$buttonKey]['#value'] = $this->t('Update "@title" choice (@number)', [ '@title' => $buttonTitle, '@number' => ++static::$ajaxTriggerButtons[$buttonTitle], ]); } // Attach custom data to the triggering element and submit button, so we can // use it in both the process function and AJAX callback. $ajaxData = [ 'wrapper' => $triggeringElement['#ajax']['wrapper'], 'trigger_key' => $triggerKey, 'refresh_parents' => $refreshParents, ]; $triggeringElement['#views_ui_ajax_data'] = $ajaxData; $wrappingElement[$buttonKey]['#views_ui_ajax_data'] = $ajaxData; } /** * Limits validation errors for a non-JavaScript fallback submit button. * * @param array $element * The form element render array. * @param \Drupal\Core\Form\FormStateInterface $formState * The current state of the form. * * @return array * Render array. */ public static function addLimitedValidation(array $element, FormStateInterface $formState): array { // Retrieve the AJAX triggering element so we can determine its parents. (We // know it's at the same level of the complete form array as the // submit-button, so all we have to do to find it is swap out the // submit-button's last array parent.) $arrayParents = $element['#array_parents']; array_pop($arrayParents); $arrayParents[] = $element['#views_ui_ajax_data']['trigger_key']; $ajaxTriggeringElement = NestedArray::getValue($formState->getCompleteForm(), $arrayParents); // Limit this button's validation to the AJAX triggering element, so it can // update the form for that change without requiring that the rest of the // form be already filled out properly. $element['#limit_validation_errors'] = [$ajaxTriggeringElement['#parents']]; // If we are in the process of a form submission and this is the button that // was clicked, the form API workflow in // \Drupal::formBuilder()->doBuildForm() will have already copied it to // $formState->getTriggeringElement() before our #process function is run. // So we need to make the same modifications in $formState as we did to the // element itself to ensure that #limit_validation_errors will actually be // set in the correct place. $clickedButton = &$formState->getTriggeringElement(); if ($clickedButton && $clickedButton['#name'] == $element['#name'] && $clickedButton['#value'] == $element['#value']) { $clickedButton['#limit_validation_errors'] = $element['#limit_validation_errors']; } return $element; } /** * Adds a wrapper to a form region (for AJAX refreshes) after the build. * * This function inserts a wrapper around the region of the form that needs to * be refreshed by AJAX, based on information stored in the corresponding * submit-button form element. * * @param array $element * The form element render array. * @param \Drupal\Core\Form\FormStateInterface $formState * The current state of the form. * * @return array * Render array. */ public static function addAjaxWrapper(array $element, FormStateInterface $formState): array { // Find the region of the complete form that needs to be refreshed by AJAX. // This was earlier stored in a property on the element. $completeForm = &$formState->getCompleteForm(); $refreshParents = $element['#views_ui_ajax_data']['refresh_parents']; $refreshElement = NestedArray::getValue($completeForm, $refreshParents); // The HTML ID that AJAX expects was also stored in a property on the // element, so use that information to insert the wrapper <div> here. $id = $element['#views_ui_ajax_data']['wrapper']; $refreshElement += [ '#prefix' => '', '#suffix' => '', ]; $refreshElement['#prefix'] = '<div id="' . $id . '" class="views-ui-ajax-wrapper">' . $refreshElement['#prefix']; $refreshElement['#suffix'] .= '</div>'; // Copy the element that needs to be refreshed back into the form, with our // modifications to it. NestedArray::setValue($completeForm, $refreshParents, $refreshElement); return $element; } /** * Provides a triggering element Ajax callback. * * @param array $form * The form render array. * @param \Drupal\Core\Form\FormStateInterface $formState * The current state of the form. * * @return array * Render array. */ public function ajaxUpdateForm(array $form, FormStateInterface $formState): array { // The region that needs to be updated was stored in a property of the // triggering element by self::addAjaxTrigger(), so all we have to do is // retrieve that here. // @see \Drupal\views\ViewsFormAjaxHelperTrait::addAjaxTrigger() return NestedArray::getValue($form, $formState->getTriggeringElement()['#views_ui_ajax_data']['refresh_parents']); } /** * Provides a callback for non-JavaScript submit. * * @param array $form * The form render array. * @param \Drupal\Core\Form\FormStateInterface $formState * The current state of the form. */ public static function noJsSubmit(array $form, FormStateInterface $formState): void { $formState->setRebuild(); } /** * Returns the element info plugin manager. * * @return \Drupal\Core\Render\ElementInfoManagerInterface * The element info plugin manager. */ protected function getElementInfo(): ElementInfoManagerInterface { return \Drupal::service('plugin.manager.element_info'); } }
core/modules/views/src/ViewsFormHelperTrait.php 0 → 100644 +167 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\views; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Url; /** * Provides reusable code to be shared by Views forms. */ trait ViewsFormHelperTrait { use StringTranslationTrait; /** * Adds an element to select either the default or the current display. * * @param array $form * The form render array to be altered. * @param \Drupal\Core\Form\FormStateInterface $formState * The current state of the form. * @param string $section * The section to which the display dropdown belongs. */ protected function standardDisplayDropdown(array &$form, FormStateInterface $formState, string $section): void { $view = $formState->get('view'); $displayId = $formState->get('display_id'); $executable = $view->getExecutable(); $displays = $executable->displayHandlers; $currentDisplay = $executable->display_handler; // @todo Move this to a separate function if it's needed on any forms that // don't have the display dropdown. $form['override'] = [ '#prefix' => '<div class="views-override clearfix form--inline views-offset-top" data-drupal-views-offset="top">', '#suffix' => '</div>', '#weight' => -1000, '#tree' => TRUE, ]; // Add the "2 of 3" progress indicator. if ($formProgress = $view->getFormProgress()) { $arguments = $form['#title']->getArguments() + [ '@current' => $formProgress['current'], '@total' => $formProgress['total'], ]; $form['#title'] = $this->t('Configure @type @current of @total: @item', $arguments); } // The dropdown should not be added when any of the following is true: // - this is the default display. // - there is no default shown and just one additional display (mostly // page), and the current display is defaulted. if ( $currentDisplay->isDefaultDisplay() || ( $currentDisplay->isDefaulted($section) && !$this->getConfigFactory()->get('views.settings')->get('ui.show.default_display') && count($displays) <= 2 ) ) { return; } // Determine whether any other displays have overrides for this section. $sectionOverrides = FALSE; $sectionDefaulted = $currentDisplay->isDefaulted($section); foreach ($displays as $id => $display) { if ($id === 'default' || $id === $displayId) { continue; } if ($display && !$display->isDefaulted($section)) { $sectionOverrides = TRUE; } } $displayDropdown['default'] = ($sectionOverrides ? $this->t('All displays (except overridden)') : $this->t('All displays')); $displayDropdown[$displayId] = $this->t('This @display_type (override)', ['@display_type' => $currentDisplay->getPluginId()]); // Only display the revert option if we are in an overridden section. if (!$sectionDefaulted) { $displayDropdown['default_revert'] = $this->t('Revert to default'); } $form['override']['dropdown'] = [ '#type' => 'select', // @todo Translators may need more context than this. '#title' => $this->t('For'), '#options' => $displayDropdown, ]; if ($currentDisplay->isDefaulted($section)) { $form['override']['dropdown']['#default_value'] = 'defaults'; } else { $form['override']['dropdown']['#default_value'] = $displayId; } } /** * Creates the menu path for a standard AJAX form given the form state. * * @return \Drupal\Core\Url * The URL object pointing to the form URL. */ protected function buildFormUrl(FormStateInterface $formState): Url { $ajax = !$formState->get('ajax') ? 'nojs' : 'ajax'; $name = $formState->get('view')->id(); $formKey = $formState->get('form_key'); $displayId = $formState->get('display_id'); $formKey = str_replace('-', '_', $formKey); $routeName = "views_ui.form_$formKey"; $routeParameters = [ 'js' => $ajax, 'view' => $name, 'display_id' => $displayId, ]; $url = Url::fromRoute($routeName, $routeParameters); if ($type = $formState->get('type')) { $url->setRouteParameter('type', $type); } if ($id = $formState->get('id')) { $url->setRouteParameter('id', $id); } return $url; } /** * The #process callback for a button. * * Determines if a button is the form's triggering element. * * The Form API has logic to determine the form's triggering element based on * the data in POST. However, it only checks buttons based on a single #value * per button. This function may be added to a button's #process callbacks to * extend button click detection to support multiple #values per button. If * the data in POST matches any value in the button's #values array, then the * button is detected as having been clicked. This can be used when the value * (label) of the same logical button may be different based on context (e.g., * "Apply" vs. "Apply and continue"). * * @see \Drupal\Core\Form\FormBuilder::handleInputElement() * @see \Drupal\Core\Form\FormBuilder::buttonWasClicked() */ public static function formButtonWasClicked(array $element, FormStateInterface $formState): array { $userInput = $formState->getUserInput(); $processInput = empty($element['#disabled']) && ($formState->isProgrammed() || ($formState->isProcessingInput() && (!isset($element['#access']) || $element['#access']))); if ($processInput && !$formState->getTriggeringElement() && !empty($element['#is_button']) && isset($userInput[$element['#name']]) && isset($element['#values']) && in_array($userInput[$element['#name']], array_map('strval', $element['#values']), TRUE)) { $formState->setTriggeringElement($element); } return $element; } /** * Returns the config factory service. * * @return \Drupal\Core\Config\ConfigFactoryInterface * The config factory service. */ protected function getConfigFactory(): ConfigFactoryInterface { return \Drupal::service('config.factory'); } }