/** * @file * Attaches behaviors for the Clientside Validation jQuery module. */ (function ($, Drupal, once, drupalSettings) { 'use strict'; if (typeof drupalSettings.cvJqueryValidateOptions === 'undefined') { drupalSettings.cvJqueryValidateOptions = {}; } if (drupalSettings.clientside_validation_jquery.force_validate_on_blur) { drupalSettings.cvJqueryValidateOptions.onfocusout = function (element) { // "eager" validation this.element(element); }; } // Add messages with translations from backend. $.extend($.validator.messages, drupalSettings.clientside_validation_jquery.messages); /** * Attaches jQuery validate behavior to forms. * * @type {Drupal~behavior} * * @prop {Drupal~behaviorAttach} attach * Attaches the outline behavior to the right context. */ Drupal.behaviors.cvJqueryValidate = { attach: function (context) { if (typeof Drupal.Ajax !== 'undefined') { // Update Drupal.Ajax.prototype.beforeSend only once. if (typeof Drupal.Ajax.prototype.beforeSubmitCVOriginal === 'undefined') { var validateAll = 2; try { validateAll = drupalSettings.clientside_validation_jquery.validate_all_ajax_forms; } catch(e) { // Do nothing if we do not have settings or value in settings. } Drupal.Ajax.prototype.beforeSubmitCVOriginal = Drupal.Ajax.prototype.beforeSubmit; Drupal.Ajax.prototype.beforeSubmit = function (form_values, element_settings, options) { if (typeof this.$form !== 'undefined' && (validateAll === 1 || $(this.element).hasClass('cv-validate-before-ajax')) && $(this.element).attr("formnovalidate") == undefined) { $(this.$form).removeClass('ajax-submit-prevented'); $(this.$form).validate(); if (!($(this.$form).valid())) { this.ajaxing = false; $(this.$form).addClass('ajax-submit-prevented'); return false; } } return this.beforeSubmitCVOriginal.apply(this, arguments); }; } } // Allow all modules to update the validate options. // Example of how to do this is shown below. $(document).trigger('cv-jquery-validate-options-update', drupalSettings.cvJqueryValidateOptions); // Process for all the forms on the page everytime, // we already use once so we should be good. once('cvJqueryValidate', 'body form').forEach(function(element) { $(element).validate(drupalSettings.cvJqueryValidateOptions); }); } }; })(jQuery, Drupal, once, drupalSettings); ; /** * @file * Attaches behaviors for the Clientside Validation jQuery module. */ (function ($, once) { // Override clientside validation jquery validation options. // We do this to display the error markup same as in inline_form_errors. // Using once can not use `window` or `document` directly. if (!once('cvjquery', 'html').length) { // Early return avoid changing the indentation // for the rest of the code. return; } $(document).on('cv-jquery-validate-options-update', function (event, options) { options.errorElement = 'strong'; options.showErrors = function(errorMap, errorList) { // First remove all errors. for (var i in errorList) { $(errorList[i].element).parent().find('.form-item--error-message').remove(); } // Show errors using defaultShowErrors(). this.defaultShowErrors(); // Wrap all errors with div.form-item--error-message. $(this.currentForm).find('strong.error').each(function () { if (!$(this).parent().hasClass('form-item--error-message')) { $(this).wrap('
'); } }); }; }); })(jQuery, once); ; /** * @file * Wide viewport search bar interactions. */ ((Drupal) => { const searchWideButtonSelector = '[data-drupal-selector="block-search-wide-button"]'; const searchWideButton = document.querySelector(searchWideButtonSelector); const searchWideWrapperSelector = '[data-drupal-selector="block-search-wide-wrapper"]'; const searchWideWrapper = document.querySelector(searchWideWrapperSelector); /** * Determine if search is visible. * * @return {boolean} * True if the search wrapper contains "is-active" class, false if not. */ function searchIsVisible() { return searchWideWrapper.classList.contains('is-active'); } Drupal.olivero.searchIsVisible = searchIsVisible; /** * Closes search bar when a click event does not happen at an (x,y) coordinate * that does not overlap with either the search wrapper or button. * * @see https://bugs.webkit.org/show_bug.cgi?id=229895 * * @param {Event} e click event */ function watchForClickOut(e) { const clickInSearchArea = e.target.matches(` ${searchWideWrapperSelector}, ${searchWideWrapperSelector} *, ${searchWideButtonSelector}, ${searchWideButtonSelector} * `); if (!clickInSearchArea && searchIsVisible()) { // eslint-disable-next-line no-use-before-define toggleSearchVisibility(false); } } /** * Closes search bar when focus moves to another target. * Avoids closing search bar if event does not have related target - required for Safari. * * @see https://bugs.webkit.org/show_bug.cgi?id=229895 * * @param {Event} e focusout event */ function watchForFocusOut(e) { if (e.relatedTarget) { const inSearchBar = e.relatedTarget.matches( `${searchWideWrapperSelector}, ${searchWideWrapperSelector} *`, ); const inSearchButton = e.relatedTarget.matches( `${searchWideButtonSelector}, ${searchWideButtonSelector} *`, ); if (!inSearchBar && !inSearchButton) { // eslint-disable-next-line no-use-before-define toggleSearchVisibility(false); } } } /** * Closes search bar on escape keyup, if open. * * @param {Event} e keyup event */ function watchForEscapeOut(e) { if (e.key === 'Escape') { // eslint-disable-next-line no-use-before-define toggleSearchVisibility(false); } } /** * Set focus for the search input element. */ function handleFocus() { if (searchIsVisible()) { searchWideWrapper.querySelector('input[type="search"]').focus(); } else if (searchWideWrapper.contains(document.activeElement)) { // Return focus to button only if focus was inside of the search wrapper. searchWideButton.focus(); } } /** * Toggle search functionality visibility. * * @param {boolean} visibility * True if we want to show the form, false if we want to hide it. */ function toggleSearchVisibility(visibility) { searchWideButton.setAttribute('aria-expanded', visibility === true); searchWideWrapper.addEventListener('transitionend', handleFocus, { once: true, }); if (visibility === true) { Drupal.olivero.closeAllSubNav(); searchWideWrapper.classList.add('is-active'); document.addEventListener('click', watchForClickOut, { capture: true }); document.addEventListener('focusout', watchForFocusOut, { capture: true, }); document.addEventListener('keyup', watchForEscapeOut, { capture: true }); } else { searchWideWrapper.classList.remove('is-active'); document.removeEventListener('click', watchForClickOut, { capture: true, }); document.removeEventListener('focusout', watchForFocusOut, { capture: true, }); document.removeEventListener('keyup', watchForEscapeOut, { capture: true, }); } } Drupal.olivero.toggleSearchVisibility = toggleSearchVisibility; /** * Initializes the search wide button. * * @type {Drupal~behavior} * * @prop {Drupal~behaviorAttach} attach * Adds aria-expanded attribute to the search wide button. */ Drupal.behaviors.searchWide = { attach(context) { const searchWideButtonEl = once( 'search-wide', searchWideButtonSelector, context, ).shift(); if (searchWideButtonEl) { searchWideButtonEl.setAttribute('aria-expanded', searchIsVisible()); searchWideButtonEl.addEventListener('click', () => { toggleSearchVisibility(!searchIsVisible()); }); } }, }; })(Drupal); ; /** * @file * Customization of navigation. */ ((Drupal, once, tabbable) => { /** * Checks if navWrapper contains "is-active" class. * * @param {Element} navWrapper * Header navigation. * * @return {boolean} * True if navWrapper contains "is-active" class, false if not. */ function isNavOpen(navWrapper) { return navWrapper.classList.contains('is-active'); } /** * Opens or closes the header navigation. * * @param {object} props * Navigation props. * @param {boolean} state * State which to transition the header navigation menu into. */ function toggleNav(props, state) { const value = !!state; props.navButton.setAttribute('aria-expanded', value); if (value) { props.body.classList.add('is-overlay-active'); props.body.classList.add('is-fixed'); props.navWrapper.classList.add('is-active'); } else { props.body.classList.remove('is-overlay-active'); props.body.classList.remove('is-fixed'); props.navWrapper.classList.remove('is-active'); } } /** * Initialize the header navigation. * * @param {object} props * Navigation props. */ function init(props) { props.navButton.setAttribute('aria-controls', props.navWrapperId); props.navButton.setAttribute('aria-expanded', 'false'); props.navButton.addEventListener('click', () => { toggleNav(props, !isNavOpen(props.navWrapper)); }); // Close any open sub-navigation first, then close the header navigation. document.addEventListener('keyup', (e) => { if (e.key === 'Escape') { if (props.olivero.areAnySubNavsOpen()) { props.olivero.closeAllSubNav(); } else { toggleNav(props, false); } } }); props.overlay.addEventListener('click', () => { toggleNav(props, false); }); props.overlay.addEventListener('touchstart', () => { toggleNav(props, false); }); // Focus trap. This is added to the header element because the navButton // element is not a child element of the navWrapper element, and the keydown // event would not fire if focus is on the navButton element. props.header.addEventListener('keydown', (e) => { if (e.key === 'Tab' && isNavOpen(props.navWrapper)) { const tabbableNavElements = tabbable.tabbable(props.navWrapper); tabbableNavElements.unshift(props.navButton); const firstTabbableEl = tabbableNavElements[0]; const lastTabbableEl = tabbableNavElements[tabbableNavElements.length - 1]; if (e.shiftKey) { if ( document.activeElement === firstTabbableEl && !props.olivero.isDesktopNav() ) { lastTabbableEl.focus(); e.preventDefault(); } } else if ( document.activeElement === lastTabbableEl && !props.olivero.isDesktopNav() ) { firstTabbableEl.focus(); e.preventDefault(); } } }); // Remove overlays when browser is resized and desktop nav appears. window.addEventListener('resize', () => { if (props.olivero.isDesktopNav()) { toggleNav(props, false); props.body.classList.remove('is-overlay-active'); props.body.classList.remove('is-fixed'); } // Ensure that all sub-navigation menus close when the browser is resized. Drupal.olivero.closeAllSubNav(); }); // If hyperlink links to an anchor in the current page, close the // mobile menu after the click. props.navWrapper.addEventListener('click', (e) => { if ( e.target.matches( `[href*="${window.location.pathname}#"], [href*="${window.location.pathname}#"] *, [href^="#"], [href^="#"] *`, ) ) { toggleNav(props, false); } }); } /** * Initialize the navigation. * * @type {Drupal~behavior} * * @prop {Drupal~behaviorAttach} attach * Attach context and settings for navigation. */ Drupal.behaviors.oliveroNavigation = { attach(context) { const headerId = 'header'; const header = once('navigation', `#${headerId}`, context).shift(); const navWrapperId = 'header-nav'; if (header) { const navWrapper = header.querySelector(`#${navWrapperId}`); const { olivero } = Drupal; const navButton = context.querySelector( '[data-drupal-selector="mobile-nav-button"]', ); const body = document.body; const overlay = context.querySelector( '[data-drupal-selector="header-nav-overlay"]', ); init({ olivero, header, navWrapperId, navWrapper, navButton, body, overlay, }); } }, }; })(Drupal, once, tabbable); ; /** * @file * Provides functionality for second level submenu navigation. */ ((Drupal) => { const { isDesktopNav } = Drupal.olivero; const secondLevelNavMenus = document.querySelectorAll( '[data-drupal-selector="primary-nav-menu-item-has-children"]', ); /** * Shows and hides the specified menu item's second level submenu. * * @param {Element} topLevelMenuItem * The