@preprio/toolkit 0.4.2 → 0.5.0-rc.1

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.
@@ -267,6 +267,7 @@ function createElementCache(query, ttl = 1e3) {
267
267
  }
268
268
 
269
269
  // src/core/iframe-bridge.ts
270
+ var debug = createScopedLogger("iframe-bridge");
270
271
  var EDITOR_BASE_DOMAIN = "prepr.io";
271
272
  function isAllowedEditorOrigin(origin, allowed) {
272
273
  if (allowed) return allowed.includes(origin);
@@ -284,6 +285,7 @@ function isAllowedEditorOrigin(origin, allowed) {
284
285
  }
285
286
  function createIframeBridge(store, options = {}) {
286
287
  const allowedOrigins = options.allowedEditorOrigins;
288
+ const onScrollToField = options.onScrollToField;
287
289
  let parentOrigin = null;
288
290
  const onKeyDown = (event) => {
289
291
  const key = event.key.toLowerCase();
@@ -307,6 +309,15 @@ function createIframeBridge(store, options = {}) {
307
309
  const currentScrollY = window.scrollY || document.documentElement.scrollTop;
308
310
  sendPreprEvent("getScrollPosition", { value: currentScrollY });
309
311
  }
312
+ if (data?.event === "prepr:scrollToField") {
313
+ if (typeof data.field !== "string" || !data.field) return;
314
+ if (data.id !== void 0 && typeof data.id !== "string") return;
315
+ try {
316
+ onScrollToField?.({ field: data.field, id: data.id });
317
+ } catch (error) {
318
+ debug.warn("scrollToField handler failed", error);
319
+ }
320
+ }
310
321
  };
311
322
  return {
312
323
  start() {
@@ -331,7 +342,7 @@ import { vercelStegaSplit } from "@vercel/stega";
331
342
 
332
343
  // src/core/stega/clean.ts
333
344
  import { vercelStegaClean, vercelStegaDecode } from "@vercel/stega";
334
- var debug = createScopedLogger("stega:clean");
345
+ var debug2 = createScopedLogger("stega:clean");
335
346
  function decodeStega(str) {
336
347
  if (!str) return null;
337
348
  try {
@@ -340,7 +351,7 @@ function decodeStega(str) {
340
351
  return decoded;
341
352
  }
342
353
  } catch (error) {
343
- debug.log("error decoding stega data", error);
354
+ debug2.log("error decoding stega data", error);
344
355
  const match = str.match(/{"origin.*?}/);
345
356
  if (match) {
346
357
  try {
@@ -349,7 +360,7 @@ function decodeStega(str) {
349
360
  return decodedMatch;
350
361
  }
351
362
  } catch (innerError) {
352
- debug.log("error decoding stega regex match", innerError);
363
+ debug2.log("error decoding stega regex match", innerError);
353
364
  }
354
365
  }
355
366
  }
@@ -385,14 +396,37 @@ function isRenderedElement(element) {
385
396
  if (!style) return true;
386
397
  return style.visibility !== "hidden" && style.display !== "none";
387
398
  }
399
+ function parseEditHref(href) {
400
+ try {
401
+ const url = new URL(href, window.location.origin);
402
+ return {
403
+ id: url.pathname.split("/").pop() || void 0,
404
+ field: url.searchParams.get("field") || void 0,
405
+ locale: url.searchParams.get("locale") || void 0
406
+ };
407
+ } catch {
408
+ return {};
409
+ }
410
+ }
411
+ function setOrRemove(element, name, value) {
412
+ if (value) {
413
+ element.setAttribute(name, value);
414
+ } else {
415
+ element.removeAttribute(name);
416
+ }
417
+ }
388
418
  function tagEncodedElement(element, decoded) {
389
419
  element.setAttribute("data-prepr-encoded", "");
390
420
  element.setAttribute("data-prepr-href", decoded.href);
391
421
  element.setAttribute("data-prepr-origin", decoded.origin);
422
+ const { id, field, locale } = parseEditHref(decoded.href);
423
+ setOrRemove(element, "data-prepr-id", id);
424
+ setOrRemove(element, "data-prepr-field", field);
425
+ setOrRemove(element, "data-prepr-locale", locale);
392
426
  }
393
427
 
394
428
  // src/core/stega/auto-clean.ts
395
- var debug2 = createScopedLogger("stega:auto-clean");
429
+ var debug3 = createScopedLogger("stega:auto-clean");
396
430
  function createStegaAutoClean(options) {
397
431
  const stripText = options?.enabled ?? true;
398
432
  let observer = null;
@@ -435,7 +469,7 @@ function createStegaAutoClean(options) {
435
469
  }
436
470
  pendingHidden.add(textNode);
437
471
  } catch (error) {
438
- debug2.log("error cleaning text node", error);
472
+ debug3.log("error cleaning text node", error);
439
473
  }
440
474
  return false;
441
475
  }
@@ -521,7 +555,7 @@ function createStegaAutoClean(options) {
521
555
  if (observer || typeof window === "undefined") return;
522
556
  scanAndClean();
523
557
  setupObserver();
524
- debug2.log("stega auto-clean started");
558
+ debug3.log("stega auto-clean started");
525
559
  }
526
560
  function stop() {
527
561
  if (debounceTimeout) {
@@ -535,13 +569,13 @@ function createStegaAutoClean(options) {
535
569
  cleaned = /* @__PURE__ */ new WeakMap();
536
570
  pendingHidden = /* @__PURE__ */ new Set();
537
571
  pendingDirty = /* @__PURE__ */ new Set();
538
- debug2.log("stega auto-clean stopped");
572
+ debug3.log("stega auto-clean stopped");
539
573
  }
540
574
  return { start, stop };
541
575
  }
542
576
 
543
577
  // src/core/stega/elements.ts
544
- var debug3 = createScopedLogger("stega:elements");
578
+ var debug4 = createScopedLogger("stega:elements");
545
579
  var StegaElements = class {
546
580
  constructor(decode = decodeStega) {
547
581
  this.observer = null;
@@ -586,7 +620,7 @@ var StegaElements = class {
586
620
  walkTextNodes(document.body, (textNode) => {
587
621
  if (this.tagFromTextNode(textNode)) encodedCount++;
588
622
  });
589
- debug3.log("document scan complete, encoded", encodedCount, "elements");
623
+ debug4.log("document scan complete, encoded", encodedCount, "elements");
590
624
  this.elements = document.querySelectorAll("[data-prepr-encoded]");
591
625
  }
592
626
  /** `onUpdate` fires after each debounced batch, for refreshing derived state. */
@@ -642,6 +676,9 @@ var StegaElements = class {
642
676
  element.removeAttribute("data-prepr-encoded");
643
677
  element.removeAttribute("data-prepr-href");
644
678
  element.removeAttribute("data-prepr-origin");
679
+ element.removeAttribute("data-prepr-id");
680
+ element.removeAttribute("data-prepr-field");
681
+ element.removeAttribute("data-prepr-locale");
645
682
  element.classList.remove("prepr-overlay-active");
646
683
  });
647
684
  this.elements = void 0;
@@ -649,7 +686,7 @@ var StegaElements = class {
649
686
  };
650
687
 
651
688
  // src/core/stega/overlay.ts
652
- var debug4 = createScopedLogger("stega:overlay");
689
+ var debug5 = createScopedLogger("stega:overlay");
653
690
  var StegaOverlay = class {
654
691
  constructor(onEdit) {
655
692
  this.overlay = null;
@@ -694,7 +731,8 @@ var StegaOverlay = class {
694
731
  const origin = element.getAttribute("data-prepr-origin");
695
732
  const id = element.getAttribute("data-prepr-id") ?? void 0;
696
733
  const field = element.getAttribute("data-prepr-field") ?? void 0;
697
- debug4.log("showing overlay", { href, origin });
734
+ const locale = element.getAttribute("data-prepr-locale") ?? void 0;
735
+ debug5.log("showing overlay", { href, origin });
698
736
  const overlay = this.overlay;
699
737
  overlay.style.display = "block";
700
738
  overlay.style.top = `${rect.top + window.scrollY - 2}px`;
@@ -726,7 +764,7 @@ var StegaOverlay = class {
726
764
  tooltip.style.left = `${left}px`;
727
765
  });
728
766
  tooltip.onclick = () => {
729
- this.onEdit({ href, origin, id, field });
767
+ this.onEdit({ href, origin, id, field, locale });
730
768
  };
731
769
  }
732
770
  this.currentElement = element;
@@ -766,7 +804,7 @@ var StegaOverlay = class {
766
804
  };
767
805
 
768
806
  // src/core/stega/proximity.ts
769
- var debug5 = createScopedLogger("stega:proximity");
807
+ var debug6 = createScopedLogger("stega:proximity");
770
808
  var StegaProximity = class {
771
809
  constructor() {
772
810
  this.highlighted = /* @__PURE__ */ new Set();
@@ -801,9 +839,9 @@ var StegaProximity = class {
801
839
  );
802
840
  const nodes = this.getEncodedElements();
803
841
  nodes.forEach((el) => this.observer.observe(el));
804
- debug5.log("observing", nodes.length, "encoded elements");
842
+ debug6.log("observing", nodes.length, "encoded elements");
805
843
  } catch (error) {
806
- debug5.log("error setting up IntersectionObserver", error);
844
+ debug6.log("error setting up IntersectionObserver", error);
807
845
  }
808
846
  }
809
847
  stopObserving() {
@@ -873,7 +911,7 @@ function removeStegaStyles() {
873
911
  }
874
912
 
875
913
  // src/core/stega/scan.ts
876
- var debug6 = createScopedLogger("stega:scan");
914
+ var debug7 = createScopedLogger("stega:scan");
877
915
  var EDITOR_MODE_STYLES = "[data-prepr-encoded]{cursor:pointer}";
878
916
  function createStegaController(opts) {
879
917
  const tooltipEnabled = opts.tooltip ?? true;
@@ -906,12 +944,13 @@ function createStegaController(opts) {
906
944
  href: encoded.getAttribute("data-prepr-href") ?? "",
907
945
  origin: encoded.getAttribute("data-prepr-origin") ?? "",
908
946
  id: encoded.getAttribute("data-prepr-id") ?? void 0,
909
- field: encoded.getAttribute("data-prepr-field") ?? void 0
947
+ field: encoded.getAttribute("data-prepr-field") ?? void 0,
948
+ locale: encoded.getAttribute("data-prepr-locale") ?? void 0
910
949
  });
911
950
  };
912
951
  const start = () => {
913
952
  if (initialized) {
914
- debug6.log("already initialized, skipping setup");
953
+ debug7.log("already initialized, skipping setup");
915
954
  return;
916
955
  }
917
956
  if (!tooltipEnabled) {
@@ -921,7 +960,7 @@ function createStegaController(opts) {
921
960
  });
922
961
  document.addEventListener("click", handleClick, { capture: true });
923
962
  initialized = true;
924
- debug6.log("stega controller started (editor mode)");
963
+ debug7.log("stega controller started (editor mode)");
925
964
  return;
926
965
  }
927
966
  injectStegaStyles();
@@ -952,7 +991,7 @@ function createStegaController(opts) {
952
991
  passive: true
953
992
  });
954
993
  initialized = true;
955
- debug6.log("stega controller started");
994
+ debug7.log("stega controller started");
956
995
  };
957
996
  const stop = () => {
958
997
  if (!initialized) return;
@@ -961,7 +1000,7 @@ function createStegaController(opts) {
961
1000
  elements.cleanup();
962
1001
  removeStegaStyles();
963
1002
  initialized = false;
964
- debug6.log("stega controller stopped (editor mode)");
1003
+ debug7.log("stega controller stopped (editor mode)");
965
1004
  return;
966
1005
  }
967
1006
  if (throttledMouseMove) {
@@ -981,11 +1020,43 @@ function createStegaController(opts) {
981
1020
  elements.cleanup();
982
1021
  removeStegaStyles();
983
1022
  initialized = false;
984
- debug6.log("stega controller stopped");
1023
+ debug7.log("stega controller stopped");
985
1024
  };
986
1025
  return { start, stop };
987
1026
  }
988
1027
 
1028
+ // src/core/stega/scroll-to-field.ts
1029
+ var debug8 = createScopedLogger("stega:scroll");
1030
+ var FLASH_DURATION_MS = 1200;
1031
+ var flashTimeout = null;
1032
+ function scrollToField(request) {
1033
+ const { field, id } = request;
1034
+ if (!field) return false;
1035
+ const candidates = document.querySelectorAll(
1036
+ "[data-prepr-encoded]"
1037
+ );
1038
+ let target = null;
1039
+ for (const candidate of candidates) {
1040
+ if (candidate.getAttribute("data-prepr-field") !== field) continue;
1041
+ if (id && candidate.getAttribute("data-prepr-id") !== id) continue;
1042
+ target = candidate;
1043
+ break;
1044
+ }
1045
+ if (!target) {
1046
+ debug8.log("no element found for field", field);
1047
+ return false;
1048
+ }
1049
+ target.scrollIntoView({ behavior: "smooth", block: "center" });
1050
+ if (flashTimeout) clearTimeout(flashTimeout);
1051
+ document.querySelectorAll(".prepr-overlay-active").forEach((el) => el.classList.remove("prepr-overlay-active"));
1052
+ target.classList.add("prepr-overlay-active");
1053
+ flashTimeout = setTimeout(() => {
1054
+ target.classList.remove("prepr-overlay-active");
1055
+ flashTimeout = null;
1056
+ }, FLASH_DURATION_MS);
1057
+ return true;
1058
+ }
1059
+
989
1060
  // src/core/store.ts
990
1061
  var DEFAULT_STATE = {
991
1062
  locale: "en",
@@ -2098,7 +2169,7 @@ function definePreprToolbar() {
2098
2169
  }
2099
2170
 
2100
2171
  // src/core/create-preview.ts
2101
- var debug7 = createScopedLogger("create-preview");
2172
+ var debug9 = createScopedLogger("create-preview");
2102
2173
  var controllerStores = /* @__PURE__ */ new WeakMap();
2103
2174
  function defaultNavigation() {
2104
2175
  return {
@@ -2140,7 +2211,7 @@ function createPreprPreview(opts = {}) {
2140
2211
  const search = new URLSearchParams(window.location.search);
2141
2212
  const hideBar = search.get(PARAM_HIDE_BAR) === "true";
2142
2213
  if (hideBar) {
2143
- debug7.log(`${PARAM_HIDE_BAR}=true \u2014 no visible toolbar`);
2214
+ debug9.log(`${PARAM_HIDE_BAR}=true \u2014 no visible toolbar`);
2144
2215
  }
2145
2216
  const mountUi = (options?.ui ?? true) && !isIframe && !hideBar;
2146
2217
  const locale = resolveLocale(options);
@@ -2176,18 +2247,17 @@ function createPreprPreview(opts = {}) {
2176
2247
  tooltip: mountUi,
2177
2248
  // In the editor, ask the parent to focus the field instead of opening a new
2178
2249
  // tab. Standalone previews keep the window.open behaviour.
2179
- onEdit: ({ href, origin, id, field }) => {
2250
+ onEdit: ({ href, id, field, locale: contentLocale }) => {
2180
2251
  const safeHref = href ? safeEditUrl(href) : null;
2181
2252
  if (href && !safeHref) {
2182
- debug7.warn("ignored edit request with unsupported href scheme");
2253
+ debug9.warn("ignored edit request with unsupported href scheme");
2183
2254
  return;
2184
2255
  }
2185
2256
  if (isIframe) {
2186
2257
  sendPreprEvent("field_edit_requested", {
2187
- href: safeHref ?? void 0,
2188
- origin,
2189
2258
  id,
2190
- field
2259
+ field,
2260
+ locale: contentLocale
2191
2261
  });
2192
2262
  } else if (safeHref) {
2193
2263
  window.open(safeHref, "_blank", "noopener,noreferrer");
@@ -2214,7 +2284,15 @@ function createPreprPreview(opts = {}) {
2214
2284
  handleChange(before, state);
2215
2285
  });
2216
2286
  const bridge = createIframeBridge(store, {
2217
- allowedEditorOrigins: options?.allowedEditorOrigins
2287
+ allowedEditorOrigins: options?.allowedEditorOrigins,
2288
+ // The editor selecting a field scrolls the matching element into view. The
2289
+ // lookup relies on the attributes the stega scan writes, so it finds
2290
+ // nothing until encoded content has been tagged.
2291
+ onScrollToField: ({ field, id }) => {
2292
+ if (!scrollToField({ field, id })) {
2293
+ debug9.log("no element on this page renders field", field);
2294
+ }
2295
+ }
2218
2296
  });
2219
2297
  sendPreprEvent(
2220
2298
  "getScrollPosition",
@@ -2234,7 +2312,7 @@ function createPreprPreview(opts = {}) {
2234
2312
  bridge.stop();
2235
2313
  }
2236
2314
  el?.remove();
2237
- debug7.log("preview destroyed");
2315
+ debug9.log("preview destroyed");
2238
2316
  }
2239
2317
  const controller = { destroy };
2240
2318
  controllerStores.set(controller, store);
@@ -2242,7 +2320,7 @@ function createPreprPreview(opts = {}) {
2242
2320
  }
2243
2321
 
2244
2322
  // src/version.ts
2245
- var VERSION = "0.4.2";
2323
+ var VERSION = "0.5.0-rc.1";
2246
2324
 
2247
2325
  // src/core/pixel.ts
2248
2326
  var PIXEL_SCRIPT_URL = "https://cdn.tracking.prepr.io/js/prepr.min.js";
@@ -35,7 +35,7 @@ __export(sveltekit_exports, {
35
35
  module.exports = __toCommonJS(sveltekit_exports);
36
36
 
37
37
  // src/version.ts
38
- var VERSION = "0.4.2";
38
+ var VERSION = "0.5.0-rc.1";
39
39
 
40
40
  // src/core/constants.ts
41
41
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
@@ -144,7 +144,7 @@ function processFrameworkRequest(request, opts, framework) {
144
144
  }
145
145
  const userAgent = request.headers.get("user-agent");
146
146
  if (userAgent) {
147
- requestHeaders.set("Prepr-User-Agent", sanitizeHeaderValue(userAgent));
147
+ requestHeaders.set("User-Agent", sanitizeHeaderValue(userAgent));
148
148
  }
149
149
  const packageParts = [`@preprio/toolkit@${opts?.version ?? VERSION}`];
150
150
  if (framework) packageParts.push(`framework=${framework}`);
@@ -292,7 +292,7 @@ var PREPR_HEADER_KEYS = [
292
292
  "Prepr-Visitor-IP",
293
293
  "Prepr-Hubspot-Id",
294
294
  "Prepr-Customer-Id-Created",
295
- "Prepr-User-Agent",
295
+ "User-Agent",
296
296
  "Prepr-Package"
297
297
  ];
298
298
  function getPreprHeadersFromHeaders(headers) {
@@ -59,6 +59,9 @@ interface PreprHeaders {
59
59
  readonly 'Prepr-Visitor-IP'?: string;
60
60
  readonly 'Prepr-Hubspot-Id'?: string;
61
61
  readonly 'Prepr-Customer-Id-Created'?: 'true';
62
+ /** The visitor's browser User-Agent, used by the API for device detection. */
63
+ readonly 'User-Agent'?: string;
64
+ /** @deprecated Never populated; the visitor's value is under `User-Agent`. */
62
65
  readonly 'Prepr-User-Agent'?: string;
63
66
  readonly 'Prepr-Package'?: string;
64
67
  }
@@ -59,6 +59,9 @@ interface PreprHeaders {
59
59
  readonly 'Prepr-Visitor-IP'?: string;
60
60
  readonly 'Prepr-Hubspot-Id'?: string;
61
61
  readonly 'Prepr-Customer-Id-Created'?: 'true';
62
+ /** The visitor's browser User-Agent, used by the API for device detection. */
63
+ readonly 'User-Agent'?: string;
64
+ /** @deprecated Never populated; the visitor's value is under `User-Agent`. */
62
65
  readonly 'Prepr-User-Agent'?: string;
63
66
  readonly 'Prepr-Package'?: string;
64
67
  }
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "0.4.2";
2
+ var VERSION = "0.5.0-rc.1";
3
3
 
4
4
  // src/core/constants.ts
5
5
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
@@ -108,7 +108,7 @@ function processFrameworkRequest(request, opts, framework) {
108
108
  }
109
109
  const userAgent = request.headers.get("user-agent");
110
110
  if (userAgent) {
111
- requestHeaders.set("Prepr-User-Agent", sanitizeHeaderValue(userAgent));
111
+ requestHeaders.set("User-Agent", sanitizeHeaderValue(userAgent));
112
112
  }
113
113
  const packageParts = [`@preprio/toolkit@${opts?.version ?? VERSION}`];
114
114
  if (framework) packageParts.push(`framework=${framework}`);
@@ -256,7 +256,7 @@ var PREPR_HEADER_KEYS = [
256
256
  "Prepr-Visitor-IP",
257
257
  "Prepr-Hubspot-Id",
258
258
  "Prepr-Customer-Id-Created",
259
- "Prepr-User-Agent",
259
+ "User-Agent",
260
260
  "Prepr-Package"
261
261
  ];
262
262
  function getPreprHeadersFromHeaders(headers) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preprio/toolkit",
3
- "version": "0.4.2",
3
+ "version": "0.5.0-rc.1",
4
4
  "description": "Prepr personalization, A/B testing and preview toolkit for React, Next.js, Nuxt, Astro, SvelteKit and Express",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Prepr B.V. (https://prepr.io)",