Move into nested docroot

This commit is contained in:
Rob Davies 2017-02-13 15:31:17 +00:00
parent 83a0d3a149
commit c8b70abde9
13405 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,300 @@
/**
* @file
* Styling for toolbar module icons.
*/
.toolbar .toolbar-icon {
padding-left: 2.75em; /* LTR */
position: relative;
}
[dir="rtl"] .toolbar .toolbar-icon {
padding-left: 1.3333em;
padding-right: 2.75em;
}
.toolbar .toolbar-icon:before {
background-attachment: scroll;
background-color: transparent;
background-position: center center;
background-repeat: no-repeat;
background-size: 100% auto;
content: '';
display: block;
height: 100%;
left: 0.6667em; /* LTR */
position: absolute;
top: 0;
width: 20px;
}
[dir="rtl"] .toolbar .toolbar-icon:before {
left: auto;
right: 0.6667em;
}
.toolbar button.toolbar-icon {
background-color: transparent;
border: 0;
font-size: 1em;
}
.toolbar .toolbar-menu ul .toolbar-icon {
padding-left: 1.3333em; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-menu ul .toolbar-icon {
padding-left: 0;
padding-right: 1.3333em;
}
.toolbar .toolbar-menu ul a.toolbar-icon:before {
display: none;
}
.toolbar .toolbar-tray-vertical .toolbar-menu ul a {
padding-left: 2.75em; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-tray-vertical .toolbar-menu ul a {
padding-left: 0;
padding-right: 2.75em;
}
.toolbar .toolbar-tray-vertical .toolbar-menu ul ul a {
padding-left: 3.75em; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-tray-vertical .toolbar-menu ul ul a {
padding-left: 0;
padding-right: 3.75em;
}
.toolbar .toolbar-tray-vertical .toolbar-menu a {
padding-left: 2.75em; /* LTR */
padding-right: 4em; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-tray-vertical .toolbar-menu a {
padding-left: 4em;
padding-right: 2.75em;
}
/**
* Top level icons.
*/
.toolbar-bar .toolbar-icon-menu:before {
background-image: url(../../../misc/icons/bebebe/hamburger.svg);
}
.toolbar-bar .toolbar-icon-menu:active:before,
.toolbar-bar .toolbar-icon-menu.is-active:before {
background-image: url(../../../misc/icons/ffffff/hamburger.svg);
}
.toolbar-bar .toolbar-icon-help:before {
background-image: url(../../../misc/icons/bebebe/questionmark-disc.svg);
}
.toolbar-bar .toolbar-icon-help:active:before,
.toolbar-bar .toolbar-icon-help.is-active:before {
background-image: url(../../../misc/icons/ffffff/questionmark-disc.svg);
}
/**
* Main menu icons.
*/
.toolbar-icon-system-admin-content:before {
background-image: url(../../../misc/icons/787878/file.svg);
}
.toolbar-icon-system-admin-content:active:before,
.toolbar-icon-system-admin-content.is-active:before {
background-image: url(../../../misc/icons/000000/file.svg);
}
.toolbar-icon-system-admin-structure:before {
background-image: url(../../../misc/icons/787878/orgchart.svg);
}
.toolbar-icon-system-admin-structure:active:before,
.toolbar-icon-system-admin-structure.is-active:before {
background-image: url(../../../misc/icons/000000/orgchart.svg);
}
.toolbar-icon-system-themes-page:before {
background-image: url(../../../misc/icons/787878/paintbrush.svg);
}
.toolbar-icon-system-themes-page:active:before,
.toolbar-icon-system-themes-page.is-active:before {
background-image: url(../../../misc/icons/000000/paintbrush.svg);
}
.toolbar-icon-entity-user-collection:before {
background-image: url(../../../misc/icons/787878/people.svg);
}
.toolbar-icon-entity-user-collection:active:before,
.toolbar-icon-entity-user-collection.is-active:before {
background-image: url(../../../misc/icons/000000/people.svg);
}
.toolbar-icon-system-modules-list:before {
background-image: url(../../../misc/icons/787878/puzzlepiece.svg);
}
.toolbar-icon-system-modules-list:active:before,
.toolbar-icon-system-modules-list.is-active:before {
background-image: url(../../../misc/icons/000000/puzzlepiece.svg);
}
.toolbar-icon-system-admin-config:before {
background-image: url(../../../misc/icons/787878/wrench.svg);
}
.toolbar-icon-system-admin-config:active:before,
.toolbar-icon-system-admin-config.is-active:before {
background-image: url(../../../misc/icons/000000/wrench.svg);
}
.toolbar-icon-system-admin-reports:before {
background-image: url(../../../misc/icons/787878/barchart.svg);
}
.toolbar-icon-system-admin-reports:active:before,
.toolbar-icon-system-admin-reports.is-active:before {
background-image: url(../../../misc/icons/000000/barchart.svg);
}
.toolbar-icon-help-main:before {
background-image: url(../../../misc/icons/787878/questionmark-disc.svg);
}
.toolbar-icon-help-main:active:before,
.toolbar-icon-help-main.is-active:before {
background-image: url(../../../misc/icons/000000/questionmark-disc.svg);
}
@media only screen and (min-width: 16.5em) {
.toolbar .toolbar-bar .toolbar-tab > .toolbar-icon {
margin-left: 0;
margin-right: 0;
padding-left: 0;
padding-right: 0;
text-indent: -9999px;
width: 4em;
}
.toolbar .toolbar-bar .toolbar-tab > .toolbar-icon:before {
background-size: 42% auto;
left: 0; /* LTR */
width: 100%;
}
.no-svg .toolbar .toolbar-bar .toolbar-tab > .toolbar-icon:before {
background-size: auto auto;
}
[dir="rtl"] .toolbar .toolbar-bar .toolbar-tab > .toolbar-icon:before {
left: auto;
right: 0;
}
}
@media only screen and (min-width: 36em) {
.toolbar .toolbar-bar .toolbar-tab > .toolbar-icon {
background-position: left center; /* LTR */
padding-left: 2.75em; /* LTR */
padding-right: 1.3333em; /* LTR */
text-indent: 0;
width: auto;
}
[dir="rtl"] .toolbar .toolbar-bar .toolbar-tab > .toolbar-icon {
background-position: right center;
padding-left: 1.3333em;
padding-right: 2.75em;
}
.toolbar .toolbar-bar .toolbar-tab > .toolbar-icon:before {
background-size: 100% auto;
left: 0.6667em; /* LTR */
width: 20px;
}
.no-svg .toolbar .toolbar-bar .toolbar-tab > .toolbar-icon:before {
background-size: auto auto;
}
[dir="rtl"] .toolbar .toolbar-bar .toolbar-tab > .toolbar-icon:before {
left: 0;
right: 0.6667em;
}
}
/**
* Accessibility/focus
*/
.toolbar-tab a:focus {
outline: none;
text-decoration: underline;
}
.toolbar-lining button:focus {
outline: none;
}
.toolbar-tray-horizontal a:focus,
.toolbar-box a:focus {
outline: none;
background-color: #f5f5f5;
}
.toolbar-box a:hover:focus {
text-decoration: underline;
}
.toolbar .toolbar-icon.toolbar-handle:focus {
outline: none;
background-color: #f5f5f5;
}
/**
* Handle.
*/
.toolbar .toolbar-icon.toolbar-handle {
width: 4em;
text-indent: -9999px;
}
.toolbar .toolbar-icon.toolbar-handle:before {
left: 1.6667em; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-icon.toolbar-handle:before {
left: auto;
right: 1.6667em;
}
.toolbar .toolbar-icon.toolbar-handle:before {
background-image: url(../../../misc/icons/5181c6/chevron-disc-down.svg);
}
.toolbar .toolbar-icon.toolbar-handle.open:before {
background-image: url(../../../misc/icons/787878/chevron-disc-up.svg);
}
.toolbar .toolbar-menu .toolbar-menu .toolbar-icon.toolbar-handle:before {
background-image: url(../../../misc/icons/5181c6/twistie-down.svg);
background-size: 75%;
}
.toolbar .toolbar-menu .toolbar-menu .toolbar-icon.toolbar-handle.open:before {
background-image: url(../../../misc/icons/787878/twistie-up.svg);
background-size: 75%;
}
.toolbar .toolbar-icon-escape-admin:before {
background-image: url(../../../misc/icons/bebebe/chevron-disc-left.svg);
}
[dir="rtl"] .toolbar .toolbar-icon-escape-admin:before {
background-image: url(../../../misc/icons/bebebe/chevron-disc-right.svg);
}
/**
* Orientation toggle.
*/
.toolbar .toolbar-toggle-orientation button {
height: 39px;
padding: 0;
text-indent: -999em;
width: 39px;
}
.toolbar .toolbar-toggle-orientation button:before {
left: 0;
right: 0;
margin: 0 auto;
}
[dir="rtl"] .toolbar .toolbar-toggle-orientation .toolbar-icon {
padding: 0;
}
/**
* In order to support a hover effect on the SVG images, while also supporting
* RTL text direction and no SVG support, this little icon requires some very
* specific targeting, setting and unsetting.
*/
.toolbar .toolbar-toggle-orientation [value="vertical"]:before {
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
{
background-image: url(../../../misc/icons/787878/push-left.svg); /* LTR */
}
[dir="rtl"] .toolbar .toolbar-toggle-orientation [value="vertical"]:before {
background-image: url(../../../misc/icons/bebebe/push-right.svg);
}
[dir="rtl"] .toolbar .toolbar-toggle-orientation [value="vertical"]:hover:before,
[dir="rtl"] .toolbar .toolbar-toggle-orientation [value="vertical"]:focus:before {
background-image: url(../../../misc/icons/787878/push-right.svg);
}
.toolbar .toolbar-toggle-orientation [value="horizontal"]:before {
background-image: url(../../../misc/icons/bebebe/push-up.svg);
}
.toolbar .toolbar-toggle-orientation [value="horizontal"]:hover:before,
.toolbar .toolbar-toggle-orientation [value="horizontal"]:focus:before {
background-image: url(../../../misc/icons/787878/push-up.svg);
}

View file

@ -0,0 +1,103 @@
/**
* @file toolbar.menu.css
*/
.toolbar .toolbar-menu,
[dir="rtl"] .toolbar .toolbar-menu {
list-style: none;
margin: 0;
padding: 0;
}
.toolbar .toolbar-box {
display: block;
line-height: 1em; /* this prevents the value "normal" from being returned as the line-height */
position: relative;
width: auto;
}
.toolbar .toolbar-tray-horizontal .toolbar-menu .toolbar-handle,
.toolbar .toolbar-tray-horizontal .toolbar-menu ul,
.toolbar .toolbar-tray-vertical .toolbar-menu ul {
display: none;
}
.toolbar .toolbar-tray-vertical li.open > ul {
display: block; /* Show the sub-menus */
}
.toolbar .toolbar-tray-vertical .toolbar-handle + a {
margin-right: 3em; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-tray-vertical .toolbar-handle + a {
margin-left: 3em;
margin-right: 0;
}
.toolbar .toolbar-tray .menu-item--active-trail > .toolbar-box a,
.toolbar .toolbar-tray a.is-active {
color: #000;
font-weight: bold;
}
/* ----- Toolbar menu tray for viewports less than 320px ------ */
@media screen and (max-width: 319px) {
.toolbar .toolbar-tray-vertical.is-active {
width: 100%;
}
}
/**
* Items.
*/
.toolbar .level-2 > ul {
background-color: #fafafa;
border-bottom-color: #cccccc;
border-top-color: #e5e5e5;
}
.toolbar .level-3 > ul {
background-color: #f5f5f5;
border-bottom-color: #c5c5c5;
border-top-color: #dddddd;
}
.toolbar .level-4 > ul {
background-color: #eeeeee;
border-bottom-color: #bbbbbb;
border-top-color: #d5d5d5;
}
.toolbar .level-5 > ul {
background-color: #e5e5e5;
border-bottom-color: #b5b5b5;
border-top-color: #cccccc;
}
.toolbar .level-6 > ul {
background-color: #eeeeee;
border-bottom-color: #aaaaaa;
border-top-color: #c5c5c5;
}
.toolbar .level-7 > ul {
background-color: #fafafa;
border-bottom-color: #b5b5b5;
border-top-color: #cccccc;
}
.toolbar .level-8 > ul {
background-color: #dddddd;
border-bottom-color: #cccccc;
border-top-color: #dddddd;
}
/**
* Handle.
*/
.toolbar .toolbar-handle:hover {
cursor: pointer;
}
.toolbar .toolbar-icon.toolbar-handle {
bottom: 0;
display: block;
height: 100%;
padding: 0;
position: absolute;
right: 0; /* LTR */
top: 0;
z-index: 1;
}
[dir="rtl"] .toolbar .toolbar-icon.toolbar-handle {
left: 0;
padding: 0;
right: auto;
}

View file

@ -0,0 +1,260 @@
/**
* @file toolbar.module.css
*
*
* Aggressive resets so we can achieve a consistent look in hostile CSS
* environments.
*/
#toolbar-administration,
#toolbar-administration * {
box-sizing: border-box;
}
#toolbar-administration {
font-size: small;
line-height: 1;
margin: 0;
padding: 0;
vertical-align: baseline;
}
@media print {
#toolbar-administration {
display: none;
}
}
/**
* Very specific overrides for Drupal system CSS.
*/
.toolbar li,
.toolbar .item-list,
.toolbar .item-list li,
.toolbar .menu-item,
.toolbar .menu-item--expanded {
list-style-type: none;
list-style-image: none;
}
.toolbar .menu-item {
padding-top: 0;
}
.toolbar .toolbar-bar .toolbar-tab,
.toolbar .menu-item {
display: block;
}
.toolbar .toolbar-bar .toolbar-tab.hidden {
display: none;
}
.toolbar a {
display: block;
line-height: 1;
}
/**
* Administration menu.
*/
.toolbar .toolbar-bar,
.toolbar .toolbar-tray {
position: relative;
z-index: 1250;
}
/* 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 {
left: 0;
position: absolute;
right: 0;
top: 0;
}
/* Layer the bar just above the trays and above contextual link triggers. */
.toolbar-oriented .toolbar-bar {
z-index: 502;
}
/* Position the admin toolbar fixed when the configured standard breakpoint is
* active. */
body.toolbar-fixed .toolbar-oriented .toolbar-bar {
position: fixed;
}
/* When the configured narrow breakpoint is active, the toolbar is sized to wrap
* around the trays in order to provide a context for scrolling tray content
* that is taller than the viewport. */
body.toolbar-tray-open.toolbar-fixed.toolbar-vertical .toolbar-oriented {
bottom: 0;
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 .toolbar-bar .toolbar-tab,
.toolbar .toolbar-tray-horizontal li {
float: left; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-bar .toolbar-tab,
[dir="rtl"] .toolbar .toolbar-tray-horizontal li {
float: right;
}
/* Present the admin toolbar tabs vertically by default on user agents that
* that understand media queries. This will be the small screen default. */
@media only screen {
.toolbar .toolbar-bar .toolbar-tab,
.toolbar .toolbar-tray-horizontal li {
float: none; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-bar .toolbar-tab,
[dir="rtl"] .toolbar .toolbar-tray-horizontal li {
float: none;
}
}
/* 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) {
.toolbar .toolbar-bar .toolbar-tab,
.toolbar .toolbar-tray-horizontal li {
float: left; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-bar .toolbar-tab,
[dir="rtl"] .toolbar .toolbar-tray-horizontal li {
float: right;
}
}
/* Present the admin toolbar tabs horizontally when the configured narrow
* breakpoint is active. */
.toolbar-oriented .toolbar-bar .toolbar-tab,
.toolbar-oriented .toolbar-tray-horizontal li {
float: left; /* LTR */
}
[dir="rtl"] .toolbar-oriented .toolbar-bar .toolbar-tab,
[dir="rtl"] .toolbar-oriented .toolbar-tray-horizontal li {
float: right;
}
/**
* Toolbar tray.
*/
.toolbar .toolbar-tray {
display: none;
z-index: 501;
}
.toolbar-oriented .toolbar-tray-vertical {
left: -100%; /* LTR */
position: absolute;
width: 240px;
width: 15rem;
}
[dir="rtl"] .toolbar-oriented .toolbar-tray-vertical {
left: auto;
right: -100%;
}
.toolbar .toolbar-tray-vertical > .toolbar-lining {
min-height: 100%;
}
.toolbar .toolbar-tray-vertical > .toolbar-lining:before {
width: 100%;
}
.toolbar-oriented .toolbar-tray-vertical > .toolbar-lining:before {
bottom: 0;
content: '';
display: block;
left: 0; /* LTR */
position: fixed;
top: 0;
width: 240px;
width: 14rem;
z-index: -1;
}
[dir="rtl"] .toolbar .toolbar-tray-vertical > .toolbar-lining:before {
left: auto;
right: 0;
}
/* Layer the links just above the toolbar-tray. */
.toolbar .toolbar-bar .toolbar-tab > .toolbar-icon{
position: relative;
z-index: 502;
}
/* Hide secondary menus when the tray is horizontal. */
.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. */
.toolbar .toolbar-tray-vertical.is-active,
body.toolbar-fixed .toolbar .toolbar-tray-vertical {
height: 100%;
overflow-x: hidden;
overflow-y: auto;
position: fixed;
}
.toolbar .toolbar-tray.is-active {
display: block;
}
/* Bring the tray into the viewport. By default it is just off-screen. */
.toolbar-oriented .toolbar-tray-vertical.is-active {
left: 0; /* LTR */
}
[dir="rtl"] .toolbar-oriented .toolbar-tray-vertical.is-active {
left: auto;
right: 0;
}
/* When the configured standard breakpoint is active, the tray appears to push
* the page content away from the edge of the viewport. */
body.toolbar-tray-open.toolbar-vertical.toolbar-fixed {
margin-left: 240px; /* LTR */
margin-left: 15rem; /* LTR */
}
@media print {
body.toolbar-tray-open.toolbar-vertical.toolbar-fixed {
margin-left: 0;
}
}
[dir="rtl"] body.toolbar-tray-open.toolbar-vertical.toolbar-fixed {
margin-left: auto;
margin-left: auto;
margin-right: 240px;
margin-right: 15rem;
}
@media print {
[dir="rtl"] body.toolbar-tray-open.toolbar-vertical.toolbar-fixed {
margin-right: 0;
}
}
/**
* ToolBar tray orientation toggle.
*/
/* Hide the orientation toggle when the configured narrow breakpoint is not
* active. */
.toolbar .toolbar-tray .toolbar-toggle-orientation {
display: none;
}
/* Show the orientation toggle when the configured narrow breakpoint is
* active. */
.toolbar-oriented .toolbar-tray .toolbar-toggle-orientation {
display: block;
}
.toolbar-oriented .toolbar-tray-horizontal .toolbar-toggle-orientation {
bottom: 0;
position: absolute;
right: 0; /* LTR */
top: auto;
}
[dir="rtl"] .toolbar-oriented .toolbar-tray-horizontal .toolbar-toggle-orientation {
left: 0;
right: auto;
}
.toolbar-oriented .toolbar-tray-vertical .toolbar-toggle-orientation {
float: right; /* LTR */
width: 100%;
}
[dir="rtl"] .toolbar-oriented .toolbar-tray-vertical .toolbar-toggle-orientation {
float: left;
}

View file

@ -0,0 +1,168 @@
/**
* @file toolbar.theme.css
*/
.toolbar {
font-family: "Source Sans Pro", "Lucida Grande", Verdana, sans-serif;
/* Set base font size to 13px based on root ems. */
font-size: 0.8125rem;
-moz-tap-highlight-color: rgba(0,0,0,0);
-o-tap-highlight-color: rgba(0,0,0,0);
-webkit-tap-highlight-color: rgba(0,0,0,0);
tap-highlight-color: rgba(0,0,0,0);
-moz-touch-callout: none;
-o-touch-callout: none;
-webkit-touch-callout: none;
touch-callout: none;
}
.toolbar .toolbar-item {
cursor: pointer;
padding: 1em 1.3333em;
line-height: 1em;
text-decoration: none;
}
.toolbar .toolbar-item:hover, .toolbar .toolbar-item:focus {
text-decoration: underline;
}
/**
* Toolbar bar.
*/
.toolbar .toolbar-bar {
background-color: #0f0f0f;
box-shadow: -1px 0 3px 1px rgba(0, 0, 0, 0.3333); /* LTR */
color: #dddddd;
}
[dir="rtl"] .toolbar .toolbar-bar {
box-shadow: 1px 0 3px 1px rgba(0, 0, 0, 0.3333);
}
.toolbar .toolbar-bar .toolbar-item {
color: #ffffff;
}
.toolbar .toolbar-bar .toolbar-tab > .toolbar-item {
font-weight: bold;
}
.toolbar .toolbar-bar .toolbar-tab > .toolbar-item:hover,
.toolbar .toolbar-bar .toolbar-tab > .toolbar-item:focus {
background-image: -webkit-linear-gradient(rgba(255, 255, 255, 0.125) 20%, transparent 200%);
background-image: linear-gradient(rgba(255, 255, 255, 0.125) 20%, transparent 200%);
}
.toolbar .toolbar-bar .toolbar-tab > .toolbar-item.is-active {
background-image: -webkit-linear-gradient(rgba(255, 255, 255, 0.25) 20%, transparent 200%);
background-image: linear-gradient(rgba(255, 255, 255, 0.25) 20%, transparent 200%);
}
/**
* Toolbar tray.
*/
.toolbar .toolbar-tray {
background-color: #ffffff;
}
.toolbar .toolbar-tray-horizontal > .toolbar-lining {
padding-right: 5em; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-tray-horizontal > .toolbar-lining {
padding-right: 0;
padding-left: 5em;
}
.toolbar .toolbar-tray-vertical {
background-color: #f5f5f5;
border-right: 1px solid #aaaaaa; /* LTR */
box-shadow: -1px 0 5px 2px rgba(0, 0, 0, 0.3333); /* LTR */
}
[dir="rtl"] .toolbar .toolbar-tray-vertical {
border-left: 1px solid #aaaaaa;
border-right: 0 none;
box-shadow: 1px 0 5px 2px rgba(0, 0, 0, 0.3333);
}
.toolbar .toolbar-tray-horizontal {
border-bottom: 1px solid #aaaaaa;
box-shadow: -2px 1px 3px 1px rgba(0, 0, 0, 0.3333); /* LTR */
}
[dir="rtl"] .toolbar .toolbar-tray-horizontal {
box-shadow: 2px 1px 3px 1px rgba(0, 0, 0, 0.3333);
}
.toolbar .toolbar-tray-horizontal .toolbar-tray {
background-color: #f5f5f5;
}
.toolbar-tray a {
color: #565656;
cursor: pointer;
padding: 1em 1.3333em;
text-decoration: none;
}
.toolbar-tray a:hover,
.toolbar-tray a:active,
.toolbar-tray a:focus,
.toolbar-tray a.is-active
{
color: #000;
text-decoration: underline;
}
.toolbar .toolbar-menu {
background-color: #ffffff;
}
.toolbar .toolbar-tray-horizontal .menu-item + .menu-item {
border-left: 1px solid #dddddd; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-tray-horizontal .menu-item + .menu-item {
border-left: 0 none ;
border-right: 1px solid #dddddd;
}
.toolbar .toolbar-tray-horizontal .menu-item:last-child {
border-right: 1px solid #dddddd; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-tray-horizontal .menu-item:last-child {
border-left: 1px solid #dddddd;
}
.toolbar .toolbar-tray-vertical .menu-item + .menu-item {
border-top: 1px solid #dddddd;
}
.toolbar .toolbar-tray-vertical .menu-item:last-child {
border-bottom: 1px solid #dddddd;
}
.toolbar .toolbar-tray-vertical .menu-item .menu-item {
border: 0 none;
}
.toolbar .toolbar-tray-vertical .toolbar-menu ul ul {
border-bottom: 1px solid #dddddd;
border-top: 1px solid #dddddd;
}
.toolbar .toolbar-tray-vertical .menu-item:last-child > ul {
border-bottom: 0;
}
.toolbar .toolbar-tray-vertical .toolbar-menu .toolbar-menu .toolbar-menu .toolbar-menu {
margin-left: 0.25em; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-tray-vertical .toolbar-menu .toolbar-menu .toolbar-menu .toolbar-menu {
margin-left: 0;
margin-right: 0.25em;
}
.toolbar .toolbar-menu .toolbar-menu a {
color: #434343;
}
/**
* Orientation toggle.
*/
.toolbar .toolbar-toggle-orientation {
background-color: #f5f5f5;
padding: 0;
height: 100%;
}
.toolbar .toolbar-tray-horizontal .toolbar-toggle-orientation {
border-left: 1px solid #c9c9c9; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-tray-horizontal .toolbar-toggle-orientation {
border-left: 0 none;
border-right: 1px solid #c9c9c9;
}
.toolbar .toolbar-toggle-orientation > .toolbar-lining {
float: right; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-toggle-orientation > .toolbar-lining {
float: left;
}
.toolbar .toolbar-toggle-orientation button {
cursor: pointer;
display: inline-block;
}

View file

@ -0,0 +1,48 @@
/**
* @file
* Replaces the home link in toolbar with a back to site link.
*/
(function ($, Drupal, drupalSettings) {
'use strict';
var pathInfo = drupalSettings.path;
var escapeAdminPath = sessionStorage.getItem('escapeAdminPath');
var windowLocation = window.location;
// Saves the last non-administrative page in the browser to be able to link
// back to it when browsing administrative pages. If there is a destination
// parameter there is not need to save the current path because the page is
// loaded within an existing "workflow".
if (!pathInfo.currentPathIsAdmin && !/destination=/.test(windowLocation.search)) {
sessionStorage.setItem('escapeAdminPath', windowLocation);
}
/**
* Replaces the "Home" link with "Back to site" link.
*
* Back to site link points to the last non-administrative page the user
* visited within the same browser tab.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the replacement functionality to the toolbar-escape-admin element.
*/
Drupal.behaviors.escapeAdmin = {
attach: function () {
var $toolbarEscape = $('[data-toolbar-escape-admin]').once('escapeAdmin');
if ($toolbarEscape.length && pathInfo.currentPathIsAdmin) {
if (escapeAdminPath !== null) {
$toolbarEscape.attr('href', escapeAdminPath);
}
else {
$toolbarEscape.text(Drupal.t('Home'));
}
$toolbarEscape.closest('.toolbar-tab').removeClass('hidden');
}
}
};
})(jQuery, Drupal, drupalSettings);

View file

@ -0,0 +1,33 @@
/**
* @file
* A Backbone Model for collapsible menus.
*/
(function (Backbone, Drupal) {
'use strict';
/**
* Backbone Model for collapsible menus.
*
* @constructor
*
* @augments Backbone.Model
*/
Drupal.toolbar.MenuModel = Backbone.Model.extend(/** @lends Drupal.toolbar.MenuModel# */{
/**
* @type {object}
*
* @prop {object} subtrees
*/
defaults: /** @lends Drupal.toolbar.MenuModel# */{
/**
* @type {object}
*/
subtrees: {}
}
});
}(Backbone, Drupal));

View file

@ -0,0 +1,157 @@
/**
* @file
* A Backbone Model for the toolbar.
*/
(function (Backbone, Drupal) {
'use strict';
/**
* Backbone model for the toolbar.
*
* @constructor
*
* @augments Backbone.Model
*/
Drupal.toolbar.ToolbarModel = Backbone.Model.extend(/** @lends Drupal.toolbar.ToolbarModel# */{
/**
* @type {object}
*
* @prop activeTab
* @prop activeTray
* @prop isOriented
* @prop isFixed
* @prop areSubtreesLoaded
* @prop isViewportOverflowConstrained
* @prop orientation
* @prop locked
* @prop isTrayToggleVisible
* @prop height
* @prop offsets
*/
defaults: /** @lends Drupal.toolbar.ToolbarModel# */{
/**
* The active toolbar tab. All other tabs should be inactive under
* normal circumstances. It will remain active across page loads. The
* active item is stored as an ID selector e.g. '#toolbar-item--1'.
*
* @type {string}
*/
activeTab: null,
/**
* Represents whether a tray is open or not. Stored as an ID selector e.g.
* '#toolbar-item--1-tray'.
*
* @type {string}
*/
activeTray: null,
/**
* Indicates whether the toolbar is displayed in an oriented fashion,
* either horizontal or vertical.
*
* @type {bool}
*/
isOriented: false,
/**
* Indicates whether the toolbar is positioned absolute (false) or fixed
* (true).
*
* @type {bool}
*/
isFixed: false,
/**
* Menu subtrees are loaded through an AJAX request only when the Toolbar
* is set to a vertical orientation.
*
* @type {bool}
*/
areSubtreesLoaded: false,
/**
* If the viewport overflow becomes constrained, isFixed must be true so
* that elements in the trays aren't lost off-screen and impossible to
* get to.
*
* @type {bool}
*/
isViewportOverflowConstrained: false,
/**
* The orientation of the active tray.
*
* @type {string}
*/
orientation: 'vertical',
/**
* A tray is locked if a user toggled it to vertical. Otherwise a tray
* will switch between vertical and horizontal orientation based on the
* configured breakpoints. The locked state will be maintained across page
* loads.
*
* @type {bool}
*/
locked: false,
/**
* Indicates whether the tray orientation toggle is visible.
*
* @type {bool}
*/
isTrayToggleVisible: false,
/**
* The height of the toolbar.
*
* @type {number}
*/
height: null,
/**
* The current viewport offsets determined by {@link Drupal.displace}. The
* offsets suggest how a module might position is components relative to
* the viewport.
*
* @type {object}
*
* @prop {number} top
* @prop {number} right
* @prop {number} bottom
* @prop {number} left
*/
offsets: {
top: 0,
right: 0,
bottom: 0,
left: 0
}
},
/**
* @inheritdoc
*
* @param {object} attributes
* Attributes for the toolbar.
* @param {object} options
* Options for the toolbar.
*
* @return {string|undefined}
* Returns an error message if validation failed.
*/
validate: function (attributes, options) {
// Prevent the orientation being set to horizontal if it is locked, unless
// override has not been passed as an option.
if (attributes.orientation === 'horizontal' && this.get('locked') && !options.override) {
return Drupal.t('The toolbar cannot be set to a horizontal orientation when it is locked.');
}
}
});
}(Backbone, Drupal));

View file

@ -0,0 +1,257 @@
/**
* @file
* Defines the behavior of the Drupal administration toolbar.
*/
(function ($, Drupal, drupalSettings) {
'use strict';
// Merge run-time settings with the defaults.
var options = $.extend(
{
breakpoints: {
'toolbar.narrow': '',
'toolbar.standard': '',
'toolbar.wide': ''
}
},
drupalSettings.toolbar,
// Merge strings on top of drupalSettings so that they are not mutable.
{
strings: {
horizontal: Drupal.t('Horizontal orientation'),
vertical: Drupal.t('Vertical orientation')
}
}
);
/**
* Registers tabs with the toolbar.
*
* The Drupal toolbar allows modules to register top-level tabs. These may
* point directly to a resource or toggle the visibility of a tray.
*
* Modules register tabs with hook_toolbar().
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the toolbar rendering functionality to the toolbar element.
*/
Drupal.behaviors.toolbar = {
attach: function (context) {
// Verify that the user agent understands media queries. Complex admin
// toolbar layouts require media query support.
if (!window.matchMedia('only screen').matches) {
return;
}
// Process the administrative toolbar.
$(context).find('#toolbar-administration').once('toolbar').each(function () {
// Establish the toolbar models and views.
var model = Drupal.toolbar.models.toolbarModel = new Drupal.toolbar.ToolbarModel({
locked: JSON.parse(localStorage.getItem('Drupal.toolbar.trayVerticalLocked')) || false,
activeTab: document.getElementById(JSON.parse(localStorage.getItem('Drupal.toolbar.activeTabID')))
});
Drupal.toolbar.views.toolbarVisualView = new Drupal.toolbar.ToolbarVisualView({
el: this,
model: model,
strings: options.strings
});
Drupal.toolbar.views.toolbarAuralView = new Drupal.toolbar.ToolbarAuralView({
el: this,
model: model,
strings: options.strings
});
Drupal.toolbar.views.bodyVisualView = new Drupal.toolbar.BodyVisualView({
el: this,
model: model
});
// Render collapsible menus.
var menuModel = Drupal.toolbar.models.menuModel = new Drupal.toolbar.MenuModel();
Drupal.toolbar.views.menuVisualView = new Drupal.toolbar.MenuVisualView({
el: $(this).find('.toolbar-menu-administration').get(0),
model: menuModel,
strings: options.strings
});
// Handle the resolution of Drupal.toolbar.setSubtrees.
// This is handled with a deferred so that the function may be invoked
// asynchronously.
Drupal.toolbar.setSubtrees.done(function (subtrees) {
menuModel.set('subtrees', subtrees);
var theme = drupalSettings.ajaxPageState.theme;
localStorage.setItem('Drupal.toolbar.subtrees.' + theme, JSON.stringify(subtrees));
// Indicate on the toolbarModel that subtrees are now loaded.
model.set('areSubtreesLoaded', true);
});
// Attach a listener to the configured media query breakpoints.
for (var label in options.breakpoints) {
if (options.breakpoints.hasOwnProperty(label)) {
var mq = options.breakpoints[label];
var mql = Drupal.toolbar.mql[label] = window.matchMedia(mq);
// Curry the model and the label of the media query breakpoint to
// the mediaQueryChangeHandler function.
mql.addListener(Drupal.toolbar.mediaQueryChangeHandler.bind(null, model, label));
// Fire the mediaQueryChangeHandler for each configured breakpoint
// so that they process once.
Drupal.toolbar.mediaQueryChangeHandler.call(null, model, label, mql);
}
}
// Trigger an initial attempt to load menu subitems. This first attempt
// is made after the media query handlers have had an opportunity to
// process. The toolbar starts in the vertical orientation by default,
// unless the viewport is wide enough to accommodate a horizontal
// orientation. Thus we give the Toolbar a chance to determine if it
// should be set to horizontal orientation before attempting to load
// menu subtrees.
Drupal.toolbar.views.toolbarVisualView.loadSubtrees();
$(document)
// Update the model when the viewport offset changes.
.on('drupalViewportOffsetChange.toolbar', function (event, offsets) {
model.set('offsets', offsets);
});
// Broadcast model changes to other modules.
model
.on('change:orientation', function (model, orientation) {
$(document).trigger('drupalToolbarOrientationChange', orientation);
})
.on('change:activeTab', function (model, tab) {
$(document).trigger('drupalToolbarTabChange', tab);
})
.on('change:activeTray', function (model, tray) {
$(document).trigger('drupalToolbarTrayChange', tray);
});
// If the toolbar's orientation is horizontal and no active tab is
// defined then show the tray of the first toolbar tab by default (but
// not the first 'Home' toolbar tab).
if (Drupal.toolbar.models.toolbarModel.get('orientation') === 'horizontal' && Drupal.toolbar.models.toolbarModel.get('activeTab') === null) {
Drupal.toolbar.models.toolbarModel.set({
activeTab: $('.toolbar-bar .toolbar-tab:not(.home-toolbar-tab) a').get(0)
});
}
});
}
};
/**
* Toolbar methods of Backbone objects.
*
* @namespace
*/
Drupal.toolbar = {
/**
* A hash of View instances.
*
* @type {object.<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) {
switch (label) {
case 'toolbar.narrow':
model.set({
isOriented: mql.matches,
isTrayToggleVisible: false
});
// If the toolbar doesn't have an explicit orientation yet, or if the
// narrow media query doesn't match then set the orientation to
// vertical.
if (!mql.matches || !model.get('orientation')) {
model.set({orientation: 'vertical'}, {validate: true});
}
break;
case 'toolbar.standard':
model.set({
isFixed: mql.matches
});
break;
case 'toolbar.wide':
model.set({
orientation: ((mql.matches) ? 'horizontal' : 'vertical')
}, {validate: true});
// The tray orientation toggle visibility does not need to be
// validated.
model.set({
isTrayToggleVisible: mql.matches
});
break;
default:
break;
}
}
};
/**
* A toggle is an interactive element often bound to a click handler.
*
* @return {string}
* A string representing a DOM fragment.
*/
Drupal.theme.toolbarOrientationToggle = function () {
return '<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));

View file

@ -0,0 +1,197 @@
/**
* @file
* Builds a nested accordion widget.
*
* Invoke on an HTML list element with the jQuery plugin pattern.
*
* @example
* $('.toolbar-menu').drupalToolbarMenu();
*/
(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.
$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) {
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));
}
});
}
/**
* Adds a level class to each list based on its depth in the menu.
*
* This function is called recursively on each sub level of lists elements
* until the depth of the menu is exhausted.
*
* @param {jQuery} $lists
* A jQuery object of ul elements.
*
* @param {number} level
* The current level number to be assigned to the list elements.
*/
function markListLevels($lists, level) {
level = (!level) ? 1 : level;
var $lis = $lists.children('li').addClass('level-' + level);
$lists = $lis.children('ul');
if ($lists.length) {
markListLevels($lists, level + 1);
}
}
/**
* On page load, open the active menu item.
*
* Marks the trail of the active link in the menu back to the root of the
* menu with .menu-item--active-trail.
*
* @param {jQuery} $menu
* The root of the menu.
*/
function openActiveItem($menu) {
var pathItem = $menu.find('a[href="' + location.pathname + '"]');
if (pathItem.length && !activeItem) {
activeItem = location.pathname;
}
if (activeItem) {
var $activeItem = $menu.find('a[href="' + activeItem + '"]').addClass('menu-item--active');
var $activeTrail = $activeItem.parentsUntil('.root', 'li').addClass('menu-item--active-trail');
toggleList($activeTrail, true);
}
}
// Return the jQuery object.
return this.each(function (selector) {
var $menu = $(this).once('toolbar-menu');
if ($menu.length) {
// Bind event handlers.
$menu
.on('click.toolbar', '.toolbar-box', toggleClickHandler)
.on('click.toolbar', '.toolbar-box a', linkClickHandler);
$menu.addClass('root');
initItems($menu);
markListLevels($menu);
// Restore previous and active states.
openActiveItem($menu);
}
});
};
/**
* A toggle is an interactive element often bound to a click handler.
*
* @param {object} options
* Options for the button.
* @param {string} options.class
* Class to set on the button.
* @param {string} options.action
* Action for the button.
* @param {string} options.text
* Used as label for the button.
*
* @return {string}
* A string representing a DOM fragment.
*/
Drupal.theme.toolbarMenuItemToggle = function (options) {
return '<button class="' + options['class'] + '"><span class="action">' + options.action + '</span><span class="label">' + options.text + '</span></button>';
};
}(jQuery, Drupal, drupalSettings));

View file

@ -0,0 +1,53 @@
/**
* @file
* A Backbone view for the body element.
*/
(function ($, Drupal, Backbone) {
'use strict';
Drupal.toolbar.BodyVisualView = Backbone.View.extend(/** @lends Drupal.toolbar.BodyVisualView# */{
/**
* Adjusts the body element with the toolbar position and dimension changes.
*
* @constructs
*
* @augments Backbone.View
*/
initialize: function () {
this.listenTo(this.model, 'change:orientation change:offsets change:activeTray change:isOriented change:isFixed change:isViewportOverflowConstrained', this.render);
},
/**
* @inheritdoc
*/
render: function () {
var $body = $('body');
var orientation = this.model.get('orientation');
var isOriented = this.model.get('isOriented');
var isViewportOverflowConstrained = this.model.get('isViewportOverflowConstrained');
$body
// We are using JavaScript to control media-query handling for two
// reasons: (1) Using JavaScript let's us leverage the breakpoint
// configurations and (2) the CSS is really complex if we try to hide
// some styling from browsers that don't understand CSS media queries.
// If we drive the CSS from classes added through JavaScript,
// then the CSS becomes simpler and more robust.
.toggleClass('toolbar-vertical', (orientation === 'vertical'))
.toggleClass('toolbar-horizontal', (isOriented && orientation === 'horizontal'))
// When the toolbar is fixed, it will not scroll with page scrolling.
.toggleClass('toolbar-fixed', (isViewportOverflowConstrained || this.model.get('isFixed')))
// Toggle the toolbar-tray-open class on the body element. The class is
// applied when a toolbar tray is active. Padding might be applied to
// the body element to prevent the tray from overlapping content.
.toggleClass('toolbar-tray-open', !!this.model.get('activeTray'))
// Apply padding to the top of the body to offset the placement of the
// toolbar bar element.
.css('padding-top', this.model.get('offsets').top);
}
});
}(jQuery, Drupal, Backbone));

View file

@ -0,0 +1,46 @@
/**
* @file
* A Backbone view for the collapsible menus.
*/
(function ($, Backbone, Drupal) {
'use strict';
Drupal.toolbar.MenuVisualView = Backbone.View.extend(/** @lends Drupal.toolbar.MenuVisualView# */{
/**
* Backbone View for collapsible menus.
*
* @constructs
*
* @augments Backbone.View
*/
initialize: function () {
this.listenTo(this.model, 'change:subtrees', this.render);
},
/**
* @inheritdoc
*/
render: function () {
var subtrees = this.model.get('subtrees');
// Add subtrees.
for (var id in subtrees) {
if (subtrees.hasOwnProperty(id)) {
this.$el
.find('#toolbar-link-' + id)
.once('toolbar-subtrees')
.after(subtrees[id]);
}
}
// Render the main menu as a nested, collapsible accordion.
if ('drupalToolbarMenu' in $.fn) {
this.$el
.children('.toolbar-menu')
.drupalToolbarMenu();
}
}
});
}(jQuery, Backbone, Drupal));

View file

@ -0,0 +1,70 @@
/**
* @file
* A Backbone view for the aural feedback of the toolbar.
*/
(function (Backbone, Drupal) {
'use strict';
Drupal.toolbar.ToolbarAuralView = Backbone.View.extend(/** @lends Drupal.toolbar.ToolbarAuralView# */{
/**
* Backbone view for the aural feedback of the toolbar.
*
* @constructs
*
* @augments Backbone.View
*
* @param {object} options
* Options for the view.
* @param {object} options.strings
* Various strings to use in the view.
*/
initialize: function (options) {
this.strings = options.strings;
this.listenTo(this.model, 'change:orientation', this.onOrientationChange);
this.listenTo(this.model, 'change:activeTray', this.onActiveTrayChange);
},
/**
* Announces an orientation change.
*
* @param {Drupal.toolbar.ToolbarModel} model
* The toolbar model in question.
* @param {string} orientation
* The new value of the orientation attribute in the model.
*/
onOrientationChange: function (model, orientation) {
Drupal.announce(Drupal.t('Tray orientation changed to @orientation.', {
'@orientation': orientation
}));
},
/**
* Announces a changed active tray.
*
* @param {Drupal.toolbar.ToolbarModel} model
* The toolbar model in question.
* @param {HTMLElement} tray
* The new value of the tray attribute in the model.
*/
onActiveTrayChange: function (model, tray) {
var relevantTray = (tray === null) ? model.previous('activeTray') : tray;
var action = (tray === null) ? Drupal.t('closed') : Drupal.t('opened');
var trayNameElement = relevantTray.querySelector('.toolbar-tray-name');
var text;
if (trayNameElement !== null) {
text = Drupal.t('Tray "@tray" @action.', {
'@tray': trayNameElement.textContent, '@action': action
});
}
else {
text = Drupal.t('Tray @action.', {'@action': action});
}
Drupal.announce(text);
}
});
}(Backbone, Drupal));

View file

@ -0,0 +1,305 @@
/**
* @file
* A Backbone view for the toolbar element. Listens to mouse & touch.
*/
(function ($, Drupal, drupalSettings, Backbone) {
'use strict';
Drupal.toolbar.ToolbarVisualView = Backbone.View.extend(/** @lends Drupal.toolbar.ToolbarVisualView# */{
/**
* Event map for the `ToolbarVisualView`.
*
* @return {object}
* A map of events.
*/
events: function () {
// Prevents delay and simulated mouse events.
var touchEndToClick = function (event) {
event.preventDefault();
event.target.click();
};
return {
'click .toolbar-bar .toolbar-tab .trigger': 'onTabClick',
'click .toolbar-toggle-orientation button': 'onOrientationToggleClick',
'touchend .toolbar-bar .toolbar-tab .trigger': touchEndToClick,
'touchend .toolbar-toggle-orientation button': touchEndToClick
};
},
/**
* Backbone view for the toolbar element. Listens to mouse & touch.
*
* @constructs
*
* @augments Backbone.View
*
* @param {object} options
* Options for the view object.
* @param {object} options.strings
* Various strings to use in the view.
*/
initialize: function (options) {
this.strings = options.strings;
this.listenTo(this.model, 'change:activeTab change:orientation change:isOriented change:isTrayToggleVisible', this.render);
this.listenTo(this.model, 'change:mqMatches', this.onMediaQueryChange);
this.listenTo(this.model, 'change:offsets', this.adjustPlacement);
// Add the tray orientation toggles.
this.$el
.find('.toolbar-tray .toolbar-lining')
.append(Drupal.theme('toolbarOrientationToggle'));
// Trigger an activeTab change so that listening scripts can respond on
// page load. This will call render.
this.model.trigger('change:activeTab');
},
/**
* @inheritdoc
*
* @return {Drupal.toolbar.ToolbarVisualView}
* The `ToolbarVisualView` instance.
*/
render: function () {
this.updateTabs();
this.updateTrayOrientation();
this.updateBarAttributes();
// Load the subtrees if the orientation of the toolbar is changed to
// vertical. This condition responds to the case that the toolbar switches
// from horizontal to vertical orientation. The toolbar starts in a
// vertical orientation by default and then switches to horizontal during
// initialization if the media query conditions are met. Simply checking
// that the orientation is vertical here would result in the subtrees
// always being loaded, even when the toolbar initialization ultimately
// results in a horizontal orientation.
//
// @see Drupal.behaviors.toolbar.attach() where admin menu subtrees
// loading is invoked during initialization after media query conditions
// have been processed.
if (this.model.changed.orientation === 'vertical' || this.model.changed.activeTab) {
this.loadSubtrees();
}
// Trigger a recalculation of viewport displacing elements. Use setTimeout
// to ensure this recalculation happens after changes to visual elements
// have processed.
window.setTimeout(function () {
Drupal.displace(true);
}, 0);
return this;
},
/**
* Responds to a toolbar tab click.
*
* @param {jQuery.Event} event
* The event triggered.
*/
onTabClick: function (event) {
// If this tab has a tray associated with it, it is considered an
// activatable tab.
if (event.target.hasAttribute('data-toolbar-tray')) {
var activeTab = this.model.get('activeTab');
var clickedTab = event.target;
// Set the event target as the active item if it is not already.
this.model.set('activeTab', (!activeTab || clickedTab !== activeTab) ? clickedTab : null);
event.preventDefault();
event.stopPropagation();
}
},
/**
* Toggles the orientation of a toolbar tray.
*
* @param {jQuery.Event} event
* The event triggered.
*/
onOrientationToggleClick: function (event) {
var orientation = this.model.get('orientation');
// Determine the toggle-to orientation.
var antiOrientation = (orientation === 'vertical') ? 'horizontal' : 'vertical';
var locked = antiOrientation === 'vertical';
// Remember the locked state.
if (locked) {
localStorage.setItem('Drupal.toolbar.trayVerticalLocked', 'true');
}
else {
localStorage.removeItem('Drupal.toolbar.trayVerticalLocked');
}
// Update the model.
this.model.set({
locked: locked,
orientation: antiOrientation
}, {
validate: true,
override: true
});
event.preventDefault();
event.stopPropagation();
},
/**
* Updates the display of the tabs: toggles a tab and the associated tray.
*/
updateTabs: function () {
var $tab = $(this.model.get('activeTab'));
// Deactivate the previous tab.
$(this.model.previous('activeTab'))
.removeClass('is-active')
.prop('aria-pressed', false);
// Deactivate the previous tray.
$(this.model.previous('activeTray'))
.removeClass('is-active');
// Activate the selected tab.
if ($tab.length > 0) {
$tab
.addClass('is-active')
// Mark the tab as pressed.
.prop('aria-pressed', true);
var name = $tab.attr('data-toolbar-tray');
// Store the active tab name or remove the setting.
var id = $tab.get(0).id;
if (id) {
localStorage.setItem('Drupal.toolbar.activeTabID', JSON.stringify(id));
}
// Activate the associated tray.
var $tray = this.$el.find('[data-toolbar-tray="' + name + '"].toolbar-tray');
if ($tray.length) {
$tray.addClass('is-active');
this.model.set('activeTray', $tray.get(0));
}
else {
// There is no active tray.
this.model.set('activeTray', null);
}
}
else {
// There is no active tray.
this.model.set('activeTray', null);
localStorage.removeItem('Drupal.toolbar.activeTabID');
}
},
/**
* Update the attributes of the toolbar bar element.
*/
updateBarAttributes: function () {
var isOriented = this.model.get('isOriented');
if (isOriented) {
this.$el.find('.toolbar-bar').attr('data-offset-top', '');
}
else {
this.$el.find('.toolbar-bar').removeAttr('data-offset-top');
}
// Toggle between a basic vertical view and a more sophisticated
// horizontal and vertical display of the toolbar bar and trays.
this.$el.toggleClass('toolbar-oriented', isOriented);
},
/**
* Updates the orientation of the active tray if necessary.
*/
updateTrayOrientation: function () {
var orientation = this.model.get('orientation');
// The antiOrientation is used to render the view of action buttons like
// the tray orientation toggle.
var antiOrientation = (orientation === 'vertical') ? 'horizontal' : 'vertical';
// Update the orientation of the trays.
var $trays = this.$el.find('.toolbar-tray')
.removeClass('toolbar-tray-horizontal toolbar-tray-vertical')
.addClass('toolbar-tray-' + orientation);
// Update the tray orientation toggle button.
var iconClass = 'toolbar-icon-toggle-' + orientation;
var iconAntiClass = 'toolbar-icon-toggle-' + antiOrientation;
var $orientationToggle = this.$el.find('.toolbar-toggle-orientation')
.toggle(this.model.get('isTrayToggleVisible'));
$orientationToggle.find('button')
.val(antiOrientation)
.attr('title', this.strings[antiOrientation])
.text(this.strings[antiOrientation])
.removeClass(iconClass)
.addClass(iconAntiClass);
// Update data offset attributes for the trays.
var dir = document.documentElement.dir;
var edge = (dir === 'rtl') ? 'right' : 'left';
// Remove data-offset attributes from the trays so they can be refreshed.
$trays.removeAttr('data-offset-left data-offset-right data-offset-top');
// If an active vertical tray exists, mark it as an offset element.
$trays.filter('.toolbar-tray-vertical.is-active').attr('data-offset-' + edge, '');
// If an active horizontal tray exists, mark it as an offset element.
$trays.filter('.toolbar-tray-horizontal.is-active').attr('data-offset-top', '');
},
/**
* Sets the tops of the trays so that they align with the bottom of the bar.
*/
adjustPlacement: function () {
var $trays = this.$el.find('.toolbar-tray');
if (!this.model.get('isOriented')) {
$trays.css('margin-top', 0);
$trays.removeClass('toolbar-tray-horizontal').addClass('toolbar-tray-vertical');
}
else {
// The toolbar container is invisible. Its placement is used to
// determine the container for the trays.
$trays.css('margin-top', this.$el.find('.toolbar-bar').outerHeight());
}
},
/**
* Calls the endpoint URI that builds an AJAX command with the rendered
* subtrees.
*
* The rendered admin menu subtrees HTML is cached on the client in
* localStorage until the cache of the admin menu subtrees on the server-
* side is invalidated. The subtreesHash is stored in localStorage as well
* and compared to the subtreesHash in drupalSettings to determine when the
* admin menu subtrees cache has been invalidated.
*/
loadSubtrees: function () {
var $activeTab = $(this.model.get('activeTab'));
var orientation = this.model.get('orientation');
// Only load and render the admin menu subtrees if:
// (1) They have not been loaded yet.
// (2) The active tab is the administration menu tab, indicated by the
// presence of the data-drupal-subtrees attribute.
// (3) The orientation of the tray is vertical.
if (!this.model.get('areSubtreesLoaded') && typeof $activeTab.data('drupal-subtrees') !== 'undefined' && orientation === 'vertical') {
var subtreesHash = drupalSettings.toolbar.subtreesHash;
var theme = drupalSettings.ajaxPageState.theme;
var endpoint = Drupal.url('toolbar/subtrees/' + subtreesHash);
var cachedSubtreesHash = localStorage.getItem('Drupal.toolbar.subtreesHash.' + theme);
var cachedSubtrees = JSON.parse(localStorage.getItem('Drupal.toolbar.subtrees.' + theme));
var isVertical = this.model.get('orientation') === 'vertical';
// If we have the subtrees in localStorage and the subtree hash has not
// changed, then use the cached data.
if (isVertical && subtreesHash === cachedSubtreesHash && cachedSubtrees) {
Drupal.toolbar.setSubtrees.resolve(cachedSubtrees);
}
// Only make the call to get the subtrees if the orientation of the
// toolbar is vertical.
else if (isVertical) {
// Remove the cached menu information.
localStorage.removeItem('Drupal.toolbar.subtreesHash.' + theme);
localStorage.removeItem('Drupal.toolbar.subtrees.' + theme);
// The AJAX response's command will trigger the resolve method of the
// Drupal.toolbar.setSubtrees Promise.
Drupal.ajax({url: endpoint}).execute();
// Cache the hash for the subtrees locally.
localStorage.setItem('Drupal.toolbar.subtreesHash.' + theme, subtreesHash);
}
}
}
});
}(jQuery, Drupal, drupalSettings, Backbone));

View file

@ -0,0 +1,39 @@
<?php
namespace Drupal\toolbar\Ajax;
use Drupal\Core\Ajax\CommandInterface;
/**
* Defines an AJAX command that sets the toolbar subtrees.
*/
class SetSubtreesCommand implements CommandInterface {
/**
* The toolbar subtrees.
*
* @var array
*/
protected $subtrees;
/**
* Constructs a SetSubtreesCommand object.
*
* @param array $subtrees
* The toolbar subtrees that will be set.
*/
public function __construct($subtrees) {
$this->subtrees = $subtrees;
}
/**
* {@inheritdoc}
*/
public function render() {
return [
'command' => 'setToolbarSubtrees',
'subtrees' => array_map('strval', $this->subtrees),
];
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace Drupal\toolbar\Controller;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Controller\ControllerBase;
use Drupal\toolbar\Ajax\SetSubtreesCommand;
/**
* Defines a controller for the toolbar module.
*/
class ToolbarController extends ControllerBase {
/**
* Returns an AJAX response to render the toolbar subtrees.
*
* @return \Drupal\Core\Ajax\AjaxResponse
*/
public function subtreesAjax() {
list($subtrees, $cacheability) = toolbar_get_rendered_subtrees();
$response = new AjaxResponse();
$response->addCommand(new SetSubtreesCommand($subtrees));
// The Expires HTTP header is the heart of the client-side HTTP caching. The
// additional server-side page cache only takes effect when the client
// accesses the callback URL again (e.g., after clearing the browser cache
// or when force-reloading a Drupal page).
$max_age = 365 * 24 * 60 * 60;
$response->setPrivate();
$response->setMaxAge($max_age);
$expires = new \DateTime();
$expires->setTimestamp(REQUEST_TIME + $max_age);
$response->setExpires($expires);
return $response;
}
/**
* Checks access for the subtree controller.
*
* @param string $hash
* The hash of the toolbar subtrees.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function checkSubTreeAccess($hash) {
$expected_hash = _toolbar_get_subtrees_hash()[0];
return AccessResult::allowedIf($this->currentUser()->hasPermission('access toolbar') && Crypt::hashEquals($expected_hash, $hash))->cachePerPermissions();
}
}

View file

@ -0,0 +1,116 @@
<?php
namespace Drupal\toolbar\Element;
use Drupal\Component\Utility\Html;
use Drupal\Core\Render\Element\RenderElement;
use Drupal\Core\Render\Element;
/**
* Provides a render element for the default Drupal toolbar.
*
* @RenderElement("toolbar")
*/
class Toolbar extends RenderElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#pre_render' => array(
array($class, 'preRenderToolbar'),
),
'#theme' => 'toolbar',
'#attached' => array(
'library' => array(
'toolbar/toolbar',
),
),
// Metadata for the toolbar wrapping element.
'#attributes' => array(
// The id cannot be simply "toolbar" or it will clash with the
// simpletest tests listing which produces a checkbox with attribute
// id="toolbar".
'id' => 'toolbar-administration',
'role' => 'group',
'aria-label' => $this->t('Site administration toolbar'),
),
// Metadata for the administration bar.
'#bar' => array(
'#heading' => $this->t('Toolbar items'),
'#attributes' => array(
'id' => 'toolbar-bar',
'role' => 'navigation',
'aria-label' => $this->t('Toolbar items'),
),
),
);
}
/**
* Builds the Toolbar as a structured array ready for drupal_render().
*
* Since building the toolbar takes some time, it is done just prior to
* rendering to ensure that it is built only if it will be displayed.
*
* @param array $element
* A renderable array.
*
* @return array
* A renderable array.
*
* @see toolbar_page_top()
*/
public static function preRenderToolbar($element) {
// Get the configured breakpoints to switch from vertical to horizontal
// toolbar presentation.
$breakpoints = static::breakpointManager()->getBreakpointsByGroup('toolbar');
if (!empty($breakpoints)) {
$media_queries = array();
foreach ($breakpoints as $id => $breakpoint) {
$media_queries[$id] = $breakpoint->getMediaQuery();
}
$element['#attached']['drupalSettings']['toolbar']['breakpoints'] = $media_queries;
}
$module_handler = static::moduleHandler();
// Get toolbar items from all modules that implement hook_toolbar().
$items = $module_handler->invokeAll('toolbar');
// Allow for altering of hook_toolbar().
$module_handler->alter('toolbar', $items);
// Sort the children.
uasort($items, array('\Drupal\Component\Utility\SortArray', 'sortByWeightProperty'));
// Merge in the original toolbar values.
$element = array_merge($element, $items);
// Assign each item a unique ID, based on its key.
foreach (Element::children($element) as $key) {
$element[$key]['#id'] = Html::getId('toolbar-item-' . $key);
}
return $element;
}
/**
* Wraps the breakpoint manager.
*
* @return \Drupal\breakpoint\BreakpointManagerInterface
*/
protected static function breakpointManager() {
return \Drupal::service('breakpoint.manager');
}
/**
* Wraps the module handler.
*
* @return \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected static function moduleHandler() {
return \Drupal::moduleHandler();
}
}

View file

@ -0,0 +1,87 @@
<?php
namespace Drupal\toolbar\Element;
use Drupal\Core\Render\Element\RenderElement;
use Drupal\Core\Url;
/**
* Provides a toolbar item that is wrapped in markup for common styling.
*
* The 'tray' property contains a renderable array.
*
* @RenderElement("toolbar_item")
*/
class ToolbarItem extends RenderElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#pre_render' => array(
array($class, 'preRenderToolbarItem'),
),
'tab' => array(
'#type' => 'link',
'#title' => NULL,
'#url' => Url::fromRoute('<front>'),
),
);
}
/**
* Provides markup for associating a tray trigger with a tray element.
*
* A tray is a responsive container that wraps renderable content. Trays
* present content well on small and large screens alike.
*
* @param array $element
* A renderable array.
*
* @return array
* A renderable array.
*/
public static function preRenderToolbarItem($element) {
$id = $element['#id'];
// Provide attributes for a toolbar item.
$attributes = array(
'id' => $id,
);
// If tray content is present, markup the tray and its associated trigger.
if (!empty($element['tray'])) {
// Provide attributes necessary for trays.
$attributes += array(
'data-toolbar-tray' => $id . '-tray',
'aria-owns' => $id . '-tray',
'role' => 'button',
'aria-pressed' => 'false',
);
// Merge in module-provided attributes.
$element['tab'] += array('#attributes' => array());
$element['tab']['#attributes'] += $attributes;
$element['tab']['#attributes']['class'][] = 'trigger';
// Provide attributes for the tray theme wrapper.
$attributes = array(
'id' => $id . '-tray',
'data-toolbar-tray' => $id . '-tray',
);
// Merge in module-provided attributes.
if (!isset($element['tray']['#wrapper_attributes'])) {
$element['tray']['#wrapper_attributes'] = array();
}
$element['tray']['#wrapper_attributes'] += $attributes;
$element['tray']['#wrapper_attributes']['class'][] = 'toolbar-tray';
}
$element['tab']['#attributes']['class'][] = 'toolbar-item';
return $element;
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace Drupal\toolbar\Menu;
use Drupal\Core\Menu\MenuLinkTree;
/**
* Extends MenuLinkTree to add specific theme suggestions for the toolbar.
*/
class ToolbarMenuLinkTree extends MenuLinkTree {
/**
* {@inheritdoc}
*/
public function build(array $tree, $level = 0) {
if ($level == 0) {
if (!$tree) {
return array();
}
$build = parent::build($tree, $level);
/** @var \Drupal\Core\Menu\MenuLinkInterface $link */
$first_link = reset($tree)->link;
// Get the menu name of the first link.
$menu_name = $first_link->getMenuName();
// Add a more specific theme suggestion to differentiate this rendered
// menu from others.
$build['#menu_name'] = $menu_name;
$build['#theme'] = 'menu__toolbar__' . strtr($menu_name, '-', '_');
return $build;
}
else {
return parent::build($tree, $level);
}
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace Drupal\toolbar\PageCache;
use Drupal\Core\PageCache\RequestPolicyInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Cache policy for the toolbar page cache service.
*
* This policy allows caching of requests directed to /toolbar/subtrees/{hash}
* even for authenticated users.
*/
class AllowToolbarPath implements RequestPolicyInterface {
/**
* {@inheritdoc}
*/
public function check(Request $request) {
// Note that this regular expression matches the end of pathinfo in order to
// support multilingual sites using path prefixes.
if (preg_match('#/toolbar/subtrees/[^/]+(/[^/]+)?$#', $request->getPathInfo())) {
return static::ALLOW;
}
}
}

View file

@ -0,0 +1,429 @@
<?php
namespace Drupal\toolbar\Tests;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Url;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\simpletest\WebTestBase;
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 WebTestBase {
/**
* 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 = array('node', 'block', 'menu_ui', 'user', 'taxonomy', 'toolbar', 'language', 'test_page_test', 'locale');
protected function setUp() {
parent::setUp();
$perms = array(
'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.
*/
function testModuleStatusChangeSubtreesHashCacheClear() {
// Uninstall a module.
$edit = array();
$edit['uninstall[taxonomy]'] = TRUE;
$this->drupalPostForm('admin/modules/uninstall', $edit, t('Uninstall'));
// Confirm the uninstall form.
$this->drupalPostForm(NULL, array(), t('Uninstall'));
$this->rebuildContainer();
// Assert that the subtrees hash has been altered because the subtrees
// structure changed.
$this->assertDifferentHash();
// Enable a module.
$edit = array();
$edit['modules[Core][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.
*/
function testMenuLinkUpdateSubtreesHashCacheClear() {
// The ID of a (any) admin menu link.
$admin_menu_link_id = 'system.admin_config_development';
// Disable the link.
$edit = array();
$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.
*/
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 = array();
$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(array('administer content types',));
// Assign the role to the user.
$this->drupalPostForm('user/' . $this->adminUser->id() . '/edit', array("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.
*/
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(array('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', array("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.
*/
function testLocaleTranslationSubtreesHashCacheClear() {
$admin_user = $this->adminUser;
// User to translate and delete string.
$translate_user = $this->drupalCreateUser(array('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 = array(
'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, array(), array('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 = array(
'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[0]['name'];
$edit = array(
$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.
*/
function testSubtreesJsonRequest() {
$admin_user = $this->adminUser;
$this->drupalLogin($admin_user);
// Request a new page to refresh the drupalSettings object.
$subtrees_hash = $this->getSubtreesHash();
$ajax_result = $this->drupalGetAjax('toolbar/subtrees/' . $subtrees_hash);
$this->assertResponse('200');
$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.
*/
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', array('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;
}
}

View file

@ -0,0 +1,146 @@
<?php
namespace Drupal\toolbar\Tests;
use Drupal\Core\Cache\Cache;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\simpletest\WebTestBase;
use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
/**
* Tests the cache contexts for toolbar.
*
* @group toolbar
*/
class ToolbarCacheContextsTest extends WebTestBase {
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();
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace Drupal\toolbar\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests the implementation of hook_toolbar() by a module.
*
* @group toolbar
*/
class ToolbarHookToolbarTest extends WebTestBase {
/**
* A user with permission to access the administrative toolbar.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('toolbar', 'toolbar_test', 'test_page_test');
protected function setUp() {
parent::setUp();
// Create an administrative user and log it in.
$this->adminUser = $this->drupalCreateUser(array('access toolbar'));
$this->drupalLogin($this->adminUser);
}
/**
* Tests for a tab and tray provided by a module implementing hook_toolbar().
*/
function testHookToolbar() {
$this->drupalGet('test-page');
$this->assertResponse(200);
// Assert that the toolbar is present in the HTML.
$this->assertRaw('id="toolbar-administration"');
// Assert that the tab registered by toolbar_test is present.
$this->assertRaw('id="toolbar-tab-testing"');
// Assert that the tab item descriptions are present.
$this->assertRaw('title="Test tab"');
// Assert that the tray registered by toolbar_test is present.
$this->assertRaw('id="toolbar-tray-testing"');
// Assert that tray item descriptions are present.
$this->assertRaw('title="Test link 1 title"');
}
}

View file

@ -0,0 +1,96 @@
<?php
namespace Drupal\toolbar\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests that the toolbar icon class remains for translated menu items.
*
* @group toolbar
*/
class ToolbarMenuTranslationTest extends WebTestBase {
/**
* A user with permission to access the administrative toolbar.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('toolbar', 'toolbar_test', 'locale', 'locale_test');
protected function setUp() {
parent::setUp();
// Create an administrative user and log it in.
$this->adminUser = $this->drupalCreateUser(array('access toolbar', 'translate interface', 'administer languages', 'access administration pages'));
$this->drupalLogin($this->adminUser);
}
/**
* Tests that toolbar classes don't change when adding a translation.
*/
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 = array(
'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[0]['name'];
$edit = array(
$lid => $menu_item_translated,
);
$this->drupalPostForm('admin/config/regional/translate', $edit, t('Save translations'));
// Search for the translated menu item.
$search = array(
'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.');
}
}

View file

@ -0,0 +1,57 @@
{#
/**
* @file
* Default theme implementation to display a toolbar menu.
*
* Available variables:
* - menu_name: The machine name of the menu.
* - items: A nested list of menu items. Each menu item contains:
* - attributes: HTML attributes for the menu item.
* - below: The menu item child items.
* - title: The menu link title.
* - url: The menu link url, instance of \Drupal\Core\Url
* - localized_options: Menu link localized options.
* - is_expanded: TRUE if the link has visible children within the current
* menu tree.
* - is_collapsed: TRUE if the link has children within the current menu tree
* that are not currently visible.
* - in_active_trail: TRUE if the link is in the active trail.
*
* @ingroup themeable
*/
#}
{% import _self as menus %}
{#
We call a macro which calls itself to render the full tree.
@see http://twig.sensiolabs.org/doc/tags/macro.html
#}
{{ menus.menu_links(items, attributes, 0) }}
{% macro menu_links(items, attributes, menu_level) %}
{% import _self as menus %}
{% if items %}
{% if menu_level == 0 %}
<ul{{ attributes.addClass('toolbar-menu') }}>
{% else %}
<ul class="toolbar-menu">
{% endif %}
{% for item in items %}
{%
set classes = [
'menu-item',
item.is_expanded ? 'menu-item--expanded',
item.is_collapsed ? 'menu-item--collapsed',
item.in_active_trail ? 'menu-item--active-trail',
]
%}
<li{{ item.attributes.addClass(classes) }}>
{{ link(item.title, item.url) }}
{% if item.below %}
{{ menus.menu_links(item.below, attributes, menu_level + 1) }}
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
{% endmacro %}

View file

@ -0,0 +1,48 @@
{#
/**
* @file
* Default theme implementation for the administrative toolbar.
*
* Available variables:
* - attributes: HTML attributes for the wrapper.
* - toolbar_attributes: HTML attributes to apply to the toolbar.
* - toolbar_heading: The heading or label for the toolbar.
* - tabs: List of tabs for the toolbar.
* - attributes: HTML attributes for the tab container.
* - link: Link or button for the menu tab.
* - trays: Toolbar tray list, each associated with a tab. Each tray in trays
* contains:
* - attributes: HTML attributes to apply to the tray.
* - label: The tray's label.
* - links: The tray menu links.
* - remainder: Any non-tray, non-tab elements left to be rendered.
*
* @see template_preprocess_toolbar()
*
* @ingroup themeable
*/
#}
<div{{ attributes.addClass('toolbar') }}>
<nav{{ toolbar_attributes.addClass('toolbar-bar') }}>
<h2 class="visually-hidden">{{ toolbar_heading }}</h2>
{% for key, tab in tabs %}
{% set tray = trays[key] %}
<div{{ tab.attributes.addClass('toolbar-tab') }}>
{{ tab.link }}
{% spaceless %}
<div{{ tray.attributes }}>
{% if tray.label %}
<nav class="toolbar-lining clearfix" role="navigation" aria-label="{{ tray.label }}">
<h3 class="toolbar-tray-name visually-hidden">{{ tray.label }}</h3>
{% else %}
<nav class="toolbar-lining clearfix" role="navigation">
{% endif %}
{{ tray.links }}
</nav>
</div>
{% endspaceless %}
</div>
{% endfor %}
</nav>
{{ remainder }}
</div>

View file

@ -0,0 +1,6 @@
name: 'Disable user toolbar'
type: module
description: 'Support module for testing toolbar without user toolbar'
package: Testing
version: VERSION
core: 8.x

View file

@ -0,0 +1,13 @@
<?php
/**
* @file
* Test module.
*/
/**
* Implements hook_toolbar_alter().
*/
function toolbar_disable_user_toolbar_toolbar_alter(&$items) {
unset($items['user']);
}

View file

@ -0,0 +1,6 @@
name: 'Toolbar module API tests'
type: module
description: 'Support module for toolbar testing.'
package: Testing
version: VERSION
core: 8.x

View file

@ -0,0 +1,49 @@
<?php
/**
* @file
* A dummy module to test API interaction with the Toolbar module.
*/
use Drupal\Core\Url;
/**
* Implements hook_toolbar().
*/
function toolbar_test_toolbar() {
$items['testing'] = array(
'#type' => 'toolbar_item',
'tab' => array(
'#type' => 'link',
'#title' => t('Test tab'),
'#url' => Url::fromRoute('<front>'),
'#options' => array(
'attributes' => array(
'id' => 'toolbar-tab-testing',
'title' => t('Test tab'),
),
),
),
'tray' => array(
'#heading' => t('Test tray'),
'#wrapper_attributes' => array(
'id' => 'toolbar-tray-testing',
),
'content' => array(
'#theme' => 'item_list',
'#items' => array(
\Drupal::l(t('link 1'), new Url('<front>', [], array('attributes' => array('title' => 'Test link 1 title')))),
\Drupal::l(t('link 2'), new Url('<front>', [], array('attributes' => array('title' => 'Test link 2 title')))),
\Drupal::l(t('link 3'), new Url('<front>', [], array('attributes' => array('title' => 'Test link 3 title')))),
),
'#attributes' => array(
'class' => array('toolbar-menu'),
),
),
),
'#weight' => 50,
);
return $items;
}

View file

@ -0,0 +1,51 @@
<?php
namespace Drupal\Tests\toolbar\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
/**
* Tests the JavaScript functionality of the toolbar.
*
* @group toolbar
*/
class ToolbarIntegrationTest extends JavascriptTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['toolbar', 'node'];
/**
* Tests if the toolbar can be toggled with JavaScript.
*/
public function testToolbarToggling() {
$admin_user = $this->drupalCreateUser([
'access toolbar',
'administer site configuration',
'access content overview',
]);
$this->drupalLogin($admin_user);
$this->drupalGet('<front>');
$page = $this->getSession()->getPage();
// Test that it is possible to toggle the toolbar tray.
$content = $page->findLink('Content');
$this->assertTrue($content->isVisible(), 'Toolbar tray is open by default.');
$page->clickLink('Manage');
$this->assertFalse($content->isVisible(), 'Toolbar tray is closed after clicking the "Manage" link.');
$page->clickLink('Manage');
$this->assertTrue($content->isVisible(), 'Toolbar tray is visible again after clicking the "Manage" button a second time.');
// Test toggling the toolbar tray between horizontal and vertical.
$tray = $page->findById('toolbar-item-administration-tray');
$this->assertFalse($tray->hasClass('toolbar-tray-vertical'), 'Toolbar tray is not vertically oriented by default.');
$page->pressButton('Vertical orientation');
$this->assertTrue($tray->hasClass('toolbar-tray-vertical'), 'After toggling the orientation the toolbar tray is now displayed vertically.');
$page->pressButton('Horizontal orientation');
$this->assertTrue($tray->hasClass('toolbar-tray-horizontal'), 'After toggling the orientation a second time the toolbar tray is displayed horizontally again.');
}
}

View file

@ -0,0 +1,60 @@
<?php
namespace Drupal\Tests\toolbar\Unit\PageCache;
use Drupal\toolbar\PageCache\AllowToolbarPath;
use Drupal\Core\PageCache\RequestPolicyInterface;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\HttpFoundation\Request;
/**
* @coversDefaultClass \Drupal\toolbar\PageCache\AllowToolbarPath
* @group toolbar
*/
class AllowToolbarPathTest extends UnitTestCase {
/**
* The toolbar path policy under test.
*
* @var \Drupal\toolbar\PageCache\AllowToolbarPath
*/
protected $policy;
protected function setUp() {
$this->policy = new AllowToolbarPath();
}
/**
* Asserts that caching is allowed if the request goes to toolbar subtree.
*
* @dataProvider providerTestAllowToolbarPath
* @covers ::check
*/
public function testAllowToolbarPath($expected_result, $path) {
$request = Request::create($path);
$result = $this->policy->check($request);
$this->assertSame($expected_result, $result);
}
/**
* Provides data and expected results for the test method.
*
* @return array
* Data and expected results.
*/
public function providerTestAllowToolbarPath() {
return [
[NULL, '/'],
[NULL, '/other-path?q=/toolbar/subtrees/'],
[NULL, '/toolbar/subtrees/'],
[NULL, '/toolbar/subtrees/some-hash/langcode/additional-stuff'],
[RequestPolicyInterface::ALLOW, '/de/toolbar/subtrees/abcd'],
[RequestPolicyInterface::ALLOW, '/en-us/toolbar/subtrees/xyz'],
[RequestPolicyInterface::ALLOW, '/en-us/toolbar/subtrees/xyz/de'],
[RequestPolicyInterface::ALLOW, '/a/b/c/toolbar/subtrees/xyz/de'],
[RequestPolicyInterface::ALLOW, '/toolbar/subtrees/some-hash'],
[RequestPolicyInterface::ALLOW, '/toolbar/subtrees/some-hash/en'],
];
}
}

View file

@ -0,0 +1,170 @@
<?php
/**
* @file
* Hooks provided by the toolbar module.
*/
use Drupal\Core\Url;
/**
* @addtogroup hooks
* @{
*/
/**
* Add items to the toolbar menu.
*
* The toolbar is a container for administrative and site-global interactive
* components.
*
* The toolbar provides a common styling for items denoted by the
* .toolbar-tab class.
*
* The toolbar provides a construct called a 'tray'. The tray is a container
* for content. The tray may be associated with a toggle in the administration
* bar. The toggle shows or hides the tray and is optimized for small and
* large screens. To create this association, hook_toolbar() returns one or
* more render elements of type 'toolbar_item', containing the toggle and tray
* elements in its 'tab' and 'tray' properties.
*
* The following properties are available:
* - 'tab': A renderable array.
* - 'tray': Optional. A renderable array.
* - '#weight': Optional. Integer weight used for sorting toolbar items in
* administration bar area.
*
* This hook is invoked in toolbar_pre_render().
*
* @return
* An array of toolbar items, keyed by unique identifiers such as 'home' or
* 'administration', or the short name of the module implementing the hook.
* The corresponding value is a render element of type 'toolbar_item'.
*
* @see toolbar_pre_render()
* @ingroup toolbar_tabs
*/
function hook_toolbar() {
$items = array();
// Add a search field to the toolbar. The search field employs no toolbar
// module theming functions.
$items['global_search'] = array(
'#type' => 'toolbar_item',
'tab' => array(
'#type' => 'search',
'#attributes' => array(
'placeholder' => t('Search the site'),
'class' => array('search-global'),
),
),
'#weight' => 200,
// Custom CSS, JS or a library can be associated with the toolbar item.
'#attached' => array(
'library' => array(
'search/global',
),
),
);
// The 'Home' tab is a simple link, which is wrapped in markup associated
// with a visual tab styling.
$items['home'] = array(
'#type' => 'toolbar_item',
'tab' => array(
'#type' => 'link',
'#title' => t('Home'),
'#url' => Url::fromRoute('<front>'),
'#options' => array(
'attributes' => array(
'title' => t('Home page'),
'class' => array('toolbar-icon', 'toolbar-icon-home'),
),
),
),
'#weight' => -20,
);
// A tray may be associated with a tab.
//
// When the tab is activated, the tray will become visible, either in a
// horizontal or vertical orientation on the screen.
//
// The tray should contain a renderable array. An optional #heading property
// can be passed. This text is written to a heading tag in the tray as a
// landmark for accessibility.
$items['commerce'] = array(
'#type' => 'toolbar_item',
'tab' => array(
'#type' => 'link',
'#title' => t('Shopping cart'),
'#url' => Url::fromRoute('cart'),
'#options' => array(
'attributes' => array(
'title' => t('Shopping cart'),
),
),
),
'tray' => array(
'#heading' => t('Shopping cart actions'),
'shopping_cart' => array(
'#theme' => 'item_list',
'#items' => array( /* An item list renderable array */ ),
),
),
'#weight' => 150,
);
// The tray can be used to render arbitrary content.
//
// A renderable array passed to the 'tray' property will be rendered outside
// the administration bar but within the containing toolbar element.
//
// If the default behavior and styling of a toolbar tray is not desired, one
// can render content to the toolbar element and apply custom theming and
// behaviors.
$items['user_messages'] = array(
// Include the toolbar_tab_wrapper to style the link like a toolbar tab.
// Exclude the theme wrapper if custom styling is desired.
'#type' => 'toolbar_item',
'tab' => array(
'#type' => 'link',
'#theme' => 'user_message_toolbar_tab',
'#theme_wrappers' => array(),
'#title' => t('Messages'),
'#url' => Url::fromRoute('user.message'),
'#options' => array(
'attributes' => array(
'title' => t('Messages'),
),
),
),
'tray' => array(
'#heading' => t('User messages'),
'messages' => array(/* renderable content */),
),
'#weight' => 125,
);
return $items;
}
/**
* Alter the toolbar menu after hook_toolbar() is invoked.
*
* This hook is invoked by toolbar_view() immediately after hook_toolbar(). The
* toolbar definitions are passed in by reference. Each element of the $items
* array is one item returned by a module from hook_toolbar(). Additional items
* may be added, or existing items altered.
*
* @param $items
* Associative array of toolbar menu definitions returned from hook_toolbar().
*/
function hook_toolbar_alter(&$items) {
// Move the User tab to the right.
$items['commerce']['#weight'] = 5;
}
/**
* @} End of "addtogroup hooks".
*/

View file

@ -0,0 +1,18 @@
toolbar.narrow:
label: narrow
mediaQuery: 'only screen and (min-width: 16.5em)'
weight: 0
multipliers:
- 1x
toolbar.standard:
label: standard
mediaQuery: 'only screen and (min-width: 38.125em)'
weight: 1
multipliers:
- 1x
toolbar.wide:
label: wide
mediaQuery: 'only screen and (min-width: 61em)'
weight: 2
multipliers:
- 1x

View file

@ -0,0 +1,8 @@
name: Toolbar
type: module
description: 'Provides a toolbar that shows the top-level administration menu items and links from other modules.'
core: 8.x
package: Core
version: VERSION
dependencies:
- breakpoint

View file

@ -0,0 +1,54 @@
toolbar:
version: VERSION
js:
# Core.
js/toolbar.js: {}
# Models.
js/models/MenuModel.js: {}
js/models/ToolbarModel.js: {}
# Views.
js/views/BodyVisualView.js: {}
js/views/MenuVisualView.js: {}
js/views/ToolbarAuralView.js: {}
js/views/ToolbarVisualView.js: {}
css:
component:
css/toolbar.module.css: {}
theme:
css/toolbar.theme.css: {}
css/toolbar.icons.theme.css: {}
dependencies:
- core/modernizr
- core/jquery
- core/drupal
- core/drupalSettings
- core/drupal.ajax
- core/drupal.announce
- core/backbone
- core/matchmedia
- core/matchmedia.addListener
- core/jquery.once
- core/drupal.displace
- toolbar/toolbar.menu
toolbar.menu:
version: VERSION
js:
js/toolbar.menu.js: {}
css:
state:
css/toolbar.menu.css: {}
dependencies:
- core/jquery
- core/drupal
- core/jquery.once
toolbar.escapeAdmin:
version: VERSION
js:
js/escapeAdmin.js: {}
dependencies:
- core/jquery
- core/drupal
- core/drupalSettings
- core/jquery.once

View file

@ -0,0 +1,358 @@
<?php
/**
* @file
* Administration toolbar for quick access to top level administration items.
*/
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Template\Attribute;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Url;
/**
* Implements hook_help().
*/
function toolbar_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.toolbar':
$output = '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Toolbar module provides a toolbar for site administrators, which displays tabs and trays provided by the Toolbar module itself and other modules. For more information, see the <a href=":toolbar_docs">online documentation for the Toolbar module</a>.', array(':toolbar_docs' => 'https://www.drupal.org/documentation/modules/toolbar')) . '</p>';
$output .= '<h4>' . t('Terminology') . '</h4>';
$output .= '<dl>';
$output .= '<dt>' . t('Tabs') . '</dt>';
$output .= '<dd>' . t('Tabs are buttons, displayed in a bar across the top of the screen. Some tabs execute an action (such as starting Edit mode), while other tabs toggle which tray is open.') . '</dd>';
$output .= '<dt>' . t('Trays') . '</dt>';
$output .= '<dd>' . t('Trays are usually lists of links, which can be hierarchical like a menu. If a tray has been toggled open, it is displayed either vertically or horizontally below the tab bar, depending on the browser width. Only one tray may be open at a time. If you click another tab, that tray will replace the tray being displayed. In wide browser widths, the user has the ability to toggle from vertical to horizontal, using a link at the bottom or right of the tray. Hierarchical menus only have open/close behavior in vertical mode; if you display a tray containing a hierarchical menu horizontally, only the top-level links will be available.') . '</dd>';
$output .= '</dl>';
return $output;
}
}
/**
* Implements hook_theme().
*/
function toolbar_theme($existing, $type, $theme, $path) {
$items['toolbar'] = array(
'render element' => 'element',
);
$items['menu__toolbar'] = array(
'base hook' => 'menu',
'variables' => array('items' => array(), 'attributes' => array()),
);
return $items;
}
/**
* Implements hook_page_top().
*
* Add admin toolbar to the top of the page automatically.
*/
function toolbar_page_top(array &$page_top) {
$page_top['toolbar'] = array(
'#type' => 'toolbar',
'#access' => \Drupal::currentUser()->hasPermission('access toolbar'),
'#cache' => [
'keys' => ['toolbar'],
'contexts' => ['user.permissions'],
],
);
}
/**
* Prepares variables for administration toolbar templates.
*
* Default template: toolbar.html.twig.
*
* @param array $variables
* An associative array containing:
* - element: An associative array containing the properties and children of
* the tray. Properties used: #children, #attributes and #bar.
*/
function template_preprocess_toolbar(&$variables) {
$element = $variables['element'];
// Prepare the toolbar attributes.
$variables['attributes'] = $element['#attributes'];
$variables['toolbar_attributes'] = new Attribute($element['#bar']['#attributes']);
$variables['toolbar_heading'] = $element['#bar']['#heading'];
// Prepare the trays and tabs for each toolbar item as well as the remainder
// variable that will hold any non-tray, non-tab elements.
$variables['trays'] = array();
$variables['tabs'] = array();
$variables['remainder'] = array();
foreach (Element::children($element) as $key) {
// Early rendering to collect the wrapper attributes from
// ToolbarItem elements.
if (!empty($element[$key])) {
Drupal::service('renderer')->render($element[$key]);
}
// Add the tray.
if (isset($element[$key]['tray'])) {
$attributes = array();
if (!empty($element[$key]['tray']['#wrapper_attributes'])) {
$attributes = $element[$key]['tray']['#wrapper_attributes'];
}
$variables['trays'][$key] = array(
'links' => $element[$key]['tray'],
'attributes' => new Attribute($attributes),
);
if (array_key_exists('#heading', $element[$key]['tray'])) {
$variables['trays'][$key]['label'] = $element[$key]['tray']['#heading'];
}
}
// Add the tab.
if (isset($element[$key]['tab'])) {
$attributes = array();
// Pass the wrapper attributes along.
if (!empty($element[$key]['#wrapper_attributes'])) {
$attributes = $element[$key]['#wrapper_attributes'];
}
$variables['tabs'][$key] = array(
'link' => $element[$key]['tab'],
'attributes' => new Attribute($attributes),
);
}
// Add other non-tray, non-tab child elements to the remainder variable for
// later rendering.
foreach (Element::children($element[$key]) as $child_key) {
if (!in_array($child_key, array('tray', 'tab'))) {
$variables['remainder'][$key][$child_key] = $element[$key][$child_key];
}
}
}
}
/**
* Implements hook_toolbar().
*/
function toolbar_toolbar() {
// The 'Home' tab is a simple link, with no corresponding tray.
$items['home'] = array(
'#type' => 'toolbar_item',
'tab' => array(
'#type' => 'link',
'#title' => t('Back to site'),
'#url' => Url::fromRoute('<front>'),
'#attributes' => array(
'title' => t('Return to site content'),
'class' => array('toolbar-icon', 'toolbar-icon-escape-admin'),
'data-toolbar-escape-admin' => TRUE,
),
),
'#wrapper_attributes' => array(
'class' => array('hidden', 'home-toolbar-tab'),
),
'#attached' => array(
'library' => array(
'toolbar/toolbar.escapeAdmin',
),
),
'#weight' => -20,
);
// To conserve bandwidth, we only include the top-level links in the HTML.
// The subtrees are fetched through a JSONP script that is generated at the
// toolbar_subtrees route. We provide the JavaScript requesting that JSONP
// script here with the hash parameter that is needed for that route.
// @see toolbar_subtrees_jsonp()
list($hash, $hash_cacheability) = _toolbar_get_subtrees_hash();
$subtrees_attached['drupalSettings']['toolbar'] = [
'subtreesHash' => $hash,
];
// The administration element has a link that is themed to correspond to
// a toolbar tray. The tray contains the full administrative menu of the site.
$items['administration'] = array(
'#type' => 'toolbar_item',
'tab' => array(
'#type' => 'link',
'#title' => t('Manage'),
'#url' => Url::fromRoute('system.admin'),
'#attributes' => array(
'title' => t('Admin menu'),
'class' => array('toolbar-icon', 'toolbar-icon-menu'),
// A data attribute that indicates to the client to defer loading of
// the admin menu subtrees until this tab is activated. Admin menu
// subtrees will not render to the DOM if this attribute is removed.
// The value of the attribute is intentionally left blank. Only the
// presence of the attribute is necessary.
'data-drupal-subtrees' => '',
),
),
'tray' => array(
'#heading' => t('Administration menu'),
'#attached' => $subtrees_attached,
'toolbar_administration' => array(
'#pre_render' => array(
'toolbar_prerender_toolbar_administration_tray',
),
'#type' => 'container',
'#attributes' => array(
'class' => array('toolbar-menu-administration'),
),
),
),
'#weight' => -15,
);
$hash_cacheability->applyTo($items['administration']);
return $items;
}
/**
* Renders the toolbar's administration tray.
*
* @param array $element
* A renderable array.
*
* @return array
* The updated renderable array.
*
* @see drupal_render()
*/
function toolbar_prerender_toolbar_administration_tray(array $element) {
$menu_tree = \Drupal::service('toolbar.menu_tree');
// Load the administrative menu. The first level is the "Administration" link.
// In order to load the children of that link, start and end on the second
// level.
$parameters = new MenuTreeParameters();
$parameters->setMinDepth(2)->setMaxDepth(2)->onlyEnabledLinks();
// @todo Make the menu configurable in https://www.drupal.org/node/1869638.
$tree = $menu_tree->load('admin', $parameters);
$manipulators = array(
array('callable' => 'menu.default_tree_manipulators:checkAccess'),
array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'),
array('callable' => 'toolbar_menu_navigation_links'),
);
$tree = $menu_tree->transform($tree, $manipulators);
$element['administration_menu'] = $menu_tree->build($tree);
return $element;
}
/**
* Adds toolbar-specific attributes to the menu link tree.
*
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
* The menu link tree to manipulate.
*
* @return \Drupal\Core\Menu\MenuLinkTreeElement[]
* The manipulated menu link tree.
*/
function toolbar_menu_navigation_links(array $tree) {
foreach ($tree as $element) {
if ($element->subtree) {
toolbar_menu_navigation_links($element->subtree);
}
// Make sure we have a path specific ID in place, so we can attach icons
// and behaviors to the menu links.
$link = $element->link;
$url = $link->getUrlObject();
if (!$url->isRouted()) {
// This is an unusual case, so just get a distinct, safe string.
$id = substr(Crypt::hashBase64($url->getUri()), 0, 16);
}
else {
$id = str_replace(array('.', '<', '>'), array('-', '', ''), $url->getRouteName());
}
// Get the non-localized title to make the icon class.
$definition = $link->getPluginDefinition();
$element->options['attributes']['id'] = 'toolbar-link-' . $id;
$element->options['attributes']['class'][] = 'toolbar-icon';
$element->options['attributes']['class'][] = 'toolbar-icon-' . strtolower(str_replace(array('.', ' ', '_'), array('-', '-', '-'), $definition['id']));
$element->options['attributes']['title'] = $link->getDescription();
}
return $tree;
}
/**
* Returns the rendered subtree of each top-level toolbar link.
*
* @return array
* An array with the following key-value pairs:
* - 'subtrees': the rendered subtrees
* - 'cacheability: the associated cacheability.
*/
function toolbar_get_rendered_subtrees() {
$data = [
'#pre_render' => ['_toolbar_do_get_rendered_subtrees'],
'#cache' => [
'keys' => [
'toolbar_rendered_subtrees',
],
],
'#cache_properties' => ['#subtrees'],
];
\Drupal::service('renderer')->renderPlain($data);
return [$data['#subtrees'], CacheableMetadata::createFromRenderArray($data)];
}
/**
* #pre_render callback for toolbar_get_rendered_subtrees().
*/
function _toolbar_do_get_rendered_subtrees(array $data) {
$menu_tree = \Drupal::service('toolbar.menu_tree');
// Load the administration menu. The first level is the "Administration" link.
// In order to load the children of that link and the subsequent two levels,
// start at the second level and end at the fourth.
$parameters = new MenuTreeParameters();
$parameters->setMinDepth(2)->setMaxDepth(4)->onlyEnabledLinks();
// @todo Make the menu configurable in https://www.drupal.org/node/1869638.
$tree = $menu_tree->load('admin', $parameters);
$manipulators = array(
array('callable' => 'menu.default_tree_manipulators:checkAccess'),
array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'),
array('callable' => 'toolbar_menu_navigation_links'),
);
$tree = $menu_tree->transform($tree, $manipulators);
$subtrees = array();
// Calculated the combined cacheability of all subtrees.
$cacheability = new CacheableMetadata();
foreach ($tree as $element) {
/** @var \Drupal\Core\Menu\MenuLinkInterface $link */
$link = $element->link;
if ($element->subtree) {
$subtree = $menu_tree->build($element->subtree);
$output = \Drupal::service('renderer')->renderPlain($subtree);
$cacheability = $cacheability->merge(CacheableMetadata::createFromRenderArray($subtree));
}
else {
$output = '';
}
// Many routes have dots as route name, while some special ones like <front>
// have <> characters in them.
$url = $link->getUrlObject();
$id = str_replace(array('.', '<', '>'), array('-', '', '' ), $url->isRouted() ? $url->getRouteName() : $url->getUri());
$subtrees[$id] = $output;
}
// Store the subtrees, along with the cacheability metadata.
$cacheability->applyTo($data);
$data['#subtrees'] = $subtrees;
return $data;
}
/**
* Returns the hash of the per-user rendered toolbar subtrees.
*
* @return string
* The hash of the admin_menu subtrees.
*/
function _toolbar_get_subtrees_hash() {
list($subtrees, $cacheability) = toolbar_get_rendered_subtrees();
$hash = Crypt::hashBase64(serialize($subtrees));
return [$hash, $cacheability];
}

View file

@ -0,0 +1,2 @@
access toolbar:
title: 'Use the administration toolbar'

View file

@ -0,0 +1,6 @@
toolbar.subtrees:
path: '/toolbar/subtrees/{hash}'
defaults:
_controller: '\Drupal\toolbar\Controller\ToolbarController::subtreesAjax'
requirements:
_custom_access: '\Drupal\toolbar\Controller\ToolbarController::checkSubTreeAccess'

View file

@ -0,0 +1,14 @@
services:
cache.toolbar:
class: Drupal\Core\Cache\CacheBackendInterface
tags:
- { name: cache.bin }
factory: cache_factory:get
arguments: [toolbar]
toolbar.page_cache_request_policy.allow_toolbar_path:
class: Drupal\toolbar\PageCache\AllowToolbarPath
tags:
- { name: page_cache_request_policy }
toolbar.menu_tree:
class: Drupal\toolbar\Menu\ToolbarMenuLinkTree
arguments: ['@menu.tree_storage', '@plugin.manager.menu.link', '@router.route_provider', '@menu.active_trail', '@controller_resolver', '@cache.menu', '@current_route_match']