@preprio/toolkit 0.4.3 → 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.
@@ -291,6 +291,7 @@ function createElementCache(query, ttl = 1e3) {
291
291
  }
292
292
 
293
293
  // src/core/iframe-bridge.ts
294
+ var debug = createScopedLogger("iframe-bridge");
294
295
  var EDITOR_BASE_DOMAIN = "prepr.io";
295
296
  function isAllowedEditorOrigin(origin, allowed) {
296
297
  if (allowed) return allowed.includes(origin);
@@ -308,6 +309,7 @@ function isAllowedEditorOrigin(origin, allowed) {
308
309
  }
309
310
  function createIframeBridge(store, options = {}) {
310
311
  const allowedOrigins = options.allowedEditorOrigins;
312
+ const onScrollToField = options.onScrollToField;
311
313
  let parentOrigin = null;
312
314
  const onKeyDown = (event) => {
313
315
  const key = event.key.toLowerCase();
@@ -331,6 +333,15 @@ function createIframeBridge(store, options = {}) {
331
333
  const currentScrollY = window.scrollY || document.documentElement.scrollTop;
332
334
  sendPreprEvent("getScrollPosition", { value: currentScrollY });
333
335
  }
336
+ if (data?.event === "prepr:scrollToField") {
337
+ if (typeof data.field !== "string" || !data.field) return;
338
+ if (data.id !== void 0 && typeof data.id !== "string") return;
339
+ try {
340
+ onScrollToField?.({ field: data.field, id: data.id });
341
+ } catch (error) {
342
+ debug.warn("scrollToField handler failed", error);
343
+ }
344
+ }
334
345
  };
335
346
  return {
336
347
  start() {
@@ -355,7 +366,7 @@ var import_stega2 = require("@vercel/stega");
355
366
 
356
367
  // src/core/stega/clean.ts
357
368
  var import_stega = require("@vercel/stega");
358
- var debug = createScopedLogger("stega:clean");
369
+ var debug2 = createScopedLogger("stega:clean");
359
370
  function decodeStega(str) {
360
371
  if (!str) return null;
361
372
  try {
@@ -364,7 +375,7 @@ function decodeStega(str) {
364
375
  return decoded;
365
376
  }
366
377
  } catch (error) {
367
- debug.log("error decoding stega data", error);
378
+ debug2.log("error decoding stega data", error);
368
379
  const match = str.match(/{"origin.*?}/);
369
380
  if (match) {
370
381
  try {
@@ -373,7 +384,7 @@ function decodeStega(str) {
373
384
  return decodedMatch;
374
385
  }
375
386
  } catch (innerError) {
376
- debug.log("error decoding stega regex match", innerError);
387
+ debug2.log("error decoding stega regex match", innerError);
377
388
  }
378
389
  }
379
390
  }
@@ -409,14 +420,37 @@ function isRenderedElement(element) {
409
420
  if (!style) return true;
410
421
  return style.visibility !== "hidden" && style.display !== "none";
411
422
  }
423
+ function parseEditHref(href) {
424
+ try {
425
+ const url = new URL(href, window.location.origin);
426
+ return {
427
+ id: url.pathname.split("/").pop() || void 0,
428
+ field: url.searchParams.get("field") || void 0,
429
+ locale: url.searchParams.get("locale") || void 0
430
+ };
431
+ } catch {
432
+ return {};
433
+ }
434
+ }
435
+ function setOrRemove(element, name, value) {
436
+ if (value) {
437
+ element.setAttribute(name, value);
438
+ } else {
439
+ element.removeAttribute(name);
440
+ }
441
+ }
412
442
  function tagEncodedElement(element, decoded) {
413
443
  element.setAttribute("data-prepr-encoded", "");
414
444
  element.setAttribute("data-prepr-href", decoded.href);
415
445
  element.setAttribute("data-prepr-origin", decoded.origin);
446
+ const { id, field, locale } = parseEditHref(decoded.href);
447
+ setOrRemove(element, "data-prepr-id", id);
448
+ setOrRemove(element, "data-prepr-field", field);
449
+ setOrRemove(element, "data-prepr-locale", locale);
416
450
  }
417
451
 
418
452
  // src/core/stega/auto-clean.ts
419
- var debug2 = createScopedLogger("stega:auto-clean");
453
+ var debug3 = createScopedLogger("stega:auto-clean");
420
454
  function createStegaAutoClean(options) {
421
455
  const stripText = options?.enabled ?? true;
422
456
  let observer = null;
@@ -459,7 +493,7 @@ function createStegaAutoClean(options) {
459
493
  }
460
494
  pendingHidden.add(textNode);
461
495
  } catch (error) {
462
- debug2.log("error cleaning text node", error);
496
+ debug3.log("error cleaning text node", error);
463
497
  }
464
498
  return false;
465
499
  }
@@ -545,7 +579,7 @@ function createStegaAutoClean(options) {
545
579
  if (observer || typeof window === "undefined") return;
546
580
  scanAndClean();
547
581
  setupObserver();
548
- debug2.log("stega auto-clean started");
582
+ debug3.log("stega auto-clean started");
549
583
  }
550
584
  function stop() {
551
585
  if (debounceTimeout) {
@@ -559,13 +593,13 @@ function createStegaAutoClean(options) {
559
593
  cleaned = /* @__PURE__ */ new WeakMap();
560
594
  pendingHidden = /* @__PURE__ */ new Set();
561
595
  pendingDirty = /* @__PURE__ */ new Set();
562
- debug2.log("stega auto-clean stopped");
596
+ debug3.log("stega auto-clean stopped");
563
597
  }
564
598
  return { start, stop };
565
599
  }
566
600
 
567
601
  // src/core/stega/elements.ts
568
- var debug3 = createScopedLogger("stega:elements");
602
+ var debug4 = createScopedLogger("stega:elements");
569
603
  var StegaElements = class {
570
604
  constructor(decode = decodeStega) {
571
605
  this.observer = null;
@@ -610,7 +644,7 @@ var StegaElements = class {
610
644
  walkTextNodes(document.body, (textNode) => {
611
645
  if (this.tagFromTextNode(textNode)) encodedCount++;
612
646
  });
613
- debug3.log("document scan complete, encoded", encodedCount, "elements");
647
+ debug4.log("document scan complete, encoded", encodedCount, "elements");
614
648
  this.elements = document.querySelectorAll("[data-prepr-encoded]");
615
649
  }
616
650
  /** `onUpdate` fires after each debounced batch, for refreshing derived state. */
@@ -666,6 +700,9 @@ var StegaElements = class {
666
700
  element.removeAttribute("data-prepr-encoded");
667
701
  element.removeAttribute("data-prepr-href");
668
702
  element.removeAttribute("data-prepr-origin");
703
+ element.removeAttribute("data-prepr-id");
704
+ element.removeAttribute("data-prepr-field");
705
+ element.removeAttribute("data-prepr-locale");
669
706
  element.classList.remove("prepr-overlay-active");
670
707
  });
671
708
  this.elements = void 0;
@@ -673,7 +710,7 @@ var StegaElements = class {
673
710
  };
674
711
 
675
712
  // src/core/stega/overlay.ts
676
- var debug4 = createScopedLogger("stega:overlay");
713
+ var debug5 = createScopedLogger("stega:overlay");
677
714
  var StegaOverlay = class {
678
715
  constructor(onEdit) {
679
716
  this.overlay = null;
@@ -718,7 +755,8 @@ var StegaOverlay = class {
718
755
  const origin = element.getAttribute("data-prepr-origin");
719
756
  const id = element.getAttribute("data-prepr-id") ?? void 0;
720
757
  const field = element.getAttribute("data-prepr-field") ?? void 0;
721
- debug4.log("showing overlay", { href, origin });
758
+ const locale = element.getAttribute("data-prepr-locale") ?? void 0;
759
+ debug5.log("showing overlay", { href, origin });
722
760
  const overlay = this.overlay;
723
761
  overlay.style.display = "block";
724
762
  overlay.style.top = `${rect.top + window.scrollY - 2}px`;
@@ -750,7 +788,7 @@ var StegaOverlay = class {
750
788
  tooltip.style.left = `${left}px`;
751
789
  });
752
790
  tooltip.onclick = () => {
753
- this.onEdit({ href, origin, id, field });
791
+ this.onEdit({ href, origin, id, field, locale });
754
792
  };
755
793
  }
756
794
  this.currentElement = element;
@@ -790,7 +828,7 @@ var StegaOverlay = class {
790
828
  };
791
829
 
792
830
  // src/core/stega/proximity.ts
793
- var debug5 = createScopedLogger("stega:proximity");
831
+ var debug6 = createScopedLogger("stega:proximity");
794
832
  var StegaProximity = class {
795
833
  constructor() {
796
834
  this.highlighted = /* @__PURE__ */ new Set();
@@ -825,9 +863,9 @@ var StegaProximity = class {
825
863
  );
826
864
  const nodes = this.getEncodedElements();
827
865
  nodes.forEach((el) => this.observer.observe(el));
828
- debug5.log("observing", nodes.length, "encoded elements");
866
+ debug6.log("observing", nodes.length, "encoded elements");
829
867
  } catch (error) {
830
- debug5.log("error setting up IntersectionObserver", error);
868
+ debug6.log("error setting up IntersectionObserver", error);
831
869
  }
832
870
  }
833
871
  stopObserving() {
@@ -897,7 +935,7 @@ function removeStegaStyles() {
897
935
  }
898
936
 
899
937
  // src/core/stega/scan.ts
900
- var debug6 = createScopedLogger("stega:scan");
938
+ var debug7 = createScopedLogger("stega:scan");
901
939
  var EDITOR_MODE_STYLES = "[data-prepr-encoded]{cursor:pointer}";
902
940
  function createStegaController(opts) {
903
941
  const tooltipEnabled = opts.tooltip ?? true;
@@ -930,12 +968,13 @@ function createStegaController(opts) {
930
968
  href: encoded.getAttribute("data-prepr-href") ?? "",
931
969
  origin: encoded.getAttribute("data-prepr-origin") ?? "",
932
970
  id: encoded.getAttribute("data-prepr-id") ?? void 0,
933
- field: encoded.getAttribute("data-prepr-field") ?? void 0
971
+ field: encoded.getAttribute("data-prepr-field") ?? void 0,
972
+ locale: encoded.getAttribute("data-prepr-locale") ?? void 0
934
973
  });
935
974
  };
936
975
  const start = () => {
937
976
  if (initialized) {
938
- debug6.log("already initialized, skipping setup");
977
+ debug7.log("already initialized, skipping setup");
939
978
  return;
940
979
  }
941
980
  if (!tooltipEnabled) {
@@ -945,7 +984,7 @@ function createStegaController(opts) {
945
984
  });
946
985
  document.addEventListener("click", handleClick, { capture: true });
947
986
  initialized = true;
948
- debug6.log("stega controller started (editor mode)");
987
+ debug7.log("stega controller started (editor mode)");
949
988
  return;
950
989
  }
951
990
  injectStegaStyles();
@@ -976,7 +1015,7 @@ function createStegaController(opts) {
976
1015
  passive: true
977
1016
  });
978
1017
  initialized = true;
979
- debug6.log("stega controller started");
1018
+ debug7.log("stega controller started");
980
1019
  };
981
1020
  const stop = () => {
982
1021
  if (!initialized) return;
@@ -985,7 +1024,7 @@ function createStegaController(opts) {
985
1024
  elements.cleanup();
986
1025
  removeStegaStyles();
987
1026
  initialized = false;
988
- debug6.log("stega controller stopped (editor mode)");
1027
+ debug7.log("stega controller stopped (editor mode)");
989
1028
  return;
990
1029
  }
991
1030
  if (throttledMouseMove) {
@@ -1005,11 +1044,43 @@ function createStegaController(opts) {
1005
1044
  elements.cleanup();
1006
1045
  removeStegaStyles();
1007
1046
  initialized = false;
1008
- debug6.log("stega controller stopped");
1047
+ debug7.log("stega controller stopped");
1009
1048
  };
1010
1049
  return { start, stop };
1011
1050
  }
1012
1051
 
1052
+ // src/core/stega/scroll-to-field.ts
1053
+ var debug8 = createScopedLogger("stega:scroll");
1054
+ var FLASH_DURATION_MS = 1200;
1055
+ var flashTimeout = null;
1056
+ function scrollToField(request) {
1057
+ const { field, id } = request;
1058
+ if (!field) return false;
1059
+ const candidates = document.querySelectorAll(
1060
+ "[data-prepr-encoded]"
1061
+ );
1062
+ let target = null;
1063
+ for (const candidate of candidates) {
1064
+ if (candidate.getAttribute("data-prepr-field") !== field) continue;
1065
+ if (id && candidate.getAttribute("data-prepr-id") !== id) continue;
1066
+ target = candidate;
1067
+ break;
1068
+ }
1069
+ if (!target) {
1070
+ debug8.log("no element found for field", field);
1071
+ return false;
1072
+ }
1073
+ target.scrollIntoView({ behavior: "smooth", block: "center" });
1074
+ if (flashTimeout) clearTimeout(flashTimeout);
1075
+ document.querySelectorAll(".prepr-overlay-active").forEach((el) => el.classList.remove("prepr-overlay-active"));
1076
+ target.classList.add("prepr-overlay-active");
1077
+ flashTimeout = setTimeout(() => {
1078
+ target.classList.remove("prepr-overlay-active");
1079
+ flashTimeout = null;
1080
+ }, FLASH_DURATION_MS);
1081
+ return true;
1082
+ }
1083
+
1013
1084
  // src/core/store.ts
1014
1085
  var DEFAULT_STATE = {
1015
1086
  locale: "en",
@@ -2122,7 +2193,7 @@ function definePreprToolbar() {
2122
2193
  }
2123
2194
 
2124
2195
  // src/core/create-preview.ts
2125
- var debug7 = createScopedLogger("create-preview");
2196
+ var debug9 = createScopedLogger("create-preview");
2126
2197
  var controllerStores = /* @__PURE__ */ new WeakMap();
2127
2198
  function defaultNavigation() {
2128
2199
  return {
@@ -2164,7 +2235,7 @@ function createPreprPreview(opts = {}) {
2164
2235
  const search = new URLSearchParams(window.location.search);
2165
2236
  const hideBar = search.get(PARAM_HIDE_BAR) === "true";
2166
2237
  if (hideBar) {
2167
- debug7.log(`${PARAM_HIDE_BAR}=true \u2014 no visible toolbar`);
2238
+ debug9.log(`${PARAM_HIDE_BAR}=true \u2014 no visible toolbar`);
2168
2239
  }
2169
2240
  const mountUi = (options?.ui ?? true) && !isIframe && !hideBar;
2170
2241
  const locale = resolveLocale(options);
@@ -2200,18 +2271,17 @@ function createPreprPreview(opts = {}) {
2200
2271
  tooltip: mountUi,
2201
2272
  // In the editor, ask the parent to focus the field instead of opening a new
2202
2273
  // tab. Standalone previews keep the window.open behaviour.
2203
- onEdit: ({ href, origin, id, field }) => {
2274
+ onEdit: ({ href, id, field, locale: contentLocale }) => {
2204
2275
  const safeHref = href ? safeEditUrl(href) : null;
2205
2276
  if (href && !safeHref) {
2206
- debug7.warn("ignored edit request with unsupported href scheme");
2277
+ debug9.warn("ignored edit request with unsupported href scheme");
2207
2278
  return;
2208
2279
  }
2209
2280
  if (isIframe) {
2210
2281
  sendPreprEvent("field_edit_requested", {
2211
- href: safeHref ?? void 0,
2212
- origin,
2213
2282
  id,
2214
- field
2283
+ field,
2284
+ locale: contentLocale
2215
2285
  });
2216
2286
  } else if (safeHref) {
2217
2287
  window.open(safeHref, "_blank", "noopener,noreferrer");
@@ -2238,7 +2308,15 @@ function createPreprPreview(opts = {}) {
2238
2308
  handleChange(before, state);
2239
2309
  });
2240
2310
  const bridge = createIframeBridge(store, {
2241
- allowedEditorOrigins: options?.allowedEditorOrigins
2311
+ allowedEditorOrigins: options?.allowedEditorOrigins,
2312
+ // The editor selecting a field scrolls the matching element into view. The
2313
+ // lookup relies on the attributes the stega scan writes, so it finds
2314
+ // nothing until encoded content has been tagged.
2315
+ onScrollToField: ({ field, id }) => {
2316
+ if (!scrollToField({ field, id })) {
2317
+ debug9.log("no element on this page renders field", field);
2318
+ }
2319
+ }
2242
2320
  });
2243
2321
  sendPreprEvent(
2244
2322
  "getScrollPosition",
@@ -2258,7 +2336,7 @@ function createPreprPreview(opts = {}) {
2258
2336
  bridge.stop();
2259
2337
  }
2260
2338
  el?.remove();
2261
- debug7.log("preview destroyed");
2339
+ debug9.log("preview destroyed");
2262
2340
  }
2263
2341
  const controller = { destroy };
2264
2342
  controllerStores.set(controller, store);
@@ -2266,7 +2344,7 @@ function createPreprPreview(opts = {}) {
2266
2344
  }
2267
2345
 
2268
2346
  // src/version.ts
2269
- var VERSION = "0.4.3";
2347
+ var VERSION = "0.5.0-rc.1";
2270
2348
 
2271
2349
  // src/core/pixel.ts
2272
2350
  var PIXEL_SCRIPT_URL = "https://cdn.tracking.prepr.io/js/prepr.min.js";