| 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.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])').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 |
|
| 670 |
/** |
| 671 |
* Featherlight Gallery – an extension for the ultra slim jQuery lightbox |
| 672 |
* Version 1.7.14-UMD - http://noelboss.github.io/featherlight/ |
| 673 |
* |
| 674 |
* Copyright 2019, Noël Raoul Bossart (http://www.noelboss.com) |
| 675 |
* MIT Licensed. |
| 676 |
**/ |
| 677 |
(function (factory) { |
| 678 |
if (typeof define === 'function' && define.amd) { |
| 679 |
// AMD. Register as an anonymous module. |
| 680 |
define(['jquery'], factory); |
| 681 |
} else if (typeof module === 'object' && module.exports) { |
| 682 |
// Node/CommonJS |
| 683 |
module.exports = function (root, jQuery) { |
| 684 |
if (jQuery === undefined) { |
| 685 |
// require('jQuery') returns a factory that requires window to |
| 686 |
// build a jQuery instance, we normalize how we use modules |
| 687 |
// that require this pattern but the window provided is a noop |
| 688 |
// if it's defined (how jquery works) |
| 689 |
if (typeof window !== 'undefined') { |
| 690 |
jQuery = require('jquery'); |
| 691 |
} else { |
| 692 |
jQuery = require('jquery')(root); |
| 693 |
} |
| 694 |
} |
| 695 |
factory(jQuery); |
| 696 |
return jQuery; |
| 697 |
}; |
| 698 |
} else { |
| 699 |
// Browser globals |
| 700 |
factory(jQuery); |
| 701 |
} |
| 702 |
})(function($) { |
| 703 |
"use strict"; |
| 704 |
|
| 705 |
var warn = function(m) { |
| 706 |
if(window.console && window.console.warn) { |
| 707 |
window.console.warn('FeatherlightGallery: ' + m); |
| 708 |
} |
| 709 |
}; |
| 710 |
|
| 711 |
if('undefined' === typeof $) { |
| 712 |
return warn('Too much lightness, Featherlight needs jQuery.'); |
| 713 |
} else if(!$.featherlight) { |
| 714 |
return warn('Load the featherlight plugin before the gallery plugin'); |
| 715 |
} |
| 716 |
|
| 717 |
var isTouchAware = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch, |
| 718 |
jQueryConstructor = $.event && $.event.special.swipeleft && $, |
| 719 |
hammerConstructor = window.Hammer && function($el){ |
| 720 |
var mc = new window.Hammer.Manager($el[0]); |
| 721 |
mc.add(new window.Hammer.Swipe()); |
| 722 |
return mc; |
| 723 |
}, |
| 724 |
swipeAwareConstructor = isTouchAware && (jQueryConstructor || hammerConstructor); |
| 725 |
if(isTouchAware && !swipeAwareConstructor) { |
| 726 |
warn('No compatible swipe library detected; one must be included before featherlightGallery for swipe motions to navigate the galleries.'); |
| 727 |
} |
| 728 |
|
| 729 |
var callbackChain = { |
| 730 |
afterClose: function(_super, event) { |
| 731 |
var self = this; |
| 732 |
self.$instance.off('next.'+self.namespace+' previous.'+self.namespace); |
| 733 |
if (self._swiper) { |
| 734 |
self._swiper |
| 735 |
.off('swipeleft', self._swipeleft) /* See http://stackoverflow.com/questions/17367198/hammer-js-cant-remove-event-listener */ |
| 736 |
.off('swiperight', self._swiperight); |
| 737 |
self._swiper = null; |
| 738 |
} |
| 739 |
return _super(event); |
| 740 |
}, |
| 741 |
beforeOpen: function(_super, event){ |
| 742 |
var self = this; |
| 743 |
|
| 744 |
self.$instance.on('next.'+self.namespace+' previous.'+self.namespace, function(event){ |
| 745 |
var offset = event.type === 'next' ? +1 : -1; |
| 746 |
self.navigateTo(self.currentNavigation() + offset); |
| 747 |
}); |
| 748 |
|
| 749 |
if (swipeAwareConstructor) { |
| 750 |
self._swiper = swipeAwareConstructor(self.$instance) |
| 751 |
.on('swipeleft', self._swipeleft = function() { self.$instance.trigger('next'); }) |
| 752 |
.on('swiperight', self._swiperight = function() { self.$instance.trigger('previous'); }); |
| 753 |
|
| 754 |
self.$instance |
| 755 |
.addClass(this.namespace+'-swipe-aware', swipeAwareConstructor); |
| 756 |
} |
| 757 |
|
| 758 |
self.$instance.find('.'+self.namespace+'-content') |
| 759 |
.append(self.createNavigation('previous')) |
| 760 |
.append(self.createNavigation('next')); |
| 761 |
|
| 762 |
return _super(event); |
| 763 |
}, |
| 764 |
beforeContent: function(_super, event) { |
| 765 |
var index = this.currentNavigation(); |
| 766 |
var len = this.slides().length; |
| 767 |
this.$instance |
| 768 |
.toggleClass(this.namespace+'-first-slide', index === 0) |
| 769 |
.toggleClass(this.namespace+'-last-slide', index === len - 1); |
| 770 |
return _super(event); |
| 771 |
}, |
| 772 |
onKeyUp: function(_super, event){ |
| 773 |
var dir = { |
| 774 |
37: 'previous', /* Left arrow */ |
| 775 |
39: 'next' /* Rigth arrow */ |
| 776 |
}[event.keyCode]; |
| 777 |
if(dir) { |
| 778 |
this.$instance.trigger(dir); |
| 779 |
return false; |
| 780 |
} else { |
| 781 |
return _super(event); |
| 782 |
} |
| 783 |
} |
| 784 |
}; |
| 785 |
|
| 786 |
function FeatherlightGallery($source, config) { |
| 787 |
if(this instanceof FeatherlightGallery) { /* called with new */ |
| 788 |
$.featherlight.apply(this, arguments); |
| 789 |
this.chainCallbacks(callbackChain); |
| 790 |
} else { |
| 791 |
var flg = new FeatherlightGallery($.extend({$source: $source, $currentTarget: $source.first()}, config)); |
| 792 |
flg.open(); |
| 793 |
return flg; |
| 794 |
} |
| 795 |
} |
| 796 |
|
| 797 |
$.featherlight.extend(FeatherlightGallery, { |
| 798 |
autoBind: '[data-featherlight-gallery]' |
| 799 |
}); |
| 800 |
|
| 801 |
$.extend(FeatherlightGallery.prototype, { |
| 802 |
/** Additional settings for Gallery **/ |
| 803 |
previousIcon: '◀', /* Code that is used as previous icon */ |
| 804 |
nextIcon: '▶', /* Code that is used as next icon */ |
| 805 |
galleryFadeIn: 100, /* fadeIn speed when image is loaded */ |
| 806 |
galleryFadeOut: 300, /* fadeOut speed before image is loaded */ |
| 807 |
|
| 808 |
slides: function() { |
| 809 |
if (this.filter) { |
| 810 |
return this.$source.find(this.filter); |
| 811 |
} |
| 812 |
return this.$source; |
| 813 |
}, |
| 814 |
|
| 815 |
images: function() { |
| 816 |
warn('images is deprecated, please use slides instead'); |
| 817 |
return this.slides(); |
| 818 |
}, |
| 819 |
|
| 820 |
currentNavigation: function() { |
| 821 |
return this.slides().index(this.$currentTarget); |
| 822 |
}, |
| 823 |
|
| 824 |
navigateTo: function(index) { |
| 825 |
var self = this, |
| 826 |
source = self.slides(), |
| 827 |
len = source.length, |
| 828 |
$inner = self.$instance.find('.' + self.namespace + '-inner'); |
| 829 |
index = ((index % len) + len) % len; /* pin index to [0, len[ */ |
| 830 |
|
| 831 |
this.$instance.addClass(this.namespace+'-loading'); |
| 832 |
self.$currentTarget = source.eq(index); |
| 833 |
self.beforeContent(); |
| 834 |
return $.when( |
| 835 |
self.getContent(), |
| 836 |
$inner.fadeTo(self.galleryFadeOut,0.2) |
| 837 |
).always(function($newContent) { |
| 838 |
self.setContent($newContent); |
| 839 |
self.afterContent(); |
| 840 |
$newContent.fadeTo(self.galleryFadeIn,1); |
| 841 |
}); |
| 842 |
}, |
| 843 |
|
| 844 |
createNavigation: function(target) { |
| 845 |
var self = this; |
| 846 |
return $('<span title="'+target+'" class="'+this.namespace+'-'+target+'"><span>'+this[target+'Icon']+'</span></span>').click(function(evt){ |
| 847 |
$(this).trigger(target+'.'+self.namespace); |
| 848 |
evt.preventDefault(); |
| 849 |
}); |
| 850 |
} |
| 851 |
}); |
| 852 |
|
| 853 |
$.featherlightGallery = FeatherlightGallery; |
| 854 |
|
| 855 |
/* extend jQuery with selector featherlight method $(elm).featherlight(config, elm); */ |
| 856 |
$.fn.featherlightGallery = function(config) { |
| 857 |
FeatherlightGallery.attach(this, config); |
| 858 |
return this; |
| 859 |
}; |
| 860 |
|
| 861 |
/* bind featherlight on ready if config autoBind is set */ |
| 862 |
$(document).ready(function(){ FeatherlightGallery._onReady(); }); |
| 863 |
|
| 864 |
}); |
| 865 |
|