@obelism/content 0.28.0 → 0.28.2
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 +12 -8
- package/dist/bridge.d.mts +6 -1
- package/dist/bridge.d.mts.map +1 -1
- package/dist/bridge.d.ts +6 -1
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.global.js +23 -7
- package/dist/bridge.js +223 -81
- package/dist/bridge.mjs +224 -82
- package/dist/site.js +5 -0
- package/dist/site.mjs +5 -0
- package/package.json +1 -1
package/dist/bridge.mjs
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { U as UID_ATTRIBUTE, P as PREVIEW_PARAMS, E as ENTRY_ATTRIBUTE, C as COLLECTION_ATTRIBUTE, a as COLLECTION_HANDLE_ATTRIBUTE, b as PRODUCT_ATTRIBUTE, c as PRODUCT_HANDLE_ATTRIBUTE } from './preview-el7sfk3h.mjs';
|
|
2
2
|
import { SITE_EDITING_COOKIE, SITE_REQUEST_HEADER } from './site.mjs';
|
|
3
3
|
import { n as normalizeUrl, D as DEFAULT_URL } from './config-c6qktlz6.mjs';
|
|
4
4
|
|
|
5
|
-
// Click rates on the page: in the visual editor, a small label on each block
|
|
6
|
-
// that numbers has clicks for ("24% clicked"), from the editor's `stats`
|
|
7
|
-
// message. The editor's "Show click rates" switch turns it on and off. Plain
|
|
8
|
-
// DOM in a shadow root, like the prompt box, in a fixed layer that follows
|
|
9
|
-
// the blocks as the page scrolls, resizes and re-renders.
|
|
10
|
-
//
|
|
11
|
-
// Sites tag a block's clickable element with `data-numbers-id={block._uid}`
|
|
12
|
-
// and load Obelism numbers' script; the CMS reads the rates from numbers
|
|
13
|
-
// (the CMS's content/docs/guides/canvas.mdx, "Click rates").
|
|
14
5
|
const DEFAULT_STATS_LABELS = {
|
|
15
6
|
clicked: '{rate}% clicked',
|
|
16
|
-
|
|
7
|
+
clickedShort: '{rate}%',
|
|
8
|
+
title: '{clickers} of {viewers} visitors of its page clicked it in the last 30 days ({clicks} clicks)',
|
|
9
|
+
noClicks: 'No clicks yet in the last 30 days',
|
|
10
|
+
counting: '{clicks} clicks so far: a click rate once {needed} visitors saw its page ({viewers} yet)'
|
|
11
|
+
};
|
|
12
|
+
// Fewer visitors than this make a rate noise.
|
|
13
|
+
const MIN_VIEWERS = 20;
|
|
14
|
+
const TAG_ATTRIBUTE = 'data-numbers-id';
|
|
15
|
+
const statsStatus = (block)=>!block || block.clicks === 0 ? 'none' : block.viewers < MIN_VIEWERS ? 'counting' : 'rate';
|
|
16
|
+
// Lucide's x and hourglass.
|
|
17
|
+
const ICONS$1 = {
|
|
18
|
+
none: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
|
|
19
|
+
counting: '<path d="M5 22h14"/><path d="M5 2h14"/><path d="M17 22v-4.172a2 2 0 0 0-.586-1.414L12 12l-4.414 4.414A2 2 0 0 0 7 17.828V22"/><path d="M7 2v4.172a2 2 0 0 0 .586 1.414L12 12l4.414-4.414A2 2 0 0 0 17 6.172V2"/>'
|
|
17
20
|
};
|
|
18
21
|
const fill$1 = (template, values)=>template.replace(/\{(\w+)\}/g, (match, key)=>key in values ? String(values[key]) : match);
|
|
19
22
|
// The same tokens as the prompt box: an ink pill, paper text, the orange
|
|
@@ -39,37 +42,42 @@ const CSS$2 = `
|
|
|
39
42
|
content: ''; width: 8px; height: 8px; border-radius: 50%;
|
|
40
43
|
background: rgb(212 130 30);
|
|
41
44
|
}
|
|
42
|
-
.badge
|
|
43
|
-
.
|
|
44
|
-
|
|
45
|
+
.badge:focus-visible, .tag:focus-visible { outline: 2px solid rgb(212 130 30); outline-offset: 2px; }
|
|
46
|
+
.tag {
|
|
47
|
+
position: absolute; transform: translate(-50%, -50%);
|
|
48
|
+
display: grid; place-items: center; width: 20px; height: 20px; padding: 0;
|
|
49
|
+
cursor: pointer; pointer-events: auto; border: 0; border-radius: 50%;
|
|
50
|
+
color: rgb(242 240 236); background: rgb(23 22 20 / .88);
|
|
51
|
+
box-shadow: 0 4px 10px -6px rgb(23 22 20 / .5);
|
|
52
|
+
}
|
|
53
|
+
.tag::after { content: ''; position: absolute; inset: -6px; }
|
|
54
|
+
.tag[data-status='counting'] { color: rgb(212 130 30); }
|
|
55
|
+
.tag[data-status='rate'] {
|
|
56
|
+
width: auto; height: 20px; padding: 0 7px; border-radius: 5em;
|
|
57
|
+
font-size: 11px; transform: translate(-70%, -50%);
|
|
58
|
+
}
|
|
59
|
+
.tag svg {
|
|
60
|
+
width: 12px; height: 12px; fill: none; stroke: currentColor;
|
|
61
|
+
stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round;
|
|
62
|
+
}
|
|
45
63
|
`;
|
|
46
|
-
// Inset from a block's top-right corner, or gap beside a small element.
|
|
47
64
|
const INSET = 8;
|
|
48
|
-
// Below this an element is a button or a link, not a section:
|
|
49
|
-
//
|
|
65
|
+
// Below this an element is a button or a link, not a section: a tag on its
|
|
66
|
+
// corner rather than a label over it.
|
|
50
67
|
const SMALL_HEIGHT = 64;
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// block, beside a small element, or above one at the right edge.
|
|
55
|
-
const placeLabel = (rect, viewportWidth)=>{
|
|
56
|
-
if (rect.height >= SMALL_HEIGHT) return {
|
|
57
|
-
place: 'inside',
|
|
68
|
+
// A block's label: inside its top-right corner, in view when it starts above
|
|
69
|
+
// the screen.
|
|
70
|
+
const placeLabel = (rect)=>({
|
|
58
71
|
top: Math.max(rect.top, 0) + INSET,
|
|
59
72
|
left: rect.right - INSET
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
top: rect.top - INSET / 2,
|
|
69
|
-
left: rect.right
|
|
70
|
-
};
|
|
71
|
-
};
|
|
72
|
-
const mountStatsOverlay = (win, { selector, onSelect, labels: labelOverrides })=>{
|
|
73
|
+
});
|
|
74
|
+
// A tag's: on the element's top-right corner, like a notification badge,
|
|
75
|
+
// kept on screen.
|
|
76
|
+
const placeTag = (rect, viewport)=>({
|
|
77
|
+
top: Math.min(Math.max(rect.top, 10), viewport.height - 10),
|
|
78
|
+
left: Math.min(rect.right, viewport.width - 10)
|
|
79
|
+
});
|
|
80
|
+
const mountStatsOverlay = (win, { selector, onSelect, checked, labels: labelOverrides })=>{
|
|
73
81
|
const doc = win.document;
|
|
74
82
|
const labels = {
|
|
75
83
|
...DEFAULT_STATS_LABELS,
|
|
@@ -89,37 +97,71 @@ const mountStatsOverlay = (win, { selector, onSelect, labels: labelOverrides })=
|
|
|
89
97
|
let stats = {};
|
|
90
98
|
let visible = false;
|
|
91
99
|
let frame = null;
|
|
100
|
+
const draw = (id, element, blocks)=>{
|
|
101
|
+
const rect = element.getBoundingClientRect();
|
|
102
|
+
// Not rendered, or off screen.
|
|
103
|
+
if (!rect.width && !rect.height) return;
|
|
104
|
+
if (rect.bottom < 0 || rect.top > win.innerHeight) return;
|
|
105
|
+
const block = stats[id];
|
|
106
|
+
const status = statsStatus(block);
|
|
107
|
+
const label = doc.createElement('button');
|
|
108
|
+
label.type = 'button';
|
|
109
|
+
label.dataset.uid = id;
|
|
110
|
+
label.dataset.status = status;
|
|
111
|
+
const rate = block ? Math.round(block.rate * 100) : 0;
|
|
112
|
+
label.title = status === 'rate' ? fill$1(labels.title, {
|
|
113
|
+
clickers: block.clickers,
|
|
114
|
+
viewers: block.viewers,
|
|
115
|
+
clicks: block.clicks
|
|
116
|
+
}) : status === 'none' ? labels.noClicks : fill$1(labels.counting, {
|
|
117
|
+
clicks: block.clicks,
|
|
118
|
+
viewers: block.viewers,
|
|
119
|
+
needed: MIN_VIEWERS
|
|
120
|
+
});
|
|
121
|
+
const { top, left } = status === 'rate' && rect.height >= SMALL_HEIGHT ? placeLabel(rect) : placeTag(rect, {
|
|
122
|
+
width: win.innerWidth,
|
|
123
|
+
height: win.innerHeight
|
|
124
|
+
});
|
|
125
|
+
if (status === 'rate' && rect.height >= SMALL_HEIGHT) {
|
|
126
|
+
label.className = 'badge';
|
|
127
|
+
label.textContent = fill$1(labels.clicked, {
|
|
128
|
+
rate
|
|
129
|
+
});
|
|
130
|
+
} else {
|
|
131
|
+
label.className = 'tag';
|
|
132
|
+
if (status === 'rate') label.textContent = fill$1(labels.clickedShort, {
|
|
133
|
+
rate
|
|
134
|
+
});
|
|
135
|
+
else label.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true">${ICONS$1[status]}</svg>`;
|
|
136
|
+
label.setAttribute('aria-label', label.title);
|
|
137
|
+
}
|
|
138
|
+
label.style.top = `${top}px`;
|
|
139
|
+
label.style.left = `${left}px`;
|
|
140
|
+
// Its id names a block by convention; else the block around it.
|
|
141
|
+
const uid = blocks.has(id) ? id : element.closest(`[${UID_ATTRIBUTE}]`)?.getAttribute(UID_ATTRIBUTE);
|
|
142
|
+
if (uid) label.addEventListener('click', ()=>onSelect(uid));
|
|
143
|
+
layer.appendChild(label);
|
|
144
|
+
};
|
|
92
145
|
const render = ()=>{
|
|
93
146
|
frame = null;
|
|
94
147
|
layer.replaceChildren();
|
|
95
148
|
layer.hidden = !visible;
|
|
96
149
|
if (!visible) return;
|
|
97
|
-
const
|
|
98
|
-
for (const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (!
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
badge.title = fill$1(labels.title, {
|
|
113
|
-
clickers: block.clickers,
|
|
114
|
-
viewers: block.viewers,
|
|
115
|
-
clicks: block.clicks
|
|
116
|
-
});
|
|
117
|
-
const { place, top, left } = placeLabel(rect, win.innerWidth);
|
|
118
|
-
badge.dataset.place = place;
|
|
119
|
-
badge.style.top = `${top}px`;
|
|
120
|
-
badge.style.left = `${left}px`;
|
|
121
|
-
badge.addEventListener('click', ()=>onSelect(uid));
|
|
122
|
-
layer.appendChild(badge);
|
|
150
|
+
const blocks = new Set();
|
|
151
|
+
for (const element of doc.querySelectorAll(`[${UID_ATTRIBUTE}]`))blocks.add(element.getAttribute(UID_ATTRIBUTE) ?? '');
|
|
152
|
+
const isChecked = checked ?? ((id)=>blocks.has(id));
|
|
153
|
+
const tagged = new Set();
|
|
154
|
+
for (const element of doc.querySelectorAll(`[${TAG_ATTRIBUTE}]`)){
|
|
155
|
+
const id = element.getAttribute(TAG_ATTRIBUTE);
|
|
156
|
+
if (!id || !stats[id] && !isChecked(id)) continue;
|
|
157
|
+
tagged.add(id);
|
|
158
|
+
draw(id, element, blocks);
|
|
159
|
+
}
|
|
160
|
+
// Rates of blocks the site doesn't tag an element of.
|
|
161
|
+
for (const id of Object.keys(stats)){
|
|
162
|
+
if (tagged.has(id) || !blocks.has(id)) continue;
|
|
163
|
+
const element = doc.querySelector(selector(id));
|
|
164
|
+
if (element) draw(id, element, blocks);
|
|
123
165
|
}
|
|
124
166
|
};
|
|
125
167
|
// At most once a frame, however many scroll and mutation events come in.
|
|
@@ -784,6 +826,7 @@ const DEFAULT_SITE_EDITOR_LABELS = {
|
|
|
784
826
|
envProduction: 'live',
|
|
785
827
|
scopeNoteBlock: 'Working on {name}. Click another block to switch.',
|
|
786
828
|
scopeNotePage: 'Working on the whole page. Click a block to change only that block.',
|
|
829
|
+
scopeUp: 'Up to {name}',
|
|
787
830
|
block: 'Block',
|
|
788
831
|
previewChip: '{name} · preview',
|
|
789
832
|
askIntro: 'Describe a change and Obelism shows it on the page. Nothing is stored until you keep it and save.',
|
|
@@ -1948,6 +1991,11 @@ const mountFormFrame = (win, container, url, title, handlers)=>{
|
|
|
1948
1991
|
// kept in step with the content here, see formFrame.ts), edits JSON (`view`,
|
|
1949
1992
|
// then `applyJson`: the raw alternative), and lists the space's pages to go
|
|
1950
1993
|
// to one.
|
|
1994
|
+
// The CMS's limit on ids in one stats request.
|
|
1995
|
+
const MAX_STATS_IDS = 500;
|
|
1996
|
+
const MAX_STATS_ENTRIES = 20;
|
|
1997
|
+
// After the page changes, before asking for the ids it newly tags.
|
|
1998
|
+
const STATS_DELAY_MS = 1000;
|
|
1951
1999
|
// Plain path segments only: the URL parser drops tabs and newlines and reads
|
|
1952
2000
|
// \ as /, so anything looser can resolve to another origin.
|
|
1953
2001
|
const BASE_PATH = /^(?:\/[\w.~-]+)+$/;
|
|
@@ -2534,8 +2582,10 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2534
2582
|
if (selecting) doc.documentElement.setAttribute('data-cms-selecting', '');
|
|
2535
2583
|
else doc.documentElement.removeAttribute('data-cms-selecting');
|
|
2536
2584
|
setState({
|
|
2537
|
-
selecting
|
|
2585
|
+
selecting,
|
|
2586
|
+
statsVisible: selecting
|
|
2538
2587
|
});
|
|
2588
|
+
showStats();
|
|
2539
2589
|
},
|
|
2540
2590
|
destroy () {
|
|
2541
2591
|
closeForm?.();
|
|
@@ -2569,11 +2619,81 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2569
2619
|
};
|
|
2570
2620
|
win.addEventListener('beforeunload', onBeforeUnload);
|
|
2571
2621
|
cleanup.push(()=>win.removeEventListener('beforeunload', onBeforeUnload));
|
|
2622
|
+
// The tags on the page's links (statsOverlay.ts) while blocks are
|
|
2623
|
+
// outlined: the rates of the ids the page tags, asked for as they appear.
|
|
2624
|
+
const asked = new Set();
|
|
2625
|
+
const answered = new Set();
|
|
2626
|
+
let statsOff = false;
|
|
2627
|
+
let overlay = null;
|
|
2628
|
+
let statsTimer = null;
|
|
2629
|
+
const showStats = ()=>overlay?.update(state.stats, state.statsVisible);
|
|
2630
|
+
const loadStats = async ()=>{
|
|
2631
|
+
if (statsOff) return;
|
|
2632
|
+
const ids = [
|
|
2633
|
+
...new Set([
|
|
2634
|
+
...doc.querySelectorAll(`[${TAG_ATTRIBUTE}]`)
|
|
2635
|
+
].map((element)=>element.getAttribute(TAG_ATTRIBUTE) ?? ''))
|
|
2636
|
+
].filter((id)=>id && !asked.has(id));
|
|
2637
|
+
const batch = ids.slice(0, MAX_STATS_IDS);
|
|
2638
|
+
if (!batch.length) return;
|
|
2639
|
+
batch.forEach((id)=>asked.add(id));
|
|
2640
|
+
// So a draft's copy of a live block gets the live one's clicks.
|
|
2641
|
+
const entries = [
|
|
2642
|
+
...new Set([
|
|
2643
|
+
...doc.querySelectorAll(`[${ENTRY_ATTRIBUTE}]`)
|
|
2644
|
+
].map((element)=>element.getAttribute(ENTRY_ATTRIBUTE) ?? ''))
|
|
2645
|
+
].filter(Boolean).slice(0, MAX_STATS_ENTRIES);
|
|
2646
|
+
try {
|
|
2647
|
+
const { stats } = await api('stats', {
|
|
2648
|
+
uids: batch,
|
|
2649
|
+
...entries.length > 0 && {
|
|
2650
|
+
entries
|
|
2651
|
+
}
|
|
2652
|
+
});
|
|
2653
|
+
batch.forEach((id)=>answered.add(id));
|
|
2654
|
+
setState({
|
|
2655
|
+
stats: {
|
|
2656
|
+
...state.stats,
|
|
2657
|
+
...readStats(stats)
|
|
2658
|
+
}
|
|
2659
|
+
});
|
|
2660
|
+
} catch {
|
|
2661
|
+
// No tags rather than wrong ones, and no asking again.
|
|
2662
|
+
statsOff = true;
|
|
2663
|
+
return;
|
|
2664
|
+
}
|
|
2665
|
+
overlay ??= mountStatsOverlay(win, {
|
|
2666
|
+
selector: blockSelector,
|
|
2667
|
+
onSelect: (uid)=>highlight(uid),
|
|
2668
|
+
checked: (id)=>answered.has(id)
|
|
2669
|
+
});
|
|
2670
|
+
showStats();
|
|
2671
|
+
if (ids.length > batch.length) scheduleStats();
|
|
2672
|
+
};
|
|
2673
|
+
const scheduleStats = ()=>{
|
|
2674
|
+
if (statsTimer !== null || statsOff) return;
|
|
2675
|
+
statsTimer = setTimeout(()=>{
|
|
2676
|
+
statsTimer = null;
|
|
2677
|
+
void loadStats();
|
|
2678
|
+
}, STATS_DELAY_MS);
|
|
2679
|
+
};
|
|
2680
|
+
const Observer = win.MutationObserver;
|
|
2681
|
+
const statsObserver = typeof Observer === 'function' ? new Observer(scheduleStats) : null;
|
|
2682
|
+
cleanup.push(()=>{
|
|
2683
|
+
statsObserver?.disconnect();
|
|
2684
|
+
if (statsTimer !== null) clearTimeout(statsTimer);
|
|
2685
|
+
overlay?.destroy();
|
|
2686
|
+
});
|
|
2572
2687
|
api('session').then((info)=>{
|
|
2573
2688
|
session = info;
|
|
2574
2689
|
setState({
|
|
2575
2690
|
features: info.features
|
|
2576
2691
|
});
|
|
2692
|
+
void loadStats();
|
|
2693
|
+
statsObserver?.observe(doc.body, {
|
|
2694
|
+
childList: true,
|
|
2695
|
+
subtree: true
|
|
2696
|
+
});
|
|
2577
2697
|
}).catch((error)=>console.error('[@obelism/content bridge]', error));
|
|
2578
2698
|
return bridge;
|
|
2579
2699
|
};
|
|
@@ -2757,6 +2877,7 @@ button:disabled { cursor: not-allowed; }
|
|
|
2757
2877
|
flex: none; font-size: 12px; font-weight: 500; text-decoration: underline; text-underline-offset: 2px;
|
|
2758
2878
|
}
|
|
2759
2879
|
.note button::after { content: ''; position: absolute; inset: -13px -6px; }
|
|
2880
|
+
.scroll > .note { position: sticky; top: 0; z-index: 1; }
|
|
2760
2881
|
.intro { color: rgb(var(--muted)); font-size: 13px; }
|
|
2761
2882
|
.gate {
|
|
2762
2883
|
display: grid; gap: 8px; margin: 10px 14px 0; padding: 10px 12px; border-radius: 12px;
|
|
@@ -3065,11 +3186,11 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3065
3186
|
}, gateKeep, gateUndo));
|
|
3066
3187
|
gate.setAttribute('role', 'alert');
|
|
3067
3188
|
const noteText = element(doc, 'span');
|
|
3068
|
-
const
|
|
3189
|
+
const up = button('', labels.scopeEntry);
|
|
3069
3190
|
const cancelNew = button('', labels.newPageCancel);
|
|
3070
3191
|
const note = element(doc, 'div', {
|
|
3071
3192
|
className: 'note'
|
|
3072
|
-
}, icon('scope'), noteText,
|
|
3193
|
+
}, icon('scope'), noteText, up, cancelNew);
|
|
3073
3194
|
const intro = element(doc, 'p', {
|
|
3074
3195
|
className: 'intro',
|
|
3075
3196
|
textContent: labels.askIntro
|
|
@@ -3135,10 +3256,10 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3135
3256
|
}, formNote);
|
|
3136
3257
|
// The same way back as the ask view's note, above the form it changes.
|
|
3137
3258
|
const fieldsNoteText = element(doc, 'span');
|
|
3138
|
-
const
|
|
3259
|
+
const upFields = button('', labels.scopeEntry);
|
|
3139
3260
|
const fieldsNote = element(doc, 'div', {
|
|
3140
3261
|
className: 'note'
|
|
3141
|
-
}, icon('scope'), fieldsNoteText,
|
|
3262
|
+
}, icon('scope'), fieldsNoteText, upFields);
|
|
3142
3263
|
const fields = element(doc, 'div', {
|
|
3143
3264
|
className: 'fields',
|
|
3144
3265
|
id: 'obelism-fields'
|
|
@@ -3218,6 +3339,7 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3218
3339
|
let hoverUid = null;
|
|
3219
3340
|
// Block names by uid, from the views; null when it couldn't be read.
|
|
3220
3341
|
const names = new Map();
|
|
3342
|
+
const parents = new Map();
|
|
3221
3343
|
let pageNames = 'idle';
|
|
3222
3344
|
let destroyed = false;
|
|
3223
3345
|
const formSlot = createFormSlot(bridge, formBox, labels.formTitle, {
|
|
@@ -3253,13 +3375,21 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3253
3375
|
}
|
|
3254
3376
|
return names.get(uid) ?? labels.block;
|
|
3255
3377
|
};
|
|
3256
|
-
const readNames = (value)=>{
|
|
3257
|
-
if (Array.isArray(value)) value.forEach(readNames);
|
|
3378
|
+
const readNames = (value, parent = null)=>{
|
|
3379
|
+
if (Array.isArray(value)) value.forEach((item)=>readNames(item, parent));
|
|
3258
3380
|
else if (isRecord(value)) {
|
|
3259
|
-
|
|
3260
|
-
|
|
3381
|
+
let uid = parent;
|
|
3382
|
+
if (typeof value._uid === 'string' && typeof value._type === 'string') {
|
|
3383
|
+
uid = value._uid;
|
|
3384
|
+
parents.set(uid, parent);
|
|
3385
|
+
names.set(uid, humanize(value._type));
|
|
3386
|
+
}
|
|
3387
|
+
Object.values(value).forEach((item)=>readNames(item, uid));
|
|
3261
3388
|
}
|
|
3262
3389
|
};
|
|
3390
|
+
// The block around `uid`, null at the top of the page: by the page as it
|
|
3391
|
+
// is now, else by its view, for a block whose wrapper the site doesn't mark.
|
|
3392
|
+
const parentOf = (uid)=>doc.querySelector(blockSelector(uid))?.parentElement?.closest(`[${UID_ATTRIBUTE}]`)?.getAttribute(UID_ATTRIBUTE) ?? parents.get(uid) ?? null;
|
|
3263
3393
|
const readPageNames = ()=>{
|
|
3264
3394
|
if (pageNames !== 'idle' || bridge.entryId === null) return;
|
|
3265
3395
|
pageNames = 'loading';
|
|
@@ -3553,9 +3683,17 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3553
3683
|
} else {
|
|
3554
3684
|
noteText.textContent = labels.scopeNotePage;
|
|
3555
3685
|
}
|
|
3686
|
+
const parent = state.selectedUid ? parentOf(state.selectedUid) : null;
|
|
3687
|
+
const upText = parent ? fill(labels.scopeUp, {
|
|
3688
|
+
name: nameOf(parent)
|
|
3689
|
+
}) : labels.scopeEntry;
|
|
3690
|
+
for (const link of [
|
|
3691
|
+
up,
|
|
3692
|
+
upFields
|
|
3693
|
+
])if (link.textContent !== upText) link.textContent = upText;
|
|
3556
3694
|
fieldsNote.hidden = creating || !state.selectedUid;
|
|
3557
3695
|
note.hidden = offerShop || pageless && !creating && !options.promptWithoutPage;
|
|
3558
|
-
|
|
3696
|
+
up.hidden = creating || !state.selectedUid;
|
|
3559
3697
|
cancelNew.hidden = !creating || onlyNew;
|
|
3560
3698
|
intro.hidden = answers.length > 0 || offerShop || creating;
|
|
3561
3699
|
shop.hidden = !offerShop;
|
|
@@ -3665,13 +3803,19 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3665
3803
|
refresh();
|
|
3666
3804
|
(picking ? picker.search : pageButton).focus();
|
|
3667
3805
|
};
|
|
3668
|
-
//
|
|
3669
|
-
|
|
3806
|
+
// True once back at the page.
|
|
3807
|
+
const stepUp = ()=>{
|
|
3808
|
+
const { selectedUid } = bridge.getState();
|
|
3809
|
+
if (selectedUid) bridge.select(parentOf(selectedUid));
|
|
3810
|
+
return !bridge.getState().selectedUid;
|
|
3811
|
+
};
|
|
3812
|
+
// Escape, on the page or in the form: from the pages, from a block a level
|
|
3813
|
+
// up. True when it did something.
|
|
3670
3814
|
const stepBack = ()=>{
|
|
3671
3815
|
const state = bridge.getState();
|
|
3672
3816
|
if (!panelShown(state)) return false;
|
|
3673
3817
|
if (picking) setPicking(false);
|
|
3674
|
-
else if (state.selectedUid)
|
|
3818
|
+
else if (state.selectedUid) stepUp();
|
|
3675
3819
|
else return false;
|
|
3676
3820
|
return true;
|
|
3677
3821
|
};
|
|
@@ -3785,13 +3929,11 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3785
3929
|
pageButton.addEventListener('click', ()=>setPicking(!picking));
|
|
3786
3930
|
newPage.addEventListener('click', ()=>setCreating(true));
|
|
3787
3931
|
cancelNew.addEventListener('click', ()=>setCreating(false));
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
input.focus();
|
|
3932
|
+
up.addEventListener('click', ()=>{
|
|
3933
|
+
if (stepUp()) input.focus();
|
|
3791
3934
|
});
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
focusForm();
|
|
3935
|
+
upFields.addEventListener('click', ()=>{
|
|
3936
|
+
if (stepUp()) focusForm();
|
|
3795
3937
|
});
|
|
3796
3938
|
shopButton.addEventListener('click', async ()=>{
|
|
3797
3939
|
shopMessage = null;
|
package/dist/site.js
CHANGED
|
@@ -115,6 +115,11 @@ const PROXY_ROUTES = [
|
|
|
115
115
|
method: 'POST',
|
|
116
116
|
pattern: /^form$/
|
|
117
117
|
},
|
|
118
|
+
// Clicks on the links the page tags, from Numbers.
|
|
119
|
+
{
|
|
120
|
+
method: 'POST',
|
|
121
|
+
pattern: /^stats$/
|
|
122
|
+
},
|
|
118
123
|
{
|
|
119
124
|
method: 'POST',
|
|
120
125
|
pattern: /^entries\/[\w-]+\/(preview|prompt|view|apply|save|publish)$/
|
package/dist/site.mjs
CHANGED
|
@@ -113,6 +113,11 @@ const PROXY_ROUTES = [
|
|
|
113
113
|
method: 'POST',
|
|
114
114
|
pattern: /^form$/
|
|
115
115
|
},
|
|
116
|
+
// Clicks on the links the page tags, from Numbers.
|
|
117
|
+
{
|
|
118
|
+
method: 'POST',
|
|
119
|
+
pattern: /^stats$/
|
|
120
|
+
},
|
|
116
121
|
{
|
|
117
122
|
method: 'POST',
|
|
118
123
|
pattern: /^entries\/[\w-]+\/(preview|prompt|view|apply|save|publish)$/
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@obelism/content",
|
|
3
3
|
"description": "Obelism Content SDK: the delivery API client, the visual editor bridge with prompt editing, editing on your own site, webhook verification, and the content model CLI",
|
|
4
|
-
"version": "0.28.
|
|
4
|
+
"version": "0.28.2",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cms",
|
|
7
7
|
"headless-cms",
|