admin.inc 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. <?php
  2. /**
  3. * @file
  4. * Provides the Views' administrative interface.
  5. */
  6. use Drupal\Component\Utility\NestedArray;
  7. use Drupal\Core\Form\FormStateInterface;
  8. use Drupal\Core\Url;
  9. /**
  10. * Converts a form element in the add view wizard to be AJAX-enabled.
  11. *
  12. * This function takes a form element and adds AJAX behaviors to it such that
  13. * changing it triggers another part of the form to update automatically. It
  14. * also adds a submit button to the form that appears next to the triggering
  15. * element and that duplicates its functionality for users who do not have
  16. * JavaScript enabled (the button is automatically hidden for users who do have
  17. * JavaScript).
  18. *
  19. * To use this function, call it directly from your form builder function
  20. * immediately after you have defined the form element that will serve as the
  21. * JavaScript trigger. Calling it elsewhere (such as in hook_form_alter()) may
  22. * mean that the non-JavaScript fallback button does not appear in the correct
  23. * place in the form.
  24. *
  25. * @param $wrapping_element
  26. * The element whose child will server as the AJAX trigger. For example, if
  27. * $form['some_wrapper']['triggering_element'] represents the element which
  28. * will trigger the AJAX behavior, you would pass $form['some_wrapper'] for
  29. * this parameter.
  30. * @param $trigger_key
  31. * The key within the wrapping element that identifies which of its children
  32. * serves as the AJAX trigger. In the above example, you would pass
  33. * 'triggering_element' for this parameter.
  34. * @param $refresh_parents
  35. * An array of parent keys that point to the part of the form that will be
  36. * refreshed by AJAX. For example, if triggering the AJAX behavior should
  37. * cause $form['dynamic_content']['section'] to be refreshed, you would pass
  38. * array('dynamic_content', 'section') for this parameter.
  39. */
  40. function views_ui_add_ajax_trigger(&$wrapping_element, $trigger_key, $refresh_parents) {
  41. $seen_ids = &drupal_static(__FUNCTION__ . ':seen_ids', []);
  42. $seen_buttons = &drupal_static(__FUNCTION__ . ':seen_buttons', []);
  43. // Add the AJAX behavior to the triggering element.
  44. $triggering_element = &$wrapping_element[$trigger_key];
  45. $triggering_element['#ajax']['callback'] = 'views_ui_ajax_update_form';
  46. // We do not use \Drupal\Component\Utility\Html::getUniqueId() to get an ID
  47. // for the AJAX wrapper, because it remembers IDs across AJAX requests (and
  48. // won't reuse them), but in our case we need to use the same ID from request
  49. // to request so that the wrapper can be recognized by the AJAX system and
  50. // its content can be dynamically updated. So instead, we will keep track of
  51. // duplicate IDs (within a single request) on our own, later in this function.
  52. $triggering_element['#ajax']['wrapper'] = 'edit-view-' . implode('-', $refresh_parents) . '-wrapper';
  53. // Add a submit button for users who do not have JavaScript enabled. It
  54. // should be displayed next to the triggering element on the form.
  55. $button_key = $trigger_key . '_trigger_update';
  56. $element_info = \Drupal::service('element_info');
  57. $wrapping_element[$button_key] = [
  58. '#type' => 'submit',
  59. // Hide this button when JavaScript is enabled.
  60. '#attributes' => ['class' => ['js-hide']],
  61. '#submit' => ['views_ui_nojs_submit'],
  62. // Add a process function to limit this button's validation errors to the
  63. // triggering element only. We have to do this in #process since until the
  64. // form API has added the #parents property to the triggering element for
  65. // us, we don't have any (easy) way to find out where its submitted values
  66. // will eventually appear in $form_state->getValues().
  67. '#process' => array_merge(['views_ui_add_limited_validation'], $element_info->getInfoProperty('submit', '#process', [])),
  68. // Add an after-build function that inserts a wrapper around the region of
  69. // the form that needs to be refreshed by AJAX (so that the AJAX system can
  70. // detect and dynamically update it). This is done in #after_build because
  71. // it's a convenient place where we have automatic access to the complete
  72. // form array, but also to minimize the chance that the HTML we add will
  73. // get clobbered by code that runs after we have added it.
  74. '#after_build' => array_merge($element_info->getInfoProperty('submit', '#after_build', []), ['views_ui_add_ajax_wrapper']),
  75. ];
  76. // Copy #weight and #access from the triggering element to the button, so
  77. // that the two elements will be displayed together.
  78. foreach (['#weight', '#access'] as $property) {
  79. if (isset($triggering_element[$property])) {
  80. $wrapping_element[$button_key][$property] = $triggering_element[$property];
  81. }
  82. }
  83. // For easiest integration with the form API and the testing framework, we
  84. // always give the button a unique #value, rather than playing around with
  85. // #name. We also cast the #title to string as we will use it as an array
  86. // key and it may be a TranslatableMarkup.
  87. $button_title = !empty($triggering_element['#title']) ? (string) $triggering_element['#title'] : $trigger_key;
  88. if (empty($seen_buttons[$button_title])) {
  89. $wrapping_element[$button_key]['#value'] = t('Update "@title" choice', [
  90. '@title' => $button_title,
  91. ]);
  92. $seen_buttons[$button_title] = 1;
  93. }
  94. else {
  95. $wrapping_element[$button_key]['#value'] = t('Update "@title" choice (@number)', [
  96. '@title' => $button_title,
  97. '@number' => ++$seen_buttons[$button_title],
  98. ]);
  99. }
  100. // Attach custom data to the triggering element and submit button, so we can
  101. // use it in both the process function and AJAX callback.
  102. $ajax_data = [
  103. 'wrapper' => $triggering_element['#ajax']['wrapper'],
  104. 'trigger_key' => $trigger_key,
  105. 'refresh_parents' => $refresh_parents,
  106. ];
  107. $seen_ids[$triggering_element['#ajax']['wrapper']] = TRUE;
  108. $triggering_element['#views_ui_ajax_data'] = $ajax_data;
  109. $wrapping_element[$button_key]['#views_ui_ajax_data'] = $ajax_data;
  110. }
  111. /**
  112. * Processes a non-JavaScript fallback submit button to limit its validation errors.
  113. */
  114. function views_ui_add_limited_validation($element, FormStateInterface $form_state) {
  115. // Retrieve the AJAX triggering element so we can determine its parents. (We
  116. // know it's at the same level of the complete form array as the submit
  117. // button, so all we have to do to find it is swap out the submit button's
  118. // last array parent.)
  119. $array_parents = $element['#array_parents'];
  120. array_pop($array_parents);
  121. $array_parents[] = $element['#views_ui_ajax_data']['trigger_key'];
  122. $ajax_triggering_element = NestedArray::getValue($form_state->getCompleteForm(), $array_parents);
  123. // Limit this button's validation to the AJAX triggering element, so it can
  124. // update the form for that change without requiring that the rest of the
  125. // form be filled out properly yet.
  126. $element['#limit_validation_errors'] = [$ajax_triggering_element['#parents']];
  127. // If we are in the process of a form submission and this is the button that
  128. // was clicked, the form API workflow in \Drupal::formBuilder()->doBuildForm()
  129. // will have already copied it to $form_state->getTriggeringElement() before
  130. // our #process function is run. So we need to make the same modifications in
  131. // $form_state as we did to the element itself, to ensure that
  132. // #limit_validation_errors will actually be set in the correct place.
  133. $clicked_button = &$form_state->getTriggeringElement();
  134. if ($clicked_button && $clicked_button['#name'] == $element['#name'] && $clicked_button['#value'] == $element['#value']) {
  135. $clicked_button['#limit_validation_errors'] = $element['#limit_validation_errors'];
  136. }
  137. return $element;
  138. }
  139. /**
  140. * After-build function that adds a wrapper to a form region (for AJAX refreshes).
  141. *
  142. * This function inserts a wrapper around the region of the form that needs to
  143. * be refreshed by AJAX, based on information stored in the corresponding
  144. * submit button form element.
  145. */
  146. function views_ui_add_ajax_wrapper($element, FormStateInterface $form_state) {
  147. // Find the region of the complete form that needs to be refreshed by AJAX.
  148. // This was earlier stored in a property on the element.
  149. $complete_form = &$form_state->getCompleteForm();
  150. $refresh_parents = $element['#views_ui_ajax_data']['refresh_parents'];
  151. $refresh_element = NestedArray::getValue($complete_form, $refresh_parents);
  152. // The HTML ID that AJAX expects was also stored in a property on the
  153. // element, so use that information to insert the wrapper <div> here.
  154. $id = $element['#views_ui_ajax_data']['wrapper'];
  155. $refresh_element += [
  156. '#prefix' => '',
  157. '#suffix' => '',
  158. ];
  159. $refresh_element['#prefix'] = '<div id="' . $id . '" class="views-ui-ajax-wrapper">' . $refresh_element['#prefix'];
  160. $refresh_element['#suffix'] .= '</div>';
  161. // Copy the element that needs to be refreshed back into the form, with our
  162. // modifications to it.
  163. NestedArray::setValue($complete_form, $refresh_parents, $refresh_element);
  164. return $element;
  165. }
  166. /**
  167. * Updates a part of the add view form via AJAX.
  168. *
  169. * @return
  170. * The part of the form that has changed.
  171. */
  172. function views_ui_ajax_update_form($form, FormStateInterface $form_state) {
  173. // The region that needs to be updated was stored in a property of the
  174. // triggering element by views_ui_add_ajax_trigger(), so all we have to do is
  175. // retrieve that here.
  176. return NestedArray::getValue($form, $form_state->getTriggeringElement()['#views_ui_ajax_data']['refresh_parents']);
  177. }
  178. /**
  179. * Non-Javascript fallback for updating the add view form.
  180. */
  181. function views_ui_nojs_submit($form, FormStateInterface $form_state) {
  182. $form_state->setRebuild();
  183. }
  184. /**
  185. * Add a <select> dropdown for a given section, allowing the user to
  186. * change whether this info is stored on the default display or on
  187. * the current display.
  188. */
  189. function views_ui_standard_display_dropdown(&$form, FormStateInterface $form_state, $section) {
  190. $view = $form_state->get('view');
  191. $display_id = $form_state->get('display_id');
  192. $executable = $view->getExecutable();
  193. $displays = $executable->displayHandlers;
  194. $current_display = $executable->display_handler;
  195. // @todo Move this to a separate function if it's needed on any forms that
  196. // don't have the display dropdown.
  197. $form['override'] = [
  198. '#prefix' => '<div class="views-override clearfix form--inline views-offset-top" data-drupal-views-offset="top">',
  199. '#suffix' => '</div>',
  200. '#weight' => -1000,
  201. '#tree' => TRUE,
  202. ];
  203. // Add the "2 of 3" progress indicator.
  204. if ($form_progress = $view->getFormProgress()) {
  205. $form['progress']['#markup'] = '<div id="views-progress-indicator" class="views-progress-indicator">' . t('@current of @total', ['@current' => $form_progress['current'], '@total' => $form_progress['total']]) . '</div>';
  206. $form['progress']['#weight'] = -1001;
  207. }
  208. // The dropdown should not be added when :
  209. // - this is the default display.
  210. // - there is no master shown and just one additional display (mostly page)
  211. // and the current display is defaulted.
  212. if ($current_display->isDefaultDisplay() || ($current_display->isDefaulted($section) && !\Drupal::config('views.settings')->get('ui.show.master_display') && count($displays) <= 2)) {
  213. return;
  214. }
  215. // Determine whether any other displays have overrides for this section.
  216. $section_overrides = FALSE;
  217. $section_defaulted = $current_display->isDefaulted($section);
  218. foreach ($displays as $id => $display) {
  219. if ($id === 'default' || $id === $display_id) {
  220. continue;
  221. }
  222. if ($display && !$display->isDefaulted($section)) {
  223. $section_overrides = TRUE;
  224. }
  225. }
  226. $display_dropdown['default'] = ($section_overrides ? t('All displays (except overridden)') : t('All displays'));
  227. $display_dropdown[$display_id] = t('This @display_type (override)', ['@display_type' => $current_display->getPluginId()]);
  228. // Only display the revert option if we are in a overridden section.
  229. if (!$section_defaulted) {
  230. $display_dropdown['default_revert'] = t('Revert to default');
  231. }
  232. $form['override']['dropdown'] = [
  233. '#type' => 'select',
  234. // @TODO: Translators may need more context than this.
  235. '#title' => t('For'),
  236. '#options' => $display_dropdown,
  237. ];
  238. if ($current_display->isDefaulted($section)) {
  239. $form['override']['dropdown']['#default_value'] = 'defaults';
  240. }
  241. else {
  242. $form['override']['dropdown']['#default_value'] = $display_id;
  243. }
  244. }
  245. /**
  246. * Create the menu path for one of our standard AJAX forms based upon known
  247. * information about the form.
  248. *
  249. * @return \Drupal\Core\Url
  250. * The URL object pointing to the form URL.
  251. */
  252. function views_ui_build_form_url(FormStateInterface $form_state) {
  253. $ajax = !$form_state->get('ajax') ? 'nojs' : 'ajax';
  254. $name = $form_state->get('view')->id();
  255. $form_key = $form_state->get('form_key');
  256. $display_id = $form_state->get('display_id');
  257. $form_key = str_replace('-', '_', $form_key);
  258. $route_name = "views_ui.form_{$form_key}";
  259. $route_parameters = [
  260. 'js' => $ajax,
  261. 'view' => $name,
  262. 'display_id' => $display_id,
  263. ];
  264. $url = Url::fromRoute($route_name, $route_parameters);
  265. if ($type = $form_state->get('type')) {
  266. $url->setRouteParameter('type', $type);
  267. }
  268. if ($id = $form_state->get('id')) {
  269. $url->setRouteParameter('id', $id);
  270. }
  271. return $url;
  272. }
  273. /**
  274. * #process callback for a button; determines if a button is the form's triggering element.
  275. *
  276. * The Form API has logic to determine the form's triggering element based on
  277. * the data in POST. However, it only checks buttons based on a single #value
  278. * per button. This function may be added to a button's #process callbacks to
  279. * extend button click detection to support multiple #values per button. If the
  280. * data in POST matches any value in the button's #values array, then the
  281. * button is detected as having been clicked. This can be used when the value
  282. * (label) of the same logical button may be different based on context (e.g.,
  283. * "Apply" vs. "Apply and continue").
  284. *
  285. * @see _form_builder_handle_input_element()
  286. * @see _form_button_was_clicked()
  287. */
  288. function views_ui_form_button_was_clicked($element, FormStateInterface $form_state) {
  289. $user_input = $form_state->getUserInput();
  290. $process_input = empty($element['#disabled']) && ($form_state->isProgrammed() || ($form_state->isProcessingInput() && (!isset($element['#access']) || $element['#access'])));
  291. if ($process_input && !$form_state->getTriggeringElement() && !empty($element['#is_button']) && isset($user_input[$element['#name']]) && isset($element['#values']) && in_array($user_input[$element['#name']], array_map('strval', $element['#values']), TRUE)) {
  292. $form_state->setTriggeringElement($element);
  293. }
  294. return $element;
  295. }