From be5f010e91377f3a5628d9a50e969862e2f5a518 Mon Sep 17 00:00:00 2001 From: Matei Stanca Date: Sat, 26 Oct 2024 14:20:59 -0400 Subject: [PATCH] Issue #23: Swipe gesture now open/close the off-canvas sidebar menu. --- javascript/sidebars.gestures.js | 281 +++++++++++++++++++++++++++++ omnipedia_site_theme.libraries.yml | 12 ++ package.json | 2 + 3 files changed, 295 insertions(+) create mode 100644 javascript/sidebars.gestures.js diff --git a/javascript/sidebars.gestures.js b/javascript/sidebars.gestures.js new file mode 100644 index 0000000..3738aab --- /dev/null +++ b/javascript/sidebars.gestures.js @@ -0,0 +1,281 @@ +// ----------------------------------------------------------------------------- +// Omnipedia - Site theme - Sidebars gestures +// ----------------------------------------------------------------------------- + +// Gesture support using Hammer.js. +// +// @see https://hammerjs.github.io/ +// +// @see https://github.com/naver/hammer.js +// We're using this fork of the original which is a bit more maintained. + +AmbientImpact.onGlobals('Hammer', function() { +AmbientImpact.on([ + 'fastdom', + 'OmnipediaSiteThemeSidebars', +], function(aiFastDom, sidebars) { +AmbientImpact.addComponent('OmnipediaSiteThemeSidebarsGestures', function( + sidebarsGestures, $, +) { + + 'use strict'; + + /** + * Event namespace name. + * + * @type {String} + */ + const eventNamespace = this.getName(); + + /** + * FastDom instance. + * + * @type {FastDom} + */ + const fastdom = aiFastDom.getInstance(); + + /** + * Represents the sidebars gestures support. + */ + class SidebarsGestures { + + /** + * The Sidebars instance we're providing gesture support for. + * + * @type {Sidebars} + */ + #sidebars; + + /** + * Hammer instance watching the root () element. + * + * @type {Hammer} + * + * @see https://hammerjs.github.io/ + */ + #hammer; + + /** + * Constructor. + * + * @param {Sidebars} sidebars + * A Sidebars instance. + */ + constructor(sidebars) { + + this.#sidebars = sidebars; + + this.#hammer = new Hammer(document.documentElement, {cssProps: { + // Ensure text is still selectable; by default, Hammer sets this + // to 'none', which is terrible for our UX since we're very text-heavy + // and content-oriented. + // + // @see https://developer.mozilla.org/en-US/docs/Web/CSS/user-select + userSelect: 'auto', + }}); + + this.#bindEventHandlers(); + + } + + /** + * Destroy this instance. + * + * @return {Promise} + * A Promise that resolves when various DOM tasks are complete. + */ + destroy() { + + this.#unbindEventHandlers(); + + this.#hammer.destroy(); + + return Promise.resolve(); + + } + + /** + * Bind all of our event handlers. + * + * @see this~#unbindEventHandlers() + */ + async #bindEventHandlers() { + + /** + * Reference to the current instance. + * + * @type {SidebarsGestures} + */ + const that = this; + + await this.#sidebars.updateOffCanvas(); + + if (this.#sidebars.isOpen() === true) { + + this.#bindOpenedHandlers(); + + } else { + + this.#bindClosedHandlers(); + + } + + this.#sidebars.$sidebars.on( + `omnipediaSidebarsMenuOpen.${eventNamespace}`, + async function(event, sidebars) { + + that.#bindOpenedHandlers(); + + }).on(`omnipediaSidebarsMenuClose.${eventNamespace}`, async function( + event, sidebars, + ) { + + that.#bindClosedHandlers(); + + }).one(`OmnipediaSidebarsDestroyed.${eventNamespace}`, function( + event, sidebars, + ) { + that.destroy(); + }); + + } + + /** + * Unbind all of our event handlers. + * + * @see this~#bindEventHandlers() + */ + #unbindEventHandlers() { + + this.#sidebars.$sidebars.off([ + `omnipediaSidebarsMenuOpen.${eventNamespace}`, + `omnipediaSidebarsMenuClose.${eventNamespace}`, + // Don't unbind the one-off OmnipediaSidebarsDestroyed event handler as + // that auto destroys this instance. + ].join(' ')); + + // We call this.#hammer.destroy() so we probably don't need to unbind from + // it at this point as it's supposed to do that for us. + + } + + /** + * Get the sidebar gesture direction based on the document text direction. + * + * @param {Boolean} reverse + * Whether to reverse the direction. + * + * @return {String} + */ + async #getGestureEventDirection(reverse) { + + const documentDirection = await fastdom.measure(function() { + return $(document.documentElement).attr('dir'); + }); + + if (documentDirection === 'ltr') { + + return reverse !== true ? 'left' : 'right'; + + } else if (documentDirection === 'rtl') { + + return reverse !== true ? 'right' : 'left'; + + } + + } + + /** + * Bind event handlers when in an opened state. + */ + async #bindOpenedHandlers() { + + /** + * Reference to the current instance. + * + * @type {SidebarsGestures} + */ + const that = this; + + this.#hammer.off( + 'swipeleft swiperight', + ); + + const direction = await this.#getGestureEventDirection(true); + + this.#hammer.on( + `swipe${direction}`, + function(event) { + + // Don't do anything if sidebars are not off-canvas. + if (that.#sidebars.isOffCanvas() !== true) { + return; + } + + that.#sidebars.close(); + + }, + ); + + } + + /** + * Bind event handlers when in an closed state. + */ + async #bindClosedHandlers() { + + /** + * Reference to the current instance. + * + * @type {SidebarsGestures} + */ + const that = this; + + this.#hammer.off( + 'swipeleft swiperight', + ); + + const direction = await this.#getGestureEventDirection(false); + + this.#hammer.on( + `swipe${direction}`, + function(event) { + + // Don't do anything if sidebars are not off-canvas. + if (that.#sidebars.isOffCanvas() !== true) { + return; + } + + that.#sidebars.open(); + + }, + ); + + } + + } + + this.addBehaviour( + 'OmnipediaSiteThemeSidebarsGestures', + 'omnipedia-site-theme-sidebars-gestures', + '.layout-container', + function(context, settings) { + + $(this).prop('OmnipediaSidebarsGestures', new SidebarsGestures( + $(this).prop('OmnipediaSidebars'), + )); + + }, + function(context, settings, trigger) { + + // SidebarsGestures destroys itself on the OmnipediaSidebarsDestroyed + // event so we just need to remove the property and let browser garbage + // collection handle the rest. + $(this).removeProp('OmnipediaSidebarsGestures'); + + } + ); + +}); +}); +}); diff --git a/omnipedia_site_theme.libraries.yml b/omnipedia_site_theme.libraries.yml index f6fa585..f95a798 100644 --- a/omnipedia_site_theme.libraries.yml +++ b/omnipedia_site_theme.libraries.yml @@ -68,6 +68,16 @@ form_radios: theme: stylesheets/components/form_radios.css: {} +hammerjs: + remote: https://github.com/naver/hammer.js + version: "2.0.17" + license: + name: MIT + url: https://github.com/naver/hammer.js/blob/2.0.17/LICENSE.md + gpl-compatible: true + js: + vendor/@egjs/hammerjs/dist/hammer.min.js: { minified: true } + header: css: theme: @@ -206,6 +216,7 @@ sidebars: js: javascript/sidebars.js: { attributes: { defer: true } } javascript/sidebars.focus.js: { attributes: { defer: true } } + javascript/sidebars.gestures.js: { attributes: { defer: true } } javascript/sidebars.keyboard.js: { attributes: { defer: true } } javascript/sidebars.overlay.js: { attributes: { defer: true } } dependencies: @@ -218,6 +229,7 @@ sidebars: - ambientimpact_ux/component.pointer_focus_hide - ambientimpact_ux/component.responsive_style_property - ambientimpact_ux/component.scrollbar_gutter + - omnipedia_site_theme/hammerjs site_branding: css: diff --git a/package.json b/package.json index 3f39d24..521bb98 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "webpack-remove-empty-scripts": "^1.0.1" }, "dependencies": { + "@egjs/hammerjs": "^2.0.17", "@fontsource/exo-2": "^4.5.10", "drupal-ambientimpact-base": "workspace:^6", "drupal-ambientimpact-core": "workspace:^2", @@ -53,6 +54,7 @@ "js-cookie": "^3.0.5" }, "vendorize": [ + "@egjs/hammerjs", "@fontsource/exo-2", "js-cookie" ]