Compare commits

...

10 commits

Author SHA1 Message Date
FloatingGhost 4741a2f932 update version 2023-01-02 14:48:59 +00:00
FloatingGhost dc8dfa6a91 extract CSS to its own file 2023-01-02 14:47:21 +00:00
Tusooa Zhu ff8e6f380e
Bump version to 1.2.0 2021-09-07 14:14:45 -04:00
Tusooa Zhu b5d2e9fb41
Scale to minScale when scaling beyond minScale 2021-08-02 23:20:11 -04:00
Tusooa Zhu fbb4d71b1c
Do not propagate click if the event is handled 2021-08-02 20:58:32 -04:00
Tusooa Zhu de150c0105
Do not cancel when panning 2021-08-02 17:15:53 -04:00
Tusooa Zhu a94fb753a1
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.
2021-08-02 16:48:32 -04:00
Tusooa Zhu 390b0e0278
Rebrand package 2021-08-02 13:04:11 -04:00
Tusooa Zhu 9f6bd1d6b0
Audit 2021-08-02 12:56:06 -04:00
Jake Archibald f5866785e1 1.1.1 2019-02-22 09:33:42 +00:00
12 changed files with 5647 additions and 252 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
*.css.d.ts
.rpt2_cache
node_modules

File diff suppressed because one or more lines are too long

147
dist/pinch-zoom.cjs.js vendored
View file

@ -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,12 @@ function createPoint() {
return getSVG().createSVGPoint();
}
const MIN_SCALE = 0.01;
const ALLOW_PAN_MIN_SCALE = -1;
const RESET_TO_MIN_SCALE_LIMIT = -1;
const BUTTON_LEFT = 0;
const roundedCmp = (a, b) => {
return Math.round(a * 100) - Math.round(b * 100);
};
class PinchZoom extends HTMLElement {
constructor() {
super();
@ -85,14 +96,26 @@ 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);
if (!isPan) { // only cancel if something was propagated
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));
this.addEventListener('click', event => this._onClick(event, pointerTracker));
}
static get observedAttributes() { return [minScaleAttr]; }
attributeChangedCallback(name, oldValue, newValue) {
@ -114,6 +137,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();
}
@ -215,8 +281,9 @@ class PinchZoom extends HTMLElement {
*/
_updateTransform(scale, x, y, allowChangeEvent) {
// Avoid scaling to zero
if (scale < this.minScale)
return;
if (scale < this.minScale) {
scale = this.minScale;
}
// Return if there's no change
if (scale === this.scale &&
x === this.x &&
@ -264,16 +331,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 +367,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 +418,40 @@ 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));
}
});
}
}
_onClick(event, pointerTracker) {
// We never handle non-left-clicks
if (event.button !== BUTTON_LEFT) {
return;
}
const wasPanning = pointerTracker.currentPointers.length === 0;
const handled = !(wasPanning && !this._allowPan());
if (handled) {
this._maybeStopPropagate(event);
}
}
}
customElements.define('pinch-zoom', PinchZoom);

14
dist/pinch-zoom.css vendored Normal file
View file

@ -0,0 +1,14 @@
pinch-zoom {
display: block;
overflow: hidden;
touch-action: none;
--scale: 1;
--x: 0;
--y: 0;
}
pinch-zoom > * {
transform: translate(var(--x), var(--y)) scale(var(--scale));
transform-origin: 0 0;
will-change: transform;
}

12
dist/pinch-zoom.d.ts vendored
View file

@ -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,14 @@ 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;
private _onClick;
}
export {};

147
dist/pinch-zoom.es.js vendored
View file

