Files
TaborgMain/public/assets/vendors/fresco/js/fresco-src.js
T
2024-09-06 13:32:15 -04:00

5868 lines
149 KiB
JavaScript

/**
* MODIFIED BY LIQUID THEMES
* Fresco - A Beautiful Responsive Lightbox - v2.3.2
* (c) 2012-2021 Nick Stakenburg
*
* https://github.com/staaky/fresco
*
* @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.Fresco = factory(jQuery);
}
})(this, function($) {
var Fresco = {};
$.extend(Fresco, {
version: "2.3.2",
});
Fresco.Skins = {
// the default skin
fresco: {},
};
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;
},
};
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;
function baseToString(value) {
if (typeof value === "string") {
return value;
}
return value == null ? "" : value + "";
}
var _ = {
isElement: function(object) {
return object && object.nodeType === 1;
},
String: {
capitalize: function(string) {
string = baseToString(string);
return string && string.charAt(0).toUpperCase() + string.slice(1);
},
},
};
//mousewheel
(function() {
function wheel(event) {
/**
* Added by Liquid Themes
*/
if ( ! event || ! event.originalEvent ) return;
var realDelta;
// normalize the delta
if (event.originalEvent.wheelDelta)
// IE & Opera
realDelta = event.originalEvent.wheelDelta / 120;
else if (event.originalEvent.detail)
// W3C
realDelta = -event.originalEvent.detail / 3;
if (!realDelta) return;
var customEvent = $.Event("fresco:mousewheel");
$(event.target).trigger(customEvent, realDelta);
if (customEvent.isPropagationStopped()) {
event.stopPropagation();
}
if (customEvent.isDefaultPrevented()) {
event.preventDefault();
}
}
$(document.documentElement).on("mousewheel DOMMouseScroll", wheel);
})();
// 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
scale = Math.min(scaleX, scaleY);
// adjust current size, based on original dimensions
size = {
width: dimensions.width * scale,
height: 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, {
frescoEaseInCubic: function(x, t, b, c, d) {
return c * (t /= d) * t * t + b;
},
frescoEaseInSine: function(x, t, b, c, d) {
return -c * Math.cos((t / d) * (Math.PI / 2)) + c + b;
},
frescoEaseOutSine: 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 {
canvas: (function() {
var canvas = document.createElement("canvas");
return !!(canvas.getContext && canvas.getContext("2d"));
})(),
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.detectMobileTouch = function() {
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
//Support.mobileTouch = true;
};
Support.detectMobileTouch();
/* ImageReady (standalone) - part of Voilà
* http://voila.nickstakenburg.com
* MIT License
*/
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: "naturalWidth",
pollFallbackAfter: 1000,
},
arguments[3] || {}
);
// a fallback is used when we're not polling for naturalWidth/Height
// IE6-7 also use this to add support for naturalWidth/Height
if (!this.supports.naturalWidth || this.options.method === "onload") {
setTimeout(this.fallback.bind(this));
return;
}
// can exit out right away if we have a naturalWidth
if (this.img.complete && typeof this.img.naturalWidth !== "undefined") {
setTimeout(
function() {
if (this.img.naturalWidth > 0) {
this.success();
} else {
this.error();
}
}.bind(this)
);
return;
}
// we instantly bind to onerror so we catch right away
$(this.img).bind(
"error",
function() {
setTimeout(
function() {
this.error();
}.bind(this)
);
}.bind(this)
);
this.intervals = [
[1000, 10],
[2 * 1000, 50],
[4 * 1000, 100],
[20 * 1000, 500],
];
// for testing, 2sec delay
// this.intervals = [[20 * 1000, 2000]];
this._ipos = 0;
this._time = 0;
this._delay = this.intervals[this._ipos][1];
// start polling
this.poll();
},
poll: function() {
this._polling = setTimeout(
function() {
if (this.img.naturalWidth > 0) {
this.success();
return;
}
// update time spend
this._time += this._delay;
// use a fallback after waiting
if (
this.options.pollFallbackAfter &&
this._time >= this.options.pollFallbackAfter &&
!this._usedPollFallback
) {
this._usedPollFallback = true;
this.fallback();
}
// next i within the interval
if (this._time > this.intervals[this._ipos][0]) {
// if there's no next interval, we asume
// the image image errored out
if (!this.intervals[this._ipos + 1]) {
this.error();
return;
}
this._ipos++;
// update to the new bracket
this._delay = this.intervals[this._ipos][1];
}
this.poll();
}.bind(this),
this._delay
);
},
fallback: function() {
var img = new Image();
this._fallbackImg = img;
img.onload = function() {
img.onload = function() {};
if (!this.supports.naturalWidth) {
this.img.naturalWidth = img.width;
this.img.naturalHeight = img.height;
}
this.success();
}.bind(this);
img.onerror = this.error.bind(this);
img.src = this.img.src;
},
abort: function() {
if (this._fallbackImg) {
this._fallbackImg.onload = function() {};
}
if (this._polling) {
clearTimeout(this._polling);
this._polling = null;
}
},
success: function() {
if (this._calledSuccess) return;
this._calledSuccess = true;
this.isLoaded = true;
this.successCallback(this);
},
error: function() {
if (this._calledError) return;
this._calledError = true;
this.abort();
if (this.errorCallback) this.errorCallback(this);
},
});
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 Type = {
isVideo: function(type) {
return /^(youtube|vimeo)$/.test(type);
},
};
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),
};
},
},
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,
};
},
},
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,
};
},
},
};
var VimeoThumbnail = (function() {
var VimeoThumbnail = function() {
return this.initialize.apply(this, _slice.call(arguments));
};
$.extend(VimeoThumbnail.prototype, {
initialize: function(url, successCallback, errorCallback) {
this.url = url;
this.successCallback = successCallback;
this.errorCallback = errorCallback;
this.load();
},
load: function() {
// first try the cache
var cache = Cache.get(this.url);
if (cache) {
return this.successCallback(cache.data.url);
}
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 +
"&callback=?",
function(_data) {
if (_data && _data.thumbnail_url) {
var data = {
url: _data.thumbnail_url,
};
Cache.set(this.url, data);
this.successCallback(data.url);
} else {
this.errorCallback();
}
}.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 VimeoThumbnail;
})();
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;
// NOTE: We're using a maxwidth/maxheight hack because of a regression in the oEmbed API
// see: https://vimeo.com/forums/api/topic:283559
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: {
content: { show: 0, hide: 0 },
spinner: { show: 150, hide: 150 },
window: { show: 440, hide: 300 },
thumbnail: { show: 300, delay: 150 },
thumbnails: { slide: 0 },
},
keyboard: {
left: true,
right: true,
esc: true,
},
loadedMethod: "naturalWidth",
loop: false,
onClick: "previous-next",
overflow: false,
overlay: {
close: true,
},
preload: [1, 2],
position: true,
skin: "fresco",
spinner: true,
spinnerDelay: 300,
sync: true,
thumbnails: "horizontal",
ui: "outside",
uiDelay: 3000,
vimeo: {
autoplay: 1,
api: 1,
title: 1,
byline: 1,
portrait: 0,
loop: 0,
},
youtube: {
autoplay: 1,
controls: 1,
//cc_load_policy: 0,
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(
{},
Fresco.Skins[opts.skin] || Fresco.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);
// touch should never use ui:inside
if (Support.mobileTouch && options.ui === "inside") {
options.ui = "outside";
}
// 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 });
}
}
// overflow
if (!options.overflow || Support.mobileTouch) {
// false
options.overflow = { x: false, y: false };
} else {
if (typeof options.overflow === "boolean") {
// true
options.overflow = { x: false, y: true };
}
}
// vimeo & youtube always have no overlap
if (type === "vimeo" || type === "youtube") {
options.overlap = false;
}
// disabled thumbnails IE < 9 & touch based devices
if ((Browser.IE && Browser.IE < 9) || Support.mobileTouch) {
options.thumbnail = false;
options.thumbnails = false;
}
// width/height are only used for youtube
// convert it to maxWidth/Height for the other content
// when no max values have been set
if (type !== "youtube") {
if (options.width && !options.maxWidth) {
options.maxWidth = options.width;
}
if (options.height && !options.maxHeight) {
options.maxHeight = options.height;
}
}
// youtube thumbnails
if (!options.thumbnail && typeof options.thumbnail !== "boolean") {
// only continue if undefined, forced false stays false
var thumbnail = false;
switch (type) {
case "youtube":
var protocol =
"http" +
(window.location && window.location.protocol === "https:"
? "s"
: "") +
":";
thumbnail = protocol + "//img.youtube.com/vi/" + data.id + "/0.jpg";
break;
case "image":
case "vimeo":
thumbnail = true;
break;
}
options.thumbnail = thumbnail;
}
return options;
},
};
var Overlay = {
initialize: function() {
this.build();
this.visible = false;
},
build: function() {
this.element = $("<div>")
.addClass("fr-overlay")
.hide()
.append($("<div>").addClass("fr-overlay-background"));
this.element.on(
"click",
function() {
var page = Pages.page;
if (
page &&
page.view &&
page.view.options.overlay &&
!page.view.options.overlay.close
) {
return;
}
Window.hide();
}.bind(this)
);
if (Support.mobileTouch) {
this.element.addClass("fr-mobile-touch");
}
// prevent mousewheel scroll
this.element.on("fresco:mousewheel", function(event) {
event.preventDefault();
});
},
setSkin: function(skin) {
if (this.skin) {
this.element.removeClass("fr-overlay-skin-" + this.skin);
}
this.element.addClass("fr-overlay-skin-" + skin);
this.skin = skin;
},
attach: function() {
$(document.body).append(this.element);
},
detach: function() {
this.element.detach();
},
show: function(callback, alternateDuration) {
if (this.visible) {
if (callback) callback();
return;
}
this.visible = true;
this.attach();
this.max();
var pDuration =
(Pages.page && Pages.page.view.options.effects.window.show) || 0,
duration =
(typeof alternateDuration === "number"
? alternateDuration
: pDuration) || 0;
this.element.stop(true).fadeTo(duration, 1, callback);
},
hide: function(callback, alternateDuration) {
if (!this.visible) {
if (callback) callback();
return;
}
var pDuration =
(Pages.page && Pages.page.view.options.effects.window.hide) || 0,
duration =
(typeof alternateDuration === "number"
? alternateDuration
: pDuration) || 0;
this.element.stop(true).fadeOut(
duration || 0,
function() {
this.detach();
this.visible = false;
if (callback) callback();
}.bind(this)
);
},
getScrollDimensions: function() {
var dimensions = {};
$.each(["width", "height"], function(i, d) {
var D = d.substr(0, 1).toUpperCase() + d.substr(1),
ddE = document.documentElement;
dimensions[d] =
(Browser.IE
? Math.max(ddE["offset" + D], ddE["scroll" + D])
: Browser.WebKit
? document.body["scroll" + D]
: ddE["scroll" + D]) || 0;
});
return dimensions;
},
max: function() {
var scrollDimensions;
if (Browser.MobileSafari && (Browser.WebKit && Browser.WebKit < 533.18)) {
scrollDimensions = this.getScrollDimensions();
this.element.css(scrollDimensions);
}
if (Browser.IE && Browser.IE < 9) {
var viewport = Bounds.viewport();
this.element.css({ height: viewport.height, width: viewport.width });
}
if (Support.mobileTouch && !scrollDimensions) {
this.element.css({
height: this.getScrollDimensions().height,
});
}
},
};
var Window = {
initialize: function() {
this.queues = [];
this.queues.hide = $({});
this.pages = [];
this._tracking = [];
this._first = true;
this.timers = new Timers();
this.build();
this.setSkin(Options.defaults.skin);
},
build: function() {
// window
this.element = $("<div>")
.addClass("fr-window fr-measured")
.hide() // start hidden
.append(
(this._box = $("<div>")
.addClass("fr-box")
.append((this._pages = $("<div>").addClass("fr-pages"))))
)
.append((this._thumbnails = $("<div>").addClass("fr-thumbnails")));
Overlay.initialize();
Pages.initialize(this._pages);
Thumbnails.initialize(this._thumbnails);
Spinner.initialize();
UI.initialize();
// support classes
this.element.addClass(
"fr" + (!Support.mobileTouch ? "-no" : "") + "-mobile-touch"
);
this.element.addClass("fr" + (!Support.svg ? "-no" : "") + "-svg");
if (Browser.IE) {
for (var i = 7; i <= 9; i++) {
if (Browser.IE < i) {
this.element.addClass("fr-ltIE" + i);
}
}
}
// prevent mousewheel scroll
this.element.on("fresco:mousewheel", function(event) {
event.preventDefault();
});
},
attach: function() {
if (this._attached) return;
$(document.body).append(this.element);
this._attached = true;
},
detach: function() {
if (!this._attached) return;
this.element.detach();
this._attached = false;
},
setSkin: function(skin) {
if (this._skin) {
this.element.removeClass("fr-window-skin-" + this._skin);
}
this.element.addClass("fr-window-skin-" + skin);
Overlay.setSkin(skin);
this._skin = skin;
},
setShowingType: function(type) {
if (this._showingType === type) return;
if (this._showingType) {
this.element.removeClass("fr-showing-type-" + this._showingType);
if (Type.isVideo(this._showingType)) {
this.element.removeClass("fr-showing-type-video");
}
}
this.element.addClass("fr-showing-type-" + type);
if (Type.isVideo(type)) {
this.element.addClass("fr-showing-type-video");
}
this._showingType = type;
},
// Resize
startObservingResize: function() {
if (this._onWindowResizeHandler) return;
$(window).on(
"resize orientationchange",
(this._onWindowResizeHandler = this._onWindowResize.bind(this))
);
},
stopObservingResize: function() {
if (this._onWindowResizeHandler) {
$(window).off("resize orientationchange", this._onWindowResizeHandler);
this._onWindowResizeHandler = null;
}
},
_onScroll: function() {
if (!Support.mobileTouch) return;
// the timeout is a hack for iOS not responding
this.timers.set("scroll", this.adjustToScroll.bind(this), 0);
},
_onWindowResize: function() {
var page;
if (!(page = Pages.page)) return;
Thumbnails.fitToViewport();
this.updateBoxDimensions();
page.fitToBox();
// update the UI to the current size
UI.update();
// instantly update previous/next
UI.adjustPrevNext(null, 0);
// reposition spinner
Spinner.center();
Overlay.max(); // IE7-8
// show UI for touch on resize
UI._onWindowResize();
this._onScroll();
},
adjustToScroll: function() {
if (!Support.mobileTouch) return;
this.element.css({
top: $(window).scrollTop(),
});
},
getBoxDimensions: function() {
return this._boxDimensions;
},
updateBoxDimensions: function() {
var page;
if (!(page = Pages.page)) return;
var viewport = Bounds.viewport(),
thumbnails = Thumbnails.getDimensions();
var isHorizontal = Thumbnails._orientation === "horizontal";
this._boxDimensions = {
width: isHorizontal ? viewport.width : viewport.width - thumbnails.width,
height: isHorizontal
? viewport.height - thumbnails.height
: viewport.height,
};
// resize
this._boxPosition = {
top: 0,
left: isHorizontal ? 0 : thumbnails.width,
};
this._box.css($.extend({}, this._boxDimensions, this._boxPosition));
},
show: function(callback, alternateDuration) {
if (this.visible) {
if (callback) callback();
return;
}
this.visible = true;
this.opening = true;
this.attach();
// clear timers that possible break toggling between show/hide()
this.timers.clear("show-window");
this.timers.clear("hide-overlay");
// position the window at the top if mobile touch
this.adjustToScroll();
var duration =
(typeof alternateDuration === "number"
? alternateDuration
: Pages.page && Pages.page.view.options.effects.window.show) || 0;
var fx = 2;
// overlay
Overlay[Pages.page && Pages.page.view.options.overlay ? "show" : "hide"](
function() {
if (callback && --fx < 1) callback();
},
duration
);
// window
// using a timeout here removes a sharp visible edge of the window while fading in
// because the fading happens on top of a solid area
this.timers.set(
"show-window",
function() {
this._show(
function() {
this.opening = false;
if (callback && --fx < 1) callback();
}.bind(this),
duration
);
}.bind(this),
duration > 1 ? Math.min(duration * 0.5, 50) : 1
);
},
_show: function(callback, alternateDuration) {
var duration =
(typeof alternateDuration === "number"
? alternateDuration
: Pages.page && Pages.page.view.options.effects.window.show) || 0;
this.element.stop(true).fadeTo(duration, 1, callback);
},
hide: function(callback) {
if (!this.view) return;
var hideQueue = this.queues.hide;
hideQueue.queue([]); // clear queue
// clear timers that possible break toggling between show/hide()
this.timers.clear("show-window");
this.timers.clear("hide-overlay");
var duration = Pages.page ? Pages.page.view.options.effects.window.hide : 0;
hideQueue.queue(
function(next_stop) {
Pages.stop();
// hide the spinner here so its effect ends early enough
Spinner.hide();
next_stop();
}.bind(this)
);
hideQueue.queue(
function(next_unbinds) {
// ui
UI.disable();
UI.hide(null, duration);
// keyboard
Keyboard.disable();
next_unbinds();
}.bind(this)
);
hideQueue.queue(
function(next_hidden) {
var fx = 2;
this._hide(function() {
if (--fx < 1) next_hidden();
}, duration);
// using a timeout here removes a sharp visible edge of the window while fading out
this.timers.set(
"hide-overlay",
function() {
Overlay.hide(function() {
if (--fx < 1) next_hidden();
}, duration);
}.bind(this),
duration > 1 ? Math.min(duration * 0.5, 150) : 1
);
// after we initiate hide, the next show() should bring up the UI
// we to this using a flag
this._first = 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._reset();
// all of the below we cannot safely call safely
this.stopObservingResize();
//this.stopObservingScroll();
Pages.removeAll();
Thumbnails.clear();
this.timers.clear();
this._position = -1;
// afterHide callback
var afterHide = Pages.page && Pages.page.view.options.afterHide;
if (typeof afterHide === "function") {
afterHide.call(Fresco);
}
this.view = null;
this.opening = false;
this.closing = false;
// remove from DOM
this.detach();
next_after_resize();
}.bind(this)
);
if (typeof callback === "function") {
hideQueue.queue(
function(next_callback) {
callback();
next_callback();
}.bind(this)
);
}
},
_hide: function(callback, alternateDuration) {
var duration =
(typeof alternateDuration === "number"
? alternateDuration
: Pages.page && Pages.page.view.options.effects.window.hide) || 0;
this.element.stop(true).fadeOut(duration, callback);
},
// Load
load: function(views, position) {
this.views = views;
// dimension and visibility based code needs
// the window attached
this.attach();
Thumbnails.load(views);
Pages.load(views);
this.startObservingResize();
//this.startObservingScroll();
if (position) {
this.setPosition(position);
}
},
// loading indicator
/*
startLoading: function() {
if (!Spinner.supported) return;
Spinner.show();
Spinner.center();
},
stopLoading: function() {
if (!Spinner.supported) return;
// we only stop loading if there are no loading pages anymore
var loadingCount = Pages.getLoadingCount();
if (loadingCount < 1) {
Spinner.hide();
}
},*/
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() {
if (callback) 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([]);
},
_reset: function() {
this.visible = false;
UI.hide(null, 0);
UI.reset();
},
// 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,
};
},
};
// 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) 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) 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;
},
};
var Page = (function() {
var _uid = 0,
_loadedUrlCache = {},
// a group of elements defining the 1px stroke, cloned later on
_strokes = $("<div>")
.addClass("fr-stroke fr-stroke-top fr-stroke-horizontal")
.append($("<div>").addClass("fr-stroke-color"))
.add(
$("<div>")
.addClass("fr-stroke fr-stroke-bottom fr-stroke-horizontal")
.append($("<div>").addClass("fr-stroke-color"))
)
.add(
$("<div>")
.addClass("fr-stroke fr-stroke-left fr-stroke-vertical")
.append($("<div>").addClass("fr-stroke-color"))
)
.add(
$("<div>")
.addClass("fr-stroke fr-stroke-right fr-stroke-vertical")
.append($("<div>").addClass("fr-stroke-color"))
);
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._fullClick = 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("fr-page")
.append((this.container = $("<div>").addClass("fr-container")))
.css({ opacity: 0 })
.hide())
);
// check if we have a position
var hasPosition = this.view.options.position && this._total > 1;
if (hasPosition) {
// mark it if so
this.element.addClass("fr-has-position");
}
// info (caption/position)
if (this.view.caption || hasPosition) {
this.element.append(
(this.info = $("<div>")
.addClass("fr-info")
.append($("<div>").addClass("fr-info-background"))
.append(_strokes.clone(true))
.append((this.infoPadder = $("<div>").addClass("fr-info-padder"))))
);
// insert position first because it floats right
if (hasPosition) {
this.element.addClass("fr-has-position");
this.infoPadder.append(
(this.pos = $("<div>")
.addClass("fr-position")
.append(
$("<span>")
.addClass("fr-position-text")
.html(this._position + " / " + this._total)
))
);
}
if (this.view.caption) {
this.infoPadder.append(
(this.caption = $("<div>")
.addClass("fr-caption")
.html(this.view.caption))
);
}
}
// background
this.container
.append(
(this.background = $("<div>").addClass("fr-content-background"))
)
.append((this.content = $("<div>").addClass("fr-content")));
// append images instantly
if (this.view.type == "image") {
this.content.append(
(this.image = $("<img>")
.addClass("fr-content-element")
.attr({ src: this.view.url }))
);
this.content.append(_strokes.clone(true));
}
// ui:outside needs a position outside of the info bar
if (hasPosition && this.view.options.ui == "outside") {
this.container.append(
(this.positionOutside = $("<div>")
.addClass("fr-position-outside")
.append($("<div>").addClass("fr-position-background"))
.append(
$("<span>")
.addClass("fr-position-text")
.html(this._position + " / " + this._total)
))
);
}
// ui:inside has everything inside the content
if (this.view.options.ui == "inside") {
// buttons
this.content
// < previous
.append(
(this.previousInside = $("<div>")
.addClass("fr-side fr-side-previous fr-toggle-ui")
.append(
$("<div>")
.addClass("fr-side-button")
.append($("<div>").addClass("fr-side-button-background"))
.append($("<div>").addClass("fr-side-button-icon"))
))
)
// > next
.append(
(this.nextInside = $("<div>")
.addClass("fr-side fr-side-next fr-toggle-ui")
.append(
$("<div>")
.addClass("fr-side-button")
.append($("<div>").addClass("fr-side-button-background"))
.append($("<div>").addClass("fr-side-button-icon"))
))
)
// X close
.append(
(this.closeInside = $("<div>")
.addClass("fr-close fr-toggle-ui")
.append($("<div>").addClass("fr-close-background"))
.append($("<div>").addClass("fr-close-icon")))
);
// info (only inserted when there is a caption)
// if there is no caption we insert a separate position element below
// but if 1 item in the group has a caption we insert the info bar
if (this.view.caption || (hasPosition && this.view.grouped.caption)) {
this.content.append(
(this.infoInside = $("<div>")
.addClass("fr-info fr-toggle-ui")
.append($("<div>").addClass("fr-info-background"))
.append(_strokes.clone(true))
.append(
(this.infoPadderInside = $("<div>").addClass("fr-info-padder"))
))
);
// insert position first because it floats right
if (hasPosition) {
this.infoPadderInside.append(
(this.posInside = $("<div>")
.addClass("fr-position")
.append(
$("<span>")
.addClass("fr-position-text")
.html(this._position + " / " + this._total)
))
);
}
if (this.view.caption) {
this.infoPadderInside.append(
(this.captionInside = $("<div>")
.addClass("fr-caption")
.html(this.view.caption))
);
}
}
// insert a separate position for when there's no caption
// avoid adding it when the group has at least one caption,
// the info bar is shown then
if (!this.view.caption && hasPosition && !this.view.grouped.caption) {
this.content.append(
(this.positionInside = $("<div>")
.addClass("fr-position-inside fr-toggle-ui")
.append($("<div>").addClass("fr-position-background"))
.append(
$("<span>")
.addClass("fr-position-text")
.html(this._position + " / " + this._total)
))
);
}
// disabled states on buttons
var mayPrevious =
(this.view.options.loop && this._total > 1) || this._position != 1,
mayNext =
(this.view.options.loop && this._total > 1) ||
this._position < this._total;
this.previousInside[(mayPrevious ? "remove" : "add") + "Class"](
"fr-side-disabled"
);
this.nextInside[(mayNext ? "remove" : "add") + "Class"](
"fr-side-disabled"
);
}
// overlap (this affects padding)
$.each(
["x", "y"],
function(i, z) {
if (this.view.options.overflow[z]) {
this.element.addClass("fr-overflow-" + z);
}
}.bind(this)
);
// add the type
this.element.addClass("fr-type-" + this.view.type);
// add type-video
if (Type.isVideo(this.view.type)) {
this.element.addClass("fr-type-video");
}
// no sides
if (this._total < 2) {
this.element.addClass("fr-no-sides");
}
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[i - 1];
if (page._position != pos) pages.push(page);
}
for (var i = pos; i >= begin; i--) {
var page = Pages.pages[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;
this.preloadReady = new ImageReady(
this.image[0],
function(imageReady) {
// mark this page as loaded, without hiding the spinner
this.loaded = true;
_loadedUrlCache[this.view.url] = true;
this.preloading = false;
this.preloaded = true;
this.dimensions = {
width: imageReady.img.naturalWidth,
height: imageReady.img.naturalHeight,
};
}.bind(this),
null,
{
// have the preload always use naturalWidth,
// this avoid an extra new Image() request
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();
// mark as loading
this.loading = true;
// start the spinner after waiting for some duration
if (this.view.options.spinner) {
// && !_loadedUrlCache[this.view.url]
this._spinnerDelay = setTimeout(
function() {
Spinner.show();
}.bind(this),
this.view.options.spinnerDelay || 0
);
}
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.image[0],
function(imageReady) {
// mark as loaded
this._markAsLoaded();
this.setDimensions({
width: imageReady.img.naturalWidth,
height: imageReady.img.naturalHeight,
});
if (callback) callback();
}.bind(this),
function() {
// mark as loaded
this._markAsLoaded();
this.image.hide();
this.content.prepend(
(this.error = $("<div>")
.addClass("fr-error fr-content-element")
.append($("<div>").addClass("fr-error-icon")))
);
this.element.addClass("fr-has-error");
this.setDimensions({
width: this.error.outerWidth(),
height: this.error.outerHeight(),
});
// allow resizing
this.error.css({ width: "100%", height: "100%" });
if (callback) callback();
}.bind(this),
{
method: this.view.options.loadedMethod,
}
);
break;
case "vimeo":
this.vimeoReady = new VimeoReady(
this.view.url,
function(data) {
// mark as loaded
this._markAsLoaded();
this.setDimensions({
width: data.dimensions.width,
height: data.dimensions.height,
});
if (callback) callback();
}.bind(this)
);
break;
case "youtube":
// mark as loaded
this._markAsLoaded();
this.setDimensions({
width: this.view.options.width,
height: this.view.options.height,
});
if (callback) callback();
break;
}
},
// sets dimensions taking maxWidth/Height into account
setDimensions: function(dimensions) {
this.dimensions = dimensions;
if (this.view.options.maxWidth || this.view.options.maxHeight) {
var opts = this.view.options,
bounds = {
width: opts.maxWidth ? opts.maxWidth : this.dimensions.width,
height: opts.maxHeight ? opts.maxHeight : this.dimensions.height,
};
this.dimensions = Fit.within(bounds, this.dimensions);
}
},
// helper for load()
_markAsLoaded: function() {
this._abortSpinnerDelay();
this.loading = false;
this.loaded = true;
// mark url as loaded so we can avoid
// showing the spinner again
_loadedUrlCache[this.view.url] = true;
Spinner.hide(null, null, this._position);
},
isVideo: function() {
return Type.isVideo(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" : "") +
":";
var 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.prepend(
(this.playerIframe = $(
"<iframe webkitAllowFullScreen mozallowfullscreen allowFullScreen>"
)
.addClass("fr-content-element")
.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
shq.queue(
function(next_stopped_inactive) {
// hide the spinner only if it's visible, and when this page doesn't need loading
var needsLoading =
this.view.options.spinner && !_loadedUrlCache[this.view.url];
if (Spinner._visible && !needsLoading) {
Spinner.hide();
}
Pages.stopInactive();
next_stopped_inactive();
}.bind(this)
);
// update the mode is something we can do instantly
shq.queue(
function(next_updated_UI) {
this.updateUI(); // first this page
UI.set(this._ui); // then the window to match
next_updated_UI();
}.bind(this)
);
// keyboard, enabled here so escape can be pressed on load
shq.queue(
function(next_keyboard) {
Keyboard.enable(this.view.options.keyboard);
next_keyboard();
}.bind(this)
);
// load
shq.queue(
function(next_loaded) {
// skin spinner
Spinner.setSkin(this.view.options.skin);
// 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);
UI.enable(); // enable ui controls
this.fitToBox();
// adjust to scroll
Window.adjustToScroll();
next_utility();
}.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)
);
}
// if we're not syncing, hide other visible pages before this one
if (!this.view.options.sync) {
shq.queue(
function(next_synced) {
Pages.hideInactive(next_synced);
}.bind(this)
);
}
shq.queue(
function(next_shown) {
var fx = 3,
duration = this.view.options.effects.content.show;
// set type on the window
Window.setShowingType(this.view.type);
// if this is the first thing we open, we should math the duration
// of the page opening with that of the window
if (!Window.visible) {
duration = this.view.options.effects.window.show;
if (typeof this.view.options.onShow === "function") {
this.view.options.onShow.call(Fresco);
}
}
// when syncing, hide other pages while showing this one
if (this.view.options.sync) {
fx++;
Pages.hideInactive(function() {
if (--fx < 1) next_shown();
});
}
Window.show(function() {
if (--fx < 1) next_shown();
}, this.view.options.effects.window.show);
this._show(function() {
if (--fx < 1) next_shown();
}, duration);
UI.adjustPrevNext(
function() {
if (--fx < 1) next_shown();
},
Window._first ? 0 : duration
);
if (Window._first) {
UI.show(null, 0);
// don't show the UI the next time, it'll show up
// when we set this flag again
Window._first = false;
} else {
//if (this._ui == 'inside')
UI.show(null, 0);
}
// call afterPosition right after starting the _show() but within
// this queue step so triggers before the animation completes
var afterPosition = this.view.options.afterPosition;
if (typeof afterPosition === "function") {
afterPosition.call(Fresco, this._position);
}
}.bind(this)
);
shq.queue(
function(next_set_visible) {
this._visible = true;
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.content.show;
this.element
.stop(true)
.show()
.fadeTo(duration || 0, 1, callback);
},
hide: function(callback, alternateDuration) {
if (!this.element) {
if (callback) callback();
return; // nothing to hide yet
}
this.removeVideo();
// abort possible loading
this.abort();
var duration =
typeof alternateDuration === "number"
? alternateDuration
: this.view.options.effects.content.hide;
// hide video instantly
if (this.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,
"frescoEaseInCubic",
function() {
this.element.hide();
this._visible = false;
Pages.removeTracking(this._position);
if (callback) callback();
}.bind(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();
if (this._track) {
Pages.removeTracking(this._position);
this._track = false;
}
// only cancel preloading on remove
if (this.preloadReady) {
this.preloadReady.abort();
this.preloadReady = null;
this.preloading = null;
this.preloaded = null;
}
this._visible = false;
this.removed = true;
},
abort: function() {
// we don't stop preloading when aborting
if (this.imageReady) {
this.imageReady.abort();
this.imageReady = null;
}
if (this.vimeoReady) {
this.vimeoReady.abort();
this.vimeoReady = null;
}
this._abortSpinnerDelay();
this.loading = false;
//Window.stopLoading();
},
_abortSpinnerDelay: function() {
if (this._spinnerDelay) {
clearTimeout(this._spinnerDelay);
this._spinnerDelay = null;
}
},
_getInfoHeight: function(width) {
// height is always 0 when no caption
//if (!this.view.caption) return 0;
// not measuring height? should this be based on visibility?
var hasPosition = this.view.options.position && this._total > 1;
switch (this._ui) {
case "fullclick":
case "inside":
if (!(this.view.caption || hasPosition)) return 0;
break;
case "outside":
if (!this.view.caption) return 0;
break;
}
var info = this._ui === "inside" ? this.infoInside : this.info;
// make sure width fits within the bounds if we use the outside ui
if (this._ui === "outside") {
width = Math.min(width, Window._boxDimensions.width);
}
// css('width') return an incorrected parsed value, we need the one on the style attribute
var height,
oldWidth = info[0].style.width; //.css('width');
// always use 100% restore width for inside/fullclick
if (this._ui === "inside" || this._ui === "fullclick") oldWidth = "100%";
info.css({ width: width + "px" });
height = parseFloat(info.outerHeight());
info.css({ width: oldWidth });
return height;
},
_whileVisible: function(fn, extraElements) {
var shown = [],
showElements = Window.element.add(this.element);
if (extraElements) {
showElements = showElements.add(extraElements);
}
// show hidden elements and track which elements got shown
$.each(showElements, function(i, element) {
var visible = $(element).is(":visible");
if (!visible) {
shown.push($(element).show());
}
});
// need to measure assuming we have a caption, otherwise info.caption will flip between true/false
// because this class could have been applied at some point
// having a caption also means these classes are set a certain way, restore them later
var has_nocap = this.element.hasClass("fr-no-caption");
this.element.removeClass("fr-no-caption");
var has_hascap = this.element.hasClass("fr-has-caption");
this.element.addClass("fr-has-caption");
// prevent fragments
Window.element.css({ visibility: "hidden" });
fn(); // run some code
// restore visibility
Window.element.css({ visibility: "visible" });
// restore classes
if (has_nocap) this.element.addClass("fr-no-caption");
if (!has_hascap) this.element.removeClass("fr-has-caption");
$.each(shown, function(i, element) {
element.hide();
});
},
// this keeps tracks of flags set in CSS to force certain options
// like fullclick or no-overlap
updateForced: function() {
this.create(); // make sure the element is created
this._fullClick = this.view.options.fullClick;
this._noOverflow = false;
if (parseInt(this.element.css("min-width")) > 0) this._fullClick = true;
if (parseInt(this.element.css("min-height")) > 0) this._noOverflow = true;
},
updateUI: function() {
// check if we have forced properties
this.updateForced();
var ui = this._fullClick ? "fullclick" : this.view.options.ui;
if (this._ui) this.element.removeClass("fr-ui-" + this._ui);
this.element.addClass("fr-ui-" + ui);
this._ui = ui;
},
fitToBox: function() {
// if we have no content, no need to update further
if (!this.content) return;
var page = this.element,
bounds = $.extend({}, Window.getBoxDimensions()),
dimensions = $.extend({}, this.dimensions),
container = this.container;
// need to know which mode we are in before continuing
this.updateUI();
var padding = {
left: parseInt(container.css("padding-left")),
top: parseInt(container.css("padding-top")),
};
// if the ui is outside and we're showing a position, it might be larger then left padding
// so increase it if that's the case
if (this._ui === "outside" && this._positionOutside) {
var positionWidth = 0;
this._whileVisible(
function() {
if (this._positionOutside.is(":visible")) {
positionWidth = this._positionOutside.outerWidth(true);
}
}.bind(this)
);
if (positionWidth > padding.left) {
padding.left = positionWidth;
}
}
bounds.width -= 2 * padding.left;
bounds.height -= 2 * padding.top;
var fitOptions = {
width: true,
height: this._noOverflow ? true : !this.view.options.overflow.y,
};
var fitted = Fit.within(bounds, dimensions, fitOptions),
contentDimensions = $.extend({}, fitted),
content = this.content,
infoHeight = 0,
backgroundDimensions;
var isInside = this._ui === "inside",
info = isInside ? this.infoInside : this.info,
caption = isInside ? this.captionInside : this.caption,
pos = isInside ? this.posInside : this.pos;
var infoCaption = !!caption;
var extraShowElements;
// set the max-height on the info window so the height calculation takes it into account
//this.setInfoMaxHeight();
switch (this._ui) {
case "outside":
var preScale = $.extend({}, contentDimensions);
if (this.caption) {
extraShowElements = this.caption;
this._whileVisible(
function() {
var count = 0,
attempts = 2;
while (count < attempts) {
infoHeight = this._getInfoHeight(contentDimensions.width);
var spaceBottom = bounds.height - contentDimensions.height;
if (spaceBottom < infoHeight) {
contentDimensions = Fit.within(
{
width: contentDimensions.width,
height: Math.max(
contentDimensions.height - (infoHeight - spaceBottom),
0
), // prevents NaN
},
contentDimensions,
fitOptions
);
}
count++;
}
// grab info_height one final time
infoHeight = this._getInfoHeight(contentDimensions.width);
var infoShowLimit = 0.5; //this.view.options.infoShowLimit;
if (
// too much overflow after resizing the info box to the content area
(!this.view.options.overflow.y &&
infoHeight + contentDimensions.height > bounds.height) ||
// caption disabled through css
(info && info.css("display") === "none") ||
// info height is >= 50% of image height
(infoShowLimit &&
infoHeight >= infoShowLimit * contentDimensions.height)
) {
// info is almost a square compared to the image width
infoCaption = false;
infoHeight = 0;
contentDimensions = preScale;
}
}.bind(this),
extraShowElements
);
} // end caption
if (info) {
info.css({
width: contentDimensions.width + "px",
});
}
backgroundDimensions = {
width: contentDimensions.width,
height: contentDimensions.height + infoHeight,
};
break;
case "inside":
if (this.caption) {
// store elements
extraShowElements = caption;
this._whileVisible(
function() {
infoHeight = this._getInfoHeight(contentDimensions.width);
var infoShowLimit = 0.45;
// hide caption if it is too large
if (
infoShowLimit &&
infoHeight >= infoShowLimit * contentDimensions.height
) {
infoCaption = false;
infoHeight = 0;
}
}.bind(this),
extraShowElements
);
}
backgroundDimensions = contentDimensions;
break;
case "fullclick":
var extraElements = [];
if (caption) {
extraElements.push(caption);
}
this._whileVisible(
function() {
// make the caption 100% width
if (caption || pos) {
info.css({ width: "100%" });
}
infoHeight = this._getInfoHeight(Window._boxDimensions.width);
if (caption) {
if (infoHeight > bounds.height * 0.5) {
infoCaption = false;
if (pos) {
var c_disp = this.caption.is(":visible");
this.caption.hide();
infoHeight = this._getInfoHeight(
Window._boxDimensions.width
);
if (c_disp) this.caption.show();
} else {
infoHeight = 0;
}
}
}
contentDimensions = Fit.within(
{
width: bounds.width,
height: Math.max(0, bounds.height - infoHeight),
},
contentDimensions,
fitOptions
);
backgroundDimensions = contentDimensions;
}.bind(this),
extraElements
);
// always show the caption if we have a position
// at some point to fix something, look into this.
/*if (pos) {
infoCaption = true;
}*/
// remove possible padding on content added by ui:outside
this.content.css({ "padding-bottom": 0 });
break;
}
// show/hide caption and mark its visibility with a class
if (caption) {
caption[infoCaption ? "show" : "hide"]();
}
this.element[(!infoCaption ? "add" : "remove") + "Class"](
"fr-no-caption"
);
this.element[(!infoCaption ? "remove" : "add") + "Class"](
"fr-has-caption"
);
this.content.css(contentDimensions);
this.background.css(backgroundDimensions);
// on iframes we force dimensions
if (this.playerIframe) {
this.playerIframe.attr(contentDimensions);
}
// Overlap, and tracking start/stop based on dimensions
this.overlap = {
y:
backgroundDimensions.height +
(this._ui === "fullclick" ? infoHeight : 0) -
Window._boxDimensions.height,
x: 0,
};
this._track =
!this._noOverflow && this.view.options.overflow.y && this.overlap.y > 0;
// store some values for later use in .position()
this._infoHeight = infoHeight;
this._padding = padding;
this._contentDimensions = contentDimensions;
this._backgroundDimensions = backgroundDimensions;
Pages[(this._track ? "set" : "remove") + "Tracking"](this._position);
this.position();
},
position: function() {
// exit early if we have nothing to position
if (!this.content) return;
var contentDimensions = this._contentDimensions,
backgroundDimensions = this._backgroundDimensions;
// figure out top/left of the content
var contentPosition = {
top:
Window._boxDimensions.height * 0.5 -
backgroundDimensions.height * 0.5,
left:
Window._boxDimensions.width * 0.5 - backgroundDimensions.width * 0.5,
};
var infoPosition = {
top: contentPosition.top + contentDimensions.height,
left: contentPosition.left,
};
var containerBottom = 0;
var info = this._ui === "inside" ? this.infoInside : this.info;
switch (this._ui) {
case "fullclick":
contentPosition.top =
(Window._boxDimensions.height - this._infoHeight) * 0.5 -
backgroundDimensions.height * 0.5;
infoPosition = {
top: Window._boxDimensions.height - this._infoHeight,
left: 0,
bottom: "auto",
};
containerBottom = this._infoHeight;
break;
case "inside":
// we have to do some resets when switching between inside and fullclick
infoPosition = {
top: "auto",
left: 0,
bottom: 0,
};
break;
}
// overlap requires further modification
if (this.overlap.y > 0) {
var xyp = Pages.getXYP();
contentPosition.top = 0 - xyp.y * this.overlap.y;
switch (this._ui) {
case "outside":
case "fullclick":
infoPosition.top = Window._boxDimensions.height - this._infoHeight;
break;
case "inside":
var bottom =
contentPosition.top +
contentDimensions.height -
Window._boxDimensions.height;
var top = -1 * contentPosition.top;
// info
infoPosition.bottom = bottom;
// close X
this.closeInside.css({
top: top,
});
if (this._total > 1) {
// < >
var windowVisible = Window.element.is(":visible");
if (!windowVisible) Window.element.show();
var pRestoreStyle = this.previousInside.attr("style");
this.previousInside.removeAttr("style");
var pnMarginTop = parseInt(this.previousInside.css("margin-top")); // the original margin top
this.previousInside.attr({ style: pRestoreStyle });
if (!windowVisible) Window.element.hide();
var buttons = this.previousInside.add(this.nextInside),
center = this.overlap.y * 0.5;
buttons.css({
"margin-top": pnMarginTop + (top - center),
});
// position inside
if (this.positionInside) {
this.positionInside.css({ bottom: bottom });
}
}
break;
}
} else {
// we have to remove offset from the inner elements possible set by overlap
if (this._ui === "inside") {
this.element
.find(".fr-info, .fr-side, .fr-close, .fr-position-inside")
.removeAttr("style");
}
}
if (info) info.css(infoPosition);
this.container.css({ bottom: containerBottom });
this.content.css(contentPosition);
this.background.css(contentPosition);
},
});
return Page;
})();
var Pages = {
initialize: function(element) {
this.element = element;
this.pages = [];
this.uid = 1;
this._tracking = [];
},
load: function(views) {
this.views = views;
// remove all inactive page groups
this.removeAll();
// add pages for all these views
$.each(
views,
function(i, view) {
this.pages.push(new Page(view, i + 1, this.views.length));
}.bind(this)
);
},
show: function(position, callback) {
var page = this.pages[position - 1];
// never try to reload the exact same frame
if (this.page && this.page.uid === page.uid) {
return;
}
// update the page
this.page = page;
// reposition thumbnails
Thumbnails.show(position);
Window.updateBoxDimensions(); // these are based on Thumbnails, so after thumbnails
page.show(
function() {
if (callback) callback();
}.bind(this)
);
},
// used by the API when opening
// checks if the page is in the currently open group
getPositionInActivePageGroup: function(element) {
var position = 0;
$.each(this.pages, function(i, page) {
if (page.view.element && page.view.element === element) {
position = i + 1;
}
});
return position;
},
getLoadingCount: function() {
// we only stop loading if all the frames we have are not loading anymore
var count = 0;
$.each(this.pages, function(i, page) {
if (page.loading) count++;
});
return count;
},
// Window.hide will call thise when fully closed
removeAll: function() {
$.each(this.pages, function(i, page) {
page.remove();
});
this.pages = [];
},
hideInactive: function(callback, alternateDuration) {
var _pages = [];
$.each(
this.pages,
function(i, page) {
if (page.uid !== this.page.uid) {
_pages.push(page);
}
}.bind(this)
);
var fx = 0 + _pages.length;
if (fx < 1) {
if (callback) callback();
} else {
$.each(_pages, function(i, page) {
page.hide(function() {
if (callback && --fx < 1) callback();
}, alternateDuration);
});
}
return _pages.length;
},
stopInactive: function() {
$.each(
this.pages,
function(i, page) {
if (page.uid !== this.page.uid /* && !page.preloading*/) {
page.stop();
}
}.bind(this)
);
},
stop: function() {
$.each(this.pages, function(i, page) {
page.stop();
});
},
// Tracking
/* Tracking x/y */
handleTracking: function(event) {
if (Browser.IE && Browser.IE < 9) {
this.setXY({ x: event.pageX, y: event.pageY });
this.updatePositions();
} else {
this._tracking_timer = setTimeout(
function() {
this.setXY({ x: event.pageX, y: event.pageY });
this.updatePositions();
}.bind(this),
30
);
}
},
clearTrackingTimer: function() {
if (this._tracking_timer) {
clearTimeout(this._tracking_timer);
this._tracking_timer = null;
}
},
startTracking: function() {
if (Support.mobileTouch || this._handleTracking) return;
// we observe the document so that even when the page is
// still loading and the window isn't in view we can still
// shift x/y positions to get a correct position after load.
$(document.documentElement).on(
"mousemove",
(this._handleTracking = this.handleTracking.bind(this))
);
},
stopTracking: function() {
if (Support.mobileTouch || !this._handleTracking) return;
$(document.documentElement).off("mousemove", this._handleTracking);
this._handleTracking = null;
this.clearTrackingTimer();
},
setTracking: function(position) {
if (!this.isTracking(position)) {
this._tracking.push(this.pages[position - 1]);
if (this._tracking.length === 1) this.startTracking();
}
},
clearTracking: function() {
this._tracking = [];
},
removeTracking: function(position) {
this._tracking = $.grep(this._tracking, function(page) {
return page._position !== position;
});
if (this._tracking.length < 1) this.stopTracking();
},
isTracking: function(position) {
var found = false;
$.each(this._tracking, function(i, page) {
if (page._position === position) {
found = true;
return false;
}
});
return found;
},
// the tracking itself
// Pointer %
setXY: function(xy) {
this._xy = xy;
},
getXYP: function() {
var page = Pages.page;
var dimensions = $.extend({}, Window._boxDimensions);
var xy = $.extend({}, this._xy);
// subtract scroll
xy.y -= $(window).scrollTop();
if (
page &&
(page._ui === "outside" || page._ui === "fullclick") &&
page._infoHeight > 0
) {
dimensions.height -= page._infoHeight;
}
xy.y -= Window._boxPosition.top;
// BUG: in Chrome, events can trigger past the range of the browser
// window causing incorrect offset, when moving onto the developer tools
// for example, haven't found a workaround for this yet.
var xyp = {
//x: Math.min(Math.max(xy.x / dimensions.width, 0), 1),
x: 0,
y: Math.min(Math.max(xy.y / dimensions.height, 0), 1),
};
// safety should be a percentage
var safetyPX = 20,
wh = { x: "width", y: "height" },
safety = {};
$.each(
"y".split(" "),
function(i, z) {
// safety should be a percentage, so convert pixel to %
safety[z] = Math.min(Math.max(safetyPX / dimensions[wh[z]], 0), 1);
// now convert
xyp[z] *= 1 + 2 * safety[z]; // increase the range by 2*%
xyp[z] -= safety[z]; // shift back by %
xyp[z] = Math.min(Math.max(xyp[z], 0), 1); // chop of the sides
}.bind(this)
);
this.setXYP(xyp);
return this._xyp;
},
setXYP: function(xyp) {
this._xyp = xyp;
},
// update all page positions
updatePositions: function() {
if (this._tracking.length < 1) return;
$.each(this._tracking, function(i, page) {
page.position();
});
},
};
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-fresco-caption"),
group: element.attr("data-fresco-group"),
extension: element.attr("data-fresco-extension"),
type: element.attr("data-fresco-type"),
options:
(element.attr("data-fresco-options") &&
eval("({" + element.attr("data-fresco-options") + "})")) ||
{},
};
}
if (object) {
// detect type if none is set
if (!object.extension) {
object.extension = detectExtension(object.url);
}
if (!object.type) {
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;
},
});
// Spinner
// a pure CSS based spinner
var Spinner = {
// mark as supported
supported: Support.css.transform && Support.css.animation,
initialize: function(element) {
this.element = $("<div>")
.addClass("fr-spinner")
.hide();
for (var i = 1; i <= 12; i++) {
this.element.append($("<div>").addClass("fr-spin-" + i));
}
this.element.on(
"click",
function() {
Window.hide();
}.bind(this)
);
// prevent mousewheel scroll
this.element.on("fresco:mousewheel", function(event) {
event.preventDefault();
});
},
setSkin: function(skin) {
if (!this.supported) return;
if (this._skin) {
this.element.removeClass("fr-spinner-skin-" + this._skin);
}
// store dimensions, this avoids having to recalculate it every time we center()
this.updateDimensions();
this.element.addClass("fr-spinner-skin-" + skin);
this._skin = skin;
},
updateDimensions: function() {
// need to be attached to measure dimensions
var attached = this._attached;
if (!attached) this.attach();
this._dimensions = {
width: this.element.outerWidth(),
height: this.element.outerHeight(),
};
if (!attached) this.detach();
},
attach: function() {
if (this._attached) return;
$(document.body).append(this.element);
this._attached = true;
},
detach: function() {
if (!this._attached) return;
this.element.detach();
this._attached = false;
},
show: function(callback, alternateDuration) {
this._visible = true;
this.attach();
this.center();
var pDuration =
(Pages.page && Pages.page.view.options.effects.spinner.show) || 0,
duration =
(typeof alternateDuration === "number"
? alternateDuration
: pDuration) || 0;
this.element.stop(true).fadeTo(duration, 1, callback);
},
hide: function(callback, alternateDuration, position) {
this._visible = false;
var pDuration =
(Pages.page && Pages.page.view.options.effects.spinner.hide) || 0,
duration =
(typeof alternateDuration === "number"
? alternateDuration
: pDuration) || 0;
this.element.stop(true).fadeOut(
duration || 0,
function() {
this.detach();
if (callback) callback();
}.bind(this)
);
},
// center on the current page
center: function() {
if (!this.supported) return;
// make sure dimensions are set
if (!this._dimensions) this.updateDimensions();
// for ui:fullclick we can already figure out the info height
// before the content is loaded and center accordingly
var page = Pages.page,
iH = 0;
if (page && page._ui === "fullclick") {
page._whileVisible(function() {
iH = page._getInfoHeight(Window._boxDimensions.width);
});
}
this.element.css({
top:
Window._boxPosition.top +
Window._boxDimensions.height * 0.5 -
this._dimensions.height * 0.5 -
iH * 0.5,
left:
Window._boxPosition.left +
Window._boxDimensions.width * 0.5 -
this._dimensions.width * 0.5,
});
},
};
// API
// an unexposed object for internal use
var _Fresco = {
_disabled: false,
_fallback: true,
initialize: function() {
Window.initialize();
if (!this._disabled) this.startDelegating();
},
// click delegation
startDelegating: function() {
if (this._delegateHandler) return;
$(document.documentElement)
.on(
"click",
".fresco[href]",
(this._delegateHandler = this.delegate.bind(this))
)
// observe document clicks for XY setting, this makes sure that
// positioning is correct when opening overflow with the API
.on("click", (this._setClickXYHandler = this.setClickXY.bind(this)));
},
stopDelegating: function() {
if (!this._delegateHandler) return;
$(document.documentElement)
.off("click", ".fresco[href]", this._delegateHandler)
.off("click", this._setClickXYHandler);
this._setClickXYHandler = null;
this._delegateHandler = null;
},
setClickXY: function(event) {
Pages.setXY({
x: event.pageX,
y: event.pageY,
});
},
delegate: function(event) {
if (this._disabled) return;
event.stopPropagation();
event.preventDefault();
var element = event.currentTarget;
this.setClickXY(event);
_Fresco.show(element);
},
show: function(object) {
if (this._disabled) {
this.showFallback.apply(_Fresco, _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 = _.isElement(object);
switch ((object_type = typeof object)) {
case "string":
case "object":
var view = new View(object, options),
_dgo = "data-fresco-group-options",
groupOptions = {};
if (view.group) {
// extend the entire group
// if we have an element, look for other elements
if (isElement) {
var elements = $(
'.fresco[data-fresco-group="' +
$(object).attr("data-fresco-group") +
'"]'
);
// find possible group options
elements.filter("[" + _dgo + "]").each(function(i, element) {
$.extend(
groupOptions,
eval("({" + ($(element).attr(_dgo) || "") + "})")
);
});
elements.each(function(i, element) {
// adjust the position if we find that the given object position
if (!position && element === object) position = i + 1;
views.push(
new View(element, $.extend({}, groupOptions, options))
);
});
}
} else {
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;
}
// grouped settings
var groupExtend = { grouped: { caption: false } },
firstUI = views[0].options.ui;
$.each(views, function(i, view) {
// at least one view in the group has a caption
if (view.caption) {
groupExtend.grouped.caption = true;
}
// make sure all items in a group have the same ui
if (i > 0 && view.options.ui !== firstUI) {
view.options.ui = firstUI;
}
});
// put the grouped settings on every view
$.each(views, function(i, view) {
view = $.extend(view, groupExtend);
});
// 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;
// if we've clicked an element, search for it in the currently open pagegroup
var positionInAPG;
if (
isElement &&
(positionInAPG = Pages.getPositionInActivePageGroup(object))
) {
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 (!this._fallback) return;
var url = getUrl(object);
if (url) window.location.href = url;
};
})(),
};
$.extend(Fresco, {
show: function(object) {
_Fresco.show.apply(_Fresco, _slice.call(arguments));
return this;
},
hide: function() {
Window.hide();
return this;
},
disable: function() {
_Fresco.stopDelegating();
_Fresco._disabled = true;
return this;
},
enable: function() {
_Fresco._disabled = false;
_Fresco.startDelegating();
return this;
},
fallback: function(fallback) {
_Fresco._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
_Fresco.show = _Fresco.showFallback;
}
// Thumbnails
var Thumbnails = {
initialize: function(element) {
this.element = element;
this._thumbnails = [];
this._orientation = "vertical";
this._vars = {
thumbnail: {},
thumbnailFrame: {},
thumbnails: {},
};
this.build();
this.startObserving();
},
build: function() {
this.element.append(
(this.wrapper = $("<div>")
.addClass("fr-thumbnails-wrapper")
.append(
(this._slider = $("<div>")
.addClass("fr-thumbnails-slider")
.append(
(this._previous = $("<div>")
.addClass("fr-thumbnails-side fr-thumbnails-side-previous")
.append(
(this._previous_button = $("<div>")
.addClass("fr-thumbnails-side-button")
.append(
$("<div>").addClass(
"fr-thumbnails-side-button-background"
)
)
.append(
$("<div>").addClass("fr-thumbnails-side-button-icon")
))
))
)
.append(
(this._thumbs = $("<div>")
.addClass("fr-thumbnails-thumbs")
.append(
(this._slide = $("<div>").addClass("fr-thumbnails-slide"))
))
)
.append(
(this._next = $("<div>")
.addClass("fr-thumbnails-side fr-thumbnails-side-next")
.append(
(this._next_button = $("<div>")
.addClass("fr-thumbnails-side-button")
.append(
$("<div>").addClass(
"fr-thumbnails-side-button-background"
)
)
.append(
$("<div>").addClass("fr-thumbnails-side-button-icon")
))
))
))
))
);
},
startObserving: function() {
this._slider.delegate(
".fr-thumbnail",
"click",
function(event) {
event.stopPropagation();
var thumbnail = $(event.target).closest(".fr-thumbnail")[0];
var position = thumbnail && $(thumbnail).data("fr-position");
if (position) {
this.setActive(position);
Window.setPosition(position);
}
}.bind(this)
);
// prevent bubbling on slider click, so you can safely click next to a thumbnail
this._slider.bind("click", function(event) {
event.stopPropagation();
});
// previous / next
this._previous.bind("click", this.previousPage.bind(this));
this._next.bind("click", this.nextPage.bind(this));
},
load: function(views) {
// first clear out any previous thumbnails
this.clear();
// set orientation
// it's always horizontal unless 1 view has vertical
var orientation = "horizontal",
disabled = false;
$.each(
views,
function(i, view) {
if (view.options.thumbnails === "vertical") {
orientation = "vertical";
}
if (!view.options.thumbnails) disabled = true;
}.bind(this)
);
this.setOrientation(orientation);
this._disabledGroup = disabled;
$.each(
views,
function(i, view) {
this._thumbnails.push(new Thumbnail(view, i + 1));
}.bind(this)
);
this.fitToViewport();
},
clear: function() {
$.each(this._thumbnails, function(i, thumbnail) {
thumbnail.remove();
});
this._thumbnails = [];
this._position = -1;
this._page = -1;
},
setOrientation: function(orientation) {
if (this._orientation) {
Window.element.removeClass("fr-thumbnails-" + this._orientation);
}
Window.element.addClass("fr-thumbnails-" + orientation);
this._orientation = orientation;
},
// disable / enable
disable: function() {
Window.element
.removeClass("fr-thumbnails-enabled")
.addClass("fr-thumbnails-disabled");
this._disabled = true;
},
enable: function() {
Window.element
.removeClass("fr-thumbnails-disabled")
.addClass("fr-thumbnails-enabled");
this._disabled = false;
},
enabled: function() {
return !this._disabled;
},
disabled: function() {
return this._disabled;
},
// update current dimension variables
updateVars: function() {
var win = Window.element,
vars = this._vars,
orientation = this._orientation,
isHorizontal = orientation === "horizontal",
_top = isHorizontal ? "top" : "left",
_left = isHorizontal ? "left" : "top",
_mbottom = isHorizontal ? "bottom" : "left",
_mtop = isHorizontal ? "top" : "right",
_width = isHorizontal ? "width" : "height",
_height = isHorizontal ? "height" : "width",
_swapZ = { left: "right", right: "left", top: "bottom", bottom: "top" };
this.element.removeClass("fr-thumbnails-measured");
var w_vis = win.is(":visible");
if (!w_vis) win.show();
// we have to observe visibility without having the disabled class on the window
if (this.disabled()) this.enable();
// exit early if we're not showing thumbnails
if (
!this.element.is(":visible") ||
this._thumbnails.length < 2 ||
this._disabledGroup
) {
this.disable();
// store some variables so getDimensions at least has these
$.extend(this._vars.thumbnails, { width: 0, height: 0 });
// show the window again
if (!w_vis) win.hide();
this.element.addClass("fr-thumbnails-measured");
// exit early
return;
} else {
// otherwise enable and continue filling variables
this.enable();
}
var previous = this._previous,
next = this._next,
viewport = Bounds.viewport();
// NOTE: All dimensions are stored as if the orientation is horizontal.
// base the height of the thumbnail on the height of the element, minus padding
var height = this.element["inner" + _.String.capitalize(_height)](),
paddingTop = parseInt(this._thumbs.css("padding-" + _top)) || 0,
thumbnailHeight = Math.max(height - paddingTop * 2, 0),
paddingLeft = parseInt(this._thumbs.css("padding-" + _left)) || 0,
marginTotal =
(parseInt(this.element.css("margin-" + _mbottom)) || 0) +
(parseInt(this.element.css("margin-" + _mtop)) || 0);
$.extend(vars.thumbnails, {
height: height + marginTotal, // we store as z just to make dimensioning later easier
width: viewport[isHorizontal ? "width" : "height"],
paddingTop: paddingTop,
});
$.extend(vars.thumbnail, {
height: thumbnailHeight,
width: thumbnailHeight,
});
$.extend(vars.thumbnailFrame, {
width: thumbnailHeight + paddingLeft * 2,
height: height,
});
// previous/next
vars.sides = {
previous: {
width: next["inner" + _.String.capitalize(_width)](),
marginLeft: parseInt(previous.css("margin-" + _left)) || 0, // left
marginRight: parseInt(previous.css("margin-" + _swapZ[_left])) || 0, // right
},
next: {
width: next["inner" + _.String.capitalize(_width)](),
marginLeft: parseInt(next.css("margin-" + _left)) || 0, // left
marginRight: parseInt(next.css("margin-" + _swapZ[_left])) || 0, // right
},
};
// how many pages and ipp
// first try to fit all the thu
var viewportWidth = viewport[_width], //$(window).width()
thumbnailOuterWidth = vars.thumbnailFrame.width,
thumbs = this._thumbnails.length;
vars.thumbnails.width = viewportWidth;
vars.sides.enabled = (thumbs * thumbnailOuterWidth) / viewportWidth > 1;
// disable the sides if we have only 1 thumbnail
var thumbsWidth = viewportWidth,
vs = vars.sides,
vsPrevious = vs.previous,
vsNext = vs.next,
sidesWidth =
vsPrevious.marginLeft +
vsPrevious.width +
vsPrevious.marginRight +
vsNext.marginLeft +
vsNext.width +
vsNext.marginRight;
if (vars.sides.enabled) {
thumbsWidth -= sidesWidth;
}
// lower the thumbnail width to a factor of a thumbnail
thumbsWidth =
Math.floor(thumbsWidth / thumbnailOuterWidth) * thumbnailOuterWidth;
var totalThumbsWidth = thumbs * thumbnailOuterWidth;
if (totalThumbsWidth < thumbsWidth) {
thumbsWidth = totalThumbsWidth;
}
// now adust the wrapper to match that size
var wrapperWidth = thumbsWidth + (vars.sides.enabled ? sidesWidth : 0);
// items per page
vars.ipp = Math.round(thumbsWidth / thumbnailOuterWidth); //Math.max(thumbsWidth / tw, 1); // at least one
this._mode = "page";
if (vars.ipp <= 1) {
// recalculate width
thumbsWidth = viewportWidth;
wrapperWidth = viewportWidth;
vars.sides.enabled = false;
this._mode = "center";
}
// now find out the pages
vars.pages = Math.ceil((thumbs * thumbnailOuterWidth) / thumbsWidth);
vars.wrapper = {
width: wrapperWidth + 1, // IE fix
height: height,
};
vars.thumbs = {
width: thumbsWidth,
height: height,
};
vars.slide = {
width: thumbs * thumbnailOuterWidth + 1, // IE fix
height: height,
};
if (!w_vis) win.hide();
this.element.addClass("fr-thumbnails-measured");
},
hide: function() {
this.disable();
this.thumbnails.hide();
this._visible = false;
},
getDimensions: function() {
var isHorizontal = this._orientation === "horizontal";
return {
width: isHorizontal
? this._vars.thumbnails.width
: this._vars.thumbnails.height,
height: isHorizontal
? this._vars.thumbnails.height
: this._vars.thumbnails.width,
};
},
// resize
fitToViewport: function() {
// make sure vars are set so we can use them
this.updateVars();
if (this.disabled()) return;
var vars = $.extend({}, this._vars),
isHorizontal = this._orientation === "horizontal";
// individual thumbnails
$.each(this._thumbnails, function(i, thumbnail) {
thumbnail.resize();
});
// show hide sides
this._previous[vars.sides.enabled ? "show" : "hide"]();
this._next[vars.sides.enabled ? "show" : "hide"]();
this._thumbs.css({
width: vars.thumbs[isHorizontal ? "width" : "height"],
height: vars.thumbs[isHorizontal ? "height" : "width"],
});
this._slide.css({
width: vars.slide[isHorizontal ? "width" : "height"],
height: vars.slide[isHorizontal ? "height" : "width"],
});
var wrapperCSS = {
width: vars.wrapper[isHorizontal ? "width" : "height"],
height: vars.wrapper[isHorizontal ? "height" : "width"],
};
wrapperCSS["margin-" + (isHorizontal ? "left" : "top")] =
Math.round(-0.5 * vars.wrapper.width) + "px";
wrapperCSS["margin-" + (!isHorizontal ? "left" : "top")] = 0;
this.wrapper.css(wrapperCSS);
// move to the correct position instantly
if (this._position) {
this.moveTo(this._position, true);
}
},
moveToPage: function(page) {
if (page < 1 || page > this._vars.pages || page === this._page) return;
var position = this._vars.ipp * (page - 1) + 1;
this.moveTo(position);
},
previousPage: function() {
this.moveToPage(this._page - 1);
},
nextPage: function() {
this.moveToPage(this._page + 1);
},
show: function(position) {
// move instantly when the position wasn't set before
var instant = this._position < 0;
// make sure position is available
if (position < 1) position = 1;
var ic = this._thumbnails.length;
if (position > ic) position = ic;
// set it
this._position = position;
this.setActive(position);
// don't move if we are using page mode and are on this page
if (
this._mode === "page" &&
this._page === Math.ceil(position / this._vars.ipp)
)
return;
this.moveTo(position, instant);
},
moveTo: function(position, instant) {
this.updateVars();
if (this.disabled()) return;
var left,
isHorizontal = this._orientation === "horizontal",
vp_z = Bounds.viewport()[isHorizontal ? "width" : "height"],
vp_center = vp_z * 0.5,
t_width = this._vars.thumbnailFrame.width,
page;
if (this._mode === "page") {
page = Math.ceil(position / this._vars.ipp);
// set the page
this._page = page;
left = -1 * (t_width * (this._page - 1) * this._vars.ipp);
// disabled states on buttons
var disabled = "fr-thumbnails-side-button-disabled";
this._previous_button[(page < 2 ? "add" : "remove") + "Class"](disabled);
this._next_button[
(page >= this._vars.pages ? "add" : "remove") + "Class"
](disabled);
} else {
// center
left = vp_center + -1 * (t_width * (position - 1) + t_width * 0.5);
}
page = Pages.page;
// now move there
var resetCSS = {},
animateCSS = {};
resetCSS[!isHorizontal ? "left" : "top"] = 0; // zero out the other offset in case of switching orientation
animateCSS[isHorizontal ? "left" : "top"] = left + "px";
this._slide
.stop(true)
.css(resetCSS)
.animate(
animateCSS,
instant
? 0
: page
? page.view.options.effects.thumbnails.slide || 0
: 0,
function() {
// load all thumbnails on this page
this.loadCurrentPage();
}.bind(this)
);
},
loadCurrentPage: function() {
var min, max;
// load protection, don't load when there's no position or width on the thumbnail
if (
!this._position ||
!this._vars.thumbnailFrame.width ||
this._thumbnails.length < 1
)
return;
if (this._mode === "page") {
// load the thumbnail on the current page
if (this._page < 1) return;
min = (this._page - 1) * this._vars.ipp + 1;
max = Math.min(min - 1 + this._vars.ipp, this._thumbnails.length);
} else {
// mode is 'center', load the thumbnails within the viewport
var thumbnail_count = Math.ceil(
this._vars.thumbnails.width / this._vars.thumbnailFrame.width
);
min = Math.max(
Math.floor(Math.max(this._position - thumbnail_count * 0.5, 0)),
1
);
max = Math.ceil(Math.min(this._position + thumbnail_count * 0.5));
if (this._thumbnails.length < max) max = this._thumbnails.length;
}
// load the thumbnails
for (var i = min; i <= max; i++) {
this._thumbnails[i - 1].load();
}
},
// only the active class
setActive: function(position) {
this._slide.find(".fr-thumbnail-active").removeClass("fr-thumbnail-active");
var thumbnail = position && this._thumbnails[position - 1];
if (thumbnail) thumbnail.activate();
},
refresh: function() {
if (this._position) this.setPosition(this._position);
},
};
// Thumbnail
function Thumbnail() {
this.initialize.apply(this, _slice.call(arguments));
}
$.extend(Thumbnail.prototype, {
initialize: function(view, position) {
this.view = view;
this._position = position;
this.preBuild();
},
preBuild: function() {
this.thumbnail = $("<div>")
.addClass("fr-thumbnail")
.data("fr-position", this._position);
},
build: function() {
if (this.thumbnailFrame) return;
var options = this.view.options;
Thumbnails._slide.append(
(this.thumbnailFrame = $("<div>")
.addClass("fr-thumbnail-frame")
.append(
this.thumbnail.append(
(this.thumbnailWrapper = $("<div>").addClass(
"fr-thumbnail-wrapper"
))
)
))
);
if (this.view.type === "image") {
this.thumbnail.addClass("fr-load-thumbnail").data("thumbnail", {
view: this.view,
src: options.thumbnail || this.view.url,
});
}
// icon
var icon = options.thumbnail && options.thumbnail.icon;
if (icon) {
this.thumbnail.append(
$("<div>").addClass("fr-thumbnail-icon fr-thumbnail-icon-" + icon)
);
}
// overlay
var overlay;
this.thumbnail.append(
(overlay = $("<div>")
.addClass("fr-thumbnail-overlay")
.append($("<div>").addClass("fr-thumbnail-overlay-background"))
.append(
(this.loading = $("<div>")
.addClass("fr-thumbnail-loading")
.append($("<div>").addClass("fr-thumbnail-loading-background"))
.append(
(this.spinner = $("<div>")
.addClass("fr-thumbnail-spinner")
.hide()
.append($("<div>").addClass("fr-thumbnail-spinner-spin")))
))
)
.append($("<div>").addClass("fr-thumbnail-overlay-border")))
);
this.thumbnail.append($("<div>").addClass("fr-thumbnail-state"));
this.resize();
},
remove: function() {
if (this.thumbnailFrame) {
this.thumbnailFrame.remove();
this.thumbnailFrame = null;
this.image = null;
}
if (this.ready) {
this.ready.abort();
this.ready = null;
}
if (this.vimeoThumbnail) {
this.vimeoThumbnail.abort();
this.vimeoThumbnail = null;
}
this._loading = false;
this._removed = true;
// clean up
this.view = null;
this._clearDelay();
},
load: function() {
if (this._loaded || this._loading || this._removed) return;
if (!this.thumbnailWrapper) this.build();
this._loading = true;
var thumbnail = this.view.options.thumbnail;
var url =
thumbnail && typeof thumbnail === "boolean"
? this.view.url
: thumbnail || this.view.url;
// store this now so we can modify it
this._url = url;
// vimeo needs an extra wrapper with a JSONP request
if (url) {
if (this.view.type === "vimeo") {
if (url === thumbnail) {
this._url = url;
this._load(this._url);
} else {
switch (this.view.type) {
case "vimeo":
this.vimeoThumbnail = new VimeoThumbnail(
this.view.url,
function(url) {
this._url = url;
this._load(url);
}.bind(this),
function() {
this._error();
}.bind(this)
);
break;
}
}
} else {
// youtube
this._load(this._url);
}
}
},
activate: function() {
this.thumbnail.addClass("fr-thumbnail-active");
},
_load: function(url) {
this.thumbnailWrapper.prepend(
(this.image = $("<img>")
.addClass("fr-thumbnail-image")
.attr({ src: url })
.css({ opacity: 0.0001 }))
);
this.fadeInSpinner();
this.ready = new ImageReady(
this.image[0],
function(imageready) {
var img = imageready.img;
// if the thumbnail has been removed before we finish,
// or if the _loading has been cancelled,
// just quit
if (!this.thumbnailFrame || !this._loading) return;
this._loaded = true;
this._loading = false;
// store dimensions, used by resize
this._dimensions = {
width: img.naturalWidth,
height: img.naturalHeight,
};
// set dimensions after having loaded
this.resize();
// fadeout spinner
this.show();
}.bind(this),
function() {
this._error();
}.bind(this),
{
method: this.view.options.loadedMethod,
}
);
},
// error callback
_error: function() {
this._loaded = true;
this._loading = false;
this.thumbnail.addClass("fr-thumbnail-error");
if (this.image) {
this.image.hide();
}
this.thumbnailWrapper.append($("<div>").addClass("fr-thumbnail-image"));
this.show();
},
fadeInSpinner: function() {
if (!Spinner.supported || !this.view.options.spinner) return;
// clear possible delay
this._clearDelay();
var fx = this.view.options.effects.thumbnail;
this._delay = setTimeout(
function() {
this.spinner.stop(true).fadeTo(fx.show || 0, 1);
}.bind(this),
this.view.options.spinnerDelay || 0
);
},
// fades out the loading block
// which could also contain a spinner
show: function() {
// clear possible delay
this._clearDelay();
var fx = this.view.options.effects.thumbnail;
this.loading
.stop(true)
.delay(fx.delay)
.fadeTo(fx.show, 0);
},
_clearDelay: function() {
if (this._delay) {
clearTimeout(this._delay);
this._delay = null;
}
},
// center image based on current dimensions
resize: function() {
if (!this.thumbnailFrame) return;
var isHorizontal = Thumbnails._orientation === "horizontal";
// frame
this.thumbnailFrame.css({
width: Thumbnails._vars.thumbnailFrame[isHorizontal ? "width" : "height"],
height:
Thumbnails._vars.thumbnailFrame[isHorizontal ? "height" : "width"],
});
// position frame
this.thumbnailFrame.css({
top: isHorizontal
? 0
: Thumbnails._vars.thumbnailFrame.width * (this._position - 1),
left: isHorizontal
? Thumbnails._vars.thumbnailFrame.width * (this._position - 1)
: 0,
});
if (!this.thumbnailWrapper) return;
// resize the wrapper
var thumbnail = Thumbnails._vars.thumbnail;
this.thumbnail.css({
width: thumbnail.width,
height: thumbnail.height,
"margin-top": Math.round(-0.5 * thumbnail.height),
"margin-left": Math.round(-0.5 * thumbnail.width),
"margin-bottom": 0,
"margin-right": 0,
});
// if there's no image, don't resize the rest
if (!this._dimensions) return;
var bounds = {
width: thumbnail.width, //this.thumbnail.innerWidth(),
height: thumbnail.height, //this.thumbnail.innerHeight()
};
var maxZ = Math.max(bounds.width, bounds.height);
var dimensions;
var image = $.extend({}, this._dimensions);
if (image.width > bounds.width && image.height > bounds.height) {
dimensions = Fit.within(bounds, image);
// if the dimensions are smaller than bounds, increase them
var scaleX = 1,
scaleY = 1;
if (dimensions.width < bounds.width) {
scaleX = bounds.width / dimensions.width;
}
if (dimensions.height < bounds.height) {
scaleY = bounds.height / dimensions.height;
}
var scale = Math.max(scaleX, scaleY);
if (scale > 1) {
dimensions.width *= scale;
dimensions.height *= scale;
}
$.each("width height".split(" "), function(i, z) {
dimensions[z] = Math.round(dimensions[z]); // .5 for anti-aliasing
});
} else {
dimensions = Fit.within(
this._dimensions,
image.width < bounds.width || image.height < bounds.height
? { width: maxZ, height: maxZ }
: bounds
);
}
var x = Math.round(bounds.width * 0.5 - dimensions.width * 0.5),
y = Math.round(bounds.height * 0.5 - dimensions.height * 0.5);
this.image
.removeAttr("style") // remove the opacity
.css($.extend({}, dimensions, { top: y, left: x }));
},
});
// UI Modes
var UI = {
_modes: ["fullclick", "outside", "inside"],
_ui: false,
_validClickTargetSelector: [
".fr-content-element",
".fr-content",
".fr-content > .fr-stroke",
".fr-content > .fr-stroke .fr-stroke-color",
].join(", "),
initialize: function(element) {
// initialize the 3 different UI types
$.each(
this._modes,
function(i, mode) {
this[mode].initialize();
}.bind(this)
);
// start with hidden ui
Window.element.addClass("fr-ui-inside-hidden fr-ui-fullclick-hidden");
},
// set the ui mode, this only changes the class and with that visibility
set: function(ui) {
if (this._ui) {
Window.element.removeClass("fr-window-ui-" + this._ui);
Overlay.element.removeClass("fr-overlay-ui-" + this._ui);
}
Window.element.addClass("fr-window-ui-" + ui);
Overlay.element.addClass("fr-overlay-ui-" + ui);
// if we've switched ui's disable one and enable the other
if (this._enabled && this._ui && this._ui !== ui) {
this[this._ui].disable();
this[ui].enable();
// also show the UI when switched
UI[ui].show();
// stop timers on all other ui modes instantly
//this.setActiveTimers(ui);
}
this._ui = ui;
},
// show ui for touch onresize
_onWindowResize: function() {
if (Support.mobileTouch) this.show();
},
// enable the currently set ui, disable all others
enable: function() {
$.each(
this._modes,
function(i, mode) {
UI[mode][mode === this._ui ? "enable" : "disable"]();
}.bind(this)
);
this._enabled = true;
},
disable: function() {
$.each(
this._modes,
function(i, mode) {
UI[mode].disable();
}.bind(this)
);
this._enabled = false;
},
adjustPrevNext: function(callback, alternateDuration) {
// only fullclick adjusts sides
UI[this._ui].adjustPrevNext(callback, alternateDuration);
},
show: function(callback, alternateDuration) {
UI[this._ui].show(callback, alternateDuration);
//this.setActiveTimers(this._ui);
},
hide: function(callback, alternateDuration) {
UI[this._ui].hide(callback, alternateDuration);
},
// called on Window.reset
reset: function() {
$.each(
this._modes,
function(i, mode) {
UI[mode].reset();
}.bind(this)
);
},
update: function() {
var page = Pages.page;
if (!page) return;
this.set(page._ui);
},
};
UI.fullclick = {
initialize: function() {
this.build();
this._scrollLeft = -1;
},
build: function() {
Window._box
.append(
(this._previous = $("<div>")
.addClass(
"fr-side fr-side-previous fr-side-previous-fullclick fr-toggle-ui"
)
.append(
$("<div>")
.addClass("fr-side-button")
.append($("<div>").addClass("fr-side-button-background"))
.append($("<div>").addClass("fr-side-button-icon"))
))
)
.append(
(this._next = $("<div>")
.addClass("fr-side fr-side-next fr-side-next-fullclick fr-toggle-ui")
.append(
$("<div>")
.addClass("fr-side-button")
.append($("<div>").addClass("fr-side-button-background"))
.append($("<div>").addClass("fr-side-button-icon"))
))
)
// close
.append(
(this._close = $("<div>")
.addClass("fr-close fr-close-fullclick")
.append($("<div>").addClass("fr-close-background"))
.append($("<div>").addClass("fr-close-icon")))
);
// IE7 has a bug that causes multiple UI buttons from showing up
// starting hidden fixes it
if (Browser.IE && Browser.IE <= 7) {
this._previous
.add(this._next)
.add(this._close)
.hide();
}
// events
this._close.on(
"click",
function(event) {
event.preventDefault();
Window.hide();
}.bind(this)
);
this._previous.on(
"click",
function(event) {
Window.previous();
this._onMouseMove(event); // update cursor
}.bind(this)
);
this._next.on(
"click",
function(event) {
Window.next();
this._onMouseMove(event); // update cursor
}.bind(this)
);
},
enable: function() {
this.bind();
},
disable: function() {
this.unbind();
},
reset: function() {
Window.timers.clear("ui-fullclick");
// clear cached mousemove
this._x = -1;
this._y = -1;
this._scrollLeft = -1;
this.resetPrevNext();
// reset the classes by faking a mouseleave
this._onMouseLeave();
},
resetPrevNext: function() {
var buttons = this._previous.add(this._next);
buttons.stop(true).removeAttr("style");
},
// events
bind: function() {
// no need to bind twice
if (this._onMouseUpHandler) return;
this.unbind();
// clicks
Window._pages.on(
"mouseup",
".fr-container",
(this._onMouseUpHandler = this._onMouseUp.bind(this))
);
// track <> only on desktop
if (!Support.mobileTouch) {
Window.element
.on("mouseenter", (this._showHandler = this.show.bind(this)))
.on("mouseleave", (this._hideHandler = this.hide.bind(this)));
Window.element.on(
"mousemove",
(this._mousemoveHandler = 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._hoveringSideButton || (y === this._y && x === this._x)) {
return;
}
// cache x/y
this._x = x;
this._y = y;
this.show();
this.startTimer();
}.bind(this))
);
// delegate <> mousemove/click states
Window._pages
.on(
"mousemove",
".fr-container",
(this._onMouseMoveHandler = this._onMouseMove.bind(this))
)
.on(
"mouseleave",
".fr-container",
(this._onMouseLeaveHandler = this._onMouseLeave.bind(this))
)
.on(
"mouseenter",
".fr-container",
(this._onMouseEnterHandler = this._onMouseEnter.bind(this))
);
// delegate moving onto the <> buttons
// keeping the mouse on them should keep the buttons visible
Window.element
.on(
"mouseenter",
".fr-side",
(this._onSideMouseEnterHandler = this._onSideMouseEnter.bind(this))
)
.on(
"mouseleave",
".fr-side",
(this._onSideMouseLeaveHandler = this._onSideMouseLeave.bind(this))
);
$(window).on(
"scroll",
(this._onScrollHandler = this._onScroll.bind(this))
);
}
},
unbind: function() {
if (!this._onMouseUpHandler) return;
// clicks
Window._pages.off("mouseup", ".fr-container", this._onMouseUpHandler);
this._onMouseUpHandler = null;
if (this._showHandler) {
Window.element
.off("mouseenter", this._showHandler)
.off("mouseleave", this._hideHandler)
.off("mousemove", this._mousemoveHandler);
Window._pages
.off("mousemove", ".fr-container", this._onMouseMoveHandler)
.off("mouseleave", ".fr-container", this._onMouseLeaveHandler)
.off("mouseenter", ".fr-container", this._onMouseEnterHandler);
Window.element
.off("mouseenter", ".fr-side", this._onSideMouseEnterHandler)
.off("mouseleave", ".fr-side", this._onSideMouseLeaveHandler);
$(window).off("scroll", this._onScrollHandler);
this._showHandler = null;
}
},
adjustPrevNext: function(callback, alternateDuration) {
var page = Pages.page;
if (!page) {
if (callback) callback();
return;
}
// offset <>
var windowVisible = Window.element.is(":visible");
if (!windowVisible) Window.element.show();
var pRestoreStyle = 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) Window.element.hide();
// only the fullclick UI changes previous/next position based on info height
var iH = page._infoHeight || 0;
var buttons = this._previous.add(this._next),
css = { "margin-top": pnMarginTop - iH * 0.5 };
var duration =
typeof alternateDuration === "number"
? alternateDuration
: (Pages.page && Pages.page.view.options.effects.content.show) || 0;
// adjust <> instantly when opening
if (this.opening) duration = 0;
buttons.stop(true).animate(css, duration, callback);
// disabled states
this._previous[(Window.mayPrevious() ? "remove" : "add") + "Class"](
"fr-side-disabled"
);
this._next[(Window.mayNext() ? "remove" : "add") + "Class"](
"fr-side-disabled"
);
// hide buttons for single content
buttons[(page._total < 2 ? "add" : "remove") + "Class"]("fr-side-hidden");
if (callback) callback();
},
_onScroll: function() {
this._scrollLeft = $(window).scrollLeft();
},
_onMouseMove: function(event) {
// no need for this on mobile-touch
if (Support.mobileTouch) return;
var side = this._getEventSide(event),
Side = _.String.capitalize(side),
mayClickHoveringSide = side ? Window["may" + Side]() : false;
// prevent doing this on every mousemove
if (
side === this._hoveringSide &&
mayClickHoveringSide === this._mayClickHoveringSide
) {
return;
}
this._hoveringSide = side;
this._mayClickHoveringSide = mayClickHoveringSide;
Window._box[(mayClickHoveringSide ? "add" : "remove") + "Class"](
"fr-hovering-clickable"
);
// previous
switch (side) {
case "previous":
Window._box
.addClass("fr-hovering-previous")
.removeClass("fr-hovering-next");
break;
case "next":
Window._box
.addClass("fr-hovering-next")
.removeClass("fr-hovering-previous");
break;
}
},
_onMouseLeave: function(event) {
Window._box.removeClass(
"fr-hovering-clickable fr-hovering-previous fr-hovering-next"
);
this._hoveringSide = false;
},
// click == mouseup
// We use mouseup instead of click because it is more reliable.
// Multiple clicks on images coming in and out of view can be
// seen as triggering a selection, which doesn't trigger a click
// event, but mouseup always fires.
_onMouseUp: function(event) {
// don't respond to middle/right clicks (2 & 3)
if (event.which > 1) {
return;
}
// close when needed
// NOTE: we only do this for 1 item for fullclick
// not for onClick:'close' since the arrow moves around
// making it possible to accidentally close
if (Pages.pages.length === 1) {
Window.hide();
return;
}
var side = this._getEventSide(event);
Window[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 scrollLeft =
this._scrollLeft > -1
? this._scrollLeft
: (this._scrollLeft = $(window).scrollLeft()),
left = event.pageX - Window._boxPosition.left - this._scrollLeft,
width = Window._boxDimensions.width;
return left < 0.5 * width ? "previous" : "next";
},
_onSideMouseEnter: function(event) {
this._hoveringSideButton = true;
this._hoveringSide = this._getEventSide(event);
this._mayClickHoveringSide = Window[
"may" + _.String.capitalize(this._hoveringSide)
]();
this.clearTimer();
},
_onSideMouseLeave: function(event) {
this._hoveringSideButton = false;
this._hoveringSide = false;
this._mayClickHoveringSide = false;
this.startTimer();
},
show: function(callback) {
if (this._visible) {
// still clear a timer that could possible trigger hide
// and start a new one
this.startTimer();
if (typeof callback === "function") callback();
return;
}
this._visible = true;
this.startTimer();
Window.element
.addClass("fr-visible-fullclick-ui")
.removeClass("fr-hidden-fullclick-ui");
if (Browser.IE && Browser.IE <= 7) {
this._previous
.add(this._next)
.add(this._close)
.show();
}
if (typeof callback === "function") callback();
},
hide: function(callback) {
// never hide the ui for video
var type = Pages.page && Pages.page.view.type;
if (!this._visible || (type && (type === "youtube" || type === "vimeo"))) {
if (typeof callback === "function") callback();
return;
}
this._visible = false;
Window.element
.removeClass("fr-visible-fullclick-ui")
.addClass("fr-hidden-fullclick-ui");
if ($.type(callback) === "function") callback();
},
// UI Timer
// not used on mobile-touch based devices
clearTimer: function() {
if (Support.mobileTouch) return;
Window.timers.clear("ui-fullclick");
},
startTimer: function() {
if (Support.mobileTouch) return;
this.clearTimer();
Window.timers.set(
"ui-fullclick",
function() {
this.hide();
}.bind(this),
Window.view ? Window.view.options.uiDelay : 0
);
},
};
UI.inside = {
initialize: function() {},
enable: function() {
this.bind();
},
disable: function() {
this.unbind();
},
bind: function() {
// no need to bind twice
if (this._onMouseUpHandler) return;
this.unbind();
// clicks
Window._pages.on(
"mouseup",
".fr-content",
(this._onMouseUpHandler = this._onMouseUp.bind(this))
);
// buttons
Window._pages
.on(
"click",
".fr-content .fr-close",
function(event) {
event.preventDefault();
Window.hide();
}.bind(this)
)
.on(
"click",
".fr-content .fr-side-previous",
function(event) {
Window.previous();
this._onMouseMove(event); // update cursor
}.bind(this)
)
.on(
"click",
".fr-content .fr-side-next",
function(event) {
Window.next();
this._onMouseMove(event); // update cursor
}.bind(this)
);
// overlay
Window.element.on(
"click",
".fr-container, .fr-thumbnails, .fr-thumbnails-wrapper",
(this._delegateOverlayCloseHandler = this._delegateOverlayClose.bind(
this
))
);
// track <> only on desktop
if (!Support.mobileTouch) {
Window.element
.on(
"mouseenter",
".fr-content",
(this._showHandler = this.show.bind(this))
)
.on(
"mouseleave",
".fr-content",
(this._hideHandler = this.hide.bind(this))
);
Window.element.on(
"mousemove",
".fr-content",
(this._mousemoveHandler = 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._hoveringSideButton || (y === this._y && x === this._x)) {
return;
}
// cache x/y
this._x = x;
this._y = y;
this.show();
this.startTimer();
}.bind(this))
);
// block mousemove on caption and info
Window._pages.on(
"mousemove",
".fr-info, .fr-close",
function(event) {
event.stopPropagation();
this._onMouseLeave(event);
}.bind(this)
);
// hovering info shouldn't hide it
// mousemove is used here since mouseenter would conflict with
// mouseenter on the content triggering later. That would make it
// impossible to enter the content on the info box and keep it
// visible since the timer would start again on the 2nd mouseenter.
Window._pages.on(
"mousemove",
".fr-info",
function() {
this.clearTimer();
}.bind(this)
);
// delegate <> mousemove/click states
Window._pages
.on(
"mousemove",
".fr-content",
(this._onMouseMoveHandler = this._onMouseMove.bind(this))
)
.on(
"mouseleave",
".fr-content",
(this._onMouseLeaveHandler = this._onMouseLeave.bind(this))
)
.on(
"mouseenter",
".fr-content",
(this._onMouseEnterHandler = this._onMouseEnter.bind(this))
);
// delegate moving onto the <> buttons
// keeping the mouse on them should keep the buttons visible
Window.element
.on(
"mouseenter",
".fr-side",
(this._onSideMouseEnterHandler = this._onSideMouseEnter.bind(this))
)
.on(
"mouseleave",
".fr-side",
(this._onSideMouseLeaveHandler = this._onSideMouseLeave.bind(this))
);
$(window).on(
"scroll",
(this._onScrollHandler = this._onScroll.bind(this))
);
}
},
unbind: function() {
if (!this._onMouseUpHandler) return;
// clicks
Window._pages.off("mouseup", ".fr-content", this._onMouseUpHandler);
this._onMouseUpHandler = null;
// buttons
Window._pages
.off("click", ".fr-content .fr-close")
.off("click", ".fr-content .fr-side-previous")
.off("click", ".fr-content .fr-side-next");
// overlay
Window.element.off(
"click",
".fr-container, .fr-thumbnails, .fr-thumbnails-wrapper",
this._delegateOverlayCloseHandler
);
if (this._showHandler) {
Window.element
.off("mouseenter", ".fr-content", this._showHandler)
.off("mouseleave", ".fr-content", this._hideHandler)
.off("mousemove", ".fr-content", this._mousemoveHandler);
// blocked mousemove
Window._pages.off("mousemove", ".fr-info, .fr-close");
// info
Window._pages.off("mousemove", ".fr-info");
Window._pages
.off("mousemove", ".fr-content-element", this._onMouseMoveHandler)
.off("mouseleave", ".fr-content", this._onMouseLeaveHandler)
.off("mouseenter", ".fr-content", this._onMouseEnterHandler);
Window.element
.off("mouseenter", ".fr-side", this._onSideMouseEnterHandler)
.off("mouseleave", ".fr-side", this._onSideMouseLeaveHandler);
$(window).off("scroll", this._onScrollHandler);
this._showHandler = null;
}
},
reset: function() {
Window.timers.clear("ui-fullclick");
// clear cached mousemove
this._x = -1;
this._y = -1;
this._scrollLeft = -1;
this._hoveringSide = false;
// reset the classes by faking a mouseleave
this._onMouseLeave();
},
adjustPrevNext: function(callback) {
if (callback) callback();
},
_onScroll: function() {
this._scrollLeft = $(window).scrollLeft();
},
_delegateOverlayClose: function(event) {
var page = Pages.page;
if (page && page.view.options.overlay && !page.view.options.overlay.close)
return;
// we don't want to respond to clicks on children
if (
!$(event.target).is(
".fr-container, .fr-thumbnails, .fr-thumbnails-wrapper"
)
)
return;
event.preventDefault();
event.stopPropagation();
Window.hide();
},
_onMouseMove: function(event) {
// no need for this on mobile-touch
if (Support.mobileTouch) return;
var side = this._getEventSide(event),
Side = _.String.capitalize(side),
mayClickHoveringSide = side ? Window["may" + Side]() : false;
// clear out side when onClick:'close'
if (
Pages.pages.length === 1 ||
(Pages.page && Pages.page.view.options.onClick === "close")
) {
side = false;
}
// prevent doing this on every mousemove
if (
side === this._hoveringSide &&
mayClickHoveringSide === this._mayClickHoveringSide
) {
return;
}
this._hoveringSide = side;
this._mayClickHoveringSide = mayClickHoveringSide;
if (side) {
Window._box[(mayClickHoveringSide ? "add" : "remove") + "Class"](
"fr-hovering-clickable"
);
// previous
switch (side) {
case "previous":
Window._box
.addClass("fr-hovering-previous")
.removeClass("fr-hovering-next");
break;
case "next":
Window._box
.addClass("fr-hovering-next")
.removeClass("fr-hovering-previous");
break;
}
} else {
Window._box.removeClass(
"fr-hovering-clickable fr-hovering-previous fr-hovering-next"
);
}
},
_onMouseLeave: function(event) {
Window._box.removeClass(
"fr-hovering-clickable fr-hovering-previous fr-hovering-next"
);
this._hoveringSide = false;
},
// click == mouseup
// We use mouseup instead of click because it is more reliable.
// Multiple clicks on images coming in and out of view can be
// seen as triggering a selection, which doesn't trigger a click
// event, but mouseup always fires.
_onMouseUp: function(event) {
if (
// don't respond to middle/right clicks (2 & 3)
event.which > 1 ||
// or invalid targets
!$(event.target).is(UI._validClickTargetSelector)
) {
return;
}
// close when needed
if (
Pages.pages.length === 1 ||
(Pages.page && Pages.page.view.options.onClick === "close")
) {
Window.hide();
return;
}
var side = this._getEventSide(event);
Window[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 scrollLeft =
this._scrollLeft > -1
? this._scrollLeft
: (this._scrollLeft = $(window).scrollLeft()),
left = event.pageX - Window._boxPosition.left - this._scrollLeft,
width = Window._boxDimensions.width;
return left < 0.5 * width ? "previous" : "next";
},
_onSideMouseEnter: function(event) {
this._hoveringSideButton = true;
this._hoveringSide = this._getEventSide(event);
this._mayClickHoveringSide = Window[
"may" + _.String.capitalize(this._hoveringSide)
]();
this.clearTimer();
},
_onSideMouseLeave: function(event) {
this._hoveringSideButton = false;
this._hoveringSide = false;
this._mayClickHoveringSide = false;
this.startTimer();
},
show: function(callback) {
if (this._visible) {
// still clear a timer that could possible trigger hide
// and start a new one
this.startTimer();
if (typeof callback === "function") callback();
return;
}
this._visible = true;
//UI.fullclick.visible = true;
this.startTimer();
Window.element
.addClass("fr-visible-inside-ui")
.removeClass("fr-hidden-inside-ui");
if (typeof callback === "function") callback();
},
hide: function(callback) {
if (!this._visible) {
if (typeof callback === "function") callback();
return;
}
this._visible = false;
//UI.fullclick.visible = false;
Window.element
.removeClass("fr-visible-inside-ui")
.addClass("fr-hidden-inside-ui");
if (typeof callback === "function") callback();
},
// timers
// not used on mobile-touch based devices
clearTimer: function() {
if (Support.mobileTouch) return;
Window.timers.clear("ui-inside");
},
startTimer: function() {
if (Support.mobileTouch) return;
this.clearTimer();
Window.timers.set(
"ui-inside",
function() {
this.hide();
}.bind(this),
Window.view ? Window.view.options.uiDelay : 0
);
},
};
UI.outside = {
initialize: function() {
this.build();
this._scrollLeft = -1;
},
build: function() {
Window._box
.append(
(this._previous = $("<div>")
.addClass("fr-side fr-side-previous fr-side-previous-outside")
.append(
$("<div>")
.addClass("fr-side-button")
.append($("<div>").addClass("fr-side-button-background"))
.append($("<div>").addClass("fr-side-button-icon"))
))
)
.append(
(this._next = $("<div>")
.addClass("fr-side fr-side-next fr-side-next-outside")
.append(
$("<div>")
.addClass("fr-side-button")
.append($("<div>").addClass("fr-side-button-background"))
.append($("<div>").addClass("fr-side-button-icon"))
))
)
// close
.append(
(this._close = $("<div>")
.addClass("fr-close fr-close-outside")
.append($("<div>").addClass("fr-close-background"))
.append($("<div>").addClass("fr-close-icon")))
);
// IE7 has a bug that causes multiple UI buttons from showing up
// starting hidden fixes it
if (Browser.IE && Browser.IE <= 7) {
this._previous
.add(this._next)
.add(this._close)
.hide();
}
// events
this._close.on(
"click",
function(event) {
event.preventDefault();
Window.hide();
}.bind(this)
);
this._previous.on(
"click",
function(event) {
Window.previous();
this._onMouseMove(event); // update cursor
}.bind(this)
);
this._next.on(
"click",
function(event) {
Window.next();
this._onMouseMove(event); // update cursor
}.bind(this)
);
},
enable: function() {
this.bind();
},
disable: function() {
this.unbind();
},
reset: function() {
Window.timers.clear("ui-outside");
// clear cached mousemove
this._x = -1;
this._y = -1;
this._scrollLeft = -1;
// reset the classes by faking a mouseleave
this._onMouseLeave();
},
// events
bind: function() {
// no need to bind twice
if (this._onMouseUpHandler) return;
this.unbind();
// clicks
Window.element.on(
"mouseup",
".fr-content",
(this._onMouseUpHandler = this._onMouseUp.bind(this))
);
// overlay
Window.element.on(
"click",
".fr-container, .fr-thumbnails, .fr-thumbnails-wrapper",
(this._delegateOverlayCloseHandler = this._delegateOverlayClose.bind(
this
))
);
// track <> only on desktop
if (!Support.mobileTouch) {
// delegate <> mousemove/click states
Window._pages
.on(
"mousemove",
".fr-content",
(this._onMouseMoveHandler = this._onMouseMove.bind(this))
)
.on(
"mouseleave",
".fr-content",
(this._onMouseLeaveHandler = this._onMouseLeave.bind(this))
)
.on(
"mouseenter",
".fr-content",
(this._onMouseEnterHandler = this._onMouseEnter.bind(this))
);
// delegate moving onto the <> buttons
// keeping the mouse on them should keep the buttons visible
Window.element
.on(
"mouseenter",
".fr-side",
(this._onSideMouseEnterHandler = this._onSideMouseEnter.bind(this))
)
.on(
"mouseleave",
".fr-side",
(this._onSideMouseLeaveHandler = this._onSideMouseLeave.bind(this))
);
$(window).on(
"scroll",
(this._onScrollHandler = this._onScroll.bind(this))
);
}
},
unbind: function() {
if (!this._onMouseUpHandler) return;
// clicks
Window.element.off("mouseup", ".fr-content", this._onMouseUpHandler);
this._onMouseUpHandler = null;
// overlay
Window.element.off(
"click",
".fr-container, .fr-thumbnails, .fr-thumbnails-wrapper",
this._delegateOverlayCloseHandler
);
if (this._onMouseMoveHandler) {
Window._pages
.off("mousemove", ".fr-content", this._onMouseMoveHandler)
.off("mouseleave", ".fr-content", this._onMouseLeaveHandler)
.off("mouseenter", ".fr-content", this._onMouseEnterHandler);
Window.element
.off("mouseenter", ".fr-side", this._onSideMouseEnterHandler)
.off("mouseleave", ".fr-side", this._onSideMouseLeaveHandler);
$(window).off("scroll", this._onScrollHandler);
this._onMouseMoveHandler = null;
}
},
adjustPrevNext: function(callback, alternateDuration) {
var page = Pages.page;
if (!page) {
if (callback) callback();
return;
}
var buttons = this._previous.add(this._next);
// disabled states
this._previous[(Window.mayPrevious() ? "remove" : "add") + "Class"](
"fr-side-disabled"
);
this._next[(Window.mayNext() ? "remove" : "add") + "Class"](
"fr-side-disabled"
);
// hide buttons for single content
buttons[(page._total < 2 ? "add" : "remove") + "Class"]("fr-side-hidden");
if (callback) callback();
},
_onScroll: function() {
this._scrollLeft = $(window).scrollLeft();
},
_delegateOverlayClose: function(event) {
var page = Pages.page;
if (page && page.view.options.overlay && !page.view.options.overlay.close)
return;
// we don't want to respond to clicks on children
if (
!$(event.target).is(
".fr-container, .fr-thumbnails, .fr-thumbnails-wrapper"
)
)
return;
event.preventDefault();
event.stopPropagation();
Window.hide();
},
_onMouseMove: function(event) {
// no need for this on mobile-touch
if (Support.mobileTouch) return;
var side = this._getEventSide(event),
Side = _.String.capitalize(side),
mayClickHoveringSide = side ? Window["may" + Side]() : false;
// clear out side when onClick:'close'
if (
Pages.pages.length === 1 ||
(Pages.page && Pages.page.view.options.onClick === "close")
) {
side = false;
}
// prevent doing this on every mousemove
if (
side === this._hoveringSide &&
mayClickHoveringSide === this._mayClickHoveringSide
) {
return;
}
this._hoveringSide = side;
this._mayClickHoveringSide = mayClickHoveringSide;
if (side) {
Window._box[(mayClickHoveringSide ? "add" : "remove") + "Class"](
"fr-hovering-clickable"
);
// previous
switch (side) {
case "previous":
Window._box
.addClass("fr-hovering-previous")
.removeClass("fr-hovering-next");
break;
case "next":
Window._box
.addClass("fr-hovering-next")
.removeClass("fr-hovering-previous");
break;
}
} else {
Window._box.removeClass(
"fr-hovering-clickable fr-hovering-previous fr-hovering-next"
);
}
},
_onMouseLeave: function(event) {
Window._box.removeClass(
"fr-hovering-clickable fr-hovering-previous fr-hovering-next"
);
this._hoveringSide = false;
},
// click == mouseup
// We use mouseup instead of click because it is more reliable.
// Multiple clicks on images coming in and out of view can be
// seen as triggering a selection, which doesn't trigger a click
// event, but mouseup always fires.
_onMouseUp: function(event) {
if (
// don't respond to middle/right clicks (2 & 3)
event.which > 1 ||
// or invalid targets
!$(event.target).is(UI._validClickTargetSelector)
) {
return;
}
// close when needed
if (
Pages.pages.length === 1 ||
(Pages.page && Pages.page.view.options.onClick === "close")
) {
Window.hide();
return;
}
var side = this._getEventSide(event);
Window[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 scrollLeft =
this._scrollLeft > -1
? this._scrollLeft
: (this._scrollLeft = $(window).scrollLeft()),
left = event.pageX - Window._boxPosition.left - this._scrollLeft,
width = Window._boxDimensions.width;
return left < 0.5 * width ? "previous" : "next";
},
show: function() {
if (Browser.IE && Browser.IE <= 7) {
this._previous
.add(this._next)
.add(this._close)
.show();
}
},
hide: function() {},
_onSideMouseEnter: function(event) {
this._hoveringSideButton = true;
this._hoveringSide = this._getEventSide(event);
this._mayClickHoveringSide = Window[
"may" + _.String.capitalize(this._hoveringSide)
]();
},
_onSideMouseLeave: function(event) {
this._hoveringSideButton = false;
this._hoveringSide = false;
this._mayClickHoveringSide = false;
},
clearTimer: function() {},
};
// start
$(function() {
_Fresco.initialize();
});
return Fresco;
});