public/javascripts/ao3modal.js
/*
THIS FILE GETS MINIFIED! It is included as "ao3modal.min.js".
USE A MINIFIER AND UPDATE THE .min.js FILE AFTER MAKING ANY CHANGES HERE!
*/
jQuery(document).ready(function() {
window.ao3modal = (function($) {
var _loading = false,
_bgDiv = $('<div>', {'id': 'modal-bg'}).addClass('modal-closer'),
_loadingDiv = $('<div>').addClass('loading'),
_wrapDiv = $('<div>', {'id': 'modal-wrap'}).addClass('modal-closer'),
_modalDiv = $('<div>', {'id': 'modal'}),
_contentDiv = $('<div>').addClass('content userstuff'),
_closeButton = $('<a>').addClass('action modal-closer')
.click(function(event) { event.preventDefault(); })
.attr('href', '#')
.text('Close'),
_title = $('<span>').addClass('title'),
_tallHeight,
_mobile = (function(uastring) { // detectmobilebrowsers.com (added ipad to regex)
return /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(ad|hone|od)|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(uastring) ||
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(uastring.substr(0,4));
})(navigator.userAgent||navigator.vendor||window.opera),
_mobileScrollTop = -1, // for returning to previous page scroll position after closing modal
_scrollbarWidth = _mobile ? 0 : (function() { // used as a margin when scrollbar is hidden
var inner = $('<p>').css({'width': '100%', 'height': 200}),
outer = $('<div>').css({
'height': 150,
'left': 0,
'overflow': 'hidden',
'top': 0,
'visibility': 'hidden',
'width': 200
}).append(inner).appendTo($('body')),
w1 = inner[0].offsetWidth,
w2;
outer.css('overflow', 'scroll');
w2 = inner[0].offsetWidth;
if (w1 == w2) w2 = outer[0].clientWidth;
outer.remove();
return (w1 - w2);
})(),
_keyboard;
function _toggleScroll(on) {
if (_mobile) { return; }
$('body').css({
'margin-right': on ? '' : _scrollbarWidth,
'overflow': on ? '' : 'hidden',
'height': on ? '' : _bgDiv.height()
});
}
function _calcSize() {
var img, scaledHeight, maxHeight,
hidden = !_modalDiv.is(':visible');
if (_mobile && _mobileScrollTop < 0) {
_mobileScrollTop = $(window).scrollTop();
}
if (hidden) { _modalDiv.css('opacity', 0).show(); }
_modalDiv.removeClass('tall');
if (_modalDiv.hasClass('img')) {
img = _contentDiv.children('img').first();
_modalDiv.toggleClass('tall', _modalDiv.height() >= 0.95*_bgDiv.height());
} else if (!_mobile) {
scaledHeight = _bgDiv.height()*_tallHeight.scale;
maxHeight = Math.min(scaledHeight, _tallHeight.max);
_modalDiv.toggleClass('tall', (!maxHeight || _modalDiv[0].scrollHeight > maxHeight));
}
if (hidden) { _modalDiv.hide().css('opacity', ''); }
if (_mobile) {
_wrapDiv.css('top', _mobileScrollTop);
} else {
_wrapDiv.css('top', $(window).scrollTop());
}
}
function _setContent(content, title) {
_loading = false;
_contentDiv.empty();
if (content instanceof $) { _contentDiv.append(content); }
else { _contentDiv.html(content); }
title = (title instanceof $) ? title : $('<span>').addClass('title').text(title || '');
_title.replaceWith(title);
_title = title;
if (!_contentDiv.is(':empty')) {
_calcSize();
if (!_contentDiv.is(':visible')) {
_keyboard.toggleHandlers(true);
_loadingDiv.hide();
_modalDiv.fadeIn(function() {
_contentDiv.focus();
});
}
}
}
function _show(href, title) {
_modalDiv.hide();
_wrapDiv.show();
if ($.support.opacity) { _bgDiv.add(_loadingDiv).fadeIn(); }
else { _bgDiv.add(_loadingDiv).show(); }
_toggleScroll(false);
_loading = true;
if (href.indexOf('#') === 0) {
_setContent($(href).html(), title || '');
} else if (href.indexOf('.jpg') == href.length-'.jpg'.length ||
href.indexOf('.gif') == href.length-'.gif'.length ||
href.indexOf('.bmp') == href.length-'.bmp'.length ||
href.indexOf('.png') == href.length-'.png'.length) {
_modalDiv.addClass('img');
var img = $('<img>').appendTo($('body')).attr('src', href),
imgLoad = function() {
var srcLink = $('<a>').addClass('title')
.attr({'href': href, 'title': 'View original', 'target': '_blank'})
.text(title ||
(href.indexOf('/') != -1 ? href.substring(href.lastIndexOf('/')+1) : href)
).css('text-decoration', 'underline');
img.remove();
if (!_loading) { return; }
if (title) { img.attr('alt', title); }
_setContent(img[0], srcLink);
};
if (img[0].complete) { imgLoad(); }
else { img.load(imgLoad); }
} else {
$.ajax({
url: href,
success: function(data) {
if (!_loading) { return; }
_setContent(data, title);
}
});
}
}
function _hide() {
_loading = false;
_title.text('');
_keyboard.toggleHandlers(false);
_wrapDiv.fadeOut(function() {
_setContent('');
_toggleScroll(true);
_modalDiv.css('width', '').removeClass('tall img');
if (_mobile && _mobileScrollTop >= 0) {
$('html, body').animate({'scrollTop': _mobileScrollTop}, 'fast');
_mobileScrollTop = -1;
}
});
if ($.support.opacity) { _bgDiv.fadeOut(); }
else { _bgDiv.hide(); }
}
function _addLink(elements) {
elements.each(function() {
var img = $(this).is('img') ? $(this).removeClass('modal') : false,
a = !img ? $(this) : $('<a>')
.css('border', 'none')
.attr({
'title': (img.attr('title') || img.attr('alt')),
'href': img.attr('src')
}).replaceAll(img)
.append(img);
a.addClass('modal modal-attached')
.attr('aria-controls', 'modal')
.filter(function() {
return $(this).closest('.userstuff').length === 0;
}).click(function(event){
_show($(this).attr('href'), $(this).attr('title'));
event.preventDefault();
});
// if link refers to in-page modal content, find it and hide it
if (a.attr('href').indexOf('#') === 0) {
$(a.attr('href')).hide();
}
});
}
// add modal elements to page
$('body').append(
_bgDiv.append(_loadingDiv),
_wrapDiv.append(
_modalDiv.append(
_contentDiv,
$('<div>').addClass('footer')
.append(
_title,
_closeButton
)
).trap()
)
);
// find the height scale and max-height of modal windows whose content is taller than the viewport
// values should come from the css ruleset for #modal.tall
_tallHeight = _mobile ? null : (function() {
var heights = {}, modalDiv = _modalDiv.clone();
modalDiv.css('margin-top', 9001).addClass('tall').appendTo(_bgDiv);
heights.scale = !modalDiv.height() ? 0.8 : parseInt(modalDiv.css('height'))/100;
heights.max = parseInt(modalDiv.css('max-height'));
modalDiv.remove();
return heights;
})();
// enable exit controls
$('body').click(function(event) {
if ($(event.target).hasClass('modal-closer') ||
(_modalDiv.hasClass('img') && $(event.target).hasClass('content'))) {
_hide();
}
});
// recalculate modal size on viewport resize
$(window).resize(function() {
if (_modalDiv.is(':visible')) { _calcSize(); }
});
// set up keyboard handling
_keyboard = (function() {
var scrollValues = {
38: -20, // up arrow
40: 20, // down arrow
33: -200, // page up
34: 200 // page down
},
tabbed = false,
handlers = {
'keydown': function(event) {
if (scrollValues[event.which]) {
_contentDiv[0].scrollTop += scrollValues[event.which];
event.preventDefault();
} else if (_modalDiv.is(':visible')) {
var target = event.target,
tag = target.tagName.toLowerCase(),
escKey = event.which == 27,
enterKey = event.which == 13,
targetIsInput = /^(input|textarea|a|button)$/.test(tag),
// ignore enter keypresses on links & inputs
targetInModal = !!$(target).closest('#modal')[0];
if (escKey || enterKey && (!targetInModal || !targetIsInput)) {
_hide();
}
// key events triggered from outside the modal should also die,
// except for ctrl combinations like ctrl+c (or cmd+c on macOS)
var keyShortcut = event.ctrlKey || event.metaKey;
if (escKey || (!targetInModal && !keyShortcut) || enterKey && !targetIsInput) {
event.preventDefault();
event.stopPropagation();
}
}
},
'keypress': function(event) {
if (scrollValues[event.which]) { event.preventDefault(); }
},
'keyup': function(event) {
if (scrollValues[event.which]) { event.preventDefault(); }
else if (event.which == 9 && !tabbed) {
_closeButton.focus();
tabbed = true;
event.preventDefault();
}
}
};
return {
toggleHandlers: function(on) {
tabbed = false;
for (var eventType in handlers) {
if (handlers.hasOwnProperty(eventType)) {
if (on) {
$('body').on(eventType, handlers[eventType]);
} else {
$('body').off(eventType, handlers[eventType]);
}
}
}
}
};
})();
// set modal-classed links to open modal boxes
_addLink($('a.modal, img.modal'));
// ensure handlers are attached to dynamically added modal invokers
$('body').on('click', 'a.modal, img.modal', function(event) {
var $this = $(this);
if ($this.is('.modal-attached')) { return; }
_addLink($this);
event.preventDefault();
if ($this.is('img')) {
$this.parent().click();
} else {
$this.click();
}
});
return {
show: _show,
setContent: _setContent,
hide: _hide,
addLink: _addLink
};
})(jQuery);
});