upgrades core to 8.4.2

This commit is contained in:
Bachir Soussi Chiadmi
2017-11-14 16:09:54 +01:00
parent bd60eff9b3
commit c2b4e25be4
3504 changed files with 140306 additions and 38684 deletions
@@ -219,7 +219,6 @@
background-color: #f5f5f5;
}
/**
* Handle.
*/
@@ -280,8 +279,7 @@
background-image: url(../../../misc/icons/bebebe/push-left.svg); /* LTR */
}
.toolbar .toolbar-toggle-orientation [value="vertical"]:hover:before,
.toolbar .toolbar-toggle-orientation [value="vertical"]:focus:before
{
.toolbar .toolbar-toggle-orientation [value="vertical"]:focus:before {
background-image: url(../../../misc/icons/787878/push-left.svg); /* LTR */
}
[dir="rtl"] .toolbar .toolbar-toggle-orientation [value="vertical"]:before {
+17 -2
View File
@@ -13,11 +13,26 @@
position: relative;
width: auto;
}
.toolbar .toolbar-tray-horizontal .toolbar-menu .toolbar-handle,
.toolbar .toolbar-tray-horizontal .toolbar-menu ul,
/**
* Hidden vertical toolbar sub-menus by default.
*/
.toolbar .toolbar-tray-vertical .toolbar-menu ul {
display: none;
}
/**
* Hidden horizontal toolbar handle icon.
*/
.toolbar .toolbar-tray-horizontal .toolbar-menu .toolbar-handle {
display: none;
}
/**
* Hidden toolbar sub-menus by default.
*/
.toolbar-tray-open .toolbar-menu .menu-item--expanded ul {
display: none;
}
.toolbar .toolbar-tray-vertical li.open > ul {
display: block; /* Show the sub-menus */
}
+49 -13
View File
@@ -21,7 +21,9 @@
display: none;
}
}
.toolbar-loading #toolbar-administration {
overflow: hidden;
}
/**
* Very specific overrides for Drupal system CSS.
*/
@@ -56,18 +58,44 @@
position: relative;
z-index: 1250;
}
.toolbar-horizontal .toolbar-tray {
position: fixed;
width: 100%;
left: 0;
}
/* Position the admin toolbar absolutely when the configured standard breakpoint
* is active. The toolbar container, that contains the bar and the trays, is
* position absolutely so that it scrolls with the page. Otherwise, on smaller
* screens, the components of the admin toolbar are positioned statically. */
body.toolbar-fixed .toolbar-oriented,
.toolbar-oriented .toolbar-bar,
.toolbar-oriented .toolbar-tray {
.toolbar-oriented .toolbar-bar {
left: 0;
position: absolute;
right: 0;
top: 0;
}
.toolbar-oriented .toolbar-tray {
left: 0;
position: absolute;
right: 0;
}
/* .toolbar-loading is required by toolbar JavaScript to pre-render markup
* style to avoid extra reflow & flicker. */
@media (min-width: 61em) {
.toolbar-loading.toolbar-horizontal .toolbar .toolbar-bar .toolbar-tab:last-child .toolbar-tray {
position: relative;
display: block;
z-index: -999;
visibility: hidden;
width: 1px;
}
.toolbar-loading.toolbar-horizontal .toolbar .toolbar-bar .toolbar-tab:last-child .toolbar-tray .toolbar-lining {
width: 999em;
}
.toolbar-loading.toolbar-horizontal .toolbar .toolbar-bar .home-toolbar-tab + .toolbar-tab .toolbar-tray {
display: block;
}
}
/* Layer the bar just above the trays and above contextual link triggers. */
.toolbar-oriented .toolbar-bar {
z-index: 502;
@@ -85,13 +113,16 @@ body.toolbar-tray-open.toolbar-fixed.toolbar-vertical .toolbar-oriented {
width: 240px;
width: 15rem;
}
/* Present the admin toolbar tabs horizontally as a default on user agents that
* do not understand media queries or on user agents where JavaScript is
* disabled. */
.toolbar-loading.toolbar-horizontal .toolbar .toolbar-tray .toolbar-menu > li,
.toolbar .toolbar-bar .toolbar-tab,
.toolbar .toolbar-tray-horizontal li {
float: left; /* LTR */
}
[dir="rtl"] .toolbar-loading.toolbar-horizontal .toolbar .toolbar-tray .toolbar-menu > li,
[dir="rtl"] .toolbar .toolbar-bar .toolbar-tab,
[dir="rtl"] .toolbar .toolbar-tray-horizontal li {
float: right;
@@ -111,7 +142,7 @@ body.toolbar-tray-open.toolbar-fixed.toolbar-vertical .toolbar-oriented {
/* This min-width media query is meant to provide basic horizontal layout to
* the main menu tabs when JavaScript is disabled on user agents that understand
* media queries. */
@media (min-width:16.5em) {
@media (min-width: 16.5em) {
.toolbar .toolbar-bar .toolbar-tab,
.toolbar .toolbar-tray-horizontal li {
float: left; /* LTR */
@@ -171,7 +202,7 @@ body.toolbar-tray-open.toolbar-fixed.toolbar-vertical .toolbar-oriented {
right: 0;
}
/* Layer the links just above the toolbar-tray. */
.toolbar .toolbar-bar .toolbar-tab > .toolbar-icon{
.toolbar .toolbar-bar .toolbar-tab > .toolbar-icon {
position: relative;
z-index: 502;
}
@@ -179,15 +210,10 @@ body.toolbar-tray-open.toolbar-fixed.toolbar-vertical .toolbar-oriented {
.toolbar-oriented .toolbar-tray-horizontal .menu-item ul {
display: none;
}
/* When the configured standard breakpoint is active and the tray is in a
* horizontal position, the tray is fixed to the top of the viewport and does
* not scroll with the page contents. */
body.toolbar-fixed .toolbar .toolbar-tray-horizontal {
position: fixed;
}
/* When the configured standard breakpoint is active and the tray is in a
* vertical position, the tray does not scroll with the page. The contents of
* the tray scroll within the confines of the viewport. */
* the tray scroll within the confines of the viewport.
*/
.toolbar .toolbar-tray-vertical.is-active,
body.toolbar-fixed .toolbar .toolbar-tray-vertical {
height: 100%;
@@ -258,3 +284,13 @@ body.toolbar-tray-open.toolbar-vertical.toolbar-fixed {
[dir="rtl"] .toolbar-oriented .toolbar-tray-vertical .toolbar-toggle-orientation {
float: left;
}
/**
* Toolbar home button toggle.
*/
.toolbar .toolbar-bar .home-toolbar-tab {
display: none;
}
.path-admin .toolbar-bar .home-toolbar-tab {
display: block;
}
+14 -14
View File
@@ -20,7 +20,8 @@
line-height: 1em;
text-decoration: none;
}
.toolbar .toolbar-item:hover, .toolbar .toolbar-item:focus {
.toolbar .toolbar-item:hover,
.toolbar .toolbar-item:focus {
text-decoration: underline;
}
@@ -57,10 +58,10 @@
.toolbar .toolbar-tray {
background-color: #ffffff;
}
.toolbar .toolbar-tray-horizontal > .toolbar-lining {
.toolbar-horizontal .toolbar-tray > .toolbar-lining {
padding-right: 5em; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-tray-horizontal > .toolbar-lining {
[dir="rtl"] .toolbar-horizontal .toolbar-tray > .toolbar-lining {
padding-right: 0;
padding-left: 5em;
}
@@ -74,11 +75,11 @@
border-right: 0 none;
box-shadow: 1px 0 5px 2px rgba(0, 0, 0, 0.3333);
}
.toolbar .toolbar-tray-horizontal {
.toolbar-horizontal .toolbar-tray {
border-bottom: 1px solid #aaaaaa;
box-shadow: -2px 1px 3px 1px rgba(0, 0, 0, 0.3333); /* LTR */
}
[dir="rtl"] .toolbar .toolbar-tray-horizontal {
[dir="rtl"] .toolbar-horizontal .toolbar-tray {
box-shadow: 2px 1px 3px 1px rgba(0, 0, 0, 0.3333);
}
.toolbar .toolbar-tray-horizontal .toolbar-tray {
@@ -93,25 +94,24 @@
.toolbar-tray a:hover,
.toolbar-tray a:active,
.toolbar-tray a:focus,
.toolbar-tray a.is-active
{
.toolbar-tray a.is-active {
color: #000;
text-decoration: underline;
}
.toolbar .toolbar-menu {
background-color: #ffffff;
}
.toolbar .toolbar-tray-horizontal .menu-item + .menu-item {
.toolbar-horizontal .toolbar-tray .menu-item + .menu-item {
border-left: 1px solid #dddddd; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-tray-horizontal .menu-item + .menu-item {
border-left: 0 none ;
[dir="rtl"] .toolbar-horizontal .toolbar-tray .menu-item + .menu-item {
border-left: 0 none;
border-right: 1px solid #dddddd;
}
.toolbar .toolbar-tray-horizontal .menu-item:last-child {
.toolbar-horizontal .toolbar-tray .menu-item:last-child {
border-right: 1px solid #dddddd; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-tray-horizontal .menu-item:last-child {
[dir="rtl"] .toolbar-horizontal .toolbar-tray .menu-item:last-child {
border-left: 1px solid #dddddd;
}
.toolbar .toolbar-tray-vertical .menu-item + .menu-item {
@@ -149,10 +149,10 @@
padding: 0;
height: 100%;
}
.toolbar .toolbar-tray-horizontal .toolbar-toggle-orientation {
.toolbar-horizontal .toolbar-tray .toolbar-toggle-orientation {
border-left: 1px solid #c9c9c9; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-tray-horizontal .toolbar-toggle-orientation {
[dir="rtl"] .toolbar-horizontal .toolbar-tray .toolbar-toggle-orientation {
border-left: 0 none;
border-right: 1px solid #c9c9c9;
}
@@ -0,0 +1,43 @@
/**
* @file
* Replaces the home link in toolbar with a back to site link.
*/
(function ($, Drupal, drupalSettings) {
const pathInfo = drupalSettings.path;
const escapeAdminPath = sessionStorage.getItem('escapeAdminPath');
const 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() {
const $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'));
}
}
},
};
}(jQuery, Drupal, drupalSettings));
+8 -27
View File
@@ -1,48 +1,29 @@
/**
* @file
* Replaces the home link in toolbar with a back to site link.
*/
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/2815083
* @preserve
**/
(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 () {
attach: function attach() {
var $toolbarEscape = $('[data-toolbar-escape-admin]').once('escapeAdmin');
if ($toolbarEscape.length && pathInfo.currentPathIsAdmin) {
if (escapeAdminPath !== null) {
$toolbarEscape.attr('href', escapeAdminPath);
}
else {
} else {
$toolbarEscape.text(Drupal.t('Home'));
}
$toolbarEscape.closest('.toolbar-tab').removeClass('hidden');
}
}
};
})(jQuery, Drupal, drupalSettings);
})(jQuery, Drupal, drupalSettings);
@@ -0,0 +1,29 @@
/**
* @file
* A Backbone Model for collapsible menus.
*/
(function (Backbone, Drupal) {
/**
* 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));
+8 -27
View File
@@ -1,33 +1,14 @@
/**
* @file
* A Backbone Model for collapsible menus.
*/
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/2815083
* @preserve
**/
(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}
*/
Drupal.toolbar.MenuModel = Backbone.Model.extend({
defaults: {
subtrees: {}
}
});
}(Backbone, Drupal));
})(Backbone, Drupal);
@@ -0,0 +1,153 @@
/**
* @file
* A Backbone Model for the toolbar.
*/
(function (Backbone, Drupal) {
/**
* 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: 'horizontal',
/**
* 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: true,
/**
* 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(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));
+11 -123
View File
@@ -1,131 +1,33 @@
/**
* @file
* A Backbone Model for the toolbar.
*/
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/2815083
* @preserve
**/
(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}
*/
Drupal.toolbar.ToolbarModel = Backbone.Model.extend({
defaults: {
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',
orientation: 'horizontal',
/**
* 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,
isTrayToggleVisible: true,
/**
* 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,
@@ -134,24 +36,10 @@
}
},
/**
* @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.
validate: function validate(attributes, options) {
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));
})(Backbone, Drupal);
+260
View File
@@ -0,0 +1,260 @@
/**
* @file
* Defines the behavior of the Drupal administration toolbar.
*/
(function ($, Drupal, drupalSettings) {
// Merge run-time settings with the defaults.
const 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(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.
const model = Drupal.toolbar.models.toolbarModel = new Drupal.toolbar.ToolbarModel({
locked: JSON.parse(localStorage.getItem('Drupal.toolbar.trayVerticalLocked')),
activeTab: document.getElementById(JSON.parse(localStorage.getItem('Drupal.toolbar.activeTabID'))),
height: $('#toolbar-administration').outerHeight(),
});
// Attach a listener to the configured media query breakpoints.
// Executes it before Drupal.toolbar.views to avoid extra rendering.
for (const label in options.breakpoints) {
if (options.breakpoints.hasOwnProperty(label)) {
const mq = options.breakpoints[label];
const 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);
}
}
Drupal.toolbar.views.toolbarVisualView = new Drupal.toolbar.ToolbarVisualView({
el: this,
model,
strings: options.strings,
});
Drupal.toolbar.views.toolbarAuralView = new Drupal.toolbar.ToolbarAuralView({
el: this,
model,
strings: options.strings,
});
Drupal.toolbar.views.bodyVisualView = new Drupal.toolbar.BodyVisualView({
el: this,
model,
});
// Force layout render to fix mobile view. Only needed on load, not
// for every media query match.
model.trigger('change:isFixed', model, model.get('isFixed'));
model.trigger('change:activeTray', model, model.get('activeTray'));
// Render collapsible menus.
const 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((subtrees) => {
menuModel.set('subtrees', subtrees);
const 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);
});
// 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', (event, offsets) => {
model.set('offsets', offsets);
});
// Broadcast model changes to other modules.
model
.on('change:orientation', (model, orientation) => {
$(document).trigger('drupalToolbarOrientationChange', orientation);
})
.on('change:activeTab', (model, tab) => {
$(document).trigger('drupalToolbarTabChange', tab);
})
.on('change:activeTray', (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.<string, Backbone.View>}
*/
views: {},
/**
* A hash of Model instances.
*
* @type {object.<string, Backbone.Model>}
*/
models: {},
/**
* A hash of MediaQueryList objects tracked by the toolbar.
*
* @type {object.<string, 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(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 && !model.get('locked')) ? '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 '<div class="toolbar-toggle-orientation"><div class="toolbar-lining">' +
'<button class="toolbar-icon" type="button"></button>' +
'</div></div>';
};
/**
* 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));
+55 -159
View File
@@ -1,59 +1,48 @@
/**
* @file
* Defines the behavior of the Drupal administration toolbar.
*/
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/2815083
* @preserve
**/
(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')
}
var options = $.extend({
breakpoints: {
'toolbar.narrow': '',
'toolbar.standard': '',
'toolbar.wide': ''
}
);
}, drupalSettings.toolbar, {
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.
attach: function attach(context) {
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.
$(context).find('#toolbar-administration').once('toolbar').each(function () {
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')))
locked: JSON.parse(localStorage.getItem('Drupal.toolbar.trayVerticalLocked')),
activeTab: document.getElementById(JSON.parse(localStorage.getItem('Drupal.toolbar.activeTabID'))),
height: $('#toolbar-administration').outerHeight()
});
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);
mql.addListener(Drupal.toolbar.mediaQueryChangeHandler.bind(null, model, label));
Drupal.toolbar.mediaQueryChangeHandler.call(null, model, label, mql);
}
}
Drupal.toolbar.views.toolbarVisualView = new Drupal.toolbar.ToolbarVisualView({
el: this,
model: model,
@@ -69,7 +58,9 @@
model: model
});
// Render collapsible menus.
model.trigger('change:isFixed', model, model.get('isFixed'));
model.trigger('change:activeTray', model, model.get('activeTray'));
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),
@@ -77,61 +68,28 @@
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);
});
$(document).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);
});
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)
@@ -141,67 +99,25 @@
}
};
/**
* Toolbar methods of Backbone objects.
*
* @namespace
*/
Drupal.toolbar = {
/**
* A hash of View instances.
*
* @type {object.<string, Backbone.View>}
*/
views: {},
/**
* A hash of Model instances.
*
* @type {object.<string, Backbone.Model>}
*/
models: {},
/**
* A hash of MediaQueryList objects tracked by the toolbar.
*
* @type {object.<string, 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) {
mediaQueryChangeHandler: function mediaQueryChangeHandler(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});
model.set({ orientation: 'vertical' }, { validate: true });
}
break;
@@ -213,10 +129,9 @@
case 'toolbar.wide':
model.set({
orientation: ((mql.matches) ? 'horizontal' : 'vertical')
}, {validate: true});
// The tray orientation toggle visibility does not need to be
// validated.
orientation: mql.matches && !model.get('locked') ? 'horizontal' : 'vertical'
}, { validate: true });
model.set({
isTrayToggleVisible: mql.matches
});
@@ -228,30 +143,11 @@
}
};
/**
* 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 '<div class="toolbar-toggle-orientation"><div class="toolbar-lining">' +
'<button class="toolbar-icon" type="button"></button>' +
'</div></div>';
return '<div class="toolbar-toggle-orientation"><div class="toolbar-lining">' + '<button class="toolbar-icon" type="button"></button>' + '</div></div>';
};
/**
* 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));
})(jQuery, Drupal, drupalSettings);
+192
View File
@@ -0,0 +1,192 @@
/**
* @file
* Builds a nested accordion widget.
*
* Invoke on an HTML list element with the jQuery plugin pattern.
*
* @example
* $('.toolbar-menu').drupalToolbarMenu();
*/
(function ($, Drupal, drupalSettings) {
/**
* Store the open menu tray.
*/
let activeItem = Drupal.url(drupalSettings.path.currentPath);
$.fn.drupalToolbarMenu = function () {
const ui = {
handleOpen: Drupal.t('Extend'),
handleClose: Drupal.t('Collapse'),
};
/**
* Handle clicks from the disclosure button on an item with sub-items.
*
* @param {Object} event
* A jQuery Event object.
*/
function toggleClickHandler(event) {
const $toggle = $(event.target);
const $item = $toggle.closest('li');
// Toggle the list item.
toggleList($item);
// Close open sibling menus.
const $openItems = $item.siblings().filter('.open');
toggleList($openItems, false);
}
/**
* Handle clicks from a menu item link.
*
* @param {Object} event
* A jQuery Event object.
*/
function linkClickHandler(event) {
// If the toolbar is positioned fixed (and therefore hiding content
// underneath), then users expect clicks in the administration menu tray
// to take them to that destination but for the menu tray to be closed
// after clicking: otherwise the toolbar itself is obstructing the view
// of the destination they chose.
if (!Drupal.toolbar.models.toolbarModel.get('isFixed')) {
Drupal.toolbar.models.toolbarModel.set('activeTab', null);
}
// Stopping propagation to make sure that once a toolbar-box is clicked
// (the whitespace part), the page is not redirected anymore.
event.stopPropagation();
}
/**
* Toggle the open/close state of a list is a menu.
*
* @param {jQuery} $item
* The li item to be toggled.
*
* @param {Boolean} switcher
* A flag that forces toggleClass to add or a remove a class, rather than
* simply toggling its presence.
*/
function toggleList($item, switcher) {
const $toggle = $item.children('.toolbar-box').children('.toolbar-handle');
switcher = (typeof switcher !== 'undefined') ? switcher : !$item.hasClass('open');
// Toggle the item open state.
$item.toggleClass('open', switcher);
// Twist the toggle.
$toggle.toggleClass('open', switcher);
// Adjust the toggle text.
$toggle
.find('.action')
// Expand Structure, Collapse Structure.
.text((switcher) ? ui.handleClose : ui.handleOpen);
}
/**
* Add markup to the menu elements.
*
* Items with sub-elements have a list toggle attached to them. Menu item
* links and the corresponding list toggle are wrapped with in a div
* classed with .toolbar-box. The .toolbar-box div provides a positioning
* context for the item list toggle.
*
* @param {jQuery} $menu
* The root of the menu to be initialized.
*/
function initItems($menu) {
const options = {
class: 'toolbar-icon toolbar-handle',
action: ui.handleOpen,
text: '',
};
// Initialize items and their links.
$menu.find('li > a').wrap('<div class="toolbar-box">');
// Add a handle to each list item if it has a menu.
$menu.find('li').each((index, element) => {
const $item = $(element);
if ($item.children('ul.toolbar-menu').length) {
const $box = $item.children('.toolbar-box');
options.text = Drupal.t('@label', { '@label': $box.find('a').text() });
$item.children('.toolbar-box')
.append(Drupal.theme('toolbarMenuItemToggle', options));
}
});
}
/**
* Adds a level class to each list based on its depth in the menu.
*
* This function is called recursively on each sub level of lists elements
* until the depth of the menu is exhausted.
*
* @param {jQuery} $lists
* A jQuery object of ul elements.
*
* @param {number} level
* The current level number to be assigned to the list elements.
*/
function markListLevels($lists, level) {
level = (!level) ? 1 : level;
const $lis = $lists.children('li').addClass(`level-${level}`);
$lists = $lis.children('ul');
if ($lists.length) {
markListLevels($lists, level + 1);
}
}
/**
* On page load, open the active menu item.
*
* Marks the trail of the active link in the menu back to the root of the
* menu with .menu-item--active-trail.
*
* @param {jQuery} $menu
* The root of the menu.
*/
function openActiveItem($menu) {
const pathItem = $menu.find(`a[href="${location.pathname}"]`);
if (pathItem.length && !activeItem) {
activeItem = location.pathname;
}
if (activeItem) {
const $activeItem = $menu.find(`a[href="${activeItem}"]`).addClass('menu-item--active');
const $activeTrail = $activeItem.parentsUntil('.root', 'li').addClass('menu-item--active-trail');
toggleList($activeTrail, true);
}
}
// Return the jQuery object.
return this.each(function (selector) {
const $menu = $(this).once('toolbar-menu');
if ($menu.length) {
// Bind event handlers.
$menu
.on('click.toolbar', '.toolbar-box', toggleClickHandler)
.on('click.toolbar', '.toolbar-box a', linkClickHandler);
$menu.addClass('root');
initItems($menu);
markListLevels($menu);
// Restore previous and active states.
openActiveItem($menu);
}
});
};
/**
* A toggle is an interactive element often bound to a click handler.
*
* @param {object} options
* Options for the button.
* @param {string} options.class
* Class to set on the button.
* @param {string} options.action
* Action for the button.
* @param {string} options.text
* Used as label for the button.
*
* @return {string}
* A string representing a DOM fragment.
*/
Drupal.theme.toolbarMenuItemToggle = function (options) {
return `<button class="${options.class}"><span class="action">${options.action}</span><span class="label">${options.text}</span></button>`;
};
}(jQuery, Drupal, drupalSettings));
+22 -116
View File
@@ -1,134 +1,69 @@
/**
* @file
* Builds a nested accordion widget.
*
* Invoke on an HTML list element with the jQuery plugin pattern.
*
* @example
* $('.toolbar-menu').drupalToolbarMenu();
*/
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/2815083
* @preserve
**/
(function ($, Drupal, drupalSettings) {
'use strict';
/**
* Store the open menu tray.
*/
var activeItem = Drupal.url(drupalSettings.path.currentPath);
$.fn.drupalToolbarMenu = function () {
var ui = {
handleOpen: Drupal.t('Extend'),
handleClose: Drupal.t('Collapse')
};
/**
* Handle clicks from the disclosure button on an item with sub-items.
*
* @param {Object} event
* A jQuery Event object.
*/
function toggleClickHandler(event) {
var $toggle = $(event.target);
var $item = $toggle.closest('li');
// Toggle the list item.
toggleList($item);
// Close open sibling menus.
var $openItems = $item.siblings().filter('.open');
toggleList($openItems, false);
}
/**
* Handle clicks from a menu item link.
*
* @param {Object} event
* A jQuery Event object.
*/
function linkClickHandler(event) {
// If the toolbar is positioned fixed (and therefore hiding content
// underneath), then users expect clicks in the administration menu tray
// to take them to that destination but for the menu tray to be closed
// after clicking: otherwise the toolbar itself is obstructing the view
// of the destination they chose.
if (!Drupal.toolbar.models.toolbarModel.get('isFixed')) {
Drupal.toolbar.models.toolbarModel.set('activeTab', null);
}
// Stopping propagation to make sure that once a toolbar-box is clicked
// (the whitespace part), the page is not redirected anymore.
event.stopPropagation();
}
/**
* Toggle the open/close state of a list is a menu.
*
* @param {jQuery} $item
* The li item to be toggled.
*
* @param {Boolean} switcher
* A flag that forces toggleClass to add or a remove a class, rather than
* simply toggling its presence.
*/
function toggleList($item, switcher) {
var $toggle = $item.children('.toolbar-box').children('.toolbar-handle');
switcher = (typeof switcher !== 'undefined') ? switcher : !$item.hasClass('open');
// Toggle the item open state.
switcher = typeof switcher !== 'undefined' ? switcher : !$item.hasClass('open');
$item.toggleClass('open', switcher);
// Twist the toggle.
$toggle.toggleClass('open', switcher);
// Adjust the toggle text.
$toggle
.find('.action')
// Expand Structure, Collapse Structure.
.text((switcher) ? ui.handleClose : ui.handleOpen);
$toggle.find('.action').text(switcher ? ui.handleClose : ui.handleOpen);
}
/**
* Add markup to the menu elements.
*
* Items with sub-elements have a list toggle attached to them. Menu item
* links and the corresponding list toggle are wrapped with in a div
* classed with .toolbar-box. The .toolbar-box div provides a positioning
* context for the item list toggle.
*
* @param {jQuery} $menu
* The root of the menu to be initialized.
*/
function initItems($menu) {
var options = {
class: 'toolbar-icon toolbar-handle',
action: ui.handleOpen,
text: ''
};
// Initialize items and their links.
$menu.find('li > a').wrap('<div class="toolbar-box">');
// Add a handle to each list item if it has a menu.
$menu.find('li').each(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));
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;
level = !level ? 1 : level;
var $lis = $lists.children('li').addClass('level-' + level);
$lists = $lis.children('ul');
if ($lists.length) {
@@ -136,15 +71,6 @@
}
}
/**
* 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) {
@@ -157,41 +83,21 @@
}
}
// 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.on('click.toolbar', '.toolbar-box', toggleClickHandler).on('click.toolbar', '.toolbar-box a', linkClickHandler);
$menu.addClass('root');
initItems($menu);
markListLevels($menu);
// Restore previous and active states.
openActiveItem($menu);
}
});
};
/**
* A toggle is an interactive element often bound to a click handler.
*
* @param {object} options
* Options for the button.
* @param {string} options.class
* Class to set on the button.
* @param {string} options.action
* Action for the button.
* @param {string} options.text
* Used as label for the button.
*
* @return {string}
* A string representing a DOM fragment.
*/
Drupal.theme.toolbarMenuItemToggle = function (options) {
return '<button class="' + options['class'] + '"><span class="action">' + options.action + '</span><span class="label">' + options.text + '</span></button>';
return '<button class="' + options.class + '"><span class="action">' + options.action + '</span><span class="label">' + options.text + '</span></button>';
};
}(jQuery, Drupal, drupalSettings));
})(jQuery, Drupal, drupalSettings);
@@ -0,0 +1,37 @@
/**
* @file
* A Backbone view for the body element.
*/
(function ($, Drupal, Backbone) {
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() {
this.listenTo(this.model, 'change:activeTray ', this.render);
this.listenTo(this.model, 'change:isFixed change:isViewportOverflowConstrained', this.isToolbarFixed);
},
isToolbarFixed() {
// When the toolbar is fixed, it will not scroll with page scrolling.
const isViewportOverflowConstrained = this.model.get('isViewportOverflowConstrained');
$('body').toggleClass('toolbar-fixed', (isViewportOverflowConstrained || this.model.get('isFixed')));
},
/**
* @inheritdoc
*/
render() {
$('body')
// 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'));
},
});
}(jQuery, Drupal, Backbone));
+15 -46
View File
@@ -1,53 +1,22 @@
/**
* @file
* A Backbone view for the body element.
*/
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/2815083
* @preserve
**/
(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);
Drupal.toolbar.BodyVisualView = Backbone.View.extend({
initialize: function initialize() {
this.listenTo(this.model, 'change:activeTray ', this.render);
this.listenTo(this.model, 'change:isFixed change:isViewportOverflowConstrained', this.isToolbarFixed);
},
/**
* @inheritdoc
*/
render: function () {
var $body = $('body');
var orientation = this.model.get('orientation');
var isOriented = this.model.get('isOriented');
isToolbarFixed: function isToolbarFixed() {
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);
$('body').toggleClass('toolbar-fixed', isViewportOverflowConstrained || this.model.get('isFixed'));
},
render: function render() {
$('body').toggleClass('toolbar-tray-open', !!this.model.get('activeTray'));
}
});
}(jQuery, Drupal, Backbone));
})(jQuery, Drupal, Backbone);
@@ -0,0 +1,42 @@
/**
* @file
* A Backbone view for the collapsible menus.
*/
(function ($, Backbone, Drupal) {
Drupal.toolbar.MenuVisualView = Backbone.View.extend(/** @lends Drupal.toolbar.MenuVisualView# */{
/**
* Backbone View for collapsible menus.
*
* @constructs
*
* @augments Backbone.View
*/
initialize() {
this.listenTo(this.model, 'change:subtrees', this.render);
},
/**
* @inheritdoc
*/
render() {
const subtrees = this.model.get('subtrees');
// Add subtrees.
for (const 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));
+13 -32
View File
@@ -1,46 +1,27 @@
/**
* @file
* A Backbone view for the collapsible menus.
*/
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/2815083
* @preserve
**/
(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 () {
Drupal.toolbar.MenuVisualView = Backbone.View.extend({
initialize: function initialize() {
this.listenTo(this.model, 'change:subtrees', this.render);
},
/**
* @inheritdoc
*/
render: function () {
render: function render() {
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]);
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();
this.$el.children('.toolbar-menu').drupalToolbarMenu();
}
}
});
}(jQuery, Backbone, Drupal));
})(jQuery, Backbone, Drupal);
@@ -0,0 +1,71 @@
/**
* @file
* A Backbone view for the aural feedback of the toolbar.
*/
(function (Backbone, Drupal) {
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(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(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(model, tray) {
const relevantTray = (tray === null) ? model.previous('activeTray') : tray;
// Current activeTray and previous activeTray are empty, no state change
// to announce.
if (!relevantTray) {
return;
}
const action = (tray === null) ? Drupal.t('closed') : Drupal.t('opened');
const trayNameElement = relevantTray.querySelector('.toolbar-tray-name');
let 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));
@@ -1,70 +1,40 @@
/**
* @file
* A Backbone view for the aural feedback of the toolbar.
*/
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/2815083
* @preserve
**/
(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) {
Drupal.toolbar.ToolbarAuralView = Backbone.View.extend({
initialize: function initialize(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) {
onOrientationChange: function onOrientationChange(model, orientation) {
Drupal.announce(Drupal.t('Tray orientation changed to @orientation.', {
'@orientation': orientation
}));
},
onActiveTrayChange: function onActiveTrayChange(model, tray) {
var relevantTray = tray === null ? model.previous('activeTray') : tray;
/**
* 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');
if (!relevantTray) {
return;
}
var action = tray === null ? Drupal.t('closed') : Drupal.t('opened');
var trayNameElement = relevantTray.querySelector('.toolbar-tray-name');
var text;
var text = void 0;
if (trayNameElement !== null) {
text = Drupal.t('Tray "@tray" @action.', {
'@tray': trayNameElement.textContent, '@action': action
});
}
else {
text = Drupal.t('Tray @action.', {'@action': action});
} else {
text = Drupal.t('Tray @action.', { '@action': action });
}
Drupal.announce(text);
}
});
}(Backbone, Drupal));
})(Backbone, Drupal);
@@ -0,0 +1,330 @@
/**
* @file
* A Backbone view for the toolbar element. Listens to mouse & touch.
*/
(function ($, Drupal, drupalSettings, Backbone) {
Drupal.toolbar.ToolbarVisualView = Backbone.View.extend(/** @lends Drupal.toolbar.ToolbarVisualView# */{
/**
* Event map for the `ToolbarVisualView`.
*
* @return {object}
* A map of events.
*/
events() {
// Prevents delay and simulated mouse events.
const 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(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);
this.listenTo(this.model, 'change:activeTab change:orientation change:isOriented', this.updateToolbarHeight);
// 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');
},
/**
* Update the toolbar element height.
*
* @constructs
*
* @augments Backbone.View
*/
updateToolbarHeight() {
const toolbarTabOuterHeight = $('#toolbar-bar').find('.toolbar-tab').outerHeight() || 0;
const toolbarTrayHorizontalOuterHeight = $('.is-active.toolbar-tray-horizontal').outerHeight() || 0;
this.model.set('height', toolbarTabOuterHeight + toolbarTrayHorizontalOuterHeight);
$('body').css({
'padding-top': this.model.get('height'),
});
this.triggerDisplace();
},
// Trigger a recalculation of viewport displacing elements. Use setTimeout
// to ensure this recalculation happens after changes to visual elements
// have processed.
triggerDisplace() {
_.defer(() => {
Drupal.displace(true);
});
},
/**
* @inheritdoc
*
* @return {Drupal.toolbar.ToolbarVisualView}
* The `ToolbarVisualView` instance.
*/
render() {
this.updateTabs();
this.updateTrayOrientation();
this.updateBarAttributes();
$('body').removeClass('toolbar-loading');
// 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();
}
return this;
},
/**
* Responds to a toolbar tab click.
*
* @param {jQuery.Event} event
* The event triggered.
*/
onTabClick(event) {
// If this tab has a tray associated with it, it is considered an
// activatable tab.
if (event.target.hasAttribute('data-toolbar-tray')) {
const activeTab = this.model.get('activeTab');
const 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(event) {
const orientation = this.model.get('orientation');
// Determine the toggle-to orientation.
const antiOrientation = (orientation === 'vertical') ? 'horizontal' : 'vertical';
const 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,
orientation: antiOrientation,
}, {
validate: true,
override: true,
});
event.preventDefault();
event.stopPropagation();
},
/**
* Updates the display of the tabs: toggles a tab and the associated tray.
*/
updateTabs() {
const $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);
const name = $tab.attr('data-toolbar-tray');
// Store the active tab name or remove the setting.
const id = $tab.get(0).id;
if (id) {
localStorage.setItem('Drupal.toolbar.activeTabID', JSON.stringify(id));
}
// Activate the associated tray.
const $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() {
const 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() {
const orientation = this.model.get('orientation');
// The antiOrientation is used to render the view of action buttons like
// the tray orientation toggle.
const antiOrientation = (orientation === 'vertical') ? 'horizontal' : 'vertical';
// Toggle toolbar's parent classes before other toolbar classes to avoid
// potential flicker and re-rendering.
$('body')
.toggleClass('toolbar-vertical', (orientation === 'vertical'))
.toggleClass('toolbar-horizontal', (orientation === 'horizontal'));
const removeClass = (antiOrientation === 'horizontal') ? 'toolbar-tray-horizontal' : 'toolbar-tray-vertical';
const $trays = this.$el.find('.toolbar-tray')
.removeClass(removeClass)
.addClass(`toolbar-tray-${orientation}`);
// Update the tray orientation toggle button.
const iconClass = `toolbar-icon-toggle-${orientation}`;
const iconAntiClass = `toolbar-icon-toggle-${antiOrientation}`;
const $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.
const dir = document.documentElement.dir;
const 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() {
const $trays = this.$el.find('.toolbar-tray');
if (!this.model.get('isOriented')) {
$trays.removeClass('toolbar-tray-horizontal').addClass('toolbar-tray-vertical');
}
},
/**
* 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() {
const $activeTab = $(this.model.get('activeTab'));
const 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') {
const subtreesHash = drupalSettings.toolbar.subtreesHash;
const theme = drupalSettings.ajaxPageState.theme;
const endpoint = Drupal.url(`toolbar/subtrees/${subtreesHash}`);
const cachedSubtreesHash = localStorage.getItem(`Drupal.toolbar.subtreesHash.${theme}`);
const cachedSubtrees = JSON.parse(localStorage.getItem(`Drupal.toolbar.subtrees.${theme}`));
const 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));
@@ -1,23 +1,14 @@
/**
* @file
* A Backbone view for the toolbar element. Listens to mouse & touch.
*/
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/2815083
* @preserve
**/
(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) {
Drupal.toolbar.ToolbarVisualView = Backbone.View.extend({
events: function events() {
var touchEndToClick = function touchEndToClick(event) {
event.preventDefault();
event.target.click();
};
@@ -29,110 +20,70 @@
'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) {
initialize: function initialize(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);
this.listenTo(this.model, 'change:activeTab change:orientation change:isOriented', this.updateToolbarHeight);
// Add the tray orientation toggles.
this.$el
.find('.toolbar-tray .toolbar-lining')
.append(Drupal.theme('toolbarOrientationToggle'));
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');
},
updateToolbarHeight: function updateToolbarHeight() {
var toolbarTabOuterHeight = $('#toolbar-bar').find('.toolbar-tab').outerHeight() || 0;
var toolbarTrayHorizontalOuterHeight = $('.is-active.toolbar-tray-horizontal').outerHeight() || 0;
this.model.set('height', toolbarTabOuterHeight + toolbarTrayHorizontalOuterHeight);
/**
* @inheritdoc
*
* @return {Drupal.toolbar.ToolbarVisualView}
* The `ToolbarVisualView` instance.
*/
render: function () {
$('body').css({
'padding-top': this.model.get('height')
});
this.triggerDisplace();
},
triggerDisplace: function triggerDisplace() {
_.defer(function () {
Drupal.displace(true);
});
},
render: function render() {
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.
$('body').removeClass('toolbar-loading');
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.
onTabClick: function onTabClick(event) {
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);
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) {
onOrientationToggleClick: function onOrientationToggleClick(event) {
var orientation = this.model.get('orientation');
// Determine the toggle-to orientation.
var antiOrientation = (orientation === 'vertical') ? 'horizontal' : 'vertical';
var antiOrientation = orientation === 'vertical' ? 'horizontal' : 'vertical';
var locked = antiOrientation === 'vertical';
// Remember the locked state.
if (locked) {
localStorage.setItem('Drupal.toolbar.trayVerticalLocked', 'true');
}
else {
} else {
localStorage.removeItem('Drupal.toolbar.trayVerticalLocked');
}
// Update the model.
this.model.set({
locked: locked,
orientation: antiOrientation
@@ -144,136 +95,78 @@
event.preventDefault();
event.stopPropagation();
},
/**
* Updates the display of the tabs: toggles a tab and the associated tray.
*/
updateTabs: function () {
updateTabs: function updateTabs() {
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.
$(this.model.previous('activeTab')).removeClass('is-active').prop('aria-pressed', false);
$(this.model.previous('activeTray')).removeClass('is-active');
if ($tab.length > 0) {
$tab
.addClass('is-active')
// Mark the tab as pressed.
.prop('aria-pressed', true);
$tab.addClass('is-active').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.
} else {
this.model.set('activeTray', null);
}
}
else {
// There is no active tray.
} else {
this.model.set('activeTray', null);
localStorage.removeItem('Drupal.toolbar.activeTabID');
}
},
/**
* Update the attributes of the toolbar bar element.
*/
updateBarAttributes: function () {
updateBarAttributes: function updateBarAttributes() {
var isOriented = this.model.get('isOriented');
if (isOriented) {
this.$el.find('.toolbar-bar').attr('data-offset-top', '');
}
else {
} 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 () {
updateTrayOrientation: function updateTrayOrientation() {
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 antiOrientation = orientation === 'vertical' ? 'horizontal' : 'vertical';
$('body').toggleClass('toolbar-vertical', orientation === 'vertical').toggleClass('toolbar-horizontal', orientation === 'horizontal');
var removeClass = antiOrientation === 'horizontal' ? 'toolbar-tray-horizontal' : 'toolbar-tray-vertical';
var $trays = this.$el.find('.toolbar-tray').removeClass(removeClass).addClass('toolbar-tray-' + orientation);
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);
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.
var edge = dir === 'rtl' ? 'right' : 'left';
$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 () {
adjustPlacement: function adjustPlacement() {
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 () {
loadSubtrees: function loadSubtrees() {
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;
@@ -281,25 +174,18 @@
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);
}
} else if (isVertical) {
localStorage.removeItem('Drupal.toolbar.subtreesHash.' + theme);
localStorage.removeItem('Drupal.toolbar.subtrees.' + theme);
Drupal.ajax({ url: endpoint }).execute();
localStorage.setItem('Drupal.toolbar.subtreesHash.' + theme, subtreesHash);
}
}
}
});
}(jQuery, Drupal, drupalSettings, Backbone));
})(jQuery, Drupal, drupalSettings, Backbone);
@@ -5,8 +5,8 @@ package: Testing
# version: VERSION
# core: 8.x
# Information added by Drupal.org packaging script on 2017-08-16
version: '8.3.7'
# Information added by Drupal.org packaging script on 2017-11-03
version: '8.4.2'
core: '8.x'
project: 'drupal'
datestamp: 1502903957
datestamp: 1509719929
@@ -5,8 +5,8 @@ package: Testing
# version: VERSION
# core: 8.x
# Information added by Drupal.org packaging script on 2017-08-16
version: '8.3.7'
# Information added by Drupal.org packaging script on 2017-11-03
version: '8.4.2'
core: '8.x'
project: 'drupal'
datestamp: 1502903957
datestamp: 1509719929
@@ -0,0 +1,430 @@
<?php
namespace Drupal\Tests\toolbar\Functional;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Url;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\RoleInterface;
/**
* Tests the caching of the admin menu subtree items.
*
* The cache of the admin menu subtree items will be invalidated if the
* following hooks are invoked.
*
* toolbar_modules_enabled()
* toolbar_modules_disabled()
* toolbar_menu_link_update()
* toolbar_user_update()
* toolbar_user_role_update()
*
* Each hook invocation is simulated and then the previous hash of the admin
* menu subtrees is compared to the new hash.
*
* @group toolbar
*/
class ToolbarAdminMenuTest extends BrowserTestBase {
/**
* A user with permission to access the administrative toolbar.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* A second user with permission to access the administrative toolbar.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser2;
/**
* The current admin menu subtrees hash for adminUser.
*
* @var string
*/
protected $hash;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['node', 'block', 'menu_ui', 'user', 'taxonomy', 'toolbar', 'language', 'test_page_test', 'locale'];
protected function setUp() {
parent::setUp();
$perms = [
'access toolbar',
'access administration pages',
'administer site configuration',
'bypass node access',
'administer themes',
'administer nodes',
'access content overview',
'administer blocks',
'administer menu',
'administer modules',
'administer permissions',
'administer users',
'access user profiles',
'administer taxonomy',
'administer languages',
'translate interface',
];
// Create an administrative user and log it in.
$this->adminUser = $this->drupalCreateUser($perms);
$this->adminUser2 = $this->drupalCreateUser($perms);
$this->drupalLogin($this->adminUser);
$this->drupalGet('test-page');
$this->assertResponse(200);
// Assert that the toolbar is present in the HTML.
$this->assertRaw('id="toolbar-administration"');
// Store the adminUser admin menu subtrees hash for comparison later.
$this->hash = $this->getSubtreesHash();
}
/**
* Tests the toolbar_modules_installed() and toolbar_modules_uninstalled() hook
* implementations.
*/
public function testModuleStatusChangeSubtreesHashCacheClear() {
// Uninstall a module.
$edit = [];
$edit['uninstall[taxonomy]'] = TRUE;
$this->drupalPostForm('admin/modules/uninstall', $edit, t('Uninstall'));
// Confirm the uninstall form.
$this->drupalPostForm(NULL, [], t('Uninstall'));
$this->rebuildContainer();
// Assert that the subtrees hash has been altered because the subtrees
// structure changed.
$this->assertDifferentHash();
// Enable a module.
$edit = [];
$edit['modules[taxonomy][enable]'] = TRUE;
$this->drupalPostForm('admin/modules', $edit, t('Install'));
$this->rebuildContainer();
// Assert that the subtrees hash has been altered because the subtrees
// structure changed.
$this->assertDifferentHash();
}
/**
* Tests toolbar cache tags implementation.
*/
public function testMenuLinkUpdateSubtreesHashCacheClear() {
// The ID of a (any) admin menu link.
$admin_menu_link_id = 'system.admin_config_development';
// Disable the link.
$edit = [];
$edit['enabled'] = FALSE;
$this->drupalPostForm("admin/structure/menu/link/" . $admin_menu_link_id . "/edit", $edit, t('Save'));
$this->assertResponse(200);
$this->assertText('The menu link has been saved.');
// Assert that the subtrees hash has been altered because the subtrees
// structure changed.
$this->assertDifferentHash();
}
/**
* Exercises the toolbar_user_role_update() and toolbar_user_update() hook
* implementations.
*/
public function testUserRoleUpdateSubtreesHashCacheClear() {
// Find the new role ID.
$all_rids = $this->adminUser->getRoles();
unset($all_rids[array_search(RoleInterface::AUTHENTICATED_ID, $all_rids)]);
$rid = reset($all_rids);
$edit = [];
$edit[$rid . '[administer taxonomy]'] = FALSE;
$this->drupalPostForm('admin/people/permissions', $edit, t('Save permissions'));
// Assert that the subtrees hash has been altered because the subtrees
// structure changed.
$this->assertDifferentHash();
// Test that assigning a user an extra role only affects that single user.
// Get the hash for a second user.
$this->drupalLogin($this->adminUser2);
$this->drupalGet('test-page');
$this->assertResponse(200);
// Assert that the toolbar is present in the HTML.
$this->assertRaw('id="toolbar-administration"');
$admin_user_2_hash = $this->getSubtreesHash();
// Log in the first admin user again.
$this->drupalLogin($this->adminUser);
$this->drupalGet('test-page');
$this->assertResponse(200);
// Assert that the toolbar is present in the HTML.
$this->assertRaw('id="toolbar-administration"');
$this->hash = $this->getSubtreesHash();
$rid = $this->drupalCreateRole(['administer content types']);
// Assign the role to the user.
$this->drupalPostForm('user/' . $this->adminUser->id() . '/edit', ["roles[$rid]" => $rid], t('Save'));
$this->assertText(t('The changes have been saved.'));
// Assert that the subtrees hash has been altered because the subtrees
// structure changed.
$this->assertDifferentHash();
// Log in the second user again and assert that their subtrees hash did not
// change.
$this->drupalLogin($this->adminUser2);
// Request a new page to refresh the drupalSettings object.
$this->drupalGet('test-page');
$this->assertResponse(200);
$new_subtree_hash = $this->getSubtreesHash();
// Assert that the old admin menu subtree hash and the new admin menu
// subtree hash are the same.
$this->assertTrue($new_subtree_hash, 'A valid hash value for the admin menu subtrees was created.');
$this->assertEqual($admin_user_2_hash, $new_subtree_hash, 'The user-specific subtree menu hash has not been updated.');
}
/**
* Tests that changes to a user account by another user clears the changed
* account's toolbar cached, not the user's who took the action.
*/
public function testNonCurrentUserAccountUpdates() {
$admin_user_id = $this->adminUser->id();
$this->hash = $this->getSubtreesHash();
// adminUser2 will add a role to adminUser.
$this->drupalLogin($this->adminUser2);
$rid = $this->drupalCreateRole(['administer content types']);
// Get the subtree hash for adminUser2 to check later that it has not
// changed. Request a new page to refresh the drupalSettings object.
$this->drupalGet('test-page');
$this->assertResponse(200);
$admin_user_2_hash = $this->getSubtreesHash();
// Assign the role to the user.
$this->drupalPostForm('user/' . $admin_user_id . '/edit', ["roles[$rid]" => $rid], t('Save'));
$this->assertText(t('The changes have been saved.'));
// Log in adminUser and assert that the subtrees hash has changed.
$this->drupalLogin($this->adminUser);
$this->assertDifferentHash();
// Log in adminUser2 to check that its subtrees hash has not changed.
$this->drupalLogin($this->adminUser2);
$new_subtree_hash = $this->getSubtreesHash();
// Assert that the old adminUser subtree hash and the new adminUser
// subtree hash are the same.
$this->assertTrue($new_subtree_hash, 'A valid hash value for the admin menu subtrees was created.');
$this->assertEqual($admin_user_2_hash, $new_subtree_hash, 'The user-specific subtree menu hash has not been updated.');
}
/**
* Tests that toolbar cache is cleared when string translations are made.
*/
public function testLocaleTranslationSubtreesHashCacheClear() {
$admin_user = $this->adminUser;
// User to translate and delete string.
$translate_user = $this->drupalCreateUser(['translate interface', 'access administration pages']);
// Create a new language with the langcode 'xx'.
$langcode = 'xx';
// The English name for the language. This will be translated.
$name = $this->randomMachineName(16);
// This will be the translation of $name.
$translation = $this->randomMachineName(16);
// Add custom language.
$this->drupalLogin($admin_user);
$edit = [
'predefined_langcode' => 'custom',
'langcode' => $langcode,
'label' => $name,
'direction' => LanguageInterface::DIRECTION_LTR,
];
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language'));
t($name, [], ['langcode' => $langcode]);
// Reset locale cache.
$this->container->get('string_translation')->reset();
$this->assertRaw('"edit-languages-' . $langcode . '-weight"', 'Language code found.');
$this->assertText(t($name), 'Test language added.');
// Have the adminUser request a page in the new language.
$this->drupalGet($langcode . '/test-page');
$this->assertResponse(200);
// Get a baseline hash for the admin menu subtrees before translating one
// of the menu link items.
$original_subtree_hash = $this->getSubtreesHash();
$this->assertTrue($original_subtree_hash, 'A valid hash value for the admin menu subtrees was created.');
$this->drupalLogout();
// Translate the string 'Search and metadata' in the xx language. This
// string appears in a link in the admin menu subtrees. Changing the string
// should create a new menu hash if the toolbar subtrees cache is correctly
// invalidated.
$this->drupalLogin($translate_user);
$search = [
'string' => 'Search and metadata',
'langcode' => $langcode,
'translation' => 'untranslated',
];
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
$this->assertNoText(t('No strings available'));
$this->assertText($name, 'Search found the string as untranslated.');
// Assume this is the only result.
// Translate the string to a random string.
$textarea = current($this->xpath('//textarea'));
$lid = (string) $textarea->getAttribute('name');
$edit = [
$lid => $translation,
];
$this->drupalPostForm('admin/config/regional/translate', $edit, t('Save translations'));
$this->assertText(t('The strings have been saved.'), 'The strings have been saved.');
$this->assertUrl(\Drupal::url('locale.translate_page', [], ['absolute' => TRUE]), [], 'Correct page redirection.');
$this->drupalLogout();
// Log in the adminUser. Check the admin menu subtrees hash now that one
// of the link items in the Structure tree (Menus) has had its text
// translated.
$this->drupalLogin($admin_user);
// Have the adminUser request a page in the new language.
$this->drupalGet($langcode . '/test-page');
$this->assertResponse(200);
$new_subtree_hash = $this->getSubtreesHash();
// Assert that the old admin menu subtrees hash and the new admin menu
// subtrees hash are different.
$this->assertTrue($new_subtree_hash, 'A valid hash value for the admin menu subtrees was created.');
$this->assertNotEqual($original_subtree_hash, $new_subtree_hash, 'The user-specific subtree menu hash has been updated.');
}
/**
* Tests that the 'toolbar/subtrees/{hash}' is reachable and correct.
*/
public function testSubtreesJsonRequest() {
$admin_user = $this->adminUser;
$this->drupalLogin($admin_user);
// Request a new page to refresh the drupalSettings object.
$subtrees_hash = $this->getSubtreesHash();
$this->drupalGet('toolbar/subtrees/' . $subtrees_hash, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']], ['X-Requested-With: XMLHttpRequest']);
$ajax_result = json_decode($this->getSession()->getPage()->getContent(), TRUE);
$this->assertEqual($ajax_result[0]['command'], 'setToolbarSubtrees', 'Subtrees response uses the correct command.');
$this->assertEqual(array_keys($ajax_result[0]['subtrees']), ['system-admin_content', 'system-admin_structure', 'system-themes_page', 'system-modules_list', 'system-admin_config', 'entity-user-collection', 'front'], 'Correct subtrees returned.');
}
/**
* Test that subtrees hashes vary by the language of the page.
*/
public function testLanguageSwitching() {
// Create a new language with the langcode 'xx'.
$langcode = 'xx';
$language = ConfigurableLanguage::createFromLangcode($langcode);
$language->save();
// The language path processor is just registered for more than one
// configured language, so rebuild the container now that we are
// multilingual.
$this->rebuildContainer();
// Get a page with the new language langcode in the URL.
$this->drupalGet('test-page', ['language' => $language]);
// Assert different hash.
$new_subtree_hash = $this->getSubtreesHash();
// Assert that the old admin menu subtree hash and the new admin menu
// subtree hash are different.
$this->assertTrue($new_subtree_hash, 'A valid hash value for the admin menu subtrees was created.');
$this->assertNotEqual($this->hash, $new_subtree_hash, 'The user-specific subtree menu hash has been updated.');
}
/**
* Test that back to site link exists on admin pages, not on content pages.
*/
public function testBackToSiteLink() {
// Back to site link should exist in the markup.
$this->drupalGet('test-page');
$back_link = $this->cssSelect('.home-toolbar-tab');
$this->assertTrue($back_link);
}
/**
* Tests that external links added to the menu appear in the toolbar.
*/
public function testExternalLink() {
$edit = [
'title[0][value]' => 'External URL',
'link[0][uri]' => 'http://example.org',
'menu_parent' => 'admin:system.admin',
'description[0][value]' => 'External URL & escaped',
];
$this->drupalPostForm('admin/structure/menu/manage/admin/add', $edit, 'Save');
// Assert that the new menu link is shown on the menu link listing.
$this->drupalGet('admin/structure/menu/manage/admin');
$this->assertText('External URL');
// Assert that the new menu link is shown in the toolbar on a regular page.
$this->drupalGet(Url::fromRoute('<front>'));
$this->assertText('External URL');
// Ensure the description is escaped as expected.
$this->assertRaw('title="External URL &amp; escaped"');
}
/**
* Get the hash value from the admin menu subtrees route path.
*
* @return string
* The hash value from the admin menu subtrees route path.
*/
private function getSubtreesHash() {
$settings = $this->getDrupalSettings();
// The toolbar module defines a route '/toolbar/subtrees/{hash}' that
// returns JSON for the rendered subtrees. This hash is provided to the
// client in drupalSettings.
return $settings['toolbar']['subtreesHash'];
}
/**
* Asserts the subtrees hash on a fresh page GET is different from the hash
* from the previous page GET.
*/
private function assertDifferentHash() {
// Request a new page to refresh the drupalSettings object.
$this->drupalGet('test-page');
$this->assertResponse(200);
$new_subtree_hash = $this->getSubtreesHash();
// Assert that the old admin menu subtree hash and the new admin menu
// subtree hash are different.
$this->assertTrue($new_subtree_hash, 'A valid hash value for the admin menu subtrees was created.');
$this->assertNotEqual($this->hash, $new_subtree_hash, 'The user-specific subtree menu hash has been updated.');
// Save the new subtree hash as the original.
$this->hash = $new_subtree_hash;
}
}
@@ -0,0 +1,145 @@
<?php
namespace Drupal\Tests\toolbar\Functional;
use Drupal\Core\Cache\Cache;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the cache contexts for toolbar.
*
* @group toolbar
*/
class ToolbarCacheContextsTest extends BrowserTestBase {
use AssertPageCacheContextsAndTagsTrait;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['toolbar', 'test_page_test'];
/**
* An authenticated user to use for testing.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* An authenticated user to use for testing.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser2;
/**
* A list of default permissions for test users.
*
* @var array
*/
protected $perms = [
'access toolbar',
'access administration pages',
'administer site configuration',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->adminUser = $this->drupalCreateUser($this->perms);
$this->adminUser2 = $this->drupalCreateUser($this->perms);
}
/**
* Tests toolbar cache contexts.
*/
public function testToolbarCacheContextsCaller() {
// Test with default combination and permission to see toolbar.
$this->assertToolbarCacheContexts(['user'], 'Expected cache contexts found for default combination and permission to see toolbar.');
// Test without user toolbar tab. User module is a required module so we have to
// manually remove the user toolbar tab.
$this->installExtraModules(['toolbar_disable_user_toolbar']);
$this->assertToolbarCacheContexts(['user.permissions'], 'Expected cache contexts found without user toolbar tab.');
// Test with the toolbar and contextual enabled.
$this->installExtraModules(['contextual']);
$this->adminUser2 = $this->drupalCreateUser(array_merge($this->perms, ['access contextual links']));
$this->assertToolbarCacheContexts(['user.permissions'], 'Expected cache contexts found with contextual module enabled.');
\Drupal::service('module_installer')->uninstall(['contextual']);
// Test with the tour module enabled.
$this->installExtraModules(['tour']);
$this->adminUser2 = $this->drupalCreateUser(array_merge($this->perms, ['access tour']));
$this->assertToolbarCacheContexts(['user.permissions'], 'Expected cache contexts found with tour module enabled.');
\Drupal::service('module_installer')->uninstall(['tour']);
// Test with shortcut module enabled.
$this->installExtraModules(['shortcut']);
$this->adminUser2 = $this->drupalCreateUser(array_merge($this->perms, ['access shortcuts', 'administer shortcuts']));
$this->assertToolbarCacheContexts(['user'], 'Expected cache contexts found with shortcut module enabled.');
}
/**
* Tests that cache contexts are applied for both users.
*
* @param string[] $cache_contexts
* Expected cache contexts for both users.
* @param string $message
* (optional) A verbose message to output.
*
* @return
* TRUE if the assertion succeeded, FALSE otherwise.
*/
protected function assertToolbarCacheContexts(array $cache_contexts, $message = NULL) {
// Default cache contexts that should exist on all test cases.
$default_cache_contexts = [
'languages:language_interface',
'theme',
'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT,
];
$cache_contexts = Cache::mergeContexts($default_cache_contexts, $cache_contexts);
// Assert contexts for user1 which has only default permissions.
$this->drupalLogin($this->adminUser);
$this->drupalGet('test-page');
$return = $this->assertCacheContexts($cache_contexts);
$this->drupalLogout();
// Assert contexts for user2 which has some additional permissions.
$this->drupalLogin($this->adminUser2);
$this->drupalGet('test-page');
$return = $return && $this->assertCacheContexts($cache_contexts);
if ($return) {
$this->pass($message);
}
else {
$this->fail($message);
}
return $return;
}
/**
* Installs a given list of modules and rebuilds the cache.
*
* @param string[] $module_list
* An array of module names.
*/
protected function installExtraModules(array $module_list) {
\Drupal::service('module_installer')->install($module_list);
// Installing modules updates the container and needs a router rebuild.
$this->container = \Drupal::getContainer();
$this->container->get('router.builder')->rebuildIfNeeded();
}
}
@@ -0,0 +1,96 @@
<?php
namespace Drupal\Tests\toolbar\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests that the toolbar icon class remains for translated menu items.
*
* @group toolbar
*/
class ToolbarMenuTranslationTest extends BrowserTestBase {
/**
* A user with permission to access the administrative toolbar.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['toolbar', 'toolbar_test', 'locale', 'locale_test'];
protected function setUp() {
parent::setUp();
// Create an administrative user and log it in.
$this->adminUser = $this->drupalCreateUser(['access toolbar', 'translate interface', 'administer languages', 'access administration pages']);
$this->drupalLogin($this->adminUser);
}
/**
* Tests that toolbar classes don't change when adding a translation.
*/
public function testToolbarClasses() {
$langcode = 'es';
// Add Spanish.
$edit['predefined_langcode'] = $langcode;
$this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add language'));
// The menu item 'Structure' in the toolbar will be translated.
$menu_item = 'Structure';
// Visit a page that has the string on it so it can be translated.
$this->drupalGet($langcode . '/admin/structure');
// Search for the menu item.
$search = [
'string' => $menu_item,
'langcode' => $langcode,
'translation' => 'untranslated',
];
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
// Make sure will be able to translate the menu item.
$this->assertNoText('No strings available.', 'Search found the menu item as untranslated.');
// Check that the class is on the item before we translate it.
$xpath = $this->xpath('//a[contains(@class, "icon-system-admin-structure")]');
$this->assertEqual(count($xpath), 1, 'The menu item class ok before translation.');
// Translate the menu item.
$menu_item_translated = $this->randomMachineName();
$textarea = current($this->xpath('//textarea'));
$lid = (string) $textarea->getAttribute('name');
$edit = [
$lid => $menu_item_translated,
];
$this->drupalPostForm('admin/config/regional/translate', $edit, t('Save translations'));
// Search for the translated menu item.
$search = [
'string' => $menu_item,
'langcode' => $langcode,
'translation' => 'translated',
];
$this->drupalPostForm('admin/config/regional/translate', $search, t('Filter'));
// Make sure the menu item string was translated.
$this->assertText($menu_item_translated, 'Search found the menu item as translated: ' . $menu_item_translated . '.');
// Go to another page in the custom language and make sure the menu item
// was translated.
$this->drupalGet($langcode . '/admin/structure');
$this->assertText($menu_item_translated, 'Found the menu translated.');
// Toolbar icons are included based on the presence of a specific class on
// the menu item. Ensure that class also exists for a translated menu item.
$xpath = $this->xpath('//a[contains(@class, "icon-system-admin-structure")]');
$this->assertEqual(count($xpath), 1, 'The menu item class is the same.');
}
}
+1 -1
View File
@@ -109,7 +109,7 @@ function hook_toolbar() {
'#heading' => t('Shopping cart actions'),
'shopping_cart' => [
'#theme' => 'item_list',
'#items' => [ /* An item list renderable array */ ],
'#items' => [/* An item list renderable array */],
],
],
'#weight' => 150,
+3 -3
View File
@@ -7,8 +7,8 @@ package: Core
dependencies:
- breakpoint
# Information added by Drupal.org packaging script on 2017-08-16
version: '8.3.7'
# Information added by Drupal.org packaging script on 2017-11-03
version: '8.4.2'
core: '8.x'
project: 'drupal'
datestamp: 1502903957
datestamp: 1509719929
+14 -2
View File
@@ -149,7 +149,7 @@ function toolbar_toolbar() {
],
],
'#wrapper_attributes' => [
'class' => ['hidden', 'home-toolbar-tab'],
'class' => ['home-toolbar-tab'],
],
'#attached' => [
'library' => [
@@ -276,6 +276,18 @@ function toolbar_menu_navigation_links(array $tree) {
return $tree;
}
/**
* Implements hook_preprocess_HOOK() for HTML document templates.
*/
function toolbar_preprocess_html(&$variables) {
if (!\Drupal::currentUser()->hasPermission('access toolbar')) {
return;
}
$variables['attributes'] = new Attribute($variables['attributes']);
$variables['attributes']->addClass(['toolbar-tray-open', 'toolbar-horizontal', 'toolbar-fixed', 'toolbar-loading']);
}
/**
* Returns the rendered subtree of each top-level toolbar link.
*
@@ -333,7 +345,7 @@ function _toolbar_do_get_rendered_subtrees(array $data) {
// Many routes have dots as route name, while some special ones like <front>
// have <> characters in them.
$url = $link->getUrlObject();
$id = str_replace(['.', '<', '>'], ['-', '', '' ], $url->isRouted() ? $url->getRouteName() : $url->getUri());
$id = str_replace(['.', '<', '>'], ['-', '', ''], $url->isRouted() ? $url->getRouteName() : $url->getUri());
$subtrees[$id] = $output;
}