@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/README.md +36 -0
- package/dist/bridge.d.mts +14 -2
- package/dist/bridge.d.mts.map +1 -1
- package/dist/bridge.d.ts +14 -2
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.global.js +5 -5
- package/dist/bridge.js +245 -31
- package/dist/bridge.mjs +245 -31
- 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;
|
|
@@ -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
|
|
927
|
-
//
|
|
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
|
|
1182
|
-
|
|
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 ?
|
|
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 ?
|
|
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
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
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
|
|
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
|
};
|