sp-magic.de/bower_components/foundation-sites/js/foundation.offcanvas.js
2016-10-28 19:56:41 +02:00

398 lines
11 KiB
JavaScript

'use strict';
!function($) {
/**
* OffCanvas module.
* @module foundation.offcanvas
* @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');
}
/**
* 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');
// 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 a close trigger over the body if necessary
if (this.options.closeOnClick) {
if ($('.js-off-canvas-exit').length) {
this.$exiter = $('.js-off-canvas-exit');
} else {
var exiter = document.createElement('div');
exiter.setAttribute('class', 'js-off-canvas-exit');
$('[data-off-canvas-content]').append(exiter);
this.$exiter = $(exiter);
}
}
this.options.isRevealed = this.options.isRevealed || new RegExp(this.options.revealClass, 'g').test(this.$element[0].className);
if (this.options.isRevealed) {
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) {
this.options.transitionTime = parseFloat(window.getComputedStyle($('[data-off-canvas-wrapper]')[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 && this.$exiter.length) {
this.$exiter.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;
// if (!this.options.forceTop) {
// var scrollPos = parseInt(window.pageYOffset);
// this.$element[0].style.transform = 'translate(0,' + scrollPos + 'px)';
// }
// if (this.options.isSticky) { this._stick(); }
this.$element.off('open.zf.trigger toggle.zf.trigger');
if ($closer.length) { $closer.hide(); }
} else {
this.isRevealed = false;
// if (this.options.isSticky || !this.options.forceTop) {
// this.$element[0].style.transform = '';
// $(window).off('scroll.zf.offcanvas');
// }
this.$element.on({
'open.zf.trigger': this.open.bind(this),
'toggle.zf.trigger': this.toggle.bind(this)
});
if ($closer.length) {
$closer.show();
}
}
}
/**
* 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,
$body = $(document.body);
if (this.options.forceTop) {
$('body').scrollTop(0);
}
// window.pageYOffset = 0;
// if (!this.options.forceTop) {
// var scrollPos = parseInt(window.pageYOffset);
// this.$element[0].style.transform = 'translate(0,' + scrollPos + 'px)';
// if (this.$exiter.length) {
// this.$exiter[0].style.transform = 'translate(0,' + scrollPos + 'px)';
// }
// }
/**
* Fires when the off-canvas menu opens.
* @event OffCanvas#opened
*/
Foundation.Move(this.options.transitionTime, this.$element, function() {
$('[data-off-canvas-wrapper]').addClass('is-off-canvas-open is-open-'+ _this.options.position);
_this.$element
.addClass('is-open')
// if (_this.options.isSticky) {
// _this._stick();
// }
});
this.$triggers.attr('aria-expanded', 'true');
this.$element.attr('aria-hidden', 'false')
.trigger('opened.zf.offcanvas');
if (this.options.closeOnClick) {
this.$exiter.addClass('is-visible');
}
if (trigger) {
this.$lastTrigger = trigger;
}
if (this.options.autoFocus) {
this.$element.one(Foundation.transitionend(this.$element), function() {
_this.$element.find('a, button').eq(0).focus();
});
}
if (this.options.trapFocus) {
$('[data-off-canvas-content]').attr('tabindex', '-1');
this._trapFocus();
}
}
/**
* Traps focus within the offcanvas on open.
* @private
*/
_trapFocus() {
var focusable = Foundation.Keyboard.findFocusable(this.$element),
first = focusable.eq(0),
last = focusable.eq(-1);
focusable.off('.zf.offcanvas').on('keydown.zf.offcanvas', function(e) {
if (e.which === 9 || e.keycode === 9) {
if (e.target === last[0] && !e.shiftKey) {
e.preventDefault();
first.focus();
}
if (e.target === first[0] && e.shiftKey) {
e.preventDefault();
last.focus();
}
}
});
}
/**
* Allows the offcanvas to appear sticky utilizing translate properties.
* @private
*/
// OffCanvas.prototype._stick = function() {
// var elStyle = this.$element[0].style;
//
// if (this.options.closeOnClick) {
// var exitStyle = this.$exiter[0].style;
// }
//
// $(window).on('scroll.zf.offcanvas', function(e) {
// console.log(e);
// var pageY = window.pageYOffset;
// elStyle.transform = 'translate(0,' + pageY + 'px)';
// if (exitStyle !== undefined) { exitStyle.transform = 'translate(0,' + pageY + 'px)'; }
// });
// // this.$element.trigger('stuck.zf.offcanvas');
// };
/**
* 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;
// Foundation.Move(this.options.transitionTime, this.$element, function() {
$('[data-off-canvas-wrapper]').removeClass(`is-off-canvas-open is-open-${_this.options.position}`);
_this.$element.removeClass('is-open');
// Foundation._reflow();
// });
this.$element.attr('aria-hidden', 'true')
/**
* Fires when the off-canvas menu opens.
* @event OffCanvas#closed
*/
.trigger('closed.zf.offcanvas');
// if (_this.options.isSticky || !_this.options.forceTop) {
// setTimeout(function() {
// _this.$element[0].style.transform = '';
// $(window).off('scroll.zf.offcanvas');
// }, this.options.transitionTime);
// }
if (this.options.closeOnClick) {
this.$exiter.removeClass('is-visible');
}
this.$triggers.attr('aria-expanded', 'false');
if (this.options.trapFocus) {
$('[data-off-canvas-content]').removeAttr('tabindex');
}
}
/**
* 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(event) {
if (event.which !== 27) return;
event.stopPropagation();
event.preventDefault();
this.close();
this.$lastTrigger.focus();
}
/**
* Destroys the offcanvas plugin.
* @function
*/
destroy() {
this.close();
this.$element.off('.zf.trigger .zf.offcanvas');
this.$exiter.off('.zf.offcanvas');
Foundation.unregisterPlugin(this);
}
}
OffCanvas.defaults = {
/**
* Allow the user to click outside of the menu to close it.
* @option
* @example true
*/
closeOnClick: true,
/**
* Amount of time in ms the open and close transition requires. If none selected, pulls from body style.
* @option
* @example 500
*/
transitionTime: 0,
/**
* Direction the offcanvas opens from. Determines class applied to body.
* @option
* @example left
*/
position: 'left',
/**
* Force the page to scroll to top on open.
* @option
* @example true
*/
forceTop: true,
/**
* Allow the offcanvas to remain open for certain breakpoints.
* @option
* @example 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
* @example reveal-for-large
*/
revealOn: null,
/**
* Force focus to the offcanvas on open. If true, will focus the opening trigger on close.
* @option
* @example true
*/
autoFocus: true,
/**
* Class used to force an offcanvas to remain open. Foundation defaults for this are `reveal-for-large` & `reveal-for-medium`.
* @option
* TODO improve the regex testing for this.
* @example reveal-for-large
*/
revealClass: 'reveal-for-',
/**
* Triggers optional focus trapping when opening an offcanvas. Sets tabindex of [data-off-canvas-content] to -1 for accessibility purposes.
* @option
* @example true
*/
trapFocus: false
}
// Window exports
Foundation.plugin(OffCanvas, 'OffCanvas');
}(jQuery);