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