@obelism/content 0.19.2 → 0.21.0

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.
package/dist/bridge.js CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, '__esModule', { value: true });
2
2
 
3
- var preview = require('./preview-ijk3ixwb.js');
3
+ var preview = require('./preview-ooq8w3jv.js');
4
4
  var site = require('./site.js');
5
5
  var config = require('./config-ci2hxmas.js');
6
6
 
@@ -12,7 +12,7 @@ var config = require('./config-ci2hxmas.js');
12
12
  //
13
13
  // Sites tag a block's clickable element with `data-numbers-id={block._uid}`
14
14
  // and load Obelism numbers' script; the CMS reads the rates from numbers
15
- // (the CMS's content/docs/guides/visual-editor.mdx, "Click rates").
15
+ // (the CMS's content/docs/guides/canvas.mdx, "Click rates").
16
16
  const DEFAULT_STATS_LABELS = {
17
17
  clicked: '{rate}% clicked',
18
18
  title: '{clickers} of {viewers} visitors of its page clicked it in the last 30 days ({clicks} clicks)'
@@ -165,6 +165,91 @@ const mountStatsOverlay = (win, { selector, onSelect, labels: labelOverrides })=
165
165
  };
166
166
  };
167
167
 
168
+ // Content's canvas shows a whole page in a frame as tall as the page, so the
169
+ // frame is the page's height, not the screen's. Sizes in viewport-height
170
+ // units (`vh`, `svh`, `lvh`, `dvh`) would follow it: a `100vh` hero makes
171
+ // the page taller, the canvas makes the frame taller, and the hero grows
172
+ // again. The editor sends the height the frame stands for (`viewport`), and
173
+ // this rewrites those units in the page's stylesheets as pixels of that
174
+ // height. Stylesheets from another origin can't be read and keep theirs.
175
+ const UNIT = /(-?\d*\.?\d+)(?:s|l|d)?vh\b/g;
176
+ // A value with its viewport-height units as pixels of `height`, or null when
177
+ // it has none.
178
+ const freezeValue = (value, height)=>{
179
+ UNIT.lastIndex = 0;
180
+ if (!UNIT.test(value)) return null;
181
+ return value.replace(UNIT, (_, amount)=>`${Math.round(Number(amount) * height / 100 * 100) / 100}px`);
182
+ };
183
+ const freezeStyle = (style, height)=>{
184
+ for(let index = 0; index < style.length; index++){
185
+ const property = style.item(index);
186
+ const frozen = freezeValue(style.getPropertyValue(property), height);
187
+ if (frozen !== null) {
188
+ style.setProperty(property, frozen, style.getPropertyPriority(property));
189
+ }
190
+ }
191
+ };
192
+ // Style rules, and those inside @media, @supports and @layer blocks.
193
+ const freezeRules = (rules, height)=>{
194
+ for (const rule of Array.from(rules)){
195
+ const { style, cssRules } = rule;
196
+ if (style) freezeStyle(style, height);
197
+ if (cssRules) freezeRules(cssRules, height);
198
+ }
199
+ };
200
+ const createViewportUnits = (doc)=>{
201
+ const done = new WeakSet();
202
+ let height = null;
203
+ let observer = null;
204
+ let scheduled = false;
205
+ const freezeSheets = ()=>{
206
+ if (height === null) return;
207
+ for (const sheet of Array.from(doc.styleSheets)){
208
+ if (done.has(sheet)) continue;
209
+ let rules;
210
+ try {
211
+ rules = sheet.cssRules;
212
+ } catch {
213
+ continue;
214
+ }
215
+ done.add(sheet);
216
+ freezeRules(rules, height);
217
+ }
218
+ doc.querySelectorAll('[style*="vh"]').forEach((element)=>{
219
+ freezeStyle(element.style, height);
220
+ });
221
+ };
222
+ return {
223
+ freeze (next) {
224
+ // Once: the frame stands for one screen size until it reloads.
225
+ if (height !== null || !(next > 0)) return;
226
+ height = next;
227
+ freezeSheets();
228
+ const View = doc.defaultView?.MutationObserver;
229
+ if (!View) return;
230
+ // Once per frame, however much the page changes in it.
231
+ observer = new View(()=>{
232
+ if (scheduled) return;
233
+ scheduled = true;
234
+ const run = ()=>{
235
+ scheduled = false;
236
+ freezeSheets();
237
+ };
238
+ if (doc.defaultView?.requestAnimationFrame) {
239
+ doc.defaultView.requestAnimationFrame(run);
240
+ } else run();
241
+ });
242
+ observer.observe(doc.documentElement, {
243
+ childList: true,
244
+ subtree: true
245
+ });
246
+ },
247
+ destroy () {
248
+ observer?.disconnect();
249
+ }
250
+ };
251
+ };
252
+
168
253
  // The site's half of the visual editor (the CMS's public/bridge.js, as a
