vanilla-framework
Version:
A simple, extendable CSS framework.
419 lines (351 loc) • 14.1 kB
JavaScript
// Setup toggling of side navigation drawer
(function () {
// throttling function calls, by Remy Sharp
// http://remysharp.com/2010/07/21/throttling-function-calls/
const throttle = function (fn, delay) {
let timer = null;
return function () {
let context = this,
args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
}, delay);
};
};
var expandedSidenavContainer = null;
var lastFocus = null;
var ignoreFocusChanges = false;
var focusAfterClose = null;
// Traps the focus within the currently expanded sidenav drawer
function trapFocus(event) {
if (ignoreFocusChanges || !expandedSidenavContainer) return;
// skip the focus trap if the sidenav is not in the expanded status (large screens)
if (!expandedSidenavContainer.classList.contains('is-drawer-expanded')) return;
var sidenavDrawer = expandedSidenavContainer.querySelector('.p-side-navigation__drawer');
if (sidenavDrawer.contains(event.target)) {
lastFocus = event.target;
} else {
focusFirstDescendant(sidenavDrawer);
if (lastFocus == document.activeElement) {
focusLastDescendant(sidenavDrawer);
}
lastFocus = document.activeElement;
}
}
// Attempts to focus given element
function attemptFocus(child) {
if (child.focus) {
ignoreFocusChanges = true;
child.focus();
ignoreFocusChanges = false;
return document.activeElement === child;
}
return false;
}
// Focuses first child element
function focusFirstDescendant(element) {
for (var i = 0; i < element.childNodes.length; i++) {
var child = element.childNodes[i];
if (attemptFocus(child) || focusFirstDescendant(child)) {
return true;
}
}
return false;
}
// Focuses last child element
function focusLastDescendant(element) {
for (var i = element.childNodes.length - 1; i >= 0; i--) {
var child = element.childNodes[i];
if (attemptFocus(child) || focusLastDescendant(child)) {
return true;
}
}
return false;
}
/**
Toggles the expanded/collapsed classes on side navigation element.
@param {HTMLElement} sideNavigation The side navigation element.
@param {Boolean} show Whether to show or hide the drawer.
*/
function toggleDrawer(sideNavigation, show) {
expandedSidenavContainer = show ? sideNavigation : null;
const toggleButtonOutsideDrawer = sideNavigation.querySelector('.p-side-navigation__toggle, .js-drawer-toggle');
const toggleButtonInsideDrawer = sideNavigation.querySelector('.p-side-navigation__toggle--in-drawer');
if (sideNavigation) {
if (show) {
sideNavigation.classList.remove('is-drawer-collapsed');
sideNavigation.classList.add('is-drawer-expanded');
toggleButtonInsideDrawer.focus();
toggleButtonOutsideDrawer.setAttribute('aria-expanded', true);
toggleButtonInsideDrawer.setAttribute('aria-expanded', true);
focusFirstDescendant(sideNavigation);
focusAfterClose = toggleButtonOutsideDrawer;
document.addEventListener('focus', trapFocus, true);
} else {
sideNavigation.classList.remove('is-drawer-expanded');
sideNavigation.classList.add('is-drawer-collapsed');
toggleButtonOutsideDrawer.focus();
toggleButtonOutsideDrawer.setAttribute('aria-expanded', false);
toggleButtonInsideDrawer.setAttribute('aria-expanded', false);
if (focusAfterClose && focusAfterClose.focus) {
focusAfterClose.focus();
}
document.removeEventListener('focus', trapFocus, true);
}
}
}
/**
Attaches event listeners for the side navigation toggles
@param {HTMLElement} sideNavigation The side navigation element.
*/
function setupSideNavigation(sideNavigation) {
var toggles = [].slice.call(sideNavigation.querySelectorAll('.js-drawer-toggle'));
var drawerEl = sideNavigation.querySelector('.p-side-navigation__drawer');
// hide navigation drawer on small screens
sideNavigation.classList.add('is-drawer-hidden');
// setup drawer element
drawerEl.addEventListener('animationend', () => {
if (!sideNavigation.classList.contains('is-drawer-expanded')) {
sideNavigation.classList.add('is-drawer-hidden');
}
});
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
toggleDrawer(sideNavigation, false);
}
});
// setup toggle buttons
toggles.forEach(function (toggle) {
toggle.addEventListener('click', function (event) {
event.preventDefault();
if (sideNavigation) {
sideNavigation.classList.remove('is-drawer-hidden');
toggleDrawer(sideNavigation, !sideNavigation.classList.contains('is-drawer-expanded'));
}
});
});
// hide side navigation drawer when screen is resized
window.addEventListener(
'resize',
throttle(function () {
toggles.forEach((toggle) => {
return toggle.setAttribute('aria-expanded', false);
});
// remove expanded/collapsed class names to avoid unexpected animations
sideNavigation.classList.remove('is-drawer-expanded');
sideNavigation.classList.remove('is-drawer-collapsed');
sideNavigation.classList.add('is-drawer-hidden');
}, 10),
);
}
/**
Attaches event listeners for all the side navigations in the document.
@param {String} sideNavigationSelector The CSS selector matching side navigation elements.
*/
function setupSideNavigations(sideNavigationSelector) {
// Setup all side navigations on the page.
var sideNavigations = [].slice.call(document.querySelectorAll(sideNavigationSelector));
sideNavigations.forEach(setupSideNavigation);
}
setupSideNavigations('.p-side-navigation, [class*="p-side-navigation--"]');
// Add table of contents to side navigation on documentation pages
const sideNav = document.querySelector('.p-side-navigation, [class*="p-side-navigation--"]');
// Generate id from H2s content when it does not exist
document.querySelectorAll('main h2:not([id])').forEach(function (heading) {
// Only get direct text from h2 node, excluding any child nodes
var id = heading.childNodes[0].textContent
.trim()
.toLowerCase()
.replaceAll(/\s+/g, '-')
.replaceAll(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g, '');
heading.setAttribute('id', id);
});
// get all headings from page and add it to table of contents
var list = document.createElement('ul');
list.classList.add('p-table-of-contents__list');
var item = document.createElement('li');
item.classList.add('p-table-of-contents__item');
var anchor = document.createElement('a');
anchor.classList.add('p-table-of-contents__link');
// Add all H2s with IDs to the table of contents list
[].slice.call(document.querySelectorAll('main h2[id]')).forEach(function (heading) {
var thisItem = item.cloneNode();
var thisAnchor = anchor.cloneNode();
thisAnchor.setAttribute('href', '#' + heading.id);
// Only get direct text from h2 node, excluding any child nodes
thisAnchor.textContent = heading.childNodes[0].textContent.trim();
thisItem.appendChild(thisAnchor);
list.appendChild(thisItem);
});
// Add table of contents as nested list to side navigation
if (list.querySelectorAll('li').length > 0) {
var toc = document.querySelector('#toc');
if (toc) {
toc.appendChild(list);
toc.closest('.u-hide').classList.remove('u-hide');
}
}
// accordion side navigation
var currentPage = document.querySelector('.p-side-navigation__link[aria-current="page"]');
if (currentPage) {
var parentList = currentPage.parentNode.parentNode;
parentList.setAttribute('aria-expanded', true);
parentList.previousElementSibling.setAttribute('aria-expanded', true);
}
function setupSideNavigationExpandToggle(toggle) {
const isExpanded = toggle.getAttribute('aria-expanded') === 'true';
if (!isExpanded) {
toggle.setAttribute('aria-expanded', isExpanded);
}
const item = toggle.closest('.p-side-navigation__item');
const link = item.querySelector('.p-side-navigation__link');
const nestedList = item.querySelector('.p-side-navigation__list');
if (!link?.hasAttribute('aria-expanded')) {
link.setAttribute('aria-expanded', isExpanded);
}
if (!nestedList?.hasAttribute('aria-expanded')) {
nestedList.setAttribute('aria-expanded', isExpanded);
}
}
function handleExpandToggle(event) {
const item = event.currentTarget.closest('.p-side-navigation__item');
const button = item.querySelector('.p-side-navigation__expand, .p-side-navigation__accordion-button');
const link = item.querySelector('.p-side-navigation__link');
const nestedList = item.querySelector('.p-side-navigation__list');
[button, link, nestedList].forEach((el) => {
el.setAttribute('aria-expanded', el.getAttribute('aria-expanded') === 'true' ? 'false' : 'true');
});
}
function setupSideNavigationExpands() {
var expandToggles = document.querySelectorAll('.p-side-navigation__expand, .p-side-navigation__accordion-button');
expandToggles.forEach((toggle) => {
setupSideNavigationExpandToggle(toggle);
toggle.addEventListener('click', (e) => {
handleExpandToggle(e);
});
});
}
setupSideNavigationExpands();
})();
// scroll active side navigation item into view (without scrolling whole page)
(function () {
var sideNav = document.querySelector('.p-side-navigation');
var currentItem = document.querySelector('.p-side-navigation__link[aria-current="page"]');
if (sideNav && currentItem) {
// calculate scroll by comparing top of side nav and top of active item
var currentItemOffset = currentItem.getBoundingClientRect().top;
var offset = currentItemOffset - sideNav.getBoundingClientRect().top;
// only scroll if active link is off screen or close to bottom of the window
if (currentItemOffset > window.innerHeight * 0.7) {
setTimeout(function () {
sideNav.scrollTop = offset;
}, 0);
}
}
})();
// Docs search functions
(function () {
var searchDocsReset = document.getElementById('search-docs-reset');
var searchBox = document.getElementById('search-docs');
if (searchDocsReset) {
searchDocsReset.addEventListener('click', function (e) {
searchBox.value = '';
searchBox.focus();
e.preventDefault();
});
}
})();
(function () {
function initNavigationSearch(element) {
const searchButtons = element.querySelectorAll('.js-search-button');
searchButtons.forEach((searchButton) => {
searchButton.addEventListener('click', toggleSearch);
});
const menuButton = element.querySelector('.js-menu-button');
if (menuButton) {
menuButton.addEventListener('click', toggleMenu);
}
const overlay = element.querySelector('.p-navigation__search-overlay');
if (overlay) {
overlay.addEventListener('click', closeAll);
}
function toggleMenu(e) {
e.preventDefault();
var navigation = e.target.closest('.p-navigation');
if (navigation.classList.contains('has-menu-open')) {
closeAll();
} else {
closeAll();
openMenu(e);
}
}
function toggleSearch(e) {
e.preventDefault();
var navigation = e.target.closest('.p-navigation');
if (navigation.classList.contains('has-search-open')) {
closeAll();
} else {
closeAll();
openSearch(e);
}
}
function openSearch(e) {
e.preventDefault();
var navigation = e.target.closest('.p-navigation');
var nav = navigation.querySelector('.p-navigation__nav');
var searchInput = navigation.querySelector('.p-search-box__input');
var buttons = document.querySelectorAll('.js-search-button');
buttons.forEach((searchButton) => {
searchButton.setAttribute('aria-pressed', true);
});
navigation.classList.add('has-search-open');
searchInput.focus();
document.addEventListener('keyup', keyPressHandler);
}
function openMenu(e) {
e.preventDefault();
var navigation = e.target.closest('.p-navigation');
var nav = navigation.querySelector('.p-navigation__nav');
var buttons = document.querySelectorAll('.js-menu-button');
buttons.forEach((searchButton) => {
searchButton.setAttribute('aria-pressed', true);
});
navigation.classList.add('has-menu-open');
document.addEventListener('keyup', keyPressHandler);
}
function closeSearch() {
var navigation = document.querySelector('.p-navigation');
var nav = navigation.querySelector('.p-navigation__nav');
var banner = document.querySelector('.p-navigation__banner');
var buttons = document.querySelectorAll('.js-search-button');
buttons.forEach((searchButton) => {
searchButton.removeAttribute('aria-pressed');
});
navigation.classList.remove('has-search-open');
document.removeEventListener('keyup', keyPressHandler);
}
function closeMenu() {
var navigation = document.querySelector('.p-navigation');
var nav = navigation.querySelector('.p-navigation__nav');
var banner = document.querySelector('.p-navigation__banner');
var buttons = document.querySelectorAll('.js-menu-button');
buttons.forEach((searchButton) => {
searchButton.removeAttribute('aria-pressed');
});
navigation.classList.remove('has-menu-open');
document.removeEventListener('keyup', keyPressHandler);
}
function closeAll() {
closeSearch();
closeMenu();
}
function keyPressHandler(e) {
if (e.key === 'Escape') {
closeAll();
}
}
}
var navigation = document.querySelector('#navigation');
initNavigationSearch(navigation);
})();