/*! PhotoSwipe - v4.1.3 - 2019-01-08
|
|
* http://photoswipe.com
|
|
* Copyright (c) 2019 Dmitry Semenov; */
|
|
(function(root, factory) {
|
|
if (typeof define === "function" && define.amd) {
|
|
define(factory);
|
|
} else if (typeof exports === "object") {
|
|
module.exports = factory();
|
|
} else {
|
|
root.PhotoSwipe = factory();
|
|
}
|
|
})(this, function() {
|
|
"use strict";
|
|
var PhotoSwipe = function(template, UiClass, items, options) {
|
|
/*>>framework-bridge*/
|
|
/**
|
|
*
|
|
* Set of generic functions used by gallery.
|
|
*
|
|
* You're free to modify anything here as long as functionality is kept.
|
|
*
|
|
*/
|
|
var framework = {
|
|
features: null,
|
|
bind: function(target, type, listener, unbind) {
|
|
var methodName = (unbind ? "remove" : "add") + "EventListener";
|
|
type = type.split(" ");
|
|
for (var i = 0; i < type.length; i++) {
|
|
if (type[i]) {
|
|
target[methodName](type[i], listener, false);
|
|
}
|
|
}
|
|
},
|
|
isArray: function(obj) {
|
|
return obj instanceof Array;
|
|
},
|
|
createEl: function(classes, tag) {
|
|
var el = document.createElement(tag || "div");
|
|
if (classes) {
|
|
el.className = classes;
|
|
}
|
|
return el;
|
|
},
|
|
getScrollY: function() {
|
|
var yOffset = window.pageYOffset;
|
|
return yOffset !== undefined
|
|
? yOffset
|
|
: document.documentElement.scrollTop;
|
|
},
|
|
unbind: function(target, type, listener) {
|
|
framework.bind(target, type, listener, true);
|
|
},
|
|
removeClass: function(el, className) {
|
|
var reg = new RegExp("(\\s|^)" + className + "(\\s|$)");
|
|
el.className = el.className
|
|
.replace(reg, " ")
|
|
.replace(/^\s\s*/, "")
|
|
.replace(/\s\s*$/, "");
|
|
},
|
|
addClass: function(el, className) {
|
|
if (!framework.hasClass(el, className)) {
|
|
el.className += (el.className ? " " : "") + className;
|
|
}
|
|
},
|
|
hasClass: function(el, className) {
|
|
return (
|
|
el.className &&
|
|
new RegExp("(^|\\s)" + className + "(\\s|$)").test(el.className)
|
|
);
|
|
},
|
|
getChildByClass: function(parentEl, childClassName) {
|
|
var node = parentEl.firstChild;
|
|
while (node) {
|
|
if (framework.hasClass(node, childClassName)) {
|
|
return node;
|
|
}
|
|
node = node.nextSibling;
|
|
}
|
|
},
|
|
arraySearch: function(array, value, key) {
|
|
var i = array.length;
|
|
while (i--) {
|
|
if (array[i][key] === value) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
},
|
|
extend: function(o1, o2, preventOverwrite) {
|
|
for (var prop in o2) {
|
|
if (o2.hasOwnProperty(prop)) {
|
|
if (preventOverwrite && o1.hasOwnProperty(prop)) {
|
|
continue;
|
|
}
|
|
o1[prop] = o2[prop];
|
|
}
|
|
}
|
|
},
|
|
easing: {
|
|
sine: {
|
|
out: function(k) {
|
|
return Math.sin(k * (Math.PI / 2));
|
|
},
|
|
inOut: function(k) {
|
|
return -(Math.cos(Math.PI * k) - 1) / 2;
|
|
}
|
|
},
|
|
cubic: {
|
|
out: function(k) {
|
|
return --k * k * k + 1;
|
|
}
|
|
}
|
|
/*
|
|
elastic: {
|
|
out: function ( k ) {
|
|
|
|
var s, a = 0.1, p = 0.4;
|
|
if ( k === 0 ) return 0;
|
|
if ( k === 1 ) return 1;
|
|
if ( !a || a < 1 ) { a = 1; s = p / 4; }
|
|
else s = p * Math.asin( 1 / a ) / ( 2 * Math.PI );
|
|
return ( a * Math.pow( 2, - 10 * k) * Math.sin( ( k - s ) * ( 2 * Math.PI ) / p ) + 1 );
|
|
|
|
},
|
|
},
|
|
back: {
|
|
out: function ( k ) {
|
|
var s = 1.70158;
|
|
return --k * k * ( ( s + 1 ) * k + s ) + 1;
|
|
}
|
|
}
|
|
*/
|
|
},
|
|
|
|
/**
|
|
*
|
|
* @return {object}
|
|
*
|
|
* {
|
|
* raf : request animation frame function
|
|
* caf : cancel animation frame function
|
|
* transfrom : transform property key (with vendor), or null if not supported
|
|
* oldIE : IE8 or below
|
|
* }
|
|
*
|
|
*/
|
|
detectFeatures: function() {
|
|
if (framework.features) {
|
|
return framework.features;
|
|
}
|
|
var helperEl = framework.createEl(),
|
|
helperStyle = helperEl.style,
|
|
vendor = "",
|
|
features = {};
|
|
|
|
// IE8 and below
|
|
features.oldIE = document.all && !document.addEventListener;
|
|
|
|
features.touch = "ontouchstart" in window;
|
|
|
|
if (window.requestAnimationFrame) {
|
|
features.raf = window.requestAnimationFrame;
|
|
features.caf = window.cancelAnimationFrame;
|
|
}
|
|
|
|
features.pointerEvent =
|
|
!!window.PointerEvent || navigator.msPointerEnabled;
|
|
|
|
// fix false-positive detection of old Android in new IE
|
|
// (IE11 ua string contains "Android 4.0")
|
|
|
|
if (!features.pointerEvent) {
|
|
var ua = navigator.userAgent;
|
|
|
|
// Detect if device is iPhone or iPod and if it's older than iOS 8
|
|
// http://stackoverflow.com/a/14223920
|
|
//
|
|
// This detection is made because of buggy top/bottom toolbars
|
|
// that don't trigger window.resize event.
|
|
// For more info refer to _isFixedPosition variable in core.js
|
|
|
|
if (/iP(hone|od)/.test(navigator.platform)) {
|
|
var v = navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/);
|
|
if (v && v.length > 0) {
|
|
v = parseInt(v[1], 10);
|
|
if (v >= 1 && v < 8) {
|
|
features.isOldIOSPhone = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Detect old Android (before KitKat)
|
|
// due to bugs related to position:fixed
|
|
// http://stackoverflow.com/questions/7184573/pick-up-the-android-version-in-the-browser-by-javascript
|
|
|
|
var match = ua.match(/Android\s([0-9\.]*)/);
|
|
var androidversion = match ? match[1] : 0;
|
|
androidversion = parseFloat(androidversion);
|
|
if (androidversion >= 1) {
|
|
if (androidversion < 4.4) {
|
|
features.isOldAndroid = true; // for fixed position bug & performance
|
|
}
|
|
features.androidVersion = androidversion; // for touchend bug
|
|
}
|
|
features.isMobileOpera = /opera mini|opera mobi/i.test(ua);
|
|
|
|
// p.s. yes, yes, UA sniffing is bad, propose your solution for above bugs.
|
|
}
|
|
|
|
var styleChecks = ["transform", "perspective", "animationName"],
|
|
vendors = ["", "webkit", "Moz", "ms", "O"],
|
|
styleCheckItem,
|
|
styleName;
|
|
|
|
for (var i = 0; i < 4; i++) {
|
|
vendor = vendors[i];
|
|
|
|
for (var a = 0; a < 3; a++) {
|
|
styleCheckItem = styleChecks[a];
|
|
|
|
// uppercase first letter of property name, if vendor is present
|
|
styleName =
|
|
vendor +
|
|
(vendor
|
|
? styleCheckItem.charAt(0).toUpperCase() +
|
|
styleCheckItem.slice(1)
|
|
: styleCheckItem);
|
|
|
|
if (!features[styleCheckItem] && styleName in helperStyle) {
|
|
features[styleCheckItem] = styleName;
|
|
}
|
|
}
|
|
|
|
if (vendor && !features.raf) {
|
|
vendor = vendor.toLowerCase();
|
|
features.raf = window[vendor + "RequestAnimationFrame"];
|
|
if (features.raf) {
|
|
features.caf =
|
|
window[vendor + "CancelAnimationFrame"] ||
|
|
window[vendor + "CancelRequestAnimationFrame"];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!features.raf) {
|
|
var lastTime = 0;
|
|
features.raf = function(fn) {
|
|
var currTime = new Date().getTime();
|
|
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
|
|
var id = window.setTimeout(function() {
|
|
fn(currTime + timeToCall);
|
|
}, timeToCall);
|
|
lastTime = currTime + timeToCall;
|
|
return id;
|
|
};
|
|
features.caf = function(id) {
|
|
clearTimeout(id);
|
|
};
|
|
}
|
|
|
|
// Detect SVG support
|
|
features.svg =
|
|
!!document.createElementNS &&
|
|
!!document.createElementNS("http://www.w3.org/2000/svg", "svg")
|
|
.createSVGRect;
|
|
|
|
framework.features = features;
|
|
|
|
return features;
|
|
}
|
|
};
|
|
|
|
framework.detectFeatures();
|
|
|
|
// Override addEventListener for old versions of IE
|
|
if (framework.features.oldIE) {
|
|
framework.bind = function(target, type, listener, unbind) {
|
|
type = type.split(" ");
|
|
|
|
var methodName = (unbind ? "detach" : "attach") + "Event",
|
|
evName,
|
|
_handleEv = function() {
|
|
listener.handleEvent.call(listener);
|
|
};
|
|
|
|
for (var i = 0; i < type.length; i++) {
|
|
evName = type[i];
|
|
if (evName) {
|
|
if (typeof listener === "object" && listener.handleEvent) {
|
|
if (!unbind) {
|
|
listener["oldIE" + evName] = _handleEv;
|
|
} else {
|
|
if (!listener["oldIE" + evName]) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
target[methodName]("on" + evName, listener["oldIE" + evName]);
|
|
} else {
|
|
target[methodName]("on" + evName, listener);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
/*>>framework-bridge*/
|
|
|
|
/*>>core*/
|
|
//function(template, UiClass, items, options)
|
|
|
|
var self = this;
|
|
|
|
/**
|
|
* Static vars, don't change unless you know what you're doing.
|
|
*/
|
|
var DOUBLE_TAP_RADIUS = 25,
|
|
NUM_HOLDERS = 3;
|
|
|
|
/**
|
|
* Options
|
|
*/
|
|
var _options = {
|
|
allowPanToNext: true,
|
|
spacing: 0.12,
|
|
bgOpacity: 1,
|
|
mouseUsed: false,
|
|
loop: true,
|
|
pinchToClose: true,
|
|
closeOnScroll: true,
|
|
closeOnVerticalDrag: true,
|
|
verticalDragRange: 0.75,
|
|
hideAnimationDuration: 333,
|
|
showAnimationDuration: 333,
|
|
showHideOpacity: false,
|
|
focus: true,
|
|
escKey: true,
|
|
arrowKeys: true,
|
|
mainScrollEndFriction: 0.35,
|
|
panEndFriction: 0.35,
|
|
isClickableElement: function(el) {
|
|
return el.tagName === "A";
|
|
},
|
|
getDoubleTapZoom: function(isMouseClick, item) {
|
|
if (isMouseClick) {
|
|
return 1;
|
|
} else {
|
|
return item.initialZoomLevel < 0.7 ? 1 : 1.33;
|
|
}
|
|
},
|
|
maxSpreadZoom: 1.33,
|
|
modal: true,
|
|
|
|
// not fully implemented yet
|
|
scaleMode: "fit" // TODO
|
|
};
|
|
framework.extend(_options, options);
|
|
|
|
/**
|
|
* Private helper variables & functions
|
|
*/
|
|
|
|
var _getEmptyPoint = function() {
|
|
return { x: 0, y: 0 };
|
|
};
|
|
|
|
var _isOpen,
|
|
_isDestroying,
|
|
_closedByScroll,
|
|
_currentItemIndex,
|
|
_containerStyle,
|
|
_containerShiftIndex,
|
|
_currPanDist = _getEmptyPoint(),
|
|
_startPanOffset = _getEmptyPoint(),
|
|
_panOffset = _getEmptyPoint(),
|
|
_upMoveEvents, // drag move, drag end & drag cancel events array
|
|
_downEvents, // drag start events array
|
|
_globalEventHandlers,
|
|
_viewportSize = {},
|
|
_currZoomLevel,
|
|
_startZoomLevel,
|
|
_translatePrefix,
|
|
_translateSufix,
|
|
_updateSizeInterval,
|
|
_itemsNeedUpdate,
|
|
_currPositionIndex = 0,
|
|
_offset = {},
|
|
_slideSize = _getEmptyPoint(), // size of slide area, including spacing
|
|
_itemHolders,
|
|
_prevItemIndex,
|
|
_indexDiff = 0, // difference of indexes since last content update
|
|
_dragStartEvent,
|
|
_dragMoveEvent,
|
|
_dragEndEvent,
|
|
_dragCancelEvent,
|
|
_transformKey,
|
|
_pointerEventEnabled,
|
|
_isFixedPosition = true,
|
|
_likelyTouchDevice,
|
|
_modules = [],
|
|
_requestAF,
|
|
_cancelAF,
|
|
_initalClassName,
|
|
_initalWindowScrollY,
|
|
_oldIE,
|
|
_currentWindowScrollY,
|
|
_features,
|
|
_windowVisibleSize = {},
|
|
_renderMaxResolution = false,
|
|
_orientationChangeTimeout,
|
|
// Registers PhotoSWipe module (History, Controller ...)
|
|
_registerModule = function(name, module) {
|
|
framework.extend(self, module.publicMethods);
|
|
_modules.push(name);
|
|
},
|
|
_getLoopedId = function(index) {
|
|
var numSlides = _getNumItems();
|
|
if (index > numSlides - 1) {
|
|
return index - numSlides;
|
|
} else if (index < 0) {
|
|
return numSlides + index;
|
|
}
|
|
return index;
|
|
},
|
|
// Micro bind/trigger
|
|
_listeners = {},
|
|
_listen = function(name, fn) {
|
|
if (!_listeners[name]) {
|
|
_listeners[name] = [];
|
|
}
|
|
return _listeners[name].push(fn);
|
|
},
|
|
_shout = function(name) {
|
|
var listeners = _listeners[name];
|
|
|
|
if (listeners) {
|
|
var args = Array.prototype.slice.call(arguments);
|
|
args.shift();
|
|
|
|
for (var i = 0; i < listeners.length; i++) {
|
|
listeners[i].apply(self, args);
|
|
}
|
|
}
|
|
},
|
|
_getCurrentTime = function() {
|
|
return new Date().getTime();
|
|
},
|
|
_applyBgOpacity = function(opacity) {
|
|
_bgOpacity = opacity;
|
|
self.bg.style.opacity = opacity * _options.bgOpacity;
|
|
},
|
|
_applyZoomTransform = function(styleObj, x, y, zoom, item) {
|
|
if (!_renderMaxResolution || (item && item !== self.currItem)) {
|
|
zoom = zoom / (item ? item.fitRatio : self.currItem.fitRatio);
|
|
}
|
|
|
|
styleObj[_transformKey] =
|
|
_translatePrefix +
|
|
x +
|
|
"px, " +
|
|
y +
|
|
"px" +
|
|
_translateSufix +
|
|
" scale(" +
|
|
zoom +
|
|
")";
|
|
},
|
|
_applyCurrentZoomPan = function(allowRenderResolution) {
|
|
if (_currZoomElementStyle) {
|
|
if (allowRenderResolution) {
|
|
if (_currZoomLevel > self.currItem.fitRatio) {
|
|
if (!_renderMaxResolution) {
|
|
_setImageSize(self.currItem, false, true);
|
|
_renderMaxResolution = true;
|
|
}
|
|
} else {
|
|
if (_renderMaxResolution) {
|
|
_setImageSize(self.currItem);
|
|
_renderMaxResolution = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
_applyZoomTransform(
|
|
_currZoomElementStyle,
|
|
_panOffset.x,
|
|
_panOffset.y,
|
|
_currZoomLevel
|
|
);
|
|
}
|
|
},
|
|
_applyZoomPanToItem = function(item) {
|
|
if (item.container) {
|
|
_applyZoomTransform(
|
|
item.container.style,
|
|
item.initialPosition.x,
|
|
item.initialPosition.y,
|
|
item.initialZoomLevel,
|
|
item
|
|
);
|
|
}
|
|
},
|
|
_setTranslateX = function(x, elStyle) {
|
|
elStyle[_transformKey] =
|
|
_translatePrefix + x + "px, 0px" + _translateSufix;
|
|
},
|
|
_moveMainScroll = function(x, dragging) {
|
|
if (!_options.loop && dragging) {
|
|
var newSlideIndexOffset =
|
|
_currentItemIndex +
|
|
(_slideSize.x * _currPositionIndex - x) / _slideSize.x,
|
|
delta = Math.round(x - _mainScrollPos.x);
|
|
|
|
if (
|
|
(newSlideIndexOffset < 0 && delta > 0) ||
|
|
(newSlideIndexOffset >= _getNumItems() - 1 && delta < 0)
|
|
) {
|
|
x = _mainScrollPos.x + delta * _options.mainScrollEndFriction;
|
|
}
|
|
}
|
|
|
|
_mainScrollPos.x = x;
|
|
_setTranslateX(x, _containerStyle);
|
|
},
|
|
_calculatePanOffset = function(axis, zoomLevel) {
|
|
var m = _midZoomPoint[axis] - _offset[axis];
|
|
return (
|
|
_startPanOffset[axis] +
|
|
_currPanDist[axis] +
|
|
m -
|
|
m * (zoomLevel / _startZoomLevel)
|
|
);
|
|
},
|
|
_equalizePoints = function(p1, p2) {
|
|
p1.x = p2.x;
|
|
p1.y = p2.y;
|
|
if (p2.id) {
|
|
p1.id = p2.id;
|
|
}
|
|
},
|
|
_roundPoint = function(p) {
|
|
p.x = Math.round(p.x);
|
|
p.y = Math.round(p.y);
|
|
},
|
|
_mouseMoveTimeout = null,
|
|
_onFirstMouseMove = function() {
|
|
// Wait until mouse move event is fired at least twice during 100ms
|
|
// We do this, because some mobile browsers trigger it on touchstart
|
|
if (_mouseMoveTimeout) {
|
|
framework.unbind(document, "mousemove", _onFirstMouseMove);
|
|
framework.addClass(template, "pswp--has_mouse");
|
|
_options.mouseUsed = true;
|
|
_shout("mouseUsed");
|
|
}
|
|
_mouseMoveTimeout = setTimeout(function() {
|
|
_mouseMoveTimeout = null;
|
|
}, 100);
|
|
},
|
|
_bindEvents = function() {
|
|
framework.bind(document, "keydown", self);
|
|
|
|
if (_features.transform) {
|
|
// don't bind click event in browsers that don't support transform (mostly IE8)
|
|
framework.bind(self.scrollWrap, "click", self);
|
|
}
|
|
|
|
if (!_options.mouseUsed) {
|
|
framework.bind(document, "mousemove", _onFirstMouseMove);
|
|
}
|
|
|
|
framework.bind(window, "resize scroll orientationchange", self);
|
|
|
|
_shout("bindEvents");
|
|
},
|
|
_unbindEvents = function() {
|
|
framework.unbind(window, "resize scroll orientationchange", self);
|
|
framework.unbind(window, "scroll", _globalEventHandlers.scroll);
|
|
framework.unbind(document, "keydown", self);
|
|
framework.unbind(document, "mousemove", _onFirstMouseMove);
|
|
|
|
if (_features.transform) {
|
|
framework.unbind(self.scrollWrap, "click", self);
|
|
}
|
|
|
|
if (_isDragging) {
|
|
framework.unbind(window, _upMoveEvents, self);
|
|
}
|
|
|
|
clearTimeout(_orientationChangeTimeout);
|
|
|
|
_shout("unbindEvents");
|
|
},
|
|
_calculatePanBounds = function(zoomLevel, update) {
|
|
var bounds = _calculateItemSize(
|
|
self.currItem,
|
|
_viewportSize,
|
|
zoomLevel
|
|
);
|
|
if (update) {
|
|
_currPanBounds = bounds;
|
|
}
|
|
return bounds;
|
|
},
|
|
_getMinZoomLevel = function(item) {
|
|
if (!item) {
|
|
item = self.currItem;
|
|
}
|
|
return item.initialZoomLevel;
|
|
},
|
|
_getMaxZoomLevel = function(item) {
|
|
if (!item) {
|
|
item = self.currItem;
|
|
}
|
|
return item.w > 0 ? _options.maxSpreadZoom : 1;
|
|
},
|
|
// Return true if offset is out of the bounds
|
|
_modifyDestPanOffset = function(
|
|
axis,
|
|
destPanBounds,
|
|
destPanOffset,
|
|
destZoomLevel
|
|
) {
|
|
if (destZoomLevel === self.currItem.initialZoomLevel) {
|
|
destPanOffset[axis] = self.currItem.initialPosition[axis];
|
|
return true;
|
|
} else {
|
|
destPanOffset[axis] = _calculatePanOffset(axis, destZoomLevel);
|
|
|
|
if (destPanOffset[axis] > destPanBounds.min[axis]) {
|
|
destPanOffset[axis] = destPanBounds.min[axis];
|
|
return true;
|
|
} else if (destPanOffset[axis] < destPanBounds.max[axis]) {
|
|
destPanOffset[axis] = destPanBounds.max[axis];
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
_setupTransforms = function() {
|
|
if (_transformKey) {
|
|
// setup 3d transforms
|
|
var allow3dTransform = _features.perspective && !_likelyTouchDevice;
|
|
_translatePrefix = "translate" + (allow3dTransform ? "3d(" : "(");
|
|
_translateSufix = _features.perspective ? ", 0px)" : ")";
|
|
return;
|
|
}
|
|
|
|
// Override zoom/pan/move functions in case old browser is used (most likely IE)
|
|
// (so they use left/top/width/height, instead of CSS transform)
|
|
|
|
_transformKey = "left";
|
|
framework.addClass(template, "pswp--ie");
|
|
|
|
_setTranslateX = function(x, elStyle) {
|
|
elStyle.left = x + "px";
|
|
};
|
|
_applyZoomPanToItem = function(item) {
|
|
var zoomRatio = item.fitRatio > 1 ? 1 : item.fitRatio,
|
|
s = item.container.style,
|
|
w = zoomRatio * item.w,
|
|
h = zoomRatio * item.h;
|
|
|
|
s.width = w + "px";
|
|
s.height = h + "px";
|
|
s.left = item.initialPosition.x + "px";
|
|
s.top = item.initialPosition.y + "px";
|
|
};
|
|
_applyCurrentZoomPan = function() {
|
|
if (_currZoomElementStyle) {
|
|
var s = _currZoomElementStyle,
|
|
item = self.currItem,
|
|
zoomRatio = item.fitRatio > 1 ? 1 : item.fitRatio,
|
|
w = zoomRatio * item.w,
|
|
h = zoomRatio * item.h;
|
|
|
|
s.width = w + "px";
|
|
s.height = h + "px";
|
|
|
|
s.left = _panOffset.x + "px";
|
|
s.top = _panOffset.y + "px";
|
|
}
|
|
};
|
|
},
|
|
_onKeyDown = function(e) {
|
|
var keydownAction = "";
|
|
if (_options.escKey && e.keyCode === 27) {
|
|
keydownAction = "close";
|
|
} else if (_options.arrowKeys) {
|
|
if (e.keyCode === 37) {
|
|
keydownAction = "prev";
|
|
} else if (e.keyCode === 39) {
|
|
keydownAction = "next";
|
|
}
|
|
}
|
|
|
|
if (keydownAction) {
|
|
// don't do anything if special key pressed to prevent from overriding default browser actions
|
|
// e.g. in Chrome on Mac cmd+arrow-left returns to previous page
|
|
if (!e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) {
|
|
if (e.preventDefault) {
|
|
e.preventDefault();
|
|
} else {
|
|
e.returnValue = false;
|
|
}
|
|
self[keydownAction]();
|
|
}
|
|
}
|
|
},
|
|
_onGlobalClick = function(e) {
|
|
if (!e) {
|
|
return;
|
|
}
|
|
|
|
// don't allow click event to pass through when triggering after drag or some other gesture
|
|
if (
|
|
_moved ||
|
|
_zoomStarted ||
|
|
_mainScrollAnimating ||
|
|
_verticalDragInitiated
|
|
) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
},
|
|
_updatePageScrollOffset = function() {
|
|
self.setScrollOffset(0, framework.getScrollY());
|
|
};
|
|
|
|
// Micro animation engine
|
|
var _animations = {},
|
|
_numAnimations = 0,
|
|
_stopAnimation = function(name) {
|
|
if (_animations[name]) {
|
|
if (_animations[name].raf) {
|
|
_cancelAF(_animations[name].raf);
|
|
}
|
|
_numAnimations--;
|
|
delete _animations[name];
|
|
}
|
|
},
|
|
_registerStartAnimation = function(name) {
|
|
if (_animations[name]) {
|
|
_stopAnimation(name);
|
|
}
|
|
if (!_animations[name]) {
|
|
_numAnimations++;
|
|
_animations[name] = {};
|
|
}
|
|
},
|
|
_stopAllAnimations = function() {
|
|
for (var prop in _animations) {
|
|
if (_animations.hasOwnProperty(prop)) {
|
|
_stopAnimation(prop);
|
|
}
|
|
}
|
|
},
|
|
_animateProp = function(
|
|
name,
|
|
b,
|
|
endProp,
|
|
d,
|
|
easingFn,
|
|
onUpdate,
|
|
onComplete
|
|
) {
|
|
var startAnimTime = _getCurrentTime(),
|
|
t;
|
|
_registerStartAnimation(name);
|
|
|
|
var animloop = function() {
|
|
if (_animations[name]) {
|
|
t = _getCurrentTime() - startAnimTime; // time diff
|
|
//b - beginning (start prop)
|
|
//d - anim duration
|
|
|
|
if (t >= d) {
|
|
_stopAnimation(name);
|
|
onUpdate(endProp);
|
|
if (onComplete) {
|
|
onComplete();
|
|
}
|
|
return;
|
|
}
|
|
onUpdate((endProp - b) * easingFn(t / d) + b);
|
|
|
|
_animations[name].raf = _requestAF(animloop);
|
|
}
|
|
};
|
|
animloop();
|
|
};
|
|
|
|
var publicMethods = {
|
|
// make a few local variables and functions public
|
|
shout: _shout,
|
|
listen: _listen,
|
|
viewportSize: _viewportSize,
|
|
options: _options,
|
|
|
|
isMainScrollAnimating: function() {
|
|
return _mainScrollAnimating;
|
|
},
|
|
getZoomLevel: function() {
|
|
return _currZoomLevel;
|
|
},
|
|
getCurrentIndex: function() {
|
|
return _currentItemIndex;
|
|
},
|
|
isDragging: function() {
|
|
return _isDragging;
|
|
},
|
|
isZooming: function() {
|
|
return _isZooming;
|
|
},
|
|
setScrollOffset: function(x, y) {
|
|
_offset.x = x;
|
|
_currentWindowScrollY = _offset.y = y;
|
|
_shout("updateScrollOffset", _offset);
|
|
},
|
|
applyZoomPan: function(zoomLevel, panX, panY, allowRenderResolution) {
|
|
_panOffset.x = panX;
|
|
_panOffset.y = panY;
|
|
_currZoomLevel = zoomLevel;
|
|
_applyCurrentZoomPan(allowRenderResolution);
|
|
},
|
|
|
|
init: function() {
|
|
if (_isOpen || _isDestroying) {
|
|
return;
|
|
}
|
|
|
|
var i;
|
|
|
|
self.framework = framework; // basic functionality
|
|
self.template = template; // root DOM element of PhotoSwipe
|
|
self.bg = framework.getChildByClass(template, "pswp__bg");
|
|
|
|
_initalClassName = template.className;
|
|
_isOpen = true;
|
|
|
|
_features = framework.detectFeatures();
|
|
_requestAF = _features.raf;
|
|
_cancelAF = _features.caf;
|
|
_transformKey = _features.transform;
|
|
_oldIE = _features.oldIE;
|
|
|
|
self.scrollWrap = framework.getChildByClass(
|
|
template,
|
|
"pswp__scroll-wrap"
|
|
);
|
|
self.container = framework.getChildByClass(
|
|
self.scrollWrap,
|
|
"pswp__container"
|
|
);
|
|
|
|
_containerStyle = self.container.style; // for fast access
|
|
|
|
// Objects that hold slides (there are only 3 in DOM)
|
|
self.itemHolders = _itemHolders = [
|
|
{ el: self.container.children[0], wrap: 0, index: -1 },
|
|
{ el: self.container.children[1], wrap: 0, index: -1 },
|
|
{ el: self.container.children[2], wrap: 0, index: -1 }
|
|
];
|
|
|
|
// hide nearby item holders until initial zoom animation finishes (to avoid extra Paints)
|
|
_itemHolders[0].el.style.display = _itemHolders[2].el.style.display =
|
|
"none";
|
|
|
|
_setupTransforms();
|
|
|
|
// Setup global events
|
|
_globalEventHandlers = {
|
|
resize: self.updateSize,
|
|
|
|
// Fixes: iOS 10.3 resize event
|
|
// does not update scrollWrap.clientWidth instantly after resize
|
|
// https://github.com/dimsemenov/PhotoSwipe/issues/1315
|
|
orientationchange: function() {
|
|
clearTimeout(_orientationChangeTimeout);
|
|
_orientationChangeTimeout = setTimeout(function() {
|
|
if (_viewportSize.x !== self.scrollWrap.clientWidth) {
|
|
self.updateSize();
|
|
}
|
|
}, 500);
|
|
},
|
|
scroll: _updatePageScrollOffset,
|
|
keydown: _onKeyDown,
|
|
click: _onGlobalClick
|
|
};
|
|
|
|
// disable show/hide effects on old browsers that don't support CSS animations or transforms,
|
|
// old IOS, Android and Opera mobile. Blackberry seems to work fine, even older models.
|
|
var oldPhone =
|
|
_features.isOldIOSPhone ||
|
|
_features.isOldAndroid ||
|
|
_features.isMobileOpera;
|
|
if (!_features.animationName || !_features.transform || oldPhone) {
|
|
_options.showAnimationDuration = _options.hideAnimationDuration = 0;
|
|
}
|
|
|
|
// init modules
|
|
for (i = 0; i < _modules.length; i++) {
|
|
self["init" + _modules[i]]();
|
|
}
|
|
|
|
// init
|
|
if (UiClass) {
|
|
var ui = (self.ui = new UiClass(self, framework));
|
|
ui.init();
|
|
}
|
|
|
|
_shout("firstUpdate");
|
|
_currentItemIndex = _currentItemIndex || _options.index || 0;
|
|
// validate index
|
|
if (
|
|
isNaN(_currentItemIndex) ||
|
|
_currentItemIndex < 0 ||
|
|
_currentItemIndex >= _getNumItems()
|
|
) {
|
|
_currentItemIndex = 0;
|
|
}
|
|
self.currItem = _getItemAt(_currentItemIndex);
|
|
|
|
if (_features.isOldIOSPhone || _features.isOldAndroid) {
|
|
_isFixedPosition = false;
|
|
}
|
|
|
|
template.setAttribute("aria-hidden", "false");
|
|
if (_options.modal) {
|
|
if (!_isFixedPosition) {
|
|
template.style.position = "absolute";
|
|
template.style.top = framework.getScrollY() + "px";
|
|
} else {
|
|
template.style.position = "fixed";
|
|
}
|
|
}
|
|
|
|
if (_currentWindowScrollY === undefined) {
|
|
_shout("initialLayout");
|
|
_currentWindowScrollY = _initalWindowScrollY = framework.getScrollY();
|
|
}
|
|
|
|
// add classes to root element of PhotoSwipe
|
|
var rootClasses = "pswp--open ";
|
|
if (_options.mainClass) {
|
|
rootClasses += _options.mainClass + " ";
|
|
}
|
|
if (_options.showHideOpacity) {
|
|
rootClasses += "pswp--animate_opacity ";
|
|
}
|
|
rootClasses += _likelyTouchDevice ? "pswp--touch" : "pswp--notouch";
|
|
rootClasses += _features.animationName ? " pswp--css_animation" : "";
|
|
rootClasses += _features.svg ? " pswp--svg" : "";
|
|
framework.addClass(template, rootClasses);
|
|
|
|
self.updateSize();
|
|
|
|
// initial update
|
|
_containerShiftIndex = -1;
|
|
_indexDiff = null;
|
|
for (i = 0; i < NUM_HOLDERS; i++) {
|
|
_setTranslateX(
|
|
(i + _containerShiftIndex) * _slideSize.x,
|
|
_itemHolders[i].el.style
|
|
);
|
|
}
|
|
|
|
if (!_oldIE) {
|
|
framework.bind(self.scrollWrap, _downEvents, self); // no dragging for old IE
|
|
}
|
|
|
|
_listen("initialZoomInEnd", function() {
|
|
self.setContent(_itemHolders[0], _currentItemIndex - 1);
|
|
self.setContent(_itemHolders[2], _currentItemIndex + 1);
|
|
|
|
_itemHolders[0].el.style.display = _itemHolders[2].el.style.display =
|
|
"block";
|
|
|
|
if (_options.focus) {
|
|
// focus causes layout,
|
|
// which causes lag during the animation,
|
|
// that's why we delay it untill the initial zoom transition ends
|
|
template.focus();
|
|
}
|
|
|
|
_bindEvents();
|
|
});
|
|
|
|
// set content for center slide (first time)
|
|
self.setContent(_itemHolders[1], _currentItemIndex);
|
|
|
|
self.updateCurrItem();
|
|
|
|
_shout("afterInit");
|
|
|
|
if (!_isFixedPosition) {
|
|
// On all versions of iOS lower than 8.0, we check size of viewport every second.
|
|
//
|
|
// This is done to detect when Safari top & bottom bars appear,
|
|
// as this action doesn't trigger any events (like resize).
|
|
//
|
|
// On iOS8 they fixed this.
|
|
//
|
|
// 10 Nov 2014: iOS 7 usage ~40%. iOS 8 usage 56%.
|
|
|
|
_updateSizeInterval = setInterval(function() {
|
|
if (
|
|
!_numAnimations &&
|
|
!_isDragging &&
|
|
!_isZooming &&
|
|
_currZoomLevel === self.currItem.initialZoomLevel
|
|
) {
|
|
self.updateSize();
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
framework.addClass(template, "pswp--visible");
|
|
},
|
|
|
|
// Close the gallery, then destroy it
|
|
close: function() {
|
|
if (!_isOpen) {
|
|
return;
|
|
}
|
|
|
|
_isOpen = false;
|
|
_isDestroying = true;
|
|
_shout("close");
|
|
_unbindEvents();
|
|
|
|
_showOrHide(self.currItem, null, true, self.destroy);
|
|
},
|
|
|
|
// destroys the gallery (unbinds events, cleans up intervals and timeouts to avoid memory leaks)
|
|
destroy: function() {
|
|
_shout("destroy");
|
|
|
|
if (_showOrHideTimeout) {
|
|
clearTimeout(_showOrHideTimeout);
|
|
}
|
|
|
|
template.setAttribute("aria-hidden", "true");
|
|
template.className = _initalClassName;
|
|
|
|
if (_updateSizeInterval) {
|
|
clearInterval(_updateSizeInterval);
|
|
}
|
|
|
|
framework.unbind(self.scrollWrap, _downEvents, self);
|
|
|
|
// we unbind scroll event at the end, as closing animation may depend on it
|
|
framework.unbind(window, "scroll", self);
|
|
|
|
_stopDragUpdateLoop();
|
|
|
|
_stopAllAnimations();
|
|
|
|
_listeners = null;
|
|
},
|
|
|
|
/**
|
|
* Pan image to position
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
* @param {Boolean} force Will ignore bounds if set to true.
|
|
*/
|
|
panTo: function(x, y, force) {
|
|
if (!force) {
|
|
if (x > _currPanBounds.min.x) {
|
|
x = _currPanBounds.min.x;
|
|
} else if (x < _currPanBounds.max.x) {
|
|
x = _currPanBounds.max.x;
|
|
}
|
|
|
|
if (y > _currPanBounds.min.y) {
|
|
y = _currPanBounds.min.y;
|
|
} else if (y < _currPanBounds.max.y) {
|
|
y = _currPanBounds.max.y;
|
|
}
|
|
}
|
|
|
|
_panOffset.x = x;
|
|
_panOffset.y = y;
|
|
_applyCurrentZoomPan();
|
|
},
|
|
|
|
handleEvent: function(e) {
|
|
e = e || window.event;
|
|
if (_globalEventHandlers[e.type]) {
|
|
_globalEventHandlers[e.type](e);
|
|
}
|
|
},
|
|
|
|
goTo: function(index) {
|
|
index = _getLoopedId(index);
|
|
|
|
var diff = index - _currentItemIndex;
|
|
_indexDiff = diff;
|
|
|
|
_currentItemIndex = index;
|
|
self.currItem = _getItemAt(_currentItemIndex);
|
|
_currPositionIndex -= diff;
|
|
|
|
_moveMainScroll(_slideSize.x * _currPositionIndex);
|
|
|
|
_stopAllAnimations();
|
|
_mainScrollAnimating = false;
|
|
|
|
self.updateCurrItem();
|
|
},
|
|
next: function() {
|
|
self.goTo(_currentItemIndex + 1);
|
|
},
|
|
prev: function() {
|
|
self.goTo(_currentItemIndex - 1);
|
|
},
|
|
|
|
// update current zoom/pan objects
|
|
updateCurrZoomItem: function(emulateSetContent) {
|
|
if (emulateSetContent) {
|
|
_shout("beforeChange", 0);
|
|
}
|
|
|
|
// itemHolder[1] is middle (current) item
|
|
if (_itemHolders[1].el.children.length) {
|
|
var zoomElement = _itemHolders[1].el.children[0];
|
|
if (framework.hasClass(zoomElement, "pswp__zoom-wrap")) {
|
|
_currZoomElementStyle = zoomElement.style;
|
|
} else {
|
|
_currZoomElementStyle = null;
|
|
}
|
|
} else {
|
|
_currZoomElementStyle = null;
|
|
}
|
|
|
|
_currPanBounds = self.currItem.bounds;
|
|
_startZoomLevel = _currZoomLevel = self.currItem.initialZoomLevel;
|
|
|
|
_panOffset.x = _currPanBounds.center.x;
|
|
_panOffset.y = _currPanBounds.center.y;
|
|
|
|
if (emulateSetContent) {
|
|
_shout("afterChange");
|
|
}
|
|
},
|
|
|
|
invalidateCurrItems: function() {
|
|
_itemsNeedUpdate = true;
|
|
for (var i = 0; i < NUM_HOLDERS; i++) {
|
|
if (_itemHolders[i].item) {
|
|
_itemHolders[i].item.needsUpdate = true;
|
|
}
|
|
}
|
|
},
|
|
|
|
updateCurrItem: function(beforeAnimation) {
|
|
if (_indexDiff === 0) {
|
|
return;
|
|
}
|
|
|
|
var diffAbs = Math.abs(_indexDiff),
|
|
tempHolder;
|
|
|
|
if (beforeAnimation && diffAbs < 2) {
|
|
return;
|
|
}
|
|
|
|
self.currItem = _getItemAt(_currentItemIndex);
|
|
_renderMaxResolution = false;
|
|
|
|
_shout("beforeChange", _indexDiff);
|
|
|
|
if (diffAbs >= NUM_HOLDERS) {
|
|
_containerShiftIndex +=
|
|
_indexDiff + (_indexDiff > 0 ? -NUM_HOLDERS : NUM_HOLDERS);
|
|
diffAbs = NUM_HOLDERS;
|
|
}
|
|
for (var i = 0; i < diffAbs; i++) {
|
|
if (_indexDiff > 0) {
|
|
tempHolder = _itemHolders.shift();
|
|
_itemHolders[NUM_HOLDERS - 1] = tempHolder; // move first to last
|
|
|
|
_containerShiftIndex++;
|
|
_setTranslateX(
|
|
(_containerShiftIndex + 2) * _slideSize.x,
|
|
tempHolder.el.style
|
|
);
|
|
self.setContent(
|
|
tempHolder,
|
|
_currentItemIndex - diffAbs + i + 1 + 1
|
|
);
|
|
} else {
|
|
tempHolder = _itemHolders.pop();
|
|
_itemHolders.unshift(tempHolder); // move last to first
|
|
|
|
_containerShiftIndex--;
|
|
_setTranslateX(
|
|
_containerShiftIndex * _slideSize.x,
|
|
tempHolder.el.style
|
|
);
|
|
self.setContent(
|
|
tempHolder,
|
|
_currentItemIndex + diffAbs - i - 1 - 1
|
|
);
|
|
}
|
|
}
|
|
|
|
// reset zoom/pan on previous item
|
|
if (_currZoomElementStyle && Math.abs(_indexDiff) === 1) {
|
|
var prevItem = _getItemAt(_prevItemIndex);
|
|
if (prevItem.initialZoomLevel !== _currZoomLevel) {
|
|
_calculateItemSize(prevItem, _viewportSize);
|
|
_setImageSize(prevItem);
|
|
_applyZoomPanToItem(prevItem);
|
|
}
|
|
}
|
|
|
|
// reset diff after update
|
|
_indexDiff = 0;
|
|
|
|
self.updateCurrZoomItem();
|
|
|
|
_prevItemIndex = _currentItemIndex;
|
|
|
|
_shout("afterChange");
|
|
},
|
|
|
|
updateSize: function(force) {
|
|
if (!_isFixedPosition && _options.modal) {
|
|
var windowScrollY = framework.getScrollY();
|
|
if (_currentWindowScrollY !== windowScrollY) {
|
|
template.style.top = windowScrollY + "px";
|
|
_currentWindowScrollY = windowScrollY;
|
|
}
|
|
if (
|
|
!force &&
|
|
_windowVisibleSize.x === window.innerWidth &&
|
|
_windowVisibleSize.y === window.innerHeight
|
|
) {
|
|
return;
|
|
}
|
|
_windowVisibleSize.x = window.innerWidth;
|
|
_windowVisibleSize.y = window.innerHeight;
|
|
|
|
//template.style.width = _windowVisibleSize.x + 'px';
|
|
template.style.height = _windowVisibleSize.y + "px";
|
|
}
|
|
|
|
_viewportSize.x = self.scrollWrap.clientWidth;
|
|
_viewportSize.y = self.scrollWrap.clientHeight;
|
|
|
|
_updatePageScrollOffset();
|
|
|
|
_slideSize.x =
|
|
_viewportSize.x + Math.round(_viewportSize.x * _options.spacing);
|
|
_slideSize.y = _viewportSize.y;
|
|
|
|
_moveMainScroll(_slideSize.x * _currPositionIndex);
|
|
|
|
_shout("beforeResize"); // even may be used for example to switch image sources
|
|
|
|
// don't re-calculate size on inital size update
|
|
if (_containerShiftIndex !== undefined) {
|
|
var holder, item, hIndex;
|
|
|
|
for (var i = 0; i < NUM_HOLDERS; i++) {
|
|
holder = _itemHolders[i];
|
|
_setTranslateX(
|
|
(i + _containerShiftIndex) * _slideSize.x,
|
|
holder.el.style
|
|
);
|
|
|
|
hIndex = _currentItemIndex + i - 1;
|
|
|
|
if (_options.loop && _getNumItems() > 2) {
|
|
hIndex = _getLoopedId(hIndex);
|
|
}
|
|
|
|
// update zoom level on items and refresh source (if needsUpdate)
|
|
item = _getItemAt(hIndex);
|
|
|
|
// re-render gallery item if `needsUpdate`,
|
|
// or doesn't have `bounds` (entirely new slide object)
|
|
if (
|
|
item &&
|
|
(_itemsNeedUpdate || item.needsUpdate || !item.bounds)
|
|
) {
|
|
self.cleanSlide(item);
|
|
|
|
self.setContent(holder, hIndex);
|
|
|
|
// if "center" slide
|
|
if (i === 1) {
|
|
self.currItem = item;
|
|
self.updateCurrZoomItem(true);
|
|
}
|
|
|
|
item.needsUpdate = false;
|
|
} else if (holder.index === -1 && hIndex >= 0) {
|
|
// add content first time
|
|
self.setContent(holder, hIndex);
|
|
}
|
|
if (item && item.container) {
|
|
_calculateItemSize(item, _viewportSize);
|
|
_setImageSize(item);
|
|
_applyZoomPanToItem(item);
|
|
}
|
|
}
|
|
_itemsNeedUpdate = false;
|
|
}
|
|
|
|
_startZoomLevel = _currZoomLevel = self.currItem.initialZoomLevel;
|
|
_currPanBounds = self.currItem.bounds;
|
|
|
|
if (_currPanBounds) {
|
|
_panOffset.x = _currPanBounds.center.x;
|
|
_panOffset.y = _currPanBounds.center.y;
|
|
_applyCurrentZoomPan(true);
|
|
}
|
|
|
|
_shout("resize");
|
|
},
|
|
|
|
// Zoom current item to
|
|
zoomTo: function(destZoomLevel, centerPoint, speed, easingFn, updateFn) {
|
|
/*
|
|
if(destZoomLevel === 'fit') {
|
|
destZoomLevel = self.currItem.fitRatio;
|
|
} else if(destZoomLevel === 'fill') {
|
|
destZoomLevel = self.currItem.fillRatio;
|
|
}
|
|
*/
|
|
|
|
if (centerPoint) {
|
|
_startZoomLevel = _currZoomLevel;
|
|
_midZoomPoint.x = Math.abs(centerPoint.x) - _panOffset.x;
|
|
_midZoomPoint.y = Math.abs(centerPoint.y) - _panOffset.y;
|
|
_equalizePoints(_startPanOffset, _panOffset);
|
|
}
|
|
|
|
var destPanBounds = _calculatePanBounds(destZoomLevel, false),
|
|
destPanOffset = {};
|
|
|
|
_modifyDestPanOffset("x", destPanBounds, destPanOffset, destZoomLevel);
|
|
_modifyDestPanOffset("y", destPanBounds, destPanOffset, destZoomLevel);
|
|
|
|
var initialZoomLevel = _currZoomLevel;
|
|
var initialPanOffset = {
|
|
x: _panOffset.x,
|
|
y: _panOffset.y
|
|
};
|
|
|
|
_roundPoint(destPanOffset);
|
|
|
|
var onUpdate = function(now) {
|
|
if (now === 1) {
|
|
_currZoomLevel = destZoomLevel;
|
|
_panOffset.x = destPanOffset.x;
|
|
_panOffset.y = destPanOffset.y;
|
|
} else {
|
|
_currZoomLevel =
|
|
(destZoomLevel - initialZoomLevel) * now + initialZoomLevel;
|
|
_panOffset.x =
|
|
(destPanOffset.x - initialPanOffset.x) * now + initialPanOffset.x;
|
|
_panOffset.y =
|
|
(destPanOffset.y - initialPanOffset.y) * now + initialPanOffset.y;
|
|
}
|
|
|
|
if (updateFn) {
|
|
updateFn(now);
|
|
}
|
|
|
|
_applyCurrentZoomPan(now === 1);
|
|
};
|
|
|
|
if (speed) {
|
|
_animateProp(
|
|
"customZoomTo",
|
|
0,
|
|
1,
|
|
speed,
|
|
easingFn || framework.easing.sine.inOut,
|
|
onUpdate
|
|
);
|
|
} else {
|
|
onUpdate(1);
|
|
}
|
|
}
|
|
};
|
|
|
|
/*>>core*/
|
|
|
|
/*>>gestures*/
|
|
/**
|
|
* Mouse/touch/pointer event handlers.
|
|
*
|
|
* separated from @core.js for readability
|
|
*/
|
|
|
|
var MIN_SWIPE_DISTANCE = 30,
|
|
DIRECTION_CHECK_OFFSET = 10; // amount of pixels to drag to determine direction of swipe
|
|
|
|
var _gestureStartTime,
|
|
_gestureCheckSpeedTime,
|
|
// pool of objects that are used during dragging of zooming
|
|
p = {}, // first point
|
|
p2 = {}, // second point (for zoom gesture)
|
|
delta = {},
|
|
_currPoint = {},
|
|
_startPoint = {},
|
|
_currPointers = [],
|
|
_startMainScrollPos = {},
|
|
_releaseAnimData,
|
|
_posPoints = [], // array of points during dragging, used to determine type of gesture
|
|
_tempPoint = {},
|
|
_isZoomingIn,
|
|
_verticalDragInitiated,
|
|
_oldAndroidTouchEndTimeout,
|
|
_currZoomedItemIndex = 0,
|
|
_centerPoint = _getEmptyPoint(),
|
|
_lastReleaseTime = 0,
|
|
_isDragging, // at least one pointer is down
|
|
_isMultitouch, // at least two _pointers are down
|
|
_zoomStarted, // zoom level changed during zoom gesture
|
|
_moved,
|
|
_dragAnimFrame,
|
|
_mainScrollShifted,
|
|
_currentPoints, // array of current touch points
|
|
_isZooming,
|
|
_currPointsDistance,
|
|
_startPointsDistance,
|
|
_currPanBounds,
|
|
_mainScrollPos = _getEmptyPoint(),
|
|
_currZoomElementStyle,
|
|
_mainScrollAnimating, // true, if animation after swipe gesture is running
|
|
_midZoomPoint = _getEmptyPoint(),
|
|
_currCenterPoint = _getEmptyPoint(),
|
|
_direction,
|
|
_isFirstMove,
|
|
_opacityChanged,
|
|
_bgOpacity,
|
|
_wasOverInitialZoom,
|
|
_isEqualPoints = function(p1, p2) {
|
|
return p1.x === p2.x && p1.y === p2.y;
|
|
},
|
|
_isNearbyPoints = function(touch0, touch1) {
|
|
return (
|
|
Math.abs(touch0.x - touch1.x) < DOUBLE_TAP_RADIUS &&
|
|
Math.abs(touch0.y - touch1.y) < DOUBLE_TAP_RADIUS
|
|
);
|
|
},
|
|
_calculatePointsDistance = function(p1, p2) {
|
|
_tempPoint.x = Math.abs(p1.x - p2.x);
|
|
_tempPoint.y = Math.abs(p1.y - p2.y);
|
|
return Math.sqrt(
|
|
_tempPoint.x * _tempPoint.x + _tempPoint.y * _tempPoint.y
|
|
);
|
|
},
|
|
_stopDragUpdateLoop = function() {
|
|
if (_dragAnimFrame) {
|
|
_cancelAF(_dragAnimFrame);
|
|
_dragAnimFrame = null;
|
|
}
|
|
},
|
|
_dragUpdateLoop = function() {
|
|
if (_isDragging) {
|
|
_dragAnimFrame = _requestAF(_dragUpdateLoop);
|
|
_renderMovement();
|
|
}
|
|
},
|
|
_canPan = function() {
|
|
return !(
|
|
_options.scaleMode === "fit" &&
|
|
_currZoomLevel === self.currItem.initialZoomLevel
|
|
);
|
|
},
|
|
// find the closest parent DOM element
|
|
_closestElement = function(el, fn) {
|
|
if (!el || el === document) {
|
|
return false;
|
|
}
|
|
|
|
// don't search elements above pswp__scroll-wrap
|
|
if (
|
|
el.getAttribute("class") &&
|
|
el.getAttribute("class").indexOf("pswp__scroll-wrap") > -1
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if (fn(el)) {
|
|
return el;
|
|
}
|
|
|
|
return _closestElement(el.parentNode, fn);
|
|
},
|
|
_preventObj = {},
|
|
_preventDefaultEventBehaviour = function(e, isDown) {
|
|
_preventObj.prevent = !_closestElement(
|
|
e.target,
|
|
_options.isClickableElement
|
|
);
|
|
|
|
_shout("preventDragEvent", e, isDown, _preventObj);
|
|
return _preventObj.prevent;
|
|
},
|
|
_convertTouchToPoint = function(touch, p) {
|
|
p.x = touch.pageX;
|
|
p.y = touch.pageY;
|
|
p.id = touch.identifier;
|
|
return p;
|
|
},
|
|
_findCenterOfPoints = function(p1, p2, pCenter) {
|
|
pCenter.x = (p1.x + p2.x) * 0.5;
|
|
pCenter.y = (p1.y + p2.y) * 0.5;
|
|
},
|
|
_pushPosPoint = function(time, x, y) {
|
|
if (time - _gestureCheckSpeedTime > 50) {
|
|
var o = _posPoints.length > 2 ? _posPoints.shift() : {};
|
|
o.x = x;
|
|
o.y = y;
|
|
_posPoints.push(o);
|
|
_gestureCheckSpeedTime = time;
|
|
}
|
|
},
|
|
_calculateVerticalDragOpacityRatio = function() {
|
|
var yOffset = _panOffset.y - self.currItem.initialPosition.y; // difference between initial and current position
|
|
return 1 - Math.abs(yOffset / (_viewportSize.y / 2));
|
|
},
|
|
// points pool, reused during touch events
|
|
_ePoint1 = {},
|
|
_ePoint2 = {},
|
|
_tempPointsArr = [],
|
|
_tempCounter,
|
|
_getTouchPoints = function(e) {
|
|
// clean up previous points, without recreating array
|
|
while (_tempPointsArr.length > 0) {
|
|
_tempPointsArr.pop();
|
|
}
|
|
|
|
if (!_pointerEventEnabled) {
|
|
if (e.type.indexOf("touch") > -1) {
|
|
if (e.touches && e.touches.length > 0) {
|
|
_tempPointsArr[0] = _convertTouchToPoint(e.touches[0], _ePoint1);
|
|
if (e.touches.length > 1) {
|
|
_tempPointsArr[1] = _convertTouchToPoint(
|
|
e.touches[1],
|
|
_ePoint2
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
_ePoint1.x = e.pageX;
|
|
_ePoint1.y = e.pageY;
|
|
_ePoint1.id = "";
|
|
_tempPointsArr[0] = _ePoint1; //_ePoint1;
|
|
}
|
|
} else {
|
|
_tempCounter = 0;
|
|
// we can use forEach, as pointer events are supported only in modern browsers
|
|
_currPointers.forEach(function(p) {
|
|
if (_tempCounter === 0) {
|
|
_tempPointsArr[0] = p;
|
|
} else if (_tempCounter === 1) {
|
|
_tempPointsArr[1] = p;
|
|
}
|
|
_tempCounter++;
|
|
});
|
|
}
|
|
return _tempPointsArr;
|
|
},
|
|
_panOrMoveMainScroll = function(axis, delta) {
|
|
var panFriction,
|
|
overDiff = 0,
|
|
newOffset = _panOffset[axis] + delta[axis],
|
|
startOverDiff,
|
|
dir = delta[axis] > 0,
|
|
newMainScrollPosition = _mainScrollPos.x + delta.x,
|
|
mainScrollDiff = _mainScrollPos.x - _startMainScrollPos.x,
|
|
newPanPos,
|
|
newMainScrollPos;
|
|
|
|
// calculate fdistance over the bounds and friction
|
|
if (
|
|
newOffset > _currPanBounds.min[axis] ||
|
|
newOffset < _currPanBounds.max[axis]
|
|
) {
|
|
panFriction = _options.panEndFriction;
|
|
// Linear increasing of friction, so at 1/4 of viewport it's at max value.
|
|
// Looks not as nice as was expected. Left for history.
|
|
// panFriction = (1 - (_panOffset[axis] + delta[axis] + panBounds.min[axis]) / (_viewportSize[axis] / 4) );
|
|
} else {
|
|
panFriction = 1;
|
|
}
|
|
|
|
newOffset = _panOffset[axis] + delta[axis] * panFriction;
|
|
|
|
// move main scroll or start panning
|
|
if (
|
|
_options.allowPanToNext ||
|
|
_currZoomLevel === self.currItem.initialZoomLevel
|
|
) {
|
|
if (!_currZoomElementStyle) {
|
|
newMainScrollPos = newMainScrollPosition;
|
|
} else if (_direction === "h" && axis === "x" && !_zoomStarted) {
|
|
if (dir) {
|
|
if (newOffset > _currPanBounds.min[axis]) {
|
|
panFriction = _options.panEndFriction;
|
|
overDiff = _currPanBounds.min[axis] - newOffset;
|
|
startOverDiff =
|
|
_currPanBounds.min[axis] - _startPanOffset[axis];
|
|
}
|
|
|
|
// drag right
|
|
if (
|
|
(startOverDiff <= 0 || mainScrollDiff < 0) &&
|
|
_getNumItems() > 1
|
|
) {
|
|
newMainScrollPos = newMainScrollPosition;
|
|
if (
|
|
mainScrollDiff < 0 &&
|
|
newMainScrollPosition > _startMainScrollPos.x
|
|
) {
|
|
newMainScrollPos = _startMainScrollPos.x;
|
|
}
|
|
} else {
|
|
if (_currPanBounds.min.x !== _currPanBounds.max.x) {
|
|
newPanPos = newOffset;
|
|
}
|
|
}
|
|
} else {
|
|
if (newOffset < _currPanBounds.max[axis]) {
|
|
panFriction = _options.panEndFriction;
|
|
overDiff = newOffset - _currPanBounds.max[axis];
|
|
startOverDiff =
|
|
_startPanOffset[axis] - _currPanBounds.max[axis];
|
|
}
|
|
|
|
if (
|
|
(startOverDiff <= 0 || mainScrollDiff > 0) &&
|
|
_getNumItems() > 1
|
|
) {
|
|
newMainScrollPos = newMainScrollPosition;
|
|
|
|
if (
|
|
mainScrollDiff > 0 &&
|
|
newMainScrollPosition < _startMainScrollPos.x
|
|
) {
|
|
newMainScrollPos = _startMainScrollPos.x;
|
|
}
|
|
} else {
|
|
if (_currPanBounds.min.x !== _currPanBounds.max.x) {
|
|
newPanPos = newOffset;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
}
|
|
|
|
if (axis === "x") {
|
|
if (newMainScrollPos !== undefined) {
|
|
_moveMainScroll(newMainScrollPos, true);
|
|
if (newMainScrollPos === _startMainScrollPos.x) {
|
|
_mainScrollShifted = false;
|
|
} else {
|
|
_mainScrollShifted = true;
|
|
}
|
|
}
|
|
|
|
if (_currPanBounds.min.x !== _currPanBounds.max.x) {
|
|
if (newPanPos !== undefined) {
|
|
_panOffset.x = newPanPos;
|
|
} else if (!_mainScrollShifted) {
|
|
_panOffset.x += delta.x * panFriction;
|
|
}
|
|
}
|
|
|
|
return newMainScrollPos !== undefined;
|
|
}
|
|
}
|
|
|
|
if (!_mainScrollAnimating) {
|
|
if (!_mainScrollShifted) {
|
|
if (_currZoomLevel > self.currItem.fitRatio) {
|
|
_panOffset[axis] += delta[axis] * panFriction;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
// Pointerdown/touchstart/mousedown handler
|
|
_onDragStart = function(e) {
|
|
// Allow dragging only via left mouse button.
|
|
// As this handler is not added in IE8 - we ignore e.which
|
|
//
|
|
// http://www.quirksmode.org/js/events_properties.html
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/event.button
|
|
if (e.type === "mousedown" && e.button > 0) {
|
|
return;
|
|
}
|
|
|
|
if (_initialZoomRunning) {
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
|
|
if (_oldAndroidTouchEndTimeout && e.type === "mousedown") {
|
|
return;
|
|
}
|
|
|
|
if (_preventDefaultEventBehaviour(e, true)) {
|
|
e.preventDefault();
|
|
}
|
|
|
|
_shout("pointerDown");
|
|
|
|
if (_pointerEventEnabled) {
|
|
var pointerIndex = framework.arraySearch(
|
|
_currPointers,
|
|
e.pointerId,
|
|
"id"
|
|
);
|
|
if (pointerIndex < 0) {
|
|
pointerIndex = _currPointers.length;
|
|
}
|
|
_currPointers[pointerIndex] = {
|
|
x: e.pageX,
|
|
y: e.pageY,
|
|
id: e.pointerId
|
|
};
|
|
}
|
|
|
|
var startPointsList = _getTouchPoints(e),
|
|
numPoints = startPointsList.length;
|
|
|
|
_currentPoints = null;
|
|
|
|
_stopAllAnimations();
|
|
|
|
// init drag
|
|
if (!_isDragging || numPoints === 1) {
|
|
_isDragging = _isFirstMove = true;
|
|
framework.bind(window, _upMoveEvents, self);
|
|
|
|
_isZoomingIn = _wasOverInitialZoom = _opacityChanged = _verticalDragInitiated = _mainScrollShifted = _moved = _isMultitouch = _zoomStarted = false;
|
|
|
|
_direction = null;
|
|
|
|
_shout("firstTouchStart", startPointsList);
|
|
|
|
_equalizePoints(_startPanOffset, _panOffset);
|
|
|
|
_currPanDist.x = _currPanDist.y = 0;
|
|
_equalizePoints(_currPoint, startPointsList[0]);
|
|
_equalizePoints(_startPoint, _currPoint);
|
|
|
|
//_equalizePoints(_startMainScrollPos, _mainScrollPos);
|
|
_startMainScrollPos.x = _slideSize.x * _currPositionIndex;
|
|
|
|
_posPoints = [
|
|
{
|
|
x: _currPoint.x,
|
|
y: _currPoint.y
|
|
}
|
|
];
|
|
|
|
_gestureCheckSpeedTime = _gestureStartTime = _getCurrentTime();
|
|
|
|
//_mainScrollAnimationEnd(true);
|
|
_calculatePanBounds(_currZoomLevel, true);
|
|
|
|
// Start rendering
|
|
_stopDragUpdateLoop();
|
|
_dragUpdateLoop();
|
|
}
|
|
|
|
// init zoom
|
|
if (
|
|
!_isZooming &&
|
|
numPoints > 1 &&
|
|
!_mainScrollAnimating &&
|
|
!_mainScrollShifted
|
|
) {
|
|
_startZoomLevel = _currZoomLevel;
|
|
_zoomStarted = false; // true if zoom changed at least once
|
|
|
|
_isZooming = _isMultitouch = true;
|
|
_currPanDist.y = _currPanDist.x = 0;
|
|
|
|
_equalizePoints(_startPanOffset, _panOffset);
|
|
|
|
_equalizePoints(p, startPointsList[0]);
|
|
_equalizePoints(p2, startPointsList[1]);
|
|
|
|
_findCenterOfPoints(p, p2, _currCenterPoint);
|
|
|
|
_midZoomPoint.x = Math.abs(_currCenterPoint.x) - _panOffset.x;
|
|
_midZoomPoint.y = Math.abs(_currCenterPoint.y) - _panOffset.y;
|
|
_currPointsDistance = _startPointsDistance = _calculatePointsDistance(
|
|
p,
|
|
p2
|
|
);
|
|
}
|
|
},
|
|
// Pointermove/touchmove/mousemove handler
|
|
_onDragMove = function(e) {
|
|
e.preventDefault();
|
|
|
|
if (_pointerEventEnabled) {
|
|
var pointerIndex = framework.arraySearch(
|
|
_currPointers,
|
|
e.pointerId,
|
|
"id"
|
|
);
|
|
if (pointerIndex > -1) {
|
|
var p = _currPointers[pointerIndex];
|
|
p.x = e.pageX;
|
|
p.y = e.pageY;
|
|
}
|
|
}
|
|
|
|
if (_isDragging) {
|
|
var touchesList = _getTouchPoints(e);
|
|
if (!_direction && !_moved && !_isZooming) {
|
|
if (_mainScrollPos.x !== _slideSize.x * _currPositionIndex) {
|
|
// if main scroll position is shifted – direction is always horizontal
|
|
_direction = "h";
|
|
} else {
|
|
var diff =
|
|
Math.abs(touchesList[0].x - _currPoint.x) -
|
|
Math.abs(touchesList[0].y - _currPoint.y);
|
|
// check the direction of movement
|
|
if (Math.abs(diff) >= DIRECTION_CHECK_OFFSET) {
|
|
_direction = diff > 0 ? "h" : "v";
|
|
_currentPoints = touchesList;
|
|
}
|
|
}
|
|
} else {
|
|
_currentPoints = touchesList;
|
|
}
|
|
}
|
|
},
|
|
//
|
|
_renderMovement = function() {
|
|
if (!_currentPoints) {
|
|
return;
|
|
}
|
|
|
|
var numPoints = _currentPoints.length;
|
|
|
|
if (numPoints === 0) {
|
|
return;
|
|
}
|
|
|
|
_equalizePoints(p, _currentPoints[0]);
|
|
|
|
delta.x = p.x - _currPoint.x;
|
|
delta.y = p.y - _currPoint.y;
|
|
|
|
if (_isZooming && numPoints > 1) {
|
|
// Handle behaviour for more than 1 point
|
|
|
|
_currPoint.x = p.x;
|
|
_currPoint.y = p.y;
|
|
|
|
// check if one of two points changed
|
|
if (!delta.x && !delta.y && _isEqualPoints(_currentPoints[1], p2)) {
|
|
return;
|
|
}
|
|
|
|
_equalizePoints(p2, _currentPoints[1]);
|
|
|
|
if (!_zoomStarted) {
|
|
_zoomStarted = true;
|
|
_shout("zoomGestureStarted");
|
|
}
|
|
|
|
// Distance between two points
|
|
var pointsDistance = _calculatePointsDistance(p, p2);
|
|
|
|
var zoomLevel = _calculateZoomLevel(pointsDistance);
|
|
|
|
// slightly over the of initial zoom level
|
|
if (
|
|
zoomLevel >
|
|
self.currItem.initialZoomLevel + self.currItem.initialZoomLevel / 15
|
|
) {
|
|
_wasOverInitialZoom = true;
|
|
}
|
|
|
|
// Apply the friction if zoom level is out of the bounds
|
|
var zoomFriction = 1,
|
|
minZoomLevel = _getMinZoomLevel(),
|
|
maxZoomLevel = _getMaxZoomLevel();
|
|
|
|
if (zoomLevel < minZoomLevel) {
|
|
if (
|
|
_options.pinchToClose &&
|
|
!_wasOverInitialZoom &&
|
|
_startZoomLevel <= self.currItem.initialZoomLevel
|
|
) {
|
|
// fade out background if zooming out
|
|
var minusDiff = minZoomLevel - zoomLevel;
|
|
var percent = 1 - minusDiff / (minZoomLevel / 1.2);
|
|
|
|
_applyBgOpacity(percent);
|
|
_shout("onPinchClose", percent);
|
|
_opacityChanged = true;
|
|
} else {
|
|
zoomFriction = (minZoomLevel - zoomLevel) / minZoomLevel;
|
|
if (zoomFriction > 1) {
|
|
zoomFriction = 1;
|
|
}
|
|
zoomLevel = minZoomLevel - zoomFriction * (minZoomLevel / 3);
|
|
}
|
|
} else if (zoomLevel > maxZoomLevel) {
|
|
// 1.5 - extra zoom level above the max. E.g. if max is x6, real max 6 + 1.5 = 7.5
|
|
zoomFriction = (zoomLevel - maxZoomLevel) / (minZoomLevel * 6);
|
|
if (zoomFriction > 1) {
|
|
zoomFriction = 1;
|
|
}
|
|
zoomLevel = maxZoomLevel + zoomFriction * minZoomLevel;
|
|
}
|
|
|
|
if (zoomFriction < 0) {
|
|
zoomFriction = 0;
|
|
}
|
|
|
|
// distance between touch points after friction is applied
|
|
_currPointsDistance = pointsDistance;
|
|
|
|
// _centerPoint - The point in the middle of two pointers
|
|
_findCenterOfPoints(p, p2, _centerPoint);
|
|
|
|
// paning with two pointers pressed
|
|
_currPanDist.x += _centerPoint.x - _currCenterPoint.x;
|
|
_currPanDist.y += _centerPoint.y - _currCenterPoint.y;
|
|
_equalizePoints(_currCenterPoint, _centerPoint);
|
|
|
|
_panOffset.x = _calculatePanOffset("x", zoomLevel);
|
|
_panOffset.y = _calculatePanOffset("y", zoomLevel);
|
|
|
|
_isZoomingIn = zoomLevel > _currZoomLevel;
|
|
_currZoomLevel = zoomLevel;
|
|
_applyCurrentZoomPan();
|
|
} else {
|
|
// handle behaviour for one point (dragging or panning)
|
|
|
|
if (!_direction) {
|
|
return;
|
|
}
|
|
|
|
if (_isFirstMove) {
|
|
_isFirstMove = false;
|
|
|
|
// subtract drag distance that was used during the detection direction
|
|
|
|
if (Math.abs(delta.x) >= DIRECTION_CHECK_OFFSET) {
|
|
delta.x -= _currentPoints[0].x - _startPoint.x;
|
|
}
|
|
|
|
if (Math.abs(delta.y) >= DIRECTION_CHECK_OFFSET) {
|
|
delta.y -= _currentPoints[0].y - _startPoint.y;
|
|
}
|
|
}
|
|
|
|
_currPoint.x = p.x;
|
|
_currPoint.y = p.y;
|
|
|
|
// do nothing if pointers position hasn't changed
|
|
if (delta.x === 0 && delta.y === 0) {
|
|
return;
|
|
}
|
|
|
|
if (_direction === "v" && _options.closeOnVerticalDrag) {
|
|
if (!_canPan()) {
|
|
_currPanDist.y += delta.y;
|
|
_panOffset.y += delta.y;
|
|
|
|
var opacityRatio = _calculateVerticalDragOpacityRatio();
|
|
|
|
_verticalDragInitiated = true;
|
|
_shout("onVerticalDrag", opacityRatio);
|
|
|
|
_applyBgOpacity(opacityRatio);
|
|
_applyCurrentZoomPan();
|
|
return;
|
|
}
|
|
}
|
|
|
|
_pushPosPoint(_getCurrentTime(), p.x, p.y);
|
|
|
|
_moved = true;
|
|
_currPanBounds = self.currItem.bounds;
|
|
|
|
var mainScrollChanged = _panOrMoveMainScroll("x", delta);
|
|
if (!mainScrollChanged) {
|
|
_panOrMoveMainScroll("y", delta);
|
|
|
|
_roundPoint(_panOffset);
|
|
_applyCurrentZoomPan();
|
|
}
|
|
}
|
|
},
|
|
// Pointerup/pointercancel/touchend/touchcancel/mouseup event handler
|
|
_onDragRelease = function(e) {
|
|
if (_features.isOldAndroid) {
|
|
if (_oldAndroidTouchEndTimeout && e.type === "mouseup") {
|
|
return;
|
|
}
|
|
|
|
// on Android (v4.1, 4.2, 4.3 & possibly older)
|
|
// ghost mousedown/up event isn't preventable via e.preventDefault,
|
|
// which causes fake mousedown event
|
|
// so we block mousedown/up for 600ms
|
|
if (e.type.indexOf("touch") > -1) {
|
|
clearTimeout(_oldAndroidTouchEndTimeout);
|
|
_oldAndroidTouchEndTimeout = setTimeout(function() {
|
|
_oldAndroidTouchEndTimeout = 0;
|
|
}, 600);
|
|
}
|
|
}
|
|
|
|
_shout("pointerUp");
|
|
|
|
if (_preventDefaultEventBehaviour(e, false)) {
|
|
e.preventDefault();
|
|
}
|
|
|
|
var releasePoint;
|
|
|
|
if (_pointerEventEnabled) {
|
|
var pointerIndex = framework.arraySearch(
|
|
_currPointers,
|
|
e.pointerId,
|
|
"id"
|
|
);
|
|
|
|
if (pointerIndex > -1) {
|
|
releasePoint = _currPointers.splice(pointerIndex, 1)[0];
|
|
|
|
if (navigator.msPointerEnabled) {
|
|
var MSPOINTER_TYPES = {
|
|
4: "mouse", // event.MSPOINTER_TYPE_MOUSE
|
|
2: "touch", // event.MSPOINTER_TYPE_TOUCH
|
|
3: "pen" // event.MSPOINTER_TYPE_PEN
|
|
};
|
|
releasePoint.type = MSPOINTER_TYPES[e.pointerType];
|
|
|
|
if (!releasePoint.type) {
|
|
releasePoint.type = e.pointerType || "mouse";
|
|
}
|
|
} else {
|
|
releasePoint.type = e.pointerType || "mouse";
|
|
}
|
|
}
|
|
}
|
|
|
|
var touchList = _getTouchPoints(e),
|
|
gestureType,
|
|
numPoints = touchList.length;
|
|
|
|
if (e.type === "mouseup") {
|
|
numPoints = 0;
|
|
}
|
|
|
|
// Do nothing if there were 3 touch points or more
|
|
if (numPoints === 2) {
|
|
_currentPoints = null;
|
|
return true;
|
|
}
|
|
|
|
// if second pointer released
|
|
if (numPoints === 1) {
|
|
_equalizePoints(_startPoint, touchList[0]);
|
|
}
|
|
|
|
// pointer hasn't moved, send "tap release" point
|
|
if (numPoints === 0 && !_direction && !_mainScrollAnimating) {
|
|
if (!releasePoint) {
|
|
if (e.type === "mouseup") {
|
|
releasePoint = { x: e.pageX, y: e.pageY, type: "mouse" };
|
|
} else if (e.changedTouches && e.changedTouches[0]) {
|
|
releasePoint = {
|
|
x: e.changedTouches[0].pageX,
|
|
y: e.changedTouches[0].pageY,
|
|
type: "touch"
|
|
};
|
|
}
|
|
}
|
|
|
|
_shout("touchRelease", e, releasePoint);
|
|
}
|
|
|
|
// Difference in time between releasing of two last touch points (zoom gesture)
|
|
var releaseTimeDiff = -1;
|
|
|
|
// Gesture completed, no pointers left
|
|
if (numPoints === 0) {
|
|
_isDragging = false;
|
|
framework.unbind(window, _upMoveEvents, self);
|
|
|
|
_stopDragUpdateLoop();
|
|
|
|
if (_isZooming) {
|
|
// Two points released at the same time
|
|
releaseTimeDiff = 0;
|
|
} else if (_lastReleaseTime !== -1) {
|
|
releaseTimeDiff = _getCurrentTime() - _lastReleaseTime;
|
|
}
|
|
}
|
|
_lastReleaseTime = numPoints === 1 ? _getCurrentTime() : -1;
|
|
|
|
if (releaseTimeDiff !== -1 && releaseTimeDiff < 150) {
|
|
gestureType = "zoom";
|
|
} else {
|
|
gestureType = "swipe";
|
|
}
|
|
|
|
if (_isZooming && numPoints < 2) {
|
|
_isZooming = false;
|
|
|
|
// Only second point released
|
|
if (numPoints === 1) {
|
|
gestureType = "zoomPointerUp";
|
|
}
|
|
_shout("zoomGestureEnded");
|
|
}
|
|
|
|
_currentPoints = null;
|
|
if (
|
|
!_moved &&
|
|
!_zoomStarted &&
|
|
!_mainScrollAnimating &&
|
|
!_verticalDragInitiated
|
|
) {
|
|
// nothing to animate
|
|
return;
|
|
}
|
|
|
|
_stopAllAnimations();
|
|
|
|
if (!_releaseAnimData) {
|
|
_releaseAnimData = _initDragReleaseAnimationData();
|
|
}
|
|
|
|
_releaseAnimData.calculateSwipeSpeed("x");
|
|
|
|
if (_verticalDragInitiated) {
|
|
var opacityRatio = _calculateVerticalDragOpacityRatio();
|
|
|
|
if (opacityRatio < _options.verticalDragRange) {
|
|
self.close();
|
|
} else {
|
|
var initalPanY = _panOffset.y,
|
|
initialBgOpacity = _bgOpacity;
|
|
|
|
_animateProp(
|
|
"verticalDrag",
|
|
0,
|
|
1,
|
|
300,
|
|
framework.easing.cubic.out,
|
|
function(now) {
|
|
_panOffset.y =
|
|
(self.currItem.initialPosition.y - initalPanY) * now +
|
|
initalPanY;
|
|
|
|
_applyBgOpacity(
|
|
(1 - initialBgOpacity) * now + initialBgOpacity
|
|
);
|
|
_applyCurrentZoomPan();
|
|
}
|
|
);
|
|
|
|
_shout("onVerticalDrag", 1);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// main scroll
|
|
if ((_mainScrollShifted || _mainScrollAnimating) && numPoints === 0) {
|
|
var itemChanged = _finishSwipeMainScrollGesture(
|
|
gestureType,
|
|
_releaseAnimData
|
|
);
|
|
if (itemChanged) {
|
|
return;
|
|
}
|
|
gestureType = "zoomPointerUp";
|
|
}
|
|
|
|
// prevent zoom/pan animation when main scroll animation runs
|
|
if (_mainScrollAnimating) {
|
|
return;
|
|
}
|
|
|
|
// Complete simple zoom gesture (reset zoom level if it's out of the bounds)
|
|
if (gestureType !== "swipe") {
|
|
_completeZoomGesture();
|
|
return;
|
|
}
|
|
|
|
// Complete pan gesture if main scroll is not shifted, and it's possible to pan current image
|
|
if (!_mainScrollShifted && _currZoomLevel > self.currItem.fitRatio) {
|
|
_completePanGesture(_releaseAnimData);
|
|
}
|
|
},
|
|
// Returns object with data about gesture
|
|
// It's created only once and then reused
|
|
_initDragReleaseAnimationData = function() {
|
|
// temp local vars
|
|
var lastFlickDuration, tempReleasePos;
|
|
|
|
// s = this
|
|
var s = {
|
|
lastFlickOffset: {},
|
|
lastFlickDist: {},
|
|
lastFlickSpeed: {},
|
|
slowDownRatio: {},
|
|
slowDownRatioReverse: {},
|
|
speedDecelerationRatio: {},
|
|
speedDecelerationRatioAbs: {},
|
|
distanceOffset: {},
|
|
backAnimDestination: {},
|
|
backAnimStarted: {},
|
|
calculateSwipeSpeed: function(axis) {
|
|
if (_posPoints.length > 1) {
|
|
lastFlickDuration =
|
|
_getCurrentTime() - _gestureCheckSpeedTime + 50;
|
|
tempReleasePos = _posPoints[_posPoints.length - 2][axis];
|
|
} else {
|
|
lastFlickDuration = _getCurrentTime() - _gestureStartTime; // total gesture duration
|
|
tempReleasePos = _startPoint[axis];
|
|
}
|
|
s.lastFlickOffset[axis] = _currPoint[axis] - tempReleasePos;
|
|
s.lastFlickDist[axis] = Math.abs(s.lastFlickOffset[axis]);
|
|
if (s.lastFlickDist[axis] > 20) {
|
|
s.lastFlickSpeed[axis] =
|
|
s.lastFlickOffset[axis] / lastFlickDuration;
|
|
} else {
|
|
s.lastFlickSpeed[axis] = 0;
|
|
}
|
|
if (Math.abs(s.lastFlickSpeed[axis]) < 0.1) {
|
|
s.lastFlickSpeed[axis] = 0;
|
|
}
|
|
|
|
s.slowDownRatio[axis] = 0.95;
|
|
s.slowDownRatioReverse[axis] = 1 - s.slowDownRatio[axis];
|
|
s.speedDecelerationRatio[axis] = 1;
|
|
},
|
|
|
|
calculateOverBoundsAnimOffset: function(axis, speed) {
|
|
if (!s.backAnimStarted[axis]) {
|
|
if (_panOffset[axis] > _currPanBounds.min[axis]) {
|
|
s.backAnimDestination[axis] = _currPanBounds.min[axis];
|
|
} else if (_panOffset[axis] < _currPanBounds.max[axis]) {
|
|
s.backAnimDestination[axis] = _currPanBounds.max[axis];
|
|
}
|
|
|
|
if (s.backAnimDestination[axis] !== undefined) {
|
|
s.slowDownRatio[axis] = 0.7;
|
|
s.slowDownRatioReverse[axis] = 1 - s.slowDownRatio[axis];
|
|
if (s.speedDecelerationRatioAbs[axis] < 0.05) {
|
|
s.lastFlickSpeed[axis] = 0;
|
|
s.backAnimStarted[axis] = true;
|
|
|
|
_animateProp(
|
|
"bounceZoomPan" + axis,
|
|
_panOffset[axis],
|
|
s.backAnimDestination[axis],
|
|
speed || 300,
|
|
framework.easing.sine.out,
|
|
function(pos) {
|
|
_panOffset[axis] = pos;
|
|
_applyCurrentZoomPan();
|
|
}
|
|
);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
// Reduces the speed by slowDownRatio (per 10ms)
|
|
calculateAnimOffset: function(axis) {
|
|
if (!s.backAnimStarted[axis]) {
|
|
s.speedDecelerationRatio[axis] =
|
|
s.speedDecelerationRatio[axis] *
|
|
(s.slowDownRatio[axis] +
|
|
s.slowDownRatioReverse[axis] -
|
|
(s.slowDownRatioReverse[axis] * s.timeDiff) / 10);
|
|
|
|
s.speedDecelerationRatioAbs[axis] = Math.abs(
|
|
s.lastFlickSpeed[axis] * s.speedDecelerationRatio[axis]
|
|
);
|
|
s.distanceOffset[axis] =
|
|
s.lastFlickSpeed[axis] *
|
|
s.speedDecelerationRatio[axis] *
|
|
s.timeDiff;
|
|
_panOffset[axis] += s.distanceOffset[axis];
|
|
}
|
|
},
|
|
|
|
panAnimLoop: function() {
|
|
if (_animations.zoomPan) {
|
|
_animations.zoomPan.raf = _requestAF(s.panAnimLoop);
|
|
|
|
s.now = _getCurrentTime();
|
|
s.timeDiff = s.now - s.lastNow;
|
|
s.lastNow = s.now;
|
|
|
|
s.calculateAnimOffset("x");
|
|
s.calculateAnimOffset("y");
|
|
|
|
_applyCurrentZoomPan();
|
|
|
|
s.calculateOverBoundsAnimOffset("x");
|
|
s.calculateOverBoundsAnimOffset("y");
|
|
|
|
if (
|
|
s.speedDecelerationRatioAbs.x < 0.05 &&
|
|
s.speedDecelerationRatioAbs.y < 0.05
|
|
) {
|
|
// round pan position
|
|
_panOffset.x = Math.round(_panOffset.x);
|
|
_panOffset.y = Math.round(_panOffset.y);
|
|
_applyCurrentZoomPan();
|
|
|
|
_stopAnimation("zoomPan");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
return s;
|
|
},
|
|
_completePanGesture = function(animData) {
|
|
// calculate swipe speed for Y axis (paanning)
|
|
animData.calculateSwipeSpeed("y");
|
|
|
|
_currPanBounds = self.currItem.bounds;
|
|
|
|
animData.backAnimDestination = {};
|
|
animData.backAnimStarted = {};
|
|
|
|
// Avoid acceleration animation if speed is too low
|
|
if (
|
|
Math.abs(animData.lastFlickSpeed.x) <= 0.05 &&
|
|
Math.abs(animData.lastFlickSpeed.y) <= 0.05
|
|
) {
|
|
animData.speedDecelerationRatioAbs.x = animData.speedDecelerationRatioAbs.y = 0;
|
|
|
|
// Run pan drag release animation. E.g. if you drag image and release finger without momentum.
|
|
animData.calculateOverBoundsAnimOffset("x");
|
|
animData.calculateOverBoundsAnimOffset("y");
|
|
return true;
|
|
}
|
|
|
|
// Animation loop that controls the acceleration after pan gesture ends
|
|
_registerStartAnimation("zoomPan");
|
|
animData.lastNow = _getCurrentTime();
|
|
animData.panAnimLoop();
|
|
},
|
|
_finishSwipeMainScrollGesture = function(gestureType, _releaseAnimData) {
|
|
var itemChanged;
|
|
if (!_mainScrollAnimating) {
|
|
_currZoomedItemIndex = _currentItemIndex;
|
|
}
|
|
|
|
var itemsDiff;
|
|
|
|
if (gestureType === "swipe") {
|
|
var totalShiftDist = _currPoint.x - _startPoint.x,
|
|
isFastLastFlick = _releaseAnimData.lastFlickDist.x < 10;
|
|
|
|
// if container is shifted for more than MIN_SWIPE_DISTANCE,
|
|
// and last flick gesture was in right direction
|
|
if (
|
|
totalShiftDist > MIN_SWIPE_DISTANCE &&
|
|
(isFastLastFlick || _releaseAnimData.lastFlickOffset.x > 20)
|
|
) {
|
|
// go to prev item
|
|
itemsDiff = -1;
|
|
} else if (
|
|
totalShiftDist < -MIN_SWIPE_DISTANCE &&
|
|
(isFastLastFlick || _releaseAnimData.lastFlickOffset.x < -20)
|
|
) {
|
|
// go to next item
|
|
itemsDiff = 1;
|
|
}
|
|
}
|
|
|
|
var nextCircle;
|
|
|
|
if (itemsDiff) {
|
|
_currentItemIndex += itemsDiff;
|
|
|
|
if (_currentItemIndex < 0) {
|
|
_currentItemIndex = _options.loop ? _getNumItems() - 1 : 0;
|
|
nextCircle = true;
|
|
} else if (_currentItemIndex >= _getNumItems()) {
|
|
_currentItemIndex = _options.loop ? 0 : _getNumItems() - 1;
|
|
nextCircle = true;
|
|
}
|
|
|
|
if (!nextCircle || _options.loop) {
|
|
_indexDiff += itemsDiff;
|
|
_currPositionIndex -= itemsDiff;
|
|
itemChanged = true;
|
|
}
|
|
}
|
|
|
|
var animateToX = _slideSize.x * _currPositionIndex;
|
|
var animateToDist = Math.abs(animateToX - _mainScrollPos.x);
|
|
var finishAnimDuration;
|
|
|
|
if (
|
|
!itemChanged &&
|
|
animateToX > _mainScrollPos.x !==
|
|
_releaseAnimData.lastFlickSpeed.x > 0
|
|
) {
|
|
// "return to current" duration, e.g. when dragging from slide 0 to -1
|
|
finishAnimDuration = 333;
|
|
} else {
|
|
finishAnimDuration =
|
|
Math.abs(_releaseAnimData.lastFlickSpeed.x) > 0
|
|
? animateToDist / Math.abs(_releaseAnimData.lastFlickSpeed.x)
|
|
: 333;
|
|
|
|
finishAnimDuration = Math.min(finishAnimDuration, 400);
|
|
finishAnimDuration = Math.max(finishAnimDuration, 250);
|
|
}
|
|
|
|
if (_currZoomedItemIndex === _currentItemIndex) {
|
|
itemChanged = false;
|
|
}
|
|
|
|
_mainScrollAnimating = true;
|
|
|
|
_shout("mainScrollAnimStart");
|
|
|
|
_animateProp(
|
|
"mainScroll",
|
|
_mainScrollPos.x,
|
|
animateToX,
|
|
finishAnimDuration,
|
|
framework.easing.cubic.out,
|
|
_moveMainScroll,
|
|
function() {
|
|
_stopAllAnimations();
|
|
_mainScrollAnimating = false;
|
|
_currZoomedItemIndex = -1;
|
|
|
|
if (itemChanged || _currZoomedItemIndex !== _currentItemIndex) {
|
|
self.updateCurrItem();
|
|
}
|
|
|
|
_shout("mainScrollAnimComplete");
|
|
}
|
|
);
|
|
|
|
if (itemChanged) {
|
|
self.updateCurrItem(true);
|
|
}
|
|
|
|
return itemChanged;
|
|
},
|
|
_calculateZoomLevel = function(touchesDistance) {
|
|
return (1 / _startPointsDistance) * touchesDistance * _startZoomLevel;
|
|
},
|
|
// Resets zoom if it's out of bounds
|
|
_completeZoomGesture = function() {
|
|
var destZoomLevel = _currZoomLevel,
|
|
minZoomLevel = _getMinZoomLevel(),
|
|
maxZoomLevel = _getMaxZoomLevel();
|
|
|
|
if (_currZoomLevel < minZoomLevel) {
|
|
destZoomLevel = minZoomLevel;
|
|
} else if (_currZoomLevel > maxZoomLevel) {
|
|
destZoomLevel = maxZoomLevel;
|
|
}
|
|
|
|
var destOpacity = 1,
|
|
onUpdate,
|
|
initialOpacity = _bgOpacity;
|
|
|
|
if (
|
|
_opacityChanged &&
|
|
!_isZoomingIn &&
|
|
!_wasOverInitialZoom &&
|
|
_currZoomLevel < minZoomLevel
|
|
) {
|
|
//_closedByScroll = true;
|
|
self.close();
|
|
return true;
|
|
}
|
|
|
|
if (_opacityChanged) {
|
|
onUpdate = function(now) {
|
|
_applyBgOpacity(
|
|
(destOpacity - initialOpacity) * now + initialOpacity
|
|
);
|
|
};
|
|
}
|
|
|
|
self.zoomTo(
|
|
destZoomLevel,
|
|
0,
|
|
200,
|
|
framework.easing.cubic.out,
|
|
onUpdate
|
|
);
|
|
return true;
|
|
};
|
|
|
|
_registerModule("Gestures", {
|
|
publicMethods: {
|
|
initGestures: function() {
|
|
// helper function that builds touch/pointer/mouse events
|
|
var addEventNames = function(pref, down, move, up, cancel) {
|
|
_dragStartEvent = pref + down;
|
|
_dragMoveEvent = pref + move;
|
|
_dragEndEvent = pref + up;
|
|
if (cancel) {
|
|
_dragCancelEvent = pref + cancel;
|
|
} else {
|
|
_dragCancelEvent = "";
|
|
}
|
|
};
|
|
|
|
_pointerEventEnabled = _features.pointerEvent;
|
|
if (_pointerEventEnabled && _features.touch) {
|
|
// we don't need touch events, if browser supports pointer events
|
|
_features.touch = false;
|
|
}
|
|
|
|
if (_pointerEventEnabled) {
|
|
if (navigator.msPointerEnabled) {
|
|
// IE10 pointer events are case-sensitive
|
|
addEventNames("MSPointer", "Down", "Move", "Up", "Cancel");
|
|
} else {
|
|
addEventNames("pointer", "down", "move", "up", "cancel");
|
|
}
|
|
} else if (_features.touch) {
|
|
addEventNames("touch", "start", "move", "end", "cancel");
|
|
_likelyTouchDevice = true;
|
|
} else {
|
|
addEventNames("mouse", "down", "move", "up");
|
|
}
|
|
|
|
_upMoveEvents =
|
|
_dragMoveEvent + " " + _dragEndEvent + " " + _dragCancelEvent;
|
|
_downEvents = _dragStartEvent;
|
|
|
|
if (_pointerEventEnabled && !_likelyTouchDevice) {
|
|
_likelyTouchDevice =
|
|
navigator.maxTouchPoints > 1 || navigator.msMaxTouchPoints > 1;
|
|
}
|
|
// make variable public
|
|
self.likelyTouchDevice = _likelyTouchDevice;
|
|
|
|
_globalEventHandlers[_dragStartEvent] = _onDragStart;
|
|
_globalEventHandlers[_dragMoveEvent] = _onDragMove;
|
|
_globalEventHandlers[_dragEndEvent] = _onDragRelease; // the Kraken
|
|
|
|
if (_dragCancelEvent) {
|
|
_globalEventHandlers[_dragCancelEvent] =
|
|
_globalEventHandlers[_dragEndEvent];
|
|
}
|
|
|
|
// Bind mouse events on device with detected hardware touch support, in case it supports multiple types of input.
|
|
if (_features.touch) {
|
|
_downEvents += " mousedown";
|
|
_upMoveEvents += " mousemove mouseup";
|
|
_globalEventHandlers.mousedown =
|
|
_globalEventHandlers[_dragStartEvent];
|
|
_globalEventHandlers.mousemove =
|
|
_globalEventHandlers[_dragMoveEvent];
|
|
_globalEventHandlers.mouseup = _globalEventHandlers[_dragEndEvent];
|
|
}
|
|
|
|
if (!_likelyTouchDevice) {
|
|
// don't allow pan to next slide from zoomed state on Desktop
|
|
_options.allowPanToNext = false;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
/*>>gestures*/
|
|
|
|
/*>>show-hide-transition*/
|
|
/**
|
|
* show-hide-transition.js:
|
|
*
|
|
* Manages initial opening or closing transition.
|
|
*
|
|
* If you're not planning to use transition for gallery at all,
|
|
* you may set options hideAnimationDuration and showAnimationDuration to 0,
|
|
* and just delete startAnimation function.
|
|
*
|
|
*/
|
|
|
|
var _showOrHideTimeout,
|
|
_showOrHide = function(item, img, out, completeFn) {
|
|
if (_showOrHideTimeout) {
|
|
clearTimeout(_showOrHideTimeout);
|
|
}
|
|
|
|
_initialZoomRunning = true;
|
|
_initialContentSet = true;
|
|
|
|
// dimensions of small thumbnail {x:,y:,w:}.
|
|
// Height is optional, as calculated based on large image.
|
|
var thumbBounds;
|
|
if (item.initialLayout) {
|
|
thumbBounds = item.initialLayout;
|
|
item.initialLayout = null;
|
|
} else {
|
|
thumbBounds =
|
|
_options.getThumbBoundsFn &&
|
|
_options.getThumbBoundsFn(_currentItemIndex);
|
|
}
|
|
|
|
var duration = out
|
|
? _options.hideAnimationDuration
|
|
: _options.showAnimationDuration;
|
|
|
|
var onComplete = function() {
|
|
_stopAnimation("initialZoom");
|
|
if (!out) {
|
|
_applyBgOpacity(1);
|
|
if (img) {
|
|
img.style.display = "block";
|
|
}
|
|
framework.addClass(template, "pswp--animated-in");
|
|
_shout("initialZoom" + (out ? "OutEnd" : "InEnd"));
|
|
} else {
|
|
self.template.removeAttribute("style");
|
|
self.bg.removeAttribute("style");
|
|
}
|
|
|
|
if (completeFn) {
|
|
completeFn();
|
|
}
|
|
_initialZoomRunning = false;
|
|
};
|
|
|
|
// if bounds aren't provided, just open gallery without animation
|
|
if (!duration || !thumbBounds || thumbBounds.x === undefined) {
|
|
_shout("initialZoom" + (out ? "Out" : "In"));
|
|
|
|
_currZoomLevel = item.initialZoomLevel;
|
|
_equalizePoints(_panOffset, item.initialPosition);
|
|
_applyCurrentZoomPan();
|
|
|
|
template.style.opacity = out ? 0 : 1;
|
|
_applyBgOpacity(1);
|
|
|
|
if (duration) {
|
|
setTimeout(function() {
|
|
onComplete();
|
|
}, duration);
|
|
} else {
|
|
onComplete();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
var startAnimation = function() {
|
|
var closeWithRaf = _closedByScroll,
|
|
fadeEverything =
|
|
!self.currItem.src ||
|
|
self.currItem.loadError ||
|
|
_options.showHideOpacity;
|
|
|
|
// apply hw-acceleration to image
|
|
if (item.miniImg) {
|
|
item.miniImg.style.webkitBackfaceVisibility = "hidden";
|
|
}
|
|
|
|
if (!out) {
|
|
_currZoomLevel = thumbBounds.w / item.w;
|
|
_panOffset.x = thumbBounds.x;
|
|
_panOffset.y = thumbBounds.y - _initalWindowScrollY;
|
|
|
|
self[fadeEverything ? "template" : "bg"].style.opacity = 0.001;
|
|
_applyCurrentZoomPan();
|
|
}
|
|
|
|
_registerStartAnimation("initialZoom");
|
|
|
|
if (out && !closeWithRaf) {
|
|
framework.removeClass(template, "pswp--animated-in");
|
|
}
|
|
|
|
if (fadeEverything) {
|
|
if (out) {
|
|
framework[(closeWithRaf ? "remove" : "add") + "Class"](
|
|
template,
|
|
"pswp--animate_opacity"
|
|
);
|
|
} else {
|
|
setTimeout(function() {
|
|
framework.addClass(template, "pswp--animate_opacity");
|
|
}, 30);
|
|
}
|
|
}
|
|
|
|
_showOrHideTimeout = setTimeout(
|
|
function() {
|
|
_shout("initialZoom" + (out ? "Out" : "In"));
|
|
|
|
if (!out) {
|
|
// "in" animation always uses CSS transitions (instead of rAF).
|
|
// CSS transition work faster here,
|
|
// as developer may also want to animate other things,
|
|
// like ui on top of sliding area, which can be animated just via CSS
|
|
|
|
_currZoomLevel = item.initialZoomLevel;
|
|
_equalizePoints(_panOffset, item.initialPosition);
|
|
_applyCurrentZoomPan();
|
|
_applyBgOpacity(1);
|
|
|
|
if (fadeEverything) {
|
|
template.style.opacity = 1;
|
|
} else {
|
|
_applyBgOpacity(1);
|
|
}
|
|
|
|
_showOrHideTimeout = setTimeout(onComplete, duration + 20);
|
|
} else {
|
|
// "out" animation uses rAF only when PhotoSwipe is closed by browser scroll, to recalculate position
|
|
var destZoomLevel = thumbBounds.w / item.w,
|
|
initialPanOffset = {
|
|
x: _panOffset.x,
|
|
y: _panOffset.y
|
|
},
|
|
initialZoomLevel = _currZoomLevel,
|
|
initalBgOpacity = _bgOpacity,
|
|
onUpdate = function(now) {
|
|
if (now === 1) {
|
|
_currZoomLevel = destZoomLevel;
|
|
_panOffset.x = thumbBounds.x;
|
|
_panOffset.y = thumbBounds.y - _currentWindowScrollY;
|
|
} else {
|
|
_currZoomLevel =
|
|
(destZoomLevel - initialZoomLevel) * now +
|
|
initialZoomLevel;
|
|
_panOffset.x =
|
|
(thumbBounds.x - initialPanOffset.x) * now +
|
|
initialPanOffset.x;
|
|
_panOffset.y =
|
|
(thumbBounds.y -
|
|
_currentWindowScrollY -
|
|
initialPanOffset.y) *
|
|
now +
|
|
initialPanOffset.y;
|
|
}
|
|
|
|
_applyCurrentZoomPan();
|
|
if (fadeEverything) {
|
|
template.style.opacity = 1 - now;
|
|
} else {
|
|
_applyBgOpacity(initalBgOpacity - now * initalBgOpacity);
|
|
}
|
|
};
|
|
|
|
if (closeWithRaf) {
|
|
_animateProp(
|
|
"initialZoom",
|
|
0,
|
|
1,
|
|
duration,
|
|
framework.easing.cubic.out,
|
|
onUpdate,
|
|
onComplete
|
|
);
|
|
} else {
|
|
onUpdate(1);
|
|
_showOrHideTimeout = setTimeout(onComplete, duration + 20);
|
|
}
|
|
}
|
|
},
|
|
out ? 25 : 90
|
|
); // Main purpose of this delay is to give browser time to paint and
|
|
// create composite layers of PhotoSwipe UI parts (background, controls, caption, arrows).
|
|
// Which avoids lag at the beginning of scale transition.
|
|
};
|
|
startAnimation();
|
|
};
|
|
|
|
/*>>show-hide-transition*/
|
|
|
|
/*>>items-controller*/
|
|
/**
|
|
*
|
|
* Controller manages gallery items, their dimensions, and their content.
|
|
*
|
|
*/
|
|
|
|
var _items,
|
|
_tempPanAreaSize = {},
|
|
_imagesToAppendPool = [],
|
|
_initialContentSet,
|
|
_initialZoomRunning,
|
|
_controllerDefaultOptions = {
|
|
index: 0,
|
|
errorMsg:
|
|
'<div class="pswp__error-msg"><a href="%url%" target="_blank">The image</a> could not be loaded.</div>',
|
|
forceProgressiveLoading: false, // TODO
|
|
preload: [1, 1],
|
|
getNumItemsFn: function() {
|
|
return _items.length;
|
|
}
|
|
};
|
|
|
|
var _getItemAt,
|
|
_getNumItems,
|
|
_initialIsLoop,
|
|
_getZeroBounds = function() {
|
|
return {
|
|
center: { x: 0, y: 0 },
|
|
max: { x: 0, y: 0 },
|
|
min: { x: 0, y: 0 }
|
|
};
|
|
},
|
|
_calculateSingleItemPanBounds = function(
|
|
item,
|
|
realPanElementW,
|
|
realPanElementH
|
|
) {
|
|
var bounds = item.bounds;
|
|
|
|
// position of element when it's centered
|
|
bounds.center.x = Math.round(
|
|
(_tempPanAreaSize.x - realPanElementW) / 2
|
|
);
|
|
bounds.center.y =
|
|
Math.round((_tempPanAreaSize.y - realPanElementH) / 2) +
|
|
item.vGap.top;
|
|
|
|
// maximum pan position
|
|
bounds.max.x =
|
|
realPanElementW > _tempPanAreaSize.x
|
|
? Math.round(_tempPanAreaSize.x - realPanElementW)
|
|
: bounds.center.x;
|
|
|
|
bounds.max.y =
|
|
realPanElementH > _tempPanAreaSize.y
|
|
? Math.round(_tempPanAreaSize.y - realPanElementH) + item.vGap.top
|
|
: bounds.center.y;
|
|
|
|
// minimum pan position
|
|
bounds.min.x =
|
|
realPanElementW > _tempPanAreaSize.x ? 0 : bounds.center.x;
|
|
bounds.min.y =
|
|
realPanElementH > _tempPanAreaSize.y
|
|
? item.vGap.top
|
|
: bounds.center.y;
|
|
},
|
|
_calculateItemSize = function(item, viewportSize, zoomLevel) {
|
|
if (item.src && !item.loadError) {
|
|
var isInitial = !zoomLevel;
|
|
|
|
if (isInitial) {
|
|
if (!item.vGap) {
|
|
item.vGap = { top: 0, bottom: 0 };
|
|
}
|
|
// allows overriding vertical margin for individual items
|
|
_shout("parseVerticalMargin", item);
|
|
}
|
|
|
|
_tempPanAreaSize.x = viewportSize.x;
|
|
_tempPanAreaSize.y =
|
|
viewportSize.y - item.vGap.top - item.vGap.bottom;
|
|
|
|
if (isInitial) {
|
|
var hRatio = _tempPanAreaSize.x / item.w;
|
|
var vRatio = _tempPanAreaSize.y / item.h;
|
|
|
|
item.fitRatio = hRatio < vRatio ? hRatio : vRatio;
|
|
//item.fillRatio = hRatio > vRatio ? hRatio : vRatio;
|
|
|
|
var scaleMode = _options.scaleMode;
|
|
|
|
if (scaleMode === "orig") {
|
|
zoomLevel = 1;
|
|
} else if (scaleMode === "fit") {
|
|
zoomLevel = item.fitRatio;
|
|
}
|
|
|
|
if (zoomLevel > 1) {
|
|
zoomLevel = 1;
|
|
}
|
|
|
|
item.initialZoomLevel = zoomLevel;
|
|
|
|
if (!item.bounds) {
|
|
// reuse bounds object
|
|
item.bounds = _getZeroBounds();
|
|
}
|
|
}
|
|
|
|
if (!zoomLevel) {
|
|
return;
|
|
}
|
|
|
|
_calculateSingleItemPanBounds(
|
|
item,
|
|
item.w * zoomLevel,
|
|
item.h * zoomLevel
|
|
);
|
|
|
|
if (isInitial && zoomLevel === item.initialZoomLevel) {
|
|
item.initialPosition = item.bounds.center;
|
|
}
|
|
|
|
return item.bounds;
|
|
} else {
|
|
item.w = item.h = 0;
|
|
item.initialZoomLevel = item.fitRatio = 1;
|
|
item.bounds = _getZeroBounds();
|
|
item.initialPosition = item.bounds.center;
|
|
|
|
// if it's not image, we return zero bounds (content is not zoomable)
|
|
return item.bounds;
|
|
}
|
|
},
|
|
_appendImage = function(
|
|
index,
|
|
item,
|
|
baseDiv,
|
|
img,
|
|
preventAnimation,
|
|
keepPlaceholder
|
|
) {
|
|
if (item.loadError) {
|
|
return;
|
|
}
|
|
|
|
if (img) {
|
|
item.imageAppended = true;
|
|
_setImageSize(
|
|
item,
|
|
img,
|
|
item === self.currItem && _renderMaxResolution
|
|
);
|
|
|
|
baseDiv.appendChild(img);
|
|
|
|
if (keepPlaceholder) {
|
|
setTimeout(function() {
|
|
if (item && item.loaded && item.placeholder) {
|
|
item.placeholder.style.display = "none";
|
|
item.placeholder = null;
|
|
}
|
|
}, 500);
|
|
}
|
|
}
|
|
},
|
|
_preloadImage = function(item) {
|
|
item.loading = true;
|
|
item.loaded = false;
|
|
var img = (item.img = framework.createEl("pswp__img", "img"));
|
|
var onComplete = function() {
|
|
item.loading = false;
|
|
item.loaded = true;
|
|
|
|
if (item.loadComplete) {
|
|
item.loadComplete(item);
|
|
} else {
|
|
item.img = null; // no need to store image object
|
|
}
|
|
img.onload = img.onerror = null;
|
|
img = null;
|
|
};
|
|
img.onload = onComplete;
|
|
img.onerror = function() {
|
|
item.loadError = true;
|
|
onComplete();
|
|
};
|
|
|
|
img.src = item.src; // + '?a=' + Math.random();
|
|
|
|
return img;
|
|
},
|
|
_checkForError = function(item, cleanUp) {
|
|
if (item.src && item.loadError && item.container) {
|
|
if (cleanUp) {
|
|
item.container.innerHTML = "";
|
|
}
|
|
|
|
item.container.innerHTML = _options.errorMsg.replace(
|
|
"%url%",
|
|
item.src
|
|
);
|
|
return true;
|
|
}
|
|
},
|
|
_setImageSize = function(item, img, maxRes) {
|
|
if (!item.src) {
|
|
return;
|
|
}
|
|
|
|
if (!img) {
|
|
img = item.container.lastChild;
|
|
}
|
|
|
|
var w = maxRes ? item.w : Math.round(item.w * item.fitRatio),
|
|
h = maxRes ? item.h : Math.round(item.h * item.fitRatio);
|
|
|
|
if (item.placeholder && !item.loaded) {
|
|
item.placeholder.style.width = w + "px";
|
|
item.placeholder.style.height = h + "px";
|
|
}
|
|
|
|
img.style.width = w + "px";
|
|
img.style.height = h + "px";
|
|
},
|
|
_appendImagesPool = function() {
|
|
if (_imagesToAppendPool.length) {
|
|
var poolItem;
|
|
|
|
for (var i = 0; i < _imagesToAppendPool.length; i++) {
|
|
poolItem = _imagesToAppendPool[i];
|
|
if (poolItem.holder.index === poolItem.index) {
|
|
_appendImage(
|
|
poolItem.index,
|
|
poolItem.item,
|
|
poolItem.baseDiv,
|
|
poolItem.img,
|
|
false,
|
|
poolItem.clearPlaceholder
|
|
);
|
|
}
|
|
}
|
|
_imagesToAppendPool = [];
|
|
}
|
|
};
|
|
|
|
_registerModule("Controller", {
|
|
publicMethods: {
|
|
lazyLoadItem: function(index) {
|
|
index = _getLoopedId(index);
|
|
var item = _getItemAt(index);
|
|
|
|
if (!item || ((item.loaded || item.loading) && !_itemsNeedUpdate)) {
|
|
return;
|
|
}
|
|
|
|
_shout("gettingData", index, item);
|
|
|
|
if (!item.src) {
|
|
return;
|
|
}
|
|
|
|
_preloadImage(item);
|
|
},
|
|
initController: function() {
|
|
framework.extend(_options, _controllerDefaultOptions, true);
|
|
self.items = _items = items;
|
|
_getItemAt = self.getItemAt;
|
|
_getNumItems = _options.getNumItemsFn; //self.getNumItems;
|
|
|
|
_initialIsLoop = _options.loop;
|
|
if (_getNumItems() < 3) {
|
|
_options.loop = false; // disable loop if less then 3 items
|
|
}
|
|
|
|
_listen("beforeChange", function(diff) {
|
|
var p = _options.preload,
|
|
isNext = diff === null ? true : diff >= 0,
|
|
preloadBefore = Math.min(p[0], _getNumItems()),
|
|
preloadAfter = Math.min(p[1], _getNumItems()),
|
|
i;
|
|
|
|
for (i = 1; i <= (isNext ? preloadAfter : preloadBefore); i++) {
|
|
self.lazyLoadItem(_currentItemIndex + i);
|
|
}
|
|
for (i = 1; i <= (isNext ? preloadBefore : preloadAfter); i++) {
|
|
self.lazyLoadItem(_currentItemIndex - i);
|
|
}
|
|
});
|
|
|
|
_listen("initialLayout", function() {
|
|
self.currItem.initialLayout =
|
|
_options.getThumbBoundsFn &&
|
|
_options.getThumbBoundsFn(_currentItemIndex);
|
|
});
|
|
|
|
_listen("mainScrollAnimComplete", _appendImagesPool);
|
|
_listen("initialZoomInEnd", _appendImagesPool);
|
|
|
|
_listen("destroy", function() {
|
|
var item;
|
|
for (var i = 0; i < _items.length; i++) {
|
|
item = _items[i];
|
|
// remove reference to DOM elements, for GC
|
|
if (item.container) {
|
|
item.container = null;
|
|
}
|
|
if (item.placeholder) {
|
|
item.placeholder = null;
|
|
}
|
|
if (item.img) {
|
|
item.img = null;
|
|
}
|
|
if (item.preloader) {
|
|
item.preloader = null;
|
|
}
|
|
if (item.loadError) {
|
|
item.loaded = item.loadError = false;
|
|
}
|
|
}
|
|
_imagesToAppendPool = null;
|
|
});
|
|
},
|
|
|
|
getItemAt: function(index) {
|
|
if (index >= 0) {
|
|
return _items[index] !== undefined ? _items[index] : false;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
allowProgressiveImg: function() {
|
|
// 1. Progressive image loading isn't working on webkit/blink
|
|
// when hw-acceleration (e.g. translateZ) is applied to IMG element.
|
|
// That's why in PhotoSwipe parent element gets zoom transform, not image itself.
|
|
//
|
|
// 2. Progressive image loading sometimes blinks in webkit/blink when applying animation to parent element.
|
|
// That's why it's disabled on touch devices (mainly because of swipe transition)
|
|
//
|
|
// 3. Progressive image loading sometimes doesn't work in IE (up to 11).
|
|
|
|
// Don't allow progressive loading on non-large touch devices
|
|
return (
|
|
_options.forceProgressiveLoading ||
|
|
!_likelyTouchDevice ||
|
|
_options.mouseUsed ||
|
|
screen.width > 1200
|
|
);
|
|
// 1200 - to eliminate touch devices with large screen (like Chromebook Pixel)
|
|
},
|
|
|
|
setContent: function(holder, index) {
|
|
if (_options.loop) {
|
|
index = _getLoopedId(index);
|
|
}
|
|
|
|
var prevItem = self.getItemAt(holder.index);
|
|
if (prevItem) {
|
|
prevItem.container = null;
|
|
}
|
|
|
|
var item = self.getItemAt(index),
|
|
img;
|
|
|
|
if (!item) {
|
|
holder.el.innerHTML = "";
|
|
return;
|
|
}
|
|
|
|
// allow to override data
|
|
_shout("gettingData", index, item);
|
|
|
|
holder.index = index;
|
|
holder.item = item;
|
|
|
|
// base container DIV is created only once for each of 3 holders
|
|
var baseDiv = (item.container = framework.createEl(
|
|
"pswp__zoom-wrap"
|
|
));
|
|
|
|
if (!item.src && item.html) {
|
|
if (item.html.tagName) {
|
|
baseDiv.appendChild(item.html);
|
|
} else {
|
|
baseDiv.innerHTML = item.html;
|
|
}
|
|
}
|
|
|
|
_checkForError(item);
|
|
|
|
_calculateItemSize(item, _viewportSize);
|
|
|
|
if (item.src && !item.loadError && !item.loaded) {
|
|
item.loadComplete = function(item) {
|
|
// gallery closed before image finished loading
|
|
if (!_isOpen) {
|
|
return;
|
|
}
|
|
|
|
// check if holder hasn't changed while image was loading
|
|
if (holder && holder.index === index) {
|
|
if (_checkForError(item, true)) {
|
|
item.loadComplete = item.img = null;
|
|
_calculateItemSize(item, _viewportSize);
|
|
_applyZoomPanToItem(item);
|
|
|
|
if (holder.index === _currentItemIndex) {
|
|
// recalculate dimensions
|
|
self.updateCurrZoomItem();
|
|
}
|
|
return;
|
|
}
|
|
if (!item.imageAppended) {
|
|
if (
|
|
_features.transform &&
|
|
(_mainScrollAnimating || _initialZoomRunning)
|
|
) {
|
|
_imagesToAppendPool.push({
|
|
item: item,
|
|
baseDiv: baseDiv,
|
|
img: item.img,
|
|
index: index,
|
|
holder: holder,
|
|
clearPlaceholder: true
|
|
});
|
|
} else {
|
|
_appendImage(
|
|
index,
|
|
item,
|
|
baseDiv,
|
|
item.img,
|
|
_mainScrollAnimating || _initialZoomRunning,
|
|
true
|
|
);
|
|
}
|
|
} else {
|
|
// remove preloader & mini-img
|
|
if (!_initialZoomRunning && item.placeholder) {
|
|
item.placeholder.style.display = "none";
|
|
item.placeholder = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
item.loadComplete = null;
|
|
item.img = null; // no need to store image element after it's added
|
|
|
|
_shout("imageLoadComplete", index, item);
|
|
};
|
|
|
|
if (framework.features.transform) {
|
|
var placeholderClassName = "pswp__img pswp__img--placeholder";
|
|
placeholderClassName += item.msrc
|
|
? ""
|
|
: " pswp__img--placeholder--blank";
|
|
|
|
var placeholder = framework.createEl(
|
|
placeholderClassName,
|
|
item.msrc ? "img" : ""
|
|
);
|
|
if (item.msrc) {
|
|
placeholder.src = item.msrc;
|
|
}
|
|
|
|
_setImageSize(item, placeholder);
|
|
|
|
baseDiv.appendChild(placeholder);
|
|
item.placeholder = placeholder;
|
|
}
|
|
|
|
if (!item.loading) {
|
|
_preloadImage(item);
|
|
}
|
|
|
|
if (self.allowProgressiveImg()) {
|
|
// just append image
|
|
if (!_initialContentSet && _features.transform) {
|
|
_imagesToAppendPool.push({
|
|
item: item,
|
|
baseDiv: baseDiv,
|
|
img: item.img,
|
|
index: index,
|
|
holder: holder
|
|
});
|
|
} else {
|
|
_appendImage(index, item, baseDiv, item.img, true, true);
|
|
}
|
|
}
|
|
} else if (item.src && !item.loadError) {
|
|
// image object is created every time, due to bugs of image loading & delay when switching images
|
|
img = framework.createEl("pswp__img", "img");
|
|
img.style.opacity = 1;
|
|
img.src = item.src;
|
|
_setImageSize(item, img);
|
|
_appendImage(index, item, baseDiv, img, true);
|
|
}
|
|
|
|
if (!_initialContentSet && index === _currentItemIndex) {
|
|
_currZoomElementStyle = baseDiv.style;
|
|
_showOrHide(item, img || item.img);
|
|
} else {
|
|
_applyZoomPanToItem(item);
|
|
}
|
|
|
|
holder.el.innerHTML = "";
|
|
holder.el.appendChild(baseDiv);
|
|
},
|
|
|
|
cleanSlide: function(item) {
|
|
if (item.img) {
|
|
item.img.onload = item.img.onerror = null;
|
|
}
|
|
item.loaded = item.loading = item.img = item.imageAppended = false;
|
|
}
|
|
}
|
|
});
|
|
|
|
/*>>items-controller*/
|
|
|
|
/*>>tap*/
|
|
/**
|
|
* tap.js:
|
|
*
|
|
* Displatches tap and double-tap events.
|
|
*
|
|
*/
|
|
|
|
var tapTimer,
|
|
tapReleasePoint = {},
|
|
_dispatchTapEvent = function(origEvent, releasePoint, pointerType) {
|
|
var e = document.createEvent("CustomEvent"),
|
|
eDetail = {
|
|
origEvent: origEvent,
|
|
target: origEvent.target,
|
|
releasePoint: releasePoint,
|
|
pointerType: pointerType || "touch"
|
|
};
|
|
|
|
e.initCustomEvent("pswpTap", true, true, eDetail);
|
|
origEvent.target.dispatchEvent(e);
|
|
};
|
|
|
|
_registerModule("Tap", {
|
|
publicMethods: {
|
|
initTap: function() {
|
|
_listen("firstTouchStart", self.onTapStart);
|
|
_listen("touchRelease", self.onTapRelease);
|
|
_listen("destroy", function() {
|
|
tapReleasePoint = {};
|
|
tapTimer = null;
|
|
});
|
|
},
|
|
onTapStart: function(touchList) {
|
|
if (touchList.length > 1) {
|
|
clearTimeout(tapTimer);
|
|
tapTimer = null;
|
|
}
|
|
},
|
|
onTapRelease: function(e, releasePoint) {
|
|
if (!releasePoint) {
|
|
return;
|
|
}
|
|
|
|
if (!_moved && !_isMultitouch && !_numAnimations) {
|
|
var p0 = releasePoint;
|
|
if (tapTimer) {
|
|
clearTimeout(tapTimer);
|
|
tapTimer = null;
|
|
|
|
// Check if taped on the same place
|
|
if (_isNearbyPoints(p0, tapReleasePoint)) {
|
|
_shout("doubleTap", p0);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (releasePoint.type === "mouse") {
|
|
_dispatchTapEvent(e, releasePoint, "mouse");
|
|
return;
|
|
}
|
|
|
|
var clickedTagName = e.target.tagName.toUpperCase();
|
|
// avoid double tap delay on buttons and elements that have class pswp__single-tap
|
|
if (
|
|
clickedTagName === "BUTTON" ||
|
|
framework.hasClass(e.target, "pswp__single-tap")
|
|
) {
|
|
_dispatchTapEvent(e, releasePoint);
|
|
return;
|
|
}
|
|
|
|
_equalizePoints(tapReleasePoint, p0);
|
|
|
|
tapTimer = setTimeout(function() {
|
|
_dispatchTapEvent(e, releasePoint);
|
|
tapTimer = null;
|
|
}, 300);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
/*>>tap*/
|
|
|
|
/*>>desktop-zoom*/
|
|
/**
|
|
*
|
|
* desktop-zoom.js:
|
|
*
|
|
* - Binds mousewheel event for paning zoomed image.
|
|
* - Manages "dragging", "zoomed-in", "zoom-out" classes.
|
|
* (which are used for cursors and zoom icon)
|
|
* - Adds toggleDesktopZoom function.
|
|
*
|
|
*/
|
|
|
|
var _wheelDelta;
|
|
|
|
_registerModule("DesktopZoom", {
|
|
publicMethods: {
|
|
initDesktopZoom: function() {
|
|
if (_oldIE) {
|
|
// no zoom for old IE (<=8)
|
|
return;
|
|
}
|
|
|
|
if (_likelyTouchDevice) {
|
|
// if detected hardware touch support, we wait until mouse is used,
|
|
// and only then apply desktop-zoom features
|
|
_listen("mouseUsed", function() {
|
|
self.setupDesktopZoom();
|
|
});
|
|
} else {
|
|
self.setupDesktopZoom(true);
|
|
}
|
|
},
|
|
|
|
setupDesktopZoom: function(onInit) {
|
|
_wheelDelta = {};
|
|
|
|
var events = "wheel mousewheel DOMMouseScroll";
|
|
|
|
_listen("bindEvents", function() {
|
|
framework.bind(template, events, self.handleMouseWheel);
|
|
});
|
|
|
|
_listen("unbindEvents", function() {
|
|
if (_wheelDelta) {
|
|
framework.unbind(template, events, self.handleMouseWheel);
|
|
}
|
|
});
|
|
|
|
self.mouseZoomedIn = false;
|
|
|
|
var hasDraggingClass,
|
|
updateZoomable = function() {
|
|
if (self.mouseZoomedIn) {
|
|
framework.removeClass(template, "pswp--zoomed-in");
|
|
self.mouseZoomedIn = false;
|
|
}
|
|
if (_currZoomLevel < 1) {
|
|
framework.addClass(template, "pswp--zoom-allowed");
|
|
} else {
|
|
framework.removeClass(template, "pswp--zoom-allowed");
|
|
}
|
|
removeDraggingClass();
|
|
},
|
|
removeDraggingClass = function() {
|
|
if (hasDraggingClass) {
|
|
framework.removeClass(template, "pswp--dragging");
|
|
hasDraggingClass = false;
|
|
}
|
|
};
|
|
|
|
_listen("resize", updateZoomable);
|
|
_listen("afterChange", updateZoomable);
|
|
_listen("pointerDown", function() {
|
|
if (self.mouseZoomedIn) {
|
|
hasDraggingClass = true;
|
|
framework.addClass(template, "pswp--dragging");
|
|
}
|
|
});
|
|
_listen("pointerUp", removeDraggingClass);
|
|
|
|
if (!onInit) {
|
|
updateZoomable();
|
|
}
|
|
},
|
|
|
|
handleMouseWheel: function(e) {
|
|
if (_currZoomLevel <= self.currItem.fitRatio) {
|
|
if (_options.modal) {
|
|
if (!_options.closeOnScroll || _numAnimations || _isDragging) {
|
|
e.preventDefault();
|
|
} else if (_transformKey && Math.abs(e.deltaY) > 2) {
|
|
// close PhotoSwipe
|
|
// if browser supports transforms & scroll changed enough
|
|
_closedByScroll = true;
|
|
self.close();
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// allow just one event to fire
|
|
e.stopPropagation();
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/Events/wheel
|
|
_wheelDelta.x = 0;
|
|
|
|
if ("deltaX" in e) {
|
|
if (e.deltaMode === 1 /* DOM_DELTA_LINE */) {
|
|
// 18 - average line height
|
|
_wheelDelta.x = e.deltaX * 18;
|
|
_wheelDelta.y = e.deltaY * 18;
|
|
} else {
|
|
_wheelDelta.x = e.deltaX;
|
|
_wheelDelta.y = e.deltaY;
|
|
}
|
|
} else if ("wheelDelta" in e) {
|
|
if (e.wheelDeltaX) {
|
|
_wheelDelta.x = -0.16 * e.wheelDeltaX;
|
|
}
|
|
if (e.wheelDeltaY) {
|
|
_wheelDelta.y = -0.16 * e.wheelDeltaY;
|
|
} else {
|
|
_wheelDelta.y = -0.16 * e.wheelDelta;
|
|
}
|
|
} else if ("detail" in e) {
|
|
_wheelDelta.y = e.detail;
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
_calculatePanBounds(_currZoomLevel, true);
|
|
|
|
var newPanX = _panOffset.x - _wheelDelta.x,
|
|
newPanY = _panOffset.y - _wheelDelta.y;
|
|
|
|
// only prevent scrolling in nonmodal mode when not at edges
|
|
if (
|
|
_options.modal ||
|
|
(newPanX <= _currPanBounds.min.x &&
|
|
newPanX >= _currPanBounds.max.x &&
|
|
newPanY <= _currPanBounds.min.y &&
|
|
newPanY >= _currPanBounds.max.y)
|
|
) {
|
|
e.preventDefault();
|
|
}
|
|
|
|
// TODO: use rAF instead of mousewheel?
|
|
self.panTo(newPanX, newPanY);
|
|
},
|
|
|
|
toggleDesktopZoom: function(centerPoint) {
|
|
centerPoint = centerPoint || {
|
|
x: _viewportSize.x / 2 + _offset.x,
|
|
y: _viewportSize.y / 2 + _offset.y
|
|
};
|
|
|
|
var doubleTapZoomLevel = _options.getDoubleTapZoom(
|
|
true,
|
|
self.currItem
|
|
);
|
|
var zoomOut = _currZoomLevel === doubleTapZoomLevel;
|
|
|
|
self.mouseZoomedIn = !zoomOut;
|
|
|
|
self.zoomTo(
|
|
zoomOut ? self.currItem.initialZoomLevel : doubleTapZoomLevel,
|
|
centerPoint,
|
|
333
|
|
);
|
|
framework[(!zoomOut ? "add" : "remove") + "Class"](
|
|
template,
|
|
"pswp--zoomed-in"
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
/*>>desktop-zoom*/
|
|
|
|
/*>>history*/
|
|
/**
|
|
*
|
|
* history.js:
|
|
*
|
|
* - Back button to close gallery.
|
|
*
|
|
* - Unique URL for each slide: example.com/&pid=1&gid=3
|
|
* (where PID is picture index, and GID and gallery index)
|
|
*
|
|
* - Switch URL when slides change.
|
|
*
|
|
*/
|
|
|
|
var _historyDefaultOptions = {
|
|
history: true,
|
|
galleryUID: 1
|
|
};
|
|
|
|
var _historyUpdateTimeout,
|
|
_hashChangeTimeout,
|
|
_hashAnimCheckTimeout,
|
|
_hashChangedByScript,
|
|
_hashChangedByHistory,
|
|
_hashReseted,
|
|
_initialHash,
|
|
_historyChanged,
|
|
_closedFromURL,
|
|
_urlChangedOnce,
|
|
_windowLoc,
|
|
_supportsPushState,
|
|
_getHash = function() {
|
|
return _windowLoc.hash.substring(1);
|
|
},
|
|
_cleanHistoryTimeouts = function() {
|
|
if (_historyUpdateTimeout) {
|
|
clearTimeout(_historyUpdateTimeout);
|
|
}
|
|
|
|
if (_hashAnimCheckTimeout) {
|
|
clearTimeout(_hashAnimCheckTimeout);
|
|
}
|
|
},
|
|
// pid - Picture index
|
|
// gid - Gallery index
|
|
_parseItemIndexFromURL = function() {
|
|
var hash = _getHash(),
|
|
params = {};
|
|
|
|
if (hash.length < 5) {
|
|
// pid=1
|
|
return params;
|
|
}
|
|
|
|
var i,
|
|
vars = hash.split("&");
|
|
for (i = 0; i < vars.length; i++) {
|
|
if (!vars[i]) {
|
|
continue;
|
|
}
|
|
var pair = vars[i].split("=");
|
|
if (pair.length < 2) {
|
|
continue;
|
|
}
|
|
params[pair[0]] = pair[1];
|
|
}
|
|
if (_options.galleryPIDs) {
|
|
// detect custom pid in hash and search for it among the items collection
|
|
var searchfor = params.pid;
|
|
params.pid = 0; // if custom pid cannot be found, fallback to the first item
|
|
for (i = 0; i < _items.length; i++) {
|
|
if (_items[i].pid === searchfor) {
|
|
params.pid = i;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
params.pid = parseInt(params.pid, 10) - 1;
|
|
}
|
|
if (params.pid < 0) {
|
|
params.pid = 0;
|
|
}
|
|
return params;
|
|
},
|
|
_updateHash = function() {
|
|
if (_hashAnimCheckTimeout) {
|
|
clearTimeout(_hashAnimCheckTimeout);
|
|
}
|
|
|
|
if (_numAnimations || _isDragging) {
|
|
// changing browser URL forces layout/paint in some browsers, which causes noticable lag during animation
|
|
// that's why we update hash only when no animations running
|
|
_hashAnimCheckTimeout = setTimeout(_updateHash, 500);
|
|
return;
|
|
}
|
|
|
|
if (_hashChangedByScript) {
|
|
clearTimeout(_hashChangeTimeout);
|
|
} else {
|
|
_hashChangedByScript = true;
|
|
}
|
|
|
|
var pid = _currentItemIndex + 1;
|
|
var item = _getItemAt(_currentItemIndex);
|
|
if (item.hasOwnProperty("pid")) {
|
|
// carry forward any custom pid assigned to the item
|
|
pid = item.pid;
|
|
}
|
|
var newHash =
|
|
_initialHash +
|
|
"&" +
|
|
"gid=" +
|
|
_options.galleryUID +
|
|
"&" +
|
|
"pid=" +
|
|
pid;
|
|
|
|
if (!_historyChanged) {
|
|
if (_windowLoc.hash.indexOf(newHash) === -1) {
|
|
_urlChangedOnce = true;
|
|
}
|
|
// first time - add new hisory record, then just replace
|
|
}
|
|
|
|
var newURL = _windowLoc.href.split("#")[0] + "#" + newHash;
|
|
|
|
if (_supportsPushState) {
|
|
if ("#" + newHash !== window.location.hash) {
|
|
history[_historyChanged ? "replaceState" : "pushState"](
|
|
"",
|
|
document.title,
|
|
newURL
|
|
);
|
|
}
|
|
} else {
|
|
if (_historyChanged) {
|
|
_windowLoc.replace(newURL);
|
|
} else {
|
|
_windowLoc.hash = newHash;
|
|
}
|
|
}
|
|
|
|
_historyChanged = true;
|
|
_hashChangeTimeout = setTimeout(function() {
|
|
_hashChangedByScript = false;
|
|
}, 60);
|
|
};
|
|
|
|
_registerModule("History", {
|
|
publicMethods: {
|
|
initHistory: function() {
|
|
framework.extend(_options, _historyDefaultOptions, true);
|
|
|
|
if (!_options.history) {
|
|
return;
|
|
}
|
|
|
|
_windowLoc = window.location;
|
|
_urlChangedOnce = false;
|
|
_closedFromURL = false;
|
|
_historyChanged = false;
|
|
_initialHash = _getHash();
|
|
_supportsPushState = "pushState" in history;
|
|
|
|
if (_initialHash.indexOf("gid=") > -1) {
|
|
_initialHash = _initialHash.split("&gid=")[0];
|
|
_initialHash = _initialHash.split("?gid=")[0];
|
|
}
|
|
|
|
_listen("afterChange", self.updateURL);
|
|
_listen("unbindEvents", function() {
|
|
framework.unbind(window, "hashchange", self.onHashChange);
|
|
});
|
|
|
|
var returnToOriginal = function() {
|
|
_hashReseted = true;
|
|
if (!_closedFromURL) {
|
|
if (_urlChangedOnce) {
|
|
history.back();
|
|
} else {
|
|
if (_initialHash) {
|
|
_windowLoc.hash = _initialHash;
|
|
} else {
|
|
if (_supportsPushState) {
|
|
// remove hash from url without refreshing it or scrolling to top
|
|
history.pushState(
|
|
"",
|
|
document.title,
|
|
_windowLoc.pathname + _windowLoc.search
|
|
);
|
|
} else {
|
|
_windowLoc.hash = "";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_cleanHistoryTimeouts();
|
|
};
|
|
|
|
_listen("unbindEvents", function() {
|
|
if (_closedByScroll) {
|
|
// if PhotoSwipe is closed by scroll, we go "back" before the closing animation starts
|
|
// this is done to keep the scroll position
|
|
returnToOriginal();
|
|
}
|
|
});
|
|
_listen("destroy", function() {
|
|
if (!_hashReseted) {
|
|
returnToOriginal();
|
|
}
|
|
});
|
|
_listen("firstUpdate", function() {
|
|
_currentItemIndex = _parseItemIndexFromURL().pid;
|
|
});
|
|
|
|
var index = _initialHash.indexOf("pid=");
|
|
if (index > -1) {
|
|
_initialHash = _initialHash.substring(0, index);
|
|
if (_initialHash.slice(-1) === "&") {
|
|
_initialHash = _initialHash.slice(0, -1);
|
|
}
|
|
}
|
|
|
|
setTimeout(function() {
|
|
if (_isOpen) {
|
|
// hasn't destroyed yet
|
|
framework.bind(window, "hashchange", self.onHashChange);
|
|
}
|
|
}, 40);
|
|
},
|
|
onHashChange: function() {
|
|
if (_getHash() === _initialHash) {
|
|
_closedFromURL = true;
|
|
self.close();
|
|
return;
|
|
}
|
|
if (!_hashChangedByScript) {
|
|
_hashChangedByHistory = true;
|
|
self.goTo(_parseItemIndexFromURL().pid);
|
|
_hashChangedByHistory = false;
|
|
}
|
|
},
|
|
updateURL: function() {
|
|
// Delay the update of URL, to avoid lag during transition,
|
|
// and to not to trigger actions like "refresh page sound" or "blinking favicon" to often
|
|
|
|
_cleanHistoryTimeouts();
|
|
|
|
if (_hashChangedByHistory) {
|
|
return;
|
|
}
|
|
|
|
if (!_historyChanged) {
|
|
_updateHash(); // first time
|
|
} else {
|
|
_historyUpdateTimeout = setTimeout(_updateHash, 800);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
/*>>history*/
|
|
framework.extend(self, publicMethods);
|
|
};
|
|
return PhotoSwipe;
|
|
});
|