169
254
  // module). Inside the editor's iframe it outlines blocks, tells the editor
170
255
  // which one was clicked, hands the editor's live, unsaved entry to listeners
@@ -173,10 +258,10 @@ const mountStatsOverlay = (win, { selector, onSelect, labels: labelOverrides })=
173
258
  // Messages are only exchanged with the CMS origin, and only with the parent
174
259
  // window:
175
260
  // - site → editor: { source: 'obelism-content-bridge', type: 'ready' |
176
- // 'select' | 'prompt' | 'undo', uid?, id?, prompt? }
261
+ // 'size' | 'select' | 'prompt' | 'undo', uid?, id?, prompt?, height? }
177
262
  // - editor → site: { source: 'obelism-content-editor', type: 'connect' |
178
263
  // 'update' | 'select' | 'saved' | 'published' | 'features' |
179
- // 'promptResult' | 'promptState' | 'stats', … }
264
+ // 'promptResult' | 'promptState' | 'stats' | 'viewport', … }
180
265
  const BRIDGE_SOURCE = 'obelism-content-bridge';
181
266
  const EDITOR_SOURCE = 'obelism-content-editor';
182
267
  // How long a prompt may take before it counts as failed.
@@ -230,6 +315,7 @@ const INERT_METHODS = {
230
315
  prompt: inertPrompt,
231
316
  createPage: ()=>Promise.reject(new Error('New pages are made on the site itself')),
232
317
  createProductPage: ()=>Promise.reject(new Error('Product pages are made on the site itself')),
318
+ createCollectionPage: ()=>Promise.reject(new Error('Collection pages are made on the site itself')),
233
319
  view: onlyWhileEditingSite,
234
320
  applyJson: onlyWhileEditingSite,
235
321
  openForm: (_container, options = {})=>{
@@ -288,6 +374,7 @@ const createBridge = (win, cmsOrigin)=>{
288
374
  entryId: params.get(preview.PREVIEW_PARAMS.entry),
289
375
  environment: params.get(preview.PREVIEW_PARAMS.environment),
290
376
  product: null,
377
+ collection: null,
291
378
  on: emitter.on,
292
379
  getState: ()=>state,
293
380
  ...INERT_METHODS,
@@ -342,9 +429,32 @@ const createBridge = (win, cmsOrigin)=>{
342
429
  };
343
430
  doc.addEventListener('click', onClick, true);
344
431
  cleanup.push(()=>doc.removeEventListener('click', onClick, true));
345
- const ready = ()=>send('ready', {
346
- url: win.location.href
432
+ // The page's height, which an editor on another origin can't read: the
433
+ // canvas makes its frame as tall as the page. With `ready`, and as `size`
434
+ // whenever it changes.
435
+ const pageHeight = ()=>Math.ceil(Math.max(doc.documentElement.scrollHeight, doc.body?.scrollHeight ?? 0));
436
+ let sentHeight = 0;
437
+ const ready = ()=>{
438
+ sentHeight = pageHeight();
439
+ send('ready', {
440
+ url: win.location.href,
441
+ height: sentHeight
347
442
  });
443
+ };
444
+ const Observer = win.ResizeObserver;
445
+ if (typeof Observer === 'function') {
446
+ const observer = new Observer(()=>{
447
+ const height = pageHeight();
448
+ if (height === sentHeight) return;
449
+ sentHeight = height;
450
+ send('size', {
451
+ height
452
+ });
453
+ });
454
+ observer.observe(doc.documentElement);
455
+ if (doc.body) observer.observe(doc.body);
456
+ cleanup.push(()=>observer.disconnect());
457
+ }
348
458
  // Prompts waiting for the editor's answer, by id.
349
459
  const pending = new Map();
350
460
  let nextId = 0;
@@ -408,6 +518,8 @@ const createBridge = (win, cmsOrigin)=>{
408
518
  settle(id)?.reject(new Error('The bridge was destroyed'));
409
519
  }
410
520
  });
521
+ const viewportUnits = createViewportUnits(doc);
522
+ cleanup.push(()=>viewportUnits.destroy());
411
523
  const onMessage = (event)=>{
412
524
  if (event.origin !== cmsOrigin || event.source !== win.parent) return;
413
525
  const data = event.data;
@@ -440,6 +552,10 @@ const createBridge = (win, cmsOrigin)=>{
440
552
  statsVisible: data.visible !== false
441
553
  });
442
554
  showStats();
555
+ } else if (data.type === 'viewport' && typeof data.height === 'number') {
556
+ // The canvas: the frame is as tall as the page (`size`), so `vh`
557
+ // sizes are fixed at the screen height it stands for.
558
+ viewportUnits.freeze(data.height);
443
559
  } else if (data.type === 'promptState') {
444
560
  setState({
445
561
  canUndo: data.canUndo === true
@@ -481,6 +597,7 @@ const wrapScriptBridge = (script, params)=>{
481
597
  entryId: script.entryId,
482
598
  environment: params.get(preview.PREVIEW_PARAMS.environment),
483
599
  product: null,
600
+ collection: null,
484
601
  on: (event, listener)=>{
485
602
  if (event === 'update' && !forwardingUpdates) {
486
603
  forwardingUpdates = true;
@@ -509,6 +626,7 @@ const DEFAULT_PROMPT_LABELS = {
509
626
  minimize: 'Minimize, to see the page',
510
627
  expand: 'Expand',
511
628
  scopeEntry: 'Whole page',
629
+ scopeNoPage: 'Whole page',
512
630
  scopeBlock: 'Selected block',
513
631
  clearBlock: 'Edit the whole page instead',
514
632
  working: 'Working on it…',
@@ -544,7 +662,11 @@ const DEFAULT_PROMPT_LABELS = {
544
662
  addProductPage: 'Add a product page',
545
663
  addingProductPage: 'Making the product page…',
546
664
  addedProductPage: 'Opening the product page…',
547
- productPageHint: 'This product has no page yet. Add one for what the store doesn’t say.'
665
+ productPageHint: 'This product has no page yet. Add one for what the store doesn’t say.',
666
+ addCollectionPage: 'Add a collection page',
667
+ addingCollectionPage: 'Making the collection page…',
668
+ addedCollectionPage: 'Opening the collection page…',
669
+ collectionPageHint: 'This collection has no page yet. Add one for what goes around its products.'
548
670
  };
549
671
  const SIDE_KEY = 'obelism-content-prompt-side';
550
672
  // How far the pointer moves before it drags the box, not clicks it.
@@ -634,7 +756,7 @@ button:disabled { cursor: not-allowed; }
634
756
  button:focus-visible { outline: 2px solid rgb(var(--theme-accent-rgb)); outline-offset: 2px; }
635
757
 
636
758
  /* Closed: one floating pill that reads like an empty field, with the mark. */
637
- .bar { display: flex; justify-content: center; }
759
+ .bar { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 8px; }
638
760
  .pill {
639
761
  display: flex; align-items: center; gap: 12px; width: min(560px, 100%); height: 56px;
640
762
  padding: 0 22px 0 26px; border-radius: var(--border-radius-pill);
@@ -765,7 +887,7 @@ button.chip:disabled { opacity: .5; }
765
887
 
766
888
  /* On the site: storing the draft, and the way out. */
767
889
  .footer {
768
- display: flex; align-items: center; gap: 8px; padding: 10px 12px 10px 22px;
890
+ display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 10px 12px 10px 22px;
769
891
  border-top: 1px solid rgb(var(--theme-border-rgb) / .2);
770
892
  background: rgb(var(--theme-background-actions-rgb) / .45);
771
893
  }
@@ -923,8 +1045,9 @@ const mountPromptBox = (doc, bridge, options = {})=>{
923
1045
  className: 'action chip',
924
1046
  type: 'button'
925
1047
  }, icon(doc, 'newPage'), labels.newPage);
926
- // A product the site marks (editableProduct) without a page of its own:
927
- // one click makes its product page, which the page then shows.
1048
+ // A product (collection) the site marks (editableProduct,
1049
+ // editableCollection) without a page of its own: one click makes its
1050
+ // page, which the page then shows.
928
1051
  const addProduct = element(doc, 'button', {
929
1052
  className: 'action chip product',
930
1053
  type: 'button'
@@ -1019,6 +1142,16 @@ const mountPromptBox = (doc, bridge, options = {})=>{
1019
1142
  }, element(doc, 'label', {
1020
1143
  className: 'search'
1021
1144
  }, icon(doc, 'search'), search), optionList, pagesNote);
1145
+ // The page's own controls (`actions`), in the footer while open and next
1146
+ // to the pill while closed.
1147
+ const actionsInFooter = element(doc, 'slot');
1148
+ actionsInFooter.name = 'actions-footer';
1149
+ const actionsInBar = element(doc, 'slot');
1150
+ actionsInBar.name = 'actions-bar';
1151
+ if (options.actions) {
1152
+ options.actions.slot = 'actions-bar';
1153
+ host.appendChild(options.actions);
1154
+ }
1022
1155
  const unsaved = element(doc, 'span', {
1023
1156
  className: 'unsaved'
1024
1157
  });
@@ -1045,7 +1178,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
1045
1178
  className: 'footer'
1046
1179
  }, pageChip, unsaved, element(doc, 'span', {
1047
1180
  className: 'spacer'
1048
- }), signOut, save, publish);
1181
+ }), actionsInFooter, signOut, save, publish);
1049
1182
  const panel = element(doc, 'form', {
1050
1183
  className: 'panel'
1051
1184
  }, status, pathRow, input, formBox, toolbar, pagesSection, footer);
@@ -1053,7 +1186,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
1053
1186
  className: 'root'
1054
1187
  }, element(doc, 'div', {
1055
1188
  className: 'bar'
1056
- }, pill, viewing), panel);
1189
+ }, pill, viewing, actionsInBar), panel);
1057
1190
  shadow.append(element(doc, 'style', {
1058
1191
  textContent: CSS
1059
1192
  }), root);
