You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1150 lines
53 KiB

/*
---
description: jBox is a powerful and flexible jQuery plugin, taking care of all your modal windows, tooltips, notices and more.
authors: Stephan Wagner (http://stephanwagner.me)
license: MIT (http://www.opensource.org/licenses/mit-license.php)
requires: jQuery 1.11.0 (http://code.jquery.com/jquery-1.11.0.min.js)
jQuery 2.1.0 (http://code.jquery.com/jquery-2.1.0.min.js)
documentation: http://stephanwagner.me/jBox/documentation
...
*/
function jBox(type, options) {
this.options = {
// jBox ID
id: null, // Choose a unique id, otherwise jBox will set one for you (jBox1, jBox2, ...)
// Dimensions
width: 'auto', // Width of container (e.g. 'auto', 100)
height: 'auto', // Height of container
// Attach
attach: null, // Attach jBox to elements (if no target element is provided, jBox will use the attached element as target)
trigger: 'click', // The event to open or close your jBoxes, use 'click' or 'mouseenter'
preventDefault: false, // Prevent default event when opening jBox (e.g. don't follow the href in a link when clicking on it)
// Content
title: null, // Adds a title to your jBox
content: null, // You can use a string to set text or HTML as content, or an element selector (e.g. jQuery('#jBox-content')) to append one or several elements (elements appended will get style display: 'block', so hide them with CSS style display: 'none' beforehand)
getTitle: null, // Get the title from an attribute when jBox opens
getContent: null, // Get the content from an attribute when jBox opens
// Content from Ajax
ajax: null, // Set a url to get content from an ajax call and inject into content area
data: '', // Ajax data to send with your ajax call (e.g. 'id=82')
reload: false, // Each time jBox is opened, reload the ajax call
// Position
target: null, // The target element where jBox will be opened
position: {
x: 'center', // Horizontal Position (Use a number, 'left', 'right' or 'center')
y: 'center' // Vertical Position (Use a number, 'top', 'bottom' or 'center')
},
outside: null, // Use 'x', 'y', or 'xy' to move your jBox outside of the target element
offset: 0, // Offset to final position, you can set different values for x and y with an object e.g. {x: 15, y: 0}
attributes: { // Note that attributes can only be 'left' or 'right' when using numbers for position, e.g. {x: 300, y: 20}
x: 'left', // Horizontal position, use 'left' or 'right'
y: 'top' // Vertical position, use 'top' or 'bottom'
},
adjustPosition: false, // Adjusts the position when there is not enough space (use true, 'flip' or 'move')
adjustTracker: false, // By default jBox adjusts the position when opening, to adjust when scrolling or resizing, use 'scroll', 'resize' or 'true' (both events)
adjustDistance: 5, // How far from the window edge we start adjusting, use an object to set different values: {bottom: 5, top: 50, left: 5, right: 20}
fixed: false, // Your jBox will stay on position when scrolling
reposition: false, // Calculates new position when the window-size changes
// Pointer
pointer: false, // Your pointer will always point towards the target element, so the option outside should be 'x' or 'y'
// Animations
fade: 180, // Fade duration in ms, set to 0 or false to disable
animation: null, // Animation when opening or closing (use 'pulse', 'zoomIn', 'zoomOut', 'move', 'slide', 'flip', 'tada') (CSS inspired from Daniel Edens Animate.css: http://daneden.me/animate)
// Appearance
theme: 'Default', // Set a jBox theme class
addClass: '', // Adds classes to the wrapper
overlay: false, // Adds an overlay when jBox opens (set color and opacity with CSS)
zIndex: 10000, // Use a high zIndex (your overlay will have the lowest zIndex of all your jBoxes (with overlays) minus one)
// Delays
delayOpen: 0, // Delay opening in ms (Note that the delay will be ignored if your jBox didn't finish closing)
delayClose: 0, // Delay closing in ms (Note that there is always a closing delay of at least 10ms to ensure jBox won't be closed when opening right away)
// Closing events
closeOnEsc: false, // Close jBox when pressing [esc] key
closeOnClick: false, // Close jBox with mouseclick, use 'true' (click anywhere), 'box' (click on jBox itself), 'body' (click anywhere but jBox)
closeOnMouseleave: false, // Close jBox when the mouse leaves the jBox area or the area of the attached element
closeButton: false, // Adds a close button to your jBox, use 'true', 'title' or 'box' ('true' will put the button in title when there is one, otherwise in box)
// Other options
constructOnInit: false, // Construct jBox when it's being initialized
blockScroll: false, // When jBox is open, block scrolling
appendTo: jQuery('body'), // Provide an element if you want the jBox to be positioned inside a specific element (only useful for fixed positions or when position values are numbers)
draggable: null, // Make your jBox draggable (use 'true', 'title' or provide an element as handle) (inspired from Chris Coyiers CSS-Tricks http://css-tricks.com/snippets/jquery/draggable-without-jquery-ui/)
// Events // Note: You can use 'this' in the event functions, it refers to your jBox object (e.g. onInit: function() { this.open(); })
onInit: function () { }, // Triggered when jBox is initialized, just before it's being created
onCreated: function () { }, // Triggered when jBox is created and is availible in DOM
onOpen: function () { }, // Triggered when jBox is opened
onClose: function () { }, // Triggered when jBox is closed
onAjax: function () { }, // Triggered when the ajax call starts
onAjaxComplete: function () { }, // Triggered when the ajax call is completed
// Only for Notices:
autoClose: 7000, // Time when jBox should close automatically
color: null, // Makes your notices colorful, use 'black', 'red', 'green', 'blue', 'yellow'
stack: true, // Set to false to disable notice-stacking
audio: false, // Set the url to an audio file without extention, e.g. '/url/filename'. jBox will look for an .mp3 and an .ogg file
volume: 100 // Percent of volume for audio files
};
// Default type options
this.defaultOptions = {
// Default options for tooltips
'Tooltip': {
getContent: 'title',
trigger: 'mouseenter',
position: { x: 'center', y: 'top' },
outside: 'y',
pointer: true,
adjustPosition: true,
reposition: true
},
// Default options for mouse tooltips
'Mouse': {
target: 'mouse',
position: { x: 'right', y: 'bottom' },
offset: 15,
trigger: 'mouseenter',
adjustPosition: 'flip'
},
// Default options for modal windows
'Modal': {
target: jQuery(window),
fixed: true,
blockScroll: true,
closeOnEsc: true,
closeOnClick: 'body',
closeButton: 'title',
overlay: true,
animation: 'zoomOut'
},
// Default options for notices
'Notice': {
target: jQuery(window),
fixed: true,
position: { x: 20, y: 20 },
attributes: { x: 'right', y: 'top' },
animation: 'zoomIn',
closeOnClick: 'box',
_onInit: function () {
this.open();
this.options.delayClose = this.options.autoClose;
this.options.delayClose && this.close();
}.bind(this),
_onCreated: function () {
this.options.color && this.wrapper.addClass('jBox-Notice-color jBox-Notice-' + this.options.color);
},
_onOpen: function () {
// Loop through notices at same window corner and either move or destroy them
jQuery.each(jQuery('.jBox-Notice'), function (index, el) {
el = jQuery(el);
if (el.attr('id') == this.id || el.css(this.options.attributes.y) == 'auto' || el.css(this.options.attributes.x) == 'auto') return;
if (!this.options.stack) {
el.data('jBox').close({ ignoreDelay: true });
return;
}
el.css('margin-' + this.options.attributes.y, parseInt(el.css('margin-' + this.options.attributes.y)) + this.dimensions.y + 10);
}.bind(this));
// Play audio file, IE8 doesn't support audio
if (this.options.audio && !this.IE8) {
this.audio = jQuery('<audio/>');
jQuery('<source/>', { src: this.options.audio + '.mp3' }).appendTo(this.audio);
jQuery('<source/>', { src: this.options.audio + '.ogg' }).appendTo(this.audio);
this.audio[0].volume = Math.min((this.options.volume / 100), 1);
this.audio[0].play();
}
}.bind(this),
// Remove from DOM when closing finishes
_onCloseComplete: function () {
this.destroy();
}.bind(this)
}
};
// Set default options for jBox types
if (jQuery.type(type) == 'string') {
this.type = type;
type = this.defaultOptions[type];
}
// Merge options
this.options = jQuery.extend(this.options, type, options);
// Get unique ID
if (this.options.id === null) {
var i = 1;
while (jQuery('#jBox' + i).length != 0) i++;
this.options.id = 'jBox' + i;
}
this.id = this.options.id;
// Correct impossible options
((this.options.position.x == 'center' && this.options.outside == 'x') || (this.options.position.y == 'center' && this.options.outside == 'y')) && (this.options.outside = false);
(!this.options.outside || this.options.outside == 'xy') && (this.options.pointer = false);
// Correct multiple choice options
jQuery.type(this.options.offset) != 'object' && (this.options.offset = { x: this.options.offset, y: this.options.offset });
this.options.offset.x || (this.options.offset.x = 0);
this.options.offset.y || (this.options.offset.y = 0);
jQuery.type(this.options.adjustDistance) != 'object' && (this.options.adjustDistance = { top: this.options.adjustDistance, right: this.options.adjustDistance, bottom: this.options.adjustDistance, left: this.options.adjustDistance });
// Save where the jBox is aligned to
this.align = (this.options.outside && this.options.outside != 'xy') ? this.options.position[this.options.outside] : (this.options.position.y != 'center' && jQuery.type(this.options.position.y) != 'number' ? this.options.position.x : (this.options.position.x != 'center' && jQuery.type(this.options.position.x) != 'number' ? this.options.position.y : this.options.attributes.x));
// Save default outside position
this.options.outside && this.options.outside != 'xy' && (this.outside = this.options.position[this.options.outside]);
// I know browser detection is bad practice, but for now it seems the only option to get jBox working in IE8
var userAgent = navigator.userAgent.toLowerCase();
this.IE8 = userAgent.indexOf('msie') != -1 && parseInt(userAgent.split('msie')[1]) == 8;
// Save global var for webkit prefix
this.prefix = userAgent.indexOf('webkit') != -1 ? '-webkit-' : '';
// Internal functions, used to easily get values
this._getOpp = function (opp) { return { left: 'right', right: 'left', top: 'bottom', bottom: 'top', x: 'y', y: 'x' }[opp]; };
this._getXY = function (xy) { return { left: 'x', right: 'x', top: 'y', bottom: 'y', center: 'x' }[xy]; };
this._getTL = function (tl) { return { left: 'left', right: 'left', top: 'top', bottom: 'top', center: 'left', x: 'left', y: 'top' }[tl]; };
// Create jBox
this._create = function () {
if (this.wrapper) return;
// Create wrapper
this.wrapper = jQuery('<div/>', {
id: this.id,
'class': 'jBox-wrapper' + (this.type ? ' jBox-' + this.type : '') + (this.options.theme ? ' jBox-' + this.options.theme : '') + (this.options.addClass ? ' ' + this.options.addClass : '') + (this.IE8 ? ' jBox-IE8' : '')
}).css({
position: (this.options.fixed ? 'fixed' : 'absolute'),
display: 'none',
opacity: 0,
zIndex: this.options.zIndex
// Save the jBox instance in the wrapper, so you gan get access to your jBox when you only have the element
}).data('jBox', this);
// Add mouseleave event
this.options.closeOnMouseleave && this.wrapper.mouseenter(function () { this.open(); }.bind(this)).mouseleave(function () { this.close(); }.bind(this));
// Create container
this.container = jQuery('<div/>', { 'class': 'jBox-container' }).css({ width: this.options.width, height: this.options.height }).appendTo(this.wrapper);
// Create content
this.content = jQuery('<div/>', { 'class': 'jBox-content' }).appendTo(this.container);
// Create close button
if (this.options.closeButton) {
this.closeButton = jQuery('<div/>', { 'class': 'jBox-closeButton jBox-noDrag' }).click(function () { this.close(); }.bind(this));
if (this.options.closeButton != 'title') {
this.wrapper.addClass('jBox-closeButton-box');
this.closeButton.appendTo(this.container);
}
}
// Append jBox to DOM
this.wrapper.appendTo(this.options.appendTo);
// Create pointer
if (this.options.pointer) {
// Get pointer vars and save globally
this.pointer = {
position: this._getOpp(this.outside),
xy: this._getXY(this.outside),
align: 'center',
offset: 0
};
this.pointer.element = jQuery('<div/>', { 'class': 'jBox-pointer jBox-pointer-' + this.pointer.position }).appendTo(this.wrapper);
this.pointer.dimensions = {
x: this.pointer.element.outerWidth(),
y: this.pointer.element.outerHeight()
};
if (jQuery.type(this.options.pointer) == 'string') {
var split = this.options.pointer.split(':');
split[0] && (this.pointer.align = split[0]);
split[1] && (this.pointer.offset = parseInt(split[1]));
}
this.pointer.alignAttribute = (this.pointer.xy == 'x' ? (this.pointer.align == 'bottom' ? 'bottom' : 'top') : (this.pointer.align == 'right' ? 'right' : 'left'));
// Set wrapper CSS
this.wrapper.css('padding-' + this.pointer.position, this.pointer.dimensions[this.pointer.xy]);
// Set pointer CSS
this.pointer.element.css(this.pointer.alignAttribute, (this.pointer.align == 'center' ? '50%' : 0)).css('margin-' + this.pointer.alignAttribute, this.pointer.offset);
this.pointer.margin = { margin: this.pointer.element.css('margin') };
// Add a transform to fix centered position
(this.pointer.align == 'center') && this.pointer.element.css(this.prefix + 'transform', 'translate(' + (this.pointer.xy == 'y' ? (this.pointer.dimensions.x * -0.5 + 'px') : 0) + ', ' + (this.pointer.xy == 'x' ? (this.pointer.dimensions.y * -0.5 + 'px') : 0) + ')');
this.pointer.element.css((this.pointer.xy == 'x' ? 'width' : 'height'), parseInt(this.pointer.dimensions[this.pointer.xy]) + parseInt(this.container.css('border-' + this.pointer.alignAttribute + '-width')));
// Add class to wrapper for CSS access
this.wrapper.addClass('jBox-pointerPosition-' + this.pointer.position);
}
// Set title and content
this.setContent(this.options.content);
this.setTitle(this.options.title);
// Make jBox draggable
if (this.options.draggable) {
var handle = (this.options.draggable == 'title') ? this.titleContainer : (this.options.draggable.length > 0 ? this.options.draggable : this.wrapper);
handle.addClass('jBox-draggable').on('mousedown', function (ev) {
if (ev.button == 2 || jQuery(ev.target).hasClass('jBox-noDrag') || jQuery(ev.target).parents('.jBox-noDrag').length) return;
var drg_h = this.wrapper.outerHeight(),
drg_w = this.wrapper.outerWidth(),
pos_y = this.wrapper.offset().top + drg_h - ev.pageY,
pos_x = this.wrapper.offset().left + drg_w - ev.pageX;
jQuery(document).on('mousemove.jBox-draggable-' + this.id, function (ev) {
this.wrapper.offset({
top: ev.pageY + pos_y - drg_h,
left: ev.pageX + pos_x - drg_w
});
}.bind(this));
ev.preventDefault();
}.bind(this)).on('mouseup', function () { jQuery(document).off('mousemove.jBox-draggable-' + this.id); }.bind(this));
}
// Fire onCreated event
(this.options.onCreated.bind(this))();
this.options._onCreated && (this.options._onCreated.bind(this))();
};
// Create jBox onInit
this.options.constructOnInit && this._create();
// Attach jBox
this.options.attach && this.attach();
// Position jBox on mouse
this._positionMouse = function (ev) {
// Calculate positions
this.pos = {
left: ev.pageX,
top: ev.pageY
};
var setPosition = function (a, p) {
// Set centered position
if (this.options.position[p] == 'center') {
this.pos[a] -= Math.ceil(this.dimensions[p] / 2);
return;
}
// Move to left or top
this.pos[a] += (a == this.options.position[p]) ? ((this.dimensions[p] * -1) - this.options.offset[p]) : this.options.offset[p];
return this.pos[a];
}.bind(this);
// Set position to wrapper
this.wrapper.css({
left: setPosition('left', 'x'),
top: setPosition('top', 'y')
});
// Adjust mouse position
this.targetDimensions = { x: 0, y: 0, left: ev.pageX, top: ev.pageY };
this._adjustPosition();
};
// Attach events
this._attachEvents = function () {
// Closing event: closeOnEsc
this.options.closeOnEsc && jQuery(document).on('keyup.jBox-' + this.id, function (ev) { if (ev.keyCode == 27) { this.close({ ignoreDelay: true }); } }.bind(this));
// Closing event: closeOnClick
this.options.closeOnClick && jQuery(document).on('click.jBox-' + this.id, function (ev) {
if (this.blockBodyClick ||
(this.options.closeOnClick == 'box' && ev.target != this.wrapper[0] && !this.wrapper.has(ev.target).length) ||
(this.options.closeOnClick == 'body' && (ev.target == this.wrapper[0] || this.wrapper.has(ev.target).length)))
return;
this.close({ ignoreDelay: true });
}.bind(this));
// Positioning events
if (((this.options.adjustPosition && this.options.adjustTracker) || this.options.reposition) && !this.fixed && this.outside) {
var scrollTimer,
scrollTimerTriggered = 0,
scrollTriggerDelay = 150; // Trigger scroll and resize events every 150 ms (set a higher value to improve performance)
// Function to delay positioning event
var positionDelay = function () {
var now = new Date().getTime();
if (!scrollTimer) {
if (now - scrollTimerTriggered > scrollTriggerDelay) {
this.options.reposition && this.position();
this.options.adjustTracker && this._adjustPosition();
scrollTimerTriggered = now;
}
scrollTimer = setTimeout(function () {
scrollTimer = null;
scrollTimerTriggered = new Date().getTime();
this.options.reposition && this.position();
this.options.adjustTracker && this._adjustPosition();
}.bind(this), scrollTriggerDelay);
}
}.bind(this);
// Trigger position events when scrolling
(this.options.adjustTracker && this.options.adjustTracker != 'resize') && jQuery(window).on('scroll.jBox-' + this.id, function (ev) { positionDelay(); }.bind(this));
// Trigger position events when resizing
((this.options.adjustTracker && this.options.adjustTracker != 'scroll') || this.options.reposition) && jQuery(window).on('resize.jBox-' + this.id, function (ev) { positionDelay(); }.bind(this));
}
// Mousemove events
this.options.target == 'mouse' && jQuery('body').on('mousemove.jBox-' + this.id, function (ev) { this._positionMouse(ev); }.bind(this));
};
// Detach events
this._detachEvents = function () {
// Closing event: closeOnEsc
this.options.closeOnEsc && jQuery(document).off('keyup.jBox-' + this.id);
// Closing event: closeOnClick
this.options.closeOnClick && jQuery(document).off('click.jBox-' + this.id);
// Positioning events
if ((this.options.adjustPosition && this.options.adjustTracker) || this.options.reposition) {
jQuery(window).off('scroll.jBox-' + this.id);
jQuery(window).off('resize.jBox-' + this.id);
}
// Mousemove events
this.options.target == 'mouse' && jQuery('body').off('mousemove.jBox-' + this.id);
};
// Add overlay
this._addOverlay = function () {
// If the overlay isn't cached, set overlay or create it
!this.overlay && (this.overlay = jQuery('#jBox-overlay').length ? jQuery('#jBox-overlay').css({ zIndex: Math.min(jQuery('#jBox-overlay').css('z-index'), (this.options.zIndex - 1)) }) : (jQuery('<div/>', { id: 'jBox-overlay' }).css({ display: 'none', opacity: 0, zIndex: (this.options.zIndex - 1) }).appendTo(jQuery('body'))));
// Add jBox to data
var overlay_data = this.overlay.data('jBox') || {};
overlay_data['jBox-' + this.id] = true;
this.overlay.data('jBox', overlay_data);
// Abort if overlay shown already
if (this.overlay.css('display') == 'block') return;
// Show overlay
this.options.fade ? (this.overlay.stop() && this.overlay.animate({ opacity: 1 }, {
queue: false,
duration: this.options.fade,
start: function () { this.overlay.css({ display: 'block' }); }.bind(this)
})) : this.overlay.css({ display: 'block', opacity: 1 });
};
// Remove overlay
this._removeOverlay = function () {
// Abort if no overlay found
if (!this.overlay) return;
// Remove jBox from data
var overlay_data = this.overlay.data('jBox');
delete overlay_data['jBox-' + this.id];
this.overlay.data('jBox', overlay_data);
// Hide overlay if no other jBox needs it
if (jQuery.isEmptyObject(overlay_data)) {
this.options.fade ? (this.overlay.stop() && this.overlay.animate({ opacity: 0 }, {
queue: false,
duration: this.options.fade,
complete: function () { this.overlay.css({ display: 'none' }); }.bind(this)
})) : this.overlay.css({ display: 'none', opacity: 0 });
}
};
// Generate CSS for animations and append to header
this._generateCSS = function () {
if (this.IE8) return;
// Get open and close animations if none provided
(jQuery.type(this.options.animation) != 'object') && (this.options.animation = {
pulse: { open: 'pulse', close: 'zoomOut' },
zoomIn: { open: 'zoomIn', close: 'zoomIn' },
zoomOut: { open: 'zoomOut', close: 'zoomOut' },
move: { open: 'move', close: 'move' },
slide: { open: 'slide', close: 'slide' },
flip: { open: 'flip', close: 'flip' },
tada: { open: 'tada', close: 'zoomOut' }
}[this.options.animation]);
// Get direction var
this.options.animation.open && (this.options.animation.open = this.options.animation.open.split(':'));
this.options.animation.close && (this.options.animation.close = this.options.animation.close.split(':'));
this.options.animation.openDirection = this.options.animation.open ? this.options.animation.open[1] : null;
this.options.animation.closeDirection = this.options.animation.close ? this.options.animation.close[1] : null;
this.options.animation.open && (this.options.animation.open = this.options.animation.open[0]);
this.options.animation.close && (this.options.animation.close = this.options.animation.close[0]);
// Add 'Open' and 'Close' to animation names
this.options.animation.open && (this.options.animation.open += 'Open');
this.options.animation.close && (this.options.animation.close += 'Close');
// All animations
var animations = {
pulse: {
duration: 350,
css: [['0%', 'scale(1)'], ['50%', 'scale(1.1)'], ['100%', 'scale(1)']]
},
zoomInOpen: {
duration: (this.options.fade || 180),
css: [['0%', 'scale(0.9)'], ['100%', 'scale(1)']]
},
zoomInClose: {
duration: (this.options.fade || 180),
css: [['0%', 'scale(1)'], ['100%', 'scale(0.9)']]
},
zoomOutOpen: {
duration: (this.options.fade || 180),
css: [['0%', 'scale(1.1)'], ['100%', 'scale(1)']]
},
zoomOutClose: {
duration: (this.options.fade || 180),
css: [['0%', 'scale(1)'], ['100%', 'scale(1.1)']]
},
moveOpen: {
duration: (this.options.fade || 180),
positions: { top: { '0%': -12 }, right: { '0%': 12 }, bottom: { '0%': 12 }, left: { '0%': -12 } },
css: [['0%', 'translate%XY(%Vpx)'], ['100%', 'translate%XY(0px)']]
},
moveClose: {
duration: (this.options.fade || 180),
timing: 'ease-in',
positions: { top: { '100%': -12 }, right: { '100%': 12 }, bottom: { '100%': 12 }, left: { '100%': -12 } },
css: [['0%', 'translate%XY(0px)'], ['100%', 'translate%XY(%Vpx)']]
},
slideOpen: {
duration: 400,
positions: { top: { '0%': -400 }, right: { '0%': 400 }, bottom: { '0%': 400 }, left: { '0%': -400 } },
css: [['0%', 'translate%XY(%Vpx)'], ['100%', 'translate%XY(0px)']]
},
slideClose: {
duration: 400,
timing: 'ease-in',
positions: { top: { '100%': -400 }, right: { '100%': 400 }, bottom: { '100%': 400 }, left: { '100%': -400 } },
css: [['0%', 'translate%XY(0px)'], ['100%', 'translate%XY(%Vpx)']]
},
flipOpen: {
duration: 600,
css: [['0%', 'perspective(400px) rotateX(90deg)'], ['40%', 'perspective(400px) rotateX(-15deg)'], ['70%', 'perspective(400px) rotateX(15deg)'], ['100%', 'perspective(400px) rotateX(0deg)']]
},
flipClose: {
duration: (this.options.fade || 300),
css: [['0%', 'perspective(400px) rotateX(0deg)'], ['100%', 'perspective(400px) rotateX(90deg)']]
},
tada: {
duration: 800,
css: [['0%', 'scale(1)'], ['10%, 20%', 'scale(0.9) rotate(-3deg)'], ['30%, 50%, 70%, 90%', 'scale(1.1) rotate(3deg)'], ['40%, 60%, 80%', 'scale(1.1) rotate(-3deg)'], ['100%', 'scale(1) rotate(0)']]
}
};
// Set Open and Close names for standalone animations
jQuery.each(['pulse', 'tada'], function (index, item) { animations[item + 'Open'] = animations[item + 'Close'] = animations[item]; });
// Function to generate the CSS for the keyframes
var generateKeyframeCSS = function (ev, position) {
// Generate keyframes CSS
keyframe_css = '@' + this.prefix + 'keyframes jBox-animation-' + this.options.animation[ev] + '-' + ev + (position ? '-' + position : '') + ' {';
jQuery.each(animations[this.options.animation[ev]].css, function (index, item) {
var translate = position ? item[1].replace('%XY', this._getXY(position).toUpperCase()) : item[1];
animations[this.options.animation[ev]].positions && (translate = translate.replace('%V', animations[this.options.animation[ev]].positions[position][item[0]]));
keyframe_css += item[0] + ' {' + this.prefix + 'transform:' + translate + ';}';
}.bind(this));
keyframe_css += '}';
// Generate class CSS
keyframe_css += '.jBox-animation-' + this.options.animation[ev] + '-' + ev + (position ? '-' + position : '') + ' {';
keyframe_css += this.prefix + 'animation-duration: ' + animations[this.options.animation[ev]].duration + 'ms;';
keyframe_css += this.prefix + 'animation-name: jBox-animation-' + this.options.animation[ev] + '-' + ev + (position ? '-' + position : '') + ';';
keyframe_css += animations[this.options.animation[ev]].timing ? (this.prefix + 'animation-timing-function: ' + animations[this.options.animation[ev]].timing + ';') : '';
keyframe_css += '}';
return keyframe_css;
}.bind(this);
// Generate css for each event and positions
var css = '';
jQuery.each(['open', 'close'], function (index, ev) {
// No CSS needed for closing with no fade
if (!this.options.animation[ev] || !animations[this.options.animation[ev]] || (ev == 'close' && !this.options.fade)) return '';
// Generate CSS
animations[this.options.animation[ev]].positions ?
jQuery.each(['top', 'right', 'bottom', 'left'], function (index2, position) { css += generateKeyframeCSS(ev, position); }) :
css += generateKeyframeCSS(ev);
}.bind(this));
jQuery('<style/>').append(css).appendTo(jQuery('head'));
};
this._blockBodyClick = function () {
this.blockBodyClick = true;
setTimeout(function () { this.blockBodyClick = false; }.bind(this), 10);
};
// Add css for animations
this.options.animation && this._generateCSS();
// Animations
this._animate = function (ev) {
if (this.IE8) return;
ev || (ev = this.isOpen ? 'open' : 'close');
// Don't animate when closing with no fade duration
if (!this.options.fade && ev == 'close') return null;
// Get the current position, use opposite if jBox is flipped
var animationDirection = (this.options.animation[ev + 'Direction'] || ((this.align != 'center') ? this.align : this.options.attributes.x));
this.flipped && this._getXY(animationDirection) == (this._getXY(this.align)) && (animationDirection = this._getOpp(animationDirection));
// Add event and position classes
var classnames = 'jBox-animation-' + this.options.animation[ev] + '-' + ev + ' jBox-animation-' + this.options.animation[ev] + '-' + ev + '-' + animationDirection;
this.wrapper.addClass(classnames);
// Get duration of animation
var animationDuration = parseFloat(this.wrapper.css(this.prefix + 'animation-duration')) * 1000;
ev == 'close' && (animationDuration = Math.min(animationDuration, this.options.fade));
// Remove animation classes when animation is finished
setTimeout(function () { this.wrapper.removeClass(classnames); }.bind(this), animationDuration);
};
// Abort animation
this._abortAnimation = function () {
if (this.IE8) return;
// Remove all animation classes
var prefix = 'jBox-animation';
var classes = this.wrapper.attr('class').split(' ').filter(function (c) {
return c.lastIndexOf(prefix, 0) !== 0;
});
this.wrapper.attr('class', classes.join(' '));
};
// Adjust position
this._adjustPosition = function () {
if (!this.options.adjustPosition) return null;
// Reset cached pointer position
if (this.positionAdjusted) {
this.wrapper.css(this.pos);
this.pointer && this.wrapper.css('padding', 0).css('padding-' + this._getOpp(this.outside), this.pointer.dimensions[this._getXY(this.outside)]).removeClass('jBox-pointerPosition-' + this._getOpp(this.pointer.position)).addClass('jBox-pointerPosition-' + this.pointer.position);
this.pointer && this.pointer.element.attr('class', 'jBox-pointer jBox-pointer-' + this._getOpp(this.outside)).css(this.pointer.margin);
this.positionAdjusted = false;
this.flipped = false;
}
// Get the window dimensions
var win = jQuery(window);
var windowDimensions = {
x: win.width(),
y: win.height(),
top: win.scrollTop(),
left: win.scrollLeft()
};
windowDimensions.bottom = windowDimensions.top + windowDimensions.y;
windowDimensions.right = windowDimensions.left + windowDimensions.x;
// Find out where the jBox is out of view area
var outYT = (windowDimensions.top > this.pos.top - (this.options.adjustDistance.top || 0)),
outXR = (windowDimensions.right < this.pos.left + this.dimensions.x + (this.options.adjustDistance.right || 0)),
outYB = (windowDimensions.bottom < this.pos.top + this.dimensions.y + (this.options.adjustDistance.bottom || 0)),
outXL = (windowDimensions.left > this.pos.left - (this.options.adjustDistance.left || 0)),
outX = outXL ? 'left' : (outXR ? 'right' : null),
outY = outYT ? 'top' : (outYB ? 'bottom' : null),
out = outX || outY;
// Stop here if jBox is not out of view area
if (!out) return;
// Flip jBox
if (this.options.adjustPosition != 'move' && (outX == this.outside || outY == this.outside)) {
this.target == 'mouse' && (this.outside = 'right');
// Check if enough space is availible on opposite position
if (((this.outside == 'top' || this.outside == 'left') ?
(windowDimensions[this._getXY(this.outside)] - (this.targetDimensions[this._getTL(this.outside)] - windowDimensions[this._getTL(this.outside)]) - this.targetDimensions[this._getXY(this.outside)]) :
(this.targetDimensions[this._getTL(this.outside)] - windowDimensions[this._getTL(this.outside)])) > this.dimensions[this._getXY(this.outside)] + this.options.adjustDistance[this._getOpp(this.outside)]) {
// Adjust wrapper and pointer
this.wrapper.css(this._getTL(this.outside), this.pos[this._getTL(this.outside)] + ((this.dimensions[this._getXY(this.outside)] + this.options.offset[this._getXY(this.outside)] + this.targetDimensions[this._getXY(this.outside)]) * (this.outside == 'top' || this.outside == 'left' ? 1 : -1))).removeClass('jBox-pointerPosition-' + this.pointer.position).addClass('jBox-pointerPosition-' + this._getOpp(this.pointer.position));
this.pointer && this.wrapper.css('padding', 0).css('padding-' + this.outside, this.pointer.dimensions[this._getXY(this.outside)]);
this.pointer && this.pointer.element.attr('class', 'jBox-pointer jBox-pointer-' + this.outside);
this.positionAdjusted = true;
this.flipped = true;
}
}
// Move jBox (only possible with pointer)
var outMove = (this._getXY(this.outside) == 'x') ? outY : outX;
if (this.pointer && this.options.adjustPosition != 'flip' && this._getXY(outMove) == this._getOpp(this._getXY(this.outside))) {
// Get the maximum space we have availible to adjust
if (this.pointer.align == 'center') {
var spaceAvail = (this.dimensions[this._getXY(outMove)] / 2) - (this.pointer.dimensions[this._getOpp(this.pointer.xy)] / 2) - (parseInt(this.pointer.element.css('margin-' + this.pointer.alignAttribute)) * (outMove != this._getTL(outMove) ? -1 : 1));
} else {
var spaceAvail = (outMove == this.pointer.alignAttribute) ?
parseInt(this.pointer.element.css('margin-' + this.pointer.alignAttribute)) :
this.dimensions[this._getXY(outMove)] - parseInt(this.pointer.element.css('margin-' + this.pointer.alignAttribute)) - this.pointer.dimensions[this._getXY(outMove)];
}
// Get the overlapping space
spaceDiff = (outMove == this._getTL(outMove)) ?
windowDimensions[this._getTL(outMove)] - this.pos[this._getTL(outMove)] + this.options.adjustDistance[outMove] :
(windowDimensions[this._getOpp(this._getTL(outMove))] - this.pos[this._getTL(outMove)] - this.options.adjustDistance[outMove] - this.dimensions[this._getXY(outMove)]) * -1;
// Add overlapping space on left or top window edge
if (outMove == this._getOpp(this._getTL(outMove)) && this.pos[this._getTL(outMove)] - spaceDiff < windowDimensions[this._getTL(outMove)] + this.options.adjustDistance[this._getTL(outMove)]) {
spaceDiff -= windowDimensions[this._getTL(outMove)] + this.options.adjustDistance[this._getTL(outMove)] - (this.pos[this._getTL(outMove)] - spaceDiff);
}
// Only adjust the maximum availible
spaceDiff = Math.min(spaceDiff, spaceAvail);
// Move jBox
if (spaceDiff <= spaceAvail && spaceDiff > 0) {
this.pointer.element.css('margin-' + this.pointer.alignAttribute, parseInt(this.pointer.element.css('margin-' + this.pointer.alignAttribute)) - (spaceDiff * (outMove != this.pointer.alignAttribute ? -1 : 1)));
this.wrapper.css(this._getTL(outMove), this.pos[this._getTL(outMove)] + (spaceDiff * (outMove != this._getTL(outMove) ? -1 : 1)));
this.positionAdjusted = true;
}
}
};
// Fire onInit event
(this.options.onInit.bind(this))();
this.options._onInit && (this.options._onInit.bind(this))();
return this;
};
// Attach jBox to elements
jBox.prototype.attach = function (elements, trigger) {
elements || (elements = this.options.attach);
trigger || (trigger = this.options.trigger);
elements && elements.length && jQuery.each(elements, function (index, el) {
el = jQuery(el);
if (!el.data('jBox-attached-' + this.id)) {
// Remove title attribute and store content on element
(this.options.getContent == 'title' && el.attr('title') != undefined) && el.data('jBox-getContent', el.attr('title')).removeAttr('title');
// Add Element to collection
this.attachedElements || (this.attachedElements = []);
this.attachedElements.push(el[0]);
// Add click or mouseenter event, click events can prevent default as well
el.on(trigger + '.jBox-attach-' + this.id, function (ev) {
// Only close jBox if you click the current target element, otherwise open at new target
if (this.isOpen && this.source[0] != el[0]) var forceOpen = true;
// Set new source element
this.source = el;
!this.options.target && (this.target = el);
trigger == 'click' && this.options.preventDefault && ev.preventDefault();
this[trigger == 'click' && !forceOpen ? 'toggle' : 'open']();
}.bind(this));
// Add close event for mouseenter
(this.options.trigger == 'mouseenter') && el.on('mouseleave', function () { this.close(); }.bind(this));
el.data('jBox-attached-' + this.id, trigger);
}
}.bind(this));
return this;
};
// Detach jBox from elements
jBox.prototype.detach = function (elements) {
elements || (elements = this.attachedElements || []);
elements && elements.length && jQuery.each(elements, function (index, el) {
el = jQuery(el);
if (el.data('jBox-attached-' + this.id)) {
el.off(el.data('jBox-attached-' + this.id + '.jBox-attach-' + this.id));
el.data('jBox-attached-' + this.id, null);
}
});
return this;
};
// Set title
jBox.prototype.setTitle = function (title) {
if (title == null || title == undefined) return this;
!this.wrapper && this._create();
if (!this.title) {
this.titleContainer = jQuery('<div/>', { 'class': 'jBox-title' });
this.title = jQuery('<div/>').appendTo(this.titleContainer);
this.wrapper.addClass('jBox-hasTitle');
if (this.options.closeButton == 'title') {
this.wrapper.addClass('jBox-closeButton-title');
this.closeButton.appendTo(this.titleContainer);
}
this.titleContainer.insertBefore(this.content);
}
this.title.html(title);
this.position();
return this;
};
// Set content
jBox.prototype.setContent = function (content) {
if (content == null) return this;
!this.wrapper && this._create();
switch (jQuery.type(content)) {
case 'string': this.content.html(content); break;
case 'object': this.content.children().css({ display: 'none' }); this.options.content.appendTo(this.content).css({ display: 'block' }); break;
}
this.position();
return this;
};
// Position jBox
jBox.prototype.position = function (options) {
options || (options = {});
// Get target
this.target = options.target || this.target || this.options.target || jQuery(window);
// Cache total current dimensions of jBox
this.dimensions = {
x: this.wrapper.outerWidth(),
y: this.wrapper.outerHeight()
};
// Mousemouve can't be positioned
if (this.target == 'mouse') return;
// Set percent and margin for centered inside
if (this.options.position.x == 'center' && this.options.position.y == 'center') {
this.wrapper.css({ left: '50%', top: '50%', marginLeft: (this.dimensions.x * -0.5 + this.options.offset.x), marginTop: (this.dimensions.y * -0.5 + this.options.offset.y) });
return this;
}
// Total current dimensions of target element
var targetOffset = this.target.offset();
this.targetDimensions = {
x: this.target.outerWidth(),
y: this.target.outerHeight(),
top: (targetOffset ? targetOffset.top : 0),
left: (targetOffset ? targetOffset.left : 0)
};
this.pos = {};
// Calculate positions
var setPosition = function (p) {
// Set number positions
if (jQuery.inArray(this.options.position[p], ['top', 'right', 'bottom', 'left', 'center']) == -1) {
this.pos[this.options.attributes[p]] = this.options.position[p];
return;
}
// We have a target, so use 'left' or 'top' as attributes
var a = this.options.attributes[p] = (p == 'x' ? 'left' : 'top');
// Start at target position
this.pos[a] = this.targetDimensions[a];
// Set centered position
if (this.options.position[p] == 'center') {
this.pos[a] += Math.ceil((this.targetDimensions[p] - this.dimensions[p]) / 2);
return;
}
// Move inside
(a != this.options.position[p]) && (this.pos[a] += this.targetDimensions[p] - this.dimensions[p]);
// Move outside
(this.options.outside == p || this.options.outside == 'xy') && (this.pos[a] += this.dimensions[p] * (a != this.options.position[p] ? 1 : -1));
}.bind(this);
// Set position including offset
setPosition('x');
setPosition('y');
// Adjust position depending on pointer align
if (this.options.pointer) {
var adjustWrapper = 0;
// Where is the pointer aligned? Add or substract accordingly
switch (this.pointer.align) {
case 'center':
if (this.options.position[this._getOpp(this.options.outside)] != 'center') {
adjustWrapper += (this.dimensions[this._getOpp(this.options.outside)] / 2);
}
break;
default:
switch (this.options.position[this._getOpp(this.options.outside)]) {
case 'center':
adjustWrapper += ((this.dimensions[this._getOpp(this.options.outside)] / 2) - (this.pointer.dimensions[this._getOpp(this.options.outside)] / 2)) * (this.pointer.align == this._getTL(this.pointer.align) ? 1 : -1);
break;
default:
adjustWrapper += (this.pointer.align != this.options.position[this._getOpp(this.options.outside)]) ?
this.dimensions[this._getOpp(this.options.outside)] - (this.pointer.dimensions[this._getOpp(this.options.outside)] / 2) :
(this.pointer.dimensions[this._getOpp(this.options.outside)] / 2);
break;
}
break;
}
adjustWrapper *= (this.options.position[this._getOpp(this.options.outside)] == this.pointer.alignAttribute ? -1 : 1);
adjustWrapper += this.pointer.offset * (this.pointer.align == this._getOpp(this._getTL(this.pointer.align)) ? 1 : -1);
this.pos[this._getTL(this._getOpp(this.pointer.xy))] += adjustWrapper;
}
// Add final offset
this.pos[this.options.attributes.x] += this.options.offset.x;
this.pos[this.options.attributes.y] += this.options.offset.y;
// Set CSS
this.wrapper.css(this.pos);
// Adjust position
this._adjustPosition();
return this;
};
// Open jBox
jBox.prototype.open = function (options) {
options || (options = {});
// Construct jBox if not already constructed
!this.wrapper && this._create();
// Abort any opening or closing timer
this.timer && clearTimeout(this.timer);
// Block body click for 10ms, so jBox can open on attached elements while closeOnClick = 'body'
this._blockBodyClick();
// Block opening
if (this.isDisabled) return this;
// Opening function
var open = function () {
// Set title from source element
this.source && this.options.getTitle && (this.source.attr(this.options.getTitle) != undefined && this.setTitle(this.source.attr(this.options.getTitle)));
// Set content from source element
this.source && this.options.getContent && (this.source.data('jBox-getContent') != undefined ? this.setContent(this.source.data('jBox-getContent')) : (this.source.attr(this.options.getContent) != undefined ? this.setContent(this.source.attr(this.options.getContent)) : null));
// Set position
this.position({ target: options.target });
// Fire onOpen event
(this.options.onOpen.bind(this))();
this.options._onOpen && (this.options._onOpen.bind(this))();
// Get content from ajax
this.options.ajax && (!this.ajaxLoaded || this.options.reload) && this.ajax();
// Abort closing
this.isClosing && this._abortAnimation();
// Open functions to call when jBox is closed
if (!this.isOpen) {
// jBox is open now
this.isOpen = true;
// Attach events
this._attachEvents();
// Block scrolling
this.options.blockScroll && jQuery('body').addClass('jBox-blockScroll-' + this.id);
// Add overlay
this.options.overlay && this._addOverlay();
// Only animate if jBox is compleately closed
this.options.animation && !this.isClosing && this._animate('open');
// Fading animation or show immediately
if (this.options.fade) {
this.wrapper.stop().animate({ opacity: 1 }, {
queue: false,
duration: this.options.fade,
start: function () {
this.isOpening = true;
this.wrapper.css({ display: 'block' });
}.bind(this),
always: function () {
this.isOpening = false;
}.bind(this)
});
} else {
this.wrapper.css({ display: 'block', opacity: 1 });
}
}
}.bind(this);
// Open jBox
this.options.delayOpen && !this.isOpen && !this.isClosing && !options.ignoreDelay ? (this.timer = setTimeout(open, this.options.delayOpen)) : open();
return this;
};
// Close jBox
jBox.prototype.close = function (options) {
options || (options = {});
// Abort opening
this.timer && clearTimeout(this.timer);
// Block body click for 10ms, so jBox can open on attached elements while closeOnClock = 'body' is true
this._blockBodyClick();
// Block closing
if (this.isDisabled) return this;
// Close function
var close = function () {
// Fire onClose event
(this.options.onClose.bind(this))();
// Only close if jBox is open
if (this.isOpen) {
// jBox is not open anymore
this.isOpen = false;
// Detach events
this._detachEvents();
// Unblock scrolling
this.options.blockScroll && jQuery('body').removeClass('jBox-blockScroll-' + this.id);
// Remove overlay
this.options.overlay && this._removeOverlay();
// Only animate if jBox is compleately closed
this.options.animation && !this.isOpening && this._animate('close');
// Remove source element
this.source = null;
// Fading animation or show immediately
if (this.options.fade) {
this.wrapper.stop().animate({ opacity: 0 }, {
queue: false,
duration: this.options.fade,
start: function () {
this.isClosing = true;
}.bind(this),
complete: function () {
this.wrapper.css({ display: 'none' });
this.options._onCloseComplete && (this.options._onCloseComplete.bind(this))();
}.bind(this),
always: function () {
this.isClosing = false;
}.bind(this),
});
} else {
this.wrapper.css({ display: 'none', opacity: 0 });
this.options._onCloseComplete && (this.options._onCloseComplete.bind(this))();
}
}
}.bind(this);
// Close jBox
options.ignoreDelay ? close() : (this.timer = setTimeout(close, Math.max(this.options.delayClose, 10)));
return this;
};
// Open or close jBox
jBox.prototype.toggle = function (options) {
this[this.isOpen ? 'close' : 'open'](options);
return this;
};
// Block opening and closing
jBox.prototype.disable = function () {
this.isDisabled = true;
return this;
};
// Unblock opening and closing
jBox.prototype.enable = function () {
this.isDisabled = false;
return this;
};
// Get content from ajax
jBox.prototype.ajax = function (options) {
options || (options = {});
// Abort running ajax calls
this.ajaxRequest && this.ajaxRequest.abort();
// Set new ajax call
this.ajaxRequest = jQuery.ajax({
url: options.url || this.options.ajax,
data: options.data || this.options.data,
beforeSend: function () {
// Clear content, add spinner and reposition jBox
this.content.html('');
this.wrapper.addClass('jBox-loading');
this.position();
// Fire onAjax event
(this.options.onAjax.bind(this))();
}.bind(this),
complete: function (response) {
// Remove spinner, set content and reposition jBox
this.wrapper.removeClass('jBox-loading');
this.content.html(response.responseText);
this.position();
this.ajaxLoaded = true;
// Fire onAjaxComplete event
(this.options.onAjaxComplete.bind(this))();
}.bind(this)
});
return this;
};
// Destroy jBox and remove it from DOM
jBox.prototype.destroy = function () {
this.close({ ignoreDelay: true });
this.wrapper.remove();
return this;
};
// Make jBox usable with jQuery selectors
jQuery.fn.jBox = function (type, options) {
type || (type = {});
options || (options = {});
return new jBox(type, jQuery.extend(options, { attach: this }));
};
// Add the .bind() function for IE 8 support
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () { },
fBound = function () { return fToBind.apply(this instanceof fNOP && oThis ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments))); };
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}