@abreen/tada 1.19.0 → 1.19.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 +5 -3
- package/bin/tada.ts +1 -1
- package/build/reachability.ts +18 -54
- package/build/utils/render.ts +2 -2
- package/build/utils/trace-layout.ts +2 -14
- package/package.json +1 -1
- package/src/index.ts +4 -2
- package/src/navigate/_index.scss +5 -0
- package/src/navigate/runtime.ts +21 -4
- package/src/question/index.ts +7 -0
- package/src/search/index.ts +10 -60
- package/src/top/README.md +7 -2
- package/src/top/index.ts +25 -3
- package/templates/_top.html +3 -6
package/README.md
CHANGED
|
@@ -71,9 +71,11 @@ A static site generator. The successor to Presto.
|
|
|
71
71
|
* Special heading subtitles with `## Heading # A subtitle here`
|
|
72
72
|
* `{{{ _partial.md }}}` syntax for including partials
|
|
73
73
|
|
|
74
|
-
- Extremely
|
|
75
|
-
* A miniature SPA
|
|
76
|
-
*
|
|
74
|
+
- Extremely efficient client-side navigation
|
|
75
|
+
* A miniature SPA that uses internal links tagged at build time
|
|
76
|
+
* Get the benefits of DOM tree replacement without an entire bloated framework
|
|
77
|
+
* Thoroughly covered by browser tests across multiple engines
|
|
78
|
+
* Clear loading indicators for slow requests
|
|
77
79
|
* Also detects a new version of the current page and supplies a reload button
|
|
78
80
|
|
|
79
81
|
### Experimental features
|
package/bin/tada.ts
CHANGED
|
@@ -50,7 +50,7 @@ function parsePortArg(name: string): number | undefined {
|
|
|
50
50
|
const idx = process.argv.indexOf(name);
|
|
51
51
|
if (idx !== -1 && process.argv[idx + 1]) {
|
|
52
52
|
const val = parseInt(process.argv[idx + 1], 10);
|
|
53
|
-
if (val
|
|
53
|
+
if (val >= 0 && val < 65536) {
|
|
54
54
|
return val;
|
|
55
55
|
}
|
|
56
56
|
}
|
package/build/reachability.ts
CHANGED
|
@@ -2,59 +2,24 @@ import path from 'path';
|
|
|
2
2
|
import { normalizeOutputPath } from './util';
|
|
3
3
|
import type { HtmlOutputAnalysis } from './types';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
return normalizeOutputPath(target);
|
|
7
|
-
}
|
|
8
|
-
|
|
5
|
+
/** HTML candidates for an already normalized target without a non-HTML extension. */
|
|
9
6
|
function toCandidateHtmlAssetPaths(target: string): string[] {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (normalizedTarget === '/') {
|
|
7
|
+
if (target === '/') {
|
|
13
8
|
return ['index.html'];
|
|
14
9
|
}
|
|
15
10
|
|
|
16
|
-
if (
|
|
17
|
-
return [
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (path.posix.extname(normalizedTarget)) {
|
|
21
|
-
return [];
|
|
11
|
+
if (target.endsWith('.html')) {
|
|
12
|
+
return [target.slice(1)];
|
|
22
13
|
}
|
|
23
14
|
|
|
24
|
-
const withoutLeadingSlash =
|
|
25
|
-
if (
|
|
15
|
+
const withoutLeadingSlash = target.slice(1);
|
|
16
|
+
if (target.endsWith('/')) {
|
|
26
17
|
return [`${withoutLeadingSlash}index.html`];
|
|
27
18
|
}
|
|
28
19
|
|
|
29
20
|
return [`${withoutLeadingSlash}/index.html`, `${withoutLeadingSlash}.html`];
|
|
30
21
|
}
|
|
31
22
|
|
|
32
|
-
function resolveTargetToAssetTarget(target: string): string | null {
|
|
33
|
-
const normalizedTarget = normalizeInternalTarget(target);
|
|
34
|
-
if (normalizedTarget === '/' || normalizedTarget.endsWith('.html')) {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
return path.posix.extname(normalizedTarget) ? normalizedTarget : null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function collectOutgoingHtmlAssetPaths(
|
|
41
|
-
analysis: HtmlOutputAnalysis,
|
|
42
|
-
knownHtmlAssetPaths: Set<string>,
|
|
43
|
-
): string[] {
|
|
44
|
-
const htmlAssetPaths = new Set<string>();
|
|
45
|
-
|
|
46
|
-
for (const target of analysis.outgoingTargets) {
|
|
47
|
-
for (const candidate of toCandidateHtmlAssetPaths(target)) {
|
|
48
|
-
if (knownHtmlAssetPaths.has(candidate)) {
|
|
49
|
-
htmlAssetPaths.add(candidate);
|
|
50
|
-
break;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return [...htmlAssetPaths].sort();
|
|
56
|
-
}
|
|
57
|
-
|
|
58
23
|
interface CollectReachableOptions {
|
|
59
24
|
htmlAnalysisByPath: Map<string, HtmlOutputAnalysis>;
|
|
60
25
|
knownAssetTargets?: Set<string>;
|
|
@@ -75,7 +40,6 @@ export function collectReachableSiteAssets({
|
|
|
75
40
|
throw new Error(`Pagefind reachability root not found: ${rootPath}`);
|
|
76
41
|
}
|
|
77
42
|
|
|
78
|
-
const knownHtmlAssetPaths = new Set(htmlAnalysisByPath.keys());
|
|
79
43
|
const reachableHtmlPaths = new Set<string>();
|
|
80
44
|
const reachableAssetTargets = new Set<string>();
|
|
81
45
|
const pending: string[] = [rootPath];
|
|
@@ -89,19 +53,19 @@ export function collectReachableSiteAssets({
|
|
|
89
53
|
|
|
90
54
|
const analysis = htmlAnalysisByPath.get(currentPath)!;
|
|
91
55
|
|
|
92
|
-
for (const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
56
|
+
for (const outgoingTarget of analysis.outgoingTargets) {
|
|
57
|
+
const target = normalizeOutputPath(outgoingTarget);
|
|
58
|
+
if (!target.endsWith('.html') && path.posix.extname(target)) {
|
|
59
|
+
if (knownAssetTargets.has(target)) {
|
|
60
|
+
reachableAssetTargets.add(target);
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
98
63
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
reachableAssetTargets.add(assetTarget);
|
|
64
|
+
const targetPath = toCandidateHtmlAssetPaths(target).find(candidate =>
|
|
65
|
+
htmlAnalysisByPath.has(candidate),
|
|
66
|
+
);
|
|
67
|
+
if (targetPath && !reachableHtmlPaths.has(targetPath)) {
|
|
68
|
+
pending.push(targetPath);
|
|
105
69
|
}
|
|
106
70
|
}
|
|
107
71
|
}
|
package/build/utils/render.ts
CHANGED
|
@@ -426,7 +426,7 @@ export function renderCopiedContentAsset({
|
|
|
426
426
|
const rawSource = fs.readFileSync(filePath, 'utf-8');
|
|
427
427
|
const templated = applySourceTemplate(rawSource, siteVariables, filePath);
|
|
428
428
|
|
|
429
|
-
if (filePath.
|
|
429
|
+
if (path.extname(filePath).toLowerCase() === '.java') {
|
|
430
430
|
const pageDirPath = toPosix(
|
|
431
431
|
path.relative(contentDir, path.dirname(filePath)),
|
|
432
432
|
);
|
|
@@ -568,7 +568,7 @@ function renderPlainTextContent(
|
|
|
568
568
|
} {
|
|
569
569
|
const applyBasePath = createApplyBasePath(siteVariables);
|
|
570
570
|
|
|
571
|
-
const ext = path.extname(filePath);
|
|
571
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
572
572
|
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
573
573
|
const { pageVariables: rawPageVariables, content } =
|
|
574
574
|
parseFrontMatterAndContent(raw, ext);
|
|
@@ -34,18 +34,13 @@ const MIN_ARRAY_CELL_WIDTH = 48;
|
|
|
34
34
|
|
|
35
35
|
/** Information collected about a heap object across all trace steps. */
|
|
36
36
|
interface ObjectInfo {
|
|
37
|
-
id: string;
|
|
38
37
|
type: string;
|
|
39
38
|
firstStep: number;
|
|
40
39
|
lastStep: number;
|
|
41
|
-
/** Set of object IDs this object ever references (across all steps). */
|
|
42
|
-
references: Set<string>;
|
|
43
40
|
/** Step indices where this object is actually present. */
|
|
44
41
|
activeSteps: Set<number>;
|
|
45
42
|
/** Per-field reference targets (field name -> set of referenced object IDs). */
|
|
46
43
|
fieldRefs: Map<string, Set<string>>;
|
|
47
|
-
/** Max number of fields seen on this object (for field objects). */
|
|
48
|
-
maxFields: number;
|
|
49
44
|
/** Max number of elements seen on this object (for arrays). */
|
|
50
45
|
maxElements: number;
|
|
51
46
|
/** Scalar value (for heap objects represented by a single value). */
|
|
@@ -70,7 +65,7 @@ interface TreeNode {
|
|
|
70
65
|
|
|
71
66
|
/**
|
|
72
67
|
* Walk every step, collect every heap object's type, lifetime (first/last step),
|
|
73
|
-
*
|
|
68
|
+
* reference targets by field, display widths, and whether it's an array or string.
|
|
74
69
|
*/
|
|
75
70
|
export function scanSteps(steps: TraceStep[]): Map<string, ObjectInfo> {
|
|
76
71
|
const objects = new Map<string, ObjectInfo>();
|
|
@@ -81,14 +76,11 @@ export function scanSteps(steps: TraceStep[]): Map<string, ObjectInfo> {
|
|
|
81
76
|
let info = objects.get(id);
|
|
82
77
|
if (!info) {
|
|
83
78
|
info = {
|
|
84
|
-
id,
|
|
85
79
|
type: obj.type,
|
|
86
80
|
firstStep: stepIndex,
|
|
87
81
|
lastStep: stepIndex,
|
|
88
|
-
references: new Set(),
|
|
89
82
|
activeSteps: new Set(),
|
|
90
83
|
fieldRefs: new Map(),
|
|
91
|
-
maxFields: 0,
|
|
92
84
|
maxElements: 0,
|
|
93
85
|
value: null,
|
|
94
86
|
hasValue: false,
|
|
@@ -115,18 +107,14 @@ export function scanSteps(steps: TraceStep[]): Map<string, ObjectInfo> {
|
|
|
115
107
|
|
|
116
108
|
for (const elem of obj.elements) {
|
|
117
109
|
if (elem.type === 'ref') {
|
|
118
|
-
info.references.add(elem.id);
|
|
119
110
|
elemRefs.add(elem.id);
|
|
120
111
|
}
|
|
121
112
|
}
|
|
122
113
|
} else if ('fields' in obj) {
|
|
123
|
-
const
|
|
124
|
-
info.maxFields = Math.max(info.maxFields, fieldEntries.length);
|
|
125
|
-
for (const [fieldName, val] of fieldEntries) {
|
|
114
|
+
for (const [fieldName, val] of Object.entries(obj.fields)) {
|
|
126
115
|
info.fieldNames.add(fieldName);
|
|
127
116
|
|
|
128
117
|
if (val.type === 'ref') {
|
|
129
|
-
info.references.add(val.id);
|
|
130
118
|
if (!info.fieldRefs.has(fieldName)) {
|
|
131
119
|
info.fieldRefs.set(fieldName, new Set());
|
|
132
120
|
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -25,6 +25,8 @@ const PERSISTENT_COMPONENTS: Record<
|
|
|
25
25
|
let startTime = -1;
|
|
26
26
|
|
|
27
27
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
28
|
+
const initialHref = window.location.href;
|
|
29
|
+
const initialHash = window.location.hash;
|
|
28
30
|
startTime = window.performance.now();
|
|
29
31
|
|
|
30
32
|
const failed: Record<string, string> = {};
|
|
@@ -54,8 +56,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|
|
54
56
|
// fragment scroll. Do it ourselves once per-page components have
|
|
55
57
|
// finished mutating the DOM. On a fresh cold load the browser already
|
|
56
58
|
// scrolled, so this is a harmless re-align.
|
|
57
|
-
if (window.location.
|
|
58
|
-
getHashTarget(window.document,
|
|
59
|
+
if (initialHash && window.location.href === initialHref) {
|
|
60
|
+
getHashTarget(window.document, initialHash)?.scrollIntoView();
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
for (const [name, reason] of Object.entries(failed)) {
|
package/src/navigate/_index.scss
CHANGED
|
@@ -7,6 +7,11 @@ $movement-easing: cubic-bezier(0.2, 0, 0, 1);
|
|
|
7
7
|
--page-transition-movement-easing: #{$movement-easing};
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
html.navigation-loading,
|
|
11
|
+
html.navigation-loading * {
|
|
12
|
+
cursor: progress !important;
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
@keyframes page-fade-out {
|
|
11
16
|
to {
|
|
12
17
|
opacity: 0;
|
package/src/navigate/runtime.ts
CHANGED
|
@@ -9,6 +9,8 @@ import { swapHeaderTitle } from '../header';
|
|
|
9
9
|
|
|
10
10
|
export const NAVIGATION_EVENT = 'tada:navigation';
|
|
11
11
|
|
|
12
|
+
const LOADING_CURSOR_DELAY = 400;
|
|
13
|
+
|
|
12
14
|
let currentAbortController: AbortController | null = null;
|
|
13
15
|
let historyIndex = 0;
|
|
14
16
|
let currentPath = '';
|
|
@@ -235,15 +237,28 @@ export async function navigateToUrl(
|
|
|
235
237
|
|
|
236
238
|
const header = document.querySelector('header');
|
|
237
239
|
header?.classList.add('loading');
|
|
240
|
+
const loadingCursorTimeout = window.setTimeout(() => {
|
|
241
|
+
if (isActiveNavigation(controller)) {
|
|
242
|
+
document.documentElement.classList.add('navigation-loading');
|
|
243
|
+
}
|
|
244
|
+
}, LOADING_CURSOR_DELAY);
|
|
245
|
+
const stopLoading = (): void => {
|
|
246
|
+
window.clearTimeout(loadingCursorTimeout);
|
|
247
|
+
if (!isActiveNavigation(controller)) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
header?.classList.remove('loading');
|
|
251
|
+
document.documentElement.classList.remove('navigation-loading');
|
|
252
|
+
};
|
|
238
253
|
|
|
239
254
|
let response: Response;
|
|
240
255
|
try {
|
|
241
256
|
response = await globals.fetch(url, { signal: controller.signal });
|
|
242
257
|
} catch (err: unknown) {
|
|
258
|
+
stopLoading();
|
|
243
259
|
if (!isActiveNavigation(controller)) {
|
|
244
260
|
return;
|
|
245
261
|
}
|
|
246
|
-
header?.classList.remove('loading');
|
|
247
262
|
if (isAbortError(err)) {
|
|
248
263
|
return;
|
|
249
264
|
}
|
|
@@ -252,11 +267,12 @@ export async function navigateToUrl(
|
|
|
252
267
|
}
|
|
253
268
|
|
|
254
269
|
if (!isActiveNavigation(controller)) {
|
|
270
|
+
stopLoading();
|
|
255
271
|
return;
|
|
256
272
|
}
|
|
257
273
|
|
|
258
274
|
if (!response.ok) {
|
|
259
|
-
|
|
275
|
+
stopLoading();
|
|
260
276
|
globals.setLocationHref(window, url);
|
|
261
277
|
return;
|
|
262
278
|
}
|
|
@@ -265,10 +281,10 @@ export async function navigateToUrl(
|
|
|
265
281
|
try {
|
|
266
282
|
html = await response.text();
|
|
267
283
|
} catch (err: unknown) {
|
|
284
|
+
stopLoading();
|
|
268
285
|
if (!isActiveNavigation(controller)) {
|
|
269
286
|
return;
|
|
270
287
|
}
|
|
271
|
-
header?.classList.remove('loading');
|
|
272
288
|
if (isAbortError(err)) {
|
|
273
289
|
return;
|
|
274
290
|
}
|
|
@@ -277,10 +293,11 @@ export async function navigateToUrl(
|
|
|
277
293
|
}
|
|
278
294
|
|
|
279
295
|
if (!isActiveNavigation(controller)) {
|
|
296
|
+
stopLoading();
|
|
280
297
|
return;
|
|
281
298
|
}
|
|
282
299
|
|
|
283
|
-
|
|
300
|
+
stopLoading();
|
|
284
301
|
|
|
285
302
|
const DOMParserCtor = (window as unknown as { DOMParser: typeof DOMParser })
|
|
286
303
|
.DOMParser;
|
package/src/question/index.ts
CHANGED
|
@@ -62,12 +62,19 @@ export default (window: Window) => {
|
|
|
62
62
|
const bodies =
|
|
63
63
|
window.document.querySelectorAll<HTMLElement>('.question-a-body');
|
|
64
64
|
bodies.forEach(el => {
|
|
65
|
+
const content = el.querySelector<HTMLElement>('.question-a-content');
|
|
66
|
+
if (content) {
|
|
67
|
+
content.inert = !el.hasAttribute('data-revealed');
|
|
68
|
+
}
|
|
65
69
|
el.setAttribute('role', 'button');
|
|
66
70
|
el.setAttribute('tabindex', '0');
|
|
67
71
|
el.setAttribute('aria-label', 'Click to reveal answer');
|
|
68
72
|
|
|
69
73
|
const reveal = () => {
|
|
70
74
|
el.setAttribute('data-revealed', '');
|
|
75
|
+
if (content) {
|
|
76
|
+
content.inert = false;
|
|
77
|
+
}
|
|
71
78
|
el.removeAttribute('role');
|
|
72
79
|
el.removeAttribute('tabindex');
|
|
73
80
|
el.removeAttribute('aria-label');
|
package/src/search/index.ts
CHANGED
|
@@ -94,25 +94,7 @@ async function doSearch(query: string, window: Window): Promise<SearchState> {
|
|
|
94
94
|
return { totalResults: grouped.length, results: grouped };
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
function
|
|
98
|
-
resultsContainer: HTMLElement,
|
|
99
|
-
focusedEl: HTMLElement | null,
|
|
100
|
-
) {
|
|
101
|
-
const options = Array.from(
|
|
102
|
-
resultsContainer.querySelectorAll('[role="option"]'),
|
|
103
|
-
) as HTMLElement[];
|
|
104
|
-
options.forEach(opt => {
|
|
105
|
-
const selected = focusedEl !== null && opt.contains(focusedEl);
|
|
106
|
-
opt.setAttribute('aria-selected', selected ? 'true' : 'false');
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function render(
|
|
111
|
-
input: HTMLInputElement,
|
|
112
|
-
resultsContainer: HTMLElement,
|
|
113
|
-
state: State,
|
|
114
|
-
loading = false,
|
|
115
|
-
) {
|
|
97
|
+
function render(resultsContainer: HTMLElement, state: State, loading = false) {
|
|
116
98
|
const doc = resultsContainer.ownerDocument;
|
|
117
99
|
|
|
118
100
|
if (state.showResults) {
|
|
@@ -129,16 +111,10 @@ function render(
|
|
|
129
111
|
}
|
|
130
112
|
|
|
131
113
|
const ol = doc.createElement('ol');
|
|
132
|
-
ol.id = `${input.name}-results`;
|
|
133
|
-
ol.role = 'listbox';
|
|
134
|
-
ol.tabIndex = -1;
|
|
135
114
|
ol.setAttribute('aria-label', 'Search results');
|
|
136
115
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
state.results.forEach((result, i) => {
|
|
116
|
+
state.results.forEach(result => {
|
|
140
117
|
const a = doc.createElement('a');
|
|
141
|
-
a.id = `result-${i}`;
|
|
142
118
|
a.className = 'result';
|
|
143
119
|
a.href = result.url;
|
|
144
120
|
const isGeneratedPage = Boolean(result.template);
|
|
@@ -148,7 +124,6 @@ function render(
|
|
|
148
124
|
a.tabIndex = 0;
|
|
149
125
|
|
|
150
126
|
const titleEl = doc.createElement('div');
|
|
151
|
-
titleEl.id = `title-${i}`;
|
|
152
127
|
titleEl.className = 'title';
|
|
153
128
|
if (result.template === 'code') {
|
|
154
129
|
titleEl.classList.add('code-page');
|
|
@@ -166,12 +141,6 @@ function render(
|
|
|
166
141
|
a.appendChild(excerpt);
|
|
167
142
|
|
|
168
143
|
const li = doc.createElement('li');
|
|
169
|
-
li.id = `option-${i}`;
|
|
170
|
-
li.setAttribute('role', 'option');
|
|
171
|
-
li.setAttribute('aria-selected', 'false');
|
|
172
|
-
li.setAttribute('aria-setsize', String(totalVisible));
|
|
173
|
-
li.setAttribute('aria-posinset', String(i + 1));
|
|
174
|
-
li.setAttribute('aria-labelledby', `title-${i}`);
|
|
175
144
|
li.appendChild(a);
|
|
176
145
|
|
|
177
146
|
const subsToShow = result.subResults;
|
|
@@ -265,9 +234,6 @@ function render(
|
|
|
265
234
|
resultsContainer.setAttribute('aria-hidden', 'true');
|
|
266
235
|
resultsContainer.setAttribute('inert', '');
|
|
267
236
|
}
|
|
268
|
-
|
|
269
|
-
input.setAttribute('aria-expanded', String(state.showResults));
|
|
270
|
-
input.setAttribute('aria-controls', ol.id);
|
|
271
237
|
}
|
|
272
238
|
|
|
273
239
|
export default (window: Window) => {
|
|
@@ -335,7 +301,7 @@ export default (window: Window) => {
|
|
|
335
301
|
pagefindLoadGeneration += 1;
|
|
336
302
|
console.log(`failed to load Pagefind: ${err}`);
|
|
337
303
|
if (state.showResults) {
|
|
338
|
-
render(
|
|
304
|
+
render(resultsContainer, state, false);
|
|
339
305
|
}
|
|
340
306
|
}
|
|
341
307
|
return false;
|
|
@@ -362,7 +328,7 @@ export default (window: Window) => {
|
|
|
362
328
|
|
|
363
329
|
async function update(updateId: number) {
|
|
364
330
|
if (state.showResults) {
|
|
365
|
-
render(
|
|
331
|
+
render(resultsContainer, state, true);
|
|
366
332
|
}
|
|
367
333
|
const query = state.value;
|
|
368
334
|
if (!(await loadPagefind()) || updateId !== latestUpdateId) {
|
|
@@ -380,7 +346,7 @@ export default (window: Window) => {
|
|
|
380
346
|
if (!state.showResults) {
|
|
381
347
|
return;
|
|
382
348
|
}
|
|
383
|
-
render(
|
|
349
|
+
render(resultsContainer, state, false);
|
|
384
350
|
}
|
|
385
351
|
|
|
386
352
|
function queueUpdate() {
|
|
@@ -402,7 +368,7 @@ export default (window: Window) => {
|
|
|
402
368
|
.catch(err => {
|
|
403
369
|
console.log(`failed to load Pagefind: ${err}`);
|
|
404
370
|
if (state.showResults) {
|
|
405
|
-
render(
|
|
371
|
+
render(resultsContainer, state, false);
|
|
406
372
|
}
|
|
407
373
|
});
|
|
408
374
|
}
|
|
@@ -418,7 +384,7 @@ export default (window: Window) => {
|
|
|
418
384
|
const stalePagefind = pagefind;
|
|
419
385
|
invalidatePagefind();
|
|
420
386
|
if (state.showResults) {
|
|
421
|
-
render(
|
|
387
|
+
render(resultsContainer, state, true);
|
|
422
388
|
}
|
|
423
389
|
if (stalePagefind) {
|
|
424
390
|
try {
|
|
@@ -434,7 +400,7 @@ export default (window: Window) => {
|
|
|
434
400
|
reloadPagefindAfterPageUpdateRefresh().catch(err => {
|
|
435
401
|
console.log(`failed to reload Pagefind: ${err}`);
|
|
436
402
|
if (state.showResults) {
|
|
437
|
-
render(
|
|
403
|
+
render(resultsContainer, state, false);
|
|
438
404
|
}
|
|
439
405
|
});
|
|
440
406
|
}
|
|
@@ -446,7 +412,7 @@ export default (window: Window) => {
|
|
|
446
412
|
return;
|
|
447
413
|
}
|
|
448
414
|
state.showResults = false;
|
|
449
|
-
render(
|
|
415
|
+
render(resultsContainer, state);
|
|
450
416
|
}
|
|
451
417
|
|
|
452
418
|
function handleInput(e: Event) {
|
|
@@ -573,23 +539,11 @@ export default (window: Window) => {
|
|
|
573
539
|
}
|
|
574
540
|
} else if (e.key === 'Escape') {
|
|
575
541
|
e.preventDefault();
|
|
576
|
-
hide();
|
|
577
542
|
input!.focus();
|
|
543
|
+
hide();
|
|
578
544
|
}
|
|
579
545
|
}
|
|
580
546
|
|
|
581
|
-
function handleResultsFocusIn(e: FocusEvent) {
|
|
582
|
-
applyHighlight(resultsContainer, e.target as HTMLElement);
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
function handleResultsFocusOut(e: FocusEvent) {
|
|
586
|
-
const related = e.relatedTarget as HTMLElement | null;
|
|
587
|
-
if (related && (resultsContainer.contains(related) || related === input)) {
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
applyHighlight(resultsContainer, null);
|
|
591
|
-
}
|
|
592
|
-
|
|
593
547
|
function handleWindowPointerDown(e: PointerEvent) {
|
|
594
548
|
const target = e.target as Node;
|
|
595
549
|
if (!input!.contains(target) && !resultsContainer.contains(target)) {
|
|
@@ -616,8 +570,6 @@ export default (window: Window) => {
|
|
|
616
570
|
resultsContainer.addEventListener('pointerdown', handlePointerDown);
|
|
617
571
|
resultsContainer.addEventListener('click', handleResultClick);
|
|
618
572
|
resultsContainer.addEventListener('keydown', handleResultsKeyDown);
|
|
619
|
-
resultsContainer.addEventListener('focusin', handleResultsFocusIn);
|
|
620
|
-
resultsContainer.addEventListener('focusout', handleResultsFocusOut);
|
|
621
573
|
window.addEventListener('pointerup', handleWindowPointerUp);
|
|
622
574
|
window.addEventListener('pointerdown', handleWindowPointerDown);
|
|
623
575
|
window.addEventListener('pointermove', handleWindowPointerMove);
|
|
@@ -637,8 +589,6 @@ export default (window: Window) => {
|
|
|
637
589
|
window.removeEventListener('pointermove', handleWindowPointerMove);
|
|
638
590
|
window.removeEventListener('pointerdown', handleWindowPointerDown);
|
|
639
591
|
window.removeEventListener('pointerup', handleWindowPointerUp);
|
|
640
|
-
resultsContainer.removeEventListener('focusout', handleResultsFocusOut);
|
|
641
|
-
resultsContainer.removeEventListener('focusin', handleResultsFocusIn);
|
|
642
592
|
resultsContainer.removeEventListener('keydown', handleResultsKeyDown);
|
|
643
593
|
resultsContainer.removeEventListener('click', handleResultClick);
|
|
644
594
|
resultsContainer.removeEventListener('pointerdown', handlePointerDown);
|
package/src/top/README.md
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
# The `top` component
|
|
2
2
|
|
|
3
|
-
Show a floating "back to top" button once the
|
|
4
|
-
and keep it visible until
|
|
3
|
+
Show a floating "back to top" button on long pages once the reader has scrolled
|
|
4
|
+
well down the page, and keep it visible until they are back above that point.
|
|
5
|
+
|
|
6
|
+
- The page must be at least 3 viewports tall; shorter pages never show it.
|
|
7
|
+
- It shows once the scroll position passes 1.5 viewports or 25% of the
|
|
8
|
+
scrollable distance, whichever is farther.
|
|
9
|
+
- Visibility is recalculated on scroll and on resize.
|
package/src/top/index.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { debounce, removeClass } from '../util';
|
|
2
2
|
import { globals } from '../globals';
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
const
|
|
4
|
+
/** Only show the "Back to top" button on pages at least this many viewports tall */
|
|
5
|
+
const MIN_PAGE_SCREENS = 3;
|
|
6
|
+
|
|
7
|
+
/** Show the button once the user is this many viewports down the page... */
|
|
8
|
+
const SHOW_AFTER_SCREENS = 1.5;
|
|
9
|
+
|
|
10
|
+
/** ...or this fraction of the scrollable distance, whichever is farther */
|
|
11
|
+
const SHOW_AFTER_FRACTION = 0.25;
|
|
6
12
|
|
|
7
13
|
/** Debounce time (maximum amount of time to wait before updates) */
|
|
8
14
|
const LATENCY_MS = 50;
|
|
@@ -60,8 +66,22 @@ export default (window: Window) => {
|
|
|
60
66
|
isShowing = false;
|
|
61
67
|
}
|
|
62
68
|
|
|
69
|
+
function shouldShow(): boolean {
|
|
70
|
+
const viewportHeight = window.innerHeight;
|
|
71
|
+
const maxScroll =
|
|
72
|
+
window.document.documentElement.scrollHeight - viewportHeight;
|
|
73
|
+
if (maxScroll < (MIN_PAGE_SCREENS - 1) * viewportHeight) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const threshold = Math.max(
|
|
77
|
+
SHOW_AFTER_SCREENS * viewportHeight,
|
|
78
|
+
SHOW_AFTER_FRACTION * maxScroll,
|
|
79
|
+
);
|
|
80
|
+
return window.scrollY > threshold;
|
|
81
|
+
}
|
|
82
|
+
|
|
63
83
|
function updateVisibility() {
|
|
64
|
-
if (
|
|
84
|
+
if (shouldShow()) {
|
|
65
85
|
show(link);
|
|
66
86
|
} else {
|
|
67
87
|
hide(link);
|
|
@@ -70,9 +90,11 @@ export default (window: Window) => {
|
|
|
70
90
|
|
|
71
91
|
const debounced = debounce(window, updateVisibility, LATENCY_MS);
|
|
72
92
|
window.addEventListener('scroll', debounced, { passive: true });
|
|
93
|
+
window.addEventListener('resize', debounced, { passive: true });
|
|
73
94
|
updateVisibility();
|
|
74
95
|
|
|
75
96
|
return () => {
|
|
76
97
|
window.removeEventListener('scroll', debounced);
|
|
98
|
+
window.removeEventListener('resize', debounced);
|
|
77
99
|
};
|
|
78
100
|
};
|
package/templates/_top.html
CHANGED
|
@@ -192,20 +192,17 @@
|
|
|
192
192
|
<div class="header-controls">
|
|
193
193
|
<div id="to-top-container" class="to-top-container"></div>
|
|
194
194
|
<% if (site.features.search) { %>
|
|
195
|
-
<div class="search-controls">
|
|
195
|
+
<div class="search-controls" role="search">
|
|
196
196
|
<input type="search" class="search quick-search"
|
|
197
197
|
autocomplete="off" name="quick-search"
|
|
198
198
|
placeholder="Search"
|
|
199
|
-
|
|
200
|
-
aria-haspopup="listbox"
|
|
201
|
-
aria-autocomplete="list"
|
|
202
|
-
aria-expanded="false"
|
|
199
|
+
aria-label="Search"
|
|
203
200
|
aria-controls="quick-search-results"
|
|
204
201
|
disabled />
|
|
205
202
|
</div>
|
|
206
203
|
<% } %>
|
|
207
204
|
</div>
|
|
208
|
-
<div class="results-container" role="region"
|
|
205
|
+
<div id="quick-search-results" class="results-container" role="region"
|
|
209
206
|
aria-label="Search results"
|
|
210
207
|
aria-live="polite" aria-hidden="true" inert>
|
|
211
208
|
<div class="results" style="display:none"></div>
|