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