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