1/** 2 * Featherlight - ultra slim jQuery lightbox 3 * Version 1.7.8 - http://noelboss.github.io/featherlight/ 4 * 5 * Copyright 2017, Noël Raoul Bossart (http://www.noelboss.com) 6 * MIT Licensed. 7**/ 8(function($) { 9 "use strict"; 10 11 if('undefined' === typeof $) { 12 if('console' in window){ window.console.info('Too much lightness, Featherlight needs jQuery.'); } 13 return; 14 } 15 16 /* Featherlight is exported as $.featherlight. 17 It is a function used to open a featherlight lightbox. 18 19 [tech] 20 Featherlight uses prototype inheritance. 21 Each opened lightbox will have a corresponding object. 22 That object may have some attributes that override the 23 prototype's. 24 Extensions created with Featherlight.extend will have their 25 own prototype that inherits from Featherlight's prototype, 26 thus attributes can be overriden either at the object level, 27 or at the extension level. 28 To create callbacks that chain themselves instead of overriding, 29 use chainCallbacks. 30 For those familiar with CoffeeScript, this correspond to 31 Featherlight being a class and the Gallery being a class 32 extending Featherlight. 33 The chainCallbacks is used since we don't have access to 34 CoffeeScript's `super`. 35 */ 36 37 function Featherlight($content, config) { 38 if(this instanceof Featherlight) { /* called with new */ 39 this.id = Featherlight.id++; 40 this.setup($content, config); 41 this.chainCallbacks(Featherlight._callbackChain); 42 } else { 43 var fl = new Featherlight($content, config); 44 fl.open(); 45 return fl; 46 } 47 } 48 49 var opened = [], 50 pruneOpened = function(remove) { 51 opened = $.grep(opened, function(fl) { 52 return fl !== remove && fl.$instance.closest('body').length > 0; 53 } ); 54 return opened; 55 }; 56 57 // Removes keys of `set` from `obj` and returns the removed key/values. 58 function slice(obj, set) { 59 var r = {}; 60 for (var key in obj) { 61 if (key in set) { 62 r[key] = obj[key]; 63 delete obj[key]; 64 } 65 } 66 return r; 67 } 68 69 // NOTE: List of available [iframe attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe). 70 var iFrameAttributeSet = { 71 allowfullscreen: 1, frameborder: 1, height: 1, longdesc: 1, marginheight: 1, marginwidth: 1, 72 name: 1, referrerpolicy: 1, scrolling: 1, sandbox: 1, src: 1, srcdoc: 1, width: 1 73 }; 74 75 // Converts camelCased attributes to dasherized versions for given prefix: 76 // parseAttrs({hello: 1, hellFrozeOver: 2}, 'hell') => {froze-over: 2} 77 function parseAttrs(obj, prefix) { 78 var attrs = {}, 79 regex = new RegExp('^' + prefix + '([A-Z])(.*)'); 80 for (var key in obj) { 81 var match = key.match(regex); 82 if (match) { 83 var dasherized = (match[1] + match[2].replace(/([A-Z])/g, '-$1')).toLowerCase(); 84 attrs[dasherized] = obj[key]; 85 } 86 } 87 return attrs; 88 } 89 90 /* document wide key handler */ 91 var eventMap = { keyup: 'onKeyUp', resize: 'onResize' }; 92 93 var globalEventHandler = function(event) { 94 $.each(Featherlight.opened().reverse(), function() { 95 if (!event.isDefaultPrevented()) { 96 if (false === this[eventMap[event.type]](event)) { 97 event.preventDefault(); event.stopPropagation(); return false; 98 } 99 } 100 }); 101 }; 102 103 var toggleGlobalEvents = function(set) { 104 if(set !== Featherlight._globalHandlerInstalled) { 105 Featherlight._globalHandlerInstalled = set; 106 var events = $.map(eventMap, function(_, name) { return name+'.'+Featherlight.prototype.namespace; } ).join(' '); 107 $(window)[set ? 'on' : 'off'](events, globalEventHandler); 108 } 109 }; 110 111 var globalNoClose_b = true; 112 var globalOpenTimeout; 113 Featherlight.prototype = { 114 constructor: Featherlight, 115 /*** defaults ***/ 116 /* extend featherlight with defaults and methods */ 117 namespace: 'featherlight', /* Name of the events and css class prefix */ 118 targetAttr: 'data-featherlight', /* Attribute of the triggered element that contains the selector to the lightbox content */ 119 variant: null, /* Class that will be added to change look of the lightbox */ 120 resetCss: false, /* Reset all css */ 121 background: null, /* Custom DOM for the background, wrapper and the closebutton */ 122 openTrigger: 'click', /* Event that triggers the lightbox */ 123 closeTrigger: 'click', /* Event that triggers the closing of the lightbox */ 124 filter: null, /* Selector to filter events. Think $(...).on('click', filter, eventHandler) */ 125 root: 'body', /* Where to append featherlights */ 126 openSpeed: 250, /* Duration of opening animation */ 127 closeSpeed: 250, /* Duration of closing animation */ 128 closeOnClick: 'background', /* Close lightbox on click ('background', 'anywhere' or false) */ 129 closeOnEsc: true, /* Close lightbox when pressing esc */ 130 closeIcon: '✕', /* Close icon */ 131 loading: '', /* Content to show while initial content is loading */ 132 persist: false, /* If set, the content will persist and will be shown again when opened again. 'shared' is a special value when binding multiple elements for them to share the same content */ 133 otherClose: null, /* Selector for alternate close buttons (e.g. "a.close") */ 134 beforeOpen: $.noop, /* Called before open. can return false to prevent opening of lightbox. Gets event as parameter, this contains all data */ 135 beforeContent: $.noop, /* Called when content is loaded. Gets event as parameter, this contains all data */ 136 beforeClose: $.noop, /* Called before close. can return false to prevent opening of lightbox. Gets event as parameter, this contains all data */ 137 afterOpen: $.noop, /* Called after open. Gets event as parameter, this contains all data */ 138 afterContent: $.noop, /* Called after content is ready and has been set. Gets event as parameter, this contains all data */ 139 afterClose: $.noop, /* Called after close. Gets event as parameter, this contains all data */ 140 onKeyUp: $.noop, /* Called on key up for the frontmost featherlight */ 141 onResize: $.noop, /* Called after new content and when a window is resized */ 142 type: null, /* Specify type of lightbox. If unset, it will check for the targetAttrs value. */ 143 contentFilters: ['jquery', 'image', 'html', 'ajax', 'iframe', 'text'], /* List of content filters to use to determine the content */ 144 145 /*** methods ***/ 146 /* setup iterates over a single instance of featherlight and prepares the background and binds the events */ 147 setup: function(target, config){ 148 /* all arguments are optional */ 149 if (typeof target === 'object' && target instanceof $ === false && !config) { 150 config = target; 151 target = undefined; 152 } 153 154 var self = $.extend(this, config, {target: target}), 155 css = !self.resetCss ? self.namespace : self.namespace+'-reset', /* by adding -reset to the classname, we reset all the default css */ 156 $background = $(self.background || [ 157 '<div class="'+css+'-loading '+css+'">', 158 '<div class="'+css+'-content">', 159 '<button class="'+css+'-close-icon '+ self.namespace + '-close" aria-label="Close">', 160 self.closeIcon, 161 '</button>', 162 '<div class="'+self.namespace+'-inner">' + self.loading + '</div>', 163 '</div>', 164 '</div>'].join('')), 165 closeButtonSelector = '.'+self.namespace+'-close' + (self.otherClose ? ',' + self.otherClose : ''); 166 167 self.$instance = $background.clone().addClass(self.variant); /* clone DOM for the background, wrapper and the close button */ 168 169 /* close when click on background/anywhere/null or closebox */ 170 self.$instance.on(self.closeTrigger+'.'+self.namespace, function(event) { 171 var $target = $(event.target); 172 if( ('background' === self.closeOnClick && $target.is('.'+self.namespace)) 173 || 'anywhere' === self.closeOnClick 174 || $target.closest(closeButtonSelector).length ){ 175 176 // DK: we're going to close on 177 // mousemove, but not within a while 178 // of open. 179 if (! globalNoClose_b) { 180 self.close(event); 181 event.preventDefault(); 182 } 183 } 184 }); 185 186 return this; 187 }, 188
189 /* this method prepares the content and converts it into a jQuery object or a promise */ 190 getContent: function(){ 191 if(this.persist !== false && this.$content) { 192 return this.$content; 193 } 194 var self = this, 195 filters = this.constructor.contentFilters, 196 readTargetAttr = function(name){ return self.$currentTarget && self.$currentTarget.attr(name); }, 197 targetValue = readTargetAttr(self.targetAttr), 198 data = self.target || targetValue || ''; 199 200 /* Find which filter applies */ 201 var filter = filters[self.type]; /* check explicit type like {type: 'image'} */ 202 203 /* check explicit type like data-featherlight="image" */ 204 if(!filter && data in filters) { 205 filter = filters[data]; 206 data = self.target && targetValue; 207 } 208 data = data || readTargetAttr('href') || ''; 209 210 /* check explicity type & content like {image: 'photo.jpg'} */ 211 if(!filter) { 212 for(var filterName in filters) { 213 if(self[filterName]) { 214 filter = filters[filterName]; 215 data = self[filterName]; 216 } 217 } 218 } 219 220 /* otherwise it's implicit, run checks */ 221 if(!filter) { 222 var target = data; 223 data = null; 224 $.each(self.contentFilters, function() { 225 filter = filters[this]; 226 if(filter.test) { 227 data = filter.test(target); 228 } 229 if(!data && filter.regex && target.match && target.match(filter.regex)) { 230 data = target; 231 } 232 return !data; 233 }); 234 if(!data) { 235 if('console' in window){ window.console.error('Featherlight: no content filter found ' + (target ? ' for "' + target + '"' : ' (no target specified)')); } 236 return false; 237 } 238 } 239 /* Process it */ 240 return filter.process.call(self, data); 241 }, 242 243 /* sets the content of $instance to $content */ 244 setContent: function($content){ 245 var self = this; 246 /* we need a special class for the iframe */ 247 if($content.is('iframe')) { 248 self.$instance.addClass(self.namespace+'-iframe'); 249 } 250 251 self.$instance.removeClass(self.namespace+'-loading'); 252 253 /* replace content by appending to existing one before it is removed 254 this insures that featherlight-inner remain at the same relative 255 position to any other items added to featherlight-content */ 256 self.$instance.find('.'+self.namespace+'-inner') 257 .not($content) /* excluded new content, important if persisted */ 258 .slice(1).remove().end() /* In the unexpected event where there are many inner elements, remove all but the first one */ 259 .replaceWith($.contains(self.$instance[0], $content[0]) ? '' : $content); 260 261 self.$content = $content.addClass(self.namespace+'-inner'); 262 263 return self; 264 }, 265 266 /* opens the lightbox. "this" contains $instance with the lightbox, and with the config. 267 Returns a promise that is resolved after is successfully opened. */ 268 open: function(event){ 269 var self = this; 270 self.$instance.hide().appendTo(self.root); 271 if((!event || !event.isDefaultPrevented()) 272 && self.beforeOpen(event) !== false) { 273 274 if(event){ 275 event.preventDefault(); 276 } 277 var $content = self.getContent(); 278 279 if($content) { 280 opened.push(self); 281 282 toggleGlobalEvents(true); 283 284 self.$instance.fadeIn(self.openSpeed); 285 self.beforeContent(event); 286 287 /* Set content and show */ 288 return $.when($content) 289 .always(function($content){ 290 self.setContent($content); 291 self.afterContent(event); 292 }) 293 .then(self.$instance.promise()) 294 /* Call afterOpen after fadeIn is done */ 295 .done(function(){ self.afterOpen(event); }); 296 } 297 } 298 self.$instance.detach(); 299 return $.Deferred().reject().promise(); 300 }, 301 302 /* closes the lightbox. "this" contains $instance with the lightbox, and with the config 303 returns a promise, resolved after the lightbox is successfully closed. */ 304 close: function(event){ 305 var self = this, 306 deferred = $.Deferred(); 307 308 if(self.beforeClose(event) === false) { 309 deferred.reject(); 310 } else { 311 312 if (0 === pruneOpened(self).length) { 313 toggleGlobalEvents(false); 314 } 315 316 self.$instance.fadeOut(self.closeSpeed,function(){ 317 self.$instance.detach(); 318 self.afterClose(event); 319 deferred.resolve(); 320 }); 321 } 322 return deferred.promise(); 323 }, 324 325 /* resizes the content so it fits in visible area and keeps the same aspect ratio. 326 Does nothing if either the width or the height is not specified. 327 Called automatically on window resize. 328 Override if you want different behavior. */ 329 resize: function(w, h) { 330 if (w && h) { 331 /* Reset apparent image size first so container grows */ 332 this.$content.css('width', '').css('height', ''); 333 /* Calculate the worst ratio so that dimensions fit */ 334 /* Note: -1 to avoid rounding errors */ 335 var ratio = Math.max( 336 w / (this.$content.parent().width()-1), 337 h / (this.$content.parent().height()-1)); 338 /* Resize content */ 339 if (ratio > 1) { 340 ratio = h / Math.floor(h / ratio); /* Round ratio down so height calc works */ 341 this.$content.css('width', '' + w / ratio + 'px').css('height', '' + h / ratio + 'px'); 342 } 343 } 344 }, 345 346 /* Utility function to chain callbacks 347 [Warning: guru-level] 348 Used be extensions that want to let users specify callbacks but 349 also need themselves to use the callbacks. 350 The argument 'chain' has callback names as keys and function(super, event) 351 as values. That function is meant to call `super` at some point. 352 */
353 chainCallbacks: function(chain) { 354 for (var name in chain) { 355 this[name] = $.proxy(chain[name], this, $.proxy(this[name], this)); 356 } 357 } 358 }; 359 360 $.extend(Featherlight, { 361 id: 0, /* Used to id single featherlight instances */ 362 autoBind: '[data-featherlight]', /* Will automatically bind elements matching this selector. Clear or set before onReady */ 363 defaults: Featherlight.prototype, /* You can access and override all defaults using $.featherlight.defaults, which is just a synonym for $.featherlight.prototype */ 364 /* Contains the logic to determine content */ 365 contentFilters: { 366 jquery: { 367 regex: /^[#.]\w/, /* Anything that starts with a class name or identifiers */ 368 test: function(elem) { return elem instanceof $ && elem; }, 369 process: function(elem) { return this.persist !== false ? $(elem) : $(elem).clone(true); } 370 }, 371 image: { 372 regex: /\.(png|jpg|jpeg|gif|tiff|bmp|svg)(\?\S*)?$/i, 373 process: function(url) { 374 var self = this, 375 deferred = $.Deferred(), 376 img = new Image(), 377 $img = $('<img src="'+url+'" alt="Could not load full-size image" class="'+self.namespace+'-image" />'); 378 img.onload = function() { 379 /* Store naturalWidth & height for IE8 */ 380 $img.naturalWidth = img.width; $img.naturalHeight = img.height; 381 deferred.resolve( $img ); 382 }; 383 img.onerror = function() { deferred.reject($img); }; 384 img.src = url; 385 return deferred.promise(); 386 } 387 }, 388 html: { 389 regex: /^\s*<[\w!][^<]*>/, /* Anything that starts with some kind of valid tag */ 390 process: function(html) { return $(html); } 391 }, 392 ajax: { 393 regex: /./, /* At this point, any content is assumed to be an URL */ 394 process: function(url) { 395 var self = this, 396 deferred = $.Deferred(); 397 /* we are using load so one can specify a target with: url.html #targetelement */
398 var $container = $('<div></div>').load(url, function(response, status){ 399 if ( status !== "error" ) { 400 deferred.resolve($container.contents()); 401 } 402 deferred.fail(); 403 }); 404 return deferred.promise(); 405 } 406 }, 407 iframe: { 408 process: function(url) { 409 var deferred = new $.Deferred(); 410 var $content = $('<iframe/>'); 411 var css = parseAttrs(this, 'iframe'); 412 var attrs = slice(css, iFrameAttributeSet); 413 $content.hide() 414 .attr('src', url) 415 .attr(attrs) 416 .css(css) 417 .on('load', function() { deferred.resolve($content.show()); }) 418 // We can't move an <iframe> and avoid reloading it, 419 // so let's put it in place ourselves right now: 420 .appendTo(this.$instance.find('.' + this.namespace + '-content')); 421 return deferred.promise(); 422 } 423 }, 424 text: { 425 process: function(text) { return $('<div>', {text: text}); } 426 } 427 }, 428 429 functionAttributes: ['beforeOpen', 'afterOpen', 'beforeContent', 'afterContent', 'beforeClose', 'afterClose'], 430 431 /*** class methods ***/ 432 /* read element's attributes starting with data-featherlight- */ 433 readElementConfig: function(element, namespace) { 434 var Klass = this, 435 regexp = new RegExp('^data-' + namespace + '-(.*)'), 436 config = {}; 437 if (element && element.attributes) { 438 $.each(element.attributes, function(){ 439 var match = this.name.match(regexp); 440 if (match) { 441 var val = this.value, 442 name = $.camelCase(match[1]); 443 if ($.inArray(name, Klass.functionAttributes) >= 0) { /* jshint -W054 */ 444 val = new Function(val); /* jshint +W054 */ 445 } else { 446 try { val = JSON.parse(val); } 447 catch(e) {} 448 } 449 config[name] = val; 450 } 451 }); 452 } 453 return config; 454 }, 455 456 /* Used to create a Featherlight extension 457 [Warning: guru-level] 458 Creates the extension's prototype that in turn 459 inherits Featherlight's prototype. 460 Could be used to extend an extension too... 461 This is pretty high level wizardy, it comes pretty much straight 462 from CoffeeScript and won't teach you anything about Featherlight 463 as it's not really specific to this library. 464 My suggestion: move along and keep your sanity. 465 */ 466 extend: function(child, defaults) { 467 /* Setup class hierarchy, adapted from CoffeeScript */ 468 var Ctor = function(){ this.constructor = child; }; 469 Ctor.prototype = this.prototype; 470 child.prototype = new Ctor(); 471 child.__super__ = this.prototype; 472 /* Copy class methods & attributes */ 473 $.extend(child, this, defaults); 474 child.defaults = child.prototype; 475 return child; 476 }, 477 478 attach: function($source, $content, config) { 479 var Klass = this; 480 if (typeof $content === 'object' && $content instanceof $ === false && !config) { 481 config = $content; 482 $content = undefined; 483 } 484 /* make a copy */ 485 config = $.extend({}, config); 486 487 /* Only for openTrigger and namespace... */ 488 var namespace = config.namespace || Klass.defaults.namespace, 489 tempConfig = $.extend({}, Klass.defaults, Klass.readElementConfig($source[0], namespace), config), 490 sharedPersist; 491 var handler = function(event) { 492 var $target = $(event.currentTarget); 493 /* ... since we might as well compute the config on the actual target */ 494 var elemConfig = $.extend( 495 {$source: $source, $currentTarget: $target}, 496 Klass.readElementConfig($source[0], tempConfig.namespace), 497 Klass.readElementConfig(event.currentTarget, tempConfig.namespace), 498 config); 499 var fl = sharedPersist || $target.data('featherlight-persisted') || new Klass($content, elemConfig); 500 if(fl.persist === 'shared') { 501 sharedPersist = fl; 502 } else if(fl.persist !== false) { 503 $target.data('featherlight-persisted', fl); 504 } 505 if (elemConfig.$currentTarget.blur) { 506 elemConfig.$currentTarget.blur(); // Otherwise 'enter' key might trigger the dialog again 507 } 508 509 // DK: Don't do open just yet, in case immediate 510 // mouseleave. 511 globalOpenTimeout = setTimeout (function () { 512 fl.open(event); 513 }, 250); 514 515 // DK: set flag and start timer - no close for 516 // a while. 517 globalNoClose_b = true; 518 setTimeout (function () { 519 globalNoClose_b = false;
520 }, 500); 521 }; 522 523 $source.on(tempConfig.openTrigger+'.'+tempConfig.namespace, tempConfig.filter, handler); 524 525 // DK: if immediate mouseleave (before open timeOut 526 // finishes), cancel. 527 $source.on('mouseleave', function () { 528 clearTimeout (globalOpenTimeout); 529 }); 530 531 return handler; 532 }, 533 534 current: function() { 535 var all = this.opened(); 536 return all[all.length - 1] || null; 537 }, 538 539 opened: function() { 540 var klass = this; 541 pruneOpened(); 542 return $.grep(opened, function(fl) { return fl instanceof klass; } ); 543 }, 544 545 close: function(event) { 546 var cur = this.current(); 547 if(cur) { return cur.close(event); } 548 }, 549 550 /* Does the auto binding on startup. 551 Meant only to be used by Featherlight and its extensions 552 */ 553 _onReady: function() { 554 var Klass = this; 555 if(Klass.autoBind){ 556 /* Bind existing elements */ 557 $(Klass.autoBind).each(function(){ 558 Klass.attach($(this)); 559 }); 560 /* If a click propagates to the document level, then we have an item that was added later on */ 561 $(document).on('click', Klass.autoBind, function(evt) { 562 if (evt.isDefaultPrevented()) { 563 return; 564 } 565 /* Bind featherlight */ 566 var handler = Klass.attach($(evt.currentTarget)); 567 /* Dispatch event directly */ 568 handler(evt); 569 }); 570 } 571 }, 572 573 /* Featherlight uses the onKeyUp callback to intercept the escape key. 574 Private to Featherlight. 575 */ 576 _callbackChain: { 577 onKeyUp: function(_super, event){ 578 if(27 === event.keyCode) { 579 if (this.closeOnEsc) { 580 $.featherlight.close(event); 581 } 582 return false; 583 } else { 584 return _super(event); 585 } 586 }, 587 588 beforeOpen: function(_super, event) { 589 // Used to disable scrolling 590 $(document.documentElement).addClass('with-featherlight'); 591 592 // Remember focus: 593 this._previouslyActive = document.activeElement; 594 595 // Disable tabbing: 596 // See http://stackoverflow.com/questions/1599660/which-html-elements-can-receive-focus 597 this._$previouslyTabbable = $("a, input, select, textarea, iframe, button, iframe, [contentEditable=true]") 598 .not('[tabindex]') 599 .not(this.$instance.find('button')); 600 601 this._$previouslyWithTabIndex = $('[tabindex]').not('[tabindex="-1"]'); 602 this._previousWithTabIndices = this._$previouslyWithTabIndex.map(function(_i, elem) { 603 return $(elem).attr('tabindex'); 604 }); 605 606 this._$previouslyWithTabIndex.add(this._$previouslyTabbable).attr('tabindex', -1); 607 608 if (document.activeElement.blur) { 609 document.activeElement.blur(); 610 } 611 return _super(event); 612 }, 613 614 afterClose: function(_super, event) { 615 var r = _super(event); 616 // Restore focus 617 var self = this; 618 this._$previouslyTabbable.removeAttr('tabindex'); 619 this._$previouslyWithTabIndex.each(function(i, elem) { 620 $(elem).attr('tabindex', self._previousWithTabIndices[i]); 621 }); 622 this._previouslyActive.focus(); 623 // Restore scroll 624 if(Featherlight.opened().length === 0) { 625 $(document.documentElement).removeClass('with-featherlight'); 626 } 627 return r; 628 }, 629 630 onResize: function(_super, event){ 631 this.resize(this.$content.naturalWidth, this.$content.naturalHeight); 632 return _super(event); 633 }, 634 635 afterContent: function(_super, event){ 636 var r = _super(event); 637 this.$instance.find('[autofocus]:not([disabled])').focus(); 638 this.onResize(event); 639 return r; 640 } 641 } 642 }); 643 644 $.featherlight = Featherlight; 645 646 /* bind jQuery elements to trigger featherlight */ 647 $.fn.featherlight = function($content, config) { 648 Featherlight.attach(this, $content, config); 649 return this; 650 }; 651 652 /* bind featherlight on ready if config autoBind is set */ 653 $(document).ready(function(){ Featherlight._onReady(); }); 654}(jQuery));
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.