--- /dev/null
+/**
+ * @file
+ * Builds a nested accordion widget.
+ *
+ * Invoke on an HTML list element with the jQuery plugin pattern.
+ *
+ * @example
+ * $('.toolbar-menu').drupalToolbarMenu();
+ */
+
+(function ($, Drupal, drupalSettings) {
+ /**
+ * Store the open menu tray.
+ */
+ let activeItem = Drupal.url(drupalSettings.path.currentPath);
+
+ $.fn.drupalToolbarMenu = function () {
+ const ui = {
+ handleOpen: Drupal.t('Extend'),
+ handleClose: Drupal.t('Collapse'),
+ };
+
+ /**
+ * Handle clicks from the disclosure button on an item with sub-items.
+ *
+ * @param {Object} event
+ * A jQuery Event object.
+ */
+ function toggleClickHandler(event) {
+ const $toggle = $(event.target);
+ const $item = $toggle.closest('li');
+ // Toggle the list item.
+ toggleList($item);
+ // Close open sibling menus.
+ const $openItems = $item.siblings().filter('.open');
+ toggleList($openItems, false);
+ }
+
+ /**
+ * Handle clicks from a menu item link.
+ *
+ * @param {Object} event
+ * A jQuery Event object.
+ */
+ function linkClickHandler(event) {
+ // If the toolbar is positioned fixed (and therefore hiding content
+ // underneath), then users expect clicks in the administration menu tray
+ // to take them to that destination but for the menu tray to be closed
+ // after clicking: otherwise the toolbar itself is obstructing the view
+ // of the destination they chose.
+ if (!Drupal.toolbar.models.toolbarModel.get('isFixed')) {
+ Drupal.toolbar.models.toolbarModel.set('activeTab', null);
+ }
+ // Stopping propagation to make sure that once a toolbar-box is clicked
+ // (the whitespace part), the page is not redirected anymore.
+ event.stopPropagation();
+ }
+
+ /**
+ * Toggle the open/close state of a list is a menu.
+ *
+ * @param {jQuery} $item
+ * The li item to be toggled.
+ *
+ * @param {Boolean} switcher
+ * A flag that forces toggleClass to add or a remove a class, rather than
+ * simply toggling its presence.
+ */
+ function toggleList($item, switcher) {
+ const $toggle = $item.children('.toolbar-box').children('.toolbar-handle');
+ switcher = (typeof switcher !== 'undefined') ? switcher : !$item.hasClass('open');
+ // Toggle the item open state.
+ $item.toggleClass('open', switcher);
+ // Twist the toggle.
+ $toggle.toggleClass('open', switcher);
+ // Adjust the toggle text.
+ $toggle
+ .find('.action')
+ // Expand Structure, Collapse Structure.
+ .text((switcher) ? ui.handleClose : ui.handleOpen);
+ }
+
+ /**
+ * Add markup to the menu elements.
+ *
+ * Items with sub-elements have a list toggle attached to them. Menu item
+ * links and the corresponding list toggle are wrapped with in a div
+ * classed with .toolbar-box. The .toolbar-box div provides a positioning
+ * context for the item list toggle.
+ *
+ * @param {jQuery} $menu
+ * The root of the menu to be initialized.
+ */
+ function initItems($menu) {
+ const options = {
+ class: 'toolbar-icon toolbar-handle',
+ action: ui.handleOpen,
+ text: '',
+ };
+ // Initialize items and their links.
+ $menu.find('li > a').wrap('<div class="toolbar-box">');
+ // Add a handle to each list item if it has a menu.
+ $menu.find('li').each((index, element) => {
+ const $item = $(element);
+ if ($item.children('ul.toolbar-menu').length) {
+ const $box = $item.children('.toolbar-box');
+ options.text = Drupal.t('@label', { '@label': $box.find('a').text() });
+ $item.children('.toolbar-box')
+ .append(Drupal.theme('toolbarMenuItemToggle', options));
+ }
+ });
+ }
+
+ /**
+ * Adds a level class to each list based on its depth in the menu.
+ *
+ * This function is called recursively on each sub level of lists elements
+ * until the depth of the menu is exhausted.
+ *
+ * @param {jQuery} $lists
+ * A jQuery object of ul elements.
+ *
+ * @param {number} level
+ * The current level number to be assigned to the list elements.
+ */
+ function markListLevels($lists, level) {
+ level = (!level) ? 1 : level;
+ const $lis = $lists.children('li').addClass(`level-${level}`);
+ $lists = $lis.children('ul');
+ if ($lists.length) {
+ markListLevels($lists, level + 1);
+ }
+ }
+
+ /**
+ * On page load, open the active menu item.
+ *
+ * Marks the trail of the active link in the menu back to the root of the
+ * menu with .menu-item--active-trail.
+ *
+ * @param {jQuery} $menu
+ * The root of the menu.
+ */
+ function openActiveItem($menu) {
+ const pathItem = $menu.find(`a[href="${location.pathname}"]`);
+ if (pathItem.length && !activeItem) {
+ activeItem = location.pathname;
+ }
+ if (activeItem) {
+ const $activeItem = $menu.find(`a[href="${activeItem}"]`).addClass('menu-item--active');
+ const $activeTrail = $activeItem.parentsUntil('.root', 'li').addClass('menu-item--active-trail');
+ toggleList($activeTrail, true);
+ }
+ }
+
+ // Return the jQuery object.
+ return this.each(function (selector) {
+ const $menu = $(this).once('toolbar-menu');
+ if ($menu.length) {
+ // Bind event handlers.
+ $menu
+ .on('click.toolbar', '.toolbar-box', toggleClickHandler)
+ .on('click.toolbar', '.toolbar-box a', linkClickHandler);
+
+ $menu.addClass('root');
+ initItems($menu);
+ markListLevels($menu);
+ // Restore previous and active states.
+ openActiveItem($menu);
+ }
+ });
+ };
+
+ /**
+ * A toggle is an interactive element often bound to a click handler.
+ *
+ * @param {object} options
+ * Options for the button.
+ * @param {string} options.class
+ * Class to set on the button.
+ * @param {string} options.action
+ * Action for the button.
+ * @param {string} options.text
+ * Used as label for the button.
+ *
+ * @return {string}
+ * A string representing a DOM fragment.
+ */
+ Drupal.theme.toolbarMenuItemToggle = function (options) {
+ return `<button class="${options.class}"><span class="action">${options.action}</span><span class="label">${options.text}</span></button>`;
+ };
+}(jQuery, Drupal, drupalSettings));