@kenjura/ursa 0.90.0 → 0.93.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/CHANGELOG.md +54 -0
- package/README.md +35 -0
- package/meta/templates/default-template/default.css +1574 -1323
- package/meta/templates/default-template/index.html +175 -129
- package/meta/templates/default-template/lightbox.css +225 -216
- package/meta/templates/default-template/lightbox.js +3 -0
- package/meta/templates/default-template/menu.js +9 -6
- package/meta/templates/default-template/toc-generator.js +58 -0
- package/meta/templates/default-template/widgets.js +88 -15
- package/package.json +5 -3
- package/src/dev.js +1 -1
- package/src/helper/__test__/breadcrumbs.test.js +71 -0
- package/src/helper/__test__/contentHash.test.js +114 -0
- package/src/helper/automenu.js +9 -89
- package/src/helper/breadcrumbs.js +21 -6
- package/src/helper/build/__test__/autoIndex.test.js +68 -1
- package/src/helper/build/autoIndex.js +94 -42
- package/src/helper/build/ursaMetadata.js +3 -26
- package/src/helper/contentHash.js +54 -1
- package/src/helper/menuLabels.js +136 -0
- package/src/helper/ursaVersion.js +26 -0
- package/src/jobs/generate.js +17 -3
- package/meta/default.css +0 -1206
- package/meta/menu.js +0 -898
- package/meta/sectionify.js +0 -46
|
@@ -1,248 +1,257 @@
|
|
|
1
|
-
/* Image hover controls and the full-screen viewer (see lightbox.js).
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
/* Image hover controls and the full-screen viewer (see lightbox.js).
|
|
2
|
+
|
|
3
|
+
Both are Ursa's furniture: the hover controls are injected into the
|
|
4
|
+
document's own markup and the viewer is appended to <body>. Like the
|
|
5
|
+
breadcrumbs in default.css they are scoped but *not* layered, so a broad
|
|
6
|
+
site rule cannot bleed into them, and they stay out of `.ursa-unstyled`.
|
|
7
|
+
(lightbox.js skips images inside `.ursa-unstyled` for the same reason.) */
|
|
8
|
+
|
|
9
|
+
@scope (body) to (.ursa-unstyled) {
|
|
10
|
+
/* --- hover controls --- */
|
|
11
|
+
|
|
12
|
+
.ursa-image-frame {
|
|
13
|
+
position: relative;
|
|
14
|
+
display: inline-block;
|
|
15
|
+
max-width: 100%;
|
|
16
|
+
line-height: 0;
|
|
17
|
+
}
|
|
11
18
|
|
|
12
|
-
.ursa-image-controls {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
19
|
+
.ursa-image-controls {
|
|
20
|
+
position: absolute;
|
|
21
|
+
top: 8px;
|
|
22
|
+
right: 8px;
|
|
23
|
+
display: flex;
|
|
24
|
+
gap: 6px;
|
|
25
|
+
opacity: 0;
|
|
26
|
+
transition: opacity 0.15s ease;
|
|
27
|
+
line-height: 0;
|
|
28
|
+
}
|
|
22
29
|
|
|
23
|
-
.ursa-image-frame:hover .ursa-image-controls,
|
|
24
|
-
.ursa-image-frame:focus-within .ursa-image-controls {
|
|
25
|
-
|
|
26
|
-
}
|
|
30
|
+
.ursa-image-frame:hover .ursa-image-controls,
|
|
31
|
+
.ursa-image-frame:focus-within .ursa-image-controls {
|
|
32
|
+
opacity: 1;
|
|
33
|
+
}
|
|
27
34
|
|
|
28
|
-
/* No hover to reveal them on touch devices, so keep them visible but quiet. */
|
|
29
|
-
@media (hover: none) {
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
/* No hover to reveal them on touch devices, so keep them visible but quiet. */
|
|
36
|
+
@media (hover: none) {
|
|
37
|
+
.ursa-image-controls {
|
|
38
|
+
opacity: 0.65;
|
|
39
|
+
}
|
|
32
40
|
}
|
|
33
|
-
}
|
|
34
41
|
|
|
35
|
-
.ursa-image-btn {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
42
|
+
.ursa-image-btn {
|
|
43
|
+
display: flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
justify-content: center;
|
|
46
|
+
width: 30px;
|
|
47
|
+
height: 30px;
|
|
48
|
+
padding: 0;
|
|
49
|
+
border: none;
|
|
50
|
+
border-radius: 50%;
|
|
51
|
+
background: rgba(0, 0, 0, 0.55);
|
|
52
|
+
color: white;
|
|
53
|
+
cursor: pointer;
|
|
54
|
+
backdrop-filter: blur(2px);
|
|
55
|
+
transition: background-color 0.15s ease;
|
|
56
|
+
}
|
|
50
57
|
|
|
51
|
-
.ursa-image-btn:hover,
|
|
52
|
-
.ursa-image-btn:focus-visible {
|
|
53
|
-
|
|
54
|
-
}
|
|
58
|
+
.ursa-image-btn:hover,
|
|
59
|
+
.ursa-image-btn:focus-visible {
|
|
60
|
+
background: rgba(0, 0, 0, 0.8);
|
|
61
|
+
}
|
|
55
62
|
|
|
56
|
-
.ursa-image-btn svg {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
63
|
+
.ursa-image-btn svg {
|
|
64
|
+
width: 17px;
|
|
65
|
+
height: 17px;
|
|
66
|
+
fill: none;
|
|
67
|
+
stroke: currentColor;
|
|
68
|
+
stroke-width: 2;
|
|
69
|
+
stroke-linecap: round;
|
|
70
|
+
stroke-linejoin: round;
|
|
71
|
+
}
|
|
65
72
|
|
|
66
|
-
/* --- full-screen viewer --- */
|
|
73
|
+
/* --- full-screen viewer --- */
|
|
67
74
|
|
|
68
|
-
body.
|
|
69
|
-
|
|
70
|
-
|
|
75
|
+
/* `:scope` is <body>: a bare `body` here would mean a body inside the body. */
|
|
76
|
+
:scope.ursa-lightbox-open {
|
|
77
|
+
overflow: hidden;
|
|
78
|
+
}
|
|
71
79
|
|
|
72
|
-
.ursa-lightbox {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
80
|
+
.ursa-lightbox {
|
|
81
|
+
position: fixed;
|
|
82
|
+
inset: 0;
|
|
83
|
+
z-index: 2000;
|
|
84
|
+
display: flex;
|
|
85
|
+
}
|
|
78
86
|
|
|
79
|
-
.ursa-lightbox[hidden] {
|
|
80
|
-
|
|
81
|
-
}
|
|
87
|
+
.ursa-lightbox[hidden] {
|
|
88
|
+
display: none;
|
|
89
|
+
}
|
|
82
90
|
|
|
83
|
-
.ursa-lightbox-backdrop {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
91
|
+
.ursa-lightbox-backdrop {
|
|
92
|
+
position: absolute;
|
|
93
|
+
inset: 0;
|
|
94
|
+
background: rgba(0, 0, 0, 0.85);
|
|
95
|
+
}
|
|
88
96
|
|
|
89
|
-
.ursa-lightbox-stage {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
97
|
+
.ursa-lightbox-stage {
|
|
98
|
+
position: relative;
|
|
99
|
+
flex: 1;
|
|
100
|
+
display: flex;
|
|
101
|
+
overflow: auto;
|
|
102
|
+
padding: 32px;
|
|
103
|
+
box-sizing: border-box;
|
|
104
|
+
outline: none;
|
|
105
|
+
overscroll-behavior: contain;
|
|
106
|
+
}
|
|
99
107
|
|
|
100
|
-
/* margin:auto centres the image while it fits, and collapses to 0 once it is
|
|
101
|
-
|
|
102
|
-
.ursa-lightbox-image {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
+
/* margin:auto centres the image while it fits, and collapses to 0 once it is
|
|
109
|
+
larger than the stage, so the overflow stays scrollable in both directions. */
|
|
110
|
+
.ursa-lightbox-image {
|
|
111
|
+
margin: auto;
|
|
112
|
+
display: block;
|
|
113
|
+
user-select: none;
|
|
114
|
+
-webkit-user-drag: none;
|
|
115
|
+
}
|
|
108
116
|
|
|
109
|
-
.ursa-lightbox-image.is-placeholder {
|
|
110
|
-
|
|
111
|
-
}
|
|
117
|
+
.ursa-lightbox-image.is-placeholder {
|
|
118
|
+
filter: blur(1px);
|
|
119
|
+
}
|
|
112
120
|
|
|
113
|
-
.ursa-lightbox-stage.is-pannable {
|
|
114
|
-
|
|
115
|
-
}
|
|
121
|
+
.ursa-lightbox-stage.is-pannable {
|
|
122
|
+
cursor: grab;
|
|
123
|
+
}
|
|
116
124
|
|
|
117
|
-
.ursa-lightbox-stage.is-panning {
|
|
118
|
-
|
|
119
|
-
}
|
|
125
|
+
.ursa-lightbox-stage.is-panning {
|
|
126
|
+
cursor: grabbing;
|
|
127
|
+
}
|
|
120
128
|
|
|
121
|
-
/* Centred over the blurred placeholder, clear of the toolbar. */
|
|
122
|
-
.ursa-lightbox-loading {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
129
|
+
/* Centred over the blurred placeholder, clear of the toolbar. */
|
|
130
|
+
.ursa-lightbox-loading {
|
|
131
|
+
position: absolute;
|
|
132
|
+
left: 50%;
|
|
133
|
+
top: 50%;
|
|
134
|
+
transform: translate(-50%, -50%);
|
|
135
|
+
display: flex;
|
|
136
|
+
padding: 10px;
|
|
137
|
+
border-radius: 50%;
|
|
138
|
+
background: rgba(0, 0, 0, 0.55);
|
|
139
|
+
pointer-events: none;
|
|
140
|
+
}
|
|
133
141
|
|
|
134
|
-
.ursa-lightbox-loading[hidden] {
|
|
135
|
-
|
|
136
|
-
}
|
|
142
|
+
.ursa-lightbox-loading[hidden] {
|
|
143
|
+
display: none;
|
|
144
|
+
}
|
|
137
145
|
|
|
138
|
-
.ursa-lightbox-loading .ursa-spinner {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
146
|
+
.ursa-lightbox-loading .ursa-spinner {
|
|
147
|
+
width: 18px;
|
|
148
|
+
height: 18px;
|
|
149
|
+
border-color: rgba(255, 255, 255, 0.3);
|
|
150
|
+
border-top-color: rgba(255, 255, 255, 0.9);
|
|
151
|
+
}
|
|
144
152
|
|
|
145
|
-
.ursa-lightbox-toolbar {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
153
|
+
.ursa-lightbox-toolbar {
|
|
154
|
+
position: absolute;
|
|
155
|
+
left: 50%;
|
|
156
|
+
bottom: 20px;
|
|
157
|
+
transform: translateX(-50%);
|
|
158
|
+
display: flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
gap: 4px;
|
|
161
|
+
padding: 4px;
|
|
162
|
+
border-radius: 999px;
|
|
163
|
+
background: rgba(0, 0, 0, 0.6);
|
|
164
|
+
backdrop-filter: blur(2px);
|
|
165
|
+
}
|
|
158
166
|
|
|
159
|
-
.ursa-lightbox-zoom {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
167
|
+
.ursa-lightbox-zoom {
|
|
168
|
+
display: flex;
|
|
169
|
+
align-items: center;
|
|
170
|
+
gap: 4px;
|
|
171
|
+
}
|
|
164
172
|
|
|
165
|
-
.ursa-lightbox-zoom[hidden] {
|
|
166
|
-
|
|
167
|
-
}
|
|
173
|
+
.ursa-lightbox-zoom[hidden] {
|
|
174
|
+
display: none;
|
|
175
|
+
}
|
|
168
176
|
|
|
169
|
-
.ursa-lightbox-level {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
+
.ursa-lightbox-level {
|
|
178
|
+
min-width: 3.5em;
|
|
179
|
+
text-align: center;
|
|
180
|
+
color: white;
|
|
181
|
+
font-family: sans-serif;
|
|
182
|
+
font-size: 0.8rem;
|
|
183
|
+
font-variant-numeric: tabular-nums;
|
|
184
|
+
}
|
|
177
185
|
|
|
178
|
-
.ursa-lightbox-btn {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
186
|
+
.ursa-lightbox-btn {
|
|
187
|
+
display: flex;
|
|
188
|
+
align-items: center;
|
|
189
|
+
justify-content: center;
|
|
190
|
+
width: 34px;
|
|
191
|
+
height: 34px;
|
|
192
|
+
padding: 0;
|
|
193
|
+
border: none;
|
|
194
|
+
border-radius: 50%;
|
|
195
|
+
background: transparent;
|
|
196
|
+
color: white;
|
|
197
|
+
font-size: 1.2rem;
|
|
198
|
+
font-family: sans-serif;
|
|
199
|
+
line-height: 1;
|
|
200
|
+
cursor: pointer;
|
|
201
|
+
transition: background-color 0.15s ease, opacity 0.15s ease;
|
|
202
|
+
}
|
|
195
203
|
|
|
196
|
-
.ursa-lightbox-btn:hover,
|
|
197
|
-
.ursa-lightbox-btn:focus-visible {
|
|
198
|
-
|
|
199
|
-
}
|
|
204
|
+
.ursa-lightbox-btn:hover,
|
|
205
|
+
.ursa-lightbox-btn:focus-visible {
|
|
206
|
+
background: rgba(255, 255, 255, 0.18);
|
|
207
|
+
}
|
|
200
208
|
|
|
201
|
-
.ursa-lightbox-btn:disabled {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
209
|
+
.ursa-lightbox-btn:disabled {
|
|
210
|
+
opacity: 0.35;
|
|
211
|
+
cursor: default;
|
|
212
|
+
background: transparent;
|
|
213
|
+
}
|
|
206
214
|
|
|
207
|
-
.ursa-lightbox-btn svg {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
215
|
+
.ursa-lightbox-btn svg {
|
|
216
|
+
width: 18px;
|
|
217
|
+
height: 18px;
|
|
218
|
+
fill: none;
|
|
219
|
+
stroke: currentColor;
|
|
220
|
+
stroke-width: 2;
|
|
221
|
+
stroke-linecap: round;
|
|
222
|
+
stroke-linejoin: round;
|
|
223
|
+
}
|
|
216
224
|
|
|
217
|
-
.ursa-lightbox-close {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
225
|
+
.ursa-lightbox-close {
|
|
226
|
+
position: absolute;
|
|
227
|
+
top: 16px;
|
|
228
|
+
right: 16px;
|
|
229
|
+
display: flex;
|
|
230
|
+
align-items: center;
|
|
231
|
+
justify-content: center;
|
|
232
|
+
width: 44px;
|
|
233
|
+
height: 44px;
|
|
234
|
+
padding: 0;
|
|
235
|
+
border: none;
|
|
236
|
+
border-radius: 50%;
|
|
237
|
+
background: rgba(0, 0, 0, 0.6);
|
|
238
|
+
color: white;
|
|
239
|
+
cursor: pointer;
|
|
240
|
+
backdrop-filter: blur(2px);
|
|
241
|
+
transition: background-color 0.15s ease;
|
|
242
|
+
}
|
|
235
243
|
|
|
236
|
-
.ursa-lightbox-close:hover,
|
|
237
|
-
.ursa-lightbox-close:focus-visible {
|
|
238
|
-
|
|
239
|
-
}
|
|
244
|
+
.ursa-lightbox-close:hover,
|
|
245
|
+
.ursa-lightbox-close:focus-visible {
|
|
246
|
+
background: rgba(255, 255, 255, 0.22);
|
|
247
|
+
}
|
|
240
248
|
|
|
241
|
-
.ursa-lightbox-close svg {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
249
|
+
.ursa-lightbox-close svg {
|
|
250
|
+
width: 22px;
|
|
251
|
+
height: 22px;
|
|
252
|
+
fill: none;
|
|
253
|
+
stroke: currentColor;
|
|
254
|
+
stroke-width: 2;
|
|
255
|
+
stroke-linecap: round;
|
|
256
|
+
}
|
|
248
257
|
}
|
|
@@ -101,6 +101,9 @@
|
|
|
101
101
|
function decorate(img) {
|
|
102
102
|
if (img.dataset.ursaLightbox) return;
|
|
103
103
|
if (img.closest('[data-no-lightbox]')) return;
|
|
104
|
+
// Ursa's CSS does not reach inside .ursa-unstyled, so controls injected
|
|
105
|
+
// there would render unstyled. Leave those images alone entirely.
|
|
106
|
+
if (img.closest('.ursa-unstyled')) return;
|
|
104
107
|
|
|
105
108
|
const url = fullSizeUrl(img);
|
|
106
109
|
if (!url) return;
|
|
@@ -501,7 +501,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
501
501
|
|
|
502
502
|
if (menuButton) {
|
|
503
503
|
function updateButtonIcon(isOpen) {
|
|
504
|
-
menuButton.
|
|
504
|
+
menuButton.dataset.icon = isOpen ? 'close' : 'menu';
|
|
505
505
|
}
|
|
506
506
|
|
|
507
507
|
menuButton.addEventListener('click', (e) => {
|
|
@@ -574,8 +574,11 @@ function initTopMenu() {
|
|
|
574
574
|
|
|
575
575
|
/**
|
|
576
576
|
* Set up the menu button:
|
|
577
|
-
* - Desktop: shows
|
|
578
|
-
* - Mobile: shows
|
|
577
|
+
* - Desktop: shows the house, navigates to "/" on click
|
|
578
|
+
* - Mobile: shows the hamburger, toggles mobile side menu on click
|
|
579
|
+
*
|
|
580
|
+
* All three icons are in the button already (index.html); data-icon is how one
|
|
581
|
+
* of them is chosen, so nothing here has to know what any of them look like.
|
|
579
582
|
*/
|
|
580
583
|
function setupMenuButton(menuButton, navMain) {
|
|
581
584
|
if (!menuButton) return;
|
|
@@ -585,10 +588,10 @@ function setupMenuButton(menuButton, navMain) {
|
|
|
585
588
|
function updateButtonIcon() {
|
|
586
589
|
if (isMobile()) {
|
|
587
590
|
const isActive = navMain?.classList.contains('active');
|
|
588
|
-
menuButton.
|
|
591
|
+
menuButton.dataset.icon = isActive ? 'close' : 'menu';
|
|
589
592
|
menuButton.setAttribute('aria-label', isActive ? 'Close menu' : 'Menu');
|
|
590
593
|
} else {
|
|
591
|
-
menuButton.
|
|
594
|
+
menuButton.dataset.icon = 'home';
|
|
592
595
|
menuButton.setAttribute('aria-label', 'Home');
|
|
593
596
|
}
|
|
594
597
|
}
|
|
@@ -606,7 +609,7 @@ function setupMenuButton(menuButton, navMain) {
|
|
|
606
609
|
// Mobile: toggle side menu
|
|
607
610
|
if (navMain) {
|
|
608
611
|
const isNowActive = navMain.classList.toggle('active');
|
|
609
|
-
menuButton.
|
|
612
|
+
menuButton.dataset.icon = isNowActive ? 'close' : 'menu';
|
|
610
613
|
menuButton.setAttribute('aria-label', isNowActive ? 'Close menu' : 'Menu');
|
|
611
614
|
}
|
|
612
615
|
} else {
|
|
@@ -103,6 +103,64 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
/* --- Keeping the current entry in view -------------------------------
|
|
107
|
+
*
|
|
108
|
+
* Below the panel breakpoint the stylesheet lays this same list on its
|
|
109
|
+
* side: one line tall, along the bottom of the viewport. A line that long
|
|
110
|
+
* cannot show every heading at once, so the entry the reader is currently
|
|
111
|
+
* under has to be brought to them rather than looked for.
|
|
112
|
+
*
|
|
113
|
+
* It goes to the middle where there is room to put it there. Where there is
|
|
114
|
+
* not — a document with too few headings to fill the line, or a reader
|
|
115
|
+
* still near the top of a long one — asking for the middle would mean
|
|
116
|
+
* scrolling past the start of the list and showing blank strip before the
|
|
117
|
+
* first entry. Nothing below special-cases either of those: scrollLeft is
|
|
118
|
+
* clamped to the content by the browser, so both come to rest in their
|
|
119
|
+
* natural position on their own. In the vertical panel there is no
|
|
120
|
+
* horizontal overflow to begin with and the same call does nothing at all.
|
|
121
|
+
*
|
|
122
|
+
* Measured from bounding rects rather than offsetLeft because the offset
|
|
123
|
+
* parent here is the widget dropdown, not the scrolling element, and a
|
|
124
|
+
* theme is free to move one without the other.
|
|
125
|
+
*/
|
|
126
|
+
function centreActiveEntry(behavior) {
|
|
127
|
+
const active = tocTarget.querySelector('a.active');
|
|
128
|
+
if (!active) return;
|
|
129
|
+
|
|
130
|
+
const container = tocTarget.getBoundingClientRect();
|
|
131
|
+
const entry = active.getBoundingClientRect();
|
|
132
|
+
if (!container.width) return; // panel is closed; nothing is laid out
|
|
133
|
+
|
|
134
|
+
const delta = (entry.left + entry.width / 2) - (container.left + container.width / 2);
|
|
135
|
+
tocTarget.scrollTo({ left: tocTarget.scrollLeft + delta, behavior: behavior || 'smooth' });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Both highlighters — the scroll handler below and the sentinel observer in
|
|
139
|
+
// toc.js — say which entry is current by putting .active on its link, and
|
|
140
|
+
// neither knows the strip exists. Watching for the class is what lets the
|
|
141
|
+
// two stay that way. Coalesced to a frame because marking the new entry
|
|
142
|
+
// means unmarking every other one, which is a mutation apiece.
|
|
143
|
+
let centreQueued = false;
|
|
144
|
+
function queueCentreActiveEntry() {
|
|
145
|
+
if (centreQueued) return;
|
|
146
|
+
centreQueued = true;
|
|
147
|
+
requestAnimationFrame(() => {
|
|
148
|
+
centreQueued = false;
|
|
149
|
+
centreActiveEntry();
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
new MutationObserver(queueCentreActiveEntry)
|
|
154
|
+
.observe(tocList, { subtree: true, attributeFilter: ['class'] });
|
|
155
|
+
|
|
156
|
+
// Nothing has a width until the widget is open, so the first placement has
|
|
157
|
+
// to wait for it — and it is a first appearance, not a move, so it does not
|
|
158
|
+
// animate. Same for a resize, which can change what fits either way.
|
|
159
|
+
document.addEventListener('widget-opened', (event) => {
|
|
160
|
+
if (event.detail.widget === 'toc') centreActiveEntry('auto');
|
|
161
|
+
});
|
|
162
|
+
window.addEventListener('resize', () => centreActiveEntry('auto'), { passive: true });
|
|
163
|
+
|
|
106
164
|
// Listen for heading stuck state changes from sticky.js
|
|
107
165
|
document.addEventListener('headingStuckStateChanged', (event) => {
|
|
108
166
|
if (event.detail.currentStuckHeading) {
|