vue-zoomer
Version:
Zoom the image or other thing with mouse or touch
1,062 lines (1,015 loc) • 38.9 kB
JavaScript
// vue-zoomer v0.3.10 - Jarvis Niu
// https://github.com/jarvisniu/vue-zoomer
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = global || self, global.VueZoomer = factory());
}(this, (function () { 'use strict';
/* esm.sh - esbuild bundle(lodash@4.17.20/debounce) esnext production */
if (typeof global === "undefined") var global = window;var ce=Object.create,S=Object.defineProperty,se=Object.getPrototypeOf,ue=Object.prototype.hasOwnProperty,be=Object.getOwnPropertyNames,le=Object.getOwnPropertyDescriptor,ge=e=>S(e,"__esModule",{value:!0}),a=(e,r)=>()=>(r||(r={exports:{}},e(r.exports,r)),r.exports),de=(e,r,n)=>{if(ge(e),r&&typeof r=="object"||typeof r=="function")for(let i of be(r))!ue.call(e,i)&&i!=="default"&&S(e,i,{get:()=>r[i],enumerable:!(n=le(r,i))||n.enumerable});return e},me=e=>e&&e.__esModule?e:de(S(e!=null?ce(se(e)):{},"default",{value:e,enumerable:!0}),e),x=a((rr,E)=>{function pe(e){var r=typeof e;return e!=null&&(r=="object"||r=="function")}E.exports=pe;}),N=a((tr,L)=>{var ye=typeof global=="object"&&global&&global.Object===Object&&global;L.exports=ye;}),O=a((nr,G)=>{var Te=N(),je=typeof self=="object"&&self&&self.Object===Object&&self,Se=Te||je||Function("return this")();G.exports=Se;}),w=a((ir,R)=>{var xe=O(),Oe=function(){return xe.Date.now()};R.exports=Oe;}),v=a((or,P)=>{var ve=O(),qe=ve.Symbol;P.exports=qe;}),$=a((ar,W)=>{var C=v(),M=Object.prototype,he=M.hasOwnProperty,Ie=M.toString,m=C?C.toStringTag:void 0;function _e(e){var r=he.call(e,m),n=e[m];try{e[m]=void 0;var i=!0;}catch(b){}var c=Ie.call(e);return i&&(r?e[m]=n:delete e[m]),c}W.exports=_e;}),B=a((fr,A)=>{var ke=Object.prototype,Ee=ke.toString;function Le(e){return Ee.call(e)}A.exports=Le;}),H=a((cr,F)=>{var U=v(),Ne=$(),Ge=B(),Re="[object Null]",we="[object Undefined]",D=U?U.toStringTag:void 0;function Pe(e){return e==null?e===void 0?we:Re:D&&D in Object(e)?Ne(e):Ge(e)}F.exports=Pe;}),z=a((sr,X)=>{function We(e){return e!=null&&typeof e=="object"}X.exports=We;}),K=a((ur,J)=>{var Ce=H(),Me=z(),$e="[object Symbol]";function Ae(e){return typeof e=="symbol"||Me(e)&&Ce(e)==$e}J.exports=Ae;}),Z=a((br,Q)=>{var V=x(),Be=K(),Y=0/0,Fe=/^\s+|\s+$/g,Ue=/^[-+]0x[0-9a-f]+$/i,De=/^0b[01]+$/i,He=/^0o[0-7]+$/i,Xe=parseInt;function ze(e){if(typeof e=="number")return e;if(Be(e))return Y;if(V(e)){var r=typeof e.valueOf=="function"?e.valueOf():e;e=V(r)?r+"":r;}if(typeof e!="string")return e===0?e:+e;e=e.replace(Fe,"");var n=De.test(e);return n||He.test(e)?Xe(e.slice(2),n?2:8):Ue.test(e)?Y:+e}Q.exports=ze;}),te=a((lr,ee)=>{var Je=x(),q=w(),re=Z(),Ke="Expected a function",Qe=Math.max,Ve=Math.min;function Ye(e,r,n){var i,c,b,u,o,s,l=0,h=!1,g=!1,y=!0;if(typeof e!="function")throw new TypeError(Ke);r=re(r)||0,Je(n)&&(h=!!n.leading,g="maxWait"in n,b=g?Qe(re(n.maxWait)||0,r):b,y="trailing"in n?!!n.trailing:y);function T(t){var f=i,d=c;return i=c=void 0,l=t,u=e.apply(d,f),u}function ie(t){return l=t,o=setTimeout(p,r),h?T(t):u}function oe(t){var f=t-s,d=t-l,k=r-f;return g?Ve(k,b-d):k}function I(t){var f=t-s,d=t-l;return s===void 0||f>=r||f<0||g&&d>=b}function p(){var t=q();if(I(t))return _(t);o=setTimeout(p,oe(t));}function _(t){return o=void 0,y&&i?T(t):(i=c=void 0,u)}function ae(){o!==void 0&&clearTimeout(o),l=0,i=s=c=o=void 0;}function fe(){return o===void 0?u:_(q())}function j(){var t=q(),f=I(t);if(i=arguments,c=this,s=t,f){if(o===void 0)return ie(s);if(g)return clearTimeout(o),o=setTimeout(p,r),T(s)}return o===void 0&&(o=setTimeout(p,r)),u}return j.cancel=ae,j.flush=fe,j}ee.exports=Ye;}),ne=me(te()),Ze=ne.default;
/**
* Detect double tap events
* # Deals with:
* - Time intervals between taps
* - Swipe will stop the taps
* # Usage:
* let tapDetector = new TapDetector()
* tapDetector.attach(this.$el) // in mounted
* tapDetector.detach(this.$el) // in destroy
* tapDetector.onSingle(callback)
* tapDetector.onDouble(callback)
*/
function TapDetector () {
// Callbacks -----------------------------------------------------------------
let singleTapCallbacks = [];
let doubleTapCallbacks = [];
function triggerCallbacks (cbList, arg) {
cbList.forEach(cbItem => {
cbItem.call(null, arg);
});
}
this.onSingleTap = function (cb) {
if (typeof cb === 'function' && !singleTapCallbacks.includes(cb)) {
singleTapCallbacks.push(cb);
}
};
this.onDoubleTap = function (cb) {
if (typeof cb === 'function' && !doubleTapCallbacks.includes(cb)) {
doubleTapCallbacks.push(cb);
}
};
this.attach = function (dom) {
if (!(dom instanceof Element)) {
console.error('TapDetector.attach: arg must be an Element');
return
}
dom.addEventListener('touchstart', onTouchStart);
dom.addEventListener('touchmove', onTouchMove);
dom.addEventListener('touchend', onTouchEnd);
dom.addEventListener('mousedown', onMouseDown);
dom.addEventListener('mouseup', onMouseUp);
dom.addEventListener('mousemove', onMouseMove);
};
this.detach = function (dom) {
dom.removeEventListener('touchstart', onTouchStart);
dom.removeEventListener('touchmove', onTouchMove);
dom.removeEventListener('touchend', onTouchEnd);
dom.removeEventListener('mousedown', onMouseDown);
dom.removeEventListener('mouseup', onMouseUp);
dom.removeEventListener('mousemove', onMouseMove);
};
// Main logic ----------------------------------------------------------------
// in touch mode mouse events will be disabled. Otherwise touches will
// trigger both touchend end mouseup, i.e. one touch triggers two onPointerUp.
let isTouchMode = false;
let lastTapTimestamp = 0;
let tappedCount = 0;
let touchMovedLength = 0;
let lastPointerX = 0;
let lastPointerY = 0;
function onTouchStart (ev) {
isTouchMode = true;
// console.log('onTouchStart')
if (ev.touches.length === 1) {
onPointerDown(ev.touches[0].clientX, ev.touches[0].clientY);
}
}
function onTouchEnd (ev) {
// console.log('onTouchEnd')
if (ev.touches.length === 0) {
onPointerUp();
}
}
function onTouchMove (ev) {
// console.log('onTouchMove', ev)
if (ev.touches.length === 1) {
onPointerMove(ev.touches[0].clientX, ev.touches[0].clientY);
}
}
function onMouseDown (ev) {
if (isTouchMode) return
// console.log('onMouseDown')
onPointerDown(ev.clientX, ev.clientY);
}
function onMouseUp (ev) {
if (isTouchMode) return
// console.log('onMouseUp')
onPointerUp();
}
function onMouseMove (ev) {
if (isTouchMode) return
// console.log('onMouseMove', ev)
if (ev.button === 0) {
onPointerMove(ev.clientX, ev.clientY);
}
}
function onPointerDown (x, y) {
lastPointerX = x;
lastPointerY = y;
touchMovedLength = 0;
}
function onPointerUp () {
let currTimeStamp = Date.now();
// console.log('touchMovedLength', touchMovedLength)
if (touchMovedLength < 10) {
// Only when no sliding two far is considered as a valid tap
if (currTimeStamp - lastTapTimestamp < 300) {
tappedCount += 1;
} else {
tappedCount = 1;
}
lastTapTimestamp = Date.now();
// console.log('tappedCount', tappedCount)
triggerCallbacks(singleTapCallbacks, {
clientX: lastPointerX,
clientY: lastPointerY,
});
if (tappedCount === 2) {
triggerCallbacks(doubleTapCallbacks, {
clientX: lastPointerX,
clientY: lastPointerY,
});
tappedCount = 0; // clear count on maximum tap count
}
}
touchMovedLength = 0;
}
function onPointerMove (x, y) {
let deltaX = lastPointerX - x;
let deltaY = lastPointerY - y;
let length = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// console.log('onTouchMove length', length)
touchMovedLength += length;
lastPointerX = x;
lastPointerY = y;
}
}
//
var script = {
props: {
minScale: { type: Number, default: 1 },
maxScale: { type: Number, default: 5 },
zoomed: { type: Boolean, default: false },
resetTrigger: { type: Number, default: 1e5 },
aspectRatio: { type: Number, default: 1 },
backgroundColor: { type: String, default: 'transparent' },
pivot: { type: String, default: 'cursor' }, // other options: image-center
zoomingElastic: { type: Boolean, default: true },
limitTranslation: { type: Boolean, default: true },
doubleClickToZoom: { type: Boolean, default: true },
mouseWheelToZoom: { type: Boolean, default: true },
},
data () {
return {
// Container sizes, used to determin the initial zoomer size.
// Need to reactive to window resizing.
containerWidth: 1,
containerHeight: 1,
containerLeft: 0,
containerTop: 0,
// Store values: Interactions will at last change these values.
// After rotation or resize, these values will keep still.
// These three values are all relative to the container size.
translateX: 0,
animTranslateX: 0,
translateY: 0,
animTranslateY: 0,
scale: 1,
animScale: 1,
// Mouse states
lastFullWheelTime: 0,
lastWheelTime: 0,
lastWheelDirection: 'y',
isPointerDown: false,
pointerPosX: -1,
pointerPosY: -1,
twoFingerInitDist: 0,
panLocked: true,
// Others
raf: null,
tapDetector: null,
}
},
computed: {
wrapperStyle () {
let translateX = this.containerWidth * this.animTranslateX;
let translateY = this.containerHeight * this.animTranslateY;
return {
transform: [
`translate(${ translateX }px, ${ translateY }px)`,
`scale(${ this.animScale })`,
].join(' ')
}
},
},
watch: {
scale (val) {
if (val !== 1) {
this.$emit('update:zoomed', true);
this.panLocked = false;
}
},
resetTrigger: 'reset',
},
mounted () {
this.tapDetector = new TapDetector();
this.tapDetector.attach(this.$el);
if (this.doubleClickToZoom) {
this.tapDetector.onDoubleTap(this.onDoubleTap);
}
// console.log('container size: ', this.containerWidth, this.containerHeight)
window.addEventListener('resize', this.onWindowResize);
this.onWindowResize();
this.refreshContainerPos();
this.loop();
},
destroyed () {
this.tapDetector.detach(this.$el);
window.removeEventListener('resize', this.onWindowResize);
window.cancelAnimationFrame(this.raf);
// console.log('destroy')
},
methods: {
// API ---------------------------------------------------------------------
reset () {
this.scale = 1;
this.panLocked = true;
this.translateX = 0;
this.translateY = 0;
},
zoomIn(scale = 2) {
this.tryToScale(scale);
this.onInteractionEnd();
},
zoomOut(scale = 0.5) {
this.tryToScale(scale);
this.onInteractionEnd();
},
// Main Logic --------------------------------------------------------------
// scale
// Zoom the image with the point at the pointer(mouse or pinch center) pinned.
// Simplify: This can be regard as vector pointer to old-image-center scaling.
tryToScale (scaleDelta) {
let newScale = this.scale * scaleDelta;
if (this.zoomingElastic) {
// damping
if (newScale < this.minScale || newScale > this.maxScale) {
let log = Math.log2(scaleDelta);
log *= 0.2;
scaleDelta = Math.pow(2, log);
newScale = this.scale * scaleDelta;
}
} else {
if (newScale < this.minScale) newScale = this.minScale;
else if (newScale > this.maxScale) newScale = this.maxScale;
}
scaleDelta = newScale / this.scale;
this.scale = newScale;
if (this.pivot !== 'image-center') {
let normMousePosX = (this.pointerPosX - this.containerLeft) / this.containerWidth;
let normMousePosY = (this.pointerPosY - this.containerTop) / this.containerHeight;
this.translateX = (0.5 + this.translateX - normMousePosX) * scaleDelta + normMousePosX - 0.5;
this.translateY = (0.5 + this.translateY - normMousePosY) * scaleDelta + normMousePosY - 0.5;
}
},
setPointerPosCenter () {
this.pointerPosX = this.containerLeft + this.containerWidth / 2;
this.pointerPosY = this.containerTop + this.containerHeight / 2;
},
// pan
onPointerMove (newMousePosX, newMousePosY) {
if (this.isPointerDown) {
let pixelDeltaX = newMousePosX - this.pointerPosX;
let pixelDeltaY = newMousePosY - this.pointerPosY;
// console.log('pixelDeltaX, pixelDeltaY', pixelDeltaX, pixelDeltaY)
if (!this.panLocked) {
this.translateX += pixelDeltaX / this.containerWidth;
this.translateY += pixelDeltaY / this.containerHeight;
}
}
this.pointerPosX = newMousePosX;
this.pointerPosY = newMousePosY;
},
onInteractionEnd: Ze(function () {
this.limit();
this.panLocked = this.scale === 1;
this.$emit('update:zoomed', !this.panLocked);
}, 100),
// limit the scale between max and min and the translate within the viewport
limit () {
// scale
if (this.scale < this.minScale) {
this.scale = this.minScale;
// FIXME this sometimes will not reset when pinching in
// this.tryToScale(this.minScale / this.scale)
} else if (this.scale > this.maxScale) {
this.tryToScale(this.maxScale / this.scale);
}
// translate
if (this.limitTranslation) {
let translateLimit = this.calcTranslateLimit();
if (Math.abs(this.translateX) > translateLimit.x) {
this.translateX *= translateLimit.x / Math.abs(this.translateX);
}
if (Math.abs(this.translateY) > translateLimit.y) {
this.translateY *= translateLimit.y / Math.abs(this.translateY);
}
}
},
calcTranslateLimit () {
if (this.getMarginDirection() === 'y') {
let imageToContainerRatio = this.containerWidth / this.aspectRatio / this.containerHeight;
let translateLimitY = (this.scale * imageToContainerRatio - 1) / 2;
if (translateLimitY < 0) translateLimitY = 0;
return {
x: (this.scale - 1) / 2,
y: translateLimitY,
}
} else {
let imageToContainerRatio = this.containerHeight * this.aspectRatio / this.containerWidth;
let translateLimitX = (this.scale * imageToContainerRatio - 1) / 2;
if (translateLimitX < 0) translateLimitX = 0;
return {
x: translateLimitX,
y: (this.scale - 1) / 2,
}
}
},
getMarginDirection () {
let containerRatio = this.containerWidth / this.containerHeight;
return containerRatio > this.aspectRatio ? 'x' : 'y'
},
onDoubleTap (ev) {
if (this.scale === 1) {
if (ev.clientX > 0) {
this.pointerPosX = ev.clientX;
this.pointerPosY = ev.clientY;
}
this.tryToScale(Math.min(3, this.maxScale));
} else {
this.reset();
}
this.onInteractionEnd();
},
// reactive
onWindowResize () {
let styles = window.getComputedStyle(this.$el);
this.containerWidth = parseFloat(styles.width);
this.containerHeight = parseFloat(styles.height);
this.setPointerPosCenter();
this.limit();
},
refreshContainerPos () {
let rect = this.$el.getBoundingClientRect();
this.containerLeft = rect.left;
this.containerTop = rect.top;
},
loop () {
this.animScale = this.gainOn(this.animScale, this.scale);
this.animTranslateX = this.gainOn(this.animTranslateX, this.translateX);
this.animTranslateY = this.gainOn(this.animTranslateY, this.translateY);
this.raf = window.requestAnimationFrame(this.loop);
// console.log('loop', this.raf)
},
gainOn (from, to) {
let delta = (to - from) * 0.3;
// console.log('gainOn', from, to, from + delta)
if (Math.abs(delta) > 1e-5) {
return from + delta
} else {
return to
}
},
// Mouse Events ------------------------------------------------------------
// Mouse wheel scroll, TrackPad pinch or TrackPad scroll
onMouseWheel (ev) {
if (!this.mouseWheelToZoom) return
// prevent is used to stop the page scroll elastic effects
ev.preventDefault();
if (ev.detail) ev.wheelDelta = ev.detail * -10;
let currTime = Date.now();
if (Math.abs(ev.wheelDelta) === 120) {
// Throttle the TouchPad pinch on Mac, or it will be too sensitive
if (currTime - this.lastFullWheelTime > 50) {
this.onMouseWheelDo(ev.wheelDelta);
this.lastFullWheelTime = currTime;
}
} else {
if (currTime - this.lastWheelTime > 50 && typeof ev.deltaX === 'number') {
this.lastWheelDirection = (ev.detail == 0 && Math.abs(ev.deltaX) > Math.abs(ev.deltaY)) ? 'x' : 'y';
if (this.lastWheelDirection === 'x') {
this.$emit('swipe', ev.deltaX > 0 ? 'left' : 'right');
}
}
if (this.lastWheelDirection === 'y') {
this.onMouseWheelDo(ev.wheelDelta);
}
}
this.lastWheelTime = currTime;
},
onMouseWheelDo (wheelDelta) {
// Value basis: One mouse wheel (wheelDelta=+-120) means 1.25/0.8 scale.
let scaleDelta = Math.pow(1.25, wheelDelta / 120);
this.tryToScale(scaleDelta);
this.onInteractionEnd();
},
onMouseDown (ev) {
this.refreshContainerPos();
this.isPointerDown = true;
// Open the context menu then click other place will skip the mousemove events.
// This will cause the pointerPosX/Y NOT sync, then we will need to fix it on mousedown event.
this.pointerPosX = ev.clientX;
this.pointerPosY = ev.clientY;
// console.log('onMouseDown', ev)
},
onMouseUp (ev) {
this.isPointerDown = false;
this.onInteractionEnd();
},
onMouseMove (ev) {
this.onPointerMove(ev.clientX, ev.clientY);
// console.log('onMouseMove client, offset', ev.clientX, ev.clientY)
},
// Touch Events ------------------------------------------------------------
onTouchStart (ev) {
if (ev.touches.length === 1) {
this.refreshContainerPos();
this.pointerPosX = ev.touches[0].clientX;
this.pointerPosY = ev.touches[0].clientY;
this.isPointerDown = true;
} else if (ev.touches.length === 2) {
this.isPointerDown = true;
// pos
this.pointerPosX = (ev.touches[0].clientX + ev.touches[1].clientX) / 2;
this.pointerPosY = (ev.touches[0].clientY + ev.touches[1].clientY) / 2;
// dist
let distX = ev.touches[0].clientX - ev.touches[1].clientX;
let distY = ev.touches[0].clientY - ev.touches[1].clientY;
this.twoFingerInitDist = Math.sqrt(distX * distX + distY * distY);
}
// console.log('onTouchStart', ev.touches)
},
onTouchEnd (ev) {
if (ev.touches.length === 0) {
this.isPointerDown = false;
// Near 1 to set 1
if (Math.abs(this.scale - 1) < 0.1) this.scale = 1;
this.onInteractionEnd();
} else if (ev.touches.length === 1) {
this.pointerPosX = ev.touches[0].clientX;
this.pointerPosY = ev.touches[0].clientY;
}
// console.log('onTouchEnd', ev.touches.length)
},
onTouchMove (ev) {
if (ev.touches.length === 1) {
this.onPointerMove(ev.touches[0].clientX, ev.touches[0].clientY);
} else if (ev.touches.length === 2) {
// pos
let newMousePosX = (ev.touches[0].clientX + ev.touches[1].clientX) / 2;
let newMousePosY = (ev.touches[0].clientY + ev.touches[1].clientY) / 2;
this.onPointerMove(newMousePosX, newMousePosY);
this.pointerPosX = newMousePosX;
this.pointerPosY = newMousePosY;
// dist
let distX = ev.touches[0].clientX - ev.touches[1].clientX;
let distY = ev.touches[0].clientY - ev.touches[1].clientY;
let newTwoFingerDist = Math.sqrt(distX * distX + distY * distY);
this.tryToScale(newTwoFingerDist / this.twoFingerInitDist);
this.twoFingerInitDist = newTwoFingerDist;
}
// console.log('onTouchMove', this.pointerPosX, this.pointerPosY)
},
},
};
function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier /* server only */, shadowMode, createInjector, createInjectorSSR, createInjectorShadow) {
if (typeof shadowMode !== 'boolean') {
createInjectorSSR = createInjector;
createInjector = shadowMode;
shadowMode = false;
}
// Vue.extend constructor export interop.
const options = typeof script === 'function' ? script.options : script;
// render functions
if (template && template.render) {
options.render = template.render;
options.staticRenderFns = template.staticRenderFns;
options._compiled = true;
// functional template
if (isFunctionalTemplate) {
options.functional = true;
}
}
// scopedId
if (scopeId) {
options._scopeId = scopeId;
}
let hook;
if (moduleIdentifier) {
// server build
hook = function (context) {
// 2.3 injection
context =
context || // cached call
(this.$vnode && this.$vnode.ssrContext) || // stateful
(this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext); // functional
// 2.2 with runInNewContext: true
if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {
context = __VUE_SSR_CONTEXT__;
}
// inject component styles
if (style) {
style.call(this, createInjectorSSR(context));
}
// register component module identifier for async chunk inference
if (context && context._registeredComponents) {
context._registeredComponents.add(moduleIdentifier);
}
};
// used by ssr in case component is cached and beforeCreate
// never gets called
options._ssrRegister = hook;
}
else if (style) {
hook = shadowMode
? function (context) {
style.call(this, createInjectorShadow(context, this.$root.$options.shadowRoot));
}
: function (context) {
style.call(this, createInjector(context));
};
}
if (hook) {
if (options.functional) {
// register for functional component in vue file
const originalRender = options.render;
options.render = function renderWithStyleInjection(h, context) {
hook.call(context);
return originalRender(h, context);
};
}
else {
// inject component registration as beforeCreate hook
const existing = options.beforeCreate;
options.beforeCreate = existing ? [].concat(existing, hook) : [hook];
}
}
return script;
}
const isOldIE = typeof navigator !== 'undefined' &&
/msie [6-9]\\b/.test(navigator.userAgent.toLowerCase());
function createInjector(context) {
return (id, style) => addStyle(id, style);
}
let HEAD;
const styles = {};
function addStyle(id, css) {
const group = isOldIE ? css.media || 'default' : id;
const style = styles[group] || (styles[group] = { ids: new Set(), styles: [] });
if (!style.ids.has(id)) {
style.ids.add(id);
let code = css.source;
if (css.map) {
// https://developer.chrome.com/devtools/docs/javascript-debugging
// this makes source maps inside style tags work properly in Chrome
code += '\n/*# sourceURL=' + css.map.sources[0] + ' */';
// http://stackoverflow.com/a/26603875
code +=
'\n/*# sourceMappingURL=data:application/json;base64,' +
btoa(unescape(encodeURIComponent(JSON.stringify(css.map)))) +
' */';
}
if (!style.element) {
style.element = document.createElement('style');
style.element.type = 'text/css';
if (css.media)
style.element.setAttribute('media', css.media);
if (HEAD === undefined) {
HEAD = document.head || document.getElementsByTagName('head')[0];
}
HEAD.appendChild(style.element);
}
if ('styleSheet' in style.element) {
style.styles.push(code);
style.element.styleSheet.cssText = style.styles
.filter(Boolean)
.join('\n');
}
else {
const index = style.ids.size - 1;
const textNode = document.createTextNode(code);
const nodes = style.element.childNodes;
if (nodes[index])
style.element.removeChild(nodes[index]);
if (nodes.length)
style.element.insertBefore(textNode, nodes[index]);
else
style.element.appendChild(textNode);
}
}
}
/* script */
const __vue_script__ = script;
/* template */
var __vue_render__ = function () {
var _vm = this;
var _h = _vm.$createElement;
var _c = _vm._self._c || _h;
return _c(
"div",
{
staticClass: "vue-zoomer",
style: { backgroundColor: _vm.backgroundColor },
on: {
mousewheel: _vm.onMouseWheel,
DOMMouseScroll: _vm.onMouseWheel,
mousedown: _vm.onMouseDown,
mouseup: _vm.onMouseUp,
mousemove: _vm.onMouseMove,
mouseout: _vm.setPointerPosCenter,
touchstart: _vm.onTouchStart,
touchend: _vm.onTouchEnd,
touchmove: _vm.onTouchMove,
},
},
[
_c(
"div",
{ staticClass: "zoomer", style: _vm.wrapperStyle },
[_vm._t("default")],
2
),
]
)
};
var __vue_staticRenderFns__ = [];
__vue_render__._withStripped = true;
/* style */
const __vue_inject_styles__ = function (inject) {
if (!inject) return
inject("data-v-e532c06e_0", { source: "\n.vue-zoomer[data-v-e532c06e] {\n overflow: hidden;\n}\n.zoomer[data-v-e532c06e] {\n transform-origin: 50% 50%;\n width: 100%;\n height: 100%;\n}\n.zoomer > img[data-v-e532c06e] {\n /* remove the 4px gap below the image */\n vertical-align: top;\n user-drag: none;\n -webkit-user-drag: none;\n -moz-user-drag: none;\n}\n", map: undefined, media: undefined });
};
/* scoped */
const __vue_scope_id__ = "data-v-e532c06e";
/* module identifier */
const __vue_module_identifier__ = undefined;
/* functional template */
const __vue_is_functional_template__ = false;
/* style inject SSR */
/* style inject shadow dom */
const __vue_component__ = /*#__PURE__*/normalizeComponent(
{ render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ },
__vue_inject_styles__,
__vue_script__,
__vue_scope_id__,
__vue_is_functional_template__,
__vue_module_identifier__,
false,
createInjector,
undefined,
undefined
);
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
//
const SLIDE_WIDTH_THRESH = 50; // in px
var script$1 = {
props: {
value: { type: Number, required: true },
list: { type: Array, required: true },
backgroundColor: { type: String, default: '#333' },
pivot: { type: String, default: 'cursor' },
zoomingElastic: { type: Boolean, default: true },
limitTranslation: { type: Boolean, default: true },
doubleClickToZoom: { type: Boolean, default: true },
mouseWheelToZoom: { type: Boolean, default: true },
},
data () {
return {
// env states
containerWidth: 1,
containerHeight: 1,
// main states
selIndex: this.value,
animSelIndex: this.value,
currentZoomed: false,
autoSliding: false,
imageAspectRatios: [], // aspect ratio (width / height) of images
// interaction states
isPointerDown: false,
lastPointerX: 0,
slideOffsetX: 0,
}
},
computed: {
middleStyle () {
return {
left: `${ 0 + this.slideOffsetX }px`,
}
},
leftStyle () {
return {
left: `${ -this.containerWidth + this.slideOffsetX }px`,
}
},
rightStyle () {
return {
left: `${ this.containerWidth + this.slideOffsetX }px`,
}
},
slideThresh () {
return Math.max(SLIDE_WIDTH_THRESH, this.containerWidth * 0.1)
},
},
watch: {
value (val) {
if (val !== this.animSelIndex) {
this.selIndex = val;
this.animSelIndex = val;
}
},
selIndex() {
this.$nextTick(() => {
this.$refs.zoomers.forEach(zoomer => {
zoomer.refreshContainerPos();
});
});
},
},
mounted () {
window.addEventListener('resize', this.onWindowResize);
this.onWindowResize();
},
destroyed () {
window.removeEventListener('resize', this.onWindowResize);
},
methods: {
// api ---------------------------------------------------------------------
reset () {
this.$refs.zoomers.forEach(zoomer => {
zoomer.reset();
});
},
zoomIn (scale) {
if (this.$refs.zoomers[1]) this.$refs.zoomers[1].zoomIn(scale);
},
zoomOut (scale) {
if (this.$refs.zoomers[1]) this.$refs.zoomers[1].zoomOut(scale);
},
// events ------------------------------------------------------------------
onWindowResize () {
let styles = window.getComputedStyle(this.$el);
this.containerWidth = parseFloat(styles.width);
this.containerHeight = parseFloat(styles.height);
},
onPointerMove (deltaX) {
if (this.isPointerDown && !this.currentZoomed) {
let factor = (
(this.selIndex === 0 && deltaX > 0 && this.slideOffsetX + deltaX > 0) ||
(this.selIndex === this.list.length - 1 && deltaX < 0 && this.slideOffsetX + deltaX < 0)
) ? 0.3 : 1;
this.slideOffsetX += deltaX * factor;
}
},
onPointerUp () {
if (this.slideOffsetX < -this.slideThresh) {
// next page
this.paginate(1);
} else if (this.slideOffsetX > this.slideThresh) {
// prev page
this.paginate(-1);
} else {
// only apply the animation
this.paginate(0);
}
},
// Stop Firefox dragging the image
onImageDragStart(ev) {
ev.preventDefault();
return false
},
paginate (deltaIndex) {
let targetIndex = this.selIndex + deltaIndex;
if (targetIndex < 0 || targetIndex >= this.list.length) {
this.slideOffsetX = 0;
return
}
this.slideOffsetX = this.containerWidth * -deltaIndex;
this.autoSliding = true;
// update the selIndex before the animation to remove the delay feeling
this.$emit('input', targetIndex);
this.animSelIndex = targetIndex;
setTimeout(() => {
this.selIndex = targetIndex;
this.slideOffsetX = 0;
this.autoSliding = false;
}, 400);
},
onMouseDown (ev) {
this.isPointerDown = true;
this.lastPointerX = ev.clientX;
},
onMouseUp (ev) {
this.isPointerDown = false;
this.onPointerUp();
},
onMouseMove (ev) {
if (this.isPointerDown) {
this.onPointerMove(ev.clientX - this.lastPointerX);
this.lastPointerX = ev.clientX;
}
},
onTouchStart (ev) {
if (ev.touches.length === 1) {
this.isPointerDown = true;
this.lastPointerX = ev.touches[0].clientX;
}
},
onTouchEnd (ev) {
if (ev.touches.length === 0) {
this.isPointerDown = false;
this.onPointerUp();
}
},
onTouchMove (ev) {
if (ev.touches.length === 1) {
this.onPointerMove(ev.touches[0].clientX - this.lastPointerX);
this.lastPointerX = ev.touches[0].clientX;
}
},
onImageLoad (index, ev) {
let aspectRatio = ev.target.naturalWidth / ev.target.naturalHeight;
this.$set(this.imageAspectRatios, index, aspectRatio);
},
onImageSwipe (direction) {
this.paginate(direction == 'right' ? -1 : 1);
},
},
};
/* script */
const __vue_script__$1 = script$1;
/* template */
var __vue_render__$1 = function () {
var _vm = this;
var _h = _vm.$createElement;
var _c = _vm._self._c || _h;
return _c(
"div",
{
staticClass: "vue-zoomer-gallery",
class: {
anim: _vm.autoSliding && !_vm.isPointerDown,
},
style: {
"background-color": _vm.backgroundColor,
},
on: {
mousemove: _vm.onMouseMove,
mousedown: _vm.onMouseDown,
mouseout: _vm.onMouseUp,
mouseup: _vm.onMouseUp,
touchstart: _vm.onTouchStart,
touchend: _vm.onTouchEnd,
touchmove: function ($event) {
$event.preventDefault();
return _vm.onTouchMove.apply(null, arguments)
},
},
},
_vm._l(3, function (n, i) {
return _c(
"v-zoomer",
{
key: i + "-" + _vm.selIndex,
ref: "zoomers",
refInFor: true,
staticClass: "slide",
class: ["left", "middle", "right"][i],
style: [_vm.leftStyle, _vm.middleStyle, _vm.rightStyle][i],
attrs: {
"max-scale": 10,
zoomed: _vm.currentZoomed,
"reset-trigger": i,
"aspect-ratio": _vm.imageAspectRatios[_vm.selIndex + i - 1] || 1,
pivot: _vm.pivot,
"zooming-elastic": _vm.zoomingElastic,
"limit-translation": _vm.limitTranslation,
"double-click-to-zoom": _vm.doubleClickToZoom,
"mouse-wheel-to-zoom": _vm.mouseWheelToZoom,
},
on: {
"update:zoomed": function ($event) {
_vm.currentZoomed = $event;
},
swipe: _vm.onImageSwipe,
},
},
[
_vm.selIndex + i - 1 > -1 && _vm.selIndex + i - 1 < _vm.list.length
? _c("img", {
staticStyle: {
"object-fit": "contain",
width: "100%",
height: "100%",
},
attrs: {
src: _vm.list[_vm.selIndex + i - 1],
draggable: "false",
},
on: {
load: function ($event) {
return _vm.onImageLoad(_vm.selIndex + i - 1, $event)
},
dragstart: _vm.onImageDragStart,
},
})
: _vm._e(),
]
)
}),
1
)
};
var __vue_staticRenderFns__$1 = [];
__vue_render__$1._withStripped = true;
/* style */
const __vue_inject_styles__$1 = function (inject) {
if (!inject) return
inject("data-v-fc52b35c_0", { source: "\n.vue-zoomer-gallery[data-v-fc52b35c] {\n position: relative;\n overflow: hidden;\n user-select: none;\n min-width: 100px;\n min-height: 100px;\n}\n.vue-zoomer-gallery > *[data-v-fc52b35c] {\n display: inline-block;\n}\n.vue-zoomer-gallery.anim .slide[data-v-fc52b35c] {\n transition: left 0.4s;\n}\n.slide[data-v-fc52b35c] {\n position: absolute;\n top: 0;\n object-fit: contain;\n width: 100%;\n height: 100%;\n -webkit-user-drag: none;\n}\n", map: undefined, media: undefined });
};
/* scoped */
const __vue_scope_id__$1 = "data-v-fc52b35c";
/* module identifier */
const __vue_module_identifier__$1 = undefined;
/* functional template */
const __vue_is_functional_template__$1 = false;
/* style inject SSR */
/* style inject shadow dom */
const __vue_component__$1 = /*#__PURE__*/normalizeComponent(
{ render: __vue_render__$1, staticRenderFns: __vue_staticRenderFns__$1 },
__vue_inject_styles__$1,
__vue_script__$1,
__vue_scope_id__$1,
__vue_is_functional_template__$1,
__vue_module_identifier__$1,
false,
createInjector,
undefined,
undefined
);
var index = {
install (Vue) {
Vue.component('VZoomer', __vue_component__);
Vue.component('VZoomerGallery', __vue_component__$1);
},
// for locally register
Zoomer: __vue_component__,
Gallery: __vue_component__$1,
};
return index;
})));