@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.
@@ -291,9 +291,13 @@ 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
- if (allowed) return allowed.includes(origin);
297
+ if (allowed) {
298
+ if (allowed.includes("*")) return origin !== "null";
299
+ return allowed.includes(origin);
300
+ }
297
301
  let url;
298
302
  try {
299
303
  url = new URL(origin);
@@ -308,6 +312,7 @@ function isAllowedEditorOrigin(origin, allowed) {
308
312
  }
309
313
  function createIframeBridge(store, options = {}) {
310
314
  const allowedOrigins = options.allowedEditorOrigins;
315
+ const onScrollToField = options.onScrollToField;
311
316
  let parentOrigin = null;
312
317
  const onKeyDown = (event) => {
313
318
  const key = event.key.toLowerCase();
@@ -317,7 +322,10 @@ function createIframeBridge(store, options = {}) {
317
322
  const onMessage = (evt) => {
318
323
  const data = evt?.data;
319
324
  if (data?.event === "prepr:initVE" && !parentOrigin) {
320
- if (!isAllowedEditorOrigin(evt.origin, allowedOrigins)) return;
325
+ if (!isAllowedEditorOrigin(evt.origin, allowedOrigins)) {
326
+ debug.warn("ignored editor handshake from origin", evt.origin);
327
+ return;
328
+ }
321
329
  parentOrigin = evt.origin;
322
330
  setTrustedParentOrigin(parentOrigin);
323
331
  if (data.scrollPosition != null) {
@@ -331,6 +339,15 @@ function createIframeBridge(store, options = {}) {
331
339
  const currentScrollY = window.scrollY || document.documentElement.scrollTop;
332
340
  sendPreprEvent("getScrollPosition", { value: currentScrollY });
333
341
  }
342
+ if (data?.event === "prepr:scrollToField") {
343
+ if (typeof data.field !== "string" || !data.field) return;
344
+ if (data.id !== void 0 && typeof data.id !== "string") return;
345
+ try {
346
+ onScrollToField?.({ field: data.field, id: data.id });
347
+ } catch (error) {
348
+ debug.warn("scrollToField handler failed", error);
349
+ }
350
+ }
334
351
  };
335
352
  return {
336
353
  start() {
@@ -355,7 +372,7 @@ var import_stega2 = require("@vercel/stega");
355
372
 
356
373
  // src/core/stega/clean.ts
357
374
  var import_stega = require("@vercel/stega");
358
- var debug = createScopedLogger("stega:clean");
375
+ var debug2 = createScopedLogger("stega:clean");
359
376
  function decodeStega(str) {
360
377
  if (!str) return null;
361
378
  try {
@@ -364,7 +381,7 @@ function decodeStega(str) {
364
381
  return decoded;
365
382
  }
366
383
  } catch (error) {
367
- debug.log("error decoding stega data", error);
384
+ debug2.log("error decoding stega data", error);
368
385
  const match = str.match(/{"origin.*?}/);
369
386
  if (match) {
370
387
  try {
@@ -373,7 +390,7 @@ function decodeStega(str) {
373
390
  return decodedMatch;
374
391
  }
375
392
  } catch (innerError) {
376
- debug.log("error decoding stega regex match", innerError);
393
+ debug2.log("error decoding stega regex match", innerError);
377
394
  }
378
395
  }
379
396
  }
@@ -409,14 +426,37 @@ function isRenderedElement(element) {
409
426
  if (!style) return true;
410
427
  return style.visibility !== "hidden" && style.display !== "none";
411
428
  }
429
+ function parseEditHref(href) {
430
+ try {
431
+ const url = new URL(href, window.location.origin);
432
+ return {
433
+ id: url.pathname.split("/").pop() || void 0,
434
+ field: url.searchParams.get("field") || void 0,
435
+ locale: url.searchParams.get("locale") || void 0
436
+ };
437
+ } catch {
438
+ return {};
439
+ }
440
+ }
441
+ function setOrRemove(element, name, value) {
442
+ if (value) {
443
+ element.setAttribute(name, value);
444
+ } else {
445
+ element.removeAttribute(name);
446
+ }
447
+ }
412
448
  function tagEncodedElement(element, decoded) {
413
449
  element.setAttribute("data-prepr-encoded", "");
414
450
  element.setAttribute("data-prepr-href", decoded.href);
415
451
  element.setAttribute("data-prepr-origin", decoded.origin);
452
+ const { id, field, locale } = parseEditHref(decoded.href);
453
+ setOrRemove(element, "data-prepr-id", id);
454
+ setOrRemove(element, "data-prepr-field", field);
455
+ setOrRemove(element, "data-prepr-locale", locale);
416
456
  }
417
457
 
418
458
  // src/core/stega/auto-clean.ts
419
- var debug2 = createScopedLogger("stega:auto-clean");
459
+ var debug3 = createScopedLogger("stega:auto-clean");
420
460
  function createStegaAutoClean(options) {
421
461
  const stripText = options?.enabled ?? true;
422
462
  let observer = null;
@@ -459,7 +499,7 @@ function createStegaAutoClean(options) {
459
499
  }
460
500
  pendingHidden.add(textNode);
461
501
  } catch (error) {
462
- debug2.log("error cleaning text node", error);
502
+ debug3.log("error cleaning text node", error);
463
503
  }
464
504
  return false;
465
505
  }
@@ -545,7 +585,7 @@ function createStegaAutoClean(options) {
545
585
  if (observer || typeof window === "undefined") return;
546
586
  scanAndClean();
547
587
  setupObserver();
548
- debug2.log("stega auto-clean started");
588
+ debug3.log("stega auto-clean started");
549
589
  }
550
590
  function stop() {
551
591
  if (debounceTimeout) {
@@ -559,13 +599,13 @@ function createStegaAutoClean(options) {
559
599
  cleaned = /* @__PURE__ */ new WeakMap();
560
600
  pendingHidden = /* @__PURE__ */ new Set();
561
601
  pendingDirty = /* @__PURE__ */ new Set();
562
- debug2.log("stega auto-clean stopped");
602
+ debug3.log("stega auto-clean stopped");
563
603
  }
564
604
  return { start, stop };
565
605
  }
566
606
 
567
607
  // src/core/stega/elements.ts
568
- var debug3 = createScopedLogger("stega:elements");
608
+ var debug4 = createScopedLogger("stega:elements");
569
609
  var StegaElements = class {
570
610
  constructor(decode = decodeStega) {
571
611
  this.observer = null;
@@ -610,7 +650,7 @@ var StegaElements = class {
610
650
  walkTextNodes(document.body, (textNode) => {
611
651
  if (this.tagFromTextNode(textNode)) encodedCount++;
612
652
  });
613
- debug3.log("document scan complete, encoded", encodedCount, "elements");
653
+ debug4.log("document scan complete, encoded", encodedCount, "elements");
614
654
  this.elements = document.querySelectorAll("[data-prepr-encoded]");
615
655
  }
616
656
  /** `onUpdate` fires after each debounced batch, for refreshing derived state. */
@@ -666,6 +706,9 @@ var StegaElements = class {
666
706
  element.removeAttribute("data-prepr-encoded");
667
707
  element.removeAttribute("data-prepr-href");
668
708
  element.removeAttribute("data-prepr-origin");
709
+ element.removeAttribute("data-prepr-id");
710
+ element.removeAttribute("data-prepr-field");
711
+ element.removeAttribute("data-prepr-locale");
669
712
  element.classList.remove("prepr-overlay-active");
670
713
  });
671
714
  this.elements = void 0;
@@ -673,7 +716,7 @@ var StegaElements = class {
673
716
  };
674
717
 
675
718
  // src/core/stega/overlay.ts
676
- var debug4 = createScopedLogger("stega:overlay");
719
+ var debug5 = createScopedLogger("stega:overlay");
677
720
  var StegaOverlay = class {
678
721
  constructor(onEdit) {
679
722
  this.overlay = null;
@@ -718,7 +761,8 @@ var StegaOverlay = class {
718
761
  const origin = element.getAttribute("data-prepr-origin");
719
762
  const id = element.getAttribute("data-prepr-id") ?? void 0;
720
763
  const field = element.getAttribute("data-prepr-field") ?? void 0;
721
- debug4.log("showing overlay", { href, origin });
764
+ const locale = element.getAttribute("data-prepr-locale") ?? void 0;
765
+ debug5.log("showing overlay", { href, origin });
722
766
  const overlay = this.overlay;
723
767
  overlay.style.display = "block";
724
768
  overlay.style.top = `${rect.top + window.scrollY - 2}px`;
@@ -750,7 +794,7 @@ var StegaOverlay = class {
750
794
  tooltip.style.left = `${left}px`;
751
795
  });
752
796
  tooltip.onclick = () => {
753
- this.onEdit({ href, origin, id, field });
797
+ this.onEdit({ href, origin, id, field, locale });
754
798
  };
755
799
  }
756
800
  this.currentElement = element;
@@ -790,7 +834,7 @@ var StegaOverlay = class {
790
834
  };
791
835
 
792
836
  // src/core/stega/proximity.ts
793
- var debug5 = createScopedLogger("stega:proximity");
837
+ var debug6 = createScopedLogger("stega:proximity");
794
838
  var StegaProximity = class {
795
839
  constructor() {
796
840
  this.highlighted = /* @__PURE__ */ new Set();
@@ -825,9 +869,9 @@ var StegaProximity = class {
825
869
  );
826
870
  const nodes = this.getEncodedElements();
827
871
  nodes.forEach((el) => this.observer.observe(el));
828
- debug5.log("observing", nodes.length, "encoded elements");
872
+ debug6.log("observing", nodes.length, "encoded elements");
829
873
  } catch (error) {
830
- debug5.log("error setting up IntersectionObserver", error);
874
+ debug6.log("error setting up IntersectionObserver", error);
831
875
  }
832
876
  }
833
877
  stopObserving() {
@@ -897,7 +941,7 @@ function removeStegaStyles() {
897
941
  }
898
942
 
899
943
  // src/core/stega/scan.ts
900
- var debug6 = createScopedLogger("stega:scan");
944
+ var debug7 = createScopedLogger("stega:scan");
901
945
  var EDITOR_MODE_STYLES = "[data-prepr-encoded]{cursor:pointer}";
902
946
  function createStegaController(opts) {
903
947
  const tooltipEnabled = opts.tooltip ?? true;
@@ -930,12 +974,13 @@ function createStegaController(opts) {
930
974
  href: encoded.getAttribute("data-prepr-href") ?? "",
931
975
  origin: encoded.getAttribute("data-prepr-origin") ?? "",
932
976
  id: encoded.getAttribute("data-prepr-id") ?? void 0,
933
- field: encoded.getAttribute("data-prepr-field") ?? void 0
977
+ field: encoded.getAttribute("data-prepr-field") ?? void 0,
978
+ locale: encoded.getAttribute("data-prepr-locale") ?? void 0
934
979
  });
935
980
  };
936
981
  const start = () => {
937
982
  if (initialized) {
938
- debug6.log("already initialized, skipping setup");
983
+ debug7.log("already initialized, skipping setup");
939
984
  return;
940
985
  }
941
986
  if (!tooltipEnabled) {
@@ -945,7 +990,7 @@ function createStegaController(opts) {
945
990
  });
946
991
  document.addEventListener("click", handleClick, { capture: true });
947
992
  initialized = true;
948
- debug6.log("stega controller started (editor mode)");
993
+ debug7.log("stega controller started (editor mode)");
949
994
  return;
950
995
  }
951
996
  injectStegaStyles();
@@ -976,7 +1021,7 @@ function createStegaController(opts) {
976
1021
  passive: true
977
1022
  });
978
1023
  initialized = true;
979
- debug6.log("stega controller started");
1024
+ debug7.log("stega controller started");
980
1025
  };
981
1026
  const stop = () => {
982
1027
  if (!initialized) return;
@@ -985,7 +1030,7 @@ function createStegaController(opts) {
985
1030
  elements.cleanup();
986
1031
  removeStegaStyles();
987
1032
  initialized = false;
988
- debug6.log("stega controller stopped (editor mode)");
1033
+ debug7.log("stega controller stopped (editor mode)");
989
1034
  return;
990
1035
  }
991
1036
  if (throttledMouseMove) {
@@ -1005,11 +1050,43 @@ function createStegaController(opts) {
1005
1050
  elements.cleanup();
1006
1051
  removeStegaStyles();
1007
1052
  initialized = false;
1008
- debug6.log("stega controller stopped");
1053
+ debug7.log("stega controller stopped");
1009
1054
  };
1010
1055
  return { start, stop };
1011
1056
  }
1012
1057
 
1058
+ // src/core/stega/scroll-to-field.ts
1059
+ var debug8 = createScopedLogger("stega:scroll");
1060
+ var FLASH_DURATION_MS = 1200;
1061
+ var flashTimeout = null;
1062
+ function scrollToField(request) {
1063
+ const { field, id } = request;
1064
+ if (!field) return false;
1065
+ const candidates = document.querySelectorAll(
1066
+ "[data-prepr-encoded]"
1067
+ );
1068
+ let target = null;
1069
+ for (const candidate of candidates) {
1070
+ if (candidate.getAttribute("data-prepr-field") !== field) continue;
1071
+ if (id && candidate.getAttribute("data-prepr-id") !== id) continue;
1072
+ target = candidate;
1073
+ break;
1074
+ }
1075
+ if (!target) {
1076
+ debug8.log("no element found for field", field);
1077
+ return false;
1078
+ }
1079
+ target.scrollIntoView({ behavior: "smooth", block: "center" });
1080
+ if (flashTimeout) clearTimeout(flashTimeout);
1081
+ document.querySelectorAll(".prepr-overlay-active").forEach((el) => el.classList.remove("prepr-overlay-active"));
1082
+ target.classList.add("prepr-overlay-active");
1083
+ flashTimeout = setTimeout(() => {
1084
+ target.classList.remove("prepr-overlay-active");
1085
+ flashTimeout = null;
1086
+ }, FLASH_DURATION_MS);
1087
+ return true;
1088
+ }
1089
+
1013
1090
  // src/core/store.ts
1014
1091
  var DEFAULT_STATE = {
1015
1092
  locale: "en",
@@ -2122,7 +2199,7 @@ function definePreprToolbar() {
2122
2199
  }
2123
2200
 
2124
2201
  // src/core/create-preview.ts
2125
- var debug7 = createScopedLogger("create-preview");
2202
+ var debug9 = createScopedLogger("create-preview");
2126
2203
  var controllerStores = /* @__PURE__ */ new WeakMap();
2127
2204
  function defaultNavigation() {
2128
2205
  return {
@@ -2164,7 +2241,7 @@ function createPreprPreview(opts = {}) {
2164
2241
  const search = new URLSearchParams(window.location.search);
2165
2242
  const hideBar = search.get(PARAM_HIDE_BAR) === "true";
2166
2243
  if (hideBar) {
2167
- debug7.log(`${PARAM_HIDE_BAR}=true \u2014 no visible toolbar`);
2244
+ debug9.log(`${PARAM_HIDE_BAR}=true \u2014 no visible toolbar`);
2168
2245
  }
2169
2246
  const mountUi = (options?.ui ?? true) && !isIframe && !hideBar;
2170
2247
  const locale = resolveLocale(options);
@@ -2200,18 +2277,17 @@ function createPreprPreview(opts = {}) {
2200
2277
  tooltip: mountUi,
2201
2278
  // In the editor, ask the parent to focus the field instead of opening a new
2202
2279
  // tab. Standalone previews keep the window.open behaviour.
2203
- onEdit: ({ href, origin, id, field }) => {
2280
+ onEdit: ({ href, id, field, locale: contentLocale }) => {
2204
2281
  const safeHref = href ? safeEditUrl(href) : null;
2205
2282
  if (href && !safeHref) {
2206
- debug7.warn("ignored edit request with unsupported href scheme");
2283
+ debug9.warn("ignored edit request with unsupported href scheme");
2207
2284
  return;
2208
2285
  }
2209
2286
  if (isIframe) {
2210
2287
  sendPreprEvent("field_edit_requested", {
2211
- href: safeHref ?? void 0,
2212
- origin,
2213
2288
  id,
2214
- field
2289
+ field,
2290
+ locale: contentLocale
2215
2291
  });
2216
2292
  } else if (safeHref) {
2217
2293
  window.open(safeHref, "_blank", "noopener,noreferrer");
@@ -2238,7 +2314,15 @@ function createPreprPreview(opts = {}) {
2238
2314
  handleChange(before, state);
2239
2315
  });
2240
2316
  const bridge = createIframeBridge(store, {
2241
- allowedEditorOrigins: options?.allowedEditorOrigins
2317
+ allowedEditorOrigins: options?.allowedEditorOrigins,
2318
+ // The editor selecting a field scrolls the matching element into view. The
2319
+ // lookup relies on the attributes the stega scan writes, so it finds
2320
+ // nothing until encoded content has been tagged.
2321
+ onScrollToField: ({ field, id }) => {
2322
+ if (!scrollToField({ field, id })) {
2323
+ debug9.log("no element on this page renders field", field);
2324
+ }
2325
+ }
2242
2326
  });
2243
2327
  sendPreprEvent(
2244
2328
  "getScrollPosition",
@@ -2258,7 +2342,7 @@ function createPreprPreview(opts = {}) {
2258
2342
  bridge.stop();
2259
2343
  }
2260
2344
  el?.remove();
2261
- debug7.log("preview destroyed");
2345
+ debug9.log("preview destroyed");
2262
2346
  }
2263
2347
  const controller = { destroy };
2264
2348
  controllerStores.set(controller, store);
@@ -2266,7 +2350,7 @@ function createPreprPreview(opts = {}) {
2266
2350
  }
2267
2351
 
2268
2352
  // src/version.ts
2269
- var VERSION = "0.4.3";
2353
+ var VERSION = "0.5.0-rc.2";
2270
2354
 
2271
2355
  // src/core/pixel.ts
2272
2356
  var PIXEL_SCRIPT_URL = "https://cdn.tracking.prepr.io/js/prepr.min.js";
@@ -58,8 +58,14 @@ interface PreprPreviewOptions extends PreprToolbarOptions {
58
58
  */
59
59
  ui?: boolean;
60
60
  /**
61
- * Replace the `*.prepr.io` editor-origin wildcard with an exact list.
62
- * Intended for self-hosted editors; when set, the wildcard no longer applies.
61
+ * Replace the `*.prepr.io` editor-origin wildcard with an exact list, e.g.
62
+ * `['http://acme.localhost:8000']`. Intended for self-hosted or locally run
63
+ * editors; when set, the wildcard no longer applies. Entries are compared to
64
+ * the editor's origin exactly: scheme, host and port.
65
+ *
66
+ * `['*']` accepts any editor origin. Use it for development only: any page
67
+ * that frames the preview can then switch on preview and edit mode and
68
+ * receives the field-edit events, which carry content item ids.
63
69
  */
64
70
  allowedEditorOrigins?: string[];
65
71
  /**
@@ -58,8 +58,14 @@ interface PreprPreviewOptions extends PreprToolbarOptions {
58
58
  */
59
59
  ui?: boolean;
60
60
  /**
61
- * Replace the `*.prepr.io` editor-origin wildcard with an exact list.
62
- * Intended for self-hosted editors; when set, the wildcard no longer applies.
61
+ * Replace the `*.prepr.io` editor-origin wildcard with an exact list, e.g.
62
+ * `['http://acme.localhost:8000']`. Intended for self-hosted or locally run
63
+ * editors; when set, the wildcard no longer applies. Entries are compared to
64
+ * the editor's origin exactly: scheme, host and port.
65
+ *
66
+ * `['*']` accepts any editor origin. Use it for development only: any page
67
+ * that frames the preview can then switch on preview and edit mode and
68
+ * receives the field-edit events, which carry content item ids.
63
69
  */
64
70
  allowedEditorOrigins?: string[];
65
71
  /**