first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,368 @@
/*
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/3084859
* @preserve
*/
/**
* @file
* Styling for toolbar module icons.
*/
.toolbar .toolbar-icon {
position: relative;
padding-left: 2.75em; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-icon {
padding-right: 2.75em;
padding-left: 1.3333em;
}
.toolbar .toolbar-icon::before {
position: absolute;
top: 0;
left: 0.6667em; /* LTR */
display: block;
width: 20px;
height: 100%;
content: "";
background-color: transparent;
background-repeat: no-repeat;
background-attachment: scroll;
background-position: center center;
background-size: 100% auto;
}
[dir="rtl"] .toolbar .toolbar-icon::before {
right: 0.6667em;
left: auto;
}
.toolbar button.toolbar-icon {
border: 0;
background-color: transparent;
font-size: 1em;
}
.toolbar .toolbar-menu ul .toolbar-icon {
padding-left: 1.3333em; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-menu ul .toolbar-icon {
padding-right: 1.3333em;
padding-left: 0;
}
.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-right: 2.75em;
padding-left: 0;
}
.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-right: 3.75em;
padding-left: 0;
}
.toolbar .toolbar-tray-vertical .toolbar-menu a {
padding-right: 4em; /* LTR */
padding-left: 2.75em; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-tray-vertical .toolbar-menu a {
padding-right: 2.75em;
padding-left: 4em;
}
.toolbar .toolbar-bar .toolbar-tab > .toolbar-icon.is-active::before {
filter: invert(100%);
}
/**
* Top level icons.
*/
.toolbar-bar .toolbar-icon-menu::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cg%3e%3cpath fill='%23bebebe' d='M14.752 6h-13.502c-.69 0-1.25.56-1.25 1.25v.5c0 .689.56 1.25 1.25 1.25h13.502c.689 0 1.25-.561 1.25-1.25v-.5c0-.69-.561-1.25-1.25-1.25zM14.752 0h-13.502c-.69 0-1.25.56-1.25 1.25v.5c0 .69.56 1.25 1.25 1.25h13.502c.689 0 1.25-.56 1.25-1.25v-.5c0-.69-.561-1.25-1.25-1.25zM14.752 12h-13.502c-.69 0-1.25.561-1.25 1.25v.5c0 .689.56 1.25 1.25 1.25h13.502c.689 0 1.25-.561 1.25-1.25v-.5c0-.689-.561-1.25-1.25-1.25z'/%3e%3c/g%3e%3c/svg%3e");
}
.toolbar-bar .toolbar-icon-menu:active::before,
.toolbar-bar .toolbar-icon-menu.is-active::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cg%3e%3cpath fill='%23ffffff' d='M14.752 6h-13.502c-.69 0-1.25.56-1.25 1.25v.5c0 .689.56 1.25 1.25 1.25h13.502c.689 0 1.25-.561 1.25-1.25v-.5c0-.69-.561-1.25-1.25-1.25zM14.752 0h-13.502c-.69 0-1.25.56-1.25 1.25v.5c0 .69.56 1.25 1.25 1.25h13.502c.689 0 1.25-.56 1.25-1.25v-.5c0-.69-.561-1.25-1.25-1.25zM14.752 12h-13.502c-.69 0-1.25.561-1.25 1.25v.5c0 .689.56 1.25 1.25 1.25h13.502c.689 0 1.25-.561 1.25-1.25v-.5c0-.689-.561-1.25-1.25-1.25z'/%3e%3c/g%3e%3c/svg%3e");
}
.toolbar-bar .toolbar-icon-help::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cpath fill='%23bebebe' d='M8.002 1c-3.868 0-7.002 3.134-7.002 7s3.134 7 7.002 7c3.865 0 7-3.134 7-7s-3.135-7-7-7zm3 5c0 .551-.16 1.085-.477 1.586l-.158.22c-.07.093-.189.241-.361.393-.168.148-.35.299-.545.447l-.203.189-.141.129-.096.17-.021.235v.63h-2.001v-.704c.026-.396.078-.73.204-.999.125-.269.271-.498.439-.688l.225-.21-.01-.015.176-.14.137-.128c.186-.139.357-.277.516-.417l.148-.18c.098-.152.168-.323.168-.518 0-.552-.447-1-1-1s-1.002.448-1.002 1h-2c0-1.657 1.343-3 3.002-3 1.656 0 3 1.343 3 3zm-1.75 6.619c0 .344-.281.625-.625.625h-1.25c-.345 0-.626-.281-.626-.625v-1.238c0-.344.281-.625.626-.625h1.25c.344 0 .625.281.625.625v1.238z'/%3e%3c/svg%3e");
}
.toolbar-bar .toolbar-icon-help:active::before,
.toolbar-bar .toolbar-icon-help.is-active::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cpath fill='%23ffffff' d='M8.002 1c-3.868 0-7.002 3.134-7.002 7s3.134 7 7.002 7c3.865 0 7-3.134 7-7s-3.135-7-7-7zm3 5c0 .551-.16 1.085-.477 1.586l-.158.22c-.07.093-.189.241-.361.393-.168.148-.35.299-.545.447l-.203.189-.141.129-.096.17-.021.235v.63h-2.001v-.704c.026-.396.078-.73.204-.999.125-.269.271-.498.439-.688l.225-.21-.01-.015.176-.14.137-.128c.186-.139.357-.277.516-.417l.148-.18c.098-.152.168-.323.168-.518 0-.552-.447-1-1-1s-1.002.448-1.002 1h-2c0-1.657 1.343-3 3.002-3 1.656 0 3 1.343 3 3zm-1.75 6.619c0 .344-.281.625-.625.625h-1.25c-.345 0-.626-.281-.626-.625v-1.238c0-.344.281-.625.626-.625h1.25c.344 0 .625.281.625.625v1.238z'/%3e%3c/svg%3e");
}
/**
* Main menu icons.
*/
.toolbar-icon-system-admin-content::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cg%3e%3cpath fill='%23787878' d='M12.502 7h-5c-.276 0-.502-.225-.502-.5v-5c0-.275-.225-.5-.5-.5h-3c-.275 0-.5.225-.5.5v12.029c0 .275.225.5.5.5h9.002c.275 0 .5-.225.5-.5v-6.029c0-.275-.225-.5-.5-.5zM8.502 6h4c.275 0 .34-.159.146-.354l-4.293-4.292c-.195-.195-.353-.129-.353.146v4c0 .275.225.5.5.5z'/%3e%3c/g%3e%3c/svg%3e");
}
.toolbar-icon-system-admin-content:active::before,
.toolbar-icon-system-admin-content.is-active::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cg%3e%3cpath fill='%23000000' d='M12.502 7h-5c-.276 0-.502-.225-.502-.5v-5c0-.275-.225-.5-.5-.5h-3c-.275 0-.5.225-.5.5v12.029c0 .275.225.5.5.5h9.002c.275 0 .5-.225.5-.5v-6.029c0-.275-.225-.5-.5-.5zM8.502 6h4c.275 0 .34-.159.146-.354l-4.293-4.292c-.195-.195-.353-.129-.353.146v4c0 .275.225.5.5.5z'/%3e%3c/g%3e%3c/svg%3e");
}
.toolbar-icon-system-admin-structure::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16px' height='16px'%3e%3cpath fill='%23787878' d='M15.002,11.277c0-0.721,0-1.471,0-2.014c0-1.456-0.824-2.25-2.25-2.25c-1.428,0-3.5,0-3.5,0c-0.139,0-0.25-0.112-0.25-0.25v-2.04c0.596-0.346,1-0.984,1-1.723c0-1.104-0.895-2-2-2C6.896,1,6,1.896,6,3c0,0.738,0.405,1.376,1,1.722v2.042c0,0.138-0.112,0.25-0.25,0.25c0,0-2.138,0-3.5,0S1,7.932,1,9.266c0,0.521,0,1.277,0,2.012c-0.595,0.353-1,0.984-1,1.729c0,1.104,0.896,2,2,2s2-0.896,2-2c0-0.732-0.405-1.377-1-1.729V9.266c0-0.139,0.112-0.25,0.25-0.25h3.536C6.904,9.034,7,9.126,7,9.25v2.027C6.405,11.624,6,12.26,6,13c0,1.104,0.896,2,2.002,2c1.105,0,2-0.896,2-2c0-0.738-0.404-1.376-1-1.723V9.25c0-0.124,0.098-0.216,0.215-0.234h3.535c0.137,0,0.25,0.111,0.25,0.25v2.012c-0.596,0.353-1,0.984-1,1.729c0,1.104,0.896,2,2,2s2-0.896,2-2C16.002,12.262,15.598,11.623,15.002,11.277z'/%3e%3c/svg%3e");
}
.toolbar-icon-system-admin-structure:active::before,
.toolbar-icon-system-admin-structure.is-active::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16px' height='16px'%3e%3cpath d='M15.002,11.277c0-0.721,0-1.471,0-2.014c0-1.456-0.824-2.25-2.25-2.25c-1.428,0-3.5,0-3.5,0c-0.139,0-0.25-0.112-0.25-0.25v-2.04c0.596-0.346,1-0.984,1-1.723c0-1.104-0.895-2-2-2C6.896,1,6,1.896,6,3c0,0.738,0.405,1.376,1,1.722v2.042c0,0.138-0.112,0.25-0.25,0.25c0,0-2.138,0-3.5,0S1,7.932,1,9.266c0,0.521,0,1.277,0,2.012c-0.595,0.353-1,0.984-1,1.729c0,1.104,0.896,2,2,2s2-0.896,2-2c0-0.732-0.405-1.377-1-1.729V9.266c0-0.139,0.112-0.25,0.25-0.25h3.536C6.904,9.034,7,9.126,7,9.25v2.027C6.405,11.624,6,12.26,6,13c0,1.104,0.896,2,2.002,2c1.105,0,2-0.896,2-2c0-0.738-0.404-1.376-1-1.723V9.25c0-0.124,0.098-0.216,0.215-0.234h3.535c0.137,0,0.25,0.111,0.25,0.25v2.012c-0.596,0.353-1,0.984-1,1.729c0,1.104,0.896,2,2,2s2-0.896,2-2C16.002,12.262,15.598,11.623,15.002,11.277z'/%3e%3c/svg%3e");
}
.toolbar-icon-system-themes-page::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cpath fill='%23787878' d='M8.184 7.928l-1.905 1.983-3.538-2.436 4.714-4.713 2.623 3.183-1.894 1.983zm-1.746-7.523c-.236-.416-.803-.649-1.346.083-.259.349-4.727 4.764-4.91 4.983-.182.218-.294.721.044.976.34.258 5.611 3.933 5.611 3.933l-.225.229c.7.729.816.854 1.046.863.75.016 2.035-1.457 2.578-.854.541.604 3.537 3.979 3.537 3.979.51.531 1.305.559 1.815.041.521-.521.541-1.311.025-1.848 0 0-2.742-2.635-3.904-3.619-.578-.479.869-2.051.854-2.839-.008-.238-.125-.361-.823-1.095l-.22.243c0 .003-3.846-4.659-4.082-5.075z'/%3e%3c/svg%3e");
}
.toolbar-icon-system-themes-page:active::before,
.toolbar-icon-system-themes-page.is-active::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cpath fill='%23000000' d='M8.184 7.928l-1.905 1.983-3.538-2.436 4.714-4.713 2.623 3.183-1.894 1.983zm-1.746-7.523c-.236-.416-.803-.649-1.346.083-.259.349-4.727 4.764-4.91 4.983-.182.218-.294.721.044.976.34.258 5.611 3.933 5.611 3.933l-.225.229c.7.729.816.854 1.046.863.75.016 2.035-1.457 2.578-.854.541.604 3.537 3.979 3.537 3.979.51.531 1.305.559 1.815.041.521-.521.541-1.311.025-1.848 0 0-2.742-2.635-3.904-3.619-.578-.479.869-2.051.854-2.839-.008-.238-.125-.361-.823-1.095l-.22.243c0 .003-3.846-4.659-4.082-5.075z'/%3e%3c/svg%3e");
}
.toolbar-icon-entity-user-collection::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cg%3e%3cpath fill='%23787878' d='M6.722 11.291l.451-.17-.165-.32c-.536-1.039-.685-1.934-.761-2.672-.082-.808-.144-2.831 1.053-4.189.244-.278.493-.493.743-.675.012-.826-.135-1.766-.646-2.345-.618-.7-1.4-.787-1.4-.787l-.497-.055-.498.055s-.783.087-1.398.787c-.617.702-.717 1.948-.625 2.855.06.583.17 1.263.574 2.05.274.533.341.617.355 1.01.022.595.027 1.153-.671 1.538-.697.383-1.564.508-2.403 1.088-.596.41-.709 1.033-.78 1.459l-.051.41c-.029.273.173.498.448.498h5.012c.457-.24.89-.402 1.259-.537zM5.064 15.096c.07-.427.184-1.05.78-1.46.838-.581 1.708-.706 2.404-1.089.699-.385.693-.943.672-1.537-.014-.393-.08-.477-.354-1.01-.406-.787-.515-1.467-.576-2.049-.093-.909.008-2.155.625-2.856.615-.7 1.398-.787 1.398-.787l.492-.055h.002l.496.055s.781.087 1.396.787c.615.701.72 1.947.623 2.855-.062.583-.172 1.262-.571 2.049-.271.533-.341.617-.354 1.01-.021.595-.062 1.22.637 1.604.697.385 1.604.527 2.438 1.104.923.641.822 1.783.822 1.783-.022.275-.269.5-.542.5h-9.991c-.275 0-.477-.223-.448-.496l.051-.408z'/%3e%3c/g%3e%3c/svg%3e");
}
.toolbar-icon-entity-user-collection:active::before,
.toolbar-icon-entity-user-collection.is-active::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cg%3e%3cpath fill='%23000000' d='M6.722 11.291l.451-.17-.165-.32c-.536-1.039-.685-1.934-.761-2.672-.082-.808-.144-2.831 1.053-4.189.244-.278.493-.493.743-.675.012-.826-.135-1.766-.646-2.345-.618-.7-1.4-.787-1.4-.787l-.497-.055-.498.055s-.783.087-1.398.787c-.617.702-.717 1.948-.625 2.855.06.583.17 1.263.574 2.05.274.533.341.617.355 1.01.022.595.027 1.153-.671 1.538-.697.383-1.564.508-2.403 1.088-.596.41-.709 1.033-.78 1.459l-.051.41c-.029.273.173.498.448.498h5.012c.457-.24.89-.402 1.259-.537zM5.064 15.096c.07-.427.184-1.05.78-1.46.838-.581 1.708-.706 2.404-1.089.699-.385.693-.943.672-1.537-.014-.393-.08-.477-.354-1.01-.406-.787-.515-1.467-.576-2.049-.093-.909.008-2.155.625-2.856.615-.7 1.398-.787 1.398-.787l.492-.055h.002l.496.055s.781.087 1.396.787c.615.701.72 1.947.623 2.855-.062.583-.172 1.262-.571 2.049-.271.533-.341.617-.354 1.01-.021.595-.062 1.22.637 1.604.697.385 1.604.527 2.438 1.104.923.641.822 1.783.822 1.783-.022.275-.269.5-.542.5h-9.991c-.275 0-.477-.223-.448-.496l.051-.408z'/%3e%3c/g%3e%3c/svg%3e");
}
.toolbar-icon-system-modules-list::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cpath fill='%23787878' d='M11.002 11v2.529c0 .275-.225.471-.5.471h-3c-.827 0-1.112-.754-.604-1.316l.81-.725c.509-.562.513-1.461-.097-2.01-.383-.344-1.027-.728-2.111-.728s-1.727.386-2.109.731c-.609.549-.606 1.45-.097 2.015l.808.717c.509.562.223 1.316-.602 1.316h-3c-.276 0-.5-.193-.5-.471v-9.029c0-.276.224-.5.5-.5h3c.825 0 1.111-.768.602-1.332l-.808-.73c-.51-.563-.513-1.465.097-2.014.382-.344 1.025-.731 2.109-.731s1.728.387 2.111.731c.608.548.606 1.45.097 2.014l-.81.73c-.509.564-.223 1.332.603 1.332h3c.274 0 .5.224.5.5v2.5c0 .825.642 1.111 1.233.602l.771-.808c.599-.51 1.547-.513 2.127.097.364.383.772 1.025.772 2.109s-.408 1.727-.771 2.109c-.58.604-1.529.604-2.127.097l-.77-.808c-.593-.509-1.234-.223-1.234.602z'/%3e%3c/svg%3e");
}
.toolbar-icon-system-modules-list:active::before,
.toolbar-icon-system-modules-list.is-active::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cpath fill='%23000000' d='M11.002 11v2.529c0 .275-.225.471-.5.471h-3c-.827 0-1.112-.754-.604-1.316l.81-.725c.509-.562.513-1.461-.097-2.01-.383-.344-1.027-.728-2.111-.728s-1.727.386-2.109.731c-.609.549-.606 1.45-.097 2.015l.808.717c.509.562.223 1.316-.602 1.316h-3c-.276 0-.5-.193-.5-.471v-9.029c0-.276.224-.5.5-.5h3c.825 0 1.111-.768.602-1.332l-.808-.73c-.51-.563-.513-1.465.097-2.014.382-.344 1.025-.731 2.109-.731s1.728.387 2.111.731c.608.548.606 1.45.097 2.014l-.81.73c-.509.564-.223 1.332.603 1.332h3c.274 0 .5.224.5.5v2.5c0 .825.642 1.111 1.233.602l.771-.808c.599-.51 1.547-.513 2.127.097.364.383.772 1.025.772 2.109s-.408 1.727-.771 2.109c-.58.604-1.529.604-2.127.097l-.77-.808c-.593-.509-1.234-.223-1.234.602z'/%3e%3c/svg%3e");
}
.toolbar-icon-system-admin-config::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cpath fill='%23787878' d='M14.416 11.586l-.01-.008v-.001l-5.656-5.656c.15-.449.252-.921.252-1.421 0-2.485-2.016-4.5-4.502-4.5-.505 0-.981.102-1.434.255l2.431 2.431-.588 2.196-2.196.588-2.445-2.445c-.162.464-.268.956-.268 1.475 0 2.486 2.014 4.5 4.5 4.5.5 0 .972-.102 1.421-.251l5.667 5.665c.781.781 2.047.781 2.828 0s.781-2.047 0-2.828z'/%3e%3c/svg%3e");
}
.toolbar-icon-system-admin-config:active::before,
.toolbar-icon-system-admin-config.is-active::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cpath fill='%23000000' d='M14.416 11.586l-.01-.008v-.001l-5.656-5.656c.15-.449.252-.921.252-1.421 0-2.485-2.016-4.5-4.502-4.5-.505 0-.981.102-1.434.255l2.431 2.431-.588 2.196-2.196.588-2.445-2.445c-.162.464-.268.956-.268 1.475 0 2.486 2.014 4.5 4.5 4.5.5 0 .972-.102 1.421-.251l5.667 5.665c.781.781 2.047.781 2.828 0s.781-2.047 0-2.828z'/%3e%3c/svg%3e");
}
.toolbar-icon-system-admin-reports::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cg%3e%3cpath fill='%23787878' d='M4 13.529c0 .275-.225.5-.5.5h-3c-.275 0-.5-.225-.5-.5v-4.25c0-.274.225-.5.5-.5h3c.275 0 .5.226.5.5v4.25zM10.002 13.529c0 .275-.225.5-.5.5h-3.002c-.275 0-.5-.225-.5-.5v-13c0-.275.225-.5.5-.5h3.002c.275 0 .5.225.5.5v13zM16.002 13.529c0 .275-.225.5-.5.5h-3c-.275 0-.5-.225-.5-.5v-9.5c0-.275.225-.5.5-.5h3c.275 0 .5.225.5.5v9.5z'/%3e%3c/g%3e%3c/svg%3e");
}
.toolbar-icon-system-admin-reports:active::before,
.toolbar-icon-system-admin-reports.is-active::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cg%3e%3cpath fill='%23000000' d='M4 13.529c0 .275-.225.5-.5.5h-3c-.275 0-.5-.225-.5-.5v-4.25c0-.274.225-.5.5-.5h3c.275 0 .5.226.5.5v4.25zM10.002 13.529c0 .275-.225.5-.5.5h-3.002c-.275 0-.5-.225-.5-.5v-13c0-.275.225-.5.5-.5h3.002c.275 0 .5.225.5.5v13zM16.002 13.529c0 .275-.225.5-.5.5h-3c-.275 0-.5-.225-.5-.5v-9.5c0-.275.225-.5.5-.5h3c.275 0 .5.225.5.5v9.5z'/%3e%3c/g%3e%3c/svg%3e");
}
.toolbar-icon-help-main::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cpath fill='%23787878' d='M8.002 1c-3.868 0-7.002 3.134-7.002 7s3.134 7 7.002 7c3.865 0 7-3.134 7-7s-3.135-7-7-7zm3 5c0 .551-.16 1.085-.477 1.586l-.158.22c-.07.093-.189.241-.361.393-.168.148-.35.299-.545.447l-.203.189-.141.129-.096.17-.021.235v.63h-2.001v-.704c.026-.396.078-.73.204-.999.125-.269.271-.498.439-.688l.225-.21-.01-.015.176-.14.137-.128c.186-.139.357-.277.516-.417l.148-.18c.098-.152.168-.323.168-.518 0-.552-.447-1-1-1s-1.002.448-1.002 1h-2c0-1.657 1.343-3 3.002-3 1.656 0 3 1.343 3 3zm-1.75 6.619c0 .344-.281.625-.625.625h-1.25c-.345 0-.626-.281-.626-.625v-1.238c0-.344.281-.625.626-.625h1.25c.344 0 .625.281.625.625v1.238z'/%3e%3c/svg%3e");
}
.toolbar-icon-help-main:active::before,
.toolbar-icon-help-main.is-active::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cpath fill='%23000000' d='M8.002 1c-3.868 0-7.002 3.134-7.002 7s3.134 7 7.002 7c3.865 0 7-3.134 7-7s-3.135-7-7-7zm3 5c0 .551-.16 1.085-.477 1.586l-.158.22c-.07.093-.189.241-.361.393-.168.148-.35.299-.545.447l-.203.189-.141.129-.096.17-.021.235v.63h-2.001v-.704c.026-.396.078-.73.204-.999.125-.269.271-.498.439-.688l.225-.21-.01-.015.176-.14.137-.128c.186-.139.357-.277.516-.417l.148-.18c.098-.152.168-.323.168-.518 0-.552-.447-1-1-1s-1.002.448-1.002 1h-2c0-1.657 1.343-3 3.002-3 1.656 0 3 1.343 3 3zm-1.75 6.619c0 .344-.281.625-.625.625h-1.25c-.345 0-.626-.281-.626-.625v-1.238c0-.344.281-.625.626-.625h1.25c.344 0 .625.281.625.625v1.238z'/%3e%3c/svg%3e");
}
@media only screen and (min-width: 16.5em) {
.toolbar .toolbar-bar .toolbar-tab > .toolbar-icon {
width: 4em;
margin-right: 0;
margin-left: 0;
padding-right: 0;
padding-left: 0;
text-indent: -9999px;
}
.toolbar .toolbar-bar .toolbar-tab > .toolbar-icon::before {
left: 0; /* LTR */
width: 100%;
background-size: 42% auto;
}
.no-svg .toolbar .toolbar-bar .toolbar-tab > .toolbar-icon::before {
background-size: auto auto;
}
[dir="rtl"] .toolbar .toolbar-bar .toolbar-tab > .toolbar-icon::before {
right: 0;
left: auto;
}
}
@media only screen and (min-width: 36em) {
.toolbar .toolbar-bar .toolbar-tab > .toolbar-icon {
width: auto;
padding-right: 1.3333em; /* LTR */
padding-left: 2.75em; /* LTR */
text-indent: 0;
background-position: left center; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-bar .toolbar-tab > .toolbar-icon {
padding-right: 2.75em;
padding-left: 1.3333em;
background-position: right center;
}
.toolbar .toolbar-bar .toolbar-tab > .toolbar-icon::before {
left: 0.6667em; /* LTR */
width: 20px;
background-size: 100% auto;
}
.no-svg .toolbar .toolbar-bar .toolbar-tab > .toolbar-icon::before {
background-size: auto auto;
}
[dir="rtl"] .toolbar .toolbar-bar .toolbar-tab > .toolbar-icon::before {
right: 0.6667em;
left: 0;
}
}
/**
* Accessibility/focus
*/
.toolbar-tab a:focus {
-webkit-text-decoration: underline;
text-decoration: underline;
outline: none;
}
.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 {
-webkit-text-decoration: underline;
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 {
right: 1.6667em;
left: auto;
}
.toolbar .toolbar-icon.toolbar-handle::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cpath fill='%235181C6' d='M8.002 1c-3.869 0-7.002 3.134-7.002 7s3.133 7 7.002 7c3.865 0 7-3.134 7-7s-3.135-7-7-7zm4.459 6.336l-4.105 4.105c-.196.189-.515.189-.708 0l-4.107-4.105c-.194-.194-.194-.513 0-.707l.977-.978c.194-.194.513-.194.707 0l2.422 2.421c.192.195.513.195.708 0l2.422-2.42c.188-.194.512-.194.707 0l.977.977c.193.194.193.513 0 .707z'/%3e%3c/svg%3e");
}
.toolbar .toolbar-icon.toolbar-handle.open::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cpath fill='%23787878' d='M8.002 1c-3.867 0-7.002 3.134-7.002 7s3.135 7 7.002 7 7-3.134 7-7-3.133-7-7-7zm4.462 8.37l-.979.979c-.19.19-.516.19-.707 0l-2.422-2.419c-.196-.194-.515-.194-.708 0l-2.423 2.417c-.194.193-.513.193-.707 0l-.977-.976c-.194-.194-.194-.514 0-.707l4.106-4.106c.193-.194.515-.194.708 0l4.109 4.105c.19.192.19.513 0 .707z'/%3e%3c/svg%3e");
}
.toolbar .toolbar-menu .toolbar-menu .toolbar-icon.toolbar-handle::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cpath fill='%235181C6' d='M2.611 5.393c-.17-.216-.084-.393.191-.393h10.397c.275 0 .361.177.191.393l-5.08 6.464c-.17.216-.452.216-.622 0l-5.077-6.464z'/%3e%3c/svg%3e");
background-size: 75%;
}
.toolbar .toolbar-menu .toolbar-menu .toolbar-icon.toolbar-handle.open::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cpath fill='%23787878' d='M13.391 10.607c.17.216.084.393-.191.393h-10.398c-.275 0-.361-.177-.191-.393l5.08-6.464c.17-.216.45-.216.62 0l5.08 6.464z'/%3e%3c/svg%3e");
background-size: 75%;
}
.toolbar .toolbar-icon-escape-admin::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cpath fill='%23bebebe' d='M8.002 1c-3.868 0-7.002 3.133-7.002 7 0 3.865 3.134 7 7.002 7 3.865 0 7-3.135 7-7 0-3.867-3.135-7-7-7zm2.348 10.482l-.977.977c-.195.193-.514.193-.707 0l-4.108-4.105c-.194-.195-.194-.514 0-.708l4.108-4.105c.193-.194.512-.194.707 0l.979.977c.191.194.191.513 0 .707l-2.422 2.421c-.195.194-.195.515 0 .708l2.419 2.421c.196.19.196.512.001.707z'/%3e%3c/svg%3e");
}
[dir="rtl"] .toolbar .toolbar-icon-escape-admin::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cpath fill='%23bebebe' d='M8.002 1c-3.868 0-7.002 3.135-7.002 7 0 3.867 3.134 7 7.002 7 3.865 0 7-3.133 7-7 0-3.865-3.135-7-7-7zm3.441 7.357l-4.106 4.104c-.194.191-.514.191-.708 0l-.978-.979c-.194-.193-.194-.518 0-.707l2.423-2.421c.195-.195.195-.514 0-.708l-2.422-2.421c-.194-.194-.194-.513 0-.707l.977-.977c.194-.194.514-.194.708 0l4.106 4.108c.191.194.191.515 0 .708z'/%3e%3c/svg%3e");
}
/**
* Orientation toggle.
*/
.toolbar .toolbar-toggle-orientation button {
width: 39px;
height: 39px;
padding: 0;
text-indent: -999em;
}
.toolbar .toolbar-toggle-orientation button::before {
right: 0;
left: 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("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cg%3e%3cpath fill='%23bebebe' d='M2.5 2h-2.491v12.029h2.491c.276 0 .5-.225.5-.5v-11.029c0-.276-.224-.5-.5-.5zM14.502 6.029h-4c-.275 0-.5-.225-.5-.5v-1c0-.275-.16-.341-.354-.146l-3.294 3.292c-.194.194-.194.513 0 .708l3.294 3.293c.188.193.354.129.354-.146v-1c0-.271.227-.5.5-.5h4c.275 0 .5-.225.5-.5v-3c0-.276-.225-.501-.5-.501z'/%3e%3c/g%3e%3c/svg%3e"); /* LTR */
}
.toolbar .toolbar-toggle-orientation [value="vertical"]:hover::before,
.toolbar .toolbar-toggle-orientation [value="vertical"]:focus::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cg%3e%3cpath fill='%23787878' d='M2.5 2h-2.491v12.029h2.491c.276 0 .5-.225.5-.5v-11.029c0-.276-.224-.5-.5-.5zM14.502 6.029h-4c-.275 0-.5-.225-.5-.5v-1c0-.275-.16-.341-.354-.146l-3.294 3.292c-.194.194-.194.513 0 .708l3.294 3.293c.188.193.354.129.354-.146v-1c0-.271.227-.5.5-.5h4c.275 0 .5-.225.5-.5v-3c0-.276-.225-.501-.5-.501z'/%3e%3c/g%3e%3c/svg%3e"); /* LTR */
}
[dir="rtl"] .toolbar .toolbar-toggle-orientation [value="vertical"]::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cg%3e%3cpath fill='%23bebebe' d='M13.51 2c-.275 0-.5.224-.5.5v11.029c0 .275.225.5.5.5h2.492v-12.029h-2.492zM6.362 4.382c-.194-.194-.353-.128-.353.147v1c0 .275-.225.5-.5.5h-4c-.275 0-.5.225-.5.5v3c0 .271.225.5.5.5h4c.275 0 .5.225.5.5v1c0 .271.159.34.354.146l3.295-3.293c.193-.194.193-.513 0-.708l-3.296-3.292z'/%3e%3c/g%3e%3c/svg%3e");
}
[dir="rtl"] .toolbar .toolbar-toggle-orientation [value="vertical"]:hover::before,
[dir="rtl"] .toolbar .toolbar-toggle-orientation [value="vertical"]:focus::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cg%3e%3cpath fill='%23787878' d='M13.51 2c-.275 0-.5.224-.5.5v11.029c0 .275.225.5.5.5h2.492v-12.029h-2.492zM6.362 4.382c-.194-.194-.353-.128-.353.147v1c0 .275-.225.5-.5.5h-4c-.275 0-.5.225-.5.5v3c0 .271.225.5.5.5h4c.275 0 .5.225.5.5v1c0 .271.159.34.354.146l3.295-3.293c.193-.194.193-.513 0-.708l-3.296-3.292z'/%3e%3c/g%3e%3c/svg%3e");
}
.toolbar .toolbar-toggle-orientation [value="horizontal"]::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cg%3e%3cpath fill='%23bebebe' d='M1.986.019v2.491c0 .276.225.5.5.5h11.032c.275 0 .5-.224.5-.5v-2.491h-12.032zM8.342 6.334c-.193-.194-.513-.194-.708 0l-3.294 3.293c-.194.195-.129.353.146.353h1c.275 0 .5.227.5.5v4.02c0 .275.225.5.5.5h3.002c.271 0 .5-.225.5-.5v-4.02c0-.274.225-.5.5-.5h1c.271 0 .34-.158.145-.354l-3.291-3.292z'/%3e%3c/g%3e%3c/svg%3e");
}
.toolbar .toolbar-toggle-orientation [value="horizontal"]:hover::before,
.toolbar .toolbar-toggle-orientation [value="horizontal"]:focus::before {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3e%3cg%3e%3cpath fill='%23787878' d='M1.986.019v2.491c0 .276.225.5.5.5h11.032c.275 0 .5-.224.5-.5v-2.491h-12.032zM8.342 6.334c-.193-.194-.513-.194-.708 0l-3.294 3.293c-.194.195-.129.353.146.353h1c.275 0 .5.227.5.5v4.02c0 .275.225.5.5.5h3.002c.271 0 .5-.225.5-.5v-4.02c0-.274.225-.5.5-.5h1c.271 0 .34-.158.145-.354l-3.291-3.292z'/%3e%3c/g%3e%3c/svg%3e");
}

