angular-bootstrap-lightbox
Version:
An AngularJS lightbox built using UI Bootstrap Modal.
450 lines (401 loc) • 14.6 kB
JavaScript
/**
* @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;
}];
});