govlab-styleguide
Version:
A styleguide / lightweight frontend framework for GovLab websites
479 lines (439 loc) • 14.3 kB
JavaScript
/**
* 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
*/
!function(Foundation, $) {
'use strict';
/**
* 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) {
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'
});
}
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 100
*/
vOffset: 100,
/**
* Distance, in pixels, the modal should push in from the side of the screen.
* @option
* @example 0
*/
hOffset: 0,
/**
* 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.
* @option
* @example false
*/
resetOnClose: false
};
/**
* Initializes the modal by adding the overlay and close buttons, (if selected).
* @private
*/
Reveal.prototype._init = function(){
this.id = this.$element.attr('id');
this.isActive = false;
this.$anchor = $('[data-open="' + this.id + '"]').length ? $('[data-open="' + this.id + '"]') : $('[data-toggle="' + this.id + '"]');
if(this.$anchor.length){
var anchorId = this.$anchor[0].id || Foundation.GetYoDigits(6, 'reveal');
this.$anchor.attr({
'aria-controls': this.id,
'id': anchorId,
'aria-haspopup': true,
'tabindex': 0
});
this.$element.attr({'aria-labelledby': anchorId});
}
// this.options.fullScreen = this.$element.hasClass('full');
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
});
this._events();
};
/**
* Creates an overlay div to display behind the modal.
* @private
*/
Reveal.prototype._makeOverlay = function(id){
var $overlay = $('<div></div>')
.addClass('reveal-overlay')
.attr({'tabindex': -1, 'aria-hidden': true})
.appendTo('body');
if(this.options.closeOnClick){
$overlay.attr({
'data-close': id
});
}
return $overlay;
};
/**
* Adds event handlers for the modal.
* @private
*/
Reveal.prototype._events = function(){
var _this = this;
this.$element.on({
'open.zf.trigger': this.open.bind(this),
'close.zf.trigger': this.close.bind(this),
'toggle.zf.trigger': this.toggle.bind(this),
'resizeme.zf.trigger': function(){
if(_this.$element.is(':visible')){
_this._setPosition(function(){});
}
}
});
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', this.close.bind(this));
}
};
/**
* Sets the position of the modal before opening
* @param {Function} cb - a callback function to execute when positioning is complete.
* @private
*/
Reveal.prototype._setPosition = function(cb){
var eleDims = Foundation.Box.GetDimensions(this.$element);
var elePos = this.options.fullScreen ? 'reveal full' : (eleDims.height >= (0.5 * eleDims.windowDims.height)) ? 'reveal' : 'center';
if(elePos === 'reveal full'){
//set to full height/width
this.$element
.offset(Foundation.Box.GetOffsets(this.$element, null, elePos, this.options.vOffset))
.css({
'height': eleDims.windowDims.height,
'width': eleDims.windowDims.width
});
}else if(!Foundation.MediaQuery.atLeast('medium') || !Foundation.Box.ImNotTouchingYou(this.$element, null, true, false)){
//if smaller than medium, resize to 100% width minus any custom L/R margin
this.$element
.css({
'width': eleDims.windowDims.width - (this.options.hOffset * 2)
})
.offset(Foundation.Box.GetOffsets(this.$element, null, 'center', this.options.vOffset, this.options.hOffset));
//flag a boolean so we can reset the size after the element is closed.
this.changedSize = true;
}else{
this.$element
.css({
'max-height': eleDims.windowDims.height - (this.options.vOffset * (this.options.btmOffsetPct / 100 + 1)),
'width': ''
})
.offset(Foundation.Box.GetOffsets(this.$element, null, elePos, this.options.vOffset));
//the max height based on a percentage of vertical offset plus vertical offset
}
cb();
};
/**
* Opens the modal controlled by `this.$anchor`, and closes all others by default.
* @function
* @fires Reveal#closeme
* @fires Reveal#open
*/
Reveal.prototype.open = function(){
var _this = this;
this.isActive = true;
//make element invisible, but remove display: none so we can get size and positioning
this.$element
.css({'visibility': 'hidden'})
.show()
.scrollTop(0);
this._setPosition(function(){
_this.$element.hide()
.css({'visibility': ''});
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);
}
if(_this.options.animationIn){
if(_this.options.overlay){
Foundation.Motion.animateIn(_this.$overlay, 'fade-in', function(){
Foundation.Motion.animateIn(_this.$element, _this.options.animationIn, function(){
_this.focusableElements = Foundation.Keyboard.findFocusable(_this.$element);
});
});
}else{
Foundation.Motion.animateIn(_this.$element, _this.options.animationIn, function(){
_this.focusableElements = Foundation.Keyboard.findFocusable(_this.$element);
});
}
}else{
if(_this.options.overlay){
_this.$overlay.show(0, function(){
_this.$element.show(_this.options.showDelay, function(){
});
});
}else{
_this.$element.show(_this.options.showDelay, function(){
});
}
}
});
// handle accessibility
this.$element.attr({'aria-hidden': false}).attr('tabindex', -1).focus()
/**
* Fires when the modal has successfully opened.
* @event Reveal#open
*/
.trigger('open.zf.reveal');
$('body').addClass('is-reveal-open')
.attr({'aria-hidden': (this.options.overlay || this.options.fullScreen) ? true : false});
setTimeout(function(){
_this._extraHandlers();
// Foundation.reflow();
}, 0);
};
/**
* Adds extra event handlers for the body and window if necessary.
* @private
*/
Reveal.prototype._extraHandlers = function(){
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()
_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();
}
}
});
if (_this.focusableElements.length === 0) { // no focusable elements inside the modal at all, prevent tabbing in general
e.preventDefault();
}
});
}
// 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();
e.preventDefault();
}
},
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();
e.preventDefault();
}
},
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();
}
}
});
});
};
/**
* Closes the modal.
* @function
* @fires Reveal#closed
*/
Reveal.prototype.close = function(){
if(!this.isActive || !this.$element.is(':visible')){
return false;
}
var _this = this;
if(this.options.animationOut){
Foundation.Motion.animateOut(this.$element, this.options.animationOut, function(){
if(_this.options.overlay){
Foundation.Motion.animateOut(_this.$overlay, 'fade-out', function(){
});
}
});
}else{
this.$element.hide(_this.options.hideDelay, function(){
if(_this.options.overlay){
_this.$overlay.hide(0, function(){
});
}
});
}
//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');
//if the modal changed size, reset it
if(this.changedSize){
this.$element.css({
'height': '',
'width': ''
});
}
$('body').removeClass('is-reveal-open').attr({'aria-hidden': false, 'tabindex': ''});
/**
* 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;
this.$element.attr({'aria-hidden': true})
/**
* Fires when the modal is done closing.
* @event Reveal#closed
*/
.trigger('closed.zf.reveal');
};
/**
* Toggles the open/closed state of a modal.
* @function
*/
Reveal.prototype.toggle = function(){
if(this.isActive){
this.close();
}else{
this.open();
}
};
/**
* Destroys an instance of a modal.
* @function
*/
Reveal.prototype.destroy = function() {
if(this.options.overlay){
this.$overlay.hide().off().remove();
}
this.$element.hide();
this.$anchor.off();
Foundation.unregisterPlugin(this);
};
Foundation.plugin(Reveal, 'Reveal');
// Exports for AMD/Browserify
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined')
module.exports = Reveal;
if (typeof define === 'function')
define(['foundation'], function() {
return Reveal;
});
}(Foundation, jQuery);