file BootstrapDropdown.php

Namespace

Drupal\bootstrap\Plugin\Preprocess
  1. <?php
  2. namespace Drupal\bootstrap\Plugin\Preprocess;
  3. use Drupal\bootstrap\Utility\Element;
  4. use Drupal\bootstrap\Utility\Unicode;
  5. use Drupal\bootstrap\Utility\Variables;
  6. use Drupal\Component\Utility\Html;
  7. use Drupal\Component\Utility\NestedArray;
  8. use Drupal\Core\Language\LanguageInterface;
  9. use Drupal\Core\Url;
  10. /**
  11. * Pre-processes variables for the "bootstrap_dropdown" theme hook.
  12. *
  13. * @ingroup plugins_preprocess
  14. *
  15. * @BootstrapPreprocess("bootstrap_dropdown")
  16. */
  17. class BootstrapDropdown extends PreprocessBase implements PreprocessInterface {
  18. /**
  19. * {@inheritdoc}
  20. */
  21. protected function preprocessVariables(Variables $variables) {
  22. $this->preprocessLinks($variables);
  23. $toggle = Element::create($variables->toggle);
  24. $toggle->setProperty('split', $variables->split);
  25. // Convert the items into a proper item list.
  26. $variables->items = [
  27. '#theme' => 'item_list__dropdown',
  28. '#items' => $variables->items,
  29. '#context' => [
  30. 'alignment' => $variables->alignment,
  31. ],
  32. ];
  33. // Ensure all attributes are proper objects.
  34. $this->preprocessAttributes();
  35. }
  36. /**
  37. * Preprocess links in the variables array to convert them from dropbuttons.
  38. *
  39. * @param \Drupal\bootstrap\Utility\Variables $variables
  40. * A variables object.
  41. */
  42. protected function preprocessLinks(Variables $variables) {
  43. // Convert "dropbutton" theme suggestion variables.
  44. if (Unicode::strpos($variables->theme_hook_original, 'links__dropbutton') !== FALSE && !empty($variables->links)) {
  45. $operations = !!Unicode::strpos($variables->theme_hook_original, 'operations');
  46. // Normal dropbutton links are not actually render arrays, convert them.
  47. foreach ($variables->links as &$element) {
  48. // Only process links that have "title".
  49. if (!isset($element['title'])) {
  50. continue;
  51. }
  52. // If title is an actual render array, just move it up.
  53. if (Element::isRenderArray($element['title']) && !isset($element['url'])) {
  54. $element = $element['title'];
  55. }
  56. // Otherwise, convert into an actual "link" render array element.
  57. else {
  58. if (!isset($element['url'])) {
  59. $element['url'] = Url::fromRoute('<none>');
  60. }
  61. $attributes = isset($element['attributes']) ? $element['attributes'] : [];
  62. $wrapper_attributes = isset($element['wrapper_attributes']) ? $element['wrapper_attributes'] : [];
  63. if (isset($element['language']) && $element['language'] instanceof LanguageInterface) {
  64. $attributes['hreflang'] = $element['language']->getId();
  65. $wrapper_attributes['hreflang'] = $element['language']->getId();
  66. // Ensure the Url language is set on the object itself.
  67. // @todo Revisit, possibly a core bug?
  68. // @see https://www.drupal.org/project/bootstrap/issues/2868100
  69. $element['url']->setOption('language', $element['language']);
  70. }
  71. // Preserve query parameters (if any)
  72. if (!empty($element['query'])) {
  73. $url_query = $element['url']->getOption('query') ?: [];
  74. $element['url']->setOption('query', NestedArray::mergeDeep($url_query, $element['query']));
  75. }
  76. // Build render array.
  77. $element = [
  78. '#type' => 'link',
  79. '#title' => $element['title'],
  80. '#url' => $element['url'],
  81. '#ajax' => isset($element['ajax']) ? $element['ajax'] : [],
  82. '#attributes' => $attributes,
  83. '#wrapper_attributes' => $wrapper_attributes,
  84. ];
  85. }
  86. }
  87. $items = Element::createStandalone();
  88. /** @var \Drupal\bootstrap\Utility\Element $primary_action */
  89. $primary_action = NULL;
  90. $links = Element::create($variables->links);
  91. // Iterate over all provided "links". The array may be associative, so
  92. // this cannot rely on the key to be numeric, it must be tracked manually.
  93. $i = -1;
  94. foreach ($links->children(TRUE) as $key => $child) {
  95. $i++;
  96. // Ensure validation errors are limited.
  97. if ($child->getProperty('limit_validation_errors') !== FALSE) {
  98. $child->setAttribute('formnovalidate', 'formnovalidate');
  99. }
  100. // Generate the current timestamp to use with identifiers. This helps
  101. // eliminate any render cache issues when dealing with multiple
  102. // dropdown elements on the same page, as in a listing.
  103. // @see https://www.drupal.org/project/bootstrap/issues/2939166
  104. $current_time = \Drupal::time()->getCurrentTime();
  105. // The first item is always the "primary link".
  106. if ($i === 0) {
  107. // Must generate an ID for this child because the toggle will use it.
  108. if (!$child->getAttribute('id')) {
  109. $child->setAttribute('id', $child->getProperty('id', Html::getUniqueId("dropdown-item-$current_time")));
  110. }
  111. $primary_action = $child->addClass('hidden');
  112. }
  113. // Convert into a proper link.
  114. if (!$child->isType('link')) {
  115. // Retrieve any set HTML identifier for the original element,
  116. // generating a new one if necessary. This is needed to ensure events
  117. // are bound on the original element (which may be DOM specific).
  118. // When the corresponding link below is clicked, it proxies all
  119. // events to the "dropdown-target" (the original element).
  120. $id = $child->getAttribute('id');
  121. if (!$id) {
  122. $id = $child->getProperty('id', Html::getUniqueId("dropdown-item-$current_time"));
  123. $child->setAttribute('id', $id);
  124. }
  125. // Add the original element to the item list, but hide it.
  126. $items->{$key . '_original'} = $child->addClass('hidden')->getArrayCopy();
  127. // Replace the child element with a proper link.
  128. $child = Element::createStandalone([
  129. '#type' => 'link',
  130. '#title' => $child->getProperty('value', $child->getProperty('title', $child->getProperty('text'))),
  131. '#url' => Url::fromUserInput('#'),
  132. '#attributes' => ['data-dropdown-target' => "#$id"],
  133. ]);
  134. // Also hide the real link if it's the primary action.
  135. if ($i === 0) {
  136. $child->addClass('hidden');
  137. }
  138. }
  139. // If no HTML ID was found, automatically create one.
  140. if ($child->hasProperty('ajax') && !$child->hasProperty('ajax_processed') && !$child->hasProperty('id')) {
  141. $child->setProperty('id', $child->getAttribute('id', Html::getUniqueId("ajax-link-$current_time")));
  142. }
  143. $items->$key = $child->getArrayCopy();
  144. }
  145. // Create a toggle button, extracting relevant info from primary action.
  146. $toggle = Element::createStandalone([
  147. '#type' => 'button',
  148. '#attributes' => $primary_action->getAttributes()->getArrayCopy(),
  149. '#value' => $primary_action->getProperty('value', $primary_action->getProperty('title', $primary_action->getProperty('text'))),
  150. ]);
  151. // Remove the "hidden" class that was added to the primary action.
  152. $toggle->removeClass('hidden')->removeAttribute('id')->setAttribute('data-dropdown-target', '#' . $primary_action->getAttribute('id'));
  153. // Make operations smaller.
  154. if ($operations) {
  155. $toggle->setButtonSize('btn-xs', FALSE);
  156. }
  157. // Add the toggle render array to the variables.
  158. $variables->toggle = $toggle->getArrayCopy();
  159. // Determine if toggle should be a split button.
  160. $variables->split = count($items) > 1;
  161. // Add the items variable for "bootstrap_dropdown".
  162. $variables->items = $items->getArrayCopy();
  163. // Remove the unnecessary "links" variable now.
  164. unset($variables->links);
  165. }
  166. }
  167. }

Classes

Name Description
BootstrapDropdown Pre-processes variables for the "bootstrap_dropdown" theme hook.