@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.cjs
CHANGED
|
@@ -455,6 +455,123 @@ function clearFingerprint() {
|
|
|
455
455
|
}
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
+
// src/context-capture.ts
|
|
459
|
+
var DEFAULT_CONTEXT_CAPTURE_CONFIG = {
|
|
460
|
+
enabled: true,
|
|
461
|
+
maxDepth: 5,
|
|
462
|
+
maxHeadingLength: 100,
|
|
463
|
+
dataAttributeWhitelist: [
|
|
464
|
+
"data-section",
|
|
465
|
+
"data-feature",
|
|
466
|
+
"data-component",
|
|
467
|
+
"data-fv-section",
|
|
468
|
+
"data-fv-feature"
|
|
469
|
+
]
|
|
470
|
+
};
|
|
471
|
+
function findNearestWithId(element, maxDepth) {
|
|
472
|
+
let current = element;
|
|
473
|
+
let depth = 0;
|
|
474
|
+
while (current && depth < maxDepth) {
|
|
475
|
+
if (current.id) {
|
|
476
|
+
return current;
|
|
477
|
+
}
|
|
478
|
+
current = current.parentElement;
|
|
479
|
+
depth++;
|
|
480
|
+
}
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
function findNearestHeading(element, section, maxDepth) {
|
|
484
|
+
if (section) {
|
|
485
|
+
const heading = section.querySelector("h1, h2, h3, h4, h5, h6");
|
|
486
|
+
if (heading) return heading;
|
|
487
|
+
}
|
|
488
|
+
let current = element;
|
|
489
|
+
let depth = 0;
|
|
490
|
+
while (current && depth < maxDepth) {
|
|
491
|
+
let sibling = current.previousElementSibling;
|
|
492
|
+
while (sibling) {
|
|
493
|
+
if (/^H[1-6]$/.test(sibling.tagName)) {
|
|
494
|
+
return sibling;
|
|
495
|
+
}
|
|
496
|
+
sibling = sibling.previousElementSibling;
|
|
497
|
+
}
|
|
498
|
+
current = current.parentElement;
|
|
499
|
+
depth++;
|
|
500
|
+
}
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
function captureDataAttributes(element, whitelist, maxDepth) {
|
|
504
|
+
const result = {};
|
|
505
|
+
let current = element;
|
|
506
|
+
let depth = 0;
|
|
507
|
+
while (current && depth < maxDepth) {
|
|
508
|
+
for (const attr of Array.from(current.attributes)) {
|
|
509
|
+
if (whitelist.includes(attr.name) && !result[attr.name]) {
|
|
510
|
+
result[attr.name] = attr.value;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
current = current.parentElement;
|
|
514
|
+
depth++;
|
|
515
|
+
}
|
|
516
|
+
return result;
|
|
517
|
+
}
|
|
518
|
+
function generateSelector(element, maxDepth) {
|
|
519
|
+
const parts = [];
|
|
520
|
+
let current = element;
|
|
521
|
+
let depth = 0;
|
|
522
|
+
while (current && depth < maxDepth && current !== document.body) {
|
|
523
|
+
let selector = current.tagName.toLowerCase();
|
|
524
|
+
if (current.id) {
|
|
525
|
+
selector = `#${current.id}`;
|
|
526
|
+
parts.unshift(selector);
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
if (current.className && typeof current.className === "string") {
|
|
530
|
+
const classes = current.className.split(" ").filter((c) => c.trim()).slice(0, 2);
|
|
531
|
+
if (classes.length) {
|
|
532
|
+
selector += "." + classes.join(".");
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
parts.unshift(selector);
|
|
536
|
+
current = current.parentElement;
|
|
537
|
+
depth++;
|
|
538
|
+
}
|
|
539
|
+
return parts.join(" > ");
|
|
540
|
+
}
|
|
541
|
+
function truncate(str, maxLength) {
|
|
542
|
+
if (str.length <= maxLength) return str;
|
|
543
|
+
return str.substring(0, maxLength - 3) + "...";
|
|
544
|
+
}
|
|
545
|
+
function captureContext(triggerElement, config = DEFAULT_CONTEXT_CAPTURE_CONFIG) {
|
|
546
|
+
if (!config.enabled || !triggerElement || typeof document === "undefined") {
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
const context = {};
|
|
550
|
+
const sectionWithId = findNearestWithId(triggerElement, config.maxDepth);
|
|
551
|
+
if (sectionWithId) {
|
|
552
|
+
context.sectionId = sectionWithId.id;
|
|
553
|
+
context.sectionTag = sectionWithId.tagName.toLowerCase();
|
|
554
|
+
}
|
|
555
|
+
const heading = findNearestHeading(triggerElement, sectionWithId, config.maxDepth);
|
|
556
|
+
if (heading) {
|
|
557
|
+
context.nearestHeading = truncate(heading.textContent?.trim() || "", config.maxHeadingLength);
|
|
558
|
+
const level = heading.tagName[1];
|
|
559
|
+
if (level) {
|
|
560
|
+
context.headingLevel = parseInt(level, 10);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const dataAttrs = captureDataAttributes(
|
|
564
|
+
triggerElement,
|
|
565
|
+
config.dataAttributeWhitelist,
|
|
566
|
+
config.maxDepth
|
|
567
|
+
);
|
|
568
|
+
if (Object.keys(dataAttrs).length > 0) {
|
|
569
|
+
context.dataAttributes = dataAttrs;
|
|
570
|
+
}
|
|
571
|
+
context.cssSelector = generateSelector(triggerElement, 3);
|
|
572
|
+
return Object.keys(context).length > 0 ? context : null;
|
|
573
|
+
}
|
|
574
|
+
|
|
458
575
|
// src/feedvalue.ts
|
|
459
576
|
var SUCCESS_AUTO_CLOSE_DELAY_MS = 3e3;
|
|
460
577
|
var VALID_SENTIMENTS = ["angry", "disappointed", "satisfied", "excited"];
|
|
@@ -477,6 +594,7 @@ var _FeedValue = class _FeedValue {
|
|
|
477
594
|
__publicField(this, "apiClient");
|
|
478
595
|
__publicField(this, "emitter");
|
|
479
596
|
__publicField(this, "headless");
|
|
597
|
+
__publicField(this, "contextCaptureConfig");
|
|
480
598
|
__publicField(this, "config");
|
|
481
599
|
__publicField(this, "widgetConfig", null);
|
|
482
600
|
// State
|
|
@@ -499,6 +617,8 @@ var _FeedValue = class _FeedValue {
|
|
|
499
617
|
__publicField(this, "modal", null);
|
|
500
618
|
__publicField(this, "overlay", null);
|
|
501
619
|
__publicField(this, "stylesInjected", false);
|
|
620
|
+
// Viewport resize compensation cleanup
|
|
621
|
+
__publicField(this, "viewportResizeCleanup", null);
|
|
502
622
|
// Auto-close timeout reference (for cleanup on destroy)
|
|
503
623
|
__publicField(this, "autoCloseTimeout", null);
|
|
504
624
|
// Destroyed flag - guards async continuations (fixes React StrictMode race condition)
|
|
@@ -506,6 +626,10 @@ var _FeedValue = class _FeedValue {
|
|
|
506
626
|
this.widgetId = options.widgetId;
|
|
507
627
|
this.headless = options.headless ?? false;
|
|
508
628
|
this.config = { ...DEFAULT_CONFIG, ...options.config };
|
|
629
|
+
this.contextCaptureConfig = {
|
|
630
|
+
...DEFAULT_CONTEXT_CAPTURE_CONFIG,
|
|
631
|
+
...options.contextCapture
|
|
632
|
+
};
|
|
509
633
|
this.apiClient = new ApiClient(
|
|
510
634
|
options.apiBaseUrl ?? DEFAULT_API_BASE_URL,
|
|
511
635
|
this.config.debug
|
|
@@ -569,12 +693,28 @@ var _FeedValue = class _FeedValue {
|
|
|
569
693
|
thankYouMessage: configResponse.config.thankYouMessage ?? "Thank you for your feedback!",
|
|
570
694
|
showBranding: configResponse.config.showBranding ?? true,
|
|
571
695
|
customFields: configResponse.config.customFields,
|
|
572
|
-
// Reaction config (for reaction widgets) -
|
|
696
|
+
// Reaction config (for reaction widgets) - pass through all fields
|
|
573
697
|
...configResponse.config.template && { template: configResponse.config.template },
|
|
574
698
|
...configResponse.config.options && { options: configResponse.config.options },
|
|
575
699
|
followUpLabel: configResponse.config.followUpLabel ?? "Tell us more (optional)",
|
|
576
|
-
submitText: configResponse.config.submitText ?? "Send"
|
|
700
|
+
submitText: configResponse.config.submitText ?? "Send",
|
|
701
|
+
// Reaction widget display options (support both camelCase and snake_case from API)
|
|
702
|
+
...(configResponse.config.showLabels !== void 0 || configResponse.config.show_labels !== void 0) && {
|
|
703
|
+
showLabels: configResponse.config.showLabels ?? configResponse.config.show_labels
|
|
704
|
+
},
|
|
705
|
+
...(configResponse.config.buttonSize || configResponse.config.button_size) && {
|
|
706
|
+
buttonSize: configResponse.config.buttonSize ?? configResponse.config.button_size
|
|
707
|
+
},
|
|
708
|
+
...(configResponse.config.followUpTrigger || configResponse.config.follow_up_trigger) && {
|
|
709
|
+
followUpTrigger: configResponse.config.followUpTrigger ?? configResponse.config.follow_up_trigger
|
|
710
|
+
}
|
|
577
711
|
};
|
|
712
|
+
this.log("Built baseConfig:", {
|
|
713
|
+
buttonSize: baseConfig.buttonSize,
|
|
714
|
+
showLabels: baseConfig.showLabels,
|
|
715
|
+
followUpTrigger: baseConfig.followUpTrigger,
|
|
716
|
+
template: baseConfig.template
|
|
717
|
+
});
|
|
578
718
|
this.widgetConfig = {
|
|
579
719
|
widgetId: configResponse.widget_id,
|
|
580
720
|
widgetKey: configResponse.widget_key,
|
|
@@ -582,12 +722,16 @@ var _FeedValue = class _FeedValue {
|
|
|
582
722
|
type: configResponse.type ?? "feedback",
|
|
583
723
|
config: baseConfig,
|
|
584
724
|
styling: {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
725
|
+
// Pass through all styling properties from API
|
|
726
|
+
...configResponse.styling,
|
|
727
|
+
// Apply defaults for required fields (support both camelCase and snake_case from API)
|
|
728
|
+
primaryColor: configResponse.styling.primaryColor ?? configResponse.styling.primary_color ?? "#3b82f6",
|
|
729
|
+
backgroundColor: configResponse.styling.backgroundColor ?? configResponse.styling.background_color ?? "#ffffff",
|
|
730
|
+
textColor: configResponse.styling.textColor ?? configResponse.styling.text_color ?? "#1f2937",
|
|
731
|
+
buttonTextColor: configResponse.styling.buttonTextColor ?? configResponse.styling.button_text_color ?? "#ffffff",
|
|
732
|
+
borderColor: configResponse.styling.borderColor ?? configResponse.styling.border_color ?? "#e5e7eb",
|
|
733
|
+
borderWidth: configResponse.styling.borderWidth ?? configResponse.styling.border_width ?? "1",
|
|
734
|
+
borderRadius: configResponse.styling.borderRadius ?? configResponse.styling.border_radius ?? "8px"
|
|
591
735
|
}
|
|
592
736
|
};
|
|
593
737
|
if (!this.headless && typeof window !== "undefined" && typeof document !== "undefined") {
|
|
@@ -613,6 +757,8 @@ var _FeedValue = class _FeedValue {
|
|
|
613
757
|
clearTimeout(this.autoCloseTimeout);
|
|
614
758
|
this.autoCloseTimeout = null;
|
|
615
759
|
}
|
|
760
|
+
this.viewportResizeCleanup?.();
|
|
761
|
+
this.viewportResizeCleanup = null;
|
|
616
762
|
this.triggerButton?.remove();
|
|
617
763
|
this.modal?.remove();
|
|
618
764
|
this.overlay?.remove();
|
|
@@ -821,12 +967,12 @@ var _FeedValue = class _FeedValue {
|
|
|
821
967
|
getTemplateOptions(template) {
|
|
822
968
|
const templates = {
|
|
823
969
|
thumbs: [
|
|
824
|
-
{ label: "Helpful", value: "helpful", icon: "
|
|
825
|
-
{ label: "Not Helpful", value: "not_helpful", icon: "
|
|
970
|
+
{ label: "Helpful", value: "helpful", icon: "\u{1F44D}", showFollowUp: false },
|
|
971
|
+
{ label: "Not Helpful", value: "not_helpful", icon: "\u{1F44E}", showFollowUp: true }
|
|
826
972
|
],
|
|
827
973
|
helpful: [
|
|
828
|
-
{ label: "Yes", value: "yes", icon: "
|
|
829
|
-
{ label: "No", value: "no", icon: "
|
|
974
|
+
{ label: "Yes", value: "yes", icon: "\u2713", showFollowUp: false },
|
|
975
|
+
{ label: "No", value: "no", icon: "\u2717", showFollowUp: true }
|
|
830
976
|
],
|
|
831
977
|
emoji: [
|
|
832
978
|
{ label: "Angry", value: "angry", icon: "\u{1F620}", showFollowUp: true },
|
|
@@ -848,7 +994,7 @@ var _FeedValue = class _FeedValue {
|
|
|
848
994
|
/**
|
|
849
995
|
* Submit a reaction.
|
|
850
996
|
* @param value - Selected reaction option value
|
|
851
|
-
* @param options - Optional follow-up text
|
|
997
|
+
* @param options - Optional follow-up text and trigger element for context capture
|
|
852
998
|
*/
|
|
853
999
|
async react(value, options) {
|
|
854
1000
|
if (!this.state.isReady) {
|
|
@@ -866,13 +1012,25 @@ var _FeedValue = class _FeedValue {
|
|
|
866
1012
|
const validValues = reactionOptions.map((opt) => opt.value).join(", ");
|
|
867
1013
|
throw new Error(`Invalid reaction value. Must be one of: ${validValues}`);
|
|
868
1014
|
}
|
|
1015
|
+
let capturedContext = null;
|
|
1016
|
+
if (options?.triggerElement) {
|
|
1017
|
+
capturedContext = captureContext(options.triggerElement, this.contextCaptureConfig);
|
|
1018
|
+
this.log("Captured context", capturedContext);
|
|
1019
|
+
}
|
|
869
1020
|
this.emitter.emit("react", { value, hasFollowUp: selectedOption.showFollowUp });
|
|
870
1021
|
this.updateState({ isSubmitting: true });
|
|
871
1022
|
try {
|
|
872
1023
|
const reactionData = {
|
|
873
1024
|
value,
|
|
874
1025
|
metadata: {
|
|
875
|
-
page_url: typeof window !== "undefined" ? window.location.href : ""
|
|
1026
|
+
page_url: typeof window !== "undefined" ? window.location.href : "",
|
|
1027
|
+
// Spread captured context into metadata
|
|
1028
|
+
...capturedContext?.sectionId && { section_id: capturedContext.sectionId },
|
|
1029
|
+
...capturedContext?.sectionTag && { section_tag: capturedContext.sectionTag },
|
|
1030
|
+
...capturedContext?.nearestHeading && { nearest_heading: capturedContext.nearestHeading },
|
|
1031
|
+
...capturedContext?.headingLevel && { heading_level: capturedContext.headingLevel },
|
|
1032
|
+
...capturedContext?.dataAttributes && { data_attributes: capturedContext.dataAttributes },
|
|
1033
|
+
...capturedContext?.cssSelector && { css_selector: capturedContext.cssSelector }
|
|
876
1034
|
},
|
|
877
1035
|
...options?.followUp && { followUp: options.followUp }
|
|
878
1036
|
};
|
|
@@ -1024,6 +1182,7 @@ var _FeedValue = class _FeedValue {
|
|
|
1024
1182
|
}
|
|
1025
1183
|
this.renderTrigger();
|
|
1026
1184
|
this.renderModal();
|
|
1185
|
+
this.setupViewportCompensation();
|
|
1027
1186
|
}
|
|
1028
1187
|
/**
|
|
1029
1188
|
* Sanitize CSS to block potentially dangerous patterns
|
|
@@ -1236,7 +1395,7 @@ var _FeedValue = class _FeedValue {
|
|
|
1236
1395
|
getPositionStyles(position) {
|
|
1237
1396
|
switch (position) {
|
|
1238
1397
|
case "bottom-left":
|
|
1239
|
-
return "bottom: 20px; left: 20px;";
|
|
1398
|
+
return "bottom: calc(20px + env(safe-area-inset-bottom, 0px)); left: 20px;";
|
|
1240
1399
|
case "top-right":
|
|
1241
1400
|
return "top: 20px; right: 20px;";
|
|
1242
1401
|
case "top-left":
|
|
@@ -1245,7 +1404,7 @@ var _FeedValue = class _FeedValue {
|
|
|
1245
1404
|
return "top: 50%; left: 50%; transform: translate(-50%, -50%);";
|
|
1246
1405
|
case "bottom-right":
|
|
1247
1406
|
default:
|
|
1248
|
-
return "bottom: 20px; right: 20px;";
|
|
1407
|
+
return "bottom: calc(20px + env(safe-area-inset-bottom, 0px)); right: 20px;";
|
|
1249
1408
|
}
|
|
1250
1409
|
}
|
|
1251
1410
|
/**
|
|
@@ -1254,9 +1413,9 @@ var _FeedValue = class _FeedValue {
|
|
|
1254
1413
|
getModalPositionStyles(position) {
|
|
1255
1414
|
switch (position) {
|
|
1256
1415
|
case "bottom-left":
|
|
1257
|
-
return "bottom: 20px; left: 20px;";
|
|
1416
|
+
return "bottom: calc(20px + env(safe-area-inset-bottom, 0px)); left: 20px;";
|
|
1258
1417
|
case "bottom-right":
|
|
1259
|
-
return "bottom: 20px; right: 20px;";
|
|
1418
|
+
return "bottom: calc(20px + env(safe-area-inset-bottom, 0px)); right: 20px;";
|
|
1260
1419
|
case "top-right":
|
|
1261
1420
|
return "top: 20px; right: 20px;";
|
|
1262
1421
|
case "top-left":
|
|
@@ -1266,6 +1425,37 @@ var _FeedValue = class _FeedValue {
|
|
|
1266
1425
|
return "top: 50%; left: 50%; transform: translate(-50%, -50%);";
|
|
1267
1426
|
}
|
|
1268
1427
|
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Smooth viewport resize compensation for mobile browser toolbar show/hide.
|
|
1430
|
+
*
|
|
1431
|
+
* When Chrome/Safari's bottom toolbar appears or disappears, the layout viewport
|
|
1432
|
+
* resizes and position:fixed elements jump. This detects toolbar-sized height
|
|
1433
|
+
* changes and applies an inverse CSS translate to cancel the jump, then animates
|
|
1434
|
+
* back to the natural position. Uses the `translate` CSS property (separate from
|
|
1435
|
+
* `transform`) to avoid conflicting with the hover transform on the trigger.
|
|
1436
|
+
*/
|
|
1437
|
+
setupViewportCompensation() {
|
|
1438
|
+
if (typeof window === "undefined") return;
|
|
1439
|
+
let lastHeight = window.innerHeight;
|
|
1440
|
+
const handleResize = () => {
|
|
1441
|
+
const newHeight = window.innerHeight;
|
|
1442
|
+
const delta = newHeight - lastHeight;
|
|
1443
|
+
lastHeight = newHeight;
|
|
1444
|
+
if (Math.abs(delta) <= 5 || Math.abs(delta) >= 120) return;
|
|
1445
|
+
const elements = [this.triggerButton, this.modal].filter(Boolean);
|
|
1446
|
+
for (const el of elements) {
|
|
1447
|
+
el.style.transition = "none";
|
|
1448
|
+
el.style.translate = `0 ${delta}px`;
|
|
1449
|
+
void el.offsetHeight;
|
|
1450
|
+
el.style.transition = "translate 0.3s ease-out";
|
|
1451
|
+
el.style.translate = "0 0";
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
window.addEventListener("resize", handleResize, { passive: true });
|
|
1455
|
+
this.viewportResizeCleanup = () => {
|
|
1456
|
+
window.removeEventListener("resize", handleResize);
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1269
1459
|
/**
|
|
1270
1460
|
* Parse SVG string to DOM element safely using DOMParser
|
|
1271
1461
|
*/
|
|
@@ -1466,9 +1656,6 @@ var _FeedValue = class _FeedValue {
|
|
|
1466
1656
|
}
|
|
1467
1657
|
}
|
|
1468
1658
|
};
|
|
1469
|
-
/**
|
|
1470
|
-
* SVG icons for trigger button (matching widget-bundle exactly)
|
|
1471
|
-
*/
|
|
1472
1659
|
/**
|
|
1473
1660
|
* SVG icons for trigger button - must match Lucide icons used in frontend-web
|
|
1474
1661
|
* chat = MessageCircle, message = MessageSquare, feedback = MessagesSquare,
|
|
@@ -1494,9 +1681,11 @@ var NEGATIVE_OPTIONS_MAP = {
|
|
|
1494
1681
|
|
|
1495
1682
|
exports.ApiClient = ApiClient;
|
|
1496
1683
|
exports.DEFAULT_API_BASE_URL = DEFAULT_API_BASE_URL;
|
|
1684
|
+
exports.DEFAULT_CONTEXT_CAPTURE_CONFIG = DEFAULT_CONTEXT_CAPTURE_CONFIG;
|
|
1497
1685
|
exports.FeedValue = FeedValue;
|
|
1498
1686
|
exports.NEGATIVE_OPTIONS_MAP = NEGATIVE_OPTIONS_MAP;
|
|
1499
1687
|
exports.TypedEventEmitter = TypedEventEmitter;
|
|
1688
|
+
exports.captureContext = captureContext;
|
|
1500
1689
|
exports.clearFingerprint = clearFingerprint;
|
|
1501
1690
|
exports.generateFingerprint = generateFingerprint;
|
|
1502
1691
|
//# sourceMappingURL=index.cjs.map
|