@digia-engage/core 2.0.3 → 2.2.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/DigiaEngageReactNative.podspec +1 -1
- package/android/.project +28 -0
- package/android/bin/.gradle/8.13/fileHashes/fileHashes.lock +0 -0
- package/android/bin/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/bin/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/android/bin/.project +34 -0
- package/android/bin/build/generated/source/buildConfig/debug/com/digia/engage/rn/BuildConfig.class +0 -0
- package/android/bin/build/generated/source/codegen/java/com/digia/engage/rn/NativeDigiaEngageSpec.class +0 -0
- package/android/bin/build.gradle +97 -0
- package/android/bin/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/bin/gradle/wrapper/gradle-wrapper.properties +5 -0
- package/android/bin/gradle.properties +2 -0
- package/android/bin/gradlew +185 -0
- package/android/bin/gradlew.bat +89 -0
- package/android/bin/local.properties +1 -0
- package/android/bin/settings.gradle +25 -0
- package/android/bin/src/main/AndroidManifest.xml +2 -0
- package/android/bin/src/main/java/com/digia/engage/rn/DigiaAnchorViewManager.kt +90 -0
- package/android/bin/src/main/java/com/digia/engage/rn/DigiaModule.kt +309 -0
- package/android/bin/src/main/java/com/digia/engage/rn/DigiaPackage.kt +70 -0
- package/android/bin/src/main/java/com/digia/engage/rn/DigiaSlotViewManager.kt +183 -0
- package/android/bin/src/main/java/com/digia/engage/rn/DigiaViewManager.kt +64 -0
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/digia/engage/rn/DigiaModule.kt +37 -13
- package/ios/DigiaEngageModule.m +25 -26
- package/ios/DigiaModule.swift +71 -10
- package/lib/commonjs/Digia.js +13 -2
- package/lib/commonjs/Digia.js.map +1 -1
- package/lib/commonjs/DigiaAnchorView.js +7 -3
- package/lib/commonjs/DigiaAnchorView.js.map +1 -1
- package/lib/commonjs/DigiaGuideController.js.map +1 -1
- package/lib/commonjs/DigiaProvider.js +70 -26
- package/lib/commonjs/DigiaProvider.js.map +1 -1
- package/lib/commonjs/digiaAnchorRegistry.js +15 -1
- package/lib/commonjs/digiaAnchorRegistry.js.map +1 -1
- package/lib/commonjs/interpolate.js +41 -0
- package/lib/commonjs/interpolate.js.map +1 -0
- package/lib/module/Digia.js +13 -2
- package/lib/module/Digia.js.map +1 -1
- package/lib/module/DigiaAnchorView.js +7 -3
- package/lib/module/DigiaAnchorView.js.map +1 -1
- package/lib/module/DigiaGuideController.js.map +1 -1
- package/lib/module/DigiaProvider.js +72 -27
- package/lib/module/DigiaProvider.js.map +1 -1
- package/lib/module/digiaAnchorRegistry.js +15 -1
- package/lib/module/digiaAnchorRegistry.js.map +1 -1
- package/lib/module/interpolate.js +34 -0
- package/lib/module/interpolate.js.map +1 -0
- package/lib/typescript/Digia.d.ts +1 -0
- package/lib/typescript/Digia.d.ts.map +1 -1
- package/lib/typescript/DigiaGuideController.d.ts +2 -0
- package/lib/typescript/DigiaGuideController.d.ts.map +1 -1
- package/lib/typescript/DigiaProvider.d.ts.map +1 -1
- package/lib/typescript/digiaAnchorRegistry.d.ts +4 -0
- package/lib/typescript/digiaAnchorRegistry.d.ts.map +1 -1
- package/lib/typescript/interpolate.d.ts +4 -0
- package/lib/typescript/interpolate.d.ts.map +1 -0
- package/package.json +4 -5
- package/react-native.config.js +1 -1
- package/src/Digia.ts +15 -2
- package/src/DigiaAnchorView.tsx +8 -1
- package/src/DigiaGuideController.ts +2 -0
- package/src/DigiaProvider.tsx +185 -145
- package/src/digiaAnchorRegistry.ts +16 -1
- package/src/interpolate.ts +44 -0
package/src/DigiaProvider.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
1
|
+
import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
Animated,
|
|
4
4
|
Dimensions,
|
|
@@ -17,6 +17,18 @@ import { digiaAnchorRegistry, type AnchorLayout } from './digiaAnchorRegistry';
|
|
|
17
17
|
import { digiaActionHandler, type ActionCallbacks } from './actionHandler';
|
|
18
18
|
import type { DismissReason } from './types';
|
|
19
19
|
import type { Action, SpotlightConfig, SpotlightStep, TooltipConfig, TooltipStep } from './templateTypes';
|
|
20
|
+
import { interpolateVariables, type VariableMap } from './interpolate';
|
|
21
|
+
|
|
22
|
+
// ─── Variable context ─────────────────────────────────────────────────────────
|
|
23
|
+
// Provides the active campaign's variable map to all descendant components.
|
|
24
|
+
// Avoids threading `variables` through every prop chain.
|
|
25
|
+
|
|
26
|
+
const VariableContext = createContext<VariableMap | undefined>(undefined);
|
|
27
|
+
|
|
28
|
+
function TextWithVariables({ children, ...props }: Omit<React.ComponentProps<typeof Text>, 'children'> & { children: string }) {
|
|
29
|
+
const variables = useContext(VariableContext);
|
|
30
|
+
return <Text {...props}>{interpolateVariables(children, variables)}</Text>;
|
|
31
|
+
}
|
|
20
32
|
|
|
21
33
|
// ─── @floating-ui/core platform adapter ──────────────────────────────────────
|
|
22
34
|
|
|
@@ -184,9 +196,9 @@ function ActionButton({
|
|
|
184
196
|
onPress={onPress}
|
|
185
197
|
style={[s.button, isPrimary && { backgroundColor: btnPrimaryBg }]}
|
|
186
198
|
>
|
|
187
|
-
<
|
|
199
|
+
<TextWithVariables style={{ color: isPrimary ? btnPrimaryText : btnGhostText, fontSize: 13, fontWeight: '600', fontFamily }}>
|
|
188
200
|
{action.label}
|
|
189
|
-
</
|
|
201
|
+
</TextWithVariables>
|
|
190
202
|
</Pressable>
|
|
191
203
|
);
|
|
192
204
|
}
|
|
@@ -221,10 +233,20 @@ function TooltipOverlay({
|
|
|
221
233
|
useEffect(() => {
|
|
222
234
|
setLayout(null);
|
|
223
235
|
setFloatPos(null);
|
|
224
|
-
|
|
236
|
+
let skipCached = false;
|
|
237
|
+
const unsub = digiaAnchorRegistry.subscribe(step.anchorKey, (l) => {
|
|
238
|
+
if (!skipCached) return;
|
|
239
|
+
const { width: screenW, height: screenH } = Dimensions.get('window');
|
|
240
|
+
if (l.pageY + l.height <= 0 || l.pageY >= screenH || l.pageX + l.width <= 0 || l.pageX >= screenW) {
|
|
241
|
+
digiaGuideController.cancel(request.payloadId);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
225
244
|
setLayout(l);
|
|
226
245
|
});
|
|
227
|
-
|
|
246
|
+
skipCached = true;
|
|
247
|
+
digiaAnchorRegistry.remeasure(step.anchorKey);
|
|
248
|
+
return unsub;
|
|
249
|
+
}, [step.anchorKey]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
228
250
|
|
|
229
251
|
useEffect(() => {
|
|
230
252
|
if (!layout || !floatingSize) return;
|
|
@@ -317,104 +339,110 @@ function TooltipOverlay({
|
|
|
317
339
|
}, [config.outsideTapBehavior, next, dismiss]);
|
|
318
340
|
|
|
319
341
|
return (
|
|
320
|
-
<
|
|
321
|
-
<
|
|
322
|
-
{/*
|
|
323
|
-
|
|
324
|
-
{
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
<View
|
|
401
|
-
pointerEvents="none"
|
|
402
|
-
onLayout={(e) => setFloatingSize({ w: e.nativeEvent.layout.width, h: e.nativeEvent.layout.height })}
|
|
403
|
-
style={[s.tooltipBubble, { left: -9999, top: -9999, width: tooltipW, padding: step.padding }]}
|
|
404
|
-
>
|
|
405
|
-
<Text style={{ fontSize: step.titleSize, fontFamily }}>{step.title}</Text>
|
|
406
|
-
{!!step.body && <Text style={{ fontSize: step.bodySize, fontFamily }}>{step.body}</Text>}
|
|
407
|
-
<View style={s.actionRow}>
|
|
408
|
-
{step.actions.map((a, i) => (
|
|
409
|
-
<View key={i} style={s.button}>
|
|
410
|
-
<Text style={{ fontSize: 13, fontFamily }}>{a.label}</Text>
|
|
342
|
+
<VariableContext.Provider value={request.variables}>
|
|
343
|
+
<Modal transparent statusBarTranslucent animationType="none" visible>
|
|
344
|
+
{/* pointerEvents="box-none": container passes touches through; only children intercept.
|
|
345
|
+
This prevents the invisible measurement pass from blocking the screen. */}
|
|
346
|
+
<Animated.View style={[StyleSheet.absoluteFill, { opacity: opacityAnim }]} pointerEvents="box-none">
|
|
347
|
+
{floatPos ? (
|
|
348
|
+
<>
|
|
349
|
+
{/* Full-screen backdrop: blocks all touches once bubble is positioned */}
|
|
350
|
+
<Pressable style={StyleSheet.absoluteFill} onPress={handleBackdropPress} />
|
|
351
|
+
{/* Bubble as Pressable so tapping the bubble body also advances */}
|
|
352
|
+
<Pressable
|
|
353
|
+
onLayout={(e) => {
|
|
354
|
+
const { width, height } = e.nativeEvent.layout;
|
|
355
|
+
if (floatingSize?.w !== width || floatingSize?.h !== height) {
|
|
356
|
+
setFloatingSize({ w: width, h: height });
|
|
357
|
+
}
|
|
358
|
+
}}
|
|
359
|
+
onPress={handleBackdropPress}
|
|
360
|
+
style={[
|
|
361
|
+
s.tooltipBubble,
|
|
362
|
+
{
|
|
363
|
+
left: floatPos.x,
|
|
364
|
+
top: floatPos.y,
|
|
365
|
+
width: tooltipW,
|
|
366
|
+
backgroundColor: step.backgroundColor,
|
|
367
|
+
borderRadius: step.cornerRadius,
|
|
368
|
+
borderWidth: step.borderWidth,
|
|
369
|
+
borderColor: step.borderColor,
|
|
370
|
+
padding: step.padding,
|
|
371
|
+
},
|
|
372
|
+
step.shadow && s.shadow,
|
|
373
|
+
]}
|
|
374
|
+
>
|
|
375
|
+
{showArrow && (
|
|
376
|
+
<GuideArrow
|
|
377
|
+
placement={resolvedPlacement}
|
|
378
|
+
color={step.arrowColor ?? step.backgroundColor}
|
|
379
|
+
borderColor={step.arrowBorderColor ?? step.borderColor}
|
|
380
|
+
size={arrowSize}
|
|
381
|
+
arrowOffset={arrowOffset}
|
|
382
|
+
/>
|
|
383
|
+
)}
|
|
384
|
+
<TextWithVariables style={{ color: step.titleColor, fontSize: step.titleSize, fontWeight: step.titleWeight, fontFamily }}>
|
|
385
|
+
{step.title}
|
|
386
|
+
</TextWithVariables>
|
|
387
|
+
{!!step.body && (
|
|
388
|
+
<TextWithVariables style={{ marginTop: 4, color: step.bodyColor, fontSize: step.bodySize, fontFamily }}>
|
|
389
|
+
{step.body}
|
|
390
|
+
</TextWithVariables>
|
|
391
|
+
)}
|
|
392
|
+
<View style={s.actionRow}>
|
|
393
|
+
{step.actions.map((action, i) => (
|
|
394
|
+
<ActionButton
|
|
395
|
+
key={i}
|
|
396
|
+
action={action}
|
|
397
|
+
btnPrimaryBg={step.buttonPrimaryBackgroundColor}
|
|
398
|
+
btnPrimaryText={step.buttonPrimaryTextColor}
|
|
399
|
+
btnGhostText={step.buttonGhostTextColor}
|
|
400
|
+
onPress={() => {
|
|
401
|
+
const isMultiStep = config.steps.length > 1;
|
|
402
|
+
const isLastStep = stepIndex === config.steps.length - 1;
|
|
403
|
+
const actionUrl = 'url' in action ? (action as { url: string }).url : undefined;
|
|
404
|
+
request.onExperienceEvent({ type: 'clicked', stepIndex, stepTotal: config.steps.length, anchorKey: step.anchorKey, displayStyle: 'tooltip', ctaLabel: action.label, actionType: action.type, actionUrl });
|
|
405
|
+
if (isMultiStep) {
|
|
406
|
+
request.onExperienceEvent({ type: 'step_clicked', stepIndex, stepTotal: config.steps.length, anchorKey: step.anchorKey, displayStyle: 'tooltip', ctaLabel: action.label, actionType: action.type, actionUrl });
|
|
407
|
+
}
|
|
408
|
+
if (isMultiStep && isLastStep && action.type !== 'back') {
|
|
409
|
+
request.onExperienceEvent({ type: 'completed', stepIndex, stepTotal: config.steps.length, anchorKey: step.anchorKey, displayStyle: 'tooltip' });
|
|
410
|
+
}
|
|
411
|
+
void digiaActionHandler.execute(action, {
|
|
412
|
+
campaign_id: request.payloadId,
|
|
413
|
+
campaign_key: request.campaignKey,
|
|
414
|
+
campaign_type: 'guide',
|
|
415
|
+
source: { kind: 'button', button_label: action.label },
|
|
416
|
+
step_index: stepIndex,
|
|
417
|
+
step_total: config.steps.length,
|
|
418
|
+
}, actionCallbacks());
|
|
419
|
+
}}
|
|
420
|
+
/>
|
|
421
|
+
))}
|
|
411
422
|
</View>
|
|
412
|
-
|
|
423
|
+
</Pressable>
|
|
424
|
+
</>
|
|
425
|
+
) : (
|
|
426
|
+
// Off-screen measurement pass to determine bubble size before positioning.
|
|
427
|
+
<View
|
|
428
|
+
pointerEvents="none"
|
|
429
|
+
onLayout={(e) => setFloatingSize({ w: e.nativeEvent.layout.width, h: e.nativeEvent.layout.height })}
|
|
430
|
+
style={[s.tooltipBubble, { left: -9999, top: -9999, width: tooltipW, padding: step.padding }]}
|
|
431
|
+
>
|
|
432
|
+
<TextWithVariables style={{ fontSize: step.titleSize, fontFamily }}>{step.title}</TextWithVariables>
|
|
433
|
+
{!!step.body && <TextWithVariables style={{ fontSize: step.bodySize, fontFamily }}>{step.body}</TextWithVariables>}
|
|
434
|
+
<View style={s.actionRow}>
|
|
435
|
+
{step.actions.map((a, i) => (
|
|
436
|
+
<View key={i} style={s.button}>
|
|
437
|
+
<TextWithVariables style={{ fontSize: 13, fontFamily }}>{a.label}</TextWithVariables>
|
|
438
|
+
</View>
|
|
439
|
+
))}
|
|
440
|
+
</View>
|
|
413
441
|
</View>
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
</
|
|
417
|
-
</
|
|
442
|
+
)}
|
|
443
|
+
</Animated.View>
|
|
444
|
+
</Modal>
|
|
445
|
+
</VariableContext.Provider>
|
|
418
446
|
);
|
|
419
447
|
}
|
|
420
448
|
|
|
@@ -505,12 +533,12 @@ function SpotlightCallout({
|
|
|
505
533
|
onLayout={(e) => setFloatingSize({ w: e.nativeEvent.layout.width, h: e.nativeEvent.layout.height })}
|
|
506
534
|
style={[calloutStyle, { position: 'absolute', left: -9999, top: -9999 }]}
|
|
507
535
|
>
|
|
508
|
-
<
|
|
509
|
-
{!!step.body && <
|
|
536
|
+
<TextWithVariables style={{ fontSize: step.titleSize, fontFamily }}>{step.title}</TextWithVariables>
|
|
537
|
+
{!!step.body && <TextWithVariables style={{ marginTop: 4, fontSize: step.bodySize, fontFamily }}>{step.body}</TextWithVariables>}
|
|
510
538
|
<View style={s.actionRow}>
|
|
511
539
|
{step.actions.map((a, i) => (
|
|
512
540
|
<View key={i} style={s.button}>
|
|
513
|
-
<
|
|
541
|
+
<TextWithVariables style={{ fontSize: 13, fontFamily }}>{a.label}</TextWithVariables>
|
|
514
542
|
</View>
|
|
515
543
|
))}
|
|
516
544
|
</View>
|
|
@@ -535,13 +563,13 @@ function SpotlightCallout({
|
|
|
535
563
|
arrowOffset={arrowOffset}
|
|
536
564
|
/>
|
|
537
565
|
)}
|
|
538
|
-
<
|
|
566
|
+
<TextWithVariables style={{ color: step.titleColor, fontSize: step.titleSize, fontWeight: step.titleWeight, fontFamily }}>
|
|
539
567
|
{step.title}
|
|
540
|
-
</
|
|
568
|
+
</TextWithVariables>
|
|
541
569
|
{!!step.body && (
|
|
542
|
-
<
|
|
570
|
+
<TextWithVariables style={{ marginTop: 4, color: step.bodyColor, fontSize: step.bodySize, fontFamily }}>
|
|
543
571
|
{step.body}
|
|
544
|
-
</
|
|
572
|
+
</TextWithVariables>
|
|
545
573
|
)}
|
|
546
574
|
<View style={s.actionRow}>
|
|
547
575
|
{step.actions.map((action, i) => (
|
|
@@ -575,10 +603,20 @@ function SpotlightOverlay({
|
|
|
575
603
|
|
|
576
604
|
useEffect(() => {
|
|
577
605
|
setLayout(null);
|
|
578
|
-
|
|
606
|
+
let skipCached = false;
|
|
607
|
+
const unsub = digiaAnchorRegistry.subscribe(step.anchorKey, (l) => {
|
|
608
|
+
if (!skipCached) return;
|
|
609
|
+
const { width: screenW, height: screenH } = Dimensions.get('window');
|
|
610
|
+
if (l.pageY + l.height <= 0 || l.pageY >= screenH || l.pageX + l.width <= 0 || l.pageX >= screenW) {
|
|
611
|
+
digiaGuideController.cancel(request.payloadId);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
579
614
|
setLayout(l);
|
|
580
615
|
});
|
|
581
|
-
|
|
616
|
+
skipCached = true;
|
|
617
|
+
digiaAnchorRegistry.remeasure(step.anchorKey);
|
|
618
|
+
return unsub;
|
|
619
|
+
}, [step.anchorKey]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
582
620
|
|
|
583
621
|
useEffect(() => {
|
|
584
622
|
if (layout && pendingFadeIn.current) {
|
|
@@ -676,42 +714,44 @@ function SpotlightOverlay({
|
|
|
676
714
|
: '';
|
|
677
715
|
|
|
678
716
|
return (
|
|
679
|
-
<
|
|
680
|
-
<
|
|
681
|
-
{
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
fillRule="evenodd"
|
|
691
|
-
d={`${screenPath} ${cutoutPath}`}
|
|
692
|
-
fill={step.overlayColor}
|
|
693
|
-
fillOpacity={step.overlayOpacity}
|
|
694
|
-
/>
|
|
695
|
-
{step.highlightGlowWidth > 0 && (
|
|
717
|
+
<VariableContext.Provider value={request.variables}>
|
|
718
|
+
<Modal transparent statusBarTranslucent animationType="none" visible>
|
|
719
|
+
<Animated.View style={[StyleSheet.absoluteFill, { opacity: opacityAnim }]} pointerEvents="box-none">
|
|
720
|
+
{layout && (
|
|
721
|
+
<>
|
|
722
|
+
<Svg
|
|
723
|
+
width={screenW}
|
|
724
|
+
height={screenH}
|
|
725
|
+
style={StyleSheet.absoluteFill}
|
|
726
|
+
pointerEvents="none"
|
|
727
|
+
>
|
|
696
728
|
<Path
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
729
|
+
fillRule="evenodd"
|
|
730
|
+
d={`${screenPath} ${cutoutPath}`}
|
|
731
|
+
fill={step.overlayColor}
|
|
732
|
+
fillOpacity={step.overlayOpacity}
|
|
701
733
|
/>
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
734
|
+
{step.highlightGlowWidth > 0 && (
|
|
735
|
+
<Path
|
|
736
|
+
d={cutoutPath}
|
|
737
|
+
fill="none"
|
|
738
|
+
stroke={step.highlightGlowColor}
|
|
739
|
+
strokeWidth={step.highlightGlowWidth}
|
|
740
|
+
/>
|
|
741
|
+
)}
|
|
742
|
+
</Svg>
|
|
743
|
+
{/* Backdrop with configurable outside-tap behaviour */}
|
|
744
|
+
<Pressable style={StyleSheet.absoluteFill} onPress={handleBackdropPress} />
|
|
745
|
+
<SpotlightCallout
|
|
746
|
+
step={step}
|
|
747
|
+
layout={layout}
|
|
748
|
+
onActionPress={handleActionPress}
|
|
749
|
+
/>
|
|
750
|
+
</>
|
|
751
|
+
)}
|
|
752
|
+
</Animated.View>
|
|
753
|
+
</Modal>
|
|
754
|
+
</VariableContext.Provider>
|
|
715
755
|
);
|
|
716
756
|
}
|
|
717
757
|
|
|
@@ -773,4 +813,4 @@ const s = StyleSheet.create({
|
|
|
773
813
|
borderRadius: 8,
|
|
774
814
|
paddingHorizontal: 12,
|
|
775
815
|
},
|
|
776
|
-
});
|
|
816
|
+
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
type AnchorLayout = { pageX: number; pageY: number; width: number; height: number }
|
|
2
2
|
type Listener = (layout: AnchorLayout) => void
|
|
3
|
+
type MeasureCallback = () => void
|
|
3
4
|
|
|
4
5
|
const _layouts = new Map<string, AnchorLayout>()
|
|
5
6
|
const _listeners = new Map<string, Listener>()
|
|
7
|
+
const _measureCallbacks = new Map<string, MeasureCallback>()
|
|
6
8
|
|
|
7
9
|
const setLayout = (key: string, layout: AnchorLayout) => {
|
|
8
10
|
_layouts.set(key, layout)
|
|
@@ -21,7 +23,20 @@ const subscribe = (key: string, listener: Listener): () => void => {
|
|
|
21
23
|
const remove = (key: string) => {
|
|
22
24
|
_layouts.delete(key)
|
|
23
25
|
_listeners.delete(key)
|
|
26
|
+
_measureCallbacks.delete(key)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const registerMeasure = (key: string, cb: MeasureCallback) => {
|
|
30
|
+
_measureCallbacks.set(key, cb)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const unregisterMeasure = (key: string, cb: MeasureCallback) => {
|
|
34
|
+
if (_measureCallbacks.get(key) === cb) _measureCallbacks.delete(key)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const remeasure = (key: string) => {
|
|
38
|
+
_measureCallbacks.get(key)?.()
|
|
24
39
|
}
|
|
25
40
|
|
|
26
41
|
export type { AnchorLayout }
|
|
27
|
-
export const digiaAnchorRegistry = { setLayout, getLayout, subscribe, remove }
|
|
42
|
+
export const digiaAnchorRegistry = { setLayout, getLayout, subscribe, remove, registerMeasure, unregisterMeasure, remeasure }
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type VariableMap = Record<string, string>;
|
|
2
|
+
|
|
3
|
+
const PLACEHOLDER_PATTERN = /\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
|
|
4
|
+
|
|
5
|
+
export function interpolateVariables(
|
|
6
|
+
text: string,
|
|
7
|
+
variables: VariableMap | undefined,
|
|
8
|
+
): string {
|
|
9
|
+
if (!variables) return text;
|
|
10
|
+
return text.replace(PLACEHOLDER_PATTERN, (match, name: string) => {
|
|
11
|
+
const value = variables[name];
|
|
12
|
+
return value === undefined ? '' : value;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function parseVariableMap(raw: unknown): VariableMap | undefined {
|
|
17
|
+
const source = parseVariableSource(raw);
|
|
18
|
+
if (!source) return undefined;
|
|
19
|
+
|
|
20
|
+
const variables: VariableMap = {};
|
|
21
|
+
for (const [key, value] of Object.entries(source)) {
|
|
22
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
|
|
23
|
+
if (typeof value === 'string') variables[key] = value;
|
|
24
|
+
else if (typeof value === 'number' || typeof value === 'boolean') variables[key] = String(value);
|
|
25
|
+
}
|
|
26
|
+
return Object.keys(variables).length > 0 ? variables : undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseVariableSource(raw: unknown): Record<string, unknown> | undefined {
|
|
30
|
+
if (!raw) return undefined;
|
|
31
|
+
if (typeof raw === 'string') {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(raw);
|
|
34
|
+
return isPlainObject(parsed) ? parsed : undefined;
|
|
35
|
+
} catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return isPlainObject(raw) ? raw : undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
43
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
44
|
+
}
|