2 * @namespace bootstrapLightbox
4 angular.module('bootstrapLightbox', [
8 // optional dependencies
10 angular.module('angular-loading-bar');
11 angular.module('bootstrapLightbox').requires.push('angular-loading-bar');
15 angular.module('ngTouch');
16 angular.module('bootstrapLightbox').requires.push('ngTouch');
20 angular.module('videosharing-embed');
21 angular.module('bootstrapLightbox').requires.push('videosharing-embed');
23 angular.module('bootstrapLightbox').run(['$templateCache', function($templateCache) {
26 $templateCache.put('lightbox.html',
27 "<div class=modal-body ng-swipe-left=Lightbox.nextImage() ng-swipe-right=Lightbox.prevImage()><div class=lightbox-nav><button class=close aria-hidden=true ng-click=$dismiss()>×</button><div class=btn-group ng-if=\"Lightbox.images.length > 1\"><a class=\"btn btn-xs btn-default\" ng-click=Lightbox.prevImage()>‹ Previous</a> <a ng-href={{Lightbox.imageUrl}} target=_blank class=\"btn btn-xs btn-default\" title=\"Open in new tab\">Open image in new tab</a> <a class=\"btn btn-xs btn-default\" ng-click=Lightbox.nextImage()>Next ›</a></div></div><div class=lightbox-image-container><div class=lightbox-image-caption><span>{{Lightbox.imageCaption}}</span></div><img ng-if=!Lightbox.isVideo(Lightbox.image) lightbox-src={{Lightbox.imageUrl}}><div ng-if=Lightbox.isVideo(Lightbox.image) class=\"embed-responsive embed-responsive-16by9\"><video ng-if=!Lightbox.isSharedVideo(Lightbox.image) lightbox-src={{Lightbox.imageUrl}} controls autoplay></video><embed-video ng-if=Lightbox.isSharedVideo(Lightbox.image) lightbox-src={{Lightbox.imageUrl}} ng-href={{Lightbox.imageUrl}} iframe-id=lightbox-video class=embed-responsive-item><a ng-href={{Lightbox.imageUrl}}>Watch video</a></embed-video></div></div></div>"
33 * @classdesc Service for loading an image.
34 * @memberOf bootstrapLightbox
36 angular.module('bootstrapLightbox').service('ImageLoader', ['$q',
39 * Load the image at the given URL.
41 * @return {Promise} A $q promise that resolves when the image has loaded
45 * @memberOf bootstrapLightbox.ImageLoader
47 this.load = function (url) {
48 var deferred = $q.defer();
50 var image = new Image();
52 // when the image has loaded
53 image.onload = function () {
54 // check image properties for possible errors
55 if ((typeof this.complete === 'boolean' && this.complete === false) ||
56 (typeof this.naturalWidth === 'number' && this.naturalWidth === 0)) {
60 deferred.resolve(image);
63 // when the image fails to load
64 image.onerror = function () {
68 // start loading the image
71 return deferred.promise;
76 * @classdesc Lightbox service.
77 * @memberOf bootstrapLightbox
79 angular.module('bootstrapLightbox').provider('Lightbox', function () {
81 * Template URL passed into `$uibModal.open()`.
84 * @memberOf bootstrapLightbox.Lightbox
86 this.templateUrl = 'lightbox.html';
89 * Whether images should be scaled to the maximum possible dimensions.
91 * @name fullScreenMode
92 * @memberOf bootstrapLightbox.Lightbox
94 this.fullScreenMode = false;
97 * @param {*} image An element in the array of images.
98 * @return {String} The URL of the given image.
101 * @memberOf bootstrapLightbox.Lightbox
103 this.getImageUrl = function (image) {
104 return typeof image === 'string' ? image : image.url;
108 * @param {*} image An element in the array of images.
109 * @return {String} The caption of the given image.
111 * @name getImageCaption
112 * @memberOf bootstrapLightbox.Lightbox
114 this.getImageCaption = function (image) {
115 return image.caption;
119 * Calculate the max and min limits to the width and height of the displayed
120 * image (all are optional). The max dimensions override the min
121 * dimensions if they conflict.
122 * @param {Object} dimensions Contains the properties `windowWidth`,
123 * `windowHeight`, `imageWidth`, and `imageHeight`.
124 * @return {Object} May optionally contain the properties `minWidth`,
125 * `minHeight`, `maxWidth`, and `maxHeight`.
127 * @name calculateImageDimensionLimits
128 * @memberOf bootstrapLightbox.Lightbox
130 this.calculateImageDimensionLimits = function (dimensions) {
131 if (dimensions.windowWidth >= 768) {
133 // 92px = 2 * (30px margin of .modal-dialog
134 // + 1px border of .modal-content
135 // + 15px padding of .modal-body)
136 // with the goal of 30px side margins; however, the actual side margins
137 // will be slightly less (at 22.5px) due to the vertical scrollbar
138 'maxWidth': dimensions.windowWidth - 92,
139 // 126px = 92px as above
140 // + 34px outer height of .lightbox-nav
141 'maxHeight': dimensions.windowHeight - 126
145 // 52px = 2 * (10px margin of .modal-dialog
146 // + 1px border of .modal-content
147 // + 15px padding of .modal-body)
148 'maxWidth': dimensions.windowWidth - 52,
149 // 86px = 52px as above
150 // + 34px outer height of .lightbox-nav
151 'maxHeight': dimensions.windowHeight - 86
157 * Calculate the width and height of the modal. This method gets called
158 * after the width and height of the image, as displayed inside the modal,
160 * @param {Object} dimensions Contains the properties `windowWidth`,
161 * `windowHeight`, `imageDisplayWidth`, and `imageDisplayHeight`.
162 * @return {Object} Must contain the properties `width` and `height`.
164 * @name calculateModalDimensions
165 * @memberOf bootstrapLightbox.Lightbox
167 this.calculateModalDimensions = function (dimensions) {
168 // 400px = arbitrary min width
169 // 32px = 2 * (1px border of .modal-content
170 // + 15px padding of .modal-body)
171 var width = Math.max(400, dimensions.imageDisplayWidth + 32);
173 // 200px = arbitrary min height
174 // 66px = 32px as above
175 // + 34px outer height of .lightbox-nav
176 var height = Math.max(200, dimensions.imageDisplayHeight + 66);
178 // first case: the modal width cannot be larger than the window width
179 // 20px = arbitrary value larger than the vertical scrollbar
180 // width in order to avoid having a horizontal scrollbar
181 // second case: Bootstrap modals are not centered below 768px
182 if (width >= dimensions.windowWidth - 20 || dimensions.windowWidth < 768) {
186 // the modal height cannot be larger than the window height
187 if (height >= dimensions.windowHeight) {
198 * @param {*} image An element in the array of images.
199 * @return {Boolean} Whether the provided element is a video.
202 * @memberOf bootstrapLightbox.Lightbox
204 this.isVideo = function (image) {
205 if (typeof image === 'object' && image && image.type) {
206 return image.type === 'video';
213 * @param {*} image An element in the array of images.
214 * @return {Boolean} Whether the provided element is a video that is to be
215 * embedded with an external service like YouTube. By default, this is
216 * determined by the url not ending in `.mp4`, `.ogg`, or `.webm`.
218 * @name isSharedVideo
219 * @memberOf bootstrapLightbox.Lightbox
221 this.isSharedVideo = function (image) {
222 return this.isVideo(image) &&
223 !this.getImageUrl(image).match(/\.(mp4|ogg|webm)$/);
226 this.$get = ['$document', '$injector', '$uibModal', '$timeout', 'ImageLoader',
227 function ($document, $injector, $uibModal, $timeout, ImageLoader) {
228 // optional dependency
229 var cfpLoadingBar = $injector.has('cfpLoadingBar') ?
230 $injector.get('cfpLoadingBar'): null;
235 * Array of all images to be shown in the lightbox (not `Image` objects).
238 * @memberOf bootstrapLightbox.Lightbox
240 Lightbox.images = [];
243 * The index in the `Lightbox.images` aray of the image that is currently
244 * shown in the lightbox.
247 * @memberOf bootstrapLightbox.Lightbox
251 // set the configurable properties and methods, the defaults of which are
253 Lightbox.templateUrl = this.templateUrl;
254 Lightbox.fullScreenMode = this.fullScreenMode;
255 Lightbox.getImageUrl = this.getImageUrl;
256 Lightbox.getImageCaption = this.getImageCaption;
257 Lightbox.calculateImageDimensionLimits = this.calculateImageDimensionLimits;
258 Lightbox.calculateModalDimensions = this.calculateModalDimensions;
259 Lightbox.isVideo = this.isVideo;
260 Lightbox.isSharedVideo = this.isSharedVideo;
263 * Whether keyboard navigation is currently enabled for navigating through
264 * images in the lightbox.
266 * @name keyboardNavEnabled
267 * @memberOf bootstrapLightbox.Lightbox
269 Lightbox.keyboardNavEnabled = false;
272 * The image currently shown in the lightbox.
275 * @memberOf bootstrapLightbox.Lightbox
280 * The UI Bootstrap modal instance. See {@link
281 * http://angular-ui.github.io/bootstrap/#/modal}.
283 * @name modalInstance
284 * @memberOf bootstrapLightbox.Lightbox
286 Lightbox.modalInstance = null;
289 * The URL of the current image. This is a property of the service rather
290 * than of `Lightbox.image` because `Lightbox.image` need not be an
291 * object, and besides it would be poor practice to alter the given
295 * @memberOf bootstrapLightbox.Lightbox
299 * The optional caption of the current image.
302 * @memberOf bootstrapLightbox.Lightbox
306 * Whether an image is currently being loaded.
309 * @memberOf bootstrapLightbox.Lightbox
311 Lightbox.loading = false;
314 * Open the lightbox modal.
315 * @param {Array} newImages An array of images. Each image may be of
317 * @param {Number} newIndex The index in `newImages` to set as the
319 * @param {Object} modalParams Custom params for the angular UI
320 * bootstrap modal (in $uibModal.open()).
321 * @return {Object} The created UI Bootstrap modal instance.
324 * @memberOf bootstrapLightbox.Lightbox
326 Lightbox.openModal = function (newImages, newIndex, modalParams) {
327 Lightbox.images = newImages;
328 Lightbox.setImage(newIndex);
330 // store the modal instance so we can close it manually if we need to
331 Lightbox.modalInstance = $uibModal.open(angular.extend({
332 'templateUrl': Lightbox.templateUrl,
333 'controller': ['$scope', function ($scope) {
334 // $scope is the modal scope, a child of $rootScope
335 $scope.Lightbox = Lightbox;
337 Lightbox.keyboardNavEnabled = true;
339 'windowClass': 'lightbox-modal'
340 }, modalParams || {}));
342 // modal close handler
343 Lightbox.modalInstance.result['finally'](function () {
344 // prevent the lightbox from flickering from the old image when it gets
346 Lightbox.images = [];
349 Lightbox.imageUrl = null;
350 Lightbox.imageCaption = null;
352 Lightbox.keyboardNavEnabled = false;
354 // complete any lingering loading bar progress
356 cfpLoadingBar.complete();
360 return Lightbox.modalInstance;
364 * Close the lightbox modal.
365 * @param {*} result This argument can be useful if the modal promise
366 * gets handler(s) attached to it.
369 * @memberOf bootstrapLightbox.Lightbox
371 Lightbox.closeModal = function (result) {
372 return Lightbox.modalInstance.close(result);
376 * This method can be used in all methods which navigate/change the
378 * @param {Number} newIndex The index in the array of images to set as
379 * the new current image.
382 * @memberOf bootstrapLightbox.Lightbox
384 Lightbox.setImage = function (newIndex) {
385 if (!(newIndex in Lightbox.images)) {
386 throw 'Invalid image.';
389 // update the loading flag and start the loading bar
390 Lightbox.loading = true;
392 cfpLoadingBar.start();
395 var image = Lightbox.images[newIndex];
396 var imageUrl = Lightbox.getImageUrl(image);
398 var success = function (properties) {
399 // update service properties for the image
400 properties = properties || {};
401 Lightbox.index = properties.index || newIndex;
402 Lightbox.image = properties.image || image;
403 Lightbox.imageUrl = properties.imageUrl || imageUrl;
404 Lightbox.imageCaption = properties.imageCaption ||
405 Lightbox.getImageCaption(image);
407 // restore the loading flag and complete the loading bar
408 Lightbox.loading = false;
410 cfpLoadingBar.complete();
414 if (!Lightbox.isVideo(image)) {
415 // load the image before setting it, so everything in the view is
416 // updated at the same time; otherwise, the previous image remains while
417 // the current image is loading
418 ImageLoader.load(imageUrl).then(function () {
422 'imageUrl': '#', // blank image
423 // use the caption to show the user an error
424 'imageCaption': 'Failed to load image'
433 * Navigate to the first image.
436 * @memberOf bootstrapLightbox.Lightbox
438 Lightbox.firstImage = function () {
439 Lightbox.setImage(0);
443 * Navigate to the previous image.
446 * @memberOf bootstrapLightbox.Lightbox
448 Lightbox.prevImage = function () {
449 Lightbox.setImage((Lightbox.index - 1 + Lightbox.images.length) %
450 Lightbox.images.length);
454 * Navigate to the next image.
457 * @memberOf bootstrapLightbox.Lightbox
459 Lightbox.nextImage = function () {
460 Lightbox.setImage((Lightbox.index + 1) % Lightbox.images.length);
464 * Navigate to the last image.
467 * @memberOf bootstrapLightbox.Lightbox
469 Lightbox.lastImage = function () {
470 Lightbox.setImage(Lightbox.images.length - 1);
474 * Call this method to set both the array of images and the current image
475 * (based on the current index). A use case is when the image collection
476 * gets changed dynamically in some way while the lightbox is still
478 * @param {Array} newImages The new array of images.
481 * @memberOf bootstrapLightbox.Lightbox
483 Lightbox.setImages = function (newImages) {
484 Lightbox.images = newImages;
485 Lightbox.setImage(Lightbox.index);
488 // Bind the left and right arrow keys for image navigation. This event
489 // handler never gets unbinded. Disable this using the `keyboardNavEnabled`
490 // flag. It is automatically disabled when the target is an input and or a
491 // textarea. TODO: Move this to a directive.
492 $document.bind('keydown', function (event) {
493 if (!Lightbox.keyboardNavEnabled) {
497 // method of Lightbox to call
500 switch (event.which) {
501 case 39: // right arrow key
502 method = 'nextImage';
504 case 37: // left arrow key
505 method = 'prevImage';
509 if (method !== null && ['input', 'textarea'].indexOf(
510 event.target.tagName.toLowerCase()) === -1) {
511 // the view doesn't update without a manual digest
512 $timeout(function () {
516 event.preventDefault();
525 * @classdesc This attribute directive is used in an `<img>` element in the
526 * modal template in place of `src`. It handles resizing both the `<img>`
527 * element and its relevant parent elements within the modal.
528 * @memberOf bootstrapLightbox
530 angular.module('bootstrapLightbox').directive('lightboxSrc', ['$window',
531 'ImageLoader', 'Lightbox', function ($window, ImageLoader, Lightbox) {
532 // Calculate the dimensions to display the image. The max dimensions override
533 // the min dimensions if they conflict.
534 var calculateImageDisplayDimensions = function (dimensions, fullScreenMode) {
535 var w = dimensions.width;
536 var h = dimensions.height;
537 var minW = dimensions.minWidth;
538 var minH = dimensions.minHeight;
539 var maxW = dimensions.maxWidth;
540 var maxH = dimensions.maxHeight;
545 if (!fullScreenMode) {
546 // resize the image if it is too small
547 if (w < minW && h < minH) {
548 // the image is both too thin and short, so compare the aspect ratios to
549 // determine whether to min the width or height
550 if (w / h > maxW / maxH) {
552 displayW = Math.round(w * minH / h);
555 displayH = Math.round(h * minW / w);
557 } else if (w < minW) {
558 // the image is too thin
560 displayH = Math.round(h * minW / w);
561 } else if (h < minH) {
562 // the image is too short
564 displayW = Math.round(w * minH / h);
567 // resize the image if it is too large
568 if (w > maxW && h > maxH) {
569 // the image is both too tall and wide, so compare the aspect ratios
570 // to determine whether to max the width or height
571 if (w / h > maxW / maxH) {
573 displayH = Math.round(h * maxW / w);
576 displayW = Math.round(w * maxH / h);
578 } else if (w > maxW) {
579 // the image is too wide
581 displayH = Math.round(h * maxW / w);
582 } else if (h > maxH) {
583 // the image is too tall
585 displayW = Math.round(w * maxH / h);
589 var ratio = Math.min(maxW / w, maxH / h);
591 var zoomedW = Math.round(w * ratio);
592 var zoomedH = Math.round(h * ratio);
594 displayW = Math.max(minW, zoomedW);
595 displayH = Math.max(minH, zoomedH);
599 'width': displayW || 0,
600 'height': displayH || 0 // NaN is possible when dimensions.width is 0
604 // format the given dimension for passing into the `css()` method of `jqLite`
605 var formatDimension = function (dimension) {
606 return typeof dimension === 'number' ? dimension + 'px' : dimension;
609 // the dimensions of the image
614 'link': function (scope, element, attrs) {
615 // resize the img element and the containing modal
616 var resize = function () {
617 // get the window dimensions
618 var windowWidth = $window.innerWidth;
619 var windowHeight = $window.innerHeight;
621 // calculate the max/min dimensions for the image
622 var imageDimensionLimits = Lightbox.calculateImageDimensionLimits({
623 'windowWidth': windowWidth,
624 'windowHeight': windowHeight,
625 'imageWidth': imageWidth,
626 'imageHeight': imageHeight
629 // calculate the dimensions to display the image
630 var imageDisplayDimensions = calculateImageDisplayDimensions(
633 'height': imageHeight,
638 }, imageDimensionLimits),
639 Lightbox.fullScreenMode
642 // calculate the dimensions of the modal container
643 var modalDimensions = Lightbox.calculateModalDimensions({
644 'windowWidth': windowWidth,
645 'windowHeight': windowHeight,
646 'imageDisplayWidth': imageDisplayDimensions.width,
647 'imageDisplayHeight': imageDisplayDimensions.height
652 'width': imageDisplayDimensions.width + 'px',
653 'height': imageDisplayDimensions.height + 'px'
656 // setting the height on .modal-dialog does not expand the div with the
657 // background, which is .modal-content
659 document.querySelector('.lightbox-modal .modal-dialog')
661 'width': formatDimension(modalDimensions.width)
664 // .modal-content has no width specified; if we set the width on
665 // .modal-content and not on .modal-dialog, .modal-dialog retains its
666 // default width of 600px and that places .modal-content off center
668 document.querySelector('.lightbox-modal .modal-content')
670 'height': formatDimension(modalDimensions.height)
674 // load the new image and/or resize the video whenever the attr changes
675 scope.$watch(function () {
676 return attrs.lightboxSrc;
678 // do nothing if there's no image
679 if (!Lightbox.image) {
683 if (!Lightbox.isVideo(Lightbox.image)) { // image
684 // blank the image before resizing the element
685 element[0].src = '#';
687 // handle failure to load the image
688 var failure = function () {
696 ImageLoader.load(src).then(function (image) {
697 // these variables must be set before resize(), as they are used
699 imageWidth = image.naturalWidth;
700 imageHeight = image.naturalHeight;
702 // resize the img element and the containing modal
706 element[0].src = src;
712 // default dimensions
716 // resize the video element and the containing modal
719 // the src attribute applies to `<video>` and not `<embed-video>`
720 element[0].src = src;
724 // resize the image and modal whenever the window gets resized
725 angular.element($window).on('resize', resize);