View File

@@ -0,0 +1,301 @@
/**
* @file
* Styling for toolbar module icons.
*/
.toolbar .toolbar-icon {
position: relative;
padding-left: 2.75em; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-icon {
padding-right: 2.75em;
padding-left: 1.3333em;
}
.toolbar .toolbar-icon::before {
position: absolute;
top: 0;
left: 0.6667em; /* LTR */
display: block;
width: 20px;
height: 100%;
content: "";
background-color: transparent;
background-repeat: no-repeat;
background-attachment: scroll;
background-position: center center;
background-size: 100% auto;
}
[dir="rtl"] .toolbar .toolbar-icon::before {
right: 0.6667em;
left: auto;
}
.toolbar button.toolbar-icon {
border: 0;
background-color: transparent;
font-size: 1em;
}
.toolbar .toolbar-menu ul .toolbar-icon {
padding-left: 1.3333em; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-menu ul .toolbar-icon {
padding-right: 1.3333em;
padding-left: 0;
}
.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-right: 2.75em;
padding-left: 0;
}
.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-right: 3.75em;
padding-left: 0;
}
.toolbar .toolbar-tray-vertical .toolbar-menu a {
padding-right: 4em; /* LTR */
padding-left: 2.75em; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-tray-vertical .toolbar-menu a {
padding-right: 2.75em;
padding-left: 4em;
}
.toolbar .toolbar-bar .toolbar-tab > .toolbar-icon.is-active::before {
filter: invert(100%);
}
/**
* 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 {
width: 4em;
margin-right: 0;
margin-left: 0;
padding-right: 0;
padding-left: 0;
text-indent: -9999px;
}
.toolbar .toolbar-bar .toolbar-tab > .toolbar-icon::before {
left: 0; /* LTR */
width: 100%;
background-size: 42% auto;
}
.no-svg .toolbar .toolbar-bar .toolbar-tab > .toolbar-icon::before {
background-size: auto auto;
}
[dir="rtl"] .toolbar .toolbar-bar .toolbar-tab > .toolbar-icon::before {
right: 0;
left: auto;
}
}
@media only screen and (min-width: 36em) {
.toolbar .toolbar-bar .toolbar-tab > .toolbar-icon {
width: auto;
padding-right: 1.3333em; /* LTR */
padding-left: 2.75em; /* LTR */
text-indent: 0;
background-position: left center; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-bar .toolbar-tab > .toolbar-icon {
padding-right: 2.75em;
padding-left: 1.3333em;
background-position: right center;
}
.toolbar .toolbar-bar .toolbar-tab > .toolbar-icon::before {
left: 0.6667em; /* LTR */
width: 20px;
background-size: 100% auto;
}
.no-svg .toolbar .toolbar-bar .toolbar-tab > .toolbar-icon::before {
background-size: auto auto;
}
[dir="rtl"] .toolbar .toolbar-bar .toolbar-tab > .toolbar-icon::before {
right: 0.6667em;
left: 0;
}
}
/**
* Accessibility/focus
*/
.toolbar-tab a:focus {
text-decoration: underline;
outline: none;
}
.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 {
right: 1.6667em;
left: auto;
}
.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 {
width: 39px;
height: 39px;
padding: 0;
text-indent: -999em;
}
.toolbar .toolbar-toggle-orientation button::before {
right: 0;
left: 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,119 @@
/**
* @file toolbar.menu.css
*/
.toolbar .toolbar-menu,
[dir="rtl"] .toolbar .toolbar-menu {
margin: 0;
padding: 0;
list-style: none;
}
.toolbar .toolbar-box {
position: relative;
display: block;
width: auto;
line-height: 1em; /* this prevents the value "normal" from being returned as the line-height */
}
/**
* Hidden vertical toolbar sub-menus by default.
*/
.toolbar .toolbar-tray-vertical .toolbar-menu ul {
display: none;
}
/**
* Hidden horizontal toolbar handle icon.
*/
.toolbar .toolbar-tray-horizontal .toolbar-menu .toolbar-handle {
display: none;
}
/**
* Hidden toolbar sub-menus by default.
*/
.toolbar-tray-open .toolbar-menu .menu-item--expanded ul {
display: none;
}
.toolbar .toolbar-tray-vertical li.open > ul {
display: block; /* Show the sub-menus */
}
.toolbar .toolbar-tray-vertical .toolbar-handle + a {
margin-right: 3em; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-tray-vertical .toolbar-handle + a {
margin-right: 0;
margin-left: 3em;
}
.toolbar .toolbar-tray .menu-item--active-trail > .toolbar-box a,
.toolbar .toolbar-tray a.is-active {
color: #000;
background-color: #f5f5f5;
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 {
border-top-color: #e5e5e5;
border-bottom-color: #ccc;
background-color: #fafafa;
}
.toolbar .level-3 > ul {
border-top-color: #ddd;
border-bottom-color: #c5c5c5;
background-color: #f5f5f5;
}
.toolbar .level-4 > ul {
border-top-color: #d5d5d5;
border-bottom-color: #bbb;
background-color: #eee;
}
.toolbar .level-5 > ul {
border-top-color: #ccc;
border-bottom-color: #b5b5b5;
background-color: #e5e5e5;
}
.toolbar .level-6 > ul {
border-top-color: #c5c5c5;
border-bottom-color: #aaa;
background-color: #eee;
}
.toolbar .level-7 > ul {
border-top-color: #ccc;
border-bottom-color: #b5b5b5;
background-color: #fafafa;
}
.toolbar .level-8 > ul {
border-top-color: #ddd;
border-bottom-color: #ccc;
background-color: #ddd;
}
/**
* Handle.
*/
.toolbar .toolbar-handle:hover {
cursor: pointer;
}
.toolbar .toolbar-icon.toolbar-handle {
position: absolute;
z-index: 1;
top: 0;
right: 0; /* LTR */
bottom: 0;
display: block;
height: 100%;
padding: 0;
}
[dir="rtl"] .toolbar .toolbar-icon.toolbar-handle {
right: auto;
left: 0;
padding: 0;
}

View File

@@ -0,0 +1,295 @@
/**
* @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 {
margin: 0;
padding: 0;
vertical-align: baseline;
font-size: small;
line-height: 1;
}
@media print {
#toolbar-administration {
display: none;
}
}
.toolbar-loading #toolbar-administration {
overflow: hidden;
}
/**
* 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;
}
.toolbar-horizontal .toolbar-tray {
position: fixed;
left: 0;
width: 100%;
}
/* 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. */
.toolbar-oriented .toolbar-bar {
position: absolute;
top: 0;
right: 0;
left: 0;
}
.toolbar-oriented .toolbar-tray {
position: absolute;
right: 0;
left: 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. */
.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. */
.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-loading.toolbar-horizontal .toolbar .toolbar-tray .toolbar-menu > li,
.toolbar .toolbar-bar .toolbar-tab,
.toolbar .toolbar-tray-horizontal li {
float: left; /* LTR */
}
[dir="rtl"] .toolbar-loading.toolbar-horizontal .toolbar .toolbar-tray .toolbar-menu > li,
[dir="rtl"] .toolbar .toolbar-bar .toolbar-tab,
[dir="rtl"] .toolbar .toolbar-tray-horizontal li {
float: right;
}
/* 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 {
z-index: 501;
display: none;
}
.toolbar-oriented .toolbar-tray-vertical {
position: absolute;
left: -100%; /* LTR */
width: 240px;
width: 15rem;
}
[dir="rtl"] .toolbar-oriented .toolbar-tray-vertical {
right: -100%;
left: auto;
}
.toolbar .toolbar-tray-vertical > .toolbar-lining {
min-height: 100%;
}
/* 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
* 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,
.toolbar-fixed .toolbar .toolbar-tray-vertical {
position: fixed;
overflow-x: hidden;
overflow-y: auto;
height: 100%;
}
.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 {
right: 0;
left: auto;
}
/* When the configured standard breakpoint is active, the tray appears to push
* the page content away from the edge of the viewport. */
.toolbar-tray-open.toolbar-vertical.toolbar-fixed {
margin-inline-start: 15rem;
}
@media print {
.toolbar-tray-open.toolbar-vertical.toolbar-fixed {
margin-inline-start: 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 {
position: absolute;
top: auto;
right: 0; /* LTR */
bottom: 0;
}
[dir="rtl"] .toolbar-oriented .toolbar-tray-horizontal .toolbar-toggle-orientation {
right: auto;
left: 0;
}
.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;
}
/**
* Toolbar home button toggle.
*/
.toolbar .toolbar-bar .home-toolbar-tab {
display: none;
}
.path-admin .toolbar-bar .home-toolbar-tab {
display: block;
}
/* Anti flicker styling. */
.toolbar-anti-flicker.toolbar-loading.toolbar-fixed body {
padding-top: 2.4375rem;
}
.toolbar-anti-flicker.toolbar-loading.toolbar-fixed.toolbar-horizontal.toolbar-tray-open body {
padding-top: 4.91331rem;
}
.toolbar-anti-flicker.toolbar-loading.toolbar-vertical.toolbar-tray-open .toolbar-tray {
position: fixed;
z-index: -1;
top: 2.4375rem;
bottom: 0;
display: block;
width: 15rem;
inset-inline-start: 0;
}
.toolbar-tray-lazy-placeholder-link {
position: relative;
z-index: 0;
display: block;
}
.toolbar-tray-open.toolbar-fixed.toolbar-vertical #toolbar-administration {
margin-inline-start: -15rem;
}
.toolbar .toolbar-tray-vertical > .toolbar-lining::before {
width: 100%;
}
.toolbar-oriented .toolbar-tray-vertical > .toolbar-lining::before {
position: fixed;
z-index: -1;
top: 0;
bottom: 0;
display: block;
width: 15rem;
content: "";
inset-inline-start: 0;
}
.toolbar-anti-flicker.toolbar-vertical.toolbar-tray-open .menu-item + .menu-item {
border-top: 1px solid #ddd;
}

View File

@@ -0,0 +1,169 @@
/**
* @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 {
padding: 1em 1.3333em;
cursor: pointer;
text-decoration: none;
line-height: 1em;
}
.toolbar .toolbar-item:hover,
.toolbar .toolbar-item:focus {
text-decoration: underline;
}
/**
* Toolbar bar.
*/
.toolbar .toolbar-bar {
color: #ddd;
background-color: #0f0f0f;
box-shadow: -1px 0 3px 1px rgba(0, 0, 0, 0.3333); /* LTR */
}
[dir="rtl"] .toolbar .toolbar-bar {
box-shadow: 1px 0 3px 1px rgba(0, 0, 0, 0.3333);
}
.toolbar .toolbar-bar .toolbar-item {
color: #fff;
}
.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: linear-gradient(rgba(255, 255, 255, 0.125) 20%, transparent 200%);
}
.toolbar .toolbar-bar .toolbar-tab > .toolbar-item.is-active {
color: #000;
border-bottom: 1px solid #ddd;
background-color: #fff;
}
/**
* Toolbar tray.
*/
.toolbar .toolbar-tray {
background-color: #fff;
}
.toolbar-horizontal .toolbar-tray > .toolbar-lining {
padding-right: 5em; /* LTR */
}
[dir="rtl"] .toolbar-horizontal .toolbar-tray > .toolbar-lining {
padding-right: 0;
padding-left: 5em;
}
.toolbar .toolbar-tray-vertical {
border-right: 1px solid #aaa; /* LTR */
background-color: #f5f5f5;
box-shadow: -1px 0 5px 2px rgba(0, 0, 0, 0.3333); /* LTR */
}
[dir="rtl"] .toolbar .toolbar-tray-vertical {
border-right: 0 none;
border-left: 1px solid #aaa;
box-shadow: 1px 0 5px 2px rgba(0, 0, 0, 0.3333);
}
.toolbar-horizontal .toolbar-tray {
border-bottom: 1px solid #aaa;
box-shadow: -2px 1px 3px 1px rgba(0, 0, 0, 0.3333); /* LTR */
}
[dir="rtl"] .toolbar-horizontal .toolbar-tray {
box-shadow: 2px 1px 3px 1px rgba(0, 0, 0, 0.3333);
}
.toolbar .toolbar-tray-horizontal .toolbar-tray {
background-color: #f5f5f5;
}
.toolbar-tray a,
.toolbar-tray a:visited {
padding: 1em 1.3333em;
cursor: pointer;
text-decoration: none;
color: #565656;
}
.toolbar-tray a:hover,
.toolbar-tray a:active,
.toolbar-tray a:focus,
.toolbar-tray a.is-active {
text-decoration: underline;
color: #000;
}
.toolbar .toolbar-menu {
background-color: #fff;
}
.toolbar-horizontal .toolbar-tray .menu-item + .menu-item {
border-left: 1px solid #ddd; /* LTR */
}
[dir="rtl"] .toolbar-horizontal .toolbar-tray .menu-item + .menu-item {
border-right: 1px solid #ddd;
border-left: 0 none;
}
.toolbar-horizontal .toolbar-tray .menu-item:last-child {
border-right: 1px solid #ddd; /* LTR */
}
[dir="rtl"] .toolbar-horizontal .toolbar-tray .menu-item:last-child {
border-left: 1px solid #ddd;
}
.toolbar .toolbar-tray-vertical .menu-item + .menu-item {
border-top: 1px solid #ddd;
}
.toolbar .toolbar-tray-vertical .menu-item:last-child {
border-bottom: 1px solid #ddd;
}
.toolbar .toolbar-tray-vertical .menu-item .menu-item {
border: 0 none;
}
.toolbar .toolbar-tray-vertical .toolbar-menu ul ul {
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
}
.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-right: 0.25em;
margin-left: 0;
}
.toolbar .toolbar-menu .toolbar-menu a {
color: #434343;
}
/**
* Orientation toggle.
*/
.toolbar .toolbar-toggle-orientation {
height: 100%;
padding: 0;
background-color: #f5f5f5;
}
.toolbar-horizontal .toolbar-tray .toolbar-toggle-orientation {
border-left: 1px solid #c9c9c9; /* LTR */
}
[dir="rtl"] .toolbar-horizontal .toolbar-tray .toolbar-toggle-orientation {
border-right: 1px solid #c9c9c9;
border-left: 0 none;
}
.toolbar .toolbar-toggle-orientation > .toolbar-lining {
float: right; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-toggle-orientation > .toolbar-lining {
float: left;
}
.toolbar .toolbar-toggle-orientation button {
display: inline-block;
cursor: pointer;
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,72 @@
/**
* @file
* Prevents flicker of the toolbar on page load.
*/
(() => {
const toolbarState = sessionStorage.getItem('Drupal.toolbar.toolbarState')
? JSON.parse(sessionStorage.getItem('Drupal.toolbar.toolbarState'))
: false;
// These are classes that toolbar typically adds to <body>, but this code
// executes before the first paint, when <body> is not yet present. The
// classes are added to <html> so styling immediately reflects the current
// toolbar state. The classes are removed after the toolbar completes
// initialization.
const classesToAdd = ['toolbar-loading', 'toolbar-anti-flicker'];
if (toolbarState) {
const {
orientation,
hasActiveTab,
isFixed,
activeTray,
activeTabId,
isOriented,
userButtonMinWidth,
} = toolbarState;
classesToAdd.push(
orientation ? `toolbar-${orientation}` : 'toolbar-horizontal',
);
if (hasActiveTab !== false) {
classesToAdd.push('toolbar-tray-open');
}
if (isFixed) {
classesToAdd.push('toolbar-fixed');
}
if (isOriented) {
classesToAdd.push('toolbar-oriented');
}
if (activeTray) {
// These styles are added so the active tab/tray styles are present
// immediately instead of "flickering" on as the toolbar initializes. In
// instances where a tray is lazy loaded, these styles facilitate the
// lazy loaded tray appearing gracefully and without reflow.
const styleContent = `
.toolbar-loading #${activeTabId} {
background-image: linear-gradient(rgba(255, 255, 255, 0.25) 20%, transparent 200%);
}
.toolbar-loading #${activeTabId}-tray {
display: block; box-shadow: -1px 0 5px 2px rgb(0 0 0 / 33%);
border-right: 1px solid #aaa; background-color: #f5f5f5;
z-index: 0;
}
.toolbar-loading.toolbar-vertical.toolbar-tray-open #${activeTabId}-tray {
width: 15rem; height: 100vh;
}
.toolbar-loading.toolbar-horizontal :not(#${activeTray}) > .toolbar-lining {opacity: 0}`;
const style = document.createElement('style');
style.textContent = styleContent;
style.setAttribute('data-toolbar-anti-flicker-loading', true);
document.querySelector('head').appendChild(style);
if (userButtonMinWidth) {
const userButtonStyle = document.createElement('style');
userButtonStyle.textContent = `
#toolbar-item-user {min-width: ${userButtonMinWidth}px;}`;
document.querySelector('head').appendChild(userButtonStyle);
}
}
}
document.querySelector('html').classList.add(...classesToAdd);
})();

View File

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

View File

@@ -0,0 +1,256 @@
/**
* @file
* Builds a nested accordion widget.
*
* Invoke on an HTML list element with the jQuery plugin pattern.
*
* @example
* $('.toolbar-menu').drupalToolbarMenu();
*/
(function ($, Drupal, drupalSettings) {
/**
* Store the open menu tray.
*/
let activeItem = Drupal.url(drupalSettings.path.currentPath);
/**
* Maintains active tab in horizontal orientation.
*/
$.fn.drupalToolbarMenuHorizontal = function () {
let currentPath = drupalSettings.path.currentPath;
const menu = once('toolbar-menu-horizontal', this);
if (menu.length) {
const $menu = $(menu);
if (activeItem) {
const count = currentPath.split('/').length;
// Find the deepest link with its parent info and start
// marking active.
for (let i = 0; i < count; i++) {
const $menuItem = $menu.find(
`a[data-drupal-link-system-path="${currentPath}"]`,
);
if ($menuItem.length !== 0) {
$menuItem.closest('a').addClass('is-active');
break;
}
const lastIndex = currentPath.lastIndexOf('/');
currentPath = currentPath.slice(0, lastIndex);
}
}
}
};
$.fn.drupalToolbarMenu = function () {
const ui = {
handleOpen: Drupal.t('Extend'),
handleClose: Drupal.t('Collapse'),
};
/**
* Toggle the open/close state of a list is a menu.
*
* @param {jQuery} $item
* The li item to be toggled.
*
* @param {Boolean} switcher
* A flag that forces toggleClass to add or a remove a class, rather than
* simply toggling its presence.
*/
function toggleList($item, switcher) {
const $toggle = $item
.children('.toolbar-box')
.children('.toolbar-handle');
switcher =
typeof switcher !== 'undefined' ? switcher : !$item.hasClass('open');
// Toggle the item open state.
$item.toggleClass('open', switcher);
// Twist the toggle.
$toggle.toggleClass('open', switcher);
// Adjust the toggle text.
$toggle.find('.action').each((index, element) => {
// Expand Structure, Collapse Structure.
element.textContent = switcher ? ui.handleClose : ui.handleOpen;
});
}
/**
* Handle clicks from the disclosure button on an item with sub-items.
*
* @param {Object} event
* A jQuery Event object.
*/
function toggleClickHandler(event) {
const $toggle = $(event.target);
const $item = $toggle.closest('li');
// Toggle the list item.
toggleList($item);
// Close open sibling menus.
const $openItems = $item.siblings().filter('.open');
toggleList($openItems, false);
}
/**
* Handle clicks from a menu item link.
*
* @param {Object} event
* A jQuery Event object.
*/
function linkClickHandler(event) {
// If the toolbar is positioned fixed (and therefore hiding content
// underneath), then users expect clicks in the administration menu tray
// to take them to that destination but for the menu tray to be closed
// after clicking: otherwise the toolbar itself is obstructing the view
// of the destination they chose.
if (!Drupal.toolbar.models.toolbarModel.get('isFixed')) {
Drupal.toolbar.models.toolbarModel.set('activeTab', null);
}
// Stopping propagation to make sure that once a toolbar-box is clicked
// (the whitespace part), the page is not redirected anymore.
event.stopPropagation();
}
/**
* Add markup to the menu elements.
*
* Items with sub-elements have a list toggle attached to them. Menu item
* links and the corresponding list toggle are wrapped with in a div
* classed with .toolbar-box. The .toolbar-box div provides a positioning
* context for the item list toggle.
*
* @param {jQuery} $menu
* The root of the menu to be initialized.
*/
function initItems($menu) {
const options = {
class: 'toolbar-icon toolbar-handle',
action: ui.handleOpen,
text: '',
};
// Initialize items and their links.
$menu.find('li > a').wrap('<div class="toolbar-box">');
// Add a handle to each list item if it has a menu.
$menu.find('li').each((index, element) => {
const $item = $(element);
if ($item.children('ul.toolbar-menu').length) {
const $box = $item.children('.toolbar-box');
const $link = $box.find('a');
options.text = Drupal.t('@label', {
'@label': $link.length ? $link[0].textContent : '',
});
$item
.children('.toolbar-box')
.append(
$(Drupal.theme('toolbarMenuItemToggle', options))
.hide()
.fadeIn(150),
);
}
});
}
/**
* Adds a level class to each list based on its depth in the menu.
*
* This function is called recursively on each sub level of lists elements
* until the depth of the menu is exhausted.
*
* @param {jQuery} $lists
* A jQuery object of ul elements.
*
* @param {number} level
* The current level number to be assigned to the list elements.
*/
function markListLevels($lists, level) {
level = !level ? 1 : level;
const $lis = $lists.children('li').addClass(`level-${level}`);
$lists = $lis.children('ul');
if ($lists.length) {
markListLevels($lists, level + 1);
}
}
/**
* On page load, open the active menu item.
*
* Marks the trail of the active link in the menu back to the root of the
* menu with .menu-item--active-trail.
*
* @param {jQuery} $menu
* The root of the menu.
*/
function openActiveItem($menu) {
let currentPath = drupalSettings.path.currentPath;
const pathItem = $menu.find(`a[href="${window.location.pathname}"]`);
if (pathItem.length && !activeItem) {
activeItem = window.location.pathname;
}
if (activeItem) {
const $activeItem = $menu
.find(`a[href="${activeItem}"]`)
.addClass('menu-item--active');
if (pathItem.length === 0 && activeItem) {
const count = currentPath.split('/').length;
// Find the deepest link with its parent info and start
// marking active.
for (let i = 0; i < count; i++) {
const $menuItem = $menu.find(
`a[data-drupal-link-system-path="${currentPath}"]`,
);
if ($menuItem.length !== 0) {
const $activeTrail = $menuItem
.parentsUntil('.root', 'li')
.addClass('menu-item--active-trail');
toggleList($activeTrail, true);
break;
}
const lastIndex = currentPath.lastIndexOf('/');
currentPath = currentPath.slice(0, lastIndex);
}
} else {
const $activeTrail = $activeItem
.parentsUntil('.root', 'li')
.addClass('menu-item--active-trail');
toggleList($activeTrail, true);
}
}
}
// Return the jQuery object.
return this.each(function (selector) {
const menu = once('toolbar-menu-vertical', this);
if (menu.length) {
const $menu = $(menu);
// 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,48 @@
/**
* @file
* A Backbone view for the body element.
*/
(function ($, Drupal, Backbone) {
Drupal.toolbar.BodyVisualView = Backbone.View.extend(
/** @lends Drupal.toolbar.BodyVisualView# */ {
/**
* Adjusts the body element with the toolbar position and dimension changes.
*
* @constructs
*
* @augments Backbone.View
*/
initialize() {
this.listenTo(this.model, 'change:activeTray ', this.render);
this.listenTo(
this.model,
'change:isFixed change:isViewportOverflowConstrained',
this.isToolbarFixed,
);
},
isToolbarFixed() {
// When the toolbar is fixed, it will not scroll with page scrolling.
const isViewportOverflowConstrained = this.model.get(
'isViewportOverflowConstrained',
);
$('body').toggleClass(
'toolbar-fixed',
isViewportOverflowConstrained || this.model.get('isFixed'),
);
},
/**
* {@inheritdoc}
*/
render() {
$('body')
// Toggle the toolbar-tray-open class on the body element. The class is
// applied when a toolbar tray is active. Padding might be applied to
// the body element to prevent the tray from overlapping content.
.toggleClass('toolbar-tray-open', !!this.model.get('activeTray'));
},
},
);
})(jQuery, Drupal, Backbone);

View File

@@ -0,0 +1,65 @@
/**
* @file
* A Backbone view for the collapsible menus.
*/
(function ($, Backbone, Drupal) {
Drupal.toolbar.MenuVisualView = Backbone.View.extend(
/** @lends Drupal.toolbar.MenuVisualView# */ {
/**
* Backbone View for collapsible menus.
*
* @constructs
*
* @augments Backbone.View
*/
initialize() {
this.listenTo(this.model, 'change:subtrees', this.render);
// Render the view immediately on initialization.
this.render();
},
/**
* {@inheritdoc}
*/
render() {
this.renderVertical();
this.renderHorizontal();
},
/**
* Renders the toolbar menu in horizontal mode.
*/
renderHorizontal() {
// Render horizontal.
if ('drupalToolbarMenu' in $.fn) {
this.$el.children('.toolbar-menu').drupalToolbarMenuHorizontal();
}
},
/**
* Renders the toolbar menu in vertical mode.
*/
renderVertical() {
const subtrees = this.model.get('subtrees');
// Rendering the vertical menu depends on the subtrees.
if (!this.model.get('subtrees')) {
return;
}
// Add subtrees.
Object.keys(subtrees || {}).forEach((id) => {
$(
once('toolbar-subtrees', this.$el.find(`#toolbar-link-${id}`)),
).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,80 @@
/**
* @file
* A Backbone view for the aural feedback of the toolbar.
*/
(function (Backbone, Drupal) {
Drupal.toolbar.ToolbarAuralView = Backbone.View.extend(
/** @lends Drupal.toolbar.ToolbarAuralView# */ {
/**
* Backbone view for the aural feedback of the toolbar.
*
* @constructs
*
* @augments Backbone.View
*
* @param {object} options
* Options for the view.
* @param {object} options.strings
* Various strings to use in the view.
*/
initialize(options) {
this.strings = options.strings;
this.listenTo(
this.model,
'change:orientation',
this.onOrientationChange,
);
this.listenTo(this.model, 'change:activeTray', this.onActiveTrayChange);
},
/**
* Announces an orientation change.
*
* @param {Drupal.toolbar.ToolbarModel} model
* The toolbar model in question.
* @param {string} orientation
* The new value of the orientation attribute in the model.
*/
onOrientationChange(model, orientation) {
Drupal.announce(
Drupal.t('Tray orientation changed to @orientation.', {
'@orientation': orientation,
}),
);
},
/**
* Announces a changed active tray.
*
* @param {Drupal.toolbar.ToolbarModel} model
* The toolbar model in question.
* @param {HTMLElement} tray
* The new value of the tray attribute in the model.
*/
onActiveTrayChange(model, tray) {
const relevantTray =
tray === null ? model.previous('activeTray') : tray;
// Current activeTray and previous activeTray are empty, no state change
// to announce.
if (!relevantTray) {
return;
}
const action = tray === null ? Drupal.t('closed') : Drupal.t('opened');
const trayNameElement =
relevantTray.querySelector('.toolbar-tray-name');
let text;
if (trayNameElement !== null) {
text = Drupal.t('Tray "@tray" @action.', {
'@tray': trayNameElement.textContent,
'@action': action,
});
} else {
text = Drupal.t('Tray @action.', { '@action': action });
}
Drupal.announce(text);
},
},
);
})(Backbone, Drupal);

View File

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

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,163 @@
<?php
namespace Drupal\toolbar\Controller;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\toolbar\Ajax\SetSubtreesCommand;
/**
* Defines a controller for the toolbar module.
*/
class ToolbarController extends ControllerBase implements TrustedCallbackInterface {
/**
* Constructs a ToolbarController object.
*
* @param \Drupal\Component\Datetime\TimeInterface|null $time
* The time service.
*/
public function __construct(
protected ?TimeInterface $time = NULL,
) {
if ($this->time === NULL) {
@trigger_error('Calling ' . __METHOD__ . ' without the $time argument is deprecated in drupal:10.3.0 and it will be required in drupal:11.0.0. See https://www.drupal.org/node/3112298', E_USER_DEPRECATED);
$this->time = \Drupal::service('datetime.time');
}
}
/**
* Returns an AJAX response to render the toolbar subtrees.
*
* @return \Drupal\Core\Ajax\AjaxResponse
*/
public function subtreesAjax() {
[$subtrees] = 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($this->time->getRequestTime() + $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') && hash_equals($expected_hash, $hash))->cachePerPermissions();
}
/**
* Renders the toolbar's administration tray.
*
* @param array $element
* A renderable array.
*
* @return array
* The updated renderable array.
*
* @see \Drupal\Core\Render\RendererInterface::render()
*/
public static function preRenderAdministrationTray(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 = [
['callable' => 'menu.default_tree_manipulators:checkAccess'],
['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'],
['callable' => 'toolbar_menu_navigation_links'],
];
$tree = $menu_tree->transform($tree, $manipulators);
$element['administration_menu'] = $menu_tree->build($tree);
return $element;
}
/**
* #pre_render callback for toolbar_get_rendered_subtrees().
*
* @internal
*/
public static function preRenderGetRenderedSubtrees(array $data) {
$menu_tree = \Drupal::service('toolbar.menu_tree');
$renderer = \Drupal::service('renderer');
// 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 = [
['callable' => 'menu.default_tree_manipulators:checkAccess'],
['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'],
['callable' => 'toolbar_menu_navigation_links'],
];
$tree = $menu_tree->transform($tree, $manipulators);
$subtrees = [];
// Calculated the combined cacheability of all subtrees.
$cacheability = CacheableMetadata::createFromRenderArray($data);
foreach ($tree as $element) {
/** @var \Drupal\Core\Menu\MenuLinkInterface $link */
$link = $element->link;
if ($element->subtree) {
$subtree = $menu_tree->build($element->subtree);
$output = $renderer->executeInRenderContext(new RenderContext(), function () use ($renderer, $subtree) {
return $renderer->render($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(['.', '<', '>'], ['-', '', ''], $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;
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['preRenderAdministrationTray', 'preRenderGetRenderedSubtrees'];
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace Drupal\toolbar\Element;
use Drupal\Component\Utility\Html;
use Drupal\Core\Render\Attribute\RenderElement;
use Drupal\Core\Render\Element\RenderElementBase;
use Drupal\Core\Render\Element;
/**
* Provides a render element for the default Drupal toolbar.
*/
#[RenderElement('toolbar')]
class Toolbar extends RenderElementBase {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = static::class;
return [
'#pre_render' => [
[$class, 'preRenderToolbar'],
],
'#theme' => 'toolbar',
'#attached' => [
'library' => [
'toolbar/toolbar',
],
],
// Metadata for the toolbar wrapping element.
'#attributes' => [
'id' => 'toolbar-administration',
'role' => 'group',
'aria-label' => $this->t('Site administration toolbar'),
],
// Metadata for the administration bar.
'#bar' => [
'#heading' => $this->t('Toolbar items'),
'#attributes' => [
'id' => 'toolbar-bar',
'role' => 'navigation',
'aria-label' => $this->t('Toolbar items'),
],
],
];
}
/**
* Builds the Toolbar as a structured array ready for rendering.
*
* 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 = [];
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, ['\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,86 @@
<?php
namespace Drupal\toolbar\Element;
use Drupal\Core\Render\Attribute\RenderElement;
use Drupal\Core\Render\Element\RenderElementBase;
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 RenderElementBase {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = static::class;
return [
'#pre_render' => [
[$class, 'preRenderToolbarItem'],
],
'tab' => [
'#type' => 'link',
'#title' => '',
'#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 = [
'id' => $id,
];
// If tray content is present, markup the tray and its associated trigger.
if (!empty($element['tray'])) {
// Provide attributes necessary for trays.
$attributes += [
'data-toolbar-tray' => $id . '-tray',
'role' => 'button',
'aria-pressed' => 'false',
];
// Merge in module-provided attributes.
$element['tab'] += ['#attributes' => []];
$element['tab']['#attributes'] += $attributes;
$element['tab']['#attributes']['class'][] = 'trigger';
// Provide attributes for the tray theme wrapper.
$attributes = [
'id' => $id . '-tray',
'data-toolbar-tray' => $id . '-tray',
];
// Merge in module-provided attributes.
if (!isset($element['tray']['#wrapper_attributes'])) {
$element['tray']['#wrapper_attributes'] = [];
}
$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 [];
}
$build = parent::build($tree);
/** @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);
}
}
}

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,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 https://twig.symfony.com/doc/3.x/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', 'clearfix') }}>
<h2 class="visually-hidden">{{ toolbar_heading }}</h2>
{% for key, tab in tabs %}
{% set tray = trays[key] %}
<div{{ tab.attributes.addClass('toolbar-tab') }}>
{{ tab.link }}
{% apply 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>
{% endapply %}
</div>
{% endfor %}
</nav>
{{ remainder }}
</div>

View File

@@ -0,0 +1,10 @@
name: 'Disable user toolbar'
type: module
description: 'Support module for testing toolbar without user toolbar'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

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,10 @@
name: 'Toolbar module API tests'
type: module
description: 'Support module for toolbar testing.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,64 @@
<?php
/**
* @file
* A dummy module to test API interaction with the Toolbar module.
*/
use Drupal\Core\Link;
use Drupal\Core\Url;
/**
* Implements hook_toolbar().
*/
function toolbar_test_toolbar() {
$items['testing'] = [
'#type' => 'toolbar_item',
'tab' => [
'#type' => 'link',
'#title' => t('Test tab'),
'#url' => Url::fromRoute('<front>'),
'#options' => [
'attributes' => [
'id' => 'toolbar-tab-testing',
'title' => t('Test tab'),
],
],
],
'tray' => [
'#heading' => t('Test tray'),
'#wrapper_attributes' => [
'id' => 'toolbar-tray-testing',
],
'content' => [
'#theme' => 'item_list',
'#items' => [
Link::fromTextAndUrl(t('link 1'), Url::fromRoute('<front>', [], ['attributes' => ['title' => 'Test link 1 title']]))->toRenderable(),
Link::fromTextAndUrl(t('link 2'), Url::fromRoute('<front>', [], ['attributes' => ['title' => 'Test link 2 title']]))->toRenderable(),
Link::fromTextAndUrl(t('link 3'), Url::fromRoute('<front>', [], ['attributes' => ['title' => 'Test link 3 title']]))->toRenderable(),
],
'#attributes' => [
'class' => ['toolbar-menu'],
],
],
],
'#weight' => 50,
];
$items['empty'] = [
'#type' => 'toolbar_item',
];
return $items;
}
/**
* Implements hook_preprocess_HOOK().
*/
function toolbar_test_preprocess_menu(&$variables) {
// All the standard hook_theme variables should be populated when the
// Toolbar module is rendering a menu.
foreach (['menu_name', 'items', 'attributes'] as $variable) {
$variables[$variable];
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\toolbar\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for toolbar.
*
* @group toolbar
*/
class GenericTest extends GenericModuleTestBase {}

View File

@@ -0,0 +1,489 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\toolbar\Functional;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Url;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\Entity\Role;
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
* @group #slow
*/
class ToolbarAdminMenuTest extends BrowserTestBase {
/**
* A user with permission to access the administrative toolbar.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* A second user with permission to access the administrative toolbar.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser2;
/**
* The current admin menu subtrees hash for adminUser.
*
* @var string
*/
protected $hash;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'node',
'block',
'menu_ui',
'user',
'taxonomy',
'toolbar',
'language',
'test_page_test',
'locale',
'search',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$perms = [
'access toolbar',
'access administration pages',
'administer site configuration',
'bypass node access',
'administer themes',
'administer nodes',
'access content overview',
'administer blocks',
'administer menu',
'administer modules',
'administer permissions',
'administer users',
'access user profiles',
'administer taxonomy',
'administer languages',
'translate interface',
'administer search',
];
// 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->assertSession()->statusCodeEquals(200);
// Assert that the toolbar is present in the HTML.
$this->assertSession()->responseContains('id="toolbar-administration"');
// Store the adminUser admin menu subtrees hash for comparison later.
$this->hash = $this->getSubtreesHash();
}
/**
* Tests Toolbar's responses to installing and uninstalling modules.
*
* @see toolbar_modules_installed()
* @see toolbar_modules_uninstalled()
*/
public function testModuleStatusChangeSubtreesHashCacheClear(): void {
// Use an admin role to ensure the user has all available permissions. This
// results in the admin menu links changing as the taxonomy module is
// installed and uninstalled because the role will always have the
// 'administer taxonomy' permission if it exists.
$role = Role::load($this->createRole([]));
$role->setIsAdmin(TRUE);
$role->save();
$this->adminUser->addRole($role->id())->save();
// Uninstall a module.
$edit = [];
$edit['uninstall[taxonomy]'] = TRUE;
$this->drupalGet('admin/modules/uninstall');
$this->submitForm($edit, 'Uninstall');
// Confirm the uninstall form.
$this->submitForm([], 'Uninstall');
$this->rebuildContainer();
// Assert that the subtrees hash has been altered because the subtrees
// structure changed.
$this->assertDifferentHash();
// Enable a module.
$edit = [];
$edit['modules[taxonomy][enable]'] = TRUE;
$this->drupalGet('admin/modules');
$this->submitForm($edit, 'Install');
$this->rebuildContainer();
// Assert that the subtrees hash has been altered because the subtrees
// structure changed.
$this->assertDifferentHash();
}
/**
* Tests toolbar cache tags implementation.
*/
public function testMenuLinkUpdateSubtreesHashCacheClear(): void {
// The ID of (any) admin menu link.
$admin_menu_link_id = 'system.admin_config_development';
// Disable the link.
$edit = [];
$edit['enabled'] = FALSE;
$this->drupalGet("admin/structure/menu/link/" . $admin_menu_link_id . "/edit");
$this->submitForm($edit, 'Save');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('The menu link has been saved.');
// Assert that the subtrees hash has been altered because the subtrees
// structure changed.
$this->assertDifferentHash();
}
/**
* Tests Toolbar's responses to user data updates.
*
* @see toolbar_user_role_update()
* @see toolbar_user_update()
*/
public function testUserRoleUpdateSubtreesHashCacheClear(): void {
// Find the new role ID.
$all_rids = $this->adminUser->getRoles();
unset($all_rids[array_search(RoleInterface::AUTHENTICATED_ID, $all_rids)]);
$rid = reset($all_rids);
$edit = [];
$edit[$rid . '[administer taxonomy]'] = FALSE;
$this->drupalGet('admin/people/permissions');
$this->submitForm($edit, '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->assertSession()->statusCodeEquals(200);
// Assert that the toolbar is present in the HTML.
$this->assertSession()->responseContains('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->assertSession()->statusCodeEquals(200);
// Assert that the toolbar is present in the HTML.
$this->assertSession()->responseContains('id="toolbar-administration"');
$this->hash = $this->getSubtreesHash();
$rid = $this->drupalCreateRole(['administer content types']);
// Assign the role to the user.
$this->drupalGet('user/' . $this->adminUser->id() . '/edit');
$this->submitForm(["roles[{$rid}]" => $rid], 'Save');
$this->assertSession()->pageTextContains('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->assertSession()->statusCodeEquals(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->assertNotEmpty($new_subtree_hash, 'A valid hash value for the admin menu subtrees was created.');
$this->assertEquals($admin_user_2_hash, $new_subtree_hash, 'The user-specific subtree menu hash has not been updated.');
}
/**
* Tests cache invalidation when one user modifies another user.
*/
public function testNonCurrentUserAccountUpdates(): void {
$admin_user_id = $this->adminUser->id();
$this->hash = $this->getSubtreesHash();
// adminUser2 will add a role to adminUser.
$this->drupalLogin($this->adminUser2);
$rid = $this->drupalCreateRole(['administer content types']);
// Get the subtree hash for adminUser2 to check later that it has not
// changed. Request a new page to refresh the drupalSettings object.
$this->drupalGet('test-page');
$this->assertSession()->statusCodeEquals(200);
$admin_user_2_hash = $this->getSubtreesHash();
// Assign the role to the user.
$this->drupalGet('user/' . $admin_user_id . '/edit');
$this->submitForm(["roles[{$rid}]" => $rid], 'Save');
$this->assertSession()->pageTextContains('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->assertNotEmpty($new_subtree_hash, 'A valid hash value for the admin menu subtrees was created.');
$this->assertEquals($new_subtree_hash, $admin_user_2_hash, 'The user-specific subtree menu hash has not been updated.');
}
/**
* Tests that toolbar cache is cleared when string translations are made.
*/
public function testLocaleTranslationSubtreesHashCacheClear(): void {
$admin_user = $this->adminUser;
// User to translate and delete string.
$translate_user = $this->drupalCreateUser([
'translate interface',
'access administration pages',
]);
// Create a new language with the langcode 'xx'.
$langcode = 'xx';
// The English name for the language. This will be translated.
$name = $this->randomMachineName(16);
// This will be the translation of $name.
$translation = $this->randomMachineName(16);
// Add custom language.
$this->drupalLogin($admin_user);
$edit = [
'predefined_langcode' => 'custom',
'langcode' => $langcode,
'label' => $name,
'direction' => LanguageInterface::DIRECTION_LTR,
];
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add custom language');
t($name, [], ['langcode' => $langcode]);
// Reset locale cache.
$this->container->get('string_translation')->reset();
$this->assertSession()->responseContains('"edit-languages-' . $langcode . '-weight"');
// Verify that the test language was added.
$this->assertSession()->pageTextContains($name);
// Have the adminUser request a page in the new language.
$this->drupalGet($langcode . '/test-page');
$this->assertSession()->statusCodeEquals(200);
// Get a baseline hash for the admin menu subtrees before translating one
// of the menu link items.
$original_subtree_hash = $this->getSubtreesHash();
$this->assertNotEmpty($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);
// We need to visit the page to get the string to be translated.
$this->drupalGet($langcode . '/admin/config');
$search = [
'string' => 'Search and metadata',
'langcode' => $langcode,
'translation' => 'untranslated',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
$this->assertSession()->pageTextNotContains('No strings available');
// Verify that search found the string as untranslated.
$this->assertSession()->pageTextContains($name);
// Assume this is the only result.
// Translate the string to a random string.
$textarea = $this->assertSession()->elementExists('xpath', '//textarea');
$lid = (string) $textarea->getAttribute('name');
$edit = [
$lid => $translation,
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($edit, 'Save translations');
$this->assertSession()->pageTextContains('The strings have been saved.');
// Verify that the user is redirected to the correct page.
$this->assertSession()->addressEquals(Url::fromRoute('locale.translate_page'));
$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);
$this->drupalGet('admin/config');
// Have the adminUser request a page in the new language.
$this->drupalGet($langcode . '/test-page');
$this->assertSession()->statusCodeEquals(200);
$new_subtree_hash = $this->getSubtreesHash();
// Assert that the old admin menu subtrees hash and the new admin menu
// subtrees hash are different.
$this->assertNotEmpty($new_subtree_hash, 'A valid hash value for the admin menu subtrees was created.');
$this->assertNotEquals($original_subtree_hash, $new_subtree_hash, 'The user-specific subtree menu hash has been updated.');
}
/**
* Tests that the 'toolbar/subtrees/{hash}' is reachable and correct.
*/
public function testSubtreesJsonRequest(): void {
$admin_user = $this->adminUser;
$this->drupalLogin($admin_user);
// Request a new page to refresh the drupalSettings object.
$subtrees_hash = $this->getSubtreesHash();
$this->drupalGet('toolbar/subtrees/' . $subtrees_hash, ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']], ['X-Requested-With' => 'XMLHttpRequest']);
$ajax_result = json_decode($this->getSession()->getPage()->getContent(), TRUE);
$this->assertEquals('setToolbarSubtrees', $ajax_result[0]['command'], 'Subtrees response uses the correct command.');
$this->assertEquals(['system-admin_content', 'system-admin_structure', 'system-themes_page', 'system-modules_list', 'system-admin_config', 'entity-user-collection', 'front'], array_keys($ajax_result[0]['subtrees']), 'Correct subtrees returned.');
}
/**
* Tests that subtrees hashes vary by the language of the page.
*/
public function testLanguageSwitching(): void {
// Create a new language with the langcode 'xx'.
$langcode = 'xx';
$language = ConfigurableLanguage::createFromLangcode($langcode);
$language->save();
// The language path processor is just registered for more than one
// configured language, so rebuild the container now that we are
// multilingual.
$this->rebuildContainer();
// Get a page with the new language langcode in the URL.
$this->drupalGet('test-page', ['language' => $language]);
// Assert different hash.
$new_subtree_hash = $this->getSubtreesHash();
// Assert that the old admin menu subtree hash and the new admin menu
// subtree hash are different.
$this->assertNotEmpty($new_subtree_hash, 'A valid hash value for the admin menu subtrees was created.');
$this->assertNotEquals($this->hash, $new_subtree_hash, 'The user-specific subtree menu hash has been updated.');
}
/**
* Tests that back to site link exists on admin pages, not on content pages.
*/
public function testBackToSiteLink(): void {
// Back to site link should exist in the markup.
$this->drupalGet('test-page');
$back_link = $this->cssSelect('.home-toolbar-tab');
$this->assertNotEmpty($back_link);
}
/**
* Tests that external links added to the menu appear in the toolbar.
*/
public function testExternalLink(): void {
$edit = [
'title[0][value]' => 'External URL',
'link[0][uri]' => 'http://example.org',
'menu_parent' => 'admin:system.admin',
'description[0][value]' => 'External URL & escaped',
];
$this->drupalGet('admin/structure/menu/manage/admin/add');
$this->submitForm($edit, 'Save');
// Assert that the new menu link is shown on the menu link listing.
$this->drupalGet('admin/structure/menu/manage/admin');
$this->assertSession()->pageTextContains('External URL');
// Assert that the new menu link is shown in the toolbar on a regular page.
$this->drupalGet(Url::fromRoute('<front>'));
$this->assertSession()->pageTextContains('External URL');
// Ensure the description is escaped as expected.
$this->assertSession()->responseContains('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'];
}
/**
* Checks the subtree hash of the current page with that of the previous page.
*
* Asserts that the subtrees hash on a fresh page GET is different from the
* subtree hash from the previous page GET.
*
* @internal
*/
private function assertDifferentHash(): void {
// Request a new page to refresh the drupalSettings object.
$this->drupalGet('test-page');
$this->assertSession()->statusCodeEquals(200);
$new_subtree_hash = $this->getSubtreesHash();
// Assert that the old admin menu subtree hash and the new admin menu
// subtree hash are different.
$this->assertNotEmpty($new_subtree_hash, 'A valid hash value for the admin menu subtrees was created.');
$this->assertNotEquals($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,152 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\toolbar\Functional;
use Drupal\Core\Cache\Cache;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the cache contexts for toolbar.
*
* @group toolbar
*/
class ToolbarCacheContextsTest extends BrowserTestBase {
use AssertPageCacheContextsAndTagsTrait;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['toolbar', 'test_page_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* 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(): void {
parent::setUp();
$this->adminUser = $this->drupalCreateUser($this->perms);
$this->adminUser2 = $this->drupalCreateUser($this->perms);
}
/**
* Tests toolbar cache integration.
*/
public function testCacheIntegration(): void {
$this->installExtraModules(['csrf_test', 'dynamic_page_cache']);
$this->drupalLogin($this->adminUser);
$this->drupalGet('test-page');
$this->assertSession()->responseHeaderEquals('X-Drupal-Dynamic-Cache', 'MISS');
$this->assertCacheContexts(['session', 'user', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT], 'Expected cache contexts found with CSRF token link.');
$this->drupalGet('test-page');
$this->assertSession()->responseHeaderEquals('X-Drupal-Dynamic-Cache', 'HIT');
$this->assertCacheContexts(['session', 'user', 'url.query_args:' . MainContentViewSubscriber::WRAPPER_FORMAT], 'Expected cache contexts found with CSRF token link.');
}
/**
* Tests toolbar cache contexts.
*/
public function testToolbarCacheContextsCaller(): void {
// Test with default combination and permission to see toolbar.
$this->assertToolbarCacheContexts(['user', 'session'], '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 comment module enabled.
$this->installExtraModules(['comment']);
$this->adminUser2 = $this->drupalCreateUser(array_merge($this->perms, ['access comments']));
$this->assertToolbarCacheContexts(['user.permissions'], 'Expected cache contexts found with comment module enabled.');
\Drupal::service('module_installer')->uninstall(['comment']);
}
/**
* 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.
*
* @internal
*/
protected function assertToolbarCacheContexts(array $cache_contexts, ?string $message = NULL): void {
// 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');
$this->assertCacheContexts($cache_contexts, $message);
$this->drupalLogout();
// Assert contexts for user2 which has some additional permissions.
$this->drupalLogin($this->adminUser2);
$this->drupalGet('test-page');
$this->assertCacheContexts($cache_contexts, $message);
}
/**
* 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,69 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\toolbar\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the implementation of hook_toolbar() by a module.
*
* @group toolbar
*/
class ToolbarHookToolbarTest extends BrowserTestBase {
/**
* A user with permission to access the administrative toolbar.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['toolbar', 'toolbar_test', 'test_page_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create an administrative user and log it in.
$this->adminUser = $this->drupalCreateUser(['access toolbar']);
$this->drupalLogin($this->adminUser);
}
/**
* Tests for a tab and tray provided by a module implementing hook_toolbar().
*/
public function testHookToolbar(): void {
$this->drupalGet('test-page');
$this->assertSession()->statusCodeEquals(200);
// Assert that the toolbar is present in the HTML.
$this->assertSession()->responseContains('id="toolbar-administration"');
// Assert that the tab registered by toolbar_test is present.
$this->assertSession()->responseContains('id="toolbar-tab-testing"');
// Assert that the tab item descriptions are present.
$this->assertSession()->responseContains('title="Test tab"');
// Assert that the tray registered by toolbar_test is present.
$this->assertSession()->responseContains('id="toolbar-tray-testing"');
// Assert that tray item descriptions are present.
$this->assertSession()->responseContains('title="Test link 1 title"');
}
}

View File

@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\toolbar\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests that the toolbar icon class remains for translated menu items.
*
* @group toolbar
*/
class ToolbarMenuTranslationTest extends BrowserTestBase {
/**
* A user with permission to access the administrative toolbar.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'toolbar',
'toolbar_test',
'locale',
'locale_test',
'block',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create an administrative user and log it in.
$this->adminUser = $this->drupalCreateUser([
'access toolbar',
'translate interface',
'administer languages',
'access administration pages',
'administer blocks',
]);
$this->drupalLogin($this->adminUser);
}
/**
* Tests that toolbar classes don't change when adding a translation.
*/
public function testToolbarClasses(): void {
$langcode = 'es';
// Add Spanish.
$edit['predefined_langcode'] = $langcode;
$this->drupalGet('admin/config/regional/language/add');
$this->submitForm($edit, 'Add language');
// The menu item 'Structure' in the toolbar will be translated.
$menu_item = 'Structure';
// Visit a page that has the string on it so it can be translated.
$this->drupalGet($langcode . '/admin/structure');
// Search for the menu item.
$search = [
'string' => $menu_item,
'langcode' => $langcode,
'translation' => 'untranslated',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
// Make sure will be able to translate the menu item.
$this->assertSession()->pageTextNotContains('No strings available.');
// Check that the class is on the item before we translate it.
$this->assertSession()->elementsCount('xpath', '//a[contains(@class, "icon-system-admin-structure")]', 1);
// Translate the menu item.
$menu_item_translated = $this->randomMachineName();
$textarea = $this->assertSession()->elementExists('xpath', '//textarea');
$lid = (string) $textarea->getAttribute('name');
$edit = [
$lid => $menu_item_translated,
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($edit, 'Save translations');
// Search for the translated menu item.
$search = [
'string' => $menu_item,
'langcode' => $langcode,
'translation' => 'translated',
];
$this->drupalGet('admin/config/regional/translate');
$this->submitForm($search, 'Filter');
// Make sure the menu item string was translated.
$this->assertSession()->pageTextContains($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->assertSession()->pageTextContains($menu_item_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.
$this->assertSession()->elementsCount('xpath', '//a[contains(@class, "icon-system-admin-structure")]', 1);
}
}

View File

@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\toolbar\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests that the active trail is maintained in the toolbar.
*
* @group toolbar
*/
class ToolbarActiveTrailTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['toolbar', 'node', 'field_ui'];
/**
* {@inheritdoc}
*
* @todo Remove and fix test to not rely on super user.
* @see https://www.drupal.org/project/drupal/issues/3437620
*/
protected bool $usesSuperUserAccessPolicy = TRUE;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalLogin($this->rootUser);
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
}
/**
* Tests that the active trail is maintained even when traversed deeper.
*
* @param string $orientation
* The toolbar orientation.
*
* @testWith ["vertical"]
* ["horizontal"]
*
* @throws \Behat\Mink\Exception\ElementNotFoundException
*/
public function testToolbarActiveTrail(string $orientation): void {
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
$this->drupalGet('<front>');
$this->assertNotEmpty($this->assertSession()->waitForElement('css', 'body.toolbar-horizontal'));
$this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', '.toolbar-tray'));
$this->assertSession()->waitForElementRemoved('css', '.toolbar-loading');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#toolbar-item-administration.is-active'));
// If testing for vertical orientation of the toolbar then switch to it.
if ($orientation === 'vertical') {
$page->pressButton('Vertical orientation');
}
// Traverse deeper.
$this->clickLink('Structure');
$this->clickLink('Content types');
$this->clickLink('Manage fields');
$this->clickLink('Edit');
if ($orientation === 'vertical') {
$this->assertNotEmpty($assert_session->waitForElementVisible('named',
['link', 'Structure']));
// Assert that menu-item--active-trail was maintained.
$this->assertTrue($assert_session->waitForElementVisible('named',
['link', 'Structure'])->getParent()->getParent()->hasClass('menu-item--active-trail'));
$this->assertTrue($assert_session->waitForElementVisible('named',
['link', 'Content types'])->getParent()->getParent()->hasClass('menu-item--active-trail'));
// Change orientation and check focus is maintained.
$page->pressButton('Horizontal orientation');
$this->assertTrue($assert_session->waitForElementVisible('css',
'#toolbar-link-system-admin_structure')->hasClass('is-active'));
}
else {
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#toolbar-link-system-admin_structure'));
// Assert that is-active was maintained.
$this->assertTrue($assert_session->waitForElementVisible('css', '#toolbar-link-system-admin_structure')->hasClass('is-active'));
// Change orientation and check focus is maintained.
$page->pressButton('Vertical orientation');
// Introduce a delay to let the focus load.
$this->getSession()->wait(150);
$this->assertTrue($assert_session->waitForElementVisible('named',
['link', 'Structure'])->getParent()->getParent()->hasClass('menu-item--active-trail'));
$this->assertTrue($assert_session->waitForElementVisible('named',
['link', 'Content types'])->getParent()->getParent()->hasClass('menu-item--active-trail'));
}
}
}

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\toolbar\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests the JavaScript functionality of the toolbar.
*
* @group toolbar
*/
class ToolbarIntegrationTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['toolbar', 'node'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests if the toolbar can be toggled with JavaScript.
*/
public function testToolbarToggling(): void {
$admin_user = $this->drupalCreateUser([
'access toolbar',
'administer site configuration',
'access content overview',
]);
$this->drupalLogin($admin_user);
// Set size for horizontal toolbar.
$this->getSession()->resizeWindow(1200, 600);
$this->drupalGet('<front>');
$this->assertNotEmpty($this->assertSession()->waitForElement('css', 'body.toolbar-horizontal'));
$this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', '.toolbar-tray'));
$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.');
}
/**
* Tests that the orientation toggle is not shown for empty toolbar items.
*/
public function testEmptyTray(): void {
// Granting access to the toolbar but not any administrative menu links will
// result in an empty toolbar tray for the "Manage" toolbar item.
$admin_user = $this->drupalCreateUser([
'access toolbar',
]);
$this->drupalLogin($admin_user);
// Set size for horizontal toolbar.
$this->getSession()->resizeWindow(1200, 600);
$this->drupalGet('<front>');
$this->assertNotEmpty($this->assertSession()->waitForElement('css', 'body.toolbar-horizontal'));
$this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', '.toolbar-tray'));
// Test that the orientation toggle does not appear.
$page = $this->getSession()->getPage();
$tray = $page->findById('toolbar-item-administration-tray');
$this->assertTrue($tray->hasClass('toolbar-tray-horizontal'), 'Toolbar tray is horizontally oriented by default.');
$this->assertSession()->elementNotExists('css', '#toolbar-item-administration-tray .toolbar-menu');
$this->assertSession()->elementNotExists('css', '#toolbar-item-administration-tray .toolbar-toggle-orientation');
$button = $page->findButton('Vertical orientation');
$this->assertFalse($button->isVisible(), 'Orientation toggle from other tray is not visible');
}
}

View File

@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\toolbar\FunctionalJavascript;
use Drupal\Component\Serialization\Json;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests the sessionStorage state set by the toolbar.
*
* @group toolbar
*/
class ToolbarStoredStateTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['toolbar', 'node'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
public function testToolbarStoredState(): void {
$admin_user = $this->drupalCreateUser([
'access toolbar',
'administer site configuration',
'access content overview',
]);
$this->drupalLogin($admin_user);
$page = $this->getSession()->getPage();
$assert_session = $this->assertSession();
$this->drupalGet('<front>');
$this->assertNotEmpty($this->assertSession()->waitForElement('css', 'body.toolbar-horizontal'));
$this->assertNotEmpty($this->assertSession()->waitForElementVisible('css', '.toolbar-tray'));
$this->assertSession()->waitForElementRemoved('css', '.toolbar-loading');
$page->clickLink('toolbar-item-user');
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '#toolbar-item-user.is-active'));
// Expected state values with the user tray open with horizontal
// orientation.
$expected = [
'orientation' => 'horizontal',
'hasActiveTab' => TRUE,
'activeTabId' => 'toolbar-item-user',
'activeTray' => 'toolbar-item-user-tray',
'isOriented' => TRUE,
'isFixed' => TRUE,
];
$toolbar_stored_state = JSON::decode(
$this->getSession()->evaluateScript("sessionStorage.getItem('Drupal.toolbar.toolbarState')")
);
// The userButtonMinWidth property will differ depending on the length of
// the test-generated username, so it is checked differently and the value
// is copied to the expected value array.
$this->assertNotNull($toolbar_stored_state['userButtonMinWidth']);
$this->assertIsNumeric($toolbar_stored_state['userButtonMinWidth']);
$this->assertGreaterThan(60, $toolbar_stored_state['userButtonMinWidth']);
$expected['userButtonMinWidth'] = $toolbar_stored_state['userButtonMinWidth'];
$this->assertSame($expected, $toolbar_stored_state);
$page->clickLink('toolbar-item-user');
$assert_session->assertNoElementAfterWait('css', '#toolbar-item-user.is-active');
// Update expected state values to reflect no tray being open.
$expected['hasActiveTab'] = FALSE;
$expected['activeTabId'] = NULL;
unset($expected['activeTray']);
$toolbar_stored_state = JSON::decode(
$this->getSession()->evaluateScript("sessionStorage.getItem('Drupal.toolbar.toolbarState')")
);
$this->assertSame($expected, $toolbar_stored_state);
$page->clickLink('toolbar-item-administration');
$orientation_toggle = $assert_session->waitForElementVisible('css', '[title="Vertical orientation"]');
$orientation_toggle->click();
$assert_session->waitForElementVisible('css', 'body.toolbar-vertical');
// Update expected state values to reflect the administration tray being
// open with vertical orientation.
$expected['orientation'] = 'vertical';
$expected['hasActiveTab'] = TRUE;
$expected['activeTabId'] = 'toolbar-item-administration';
$expected['activeTray'] = 'toolbar-item-administration-tray';
$toolbar_stored_state = JSON::decode(
$this->getSession()->evaluateScript("sessionStorage.getItem('Drupal.toolbar.toolbarState')")
);
$this->assertSame($expected, $toolbar_stored_state);
$this->getSession()->resizeWindow(600, 600);
// Update expected state values to reflect the viewport being at a width
// that is narrow enough that the toolbar isn't fixed.
$expected['isFixed'] = FALSE;
$toolbar_stored_state = JSON::decode(
$this->getSession()->evaluateScript("sessionStorage.getItem('Drupal.toolbar.toolbarState')")
);
$this->assertSame($expected, $toolbar_stored_state);
}
}

View File

@@ -0,0 +1,263 @@
/**
* @file
* Tests of the existing Toolbar JS Api.
*/
module.exports = {
'@tags': ['core'],
before(browser) {
browser
.drupalInstall()
.drupalInstallModule('toolbar', true)
.drupalCreateUser({
name: 'user',
password: '123',
permissions: [
'access site reports',
'access toolbar',
'administer menu',
'administer modules',
'administer site configuration',
'administer account settings',
'administer software updates',
'access content',
'administer permissions',
'administer users',
],
})
.drupalLogin({ name: 'user', password: '123' });
},
beforeEach(browser) {
// Set the resolution to the default desktop resolution. Ensure the default
// toolbar is horizontal in headless mode.
browser
.setWindowSize(1920, 1080)
// To clear active tab/tray from previous tests
.execute(function () {
localStorage.clear();
// Clear escapeAdmin URL values.
sessionStorage.clear();
})
.drupalRelativeURL('/')
.waitForElementPresent('#toolbar-administration', 50000, 1000, false);
},
after(browser) {
browser.drupalUninstall();
},
'Drupal.Toolbar.models': (browser) => {
browser.execute(
function () {
const toReturn = {};
const { models } = Drupal.toolbar;
toReturn.hasMenuModel = models.hasOwnProperty('menuModel');
toReturn.menuModelType = typeof models.menuModel === 'object';
toReturn.hasToolbarModel = models.hasOwnProperty('toolbarModel');
toReturn.toolbarModelType = typeof models.toolbarModel === 'object';
toReturn.toolbarModelActiveTab =
models.toolbarModel.get('activeTab').id ===
'toolbar-item-administration';
toReturn.toolbarModelActiveTray =
models.toolbarModel.get('activeTray').id ===
'toolbar-item-administration-tray';
toReturn.toolbarModelIsOriented =
models.toolbarModel.get('isOriented') === true;
toReturn.toolbarModelIsFixed =
models.toolbarModel.get('isFixed') === true;
toReturn.toolbarModelAreSubtreesLoaded =
models.toolbarModel.get('areSubtreesLoaded') === false;
toReturn.toolbarModelIsViewportOverflowConstrained =
models.toolbarModel.get('isViewportOverflowConstrained') === false;
toReturn.toolbarModelOrientation =
models.toolbarModel.get('orientation') === 'horizontal';
toReturn.toolbarModelLocked =
models.toolbarModel.get('locked') === null;
toReturn.toolbarModelIsTrayToggleVisible =
models.toolbarModel.get('isTrayToggleVisible') === true;
toReturn.toolbarModelHeight = models.toolbarModel.get('height') === 79;
toReturn.toolbarModelOffsetsBottom =
models.toolbarModel.get('offsets').bottom === 0;
toReturn.toolbarModelOffsetsLeft =
models.toolbarModel.get('offsets').left === 0;
toReturn.toolbarModelOffsetsRight =
models.toolbarModel.get('offsets').right === 0;
toReturn.toolbarModelOffsetsTop =
models.toolbarModel.get('offsets').top === 80;
toReturn.toolbarModelSubtrees =
models.menuModel.get('subtrees') === null;
return toReturn;
},
[],
(result) => {
const expectedTrue = {
hasMenuModel: 'has menu model',
menuModelType: 'menu model is an object',
hasToolbarModel: 'has toolbar model',
toolbarModelType: 'toolbar model is an object',
toolbarModelActiveTab: 'get("activeTab") has expected result',
toolbarModelActiveTray: 'get("activeTray") has expected result',
toolbarModelIsOriented: 'get("isOriented") has expected result',
toolbarModelIsFixed: 'get("isFixed") has expected result',
toolbarModelAreSubtreesLoaded:
'get("areSubtreesLoaded") has expected result',
toolbarModelIsViewportOverflowConstrained:
'get("isViewportOverflowConstrained") has expected result',
toolbarModelOrientation: 'get("orientation") has expected result',
toolbarModelLocked: 'get("locked") has expected result',
toolbarModelIsTrayToggleVisible:
'get("isTrayToggleVisible") has expected result',
toolbarModelHeight: 'get("height") has expected result',
toolbarModelOffsetsBottom:
'get("offsets") bottom has expected result',
toolbarModelOffsetsLeft: 'get("offsets") left has expected result',
toolbarModelOffsetsRight: 'get("offsets") right has expected result',
toolbarModelOffsetsTop: 'get("offsets") top has expected result',
toolbarModelSubtrees: 'get("subtrees") has expected result',
};
browser.assert.deepEqual(
Object.keys(expectedTrue).sort(),
Object.keys(result.value).sort(),
'Keys to check match',
);
Object.keys(expectedTrue).forEach((property) => {
browser.assert.equal(
result.value[property],
true,
expectedTrue[property],
);
});
},
);
},
'Change tab': (browser) => {
browser.execute(
function () {
const toReturn = {};
const { models } = Drupal.toolbar;
toReturn.hasMenuModel = models.hasOwnProperty('menuModel');
toReturn.menuModelType = typeof models.menuModel === 'object';
toReturn.hasToolbarModel = models.hasOwnProperty('toolbarModel');
toReturn.toolbarModelType = typeof models.toolbarModel === 'object';
const tab = document.querySelector('#toolbar-item-user');
tab.dispatchEvent(new MouseEvent('click', { bubbles: true }));
toReturn.toolbarModelChangedTab =
models.toolbarModel.get('activeTab').id === 'toolbar-item-user';
toReturn.toolbarModelChangedTray =
models.toolbarModel.get('activeTray').id === 'toolbar-item-user-tray';
return toReturn;
},
[],
(result) => {
const expectedTrue = {
hasMenuModel: 'has menu model',
menuModelType: 'menu model is an object',
hasToolbarModel: 'has toolbar model',
toolbarModelType: 'toolbar model is an object',
toolbarModelChangedTab: 'get("activeTab") has expected result',
toolbarModelChangedTray: 'get("activeTray") has expected result',
};
browser.assert.deepEqual(
Object.keys(expectedTrue).sort(),
Object.keys(result.value).sort(),
'Keys to check match',
);
Object.keys(expectedTrue).forEach((property) => {
browser.assert.equal(
result.value[property],
true,
expectedTrue[property],
);
});
},
);
},
'Change orientation': (browser) => {
browser.executeAsync(
function (done) {
const toReturn = {};
const { models } = Drupal.toolbar;
const orientationToggle = document.querySelector(
'#toolbar-item-administration-tray .toolbar-toggle-orientation button',
);
toReturn.toolbarOrientation =
models.toolbarModel.get('orientation') === 'horizontal';
orientationToggle.dispatchEvent(
new MouseEvent('click', { bubbles: true }),
);
setTimeout(() => {
toReturn.toolbarChangeOrientation =
models.toolbarModel.get('orientation') === 'vertical';
done(toReturn);
}, 100);
},
[],
(result) => {
const expectedTrue = {
toolbarOrientation: 'get("orientation") has expected result',
toolbarChangeOrientation: 'changing orientation has expected result',
};
browser.assert.deepEqual(
Object.keys(expectedTrue).sort(),
Object.keys(result.value).sort(),
'Keys to check match',
);
Object.keys(expectedTrue).forEach((property) => {
browser.assert.equal(
result.value[property],
true,
expectedTrue[property],
);
});
},
);
},
'Open submenu': (browser) => {
browser.executeAsync(
function (done) {
const toReturn = {};
const { models } = Drupal.toolbar;
Drupal.toolbar.models.toolbarModel.set('orientation', 'vertical');
toReturn.toolbarOrientation =
models.toolbarModel.get('orientation') === 'vertical';
const manageTab = document.querySelector(
'#toolbar-item-administration',
);
Drupal.toolbar.models.toolbarModel.set('activeTab', manageTab);
const menuDropdown = document.querySelector(
'#toolbar-item-administration-tray button',
);
menuDropdown.dispatchEvent(new MouseEvent('click', { bubbles: true }));
setTimeout(() => {
const statReportElement = document.querySelector(
'#toolbar-link-system-status',
);
toReturn.submenuItem =
statReportElement.textContent === 'Status report';
done(toReturn);
}, 100);
},
[],
(result) => {
const expectedTrue = {
toolbarOrientation: 'get("orientation") has expected result',
submenuItem: 'opening submenu has expected result',
};
browser.assert.deepEqual(
Object.keys(expectedTrue).sort(),
Object.keys(result.value).sort(),
'Keys to check match',
);
Object.keys(expectedTrue).forEach((property) => {
browser.assert.equal(
result.value[property],
true,
expectedTrue[property],
);
});
},
);
},
};

View File

@@ -0,0 +1,368 @@
/**
* @file
* Test the expected toolbar functionality.
*/
const itemAdministration = '#toolbar-item-administration';
const itemAdministrationTray = '#toolbar-item-administration-tray';
const adminOrientationButton = `${itemAdministrationTray} .toolbar-toggle-orientation button`;
const itemUser = '#toolbar-item-user';
const itemUserTray = '#toolbar-item-user-tray';
const userOrientationBtn = `${itemUserTray} .toolbar-toggle-orientation button`;
module.exports = {
'@tags': ['core'],
before(browser) {
browser
.drupalInstall()
.drupalInstallModule('toolbar', true)
.drupalCreateUser({
name: 'user',
password: '123',
permissions: [
'access site reports',
'access toolbar',
'access administration pages',
'administer menu',
'administer modules',
'administer site configuration',
'administer account settings',
'administer software updates',
'access content',
'administer permissions',
'administer users',
],
})
.drupalLogin({ name: 'user', password: '123' });
},
beforeEach(browser) {
// Set the resolution to the default desktop resolution. Ensure the default
// toolbar is horizontal in headless mode.
browser
.setWindowSize(1920, 1080)
// To clear active tab/tray from previous tests
.execute(function () {
localStorage.clear();
// Clear escapeAdmin URL values.
sessionStorage.clear();
})
.drupalRelativeURL('/')
.waitForElementPresent('#toolbar-administration');
},
after(browser) {
browser.drupalUninstall();
},
'Change tab': (browser) => {
browser.waitForElementPresent(itemUserTray);
browser.assert.not.hasClass(itemUser, 'is-active');
browser.assert.not.hasClass(itemUserTray, 'is-active');
browser.click(itemUser);
browser.assert.hasClass(itemUser, 'is-active');
browser.assert.hasClass(itemUserTray, 'is-active');
},
'Change orientation': (browser) => {
browser.waitForElementPresent(adminOrientationButton);
browser.assert.hasClass(
itemAdministrationTray,
'is-active toolbar-tray-horizontal',
);
browser.click(adminOrientationButton);
browser.assert.hasClass(
itemAdministrationTray,
'is-active toolbar-tray-vertical',
);
browser.click(adminOrientationButton);
browser.assert.hasClass(
itemAdministrationTray,
'is-active toolbar-tray-horizontal',
);
},
'Toggle tray': (browser) => {
browser.waitForElementPresent(itemUserTray);
browser.click(itemUser);
browser.assert.hasClass(itemUserTray, 'is-active');
browser.click(itemUser);
browser.assert.not.hasClass(itemUserTray, 'is-active');
browser.click(itemUser);
browser.assert.hasClass(itemUserTray, 'is-active');
},
'Toggle submenu and sub-submenu': (browser) => {
browser.waitForElementPresent(adminOrientationButton);
browser.assert.hasClass(
itemAdministrationTray,
'is-active toolbar-tray-horizontal',
);
browser.click(adminOrientationButton);
browser.assert.hasClass(
itemAdministrationTray,
'is-active toolbar-tray-vertical',
);
browser.waitForElementPresent(
'#toolbar-item-administration-tray li:nth-child(2) button',
);
browser.assert.not.hasClass(
'#toolbar-item-administration-tray li:nth-child(2)',
'open',
);
browser.assert.not.hasClass(
'#toolbar-item-administration-tray li:nth-child(2) button',
'open',
);
browser.click('#toolbar-item-administration-tray li:nth-child(2) button');
browser.assert.hasClass(
'#toolbar-item-administration-tray li:nth-child(2)',
'open',
);
browser.assert.hasClass(
'#toolbar-item-administration-tray li:nth-child(2) button',
'open',
);
browser.expect
.element('#toolbar-link-user-admin_index')
.text.to.equal('People');
browser.expect
.element('#toolbar-link-system-admin_config_system')
.text.to.equal('System');
// Check sub-submenu.
browser.waitForElementPresent(
'#toolbar-item-administration-tray li.menu-item.level-2',
);
browser.assert.not.hasClass(
'#toolbar-item-administration-tray li.menu-item.level-2',
'open',
);
browser.assert.not.hasClass(
'#toolbar-item-administration-tray li.menu-item.level-2 button',
'open',
);
browser.click(
'#toolbar-item-administration-tray li.menu-item.level-2 button',
);
browser.assert.hasClass(
'#toolbar-item-administration-tray li.menu-item.level-2',
'open',
);
browser.assert.hasClass(
'#toolbar-item-administration-tray li.menu-item.level-2 button',
'open',
);
browser.expect
.element('#toolbar-link-entity-user-admin_form')
.text.to.equal('Account settings');
},
'Narrow toolbar width breakpoint': (browser) => {
browser.waitForElementPresent(adminOrientationButton);
browser.assert.hasClass(
itemAdministrationTray,
'is-active toolbar-tray-horizontal',
);
browser.assert.hasClass('#toolbar-administration', 'toolbar-oriented');
browser.setWindowSize(263, 900);
browser.assert.hasClass(
itemAdministrationTray,
'is-active toolbar-tray-vertical',
);
browser.assert.not.hasClass(itemAdministration, 'toolbar-oriented');
},
'Standard width toolbar breakpoint': (browser) => {
browser.setWindowSize(1000, 900);
browser.waitForElementPresent(adminOrientationButton);
browser.assert.hasClass('body', 'toolbar-fixed');
browser.setWindowSize(609, 900);
browser.assert.hasClass(
itemAdministrationTray,
'is-active toolbar-tray-vertical',
);
browser.assert.not.hasClass('body', 'toolbar-fixed');
},
'Wide toolbar breakpoint': (browser) => {
browser.waitForElementPresent(adminOrientationButton);
browser.setWindowSize(975, 900);
browser.assert.hasClass(
itemAdministrationTray,
'is-active toolbar-tray-vertical',
);
},
'Back to site link': (browser) => {
const escapeSelector = '[data-toolbar-escape-admin]';
browser.drupalRelativeURL('/user');
browser.drupalRelativeURL('/admin');
// Don't check the visibility as stark doesn't add the .path-admin class
// to the <body> required to display the button.
browser.assert.attributeContains(escapeSelector, 'href', '/user/2');
},
'Aural view test: tray orientation': (browser) => {
browser.waitForElementPresent(
'#toolbar-item-administration-tray .toolbar-toggle-orientation button',
);
browser.executeAsync(
function (done) {
Drupal.announce = done;
const orientationButton = document.querySelector(
'#toolbar-item-administration-tray .toolbar-toggle-orientation button',
);
orientationButton.dispatchEvent(
new MouseEvent('click', { bubbles: true }),
);
},
(result) => {
browser.assert.equal(
result.value,
'Tray orientation changed to vertical.',
);
},
);
browser.executeAsync(
function (done) {
Drupal.announce = done;
const orientationButton = document.querySelector(
'#toolbar-item-administration-tray .toolbar-toggle-orientation button',
);
orientationButton.dispatchEvent(
new MouseEvent('click', { bubbles: true }),
);
},
(result) => {
browser.assert.equal(
result.value,
'Tray orientation changed to horizontal.',
);
},
);
},
'Aural view test: tray toggle': (browser) => {
browser.executeAsync(
function (done) {
Drupal.announce = done;
const $adminButton = jQuery('#toolbar-item-administration');
$adminButton.trigger('click');
},
(result) => {
browser.assert.equal(
result.value,
'Tray "Administration menu" closed.',
);
},
);
browser.executeAsync(
function (done) {
Drupal.announce = done;
const $adminButton = jQuery('#toolbar-item-administration');
$adminButton.trigger('click');
},
(result) => {
browser.assert.equal(
result.value,
'Tray "Administration menu" opened.',
);
},
);
},
'Toolbar event: drupalToolbarOrientationChange': (browser) => {
browser.executeAsync(
function (done) {
jQuery(document).on(
'drupalToolbarOrientationChange',
function (event, orientation) {
done(orientation);
},
);
const orientationButton = document.querySelector(
'#toolbar-item-administration-tray .toolbar-toggle-orientation button',
);
orientationButton.dispatchEvent(
new MouseEvent('click', { bubbles: true }),
);
},
(result) => {
browser.assert.equal(result.value, 'vertical');
},
);
},
'Toolbar event: drupalToolbarTabChange': (browser) => {
browser.executeAsync(
function (done) {
jQuery(document).on('drupalToolbarTabChange', function (event, tab) {
done(tab.id);
});
jQuery('#toolbar-item-user').trigger('click');
},
(result) => {
browser.assert.equal(result.value, 'toolbar-item-user');
},
);
},
'Toolbar event: drupalToolbarTrayChange': (browser) => {
browser.executeAsync(
function (done) {
const $adminButton = jQuery('#toolbar-item-administration');
// Hide the admin menu first, this event is not firing reliably
// otherwise.
$adminButton.trigger('click');
jQuery(document).on('drupalToolbarTrayChange', function (event, tray) {
done(tray.id);
});
$adminButton.trigger('click');
},
(result) => {
browser.assert.equal(result.value, 'toolbar-item-administration-tray');
},
);
},
'Locked toolbar vertical wide viewport': (browser) => {
browser.setWindowSize(1000, 900);
browser.waitForElementPresent(adminOrientationButton);
// eslint-disable-next-line no-unused-expressions
browser.expect.element(adminOrientationButton).to.be.visible;
browser.setWindowSize(975, 900);
browser.assert.hasClass(
itemAdministrationTray,
'is-active toolbar-tray-vertical',
);
// eslint-disable-next-line no-unused-expressions
browser.expect.element(adminOrientationButton).to.not.be.visible;
},
'Settings are retained on refresh': (browser) => {
browser.waitForElementPresent(itemUser);
// Set user as active tab.
browser.assert.not.hasClass(itemUser, 'is-active');
browser.assert.not.hasClass(itemUserTray, 'is-active');
browser.click(itemUser);
// Check tab and tray are open.
browser.assert.hasClass(itemUser, 'is-active');
browser.assert.hasClass(itemUserTray, 'is-active');
// Set orientation to vertical.
browser.waitForElementPresent(userOrientationBtn);
browser.assert.hasClass(itemUserTray, 'is-active toolbar-tray-horizontal');
browser.click(userOrientationBtn);
browser.assert.hasClass(itemUserTray, 'is-active toolbar-tray-vertical');
browser.refresh();
// Check user tab is active.
browser.assert.hasClass(itemUser, 'is-active');
// Check tray is active and orientation is vertical.
browser.assert.hasClass(itemUserTray, 'is-active toolbar-tray-vertical');
},
'Check toolbar overlap with page content': (browser) => {
browser.assert.hasClass('body', 'toolbar-horizontal');
browser.execute(
() => {
const toolbar = document.querySelector('#toolbar-administration');
const nextElement = toolbar.nextElementSibling.getBoundingClientRect();
const tray = document
.querySelector('#toolbar-item-administration-tray')
.getBoundingClientRect();
// Page content should start after the toolbar height to not overlap.
return nextElement.top > tray.top + tray.height;
},
(result) => {
browser.assert.equal(
result.value,
true,
'Toolbar and page content do not overlap',
);
},
);
},
};

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
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;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::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): void {
$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 static 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::preRenderToolbar().
*
* @return array
* 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 \Drupal\toolbar\Element\Toolbar::preRenderToolbar()
* @ingroup toolbar_tabs
*/
function hook_toolbar() {
$items = [];
// Add a search field to the toolbar. The search field employs no toolbar
// module theming functions.
$items['global_search'] = [
'#type' => 'toolbar_item',
'tab' => [
'#type' => 'search',
'#attributes' => [
'placeholder' => t('Search the site'),
'class' => ['search-global'],
],
],
'#weight' => 200,
// Custom CSS, JS or a library can be associated with the toolbar item.
'#attached' => [
'library' => [
'search/global',
],
],
];
// The 'Home' tab is a simple link, which is wrapped in markup associated
// with a visual tab styling.
$items['home'] = [
'#type' => 'toolbar_item',
'tab' => [
'#type' => 'link',
'#title' => t('Home'),
'#url' => Url::fromRoute('<front>'),
'#options' => [
'attributes' => [
'title' => t('Home page'),
'class' => ['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'] = [
'#type' => 'toolbar_item',
'tab' => [
'#type' => 'link',
'#title' => t('Shopping cart'),
'#url' => Url::fromRoute('cart'),
'#options' => [
'attributes' => [
'title' => t('Shopping cart'),
],
],
],
'tray' => [
'#heading' => t('Shopping cart actions'),
'shopping_cart' => [
'#theme' => 'item_list',
'#items' => [/* 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'] = [
// 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' => [
'#type' => 'link',
'#theme' => 'user_message_toolbar_tab',
'#theme_wrappers' => [],
'#title' => t('Messages'),
'#url' => Url::fromRoute('user.message'),
'#options' => [
'attributes' => [
'title' => t('Messages'),
],
],
],
'tray' => [
'#heading' => t('User messages'),
'messages' => [/* renderable content */],
],
'#weight' => 125,
];
return $items;
}
/**
* Alter the toolbar menu after hook_toolbar() is invoked.
*
* This hook is invoked by Toolbar::preRenderToolbar() 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,12 @@
name: Toolbar
type: module
description: 'Provides an administration toolbar to display links provided by modules.'
package: Core
# version: VERSION
dependencies:
- drupal:breakpoint
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,59 @@
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/jquery
- core/drupal
- core/drupalSettings
- core/drupal.ajax
- core/drupal.announce
# @todo Remove this in https://www.drupal.org/project/drupal/issues/3204015
- core/internal.backbone
- core/once
- core/drupal.displace
- toolbar/toolbar.menu
- toolbar/toolbar.anti-flicker
toolbar.menu:
version: VERSION
js:
js/toolbar.menu.js: {}
css:
state:
css/toolbar.menu.css: {}
dependencies:
- core/jquery
- core/drupal
- core/once
toolbar.escapeAdmin:
version: VERSION
js:
js/escapeAdmin.js: {}
dependencies:
- core/jquery
- core/drupal
- core/drupalSettings
- core/once
toolbar.anti-flicker:
# Block the page from being loaded until anti-flicker is initialized.
version: VERSION
header: true
js:
js/toolbar.anti-flicker.js: {}

View File

@@ -0,0 +1,298 @@
<?php
/**
* @file
* Administration toolbar for quick access to top level administration items.
*/
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Template\Attribute;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Url;
use Drupal\toolbar\Controller\ToolbarController;
/**
* Implements hook_help().
*/
function toolbar_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.toolbar':
$output = '<h2>' . t('About') . '</h2>';
$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>.', [':toolbar_docs' => 'https://www.drupal.org/docs/8/core/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'] = [
'render element' => 'element',
];
$items['menu__toolbar'] = [
'base hook' => 'menu',
'variables' => ['menu_name' => NULL, 'items' => [], 'attributes' => []],
];
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'] = [
'#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'] = [];
$variables['tabs'] = [];
$variables['remainder'] = [];
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 = [];
if (!empty($element[$key]['tray']['#wrapper_attributes'])) {
$attributes = $element[$key]['tray']['#wrapper_attributes'];
}
$variables['trays'][$key] = [
'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 = [];
// Pass the wrapper attributes along.
if (!empty($element[$key]['#wrapper_attributes'])) {
$attributes = $element[$key]['#wrapper_attributes'];
}
$variables['tabs'][$key] = [
'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, ['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'] = [
'#type' => 'toolbar_item',
'tab' => [
'#type' => 'link',
'#title' => t('Back to site'),
'#url' => Url::fromRoute('<front>'),
'#attributes' => [
'title' => t('Return to site content'),
'class' => ['toolbar-icon', 'toolbar-icon-escape-admin'],
'data-toolbar-escape-admin' => TRUE,
],
],
'#wrapper_attributes' => [
'class' => ['home-toolbar-tab'],
],
'#attached' => [
'library' => [
'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()
[$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'] = [
'#type' => 'toolbar_item',
'tab' => [
'#type' => 'link',
'#title' => t('Manage'),
'#url' => Url::fromRoute('system.admin'),
'#attributes' => [
'title' => t('Admin menu'),
'class' => ['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' => [
'#heading' => t('Administration menu'),
'#attached' => $subtrees_attached,
'toolbar_administration' => [
'#pre_render' => [[ToolbarController::class, 'preRenderAdministrationTray']],
'#type' => 'container',
'#attributes' => [
'class' => ['toolbar-menu-administration'],
],
],
],
'#weight' => -15,
];
$hash_cacheability->applyTo($items['administration']);
return $items;
}
/**
* 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(['.', '<', '>'], ['-', '', ''], $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(['.', ' ', '_'], ['-', '-', '-'], $definition['id']));
$element->options['attributes']['title'] = $link->getDescription();
}
return $tree;
}
/**
* Implements hook_preprocess_HOOK() for HTML document templates.
*/
function toolbar_preprocess_html(&$variables) {
if (!\Drupal::currentUser()->hasPermission('access toolbar')) {
return;
}
$variables['attributes']['class'][] = 'toolbar-loading';
}
/**
* 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' => [[ToolbarController::class, 'preRenderGetRenderedSubtrees']],
'#cache' => [
'keys' => [
'toolbar_rendered_subtrees',
],
],
'#cache_properties' => ['#subtrees'],
];
/** @var \Drupal\Core\Render\Renderer $renderer */
$renderer = \Drupal::service('renderer');
// The pre_render process populates $data during the render pipeline.
// We need to pass by reference so that populated data can be returned and
// used to resolve cacheability.
$renderer->executeInRenderContext(new RenderContext(), function () use ($renderer, &$data) {
$renderer->render($data);
});
return [$data['#subtrees'], CacheableMetadata::createFromRenderArray($data)];
}
/**
* Returns the hash of the user-rendered toolbar subtrees and cacheability.
*
* @return array
* An array with the hash of the toolbar subtrees and cacheability.
*/
function _toolbar_get_subtrees_hash() {
[$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 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', '@callable_resolver']