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