@ -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,12 @@ function createPoint() {
return getSVG().createSVGPoint();
}
const MIN_SCALE = 0.01;
const ALLOW_PAN_MIN_SCALE = -1;
const RESET_TO_MIN_SCALE_LIMIT = -1;
const BUTTON_LEFT = 0;
const roundedCmp = (a, b) => {
return Math.round(a * 100) - Math.round(b * 100);
};
class PinchZoom extends HTMLElement {
constructor() {
super();
@ -81,14 +92,26 @@ 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);
if (!isPan) { // only cancel if something was propagated
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));
this.addEventListener('click', event => this._onClick(event, pointerTracker));
}
static get observedAttributes() { return [minScaleAttr]; }
attributeChangedCallback(name, oldValue, newValue) {
@ -110,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();
}
@ -211,8 +277,9 @@ class PinchZoom extends HTMLElement {
*/
_updateTransform(scale, x, y, allowChangeEvent) {
// Avoid scaling to zero
if (scale < this.minScale)
return;
if (scale < this.minScale) {
scale = this.minScale;
}
// Return if there's no change
if (scale === this.scale &&
x === this.x &&
@ -260,16 +327,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 +363,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 +414,40 @@ 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));
}
});
}
}
_onClick(event, pointerTracker) {
// We never handle non-left-clicks
if (event.button !== BUTTON_LEFT) {
return;
}
const wasPanning = pointerTracker.currentPointers.length === 0;
const handled = !(wasPanning && !this._allowPan());
if (handled) {
this._maybeStopPropagate(event);
}
}
}
customElements.define('pinch-zoom', PinchZoom);

177
dist/pinch-zoom.js vendored
View file

@ -185,37 +185,12 @@ var PinchZoom = (function () {
}
}
function styleInject(css, ref) {
if ( ref === void 0 ) ref = {};
var insertAt = ref.insertAt;
if (!css || typeof document === 'undefined') { return; }
var head = document.head || document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
if (insertAt === 'top') {
if (head.firstChild) {
head.insertBefore(style, head.firstChild);
} else {
head.appendChild(style);
}
} else {
head.appendChild(style);
}
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
}
var css = "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";
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 +225,12 @@ 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 BUTTON_LEFT = 0;
const roundedCmp = (a, b) => {
return Math.round(a * 100) - Math.round(b * 100);
};
class PinchZoom extends HTMLElement {
constructor() {
super();
@ -266,14 +247,26 @@ 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);
if (!isPan) { // only cancel if something was propagated
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));
this.addEventListener('click', event => this._onClick(event, pointerTracker));
}
static get observedAttributes() { return [minScaleAttr]; }
attributeChangedCallback(name, oldValue, newValue) {
@ -295,6 +288,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();
}
@ -396,8 +432,9 @@ var PinchZoom = (function () {
*/
_updateTransform(scale, x, y, allowChangeEvent) {
// Avoid scaling to zero
if (scale < this.minScale)
return;
if (scale < this.minScale) {
scale = this.minScale;
}
// Return if there's no change
if (scale === this.scale &&
x === this.x &&
@ -445,16 +482,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 +518,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 +569,40 @@ 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));
}
});
}
}
_onClick(event, pointerTracker) {
// We never handle non-left-clicks
if (event.button !== BUTTON_LEFT) {
return;
}
const wasPanning = pointerTracker.currentPointers.length === 0;
const handled = !(wasPanning && !this._allowPan());
if (handled) {
this._maybeStopPropagate(event);
}
}
}
customElements.define('pinch-zoom', PinchZoom);

177
dist/pinch-zoom.mjs vendored
View file

