diff --git a/javascript/sidebars.elements.js b/javascript/sidebars.elements.js deleted file mode 100644 index 388d9e5..0000000 --- a/javascript/sidebars.elements.js +++ /dev/null @@ -1,149 +0,0 @@ -// ----------------------------------------------------------------------------- -// Omnipedia - Site theme - Sidebars elements -// ----------------------------------------------------------------------------- - -AmbientImpact.on('OmnipediaSiteThemeHeaderElements', function( - headerElements, $ -) { -AmbientImpact.addComponent('OmnipediaSiteThemeSidebarsElements', function( - sidebarsElements, $ -) { - - 'use strict'; - - /** - * The sidebars container, if any, wrapped in a jQuery collection. - * - * @type {jQuery} - */ - let $container = $(); - - /** - * The sidebars close control, if any, wrapped in a jQuery collection. - * - * @type {jQuery} - */ - let $menuClose = $(); - - /** - * The sidebars closed anchor, if any, wrapped in a jQuery collection. - * - * @type {jQuery} - */ - let $menuClosedAnchor = $(); - - /** - * The sidebars closed target, if any, wrapped in a jQuery collection. - * - * @type {jQuery} - */ - let $menuClosedTarget = $(); - - /** - * Get the sidebars container jQuery collection. - * - * @return {jQuery} - */ - this.getSidebarsContainer = function() { - return $container; - }; - - /** - * Get the sidebars menu open control jQuery collection. - * - * @return {jQuery} - */ - this.getSidebarsMenuOpen = function() { - return headerElements.getMenuOpen(); - }; - - /** - * Get the sidebars menu close control jQuery collection. - * - * @return {jQuery} - */ - this.getSidebarsMenuClose = function() { - return $menuClose; - }; - - /** - * Get the sidebars menu closed anchor jQuery collection. - * - * @return {jQuery} - */ - this.getSidebarsMenuClosedAnchor = function() { - return $menuClosedAnchor; - }; - - /** - * Get the sidebars menu closed target jQuery collection. - * - * @return {jQuery} - */ - this.getSidebarsMenuClosedTarget = function() { - return $menuClosedTarget; - }; - - /** - * Get the selector to attach sidebars behaviours to. - * - * @return {String} - */ - this.getSidebarsBehaviourSelector = function() { - return '.layout-container'; - }; - - this.addBehaviour( - 'OmnipediaSiteThemeSidebarsElements', - 'omnipedia-site-theme-sidebars-elements', - this.getSidebarsBehaviourSelector(), - function(context, settings) { - - $container = $('.layout-sidebars', context); - - $menuClose = $container.find('.layout-sidebars__close', context); - - $menuClosedAnchor = $('.layout-sidebars__closed-anchor', context); - - $menuClosedTarget = $('.layout-sidebars__closed-target', context); - - // Reset jQuery collections and bail if we can't find one of the required - // elements. - if ( - $container.length === 0 || - $menuClose.length === 0 || - $menuClosedAnchor.length === 0 || - $menuClosedTarget.length === 0 - ) { - console.error( - 'Could not find one of the required elements. Found:', - $container, $menuClose, $menuClosedAnchor, $menuClosedTarget - ); - - $container = $(); - - $menuClose = $(); - - $menuClosedAnchor = $(); - - $menuClosedTarget = $(); - - return; - } - - }, - function(context, settings, trigger) { - - $container = $(); - - $menuClose = $(); - - $menuClosedAnchor = $(); - - $menuClosedTarget = $(); - - } - ); - -}); -}); diff --git a/javascript/sidebars.focus.js b/javascript/sidebars.focus.js index bbc2a40..82aed40 100644 --- a/javascript/sidebars.focus.js +++ b/javascript/sidebars.focus.js @@ -10,13 +10,15 @@ // - 'omnipediaSidebarsMenuClose': will move focus to the menu open control if // sidebars are off-canvas. +AmbientImpact.onGlobals([ + 'ally.query.tabbable', +], function() { AmbientImpact.on([ - 'OmnipediaSiteThemeSidebarsElements', - 'OmnipediaSiteThemeSidebarsState', + 'OmnipediaSiteThemeSidebars', 'pointerFocusHide', -], function(sidebarsElements, sidebarsState, aiPointerFocusHide, $) { +], function(sidebars, aiPointerFocusHide, $) { AmbientImpact.addComponent('OmnipediaSiteThemeSidebarsFocus', function( - sidebarsFocus, $ + sidebarsFocus, $, ) { 'use strict'; @@ -31,34 +33,39 @@ AmbientImpact.addComponent('OmnipediaSiteThemeSidebarsFocus', function( this.addBehaviour( 'OmnipediaSiteThemeSidebarsFocus', 'omnipedia-site-theme-sidebars-focus', - sidebarsElements.getSidebarsBehaviourSelector(), + 'body', function(context, settings) { - $(this).on( - 'omnipediaSidebarsMenuOpen.' + eventNamespace, - function(event) { + $(this).on(`omnipediaSidebarsMenuOpen.${eventNamespace}`, function( + event, instance, + ) { - if (sidebarsState.isOffCanvas() === true) { - - aiPointerFocusHide.lock(); - - sidebarsElements.getSidebarsContainer().focus(); + if (instance.isOffCanvas() !== true) { + return; + } - aiPointerFocusHide.unlock(); + aiPointerFocusHide.lock(); - } + $(ally.query.tabbable({ + context: instance.$sidebars, + includeContext: true, + })).first().focus(); - }).on('omnipediaSidebarsMenuClose.' + eventNamespace, function(event) { + aiPointerFocusHide.unlock(); - if (sidebarsState.isOffCanvas() === true) { + }).on(`omnipediaSidebarsMenuClose.${eventNamespace}`, function( + event, instance, + ) { - aiPointerFocusHide.lock(); + if (instance.isOffCanvas() !== true) { + return; + } - sidebarsElements.getSidebarsMenuOpen().focus(); + aiPointerFocusHide.lock(); - aiPointerFocusHide.unlock(); + instance.$menuOpen.focus(); - } + aiPointerFocusHide.unlock(); }); @@ -66,8 +73,8 @@ AmbientImpact.addComponent('OmnipediaSiteThemeSidebarsFocus', function( function(context, settings, trigger) { $(this).off([ - 'omnipediaSidebarsMenuOpen.' + eventNamespace, - 'omnipediaSidebarsMenuClose.' + eventNamespace, + `omnipediaSidebarsMenuOpen.${eventNamespace}`, + `omnipediaSidebarsMenuClose.${eventNamespace}`, ].join(' ')); } @@ -75,3 +82,4 @@ AmbientImpact.addComponent('OmnipediaSiteThemeSidebarsFocus', function( }); }); +}); diff --git a/javascript/sidebars.js b/javascript/sidebars.js new file mode 100644 index 0000000..dd44cba --- /dev/null +++ b/javascript/sidebars.js @@ -0,0 +1,444 @@ +// ----------------------------------------------------------------------------- +// Omnipedia - Site theme - Sidebars component +// ----------------------------------------------------------------------------- + +AmbientImpact.on([ + 'fastdom', + 'hashMatcher', + 'responsiveStyleProperty', +], function( + aiFastDom, + aiHashMatcher, + aiResponsiveStyleProperty, +) { +AmbientImpact.addComponent('OmnipediaSiteThemeSidebars', function(sidebars, $) { + + 'use strict'; + + /** + * Event namespace name. + * + * @type {String} + */ + const eventNamespace = this.getName(); + + /** + * FastDom instance. + * + * @type {FastDom} + */ + const fastdom = aiFastDom.getInstance(); + + /** + * The CSS custom property name that we watch for the off-canvas state. + * + * The expected value should be a string of either: + * + * - 'true': the layout is in compact mode with the anchor visible. + * + * - 'false': the layout has the sidebars beside the content column, so the + * anchor is hidden. + * + * This allows us to detect the current state of the sidebars, which + * delegates the state to CSS without having to hard code any media queries + * in JavaScript. + * + * @type {String} + */ + const offCanvasStatePropertyName = '--omnipedia-sidebars-off-canvas'; + + /** + * Represents the sidebars. + */ + class Sidebars { + + /** + * The sidebars container wrapped in a jQuery collection. + * + * @type {jQuery} + */ + #$container; + + /** + * The sidebars close element wrapped in a jQuery collection. + * + * @type {jQuery} + */ + #$menuClose; + + /** + * The sidebars closed anchor wrapped in a jQuery collection. + * + * @type {jQuery} + */ + #$menuClosedAnchor; + + /** + * The sidebars closed target element wrapped in a jQuery collection. + * + * @type {jQuery} + */ + #$menuClosedTarget; + + /** + * The menu open element wrapped in a jQuery collection. + * + * @type {jQuery} + */ + #$menuOpen; + + /** + * The sidebars element wrapped in a jQuery collection. + * + * @type {jQuery} + */ + #$sidebars; + + /** + * The hash value stored in the open link's 'hash' property. + * + * @type {USVString} + */ + #menuOpenHash; + + /** + * Hash matcher instance. + * + * @type {hashMatcher} + */ + #hashMatcher; + + /** + * A responsive style property instance; watches off canvas state. + * + * @type {responsiveStyleProperty} + */ + #responsiveStyleProperty; + + /** + * Constructor. + * + * @param {jQuery|HTMLElement} $container + * The layout container element wrapped in a jQuery collection or as an + * HTMLElement. + */ + constructor($container) { + + this.#$container = $($container); + + this.#$sidebars = this.#$container.find('.layout-sidebars'); + + this.#$menuClose = this.#$container.find('.layout-sidebars__close'); + + this.#$menuClosedAnchor = this.#$container.find( + '.layout-sidebars__closed-anchor', + ); + + this.#$menuClosedTarget = this.#$container.find( + '.layout-sidebars__closed-target', + ); + + this.#$menuOpen = this.#$container.find('.omnipedia-header__menu-link'); + + this.#validateElements(); + + const beforeConstructEvent = new $.Event( + 'OmnipediaSidebarsBeforeConstruct', + ); + + this.#$sidebars.trigger(beforeConstructEvent, [this]); + + this.#menuOpenHash = this.#$menuOpen.prop('hash'); + + this.#bindEventHandlers(); + + this.#hashMatcher = aiHashMatcher.create(this.#menuOpenHash); + + this.#responsiveStyleProperty = aiResponsiveStyleProperty.create( + offCanvasStatePropertyName, this.#$sidebars, + ); + + const constructedEvent = new $.Event('OmnipediaSidebarsConstructed'); + + this.#$sidebars.trigger(constructedEvent, [this]); + + } + + /** + * Destroy this instance. + * + * @return {Promise} + * A Promise that resolves when various DOM tasks are complete. + */ + destroy() { + + const beforeDestroyEvent = new $.Event('OmnipediaSidebarsBeforeDestroy'); + + this.#$sidebars.trigger(beforeDestroyEvent, [this]); + + this.#unbindEventHandlers(); + + this.#hashMatcher.destroy(); + + this.#responsiveStyleProperty.destroy(); + + const destroyedEvent = new $.Event('OmnipediaSidebarsDestroyed'); + + this.#$sidebars.trigger(destroyedEvent, [this]); + + return Promise.resolve(); + + } + + /** + * Validate that we've found all of the required elements. + * + * @throws {Error} + * If one or more of the elements could not be found. + */ + #validateElements() { + + if ( + this.#$menuClose.length > 0 && + this.#$menuClosedAnchor.length > 0 && + this.#$menuClosedTarget.length > 0 && + this.#$menuOpen.length > 0 && + this.#$sidebars.length > 0 + ) { + return; + } + + /** + * One or more missing element names to add to the thrown error. + * + * @type {String[]} + */ + let missing = []; + + if (this.#$menuClose.length === 0) { + missing.push('menu close'); + } + + if (this.#$menuClosedAnchor.length === 0) { + missing.push('menu closed anchor'); + } + + if (this.#$menuClosedTarget.length === 0) { + missing.push('menu closed target'); + } + + if (this.#$menuOpen.length === 0) { + missing.push('menu open'); + } + + if (this.#$sidebars.length === 0) { + missing.push('.layout-sidebars'); + } + + throw new Error( + `Could not find one of the required elements. Missing: ${missing.join( + ', ', + )}`, + ); + + } + + /** + * Bind all of our event handlers. + * + * @see this~#unbindEventHandlers() + */ + #bindEventHandlers() { + + /** + * Reference to the current instance. + * + * @type {Sidebars} + */ + const that = this; + + $(document).on(`hashMatchChange.${eventNamespace}`, function( + event, hash, matches, + ) { + + if (hash !== that.#menuOpenHash) { + return; + } + + if (matches === true) { + that.#$sidebars.trigger('omnipediaSidebarsMenuOpen', that); + + } else { + that.#$sidebars.trigger('omnipediaSidebarsMenuClose', that); + } + + }); + + // If the menu is open and the viewport is resized so the sidebars are no + // longer off-canvas, close the menu so that the page isn't left in a + // state that may make it unusable. + this.#$sidebars.on( + `responsivePropertyChange.${eventNamespace}`, + function(event, instance) { + + // Ignore any other instance's events. + if (instance.getPropertyName() !== offCanvasStatePropertyName) { + return; + } + + if (that.isOffCanvas() === false) { + that.close(); + } + + }); + + this.#$menuClose.on(`click.${eventNamespace}`, function(event) { + + if (that.isOffCanvas() === true) { + that.close(); + } + + event.preventDefault(); + + }); + + } + + /** + * Unbind all of our event handlers. + * + * @see this~#bindEventHandlers() + */ + #unbindEventHandlers() { + + $(document).off(`hashMatchChange.${eventNamespace}`); + + this.#$sidebars.off(`responsivePropertyChange.${eventNamespace}`); + + this.#$menuClose.off(`click.${eventNamespace}`); + + } + + /** + * Get the sidebars close element jQuery collection. + * + * @return {jQuery} + */ + get $menuClose() { + return this.#$menuClose; + } + + /** + * Get the sidebars closed anchor jQuery collection. + * + * @return {jQuery} + */ + get $menuClosedAnchor() { + return this.#$menuClosedAnchor; + } + + /** + * Get the sidebars closed target element jQuery collection. + * + * @return {jQuery} + */ + get $menuClosedTarget() { + return this.#$menuClosedTarget; + } + + /** + * Get the menu open element jQuery collection. + * + * @return {jQuery} + */ + get $menuOpen() { + return this.#$menuOpen; + } + + /** + * Get the sidebars element jQuery collection. + * + * @return {jQuery} + */ + get $sidebars() { + return this.#$sidebars; + } + + /** + * Whether the sidebars are currently off-canvas, i.e. on a narrow screen. + * + * @return {Boolean} + */ + isOffCanvas() { + + return this.#responsiveStyleProperty.getValue() === 'true'; + + } + + /** + * Whether the off-canvas sidebars menu is currently open. + * + * Note that this doesn't consider whether the sidebars are actually in + * off-canvas mode. This method should be used in conjuction with + * this.isOffCanvas() for that purpose. + * + * @return {Boolean} + * + * @see this.isOffCanvas() + */ + isOpen() { + + return this.#hashMatcher.matches(); + + }; + + /** + * Open the menu. + */ + open() { + + this.#hashMatcher.setActive(); + + }; + + /** + * Close the menu. + */ + close() { + + this.#hashMatcher.setInactive(); + + }; + + } + + this.addBehaviour( + 'OmnipediaSiteThemeSidebars', + 'omnipedia-site-theme-sidebars', + '.layout-container', + function(context, settings) { + + $(this).prop('OmnipediaSidebars', new Sidebars(this)); + + }, + function(context, settings, trigger) { + + /** + * Reference to the HTML element being detached from. + * + * @type {HTMLElement} + */ + const that = this; + + $(this).prop('OmnipediaSidebars').destroy().then(function() { + + $(that).removeProp('OmnipediaSidebars'); + + }); + + } + ); + + +}); +}); diff --git a/javascript/sidebars.keyboard.js b/javascript/sidebars.keyboard.js index 20ece93..651d9bc 100644 --- a/javascript/sidebars.keyboard.js +++ b/javascript/sidebars.keyboard.js @@ -1,28 +1,20 @@ // ----------------------------------------------------------------------------- -// Omnipedia - Site theme - Header keyboard enhancements +// Omnipedia - Site theme - Sidebars keyboard enhancements // ----------------------------------------------------------------------------- // This adds support for the escape key so that it can close the off-canvas // sidebars menu on narrow screens. +// +// @see https://allyjs.io/api/when/key.html AmbientImpact.onGlobals('ally.when.key', function() { -AmbientImpact.on([ - 'OmnipediaSiteThemeSidebarsElements', - 'OmnipediaSiteThemeSidebarsState', -], function(sidebarsElements, sidebarsState, $) { +AmbientImpact.on('OmnipediaSiteThemeSidebars', function(sidebars) { AmbientImpact.addComponent('OmnipediaSiteThemeSidebarsKeyboard', function( - sidebarsKeyboard, $ + sidebarsKeyboard, $, ) { 'use strict'; - /** - * ally.js when key handle. - * - * @see https://allyjs.io/api/when/key.html - */ - let allyWhenKeyHandle; - /** * Event namespace name. * @@ -30,43 +22,174 @@ AmbientImpact.addComponent('OmnipediaSiteThemeSidebarsKeyboard', function( */ const eventNamespace = this.getName(); - this.addBehaviour( - 'OmnipediaSiteThemeSidebarsKeyboard', - 'omnipedia-site-theme-sidebars-keyboard', - sidebarsElements.getSidebarsBehaviourSelector(), - function(context, settings) { + /** + * Represents the sidebars keyboard enhancements. + */ + class SidebarsKeyboard { + + /** + * The Sidebars instance we're providing an overlay for. + * + * @type {Sidebars} + */ + #sidebars; + + /** + * ally.when.key() instance; watches for escape key press. + * + * @type {Object} + * + * @see https://allyjs.io/api/when/key.html + */ + #whenKeyHandle; + + /** + * Constructor. + * + * @param {Sidebars} sidebars + * A Sidebars instance. + */ + constructor(sidebars) { + + this.#sidebars = sidebars; + + this.#bindEventHandlers(); + + } - $(this).on( - 'omnipediaSidebarsMenuOpen.' + eventNamespace, - function(event) { + /** + * Destroy this instance. + * + * @return {Promise} + * A Promise that resolves when various DOM tasks are complete. + */ + destroy() { - allyWhenKeyHandle = ally.when.key({ - escape: function(event, disengage) { - sidebarsState.closeMenu(); + this.#unbindEventHandlers(); - disengage(); - } - }); + this.unwatch(); + + return Promise.resolve(); + + } - }).on('omnipediaSidebarsMenuClose.' + eventNamespace, function(event) { + /** + * Bind all of our event handlers. + * + * @see this~#unbindEventHandlers() + */ + #bindEventHandlers() { - if (AmbientImpact.objectPathExists('disengage', allyWhenKeyHandle)) { - allyWhenKeyHandle.disengage(); - } + /** + * Reference to the current instance. + * + * @type {SidebarsKeyboard} + */ + const that = this; + + this.#sidebars.$sidebars.on( + `omnipediaSidebarsMenuOpen.${eventNamespace}`, + function(event, sidebars) { + + that.watch(); + + }).on(`omnipediaSidebarsMenuClose.${eventNamespace}`, function( + event, sidebars, + ) { + + that.unwatch(); + + }).one(`OmnipediaSidebarsDestroyed.${eventNamespace}`, function( + event, sidebars, + ) { + + that.destroy(); }); - }, - function(context, settings, trigger) { + } + + /** + * Unbind all of our event handlers. + * + * @see this~#bindEventHandlers() + */ + #unbindEventHandlers() { + + this.#sidebars.$sidebars.off([ + `omnipediaSidebarsMenuOpen.${eventNamespace}`, + `omnipediaSidebarsMenuClose.${eventNamespace}`, + // Don't remove the OmnipediaSidebarsDestroyed handler as it's a one-off + // and must be triggered to destroy; removing it here will likely + // prevent it triggering. + ].join(' ')); + + } + + /** + * Start watching for an escape key press if not already watching. + */ + watch() { - if (AmbientImpact.objectPathExists('disengage', allyWhenKeyHandle)) { - allyWhenKeyHandle.disengage(); + if (typeof this.#whenKeyHandle !== 'undefined') { + return; } - $(this).off([ - 'omnipediaSidebarsMenuOpen.' + eventNamespace, - 'omnipediaSidebarsMenuClose.' + eventNamespace, - ].join(' ')); + /** + * Reference to the current instance. + * + * @type {SidebarsKeyboard} + */ + const that = this; + + this.#whenKeyHandle = ally.when.key({ + + escape: function(event, disengage) { + + that.#sidebars.close(); + + disengage(); + + }, + + }); + + } + + /** + * Stop watching for an escape key press if currently watching. + */ + unwatch() { + + if (typeof this.#whenKeyHandle === 'undefined') { + return; + } + + this.#whenKeyHandle.disengage(); + + this.#whenKeyHandle = undefined; + + } + + } + + this.addBehaviour( + 'OmnipediaSiteThemeSidebarsKeyboard', + 'omnipedia-site-theme-sidebars-keyboard', + '.layout-container', + function(context, settings) { + + $(this).prop('OmnipediaSidebarsKeyboard', new SidebarsKeyboard( + $(this).prop('OmnipediaSidebars'), + )); + + }, + function(context, settings, trigger) { + + // SidebarsKeyboard destroys itself on the OmnipediaSidebarsDestroyed + // event so we just need to remove the property and let browser garbage + // collection handle the rest. + $(this).removeProp('OmnipediaSidebarsKeyboard'); } ); diff --git a/javascript/sidebars.overlay.js b/javascript/sidebars.overlay.js index 494737a..2d85f78 100644 --- a/javascript/sidebars.overlay.js +++ b/javascript/sidebars.overlay.js @@ -2,19 +2,16 @@ // Omnipedia - Site theme - Sidebars overlay // ----------------------------------------------------------------------------- -// This adds a one off click handler to the menu closed anchor element (which -// doubles as the overlay) when the off-canvas sidebars container is open, -// allowing a click or tap on the overlay to close the menu and overlay. -// Additionally, this marks that same element as being an active overlay for the -// purpose of preventing viewport scrolling while the overlay is open. +// This progressively enhances the CSS-based overlay with the JavaScript-powered +// overlay that prevents viewport scrolling when open for better UX. AmbientImpact.on([ - 'OmnipediaSiteThemeSidebarsElements', - 'OmnipediaSiteThemeSidebarsState', + 'fastdom', + 'OmnipediaSiteThemeSidebars', 'overlay', -], function(sidebarsElements, sidebarsState, aiOverlay, $) { +], function(aiFastDom, sidebars, aiOverlay) { AmbientImpact.addComponent('OmnipediaSiteThemeSidebarsOverlay', function( - sidebarsOverlay, $ + sidebarsOverlay, $, ) { 'use strict'; @@ -26,6 +23,13 @@ AmbientImpact.addComponent('OmnipediaSiteThemeSidebarsOverlay', function( */ const eventNamespace = this.getName(); + /** + * FastDom instance. + * + * @type {FastDom} + */ + const fastdom = aiFastDom.getInstance(); + /** * Class applied to the sidebars menu closed anchor when disabled. * @@ -50,110 +54,203 @@ AmbientImpact.addComponent('OmnipediaSiteThemeSidebarsOverlay', function( */ const overlayClass = 'overlay--layout-sidebars'; - this.addBehaviour( - 'OmnipediaSiteThemeSidebarsOverlay', - 'omnipedia-site-theme-sidebars-overlay', - sidebarsElements.getSidebarsBehaviourSelector(), - function(context, settings) { + /** + * Represents the sidebars overlay. + */ + class SidebarsOverlay { - sidebarsElements.getSidebarsMenuClosedAnchor().addClass( - menuClosedAnchorDisabledClass - ); + /** + * The overlay instance. + * + * @type {Object} + */ + #overlay; - /** - * The header overlay, wrapped in a jQuery object. - * - * @type {jQuery} - */ - let $overlay = aiOverlay.create(); + /** + * The overlay element wrapped in a jQuery collection. + * + * @type {jQuery} + */ + #$overlay; + + /** + * The Sidebars instance we're providing an overlay for. + * + * @type {Sidebars} + */ + #sidebars; + + /** + * Constructor. + * + * @param {Sidebars} sidebars + * A Sidebars instance. + */ + constructor(sidebars) { + + this.#sidebars = sidebars; + + this.#$overlay = aiOverlay.create(); + + this.#$overlay.addClass(overlayClass); + + this.#overlay = this.#$overlay.prop('aiOverlay'); + + this.#bindEventHandlers(); /** - * The overlay instance. + * Reference to the current instance. * - * @type {Object} + * @type {SidebarsOverlay} */ - let overlay = $overlay.prop('aiOverlay'); + const that = this; + + fastdom.mutate(function() { + + that.#sidebars.$menuClosedTarget + .before(that.#$overlay) + .addClass(hasOverlayClass); + + that.#sidebars.$menuClosedAnchor.addClass( + menuClosedAnchorDisabledClass, + ); + + }); + + } + + /** + * Destroy this instance. + * + * @return {Promise} + * A Promise that resolves when various DOM tasks are complete. + */ + destroy() { /** - * The sidebars closed target jQuery collection. + * Reference to the current instance. * - * @type {jQuery} + * @type {SidebarsOverlay} */ - let $menuClosedTarget = sidebarsElements.getSidebarsMenuClosedTarget(); + const that = this; - // Save overlay instance to a property for the detach handler. - $menuClosedTarget.prop('aiOverlay', overlay); + this.#unbindEventHandlers(); - $overlay.addClass(overlayClass).insertBefore($menuClosedTarget); + return fastdom.measure(function() { - // Add class indicating JavaScript overlay is active. - $menuClosedTarget.addClass(hasOverlayClass); + return that.#overlay.isActive(); - $overlay.on('click.' + eventNamespace, function(event) { - sidebarsState.closeMenu(); - }); + }).then(function(isOverlayActive) {return fastdom.mutate(function() { + + that.#sidebars.$menuClosedTarget.removeClass(hasOverlayClass); + + that.#sidebars.$menuClosedAnchor.removeClass( + menuClosedAnchorDisabledClass, + ); + + if (isOverlayActive === false) { - $(this).on( - 'omnipediaSidebarsMenuOpen.' + eventNamespace, - function(event) { + that.#$overlay.detach(); - if (!sidebarsState.isOffCanvas()) { return; + } - overlay.show(); + // Attach a one-off event handler to remove the overlay element and + // related properties/classes when the overlay has finished hiding. + that.#$overlay.one('overlayHidden', function(event) { + + that.#$overlay.detach(); - }).on('omnipediaSidebarsMenuClose.' + eventNamespace, function(event) { + }); - overlay.hide(); + // Tell the overlay to hide itself, which will trigger the above handler + // when complete. + that.#overlay.hide(); - }); + })}); - }, - function(context, settings, trigger) { + } - $(this).off([ - 'omnipediaSidebarsMenuOpen.' + eventNamespace, - 'omnipediaSidebarsMenuClose.' + eventNamespace, - ].join(' ')); + /** + * Bind all of our event handlers. + * + * @see this~#unbindEventHandlers() + */ + #bindEventHandlers() { /** - * The sidebars closed target jQuery collection. + * Reference to the current instance. * - * @type {jQuery} + * @type {SidebarsOverlay} */ - let $menuClosedTarget = sidebarsElements.getSidebarsMenuClosedTarget(); + const that = this; - /** - * The overlay instance. - * - * @type {Object} - */ - let overlay = $menuClosedTarget.prop('aiOverlay'); + this.#$overlay.on(`click.${eventNamespace}`, function(event) { + that.#sidebars.close(); + }); - if (typeof overlay !== 'undefined') { + this.#sidebars.$sidebars.on( + `omnipediaSidebarsMenuOpen.${eventNamespace}`, + function(event, sidebars) { - // Attach a one-off event handler to remove the overlay element and - // related properties/classes when the overlay has finished hiding. - overlay.$overlay.one('overlayHidden', function(event) { + if (sidebars.isOffCanvas() !== true) { + return; + } - overlay.$overlay.remove(); + that.#overlay.show(); - $menuClosedTarget.removeProp('aiOverlay'); + }).on(`omnipediaSidebarsMenuClose.${eventNamespace}`, function( + event, sidebars, + ) { - $menuClosedTarget.removeClass(hasOverlayClass); + that.#overlay.hide(); - }); + }).one(`OmnipediaSidebarsDestroyed.${eventNamespace}`, function( + event, sidebars, + ) { + that.destroy(); + }); - // Tell the overlay to hide itself, which will trigger the above handler - // when complete. - overlay.hide(); + } + + /** + * Unbind all of our event handlers. + * + * @see this~#bindEventHandlers() + */ + #unbindEventHandlers() { + + this.#$overlay.off(`click.${eventNamespace}`); + + this.#sidebars.$sidebars.off([ + `omnipediaSidebarsMenuOpen.${eventNamespace}`, + `omnipediaSidebarsMenuClose.${eventNamespace}`, + // Don't unbind the one-off OmnipediaSidebarsDestroyed event handler as + // that auto destroys this instance. + ].join(' ')); + + } - } + } + + this.addBehaviour( + 'OmnipediaSiteThemeSidebarsOverlay', + 'omnipedia-site-theme-sidebars-overlay', + '.layout-container', + function(context, settings) { + + $(this).prop('OmnipediaSidebarsOverlay', new SidebarsOverlay( + $(this).prop('OmnipediaSidebars'), + )); + + }, + function(context, settings, trigger) { - sidebarsElements.getSidebarsMenuClosedAnchor().removeClass( - menuClosedAnchorDisabledClass - ); + // SidebarsOverlay destroys itself on the OmnipediaSidebarsDestroyed event + // so we just need to remove the property and let browser garbage + // collection handle the rest. + $(this).removeProp('OmnipediaSidebarsOverlay'); } ); diff --git a/javascript/sidebars.state.js b/javascript/sidebars.state.js deleted file mode 100644 index c91db73..0000000 --- a/javascript/sidebars.state.js +++ /dev/null @@ -1,246 +0,0 @@ -// ----------------------------------------------------------------------------- -// Omnipedia - Site theme - Sidebars state -// ----------------------------------------------------------------------------- - -// This provides a centralized API and events for the sidebars state. Note that -// this assumes there is only once instance of the sidebars container on the -// page, and in the edge case that more than one is present, only the first will -// be used. - -AmbientImpact.on([ - 'hashMatcher', - 'OmnipediaSiteThemeSidebarsElements', - 'responsiveStyleProperty', -], function(aiHashMatcher, sidebarsElements, aiResponsiveStyleProperty, $) { -AmbientImpact.addComponent('OmnipediaSiteThemeSidebarsState', function( - sidebarsState, $ -) { - - 'use strict'; - - /** - * Event namespace name. - * - * @type {String} - */ - const eventNamespace = this.getName(); - - /** - * The CSS custom property name that we watch for the off-canvas state. - * - * The expected value should be a string of either: - * - * - 'true': the layout is in compact mode with the anchor visible. - * - * - 'false': the layout has the sidebars beside the content column, so the - * anchor is hidden. - * - * This allows us to detect the current state of the sidebars, which - * delegates the state to CSS without having to hard code any media queries - * in JavaScript. - * - * @type {String} - */ - const offCanvasStatePropertyName = '--omnipedia-sidebars-off-canvas'; - - /** - * The name that the responsive property instance is saved under. - * - * This should be unique so it doesn't potentially colide with another - * instance saved to the same element. - * - * @type {String} - */ - const responsivePropertyInstanceName = 'offCanvasResponsiveStyleProperty'; - - /** - * Whether the sidebars are currently off-canvas, i.e. on a narrow screen. - * - * @return {Boolean} - */ - this.isOffCanvas = function() { - - return ( - sidebarsElements.getSidebarsContainer() - .prop(responsivePropertyInstanceName).getValue() === 'true' - ); - - }; - - /** - * Whether the off-canvas sidebars menu is currently open. - * - * Note that this doesn't consider whether the sidebars are actually in - * off-canvas mode. This method should be used in conjuction with - * this.isOffCanvas() for that purpose. - * - * @return {Boolean} - * - * @see this.isOffCanvas() - */ - this.isMenuOpen = function() { - return sidebarsElements.getSidebarsMenuOpen().prop('hashMatcher').matches(); - }; - - /** - * Open the menu. - */ - this.openMenu = function() { - sidebarsElements.getSidebarsMenuOpen().prop('hashMatcher').setActive(); - }; - - /** - * Close the menu. - */ - this.closeMenu = function() { - sidebarsElements.getSidebarsMenuOpen().prop('hashMatcher').setInactive(); - }; - - /** - * Menu close control click event handler. - * - * @param {jQuery.Event} event - * The jQuery Event object. - */ - function menuCloseClickHandler(event) { - - if (sidebarsState.isOffCanvas() === true) { - sidebarsState.closeMenu(); - } - - event.preventDefault(); - - }; - - this.addBehaviour( - 'OmnipediaSiteThemeSidebarsState', - 'omnipedia-site-theme-sidebars-state', - sidebarsElements.getSidebarsBehaviourSelector(), - function(context, settings) { - - /** - * The menu open control jQuery collection. - * - * @type {jQuery} - */ - let $menuOpen = sidebarsElements.getSidebarsMenuOpen(); - - /** - * The hash value stored in the open link's 'hash' property. - * - * @type {USVString} - */ - const menuOpenHash = $menuOpen.prop('hash'); - - /** - * Hash matcher instance. - * - * @type {hashMatcher} - */ - let hashMatcher = aiHashMatcher.create(menuOpenHash); - - $menuOpen.prop('hashMatcher', hashMatcher); - - $(document).on('hashMatchChange.' + eventNamespace, function( - event, hash, matches - ) { - - if (hash !== menuOpenHash) { - return; - } - - if (matches === true) { - sidebarsElements.getSidebarsContainer().trigger( - 'omnipediaSidebarsMenuOpen' - ); - - } else { - sidebarsElements.getSidebarsContainer().trigger( - 'omnipediaSidebarsMenuClose' - ); - } - - }); - - sidebarsElements.getSidebarsMenuClose().on( - 'click.' + eventNamespace, menuCloseClickHandler - ); - - /** - * A responsive style property instance; watches off canvas state. - * - * @type {responsiveStyleProperty} - */ - let responsiveStyleProperty = aiResponsiveStyleProperty.create( - offCanvasStatePropertyName, sidebarsElements.getSidebarsContainer() - ); - - // If the menu is open and the viewport is resized so the sidebars are no - // longer off-canvas, close the menu so that the page isn't left in a - // state that may make it unusable. - sidebarsElements.getSidebarsContainer() - .on('responsivePropertyChange.' + eventNamespace, function( - event, instance - ) { - - // Ignore any other instance's events. - if (instance.getPropertyName() !== offCanvasStatePropertyName) { - return; - } - - if (sidebarsState.isOffCanvas() === false) { - sidebarsState.closeMenu(); - } - - }); - - sidebarsElements.getSidebarsContainer().prop( - responsivePropertyInstanceName, responsiveStyleProperty - ); - - }, - function(context, settings, trigger) { - - /** - * The sidebars container jQuery collection. - * - * @type {jQuery} - */ - let $sidebarsContainer = sidebarsElements.getSidebarsContainer(); - - /** - * The menu open control jQuery collection. - * - * @type {jQuery} - */ - let $menuOpen = sidebarsElements.getSidebarsMenuOpen(); - - sidebarsElements.getSidebarsMenuClose().off( - 'click.' + eventNamespace, menuCloseClickHandler - ); - - $(document).off('hashMatchChange.' + eventNamespace); - - if ($menuOpen.length > 0) { - - $menuOpen.prop('hashMatcher').destroy(); - - $menuOpen.removeProp('hashMatcher'); - - } - - if ($sidebarsContainer.length > 0) { - - $sidebarsContainer.off('responsivePropertyChange.' + eventNamespace); - - $sidebarsContainer.prop(responsivePropertyInstanceName).destroy(); - - $sidebarsContainer.removeProp(responsivePropertyInstanceName); - - } - - } - ); - -}); -}); diff --git a/omnipedia_site_theme.libraries.yml b/omnipedia_site_theme.libraries.yml index 180d67c..f5961dd 100644 --- a/omnipedia_site_theme.libraries.yml +++ b/omnipedia_site_theme.libraries.yml @@ -199,13 +199,13 @@ search_form: sidebars: js: - javascript/sidebars.elements.js: { attributes: { defer: true } } - javascript/sidebars.state.js: { attributes: { defer: true } } + javascript/sidebars.js: { attributes: { defer: true } } javascript/sidebars.focus.js: { attributes: { defer: true } } - javascript/sidebars.overlay.js: { attributes: { defer: true } } javascript/sidebars.keyboard.js: { attributes: { defer: true } } + javascript/sidebars.overlay.js: { attributes: { defer: true } } dependencies: - ambientimpact_core/ally.js + - ambientimpact_core/component.fastdom - ambientimpact_core/framework - ambientimpact_ux/component.hash_matcher - ambientimpact_ux/component.headroom @@ -213,7 +213,6 @@ sidebars: - ambientimpact_ux/component.pointer_focus_hide - ambientimpact_ux/component.responsive_style_property - ambientimpact_ux/component.scrollbar_gutter - - omnipedia_site_theme/header site_branding: css: