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