diff --git a/assets/css/contextual/contextual.dropdown.css b/assets/css/contextual/contextual.dropdown.css new file mode 100644 index 0000000000000000000000000000000000000000..abc940ef355c8398eb8fe12bbe2309cb9ebc6e1f --- /dev/null +++ b/assets/css/contextual/contextual.dropdown.css @@ -0,0 +1,3 @@ +.contextual .dropdown-menu { + --bs-dropdown-min-width: auto; +} diff --git a/assets/css/contextual/contextual.theme.css b/assets/css/contextual/contextual.theme.css new file mode 100644 index 0000000000000000000000000000000000000000..582556a4267f989a21fe596a7d6f9968707ae84f --- /dev/null +++ b/assets/css/contextual/contextual.theme.css @@ -0,0 +1,4 @@ +.contextual.open .trigger { + border-bottom-color: #ccc !important; + border-radius: 13px !important; +} diff --git a/assets/js/contextual/contextual.js b/assets/js/contextual/contextual.js new file mode 100644 index 0000000000000000000000000000000000000000..491036118450fc54329a44f672dbf295b65c780f --- /dev/null +++ b/assets/js/contextual/contextual.js @@ -0,0 +1,17 @@ +(($, Drupal) => { + /** + * Theme function for a contextual trigger. + * + * @return {string} + * A string representing a DOM fragment. + */ + Drupal.theme.contextualTrigger = () => { + return ( + '<button class="trigger focusable visually-hidden dropdown-toggle"' + + 'type="button"' + + 'data-bs-toggle="dropdown"' + + 'aria-expanded="false"' + + '></button>' + ); + }; +})(jQuery, Drupal); diff --git a/src/Element/ElementPreRenderContextualLinksPlaceholder.php b/src/Element/ElementPreRenderContextualLinksPlaceholder.php new file mode 100644 index 0000000000000000000000000000000000000000..12452371d7257ad1dcb34d01c9a8d5ea036bc7a8 --- /dev/null +++ b/src/Element/ElementPreRenderContextualLinksPlaceholder.php @@ -0,0 +1,65 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\ui_suite_bootstrap\Element; + +use Drupal\Component\Render\FormattableMarkup; +use Drupal\Component\Render\MarkupInterface; +use Drupal\Component\Utility\Html; +use Drupal\Core\Security\TrustedCallbackInterface; +use Drupal\Core\Template\Attribute; + +/** + * Element Prerender methods for contextual_links_placeholder. + */ +class ElementPreRenderContextualLinksPlaceholder implements TrustedCallbackInterface { + + /** + * Add classes for dropdown styling. + */ + public static function preRenderContextualLinksPlaceholder(array $element): array { + if (!isset($element['#markup']) || !($element['#markup'] instanceof MarkupInterface)) { + return $element; + } + + $placeholder = (string) $element['#markup']; + $attributes = static::extractAttributes($placeholder); + $attributes['class'][] = 'dropdown'; + $attributes['class'][] = 'position-absolute'; + + $attribute = new Attribute($attributes); + $element['#markup'] = new FormattableMarkup('<div@attributes></div>', ['@attributes' => $attribute]); + return $element; + } + + /** + * {@inheritdoc} + */ + public static function trustedCallbacks(): array { + return ['preRenderContextualLinksPlaceholder']; + } + + /** + * Extract attributes. + * + * @param string $html + * The HTML to parse. Expected to be one div. + * + * @return array + * The array of attributes. + */ + protected static function extractAttributes(string $html): array { + $attributes = []; + // Extract existing attributes. + /** @var \DOMElement $div */ + foreach (Html::load($html)->getElementsByTagName('div') as $div) { + /** @var \DOMAttr $attr */ + foreach ($div->attributes as $attr) { + $attributes[$attr->nodeName] = $attr->nodeValue; + } + } + return $attributes; + } + +} diff --git a/src/HookHandler/ElementInfoAlter.php b/src/HookHandler/ElementInfoAlter.php index afee3c14cc4e6b317c52f1c34d7015b73b417d55..1964d1ff987a6ef0af4aaad6410f3a2e44100e9a 100644 --- a/src/HookHandler/ElementInfoAlter.php +++ b/src/HookHandler/ElementInfoAlter.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Drupal\ui_suite_bootstrap\HookHandler; +use Drupal\ui_suite_bootstrap\Element\ElementPreRenderContextualLinksPlaceholder; use Drupal\ui_suite_bootstrap\Element\ElementPreRenderDropbutton; use Drupal\ui_suite_bootstrap\Element\ElementPreRenderLayoutBuilder; use Drupal\ui_suite_bootstrap\Element\ElementPreRenderLink; @@ -190,6 +191,14 @@ class ElementInfoAlter { ]; } + // Contextual link placeholder. + if (isset($info['contextual_links_placeholder'])) { + $info['contextual_links_placeholder']['#pre_render'][] = [ + ElementPreRenderContextualLinksPlaceholder::class, + 'preRenderContextualLinksPlaceholder', + ]; + } + // Dropbutton. if (isset($info['dropbutton'])) { // Remove Core pre_render to remove wrapper and classes. diff --git a/src/HookHandler/PreprocessLinksContextual.php b/src/HookHandler/PreprocessLinksContextual.php new file mode 100644 index 0000000000000000000000000000000000000000..7dc222423d5d52ba5d8ee32ce9f0a066ecf402b6 --- /dev/null +++ b/src/HookHandler/PreprocessLinksContextual.php @@ -0,0 +1,12 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\ui_suite_bootstrap\HookHandler; + +/** + * Ensure links structure fits into list group structure. + */ +class PreprocessLinksContextual extends PreprocessLinksMediaLibraryMenu { + +} diff --git a/templates/system/links--contextual.html.twig b/templates/system/links--contextual.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..93bf40017f41ea231b2c34bab5b63fdccf024854 --- /dev/null +++ b/templates/system/links--contextual.html.twig @@ -0,0 +1,86 @@ +{# +/** + * @file + * Default theme implementation for a set of links. + * + * Available variables: + * - attributes: Attributes for the UL containing the list of links. + * - links: Links to be output. + * Each link will have the following elements: + * - link: (optional) A render array that returns a link. See + * template_preprocess_links() for details how it is generated. + * - text: The link text. + * - attributes: HTML attributes for the list item element. + * - text_attributes: (optional) HTML attributes for the span element if no + * 'url' was supplied. + * - heading: (optional) A heading to precede the links. + * - text: The heading text. + * - level: The heading level (e.g. 'h2', 'h3'). + * - attributes: (optional) A keyed list of attributes for the heading. + * If the heading is a string, it will be used as the text of the heading and + * the level will default to 'h2'. + * + * Headings should be used on navigation menus and any list of links that + * consistently appears on multiple pages. To make the heading invisible use + * the 'visually-hidden' CSS class. Do not use 'display:none', which + * removes it from screen readers and assistive technology. Headings allow + * screen reader and keyboard only users to navigate to or skip the links. + * See http://juicystudio.com/article/screen-readers-display-none.php and + * http://www.w3.org/TR/WCAG-TECHS/H42.html for more information. + * + * @see template_preprocess_links() + * + * @ingroup themeable + */ +#} +{% if links -%} + {%- if heading -%} + {%- if heading.level -%} + <{{ heading.level }}{{ heading.attributes }}>{{ heading.text }}</{{ heading.level }}> + {%- else -%} + <h2{{ heading.attributes }}>{{ heading.text }}</h2> + {%- endif -%} + {%- endif -%} + + {% if preprocessed_items -%} + {# + Can't use the dropdown component directly because it handles the dropdown + button which is handled by JS in contextual links. + #} + <ul{{ attributes.addClass('dropdown-menu').removeClass('contextual-links') }}> + {% for item in preprocessed_items %} + {% set item_attributes = create_attribute(item.attributes|default({})) %} + <li{{ item_attributes }}> + {% set link_attributes = create_attribute(item.link_attributes|default({})) %} + {% if item.title and item.url %} + <a{{ link_attributes.setAttribute('href', item.url).addClass('dropdown-item') }}>{{ item.title }}</a> + {% elseif (not item.title and not item.url) or link_attributes.hasClass('dropdown-divider') %} + <hr{{ link_attributes.addClass('dropdown-divider') }}> + {% elseif item.title and not item.url %} + {% if link_attributes.hasClass('dropdown-header') %} + <h{{ heading_level }}{{ link_attributes }}>{{ item.title }}</h{{ heading_level }}> + {% elseif link_attributes.hasClass('dropdown-item') %} + <button{{ link_attributes.setAttribute('type', 'button') }}>{{ item.title }}</button> + {% else %} + <span{{ link_attributes.addClass('dropdown-item-text') }}>{{ item.title }}</span> + {% endif %} + {% endif %} + </li> + {% endfor %} + </ul> + {%- else -%} + <ul{{ attributes }}> + {%- for item in links -%} + <li{{ item.attributes }}> + {%- if item.link -%} + {{ item.link }} + {%- elseif item.text_attributes -%} + <span{{ item.text_attributes }}>{{ item.text }}</span> + {%- else -%} + {{ item.text }} + {%- endif -%} + </li> + {%- endfor -%} + </ul> + {%- endif -%} +{%- endif %} diff --git a/ui_suite_bootstrap.info.yml b/ui_suite_bootstrap.info.yml index 8bbf6fe85a9dd46bb5a758ef21882e3a09ccce78..0ccc69f2b93086ff22ea01f29043e212815be66d 100644 --- a/ui_suite_bootstrap.info.yml +++ b/ui_suite_bootstrap.info.yml @@ -117,6 +117,8 @@ libraries-extend: - ui_suite_bootstrap/drupal.progress core/drupal.tabledrag: - ui_suite_bootstrap/drupal.tabledrag + contextual/drupal.contextual-links: + - ui_suite_bootstrap/drupal.contextual-links layout_builder/drupal.layout_builder: - ui_suite_bootstrap/drupal.layout_builder media_library/view: diff --git a/ui_suite_bootstrap.libraries.yml b/ui_suite_bootstrap.libraries.yml index 6d27daca7baa75482420c418edb0c5ff9ef96387..6367d016f182dea9a6ab969296ab0071bd894481 100644 --- a/ui_suite_bootstrap.libraries.yml +++ b/ui_suite_bootstrap.libraries.yml @@ -46,6 +46,17 @@ drupal.checkbox: dependencies: - core/drupal +drupal.contextual-links: + js: + assets/js/contextual/contextual.js: {} + css: + theme: + assets/css/contextual/contextual.dropdown.css: {} + assets/css/contextual/contextual.theme.css: {} + dependencies: + - core/drupal + - core/jquery + drupal.dialog: js: assets/js/misc/dialog/dialog.js: {} diff --git a/ui_suite_bootstrap.theme b/ui_suite_bootstrap.theme index 4f36bcb3b2d6d07140c2a1d664f8a0e2ca6c29b4..2536d359534fc63477ddc14f006cade3d7839833 100644 --- a/ui_suite_bootstrap.theme +++ b/ui_suite_bootstrap.theme @@ -22,6 +22,7 @@ use Drupal\ui_suite_bootstrap\HookHandler\PreprocessFileLink; use Drupal\ui_suite_bootstrap\HookHandler\PreprocessFilterTips; use Drupal\ui_suite_bootstrap\HookHandler\PreprocessFormElement; use Drupal\ui_suite_bootstrap\HookHandler\PreprocessInput; +use Drupal\ui_suite_bootstrap\HookHandler\PreprocessLinksContextual; use Drupal\ui_suite_bootstrap\HookHandler\PreprocessLinksDropbutton; use Drupal\ui_suite_bootstrap\HookHandler\PreprocessLinksLayoutBuilderLinks; use Drupal\ui_suite_bootstrap\HookHandler\PreprocessLinksMediaLibraryMenu; @@ -191,6 +192,16 @@ function ui_suite_bootstrap_preprocess_input(array &$variables): void { $instance->preprocess($variables); } +/** + * Implements hook_preprocess_HOOK() for 'links__contextual'. + */ +function ui_suite_bootstrap_preprocess_links__contextual(array &$variables): void { + /** @var \Drupal\ui_suite_bootstrap\HookHandler\PreprocessLinksContextual $instance */ + $instance = \Drupal::service('class_resolver') + ->getInstanceFromDefinition(PreprocessLinksContextual::class); + $instance->preprocess($variables); +} + /** * Implements hook_preprocess_HOOK() for 'links__dropbutton'. */