1/** 2 * Featherlight - ultra slim jQuery lightbox 3 * Version 1.3.4 - http://noelboss.github.io/featherlight/ 4 * 5 * Copyright 2015, 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 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 down 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 event.preventDefault(); 156 self.close(); 157 } 158 }); 159 160 return this; 161 }, 162 163 /* this method prepares the content and converts it into a
163jQuery 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 /* Utility function to chain callbacks 300 [Warning: guru-level] 301 Used be extensions that want to let users specify callbacks but 302 also need themselves to use the callbacks. 303 The argument 'chain' has callback names as keys and function(super, event) 304 as values. That function is meant to call `super` at some point. 305 */ 306 chainCallbacks: function (chain) { 307 for (var name in chain) { 308 this[name] = $.proxy(chain[name], this, $.proxy(this[name], this)); 309 } 310 } 311 }; 312 313 $.extend(Featherlight, { 314 id: 0, /* Used to id single featherlight instances */ 315 autoBind: '[data-featherlight]', /* Will automatically bind elements matching this selector. Clear or set before onReady */ 316 defaults: Featherlight.prototype, /* You can access and override all defaults using $.featherlight.defaults, which is just a synonym for $.featherlight.prototype */ 317 /* Contains the logic to determine content */ 318 contentFilters: { 319 jquery: { 320 regex: /^[#.]\w/, /* Anything that starts with a class name or identifiers */ 321 test: function (elem) { return elem instanceof $ && elem; }, 322 process: function (elem) { return this.persist !== false ? $(elem) : $(elem).clone(true); } 323 }, 324 image: { 325 regex: /\.(png|jpg|jpeg|gif|tiff|bmp|svg)(\?\S*)?$/i, 326 process: function (url) { 327 var self = this, 328 deferred = $.Deferred(), 329 img = new Image(), 330 $img = $('<img src="' + url + '" alt="" class="' + self.namespace + '-image" />'); 331 img.onload = function () {
332 /* Store naturalWidth & height for IE8 */ 333 $img.naturalWidth = img.width; $img.naturalHeight = img.height; 334 deferred.resolve($img); 335 }; 336 img.onerror = function () { deferred.reject($img); }; 337 img.src = url; 338 return deferred.promise(); 339 } 340 }, 341 html: { 342 regex: /^\s*<[\w!][^<]*>/, /* Anything that starts with some kind of valid tag */ 343 process: function (html) { return $(html); } 344 }, 345 ajax: { 346 regex: /./, /* At this point, any content is assumed to be an URL */ 347 process: function (url) { 348 var self = this, 349 deferred = $.Deferred(); 350 /* we are using load so one can specify a target with: url.html #targetelement */ 351 var $container = $('<div></div>').load(url, function (response, status) { 352 if (status !== "error") { 353 deferred.resolve($container.contents()); 354 } 355 deferred.fail(); 356 }); 357 return deferred.promise(); 358 } 359 }, 360 iframe: { 361 process: function (url) { 362 var deferred = new $.Deferred(); 363 var $content = $('<iframe/>') 364 .hide() 365 .attr('src', url) 366 .css(structure(this, 'iframe')) 367 .on('load', function () { deferred.resolve($content.show()); }) 368 // We can't move an <iframe> and avoid reloading it, 369 // so let's put it in place ourselves right now: 370 .appendTo(this.$instance.find('.' + this.namespace + '-content')); 371 return deferred.promise(); 372 } 373 }, 374 text: { 375 process: function (text) { return $('<div>', { text: text }); } 376 } 377 }, 378 379 functionAttributes: ['beforeOpen', 'afterOpen', 'beforeContent', 'afterContent', 'beforeClose', 'afterClose'], 380 381 /*** class methods ***/ 382 /* read element's attributes starting with data-featherlight- */ 383 readElementConfig: function (element, namespace) { 384 var Klass = this, 385 regexp = new RegExp('^data-' + namespace + '-(.*)'), 386 config = {}; 387 if (element && element.attributes) { 388 $.each(element.attributes, function () { 389 var match = this.name.match(regexp); 390 if (match) { 391 var val = this.value, 392 name = $.camelCase(match[1]); 393 if ($.inArray(name, Klass.functionAttributes) >= 0) { /* jshint -W054 */ 394 val = new Function(val); /* jshint +W054 */ 395 } else { 396 try { val = $.parseJSON(val); } 397 catch (e) { } 398 } 399 config[name] = val; 400 } 401 }); 402 } 403 return config; 404 }, 405 406 /* Used to create a Featherlight extension 407 [Warning: guru-level] 408 Creates the extension's prototype that in turn 409 inherits Featherlight's prototype. 410 Could be used to extend an extension too... 411 This is pretty high level wizardy, it comes pretty much straight 412 from CoffeeScript and won't teach you anything about Featherlight 413 as it's not really specific to this library. 414 My suggestion: move along and keep your sanity. 415 */ 416 extend: function (child, defaults) { 417 /* Setup class hierarchy, adapted from CoffeeScript */ 418 var Ctor = function () { this.constructor = child; }; 419 Ctor.prototype = this.prototype; 420 child.prototype = new Ctor(); 421 child.__super__ = this.prototype; 422 /* Copy class methods & attributes */ 423 $.extend(child, this, defaults); 424 child.defaults = child.prototype; 425 return child; 426 }, 427 428 attach: function ($source, $content, config) { 429 var Klass = this; 430 if (typeof $content === 'object' && $content instanceof $ === false && !config) { 431 config = $content; 432 $content = undefined; 433 } 434 /* make a copy */ 435 config = $.extend({}, config); 436 437 /* Only for openTrigger and namespace... */ 438 var namespace = config.namespace || Klass.defaults.namespace, 439 tempConfig = $.extend({}, Klass.defaults, Klass.readElementConfig($source[0], namespace), c
439onfig), 440 sharedPersist; 441 442 $source.on(tempConfig.openTrigger + '.' + tempConfig.namespace, tempConfig.filter, function (event) { 443 /* ... since we might as well compute the config on the actual target */ 444 var elemConfig = $.extend( 445 { $source: $source, $currentTarget: $(this) }, 446 Klass.readElementConfig($source[0], tempConfig.namespace), 447 Klass.readElementConfig(this, tempConfig.namespace), 448 config); 449 var fl = sharedPersist || $(this).data('featherlight-persisted') || new Klass($content, elemConfig); 450 if (fl.persist === 'shared') { 451 sharedPersist = fl; 452 } else if (fl.persist !== false) { 453 $(this).data('featherlight-persisted', fl); 454 } 455 elemConfig.$currentTarget.blur(); // Otherwise 'enter' key might trigger the dialog again 456 fl.open(event); 457 }); 458 return $source; 459 }, 460 461 current: function () { 462 var all = this.opened(); 463 return all[all.length - 1] || null; 464 }, 465 466 opened: function () { 467 var klass = this; 468 pruneOpened(); 469 return $.grep(opened, function (fl) { return fl instanceof klass; }); 470 }, 471 472 close: function () { 473 var cur = this.current(); 474 if (cur) { return cur.close(); } 475 }, 476 477 /* Does the auto binding on startup. 478 Meant only to be used by Featherlight and its extensions 479 */ 480 _onReady: function () { 481 var Klass = this; 482 if (Klass.autoBind) { 483 /* Bind existing elements */ 484 $(Klass.autoBind).each(function () { 485 Klass.attach($(this)); 486 }); 487 /* If a click propagates to the document level, then we have an item that was added later on */ 488 $(document).on('click', Klass.autoBind, function (evt) { 489 if (evt.isDefaultPrevented()) { 490 return; 491 } 492 evt.preventDefault(); 493 /* Bind featherlight */ 494 Klass.attach($(evt.currentTarget)); 495 /* Click again; this time our binding will catch it */ 496 $(evt.target).click(); 497 }); 498 } 499 }, 500 501 /* Featherlight uses the onKeyUp callback to intercept the escape key. 502 Private to Featherlight. 503 */ 504 _callbackChain: { 505 onKeyUp: function (_super, event) { 506 if (27 === event.keyCode) { 507 if (this.closeOnEsc) { 508 this.$instance.find('.' + this.namespace + '-close:first').click(); 509 } 510 return false; 511 } else { 512 return _super(event); 513 } 514 }, 515 516 onResize: function (_super, event) { 517 if (this.$content.naturalWidth) { 518 var w = this.$content.naturalWidth, h = this.$content.naturalHeight; 519 /* Reset apparent image size first so container grows */ 520 this.$content.css('width', '').css('height', ''); 521 /* Calculate the worst ratio so that dimensions fit */ 522 var ratio = Math.max( 523 w / parseInt(this.$content.parent().css('width'), 10), 524 h / parseInt(this.$content.parent().css('height'), 10)); 525 /* Resize content */ 526 if (ratio > 1) { 527 this.$content.css('width', '' + w / ratio + 'px').css('height', '' + h / ratio + 'px'); 528 } 529 } 530 return _super(event); 531 }, 532 533 afterContent: function (_super, event) { 534 var r = _super(event); 535 this.onResize(event); 536 return r; 537 } 538 } 539 }); 540 541 $.featherlight = Featherlight; 542 543 /* bind jQuery elements to trigger featherlight */ 544 $.fn.featherlight = function ($content, config) { 545 return Featherlight.attach(this, $content, config); 546 }; 547 548 /* bind featherlight on ready if config autoBind is set */ 549 $(document).ready(function () { Featherlight._onReady(); }); 550}(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.