@abreen/tada 1.19.0 → 1.19.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 +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/hash-target.ts +21 -2
- package/src/index.ts +4 -2
- package/src/navigate/_index.scss +22 -6
- package/src/navigate/index.ts +23 -12
- package/src/navigate/runtime.ts +119 -5
- 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/_theme.scss +1 -1
- 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/hash-target.ts
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* Tada also allows raw HTML, so an author can create a decoded ID like
|
|
6
6
|
* <h2 id="hello world">Hello</h2>. A link to that target becomes
|
|
7
7
|
* #hello%20world, and getElementById('hello%20world') would not find it.
|
|
8
|
+
*
|
|
9
|
+
* Like the browser, fall back to a legacy <a name="..."> anchor when no
|
|
10
|
+
* element has the ID.
|
|
8
11
|
*/
|
|
9
12
|
export function getHashTarget(
|
|
10
13
|
document: Document,
|
|
@@ -15,15 +18,31 @@ export function getHashTarget(
|
|
|
15
18
|
return null;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
|
-
const rawTarget = document
|
|
21
|
+
const rawTarget = findIndicatedElement(document, id);
|
|
19
22
|
if (rawTarget) {
|
|
20
23
|
return rawTarget;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
try {
|
|
24
27
|
const decodedId = decodeURIComponent(id);
|
|
25
|
-
return decodedId === id ? null : document
|
|
28
|
+
return decodedId === id ? null : findIndicatedElement(document, decodedId);
|
|
26
29
|
} catch {
|
|
27
30
|
return null;
|
|
28
31
|
}
|
|
29
32
|
}
|
|
33
|
+
|
|
34
|
+
function findIndicatedElement(
|
|
35
|
+
document: Document,
|
|
36
|
+
fragment: string,
|
|
37
|
+
): HTMLElement | null {
|
|
38
|
+
const byId = document.getElementById(fragment);
|
|
39
|
+
if (byId) {
|
|
40
|
+
return byId;
|
|
41
|
+
}
|
|
42
|
+
for (const element of document.getElementsByName(fragment)) {
|
|
43
|
+
if (element.localName === 'a') {
|
|
44
|
+
return element;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
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;
|
|
@@ -100,12 +105,10 @@ $movement-easing: cubic-bezier(0.2, 0, 0, 1);
|
|
|
100
105
|
mix-blend-mode: normal;
|
|
101
106
|
}
|
|
102
107
|
|
|
103
|
-
// The footer and appearance controls get their own
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
108
|
+
// The footer and appearance controls get their own layer while they are in the
|
|
109
|
+
// viewport. view-transition-name is set via inline style by the navigator. On
|
|
110
|
+
// screen on both pages, the layer stays put and shows only the incoming
|
|
111
|
+
// snapshot so disabled defaults and stale hover states cannot blend.
|
|
109
112
|
::view-transition-group(page-bottom),
|
|
110
113
|
::view-transition-old(page-bottom),
|
|
111
114
|
::view-transition-new(page-bottom) {
|
|
@@ -120,6 +123,19 @@ $movement-easing: cubic-bezier(0.2, 0, 0, 1);
|
|
|
120
123
|
mix-blend-mode: normal;
|
|
121
124
|
}
|
|
122
125
|
|
|
126
|
+
// On screen on only one page, such as between a short page and the top of a
|
|
127
|
+
// long page: fade out or in where they are, in step with the page. Fill both
|
|
128
|
+
// ways, like the UA default, so the faded-out snapshot cannot reappear in the
|
|
129
|
+
// transition's last frame.
|
|
130
|
+
::view-transition-old(page-bottom):only-child {
|
|
131
|
+
display: block;
|
|
132
|
+
animation: page-fade-out $duration ease-in both;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
::view-transition-new(page-bottom):only-child {
|
|
136
|
+
animation: page-fade-in $duration ease-out both;
|
|
137
|
+
}
|
|
138
|
+
|
|
123
139
|
// Forward navigation: old content slides left, new content slides in from right
|
|
124
140
|
.nav-forward::view-transition-old(root) {
|
|
125
141
|
animation:
|
package/src/navigate/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isEligibleLink } from './eligible';
|
|
2
2
|
import {
|
|
3
|
+
applyFragmentTarget,
|
|
3
4
|
clearSearch,
|
|
4
5
|
closeHeaderDetails,
|
|
5
6
|
getCurrentPath,
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
getHistoryIndex,
|
|
8
9
|
getSavedScroll,
|
|
9
10
|
initNavigation,
|
|
11
|
+
isApplyingFragment,
|
|
10
12
|
navigateToUrl,
|
|
11
13
|
saveScrollPosition,
|
|
12
14
|
setCurrentPath,
|
|
@@ -134,6 +136,10 @@ export default function mountNavigate(window: Window): () => void {
|
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
function handlePopState(event: PopStateEvent) {
|
|
139
|
+
if (isApplyingFragment()) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
137
143
|
if (clearingFragment) {
|
|
138
144
|
clearingFragment = false;
|
|
139
145
|
if (!window.location.hash) {
|
|
@@ -145,19 +151,24 @@ export default function mountNavigate(window: Window): () => void {
|
|
|
145
151
|
if (newPath === getCurrentPath()) {
|
|
146
152
|
const locationKey = newPath + window.location.hash;
|
|
147
153
|
const savedY = getSavedLocationScroll(locationKey);
|
|
154
|
+
// Fragment traversal can update :target and apply its native target
|
|
155
|
+
// scroll after popstate. Correct both in the next task so the URL and our
|
|
156
|
+
// saved position win. Capture savedY now because the native scroll event
|
|
157
|
+
// can update the location map first.
|
|
158
|
+
window.setTimeout(() => {
|
|
159
|
+
const currentLocationKey =
|
|
160
|
+
window.location.pathname +
|
|
161
|
+
window.location.search +
|
|
162
|
+
window.location.hash;
|
|
163
|
+
if (currentLocationKey !== locationKey) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
applyFragmentTarget(window);
|
|
167
|
+
if (typeof savedY === 'number') {
|
|
168
|
+
window.scrollTo({ top: savedY });
|
|
169
|
+
}
|
|
170
|
+
}, 0);
|
|
148
171
|
if (typeof savedY === 'number') {
|
|
149
|
-
// Fragment traversal can apply its native target scroll after popstate.
|
|
150
|
-
// Restore in the next task so our saved position wins. Capture savedY
|
|
151
|
-
// now because the native scroll event can update the location map first.
|
|
152
|
-
window.setTimeout(() => {
|
|
153
|
-
const currentLocationKey =
|
|
154
|
-
window.location.pathname +
|
|
155
|
-
window.location.search +
|
|
156
|
-
window.location.hash;
|
|
157
|
-
if (currentLocationKey === locationKey) {
|
|
158
|
-
window.scrollTo({ top: savedY });
|
|
159
|
-
}
|
|
160
|
-
}, 0);
|
|
161
172
|
return;
|
|
162
173
|
}
|
|
163
174
|
|
package/src/navigate/runtime.ts
CHANGED
|
@@ -5,16 +5,24 @@ import {
|
|
|
5
5
|
teardownPerPageComponents,
|
|
6
6
|
} from './lifecycle';
|
|
7
7
|
import { globals } from '../globals';
|
|
8
|
+
import { getHashTarget } from '../hash-target';
|
|
8
9
|
import { swapHeaderTitle } from '../header';
|
|
9
10
|
|
|
10
11
|
export const NAVIGATION_EVENT = 'tada:navigation';
|
|
11
12
|
|
|
13
|
+
const LOADING_CURSOR_DELAY = 400;
|
|
14
|
+
|
|
12
15
|
let currentAbortController: AbortController | null = null;
|
|
13
16
|
let historyIndex = 0;
|
|
14
17
|
let currentPath = '';
|
|
18
|
+
let applyingFragment = false;
|
|
19
|
+
let fragmentTargetRequest = 0;
|
|
15
20
|
|
|
16
21
|
const scrollByIndex = new Map<number, number>();
|
|
17
22
|
const scrollByLocation = new Map<string, number>();
|
|
23
|
+
// Stylesheets the navigator added that have not loaded or failed yet. Those in
|
|
24
|
+
// the initial document have settled before the deferred client script runs.
|
|
25
|
+
const pendingStylesheets = new Map<Element, Promise<void>>();
|
|
18
26
|
|
|
19
27
|
type Direction = 'forward' | 'back';
|
|
20
28
|
|
|
@@ -63,7 +71,19 @@ function updateHead(document: Document, newDoc: Document): void {
|
|
|
63
71
|
|
|
64
72
|
for (const link of newDoc.querySelectorAll('link[rel="stylesheet"]')) {
|
|
65
73
|
if (!existingHrefs.has(link.getAttribute('href'))) {
|
|
66
|
-
|
|
74
|
+
const clone = link.cloneNode(true) as Element;
|
|
75
|
+
pendingStylesheets.set(
|
|
76
|
+
clone,
|
|
77
|
+
new Promise(resolve => {
|
|
78
|
+
const settle = () => {
|
|
79
|
+
pendingStylesheets.delete(clone);
|
|
80
|
+
resolve();
|
|
81
|
+
};
|
|
82
|
+
clone.addEventListener('load', settle, { once: true });
|
|
83
|
+
clone.addEventListener('error', settle, { once: true });
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
document.head.appendChild(clone);
|
|
67
87
|
}
|
|
68
88
|
}
|
|
69
89
|
}
|
|
@@ -136,11 +156,29 @@ function setTitleTransitionNames(titleEl: Element, enabled: boolean): void {
|
|
|
136
156
|
}
|
|
137
157
|
}
|
|
138
158
|
|
|
159
|
+
// The footer and appearance pickers get their own layer only while they are in
|
|
160
|
+
// the viewport, so the layer stays put when they are on screen on both pages
|
|
161
|
+
// and fades on its own when they are on screen on only one.
|
|
162
|
+
function setPageBottomTransitionName(window: Window): void {
|
|
163
|
+
const bottom = window.document.querySelector<HTMLElement>('.page-bottom');
|
|
164
|
+
if (!bottom) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const rect = bottom.getBoundingClientRect();
|
|
168
|
+
if (rect.bottom > 0 && rect.top < window.innerHeight) {
|
|
169
|
+
bottom.style.viewTransitionName = 'page-bottom';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
139
173
|
function cleanupViewTransitionNames(document: Document): void {
|
|
140
174
|
const titleEl = document.querySelector('.title-and-info');
|
|
141
175
|
if (titleEl) {
|
|
142
176
|
setTitleTransitionNames(titleEl, false);
|
|
143
177
|
}
|
|
178
|
+
const bottom = document.querySelector<HTMLElement>('.page-bottom');
|
|
179
|
+
if (bottom) {
|
|
180
|
+
bottom.style.viewTransitionName = '';
|
|
181
|
+
}
|
|
144
182
|
}
|
|
145
183
|
|
|
146
184
|
function getLocationKey(window: Window): string {
|
|
@@ -211,6 +249,60 @@ export function getSavedLocationScroll(
|
|
|
211
249
|
return scrollByLocation.get(locationKey);
|
|
212
250
|
}
|
|
213
251
|
|
|
252
|
+
// Browsers fire popstate synchronously during applyFragmentTarget
|
|
253
|
+
export function isApplyingFragment(): boolean {
|
|
254
|
+
return applyingFragment;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Restoring a history entry can leave :target stale. With manual scroll
|
|
258
|
+
// restoration, WebKit skips fragment processing on Back/Forward, and swapped
|
|
259
|
+
// content loses it in every browser. A fragment navigation sets it in every
|
|
260
|
+
// browser, but it can reset the entry's history state and scroll position, so
|
|
261
|
+
// restore both.
|
|
262
|
+
//
|
|
263
|
+
// WebKit defers fragment scrolling until pending stylesheets load, which would
|
|
264
|
+
// later move the page away from the restored scroll position. Wait for them
|
|
265
|
+
// first, and skip the update if another request or navigation replaced it.
|
|
266
|
+
export function applyFragmentTarget(window: Window): void {
|
|
267
|
+
const request = ++fragmentTargetRequest;
|
|
268
|
+
const { href } = window.location;
|
|
269
|
+
const apply = (): void => {
|
|
270
|
+
if (request !== fragmentTargetRequest || window.location.href !== href) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const pending = Array.from(pendingStylesheets.values());
|
|
274
|
+
if (pending.length > 0) {
|
|
275
|
+
void Promise.all(pending).then(apply);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
replaceFragmentTarget(window);
|
|
279
|
+
};
|
|
280
|
+
apply();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function replaceFragmentTarget(window: Window): void {
|
|
284
|
+
const { document, history, location } = window;
|
|
285
|
+
if (
|
|
286
|
+
document.querySelector(':target') === getHashTarget(document, location.hash)
|
|
287
|
+
) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const { href } = location;
|
|
291
|
+
const state = history.state;
|
|
292
|
+
const { scrollX, scrollY } = window;
|
|
293
|
+
applyingFragment = true;
|
|
294
|
+
try {
|
|
295
|
+
globals.replaceLocation(
|
|
296
|
+
window,
|
|
297
|
+
location.pathname + location.search + (location.hash || '#'),
|
|
298
|
+
);
|
|
299
|
+
} finally {
|
|
300
|
+
applyingFragment = false;
|
|
301
|
+
}
|
|
302
|
+
history.replaceState(state, '', href);
|
|
303
|
+
window.scrollTo({ left: scrollX, top: scrollY });
|
|
304
|
+
}
|
|
305
|
+
|
|
214
306
|
export async function navigateToUrl(
|
|
215
307
|
window: Window,
|
|
216
308
|
options: NavigationOptions,
|
|
@@ -235,15 +327,28 @@ export async function navigateToUrl(
|
|
|
235
327
|
|
|
236
328
|
const header = document.querySelector('header');
|
|
237
329
|
header?.classList.add('loading');
|
|
330
|
+
const loadingCursorTimeout = window.setTimeout(() => {
|
|
331
|
+
if (isActiveNavigation(controller)) {
|
|
332
|
+
document.documentElement.classList.add('navigation-loading');
|
|
333
|
+
}
|
|
334
|
+
}, LOADING_CURSOR_DELAY);
|
|
335
|
+
const stopLoading = (): void => {
|
|
336
|
+
window.clearTimeout(loadingCursorTimeout);
|
|
337
|
+
if (!isActiveNavigation(controller)) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
header?.classList.remove('loading');
|
|
341
|
+
document.documentElement.classList.remove('navigation-loading');
|
|
342
|
+
};
|
|
238
343
|
|
|
239
344
|
let response: Response;
|
|
240
345
|
try {
|
|
241
346
|
response = await globals.fetch(url, { signal: controller.signal });
|
|
242
347
|
} catch (err: unknown) {
|
|
348
|
+
stopLoading();
|
|
243
349
|
if (!isActiveNavigation(controller)) {
|
|
244
350
|
return;
|
|
245
351
|
}
|
|
246
|
-
header?.classList.remove('loading');
|
|
247
352
|
if (isAbortError(err)) {
|
|
248
353
|
return;
|
|
249
354
|
}
|
|
@@ -252,11 +357,12 @@ export async function navigateToUrl(
|
|
|
252
357
|
}
|
|
253
358
|
|
|
254
359
|
if (!isActiveNavigation(controller)) {
|
|
360
|
+
stopLoading();
|
|
255
361
|
return;
|
|
256
362
|
}
|
|
257
363
|
|
|
258
364
|
if (!response.ok) {
|
|
259
|
-
|
|
365
|
+
stopLoading();
|
|
260
366
|
globals.setLocationHref(window, url);
|
|
261
367
|
return;
|
|
262
368
|
}
|
|
@@ -265,10 +371,10 @@ export async function navigateToUrl(
|
|
|
265
371
|
try {
|
|
266
372
|
html = await response.text();
|
|
267
373
|
} catch (err: unknown) {
|
|
374
|
+
stopLoading();
|
|
268
375
|
if (!isActiveNavigation(controller)) {
|
|
269
376
|
return;
|
|
270
377
|
}
|
|
271
|
-
header?.classList.remove('loading');
|
|
272
378
|
if (isAbortError(err)) {
|
|
273
379
|
return;
|
|
274
380
|
}
|
|
@@ -277,10 +383,11 @@ export async function navigateToUrl(
|
|
|
277
383
|
}
|
|
278
384
|
|
|
279
385
|
if (!isActiveNavigation(controller)) {
|
|
386
|
+
stopLoading();
|
|
280
387
|
return;
|
|
281
388
|
}
|
|
282
389
|
|
|
283
|
-
|
|
390
|
+
stopLoading();
|
|
284
391
|
|
|
285
392
|
const DOMParserCtor = (window as unknown as { DOMParser: typeof DOMParser })
|
|
286
393
|
.DOMParser;
|
|
@@ -306,6 +413,11 @@ export async function navigateToUrl(
|
|
|
306
413
|
|
|
307
414
|
teardownPerPageComponents();
|
|
308
415
|
swapContent(document, newDoc);
|
|
416
|
+
if (!pushHistory) {
|
|
417
|
+
// Restore the entry's :target before updateHead adds stylesheets, so it
|
|
418
|
+
// applies right away unless an earlier stylesheet is still loading
|
|
419
|
+
applyFragmentTarget(window);
|
|
420
|
+
}
|
|
309
421
|
updateHead(document, newDoc);
|
|
310
422
|
mountAppearancePickerForPage(window);
|
|
311
423
|
currentPath = parsed.pathname + parsed.search;
|
|
@@ -352,6 +464,7 @@ export async function navigateToUrl(
|
|
|
352
464
|
if (titleVisible) {
|
|
353
465
|
setTitleTransitionNames(titleEl, true);
|
|
354
466
|
}
|
|
467
|
+
setPageBottomTransitionName(window);
|
|
355
468
|
|
|
356
469
|
document.documentElement.classList.add(
|
|
357
470
|
direction === 'forward' ? 'nav-forward' : 'nav-back',
|
|
@@ -372,6 +485,7 @@ export async function navigateToUrl(
|
|
|
372
485
|
if (newTitleVisible && newTitleEl) {
|
|
373
486
|
setTitleTransitionNames(newTitleEl, true);
|
|
374
487
|
}
|
|
488
|
+
setPageBottomTransitionName(window);
|
|
375
489
|
});
|
|
376
490
|
|
|
377
491
|
await transition.finished;
|
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/_theme.scss
CHANGED
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
'Segoe UI Symbol';
|
|
55
55
|
--sans-mono-font: 'Google Sans Code', 'Consolas', 'Courier', monospace;
|
|
56
56
|
--serif-font: <%= serifFontStack %>;
|
|
57
|
-
--serif-preview-font: 'Times New Roman', 'Times', serif;
|
|
57
|
+
--serif-preview-font: 'Georgia', 'Times New Roman', 'Times', serif;
|
|
58
58
|
--serif-mono-font: <%= serifMonoFontStack %>;
|
|
59
59
|
--sans-font-feature-settings:
|
|
60
60
|
'liga', 'calt', 'ss07', 'ss08', 'cv01', 'cv08', 'cv10';
|
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>
|