file BootstrapDropdown.php

Namespace

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

Classes

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