@ -1,36 +1,11 @@
import PointerTracker from 'pointer-tracker';
function styleInject(css, ref) {
if ( ref === void 0 ) ref = {};
var insertAt = ref.insertAt;
if (!css || typeof document === 'undefined') { return; }
var head = document.head || document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
if (insertAt === 'top') {
if (head.firstChild) {
head.insertBefore(style, head.firstChild);
} else {
head.appendChild(style);
}
} else {
head.appendChild(style);
}
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
}
var css = "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";
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 +40,12 @@ function createPoint() {
return getSVG().createSVGPoint();
}
const MIN_SCALE = 0.01;
const ALLOW_PAN_MIN_SCALE = -1;
const RESET_TO_MIN_SCALE_LIMIT = -1;
const BUTTON_LEFT = 0;
const roundedCmp = (a, b) => {
return Math.round(a * 100) - Math.round(b * 100);
};
class PinchZoom extends HTMLElement {
constructor() {
super();
@ -81,14 +62,26 @@ 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);
if (!isPan) { // only cancel if something was propagated
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));
this.addEventListener('click', event => this._onClick(event, pointerTracker));
}
static get observedAttributes() { return [minScaleAttr]; }
attributeChangedCallback(name, oldValue, newValue) {
@ -110,6 +103,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();
}
@ -211,8 +247,9 @@ class PinchZoom extends HTMLElement {
*/
_updateTransform(scale, x, y, allowChangeEvent) {
// Avoid scaling to zero
if (scale < this.minScale)
return;
if (scale < this.minScale) {
scale = this.minScale;
}
// Return if there's no change
if (scale === this.scale &&
x === this.x &&
@ -260,16 +297,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 +333,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 +384,40 @@ 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));
}
});
}
}
_onClick(event, pointerTracker) {
// We never handle non-left-clicks
if (event.button !== BUTTON_LEFT) {
return;
}
const wasPanning = pointerTracker.currentPointers.length === 0;
const handled = !(wasPanning && !this._allowPan());
if (handled) {
this._maybeStopPropagate(event);
}
}
}
customElements.define('pinch-zoom', PinchZoom);

View file

@ -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,14 @@ function createPoint(): SVGPoint {
}
const MIN_SCALE = 0.01;
const ALLOW_PAN_MIN_SCALE = -1;
const RESET_TO_MIN_SCALE_LIMIT = -1;
const BUTTON_LEFT = 0;
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,15 +121,30 @@ 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);
if (!isPan) { // only cancel if something was propagated
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));
this.addEventListener('click', event => this._onClick(event, pointerTracker));
}
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
@ -138,6 +169,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();
}
@ -270,7 +353,9 @@ export default class PinchZoom extends HTMLElement {
*/
private _updateTransform(scale: number, x: number, y: number, allowChangeEvent: boolean) {
// Avoid scaling to zero
if (scale < this.minScale) return;
if (scale < this.minScale) {
scale = this.minScale;
}
// Return if there's no change
if (
@ -331,17 +416,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 +461,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 +526,48 @@ 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));
}
});
}
}
private _onClick(event: MouseEvent, pointerTracker: PointerTracker) {
// We never handle non-left-clicks
if (event.button !== BUTTON_LEFT) {
return;
}
const wasPanning = pointerTracker.currentPointers.length === 0;
const handled = !(wasPanning && !this._allowPan());
if (handled) {
this._maybeStopPropagate(event);
}
}
}

5025
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,16 +1,16 @@
{
"name": "pinch-zoom-element",
"version": "1.1.0",
"version": "1.3.0",
"description": "Put stuff in an element, now you can pinch-zoom it!",
"main": "dist/pinch-zoom.cjs.js",
"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",
"url": "git+https://github.com/GoogleChromeLabs/pinch-zoom.git"
"url": "https://akkoma.dev/pinch-zoom-element.git"
},
"keywords": [
"pointer",
@ -19,12 +19,12 @@
"pinch zoom",
"pan"
],
"author": "Jake Archibald",
"author": "Jake Archibald, tusooa, FloatingGhost",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/GoogleChromeLabs/pinch-zoom/issues"
"url": "https://lily.kazv.moe/infra/pinch-zoom-element/-/issues"
},
"homepage": "https://github.com/GoogleChromeLabs/pinch-zoom#readme",
"homepage": "https://lily.kazv.moe/infra/pinch-zoom-element",
"dependencies": {
"pointer-tracker": "^2.0.3"
},

View file

@ -7,7 +7,7 @@ import { dependencies } from './package.json';
const mjs = {
plugins: [
typescript({ useTsconfigDeclarationDir: false }),
postcss()
postcss({ extract: 'dist/pinch-zoom.css' })
],
external: Object.keys(dependencies),
input: 'lib/index.ts',