/** * @file * Adapted from underscore.js with the addition Drupal namespace. */ /** * Limits the invocations of a function in a given time frame. * * The debounce function wrapper should be used sparingly. One clear use case * is limiting the invocation of a callback attached to the window resize event. * * Before using the debounce function wrapper, consider first whether the * callback could be attached to an event that fires less frequently or if the * function can be written in such a way that it is only invoked under specific * conditions. * * @param {function} func * The function to be invoked. * @param {number} wait * The time period within which the callback function should only be * invoked once. For example if the wait period is 250ms, then the callback * will only be called at most 4 times per second. * @param {bool} immediate * Whether we wait at the beginning or end to execute the function. * * @return {function} * The debounced function. */ Drupal.debounce = function (func, wait, immediate) { 'use strict'; var timeout; var result; return function () { var context = this; var args = arguments; var later = function () { timeout = null; if (!immediate) { result = func.apply(context, args); } }; var callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) { result = func.apply(context, args); } return result; }; }; ; /** * @file * Adds an HTML element and method to trigger audio UAs to read system messages. * * Use {@link Drupal.announce} to indicate to screen reader users that an * element on the page has changed state. For instance, if clicking a link * loads 10 more items into a list, one might announce the change like this. * * @example * $('#search-list') * .on('itemInsert', function (event, data) { * // Insert the new items. * $(data.container.el).append(data.items.el); * // Announce the change to the page contents. * Drupal.announce(Drupal.t('@count items added to @container', * {'@count': data.items.length, '@container': data.container.title} * )); * }); */ (function (Drupal, debounce) { 'use strict'; var liveElement; var announcements = []; /** * Builds a div element with the aria-live attribute and add it to the DOM. * * @type {Drupal~behavior} * * @prop {Drupal~behaviorAttach} attach * Attaches the behavior for drupalAnnouce. */ Drupal.behaviors.drupalAnnounce = { attach: function (context) { // Create only one aria-live element. if (!liveElement) { liveElement = document.createElement('div'); liveElement.id = 'drupal-live-announce'; liveElement.className = 'visually-hidden'; liveElement.setAttribute('aria-live', 'polite'); liveElement.setAttribute('aria-busy', 'false'); document.body.appendChild(liveElement); } } }; /** * Concatenates announcements to a single string; appends to the live region. */ function announce() { var text = []; var priority = 'polite'; var announcement; // Create an array of announcement strings to be joined and appended to the // aria live region. var il = announcements.length; for (var i = 0; i < il; i++) { announcement = announcements.pop(); text.unshift(announcement.text); // If any of the announcements has a priority of assertive then the group // of joined announcements will have this priority. if (announcement.priority === 'assertive') { priority = 'assertive'; } } if (text.length) { // Clear the liveElement so that repeated strings will be read. liveElement.innerHTML = ''; // Set the busy state to true until the node changes are complete. liveElement.setAttribute('aria-busy', 'true'); // Set the priority to assertive, or default to polite. liveElement.setAttribute('aria-live', priority); // Print the text to the live region. Text should be run through // Drupal.t() before being passed to Drupal.announce(). liveElement.innerHTML = text.join('\n'); // The live text area is updated. Allow the AT to announce the text. liveElement.setAttribute('aria-busy', 'false'); } } /** * Triggers audio UAs to read the supplied text. * * The aria-live region will only read the text that currently populates its * text node. Replacing text quickly in rapid calls to announce results in * only the text from the most recent call to {@link Drupal.announce} being * read. By wrapping the call to announce in a debounce function, we allow for * time for multiple calls to {@link Drupal.announce} to queue up their * messages. These messages are then joined and append to the aria-live region * as one text node. * * @param {string} text * A string to be read by the UA. * @param {string} [priority='polite'] * A string to indicate the priority of the message. Can be either * 'polite' or 'assertive'. * * @return {function} * The return of the call to debounce. * * @see http://www.w3.org/WAI/PF/aria-practices/#liveprops */ Drupal.announce = function (text, priority) { // Save the text and priority into a closure variable. Multiple simultaneous // announcements will be concatenated and read in sequence. announcements.push({ text: text, priority: priority }); // Immediately invoke the function that debounce returns. 200 ms is right at // the cusp where humans notice a pause, so we will wait // at most this much time before the set of queued announcements is read. return (debounce(announce, 200)()); }; }(Drupal, Drupal.debounce)); ; window.matchMedia||(window.matchMedia=function(){"use strict";var e=window.styleMedia||window.media;if(!e){var t=document.createElement("style"),i=document.getElementsByTagName("script")[0],n=null;t.type="text/css";t.id="matchmediajs-test";i.parentNode.insertBefore(t,i);n="getComputedStyle"in window&&window.getComputedStyle(t,null)||t.currentStyle;e={matchMedium:function(e){var i="@media "+e+"{ #matchmediajs-test { width: 1px; } }";if(t.styleSheet){t.styleSheet.cssText=i}else{t.textContent=i}return n.width==="1px"}}}return function(t){return{matches:e.matchMedium(t||"all"),media:t||"all"}}}()); ; (function(){if(window.matchMedia&&window.matchMedia("all").addListener){return false}var e=window.matchMedia,i=e("only all").matches,n=false,t=0,a=[],r=function(i){clearTimeout(t);t=setTimeout(function(){for(var i=0,n=a.length;i a').wrap('
'); // Add a handle to each list item if it has a menu. $menu.find('li').each(function (index, element) { var $item = $(element); if ($item.children('ul.toolbar-menu').length) { var $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; var $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) { var pathItem = $menu.find('a[href="' + location.pathname + '"]'); if (pathItem.length && !activeItem) { activeItem = location.pathname; } if (activeItem) { var $activeItem = $menu.find('a[href="' + activeItem + '"]').addClass('menu-item--active'); var $activeTrail = $activeItem.parentsUntil('.root', 'li').addClass('menu-item--active-trail'); toggleList($activeTrail, true); } } // Return the jQuery object. return this.each(function (selector) { var $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 ''; }; }(jQuery, Drupal, drupalSettings)); ; /** * @file * Defines the behavior of the Drupal administration toolbar. */ (function ($, Drupal, drupalSettings) { 'use strict'; // Merge run-time settings with the defaults. var options = $.extend( { breakpoints: { 'toolbar.narrow': '', 'toolbar.standard': '', 'toolbar.wide': '' } }, drupalSettings.toolbar, // Merge strings on top of drupalSettings so that they are not mutable. { strings: { horizontal: Drupal.t('Horizontal orientation'), vertical: Drupal.t('Vertical orientation') } } ); /** * Registers 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(). * * @type {Drupal~behavior} * * @prop {Drupal~behaviorAttach} attach * Attaches the toolbar rendering functionality to the toolbar element. */ Drupal.behaviors.toolbar = { attach: function (context) { // Verify that the user agent understands media queries. Complex admin // toolbar layouts require media query support. if (!window.matchMedia('only screen').matches) { return; } // Process the administrative toolbar. $(context).find('#toolbar-administration').once('toolbar').each(function () { // Establish the toolbar models and views. var model = Drupal.toolbar.models.toolbarModel = new Drupal.toolbar.ToolbarModel({ locked: JSON.parse(localStorage.getItem('Drupal.toolbar.trayVerticalLocked')) || false, activeTab: document.getElementById(JSON.parse(localStorage.getItem('Drupal.toolbar.activeTabID'))) }); Drupal.toolbar.views.toolbarVisualView = new Drupal.toolbar.ToolbarVisualView({ el: this, model: model, strings: options.strings }); Drupal.toolbar.views.toolbarAuralView = new Drupal.toolbar.ToolbarAuralView({ el: this, model: model, strings: options.strings }); Drupal.toolbar.views.bodyVisualView = new Drupal.toolbar.BodyVisualView({ el: this, model: model }); // Render collapsible menus. var menuModel = Drupal.toolbar.models.menuModel = new Drupal.toolbar.MenuModel(); Drupal.toolbar.views.menuVisualView = new Drupal.toolbar.MenuVisualView({ el: $(this).find('.toolbar-menu-administration').get(0), model: menuModel, strings: options.strings }); // Handle the resolution of Drupal.toolbar.setSubtrees. // This is handled with a deferred so that the function may be invoked // asynchronously. Drupal.toolbar.setSubtrees.done(function (subtrees) { menuModel.set('subtrees', subtrees); var theme = drupalSettings.ajaxPageState.theme; localStorage.setItem('Drupal.toolbar.subtrees.' + theme, JSON.stringify(subtrees)); // Indicate on the toolbarModel that subtrees are now loaded. model.set('areSubtreesLoaded', true); }); // Attach a listener to the configured media query breakpoints. for (var label in options.breakpoints) { if (options.breakpoints.hasOwnProperty(label)) { var mq = options.breakpoints[label]; var mql = Drupal.toolbar.mql[label] = window.matchMedia(mq); // Curry the model and the label of the media query breakpoint to // the mediaQueryChangeHandler function. mql.addListener(Drupal.toolbar.mediaQueryChangeHandler.bind(null, model, label)); // Fire the mediaQueryChangeHandler for each configured breakpoint // so that they process once. Drupal.toolbar.mediaQueryChangeHandler.call(null, model, label, mql); } } // Trigger an initial attempt to load menu subitems. This first attempt // is made after the media query handlers have had an opportunity to // process. The toolbar starts in the vertical orientation by default, // unless the viewport is wide enough to accommodate a horizontal // orientation. Thus we give the Toolbar a chance to determine if it // should be set to horizontal orientation before attempting to load // menu subtrees. Drupal.toolbar.views.toolbarVisualView.loadSubtrees(); $(document) // Update the model when the viewport offset changes. .on('drupalViewportOffsetChange.toolbar', function (event, offsets) { model.set('offsets', offsets); }); // Broadcast model changes to other modules. model .on('change:orientation', function (model, orientation) { $(document).trigger('drupalToolbarOrientationChange', orientation); }) .on('change:activeTab', function (model, tab) { $(document).trigger('drupalToolbarTabChange', tab); }) .on('change:activeTray', function (model, tray) { $(document).trigger('drupalToolbarTrayChange', tray); }); // If the toolbar's orientation is horizontal and no active tab is // defined then show the tray of the first toolbar tab by default (but // not the first 'Home' toolbar tab). if (Drupal.toolbar.models.toolbarModel.get('orientation') === 'horizontal' && Drupal.toolbar.models.toolbarModel.get('activeTab') === null) { Drupal.toolbar.models.toolbarModel.set({ activeTab: $('.toolbar-bar .toolbar-tab:not(.home-toolbar-tab) a').get(0) }); } }); } }; /** * Toolbar methods of Backbone objects. * * @namespace */ Drupal.toolbar = { /** * A hash of View instances. * * @type {object.} */ views: {}, /** * A hash of Model instances. * * @type {object.} */ models: {}, /** * A hash of MediaQueryList objects tracked by the toolbar. * * @type {object.} */ mql: {}, /** * Accepts a list of subtree menu elements. * * A deferred object that is resolved by an inlined JavaScript callback. * * @type {jQuery.Deferred} * * @see toolbar_subtrees_jsonp(). */ setSubtrees: new $.Deferred(), /** * Respond to configured narrow media query changes. * * @param {Drupal.toolbar.ToolbarModel} model * A toolbar model * @param {string} label * Media query label. * @param {object} mql * A MediaQueryList object. */ mediaQueryChangeHandler: function (model, label, mql) { switch (label) { case 'toolbar.narrow': model.set({ isOriented: mql.matches, isTrayToggleVisible: false }); // If the toolbar doesn't have an explicit orientation yet, or if the // narrow media query doesn't match then set the orientation to // vertical. if (!mql.matches || !model.get('orientation')) { model.set({orientation: 'vertical'}, {validate: true}); } break; case 'toolbar.standard': model.set({ isFixed: mql.matches }); break; case 'toolbar.wide': model.set({ orientation: ((mql.matches) ? 'horizontal' : 'vertical') }, {validate: true}); // The tray orientation toggle visibility does not need to be // validated. model.set({ isTrayToggleVisible: mql.matches }); break; default: break; } } }; /** * 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 '
' + '' + '
'; }; /** * Ajax command to set the toolbar subtrees. * * @param {Drupal.Ajax} ajax * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. * @param {object} response * JSON response from the Ajax request. * @param {number} [status] * XMLHttpRequest status. */ Drupal.AjaxCommands.prototype.setToolbarSubtrees = function (ajax, response, status) { Drupal.toolbar.setSubtrees.resolve(response.subtrees); }; }(jQuery, Drupal, drupalSettings)); ; /** * @file * A Backbone Model for collapsible menus. */ (function (Backbone, Drupal) { 'use strict'; /** * Backbone Model for collapsible menus. * * @constructor * * @augments Backbone.Model */ Drupal.toolbar.MenuModel = Backbone.Model.extend(/** @lends Drupal.toolbar.MenuModel# */{ /** * @type {object} * * @prop {object} subtrees */ defaults: /** @lends Drupal.toolbar.MenuModel# */{ /** * @type {object} */ subtrees: {} } }); }(Backbone, Drupal)); ; /** * @file * A Backbone Model for the toolbar. */ (function (Backbone, Drupal) { 'use strict'; /** * Backbone model for the toolbar. * * @constructor * * @augments Backbone.Model */ Drupal.toolbar.ToolbarModel = Backbone.Model.extend(/** @lends Drupal.toolbar.ToolbarModel# */{ /** * @type {object} * * @prop activeTab * @prop activeTray * @prop isOriented * @prop isFixed * @prop areSubtreesLoaded * @prop isViewportOverflowConstrained * @prop orientation * @prop locked * @prop isTrayToggleVisible * @prop height * @prop offsets */ defaults: /** @lends Drupal.toolbar.ToolbarModel# */{ /** * The active toolbar tab. All other tabs should be inactive under * normal circumstances. It will remain active across page loads. The * active item is stored as an ID selector e.g. '#toolbar-item--1'. * * @type {string} */ activeTab: null, /** * Represents whether a tray is open or not. Stored as an ID selector e.g. * '#toolbar-item--1-tray'. * * @type {string} */ activeTray: null, /** * Indicates whether the toolbar is displayed in an oriented fashion, * either horizontal or vertical. * * @type {bool} */ isOriented: false, /** * Indicates whether the toolbar is positioned absolute (false) or fixed * (true). * * @type {bool} */ isFixed: false, /** * Menu subtrees are loaded through an AJAX request only when the Toolbar * is set to a vertical orientation. * * @type {bool} */ areSubtreesLoaded: false, /** * If the viewport overflow becomes constrained, isFixed must be true so * that elements in the trays aren't lost off-screen and impossible to * get to. * * @type {bool} */ isViewportOverflowConstrained: false, /** * The orientation of the active tray. * * @type {string} */ orientation: 'vertical', /** * A tray is locked if a user toggled it to vertical. Otherwise a tray * will switch between vertical and horizontal orientation based on the * configured breakpoints. The locked state will be maintained across page * loads. * * @type {bool} */ locked: false, /** * Indicates whether the tray orientation toggle is visible. * * @type {bool} */ isTrayToggleVisible: false, /** * The height of the toolbar. * * @type {number} */ height: null, /** * The current viewport offsets determined by {@link Drupal.displace}. The * offsets suggest how a module might position is components relative to * the viewport. * * @type {object} * * @prop {number} top * @prop {number} right * @prop {number} bottom * @prop {number} left */ offsets: { top: 0, right: 0, bottom: 0, left: 0 } }, /** * @inheritdoc * * @param {object} attributes * Attributes for the toolbar. * @param {object} options * Options for the toolbar. * * @return {string|undefined} * Returns an error message if validation failed. */ validate: function (attributes, options) { // Prevent the orientation being set to horizontal if it is locked, unless // override has not been passed as an option. if (attributes.orientation === 'horizontal' && this.get('locked') && !options.override) { return Drupal.t('The toolbar cannot be set to a horizontal orientation when it is locked.'); } } }); }(Backbone, Drupal)); ; /** * @file * A Backbone view for the body element. */ (function ($, Drupal, Backbone) { 'use strict'; Drupal.toolbar.BodyVisualView = Backbone.View.extend(/** @lends Drupal.toolbar.BodyVisualView# */{ /** * Adjusts the body element with the toolbar position and dimension changes. * * @constructs * * @augments Backbone.View */ initialize: function () { this.listenTo(this.model, 'change:orientation change:offsets change:activeTray change:isOriented change:isFixed change:isViewportOverflowConstrained', this.render); }, /** * @inheritdoc */ render: function () { var $body = $('body'); var orientation = this.model.get('orientation'); var isOriented = this.model.get('isOriented'); var isViewportOverflowConstrained = this.model.get('isViewportOverflowConstrained'); $body // We are using JavaScript to control media-query handling for two // reasons: (1) Using JavaScript let's us leverage the breakpoint // configurations and (2) the CSS is really complex if we try to hide // some styling from browsers that don't understand CSS media queries. // If we drive the CSS from classes added through JavaScript, // then the CSS becomes simpler and more robust. .toggleClass('toolbar-vertical', (orientation === 'vertical')) .toggleClass('toolbar-horizontal', (isOriented && orientation === 'horizontal')) // When the toolbar is fixed, it will not scroll with page scrolling. .toggleClass('toolbar-fixed', (isViewportOverflowConstrained || this.model.get('isFixed'))) // Toggle the toolbar-tray-open class on the body element. The class is // applied when a toolbar tray is active. Padding might be applied to // the body element to prevent the tray from overlapping content. .toggleClass('toolbar-tray-open', !!this.model.get('activeTray')) // Apply padding to the top of the body to offset the placement of the // toolbar bar element. .css('padding-top', this.model.get('offsets').top); } }); }(jQuery, Drupal, Backbone)); ; /** * @file * A Backbone view for the collapsible menus. */ (function ($, Backbone, Drupal) { 'use strict'; Drupal.toolbar.MenuVisualView = Backbone.View.extend(/** @lends Drupal.toolbar.MenuVisualView# */{ /** * Backbone View for collapsible menus. * * @constructs * * @augments Backbone.View */ initialize: function () { this.listenTo(this.model, 'change:subtrees', this.render); }, /** * @inheritdoc */ render: function () { var subtrees = this.model.get('subtrees'); // Add subtrees. for (var id in subtrees) { if (subtrees.hasOwnProperty(id)) { this.$el .find('#toolbar-link-' + id) .once('toolbar-subtrees') .after(subtrees[id]); } } // Render the main menu as a nested, collapsible accordion. if ('drupalToolbarMenu' in $.fn) { this.$el .children('.toolbar-menu') .drupalToolbarMenu(); } } }); }(jQuery, Backbone, Drupal)); ; /** * @file * A Backbone view for the aural feedback of the toolbar. */ (function (Backbone, Drupal) { 'use strict'; Drupal.toolbar.ToolbarAuralView = Backbone.View.extend(/** @lends Drupal.toolbar.ToolbarAuralView# */{ /** * Backbone view for the aural feedback of the toolbar. * * @constructs * * @augments Backbone.View * * @param {object} options * Options for the view. * @param {object} options.strings * Various strings to use in the view. */ initialize: function (options) { this.strings = options.strings; this.listenTo(this.model, 'change:orientation', this.onOrientationChange); this.listenTo(this.model, 'change:activeTray', this.onActiveTrayChange); }, /** * Announces an orientation change. * * @param {Drupal.toolbar.ToolbarModel} model * The toolbar model in question. * @param {string} orientation * The new value of the orientation attribute in the model. */ onOrientationChange: function (model, orientation) { Drupal.announce(Drupal.t('Tray orientation changed to @orientation.', { '@orientation': orientation })); }, /** * Announces a changed active tray. * * @param {Drupal.toolbar.ToolbarModel} model * The toolbar model in question. * @param {HTMLElement} tray * The new value of the tray attribute in the model. */ onActiveTrayChange: function (model, tray) { var relevantTray = (tray === null) ? model.previous('activeTray') : tray; var action = (tray === null) ? Drupal.t('closed') : Drupal.t('opened'); var trayNameElement = relevantTray.querySelector('.toolbar-tray-name'); var text; if (trayNameElement !== null) { text = Drupal.t('Tray "@tray" @action.', { '@tray': trayNameElement.textContent, '@action': action }); } else { text = Drupal.t('Tray @action.', {'@action': action}); } Drupal.announce(text); } }); }(Backbone, Drupal)); ; /** * @file * A Backbone view for the toolbar element. Listens to mouse & touch. */ (function ($, Drupal, drupalSettings, Backbone) { 'use strict'; Drupal.toolbar.ToolbarVisualView = Backbone.View.extend(/** @lends Drupal.toolbar.ToolbarVisualView# */{ /** * Event map for the `ToolbarVisualView`. * * @return {object} * A map of events. */ events: function () { // Prevents delay and simulated mouse events. var touchEndToClick = function (event) { event.preventDefault(); event.target.click(); }; return { 'click .toolbar-bar .toolbar-tab .trigger': 'onTabClick', 'click .toolbar-toggle-orientation button': 'onOrientationToggleClick', 'touchend .toolbar-bar .toolbar-tab .trigger': touchEndToClick, 'touchend .toolbar-toggle-orientation button': touchEndToClick }; }, /** * Backbone view for the toolbar element. Listens to mouse & touch. * * @constructs * * @augments Backbone.View * * @param {object} options * Options for the view object. * @param {object} options.strings * Various strings to use in the view. */ initialize: function (options) { this.strings = options.strings; this.listenTo(this.model, 'change:activeTab change:orientation change:isOriented change:isTrayToggleVisible', this.render); this.listenTo(this.model, 'change:mqMatches', this.onMediaQueryChange); this.listenTo(this.model, 'change:offsets', this.adjustPlacement); // Add the tray orientation toggles. this.$el .find('.toolbar-tray .toolbar-lining') .append(Drupal.theme('toolbarOrientationToggle')); // Trigger an activeTab change so that listening scripts can respond on // page load. This will call render. this.model.trigger('change:activeTab'); }, /** * @inheritdoc * * @return {Drupal.toolbar.ToolbarVisualView} * The `ToolbarVisualView` instance. */ render: function () { this.updateTabs(); this.updateTrayOrientation(); this.updateBarAttributes(); // Load the subtrees if the orientation of the toolbar is changed to // vertical. This condition responds to the case that the toolbar switches // from horizontal to vertical orientation. The toolbar starts in a // vertical orientation by default and then switches to horizontal during // initialization if the media query conditions are met. Simply checking // that the orientation is vertical here would result in the subtrees // always being loaded, even when the toolbar initialization ultimately // results in a horizontal orientation. // // @see Drupal.behaviors.toolbar.attach() where admin menu subtrees // loading is invoked during initialization after media query conditions // have been processed. if (this.model.changed.orientation === 'vertical' || this.model.changed.activeTab) { this.loadSubtrees(); } // Trigger a recalculation of viewport displacing elements. Use setTimeout // to ensure this recalculation happens after changes to visual elements // have processed. window.setTimeout(function () { Drupal.displace(true); }, 0); return this; }, /** * Responds to a toolbar tab click. * * @param {jQuery.Event} event * The event triggered. */ onTabClick: function (event) { // If this tab has a tray associated with it, it is considered an // activatable tab. if (event.target.hasAttribute('data-toolbar-tray')) { var activeTab = this.model.get('activeTab'); var clickedTab = event.target; // Set the event target as the active item if it is not already. this.model.set('activeTab', (!activeTab || clickedTab !== activeTab) ? clickedTab : null); event.preventDefault(); event.stopPropagation(); } }, /** * Toggles the orientation of a toolbar tray. * * @param {jQuery.Event} event * The event triggered. */ onOrientationToggleClick: function (event) { var orientation = this.model.get('orientation'); // Determine the toggle-to orientation. var antiOrientation = (orientation === 'vertical') ? 'horizontal' : 'vertical'; var locked = antiOrientation === 'vertical'; // Remember the locked state. if (locked) { localStorage.setItem('Drupal.toolbar.trayVerticalLocked', 'true'); } else { localStorage.removeItem('Drupal.toolbar.trayVerticalLocked'); } // Update the model. this.model.set({ locked: locked, orientation: antiOrientation }, { validate: true, override: true }); event.preventDefault(); event.stopPropagation(); }, /** * Updates the display of the tabs: toggles a tab and the associated tray. */ updateTabs: function () { var $tab = $(this.model.get('activeTab')); // Deactivate the previous tab. $(this.model.previous('activeTab')) .removeClass('is-active') .prop('aria-pressed', false); // Deactivate the previous tray. $(this.model.previous('activeTray')) .removeClass('is-active'); // Activate the selected tab. if ($tab.length > 0) { $tab .addClass('is-active') // Mark the tab as pressed. .prop('aria-pressed', true); var name = $tab.attr('data-toolbar-tray'); // Store the active tab name or remove the setting. var id = $tab.get(0).id; if (id) { localStorage.setItem('Drupal.toolbar.activeTabID', JSON.stringify(id)); } // Activate the associated tray. var $tray = this.$el.find('[data-toolbar-tray="' + name + '"].toolbar-tray'); if ($tray.length) { $tray.addClass('is-active'); this.model.set('activeTray', $tray.get(0)); } else { // There is no active tray. this.model.set('activeTray', null); } } else { // There is no active tray. this.model.set('activeTray', null); localStorage.removeItem('Drupal.toolbar.activeTabID'); } }, /** * Update the attributes of the toolbar bar element. */ updateBarAttributes: function () { var isOriented = this.model.get('isOriented'); if (isOriented) { this.$el.find('.toolbar-bar').attr('data-offset-top', ''); } else { this.$el.find('.toolbar-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. this.$el.toggleClass('toolbar-oriented', isOriented); }, /** * Updates the orientation of the active tray if necessary. */ updateTrayOrientation: function () { var orientation = this.model.get('orientation'); // The antiOrientation is used to render the view of action buttons like // the tray orientation toggle. var antiOrientation = (orientation === 'vertical') ? 'horizontal' : 'vertical'; // Update the orientation of the trays. var $trays = this.$el.find('.toolbar-tray') .removeClass('toolbar-tray-horizontal toolbar-tray-vertical') .addClass('toolbar-tray-' + orientation); // Update the tray orientation toggle button. var iconClass = 'toolbar-icon-toggle-' + orientation; var iconAntiClass = 'toolbar-icon-toggle-' + antiOrientation; var $orientationToggle = this.$el.find('.toolbar-toggle-orientation') .toggle(this.model.get('isTrayToggleVisible')); $orientationToggle.find('button') .val(antiOrientation) .attr('title', this.strings[antiOrientation]) .text(this.strings[antiOrientation]) .removeClass(iconClass) .addClass(iconAntiClass); // Update data offset attributes for the trays. var dir = document.documentElement.dir; var edge = (dir === 'rtl') ? 'right' : 'left'; // Remove data-offset attributes from the trays so they can be refreshed. $trays.removeAttr('data-offset-left data-offset-right data-offset-top'); // If an active vertical tray exists, mark it as an offset element. $trays.filter('.toolbar-tray-vertical.is-active').attr('data-offset-' + edge, ''); // If an active horizontal tray exists, mark it as an offset element. $trays.filter('.toolbar-tray-horizontal.is-active').attr('data-offset-top', ''); }, /** * Sets the tops of the trays so that they align with the bottom of the bar. */ adjustPlacement: function () { var $trays = this.$el.find('.toolbar-tray'); if (!this.model.get('isOriented')) { $trays.css('margin-top', 0); $trays.removeClass('toolbar-tray-horizontal').addClass('toolbar-tray-vertical'); } else { // The toolbar container is invisible. Its placement is used to // determine the container for the trays. $trays.css('margin-top', this.$el.find('.toolbar-bar').outerHeight()); } }, /** * Calls the endpoint URI that builds an AJAX command with the rendered * subtrees. * * The rendered admin menu subtrees HTML is cached on the client in * localStorage until the cache of the admin menu subtrees on the server- * side is invalidated. The subtreesHash is stored in localStorage as well * and compared to the subtreesHash in drupalSettings to determine when the * admin menu subtrees cache has been invalidated. */ loadSubtrees: function () { var $activeTab = $(this.model.get('activeTab')); var orientation = this.model.get('orientation'); // Only load and render the admin menu subtrees if: // (1) They have not been loaded yet. // (2) The active tab is the administration menu tab, indicated by the // presence of the data-drupal-subtrees attribute. // (3) The orientation of the tray is vertical. if (!this.model.get('areSubtreesLoaded') && typeof $activeTab.data('drupal-subtrees') !== 'undefined' && orientation === 'vertical') { var subtreesHash = drupalSettings.toolbar.subtreesHash; var theme = drupalSettings.ajaxPageState.theme; var endpoint = Drupal.url('toolbar/subtrees/' + subtreesHash); var cachedSubtreesHash = localStorage.getItem('Drupal.toolbar.subtreesHash.' + theme); var cachedSubtrees = JSON.parse(localStorage.getItem('Drupal.toolbar.subtrees.' + theme)); var isVertical = this.model.get('orientation') === 'vertical'; // If we have the subtrees in localStorage and the subtree hash has not // changed, then use the cached data. if (isVertical && subtreesHash === cachedSubtreesHash && cachedSubtrees) { Drupal.toolbar.setSubtrees.resolve(cachedSubtrees); } // Only make the call to get the subtrees if the orientation of the // toolbar is vertical. else if (isVertical) { // Remove the cached menu information. localStorage.removeItem('Drupal.toolbar.subtreesHash.' + theme); localStorage.removeItem('Drupal.toolbar.subtrees.' + theme); // The AJAX response's command will trigger the resolve method of the // Drupal.toolbar.setSubtrees Promise. Drupal.ajax({url: endpoint}).execute(); // Cache the hash for the subtrees locally. localStorage.setItem('Drupal.toolbar.subtreesHash.' + theme, subtreesHash); } } } }); }(jQuery, Drupal, drupalSettings, Backbone)); ; /*! jquery.cookie v1.4.1 | MIT */ !function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof exports?a(require("jquery")):a(jQuery)}(function(a){function b(a){return h.raw?a:encodeURIComponent(a)}function c(a){return h.raw?a:decodeURIComponent(a)}function d(a){return b(h.json?JSON.stringify(a):String(a))}function e(a){0===a.indexOf('"')&&(a=a.slice(1,-1).replace(/\\"/g,'"').replace(/\\\\/g,"\\"));try{return a=decodeURIComponent(a.replace(g," ")),h.json?JSON.parse(a):a}catch(b){}}function f(b,c){var d=h.raw?b:e(b);return a.isFunction(c)?c(d):d}var g=/\+/g,h=a.cookie=function(e,g,i){if(void 0!==g&&!a.isFunction(g)){if(i=a.extend({},h.defaults,i),"number"==typeof i.expires){var j=i.expires,k=i.expires=new Date;k.setTime(+k+864e5*j)}return document.cookie=[b(e),"=",d(g),i.expires?"; expires="+i.expires.toUTCString():"",i.path?"; path="+i.path:"",i.domain?"; domain="+i.domain:"",i.secure?"; secure":""].join("")}for(var l=e?void 0:{},m=document.cookie?document.cookie.split("; "):[],n=0,o=m.length;o>n;n++){var p=m[n].split("="),q=c(p.shift()),r=p.join("=");if(e&&e===q){l=f(r,g);break}e||void 0===(r=f(r))||(l[q]=r)}return l};h.defaults={},a.removeCookie=function(b,c){return void 0===a.cookie(b)?!1:(a.cookie(b,"",a.extend({},c,{expires:-1})),!a.cookie(b))}});; /* jQuery Foundation Joyride Plugin 2.1 | Copyright 2012, ZURB | www.opensource.org/licenses/mit-license.php */ (function(e,t,n){"use strict";var r={version:"2.0.3",tipLocation:"bottom",nubPosition:"auto",scroll:!0,scrollSpeed:300,timer:0,autoStart:!1,startTimerOnClick:!0,startOffset:0,nextButton:!0,tipAnimation:"fade",pauseAfter:[],tipAnimationFadeSpeed:300,cookieMonster:!1,cookieName:"joyride",cookieDomain:!1,cookiePath:!1,localStorage:!1,localStorageKey:"joyride",tipContainer:"body",modal:!1,expose:!1,postExposeCallback:e.noop,preRideCallback:e.noop,postRideCallback:e.noop,preStepCallback:e.noop,postStepCallback:e.noop,template:{link:'X',timer:'
',tip:'
',wrapper:'',button:'',modal:'
',expose:'
',exposeCover:'
'}},i=i||!1,s={},o={init:function(n){return this.each(function(){e.isEmptyObject(s)?(s=e.extend(!0,r,n),s.document=t.document,s.$document=e(s.document),s.$window=e(t),s.$content_el=e(this),s.$body=e(s.tipContainer),s.body_offset=e(s.tipContainer).position(),s.$tip_content=e("> li",s.$content_el),s.paused=!1,s.attempts=0,s.tipLocationPatterns={top:["bottom"],bottom:[],left:["right","top","bottom"],right:["left","top","bottom"]},o.jquery_check(),e.isFunction(e.cookie)||(s.cookieMonster=!1),(!s.cookieMonster||!e.cookie(s.cookieName))&&(!s.localStorage||!o.support_localstorage()||!localStorage.getItem(s.localStorageKey))&&(s.$tip_content.each(function(t){o.create({$li:e(this),index:t})}),s.autoStart&&(!s.startTimerOnClick&&s.timer>0?(o.show("init"),o.startTimer()):o.show("init"))),s.$document.on("click.joyride",".joyride-next-tip, .joyride-modal-bg",function(e){e.preventDefault(),s.$li.next().length<1?o.end():s.timer>0?(clearTimeout(s.automate),o.hide(),o.show(),o.startTimer()):(o.hide(),o.show())}),s.$document.on("click.joyride",".joyride-close-tip",function(e){e.preventDefault(),o.end()}),s.$window.bind("resize.joyride",function(t){if(s.$li){if(s.exposed&&s.exposed.length>0){var n=e(s.exposed);n.each(function(){var t=e(this);o.un_expose(t),o.expose(t)})}o.is_phone()?o.pos_phone():o.pos_default()}})):o.restart()})},resume:function(){o.set_li(),o.show()},nextTip:function(){s.$li.next().length<1?o.end():s.timer>0?(clearTimeout(s.automate),o.hide(),o.show(),o.startTimer()):(o.hide(),o.show())},tip_template:function(t){var n,r,i;return t.tip_class=t.tip_class||"",n=e(s.template.tip).addClass(t.tip_class),r=e.trim(e(t.li).html())+o.button_text(t.button_text)+s.template.link+o.timer_instance(t.index),i=e(s.template.wrapper),t.li.attr("data-aria-labelledby")&&i.attr("aria-labelledby",t.li.attr("data-aria-labelledby")),t.li.attr("data-aria-describedby")&&i.attr("aria-describedby",t.li.attr("data-aria-describedby")),n.append(i),n.first().attr("data-index",t.index),e(".joyride-content-wrapper",n).append(r),n[0]},timer_instance:function(t){var n;return t===0&&s.startTimerOnClick&&s.timer>0||s.timer===0?n="":n=o.outerHTML(e(s.template.timer)[0]),n},button_text:function(t){return s.nextButton?(t=e.trim(t)||"Next",t=o.outerHTML(e(s.template.button).append(t)[0])):t="",t},create:function(t){var n=t.$li.attr("data-button")||t.$li.attr("data-text"),r=t.$li.attr("class"),i=e(o.tip_template({tip_class:r,index:t.index,button_text:n,li:t.$li}));e(s.tipContainer).append(i)},show:function(t){var r={},i,u=[],a=0,f,l=null;if(s.$li===n||e.inArray(s.$li.index(),s.pauseAfter)===-1){s.paused?s.paused=!1:o.set_li(t),s.attempts=0;if(s.$li.length&&s.$target.length>0){t&&(s.preRideCallback(s.$li.index(),s.$next_tip),s.modal&&o.show_modal()),s.preStepCallback(s.$li.index(),s.$next_tip),u=(s.$li.data("options")||":").split(";"),a=u.length;for(i=a-1;i>=0;i--)f=u[i].split(":"),f.length===2&&(r[e.trim(f[0])]=e.trim(f[1]));s.tipSettings=e.extend({},s,r),s.tipSettings.tipLocationPattern=s.tipLocationPatterns[s.tipSettings.tipLocation],s.modal&&s.expose&&o.expose(),!/body/i.test(s.$target.selector)&&s.scroll&&o.scroll_to(),o.is_phone()?o.pos_phone(!0):o.pos_default(!0),l=e(".joyride-timer-indicator",s.$next_tip),/pop/i.test(s.tipAnimation)?(l.outerWidth(0),s.timer>0?(s.$next_tip.show(),l.animate({width:e(".joyride-timer-indicator-wrap",s.$next_tip).outerWidth()},s.timer)):s.$next_tip.show()):/fade/i.test(s.tipAnimation)&&(l.outerWidth(0),s.timer>0?(s.$next_tip.fadeIn(s.tipAnimationFadeSpeed),s.$next_tip.show(),l.animate({width:e(".joyride-timer-indicator-wrap",s.$next_tip).outerWidth()},s.timer)):s.$next_tip.fadeIn(s.tipAnimationFadeSpeed)),s.$current_tip=s.$next_tip,e(".joyride-next-tip",s.$current_tip).focus(),o.tabbable(s.$current_tip)}else s.$li&&s.$target.length<1?o.show():o.end()}else s.paused=!0},is_phone:function(){return i?i.mq("only screen and (max-width: 767px)"):s.$window.width()<767?!0:!1},support_localstorage:function(){return i?i.localstorage:!!t.localStorage},hide:function(){s.modal&&s.expose&&o.un_expose(),s.modal||e(".joyride-modal-bg").hide(),s.$current_tip.hide(),s.postStepCallback(s.$li.index(),s.$current_tip)},set_li:function(e){e?(s.$li=s.$tip_content.eq(s.startOffset),o.set_next_tip(),s.$current_tip=s.$next_tip):(s.$li=s.$li.next(),o.set_next_tip()),o.set_target()},set_next_tip:function(){s.$next_tip=e(".joyride-tip-guide[data-index="+s.$li.index()+"]")},set_target:function(){var t=s.$li.attr("data-class"),n=s.$li.attr("data-id"),r=function(){return n?e(s.document.getElementById(n)):t?e("."+t).filter(":visible").first():e("body")};s.$target=r()},scroll_to:function(){var t,n;t=s.$window.height()/2,n=Math.ceil(s.$target.offset().top-t+s.$next_tip.outerHeight()),e("html, body").stop().animate({scrollTop:n},s.scrollSpeed)},paused:function(){return e.inArray(s.$li.index()+1,s.pauseAfter)===-1?!0:!1},destroy:function(){e.isEmptyObject(s)||s.$document.off(".joyride"),e(t).off(".joyride"),e(".joyride-close-tip, .joyride-next-tip, .joyride-modal-bg").off(".joyride"),e(".joyride-tip-guide, .joyride-modal-bg").remove(),clearTimeout(s.automate),s={}},restart:function(){s.autoStart?(o.hide(),s.$li=n,o.show("init")):(!s.startTimerOnClick&&s.timer>0?(o.show("init"),o.startTimer()):o.show("init"),s.autoStart=!0)},pos_default:function(t){var n=Math.ceil(s.$window.height()/2),r=s.$next_tip.offset(),i=e(".joyride-nub",s.$next_tip),u=Math.ceil(i.outerWidth()/2),a=Math.ceil(i.outerHeight()/2),f=t||!1;f&&(s.$next_tip.css("visibility","hidden"),s.$next_tip.show());if(!/body/i.test(s.$target.selector)){var l=s.tipSettings.tipAdjustmentY?parseInt(s.tipSettings.tipAdjustmentY):0,c=s.tipSettings.tipAdjustmentX?parseInt(s.tipSettings.tipAdjustmentX):0;o.bottom()?(s.$next_tip.css({top:s.$target.offset().top+a+s.$target.outerHeight()+l,left:s.$target.offset().left+c}),/right/i.test(s.tipSettings.nubPosition)&&s.$next_tip.css("left",s.$target.offset().left-s.$next_tip.outerWidth()+s.$target.outerWidth()),o.nub_position(i,s.tipSettings.nubPosition,"top")):o.top()?(s.$next_tip.css({top:s.$target.offset().top-s.$next_tip.outerHeight()-a+l,left:s.$target.offset().left+c}),o.nub_position(i,s.tipSettings.nubPosition,"bottom")):o.right()?(s.$next_tip.css({top:s.$target.offset().top+l,left:s.$target.outerWidth()+s.$target.offset().left+u+c}),o.nub_position(i,s.tipSettings.nubPosition,"left")):o.left()&&(s.$next_tip.css({top:s.$target.offset().top+l,left:s.$target.offset().left-s.$next_tip.outerWidth()-u+c}),o.nub_position(i,s.tipSettings.nubPosition,"right")),!o.visible(o.corners(s.$next_tip))&&s.attempts0&&arguments[0]instanceof e)i=arguments[0];else{if(!s.$target||!!/body/i.test(s.$target.selector))return!1;i=s.$target}if(i.length<1)return t.console&&console.error("element not valid",i),!1;n=e(s.template.expose),s.$body.append(n),n.css({top:i.offset().top,left:i.offset().left,width:i.outerWidth(!0),height:i.outerHeight(!0)}),r=e(s.template.exposeCover),u={zIndex:i.css("z-index"),position:i.css("position")},i.css("z-index",n.css("z-index")*1+1),u.position=="static"&&i.css("position","relative"),i.data("expose-css",u),r.css({top:i.offset().top,left:i.offset().left,width:i.outerWidth(!0),height:i.outerHeight(!0)}),s.$body.append(r),n.addClass(a),r.addClass(a),s.tipSettings.exposeClass&&(n.addClass(s.tipSettings.exposeClass),r.addClass(s.tipSettings.exposeClass)),i.data("expose",a),s.postExposeCallback(s.$li.index(),s.$next_tip,i),o.add_exposed(i)},un_expose:function(){var n,r,i,u,a=!1;if(arguments.length>0&&arguments[0]instanceof e)r=arguments[0];else{if(!s.$target||!!/body/i.test(s.$target.selector))return!1;r=s.$target}if(r.length<1)return t.console&&console.error("element not valid",r),!1;n=r.data("expose"),i=e("."+n),arguments.length>1&&(a=arguments[1]),a===!0?e(".joyride-expose-wrapper,.joyride-expose-cover").remove():i.remove(),u=r.data("expose-css"),u.zIndex=="auto"?r.css("z-index",""):r.css("z-index",u.zIndex),u.position!=r.css("position")&&(u.position=="static"?r.css("position",""):r.css("position",u.position)),r.removeData("expose"),r.removeData("expose-z-index"),o.remove_exposed(r)},add_exposed:function(t){s.exposed=s.exposed||[],t instanceof e?s.exposed.push(t[0]):typeof t=="string"&&s.exposed.push(t)},remove_exposed:function(t){var n;t instanceof e?n=t[0]:typeof t=="string"&&(n=t),s.exposed=s.exposed||[];for(var r=0;ru&&(u=o),[e.offset().tope.offset().left]},visible:function(e){var t=e.length;while(t--)if(e[t])return!1;return!0},nub_position:function(e,t,n){t==="auto"?e.addClass(n):e.addClass(t)},startTimer:function(){s.$li.length?s.automate=setTimeout(function(){o.hide(),o.show(),o.startTimer()},s.timer):clearTimeout(s.automate)},end:function(){s.cookieMonster&&e.cookie(s.cookieName,"ridden",{expires:365,domain:s.cookieDomain,path:s.cookiePath}),s.localStorage&&localStorage.setItem(s.localStorageKey,!0),s.timer>0&&clearTimeout(s.automate),s.modal&&s.expose&&o.un_expose(),s.$current_tip&&s.$current_tip.hide(),s.$li&&(s.postStepCallback(s.$li.index(),s.$current_tip),s.postRideCallback(s.$li.index(),s.$current_tip)),e(".joyride-modal-bg").hide()},jquery_check:function(){return e.isFunction(e.fn.on)?!0:(e.fn.on=function(e,t,n){return this.delegate(t,e,n)},e.fn.off=function(e,t,n){return this.undelegate(t,e,n)},!1)},outerHTML:function(e){return e.outerHTML||(new XMLSerializer).serializeToString(e)},version:function(){return s.version},tabbable:function(t){e(t).on("keydown",function(n){if(!n.isDefaultPrevented()&&n.keyCode&&n.keyCode===27){n.preventDefault(),o.end();return}if(n.keyCode!==9)return;var r=e(t).find(":tabbable"),i=r.filter(":first"),s=r.filter(":last");n.target===s[0]&&!n.shiftKey?(i.focus(1),n.preventDefault()):n.target===i[0]&&n.shiftKey&&(s.focus(1),n.preventDefault())})}};e.fn.joyride=function(t){if(o[t])return o[t].apply(this,Array.prototype.slice.call(arguments,1));if(typeof t=="object"||!t)return o.init.apply(this,arguments);e.error("Method "+t+" does not exist on jQuery.joyride")}})(jQuery,this); ; /** * @file * Attaches behaviors for the Tour module's toolbar tab. */ (function ($, Backbone, Drupal, document) { 'use strict'; var queryString = decodeURI(window.location.search); /** * Attaches the tour's toolbar tab behavior. * * It uses the query string for: * - tour: When ?tour=1 is present, the tour will start automatically after * the page has loaded. * - tips: Pass ?tips=class in the url to filter the available tips to the * subset which match the given class. * * @example * http://example.com/foo?tour=1&tips=bar * * @type {Drupal~behavior} * * @prop {Drupal~behaviorAttach} attach * Attach tour functionality on `tour` events. */ Drupal.behaviors.tour = { attach: function (context) { $('body').once('tour').each(function () { var model = new Drupal.tour.models.StateModel(); new Drupal.tour.views.ToggleTourView({ el: $(context).find('#toolbar-tab-tour'), model: model }); model // Allow other scripts to respond to tour events. .on('change:isActive', function (model, isActive) { $(document).trigger((isActive) ? 'drupalTourStarted' : 'drupalTourStopped'); }) // Initialization: check whether a tour is available on the current // page. .set('tour', $(context).find('ol#tour')); // Start the tour immediately if toggled via query string. if (/tour=?/i.test(queryString)) { model.set('isActive', true); } }); } }; /** * @namespace */ Drupal.tour = Drupal.tour || { /** * @namespace Drupal.tour.models */ models: {}, /** * @namespace Drupal.tour.views */ views: {} }; /** * Backbone Model for tours. * * @constructor * * @augments Backbone.Model */ Drupal.tour.models.StateModel = Backbone.Model.extend(/** @lends Drupal.tour.models.StateModel# */{ /** * @type {object} */ defaults: /** @lends Drupal.tour.models.StateModel# */{ /** * Indicates whether the Drupal root window has a tour. * * @type {Array} */ tour: [], /** * Indicates whether the tour is currently running. * * @type {bool} */ isActive: false, /** * Indicates which tour is the active one (necessary to cleanly stop). * * @type {Array} */ activeTour: [] } }); Drupal.tour.views.ToggleTourView = Backbone.View.extend(/** @lends Drupal.tour.views.ToggleTourView# */{ /** * @type {object} */ events: {click: 'onClick'}, /** * Handles edit mode toggle interactions. * * @constructs * * @augments Backbone.View */ initialize: function () { this.listenTo(this.model, 'change:tour change:isActive', this.render); this.listenTo(this.model, 'change:isActive', this.toggleTour); }, /** * @inheritdoc * * @return {Drupal.tour.views.ToggleTourView} * The `ToggleTourView` view. */ render: function () { // Render the visibility. this.$el.toggleClass('hidden', this._getTour().length === 0); // Render the state. var isActive = this.model.get('isActive'); this.$el.find('button') .toggleClass('is-active', isActive) .prop('aria-pressed', isActive); return this; }, /** * Model change handler; starts or stops the tour. */ toggleTour: function () { if (this.model.get('isActive')) { var $tour = this._getTour(); this._removeIrrelevantTourItems($tour, this._getDocument()); var that = this; if ($tour.find('li').length) { $tour.joyride({ autoStart: true, postRideCallback: function () { that.model.set('isActive', false); }, // HTML segments for tip layout. template: { link: '×', button: '' } }); this.model.set({isActive: true, activeTour: $tour}); } } else { this.model.get('activeTour').joyride('destroy'); this.model.set({isActive: false, activeTour: []}); } }, /** * Toolbar tab click event handler; toggles isActive. * * @param {jQuery.Event} event * The click event. */ onClick: function (event) { this.model.set('isActive', !this.model.get('isActive')); event.preventDefault(); event.stopPropagation(); }, /** * Gets the tour. * * @return {jQuery} * A jQuery element pointing to a `
    ` containing tour items. */ _getTour: function () { return this.model.get('tour'); }, /** * Gets the relevant document as a jQuery element. * * @return {jQuery} * A jQuery element pointing to the document within which a tour would be * started given the current state. */ _getDocument: function () { return $(document); }, /** * Removes tour items for elements that don't have matching page elements. * * Or that are explicitly filtered out via the 'tips' query string. * * @example * This will filter out tips that do not have a matching * page element or don't have the "bar" class. * http://example.com/foo?tips=bar * * @param {jQuery} $tour * A jQuery element pointing to a `
      ` containing tour items. * @param {jQuery} $document * A jQuery element pointing to the document within which the elements * should be sought. * * @see Drupal.tour.views.ToggleTourView#_getDocument */ _removeIrrelevantTourItems: function ($tour, $document) { var removals = false; var tips = /tips=([^&]+)/.exec(queryString); $tour .find('li') .each(function () { var $this = $(this); var itemId = $this.attr('data-id'); var itemClass = $this.attr('data-class'); // If the query parameter 'tips' is set, remove all tips that don't // have the matching class. if (tips && !$(this).hasClass(tips[1])) { removals = true; $this.remove(); return; } // Remove tip from the DOM if there is no corresponding page element. if ((!itemId && !itemClass) || (itemId && $document.find('#' + itemId).length) || (itemClass && $document.find('.' + itemClass).length)) { return; } removals = true; $this.remove(); }); // If there were removals, we'll have to do some clean-up. if (removals) { var total = $tour.find('li').length; if (!total) { this.model.set({tour: []}); } $tour .find('li') // Rebuild the progress data. .each(function (index) { var progress = Drupal.t('!tour_item of !total', {'!tour_item': index + 1, '!total': total}); $(this).find('.tour-progress').text(progress); }) // Update the last item to have "End tour" as the button. .eq(-1) .attr('data-text', Drupal.t('End tour')); } } }); })(jQuery, Backbone, Drupal, document); ; /** * @file * Manages page tabbing modifications made by modules. */ /** * Allow modules to respond to the constrain event. * * @event drupalTabbingConstrained */ /** * Allow modules to respond to the tabbingContext release event. * * @event drupalTabbingContextReleased */ /** * Allow modules to respond to the constrain event. * * @event drupalTabbingContextActivated */ /** * Allow modules to respond to the constrain event. * * @event drupalTabbingContextDeactivated */ (function ($, Drupal) { 'use strict'; /** * Provides an API for managing page tabbing order modifications. * * @constructor Drupal~TabbingManager */ function TabbingManager() { /** * Tabbing sets are stored as a stack. The active set is at the top of the * stack. We use a JavaScript array as if it were a stack; we consider the * first element to be the bottom and the last element to be the top. This * allows us to use JavaScript's built-in Array.push() and Array.pop() * methods. * * @type {Array.} */ this.stack = []; } /** * Add public methods to the TabbingManager class. */ $.extend(TabbingManager.prototype, /** @lends Drupal~TabbingManager# */{ /** * Constrain tabbing to the specified set of elements only. * * Makes elements outside of the specified set of elements unreachable via * the tab key. * * @param {jQuery} elements * The set of elements to which tabbing should be constrained. Can also * be a jQuery-compatible selector string. * * @return {Drupal~TabbingContext} * The TabbingContext instance. * * @fires event:drupalTabbingConstrained */ constrain: function (elements) { // Deactivate all tabbingContexts to prepare for the new constraint. A // tabbingContext instance will only be reactivated if the stack is // unwound to it in the _unwindStack() method. var il = this.stack.length; for (var i = 0; i < il; i++) { this.stack[i].deactivate(); } // The "active tabbing set" are the elements tabbing should be constrained // to. var $elements = $(elements).find(':tabbable').addBack(':tabbable'); var tabbingContext = new TabbingContext({ // The level is the current height of the stack before this new // tabbingContext is pushed on top of the stack. level: this.stack.length, $tabbableElements: $elements }); this.stack.push(tabbingContext); // Activates the tabbingContext; this will manipulate the DOM to constrain // tabbing. tabbingContext.activate(); // Allow modules to respond to the constrain event. $(document).trigger('drupalTabbingConstrained', tabbingContext); return tabbingContext; }, /** * Restores a former tabbingContext when an active one is released. * * The TabbingManager stack of tabbingContext instances will be unwound * from the top-most released tabbingContext down to the first non-released * tabbingContext instance. This non-released instance is then activated. */ release: function () { // Unwind as far as possible: find the topmost non-released // tabbingContext. var toActivate = this.stack.length - 1; while (toActivate >= 0 && this.stack[toActivate].released) { toActivate--; } // Delete all tabbingContexts after the to be activated one. They have // already been deactivated, so their effect on the DOM has been reversed. this.stack.splice(toActivate + 1); // Get topmost tabbingContext, if one exists, and activate it. if (toActivate >= 0) { this.stack[toActivate].activate(); } }, /** * Makes all elements outside of the tabbingContext's set untabbable. * * Elements made untabbable have their original tabindex and autofocus * values stored so that they might be restored later when this * tabbingContext is deactivated. * * @param {Drupal~TabbingContext} tabbingContext * The TabbingContext instance that has been activated. */ activate: function (tabbingContext) { var $set = tabbingContext.$tabbableElements; var level = tabbingContext.level; // Determine which elements are reachable via tabbing by default. var $disabledSet = $(':tabbable') // Exclude elements of the active tabbing set. .not($set); // Set the disabled set on the tabbingContext. tabbingContext.$disabledElements = $disabledSet; // Record the tabindex for each element, so we can restore it later. var il = $disabledSet.length; for (var i = 0; i < il; i++) { this.recordTabindex($disabledSet.eq(i), level); } // Make all tabbable elements outside of the active tabbing set // unreachable. $disabledSet .prop('tabindex', -1) .prop('autofocus', false); // Set focus on an element in the tabbingContext's set of tabbable // elements. First, check if there is an element with an autofocus // attribute. Select the last one from the DOM order. var $hasFocus = $set.filter('[autofocus]').eq(-1); // If no element in the tabbable set has an autofocus attribute, select // the first element in the set. if ($hasFocus.length === 0) { $hasFocus = $set.eq(0); } $hasFocus.trigger('focus'); }, /** * Restores that tabbable state of a tabbingContext's disabled elements. * * Elements that were made untabbable have their original tabindex and * autofocus values restored. * * @param {Drupal~TabbingContext} tabbingContext * The TabbingContext instance that has been deactivated. */ deactivate: function (tabbingContext) { var $set = tabbingContext.$disabledElements; var level = tabbingContext.level; var il = $set.length; for (var i = 0; i < il; i++) { this.restoreTabindex($set.eq(i), level); } }, /** * Records the tabindex and autofocus values of an untabbable element. * * @param {jQuery} $el * The set of elements that have been disabled. * @param {number} level * The stack level for which the tabindex attribute should be recorded. */ recordTabindex: function ($el, level) { var tabInfo = $el.data('drupalOriginalTabIndices') || {}; tabInfo[level] = { tabindex: $el[0].getAttribute('tabindex'), autofocus: $el[0].hasAttribute('autofocus') }; $el.data('drupalOriginalTabIndices', tabInfo); }, /** * Restores the tabindex and autofocus values of a reactivated element. * * @param {jQuery} $el * The element that is being reactivated. * @param {number} level * The stack level for which the tabindex attribute should be restored. */ restoreTabindex: function ($el, level) { var tabInfo = $el.data('drupalOriginalTabIndices'); if (tabInfo && tabInfo[level]) { var data = tabInfo[level]; if (data.tabindex) { $el[0].setAttribute('tabindex', data.tabindex); } // If the element did not have a tabindex at this stack level then // remove it. else { $el[0].removeAttribute('tabindex'); } if (data.autofocus) { $el[0].setAttribute('autofocus', 'autofocus'); } // Clean up $.data. if (level === 0) { // Remove all data. $el.removeData('drupalOriginalTabIndices'); } else { // Remove the data for this stack level and higher. var levelToDelete = level; while (tabInfo.hasOwnProperty(levelToDelete)) { delete tabInfo[levelToDelete]; levelToDelete++; } $el.data('drupalOriginalTabIndices', tabInfo); } } } }); /** * Stores a set of tabbable elements. * * This constraint can be removed with the release() method. * * @constructor Drupal~TabbingContext * * @param {object} options * A set of initiating values * @param {number} options.level * The level in the TabbingManager's stack of this tabbingContext. * @param {jQuery} options.$tabbableElements * The DOM elements that should be reachable via the tab key when this * tabbingContext is active. * @param {jQuery} options.$disabledElements * The DOM elements that should not be reachable via the tab key when this * tabbingContext is active. * @param {bool} options.released * A released tabbingContext can never be activated again. It will be * cleaned up when the TabbingManager unwinds its stack. * @param {bool} options.active * When true, the tabbable elements of this tabbingContext will be reachable * via the tab key and the disabled elements will not. Only one * tabbingContext can be active at a time. */ function TabbingContext(options) { $.extend(this, /** @lends Drupal~TabbingContext# */{ /** * @type {?number} */ level: null, /** * @type {jQuery} */ $tabbableElements: $(), /** * @type {jQuery} */ $disabledElements: $(), /** * @type {bool} */ released: false, /** * @type {bool} */ active: false }, options); } /** * Add public methods to the TabbingContext class. */ $.extend(TabbingContext.prototype, /** @lends Drupal~TabbingContext# */{ /** * Releases this TabbingContext. * * Once a TabbingContext object is released, it can never be activated * again. * * @fires event:drupalTabbingContextReleased */ release: function () { if (!this.released) { this.deactivate(); this.released = true; Drupal.tabbingManager.release(this); // Allow modules to respond to the tabbingContext release event. $(document).trigger('drupalTabbingContextReleased', this); } }, /** * Activates this TabbingContext. * * @fires event:drupalTabbingContextActivated */ activate: function () { // A released TabbingContext object can never be activated again. if (!this.active && !this.released) { this.active = true; Drupal.tabbingManager.activate(this); // Allow modules to respond to the constrain event. $(document).trigger('drupalTabbingContextActivated', this); } }, /** * Deactivates this TabbingContext. * * @fires event:drupalTabbingContextDeactivated */ deactivate: function () { if (this.active) { this.active = false; Drupal.tabbingManager.deactivate(this); // Allow modules to respond to the constrain event. $(document).trigger('drupalTabbingContextDeactivated', this); } } }); // Mark this behavior as processed on the first pass and return if it is // already processed. if (Drupal.tabbingManager) { return; } /** * @type {Drupal~TabbingManager} */ Drupal.tabbingManager = new TabbingManager(); }(jQuery, Drupal)); ; /** * @file * Attaches behaviors for the Contextual module's edit toolbar tab. */ (function ($, Drupal, Backbone) { 'use strict'; var strings = { tabbingReleased: Drupal.t('Tabbing is no longer constrained by the Contextual module.'), tabbingConstrained: Drupal.t('Tabbing is constrained to a set of @contextualsCount and the edit mode toggle.'), pressEsc: Drupal.t('Press the esc key to exit.') }; /** * Initializes a contextual link: updates its DOM, sets up model and views. * * @param {HTMLElement} context * A contextual links DOM element as rendered by the server. */ function initContextualToolbar(context) { if (!Drupal.contextual || !Drupal.contextual.collection) { return; } var contextualToolbar = Drupal.contextualToolbar; var model = contextualToolbar.model = new contextualToolbar.StateModel({ // Checks whether localStorage indicates we should start in edit mode // rather than view mode. // @see Drupal.contextualToolbar.VisualView.persist isViewing: localStorage.getItem('Drupal.contextualToolbar.isViewing') !== 'false' }, { contextualCollection: Drupal.contextual.collection }); var viewOptions = { el: $('.toolbar .toolbar-bar .contextual-toolbar-tab'), model: model, strings: strings }; new contextualToolbar.VisualView(viewOptions); new contextualToolbar.AuralView(viewOptions); } /** * Attaches contextual's edit toolbar tab behavior. * * @type {Drupal~behavior} * * @prop {Drupal~behaviorAttach} attach * Attaches contextual toolbar behavior on a contextualToolbar-init event. */ Drupal.behaviors.contextualToolbar = { attach: function (context) { if ($('body').once('contextualToolbar-init').length) { initContextualToolbar(context); } } }; /** * Namespace for the contextual toolbar. * * @namespace */ Drupal.contextualToolbar = { /** * The {@link Drupal.contextualToolbar.StateModel} instance. * * @type {?Drupal.contextualToolbar.StateModel} */ model: null }; })(jQuery, Drupal, Backbone); ; /** * @file * A Backbone Model for the state of Contextual module's edit toolbar tab. */ (function (Drupal, Backbone) { 'use strict'; Drupal.contextualToolbar.StateModel = Backbone.Model.extend(/** @lends Drupal.contextualToolbar.StateModel# */{ /** * @type {object} * * @prop {bool} isViewing * @prop {bool} isVisible * @prop {number} contextualCount * @prop {Drupal~TabbingContext} tabbingContext */ defaults: /** @lends Drupal.contextualToolbar.StateModel# */{ /** * Indicates whether the toggle is currently in "view" or "edit" mode. * * @type {bool} */ isViewing: true, /** * Indicates whether the toggle should be visible or hidden. Automatically * calculated, depends on contextualCount. * * @type {bool} */ isVisible: false, /** * Tracks how many contextual links exist on the page. * * @type {number} */ contextualCount: 0, /** * A TabbingContext object as returned by {@link Drupal~TabbingManager}: * the set of tabbable elements when edit mode is enabled. * * @type {?Drupal~TabbingContext} */ tabbingContext: null }, /** * Models the state of the edit mode toggle. * * @constructs * * @augments Backbone.Model * * @param {object} attrs * Attributes for the backbone model. * @param {object} options * An object with the following option: * @param {Backbone.collection} options.contextualCollection * The collection of {@link Drupal.contextual.StateModel} models that * represent the contextual links on the page. */ initialize: function (attrs, options) { // Respond to new/removed contextual links. this.listenTo(options.contextualCollection, 'reset remove add', this.countContextualLinks); this.listenTo(options.contextualCollection, 'add', this.lockNewContextualLinks); // Automatically determine visibility. this.listenTo(this, 'change:contextualCount', this.updateVisibility); // Whenever edit mode is toggled, lock all contextual links. this.listenTo(this, 'change:isViewing', function (model, isViewing) { options.contextualCollection.each(function (contextualModel) { contextualModel.set('isLocked', !isViewing); }); }); }, /** * Tracks the number of contextual link models in the collection. * * @param {Drupal.contextual.StateModel} contextualModel * The contextual links model that was added or removed. * @param {Backbone.Collection} contextualCollection * The collection of contextual link models. */ countContextualLinks: function (contextualModel, contextualCollection) { this.set('contextualCount', contextualCollection.length); }, /** * Lock newly added contextual links if edit mode is enabled. * * @param {Drupal.contextual.StateModel} contextualModel * The contextual links model that was added. * @param {Backbone.Collection} [contextualCollection] * The collection of contextual link models. */ lockNewContextualLinks: function (contextualModel, contextualCollection) { if (!this.get('isViewing')) { contextualModel.set('isLocked', true); } }, /** * Automatically updates visibility of the view/edit mode toggle. */ updateVisibility: function () { this.set('isVisible', this.get('contextualCount') > 0); } }); })(Drupal, Backbone); ; /** * @file * A Backbone View that provides the aural view of the edit mode toggle. */ (function ($, Drupal, Backbone, _) { 'use strict'; Drupal.contextualToolbar.AuralView = Backbone.View.extend(/** @lends Drupal.contextualToolbar.AuralView# */{ /** * Tracks whether the tabbing constraint announcement has been read once. * * @type {bool} */ announcedOnce: false, /** * Renders the aural view of the edit mode toggle (screen reader support). * * @constructs * * @augments Backbone.View * * @param {object} options * Options for the view. */ initialize: function (options) { this.options = options; this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'change:isViewing', this.manageTabbing); $(document).on('keyup', _.bind(this.onKeypress, this)); }, /** * @inheritdoc * * @return {Drupal.contextualToolbar.AuralView} * The current contextual toolbar aural view. */ render: function () { // Render the state. this.$el.find('button').attr('aria-pressed', !this.model.get('isViewing')); return this; }, /** * Limits tabbing to the contextual links and edit mode toolbar tab. */ manageTabbing: function () { var tabbingContext = this.model.get('tabbingContext'); // Always release an existing tabbing context. if (tabbingContext) { tabbingContext.release(); Drupal.announce(this.options.strings.tabbingReleased); } // Create a new tabbing context when edit mode is enabled. if (!this.model.get('isViewing')) { tabbingContext = Drupal.tabbingManager.constrain($('.contextual-toolbar-tab, .contextual')); this.model.set('tabbingContext', tabbingContext); this.announceTabbingConstraint(); this.announcedOnce = true; } }, /** * Announces the current tabbing constraint. */ announceTabbingConstraint: function () { var strings = this.options.strings; Drupal.announce(Drupal.formatString(strings.tabbingConstrained, { '@contextualsCount': Drupal.formatPlural(Drupal.contextual.collection.length, '@count contextual link', '@count contextual links') })); Drupal.announce(strings.pressEsc); }, /** * Responds to esc and tab key press events. * * @param {jQuery.Event} event * The keypress event. */ onKeypress: function (event) { // The first tab key press is tracked so that an annoucement about tabbing // constraints can be raised if edit mode is enabled when the page is // loaded. if (!this.announcedOnce && event.keyCode === 9 && !this.model.get('isViewing')) { this.announceTabbingConstraint(); // Set announce to true so that this conditional block won't run again. this.announcedOnce = true; } // Respond to the ESC key. Exit out of edit mode. if (event.keyCode === 27) { this.model.set('isViewing', true); } } }); })(jQuery, Drupal, Backbone, _); ; /** * @file * A Backbone View that provides the visual view of the edit mode toggle. */ (function (Drupal, Backbone) { 'use strict'; Drupal.contextualToolbar.VisualView = Backbone.View.extend(/** @lends Drupal.contextualToolbar.VisualView# */{ /** * Events for the Backbone view. * * @return {object} * A mapping of events to be used in the view. */ events: function () { // Prevents delay and simulated mouse events. var touchEndToClick = function (event) { event.preventDefault(); event.target.click(); }; return { click: function () { this.model.set('isViewing', !this.model.get('isViewing')); }, touchend: touchEndToClick }; }, /** * Renders the visual view of the edit mode toggle. * * Listens to mouse & touch and handles edit mode toggle interactions. * * @constructs * * @augments Backbone.View */ initialize: function () { this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'change:isViewing', this.persist); }, /** * @inheritdoc * * @return {Drupal.contextualToolbar.VisualView} * The current contextual toolbar visual view. */ render: function () { // Render the visibility. this.$el.toggleClass('hidden', !this.model.get('isVisible')); // Render the state. this.$el.find('button').toggleClass('is-active', !this.model.get('isViewing')); return this; }, /** * Model change handler; persists the isViewing value to localStorage. * * `isViewing === true` is the default, so only stores in localStorage when * it's not the default value (i.e. false). * * @param {Drupal.contextualToolbar.StateModel} model * A {@link Drupal.contextualToolbar.StateModel} model. * @param {bool} isViewing * The value of the isViewing attribute in the model. */ persist: function (model, isViewing) { if (!isViewing) { localStorage.setItem('Drupal.contextualToolbar.isViewing', 'false'); } else { localStorage.removeItem('Drupal.contextualToolbar.isViewing'); } } }); })(Drupal, Backbone); ; /** * @file * Replaces the home link in toolbar with a back to site link. */ (function ($, Drupal, drupalSettings) { 'use strict'; var pathInfo = drupalSettings.path; var escapeAdminPath = sessionStorage.getItem('escapeAdminPath'); var windowLocation = window.location; // Saves the last non-administrative page in the browser to be able to link // back to it when browsing administrative pages. If there is a destination // parameter there is not need to save the current path because the page is // loaded within an existing "workflow". if (!pathInfo.currentPathIsAdmin && !/destination=/.test(windowLocation.search)) { sessionStorage.setItem('escapeAdminPath', windowLocation); } /** * Replaces the "Home" link with "Back to site" link. * * Back to site link points to the last non-administrative page the user * visited within the same browser tab. * * @type {Drupal~behavior} * * @prop {Drupal~behaviorAttach} attach * Attaches the replacement functionality to the toolbar-escape-admin element. */ Drupal.behaviors.escapeAdmin = { attach: function () { var $toolbarEscape = $('[data-toolbar-escape-admin]').once('escapeAdmin'); if ($toolbarEscape.length && pathInfo.currentPathIsAdmin) { if (escapeAdminPath !== null) { $toolbarEscape.attr('href', escapeAdminPath); } else { $toolbarEscape.text(Drupal.t('Home')); } $toolbarEscape.closest('.toolbar-tab').removeClass('hidden'); } } }; })(jQuery, Drupal, drupalSettings); ;