diff --git a/README.md b/README.md index bbf00a8..45f4700 100644 --- a/README.md +++ b/README.md @@ -347,8 +347,43 @@ panzoom(element, { The library will handle `ontouch` events very aggressively, it will `preventDefault`, and `stopPropagation` for the touch events inside container. [Sometimes](https://github.com/anvaka/panzoom/issues/12) this is not a desirable behavior. -If you want to take care about this yourself, you can pass `onTouch` callback to the options object: +If you want to take care about this yourself, you can pass `beforeTouchStart` callback to the options object. +Note: if you don't `preventDefault` yourself - make sure you test the page behavior on iOS devices. +Sometimes this may cause page to [bounce undesirably](https://stackoverflow.com/questions/23862204/disable-ios-safari-elastic-scrolling). + +### beforeTouchStart + +``` js +panzoom(element, { + beforeTouchStart: function(e) { + // `e` - is current touch event. + // below is the default behavior modify as needed + e.stopPropagation(); + e.preventDefault(); + + // You can return true to prevent library from handling this touch event. + // return true; + } +}); +``` + +E.g. Sometimes single finger touch interferes with scrolling. If you want to alleviate it you can provide a custom filter to ignore those. +``` js +panzoom(element, { + // disable when only 1 touch. + beforeTouchStart: (e) => { + // `e` - is current touch event. + e.stopPropagation(); + if (e.touches.length === 1) { + return true; // this return prevents library from handle this touch. + } + e.preventDefault(); + }, +}); +``` + +### ~~onTouch~~ (depreciated) ``` js panzoom(element, { onTouch: function(e) { @@ -359,10 +394,6 @@ panzoom(element, { }); ``` -Note: if you don't `preventDefault` yourself - make sure you test the page behavior on iOS devices. -Sometimes this may cause page to [bounce undesirably](https://stackoverflow.com/questions/23862204/disable-ios-safari-elastic-scrolling). - - ## Handling double click events By default panzoom will prevent default action on double click events - this is done to avoid @@ -370,6 +401,24 @@ accidental text selection (which is default browser action on double click). If allow default action, you can pass `onDoubleClick()` callback to options. If this callback returns false, then the library will not prevent default action: +### beforeDoubleClick + +``` js +panzoom(element, { + beforeDoubleClick: function(e) { + // `e` - is current touch event. + // below is the default behavior modify as needed + e.stopPropagation(); + e.preventDefault(); + + // You can return true to prevent library from handling this touch event. + // return true; + } +}); +``` + +### ~~onDoubleClick~~ (depreciated) + ``` js panzoom(element, { onDoubleClick: function(e) { diff --git a/demo/test/prevent_double_click.html b/demo/test/prevent_double_click.html new file mode 100644 index 0000000..06517b2 --- /dev/null +++ b/demo/test/prevent_double_click.html @@ -0,0 +1,67 @@ + + + + + + + + + + + DOM panzoom demo + + + +
I occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, ove zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable area
+
+
+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam enim lectus, euismod ac metus eget, consequat aliquam augue. Fusce vestibulum sagittis massa, eget iaculis lorem malesuada ut. Curabitur fringilla a lectus sed suscipit. Sed mollis ligula blandit ipsum posuere, et luctus sem iaculis. Suspendisse scelerisque mollis dapibus. Sed elementum placerat lacus, ac rutrum mauris varius in. Sed malesuada, ipsum in facilisis facilisis, eros massa euismod odio, id pretium augue purus sed risus. Nulla vitae purus enim. Suspendisse placerat ac turpis sed tempor. Cras et vulputate eros. Aenean volutpat tincidunt erat eu aliquam. Sed vel ex pulvinar, rutrum velit at, ullamcorper nibh. Ut ac rhoncus nulla. Pellentesque eu orci eu libero semper commodo ac sit amet massa. +

+

+ Donec condimentum odio ut lorem rhoncus pharetra. Maecenas nisl mi, faucibus ut tincidunt eu, lobortis at nunc. Suspendisse ut ipsum nec libero pharetra porta. Mauris porttitor neque nec mi rhoncus, a luctus massa consectetur. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Sed varius mauris et volutpat dignissim. Maecenas consectetur porta mollis. Morbi quis hendrerit massa. Cras vel eros vitae nisi mollis volutpat blandit id est. Vivamus fringilla iaculis lacus eu aliquet. Ut a varius augue, et accumsan nulla. Integer eu sem non erat porttitor posuere. Sed dui tortor, aliquam sed volutpat vitae, sodales non enim. Vestibulum libero nulla, tempus blandit pretium ac, ullamcorper eu nisl. +

+

+ Aenean quis rhoncus ante. Maecenas euismod non lacus nec accumsan. Integer vitae sollicitudin lacus. Aliquam in justo augue. Pellentesque nisl nisi, sollicitudin sed commodo vitae, vestibulum eget est. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis auctor laoreet lectus sit amet eleifend. Curabitur tempus est nunc, vel molestie mauris congue vel. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Praesent tincidunt aliquam massa, at ornare lacus tincidunt et. +

+

+ Nam posuere et ante in finibus. Proin sagittis iaculis lacus, scelerisque convallis nulla porttitor ac. Nunc sagittis velit vitae pharetra dapibus. Etiam tortor ante, facilisis a odio sed, pretium vehicula dui. Proin et pellentesque lacus, ac tristique nibh. Pellentesque vitae ex a justo fringilla pharetra. Aenean accumsan tempor sollicitudin. Integer elementum, quam at commodo vestibulum, odio massa egestas nibh, nec hendrerit lacus diam eget nisi. Integer pretium pretium purus, eu ornare ipsum posuere id. Nullam eget varius magna, ac blandit tellus. In hac habitasse platea dictumst. Praesent volutpat, purus quis rhoncus faucibus, orci lacus dictum purus, sed sagittis ligula ante ut augue. Curabitur eget est quis erat volutpat mattis. Aliquam eleifend ut tortor eu ultrices. +

+

+ Integer pretium erat et elit bibendum, ut laoreet tortor rutrum. Donec pulvinar faucibus enim vel molestie. Donec et euismod urna. Vestibulum nec feugiat magna. Nam at nunc lorem. Fusce sed ante eu purus posuere vulputate. In hac habitasse platea dictumst. Fusce consectetur elit a magna faucibus euismod. Ut congue efficitur ex. In dictum velit ac arcu condimentum, hendrerit venenatis dui tincidunt. Vestibulum pulvinar purus elementum felis tempus tincidunt. Aenean convallis, leo eu interdum varius, ante dui volutpat urna, a pharetra risus felis mattis mauris. Cras tincidunt justo enim, faucibus commodo nisl fringilla sed. Nullam facilisis, nisl a tincidunt euismod, purus odio sollicitudin sem, at ornare lorem sem ac nisi. Etiam a tincidunt tortor, consectetur porta nisl. Phasellus diam arcu, dapibus finibus nisi facilisis, dictum rutrum leo. +

+
+

+ Drag it or zoom it... +

+ + + +Fork me on GitHub + + diff --git a/demo/test/prevent_touch.html b/demo/test/prevent_touch.html new file mode 100644 index 0000000..0ed9a38 --- /dev/null +++ b/demo/test/prevent_touch.html @@ -0,0 +1,67 @@ + + + + + + + + + + + DOM panzoom demo + + + +
I occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, ove zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable areaI occupy space, above zoomable area
+
+
+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam enim lectus, euismod ac metus eget, consequat aliquam augue. Fusce vestibulum sagittis massa, eget iaculis lorem malesuada ut. Curabitur fringilla a lectus sed suscipit. Sed mollis ligula blandit ipsum posuere, et luctus sem iaculis. Suspendisse scelerisque mollis dapibus. Sed elementum placerat lacus, ac rutrum mauris varius in. Sed malesuada, ipsum in facilisis facilisis, eros massa euismod odio, id pretium augue purus sed risus. Nulla vitae purus enim. Suspendisse placerat ac turpis sed tempor. Cras et vulputate eros. Aenean volutpat tincidunt erat eu aliquam. Sed vel ex pulvinar, rutrum velit at, ullamcorper nibh. Ut ac rhoncus nulla. Pellentesque eu orci eu libero semper commodo ac sit amet massa. +

+

+ Donec condimentum odio ut lorem rhoncus pharetra. Maecenas nisl mi, faucibus ut tincidunt eu, lobortis at nunc. Suspendisse ut ipsum nec libero pharetra porta. Mauris porttitor neque nec mi rhoncus, a luctus massa consectetur. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Sed varius mauris et volutpat dignissim. Maecenas consectetur porta mollis. Morbi quis hendrerit massa. Cras vel eros vitae nisi mollis volutpat blandit id est. Vivamus fringilla iaculis lacus eu aliquet. Ut a varius augue, et accumsan nulla. Integer eu sem non erat porttitor posuere. Sed dui tortor, aliquam sed volutpat vitae, sodales non enim. Vestibulum libero nulla, tempus blandit pretium ac, ullamcorper eu nisl. +

+

+ Aenean quis rhoncus ante. Maecenas euismod non lacus nec accumsan. Integer vitae sollicitudin lacus. Aliquam in justo augue. Pellentesque nisl nisi, sollicitudin sed commodo vitae, vestibulum eget est. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis auctor laoreet lectus sit amet eleifend. Curabitur tempus est nunc, vel molestie mauris congue vel. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Praesent tincidunt aliquam massa, at ornare lacus tincidunt et. +

+

+ Nam posuere et ante in finibus. Proin sagittis iaculis lacus, scelerisque convallis nulla porttitor ac. Nunc sagittis velit vitae pharetra dapibus. Etiam tortor ante, facilisis a odio sed, pretium vehicula dui. Proin et pellentesque lacus, ac tristique nibh. Pellentesque vitae ex a justo fringilla pharetra. Aenean accumsan tempor sollicitudin. Integer elementum, quam at commodo vestibulum, odio massa egestas nibh, nec hendrerit lacus diam eget nisi. Integer pretium pretium purus, eu ornare ipsum posuere id. Nullam eget varius magna, ac blandit tellus. In hac habitasse platea dictumst. Praesent volutpat, purus quis rhoncus faucibus, orci lacus dictum purus, sed sagittis ligula ante ut augue. Curabitur eget est quis erat volutpat mattis. Aliquam eleifend ut tortor eu ultrices. +

+

+ Integer pretium erat et elit bibendum, ut laoreet tortor rutrum. Donec pulvinar faucibus enim vel molestie. Donec et euismod urna. Vestibulum nec feugiat magna. Nam at nunc lorem. Fusce sed ante eu purus posuere vulputate. In hac habitasse platea dictumst. Fusce consectetur elit a magna faucibus euismod. Ut congue efficitur ex. In dictum velit ac arcu condimentum, hendrerit venenatis dui tincidunt. Vestibulum pulvinar purus elementum felis tempus tincidunt. Aenean convallis, leo eu interdum varius, ante dui volutpat urna, a pharetra risus felis mattis mauris. Cras tincidunt justo enim, faucibus commodo nisl fringilla sed. Nullam facilisis, nisl a tincidunt euismod, purus odio sollicitudin sem, at ornare lorem sem ac nisi. Etiam a tincidunt tortor, consectetur porta nisl. Phasellus diam arcu, dapibus finibus nisi facilisis, dictum rutrum leo. +

+
+

+ Drag it or zoom it... +

+ + + +Fork me on GitHub + + diff --git a/dist/panzoom.js b/dist/panzoom.js index 3ed6a77..c07e8c2 100644 --- a/dist/panzoom.js +++ b/dist/panzoom.js @@ -68,6 +68,8 @@ function createPanZoom(domElement, options) { var zoomDoubleClickSpeed = typeof options.zoomDoubleClickSpeed === 'number' ? options.zoomDoubleClickSpeed : defaultDoubleTapZoomSpeed; var beforeWheel = options.beforeWheel || noop; var beforeMouseDown = options.beforeMouseDown || noop; + var beforeTouchStart = options.beforeTouchStart || options.onTouch || beforeTouchStartDefault; + var beforeDoubleClick = options.beforeDoubleClick || options.onDoubleClick || beforeDoubleClickDefault; var speed = typeof options.zoomSpeed === 'number' ? options.zoomSpeed : defaultZoomSpeed; var transformOrigin = parseTransformOrigin(options.transformOrigin); var textSelection = options.enableTextSelection ? fakeTextSelectorInterceptor : domTextSelectionInterceptor; @@ -591,7 +593,15 @@ function createPanZoom(domElement, options) { function onTouch(e) { // let them override the touch behavior - beforeTouch(e); + // support onTouch backwards compatibility. + if (beforeTouchStart(e)) { + if (options.onTouch) { + beforeTouchStartDefault(e); + } else { + return; + } + } + clearPendingClickEventTimeout(); if (e.touches.length === 1) { @@ -604,28 +614,12 @@ function createPanZoom(domElement, options) { } } - function beforeTouch(e) { - // TODO: Need to unify this filtering names. E.g. use `beforeTouch` - if (options.onTouch && !options.onTouch(e)) { - // if they return `false` from onTouch, we don't want to stop - // events propagation. Fixes https://github.com/anvaka/panzoom/issues/12 - return; - } - + function beforeTouchStartDefault(e) { e.stopPropagation(); e.preventDefault(); } - function beforeDoubleClick(e) { - clearPendingClickEventTimeout(); - - // TODO: Need to unify this filtering names. E.g. use `beforeDoubleClick`` - if (options.onDoubleClick && !options.onDoubleClick(e)) { - // if they return `false` from onTouch, we don't want to stop - // events propagation. Fixes https://github.com/anvaka/panzoom/issues/46 - return; - } - + function beforeDoubleClickDefault(e) { e.preventDefault(); e.stopPropagation(); } @@ -763,7 +757,16 @@ function createPanZoom(domElement, options) { } function onDoubleClick(e) { - beforeDoubleClick(e); + // support onDoubleClick backwards compatibility + if (beforeDoubleClick(e)) { + if (options.onDoubleClick) { + beforeDoubleClickDefault(e); + } else { + return; + } + } + clearPendingClickEventTimeout(); + var offset = getOffsetXY(e); if (transformOrigin) { // TODO: looks like this is duplicated in the file. @@ -1327,12 +1330,12 @@ function makeSvgController(svgElement, options) { } function getBBox() { - var bbox = svgElement.getBBox(); + var boundingBox = svgElement.getBBox(); return { - left: bbox.x, - top: bbox.y, - width: bbox.width, - height: bbox.height, + left: boundingBox.x, + top: boundingBox.y, + width: boundingBox.width, + height: boundingBox.height, }; } diff --git a/dist/panzoom.min.js b/dist/panzoom.min.js index a8984e4..d7495d6 100644 --- a/dist/panzoom.min.js +++ b/dist/panzoom.min.js @@ -1 +1 @@ -(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.panzoom=f()}})(function(){var define,module,exports;return function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i0){transform.x+=diff;adjusted=true}diff=boundingBox.right-clientRect.left;if(diff<0){transform.x+=diff;adjusted=true}diff=boundingBox.top-clientRect.bottom;if(diff>0){transform.y+=diff;adjusted=true}diff=boundingBox.bottom-clientRect.top;if(diff<0){transform.y+=diff;adjusted=true}return adjusted}function getBoundingBox(){if(!bounds)return;if(typeof bounds==="boolean"){var ownerRect=owner.getBoundingClientRect();var sceneWidth=ownerRect.width;var sceneHeight=ownerRect.height;return{left:sceneWidth*boundsPadding,top:sceneHeight*boundsPadding,right:sceneWidth*(1-boundsPadding),bottom:sceneHeight*(1-boundsPadding)}}return bounds}function getClientRect(){var bbox=panController.getBBox();var leftTop=client(bbox.left,bbox.top);return{left:leftTop.x,top:leftTop.y,right:bbox.width*transform.scale+leftTop.x,bottom:bbox.height*transform.scale+leftTop.y}}function client(x,y){return{x:x*transform.scale+transform.x,y:y*transform.scale+transform.y}}function makeDirty(){isDirty=true;frameAnimation=window.requestAnimationFrame(frame)}function zoomByRatio(clientX,clientY,ratio){if(isNaN(clientX)||isNaN(clientY)||isNaN(ratio)){throw new Error("zoom requires valid numbers")}var newScale=transform.scale*ratio;if(newScalemaxZoom){if(transform.scale===maxZoom)return;ratio=maxZoom/transform.scale}var size=transformToScreen(clientX,clientY);transform.x=size.x-ratio*(size.x-transform.x);transform.y=size.y-ratio*(size.y-transform.y);if(bounds&&boundsPadding===1&&minZoom===1){transform.scale*=ratio;keepTransformInsideBounds()}else{var transformAdjusted=keepTransformInsideBounds();if(!transformAdjusted)transform.scale*=ratio}triggerEvent("zoom");makeDirty()}function zoomAbs(clientX,clientY,zoomLevel){var ratio=zoomLevel/transform.scale;zoomByRatio(clientX,clientY,ratio)}function centerOn(ui){var parent=ui.ownerSVGElement;if(!parent)throw new Error("ui element is required to be within the scene");var clientRect=ui.getBoundingClientRect();var cx=clientRect.left+clientRect.width/2;var cy=clientRect.top+clientRect.height/2;var container=parent.getBoundingClientRect();var dx=container.width/2-cx;var dy=container.height/2-cy;internalMoveBy(dx,dy,true)}function smoothMoveTo(x,y){internalMoveBy(x-transform.x,y-transform.y,true)}function internalMoveBy(dx,dy,smooth){if(!smooth){return moveBy(dx,dy)}if(moveByAnimation)moveByAnimation.cancel();var from={x:0,y:0};var to={x:dx,y:dy};var lastX=0;var lastY=0;moveByAnimation=animate(from,to,{step:function(v){moveBy(v.x-lastX,v.y-lastY);lastX=v.x;lastY=v.y}})}function scroll(x,y){cancelZoomAnimation();moveTo(x,y)}function dispose(){releaseEvents()}function listenForEvents(){owner.addEventListener("mousedown",onMouseDown,{passive:false});owner.addEventListener("dblclick",onDoubleClick,{passive:false});owner.addEventListener("touchstart",onTouch,{passive:false});owner.addEventListener("keydown",onKeyDown,{passive:false});wheel.addWheelListener(owner,onMouseWheel,{passive:false});makeDirty()}function releaseEvents(){wheel.removeWheelListener(owner,onMouseWheel);owner.removeEventListener("mousedown",onMouseDown);owner.removeEventListener("keydown",onKeyDown);owner.removeEventListener("dblclick",onDoubleClick);owner.removeEventListener("touchstart",onTouch);if(frameAnimation){window.cancelAnimationFrame(frameAnimation);frameAnimation=0}smoothScroll.cancel();releaseDocumentMouse();releaseTouches();textSelection.release();triggerPanEnd()}function frame(){if(isDirty)applyTransform()}function applyTransform(){isDirty=false;panController.applyTransform(transform);triggerEvent("transform");frameAnimation=0}function onKeyDown(e){var x=0,y=0,z=0;if(e.keyCode===38){y=1}else if(e.keyCode===40){y=-1}else if(e.keyCode===37){x=1}else if(e.keyCode===39){x=-1}else if(e.keyCode===189||e.keyCode===109){z=1}else if(e.keyCode===187||e.keyCode===107){z=-1}if(filterKey(e,x,y,z)){return}if(x||y){e.preventDefault();e.stopPropagation();var clientRect=owner.getBoundingClientRect();var offset=Math.min(clientRect.width,clientRect.height);var moveSpeedRatio=.05;var dx=offset*moveSpeedRatio*x;var dy=offset*moveSpeedRatio*y;internalMoveBy(dx,dy)}if(z){var scaleMultiplier=getScaleMultiplier(z*100);var offset=transformOrigin?getTransformOriginOffset():midPoint();publicZoomTo(offset.x,offset.y,scaleMultiplier)}}function midPoint(){var ownerRect=owner.getBoundingClientRect();return{x:ownerRect.width/2,y:ownerRect.height/2}}function onTouch(e){beforeTouch(e);clearPendingClickEventTimeout();if(e.touches.length===1){return handleSingleFingerTouch(e,e.touches[0])}else if(e.touches.length===2){pinchZoomLength=getPinchZoomLength(e.touches[0],e.touches[1]);multiTouch=true;startTouchListenerIfNeeded()}}function beforeTouch(e){if(options.onTouch&&!options.onTouch(e)){return}e.stopPropagation();e.preventDefault()}function beforeDoubleClick(e){clearPendingClickEventTimeout();if(options.onDoubleClick&&!options.onDoubleClick(e)){return}e.preventDefault();e.stopPropagation()}function handleSingleFingerTouch(e){lastTouchStartTime=new Date;var touch=e.touches[0];var offset=getOffsetXY(touch);lastSingleFingerOffset=offset;var point=transformToScreen(offset.x,offset.y);mouseX=point.x;mouseY=point.y;clickX=mouseX;clickY=mouseY;smoothScroll.cancel();startTouchListenerIfNeeded()}function startTouchListenerIfNeeded(){if(touchInProgress){return}touchInProgress=true;document.addEventListener("touchmove",handleTouchMove);document.addEventListener("touchend",handleTouchEnd);document.addEventListener("touchcancel",handleTouchEnd)}function handleTouchMove(e){if(e.touches.length===1){e.stopPropagation();var touch=e.touches[0];var offset=getOffsetXY(touch);var point=transformToScreen(offset.x,offset.y);var dx=point.x-mouseX;var dy=point.y-mouseY;if(dx!==0&&dy!==0){triggerPanStart()}mouseX=point.x;mouseY=point.y;internalMoveBy(dx,dy)}else if(e.touches.length===2){multiTouch=true;var t1=e.touches[0];var t2=e.touches[1];var currentPinchLength=getPinchZoomLength(t1,t2);var scaleMultiplier=1+(currentPinchLength/pinchZoomLength-1)*pinchSpeed;var firstTouchPoint=getOffsetXY(t1);var secondTouchPoint=getOffsetXY(t2);mouseX=(firstTouchPoint.x+secondTouchPoint.x)/2;mouseY=(firstTouchPoint.y+secondTouchPoint.y)/2;if(transformOrigin){var offset=getTransformOriginOffset();mouseX=offset.x;mouseY=offset.y}publicZoomTo(mouseX,mouseY,scaleMultiplier);pinchZoomLength=currentPinchLength;e.stopPropagation();e.preventDefault()}}function clearPendingClickEventTimeout(){if(pendingClickEventTimeout){clearTimeout(pendingClickEventTimeout);pendingClickEventTimeout=0}}function handlePotentialClickEvent(e){if(!options.onClick)return;clearPendingClickEventTimeout();var dx=mouseX-clickX;var dy=mouseY-clickY;var l=Math.sqrt(dx*dx+dy*dy);if(l>5)return;pendingClickEventTimeout=setTimeout(function(){pendingClickEventTimeout=0;options.onClick(e)},doubleTapSpeedInMS)}function handleTouchEnd(e){clearPendingClickEventTimeout();if(e.touches.length>0){var offset=getOffsetXY(e.touches[0]);var point=transformToScreen(offset.x,offset.y);mouseX=point.x;mouseY=point.y}else{var now=new Date;if(now-lastTouchEndTime0)delta*=100;var scaleMultiplier=getScaleMultiplier(delta);if(scaleMultiplier!==1){var offset=transformOrigin?getTransformOriginOffset():getOffsetXY(e);publicZoomTo(offset.x,offset.y,scaleMultiplier);e.preventDefault()}}function getOffsetXY(e){var offsetX,offsetY;var ownerRect=owner.getBoundingClientRect();offsetX=e.clientX-ownerRect.left;offsetY=e.clientY-ownerRect.top;return{x:offsetX,y:offsetY}}function smoothZoom(clientX,clientY,scaleMultiplier){var fromValue=transform.scale;var from={scale:fromValue};var to={scale:scaleMultiplier*fromValue};smoothScroll.cancel();cancelZoomAnimation();zoomToAnimation=animate(from,to,{step:function(v){zoomAbs(clientX,clientY,v.scale)},done:triggerZoomEnd})}function smoothZoomAbs(clientX,clientY,toScaleValue){var fromValue=transform.scale;var from={scale:fromValue};var to={scale:toScaleValue};smoothScroll.cancel();cancelZoomAnimation();zoomToAnimation=animate(from,to,{step:function(v){zoomAbs(clientX,clientY,v.scale)}})}function getTransformOriginOffset(){var ownerRect=owner.getBoundingClientRect();return{x:ownerRect.width*transformOrigin.x,y:ownerRect.height*transformOrigin.y}}function publicZoomTo(clientX,clientY,scaleMultiplier){smoothScroll.cancel();cancelZoomAnimation();return zoomByRatio(clientX,clientY,scaleMultiplier)}function cancelZoomAnimation(){if(zoomToAnimation){zoomToAnimation.cancel();zoomToAnimation=null}}function getScaleMultiplier(delta){var sign=Math.sign(delta);var deltaAdjustedSpeed=Math.min(.25,Math.abs(speed*delta/128));return 1-sign*deltaAdjustedSpeed}function triggerPanStart(){if(!panstartFired){triggerEvent("panstart");panstartFired=true;smoothScroll.start()}}function triggerPanEnd(){if(panstartFired){if(!multiTouch)smoothScroll.stop();triggerEvent("panend")}}function triggerZoomEnd(){triggerEvent("zoomend")}function triggerEvent(name){api.fire(name,api)}}function parseTransformOrigin(options){if(!options)return;if(typeof options==="object"){if(!isNumber(options.x)||!isNumber(options.y))failTransformOrigin(options);return options}failTransformOrigin()}function failTransformOrigin(options){console.error(options);throw new Error(["Cannot parse transform origin.","Some good examples:",' "center center" can be achieved with {x: 0.5, y: 0.5}',' "top center" can be achieved with {x: 0.5, y: 0}',' "bottom right" can be achieved with {x: 1, y: 1}'].join("\n"))}function noop(){}function validateBounds(bounds){var boundsType=typeof bounds;if(boundsType==="undefined"||boundsType==="boolean")return;var validBounds=isNumber(bounds.left)&&isNumber(bounds.top)&&isNumber(bounds.bottom)&&isNumber(bounds.right);if(!validBounds)throw new Error("Bounds object is not valid. It can be: "+"undefined, boolean (true|false) or an object {left, top, right, bottom}")}function isNumber(x){return Number.isFinite(x)}function isNaN(value){if(Number.isNaN){return Number.isNaN(value)}return value!==value}function rigidScroll(){return{start:noop,stop:noop,cancel:noop}}function autoRun(){if(typeof document==="undefined")return;var scripts=document.getElementsByTagName("script");if(!scripts)return;var panzoomScript;for(var i=0;iminVelocity){ax=amplitude*vx;targetX+=ax}if(vy<-minVelocity||vy>minVelocity){ay=amplitude*vy;targetY+=ay}raf=requestAnimationFrame(autoScroll)}function autoScroll(){var elapsed=Date.now()-timestamp;var moving=false;var dx=0;var dy=0;if(ax){dx=-ax*Math.exp(-elapsed/timeConstant);if(dx>.5||dx<-.5)moving=true;else dx=ax=0}if(ay){dy=-ay*Math.exp(-elapsed/timeConstant);if(dy>.5||dy<-.5)moving=true;else dy=ay=0}if(moving){scroll(targetX+dx,targetY+dy);raf=requestAnimationFrame(autoScroll)}}}function getCancelAnimationFrame(){if(typeof cancelAnimationFrame==="function")return cancelAnimationFrame;return clearTimeout}function getRequestAnimationFrame(){if(typeof requestAnimationFrame==="function")return requestAnimationFrame;return function(handler){return setTimeout(handler,16)}}},{}],3:[function(require,module,exports){module.exports=makeDomController;module.exports.canAttach=isDomElement;function makeDomController(domElement,options){var elementValid=isDomElement(domElement);if(!elementValid){throw new Error("panzoom requires DOM element to be attached to the DOM tree")}var owner=domElement.parentElement;domElement.scrollTop=0;if(!options.disableKeyboardInteraction){owner.setAttribute("tabindex",0)}var api={getBBox:getBBox,getOwner:getOwner,applyTransform:applyTransform};return api;function getOwner(){return owner}function getBBox(){return{left:0,top:0,width:domElement.clientWidth,height:domElement.clientHeight}}function applyTransform(transform){domElement.style.transformOrigin="0 0 0";domElement.style.transform="matrix("+transform.scale+", 0, 0, "+transform.scale+", "+transform.x+", "+transform.y+")"}}function isDomElement(element){return element&&element.parentElement&&element.style}},{}],4:[function(require,module,exports){module.exports=makeSvgController;module.exports.canAttach=isSVGElement;function makeSvgController(svgElement,options){if(!isSVGElement(svgElement)){throw new Error("svg element is required for svg.panzoom to work")}var owner=svgElement.ownerSVGElement;if(!owner){throw new Error("Do not apply panzoom to the root element. "+"Use its child instead (e.g. ). "+"As of March 2016 only FireFox supported transform on the root element")}if(!options.disableKeyboardInteraction){owner.setAttribute("tabindex",0)}var api={getBBox:getBBox,getScreenCTM:getScreenCTM,getOwner:getOwner,applyTransform:applyTransform,initTransform:initTransform};return api;function getOwner(){return owner}function getBBox(){var bbox=svgElement.getBBox();return{left:bbox.x,top:bbox.y,width:bbox.width,height:bbox.height}}function getScreenCTM(){var ctm=owner.getCTM();if(!ctm){return owner.getScreenCTM()}return ctm}function initTransform(transform){var screenCTM=svgElement.getCTM();if(screenCTM===null){screenCTM=document.createElementNS("http://www.w3.org/2000/svg","svg").createSVGMatrix()}transform.x=screenCTM.e;transform.y=screenCTM.f;transform.scale=screenCTM.a;owner.removeAttributeNS(null,"viewBox")}function applyTransform(transform){svgElement.setAttribute("transform","matrix("+transform.scale+" 0 0 "+transform.scale+" "+transform.x+" "+transform.y+")")}}function isSVGElement(element){return element&&element.ownerSVGElement&&element.getCTM}},{}],5:[function(require,module,exports){module.exports=makeTextSelectionInterceptor;function makeTextSelectionInterceptor(useFake){if(useFake){return{capture:noop,release:noop}}var dragObject;var prevSelectStart;var prevDragStart;var wasCaptured=false;return{capture:capture,release:release};function capture(domObject){wasCaptured=true;prevSelectStart=window.document.onselectstart;prevDragStart=window.document.ondragstart;window.document.onselectstart=disabled;dragObject=domObject;dragObject.ondragstart=disabled}function release(){if(!wasCaptured)return;wasCaptured=false;window.document.onselectstart=prevSelectStart;if(dragObject)dragObject.ondragstart=prevDragStart}}function disabled(e){e.stopPropagation();return false}function noop(){}},{}],6:[function(require,module,exports){module.exports=Transform;function Transform(){this.x=0;this.y=0;this.scale=1}},{}],7:[function(require,module,exports){var BezierEasing=require("bezier-easing");var animations={ease:BezierEasing(.25,.1,.25,1),easeIn:BezierEasing(.42,0,1,1),easeOut:BezierEasing(0,0,.58,1),easeInOut:BezierEasing(.42,0,.58,1),linear:BezierEasing(0,0,1,1)};module.exports=animate;module.exports.makeAggregateRaf=makeAggregateRaf;module.exports.sharedScheduler=makeAggregateRaf();function animate(source,target,options){var start=Object.create(null);var diff=Object.create(null);options=options||{};var easing=typeof options.easing==="function"?options.easing:animations[options.easing];if(!easing){if(options.easing){console.warn("Unknown easing function in amator: "+options.easing)}easing=animations.ease}var step=typeof options.step==="function"?options.step:noop;var done=typeof options.done==="function"?options.done:noop;var scheduler=getScheduler(options.scheduler);var keys=Object.keys(target);keys.forEach(function(key){start[key]=source[key];diff[key]=target[key]-source[key]});var durationInMs=typeof options.duration==="number"?options.duration:400;var durationInFrames=Math.max(1,durationInMs*.06);var previousAnimationId;var frame=0;previousAnimationId=scheduler.next(loop);return{cancel:cancel};function cancel(){scheduler.cancel(previousAnimationId);previousAnimationId=0}function loop(){var t=easing(frame/durationInFrames);frame+=1;setValues(t);if(frame<=durationInFrames){previousAnimationId=scheduler.next(loop);step(source)}else{previousAnimationId=0;setTimeout(function(){done(source)},0)}}function setValues(t){keys.forEach(function(key){source[key]=diff[key]*t+start[key]})}}function noop(){}function getScheduler(scheduler){if(!scheduler){var canRaf=typeof window!=="undefined"&&window.requestAnimationFrame;return canRaf?rafScheduler():timeoutScheduler()}if(typeof scheduler.next!=="function")throw new Error("Scheduler is supposed to have next(cb) function");if(typeof scheduler.cancel!=="function")throw new Error("Scheduler is supposed to have cancel(handle) function");return scheduler}function rafScheduler(){return{next:window.requestAnimationFrame.bind(window),cancel:window.cancelAnimationFrame.bind(window)}}function timeoutScheduler(){return{next:function(cb){return setTimeout(cb,1e3/60)},cancel:function(id){return clearTimeout(id)}}}function makeAggregateRaf(){var frontBuffer=new Set;var backBuffer=new Set;var frameToken=0;return{next:next,cancel:next,clearAll:clearAll};function clearAll(){frontBuffer.clear();backBuffer.clear();cancelAnimationFrame(frameToken);frameToken=0}function next(callback){backBuffer.add(callback);renderNextFrame()}function renderNextFrame(){if(!frameToken)frameToken=requestAnimationFrame(renderFrame)}function renderFrame(){frameToken=0;var t=backBuffer;backBuffer=frontBuffer;frontBuffer=t;frontBuffer.forEach(function(callback){callback()});frontBuffer.clear()}function cancel(callback){backBuffer.delete(callback)}}},{"bezier-easing":8}],8:[function(require,module,exports){var NEWTON_ITERATIONS=4;var NEWTON_MIN_SLOPE=.001;var SUBDIVISION_PRECISION=1e-7;var SUBDIVISION_MAX_ITERATIONS=10;var kSplineTableSize=11;var kSampleStepSize=1/(kSplineTableSize-1);var float32ArraySupported=typeof Float32Array==="function";function A(aA1,aA2){return 1-3*aA2+3*aA1}function B(aA1,aA2){return 3*aA2-6*aA1}function C(aA1){return 3*aA1}function calcBezier(aT,aA1,aA2){return((A(aA1,aA2)*aT+B(aA1,aA2))*aT+C(aA1))*aT}function getSlope(aT,aA1,aA2){return 3*A(aA1,aA2)*aT*aT+2*B(aA1,aA2)*aT+C(aA1)}function binarySubdivide(aX,aA,aB,mX1,mX2){var currentX,currentT,i=0;do{currentT=aA+(aB-aA)/2;currentX=calcBezier(currentT,mX1,mX2)-aX;if(currentX>0){aB=currentT}else{aA=currentT}}while(Math.abs(currentX)>SUBDIVISION_PRECISION&&++i=NEWTON_MIN_SLOPE){return newtonRaphsonIterate(aX,guessForT,mX1,mX2)}else if(initialSlope===0){return guessForT}else{return binarySubdivide(aX,intervalStart,intervalStart+kSampleStepSize,mX1,mX2)}}return function BezierEasing(x){if(x===0){return 0}if(x===1){return 1}return calcBezier(getTForX(x),mY1,mY2)}}},{}],9:[function(require,module,exports){module.exports=function eventify(subject){validateSubject(subject);var eventsStorage=createEventsStorage(subject);subject.on=eventsStorage.on;subject.off=eventsStorage.off;subject.fire=eventsStorage.fire;return subject};function createEventsStorage(subject){var registeredEvents=Object.create(null);return{on:function(eventName,callback,ctx){if(typeof callback!=="function"){throw new Error("callback is expected to be a function")}var handlers=registeredEvents[eventName];if(!handlers){handlers=registeredEvents[eventName]=[]}handlers.push({callback:callback,ctx:ctx});return subject},off:function(eventName,callback){var wantToRemoveAll=typeof eventName==="undefined";if(wantToRemoveAll){registeredEvents=Object.create(null);return subject}if(registeredEvents[eventName]){var deleteAllCallbacksForEvent=typeof callback!=="function";if(deleteAllCallbacksForEvent){delete registeredEvents[eventName]}else{var callbacks=registeredEvents[eventName];for(var i=0;i1){fireArguments=Array.prototype.splice.call(arguments,1)}for(var i=0;i0){transform.x+=diff;adjusted=true}diff=boundingBox.right-clientRect.left;if(diff<0){transform.x+=diff;adjusted=true}diff=boundingBox.top-clientRect.bottom;if(diff>0){transform.y+=diff;adjusted=true}diff=boundingBox.bottom-clientRect.top;if(diff<0){transform.y+=diff;adjusted=true}return adjusted}function getBoundingBox(){if(!bounds)return;if(typeof bounds==="boolean"){var ownerRect=owner.getBoundingClientRect();var sceneWidth=ownerRect.width;var sceneHeight=ownerRect.height;return{left:sceneWidth*boundsPadding,top:sceneHeight*boundsPadding,right:sceneWidth*(1-boundsPadding),bottom:sceneHeight*(1-boundsPadding)}}return bounds}function getClientRect(){var bbox=panController.getBBox();var leftTop=client(bbox.left,bbox.top);return{left:leftTop.x,top:leftTop.y,right:bbox.width*transform.scale+leftTop.x,bottom:bbox.height*transform.scale+leftTop.y}}function client(x,y){return{x:x*transform.scale+transform.x,y:y*transform.scale+transform.y}}function makeDirty(){isDirty=true;frameAnimation=window.requestAnimationFrame(frame)}function zoomByRatio(clientX,clientY,ratio){if(isNaN(clientX)||isNaN(clientY)||isNaN(ratio)){throw new Error("zoom requires valid numbers")}var newScale=transform.scale*ratio;if(newScalemaxZoom){if(transform.scale===maxZoom)return;ratio=maxZoom/transform.scale}var size=transformToScreen(clientX,clientY);transform.x=size.x-ratio*(size.x-transform.x);transform.y=size.y-ratio*(size.y-transform.y);if(bounds&&boundsPadding===1&&minZoom===1){transform.scale*=ratio;keepTransformInsideBounds()}else{var transformAdjusted=keepTransformInsideBounds();if(!transformAdjusted)transform.scale*=ratio}triggerEvent("zoom");makeDirty()}function zoomAbs(clientX,clientY,zoomLevel){var ratio=zoomLevel/transform.scale;zoomByRatio(clientX,clientY,ratio)}function centerOn(ui){var parent=ui.ownerSVGElement;if(!parent)throw new Error("ui element is required to be within the scene");var clientRect=ui.getBoundingClientRect();var cx=clientRect.left+clientRect.width/2;var cy=clientRect.top+clientRect.height/2;var container=parent.getBoundingClientRect();var dx=container.width/2-cx;var dy=container.height/2-cy;internalMoveBy(dx,dy,true)}function smoothMoveTo(x,y){internalMoveBy(x-transform.x,y-transform.y,true)}function internalMoveBy(dx,dy,smooth){if(!smooth){return moveBy(dx,dy)}if(moveByAnimation)moveByAnimation.cancel();var from={x:0,y:0};var to={x:dx,y:dy};var lastX=0;var lastY=0;moveByAnimation=animate(from,to,{step:function(v){moveBy(v.x-lastX,v.y-lastY);lastX=v.x;lastY=v.y}})}function scroll(x,y){cancelZoomAnimation();moveTo(x,y)}function dispose(){releaseEvents()}function listenForEvents(){owner.addEventListener("mousedown",onMouseDown,{passive:false});owner.addEventListener("dblclick",onDoubleClick,{passive:false});owner.addEventListener("touchstart",onTouch,{passive:false});owner.addEventListener("keydown",onKeyDown,{passive:false});wheel.addWheelListener(owner,onMouseWheel,{passive:false});makeDirty()}function releaseEvents(){wheel.removeWheelListener(owner,onMouseWheel);owner.removeEventListener("mousedown",onMouseDown);owner.removeEventListener("keydown",onKeyDown);owner.removeEventListener("dblclick",onDoubleClick);owner.removeEventListener("touchstart",onTouch);if(frameAnimation){window.cancelAnimationFrame(frameAnimation);frameAnimation=0}smoothScroll.cancel();releaseDocumentMouse();releaseTouches();textSelection.release();triggerPanEnd()}function frame(){if(isDirty)applyTransform()}function applyTransform(){isDirty=false;panController.applyTransform(transform);triggerEvent("transform");frameAnimation=0}function onKeyDown(e){var x=0,y=0,z=0;if(e.keyCode===38){y=1}else if(e.keyCode===40){y=-1}else if(e.keyCode===37){x=1}else if(e.keyCode===39){x=-1}else if(e.keyCode===189||e.keyCode===109){z=1}else if(e.keyCode===187||e.keyCode===107){z=-1}if(filterKey(e,x,y,z)){return}if(x||y){e.preventDefault();e.stopPropagation();var clientRect=owner.getBoundingClientRect();var offset=Math.min(clientRect.width,clientRect.height);var moveSpeedRatio=.05;var dx=offset*moveSpeedRatio*x;var dy=offset*moveSpeedRatio*y;internalMoveBy(dx,dy)}if(z){var scaleMultiplier=getScaleMultiplier(z*100);var offset=transformOrigin?getTransformOriginOffset():midPoint();publicZoomTo(offset.x,offset.y,scaleMultiplier)}}function midPoint(){var ownerRect=owner.getBoundingClientRect();return{x:ownerRect.width/2,y:ownerRect.height/2}}function onTouch(e){if(beforeTouchStart(e)){if(options.onTouch){beforeTouchStartDefault(e)}else{return}}clearPendingClickEventTimeout();if(e.touches.length===1){return handleSingleFingerTouch(e,e.touches[0])}else if(e.touches.length===2){pinchZoomLength=getPinchZoomLength(e.touches[0],e.touches[1]);multiTouch=true;startTouchListenerIfNeeded()}}function beforeTouchStartDefault(e){e.stopPropagation();e.preventDefault()}function beforeDoubleClickDefault(e){e.preventDefault();e.stopPropagation()}function handleSingleFingerTouch(e){lastTouchStartTime=new Date;var touch=e.touches[0];var offset=getOffsetXY(touch);lastSingleFingerOffset=offset;var point=transformToScreen(offset.x,offset.y);mouseX=point.x;mouseY=point.y;clickX=mouseX;clickY=mouseY;smoothScroll.cancel();startTouchListenerIfNeeded()}function startTouchListenerIfNeeded(){if(touchInProgress){return}touchInProgress=true;document.addEventListener("touchmove",handleTouchMove);document.addEventListener("touchend",handleTouchEnd);document.addEventListener("touchcancel",handleTouchEnd)}function handleTouchMove(e){if(e.touches.length===1){e.stopPropagation();var touch=e.touches[0];var offset=getOffsetXY(touch);var point=transformToScreen(offset.x,offset.y);var dx=point.x-mouseX;var dy=point.y-mouseY;if(dx!==0&&dy!==0){triggerPanStart()}mouseX=point.x;mouseY=point.y;internalMoveBy(dx,dy)}else if(e.touches.length===2){multiTouch=true;var t1=e.touches[0];var t2=e.touches[1];var currentPinchLength=getPinchZoomLength(t1,t2);var scaleMultiplier=1+(currentPinchLength/pinchZoomLength-1)*pinchSpeed;var firstTouchPoint=getOffsetXY(t1);var secondTouchPoint=getOffsetXY(t2);mouseX=(firstTouchPoint.x+secondTouchPoint.x)/2;mouseY=(firstTouchPoint.y+secondTouchPoint.y)/2;if(transformOrigin){var offset=getTransformOriginOffset();mouseX=offset.x;mouseY=offset.y}publicZoomTo(mouseX,mouseY,scaleMultiplier);pinchZoomLength=currentPinchLength;e.stopPropagation();e.preventDefault()}}function clearPendingClickEventTimeout(){if(pendingClickEventTimeout){clearTimeout(pendingClickEventTimeout);pendingClickEventTimeout=0}}function handlePotentialClickEvent(e){if(!options.onClick)return;clearPendingClickEventTimeout();var dx=mouseX-clickX;var dy=mouseY-clickY;var l=Math.sqrt(dx*dx+dy*dy);if(l>5)return;pendingClickEventTimeout=setTimeout(function(){pendingClickEventTimeout=0;options.onClick(e)},doubleTapSpeedInMS)}function handleTouchEnd(e){clearPendingClickEventTimeout();if(e.touches.length>0){var offset=getOffsetXY(e.touches[0]);var point=transformToScreen(offset.x,offset.y);mouseX=point.x;mouseY=point.y}else{var now=new Date;if(now-lastTouchEndTime0)delta*=100;var scaleMultiplier=getScaleMultiplier(delta);if(scaleMultiplier!==1){var offset=transformOrigin?getTransformOriginOffset():getOffsetXY(e);publicZoomTo(offset.x,offset.y,scaleMultiplier);e.preventDefault()}}function getOffsetXY(e){var offsetX,offsetY;var ownerRect=owner.getBoundingClientRect();offsetX=e.clientX-ownerRect.left;offsetY=e.clientY-ownerRect.top;return{x:offsetX,y:offsetY}}function smoothZoom(clientX,clientY,scaleMultiplier){var fromValue=transform.scale;var from={scale:fromValue};var to={scale:scaleMultiplier*fromValue};smoothScroll.cancel();cancelZoomAnimation();zoomToAnimation=animate(from,to,{step:function(v){zoomAbs(clientX,clientY,v.scale)},done:triggerZoomEnd})}function smoothZoomAbs(clientX,clientY,toScaleValue){var fromValue=transform.scale;var from={scale:fromValue};var to={scale:toScaleValue};smoothScroll.cancel();cancelZoomAnimation();zoomToAnimation=animate(from,to,{step:function(v){zoomAbs(clientX,clientY,v.scale)}})}function getTransformOriginOffset(){var ownerRect=owner.getBoundingClientRect();return{x:ownerRect.width*transformOrigin.x,y:ownerRect.height*transformOrigin.y}}function publicZoomTo(clientX,clientY,scaleMultiplier){smoothScroll.cancel();cancelZoomAnimation();return zoomByRatio(clientX,clientY,scaleMultiplier)}function cancelZoomAnimation(){if(zoomToAnimation){zoomToAnimation.cancel();zoomToAnimation=null}}function getScaleMultiplier(delta){var sign=Math.sign(delta);var deltaAdjustedSpeed=Math.min(.25,Math.abs(speed*delta/128));return 1-sign*deltaAdjustedSpeed}function triggerPanStart(){if(!panstartFired){triggerEvent("panstart");panstartFired=true;smoothScroll.start()}}function triggerPanEnd(){if(panstartFired){if(!multiTouch)smoothScroll.stop();triggerEvent("panend")}}function triggerZoomEnd(){triggerEvent("zoomend")}function triggerEvent(name){api.fire(name,api)}}function parseTransformOrigin(options){if(!options)return;if(typeof options==="object"){if(!isNumber(options.x)||!isNumber(options.y))failTransformOrigin(options);return options}failTransformOrigin()}function failTransformOrigin(options){console.error(options);throw new Error(["Cannot parse transform origin.","Some good examples:",' "center center" can be achieved with {x: 0.5, y: 0.5}',' "top center" can be achieved with {x: 0.5, y: 0}',' "bottom right" can be achieved with {x: 1, y: 1}'].join("\n"))}function noop(){}function validateBounds(bounds){var boundsType=typeof bounds;if(boundsType==="undefined"||boundsType==="boolean")return;var validBounds=isNumber(bounds.left)&&isNumber(bounds.top)&&isNumber(bounds.bottom)&&isNumber(bounds.right);if(!validBounds)throw new Error("Bounds object is not valid. It can be: "+"undefined, boolean (true|false) or an object {left, top, right, bottom}")}function isNumber(x){return Number.isFinite(x)}function isNaN(value){if(Number.isNaN){return Number.isNaN(value)}return value!==value}function rigidScroll(){return{start:noop,stop:noop,cancel:noop}}function autoRun(){if(typeof document==="undefined")return;var scripts=document.getElementsByTagName("script");if(!scripts)return;var panzoomScript;for(var i=0;iminVelocity){ax=amplitude*vx;targetX+=ax}if(vy<-minVelocity||vy>minVelocity){ay=amplitude*vy;targetY+=ay}raf=requestAnimationFrame(autoScroll)}function autoScroll(){var elapsed=Date.now()-timestamp;var moving=false;var dx=0;var dy=0;if(ax){dx=-ax*Math.exp(-elapsed/timeConstant);if(dx>.5||dx<-.5)moving=true;else dx=ax=0}if(ay){dy=-ay*Math.exp(-elapsed/timeConstant);if(dy>.5||dy<-.5)moving=true;else dy=ay=0}if(moving){scroll(targetX+dx,targetY+dy);raf=requestAnimationFrame(autoScroll)}}}function getCancelAnimationFrame(){if(typeof cancelAnimationFrame==="function")return cancelAnimationFrame;return clearTimeout}function getRequestAnimationFrame(){if(typeof requestAnimationFrame==="function")return requestAnimationFrame;return function(handler){return setTimeout(handler,16)}}},{}],3:[function(require,module,exports){module.exports=makeDomController;module.exports.canAttach=isDomElement;function makeDomController(domElement,options){var elementValid=isDomElement(domElement);if(!elementValid){throw new Error("panzoom requires DOM element to be attached to the DOM tree")}var owner=domElement.parentElement;domElement.scrollTop=0;if(!options.disableKeyboardInteraction){owner.setAttribute("tabindex",0)}var api={getBBox:getBBox,getOwner:getOwner,applyTransform:applyTransform};return api;function getOwner(){return owner}function getBBox(){return{left:0,top:0,width:domElement.clientWidth,height:domElement.clientHeight}}function applyTransform(transform){domElement.style.transformOrigin="0 0 0";domElement.style.transform="matrix("+transform.scale+", 0, 0, "+transform.scale+", "+transform.x+", "+transform.y+")"}}function isDomElement(element){return element&&element.parentElement&&element.style}},{}],4:[function(require,module,exports){module.exports=makeSvgController;module.exports.canAttach=isSVGElement;function makeSvgController(svgElement,options){if(!isSVGElement(svgElement)){throw new Error("svg element is required for svg.panzoom to work")}var owner=svgElement.ownerSVGElement;if(!owner){throw new Error("Do not apply panzoom to the root element. "+"Use its child instead (e.g. ). "+"As of March 2016 only FireFox supported transform on the root element")}if(!options.disableKeyboardInteraction){owner.setAttribute("tabindex",0)}var api={getBBox:getBBox,getScreenCTM:getScreenCTM,getOwner:getOwner,applyTransform:applyTransform,initTransform:initTransform};return api;function getOwner(){return owner}function getBBox(){var boundingBox=svgElement.getBBox();return{left:boundingBox.x,top:boundingBox.y,width:boundingBox.width,height:boundingBox.height}}function getScreenCTM(){var ctm=owner.getCTM();if(!ctm){return owner.getScreenCTM()}return ctm}function initTransform(transform){var screenCTM=svgElement.getCTM();if(screenCTM===null){screenCTM=document.createElementNS("http://www.w3.org/2000/svg","svg").createSVGMatrix()}transform.x=screenCTM.e;transform.y=screenCTM.f;transform.scale=screenCTM.a;owner.removeAttributeNS(null,"viewBox")}function applyTransform(transform){svgElement.setAttribute("transform","matrix("+transform.scale+" 0 0 "+transform.scale+" "+transform.x+" "+transform.y+")")}}function isSVGElement(element){return element&&element.ownerSVGElement&&element.getCTM}},{}],5:[function(require,module,exports){module.exports=makeTextSelectionInterceptor;function makeTextSelectionInterceptor(useFake){if(useFake){return{capture:noop,release:noop}}var dragObject;var prevSelectStart;var prevDragStart;var wasCaptured=false;return{capture:capture,release:release};function capture(domObject){wasCaptured=true;prevSelectStart=window.document.onselectstart;prevDragStart=window.document.ondragstart;window.document.onselectstart=disabled;dragObject=domObject;dragObject.ondragstart=disabled}function release(){if(!wasCaptured)return;wasCaptured=false;window.document.onselectstart=prevSelectStart;if(dragObject)dragObject.ondragstart=prevDragStart}}function disabled(e){e.stopPropagation();return false}function noop(){}},{}],6:[function(require,module,exports){module.exports=Transform;function Transform(){this.x=0;this.y=0;this.scale=1}},{}],7:[function(require,module,exports){var BezierEasing=require("bezier-easing");var animations={ease:BezierEasing(.25,.1,.25,1),easeIn:BezierEasing(.42,0,1,1),easeOut:BezierEasing(0,0,.58,1),easeInOut:BezierEasing(.42,0,.58,1),linear:BezierEasing(0,0,1,1)};module.exports=animate;module.exports.makeAggregateRaf=makeAggregateRaf;module.exports.sharedScheduler=makeAggregateRaf();function animate(source,target,options){var start=Object.create(null);var diff=Object.create(null);options=options||{};var easing=typeof options.easing==="function"?options.easing:animations[options.easing];if(!easing){if(options.easing){console.warn("Unknown easing function in amator: "+options.easing)}easing=animations.ease}var step=typeof options.step==="function"?options.step:noop;var done=typeof options.done==="function"?options.done:noop;var scheduler=getScheduler(options.scheduler);var keys=Object.keys(target);keys.forEach(function(key){start[key]=source[key];diff[key]=target[key]-source[key]});var durationInMs=typeof options.duration==="number"?options.duration:400;var durationInFrames=Math.max(1,durationInMs*.06);var previousAnimationId;var frame=0;previousAnimationId=scheduler.next(loop);return{cancel:cancel};function cancel(){scheduler.cancel(previousAnimationId);previousAnimationId=0}function loop(){var t=easing(frame/durationInFrames);frame+=1;setValues(t);if(frame<=durationInFrames){previousAnimationId=scheduler.next(loop);step(source)}else{previousAnimationId=0;setTimeout(function(){done(source)},0)}}function setValues(t){keys.forEach(function(key){source[key]=diff[key]*t+start[key]})}}function noop(){}function getScheduler(scheduler){if(!scheduler){var canRaf=typeof window!=="undefined"&&window.requestAnimationFrame;return canRaf?rafScheduler():timeoutScheduler()}if(typeof scheduler.next!=="function")throw new Error("Scheduler is supposed to have next(cb) function");if(typeof scheduler.cancel!=="function")throw new Error("Scheduler is supposed to have cancel(handle) function");return scheduler}function rafScheduler(){return{next:window.requestAnimationFrame.bind(window),cancel:window.cancelAnimationFrame.bind(window)}}function timeoutScheduler(){return{next:function(cb){return setTimeout(cb,1e3/60)},cancel:function(id){return clearTimeout(id)}}}function makeAggregateRaf(){var frontBuffer=new Set;var backBuffer=new Set;var frameToken=0;return{next:next,cancel:next,clearAll:clearAll};function clearAll(){frontBuffer.clear();backBuffer.clear();cancelAnimationFrame(frameToken);frameToken=0}function next(callback){backBuffer.add(callback);renderNextFrame()}function renderNextFrame(){if(!frameToken)frameToken=requestAnimationFrame(renderFrame)}function renderFrame(){frameToken=0;var t=backBuffer;backBuffer=frontBuffer;frontBuffer=t;frontBuffer.forEach(function(callback){callback()});frontBuffer.clear()}function cancel(callback){backBuffer.delete(callback)}}},{"bezier-easing":8}],8:[function(require,module,exports){var NEWTON_ITERATIONS=4;var NEWTON_MIN_SLOPE=.001;var SUBDIVISION_PRECISION=1e-7;var SUBDIVISION_MAX_ITERATIONS=10;var kSplineTableSize=11;var kSampleStepSize=1/(kSplineTableSize-1);var float32ArraySupported=typeof Float32Array==="function";function A(aA1,aA2){return 1-3*aA2+3*aA1}function B(aA1,aA2){return 3*aA2-6*aA1}function C(aA1){return 3*aA1}function calcBezier(aT,aA1,aA2){return((A(aA1,aA2)*aT+B(aA1,aA2))*aT+C(aA1))*aT}function getSlope(aT,aA1,aA2){return 3*A(aA1,aA2)*aT*aT+2*B(aA1,aA2)*aT+C(aA1)}function binarySubdivide(aX,aA,aB,mX1,mX2){var currentX,currentT,i=0;do{currentT=aA+(aB-aA)/2;currentX=calcBezier(currentT,mX1,mX2)-aX;if(currentX>0){aB=currentT}else{aA=currentT}}while(Math.abs(currentX)>SUBDIVISION_PRECISION&&++i=NEWTON_MIN_SLOPE){return newtonRaphsonIterate(aX,guessForT,mX1,mX2)}else if(initialSlope===0){return guessForT}else{return binarySubdivide(aX,intervalStart,intervalStart+kSampleStepSize,mX1,mX2)}}return function BezierEasing(x){if(x===0){return 0}if(x===1){return 1}return calcBezier(getTForX(x),mY1,mY2)}}},{}],9:[function(require,module,exports){module.exports=function eventify(subject){validateSubject(subject);var eventsStorage=createEventsStorage(subject);subject.on=eventsStorage.on;subject.off=eventsStorage.off;subject.fire=eventsStorage.fire;return subject};function createEventsStorage(subject){var registeredEvents=Object.create(null);return{on:function(eventName,callback,ctx){if(typeof callback!=="function"){throw new Error("callback is expected to be a function")}var handlers=registeredEvents[eventName];if(!handlers){handlers=registeredEvents[eventName]=[]}handlers.push({callback:callback,ctx:ctx});return subject},off:function(eventName,callback){var wantToRemoveAll=typeof eventName==="undefined";if(wantToRemoveAll){registeredEvents=Object.create(null);return subject}if(registeredEvents[eventName]){var deleteAllCallbacksForEvent=typeof callback!=="function";if(deleteAllCallbacksForEvent){delete registeredEvents[eventName]}else{var callbacks=registeredEvents[eventName];for(var i=0;i1){fireArguments=Array.prototype.splice.call(arguments,1)}for(var i=0;i void; beforeMouseDown?: (e: MouseEvent) => void; autocenter?: boolean; + beforeTouchStart?: (e: TouchEvent) => void; + /** + * @deprecated Use beforeTouchStart instead. Warning: this involves more than just a name change. Please read the documentation for details. + */ onTouch?: (e: TouchEvent) => void; + beforeDoubleClick?: (e: Event) => void; + /** + * @deprecated Use beforeDoubleClick instead. Warning: this involves more than just a name change. Please read the documentation for details. + */ onDoubleClick?: (e: Event) => void; /** diff --git a/index.js b/index.js index 5836d8d..5069823 100644 --- a/index.js +++ b/index.js @@ -67,6 +67,8 @@ function createPanZoom(domElement, options) { var zoomDoubleClickSpeed = typeof options.zoomDoubleClickSpeed === 'number' ? options.zoomDoubleClickSpeed : defaultDoubleTapZoomSpeed; var beforeWheel = options.beforeWheel || noop; var beforeMouseDown = options.beforeMouseDown || noop; + var beforeTouchStart = options.beforeTouchStart || options.onTouch || beforeTouchStartDefault; + var beforeDoubleClick = options.beforeDoubleClick || options.onDoubleClick || beforeDoubleClickDefault; var speed = typeof options.zoomSpeed === 'number' ? options.zoomSpeed : defaultZoomSpeed; var transformOrigin = parseTransformOrigin(options.transformOrigin); var textSelection = options.enableTextSelection ? fakeTextSelectorInterceptor : domTextSelectionInterceptor; @@ -590,7 +592,15 @@ function createPanZoom(domElement, options) { function onTouch(e) { // let them override the touch behavior - beforeTouch(e); + // support onTouch backwards compatibility. + if (beforeTouchStart(e)) { + if (options.onTouch) { + beforeTouchStartDefault(e); + } else { + return; + } + } + clearPendingClickEventTimeout(); if (e.touches.length === 1) { @@ -603,28 +613,12 @@ function createPanZoom(domElement, options) { } } - function beforeTouch(e) { - // TODO: Need to unify this filtering names. E.g. use `beforeTouch` - if (options.onTouch && !options.onTouch(e)) { - // if they return `false` from onTouch, we don't want to stop - // events propagation. Fixes https://github.com/anvaka/panzoom/issues/12 - return; - } - + function beforeTouchStartDefault(e) { e.stopPropagation(); e.preventDefault(); } - function beforeDoubleClick(e) { - clearPendingClickEventTimeout(); - - // TODO: Need to unify this filtering names. E.g. use `beforeDoubleClick`` - if (options.onDoubleClick && !options.onDoubleClick(e)) { - // if they return `false` from onTouch, we don't want to stop - // events propagation. Fixes https://github.com/anvaka/panzoom/issues/46 - return; - } - + function beforeDoubleClickDefault(e) { e.preventDefault(); e.stopPropagation(); } @@ -762,7 +756,16 @@ function createPanZoom(domElement, options) { } function onDoubleClick(e) { - beforeDoubleClick(e); + // support onDoubleClick backwards compatibility + if (beforeDoubleClick(e)) { + if (options.onDoubleClick) { + beforeDoubleClickDefault(e); + } else { + return; + } + } + clearPendingClickEventTimeout(); + var offset = getOffsetXY(e); if (transformOrigin) { // TODO: looks like this is duplicated in the file. diff --git a/test/panzoom.js b/test/panzoom.js index 09fa82e..fa880d5 100644 --- a/test/panzoom.js +++ b/test/panzoom.js @@ -225,7 +225,7 @@ test('double click zooms in', t => { } }); -test('Can cancel preventDefault', t => { +test('double click can cancel preventDefault', t => { var dom = new JSDOM(`
`); const document = dom.window.document; var content = document.querySelector('.content'); @@ -267,14 +267,56 @@ test('Can cancel preventDefault', t => { } }); +test('onDoubleClick true doesn\'t cancel preventDefault', t => { + var dom = new JSDOM(`
`); + const document = dom.window.document; + var content = document.querySelector('.content'); + // JSDOM does not support this, have to override: + content.parentElement.getBoundingClientRect = makeBoundingRect(100, 100); + + var panzoom = createPanzoom(content, { + onDoubleClick() { + // we don't want to prevent default! + return true; + } + }); + + var calledTimes = 0; + panzoom.on('zoom', function() { + calledTimes += 1; + }); + + var doubleClick = new dom.window.MouseEvent('dblclick', { + bubbles: true, + cancelable: true, + clientX: 50, + clientY: 50 + }); + + content.dispatchEvent(doubleClick); + t.ok(doubleClick.defaultPrevented, 'default should be prevented'); + setTimeout(verifyTransformIsChanged, 40); + + function verifyTransformIsChanged() { + var transform = parseMatrixTransform(content.style.transform); + t.ok(transform, 'Transform is defined'); + t.ok(transform.scaleX !== 1, 'Scale has changed'); + t.ok(transform.scaleX === transform.scaleY, 'Scale is proportional'); + t.ok(transform.dx !== 0 && transform.dy !== 0, 'translated a bit'); + t.ok(calledTimes > 0, 'zoom event triggered'); + panzoom.dispose(); + t.end(); + } +}); + function makeBoundingRect(width, height) { - return function getBoundingClientRect() { - return { - left: 0, - top: 0, - width: width, - height: height - }; + return function getBoundingClientRect() { + return { + left: 0, + top: 0, + width: width, + height: height + }; }; } @@ -284,9 +326,9 @@ function parseMatrixTransform(transformString) { if (!matches) return; return { - scaleX: parseFloat(matches[1]), - scaleY: parseFloat(matches[2]), - dx: parseFloat(matches[3]), + scaleX: parseFloat(matches[1]), + scaleY: parseFloat(matches[2]), + dx: parseFloat(matches[3]), dy: parseFloat(matches[4]) }; }