@obelism/content 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +112 -2
- package/dist/bin/content.js +216 -0
- package/dist/bridge.d.mts +82 -0
- package/dist/bridge.d.mts.map +1 -0
- package/dist/bridge.d.ts +82 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +920 -0
- package/dist/bridge.mjs +915 -0
- package/dist/config-c6qktlz6.mjs +8 -0
- package/dist/config-ci2hxmas.js +13 -0
- package/dist/index.d.mts +93 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +300 -0
- package/dist/index.mjs +284 -0
- package/dist/preview-by8d54dt.js +68 -0
- package/dist/preview-kvotryzw.mjs +64 -0
- package/dist/prompt.d.mts +26 -0
- package/dist/prompt.d.mts.map +1 -0
- package/dist/prompt.d.ts +26 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +112 -0
- package/dist/prompt.mjs +108 -0
- package/dist/site.d.mts +34 -0
- package/dist/site.d.mts.map +1 -0
- package/dist/site.d.ts +34 -0
- package/dist/site.d.ts.map +1 -0
- package/dist/site.js +310 -0
- package/dist/site.mjs +302 -0
- package/dist/types-pa96z6po.d.ts +120 -0
- package/dist/types-pa96z6po.d.ts.map +1 -0
- package/package.json +91 -5
package/dist/bridge.js
ADDED
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
2
|
+
|
|
3
|
+
var preview = require('./preview-by8d54dt.js');
|
|
4
|
+
var site = require('./site.js');
|
|
5
|
+
var config = require('./config-ci2hxmas.js');
|
|
6
|
+
|
|
7
|
+
// The site's half of the visual editor (the CMS's public/bridge.js, as a
|
|
8
|
+
// module). Inside the editor's iframe it outlines blocks, tells the editor
|
|
9
|
+
// which one was clicked, hands the editor's live, unsaved entry to listeners
|
|
10
|
+
// and sends the editor prompts. Outside the editor it does nothing.
|
|
11
|
+
//
|
|
12
|
+
// Messages are only exchanged with the CMS origin, and only with the parent
|
|
13
|
+
// window:
|
|
14
|
+
// - site → editor: { source: 'obelism-content-bridge', type: 'ready' |
|
|
15
|
+
// 'select' | 'prompt' | 'undo', uid?, id?, prompt? }
|
|
16
|
+
// - editor → site: { source: 'obelism-content-editor', type: 'connect' |
|
|
17
|
+
// 'update' | 'select' | 'saved' | 'published' | 'features' |
|
|
18
|
+
// 'promptResult' | 'promptState', … }
|
|
19
|
+
const BRIDGE_SOURCE = 'obelism-content-bridge';
|
|
20
|
+
const EDITOR_SOURCE = 'obelism-content-editor';
|
|
21
|
+
// How long a prompt may take before it counts as failed.
|
|
22
|
+
const PROMPT_TIMEOUT_MS = 120_000;
|
|
23
|
+
const OUTLINE = '#d58a1f';
|
|
24
|
+
// Outlines on blocks; `scope` limits them to e.g. `html[data-cms-selecting] `.
|
|
25
|
+
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}`;
|
|
26
|
+
const INITIAL_STATE = {
|
|
27
|
+
features: [],
|
|
28
|
+
selectedUid: null,
|
|
29
|
+
prompting: false,
|
|
30
|
+
canUndo: false,
|
|
31
|
+
unsaved: false,
|
|
32
|
+
saving: false,
|
|
33
|
+
publishing: false,
|
|
34
|
+
selecting: false
|
|
35
|
+
};
|
|
36
|
+
const createEmitter = ()=>{
|
|
37
|
+
const listeners = {
|
|
38
|
+
update: new Set(),
|
|
39
|
+
saved: new Set(),
|
|
40
|
+
published: new Set(),
|
|
41
|
+
change: new Set()
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
listeners,
|
|
45
|
+
on (event, listener) {
|
|
46
|
+
listeners[event].add(listener);
|
|
47
|
+
return ()=>{
|
|
48
|
+
listeners[event].delete(listener);
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
emit (event, payload) {
|
|
52
|
+
for (const listener of listeners[event]){
|
|
53
|
+
try {
|
|
54
|
+
listener(payload);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('[@obelism/content bridge]', error);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
const inertPrompt = ()=>Promise.reject(new Error('Prompts only work inside the visual editor'));
|
|
63
|
+
const onlyOnSite = ()=>Promise.reject(new Error('Save and publish in the visual editor itself'));
|
|
64
|
+
// What every bridge without a feature does for it.
|
|
65
|
+
const INERT_METHODS = {
|
|
66
|
+
prompt: inertPrompt,
|
|
67
|
+
undo: ()=>{},
|
|
68
|
+
save: onlyOnSite,
|
|
69
|
+
publish: onlyOnSite,
|
|
70
|
+
signOut: ()=>{},
|
|
71
|
+
select: ()=>{},
|
|
72
|
+
setSelecting: ()=>{}
|
|
73
|
+
};
|
|
74
|
+
// The selector of the element marked with a block's uid.
|
|
75
|
+
const blockSelector = (uid)=>`[${preview.UID_ATTRIBUTE}="${uid.replace(/["\\]/g, '\\$&')}"]`;
|
|
76
|
+
// `cmsOrigin` is the only origin messages are accepted from and sent to.
|
|
77
|
+
const createBridge = (win, cmsOrigin)=>{
|
|
78
|
+
const params = new URLSearchParams(win.location.search);
|
|
79
|
+
const isEditing = win.parent !== win && params.has(preview.PREVIEW_PARAMS.preview);
|
|
80
|
+
const emitter = createEmitter();
|
|
81
|
+
const cleanup = [];
|
|
82
|
+
let state = {
|
|
83
|
+
...INITIAL_STATE,
|
|
84
|
+
selecting: true
|
|
85
|
+
};
|
|
86
|
+
const setState = (next)=>{
|
|
87
|
+
state = {
|
|
88
|
+
...state,
|
|
89
|
+
...next
|
|
90
|
+
};
|
|
91
|
+
emitter.emit('change', state);
|
|
92
|
+
};
|
|
93
|
+
const bridge = {
|
|
94
|
+
isEditing,
|
|
95
|
+
mode: isEditing ? 'editor' : null,
|
|
96
|
+
entryId: params.get(preview.PREVIEW_PARAMS.entry),
|
|
97
|
+
environment: params.get(preview.PREVIEW_PARAMS.environment),
|
|
98
|
+
on: emitter.on,
|
|
99
|
+
getState: ()=>state,
|
|
100
|
+
...INERT_METHODS,
|
|
101
|
+
destroy: ()=>{
|
|
102
|
+
cleanup.splice(0).forEach((undo)=>undo());
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
if (!isEditing) return bridge;
|
|
106
|
+
const doc = win.document;
|
|
107
|
+
const send = (type, data = {})=>{
|
|
108
|
+
win.parent.postMessage({
|
|
109
|
+
source: BRIDGE_SOURCE,
|
|
110
|
+
type,
|
|
111
|
+
...data
|
|
112
|
+
}, cmsOrigin);
|
|
113
|
+
};
|
|
114
|
+
const style = doc.createElement('style');
|
|
115
|
+
style.textContent = selectionStyle();
|
|
116
|
+
doc.head.appendChild(style);
|
|
117
|
+
cleanup.push(()=>style.remove());
|
|
118
|
+
const highlight = (uid, scroll)=>{
|
|
119
|
+
doc.querySelectorAll('[data-cms-selected]').forEach((element)=>{
|
|
120
|
+
element.removeAttribute('data-cms-selected');
|
|
121
|
+
});
|
|
122
|
+
if (state.selectedUid !== (uid ?? null)) {
|
|
123
|
+
setState({
|
|
124
|
+
selectedUid: uid ?? null
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (!uid) return;
|
|
128
|
+
const element = doc.querySelector(blockSelector(uid));
|
|
129
|
+
if (!element) return;
|
|
130
|
+
element.setAttribute('data-cms-selected', '');
|
|
131
|
+
if (scroll) element.scrollIntoView({
|
|
132
|
+
behavior: 'smooth',
|
|
133
|
+
block: 'center'
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
// Capture phase, so a block that is also a link selects instead of
|
|
137
|
+
// navigating away.
|
|
138
|
+
const onClick = (event)=>{
|
|
139
|
+
const target = event.target;
|
|
140
|
+
const element = target?.closest?.(`[${preview.UID_ATTRIBUTE}]`);
|
|
141
|
+
if (!element) return;
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
event.stopPropagation();
|
|
144
|
+
const uid = element.getAttribute(preview.UID_ATTRIBUTE);
|
|
145
|
+
highlight(uid, false);
|
|
146
|
+
send('select', {
|
|
147
|
+
uid
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
doc.addEventListener('click', onClick, true);
|
|
151
|
+
cleanup.push(()=>doc.removeEventListener('click', onClick, true));
|
|
152
|
+
const ready = ()=>send('ready', {
|
|
153
|
+
url: win.location.href
|
|
154
|
+
});
|
|
155
|
+
// Prompts waiting for the editor's answer, by id.
|
|
156
|
+
const pending = new Map();
|
|
157
|
+
let nextId = 0;
|
|
158
|
+
const settle = (id)=>{
|
|
159
|
+
const waiting = pending.get(id);
|
|
160
|
+
if (!waiting) return null;
|
|
161
|
+
clearTimeout(waiting.timer);
|
|
162
|
+
pending.delete(id);
|
|
163
|
+
if (pending.size === 0) setState({
|
|
164
|
+
prompting: false
|
|
165
|
+
});
|
|
166
|
+
return waiting;
|
|
167
|
+
};
|
|
168
|
+
bridge.prompt = (prompt, options = {})=>{
|
|
169
|
+
const text = prompt.trim();
|
|
170
|
+
if (!text) return Promise.reject(new Error('Say what to change'));
|
|
171
|
+
if (!state.features.includes('prompt')) {
|
|
172
|
+
return Promise.reject(new Error('The editor does not offer prompts here'));
|
|
173
|
+
}
|
|
174
|
+
const id = `prompt_${++nextId}`;
|
|
175
|
+
const uid = options.uid === undefined ? state.selectedUid : options.uid;
|
|
176
|
+
return new Promise((resolve, reject)=>{
|
|
177
|
+
const timer = setTimeout(()=>{
|
|
178
|
+
settle(id)?.reject(new Error('The editor did not answer in time'));
|
|
179
|
+
}, PROMPT_TIMEOUT_MS);
|
|
180
|
+
pending.set(id, {
|
|
181
|
+
resolve,
|
|
182
|
+
reject,
|
|
183
|
+
timer
|
|
184
|
+
});
|
|
185
|
+
setState({
|
|
186
|
+
prompting: true
|
|
187
|
+
});
|
|
188
|
+
send('prompt', {
|
|
189
|
+
id,
|
|
190
|
+
prompt: text,
|
|
191
|
+
uid
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
bridge.undo = ()=>send('undo');
|
|
196
|
+
bridge.select = (uid)=>{
|
|
197
|
+
highlight(uid, false);
|
|
198
|
+
send('select', {
|
|
199
|
+
uid
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
cleanup.push(()=>{
|
|
203
|
+
for (const id of pending.keys()){
|
|
204
|
+
settle(id)?.reject(new Error('The bridge was destroyed'));
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
const onMessage = (event)=>{
|
|
208
|
+
if (event.origin !== cmsOrigin || event.source !== win.parent) return;
|
|
209
|
+
const data = event.data;
|
|
210
|
+
if (!data || data.source !== EDITOR_SOURCE) return;
|
|
211
|
+
if (data.type === 'connect') {
|
|
212
|
+
// The editor asks once its listener is up: the first `ready` may
|
|
213
|
+
// have been sent before it was listening.
|
|
214
|
+
ready();
|
|
215
|
+
} else if (data.type === 'update' && data.entry) {
|
|
216
|
+
emitter.emit('update', {
|
|
217
|
+
entry: data.entry,
|
|
218
|
+
selectedUid: data.selectedUid ?? null
|
|
219
|
+
});
|
|
220
|
+
// Re-apply the selection to whatever the site rendered.
|
|
221
|
+
win.requestAnimationFrame(()=>highlight(data.selectedUid, false));
|
|
222
|
+
} else if (data.type === 'select') {
|
|
223
|
+
highlight(data.uid, true);
|
|
224
|
+
} else if (data.type === 'saved' || data.type === 'published') {
|
|
225
|
+
emitter.emit(data.type, {});
|
|
226
|
+
if (emitter.listeners.update.size === 0) win.location.reload();
|
|
227
|
+
} else if (data.type === 'features' && Array.isArray(data.features)) {
|
|
228
|
+
setState({
|
|
229
|
+
features: data.features.filter((feature)=>typeof feature === 'string')
|
|
230
|
+
});
|
|
231
|
+
} else if (data.type === 'promptState') {
|
|
232
|
+
setState({
|
|
233
|
+
canUndo: data.canUndo === true
|
|
234
|
+
});
|
|
235
|
+
} else if (data.type === 'promptResult' && typeof data.id === 'string') {
|
|
236
|
+
const waiting = settle(data.id);
|
|
237
|
+
if (!waiting) return;
|
|
238
|
+
if (data.ok === true) {
|
|
239
|
+
waiting.resolve({
|
|
240
|
+
summary: typeof data.summary === 'string' ? data.summary : ''
|
|
241
|
+
});
|
|
242
|
+
} else {
|
|
243
|
+
waiting.reject(new Error(typeof data.error === 'string' ? data.error : 'The prompt failed'));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
win.addEventListener('message', onMessage);
|
|
248
|
+
cleanup.push(()=>win.removeEventListener('message', onMessage));
|
|
249
|
+
if (doc.readyState === 'loading') {
|
|
250
|
+
doc.addEventListener('DOMContentLoaded', ready, {
|
|
251
|
+
once: true
|
|
252
|
+
});
|
|
253
|
+
cleanup.push(()=>doc.removeEventListener('DOMContentLoaded', ready));
|
|
254
|
+
} else {
|
|
255
|
+
ready();
|
|
256
|
+
}
|
|
257
|
+
return bridge;
|
|
258
|
+
};
|
|
259
|
+
const wrapScriptBridge = (script, params)=>{
|
|
260
|
+
const emitter = createEmitter();
|
|
261
|
+
script.on('saved', ()=>emitter.emit('saved', {}));
|
|
262
|
+
script.on('published', ()=>emitter.emit('published', {}));
|
|
263
|
+
// Only once something listens: bridge.js reloads after a save while it has
|
|
264
|
+
// no `update` listener, and has no way to remove one.
|
|
265
|
+
let forwardingUpdates = false;
|
|
266
|
+
return {
|
|
267
|
+
isEditing: script.isEditing,
|
|
268
|
+
mode: script.isEditing ? 'editor' : null,
|
|
269
|
+
entryId: script.entryId,
|
|
270
|
+
environment: params.get(preview.PREVIEW_PARAMS.environment),
|
|
271
|
+
on: (event, listener)=>{
|
|
272
|
+
if (event === 'update' && !forwardingUpdates) {
|
|
273
|
+
forwardingUpdates = true;
|
|
274
|
+
script.on('update', (payload)=>{
|
|
275
|
+
const entry = payload?.entry;
|
|
276
|
+
if (entry) emitter.emit('update', {
|
|
277
|
+
entry,
|
|
278
|
+
selectedUid: null
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return emitter.on(event, listener);
|
|
283
|
+
},
|
|
284
|
+
getState: ()=>INITIAL_STATE,
|
|
285
|
+
...INERT_METHODS,
|
|
286
|
+
destroy: ()=>{}
|
|
287
|
+
};
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const DEFAULT_PROMPT_LABELS = {
|
|
291
|
+
open: 'Edit with a prompt',
|
|
292
|
+
placeholder: 'Describe a change, e.g. “make the heading shorter”',
|
|
293
|
+
send: 'Apply',
|
|
294
|
+
undo: 'Undo',
|
|
295
|
+
close: 'Close',
|
|
296
|
+
scopeEntry: 'Whole page',
|
|
297
|
+
scopeBlock: 'Selected block',
|
|
298
|
+
clearBlock: 'Edit the whole page instead',
|
|
299
|
+
working: 'Working on it…',
|
|
300
|
+
save: 'Save',
|
|
301
|
+
publish: 'Publish',
|
|
302
|
+
saving: 'Saving…',
|
|
303
|
+
publishing: 'Publishing…',
|
|
304
|
+
saved: 'Saved as a draft',
|
|
305
|
+
published: 'Published',
|
|
306
|
+
unsaved: 'Unsaved changes',
|
|
307
|
+
signOut: 'Exit editing',
|
|
308
|
+
viewingDrafts: 'Viewing drafts',
|
|
309
|
+
selectHint: 'Click a block to change only that block'
|
|
310
|
+
};
|
|
311
|
+
const ACCENT = '#d58a1f';
|
|
312
|
+
const CSS = `
|
|
313
|
+
:host { all: initial; }
|
|
314
|
+
* { box-sizing: border-box; font: 14px/1.4 system-ui, -apple-system, 'Segoe UI', sans-serif; }
|
|
315
|
+
.root {
|
|
316
|
+
--bg: #fff; --fg: #1d1b19; --muted: #6b655e; --line: #e4ded6; --error: #b3261e;
|
|
317
|
+
position: fixed; left: 50%; bottom: 16px; transform: translateX(-50%);
|
|
318
|
+
z-index: 2147483647; width: min(640px, calc(100vw - 32px)); color: var(--fg);
|
|
319
|
+
}
|
|
320
|
+
@media (prefers-color-scheme: dark) {
|
|
321
|
+
.root { --bg: #23201d; --fg: #f3efe9; --muted: #a9a198; --line: #3b3631; --error: #ff8a80; }
|
|
322
|
+
}
|
|
323
|
+
.pill, .panel { background: var(--bg); border: 1px solid var(--line); box-shadow: 0 8px 30px rgba(0,0,0,.18); }
|
|
324
|
+
.pill {
|
|
325
|
+
display: block; margin: 0 auto; border-radius: 999px; padding: 8px 16px; cursor: pointer;
|
|
326
|
+
color: var(--fg);
|
|
327
|
+
}
|
|
328
|
+
.pill:hover { border-color: ${ACCENT}; }
|
|
329
|
+
.panel { border-radius: 14px; padding: 10px; display: grid; gap: 8px; }
|
|
330
|
+
.row { display: flex; align-items: center; gap: 8px; }
|
|
331
|
+
.scope { color: var(--muted); font-size: 12px; flex: 1; min-width: 0; }
|
|
332
|
+
.scope button { color: inherit; font-size: 12px; text-decoration: underline; background: none; border: 0; padding: 0 0 0 6px; cursor: pointer; }
|
|
333
|
+
textarea {
|
|
334
|
+
width: 100%; resize: none; min-height: 44px; max-height: 160px; padding: 8px 10px;
|
|
335
|
+
border-radius: 10px; border: 1px solid var(--line); background: transparent; color: var(--fg);
|
|
336
|
+
}
|
|
337
|
+
textarea:focus { outline: 2px solid ${ACCENT}; outline-offset: -1px; }
|
|
338
|
+
button.action {
|
|
339
|
+
border-radius: 8px; border: 1px solid var(--line); background: transparent; color: var(--fg);
|
|
340
|
+
padding: 6px 12px; cursor: pointer;
|
|
341
|
+
}
|
|
342
|
+
button.primary { background: ${ACCENT}; border-color: ${ACCENT}; color: #fff; }
|
|
343
|
+
button:disabled { opacity: .5; cursor: default; }
|
|
344
|
+
.close { border: 0; background: none; color: var(--muted); cursor: pointer; font-size: 18px; line-height: 1; padding: 2px 6px; }
|
|
345
|
+
.status { font-size: 13px; color: var(--muted); flex: 1; min-width: 0; }
|
|
346
|
+
.status.error { color: var(--error); }
|
|
347
|
+
.bar { display: flex; justify-content: center; gap: 8px; }
|
|
348
|
+
.row > .link { margin-right: auto; }
|
|
349
|
+
.link { border: 0; background: none; color: var(--muted); cursor: pointer; font-size: 12px; text-decoration: underline; padding: 0; }
|
|
350
|
+
.dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: ${ACCENT}; margin-right: 6px; vertical-align: 1px; }
|
|
351
|
+
[hidden] { display: none !important; }
|
|
352
|
+
`;
|
|
353
|
+
const element = (doc, tag, props = {}, ...children)=>{
|
|
354
|
+
const node = doc.createElement(tag);
|
|
355
|
+
Object.assign(node, props);
|
|
356
|
+
// appendChild, not append: Workers types redefine Element.append, and a
|
|
357
|
+
// site's server code may well have them loaded.
|
|
358
|
+
for (const child of children){
|
|
359
|
+
node.appendChild(typeof child === 'string' ? doc.createTextNode(child) : child);
|
|
360
|
+
}
|
|
361
|
+
return node;
|
|
362
|
+
};
|
|
363
|
+
// Mounts the box on the page; returns a function that removes it.
|
|
364
|
+
const mountPromptBox = (doc, bridge, options = {})=>{
|
|
365
|
+
const labels = {
|
|
366
|
+
...DEFAULT_PROMPT_LABELS,
|
|
367
|
+
...options.labels
|
|
368
|
+
};
|
|
369
|
+
const host = element(doc, 'div');
|
|
370
|
+
host.setAttribute('data-obelism-content-prompt', '');
|
|
371
|
+
const shadow = host.attachShadow({
|
|
372
|
+
mode: 'open'
|
|
373
|
+
});
|
|
374
|
+
const site = bridge.mode === 'site';
|
|
375
|
+
const pill = element(doc, 'button', {
|
|
376
|
+
className: 'pill',
|
|
377
|
+
type: 'button',
|
|
378
|
+
textContent: `✨ ${labels.open}`
|
|
379
|
+
});
|
|
380
|
+
// Someone signed in who may only read: a note that drafts show, and a
|
|
381
|
+
// way out.
|
|
382
|
+
const viewing = element(doc, 'span', {
|
|
383
|
+
className: 'pill'
|
|
384
|
+
}, `${labels.viewingDrafts} · `);
|
|
385
|
+
const pillSignOut = element(doc, 'button', {
|
|
386
|
+
className: 'link',
|
|
387
|
+
type: 'button',
|
|
388
|
+
textContent: labels.signOut
|
|
389
|
+
});
|
|
390
|
+
viewing.appendChild(pillSignOut);
|
|
391
|
+
const scopeText = element(doc, 'span');
|
|
392
|
+
const clearBlock = element(doc, 'button', {
|
|
393
|
+
type: 'button',
|
|
394
|
+
textContent: labels.clearBlock
|
|
395
|
+
});
|
|
396
|
+
const scope = element(doc, 'div', {
|
|
397
|
+
className: 'scope'
|
|
398
|
+
}, '✨ ', scopeText, clearBlock);
|
|
399
|
+
const close = element(doc, 'button', {
|
|
400
|
+
className: 'close',
|
|
401
|
+
type: 'button',
|
|
402
|
+
textContent: '×',
|
|
403
|
+
title: labels.close
|
|
404
|
+
});
|
|
405
|
+
close.setAttribute('aria-label', labels.close);
|
|
406
|
+
const input = element(doc, 'textarea', {
|
|
407
|
+
placeholder: labels.placeholder,
|
|
408
|
+
rows: 1
|
|
409
|
+
});
|
|
410
|
+
input.setAttribute('aria-label', labels.open);
|
|
411
|
+
const status = element(doc, 'div', {
|
|
412
|
+
className: 'status'
|
|
413
|
+
});
|
|
414
|
+
status.setAttribute('role', 'status');
|
|
415
|
+
const undo = element(doc, 'button', {
|
|
416
|
+
className: 'action',
|
|
417
|
+
type: 'button',
|
|
418
|
+
textContent: labels.undo
|
|
419
|
+
});
|
|
420
|
+
const send = element(doc, 'button', {
|
|
421
|
+
className: 'action primary',
|
|
422
|
+
type: 'submit',
|
|
423
|
+
textContent: labels.send
|
|
424
|
+
});
|
|
425
|
+
const save = element(doc, 'button', {
|
|
426
|
+
className: 'action',
|
|
427
|
+
type: 'button',
|
|
428
|
+
textContent: labels.save
|
|
429
|
+
});
|
|
430
|
+
const publish = element(doc, 'button', {
|
|
431
|
+
className: 'action',
|
|
432
|
+
type: 'button',
|
|
433
|
+
textContent: labels.publish
|
|
434
|
+
});
|
|
435
|
+
const signOut = element(doc, 'button', {
|
|
436
|
+
className: 'link',
|
|
437
|
+
type: 'button',
|
|
438
|
+
textContent: labels.signOut
|
|
439
|
+
});
|
|
440
|
+
const unsaved = element(doc, 'span', {
|
|
441
|
+
className: 'scope'
|
|
442
|
+
});
|
|
443
|
+
unsaved.appendChild(element(doc, 'span', {
|
|
444
|
+
className: 'dot'
|
|
445
|
+
}));
|
|
446
|
+
unsaved.appendChild(doc.createTextNode(labels.unsaved));
|
|
447
|
+
const panel = element(doc, 'form', {
|
|
448
|
+
className: 'panel'
|
|
449
|
+
}, element(doc, 'div', {
|
|
450
|
+
className: 'row'
|
|
451
|
+
}, scope, close), input, element(doc, 'div', {
|
|
452
|
+
className: 'row'
|
|
453
|
+
}, status, undo, send), element(doc, 'div', {
|
|
454
|
+
className: 'row'
|
|
455
|
+
}, unsaved, signOut, save, publish));
|
|
456
|
+
const root = element(doc, 'div', {
|
|
457
|
+
className: 'root'
|
|
458
|
+
}, element(doc, 'div', {
|
|
459
|
+
className: 'bar'
|
|
460
|
+
}, pill, viewing), panel);
|
|
461
|
+
shadow.append(element(doc, 'style', {
|
|
462
|
+
textContent: CSS
|
|
463
|
+
}), root);
|
|
464
|
+
let open = false;
|
|
465
|
+
let message = null;
|
|
466
|
+
const render = (state)=>{
|
|
467
|
+
const available = state.features.includes('prompt');
|
|
468
|
+
const storing = state.saving || state.publishing;
|
|
469
|
+
const busy = state.prompting || storing;
|
|
470
|
+
root.hidden = !available && !site;
|
|
471
|
+
pill.hidden = open || !available;
|
|
472
|
+
viewing.hidden = available || !site;
|
|
473
|
+
panel.hidden = !open || !available;
|
|
474
|
+
scopeText.textContent = state.selectedUid ? labels.scopeBlock : labels.scopeEntry;
|
|
475
|
+
clearBlock.hidden = !state.selectedUid;
|
|
476
|
+
undo.hidden = !state.canUndo;
|
|
477
|
+
undo.disabled = busy;
|
|
478
|
+
send.disabled = busy || input.value.trim() === '';
|
|
479
|
+
input.disabled = busy;
|
|
480
|
+
save.hidden = !state.features.includes('save');
|
|
481
|
+
save.disabled = busy || !state.unsaved;
|
|
482
|
+
save.textContent = state.saving ? labels.saving : labels.save;
|
|
483
|
+
publish.hidden = !state.features.includes('publish');
|
|
484
|
+
publish.disabled = busy;
|
|
485
|
+
publish.textContent = state.publishing ? labels.publishing : labels.publish;
|
|
486
|
+
signOut.hidden = !site;
|
|
487
|
+
unsaved.hidden = !state.unsaved;
|
|
488
|
+
const text = state.prompting ? labels.working : message?.text ?? (site && !state.selectedUid ? labels.selectHint : '');
|
|
489
|
+
status.textContent = text;
|
|
490
|
+
status.className = `status${!busy && message?.error ? ' error' : ''}`;
|
|
491
|
+
};
|
|
492
|
+
const refresh = ()=>render(bridge.getState());
|
|
493
|
+
const grow = ()=>{
|
|
494
|
+
input.style.height = 'auto';
|
|
495
|
+
input.style.height = `${input.scrollHeight}px`;
|
|
496
|
+
};
|
|
497
|
+
const submit = async ()=>{
|
|
498
|
+
const prompt = input.value.trim();
|
|
499
|
+
if (!prompt || bridge.getState().prompting) return;
|
|
500
|
+
message = null;
|
|
501
|
+
try {
|
|
502
|
+
const { summary } = await bridge.prompt(prompt);
|
|
503
|
+
message = {
|
|
504
|
+
text: summary,
|
|
505
|
+
error: false
|
|
506
|
+
};
|
|
507
|
+
input.value = '';
|
|
508
|
+
grow();
|
|
509
|
+
} catch (error) {
|
|
510
|
+
message = {
|
|
511
|
+
text: error instanceof Error ? error.message : String(error),
|
|
512
|
+
error: true
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
refresh();
|
|
516
|
+
input.focus();
|
|
517
|
+
};
|
|
518
|
+
const setOpen = (next)=>{
|
|
519
|
+
open = next;
|
|
520
|
+
if (site) bridge.setSelecting(open);
|
|
521
|
+
refresh();
|
|
522
|
+
};
|
|
523
|
+
const store = async (action)=>{
|
|
524
|
+
message = null;
|
|
525
|
+
try {
|
|
526
|
+
await bridge[action]();
|
|
527
|
+
message = {
|
|
528
|
+
text: action === 'save' ? labels.saved : labels.published,
|
|
529
|
+
error: false
|
|
530
|
+
};
|
|
531
|
+
} catch (error) {
|
|
532
|
+
message = {
|
|
533
|
+
text: error instanceof Error ? error.message : String(error),
|
|
534
|
+
error: true
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
refresh();
|
|
538
|
+
};
|
|
539
|
+
pill.addEventListener('click', ()=>{
|
|
540
|
+
setOpen(true);
|
|
541
|
+
input.focus();
|
|
542
|
+
});
|
|
543
|
+
close.addEventListener('click', ()=>setOpen(false));
|
|
544
|
+
save.addEventListener('click', ()=>void store('save'));
|
|
545
|
+
publish.addEventListener('click', ()=>void store('publish'));
|
|
546
|
+
signOut.addEventListener('click', ()=>bridge.signOut());
|
|
547
|
+
pillSignOut.addEventListener('click', ()=>bridge.signOut());
|
|
548
|
+
clearBlock.addEventListener('click', ()=>bridge.select(null));
|
|
549
|
+
undo.addEventListener('click', ()=>{
|
|
550
|
+
message = null;
|
|
551
|
+
bridge.undo();
|
|
552
|
+
});
|
|
553
|
+
panel.addEventListener('submit', (event)=>{
|
|
554
|
+
event.preventDefault();
|
|
555
|
+
void submit();
|
|
556
|
+
});
|
|
557
|
+
input.addEventListener('input', ()=>{
|
|
558
|
+
grow();
|
|
559
|
+
refresh();
|
|
560
|
+
});
|
|
561
|
+
input.addEventListener('keydown', (event)=>{
|
|
562
|
+
if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {
|
|
563
|
+
event.preventDefault();
|
|
564
|
+
void submit();
|
|
565
|
+
} else if (event.key === 'Escape') {
|
|
566
|
+
setOpen(false);
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
const unsubscribe = bridge.on('change', render);
|
|
570
|
+
refresh();
|
|
571
|
+
doc.body.appendChild(host);
|
|
572
|
+
return ()=>{
|
|
573
|
+
unsubscribe();
|
|
574
|
+
host.remove();
|
|
575
|
+
};
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
// The bridge on a site signed in to edit (`/cms/login`, from
|
|
579
|
+
// @obelism/content/site), outside the visual editor. It plays the editor's
|
|
580
|
+
// part itself: it keeps the changed content in memory, asks the site's
|
|
581
|
+
// `/cms/api/…` (which forwards to the CMS with the session's token) to apply
|
|
582
|
+
// prompts and to shape the content for rendering, hands that to `update`
|
|
583
|
+
// listeners like the editor would, and saves or publishes on request.
|
|
584
|
+
//
|
|
585
|
+
// What it edits is the page's entry — the outermost element marked with
|
|
586
|
+
// `editable(entry)` — or, for a selected block, the entry around that block.
|
|
587
|
+
// The base path of the site's handler while signed in to edit, else null.
|
|
588
|
+
const readSiteEditing = (doc)=>{
|
|
589
|
+
for (const part of doc.cookie.split(';')){
|
|
590
|
+
const [name, ...value] = part.trim().split('=');
|
|
591
|
+
if (name !== site.SITE_EDITING_COOKIE) continue;
|
|
592
|
+
try {
|
|
593
|
+
const basePath = decodeURIComponent(value.join('='));
|
|
594
|
+
return basePath.startsWith('/') && !basePath.startsWith('//') ? basePath : null;
|
|
595
|
+
} catch {
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return null;
|
|
600
|
+
};
|
|
601
|
+
const same = (a, b)=>JSON.stringify(a) === JSON.stringify(b);
|
|
602
|
+
const createSiteBridge = (win, basePath)=>{
|
|
603
|
+
const doc = win.document;
|
|
604
|
+
const emitter = createEmitter();
|
|
605
|
+
const cleanup = [];
|
|
606
|
+
const working = new Map();
|
|
607
|
+
// Content before each prompt, newest last, for undo.
|
|
608
|
+
const history = [];
|
|
609
|
+
// Entries changed here since the last publish.
|
|
610
|
+
const touched = new Set();
|
|
611
|
+
let session = null;
|
|
612
|
+
let state = INITIAL_STATE;
|
|
613
|
+
const setState = (next)=>{
|
|
614
|
+
state = {
|
|
615
|
+
...state,
|
|
616
|
+
...next
|
|
617
|
+
};
|
|
618
|
+
emitter.emit('change', state);
|
|
619
|
+
};
|
|
620
|
+
const refreshUnsaved = ()=>setState({
|
|
621
|
+
unsaved: [
|
|
622
|
+
...working.values()
|
|
623
|
+
].some(({ content, saved })=>!same(content, saved)),
|
|
624
|
+
canUndo: history.length > 0
|
|
625
|
+
});
|
|
626
|
+
const api = async (path, body)=>{
|
|
627
|
+
const response = await win.fetch(`${basePath}/api/${path}`, body === undefined ? {
|
|
628
|
+
credentials: 'same-origin'
|
|
629
|
+
} : {
|
|
630
|
+
method: 'POST',
|
|
631
|
+
credentials: 'same-origin',
|
|
632
|
+
headers: {
|
|
633
|
+
'Content-Type': 'application/json',
|
|
634
|
+
[site.SITE_REQUEST_HEADER]: '1'
|
|
635
|
+
},
|
|
636
|
+
body: JSON.stringify(body)
|
|
637
|
+
});
|
|
638
|
+
const data = await response.json().catch(()=>({}));
|
|
639
|
+
if (response.status === 401) {
|
|
640
|
+
setState({
|
|
641
|
+
features: []
|
|
642
|
+
});
|
|
643
|
+
throw new Error('You were signed out. Sign in again to edit.');
|
|
644
|
+
}
|
|
645
|
+
if (!response.ok) {
|
|
646
|
+
throw new Error(typeof data.error === 'string' ? data.error : `Obelism Content answered ${response.status}`);
|
|
647
|
+
}
|
|
648
|
+
return data;
|
|
649
|
+
};
|
|
650
|
+
const pageEntryId = ()=>doc.querySelector(`[${preview.ENTRY_ATTRIBUTE}]`)?.getAttribute(preview.ENTRY_ATTRIBUTE) ?? null;
|
|
651
|
+
const entryFor = (uid)=>{
|
|
652
|
+
const block = uid ? doc.querySelector(blockSelector(uid)) : null;
|
|
653
|
+
return block?.closest(`[${preview.ENTRY_ATTRIBUTE}]`)?.getAttribute(preview.ENTRY_ATTRIBUTE) ?? pageEntryId();
|
|
654
|
+
};
|
|
655
|
+
// The page's language when the space has it (or its base language), else
|
|
656
|
+
// the space's default.
|
|
657
|
+
const locale = ()=>{
|
|
658
|
+
if (!session) return undefined;
|
|
659
|
+
const codes = session.space.locales.map(({ code })=>code);
|
|
660
|
+
const lang = doc.documentElement.lang;
|
|
661
|
+
if (codes.includes(lang)) return lang;
|
|
662
|
+
const base = lang.split('-')[0] ?? '';
|
|
663
|
+
return codes.includes(base) ? base : session.space.defaultLocale;
|
|
664
|
+
};
|
|
665
|
+
const load = async (entryId)=>{
|
|
666
|
+
const loaded = working.get(entryId);
|
|
667
|
+
if (loaded) return loaded;
|
|
668
|
+
const entry = await api(`entries/${encodeURIComponent(entryId)}`);
|
|
669
|
+
const fresh = {
|
|
670
|
+
content: entry.content,
|
|
671
|
+
saved: entry.content,
|
|
672
|
+
revision: entry.draftRevision
|
|
673
|
+
};
|
|
674
|
+
working.set(entryId, fresh);
|
|
675
|
+
return fresh;
|
|
676
|
+
};
|
|
677
|
+
const highlight = (uid)=>{
|
|
678
|
+
doc.querySelectorAll('[data-cms-selected]').forEach((element)=>{
|
|
679
|
+
element.removeAttribute('data-cms-selected');
|
|
680
|
+
});
|
|
681
|
+
if (state.selectedUid !== uid) setState({
|
|
682
|
+
selectedUid: uid
|
|
683
|
+
});
|
|
684
|
+
if (uid) doc.querySelector(blockSelector(uid))?.setAttribute('data-cms-selected', '');
|
|
685
|
+
};
|
|
686
|
+
// Shows the in-memory content through `update`, shaped like the delivery
|
|
687
|
+
// API. A page without `update` listeners shows it once saved (it reloads).
|
|
688
|
+
const render = async (entryId)=>{
|
|
689
|
+
if (emitter.listeners.update.size === 0) return;
|
|
690
|
+
const content = working.get(entryId)?.content;
|
|
691
|
+
const { entry } = await api(`entries/${encodeURIComponent(entryId)}/preview`, {
|
|
692
|
+
content,
|
|
693
|
+
locale: locale()
|
|
694
|
+
});
|
|
695
|
+
emitter.emit('update', {
|
|
696
|
+
entry,
|
|
697
|
+
selectedUid: state.selectedUid
|
|
698
|
+
});
|
|
699
|
+
win.requestAnimationFrame(()=>highlight(state.selectedUid));
|
|
700
|
+
};
|
|
701
|
+
const afterStore = (event)=>{
|
|
702
|
+
emitter.emit(event, {});
|
|
703
|
+
if (emitter.listeners.update.size === 0) win.location.reload();
|
|
704
|
+
};
|
|
705
|
+
const saveAll = async ()=>{
|
|
706
|
+
for (const [entryId, entry] of working){
|
|
707
|
+
if (same(entry.content, entry.saved)) continue;
|
|
708
|
+
const { draftRevision } = await api(`entries/${encodeURIComponent(entryId)}/save`, {
|
|
709
|
+
content: entry.content,
|
|
710
|
+
baseRevision: entry.revision
|
|
711
|
+
});
|
|
712
|
+
entry.saved = entry.content;
|
|
713
|
+
entry.revision = draftRevision;
|
|
714
|
+
touched.add(entryId);
|
|
715
|
+
}
|
|
716
|
+
history.length = 0;
|
|
717
|
+
refreshUnsaved();
|
|
718
|
+
};
|
|
719
|
+
const has = (feature)=>state.features.includes(feature);
|
|
720
|
+
const bridge = {
|
|
721
|
+
isEditing: true,
|
|
722
|
+
mode: 'site',
|
|
723
|
+
get entryId () {
|
|
724
|
+
return pageEntryId();
|
|
725
|
+
},
|
|
726
|
+
environment: null,
|
|
727
|
+
on: emitter.on,
|
|
728
|
+
getState: ()=>state,
|
|
729
|
+
async prompt (prompt, options = {}) {
|
|
730
|
+
const text = prompt.trim();
|
|
731
|
+
if (!text) throw new Error('Say what to change');
|
|
732
|
+
if (!has('prompt')) throw new Error('You may not edit content here');
|
|
733
|
+
const uid = options.uid === undefined ? state.selectedUid : options.uid;
|
|
734
|
+
const entryId = entryFor(uid);
|
|
735
|
+
if (!entryId) {
|
|
736
|
+
throw new Error('Nothing on this page is marked as an entry: add editable(entry)');
|
|
737
|
+
}
|
|
738
|
+
setState({
|
|
739
|
+
prompting: true
|
|
740
|
+
});
|
|
741
|
+
try {
|
|
742
|
+
const entry = await load(entryId);
|
|
743
|
+
const result = await api(`entries/${encodeURIComponent(entryId)}/prompt`, {
|
|
744
|
+
prompt: text,
|
|
745
|
+
content: entry.content,
|
|
746
|
+
locale: locale(),
|
|
747
|
+
uid
|
|
748
|
+
});
|
|
749
|
+
history.push({
|
|
750
|
+
entryId,
|
|
751
|
+
content: entry.content
|
|
752
|
+
});
|
|
753
|
+
entry.content = result.content;
|
|
754
|
+
refreshUnsaved();
|
|
755
|
+
await render(entryId);
|
|
756
|
+
return {
|
|
757
|
+
summary: result.summary
|
|
758
|
+
};
|
|
759
|
+
} finally{
|
|
760
|
+
setState({
|
|
761
|
+
prompting: false
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
|
+
undo () {
|
|
766
|
+
const last = history.pop();
|
|
767
|
+
const entry = last && working.get(last.entryId);
|
|
768
|
+
if (!last || !entry) return;
|
|
769
|
+
entry.content = last.content;
|
|
770
|
+
refreshUnsaved();
|
|
771
|
+
void render(last.entryId).catch((error)=>console.error('[@obelism/content bridge]', error));
|
|
772
|
+
},
|
|
773
|
+
async save () {
|
|
774
|
+
if (!has('save')) throw new Error('You may not edit content here');
|
|
775
|
+
setState({
|
|
776
|
+
saving: true
|
|
777
|
+
});
|
|
778
|
+
try {
|
|
779
|
+
await saveAll();
|
|
780
|
+
} finally{
|
|
781
|
+
setState({
|
|
782
|
+
saving: false
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
afterStore('saved');
|
|
786
|
+
},
|
|
787
|
+
async publish () {
|
|
788
|
+
if (!has('publish')) throw new Error('You may not publish here');
|
|
789
|
+
setState({
|
|
790
|
+
publishing: true
|
|
791
|
+
});
|
|
792
|
+
try {
|
|
793
|
+
await saveAll();
|
|
794
|
+
const page = pageEntryId();
|
|
795
|
+
const entries = touched.size > 0 ? [
|
|
796
|
+
...touched
|
|
797
|
+
] : page ? [
|
|
798
|
+
page
|
|
799
|
+
] : [];
|
|
800
|
+
if (entries.length === 0) {
|
|
801
|
+
throw new Error('Nothing on this page is marked as an entry: add editable(entry)');
|
|
802
|
+
}
|
|
803
|
+
for (const entryId of entries){
|
|
804
|
+
await api(`entries/${encodeURIComponent(entryId)}/publish`, {});
|
|
805
|
+
touched.delete(entryId);
|
|
806
|
+
}
|
|
807
|
+
} finally{
|
|
808
|
+
setState({
|
|
809
|
+
publishing: false
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
afterStore('published');
|
|
813
|
+
},
|
|
814
|
+
signOut () {
|
|
815
|
+
if (state.unsaved && !win.confirm('Sign out and lose the changes you have not saved?')) {
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
working.clear();
|
|
819
|
+
refreshUnsaved();
|
|
820
|
+
const returnTo = win.location.pathname + win.location.search;
|
|
821
|
+
win.location.href = `${basePath}/logout?return_to=${encodeURIComponent(returnTo)}`;
|
|
822
|
+
},
|
|
823
|
+
select (uid) {
|
|
824
|
+
highlight(uid);
|
|
825
|
+
},
|
|
826
|
+
setSelecting (selecting) {
|
|
827
|
+
if (selecting) doc.documentElement.setAttribute('data-cms-selecting', '');
|
|
828
|
+
else doc.documentElement.removeAttribute('data-cms-selecting');
|
|
829
|
+
setState({
|
|
830
|
+
selecting
|
|
831
|
+
});
|
|
832
|
+
},
|
|
833
|
+
destroy () {
|
|
834
|
+
cleanup.splice(0).forEach((undo)=>undo());
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
const style = doc.createElement('style');
|
|
838
|
+
style.textContent = selectionStyle('html[data-cms-selecting] ');
|
|
839
|
+
doc.head.appendChild(style);
|
|
840
|
+
cleanup.push(()=>style.remove());
|
|
841
|
+
cleanup.push(()=>doc.documentElement.removeAttribute('data-cms-selecting'));
|
|
842
|
+
// Capture phase, so a block that is also a link selects instead of
|
|
843
|
+
// navigating — only while selecting, so the site stays usable.
|
|
844
|
+
const onClick = (event)=>{
|
|
845
|
+
if (!state.selecting) return;
|
|
846
|
+
const target = event.target;
|
|
847
|
+
const element = target?.closest?.(`[${preview.UID_ATTRIBUTE}]`);
|
|
848
|
+
if (!element) return;
|
|
849
|
+
event.preventDefault();
|
|
850
|
+
event.stopPropagation();
|
|
851
|
+
highlight(element.getAttribute(preview.UID_ATTRIBUTE));
|
|
852
|
+
};
|
|
853
|
+
doc.addEventListener('click', onClick, true);
|
|
854
|
+
cleanup.push(()=>doc.removeEventListener('click', onClick, true));
|
|
855
|
+
const onBeforeUnload = (event)=>{
|
|
856
|
+
if (!state.unsaved) return;
|
|
857
|
+
event.preventDefault();
|
|
858
|
+
event.returnValue = '';
|
|
859
|
+
};
|
|
860
|
+
win.addEventListener('beforeunload', onBeforeUnload);
|
|
861
|
+
cleanup.push(()=>win.removeEventListener('beforeunload', onBeforeUnload));
|
|
862
|
+
api('session').then((info)=>{
|
|
863
|
+
session = info;
|
|
864
|
+
setState({
|
|
865
|
+
features: info.features
|
|
866
|
+
});
|
|
867
|
+
}).catch((error)=>console.error('[@obelism/content bridge]', error));
|
|
868
|
+
return bridge;
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
// Kept on the window, so two copies of this package on one page still share
|
|
872
|
+
// one bridge.
|
|
873
|
+
const KEY = Symbol.for('@obelism/content/bridge');
|
|
874
|
+
const SERVER_BRIDGE = wrapScriptBridge({
|
|
875
|
+
isEditing: false,
|
|
876
|
+
entryId: null,
|
|
877
|
+
on: ()=>{}
|
|
878
|
+
}, new URLSearchParams());
|
|
879
|
+
// The page's bridge if something already started it, else null. Never
|
|
880
|
+
// starts one: for code that has to stay side-effect free, like a React
|
|
881
|
+
// store's snapshot.
|
|
882
|
+
const peekContentBridge = ()=>typeof window === 'undefined' ? null : window[KEY] ?? null;
|
|
883
|
+
// The page's bridge to the visual editor — or, on a site signed in to edit,
|
|
884
|
+
// its stand-in — started on first use. Call it in the browser (on the server
|
|
885
|
+
// it returns an inert bridge):
|
|
886
|
+
//
|
|
887
|
+
// const bridge = getContentBridge()
|
|
888
|
+
// if (bridge.isEditing) bridge.on('update', ({ entry }) => render(entry))
|
|
889
|
+
//
|
|
890
|
+
// The first call's options win: there is one bridge per page.
|
|
891
|
+
const getContentBridge = ({ url, prompt = true } = {})=>{
|
|
892
|
+
if (typeof window === 'undefined') return SERVER_BRIDGE;
|
|
893
|
+
const win = window;
|
|
894
|
+
if (win[KEY]) return win[KEY];
|
|
895
|
+
let bridge;
|
|
896
|
+
if (win.ObelismBridge) {
|
|
897
|
+
bridge = wrapScriptBridge(win.ObelismBridge, new URLSearchParams(win.location.search));
|
|
898
|
+
} else {
|
|
899
|
+
bridge = createBridge(win, config.normalizeUrl(url || config.DEFAULT_URL));
|
|
900
|
+
// Not in the visual editor, but signed in to edit through the site's
|
|
901
|
+
// own `/cms/login` (@obelism/content/site).
|
|
902
|
+
const sitePath = bridge.isEditing || win.parent !== win ? null : readSiteEditing(win.document);
|
|
903
|
+
if (sitePath) bridge = createSiteBridge(win, sitePath);
|
|
904
|
+
if (bridge.isEditing && prompt) {
|
|
905
|
+
const unmount = mountPromptBox(win.document, bridge, prompt === true ? {} : prompt);
|
|
906
|
+
const destroy = bridge.destroy;
|
|
907
|
+
bridge.destroy = ()=>{
|
|
908
|
+
unmount();
|
|
909
|
+
destroy();
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
win[KEY] = bridge;
|
|
914
|
+
return bridge;
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
exports.DEFAULT_PROMPT_LABELS = DEFAULT_PROMPT_LABELS;
|
|
918
|
+
exports.INITIAL_STATE = INITIAL_STATE;
|
|
919
|
+
exports.getContentBridge = getContentBridge;
|
|
920
|
+
exports.peekContentBridge = peekContentBridge;
|