photo-view
Version:
PhotoView - Pan, PinchZoom and Swipe for mobile
250 lines (209 loc) • 6.56 kB
JavaScript
'use strict';
import Hammer from 'hammerjs';
import CustomEvent from 'custom-event';
class PhotoViewManager {
constructor(options = {}) {
const defaultOptions = {
maxScale: 2,
enableMultiZoom: false,
snapToGrid: true,
tapToZoom: false
};
this.options = Object.assign(defaultOptions, options);
return this;
}
init(selector) {
let container = typeof selector === 'string' ? document.querySelectorAll(selector)[0] : selector;
if (!container) {
console.warn(`You must provide a valid container for PhotoView (selector "${selector}" did not match any element)`);
return;
}
this.image = container.querySelectorAll('img')[0];
if (!this.image) {
console.warn(`You must have a valid img tag inside your container`);
return;
}
this._manager = new Hammer.Manager(this.image, { touchAction: 'pan-y' });
this._registerGestures();
this._registerEvents();
this.scale = 1;
this.deltaX = 0;
this.deltaY = 0;
return this;
}
_dispatchEvent(type, detail) {
let event = new CustomEvent(
type,
{
bubbles: true,
cancelable: true,
detail: detail
}
);
document.dispatchEvent(event);
}
_registerGestures() {
const tap = new Hammer.Tap({ event: 'zoom', taps: 1 });
const doubleTap = new Hammer.Tap({ event: 'zoom', taps: 2 });
const pan = new Hammer.Pan({ direction: Hammer.DIRECTION_ALL, threshold: 10 });
const pinch = new Hammer.Pinch();
let gestures = [pinch, doubleTap, pan];
if(this.options.tapToZoom){
doubleTap.recognizeWith(tap);
tap.requireFailure(doubleTap);
gestures.splice(2,-1, tap);
}
this._manager.add(gestures);
}
_getZoomLevel() {
let scale;
if (this.options.enableMultiZoom) {
let midScale = this.options.maxScale / 2;
scale = this.scale === 1 ? midScale : (this.scale === midScale) ? this.options.maxScale : 1;
} else {
scale = this.scale > 1 ? 1 : this.options.maxScale;
}
return scale;
}
handleTouchEvent() {
this._dispatchEvent('photoview.scale.changed', {scale : this.scale})
}
_registerEvents() {
this._manager.on('zoom', e => {
let {x, y} = e.center;
let scale = this._getZoomLevel();
this._setTransition(true);
this._transform(x, y, scale);
this.handleTouchEvent();
});
this._manager.on('pinchstart', e => {
clearTimeout(this.panTimer);
this._enableGesture('pan', false);
let {x, y} = e.center;
this._setTransition(true);
if (this.scale === 1) {
this.pinchX = x;
this.pinchY = y;
}
});
this._manager.on('pinch', e => {
if (e.additionalEvent === 'pinchout') {
this._transform(this.pinchX, this.pinchY, this.options.maxScale);
} else if (e.additionalEvent === 'pinchin') {
this._transform(0, 0, 1);
}
this.handleTouchEvent();
});
this._manager.on('pinchend', e => {
this.panTimer = setTimeout(_ => this._enableGesture('pan', true), 1000);
});
this._manager.on('panstart', e => {
this._setTransition(false);
});
this._manager.on('pan', e => {
if (this.scale === 1) {
return;
}
e.srcEvent.stopPropagation();
this.currentDeltaX = (isNaN(this.deltaX) ? 0 : this.deltaX) + e.deltaX;
this.currentDeltaY = (isNaN(this.deltaY) ? 0 : this.deltaY) + e.deltaY;
if (this.options.snapToGrid) {
this._adjustSnapPositions();
}
this._setTransition(false);
this.image.style.transform = `translate3d(${this.currentDeltaX}px, ${this.currentDeltaY}px, 0px) scale(${this.scale})`;
});
this._manager.on('panend', e => {
this.deltaX = this.currentDeltaX;
this.deltaY = this.currentDeltaY;
});
}
_adjustSnapPositions() {
let imageOffsetLeft = this.image.offsetLeft;
let imageOffsetTop = this.image.offsetTop;
if (this.currentDeltaX + imageOffsetLeft > this.x) {
this.currentDeltaX = this.x - imageOffsetLeft;
} else if (this.x - this.currentDeltaX + imageOffsetLeft > this.image.width) {
let adjustWidth = (this.x - this.currentDeltaX + imageOffsetLeft) - this.image.width;
this.currentDeltaX = (this.currentDeltaX + adjustWidth);
}
if (this.currentDeltaY + imageOffsetTop > this.y) {
this.currentDeltaY = (this.y - imageOffsetTop);
} else if (this.y - this.currentDeltaY + imageOffsetTop > this.image.height) {
let adjustHeight = (this.y - this.currentDeltaY + imageOffsetTop) - this.image.height;
this.currentDeltaY = this.currentDeltaY + adjustHeight;
}
}
_transform(x, y, scale) {
if (scale === 1) {
x = this.x; y = this.y;
} else {
x -= this.image.offsetLeft;
y -= this.image.offsetTop;
}
this.image.style['transformOrigin'] = `${x}px ${y}px`;
this.image.style.transform = `scale3d(${scale},${scale},1)`;
this.x = x;
this.y = y;
this.scale = scale;
this._onTransformEnd();
}
_onTransformEnd() {
if (this.scale <= 1) {
this.deltaX = this.deltaY = 0;
}
}
_enableGesture(gesture, value) {
this._manager.get(gesture).set({ enable: value });
}
_setTransition(value) {
this.image.style.transition = value ? 'transform 0.5s' : 'none';
}
_unregisterEvents() {
this._manager.off('pan');
this._manager.off('pinch');
this._manager.off('zoom');
}
reset(animate) {
if(animate) {
this._setTransition(true);
this._transform(0, 0, 1);
this._setTransition(false);
} else {
this.image.style.transform = 'none';
}
this.resetScale();
}
resetScale() {
this.scale = 1;
this._dispatchEvent('photoview.scale.changed', {scale : this.scale})
}
destroy() {
this._unregisterEvents();
this._manager = null;
}
}
class PhotoView {
constructor(selector, options = {}) {
this.instances = [];
let slice = Array.prototype.slice;
let elements = slice.call(document.querySelectorAll(selector));
elements.forEach(item => {
this.instances.push(
new PhotoViewManager(options).init(item)
);
});
}
reset({ animate = false } = {}) {
this.instances.forEach(photoViewInstance => {
photoViewInstance.reset(animate);
});
}
destroy() {
this.instances.forEach(photoViewInstance => {
photoViewInstance.destroy();
});
this.instances = null;
}
}
export default PhotoView;