@obelism/content 0.28.2 → 0.28.4
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 +7 -4
- package/dist/bridge.d.mts +3 -2
- package/dist/bridge.d.mts.map +1 -1
- package/dist/bridge.d.ts +3 -2
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.global.js +4 -4
- package/dist/bridge.js +448 -373
- package/dist/bridge.mjs +449 -374
- package/dist/index.mjs +1 -1
- package/dist/{preview-el7sfk3h.mjs → preview-9qff497d.mjs} +1 -1
- package/dist/site-j5kqgtj3.js +368 -0
- package/dist/site-m85jdjid.mjs +360 -0
- package/dist/site.js +10 -340
- package/dist/site.mjs +2 -336
- package/package.json +5 -5
package/dist/bridge.js
CHANGED
|
@@ -1,9 +1,171 @@
|
|
|
1
1
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
2
2
|
|
|
3
3
|
var preview = require('./preview-c4giufnr.js');
|
|
4
|
-
var site = require('./site.js');
|
|
4
|
+
var site = require('./site-j5kqgtj3.js');
|
|
5
5
|
var config = require('./config-ci2hxmas.js');
|
|
6
6
|
|
|
7
|
+
// What every bridge to the visual editor offers, and the pieces both of them
|
|
8
|
+
// are built from: the state and its emitter, block selection, the click-rate
|
|
9
|
+
// labels. The editor's own bridge is editorBridge.ts, editing on the site is
|
|
10
|
+
// siteBridge.ts, and the CMS's old script tag is scriptBridge.ts.
|
|
11
|
+
// Messages are only exchanged with the CMS origin, and only with the parent
|
|
12
|
+
// window:
|
|
13
|
+
// - site → editor: { source: 'obelism-content-bridge', type: 'ready' |
|
|
14
|
+
// 'size' | 'select' | 'prompt' | 'undo', uid?, id?, prompt?, height? }
|
|
15
|
+
// - editor → site: { source: 'obelism-content-editor', type: 'connect' |
|
|
16
|
+
// 'update' | 'select' | 'saved' | 'published' | 'features' |
|
|
17
|
+
// 'promptResult' | 'promptState' | 'stats' | 'viewport', … }
|
|
18
|
+
const BRIDGE_SOURCE = 'obelism-content-bridge';
|
|
19
|
+
const EDITOR_SOURCE = 'obelism-content-editor';
|
|
20
|
+
// How long a prompt may take before it counts as failed.
|
|
21
|
+
const PROMPT_TIMEOUT_MS = 120_000;
|
|
22
|
+
const INITIAL_STATE = {
|
|
23
|
+
features: [],
|
|
24
|
+
selectedUid: null,
|
|
25
|
+
prompting: false,
|
|
26
|
+
canUndo: false,
|
|
27
|
+
unsaved: false,
|
|
28
|
+
saving: false,
|
|
29
|
+
publishing: false,
|
|
30
|
+
selecting: false,
|
|
31
|
+
stats: {},
|
|
32
|
+
statsVisible: false
|
|
33
|
+
};
|
|
34
|
+
const OUTLINE = '#d58a1f';
|
|
35
|
+
// Outlines on blocks; `scope` limits them to e.g. `html[data-cms-selecting] `.
|
|
36
|
+
const selectionStyle = (scope = '')=>`${scope}[${preview.UID_ATTRIBUTE}]{cursor:pointer;transition:outline-color .15s}` + `${scope}[${preview.UID_ATTRIBUTE}]:hover{outline:2px dashed ${OUTLINE};outline-offset:2px}` + `${scope}[${preview.UID_ATTRIBUTE}][data-cms-selected]{outline:2px solid ${OUTLINE};outline-offset:2px}`;
|
|
37
|
+
// The selector of the element marked with a block's uid.
|
|
38
|
+
const blockSelector = (uid)=>`[${preview.UID_ATTRIBUTE}="${uid.replace(/["\\]/g, '\\$&')}"]`;
|
|
39
|
+
const createEmitter = ()=>{
|
|
40
|
+
const listeners = {
|
|
41
|
+
update: new Set(),
|
|
42
|
+
saved: new Set(),
|
|
43
|
+
published: new Set(),
|
|
44
|
+
change: new Set()
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
listeners,
|
|
48
|
+
on (event, listener) {
|
|
49
|
+
listeners[event].add(listener);
|
|
50
|
+
return ()=>{
|
|
51
|
+
listeners[event].delete(listener);
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
emit (event, payload) {
|
|
55
|
+
for (const listener of listeners[event]){
|
|
56
|
+
try {
|
|
57
|
+
listener(payload);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error('[@obelism/content bridge]', error);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
// The state both bridges keep, and the emitter that says it changed.
|
|
66
|
+
const createBridgeState = (initial = INITIAL_STATE)=>{
|
|
67
|
+
const emitter = createEmitter();
|
|
68
|
+
let state = initial;
|
|
69
|
+
return {
|
|
70
|
+
...emitter,
|
|
71
|
+
getState: ()=>state,
|
|
72
|
+
setState (next) {
|
|
73
|
+
state = {
|
|
74
|
+
...state,
|
|
75
|
+
...next
|
|
76
|
+
};
|
|
77
|
+
emitter.emit('change', state);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
// Outlines the block's element on the page, and nothing else, in view when
|
|
82
|
+
// `scroll`.
|
|
83
|
+
const showSelection = (doc, uid, scroll = false)=>{
|
|
84
|
+
for (const element of doc.querySelectorAll('[data-cms-selected]')){
|
|
85
|
+
element.removeAttribute('data-cms-selected');
|
|
86
|
+
}
|
|
87
|
+
const element = uid ? doc.querySelector(blockSelector(uid)) : null;
|
|
88
|
+
if (!element) return;
|
|
89
|
+
element.setAttribute('data-cms-selected', '');
|
|
90
|
+
if (scroll) element.scrollIntoView({
|
|
91
|
+
behavior: 'smooth',
|
|
92
|
+
block: 'center'
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
// The click rates on the page: mounted with the first rates, so a page
|
|
96
|
+
// without Numbers never gets it, then kept.
|
|
97
|
+
const createStatsView = (mount)=>{
|
|
98
|
+
let overlay = null;
|
|
99
|
+
return {
|
|
100
|
+
update (stats, visible) {
|
|
101
|
+
if (!overlay && Object.keys(stats).length === 0) return;
|
|
102
|
+
overlay ??= mount();
|
|
103
|
+
overlay.update(stats, visible);
|
|
104
|
+
},
|
|
105
|
+
refresh: ()=>overlay?.refresh(),
|
|
106
|
+
destroy: ()=>overlay?.destroy()
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
// The rates the page can show, from the editor's `stats` message: only
|
|
110
|
+
// well-formed entries.
|
|
111
|
+
const isCount = (value)=>typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
112
|
+
const readStats = (value)=>{
|
|
113
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
114
|
+
const stats = {};
|
|
115
|
+
for (const [uid, block] of Object.entries(value)){
|
|
116
|
+
const { clicks, clickers, viewers, rate } = block ?? {};
|
|
117
|
+
if (isCount(clicks) && isCount(clickers) && isCount(viewers) && isCount(rate)) stats[uid] = {
|
|
118
|
+
clicks,
|
|
119
|
+
clickers,
|
|
120
|
+
viewers,
|
|
121
|
+
rate
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return stats;
|
|
125
|
+
};
|
|
126
|
+
const inertPrompt = ()=>Promise.reject(new Error('Prompts only work inside the visual editor'));
|
|
127
|
+
const onlyOnSite = ()=>Promise.reject(new Error('Save and publish in the visual editor itself'));
|
|
128
|
+
const onlyWhileEditingSite = ()=>Promise.reject(new Error('Only while signed in to edit on the site'));
|
|
129
|
+
// What every bridge without a feature does for it.
|
|
130
|
+
const INERT_METHODS = {
|
|
131
|
+
prompt: inertPrompt,
|
|
132
|
+
createPage: ()=>Promise.reject(new Error('New pages are made on the site itself')),
|
|
133
|
+
createProductPage: ()=>Promise.reject(new Error('Product pages are made on the site itself')),
|
|
134
|
+
createCollectionPage: ()=>Promise.reject(new Error('Collection pages are made on the site itself')),
|
|
135
|
+
view: onlyWhileEditingSite,
|
|
136
|
+
applyJson: onlyWhileEditingSite,
|
|
137
|
+
openForm: (_container, options = {})=>{
|
|
138
|
+
queueMicrotask(()=>options.onError?.('Only while signed in to edit on the site'));
|
|
139
|
+
return {
|
|
140
|
+
close: ()=>{}
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
pages: onlyWhileEditingSite,
|
|
144
|
+
openPage: ()=>{},
|
|
145
|
+
undo: ()=>{},
|
|
146
|
+
save: onlyOnSite,
|
|
147
|
+
publish: onlyOnSite,
|
|
148
|
+
signOut: ()=>{},
|
|
149
|
+
select: ()=>{},
|
|
150
|
+
setSelecting: ()=>{}
|
|
151
|
+
};
|
|
152
|
+
// A bridge for a page where nothing can be edited.
|
|
153
|
+
const createInertBridge = ()=>{
|
|
154
|
+
const emitter = createEmitter();
|
|
155
|
+
return {
|
|
156
|
+
isEditing: false,
|
|
157
|
+
mode: null,
|
|
158
|
+
entryId: null,
|
|
159
|
+
environment: null,
|
|
160
|
+
product: null,
|
|
161
|
+
collection: null,
|
|
162
|
+
on: emitter.on,
|
|
163
|
+
getState: ()=>INITIAL_STATE,
|
|
164
|
+
...INERT_METHODS,
|
|
165
|
+
destroy: ()=>{}
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
|
|
7
169
|
const DEFAULT_STATS_LABELS = {
|
|
8
170
|
clicked: '{rate}% clicked',
|
|
9
171
|
clickedShort: '{rate}%',
|
|
@@ -79,7 +241,7 @@ const placeTag = (rect, viewport)=>({
|
|
|
79
241
|
top: Math.min(Math.max(rect.top, 10), viewport.height - 10),
|
|
80
242
|
left: Math.min(rect.right, viewport.width - 10)
|
|
81
243
|
});
|
|
82
|
-
const mountStatsOverlay = (win, { selector, onSelect, checked, labels: labelOverrides })=>{
|
|
244
|
+
const mountStatsOverlay = (win, { selector, onSelect, checked, shows, labels: labelOverrides })=>{
|
|
83
245
|
const doc = win.document;
|
|
84
246
|
const labels = {
|
|
85
247
|
...DEFAULT_STATS_LABELS,
|
|
@@ -157,13 +319,14 @@ const mountStatsOverlay = (win, { selector, onSelect, checked, labels: labelOver
|
|
|
157
319
|
const id = element.getAttribute(TAG_ATTRIBUTE);
|
|
158
320
|
if (!id || !stats[id] && !isChecked(id)) continue;
|
|
159
321
|
tagged.add(id);
|
|
322
|
+
if (shows && !shows(id, element)) continue;
|
|
160
323
|
draw(id, element, blocks);
|
|
161
324
|
}
|
|
162
325
|
// Rates of blocks the site doesn't tag an element of.
|
|
163
326
|
for (const id of Object.keys(stats)){
|
|
164
327
|
if (tagged.has(id) || !blocks.has(id)) continue;
|
|
165
328
|
const element = doc.querySelector(selector(id));
|
|
166
|
-
if (element) draw(id, element, blocks);
|
|
329
|
+
if (element && (!shows || shows(id, element))) draw(id, element, blocks);
|
|
167
330
|
}
|
|
168
331
|
};
|
|
169
332
|
// At most once a frame, however many scroll and mutation events come in.
|
|
@@ -297,119 +460,15 @@ const createViewportUnits = (doc)=>{
|
|
|
297
460
|
// which one was clicked, hands the editor's live, unsaved entry to listeners
|
|
298
461
|
// and sends the editor prompts. Outside the editor it does nothing.
|
|
299
462
|
//
|
|
300
|
-
// Messages are only exchanged with the CMS origin, and only with the parent
|
|
301
|
-
// window:
|
|
302
|
-
// - site → editor: { source: 'obelism-content-bridge', type: 'ready' |
|
|
303
|
-
// 'size' | 'select' | 'prompt' | 'undo', uid?, id?, prompt?, height? }
|
|
304
|
-
// - editor → site: { source: 'obelism-content-editor', type: 'connect' |
|
|
305
|
-
// 'update' | 'select' | 'saved' | 'published' | 'features' |
|
|
306
|
-
// 'promptResult' | 'promptState' | 'stats' | 'viewport', … }
|
|
307
|
-
const BRIDGE_SOURCE = 'obelism-content-bridge';
|
|
308
|
-
const EDITOR_SOURCE = 'obelism-content-editor';
|
|
309
|
-
// How long a prompt may take before it counts as failed.
|
|
310
|
-
const PROMPT_TIMEOUT_MS = 120_000;
|
|
311
|
-
const OUTLINE = '#d58a1f';
|
|
312
|
-
// Outlines on blocks; `scope` limits them to e.g. `html[data-cms-selecting] `.
|
|
313
|
-
const selectionStyle = (scope = '')=>`${scope}[${preview.UID_ATTRIBUTE}]{cursor:pointer;transition:outline-color .15s}` + `${scope}[${preview.UID_ATTRIBUTE}]:hover{outline:2px dashed ${OUTLINE};outline-offset:2px}` + `${scope}[${preview.UID_ATTRIBUTE}][data-cms-selected]{outline:2px solid ${OUTLINE};outline-offset:2px}`;
|
|
314
|
-
const INITIAL_STATE = {
|
|
315
|
-
features: [],
|
|
316
|
-
selectedUid: null,
|
|
317
|
-
prompting: false,
|
|
318
|
-
canUndo: false,
|
|
319
|
-
unsaved: false,
|
|
320
|
-
saving: false,
|
|
321
|
-
publishing: false,
|
|
322
|
-
selecting: false,
|
|
323
|
-
stats: {},
|
|
324
|
-
statsVisible: false
|
|
325
|
-
};
|
|
326
|
-
const createEmitter = ()=>{
|
|
327
|
-
const listeners = {
|
|
328
|
-
update: new Set(),
|
|
329
|
-
saved: new Set(),
|
|
330
|
-
published: new Set(),
|
|
331
|
-
change: new Set()
|
|
332
|
-
};
|
|
333
|
-
return {
|
|
334
|
-
listeners,
|
|
335
|
-
on (event, listener) {
|
|
336
|
-
listeners[event].add(listener);
|
|
337
|
-
return ()=>{
|
|
338
|
-
listeners[event].delete(listener);
|
|
339
|
-
};
|
|
340
|
-
},
|
|
341
|
-
emit (event, payload) {
|
|
342
|
-
for (const listener of listeners[event]){
|
|
343
|
-
try {
|
|
344
|
-
listener(payload);
|
|
345
|
-
} catch (error) {
|
|
346
|
-
console.error('[@obelism/content bridge]', error);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
};
|
|
351
|
-
};
|
|
352
|
-
const inertPrompt = ()=>Promise.reject(new Error('Prompts only work inside the visual editor'));
|
|
353
|
-
const onlyOnSite = ()=>Promise.reject(new Error('Save and publish in the visual editor itself'));
|
|
354
|
-
const onlyWhileEditingSite = ()=>Promise.reject(new Error('Only while signed in to edit on the site'));
|
|
355
|
-
// What every bridge without a feature does for it.
|
|
356
|
-
const INERT_METHODS = {
|
|
357
|
-
prompt: inertPrompt,
|
|
358
|
-
createPage: ()=>Promise.reject(new Error('New pages are made on the site itself')),
|
|
359
|
-
createProductPage: ()=>Promise.reject(new Error('Product pages are made on the site itself')),
|
|
360
|
-
createCollectionPage: ()=>Promise.reject(new Error('Collection pages are made on the site itself')),
|
|
361
|
-
view: onlyWhileEditingSite,
|
|
362
|
-
applyJson: onlyWhileEditingSite,
|
|
363
|
-
openForm: (_container, options = {})=>{
|
|
364
|
-
queueMicrotask(()=>options.onError?.('Only while signed in to edit on the site'));
|
|
365
|
-
return {
|
|
366
|
-
close: ()=>{}
|
|
367
|
-
};
|
|
368
|
-
},
|
|
369
|
-
pages: onlyWhileEditingSite,
|
|
370
|
-
openPage: ()=>{},
|
|
371
|
-
undo: ()=>{},
|
|
372
|
-
save: onlyOnSite,
|
|
373
|
-
publish: onlyOnSite,
|
|
374
|
-
signOut: ()=>{},
|
|
375
|
-
select: ()=>{},
|
|
376
|
-
setSelecting: ()=>{}
|
|
377
|
-
};
|
|
378
|
-
const isCount = (value)=>typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
379
|
-
// The editor's `stats`, keeping only well-formed entries.
|
|
380
|
-
const readStats = (value)=>{
|
|
381
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
382
|
-
const stats = {};
|
|
383
|
-
for (const [uid, block] of Object.entries(value)){
|
|
384
|
-
const { clicks, clickers, viewers, rate } = block ?? {};
|
|
385
|
-
if (isCount(clicks) && isCount(clickers) && isCount(viewers) && isCount(rate)) stats[uid] = {
|
|
386
|
-
clicks,
|
|
387
|
-
clickers,
|
|
388
|
-
viewers,
|
|
389
|
-
rate
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
return stats;
|
|
393
|
-
};
|
|
394
|
-
// The selector of the element marked with a block's uid.
|
|
395
|
-
const blockSelector = (uid)=>`[${preview.UID_ATTRIBUTE}="${uid.replace(/["\\]/g, '\\$&')}"]`;
|
|
396
463
|
// `cmsOrigin` is the only origin messages are accepted from and sent to.
|
|
397
464
|
const createBridge = (win, cmsOrigin)=>{
|
|
398
465
|
const params = new URLSearchParams(win.location.search);
|
|
399
466
|
const isEditing = win.parent !== win && params.has(preview.PREVIEW_PARAMS.preview);
|
|
400
|
-
const
|
|
401
|
-
const cleanup = [];
|
|
402
|
-
let state = {
|
|
467
|
+
const { on, getState, setState, emit, listeners } = createBridgeState({
|
|
403
468
|
...INITIAL_STATE,
|
|
404
469
|
selecting: true
|
|
405
|
-
};
|
|
406
|
-
const
|
|
407
|
-
state = {
|
|
408
|
-
...state,
|
|
409
|
-
...next
|
|
410
|
-
};
|
|
411
|
-
emitter.emit('change', state);
|
|
412
|
-
};
|
|
470
|
+
});
|
|
471
|
+
const cleanup = [];
|
|
413
472
|
const bridge = {
|
|
414
473
|
isEditing,
|
|
415
474
|
mode: isEditing ? 'editor' : null,
|
|
@@ -417,8 +476,8 @@ const createBridge = (win, cmsOrigin)=>{
|
|
|
417
476
|
environment: params.get(preview.PREVIEW_PARAMS.environment),
|
|
418
477
|
product: null,
|
|
419
478
|
collection: null,
|
|
420
|
-
on
|
|
421
|
-
getState
|
|
479
|
+
on,
|
|
480
|
+
getState,
|
|
422
481
|
...INERT_METHODS,
|
|
423
482
|
destroy: ()=>{
|
|
424
483
|
cleanup.splice(0).forEach((undo)=>undo());
|
|
@@ -438,22 +497,12 @@ const createBridge = (win, cmsOrigin)=>{
|
|
|
438
497
|
doc.head.appendChild(style);
|
|
439
498
|
cleanup.push(()=>style.remove());
|
|
440
499
|
const highlight = (uid, scroll)=>{
|
|
441
|
-
|
|
442
|
-
element.removeAttribute('data-cms-selected');
|
|
443
|
-
});
|
|
444
|
-
if (state.selectedUid !== (uid ?? null)) {
|
|
500
|
+
if (getState().selectedUid !== (uid ?? null)) {
|
|
445
501
|
setState({
|
|
446
502
|
selectedUid: uid ?? null
|
|
447
503
|
});
|
|
448
504
|
}
|
|
449
|
-
|
|
450
|
-
const element = doc.querySelector(blockSelector(uid));
|
|
451
|
-
if (!element) return;
|
|
452
|
-
element.setAttribute('data-cms-selected', '');
|
|
453
|
-
if (scroll) element.scrollIntoView({
|
|
454
|
-
behavior: 'smooth',
|
|
455
|
-
block: 'center'
|
|
456
|
-
});
|
|
505
|
+
showSelection(doc, uid, scroll);
|
|
457
506
|
};
|
|
458
507
|
// Capture phase, so a block that is also a link selects instead of
|
|
459
508
|
// navigating away.
|
|
@@ -513,11 +562,11 @@ const createBridge = (win, cmsOrigin)=>{
|
|
|
513
562
|
bridge.prompt = (prompt, options = {})=>{
|
|
514
563
|
const text = prompt.trim();
|
|
515
564
|
if (!text) return Promise.reject(new Error('Say what to change'));
|
|
516
|
-
if (!
|
|
565
|
+
if (!getState().features.includes('prompt')) {
|
|
517
566
|
return Promise.reject(new Error('The editor does not offer prompts here'));
|
|
518
567
|
}
|
|
519
568
|
const id = `prompt_${++nextId}`;
|
|
520
|
-
const uid = options.uid === undefined ?
|
|
569
|
+
const uid = options.uid === undefined ? getState().selectedUid : options.uid;
|
|
521
570
|
return new Promise((resolve, reject)=>{
|
|
522
571
|
const timer = setTimeout(()=>{
|
|
523
572
|
settle(id)?.reject(new Error('The editor did not answer in time'));
|
|
@@ -544,17 +593,11 @@ const createBridge = (win, cmsOrigin)=>{
|
|
|
544
593
|
uid
|
|
545
594
|
});
|
|
546
595
|
};
|
|
547
|
-
|
|
548
|
-
let overlay = null;
|
|
549
|
-
const showStats = ()=>{
|
|
550
|
-
if (!overlay && !Object.keys(state.stats).length) return;
|
|
551
|
-
overlay ??= mountStatsOverlay(win, {
|
|
596
|
+
const stats = createStatsView(()=>mountStatsOverlay(win, {
|
|
552
597
|
selector: blockSelector,
|
|
553
598
|
onSelect: (uid)=>bridge.select(uid)
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
};
|
|
557
|
-
cleanup.push(()=>overlay?.destroy());
|
|
599
|
+
}));
|
|
600
|
+
cleanup.push(()=>stats.destroy());
|
|
558
601
|
cleanup.push(()=>{
|
|
559
602
|
for (const id of pending.keys()){
|
|
560
603
|
settle(id)?.reject(new Error('The bridge was destroyed'));
|
|
@@ -571,29 +614,31 @@ const createBridge = (win, cmsOrigin)=>{
|
|
|
571
614
|
// have been sent before it was listening.
|
|
572
615
|
ready();
|
|
573
616
|
} else if (data.type === 'update' && data.entry) {
|
|
574
|
-
|
|
617
|
+
emit('update', {
|
|
575
618
|
entry: data.entry,
|
|
576
619
|
selectedUid: data.selectedUid ?? null
|
|
577
620
|
});
|
|
578
621
|
// Re-apply the selection to whatever the site rendered.
|
|
579
622
|
win.requestAnimationFrame(()=>highlight(data.selectedUid, false));
|
|
580
|
-
|
|
623
|
+
stats.refresh();
|
|
581
624
|
} else if (data.type === 'select') {
|
|
582
625
|
highlight(data.uid, true);
|
|
583
626
|
} else if (data.type === 'saved' || data.type === 'published') {
|
|
584
|
-
|
|
585
|
-
if (
|
|
627
|
+
emit(data.type, {});
|
|
628
|
+
if (listeners.update.size === 0) win.location.reload();
|
|
586
629
|
} else if (data.type === 'features' && Array.isArray(data.features)) {
|
|
587
630
|
setState({
|
|
588
631
|
features: data.features.filter((feature)=>typeof feature === 'string')
|
|
589
632
|
});
|
|
590
633
|
} else if (data.type === 'stats') {
|
|
591
634
|
// An editor from before the switch sends no `visible`: show them.
|
|
592
|
-
|
|
635
|
+
const next = {
|
|
593
636
|
stats: readStats(data.stats),
|
|
594
637
|
statsVisible: data.visible !== false
|
|
595
|
-
}
|
|
596
|
-
|
|
638
|
+
};
|
|
639
|
+
setState(next);
|
|
640
|
+
const { stats: rates, statsVisible } = getState();
|
|
641
|
+
stats.update(rates, statsVisible);
|
|
597
642
|
} else if (data.type === 'viewport' && typeof data.height === 'number') {
|
|
598
643
|
// The canvas: the frame is as tall as the page (`size`), so `vh`
|
|
599
644
|
// sizes are fixed at the screen height it stands for.
|
|
@@ -626,6 +671,7 @@ const createBridge = (win, cmsOrigin)=>{
|
|
|
626
671
|
}
|
|
627
672
|
return bridge;
|
|
628
673
|
};
|
|
674
|
+
|
|
629
675
|
const wrapScriptBridge = (script, params)=>{
|
|
630
676
|
const emitter = createEmitter();
|
|
631
677
|
script.on('saved', ()=>emitter.emit('saved', {}));
|
|
@@ -634,12 +680,11 @@ const wrapScriptBridge = (script, params)=>{
|
|
|
634
680
|
// no `update` listener, and has no way to remove one.
|
|
635
681
|
let forwardingUpdates = false;
|
|
636
682
|
return {
|
|
683
|
+
...createInertBridge(),
|
|
637
684
|
isEditing: script.isEditing,
|
|
638
685
|
mode: script.isEditing ? 'editor' : null,
|
|
639
686
|
entryId: script.entryId,
|
|
640
687
|
environment: params.get(preview.PREVIEW_PARAMS.environment),
|
|
641
|
-
product: null,
|
|
642
|
-
collection: null,
|
|
643
688
|
on: (event, listener)=>{
|
|
644
689
|
if (event === 'update' && !forwardingUpdates) {
|
|
645
690
|
forwardingUpdates = true;
|
|
@@ -652,10 +697,7 @@ const wrapScriptBridge = (script, params)=>{
|
|
|
652
697
|
});
|
|
653
698
|
}
|
|
654
699
|
return emitter.on(event, listener);
|
|
655
|
-
}
|
|
656
|
-
getState: ()=>INITIAL_STATE,
|
|
657
|
-
...INERT_METHODS,
|
|
658
|
-
destroy: ()=>{}
|
|
700
|
+
}
|
|
659
701
|
};
|
|
660
702
|
};
|
|
661
703
|
|
|
@@ -690,6 +732,11 @@ const svgSpan = (doc, className, paths)=>{
|
|
|
690
732
|
};
|
|
691
733
|
// `{name}` style placeholders.
|
|
692
734
|
const fill = (template, values)=>template.replace(/\{(\w+)\}/g, (match, key)=>key in values ? String(values[key]) : match);
|
|
735
|
+
// A textarea as tall as its text.
|
|
736
|
+
const fitToText = (textarea)=>{
|
|
737
|
+
textarea.style.height = 'auto';
|
|
738
|
+
textarea.style.height = `${textarea.scrollHeight}px`;
|
|
739
|
+
};
|
|
693
740
|
// Line icons (Lucide), drawn in the text colour.
|
|
694
741
|
const ICONS = {
|
|
695
742
|
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"/>',
|
|
@@ -1000,6 +1047,55 @@ const createPagesPicker = (doc, bridge, labels, { onLoad, onClose })=>{
|
|
|
1000
1047
|
};
|
|
1001
1048
|
};
|
|
1002
1049
|
|
|
1050
|
+
const SHOP = {
|
|
1051
|
+
product: {
|
|
1052
|
+
item: preview.PRODUCT_ATTRIBUTE,
|
|
1053
|
+
handle: preview.PRODUCT_HANDLE_ATTRIBUTE,
|
|
1054
|
+
setting: 'productPath',
|
|
1055
|
+
route: '/products/{handle}',
|
|
1056
|
+
marker: 'editableProduct(product)',
|
|
1057
|
+
add: 'addProductPage',
|
|
1058
|
+
adding: 'addingProductPage',
|
|
1059
|
+
hint: 'productPageHint',
|
|
1060
|
+
added: 'addedProductPage'
|
|
1061
|
+
},
|
|
1062
|
+
collection: {
|
|
1063
|
+
item: preview.COLLECTION_ATTRIBUTE,
|
|
1064
|
+
handle: preview.COLLECTION_HANDLE_ATTRIBUTE,
|
|
1065
|
+
setting: 'collectionPath',
|
|
1066
|
+
route: '/collections/{handle}',
|
|
1067
|
+
marker: 'editableCollection(collection)',
|
|
1068
|
+
add: 'addCollectionPage',
|
|
1069
|
+
adding: 'addingCollectionPage',
|
|
1070
|
+
hint: 'collectionPageHint',
|
|
1071
|
+
added: 'addedCollectionPage'
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
// The shop item the page marks with `editableProduct` or
|
|
1075
|
+
// `editableCollection`, if it marks one.
|
|
1076
|
+
const pageShopItem = (doc, kind)=>{
|
|
1077
|
+
const { item, handle } = SHOP[kind];
|
|
1078
|
+
const element = doc.querySelector(`[${item}]`);
|
|
1079
|
+
const id = element?.getAttribute(item);
|
|
1080
|
+
const itemHandle = element?.getAttribute(handle);
|
|
1081
|
+
return id && itemHandle ? {
|
|
1082
|
+
id,
|
|
1083
|
+
handle: itemHandle
|
|
1084
|
+
} : null;
|
|
1085
|
+
};
|
|
1086
|
+
// Which kind of shop page a page entry is, else null.
|
|
1087
|
+
const shopKindOf = (page)=>page.product ? 'product' : page.collection ? 'collection' : null;
|
|
1088
|
+
const shopItemOf = (page)=>page.product ?? page.collection ?? null;
|
|
1089
|
+
const shopCopy = (labels, kind)=>{
|
|
1090
|
+
const shop = SHOP[kind];
|
|
1091
|
+
return {
|
|
1092
|
+
add: labels[shop.add],
|
|
1093
|
+
adding: labels[shop.adding],
|
|
1094
|
+
hint: labels[shop.hint],
|
|
1095
|
+
added: labels[shop.added]
|
|
1096
|
+
};
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1003
1099
|
// What the prompt box and the on-site editor decide alike on the site.
|
|
1004
1100
|
const failure = (error)=>error instanceof Error ? error.message : String(error);
|
|
1005
1101
|
const pageContext = (bridge, state, promptWithoutPage)=>{
|
|
@@ -1010,39 +1106,26 @@ const pageContext = (bridge, state, promptWithoutPage)=>{
|
|
|
1010
1106
|
const pageless = site && bridge.entryId === null;
|
|
1011
1107
|
// …unless it shows a product (collection): then Add a product
|
|
1012
1108
|
// (collection) page first.
|
|
1013
|
-
const
|
|
1014
|
-
const onlyNew = pageless && canCreate && !
|
|
1109
|
+
const shop = pageless && canCreate ? shopKindOf(bridge) : null;
|
|
1110
|
+
const onlyNew = pageless && canCreate && !shop && !promptWithoutPage;
|
|
1015
1111
|
return {
|
|
1016
1112
|
site,
|
|
1017
1113
|
canCreate,
|
|
1018
1114
|
pageless,
|
|
1019
|
-
|
|
1115
|
+
shop,
|
|
1020
1116
|
onlyNew
|
|
1021
1117
|
};
|
|
1022
1118
|
};
|
|
1023
|
-
const shopCopy = (labels, shopHere)=>shopHere === 'collection' ? {
|
|
1024
|
-
add: labels.addCollectionPage,
|
|
1025
|
-
adding: labels.addingCollectionPage,
|
|
1026
|
-
hint: labels.collectionPageHint
|
|
1027
|
-
} : {
|
|
1028
|
-
add: labels.addProductPage,
|
|
1029
|
-
adding: labels.addingProductPage,
|
|
1030
|
-
hint: labels.productPageHint
|
|
1031
|
-
};
|
|
1032
1119
|
// Makes the page's product (collection) page; the bridge then reloads, and
|
|
1033
1120
|
// the answer shows until it does.
|
|
1034
1121
|
const addShopPage = async (bridge, labels)=>{
|
|
1122
|
+
const kind = shopKindOf(bridge);
|
|
1123
|
+
if (!kind) return null;
|
|
1035
1124
|
try {
|
|
1036
|
-
if (bridge.
|
|
1037
|
-
|
|
1038
|
-
return {
|
|
1039
|
-
text: labels.addedProductPage,
|
|
1040
|
-
error: false
|
|
1041
|
-
};
|
|
1042
|
-
}
|
|
1043
|
-
await bridge.createCollectionPage();
|
|
1125
|
+
if (kind === 'product') await bridge.createProductPage();
|
|
1126
|
+
else await bridge.createCollectionPage();
|
|
1044
1127
|
return {
|
|
1045
|
-
text: labels.
|
|
1128
|
+
text: labels[SHOP[kind].added],
|
|
1046
1129
|
error: false
|
|
1047
1130
|
};
|
|
1048
1131
|
} catch (error) {
|
|
@@ -1062,6 +1145,36 @@ const storeChanges = async (bridge, action)=>{
|
|
|
1062
1145
|
}
|
|
1063
1146
|
};
|
|
1064
1147
|
|
|
1148
|
+
const bridgeView = (bridge, state, // What the editor asked for; `creating` is that, kept in step with what
|
|
1149
|
+
// the page actually offers.
|
|
1150
|
+
intent, { promptWithoutPage, signOut, labels })=>{
|
|
1151
|
+
const page = pageContext(bridge, state, promptWithoutPage);
|
|
1152
|
+
// A page with nothing to edit has one thing to offer; a page that can't
|
|
1153
|
+
// take a new one has nothing to make.
|
|
1154
|
+
const creating = page.canCreate && (intent.creating || page.onlyNew);
|
|
1155
|
+
const available = state.features.includes('prompt');
|
|
1156
|
+
const storing = state.saving || state.publishing;
|
|
1157
|
+
const busy = state.prompting || storing;
|
|
1158
|
+
return {
|
|
1159
|
+
...page,
|
|
1160
|
+
creating,
|
|
1161
|
+
available,
|
|
1162
|
+
storing,
|
|
1163
|
+
busy,
|
|
1164
|
+
// The page's product (collection) page, when that is all it can offer.
|
|
1165
|
+
offerShop: page.shop !== null && !creating,
|
|
1166
|
+
// The CMS's form is for a page that is an entry, in its own mode,
|
|
1167
|
+
// when the CMS offers one (`state.features`).
|
|
1168
|
+
canForm: page.site && !creating && !page.pageless,
|
|
1169
|
+
placeholder: creating ? labels.newPagePlaceholder : labels.placeholder,
|
|
1170
|
+
canSave: state.features.includes('save') && !page.pageless,
|
|
1171
|
+
canPublish: state.features.includes('publish') && !page.pageless,
|
|
1172
|
+
showSignOut: page.site && signOut !== false,
|
|
1173
|
+
// Sending needs something to say, and a path when making a page.
|
|
1174
|
+
canSend: (prompt, path)=>!busy && prompt.trim() !== '' && (!creating || path.trim() !== '')
|
|
1175
|
+
};
|
|
1176
|
+
};
|
|
1177
|
+
|
|
1065
1178
|
const SIDE_KEY = 'obelism-content-prompt-side';
|
|
1066
1179
|
// How far the pointer moves before it drags the box, not clicks it.
|
|
1067
1180
|
const DRAG_THRESHOLD_PX = 6;
|
|
@@ -1643,26 +1756,29 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1643
1756
|
error: true
|
|
1644
1757
|
});
|
|
1645
1758
|
const render = (state)=>{
|
|
1646
|
-
const
|
|
1647
|
-
|
|
1648
|
-
|
|
1759
|
+
const page = bridgeView(bridge, state, {
|
|
1760
|
+
creating
|
|
1761
|
+
}, {
|
|
1762
|
+
promptWithoutPage: options.promptWithoutPage,
|
|
1763
|
+
signOut: options.signOut,
|
|
1764
|
+
labels
|
|
1765
|
+
});
|
|
1766
|
+
const { available, busy, canCreate, site, pageless, onlyNew } = page;
|
|
1767
|
+
const { offerShop, canForm } = page;
|
|
1768
|
+
creating = page.creating;
|
|
1769
|
+
if (!canForm) form = false;
|
|
1649
1770
|
root.hidden = !available && !site;
|
|
1650
1771
|
pill.hidden = open || !available;
|
|
1651
1772
|
viewing.hidden = available || !site;
|
|
1652
1773
|
panel.hidden = !open || !available;
|
|
1653
|
-
const
|
|
1654
|
-
|
|
1655
|
-
const shopCopy$1 = shopCopy(labels, shopHere);
|
|
1656
|
-
addProduct.lastChild.textContent = shopCopy$1.add;
|
|
1657
|
-
if (!canCreate) creating = false;
|
|
1658
|
-
if (onlyNew) creating = true;
|
|
1659
|
-
if (creating || pageless || !site) form = false;
|
|
1774
|
+
const words = page.shop ? shopCopy(labels, page.shop) : null;
|
|
1775
|
+
addProduct.lastChild.textContent = words?.add ?? labels.addProductPage;
|
|
1660
1776
|
// The form is there only while shown in the open panel.
|
|
1661
1777
|
const showForm = open && available && form;
|
|
1662
1778
|
if (showForm && !formSlot.mounted) formSlot.mount();
|
|
1663
1779
|
else if (!showForm && formSlot.mounted) formSlot.unmount();
|
|
1664
|
-
const offerProduct =
|
|
1665
|
-
pillText.textContent = offerProduct ?
|
|
1780
|
+
const offerProduct = offerShop;
|
|
1781
|
+
pillText.textContent = offerProduct ? words.add : onlyNew ? labels.newPage : labels.open;
|
|
1666
1782
|
cancelNew.hidden = onlyNew;
|
|
1667
1783
|
scope.hidden = creating || offerProduct;
|
|
1668
1784
|
newScope.hidden = !creating;
|
|
@@ -1679,7 +1795,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1679
1795
|
formLoader.hidden = !formSlot.loading;
|
|
1680
1796
|
toolbar.hidden = picking;
|
|
1681
1797
|
pagesSection.hidden = !picking;
|
|
1682
|
-
input.placeholder =
|
|
1798
|
+
input.placeholder = page.placeholder;
|
|
1683
1799
|
scopeText.textContent = state.selectedUid ? labels.scopeBlock : pageless ? labels.scopeNoPage : labels.scopeEntry;
|
|
1684
1800
|
clearBlock.hidden = !state.selectedUid;
|
|
1685
1801
|
pageIcon.hidden = !!state.selectedUid;
|
|
@@ -1702,16 +1818,16 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1702
1818
|
minimize.setAttribute('aria-expanded', String(!minimized));
|
|
1703
1819
|
minimizeIcon.hidden = minimized;
|
|
1704
1820
|
expandIcon.hidden = !minimized;
|
|
1705
|
-
send.disabled =
|
|
1821
|
+
send.disabled = !page.canSend(input.value, pathInput.value);
|
|
1706
1822
|
input.disabled = busy;
|
|
1707
|
-
save.hidden = !
|
|
1823
|
+
save.hidden = !page.canSave;
|
|
1708
1824
|
save.disabled = busy || !state.unsaved;
|
|
1709
1825
|
save.textContent = state.saving ? labels.saving : labels.save;
|
|
1710
|
-
publish.hidden = !
|
|
1826
|
+
publish.hidden = !page.canPublish;
|
|
1711
1827
|
publish.disabled = busy;
|
|
1712
1828
|
publish.textContent = state.publishing ? labels.publishing : labels.publish;
|
|
1713
|
-
signOut.hidden = !
|
|
1714
|
-
pillSignOut.hidden =
|
|
1829
|
+
signOut.hidden = !page.showSignOut;
|
|
1830
|
+
pillSignOut.hidden = !page.showSignOut;
|
|
1715
1831
|
if (options.actions) {
|
|
1716
1832
|
options.actions.slot = open && available ? 'actions-footer' : 'actions-bar';
|
|
1717
1833
|
}
|
|
@@ -1723,16 +1839,12 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1723
1839
|
const loadingForm = form && formSlot.loading && !picking;
|
|
1724
1840
|
const working = state.prompting;
|
|
1725
1841
|
// A form opening says so in its own place.
|
|
1726
|
-
const note = state.prompting ? creating ? labels.creating : offerProduct ?
|
|
1842
|
+
const note = state.prompting ? creating ? labels.creating : offerProduct ? words.adding : labels.working : loadingForm ? '' : message?.text ?? (offerProduct ? words.hint : site && !creating && !picking && !state.selectedUid ? labels.selectHint : '');
|
|
1727
1843
|
status.textContent = note;
|
|
1728
1844
|
status.hidden = !note || minimized;
|
|
1729
1845
|
status.className = `status${working ? ' working' : ''}${!busy && !loadingForm && message?.error ? ' error' : ''}`;
|
|
1730
1846
|
};
|
|
1731
1847
|
const refresh = ()=>render(bridge.getState());
|
|
1732
|
-
const grow = ()=>{
|
|
1733
|
-
input.style.height = 'auto';
|
|
1734
|
-
input.style.height = `${input.scrollHeight}px`;
|
|
1735
|
-
};
|
|
1736
1848
|
const setForm = (next)=>{
|
|
1737
1849
|
if (form === next) return;
|
|
1738
1850
|
form = next;
|
|
@@ -1764,7 +1876,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1764
1876
|
};
|
|
1765
1877
|
}
|
|
1766
1878
|
input.value = '';
|
|
1767
|
-
|
|
1879
|
+
fitToText(input);
|
|
1768
1880
|
} catch (error) {
|
|
1769
1881
|
message = failure$1(error);
|
|
1770
1882
|
}
|
|
@@ -1860,7 +1972,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1860
1972
|
void submit();
|
|
1861
1973
|
});
|
|
1862
1974
|
input.addEventListener('input', ()=>{
|
|
1863
|
-
|
|
1975
|
+
fitToText(input);
|
|
1864
1976
|
refresh();
|
|
1865
1977
|
});
|
|
1866
1978
|
input.addEventListener('keydown', (event)=>{
|
|
@@ -2012,17 +2124,8 @@ const webUrl = (url)=>{
|
|
|
2012
2124
|
};
|
|
2013
2125
|
// The base path of the site's handler while signed in to edit, else null.
|
|
2014
2126
|
const readSiteEditing = (doc)=>{
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
if (name !== site.SITE_EDITING_COOKIE) continue;
|
|
2018
|
-
try {
|
|
2019
|
-
const basePath = decodeURIComponent(value.join('='));
|
|
2020
|
-
return BASE_PATH.test(basePath) ? basePath : null;
|
|
2021
|
-
} catch {
|
|
2022
|
-
return null;
|
|
2023
|
-
}
|
|
2024
|
-
}
|
|
2025
|
-
return null;
|
|
2127
|
+
const basePath = site.parseCookies(doc.cookie).get(site.SITE_EDITING_COOKIE);
|
|
2128
|
+
return basePath && BASE_PATH.test(basePath) ? basePath : null;
|
|
2026
2129
|
};
|
|
2027
2130
|
const same = (a, b)=>JSON.stringify(a) === JSON.stringify(b);
|
|
2028
2131
|
// Edits in the form further apart than this are separate undo steps.
|
|
@@ -2032,7 +2135,7 @@ const FORM_BURST_MS = 1000;
|
|
|
2032
2135
|
const FORM_RENDER_MS = 300;
|
|
2033
2136
|
const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/^\/+/, '')}`, productPath: ownProductPath, collectionPath: ownCollectionPath } = {})=>{
|
|
2034
2137
|
const doc = win.document;
|
|
2035
|
-
const
|
|
2138
|
+
const { on, getState, setState, emit, listeners } = createBridgeState();
|
|
2036
2139
|
const cleanup = [];
|
|
2037
2140
|
const working = new Map();
|
|
2038
2141
|
// Content before each prompt, newest last, for undo.
|
|
@@ -2040,21 +2143,20 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2040
2143
|
// Entries changed here since the last publish.
|
|
2041
2144
|
const touched = new Set();
|
|
2042
2145
|
let session = null;
|
|
2043
|
-
|
|
2044
|
-
|
|
2146
|
+
// Where a product or collection page is on the site: the site's own
|
|
2147
|
+
// routing when it said, else the environment's setting in the CMS, else
|
|
2148
|
+
// the CMS's default path.
|
|
2149
|
+
const shopPath = (kind, handle, locale)=>{
|
|
2150
|
+
const own = kind === 'product' ? ownProductPath : ownCollectionPath;
|
|
2151
|
+
if (own) return own(handle, locale);
|
|
2152
|
+
const { setting, route } = SHOP[kind];
|
|
2153
|
+
return (session?.[setting] ?? route).replaceAll('{handle}', encodeURIComponent(handle));
|
|
2154
|
+
};
|
|
2045
2155
|
// The open form's, if any; and when the form last changed content, for
|
|
2046
2156
|
// undo steps (-Infinity: the next edit starts a new step).
|
|
2047
2157
|
let closeForm = null;
|
|
2048
2158
|
let lastFormEdit = -Infinity;
|
|
2049
2159
|
const renderTimers = new Map();
|
|
2050
|
-
let state = INITIAL_STATE;
|
|
2051
|
-
const setState = (next)=>{
|
|
2052
|
-
state = {
|
|
2053
|
-
...state,
|
|
2054
|
-
...next
|
|
2055
|
-
};
|
|
2056
|
-
emitter.emit('change', state);
|
|
2057
|
-
};
|
|
2058
2160
|
const refreshUnsaved = ()=>setState({
|
|
2059
2161
|
unsaved: [
|
|
2060
2162
|
...working.values()
|
|
@@ -2087,26 +2189,6 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2087
2189
|
return data;
|
|
2088
2190
|
};
|
|
2089
2191
|
const pageEntryId = ()=>doc.querySelector(`[${preview.ENTRY_ATTRIBUTE}]`)?.getAttribute(preview.ENTRY_ATTRIBUTE) ?? null;
|
|
2090
|
-
// The product the page shows (editableProduct), if it marks one.
|
|
2091
|
-
const pageProduct = ()=>{
|
|
2092
|
-
const element = doc.querySelector(`[${preview.PRODUCT_ATTRIBUTE}]`);
|
|
2093
|
-
const id = element?.getAttribute(preview.PRODUCT_ATTRIBUTE);
|
|
2094
|
-
const handle = element?.getAttribute(preview.PRODUCT_HANDLE_ATTRIBUTE);
|
|
2095
|
-
return id && handle ? {
|
|
2096
|
-
id,
|
|
2097
|
-
handle
|
|
2098
|
-
} : null;
|
|
2099
|
-
};
|
|
2100
|
-
// The collection the page shows (editableCollection), if it marks one.
|
|
2101
|
-
const pageCollection = ()=>{
|
|
2102
|
-
const element = doc.querySelector(`[${preview.COLLECTION_ATTRIBUTE}]`);
|
|
2103
|
-
const id = element?.getAttribute(preview.COLLECTION_ATTRIBUTE);
|
|
2104
|
-
const handle = element?.getAttribute(preview.COLLECTION_HANDLE_ATTRIBUTE);
|
|
2105
|
-
return id && handle ? {
|
|
2106
|
-
id,
|
|
2107
|
-
handle
|
|
2108
|
-
} : null;
|
|
2109
|
-
};
|
|
2110
2192
|
const entryFor = (uid)=>{
|
|
2111
2193
|
const block = uid ? doc.querySelector(blockSelector(uid)) : null;
|
|
2112
2194
|
return block?.closest(`[${preview.ENTRY_ATTRIBUTE}]`)?.getAttribute(preview.ENTRY_ATTRIBUTE) ?? pageEntryId();
|
|
@@ -2129,7 +2211,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2129
2211
|
if (loaded) return loaded;
|
|
2130
2212
|
let pending = loading.get(entryId);
|
|
2131
2213
|
if (!pending) {
|
|
2132
|
-
pending = api(
|
|
2214
|
+
pending = api(site.siteApi.entry(entryId)).then((entry)=>{
|
|
2133
2215
|
const fresh = working.get(entryId) ?? {
|
|
2134
2216
|
content: entry.content,
|
|
2135
2217
|
saved: entry.content,
|
|
@@ -2145,7 +2227,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2145
2227
|
// The entry a prompt or JSON acts on, for the block `uid` (the selected
|
|
2146
2228
|
// one when left out).
|
|
2147
2229
|
const target = (option)=>{
|
|
2148
|
-
const uid = option === undefined ?
|
|
2230
|
+
const uid = option === undefined ? getState().selectedUid : option;
|
|
2149
2231
|
const entryId = entryFor(uid);
|
|
2150
2232
|
if (!entryId) {
|
|
2151
2233
|
throw new Error('Nothing on this page is marked as an entry: add editable(entry)');
|
|
@@ -2155,29 +2237,42 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2155
2237
|
entryId
|
|
2156
2238
|
};
|
|
2157
2239
|
};
|
|
2240
|
+
// The blocks selected on this page, and the tags clicked outside the
|
|
2241
|
+
// blocks: only their rates show (statsOverlay.ts), so a few can be
|
|
2242
|
+
// compared without every tag at once.
|
|
2243
|
+
const revealed = new Set();
|
|
2244
|
+
const asked = new Set();
|
|
2245
|
+
const answered = new Set();
|
|
2246
|
+
const stats = createStatsView(()=>mountStatsOverlay(win, {
|
|
2247
|
+
selector: blockSelector,
|
|
2248
|
+
onSelect: (uid)=>highlight(uid),
|
|
2249
|
+
checked: (id)=>answered.has(id),
|
|
2250
|
+
shows: (id, element)=>revealed.has(element.closest(`[${preview.UID_ATTRIBUTE}]`)?.getAttribute(preview.UID_ATTRIBUTE) ?? id)
|
|
2251
|
+
}));
|
|
2158
2252
|
const highlight = (uid)=>{
|
|
2159
|
-
|
|
2160
|
-
element.removeAttribute('data-cms-selected');
|
|
2161
|
-
});
|
|
2162
|
-
if (state.selectedUid !== uid) setState({
|
|
2253
|
+
if (getState().selectedUid !== uid) setState({
|
|
2163
2254
|
selectedUid: uid
|
|
2164
2255
|
});
|
|
2165
|
-
|
|
2256
|
+
showSelection(doc, uid);
|
|
2257
|
+
if (uid && !revealed.has(uid)) {
|
|
2258
|
+
revealed.add(uid);
|
|
2259
|
+
stats.refresh();
|
|
2260
|
+
}
|
|
2166
2261
|
};
|
|
2167
2262
|
// Shows the in-memory content through `update`, shaped like the delivery
|
|
2168
2263
|
// API. A page without `update` listeners shows it once saved (it reloads).
|
|
2169
2264
|
const render = async (entryId)=>{
|
|
2170
|
-
if (
|
|
2265
|
+
if (listeners.update.size === 0) return;
|
|
2171
2266
|
const content = working.get(entryId)?.content;
|
|
2172
|
-
const { entry } = await api(
|
|
2267
|
+
const { entry } = await api(site.siteApi.preview(entryId), {
|
|
2173
2268
|
content,
|
|
2174
2269
|
locale: locale()
|
|
2175
2270
|
});
|
|
2176
|
-
|
|
2271
|
+
emit('update', {
|
|
2177
2272
|
entry,
|
|
2178
|
-
selectedUid:
|
|
2273
|
+
selectedUid: getState().selectedUid
|
|
2179
2274
|
});
|
|
2180
|
-
win.requestAnimationFrame(()=>highlight(
|
|
2275
|
+
win.requestAnimationFrame(()=>highlight(getState().selectedUid));
|
|
2181
2276
|
};
|
|
2182
2277
|
const logError = (error)=>console.error('[@obelism/content bridge]', error);
|
|
2183
2278
|
// Renders once the form's edits settle.
|
|
@@ -2189,13 +2284,13 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2189
2284
|
}, FORM_RENDER_MS));
|
|
2190
2285
|
};
|
|
2191
2286
|
const afterStore = (event)=>{
|
|
2192
|
-
|
|
2193
|
-
if (
|
|
2287
|
+
emit(event, {});
|
|
2288
|
+
if (listeners.update.size === 0) win.location.reload();
|
|
2194
2289
|
};
|
|
2195
2290
|
const saveAll = async ()=>{
|
|
2196
2291
|
for (const [entryId, entry] of working){
|
|
2197
2292
|
if (same(entry.content, entry.saved)) continue;
|
|
2198
|
-
const { draftRevision } = await api(
|
|
2293
|
+
const { draftRevision } = await api(site.siteApi.save(entryId), {
|
|
2199
2294
|
content: entry.content,
|
|
2200
2295
|
baseRevision: entry.revision
|
|
2201
2296
|
});
|
|
@@ -2207,7 +2302,36 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2207
2302
|
lastFormEdit = -Infinity;
|
|
2208
2303
|
refreshUnsaved();
|
|
2209
2304
|
};
|
|
2210
|
-
const has = (feature)=>
|
|
2305
|
+
const has = (feature)=>getState().features.includes(feature);
|
|
2306
|
+
// Makes the page kind's page for `item`, or the one it has already, and
|
|
2307
|
+
// reloads the page: the site's route finds it now (drafts, while editing).
|
|
2308
|
+
const createShopPage = async (kind, item)=>{
|
|
2309
|
+
const shop = SHOP[kind];
|
|
2310
|
+
if (!item) {
|
|
2311
|
+
throw new Error(`Nothing on this page is marked as a ${kind}: add ${shop.marker}`);
|
|
2312
|
+
}
|
|
2313
|
+
if (!has('create')) throw new Error('You may not add pages here');
|
|
2314
|
+
if (getState().unsaved) {
|
|
2315
|
+
throw new Error('Save or undo your changes before adding a page');
|
|
2316
|
+
}
|
|
2317
|
+
setState({
|
|
2318
|
+
prompting: true
|
|
2319
|
+
});
|
|
2320
|
+
try {
|
|
2321
|
+
const made = await api(site.siteApi.shopPages(kind), {
|
|
2322
|
+
[kind]: {
|
|
2323
|
+
id: item.id,
|
|
2324
|
+
handle: item.handle
|
|
2325
|
+
}
|
|
2326
|
+
});
|
|
2327
|
+
win.location.reload();
|
|
2328
|
+
return made;
|
|
2329
|
+
} finally{
|
|
2330
|
+
setState({
|
|
2331
|
+
prompting: false
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
};
|
|
2211
2335
|
const bridge = {
|
|
2212
2336
|
isEditing: true,
|
|
2213
2337
|
mode: 'site',
|
|
@@ -2215,16 +2339,16 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2215
2339
|
return pageEntryId();
|
|
2216
2340
|
},
|
|
2217
2341
|
get product () {
|
|
2218
|
-
return
|
|
2342
|
+
return pageShopItem(doc, 'product');
|
|
2219
2343
|
},
|
|
2220
2344
|
get collection () {
|
|
2221
|
-
return
|
|
2345
|
+
return pageShopItem(doc, 'collection');
|
|
2222
2346
|
},
|
|
2223
2347
|
get environment () {
|
|
2224
2348
|
return session?.environment ?? null;
|
|
2225
2349
|
},
|
|
2226
|
-
on
|
|
2227
|
-
getState
|
|
2350
|
+
on,
|
|
2351
|
+
getState,
|
|
2228
2352
|
async prompt (prompt, options = {}) {
|
|
2229
2353
|
const text = prompt.trim();
|
|
2230
2354
|
if (!text) throw new Error('Say what to change');
|
|
@@ -2235,7 +2359,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2235
2359
|
});
|
|
2236
2360
|
try {
|
|
2237
2361
|
const entry = await load(entryId);
|
|
2238
|
-
const result = await api(
|
|
2362
|
+
const result = await api(site.siteApi.prompt(entryId), {
|
|
2239
2363
|
prompt: text,
|
|
2240
2364
|
content: entry.content,
|
|
2241
2365
|
locale: locale(),
|
|
@@ -2264,14 +2388,14 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2264
2388
|
if (!path.trim()) throw new Error('Say where the page goes, like /pricing');
|
|
2265
2389
|
if (!has('create')) throw new Error('You may not add pages here');
|
|
2266
2390
|
// Opening the new page would lose them.
|
|
2267
|
-
if (
|
|
2391
|
+
if (getState().unsaved) {
|
|
2268
2392
|
throw new Error('Save or undo your changes before adding a page');
|
|
2269
2393
|
}
|
|
2270
2394
|
setState({
|
|
2271
2395
|
prompting: true
|
|
2272
2396
|
});
|
|
2273
2397
|
try {
|
|
2274
|
-
const created = await api(
|
|
2398
|
+
const created = await api(site.siteApi.createEntry(), {
|
|
2275
2399
|
path,
|
|
2276
2400
|
prompt: text,
|
|
2277
2401
|
locale: locale(),
|
|
@@ -2291,66 +2415,12 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2291
2415
|
});
|
|
2292
2416
|
}
|
|
2293
2417
|
},
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
throw new Error('Nothing on this page is marked as a product: add editableProduct(product)');
|
|
2297
|
-
}
|
|
2298
|
-
if (!has('create')) throw new Error('You may not add pages here');
|
|
2299
|
-
if (state.unsaved) {
|
|
2300
|
-
throw new Error('Save or undo your changes before adding a page');
|
|
2301
|
-
}
|
|
2302
|
-
setState({
|
|
2303
|
-
prompting: true
|
|
2304
|
-
});
|
|
2305
|
-
try {
|
|
2306
|
-
const made = await api('product-pages', {
|
|
2307
|
-
product: {
|
|
2308
|
-
id: product.id,
|
|
2309
|
-
handle: product.handle
|
|
2310
|
-
}
|
|
2311
|
-
});
|
|
2312
|
-
// The site's product route finds its page now (drafts, while
|
|
2313
|
-
// editing): show it.
|
|
2314
|
-
win.location.reload();
|
|
2315
|
-
return made;
|
|
2316
|
-
} finally{
|
|
2317
|
-
setState({
|
|
2318
|
-
prompting: false
|
|
2319
|
-
});
|
|
2320
|
-
}
|
|
2321
|
-
},
|
|
2322
|
-
async createCollectionPage (collection = pageCollection() ?? undefined) {
|
|
2323
|
-
if (!collection) {
|
|
2324
|
-
throw new Error('Nothing on this page is marked as a collection: add editableCollection(collection)');
|
|
2325
|
-
}
|
|
2326
|
-
if (!has('create')) throw new Error('You may not add pages here');
|
|
2327
|
-
if (state.unsaved) {
|
|
2328
|
-
throw new Error('Save or undo your changes before adding a page');
|
|
2329
|
-
}
|
|
2330
|
-
setState({
|
|
2331
|
-
prompting: true
|
|
2332
|
-
});
|
|
2333
|
-
try {
|
|
2334
|
-
const made = await api('collection-pages', {
|
|
2335
|
-
collection: {
|
|
2336
|
-
id: collection.id,
|
|
2337
|
-
handle: collection.handle
|
|
2338
|
-
}
|
|
2339
|
-
});
|
|
2340
|
-
// The site's collection route finds its page now (drafts, while
|
|
2341
|
-
// editing): show it.
|
|
2342
|
-
win.location.reload();
|
|
2343
|
-
return made;
|
|
2344
|
-
} finally{
|
|
2345
|
-
setState({
|
|
2346
|
-
prompting: false
|
|
2347
|
-
});
|
|
2348
|
-
}
|
|
2349
|
-
},
|
|
2418
|
+
createProductPage: (product = pageShopItem(doc, 'product') ?? undefined)=>createShopPage('product', product),
|
|
2419
|
+
createCollectionPage: (collection = pageShopItem(doc, 'collection') ?? undefined)=>createShopPage('collection', collection),
|
|
2350
2420
|
async view (options = {}) {
|
|
2351
2421
|
const { uid, entryId } = target(options.uid);
|
|
2352
2422
|
const entry = await load(entryId);
|
|
2353
|
-
const { view } = await api(
|
|
2423
|
+
const { view } = await api(site.siteApi.view(entryId), {
|
|
2354
2424
|
content: entry.content,
|
|
2355
2425
|
locale: locale(),
|
|
2356
2426
|
uid
|
|
@@ -2368,7 +2438,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2368
2438
|
});
|
|
2369
2439
|
try {
|
|
2370
2440
|
const entry = await load(entryId);
|
|
2371
|
-
const result = await api(
|
|
2441
|
+
const result = await api(site.siteApi.apply(entryId), {
|
|
2372
2442
|
changes,
|
|
2373
2443
|
content: entry.content,
|
|
2374
2444
|
locale: locale(),
|
|
@@ -2420,7 +2490,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2420
2490
|
// changed: prompts, undo, a reload after saving, the selection.
|
|
2421
2491
|
const show = ()=>{
|
|
2422
2492
|
if (closed || !frame) return;
|
|
2423
|
-
const entryId = entryFor(
|
|
2493
|
+
const entryId = entryFor(getState().selectedUid);
|
|
2424
2494
|
if (!entryId) return;
|
|
2425
2495
|
const entry = working.get(entryId);
|
|
2426
2496
|
if (!entry) {
|
|
@@ -2431,7 +2501,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2431
2501
|
entryId,
|
|
2432
2502
|
content: entry.content,
|
|
2433
2503
|
locale: locale(),
|
|
2434
|
-
selectedUid:
|
|
2504
|
+
selectedUid: getState().selectedUid
|
|
2435
2505
|
};
|
|
2436
2506
|
if (shown && shown.entryId === next.entryId && shown.content === next.content && shown.locale === next.locale && shown.selectedUid === next.selectedUid) {
|
|
2437
2507
|
return;
|
|
@@ -2466,7 +2536,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2466
2536
|
if (!has('form')) throw new Error('You may not edit content here');
|
|
2467
2537
|
await load(target(undefined).entryId);
|
|
2468
2538
|
// A fresh one every time: the address works once, for a minute.
|
|
2469
|
-
const { url } = await api(
|
|
2539
|
+
const { url } = await api(site.siteApi.form(), {});
|
|
2470
2540
|
if (closed) return;
|
|
2471
2541
|
const address = typeof url === 'string' ? url : '';
|
|
2472
2542
|
frame = mountFormFrame(win, container, address, title, {
|
|
@@ -2475,7 +2545,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2475
2545
|
onError: (message)=>onError?.(message),
|
|
2476
2546
|
onEscape: ()=>onEscape?.()
|
|
2477
2547
|
});
|
|
2478
|
-
unsubscribe =
|
|
2548
|
+
unsubscribe = on('change', show);
|
|
2479
2549
|
show();
|
|
2480
2550
|
};
|
|
2481
2551
|
start().catch(fail);
|
|
@@ -2484,7 +2554,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2484
2554
|
};
|
|
2485
2555
|
},
|
|
2486
2556
|
async pages () {
|
|
2487
|
-
const { pages } = await api(
|
|
2557
|
+
const { pages } = await api(site.siteApi.pages());
|
|
2488
2558
|
const preferred = [
|
|
2489
2559
|
locale(),
|
|
2490
2560
|
session?.space.defaultLocale
|
|
@@ -2494,6 +2564,8 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2494
2564
|
const code = preferred.find((code)=>code !== undefined && page.paths[code] !== undefined) ?? Object.keys(page.paths)[0];
|
|
2495
2565
|
const path = code === undefined ? undefined : page.paths[code];
|
|
2496
2566
|
if (code === undefined || path === undefined) return [];
|
|
2567
|
+
const shop = shopKindOf(page);
|
|
2568
|
+
const item = shopItemOf(page);
|
|
2497
2569
|
return [
|
|
2498
2570
|
{
|
|
2499
2571
|
entryId: page.entryId,
|
|
@@ -2501,7 +2573,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2501
2573
|
contentType: page.contentType,
|
|
2502
2574
|
status: page.status,
|
|
2503
2575
|
path,
|
|
2504
|
-
url:
|
|
2576
|
+
url: shop && item ? shopPath(shop, item.handle, code) : pagePath(path, code),
|
|
2505
2577
|
product: page.product ?? null,
|
|
2506
2578
|
collection: page.collection ?? null
|
|
2507
2579
|
}
|
|
@@ -2509,7 +2581,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2509
2581
|
});
|
|
2510
2582
|
},
|
|
2511
2583
|
openPage (page) {
|
|
2512
|
-
if (
|
|
2584
|
+
if (getState().unsaved && !win.confirm('Leave this page and lose the changes you have not saved?')) {
|
|
2513
2585
|
return;
|
|
2514
2586
|
}
|
|
2515
2587
|
// Asked already: no second question from `beforeunload`.
|
|
@@ -2558,7 +2630,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2558
2630
|
throw new Error('Nothing on this page is marked as an entry: add editable(entry)');
|
|
2559
2631
|
}
|
|
2560
2632
|
for (const entryId of entries){
|
|
2561
|
-
await api(
|
|
2633
|
+
await api(site.siteApi.publish(entryId), {});
|
|
2562
2634
|
touched.delete(entryId);
|
|
2563
2635
|
}
|
|
2564
2636
|
} finally{
|
|
@@ -2569,7 +2641,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2569
2641
|
afterStore('published');
|
|
2570
2642
|
},
|
|
2571
2643
|
signOut () {
|
|
2572
|
-
if (
|
|
2644
|
+
if (getState().unsaved && !win.confirm('Sign out and lose the changes you have not saved?')) {
|
|
2573
2645
|
return;
|
|
2574
2646
|
}
|
|
2575
2647
|
working.clear();
|
|
@@ -2604,9 +2676,22 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2604
2676
|
// Capture phase, so a block that is also a link selects instead of
|
|
2605
2677
|
// navigating — only while selecting, so the site stays usable.
|
|
2606
2678
|
const onClick = (event)=>{
|
|
2607
|
-
if (!
|
|
2679
|
+
if (!getState().selecting) return;
|
|
2608
2680
|
const target = event.target;
|
|
2609
2681
|
const element = target?.closest?.(`[${preview.UID_ATTRIBUTE}]`);
|
|
2682
|
+
// A tagged element outside the blocks (a menu button) can't be
|
|
2683
|
+
// selected: a click shows its rate, when the CMS answered for it.
|
|
2684
|
+
const tag = element ? null : target?.closest?.(`[${TAG_ATTRIBUTE}]`);
|
|
2685
|
+
const tagId = tag?.getAttribute(TAG_ATTRIBUTE);
|
|
2686
|
+
if (tagId && answered.has(tagId)) {
|
|
2687
|
+
event.preventDefault();
|
|
2688
|
+
event.stopPropagation();
|
|
2689
|
+
if (!revealed.has(tagId)) {
|
|
2690
|
+
revealed.add(tagId);
|
|
2691
|
+
stats.refresh();
|
|
2692
|
+
}
|
|
2693
|
+
return;
|
|
2694
|
+
}
|
|
2610
2695
|
if (!element) return;
|
|
2611
2696
|
event.preventDefault();
|
|
2612
2697
|
event.stopPropagation();
|
|
@@ -2615,20 +2700,18 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2615
2700
|
doc.addEventListener('click', onClick, true);
|
|
2616
2701
|
cleanup.push(()=>doc.removeEventListener('click', onClick, true));
|
|
2617
2702
|
const onBeforeUnload = (event)=>{
|
|
2618
|
-
if (!
|
|
2703
|
+
if (!getState().unsaved) return;
|
|
2619
2704
|
event.preventDefault();
|
|
2620
2705
|
event.returnValue = '';
|
|
2621
2706
|
};
|
|
2622
2707
|
win.addEventListener('beforeunload', onBeforeUnload);
|
|
2623
2708
|
cleanup.push(()=>win.removeEventListener('beforeunload', onBeforeUnload));
|
|
2624
|
-
// The tags on the page's links (statsOverlay.ts)
|
|
2625
|
-
// outlined: the rates of the ids the page tags, asked for
|
|
2626
|
-
|
|
2627
|
-
const answered = new Set();
|
|
2709
|
+
// The tags on the page's links (statsOverlay.ts) of what was selected,
|
|
2710
|
+
// while blocks are outlined: the rates of the ids the page tags, asked for
|
|
2711
|
+
// as they appear.
|
|
2628
2712
|
let statsOff = false;
|
|
2629
|
-
let overlay = null;
|
|
2630
2713
|
let statsTimer = null;
|
|
2631
|
-
const showStats = ()=>
|
|
2714
|
+
const showStats = ()=>stats.update(getState().stats, getState().statsVisible);
|
|
2632
2715
|
const loadStats = async ()=>{
|
|
2633
2716
|
if (statsOff) return;
|
|
2634
2717
|
const ids = [
|
|
@@ -2646,7 +2729,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2646
2729
|
].map((element)=>element.getAttribute(preview.ENTRY_ATTRIBUTE) ?? ''))
|
|
2647
2730
|
].filter(Boolean).slice(0, MAX_STATS_ENTRIES);
|
|
2648
2731
|
try {
|
|
2649
|
-
const { stats } = await api(
|
|
2732
|
+
const { stats } = await api(site.siteApi.stats(), {
|
|
2650
2733
|
uids: batch,
|
|
2651
2734
|
...entries.length > 0 && {
|
|
2652
2735
|
entries
|
|
@@ -2655,7 +2738,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2655
2738
|
batch.forEach((id)=>answered.add(id));
|
|
2656
2739
|
setState({
|
|
2657
2740
|
stats: {
|
|
2658
|
-
...
|
|
2741
|
+
...getState().stats,
|
|
2659
2742
|
...readStats(stats)
|
|
2660
2743
|
}
|
|
2661
2744
|
});
|
|
@@ -2664,11 +2747,6 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2664
2747
|
statsOff = true;
|
|
2665
2748
|
return;
|
|
2666
2749
|
}
|
|
2667
|
-
overlay ??= mountStatsOverlay(win, {
|
|
2668
|
-
selector: blockSelector,
|
|
2669
|
-
onSelect: (uid)=>highlight(uid),
|
|
2670
|
-
checked: (id)=>answered.has(id)
|
|
2671
|
-
});
|
|
2672
2750
|
showStats();
|
|
2673
2751
|
if (ids.length > batch.length) scheduleStats();
|
|
2674
2752
|
};
|
|
@@ -2684,9 +2762,9 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2684
2762
|
cleanup.push(()=>{
|
|
2685
2763
|
statsObserver?.disconnect();
|
|
2686
2764
|
if (statsTimer !== null) clearTimeout(statsTimer);
|
|
2687
|
-
|
|
2765
|
+
stats.destroy();
|
|
2688
2766
|
});
|
|
2689
|
-
api(
|
|
2767
|
+
api(site.siteApi.session()).then((info)=>{
|
|
2690
2768
|
session = info;
|
|
2691
2769
|
setState({
|
|
2692
2770
|
features: info.features
|
|
@@ -3202,7 +3280,7 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3202
3280
|
});
|
|
3203
3281
|
const shopButtonText = element(doc, 'span');
|
|
3204
3282
|
const shopButton = button('button primary', icon('plus'), shopButtonText);
|
|
3205
|
-
const
|
|
3283
|
+
const shopBox = element(doc, 'div', {
|
|
3206
3284
|
className: 'shop'
|
|
3207
3285
|
}, shopText, shopButton);
|
|
3208
3286
|
const log = element(doc, 'div', {
|
|
@@ -3211,7 +3289,7 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3211
3289
|
log.setAttribute('role', 'log');
|
|
3212
3290
|
const scroll = element(doc, 'div', {
|
|
3213
3291
|
className: 'scroll'
|
|
3214
|
-
}, note, intro,
|
|
3292
|
+
}, note, intro, shopBox, log);
|
|
3215
3293
|
const pathInput = element(doc, 'input', {
|
|
3216
3294
|
type: 'text',
|
|
3217
3295
|
placeholder: labels.pathPlaceholder,
|
|
@@ -3618,14 +3696,18 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3618
3696
|
};
|
|
3619
3697
|
const render = (state)=>{
|
|
3620
3698
|
if (destroyed) return;
|
|
3621
|
-
const
|
|
3622
|
-
|
|
3623
|
-
|
|
3699
|
+
const page = bridgeView(bridge, state, {
|
|
3700
|
+
creating
|
|
3701
|
+
}, {
|
|
3702
|
+
promptWithoutPage: options.promptWithoutPage,
|
|
3703
|
+
signOut: options.signOut,
|
|
3704
|
+
labels
|
|
3705
|
+
});
|
|
3706
|
+
const { available, busy, canCreate, pageless, site, shop, onlyNew } = page;
|
|
3707
|
+
const { storing } = page;
|
|
3708
|
+
const hasForm = page.canForm && state.features.includes('form');
|
|
3624
3709
|
const shown = available && open;
|
|
3625
|
-
|
|
3626
|
-
if (!canCreate) creating = false;
|
|
3627
|
-
if (onlyNew) creating = true;
|
|
3628
|
-
const hasForm = site && !creating && !pageless && state.features.includes('form');
|
|
3710
|
+
creating = page.creating;
|
|
3629
3711
|
view = hasForm ? mode : 'ask';
|
|
3630
3712
|
const waiting = pending().length > 0;
|
|
3631
3713
|
if (!waiting || view === 'fields') gated = false;
|
|
@@ -3641,7 +3723,7 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3641
3723
|
eye.setAttribute('aria-pressed', String(!outlines));
|
|
3642
3724
|
eyeOn.hidden = !outlines;
|
|
3643
3725
|
eyeOff.hidden = outlines;
|
|
3644
|
-
exit.hidden =
|
|
3726
|
+
exit.hidden = !page.showSignOut;
|
|
3645
3727
|
// The site beside the panel, wide enough; a sheet over it otherwise.
|
|
3646
3728
|
const docked = shown && wide;
|
|
3647
3729
|
html.toggleAttribute('data-obelism-docked', docked);
|
|
@@ -3671,7 +3753,7 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3671
3753
|
gateKeep.disabled = gateUndo.disabled = busy;
|
|
3672
3754
|
ask.hidden = view !== 'ask';
|
|
3673
3755
|
fields.hidden = view !== 'fields';
|
|
3674
|
-
const offerShop =
|
|
3756
|
+
const offerShop = page.offerShop;
|
|
3675
3757
|
if (creating) {
|
|
3676
3758
|
noteText.textContent = labels.newPage;
|
|
3677
3759
|
} else if (state.selectedUid) {
|
|
@@ -3698,26 +3780,26 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3698
3780
|
up.hidden = creating || !state.selectedUid;
|
|
3699
3781
|
cancelNew.hidden = !creating || onlyNew;
|
|
3700
3782
|
intro.hidden = answers.length > 0 || offerShop || creating;
|
|
3701
|
-
|
|
3702
|
-
const shopWords = shopCopy(labels,
|
|
3703
|
-
shopButtonText.textContent = state.prompting && offerShop ? shopWords.adding : shopWords.add;
|
|
3783
|
+
shopBox.hidden = !offerShop;
|
|
3784
|
+
const shopWords = shop ? shopCopy(labels, shop) : null;
|
|
3785
|
+
shopButtonText.textContent = shopWords ? state.prompting && offerShop ? shopWords.adding : shopWords.add : '';
|
|
3704
3786
|
shopButton.disabled = busy;
|
|
3705
|
-
shopText.textContent = shopMessage?.text ?? shopWords
|
|
3787
|
+
shopText.textContent = shopMessage?.text ?? shopWords?.hint ?? '';
|
|
3706
3788
|
shopText.className = shopMessage?.error ? 'failure' : 'summary';
|
|
3707
3789
|
composer.hidden = offerShop;
|
|
3708
3790
|
pathRow.hidden = !creating;
|
|
3709
3791
|
pathInput.disabled = busy;
|
|
3710
|
-
input.placeholder =
|
|
3792
|
+
input.placeholder = page.placeholder;
|
|
3711
3793
|
input.disabled = busy;
|
|
3712
|
-
send.disabled =
|
|
3794
|
+
send.disabled = !page.canSend(input.value, pathInput.value);
|
|
3713
3795
|
send.setAttribute('aria-busy', String(state.prompting));
|
|
3714
3796
|
sendIcon.hidden = state.prompting;
|
|
3715
3797
|
sendWorking.hidden = !state.prompting;
|
|
3716
3798
|
undo.hidden = waiting || !state.canUndo || creating;
|
|
3717
3799
|
undo.disabled = busy;
|
|
3718
|
-
save.hidden = !
|
|
3800
|
+
save.hidden = !page.canSave;
|
|
3719
3801
|
save.disabled = busy || waiting || !state.unsaved;
|
|
3720
|
-
publish.hidden = !
|
|
3802
|
+
publish.hidden = !page.canPublish;
|
|
3721
3803
|
publish.disabled = busy || waiting;
|
|
3722
3804
|
if (options.actions) options.actions.slot = shown ? 'actions-footer' : 'actions-bar';
|
|
3723
3805
|
foot.hidden = !site;
|
|
@@ -3755,10 +3837,6 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3755
3837
|
scheduleMarks();
|
|
3756
3838
|
};
|
|
3757
3839
|
const refresh = ()=>render(bridge.getState());
|
|
3758
|
-
const grow = ()=>{
|
|
3759
|
-
input.style.height = 'auto';
|
|
3760
|
-
input.style.height = `${input.scrollHeight}px`;
|
|
3761
|
-
};
|
|
3762
3840
|
const scrollDown = ()=>{
|
|
3763
3841
|
scroll.scrollTop = scroll.scrollHeight;
|
|
3764
3842
|
};
|
|
@@ -3887,7 +3965,7 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3887
3965
|
textContent: `${labels.you} (${scopeName}): `
|
|
3888
3966
|
}), prompt));
|
|
3889
3967
|
input.value = '';
|
|
3890
|
-
|
|
3968
|
+
fitToText(input);
|
|
3891
3969
|
const answer = addAnswer(prompt, uid);
|
|
3892
3970
|
refresh();
|
|
3893
3971
|
scrollDown();
|
|
@@ -3971,7 +4049,7 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3971
4049
|
void submit();
|
|
3972
4050
|
});
|
|
3973
4051
|
input.addEventListener('input', ()=>{
|
|
3974
|
-
|
|
4052
|
+
fitToText(input);
|
|
3975
4053
|
refresh();
|
|
3976
4054
|
});
|
|
3977
4055
|
input.addEventListener('keydown', (event)=>{
|
|
@@ -4060,11 +4138,8 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
4060
4138
|
// Kept on the window, so two copies of this package on one page still share
|
|
4061
4139
|
// one bridge.
|
|
4062
4140
|
const KEY = Symbol.for('@obelism/content/bridge');
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
entryId: null,
|
|
4066
|
-
on: ()=>{}
|
|
4067
|
-
}, new URLSearchParams());
|
|
4141
|
+
// The bridge on the server, where there is nothing to edit.
|
|
4142
|
+
const SERVER_BRIDGE = createInertBridge();
|
|
4068
4143
|
// The page's bridge if something already started it, else null. Never
|
|
4069
4144
|
// starts one: for code that has to stay side-effect free, like a React
|
|
4070
4145
|
// store's snapshot.
|