/** * @file toolbar.js * * Defines the behavior of the Drupal administration toolbar. */ (function ($, Drupal, drupalSettings) { "use strict"; Drupal.toolbar = Drupal.toolbar || {}; /** * Store the state of the active tab so it will remain active across page loads. */ var activeTab = JSON.parse(localStorage.getItem('Drupal.toolbar.activeTab')); /** * Store the state of the trays to maintain them across page loads. */ var locked = JSON.parse(localStorage.getItem('Drupal.toolbar.trayVerticalLocked')) || false; var orientation = (locked) ? 'vertical' : 'horizontal'; /** * Holds the jQuery objects of the toolbar DOM element, the trays and messages. */ var $toolbar; var $trays; var $messages; /** * Holds the mediaQueryList object. */ var mql = { narrow: null, standard: null, wide: null }; /** * Register tabs with the toolbar. * * The Drupal toolbar allows modules to register top-level tabs. These may point * directly to a resource or toggle the visibility of a tray. * * Modules register tabs with hook_toolbar(). */ Drupal.behaviors.toolbar = { attach: function(context) { // Verify that the sser agent understands media queries. Complex admin // toolbar layouts require media query support. if (!window.matchMedia('only screen').matches) { return; }; var options = $.extend(this.options, drupalSettings.toolbar); var $toolbarOnce = $(context).find('#toolbar-administration').once('toolbar'); if ($toolbarOnce.length) { // Assign the $toolbar variable in the closure. $toolbar = $toolbarOnce; // Add subtrees. // @todo Optimize this to delay adding each subtree to the DOM until it is // needed; however, take into account screen readers for determining // when the DOM elements are needed. var subtrees = Drupal.toolbar.subtrees; if (subtrees) { for (var id in subtrees) { if (subtrees.hasOwnProperty(id)) { $('#toolbar-link-' + id).after(subtrees[id]); } } } // Append a messages element for appending interaction updates for screen // readers. $messages = $(Drupal.theme('toolbarMessageBox')).appendTo($toolbar); // Store the trays in a scoped variable. $trays = $toolbar.find('.toolbar-tray'); $trays // Add the tray orientation toggles. .find('.toolbar-lining') .append(Drupal.theme('toolbarOrientationToggle')); // Store media queries and attach a handler. mql.narrow = window.matchMedia(options.breakpoints['module.toolbar.narrow']); mql.narrow.addListener(Drupal.toolbar.narrowMediaQueryChangeHandler); mql.standard = window.matchMedia(options.breakpoints['module.toolbar.standard']); mql.standard.addListener(Drupal.toolbar.standardMediaQueryChangeHandler); mql.wide = window.matchMedia(options.breakpoints['module.toolbar.wide']); mql.wide.addListener(Drupal.toolbar.wideMediaQueryChangeHandler); // Fire each MediaQuery change handler so they process once. Drupal.toolbar.narrowMediaQueryChangeHandler.call(this, mql.narrow); Drupal.toolbar.standardMediaQueryChangeHandler.call(this, mql.standard); Drupal.toolbar.wideMediaQueryChangeHandler.call(this, mql.wide); // Set the orientation of the tray. // If the tray is set to vertical in localStorage, persist the vertical // presentation. If the tray is not locked to vertical, let the media // query application decide the orientation. changeOrientation((locked) ? 'vertical' : ((mql.wide.matches) ? 'horizontal' : 'vertical'), locked); // Render the main menu as a nested, collapsible accordion. $toolbar.find('.toolbar-menu-administration > .menu').toolbarMenu(); // Attach behaviors to the document. $(document) .on('drupalViewportOffsetChange.toolbar', Drupal.toolbar.adjustPlacement); // Attach behaviors to the toolbar. $toolbar .on('click.toolbar', '.toolbar-bar a', Drupal.toolbar.toggleTray) .on('click.toolbar', '.toolbar-toggle-orientation button', Drupal.toolbar.orientationChangeHandler); // Restore the open tab. Only open the tab on wide screens. if (activeTab && window.matchMedia(options.breakpoints['module.toolbar.standard']).matches) { $toolbar.find('[data-toolbar-tray="' + activeTab + '"]').trigger('click.toolbar'); } else { // Update the page and toolbar dimension indicators. updatePeripherals(); } // Call displace to get the initial placement of offset elements. Drupal.displace(); } }, // Default options. options: { breakpoints: { 'module.toolbar.narrow': '', 'module.toolbar.standard': '', 'module.toolbar.wide': '' } } }; /** * Set subtrees. * * JSONP callback. * @see toolbar_subtrees_jsonp(). */ Drupal.toolbar.setSubtrees = function(subtrees) { Drupal.toolbar.subtrees = subtrees; }; /** * Toggle a toolbar tab and the associated tray. */ Drupal.toolbar.toggleTray = function (event) { var strings = { opened: Drupal.t('opened'), closed: Drupal.t('closed') }; var $tab = $(event.target); var name = $tab.attr('data-toolbar-tray'); // Activate the selected tab and associated tray. var $activateTray = $toolbar.find('[data-toolbar-tray="' + name + '"].toolbar-tray').toggleClass('active'); if ($activateTray.length) { event.preventDefault(); event.stopPropagation(); $tab.toggleClass('active'); // Toggle aria-pressed. var value = $tab.prop('aria-pressed'); $tab.prop('aria-pressed', (value === 'false') ? 'true' : 'false'); // Append a message that a tray has been opened. setMessage(Drupal.t('@tray tray @state.', { '@tray': name, '@state': (value === 'true') ? strings.closed : strings.opened })); // Store the active tab name or remove the setting. if ($tab.hasClass('active')) { localStorage.setItem('Drupal.toolbar.activeTab', JSON.stringify(name)); } else { localStorage.removeItem('Drupal.toolbar.activeTab'); } // Disable non-selected tabs and trays. $toolbar.find('.toolbar-bar .trigger') .not($tab) .removeClass('active') // Set aria-pressed to false. .prop('aria-pressed', false); $toolbar.find('.toolbar-tray').not($activateTray).removeClass('active'); } // Update the page and toolbar dimension indicators. updatePeripherals(); }; /** * Repositions trays and sets body padding according to the height of the bar. * * @param {Event} event * - jQuery Event object. * * @param {Object} offsets * - Contains for keys -- top, right, bottom and left -- that indicate the * viewport offset distances calculated by Drupal.displace(). */ Drupal.toolbar.adjustPlacement = function (event, offsets) { if (!mql.narrow.matches) { var $body = $('body'); var $trays = $toolbar.find('.toolbar-tray'); // Alter the padding on the top of the body element. $body.css('padding-top', 0); $trays.css('padding-top', 0); // Remove any orientation classes. Make vertical the default for trays. $body.removeClass('toolbar-vertical toolbar-horizontal'); $trays.removeClass('toolbar-tray-horizontal').addClass('toolbar-tray-vertical'); } else { // Alter the padding on the top of the body element. $('body').css('padding-top', offsets.top); // The navbar container is invisible. Its placement is used to determine the // container for the trays. $toolbar.find('.toolbar-tray').css('padding-top', $toolbar.find('.toolbar-bar').outerHeight()); } }; /** * Sets the width of a vertical tray in a data attribute. * * If the width of the tray changed, Drupal.displace is called so that elements * can adjust to the placement of the tray. */ Drupal.toolbar.setTrayWidth = function () { var dir = document.documentElement.dir; var edge = (dir === 'rtl') ? 'right' : 'left'; // Remove the side offset from the trays. $toolbar.find('.toolbar-tray').removeAttr('data-offset-' + edge + ' data-offset-top'); // If the page is wider than the narrow media query, apply offset attributes. if (mql.narrow.matches) { // If an active vertical tray exists, mark it as an offset element. $toolbar.find('.toolbar-tray.toolbar-tray-vertical.active').attr('data-offset-' + edge, ''); // If an active horizontal tray exists, mark it as an offset element. $toolbar.find('.toolbar-tray.toolbar-tray-horizontal.active').attr('data-offset-top', ''); } // Trigger a recalculation of viewport displacing elements. Drupal.displace(); }; /** * Respond to configured narrow media query changes. */ Drupal.toolbar.narrowMediaQueryChangeHandler = function (mql) { var $bar = $toolbar.find('.toolbar-bar'); if (mql.matches) { $bar.attr('data-offset-top', ''); } else { $bar.removeAttr('data-offset-top'); } // Toggle between a basic vertical view and a more sophisticated horizontal // and vertical display of the toolbar bar and trays. $toolbar.toggleClass('toolbar-oriented', mql.matches); if (mql.matches) { changeOrientation('vertical'); } // Update the page and toolbar dimension indicators. updatePeripherals(); }; /** * Respond to configured standard media query changes. */ Drupal.toolbar.standardMediaQueryChangeHandler = function (mql) { $('body').toggleClass('toolbar-paneled', mql.matches); // Update the page and toolbar dimension indicators. updatePeripherals(); }; /** * Respond to configured wide media query changes. */ Drupal.toolbar.wideMediaQueryChangeHandler = function (mql) { var orientation = (mql.matches) ? 'horizontal' : 'vertical'; changeOrientation(orientation); // Update the page and toolbar dimension indicators. updatePeripherals(); }; /** * Respond to the toggling of the tray orientation. */ Drupal.toolbar.orientationChangeHandler = function (event) { event.preventDefault(); event.stopPropagation(); var orientation = event.target.value; changeOrientation(orientation, true); // Update the page and toolbar dimension indicators. updatePeripherals(); }; /** * Change the orientation of the tray between vertical and horizontal. * * @param {String} newOrientation * Either 'vertical' or 'horizontal'. The orientation to change the tray to. * * @param {Boolean} isLock * Whether the orientation of the tray should be locked if it is being toggled * to vertical. */ function changeOrientation (newOrientation, isLock) { if (isLock) { locked = (newOrientation === 'vertical'); if (locked) { localStorage.setItem('Drupal.toolbar.trayVerticalLocked', JSON.stringify(locked)); } else { localStorage.removeItem('Drupal.toolbar.trayVerticalLocked'); } } if ((!locked && newOrientation === 'horizontal') || newOrientation === 'vertical') { $trays .removeClass('toolbar-tray-horizontal toolbar-tray-vertical') .addClass('toolbar-tray-' + newOrientation); orientation = newOrientation; toggleOrientationToggle((newOrientation === 'vertical') ? 'horizontal' : 'vertical'); } } /** * Mark up the body tag to reflect the current state of the toolbar. */ function setBodyState () { var $activeTray = $trays.filter('.active'); $('body') .toggleClass('toolbar-tray-open', !!$activeTray.length) .toggleClass('toolbar-vertical', (!!$activeTray.length && orientation === 'vertical')) .toggleClass('toolbar-horizontal', (!!$activeTray.length && orientation === 'horizontal')); } /** * Change the orientation toggle active state. */ function toggleOrientationToggle (orientation) { var strings = { horizontal: Drupal.t('Horizontal orientation'), vertical: Drupal.t('Vertical orientation') }; var antiOrientation = (orientation === 'vertical') ? 'horizontal' : 'vertical'; var iconClass = 'toolbar-icon-toggle-' + orientation; var iconAntiClass = 'toolbar-icon-toggle-' + antiOrientation; // Append a message that the tray orientation has been changed. setMessage(Drupal.t('Tray orientation changed to @orientation.', { '@orientation': antiOrientation })); // Change the tray orientation. $trays.find('.toolbar-toggle-orientation button') .val(orientation) .text(strings[orientation]) .removeClass(iconAntiClass) .addClass(iconClass); } /** * Updates elements peripheral to the toolbar. * * When the dimensions and orientation of the toolbar change, elements on the * page must either be changed or informed of the changes. */ function updatePeripherals () { // Adjust the body to accommodate trays. setBodyState(); // Adjust the tray width for vertical trays. Drupal.toolbar.setTrayWidth(); } /** * Places the message in the toolbar's ARIA live message area. * * The message will be read by speaking User Agents. * * @param {String} message * A string to be inserted into the message area. */ function setMessage (message) { $messages.html(Drupal.theme('toolbarTrayMessage', message)); } /** * A toggle is an interactive element often bound to a click handler. * * @return {String} * A string representing a DOM fragment. */ Drupal.theme.toolbarOrientationToggle = function () { return '
'; }; /** * A region to post messages that a screen reading UA will announce. * * @return {String} * A string representing a DOM fragment. */ Drupal.theme.toolbarMessageBox = function () { return ''; }; /** * Wrap a message string in a p tag. * * @return {String} * A string representing a DOM fragment. */ Drupal.theme.toolbarTrayMessage = function (message) { return '' + message + '
'; }; }(jQuery, Drupal, drupalSettings));