@obelism/content 0.28.3 → 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/dist/bridge.d.mts +1 -1
- package/dist/bridge.d.mts.map +1 -1
- package/dist/bridge.d.ts +1 -1
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.global.js +4 -4
- package/dist/bridge.js +423 -374
- package/dist/bridge.mjs +424 -375
- 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}%',
|
|
@@ -296,119 +458,15 @@ const createViewportUnits = (doc)=>{
|
|
|
296
458
|
// which one was clicked, hands the editor's live, unsaved entry to listeners
|
|
297
459
|
// and sends the editor prompts. Outside the editor it does nothing.
|
|
298
460
|
//
|
|
299
|
-
// Messages are only exchanged with the CMS origin, and only with the parent
|
|
300
|
-
// window:
|
|
301
|
-
// - site → editor: { source: 'obelism-content-bridge', type: 'ready' |
|
|
302
|
-
// 'size' | 'select' | 'prompt' | 'undo', uid?, id?, prompt?, height? }
|
|
303
|
-
// - editor → site: { source: 'obelism-content-editor', type: 'connect' |
|
|
304
|
-
// 'update' | 'select' | 'saved' | 'published' | 'features' |
|
|
305
|
-
// 'promptResult' | 'promptState' | 'stats' | 'viewport', … }
|
|
306
|
-
const BRIDGE_SOURCE = 'obelism-content-bridge';
|
|
307
|
-
const EDITOR_SOURCE = 'obelism-content-editor';
|
|
308
|
-
// How long a prompt may take before it counts as failed.
|
|
309
|
-
const PROMPT_TIMEOUT_MS = 120_000;
|
|
310
|
-
const OUTLINE = '#d58a1f';
|
|
311
|
-
// Outlines on blocks; `scope` limits them to e.g. `html[data-cms-selecting] `.
|
|
312
|
-
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}`;
|
|
313
|
-
const INITIAL_STATE = {
|
|
314
|
-
features: [],
|
|
315
|
-
selectedUid: null,
|
|
316
|
-
prompting: false,
|
|
317
|
-
canUndo: false,
|
|
318
|
-
unsaved: false,
|
|
319
|
-
saving: false,
|
|
320
|
-
publishing: false,
|
|
321
|
-
selecting: false,
|
|
322
|
-
stats: {},
|
|
323
|
-
statsVisible: false
|
|
324
|
-
};
|
|
325
|
-
const createEmitter = ()=>{
|
|
326
|
-
const listeners = {
|
|
327
|
-
update: new Set(),
|
|
328
|
-
saved: new Set(),
|
|
329
|
-
published: new Set(),
|
|
330
|
-
change: new Set()
|
|
331
|
-
};
|
|
332
|
-
return {
|
|
333
|
-
listeners,
|
|
334
|
-
on (event, listener) {
|
|
335
|
-
listeners[event].add(listener);
|
|
336
|
-
return ()=>{
|
|
337
|
-
listeners[event].delete(listener);
|
|
338
|
-
};
|
|
339
|
-
},
|
|
340
|
-
emit (event, payload) {
|
|
341
|
-
for (const listener of listeners[event]){
|
|
342
|
-
try {
|
|
343
|
-
listener(payload);
|
|
344
|
-
} catch (error) {
|
|
345
|
-
console.error('[@obelism/content bridge]', error);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
};
|
|
350
|
-
};
|
|
351
|
-
const inertPrompt = ()=>Promise.reject(new Error('Prompts only work inside the visual editor'));
|
|
352
|
-
const onlyOnSite = ()=>Promise.reject(new Error('Save and publish in the visual editor itself'));
|
|
353
|
-
const onlyWhileEditingSite = ()=>Promise.reject(new Error('Only while signed in to edit on the site'));
|
|
354
|
-
// What every bridge without a feature does for it.
|
|
355
|
-
const INERT_METHODS = {
|
|
356
|
-
prompt: inertPrompt,
|
|
357
|
-
createPage: ()=>Promise.reject(new Error('New pages are made on the site itself')),
|
|
358
|
-
createProductPage: ()=>Promise.reject(new Error('Product pages are made on the site itself')),
|
|
359
|
-
createCollectionPage: ()=>Promise.reject(new Error('Collection pages are made on the site itself')),
|
|
360
|
-
view: onlyWhileEditingSite,
|
|
361
|
-
applyJson: onlyWhileEditingSite,
|
|
362
|
-
openForm: (_container, options = {})=>{
|
|
363
|
-
queueMicrotask(()=>options.onError?.('Only while signed in to edit on the site'));
|
|
364
|
-
return {
|
|
365
|
-
close: ()=>{}
|
|
366
|
-
};
|
|
367
|
-
},
|
|
368
|
-
pages: onlyWhileEditingSite,
|
|
369
|
-
openPage: ()=>{},
|
|
370
|
-
undo: ()=>{},
|
|
371
|
-
save: onlyOnSite,
|
|
372
|
-
publish: onlyOnSite,
|
|
373
|
-
signOut: ()=>{},
|
|
374
|
-
select: ()=>{},
|
|
375
|
-
setSelecting: ()=>{}
|
|
376
|
-
};
|
|
377
|
-
const isCount = (value)=>typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
378
|
-
// The editor's `stats`, keeping only well-formed entries.
|
|
379
|
-
const readStats = (value)=>{
|
|
380
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
381
|
-
const stats = {};
|
|
382
|
-
for (const [uid, block] of Object.entries(value)){
|
|
383
|
-
const { clicks, clickers, viewers, rate } = block ?? {};
|
|
384
|
-
if (isCount(clicks) && isCount(clickers) && isCount(viewers) && isCount(rate)) stats[uid] = {
|
|
385
|
-
clicks,
|
|
386
|
-
clickers,
|
|
387
|
-
viewers,
|
|
388
|
-
rate
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
return stats;
|
|
392
|
-
};
|
|
393
|
-
// The selector of the element marked with a block's uid.
|
|
394
|
-
const blockSelector = (uid)=>`[${UID_ATTRIBUTE}="${uid.replace(/["\\]/g, '\\$&')}"]`;
|
|
395
461
|
// `cmsOrigin` is the only origin messages are accepted from and sent to.
|
|
396
462
|
const createBridge = (win, cmsOrigin)=>{
|
|
397
463
|
const params = new URLSearchParams(win.location.search);
|
|
398
464
|
const isEditing = win.parent !== win && params.has(PREVIEW_PARAMS.preview);
|
|
399
|
-
const
|
|
400
|
-
const cleanup = [];
|
|
401
|
-
let state = {
|
|
465
|
+
const { on, getState, setState, emit, listeners } = createBridgeState({
|
|
402
466
|
...INITIAL_STATE,
|
|
403
467
|
selecting: true
|
|
404
|
-
};
|
|
405
|
-
const
|
|
406
|
-
state = {
|
|
407
|
-
...state,
|
|
408
|
-
...next
|
|
409
|
-
};
|
|
410
|
-
emitter.emit('change', state);
|
|
411
|
-
};
|
|
468
|
+
});
|
|
469
|
+
const cleanup = [];
|
|
412
470
|
const bridge = {
|
|
413
471
|
isEditing,
|
|
414
472
|
mode: isEditing ? 'editor' : null,
|
|
@@ -416,8 +474,8 @@ const createBridge = (win, cmsOrigin)=>{
|
|
|
416
474
|
environment: params.get(PREVIEW_PARAMS.environment),
|
|
417
475
|
product: null,
|
|
418
476
|
collection: null,
|
|
419
|
-
on
|
|
420
|
-
getState
|
|
477
|
+
on,
|
|
478
|
+
getState,
|
|
421
479
|
...INERT_METHODS,
|
|
422
480
|
destroy: ()=>{
|
|
423
481
|
cleanup.splice(0).forEach((undo)=>undo());
|
|
@@ -437,22 +495,12 @@ const createBridge = (win, cmsOrigin)=>{
|
|
|
437
495
|
doc.head.appendChild(style);
|
|
438
496
|
cleanup.push(()=>style.remove());
|
|
439
497
|
const highlight = (uid, scroll)=>{
|
|
440
|
-
|
|
441
|
-
element.removeAttribute('data-cms-selected');
|
|
442
|
-
});
|
|
443
|
-
if (state.selectedUid !== (uid ?? null)) {
|
|
498
|
+
if (getState().selectedUid !== (uid ?? null)) {
|
|
444
499
|
setState({
|
|
445
500
|
selectedUid: uid ?? null
|
|
446
501
|
});
|
|
447
502
|
}
|
|
448
|
-
|
|
449
|
-
const element = doc.querySelector(blockSelector(uid));
|
|
450
|
-
if (!element) return;
|
|
451
|
-
element.setAttribute('data-cms-selected', '');
|
|
452
|
-
if (scroll) element.scrollIntoView({
|
|
453
|
-
behavior: 'smooth',
|
|
454
|
-
block: 'center'
|
|
455
|
-
});
|
|
503
|
+
showSelection(doc, uid, scroll);
|
|
456
504
|
};
|
|
457
505
|
// Capture phase, so a block that is also a link selects instead of
|
|
458
506
|
// navigating away.
|
|
@@ -512,11 +560,11 @@ const createBridge = (win, cmsOrigin)=>{
|
|
|
512
560
|
bridge.prompt = (prompt, options = {})=>{
|
|
513
561
|
const text = prompt.trim();
|
|
514
562
|
if (!text) return Promise.reject(new Error('Say what to change'));
|
|
515
|
-
if (!
|
|
563
|
+
if (!getState().features.includes('prompt')) {
|
|
516
564
|
return Promise.reject(new Error('The editor does not offer prompts here'));
|
|
517
565
|
}
|
|
518
566
|
const id = `prompt_${++nextId}`;
|
|
519
|
-
const uid = options.uid === undefined ?
|
|
567
|
+
const uid = options.uid === undefined ? getState().selectedUid : options.uid;
|
|
520
568
|
return new Promise((resolve, reject)=>{
|
|
521
569
|
const timer = setTimeout(()=>{
|
|
522
570
|
settle(id)?.reject(new Error('The editor did not answer in time'));
|
|
@@ -543,17 +591,11 @@ const createBridge = (win, cmsOrigin)=>{
|
|
|
543
591
|
uid
|
|
544
592
|
});
|
|
545
593
|
};
|
|
546
|
-
|
|
547
|
-
let overlay = null;
|
|
548
|
-
const showStats = ()=>{
|
|
549
|
-
if (!overlay && !Object.keys(state.stats).length) return;
|
|
550
|
-
overlay ??= mountStatsOverlay(win, {
|
|
594
|
+
const stats = createStatsView(()=>mountStatsOverlay(win, {
|
|
551
595
|
selector: blockSelector,
|
|
552
596
|
onSelect: (uid)=>bridge.select(uid)
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
};
|
|
556
|
-
cleanup.push(()=>overlay?.destroy());
|
|
597
|
+
}));
|
|
598
|
+
cleanup.push(()=>stats.destroy());
|
|
557
599
|
cleanup.push(()=>{
|
|
558
600
|
for (const id of pending.keys()){
|
|
559
601
|
settle(id)?.reject(new Error('The bridge was destroyed'));
|
|
@@ -570,29 +612,31 @@ const createBridge = (win, cmsOrigin)=>{
|
|
|
570
612
|
// have been sent before it was listening.
|
|
571
613
|
ready();
|
|
572
614
|
} else if (data.type === 'update' && data.entry) {
|
|
573
|
-
|
|
615
|
+
emit('update', {
|
|
574
616
|
entry: data.entry,
|
|
575
617
|
selectedUid: data.selectedUid ?? null
|
|
576
618
|
});
|
|
577
619
|
// Re-apply the selection to whatever the site rendered.
|
|
578
620
|
win.requestAnimationFrame(()=>highlight(data.selectedUid, false));
|
|
579
|
-
|
|
621
|
+
stats.refresh();
|
|
580
622
|
} else if (data.type === 'select') {
|
|
581
623
|
highlight(data.uid, true);
|
|
582
624
|
} else if (data.type === 'saved' || data.type === 'published') {
|
|
583
|
-
|
|
584
|
-
if (
|
|
625
|
+
emit(data.type, {});
|
|
626
|
+
if (listeners.update.size === 0) win.location.reload();
|
|
585
627
|
} else if (data.type === 'features' && Array.isArray(data.features)) {
|
|
586
628
|
setState({
|
|
587
629
|
features: data.features.filter((feature)=>typeof feature === 'string')
|
|
588
630
|
});
|
|
589
631
|
} else if (data.type === 'stats') {
|
|
590
632
|
// An editor from before the switch sends no `visible`: show them.
|
|
591
|
-
|
|
633
|
+
const next = {
|
|
592
634
|
stats: readStats(data.stats),
|
|
593
635
|
statsVisible: data.visible !== false
|
|
594
|
-
}
|
|
595
|
-
|
|
636
|
+
};
|
|
637
|
+
setState(next);
|
|
638
|
+
const { stats: rates, statsVisible } = getState();
|
|
639
|
+
stats.update(rates, statsVisible);
|
|
596
640
|
} else if (data.type === 'viewport' && typeof data.height === 'number') {
|
|
597
641
|
// The canvas: the frame is as tall as the page (`size`), so `vh`
|
|
598
642
|
// sizes are fixed at the screen height it stands for.
|
|
@@ -625,6 +669,7 @@ const createBridge = (win, cmsOrigin)=>{
|
|
|
625
669
|
}
|
|
626
670
|
return bridge;
|
|
627
671
|
};
|
|
672
|
+
|
|
628
673
|
const wrapScriptBridge = (script, params)=>{
|
|
629
674
|
const emitter = createEmitter();
|
|
630
675
|
script.on('saved', ()=>emitter.emit('saved', {}));
|
|
@@ -633,12 +678,11 @@ const wrapScriptBridge = (script, params)=>{
|
|
|
633
678
|
// no `update` listener, and has no way to remove one.
|
|
634
679
|
let forwardingUpdates = false;
|
|
635
680
|
return {
|
|
681
|
+
...createInertBridge(),
|
|
636
682
|
isEditing: script.isEditing,
|
|
637
683
|
mode: script.isEditing ? 'editor' : null,
|
|
638
684
|
entryId: script.entryId,
|
|
639
685
|
environment: params.get(PREVIEW_PARAMS.environment),
|
|
640
|
-
product: null,
|
|
641
|
-
collection: null,
|
|
642
686
|
on: (event, listener)=>{
|
|
643
687
|
if (event === 'update' && !forwardingUpdates) {
|
|
644
688
|
forwardingUpdates = true;
|
|
@@ -651,10 +695,7 @@ const wrapScriptBridge = (script, params)=>{
|
|
|
651
695
|
});
|
|
652
696
|
}
|
|
653
697
|
return emitter.on(event, listener);
|
|
654
|
-
}
|
|
655
|
-
getState: ()=>INITIAL_STATE,
|
|
656
|
-
...INERT_METHODS,
|
|
657
|
-
destroy: ()=>{}
|
|
698
|
+
}
|
|
658
699
|
};
|
|
659
700
|
};
|
|
660
701
|
|
|
@@ -689,6 +730,11 @@ const svgSpan = (doc, className, paths)=>{
|
|
|
689
730
|
};
|
|
690
731
|
// `{name}` style placeholders.
|
|
691
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
|
+
};
|
|
692
738
|
// Line icons (Lucide), drawn in the text colour.
|
|
693
739
|
const ICONS = {
|
|
694
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"/>',
|
|
@@ -999,6 +1045,55 @@ const createPagesPicker = (doc, bridge, labels, { onLoad, onClose })=>{
|
|
|
999
1045
|
};
|
|
1000
1046
|
};
|
|
1001
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
|
+
|
|
1002
1097
|
// What the prompt box and the on-site editor decide alike on the site.
|
|
1003
1098
|
const failure = (error)=>error instanceof Error ? error.message : String(error);
|
|
1004
1099
|
const pageContext = (bridge, state, promptWithoutPage)=>{
|
|
@@ -1009,39 +1104,26 @@ const pageContext = (bridge, state, promptWithoutPage)=>{
|
|
|
1009
1104
|
const pageless = site && bridge.entryId === null;
|
|
1010
1105
|
// …unless it shows a product (collection): then Add a product
|
|
1011
1106
|
// (collection) page first.
|
|
1012
|
-
const
|
|
1013
|
-
const onlyNew = pageless && canCreate && !
|
|
1107
|
+
const shop = pageless && canCreate ? shopKindOf(bridge) : null;
|
|
1108
|
+
const onlyNew = pageless && canCreate && !shop && !promptWithoutPage;
|
|
1014
1109
|
return {
|
|
1015
1110
|
site,
|
|
1016
1111
|
canCreate,
|
|
1017
1112
|
pageless,
|
|
1018
|
-
|
|
1113
|
+
shop,
|
|
1019
1114
|
onlyNew
|
|
1020
1115
|
};
|
|
1021
1116
|
};
|
|
1022
|
-
const shopCopy = (labels, shopHere)=>shopHere === 'collection' ? {
|
|
1023
|
-
add: labels.addCollectionPage,
|
|
1024
|
-
adding: labels.addingCollectionPage,
|
|
1025
|
-
hint: labels.collectionPageHint
|
|
1026
|
-
} : {
|
|
1027
|
-
add: labels.addProductPage,
|
|
1028
|
-
adding: labels.addingProductPage,
|
|
1029
|
-
hint: labels.productPageHint
|
|
1030
|
-
};
|
|
1031
1117
|
// Makes the page's product (collection) page; the bridge then reloads, and
|
|
1032
1118
|
// the answer shows until it does.
|
|
1033
1119
|
const addShopPage = async (bridge, labels)=>{
|
|
1120
|
+
const kind = shopKindOf(bridge);
|
|
1121
|
+
if (!kind) return null;
|
|
1034
1122
|
try {
|
|
1035
|
-
if (bridge.
|
|
1036
|
-
|
|
1037
|
-
return {
|
|
1038
|
-
text: labels.addedProductPage,
|
|
1039
|
-
error: false
|
|
1040
|
-
};
|
|
1041
|
-
}
|
|
1042
|
-
await bridge.createCollectionPage();
|
|
1123
|
+
if (kind === 'product') await bridge.createProductPage();
|
|
1124
|
+
else await bridge.createCollectionPage();
|
|
1043
1125
|
return {
|
|
1044
|
-
text: labels.
|
|
1126
|
+
text: labels[SHOP[kind].added],
|
|
1045
1127
|
error: false
|
|
1046
1128
|
};
|
|
1047
1129
|
} catch (error) {
|
|
@@ -1061,6 +1143,36 @@ const storeChanges = async (bridge, action)=>{
|
|
|
1061
1143
|
}
|
|
1062
1144
|
};
|
|
1063
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
|
+
|
|
1064
1176
|
const SIDE_KEY = 'obelism-content-prompt-side';
|
|
1065
1177
|
// How far the pointer moves before it drags the box, not clicks it.
|
|
1066
1178
|
const DRAG_THRESHOLD_PX = 6;
|
|
@@ -1642,26 +1754,29 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1642
1754
|
error: true
|
|
1643
1755
|
});
|
|
1644
1756
|
const render = (state)=>{
|
|
1645
|
-
const
|
|
1646
|
-
|
|
1647
|
-
|
|
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;
|
|
1648
1768
|
root.hidden = !available && !site;
|
|
1649
1769
|
pill.hidden = open || !available;
|
|
1650
1770
|
viewing.hidden = available || !site;
|
|
1651
1771
|
panel.hidden = !open || !available;
|
|
1652
|
-
const
|
|
1653
|
-
|
|
1654
|
-
const shopCopy$1 = shopCopy(labels, shopHere);
|
|
1655
|
-
addProduct.lastChild.textContent = shopCopy$1.add;
|
|
1656
|
-
if (!canCreate) creating = false;
|
|
1657
|
-
if (onlyNew) creating = true;
|
|
1658
|
-
if (creating || pageless || !site) form = false;
|
|
1772
|
+
const words = page.shop ? shopCopy(labels, page.shop) : null;
|
|
1773
|
+
addProduct.lastChild.textContent = words?.add ?? labels.addProductPage;
|
|
1659
1774
|
// The form is there only while shown in the open panel.
|
|
1660
1775
|
const showForm = open && available && form;
|
|
1661
1776
|
if (showForm && !formSlot.mounted) formSlot.mount();
|
|
1662
1777
|
else if (!showForm && formSlot.mounted) formSlot.unmount();
|
|
1663
|
-
const offerProduct =
|
|
1664
|
-
pillText.textContent = offerProduct ?
|
|
1778
|
+
const offerProduct = offerShop;
|
|
1779
|
+
pillText.textContent = offerProduct ? words.add : onlyNew ? labels.newPage : labels.open;
|
|
1665
1780
|
cancelNew.hidden = onlyNew;
|
|
1666
1781
|
scope.hidden = creating || offerProduct;
|
|
1667
1782
|
newScope.hidden = !creating;
|
|
@@ -1678,7 +1793,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1678
1793
|
formLoader.hidden = !formSlot.loading;
|
|
1679
1794
|
toolbar.hidden = picking;
|
|
1680
1795
|
pagesSection.hidden = !picking;
|
|
1681
|
-
input.placeholder =
|
|
1796
|
+
input.placeholder = page.placeholder;
|
|
1682
1797
|
scopeText.textContent = state.selectedUid ? labels.scopeBlock : pageless ? labels.scopeNoPage : labels.scopeEntry;
|
|
1683
1798
|
clearBlock.hidden = !state.selectedUid;
|
|
1684
1799
|
pageIcon.hidden = !!state.selectedUid;
|
|
@@ -1701,16 +1816,16 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1701
1816
|
minimize.setAttribute('aria-expanded', String(!minimized));
|
|
1702
1817
|
minimizeIcon.hidden = minimized;
|
|
1703
1818
|
expandIcon.hidden = !minimized;
|
|
1704
|
-
send.disabled =
|
|
1819
|
+
send.disabled = !page.canSend(input.value, pathInput.value);
|
|
1705
1820
|
input.disabled = busy;
|
|
1706
|
-
save.hidden = !
|
|
1821
|
+
save.hidden = !page.canSave;
|
|
1707
1822
|
save.disabled = busy || !state.unsaved;
|
|
1708
1823
|
save.textContent = state.saving ? labels.saving : labels.save;
|
|
1709
|
-
publish.hidden = !
|
|
1824
|
+
publish.hidden = !page.canPublish;
|
|
1710
1825
|
publish.disabled = busy;
|
|
1711
1826
|
publish.textContent = state.publishing ? labels.publishing : labels.publish;
|
|
1712
|
-
signOut.hidden = !
|
|
1713
|
-
pillSignOut.hidden =
|
|
1827
|
+
signOut.hidden = !page.showSignOut;
|
|
1828
|
+
pillSignOut.hidden = !page.showSignOut;
|
|
1714
1829
|
if (options.actions) {
|
|
1715
1830
|
options.actions.slot = open && available ? 'actions-footer' : 'actions-bar';
|
|
1716
1831
|
}
|
|
@@ -1722,16 +1837,12 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1722
1837
|
const loadingForm = form && formSlot.loading && !picking;
|
|
1723
1838
|
const working = state.prompting;
|
|
1724
1839
|
// A form opening says so in its own place.
|
|
1725
|
-
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 : '');
|
|
1726
1841
|
status.textContent = note;
|
|
1727
1842
|
status.hidden = !note || minimized;
|
|
1728
1843
|
status.className = `status${working ? ' working' : ''}${!busy && !loadingForm && message?.error ? ' error' : ''}`;
|
|
1729
1844
|
};
|
|
1730
1845
|
const refresh = ()=>render(bridge.getState());
|
|
1731
|
-
const grow = ()=>{
|
|
1732
|
-
input.style.height = 'auto';
|
|
1733
|
-
input.style.height = `${input.scrollHeight}px`;
|
|
1734
|
-
};
|
|
1735
1846
|
const setForm = (next)=>{
|
|
1736
1847
|
if (form === next) return;
|
|
1737
1848
|
form = next;
|
|
@@ -1763,7 +1874,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1763
1874
|
};
|
|
1764
1875
|
}
|
|
1765
1876
|
input.value = '';
|
|
1766
|
-
|
|
1877
|
+
fitToText(input);
|
|
1767
1878
|
} catch (error) {
|
|
1768
1879
|
message = failure$1(error);
|
|
1769
1880
|
}
|
|
@@ -1859,7 +1970,7 @@ const mountPromptBox = (doc, bridge, options = {})=>{
|
|
|
1859
1970
|
void submit();
|
|
1860
1971
|
});
|
|
1861
1972
|
input.addEventListener('input', ()=>{
|
|
1862
|
-
|
|
1973
|
+
fitToText(input);
|
|
1863
1974
|
refresh();
|
|
1864
1975
|
});
|
|
1865
1976
|
input.addEventListener('keydown', (event)=>{
|
|
@@ -2011,17 +2122,8 @@ const webUrl = (url)=>{
|
|
|
2011
2122
|
};
|
|
2012
2123
|
// The base path of the site's handler while signed in to edit, else null.
|
|
2013
2124
|
const readSiteEditing = (doc)=>{
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
if (name !== SITE_EDITING_COOKIE) continue;
|
|
2017
|
-
try {
|
|
2018
|
-
const basePath = decodeURIComponent(value.join('='));
|
|
2019
|
-
return BASE_PATH.test(basePath) ? basePath : null;
|
|
2020
|
-
} catch {
|
|
2021
|
-
return null;
|
|
2022
|
-
}
|
|
2023
|
-
}
|
|
2024
|
-
return null;
|
|
2125
|
+
const basePath = parseCookies(doc.cookie).get(SITE_EDITING_COOKIE);
|
|
2126
|
+
return basePath && BASE_PATH.test(basePath) ? basePath : null;
|
|
2025
2127
|
};
|
|
2026
2128
|
const same = (a, b)=>JSON.stringify(a) === JSON.stringify(b);
|
|
2027
2129
|
// Edits in the form further apart than this are separate undo steps.
|
|
@@ -2031,7 +2133,7 @@ const FORM_BURST_MS = 1000;
|
|
|
2031
2133
|
const FORM_RENDER_MS = 300;
|
|
2032
2134
|
const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/^\/+/, '')}`, productPath: ownProductPath, collectionPath: ownCollectionPath } = {})=>{
|
|
2033
2135
|
const doc = win.document;
|
|
2034
|
-
const
|
|
2136
|
+
const { on, getState, setState, emit, listeners } = createBridgeState();
|
|
2035
2137
|
const cleanup = [];
|
|
2036
2138
|
const working = new Map();
|
|
2037
2139
|
// Content before each prompt, newest last, for undo.
|
|
@@ -2039,21 +2141,20 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2039
2141
|
// Entries changed here since the last publish.
|
|
2040
2142
|
const touched = new Set();
|
|
2041
2143
|
let session = null;
|
|
2042
|
-
|
|
2043
|
-
|
|
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
|
+
};
|
|
2044
2153
|
// The open form's, if any; and when the form last changed content, for
|
|
2045
2154
|
// undo steps (-Infinity: the next edit starts a new step).
|
|
2046
2155
|
let closeForm = null;
|
|
2047
2156
|
let lastFormEdit = -Infinity;
|
|
2048
2157
|
const renderTimers = new Map();
|
|
2049
|
-
let state = INITIAL_STATE;
|
|
2050
|
-
const setState = (next)=>{
|
|
2051
|
-
state = {
|
|
2052
|
-
...state,
|
|
2053
|
-
...next
|
|
2054
|
-
};
|
|
2055
|
-
emitter.emit('change', state);
|
|
2056
|
-
};
|
|
2057
2158
|
const refreshUnsaved = ()=>setState({
|
|
2058
2159
|
unsaved: [
|
|
2059
2160
|
...working.values()
|
|
@@ -2086,26 +2187,6 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2086
2187
|
return data;
|
|
2087
2188
|
};
|
|
2088
2189
|
const pageEntryId = ()=>doc.querySelector(`[${ENTRY_ATTRIBUTE}]`)?.getAttribute(ENTRY_ATTRIBUTE) ?? null;
|
|
2089
|
-
// The product the page shows (editableProduct), if it marks one.
|
|
2090
|
-
const pageProduct = ()=>{
|
|
2091
|
-
const element = doc.querySelector(`[${PRODUCT_ATTRIBUTE}]`);
|
|
2092
|
-
const id = element?.getAttribute(PRODUCT_ATTRIBUTE);
|
|
2093
|
-
const handle = element?.getAttribute(PRODUCT_HANDLE_ATTRIBUTE);
|
|
2094
|
-
return id && handle ? {
|
|
2095
|
-
id,
|
|
2096
|
-
handle
|
|
2097
|
-
} : null;
|
|
2098
|
-
};
|
|
2099
|
-
// The collection the page shows (editableCollection), if it marks one.
|
|
2100
|
-
const pageCollection = ()=>{
|
|
2101
|
-
const element = doc.querySelector(`[${COLLECTION_ATTRIBUTE}]`);
|
|
2102
|
-
const id = element?.getAttribute(COLLECTION_ATTRIBUTE);
|
|
2103
|
-
const handle = element?.getAttribute(COLLECTION_HANDLE_ATTRIBUTE);
|
|
2104
|
-
return id && handle ? {
|
|
2105
|
-
id,
|
|
2106
|
-
handle
|
|
2107
|
-
} : null;
|
|
2108
|
-
};
|
|
2109
2190
|
const entryFor = (uid)=>{
|
|
2110
2191
|
const block = uid ? doc.querySelector(blockSelector(uid)) : null;
|
|
2111
2192
|
return block?.closest(`[${ENTRY_ATTRIBUTE}]`)?.getAttribute(ENTRY_ATTRIBUTE) ?? pageEntryId();
|
|
@@ -2128,7 +2209,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2128
2209
|
if (loaded) return loaded;
|
|
2129
2210
|
let pending = loading.get(entryId);
|
|
2130
2211
|
if (!pending) {
|
|
2131
|
-
pending = api(
|
|
2212
|
+
pending = api(siteApi.entry(entryId)).then((entry)=>{
|
|
2132
2213
|
const fresh = working.get(entryId) ?? {
|
|
2133
2214
|
content: entry.content,
|
|
2134
2215
|
saved: entry.content,
|
|
@@ -2144,7 +2225,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2144
2225
|
// The entry a prompt or JSON acts on, for the block `uid` (the selected
|
|
2145
2226
|
// one when left out).
|
|
2146
2227
|
const target = (option)=>{
|
|
2147
|
-
const uid = option === undefined ?
|
|
2228
|
+
const uid = option === undefined ? getState().selectedUid : option;
|
|
2148
2229
|
const entryId = entryFor(uid);
|
|
2149
2230
|
if (!entryId) {
|
|
2150
2231
|
throw new Error('Nothing on this page is marked as an entry: add editable(entry)');
|
|
@@ -2158,38 +2239,38 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2158
2239
|
// blocks: only their rates show (statsOverlay.ts), so a few can be
|
|
2159
2240
|
// compared without every tag at once.
|
|
2160
2241
|
const revealed = new Set();
|
|
2161
|
-
let overlay = null;
|
|
2162
2242
|
const asked = new Set();
|
|
2163
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
|
+
}));
|
|
2164
2250
|
const highlight = (uid)=>{
|
|
2165
|
-
|
|
2166
|
-
element.removeAttribute('data-cms-selected');
|
|
2167
|
-
});
|
|
2168
|
-
if (state.selectedUid !== uid) setState({
|
|
2251
|
+
if (getState().selectedUid !== uid) setState({
|
|
2169
2252
|
selectedUid: uid
|
|
2170
2253
|
});
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
overlay?.refresh();
|
|
2176
|
-
}
|
|
2254
|
+
showSelection(doc, uid);
|
|
2255
|
+
if (uid && !revealed.has(uid)) {
|
|
2256
|
+
revealed.add(uid);
|
|
2257
|
+
stats.refresh();
|
|
2177
2258
|
}
|
|
2178
2259
|
};
|
|
2179
2260
|
// Shows the in-memory content through `update`, shaped like the delivery
|
|
2180
2261
|
// API. A page without `update` listeners shows it once saved (it reloads).
|
|
2181
2262
|
const render = async (entryId)=>{
|
|
2182
|
-
if (
|
|
2263
|
+
if (listeners.update.size === 0) return;
|
|
2183
2264
|
const content = working.get(entryId)?.content;
|
|
2184
|
-
const { entry } = await api(
|
|
2265
|
+
const { entry } = await api(siteApi.preview(entryId), {
|
|
2185
2266
|
content,
|
|
2186
2267
|
locale: locale()
|
|
2187
2268
|
});
|
|
2188
|
-
|
|
2269
|
+
emit('update', {
|
|
2189
2270
|
entry,
|
|
2190
|
-
selectedUid:
|
|
2271
|
+
selectedUid: getState().selectedUid
|
|
2191
2272
|
});
|
|
2192
|
-
win.requestAnimationFrame(()=>highlight(
|
|
2273
|
+
win.requestAnimationFrame(()=>highlight(getState().selectedUid));
|
|
2193
2274
|
};
|
|
2194
2275
|
const logError = (error)=>console.error('[@obelism/content bridge]', error);
|
|
2195
2276
|
// Renders once the form's edits settle.
|
|
@@ -2201,13 +2282,13 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2201
2282
|
}, FORM_RENDER_MS));
|
|
2202
2283
|
};
|
|
2203
2284
|
const afterStore = (event)=>{
|
|
2204
|
-
|
|
2205
|
-
if (
|
|
2285
|
+
emit(event, {});
|
|
2286
|
+
if (listeners.update.size === 0) win.location.reload();
|
|
2206
2287
|
};
|
|
2207
2288
|
const saveAll = async ()=>{
|
|
2208
2289
|
for (const [entryId, entry] of working){
|
|
2209
2290
|
if (same(entry.content, entry.saved)) continue;
|
|
2210
|
-
const { draftRevision } = await api(
|
|
2291
|
+
const { draftRevision } = await api(siteApi.save(entryId), {
|
|
2211
2292
|
content: entry.content,
|
|
2212
2293
|
baseRevision: entry.revision
|
|
2213
2294
|
});
|
|
@@ -2219,7 +2300,36 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2219
2300
|
lastFormEdit = -Infinity;
|
|
2220
2301
|
refreshUnsaved();
|
|
2221
2302
|
};
|
|
2222
|
-
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
|
+
};
|
|
2223
2333
|
const bridge = {
|
|
2224
2334
|
isEditing: true,
|
|
2225
2335
|
mode: 'site',
|
|
@@ -2227,16 +2337,16 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2227
2337
|
return pageEntryId();
|
|
2228
2338
|
},
|
|
2229
2339
|
get product () {
|
|
2230
|
-
return
|
|
2340
|
+
return pageShopItem(doc, 'product');
|
|
2231
2341
|
},
|
|
2232
2342
|
get collection () {
|
|
2233
|
-
return
|
|
2343
|
+
return pageShopItem(doc, 'collection');
|
|
2234
2344
|
},
|
|
2235
2345
|
get environment () {
|
|
2236
2346
|
return session?.environment ?? null;
|
|
2237
2347
|
},
|
|
2238
|
-
on
|
|
2239
|
-
getState
|
|
2348
|
+
on,
|
|
2349
|
+
getState,
|
|
2240
2350
|
async prompt (prompt, options = {}) {
|
|
2241
2351
|
const text = prompt.trim();
|
|
2242
2352
|
if (!text) throw new Error('Say what to change');
|
|
@@ -2247,7 +2357,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2247
2357
|
});
|
|
2248
2358
|
try {
|
|
2249
2359
|
const entry = await load(entryId);
|
|
2250
|
-
const result = await api(
|
|
2360
|
+
const result = await api(siteApi.prompt(entryId), {
|
|
2251
2361
|
prompt: text,
|
|
2252
2362
|
content: entry.content,
|
|
2253
2363
|
locale: locale(),
|
|
@@ -2276,14 +2386,14 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2276
2386
|
if (!path.trim()) throw new Error('Say where the page goes, like /pricing');
|
|
2277
2387
|
if (!has('create')) throw new Error('You may not add pages here');
|
|
2278
2388
|
// Opening the new page would lose them.
|
|
2279
|
-
if (
|
|
2389
|
+
if (getState().unsaved) {
|
|
2280
2390
|
throw new Error('Save or undo your changes before adding a page');
|
|
2281
2391
|
}
|
|
2282
2392
|
setState({
|
|
2283
2393
|
prompting: true
|
|
2284
2394
|
});
|
|
2285
2395
|
try {
|
|
2286
|
-
const created = await api(
|
|
2396
|
+
const created = await api(siteApi.createEntry(), {
|
|
2287
2397
|
path,
|
|
2288
2398
|
prompt: text,
|
|
2289
2399
|
locale: locale(),
|
|
@@ -2303,66 +2413,12 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2303
2413
|
});
|
|
2304
2414
|
}
|
|
2305
2415
|
},
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
throw new Error('Nothing on this page is marked as a product: add editableProduct(product)');
|
|
2309
|
-
}
|
|
2310
|
-
if (!has('create')) throw new Error('You may not add pages here');
|
|
2311
|
-
if (state.unsaved) {
|
|
2312
|
-
throw new Error('Save or undo your changes before adding a page');
|
|
2313
|
-
}
|
|
2314
|
-
setState({
|
|
2315
|
-
prompting: true
|
|
2316
|
-
});
|
|
2317
|
-
try {
|
|
2318
|
-
const made = await api('product-pages', {
|
|
2319
|
-
product: {
|
|
2320
|
-
id: product.id,
|
|
2321
|
-
handle: product.handle
|
|
2322
|
-
}
|
|
2323
|
-
});
|
|
2324
|
-
// The site's product route finds its page now (drafts, while
|
|
2325
|
-
// editing): show it.
|
|
2326
|
-
win.location.reload();
|
|
2327
|
-
return made;
|
|
2328
|
-
} finally{
|
|
2329
|
-
setState({
|
|
2330
|
-
prompting: false
|
|
2331
|
-
});
|
|
2332
|
-
}
|
|
2333
|
-
},
|
|
2334
|
-
async createCollectionPage (collection = pageCollection() ?? undefined) {
|
|
2335
|
-
if (!collection) {
|
|
2336
|
-
throw new Error('Nothing on this page is marked as a collection: add editableCollection(collection)');
|
|
2337
|
-
}
|
|
2338
|
-
if (!has('create')) throw new Error('You may not add pages here');
|
|
2339
|
-
if (state.unsaved) {
|
|
2340
|
-
throw new Error('Save or undo your changes before adding a page');
|
|
2341
|
-
}
|
|
2342
|
-
setState({
|
|
2343
|
-
prompting: true
|
|
2344
|
-
});
|
|
2345
|
-
try {
|
|
2346
|
-
const made = await api('collection-pages', {
|
|
2347
|
-
collection: {
|
|
2348
|
-
id: collection.id,
|
|
2349
|
-
handle: collection.handle
|
|
2350
|
-
}
|
|
2351
|
-
});
|
|
2352
|
-
// The site's collection route finds its page now (drafts, while
|
|
2353
|
-
// editing): show it.
|
|
2354
|
-
win.location.reload();
|
|
2355
|
-
return made;
|
|
2356
|
-
} finally{
|
|
2357
|
-
setState({
|
|
2358
|
-
prompting: false
|
|
2359
|
-
});
|
|
2360
|
-
}
|
|
2361
|
-
},
|
|
2416
|
+
createProductPage: (product = pageShopItem(doc, 'product') ?? undefined)=>createShopPage('product', product),
|
|
2417
|
+
createCollectionPage: (collection = pageShopItem(doc, 'collection') ?? undefined)=>createShopPage('collection', collection),
|
|
2362
2418
|
async view (options = {}) {
|
|
2363
2419
|
const { uid, entryId } = target(options.uid);
|
|
2364
2420
|
const entry = await load(entryId);
|
|
2365
|
-
const { view } = await api(
|
|
2421
|
+
const { view } = await api(siteApi.view(entryId), {
|
|
2366
2422
|
content: entry.content,
|
|
2367
2423
|
locale: locale(),
|
|
2368
2424
|
uid
|
|
@@ -2380,7 +2436,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2380
2436
|
});
|
|
2381
2437
|
try {
|
|
2382
2438
|
const entry = await load(entryId);
|
|
2383
|
-
const result = await api(
|
|
2439
|
+
const result = await api(siteApi.apply(entryId), {
|
|
2384
2440
|
changes,
|
|
2385
2441
|
content: entry.content,
|
|
2386
2442
|
locale: locale(),
|
|
@@ -2432,7 +2488,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2432
2488
|
// changed: prompts, undo, a reload after saving, the selection.
|
|
2433
2489
|
const show = ()=>{
|
|
2434
2490
|
if (closed || !frame) return;
|
|
2435
|
-
const entryId = entryFor(
|
|
2491
|
+
const entryId = entryFor(getState().selectedUid);
|
|
2436
2492
|
if (!entryId) return;
|
|
2437
2493
|
const entry = working.get(entryId);
|
|
2438
2494
|
if (!entry) {
|
|
@@ -2443,7 +2499,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2443
2499
|
entryId,
|
|
2444
2500
|
content: entry.content,
|
|
2445
2501
|
locale: locale(),
|
|
2446
|
-
selectedUid:
|
|
2502
|
+
selectedUid: getState().selectedUid
|
|
2447
2503
|
};
|
|
2448
2504
|
if (shown && shown.entryId === next.entryId && shown.content === next.content && shown.locale === next.locale && shown.selectedUid === next.selectedUid) {
|
|
2449
2505
|
return;
|
|
@@ -2478,7 +2534,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2478
2534
|
if (!has('form')) throw new Error('You may not edit content here');
|
|
2479
2535
|
await load(target(undefined).entryId);
|
|
2480
2536
|
// A fresh one every time: the address works once, for a minute.
|
|
2481
|
-
const { url } = await api(
|
|
2537
|
+
const { url } = await api(siteApi.form(), {});
|
|
2482
2538
|
if (closed) return;
|
|
2483
2539
|
const address = typeof url === 'string' ? url : '';
|
|
2484
2540
|
frame = mountFormFrame(win, container, address, title, {
|
|
@@ -2487,7 +2543,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2487
2543
|
onError: (message)=>onError?.(message),
|
|
2488
2544
|
onEscape: ()=>onEscape?.()
|
|
2489
2545
|
});
|
|
2490
|
-
unsubscribe =
|
|
2546
|
+
unsubscribe = on('change', show);
|
|
2491
2547
|
show();
|
|
2492
2548
|
};
|
|
2493
2549
|
start().catch(fail);
|
|
@@ -2496,7 +2552,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2496
2552
|
};
|
|
2497
2553
|
},
|
|
2498
2554
|
async pages () {
|
|
2499
|
-
const { pages } = await api(
|
|
2555
|
+
const { pages } = await api(siteApi.pages());
|
|
2500
2556
|
const preferred = [
|
|
2501
2557
|
locale(),
|
|
2502
2558
|
session?.space.defaultLocale
|
|
@@ -2506,6 +2562,8 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2506
2562
|
const code = preferred.find((code)=>code !== undefined && page.paths[code] !== undefined) ?? Object.keys(page.paths)[0];
|
|
2507
2563
|
const path = code === undefined ? undefined : page.paths[code];
|
|
2508
2564
|
if (code === undefined || path === undefined) return [];
|
|
2565
|
+
const shop = shopKindOf(page);
|
|
2566
|
+
const item = shopItemOf(page);
|
|
2509
2567
|
return [
|
|
2510
2568
|
{
|
|
2511
2569
|
entryId: page.entryId,
|
|
@@ -2513,7 +2571,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2513
2571
|
contentType: page.contentType,
|
|
2514
2572
|
status: page.status,
|
|
2515
2573
|
path,
|
|
2516
|
-
url:
|
|
2574
|
+
url: shop && item ? shopPath(shop, item.handle, code) : pagePath(path, code),
|
|
2517
2575
|
product: page.product ?? null,
|
|
2518
2576
|
collection: page.collection ?? null
|
|
2519
2577
|
}
|
|
@@ -2521,7 +2579,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2521
2579
|
});
|
|
2522
2580
|
},
|
|
2523
2581
|
openPage (page) {
|
|
2524
|
-
if (
|
|
2582
|
+
if (getState().unsaved && !win.confirm('Leave this page and lose the changes you have not saved?')) {
|
|
2525
2583
|
return;
|
|
2526
2584
|
}
|
|
2527
2585
|
// Asked already: no second question from `beforeunload`.
|
|
@@ -2570,7 +2628,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2570
2628
|
throw new Error('Nothing on this page is marked as an entry: add editable(entry)');
|
|
2571
2629
|
}
|
|
2572
2630
|
for (const entryId of entries){
|
|
2573
|
-
await api(
|
|
2631
|
+
await api(siteApi.publish(entryId), {});
|
|
2574
2632
|
touched.delete(entryId);
|
|
2575
2633
|
}
|
|
2576
2634
|
} finally{
|
|
@@ -2581,7 +2639,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2581
2639
|
afterStore('published');
|
|
2582
2640
|
},
|
|
2583
2641
|
signOut () {
|
|
2584
|
-
if (
|
|
2642
|
+
if (getState().unsaved && !win.confirm('Sign out and lose the changes you have not saved?')) {
|
|
2585
2643
|
return;
|
|
2586
2644
|
}
|
|
2587
2645
|
working.clear();
|
|
@@ -2616,7 +2674,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2616
2674
|
// Capture phase, so a block that is also a link selects instead of
|
|
2617
2675
|
// navigating — only while selecting, so the site stays usable.
|
|
2618
2676
|
const onClick = (event)=>{
|
|
2619
|
-
if (!
|
|
2677
|
+
if (!getState().selecting) return;
|
|
2620
2678
|
const target = event.target;
|
|
2621
2679
|
const element = target?.closest?.(`[${UID_ATTRIBUTE}]`);
|
|
2622
2680
|
// A tagged element outside the blocks (a menu button) can't be
|
|
@@ -2628,7 +2686,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2628
2686
|
event.stopPropagation();
|
|
2629
2687
|
if (!revealed.has(tagId)) {
|
|
2630
2688
|
revealed.add(tagId);
|
|
2631
|
-
|
|
2689
|
+
stats.refresh();
|
|
2632
2690
|
}
|
|
2633
2691
|
return;
|
|
2634
2692
|
}
|
|
@@ -2640,7 +2698,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2640
2698
|
doc.addEventListener('click', onClick, true);
|
|
2641
2699
|
cleanup.push(()=>doc.removeEventListener('click', onClick, true));
|
|
2642
2700
|
const onBeforeUnload = (event)=>{
|
|
2643
|
-
if (!
|
|
2701
|
+
if (!getState().unsaved) return;
|
|
2644
2702
|
event.preventDefault();
|
|
2645
2703
|
event.returnValue = '';
|
|
2646
2704
|
};
|
|
@@ -2651,7 +2709,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2651
2709
|
// as they appear.
|
|
2652
2710
|
let statsOff = false;
|
|
2653
2711
|
let statsTimer = null;
|
|
2654
|
-
const showStats = ()=>
|
|
2712
|
+
const showStats = ()=>stats.update(getState().stats, getState().statsVisible);
|
|
2655
2713
|
const loadStats = async ()=>{
|
|
2656
2714
|
if (statsOff) return;
|
|
2657
2715
|
const ids = [
|
|
@@ -2669,7 +2727,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2669
2727
|
].map((element)=>element.getAttribute(ENTRY_ATTRIBUTE) ?? ''))
|
|
2670
2728
|
].filter(Boolean).slice(0, MAX_STATS_ENTRIES);
|
|
2671
2729
|
try {
|
|
2672
|
-
const { stats } = await api(
|
|
2730
|
+
const { stats } = await api(siteApi.stats(), {
|
|
2673
2731
|
uids: batch,
|
|
2674
2732
|
...entries.length > 0 && {
|
|
2675
2733
|
entries
|
|
@@ -2678,7 +2736,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2678
2736
|
batch.forEach((id)=>answered.add(id));
|
|
2679
2737
|
setState({
|
|
2680
2738
|
stats: {
|
|
2681
|
-
...
|
|
2739
|
+
...getState().stats,
|
|
2682
2740
|
...readStats(stats)
|
|
2683
2741
|
}
|
|
2684
2742
|
});
|
|
@@ -2687,12 +2745,6 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2687
2745
|
statsOff = true;
|
|
2688
2746
|
return;
|
|
2689
2747
|
}
|
|
2690
|
-
overlay ??= mountStatsOverlay(win, {
|
|
2691
|
-
selector: blockSelector,
|
|
2692
|
-
onSelect: (uid)=>highlight(uid),
|
|
2693
|
-
checked: (id)=>answered.has(id),
|
|
2694
|
-
shows: (id, element)=>revealed.has(element.closest(`[${UID_ATTRIBUTE}]`)?.getAttribute(UID_ATTRIBUTE) ?? id)
|
|
2695
|
-
});
|
|
2696
2748
|
showStats();
|
|
2697
2749
|
if (ids.length > batch.length) scheduleStats();
|
|
2698
2750
|
};
|
|
@@ -2708,9 +2760,9 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2708
2760
|
cleanup.push(()=>{
|
|
2709
2761
|
statsObserver?.disconnect();
|
|
2710
2762
|
if (statsTimer !== null) clearTimeout(statsTimer);
|
|
2711
|
-
|
|
2763
|
+
stats.destroy();
|
|
2712
2764
|
});
|
|
2713
|
-
api(
|
|
2765
|
+
api(siteApi.session()).then((info)=>{
|
|
2714
2766
|
session = info;
|
|
2715
2767
|
setState({
|
|
2716
2768
|
features: info.features
|
|
@@ -3226,7 +3278,7 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3226
3278
|
});
|
|
3227
3279
|
const shopButtonText = element(doc, 'span');
|
|
3228
3280
|
const shopButton = button('button primary', icon('plus'), shopButtonText);
|
|
3229
|
-
const
|
|
3281
|
+
const shopBox = element(doc, 'div', {
|
|
3230
3282
|
className: 'shop'
|
|
3231
3283
|
}, shopText, shopButton);
|
|
3232
3284
|
const log = element(doc, 'div', {
|
|
@@ -3235,7 +3287,7 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3235
3287
|
log.setAttribute('role', 'log');
|
|
3236
3288
|
const scroll = element(doc, 'div', {
|
|
3237
3289
|
className: 'scroll'
|
|
3238
|
-
}, note, intro,
|
|
3290
|
+
}, note, intro, shopBox, log);
|
|
3239
3291
|
const pathInput = element(doc, 'input', {
|
|
3240
3292
|
type: 'text',
|
|
3241
3293
|
placeholder: labels.pathPlaceholder,
|
|
@@ -3642,14 +3694,18 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3642
3694
|
};
|
|
3643
3695
|
const render = (state)=>{
|
|
3644
3696
|
if (destroyed) return;
|
|
3645
|
-
const
|
|
3646
|
-
|
|
3647
|
-
|
|
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');
|
|
3648
3707
|
const shown = available && open;
|
|
3649
|
-
|
|
3650
|
-
if (!canCreate) creating = false;
|
|
3651
|
-
if (onlyNew) creating = true;
|
|
3652
|
-
const hasForm = site && !creating && !pageless && state.features.includes('form');
|
|
3708
|
+
creating = page.creating;
|
|
3653
3709
|
view = hasForm ? mode : 'ask';
|
|
3654
3710
|
const waiting = pending().length > 0;
|
|
3655
3711
|
if (!waiting || view === 'fields') gated = false;
|
|
@@ -3665,7 +3721,7 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3665
3721
|
eye.setAttribute('aria-pressed', String(!outlines));
|
|
3666
3722
|
eyeOn.hidden = !outlines;
|
|
3667
3723
|
eyeOff.hidden = outlines;
|
|
3668
|
-
exit.hidden =
|
|
3724
|
+
exit.hidden = !page.showSignOut;
|
|
3669
3725
|
// The site beside the panel, wide enough; a sheet over it otherwise.
|
|
3670
3726
|
const docked = shown && wide;
|
|
3671
3727
|
html.toggleAttribute('data-obelism-docked', docked);
|
|
@@ -3695,7 +3751,7 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3695
3751
|
gateKeep.disabled = gateUndo.disabled = busy;
|
|
3696
3752
|
ask.hidden = view !== 'ask';
|
|
3697
3753
|
fields.hidden = view !== 'fields';
|
|
3698
|
-
const offerShop =
|
|
3754
|
+
const offerShop = page.offerShop;
|
|
3699
3755
|
if (creating) {
|
|
3700
3756
|
noteText.textContent = labels.newPage;
|
|
3701
3757
|
} else if (state.selectedUid) {
|
|
@@ -3722,26 +3778,26 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3722
3778
|
up.hidden = creating || !state.selectedUid;
|
|
3723
3779
|
cancelNew.hidden = !creating || onlyNew;
|
|
3724
3780
|
intro.hidden = answers.length > 0 || offerShop || creating;
|
|
3725
|
-
|
|
3726
|
-
const shopWords = shopCopy(labels,
|
|
3727
|
-
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 : '';
|
|
3728
3784
|
shopButton.disabled = busy;
|
|
3729
|
-
shopText.textContent = shopMessage?.text ?? shopWords
|
|
3785
|
+
shopText.textContent = shopMessage?.text ?? shopWords?.hint ?? '';
|
|
3730
3786
|
shopText.className = shopMessage?.error ? 'failure' : 'summary';
|
|
3731
3787
|
composer.hidden = offerShop;
|
|
3732
3788
|
pathRow.hidden = !creating;
|
|
3733
3789
|
pathInput.disabled = busy;
|
|
3734
|
-
input.placeholder =
|
|
3790
|
+
input.placeholder = page.placeholder;
|
|
3735
3791
|
input.disabled = busy;
|
|
3736
|
-
send.disabled =
|
|
3792
|
+
send.disabled = !page.canSend(input.value, pathInput.value);
|
|
3737
3793
|
send.setAttribute('aria-busy', String(state.prompting));
|
|
3738
3794
|
sendIcon.hidden = state.prompting;
|
|
3739
3795
|
sendWorking.hidden = !state.prompting;
|
|
3740
3796
|
undo.hidden = waiting || !state.canUndo || creating;
|
|
3741
3797
|
undo.disabled = busy;
|
|
3742
|
-
save.hidden = !
|
|
3798
|
+
save.hidden = !page.canSave;
|
|
3743
3799
|
save.disabled = busy || waiting || !state.unsaved;
|
|
3744
|
-
publish.hidden = !
|
|
3800
|
+
publish.hidden = !page.canPublish;
|
|
3745
3801
|
publish.disabled = busy || waiting;
|
|
3746
3802
|
if (options.actions) options.actions.slot = shown ? 'actions-footer' : 'actions-bar';
|
|
3747
3803
|
foot.hidden = !site;
|
|
@@ -3779,10 +3835,6 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3779
3835
|
scheduleMarks();
|
|
3780
3836
|
};
|
|
3781
3837
|
const refresh = ()=>render(bridge.getState());
|
|
3782
|
-
const grow = ()=>{
|
|
3783
|
-
input.style.height = 'auto';
|
|
3784
|
-
input.style.height = `${input.scrollHeight}px`;
|
|
3785
|
-
};
|
|
3786
3838
|
const scrollDown = ()=>{
|
|
3787
3839
|
scroll.scrollTop = scroll.scrollHeight;
|
|
3788
3840
|
};
|
|
@@ -3911,7 +3963,7 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3911
3963
|
textContent: `${labels.you} (${scopeName}): `
|
|
3912
3964
|
}), prompt));
|
|
3913
3965
|
input.value = '';
|
|
3914
|
-
|
|
3966
|
+
fitToText(input);
|
|
3915
3967
|
const answer = addAnswer(prompt, uid);
|
|
3916
3968
|
refresh();
|
|
3917
3969
|
scrollDown();
|
|
@@ -3995,7 +4047,7 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3995
4047
|
void submit();
|
|
3996
4048
|
});
|
|
3997
4049
|
input.addEventListener('input', ()=>{
|
|
3998
|
-
|
|
4050
|
+
fitToText(input);
|
|
3999
4051
|
refresh();
|
|
4000
4052
|
});
|
|
4001
4053
|
input.addEventListener('keydown', (event)=>{
|
|
@@ -4084,11 +4136,8 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
4084
4136
|
// Kept on the window, so two copies of this package on one page still share
|
|
4085
4137
|
// one bridge.
|
|
4086
4138
|
const KEY = Symbol.for('@obelism/content/bridge');
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
entryId: null,
|
|
4090
|
-
on: ()=>{}
|
|
4091
|
-
}, new URLSearchParams());
|
|
4139
|
+
// The bridge on the server, where there is nothing to edit.
|
|
4140
|
+
const SERVER_BRIDGE = createInertBridge();
|
|
4092
4141
|
// The page's bridge if something already started it, else null. Never
|
|
4093
4142
|
// starts one: for code that has to stay side-effect free, like a React
|
|
4094
4143
|
// store's snapshot.
|