623 lines
19 KiB
JavaScript
623 lines
19 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
|
||
|
|
||
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
||
|
|
||
|
!function ($) {
|
||
|
|
||
|
/**
|
||
|
* Reveal module.
|
||
|
* @module foundation.reveal
|
||
|
* @requires foundation.util.keyboard
|
||
|
* @requires foundation.util.box
|
||
|
* @requires foundation.util.triggers
|
||
|
* @requires foundation.util.mediaQuery
|
||
|
* @requires foundation.util.motion if using animations
|
||
|
*/
|
||
|
|
||
|
var Reveal = function () {
|
||
|
/**
|
||
|
* Creates a new instance of Reveal.
|
||
|
* @class
|
||
|
* @param {jQuery} element - jQuery object to use for the modal.
|
||
|
* @param {Object} options - optional parameters.
|
||
|
*/
|
||
|
|
||
|
function Reveal(element, options) {
|
||
|
_classCallCheck(this, Reveal);
|
||
|
|
||
|
this.$element = element;
|
||
|
this.options = $.extend({}, Reveal.defaults, this.$element.data(), options);
|
||
|
this._init();
|
||
|
|
||
|
Foundation.registerPlugin(this, 'Reveal');
|
||
|
Foundation.Keyboard.register('Reveal', {
|
||
|
'ENTER': 'open',
|
||
|
'SPACE': 'open',
|
||
|
'ESCAPE': 'close',
|
||
|
'TAB': 'tab_forward',
|
||
|
'SHIFT_TAB': 'tab_backward'
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Initializes the modal by adding the overlay and close buttons, (if selected).
|
||
|
* @private
|
||
|
*/
|
||
|
|
||
|
|
||
|
_createClass(Reveal, [{
|
||
|
key: '_init',
|
||
|
value: function _init() {
|
||
|
this.id = this.$element.attr('id');
|
||
|
this.isActive = false;
|
||
|
this.cached = { mq: Foundation.MediaQuery.current };
|
||
|
this.isMobile = mobileSniff();
|
||
|
|
||
|
this.$anchor = $('[data-open="' + this.id + '"]').length ? $('[data-open="' + this.id + '"]') : $('[data-toggle="' + this.id + '"]');
|
||
|
this.$anchor.attr({
|
||
|
'aria-controls': this.id,
|
||
|
'aria-haspopup': true,
|
||
|
'tabindex': 0
|
||
|
});
|
||
|
|
||
|
if (this.options.fullScreen || this.$element.hasClass('full')) {
|
||
|
this.options.fullScreen = true;
|
||
|
this.options.overlay = false;
|
||
|
}
|
||
|
if (this.options.overlay && !this.$overlay) {
|
||
|
this.$overlay = this._makeOverlay(this.id);
|
||
|
}
|
||
|
|
||
|
this.$element.attr({
|
||
|
'role': 'dialog',
|
||
|
'aria-hidden': true,
|
||
|
'data-yeti-box': this.id,
|
||
|
'data-resize': this.id
|
||
|
});
|
||
|
|
||
|
if (this.$overlay) {
|
||
|
this.$element.detach().appendTo(this.$overlay);
|
||
|
} else {
|
||
|
this.$element.detach().appendTo($('body'));
|
||
|
this.$element.addClass('without-overlay');
|
||
|
}
|
||
|
this._events();
|
||
|
if (this.options.deepLink && window.location.hash === '#' + this.id) {
|
||
|
$(window).one('load.zf.reveal', this.open.bind(this));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates an overlay div to display behind the modal.
|
||
|
* @private
|
||
|
*/
|
||
|
|
||
|
}, {
|
||
|
key: '_makeOverlay',
|
||
|
value: function _makeOverlay(id) {
|
||
|
var $overlay = $('<div></div>').addClass('reveal-overlay').appendTo('body');
|
||
|
return $overlay;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Updates position of modal
|
||
|
* TODO: Figure out if we actually need to cache these values or if it doesn't matter
|
||
|
* @private
|
||
|
*/
|
||
|
|
||
|
}, {
|
||
|
key: '_updatePosition',
|
||
|
value: function _updatePosition() {
|
||
|
var width = this.$element.outerWidth();
|
||
|
var outerWidth = $(window).width();
|
||
|
var height = this.$element.outerHeight();
|
||
|
var outerHeight = $(window).height();
|
||
|
var left, top;
|
||
|
if (this.options.hOffset === 'auto') {
|
||
|
left = parseInt((outerWidth - width) / 2, 10);
|
||
|
} else {
|
||
|
left = parseInt(this.options.hOffset, 10);
|
||
|
}
|
||
|
if (this.options.vOffset === 'auto') {
|
||
|
if (height > outerHeight) {
|
||
|
top = parseInt(Math.min(100, outerHeight / 10), 10);
|
||
|
} else {
|
||
|
top = parseInt((outerHeight - height) / 4, 10);
|
||
|
}
|
||
|
} else {
|
||
|
top = parseInt(this.options.vOffset, 10);
|
||
|
}
|
||
|
this.$element.css({ top: top + 'px' });
|
||
|
// only worry about left if we don't have an overlay or we havea horizontal offset,
|
||
|
// otherwise we're perfectly in the middle
|
||
|
if (!this.$overlay || this.options.hOffset !== 'auto') {
|
||
|
this.$element.css({ left: left + 'px' });
|
||
|
this.$element.css({ margin: '0px' });
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Adds event handlers for the modal.
|
||
|
* @private
|
||
|
*/
|
||
|
|
||
|
}, {
|
||
|
key: '_events',
|
||
|
value: function _events() {
|
||
|
var _this2 = this;
|
||
|
|
||
|
var _this = this;
|
||
|
|
||
|
this.$element.on({
|
||
|
'open.zf.trigger': this.open.bind(this),
|
||
|
'close.zf.trigger': function (event, $element) {
|
||
|
if (event.target === _this.$element[0] || $(event.target).parents('[data-closable]')[0] === $element) {
|
||
|
// only close reveal when it's explicitly called
|
||
|
return _this2.close.apply(_this2);
|
||
|
}
|
||
|
},
|
||
|
'toggle.zf.trigger': this.toggle.bind(this),
|
||
|
'resizeme.zf.trigger': function () {
|
||
|
_this._updatePosition();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
if (this.$anchor.length) {
|
||
|
this.$anchor.on('keydown.zf.reveal', function (e) {
|
||
|
if (e.which === 13 || e.which === 32) {
|
||
|
e.stopPropagation();
|
||
|
e.preventDefault();
|
||
|
_this.open();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (this.options.closeOnClick && this.options.overlay) {
|
||
|
this.$overlay.off('.zf.reveal').on('click.zf.reveal', function (e) {
|
||
|
if (e.target === _this.$element[0] || $.contains(_this.$element[0], e.target)) {
|
||
|
return;
|
||
|
}
|
||
|
_this.close();
|
||
|
});
|
||
|
}
|
||
|
if (this.options.deepLink) {
|
||
|
$(window).on('popstate.zf.reveal:' + this.id, this._handleState.bind(this));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Handles modal methods on back/forward button clicks or any other event that triggers popstate.
|
||
|
* @private
|
||
|
*/
|
||
|
|
||
|
}, {
|
||
|
key: '_handleState',
|
||
|
value: function _handleState(e) {
|
||
|
if (window.location.hash === '#' + this.id && !this.isActive) {
|
||
|
this.open();
|
||
|
} else {
|
||
|
this.close();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Opens the modal controlled by `this.$anchor`, and closes all others by default.
|
||
|
* @function
|
||
|
* @fires Reveal#closeme
|
||
|
* @fires Reveal#open
|
||
|
*/
|
||
|
|
||
|
}, {
|
||
|
key: 'open',
|
||
|
value: function open() {
|
||
|
var _this3 = this;
|
||
|
|
||
|
if (this.options.deepLink) {
|
||
|
var hash = '#' + this.id;
|
||
|
|
||
|
if (window.history.pushState) {
|
||
|
window.history.pushState(null, null, hash);
|
||
|
} else {
|
||
|
window.location.hash = hash;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
this.isActive = true;
|
||
|
|
||
|
// Make elements invisible, but remove display: none so we can get size and positioning
|
||
|
this.$element.css({ 'visibility': 'hidden' }).show().scrollTop(0);
|
||
|
if (this.options.overlay) {
|
||
|
this.$overlay.css({ 'visibility': 'hidden' }).show();
|
||
|
}
|
||
|
|
||
|
this._updatePosition();
|
||
|
|
||
|
this.$element.hide().css({ 'visibility': '' });
|
||
|
|
||
|
if (this.$overlay) {
|
||
|
this.$overlay.css({ 'visibility': '' }).hide();
|
||
|
if (this.$element.hasClass('fast')) {
|
||
|
this.$overlay.addClass('fast');
|
||
|
} else if (this.$element.hasClass('slow')) {
|
||
|
this.$overlay.addClass('slow');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!this.options.multipleOpened) {
|
||
|
/**
|
||
|
* Fires immediately before the modal opens.
|
||
|
* Closes any other modals that are currently open
|
||
|
* @event Reveal#closeme
|
||
|
*/
|
||
|
this.$element.trigger('closeme.zf.reveal', this.id);
|
||
|
}
|
||
|
// Motion UI method of reveal
|
||
|
if (this.options.animationIn) {
|
||
|
var _this;
|
||
|
|
||
|
(function () {
|
||
|
var afterAnimationFocus = function () {
|
||
|
_this.$element.attr({
|
||
|
'aria-hidden': false,
|
||
|
'tabindex': -1
|
||
|
}).focus();
|
||
|
console.log('focus');
|
||
|
};
|
||
|
|
||
|
_this = _this3;
|
||
|
|
||
|
if (_this3.options.overlay) {
|
||
|
Foundation.Motion.animateIn(_this3.$overlay, 'fade-in');
|
||
|
}
|
||
|
Foundation.Motion.animateIn(_this3.$element, _this3.options.animationIn, function () {
|
||
|
_this3.focusableElements = Foundation.Keyboard.findFocusable(_this3.$element);
|
||
|
afterAnimationFocus();
|
||
|
});
|
||
|
})();
|
||
|
}
|
||
|
// jQuery method of reveal
|
||
|
else {
|
||
|
if (this.options.overlay) {
|
||
|
this.$overlay.show(0);
|
||
|
}
|
||
|
this.$element.show(this.options.showDelay);
|
||
|
}
|
||
|
|
||
|
// handle accessibility
|
||
|
this.$element.attr({
|
||
|
'aria-hidden': false,
|
||
|
'tabindex': -1
|
||
|
}).focus();
|
||
|
|
||
|
/**
|
||
|
* Fires when the modal has successfully opened.
|
||
|
* @event Reveal#open
|
||
|
*/
|
||
|
this.$element.trigger('open.zf.reveal');
|
||
|
|
||
|
if (this.isMobile) {
|
||
|
this.originalScrollPos = window.pageYOffset;
|
||
|
$('html, body').addClass('is-reveal-open');
|
||
|
} else {
|
||
|
$('body').addClass('is-reveal-open');
|
||
|
}
|
||
|
|
||
|
setTimeout(function () {
|
||
|
_this3._extraHandlers();
|
||
|
}, 0);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Adds extra event handlers for the body and window if necessary.
|
||
|
* @private
|
||
|
*/
|
||
|
|
||
|
}, {
|
||
|
key: '_extraHandlers',
|
||
|
value: function _extraHandlers() {
|
||
|
var _this = this;
|
||
|
this.focusableElements = Foundation.Keyboard.findFocusable(this.$element);
|
||
|
|
||
|
if (!this.options.overlay && this.options.closeOnClick && !this.options.fullScreen) {
|
||
|
$('body').on('click.zf.reveal', function (e) {
|
||
|
if (e.target === _this.$element[0] || $.contains(_this.$element[0], e.target)) {
|
||
|
return;
|
||
|
}
|
||
|
_this.close();
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (this.options.closeOnEsc) {
|
||
|
$(window).on('keydown.zf.reveal', function (e) {
|
||
|
Foundation.Keyboard.handleKey(e, 'Reveal', {
|
||
|
close: function () {
|
||
|
if (_this.options.closeOnEsc) {
|
||
|
_this.close();
|
||
|
_this.$anchor.focus();
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// lock focus within modal while tabbing
|
||
|
this.$element.on('keydown.zf.reveal', function (e) {
|
||
|
var $target = $(this);
|
||
|
// handle keyboard event with keyboard util
|
||
|
Foundation.Keyboard.handleKey(e, 'Reveal', {
|
||
|
tab_forward: function () {
|
||
|
if (_this.$element.find(':focus').is(_this.focusableElements.eq(-1))) {
|
||
|
// left modal downwards, setting focus to first element
|
||
|
_this.focusableElements.eq(0).focus();
|
||
|
return true;
|
||
|
}
|
||
|
if (_this.focusableElements.length === 0) {
|
||
|
// no focusable elements inside the modal at all, prevent tabbing in general
|
||
|
return true;
|
||
|
}
|
||
|
},
|
||
|
tab_backward: function () {
|
||
|
if (_this.$element.find(':focus').is(_this.focusableElements.eq(0)) || _this.$element.is(':focus')) {
|
||
|
// left modal upwards, setting focus to last element
|
||
|
_this.focusableElements.eq(-1).focus();
|
||
|
return true;
|
||
|
}
|
||
|
if (_this.focusableElements.length === 0) {
|
||
|
// no focusable elements inside the modal at all, prevent tabbing in general
|
||
|
return true;
|
||
|
}
|
||
|
},
|
||
|
open: function () {
|
||
|
if (_this.$element.find(':focus').is(_this.$element.find('[data-close]'))) {
|
||
|
setTimeout(function () {
|
||
|
// set focus back to anchor if close button has been activated
|
||
|
_this.$anchor.focus();
|
||
|
}, 1);
|
||
|
} else if ($target.is(_this.focusableElements)) {
|
||
|
// dont't trigger if acual element has focus (i.e. inputs, links, ...)
|
||
|
_this.open();
|
||
|
}
|
||
|
},
|
||
|
close: function () {
|
||
|
if (_this.options.closeOnEsc) {
|
||
|
_this.close();
|
||
|
_this.$anchor.focus();
|
||
|
}
|
||
|
},
|
||
|
handled: function (preventDefault) {
|
||
|
if (preventDefault) {
|
||
|
e.preventDefault();
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Closes the modal.
|
||
|
* @function
|
||
|
* @fires Reveal#closed
|
||
|
*/
|
||
|
|
||
|
}, {
|
||
|
key: 'close',
|
||
|
value: function close() {
|
||
|
if (!this.isActive || !this.$element.is(':visible')) {
|
||
|
return false;
|
||
|
}
|
||
|
var _this = this;
|
||
|
|
||
|
// Motion UI method of hiding
|
||
|
if (this.options.animationOut) {
|
||
|
if (this.options.overlay) {
|
||
|
Foundation.Motion.animateOut(this.$overlay, 'fade-out', finishUp);
|
||
|
} else {
|
||
|
finishUp();
|
||
|
}
|
||
|
|
||
|
Foundation.Motion.animateOut(this.$element, this.options.animationOut);
|
||
|
}
|
||
|
// jQuery method of hiding
|
||
|
else {
|
||
|
if (this.options.overlay) {
|
||
|
this.$overlay.hide(0, finishUp);
|
||
|
} else {
|
||
|
finishUp();
|
||
|
}
|
||
|
|
||
|
this.$element.hide(this.options.hideDelay);
|
||
|
}
|
||
|
|
||
|
// Conditionals to remove extra event listeners added on open
|
||
|
if (this.options.closeOnEsc) {
|
||
|
$(window).off('keydown.zf.reveal');
|
||
|
}
|
||
|
|
||
|
if (!this.options.overlay && this.options.closeOnClick) {
|
||
|
$('body').off('click.zf.reveal');
|
||
|
}
|
||
|
|
||
|
this.$element.off('keydown.zf.reveal');
|
||
|
|
||
|
function finishUp() {
|
||
|
if (_this.isMobile) {
|
||
|
$('html, body').removeClass('is-reveal-open');
|
||
|
if (_this.originalScrollPos) {
|
||
|
$('body').scrollTop(_this.originalScrollPos);
|
||
|
_this.originalScrollPos = null;
|
||
|
}
|
||
|
} else {
|
||
|
$('body').removeClass('is-reveal-open');
|
||
|
}
|
||
|
|
||
|
_this.$element.attr('aria-hidden', true);
|
||
|
|
||
|
/**
|
||
|
* Fires when the modal is done closing.
|
||
|
* @event Reveal#closed
|
||
|
*/
|
||
|
_this.$element.trigger('closed.zf.reveal');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Resets the modal content
|
||
|
* This prevents a running video to keep going in the background
|
||
|
*/
|
||
|
if (this.options.resetOnClose) {
|
||
|
this.$element.html(this.$element.html());
|
||
|
}
|
||
|
|
||
|
this.isActive = false;
|
||
|
if (_this.options.deepLink) {
|
||
|
if (window.history.replaceState) {
|
||
|
window.history.replaceState("", document.title, window.location.pathname);
|
||
|
} else {
|
||
|
window.location.hash = '';
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Toggles the open/closed state of a modal.
|
||
|
* @function
|
||
|
*/
|
||
|
|
||
|
}, {
|
||
|
key: 'toggle',
|
||
|
value: function toggle() {
|
||
|
if (this.isActive) {
|
||
|
this.close();
|
||
|
} else {
|
||
|
this.open();
|
||
|
}
|
||
|
}
|
||
|
}, {
|
||
|
key: 'destroy',
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Destroys an instance of a modal.
|
||
|
* @function
|
||
|
*/
|
||
|
value: function destroy() {
|
||
|
if (this.options.overlay) {
|
||
|
this.$element.appendTo($('body')); // move $element outside of $overlay to prevent error unregisterPlugin()
|
||
|
this.$overlay.hide().off().remove();
|
||
|
}
|
||
|
this.$element.hide().off();
|
||
|
this.$anchor.off('.zf');
|
||
|
$(window).off('.zf.reveal:' + this.id);
|
||
|
|
||
|
Foundation.unregisterPlugin(this);
|
||
|
}
|
||
|
}]);
|
||
|
|
||
|
return Reveal;
|
||
|
}();
|
||
|
|
||
|
Reveal.defaults = {
|
||
|
/**
|
||
|
* Motion-UI class to use for animated elements. If none used, defaults to simple show/hide.
|
||
|
* @option
|
||
|
* @example 'slide-in-left'
|
||
|
*/
|
||
|
animationIn: '',
|
||
|
/**
|
||
|
* Motion-UI class to use for animated elements. If none used, defaults to simple show/hide.
|
||
|
* @option
|
||
|
* @example 'slide-out-right'
|
||
|
*/
|
||
|
animationOut: '',
|
||
|
/**
|
||
|
* Time, in ms, to delay the opening of a modal after a click if no animation used.
|
||
|
* @option
|
||
|
* @example 10
|
||
|
*/
|
||
|
showDelay: 0,
|
||
|
/**
|
||
|
* Time, in ms, to delay the closing of a modal after a click if no animation used.
|
||
|
* @option
|
||
|
* @example 10
|
||
|
*/
|
||
|
hideDelay: 0,
|
||
|
/**
|
||
|
* Allows a click on the body/overlay to close the modal.
|
||
|
* @option
|
||
|
* @example true
|
||
|
*/
|
||
|
closeOnClick: true,
|
||
|
/**
|
||
|
* Allows the modal to close if the user presses the `ESCAPE` key.
|
||
|
* @option
|
||
|
* @example true
|
||
|
*/
|
||
|
closeOnEsc: true,
|
||
|
/**
|
||
|
* If true, allows multiple modals to be displayed at once.
|
||
|
* @option
|
||
|
* @example false
|
||
|
*/
|
||
|
multipleOpened: false,
|
||
|
/**
|
||
|
* Distance, in pixels, the modal should push down from the top of the screen.
|
||
|
* @option
|
||
|
* @example auto
|
||
|
*/
|
||
|
vOffset: 'auto',
|
||
|
/**
|
||
|
* Distance, in pixels, the modal should push in from the side of the screen.
|
||
|
* @option
|
||
|
* @example auto
|
||
|
*/
|
||
|
hOffset: 'auto',
|
||
|
/**
|
||
|
* Allows the modal to be fullscreen, completely blocking out the rest of the view. JS checks for this as well.
|
||
|
* @option
|
||
|
* @example false
|
||
|
*/
|
||
|
fullScreen: false,
|
||
|
/**
|
||
|
* Percentage of screen height the modal should push up from the bottom of the view.
|
||
|
* @option
|
||
|
* @example 10
|
||
|
*/
|
||
|
btmOffsetPct: 10,
|
||
|
/**
|
||
|
* Allows the modal to generate an overlay div, which will cover the view when modal opens.
|
||
|
* @option
|
||
|
* @example true
|
||
|
*/
|
||
|
overlay: true,
|
||
|
/**
|
||
|
* Allows the modal to remove and reinject markup on close. Should be true if using video elements w/o using provider's api, otherwise, videos will continue to play in the background.
|
||
|
* @option
|
||
|
* @example false
|
||
|
*/
|
||
|
resetOnClose: false,
|
||
|
/**
|
||
|
* Allows the modal to alter the url on open/close, and allows the use of the `back` button to close modals. ALSO, allows a modal to auto-maniacally open on page load IF the hash === the modal's user-set id.
|
||
|
* @option
|
||
|
* @example false
|
||
|
*/
|
||
|
deepLink: false
|
||
|
};
|
||
|
|
||
|
// Window exports
|
||
|
Foundation.plugin(Reveal, 'Reveal');
|
||
|
|
||
|
function iPhoneSniff() {
|
||
|
return (/iP(ad|hone|od).*OS/.test(window.navigator.userAgent)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function androidSniff() {
|
||
|
return (/Android/.test(window.navigator.userAgent)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function mobileSniff() {
|
||
|
return iPhoneSniff() || androidSniff();
|
||
|
}
|
||
|
}(jQuery);
|