diff --git a/javascript/eu_cookie_compliance.decide_later.js b/javascript/eu_cookie_compliance.decide_later.js index 442ee34..d0e0e27 100644 --- a/javascript/eu_cookie_compliance.decide_later.js +++ b/javascript/eu_cookie_compliance.decide_later.js @@ -1,5 +1,5 @@ // ----------------------------------------------------------------------------- -// Omnipedia - Site theme - EU cookie compliance decide later button +// Omnipedia - Site theme - EU Cookie Compliance decide later button component // ----------------------------------------------------------------------------- // This adds an event handler to close the pop-up when clicking the "Decide @@ -9,12 +9,12 @@ AmbientImpact.onGlobals([ 'Drupal.eu_cookie_compliance', ], function() { AmbientImpact.on([ - 'OmnipediaSiteThemeEuCookieComplianceElements', -], function(euCookieComplianceElements) { + 'OmnipediaSiteThemeEuCookieCompliance', +], function(euCookieCompliance) { AmbientImpact.addComponent( 'OmnipediaSiteThemeEuCookieComplianceDecideLater', function( - euCookieComplianceDecideLater, $ + euCookieComplianceDecideLater, $, ) { 'use strict'; @@ -36,44 +36,35 @@ function( this.addBehaviour( 'OmnipediaSiteThemeEuCookieComplianceDecideLater', 'omnipedia-site-theme-eu-cookie-compliance-decide-later', - euCookieComplianceElements.getBehaviourSelector(), + '#sliding-popup', function(context, settings) { - /** - * The cookie compliance pop-up, if any, wrapped in a jQuery collection. - * - * @type {jQuery} - */ - let $popUp = euCookieComplianceElements.getPopUp(); + $(this).on( + `PrivacyPopupConstructed.${eventNamespace}`, + function(event, popup) { - // Bail if we can't find the pop-up. - if ($popUp.length === 0) { - return; - } + popup.$popup.find(`.${decideLaterButtonClass}`).on( + `click.${eventNamespace}`, + Drupal.eu_cookie_compliance.toggleWithdrawBanner, + ); - // Event handler to close the pop-up when the "Decide later" button is - // clicked. - $popUp.find('.' + decideLaterButtonClass).on( - 'click.' + eventNamespace, - Drupal.eu_cookie_compliance.toggleWithdrawBanner - ); + // Attach binds a one-off handler to the PrivacyPopupDestroyed event which + // removes the click event handler. + }).one(`PrivacyPopupDestroyed.${eventNamespace}`, function(event, popup) { - }, - function(context, settings, trigger) { + popup.$popup.find(`.${decideLaterButtonClass}`).off( + `click.${eventNamespace}`, + Drupal.eu_cookie_compliance.toggleWithdrawBanner, + ); - /** - * The cookie compliance pop-up, if any, wrapped in a jQuery collection. - * - * @type {jQuery} - */ - let $popUp = euCookieComplianceElements.getPopUp(); + }); - // Bail if we can't find the pop-up. - if ($popUp.length === 0) { - return; - } + }, + function(context, settings, trigger) { - $popUp.find('.' + decideLaterButtonClass).off('click.' + eventNamespace); + // Remove just the constructed event as the destroyed event needs to run + // and will be invoked and then removed as it's a one-off event. + $(this).off(`PrivacyPopupConstructed.${eventNamespace}`); } ); diff --git a/javascript/eu_cookie_compliance.elements.js b/javascript/eu_cookie_compliance.elements.js deleted file mode 100644 index 9cfd630..0000000 --- a/javascript/eu_cookie_compliance.elements.js +++ /dev/null @@ -1,126 +0,0 @@ -// ----------------------------------------------------------------------------- -// Omnipedia - Site theme - EU cookie compliance elements -// ----------------------------------------------------------------------------- - -// In addition to providing a centralized API for getting the various pop-up -// elements, this does the following: -// -// - Adds a 'eu-cookie-compliance-popup' to the pop-up. -// -// - Adds a BEM modifier class to the pop-up if the in-page toggle is found, so -// that the persistent toggle button can be hidden. This is preferred as it -// can get in the way on smaller screens. - -AmbientImpact.onGlobals([ - 'drupalSettings.eu_cookie_compliance', -], function() { -AmbientImpact.on([ - 'OmnipediaPrivacySettings', -], function(OmnipediaPrivacySettings) { -AmbientImpact.addComponent( - 'OmnipediaSiteThemeEuCookieComplianceElements', -function( - euCookieComplianceElements, $ -) { - - 'use strict'; - - /** - * The pop-up element, if any, wrapped in a jQuery collection. - * - * @type {jQuery} - */ - let $popUp = $(); - - /** - * The containing element, if any, wrapped in a jQuery collection. - * - * The containing element is configured in the EU Cookie Compliance module's - * settings, and is the element by default. - * - * @type {jQuery} - */ - let $containingElement = $(); - - /** - * Get the pop-up element jQuery collection. - * - * @return {jQuery} - */ - this.getPopUp = function() { - return $popUp; - }; - - /** - * Get the containing element jQuery collection. - * - * @return {jQuery} - */ - this.getContainingElement = function() { - return $containingElement; - }; - - /** - * Get the selector to attach behaviours to. - * - * @return {String} - */ - this.getBehaviourSelector = function() { - return '#sliding-popup'; - }; - - /** - * The base BEM class for the pop-up. - * - * @return {String} - */ - this.getPopUpBaseClass = function() { - return 'eu-cookie-compliance-popup'; - }; - - /** - * Get the BEM class for when the pop-up is associated with an in-page toggle. - * - * @type {String} - */ - this.getPopUpHasInPageToggleClass = function() { - return this.getPopUpBaseClass() + '--has-in-page-toggle'; - }; - - this.addBehaviour( - 'OmnipediaSiteThemeEuCookieComplianceElements', - 'omnipedia-site-theme-eu-cookie-compliance-elements', - this.getBehaviourSelector(), - function(context, settings) { - - $popUp = $(this); - - $popUp.addClass(euCookieComplianceElements.getPopUpBaseClass()); - - if (OmnipediaPrivacySettings.getToggle().length > 0) { - $popUp.addClass( - euCookieComplianceElements.getPopUpHasInPageToggleClass() - ); - } - - $containingElement = $( - drupalSettings.eu_cookie_compliance.containing_element - ); - - }, - function(context, settings, trigger) { - - $popUp.removeClass( - euCookieComplianceElements.getPopUpHasInPageToggleClass() - ); - - $popUp = $(); - - $containingElement = $(); - - } - ); - -}); -}); -}); diff --git a/javascript/eu_cookie_compliance.focus.js b/javascript/eu_cookie_compliance.focus.js index f55c62c..90fcca1 100644 --- a/javascript/eu_cookie_compliance.focus.js +++ b/javascript/eu_cookie_compliance.focus.js @@ -1,14 +1,12 @@ // ----------------------------------------------------------------------------- -// Omnipedia - Site theme - EU cookie compliance focus management +// Omnipedia - Site theme - EU Cookie Compliance focus management component // ----------------------------------------------------------------------------- -// This manages focus on the following events: -// -// - 'euCookieCompliancePopUpOpened': moves focus to the first tabbable element -// in the pop-up. -// -// - 'euCookieCompliancePopUpClosed': moves focus to the privacy settings -// toggle. +// This moves focus to the first tabble element inside the pop-up when opened +// and moves focus back to the privacy toggle when the pop-up is closed. It also +// locks the pointer focus source during pop-up opening and closing so that it +// remains set to keyboard or pointer, depending on which was used, and doesn't +// change to script focus source. AmbientImpact.onGlobals([ 'ally.query.tabbable', @@ -17,20 +15,16 @@ AmbientImpact.onGlobals([ AmbientImpact.on([ 'pointerFocusHide', 'OmnipediaPrivacySettings', - 'OmnipediaSiteThemeEuCookieComplianceElements', - 'OmnipediaSiteThemeEuCookieComplianceState', - 'OmnipediaSiteThemeSidebarsState', + 'OmnipediaSiteThemeEuCookieCompliance', ], function( aiPointerFocusHide, OmnipediaPrivacySettings, - euCookieComplianceElements, - euCookieComplianceState, - sidebarsState + euCookieCompliance, ) { AmbientImpact.addComponent( 'OmnipediaSiteThemeEuCookieComplianceFocus', function( - euCookieComplianceFocus, $ + euCookieComplianceFocus, $, ) { 'use strict'; @@ -43,108 +37,187 @@ function( const eventNamespace = this.getName(); /** - * The ally.js focus source global service object. - * - * This initializes ally.js' focus source service, which starts watching the - * document and applies the data-focus-source attribute to the element. - * - * @type {Object} - * - * @see https://allyjs.io/api/style/focus-source.html - * ally.js documentation. + * Represents privacy pop-up focus management. */ - let focusSourceHandle = ally.style.focusSource(); + class PrivacyPopupFocus { - this.addBehaviour( - 'OmnipediaSiteThemeEuCookieComplianceFocus', - 'omnipedia-site-theme-eu-cookie-compliance-focus', - euCookieComplianceElements.getBehaviourSelector(), - function(context, settings) { + /** + * ally.js focus source global service object. + * + * @type {Object} + * + * @see https://allyjs.io/api/style/focus-source.html + */ + #focusSourceHandle; + + /** + * The PrivacyPopup instance we're providing an overlay for. + * + * @type {PrivacyPopup} + */ + #popup; + + /** + * The pop-up element wrapped in a jQuery collection. + * + * @type {jQuery} + */ + #$popup; + + #sidebarsOffcanvasOpened = false; + + /** + * Constructor. + * + * @param {PrivacyPopup} popup + * A PrivacyPopup instance. + */ + constructor(popup) { + + this.#popup = popup; + + this.#$popup = popup.$popup; + + this.#focusSourceHandle = ally.style.focusSource(); + + this.#bindEventHandlers(); + + } + + /** + * Destroy this instance. + * + * @return {Promise} + * A Promise that resolves when various DOM tasks are complete. + */ + destroy() { + + this.#focusSourceHandle.disengage(); + + this.#unbindEventHandlers(); + + return Promise.resolve(); + + } + + /** + * Bind all of our event handlers. + * + * @see this~#unbindEventHandlers() + */ + #bindEventHandlers() { /** - * The cookie compliance pop-up, if any, wrapped in a jQuery collection. + * Reference to the current instance. * - * @type {jQuery} + * @type {PrivacyPopupFocus} */ - let $popUp = euCookieComplianceElements.getPopUp(); + const that = this; - // Bail if we can't find the pop-up. - if ($popUp.length === 0) { - return; - } - - $popUp.on( - 'euCookieCompliancePopUpOpened.' + eventNamespace, - function(event) { + this.#$popup.on(`PrivacyPopupBeforeOpen.${eventNamespace}`, function( + event, popup, + ) { aiPointerFocusHide.lock(); + }).on(`PrivacyPopupOpened.${eventNamespace}`, function(event, popup) { + // We have to find the first tabbable element in the pop-up because the // pop-up itself does not seem to be focusable for some reason. $(ally.query.tabbable({ - context: $popUp, - includeContext: true + context: that.#$popup, + includeContext: true, })).first().focus(); aiPointerFocusHide.unlock(); - }) - .on( - 'euCookieCompliancePopUpClose.' + eventNamespace, - function(event) { + }).on(`PrivacyPopupBeforeClose.${eventNamespace}`, function( + event, popup, + ) { aiPointerFocusHide.lock(); - }) - .on( - 'euCookieCompliancePopUpClosed.' + eventNamespace, - function(event) { - - // Only focus the toggle if the focus source was not the pointer, or if - // sidebars are not off-canvas. This is to prevent the sidebar menu - // unexpectedly opening if the pop-up was open due to some other reason, - // and not because it was invoked via the toggle in the sidebar. The - // most likely reason for this would be when the user has not yet agreed - // to anything in the pop-up, either on first navigating to the site or - // because they clicked "Decide later" on a previous page. - // - // @todo We need a better way of handling this that allows focus to be - // moved into the sidebar for the purpose of maintaining tabbing order - // but without causing the sidebar menu to open. One way to do this - // could be to alter the CSS to only show on :focus-within if - // html:not([data-focus-source="pointer"]) - if ( - focusSourceHandle.current() !== 'pointer' || - !sidebarsState.isOffCanvas() - ) { + }).on(`PrivacyPopupClosed.${eventNamespace}`, function(event, popup) { + + // Only focus the toggle if it looks like the sidebars were opened and + // off-canvas. This is to prevent the pop-up opening the sidebars if + // the pop-up was not opened via the toggle in the sidebars, such as on + // a page load where the user has not yet agreed and the pop-up shows + // itself on page load. + if (that.#sidebarsOffcanvasOpened === true) { OmnipediaPrivacySettings.getToggle().focus(); } aiPointerFocusHide.unlock(); + // Bind a one-off handler to the PrivacyPopupDestroyed event which + // disengages and removes the focus source handle. + }).one(`PrivacyPopupDestroyed.${eventNamespace}`, function(event, popup) { + + that.destroy(); + }); - }, - function(context, settings, trigger) { + $(document).on(`omnipediaSidebarsMenuOpen.${eventNamespace}`, function( + event, sidebars, + ) { - /** - * The cookie compliance pop-up, if any, wrapped in a jQuery collection. - * - * @type {jQuery} - */ - let $popUp = euCookieComplianceElements.getPopUp(); + console.debug(sidebars.isOffCanvas()); - // Bail if we can't find the pop-up. - if ($popUp.length === 0) { - return; - } + // that.#sidebarsOffcanvasOpened = sidebars.isOffCanvas(); - $popUp.off([ - 'euCookieCompliancePopUpOpened.' + eventNamespace, - 'euCookieCompliancePopUpClose.' + eventNamespace, - 'euCookieCompliancePopUpClosed.' + eventNamespace, + // @todo This kind of a hack and doesn't give us context as to when the + // sidebars were opened, i.e. were they opened right before opening + // the privacy pop-up or was it unrelated and/or a while ago? + if (sidebars.isOffCanvas() === true) { + that.#sidebarsOffcanvasOpened = true; + } + + }); + + } + + /** + * Unbind all of our event handlers. + * + * @see this~#bindEventHandlers() + */ + #unbindEventHandlers() { + + this.#$popup.off([ + `PrivacyPopupBeforeOpen.${eventNamespace}`, + `PrivacyPopupOpened.${eventNamespace}`, + `PrivacyPopupBeforeClose.${eventNamespace}`, + `PrivacyPopupClosed.${eventNamespace}`, + // Don't remove the PrivacyPopupDestroyed handler as it's a one-off and + // must be triggered to destroy; removing it here will likely prevent it + // triggering. ].join(' ')); + $(document).off(`omnipediaSidebarsMenuOpen.${eventNamespace}`); + + } + + } + + this.addBehaviour( + 'OmnipediaSiteThemeEuCookieComplianceFocus', + 'omnipedia-site-theme-eu-cookie-compliance-focus', + '#sliding-popup', + function(context, settings) { + + $(this).prop('PrivacyPopupFocus', new PrivacyPopupFocus( + $(this).prop('PrivacyPopup'), + )); + + }, + function(context, settings, trigger) { + + // PrivacyPopupFocus destroys itself on the PrivacyPopupDestroyed event so + // we just need to remove the property and let browser garbage collection + // handle the rest. + $(this).removeProp('PrivacyPopupFocus'); + } ); diff --git a/javascript/eu_cookie_compliance.js b/javascript/eu_cookie_compliance.js new file mode 100644 index 0000000..6e005df --- /dev/null +++ b/javascript/eu_cookie_compliance.js @@ -0,0 +1,348 @@ +// ----------------------------------------------------------------------------- +// Omnipedia - Site theme - EU Cookie Compliance component +// ----------------------------------------------------------------------------- + +AmbientImpact.onGlobals([ + 'Drupal.eu_cookie_compliance', + 'drupalSettings.eu_cookie_compliance', +], function() { +AmbientImpact.on([ + 'fastdom', 'OmnipediaPrivacySettings', +], function(aiFastDom, OmnipediaPrivacySettings) { +AmbientImpact.addComponent( + 'OmnipediaSiteThemeEuCookieCompliance', +function(euCookieCompliance, $) { + + 'use strict'; + + /** + * FastDom instance. + * + * @type {FastDom} + */ + const fastdom = aiFastDom.getInstance(); + + /** + * Base BEM class for the pop-up. + * + * @type {String} + */ + const baseClass = 'eu-cookie-compliance-popup'; + + /** + * The BEM class for when the pop-up is associated with an in-page toggle. + * + * @type {String} + */ + const hasInPageToggleClass = `${baseClass}--has-in-page-toggle`; + + /** + * Class applied to the containing element when the pop-up is open. + * + * @type {String} + */ + const containingElementWhenOpenClass = 'eu-cookie-compliance-popup-open'; + + /** + * Event namespace name. + * + * @type {String} + */ + const eventNamespace = this.getName(); + + /** + * The transition duration for the privacy pop-up open and close. + * + * @type {Number} + */ + const transitionDuration = drupalSettings.eu_cookie_compliance.popup_delay; + + /** + * Represents a privacy pop-up. + */ + class PrivacyPopup { + + /** + * The pop-up element wrapped in a jQuery collection. + * + * @type {jQuery} + */ + #$popup; + + /** + * The containing element wrapped in a jQuery collection. + * + * The containing element is configured in the EU Cookie Compliance module's + * settings, and is the element by default. + * + * @type {jQuery} + */ + #$containingElement; + + /** + * Constructor. + * + * @param {jQuery|HTMLElement} $popup + * The pop-up element wrapped in a jQuery collection or as an HTMLElement. + */ + constructor($popup) { + + this.#$popup = $($popup); + + this.#$containingElement = $( + drupalSettings.eu_cookie_compliance.containing_element, + ); + + const beforeConstructEvent = new $.Event('PrivacyPopupBeforeConstruct'); + + this.#$popup.trigger(beforeConstructEvent, [this]); + + /** + * Reference to the current instance. + * + * @type {PrivacyPopup} + */ + const that = this; + + fastdom.mutate(function() { + + that.#$popup.addClass(baseClass); + + if (OmnipediaPrivacySettings.getToggle().length > 0) { + that.#$popup.addClass(hasInPageToggleClass); + } + + // Save the transition duration as a custom property on the pop-up so + // that it can be used in CSS. + that.#$popup.prop('style').setProperty( + '--eu-cookie-compliance-transition-duration', + `${transitionDuration}ms`, + ); + + that.#bindEventHandlers(); + + }).then(function() { + + const constructedEvent = new $.Event('PrivacyPopupConstructed'); + + that.#$popup.trigger(constructedEvent, [that]); + + }); + + } + + /** + * Destroy this instance. + * + * @return {Promise} + * A Promise that resolves when various DOM tasks are complete. + */ + destroy() { + + /** + * Reference to the current instance. + * + * @type {PrivacyPopup} + */ + const that = this; + + const beforeDestroyEvent = new $.Event('PrivacyPopupBeforeDestroy'); + + this.#$popup.trigger(beforeDestroyEvent, [this]); + + this.#unbindEventHandlers(); + + return fastdom.mutate(function() { + + that.#$popup.removeClass([ + baseClass, hasInPageToggleClass, + ]).prop('style').removeProperty( + '--eu-cookie-compliance-transition-duration', + ); + + }).then(function() { + + const destroyedEvent = new $.Event('PrivacyPopupDestroyed'); + + that.#$popup.trigger(destroyedEvent, [that]); + + }); + + } + + /** + * Bind all of our event handlers. + * + * @see this~#unbindEventHandlers() + */ + #bindEventHandlers() { + + /** + * Reference to the current instance. + * + * @type {PrivacyPopup} + */ + const that = this; + + this.#$popup.on( + `eu_cookie_compliance_popup_open.${eventNamespace}`, + function(event) { + + const beforeOpenEvent = new $.Event('PrivacyPopupBeforeOpen'); + + that.#$popup.trigger(beforeOpenEvent, [that]); + + // The module's JavaScript does not provide an event for when the pop-up + // has finished opening, so this does that in a roundabout way. Note + // that since this is using the configured delay value, it waits in + // parallel with the value, but likely will not trigger on the exact + // frame the the pop-up's jQuery animation has actually completed. + setTimeout(function() { + + const openedEvent = new $.Event('PrivacyPopupOpened'); + + that.#$popup.trigger(openedEvent, [that]); + + }, transitionDuration); + + }).on( + `eu_cookie_compliance_popup_close.${eventNamespace}`, + function(event) { + + const beforeCloseEvent = new $.Event('PrivacyPopupBeforeClose'); + + that.#$popup.trigger(beforeCloseEvent, [that]); + + // The module's JavaScript does not provide an event for when the pop-up + // has finished closing, so this does that in a roundabout way. Note + // that since this is using the configured delay value, it waits in + // parallel with the value, but likely will not trigger on the exact + // frame the the pop-up's jQuery animation has actually completed. + setTimeout(function() {fastdom.mutate(function() { + + const currentStatus = Drupal.eu_cookie_compliance.getCurrentStatus(); + + // The module's JavaScript only updates the container (usually ) + // classes when first attached. Unfortunately, this means that CSS + // cannot depend on these updating when a user agrees to some or all + // categories, so we have to do that ourselves. Note that this must + // delay until the pop-up is expected to be closed to avoid causing + // issues with hiding the pop-up smoothly. + that.#$containingElement.removeClass([ + 'eu-cookie-compliance-status-null', + 'eu-cookie-compliance-status-1', + 'eu-cookie-compliance-status-2', + ]).addClass(`eu-cookie-compliance-status-${currentStatus}`); + + }).then(function() { + + const closedEvent = new $.Event('PrivacyPopupClosed'); + + that.#$popup.trigger(closedEvent, [that]); + + })}, transitionDuration); + + fastdom.mutate(function() { + + // The module JavaScript annoyingly does not remove this class when + // the pop-up is closed via clicking the accept buttons so make sure + // this is removed on close. + that.#$containingElement.removeClass(containingElementWhenOpenClass); + + }); + + }).on(`click.${eventNamespace}`, function(event) { + + // Slightly hacky and indirect way of triggering the close event because + // the module's JavaScript annoyingly does not trigger it when closed + // by the accept/ agree buttons. + + if ( + !$(event.target).is( + '.eu-cookie-compliance-save-preferences-button' + ) && + !$(event.target).is('.agree-button') + ) { + return; + } + + that.#$popup.trigger('eu_cookie_compliance_popup_close'); + + }); + + } + + /** + * Unbind all of our event handlers. + * + * @see this~#bindEventHandlers() + */ + #unbindEventHandlers() { + + this.#$popup.off([ + `eu_cookie_compliance_popup_open.${eventNamespace}`, + `eu_cookie_compliance_popup_close.${eventNamespace}`, + `click.${eventNamespace}`, + ].join(' ')); + + } + + /** + * Get the pop-up element jQuery collection. + * + * @return {jQuery} + */ + get $popup() { + return this.#$popup; + } + + /** + * Get the containing element jQuery collection. + * + * @return {jQuery} + */ + get $containingElement() { + return this.#$containingElement; + } + + /** + * Whether the pop-up is currently marked as open. + * + * @return {Boolean} + */ + isOpen() { + return this.#$containingElement.is(`.${containingElementWhenOpenClass}`); + } + + }; + + this.addBehaviour( + 'OmnipediaSiteThemeEuCookieCompliance', + 'omnipedia-site-theme-eu-cookie-compliance', + '#sliding-popup', + function(context, settings) { + + $(this).prop('PrivacyPopup', new PrivacyPopup(this)); + + }, + function(context, settings, trigger) { + + /** + * Reference to the HTML element being detached from. + * + * @type {HTMLElement} + */ + const that = this; + + $(this).prop('PrivacyPopup').destroy().then(function() { + + $(that).removeProp('PrivacyPopup'); + + }); + + } + ); + +}); +}); +}); diff --git a/javascript/eu_cookie_compliance.overlay.js b/javascript/eu_cookie_compliance.overlay.js index c130e29..fb7b09a 100644 --- a/javascript/eu_cookie_compliance.overlay.js +++ b/javascript/eu_cookie_compliance.overlay.js @@ -1,27 +1,22 @@ // ----------------------------------------------------------------------------- -// Omnipedia - Site theme - EU cookie compliance overlay +// Omnipedia - Site theme - EU Cookie Compliance overlay component // ----------------------------------------------------------------------------- -// This marks the pop-up as having opened and closed for the scroll blocker -// component. - AmbientImpact.on([ + 'fastdom', 'OmnipediaPrivacySettings', - 'OmnipediaSiteThemeEuCookieComplianceElements', - 'OmnipediaSiteThemeEuCookieComplianceState', - 'OmnipediaSiteThemeSidebarsState', + 'OmnipediaSiteThemeEuCookieCompliance', 'overlay', ], function( + aiFastDom, OmnipediaPrivacySettings, - euCookieComplianceElements, - euCookieComplianceState, - sidebarsState, - aiOverlay + euCookieCompliance, + aiOverlay, ) { AmbientImpact.addComponent( 'OmnipediaSiteThemeEuCookieComplianceOverlay', function( - euCookieComplianceOverlay, $ + euCookieComplianceOverlay, $, ) { 'use strict'; @@ -33,121 +28,253 @@ function( */ const eventNamespace = this.getName(); - this.addBehaviour( - 'OmnipediaSiteThemeEuCookieComplianceOverlay', - 'omnipedia-site-theme-eu-cookie-compliance-overlay', - euCookieComplianceElements.getBehaviourSelector(), - function(context, settings) { + /** + * FastDom instance. + * + * @type {FastDom} + */ + const fastdom = aiFastDom.getInstance(); + + /** + * Class applied to the overlay element for CSS. + * + * @type {String} + */ + const overlayClass = 'overlay--eu-cookie-compliance-popup'; + + /** + * Represents a privacy pop-up overlay. + */ + class PrivacyPopupOverlay { + + /** + * The overlay instance. + * + * @type {Object} + */ + #overlay; + + /** + * The overlay element wrapped in a jQuery collection. + * + * @type {jQuery} + */ + #$overlay; + + /** + * The PrivacyPopup instance we're providing an overlay for. + * + * @type {PrivacyPopup} + */ + #popup; + + /** + * The pop-up element wrapped in a jQuery collection. + * + * @type {jQuery} + */ + #$popup; + + /** + * Constructor. + * + * @param {PrivacyPopup} popup + * A PrivacyPopup instance. + */ + constructor(popup) { + + this.#popup = popup; + + this.#$popup = popup.$popup; + + this.#$overlay = aiOverlay.create({ + modal: true, + modalFilter: this.#$popup, + }); + + this.#$overlay.addClass(overlayClass); + + this.#overlay = this.#$overlay.prop('aiOverlay'); + + this.#bindEventHandlers(); /** - * The cookie compliance pop-up, if any, wrapped in a jQuery collection. + * Reference to the current instance. * - * @type {jQuery} + * @type {PrivacyPopupOverlay} */ - let $popUp = euCookieComplianceElements.getPopUp(); + const that = this; + + fastdom.mutate(function() { + + that.#$overlay.insertBefore(that.#$popup); + + }).then(function() { + + if (that.#popup.isOpen() === false) { + return; + } + + // If the pop-up is open when we attach, show the overlay. + that.show(); + + }); + + } - // Bail if we can't find the pop-up. - if ($popUp.length === 0) { - return; - } + /** + * Destroy this instance. + * + * @return {Promise} + * A Promise that resolves when various DOM tasks are complete. + */ + destroy() { + + this.#unbindEventHandlers(); /** - * The primary menu region's overlay, wrapped in a jQuery object. + * Reference to the current instance. * - * @type {jQuery} + * @type {PrivacyPopupOverlay} */ - let $overlay = aiOverlay.create({ - modal: true, - modalFilter: $popUp + const that = this; + + return fastdom.mutate(function() { + // This also removes scroll blocking and disengages focus blocking so we + // don't need to call #overlay.hide(). + that.#overlay.destroy(); }); - $popUp.prop('aiOverlay', $overlay.prop('aiOverlay')); + } - $overlay.insertBefore($popUp); + /** + * Bind all of our event handlers. + * + * @see this~#unbindEventHandlers() + */ + #bindEventHandlers() { /** - * Open the overlay and related tasks. + * Reference to the current instance. + * + * @type {PrivacyPopupOverlay} */ - function openOverlay() { - - // Only show the overlay if the sidebars are not off-canvas, as the - // the sidebars create their own overlay and this would result in two - // overlays. - // - // @todo Find a way to show an overlay over everything, including the - // sidebars? - if (!sidebarsState.isOffCanvas() || !sidebarsState.isMenuOpen()) { - $overlay.prop('aiOverlay').show(); - } + const that = this; - // Failsafe to prevent disabling scroll if the pop-up is not visible or - // has been removed for any reason, such as an add-on, e.g. uBlock - // Origin, or some other software. - if ( - $popUp.is(':hidden') || - $popUp.width() < 10 || - $popUp.height() < 10 || - $popUp.css('visibility') === 'hidden' - ) { - return; - } + this.#$popup.on(`PrivacyPopupBeforeOpen.${eventNamespace}`, function( + event, + ) { + + that.show(); + + }).on(`PrivacyPopupBeforeClose.${eventNamespace}`, function(event) { + + that.hide(); + + }).on(`PrivacyPopupClosed.${eventNamespace}`, function(event) { + + that.#$popup.trigger('immerseExit'); + + }).one(`PrivacyPopupDestroyed.${eventNamespace}`, function(event) { + + that.destroy(); - // We trigger immerse events to pause any animations on the page while - // the overlay/pop-up are open for both performance reasons and so as to - // not distract users. - $popUp.trigger('immerseEnter'); - - }; - - // If the pop-up is open when we attach, mark it as such to the overlay - // scroll component. - if (euCookieComplianceState.isPopUpOpen()) { - openOverlay(); - } - - $popUp.on( - 'euCookieCompliancePopUpOpen.' + eventNamespace, - openOverlay - ) - .on( - 'euCookieCompliancePopUpClose.' + eventNamespace, - function(event) { - - $overlay.prop('aiOverlay').hide(); - - }) - .on( - 'euCookieCompliancePopUpClosed.' + eventNamespace, - function(event) { - $popUp.trigger('immerseExit'); }); - }, - function(context, settings, trigger) { + } + + /** + * Unbind all of our event handlers. + * + * @see this~#bindEventHandlers() + */ + #unbindEventHandlers() { + + this.#$popup.off([ + `PrivacyPopupBeforeOpen.${eventNamespace}`, + `PrivacyPopupBeforeClose.${eventNamespace}`, + `PrivacyPopupClosed.${eventNamespace}`, + // Don't unbind the one-off PrivacyPopupDestroyed event handler as that + // auto destroys this instance. + ].join(' ')); + + } + + /** + * Show the overlay and perform related tasks. + * + * @return {Promise} + * A Promise that resolves when various DOM tasks are complete. + */ + show() { + + this.#overlay.show(); /** - * The cookie compliance pop-up, if any, wrapped in a jQuery collection. + * Reference to the current instance. * - * @type {jQuery} + * @type {PrivacyPopupOverlay} */ - let $popUp = euCookieComplianceElements.getPopUp(); + const that = this; - // Bail if we can't find the pop-up. - if ($popUp.length === 0) { - return; - } + // Failsafe to prevent disabling scroll if the pop-up is not visible or + // has been removed for any reason, such as an add-on, e.g. uBlock + // Origin, or some other software. + return fastdom.measure(function() { + return ( + that.#$popup.is(':hidden') || + that.#$popup.width() < 10 || + that.#$popup.height() < 10 || + that.#$popup.css('visibility') === 'hidden' + ); - $popUp.off([ - 'euCookieCompliancePopUpOpen.' + eventNamespace, - 'euCookieCompliancePopUpClose.' + eventNamespace, - 'euCookieCompliancePopUpClosed.' + eventNamespace, - ].join(' ')); + }).then(function(popupBlocked) { + + if (popupBlocked === true) { + return; + } + + that.#$popup.trigger('immerseEnter'); + + }); + + } + + /** + * Hide the overlay and perform related tasks. + * + * @return {Promise} + * A Promise that resolves when various DOM tasks are complete. + */ + hide() { + + this.#overlay.hide(); + + // @todo A real FastDom Promise when the overlay component has been + // refactored to return one. + return Promise.resolve(); + + } + + }; + + this.addBehaviour( + 'OmnipediaSiteThemeEuCookieComplianceOverlay', + 'omnipedia-site-theme-eu-cookie-compliance-overlay', + '#sliding-popup', + function(context, settings) { + + $(this).prop('PrivacyPopupOverlay', new PrivacyPopupOverlay( + $(this).prop('PrivacyPopup'), + )); + + }, + function(context, settings, trigger) { - // Destroy the overlay instance if found. Note that the destroy method - // also deletes the aiOverlay property so we don't have to. - if (!(typeof $popUp.prop('aiOverlay') === 'undefined')) { - $popUp.prop('aiOverlay').destroy(); - } + // PrivacyPopupOverlay destroys itself on the PrivacyPopupDestroyed event + // so we just need to remove the property and let browser garbage + // collection handle the rest. + $(this).removeProp('PrivacyPopupOverlay'); } ); diff --git a/javascript/eu_cookie_compliance.state.js b/javascript/eu_cookie_compliance.state.js deleted file mode 100644 index 1654af6..0000000 --- a/javascript/eu_cookie_compliance.state.js +++ /dev/null @@ -1,191 +0,0 @@ -// ----------------------------------------------------------------------------- -// Omnipedia - Site theme - EU cookie compliance events -// ----------------------------------------------------------------------------- - -// This provides custom events on various pop-up state changes. It also does the -// following: -// -// - Fixes the 'eu_cookie_compliance_popup_close' event not being triggered when -// closing the pop-up via the accept buttons. -// -// - Fixes the container (usually body) agreement status classes not updating -// after the module JavaScript has attached. - -AmbientImpact.onGlobals([ - 'Drupal.eu_cookie_compliance', - 'drupalSettings.eu_cookie_compliance', -], function() { -AmbientImpact.on([ - 'OmnipediaSiteThemeEuCookieComplianceElements', -], function(euCookieComplianceElements) { -AmbientImpact.addComponent( - 'OmnipediaSiteThemeEuCookieComplianceState', -function( - euCookieComplianceState, $ -) { - - 'use strict'; - - /** - * Class applied to the containing element when the pop-up is open. - * - * @type {String} - */ - const containingElementOpenClass = 'eu-cookie-compliance-popup-open'; - - /** - * Event namespace name. - * - * @type {String} - */ - const eventNamespace = this.getName(); - - /** - * The transition duration for the cookie compliance pop-up. - * - * @type {Number} - */ - let transitionDuration = drupalSettings.eu_cookie_compliance.popup_delay; - - /** - * Whether the pop-up is currently open. - * - * @return {Boolean} - */ - this.isPopUpOpen = function() { - return euCookieComplianceElements.getContainingElement().is( - '.' + containingElementOpenClass - ); - }; - - this.addBehaviour( - 'OmnipediaSiteThemeEuCookieComplianceState', - 'omnipedia-site-theme-eu-cookie-compliance-state', - euCookieComplianceElements.getBehaviourSelector(), - function(context, settings) { - - /** - * The cookie compliance pop-up, if any, wrapped in a jQuery collection. - * - * @type {jQuery} - */ - let $popUp = euCookieComplianceElements.getPopUp(); - - // Bail if we can't find the pop-up. - if ($popUp.length === 0) { - return; - } - - // Save the transition duration as a custom property on the pop-up so that - // it can be used in CSS. - $popUp.prop('style').setProperty( - '--eu-cookie-compliance-transition-duration', - transitionDuration + 'ms' - ); - - $popUp.on( - 'eu_cookie_compliance_popup_open.' + eventNamespace, - function(event) { - - $(this).trigger('euCookieCompliancePopUpOpen'); - - // The module's JavaScript does not provide an event for when the pop-up - // has finished opening, so this does that in a roundabout way. Note - // that since this is using the configured delay value, it waits in - // parallel with the value, but may be triggered a frame before or after - // the pop-up's jQuery animation has actually completed. - setTimeout(function() { - - $popUp.trigger('euCookieCompliancePopUpOpened'); - - }, drupalSettings.eu_cookie_compliance.popup_delay); - - }) - .on( - 'eu_cookie_compliance_popup_close.' + eventNamespace, - function(event) { - - $(this).trigger('euCookieCompliancePopUpClose'); - - // The module JavaScript annoyingly does not remove this class when the - // pop-up is closed via clicking the accept buttons so make sure this is - // removed on close. - euCookieComplianceElements.getContainingElement().removeClass( - containingElementOpenClass - ); - - // The module's JavaScript does not provide an event for when the pop-up - // has finished closing, so this does that in a roundabout way. Note - // that since this is using the configured delay value, it waits in - // parallel with the value, but may be triggered a frame before or after - // the pop-up's jQuery animation has actually completed. - setTimeout(function() { - - // The module's JavaScript only updates the container (usually body) - // classes when first attached. Unfortunately, this means that CSS - // cannot depend on these updating when a user agrees to some or all - // categories, so we have to do that ourselves. Note that this must - // delay until the pop-up is expected to be closed to avoid causing - // issues with hiding the pop-up smoothly. - euCookieComplianceElements.getContainingElement() - .removeClass([ - 'eu-cookie-compliance-status-null', - 'eu-cookie-compliance-status-1', - 'eu-cookie-compliance-status-2', - ]) - .addClass( - 'eu-cookie-compliance-status-' + - Drupal.eu_cookie_compliance.getCurrentStatus() - ); - - $popUp.trigger('euCookieCompliancePopUpClosed'); - - }, drupalSettings.eu_cookie_compliance.popup_delay); - - }) - .on('click.' + eventNamespace, function(event) { - - // Hacky way of triggering the close event because the module's - // JavaScript annoyingly does not trigger it when closed by the accept/ - // agree buttons. - if ( - $(event.target).is('.eu-cookie-compliance-save-preferences-button') || - $(event.target).is('.agree-button') - ) { - - $popUp.trigger('eu_cookie_compliance_popup_close'); - - } - - }); - - }, - function(context, settings, trigger) { - - /** - * The cookie compliance pop-up, if any, wrapped in a jQuery collection. - * - * @type {jQuery} - */ - let $popUp = euCookieComplianceElements.getPopUp(); - - // Bail if we can't find the pop-up. - if ($popUp.length === 0) { - return; - } - - $popUp.off([ - 'eu_cookie_compliance_popup_open.' + eventNamespace, - 'eu_cookie_compliance_popup_close.' + eventNamespace, - 'click.' + eventNamespace, - ].join(' ')) - .prop('style').removeProperty( - '--eu-cookie-compliance-transition-duration' - ); - - } - ); - -}); -}); -}); diff --git a/omnipedia_site_theme.libraries.yml b/omnipedia_site_theme.libraries.yml index af0e3f1..180d67c 100644 --- a/omnipedia_site_theme.libraries.yml +++ b/omnipedia_site_theme.libraries.yml @@ -45,13 +45,13 @@ eu_cookie_compliance: theme: stylesheets/components/eu_cookie_compliance.css: {} js: - javascript/eu_cookie_compliance.elements.js: { attributes: { defer: true } } - javascript/eu_cookie_compliance.state.js: { attributes: { defer: true } } - javascript/eu_cookie_compliance.focus.js: { attributes: { defer: true } } + javascript/eu_cookie_compliance.js: { attributes: { defer: true } } javascript/eu_cookie_compliance.decide_later.js: { attributes: { defer: true } } + javascript/eu_cookie_compliance.focus.js: { attributes: { defer: true } } javascript/eu_cookie_compliance.overlay.js: { attributes: { defer: true } } dependencies: - ambientimpact_core/ally.js + - ambientimpact_core/component.fastdom - ambientimpact_core/framework - ambientimpact_ux/component.overlay - ambientimpact_ux/component.pointer_focus_hide