@@ -1082,7 +1215,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
1082
1215
  dragged = false;
1083
1216
  if (event.button !== 0 || !event.isPrimary) return;
1084
1217
  const target = event.target;
1085
- const control = target.closest('button, input, textarea, iframe, a, label, .pages, .form');
1218
+ const control = target.closest('button, input, textarea, select, iframe, a, label, .pages, .form');
1086
1219
  if (control && control !== pill) return;
1087
1220
  const { left, right } = root.getBoundingClientRect();
1088
1221
  drag = {
@@ -1178,8 +1311,20 @@ const mountPromptBox = (doc, bridge, options = {})=>{
1178
1311
  // A page of the site that isn't an entry (no `editable(entry)`, like a
1179
1312
  // hand-written /about) has nothing to edit: only New page.
1180
1313
  const pageless = site && bridge.entryId === null;
1181
- // …unless it shows a product: then Add a product page first.
1182
- const productHere = pageless && canCreate && bridge.product !== null;
1314
+ // …unless it shows a product (collection): then Add a product
1315
+ // (collection) page first.
1316
+ const shopHere = !pageless || !canCreate ? null : bridge.product ? 'product' : bridge.collection ? 'collection' : null;
1317
+ const productHere = shopHere !== null;
1318
+ const shopCopy = shopHere === 'collection' ? {
1319
+ add: labels.addCollectionPage,
1320
+ adding: labels.addingCollectionPage,
1321
+ hint: labels.collectionPageHint
1322
+ } : {
1323
+ add: labels.addProductPage,
1324
+ adding: labels.addingProductPage,
1325
+ hint: labels.productPageHint
1326
+ };
1327
+ addProduct.lastChild.textContent = shopCopy.add;
1183
1328
  const onlyNew = pageless && canCreate && !productHere;
1184
1329
  if (!canCreate) creating = false;
1185
1330
  if (onlyNew) creating = true;
@@ -1189,7 +1334,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
1189
1334
  if (showForm && !formHandle) mountForm();
1190
1335
  else if (!showForm && formHandle) unmountForm();
1191
1336
  const offerProduct = productHere && !creating;
1192
- pillText.textContent = offerProduct ? labels.addProductPage : onlyNew ? labels.newPage : labels.open;
1337
+ pillText.textContent = offerProduct ? shopCopy.add : onlyNew ? labels.newPage : labels.open;
1193
1338
  cancelNew.hidden = onlyNew;
1194
1339
  scope.hidden = creating || offerProduct;
1195
1340
  newScope.hidden = !creating;
@@ -1207,7 +1352,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
1207
1352
  toolbar.hidden = picking;
1208
1353
  pagesSection.hidden = !picking;
1209
1354
  input.placeholder = creating ? labels.newPagePlaceholder : labels.placeholder;
1210
- scopeText.textContent = state.selectedUid ? labels.scopeBlock : labels.scopeEntry;
1355
+ scopeText.textContent = state.selectedUid ? labels.scopeBlock : pageless ? labels.scopeNoPage : labels.scopeEntry;
1211
1356
  clearBlock.hidden = !state.selectedUid;
1212
1357
  pageIcon.hidden = !!state.selectedUid;
1213
1358
  blockIcon.hidden = !state.selectedUid;
@@ -1237,7 +1382,11 @@ const mountPromptBox = (doc, bridge, options = {})=>{
1237
1382
  publish.hidden = !state.features.includes('publish') || pageless;
1238
1383
  publish.disabled = busy;
1239
1384
  publish.textContent = state.publishing ? labels.publishing : labels.publish;
1240
- signOut.hidden = !site;
1385
+ signOut.hidden = !site || options.signOut === false;
1386
+ pillSignOut.hidden = options.signOut === false;
1387
+ if (options.actions) {
1388
+ options.actions.slot = open && available ? 'actions-footer' : 'actions-bar';
1389
+ }
1241
1390
  footer.hidden = !site;
1242
1391
  unsaved.hidden = !state.unsaved;
1243
1392
  pageChip.hidden = !site;
@@ -1246,7 +1395,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
1246
1395
  const loadingForm = form && formLoading && !picking;
1247
1396
  const working = state.prompting;
1248
1397
  // A form opening says so in its own place.
1249
- const note = state.prompting ? creating ? labels.creating : offerProduct ? labels.addingProductPage : labels.working : loadingForm ? '' : message?.text ?? (offerProduct ? labels.productPageHint : site && !creating && !picking && !state.selectedUid ? labels.selectHint : '');
1398
+ const note = state.prompting ? creating ? labels.creating : offerProduct ? shopCopy.adding : labels.working : loadingForm ? '' : message?.text ?? (offerProduct ? shopCopy.hint : site && !creating && !picking && !state.selectedUid ? labels.selectHint : '');
1250
1399
  status.textContent = note;
1251
1400
  status.hidden = !note || minimized;
1252
1401
  status.className = `status${working ? ' working' : ''}${!busy && !loadingForm && message?.error ? ' error' : ''}`;
@@ -1469,11 +1618,19 @@ const mountPromptBox = (doc, bridge, options = {})=>{
1469
1618
  message = null;
1470
1619
  try {
1471
1620
  // The bridge reloads the page; this shows until it does.
1472
- await bridge.createProductPage();
1473
- message = {
1474
- text: labels.addedProductPage,
1475
- error: false
1476
- };
1621
+ if (bridge.product) {
1622
+ await bridge.createProductPage();
1623
+ message = {
1624
+ text: labels.addedProductPage,
1625
+ error: false
1626
+ };
1627
+ } else {
1628
+ await bridge.createCollectionPage();
1629
+ message = {
1630
+ text: labels.addedCollectionPage,
1631
+ error: false
1632
+ };
1633
+ }
1477
1634
  } catch (error) {
1478
1635
  message = failure(error);
1479
1636
  }
@@ -1670,7 +1827,7 @@ const FORM_BURST_MS = 1000;
1670
1827
  // How long the page waits for the form's edits to settle before it shows
1671
1828
  // them.
1672
1829
  const FORM_RENDER_MS = 300;
1673
- const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}`, productPath: ownProductPath } = {})=>{
1830
+ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}`, productPath: ownProductPath, collectionPath: ownCollectionPath } = {})=>{
1674
1831
  const doc = win.document;
1675
1832
  const emitter = createEmitter();
1676
1833
  const cleanup = [];
@@ -1681,6 +1838,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}`, produc
1681
1838
  const touched = new Set();
1682
1839
  let session = null;
1683
1840
  const productPath = (handle, locale)=>ownProductPath ? ownProductPath(handle, locale) : (session?.productPath ?? '/products/{handle}').replaceAll('{handle}', encodeURIComponent(handle));
1841
+ const collectionPath = (handle, locale)=>ownCollectionPath ? ownCollectionPath(handle, locale) : (session?.collectionPath ?? '/collections/{handle}').replaceAll('{handle}', encodeURIComponent(handle));
1684
1842
  // The open form's, if any; and when the form last changed content, for
1685
1843
  // undo steps (-Infinity: the next edit starts a new step).
1686
1844
  let closeForm = null;
@@ -1735,6 +1893,16 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}`, produc
1735
1893
  handle
1736
1894
  } : null;
