@preprio/toolkit 0.4.3 → 0.5.0-rc.2

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,9 +267,13 @@ 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
- if (allowed) return allowed.includes(origin);
273
+ if (allowed) {
274
+ if (allowed.includes("*")) return origin !== "null";
275
+ return allowed.includes(origin);
276
+ }
273
277
  let url;
274
278
  try {
275
279
  url = new URL(origin);
@@ -284,6 +288,7 @@ function isAllowedEditorOrigin(origin, allowed) {
284
288
  }
285
289
  function createIframeBridge(store, options = {}) {
286
290
  const allowedOrigins = options.allowedEditorOrigins;
291
+ const onScrollToField = options.onScrollToField;
287
292
  let parentOrigin = null;
288
293
  const onKeyDown = (event) => {
289
294
  const key = event.key.toLowerCase();
@@ -293,7 +298,10 @@ function createIframeBridge(store, options = {}) {
293
298
  const onMessage = (evt) => {
294
299
  const data = evt?.data;
295
300
  if (data?.event === "prepr:initVE" && !parentOrigin) {
296
- if (!isAllowedEditorOrigin(evt.origin, allowedOrigins)) return;
301
+ if (!isAllowedEditorOrigin(evt.origin, allowedOrigins)) {
302
+ debug.warn("ignored editor handshake from origin", evt.origin);
303
+ return;
304
+ }
297
305
  parentOrigin = evt.origin;
298
306
  setTrustedParentOrigin(parentOrigin);
299
307
  if (data.scrollPosition != null) {
@@ -307,6 +315,15 @@ function createIframeBridge(store, options = {}) {
307
315
  const currentScrollY = window.scrollY || document.documentElement.scrollTop;
308
316
  sendPreprEvent("getScrollPosition", { value: currentScrollY });
309
317
  }
318
+ if (data?.event === "prepr:scrollToField") {
319
+ if (typeof data.field !== "string" || !data.field) return;
320
+ if (data.id !== void 0 && typeof data.id !== "string") return;
321
+ try {
322
+ onScrollToField?.({ field: data.field, id: data.id });
323
+ } catch (error) {
324
+ debug.warn("scrollToField handler failed", error);
325
+ }
326
+ }
310
327
  };
311
328
  return {
312
329
  start() {
@@ -331,7 +348,7 @@ import { vercelStegaSplit } from "@vercel/stega";
331
348
 
332
349
  // src/core/stega/clean.ts
333
350
  import { vercelStegaClean, vercelStegaDecode } from "@vercel/stega";
334
- var debug = createScopedLogger("stega:clean");
351
+ var debug2 = createScopedLogger("stega:clean");
335
352
  function decodeStega(str) {
336
353
  if (!str) return null;
337
354
  try {
@@ -340,7 +357,7 @@ function decodeStega(str) {
340
357
  return decoded;
341
358
  }
342
359
  } catch (error) {
343
- debug.log("error decoding stega data", error);
360
+ debug2.log("error decoding stega data", error);
344
361
  const match = str.match(/{"origin.*?}/);
345
362
  if (match) {
346
363
  try {
@@ -349,7 +366,7 @@ function decodeStega(str) {
349
366
  return decodedMatch;
350
367
  }
351
368
  } catch (innerError) {
352
- debug.log("error decoding stega regex match", innerError);
369
+ debug2.log("error decoding stega regex match", innerError);
353
370
  }
354
371
  }
355
372
  }
@@ -385,14 +402,37 @@ function isRenderedElement(element) {
385
402
  if (!style) return true;
386
403
  return style.visibility !== "hidden" && style.display !== "none";
387
404
  }
405
+ function parseEditHref(href) {
406
+ try {
407
+ const url = new URL(href, window.location.origin);
408
+ return {
409
+ id: url.pathname.split("/").pop() || void 0,
410
+ field: url.searchParams.get("field") || void 0,
411
+ locale: url.searchParams.get("locale") || void 0
412
+ };
413
+ } catch {
414
+ return {};
415
+ }
416
+ }
417
+ function setOrRemove(element, name, value) {
418
+ if (value) {
419
+ element.setAttribute(name, value);
420
+ } else {
421
+ element.removeAttribute(name);
422
+ }
423
+ }
388
424
  function tagEncodedElement(element, decoded) {
389
425
  element.setAttribute("data-prepr-encoded", "");
390
426
  element.setAttribute("data-prepr-href", decoded.href);
391
427
  element.setAttribute("data-prepr-origin", decoded.origin);
428
+ const { id, field, locale } = parseEditHref(decoded.href);
429
+ setOrRemove(element, "data-prepr-id", id);
430
+ setOrRemove(element, "data-prepr-field", field);
431
+ setOrRemove(element, "data-prepr-locale", locale);
392
432
  }
393
433
 
394
434
  // src/core/stega/auto-clean.ts
395
- var debug2 = createScopedLogger("stega:auto-clean");
435
+ var debug3 = createScopedLogger("stega:auto-clean");
396
436
  function createStegaAutoClean(options) {
397
437
  const stripText = options?.enabled ?? true;
398
438
  let observer = null;
@@ -435,7 +475,7 @@ function createStegaAutoClean(options) {
435
475
  }
436
476
  pendingHidden.add(textNode);
437
477
  } catch (error) {
438
- debug2.log("error cleaning text node", error);
478
+ debug3.log("error cleaning text node", error);
439
479
  }
440
480
  return false;
441
481
  }
@@ -521,7 +561,7 @@ function createStegaAutoClean(options) {
521
561
  if (observer || typeof window === "undefined") return;
522
562
  scanAndClean();
523
563
  setupObserver();
524
- debug2.log("stega auto-clean started");
564
+ debug3.log("stega auto-clean started");
525
565
  }
526
566
  function stop() {
527
567
  if (debounceTimeout) {
@@ -535,13 +575,13 @@ function createStegaAutoClean(options) {
535
575
  cleaned = /* @__PURE__ */ new WeakMap();
536
576
  pendingHidden = /* @__PURE__ */ new Set();
537
577
  pendingDirty = /* @__PURE__ */ new Set();
538
- debug2.log("stega auto-clean stopped");
578
+ debug3.log("stega auto-clean stopped");
539
579
  }
540
580
  return { start, stop };
541
581
  }
542
582
 
543
583
  // src/core/stega/elements.ts
544
- var debug3 = createScopedLogger("stega:elements");
584
+ var debug4 = createScopedLogger("stega:elements");
545
585
  var StegaElements = class {
546
586
  constructor(decode = decodeStega) {
547
587
  this.observer = null;
@@ -586,7 +626,7 @@ var StegaElements = class {
586
626
  walkTextNodes(document.body, (textNode) => {
587
627
  if (this.tagFromTextNode(textNode)) encodedCount++;
588
628
  });
589
- debug3.log("document scan complete, encoded", encodedCount, "elements");
629
+ debug4.log("document scan complete, encoded", encodedCount, "elements");
590
630
  this.elements = document.querySelectorAll("[data-prepr-encoded]");
591
631
  }
592
632
  /** `onUpdate` fires after each debounced batch, for refreshing derived state. */
@@ -642,6 +682,9 @@ var StegaElements = class {
642
682
  element.removeAttribute("data-prepr-encoded");
643
683
  element.removeAttribute("data-prepr-href");
644
684
  element.removeAttribute("data-prepr-origin");
685
+ element.removeAttribute("data-prepr-id");
686
+ element.removeAttribute("data-prepr-field");
687
+ element.removeAttribute("data-prepr-locale");
645
688
  element.classList.remove("prepr-overlay-active");
646
689
  });
647
690
  this.elements = void 0;
@@ -649,7 +692,7 @@ var StegaElements = class {
649
692
  };
650
693
 
651
694
  // src/core/stega/overlay.ts
652
- var debug4 = createScopedLogger("stega:overlay");
695
+ var debug5 = createScopedLogger("stega:overlay");
653
696
  var StegaOverlay = class {
654
697
  constructor(onEdit) {
655
698
  this.overlay = null;
@@ -694,7 +737,8 @@ var StegaOverlay = class {
694
737
  const origin = element.getAttribute("data-prepr-origin");
695
738
  const id = element.getAttribute("data-prepr-id") ?? void 0;
696
739
  const field = element.getAttribute("data-prepr-field") ?? void 0;
697
- debug4.log("showing overlay", { href, origin });
740
+ const locale = element.getAttribute("data-prepr-locale") ?? void 0;
741
+ debug5.log("showing overlay", { href, origin });
698
742
  const overlay = this.overlay;
699
743
  overlay.style.display = "block";
700
744
  overlay.style.top = `${rect.top + window.scrollY - 2}px`;
@@ -726,7 +770,7 @@ var StegaOverlay = class {
726
770
  tooltip.style.left = `${left}px`;
727
771
  });
728
772
  tooltip.onclick = () => {
729
- this.onEdit({ href, origin, id, field });
773
+ this.onEdit({ href, origin, id, field, locale });
730
774
  };
731
775
  }
732
776
  this.currentElement = element;
@@ -766,7 +810,7 @@ var StegaOverlay = class {
766
810
  };
767
811
 
768
812
  // src/core/stega/proximity.ts
769
- var debug5 = createScopedLogger("stega:proximity");
813
+ var debug6 = createScopedLogger("stega:proximity");
770
814
  var StegaProximity = class {
771
815
  constructor() {
772
816
  this.highlighted = /* @__PURE__ */ new Set();
@@ -801,9 +845,9 @@ var StegaProximity = class {
801
845
  );
802
846
  const nodes = this.getEncodedElements();
803
847
  nodes.forEach((el) => this.observer.observe(el));
804
- debug5.log("observing", nodes.length, "encoded elements");
848
+ debug6.log("observing", nodes.length, "encoded elements");
805
849
  } catch (error) {
806
- debug5.log("error setting up IntersectionObserver", error);
850
+ debug6.log("error setting up IntersectionObserver", error);
807
851
  }
808
852
  }
809
853
  stopObserving() {
@@ -873,7 +917,7 @@ function removeStegaStyles() {
873
917
  }
874
918
 
875
919
  // src/core/stega/scan.ts
876
- var debug6 = createScopedLogger("stega:scan");
920
+ var debug7 = createScopedLogger("stega:scan");
877
921
  var EDITOR_MODE_STYLES = "[data-prepr-encoded]{cursor:pointer}";
878
922
  function createStegaController(opts) {
879
923
  const tooltipEnabled = opts.tooltip ?? true;
@@ -906,12 +950,13 @@ function createStegaController(opts) {
906
950
  href: encoded.getAttribute("data-prepr-href") ?? "",
907
951
  origin: encoded.getAttribute("data-prepr-origin") ?? "",
908
952
  id: encoded.getAttribute("data-prepr-id") ?? void 0,
909
- field: encoded.getAttribute("data-prepr-field") ?? void 0
953
+ field: encoded.getAttribute("data-prepr-field") ?? void 0,
954
+ locale: encoded.getAttribute("data-prepr-locale") ?? void 0
910
955
  });
911
956
  };
912
957
  const start = () => {
913
958
  if (initialized) {
914
- debug6.log("already initialized, skipping setup");
959
+ debug7.log("already initialized, skipping setup");
915
960
  return;
916
961
  }
917
962
  if (!tooltipEnabled) {
@@ -921,7 +966,7 @@ function createStegaController(opts) {
921
966
  });
922
967
  document.addEventListener("click", handleClick, { capture: true });
923
968
  initialized = true;
924
- debug6.log("stega controller started (editor mode)");
969
+ debug7.log("stega controller started (editor mode)");
925
970
  return;
926
971
  }
927
972
  injectStegaStyles();
@@ -952,7 +997,7 @@ function createStegaController(opts) {
952
997
  passive: true
953
998
  });
954
999
  initialized = true;
955
- debug6.log("stega controller started");
1000
+ debug7.log("stega controller started");
956
1001
  };
957
1002
  const stop = () => {
958
1003
  if (!initialized) return;
@@ -961,7 +1006,7 @@ function createStegaController(opts) {
961
1006
  elements.cleanup();
962
1007
  removeStegaStyles();
963
1008
  initialized = false;
964
- debug6.log("stega controller stopped (editor mode)");
1009
+ debug7.log("stega controller stopped (editor mode)");
965
1010
  return;
966
1011
  }
967
1012
  if (throttledMouseMove) {
@@ -981,11 +1026,43 @@ function createStegaController(opts) {
981
1026
  elements.cleanup();
982
1027
  removeStegaStyles();
983
1028
  initialized = false;
984
- debug6.log("stega controller stopped");
1029
+ debug7.log("stega controller stopped");
985
1030
  };
986
1031
  return { start, stop };
987
1032
  }
988
1033
 
1034
+ // src/core/stega/scroll-to-field.ts
1035
+ var debug8 = createScopedLogger("stega:scroll");
1036
+ var FLASH_DURATION_MS = 1200;
1037
+ var flashTimeout = null;
1038
+ function scrollToField(request) {
1039
+ const { field, id } = request;
1040
+ if (!field) return false;
1041
+ const candidates = document.querySelectorAll(
1042
+ "[data-prepr-encoded]"
1043
+ );
1044
+ let target = null;
1045
+ for (const candidate of candidates) {
1046
+ if (candidate.getAttribute("data-prepr-field") !== field) continue;
1047
+ if (id && candidate.getAttribute("data-prepr-id") !== id) continue;
1048
+ target = candidate;
1049
+ break;
1050
+ }
1051
+ if (!target) {
1052
+ debug8.log("no element found for field", field);
1053
+ return false;
1054
+ }
1055
+ target.scrollIntoView({ behavior: "smooth", block: "center" });
1056
+ if (flashTimeout) clearTimeout(flashTimeout);
1057
+ document.querySelectorAll(".prepr-overlay-active").forEach((el) => el.classList.remove("prepr-overlay-active"));
1058
+ target.classList.add("prepr-overlay-active");
1059
+ flashTimeout = setTimeout(() => {
1060
+ target.classList.remove("prepr-overlay-active");
1061
+ flashTimeout = null;
1062
+ }, FLASH_DURATION_MS);
1063
+ return true;
1064
+ }
1065
+
989
1066
  // src/core/store.ts
990
1067
  var DEFAULT_STATE = {
991
1068
  locale: "en",
@@ -2098,7 +2175,7 @@ function definePreprToolbar() {
2098
2175
  }
2099
2176
 
2100
2177
  // src/core/create-preview.ts
2101
- var debug7 = createScopedLogger("create-preview");
2178
+ var debug9 = createScopedLogger("create-preview");
2102
2179
  var controllerStores = /* @__PURE__ */ new WeakMap();
2103
2180
  function defaultNavigation() {
2104
2181
  return {
@@ -2140,7 +2217,7 @@ function createPreprPreview(opts = {}) {
2140
2217
  const search = new URLSearchParams(window.location.search);
2141
2218
  const hideBar = search.get(PARAM_HIDE_BAR) === "true";
2142
2219
  if (hideBar) {
2143
- debug7.log(`${PARAM_HIDE_BAR}=true \u2014 no visible toolbar`);
2220
+ debug9.log(`${PARAM_HIDE_BAR}=true \u2014 no visible toolbar`);
2144
2221
  }
2145
2222
  const mountUi = (options?.ui ?? true) && !isIframe && !hideBar;
2146
2223
  const locale = resolveLocale(options);
@@ -2176,18 +2253,17 @@ function createPreprPreview(opts = {}) {
2176
2253
  tooltip: mountUi,
2177
2254
  // In the editor, ask the parent to focus the field instead of opening a new
2178
2255
  // tab. Standalone previews keep the window.open behaviour.
2179
- onEdit: ({ href, origin, id, field }) => {
2256
+ onEdit: ({ href, id, field, locale: contentLocale }) => {
2180
2257
  const safeHref = href ? safeEditUrl(href) : null;
2181
2258
  if (href && !safeHref) {
2182
- debug7.warn("ignored edit request with unsupported href scheme");
2259
+ debug9.warn("ignored edit request with unsupported href scheme");
2183
2260
  return;
2184
2261
  }
2185
2262
  if (isIframe) {
2186
2263
  sendPreprEvent("field_edit_requested", {
2187
- href: safeHref ?? void 0,
2188
- origin,
2189
2264
  id,
2190
- field
2265
+ field,
2266
+ locale: contentLocale
2191
2267
  });
2192
2268
  } else if (safeHref) {
2193
2269
  window.open(safeHref, "_blank", "noopener,noreferrer");
@@ -2214,7 +2290,15 @@ function createPreprPreview(opts = {}) {
2214
2290
  handleChange(before, state);
2215
2291
  });
2216
2292
  const bridge = createIframeBridge(store, {
2217
- allowedEditorOrigins: options?.allowedEditorOrigins
2293
+ allowedEditorOrigins: options?.allowedEditorOrigins,
2294
+ // The editor selecting a field scrolls the matching element into view. The
2295
+ // lookup relies on the attributes the stega scan writes, so it finds
2296
+ // nothing until encoded content has been tagged.
2297
+ onScrollToField: ({ field, id }) => {
2298
+ if (!scrollToField({ field, id })) {
2299
+ debug9.log("no element on this page renders field", field);
2300
+ }
2301
+ }
2218
2302
  });
2219
2303
  sendPreprEvent(
2220
2304
  "getScrollPosition",
@@ -2234,7 +2318,7 @@ function createPreprPreview(opts = {}) {
2234
2318
  bridge.stop();
2235
2319
  }
2236
2320
  el?.remove();
2237
- debug7.log("preview destroyed");
2321
+ debug9.log("preview destroyed");
2238
2322
  }
2239
2323
  const controller = { destroy };
2240
2324
  controllerStores.set(controller, store);
@@ -2242,7 +2326,7 @@ function createPreprPreview(opts = {}) {
2242
2326
  }
2243
2327
 
2244
2328
  // src/version.ts
2245
- var VERSION = "0.4.3";
2329
+ var VERSION = "0.5.0-rc.2";
2246
2330
 
2247
2331
  // src/core/pixel.ts
2248
2332
  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.3";
38
+ var VERSION = "0.5.0-rc.2";
39
39
 
40
40
  // src/core/constants.ts
41
41
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var VERSION = "0.4.3";
2
+ var VERSION = "0.5.0-rc.2";
3
3
 
4
4
  // src/core/constants.ts
5
5
  var COOKIE_PREVIEW_MODE = "Prepr-Preview-Mode";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preprio/toolkit",
3
- "version": "0.4.3",
3
+ "version": "0.5.0-rc.2",
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)",