Chris@0: /** Chris@0: * @file Chris@0: * Defines the behavior of the Drupal administration toolbar. Chris@0: */ Chris@0: Chris@17: (function($, Drupal, drupalSettings) { Chris@0: // Merge run-time settings with the defaults. Chris@0: const options = $.extend( Chris@0: { Chris@0: breakpoints: { Chris@0: 'toolbar.narrow': '', Chris@0: 'toolbar.standard': '', Chris@0: 'toolbar.wide': '', Chris@0: }, Chris@0: }, Chris@0: drupalSettings.toolbar, Chris@0: // Merge strings on top of drupalSettings so that they are not mutable. Chris@0: { Chris@0: strings: { Chris@0: horizontal: Drupal.t('Horizontal orientation'), Chris@0: vertical: Drupal.t('Vertical orientation'), Chris@0: }, Chris@0: }, Chris@0: ); Chris@0: Chris@0: /** Chris@0: * Registers tabs with the toolbar. Chris@0: * Chris@0: * The Drupal toolbar allows modules to register top-level tabs. These may Chris@0: * point directly to a resource or toggle the visibility of a tray. Chris@0: * Chris@0: * Modules register tabs with hook_toolbar(). Chris@0: * Chris@0: * @type {Drupal~behavior} Chris@0: * Chris@0: * @prop {Drupal~behaviorAttach} attach Chris@0: * Attaches the toolbar rendering functionality to the toolbar element. Chris@0: */ Chris@0: Drupal.behaviors.toolbar = { Chris@0: attach(context) { Chris@0: // Verify that the user agent understands media queries. Complex admin Chris@0: // toolbar layouts require media query support. Chris@0: if (!window.matchMedia('only screen').matches) { Chris@0: return; Chris@0: } Chris@0: // Process the administrative toolbar. Chris@17: $(context) Chris@17: .find('#toolbar-administration') Chris@17: .once('toolbar') Chris@17: .each(function() { Chris@17: // Establish the toolbar models and views. Chris@17: const model = new Drupal.toolbar.ToolbarModel({ Chris@17: locked: JSON.parse( Chris@17: localStorage.getItem('Drupal.toolbar.trayVerticalLocked'), Chris@17: ), Chris@17: activeTab: document.getElementById( Chris@17: JSON.parse(localStorage.getItem('Drupal.toolbar.activeTabID')), Chris@17: ), Chris@17: height: $('#toolbar-administration').outerHeight(), Chris@0: }); Chris@0: Chris@17: Drupal.toolbar.models.toolbarModel = model; Chris@17: Chris@17: // Attach a listener to the configured media query breakpoints. Chris@17: // Executes it before Drupal.toolbar.views to avoid extra rendering. Chris@17: Object.keys(options.breakpoints).forEach(label => { Chris@17: const mq = options.breakpoints[label]; Chris@17: const mql = window.matchMedia(mq); Chris@17: Drupal.toolbar.mql[label] = mql; Chris@17: // Curry the model and the label of the media query breakpoint to Chris@17: // the mediaQueryChangeHandler function. Chris@17: mql.addListener( Chris@17: Drupal.toolbar.mediaQueryChangeHandler.bind(null, model, label), Chris@17: ); Chris@17: // Fire the mediaQueryChangeHandler for each configured breakpoint Chris@17: // so that they process once. Chris@17: Drupal.toolbar.mediaQueryChangeHandler.call( Chris@17: null, Chris@17: model, Chris@17: label, Chris@17: mql, Chris@17: ); Chris@0: }); Chris@0: Chris@17: Drupal.toolbar.views.toolbarVisualView = new Drupal.toolbar.ToolbarVisualView( Chris@17: { Chris@17: el: this, Chris@17: model, Chris@17: strings: options.strings, Chris@17: }, Chris@17: ); Chris@17: Drupal.toolbar.views.toolbarAuralView = new Drupal.toolbar.ToolbarAuralView( Chris@17: { Chris@17: el: this, Chris@17: model, Chris@17: strings: options.strings, Chris@17: }, Chris@17: ); Chris@17: Drupal.toolbar.views.bodyVisualView = new Drupal.toolbar.BodyVisualView( Chris@17: { Chris@17: el: this, Chris@17: model, Chris@17: }, Chris@17: ); Chris@17: Chris@17: // Force layout render to fix mobile view. Only needed on load, not Chris@17: // for every media query match. Chris@17: model.trigger('change:isFixed', model, model.get('isFixed')); Chris@17: model.trigger('change:activeTray', model, model.get('activeTray')); Chris@17: Chris@17: // Render collapsible menus. Chris@17: const menuModel = new Drupal.toolbar.MenuModel(); Chris@17: Drupal.toolbar.models.menuModel = menuModel; Chris@17: Drupal.toolbar.views.menuVisualView = new Drupal.toolbar.MenuVisualView( Chris@17: { Chris@17: el: $(this) Chris@17: .find('.toolbar-menu-administration') Chris@17: .get(0), Chris@17: model: menuModel, Chris@17: strings: options.strings, Chris@17: }, Chris@17: ); Chris@17: Chris@17: // Handle the resolution of Drupal.toolbar.setSubtrees. Chris@17: // This is handled with a deferred so that the function may be invoked Chris@17: // asynchronously. Chris@17: Drupal.toolbar.setSubtrees.done(subtrees => { Chris@17: menuModel.set('subtrees', subtrees); Chris@17: const theme = drupalSettings.ajaxPageState.theme; Chris@17: localStorage.setItem( Chris@17: `Drupal.toolbar.subtrees.${theme}`, Chris@17: JSON.stringify(subtrees), Chris@17: ); Chris@17: // Indicate on the toolbarModel that subtrees are now loaded. Chris@17: model.set('areSubtreesLoaded', true); Chris@0: }); Chris@17: Chris@17: // Trigger an initial attempt to load menu subitems. This first attempt Chris@17: // is made after the media query handlers have had an opportunity to Chris@17: // process. The toolbar starts in the vertical orientation by default, Chris@17: // unless the viewport is wide enough to accommodate a horizontal Chris@17: // orientation. Thus we give the Toolbar a chance to determine if it Chris@17: // should be set to horizontal orientation before attempting to load Chris@17: // menu subtrees. Chris@17: Drupal.toolbar.views.toolbarVisualView.loadSubtrees(); Chris@17: Chris@17: $(document) Chris@17: // Update the model when the viewport offset changes. Chris@17: .on('drupalViewportOffsetChange.toolbar', (event, offsets) => { Chris@17: model.set('offsets', offsets); Chris@17: }); Chris@17: Chris@17: // Broadcast model changes to other modules. Chris@17: model Chris@17: .on('change:orientation', (model, orientation) => { Chris@17: $(document).trigger( Chris@17: 'drupalToolbarOrientationChange', Chris@17: orientation, Chris@17: ); Chris@17: }) Chris@17: .on('change:activeTab', (model, tab) => { Chris@17: $(document).trigger('drupalToolbarTabChange', tab); Chris@17: }) Chris@17: .on('change:activeTray', (model, tray) => { Chris@17: $(document).trigger('drupalToolbarTrayChange', tray); Chris@17: }); Chris@17: Chris@17: // If the toolbar's orientation is horizontal and no active tab is Chris@17: // defined then show the tray of the first toolbar tab by default (but Chris@17: // not the first 'Home' toolbar tab). Chris@17: if ( Chris@17: Drupal.toolbar.models.toolbarModel.get('orientation') === Chris@17: 'horizontal' && Chris@17: Drupal.toolbar.models.toolbarModel.get('activeTab') === null Chris@17: ) { Chris@17: Drupal.toolbar.models.toolbarModel.set({ Chris@17: activeTab: $( Chris@17: '.toolbar-bar .toolbar-tab:not(.home-toolbar-tab) a', Chris@17: ).get(0), Chris@17: }); Chris@17: } Chris@17: Chris@17: $(window).on({ Chris@17: 'dialog:aftercreate': (event, dialog, $element, settings) => { Chris@17: const $toolbar = $('#toolbar-bar'); Chris@17: $toolbar.css('margin-top', '0'); Chris@17: Chris@17: // When off-canvas is positioned in top, toolbar has to be moved down. Chris@17: if (settings.drupalOffCanvasPosition === 'top') { Chris@17: const height = Drupal.offCanvas Chris@17: .getContainer($element) Chris@17: .outerHeight(); Chris@17: $toolbar.css('margin-top', `${height}px`); Chris@17: Chris@17: $element.on('dialogContentResize.off-canvas', () => { Chris@17: const newHeight = Drupal.offCanvas Chris@17: .getContainer($element) Chris@17: .outerHeight(); Chris@17: $toolbar.css('margin-top', `${newHeight}px`); Chris@17: }); Chris@17: } Chris@17: }, Chris@17: 'dialog:beforeclose': () => { Chris@17: $('#toolbar-bar').css('margin-top', '0'); Chris@17: }, Chris@17: }); Chris@17: }); Chris@0: }, Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Toolbar methods of Backbone objects. Chris@0: * Chris@0: * @namespace Chris@0: */ Chris@0: Drupal.toolbar = { Chris@0: /** Chris@0: * A hash of View instances. Chris@0: * Chris@0: * @type {object.} Chris@0: */ Chris@0: views: {}, Chris@0: Chris@0: /** Chris@0: * A hash of Model instances. Chris@0: * Chris@0: * @type {object.} Chris@0: */ Chris@0: models: {}, Chris@0: Chris@0: /** Chris@0: * A hash of MediaQueryList objects tracked by the toolbar. Chris@0: * Chris@0: * @type {object.} Chris@0: */ Chris@0: mql: {}, Chris@0: Chris@0: /** Chris@0: * Accepts a list of subtree menu elements. Chris@0: * Chris@0: * A deferred object that is resolved by an inlined JavaScript callback. Chris@0: * Chris@0: * @type {jQuery.Deferred} Chris@0: * Chris@0: * @see toolbar_subtrees_jsonp(). Chris@0: */ Chris@0: setSubtrees: new $.Deferred(), Chris@0: Chris@0: /** Chris@0: * Respond to configured narrow media query changes. Chris@0: * Chris@0: * @param {Drupal.toolbar.ToolbarModel} model Chris@0: * A toolbar model Chris@0: * @param {string} label Chris@0: * Media query label. Chris@0: * @param {object} mql Chris@0: * A MediaQueryList object. Chris@0: */ Chris@0: mediaQueryChangeHandler(model, label, mql) { Chris@0: switch (label) { Chris@0: case 'toolbar.narrow': Chris@0: model.set({ Chris@0: isOriented: mql.matches, Chris@0: isTrayToggleVisible: false, Chris@0: }); Chris@0: // If the toolbar doesn't have an explicit orientation yet, or if the Chris@0: // narrow media query doesn't match then set the orientation to Chris@0: // vertical. Chris@0: if (!mql.matches || !model.get('orientation')) { Chris@0: model.set({ orientation: 'vertical' }, { validate: true }); Chris@0: } Chris@0: break; Chris@0: Chris@0: case 'toolbar.standard': Chris@0: model.set({ Chris@0: isFixed: mql.matches, Chris@0: }); Chris@0: break; Chris@0: Chris@0: case 'toolbar.wide': Chris@17: model.set( Chris@17: { Chris@17: orientation: Chris@17: mql.matches && !model.get('locked') ? 'horizontal' : 'vertical', Chris@17: }, Chris@17: { validate: true }, Chris@17: ); Chris@0: // The tray orientation toggle visibility does not need to be Chris@0: // validated. Chris@0: model.set({ Chris@0: isTrayToggleVisible: mql.matches, Chris@0: }); Chris@0: break; Chris@0: Chris@0: default: Chris@0: break; Chris@0: } Chris@0: }, Chris@0: }; Chris@0: Chris@0: /** Chris@0: * A toggle is an interactive element often bound to a click handler. Chris@0: * Chris@0: * @return {string} Chris@0: * A string representing a DOM fragment. Chris@0: */ Chris@17: Drupal.theme.toolbarOrientationToggle = function() { Chris@17: return ( Chris@17: '
' + Chris@0: '' + Chris@17: '
' Chris@17: ); Chris@0: }; Chris@0: Chris@0: /** Chris@0: * Ajax command to set the toolbar subtrees. Chris@0: * Chris@0: * @param {Drupal.Ajax} ajax Chris@0: * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. Chris@0: * @param {object} response Chris@0: * JSON response from the Ajax request. Chris@0: * @param {number} [status] Chris@0: * XMLHttpRequest status. Chris@0: */ Chris@17: Drupal.AjaxCommands.prototype.setToolbarSubtrees = function( Chris@17: ajax, Chris@17: response, Chris@17: status, Chris@17: ) { Chris@0: Drupal.toolbar.setSubtrees.resolve(response.subtrees); Chris@0: }; Chris@17: })(jQuery, Drupal, drupalSettings);