flowboard-react 0.1.0 → 0.3.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 +3 -2
- package/lib/module/Flowboard.js +3 -2
- package/lib/module/Flowboard.js.map +1 -1
- package/lib/module/FlowboardProvider.js +18 -14
- package/lib/module/FlowboardProvider.js.map +1 -1
- package/lib/module/components/FlowboardFlow.js +70 -37
- package/lib/module/components/FlowboardFlow.js.map +1 -1
- package/lib/module/components/FlowboardRenderer.js +622 -105
- package/lib/module/components/FlowboardRenderer.js.map +1 -1
- package/lib/module/core/assetPreloader.js +20 -18
- package/lib/module/core/assetPreloader.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/types/react-native-peers.d.js +2 -0
- package/lib/module/types/react-native-peers.d.js.map +1 -0
- package/lib/module/utils/flowboardUtils.js +20 -14
- package/lib/module/utils/flowboardUtils.js.map +1 -1
- package/lib/typescript/src/Flowboard.d.ts.map +1 -1
- package/lib/typescript/src/FlowboardProvider.d.ts.map +1 -1
- package/lib/typescript/src/components/FlowboardFlow.d.ts +1 -2
- package/lib/typescript/src/components/FlowboardFlow.d.ts.map +1 -1
- package/lib/typescript/src/components/FlowboardRenderer.d.ts.map +1 -1
- package/lib/typescript/src/core/assetPreloader.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types/flowboard.d.ts +1 -0
- package/lib/typescript/src/types/flowboard.d.ts.map +1 -1
- package/lib/typescript/src/utils/flowboardUtils.d.ts +6 -0
- package/lib/typescript/src/utils/flowboardUtils.d.ts.map +1 -1
- package/package.json +17 -16
- package/src/Flowboard.ts +5 -2
- package/src/FlowboardProvider.tsx +20 -16
- package/src/components/FlowboardFlow.tsx +89 -49
- package/src/components/FlowboardRenderer.tsx +771 -98
- package/src/core/assetPreloader.ts +21 -32
- package/src/index.tsx +2 -0
- package/src/types/flowboard.ts +1 -0
- package/src/types/react-native-peers.d.ts +106 -0
- package/src/utils/flowboardUtils.ts +28 -14
|
@@ -27,6 +27,7 @@ import PagerView, {
|
|
|
27
27
|
} from 'react-native-pager-view';
|
|
28
28
|
import {
|
|
29
29
|
insetsToStyle,
|
|
30
|
+
insetsToMarginStyle,
|
|
30
31
|
parseAlignment,
|
|
31
32
|
parseColor,
|
|
32
33
|
parseCrossAlignment,
|
|
@@ -44,10 +45,13 @@ import { resolveFontAwesomeIcon, FontAwesome6 } from '../core/fontAwesome';
|
|
|
44
45
|
import { useSliderRegistry } from './widgets/sliderRegistry';
|
|
45
46
|
|
|
46
47
|
const styles = StyleSheet.create({
|
|
47
|
-
root: { flex: 1 },
|
|
48
|
+
root: { flex: 1, backgroundColor: '#ffffff' },
|
|
48
49
|
background: {
|
|
49
50
|
...StyleSheet.absoluteFillObject,
|
|
50
51
|
},
|
|
52
|
+
whiteBg: {
|
|
53
|
+
backgroundColor: '#ffffff',
|
|
54
|
+
},
|
|
51
55
|
safeArea: { flex: 1 },
|
|
52
56
|
progressWrapper: { width: '100%' },
|
|
53
57
|
});
|
|
@@ -89,12 +93,19 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
|
|
|
89
93
|
const safeArea = screenData.safeArea !== false;
|
|
90
94
|
|
|
91
95
|
const padding = parseInsets(screenData.padding);
|
|
92
|
-
const
|
|
96
|
+
const contentPaddingStyle = insetsToStyle(padding);
|
|
97
|
+
const progressPaddingStyle = {
|
|
98
|
+
paddingTop: padding.top,
|
|
99
|
+
paddingRight: padding.right,
|
|
100
|
+
paddingLeft: padding.left,
|
|
101
|
+
};
|
|
102
|
+
const rootCrossAxisAlignment =
|
|
103
|
+
screenData.crossAxisAlignment ?? screenData.crossAxis;
|
|
93
104
|
|
|
94
105
|
const content = (
|
|
95
106
|
<View style={{ flex: 1 }}>
|
|
96
107
|
{showProgress && (
|
|
97
|
-
<View style={[
|
|
108
|
+
<View style={[progressPaddingStyle, { paddingBottom: 8 }]}>
|
|
98
109
|
{renderProgressBar(
|
|
99
110
|
currentIndex,
|
|
100
111
|
totalScreens,
|
|
@@ -109,7 +120,7 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
|
|
|
109
120
|
{scrollable ? (
|
|
110
121
|
<ScrollView
|
|
111
122
|
contentContainerStyle={{
|
|
112
|
-
...
|
|
123
|
+
...contentPaddingStyle,
|
|
113
124
|
paddingTop: showProgress ? 0 : padding.top,
|
|
114
125
|
}}
|
|
115
126
|
>
|
|
@@ -117,7 +128,7 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
|
|
|
117
128
|
style={{
|
|
118
129
|
flexGrow: 1,
|
|
119
130
|
justifyContent: parseFlexAlignment(screenData.mainAxisAlignment),
|
|
120
|
-
alignItems:
|
|
131
|
+
alignItems: parseRootCrossAlignment(rootCrossAxisAlignment),
|
|
121
132
|
}}
|
|
122
133
|
>
|
|
123
134
|
{childrenData.map((child, index) =>
|
|
@@ -137,10 +148,10 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
|
|
|
137
148
|
<View
|
|
138
149
|
style={{
|
|
139
150
|
flex: 1,
|
|
140
|
-
...
|
|
151
|
+
...contentPaddingStyle,
|
|
141
152
|
paddingTop: showProgress ? 0 : padding.top,
|
|
142
153
|
justifyContent: parseFlexAlignment(screenData.mainAxisAlignment),
|
|
143
|
-
alignItems:
|
|
154
|
+
alignItems: parseRootCrossAlignment(rootCrossAxisAlignment),
|
|
144
155
|
}}
|
|
145
156
|
>
|
|
146
157
|
{childrenData.map((child, index) =>
|
|
@@ -163,7 +174,13 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
|
|
|
163
174
|
<View style={styles.root}>
|
|
164
175
|
{renderBackground(backgroundData, bgColorCode)}
|
|
165
176
|
{safeArea ? (
|
|
166
|
-
<SafeAreaView
|
|
177
|
+
<SafeAreaView
|
|
178
|
+
style={styles.safeArea}
|
|
179
|
+
mode="padding"
|
|
180
|
+
edges={['top', 'right', 'bottom', 'left']}
|
|
181
|
+
>
|
|
182
|
+
{content}
|
|
183
|
+
</SafeAreaView>
|
|
167
184
|
) : (
|
|
168
185
|
content
|
|
169
186
|
)}
|
|
@@ -173,11 +190,17 @@ export default function FlowboardRenderer(props: FlowboardRendererProps) {
|
|
|
173
190
|
|
|
174
191
|
function renderBackground(bgData: any, legacyColorCode?: string) {
|
|
175
192
|
if (!bgData) {
|
|
193
|
+
const fallbackBackground = parseColor(legacyColorCode);
|
|
176
194
|
return (
|
|
177
195
|
<View
|
|
178
196
|
style={[
|
|
179
197
|
styles.background,
|
|
180
|
-
{
|
|
198
|
+
{
|
|
199
|
+
backgroundColor:
|
|
200
|
+
fallbackBackground === 'transparent'
|
|
201
|
+
? 'rgba(255,255,255,1)'
|
|
202
|
+
: fallbackBackground,
|
|
203
|
+
},
|
|
181
204
|
]}
|
|
182
205
|
/>
|
|
183
206
|
);
|
|
@@ -210,25 +233,17 @@ function renderBackground(bgData: any, legacyColorCode?: string) {
|
|
|
210
233
|
}
|
|
211
234
|
case 'image': {
|
|
212
235
|
const url = bgData.url;
|
|
213
|
-
if (!url)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
style={styles.background}
|
|
218
|
-
resizeMode={fit}
|
|
219
|
-
/>
|
|
220
|
-
);
|
|
236
|
+
if (!hasImageUri(url)) {
|
|
237
|
+
return <View style={[styles.background, styles.whiteBg]} />;
|
|
238
|
+
}
|
|
239
|
+
return <BackgroundImageLayer uri={String(url)} fit={fit} />;
|
|
221
240
|
}
|
|
222
241
|
case 'asset': {
|
|
223
242
|
const path = bgData.path;
|
|
224
|
-
if (!path)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
style={styles.background}
|
|
229
|
-
resizeMode={fit}
|
|
230
|
-
/>
|
|
231
|
-
);
|
|
243
|
+
if (!hasImageUri(path)) {
|
|
244
|
+
return <View style={[styles.background, styles.whiteBg]} />;
|
|
245
|
+
}
|
|
246
|
+
return <BackgroundImageLayer uri={String(path)} fit={fit} />;
|
|
232
247
|
}
|
|
233
248
|
case 'color': {
|
|
234
249
|
return (
|
|
@@ -248,6 +263,46 @@ function renderBackground(bgData: any, legacyColorCode?: string) {
|
|
|
248
263
|
return <View style={[styles.background, { backgroundColor: '#ffffff' }]} />;
|
|
249
264
|
}
|
|
250
265
|
|
|
266
|
+
function BackgroundImageLayer({ uri, fit }: { uri: string; fit: any }) {
|
|
267
|
+
const [hasError, setHasError] = React.useState(false);
|
|
268
|
+
const opacity = React.useRef(new Animated.Value(0)).current;
|
|
269
|
+
|
|
270
|
+
React.useEffect(() => {
|
|
271
|
+
opacity.setValue(0);
|
|
272
|
+
}, [uri, opacity]);
|
|
273
|
+
|
|
274
|
+
const fadeIn = React.useCallback(() => {
|
|
275
|
+
Animated.timing(opacity, {
|
|
276
|
+
toValue: 1,
|
|
277
|
+
duration: 160,
|
|
278
|
+
useNativeDriver: true,
|
|
279
|
+
}).start();
|
|
280
|
+
}, [opacity]);
|
|
281
|
+
|
|
282
|
+
if (hasError) {
|
|
283
|
+
return <View style={[styles.background, styles.whiteBg]} />;
|
|
284
|
+
}
|
|
285
|
+
return (
|
|
286
|
+
<Animated.Image
|
|
287
|
+
source={{ uri }}
|
|
288
|
+
style={[styles.background, { opacity }]}
|
|
289
|
+
resizeMode={fit}
|
|
290
|
+
onLoad={fadeIn}
|
|
291
|
+
onError={() => setHasError(true)}
|
|
292
|
+
/>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function hasImageUri(value: unknown): value is string {
|
|
297
|
+
if (typeof value !== 'string') return false;
|
|
298
|
+
return value.trim().length > 0;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function isNetworkLikeUri(value: unknown): value is string {
|
|
302
|
+
if (!hasImageUri(value)) return false;
|
|
303
|
+
return /^(https?:|file:|content:|data:)/i.test(value.trim());
|
|
304
|
+
}
|
|
305
|
+
|
|
251
306
|
function renderProgressBar(
|
|
252
307
|
currentIndex: number,
|
|
253
308
|
totalScreens: number,
|
|
@@ -377,8 +432,8 @@ function buildWidget(
|
|
|
377
432
|
}
|
|
378
433
|
case 'container': {
|
|
379
434
|
const gradient = parseGradient(props.background);
|
|
380
|
-
const width = normalizeDimension(
|
|
381
|
-
const height = normalizeDimension(
|
|
435
|
+
const width = normalizeDimension(parseLayoutDimension(props.width));
|
|
436
|
+
const height = normalizeDimension(parseLayoutDimension(props.height));
|
|
382
437
|
const borderRadius = props.borderRadius
|
|
383
438
|
? Number(props.borderRadius)
|
|
384
439
|
: undefined;
|
|
@@ -504,8 +559,10 @@ function buildWidget(
|
|
|
504
559
|
case 'button': {
|
|
505
560
|
const gradient = parseGradient(props.background);
|
|
506
561
|
const borderRadius = Number(props.borderRadius ?? 10);
|
|
507
|
-
const width =
|
|
508
|
-
|
|
562
|
+
const width =
|
|
563
|
+
normalizeDimension(parseLayoutDimension(props.width)) ?? '100%';
|
|
564
|
+
const height =
|
|
565
|
+
normalizeDimension(parseLayoutDimension(props.height)) ?? 50;
|
|
509
566
|
const label = props.label ?? 'Button';
|
|
510
567
|
const textStyle = getTextStyle(
|
|
511
568
|
{ ...props, height: null },
|
|
@@ -583,8 +640,8 @@ function buildWidget(
|
|
|
583
640
|
node = (
|
|
584
641
|
<View
|
|
585
642
|
style={{
|
|
586
|
-
height: normalizeDimension(
|
|
587
|
-
width: normalizeDimension(
|
|
643
|
+
height: normalizeDimension(parseLayoutDimension(props.height)),
|
|
644
|
+
width: normalizeDimension(parseLayoutDimension(props.width)),
|
|
588
645
|
}}
|
|
589
646
|
/>
|
|
590
647
|
);
|
|
@@ -594,10 +651,12 @@ function buildWidget(
|
|
|
594
651
|
const expandedChild = childJson
|
|
595
652
|
? buildWidget(childJson, { ...params, allowFlexExpansion: true })
|
|
596
653
|
: null;
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
654
|
+
node = params.allowFlexExpansion ? (
|
|
655
|
+
<View style={{ flex: 1 }}>{expandedChild}</View>
|
|
656
|
+
) : (
|
|
657
|
+
expandedChild
|
|
658
|
+
);
|
|
659
|
+
break;
|
|
601
660
|
}
|
|
602
661
|
case 'padding': {
|
|
603
662
|
const padding = parseInsets(props.padding ?? 0);
|
|
@@ -633,7 +692,62 @@ function buildWidget(
|
|
|
633
692
|
params.onInputChange(id, value);
|
|
634
693
|
}
|
|
635
694
|
}}
|
|
636
|
-
onAction={
|
|
695
|
+
onAction={(actionName, payload) => {
|
|
696
|
+
if (
|
|
697
|
+
actionName === 'next' &&
|
|
698
|
+
params.onInputChange &&
|
|
699
|
+
id &&
|
|
700
|
+
Object.prototype.hasOwnProperty.call(
|
|
701
|
+
payload ?? {},
|
|
702
|
+
'selectedValue'
|
|
703
|
+
)
|
|
704
|
+
) {
|
|
705
|
+
params.onInputChange(id, payload?.selectedValue);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
params.onAction(actionName, {
|
|
709
|
+
...(payload ?? {}),
|
|
710
|
+
...(id ? { fieldId: id } : {}),
|
|
711
|
+
});
|
|
712
|
+
}}
|
|
713
|
+
buildWidget={(widgetJson) =>
|
|
714
|
+
buildWidget(widgetJson, { ...params, allowFlexExpansion: true })
|
|
715
|
+
}
|
|
716
|
+
/>
|
|
717
|
+
);
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
case 'wheel_picker': {
|
|
721
|
+
node = (
|
|
722
|
+
<WheelPicker
|
|
723
|
+
properties={props}
|
|
724
|
+
options={Array.isArray(json.options) ? json.options : []}
|
|
725
|
+
itemTemplate={json.itemTemplate ?? props.itemTemplate}
|
|
726
|
+
formData={params.formData}
|
|
727
|
+
initialValue={params.formData[id ?? '']}
|
|
728
|
+
onChanged={(value) => {
|
|
729
|
+
if (params.onInputChange && id) {
|
|
730
|
+
params.onInputChange(id, value);
|
|
731
|
+
}
|
|
732
|
+
}}
|
|
733
|
+
onAction={(actionName, payload) => {
|
|
734
|
+
if (
|
|
735
|
+
actionName === 'next' &&
|
|
736
|
+
params.onInputChange &&
|
|
737
|
+
id &&
|
|
738
|
+
Object.prototype.hasOwnProperty.call(
|
|
739
|
+
payload ?? {},
|
|
740
|
+
'selectedValue'
|
|
741
|
+
)
|
|
742
|
+
) {
|
|
743
|
+
params.onInputChange(id, payload?.selectedValue);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
params.onAction(actionName, {
|
|
747
|
+
...(payload ?? {}),
|
|
748
|
+
...(id ? { fieldId: id } : {}),
|
|
749
|
+
});
|
|
750
|
+
}}
|
|
637
751
|
buildWidget={(widgetJson) =>
|
|
638
752
|
buildWidget(widgetJson, { ...params, allowFlexExpansion: true })
|
|
639
753
|
}
|
|
@@ -643,23 +757,28 @@ function buildWidget(
|
|
|
643
757
|
}
|
|
644
758
|
case 'image': {
|
|
645
759
|
const source = props.source;
|
|
646
|
-
const width = normalizeDimension(
|
|
647
|
-
const height = normalizeDimension(
|
|
760
|
+
const width = normalizeDimension(parseLayoutDimension(props.width, true));
|
|
761
|
+
const height = normalizeDimension(
|
|
762
|
+
parseLayoutDimension(props.height, true)
|
|
763
|
+
);
|
|
648
764
|
if (source === 'asset') {
|
|
765
|
+
if (!hasImageUri(props.path)) return null;
|
|
649
766
|
node = (
|
|
650
|
-
<
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
767
|
+
<SduiImage
|
|
768
|
+
uri={String(props.path)}
|
|
769
|
+
fit={parseResizeMode(props.fit)}
|
|
770
|
+
width={width}
|
|
771
|
+
height={height}
|
|
654
772
|
/>
|
|
655
773
|
);
|
|
656
774
|
} else if (source === 'network') {
|
|
657
|
-
if (!props.url) return null;
|
|
775
|
+
if (!isNetworkLikeUri(props.url)) return null;
|
|
658
776
|
node = (
|
|
659
|
-
<
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
777
|
+
<SduiImage
|
|
778
|
+
uri={String(props.url)}
|
|
779
|
+
fit={parseResizeMode(props.fit)}
|
|
780
|
+
width={width}
|
|
781
|
+
height={height}
|
|
663
782
|
/>
|
|
664
783
|
);
|
|
665
784
|
} else {
|
|
@@ -669,8 +788,10 @@ function buildWidget(
|
|
|
669
788
|
}
|
|
670
789
|
case 'lottie': {
|
|
671
790
|
const source = props.source;
|
|
672
|
-
const width = normalizeDimension(
|
|
673
|
-
const height = normalizeDimension(
|
|
791
|
+
const width = normalizeDimension(parseLayoutDimension(props.width, true));
|
|
792
|
+
const height = normalizeDimension(
|
|
793
|
+
parseLayoutDimension(props.height, true)
|
|
794
|
+
);
|
|
674
795
|
const loop = props.loop === true;
|
|
675
796
|
if (source === 'asset') {
|
|
676
797
|
node = (
|
|
@@ -758,7 +879,7 @@ function buildWidget(
|
|
|
758
879
|
autoPlay={props.autoPlay === true}
|
|
759
880
|
autoPlayDelay={Number(props.autoPlayDelay ?? 3000)}
|
|
760
881
|
loop={props.loop === true}
|
|
761
|
-
height={
|
|
882
|
+
height={parseLayoutDimension(props.height)}
|
|
762
883
|
physics={props.physics}
|
|
763
884
|
>
|
|
764
885
|
{(childrenJson ?? []).map((child, index) => (
|
|
@@ -836,13 +957,13 @@ function buildWidget(
|
|
|
836
957
|
break;
|
|
837
958
|
}
|
|
838
959
|
case 'positioned': {
|
|
839
|
-
const left =
|
|
840
|
-
const top =
|
|
841
|
-
const right =
|
|
842
|
-
const bottom =
|
|
843
|
-
const width =
|
|
844
|
-
const height =
|
|
845
|
-
|
|
960
|
+
const left = parseLayoutDimension(props.left);
|
|
961
|
+
const top = parseLayoutDimension(props.top);
|
|
962
|
+
const right = parseLayoutDimension(props.right);
|
|
963
|
+
const bottom = parseLayoutDimension(props.bottom);
|
|
964
|
+
const width = parseLayoutDimension(props.width);
|
|
965
|
+
const height = parseLayoutDimension(props.height);
|
|
966
|
+
node = (
|
|
846
967
|
<View
|
|
847
968
|
style={{
|
|
848
969
|
position: 'absolute',
|
|
@@ -857,6 +978,7 @@ function buildWidget(
|
|
|
857
978
|
{childJson ? buildWidget(childJson, { ...params }) : null}
|
|
858
979
|
</View>
|
|
859
980
|
);
|
|
981
|
+
break;
|
|
860
982
|
}
|
|
861
983
|
default:
|
|
862
984
|
node = null;
|
|
@@ -867,7 +989,7 @@ function buildWidget(
|
|
|
867
989
|
const marginVal = props.margin;
|
|
868
990
|
if (marginVal !== undefined && marginVal !== null) {
|
|
869
991
|
const marginInsets = parseInsets(marginVal);
|
|
870
|
-
node = <View style={
|
|
992
|
+
node = <View style={insetsToMarginStyle(marginInsets)}>{node}</View>;
|
|
871
993
|
} else if (type !== 'container' && type !== 'padding' && props.padding) {
|
|
872
994
|
const paddingInsets = parseInsets(props.padding);
|
|
873
995
|
node = <View style={insetsToStyle(paddingInsets)}>{node}</View>;
|
|
@@ -879,14 +1001,22 @@ function buildWidget(
|
|
|
879
1001
|
shouldExpand = true;
|
|
880
1002
|
}
|
|
881
1003
|
if (props.height !== undefined) {
|
|
882
|
-
const heightVal =
|
|
1004
|
+
const heightVal = parseLayoutDimension(props.height);
|
|
883
1005
|
if (heightVal === Number.POSITIVE_INFINITY) {
|
|
884
1006
|
shouldExpand = true;
|
|
885
1007
|
}
|
|
886
1008
|
}
|
|
887
1009
|
|
|
888
1010
|
if (shouldExpand) {
|
|
889
|
-
|
|
1011
|
+
node = <View style={{ flex: 1 }}>{node}</View>;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
if (params.key !== undefined && params.key !== null) {
|
|
1016
|
+
if (React.isValidElement(node)) {
|
|
1017
|
+
node = React.cloneElement(node, { key: params.key });
|
|
1018
|
+
} else {
|
|
1019
|
+
node = <React.Fragment key={params.key}>{node}</React.Fragment>;
|
|
890
1020
|
}
|
|
891
1021
|
}
|
|
892
1022
|
|
|
@@ -992,7 +1122,7 @@ function withOpacity(color: string, opacity: number): string {
|
|
|
992
1122
|
}
|
|
993
1123
|
|
|
994
1124
|
function normalizeDimension(
|
|
995
|
-
value: number | undefined
|
|
1125
|
+
value: number | string | undefined
|
|
996
1126
|
): number | string | undefined {
|
|
997
1127
|
if (value === Number.POSITIVE_INFINITY) {
|
|
998
1128
|
return '100%';
|
|
@@ -1000,6 +1130,126 @@ function normalizeDimension(
|
|
|
1000
1130
|
return value;
|
|
1001
1131
|
}
|
|
1002
1132
|
|
|
1133
|
+
function parseRootCrossAlignment(value?: string): any {
|
|
1134
|
+
if (!value) return 'stretch';
|
|
1135
|
+
return parseCrossAlignment(value);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function parseLayoutDimension(
|
|
1139
|
+
value: any,
|
|
1140
|
+
zeroIsNull = false
|
|
1141
|
+
): number | string | undefined {
|
|
1142
|
+
if (typeof value === 'string') {
|
|
1143
|
+
const trimmed = value.trim();
|
|
1144
|
+
const lowered = trimmed.toLowerCase();
|
|
1145
|
+
|
|
1146
|
+
if (
|
|
1147
|
+
lowered === 'double.infinity' ||
|
|
1148
|
+
lowered === 'infinity' ||
|
|
1149
|
+
lowered === '+infinity'
|
|
1150
|
+
) {
|
|
1151
|
+
return Number.POSITIVE_INFINITY;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
if (trimmed.endsWith('%')) {
|
|
1155
|
+
const numeric = Number(trimmed.slice(0, -1));
|
|
1156
|
+
if (!Number.isNaN(numeric) && Number.isFinite(numeric)) {
|
|
1157
|
+
if (zeroIsNull && numeric === 0) return undefined;
|
|
1158
|
+
return `${numeric}%`;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
return parseDimension(value, zeroIsNull);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function SduiImage({
|
|
1167
|
+
uri,
|
|
1168
|
+
fit,
|
|
1169
|
+
width,
|
|
1170
|
+
height,
|
|
1171
|
+
}: {
|
|
1172
|
+
uri: string;
|
|
1173
|
+
fit: any;
|
|
1174
|
+
width: number | string | undefined;
|
|
1175
|
+
height: number | string | undefined;
|
|
1176
|
+
}) {
|
|
1177
|
+
const hasExplicitWidth = width !== undefined;
|
|
1178
|
+
const hasExplicitHeight = height !== undefined;
|
|
1179
|
+
const [aspectRatio, setAspectRatio] = React.useState<number | undefined>(
|
|
1180
|
+
undefined
|
|
1181
|
+
);
|
|
1182
|
+
const [hasError, setHasError] = React.useState(false);
|
|
1183
|
+
const opacity = React.useRef(new Animated.Value(0)).current;
|
|
1184
|
+
|
|
1185
|
+
const updateAspectRatio = (w?: number, h?: number) => {
|
|
1186
|
+
if (!w || !h) return;
|
|
1187
|
+
if (!Number.isFinite(w) || !Number.isFinite(h) || h <= 0) return;
|
|
1188
|
+
const ratio = w / h;
|
|
1189
|
+
if (Number.isFinite(ratio) && ratio > 0) {
|
|
1190
|
+
setAspectRatio(ratio);
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
|
|
1194
|
+
React.useEffect(() => {
|
|
1195
|
+
if (hasExplicitWidth && hasExplicitHeight) return;
|
|
1196
|
+
Image.getSize(
|
|
1197
|
+
uri,
|
|
1198
|
+
(w, h) => updateAspectRatio(w, h),
|
|
1199
|
+
() => null
|
|
1200
|
+
);
|
|
1201
|
+
}, [uri, hasExplicitWidth, hasExplicitHeight]);
|
|
1202
|
+
|
|
1203
|
+
React.useEffect(() => {
|
|
1204
|
+
opacity.setValue(0);
|
|
1205
|
+
}, [uri, opacity]);
|
|
1206
|
+
|
|
1207
|
+
if (hasError) {
|
|
1208
|
+
return null;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
const style: any = { width, height };
|
|
1212
|
+
|
|
1213
|
+
if (!hasExplicitWidth && !hasExplicitHeight) {
|
|
1214
|
+
style.width = '100%';
|
|
1215
|
+
if (aspectRatio) {
|
|
1216
|
+
style.aspectRatio = aspectRatio;
|
|
1217
|
+
} else {
|
|
1218
|
+
style.minHeight = 160;
|
|
1219
|
+
}
|
|
1220
|
+
} else if (hasExplicitWidth && !hasExplicitHeight) {
|
|
1221
|
+
if (aspectRatio) {
|
|
1222
|
+
style.aspectRatio = aspectRatio;
|
|
1223
|
+
} else {
|
|
1224
|
+
style.minHeight = 100;
|
|
1225
|
+
}
|
|
1226
|
+
} else if (!hasExplicitWidth && hasExplicitHeight) {
|
|
1227
|
+
if (aspectRatio) {
|
|
1228
|
+
style.aspectRatio = aspectRatio;
|
|
1229
|
+
} else {
|
|
1230
|
+
style.width = '100%';
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
return (
|
|
1235
|
+
<Animated.Image
|
|
1236
|
+
source={{ uri }}
|
|
1237
|
+
style={[style, { opacity }]}
|
|
1238
|
+
resizeMode={fit}
|
|
1239
|
+
onLoad={(event) => {
|
|
1240
|
+
const source = event?.nativeEvent?.source;
|
|
1241
|
+
updateAspectRatio(source?.width, source?.height);
|
|
1242
|
+
Animated.timing(opacity, {
|
|
1243
|
+
toValue: 1,
|
|
1244
|
+
duration: 160,
|
|
1245
|
+
useNativeDriver: true,
|
|
1246
|
+
}).start();
|
|
1247
|
+
}}
|
|
1248
|
+
onError={() => setHasError(true)}
|
|
1249
|
+
/>
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1003
1253
|
function GradientText({
|
|
1004
1254
|
text,
|
|
1005
1255
|
gradient,
|
|
@@ -1083,7 +1333,7 @@ function DynamicInput({
|
|
|
1083
1333
|
{mask ? (
|
|
1084
1334
|
<MaskInput
|
|
1085
1335
|
value={value}
|
|
1086
|
-
onChangeText={(_masked, unmasked) => {
|
|
1336
|
+
onChangeText={(_masked: string, unmasked: string) => {
|
|
1087
1337
|
setValue(unmasked);
|
|
1088
1338
|
onChanged(unmasked);
|
|
1089
1339
|
}}
|
|
@@ -1203,7 +1453,9 @@ function SelectionList({
|
|
|
1203
1453
|
next = [value];
|
|
1204
1454
|
onChanged(value);
|
|
1205
1455
|
if (properties.autoGoNext === true) {
|
|
1206
|
-
|
|
1456
|
+
requestAnimationFrame(() =>
|
|
1457
|
+
onAction('next', { selectedValue: value })
|
|
1458
|
+
);
|
|
1207
1459
|
}
|
|
1208
1460
|
}
|
|
1209
1461
|
return next;
|
|
@@ -1291,6 +1543,364 @@ function SelectionList({
|
|
|
1291
1543
|
);
|
|
1292
1544
|
}
|
|
1293
1545
|
|
|
1546
|
+
function WheelPicker({
|
|
1547
|
+
properties,
|
|
1548
|
+
options,
|
|
1549
|
+
itemTemplate,
|
|
1550
|
+
formData,
|
|
1551
|
+
initialValue,
|
|
1552
|
+
onChanged,
|
|
1553
|
+
onAction,
|
|
1554
|
+
buildWidget,
|
|
1555
|
+
}: {
|
|
1556
|
+
properties: Record<string, any>;
|
|
1557
|
+
options: Record<string, any>[];
|
|
1558
|
+
itemTemplate: unknown;
|
|
1559
|
+
formData: Record<string, any>;
|
|
1560
|
+
initialValue: any;
|
|
1561
|
+
onChanged: (value: string) => void;
|
|
1562
|
+
onAction: (action: string, data?: Record<string, any>) => void;
|
|
1563
|
+
buildWidget: (widgetJson: Record<string, any>) => React.ReactNode;
|
|
1564
|
+
}) {
|
|
1565
|
+
const multiSelect = properties.multiSelect === true;
|
|
1566
|
+
const [selectedValues, setSelectedValues] = React.useState<string[]>(() => {
|
|
1567
|
+
if (initialValue === null || initialValue === undefined) return [];
|
|
1568
|
+
if (Array.isArray(initialValue)) return initialValue.map(String);
|
|
1569
|
+
if (typeof initialValue === 'string') {
|
|
1570
|
+
if (multiSelect) {
|
|
1571
|
+
try {
|
|
1572
|
+
return initialValue
|
|
1573
|
+
.replace('[', '')
|
|
1574
|
+
.replace(']', '')
|
|
1575
|
+
.split(',')
|
|
1576
|
+
.map((val) => val.trim())
|
|
1577
|
+
.filter((val) => val.length > 0);
|
|
1578
|
+
} catch {
|
|
1579
|
+
return [initialValue];
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
return [initialValue];
|
|
1583
|
+
}
|
|
1584
|
+
return [];
|
|
1585
|
+
});
|
|
1586
|
+
|
|
1587
|
+
const resolvedOptions = React.useMemo(
|
|
1588
|
+
() =>
|
|
1589
|
+
buildWheelPickerOptions({
|
|
1590
|
+
options,
|
|
1591
|
+
properties,
|
|
1592
|
+
formData,
|
|
1593
|
+
itemTemplate,
|
|
1594
|
+
}),
|
|
1595
|
+
[formData, itemTemplate, options, properties]
|
|
1596
|
+
);
|
|
1597
|
+
|
|
1598
|
+
const toggleSelection = (value: string) => {
|
|
1599
|
+
setSelectedValues((prev) => {
|
|
1600
|
+
let next = [...prev];
|
|
1601
|
+
if (multiSelect) {
|
|
1602
|
+
if (next.includes(value)) {
|
|
1603
|
+
next = next.filter((item) => item !== value);
|
|
1604
|
+
} else {
|
|
1605
|
+
next.push(value);
|
|
1606
|
+
}
|
|
1607
|
+
onChanged(next.join(','));
|
|
1608
|
+
} else {
|
|
1609
|
+
next = [value];
|
|
1610
|
+
onChanged(value);
|
|
1611
|
+
if (properties.autoGoNext === true) {
|
|
1612
|
+
requestAnimationFrame(() =>
|
|
1613
|
+
onAction('next', { selectedValue: value })
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
return next;
|
|
1618
|
+
});
|
|
1619
|
+
};
|
|
1620
|
+
|
|
1621
|
+
const layout = properties.layout ?? 'wheel';
|
|
1622
|
+
const spacing = Number(properties.spacing ?? 8);
|
|
1623
|
+
|
|
1624
|
+
const renderItem = (option: Record<string, any>, index: number) => {
|
|
1625
|
+
const value = String(option.value ?? '');
|
|
1626
|
+
const isSelected = selectedValues.includes(value);
|
|
1627
|
+
const styleProps = isSelected
|
|
1628
|
+
? option.selectedStyle ??
|
|
1629
|
+
properties.selectedStyle ?? {
|
|
1630
|
+
backgroundColor: properties.wheelSelectedBackgroundColor,
|
|
1631
|
+
borderColor: properties.wheelSelectedBorderColor,
|
|
1632
|
+
}
|
|
1633
|
+
: option.unselectedStyle ??
|
|
1634
|
+
properties.unselectedStyle ?? {
|
|
1635
|
+
backgroundColor: properties.wheelUnselectedBackgroundColor,
|
|
1636
|
+
borderColor: properties.wheelUnselectedBorderColor,
|
|
1637
|
+
};
|
|
1638
|
+
|
|
1639
|
+
const backgroundColor = parseColor(styleProps.backgroundColor);
|
|
1640
|
+
const borderColor = parseColor(styleProps.borderColor);
|
|
1641
|
+
const borderRadius = Number(styleProps.borderRadius ?? 8);
|
|
1642
|
+
const borderWidth = Number(styleProps.borderWidth ?? 1);
|
|
1643
|
+
|
|
1644
|
+
return (
|
|
1645
|
+
<Pressable
|
|
1646
|
+
key={`wheel-option-${index}`}
|
|
1647
|
+
onPress={() => toggleSelection(value)}
|
|
1648
|
+
style={{
|
|
1649
|
+
width: '100%',
|
|
1650
|
+
backgroundColor,
|
|
1651
|
+
borderColor,
|
|
1652
|
+
borderWidth,
|
|
1653
|
+
borderRadius,
|
|
1654
|
+
padding: 0,
|
|
1655
|
+
}}
|
|
1656
|
+
>
|
|
1657
|
+
{buildWidget(option.child)}
|
|
1658
|
+
</Pressable>
|
|
1659
|
+
);
|
|
1660
|
+
};
|
|
1661
|
+
|
|
1662
|
+
if (layout === 'row') {
|
|
1663
|
+
return (
|
|
1664
|
+
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
|
|
1665
|
+
{resolvedOptions.map((option, index) => (
|
|
1666
|
+
<View
|
|
1667
|
+
key={`wheel-row-option-${index}`}
|
|
1668
|
+
style={{ marginRight: spacing, marginBottom: spacing }}
|
|
1669
|
+
>
|
|
1670
|
+
{renderItem(option, index)}
|
|
1671
|
+
</View>
|
|
1672
|
+
))}
|
|
1673
|
+
</View>
|
|
1674
|
+
);
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
if (layout === 'grid') {
|
|
1678
|
+
const crossAxisCount = Number(properties.gridCrossAxisCount ?? 2);
|
|
1679
|
+
const aspectRatio = Number(properties.gridAspectRatio ?? 1);
|
|
1680
|
+
return (
|
|
1681
|
+
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
|
|
1682
|
+
{resolvedOptions.map((option, index) => (
|
|
1683
|
+
<View
|
|
1684
|
+
key={`wheel-grid-option-${index}`}
|
|
1685
|
+
style={{
|
|
1686
|
+
width: `${100 / crossAxisCount}%`,
|
|
1687
|
+
paddingRight: (index + 1) % crossAxisCount === 0 ? 0 : spacing,
|
|
1688
|
+
paddingBottom: spacing,
|
|
1689
|
+
aspectRatio,
|
|
1690
|
+
}}
|
|
1691
|
+
>
|
|
1692
|
+
{renderItem(option, index)}
|
|
1693
|
+
</View>
|
|
1694
|
+
))}
|
|
1695
|
+
</View>
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
if (layout === 'wheel') {
|
|
1700
|
+
const itemHeight = Math.max(1, Number(properties.wheelItemHeight ?? 48));
|
|
1701
|
+
const requestedVisible = Math.max(
|
|
1702
|
+
3,
|
|
1703
|
+
Math.floor(Number(properties.visibleItems ?? 5))
|
|
1704
|
+
);
|
|
1705
|
+
const visibleItems =
|
|
1706
|
+
requestedVisible % 2 === 0 ? requestedVisible + 1 : requestedVisible;
|
|
1707
|
+
const containerHeight = itemHeight * visibleItems;
|
|
1708
|
+
const centerPadding = Math.max(0, (containerHeight - itemHeight) / 2);
|
|
1709
|
+
|
|
1710
|
+
return (
|
|
1711
|
+
<View style={{ height: containerHeight }}>
|
|
1712
|
+
<ScrollView
|
|
1713
|
+
showsVerticalScrollIndicator={false}
|
|
1714
|
+
contentContainerStyle={{ paddingVertical: centerPadding }}
|
|
1715
|
+
>
|
|
1716
|
+
{resolvedOptions.map((option, index) => (
|
|
1717
|
+
<View
|
|
1718
|
+
key={`wheel-column-option-${index}`}
|
|
1719
|
+
style={{
|
|
1720
|
+
minHeight: itemHeight,
|
|
1721
|
+
justifyContent: 'center',
|
|
1722
|
+
marginBottom:
|
|
1723
|
+
spacing > 0 && index < resolvedOptions.length - 1 ? spacing : 0,
|
|
1724
|
+
}}
|
|
1725
|
+
>
|
|
1726
|
+
{renderItem(option, index)}
|
|
1727
|
+
</View>
|
|
1728
|
+
))}
|
|
1729
|
+
</ScrollView>
|
|
1730
|
+
</View>
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
return (
|
|
1735
|
+
<View>
|
|
1736
|
+
{resolvedOptions.map((option, index) => (
|
|
1737
|
+
<View key={`wheel-list-option-${index}`} style={{ marginBottom: spacing }}>
|
|
1738
|
+
{renderItem(option, index)}
|
|
1739
|
+
</View>
|
|
1740
|
+
))}
|
|
1741
|
+
</View>
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
function buildWheelPickerOptions({
|
|
1746
|
+
options,
|
|
1747
|
+
properties,
|
|
1748
|
+
formData,
|
|
1749
|
+
itemTemplate,
|
|
1750
|
+
}: {
|
|
1751
|
+
options: Record<string, any>[];
|
|
1752
|
+
properties: Record<string, any>;
|
|
1753
|
+
formData: Record<string, any>;
|
|
1754
|
+
itemTemplate: unknown;
|
|
1755
|
+
}): Record<string, any>[] {
|
|
1756
|
+
const templateContext = buildWheelTemplateBaseContext(formData, properties);
|
|
1757
|
+
const itemCountRaw = properties.itemCount ?? properties.item_count;
|
|
1758
|
+
const itemCountResolved = resolveNumericValue(itemCountRaw, templateContext);
|
|
1759
|
+
const itemCount = normalizeItemCount(itemCountResolved);
|
|
1760
|
+
|
|
1761
|
+
if (itemCount > 0) {
|
|
1762
|
+
const generatedTemplate = normalizeWheelOptionTemplate(
|
|
1763
|
+
itemTemplate ?? properties.itemTemplate ?? options[0]
|
|
1764
|
+
);
|
|
1765
|
+
if (!generatedTemplate) return [];
|
|
1766
|
+
|
|
1767
|
+
const start =
|
|
1768
|
+
resolveNumericValue(
|
|
1769
|
+
properties.start ?? properties.itemStart ?? 0,
|
|
1770
|
+
templateContext
|
|
1771
|
+
) ?? 0;
|
|
1772
|
+
const step =
|
|
1773
|
+
resolveNumericValue(
|
|
1774
|
+
properties.step ?? properties.itemStep ?? 1,
|
|
1775
|
+
templateContext
|
|
1776
|
+
) ?? 1;
|
|
1777
|
+
|
|
1778
|
+
return Array.from({ length: itemCount }).map((_, index) => {
|
|
1779
|
+
const itemValue = start + step * index;
|
|
1780
|
+
const context = {
|
|
1781
|
+
...templateContext,
|
|
1782
|
+
start,
|
|
1783
|
+
step,
|
|
1784
|
+
index,
|
|
1785
|
+
item: itemValue,
|
|
1786
|
+
};
|
|
1787
|
+
const resolvedOption = resolveWheelTemplateValue(
|
|
1788
|
+
generatedTemplate,
|
|
1789
|
+
context
|
|
1790
|
+
);
|
|
1791
|
+
return ensureWheelOptionShape(resolvedOption, String(itemValue));
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
return options.map((option, index) => {
|
|
1796
|
+
const itemValue = option?.value ?? index;
|
|
1797
|
+
const context = {
|
|
1798
|
+
...templateContext,
|
|
1799
|
+
index,
|
|
1800
|
+
item: itemValue,
|
|
1801
|
+
};
|
|
1802
|
+
const resolvedOption = resolveWheelTemplateValue(option, context);
|
|
1803
|
+
return ensureWheelOptionShape(resolvedOption, String(itemValue));
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
function normalizeItemCount(value: number | null): number {
|
|
1808
|
+
if (value === null || !Number.isFinite(value)) return 0;
|
|
1809
|
+
return Math.max(0, Math.floor(value));
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
function buildWheelTemplateBaseContext(
|
|
1813
|
+
formData: Record<string, any>,
|
|
1814
|
+
properties: Record<string, any>
|
|
1815
|
+
): Record<string, any> {
|
|
1816
|
+
const context = { ...formData };
|
|
1817
|
+
const start = resolveNumericValue(
|
|
1818
|
+
properties.start ?? properties.itemStart,
|
|
1819
|
+
context
|
|
1820
|
+
);
|
|
1821
|
+
const step = resolveNumericValue(properties.step ?? properties.itemStep, context);
|
|
1822
|
+
|
|
1823
|
+
if (start !== null) context.start = start;
|
|
1824
|
+
if (step !== null) context.step = step;
|
|
1825
|
+
return context;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
function normalizeWheelOptionTemplate(
|
|
1829
|
+
template: unknown
|
|
1830
|
+
): Record<string, any> | null {
|
|
1831
|
+
if (!template || typeof template !== 'object') return null;
|
|
1832
|
+
const asMap = template as Record<string, any>;
|
|
1833
|
+
if (
|
|
1834
|
+
Object.prototype.hasOwnProperty.call(asMap, 'value') ||
|
|
1835
|
+
Object.prototype.hasOwnProperty.call(asMap, 'child')
|
|
1836
|
+
) {
|
|
1837
|
+
return { ...asMap };
|
|
1838
|
+
}
|
|
1839
|
+
if (typeof asMap.type === 'string') {
|
|
1840
|
+
return {
|
|
1841
|
+
value: '{{item}}',
|
|
1842
|
+
child: asMap,
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
return null;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
function ensureWheelOptionShape(
|
|
1849
|
+
option: unknown,
|
|
1850
|
+
fallbackValue: string
|
|
1851
|
+
): Record<string, any> {
|
|
1852
|
+
if (!option || typeof option !== 'object') {
|
|
1853
|
+
return {
|
|
1854
|
+
value: fallbackValue,
|
|
1855
|
+
child: {
|
|
1856
|
+
type: 'text',
|
|
1857
|
+
properties: { text: fallbackValue },
|
|
1858
|
+
},
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
const normalized = { ...(option as Record<string, any>) };
|
|
1863
|
+
const rawValue = normalized.value ?? fallbackValue;
|
|
1864
|
+
const value = String(rawValue);
|
|
1865
|
+
|
|
1866
|
+
const child =
|
|
1867
|
+
normalized.child && typeof normalized.child === 'object'
|
|
1868
|
+
? normalized.child
|
|
1869
|
+
: {
|
|
1870
|
+
type: 'text',
|
|
1871
|
+
properties: { text: value },
|
|
1872
|
+
};
|
|
1873
|
+
|
|
1874
|
+
return {
|
|
1875
|
+
...normalized,
|
|
1876
|
+
value,
|
|
1877
|
+
child,
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
function resolveWheelTemplateValue(
|
|
1882
|
+
value: unknown,
|
|
1883
|
+
context: Record<string, any>
|
|
1884
|
+
): unknown {
|
|
1885
|
+
if (typeof value === 'string') {
|
|
1886
|
+
return resolveText(value, context);
|
|
1887
|
+
}
|
|
1888
|
+
if (Array.isArray(value)) {
|
|
1889
|
+
return value.map((entry) => resolveWheelTemplateValue(entry, context));
|
|
1890
|
+
}
|
|
1891
|
+
if (value && typeof value === 'object') {
|
|
1892
|
+
const result: Record<string, any> = {};
|
|
1893
|
+
Object.keys(value as Record<string, any>).forEach((key) => {
|
|
1894
|
+
result[key] = resolveWheelTemplateValue(
|
|
1895
|
+
(value as Record<string, any>)[key],
|
|
1896
|
+
context
|
|
1897
|
+
);
|
|
1898
|
+
});
|
|
1899
|
+
return result;
|
|
1900
|
+
}
|
|
1901
|
+
return value;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1294
1904
|
function FakeProgressBar({
|
|
1295
1905
|
progressColor,
|
|
1296
1906
|
backgroundColor,
|
|
@@ -1436,38 +2046,42 @@ function SliderWidget({
|
|
|
1436
2046
|
autoPlay: boolean;
|
|
1437
2047
|
autoPlayDelay: number;
|
|
1438
2048
|
loop: boolean;
|
|
1439
|
-
height?: number;
|
|
2049
|
+
height?: number | string;
|
|
1440
2050
|
physics?: string;
|
|
1441
2051
|
children: React.ReactNode[];
|
|
1442
2052
|
}) {
|
|
1443
2053
|
const registry = useSliderRegistry();
|
|
1444
2054
|
const pagerRef = React.useRef<PagerView>(null);
|
|
1445
2055
|
const pageLength = children.length;
|
|
1446
|
-
const
|
|
1447
|
-
|
|
1448
|
-
);
|
|
2056
|
+
const pageCount = pageLength;
|
|
2057
|
+
const [currentPage, setCurrentPage] = React.useState(0);
|
|
1449
2058
|
const timerRef = React.useRef<NodeJS.Timeout | null>(null);
|
|
1450
2059
|
|
|
1451
2060
|
React.useEffect(() => {
|
|
1452
|
-
|
|
2061
|
+
setCurrentPage((prev) => clampSliderPage(prev, pageCount));
|
|
2062
|
+
}, [pageCount]);
|
|
2063
|
+
|
|
2064
|
+
React.useEffect(() => {
|
|
2065
|
+
if (autoPlay && !linkedTo && pageLength > 0) {
|
|
1453
2066
|
timerRef.current = setInterval(() => {
|
|
1454
2067
|
setCurrentPage((prev) => {
|
|
1455
|
-
|
|
1456
|
-
if (
|
|
1457
|
-
return 0;
|
|
2068
|
+
const safePrev = clampSliderPage(prev, pageCount);
|
|
2069
|
+
if (safePrev < pageLength - 1) return safePrev + 1;
|
|
2070
|
+
if (loop) return 0;
|
|
2071
|
+
return safePrev;
|
|
1458
2072
|
});
|
|
1459
2073
|
}, autoPlayDelay);
|
|
1460
2074
|
}
|
|
1461
2075
|
return () => {
|
|
1462
2076
|
if (timerRef.current) clearInterval(timerRef.current);
|
|
1463
2077
|
};
|
|
1464
|
-
}, [autoPlay, autoPlayDelay, loop,
|
|
2078
|
+
}, [autoPlay, autoPlayDelay, linkedTo, loop, pageCount, pageLength]);
|
|
1465
2079
|
|
|
1466
2080
|
React.useEffect(() => {
|
|
1467
|
-
if (pagerRef.current) {
|
|
1468
|
-
pagerRef.current.setPage(currentPage);
|
|
2081
|
+
if (pagerRef.current && pageCount > 0) {
|
|
2082
|
+
pagerRef.current.setPage(clampSliderPage(currentPage, pageCount));
|
|
1469
2083
|
}
|
|
1470
|
-
}, [currentPage]);
|
|
2084
|
+
}, [currentPage, pageCount]);
|
|
1471
2085
|
|
|
1472
2086
|
React.useEffect(() => {
|
|
1473
2087
|
if (registry && id && children.length > 0) {
|
|
@@ -1478,28 +2092,30 @@ function SliderWidget({
|
|
|
1478
2092
|
React.useEffect(() => {
|
|
1479
2093
|
if (!linkedTo || !registry) return;
|
|
1480
2094
|
return registry.getNotifier(linkedTo, (value) => {
|
|
2095
|
+
const normalized = normalizeLoopIndex(value, pageLength);
|
|
2096
|
+
const targetPage = clampSliderPage(normalized, pageCount);
|
|
2097
|
+
setCurrentPage(targetPage);
|
|
1481
2098
|
if (pagerRef.current) {
|
|
1482
|
-
pagerRef.current.setPage(
|
|
2099
|
+
pagerRef.current.setPage(targetPage);
|
|
1483
2100
|
}
|
|
1484
2101
|
});
|
|
1485
|
-
}, [linkedTo, registry]);
|
|
2102
|
+
}, [linkedTo, pageCount, pageLength, registry]);
|
|
1486
2103
|
|
|
1487
2104
|
if (pageLength === 0) return null;
|
|
1488
2105
|
|
|
1489
|
-
const pageCount = loop ? pageLength * 1000 : pageLength;
|
|
1490
|
-
|
|
1491
2106
|
return (
|
|
1492
2107
|
<PagerView
|
|
1493
2108
|
ref={pagerRef}
|
|
1494
|
-
style={{
|
|
2109
|
+
style={height === undefined ? { flex: 1 } : { height }}
|
|
1495
2110
|
orientation={direction}
|
|
1496
2111
|
scrollEnabled={physics !== 'never'}
|
|
1497
|
-
initialPage={currentPage}
|
|
2112
|
+
initialPage={clampSliderPage(currentPage, pageCount)}
|
|
1498
2113
|
onPageSelected={(event: PagerViewOnPageSelectedEvent) => {
|
|
1499
2114
|
const page = event.nativeEvent.position;
|
|
1500
|
-
|
|
2115
|
+
const safePage = clampSliderPage(page, pageCount);
|
|
2116
|
+
setCurrentPage(safePage);
|
|
1501
2117
|
if (registry && id) {
|
|
1502
|
-
registry.update(id,
|
|
2118
|
+
registry.update(id, safePage % pageLength, pageLength);
|
|
1503
2119
|
}
|
|
1504
2120
|
}}
|
|
1505
2121
|
>
|
|
@@ -1512,6 +2128,20 @@ function SliderWidget({
|
|
|
1512
2128
|
);
|
|
1513
2129
|
}
|
|
1514
2130
|
|
|
2131
|
+
function clampSliderPage(page: number, pageCount: number): number {
|
|
2132
|
+
if (pageCount <= 0) return 0;
|
|
2133
|
+
if (!Number.isFinite(page)) return 0;
|
|
2134
|
+
if (page < 0) return 0;
|
|
2135
|
+
if (page >= pageCount) return pageCount - 1;
|
|
2136
|
+
return Math.floor(page);
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
function normalizeLoopIndex(index: number, size: number): number {
|
|
2140
|
+
if (size <= 0 || !Number.isFinite(index)) return 0;
|
|
2141
|
+
const normalized = Math.floor(index) % size;
|
|
2142
|
+
return normalized < 0 ? normalized + size : normalized;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
1515
2145
|
function PageViewIndicator({
|
|
1516
2146
|
linkedTo,
|
|
1517
2147
|
activeColor,
|
|
@@ -1569,16 +2199,39 @@ function RadarChart({
|
|
|
1569
2199
|
}) {
|
|
1570
2200
|
const animate = properties.animate !== false;
|
|
1571
2201
|
const animationDurationMs = Number(properties.animationDurationMs ?? 800);
|
|
1572
|
-
const
|
|
2202
|
+
const reveal = useMemo(() => new Animated.Value(animate ? 0 : 1), [animate]);
|
|
2203
|
+
const [lineProgress, setLineProgress] = React.useState(animate ? 0 : 1);
|
|
1573
2204
|
|
|
1574
2205
|
React.useEffect(() => {
|
|
1575
|
-
if (!animate)
|
|
1576
|
-
|
|
2206
|
+
if (!animate) {
|
|
2207
|
+
reveal.setValue(1);
|
|
2208
|
+
setLineProgress(1);
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
reveal.setValue(0);
|
|
2213
|
+
setLineProgress(0);
|
|
2214
|
+
|
|
2215
|
+
const listenerId = reveal.addListener(({ value }) => {
|
|
2216
|
+
setLineProgress(Math.max(0, Math.min(1, value)));
|
|
2217
|
+
});
|
|
2218
|
+
|
|
2219
|
+
Animated.timing(reveal, {
|
|
1577
2220
|
toValue: 1,
|
|
1578
2221
|
duration: animationDurationMs,
|
|
1579
|
-
useNativeDriver:
|
|
2222
|
+
useNativeDriver: false,
|
|
1580
2223
|
}).start();
|
|
1581
|
-
|
|
2224
|
+
|
|
2225
|
+
return () => {
|
|
2226
|
+
reveal.removeListener(listenerId);
|
|
2227
|
+
};
|
|
2228
|
+
}, [animate, animationDurationMs, reveal]);
|
|
2229
|
+
|
|
2230
|
+
const parsedWidth = parseLayoutDimension(properties.width);
|
|
2231
|
+
const normalizedWidth = normalizeDimension(parsedWidth);
|
|
2232
|
+
const [measuredWidth, setMeasuredWidth] = React.useState(0);
|
|
2233
|
+
const width = resolveRadarWidth(parsedWidth, measuredWidth);
|
|
2234
|
+
|
|
1582
2235
|
const axes = parseRadarAxes(properties.axes);
|
|
1583
2236
|
if (axes.length < 3) {
|
|
1584
2237
|
return radarPlaceholder('Radar chart requires at least 3 axes.');
|
|
@@ -1597,12 +2250,11 @@ function RadarChart({
|
|
|
1597
2250
|
|
|
1598
2251
|
const normalizedAxes = normalizeAxisMaximums(axes, datasets);
|
|
1599
2252
|
const backgroundColor = parseColor(properties.backgroundColor);
|
|
1600
|
-
const
|
|
1601
|
-
const rawHeight = parseDimension(properties.height);
|
|
2253
|
+
const rawHeight = parseLayoutDimension(properties.height);
|
|
1602
2254
|
const height =
|
|
1603
|
-
rawHeight ===
|
|
1604
|
-
?
|
|
1605
|
-
:
|
|
2255
|
+
typeof rawHeight === 'number' && Number.isFinite(rawHeight) && rawHeight > 0
|
|
2256
|
+
? rawHeight
|
|
2257
|
+
: 300;
|
|
1606
2258
|
const padding = parseInsets(properties.padding);
|
|
1607
2259
|
const gridLevels = Math.min(
|
|
1608
2260
|
Math.max(Number(properties.gridLevels ?? 5), 1),
|
|
@@ -1628,9 +2280,11 @@ function RadarChart({
|
|
|
1628
2280
|
const shape = (properties.shape ?? 'polygon').toLowerCase();
|
|
1629
2281
|
|
|
1630
2282
|
const chartSize = Math.min(width, height);
|
|
1631
|
-
const radius =
|
|
2283
|
+
const radius = Math.max(
|
|
1632
2284
|
chartSize / 2 -
|
|
1633
|
-
|
|
2285
|
+
Math.max(padding.left, padding.right, padding.top, padding.bottom),
|
|
2286
|
+
8
|
|
2287
|
+
);
|
|
1634
2288
|
const center = { x: width / 2, y: height / 2 };
|
|
1635
2289
|
|
|
1636
2290
|
const axisAngle = (Math.PI * 2) / axes.length;
|
|
@@ -1650,7 +2304,7 @@ function RadarChart({
|
|
|
1650
2304
|
const points = dataset.values.map((value, index) => {
|
|
1651
2305
|
const axis = normalizedAxes[index];
|
|
1652
2306
|
const maxValue = axis?.maxValue ?? 0;
|
|
1653
|
-
const ratio = maxValue > 0 ? value / maxValue : 0;
|
|
2307
|
+
const ratio = (maxValue > 0 ? value / maxValue : 0) * lineProgress;
|
|
1654
2308
|
const angle = index * axisAngle - Math.PI / 2;
|
|
1655
2309
|
const x = center.x + radius * ratio * Math.cos(angle);
|
|
1656
2310
|
const y = center.y + radius * ratio * Math.sin(angle);
|
|
@@ -1689,13 +2343,19 @@ function RadarChart({
|
|
|
1689
2343
|
const chart = (
|
|
1690
2344
|
<Animated.View
|
|
1691
2345
|
style={{
|
|
1692
|
-
width,
|
|
2346
|
+
width: normalizedWidth ?? width,
|
|
1693
2347
|
height,
|
|
1694
2348
|
paddingLeft: padding.left,
|
|
1695
2349
|
paddingRight: padding.right,
|
|
1696
2350
|
paddingTop: padding.top,
|
|
1697
2351
|
paddingBottom: padding.bottom,
|
|
1698
|
-
|
|
2352
|
+
opacity: reveal,
|
|
2353
|
+
}}
|
|
2354
|
+
onLayout={(event) => {
|
|
2355
|
+
const nextWidth = Math.floor(event.nativeEvent.layout.width);
|
|
2356
|
+
if (nextWidth > 0 && nextWidth !== measuredWidth) {
|
|
2357
|
+
setMeasuredWidth(nextWidth);
|
|
2358
|
+
}
|
|
1699
2359
|
}}
|
|
1700
2360
|
>
|
|
1701
2361
|
<Svg width={width} height={height}>
|
|
@@ -1932,6 +2592,19 @@ export function normalizeAxisMaximums(
|
|
|
1932
2592
|
});
|
|
1933
2593
|
}
|
|
1934
2594
|
|
|
2595
|
+
function resolveRadarWidth(
|
|
2596
|
+
parsedWidth: number | string | undefined,
|
|
2597
|
+
measuredWidth: number
|
|
2598
|
+
): number {
|
|
2599
|
+
if (typeof parsedWidth === 'number' && Number.isFinite(parsedWidth)) {
|
|
2600
|
+
return parsedWidth;
|
|
2601
|
+
}
|
|
2602
|
+
if (measuredWidth > 0) {
|
|
2603
|
+
return measuredWidth;
|
|
2604
|
+
}
|
|
2605
|
+
return 300;
|
|
2606
|
+
}
|
|
2607
|
+
|
|
1935
2608
|
function radarPlaceholder(message: string) {
|
|
1936
2609
|
return (
|
|
1937
2610
|
<View
|