1737
1895
  };
1896
+ // The collection the page shows (editableCollection), if it marks one.
1897
+ const pageCollection = ()=>{
1898
+ const element = doc.querySelector(`[${preview.COLLECTION_ATTRIBUTE}]`);
1899
+ const id = element?.getAttribute(preview.COLLECTION_ATTRIBUTE);
1900
+ const handle = element?.getAttribute(preview.COLLECTION_HANDLE_ATTRIBUTE);
1901
+ return id && handle ? {
1902
+ id,
1903
+ handle
1904
+ } : null;
1905
+ };
1738
1906
  const entryFor = (uid)=>{
1739
1907
  const block = uid ? doc.querySelector(blockSelector(uid)) : null;
1740
1908
  return block?.closest(`[${preview.ENTRY_ATTRIBUTE}]`)?.getAttribute(preview.ENTRY_ATTRIBUTE) ?? pageEntryId();
@@ -1845,6 +2013,9 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}`, produc
1845
2013
  get product () {
1846
2014
  return pageProduct();
1847
2015
  },
2016
+ get collection () {
2017
+ return pageCollection();
2018
+ },
1848
2019
  environment: null,
1849
2020
  on: emitter.on,
1850
2021
  getState: ()=>state,
@@ -1942,6 +2113,34 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}`, produc
1942
2113
  });
