@sitebytom/use-zoom-pan 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -0
- package/dist/index.d.mts +103 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +648 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +641 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +78 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React = require('react');
|
|
4
|
+
|
|
5
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
6
|
+
|
|
7
|
+
var React__default = /*#__PURE__*/_interopDefault(React);
|
|
8
|
+
|
|
9
|
+
// src/useZoomPan.ts
|
|
10
|
+
var DEFAULT_OPTIONS = {
|
|
11
|
+
minScale: 1,
|
|
12
|
+
maxScale: 6,
|
|
13
|
+
zoomSensitivity: 2e-3,
|
|
14
|
+
clickZoomScale: 2.5,
|
|
15
|
+
dragThresholdMouse: 5,
|
|
16
|
+
dragThresholdTouch: 10,
|
|
17
|
+
swipeThreshold: 50,
|
|
18
|
+
boundsBuffer: 80,
|
|
19
|
+
manageCursor: true,
|
|
20
|
+
enableSwipe: true,
|
|
21
|
+
initialScale: void 0,
|
|
22
|
+
initialPosition: void 0
|
|
23
|
+
};
|
|
24
|
+
var TRANSITION_DURATION = 400;
|
|
25
|
+
var TRANSITION_CURVE = "cubic-bezier(0.2, 0, 0, 1)";
|
|
26
|
+
var calculateBounds = (targetScale, container, element, boundsBuffer) => {
|
|
27
|
+
if (!container || !element) return { xLimit: 0, yLimit: 0 };
|
|
28
|
+
const containerWidth = container.clientWidth;
|
|
29
|
+
const containerHeight = container.clientHeight;
|
|
30
|
+
const elementWidth = element.offsetWidth || containerWidth;
|
|
31
|
+
const elementHeight = element.offsetHeight || containerHeight;
|
|
32
|
+
const scaledWidth = elementWidth * targetScale;
|
|
33
|
+
const scaledHeight = elementHeight * targetScale;
|
|
34
|
+
const xLimit = (scaledWidth <= containerWidth ? 0 : (scaledWidth - containerWidth) / 2) + boundsBuffer;
|
|
35
|
+
const yLimit = (scaledHeight <= containerHeight ? 0 : (scaledHeight - containerHeight) / 2) + boundsBuffer;
|
|
36
|
+
return { xLimit, yLimit };
|
|
37
|
+
};
|
|
38
|
+
var clampPosition = (pos, targetScale, container, element, boundsBuffer) => {
|
|
39
|
+
const { xLimit, yLimit } = calculateBounds(targetScale, container, element, boundsBuffer);
|
|
40
|
+
return {
|
|
41
|
+
x: Math.max(-xLimit, Math.min(xLimit, pos.x)),
|
|
42
|
+
y: Math.max(-yLimit, Math.min(yLimit, pos.y))
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
var normalizeWheelDelta = (e, sensitivity) => {
|
|
46
|
+
const factor = e.deltaMode === 1 ? 20 : 1;
|
|
47
|
+
return -e.deltaY * factor * sensitivity;
|
|
48
|
+
};
|
|
49
|
+
var useZoomPan = ({
|
|
50
|
+
containerRef,
|
|
51
|
+
enableZoom = true,
|
|
52
|
+
onNext,
|
|
53
|
+
onPrev,
|
|
54
|
+
options = {}
|
|
55
|
+
}) => {
|
|
56
|
+
const config = React__default.default.useMemo(() => ({
|
|
57
|
+
...DEFAULT_OPTIONS,
|
|
58
|
+
...options
|
|
59
|
+
}), [
|
|
60
|
+
options.minScale,
|
|
61
|
+
options.maxScale,
|
|
62
|
+
options.zoomSensitivity,
|
|
63
|
+
options.clickZoomScale,
|
|
64
|
+
options.dragThresholdMouse,
|
|
65
|
+
options.dragThresholdTouch,
|
|
66
|
+
options.swipeThreshold,
|
|
67
|
+
options.boundsBuffer,
|
|
68
|
+
options.manageCursor,
|
|
69
|
+
options.initialScale,
|
|
70
|
+
options.initialPosition
|
|
71
|
+
]);
|
|
72
|
+
const contentRef = React__default.default.useRef(null);
|
|
73
|
+
const getContentElement = React.useCallback(() => {
|
|
74
|
+
return contentRef.current || containerRef.current?.firstElementChild;
|
|
75
|
+
}, [containerRef]);
|
|
76
|
+
const [scale, setScale] = React.useState(config.initialScale ?? config.minScale);
|
|
77
|
+
const [position, setPosition] = React.useState(config.initialPosition ?? { x: 0, y: 0 });
|
|
78
|
+
const [isDragging, setIsDragging] = React.useState(false);
|
|
79
|
+
const [isTransitioning, setIsTransitioning] = React.useState(false);
|
|
80
|
+
const dragStartRef = React.useRef({
|
|
81
|
+
x: 0,
|
|
82
|
+
y: 0,
|
|
83
|
+
hasDragged: false,
|
|
84
|
+
startX: 0,
|
|
85
|
+
startY: 0
|
|
86
|
+
});
|
|
87
|
+
const pinchRef = React.useRef({
|
|
88
|
+
startDist: 0,
|
|
89
|
+
initialScale: config.minScale,
|
|
90
|
+
startX: 0,
|
|
91
|
+
startY: 0,
|
|
92
|
+
startPos: { x: 0, y: 0 }
|
|
93
|
+
});
|
|
94
|
+
const touchStartXRef = React.useRef(0);
|
|
95
|
+
const swipeBlockedRef = React.useRef(false);
|
|
96
|
+
const stateRef = React.useRef({ scale, position, enableZoom, isDragging, config });
|
|
97
|
+
React.useEffect(() => {
|
|
98
|
+
stateRef.current = { scale, position, enableZoom, isDragging, config };
|
|
99
|
+
}, [scale, position, enableZoom, isDragging, config]);
|
|
100
|
+
React.useEffect(() => {
|
|
101
|
+
const handleGlobalUp = () => {
|
|
102
|
+
if (stateRef.current.isDragging) {
|
|
103
|
+
setIsDragging(false);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const handleBlur = () => {
|
|
107
|
+
setIsDragging(false);
|
|
108
|
+
};
|
|
109
|
+
window.addEventListener("mouseup", handleGlobalUp);
|
|
110
|
+
window.addEventListener("touchend", handleGlobalUp);
|
|
111
|
+
window.addEventListener("touchcancel", handleGlobalUp);
|
|
112
|
+
window.addEventListener("blur", handleBlur);
|
|
113
|
+
return () => {
|
|
114
|
+
window.removeEventListener("mouseup", handleGlobalUp);
|
|
115
|
+
window.removeEventListener("touchend", handleGlobalUp);
|
|
116
|
+
window.removeEventListener("touchcancel", handleGlobalUp);
|
|
117
|
+
window.removeEventListener("blur", handleBlur);
|
|
118
|
+
setIsDragging(false);
|
|
119
|
+
dragStartRef.current.hasDragged = false;
|
|
120
|
+
setIsTransitioning(false);
|
|
121
|
+
};
|
|
122
|
+
}, []);
|
|
123
|
+
const updateBoundsAndClamp = React.useCallback(() => {
|
|
124
|
+
const container = containerRef.current;
|
|
125
|
+
const content = getContentElement();
|
|
126
|
+
if (!container || !content) return;
|
|
127
|
+
const currentPos = { x: stateRef.current.position.x, y: stateRef.current.position.y };
|
|
128
|
+
const clamped = clampPosition(currentPos, stateRef.current.scale, container, content, config.boundsBuffer);
|
|
129
|
+
if (clamped.x !== currentPos.x || clamped.y !== currentPos.y) {
|
|
130
|
+
setPosition(clamped);
|
|
131
|
+
}
|
|
132
|
+
}, [containerRef, config.boundsBuffer, getContentElement]);
|
|
133
|
+
React__default.default.useLayoutEffect(() => {
|
|
134
|
+
const container = containerRef.current;
|
|
135
|
+
if (!container) return;
|
|
136
|
+
const observer = new ResizeObserver(() => {
|
|
137
|
+
updateBoundsAndClamp();
|
|
138
|
+
});
|
|
139
|
+
observer.observe(container);
|
|
140
|
+
const content = getContentElement();
|
|
141
|
+
if (content instanceof HTMLImageElement && !content.complete) {
|
|
142
|
+
content.addEventListener("load", updateBoundsAndClamp);
|
|
143
|
+
}
|
|
144
|
+
return () => {
|
|
145
|
+
observer.disconnect();
|
|
146
|
+
if (content instanceof HTMLImageElement) {
|
|
147
|
+
content.removeEventListener("load", updateBoundsAndClamp);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}, [containerRef, updateBoundsAndClamp, getContentElement]);
|
|
151
|
+
const getClampedPosition = React.useCallback(
|
|
152
|
+
(pos, targetScale, element) => {
|
|
153
|
+
return clampPosition(
|
|
154
|
+
pos,
|
|
155
|
+
targetScale,
|
|
156
|
+
containerRef.current,
|
|
157
|
+
element,
|
|
158
|
+
config.boundsBuffer
|
|
159
|
+
);
|
|
160
|
+
},
|
|
161
|
+
[containerRef, config.boundsBuffer]
|
|
162
|
+
);
|
|
163
|
+
const handleWheelManual = React.useCallback(
|
|
164
|
+
(e) => {
|
|
165
|
+
if (!stateRef.current.enableZoom) return;
|
|
166
|
+
setIsTransitioning(false);
|
|
167
|
+
e.preventDefault();
|
|
168
|
+
const { scale: currentScale, position: currentPosition, config: config2 } = stateRef.current;
|
|
169
|
+
const delta = normalizeWheelDelta(e, config2.zoomSensitivity);
|
|
170
|
+
const newScale = Math.min(Math.max(config2.minScale, currentScale + delta), config2.maxScale);
|
|
171
|
+
if (newScale === config2.minScale) {
|
|
172
|
+
setScale(config2.minScale);
|
|
173
|
+
setPosition({ x: 0, y: 0 });
|
|
174
|
+
} else {
|
|
175
|
+
const container = containerRef.current;
|
|
176
|
+
const content = getContentElement();
|
|
177
|
+
if (container && content) {
|
|
178
|
+
const rect = container.getBoundingClientRect();
|
|
179
|
+
const containerWidth = rect.width;
|
|
180
|
+
const containerHeight = rect.height;
|
|
181
|
+
const centerX = containerWidth / 2;
|
|
182
|
+
const centerY = containerHeight / 2;
|
|
183
|
+
const mouseX = e.clientX - (rect.left + centerX);
|
|
184
|
+
const mouseY = e.clientY - (rect.top + centerY);
|
|
185
|
+
const contentX = (mouseX - currentPosition.x) / currentScale;
|
|
186
|
+
const contentY = (mouseY - currentPosition.y) / currentScale;
|
|
187
|
+
const newPosition = {
|
|
188
|
+
x: mouseX - contentX * newScale,
|
|
189
|
+
y: mouseY - contentY * newScale
|
|
190
|
+
};
|
|
191
|
+
const clampedPosition = getClampedPosition(newPosition, newScale, content);
|
|
192
|
+
setPosition(clampedPosition);
|
|
193
|
+
setScale(newScale);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
[getClampedPosition, containerRef, getContentElement, config.zoomSensitivity, config.minScale, config.maxScale]
|
|
198
|
+
);
|
|
199
|
+
React.useEffect(() => {
|
|
200
|
+
const container = containerRef.current;
|
|
201
|
+
if (!container) return;
|
|
202
|
+
container.addEventListener("wheel", handleWheelManual, { passive: false });
|
|
203
|
+
return () => container.removeEventListener("wheel", handleWheelManual);
|
|
204
|
+
}, [containerRef, handleWheelManual]);
|
|
205
|
+
const reset = React.useCallback(() => {
|
|
206
|
+
setIsTransitioning(true);
|
|
207
|
+
setScale(config.minScale);
|
|
208
|
+
setPosition({ x: 0, y: 0 });
|
|
209
|
+
setIsDragging(false);
|
|
210
|
+
dragStartRef.current.hasDragged = false;
|
|
211
|
+
pinchRef.current = {
|
|
212
|
+
startDist: 0,
|
|
213
|
+
initialScale: config.minScale,
|
|
214
|
+
startX: 0,
|
|
215
|
+
startY: 0,
|
|
216
|
+
startPos: { x: 0, y: 0 }
|
|
217
|
+
};
|
|
218
|
+
}, [config.minScale]);
|
|
219
|
+
const handleFocalZoom = React.useCallback(
|
|
220
|
+
(e) => {
|
|
221
|
+
setIsTransitioning(true);
|
|
222
|
+
const container = containerRef.current;
|
|
223
|
+
const target = e.currentTarget;
|
|
224
|
+
if (!container || !target) return;
|
|
225
|
+
const rect = container.getBoundingClientRect();
|
|
226
|
+
const containerWidth = container.clientWidth;
|
|
227
|
+
const containerHeight = container.clientHeight;
|
|
228
|
+
const centerX = containerWidth / 2;
|
|
229
|
+
const centerY = containerHeight / 2;
|
|
230
|
+
const mouseX = e.clientX - (rect.left + centerX);
|
|
231
|
+
const mouseY = e.clientY - (rect.top + centerY);
|
|
232
|
+
const newPosition = {
|
|
233
|
+
x: mouseX * (1 - config.clickZoomScale),
|
|
234
|
+
y: mouseY * (1 - config.clickZoomScale)
|
|
235
|
+
};
|
|
236
|
+
const clampedPosition = getClampedPosition(newPosition, config.clickZoomScale, target);
|
|
237
|
+
setScale(config.clickZoomScale);
|
|
238
|
+
setPosition(clampedPosition);
|
|
239
|
+
},
|
|
240
|
+
[getClampedPosition, config.clickZoomScale, containerRef]
|
|
241
|
+
);
|
|
242
|
+
const onImageClick = React.useCallback(
|
|
243
|
+
(e) => {
|
|
244
|
+
if (dragStartRef.current.hasDragged) {
|
|
245
|
+
dragStartRef.current.hasDragged = false;
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (scale > config.minScale) {
|
|
249
|
+
reset();
|
|
250
|
+
} else {
|
|
251
|
+
handleFocalZoom(e);
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
[scale, reset, handleFocalZoom, config.minScale]
|
|
255
|
+
);
|
|
256
|
+
const onImageDoubleClick = React.useCallback(
|
|
257
|
+
(e) => {
|
|
258
|
+
if (scale > config.minScale) {
|
|
259
|
+
reset();
|
|
260
|
+
} else {
|
|
261
|
+
handleFocalZoom(e);
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
[scale, reset, handleFocalZoom, config.minScale]
|
|
265
|
+
);
|
|
266
|
+
const getPinchPosition = React.useCallback((centerX, centerY, newScale) => {
|
|
267
|
+
const { containerRect, startX, startY, initialScale, startPos } = pinchRef.current;
|
|
268
|
+
if (!containerRect) return { x: 0, y: 0 };
|
|
269
|
+
const containerCenterX = containerRect.width / 2;
|
|
270
|
+
const containerCenterY = containerRect.height / 2;
|
|
271
|
+
const currentPinchX = centerX - (containerRect.left + containerCenterX);
|
|
272
|
+
const currentPinchY = centerY - (containerRect.top + containerCenterY);
|
|
273
|
+
const startPinchX = startX - (containerRect.left + containerCenterX);
|
|
274
|
+
const startPinchY = startY - (containerRect.top + containerCenterY);
|
|
275
|
+
const scaleRatio = newScale / initialScale;
|
|
276
|
+
const pinchImageX = startPinchX - startPos.x;
|
|
277
|
+
const pinchImageY = startPinchY - startPos.y;
|
|
278
|
+
return {
|
|
279
|
+
x: currentPinchX - pinchImageX * scaleRatio,
|
|
280
|
+
y: currentPinchY - pinchImageY * scaleRatio
|
|
281
|
+
};
|
|
282
|
+
}, []);
|
|
283
|
+
const onImageTouchStart = React.useCallback(
|
|
284
|
+
(e) => {
|
|
285
|
+
setIsTransitioning(false);
|
|
286
|
+
swipeBlockedRef.current = e.touches.length === 2;
|
|
287
|
+
if (e.touches.length === 2) {
|
|
288
|
+
const container = containerRef.current;
|
|
289
|
+
const dist = Math.hypot(
|
|
290
|
+
e.touches[0].clientX - e.touches[1].clientX,
|
|
291
|
+
e.touches[0].clientY - e.touches[1].clientY
|
|
292
|
+
);
|
|
293
|
+
const centerX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
|
|
294
|
+
const centerY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
|
|
295
|
+
pinchRef.current = {
|
|
296
|
+
startDist: dist,
|
|
297
|
+
initialScale: scale,
|
|
298
|
+
startX: centerX,
|
|
299
|
+
startY: centerY,
|
|
300
|
+
startPos: { x: position.x, y: position.y },
|
|
301
|
+
containerRect: container?.getBoundingClientRect()
|
|
302
|
+
};
|
|
303
|
+
} else if (e.touches.length === 1 && scale > config.minScale) {
|
|
304
|
+
setIsDragging(true);
|
|
305
|
+
dragStartRef.current = {
|
|
306
|
+
x: e.touches[0].clientX - position.x,
|
|
307
|
+
y: e.touches[0].clientY - position.y,
|
|
308
|
+
hasDragged: false,
|
|
309
|
+
startX: e.touches[0].clientX,
|
|
310
|
+
startY: e.touches[0].clientY
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
[scale, position, config.minScale, containerRef]
|
|
315
|
+
);
|
|
316
|
+
const onImageTouchMove = React.useCallback(
|
|
317
|
+
(e) => {
|
|
318
|
+
if (e.touches.length === 2) {
|
|
319
|
+
e.preventDefault();
|
|
320
|
+
const dist = Math.hypot(
|
|
321
|
+
e.touches[0].clientX - e.touches[1].clientX,
|
|
322
|
+
e.touches[0].clientY - e.touches[1].clientY
|
|
323
|
+
);
|
|
324
|
+
const centerX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
|
|
325
|
+
const centerY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
|
|
326
|
+
const ratio = dist / pinchRef.current.startDist;
|
|
327
|
+
const newScale = Math.min(Math.max(config.minScale, pinchRef.current.initialScale * ratio), config.maxScale);
|
|
328
|
+
if (pinchRef.current.containerRect && newScale > config.minScale) {
|
|
329
|
+
const newPosition = getPinchPosition(
|
|
330
|
+
centerX,
|
|
331
|
+
centerY,
|
|
332
|
+
newScale
|
|
333
|
+
);
|
|
334
|
+
const clampedPosition = getClampedPosition(
|
|
335
|
+
newPosition,
|
|
336
|
+
newScale,
|
|
337
|
+
getContentElement()
|
|
338
|
+
);
|
|
339
|
+
setPosition(clampedPosition);
|
|
340
|
+
} else {
|
|
341
|
+
setPosition({ x: 0, y: 0 });
|
|
342
|
+
}
|
|
343
|
+
setScale(newScale);
|
|
344
|
+
} else if (e.touches.length === 1 && isDragging && scale > config.minScale) {
|
|
345
|
+
e.preventDefault();
|
|
346
|
+
const touchX = e.touches[0].clientX;
|
|
347
|
+
const touchY = e.touches[0].clientY;
|
|
348
|
+
if (!dragStartRef.current.hasDragged) {
|
|
349
|
+
const moveDist = Math.hypot(
|
|
350
|
+
touchX - dragStartRef.current.startX,
|
|
351
|
+
touchY - dragStartRef.current.startY
|
|
352
|
+
);
|
|
353
|
+
if (moveDist > config.dragThresholdTouch) {
|
|
354
|
+
dragStartRef.current.hasDragged = true;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (dragStartRef.current.hasDragged) {
|
|
358
|
+
const newPosition = {
|
|
359
|
+
x: touchX - dragStartRef.current.x,
|
|
360
|
+
y: touchY - dragStartRef.current.y
|
|
361
|
+
};
|
|
362
|
+
const clampedPosition = getClampedPosition(newPosition, scale, e.currentTarget);
|
|
363
|
+
setPosition(clampedPosition);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
[isDragging, scale, getClampedPosition, getContentElement, getPinchPosition, config.minScale, config.maxScale, config.dragThresholdTouch]
|
|
368
|
+
);
|
|
369
|
+
const onImageTouchEnd = React.useCallback(() => {
|
|
370
|
+
setIsDragging(false);
|
|
371
|
+
pinchRef.current = {
|
|
372
|
+
startDist: 0,
|
|
373
|
+
initialScale: scale,
|
|
374
|
+
startX: 0,
|
|
375
|
+
startY: 0,
|
|
376
|
+
startPos: { x: position.x, y: position.y }
|
|
377
|
+
};
|
|
378
|
+
}, [scale, position]);
|
|
379
|
+
const onImageMouseDown = React.useCallback(
|
|
380
|
+
(e) => {
|
|
381
|
+
setIsTransitioning(false);
|
|
382
|
+
if (scale > config.minScale) {
|
|
383
|
+
e.preventDefault();
|
|
384
|
+
setIsDragging(true);
|
|
385
|
+
dragStartRef.current = {
|
|
386
|
+
x: e.clientX - position.x,
|
|
387
|
+
y: e.clientY - position.y,
|
|
388
|
+
hasDragged: false,
|
|
389
|
+
startX: e.clientX,
|
|
390
|
+
startY: e.clientY
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
[scale, position, config.minScale]
|
|
395
|
+
);
|
|
396
|
+
React.useCallback(
|
|
397
|
+
(targetX, targetY) => {
|
|
398
|
+
if (!config.enableSwipe) return;
|
|
399
|
+
const distanceX = Math.abs(targetX - dragStartRef.current.startX);
|
|
400
|
+
const distanceY = Math.abs(targetY - dragStartRef.current.startY);
|
|
401
|
+
if (distanceX > config.swipeThreshold && distanceX > distanceY) {
|
|
402
|
+
if (targetX < dragStartRef.current.startX) {
|
|
403
|
+
onNext?.();
|
|
404
|
+
} else {
|
|
405
|
+
onPrev?.();
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
[config.enableSwipe, config.swipeThreshold, onNext, onPrev]
|
|
410
|
+
);
|
|
411
|
+
const onImageMouseMove = React.useCallback(
|
|
412
|
+
(e) => {
|
|
413
|
+
if (isDragging && scale > config.minScale) {
|
|
414
|
+
e.preventDefault();
|
|
415
|
+
if (!dragStartRef.current.hasDragged) {
|
|
416
|
+
const moveDist = Math.hypot(
|
|
417
|
+
e.clientX - dragStartRef.current.startX,
|
|
418
|
+
e.clientY - dragStartRef.current.startY
|
|
419
|
+
);
|
|
420
|
+
if (moveDist > config.dragThresholdMouse) {
|
|
421
|
+
dragStartRef.current.hasDragged = true;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (dragStartRef.current.hasDragged) {
|
|
425
|
+
const newPosition = {
|
|
426
|
+
x: e.clientX - dragStartRef.current.x,
|
|
427
|
+
y: e.clientY - dragStartRef.current.y
|
|
428
|
+
};
|
|
429
|
+
const clampedPosition = getClampedPosition(newPosition, scale, e.currentTarget);
|
|
430
|
+
setPosition(clampedPosition);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
[isDragging, scale, getClampedPosition, config.minScale, config.dragThresholdMouse]
|
|
435
|
+
);
|
|
436
|
+
const onImageMouseUp = React.useCallback(() => {
|
|
437
|
+
setIsDragging(false);
|
|
438
|
+
}, []);
|
|
439
|
+
const onImageMouseLeave = React.useCallback(() => {
|
|
440
|
+
setIsDragging(false);
|
|
441
|
+
}, []);
|
|
442
|
+
const onContainerTouchStart = React.useCallback(
|
|
443
|
+
(e) => {
|
|
444
|
+
if (scale > config.minScale) return;
|
|
445
|
+
const touch = e.changedTouches[0];
|
|
446
|
+
touchStartXRef.current = touch.clientX;
|
|
447
|
+
},
|
|
448
|
+
[scale, config.minScale]
|
|
449
|
+
);
|
|
450
|
+
const onContainerTouchEnd = React.useCallback(
|
|
451
|
+
(e) => {
|
|
452
|
+
if (swipeBlockedRef.current) {
|
|
453
|
+
swipeBlockedRef.current = false;
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (scale > config.minScale) return;
|
|
457
|
+
const touch = e.changedTouches[0];
|
|
458
|
+
const startX = touchStartXRef.current;
|
|
459
|
+
const endX = touch.clientX;
|
|
460
|
+
const diff = startX - endX;
|
|
461
|
+
if (Math.abs(diff) > config.swipeThreshold) {
|
|
462
|
+
if (diff > 0) {
|
|
463
|
+
onNext?.();
|
|
464
|
+
} else {
|
|
465
|
+
onPrev?.();
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
[scale, onNext, onPrev, config.minScale, config.swipeThreshold]
|
|
470
|
+
);
|
|
471
|
+
const onContainerMouseDown = React.useCallback(
|
|
472
|
+
(e) => {
|
|
473
|
+
if (scale > config.minScale) return;
|
|
474
|
+
touchStartXRef.current = e.clientX;
|
|
475
|
+
},
|
|
476
|
+
[scale, config.minScale]
|
|
477
|
+
);
|
|
478
|
+
const onContainerMouseUp = React.useCallback(
|
|
479
|
+
(e) => {
|
|
480
|
+
if (scale > config.minScale) return;
|
|
481
|
+
const startX = touchStartXRef.current;
|
|
482
|
+
const endX = e.clientX;
|
|
483
|
+
const diff = startX - endX;
|
|
484
|
+
if (Math.abs(diff) > config.swipeThreshold) {
|
|
485
|
+
if (diff > 0) {
|
|
486
|
+
onNext?.();
|
|
487
|
+
} else {
|
|
488
|
+
onPrev?.();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
[scale, onNext, onPrev, config.minScale, config.swipeThreshold]
|
|
493
|
+
);
|
|
494
|
+
const zoomTo = React.useCallback(
|
|
495
|
+
(x, y, targetScale) => {
|
|
496
|
+
setIsTransitioning(true);
|
|
497
|
+
const container = containerRef.current;
|
|
498
|
+
const content = contentRef.current || container?.firstElementChild;
|
|
499
|
+
if (!container || !content) return;
|
|
500
|
+
const scaleToUse = targetScale ?? config.clickZoomScale;
|
|
501
|
+
const contentWidth = content.offsetWidth;
|
|
502
|
+
const contentHeight = content.offsetHeight;
|
|
503
|
+
const newPosition = {
|
|
504
|
+
x: (contentWidth / 2 - x) * scaleToUse,
|
|
505
|
+
y: (contentHeight / 2 - y) * scaleToUse
|
|
506
|
+
};
|
|
507
|
+
const clampedPosition = getClampedPosition(newPosition, scaleToUse, content);
|
|
508
|
+
setScale(scaleToUse);
|
|
509
|
+
setPosition(clampedPosition);
|
|
510
|
+
},
|
|
511
|
+
[getClampedPosition, config.clickZoomScale, containerRef]
|
|
512
|
+
);
|
|
513
|
+
React.useEffect(() => {
|
|
514
|
+
if (!isTransitioning) return;
|
|
515
|
+
const timer = setTimeout(() => setIsTransitioning(false), TRANSITION_DURATION);
|
|
516
|
+
return () => clearTimeout(timer);
|
|
517
|
+
}, [isTransitioning]);
|
|
518
|
+
const contentStyle = React__default.default.useMemo(() => {
|
|
519
|
+
const style = {
|
|
520
|
+
transformOrigin: "center",
|
|
521
|
+
transition: isTransitioning ? `transform ${TRANSITION_DURATION}ms ${TRANSITION_CURVE}` : "none"
|
|
522
|
+
};
|
|
523
|
+
if (config.manageCursor) {
|
|
524
|
+
if (isDragging) {
|
|
525
|
+
style.cursor = "grabbing";
|
|
526
|
+
} else if (scale > config.minScale) {
|
|
527
|
+
style.cursor = "grab";
|
|
528
|
+
} else if (enableZoom) {
|
|
529
|
+
style.cursor = "zoom-in";
|
|
530
|
+
} else {
|
|
531
|
+
style.cursor = "default";
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return style;
|
|
535
|
+
}, [isTransitioning, config.manageCursor, isDragging, scale, config.minScale, enableZoom]);
|
|
536
|
+
const contentProps = React__default.default.useMemo(() => ({
|
|
537
|
+
ref: contentRef,
|
|
538
|
+
// Cast to compatible ref type
|
|
539
|
+
style: contentStyle,
|
|
540
|
+
onClick: onImageClick,
|
|
541
|
+
onDoubleClick: onImageDoubleClick,
|
|
542
|
+
onTouchStart: onImageTouchStart,
|
|
543
|
+
onTouchMove: onImageTouchMove,
|
|
544
|
+
onTouchEnd: onImageTouchEnd,
|
|
545
|
+
onMouseDown: onImageMouseDown,
|
|
546
|
+
onMouseMove: onImageMouseMove,
|
|
547
|
+
onMouseUp: onImageMouseUp,
|
|
548
|
+
onMouseLeave: onImageMouseLeave
|
|
549
|
+
}), [
|
|
550
|
+
contentStyle,
|
|
551
|
+
onImageClick,
|
|
552
|
+
onImageDoubleClick,
|
|
553
|
+
onImageTouchStart,
|
|
554
|
+
onImageTouchMove,
|
|
555
|
+
onImageTouchEnd,
|
|
556
|
+
onImageMouseDown,
|
|
557
|
+
onImageMouseMove,
|
|
558
|
+
onImageMouseUp,
|
|
559
|
+
onImageMouseLeave
|
|
560
|
+
]);
|
|
561
|
+
const containerProps = React__default.default.useMemo(() => ({
|
|
562
|
+
onTouchStart: onContainerTouchStart,
|
|
563
|
+
onTouchEnd: onContainerTouchEnd,
|
|
564
|
+
onMouseDown: onContainerMouseDown,
|
|
565
|
+
onMouseUp: onContainerMouseUp
|
|
566
|
+
}), [
|
|
567
|
+
onContainerTouchStart,
|
|
568
|
+
onContainerTouchEnd,
|
|
569
|
+
onContainerMouseDown,
|
|
570
|
+
onContainerMouseUp
|
|
571
|
+
]);
|
|
572
|
+
return {
|
|
573
|
+
scale,
|
|
574
|
+
position,
|
|
575
|
+
isDragging,
|
|
576
|
+
reset,
|
|
577
|
+
zoomTo,
|
|
578
|
+
contentProps,
|
|
579
|
+
containerProps
|
|
580
|
+
};
|
|
581
|
+
};
|
|
582
|
+
var ZoomPan = ({
|
|
583
|
+
children,
|
|
584
|
+
className = "",
|
|
585
|
+
style = {},
|
|
586
|
+
contentClassName = "",
|
|
587
|
+
contentStyle = {},
|
|
588
|
+
enableZoom = true,
|
|
589
|
+
onNext,
|
|
590
|
+
onPrev,
|
|
591
|
+
options
|
|
592
|
+
}) => {
|
|
593
|
+
const containerRef = React.useRef(null);
|
|
594
|
+
const { scale, position, contentProps, containerProps } = useZoomPan({
|
|
595
|
+
containerRef,
|
|
596
|
+
enableZoom,
|
|
597
|
+
onNext,
|
|
598
|
+
onPrev,
|
|
599
|
+
options
|
|
600
|
+
});
|
|
601
|
+
const defaultContainerStyle = {
|
|
602
|
+
width: "100%",
|
|
603
|
+
height: "100%",
|
|
604
|
+
overflow: "hidden",
|
|
605
|
+
display: "flex",
|
|
606
|
+
alignItems: "center",
|
|
607
|
+
justifyContent: "center",
|
|
608
|
+
cursor: scale > 1 ? "grab" : enableZoom ? "zoom-in" : "default",
|
|
609
|
+
position: "relative",
|
|
610
|
+
...style
|
|
611
|
+
};
|
|
612
|
+
const defaultContentStyle = {
|
|
613
|
+
transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`,
|
|
614
|
+
...contentProps.style,
|
|
615
|
+
userSelect: "none",
|
|
616
|
+
WebkitUserSelect: "none",
|
|
617
|
+
touchAction: "none",
|
|
618
|
+
maxWidth: "100%",
|
|
619
|
+
maxHeight: "100%",
|
|
620
|
+
...contentStyle
|
|
621
|
+
};
|
|
622
|
+
return /* @__PURE__ */ React__default.default.createElement(
|
|
623
|
+
"div",
|
|
624
|
+
{
|
|
625
|
+
ref: containerRef,
|
|
626
|
+
className,
|
|
627
|
+
style: defaultContainerStyle,
|
|
628
|
+
...containerProps
|
|
629
|
+
},
|
|
630
|
+
/* @__PURE__ */ React__default.default.createElement(
|
|
631
|
+
"div",
|
|
632
|
+
{
|
|
633
|
+
className: contentClassName,
|
|
634
|
+
...contentProps,
|
|
635
|
+
style: {
|
|
636
|
+
...defaultContentStyle,
|
|
637
|
+
...contentProps.style
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
children
|
|
641
|
+
)
|
|
642
|
+
);
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
exports.ZoomPan = ZoomPan;
|
|
646
|
+
exports.useZoomPan = useZoomPan;
|
|
647
|
+
//# sourceMappingURL=index.js.map
|
|
648
|
+
//# sourceMappingURL=index.js.map
|