@feedvalue/core 0.1.11 → 0.1.15
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/dist/index.cjs +210 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +81 -4
- package/dist/index.d.ts +81 -4
- package/dist/index.js +209 -22
- package/dist/index.js.map +1 -1
- package/dist/umd/index.min.js +3 -3
- package/dist/umd/index.min.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,9 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @feedvalue/core - Context Capture
|
|
3
|
+
*
|
|
4
|
+
* Captures DOM context from a trigger element for enhanced feedback metadata.
|
|
5
|
+
* This allows understanding WHERE on a page the user reacted, not just which page.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Context capture configuration
|
|
9
|
+
*/
|
|
10
|
+
interface ContextCaptureConfig {
|
|
11
|
+
/** Enable automatic context capture (default: true) */
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
/** Maximum parent traversal depth (default: 5) */
|
|
14
|
+
maxDepth: number;
|
|
15
|
+
/** Maximum heading text length (default: 100) */
|
|
16
|
+
maxHeadingLength: number;
|
|
17
|
+
/** Data attribute whitelist (default: ['data-section', 'data-feature', 'data-component']) */
|
|
18
|
+
dataAttributeWhitelist: string[];
|
|
19
|
+
}
|
|
20
|
+
declare const DEFAULT_CONTEXT_CAPTURE_CONFIG: ContextCaptureConfig;
|
|
21
|
+
/**
|
|
22
|
+
* Captured context data from DOM traversal
|
|
23
|
+
*/
|
|
24
|
+
interface CapturedContext {
|
|
25
|
+
/** ID of nearest parent element with an id attribute */
|
|
26
|
+
sectionId?: string;
|
|
27
|
+
/** Tag name of the section element (e.g., 'section', 'article', 'div') */
|
|
28
|
+
sectionTag?: string;
|
|
29
|
+
/** Text content of nearest heading (h1-h6) */
|
|
30
|
+
nearestHeading?: string;
|
|
31
|
+
/** Level of the nearest heading (1-6) */
|
|
32
|
+
headingLevel?: number;
|
|
33
|
+
/** Captured data-* attributes from element and parents */
|
|
34
|
+
dataAttributes?: Record<string, string>;
|
|
35
|
+
/** CSS selector path to the trigger element (for debugging) */
|
|
36
|
+
cssSelector?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Capture DOM context from a trigger element
|
|
40
|
+
*
|
|
41
|
+
* @param triggerElement - The element that triggered the reaction (e.g., the button)
|
|
42
|
+
* @param config - Configuration for context capture
|
|
43
|
+
* @returns Captured context object or null if disabled/unavailable
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* // Capture context from a button click
|
|
48
|
+
* const context = captureContext(event.currentTarget, config);
|
|
49
|
+
* // Returns: { sectionId: 'installation', nearestHeading: 'Installation', ... }
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
declare function captureContext(triggerElement: Element | null, config?: ContextCaptureConfig): CapturedContext | null;
|
|
53
|
+
|
|
1
54
|
/**
|
|
2
55
|
* @feedvalue/core - Type Definitions
|
|
3
56
|
*
|
|
4
57
|
* Core types for the FeedValue SDK. These types are shared across
|
|
5
58
|
* all framework packages (React, Vue) and the vanilla API.
|
|
6
59
|
*/
|
|
60
|
+
|
|
7
61
|
/**
|
|
8
62
|
* Widget position on screen
|
|
9
63
|
*/
|
|
@@ -43,6 +97,8 @@ interface WidgetStyling {
|
|
|
43
97
|
backgroundColor: string;
|
|
44
98
|
textColor: string;
|
|
45
99
|
buttonTextColor: string;
|
|
100
|
+
borderColor: string;
|
|
101
|
+
borderWidth: string;
|
|
46
102
|
borderRadius: string;
|
|
47
103
|
customCSS?: string | undefined;
|
|
48
104
|
}
|
|
@@ -91,6 +147,14 @@ interface FeedValueOptions {
|
|
|
91
147
|
* @default false
|
|
92
148
|
*/
|
|
93
149
|
headless?: boolean | undefined;
|
|
150
|
+
/**
|
|
151
|
+
* Context capture configuration for reaction widgets.
|
|
152
|
+
* When enabled, captures DOM context (section ID, nearest heading, data attributes)
|
|
153
|
+
* from the trigger element when react() is called.
|
|
154
|
+
*
|
|
155
|
+
* @default { enabled: true, maxDepth: 5, maxHeadingLength: 100, dataAttributeWhitelist: ['data-section', 'data-feature', 'data-component', 'data-fv-section', 'data-fv-feature'] }
|
|
156
|
+
*/
|
|
157
|
+
contextCapture?: Partial<ContextCaptureConfig> | undefined;
|
|
94
158
|
}
|
|
95
159
|
/**
|
|
96
160
|
* Runtime configuration (can be changed after init)
|
|
@@ -425,10 +489,11 @@ interface FeedValueInstance {
|
|
|
425
489
|
/**
|
|
426
490
|
* Submit a reaction.
|
|
427
491
|
* @param value - Selected reaction option value
|
|
428
|
-
* @param options - Optional follow-up text
|
|
492
|
+
* @param options - Optional follow-up text and trigger element for context capture
|
|
429
493
|
*/
|
|
430
494
|
react(value: string, options?: {
|
|
431
495
|
followUp?: string;
|
|
496
|
+
triggerElement?: Element | null;
|
|
432
497
|
}): Promise<void>;
|
|
433
498
|
/**
|
|
434
499
|
* Check if widget is a reaction type
|
|
@@ -450,6 +515,8 @@ interface FeedValueInstance {
|
|
|
450
515
|
setConfig(config: Partial<FeedValueConfig>): void;
|
|
451
516
|
/** Get current configuration */
|
|
452
517
|
getConfig(): FeedValueConfig;
|
|
518
|
+
/** Get widget configuration (from API) */
|
|
519
|
+
getWidgetConfig(): WidgetConfig | null;
|
|
453
520
|
/** Subscribe to state changes */
|
|
454
521
|
subscribe(callback: () => void): () => void;
|
|
455
522
|
/** Get current state snapshot */
|
|
@@ -493,6 +560,7 @@ declare class FeedValue implements FeedValueInstance {
|
|
|
493
560
|
private readonly apiClient;
|
|
494
561
|
private readonly emitter;
|
|
495
562
|
private readonly headless;
|
|
563
|
+
private readonly contextCaptureConfig;
|
|
496
564
|
private config;
|
|
497
565
|
private widgetConfig;
|
|
498
566
|
private state;
|
|
@@ -505,6 +573,7 @@ declare class FeedValue implements FeedValueInstance {
|
|
|
505
573
|
private modal;
|
|
506
574
|
private overlay;
|
|
507
575
|
private stylesInjected;
|
|
576
|
+
private viewportResizeCleanup;
|
|
508
577
|
private autoCloseTimeout;
|
|
509
578
|
private isDestroyed;
|
|
510
579
|
/**
|
|
@@ -567,10 +636,11 @@ declare class FeedValue implements FeedValueInstance {
|
|
|
567
636
|
/**
|
|
568
637
|
* Submit a reaction.
|
|
569
638
|
* @param value - Selected reaction option value
|
|
570
|
-
* @param options - Optional follow-up text
|
|
639
|
+
* @param options - Optional follow-up text and trigger element for context capture
|
|
571
640
|
*/
|
|
572
641
|
react(value: string, options?: {
|
|
573
642
|
followUp?: string;
|
|
643
|
+
triggerElement?: Element | null;
|
|
574
644
|
}): Promise<void>;
|
|
575
645
|
/**
|
|
576
646
|
* Check if widget is a reaction type
|
|
@@ -645,8 +715,15 @@ declare class FeedValue implements FeedValueInstance {
|
|
|
645
715
|
*/
|
|
646
716
|
private getModalPositionStyles;
|
|
647
717
|
/**
|
|
648
|
-
*
|
|
718
|
+
* Smooth viewport resize compensation for mobile browser toolbar show/hide.
|
|
719
|
+
*
|
|
720
|
+
* When Chrome/Safari's bottom toolbar appears or disappears, the layout viewport
|
|
721
|
+
* resizes and position:fixed elements jump. This detects toolbar-sized height
|
|
722
|
+
* changes and applies an inverse CSS translate to cancel the jump, then animates
|
|
723
|
+
* back to the natural position. Uses the `translate` CSS property (separate from
|
|
724
|
+
* `transform`) to avoid conflicting with the hover transform on the trigger.
|
|
649
725
|
*/
|
|
726
|
+
private setupViewportCompensation;
|
|
650
727
|
/**
|
|
651
728
|
* SVG icons for trigger button - must match Lucide icons used in frontend-web
|
|
652
729
|
* chat = MessageCircle, message = MessageSquare, feedback = MessagesSquare,
|
|
@@ -848,4 +925,4 @@ declare function generateFingerprint(): string;
|
|
|
848
925
|
*/
|
|
849
926
|
declare function clearFingerprint(): void;
|
|
850
927
|
|
|
851
|
-
export { ApiClient, type ButtonSize, type ConfigResponse, type CustomField, type CustomFieldType, DEFAULT_API_BASE_URL, type EmojiSentiment, type EventHandler, FeedValue, type FeedValueConfig, type FeedValueEvents, type FeedValueInstance, type FeedValueOptions, type FeedValueState, type FeedbackData, type FeedbackMetadata, type FeedbackResponse, type FollowUpTrigger, NEGATIVE_OPTIONS_MAP, type ReactionBorderRadius, type ReactionBorderWidth, type ReactionConfig, type ReactionData, type ReactionMetadata, type ReactionOption, type ReactionResponse, type ReactionState, type ReactionStyling, type ReactionTemplate, type SubmissionUserData, type TriggerIconType, TypedEventEmitter, type UserData, type UserTraits, type WidgetConfig, type WidgetPosition, type WidgetStyling, type WidgetTheme, type WidgetType, type WidgetUIConfig, clearFingerprint, generateFingerprint };
|
|
928
|
+
export { ApiClient, type ButtonSize, type CapturedContext, type ConfigResponse, type ContextCaptureConfig, type CustomField, type CustomFieldType, DEFAULT_API_BASE_URL, DEFAULT_CONTEXT_CAPTURE_CONFIG, type EmojiSentiment, type EventHandler, FeedValue, type FeedValueConfig, type FeedValueEvents, type FeedValueInstance, type FeedValueOptions, type FeedValueState, type FeedbackData, type FeedbackMetadata, type FeedbackResponse, type FollowUpTrigger, NEGATIVE_OPTIONS_MAP, type ReactionBorderRadius, type ReactionBorderWidth, type ReactionConfig, type ReactionData, type ReactionMetadata, type ReactionOption, type ReactionResponse, type ReactionState, type ReactionStyling, type ReactionTemplate, type SubmissionUserData, type TriggerIconType, TypedEventEmitter, type UserData, type UserTraits, type WidgetConfig, type WidgetPosition, type WidgetStyling, type WidgetTheme, type WidgetType, type WidgetUIConfig, captureContext, clearFingerprint, generateFingerprint };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @feedvalue/core - Context Capture
|
|
3
|
+
*
|
|
4
|
+
* Captures DOM context from a trigger element for enhanced feedback metadata.
|
|
5
|
+
* This allows understanding WHERE on a page the user reacted, not just which page.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Context capture configuration
|
|
9
|
+
*/
|
|
10
|
+
interface ContextCaptureConfig {
|
|
11
|
+
/** Enable automatic context capture (default: true) */
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
/** Maximum parent traversal depth (default: 5) */
|
|
14
|
+
maxDepth: number;
|
|
15
|
+
/** Maximum heading text length (default: 100) */
|
|
16
|
+
maxHeadingLength: number;
|
|
17
|
+
/** Data attribute whitelist (default: ['data-section', 'data-feature', 'data-component']) */
|
|
18
|
+
dataAttributeWhitelist: string[];
|
|
19
|
+
}
|
|
20
|
+
declare const DEFAULT_CONTEXT_CAPTURE_CONFIG: ContextCaptureConfig;
|
|
21
|
+
/**
|
|
22
|
+
* Captured context data from DOM traversal
|
|
23
|
+
*/
|
|
24
|
+
interface CapturedContext {
|
|
25
|
+
/** ID of nearest parent element with an id attribute */
|
|
26
|
+
sectionId?: string;
|
|
27
|
+
/** Tag name of the section element (e.g., 'section', 'article', 'div') */
|
|
28
|
+
sectionTag?: string;
|
|
29
|
+
/** Text content of nearest heading (h1-h6) */
|
|
30
|
+
nearestHeading?: string;
|
|
31
|
+
/** Level of the nearest heading (1-6) */
|
|
32
|
+
headingLevel?: number;
|
|
33
|
+
/** Captured data-* attributes from element and parents */
|
|
34
|
+
dataAttributes?: Record<string, string>;
|
|
35
|
+
/** CSS selector path to the trigger element (for debugging) */
|
|
36
|
+
cssSelector?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Capture DOM context from a trigger element
|
|
40
|
+
*
|
|
41
|
+
* @param triggerElement - The element that triggered the reaction (e.g., the button)
|
|
42
|
+
* @param config - Configuration for context capture
|
|
43
|
+
* @returns Captured context object or null if disabled/unavailable
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* // Capture context from a button click
|
|
48
|
+
* const context = captureContext(event.currentTarget, config);
|
|
49
|
+
* // Returns: { sectionId: 'installation', nearestHeading: 'Installation', ... }
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
declare function captureContext(triggerElement: Element | null, config?: ContextCaptureConfig): CapturedContext | null;
|
|
53
|
+
|
|
1
54
|
/**
|
|
2
55
|
* @feedvalue/core - Type Definitions
|
|
3
56
|
*
|
|
4
57
|
* Core types for the FeedValue SDK. These types are shared across
|
|
5
58
|
* all framework packages (React, Vue) and the vanilla API.
|
|
6
59
|
*/
|
|
60
|
+
|
|
7
61
|
/**
|
|
8
62
|
* Widget position on screen
|
|
9
63
|
*/
|
|
@@ -43,6 +97,8 @@ interface WidgetStyling {
|
|
|
43
97
|
backgroundColor: string;
|
|
44
98
|
textColor: string;
|
|
45
99
|
buttonTextColor: string;
|
|
100
|
+
borderColor: string;
|
|
101
|
+
borderWidth: string;
|
|
46
102
|
borderRadius: string;
|
|
47
103
|
customCSS?: string | undefined;
|
|
48
104
|
}
|
|
@@ -91,6 +147,14 @@ interface FeedValueOptions {
|
|
|
91
147
|
* @default false
|
|
92
148
|
*/
|
|
93
149
|
headless?: boolean | undefined;
|
|
150
|
+
/**
|
|
151
|
+
* Context capture configuration for reaction widgets.
|
|
152
|
+
* When enabled, captures DOM context (section ID, nearest heading, data attributes)
|
|
153
|
+
* from the trigger element when react() is called.
|
|
154
|
+
*
|
|
155
|
+
* @default { enabled: true, maxDepth: 5, maxHeadingLength: 100, dataAttributeWhitelist: ['data-section', 'data-feature', 'data-component', 'data-fv-section', 'data-fv-feature'] }
|
|
156
|
+
*/
|
|
157
|
+
contextCapture?: Partial<ContextCaptureConfig> | undefined;
|
|
94
158
|
}
|
|
95
159
|
/**
|
|
96
160
|
* Runtime configuration (can be changed after init)
|
|
@@ -425,10 +489,11 @@ interface FeedValueInstance {
|
|
|
425
489
|
/**
|
|
426
490
|
* Submit a reaction.
|
|
427
491
|
* @param value - Selected reaction option value
|
|
428
|
-
* @param options - Optional follow-up text
|
|
492
|
+
* @param options - Optional follow-up text and trigger element for context capture
|
|
429
493
|
*/
|
|
430
494
|
react(value: string, options?: {
|
|
431
495
|
followUp?: string;
|
|
496
|
+
triggerElement?: Element | null;
|
|
432
497
|
}): Promise<void>;
|
|
433
498
|
/**
|
|
434
499
|
* Check if widget is a reaction type
|
|
@@ -450,6 +515,8 @@ interface FeedValueInstance {
|
|
|
450
515
|
setConfig(config: Partial<FeedValueConfig>): void;
|
|
451
516
|
/** Get current configuration */
|
|
452
517
|
getConfig(): FeedValueConfig;
|
|
518
|
+
/** Get widget configuration (from API) */
|
|
519
|
+
getWidgetConfig(): WidgetConfig | null;
|
|
453
520
|
/** Subscribe to state changes */
|
|
454
521
|
subscribe(callback: () => void): () => void;
|
|
455
522
|
/** Get current state snapshot */
|
|
@@ -493,6 +560,7 @@ declare class FeedValue implements FeedValueInstance {
|
|
|
493
560
|
private readonly apiClient;
|
|
494
561
|
private readonly emitter;
|
|
495
562
|
private readonly headless;
|
|
563
|
+
private readonly contextCaptureConfig;
|
|
496
564
|
private config;
|
|
497
565
|
private widgetConfig;
|
|
498
566
|
private state;
|
|
@@ -505,6 +573,7 @@ declare class FeedValue implements FeedValueInstance {
|
|
|
505
573
|
private modal;
|
|
506
574
|
private overlay;
|
|
507
575
|
private stylesInjected;
|
|
576
|
+
private viewportResizeCleanup;
|
|
508
577
|
private autoCloseTimeout;
|
|
509
578
|
private isDestroyed;
|
|
510
579
|
/**
|
|
@@ -567,10 +636,11 @@ declare class FeedValue implements FeedValueInstance {
|
|
|
567
636
|
/**
|
|
568
637
|
* Submit a reaction.
|
|
569
638
|
* @param value - Selected reaction option value
|
|
570
|
-
* @param options - Optional follow-up text
|
|
639
|
+
* @param options - Optional follow-up text and trigger element for context capture
|
|
571
640
|
*/
|
|
572
641
|
react(value: string, options?: {
|
|
573
642
|
followUp?: string;
|
|
643
|
+
triggerElement?: Element | null;
|
|
574
644
|
}): Promise<void>;
|
|
575
645
|
/**
|
|
576
646
|
* Check if widget is a reaction type
|
|
@@ -645,8 +715,15 @@ declare class FeedValue implements FeedValueInstance {
|
|
|
645
715
|
*/
|
|
646
716
|
private getModalPositionStyles;
|
|
647
717
|
/**
|
|
648
|
-
*
|
|
718
|
+
* Smooth viewport resize compensation for mobile browser toolbar show/hide.
|
|
719
|
+
*
|
|
720
|
+
* When Chrome/Safari's bottom toolbar appears or disappears, the layout viewport
|
|
721
|
+
* resizes and position:fixed elements jump. This detects toolbar-sized height
|
|
722
|
+
* changes and applies an inverse CSS translate to cancel the jump, then animates
|
|
723
|
+
* back to the natural position. Uses the `translate` CSS property (separate from
|
|
724
|
+
* `transform`) to avoid conflicting with the hover transform on the trigger.
|
|
649
725
|
*/
|
|
726
|
+
private setupViewportCompensation;
|
|
650
727
|
/**
|
|
651
728
|
* SVG icons for trigger button - must match Lucide icons used in frontend-web
|
|
652
729
|
* chat = MessageCircle, message = MessageSquare, feedback = MessagesSquare,
|
|
@@ -848,4 +925,4 @@ declare function generateFingerprint(): string;
|
|
|
848
925
|
*/
|
|
849
926
|
declare function clearFingerprint(): void;
|
|
850
927
|
|
|
851
|
-
export { ApiClient, type ButtonSize, type ConfigResponse, type CustomField, type CustomFieldType, DEFAULT_API_BASE_URL, type EmojiSentiment, type EventHandler, FeedValue, type FeedValueConfig, type FeedValueEvents, type FeedValueInstance, type FeedValueOptions, type FeedValueState, type FeedbackData, type FeedbackMetadata, type FeedbackResponse, type FollowUpTrigger, NEGATIVE_OPTIONS_MAP, type ReactionBorderRadius, type ReactionBorderWidth, type ReactionConfig, type ReactionData, type ReactionMetadata, type ReactionOption, type ReactionResponse, type ReactionState, type ReactionStyling, type ReactionTemplate, type SubmissionUserData, type TriggerIconType, TypedEventEmitter, type UserData, type UserTraits, type WidgetConfig, type WidgetPosition, type WidgetStyling, type WidgetTheme, type WidgetType, type WidgetUIConfig, clearFingerprint, generateFingerprint };
|
|
928
|
+
export { ApiClient, type ButtonSize, type CapturedContext, type ConfigResponse, type ContextCaptureConfig, type CustomField, type CustomFieldType, DEFAULT_API_BASE_URL, DEFAULT_CONTEXT_CAPTURE_CONFIG, type EmojiSentiment, type EventHandler, FeedValue, type FeedValueConfig, type FeedValueEvents, type FeedValueInstance, type FeedValueOptions, type FeedValueState, type FeedbackData, type FeedbackMetadata, type FeedbackResponse, type FollowUpTrigger, NEGATIVE_OPTIONS_MAP, type ReactionBorderRadius, type ReactionBorderWidth, type ReactionConfig, type ReactionData, type ReactionMetadata, type ReactionOption, type ReactionResponse, type ReactionState, type ReactionStyling, type ReactionTemplate, type SubmissionUserData, type TriggerIconType, TypedEventEmitter, type UserData, type UserTraits, type WidgetConfig, type WidgetPosition, type WidgetStyling, type WidgetTheme, type WidgetType, type WidgetUIConfig, captureContext, clearFingerprint, generateFingerprint };
|
package/dist/index.js
CHANGED
|
@@ -453,6 +453,123 @@ function clearFingerprint() {
|
|
|
453
453
|
}
|
|
454
454
|
}
|
|
455
455
|
|
|
456
|
+
// src/context-capture.ts
|
|
457
|
+
var DEFAULT_CONTEXT_CAPTURE_CONFIG = {
|
|
458
|
+
enabled: true,
|
|
459
|
+
maxDepth: 5,
|
|
460
|
+
maxHeadingLength: 100,
|
|
461
|
+
dataAttributeWhitelist: [
|
|
462
|
+
"data-section",
|
|
463
|
+
"data-feature",
|
|
464
|
+
"data-component",
|
|
465
|
+
"data-fv-section",
|
|
466
|
+
"data-fv-feature"
|
|
467
|
+
]
|
|
468
|
+
};
|
|
469
|
+
function findNearestWithId(element, maxDepth) {
|
|
470
|
+
let current = element;
|
|
471
|
+
let depth = 0;
|
|
472
|
+
while (current && depth < maxDepth) {
|
|
473
|
+
if (current.id) {
|
|
474
|
+
return current;
|
|
475
|
+
}
|
|
476
|
+
current = current.parentElement;
|
|
477
|
+
depth++;
|
|
478
|
+
}
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
function findNearestHeading(element, section, maxDepth) {
|
|
482
|
+
if (section) {
|
|
483
|
+
const heading = section.querySelector("h1, h2, h3, h4, h5, h6");
|
|
484
|
+
if (heading) return heading;
|
|
485
|
+
}
|
|
486
|
+
let current = element;
|
|
487
|
+
let depth = 0;
|
|
488
|
+
while (current && depth < maxDepth) {
|
|
489
|
+
let sibling = current.previousElementSibling;
|
|
490
|
+
while (sibling) {
|
|
491
|
+
if (/^H[1-6]$/.test(sibling.tagName)) {
|
|
492
|
+
return sibling;
|
|
493
|
+
}
|
|
494
|
+
sibling = sibling.previousElementSibling;
|
|
495
|
+
}
|
|
496
|
+
current = current.parentElement;
|
|
497
|
+
depth++;
|
|
498
|
+
}
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
function captureDataAttributes(element, whitelist, maxDepth) {
|
|
502
|
+
const result = {};
|
|
503
|
+
let current = element;
|
|
504
|
+
let depth = 0;
|
|
505
|
+
while (current && depth < maxDepth) {
|
|
506
|
+
for (const attr of Array.from(current.attributes)) {
|
|
507
|
+
if (whitelist.includes(attr.name) && !result[attr.name]) {
|
|
508
|
+
result[attr.name] = attr.value;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
current = current.parentElement;
|
|
512
|
+
depth++;
|
|
513
|
+
}
|
|
514
|
+
return result;
|
|
515
|
+
}
|
|
516
|
+
function generateSelector(element, maxDepth) {
|
|
517
|
+
const parts = [];
|
|
518
|
+
let current = element;
|
|
519
|
+
let depth = 0;
|
|
520
|
+
while (current && depth < maxDepth && current !== document.body) {
|
|
521
|
+
let selector = current.tagName.toLowerCase();
|
|
522
|
+
if (current.id) {
|
|
523
|
+
selector = `#${current.id}`;
|
|
524
|
+
parts.unshift(selector);
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
if (current.className && typeof current.className === "string") {
|
|
528
|
+
const classes = current.className.split(" ").filter((c) => c.trim()).slice(0, 2);
|
|
529
|
+
if (classes.length) {
|
|
530
|
+
selector += "." + classes.join(".");
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
parts.unshift(selector);
|
|
534
|
+
current = current.parentElement;
|
|
535
|
+
depth++;
|
|
536
|
+
}
|
|
537
|
+
return parts.join(" > ");
|
|
538
|
+
}
|
|
539
|
+
function truncate(str, maxLength) {
|
|
540
|
+
if (str.length <= maxLength) return str;
|
|
541
|
+
return str.substring(0, maxLength - 3) + "...";
|
|
542
|
+
}
|
|
543
|
+
function captureContext(triggerElement, config = DEFAULT_CONTEXT_CAPTURE_CONFIG) {
|
|
544
|
+
if (!config.enabled || !triggerElement || typeof document === "undefined") {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
const context = {};
|
|
548
|
+
const sectionWithId = findNearestWithId(triggerElement, config.maxDepth);
|
|
549
|
+
if (sectionWithId) {
|
|
550
|
+
context.sectionId = sectionWithId.id;
|
|
551
|
+
context.sectionTag = sectionWithId.tagName.toLowerCase();
|
|
552
|
+
}
|
|
553
|
+
const heading = findNearestHeading(triggerElement, sectionWithId, config.maxDepth);
|
|
554
|
+
if (heading) {
|
|
555
|
+
context.nearestHeading = truncate(heading.textContent?.trim() || "", config.maxHeadingLength);
|
|
556
|
+
const level = heading.tagName[1];
|
|
557
|
+
if (level) {
|
|
558
|
+
context.headingLevel = parseInt(level, 10);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const dataAttrs = captureDataAttributes(
|
|
562
|
+
triggerElement,
|
|
563
|
+
config.dataAttributeWhitelist,
|
|
564
|
+
config.maxDepth
|
|
565
|
+
);
|
|
566
|
+
if (Object.keys(dataAttrs).length > 0) {
|
|
567
|
+
context.dataAttributes = dataAttrs;
|
|
568
|
+
}
|
|
569
|
+
context.cssSelector = generateSelector(triggerElement, 3);
|
|
570
|
+
return Object.keys(context).length > 0 ? context : null;
|
|
571
|
+
}
|
|
572
|
+
|
|
456
573
|
// src/feedvalue.ts
|
|
457
574
|
var SUCCESS_AUTO_CLOSE_DELAY_MS = 3e3;
|
|
458
575
|
var VALID_SENTIMENTS = ["angry", "disappointed", "satisfied", "excited"];
|
|
@@ -475,6 +592,7 @@ var _FeedValue = class _FeedValue {
|
|
|
475
592
|
__publicField(this, "apiClient");
|
|
476
593
|
__publicField(this, "emitter");
|
|
477
594
|
__publicField(this, "headless");
|
|
595
|
+
__publicField(this, "contextCaptureConfig");
|
|
478
596
|
__publicField(this, "config");
|
|
479
597
|
__publicField(this, "widgetConfig", null);
|
|
480
598
|
// State
|
|
@@ -497,6 +615,8 @@ var _FeedValue = class _FeedValue {
|
|
|
497
615
|
__publicField(this, "modal", null);
|
|
498
616
|
__publicField(this, "overlay", null);
|
|
499
617
|
__publicField(this, "stylesInjected", false);
|
|
618
|
+
// Viewport resize compensation cleanup
|
|
619
|
+
__publicField(this, "viewportResizeCleanup", null);
|
|
500
620
|
// Auto-close timeout reference (for cleanup on destroy)
|
|
501
621
|
__publicField(this, "autoCloseTimeout", null);
|
|
502
622
|
// Destroyed flag - guards async continuations (fixes React StrictMode race condition)
|
|
@@ -504,6 +624,10 @@ var _FeedValue = class _FeedValue {
|
|
|
504
624
|
this.widgetId = options.widgetId;
|
|
505
625
|
this.headless = options.headless ?? false;
|
|
506
626
|
this.config = { ...DEFAULT_CONFIG, ...options.config };
|
|
627
|
+
this.contextCaptureConfig = {
|
|
628
|
+
...DEFAULT_CONTEXT_CAPTURE_CONFIG,
|
|
629
|
+
...options.contextCapture
|
|
630
|
+
};
|
|
507
631
|
this.apiClient = new ApiClient(
|
|
508
632
|
options.apiBaseUrl ?? DEFAULT_API_BASE_URL,
|
|
509
633
|
this.config.debug
|
|
@@ -567,12 +691,28 @@ var _FeedValue = class _FeedValue {
|
|
|
567
691
|
thankYouMessage: configResponse.config.thankYouMessage ?? "Thank you for your feedback!",
|
|
568
692
|
showBranding: configResponse.config.showBranding ?? true,
|
|
569
693
|
customFields: configResponse.config.customFields,
|
|
570
|
-
// Reaction config (for reaction widgets) -
|
|
694
|
+
// Reaction config (for reaction widgets) - pass through all fields
|
|
571
695
|
...configResponse.config.template && { template: configResponse.config.template },
|
|
572
696
|
...configResponse.config.options && { options: configResponse.config.options },
|
|
573
697
|
followUpLabel: configResponse.config.followUpLabel ?? "Tell us more (optional)",
|
|
574
|
-
submitText: configResponse.config.submitText ?? "Send"
|
|
698
|
+
submitText: configResponse.config.submitText ?? "Send",
|
|
699
|
+
// Reaction widget display options (support both camelCase and snake_case from API)
|
|
700
|
+
...(configResponse.config.showLabels !== void 0 || configResponse.config.show_labels !== void 0) && {
|
|
701
|
+
showLabels: configResponse.config.showLabels ?? configResponse.config.show_labels
|
|
702
|
+
},
|
|
703
|
+
...(configResponse.config.buttonSize || configResponse.config.button_size) && {
|
|
704
|
+
buttonSize: configResponse.config.buttonSize ?? configResponse.config.button_size
|
|
705
|
+
},
|
|
706
|
+
...(configResponse.config.followUpTrigger || configResponse.config.follow_up_trigger) && {
|
|
707
|
+
followUpTrigger: configResponse.config.followUpTrigger ?? configResponse.config.follow_up_trigger
|
|
708
|
+
}
|
|
575
709
|
};
|
|
710
|
+
this.log("Built baseConfig:", {
|
|
711
|
+
buttonSize: baseConfig.buttonSize,
|
|
712
|
+
showLabels: baseConfig.showLabels,
|
|
713
|
+
followUpTrigger: baseConfig.followUpTrigger,
|
|
714
|
+
template: baseConfig.template
|
|
715
|
+
});
|
|
576
716
|
this.widgetConfig = {
|
|
577
717
|
widgetId: configResponse.widget_id,
|
|
578
718
|
widgetKey: configResponse.widget_key,
|
|
@@ -580,12 +720,16 @@ var _FeedValue = class _FeedValue {
|
|
|
580
720
|
type: configResponse.type ?? "feedback",
|
|
581
721
|
config: baseConfig,
|
|
582
722
|
styling: {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
723
|
+
// Pass through all styling properties from API
|
|
724
|
+
...configResponse.styling,
|
|
725
|
+
// Apply defaults for required fields (support both camelCase and snake_case from API)
|
|
726
|
+
primaryColor: configResponse.styling.primaryColor ?? configResponse.styling.primary_color ?? "#3b82f6",
|
|
727
|
+
backgroundColor: configResponse.styling.backgroundColor ?? configResponse.styling.background_color ?? "#ffffff",
|
|
728
|
+
textColor: configResponse.styling.textColor ?? configResponse.styling.text_color ?? "#1f2937",
|
|
729
|
+
buttonTextColor: configResponse.styling.buttonTextColor ?? configResponse.styling.button_text_color ?? "#ffffff",
|
|
730
|
+
borderColor: configResponse.styling.borderColor ?? configResponse.styling.border_color ?? "#e5e7eb",
|
|
731
|
+
borderWidth: configResponse.styling.borderWidth ?? configResponse.styling.border_width ?? "1",
|
|
732
|
+
borderRadius: configResponse.styling.borderRadius ?? configResponse.styling.border_radius ?? "8px"
|
|
589
733
|
}
|
|
590
734
|
};
|
|
591
735
|
if (!this.headless && typeof window !== "undefined" && typeof document !== "undefined") {
|
|
@@ -611,6 +755,8 @@ var _FeedValue = class _FeedValue {
|
|
|
611
755
|
clearTimeout(this.autoCloseTimeout);
|
|
612
756
|
this.autoCloseTimeout = null;
|
|
613
757
|
}
|
|
758
|
+
this.viewportResizeCleanup?.();
|
|
759
|
+
this.viewportResizeCleanup = null;
|
|
614
760
|
this.triggerButton?.remove();
|
|
615
761
|
this.modal?.remove();
|
|
616
762
|
this.overlay?.remove();
|
|
@@ -819,12 +965,12 @@ var _FeedValue = class _FeedValue {
|
|
|
819
965
|
getTemplateOptions(template) {
|
|
820
966
|
const templates = {
|
|
821
967
|
thumbs: [
|
|
822
|
-
{ label: "Helpful", value: "helpful", icon: "
|
|
823
|
-
{ label: "Not Helpful", value: "not_helpful", icon: "
|
|
968
|
+
{ label: "Helpful", value: "helpful", icon: "\u{1F44D}", showFollowUp: false },
|
|
969
|
+
{ label: "Not Helpful", value: "not_helpful", icon: "\u{1F44E}", showFollowUp: true }
|
|
824
970
|
],
|
|
825
971
|
helpful: [
|
|
826
|
-
{ label: "Yes", value: "yes", icon: "
|
|
827
|
-
{ label: "No", value: "no", icon: "
|
|
972
|
+
{ label: "Yes", value: "yes", icon: "\u2713", showFollowUp: false },
|
|
973
|
+
{ label: "No", value: "no", icon: "\u2717", showFollowUp: true }
|
|
828
974
|
],
|
|
829
975
|
emoji: [
|
|
830
976
|
{ label: "Angry", value: "angry", icon: "\u{1F620}", showFollowUp: true },
|
|
@@ -846,7 +992,7 @@ var _FeedValue = class _FeedValue {
|
|
|
846
992
|
/**
|
|
847
993
|
* Submit a reaction.
|
|
848
994
|
* @param value - Selected reaction option value
|
|
849
|
-
* @param options - Optional follow-up text
|
|
995
|
+
* @param options - Optional follow-up text and trigger element for context capture
|
|
850
996
|
*/
|
|
851
997
|
async react(value, options) {
|
|
852
998
|
if (!this.state.isReady) {
|
|
@@ -864,13 +1010,25 @@ var _FeedValue = class _FeedValue {
|
|
|
864
1010
|
const validValues = reactionOptions.map((opt) => opt.value).join(", ");
|
|
865
1011
|
throw new Error(`Invalid reaction value. Must be one of: ${validValues}`);
|
|
866
1012
|
}
|
|
1013
|
+
let capturedContext = null;
|
|
1014
|
+
if (options?.triggerElement) {
|
|
1015
|
+
capturedContext = captureContext(options.triggerElement, this.contextCaptureConfig);
|
|
1016
|
+
this.log("Captured context", capturedContext);
|
|
1017
|
+
}
|
|
867
1018
|
this.emitter.emit("react", { value, hasFollowUp: selectedOption.showFollowUp });
|
|
868
1019
|
this.updateState({ isSubmitting: true });
|
|
869
1020
|
try {
|
|
870
1021
|
const reactionData = {
|
|
871
1022
|
value,
|
|
872
1023
|
metadata: {
|
|
873
|
-
page_url: typeof window !== "undefined" ? window.location.href : ""
|
|
1024
|
+
page_url: typeof window !== "undefined" ? window.location.href : "",
|
|
1025
|
+
// Spread captured context into metadata
|
|
1026
|
+
...capturedContext?.sectionId && { section_id: capturedContext.sectionId },
|
|
1027
|
+
...capturedContext?.sectionTag && { section_tag: capturedContext.sectionTag },
|
|
1028
|
+
...capturedContext?.nearestHeading && { nearest_heading: capturedContext.nearestHeading },
|
|
1029
|
+
...capturedContext?.headingLevel && { heading_level: capturedContext.headingLevel },
|
|
1030
|
+
...capturedContext?.dataAttributes && { data_attributes: capturedContext.dataAttributes },
|
|
1031
|
+
...capturedContext?.cssSelector && { css_selector: capturedContext.cssSelector }
|
|
874
1032
|
},
|
|
875
1033
|
...options?.followUp && { followUp: options.followUp }
|
|
876
1034
|
};
|
|
@@ -1022,6 +1180,7 @@ var _FeedValue = class _FeedValue {
|
|
|
1022
1180
|
}
|
|
1023
1181
|
this.renderTrigger();
|
|
1024
1182
|
this.renderModal();
|
|
1183
|
+
this.setupViewportCompensation();
|
|
1025
1184
|
}
|
|
1026
1185
|
/**
|
|
1027
1186
|
* Sanitize CSS to block potentially dangerous patterns
|
|
@@ -1234,7 +1393,7 @@ var _FeedValue = class _FeedValue {
|
|
|
1234
1393
|
getPositionStyles(position) {
|
|
1235
1394
|
switch (position) {
|
|
1236
1395
|
case "bottom-left":
|
|
1237
|
-
return "bottom: 20px; left: 20px;";
|
|
1396
|
+
return "bottom: calc(20px + env(safe-area-inset-bottom, 0px)); left: 20px;";
|
|
1238
1397
|
case "top-right":
|
|
1239
1398
|
return "top: 20px; right: 20px;";
|
|
1240
1399
|
case "top-left":
|
|
@@ -1243,7 +1402,7 @@ var _FeedValue = class _FeedValue {
|
|
|
1243
1402
|
return "top: 50%; left: 50%; transform: translate(-50%, -50%);";
|
|
1244
1403
|
case "bottom-right":
|
|
1245
1404
|
default:
|
|
1246
|
-
return "bottom: 20px; right: 20px;";
|
|
1405
|
+
return "bottom: calc(20px + env(safe-area-inset-bottom, 0px)); right: 20px;";
|
|
1247
1406
|
}
|
|
1248
1407
|
}
|
|
1249
1408
|
/**
|
|
@@ -1252,9 +1411,9 @@ var _FeedValue = class _FeedValue {
|
|
|
1252
1411
|
getModalPositionStyles(position) {
|
|
1253
1412
|
switch (position) {
|
|
1254
1413
|
case "bottom-left":
|
|
1255
|
-
return "bottom: 20px; left: 20px;";
|
|
1414
|
+
return "bottom: calc(20px + env(safe-area-inset-bottom, 0px)); left: 20px;";
|
|
1256
1415
|
case "bottom-right":
|
|
1257
|
-
return "bottom: 20px; right: 20px;";
|
|
1416
|
+
return "bottom: calc(20px + env(safe-area-inset-bottom, 0px)); right: 20px;";
|
|
1258
1417
|
case "top-right":
|
|
1259
1418
|
return "top: 20px; right: 20px;";
|
|
1260
1419
|
case "top-left":
|
|
@@ -1264,6 +1423,37 @@ var _FeedValue = class _FeedValue {
|
|
|
1264
1423
|
return "top: 50%; left: 50%; transform: translate(-50%, -50%);";
|
|
1265
1424
|
}
|
|
1266
1425
|
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Smooth viewport resize compensation for mobile browser toolbar show/hide.
|
|
1428
|
+
*
|
|
1429
|
+
* When Chrome/Safari's bottom toolbar appears or disappears, the layout viewport
|
|
1430
|
+
* resizes and position:fixed elements jump. This detects toolbar-sized height
|
|
1431
|
+
* changes and applies an inverse CSS translate to cancel the jump, then animates
|
|
1432
|
+
* back to the natural position. Uses the `translate` CSS property (separate from
|
|
1433
|
+
* `transform`) to avoid conflicting with the hover transform on the trigger.
|
|
1434
|
+
*/
|
|
1435
|
+
setupViewportCompensation() {
|
|
1436
|
+
if (typeof window === "undefined") return;
|
|
1437
|
+
let lastHeight = window.innerHeight;
|
|
1438
|
+
const handleResize = () => {
|
|
1439
|
+
const newHeight = window.innerHeight;
|
|
1440
|
+
const delta = newHeight - lastHeight;
|
|
1441
|
+
lastHeight = newHeight;
|
|
1442
|
+
if (Math.abs(delta) <= 5 || Math.abs(delta) >= 120) return;
|
|
1443
|
+
const elements = [this.triggerButton, this.modal].filter(Boolean);
|
|
1444
|
+
for (const el of elements) {
|
|
1445
|
+
el.style.transition = "none";
|
|
1446
|
+
el.style.translate = `0 ${delta}px`;
|
|
1447
|
+
void el.offsetHeight;
|
|
1448
|
+
el.style.transition = "translate 0.3s ease-out";
|
|
1449
|
+
el.style.translate = "0 0";
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
window.addEventListener("resize", handleResize, { passive: true });
|
|
1453
|
+
this.viewportResizeCleanup = () => {
|
|
1454
|
+
window.removeEventListener("resize", handleResize);
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1267
1457
|
/**
|
|
1268
1458
|
* Parse SVG string to DOM element safely using DOMParser
|
|
1269
1459
|
*/
|
|
@@ -1464,9 +1654,6 @@ var _FeedValue = class _FeedValue {
|
|
|
1464
1654
|
}
|
|
1465
1655
|
}
|
|
1466
1656
|
};
|
|
1467
|
-
/**
|
|
1468
|
-
* SVG icons for trigger button (matching widget-bundle exactly)
|
|
1469
|
-
*/
|
|
1470
1657
|
/**
|
|
1471
1658
|
* SVG icons for trigger button - must match Lucide icons used in frontend-web
|
|
1472
1659
|
* chat = MessageCircle, message = MessageSquare, feedback = MessagesSquare,
|
|
@@ -1490,6 +1677,6 @@ var NEGATIVE_OPTIONS_MAP = {
|
|
|
1490
1677
|
rating: ["1", "2"]
|
|
1491
1678
|
};
|
|
1492
1679
|
|
|
1493
|
-
export { ApiClient, DEFAULT_API_BASE_URL, FeedValue, NEGATIVE_OPTIONS_MAP, TypedEventEmitter, clearFingerprint, generateFingerprint };
|
|
1680
|
+
export { ApiClient, DEFAULT_API_BASE_URL, DEFAULT_CONTEXT_CAPTURE_CONFIG, FeedValue, NEGATIVE_OPTIONS_MAP, TypedEventEmitter, captureContext, clearFingerprint, generateFingerprint };
|
|
1494
1681
|
//# sourceMappingURL=index.js.map
|
|
1495
1682
|
//# sourceMappingURL=index.js.map
|