var newsarc = newsarc || {}; // Set a default value for scrolled. newsarc.scrolled = 0; // polyfill closest // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill if (!Element.prototype.closest) { Element.prototype.closest = function (s) { var el = this; do { if (el.matches(s)) { return el; } el = el.parentElement || el.parentNode; } while (el !== null && el.nodeType === 1); return null; }; } // polyfill forEach // https://developer.mozilla.org/en-US/docs/Web/API/NodeList/forEach#Polyfill if (window.NodeList && !NodeList.prototype.forEach) { NodeList.prototype.forEach = function (callback, thisArg) { var i; var len = this.length; thisArg = thisArg || window; for (i = 0; i < len; i++) { callback.call(thisArg, this[i], i, this); } }; } // event "polyfill" newsarc.createEvent = function (eventName) { var event; if (typeof window.Event === 'function') { event = new Event(eventName); } else { event = document.createEvent('Event'); event.initEvent(eventName, true, false); } return event; }; // matches "polyfill" // https://developer.mozilla.org/es/docs/Web/API/Element/matches if (!Element.prototype.matches) { Element.prototype.matches = Element.prototype.matchesSelector || Element.prototype.mozMatchesSelector || Element.prototype.msMatchesSelector || Element.prototype.oMatchesSelector || Element.prototype.webkitMatchesSelector || function (s) { var matches = (this.document || this.ownerDocument).querySelectorAll(s), i = matches.length; while (--i >= 0 && matches.item(i) !== this) { } return i > -1; }; } // Add a class to the body for when touch is enabled for browsers that don't support media queries // for interaction media features. Adapted from . newsarc.touchEnabled = { init: function () { var matchMedia = function () { // Include the 'heartz' as a way to have a non-matching MQ to help terminate the join. See . var prefixes = ['-webkit-', '-moz-', '-o-', '-ms-']; var query = ['(', prefixes.join('touch-enabled),('), 'heartz', ')'].join(''); }; if (('ontouchstart' in window) || (window.DocumentTouch && document instanceof window.DocumentTouch) || matchMedia()) { document.body.classList.add('touch-enabled'); } } }; // newsarc.touchEnabled /* ----------------------------------------------------------------------------------------------- Cover Modals --------------------------------------------------------------------------------------------------- */ newsarc.coverModals = { init: function () { if (document.querySelector('.cover-modal')) { // Handle cover modals when they're toggled. this.onToggle(); // When toggled, untoggle if visitor clicks on the wrapping element of the modal. this.outsideUntoggle(); // Close on escape key press. this.closeOnEscape(); // Hide and show modals before and after their animations have played out. this.hideAndShowModals(); } }, // Handle cover modals when they're toggled. onToggle: function () { document.querySelectorAll('.cover-modal').forEach(function (element) { element.addEventListener('toggled', function (event) { var modal = event.target, body = document.body; if (modal.classList.contains('active')) { body.classList.add('showing-modal'); } else { body.classList.remove('showing-modal'); body.classList.add('hiding-modal'); // Remove the hiding class after a delay, when animations have been run. setTimeout(function () { body.classList.remove('hiding-modal'); }, 500); } }); }); }, // Close modal on outside click. outsideUntoggle: function () { document.addEventListener('click', function (event) { var target = event.target; var modal = document.querySelector('.cover-modal.active'); // if target onclick is with # within the href attribute if (event.target.tagName.toLowerCase() === 'a' && event.target.hash.includes('#') && modal !== null) { // untoggle the modal this.untoggleModal(modal); // wait 550 and scroll to the anchor setTimeout(function () { var anchor = document.getElementById(event.target.hash.slice(1)); anchor.scrollIntoView(); }, 550); } if (target === modal) { this.untoggleModal(target); } }.bind(this)); }, // Close modal on escape key press. closeOnEscape: function () { document.addEventListener('keydown', function (event) { if (event.keyCode === 27) { event.preventDefault(); document.querySelectorAll('.cover-modal.active').forEach(function (element) { this.untoggleModal(element); }.bind(this)); } }.bind(this)); }, // Hide and show modals before and after their animations have played out. hideAndShowModals: function () { var _doc = document, _win = window, modals = _doc.querySelectorAll('.cover-modal'), htmlStyle = _doc.documentElement.style, adminBar = _doc.querySelector('#wpadminbar'); function getAdminBarHeight(negativeValue) { var height, currentScroll = _win.pageYOffset; if (adminBar) { height = currentScroll + adminBar.getBoundingClientRect().height; return negativeValue ? -height : height; } return currentScroll === 0 ? 0 : -currentScroll; } function htmlStyles() { var overflow = _win.innerHeight > _doc.documentElement.getBoundingClientRect().height; return { }; } // Show the modal. modals.forEach(function (modal) { modal.addEventListener('toggle-target-before-inactive', function (event) { var styles = htmlStyles(), offsetY = _win.pageYOffset, paddingTop = (Math.abs(getAdminBarHeight()) - offsetY) + 'px', mQuery = _win.matchMedia('(max-width: 600px)'); if (event.target !== modal) { return; } Object.keys(styles).forEach(function (styleKey) { htmlStyle.setProperty(styleKey, styles[styleKey]); }); _win.newsarc.scrolled = parseInt(styles.top, 10); if (adminBar) { if (mQuery.matches) { if (offsetY >= getAdminBarHeight()) { modal.style.setProperty('top', 0); } else { modal.style.setProperty('top', (getAdminBarHeight() - offsetY) + 'px'); } } } modal.classList.add('show-modal'); }); // Hide the modal after a delay, so animations have time to play out. modal.addEventListener('toggle-target-after-inactive', function (event) { if (event.target !== modal) { return; } setTimeout(function () { var clickedEl = newsarc.toggles.clickedEl; modal.classList.remove('show-modal'); Object.keys(htmlStyles()).forEach(function (styleKey) { htmlStyle.removeProperty(styleKey); }); if (adminBar) { modal.style.removeProperty('top'); } if (clickedEl !== false) { clickedEl.focus(); clickedEl = false; } }, 500); }); }); }, // Untoggle a modal. untoggleModal: function (modal) { var modalTargetClass, modalToggle = false; // If the modal has specified the string (ID or class) used by toggles to target it, untoggle the toggles with that target string. // The modal-target-string must match the string toggles use to target the modal. if (modal.dataset.modalTargetString) { modalTargetClass = modal.dataset.modalTargetString; modalToggle = document.querySelector('*[data-toggle-target="' + modalTargetClass + '"]'); } // If a modal toggle exists, trigger it so all of the toggle options are included. if (modalToggle) { modalToggle.click(); // If one doesn't exist, just hide the modal. } else { modal.classList.remove('active'); } } }; // newsarc.coverModals /* ----------------------------------------------------------------------------------------------- Modal Menu --------------------------------------------------------------------------------------------------- */ newsarc.modalMenu = { init: function () { // If the current menu item is in a sub level, expand all the levels higher up on load. this.expandLevel(); this.keepFocusInModal(); }, expandLevel: function () { var modalMenus = document.querySelectorAll('.modal-menu'); modalMenus.forEach(function (modalMenu) { var activeMenuItem = modalMenu.querySelector('.current-menu-item'); if (activeMenuItem) { newsarcFindParents(activeMenuItem, 'li').forEach(function (element) { var subMenuToggle = element.querySelector('.sub-menu-toggle'); if (subMenuToggle) { newsarc.toggles.performToggle(subMenuToggle, true); } }); } }); }, keepFocusInModal: function () { var _doc = document; _doc.addEventListener('keydown', function (event) { var toggleTarget, modal, selectors, elements, menuType, bottomMenu, activeEl, lastEl, firstEl, tabKey, shiftKey, clickedEl = newsarc.toggles.clickedEl; if (clickedEl && _doc.body.classList.contains('showing-modal')) { toggleTarget = clickedEl.dataset.toggleTarget; selectors = 'input, a, button'; modal = _doc.querySelector(toggleTarget); elements = modal.querySelectorAll(selectors); elements = Array.prototype.slice.call(elements); if ('.menu-modal' === toggleTarget) { menuType = menuType ? '.expanded-menu' : '.mobile-menu'; elements = elements.filter(function (element) { return null !== element.closest(menuType) && null !== element.offsetParent; }); elements.unshift(_doc.querySelector('.close-nav-toggle')); bottomMenu = _doc.querySelector('.menu-bottom > nav'); if (bottomMenu) { bottomMenu.querySelectorAll(selectors).forEach(function (element) { elements.push(element); }); } } lastEl = elements[elements.length - 1]; firstEl = elements[0]; activeEl = _doc.activeElement; tabKey = event.keyCode === 9; shiftKey = event.shiftKey; if (!shiftKey && tabKey && lastEl === activeEl) { event.preventDefault(); firstEl.focus(); } if (shiftKey && tabKey && firstEl === activeEl) { event.preventDefault(); lastEl.focus(); } } }); } }; // newsarc.modalMenu /* ----------------------------------------------------------------------------------------------- Primary Menu --------------------------------------------------------------------------------------------------- */ newsarc.primaryMenu = { init: function () { this.focusMenuWithChildren(); }, // The focusMenuWithChildren() function implements Keyboard Navigation in the Primary Menu // by adding the '.focus' class to all 'li.menu-item-has-children' when the focus is on the 'a' element. focusMenuWithChildren: function () { // Get all the link elements within the primary menu. var links, i, len, menu = document.querySelector('.primary-menu-wrapper'); if (!menu) { return false; } links = menu.getElementsByTagName('a'); // Each time a menu link is focused or blurred, toggle focus. for (i = 0, len = links.length; i < len; i++) { links[i].addEventListener('focus', toggleFocus, true); links[i].addEventListener('blur', toggleFocus, true); } //Sets or removes the .focus class on an element. function toggleFocus() { var self = this; // Move up through the ancestors of the current link until we hit .primary-menu. while (-1 === self.className.indexOf('primary-menu')) { // On li elements toggle the class .focus. if ('li' === self.tagName.toLowerCase()) { if (-1 !== self.className.indexOf('focus')) { self.className = self.className.replace(' focus', ''); } else { self.className += ' focus'; } } self = self.parentElement; } } } }; // newsarc.primaryMenu /* ----------------------------------------------------------------------------------------------- Toggles --------------------------------------------------------------------------------------------------- */ newsarc.toggles = { clickedEl: false, init: function () { // Do the toggle. this.toggle(); // Check for toggle/untoggle on resize. this.resizeCheck(); // Check for untoggle on escape key press. this.untoggleOnEscapeKeyPress(); }, performToggle: function (element, instantly) { var target, timeOutTime, classToToggle, self = this, _doc = document, // Get our targets. toggle = element, targetString = toggle.dataset.toggleTarget, activeClass = 'active'; // Elements to focus after modals are closed. if (!_doc.querySelectorAll('.show-modal').length) { self.clickedEl = _doc.activeElement; } if (targetString === 'next') { target = toggle.nextSibling; } else { target = _doc.querySelector(targetString); } // Trigger events on the toggle targets before they are toggled. if (target.classList.contains(activeClass)) { target.dispatchEvent(newsarc.createEvent('toggle-target-before-active')); } else { target.dispatchEvent(newsarc.createEvent('toggle-target-before-inactive')); } // Get the class to toggle, if specified. classToToggle = toggle.dataset.classToToggle ? toggle.dataset.classToToggle : activeClass; // For cover modals, set a short timeout duration so the class animations have time to play out. timeOutTime = 0; if (target.classList.contains('cover-modal')) { timeOutTime = 10; } setTimeout(function () { var focusElement, subMenued = target.classList.contains('sub-menu'), newTarget = subMenued ? toggle.closest('.menu-item').querySelector('.sub-menu') : target, duration = toggle.dataset.toggleDuration; // Toggle the target of the clicked toggle. if (toggle.dataset.toggleType === 'slidetoggle' && !instantly && duration !== '0') { newsarcMenuToggle(newTarget, duration); } else { newTarget.classList.toggle(classToToggle); } // If the toggle target is 'next', only give the clicked toggle the active class. if (targetString === 'next') { toggle.classList.toggle(activeClass); } else if (target.classList.contains('sub-menu')) { toggle.classList.toggle(activeClass); } else { // If not, toggle all toggles with this toggle target. _doc.querySelector('*[data-toggle-target="' + targetString + '"]').classList.toggle(activeClass); } // Toggle aria-expanded on the toggle. newsarcToggleAttribute(toggle, 'aria-expanded', 'true', 'false'); if (self.clickedEl && -1 !== toggle.getAttribute('class').indexOf('close-')) { newsarcToggleAttribute(self.clickedEl, 'aria-expanded', 'true', 'false'); } // Toggle body class. if (toggle.dataset.toggleBodyClass) { _doc.body.classList.toggle(toggle.dataset.toggleBodyClass); } // Check whether to set focus. if (toggle.dataset.setFocus) { focusElement = _doc.querySelector(toggle.dataset.setFocus); if (focusElement) { if (target.classList.contains(activeClass)) { focusElement.focus(); } else { focusElement.blur(); } } } // Trigger the toggled event on the toggle target. target.dispatchEvent(newsarc.createEvent('toggled')); // Trigger events on the toggle targets after they are toggled. if (target.classList.contains(activeClass)) { target.dispatchEvent(newsarc.createEvent('toggle-target-after-active')); } else { target.dispatchEvent(newsarc.createEvent('toggle-target-after-inactive')); } }, timeOutTime); }, // Do the toggle. toggle: function () { var self = this; document.querySelectorAll('*[data-toggle-target]').forEach(function (element) { element.addEventListener('click', function (event) { event.preventDefault(); self.performToggle(element); }); }); }, // Check for toggle/untoggle on screen resize. resizeCheck: function () { if (document.querySelectorAll('*[data-untoggle-above], *[data-untoggle-below], *[data-toggle-above], *[data-toggle-below]').length) { window.addEventListener('resize', function () { var winWidth = window.innerWidth, toggles = document.querySelectorAll('.toggle'); toggles.forEach(function (toggle) { var unToggleAbove = toggle.dataset.untoggleAbove, unToggleBelow = toggle.dataset.untoggleBelow, toggleAbove = toggle.dataset.toggleAbove, toggleBelow = toggle.dataset.toggleBelow; // If no width comparison is set, continue. if (!unToggleAbove && !unToggleBelow && !toggleAbove && !toggleBelow) { return; } // If the toggle width comparison is true, toggle the toggle. if ((((unToggleAbove && winWidth > unToggleAbove) || (unToggleBelow && winWidth < unToggleBelow)) && toggle.classList.contains('active')) || (((toggleAbove && winWidth > toggleAbove) || (toggleBelow && winWidth < toggleBelow)) && !toggle.classList.contains('active'))) { toggle.click(); } }); }); } }, // Close toggle on escape key press. untoggleOnEscapeKeyPress: function () { document.addEventListener('keyup', function (event) { if (event.key === 'Escape') { document.querySelectorAll('*[data-untoggle-on-escape].active').forEach(function (element) { if (element.classList.contains('active')) { element.click(); } }); } }); } }; // newsarc.toggles /** * Is the DOM ready? * * This implementation is coming from https://gomakethings.com/a-native-javascript-equivalent-of-jquerys-ready-method/ * * @since NewsArc 1.0.0 * * @param {Function} fn Callback function to run. */ function newsarcDomReady(fn) { if (typeof fn !== 'function') { return; } if (document.readyState === 'interactive' || document.readyState === 'complete') { return fn(); } document.addEventListener('DOMContentLoaded', fn, false); } newsarcDomReady(function () { newsarc.toggles.init(); // Handle toggles. newsarc.coverModals.init(); // Handle cover modals. newsarc.modalMenu.init(); // Modal Menu. newsarc.primaryMenu.init(); // Primary Menu. newsarc.touchEnabled.init(); // Add class to body if device is touch-enabled. }); /* ----------------------------------------------------------------------------------------------- Helper functions --------------------------------------------------------------------------------------------------- */ /* Toggle an attribute ----------------------- */ function newsarcToggleAttribute(element, attribute, trueVal, falseVal) { var toggles; if (!element.hasAttribute(attribute)) { return; } if (trueVal === undefined) { trueVal = true; } if (falseVal === undefined) { falseVal = false; } /* * Take into account multiple toggle elements that need their state to be * synced. For example: the Search toggle buttons for desktop and mobile. */ toggles = document.querySelectorAll('[data-toggle-target="' + element.dataset.toggleTarget + '"]'); toggles.forEach(function (toggle) { if (!toggle.hasAttribute(attribute)) { return; } if (toggle.getAttribute(attribute) !== trueVal) { toggle.setAttribute(attribute, trueVal); } else { toggle.setAttribute(attribute, falseVal); } }); } /** * Toggle a menu item on or off. * * @since NewsArc 1.0.0 * * @param {HTMLElement} target * @param {number} duration */ function newsarcMenuToggle(target, duration) { var initialParentHeight, finalParentHeight, menu, menuItems, transitionListener, initialPositions = [], finalPositions = []; if (!target) { return; } menu = target.closest('.menu-wrapper'); // Step 1: look at the initial positions of every menu item. menuItems = menu.querySelectorAll('.menu-item'); menuItems.forEach(function (menuItem, index) { initialPositions[index] = {x: menuItem.offsetLeft, y: menuItem.offsetTop}; }); initialParentHeight = target.parentElement.offsetHeight; target.classList.add('toggling-target'); // Step 2: toggle target menu item and look at the final positions of every menu item. target.classList.toggle('active'); menuItems.forEach(function (menuItem, index) { finalPositions[index] = {x: menuItem.offsetLeft, y: menuItem.offsetTop}; }); finalParentHeight = target.parentElement.offsetHeight; // Step 3: close target menu item again. // The whole process happens without giving the browser a chance to render, so it's invisible. target.classList.toggle('active'); /* * Step 4: prepare animation. * Position all the items with absolute offsets, at the same starting position. * Shouldn't result in any visual changes if done right. */ menu.classList.add('is-toggling'); target.classList.toggle('active'); menuItems.forEach(function (menuItem, index) { var initialPosition = initialPositions[index]; if (initialPosition.y === 0 && menuItem.parentElement === target) { initialPosition.y = initialParentHeight; } menuItem.style.transform = 'translate(' + initialPosition.x + 'px, ' + initialPosition.y + 'px)'; }); /* * The double rAF is unfortunately needed, since we're toggling CSS classes, and * the only way to ensure layout completion here across browsers is to wait twice. * This just delays the start of the animation by 2 frames and is thus not an issue. */ requestAnimationFrame(function () { requestAnimationFrame(function () { /* * Step 5: start animation by moving everything to final position. * All the layout work has already happened, while we were preparing for the animation. * The animation now runs entirely in CSS, using cheap CSS properties (opacity and transform) * that don't trigger the layout or paint stages. */ menu.classList.add('is-animating'); menuItems.forEach(function (menuItem, index) { var finalPosition = finalPositions[index]; if (finalPosition.y === 0 && menuItem.parentElement === target) { finalPosition.y = finalParentHeight; } if (duration !== undefined) { menuItem.style.transitionDuration = duration + 'ms'; } menuItem.style.transform = 'translate(' + finalPosition.x + 'px, ' + finalPosition.y + 'px)'; }); if (duration !== undefined) { target.style.transitionDuration = duration + 'ms'; } }); // Step 6: finish toggling. // Remove all transient classes when the animation ends. transitionListener = function () { menu.classList.remove('is-animating'); menu.classList.remove('is-toggling'); target.classList.remove('toggling-target'); menuItems.forEach(function (menuItem) { menuItem.style.transform = ''; menuItem.style.transitionDuration = ''; }); target.style.transitionDuration = ''; target.removeEventListener('transitionend', transitionListener); }; target.addEventListener('transitionend', transitionListener); }); } /** * Traverses the DOM up to find elements matching the query. * * @since NewsArc 1.0.0 * * @param {HTMLElement} target * @param {string} query * @return {NodeList} parents matching query */ function newsarcFindParents(target, query) { var parents = []; // Recursively go up the DOM adding matches to the parents array. function traverse(item) { var parent = item.parentNode; if (parent instanceof HTMLElement) { if (parent.matches(query)) { parents.push(parent); } traverse(parent); } } traverse(target); return parents; }