@obelism/content 0.19.1 → 0.20.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/README.md +26 -0
- package/dist/bridge.d.mts +11 -2
- package/dist/bridge.d.mts.map +1 -1
- package/dist/bridge.d.ts +11 -2
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.global.js +17 -4
- package/dist/bridge.js +246 -27
- package/dist/bridge.mjs +246 -27
- package/dist/index.d.mts +9 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.mjs +1 -1
- package/dist/{preview-jcmxxj92.mjs → preview-fu3qtgfx.mjs} +15 -1
- package/dist/{preview-ijk3ixwb.js → preview-ooq8w3jv.js} +17 -0
- package/dist/site.js +6 -1
- package/dist/site.mjs +6 -1
- package/package.json +1 -1
package/dist/bridge.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
2
2
|
|
|
3
|
-
var preview = require('./preview-
|
|
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/
|
|
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
|
-
|
|
346
|
-
|
|
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;
|
|
@@ -544,7 +661,11 @@ const DEFAULT_PROMPT_LABELS = {
|
|
|
544
661
|
addProductPage: 'Add a product page',
|
|
545
662
|
addingProductPage: 'Making the product page…',
|
|
546
663
|
addedProductPage: 'Opening the product page…',
|
|
547
|
-
productPageHint: 'This product has no page yet. Add one for what the store doesn’t say.'
|
|
664
|
+
productPageHint: 'This product has no page yet. Add one for what the store doesn’t say.',
|
|
665
|
+
addCollectionPage: 'Add a collection page',
|
|
666
|
+
addingCollectionPage: 'Making the collection page…',
|
|
667
|
+
addedCollectionPage: 'Opening the collection page…',
|
|
668
|
+
collectionPageHint: 'This collection has no page yet. Add one for what goes around its products.'
|
|
548
669
|
};
|
|
549
670
|
const SIDE_KEY = 'obelism-content-prompt-side';
|
|
550
671
|
// How far the pointer moves before it drags the box, not clicks it.
|
|
@@ -689,7 +810,20 @@ textarea::placeholder { color: rgb(var(--theme-foreground-muted-rgb)); }
|
|
|
689
810
|
display: block; width: 100%; height: min(560px, 60vh); border: none; border-radius: 12px;
|
|
690
811
|
background: rgb(var(--theme-background-actions-rgb) / .45);
|
|
691
812
|
}
|
|
692
|
-
|
|
813
|
+
/* Loading, it keeps the form's own height, the spinner in the middle,
|
|
814
|
+
whatever the form says meanwhile; the frame shows once the form does. */
|
|
815
|
+
.form.loading { position: relative; height: calc(min(560px, 60vh) + 12px); }
|
|
816
|
+
.form.loading iframe { position: absolute; visibility: hidden; }
|
|
817
|
+
.form-loader {
|
|
818
|
+
position: absolute; inset: 12px 12px 0; display: flex; align-items: center; justify-content: center;
|
|
819
|
+
gap: 10px; border-radius: 12px; color: rgb(var(--theme-foreground-muted-rgb));
|
|
820
|
+
background: rgb(var(--theme-background-actions-rgb) / .45);
|
|
821
|
+
}
|
|
822
|
+
.form-loader::before {
|
|
823
|
+
content: ''; width: 16px; height: 16px; flex: none; border-radius: 50%;
|
|
824
|
+
border: 2px solid rgb(var(--theme-border-rgb) / .3); border-top-color: rgb(var(--theme-accent-rgb));
|
|
825
|
+
animation: spin .7s linear infinite;
|
|
826
|
+
}
|
|
693
827
|
/* New page: where it goes, above what it is for. */
|
|
694
828
|
.path {
|
|
695
829
|
display: flex; align-items: center; gap: 10px; padding: 16px 22px 0;
|
|
@@ -862,9 +996,14 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
862
996
|
});
|
|
863
997
|
input.setAttribute('aria-label', labels.open);
|
|
864
998
|
// Form mode (on the site): the CMS's form, mounted while shown.
|
|
999
|
+
const formLoader = element(doc, 'div', {
|
|
1000
|
+
className: 'form-loader',
|
|
1001
|
+
textContent: labels.formLoading
|
|
1002
|
+
});
|
|
1003
|
+
formLoader.setAttribute('role', 'status');
|
|
865
1004
|
const formBox = element(doc, 'div', {
|
|
866
1005
|
className: 'form'
|
|
867
|
-
});
|
|
1006
|
+
}, formLoader);
|
|
868
1007
|
// What the prompt acts on: the whole page, or the selected block with a
|
|
869
1008
|
// way back to the page.
|
|
870
1009
|
const pageIcon = icon(doc, 'page');
|
|
@@ -905,8 +1044,9 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
905
1044
|
className: 'action chip',
|
|
906
1045
|
type: 'button'
|
|
907
1046
|
}, icon(doc, 'newPage'), labels.newPage);
|
|
908
|
-
// A product the site marks (editableProduct
|
|
909
|
-
//
|
|
1047
|
+
// A product (collection) the site marks (editableProduct,
|
|
1048
|
+
// editableCollection) without a page of its own: one click makes its
|
|
1049
|
+
// page, which the page then shows.
|
|
910
1050
|
const addProduct = element(doc, 'button', {
|
|
911
1051
|
className: 'action chip product',
|
|
912
1052
|
type: 'button'
|
|
@@ -1160,8 +1300,20 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1160
1300
|
// A page of the site that isn't an entry (no `editable(entry)`, like a
|
|
1161
1301
|
// hand-written /about) has nothing to edit: only New page.
|
|
1162
1302
|
const pageless = site && bridge.entryId === null;
|
|
1163
|
-
// …unless it shows a product: then Add a product
|
|
1164
|
-
|
|
1303
|
+
// …unless it shows a product (collection): then Add a product
|
|
1304
|
+
// (collection) page first.
|
|
1305
|
+
const shopHere = !pageless || !canCreate ? null : bridge.product ? 'product' : bridge.collection ? 'collection' : null;
|
|
1306
|
+
const productHere = shopHere !== null;
|
|
1307
|
+
const shopCopy = shopHere === 'collection' ? {
|
|
1308
|
+
add: labels.addCollectionPage,
|
|
1309
|
+
adding: labels.addingCollectionPage,
|
|
1310
|
+
hint: labels.collectionPageHint
|
|
1311
|
+
} : {
|
|
1312
|
+
add: labels.addProductPage,
|
|
1313
|
+
adding: labels.addingProductPage,
|
|
1314
|
+
hint: labels.productPageHint
|
|
1315
|
+
};
|
|
1316
|
+
addProduct.lastChild.textContent = shopCopy.add;
|
|
1165
1317
|
const onlyNew = pageless && canCreate && !productHere;
|
|
1166
1318
|
if (!canCreate) creating = false;
|
|
1167
1319
|
if (onlyNew) creating = true;
|
|
@@ -1171,7 +1323,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1171
1323
|
if (showForm && !formHandle) mountForm();
|
|
1172
1324
|
else if (!showForm && formHandle) unmountForm();
|
|
1173
1325
|
const offerProduct = productHere && !creating;
|
|
1174
|
-
pillText.textContent = offerProduct ?
|
|
1326
|
+
pillText.textContent = offerProduct ? shopCopy.add : onlyNew ? labels.newPage : labels.open;
|
|
1175
1327
|
cancelNew.hidden = onlyNew;
|
|
1176
1328
|
scope.hidden = creating || offerProduct;
|
|
1177
1329
|
newScope.hidden = !creating;
|
|
@@ -1185,6 +1337,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1185
1337
|
input.hidden = form || picking || minimized || offerProduct;
|
|
1186
1338
|
formBox.hidden = !form || picking || minimized;
|
|
1187
1339
|
formBox.classList.toggle('loading', formLoading);
|
|
1340
|
+
formLoader.hidden = !formLoading;
|
|
1188
1341
|
toolbar.hidden = picking;
|
|
1189
1342
|
pagesSection.hidden = !picking;
|
|
1190
1343
|
input.placeholder = creating ? labels.newPagePlaceholder : labels.placeholder;
|
|
@@ -1225,8 +1378,9 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1225
1378
|
pageChip.setAttribute('aria-expanded', String(picking));
|
|
1226
1379
|
pageChipText.textContent = pages?.find(({ entryId })=>entryId === bridge.entryId)?.name ?? labels.pages;
|
|
1227
1380
|
const loadingForm = form && formLoading && !picking;
|
|
1228
|
-
const working = state.prompting
|
|
1229
|
-
|
|
1381
|
+
const working = state.prompting;
|
|
1382
|
+
// A form opening says so in its own place.
|
|
1383
|
+
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 : '');
|
|
1230
1384
|
status.textContent = note;
|
|
1231
1385
|
status.hidden = !note || minimized;
|
|
1232
1386
|
status.className = `status${working ? ' working' : ''}${!busy && !loadingForm && message?.error ? ' error' : ''}`;
|
|
@@ -1449,11 +1603,19 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1449
1603
|
message = null;
|
|
1450
1604
|
try {
|
|
1451
1605
|
// The bridge reloads the page; this shows until it does.
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1606
|
+
if (bridge.product) {
|
|
1607
|
+
await bridge.createProductPage();
|
|
1608
|
+
message = {
|
|
1609
|
+
text: labels.addedProductPage,
|
|
1610
|
+
error: false
|
|
1611
|
+
};
|
|
1612
|
+
} else {
|
|
1613
|
+
await bridge.createCollectionPage();
|
|
1614
|
+
message = {
|
|
1615
|
+
text: labels.addedCollectionPage,
|
|
1616
|
+
error: false
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1457
1619
|
} catch (error) {
|
|
1458
1620
|
message = failure(error);
|
|
1459
1621
|
}
|
|
@@ -1650,7 +1812,7 @@ const FORM_BURST_MS = 1000;
|
|
|
1650
1812
|
// How long the page waits for the form's edits to settle before it shows
|
|
1651
1813
|
// them.
|
|
1652
1814
|
const FORM_RENDER_MS = 300;
|
|
1653
|
-
const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}`, productPath: ownProductPath } = {})=>{
|
|
1815
|
+
const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}`, productPath: ownProductPath, collectionPath: ownCollectionPath } = {})=>{
|
|
1654
1816
|
const doc = win.document;
|
|
1655
1817
|
const emitter = createEmitter();
|
|
1656
1818
|
const cleanup = [];
|
|
@@ -1661,6 +1823,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}`, produc
|
|
|
1661
1823
|
const touched = new Set();
|
|
1662
1824
|
let session = null;
|
|
1663
1825
|
const productPath = (handle, locale)=>ownProductPath ? ownProductPath(handle, locale) : (session?.productPath ?? '/products/{handle}').replaceAll('{handle}', encodeURIComponent(handle));
|
|
1826
|
+
const collectionPath = (handle, locale)=>ownCollectionPath ? ownCollectionPath(handle, locale) : (session?.collectionPath ?? '/collections/{handle}').replaceAll('{handle}', encodeURIComponent(handle));
|
|
1664
1827
|
// The open form's, if any; and when the form last changed content, for
|
|
1665
1828
|
// undo steps (-Infinity: the next edit starts a new step).
|
|
1666
1829
|
let closeForm = null;
|
|
@@ -1715,6 +1878,16 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}`, produc
|
|
|
1715
1878
|
handle
|
|
1716
1879
|
} : null;
|
|
1717
1880
|
};
|
|
1881
|
+
// The collection the page shows (editableCollection), if it marks one.
|
|
1882
|
+
const pageCollection = ()=>{
|
|
1883
|
+
const element = doc.querySelector(`[${preview.COLLECTION_ATTRIBUTE}]`);
|
|
1884
|
+
const id = element?.getAttribute(preview.COLLECTION_ATTRIBUTE);
|
|
1885
|
+
const handle = element?.getAttribute(preview.COLLECTION_HANDLE_ATTRIBUTE);
|
|
1886
|
+
return id && handle ? {
|
|
1887
|
+
id,
|
|
1888
|
+
handle
|
|
1889
|
+
} : null;
|
|
1890
|
+
};
|
|
1718
1891
|
const entryFor = (uid)=>{
|
|
1719
1892
|
const block = uid ? doc.querySelector(blockSelector(uid)) : null;
|
|
1720
1893
|
return block?.closest(`[${preview.ENTRY_ATTRIBUTE}]`)?.getAttribute(preview.ENTRY_ATTRIBUTE) ?? pageEntryId();
|
|
@@ -1825,6 +1998,9 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}`, produc
|
|
|
1825
1998
|
get product () {
|
|
1826
1999
|
return pageProduct();
|
|
1827
2000
|
},
|
|
2001
|
+
get collection () {
|
|
2002
|
+
return pageCollection();
|
|
2003
|
+
},
|
|
1828
2004
|
environment: null,
|
|
1829
2005
|
on: emitter.on,
|
|
1830
2006
|
getState: ()=>state,
|
|
@@ -1922,6 +2098,34 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}`, produc
|
|
|
1922
2098
|
});
|
|
1923
2099
|
}
|
|
1924
2100
|
},
|
|
2101
|
+
async createCollectionPage (collection = pageCollection() ?? undefined) {
|
|
2102
|
+
if (!collection) {
|
|
2103
|
+
throw new Error('Nothing on this page is marked as a collection: add editableCollection(collection)');
|
|
2104
|
+
}
|
|
2105
|
+
if (!has('create')) throw new Error('You may not add pages here');
|
|
2106
|
+
if (state.unsaved) {
|
|
2107
|
+
throw new Error('Save or undo your changes before adding a page');
|
|
2108
|
+
}
|
|
2109
|
+
setState({
|
|
2110
|
+
prompting: true
|
|
2111
|
+
});
|
|
2112
|
+
try {
|
|
2113
|
+
const made = await api('collection-pages', {
|
|
2114
|
+
collection: {
|
|
2115
|
+
id: collection.id,
|
|
2116
|
+
handle: collection.handle
|
|
2117
|
+
}
|
|
2118
|
+
});
|
|
2119
|
+
// The site's collection route finds its page now (drafts, while
|
|
2120
|
+
// editing): show it.
|
|
2121
|
+
win.location.reload();
|
|
2122
|
+
return made;
|
|
2123
|
+
} finally{
|
|
2124
|
+
setState({
|
|
2125
|
+
prompting: false
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
2128
|
+
},
|
|
1925
2129
|
async view (options = {}) {
|
|
1926
2130
|
const { uid, entryId } = target(options.uid);
|
|
1927
2131
|
const entry = await load(entryId);
|
|
@@ -2075,8 +2279,9 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}`, produc
|
|
|
2075
2279
|
contentType: page.contentType,
|
|
2076
2280
|
status: page.status,
|
|
2077
2281
|
path,
|
|
2078
|
-
url: page.product ? productPath(page.product.handle, code) : pagePath(path, code),
|
|
2079
|
-
product: page.product ?? null
|
|
2282
|
+
url: page.product ? productPath(page.product.handle, code) : page.collection ? collectionPath(page.collection.handle, code) : pagePath(path, code),
|
|
2283
|
+
product: page.product ?? null,
|
|
2284
|
+
collection: page.collection ?? null
|
|
2080
2285
|
}
|
|
2081
2286
|
];
|
|
2082
2287
|
});
|
|
@@ -2221,7 +2426,7 @@ const peekContentBridge = ()=>typeof window === 'undefined' ? null : window[KEY]
|
|
|
2221
2426
|
// if (bridge.isEditing) bridge.on('update', ({ entry }) => render(entry))
|
|
2222
2427
|
//
|
|
2223
2428
|
// The first call's options win: there is one bridge per page.
|
|
2224
|
-
const getContentBridge = ({ url, prompt = true, pagePath, productPath } = {})=>{
|
|
2429
|
+
const getContentBridge = ({ url, prompt = true, pagePath, productPath, collectionPath } = {})=>{
|
|
2225
2430
|
if (typeof window === 'undefined') return SERVER_BRIDGE;
|
|
2226
2431
|
const win = window;
|
|
2227
2432
|
if (win[KEY]) return win[KEY];
|
|
@@ -2235,12 +2440,26 @@ const getContentBridge = ({ url, prompt = true, pagePath, productPath } = {})=>{
|
|
|
2235
2440
|
const sitePath = bridge.isEditing || win.parent !== win ? null : readSiteEditing(win.document);
|
|
2236
2441
|
if (sitePath) bridge = createSiteBridge(win, sitePath, {
|
|
2237
2442
|
pagePath,
|
|
2238
|
-
productPath
|
|
2443
|
+
productPath,
|
|
2444
|
+
collectionPath
|
|
2239
2445
|
});
|
|
2240
2446
|
if (bridge.isEditing && prompt) {
|
|
2241
|
-
const
|
|
2447
|
+
const doc = win.document;
|
|
2448
|
+
let unmount = ()=>{};
|
|
2449
|
+
let destroyed = false;
|
|
2450
|
+
const mount = ()=>{
|
|
2451
|
+
if (destroyed) return;
|
|
2452
|
+
unmount = mountPromptBox(doc, bridge, prompt === true ? {} : prompt);
|
|
2453
|
+
};
|
|
2454
|
+
// A script tag in <head> runs before there is a body to mount into.
|
|
2455
|
+
if (doc.body) mount();
|
|
2456
|
+
else doc.addEventListener('DOMContentLoaded', mount, {
|
|
2457
|
+
once: true
|
|
2458
|
+
});
|
|
2242
2459
|
const destroy = bridge.destroy;
|
|
2243
2460
|
bridge.destroy = ()=>{
|
|
2461
|
+
destroyed = true;
|
|
2462
|
+
doc.removeEventListener('DOMContentLoaded', mount);
|
|
2244
2463
|
unmount();
|
|
2245
2464
|
destroy();
|
|
2246
2465
|
};
|