@obelism/content 0.17.0 → 0.19.1
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 +45 -7
- package/dist/bridge.d.mts +16 -3
- package/dist/bridge.d.mts.map +1 -1
- package/dist/bridge.d.ts +16 -3
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.global.js +16 -5
- package/dist/bridge.js +239 -28
- package/dist/bridge.mjs +239 -28
- package/dist/index.d.mts +15 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +15 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.mjs +3 -3
- package/dist/{preview-mpwfmnla.js → preview-ijk3ixwb.js} +17 -0
- package/dist/{preview-h7rwp06v.mjs → preview-jcmxxj92.mjs} +15 -1
- package/dist/site.js +5 -0
- package/dist/site.mjs +5 -0
- package/dist/{types-e5nvgudo.d.ts → types-mtjco3a5.d.ts} +10 -2
- package/dist/types-mtjco3a5.d.ts.map +1 -0
- package/package.json +1 -1
- package/dist/types-e5nvgudo.d.ts.map +0 -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-ijk3ixwb.js');
|
|
4
4
|
var site = require('./site.js');
|
|
5
5
|
var config = require('./config-ci2hxmas.js');
|
|
6
6
|
|
|
@@ -229,6 +229,7 @@ const onlyWhileEditingSite = ()=>Promise.reject(new Error('Only while signed in
|
|
|
229
229
|
const INERT_METHODS = {
|
|
230
230
|
prompt: inertPrompt,
|
|
231
231
|
createPage: ()=>Promise.reject(new Error('New pages are made on the site itself')),
|
|
232
|
+
createProductPage: ()=>Promise.reject(new Error('Product pages are made on the site itself')),
|
|
232
233
|
view: onlyWhileEditingSite,
|
|
233
234
|
applyJson: onlyWhileEditingSite,
|
|
234
235
|
openForm: (_container, options = {})=>{
|
|
@@ -286,6 +287,7 @@ const createBridge = (win, cmsOrigin)=>{
|
|
|
286
287
|
mode: isEditing ? 'editor' : null,
|
|
287
288
|
entryId: params.get(preview.PREVIEW_PARAMS.entry),
|
|
288
289
|
environment: params.get(preview.PREVIEW_PARAMS.environment),
|
|
290
|
+
product: null,
|
|
289
291
|
on: emitter.on,
|
|
290
292
|
getState: ()=>state,
|
|
291
293
|
...INERT_METHODS,
|
|
@@ -478,6 +480,7 @@ const wrapScriptBridge = (script, params)=>{
|
|
|
478
480
|
mode: script.isEditing ? 'editor' : null,
|
|
479
481
|
entryId: script.entryId,
|
|
480
482
|
environment: params.get(preview.PREVIEW_PARAMS.environment),
|
|
483
|
+
product: null,
|
|
481
484
|
on: (event, listener)=>{
|
|
482
485
|
if (event === 'update' && !forwardingUpdates) {
|
|
483
486
|
forwardingUpdates = true;
|
|
@@ -537,8 +540,24 @@ const DEFAULT_PROMPT_LABELS = {
|
|
|
537
540
|
pagesEmpty: 'No pages match',
|
|
538
541
|
pageCurrent: 'This page',
|
|
539
542
|
pageDraft: 'Draft',
|
|
540
|
-
pageChanged: 'Changed'
|
|
543
|
+
pageChanged: 'Changed',
|
|
544
|
+
addProductPage: 'Add a product page',
|
|
545
|
+
addingProductPage: 'Making the product page…',
|
|
546
|
+
addedProductPage: 'Opening the product page…',
|
|
547
|
+
productPageHint: 'This product has no page yet. Add one for what the store doesn’t say.'
|
|
541
548
|
};
|
|
549
|
+
const SIDE_KEY = 'obelism-content-prompt-side';
|
|
550
|
+
// How far the pointer moves before it drags the box, not clicks it.
|
|
551
|
+
const DRAG_THRESHOLD_PX = 6;
|
|
552
|
+
// How far ahead a flick carries the box: its speed for this long.
|
|
553
|
+
const FLICK_MS = 150;
|
|
554
|
+
// …at most this far, so one coarse pointer event isn't a flick across the
|
|
555
|
+
// screen…
|
|
556
|
+
const FLICK_MAX_PX = 120;
|
|
557
|
+
// …and none when the pointer stood still this long before letting go.
|
|
558
|
+
const FLICK_STALE_MS = 80;
|
|
559
|
+
// Its distance from the side of the screen, as in the CSS.
|
|
560
|
+
const EDGE_PX = 16;
|
|
542
561
|
// Icons (Lucide), drawn in the text colour.
|
|
543
562
|
const ICONS = {
|
|
544
563
|
page: '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/>',
|
|
@@ -551,6 +570,7 @@ const ICONS = {
|
|
|
551
570
|
chevrons: '<path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/>',
|
|
552
571
|
search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
|
|
553
572
|
check: '<path d="M20 6 9 17l-5-5"/>',
|
|
573
|
+
mark: '<path d="M12 3v18M3 12h18"/><g fill="currentColor" stroke="none"><circle cx="7" cy="7" r="1.75"/><circle cx="17" cy="7" r="1.75"/><circle cx="7" cy="17" r="1.75"/><circle cx="17" cy="17" r="1.75"/></g>',
|
|
554
574
|
send: '<path d="M3.714 3.048a.498.498 0 0 0-.683.627l2.843 7.627a2 2 0 0 1 0 1.396l-2.842 7.627a.498.498 0 0 0 .682.627l18-8.5a.5.5 0 0 0 0-.904z"/><path d="M6 12h16"/>'
|
|
555
575
|
};
|
|
556
576
|
// Obelism's design tokens (obelism.studio/design.md), under their own names:
|
|
@@ -576,10 +596,20 @@ const CSS = `
|
|
|
576
596
|
--font-inter: 'InterVariable', 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
|
577
597
|
Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
|
|
578
598
|
position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%);
|
|
599
|
+
transition: translate .45s cubic-bezier(0.34, 1.3, 0.64, 1);
|
|
579
600
|
z-index: 2147483647; width: min(640px, calc(100vw - 32px)); color-scheme: light dark;
|
|
580
601
|
font: 400 14px/1.4 var(--font-inter); color: rgb(var(--theme-foreground-rgb));
|
|
581
602
|
-webkit-font-smoothing: antialiased;
|
|
582
603
|
}
|
|
604
|
+
/* Dragged to a side of the screen, it stays there, springing into place. */
|
|
605
|
+
.root[data-side='left'] { left: 16px; transform: none; }
|
|
606
|
+
.root[data-side='right'] { left: auto; right: 16px; transform: none; }
|
|
607
|
+
.root[data-side='left'] .bar { justify-content: flex-start; }
|
|
608
|
+
.root[data-side='right'] .bar { justify-content: flex-end; }
|
|
609
|
+
.root.dragging { transition: none; user-select: none; -webkit-user-select: none; }
|
|
610
|
+
.root.dragging, .root.dragging * { cursor: grabbing !important; }
|
|
611
|
+
.toolbar, .footer, .status { cursor: grab; touch-action: none; }
|
|
612
|
+
.pill { touch-action: none; }
|
|
583
613
|
@media (prefers-color-scheme: dark) {
|
|
584
614
|
.root {
|
|
585
615
|
--theme-foreground-rgb: var(--color-white-rgb);
|
|
@@ -615,7 +645,8 @@ button:focus-visible { outline: 2px solid rgb(var(--theme-accent-rgb)); outline-
|
|
|
615
645
|
}
|
|
616
646
|
button.pill:hover { color: rgb(var(--theme-foreground-rgb)); transform: translateY(-2px); }
|
|
617
647
|
.pill > span:first-child { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
618
|
-
.mark { color: rgb(var(--theme-accent-rgb));
|
|
648
|
+
.mark { color: rgb(var(--theme-accent-rgb)); }
|
|
649
|
+
.mark svg { width: 20px; height: 20px; }
|
|
619
650
|
.pill.compact { width: auto; height: 40px; padding: 0 18px; font-size: 14px; gap: 4px; }
|
|
620
651
|
|
|
621
652
|
/* Open: a floating card, the text on top and a row of pill buttons below. */
|
|
@@ -633,7 +664,7 @@ button.pill:hover { color: rgb(var(--theme-foreground-rgb)); transform: translat
|
|
|
633
664
|
@keyframes rise { from { opacity: 0; transform: translateY(8px) scale(.98); } }
|
|
634
665
|
@keyframes spin { to { transform: rotate(1turn); } }
|
|
635
666
|
@media (prefers-reduced-motion: reduce) {
|
|
636
|
-
.panel { animation: none; }
|
|
667
|
+
.root, .panel { animation: none; transition: none; }
|
|
637
668
|
.pill { transition: none; }
|
|
638
669
|
}
|
|
639
670
|
.status {
|
|
@@ -785,6 +816,11 @@ const icon = (doc, name)=>{
|
|
|
785
816
|
span.innerHTML = `<svg viewBox="0 0 24 24">${ICONS[name]}</svg>`;
|
|
786
817
|
return span;
|
|
787
818
|
};
|
|
819
|
+
const mark = (doc)=>{
|
|
820
|
+
const span = icon(doc, 'mark');
|
|
821
|
+
span.classList.add('mark');
|
|
822
|
+
return span;
|
|
823
|
+
};
|
|
788
824
|
// Mounts the box on the page; returns a function that removes it.
|
|
789
825
|
const mountPromptBox = (doc, bridge, options = {})=>{
|
|
790
826
|
const labels = {
|
|
@@ -803,11 +839,8 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
803
839
|
const pill = element(doc, 'button', {
|
|
804
840
|
className: 'pill',
|
|
805
841
|
type: 'button'
|
|
806
|
-
}, pillText, // The Obelism mark.
|
|
807
|
-
|
|
808
|
-
className: 'mark',
|
|
809
|
-
textContent: '⁜'
|
|
810
|
-
}));
|
|
842
|
+
}, pillText, // The Obelism mark (⁜), drawn: the glyph sits low in most fonts.
|
|
843
|
+
mark(doc));
|
|
811
844
|
// Someone signed in who may only read: a note that drafts show, and a
|
|
812
845
|
// way out.
|
|
813
846
|
const viewing = element(doc, 'span', {
|
|
@@ -872,6 +905,12 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
872
905
|
className: 'action chip',
|
|
873
906
|
type: 'button'
|
|
874
907
|
}, icon(doc, 'newPage'), labels.newPage);
|
|
908
|
+
// A product the site marks (editableProduct) without a page of its own:
|
|
909
|
+
// one click makes its product page, which the page then shows.
|
|
910
|
+
const addProduct = element(doc, 'button', {
|
|
911
|
+
className: 'action chip product',
|
|
912
|
+
type: 'button'
|
|
913
|
+
}, icon(doc, 'newPage'), labels.addProductPage);
|
|
875
914
|
const cancelNew = element(doc, 'button', {
|
|
876
915
|
type: 'button',
|
|
877
916
|
title: labels.newPageCancel
|
|
@@ -921,7 +960,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
921
960
|
});
|
|
922
961
|
const toolbar = element(doc, 'div', {
|
|
923
962
|
className: 'toolbar'
|
|
924
|
-
}, scope, newScope, modes, undo, newPage, element(doc, 'span', {
|
|
963
|
+
}, scope, newScope, modes, undo, addProduct, newPage, element(doc, 'span', {
|
|
925
964
|
className: 'spacer'
|
|
926
965
|
}), minimize, close, divider, send);
|
|
927
966
|
// The pages (on the site): a chip naming this page, opening a search
|
|
@@ -1000,6 +1039,95 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1000
1039
|
shadow.append(element(doc, 'style', {
|
|
1001
1040
|
textContent: CSS
|
|
1002
1041
|
}), root);
|
|
1042
|
+
// Dragged sideways, the box moves to the left, the middle or the right
|
|
1043
|
+
// of the screen, whichever third it is let go (or flicked) in; this
|
|
1044
|
+
// browser remembers where. From the pill, or the open box's bars, not its
|
|
1045
|
+
// fields and buttons.
|
|
1046
|
+
const win = doc.defaultView;
|
|
1047
|
+
const readSide = ()=>{
|
|
1048
|
+
try {
|
|
1049
|
+
const side = win?.localStorage.getItem(SIDE_KEY);
|
|
1050
|
+
return side === 'left' || side === 'right' ? side : 'center';
|
|
1051
|
+
} catch {
|
|
1052
|
+
return 'center';
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
const placeAt = (side)=>{
|
|
1056
|
+
if (side === 'center') delete root.dataset.side;
|
|
1057
|
+
else root.dataset.side = side;
|
|
1058
|
+
};
|
|
1059
|
+
placeAt(readSide());
|
|
1060
|
+
let drag = null;
|
|
1061
|
+
// A drag that ends on the pill isn't a click on it.
|
|
1062
|
+
let dragged = false;
|
|
1063
|
+
root.addEventListener('pointerdown', (event)=>{
|
|
1064
|
+
dragged = false;
|
|
1065
|
+
if (event.button !== 0 || !event.isPrimary) return;
|
|
1066
|
+
const target = event.target;
|
|
1067
|
+
const control = target.closest('button, input, textarea, iframe, a, label, .pages, .form');
|
|
1068
|
+
if (control && control !== pill) return;
|
|
1069
|
+
const { left, right } = root.getBoundingClientRect();
|
|
1070
|
+
drag = {
|
|
1071
|
+
id: event.pointerId,
|
|
1072
|
+
startX: event.clientX,
|
|
1073
|
+
x: event.clientX,
|
|
1074
|
+
velocity: 0,
|
|
1075
|
+
time: event.timeStamp,
|
|
1076
|
+
moved: false,
|
|
1077
|
+
min: Math.min(0, EDGE_PX - left),
|
|
1078
|
+
max: Math.max(0, (win?.innerWidth ?? 0) - EDGE_PX - right)
|
|
1079
|
+
};
|
|
1080
|
+
});
|
|
1081
|
+
root.addEventListener('pointermove', (event)=>{
|
|
1082
|
+
if (!drag || event.pointerId !== drag.id) return;
|
|
1083
|
+
const dx = event.clientX - drag.startX;
|
|
1084
|
+
if (!drag.moved) {
|
|
1085
|
+
if (Math.abs(dx) < DRAG_THRESHOLD_PX) return;
|
|
1086
|
+
drag.moved = true;
|
|
1087
|
+
root.classList.add('dragging');
|
|
1088
|
+
try {
|
|
1089
|
+
root.setPointerCapture(event.pointerId);
|
|
1090
|
+
} catch {}
|
|
1091
|
+
}
|
|
1092
|
+
const elapsed = event.timeStamp - drag.time;
|
|
1093
|
+
if (elapsed > 0) drag.velocity = (event.clientX - drag.x) / elapsed;
|
|
1094
|
+
drag.x = event.clientX;
|
|
1095
|
+
drag.time = event.timeStamp;
|
|
1096
|
+
// Past the edge, a third as far.
|
|
1097
|
+
const clamped = Math.min(Math.max(dx, drag.min), drag.max);
|
|
1098
|
+
root.style.translate = `${clamped + (dx - clamped) / 3}px 0`;
|
|
1099
|
+
});
|
|
1100
|
+
const drop = (event)=>{
|
|
1101
|
+
if (!drag || event.pointerId !== drag.id) return;
|
|
1102
|
+
const { moved, x, velocity, time } = drag;
|
|
1103
|
+
drag = null;
|
|
1104
|
+
if (!moved) return;
|
|
1105
|
+
dragged = true;
|
|
1106
|
+
// Springs from where it was let go to its new place.
|
|
1107
|
+
const before = root.getBoundingClientRect().left;
|
|
1108
|
+
const width = win?.innerWidth ?? 0;
|
|
1109
|
+
const flick = event.timeStamp - time > FLICK_STALE_MS ? 0 : Math.min(Math.max(velocity * FLICK_MS, -FLICK_MAX_PX), FLICK_MAX_PX);
|
|
1110
|
+
const projected = x + flick;
|
|
1111
|
+
const side = projected < width / 3 ? 'left' : projected > width * 2 / 3 ? 'right' : 'center';
|
|
1112
|
+
placeAt(side);
|
|
1113
|
+
root.style.translate = '';
|
|
1114
|
+
const after = root.getBoundingClientRect().left;
|
|
1115
|
+
root.style.translate = `${before - after}px 0`;
|
|
1116
|
+
void root.offsetWidth;
|
|
1117
|
+
root.classList.remove('dragging');
|
|
1118
|
+
root.style.translate = '';
|
|
1119
|
+
try {
|
|
1120
|
+
win?.localStorage.setItem(SIDE_KEY, side);
|
|
1121
|
+
} catch {}
|
|
1122
|
+
};
|
|
1123
|
+
root.addEventListener('pointerup', drop);
|
|
1124
|
+
root.addEventListener('pointercancel', drop);
|
|
1125
|
+
root.addEventListener('click', (event)=>{
|
|
1126
|
+
if (!dragged) return;
|
|
1127
|
+
dragged = false;
|
|
1128
|
+
event.preventDefault();
|
|
1129
|
+
event.stopPropagation();
|
|
1130
|
+
}, true);
|
|
1003
1131
|
let open = false;
|
|
1004
1132
|
let minimized = false;
|
|
1005
1133
|
let creating = false;
|
|
@@ -1032,7 +1160,9 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1032
1160
|
// A page of the site that isn't an entry (no `editable(entry)`, like a
|
|
1033
1161
|
// hand-written /about) has nothing to edit: only New page.
|
|
1034
1162
|
const pageless = site && bridge.entryId === null;
|
|
1035
|
-
|
|
1163
|
+
// …unless it shows a product: then Add a product page first.
|
|
1164
|
+
const productHere = pageless && canCreate && bridge.product !== null;
|
|
1165
|
+
const onlyNew = pageless && canCreate && !productHere;
|
|
1036
1166
|
if (!canCreate) creating = false;
|
|
1037
1167
|
if (onlyNew) creating = true;
|
|
1038
1168
|
if (creating || pageless || !site) form = false;
|
|
@@ -1040,16 +1170,19 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1040
1170
|
const showForm = open && available && form;
|
|
1041
1171
|
if (showForm && !formHandle) mountForm();
|
|
1042
1172
|
else if (!showForm && formHandle) unmountForm();
|
|
1043
|
-
|
|
1173
|
+
const offerProduct = productHere && !creating;
|
|
1174
|
+
pillText.textContent = offerProduct ? labels.addProductPage : onlyNew ? labels.newPage : labels.open;
|
|
1044
1175
|
cancelNew.hidden = onlyNew;
|
|
1045
|
-
scope.hidden = creating;
|
|
1176
|
+
scope.hidden = creating || offerProduct;
|
|
1046
1177
|
newScope.hidden = !creating;
|
|
1178
|
+
addProduct.hidden = !offerProduct;
|
|
1179
|
+
addProduct.disabled = busy;
|
|
1047
1180
|
newPage.hidden = creating || form || !canCreate;
|
|
1048
1181
|
newPage.disabled = busy;
|
|
1049
1182
|
if (minimized) picking = false;
|
|
1050
1183
|
pathRow.hidden = !creating || picking || minimized;
|
|
1051
1184
|
pathInput.disabled = busy;
|
|
1052
|
-
input.hidden = form || picking || minimized;
|
|
1185
|
+
input.hidden = form || picking || minimized || offerProduct;
|
|
1053
1186
|
formBox.hidden = !form || picking || minimized;
|
|
1054
1187
|
formBox.classList.toggle('loading', formLoading);
|
|
1055
1188
|
toolbar.hidden = picking;
|
|
@@ -1069,8 +1202,8 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1069
1202
|
undo.hidden = creating || !state.canUndo;
|
|
1070
1203
|
undo.disabled = busy;
|
|
1071
1204
|
// The form applies edits as they are made.
|
|
1072
|
-
send.hidden = form || minimized;
|
|
1073
|
-
divider.hidden = form || minimized;
|
|
1205
|
+
send.hidden = form || minimized || offerProduct;
|
|
1206
|
+
divider.hidden = form || minimized || offerProduct;
|
|
1074
1207
|
const toggleLabel = minimized ? labels.expand : labels.minimize;
|
|
1075
1208
|
minimize.title = toggleLabel;
|
|
1076
1209
|
minimize.setAttribute('aria-label', toggleLabel);
|
|
@@ -1093,7 +1226,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1093
1226
|
pageChipText.textContent = pages?.find(({ entryId })=>entryId === bridge.entryId)?.name ?? labels.pages;
|
|
1094
1227
|
const loadingForm = form && formLoading && !picking;
|
|
1095
1228
|
const working = state.prompting || loadingForm;
|
|
1096
|
-
const note = state.prompting ? creating ? labels.creating : labels.working : loadingForm ? labels.formLoading : message?.text ?? (site && !creating && !picking && !state.selectedUid ? labels.selectHint : '');
|
|
1229
|
+
const note = state.prompting ? creating ? labels.creating : offerProduct ? labels.addingProductPage : labels.working : loadingForm ? labels.formLoading : message?.text ?? (offerProduct ? labels.productPageHint : site && !creating && !picking && !state.selectedUid ? labels.selectHint : '');
|
|
1097
1230
|
status.textContent = note;
|
|
1098
1231
|
status.hidden = !note || minimized;
|
|
1099
1232
|
status.className = `status${working ? ' working' : ''}${!busy && !loadingForm && message?.error ? ' error' : ''}`;
|
|
@@ -1297,12 +1430,35 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1297
1430
|
refresh();
|
|
1298
1431
|
if (!minimized) (form ? minimize : input).focus();
|
|
1299
1432
|
});
|
|
1433
|
+
// Any other button of the minimized box expands it first (capture, so
|
|
1434
|
+
// its own handler then sees the box open), e.g. the pages or New page.
|
|
1435
|
+
panel.addEventListener('click', (event)=>{
|
|
1436
|
+
if (!minimized) return;
|
|
1437
|
+
const button = event.target.closest('button');
|
|
1438
|
+
if (!button || button === minimize || button === close) return;
|
|
1439
|
+
minimized = false;
|
|
1440
|
+
refresh();
|
|
1441
|
+
}, true);
|
|
1300
1442
|
save.addEventListener('click', ()=>void store('save'));
|
|
1301
1443
|
publish.addEventListener('click', ()=>void store('publish'));
|
|
1302
1444
|
signOut.addEventListener('click', ()=>bridge.signOut());
|
|
1303
1445
|
pillSignOut.addEventListener('click', ()=>bridge.signOut());
|
|
1304
1446
|
clearBlock.addEventListener('click', ()=>bridge.select(null));
|
|
1305
1447
|
newPage.addEventListener('click', ()=>setCreating(true));
|
|
1448
|
+
addProduct.addEventListener('click', async ()=>{
|
|
1449
|
+
message = null;
|
|
1450
|
+
try {
|
|
1451
|
+
// The bridge reloads the page; this shows until it does.
|
|
1452
|
+
await bridge.createProductPage();
|
|
1453
|
+
message = {
|
|
1454
|
+
text: labels.addedProductPage,
|
|
1455
|
+
error: false
|
|
1456
|
+
};
|
|
1457
|
+
} catch (error) {
|
|
1458
|
+
message = failure(error);
|
|
1459
|
+
}
|
|
1460
|
+
refresh();
|
|
1461
|
+
});
|
|
1306
1462
|
cancelNew.addEventListener('click', ()=>setCreating(false));
|
|
1307
1463
|
promptMode.addEventListener('click', ()=>setForm(false));
|
|
1308
1464
|
formMode.addEventListener('click', ()=>setForm(true));
|
|
@@ -1374,13 +1530,15 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1374
1530
|
// Messages are only exchanged with the URL's origin, and only with the
|
|
1375
1531
|
// iframe's window:
|
|
1376
1532
|
// - form → bridge: { source: 'obelism-content-form', type: 'ready' |
|
|
1377
|
-
// 'change' | 'error' | 'height', content?, message?,
|
|
1378
|
-
// `
|
|
1379
|
-
//
|
|
1533
|
+
// 'loaded' | 'change' | 'error' | 'height', loading?, content?, message?,
|
|
1534
|
+
// height? } — `ready` with `loading: true` when the form still loads the
|
|
1535
|
+
// entry after it and says `loaded` once it shows (a CMS from before says
|
|
1536
|
+
// only `ready`), `change` carries the whole entry content after an edit,
|
|
1537
|
+
// `height` the form's own height in px, which the iframe takes (up to 60vh)
|
|
1380
1538
|
// - bridge → form: { source: 'obelism-content-bridge', type: 'content',
|
|
1381
1539
|
// entryId, content, locale, selectedUid }
|
|
1382
1540
|
const FORM_SOURCE = 'obelism-content-form';
|
|
1383
|
-
// How long the form may take to
|
|
1541
|
+
// How long the form may take to show.
|
|
1384
1542
|
const FORM_READY_TIMEOUT_MS = 30_000;
|
|
1385
1543
|
const isObject = (value)=>!!value && typeof value === 'object' && !Array.isArray(value);
|
|
1386
1544
|
// Mounts the form at `url` in `container`. The URL is single use: mount a
|
|
@@ -1398,6 +1556,7 @@ const mountFormFrame = (win, container, url, title, handlers)=>{
|
|
|
1398
1556
|
iframe.title = title;
|
|
1399
1557
|
iframe.src = url;
|
|
1400
1558
|
let ready = false;
|
|
1559
|
+
let shown = false;
|
|
1401
1560
|
let latest = null;
|
|
1402
1561
|
const post = ()=>{
|
|
1403
1562
|
if (!ready || !latest) return;
|
|
@@ -1408,8 +1567,13 @@ const mountFormFrame = (win, container, url, title, handlers)=>{
|
|
|
1408
1567
|
}, origin);
|
|
1409
1568
|
};
|
|
1410
1569
|
const timer = setTimeout(()=>{
|
|
1411
|
-
if (!
|
|
1570
|
+
if (!shown) handlers.onError('The form did not open');
|
|
1412
1571
|
}, FORM_READY_TIMEOUT_MS);
|
|
1572
|
+
const show = ()=>{
|
|
1573
|
+
shown = true;
|
|
1574
|
+
clearTimeout(timer);
|
|
1575
|
+
handlers.onReady();
|
|
1576
|
+
};
|
|
1413
1577
|
const onMessage = (event)=>{
|
|
1414
1578
|
if (event.origin !== origin || !iframe.contentWindow || event.source !== iframe.contentWindow) {
|
|
1415
1579
|
return;
|
|
@@ -1419,9 +1583,12 @@ const mountFormFrame = (win, container, url, title, handlers)=>{
|
|
|
1419
1583
|
if (data.type === 'ready') {
|
|
1420
1584
|
// Again after the form reloads itself: it gets the content again.
|
|
1421
1585
|
ready = true;
|
|
1422
|
-
clearTimeout(timer);
|
|
1423
|
-
handlers.onReady();
|
|
1424
1586
|
post();
|
|
1587
|
+
// Until the form loaded the entry, the box's own loading state
|
|
1588
|
+
// stands in for the form's.
|
|
1589
|
+
if (data.loading !== true) show();
|
|
1590
|
+
} else if (data.type === 'loaded') {
|
|
1591
|
+
if (ready) show();
|
|
1425
1592
|
} else if (data.type === 'change' && isObject(data.content)) {
|
|
1426
1593
|
handlers.onChange(data.content, typeof data.entryId === 'string' ? data.entryId : undefined);
|
|
1427
1594
|
} else if (data.type === 'height') {
|
|
@@ -1483,7 +1650,7 @@ const FORM_BURST_MS = 1000;
|
|
|
1483
1650
|
// How long the page waits for the form's edits to settle before it shows
|
|
1484
1651
|
// them.
|
|
1485
1652
|
const FORM_RENDER_MS = 300;
|
|
1486
|
-
const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}
|
|
1653
|
+
const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}`, productPath: ownProductPath } = {})=>{
|
|
1487
1654
|
const doc = win.document;
|
|
1488
1655
|
const emitter = createEmitter();
|
|
1489
1656
|
const cleanup = [];
|
|
@@ -1493,6 +1660,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}` } = {})
|
|
|
1493
1660
|
// Entries changed here since the last publish.
|
|
1494
1661
|
const touched = new Set();
|
|
1495
1662
|
let session = null;
|
|
1663
|
+
const productPath = (handle, locale)=>ownProductPath ? ownProductPath(handle, locale) : (session?.productPath ?? '/products/{handle}').replaceAll('{handle}', encodeURIComponent(handle));
|
|
1496
1664
|
// The open form's, if any; and when the form last changed content, for
|
|
1497
1665
|
// undo steps (-Infinity: the next edit starts a new step).
|
|
1498
1666
|
let closeForm = null;
|
|
@@ -1537,6 +1705,16 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}` } = {})
|
|
|
1537
1705
|
return data;
|
|
1538
1706
|
};
|
|
1539
1707
|
const pageEntryId = ()=>doc.querySelector(`[${preview.ENTRY_ATTRIBUTE}]`)?.getAttribute(preview.ENTRY_ATTRIBUTE) ?? null;
|
|
1708
|
+
// The product the page shows (editableProduct), if it marks one.
|
|
1709
|
+
const pageProduct = ()=>{
|
|
1710
|
+
const element = doc.querySelector(`[${preview.PRODUCT_ATTRIBUTE}]`);
|
|
1711
|
+
const id = element?.getAttribute(preview.PRODUCT_ATTRIBUTE);
|
|
1712
|
+
const handle = element?.getAttribute(preview.PRODUCT_HANDLE_ATTRIBUTE);
|
|
1713
|
+
return id && handle ? {
|
|
1714
|
+
id,
|
|
1715
|
+
handle
|
|
1716
|
+
} : null;
|
|
1717
|
+
};
|
|
1540
1718
|
const entryFor = (uid)=>{
|
|
1541
1719
|
const block = uid ? doc.querySelector(blockSelector(uid)) : null;
|
|
1542
1720
|
return block?.closest(`[${preview.ENTRY_ATTRIBUTE}]`)?.getAttribute(preview.ENTRY_ATTRIBUTE) ?? pageEntryId();
|
|
@@ -1644,6 +1822,9 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}` } = {})
|
|
|
1644
1822
|
get entryId () {
|
|
1645
1823
|
return pageEntryId();
|
|
1646
1824
|
},
|
|
1825
|
+
get product () {
|
|
1826
|
+
return pageProduct();
|
|
1827
|
+
},
|
|
1647
1828
|
environment: null,
|
|
1648
1829
|
on: emitter.on,
|
|
1649
1830
|
getState: ()=>state,
|
|
@@ -1713,6 +1894,34 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}` } = {})
|
|
|
1713
1894
|
});
|
|
1714
1895
|
}
|
|
1715
1896
|
},
|
|
1897
|
+
async createProductPage (product = pageProduct() ?? undefined) {
|
|
1898
|
+
if (!product) {
|
|
1899
|
+
throw new Error('Nothing on this page is marked as a product: add editableProduct(product)');
|
|
1900
|
+
}
|
|
1901
|
+
if (!has('create')) throw new Error('You may not add pages here');
|
|
1902
|
+
if (state.unsaved) {
|
|
1903
|
+
throw new Error('Save or undo your changes before adding a page');
|
|
1904
|
+
}
|
|
1905
|
+
setState({
|
|
1906
|
+
prompting: true
|
|
1907
|
+
});
|
|
1908
|
+
try {
|
|
1909
|
+
const made = await api('product-pages', {
|
|
1910
|
+
product: {
|
|
1911
|
+
id: product.id,
|
|
1912
|
+
handle: product.handle
|
|
1913
|
+
}
|
|
1914
|
+
});
|
|
1915
|
+
// The site's product route finds its page now (drafts, while
|
|
1916
|
+
// editing): show it.
|
|
1917
|
+
win.location.reload();
|
|
1918
|
+
return made;
|
|
1919
|
+
} finally{
|
|
1920
|
+
setState({
|
|
1921
|
+
prompting: false
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1924
|
+
},
|
|
1716
1925
|
async view (options = {}) {
|
|
1717
1926
|
const { uid, entryId } = target(options.uid);
|
|
1718
1927
|
const entry = await load(entryId);
|
|
@@ -1866,7 +2075,8 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}` } = {})
|
|
|
1866
2075
|
contentType: page.contentType,
|
|
1867
2076
|
status: page.status,
|
|
1868
2077
|
path,
|
|
1869
|
-
url: pagePath(path, code)
|
|
2078
|
+
url: page.product ? productPath(page.product.handle, code) : pagePath(path, code),
|
|
2079
|
+
product: page.product ?? null
|
|
1870
2080
|
}
|
|
1871
2081
|
];
|
|
1872
2082
|
});
|
|
@@ -2011,7 +2221,7 @@ const peekContentBridge = ()=>typeof window === 'undefined' ? null : window[KEY]
|
|
|
2011
2221
|
// if (bridge.isEditing) bridge.on('update', ({ entry }) => render(entry))
|
|
2012
2222
|
//
|
|
2013
2223
|
// The first call's options win: there is one bridge per page.
|
|
2014
|
-
const getContentBridge = ({ url, prompt = true, pagePath } = {})=>{
|
|
2224
|
+
const getContentBridge = ({ url, prompt = true, pagePath, productPath } = {})=>{
|
|
2015
2225
|
if (typeof window === 'undefined') return SERVER_BRIDGE;
|
|
2016
2226
|
const win = window;
|
|
2017
2227
|
if (win[KEY]) return win[KEY];
|
|
@@ -2024,7 +2234,8 @@ const getContentBridge = ({ url, prompt = true, pagePath } = {})=>{
|
|
|
2024
2234
|
// own `/cms/login` (@obelism/content/site).
|
|
2025
2235
|
const sitePath = bridge.isEditing || win.parent !== win ? null : readSiteEditing(win.document);
|
|
2026
2236
|
if (sitePath) bridge = createSiteBridge(win, sitePath, {
|
|
2027
|
-
pagePath
|
|
2237
|
+
pagePath,
|
|
2238
|
+
productPath
|
|
2028
2239
|
});
|
|
2029
2240
|
if (bridge.isEditing && prompt) {
|
|
2030
2241
|
const unmount = mountPromptBox(win.document, bridge, prompt === true ? {} : prompt);
|