/*!
* jQuery Popup Overlay
*
* @requires jQuery v1.7.1+
* @link https://vast-engineering.github.com/jquery-popup-overlay/
*/
;(function ($) { /* eslint-disable-line */
var $window = $(window);
var options = {};
var zindexvalues = [];
var lastclicked = [];
var scrollbarwidth;
var bodymarginright = null;
var opensuffix = '_open';
var closesuffix = '_close';
var visiblePopupsArray = [];
var transitionsupport = null;
var opentimer;
var iOS = /(iPad|iPhone|iPod)/.test(navigator.userAgent);
var android = /(android)/i.test(navigator.userAgent);
var focusableElementsString = "a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]";
var methods = {
_init: function (el) {
var $el = $(el);
var options = $el.data('popupoptions');
lastclicked[el.id] = false;
zindexvalues[el.id] = 0;
if (!$el.data('popup-initialized')) {
$el.attr('data-popup-initialized', 'true');
methods._initonce(el);
}
if (options.autoopen) {
setTimeout(function() {
methods.show(el, 0);
}, 0);
}
},
_initonce: function (el) {
var $el = $(el);
var $body = $('body');
var $wrapper;
var options = $el.data('popupoptions');
bodymarginright = parseInt($body.css('margin-right'), 10);
transitionsupport = document.body.style.webkitTransition !== undefined ||
document.body.style.MozTransition !== undefined ||
document.body.style.msTransition !== undefined ||
document.body.style.OTransition !== undefined ||
document.body.style.transition !== undefined;
if (options.scrolllock) {
// Calculate the browser's scrollbar width dynamically
var parent;
var child;
if (typeof scrollbarwidth === 'undefined') {
parent = $('
').appendTo('body');
child = parent.children();
scrollbarwidth = child.innerWidth() - child.height(99).innerWidth();
parent.remove();
}
}
if (!$el.attr('id')) {
$el.attr('id', 'j-popup-' + parseInt((Math.random() * 100000000), 10));
}
$el.addClass('popup_content');
if ((options.background) && (!$('#' + el.id + '_background').length)) {
$body.append('');
var $background = $('#' + el.id + '_background');
$background.css({
opacity: 0,
visibility: 'hidden',
backgroundColor: options.color,
position: 'fixed',
top: 0,
right: 0,
bottom: 0,
left: 0
});
if (options.setzindex && !options.autozindex) {
$background.css('z-index', '100000');
}
if (options.transition) {
$background.css('transition', options.transition);
}
}
$body.append(el);
$el.wrap('');
$wrapper = $('#' + el.id + '_wrapper');
$wrapper.css({
opacity: 0,
visibility: 'hidden',
position: 'absolute'
});
// Make div clickable in iOS
if (iOS) {
$background = $('#' + el.id + '_background');
$background.css('cursor', 'pointer');
$(options.pagecontainer).css('cursor', 'pointer');
}
if (options.type == 'overlay' && !options.absolute) {
$wrapper.css('overflow','auto');
$wrapper[0].style.WebkitOverflowScrolling = 'touch'; // for smooth scrolling in overflow:auto divs in iOS
}
$el.css({
opacity: 0,
visibility: 'hidden',
'pointer-events': 'auto', // reset pointer events back to default for a child element
display: 'inline-block'
});
if (options.setzindex && !options.autozindex) {
$wrapper.css('z-index', '100001');
}
if (!options.outline) {
$el.css('outline', 'none');
}
if (options.transition) {
$el.css('transition', options.transition);
$wrapper.css('transition', options.transition);
}
// Hide popup content from screen readers initially
$el.attr('aria-hidden', true);
if (options.type == 'overlay') {
$el.css({
textAlign: 'left',
position: 'relative',
verticalAlign: 'middle'
});
$wrapper.css({
position: 'fixed',
width: '100%',
height: '100%',
top: 0,
left: 0,
textAlign: 'center'
});
// CSS vertical align helper
$wrapper.append('');
$('.popup_align').css({
display: 'inline-block',
verticalAlign: 'middle',
height: '100%'
});
}
// Add WAI ARIA role to announce dialog to screen readers
$el.attr('role', 'dialog');
var openelement = (options.openelement) ? options.openelement : ('.' + el.id + opensuffix);
$(openelement).each(function (i, item) {
$(item).attr('data-popup-ordinal', i);
if (!item.id) {
$(item).attr('id', 'open_' + parseInt((Math.random() * 100000000), 10));
}
});
// Set aria-labelledby (if aria-label or aria-labelledby is not set in html)
if (!($el.attr('aria-labelledby') || $el.attr('aria-label'))) {
$el.attr('aria-labelledby', $(openelement).attr('id'));
}
// Show and hide tooltips on hover
if(options.action == 'hover'){
options.keepfocus = false;
// Handler: mouseenter, focusin
$(openelement).on('mouseenter', function () {
methods.show(el, $(this).data('popup-ordinal'));
});
// Handler: mouseleave, focusout
$(openelement).on('mouseleave', function () {
methods.hide(el);
});
} else {
// Handler: Show popup when clicked on `open` element
$(document).on('click.jqp', openelement, function (event) {
event.preventDefault();
var ord = $(this).data('popup-ordinal');
setTimeout(function() { // setTimeout is to allow `close` method to finish (for issues with multiple tooltips)
methods.show(el, ord);
}, 0);
});
}
if (options.closebutton) {
methods.addclosebutton(el);
}
if (options.detach) {
$el.detach();
} else {
$el.hide();
}
},
/**
* Show method
*
* @param {object} el - popup instance DOM node
* @param {number} ordinal - order number of an `open` element
*/
show: function (el, ordinal) {
var $el = $(el);
if ($el.data('popup-visible')) return;
// Initialize if not initialized. Required for: $('#popup').popup('show')
if (!$el.data('popup-initialized')) {
methods._init(el);
}
$el.attr('data-popup-initialized', 'true');
var $body = $('body');
var options = $el.data('popupoptions');
var $wrapper = $('#' + el.id + '_wrapper');
var $background = $('#' + el.id + '_background');
var mustLock = false;
// `beforeopen` callback event
callback(el, ordinal, options.beforeopen);
// Remember last clicked place
lastclicked[el.id] = ordinal;
// Add popup id to visiblePopupsArray
setTimeout(function() {
visiblePopupsArray.push(el.id);
}, 0);
// Calculating maximum z-index
if (options.autozindex) {
var elements = document.getElementsByTagName('*');
var len = elements.length;
var maxzindex = 0;
for(var i=0; i= 0) {
$('#' + el.id + '_background').css({
zIndex: (zindexvalues[el.id] + 1)
});
}
}
// Add z-index to the wrapper
if (zindexvalues[el.id] >= 0) {
$wrapper.css({
zIndex: (zindexvalues[el.id] + 2)
});
}
}
if (options.detach) {
$wrapper.prepend(el);
$el.show();
} else {
$el.show();
}
if (!options.background) {
$wrapper.css({ 'pointer-events': 'none' });
// Android doesn't want to scroll the popup wrapper (i.e. to react to a swipe event) if it has a combination of
// `options.background` set to false, `pointer-events:none` and `position:fixed` on a popup wrapper, plus
// `overflow:visible` on body. If a scrollbar appears on the popup wrapper (i.e.popup is not fully in viewport),
// we locking the scrolling of background content regardless of custom options.
if (android && !options.absolute && !options.scrolllock && !isInViewport(el)) {
mustLock = true;
}
}
opentimer = setTimeout(function() {
$wrapper.css({
visibility: 'visible',
opacity: 1
});
$('html').addClass('popup_visible').addClass('popup_visible_' + el.id);
$wrapper.addClass('popup_wrapper_visible');
}, 20); // 20ms required for opening animation to occur in FF
// Disable background layer scrolling when popup is opened
if (options.scrolllock || mustLock) {
$body.css('overflow', 'hidden');
if ($body.height() > $window.height()) {
$body.css('margin-right', bodymarginright + scrollbarwidth);
}
}
$el.css({
'visibility': 'visible',
'opacity': 1
});
// Show background
if (options.background) {
$background.css({
'visibility': 'visible',
'opacity': options.opacity
});
// Fix IE8 issue with background not appearing
setTimeout(function() {
$background.css({
'opacity': options.opacity
});
}, 0);
}
$el.data('popup-visible', true);
// Position popup
methods.reposition(el, ordinal);
// Remember which element had focus before opening a popup
$el.data('focusedelementbeforepopup', document.activeElement);
// Make holder div programatically focusable with tabindex:-1 (tabindex:0 is keyboard focusable)
$el.attr('tabindex', -1);
// Focus the popup or user specified element.
// Initial timeout of 50ms is set to give some time to popup to show after clicking on
// `open` element, and after animation is complete to prevent background scrolling.
setTimeout(function() {
if (options.focuselement === 'closebutton') { // e.g. focuselement:'closebutton'
$('#' + el.id + ' .' + el.id + closesuffix + ':first').focus();
} else if (options.focuselement) { // e.g. focuselement:'#my-close-button'
$(options.focuselement).focus();
} else if (options.focuselement === true || options.keepfocus) { // e.g. focuselement:true OR keepfocus:true
$el.focus();
}
}, options.focusdelay);
// Hide main content from screen readers
if (options.keepfocus) {
$(options.pagecontainer).attr('aria-hidden', true);
}
// Reveal popup content to screen readers
$el.attr('aria-hidden', false);
callback(el, ordinal, options.onopen);
if (transitionsupport) {
$wrapper.one('transitionend', function() {
callback(el, ordinal, options.opentransitionend);
});
} else {
callback(el, ordinal, options.opentransitionend);
}
// Handler: Reposition tooltip when window is resized
if (options.type == 'tooltip') {
$(window).on('resize.' + el.id, function () {
methods.reposition(el, ordinal);
});
}
},
/**
* Hide method
*
* @param object el - popup instance DOM node
* @param boolean outerClick - click on the outer content below popup
*/
hide: function (el, outerClick) {
// Get index of popup ID inside of visiblePopupsArray
var popupIdIndex = $.inArray(el.id, visiblePopupsArray);
// If popup is not opened, ignore the rest of the function
if (popupIdIndex === -1) {
return;
}
if(opentimer) clearTimeout(opentimer);
var $body = $('body');
var $el = $(el);
var options = $el.data('popupoptions');
var $wrapper = $('#' + el.id + '_wrapper');
var $background = $('#' + el.id + '_background');
$el.data('popup-visible', false);
if (visiblePopupsArray.length === 1) {
$('html').removeClass('popup_visible').removeClass('popup_visible_' + el.id);
} else {
if($('html').hasClass('popup_visible_' + el.id)) {
$('html').removeClass('popup_visible_' + el.id);
}
}
// Remove popup from the visiblePopupsArray
visiblePopupsArray.splice(popupIdIndex, 1);
if($wrapper.hasClass('popup_wrapper_visible')) {
$wrapper.removeClass('popup_wrapper_visible');
}
// Focus back on saved element
if (options.keepfocus && !outerClick) {
setTimeout(function() {
if ($($el.data('focusedelementbeforepopup')).is(':visible')) {
$el.data('focusedelementbeforepopup').focus();
}
}, 0);
}
// Hide popup
$wrapper.css({
'visibility': 'hidden',
'opacity': 0
});
$el.css({
'visibility': 'hidden',
'opacity': 0
});
// Hide background
if (options.background) {
$background.css({
'visibility': 'hidden',
'opacity': 0
});
}
// Reveal main content to screen readers
$(options.pagecontainer).attr('aria-hidden', false);
// Hide popup content from screen readers
$el.attr('aria-hidden', true);
// `onclose` callback event
callback(el, lastclicked[el.id], options.onclose);
if (transitionsupport && $el.css('transition-duration') !== '0s') {
$el.one('transitionend', function() {
if (!($el.data('popup-visible'))) {
if (options.detach) {
$el.detach();
} else {
$el.hide();
}
}
// Re-enable scrolling of background layer, if needed
if (options.scrolllock) {
setTimeout(function() {
if ($.grep(visiblePopupsArray, function(eid) { return $("#"+eid).data('popupoptions').scrolllock }).length) {
// Some "scolllock=true" popup is currently visible, leave scrolling disabled
return;
}
$body.css({
overflow: 'visible',
'margin-right': bodymarginright
});
}, 10); // 10ms added for CSS transition in Firefox which doesn't like overflow:auto
}
callback(el, lastclicked[el.id], options.closetransitionend);
});
} else {
if (options.detach) {
$el.detach();
} else {
$el.hide();
}
// Re-enable scrolling of background layer, if needed
if (options.scrolllock) {
setTimeout(function() {
if ($.grep(visiblePopupsArray, function(eid) { return $("#"+eid).data('popupoptions').scrolllock }).length) {
// Some "scrolllock=true" popup is currently visible, leave scrolling disabled
return;
}
$body.css({
overflow: 'visible',
'margin-right': bodymarginright
});
}, 10); // 10ms added for CSS transition in Firefox which doesn't like overflow:auto
}
callback(el, lastclicked[el.id], options.closetransitionend);
}
if (options.type == 'tooltip') {
$(window).off('resize.' + el.id);
}
},
/**
* Toggle method
*
* @param {object} el - popup instance DOM node
* @param {number} ordinal - order number of an `open` element
*/
toggle: function (el, ordinal) {
if ($(el).data('popup-visible')) {
methods.hide(el);
} else {
setTimeout(function() {
methods.show(el, ordinal);
}, 0);
}
},
/**
* Reposition method
*
* @param {object} el - popup instance DOM node
* @param {number} ordinal - order number of an `open` element
*/
reposition: function (el, ordinal) {
var $el = $(el);
var options = $el.data('popupoptions');
var $wrapper = $('#' + el.id + '_wrapper');
ordinal = ordinal || 0;
// Tooltip type
if (options.type == 'tooltip') {
// TODO: this static assignments should probably be moved to init method
$wrapper.css({
'position': 'absolute'
});
var $tooltipanchor;
if (options.tooltipanchor) {
$tooltipanchor = $(options.tooltipanchor);
} else if (options.openelement) {
$tooltipanchor = $(options.openelement).filter('[data-popup-ordinal="' + ordinal + '"]');
} else {
$tooltipanchor = $('.' + el.id + opensuffix + '[data-popup-ordinal="' + ordinal + '"]');
}
var linkOffset = $tooltipanchor.offset() || { left: 0, top: 0 };
// Horizontal position for tooltip
if (options.horizontal == 'right') {
$wrapper.css('left', linkOffset.left + $tooltipanchor.outerWidth() + options.offsetleft);
} else if (options.horizontal == 'leftedge') {
$wrapper.css('left', linkOffset.left + options.offsetleft);
} else if (options.horizontal == 'left') {
$wrapper.css('right', $window.width() - linkOffset.left - options.offsetleft);
} else if (options.horizontal == 'rightedge') {
$wrapper.css('right', $window.width() - linkOffset.left - $tooltipanchor.outerWidth() - options.offsetleft);
} else {
$wrapper.css('left', linkOffset.left + ($tooltipanchor.outerWidth() / 2) - ($el.outerWidth() / 2) - parseFloat($el.css('marginLeft')) + options.offsetleft);
}
// Vertical position for tooltip
if (options.vertical == 'bottom') {
$wrapper.css('top', linkOffset.top + $tooltipanchor.outerHeight() + options.offsettop);
} else if (options.vertical == 'bottomedge') {
$wrapper.css('top', linkOffset.top + $tooltipanchor.outerHeight() - $el.outerHeight() + options.offsettop);
} else if (options.vertical == 'top') {
$wrapper.css('bottom', $window.height() - linkOffset.top - options.offsettop);
} else if (options.vertical == 'topedge') {
$wrapper.css('bottom', $window.height() - linkOffset.top - $el.outerHeight() - options.offsettop);
} else {
$wrapper.css('top', linkOffset.top + ($tooltipanchor.outerHeight() / 2) - ($el.outerHeight() / 2) - parseFloat($el.css('marginTop')) + options.offsettop);
}
// Overlay type
} else if (options.type == 'overlay') {
// TODO all static assignments in this block should probably be moved to init method
// Horizontal position for overlay
if (options.horizontal) {
$wrapper.css('text-align', options.horizontal);
} else {
$wrapper.css('text-align', 'center');
}
// Vertical position for overlay
if (options.vertical) {
$el.css('vertical-align', options.vertical);
} else {
$el.css('vertical-align', 'middle');
}
if (options.absolute) {
$wrapper.css({
position: 'absolute',
top: window.scrollY
});
}
}
},
/**
* Add-close-button method
*
* @param {object} el - popup instance DOM node
*/
addclosebutton: function (el) {
var genericCloseButton;
if ($(el).data('popupoptions').closebuttonmarkup) {
genericCloseButton = $(options.closebuttonmarkup).addClass(el.id + '_close');
} else {
genericCloseButton = '';
}
if ($(el).data('popup-initialized')){
$(el).append(genericCloseButton);
}
}
};
/**
* Callback event calls
*
* @param {object} el - popup instance DOM node
* @param {number} ordinal - order number of an `open` element
* @param {function} func - callback function
*/
var callback = function (el, ordinal, func) {
var options = $(el).data('popupoptions');
var openelement;
var elementclicked;
if (typeof options === 'undefined') return;
openelement = options.openelement ? options.openelement : ('.' + el.id + opensuffix);
elementclicked = $(openelement + '[data-popup-ordinal="' + ordinal + '"]');
if (typeof func == 'function') {
func.call($(el), el, elementclicked);
}
};
/**
* Check if element is fully in viewport
*
* @param {object} el - popup instance DOM node
*/
var isInViewport = function (el) {
var bounding = el.getBoundingClientRect();
return (
bounding.top >= 0 &&
bounding.left >= 0 &&
bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
bounding.right <= (window.innerWidth || document.documentElement.clientWidth)
);
};
// Hide popup if ESC key is pressed
$(document).on('keydown', function (event) {
if(visiblePopupsArray.length) {
var elementId = visiblePopupsArray[visiblePopupsArray.length - 1];
var el = document.getElementById(elementId);
if ($(el).data('popupoptions').escape && event.keyCode == 27) {
methods.hide(el);
}
}
});
// Hide popup on click
$(document).on('click', function (event) {
if(visiblePopupsArray.length) {
var elementId = visiblePopupsArray[visiblePopupsArray.length - 1];
var el = document.getElementById(elementId);
var closeButton = ($(el).data('popupoptions').closeelement) ? $(el).data('popupoptions').closeelement : ('.' + el.id + closesuffix);
// If Close button clicked
if ($(event.target).closest(closeButton).length) {
event.preventDefault();
methods.hide(el);
}
// If clicked outside of popup
if ($(el).data('popupoptions') && $(el).data('popupoptions').blur && !$(event.target).closest('#' + elementId).length && event.which !== 2 && $(event.target).is(':visible')) {
if ($(el).data('popupoptions').background) {
// If clicked on popup cover
methods.hide(el);
// Older iOS/Safari will trigger a click on the elements below the cover,
// when tapping on the cover, so the default action needs to be prevented.
event.preventDefault();
} else {
// If clicked on outer content
methods.hide(el, true);
}
}
}
});
// Keep keyboard focus inside of popup
$(document).on('keydown', function(event) {
if(visiblePopupsArray.length && event.which == 9) {
// If tab or shift-tab pressed
var elementId = visiblePopupsArray[visiblePopupsArray.length - 1];
var el = document.getElementById(elementId);
var options = $(el).data('popupoptions');
// If the last opened popup doesn't have `keepfocus` option, ignore the rest and don't lock the focus inside of popup.
if (!options.keepfocus) {
return;
}
// Get list of all children elements in given object
var popupItems = $(el).find('*');
// Get list of focusable items
var focusableItems = popupItems.filter(focusableElementsString).filter(':visible');
// Get currently focused item
var focusedItem = $(':focus');
// Get the number of focusable items
var numberOfFocusableItems = focusableItems.length;
// Get the index of the currently focused item
var focusedItemIndex = focusableItems.index(focusedItem);
// If popup doesn't contain focusable elements, focus popup itself
if (numberOfFocusableItems === 0) {
$(el).focus();
event.preventDefault();
} else {
if (event.shiftKey) {
// Back tab
// If focused on first item and user preses back-tab, go to the last focusable item
if (focusedItemIndex === 0) {
focusableItems.get(numberOfFocusableItems - 1).focus();
event.preventDefault();
}
} else {
// Forward tab
// If focused on the last item and user preses tab, go to the first focusable item
if (focusedItemIndex == numberOfFocusableItems - 1) {
focusableItems.get(0).focus();
event.preventDefault();
}
}
}
}
});
/**
* Plugin API
*/
$.fn.popup = function (customoptions) {
return this.each(function () {
var $el = $(this);
var newDefaults = $.extend(true, {}, $.fn.popup.defaults);
// Set defaults for tooltips dynamically instead of implicitly, so they can be overriden with custom options.
if (customoptions && customoptions.type === 'tooltip') {
newDefaults.background = false;
}
if (typeof customoptions === 'object') { // e.g. $('#popup').popup({'color':'blue'})
var opt = $.extend({}, newDefaults, $el.data('popupoptions'), customoptions);
$el.data('popupoptions', opt);
options = $el.data('popupoptions');
methods._init(this);
} else if (typeof customoptions === 'string') { // e.g. $('#popup').popup('hide')
if (!($el.data('popupoptions'))) {
$el.data('popupoptions', newDefaults);
options = $el.data('popupoptions');
}
methods[customoptions].call(this, this);
} else { // e.g. $('#popup').popup()
if (!($el.data('popupoptions'))) {
$el.data('popupoptions', newDefaults);
options = $el.data('popupoptions');
}
methods._init(this);
}
});
};
// destroy all popups
$.fn.popup.destroyall = function () {
// TODO: create tests to check if we can use `hide` method (perhaps we'll need to remove transitions)
// or we need another way of removing the data when destroying.
for(var i=0; i < visiblePopupsArray.length; i++) {
$('#' + visiblePopupsArray[i]).popup('hide');
}
$('.popup_wrapper').remove();
$('.popup_background').remove();
// visiblePopupsArray = []; // TODO: check if we need this for SPA and popups with fadeOut animation and scrolllock
$(document).off('click.jqp');
};
$.fn.popup.defaults = {
type: 'overlay',
absolute: false,
autoopen: false,
background: true,
color: 'black',
opacity: '0.5',
horizontal: 'center',
vertical: 'middle',
offsettop: 0,
offsetleft: 0,
escape: true,
blur: true,
setzindex: true,
autozindex: false,
scrolllock: false,
closebutton: false,
closebuttonmarkup: null,
keepfocus: true,
focuselement: null,
focusdelay: 50,
outline: false,
pagecontainer: null,
detach: false,
openelement: null,
closeelement: null,
transition: null,
tooltipanchor: null,
beforeopen: null,
onclose: null,
onopen: null,
opentransitionend: null,
closetransitionend: null
};
})(jQuery); /* eslint-disable-line */