@obelism/content 0.28.0 → 0.28.1
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 +213 -81
- package/dist/bridge.mjs +214 -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,10 @@ 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
|
+
// After the page changes, before asking for the ids it newly tags.
|
|
1999
|
+
const STATS_DELAY_MS = 1000;
|
|
1953
2000
|
// Plain path segments only: the URL parser drops tabs and newlines and reads
|
|
1954
2001
|
// \ as /, so anything looser can resolve to another origin.
|
|
1955
2002
|
const BASE_PATH = /^(?:\/[\w.~-]+)+$/;
|
|
@@ -2536,8 +2583,10 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2536
2583
|
if (selecting) doc.documentElement.setAttribute('data-cms-selecting', '');
|
|
2537
2584
|
else doc.documentElement.removeAttribute('data-cms-selecting');
|
|
2538
2585
|
setState({
|
|
2539
|
-
selecting
|
|
2586
|
+
selecting,
|
|
2587
|
+
statsVisible: selecting
|
|
2540
2588
|
});
|
|
2589
|
+
showStats();
|
|
2541
2590
|
},
|
|
2542
2591
|
destroy () {
|
|
2543
2592
|
closeForm?.();
|
|
@@ -2571,11 +2620,72 @@ const createSiteBridge = (win, basePath, { pagePath = (path)=>`/${path.replace(/
|
|
|
2571
2620
|
};
|
|
2572
2621
|
win.addEventListener('beforeunload', onBeforeUnload);
|
|
2573
2622
|
cleanup.push(()=>win.removeEventListener('beforeunload', onBeforeUnload));
|
|
2623
|
+
// The tags on the page's links (statsOverlay.ts) while blocks are
|
|
2624
|
+
// outlined: the rates of the ids the page tags, asked for as they appear.
|
|
2625
|
+
const asked = new Set();
|
|
2626
|
+
const answered = new Set();
|
|
2627
|
+
let statsOff = false;
|
|
2628
|
+
let overlay = null;
|
|
2629
|
+
let statsTimer = null;
|
|
2630
|
+
const showStats = ()=>overlay?.update(state.stats, state.statsVisible);
|
|
2631
|
+
const loadStats = async ()=>{
|
|
2632
|
+
if (statsOff) return;
|
|
2633
|
+
const ids = [
|
|
2634
|
+
...new Set([
|
|
2635
|
+
...doc.querySelectorAll(`[${TAG_ATTRIBUTE}]`)
|
|
2636
|
+
].map((element)=>element.getAttribute(TAG_ATTRIBUTE) ?? ''))
|
|
2637
|
+
].filter((id)=>id && !asked.has(id));
|
|
2638
|
+
const batch = ids.slice(0, MAX_STATS_IDS);
|
|
2639
|
+
if (!batch.length) return;
|
|
2640
|
+
batch.forEach((id)=>asked.add(id));
|
|
2641
|
+
try {
|
|
2642
|
+
const { stats } = await api('stats', {
|
|
2643
|
+
uids: batch
|
|
2644
|
+
});
|
|
2645
|
+
batch.forEach((id)=>answered.add(id));
|
|
2646
|
+
setState({
|
|
2647
|
+
stats: {
|
|
2648
|
+
...state.stats,
|
|
2649
|
+
...readStats(stats)
|
|
2650
|
+
}
|
|
2651
|
+
});
|
|
2652
|
+
} catch {
|
|
2653
|
+
// No tags rather than wrong ones, and no asking again.
|
|
2654
|
+
statsOff = true;
|
|
2655
|
+
return;
|
|
2656
|
+
}
|
|
2657
|
+
overlay ??= mountStatsOverlay(win, {
|
|
2658
|
+
selector: blockSelector,
|
|
2659
|
+
onSelect: (uid)=>highlight(uid),
|
|
2660
|
+
checked: (id)=>answered.has(id)
|
|
2661
|
+
});
|
|
2662
|
+
showStats();
|
|
2663
|
+
if (ids.length > batch.length) scheduleStats();
|
|
2664
|
+
};
|
|
2665
|
+
const scheduleStats = ()=>{
|
|
2666
|
+
if (statsTimer !== null || statsOff) return;
|
|
2667
|
+
statsTimer = setTimeout(()=>{
|
|
2668
|
+
statsTimer = null;
|
|
2669
|
+
void loadStats();
|
|
2670
|
+
}, STATS_DELAY_MS);
|
|
2671
|
+
};
|
|
2672
|
+
const Observer = win.MutationObserver;
|
|
2673
|
+
const statsObserver = typeof Observer === 'function' ? new Observer(scheduleStats) : null;
|
|
2674
|
+
cleanup.push(()=>{
|
|
2675
|
+
statsObserver?.disconnect();
|
|
2676
|
+
if (statsTimer !== null) clearTimeout(statsTimer);
|
|
2677
|
+
overlay?.destroy();
|
|
2678
|
+
});
|
|
2574
2679
|
api('session').then((info)=>{
|
|
2575
2680
|
session = info;
|
|
2576
2681
|
setState({
|
|
2577
2682
|
features: info.features
|
|
2578
2683
|
});
|
|
2684
|
+
void loadStats();
|
|
2685
|
+
statsObserver?.observe(doc.body, {
|
|
2686
|
+
childList: true,
|
|
2687
|
+
subtree: true
|
|
2688
|
+
});
|
|
2579
2689
|
}).catch((error)=>console.error('[@obelism/content bridge]', error));
|
|
2580
2690
|
return bridge;
|
|
2581
2691
|
};
|
|
@@ -2759,6 +2869,7 @@ button:disabled { cursor: not-allowed; }
|
|
|
2759
2869
|
flex: none; font-size: 12px; font-weight: 500; text-decoration: underline; text-underline-offset: 2px;
|
|
2760
2870
|
}
|
|
2761
2871
|
.note button::after { content: ''; position: absolute; inset: -13px -6px; }
|
|
2872
|
+
.scroll > .note { position: sticky; top: 0; z-index: 1; }
|
|
2762
2873
|
.intro { color: rgb(var(--muted)); font-size: 13px; }
|
|
2763
2874
|
.gate {
|
|
2764
2875
|
display: grid; gap: 8px; margin: 10px 14px 0; padding: 10px 12px; border-radius: 12px;
|
|
@@ -3067,11 +3178,11 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3067
3178
|
}, gateKeep, gateUndo));
|
|
3068
3179
|
gate.setAttribute('role', 'alert');
|
|
3069
3180
|
const noteText = element(doc, 'span');
|
|
3070
|
-
const
|
|
3181
|
+
const up = button('', labels.scopeEntry);
|
|
3071
3182
|
const cancelNew = button('', labels.newPageCancel);
|
|
3072
3183
|
const note = element(doc, 'div', {
|
|
3073
3184
|
className: 'note'
|
|
3074
|
-
}, icon('scope'), noteText,
|
|
3185
|
+
}, icon('scope'), noteText, up, cancelNew);
|
|
3075
3186
|
const intro = element(doc, 'p', {
|
|
3076
3187
|
className: 'intro',
|
|
3077
3188
|
textContent: labels.askIntro
|
|
@@ -3137,10 +3248,10 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3137
3248
|
}, formNote);
|
|
3138
3249
|
// The same way back as the ask view's note, above the form it changes.
|
|
3139
3250
|
const fieldsNoteText = element(doc, 'span');
|
|
3140
|
-
const
|
|
3251
|
+
const upFields = button('', labels.scopeEntry);
|
|
3141
3252
|
const fieldsNote = element(doc, 'div', {
|
|
3142
3253
|
className: 'note'
|
|
3143
|
-
}, icon('scope'), fieldsNoteText,
|
|
3254
|
+
}, icon('scope'), fieldsNoteText, upFields);
|
|
3144
3255
|
const fields = element(doc, 'div', {
|
|
3145
3256
|
className: 'fields',
|
|
3146
3257
|
id: 'obelism-fields'
|
|
@@ -3220,6 +3331,7 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3220
3331
|
let hoverUid = null;
|
|
3221
3332
|
// Block names by uid, from the views; null when it couldn't be read.
|
|
3222
3333
|
const names = new Map();
|
|
3334
|
+
const parents = new Map();
|
|
3223
3335
|
let pageNames = 'idle';
|
|
3224
3336
|
let destroyed = false;
|
|
3225
3337
|
const formSlot = createFormSlot(bridge, formBox, labels.formTitle, {
|
|
@@ -3255,13 +3367,21 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3255
3367
|
}
|
|
3256
3368
|
return names.get(uid) ?? labels.block;
|
|
3257
3369
|
};
|
|
3258
|
-
const readNames = (value)=>{
|
|
3259
|
-
if (Array.isArray(value)) value.forEach(readNames);
|
|
3370
|
+
const readNames = (value, parent = null)=>{
|
|
3371
|
+
if (Array.isArray(value)) value.forEach((item)=>readNames(item, parent));
|
|
3260
3372
|
else if (isRecord(value)) {
|
|
3261
|
-
|
|
3262
|
-
|
|
3373
|
+
let uid = parent;
|
|
3374
|
+
if (typeof value._uid === 'string' && typeof value._type === 'string') {
|
|
3375
|
+
uid = value._uid;
|
|
3376
|
+
parents.set(uid, parent);
|
|
3377
|
+
names.set(uid, humanize(value._type));
|
|
3378
|
+
}
|
|
3379
|
+
Object.values(value).forEach((item)=>readNames(item, uid));
|
|
3263
3380
|
}
|
|
3264
3381
|
};
|
|
3382
|
+
// The block around `uid`, null at the top of the page: by the page as it
|
|
3383
|
+
// is now, else by its view, for a block whose wrapper the site doesn't mark.
|
|
3384
|
+
const parentOf = (uid)=>doc.querySelector(blockSelector(uid))?.parentElement?.closest(`[${preview.UID_ATTRIBUTE}]`)?.getAttribute(preview.UID_ATTRIBUTE) ?? parents.get(uid) ?? null;
|
|
3265
3385
|
const readPageNames = ()=>{
|
|
3266
3386
|
if (pageNames !== 'idle' || bridge.entryId === null) return;
|
|
3267
3387
|
pageNames = 'loading';
|
|
@@ -3555,9 +3675,17 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3555
3675
|
} else {
|
|
3556
3676
|
noteText.textContent = labels.scopeNotePage;
|
|
3557
3677
|
}
|
|
3678
|
+
const parent = state.selectedUid ? parentOf(state.selectedUid) : null;
|
|
3679
|
+
const upText = parent ? fill(labels.scopeUp, {
|
|
3680
|
+
name: nameOf(parent)
|
|
3681
|
+
}) : labels.scopeEntry;
|
|
3682
|
+
for (const link of [
|
|
3683
|
+
up,
|
|
3684
|
+
upFields
|
|
3685
|
+
])if (link.textContent !== upText) link.textContent = upText;
|
|
3558
3686
|
fieldsNote.hidden = creating || !state.selectedUid;
|
|
3559
3687
|
note.hidden = offerShop || pageless && !creating && !options.promptWithoutPage;
|
|
3560
|
-
|
|
3688
|
+
up.hidden = creating || !state.selectedUid;
|
|
3561
3689
|
cancelNew.hidden = !creating || onlyNew;
|
|
3562
3690
|
intro.hidden = answers.length > 0 || offerShop || creating;
|
|
3563
3691
|
shop.hidden = !offerShop;
|
|
@@ -3667,13 +3795,19 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3667
3795
|
refresh();
|
|
3668
3796
|
(picking ? picker.search : pageButton).focus();
|
|
3669
3797
|
};
|
|
3670
|
-
//
|
|
3671
|
-
|
|
3798
|
+
// True once back at the page.
|
|
3799
|
+
const stepUp = ()=>{
|
|
3800
|
+
const { selectedUid } = bridge.getState();
|
|
3801
|
+
if (selectedUid) bridge.select(parentOf(selectedUid));
|
|
3802
|
+
return !bridge.getState().selectedUid;
|
|
3803
|
+
};
|
|
3804
|
+
// Escape, on the page or in the form: from the pages, from a block a level
|
|
3805
|
+
// up. True when it did something.
|
|
3672
3806
|
const stepBack = ()=>{
|
|
3673
3807
|
const state = bridge.getState();
|
|
3674
3808
|
if (!panelShown(state)) return false;
|
|
3675
3809
|
if (picking) setPicking(false);
|
|
3676
|
-
else if (state.selectedUid)
|
|
3810
|
+
else if (state.selectedUid) stepUp();
|
|
3677
3811
|
else return false;
|
|
3678
3812
|
return true;
|
|
3679
3813
|
};
|
|
@@ -3787,13 +3921,11 @@ const mountSiteEditor = (doc, bridge, options = {})=>{
|
|
|
3787
3921
|
pageButton.addEventListener('click', ()=>setPicking(!picking));
|
|
3788
3922
|
newPage.addEventListener('click', ()=>setCreating(true));
|
|
3789
3923
|
cancelNew.addEventListener('click', ()=>setCreating(false));
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
input.focus();
|
|
3924
|
+
up.addEventListener('click', ()=>{
|
|
3925
|
+
if (stepUp()) input.focus();
|
|
3793
3926
|
});
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
focusForm();
|
|
3927
|
+
upFields.addEventListener('click', ()=>{
|
|
3928
|
+
if (stepUp()) focusForm();
|
|
3797
3929
|
});
|
|
3798
3930
|
shopButton.addEventListener('click', async ()=>{
|
|
3799
3931
|
shopMessage = null;
|