/**
* @namespace bootstrapLightbox
*/
angular.module('bootstrapLightbox', [
'ui.bootstrap'
]);
// optional dependencies
try {
angular.module('angular-loading-bar');
angular.module('bootstrapLightbox').requires.push('angular-loading-bar');
} catch (e) {}
try {
angular.module('ngTouch');
angular.module('bootstrapLightbox').requires.push('ngTouch');
} catch (e) {}
try {
angular.module('videosharing-embed');
angular.module('bootstrapLightbox').requires.push('videosharing-embed');
} catch (e) {}
angular.module('bootstrapLightbox').run(['$templateCache', function($templateCache) {
'use strict';
$templateCache.put('lightbox.html',
"
{{Lightbox.imageCaption}}
![]()
"
);
}]);
/**
* @class ImageLoader
* @classdesc Service for loading an image.
* @memberOf bootstrapLightbox
*/
angular.module('bootstrapLightbox').service('ImageLoader', ['$q',
function ($q) {
/**
* Load the image at the given URL.
* @param {String} url
* @return {Promise} A $q promise that resolves when the image has loaded
* successfully.
* @type {Function}
* @name load
* @memberOf bootstrapLightbox.ImageLoader
*/
this.load = function (url) {
var deferred = $q.defer();
var image = new Image();
// when the image has loaded
image.onload = function () {
// check image properties for possible errors
if ((typeof this.complete === 'boolean' && this.complete === false) ||
(typeof this.naturalWidth === 'number' && this.naturalWidth === 0)) {
deferred.reject();
}
deferred.resolve(image);
};
// when the image fails to load
image.onerror = function () {
deferred.reject();
};
// start loading the image
image.src = url;
return deferred.promise;
};
}]);
/**
* @class Lightbox
* @classdesc Lightbox service.
* @memberOf bootstrapLightbox
*/
angular.module('bootstrapLightbox').provider('Lightbox', function () {
/**
* Template URL passed into `$uibModal.open()`.
* @type {String}
* @name templateUrl
* @memberOf bootstrapLightbox.Lightbox
*/
this.templateUrl = 'lightbox.html';
/**
* Whether images should be scaled to the maximum possible dimensions.
* @type {Boolean}
* @name fullScreenMode
* @memberOf bootstrapLightbox.Lightbox
*/
this.fullScreenMode = false;
/**
* @param {*} image An element in the array of images.
* @return {String} The URL of the given image.
* @type {Function}
* @name getImageUrl
* @memberOf bootstrapLightbox.Lightbox
*/
this.getImageUrl = function (image) {
return typeof image === 'string' ? image : image.url;
};
/**
* @param {*} image An element in the array of images.
* @return {String} The caption of the given image.
* @type {Function}
* @name getImageCaption
* @memberOf bootstrapLightbox.Lightbox
*/
this.getImageCaption = function (image) {
return image.caption;
};
/**
* Calculate the max and min limits to the width and height of the displayed
* image (all are optional). The max dimensions override the min
* dimensions if they conflict.
* @param {Object} dimensions Contains the properties `windowWidth`,
* `windowHeight`, `imageWidth`, and `imageHeight`.
* @return {Object} May optionally contain the properties `minWidth`,
* `minHeight`, `maxWidth`, and `maxHeight`.
* @type {Function}
* @name calculateImageDimensionLimits
* @memberOf bootstrapLightbox.Lightbox
*/
this.calculateImageDimensionLimits = function (dimensions) {
if (dimensions.windowWidth >= 768) {
return {
// 92px = 2 * (30px margin of .modal-dialog
// + 1px border of .modal-content
// + 15px padding of .modal-body)
// with the goal of 30px side margins; however, the actual side margins
// will be slightly less (at 22.5px) due to the vertical scrollbar
'maxWidth': dimensions.windowWidth - 92,
// 126px = 92px as above
// + 34px outer height of .lightbox-nav
'maxHeight': dimensions.windowHeight - 126
};
} else {
return {
// 52px = 2 * (10px margin of .modal-dialog
// + 1px border of .modal-content
// + 15px padding of .modal-body)
'maxWidth': dimensions.windowWidth - 52,
// 86px = 52px as above
// + 34px outer height of .lightbox-nav
'maxHeight': dimensions.windowHeight - 86
};
}
};
/**
* Calculate the width and height of the modal. This method gets called
* after the width and height of the image, as displayed inside the modal,
* are calculated.
* @param {Object} dimensions Contains the properties `windowWidth`,
* `windowHeight`, `imageDisplayWidth`, and `imageDisplayHeight`.
* @return {Object} Must contain the properties `width` and `height`.
* @type {Function}
* @name calculateModalDimensions
* @memberOf bootstrapLightbox.Lightbox
*/
this.calculateModalDimensions = function (dimensions) {
// 400px = arbitrary min width
// 32px = 2 * (1px border of .modal-content
// + 15px padding of .modal-body)
var width = Math.max(400, dimensions.imageDisplayWidth + 32);
// 200px = arbitrary min height
// 66px = 32px as above
// + 34px outer height of .lightbox-nav
var height = Math.max(200, dimensions.imageDisplayHeight + 66);
// first case: the modal width cannot be larger than the window width
// 20px = arbitrary value larger than the vertical scrollbar
// width in order to avoid having a horizontal scrollbar
// second case: Bootstrap modals are not centered below 768px
if (width >= dimensions.windowWidth - 20 || dimensions.windowWidth < 768) {
width = 'auto';
}
// the modal height cannot be larger than the window height
if (height >= dimensions.windowHeight) {
height = 'auto';
}
return {
'width': width,
'height': height
};
};
/**
* @param {*} image An element in the array of images.
* @return {Boolean} Whether the provided element is a video.
* @type {Function}
* @name isVideo
* @memberOf bootstrapLightbox.Lightbox
*/
this.isVideo = function (image) {
if (typeof image === 'object' && image && image.type) {
return image.type === 'video';
}
return false;
};
/**
* @param {*} image An element in the array of images.
* @return {Boolean} Whether the provided element is a video that is to be
* embedded with an external service like YouTube. By default, this is
* determined by the url not ending in `.mp4`, `.ogg`, or `.webm`.
* @type {Function}
* @name isSharedVideo
* @memberOf bootstrapLightbox.Lightbox
*/
this.isSharedVideo = function (image) {
return this.isVideo(image) &&
!this.getImageUrl(image).match(/\.(mp4|ogg|webm)$/);
};
this.$get = ['$document', '$injector', '$uibModal', '$timeout', 'ImageLoader',
function ($document, $injector, $uibModal, $timeout, ImageLoader) {
// optional dependency
var cfpLoadingBar = $injector.has('cfpLoadingBar') ?
$injector.get('cfpLoadingBar'): null;
var Lightbox = {};
/**
* Array of all images to be shown in the lightbox (not `Image` objects).
* @type {Array}
* @name images
* @memberOf bootstrapLightbox.Lightbox
*/
Lightbox.images = [];
/**
* The index in the `Lightbox.images` aray of the image that is currently
* shown in the lightbox.
* @type {Number}
* @name index
* @memberOf bootstrapLightbox.Lightbox
*/
Lightbox.index = -1;
// set the configurable properties and methods, the defaults of which are
// defined above
Lightbox.templateUrl = this.templateUrl;
Lightbox.fullScreenMode = this.fullScreenMode;
Lightbox.getImageUrl = this.getImageUrl;
Lightbox.getImageCaption = this.getImageCaption;
Lightbox.calculateImageDimensionLimits = this.calculateImageDimensionLimits;
Lightbox.calculateModalDimensions = this.calculateModalDimensions;
Lightbox.isVideo = this.isVideo;
Lightbox.isSharedVideo = this.isSharedVideo;
/**
* Whether keyboard navigation is currently enabled for navigating through
* images in the lightbox.
* @type {Boolean}
* @name keyboardNavEnabled
* @memberOf bootstrapLightbox.Lightbox
*/
Lightbox.keyboardNavEnabled = false;
/**
* The image currently shown in the lightbox.
* @type {*}
* @name image
* @memberOf bootstrapLightbox.Lightbox
*/
Lightbox.image = {};
/**
* The UI Bootstrap modal instance. See {@link
* http://angular-ui.github.io/bootstrap/#/modal}.
* @type {Object}
* @name modalInstance
* @memberOf bootstrapLightbox.Lightbox
*/
Lightbox.modalInstance = null;
/**
* The URL of the current image. This is a property of the service rather
* than of `Lightbox.image` because `Lightbox.image` need not be an
* object, and besides it would be poor practice to alter the given
* objects.
* @type {String}
* @name imageUrl
* @memberOf bootstrapLightbox.Lightbox
*/
/**
* The optional caption of the current image.
* @type {String}
* @name imageCaption
* @memberOf bootstrapLightbox.Lightbox
*/
/**
* Whether an image is currently being loaded.
* @type {Boolean}
* @name loading
* @memberOf bootstrapLightbox.Lightbox
*/
Lightbox.loading = false;
/**
* Open the lightbox modal.
* @param {Array} newImages An array of images. Each image may be of
* any type.
* @param {Number} newIndex The index in `newImages` to set as the
* current image.
* @param {Object} modalParams Custom params for the angular UI
* bootstrap modal (in $uibModal.open()).
* @return {Object} The created UI Bootstrap modal instance.
* @type {Function}
* @name openModal
* @memberOf bootstrapLightbox.Lightbox
*/
Lightbox.openModal = function (newImages, newIndex, modalParams) {
Lightbox.images = newImages;
Lightbox.setImage(newIndex);
// store the modal instance so we can close it manually if we need to
Lightbox.modalInstance = $uibModal.open(angular.extend({
'templateUrl': Lightbox.templateUrl,
'controller': ['$scope', function ($scope) {
// $scope is the modal scope, a child of $rootScope
$scope.Lightbox = Lightbox;
Lightbox.keyboardNavEnabled = true;
}],
'windowClass': 'lightbox-modal'
}, modalParams || {}));
// modal close handler
Lightbox.modalInstance.result['finally'](function () {
// prevent the lightbox from flickering from the old image when it gets
// opened again
Lightbox.images = [];
Lightbox.index = 1;
Lightbox.image = {};
Lightbox.imageUrl = null;
Lightbox.imageCaption = null;
Lightbox.keyboardNavEnabled = false;
// complete any lingering loading bar progress
if (cfpLoadingBar) {
cfpLoadingBar.complete();
}
});
return Lightbox.modalInstance;
};
/**
* Close the lightbox modal.
* @param {*} result This argument can be useful if the modal promise
* gets handler(s) attached to it.
* @type {Function}
* @name closeModal
* @memberOf bootstrapLightbox.Lightbox
*/
Lightbox.closeModal = function (result) {
return Lightbox.modalInstance.close(result);
};
/**
* This method can be used in all methods which navigate/change the
* current image.
* @param {Number} newIndex The index in the array of images to set as
* the new current image.
* @type {Function}
* @name setImage
* @memberOf bootstrapLightbox.Lightbox
*/
Lightbox.setImage = function (newIndex) {
if (!(newIndex in Lightbox.images)) {
throw 'Invalid image.';
}
// update the loading flag and start the loading bar
Lightbox.loading = true;
if (cfpLoadingBar) {
cfpLoadingBar.start();
}
var image = Lightbox.images[newIndex];
var imageUrl = Lightbox.getImageUrl(image);
var success = function (properties) {
// update service properties for the image
properties = properties || {};
Lightbox.index = properties.index || newIndex;
Lightbox.image = properties.image || image;
Lightbox.imageUrl = properties.imageUrl || imageUrl;
Lightbox.imageCaption = properties.imageCaption ||
Lightbox.getImageCaption(image);
// restore the loading flag and complete the loading bar
Lightbox.loading = false;
if (cfpLoadingBar) {
cfpLoadingBar.complete();
}
};
if (!Lightbox.isVideo(image)) {
// load the image before setting it, so everything in the view is
// updated at the same time; otherwise, the previous image remains while
// the current image is loading
ImageLoader.load(imageUrl).then(function () {
success();
}, function () {
success({
'imageUrl': '#', // blank image
// use the caption to show the user an error
'imageCaption': 'Failed to load image'
});
});
} else {
success();
}
};
/**
* Navigate to the first image.
* @type {Function}
* @name firstImage
* @memberOf bootstrapLightbox.Lightbox
*/
Lightbox.firstImage = function () {
Lightbox.setImage(0);
};
/**
* Navigate to the previous image.
* @type {Function}
* @name prevImage
* @memberOf bootstrapLightbox.Lightbox
*/
Lightbox.prevImage = function () {
Lightbox.setImage((Lightbox.index - 1 + Lightbox.images.length) %
Lightbox.images.length);
};
/**
* Navigate to the next image.
* @type {Function}
* @name nextImage
* @memberOf bootstrapLightbox.Lightbox
*/
Lightbox.nextImage = function () {
Lightbox.setImage((Lightbox.index + 1) % Lightbox.images.length);
};
/**
* Navigate to the last image.
* @type {Function}
* @name lastImage
* @memberOf bootstrapLightbox.Lightbox
*/
Lightbox.lastImage = function () {
Lightbox.setImage(Lightbox.images.length - 1);
};
/**
* Call this method to set both the array of images and the current image
* (based on the current index). A use case is when the image collection
* gets changed dynamically in some way while the lightbox is still
* open.
* @param {Array} newImages The new array of images.
* @type {Function}
* @name setImages
* @memberOf bootstrapLightbox.Lightbox
*/
Lightbox.setImages = function (newImages) {
Lightbox.images = newImages;
Lightbox.setImage(Lightbox.index);
};
// Bind the left and right arrow keys for image navigation. This event
// handler never gets unbinded. Disable this using the `keyboardNavEnabled`
// flag. It is automatically disabled when the target is an input and or a
// textarea. TODO: Move this to a directive.
$document.bind('keydown', function (event) {
if (!Lightbox.keyboardNavEnabled) {
return;
}
// method of Lightbox to call
var method = null;
switch (event.which) {
case 39: // right arrow key
method = 'nextImage';
break;
case 37: // left arrow key
method = 'prevImage';
break;
}
if (method !== null && ['input', 'textarea'].indexOf(
event.target.tagName.toLowerCase()) === -1) {
// the view doesn't update without a manual digest
$timeout(function () {
Lightbox[method]();
});
event.preventDefault();
}
});
return Lightbox;
}];
});
/**
* @class lightboxSrc
* @classdesc This attribute directive is used in an `
` element in the
* modal template in place of `src`. It handles resizing both the `
`
* element and its relevant parent elements within the modal.
* @memberOf bootstrapLightbox
*/
angular.module('bootstrapLightbox').directive('lightboxSrc', ['$window',
'ImageLoader', 'Lightbox', function ($window, ImageLoader, Lightbox) {
// Calculate the dimensions to display the image. The max dimensions override
// the min dimensions if they conflict.
var calculateImageDisplayDimensions = function (dimensions, fullScreenMode) {
var w = dimensions.width;
var h = dimensions.height;
var minW = dimensions.minWidth;
var minH = dimensions.minHeight;
var maxW = dimensions.maxWidth;
var maxH = dimensions.maxHeight;
var displayW = w;
var displayH = h;
if (!fullScreenMode) {
// resize the image if it is too small
if (w < minW && h < minH) {
// the image is both too thin and short, so compare the aspect ratios to
// determine whether to min the width or height
if (w / h > maxW / maxH) {
displayH = minH;
displayW = Math.round(w * minH / h);
} else {
displayW = minW;
displayH = Math.round(h * minW / w);
}
} else if (w < minW) {
// the image is too thin
displayW = minW;
displayH = Math.round(h * minW / w);
} else if (h < minH) {
// the image is too short
displayH = minH;
displayW = Math.round(w * minH / h);
}
// resize the image if it is too large
if (w > maxW && h > maxH) {
// the image is both too tall and wide, so compare the aspect ratios
// to determine whether to max the width or height
if (w / h > maxW / maxH) {
displayW = maxW;
displayH = Math.round(h * maxW / w);
} else {
displayH = maxH;
displayW = Math.round(w * maxH / h);
}
} else if (w > maxW) {
// the image is too wide
displayW = maxW;
displayH = Math.round(h * maxW / w);
} else if (h > maxH) {
// the image is too tall
displayH = maxH;
displayW = Math.round(w * maxH / h);
}
} else {
// full screen mode
var ratio = Math.min(maxW / w, maxH / h);
var zoomedW = Math.round(w * ratio);
var zoomedH = Math.round(h * ratio);
displayW = Math.max(minW, zoomedW);
displayH = Math.max(minH, zoomedH);
}
return {
'width': displayW || 0,
'height': displayH || 0 // NaN is possible when dimensions.width is 0
};
};
// format the given dimension for passing into the `css()` method of `jqLite`
var formatDimension = function (dimension) {
return typeof dimension === 'number' ? dimension + 'px' : dimension;
};
// the dimensions of the image
var imageWidth = 0;
var imageHeight = 0;
return {
'link': function (scope, element, attrs) {
// resize the img element and the containing modal
var resize = function () {
// get the window dimensions
var windowWidth = $window.innerWidth;
var windowHeight = $window.innerHeight;
// calculate the max/min dimensions for the image
var imageDimensionLimits = Lightbox.calculateImageDimensionLimits({
'windowWidth': windowWidth,
'windowHeight': windowHeight,
'imageWidth': imageWidth,
'imageHeight': imageHeight
});
// calculate the dimensions to display the image
var imageDisplayDimensions = calculateImageDisplayDimensions(
angular.extend({
'width': imageWidth,
'height': imageHeight,
'minWidth': 1,
'minHeight': 1,
'maxWidth': 3000,
'maxHeight': 3000,
}, imageDimensionLimits),
Lightbox.fullScreenMode
);
// calculate the dimensions of the modal container
var modalDimensions = Lightbox.calculateModalDimensions({
'windowWidth': windowWidth,
'windowHeight': windowHeight,
'imageDisplayWidth': imageDisplayDimensions.width,
'imageDisplayHeight': imageDisplayDimensions.height
});
// resize the image
element.css({
'width': imageDisplayDimensions.width + 'px',
'height': imageDisplayDimensions.height + 'px'
});
// setting the height on .modal-dialog does not expand the div with the
// background, which is .modal-content
angular.element(
document.querySelector('.lightbox-modal .modal-dialog')
).css({
'width': formatDimension(modalDimensions.width)
});
// .modal-content has no width specified; if we set the width on
// .modal-content and not on .modal-dialog, .modal-dialog retains its
// default width of 600px and that places .modal-content off center
angular.element(
document.querySelector('.lightbox-modal .modal-content')
).css({
'height': formatDimension(modalDimensions.height)
});
};
// load the new image and/or resize the video whenever the attr changes
scope.$watch(function () {
return attrs.lightboxSrc;
}, function (src) {
// do nothing if there's no image
if (!Lightbox.image) {
return;
}
if (!Lightbox.isVideo(Lightbox.image)) { // image
// blank the image before resizing the element
element[0].src = '#';
// handle failure to load the image
var failure = function () {
imageWidth = 0;
imageHeight = 0;
resize();
};
if (src) {
ImageLoader.load(src).then(function (image) {
// these variables must be set before resize(), as they are used
// in it
imageWidth = image.naturalWidth;
imageHeight = image.naturalHeight;
// resize the img element and the containing modal
resize();
// show the image
element[0].src = src;
}, failure);
} else {
failure();
}
} else { // video
// default dimensions
imageWidth = 1280;
imageHeight = 720;
// resize the video element and the containing modal
resize();
// the src attribute applies to `