From a94fb753a1791480231119f3467625ce4a33ba56 Mon Sep 17 00:00:00 2001 From: Tusooa Zhu Date: Mon, 2 Aug 2021 16:48:32 -0400 Subject: [PATCH] Support disabling panning at min scale and selectively passing down pointer events At min scale we can now disable panning (but still get pinch actions). In that case the pan will be passed down. Once the pan is turned into a pinch, it will no longer be passed down, and a pointercancel will be triggered on the parent. --- dist/pinch-zoom-min.js | 2 +- dist/pinch-zoom.cjs.js | 127 +++++++++++++++++++++++++++++++- dist/pinch-zoom.d.ts | 11 +++ dist/pinch-zoom.es.js | 127 +++++++++++++++++++++++++++++++- dist/pinch-zoom.js | 127 +++++++++++++++++++++++++++++++- dist/pinch-zoom.mjs | 127 +++++++++++++++++++++++++++++++- lib/pinch-zoom.ts | 161 ++++++++++++++++++++++++++++++++++++++++- package.json | 2 +- 8 files changed, 667 insertions(+), 17 deletions(-) diff --git a/dist/pinch-zoom-min.js b/dist/pinch-zoom-min.js index 01d3b0f..26e2fcb 100644 --- a/dist/pinch-zoom-min.js +++ b/dist/pinch-zoom-min.js @@ -1 +1 @@ -!function(){"use strict";!function(){class t{constructor(t){this.id=-1,this.nativePointer=t,this.pageX=t.pageX,this.pageY=t.pageY,this.clientX=t.clientX,this.clientY=t.clientY,self.Touch&&t instanceof Touch?this.id=t.identifier:e(t)&&(this.id=t.pointerId)}getCoalesced(){return"getCoalescedEvents"in this.nativePointer?this.nativePointer.getCoalescedEvents().map(e=>new t(e)):[this]}}const e=t=>self.PointerEvent&&t instanceof PointerEvent,n=()=>{};class i{constructor(t,e){this._element=t,this.startPointers=[],this.currentPointers=[];const{start:i=(()=>!0),move:s=n,end:r=n}=e;this._startCallback=i,this._moveCallback=s,this._endCallback=r,this._pointerStart=this._pointerStart.bind(this),this._touchStart=this._touchStart.bind(this),this._move=this._move.bind(this),this._triggerPointerEnd=this._triggerPointerEnd.bind(this),this._pointerEnd=this._pointerEnd.bind(this),this._touchEnd=this._touchEnd.bind(this),self.PointerEvent?this._element.addEventListener("pointerdown",this._pointerStart):(this._element.addEventListener("mousedown",this._pointerStart),this._element.addEventListener("touchstart",this._touchStart),this._element.addEventListener("touchmove",this._move),this._element.addEventListener("touchend",this._touchEnd))}_triggerPointerStart(t,e){return!!this._startCallback(t,e)&&(this.currentPointers.push(t),this.startPointers.push(t),!0)}_pointerStart(n){0===n.button&&this._triggerPointerStart(new t(n),n)&&(e(n)?(this._element.setPointerCapture(n.pointerId),this._element.addEventListener("pointermove",this._move),this._element.addEventListener("pointerup",this._pointerEnd)):(window.addEventListener("mousemove",this._move),window.addEventListener("mouseup",this._pointerEnd)))}_touchStart(e){for(const n of Array.from(e.changedTouches))this._triggerPointerStart(new t(n),e)}_move(e){const n=this.currentPointers.slice(),i="changedTouches"in e?Array.from(e.changedTouches).map(e=>new t(e)):[new t(e)],s=[];for(const t of i){const e=this.currentPointers.findIndex(e=>e.id===t.id);-1!==e&&(s.push(t),this.currentPointers[e]=t)}0!==s.length&&this._moveCallback(n,s,e)}_triggerPointerEnd(t,e){const n=this.currentPointers.findIndex(e=>e.id===t.id);return-1!==n&&(this.currentPointers.splice(n,1),this.startPointers.splice(n,1),this._endCallback(t,e),!0)}_pointerEnd(n){if(this._triggerPointerEnd(new t(n),n))if(e(n)){if(this.currentPointers.length)return;this._element.removeEventListener("pointermove",this._move),this._element.removeEventListener("pointerup",this._pointerEnd)}else window.removeEventListener("mousemove",this._move),window.removeEventListener("mouseup",this._pointerEnd)}_touchEnd(e){for(const n of Array.from(e.changedTouches))this._triggerPointerEnd(new t(n),e)}}!function(t,e){void 0===e&&(e={});var n=e.insertAt;if(t&&"undefined"!=typeof document){var i=document.head||document.getElementsByTagName("head")[0],s=document.createElement("style");s.type="text/css","top"===n&&i.firstChild?i.insertBefore(s,i.firstChild):i.appendChild(s),s.styleSheet?s.styleSheet.cssText=t:s.appendChild(document.createTextNode(t))}}("pinch-zoom {\n display: block;\n overflow: hidden;\n touch-action: none;\n --scale: 1;\n --x: 0;\n --y: 0;\n}\n\npinch-zoom > * {\n transform: translate(var(--x), var(--y)) scale(var(--scale));\n transform-origin: 0 0;\n will-change: transform;\n}\n");const s="min-scale";function r(t,e){return e?Math.sqrt((e.clientX-t.clientX)**2+(e.clientY-t.clientY)**2):0}function o(t,e){return e?{clientX:(t.clientX+e.clientX)/2,clientY:(t.clientY+e.clientY)/2}:t}function h(t,e){return"number"==typeof t?t:t.trimRight().endsWith("%")?e*parseFloat(t)/100:parseFloat(t)}let a;function l(){return a||(a=document.createElementNS("http://www.w3.org/2000/svg","svg"))}function c(){return l().createSVGMatrix()}function d(){return l().createSVGPoint()}const g=.01;class u extends HTMLElement{constructor(){super(),this._transform=c(),new MutationObserver(()=>this._stageElChange()).observe(this,{childList:!0});const t=new i(this,{start:(e,n)=>!(2===t.currentPointers.length||!this._positioningEl)&&(n.preventDefault(),!0),move:e=>{this._onPointerMove(e,t.currentPointers)}});this.addEventListener("wheel",t=>this._onWheel(t))}static get observedAttributes(){return[s]}attributeChangedCallback(t,e,n){t===s&&this.scaler.width?i+=r.width-h.x:a.x<0&&(i+=-a.x),h.y>r.height?s+=r.height-h.y:a.y<0&&(s+=-a.y),this._updateTransform(e,i,s,n)}_updateTransform(t,e,n,i){if(!(t1&&console.warn(" must not have more than one child."),this.setTransform({allowChangeEvent:!0}))}_onWheel(t){if(!this._positioningEl)return;t.preventDefault();const e=this._positioningEl.getBoundingClientRect();let{deltaY:n}=t;const{ctrlKey:i,deltaMode:s}=t;1===s&&(n*=15);const r=1-n/(i?100:300);this._applyChange({scaleDiff:r,originX:t.clientX-e.left,originY:t.clientY-e.top,allowChangeEvent:!0})}_onPointerMove(t,e){if(!this._positioningEl)return;const n=this._positioningEl.getBoundingClientRect(),i=o(t[0],t[1]),s=o(e[0],e[1]),h=i.clientX-n.left,a=i.clientY-n.top,l=r(t[0],t[1]),c=r(e[0],e[1]),d=l?c/l:1;this._applyChange({originX:h,originY:a,scaleDiff:d,panX:s.clientX-i.clientX,panY:s.clientY-i.clientY,allowChangeEvent:!0})}_applyChange(t={}){const{panX:e=0,panY:n=0,originX:i=0,originY:s=0,scaleDiff:r=1,allowChangeEvent:o=!1}=t,h=c().translate(e,n).translate(i,s).translate(this.x,this.y).scale(r).translate(-i,-s).scale(this.scale);this.setTransform({allowChangeEvent:o,scale:h.a,x:h.e,y:h.f})}}customElements.define("pinch-zoom",u)}()}(); +!function(){"use strict";!function(){class t{constructor(t){this.id=-1,this.nativePointer=t,this.pageX=t.pageX,this.pageY=t.pageY,this.clientX=t.clientX,this.clientY=t.clientY,self.Touch&&t instanceof Touch?this.id=t.identifier:e(t)&&(this.id=t.pointerId)}getCoalesced(){return"getCoalescedEvents"in this.nativePointer?this.nativePointer.getCoalescedEvents().map(e=>new t(e)):[this]}}const e=t=>self.PointerEvent&&t instanceof PointerEvent,n=()=>{};class i{constructor(t,e){this._element=t,this.startPointers=[],this.currentPointers=[];const{start:i=(()=>!0),move:s=n,end:r=n}=e;this._startCallback=i,this._moveCallback=s,this._endCallback=r,this._pointerStart=this._pointerStart.bind(this),this._touchStart=this._touchStart.bind(this),this._move=this._move.bind(this),this._triggerPointerEnd=this._triggerPointerEnd.bind(this),this._pointerEnd=this._pointerEnd.bind(this),this._touchEnd=this._touchEnd.bind(this),self.PointerEvent?this._element.addEventListener("pointerdown",this._pointerStart):(this._element.addEventListener("mousedown",this._pointerStart),this._element.addEventListener("touchstart",this._touchStart),this._element.addEventListener("touchmove",this._move),this._element.addEventListener("touchend",this._touchEnd))}_triggerPointerStart(t,e){return!!this._startCallback(t,e)&&(this.currentPointers.push(t),this.startPointers.push(t),!0)}_pointerStart(n){0===n.button&&this._triggerPointerStart(new t(n),n)&&(e(n)?(this._element.setPointerCapture(n.pointerId),this._element.addEventListener("pointermove",this._move),this._element.addEventListener("pointerup",this._pointerEnd)):(window.addEventListener("mousemove",this._move),window.addEventListener("mouseup",this._pointerEnd)))}_touchStart(e){for(const n of Array.from(e.changedTouches))this._triggerPointerStart(new t(n),e)}_move(e){const n=this.currentPointers.slice(),i="changedTouches"in e?Array.from(e.changedTouches).map(e=>new t(e)):[new t(e)],s=[];for(const t of i){const e=this.currentPointers.findIndex(e=>e.id===t.id);-1!==e&&(s.push(t),this.currentPointers[e]=t)}0!==s.length&&this._moveCallback(n,s,e)}_triggerPointerEnd(t,e){const n=this.currentPointers.findIndex(e=>e.id===t.id);return-1!==n&&(this.currentPointers.splice(n,1),this.startPointers.splice(n,1),this._endCallback(t,e),!0)}_pointerEnd(n){if(this._triggerPointerEnd(new t(n),n))if(e(n)){if(this.currentPointers.length)return;this._element.removeEventListener("pointermove",this._move),this._element.removeEventListener("pointerup",this._pointerEnd)}else window.removeEventListener("mousemove",this._move),window.removeEventListener("mouseup",this._pointerEnd)}_touchEnd(e){for(const n of Array.from(e.changedTouches))this._triggerPointerEnd(new t(n),e)}}!function(t,e){void 0===e&&(e={});var n=e.insertAt;if(t&&"undefined"!=typeof document){var i=document.head||document.getElementsByTagName("head")[0],s=document.createElement("style");s.type="text/css","top"===n&&i.firstChild?i.insertBefore(s,i.firstChild):i.appendChild(s),s.styleSheet?s.styleSheet.cssText=t:s.appendChild(document.createTextNode(t))}}("pinch-zoom {\n display: block;\n overflow: hidden;\n touch-action: none;\n --scale: 1;\n --x: 0;\n --y: 0;\n}\n\npinch-zoom > * {\n transform: translate(var(--x), var(--y)) scale(var(--scale));\n transform-origin: 0 0;\n will-change: transform;\n}\n");const s="min-scale",r="allow-pan-min-scale",o="reset-to-min-scale-limit",a="reach-min-scale-strategy",h="stop-propagate-handled",l="none";function c(t,e){return e?Math.sqrt((e.clientX-t.clientX)**2+(e.clientY-t.clientY)**2):0}function d(t,e){return e?{clientX:(t.clientX+e.clientX)/2,clientY:(t.clientY+e.clientY)/2}:t}function u(t,e){return"number"==typeof t?t:t.trimRight().endsWith("%")?e*parseFloat(t)/100:parseFloat(t)}let g;function p(){return g||(g=document.createElementNS("http://www.w3.org/2000/svg","svg"))}function m(){return p().createSVGMatrix()}function _(){return p().createSVGPoint()}const f=.01,v=-1,E=-1,P=(t,e)=>Math.round(100*t)-Math.round(100*e);class y extends HTMLElement{constructor(){super(),this._transform=m(),new MutationObserver(()=>this._stageElChange()).observe(this,{childList:!0});const t=new i(this,{start:(e,n)=>{if(2===t.currentPointers.length||!this._positioningEl)return!1;return!(t.currentPointers.length+1===1&&!this._allowPan())&&(this._maybeStopPropagate(n),this._maybeEmitCancel([e,...t.currentPointers])),n.preventDefault(),!0},move:(e,n,i)=>{this._onPointerMove(e,t.currentPointers,i)},end:(e,n)=>{this._onPointerEnd(e,t.currentPointers,n)}});this.addEventListener("wheel",t=>this._onWheel(t))}static get observedAttributes(){return[s]}attributeChangedCallback(t,e,n){t===s&&this.scaler.width?i+=r.width-a.x:h.x<0&&(i+=-h.x),a.y>r.height?s+=r.height-a.y:h.y<0&&(s+=-h.y),this._updateTransform(e,i,s,n)}_updateTransform(t,e,n,i){if(!(t1&&console.warn(" must not have more than one child."),this.setTransform({allowChangeEvent:!0}))}_onWheel(t){if(!this._positioningEl)return;t.preventDefault();const e=this._positioningEl.getBoundingClientRect();let{deltaY:n}=t;const{ctrlKey:i,deltaMode:s}=t;1===s&&(n*=15);const r=1-n/(i?100:300),o=r<1;this._applyChange({scaleDiff:r,originX:t.clientX-e.left,originY:t.clientY-e.top,allowChangeEvent:!0}),o&&this._maybeResetScale()}_onPointerMove(t,e,n){if(!this._positioningEl)return;if(t.length<2&&!this._allowPan())return;const i=this._positioningEl.getBoundingClientRect(),s=d(t[0],t[1]),r=d(e[0],e[1]),o=s.clientX-i.left,a=s.clientY-i.top,h=c(t[0],t[1]),l=c(e[0],e[1]),u=h?l/h:1;this._applyChange({originX:o,originY:a,scaleDiff:u,panX:r.clientX-s.clientX,panY:r.clientY-s.clientY,allowChangeEvent:!0}),this._maybeStopPropagate(n)}_maybeResetScale(){P(this.scale,this.resetToMinScaleLimit)<=0&&this._resetToMinScale()}_onPointerEnd(t,e,n){if(!this._positioningEl)return;const i=1+e.length,s=1==i;i>=2&&this._maybeResetScale(),s&&!this._allowPan()||this._maybeStopPropagate(n)}_resetToMinScale(){"reset"===this.reachMinScaleStrategy?this.setTransform({scale:this.minScale,x:0,y:0}):this.setTransform({scale:this.minScale})}_applyChange(t={}){const{panX:e=0,panY:n=0,originX:i=0,originY:s=0,scaleDiff:r=1,allowChangeEvent:o=!1}=t,a=m().translate(e,n).translate(i,s).translate(this.x,this.y).scale(r).translate(-i,-s).scale(this.scale);this.setTransform({allowChangeEvent:o,scale:a.a,x:a.e,y:a.f})}_maybeStopPropagate(t){this.stopPropagateHandled&&t.stopPropagation()}_allowPan(){return this.allowPanMinScale>0&&P(this.scale,this.allowPanMinScale)>0}_maybeEmitCancel(t){this.stopPropagateHandled&&t.forEach(t=>{this.parentElement&&"function"==typeof this.parentElement.dispatchEvent&&this.parentElement.dispatchEvent((t=>new PointerEvent("pointercancel",{pointerId:t.id,clientX:t.clientX,clientY:t.clientY}))(t))})}}customElements.define("pinch-zoom",y)}()}(); diff --git a/dist/pinch-zoom.cjs.js b/dist/pinch-zoom.cjs.js index ae19564..5330d71 100644 --- a/dist/pinch-zoom.cjs.js +++ b/dist/pinch-zoom.cjs.js @@ -35,6 +35,11 @@ var css = "pinch-zoom {\n display: block;\n overflow: hidden;\n touch-action: styleInject(css); const minScaleAttr = 'min-scale'; +const allowPanMinScaleAttr = 'allow-pan-min-scale'; +const resetToMinScaleLimitAttr = 'reset-to-min-scale-limit'; +const reachMinScaleStrategyAttr = 'reach-min-scale-strategy'; +const stopPropagateHandledAttr = 'stop-propagate-handled'; +const reachMinScaleStrategyDefault = 'none'; function getDistance(a, b) { if (!b) return 0; @@ -69,6 +74,11 @@ function createPoint() { return getSVG().createSVGPoint(); } const MIN_SCALE = 0.01; +const ALLOW_PAN_MIN_SCALE = -1; +const RESET_TO_MIN_SCALE_LIMIT = -1; +const roundedCmp = (a, b) => { + return Math.round(a * 100) - Math.round(b * 100); +}; class PinchZoom extends HTMLElement { constructor() { super(); @@ -85,11 +95,20 @@ class PinchZoom extends HTMLElement { // We only want to track 2 pointers at most if (pointerTracker.currentPointers.length === 2 || !this._positioningEl) return false; + const isPan = pointerTracker.currentPointers.length + 1 === 1; + const handled = !(isPan && !this._allowPan()); + if (handled) { + this._maybeStopPropagate(event); + this._maybeEmitCancel([pointer, ...pointerTracker.currentPointers]); + } event.preventDefault(); return true; }, - move: (previousPointers) => { - this._onPointerMove(previousPointers, pointerTracker.currentPointers); + move: (previousPointers, _, event) => { + this._onPointerMove(previousPointers, pointerTracker.currentPointers, event); + }, + end: (pointer, event) => { + this._onPointerEnd(pointer, pointerTracker.currentPointers, event); }, }); this.addEventListener('wheel', event => this._onWheel(event)); @@ -114,6 +133,49 @@ class PinchZoom extends HTMLElement { set minScale(value) { this.setAttribute(minScaleAttr, String(value)); } + get reachMinScaleStrategy() { + const attrValue = this.getAttribute(reachMinScaleStrategyAttr); + const v = attrValue; + return v || reachMinScaleStrategyDefault; + } + set reachMinScaleStrategy(value) { + this.setAttribute(reachMinScaleStrategyAttr, value); + } + get allowPanMinScale() { + const attrValue = this.getAttribute(allowPanMinScaleAttr); + if (!attrValue) + return ALLOW_PAN_MIN_SCALE; + const value = parseFloat(attrValue); + if (Number.isFinite(value)) + return Math.max(ALLOW_PAN_MIN_SCALE, value); + return ALLOW_PAN_MIN_SCALE; + } + set allowPanMinScale(value) { + this.setAttribute(allowPanMinScaleAttr, String(value)); + } + get resetToMinScaleLimit() { + const attrValue = this.getAttribute(resetToMinScaleLimitAttr); + if (!attrValue) + return RESET_TO_MIN_SCALE_LIMIT; + const value = parseFloat(attrValue); + if (Number.isFinite(value)) + return Math.max(RESET_TO_MIN_SCALE_LIMIT, value); + return RESET_TO_MIN_SCALE_LIMIT; + } + set resetToMinScaleLimit(value) { + this.setAttribute(resetToMinScaleLimitAttr, String(value)); + } + get stopPropagateHandled() { + return this.hasAttribute(stopPropagateHandledAttr); + } + set stopPropagateHandled(value) { + if (value) { + this.setAttribute(stopPropagateHandledAttr, ''); + } + else { + this.removeAttribute(stopPropagateHandledAttr); + } + } connectedCallback() { this._stageElChange(); } @@ -264,16 +326,24 @@ class PinchZoom extends HTMLElement { // ctrlKey is true when pinch-zooming on a trackpad. const divisor = ctrlKey ? 100 : 300; const scaleDiff = 1 - deltaY / divisor; + const isZoomOut = scaleDiff < 1; this._applyChange({ scaleDiff, originX: event.clientX - currentRect.left, originY: event.clientY - currentRect.top, allowChangeEvent: true, }); + if (isZoomOut) { + this._maybeResetScale(); + } } - _onPointerMove(previousPointers, currentPointers) { + _onPointerMove(previousPointers, currentPointers, event) { if (!this._positioningEl) return; + const isPan = previousPointers.length < 2; + if (isPan && !this._allowPan()) { + return; + } // Combine next points with previous points const currentRect = this._positioningEl.getBoundingClientRect(); // For calculating panning movement @@ -292,6 +362,34 @@ class PinchZoom extends HTMLElement { panY: newMidpoint.clientY - prevMidpoint.clientY, allowChangeEvent: true, }); + this._maybeStopPropagate(event); + } + _maybeResetScale() { + if (roundedCmp(this.scale, this.resetToMinScaleLimit) <= 0) { + this._resetToMinScale(); + } + } + _onPointerEnd(pointer, currentPointers, event) { + if (!this._positioningEl) + return; + const totalPointers = 1 + currentPointers.length; + const isPinch = totalPointers >= 2; + const isPan = totalPointers == 1; + if (isPinch) { + this._maybeResetScale(); + } + if (isPan && !this._allowPan()) { + return; + } + this._maybeStopPropagate(event); + } + _resetToMinScale() { + if (this.reachMinScaleStrategy === 'reset') { + this.setTransform({ scale: this.minScale, x: 0, y: 0 }); + } + else { + this.setTransform({ scale: this.minScale }); + } } /** Transform the view & fire a change event */ _applyChange(opts = {}) { @@ -315,6 +413,29 @@ class PinchZoom extends HTMLElement { y: matrix.f, }); } + _maybeStopPropagate(event) { + if (this.stopPropagateHandled) { + event.stopPropagation(); + } + } + _allowPan() { + return (this.allowPanMinScale > 0 + && roundedCmp(this.scale, this.allowPanMinScale) > 0); + } + _maybeEmitCancel(pointers) { + const makeCancelEvent = (pointer) => (new PointerEvent('pointercancel', { + pointerId: pointer.id, + clientX: pointer.clientX, + clientY: pointer.clientY, + })); + if (this.stopPropagateHandled) { + pointers.forEach(p => { + if (this.parentElement && typeof this.parentElement.dispatchEvent === 'function') { + this.parentElement.dispatchEvent(makeCancelEvent(p)); + } + }); + } + } } customElements.define('pinch-zoom', PinchZoom); diff --git a/dist/pinch-zoom.d.ts b/dist/pinch-zoom.d.ts index 1639e0a..7fad721 100644 --- a/dist/pinch-zoom.d.ts +++ b/dist/pinch-zoom.d.ts @@ -11,6 +11,7 @@ interface SetTransformOpts extends ChangeOptions { y?: number; } declare type ScaleRelativeToValues = 'container' | 'content'; +declare type ReachMinScaleStrategy = 'reset' | 'none'; export interface ScaleToOpts extends ChangeOptions { /** Transform origin. Can be a number, or string percent, eg "50%" */ originX?: number | string; @@ -26,6 +27,10 @@ export default class PinchZoom extends HTMLElement { constructor(); attributeChangedCallback(name: string, oldValue: string, newValue: string): void; minScale: number; + reachMinScaleStrategy: ReachMinScaleStrategy; + allowPanMinScale: number; + resetToMinScaleLimit: number; + stopPropagateHandled: boolean; connectedCallback(): void; readonly x: number; readonly y: number; @@ -51,7 +56,13 @@ export default class PinchZoom extends HTMLElement { private _stageElChange; private _onWheel; private _onPointerMove; + private _maybeResetScale; + private _onPointerEnd; + private _resetToMinScale; /** Transform the view & fire a change event */ private _applyChange; + private _maybeStopPropagate; + private _allowPan; + private _maybeEmitCancel; } export {}; diff --git a/dist/pinch-zoom.es.js b/dist/pinch-zoom.es.js index 75f27a7..db7319c 100644 --- a/dist/pinch-zoom.es.js +++ b/dist/pinch-zoom.es.js @@ -31,6 +31,11 @@ var css = "pinch-zoom {\n display: block;\n overflow: hidden;\n touch-action: styleInject(css); const minScaleAttr = 'min-scale'; +const allowPanMinScaleAttr = 'allow-pan-min-scale'; +const resetToMinScaleLimitAttr = 'reset-to-min-scale-limit'; +const reachMinScaleStrategyAttr = 'reach-min-scale-strategy'; +const stopPropagateHandledAttr = 'stop-propagate-handled'; +const reachMinScaleStrategyDefault = 'none'; function getDistance(a, b) { if (!b) return 0; @@ -65,6 +70,11 @@ function createPoint() { return getSVG().createSVGPoint(); } const MIN_SCALE = 0.01; +const ALLOW_PAN_MIN_SCALE = -1; +const RESET_TO_MIN_SCALE_LIMIT = -1; +const roundedCmp = (a, b) => { + return Math.round(a * 100) - Math.round(b * 100); +}; class PinchZoom extends HTMLElement { constructor() { super(); @@ -81,11 +91,20 @@ class PinchZoom extends HTMLElement { // We only want to track 2 pointers at most if (pointerTracker.currentPointers.length === 2 || !this._positioningEl) return false; + const isPan = pointerTracker.currentPointers.length + 1 === 1; + const handled = !(isPan && !this._allowPan()); + if (handled) { + this._maybeStopPropagate(event); + this._maybeEmitCancel([pointer, ...pointerTracker.currentPointers]); + } event.preventDefault(); return true; }, - move: (previousPointers) => { - this._onPointerMove(previousPointers, pointerTracker.currentPointers); + move: (previousPointers, _, event) => { + this._onPointerMove(previousPointers, pointerTracker.currentPointers, event); + }, + end: (pointer, event) => { + this._onPointerEnd(pointer, pointerTracker.currentPointers, event); }, }); this.addEventListener('wheel', event => this._onWheel(event)); @@ -110,6 +129,49 @@ class PinchZoom extends HTMLElement { set minScale(value) { this.setAttribute(minScaleAttr, String(value)); } + get reachMinScaleStrategy() { + const attrValue = this.getAttribute(reachMinScaleStrategyAttr); + const v = attrValue; + return v || reachMinScaleStrategyDefault; + } + set reachMinScaleStrategy(value) { + this.setAttribute(reachMinScaleStrategyAttr, value); + } + get allowPanMinScale() { + const attrValue = this.getAttribute(allowPanMinScaleAttr); + if (!attrValue) + return ALLOW_PAN_MIN_SCALE; + const value = parseFloat(attrValue); + if (Number.isFinite(value)) + return Math.max(ALLOW_PAN_MIN_SCALE, value); + return ALLOW_PAN_MIN_SCALE; + } + set allowPanMinScale(value) { + this.setAttribute(allowPanMinScaleAttr, String(value)); + } + get resetToMinScaleLimit() { + const attrValue = this.getAttribute(resetToMinScaleLimitAttr); + if (!attrValue) + return RESET_TO_MIN_SCALE_LIMIT; + const value = parseFloat(attrValue); + if (Number.isFinite(value)) + return Math.max(RESET_TO_MIN_SCALE_LIMIT, value); + return RESET_TO_MIN_SCALE_LIMIT; + } + set resetToMinScaleLimit(value) { + this.setAttribute(resetToMinScaleLimitAttr, String(value)); + } + get stopPropagateHandled() { + return this.hasAttribute(stopPropagateHandledAttr); + } + set stopPropagateHandled(value) { + if (value) { + this.setAttribute(stopPropagateHandledAttr, ''); + } + else { + this.removeAttribute(stopPropagateHandledAttr); + } + } connectedCallback() { this._stageElChange(); } @@ -260,16 +322,24 @@ class PinchZoom extends HTMLElement { // ctrlKey is true when pinch-zooming on a trackpad. const divisor = ctrlKey ? 100 : 300; const scaleDiff = 1 - deltaY / divisor; + const isZoomOut = scaleDiff < 1; this._applyChange({ scaleDiff, originX: event.clientX - currentRect.left, originY: event.clientY - currentRect.top, allowChangeEvent: true, }); + if (isZoomOut) { + this._maybeResetScale(); + } } - _onPointerMove(previousPointers, currentPointers) { + _onPointerMove(previousPointers, currentPointers, event) { if (!this._positioningEl) return; + const isPan = previousPointers.length < 2; + if (isPan && !this._allowPan()) { + return; + } // Combine next points with previous points const currentRect = this._positioningEl.getBoundingClientRect(); // For calculating panning movement @@ -288,6 +358,34 @@ class PinchZoom extends HTMLElement { panY: newMidpoint.clientY - prevMidpoint.clientY, allowChangeEvent: true, }); + this._maybeStopPropagate(event); + } + _maybeResetScale() { + if (roundedCmp(this.scale, this.resetToMinScaleLimit) <= 0) { + this._resetToMinScale(); + } + } + _onPointerEnd(pointer, currentPointers, event) { + if (!this._positioningEl) + return; + const totalPointers = 1 + currentPointers.length; + const isPinch = totalPointers >= 2; + const isPan = totalPointers == 1; + if (isPinch) { + this._maybeResetScale(); + } + if (isPan && !this._allowPan()) { + return; + } + this._maybeStopPropagate(event); + } + _resetToMinScale() { + if (this.reachMinScaleStrategy === 'reset') { + this.setTransform({ scale: this.minScale, x: 0, y: 0 }); + } + else { + this.setTransform({ scale: this.minScale }); + } } /** Transform the view & fire a change event */ _applyChange(opts = {}) { @@ -311,6 +409,29 @@ class PinchZoom extends HTMLElement { y: matrix.f, }); } + _maybeStopPropagate(event) { + if (this.stopPropagateHandled) { + event.stopPropagation(); + } + } + _allowPan() { + return (this.allowPanMinScale > 0 + && roundedCmp(this.scale, this.allowPanMinScale) > 0); + } + _maybeEmitCancel(pointers) { + const makeCancelEvent = (pointer) => (new PointerEvent('pointercancel', { + pointerId: pointer.id, + clientX: pointer.clientX, + clientY: pointer.clientY, + })); + if (this.stopPropagateHandled) { + pointers.forEach(p => { + if (this.parentElement && typeof this.parentElement.dispatchEvent === 'function') { + this.parentElement.dispatchEvent(makeCancelEvent(p)); + } + }); + } + } } customElements.define('pinch-zoom', PinchZoom); diff --git a/dist/pinch-zoom.js b/dist/pinch-zoom.js index 4c3eabb..714c2c6 100644 --- a/dist/pinch-zoom.js +++ b/dist/pinch-zoom.js @@ -216,6 +216,11 @@ var PinchZoom = (function () { styleInject(css); const minScaleAttr = 'min-scale'; + const allowPanMinScaleAttr = 'allow-pan-min-scale'; + const resetToMinScaleLimitAttr = 'reset-to-min-scale-limit'; + const reachMinScaleStrategyAttr = 'reach-min-scale-strategy'; + const stopPropagateHandledAttr = 'stop-propagate-handled'; + const reachMinScaleStrategyDefault = 'none'; function getDistance(a, b) { if (!b) return 0; @@ -250,6 +255,11 @@ var PinchZoom = (function () { return getSVG().createSVGPoint(); } const MIN_SCALE = 0.01; + const ALLOW_PAN_MIN_SCALE = -1; + const RESET_TO_MIN_SCALE_LIMIT = -1; + const roundedCmp = (a, b) => { + return Math.round(a * 100) - Math.round(b * 100); + }; class PinchZoom extends HTMLElement { constructor() { super(); @@ -266,11 +276,20 @@ var PinchZoom = (function () { // We only want to track 2 pointers at most if (pointerTracker.currentPointers.length === 2 || !this._positioningEl) return false; + const isPan = pointerTracker.currentPointers.length + 1 === 1; + const handled = !(isPan && !this._allowPan()); + if (handled) { + this._maybeStopPropagate(event); + this._maybeEmitCancel([pointer, ...pointerTracker.currentPointers]); + } event.preventDefault(); return true; }, - move: (previousPointers) => { - this._onPointerMove(previousPointers, pointerTracker.currentPointers); + move: (previousPointers, _, event) => { + this._onPointerMove(previousPointers, pointerTracker.currentPointers, event); + }, + end: (pointer, event) => { + this._onPointerEnd(pointer, pointerTracker.currentPointers, event); }, }); this.addEventListener('wheel', event => this._onWheel(event)); @@ -295,6 +314,49 @@ var PinchZoom = (function () { set minScale(value) { this.setAttribute(minScaleAttr, String(value)); } + get reachMinScaleStrategy() { + const attrValue = this.getAttribute(reachMinScaleStrategyAttr); + const v = attrValue; + return v || reachMinScaleStrategyDefault; + } + set reachMinScaleStrategy(value) { + this.setAttribute(reachMinScaleStrategyAttr, value); + } + get allowPanMinScale() { + const attrValue = this.getAttribute(allowPanMinScaleAttr); + if (!attrValue) + return ALLOW_PAN_MIN_SCALE; + const value = parseFloat(attrValue); + if (Number.isFinite(value)) + return Math.max(ALLOW_PAN_MIN_SCALE, value); + return ALLOW_PAN_MIN_SCALE; + } + set allowPanMinScale(value) { + this.setAttribute(allowPanMinScaleAttr, String(value)); + } + get resetToMinScaleLimit() { + const attrValue = this.getAttribute(resetToMinScaleLimitAttr); + if (!attrValue) + return RESET_TO_MIN_SCALE_LIMIT; + const value = parseFloat(attrValue); + if (Number.isFinite(value)) + return Math.max(RESET_TO_MIN_SCALE_LIMIT, value); + return RESET_TO_MIN_SCALE_LIMIT; + } + set resetToMinScaleLimit(value) { + this.setAttribute(resetToMinScaleLimitAttr, String(value)); + } + get stopPropagateHandled() { + return this.hasAttribute(stopPropagateHandledAttr); + } + set stopPropagateHandled(value) { + if (value) { + this.setAttribute(stopPropagateHandledAttr, ''); + } + else { + this.removeAttribute(stopPropagateHandledAttr); + } + } connectedCallback() { this._stageElChange(); } @@ -445,16 +507,24 @@ var PinchZoom = (function () { // ctrlKey is true when pinch-zooming on a trackpad. const divisor = ctrlKey ? 100 : 300; const scaleDiff = 1 - deltaY / divisor; + const isZoomOut = scaleDiff < 1; this._applyChange({ scaleDiff, originX: event.clientX - currentRect.left, originY: event.clientY - currentRect.top, allowChangeEvent: true, }); + if (isZoomOut) { + this._maybeResetScale(); + } } - _onPointerMove(previousPointers, currentPointers) { + _onPointerMove(previousPointers, currentPointers, event) { if (!this._positioningEl) return; + const isPan = previousPointers.length < 2; + if (isPan && !this._allowPan()) { + return; + } // Combine next points with previous points const currentRect = this._positioningEl.getBoundingClientRect(); // For calculating panning movement @@ -473,6 +543,34 @@ var PinchZoom = (function () { panY: newMidpoint.clientY - prevMidpoint.clientY, allowChangeEvent: true, }); + this._maybeStopPropagate(event); + } + _maybeResetScale() { + if (roundedCmp(this.scale, this.resetToMinScaleLimit) <= 0) { + this._resetToMinScale(); + } + } + _onPointerEnd(pointer, currentPointers, event) { + if (!this._positioningEl) + return; + const totalPointers = 1 + currentPointers.length; + const isPinch = totalPointers >= 2; + const isPan = totalPointers == 1; + if (isPinch) { + this._maybeResetScale(); + } + if (isPan && !this._allowPan()) { + return; + } + this._maybeStopPropagate(event); + } + _resetToMinScale() { + if (this.reachMinScaleStrategy === 'reset') { + this.setTransform({ scale: this.minScale, x: 0, y: 0 }); + } + else { + this.setTransform({ scale: this.minScale }); + } } /** Transform the view & fire a change event */ _applyChange(opts = {}) { @@ -496,6 +594,29 @@ var PinchZoom = (function () { y: matrix.f, }); } + _maybeStopPropagate(event) { + if (this.stopPropagateHandled) { + event.stopPropagation(); + } + } + _allowPan() { + return (this.allowPanMinScale > 0 + && roundedCmp(this.scale, this.allowPanMinScale) > 0); + } + _maybeEmitCancel(pointers) { + const makeCancelEvent = (pointer) => (new PointerEvent('pointercancel', { + pointerId: pointer.id, + clientX: pointer.clientX, + clientY: pointer.clientY, + })); + if (this.stopPropagateHandled) { + pointers.forEach(p => { + if (this.parentElement && typeof this.parentElement.dispatchEvent === 'function') { + this.parentElement.dispatchEvent(makeCancelEvent(p)); + } + }); + } + } } customElements.define('pinch-zoom', PinchZoom); diff --git a/dist/pinch-zoom.mjs b/dist/pinch-zoom.mjs index 75f27a7..db7319c 100644 --- a/dist/pinch-zoom.mjs +++ b/dist/pinch-zoom.mjs @@ -31,6 +31,11 @@ var css = "pinch-zoom {\n display: block;\n overflow: hidden;\n touch-action: styleInject(css); const minScaleAttr = 'min-scale'; +const allowPanMinScaleAttr = 'allow-pan-min-scale'; +const resetToMinScaleLimitAttr = 'reset-to-min-scale-limit'; +const reachMinScaleStrategyAttr = 'reach-min-scale-strategy'; +const stopPropagateHandledAttr = 'stop-propagate-handled'; +const reachMinScaleStrategyDefault = 'none'; function getDistance(a, b) { if (!b) return 0; @@ -65,6 +70,11 @@ function createPoint() { return getSVG().createSVGPoint(); } const MIN_SCALE = 0.01; +const ALLOW_PAN_MIN_SCALE = -1; +const RESET_TO_MIN_SCALE_LIMIT = -1; +const roundedCmp = (a, b) => { + return Math.round(a * 100) - Math.round(b * 100); +}; class PinchZoom extends HTMLElement { constructor() { super(); @@ -81,11 +91,20 @@ class PinchZoom extends HTMLElement { // We only want to track 2 pointers at most if (pointerTracker.currentPointers.length === 2 || !this._positioningEl) return false; + const isPan = pointerTracker.currentPointers.length + 1 === 1; + const handled = !(isPan && !this._allowPan()); + if (handled) { + this._maybeStopPropagate(event); + this._maybeEmitCancel([pointer, ...pointerTracker.currentPointers]); + } event.preventDefault(); return true; }, - move: (previousPointers) => { - this._onPointerMove(previousPointers, pointerTracker.currentPointers); + move: (previousPointers, _, event) => { + this._onPointerMove(previousPointers, pointerTracker.currentPointers, event); + }, + end: (pointer, event) => { + this._onPointerEnd(pointer, pointerTracker.currentPointers, event); }, }); this.addEventListener('wheel', event => this._onWheel(event)); @@ -110,6 +129,49 @@ class PinchZoom extends HTMLElement { set minScale(value) { this.setAttribute(minScaleAttr, String(value)); } + get reachMinScaleStrategy() { + const attrValue = this.getAttribute(reachMinScaleStrategyAttr); + const v = attrValue; + return v || reachMinScaleStrategyDefault; + } + set reachMinScaleStrategy(value) { + this.setAttribute(reachMinScaleStrategyAttr, value); + } + get allowPanMinScale() { + const attrValue = this.getAttribute(allowPanMinScaleAttr); + if (!attrValue) + return ALLOW_PAN_MIN_SCALE; + const value = parseFloat(attrValue); + if (Number.isFinite(value)) + return Math.max(ALLOW_PAN_MIN_SCALE, value); + return ALLOW_PAN_MIN_SCALE; + } + set allowPanMinScale(value) { + this.setAttribute(allowPanMinScaleAttr, String(value)); + } + get resetToMinScaleLimit() { + const attrValue = this.getAttribute(resetToMinScaleLimitAttr); + if (!attrValue) + return RESET_TO_MIN_SCALE_LIMIT; + const value = parseFloat(attrValue); + if (Number.isFinite(value)) + return Math.max(RESET_TO_MIN_SCALE_LIMIT, value); + return RESET_TO_MIN_SCALE_LIMIT; + } + set resetToMinScaleLimit(value) { + this.setAttribute(resetToMinScaleLimitAttr, String(value)); + } + get stopPropagateHandled() { + return this.hasAttribute(stopPropagateHandledAttr); + } + set stopPropagateHandled(value) { + if (value) { + this.setAttribute(stopPropagateHandledAttr, ''); + } + else { + this.removeAttribute(stopPropagateHandledAttr); + } + } connectedCallback() { this._stageElChange(); } @@ -260,16 +322,24 @@ class PinchZoom extends HTMLElement { // ctrlKey is true when pinch-zooming on a trackpad. const divisor = ctrlKey ? 100 : 300; const scaleDiff = 1 - deltaY / divisor; + const isZoomOut = scaleDiff < 1; this._applyChange({ scaleDiff, originX: event.clientX - currentRect.left, originY: event.clientY - currentRect.top, allowChangeEvent: true, }); + if (isZoomOut) { + this._maybeResetScale(); + } } - _onPointerMove(previousPointers, currentPointers) { + _onPointerMove(previousPointers, currentPointers, event) { if (!this._positioningEl) return; + const isPan = previousPointers.length < 2; + if (isPan && !this._allowPan()) { + return; + } // Combine next points with previous points const currentRect = this._positioningEl.getBoundingClientRect(); // For calculating panning movement @@ -288,6 +358,34 @@ class PinchZoom extends HTMLElement { panY: newMidpoint.clientY - prevMidpoint.clientY, allowChangeEvent: true, }); + this._maybeStopPropagate(event); + } + _maybeResetScale() { + if (roundedCmp(this.scale, this.resetToMinScaleLimit) <= 0) { + this._resetToMinScale(); + } + } + _onPointerEnd(pointer, currentPointers, event) { + if (!this._positioningEl) + return; + const totalPointers = 1 + currentPointers.length; + const isPinch = totalPointers >= 2; + const isPan = totalPointers == 1; + if (isPinch) { + this._maybeResetScale(); + } + if (isPan && !this._allowPan()) { + return; + } + this._maybeStopPropagate(event); + } + _resetToMinScale() { + if (this.reachMinScaleStrategy === 'reset') { + this.setTransform({ scale: this.minScale, x: 0, y: 0 }); + } + else { + this.setTransform({ scale: this.minScale }); + } } /** Transform the view & fire a change event */ _applyChange(opts = {}) { @@ -311,6 +409,29 @@ class PinchZoom extends HTMLElement { y: matrix.f, }); } + _maybeStopPropagate(event) { + if (this.stopPropagateHandled) { + event.stopPropagation(); + } + } + _allowPan() { + return (this.allowPanMinScale > 0 + && roundedCmp(this.scale, this.allowPanMinScale) > 0); + } + _maybeEmitCancel(pointers) { + const makeCancelEvent = (pointer) => (new PointerEvent('pointercancel', { + pointerId: pointer.id, + clientX: pointer.clientX, + clientY: pointer.clientY, + })); + if (this.stopPropagateHandled) { + pointers.forEach(p => { + if (this.parentElement && typeof this.parentElement.dispatchEvent === 'function') { + this.parentElement.dispatchEvent(makeCancelEvent(p)); + } + }); + } + } } customElements.define('pinch-zoom', PinchZoom); diff --git a/lib/pinch-zoom.ts b/lib/pinch-zoom.ts index 67a41b4..18e2b1e 100644 --- a/lib/pinch-zoom.ts +++ b/lib/pinch-zoom.ts @@ -30,6 +30,14 @@ interface SetTransformOpts extends ChangeOptions { type ScaleRelativeToValues = 'container' | 'content'; const minScaleAttr = 'min-scale'; +const allowPanMinScaleAttr = 'allow-pan-min-scale'; +const resetToMinScaleLimitAttr = 'reset-to-min-scale-limit'; +const reachMinScaleStrategyAttr = 'reach-min-scale-strategy'; +const stopPropagateHandledAttr = 'stop-propagate-handled'; + +type ReachMinScaleStrategy = 'reset' | 'none'; + +const reachMinScaleStrategyDefault: ReachMinScaleStrategy = 'none'; export interface ScaleToOpts extends ChangeOptions { /** Transform origin. Can be a number, or string percent, eg "50%" */ @@ -80,6 +88,12 @@ function createPoint(): SVGPoint { } const MIN_SCALE = 0.01; +const ALLOW_PAN_MIN_SCALE = -1; +const RESET_TO_MIN_SCALE_LIMIT = -1; + +const roundedCmp = (a: number, b: number) => { + return Math.round(a * 100) - Math.round(b * 100) +} export default class PinchZoom extends HTMLElement { // The element that we'll transform. @@ -105,11 +119,23 @@ export default class PinchZoom extends HTMLElement { start: (pointer, event) => { // We only want to track 2 pointers at most if (pointerTracker.currentPointers.length === 2 || !this._positioningEl) return false; + + const isPan = pointerTracker.currentPointers.length + 1 === 1; + + const handled = !(isPan && !this._allowPan()); + if (handled) { + this._maybeStopPropagate(event); + this._maybeEmitCancel([pointer, ...pointerTracker.currentPointers]); + } + event.preventDefault(); return true; }, - move: (previousPointers) => { - this._onPointerMove(previousPointers, pointerTracker.currentPointers); + move: (previousPointers, _, event) => { + this._onPointerMove(previousPointers, pointerTracker.currentPointers, event); + }, + end: (pointer, event) => { + this._onPointerEnd(pointer, pointerTracker.currentPointers, event); }, }); @@ -138,6 +164,58 @@ export default class PinchZoom extends HTMLElement { this.setAttribute(minScaleAttr, String(value)); } + get reachMinScaleStrategy(): ReachMinScaleStrategy { + const attrValue = this.getAttribute(reachMinScaleStrategyAttr); + + const v = attrValue as ReachMinScaleStrategy; + + return v || reachMinScaleStrategyDefault; + } + + set reachMinScaleStrategy(value: ReachMinScaleStrategy) { + this.setAttribute(reachMinScaleStrategyAttr, value as string); + } + + get allowPanMinScale(): number { + const attrValue = this.getAttribute(allowPanMinScaleAttr); + if (!attrValue) return ALLOW_PAN_MIN_SCALE; + + const value = parseFloat(attrValue); + if (Number.isFinite(value)) return Math.max(ALLOW_PAN_MIN_SCALE, value); + + return ALLOW_PAN_MIN_SCALE; + } + + set allowPanMinScale(value: number) { + this.setAttribute(allowPanMinScaleAttr, String(value)); + } + + get resetToMinScaleLimit(): number { + const attrValue = this.getAttribute(resetToMinScaleLimitAttr); + if (!attrValue) return RESET_TO_MIN_SCALE_LIMIT; + + const value = parseFloat(attrValue); + if (Number.isFinite(value)) return Math.max(RESET_TO_MIN_SCALE_LIMIT, value); + + return RESET_TO_MIN_SCALE_LIMIT; + } + + set resetToMinScaleLimit(value: number) { + this.setAttribute(resetToMinScaleLimitAttr, String(value)); + } + + get stopPropagateHandled() { + return this.hasAttribute(stopPropagateHandledAttr); + } + + set stopPropagateHandled(value: boolean) { + if (value) { + this.setAttribute(stopPropagateHandledAttr, ''); + } else { + this.removeAttribute(stopPropagateHandledAttr); + } + } + connectedCallback() { this._stageElChange(); } @@ -331,17 +409,29 @@ export default class PinchZoom extends HTMLElement { const divisor = ctrlKey ? 100 : 300; const scaleDiff = 1 - deltaY / divisor; + const isZoomOut = scaleDiff < 1; + this._applyChange({ scaleDiff, originX: event.clientX - currentRect.left, originY: event.clientY - currentRect.top, allowChangeEvent: true, }); + + if (isZoomOut) { + this._maybeResetScale(); + } } - private _onPointerMove(previousPointers: Pointer[], currentPointers: Pointer[]) { + private _onPointerMove(previousPointers: Pointer[], currentPointers: Pointer[], event: Event) { if (!this._positioningEl) return; + const isPan = previousPointers.length < 2; + + if (isPan && !this._allowPan()) { + return; + } + // Combine next points with previous points const currentRect = this._positioningEl.getBoundingClientRect(); @@ -364,8 +454,42 @@ export default class PinchZoom extends HTMLElement { panY: newMidpoint.clientY - prevMidpoint.clientY, allowChangeEvent: true, }); + + this._maybeStopPropagate(event); } + private _maybeResetScale() { + if (roundedCmp(this.scale, this.resetToMinScaleLimit) <= 0) { + this._resetToMinScale(); + } + } + + private _onPointerEnd(pointer: Pointer, currentPointers: Pointer[], event: Event) { + if (!this._positioningEl) return; + + const totalPointers = 1 + currentPointers.length; + const isPinch = totalPointers >= 2; + const isPan = totalPointers == 1; + + if (isPinch) { + this._maybeResetScale(); + } + + if (isPan && !this._allowPan()) { + return; + } + this._maybeStopPropagate(event); + } + + private _resetToMinScale() { + if (this.reachMinScaleStrategy === 'reset') { + this.setTransform({ scale: this.minScale, x: 0, y: 0 }); + } else { + this.setTransform({ scale: this.minScale }); + } + } + + /** Transform the view & fire a change event */ private _applyChange(opts: ApplyChangeOpts = {}) { const { @@ -395,4 +519,35 @@ export default class PinchZoom extends HTMLElement { y: matrix.f, }); } + + private _maybeStopPropagate(event: Event) { + if (this.stopPropagateHandled) { + event.stopPropagation(); + } + } + + private _allowPan() { + return ( + this.allowPanMinScale > 0 + && roundedCmp(this.scale, this.allowPanMinScale) > 0 + ); + } + + private _maybeEmitCancel(pointers: Pointer[]) { + const makeCancelEvent = (pointer: Pointer) => ( + new PointerEvent( + 'pointercancel', { + pointerId: pointer.id, + clientX: pointer.clientX, + clientY: pointer.clientY, + } + )); + if (this.stopPropagateHandled) { + pointers.forEach(p => { + if (this.parentElement && typeof this.parentElement.dispatchEvent === 'function') { + this.parentElement.dispatchEvent(makeCancelEvent(p)); + } + }); + } + } } diff --git a/package.json b/package.json index 29ec810..238a6d1 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "module": "dist/pinch-zoom.es.js", "types": "dist/index.d.ts", "scripts": { - "build": "rm -r dist && rollup -c" + "build": "rm -rf dist && rollup -c" }, "repository": { "type": "git",