PageSourceSearch

https://dallascounty.org/web_resources/widgets/image/lightbox/lightbox.js

js dallascounty.org collected 2026-10-01 10:29:50 UTC 19,724 bytes, 533 lines download raw bytes

vendor: 5,841 bytes, lines 1-141
1/*
2 *     Percussion CMS
3 *     Copyright (C) 1999-2020 Percussion Software, Inc.
4 *
5 *     This program is free software: you can redistribute it and/or modify
6 *     it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
7 *
8 *     This program is distributed in the hope that it will be useful,
9 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
10 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 *     GNU Affero General Public License for more details.
12 *
13 *     Mailing Address:
14 *
15 *      Percussion Software, Inc.
16 *      PO Box 767
17 *      Burlington, MA 01803, USA
18 *      +01-781-438-9900
19 *      [email protected]
20 *      https://www.percussion.com
21 *
22 *     You should have received a copy of the GNU Affero General Public License along with this program.  If not, see <https://www.gnu.org/licenses/>
23 */
24
25// Uses Node, AMD or browser globals to create a module.
26(function (root, factory) {
27    if (typeof define === 'function' && define.amd) {
28        // AMD. Register as an anonymous module.
29        define(['jquery'], factory);
30    } else if (typeof exports === 'object') {
31        // Node. Does not work with strict CommonJS, but
32        // only CommonJS-like environments that support module.exports,
33        // like Node.
34        module.exports = factory(require('jquery'));
35    } else {
36        // Browser globals (root is window)
37        root.lightbox = factory(root.jQuery);
38    }
39}(this, function ($) {
40
41  function Lightbox(options) {
42    this.album = [];
43    this.currentImageIndex = void 0;
44    this.init();
45
46    // options
47    this.options = $.extend({}, this.constructor.defaults);
48    this.option(options);
49  }
50
51  // Descriptions of all options available on the demo site:
52  // http://lokeshdhakar.com/projects/lightbox2/index.html#options
53  Lightbox.defaults = {
54    albumLabel: 'Image %1 of %2',
55    alwaysShowNavOnTouchDevices: false,
56    fadeDuration: 600,
57    fitImagesInViewport: true,
58    imageFadeDuration: 600,
59    // maxWidth: 800,
60    // maxHeight: 600,
61    positionFromTop: 50,
62    resizeDuration: 700,
63    showImageNumberLabel: true,
64    wrapAround: false,
65    disableScrolling: false,
66    /*
67    Sanitize Title
68    If the caption data is trusted, for example you are hardcoding it in, then leave this to false.
69    This will free you to add html tags, such as links, in the caption.
70
71    If the caption data is user submitted or from some other untrusted source, then set this to true
72    to prevent xss and other injection attacks.
73     */
74    sanitizeTitle: false
75  };
76
77  Lightbox.prototype.option = function(options) {
78    $.extend(this.options, options);
79  };
80
81  Lightbox.prototype.imageCountLabel = function(currentImageNum, totalImages) {
82    return this.options.albumLabel.replace(/%1/g, currentImageNum).replace(/%2/g, totalImages);
83  };
84
85  Lightbox.prototype.init = function() {
86    var self = this;
87    // Both enable and build methods require the body tag to be in the DOM.
88    $(document).ready(function() {
89      self.enable();
90      self.build();
91    });
92  };
93
94  // Loop through anchors and areamaps looking for either data-lightbox attributes or rel attributes
95  // that contain 'lightbox'. When these are clicked, start lightbox.
96  Lightbox.prototype.enable = function() {
97    var self = this;
98    $('body').on('click', 'a[rel^=lightbox], area[rel^=lightbox], a[data-lightbox], area[data-lightbox]', function(event) {
99      self.start($(event.currentTarget));
100      return false;
101    });
102  };
103
104  // Build html for the lightbox and the overlay.
105  // Attach event handlers to the new DOM elements. click click click
106  Lightbox.prototype.build = function() {
107    if ($('#lightbox').length > 0) {
108        return;
109    }
110
111    var self = this;
112    $('<div id="lightboxOverlay" class="lightboxOverlay"></div><div id="lightbox" class="lightbox"><div class="lb-outerContainer"><div class="lb-container"><img class="lb-image" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" /><div class="lb-nav"><a class="lb-prev" href="" ></a><a class="lb-next" href="" ></a></div><div class="lb-loader"><a class="lb-cancel"></a></div></div></div><div class="lb-dataContainer"><div class="lb-data"><div class="lb-details"><span class="lb-caption"></span><span class="lb-number"></span></div><div class="lb-closeContainer"><a class="lb-close"></a></div></div></div></div>').appendTo($('body'));
113
114    // Cache jQuery objects
115    this.$lightbox       = $('#lightbox');
116    this.$overlay        = $('#lightboxOverlay');
117    this.$outerContainer = this.$lightbox.find('.lb-outerContainer');
118    this.$container      = this.$lightbox.find('.lb-container');
119    this.$image          = this.$lightbox.find('.lb-image');
120    this.$nav            = this.$lightbox.find('.lb-nav');
121
122    // Store css values for future lookup
123    this.containerPadding = {
124      top: parseInt(this.$container.css('padding-top'), 10),
125      right: parseInt(this.$container.css('padding-right'), 10),
126      bottom: parseInt(this.$container.css('padding-bottom'), 10),
127      left: parseInt(this.$container.css('padding-left'), 10)
128    };
129
130    this.imageBorderWidth = {
131      top: parseInt(this.$image.css('border-top-width'), 10),
132      right: parseInt(this.$image.css('border-right-width'), 10),
133      bottom: parseInt(this.$image.css('border-bottom-width'), 10),
134      left: parseInt(this.$image.css('border-left-width'), 10)
135    };
136
137    // Attach event handlers to the newly minted DOM elements
138    this.$overlay.hide().on('click', function() {
139      self.end();
140      return false;
141    });
142
143    this.$lightbox.hide().on('click', function(event) {
144      if ($(event.target).attr('id') === 'lightbox') {
145        self.end();
146      }
147      return false;
148    });
149
150    this.$outerContainer.on('click', function(event) {
151      if ($(event.target).attr('id') === 'lightbox') {
152        self.end();
153      }
154      return false;
155    });
156
157    this.$lightbox.find('.lb-prev').on('click', function() {
158      if (self.currentImageIndex === 0) {
159        self.changeImage(self.album.length - 1);
160      } else {
161        self.changeImage(self.currentImageIndex - 1);
162      }
163      return false;
164    });
165
166    this.$lightbox.find('.lb-next').on('click', function() {
167      if (self.currentImageIndex === self.album.length - 1) {
168        self.changeImage(0);
169      } else {
170        self.changeImage(self.currentImageIndex + 1);
171      }
172      return false;
173    });
174
175    /*
176      Show context menu for image on right-click
177
178      There is a div containing the navigation that spans the entire image and lives above of it. If
179      you right-click, you are right clicking this div and not the image. This prevents users from
180      saving the image or using other context menu actions with the image.
181
182      To fix this, when we detect the right mouse button is pressed down, but not yet clicked, we
183      set pointer-events to none on the nav div. This is so that the upcoming right-click event on
184      the next mouseup will bubble down to the image. Once the right-click/contextmenu event occurs
185      we set the pointer events back to auto for the nav div so it can capture hover and left-click
186      events as usual.
187     */
188    this.$nav.on('mousedown', function(event) {
189      if (event.which === 3) {
190        self.$nav.css('pointer-events', 'none');
191
192        self.$lightbox.one('contextmenu', function() {
193          setTimeout(function() {
194              this.$nav.css('pointer-events', 'auto');
195          }.bind(self), 0);
196        });
197      }
198    });
199
200
201    this.$lightbox.find('.lb-loader, .lb-close').on('click', function() {
202      self.end();
203      return false;
204    });
205  };
206
207  // Show overlay and lightbox. If the image is part of a set, add siblings to album array.
208  Lightbox.prototype.start = function($link) {
209    var self    = this;
210    var $window = $(window);
211
212    $window.on('resize', function(evt){
213        $.proxy(this.sizeOverlay, this);
214    });
215
216    $('select, object, embed').css({
217      visibility: 'hidden'
218    });
219
220    this.sizeOverlay();
221
222    this.album = [];
223    var imageNumber = 0;
224
225    function addToAlbum($link) {
226      self.album.push({
227        alt: $link.attr('data-alt'),
228        link: $link.attr('href'),
229        title: $link.attr('data-title') || $link.attr('title')
230      });
231    }
232
233    // Support both data-lightbox attribute and rel attribute implementations
234    var dataLightboxValue = $link.attr('data-lightbox');
235    var $links;
236
237    if (dataLightboxValue) {
238      $links = $($link.prop('tagName') + '[data-lightbox="' + dataLightboxValue + '"]');
239      for (var i = 0; i < $links.length; i = ++i) {
240        addToAlbum($($links[i]));
241        if ($links[i] === $link[0]) {
242          imageNumber = i;
243        }
244      }
245    } else {
246      if ($link.attr('rel') === 'lightbox') {
247        // If image is not part of a set
248        addToAlbum($link);
249      } else {
250        // If image is part of a set
251        $links = $($link.prop('tagName') + '[rel="' + $link.attr('rel') + '"]');
252        for (var j = 0; j < $links.length; j = ++j) {
253          addToAlbum($($links[j]));
254          if ($links[j] === $link[0]) {
255            imageNumber = j;
256          }
257        }
258      }
259    }
260
261    // Position Lightbox
262    var top  = $window.scrollTop() + this.options.positionFromTop;
263    var left = $window.scrollLeft();
264    this.$lightbox.css({
265      top: top + 'px',
266      left: left + 'px'
267    }).fadeIn(this.options.fadeDuration);
268
269    // Disable scrolling of the page while open
270    if (this.options.disableScrolling) {
271      $('html').addClass('lb-disable-scrolling');
272    }
273
274    this.changeImage(imageNumber);
275  };
276
277  // Hide most UI elements in preparation for the animated resizing of the lightbox.
278  Lightbox.prototype.changeImage = function(imageNumber) {
279    var self = this;
280
281    this.disableKeyboardNav();
282    var $image = this.$lightbox.find('.lb-image');
283
284    this.$overlay.fadeIn(this.options.fadeDuration);
285
286    $('.lb-loader').fadeIn('slow');
287    this.$lightbox.find('.lb-image, .lb-nav, .lb-prev, .lb-next, .lb-dataContainer, .lb-numbers, .lb-caption').hide();
288
289    this.$outerContainer.addClass('animating');
290
291    // When image to show is preloaded, we send the width and height to sizeContainer()
292    var preloader = new Image();
293    preloader.onload = function() {
294      var $preloader;
295      var imageHeight;
296      var imageWidth;
297      var maxImageHeight;
298      var maxImageWidth;
299      var windowHeight;
300      var windowWidth;
301
302      $image.attr({
303        'alt': self.album[imageNumber].alt,
304        'src': self.album[imageNumber].link
305      });
306
307      $preloader = $(preloader);
308
309      $image.width(preloader.width);
310      $image.height(preloader.height);
311
312      if (self.options.fitImagesInViewport) {
313        // Fit image inside the viewport.
314        // Take into account the border around the image and an additional 10px gutter on each side.
315
316        windowWidth    = $(window).width();
317        windowHeight   = $(window).height();
318        maxImageWidth  = windowWidth - self.containerPadding.left - self.containerPadding.right - self.imageBorderWidth.left - self.imageBorderWidth.right - 20;
319        maxImageHeight = windowHeight - self.containerPadding.top - self.containerPadding.bottom - self.imageBorderWidth.top - self.imageBorderWidth.bottom - 120;
320
321        // Check if image size is larger then maxWidth|maxHeight in settings
322        if (self.options.maxWidth && self.options.maxWidth < maxImageWidth) {
323          maxImageWidth = self.options.maxWidth;
324        }
325        if (self.options.maxHeight && self.options.maxHeight < maxImageWidth) {
326          maxImageHeight = self.options.maxHeight;
327        }
328
329        // Is the current image's width or height is greater than the maxImageWidth or maxImageHeight
330        // option than we need to size down while maintaining the aspect ratio.
331        if ((preloader.width > maxImageWidth) || (preloader.height > maxImageHeight)) {
332          if ((preloader.width / maxImageWidth) > (preloader.height / maxImageHeight)) {
333            imageWidth  = maxImageWidth;
334            imageHeight = parseInt(preloader.height / (preloader.width / imageWidth), 10);
335            $image.width(imageWidth);
336            $image.height(imageHeight);
337          } else {
338            imageHeight = maxImageHeight;
339            imageWidth = parseInt(preloader.width / (preloader.height / imageHeight), 10);
340            $image.width(imageWidth);
341            $image.height(imageHeight);
342          }
343        }
344      }
345      self.sizeContainer($image.width(), $image.height());
346    };
347
348    preloader.src          = this.album[imageNumber].link;
349    this.currentImageIndex = imageNumber;
350  };
351
352  // Stretch overlay to fit the viewport
353  Lightbox.prototype.sizeOverlay = function() {
354    this.$overlay
355      .width($(document).width())
356      .height($(document).height());
357  };
358
359  // Animate the size of the lightbox to fit the image we are showing
360  Lightbox.prototype.sizeContainer = function(imageWidth, imageHeight) {
361    var self = this;
362
363    var oldWidth  = this.$outerContainer.outerWidth();
364    var oldHeight = this.$outerContainer.outerHeight();
365    var newWidth  = imageWidth + this.containerPadding.left + this.containerPadding.right + this.imageBorderWidth.left + this.imageBorderWidth.right;
366    var newHeight = imageHeight + this.containerPadding.top + this.containerPadding.bottom + this.imageBorderWidth.top + this.imageBorderWidth.bottom;
367
368    function postResize() {
369      self.$lightbox.find('.lb-dataContainer').width(newWidth);
370      self.$lightbox.find('.lb-prevLink').height(newHeight);
371      self.$lightbox.find('.lb-nextLink').height(newHeight);
372      self.showImage();
373    }
374
375    if (oldWidth !== newWidth || oldHeight !== newHeight) {
376      this.$outerContainer.animate({
377        width: newWidth,
378        height: newHeight
379      }, this.options.resizeDuration, 'swing', function() {
380        postResize();
381      });
382    } else {
383      postResize();
384    }
385  };
386
387  // Display the image and its details and begin preload neighboring images.
388  Lightbox.prototype.showImage = function() {
389    this.$lightbox.find('.lb-loader').stop(true).hide();
390    this.$lightbox.find('.lb-image').fadeIn(this.options.imageFadeDuration);
391
392    this.updateNav();
393    this.updateDetails();
394    this.preloadNeighboringImages();
395    this.enableKeyboardNav();
396  };
397
398  // Display previous and next navigation if appropriate.
399  Lightbox.prototype.updateNav = function() {
400    // Check to see if the browser supports touch events. If so, we take the conservative approach
401    // and assume that mouse hover events are not supported and always show prev/next navigation
402    // arrows in image sets.
403    var alwaysShowNav = false;
404    try {
405      document.createEvent('TouchEvent');
406      alwaysShowNav = (this.options.alwaysShowNavOnTouchDevices) ? true : false;
407    } catch (e) {}
408
409    this.$lightbox.find('.lb-nav').show();
410
411    if (this.album.length > 1) {
412      if (this.options.wrapAround) {
413        if (alwaysShowNav) {
414          this.$lightbox.find('.lb-prev, .lb-next').css('opacity', '1');
415        }
416        this.$lightbox.find('.lb-prev, .lb-next').show();
417      } else {
418        if (this.currentImageIndex > 0) {
419          this.$lightbox.find('.lb-prev').show();
420          if (alwaysShowNav) {
421            this.$lightbox.find('.lb-prev').css('opacity', '1');
422          }
423        }
424        if (this.currentImageIndex < this.album.length - 1) {
425          this.$lightbox.find('.lb-next').show();
426          if (alwaysShowNav) {
427            this.$lightbox.find('.lb-next').css('opacity', '1');
428          }
429        }
430      }
431    }
432  };
433
434  // Display caption, image number, and closing button.
435  Lightbox.prototype.updateDetails = function() {
436    var self = this;
437
438    // Enable anchor clicks in the injected caption html.
439    // Thanks Nate Wright for the fix. @https://github.com/NateWr
440    if (typeof this.album[this.currentImageIndex].title !== 'undefined' &&
441      this.album[this.currentImageIndex].title !== '') {
442      var $caption = this.$lightbox.find('.lb-caption');
443      if (this.options.sanitizeTitle) {
444        $caption.text(this.album[this.currentImageIndex].title);
445      } else {
446        $caption.html(this.album[this.currentImageIndex].title);
447      }
448      $caption.fadeIn('fast')
449        .find('a').on('click', function(event) {
450          if ($(this).attr('target') !== undefined) {
451            window.open($(this).attr('href'), $(this).attr('target'));
452          } else {
453            location.href = $(this).attr('href');
454          }
455        });
456    }
457
458    if (this.album.length > 1 && this.options.showImageNumberLabel) {
459      var labelText = this.imageCountLabel(this.currentImageIndex + 1, this.album.length);
460      this.$lightbox.find('.lb-number').text(labelText).fadeIn('fast');
461    } else {
462      this.$lightbox.find('.lb-number').hide();
463    }
464
465    this.$outerContainer.removeClass('animating');
466
467    this.$lightbox.find('.lb-dataContainer').fadeIn(this.options.resizeDuration, function() {
468      return self.sizeOverlay();
469    });
470  };
471
472  // Preload previous and next images in set.
473  Lightbox.prototype.preloadNeighboringImages = function() {
474    if (this.album.length > this.currentImageIndex + 1) {
475      var preloadNext = new Image();
476      preloadNext.src = this.album[this.currentImageIndex + 1].link;
477    }
478    if (this.currentImageIndex > 0) {
479      var preloadPrev = new Image();
480      preloadPrev.src = this.album[this.currentImageIndex - 1].link;
481    }
482  };
483
484  Lightbox.prototype.enableKeyboardNav = function() {
485    $(document).on('keyup.keyboard', function(evt){
486        $.proxy(this.keyboardAction, this);
487    });
488  };
489
490  Lightbox.prototype.disableKeyboardNav = function() {
491    $(document).off('.keyboard');
492  };
493
494  Lightbox.prototype.keyboardAction = function(event) {
495    var KEYCODE_ESC        = 27;
496    var KEYCODE_LEFTARROW  = 37;
497    var KEYCODE_RIGHTARROW = 39;
498
499    var keycode = event.keyCode;
500    var key     = String.fromCharCode(keycode).toLowerCase();
501    if (keycode === KEYCODE_ESC || key.match(/x|o|c/)) {
502      this.end();
503    } else if (key === 'p' || keycode === KEYCODE_LEFTARROW) {
504      if (this.currentImageIndex !== 0) {
505        this.changeImage(this.currentImageIndex - 1);
506      } else if (this.options.wrapAround && this.album.length > 1) {
507        this.changeImage(this.album.length - 1);
508      }
509    } else if (key === 'n' || keycode === KEYCODE_RIGHTARROW) {
510      if (this.currentImageIndex !== this.album.length - 1) {
511        this.changeImage(this.currentImageIndex + 1);
512      } else if (this.options.wrapAround && this.album.length > 1) {
513        this.changeImage(0);
514      }
515    }
516  };
517
518  // Closing time. :-(
519  Lightbox.prototype.end = function() {
520    this.disableKeyboardNav();
521    $(window).off('resize', this.sizeOverlay);
522    this.$lightbox.fadeOut(this.options.fadeDuration);
523    this.$overlay.fadeOut(this.options.fadeDuration);
524    $('select, object, embed').css({
525      visibility: 'visible'
526    });
527    if (this.options.disableScrolling) {
528      $('html').removeClass('lb-disable-scrolling');
529    }
530  };
531
532  return new Lightbox();
533}));

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.