@webority-technologies/mobile-ui 0.0.4 → 0.0.5
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/lib/commonjs/components/ZoomableImage/ZoomableImage.js +204 -0
- package/lib/commonjs/components/ZoomableImage/index.js +13 -0
- package/lib/commonjs/components/index.js +7 -0
- package/lib/module/components/ZoomableImage/ZoomableImage.js +198 -0
- package/lib/module/components/ZoomableImage/index.js +4 -0
- package/lib/module/components/index.js +1 -0
- package/lib/typescript/commonjs/components/ZoomableImage/ZoomableImage.d.ts +47 -0
- package/lib/typescript/commonjs/components/ZoomableImage/index.d.ts +3 -0
- package/lib/typescript/commonjs/components/index.d.ts +2 -0
- package/lib/typescript/module/components/ZoomableImage/ZoomableImage.d.ts +47 -0
- package/lib/typescript/module/components/ZoomableImage/index.d.ts +3 -0
- package/lib/typescript/module/components/index.d.ts +2 -0
- package/package.json +2 -2
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = exports.ZoomableImage = void 0;
|
|
7
|
+
var _react = _interopRequireWildcard(require("react"));
|
|
8
|
+
var _reactNative = require("react-native");
|
|
9
|
+
var _reactNativeGestureHandler = require("react-native-gesture-handler");
|
|
10
|
+
var _reactNativeReanimated = _interopRequireWildcard(require("react-native-reanimated"));
|
|
11
|
+
var _index = require("../../utils/index.js");
|
|
12
|
+
var _jsxRuntime = require("react/jsx-runtime");
|
|
13
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
14
|
+
/**
|
|
15
|
+
* ZoomableImage — a standalone pinch-to-zoom / pan / double-tap-to-zoom
|
|
16
|
+
* wrapper for a single piece of content (typically one `<Image>`).
|
|
17
|
+
*
|
|
18
|
+
* Reuses the same Reanimated 3 gesture composition `ImageGallery`'s internal
|
|
19
|
+
* lightbox slide uses (pinch clamped and rubber-banded at its limits, pan
|
|
20
|
+
* gated to only engage once zoomed in, double-tap toggling between baseline
|
|
21
|
+
* and `doubleTapScale`), stripped of the carousel-slide concerns that
|
|
22
|
+
* component needs and this one does not — there is no `index`/`total`/
|
|
23
|
+
* `active` because a standalone image is always the one being viewed.
|
|
24
|
+
*
|
|
25
|
+
* USAGE:
|
|
26
|
+
*
|
|
27
|
+
* <ZoomableImage>
|
|
28
|
+
* <Image source={{ uri }} style={styles.image} resizeMode="contain" />
|
|
29
|
+
* </ZoomableImage>
|
|
30
|
+
*
|
|
31
|
+
* Takes `children` rather than a `source`/`uri` prop: every real consumer of
|
|
32
|
+
* this pattern in the fleet wraps an already-authenticated image component
|
|
33
|
+
* (headers, caching, its own loading state) rather than a bare `<Image>`, so
|
|
34
|
+
* a `source` prop would force re-implementing that per app. Wrapping keeps
|
|
35
|
+
* this component ignorant of how the image is fetched.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const DEFAULT_MAX_SCALE = 4;
|
|
39
|
+
const DEFAULT_MIN_SCALE = 1;
|
|
40
|
+
const DEFAULT_DOUBLE_TAP_SCALE = 2;
|
|
41
|
+
const SPRING_CONFIG = {
|
|
42
|
+
damping: 18,
|
|
43
|
+
stiffness: 220,
|
|
44
|
+
mass: 1
|
|
45
|
+
};
|
|
46
|
+
const ZoomableImage = ({
|
|
47
|
+
children,
|
|
48
|
+
maxScale = DEFAULT_MAX_SCALE,
|
|
49
|
+
minScale = DEFAULT_MIN_SCALE,
|
|
50
|
+
doubleTapScale = DEFAULT_DOUBLE_TAP_SCALE,
|
|
51
|
+
disabled = false,
|
|
52
|
+
haptic,
|
|
53
|
+
style,
|
|
54
|
+
accessibilityLabel,
|
|
55
|
+
testID
|
|
56
|
+
}) => {
|
|
57
|
+
const scale = (0, _reactNativeReanimated.useSharedValue)(1);
|
|
58
|
+
const savedScale = (0, _reactNativeReanimated.useSharedValue)(1);
|
|
59
|
+
const translateX = (0, _reactNativeReanimated.useSharedValue)(0);
|
|
60
|
+
const translateY = (0, _reactNativeReanimated.useSharedValue)(0);
|
|
61
|
+
const savedTranslateX = (0, _reactNativeReanimated.useSharedValue)(0);
|
|
62
|
+
const savedTranslateY = (0, _reactNativeReanimated.useSharedValue)(0);
|
|
63
|
+
const resetTransform = (0, _react.useCallback)(() => {
|
|
64
|
+
scale.value = (0, _reactNativeReanimated.withSpring)(1, SPRING_CONFIG);
|
|
65
|
+
savedScale.value = 1;
|
|
66
|
+
translateX.value = (0, _reactNativeReanimated.withSpring)(0, SPRING_CONFIG);
|
|
67
|
+
translateY.value = (0, _reactNativeReanimated.withSpring)(0, SPRING_CONFIG);
|
|
68
|
+
savedTranslateX.value = 0;
|
|
69
|
+
savedTranslateY.value = 0;
|
|
70
|
+
}, [scale, savedScale, translateX, translateY, savedTranslateX, savedTranslateY]);
|
|
71
|
+
|
|
72
|
+
// Disabling mid-zoom must not leave the content stranded scaled/panned —
|
|
73
|
+
// snap it back the same way ImageGallery's slide resets on losing focus.
|
|
74
|
+
(0, _react.useEffect)(() => {
|
|
75
|
+
if (disabled) {
|
|
76
|
+
resetTransform();
|
|
77
|
+
}
|
|
78
|
+
}, [disabled, resetTransform]);
|
|
79
|
+
const triggerImpact = (0, _react.useCallback)(() => {
|
|
80
|
+
const h = (0, _index.resolveHaptic)(haptic, 'impactLight');
|
|
81
|
+
if (h) {
|
|
82
|
+
(0, _index.triggerHaptic)(h);
|
|
83
|
+
}
|
|
84
|
+
}, [haptic]);
|
|
85
|
+
const pinch = (0, _react.useMemo)(() => _reactNativeGestureHandler.Gesture.Pinch().enabled(!disabled).onStart(() => {
|
|
86
|
+
'worklet';
|
|
87
|
+
|
|
88
|
+
savedScale.value = scale.value;
|
|
89
|
+
}).onUpdate(e => {
|
|
90
|
+
'worklet';
|
|
91
|
+
|
|
92
|
+
const next = savedScale.value * e.scale;
|
|
93
|
+
// Clamp minScale..maxScale on the UI thread so pinch feels rubbery at limits.
|
|
94
|
+
if (next < minScale) {
|
|
95
|
+
scale.value = minScale + (next - minScale) * 0.3;
|
|
96
|
+
} else if (next > maxScale) {
|
|
97
|
+
scale.value = maxScale + (next - maxScale) * 0.2;
|
|
98
|
+
} else {
|
|
99
|
+
scale.value = next;
|
|
100
|
+
}
|
|
101
|
+
}).onEnd(() => {
|
|
102
|
+
'worklet';
|
|
103
|
+
|
|
104
|
+
if (scale.value < minScale) {
|
|
105
|
+
scale.value = (0, _reactNativeReanimated.withSpring)(minScale, SPRING_CONFIG);
|
|
106
|
+
translateX.value = (0, _reactNativeReanimated.withSpring)(0, SPRING_CONFIG);
|
|
107
|
+
translateY.value = (0, _reactNativeReanimated.withSpring)(0, SPRING_CONFIG);
|
|
108
|
+
savedScale.value = minScale;
|
|
109
|
+
savedTranslateX.value = 0;
|
|
110
|
+
savedTranslateY.value = 0;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (scale.value > maxScale) {
|
|
114
|
+
scale.value = (0, _reactNativeReanimated.withSpring)(maxScale, SPRING_CONFIG);
|
|
115
|
+
savedScale.value = maxScale;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
savedScale.value = scale.value;
|
|
119
|
+
}), [disabled, scale, savedScale, translateX, translateY, savedTranslateX, savedTranslateY, minScale, maxScale]);
|
|
120
|
+
|
|
121
|
+
// Pan only engages when zoomed in — otherwise a scroll view or carousel this
|
|
122
|
+
// is nested inside keeps owning the gesture.
|
|
123
|
+
const pan = (0, _react.useMemo)(() => _reactNativeGestureHandler.Gesture.Pan().enabled(!disabled).minPointers(1).maxPointers(1).onStart(() => {
|
|
124
|
+
'worklet';
|
|
125
|
+
|
|
126
|
+
savedTranslateX.value = translateX.value;
|
|
127
|
+
savedTranslateY.value = translateY.value;
|
|
128
|
+
}).onUpdate(e => {
|
|
129
|
+
'worklet';
|
|
130
|
+
|
|
131
|
+
if (scale.value <= 1) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
translateX.value = savedTranslateX.value + e.translationX;
|
|
135
|
+
translateY.value = savedTranslateY.value + e.translationY;
|
|
136
|
+
}).onEnd(() => {
|
|
137
|
+
'worklet';
|
|
138
|
+
|
|
139
|
+
savedTranslateX.value = translateX.value;
|
|
140
|
+
savedTranslateY.value = translateY.value;
|
|
141
|
+
}), [disabled, scale, translateX, translateY, savedTranslateX, savedTranslateY]);
|
|
142
|
+
const doubleTap = (0, _react.useMemo)(() => _reactNativeGestureHandler.Gesture.Tap().enabled(!disabled).numberOfTaps(2).onEnd(() => {
|
|
143
|
+
'worklet';
|
|
144
|
+
|
|
145
|
+
(0, _reactNativeReanimated.runOnJS)(triggerImpact)();
|
|
146
|
+
if (scale.value > 1) {
|
|
147
|
+
scale.value = (0, _reactNativeReanimated.withSpring)(1, SPRING_CONFIG);
|
|
148
|
+
translateX.value = (0, _reactNativeReanimated.withSpring)(0, SPRING_CONFIG);
|
|
149
|
+
translateY.value = (0, _reactNativeReanimated.withSpring)(0, SPRING_CONFIG);
|
|
150
|
+
savedScale.value = 1;
|
|
151
|
+
savedTranslateX.value = 0;
|
|
152
|
+
savedTranslateY.value = 0;
|
|
153
|
+
} else {
|
|
154
|
+
scale.value = (0, _reactNativeReanimated.withSpring)(doubleTapScale, SPRING_CONFIG);
|
|
155
|
+
savedScale.value = doubleTapScale;
|
|
156
|
+
}
|
|
157
|
+
}), [disabled, scale, savedScale, translateX, translateY, savedTranslateX, savedTranslateY, triggerImpact, doubleTapScale]);
|
|
158
|
+
|
|
159
|
+
// Race the gestures so they coordinate cleanly: a tap won't be misread as a
|
|
160
|
+
// tiny pan, and pan won't fire when the user actually starts a pinch.
|
|
161
|
+
const composed = (0, _react.useMemo)(() => _reactNativeGestureHandler.Gesture.Race(pinch, pan, doubleTap), [pinch, pan, doubleTap]);
|
|
162
|
+
const animatedStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => ({
|
|
163
|
+
transform: [{
|
|
164
|
+
translateX: translateX.value
|
|
165
|
+
}, {
|
|
166
|
+
translateY: translateY.value
|
|
167
|
+
}, {
|
|
168
|
+
scale: scale.value
|
|
169
|
+
}]
|
|
170
|
+
}));
|
|
171
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeGestureHandler.GestureDetector, {
|
|
172
|
+
gesture: composed,
|
|
173
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
|
|
174
|
+
style: [styles.container, style],
|
|
175
|
+
testID: testID,
|
|
176
|
+
accessibilityLabel: accessibilityLabel,
|
|
177
|
+
accessibilityState: {
|
|
178
|
+
disabled
|
|
179
|
+
},
|
|
180
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
|
|
181
|
+
style: [styles.content, animatedStyle],
|
|
182
|
+
children: children
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
exports.ZoomableImage = ZoomableImage;
|
|
188
|
+
ZoomableImage.displayName = 'ZoomableImage';
|
|
189
|
+
const styles = _reactNative.StyleSheet.create({
|
|
190
|
+
container: {
|
|
191
|
+
flex: 1,
|
|
192
|
+
alignItems: 'center',
|
|
193
|
+
justifyContent: 'center',
|
|
194
|
+
overflow: 'hidden'
|
|
195
|
+
},
|
|
196
|
+
content: {
|
|
197
|
+
width: '100%',
|
|
198
|
+
height: '100%',
|
|
199
|
+
alignItems: 'center',
|
|
200
|
+
justifyContent: 'center'
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
var _default = exports.default = ZoomableImage;
|
|
204
|
+
//# sourceMappingURL=ZoomableImage.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
Object.defineProperty(exports, "ZoomableImage", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: function () {
|
|
9
|
+
return _ZoomableImage.ZoomableImage;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
var _ZoomableImage = require("./ZoomableImage.js");
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -513,6 +513,12 @@ Object.defineProperty(exports, "Tooltip", {
|
|
|
513
513
|
return _index65.Tooltip;
|
|
514
514
|
}
|
|
515
515
|
});
|
|
516
|
+
Object.defineProperty(exports, "ZoomableImage", {
|
|
517
|
+
enumerable: true,
|
|
518
|
+
get: function () {
|
|
519
|
+
return _index66.ZoomableImage;
|
|
520
|
+
}
|
|
521
|
+
});
|
|
516
522
|
Object.defineProperty(exports, "applyMarkdownAction", {
|
|
517
523
|
enumerable: true,
|
|
518
524
|
get: function () {
|
|
@@ -682,4 +688,5 @@ var _index62 = require("./Timeline/index.js");
|
|
|
682
688
|
var _index63 = require("./TimePicker/index.js");
|
|
683
689
|
var _index64 = require("./Toast/index.js");
|
|
684
690
|
var _index65 = require("./Tooltip/index.js");
|
|
691
|
+
var _index66 = require("./ZoomableImage/index.js");
|
|
685
692
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ZoomableImage — a standalone pinch-to-zoom / pan / double-tap-to-zoom
|
|
5
|
+
* wrapper for a single piece of content (typically one `<Image>`).
|
|
6
|
+
*
|
|
7
|
+
* Reuses the same Reanimated 3 gesture composition `ImageGallery`'s internal
|
|
8
|
+
* lightbox slide uses (pinch clamped and rubber-banded at its limits, pan
|
|
9
|
+
* gated to only engage once zoomed in, double-tap toggling between baseline
|
|
10
|
+
* and `doubleTapScale`), stripped of the carousel-slide concerns that
|
|
11
|
+
* component needs and this one does not — there is no `index`/`total`/
|
|
12
|
+
* `active` because a standalone image is always the one being viewed.
|
|
13
|
+
*
|
|
14
|
+
* USAGE:
|
|
15
|
+
*
|
|
16
|
+
* <ZoomableImage>
|
|
17
|
+
* <Image source={{ uri }} style={styles.image} resizeMode="contain" />
|
|
18
|
+
* </ZoomableImage>
|
|
19
|
+
*
|
|
20
|
+
* Takes `children` rather than a `source`/`uri` prop: every real consumer of
|
|
21
|
+
* this pattern in the fleet wraps an already-authenticated image component
|
|
22
|
+
* (headers, caching, its own loading state) rather than a bare `<Image>`, so
|
|
23
|
+
* a `source` prop would force re-implementing that per app. Wrapping keeps
|
|
24
|
+
* this component ignorant of how the image is fetched.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import React, { useCallback, useEffect, useMemo } from 'react';
|
|
28
|
+
import { StyleSheet } from 'react-native';
|
|
29
|
+
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
30
|
+
import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
|
|
31
|
+
import { resolveHaptic, triggerHaptic } from "../../utils/index.js";
|
|
32
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
33
|
+
const DEFAULT_MAX_SCALE = 4;
|
|
34
|
+
const DEFAULT_MIN_SCALE = 1;
|
|
35
|
+
const DEFAULT_DOUBLE_TAP_SCALE = 2;
|
|
36
|
+
const SPRING_CONFIG = {
|
|
37
|
+
damping: 18,
|
|
38
|
+
stiffness: 220,
|
|
39
|
+
mass: 1
|
|
40
|
+
};
|
|
41
|
+
export const ZoomableImage = ({
|
|
42
|
+
children,
|
|
43
|
+
maxScale = DEFAULT_MAX_SCALE,
|
|
44
|
+
minScale = DEFAULT_MIN_SCALE,
|
|
45
|
+
doubleTapScale = DEFAULT_DOUBLE_TAP_SCALE,
|
|
46
|
+
disabled = false,
|
|
47
|
+
haptic,
|
|
48
|
+
style,
|
|
49
|
+
accessibilityLabel,
|
|
50
|
+
testID
|
|
51
|
+
}) => {
|
|
52
|
+
const scale = useSharedValue(1);
|
|
53
|
+
const savedScale = useSharedValue(1);
|
|
54
|
+
const translateX = useSharedValue(0);
|
|
55
|
+
const translateY = useSharedValue(0);
|
|
56
|
+
const savedTranslateX = useSharedValue(0);
|
|
57
|
+
const savedTranslateY = useSharedValue(0);
|
|
58
|
+
const resetTransform = useCallback(() => {
|
|
59
|
+
scale.value = withSpring(1, SPRING_CONFIG);
|
|
60
|
+
savedScale.value = 1;
|
|
61
|
+
translateX.value = withSpring(0, SPRING_CONFIG);
|
|
62
|
+
translateY.value = withSpring(0, SPRING_CONFIG);
|
|
63
|
+
savedTranslateX.value = 0;
|
|
64
|
+
savedTranslateY.value = 0;
|
|
65
|
+
}, [scale, savedScale, translateX, translateY, savedTranslateX, savedTranslateY]);
|
|
66
|
+
|
|
67
|
+
// Disabling mid-zoom must not leave the content stranded scaled/panned —
|
|
68
|
+
// snap it back the same way ImageGallery's slide resets on losing focus.
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (disabled) {
|
|
71
|
+
resetTransform();
|
|
72
|
+
}
|
|
73
|
+
}, [disabled, resetTransform]);
|
|
74
|
+
const triggerImpact = useCallback(() => {
|
|
75
|
+
const h = resolveHaptic(haptic, 'impactLight');
|
|
76
|
+
if (h) {
|
|
77
|
+
triggerHaptic(h);
|
|
78
|
+
}
|
|
79
|
+
}, [haptic]);
|
|
80
|
+
const pinch = useMemo(() => Gesture.Pinch().enabled(!disabled).onStart(() => {
|
|
81
|
+
'worklet';
|
|
82
|
+
|
|
83
|
+
savedScale.value = scale.value;
|
|
84
|
+
}).onUpdate(e => {
|
|
85
|
+
'worklet';
|
|
86
|
+
|
|
87
|
+
const next = savedScale.value * e.scale;
|
|
88
|
+
// Clamp minScale..maxScale on the UI thread so pinch feels rubbery at limits.
|
|
89
|
+
if (next < minScale) {
|
|
90
|
+
scale.value = minScale + (next - minScale) * 0.3;
|
|
91
|
+
} else if (next > maxScale) {
|
|
92
|
+
scale.value = maxScale + (next - maxScale) * 0.2;
|
|
93
|
+
} else {
|
|
94
|
+
scale.value = next;
|
|
95
|
+
}
|
|
96
|
+
}).onEnd(() => {
|
|
97
|
+
'worklet';
|
|
98
|
+
|
|
99
|
+
if (scale.value < minScale) {
|
|
100
|
+
scale.value = withSpring(minScale, SPRING_CONFIG);
|
|
101
|
+
translateX.value = withSpring(0, SPRING_CONFIG);
|
|
102
|
+
translateY.value = withSpring(0, SPRING_CONFIG);
|
|
103
|
+
savedScale.value = minScale;
|
|
104
|
+
savedTranslateX.value = 0;
|
|
105
|
+
savedTranslateY.value = 0;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (scale.value > maxScale) {
|
|
109
|
+
scale.value = withSpring(maxScale, SPRING_CONFIG);
|
|
110
|
+
savedScale.value = maxScale;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
savedScale.value = scale.value;
|
|
114
|
+
}), [disabled, scale, savedScale, translateX, translateY, savedTranslateX, savedTranslateY, minScale, maxScale]);
|
|
115
|
+
|
|
116
|
+
// Pan only engages when zoomed in — otherwise a scroll view or carousel this
|
|
117
|
+
// is nested inside keeps owning the gesture.
|
|
118
|
+
const pan = useMemo(() => Gesture.Pan().enabled(!disabled).minPointers(1).maxPointers(1).onStart(() => {
|
|
119
|
+
'worklet';
|
|
120
|
+
|
|
121
|
+
savedTranslateX.value = translateX.value;
|
|
122
|
+
savedTranslateY.value = translateY.value;
|
|
123
|
+
}).onUpdate(e => {
|
|
124
|
+
'worklet';
|
|
125
|
+
|
|
126
|
+
if (scale.value <= 1) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
translateX.value = savedTranslateX.value + e.translationX;
|
|
130
|
+
translateY.value = savedTranslateY.value + e.translationY;
|
|
131
|
+
}).onEnd(() => {
|
|
132
|
+
'worklet';
|
|
133
|
+
|
|
134
|
+
savedTranslateX.value = translateX.value;
|
|
135
|
+
savedTranslateY.value = translateY.value;
|
|
136
|
+
}), [disabled, scale, translateX, translateY, savedTranslateX, savedTranslateY]);
|
|
137
|
+
const doubleTap = useMemo(() => Gesture.Tap().enabled(!disabled).numberOfTaps(2).onEnd(() => {
|
|
138
|
+
'worklet';
|
|
139
|
+
|
|
140
|
+
runOnJS(triggerImpact)();
|
|
141
|
+
if (scale.value > 1) {
|
|
142
|
+
scale.value = withSpring(1, SPRING_CONFIG);
|
|
143
|
+
translateX.value = withSpring(0, SPRING_CONFIG);
|
|
144
|
+
translateY.value = withSpring(0, SPRING_CONFIG);
|
|
145
|
+
savedScale.value = 1;
|
|
146
|
+
savedTranslateX.value = 0;
|
|
147
|
+
savedTranslateY.value = 0;
|
|
148
|
+
} else {
|
|
149
|
+
scale.value = withSpring(doubleTapScale, SPRING_CONFIG);
|
|
150
|
+
savedScale.value = doubleTapScale;
|
|
151
|
+
}
|
|
152
|
+
}), [disabled, scale, savedScale, translateX, translateY, savedTranslateX, savedTranslateY, triggerImpact, doubleTapScale]);
|
|
153
|
+
|
|
154
|
+
// Race the gestures so they coordinate cleanly: a tap won't be misread as a
|
|
155
|
+
// tiny pan, and pan won't fire when the user actually starts a pinch.
|
|
156
|
+
const composed = useMemo(() => Gesture.Race(pinch, pan, doubleTap), [pinch, pan, doubleTap]);
|
|
157
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
158
|
+
transform: [{
|
|
159
|
+
translateX: translateX.value
|
|
160
|
+
}, {
|
|
161
|
+
translateY: translateY.value
|
|
162
|
+
}, {
|
|
163
|
+
scale: scale.value
|
|
164
|
+
}]
|
|
165
|
+
}));
|
|
166
|
+
return /*#__PURE__*/_jsx(GestureDetector, {
|
|
167
|
+
gesture: composed,
|
|
168
|
+
children: /*#__PURE__*/_jsx(Animated.View, {
|
|
169
|
+
style: [styles.container, style],
|
|
170
|
+
testID: testID,
|
|
171
|
+
accessibilityLabel: accessibilityLabel,
|
|
172
|
+
accessibilityState: {
|
|
173
|
+
disabled
|
|
174
|
+
},
|
|
175
|
+
children: /*#__PURE__*/_jsx(Animated.View, {
|
|
176
|
+
style: [styles.content, animatedStyle],
|
|
177
|
+
children: children
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
ZoomableImage.displayName = 'ZoomableImage';
|
|
183
|
+
const styles = StyleSheet.create({
|
|
184
|
+
container: {
|
|
185
|
+
flex: 1,
|
|
186
|
+
alignItems: 'center',
|
|
187
|
+
justifyContent: 'center',
|
|
188
|
+
overflow: 'hidden'
|
|
189
|
+
},
|
|
190
|
+
content: {
|
|
191
|
+
width: '100%',
|
|
192
|
+
height: '100%',
|
|
193
|
+
alignItems: 'center',
|
|
194
|
+
justifyContent: 'center'
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
export default ZoomableImage;
|
|
198
|
+
//# sourceMappingURL=ZoomableImage.js.map
|
|
@@ -67,4 +67,5 @@ export { Timeline } from "./Timeline/index.js";
|
|
|
67
67
|
export { TimePicker } from "./TimePicker/index.js";
|
|
68
68
|
export { Toast, ToastProvider, toast, useToast } from "./Toast/index.js";
|
|
69
69
|
export { Tooltip } from "./Tooltip/index.js";
|
|
70
|
+
export { ZoomableImage } from "./ZoomableImage/index.js";
|
|
70
71
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZoomableImage — a standalone pinch-to-zoom / pan / double-tap-to-zoom
|
|
3
|
+
* wrapper for a single piece of content (typically one `<Image>`).
|
|
4
|
+
*
|
|
5
|
+
* Reuses the same Reanimated 3 gesture composition `ImageGallery`'s internal
|
|
6
|
+
* lightbox slide uses (pinch clamped and rubber-banded at its limits, pan
|
|
7
|
+
* gated to only engage once zoomed in, double-tap toggling between baseline
|
|
8
|
+
* and `doubleTapScale`), stripped of the carousel-slide concerns that
|
|
9
|
+
* component needs and this one does not — there is no `index`/`total`/
|
|
10
|
+
* `active` because a standalone image is always the one being viewed.
|
|
11
|
+
*
|
|
12
|
+
* USAGE:
|
|
13
|
+
*
|
|
14
|
+
* <ZoomableImage>
|
|
15
|
+
* <Image source={{ uri }} style={styles.image} resizeMode="contain" />
|
|
16
|
+
* </ZoomableImage>
|
|
17
|
+
*
|
|
18
|
+
* Takes `children` rather than a `source`/`uri` prop: every real consumer of
|
|
19
|
+
* this pattern in the fleet wraps an already-authenticated image component
|
|
20
|
+
* (headers, caching, its own loading state) rather than a bare `<Image>`, so
|
|
21
|
+
* a `source` prop would force re-implementing that per app. Wrapping keeps
|
|
22
|
+
* this component ignorant of how the image is fetched.
|
|
23
|
+
*/
|
|
24
|
+
import React from 'react';
|
|
25
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
26
|
+
import type { HapticType } from '../../utils';
|
|
27
|
+
export interface ZoomableImageProps {
|
|
28
|
+
/** The content to pinch-zoom, pan, and double-tap-zoom — typically a single `<Image>`. */
|
|
29
|
+
children: React.ReactNode;
|
|
30
|
+
/** Upper bound on pinch and double-tap zoom. Default 4. */
|
|
31
|
+
maxScale?: number;
|
|
32
|
+
/** Lower bound on pinch zoom — the resting scale a pinch springs back to. Default 1. */
|
|
33
|
+
minScale?: number;
|
|
34
|
+
/** Scale a double tap zooms to; a second double tap returns to baseline. Default 2. */
|
|
35
|
+
doubleTapScale?: number;
|
|
36
|
+
/** Blocks pinch/pan/double-tap and smoothly resets any active zoom back to baseline. Default false. */
|
|
37
|
+
disabled?: boolean;
|
|
38
|
+
/** Haptic feedback on double-tap-to-zoom. Pass false to disable; pass a HapticType to override. */
|
|
39
|
+
haptic?: HapticType | false;
|
|
40
|
+
/** Style for the outer wrapping view. */
|
|
41
|
+
style?: StyleProp<ViewStyle>;
|
|
42
|
+
accessibilityLabel?: string;
|
|
43
|
+
testID?: string;
|
|
44
|
+
}
|
|
45
|
+
export declare const ZoomableImage: React.FC<ZoomableImageProps>;
|
|
46
|
+
export default ZoomableImage;
|
|
47
|
+
//# sourceMappingURL=ZoomableImage.d.ts.map
|
|
@@ -132,4 +132,6 @@ export type { ToastApi, ToastOptions, ToastPosition, ToastProps, ToastProviderPr
|
|
|
132
132
|
export { Toast, ToastProvider, toast, useToast } from './Toast';
|
|
133
133
|
export type { TooltipPlacement, TooltipProps, TooltipTrigger } from './Tooltip';
|
|
134
134
|
export { Tooltip } from './Tooltip';
|
|
135
|
+
export type { ZoomableImageProps } from './ZoomableImage';
|
|
136
|
+
export { ZoomableImage } from './ZoomableImage';
|
|
135
137
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZoomableImage — a standalone pinch-to-zoom / pan / double-tap-to-zoom
|
|
3
|
+
* wrapper for a single piece of content (typically one `<Image>`).
|
|
4
|
+
*
|
|
5
|
+
* Reuses the same Reanimated 3 gesture composition `ImageGallery`'s internal
|
|
6
|
+
* lightbox slide uses (pinch clamped and rubber-banded at its limits, pan
|
|
7
|
+
* gated to only engage once zoomed in, double-tap toggling between baseline
|
|
8
|
+
* and `doubleTapScale`), stripped of the carousel-slide concerns that
|
|
9
|
+
* component needs and this one does not — there is no `index`/`total`/
|
|
10
|
+
* `active` because a standalone image is always the one being viewed.
|
|
11
|
+
*
|
|
12
|
+
* USAGE:
|
|
13
|
+
*
|
|
14
|
+
* <ZoomableImage>
|
|
15
|
+
* <Image source={{ uri }} style={styles.image} resizeMode="contain" />
|
|
16
|
+
* </ZoomableImage>
|
|
17
|
+
*
|
|
18
|
+
* Takes `children` rather than a `source`/`uri` prop: every real consumer of
|
|
19
|
+
* this pattern in the fleet wraps an already-authenticated image component
|
|
20
|
+
* (headers, caching, its own loading state) rather than a bare `<Image>`, so
|
|
21
|
+
* a `source` prop would force re-implementing that per app. Wrapping keeps
|
|
22
|
+
* this component ignorant of how the image is fetched.
|
|
23
|
+
*/
|
|
24
|
+
import React from 'react';
|
|
25
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
26
|
+
import type { HapticType } from '../../utils/index.js';
|
|
27
|
+
export interface ZoomableImageProps {
|
|
28
|
+
/** The content to pinch-zoom, pan, and double-tap-zoom — typically a single `<Image>`. */
|
|
29
|
+
children: React.ReactNode;
|
|
30
|
+
/** Upper bound on pinch and double-tap zoom. Default 4. */
|
|
31
|
+
maxScale?: number;
|
|
32
|
+
/** Lower bound on pinch zoom — the resting scale a pinch springs back to. Default 1. */
|
|
33
|
+
minScale?: number;
|
|
34
|
+
/** Scale a double tap zooms to; a second double tap returns to baseline. Default 2. */
|
|
35
|
+
doubleTapScale?: number;
|
|
36
|
+
/** Blocks pinch/pan/double-tap and smoothly resets any active zoom back to baseline. Default false. */
|
|
37
|
+
disabled?: boolean;
|
|
38
|
+
/** Haptic feedback on double-tap-to-zoom. Pass false to disable; pass a HapticType to override. */
|
|
39
|
+
haptic?: HapticType | false;
|
|
40
|
+
/** Style for the outer wrapping view. */
|
|
41
|
+
style?: StyleProp<ViewStyle>;
|
|
42
|
+
accessibilityLabel?: string;
|
|
43
|
+
testID?: string;
|
|
44
|
+
}
|
|
45
|
+
export declare const ZoomableImage: React.FC<ZoomableImageProps>;
|
|
46
|
+
export default ZoomableImage;
|
|
47
|
+
//# sourceMappingURL=ZoomableImage.d.ts.map
|
|
@@ -132,4 +132,6 @@ export type { ToastApi, ToastOptions, ToastPosition, ToastProps, ToastProviderPr
|
|
|
132
132
|
export { Toast, ToastProvider, toast, useToast } from './Toast/index.js';
|
|
133
133
|
export type { TooltipPlacement, TooltipProps, TooltipTrigger } from './Tooltip/index.js';
|
|
134
134
|
export { Tooltip } from './Tooltip/index.js';
|
|
135
|
+
export type { ZoomableImageProps } from './ZoomableImage/index.js';
|
|
136
|
+
export { ZoomableImage } from './ZoomableImage/index.js';
|
|
135
137
|
//# sourceMappingURL=index.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webority-technologies/mobile-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Beautiful, animated, accessible React Native components, theme and form engine for Webority projects.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"typecheck": "tsc --noEmit"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@webority-technologies/mobile-core": "^0.0.
|
|
64
|
+
"@webority-technologies/mobile-core": "^0.0.5"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
67
|
"expo": ">=57.0.0",
|