@feedvalue/core 0.1.10 → 0.1.14

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 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
@@ -506,6 +624,10 @@ var _FeedValue = class _FeedValue {
506
624
  this.widgetId = options.widgetId;
507
625
  this.headless = options.headless ?? false;
508
626
  this.config = { ...DEFAULT_CONFIG, ...options.config };
627
+ this.contextCaptureConfig = {
628
+ ...DEFAULT_CONTEXT_CAPTURE_CONFIG,
629
+ ...options.contextCapture
630
+ };
509
631
  this.apiClient = new ApiClient(
510
632
  options.apiBaseUrl ?? DEFAULT_API_BASE_URL,
511
633
  this.config.debug
@@ -569,12 +691,28 @@ var _FeedValue = class _FeedValue {
569
691
  thankYouMessage: configResponse.config.thankYouMessage ?? "Thank you for your feedback!",
570
692
  showBranding: configResponse.config.showBranding ?? true,
571
693
  customFields: configResponse.config.customFields,
572
- // Reaction config (for reaction widgets) - only include if defined
694
+ // Reaction config (for reaction widgets) - pass through all fields
573
695
  ...configResponse.config.template && { template: configResponse.config.template },
574
696
  ...configResponse.config.options && { options: configResponse.config.options },
575
697
  followUpLabel: configResponse.config.followUpLabel ?? "Tell us more (optional)",
576
- 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
+ }
577
709
  };
710
+ this.log("Built baseConfig:", {
711
+ buttonSize: baseConfig.buttonSize,
712
+ showLabels: baseConfig.showLabels,
713
+ followUpTrigger: baseConfig.followUpTrigger,
714
+ template: baseConfig.template
715
+ });
578
716
  this.widgetConfig = {
579
717
  widgetId: configResponse.widget_id,
580
718
  widgetKey: configResponse.widget_key,
@@ -582,12 +720,16 @@ var _FeedValue = class _FeedValue {
582
720
  type: configResponse.type ?? "feedback",
583
721
  config: baseConfig,
584
722
  styling: {
585
- primaryColor: configResponse.styling.primaryColor ?? "#3b82f6",
586
- backgroundColor: configResponse.styling.backgroundColor ?? "#ffffff",
587
- textColor: configResponse.styling.textColor ?? "#1f2937",
588
- buttonTextColor: configResponse.styling.buttonTextColor ?? "#ffffff",
589
- borderRadius: configResponse.styling.borderRadius ?? "8px",
590
- customCSS: configResponse.styling.customCSS
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"
591
733
  }
592
734
  };
593
735
  if (!this.headless && typeof window !== "undefined" && typeof document !== "undefined") {
@@ -821,12 +963,12 @@ var _FeedValue = class _FeedValue {
821
963
  getTemplateOptions(template) {
822
964
  const templates = {
823
965
  thumbs: [
824
- { label: "Helpful", value: "helpful", icon: "thumbs-up", showFollowUp: false },
825
- { label: "Not Helpful", value: "not_helpful", icon: "thumbs-down", showFollowUp: true }
966
+ { label: "Helpful", value: "helpful", icon: "\u{1F44D}", showFollowUp: false },
967
+ { label: "Not Helpful", value: "not_helpful", icon: "\u{1F44E}", showFollowUp: true }
826
968
  ],
827
969
  helpful: [
828
- { label: "Yes", value: "yes", icon: "check", showFollowUp: false },
829
- { label: "No", value: "no", icon: "x", showFollowUp: true }
970
+ { label: "Yes", value: "yes", icon: "\u2713", showFollowUp: false },
971
+ { label: "No", value: "no", icon: "\u2717", showFollowUp: true }
830
972
  ],
831
973
  emoji: [
832
974
  { label: "Angry", value: "angry", icon: "\u{1F620}", showFollowUp: true },
@@ -848,7 +990,7 @@ var _FeedValue = class _FeedValue {
848
990
  /**
849
991
  * Submit a reaction.
850
992
  * @param value - Selected reaction option value
851
- * @param options - Optional follow-up text
993
+ * @param options - Optional follow-up text and trigger element for context capture
852
994
  */
853
995
  async react(value, options) {
854
996
  if (!this.state.isReady) {
@@ -866,13 +1008,25 @@ var _FeedValue = class _FeedValue {
866
1008
  const validValues = reactionOptions.map((opt) => opt.value).join(", ");
867
1009
  throw new Error(`Invalid reaction value. Must be one of: ${validValues}`);
868
1010
  }
1011
+ let capturedContext = null;
1012
+ if (options?.triggerElement) {
1013
+ capturedContext = captureContext(options.triggerElement, this.contextCaptureConfig);
1014
+ this.log("Captured context", capturedContext);
1015
+ }
869
1016
  this.emitter.emit("react", { value, hasFollowUp: selectedOption.showFollowUp });
870
1017
  this.updateState({ isSubmitting: true });
871
1018
  try {
872
1019
  const reactionData = {
873
1020
  value,
874
1021
  metadata: {
875
- page_url: typeof window !== "undefined" ? window.location.href : ""
1022
+ page_url: typeof window !== "undefined" ? window.location.href : "",
1023
+ // Spread captured context into metadata
1024
+ ...capturedContext?.sectionId && { section_id: capturedContext.sectionId },
1025
+ ...capturedContext?.sectionTag && { section_tag: capturedContext.sectionTag },
1026
+ ...capturedContext?.nearestHeading && { nearest_heading: capturedContext.nearestHeading },
1027
+ ...capturedContext?.headingLevel && { heading_level: capturedContext.headingLevel },
1028
+ ...capturedContext?.dataAttributes && { data_attributes: capturedContext.dataAttributes },
1029
+ ...capturedContext?.cssSelector && { css_selector: capturedContext.cssSelector }
876
1030
  },
877
1031
  ...options?.followUp && { followUp: options.followUp }
878
1032
  };
@@ -1494,9 +1648,11 @@ var NEGATIVE_OPTIONS_MAP = {
1494
1648
 
1495
1649
  exports.ApiClient = ApiClient;
1496
1650
  exports.DEFAULT_API_BASE_URL = DEFAULT_API_BASE_URL;
1651
+ exports.DEFAULT_CONTEXT_CAPTURE_CONFIG = DEFAULT_CONTEXT_CAPTURE_CONFIG;
1497
1652
  exports.FeedValue = FeedValue;
1498
1653
  exports.NEGATIVE_OPTIONS_MAP = NEGATIVE_OPTIONS_MAP;
1499
1654
  exports.TypedEventEmitter = TypedEventEmitter;
1655
+ exports.captureContext = captureContext;
1500
1656
  exports.clearFingerprint = clearFingerprint;
1501
1657
  exports.generateFingerprint = generateFingerprint;
1502
1658
  //# sourceMappingURL=index.cjs.map