PageSourceSearch

https://www.openapis.org/wp-content/themes/salient-child/vc-addons/js/featherlight.js?ver=3.6.1

js openapis.org collected 2026-09-24 07:16:51 UTC 23,214 bytes, 615 lines download raw bytes

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

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.