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