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