File: /www/wwwroot/claudiayancor.duckdns.org/wp-content/plugins/photonic/include/ext/strip/strip.js
/*!
* Strip - An Unobtrusive Responsive Lightbox - v1.8.0
* (c) 2014-2021 Nick Stakenburg
*
* https://github.com/staaky/strip
*
* @license: https://creativecommons.org/licenses/by/4.0
*/
// UMD wrapper
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['jquery'], factory);
} else if (typeof module === 'object' && module.exports) {
// Node/CommonJS
module.exports = factory(require('jquery'));
} else {
// Browser global
root.Strip = factory(jQuery);
}
}(this, function($) {
var Strip = {
version: "1.8.0",
};
Strip.Skins = {
// the default skin
strip: {},
};
var Browser = (function (uA) {
function getVersion(identifier) {
var version = new RegExp(identifier + "([\\d.]+)").exec(uA);
return version ? parseFloat(version[1]) : true;
}
return {
IE:
!!(window.attachEvent && uA.indexOf("Opera") === -1) &&
getVersion("MSIE "),
Opera:
uA.indexOf("Opera") > -1 &&
((!!window.opera && opera.version && parseFloat(opera.version())) ||
7.55),
WebKit: uA.indexOf("AppleWebKit/") > -1 && getVersion("AppleWebKit/"),
Gecko:
uA.indexOf("Gecko") > -1 &&
uA.indexOf("KHTML") === -1 &&
getVersion("rv:"),
MobileSafari: !!uA.match(/Apple.*Mobile.*Safari/),
Chrome: uA.indexOf("Chrome") > -1 && getVersion("Chrome/"),
ChromeMobile: uA.indexOf("CrMo") > -1 && getVersion("CrMo/"),
Android: uA.indexOf("Android") > -1 && getVersion("Android "),
IEMobile: uA.indexOf("IEMobile") > -1 && getVersion("IEMobile/"),
};
})(navigator.userAgent);
var _slice = Array.prototype.slice;
// Fit
var Fit = {
within: function (bounds, dimensions) {
var options = $.extend(
{
height: true,
width: true,
},
arguments[2] || {}
);
var size = $.extend({}, dimensions),
scale = 1,
attempts = 5;
var fit = { width: options.width, height: options.height };
// adjust the bounds depending on what to fit (width/height)
// start
while (
attempts > 0 &&
((fit.width && size.width > bounds.width) ||
(fit.height && size.height > bounds.height))
) {
// if both dimensions fall underneath a minimum, then don't event continue
//if (size.width < 100 && size.height < 100) {
var scaleX = 1,
scaleY = 1;
if (fit.width && size.width > bounds.width) {
scaleX = bounds.width / size.width;
}
if (fit.height && size.height > bounds.height) {
scaleY = bounds.height / size.height;
}
// we'll end up using the largest scaled down factor
var scale = Math.min(scaleX, scaleY);
// adjust current size, based on original dimensions
size = {
width: Math.round(dimensions.width * scale),
height: Math.round(dimensions.height * scale),
};
//}
attempts--;
}
// make sure size is never pressed into negative
size.width = Math.max(size.width, 0);
size.height = Math.max(size.height, 0);
return size;
},
};
// we only uses some of the jQueryUI easing functions
// add those with a prefix to prevent conflicts
$.extend($.easing, {
stripEaseInCubic: function (x, t, b, c, d) {
return c * (t /= d) * t * t + b;
},
stripEaseInSine: function (x, t, b, c, d) {
return -c * Math.cos((t / d) * (Math.PI / 2)) + c + b;
},
stripEaseOutSine: function (x, t, b, c, d) {
return c * Math.sin((t / d) * (Math.PI / 2)) + b;
},
});
var Support = (function () {
var testElement = document.createElement("div"),
domPrefixes = "Webkit Moz O ms Khtml".split(" ");
function prefixed(property) {
return testAllProperties(property, "prefix");
}
function testProperties(properties, prefixed) {
for (var i in properties) {
if (testElement.style[properties[i]] !== undefined) {
return prefixed == "prefix" ? properties[i] : true;
}
}
return false;
}
function testAllProperties(property, prefixed) {
var ucProperty = property.charAt(0).toUpperCase() + property.substr(1),
properties = (
property +
" " +
domPrefixes.join(ucProperty + " ") +
ucProperty
).split(" ");
return testProperties(properties, prefixed);
}
// feature detect
return {
css: {
animation: testAllProperties("animation"),
transform: testAllProperties("transform"),
prefixed: prefixed,
},
svg:
!!document.createElementNS &&
!!document.createElementNS("http://www.w3.org/2000/svg", "svg")
.createSVGRect,
touch: (function () {
try {
return !!(
"ontouchstart" in window ||
(window.DocumentTouch && document instanceof DocumentTouch)
); // firefox on Android
} catch (e) {
return false;
}
})(),
};
})();
// add mobile touch to support
Support.mobileTouch =
Support.touch &&
(Browser.MobileSafari ||
Browser.Android ||
Browser.IEMobile ||
Browser.ChromeMobile ||
!/^(Win|Mac|Linux)/.test(navigator.platform)); // otherwise, assume anything not on Windows, Mac or Linux is a mobile device
var Bounds = {
viewport: function () {
var dimensions = {
width: $(window).width(),
};
// Mobile Safari has a bugged viewport height after scrolling
// Firefox on Android also has problems with height
if (Browser.MobileSafari || (Browser.Android && Browser.Gecko)) {
var zoom = document.documentElement.clientWidth / window.innerWidth;
dimensions.height = window.innerHeight * zoom;
} else {
// default
dimensions.height = $(window).height();
}
return dimensions;
},
};
/* ImageReady (standalone) - part of VoilĂ
* http://voila.nickstakenburg.com
* MIT License
*/
var ImageReady = (function ($) {
var Poll = function () {
return this.initialize.apply(this, Array.prototype.slice.call(arguments));
};
$.extend(Poll.prototype, {
initialize: function () {
this.options = $.extend(
{
test: function () {},
success: function () {},
timeout: function () {},
callAt: false,
intervals: [
[0, 0],
[1 * 1000, 10],
[2 * 1000, 50],
[4 * 1000, 100],
[20 * 1000, 500],
],
},
arguments[0] || {}
);
this._test = this.options.test;
this._success = this.options.success;
this._timeout = this.options.timeout;
this._ipos = 0;
this._time = 0;
this._delay = this.options.intervals[this._ipos][1];
this._callTimeouts = [];
this.poll();
this._createCallsAt();
},
poll: function () {
this._polling = setTimeout(
function () {
if (this._test()) {
this.success();
return;
}
// update time
this._time += this._delay;
// next i within the interval
if (this._time >= this.options.intervals[this._ipos][0]) {
// timeout when no next interval
if (!this.options.intervals[this._ipos + 1]) {
if (typeof this._timeout === "function") {
this._timeout();
}
return;
}
this._ipos++;
// update to the new bracket
this._delay = this.options.intervals[this._ipos][1];
}
this.poll();
}.bind(this),
this._delay
);
},
success: function () {
this.abort();
this._success();
},
_createCallsAt: function () {
if (!this.options.callAt) return;
// start a timer for each call
$.each(
this.options.callAt,
function (i, at) {
var time = at[0],
fn = at[1];
var timeout = setTimeout(
function () {
fn();
}.bind(this),
time
);
this._callTimeouts.push(timeout);
}.bind(this)
);
},
_stopCallTimeouts: function () {
$.each(this._callTimeouts, function (i, timeout) {
clearTimeout(timeout);
});
this._callTimeouts = [];
},
abort: function () {
this._stopCallTimeouts();
if (this._polling) {
clearTimeout(this._polling);
this._polling = null;
}
},
});
var ImageReady = function () {
return this.initialize.apply(this, Array.prototype.slice.call(arguments));
};
$.extend(ImageReady.prototype, {
supports: {
naturalWidth: (function () {
return "naturalWidth" in new Image();
})(),
},
// NOTE: setTimeouts allow callbacks to be attached
initialize: function (img, successCallback, errorCallback) {
this.img = $(img)[0];
this.successCallback = successCallback;
this.errorCallback = errorCallback;
this.isLoaded = false;
this.options = $.extend(
{
method: "onload",
pollFallbackAfter: 1000,
},
arguments[3] || {}
);
// onload and a fallback for no naturalWidth support (IE6-7)
if (this.options.method == "onload" || !this.supports.naturalWidth) {
this.load();
return;
}
// start polling
this.poll();
},
// NOTE: Polling for naturalWidth is only reliable if the
// <img>.src never changes. naturalWidth isn't always reset
// to 0 after the src changes (depending on how the spec
// was implemented). The spec even seems to be against
// this, making polling unreliable in those cases.
poll: function () {
this._poll = new Poll({
test: function () {
return this.img.naturalWidth > 0;
}.bind(this),
success: function () {
this.success();
}.bind(this),
timeout: function () {
// error on timeout
this.error();
}.bind(this),
callAt: [
[
this.options.pollFallbackAfter,
function () {
this.load();
}.bind(this),
],
],
});
},
load: function () {
this._loading = setTimeout(
function () {
var image = new Image();
this._onloadImage = image;
image.onload = function () {
image.onload = function () {};
if (!this.supports.naturalWidth) {
this.img.naturalWidth = image.width;
this.img.naturalHeight = image.height;
image.naturalWidth = image.width;
image.naturalHeight = image.height;
}
this.success();
}.bind(this);
image.onerror = this.error.bind(this);
image.src = this.img.src;
}.bind(this)
);
},
success: function () {
if (this._calledSuccess) return;
this._calledSuccess = true;
// stop loading/polling
this.abort();
// some time to allow layout updates, IE requires this!
this.waitForRender(
function () {
this.isLoaded = true;
this.successCallback(this);
}.bind(this)
);
},
error: function () {
if (this._calledError) return;
this._calledError = true;
// stop loading/polling
this.abort();
// don't wait for an actual render on error, just timeout
// to give the browser some time to render a broken image icon
this._errorRenderTimeout = setTimeout(
function () {
if (this.errorCallback) this.errorCallback(this);
}.bind(this)
);
},
abort: function () {
this.stopLoading();
this.stopPolling();
this.stopWaitingForRender();
},
stopPolling: function () {
if (this._poll) {
this._poll.abort();
this._poll = null;
}
},
stopLoading: function () {
if (this._loading) {
clearTimeout(this._loading);
this._loading = null;
}
if (this._onloadImage) {
this._onloadImage.onload = function () {};
this._onloadImage.onerror = function () {};
}
},
// used by success() only
waitForRender: function (callback) {
this._renderTimeout = setTimeout(callback);
},
stopWaitingForRender: function () {
if (this._renderTimeout) {
clearTimeout(this._renderTimeout);
this._renderTimeout = null;
}
if (this._errorRenderTimeout) {
clearTimeout(this._errorRenderTimeout);
this._errorRenderTimeout = null;
}
},
});
return ImageReady;
})(jQuery);
// Spinner
// Create pure CSS based spinners
function Spinner() {
return this.initialize.apply(this, _slice.call(arguments));
}
// mark as supported
Spinner.supported = Support.css.transform && Support.css.animation;
$.extend(Spinner.prototype, {
initialize: function (element) {
this.element = $(element);
if (!this.element[0]) return;
this.classPrefix = "strp-";
this.setOptions(arguments[1] || {});
this.element.addClass(this.classPrefix + "spinner");
this.element.append(
(this._rotate = $("<div>").addClass(this.classPrefix + "spinner-rotate"))
);
this.build();
this.start();
},
setOptions: function (options) {
this.options = $.extend(
{
show: 200,
hide: 200,
},
options || {}
);
},
build: function () {
if (this._build) return;
this._rotate.html("");
var d = (this.options.length + this.options.radius) * 2,
dimensions = { height: d, width: d };
// we parse stuff below so make sure that happens with a visible spinner
var is_vis = this.element.is(":visible");
if (!is_vis) this.element.show();
// find the amount of lines
var frame, line;
this._rotate.append(
(frame = $("<div>")
.addClass(this.classPrefix + "spinner-frame")
.append(
(line = $("<div>").addClass(this.classPrefix + "spinner-line"))
))
);
var lines = parseInt($(line).css("z-index"));
this.lines = lines;
// now reset that z-index
line.css({ "z-index": "inherit" });
frame.remove();
// reset visibility
if (!is_vis) this.element.hide();
// insert frames
var color;
for (var i = 0; i < lines; i++) {
var frame, line;
this._rotate.append(
(frame = $("<div>")
.addClass(this.classPrefix + "spinner-frame")
.append(
(line = $("<div>").addClass(this.classPrefix + "spinner-line"))
))
);
color = color || line.css("color");
line.css({ background: color });
frame.css({ opacity: ((1 / lines) * (i + 1)).toFixed(2) });
var transformCSS = {};
transformCSS[Support.css.prefixed("transform")] =
"rotate(" + (360 / lines) * (i + 1) + "deg)";
frame.css(transformCSS);
}
this._build = true;
},
start: function () {
var rotateCSS = {};
rotateCSS[Support.css.prefixed("animation")] =
this.classPrefix + "spinner-spin 1s infinite steps(" + this.lines + ")";
this._rotate.css(rotateCSS);
},
stop: function () {
var rotateCSS = {};
rotateCSS[Support.css.prefixed("animation")] = "none";
this._rotate.css(rotateCSS);
},
show: function (callback) {
this.build();
this.start();
this.element.stop(true).fadeTo(this.options.show, 1, callback); //deferred.resolve);
},
hide: function (callback) {
this.element.stop(true).fadeOut(
this.options.hide,
function () {
this.stop();
if (callback) callback();
}.bind(this)
);
},
refresh: function () {
this._build = false;
this.build();
},
});
function Timers() {
return this.initialize.apply(this, _slice.call(arguments));
}
$.extend(Timers.prototype, {
initialize: function () {
this._timers = {};
},
set: function (name, handler, ms) {
this._timers[name] = setTimeout(handler, ms);
},
get: function (name) {
return this._timers[name];
},
clear: function (name) {
if (name) {
if (this._timers[name]) {
clearTimeout(this._timers[name]);
delete this._timers[name];
}
} else {
this.clearAll();
}
},
clearAll: function () {
$.each(this._timers, function (_i, timer) {
clearTimeout(timer);
});
this._timers = {};
},
});
// uses Types to scan a URI for info
function getURIData(url) {
var result = { type: "image" };
$.each(Types, function (i, type) {
var data = type.data(url);
if (data) {
result = data;
result.type = i;
result.url = url;
}
});
return result;
}
function detectExtension(url) {
var ext = (url || "").replace(/\?.*/g, "").match(/\.([^.]{3,4})$/);
return ext ? ext[1].toLowerCase() : null;
}
var Types = {
image: {
extensions: "bmp gif jpeg jpg png webp",
detect: function (url) {
return $.inArray(detectExtension(url), this.extensions.split(" ")) > -1;
},
data: function (url) {
if (!this.detect()) return false;
return {
extension: detectExtension(url),
};
},
},
youtube: {
detect: function (url) {
var res =
/(youtube\.com|youtu\.be)\/watch\?(?=.*vi?=([a-zA-Z0-9-_]+))(?:\S+)?$/.exec(
url
);
if (res && res[2]) return res[2];
res =
/(youtube\.com|youtu\.be)\/(vi?\/|u\/|embed\/)?([a-zA-Z0-9-_]+)(?:\S+)?$/i.exec(
url
);
if (res && res[3]) return res[3];
return false;
},
data: function (url) {
var id = this.detect(url);
if (!id) return false;
return {
id: id,
};
},
},
vimeo: {
detect: function (url) {
var res = /(vimeo\.com)\/([a-zA-Z0-9-_]+)(?:\S+)?$/i.exec(url);
if (res && res[2]) return res[2];
return false;
},
data: function (url) {
var id = this.detect(url);
if (!id) return false;
return {
id: id,
};
},
},
};
var VimeoReady = (function () {
var VimeoReady = function () {
return this.initialize.apply(this, _slice.call(arguments));
};
$.extend(VimeoReady.prototype, {
initialize: function (url, callback) {
this.url = url;
this.callback = callback;
this.load();
},
load: function () {
// first try the cache
var cache = Cache.get(this.url);
if (cache) {
return this.callback(cache.data);
}
var protocol =
"http" +
(window.location && window.location.protocol == "https:" ? "s" : "") +
":",
video_id = getURIData(this.url).id;
this._xhr = $.getJSON(
protocol +
"//vimeo.com/api/oembed.json?url=" +
protocol +
"//vimeo.com/" +
video_id +
"&maxwidth=9999999&maxheight=9999999&callback=?",
function (_data) {
var data = {
dimensions: {
width: _data.width,
height: _data.height,
},
};
Cache.set(this.url, data);
if (this.callback) this.callback(data);
}.bind(this)
);
},
abort: function () {
if (this._xhr) {
this._xhr.abort();
this._xhr = null;
}
},
});
var Cache = {
cache: [],
get: function (url) {
var entry = null;
for (var i = 0; i < this.cache.length; i++) {
if (this.cache[i] && this.cache[i].url == url) entry = this.cache[i];
}
return entry;
},
set: function (url, data) {
this.remove(url);
this.cache.push({ url: url, data: data });
},
remove: function (url) {
for (var i = 0; i < this.cache.length; i++) {
if (this.cache[i] && this.cache[i].url == url) {
delete this.cache[i];
}
}
},
};
return VimeoReady;
})();
var Options = {
defaults: {
effects: {
spinner: { show: 200, hide: 200 },
transition: { min: 175, max: 250 },
ui: { show: 0, hide: 200 },
window: { show: 300, hide: 300 },
},
hideOnClickOutside: true,
keyboard: {
left: true,
right: true,
esc: true,
},
loop: true,
overlap: true,
preload: [1, 2],
position: true,
skin: "strip",
side: "right",
spinner: true,
toggle: true,
uiDelay: 3000,
vimeo: {
autoplay: 1,
api: 1,
title: 1,
byline: 1,
portrait: 0,
loop: 0,
},
youtube: {
autoplay: 1,
controls: 1,
enablejsapi: 1,
hd: 1,
iv_load_policy: 3,
loop: 0,
modestbranding: 1,
rel: 0,
vq: "hd1080", // force hd: http://stackoverflow.com/a/12467865
},
initialTypeOptions: {
image: {},
vimeo: {
width: 1280,
},
// Youtube needs both dimensions, it doesn't support fetching video dimensions like Vimeo yet.
// Star this ticket if you'd like to get support for it at some point:
// https://code.google.com/p/gdata-issues/issues/detail?id=4329
youtube: {
width: 1280,
height: 720,
},
},
},
create: function (opts, type, data) {
opts = opts || {};
data = data || {};
opts.skin = opts.skin || this.defaults.skin;
var selected = opts.skin
? $.extend(
{},
Strip.Skins[opts.skin] || Strip.Skins[this.defaults.skin]
)
: {},
merged = $.extend(true, {}, this.defaults, selected);
// merge initial type options
if (merged.initialTypeOptions) {
if (type && merged.initialTypeOptions[type]) {
merged = $.extend(true, {}, merged.initialTypeOptions[type], merged);
}
// these aren't used further, so remove them
delete merged.initialTypeOptions;
}
// safe options to work with
var options = $.extend(true, {}, merged, opts);
// set all effect duration to 0 for effects: false
// IE8 and below never use effects
if (!options.effects || (Browser.IE && Browser.IE < 9)) {
options.effects = {};
$.each(this.defaults.effects, function (name, effect) {
$.each(
(options.effects[name] = $.extend({}, effect)),
function (option) {
options.effects[name][option] = 0;
}
);
});
// disable the spinner when effects are disabled
options.spinner = false;
}
// keyboard
if (options.keyboard) {
// when keyboard is true, enable all keys
if (typeof options.keyboard === "boolean") {
options.keyboard = {};
$.each(this.defaults.keyboard, function (key, bool) {
options.keyboard[key] = true;
});
}
// disable left and right keys for video, because players like
// youtube use these keys
if (type === "vimeo" || type === "youtube") {
$.extend(options.keyboard, { left: false, right: false });
}
}
// vimeo & youtube always have no overlap
if (type === "vimeo" || type === "youtube") {
options.overlap = false;
}
return options;
},
};
function View() {
this.initialize.apply(this, _slice.call(arguments));
}
$.extend(View.prototype, {
initialize: function (object) {
var options = arguments[1] || {},
data = {};
// string -> element
if (typeof object === "string") {
// turn the string into an element
object = { url: object };
}
// element -> object
else if (object && object.nodeType === 1) {
var element = $(object);
object = {
element: element[0],
url: element.attr("href"),
caption: element.attr("data-strip-caption"),
group: element.attr("data-strip-group"),
extension: element.attr("data-strip-extension"),
type: element.attr("data-strip-type"),
options:
(element.attr("data-strip-options") &&
eval("({" + element.attr("data-strip-options") + "})")) ||
{},
};
}
if (object) {
// detect type if none is set
if (!object.extension) {
object.extension = detectExtension(object.url);
}
if (!object.type) {
var data = getURIData(object.url);
object._data = data;
object.type = data.type;
}
}
if (!object._data) {
object._data = getURIData(object.url);
}
if (object && object.options) {
object.options = $.extend(
true,
$.extend({}, options),
$.extend({}, object.options)
);
} else {
object.options = $.extend({}, options);
}
// extend the options
object.options = Options.create(object.options, object.type, object._data);
// extend this with data
$.extend(this, object);
return this;
},
});
var Pages = {
initialize: function (element) {
this.element = element;
this.pages = {};
this.uid = 1;
},
add: function (views) {
this.uid++;
this.views = views;
this.pages[this.uid] = []; // create room for these pages
// switched pages, so show the UI on the next resize
Window._showUIOnResize = true;
// add pages for all these views
$.each(
views,
function (i, view) {
this.pages[this.uid].push(new Page(view, i + 1, this.views.length));
}.bind(this)
);
},
show: function (position, callback) {
var page = this.pages[this.uid][position - 1];
// never try to reload the exact same frame
if (this.page && this.page.uid == page.uid) {
// also hide the window if toggle is enabled
if (page.view.options.toggle) {
Window.hide();
// clear the page so double clicking when hiding will
// re-open the window even if it's in a hide animation
this.page = null;
}
return;
}
// set class names to indicate active state
Pages.setActiveClass(page);
// update the page
this.page = page;
this.removeHiddenAndLoadingInactive();
page.show(
function () {
// once a page has been fully shown we mark Pages as not being switched anymore
this._switched = false;
if (callback) callback();
}.bind(this)
);
},
getLoadingCount: function () {
// we only stop loading if all the frames we have are not loading anymore
var count = 0;
$.each(this.pages, function (_id, pages) {
$.each(pages, function (_j, page) {
if (page.loading) count++;
});
});
return count;
},
// used by the API when opening
// checks if the page is in the currently open group
getPositionInActivePageGroup: function (element) {
var position = 0,
activeGroup = this.pages[this.uid];
if (activeGroup) {
$.each(activeGroup, function (i, page) {
if (page.view.element && page.view.element == element) {
position = i + 1;
}
});
}
return position;
},
// remove pages not matching the current id
removeExpired: function (instantly) {
$.each(this.pages, function (id, pages) {
if (id != this._id) {
$.each(pages, function (_j, page) {
page.remove(instantly);
});
}
});
},
// Window.hide will call this when fully closed
removeAll: function () {
$.each(this.pages, function (_id, pages) {
$.each(pages, function (_j, page) {
page.remove();
});
});
// empty out pages
this.pages = {};
},
hideVisibleInactive: function (alternateDuration) {
$.each(
this.pages,
function (_id, pages) {
$.each(
pages,
function (_j, page) {
if (page.uid != this.page.uid) {
page.hide(null, alternateDuration);
}
}.bind(this)
);
}.bind(this)
);
},
stopInactive: function () {
$.each(
this.pages,
function (_id, pages) {
$.each(
pages,
function (_j, page) {
if (page.uid != this.page.uid && !page.preloading) {
page.stop();
}
}.bind(this)
);
}.bind(this)
);
},
// TODO: might be nice to have a hide animation before removal, it's instant now
removeHiddenAndLoadingInactive: function () {
// track which inactive page groups are empty
var empty = [];
$.each(
this.pages,
function (uid, pages) {
// only remove pages in the groups that are currently not active
if (uid != this.uid) {
var removed = 0;
$.each(
pages,
function (_j, page) {
// remove hidden or loading, but dont'remove frames in animation
if ((!page.visible || page.loading) && !page.animatingWindow) {
page.remove();
}
if (page.removed) removed++; // count all, not those we remove now
}.bind(this)
);
// if we've removed all pages from this group it's safe to remove it
// we don't do this in the loop but below
if (removed == pages.length) {
empty.push(uid);
}
}
}.bind(this)
);
// now remove all empty page groups
$.each(
empty,
function (_i, uid) {
delete this.pages[uid];
}.bind(this)
);
},
stop: function () {
$.each(this.pages, function (id, pages) {
$.each(pages, function (_j, page) {
page.stop();
});
});
},
setActiveClass: function (page) {
// switch the active element class
this.removeActiveClasses();
// add the active class if the new page is bound to an element
var element = page.view.element;
if (element) {
$(element).addClass("strip-active-element strip-active-group");
// also give other items in the group the active group class
var group = $(element).attr("data-strip-group");
if (group) {
$('.strip[data-strip-group="' + group + '"]').addClass(
"strip-active-group"
);
}
}
},
removeActiveClasses: function () {
$(".strip-active-group").removeClass(
"strip-active-group strip-active-element"
);
},
};
var Page = (function () {
var uid = 0,
loadedUrlCache = {};
function Page() {
return this.initialize.apply(this, _slice.call(arguments));
}
$.extend(Page.prototype, {
initialize: function (view, position, total) {
this.view = view;
this.dimensions = { width: 0, height: 0 };
this.uid = uid++;
// store position/total views for later use
this._position = position;
this._total = total;
this.animated = false;
this.visible = false;
this.queues = {};
this.queues.showhide = $({});
},
// create the page, this doesn't mean it's loaded
// should happen instantly
create: function () {
if (this._created) return;
Pages.element.append(
(this.element = $("<div>")
.addClass("strp-page")
.append((this.container = $("<div>").addClass("strp-container")))
.css({ opacity: 0 })
.hide())
);
var hasPosition = this.view.options.position && this._total > 1;
if (this.view.caption || hasPosition) {
this.element.append(
(this.info = $("<div>")
.addClass("strp-info")
.append(
(this.info_padder = $("<div>").addClass("strp-info-padder"))
))
);
// insert caption first because it floats right
if (hasPosition) {
this.element.addClass("strp-has-position");
this.info_padder.append(
$("<div>")
.addClass("strp-position")
.html(this._position + " / " + this._total)
);
}
if (this.view.caption) {
this.info_padder.append(
(this.caption = $("<div>")
.addClass("strp-caption")
.html(this.view.caption))
);
}
}
switch (this.view.type) {
case "image":
this.container.append(
(this.content = $("<img>").attr({ src: this.view.url }))
);
break;
case "vimeo":
case "youtube":
this.container.append((this.content = $("<div>")));
break;
}
// ui
this.element.addClass(
"strp" + (this.view.options.overlap ? "" : "-no") + "-overlap"
);
// no sides
if (this._total < 2) {
this.element.addClass("strp-no-sides");
}
this.content.addClass("strp-content-element");
this._created = true;
},
// surrounding
_getSurroundingPages: function () {
var preload;
if (!(preload = this.view.options.preload)) return [];
var pages = [],
begin = Math.max(1, this._position - preload[0]),
end = Math.min(this._position + preload[1], this._total),
pos = this._position;
// add the pages after this one first for the preloading order
for (var i = pos; i <= end; i++) {
var page = Pages.pages[Pages.uid][i - 1];
if (page._position != pos) pages.push(page);
}
for (var i = pos; i >= begin; i--) {
var page = Pages.pages[Pages.uid][i - 1];
if (page._position != pos) pages.push(page);
}
return pages;
},
preloadSurroundingImages: function () {
var pages = this._getSurroundingPages();
$.each(
pages,
function (_i, page) {
page.preload();
}.bind(this)
);
},
// preload is a non-abortable preloader,
// so that it doesn't interfere with our regular load
preload: function () {
if (
this.preloading ||
this.preloaded ||
this.view.type !== "image" ||
!this.view.options.preload ||
this.loaded // page might be loaded before it's preloaded so also stop there
) {
return;
}
// make sure the page is created
this.create();
this.preloading = true;
new ImageReady(
this.content[0],
function (imageReady) {
this.loaded = true;
this.preloading = false;
this.preloaded = true;
this.dimensions = {
width: imageReady.img.naturalWidth,
height: imageReady.img.naturalHeight,
};
}.bind(this),
null,
{ method: "naturalWidth" }
);
},
// the purpose of load is to set dimensions
// we use it to set dimensions even for content that doesn't load like youtube
load: function (callback, isPreload) {
// make sure the page is created
this.create();
// exit early if already loaded
if (this.loaded) {
if (callback) callback();
return;
}
// abort possible previous (pre)load
this.abort();
// loading indicator, we don't show it when preloading frames
this.loading = true;
// start spinner
// only when this url hasn't been loaded before
if (this.view.options.spinner && !loadedUrlCache[this.view.url]) {
Window.startLoading();
}
switch (this.view.type) {
case "image":
// if we had an error before just go through
if (this.error) {
if (callback) callback();
return;
}
this.imageReady = new ImageReady(
this.content[0],
function (imageReady) {
// mark as loaded
this._markAsLoaded();
this.dimensions = {
width: imageReady.img.naturalWidth,
height: imageReady.img.naturalHeight,
};
if (callback) callback();
}.bind(this),
function () {
// mark as loaded
this._markAsLoaded();
this.content.hide();
this.container.append(
(this.error = $("<div>").addClass("strp-error"))
);
this.element.addClass("strp-has-error");
this.dimensions = {
width: this.error.outerWidth(),
height: this.error.outerHeight(),
};
if (callback) callback();
}.bind(this),
{ method: "naturalWidth" }
);
break;
case "vimeo":
this.vimeoReady = new VimeoReady(
this.view.url,
function (data) {
// mark as loaded
this._markAsLoaded();
this.dimensions = {
width: data.dimensions.width,
height: data.dimensions.height,
};
if (callback) callback();
}.bind(this)
);
break;
case "youtube":
// mark as loaded
this._markAsLoaded();
this.dimensions = {
width: this.view.options.width,
height: this.view.options.height,
};
if (callback) callback();
break;
}
},
// helper for load()
_markAsLoaded: function () {
this.loading = false;
this.loaded = true;
// mark url as loaded so we can avoid
// showing the spinner again
loadedUrlCache[this.view.url] = true;
Window.stopLoading();
},
isVideo: function () {
return /^(youtube|vimeo)$/.test(this.view.type);
},
insertVideo: function (callback) {
// don't insert a video twice
// and stop if not a video
if (this.playerIframe || !this.isVideo()) {
if (callback) callback();
return;
}
var protocol =
"http" +
(window.location && window.location.protocol === "https:"
? "s"
: "") +
":",
playerVars = $.extend({}, this.view.options[this.view.type] || {}),
queryString = $.param(playerVars),
urls = {
vimeo: protocol + "//player.vimeo.com/video/{id}?{queryString}",
youtube: protocol + "//www.youtube.com/embed/{id}?{queryString}",
},
src = urls[this.view.type]
.replace("{id}", this.view._data.id)
.replace("{queryString}", queryString);
this.content.append(
(this.playerIframe = $(
"<iframe webkitAllowFullScreen mozallowfullscreen allowFullScreen>"
).attr({
src: src,
height: this.contentDimensions.height,
width: this.contentDimensions.width,
frameborder: 0,
}))
);
if (callback) callback();
},
raise: function () {
// no need to raise if we're already the topmost element
// this helps avoid unnecessary detaching of the element
var lastChild = Pages.element[0].lastChild;
if (lastChild && lastChild === this.element[0]) {
return;
}
Pages.element.append(this.element);
},
show: function (callback) {
var shq = this.queues.showhide;
shq.queue([]); // clear queue
this.animated = true;
this.animatingWindow = false;
shq.queue(function (next_stopped_inactive) {
Pages.stopInactive();
next_stopped_inactive();
});
shq.queue(
function (next_side) {
Window.setSide(this.view.options.side, next_side);
}.bind(this)
);
// make sure the current page is inserted
shq.queue(
function (next_loaded) {
// give the spinner the options of this page
if (this.view.options.spinner && Window._spinner) {
Window.setSpinnerSkin(this.view.options.skin);
Window._spinner.setOptions(this.view.options.effects.spinner);
Window._spinner.refresh();
}
// load
this.load(
function () {
this.preloadSurroundingImages();
next_loaded();
}.bind(this)
);
}.bind(this)
);
shq.queue(
function (next_utility) {
this.raise();
Window.setSkin(this.view.options.skin);
Window.bindUI(); // enable ui controls
// keyboard
Keyboard.enable(this.view.options.keyboard);
this.fitToWindow();
next_utility();
}.bind(this)
);
// we bind hide on click outside with a delay so API calls can pass through.
// more on this in api.js
shq.queue(
function (next_bind_hide_on_click_outside) {
Window.timers.set(
"bind-hide-on-click-outside",
function () {
Window.bindHideOnClickOutside();
next_bind_hide_on_click_outside();
}.bind(this),
1
);
}.bind(this)
);
// vimeo and youtube use this for insertion
if (this.isVideo()) {
shq.queue(
function (next_video_inserted) {
this.insertVideo(function () {
next_video_inserted();
});
}.bind(this)
);
}
shq.queue(
function (next_shown_and_resized) {
this.animatingWindow = true; // we're modifying Window size
var fx = 3;
// store duration on resize and use it for the other animations
var z = this.getOrientation() === "horizontal" ? "width" : "height";
// onShow callback
var onShow = this.view && this.view.options.onShow;
if (typeof onShow === "function") {
onShow.call(Strip);
}
var duration = Window.resize(
this[z],
function () {
if (--fx < 1) next_shown_and_resized();
},
duration
);
this._show(function () {
if (--fx < 1) next_shown_and_resized();
}, duration);
Window.adjustPrevNext(function () {
if (--fx < 1) next_shown_and_resized();
}, duration);
if (Window._showUIOnResize) {
Window.showUI(null, duration);
// don't show the UI the next time, it'll show up
// when we set this flag again
Window._showUIOnResize = false;
}
// we also don't track this
Pages.hideVisibleInactive(duration);
}.bind(this)
);
shq.queue(
function (next_set_visible) {
// Window.resize takes this into account
this.animatingWindow = false;
this.animated = false;
this.visible = true;
// NOTE: disabled to allow the UI to fade out at all times
//Window.startUITimer();
if (callback) callback();
next_set_visible();
}.bind(this)
);
},
_show: function (callback, alternateDuration) {
var duration = !Window.visible
? 0
: typeof alternateDuration === "number"
? alternateDuration
: this.view.options.effects.transition.min;
this.element
.stop(true)
.show()
.fadeTo(duration || 0, 1, callback);
},
hide: function (callback, alternateDuration) {
if (!this.element) return; // nothing to hide yet
this.removeVideo();
// abort possible loading
this.abort();
var duration = this.view.options.effects.transition.min;
if (typeof alternateDuration === "number") duration = alternateDuration;
// hide video instantly
var isVideo = this.isVideo();
if (isVideo) duration = 0;
// stop, delay & effect
this.element
.stop(true)
// we use alternative easing to minize background color showing through a lowered opacity fade
// while images are trading places
.fadeTo(
duration,
0,
"stripEaseInCubic",
$.proxy(function () {
this.element.hide();
this.visible = false;
if (callback) callback();
}, this)
);
},
// stop everything
stop: function () {
var shq = this.queues.showhide;
shq.queue([]); // clear queue
// stop animations
if (this.element) this.element.stop(true);
// stop possible loading
this.abort();
},
removeVideo: function () {
if (this.playerIframe) {
// this fixes a bug where sound keep playing after
// removing the iframe in IE10+
this.playerIframe[0].src = "//about:blank";
this.playerIframe.remove();
this.playerIframe = null;
}
},
remove: function () {
this.stop();
this.removeVideo();
if (this.element) this.element.remove();
this.visible = false;
this.removed = true;
},
abort: function () {
if (this.imageReady && !this.preloading) {
this.imageReady.abort();
this.imageReady = null;
}
if (this.vimeoReady) {
this.vimeoReady.abort();
this.vimeoReady = null;
}
this.loading = false;
Window.stopLoading();
},
_getDimensionsFitToView: function () {
var bounds = $.extend({}, this.dimensions),
dimensions = $.extend({}, this.dimensions);
var options = this.view.options;
if (options.maxWidth) bounds.width = options.maxWidth;
if (options.maxHeight) bounds.heigth = options.maxHeight;
dimensions = Fit.within(bounds, dimensions);
return dimensions;
},
getOrientation: function (side) {
return this.view.options.side === "left" ||
this.view.options.side === "right"
? "horizontal"
: "vertical";
},
fitToWindow: function () {
var page = this.element,
dimensions = this._getDimensionsFitToView(),
viewport = Bounds.viewport(),
bounds = $.extend({}, viewport),
orientation = this.getOrientation(),
z = orientation === "horizontal" ? "width" : "height";
var container = page.find(".strp-container");
// add the safety
Window.element.removeClass("strp-measured");
var win = Window.element,
isFullscreen =
z === "width"
? parseInt(win.css("min-width")) > 0
: parseInt(win.css("min-height")) > 0,
safety = isFullscreen
? 0
: parseInt(win.css("margin-" + (z === "width" ? "left" : "bottom")));
Window.element.addClass("strp-measured");
bounds[z] -= safety;
var paddingX =
parseInt(container.css("padding-left")) +
parseInt(container.css("padding-right")),
paddingY =
parseInt(container.css("padding-top")) +
parseInt(container.css("padding-bottom")),
padding = { x: paddingX, y: paddingY };
bounds.width -= paddingX;
bounds.height -= paddingY;
var fitted = Fit.within(bounds, dimensions),
contentDimensions = $.extend({}, fitted),
content = this.content;
// if we have an error message, use that as content instead
if (this.error) {
content = this.error;
}
var info = this.info,
cH = 0;
// when there's an info bar size has to be adjusted
if (info) {
// make sure the window and the page are visible during all of this
var windowVisible = Window.element.is(":visible");
if (!windowVisible) Window.element.show();
var pageVisible = page.is(":visible");
if (!pageVisible) page.show();
// width
if (z === "width") {
page.css({
width: isFullscreen ? viewport.width : fitted.width + paddingX,
});
var initialBoundsHeight = bounds.height;
content.hide();
cH = info.outerHeight();
content.show();
bounds.height = initialBoundsHeight - cH;
contentDimensions = Fit.within(bounds, dimensions);
// left/right requires further adjustment of the caption
var initialImageSize = $.extend({}, contentDimensions),
initialCH = cH,
newCW,
previousCH,
shrunkW;
var attempts = isFullscreen ? 0 : 4; // fullscreen doesn't need extra resizing
while (
attempts > 0 &&
(shrunkW = fitted.width - contentDimensions.width)
) {
page.css({ width: fitted.width + paddingX - shrunkW });
previousCH = cH;
content.hide();
cH = info.outerHeight();
newCW = Math.max(
this.caption ? this.caption.outerWidth() + paddingX : 0,
this.position ? this.position.outerWidth() + paddingX : 0
);
content.show();
if (
cH === previousCH &&
newCW <= fitted.width + paddingX - shrunkW
) {
// safe to keep this width, so store it
fitted.width -= shrunkW;
} else {
// we try again with the increased caption
bounds.height = initialBoundsHeight - cH;
contentDimensions = Fit.within(bounds, dimensions);
// restore if the last attempt failed
if (attempts - 1 <= 0) {
// otherwise the caption increased in height, go back
page.css({ width: fitted.width + paddingX });
contentDimensions = initialImageSize;
cH = initialCH;
}
}
attempts--;
}
} else {
// fix IE7 not respecting width:100% in the CSS
// so info height is measured correctly
if (Browser.IE && Browser.IE < 8) {
page.css({ width: viewport.width });
}
// height
content.hide();
cH = info.outerHeight();
content.show();
bounds.height -= cH;
contentDimensions = Fit.within(bounds, dimensions);
fitted.height = contentDimensions.height;
}
// restore visibility
if (!pageVisible) page.hide();
if (!windowVisible) Window.element.hide();
}
// page needs a fixed width to remain properly static during animation
var pageDimensions = {
width: fitted.width + paddingX,
height: fitted.height + paddingY + cH,
};
// fullscreen mode uses viewport dimensions for the page
if (isFullscreen) pageDimensions = viewport;
if (z === "width") {
page.css({ width: pageDimensions.width });
} else {
page.css({ height: pageDimensions.height });
}
container.css({ bottom: cH });
// margins
var mLeft = -0.5 * contentDimensions.width,
mTop = -0.5 * contentDimensions.height;
// floor margins on IE6-7 because it doesn't render .5px properly
if (Browser.IE && Browser.IE < 8) {
mLeft = Math.floor(mLeft);
mTop = Math.floor(mTop);
}
content.css(
$.extend({}, contentDimensions, {
"margin-left": mLeft,
"margin-top": mTop,
})
);
if (this.playerIframe) {
this.playerIframe.attr(contentDimensions);
}
this.contentDimensions = contentDimensions;
// store for later use within animation
this.width = pageDimensions.width;
this.height = pageDimensions.height;
this.z = this[z];
},
});
return Page;
})();
var Window = {
initialize: function () {
this.queues = [];
this.queues.hide = $({});
this.pages = [];
this.timers = new Timers();
this.build();
this.setSkin(Options.defaults.skin);
},
build: function () {
// spinner
if (Spinner.supported) {
$(document.body).append(
(this.spinnerMove = $("<div>")
.addClass("strp-spinner-move")
.hide()
.append((this.spinner = $("<div>").addClass("strp-spinner"))))
);
this._spinner = new Spinner(this.spinner);
this._spinnerMoveSkinless = this.spinnerMove[0].className;
}
// window
$(document.body).append(
(this.element = $("<div>")
.addClass("strp-window strp-measured")
.append((this._pages = $("<div>").addClass("strp-pages")))
.append(
(this._previous = $("<div>")
.addClass("strp-nav strp-nav-previous")
.append(
$("<div>")
.addClass("strp-nav-button")
.append($("<div>").addClass("strp-nav-button-background"))
.append($("<div>").addClass("strp-nav-button-icon"))
)
.hide())
)
.append(
(this._next = $("<div>")
.addClass("strp-nav strp-nav-next")
.append(
$("<div>")
.addClass("strp-nav-button")
.append($("<div>").addClass("strp-nav-button-background"))
.append($("<div>").addClass("strp-nav-button-icon"))
)
.hide())
)
// close
.append(
(this._close = $("<div>")
.addClass("strp-close")
.append($("<div>").addClass("strp-close-background"))
.append($("<div>").addClass("strp-close-icon")))
))
);
Pages.initialize(this._pages);
// support classes
if (Support.mobileTouch) this.element.addClass("strp-mobile-touch");
if (!Support.svg) this.element.addClass("strp-no-svg");
// events
this._close.on(
"click",
function (event) {
event.preventDefault();
this.hide();
}.bind(this)
);
this._previous.on(
"click",
function (event) {
this.previous();
this._onMouseMove(event); // update cursor
}.bind(this)
);
this._next.on(
"click",
function (event) {
this.next();
this._onMouseMove(event); // update cursor
}.bind(this)
);
this.hideUI(null, 0); // start with hidden <>
},
setSkin: function (skin) {
if (this._skin) {
this.element.removeClass("strp-window-skin-" + this._skin);
}
this.element.addClass("strp-window-skin-" + skin);
this._skin = skin;
},
setSpinnerSkin: function (skin) {
if (!this.spinnerMove) return;
if (this._spinnerSkin) {
this.spinnerMove.removeClass(
"strp-spinner-move-skin-" + this._spinnerSkin
);
}
this.spinnerMove.addClass("strp-spinner-move-skin-" + skin);
// refresh in case of styling updates
this._spinner.refresh();
this._spinnerSkin = skin;
},
// Resize
startObservingResize: function () {
if (this._isObservingResize) return;
this._onWindowResizeHandler = this._onWindowResize.bind(this);
$(window).on("resize orientationchange", this._onWindowResizeHandler);
this._isObservingResize = true;
},
stopObservingResize: function () {
if (this._onWindowResizeHandler) {
$(window).off("resize orientationchange", this._onWindowResizeHandler);
this._onWindowResizeHandler = null;
}
this._isObservingResize = false;
},
_onWindowResize: function () {
var page;
if (!(page = Pages.page)) return;
if (page.animated || page.animatingWindow) {
// we're animating, don't stop the animation,
// instead update dimensions and restart/continue showing
page.fitToWindow();
page.show();
} else {
// we're not in an animation, resize instantly
page.fitToWindow();
this.resize(page.z, null, 0);
this.adjustPrevNext(null, true);
}
},
resize: function (wh, callback, alternateDuration) {
var orientation = this.getOrientation(),
Z = orientation === "vertical" ? "Height" : "Width",
z = Z.toLowerCase();
if (wh > 0) {
this.visible = true;
this.startObservingResize();
}
var fromZ = Window.element["outer" + Z](),
duration;
// if we're opening use the show duration
if (fromZ === 0) {
duration = this.view.options.effects.window.show;
// add opening class
this.element.addClass("strp-opening");
this.opening = true;
} else if (typeof alternateDuration === "number") {
// alternate when set
duration = alternateDuration;
} else {
// otherwise decide on a duration for the transition
// based on distance
var transition = this.view.options.effects.transition,
min = transition.min,
max = transition.max,
tdiff = max - min,
viewport = Bounds.viewport(),
distance = Math.abs(fromZ - wh),
percentage = Math.min(1, distance / viewport[z]);
duration = Math.round(min + percentage * tdiff);
}
if (wh === 0) {
this.closing = true;
// we only add the closing class if we're not currently animating the window
if (!this.element.is(":animated")) {
this.element.addClass("strp-closing");
}
}
// the animations
var css = { overflow: "visible" };
css[z] = wh;
var fx = 1;
// _getEventSide checks this.element.outerWidth() on mousemove only when
// this._outerWidth isn't set, we need that during animation,
// afterResize will set it back along with the cached offsetLeft
this._outerWidth = null;
this._offsetLeft = null;
var onResize = this.view.options.onResize,
hasOnResize = typeof onResize === "function";
this.element.stop(true).animate(
css,
$.extend(
{
duration: duration,
complete: function () {
if (--fx < 1) this._afterResize(callback);
}.bind(this),
},
!hasOnResize
? {}
: {
// we only add step if there's an onResize callback
step: function (now, fx) {
if (fx.prop === z) {
onResize.call(Strip, fx.prop, now, this.side);
}
}.bind(this),
}
)
);
if (this.spinnerMove) {
fx++; // sync this effect
this.spinnerMove.stop(true).animate(
css,
duration,
function () {
if (--fx < 1) this._afterResize(callback);
}.bind(this)
);
}
// return the duration for later use in synced animations
return duration;
},
_afterResize: function (callback) {
this.opening = false;
this.closing = false;
this.element.removeClass("strp-opening strp-closing");
// cache outerWidth and offsetLeft for _getEventSide on mousemove
this._outerWidth = this.element.outerWidth();
this._offsetLeft = this.element.offset().left;
if (callback) callback();
},
adjustPrevNext: function (callback, alternateDuration) {
if (!this.view || !Pages.page) return;
var page = Pages.page;
// offset <>
var windowVisible = this.element.is(":visible");
if (!windowVisible) this.element.show();
var pRestoreStyle = this._previous.attr("style");
//this._previous.attr({ style: '' });
this._previous.removeAttr("style");
var pnMarginTop = parseInt(this._previous.css("margin-top")); // the original margin top
this._previous.attr({ style: pRestoreStyle });
if (!windowVisible) this.element.hide();
var iH = page.info ? page.info.outerHeight() : 0;
var buttons = this._previous.add(this._next),
css = { "margin-top": pnMarginTop - iH * 0.5 };
var duration = this.view.options.effects.transition.min;
if (typeof alternateDuration === "number") duration = alternateDuration;
// adjust <> instantly when opening
if (this.opening) duration = 0;
buttons.stop(true).animate(css, duration, callback);
this._previous[this.mayPrevious() ? "show" : "hide"]();
this._next[this.mayNext() ? "show" : "hide"]();
},
resetPrevNext: function () {
var buttons = this._previous.add(this._next);
buttons.stop(true).removeAttr("style");
},
// Load
load: function (views, position) {
this.views = views;
Pages.add(views);
if (position) {
this.setPosition(position);
}
},
// adjust the size based on the current view
// this might require closing the window first
setSide: function (side, callback) {
if (this.side === side) {
if (callback) callback();
return;
}
// side has change, first close the window if it isn't already closed
if (this.visible) {
// NOTE: side should be set here since the window was visible
// so using resize should be safe
// hide the UI
var duration = this.view ? this.view.options.effects.window.hide : 0;
this.hideUI(null, duration);
// avoid tracking mouse movement while the window is closing
this.unbindUI();
// hide
this.resize(
0,
function () {
// some of the things we'd normally do in hide
this._safeResetsAfterSwitchSide();
// we instantly hide the other views here
Pages.hideVisibleInactive(0);
this._setSide(side, callback);
}.bind(this)
);
// show the UI on the next resize
this._showUIOnResize = true;
} else {
this._setSide(side, callback);
}
},
_setSide: function (side, callback) {
this.side = side;
var orientation = this.getOrientation();
var elements = this.element;
if (this.spinnerMove) elements = elements.add(this.spinnerMove);
elements
.removeClass("strp-horizontal strp-vertical")
.addClass("strp-" + orientation);
var ss = "strp-side-";
elements
.removeClass(ss + "top " + ss + "right " + ss + "bottom " + ss + "left")
.addClass(ss + side);
if (callback) callback();
},
getOrientation: function (side) {
return this.side === "left" || this.side === "right"
? "horizontal"
: "vertical";
},
// loading indicator
startLoading: function () {
if (!this._spinner) return;
this.spinnerMove.show();
this._spinner.show();
},
stopLoading: function () {
if (!this._spinner) return;
// we only stop loading if there are no loading pages anymore
var loadingCount = Pages.getLoadingCount();
if (loadingCount < 1) {
this._spinner.hide(
function () {
this.spinnerMove.hide();
}.bind(this)
);
}
},
setPosition: function (position, callback) {
this._position = position;
// store the current view
this.view = this.views[position - 1];
// we need to make sure that a possible hide effect doesn't
// trigger its callbacks, as that would cancel the showing/loading
// of the page started below
this.stopHideQueue();
// store the page and show it
this.page = Pages.show(
position,
function () {
var afterPosition = this.view.options.afterPosition;
if (typeof afterPosition === "function") {
afterPosition.call(Strip, position);
}
if (callback) callback();
}.bind(this)
);
},
hide: function (callback) {
if (!this.view) return;
var hideQueue = this.queues.hide;
hideQueue.queue([]); // clear queue
hideQueue.queue(
function (next_stop) {
Pages.stop();
next_stop();
}.bind(this)
);
hideQueue.queue(
function (next_unbinds) {
// ui
var duration = this.view ? this.view.options.effects.window.hide : 0;
this.unbindUI();
this.hideUI(null, duration);
// close on click outside
this.unbindHideOnClickOutside();
// keyboard
Keyboard.disable();
next_unbinds();
}.bind(this)
);
hideQueue.queue(
function (next_zero) {
// active classes should removed right as the closing effect starts
// because clicking an element as it closes will re-open it,
// that needs to be reflected in the class
Pages.removeActiveClasses();
this.resize(0, next_zero, this.view.options.effects.window.hide);
// after we initiate the hide resize, the next resize should bring up the UI again
this._showUIOnResize = true;
}.bind(this)
);
// callbacks after resize in a separate queue
// so we can stop the hideQueue without stopping the resize
hideQueue.queue(
function (next_after_resize) {
this._safeResetsAfterSwitchSide();
this.stopObservingResize();
Pages.removeAll();
this.timers.clear();
this._position = -1;
// afterHide callback
var afterHide = this.view && this.view.options.afterHide;
if (typeof afterHide === "function") {
afterHide.call(Strip);
}
this.view = null;
next_after_resize();
}.bind(this)
);
if (typeof callback === "function") {
hideQueue.queue(
function (next_callback) {
callback();
next_callback();
}.bind(this)
);
}
},
// stop all callbacks possibly queued up into a hide animation
// this allows the hide animation to finish as we start showing/loading
// a new page, a callback could otherwise interrupt this
stopHideQueue: function () {
this.queues.hide.queue([]);
},
// these are things we can safely call when switching side as well
_safeResetsAfterSwitchSide: function () {
// remove styling from window, so no width: 100%; height: 0 issues
this.element.removeAttr("style");
if (this.spinnerMove) this.spinnerMove.removeAttr("style");
//Pages.removeExpired();
this.visible = false;
this.hideUI(null, 0);
this.timers.clear("ui");
this.resetPrevNext();
// clear cached mousemove
this._x = -1;
this._y = -1;
},
// Previous / Next
mayPrevious: function () {
return (
(this.view &&
this.view.options.loop &&
this.views &&
this.views.length > 1) ||
this._position !== 1
);
},
previous: function (force) {
var mayPrevious = this.mayPrevious();
if (force || mayPrevious) {
this.setPosition(this.getSurroundingIndexes().previous);
}
},
mayNext: function () {
var hasViews = this.views && this.views.length > 1;
return (
(this.view && this.view.options.loop && hasViews) ||
(hasViews && this.getSurroundingIndexes().next !== 1)
);
},
next: function (force) {
var mayNext = this.mayNext();
if (force || mayNext) {
this.setPosition(this.getSurroundingIndexes().next);
}
},
// surrounding
getSurroundingIndexes: function () {
if (!this.views) return {};
var pos = this._position,
length = this.views.length;
var previous = pos <= 1 ? length : pos - 1,
next = pos >= length ? 1 : pos + 1;
return {
previous: previous,
next: next,
};
},
// close when clicking outside of strip or an element opening strip
bindHideOnClickOutside: function () {
this.unbindHideOnClickOutside();
$(document.documentElement).on(
"click",
(this._delegateHideOutsideHandler = this._delegateHideOutside.bind(this))
);
},
unbindHideOnClickOutside: function () {
if (this._delegateHideOutsideHandler) {
$(document.documentElement).off(
"click",
this._delegateHideOutsideHandler
);
this._delegateHideOutsideHandler = null;
}
},
_delegateHideOutside: function (event) {
var page = Pages.page;
if (!this.visible || !(page && page.view.options.hideOnClickOutside))
return;
var element = event.target;
if (!$(element).closest(".strip, .strp-window")[0]) {
this.hide();
}
},
// UI
bindUI: function () {
this.unbindUI();
if (!Support.mobileTouch) {
this.element
.on("mouseenter", (this._showUIHandler = this.showUI.bind(this)))
.on("mouseleave", (this._hideUIHandler = this.hideUI.bind(this)));
this.element.on(
"mousemove",
(this._mousemoveUIHandler = function (event) {
// Chrome has a bug that triggers mousemove events incorrectly
// we have to work around this by comparing cursor positions
// so only true mousemove events pass through:
// https://code.google.com/p/chromium/issues/detail?id=420032
var x = event.pageX,
y = event.pageY;
if (this._hoveringNav || (y === this._y && x === this._x)) {
return;
}
// cache x/y
this._x = x;
this._y = y;
this.showUI();
this.startUITimer();
}.bind(this))
);
// delegate <> mousemove/click states
this._pages
.on(
"mousemove",
".strp-container",
(this._onMouseMoveHandler = this._onMouseMove.bind(this))
)
.on(
"mouseleave",
".strp-container",
(this._onMouseLeaveHandler = this._onMouseLeave.bind(this))
)
.on(
"mouseenter",
".strp-container",
(this._onMouseEnterHandler = this._onMouseEnter.bind(this))
);
// delegate moving onto the <> buttons
// keeping the mouse on them should keep the buttons visible
this.element
.on(
"mouseenter",
".strp-nav",
(this._onNavMouseEnterHandler = this._onNavMouseEnter.bind(this))
)
.on(
"mouseleave",
".strp-nav",
(this._onNavMouseLeaveHandler = this._onNavMouseLeave.bind(this))
);
$(window).on(
"scroll",
(this._onScrollHandler = this._onScroll.bind(this))
);
}
this._pages.on(
"click",
".strp-container",
(this._onClickHandler = this._onClick.bind(this))
);
},
unbindUI: function () {
if (this._showUIHandler) {
this.element
.off("mouseenter", this._showUIHandler)
.off("mouseleave", this._hideUIHandler)
.off("mousemove", this._mousemoveUIHandler);
this._pages
.off("mousemove", ".strp-container", this._onMouseMoveHandler)
.off("mouseleave", ".strp-container", this._onMouseLeaveHandler)
.off("mouseenter", ".strp-container", this._onMouseEnterHandler);
this.element
.off("mouseenter", ".strp-nav", this._onNavMouseEnterHandler)
.off("mouseleave", ".strp-nav", this._onNavMouseLeaveHandler);
$(window).off("scroll", this._onScrollHandler);
this._showUIHandler = null;
}
if (this._onClickHandler) {
this._pages.off("click", ".strp-container", this._onClickHandler);
this._onClickHandler = null;
}
},
// reset cached offsetLeft and outerWidth so they are recalculated after scrolling,
// the cached values might be incorrect after scrolling left/right
_onScroll: function () {
this._offsetLeft = this._outerWidth = null;
},
// events bounds by bindUI
_onMouseMove: function (event) {
var Side = this._getEventSide(event),
side = Side.toLowerCase();
this.element[(this["may" + Side]() ? "add" : "remove") + "Class"](
"strp-hovering-clickable"
);
this._previous[(side !== "next" ? "add" : "remove") + "Class"](
"strp-nav-previous-hover strp-nav-hover"
);
this._next[(side === "next" ? "add" : "remove") + "Class"](
"strp-nav-next-hover strp-nav-hover"
);
},
_onMouseLeave: function () {
this.element.removeClass("strp-hovering-clickable");
this._previous
.removeClass("strp-nav-previous-hover")
.add(this._next.removeClass("strp-nav-next-hover"))
.removeClass("strp-nav-hover");
},
_onClick: function (event) {
var Side = this._getEventSide(event),
side = Side.toLowerCase();
this[side]();
// adjust cursor, doesn't work with effects
// but _onMouseEnter is used to fix that
this._onMouseMove(event);
},
_onMouseEnter: function (event) {
// this solves clicking an area and not having an updating cursor
// when not moving cursor after click. When an overlapping page comes into view
// it'll trigger a mouseenter after the mouseout on the disappearing page
// that would normally remove the clickable class
this._onMouseMove(event);
},
_getEventSide: function (event) {
var offsetLeft = this._offsetLeft || this.element.offset().left,
left = event.pageX - offsetLeft,
width = this._outerWidth || this.element.outerWidth();
return left < 0.5 * width ? "Previous" : "Next";
},
_onNavMouseEnter: function (event) {
this._hoveringNav = true;
this.clearUITimer();
},
_onNavMouseLeave: function (event) {
this._hoveringNav = false;
this.startUITimer();
},
// Actual UI actions
showUI: function (callback, alternateDuration) {
// clear the timer everytime so we can keep clicking elements and fading
// in the ui while not having the timer interupt that with a hide
this.clearUITimer();
// we're only fading the inner button icons since the margin on their wrapper divs might change
var elements = this.element.find(".strp-nav-button");
var duration = this.view ? this.view.options.effects.ui.show : 0;
if (typeof alternateDuration === "number") duration = alternateDuration;
elements.stop(true).fadeTo(
duration,
1,
"stripEaseInSine",
function () {
this.startUITimer();
if (typeof callback === "function") callback();
}.bind(this)
);
},
hideUI: function (callback, alternateDuration) {
var elements = this.element.find(".strp-nav-button");
var duration = this.view ? this.view.options.effects.ui.hide : 0;
if (typeof alternateDuration === "number") duration = alternateDuration;
elements.stop(true).fadeOut(duration, "stripEaseOutSine", function () {
if (typeof callback === "function") callback();
});
},
// UI Timer
// not used on mobile-touch based devices
clearUITimer: function () {
if (Support.mobileTouch) return;
this.timers.clear("ui");
},
startUITimer: function () {
if (Support.mobileTouch) return;
this.clearUITimer();
this.timers.set(
"ui",
function () {
this.hideUI();
}.bind(this),
this.view ? this.view.options.uiDelay : 0
);
},
};
// Keyboard
// keeps track of keyboard events when enabled
var Keyboard = {
enabled: false,
keyCode: {
left: 37,
right: 39,
esc: 27,
},
// enable is passed the keyboard option of a page, which can be false
// or contains multiple buttons to toggle
enable: function (enabled) {
this.disable();
if (!enabled) return;
$(document)
.on("keydown", (this._onKeyDownHandler = this.onKeyDown.bind(this)))
.on("keyup", (this._onKeyUpHandler = this.onKeyUp.bind(this)));
this.enabled = enabled;
},
disable: function () {
this.enabled = false;
if (this._onKeyUpHandler) {
$(document)
.off("keyup", this._onKeyUpHandler)
.off("keydown", this._onKeyDownHandler);
this._onKeyUpHandler = this._onKeyDownHandler = null;
}
},
onKeyDown: function (event) {
if (!this.enabled || !Window.visible) return;
var key = this.getKeyByKeyCode(event.keyCode);
if (!key || (key && this.enabled && !this.enabled[key])) return;
event.preventDefault();
event.stopPropagation();
switch (key) {
case "left":
Window.previous();
break;
case "right":
Window.next();
break;
}
},
onKeyUp: function (event) {
if (!this.enabled || !Window.visible) return;
var key = this.getKeyByKeyCode(event.keyCode);
if (!key || (key && this.enabled && !this.enabled[key])) return;
switch (key) {
case "esc":
Window.hide();
break;
}
},
getKeyByKeyCode: function (keyCode) {
for (var key in this.keyCode) {
if (this.keyCode[key] === keyCode) return key;
}
return null;
},
};
// API
// an unexposed object for internal use
var _Strip = {
_disabled: false,
_fallback: true,
initialize: function () {
Window.initialize();
if (!this._disabled) this.startDelegating();
},
// click delegation
startDelegating: function () {
this.stopDelegating();
$(document.documentElement).on(
"click",
".strip[href]",
(this._delegateHandler = this.delegate.bind(this))
);
},
stopDelegating: function () {
if (this._delegateHandler) {
$(document.documentElement).off(
"click",
".strip[href]",
this._delegateHandler
);
this._delegateHandler = null;
}
},
delegate: function (event) {
if (this._disabled) return;
event.stopPropagation();
event.preventDefault();
var element = event.currentTarget;
_Strip.show(element);
},
show: function (object) {
if (this._disabled) {
this.showFallback.apply(_Strip, _slice.call(arguments));
return;
}
var options = arguments[1] || {},
position = arguments[2];
if (arguments[1] && typeof arguments[1] === "number") {
position = arguments[1];
options = {};
}
var views = [],
object_type,
isElement = object && object.nodeType === 1;
switch ((object_type = typeof object)) {
case "string":
case "object":
var view = new View(object, options),
_dgo = "data-strip-group-options";
if (view.group) {
// extend the entire group
// if we have an element, look for other elements
if (isElement) {
var elements = $(
'.strip[data-strip-group="' +
$(object).attr("data-strip-group") +
'"]'
);
// find possible group options
var groupOptions = {};
elements.filter("[" + _dgo + "]").each(function (i, element) {
$.extend(
groupOptions,
eval("({" + ($(element).attr(_dgo) || "") + "})")
);
});
elements.each(function (i, element) {
// adjust the position if we find the given object position
if (!position && element === object) position = i + 1;
views.push(
new View(element, $.extend({}, groupOptions, options))
);
});
}
} else {
var groupOptions = {};
if (isElement && $(object).is("[" + _dgo + "]")) {
$.extend(
groupOptions,
eval("({" + ($(object).attr(_dgo) || "") + "})")
);
// reset the view with group options applied
view = new View(object, $.extend({}, groupOptions, options));
}
views.push(view);
}
break;
case "array":
$.each(object, function (i, item) {
var view = new View(item, options);
views.push(view);
});
break;
}
// if we haven't found a position by now, load the first view
if (!position || position < 1) {
position = 1;
}
if (position > views.length) position = views.length;
// Allow API events to pass through by disabling hideOnClickOutside.
// It is re-enabled when bringing a page into view using a slight delay
// allowing a possible click event that triggers this show() function to
// fully bubble up. This is needed when Strip is visible and Strip.show()
// is called, the click would otherwise bubble down and instantly hide,
// cancelling the show()
Window.unbindHideOnClickOutside();
// if we've clicked an element, search for it in the currently open pagegroup
var positionInAPG;
if (
isElement &&
(positionInAPG = Pages.getPositionInActivePageGroup(object))
) {
// if we've clicked the exact same element it'll never re-enable
// hideOnClickOutside delegation because Pages.show() won't let it
// through, we re-enable it here in that case
if (positionInAPG === Window._position) {
Window.bindHideOnClickOutside();
}
Window.setPosition(positionInAPG);
} else {
// otherwise start loading and open
Window.load(views, position);
}
},
showFallback: (function () {
function getUrl(object) {
var url,
type = typeof object;
if (type === "string") {
url = object;
} else if (type === "array" && object[0]) {
url = getUrl(object[0]);
} else if (_.isElement(object) && $(object).attr("href")) {
url = $(object).attr("href");
} else if (object.url) {
url = object.url;
} else {
url = false;
}
return url;
}
return function (object) {
if (!_Strip._fallback) return;
var url = getUrl(object);
if (url) window.location.href = url;
};
})(),
};
$.extend(Strip, {
show: function (object) {
_Strip.show.apply(_Strip, _slice.call(arguments));
return this;
},
hide: function () {
Window.hide();
return this;
},
disable: function () {
_Strip.stopDelegating();
_Strip._disabled = true;
return this;
},
enable: function () {
_Strip._disabled = false;
_Strip.startDelegating();
return this;
},
fallback: function (fallback) {
_Strip._fallback = fallback;
return this;
},
setDefaultSkin: function (skin) {
Options.defaults.skin = skin;
return this;
},
});
// fallback for old browsers without full position:fixed support
if (
// IE6
(Browser.IE && Browser.IE < 7) ||
// old Android
// added a version check because Firefox on Android doesn't have a
// version number above 4.2 anymore
(typeof Browser.Android === "number" && Browser.Android < 3) ||
// old WebKit
(Browser.MobileSafari &&
typeof Browser.WebKit === "number" &&
Browser.WebKit < 533.18)
) {
// we'll reset the show function
_Strip.show = _Strip.showFallback;
// disable some functions we don't want to run
$.each(
"startDelegating stopDelegating initialize".split(" "),
function (i, fn) {
_Strip[fn] = function () {};
}
);
Strip.hide = function () {
return this;
};
}
// start
$(document).ready(function(event) {
_Strip.initialize();
});
return Strip;
}));