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",