You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
564 lines
21 KiB
564 lines
21 KiB
/*!
|
|
* better-scroll / zoom
|
|
* (c) 2016-2020 ustbhuangyi
|
|
* Released under the MIT License.
|
|
*/
|
|
var sourcePrefix = 'plugins.zoom';
|
|
var propertiesMap = [
|
|
{
|
|
key: 'zoomTo',
|
|
name: 'zoomTo'
|
|
}
|
|
];
|
|
var propertiesConfig = propertiesMap.map(function (item) {
|
|
return {
|
|
key: item.key,
|
|
sourceKey: sourcePrefix + "." + item.name
|
|
};
|
|
});
|
|
|
|
// ssr support
|
|
var inBrowser = typeof window !== 'undefined';
|
|
var ua = inBrowser && navigator.userAgent.toLowerCase();
|
|
var isWeChatDevTools = !!(ua && /wechatdevtools/.test(ua));
|
|
var isAndroid = ua && ua.indexOf('android') > 0;
|
|
/* istanbul ignore next */
|
|
var isIOSBadVersion = (function () {
|
|
if (typeof ua === 'string') {
|
|
var regex = /os (\d\d?_\d(_\d)?)/;
|
|
var matches = regex.exec(ua);
|
|
if (!matches)
|
|
return false;
|
|
var parts = matches[1].split('_').map(function (item) {
|
|
return parseInt(item, 10);
|
|
});
|
|
// ios version >= 13.4 issue 982
|
|
return !!(parts[0] >= 13 && parts[1] >= 4);
|
|
}
|
|
return false;
|
|
})();
|
|
|
|
function getNow() {
|
|
return window.performance &&
|
|
window.performance.now &&
|
|
window.performance.timing
|
|
? window.performance.now() + window.performance.timing.navigationStart
|
|
: +new Date();
|
|
}
|
|
var extend = function (target, source) {
|
|
for (var key in source) {
|
|
target[key] = source[key];
|
|
}
|
|
return target;
|
|
};
|
|
function getDistance(x, y) {
|
|
return Math.sqrt(x * x + y * y);
|
|
}
|
|
function between(x, min, max) {
|
|
if (x < min) {
|
|
return min;
|
|
}
|
|
if (x > max) {
|
|
return max;
|
|
}
|
|
return x;
|
|
}
|
|
|
|
var elementStyle = (inBrowser &&
|
|
document.createElement('div').style);
|
|
var vendor = (function () {
|
|
/* istanbul ignore if */
|
|
if (!inBrowser) {
|
|
return false;
|
|
}
|
|
var transformNames = [
|
|
{
|
|
key: 'standard',
|
|
value: 'transform',
|
|
},
|
|
{
|
|
key: 'webkit',
|
|
value: 'webkitTransform',
|
|
},
|
|
{
|
|
key: 'Moz',
|
|
value: 'MozTransform',
|
|
},
|
|
{
|
|
key: 'O',
|
|
value: 'OTransform',
|
|
},
|
|
{
|
|
key: 'ms',
|
|
value: 'msTransform',
|
|
},
|
|
];
|
|
for (var _i = 0, transformNames_1 = transformNames; _i < transformNames_1.length; _i++) {
|
|
var obj = transformNames_1[_i];
|
|
if (elementStyle[obj.value] !== undefined) {
|
|
return obj.key;
|
|
}
|
|
}
|
|
/* istanbul ignore next */
|
|
return false;
|
|
})();
|
|
/* istanbul ignore next */
|
|
function prefixStyle(style) {
|
|
if (vendor === false) {
|
|
return style;
|
|
}
|
|
if (vendor === 'standard') {
|
|
if (style === 'transitionEnd') {
|
|
return 'transitionend';
|
|
}
|
|
return style;
|
|
}
|
|
return vendor + style.charAt(0).toUpperCase() + style.substr(1);
|
|
}
|
|
function offsetToBody(el) {
|
|
var rect = el.getBoundingClientRect();
|
|
return {
|
|
left: -(rect.left + window.pageXOffset),
|
|
top: -(rect.top + window.pageYOffset),
|
|
};
|
|
}
|
|
var cssVendor = vendor && vendor !== 'standard' ? '-' + vendor.toLowerCase() + '-' : '';
|
|
var transform = prefixStyle('transform');
|
|
var transition = prefixStyle('transition');
|
|
var hasPerspective = inBrowser && prefixStyle('perspective') in elementStyle;
|
|
var style = {
|
|
transform: transform,
|
|
transition: transition,
|
|
transitionTimingFunction: prefixStyle('transitionTimingFunction'),
|
|
transitionDuration: prefixStyle('transitionDuration'),
|
|
transitionDelay: prefixStyle('transitionDelay'),
|
|
transformOrigin: prefixStyle('transformOrigin'),
|
|
transitionEnd: prefixStyle('transitionEnd'),
|
|
transitionProperty: prefixStyle('transitionProperty'),
|
|
};
|
|
function getRect(el) {
|
|
/* istanbul ignore if */
|
|
if (el instanceof window.SVGElement) {
|
|
var rect = el.getBoundingClientRect();
|
|
return {
|
|
top: rect.top,
|
|
left: rect.left,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
};
|
|
}
|
|
else {
|
|
return {
|
|
top: el.offsetTop,
|
|
left: el.offsetLeft,
|
|
width: el.offsetWidth,
|
|
height: el.offsetHeight,
|
|
};
|
|
}
|
|
}
|
|
|
|
var ease = {
|
|
// easeOutQuint
|
|
swipe: {
|
|
style: 'cubic-bezier(0.23, 1, 0.32, 1)',
|
|
fn: function (t) {
|
|
return 1 + --t * t * t * t * t;
|
|
}
|
|
},
|
|
// easeOutQuard
|
|
swipeBounce: {
|
|
style: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
|
fn: function (t) {
|
|
return t * (2 - t);
|
|
}
|
|
},
|
|
// easeOutQuart
|
|
bounce: {
|
|
style: 'cubic-bezier(0.165, 0.84, 0.44, 1)',
|
|
fn: function (t) {
|
|
return 1 - --t * t * t * t;
|
|
}
|
|
}
|
|
};
|
|
|
|
var DEFAULT_INTERVAL = 1000 / 60;
|
|
var windowCompat = inBrowser && window;
|
|
/* istanbul ignore next */
|
|
function noop() { }
|
|
var requestAnimationFrame = (function () {
|
|
/* istanbul ignore if */
|
|
if (!inBrowser) {
|
|
return noop;
|
|
}
|
|
return (windowCompat.requestAnimationFrame ||
|
|
windowCompat.webkitRequestAnimationFrame ||
|
|
windowCompat.mozRequestAnimationFrame ||
|
|
windowCompat.oRequestAnimationFrame ||
|
|
// if all else fails, use setTimeout
|
|
function (callback) {
|
|
return window.setTimeout(callback, (callback.interval || DEFAULT_INTERVAL) / 2); // make interval as precise as possible.
|
|
});
|
|
})();
|
|
var cancelAnimationFrame = (function () {
|
|
/* istanbul ignore if */
|
|
if (!inBrowser) {
|
|
return noop;
|
|
}
|
|
return (windowCompat.cancelAnimationFrame ||
|
|
windowCompat.webkitCancelAnimationFrame ||
|
|
windowCompat.mozCancelAnimationFrame ||
|
|
windowCompat.oCancelAnimationFrame ||
|
|
function (id) {
|
|
window.clearTimeout(id);
|
|
});
|
|
})();
|
|
|
|
var TWO_FINGERS = 2;
|
|
var RAW_SCALE = 1;
|
|
var Zoom = /** @class */ (function () {
|
|
function Zoom(scroll) {
|
|
this.scroll = scroll;
|
|
this.scale = RAW_SCALE;
|
|
this.prevScale = 1;
|
|
this.init();
|
|
}
|
|
Zoom.prototype.init = function () {
|
|
this.handleBScroll();
|
|
this.handleOptions();
|
|
this.handleHooks();
|
|
this.tryInitialZoomTo(this.zoomOpt);
|
|
};
|
|
Zoom.prototype.zoomTo = function (scale, x, y, bounceTime) {
|
|
var _a = this.resolveOrigin(x, y), originX = _a.originX, originY = _a.originY;
|
|
var origin = {
|
|
x: originX,
|
|
y: originY,
|
|
baseScale: this.scale,
|
|
};
|
|
this._doZoomTo(scale, origin, bounceTime, true);
|
|
};
|
|
Zoom.prototype.handleBScroll = function () {
|
|
this.scroll.proxy(propertiesConfig);
|
|
this.scroll.registerType([
|
|
'beforeZoomStart',
|
|
'zoomStart',
|
|
'zooming',
|
|
'zoomEnd',
|
|
]);
|
|
};
|
|
Zoom.prototype.handleOptions = function () {
|
|
var userOptions = (this.scroll.options.zoom === true
|
|
? {}
|
|
: this.scroll.options.zoom);
|
|
var defaultOptions = {
|
|
start: 1,
|
|
min: 1,
|
|
max: 4,
|
|
initialOrigin: [0, 0],
|
|
minimalZoomDistance: 5,
|
|
bounceTime: 800,
|
|
};
|
|
this.zoomOpt = extend(defaultOptions, userOptions);
|
|
};
|
|
Zoom.prototype.handleHooks = function () {
|
|
var _this = this;
|
|
var scroll = this.scroll;
|
|
var scroller = this.scroll.scroller;
|
|
this.wrapper = this.scroll.scroller.wrapper;
|
|
this.setTransformOrigin(this.scroll.scroller.content);
|
|
var scrollBehaviorX = scroller.scrollBehaviorX;
|
|
var scrollBehaviorY = scroller.scrollBehaviorY;
|
|
this.hooksFn = [];
|
|
// BScroll
|
|
this.registerHooks(scroll.hooks, scroll.hooks.eventTypes.contentChanged, function (content) {
|
|
_this.setTransformOrigin(content);
|
|
_this.scale = RAW_SCALE;
|
|
_this.tryInitialZoomTo(_this.zoomOpt);
|
|
});
|
|
this.registerHooks(scroll.hooks, scroll.hooks.eventTypes.beforeInitialScrollTo, function () {
|
|
// if perform a zoom action, we should prevent initial scroll when initialised
|
|
if (_this.zoomOpt.start !== RAW_SCALE) {
|
|
return true;
|
|
}
|
|
});
|
|
// enlarge boundary
|
|
this.registerHooks(scrollBehaviorX.hooks, scrollBehaviorX.hooks.eventTypes.beforeComputeBoundary, function () {
|
|
// content may change, don't cache it's size
|
|
var contentSize = getRect(_this.scroll.scroller.content);
|
|
scrollBehaviorX.contentSize = Math.floor(contentSize.width * _this.scale);
|
|
});
|
|
this.registerHooks(scrollBehaviorY.hooks, scrollBehaviorY.hooks.eventTypes.beforeComputeBoundary, function () {
|
|
// content may change, don't cache it's size
|
|
var contentSize = getRect(_this.scroll.scroller.content);
|
|
scrollBehaviorY.contentSize = Math.floor(contentSize.height * _this.scale);
|
|
});
|
|
// touch event
|
|
this.registerHooks(scroller.actions.hooks, scroller.actions.hooks.eventTypes.start, function (e) {
|
|
var numberOfFingers = (e.touches && e.touches.length) || 0;
|
|
_this.fingersOperation(numberOfFingers);
|
|
if (numberOfFingers === TWO_FINGERS) {
|
|
_this.zoomStart(e);
|
|
}
|
|
});
|
|
this.registerHooks(scroller.actions.hooks, scroller.actions.hooks.eventTypes.beforeMove, function (e) {
|
|
var numberOfFingers = (e.touches && e.touches.length) || 0;
|
|
_this.fingersOperation(numberOfFingers);
|
|
if (numberOfFingers === TWO_FINGERS) {
|
|
_this.zoom(e);
|
|
return true;
|
|
}
|
|
});
|
|
this.registerHooks(scroller.actions.hooks, scroller.actions.hooks.eventTypes.beforeEnd, function (e) {
|
|
var numberOfFingers = _this.fingersOperation();
|
|
if (numberOfFingers === TWO_FINGERS) {
|
|
_this.zoomEnd();
|
|
return true;
|
|
}
|
|
});
|
|
this.registerHooks(scroller.translater.hooks, scroller.translater.hooks.eventTypes.beforeTranslate, function (transformStyle, point) {
|
|
var scale = point.scale ? point.scale : _this.prevScale;
|
|
_this.prevScale = scale;
|
|
transformStyle.push("scale(" + scale + ")");
|
|
});
|
|
this.registerHooks(scroller.hooks, scroller.hooks.eventTypes.scrollEnd, function () {
|
|
if (_this.fingersOperation() === TWO_FINGERS) {
|
|
_this.scroll.trigger(_this.scroll.eventTypes.zoomEnd, {
|
|
scale: _this.scale,
|
|
});
|
|
}
|
|
});
|
|
this.registerHooks(this.scroll.hooks, 'destroy', this.destroy);
|
|
};
|
|
Zoom.prototype.setTransformOrigin = function (content) {
|
|
content.style[style.transformOrigin] = '0 0';
|
|
};
|
|
Zoom.prototype.tryInitialZoomTo = function (options) {
|
|
var start = options.start, initialOrigin = options.initialOrigin;
|
|
var _a = this.scroll.scroller, scrollBehaviorX = _a.scrollBehaviorX, scrollBehaviorY = _a.scrollBehaviorY;
|
|
if (start !== RAW_SCALE) {
|
|
// Movable plugin may wanna modify minScrollPos or maxScrollPos
|
|
// so we force Movable to caculate them
|
|
this.resetBoundaries([scrollBehaviorX, scrollBehaviorY]);
|
|
this.zoomTo(start, initialOrigin[0], initialOrigin[1], 0);
|
|
}
|
|
};
|
|
// getter or setter operation
|
|
Zoom.prototype.fingersOperation = function (amounts) {
|
|
if (typeof amounts === 'number') {
|
|
this.numberOfFingers = amounts;
|
|
}
|
|
else {
|
|
return this.numberOfFingers;
|
|
}
|
|
};
|
|
Zoom.prototype._doZoomTo = function (scale, origin, time, useCurrentPos) {
|
|
var _this = this;
|
|
if (time === void 0) { time = this.zoomOpt.bounceTime; }
|
|
if (useCurrentPos === void 0) { useCurrentPos = false; }
|
|
var _a = this.zoomOpt, min = _a.min, max = _a.max;
|
|
var fromScale = this.scale;
|
|
var toScale = between(scale, min, max);
|
|
(function () {
|
|
if (time === 0) {
|
|
_this.scroll.trigger(_this.scroll.eventTypes.zooming, {
|
|
scale: toScale,
|
|
});
|
|
return;
|
|
}
|
|
if (time > 0) {
|
|
var timer_1;
|
|
var startTime_1 = getNow();
|
|
var endTime_1 = startTime_1 + time;
|
|
var scheduler_1 = function () {
|
|
var now = getNow();
|
|
if (now >= endTime_1) {
|
|
_this.scroll.trigger(_this.scroll.eventTypes.zooming, {
|
|
scale: toScale,
|
|
});
|
|
cancelAnimationFrame(timer_1);
|
|
return;
|
|
}
|
|
var ratio = ease.bounce.fn((now - startTime_1) / time);
|
|
var currentScale = ratio * (toScale - fromScale) + fromScale;
|
|
_this.scroll.trigger(_this.scroll.eventTypes.zooming, {
|
|
scale: currentScale,
|
|
});
|
|
timer_1 = requestAnimationFrame(scheduler_1);
|
|
};
|
|
// start scheduler job
|
|
scheduler_1();
|
|
}
|
|
})();
|
|
// suppose you are zooming by two fingers
|
|
this.fingersOperation(2);
|
|
this._zoomTo(toScale, fromScale, origin, time, useCurrentPos);
|
|
};
|
|
Zoom.prototype._zoomTo = function (toScale, fromScale, origin, time, useCurrentPos) {
|
|
if (useCurrentPos === void 0) { useCurrentPos = false; }
|
|
var ratio = toScale / origin.baseScale;
|
|
this.setScale(toScale);
|
|
var scroller = this.scroll.scroller;
|
|
var scrollBehaviorX = scroller.scrollBehaviorX, scrollBehaviorY = scroller.scrollBehaviorY;
|
|
this.resetBoundaries([scrollBehaviorX, scrollBehaviorY]);
|
|
// position is restrained in boundary
|
|
var newX = this.getNewPos(origin.x, ratio, scrollBehaviorX, true, useCurrentPos);
|
|
var newY = this.getNewPos(origin.y, ratio, scrollBehaviorY, true, useCurrentPos);
|
|
if (scrollBehaviorX.currentPos !== Math.round(newX) ||
|
|
scrollBehaviorY.currentPos !== Math.round(newY) ||
|
|
toScale !== fromScale) {
|
|
scroller.scrollTo(newX, newY, time, ease.bounce, {
|
|
start: {
|
|
scale: fromScale,
|
|
},
|
|
end: {
|
|
scale: toScale,
|
|
},
|
|
});
|
|
}
|
|
};
|
|
Zoom.prototype.resolveOrigin = function (x, y) {
|
|
var _a = this.scroll.scroller, scrollBehaviorX = _a.scrollBehaviorX, scrollBehaviorY = _a.scrollBehaviorY;
|
|
var resolveFormula = {
|
|
left: function () {
|
|
return 0;
|
|
},
|
|
top: function () {
|
|
return 0;
|
|
},
|
|
right: function () {
|
|
return scrollBehaviorX.contentSize;
|
|
},
|
|
bottom: function () {
|
|
return scrollBehaviorY.contentSize;
|
|
},
|
|
center: function (index) {
|
|
var baseSize = index === 0
|
|
? scrollBehaviorX.contentSize
|
|
: scrollBehaviorY.contentSize;
|
|
return baseSize / 2;
|
|
},
|
|
};
|
|
return {
|
|
originX: typeof x === 'number' ? x : resolveFormula[x](0),
|
|
originY: typeof y === 'number' ? y : resolveFormula[y](1),
|
|
};
|
|
};
|
|
Zoom.prototype.zoomStart = function (e) {
|
|
var firstFinger = e.touches[0];
|
|
var secondFinger = e.touches[1];
|
|
this.startDistance = this.getFingerDistance(e);
|
|
this.startScale = this.scale;
|
|
var _a = offsetToBody(this.wrapper), left = _a.left, top = _a.top;
|
|
this.origin = {
|
|
x: Math.abs(firstFinger.pageX + secondFinger.pageX) / 2 +
|
|
left -
|
|
this.scroll.x,
|
|
y: Math.abs(firstFinger.pageY + secondFinger.pageY) / 2 +
|
|
top -
|
|
this.scroll.y,
|
|
baseScale: this.startScale,
|
|
};
|
|
this.scroll.trigger(this.scroll.eventTypes.beforeZoomStart);
|
|
};
|
|
Zoom.prototype.zoom = function (e) {
|
|
var currentDistance = this.getFingerDistance(e);
|
|
// at least minimalZoomDistance pixels for the zoom to initiate
|
|
if (!this.zoomed &&
|
|
Math.abs(currentDistance - this.startDistance) <
|
|
this.zoomOpt.minimalZoomDistance) {
|
|
return;
|
|
}
|
|
// when out of boundary , perform a damping algorithm
|
|
var endScale = this.dampingScale((currentDistance / this.startDistance) * this.startScale);
|
|
var ratio = endScale / this.startScale;
|
|
this.setScale(endScale);
|
|
if (!this.zoomed) {
|
|
this.zoomed = true;
|
|
this.scroll.trigger(this.scroll.eventTypes.zoomStart);
|
|
}
|
|
var scroller = this.scroll.scroller;
|
|
var scrollBehaviorX = scroller.scrollBehaviorX, scrollBehaviorY = scroller.scrollBehaviorY;
|
|
var x = this.getNewPos(this.origin.x, ratio, scrollBehaviorX, false, false);
|
|
var y = this.getNewPos(this.origin.y, ratio, scrollBehaviorY, false, false);
|
|
this.scroll.trigger(this.scroll.eventTypes.zooming, {
|
|
scale: this.scale,
|
|
});
|
|
scroller.translater.translate({ x: x, y: y, scale: endScale });
|
|
};
|
|
Zoom.prototype.zoomEnd = function () {
|
|
if (!this.zoomed)
|
|
return;
|
|
// if out of boundary, do rebound!
|
|
if (this.shouldRebound()) {
|
|
this._doZoomTo(this.scale, this.origin, this.zoomOpt.bounceTime);
|
|
return;
|
|
}
|
|
this.scroll.trigger(this.scroll.eventTypes.zoomEnd, { scale: this.scale });
|
|
};
|
|
Zoom.prototype.getFingerDistance = function (e) {
|
|
var firstFinger = e.touches[0];
|
|
var secondFinger = e.touches[1];
|
|
var deltaX = Math.abs(firstFinger.pageX - secondFinger.pageX);
|
|
var deltaY = Math.abs(firstFinger.pageY - secondFinger.pageY);
|
|
return getDistance(deltaX, deltaY);
|
|
};
|
|
Zoom.prototype.shouldRebound = function () {
|
|
var _a = this.zoomOpt, min = _a.min, max = _a.max;
|
|
var currentScale = this.scale;
|
|
// scale exceeded!
|
|
if (currentScale !== between(currentScale, min, max)) {
|
|
return true;
|
|
}
|
|
var _b = this.scroll.scroller, scrollBehaviorX = _b.scrollBehaviorX, scrollBehaviorY = _b.scrollBehaviorY;
|
|
// enlarge boundaries manually when zoom is end
|
|
this.resetBoundaries([scrollBehaviorX, scrollBehaviorY]);
|
|
var xInBoundary = scrollBehaviorX.checkInBoundary().inBoundary;
|
|
var yInBoundary = scrollBehaviorX.checkInBoundary().inBoundary;
|
|
return !(xInBoundary && yInBoundary);
|
|
};
|
|
Zoom.prototype.dampingScale = function (scale) {
|
|
var _a = this.zoomOpt, min = _a.min, max = _a.max;
|
|
if (scale < min) {
|
|
scale = 0.5 * min * Math.pow(2.0, scale / min);
|
|
}
|
|
else if (scale > max) {
|
|
scale = 2.0 * max * Math.pow(0.5, max / scale);
|
|
}
|
|
return scale;
|
|
};
|
|
Zoom.prototype.setScale = function (scale) {
|
|
this.scale = scale;
|
|
};
|
|
Zoom.prototype.resetBoundaries = function (scrollBehaviorPairs) {
|
|
scrollBehaviorPairs.forEach(function (behavior) { return behavior.computeBoundary(); });
|
|
};
|
|
Zoom.prototype.getNewPos = function (origin, lastScale, scrollBehavior, shouldInBoundary, useCurrentPos) {
|
|
if (useCurrentPos === void 0) { useCurrentPos = false; }
|
|
var newPos = origin -
|
|
origin * lastScale +
|
|
(useCurrentPos ? scrollBehavior.currentPos : scrollBehavior.startPos);
|
|
if (shouldInBoundary) {
|
|
newPos = between(newPos, scrollBehavior.maxScrollPos, scrollBehavior.minScrollPos);
|
|
}
|
|
// maxScrollPos or minScrollPos maybe a negative or positive digital
|
|
return newPos > 0 ? Math.floor(newPos) : Math.ceil(newPos);
|
|
};
|
|
Zoom.prototype.registerHooks = function (hooks, name, handler) {
|
|
hooks.on(name, handler, this);
|
|
this.hooksFn.push([hooks, name, handler]);
|
|
};
|
|
Zoom.prototype.destroy = function () {
|
|
this.hooksFn.forEach(function (item) {
|
|
var hooks = item[0];
|
|
var hooksName = item[1];
|
|
var handlerFn = item[2];
|
|
hooks.off(hooksName, handlerFn);
|
|
});
|
|
this.hooksFn.length = 0;
|
|
};
|
|
Zoom.pluginName = 'zoom';
|
|
return Zoom;
|
|
}());
|
|
|
|
export default Zoom;
|