/*
jQuery Popup Plugin
Copyright (c) 2011 Daniel Thomson
Licensed under the MIT license:
http://www.opensource.org/licenses/mit-license.php
*/
// release notes: version 1.0
// version 1.1 - added support for gallery on non img object, now it just has to have the gallery name in the title attribute
// - added option for image caption
// - added option for gallery counter
// - added a preloader image and option for image path
// version 1.2 - got sick of trying to support syncronised animations, turned the whole popup content into a table so that the brwoser could animate the table in one go.
// version 1.3 - modified the extend method to create a new object 'opts' that doesn't destroy the settings object. I will think about creating an
// 'opts' method so that the settings can be modified outside of the script
// version 1.4 - not finished - added support for transparency hack on IE6
// version 1.5 - not sure, I think it was a bit of validation with JsLint
// version 1.6 - removed transparent layer height bug if scroll Y > 0
// - added option to set the opacity of the transparent layer
// - removed the $(window).unbind() method, seems not to be needed
// version 1.7 - added option to set overflow hidden off so that you can hang elements (namely the close button) outside the box
// - added proper ajax loading
// - added the option to fix the "top" or "left" position of the box.
// - added the option to turn the close button off
// version 2.0 - refactored script to use new architecture: https://github.com/dansdom/plugins-template-v2
// version 2.1 - added callback functions For opening and closing the popup, fixed up a bunch of event handling too
// version 2.2 - added support for more recent versions of jquery 1.9+
//
// TO DO: make an option to allow the popup to move with page scrolling
// add close box function
// remove hasCloseBtn option - it's stupid and only used for one project
//
// level of error suppresion -> low
// This module is designed as a popup function to use whenever and wherever you might need a popup. call this module on any DOM element you wish like this:
// $(document).ready(function(){
// $(".yourClassNameHere").popup({
// plugin : options,
// go : here
// })
// });
//
// You can link multiple popup items together to form a 'gallery'. to do this you need to set the option 'gallery' to true. Also you need to set
// the title attribute of the DOM element to the gallery name that you want. All other DOM elements with the same class and title attribute will
// be linked into the navigation structure of this gallery, and the title of this gallery will be displayed above the image in the popup box.
// galllery navigation is also bound to the left and right arrow keys on the keyboard
// the link to the popup goes onto the name attribute
// You can also control the height and width of the navigation box and title box with the following options:
// titleHeight, controlHeight
//
// Other interesting options avaliable:
// autoSize: allows the box to expand and contract to the image size with an animation of the length: transition
// centerOnResize: will center the popup when you resize the browser window
// popupID, contentClass, closeBox: allows custom classes and IDs for these DOM element just in case you need them for your application
// shadowLength: the box itself has a structure around it to allow for custom drop shadows.
// This value adjusts the size of this 'outer' layer of the box
// boxWidth, boxHeight: Sets the dimensions of the box if autoSize is false, and if content node is not an image
//
(function ($) {
// this ones for you 'uncle' Doug!
'use strict';
// Plugin namespace definition
$.Popup = function (options, element, callback) {
// wrap the element in the jQuery object
this.el = $(element);
// this is the namespace for all bound event handlers in the plugin
this.namespace = "popup";
// extend the settings object with the options, make a 'deep' copy of the object using an empty 'holding' object
this.opts = $.extend(true, {}, $.Popup.settings, options);
this.init();
// run the callback function if it is defined
if (typeof callback === "function") {
callback.call();
}
};
// these are the plugin default settings that will be over-written by user settings
$.Popup.settings = {
'transparentLayer' : true, // would you like a transparent layer underneath the popup?
'transparentOpacity' : 70, // set the opacity percentage of the transparent layer
'gallery' : false, // set true for navigation options between popups of the same title attribute
'galleryCounter' : false, // add a counter for gallery
'titleHeight' : 30, // height in pixels of the gallery title box
'controlHeight' : 40, // height in pixels of the gallery navigation box
'imageDesc' : false, // add a description box underneath the gallery image
'autoSize' : true, // set whether the box with image in it will resize to the image size
'boxWidth' : 400, // when autoSize is set to false, or no image then set the dimensions of the box in pixels
'boxHeight' : 300, // when autoSize is set to false, or no image then set the dimensions of the box in pixels
'centerImage' : true, // centers the image in a fixed size box
'shadowLength' : 42, // set the width of the padding around the box for your drop shadows
'transition' : 500, // transition speed from one box to the next
'popupID' : 'popupBox', // custom class for the popup box
'contentClass' : 'popupContent', // custom class for the popup content
'closeBox' : 'popupClose', // class the close button has
'hasCloseButton' : true, // set whether you want to be able to close the box or not
'centerOnResize' : true, // set whether the box centers itself when the browser resizes
'loaderPath' : 'loader.gif', // file path to the loading image
'overflow' : 'visible', // "hidden" or "visible", can set the css overflow attribute on or off
'ajax' : false, // allows user to specify an ajax call to a resource
'ajaxType' : "text", // jQuery needs the data type to be specified - http://api.jquery.com/jQuery.ajax/
'fixedTop' : false, // false/integer : allow for the user to specify the top position of the popup
'fixedLeft' : false, // false/integer : allow for the user to specify the left position of the popup
'onOpen' : function() {}, // call back function when the box opens
'onClose' : function() {}
};
// plugin functions go here
$.Popup.prototype = {
init : function() {
var popup = this;
// this seems a bit hacky, but for now I will unbind the namespace first before binding
this.destroy();
// this is a flag to test if the popup is open. I will only call the close box function on those popup's that are currently open so that only one callback function is called at one time
this.el.isOpen = false;
// *** set content source and gallery title variables ***
this.el.boxSrc = this.el.attr('name');
// store DOM fragment as a variable
if (!this.opts.gallery) {
this.fragment = $(this.el.boxSrc);
}
if (this.opts.hasCloseButton) {
this.el.closeBtn = 'close';
} else {
this.el.closeBtn = '';
}
$(this.el).on('click.' + this.namespace, function(e) {
e.preventDefault();
//if (popup.el.isOpen === false)
popup.openBox();
});
},
findScreenPos : function() {
var dimensions = {},
win = $(window);
dimensions.winY = win.height();
dimensions.winX = win.width();
dimensions.scrY = win.scrollTop();
dimensions.scrX = win.scrollLeft();
return dimensions;
},
createBox : function() {
var popup = this,
popupBox,
dimensions;
if ($("#" + this.opts.popupID).length === 0) {
popupBox = '
';
// oops :( I forgot why I made a different box for IE6, was it to put the png class on the corners?
$("body").append(popupBox);
$("#" + this.opts.popupID).css("display","none");
}
// add transparency layer if transparency is true.
if (this.opts.transparentLayer === true && $(".transparency").length === 0) {
var transparentLayer = '';
$("body").append(transparentLayer);
// add event listeners for browser resizing and scrolling to adjust the transparent layer size
$(window).on('scroll.' + this.namespace, function(){
// find height and width of transparent layer
dimensions = popup.findScreenPos();
$(".transparency").css({height: dimensions.winY + dimensions.scrY, width: dimensions.winX + dimensions.scrX});
});
$(window).on('resize.' + this.namespace, function(){
// find height and width of transparent layer
dimensions = popup.findScreenPos();
$(".transparency").css({height: dimensions.winY + dimensions.scrY + "px", width:dimensions.winX + dimensions.scrX + "px"});
});
}
// get rid of transparency if 'false' and it already exists and don't need it
if (this.opts.transparentLayer === false && $(".transparency").length > 0) {
$(".transparency").remove();
}
// add event handling for closing box
$(document).on('keydown.' + this.namespace, function(e) {
if (e.keyCode == 27 && popup.el.isOpen === true) {
popup.closeBox();
}
});
$(".transparency").on('click.' + this.namespace, function(){
if (popup.el.isOpen === true) {
popup.closeBox();
}
});
// clear box of any content
$("#" + this.opts.popupID + " ." + this.opts.contentClass).children().remove();
// style transparent layer
dimensions = this.findScreenPos();
$(".transparency").css({
"display" : "block",
"filter" : "alpha(opacity = " + this.opts.transparentOpacity + ")",
"opacity" : this.opts.transparentOpacity / 100,
"height" : dimensions.winY + dimensions.scrY + "px",
"width" : dimensions.winX + dimensions.scrX + "px"});
},
styleBox : function(properties, image) {
var popup = this,
popupBox = $("#" + this.opts.popupID),
contentSelector = "." + this.opts.contentClass,
imgSelector = contentSelector + " img",
contentHeight,
contentWidth,
outerBoxWidth,
outerBoxHeight,
boxPos,
leftPos,
topPos,
dimensions,
oldBoxHeight,
oldBoxWidth;
if (image) {
$(imgSelector).attr("src", image.src);
$(imgSelector).attr("height", properties.imgHeight + "px");
$(imgSelector).attr("width", properties.imgWidth + "px");
}
// if this is an image being loaded
if (properties) {
contentHeight = properties.imgHeight + this.opts.titleHeight + this.opts.controlHeight;
contentWidth = properties.imgWidth;
if (this.opts.autoSize === false) {
contentHeight = this.opts.boxHeight + this.opts.titleHeight + this.opts.controlHeight;
contentWidth = this.opts.boxWidth;
}
} else {
contentHeight = this.opts.boxHeight;
contentWidth = this.opts.boxWidth;
}
outerBoxWidth = contentWidth + (this.opts.shadowLength * 2);
outerBoxHeight = contentHeight + (this.opts.shadowLength * 2);
// calculate absolute position of the box and then center it on the screen
dimensions = popup.findScreenPos();
boxPos = this.centerBox(dimensions,outerBoxWidth,outerBoxHeight);
// allow user to specify a fixed position for the popup. Use fixedTop and fixedLeft, if not then center the box
if (this.opts.fixedTop) {
boxPos.topPos = this.opts.fixedTop;
}
if (this.opts.fixedLeft) {
boxPos.leftPos = boxPos.fixedLeft;
}
topPos = boxPos.topPos;
leftPos = boxPos.leftPos;
// on window resize - center the box in the middle again
if (this.opts.centerOnResize === true) {
$(window).on('resize.' + this.namespace, function() {
dimensions = popup.findScreenPos();
boxPos = popup.centerBox(dimensions,outerBoxWidth,outerBoxHeight);
if (popup.opts.fixedTop) {
boxPos.topPos = popup.opts.fixedTop;
}
if (popup.opts.fixedLeft) {
boxPos.leftPos = boxPos.fixedLeft;
}
popupBox.css({top: boxPos.topPos + "px", left: boxPos.leftPos + "px"});
});
}
// claculate dimensions of popup
// animate to the correct size if it is already open, else just set the values
if (popupBox.css("display") === "block" && properties && this.opts.autoSize === true) {
oldBoxHeight = parseFloat(popupBox.css("height")) - (this.opts.shadowLength * 2) - (this.opts.titleHeight + this.opts.controlHeight);
oldBoxWidth = parseFloat(popupBox.css("width")) - (this.opts.shadowLength * 2);
popupBox.find(".galleryTitle").css({height: this.opts.titleHeight + "px"});
popupBox.find(".galleryControls").css({height: this.opts.controlHeight + "px", "overflow": this.opts.overflow});
popupBox.find("img").css({height: oldBoxHeight + "px", width: oldBoxWidth + "px"});
popupBox.find(".imgPane").css({"width":"100%"});
// I want to animate most of this through the step function of the main image animation for better IE results
// maybe set up some local variables as well to increase performance
popupBox.find("img").animate({height: properties.imgHeight + "px", width: properties.imgWidth + "px"}, {queue:false, duration: this.opts.transition});
popupBox.animate({height: outerBoxHeight + "px", width: outerBoxWidth + "px", "left": leftPos + "px", "top": topPos + "px"}, {queue:false, duration: this.opts.transition});
popupBox.find(".imgPane").animate({height: (contentHeight - this.opts.titleHeight - this.opts.controlHeight) + "px"}, {queue:false, duration: this.opts.transition});
popupBox.find(".popupContent").animate({height: contentHeight + "px", width: contentWidth + "px"}, {queue:false, duration: this.opts.transition});
popupBox.find(".popupTM, .popupBM").animate({width: properties.imgWidth + "px"}, {queue:false, duration: this.opts.transition});
}
// create box and set its dimensions
else {
popupBox.css({height: outerBoxHeight + "px", width: outerBoxWidth + "px", "position": "absolute", "z-index":100, "overflow": this.opts.overflow});
popupBox.find(".imgPane").css({height:(contentHeight - this.opts.titleHeight - this.opts.controlHeight) + "px"});
popupBox.find(".popupContent").css({height: contentHeight + "px", width: contentWidth + "px"});
popupBox.find(".popupML div, .popupMR div").css({height: contentHeight + "px"});
popupBox.find(".galleryTitle").css({height: this.opts.titleHeight + "px", "overflow": this.opts.overflow});
popupBox.find(".galleryControls").css({height: this.opts.controlHeight + "px", "overflow": this.opts.overflow});
popupBox.find(".corner").css({height: this.opts.shadowLength + "px", width: this.opts.shadowLength + "px"});
popupBox.find(".popupTM").css({height: this.opts.shadowLength + "px", width: contentWidth + "px"});
popupBox.find(".popupBM").css({height: this.opts.shadowLength + "px", width: contentWidth + "px"});
popupBox.css({"left": leftPos + "px", "top": topPos + "px"});
}
// this probably should go somewhere else - leaving it in for now not to confuse me :P
popupBox.fadeIn("slow");
//var pngTimer = setTimeout(function(){$(".pngbg div").addClass("popupPng");},10);
// not sure why I am doing this anymore, since I would have a seperate box markup for IE6. leaving it oput for now
//$(".pngbg div").addClass("popupPng");
},
// function centers the box in the middle of the screen
centerBox : function(dimensions,outerBoxWidth,outerBoxHeight) {
var coords = {};
coords.leftPos = ((dimensions.winX - outerBoxWidth) / 2) + dimensions.scrX;
coords.topPos = ((dimensions.winY - outerBoxHeight) / 2) + dimensions.scrY;
if (coords.topPos < 0) {
coords.topPos = 0;
}
if (coords.leftPos < 0) {
coords.leftPos = 0;
}
return coords;
},
isContentImage : function() {
var contentString = this.el.boxSrc.split("."),
ext = contentString[contentString.length-1],
isImage;
switch(ext) {
case 'jpg' : isImage = true;
break;
case 'gif' : isImage = true;
break;
case 'png' : isImage = true;
break;
case 'bmp' : isImage = true;
break;
default : isImage = false;
}
return isImage;
},
displayImage : function() {
var popup = this,
thisIndex,
galleryLength,
popUpImg,
imgProperties = {},
// add the image tag to the popup content
contentBox = $("#" + this.opts.popupID + " ." + this.opts.contentClass);
// add image markup to the popup box
contentBox.append('
');
// if gallery description is set to true then create the box that the description will go into and append after .imgPane
if (this.opts.imageDesc === true) {
$(".imgPane").css("position","relative").append('' + this.el.imageDesc+'
');
}
// if gallery is a fixed height and width and centerImage = true, then align the image to the center of the box
if (this.opts.autoSize === false && this.opts.centerImage === true) {
contentBox.find(".imgPane").prepend(" ");
contentBox.find(".imgPane").css({"line-height": this.opts.boxHeight + "px","text-align":"center"});
contentBox.find(".imgPane img").css({"display":"inline","vertical-align":"middle"});
}
if (this.opts.gallery === true) {
// add gallery controls here
if (this.el.galleryTitle !== false) {
contentBox.append('');
contentBox.prepend('' + this.el.galleryTitle + '
' + this.el.closeBtn + '');
// if gallery counter is true then add counter
if (this.opts.galleryCounter === true) {
thisIndex = $("*[title='" + this.el.galleryTitle + "']").index(this.el) + 1;
galleryLength = $("*[title='" + this.el.galleryTitle + "']").length;
contentBox.find(".galleryControls").append("Displaying " + thisIndex + " of " + galleryLength + "
");
}
}
// if not a gallery title then just add the close button
else {
contentBox.prepend('' + this.el.closeBtn + '
');
}
}
// start of image loading stuff
popUpImg = new Image();
popUpImg.onload = function() {
imgProperties.imgHeight = popUpImg.height;
imgProperties.imgWidth = popUpImg.width;
popup.styleBox(imgProperties, popUpImg);
};
popUpImg.src = this.el.boxSrc;
// add close button controls
this.addCloseButton();
// add gallery controls and key functions here
this.addGalleryControls();
},
styleNodeBox : function() {
var contentBox = $("#" + this.opts.popupID+" ." + this.opts.contentClass);
//console.log(this.fragment);
contentBox.find("img.loader").remove();
contentBox.append('' + this.el.closeBtn + '
');
contentBox.append(this.fragment);
contentBox.find(this.el.boxSrc).css("display","block");
// style popup box
this.styleBox();
// add close button controls
this.addCloseButton();
},
getAjaxContent : function() {
var popup = this;
$("#" + this.opts.popupID + " ." + this.opts.contentClass).html('
');
$.ajax({
url: popup.el.boxSrc,
dataType : popup.opts.ajaxType,
success : function(msg) {
popup.fragment = msg;
popup.styleNodeBox();
},
error : function() {
popup.fragment = "ajax request failed";
popup.styleNodeBox();
}
});
},
addGalleryControls : function() {
var popup = this;
$("#" + this.opts.popupID + " .next").on('click.' + this.namespace, function(){
popup.cycleImage(1);
return false;
});
$("#" + this.opts.popupID + " .prev").on('click.' + this.namespace, function(){
popup.cycleImage(-1);
return false;
});
// add key controls and keep escape key handler
$(document).on('keydown.' + this.namespace, function(e) {
if (e.keyCode == 39 && popup.el.isOpen === true) {
popup.cycleImage(1);
} else if (e.keyCode == 37 && popup.el.isOpen === true) {
popup.cycleImage(-1);
}
if (e.keyCode == 27 && popup.el.isOpen === true) {
popup.closeBox();
}
});
},
addCloseButton : function() {
var popup = this;
$("#" + popup.opts.popupID + " ." + popup.opts.closeBox).on('click.' + popup.namespace, function(){
if (popup.el.isOpen === true) {
popup.closeBox();
}
return false;
});
},
openBox : function() {
this.el.galleryTitle = $(this.el).attr("title");
this.el.imageDesc = $(this.el).attr("longdesc");
// *** create the markup for popup box ***
this.createBox();
// *** find the screen dimensions ***
var dimensions = this.findScreenPos();
this.el.winY = dimensions.winY;
this.el.winX = dimensions.winX;
this.el.scrY = dimensions.scrY;
this.el.scrX = dimensions.scrX;
// *** either display content as an image OR as a DOM node ***
if (this.isContentImage()) {
// find the index of this image in the gallery
this.displayImage();
} else if (this.opts.ajax === true) {
this.getAjaxContent();
} else {
this.styleNodeBox();
}
// run the callback function on box open
if (this.el.isOpen === false) {
this.opts.onOpen();
}
// set the isOpen flag
this.el.isOpen = true;
},
closeBox : function() {
// may want to do some fancy stuff here, but for now just fading out the box
$("#" + this.opts.popupID).stop().fadeOut("slow").css("display","none");
// delete the popup box from the DOM
$("#" + this.opts.popupID).remove();
$(".transparency").fadeOut("slow");
// run the callback function on box close if it is open
if (this.el.isOpen === true)
{
this.opts.onClose();
}
// unbind the key controls
$(document).off('keydown.' + this.namespace);
this.el.isOpen = false;
},
cycleImage : function(imgIndex) {
//console.log("hitting cycle image");
var thisIndex = $("*[title='" + this.el.galleryTitle + "']").index(this.el),
galleryLength = $("*[title='" + this.el.galleryTitle + "']").length,
cycleIndex = thisIndex + imgIndex;
if (cycleIndex < 0) {
cycleIndex = galleryLength - 1;
}
if (cycleIndex == galleryLength) {
cycleIndex = 0;
}
this.el.isOpen = false;
// unbind the key controls
$(document).off('keydown.' + this.namespace);
$("*[title='" + this.el.galleryTitle + "']:eq(" + cycleIndex + ")").popup("openBox");
},
option : function(args) {
this.opts = $.extend(true, {}, this.opts, args);
},
// want to change the content element the popup points to? no worries
changeContent : function(content) {
this.fragment = $(content);
},
setFragmentHtml : function(content) { // change the content of the current popup
this.fragment.html(content);
},
destroy : function() {
this.el.off("." + this.namespace);
}
};
// the plugin bridging layer to allow users to call methods and add data after the plguin has been initialised
// props to https://github.com/jsor/jcarousel/blob/master/src/jquery.jcarousel.js for the base of the code & http://isotope.metafizzy.co/ for a good implementation
$.fn.popup = function(options, callback) {
// define the plugin name here so I don't have to change it anywhere else. This name refers to the jQuery data object that will store the plugin data
var pluginName = "popup",
args;
// if the argument is a string representing a plugin method then test which one it is
if ( typeof options === 'string' ) {
// define the arguments that the plugin function call may make
args = Array.prototype.slice.call( arguments, 1 );
// iterate over each object that the function is being called upon
this.each(function() {
// test the data object that the DOM element that the plugin has for the DOM element
var pluginInstance = $.data(this, pluginName);
// if there is no data for this instance of the plugin, then the plugin needs to be initialised first, so just call an error
if (!pluginInstance) {
alert("The plugin has not been initialised yet when you tried to call this method: " + options);
return;
}
// if there is no method defined for the option being called, or it's a private function (but I may not use this) then return an error.
if (!$.isFunction(pluginInstance[options]) || options.charAt(0) === "_") {
alert("the plugin contains no such method: " + options);
return;
}
// apply the method that has been called
else {
pluginInstance[options].apply(pluginInstance, args);
}
});
}
// initialise the function using the arguments as the plugin options
else {
// initialise each instance of the plugin
this.each(function() {
// define the data object that is going to be attached to the DOM element that the plugin is being called on
var pluginInstance = $.data(this, pluginName);
// if the plugin instance already exists then apply the options to it. I don't think I need to init again, but may have to on some plugins
if (pluginInstance) {
pluginInstance.option(options);
// initialising the plugin here may be dangerous and stack multiple event handlers. if required then the plugin instance may have to be 'destroyed' first
//pluginInstance.init(callback);
}
// initialise a new instance of the plugin
else {
$.data(this, pluginName, new $.Popup(options, this, callback));
}
});
}
// return the jQuery object from here so that the plugin functions don't have to
return this;
};
// end of module
})(jQuery);