1943
2114
  }
1944
2115
  },
2116
+ async createCollectionPage (collection = pageCollection() ?? undefined) {
2117
+ if (!collection) {
2118
+ throw new Error('Nothing on this page is marked as a collection: add editableCollection(collection)');
2119
+ }
2120
+ if (!has('create')) throw new Error('You may not add pages here');
2121
+ if (state.unsaved) {
2122
+ throw new Error('Save or undo your changes before adding a page');
2123
+ }
2124
+ setState({
2125
+ prompting: true
2126
+ });
2127
+ try {
2128
+ const made = await api('collection-pages', {
2129
+ collection: {
2130
+ id: collection.id,
2131
+ handle: collection.handle
2132
+ }
2133
+ });
2134
+ // The site's collection route finds its page now (drafts, while
2135
+ // editing): show it.
2136
+ win.location.reload();
2137
+ return made;
2138
+ } finally{
2139
+ setState({
2140
+ prompting: false
2141
+ });
2142
+ }
2143
+ },
1945
2144
  async view (options = {}) {
1946
2145
  const { uid, entryId } = target(options.uid);
1947
2146
  const entry = await load(entryId);
@@ -2095,8 +2294,9 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}`, produc
2095
2294
  contentType: page.contentType,
2096
2295
  status: page.status,
2097
2296
  path,
2098
- url: page.product ? productPath(page.product.handle, code) : pagePath(path, code),
2099
- product: page.product ?? null
2297
+ url: page.product ? productPath(page.product.handle, code) : page.collection ? collectionPath(page.collection.handle, code) : pagePath(path, code),
2298
+ product: page.product ?? null,
2299
+ collection: page.collection ?? null
2100
2300
  }
2101
2301
  ];
2102
2302
  });
@@ -2241,7 +2441,7 @@ const peekContentBridge = ()=>typeof window === 'undefined' ? null : window[KEY]
2241
2441
  // if (bridge.isEditing) bridge.on('update', ({ entry }) => render(entry))
2242
2442
  //
2243
2443
  // The first call's options win: there is one bridge per page.
2244
- const getContentBridge = ({ url, prompt = true, pagePath, productPath } = {})=>{
2444
+ const getContentBridge = ({ url, prompt = true, pagePath, productPath, collectionPath } = {})=>{
2245
2445
  if (typeof window === 'undefined') return SERVER_BRIDGE;
2246
2446
  const win = window;
2247
2447
  if (win[KEY]) return win[KEY];
@@ -2255,12 +2455,26 @@ const getContentBridge = ({ url, prompt = true, pagePath, productPath } = {})=>{
2255
2455
  const sitePath = bridge.isEditing || win.parent !== win ? null : readSiteEditing(win.document);
2256
2456
  if (sitePath) bridge = createSiteBridge(win, sitePath, {
2257
2457
  pagePath,
2258
- productPath
2458
+ productPath,
2459
+ collectionPath
2259
2460
  });
2260
2461
  if (bridge.isEditing && prompt) {
2261
- const unmount = mountPromptBox(win.document, bridge, prompt === true ? {} : prompt);
2462
+ const doc = win.document;
2463
+ let unmount = ()=>{};
2464
+ let destroyed = false;
2465
+ const mount = ()=>{
2466
+ if (destroyed) return;
2467
+ unmount = mountPromptBox(doc, bridge, prompt === true ? {} : prompt);
2468
+ };
2469
+ // A script tag in <head> runs before there is a body to mount into.
2470
+ if (doc.body) mount();
2471
+ else doc.addEventListener('DOMContentLoaded', mount, {
2472
+ once: true
2473
+ });
2262
2474
  const destroy = bridge.destroy;
2263
2475
  bridge.destroy = ()=>{
2476
+ destroyed = true;
2477
+ doc.removeEventListener('DOMContentLoaded', mount);
2264
2478
  unmount();
2265
2479
  destroy();
2266
2480
  };