@kotao/storefront 0.2.6 → 0.2.8

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.
@@ -1,6 +1,7 @@
1
1
  // src/editor/bridge.ts
2
2
  var HOST_SOURCE = "kotao:workspaces-editor";
3
3
  var PREVIEW_SOURCE = "kotao:storefront-preview";
4
+ var VISUAL_READY_REQUEST_ID = /^[A-Za-z0-9_-]{1,128}$/u;
4
5
  function isReservedSectionId(sectionId) {
5
6
  return sectionId.startsWith("theme:");
6
7
  }
@@ -121,6 +122,13 @@ function parseHostMessage(data, shopId) {
121
122
  type: "editor.ready.request",
122
123
  shopId
123
124
  };
125
+ case "editor.visual-ready.request":
126
+ return typeof data.requestId === "string" && VISUAL_READY_REQUEST_ID.test(data.requestId) ? {
127
+ source: HOST_SOURCE,
128
+ type: "editor.visual-ready.request",
129
+ shopId,
130
+ requestId: data.requestId
131
+ } : null;
124
132
  case "section.hover":
125
133
  case "section.activate":
126
134
  if (data.sectionId !== null && (typeof data.sectionId !== "string" || isReservedSectionId(data.sectionId))) {
@@ -136,6 +144,8 @@ function parseHostMessage(data, shopId) {
136
144
  sectionId: data.sectionId,
137
145
  ...data.blockId !== void 0 ? { blockId: data.blockId } : {}
138
146
  };
147
+ case "inspector.set":
148
+ return typeof data.enabled === "boolean" ? { source: HOST_SOURCE, type: "inspector.set", shopId, enabled: data.enabled } : null;
139
149
  case "settings.patchSection":
140
150
  return typeof data.sectionId === "string" && !isReservedSectionId(data.sectionId) && isSettings(data.settings) ? {
141
151
  source: HOST_SOURCE,
@@ -318,6 +328,7 @@ function parsePreviewHover(data, shopId) {
318
328
  function parsePreviewReady(data, shopId) {
319
329
  if (!isRecord(data) || data.source !== PREVIEW_SOURCE || data.shopId !== shopId) return null;
320
330
  if (data.type !== "editor.ready" || !isResolvedSectionArray(data.sections)) return null;
331
+ if (data.visualReadyProtocol !== void 0 && data.visualReadyProtocol !== 1) return null;
321
332
  if (data.global !== void 0 && !isSettings(data.global)) return null;
322
333
  if (data.schemas !== void 0 && !isSectionSchemaRecord(data.schemas)) return null;
323
334
  if (data.globalInputs !== void 0 && !isSettingInputArray(data.globalInputs)) return null;
@@ -329,6 +340,7 @@ function parsePreviewReady(data, shopId) {
329
340
  type: "editor.ready",
330
341
  shopId,
331
342
  sections: data.sections,
343
+ ...data.visualReadyProtocol === 1 ? { visualReadyProtocol: 1 } : {},
332
344
  ...data.global !== void 0 ? { global: data.global } : {},
333
345
  ...data.schemas !== void 0 ? { schemas: data.schemas } : {},
334
346
  ...data.globalInputs !== void 0 ? { globalInputs: data.globalInputs } : {},
@@ -337,6 +349,33 @@ function parsePreviewReady(data, shopId) {
337
349
  ...sectionGroups !== void 0 ? { sectionGroups } : {}
338
350
  };
339
351
  }
352
+ function parsePreviewVisualReady(data, shopId) {
353
+ if (!isRecord(data) || data.source !== PREVIEW_SOURCE || data.shopId !== shopId) return null;
354
+ if (data.type !== "editor.visual-ready" || typeof data.requestId !== "string" || !VISUAL_READY_REQUEST_ID.test(data.requestId)) {
355
+ return null;
356
+ }
357
+ return {
358
+ source: PREVIEW_SOURCE,
359
+ type: "editor.visual-ready",
360
+ shopId,
361
+ requestId: data.requestId
362
+ };
363
+ }
364
+ function parsePreviewNavigation(data, shopId, previewOrigin) {
365
+ if (!isRecord(data) || data.source !== PREVIEW_SOURCE || data.shopId !== shopId) return null;
366
+ if (data.type !== "preview.navigate" || typeof data.href !== "string" || data.href.trim() === "") {
367
+ return null;
368
+ }
369
+ try {
370
+ const href = new URL(data.href);
371
+ if (href.protocol !== "https:" && href.protocol !== "http:" || href.username !== "" || href.password !== "" || href.origin !== previewOrigin) {
372
+ return null;
373
+ }
374
+ } catch {
375
+ return null;
376
+ }
377
+ return { source: PREVIEW_SOURCE, type: "preview.navigate", shopId, href: data.href };
378
+ }
340
379
  function postHostMessage(target, previewOrigin, message) {
341
380
  target.postMessage(message, previewOrigin);
342
381
  }
@@ -583,6 +622,40 @@ function findBlockElement(document, blockId) {
583
622
  );
584
623
  return candidates?.find((element) => blockIdForElement(element) === blockId) ?? null;
585
624
  }
625
+ function isModifiedClick(event) {
626
+ return event.metaKey === true || event.ctrlKey === true || event.shiftKey === true || event.altKey === true;
627
+ }
628
+ function anchorElementFromTarget(target) {
629
+ if (!isRecord(target) || typeof target.closest !== "function") return null;
630
+ const anchor = target.closest("a[href]");
631
+ if (!isRecord(anchor) || typeof anchor.getAttribute !== "function") return null;
632
+ return anchor;
633
+ }
634
+ function classifyAnchorClick(event, anchor, document) {
635
+ const download = anchor.getAttribute?.("download");
636
+ if (event.defaultPrevented === true || event.button !== void 0 && event.button !== 0 || isModifiedClick(event) || anchor.getAttribute?.("target")?.toLowerCase() === "_blank" || download !== null && download !== void 0) {
637
+ return null;
638
+ }
639
+ const rawHref = anchor.getAttribute?.("href");
640
+ const location = document.defaultView?.location;
641
+ if (typeof rawHref !== "string" || rawHref.trim() === "" || location === void 0) return null;
642
+ const hasFragmentDelimiter = rawHref.includes("#");
643
+ let href;
644
+ let current;
645
+ try {
646
+ href = new URL(rawHref, location.href);
647
+ current = new URL(location.href);
648
+ } catch {
649
+ return { kind: "unsafe" };
650
+ }
651
+ if (href.protocol === "javascript:" || href.protocol === "data:") return { kind: "unsafe" };
652
+ if (href.protocol === "mailto:" || href.protocol === "tel:") return { kind: "scheme" };
653
+ if (href.protocol !== "https:" && href.protocol !== "http:") return { kind: "unsafe" };
654
+ if (href.origin === location.origin && href.pathname === current.pathname && href.search === current.search && (href.hash !== "" || hasFragmentDelimiter)) {
655
+ return { kind: "hash" };
656
+ }
657
+ return href.origin === location.origin ? { kind: "internal", href: href.href } : { kind: "external", href: href.href };
658
+ }
586
659
  function htmlAttribute(value) {
587
660
  return value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;");
588
661
  }
@@ -616,6 +689,7 @@ function createEditorAffordances({
616
689
  labelForSection,
617
690
  onCanvasAction,
618
691
  onHoverSection,
692
+ onNavigate,
619
693
  onSelect,
620
694
  stateForSelection
621
695
  }) {
@@ -627,6 +701,7 @@ function createEditorAffordances({
627
701
  let selectedBlockId = null;
628
702
  let ownedStyle = null;
629
703
  let ownedToolbar = null;
704
+ let inspectorEnabled = true;
630
705
  const setElementState = (kind, sectionId) => {
631
706
  const previous = kind === "hovered" ? hoveredElement : selectedElement;
632
707
  previous?.removeAttribute(`data-kotao-editor-${kind}`);
@@ -639,8 +714,14 @@ function createEditorAffordances({
639
714
  return sectionElement;
640
715
  };
641
716
  const setHovered = (sectionId) => {
717
+ if (!inspectorEnabled && sectionId !== null) return;
642
718
  hoveredElement = setElementState("hovered", sectionId);
643
719
  };
720
+ const clearHovered = () => {
721
+ hoveredBlockElement?.removeAttribute("data-kotao-editor-block-hovered");
722
+ hoveredBlockElement = null;
723
+ setHovered(null);
724
+ };
644
725
  const positionToolbar = () => {
645
726
  const anchor = selectedBlockElement ?? selectedElement;
646
727
  const anchorRect = anchor?.getBoundingClientRect?.();
@@ -681,6 +762,7 @@ function createEditorAffordances({
681
762
  positionToolbar();
682
763
  };
683
764
  const setSelected = (sectionId, blockId) => {
765
+ if (!inspectorEnabled && sectionId !== null) return;
684
766
  selectedBlockElement?.removeAttribute("data-kotao-editor-block-selected");
685
767
  selectedBlockElement = null;
686
768
  selectedBlockId = blockId ?? null;
@@ -697,6 +779,7 @@ function createEditorAffordances({
697
779
  renderToolbar();
698
780
  };
699
781
  const onPointerOver = (event) => {
782
+ if (!inspectorEnabled) return;
700
783
  const element = sectionElementFromTarget(event.target);
701
784
  if (element === null) return;
702
785
  const sectionId = element.id.slice("shopify-section-".length);
@@ -716,12 +799,11 @@ function createEditorAffordances({
716
799
  }
717
800
  };
718
801
  const onPointerOut = (event) => {
802
+ if (!inspectorEnabled) return;
719
803
  if (hoveredElement === null) return;
720
804
  const nextElement = sectionElementFromTarget(event.relatedTarget);
721
805
  if (nextElement === hoveredElement || hoveredElement.contains(event.relatedTarget)) return;
722
- hoveredBlockElement?.removeAttribute("data-kotao-editor-block-hovered");
723
- hoveredBlockElement = null;
724
- setHovered(null);
806
+ clearHovered();
725
807
  onHoverSection(null);
726
808
  };
727
809
  const onClick = (event) => {
@@ -729,6 +811,35 @@ function createEditorAffordances({
729
811
  const canvas = event.target.closest(`#${EDITOR_CANVAS_ID}`);
730
812
  if (canvas !== null) return;
731
813
  }
814
+ const anchor = anchorElementFromTarget(event.target);
815
+ if (anchor !== null) {
816
+ const disposition = classifyAnchorClick(event, anchor, document);
817
+ if (disposition === null) return;
818
+ if (inspectorEnabled) {
819
+ event.preventDefault?.();
820
+ event.stopPropagation?.();
821
+ } else {
822
+ switch (disposition.kind) {
823
+ case "internal":
824
+ event.preventDefault?.();
825
+ event.stopPropagation?.();
826
+ onNavigate(disposition.href);
827
+ break;
828
+ case "external":
829
+ event.preventDefault?.();
830
+ event.stopPropagation?.();
831
+ document.defaultView?.open?.(disposition.href, "_blank", "noopener,noreferrer");
832
+ break;
833
+ case "unsafe":
834
+ event.preventDefault?.();
835
+ event.stopPropagation?.();
836
+ break;
837
+ }
838
+ return;
839
+ }
840
+ } else if (!inspectorEnabled) {
841
+ return;
842
+ }
732
843
  const section = sectionElementFromTarget(event.target);
733
844
  if (section === null) return;
734
845
  const sectionId = section.id.slice("shopify-section-".length);
@@ -784,8 +895,15 @@ function createEditorAffordances({
784
895
  return {
785
896
  setHovered,
786
897
  setSelected,
898
+ setInspectorEnabled(enabled) {
899
+ inspectorEnabled = enabled;
900
+ if (!enabled) {
901
+ clearHovered();
902
+ setSelected(null);
903
+ }
904
+ },
787
905
  dispose() {
788
- setHovered(null);
906
+ clearHovered();
789
907
  setSelected(null);
790
908
  document.documentElement.removeAttribute("data-kotao-editor");
791
909
  document.removeEventListener("pointerover", onPointerOver, true);
@@ -947,13 +1065,25 @@ function createStorefrontEditorBridge({
947
1065
  };
948
1066
  target.postMessage(message, adminOrigin);
949
1067
  };
1068
+ const postNavigation = (href) => {
1069
+ const message = {
1070
+ source: PREVIEW_SOURCE,
1071
+ type: "preview.navigate",
1072
+ shopId,
1073
+ href
1074
+ };
1075
+ target.postMessage(message, adminOrigin);
1076
+ };
950
1077
  const resolvedDocument = editorDocument ?? defaultEditorDocument();
1078
+ let disposed = false;
1079
+ const pendingAnimationFrames = /* @__PURE__ */ new Set();
951
1080
  const affordances = resolvedDocument === void 0 ? null : createEditorAffordances({
952
1081
  document: resolvedDocument,
953
1082
  labelForBlock,
954
1083
  labelForSection,
955
1084
  onCanvasAction: postCanvasAction,
956
1085
  onHoverSection: postHoverSection,
1086
+ onNavigate: postNavigation,
957
1087
  onSelect: postSelection,
958
1088
  stateForSelection
959
1089
  });
@@ -964,6 +1094,7 @@ function createStorefrontEditorBridge({
964
1094
  type: "editor.ready",
965
1095
  shopId,
966
1096
  sections: snapshot.sections,
1097
+ visualReadyProtocol: 1,
967
1098
  global: snapshot.global,
968
1099
  ...schemas !== void 0 ? { schemas } : {},
969
1100
  ...globalInputs !== void 0 ? { globalInputs } : {},
@@ -973,21 +1104,67 @@ function createStorefrontEditorBridge({
973
1104
  };
974
1105
  target.postMessage(message, adminOrigin);
975
1106
  };
1107
+ const postVisualReadyAfterPaint = (requestId) => {
1108
+ const view = resolvedDocument?.defaultView;
1109
+ const requestAnimationFrame = view?.requestAnimationFrame?.bind(view);
1110
+ if (requestAnimationFrame === void 0) {
1111
+ queueMicrotask(() => {
1112
+ queueMicrotask(() => {
1113
+ if (disposed) return;
1114
+ target.postMessage(
1115
+ {
1116
+ source: PREVIEW_SOURCE,
1117
+ type: "editor.visual-ready",
1118
+ shopId,
1119
+ requestId
1120
+ },
1121
+ adminOrigin
1122
+ );
1123
+ });
1124
+ });
1125
+ return;
1126
+ }
1127
+ const firstFrame = requestAnimationFrame(() => {
1128
+ pendingAnimationFrames.delete(firstFrame);
1129
+ if (disposed) return;
1130
+ const secondFrame = requestAnimationFrame(() => {
1131
+ pendingAnimationFrames.delete(secondFrame);
1132
+ if (disposed) return;
1133
+ target.postMessage(
1134
+ {
1135
+ source: PREVIEW_SOURCE,
1136
+ type: "editor.visual-ready",
1137
+ shopId,
1138
+ requestId
1139
+ },
1140
+ adminOrigin
1141
+ );
1142
+ });
1143
+ pendingAnimationFrames.add(secondFrame);
1144
+ });
1145
+ pendingAnimationFrames.add(firstFrame);
1146
+ };
976
1147
  const onMessage = (event) => {
977
1148
  if (event.origin !== adminOrigin) return;
978
- if (event.source !== void 0 && event.source !== null && event.source !== target) return;
1149
+ if (event.source !== target) return;
979
1150
  const message = parseHostMessage(event.data, shopId);
980
1151
  if (message === null) return;
981
1152
  switch (message.type) {
982
1153
  case "editor.ready.request":
983
1154
  postReady();
984
1155
  break;
1156
+ case "editor.visual-ready.request":
1157
+ postVisualReadyAfterPaint(message.requestId);
1158
+ break;
985
1159
  case "section.hover":
986
1160
  affordances?.setHovered(message.sectionId);
987
1161
  break;
988
1162
  case "section.activate":
989
1163
  affordances?.setSelected(message.sectionId, message.blockId);
990
1164
  break;
1165
+ case "inspector.set":
1166
+ affordances?.setInspectorEnabled(message.enabled);
1167
+ break;
991
1168
  case "settings.patchSection":
992
1169
  store.patchSection(message.sectionId, message.settings);
993
1170
  break;
@@ -1045,6 +1222,11 @@ function createStorefrontEditorBridge({
1045
1222
  postSelection(sectionId, blockId);
1046
1223
  },
1047
1224
  dispose: () => {
1225
+ disposed = true;
1226
+ for (const requestId of pendingAnimationFrames) {
1227
+ resolvedDocument?.defaultView?.cancelAnimationFrame?.(requestId);
1228
+ }
1229
+ pendingAnimationFrames.clear();
1048
1230
  replaceAppExtensionPreviewFrames(resolvedDocument, []);
1049
1231
  affordances?.dispose();
1050
1232
  transport.removeEventListener("message", onMessage);
@@ -1054,8 +1236,10 @@ function createStorefrontEditorBridge({
1054
1236
  function createStorefrontEditorHostBridge({
1055
1237
  onCanvasAction,
1056
1238
  onHoverSection,
1239
+ onNavigate,
1057
1240
  onReady,
1058
1241
  onSelectSection,
1242
+ onVisualReady,
1059
1243
  previewOrigin,
1060
1244
  resolveTarget,
1061
1245
  shopId,
@@ -1065,12 +1249,23 @@ function createStorefrontEditorHostBridge({
1065
1249
  const currentTarget = () => resolveTarget?.() ?? target;
1066
1250
  const onMessage = (event) => {
1067
1251
  if (event.origin !== previewOrigin) return;
1068
- if (event.source !== void 0 && event.source !== null && event.source !== currentTarget())
1252
+ const expectedTarget = currentTarget();
1253
+ if (event.source !== expectedTarget) return;
1254
+ const visualReady = parsePreviewVisualReady(event.data, shopId);
1255
+ if (visualReady !== null) {
1256
+ onVisualReady?.(visualReady.requestId);
1069
1257
  return;
1258
+ }
1259
+ const navigation = parsePreviewNavigation(event.data, shopId, previewOrigin);
1260
+ if (navigation !== null) {
1261
+ onNavigate?.({ href: navigation.href });
1262
+ return;
1263
+ }
1070
1264
  const ready = parsePreviewReady(event.data, shopId);
1071
1265
  if (ready !== null) {
1072
1266
  onReady?.({
1073
1267
  sections: ready.sections,
1268
+ ...ready.visualReadyProtocol === 1 ? { visualReadyProtocol: 1 } : {},
1074
1269
  ...ready.global !== void 0 ? { global: ready.global } : {},
1075
1270
  ...ready.schemas !== void 0 ? { schemas: ready.schemas } : {},
1076
1271
  ...ready.globalInputs !== void 0 ? { globalInputs: ready.globalInputs } : {},
@@ -1123,6 +1318,15 @@ function createStorefrontEditorHostBridge({
1123
1318
  shopId
1124
1319
  });
1125
1320
  },
1321
+ requestVisualReady: (requestId) => {
1322
+ if (!VISUAL_READY_REQUEST_ID.test(requestId)) return;
1323
+ postHostMessage(currentTarget(), previewOrigin, {
1324
+ source: HOST_SOURCE,
1325
+ type: "editor.visual-ready.request",
1326
+ shopId,
1327
+ requestId
1328
+ });
1329
+ },
1126
1330
  hoverSection: (sectionId) => {
1127
1331
  if (sectionId !== null && isReservedSectionId(sectionId)) return;
1128
1332
  postHostMessage(currentTarget(), previewOrigin, {
@@ -1260,6 +1464,14 @@ function createStorefrontEditorHostBridge({
1260
1464
  extensions
1261
1465
  });
1262
1466
  },
1467
+ setInspectorEnabled: (enabled) => {
1468
+ postHostMessage(currentTarget(), previewOrigin, {
1469
+ source: HOST_SOURCE,
1470
+ type: "inspector.set",
1471
+ shopId,
1472
+ enabled
1473
+ });
1474
+ },
1263
1475
  dispose: () => {
1264
1476
  transport.removeEventListener("message", onMessage);
1265
1477
  }
@@ -1378,5 +1590,5 @@ function scopeStorefrontSectionCss(value, sectionId) {
1378
1590
  }
1379
1591
 
1380
1592
  export { STOREFRONT_SECTION_CUSTOM_CSS_SETTING, createStorefrontEditorBridge, createStorefrontEditorHostBridge, positionStorefrontEditorCanvasToolbar, scopeStorefrontSectionCss };
1381
- //# sourceMappingURL=chunk-O6YZEY7X.js.map
1382
- //# sourceMappingURL=chunk-O6YZEY7X.js.map
1593
+ //# sourceMappingURL=chunk-RFKLO6TD.js.map
1594
+ //# sourceMappingURL=chunk-RFKLO6TD.js.map