@obelism/content 0.6.0 → 0.8.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/README.md +43 -5
- package/dist/bridge.d.mts +27 -3
- package/dist/bridge.d.mts.map +1 -1
- package/dist/bridge.d.ts +27 -3
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.global.js +26 -2
- package/dist/bridge.js +203 -3
- package/dist/bridge.mjs +202 -4
- package/dist/index.d.mts +22 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +22 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +53 -6
- package/dist/index.mjs +53 -6
- package/dist/{types-nyvor49r.d.ts → types-2evub96q.d.ts} +14 -2
- package/dist/types-2evub96q.d.ts.map +1 -0
- package/dist/webhooks.d.mts +2 -0
- package/dist/webhooks.d.mts.map +1 -1
- package/dist/webhooks.d.ts +2 -0
- package/dist/webhooks.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/types-nyvor49r.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -22,6 +22,11 @@ const content = new ContentClient({
|
|
|
22
22
|
})
|
|
23
23
|
|
|
24
24
|
await content.getPage('blog/hello') // Entry | null
|
|
25
|
+
await content.getPage('blog/hallo', { locale: 'nl' }) // looked up in nl's paths
|
|
26
|
+
await content.findPage('blog/old') // { entry } | { redirect: { path } } | null
|
|
27
|
+
await content.getRedirects({ locale: 'nl' }) // { items: [{ locale, from, to }], total, limit, skip }
|
|
28
|
+
for await (const redirect of content.getAllRedirects()) {
|
|
29
|
+
}
|
|
25
30
|
await content.getEntry('entry_…') // Entry | null
|
|
26
31
|
await content.getEntries({ contentType: 'post', limit: 20, skip: 0 }) // { items, total, limit, skip }
|
|
27
32
|
for await (const post of content.getAllEntries({ contentType: 'post' })) {
|
|
@@ -39,7 +44,17 @@ await content.getContentModel() // the model file
|
|
|
39
44
|
```
|
|
40
45
|
|
|
41
46
|
Each method takes `{ locale, depth, richText, init }` to override the
|
|
42
|
-
defaults.
|
|
47
|
+
defaults.
|
|
48
|
+
|
|
49
|
+
Pages and folders can have a slug per locale. `getPage` looks the path up
|
|
50
|
+
in the requested locale's paths, `entry.path` is the path in that locale and
|
|
51
|
+
`entry.paths` the path in every locale (`{ en: 'about-us', nl: 'over-ons' }`),
|
|
52
|
+
for hreflang links and language switchers. The locale is never part of a
|
|
53
|
+
path: whether it is `/nl/over-ons` or `nl.example.com/over-ons` is your
|
|
54
|
+
routing. A page that moved keeps its old path as a redirect in each locale:
|
|
55
|
+
`findPage` answers it with `{ redirect: { path } }`, so your route can send
|
|
56
|
+
a `301` instead of a 404, and `getAllRedirects` lists them for redirect rules
|
|
57
|
+
built ahead of time. Missing pages and entries are `null`; any other failure throws a
|
|
43
58
|
`ContentApiError` with `status` and the API's `message`. Type your content
|
|
44
59
|
with generics: `content.getPage<Entry<{ title: string }>>('home')`.
|
|
45
60
|
|
|
@@ -131,11 +146,12 @@ bridge.undo()
|
|
|
131
146
|
On a site signed in to edit (`@obelism/content/site`), the box also has **New
|
|
132
147
|
page**: a path (`/pricing`, or under an existing page) and what the page is
|
|
133
148
|
for. The CMS writes the page like the one the editor is on and stores it as a
|
|
134
|
-
draft, and the bridge opens it at `/` + its path.
|
|
135
|
-
|
|
149
|
+
draft, and the bridge opens it at `/` + its path. The path is the one in
|
|
150
|
+
the page's language (`<html lang>`). When your routes put CMS pages
|
|
151
|
+
elsewhere, say where:
|
|
136
152
|
|
|
137
153
|
```ts
|
|
138
|
-
getContentBridge({ pagePath: (path) =>
|
|
154
|
+
getContentBridge({ pagePath: (path, locale) => `/${locale}/${path}` })
|
|
139
155
|
await bridge.createPage('/pricing', 'Our plans, for agencies') // or your own UI
|
|
140
156
|
```
|
|
141
157
|
|
|
@@ -196,10 +212,32 @@ const event = await verifyWebhook(request, process.env.CONTENT_WEBHOOK_SECRET!)
|
|
|
196
212
|
|
|
197
213
|
`WebhookEvent` is a union on `type`: `entry.published`, `entry.unpublished`,
|
|
198
214
|
`entry.deleted` and `entry.moved` (`data.entry`: `{ id, contentType, name,
|
|
199
|
-
path }`, plus `previousPath` when moved), `asset.updated` (`data.asset`,
|
|
215
|
+
path, paths }`, plus `previousPath` and `previousPaths` when moved), `asset.updated` (`data.asset`,
|
|
200
216
|
`data.entries`), `model.changed` (`data.contentTypes`: `{ apiId, action }`),
|
|
201
217
|
`locales.changed` (`data.defaultLocale`, `data.locales`) and `webhook.test`.
|
|
202
218
|
|
|
219
|
+
## Click rates
|
|
220
|
+
|
|
221
|
+
The visual editor can show how often each block is clicked, on the page
|
|
222
|
+
itself: a small label on every block with clicks ("24% clicked"), on or off
|
|
223
|
+
with the editor's **Show click rates on the page** switch. Clicking a label selects its
|
|
224
|
+
block. The rates come from Obelism numbers, so tag a block's clickable
|
|
225
|
+
element with its uid and load numbers' script on the site (see the CMS's
|
|
226
|
+
visual editor guide, "Click rates"):
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
<a href={block.link} data-numbers-id={block._uid}>
|
|
230
|
+
{block.label}
|
|
231
|
+
</a>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The bridge draws the labels itself; it needs nothing else from your site.
|
|
235
|
+
For your own UI, the rates and the switch are in the bridge's state:
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
bridge.on('change', ({ stats, statsVisible }) => …) // stats[uid].rate, 0–1
|
|
239
|
+
```
|
|
240
|
+
|
|
203
241
|
## Helpers
|
|
204
242
|
|
|
205
243
|
- `richTextToHtml(doc, { renderEntry?, renderAsset? })`, `richTextToPlainText(doc)`
|
package/dist/bridge.d.mts
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
import { Entry } from './index.mjs';
|
|
2
2
|
|
|
3
|
+
type BlockStats = {
|
|
4
|
+
clicks: number;
|
|
5
|
+
clickers: number;
|
|
6
|
+
viewers: number;
|
|
7
|
+
rate: number;
|
|
8
|
+
};
|
|
9
|
+
type StatsLabels = {
|
|
10
|
+
clicked: string;
|
|
11
|
+
title: string;
|
|
12
|
+
};
|
|
13
|
+
declare const DEFAULT_STATS_LABELS: StatsLabels;
|
|
14
|
+
type StatsOverlay = {
|
|
15
|
+
update(stats: Readonly<Record<string, BlockStats>>, visible: boolean): void;
|
|
16
|
+
refresh(): void;
|
|
17
|
+
destroy(): void;
|
|
18
|
+
};
|
|
19
|
+
declare const mountStatsOverlay: (win: Window, { selector, onSelect, labels: labelOverrides, }: {
|
|
20
|
+
selector: (uid: string) => string;
|
|
21
|
+
onSelect: (uid: string) => void;
|
|
22
|
+
labels?: Partial<StatsLabels> | undefined;
|
|
23
|
+
}) => StatsOverlay;
|
|
24
|
+
|
|
3
25
|
type BridgeEvents = {
|
|
4
26
|
update: {
|
|
5
27
|
entry: Entry;
|
|
@@ -20,6 +42,8 @@ type BridgeState = {
|
|
|
20
42
|
saving: boolean;
|
|
21
43
|
publishing: boolean;
|
|
22
44
|
selecting: boolean;
|
|
45
|
+
stats: Readonly<Record<string, BlockStats>>;
|
|
46
|
+
statsVisible: boolean;
|
|
23
47
|
};
|
|
24
48
|
type PromptResult = {
|
|
25
49
|
summary: string;
|
|
@@ -88,11 +112,11 @@ declare const mountPromptBox: (doc: Document, bridge: ContentBridge, options?: P
|
|
|
88
112
|
type BridgeOptions = {
|
|
89
113
|
url?: string | undefined;
|
|
90
114
|
prompt?: boolean | PromptBoxOptions | undefined;
|
|
91
|
-
pagePath?: ((path: string) => string) | undefined;
|
|
115
|
+
pagePath?: ((path: string, locale: string) => string) | undefined;
|
|
92
116
|
};
|
|
93
117
|
declare const peekContentBridge: () => ContentBridge | null;
|
|
94
118
|
declare const getContentBridge: ({ url, prompt, pagePath, }?: BridgeOptions) => ContentBridge;
|
|
95
119
|
|
|
96
|
-
export { DEFAULT_PROMPT_LABELS, INITIAL_STATE, getContentBridge, mountPromptBox, peekContentBridge };
|
|
97
|
-
export type { BridgeEvent, BridgeEvents, BridgeListener, BridgeOptions, BridgeState, ContentBridge, CreatePageResult, PromptBoxOptions, PromptLabels, PromptResult };
|
|
120
|
+
export { DEFAULT_PROMPT_LABELS, DEFAULT_STATS_LABELS, INITIAL_STATE, getContentBridge, mountPromptBox, mountStatsOverlay, peekContentBridge };
|
|
121
|
+
export type { BlockStats, BridgeEvent, BridgeEvents, BridgeListener, BridgeOptions, BridgeState, ContentBridge, CreatePageResult, PromptBoxOptions, PromptLabels, PromptResult, StatsLabels, StatsOverlay };
|
|
98
122
|
//# sourceMappingURL=bridge.d.mts.map
|
package/dist/bridge.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge.d.mts","sources":["../src/bridge/createBridge.ts","../src/bridge/promptBox.ts","../src/bridge.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"bridge.d.mts","sources":["../src/bridge/statsOverlay.ts","../src/bridge/createBridge.ts","../src/bridge/promptBox.ts","../src/bridge.ts"],"mappings":";;AAAA,KAAK,UAAU;AACf;AACA;AACA;AACA;AACA;AACA,KAAK,WAAW;AAChB;AACA;AACA;AACA,cAAc,oBAAoB,EAAE,WAAW;AAc/C,KAAK,YAAY;AACjB,kBAAkB,QAAQ,CAAC,MAAM,SAAS,UAAU;AACpD;AACA;AACA;AACA,cAAc,iBAAiB,QAAQ,MAAM;AAC7C;AACA;AACA,aAAa,OAAO,CAAC,WAAW;AAChC,MAAM,YAAY;;AC5BlB,KAAK,YAAY;AACjB;AACA,eAAe,KAAK;AACpB;AACA;AACA,WAAW,MAAM;AACjB,eAAe,MAAM;AACrB,YAAY,WAAW;AACvB;AACA,KAAK,WAAW,SAAS,YAAY;AACrC,KAAK,cAAc,WAAW,WAAW,cAAc,YAAY;AACnE,KAAK,WAAW;AAChB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,WAAW,QAAQ,CAAC,MAAM,SAAS,UAAU;AAC7C;AACA;AACA,KAAK,YAAY;AACjB;AACA;AACA,KAAK,gBAAgB;AACrB;AACA;AACA;AACA;AACA;AACA,KAAK,aAAa;AAClB;AACA;AACA;AACA;AACA,iBAAiB,WAAW,sBAAsB,cAAc;AAChE,gBAAgB,WAAW;AAC3B;AACA;AACA,QAAQ,OAAO,CAAC,YAAY;AAC5B,8CAA8C,OAAO,CAAC,gBAAgB;AACtE;AACA,YAAY,OAAO;AACnB,eAAe,OAAO;AACtB;AACA;AACA;AACA;AACA;AAEA,cAAc,aAAa,EAAE,WAAW;;ACxDxC,KAAK,YAAY;AACjB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,qBAAqB,EAAE,YAAY;AACjD,KAAK,gBAAgB;AACrB,aAAa,OAAO,CAAC,YAAY;AACjC;AACA,cAAc,cAAc,QAAQ,QAAQ,UAAU,aAAa,YAAY,gBAAgB;;ACzB/F,KAAK,aAAa;AAClB;AACA,uBAAuB,gBAAgB;AACvC;AACA;AACA,cAAc,iBAAiB,QAAQ,aAAa;AACpD,cAAc,gBAAgB,gCAAgC,aAAa,KAAK,aAAa","names":[]}
|
package/dist/bridge.d.ts
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
import { Entry } from './index.js';
|
|
2
2
|
|
|
3
|
+
type BlockStats = {
|
|
4
|
+
clicks: number;
|
|
5
|
+
clickers: number;
|
|
6
|
+
viewers: number;
|
|
7
|
+
rate: number;
|
|
8
|
+
};
|
|
9
|
+
type StatsLabels = {
|
|
10
|
+
clicked: string;
|
|
11
|
+
title: string;
|
|
12
|
+
};
|
|
13
|
+
declare const DEFAULT_STATS_LABELS: StatsLabels;
|
|
14
|
+
type StatsOverlay = {
|
|
15
|
+
update(stats: Readonly<Record<string, BlockStats>>, visible: boolean): void;
|
|
16
|
+
refresh(): void;
|
|
17
|
+
destroy(): void;
|
|
18
|
+
};
|
|
19
|
+
declare const mountStatsOverlay: (win: Window, { selector, onSelect, labels: labelOverrides, }: {
|
|
20
|
+
selector: (uid: string) => string;
|
|
21
|
+
onSelect: (uid: string) => void;
|
|
22
|
+
labels?: Partial<StatsLabels> | undefined;
|
|
23
|
+
}) => StatsOverlay;
|
|
24
|
+
|
|
3
25
|
type BridgeEvents = {
|
|
4
26
|
update: {
|
|
5
27
|
entry: Entry;
|
|
@@ -20,6 +42,8 @@ type BridgeState = {
|
|
|
20
42
|
saving: boolean;
|
|
21
43
|
publishing: boolean;
|
|
22
44
|
selecting: boolean;
|
|
45
|
+
stats: Readonly<Record<string, BlockStats>>;
|
|
46
|
+
statsVisible: boolean;
|
|
23
47
|
};
|
|
24
48
|
type PromptResult = {
|
|
25
49
|
summary: string;
|
|
@@ -88,11 +112,11 @@ declare const mountPromptBox: (doc: Document, bridge: ContentBridge, options?: P
|
|
|
88
112
|
type BridgeOptions = {
|
|
89
113
|
url?: string | undefined;
|
|
90
114
|
prompt?: boolean | PromptBoxOptions | undefined;
|
|
91
|
-
pagePath?: ((path: string) => string) | undefined;
|
|
115
|
+
pagePath?: ((path: string, locale: string) => string) | undefined;
|
|
92
116
|
};
|
|
93
117
|
declare const peekContentBridge: () => ContentBridge | null;
|
|
94
118
|
declare const getContentBridge: ({ url, prompt, pagePath, }?: BridgeOptions) => ContentBridge;
|
|
95
119
|
|
|
96
|
-
export { DEFAULT_PROMPT_LABELS, INITIAL_STATE, getContentBridge, mountPromptBox, peekContentBridge };
|
|
97
|
-
export type { BridgeEvent, BridgeEvents, BridgeListener, BridgeOptions, BridgeState, ContentBridge, CreatePageResult, PromptBoxOptions, PromptLabels, PromptResult };
|
|
120
|
+
export { DEFAULT_PROMPT_LABELS, DEFAULT_STATS_LABELS, INITIAL_STATE, getContentBridge, mountPromptBox, mountStatsOverlay, peekContentBridge };
|
|
121
|
+
export type { BlockStats, BridgeEvent, BridgeEvents, BridgeListener, BridgeOptions, BridgeState, ContentBridge, CreatePageResult, PromptBoxOptions, PromptLabels, PromptResult, StatsLabels, StatsOverlay };
|
|
98
122
|
//# sourceMappingURL=bridge.d.ts.map
|
package/dist/bridge.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge.d.ts","sources":["../src/bridge/createBridge.ts","../src/bridge/promptBox.ts","../src/bridge.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"bridge.d.ts","sources":["../src/bridge/statsOverlay.ts","../src/bridge/createBridge.ts","../src/bridge/promptBox.ts","../src/bridge.ts"],"mappings":";;AAAA,KAAK,UAAU;AACf;AACA;AACA;AACA;AACA;AACA,KAAK,WAAW;AAChB;AACA;AACA;AACA,cAAc,oBAAoB,EAAE,WAAW;AAc/C,KAAK,YAAY;AACjB,kBAAkB,QAAQ,CAAC,MAAM,SAAS,UAAU;AACpD;AACA;AACA;AACA,cAAc,iBAAiB,QAAQ,MAAM;AAC7C;AACA;AACA,aAAa,OAAO,CAAC,WAAW;AAChC,MAAM,YAAY;;AC5BlB,KAAK,YAAY;AACjB;AACA,eAAe,KAAK;AACpB;AACA;AACA,WAAW,MAAM;AACjB,eAAe,MAAM;AACrB,YAAY,WAAW;AACvB;AACA,KAAK,WAAW,SAAS,YAAY;AACrC,KAAK,cAAc,WAAW,WAAW,cAAc,YAAY;AACnE,KAAK,WAAW;AAChB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,WAAW,QAAQ,CAAC,MAAM,SAAS,UAAU;AAC7C;AACA;AACA,KAAK,YAAY;AACjB;AACA;AACA,KAAK,gBAAgB;AACrB;AACA;AACA;AACA;AACA;AACA,KAAK,aAAa;AAClB;AACA;AACA;AACA;AACA,iBAAiB,WAAW,sBAAsB,cAAc;AAChE,gBAAgB,WAAW;AAC3B;AACA;AACA,QAAQ,OAAO,CAAC,YAAY;AAC5B,8CAA8C,OAAO,CAAC,gBAAgB;AACtE;AACA,YAAY,OAAO;AACnB,eAAe,OAAO;AACtB;AACA;AACA;AACA;AACA;AAEA,cAAc,aAAa,EAAE,WAAW;;ACxDxC,KAAK,YAAY;AACjB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,qBAAqB,EAAE,YAAY;AACjD,KAAK,gBAAgB;AACrB,aAAa,OAAO,CAAC,YAAY;AACjC;AACA,cAAc,cAAc,QAAQ,QAAQ,UAAU,aAAa,YAAY,gBAAgB;;ACzB/F,KAAK,aAAa;AAClB;AACA,uBAAuB,gBAAgB;AACvC;AACA;AACA,cAAc,iBAAiB,QAAQ,aAAa;AACpD,cAAc,gBAAgB,gCAAgC,aAAa,KAAK,aAAa","names":[]}
|
package/dist/bridge.global.js
CHANGED
|
@@ -1,4 +1,28 @@
|
|
|
1
|
-
!/*! Obelism Content bridge (@obelism/content), MIT. https://content.obelism.studio/docs/guides/visual-editor */function(){"use strict";var e;let t="data-cms-uid",r="data-cms-entry",n="cms_environment",o=
|
|
1
|
+
!/*! Obelism Content bridge (@obelism/content), MIT. https://content.obelism.studio/docs/guides/visual-editor */function(){"use strict";var e;let t="data-cms-uid",r="data-cms-entry",n="cms_environment",o={clicked:"{rate}% clicked",title:"{clickers} of {viewers} visitors of its page clicked it in the last 30 days ({clicks} clicks)"},a=(e,t)=>e.replace(/\{(\w+)\}/g,(e,r)=>r in t?String(t[r]):e),i=`
|
|
2
|
+
:host { all: initial; }
|
|
3
|
+
.layer {
|
|
4
|
+
position: fixed; inset: 0; pointer-events: none;
|
|
5
|
+
z-index: 2147483646;
|
|
6
|
+
font: 600 12px/1 'InterVariable', 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
7
|
+
-webkit-font-smoothing: antialiased;
|
|
8
|
+
}
|
|
9
|
+
.badge {
|
|
10
|
+
position: absolute; transform: translateX(-100%);
|
|
11
|
+
display: inline-flex; gap: 6px; align-items: center;
|
|
12
|
+
padding: 6px 10px; white-space: nowrap; cursor: pointer;
|
|
13
|
+
pointer-events: auto; border: 0;
|
|
14
|
+
color: rgb(238 236 233); background: rgb(12 12 12 / .88);
|
|
15
|
+
border-radius: 5em; box-shadow: 0 6px 16px -8px rgb(12 12 12 / .5);
|
|
16
|
+
font: inherit;
|
|
17
|
+
}
|
|
18
|
+
.badge::before {
|
|
19
|
+
content: ''; width: 8px; height: 8px; border-radius: 50%;
|
|
20
|
+
background: rgb(212 130 30);
|
|
21
|
+
}
|
|
22
|
+
.badge[data-place='beside'] { transform: translateY(-50%); }
|
|
23
|
+
.badge[data-place='above'] { transform: translate(-100%, -100%); }
|
|
24
|
+
.badge:focus-visible { outline: 2px solid rgb(212 130 30); outline-offset: 2px; }
|
|
25
|
+
`,s=(e,t)=>e.height>=64?{place:"inside",top:Math.max(e.top,0)+8,left:e.right-8}:e.right+8+140<=t?{place:"beside",top:e.top+e.height/2,left:e.right+8}:{place:"above",top:e.top-4,left:e.right},l="#d58a1f",d=(e="")=>`${e}[${t}]{cursor:pointer;transition:outline-color .15s}${e}[${t}]:hover{outline:2px dashed ${l};outline-offset:2px}${e}[${t}][data-cms-selected]{outline:2px solid ${l};outline-offset:2px}`,c={features:[],selectedUid:null,prompting:!1,canUndo:!1,unsaved:!1,saving:!1,publishing:!1,selecting:!1,stats:{},statsVisible:!1},p=()=>{let e={update:new Set,saved:new Set,published:new Set,change:new Set};return{listeners:e,on:(t,r)=>(e[t].add(r),()=>{e[t].delete(r)}),emit(t,r){for(let n of e[t])try{n(r)}catch(e){console.error("[@obelism/content bridge]",e)}}}},g=()=>Promise.reject(Error("Save and publish in the visual editor itself")),u={prompt:()=>Promise.reject(Error("Prompts only work inside the visual editor")),createPage:()=>Promise.reject(Error("New pages are made on the site itself")),undo:()=>{},save:g,publish:g,signOut:()=>{},select:()=>{},setSelecting:()=>{}},h=e=>"number"==typeof e&&Number.isFinite(e)&&e>=0,b=e=>`[${t}="${e.replace(/["\\]/g,"\\$&")}"]`,m=(e,t)=>{let r=p();e.on("saved",()=>r.emit("saved",{})),e.on("published",()=>r.emit("published",{}));let o=!1;return{isEditing:e.isEditing,mode:e.isEditing?"editor":null,entryId:e.entryId,environment:t.get(n),on:(t,n)=>("update"!==t||o||(o=!0,e.on("update",e=>{let t=e?.entry;t&&r.emit("update",{entry:t,selectedUid:null})})),r.on(t,n)),getState:()=>c,...u,destroy:()=>{}}},f={open:"Edit with a prompt",placeholder:"Describe a change, e.g. “make the heading shorter”",send:"Apply",undo:"Undo",close:"Close",scopeEntry:"Whole page",scopeBlock:"Selected block",clearBlock:"Edit the whole page instead",working:"Working on it…",save:"Save",publish:"Publish",saving:"Saving…",publishing:"Publishing…",saved:"Saved as a draft",published:"Published",unsaved:"Unsaved changes",signOut:"Exit editing",viewingDrafts:"Viewing drafts",selectHint:"Click a block to change only that block",newPage:"New page",newPageCancel:"Edit this page instead",newPagePlaceholder:"Describe the new page, e.g. “a pricing page for agencies”",path:"Path",pathPlaceholder:"/pricing",creating:"Writing the page…",created:"Opening the new page…"},v={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"/>',newPage:'<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"/><path d="M9 15h6"/><path d="M12 18v-6"/>',block:'<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M7 8h10"/><path d="M7 12h6"/>',undo:'<path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/>',close:'<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',send:'<path d="M3.714 3.048a.498.498 0 0 0-.683.627l2.843 7.627a2 2 0 0 1 0 1.396l-2.842 7.627a.498.498 0 0 0 .682.627l18-8.5a.5.5 0 0 0 0-.904z"/><path d="M6 12h16"/>'},y=`
|
|
2
26
|
:host { all: initial; }
|
|
3
27
|
* { box-sizing: border-box; font: inherit; color: inherit; margin: 0; }
|
|
4
28
|
.root {
|
|
@@ -155,4 +179,4 @@ button.chip:disabled { opacity: .5; }
|
|
|
155
179
|
background: rgb(var(--theme-accent-rgb)); margin-right: 8px;
|
|
156
180
|
}
|
|
157
181
|
[hidden] { display: none !important; }
|
|
158
|
-
`,b=(e,t,r={},...n)=>{let o=e.createElement(t);for(let t of(Object.assign(o,r),n))o.appendChild("string"==typeof t?e.createTextNode(t):t);return o},m=(e,t)=>{let r=b(e,"span",{className:"icon"});return r.setAttribute("aria-hidden","true"),r.innerHTML=`<svg viewBox="0 0 24 24">${u[t]}</svg>`,r},f=(e,t,r={})=>{let n={...g,...r.labels},o=b(e,"div");o.setAttribute("data-obelism-content-prompt","");let a=o.attachShadow({mode:"open"}),i="site"===t.mode,s=b(e,"span",{textContent:n.open}),l=b(e,"button",{className:"pill",type:"button"},s,b(e,"span",{className:"mark",textContent:"⁜"})),d=b(e,"span",{className:"pill compact"},`${n.viewingDrafts} \xb7 `),c=b(e,"button",{className:"link",type:"button",textContent:n.signOut});d.appendChild(c);let p=b(e,"p",{className:"status"});p.setAttribute("role","status");let u=b(e,"textarea",{placeholder:n.placeholder,rows:1});u.setAttribute("aria-label",n.open);let f=m(e,"page"),v=m(e,"block"),y=b(e,"span"),x=b(e,"button",{type:"button",title:n.clearBlock});x.setAttribute("aria-label",n.clearBlock),x.appendChild(m(e,"close"));let w=b(e,"span",{className:"chip scope"},f,v,y,x),k=b(e,"button",{className:"action chip",type:"button"},m(e,"undo"),n.undo),E=b(e,"button",{className:"action chip",type:"button"},m(e,"newPage"),n.newPage),S=b(e,"button",{type:"button",title:n.newPageCancel});S.setAttribute("aria-label",n.newPageCancel),S.appendChild(m(e,"close"));let C=b(e,"span",{className:"chip scope"},m(e,"newPage"),b(e,"span",{textContent:n.newPage}),S),N=b(e,"input",{type:"text",placeholder:n.pathPlaceholder,autocomplete:"off",spellcheck:!1}),U=b(e,"label",{className:"path"},b(e,"span",{textContent:n.path}),N),P=b(e,"button",{className:"close icon-button",type:"button",title:n.close},m(e,"close"));P.setAttribute("aria-label",n.close);let L=b(e,"button",{className:"action primary icon-button",type:"submit"},b(e,"span",{className:"visually-hidden",textContent:n.send}),m(e,"send"));L.title=n.send;let $=b(e,"span",{className:"unsaved"});$.appendChild(b(e,"span",{className:"dot"})),$.appendChild(e.createTextNode(n.unsaved));let A=b(e,"button",{className:"link",type:"button",textContent:n.signOut}),O=b(e,"button",{className:"action chip",type:"button",textContent:n.save}),I=b(e,"button",{className:"action chip",type:"button",textContent:n.publish}),M=b(e,"div",{className:"footer"},$,b(e,"span",{className:"spacer"}),A,O,I),j=b(e,"form",{className:"panel"},p,U,u,b(e,"div",{className:"toolbar"},w,C,k,E,b(e,"span",{className:"spacer"}),P,b(e,"span",{className:"divider"}),L),M),R=b(e,"div",{className:"root"},b(e,"div",{className:"bar"},l,d),j);a.append(b(e,"style",{textContent:h}),R);let z=!1,D=!1,T=null,B=e=>{let r=e.features.includes("prompt"),o=e.saving||e.publishing,a=e.prompting||o;R.hidden=!r&&!i,l.hidden=z||!r,d.hidden=r||!i,j.hidden=!z||!r;let c=i&&e.features.includes("create"),g=i&&null===t.entryId,h=g&&c;c||(D=!1),h&&(D=!0),s.textContent=h?n.newPage:n.open,S.hidden=h,w.hidden=D,C.hidden=!D,E.hidden=D||!c,E.disabled=a,U.hidden=!D,N.disabled=a,u.placeholder=D?n.newPagePlaceholder:n.placeholder,y.textContent=e.selectedUid?n.scopeBlock:n.scopeEntry,x.hidden=!e.selectedUid,f.hidden=!!e.selectedUid,v.hidden=!e.selectedUid,k.hidden=D||!e.canUndo,k.disabled=a,L.disabled=a||""===u.value.trim()||D&&""===N.value.trim(),u.disabled=a,O.hidden=!e.features.includes("save")||g,O.disabled=a||!e.unsaved,O.textContent=e.saving?n.saving:n.save,I.hidden=!e.features.includes("publish")||g,I.disabled=a,I.textContent=e.publishing?n.publishing:n.publish,A.hidden=!i,M.hidden=!i,$.hidden=!e.unsaved;let b=e.prompting?D?n.creating:n.working:T?.text??(!i||D||e.selectedUid?"":n.selectHint);p.textContent=b,p.hidden=!b,p.className=`status${e.prompting?" working":""}${!a&&T?.error?" error":""}`},q=()=>B(t.getState()),H=()=>{u.style.height="auto",u.style.height=`${u.scrollHeight}px`},Y=async()=>{let e=u.value.trim();if(e&&!t.getState().prompting){T=null;try{if(D){let r=N.value.trim();if(!r)return N.focus();await t.createPage(r,e),T={text:n.created,error:!1}}else{let{summary:r}=await t.prompt(e);T={text:r,error:!1}}u.value="",H()}catch(e){T={text:e instanceof Error?e.message:String(e),error:!0}}q(),u.focus()}},_=e=>{z=e,q(),i&&t.setSelecting(z&&!D)},V=e=>{D=e,T=null,t.setSelecting(z&&!D),D&&t.select(null),q(),(D?N:u).focus()},W=async e=>{T=null;try{await t[e](),T={text:"save"===e?n.saved:n.published,error:!1}}catch(e){T={text:e instanceof Error?e.message:String(e),error:!0}}q()};l.addEventListener("click",()=>{_(!0),(D?N:u).focus()}),P.addEventListener("click",()=>_(!1)),O.addEventListener("click",()=>void W("save")),I.addEventListener("click",()=>void W("publish")),A.addEventListener("click",()=>t.signOut()),c.addEventListener("click",()=>t.signOut()),x.addEventListener("click",()=>t.select(null)),E.addEventListener("click",()=>V(!0)),S.addEventListener("click",()=>V(!1)),N.addEventListener("input",q),N.addEventListener("keydown",e=>{"Enter"!==e.key||e.isComposing?"Escape"===e.key&&_(!1):(e.preventDefault(),u.focus())}),k.addEventListener("click",()=>{T=null,t.undo()}),j.addEventListener("submit",e=>{e.preventDefault(),Y()}),u.addEventListener("input",()=>{H(),q()}),u.addEventListener("keydown",e=>{"Enter"!==e.key||e.shiftKey||e.isComposing?"Escape"===e.key&&_(!1):(e.preventDefault(),Y())});let F=t.on("change",B);return q(),e.body.appendChild(o),()=>{F(),o.remove()}},v="https://content.obelism.studio",y=(e,t)=>JSON.stringify(e)===JSON.stringify(t),x=Symbol.for("@obelism/content/bridge"),w=p({isEditing:!1,entryId:null,on:()=>{}},new URLSearchParams),k=({url:e,prompt:o=!0,pagePath:l}={})=>{let g;if("u"<typeof window)return w;let u=window;if(u[x])return u[x];if(u.ObelismBridge)g=p(u.ObelismBridge,new URLSearchParams(u.location.search));else{let p=(g=((e,r)=>{let o=new URLSearchParams(e.location.search),l=e.parent!==e&&o.has("cms_preview"),p=s(),g=[],u={...i,selecting:!0},h=e=>{u={...u,...e},p.emit("change",u)},b={isEditing:l,mode:l?"editor":null,entryId:o.get("cms_entry"),environment:o.get(n),on:p.on,getState:()=>u,...d,destroy:()=>{g.splice(0).forEach(e=>e())}};if(!l)return b;let m=e.document,f=(t,n={})=>{e.parent.postMessage({source:"obelism-content-bridge",type:t,...n},r)},v=m.createElement("style");v.textContent=a(),m.head.appendChild(v),g.push(()=>v.remove());let y=(e,t)=>{if(m.querySelectorAll("[data-cms-selected]").forEach(e=>{e.removeAttribute("data-cms-selected")}),u.selectedUid!==(e??null)&&h({selectedUid:e??null}),!e)return;let r=m.querySelector(c(e));r&&(r.setAttribute("data-cms-selected",""),t&&r.scrollIntoView({behavior:"smooth",block:"center"}))},x=e=>{let r=e.target,n=r?.closest?.(`[${t}]`);if(!n)return;e.preventDefault(),e.stopPropagation();let o=n.getAttribute(t);y(o,!1),f("select",{uid:o})};m.addEventListener("click",x,!0),g.push(()=>m.removeEventListener("click",x,!0));let w=()=>f("ready",{url:e.location.href}),k=new Map,E=0,S=e=>{let t=k.get(e);return t?(clearTimeout(t.timer),k.delete(e),0===k.size&&h({prompting:!1}),t):null};b.prompt=(e,t={})=>{let r=e.trim();if(!r)return Promise.reject(Error("Say what to change"));if(!u.features.includes("prompt"))return Promise.reject(Error("The editor does not offer prompts here"));let n=`prompt_${++E}`,o=void 0===t.uid?u.selectedUid:t.uid;return new Promise((e,t)=>{let a=setTimeout(()=>{S(n)?.reject(Error("The editor did not answer in time"))},12e4);k.set(n,{resolve:e,reject:t,timer:a}),h({prompting:!0}),f("prompt",{id:n,prompt:r,uid:o})})},b.undo=()=>f("undo"),b.select=e=>{y(e,!1),f("select",{uid:e})},g.push(()=>{for(let e of k.keys())S(e)?.reject(Error("The bridge was destroyed"))});let C=t=>{if(t.origin!==r||t.source!==e.parent)return;let n=t.data;if(n&&"obelism-content-editor"===n.source){if("connect"===n.type)w();else if("update"===n.type&&n.entry)p.emit("update",{entry:n.entry,selectedUid:n.selectedUid??null}),e.requestAnimationFrame(()=>y(n.selectedUid,!1));else if("select"===n.type)y(n.uid,!0);else if("saved"===n.type||"published"===n.type)p.emit(n.type,{}),0===p.listeners.update.size&&e.location.reload();else if("features"===n.type&&Array.isArray(n.features))h({features:n.features.filter(e=>"string"==typeof e)});else if("promptState"===n.type)h({canUndo:!0===n.canUndo});else if("promptResult"===n.type&&"string"==typeof n.id){let e=S(n.id);if(!e)return;!0===n.ok?e.resolve({summary:"string"==typeof n.summary?n.summary:""}):e.reject(Error("string"==typeof n.error?n.error:"The prompt failed"))}}};return e.addEventListener("message",C),g.push(()=>e.removeEventListener("message",C)),"loading"===m.readyState?(m.addEventListener("DOMContentLoaded",w,{once:!0}),g.push(()=>m.removeEventListener("DOMContentLoaded",w))):w(),b})(u,new URL(e||v).origin)).isEditing||u.parent!==u?null:(e=>{for(let t of e.cookie.split(";")){let[e,...r]=t.trim().split("=");if("obelism_content_editing"===e)try{let e=decodeURIComponent(r.join("="));return e.startsWith("/")&&!e.startsWith("//")?e:null}catch{break}}return null})(u.document);if(p&&(g=((e,n,{pagePath:o=e=>`/${e}`}={})=>{let l=e.document,d=s(),p=[],g=new Map,u=[],h=new Set,b=null,m=i,f=e=>{m={...m,...e},d.emit("change",m)},v=()=>f({unsaved:[...g.values()].some(({content:e,saved:t})=>!y(e,t)),canUndo:u.length>0}),x=async(t,r)=>{let o=await e.fetch(`${n}/api/${t}`,void 0===r?{credentials:"same-origin"}:{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json","x-obelism-content":"1"},body:JSON.stringify(r)}),a=await o.json().catch(()=>({}));if(401===o.status)throw f({features:[]}),Error("You were signed out. Sign in again to edit.");if(!o.ok)throw Error("string"==typeof a.error?a.error:`Obelism Content answered ${o.status}`);return a},w=()=>l.querySelector(`[${r}]`)?.getAttribute(r)??null,k=()=>{if(!b)return;let e=b.space.locales.map(({code:e})=>e),t=l.documentElement.lang;if(e.includes(t))return t;let r=t.split("-")[0]??"";return e.includes(r)?r:b.space.defaultLocale},E=async e=>{let t=g.get(e);if(t)return t;let r=await x(`entries/${encodeURIComponent(e)}`),n={content:r.content,saved:r.content,revision:r.draftRevision};return g.set(e,n),n},S=e=>{l.querySelectorAll("[data-cms-selected]").forEach(e=>{e.removeAttribute("data-cms-selected")}),m.selectedUid!==e&&f({selectedUid:e}),e&&l.querySelector(c(e))?.setAttribute("data-cms-selected","")},C=async t=>{if(0===d.listeners.update.size)return;let r=g.get(t)?.content,{entry:n}=await x(`entries/${encodeURIComponent(t)}/preview`,{content:r,locale:k()});d.emit("update",{entry:n,selectedUid:m.selectedUid}),e.requestAnimationFrame(()=>S(m.selectedUid))},N=t=>{d.emit(t,{}),0===d.listeners.update.size&&e.location.reload()},U=async()=>{for(let[e,t]of g){if(y(t.content,t.saved))continue;let{draftRevision:r}=await x(`entries/${encodeURIComponent(e)}/save`,{content:t.content,baseRevision:t.revision});t.saved=t.content,t.revision=r,h.add(e)}u.length=0,v()},P={isEditing:!0,mode:"site",get entryId(){return w()},environment:null,on:d.on,getState:()=>m,async prompt(e,t={}){let n,o=e.trim();if(!o)throw Error("Say what to change");if(!m.features.includes("prompt"))throw Error("You may not edit content here");let a=void 0===t.uid?m.selectedUid:t.uid,i=(n=a?l.querySelector(c(a)):null,n?.closest(`[${r}]`)?.getAttribute(r)??w());if(!i)throw Error("Nothing on this page is marked as an entry: add editable(entry)");f({prompting:!0});try{let e=await E(i),t=await x(`entries/${encodeURIComponent(i)}/prompt`,{prompt:o,content:e.content,locale:k(),uid:a});return u.push({entryId:i,content:e.content}),e.content=t.content,v(),await C(i),{summary:t.summary}}finally{f({prompting:!1})}},async createPage(t,r){let n=r.trim();if(!n)throw Error("Say what the page is for");if(!t.trim())throw Error("Say where the page goes, like /pricing");if(!m.features.includes("create"))throw Error("You may not add pages here");if(m.unsaved)throw Error("Save or undo your changes before adding a page");f({prompting:!0});try{let r=await x("entries",{path:t,prompt:n,locale:k(),from:w()}),a=o(r.path);return e.location.assign(a),{summary:r.summary,entryId:r.entryId,path:r.path,url:a}}finally{f({prompting:!1})}},undo(){let e=u.pop(),t=e&&g.get(e.entryId);e&&t&&(t.content=e.content,v(),C(e.entryId).catch(e=>console.error("[@obelism/content bridge]",e)))},async save(){if(!m.features.includes("save"))throw Error("You may not edit content here");f({saving:!0});try{await U()}finally{f({saving:!1})}N("saved")},async publish(){if(!m.features.includes("publish"))throw Error("You may not publish here");f({publishing:!0});try{await U();let e=w(),t=h.size>0?[...h]:e?[e]:[];if(0===t.length)throw Error("Nothing on this page is marked as an entry: add editable(entry)");for(let e of t)await x(`entries/${encodeURIComponent(e)}/publish`,{}),h.delete(e)}finally{f({publishing:!1})}N("published")},signOut(){if(m.unsaved&&!e.confirm("Sign out and lose the changes you have not saved?"))return;g.clear(),v();let t=e.location.pathname+e.location.search;e.location.href=`${n}/logout?return_to=${encodeURIComponent(t)}`},select(e){S(e)},setSelecting(e){e?l.documentElement.setAttribute("data-cms-selecting",""):l.documentElement.removeAttribute("data-cms-selecting"),f({selecting:e})},destroy(){p.splice(0).forEach(e=>e())}},L=l.createElement("style");L.textContent=a("html[data-cms-selecting] "),l.head.appendChild(L),p.push(()=>L.remove()),p.push(()=>l.documentElement.removeAttribute("data-cms-selecting"));let $=e=>{if(!m.selecting)return;let r=e.target,n=r?.closest?.(`[${t}]`);n&&(e.preventDefault(),e.stopPropagation(),S(n.getAttribute(t)))};l.addEventListener("click",$,!0),p.push(()=>l.removeEventListener("click",$,!0));let A=e=>{m.unsaved&&(e.preventDefault(),e.returnValue="")};return e.addEventListener("beforeunload",A),p.push(()=>e.removeEventListener("beforeunload",A)),x("session").then(e=>{b=e,f({features:e.features})}).catch(e=>console.error("[@obelism/content bridge]",e)),P})(u,p,{pagePath:l})),g.isEditing&&o){let e=f(u.document,g,!0===o?{}:o),t=g.destroy;g.destroy=()=>{e(),t()}}}return u[x]=g,g};(e=window).ObelismBridge=k((e=>{let t=e?.dataset.url,r=t||v;if(!t&&e?.src){let t=new URL(e.src);"/bridge.js"===t.pathname&&(r=t.origin)}return{url:r,prompt:e?.dataset.prompt!=="false"}})(document.currentScript)),e.ObelismContent={getContentBridge:k,mountPromptBox:f}}();
|
|
182
|
+
`,x=(e,t,r={},...n)=>{let o=e.createElement(t);for(let t of(Object.assign(o,r),n))o.appendChild("string"==typeof t?e.createTextNode(t):t);return o},w=(e,t)=>{let r=x(e,"span",{className:"icon"});return r.setAttribute("aria-hidden","true"),r.innerHTML=`<svg viewBox="0 0 24 24">${v[t]}</svg>`,r},k=(e,t,r={})=>{let n={...f,...r.labels},o=x(e,"div");o.setAttribute("data-obelism-content-prompt","");let a=o.attachShadow({mode:"open"}),i="site"===t.mode,s=x(e,"span",{textContent:n.open}),l=x(e,"button",{className:"pill",type:"button"},s,x(e,"span",{className:"mark",textContent:"⁜"})),d=x(e,"span",{className:"pill compact"},`${n.viewingDrafts} \xb7 `),c=x(e,"button",{className:"link",type:"button",textContent:n.signOut});d.appendChild(c);let p=x(e,"p",{className:"status"});p.setAttribute("role","status");let g=x(e,"textarea",{placeholder:n.placeholder,rows:1});g.setAttribute("aria-label",n.open);let u=w(e,"page"),h=w(e,"block"),b=x(e,"span"),m=x(e,"button",{type:"button",title:n.clearBlock});m.setAttribute("aria-label",n.clearBlock),m.appendChild(w(e,"close"));let v=x(e,"span",{className:"chip scope"},u,h,b,m),k=x(e,"button",{className:"action chip",type:"button"},w(e,"undo"),n.undo),E=x(e,"button",{className:"action chip",type:"button"},w(e,"newPage"),n.newPage),S=x(e,"button",{type:"button",title:n.newPageCancel});S.setAttribute("aria-label",n.newPageCancel),S.appendChild(w(e,"close"));let C=x(e,"span",{className:"chip scope"},w(e,"newPage"),x(e,"span",{textContent:n.newPage}),S),N=x(e,"input",{type:"text",placeholder:n.pathPlaceholder,autocomplete:"off",spellcheck:!1}),L=x(e,"label",{className:"path"},x(e,"span",{textContent:n.path}),N),U=x(e,"button",{className:"close icon-button",type:"button",title:n.close},w(e,"close"));U.setAttribute("aria-label",n.close);let P=x(e,"button",{className:"action primary icon-button",type:"submit"},x(e,"span",{className:"visually-hidden",textContent:n.send}),w(e,"send"));P.title=n.send;let $=x(e,"span",{className:"unsaved"});$.appendChild(x(e,"span",{className:"dot"})),$.appendChild(e.createTextNode(n.unsaved));let A=x(e,"button",{className:"link",type:"button",textContent:n.signOut}),O=x(e,"button",{className:"action chip",type:"button",textContent:n.save}),I=x(e,"button",{className:"action chip",type:"button",textContent:n.publish}),M=x(e,"div",{className:"footer"},$,x(e,"span",{className:"spacer"}),A,O,I),j=x(e,"form",{className:"panel"},p,L,g,x(e,"div",{className:"toolbar"},v,C,k,E,x(e,"span",{className:"spacer"}),U,x(e,"span",{className:"divider"}),P),M),z=x(e,"div",{className:"root"},x(e,"div",{className:"bar"},l,d),j);a.append(x(e,"style",{textContent:y}),z);let R=!1,B=!1,D=null,T=e=>{let r=e.features.includes("prompt"),o=e.saving||e.publishing,a=e.prompting||o;z.hidden=!r&&!i,l.hidden=R||!r,d.hidden=r||!i,j.hidden=!R||!r;let c=i&&e.features.includes("create"),f=i&&null===t.entryId,y=f&&c;c||(B=!1),y&&(B=!0),s.textContent=y?n.newPage:n.open,S.hidden=y,v.hidden=B,C.hidden=!B,E.hidden=B||!c,E.disabled=a,L.hidden=!B,N.disabled=a,g.placeholder=B?n.newPagePlaceholder:n.placeholder,b.textContent=e.selectedUid?n.scopeBlock:n.scopeEntry,m.hidden=!e.selectedUid,u.hidden=!!e.selectedUid,h.hidden=!e.selectedUid,k.hidden=B||!e.canUndo,k.disabled=a,P.disabled=a||""===g.value.trim()||B&&""===N.value.trim(),g.disabled=a,O.hidden=!e.features.includes("save")||f,O.disabled=a||!e.unsaved,O.textContent=e.saving?n.saving:n.save,I.hidden=!e.features.includes("publish")||f,I.disabled=a,I.textContent=e.publishing?n.publishing:n.publish,A.hidden=!i,M.hidden=!i,$.hidden=!e.unsaved;let x=e.prompting?B?n.creating:n.working:D?.text??(!i||B||e.selectedUid?"":n.selectHint);p.textContent=x,p.hidden=!x,p.className=`status${e.prompting?" working":""}${!a&&D?.error?" error":""}`},q=()=>T(t.getState()),V=()=>{g.style.height="auto",g.style.height=`${g.scrollHeight}px`},H=async()=>{let e=g.value.trim();if(e&&!t.getState().prompting){D=null;try{if(B){let r=N.value.trim();if(!r)return N.focus();await t.createPage(r,e),D={text:n.created,error:!1}}else{let{summary:r}=await t.prompt(e);D={text:r,error:!1}}g.value="",V()}catch(e){D={text:e instanceof Error?e.message:String(e),error:!0}}q(),g.focus()}},F=e=>{R=e,q(),i&&t.setSelecting(R&&!B)},Y=e=>{B=e,D=null,t.setSelecting(R&&!B),B&&t.select(null),q(),(B?N:g).focus()},_=async e=>{D=null;try{await t[e](),D={text:"save"===e?n.saved:n.published,error:!1}}catch(e){D={text:e instanceof Error?e.message:String(e),error:!0}}q()};l.addEventListener("click",()=>{F(!0),(B?N:g).focus()}),U.addEventListener("click",()=>F(!1)),O.addEventListener("click",()=>void _("save")),I.addEventListener("click",()=>void _("publish")),A.addEventListener("click",()=>t.signOut()),c.addEventListener("click",()=>t.signOut()),m.addEventListener("click",()=>t.select(null)),E.addEventListener("click",()=>Y(!0)),S.addEventListener("click",()=>Y(!1)),N.addEventListener("input",q),N.addEventListener("keydown",e=>{"Enter"!==e.key||e.isComposing?"Escape"===e.key&&F(!1):(e.preventDefault(),g.focus())}),k.addEventListener("click",()=>{D=null,t.undo()}),j.addEventListener("submit",e=>{e.preventDefault(),H()}),g.addEventListener("input",()=>{V(),q()}),g.addEventListener("keydown",e=>{"Enter"!==e.key||e.shiftKey||e.isComposing?"Escape"===e.key&&F(!1):(e.preventDefault(),H())});let W=t.on("change",T);return q(),e.body.appendChild(o),()=>{W(),o.remove()}},E="https://content.obelism.studio",S=(e,t)=>JSON.stringify(e)===JSON.stringify(t),C=Symbol.for("@obelism/content/bridge"),N=m({isEditing:!1,entryId:null,on:()=>{}},new URLSearchParams),L=({url:e,prompt:l=!0,pagePath:g}={})=>{let f;if("u"<typeof window)return N;let v=window;if(v[C])return v[C];if(v.ObelismBridge)f=m(v.ObelismBridge,new URLSearchParams(v.location.search));else{let m=(f=((e,r)=>{let l=new URLSearchParams(e.location.search),g=e.parent!==e&&l.has("cms_preview"),m=p(),f=[],v={...c,selecting:!0},y=e=>{v={...v,...e},m.emit("change",v)},x={isEditing:g,mode:g?"editor":null,entryId:l.get("cms_entry"),environment:l.get(n),on:m.on,getState:()=>v,...u,destroy:()=>{f.splice(0).forEach(e=>e())}};if(!g)return x;let w=e.document,k=(t,n={})=>{e.parent.postMessage({source:"obelism-content-bridge",type:t,...n},r)},E=w.createElement("style");E.textContent=d(),w.head.appendChild(E),f.push(()=>E.remove());let S=(e,t)=>{if(w.querySelectorAll("[data-cms-selected]").forEach(e=>{e.removeAttribute("data-cms-selected")}),v.selectedUid!==(e??null)&&y({selectedUid:e??null}),!e)return;let r=w.querySelector(b(e));r&&(r.setAttribute("data-cms-selected",""),t&&r.scrollIntoView({behavior:"smooth",block:"center"}))},C=e=>{let r=e.target,n=r?.closest?.(`[${t}]`);if(!n)return;e.preventDefault(),e.stopPropagation();let o=n.getAttribute(t);S(o,!1),k("select",{uid:o})};w.addEventListener("click",C,!0),f.push(()=>w.removeEventListener("click",C,!0));let N=()=>k("ready",{url:e.location.href}),L=new Map,U=0,P=e=>{let t=L.get(e);return t?(clearTimeout(t.timer),L.delete(e),0===L.size&&y({prompting:!1}),t):null};x.prompt=(e,t={})=>{let r=e.trim();if(!r)return Promise.reject(Error("Say what to change"));if(!v.features.includes("prompt"))return Promise.reject(Error("The editor does not offer prompts here"));let n=`prompt_${++U}`,o=void 0===t.uid?v.selectedUid:t.uid;return new Promise((e,t)=>{let a=setTimeout(()=>{P(n)?.reject(Error("The editor did not answer in time"))},12e4);L.set(n,{resolve:e,reject:t,timer:a}),y({prompting:!0}),k("prompt",{id:n,prompt:r,uid:o})})},x.undo=()=>k("undo"),x.select=e=>{S(e,!1),k("select",{uid:e})};let $=null;f.push(()=>$?.destroy()),f.push(()=>{for(let e of L.keys())P(e)?.reject(Error("The bridge was destroyed"))});let A=t=>{if(t.origin!==r||t.source!==e.parent)return;let n=t.data;if(n&&"obelism-content-editor"===n.source){if("connect"===n.type)N();else if("update"===n.type&&n.entry)m.emit("update",{entry:n.entry,selectedUid:n.selectedUid??null}),e.requestAnimationFrame(()=>S(n.selectedUid,!1)),$?.refresh();else if("select"===n.type)S(n.uid,!0);else if("saved"===n.type||"published"===n.type)m.emit(n.type,{}),0===m.listeners.update.size&&e.location.reload();else if("features"===n.type&&Array.isArray(n.features))y({features:n.features.filter(e=>"string"==typeof e)});else if("stats"===n.type)y({stats:(e=>{if(!e||"object"!=typeof e||Array.isArray(e))return{};let t={};for(let[r,n]of Object.entries(e)){let{clicks:e,clickers:o,viewers:a,rate:i}=n??{};h(e)&&h(o)&&h(a)&&h(i)&&(t[r]={clicks:e,clickers:o,viewers:a,rate:i})}return t})(n.stats),statsVisible:!1!==n.visible}),($||Object.keys(v.stats).length)&&($??($=((e,{selector:t,onSelect:r,labels:n})=>{let l=e.document,d={...o,...n},c=l.createElement("div");c.setAttribute("data-obelism-stats","");let p=c.attachShadow({mode:"open"}),g=l.createElement("style");g.textContent=i;let u=l.createElement("div");u.className="layer",p.append(g,u),l.body.appendChild(c);let h={},b=!1,m=null,f=()=>{if(m=null,u.replaceChildren(),u.hidden=!b,!b)return;let n=e.innerHeight;for(let[o,i]of Object.entries(h)){let c=l.querySelector(t(o));if(!c)continue;let p=c.getBoundingClientRect();if(!p.width&&!p.height||p.bottom<0||p.top>n)continue;let g=l.createElement("button");g.type="button",g.className="badge",g.dataset.uid=o,g.textContent=a(d.clicked,{rate:Math.round(100*i.rate)}),g.title=a(d.title,{clickers:i.clickers,viewers:i.viewers,clicks:i.clicks});let{place:h,top:b,left:m}=s(p,e.innerWidth);g.dataset.place=h,g.style.top=`${b}px`,g.style.left=`${m}px`,g.addEventListener("click",()=>r(o)),u.appendChild(g)}},v=()=>{null===m&&b&&(m=e.requestAnimationFrame(f))};e.addEventListener("scroll",v,{capture:!0,passive:!0}),e.addEventListener("resize",v);let y=e.MutationObserver,x="function"==typeof y?new y(v):null;return x?.observe(l.body,{childList:!0,subtree:!0}),{update(t,r){h=t,b=r,null!==m&&(e.cancelAnimationFrame?.(m),m=null),f()},refresh:v,destroy(){null!==m&&e.cancelAnimationFrame?.(m),e.removeEventListener("scroll",v,{capture:!0}),e.removeEventListener("resize",v),x?.disconnect(),c.remove()}}})(e,{selector:b,onSelect:e=>x.select(e)})),$.update(v.stats,v.statsVisible));else if("promptState"===n.type)y({canUndo:!0===n.canUndo});else if("promptResult"===n.type&&"string"==typeof n.id){let e=P(n.id);if(!e)return;!0===n.ok?e.resolve({summary:"string"==typeof n.summary?n.summary:""}):e.reject(Error("string"==typeof n.error?n.error:"The prompt failed"))}}};return e.addEventListener("message",A),f.push(()=>e.removeEventListener("message",A)),"loading"===w.readyState?(w.addEventListener("DOMContentLoaded",N,{once:!0}),f.push(()=>w.removeEventListener("DOMContentLoaded",N))):N(),x})(v,new URL(e||E).origin)).isEditing||v.parent!==v?null:(e=>{for(let t of e.cookie.split(";")){let[e,...r]=t.trim().split("=");if("obelism_content_editing"===e)try{let e=decodeURIComponent(r.join("="));return e.startsWith("/")&&!e.startsWith("//")?e:null}catch{break}}return null})(v.document);if(m&&(f=((e,n,{pagePath:o=e=>`/${e}`}={})=>{let a=e.document,i=p(),s=[],l=new Map,g=[],u=new Set,h=null,m=c,f=e=>{m={...m,...e},i.emit("change",m)},v=()=>f({unsaved:[...l.values()].some(({content:e,saved:t})=>!S(e,t)),canUndo:g.length>0}),y=async(t,r)=>{let o=await e.fetch(`${n}/api/${t}`,void 0===r?{credentials:"same-origin"}:{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json","x-obelism-content":"1"},body:JSON.stringify(r)}),a=await o.json().catch(()=>({}));if(401===o.status)throw f({features:[]}),Error("You were signed out. Sign in again to edit.");if(!o.ok)throw Error("string"==typeof a.error?a.error:`Obelism Content answered ${o.status}`);return a},x=()=>a.querySelector(`[${r}]`)?.getAttribute(r)??null,w=()=>{if(!h)return;let e=h.space.locales.map(({code:e})=>e),t=a.documentElement.lang;if(e.includes(t))return t;let r=t.split("-")[0]??"";return e.includes(r)?r:h.space.defaultLocale},k=async e=>{let t=l.get(e);if(t)return t;let r=await y(`entries/${encodeURIComponent(e)}`),n={content:r.content,saved:r.content,revision:r.draftRevision};return l.set(e,n),n},E=e=>{a.querySelectorAll("[data-cms-selected]").forEach(e=>{e.removeAttribute("data-cms-selected")}),m.selectedUid!==e&&f({selectedUid:e}),e&&a.querySelector(b(e))?.setAttribute("data-cms-selected","")},C=async t=>{if(0===i.listeners.update.size)return;let r=l.get(t)?.content,{entry:n}=await y(`entries/${encodeURIComponent(t)}/preview`,{content:r,locale:w()});i.emit("update",{entry:n,selectedUid:m.selectedUid}),e.requestAnimationFrame(()=>E(m.selectedUid))},N=t=>{i.emit(t,{}),0===i.listeners.update.size&&e.location.reload()},L=async()=>{for(let[e,t]of l){if(S(t.content,t.saved))continue;let{draftRevision:r}=await y(`entries/${encodeURIComponent(e)}/save`,{content:t.content,baseRevision:t.revision});t.saved=t.content,t.revision=r,u.add(e)}g.length=0,v()},U={isEditing:!0,mode:"site",get entryId(){return x()},environment:null,on:i.on,getState:()=>m,async prompt(e,t={}){let n,o=e.trim();if(!o)throw Error("Say what to change");if(!m.features.includes("prompt"))throw Error("You may not edit content here");let i=void 0===t.uid?m.selectedUid:t.uid,s=(n=i?a.querySelector(b(i)):null,n?.closest(`[${r}]`)?.getAttribute(r)??x());if(!s)throw Error("Nothing on this page is marked as an entry: add editable(entry)");f({prompting:!0});try{let e=await k(s),t=await y(`entries/${encodeURIComponent(s)}/prompt`,{prompt:o,content:e.content,locale:w(),uid:i});return g.push({entryId:s,content:e.content}),e.content=t.content,v(),await C(s),{summary:t.summary}}finally{f({prompting:!1})}},async createPage(t,r){let n=r.trim();if(!n)throw Error("Say what the page is for");if(!t.trim())throw Error("Say where the page goes, like /pricing");if(!m.features.includes("create"))throw Error("You may not add pages here");if(m.unsaved)throw Error("Save or undo your changes before adding a page");f({prompting:!0});try{let r=await y("entries",{path:t,prompt:n,locale:w(),from:x()}),a=o(r.path,w()??"");return e.location.assign(a),{summary:r.summary,entryId:r.entryId,path:r.path,url:a}}finally{f({prompting:!1})}},undo(){let e=g.pop(),t=e&&l.get(e.entryId);e&&t&&(t.content=e.content,v(),C(e.entryId).catch(e=>console.error("[@obelism/content bridge]",e)))},async save(){if(!m.features.includes("save"))throw Error("You may not edit content here");f({saving:!0});try{await L()}finally{f({saving:!1})}N("saved")},async publish(){if(!m.features.includes("publish"))throw Error("You may not publish here");f({publishing:!0});try{await L();let e=x(),t=u.size>0?[...u]:e?[e]:[];if(0===t.length)throw Error("Nothing on this page is marked as an entry: add editable(entry)");for(let e of t)await y(`entries/${encodeURIComponent(e)}/publish`,{}),u.delete(e)}finally{f({publishing:!1})}N("published")},signOut(){if(m.unsaved&&!e.confirm("Sign out and lose the changes you have not saved?"))return;l.clear(),v();let t=e.location.pathname+e.location.search;e.location.href=`${n}/logout?return_to=${encodeURIComponent(t)}`},select(e){E(e)},setSelecting(e){e?a.documentElement.setAttribute("data-cms-selecting",""):a.documentElement.removeAttribute("data-cms-selecting"),f({selecting:e})},destroy(){s.splice(0).forEach(e=>e())}},P=a.createElement("style");P.textContent=d("html[data-cms-selecting] "),a.head.appendChild(P),s.push(()=>P.remove()),s.push(()=>a.documentElement.removeAttribute("data-cms-selecting"));let $=e=>{if(!m.selecting)return;let r=e.target,n=r?.closest?.(`[${t}]`);n&&(e.preventDefault(),e.stopPropagation(),E(n.getAttribute(t)))};a.addEventListener("click",$,!0),s.push(()=>a.removeEventListener("click",$,!0));let A=e=>{m.unsaved&&(e.preventDefault(),e.returnValue="")};return e.addEventListener("beforeunload",A),s.push(()=>e.removeEventListener("beforeunload",A)),y("session").then(e=>{h=e,f({features:e.features})}).catch(e=>console.error("[@obelism/content bridge]",e)),U})(v,m,{pagePath:g})),f.isEditing&&l){let e=k(v.document,f,!0===l?{}:l),t=f.destroy;f.destroy=()=>{e(),t()}}}return v[C]=f,f};(e=window).ObelismBridge=L((e=>{let t=e?.dataset.url,r=t||E;if(!t&&e?.src){let t=new URL(e.src);"/bridge.js"===t.pathname&&(r=t.origin)}return{url:r,prompt:e?.dataset.prompt!=="false"}})(document.currentScript)),e.ObelismContent={getContentBridge:L,mountPromptBox:k}}();
|
package/dist/bridge.js
CHANGED
|
@@ -4,6 +4,167 @@ var preview = require('./preview-mpwfmnla.js');
|
|
|
4
4
|
var site = require('./site.js');
|
|
5
5
|
var config = require('./config-ci2hxmas.js');
|
|
6
6
|
|
|
7
|
+
// Click rates on the page: in the visual editor, a small label on each block
|
|
8
|
+
// that numbers has clicks for ("24% clicked"), from the editor's `stats`
|
|
9
|
+
// message. The editor's "Show click rates" switch turns it on and off. Plain
|
|
10
|
+
// DOM in a shadow root, like the prompt box, in a fixed layer that follows
|
|
11
|
+
// the blocks as the page scrolls, resizes and re-renders.
|
|
12
|
+
//
|
|
13
|
+
// Sites tag a block's clickable element with `data-numbers-id={block._uid}`
|
|
14
|
+
// and load Obelism numbers' script; the CMS reads the rates from numbers
|
|
15
|
+
// (the CMS's content/docs/guides/visual-editor.mdx, "Click rates").
|
|
16
|
+
const DEFAULT_STATS_LABELS = {
|
|
17
|
+
clicked: '{rate}% clicked',
|
|
18
|
+
title: '{clickers} of {viewers} visitors of its page clicked it in the last 30 days ({clicks} clicks)'
|
|
19
|
+
};
|
|
20
|
+
const fill = (template, values)=>template.replace(/\{(\w+)\}/g, (match, key)=>key in values ? String(values[key]) : match);
|
|
21
|
+
// The same tokens as the prompt box: near-black pill, beige text, the orange
|
|
22
|
+
// accent for the rate.
|
|
23
|
+
const CSS$1 = `
|
|
24
|
+
:host { all: initial; }
|
|
25
|
+
.layer {
|
|
26
|
+
position: fixed; inset: 0; pointer-events: none;
|
|
27
|
+
z-index: 2147483646;
|
|
28
|
+
font: 600 12px/1 'InterVariable', 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
29
|
+
-webkit-font-smoothing: antialiased;
|
|
30
|
+
}
|
|
31
|
+
.badge {
|
|
32
|
+
position: absolute; transform: translateX(-100%);
|
|
33
|
+
display: inline-flex; gap: 6px; align-items: center;
|
|
34
|
+
padding: 6px 10px; white-space: nowrap; cursor: pointer;
|
|
35
|
+
pointer-events: auto; border: 0;
|
|
36
|
+
color: rgb(238 236 233); background: rgb(12 12 12 / .88);
|
|
37
|
+
border-radius: 5em; box-shadow: 0 6px 16px -8px rgb(12 12 12 / .5);
|
|
38
|
+
font: inherit;
|
|
39
|
+
}
|
|
40
|
+
.badge::before {
|
|
41
|
+
content: ''; width: 8px; height: 8px; border-radius: 50%;
|
|
42
|
+
background: rgb(212 130 30);
|
|
43
|
+
}
|
|
44
|
+
.badge[data-place='beside'] { transform: translateY(-50%); }
|
|
45
|
+
.badge[data-place='above'] { transform: translate(-100%, -100%); }
|
|
46
|
+
.badge:focus-visible { outline: 2px solid rgb(212 130 30); outline-offset: 2px; }
|
|
47
|
+
`;
|
|
48
|
+
// Inset from a block's top-right corner, or gap beside a small element.
|
|
49
|
+
const INSET = 8;
|
|
50
|
+
// Below this an element is a button or a link, not a section: its label goes
|
|
51
|
+
// beside it (or above, without room on the right) so it doesn't cover it.
|
|
52
|
+
const SMALL_HEIGHT = 64;
|
|
53
|
+
// Room a label needs beside an element.
|
|
54
|
+
const LABEL_WIDTH = 140;
|
|
55
|
+
// Where a label goes for an element's box: inside the top-right corner of a
|
|
56
|
+
// block, beside a small element, or above one at the right edge.
|
|
57
|
+
const placeLabel = (rect, viewportWidth)=>{
|
|
58
|
+
if (rect.height >= SMALL_HEIGHT) return {
|
|
59
|
+
place: 'inside',
|
|
60
|
+
top: Math.max(rect.top, 0) + INSET,
|
|
61
|
+
left: rect.right - INSET
|
|
62
|
+
};
|
|
63
|
+
if (rect.right + INSET + LABEL_WIDTH <= viewportWidth) return {
|
|
64
|
+
place: 'beside',
|
|
65
|
+
top: rect.top + rect.height / 2,
|
|
66
|
+
left: rect.right + INSET
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
place: 'above',
|
|
70
|
+
top: rect.top - INSET / 2,
|
|
71
|
+
left: rect.right
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
const mountStatsOverlay = (win, { selector, onSelect, labels: labelOverrides })=>{
|
|
75
|
+
const doc = win.document;
|
|
76
|
+
const labels = {
|
|
77
|
+
...DEFAULT_STATS_LABELS,
|
|
78
|
+
...labelOverrides
|
|
79
|
+
};
|
|
80
|
+
const host = doc.createElement('div');
|
|
81
|
+
host.setAttribute('data-obelism-stats', '');
|
|
82
|
+
const shadow = host.attachShadow({
|
|
83
|
+
mode: 'open'
|
|
84
|
+
});
|
|
85
|
+
const style = doc.createElement('style');
|
|
86
|
+
style.textContent = CSS$1;
|
|
87
|
+
const layer = doc.createElement('div');
|
|
88
|
+
layer.className = 'layer';
|
|
89
|
+
shadow.append(style, layer);
|
|
90
|
+
doc.body.appendChild(host);
|
|
91
|
+
let stats = {};
|
|
92
|
+
let visible = false;
|
|
93
|
+
let frame = null;
|
|
94
|
+
const render = ()=>{
|
|
95
|
+
frame = null;
|
|
96
|
+
layer.replaceChildren();
|
|
97
|
+
layer.hidden = !visible;
|
|
98
|
+
if (!visible) return;
|
|
99
|
+
const height = win.innerHeight;
|
|
100
|
+
for (const [uid, block] of Object.entries(stats)){
|
|
101
|
+
const element = doc.querySelector(selector(uid));
|
|
102
|
+
if (!element) continue;
|
|
103
|
+
const rect = element.getBoundingClientRect();
|
|
104
|
+
// Not rendered, or off screen.
|
|
105
|
+
if (!rect.width && !rect.height) continue;
|
|
106
|
+
if (rect.bottom < 0 || rect.top > height) continue;
|
|
107
|
+
const badge = doc.createElement('button');
|
|
108
|
+
badge.type = 'button';
|
|
109
|
+
badge.className = 'badge';
|
|
110
|
+
badge.dataset.uid = uid;
|
|
111
|
+
badge.textContent = fill(labels.clicked, {
|
|
112
|
+
rate: Math.round(block.rate * 100)
|
|
113
|
+
});
|
|
114
|
+
badge.title = fill(labels.title, {
|
|
115
|
+
clickers: block.clickers,
|
|
116
|
+
viewers: block.viewers,
|
|
117
|
+
clicks: block.clicks
|
|
118
|
+
});
|
|
119
|
+
const { place, top, left } = placeLabel(rect, win.innerWidth);
|
|
120
|
+
badge.dataset.place = place;
|
|
121
|
+
badge.style.top = `${top}px`;
|
|
122
|
+
badge.style.left = `${left}px`;
|
|
123
|
+
badge.addEventListener('click', ()=>onSelect(uid));
|
|
124
|
+
layer.appendChild(badge);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
// At most once a frame, however many scroll and mutation events come in.
|
|
128
|
+
const schedule = ()=>{
|
|
129
|
+
if (frame !== null || !visible) return;
|
|
130
|
+
frame = win.requestAnimationFrame(render);
|
|
131
|
+
};
|
|
132
|
+
win.addEventListener('scroll', schedule, {
|
|
133
|
+
capture: true,
|
|
134
|
+
passive: true
|
|
135
|
+
});
|
|
136
|
+
win.addEventListener('resize', schedule);
|
|
137
|
+
// The site re-renders blocks on every `update`; follow them. Our own
|
|
138
|
+
// layer lives in a shadow root, so its changes don't come back here.
|
|
139
|
+
const Observer = win.MutationObserver;
|
|
140
|
+
const observer = typeof Observer === 'function' ? new Observer(schedule) : null;
|
|
141
|
+
observer?.observe(doc.body, {
|
|
142
|
+
childList: true,
|
|
143
|
+
subtree: true
|
|
144
|
+
});
|
|
145
|
+
return {
|
|
146
|
+
update (nextStats, nextVisible) {
|
|
147
|
+
stats = nextStats;
|
|
148
|
+
visible = nextVisible;
|
|
149
|
+
if (frame !== null) {
|
|
150
|
+
win.cancelAnimationFrame?.(frame);
|
|
151
|
+
frame = null;
|
|
152
|
+
}
|
|
153
|
+
render();
|
|
154
|
+
},
|
|
155
|
+
refresh: schedule,
|
|
156
|
+
destroy () {
|
|
157
|
+
if (frame !== null) win.cancelAnimationFrame?.(frame);
|
|
158
|
+
win.removeEventListener('scroll', schedule, {
|
|
159
|
+
capture: true
|
|
160
|
+
});
|
|
161
|
+
win.removeEventListener('resize', schedule);
|
|
162
|
+
observer?.disconnect();
|
|
163
|
+
host.remove();
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
|
|
7
168
|
// The site's half of the visual editor (the CMS's public/bridge.js, as a
|
|
8
169
|
// module). Inside the editor's iframe it outlines blocks, tells the editor
|
|
9
170
|
// which one was clicked, hands the editor's live, unsaved entry to listeners
|
|
@@ -15,7 +176,7 @@ var config = require('./config-ci2hxmas.js');
|
|
|
15
176
|
// 'select' | 'prompt' | 'undo', uid?, id?, prompt? }
|
|
16
177
|
// - editor → site: { source: 'obelism-content-editor', type: 'connect' |
|
|
17
178
|
// 'update' | 'select' | 'saved' | 'published' | 'features' |
|
|
18
|
-
// 'promptResult' | 'promptState', … }
|
|
179
|
+
// 'promptResult' | 'promptState' | 'stats', … }
|
|
19
180
|
const BRIDGE_SOURCE = 'obelism-content-bridge';
|
|
20
181
|
const EDITOR_SOURCE = 'obelism-content-editor';
|
|
21
182
|
// How long a prompt may take before it counts as failed.
|
|
@@ -31,7 +192,9 @@ const INITIAL_STATE = {
|
|
|
31
192
|
unsaved: false,
|
|
32
193
|
saving: false,
|
|
33
194
|
publishing: false,
|
|
34
|
-
selecting: false
|
|
195
|
+
selecting: false,
|
|
196
|
+
stats: {},
|
|
197
|
+
statsVisible: false
|
|
35
198
|
};
|
|
36
199
|
const createEmitter = ()=>{
|
|
37
200
|
const listeners = {
|
|
@@ -72,6 +235,22 @@ const INERT_METHODS = {
|
|
|
72
235
|
select: ()=>{},
|
|
73
236
|
setSelecting: ()=>{}
|
|
74
237
|
};
|
|
238
|
+
const isCount = (value)=>typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
239
|
+
// The editor's `stats`, keeping only well-formed entries.
|
|
240
|
+
const readStats = (value)=>{
|
|
241
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
|
|
242
|
+
const stats = {};
|
|
243
|
+
for (const [uid, block] of Object.entries(value)){
|
|
244
|
+
const { clicks, clickers, viewers, rate } = block ?? {};
|
|
245
|
+
if (isCount(clicks) && isCount(clickers) && isCount(viewers) && isCount(rate)) stats[uid] = {
|
|
246
|
+
clicks,
|
|
247
|
+
clickers,
|
|
248
|
+
viewers,
|
|
249
|
+
rate
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return stats;
|
|
253
|
+
};
|
|
75
254
|
// The selector of the element marked with a block's uid.
|
|
76
255
|
const blockSelector = (uid)=>`[${preview.UID_ATTRIBUTE}="${uid.replace(/["\\]/g, '\\$&')}"]`;
|
|
77
256
|
// `cmsOrigin` is the only origin messages are accepted from and sent to.
|
|
@@ -200,6 +379,17 @@ const createBridge = (win, cmsOrigin)=>{
|
|
|
200
379
|
uid
|
|
201
380
|
});
|
|
202
381
|
};
|
|
382
|
+
// Mounted with the first rates, so pages without numbers never get it.
|
|
383
|
+
let overlay = null;
|
|
384
|
+
const showStats = ()=>{
|
|
385
|
+
if (!overlay && !Object.keys(state.stats).length) return;
|
|
386
|
+
overlay ??= mountStatsOverlay(win, {
|
|
387
|
+
selector: blockSelector,
|
|
388
|
+
onSelect: (uid)=>bridge.select(uid)
|
|
389
|
+
});
|
|
390
|
+
overlay.update(state.stats, state.statsVisible);
|
|
391
|
+
};
|
|
392
|
+
cleanup.push(()=>overlay?.destroy());
|
|
203
393
|
cleanup.push(()=>{
|
|
204
394
|
for (const id of pending.keys()){
|
|
205
395
|
settle(id)?.reject(new Error('The bridge was destroyed'));
|
|
@@ -220,6 +410,7 @@ const createBridge = (win, cmsOrigin)=>{
|
|
|
220
410
|
});
|
|
221
411
|
// Re-apply the selection to whatever the site rendered.
|
|
222
412
|
win.requestAnimationFrame(()=>highlight(data.selectedUid, false));
|
|
413
|
+
overlay?.refresh();
|
|
223
414
|
} else if (data.type === 'select') {
|
|
224
415
|
highlight(data.uid, true);
|
|
225
416
|
} else if (data.type === 'saved' || data.type === 'published') {
|
|
@@ -229,6 +420,13 @@ const createBridge = (win, cmsOrigin)=>{
|
|
|
229
420
|
setState({
|
|
230
421
|
features: data.features.filter((feature)=>typeof feature === 'string')
|
|
231
422
|
});
|
|
423
|
+
} else if (data.type === 'stats') {
|
|
424
|
+
// An editor from before the switch sends no `visible`: show them.
|
|
425
|
+
setState({
|
|
426
|
+
stats: readStats(data.stats),
|
|
427
|
+
statsVisible: data.visible !== false
|
|
428
|
+
});
|
|
429
|
+
showStats();
|
|
232
430
|
} else if (data.type === 'promptState') {
|
|
233
431
|
setState({
|
|
234
432
|
canUndo: data.canUndo === true
|
|
@@ -1021,7 +1219,7 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path}` } = {})
|
|
|
1021
1219
|
locale: locale(),
|
|
1022
1220
|
from: pageEntryId()
|
|
1023
1221
|
});
|
|
1024
|
-
const url = pagePath(created.path);
|
|
1222
|
+
const url = pagePath(created.path, locale() ?? '');
|
|
1025
1223
|
win.location.assign(url);
|
|
1026
1224
|
return {
|
|
1027
1225
|
summary: created.summary,
|
|
@@ -1190,7 +1388,9 @@ const getContentBridge = ({ url, prompt = true, pagePath } = {})=>{
|
|
|
1190
1388
|
};
|
|
1191
1389
|
|
|
1192
1390
|
exports.DEFAULT_PROMPT_LABELS = DEFAULT_PROMPT_LABELS;
|
|
1391
|
+
exports.DEFAULT_STATS_LABELS = DEFAULT_STATS_LABELS;
|
|
1193
1392
|
exports.INITIAL_STATE = INITIAL_STATE;
|
|
1194
1393
|
exports.getContentBridge = getContentBridge;
|
|
1195
1394
|
exports.mountPromptBox = mountPromptBox;
|
|
1395
|
+
exports.mountStatsOverlay = mountStatsOverlay;
|
|
1196
1396
|
exports.peekContentBridge = peekContentBridge;
|