/vendor/foundation-sites/js/foundation.offcanvas.js
JavaScript | 434 lines | 224 code | 59 blank | 151 comment | 46 complexity | 78a47ba97dcad5bb43d7bdce98562847 MD5 | raw file
- 'use strict';
- !function($) {
- /**
- * OffCanvas module.
- * @module foundation.offcanvas
- * @requires foundation.util.keyboard
- * @requires foundation.util.mediaQuery
- * @requires foundation.util.triggers
- * @requires foundation.util.motion
- */
- class OffCanvas {
- /**
- * Creates a new instance of an off-canvas wrapper.
- * @class
- * @fires OffCanvas#init
- * @param {Object} element - jQuery object to initialize.
- * @param {Object} options - Overrides to the default plugin settings.
- */
- constructor(element, options) {
- this.$element = element;
- this.options = $.extend({}, OffCanvas.defaults, this.$element.data(), options);
- this.$lastTrigger = $();
- this.$triggers = $();
- this._init();
- this._events();
- Foundation.registerPlugin(this, 'OffCanvas')
- Foundation.Keyboard.register('OffCanvas', {
- 'ESCAPE': 'close'
- });
- }
- /**
- * Initializes the off-canvas wrapper by adding the exit overlay (if needed).
- * @function
- * @private
- */
- _init() {
- var id = this.$element.attr('id');
- this.$element.attr('aria-hidden', 'true');
- this.$element.addClass(`is-transition-${this.options.transition}`);
- // Find triggers that affect this element and add aria-expanded to them
- this.$triggers = $(document)
- .find('[data-open="'+id+'"], [data-close="'+id+'"], [data-toggle="'+id+'"]')
- .attr('aria-expanded', 'false')
- .attr('aria-controls', id);
- // Add an overlay over the content if necessary
- if (this.options.contentOverlay === true) {
- var overlay = document.createElement('div');
- var overlayPosition = $(this.$element).css("position") === 'fixed' ? 'is-overlay-fixed' : 'is-overlay-absolute';
- overlay.setAttribute('class', 'js-off-canvas-overlay ' + overlayPosition);
- this.$overlay = $(overlay);
- if(overlayPosition === 'is-overlay-fixed') {
- $('body').append(this.$overlay);
- } else {
- this.$element.siblings('[data-off-canvas-content]').append(this.$overlay);
- }
- }
- this.options.isRevealed = this.options.isRevealed || new RegExp(this.options.revealClass, 'g').test(this.$element[0].className);
- if (this.options.isRevealed === true) {
- this.options.revealOn = this.options.revealOn || this.$element[0].className.match(/(reveal-for-medium|reveal-for-large)/g)[0].split('-')[2];
- this._setMQChecker();
- }
- if (!this.options.transitionTime === true) {
- this.options.transitionTime = parseFloat(window.getComputedStyle($('[data-off-canvas]')[0]).transitionDuration) * 1000;
- }
- }
- /**
- * Adds event handlers to the off-canvas wrapper and the exit overlay.
- * @function
- * @private
- */
- _events() {
- this.$element.off('.zf.trigger .zf.offcanvas').on({
- 'open.zf.trigger': this.open.bind(this),
- 'close.zf.trigger': this.close.bind(this),
- 'toggle.zf.trigger': this.toggle.bind(this),
- 'keydown.zf.offcanvas': this._handleKeyboard.bind(this)
- });
- if (this.options.closeOnClick === true) {
- var $target = this.options.contentOverlay ? this.$overlay : $('[data-off-canvas-content]');
- $target.on({'click.zf.offcanvas': this.close.bind(this)});
- }
- }
- /**
- * Applies event listener for elements that will reveal at certain breakpoints.
- * @private
- */
- _setMQChecker() {
- var _this = this;
- $(window).on('changed.zf.mediaquery', function() {
- if (Foundation.MediaQuery.atLeast(_this.options.revealOn)) {
- _this.reveal(true);
- } else {
- _this.reveal(false);
- }
- }).one('load.zf.offcanvas', function() {
- if (Foundation.MediaQuery.atLeast(_this.options.revealOn)) {
- _this.reveal(true);
- }
- });
- }
- /**
- * Handles the revealing/hiding the off-canvas at breakpoints, not the same as open.
- * @param {Boolean} isRevealed - true if element should be revealed.
- * @function
- */
- reveal(isRevealed) {
- var $closer = this.$element.find('[data-close]');
- if (isRevealed) {
- this.close();
- this.isRevealed = true;
- this.$element.attr('aria-hidden', 'false');
- this.$element.off('open.zf.trigger toggle.zf.trigger');
- if ($closer.length) { $closer.hide(); }
- } else {
- this.isRevealed = false;
- this.$element.attr('aria-hidden', 'true');
- this.$element.off('open.zf.trigger toggle.zf.trigger').on({
- 'open.zf.trigger': this.open.bind(this),
- 'toggle.zf.trigger': this.toggle.bind(this)
- });
- if ($closer.length) {
- $closer.show();
- }
- }
- }
- /**
- * Stops scrolling of the body when offcanvas is open on mobile Safari and other troublesome browsers.
- * @private
- */
- _stopScrolling(event) {
- return false;
- }
- // Taken and adapted from http://stackoverflow.com/questions/16889447/prevent-full-page-scrolling-ios
- // Only really works for y, not sure how to extend to x or if we need to.
- _recordScrollable(event) {
- let elem = this; // called from event handler context with this as elem
- // If the element is scrollable (content overflows), then...
- if (elem.scrollHeight !== elem.clientHeight) {
- // If we're at the top, scroll down one pixel to allow scrolling up
- if (elem.scrollTop === 0) {
- elem.scrollTop = 1;
- }
- // If we're at the bottom, scroll up one pixel to allow scrolling down
- if (elem.scrollTop === elem.scrollHeight - elem.clientHeight) {
- elem.scrollTop = elem.scrollHeight - elem.clientHeight - 1;
- }
- }
- elem.allowUp = elem.scrollTop > 0;
- elem.allowDown = elem.scrollTop < (elem.scrollHeight - elem.clientHeight);
- elem.lastY = event.originalEvent.pageY;
- }
- _stopScrollPropagation(event) {
- let elem = this; // called from event handler context with this as elem
- let up = event.pageY < elem.lastY;
- let down = !up;
- elem.lastY = event.pageY;
- if((up && elem.allowUp) || (down && elem.allowDown)) {
- event.stopPropagation();
- } else {
- event.preventDefault();
- }
- }
- /**
- * Opens the off-canvas menu.
- * @function
- * @param {Object} event - Event object passed from listener.
- * @param {jQuery} trigger - element that triggered the off-canvas to open.
- * @fires OffCanvas#opened
- */
- open(event, trigger) {
- if (this.$element.hasClass('is-open') || this.isRevealed) { return; }
- var _this = this;
- if (trigger) {
- this.$lastTrigger = trigger;
- }
- if (this.options.forceTo === 'top') {
- window.scrollTo(0, 0);
- } else if (this.options.forceTo === 'bottom') {
- window.scrollTo(0,document.body.scrollHeight);
- }
- /**
- * Fires when the off-canvas menu opens.
- * @event OffCanvas#opened
- */
- _this.$element.addClass('is-open')
- this.$triggers.attr('aria-expanded', 'true');
- this.$element.attr('aria-hidden', 'false')
- .trigger('opened.zf.offcanvas');
- // If `contentScroll` is set to false, add class and disable scrolling on touch devices.
- if (this.options.contentScroll === false) {
- $('body').addClass('is-off-canvas-open').on('touchmove', this._stopScrolling);
- this.$element.on('touchstart', this._recordScrollable);
- this.$element.on('touchmove', this._stopScrollPropagation);
- }
- if (this.options.contentOverlay === true) {
- this.$overlay.addClass('is-visible');
- }
- if (this.options.closeOnClick === true && this.options.contentOverlay === true) {
- this.$overlay.addClass('is-closable');
- }
- if (this.options.autoFocus === true) {
- this.$element.one(Foundation.transitionend(this.$element), function() {
- var canvasFocus = _this.$element.find('[data-autofocus]');
- if (canvasFocus.length) {
- canvasFocus.eq(0).focus();
- } else {
- _this.$element.find('a, button').eq(0).focus();
- }
- });
- }
- if (this.options.trapFocus === true) {
- this.$element.siblings('[data-off-canvas-content]').attr('tabindex', '-1');
- Foundation.Keyboard.trapFocus(this.$element);
- }
- }
- /**
- * Closes the off-canvas menu.
- * @function
- * @param {Function} cb - optional cb to fire after closure.
- * @fires OffCanvas#closed
- */
- close(cb) {
- if (!this.$element.hasClass('is-open') || this.isRevealed) { return; }
- var _this = this;
- _this.$element.removeClass('is-open');
- this.$element.attr('aria-hidden', 'true')
- /**
- * Fires when the off-canvas menu opens.
- * @event OffCanvas#closed
- */
- .trigger('closed.zf.offcanvas');
- // If `contentScroll` is set to false, remove class and re-enable scrolling on touch devices.
- if (this.options.contentScroll === false) {
- $('body').removeClass('is-off-canvas-open').off('touchmove', this._stopScrolling);
- this.$element.off('touchstart', this._recordScrollable);
- this.$element.off('touchmove', this._stopScrollPropagation);
- }
- if (this.options.contentOverlay === true) {
- this.$overlay.removeClass('is-visible');
- }
- if (this.options.closeOnClick === true && this.options.contentOverlay === true) {
- this.$overlay.removeClass('is-closable');
- }
- this.$triggers.attr('aria-expanded', 'false');
- if (this.options.trapFocus === true) {
- this.$element.siblings('[data-off-canvas-content]').removeAttr('tabindex');
- Foundation.Keyboard.releaseFocus(this.$element);
- }
- }
- /**
- * Toggles the off-canvas menu open or closed.
- * @function
- * @param {Object} event - Event object passed from listener.
- * @param {jQuery} trigger - element that triggered the off-canvas to open.
- */
- toggle(event, trigger) {
- if (this.$element.hasClass('is-open')) {
- this.close(event, trigger);
- }
- else {
- this.open(event, trigger);
- }
- }
- /**
- * Handles keyboard input when detected. When the escape key is pressed, the off-canvas menu closes, and focus is restored to the element that opened the menu.
- * @function
- * @private
- */
- _handleKeyboard(e) {
- Foundation.Keyboard.handleKey(e, 'OffCanvas', {
- close: () => {
- this.close();
- this.$lastTrigger.focus();
- return true;
- },
- handled: () => {
- e.stopPropagation();
- e.preventDefault();
- }
- });
- }
- /**
- * Destroys the offcanvas plugin.
- * @function
- */
- destroy() {
- this.close();
- this.$element.off('.zf.trigger .zf.offcanvas');
- this.$overlay.off('.zf.offcanvas');
- Foundation.unregisterPlugin(this);
- }
- }
- OffCanvas.defaults = {
- /**
- * Allow the user to click outside of the menu to close it.
- * @option
- * @type {boolean}
- * @default true
- */
- closeOnClick: true,
- /**
- * Adds an overlay on top of `[data-off-canvas-content]`.
- * @option
- * @type {boolean}
- * @default true
- */
- contentOverlay: true,
- /**
- * Enable/disable scrolling of the main content when an off canvas panel is open.
- * @option
- * @type {boolean}
- * @default true
- */
- contentScroll: true,
- /**
- * Amount of time in ms the open and close transition requires. If none selected, pulls from body style.
- * @option
- * @type {number}
- * @default 0
- */
- transitionTime: 0,
- /**
- * Type of transition for the offcanvas menu. Options are 'push', 'detached' or 'slide'.
- * @option
- * @type {string}
- * @default push
- */
- transition: 'push',
- /**
- * Force the page to scroll to top or bottom on open.
- * @option
- * @type {?string}
- * @default null
- */
- forceTo: null,
- /**
- * Allow the offcanvas to remain open for certain breakpoints.
- * @option
- * @type {boolean}
- * @default false
- */
- isRevealed: false,
- /**
- * Breakpoint at which to reveal. JS will use a RegExp to target standard classes, if changing classnames, pass your class with the `revealClass` option.
- * @option
- * @type {?string}
- * @default null
- */
- revealOn: null,
- /**
- * Force focus to the offcanvas on open. If true, will focus the opening trigger on close.
- * @option
- * @type {boolean}
- * @default true
- */
- autoFocus: true,
- /**
- * Class used to force an offcanvas to remain open. Foundation defaults for this are `reveal-for-large` & `reveal-for-medium`.
- * @option
- * @type {string}
- * @default reveal-for-
- * @todo improve the regex testing for this.
- */
- revealClass: 'reveal-for-',
- /**
- * Triggers optional focus trapping when opening an offcanvas. Sets tabindex of [data-off-canvas-content] to -1 for accessibility purposes.
- * @option
- * @type {boolean}
- * @default false
- */
- trapFocus: false
- }
- // Window exports
- Foundation.plugin(OffCanvas, 'OffCanvas');
- }(jQuery);