-
Notifications
You must be signed in to change notification settings - Fork 1
/
headingoffset-polyfill.js
157 lines (140 loc) · 4.66 KB
/
headingoffset-polyfill.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
// @ts-check
const managedHeadings = new WeakSet();
const ariaLevelObserver = new MutationObserver(function (mutationList) {
for (const mutation of mutationList) {
if (
mutation.target instanceof HTMLHeadingElement
) {
// 'aria-level' was modified
// 'aria-level' was added
if (mutation.target.hasAttribute("aria-level")) {
stopManagingHeading(mutation.target); // (it’ll get re-added if the polyfill made this change)
}
// 'aria-level' was removed
else {
startManagingHeading(mutation.target);
applyHeadingOffset(mutation.target);
}
}
}
});
if (!("headingOffset" in Element.prototype)) {
// Reflect the 'headingoffset' attribute.
Object.defineProperty(Element.prototype, "headingOffset", {
enumerable: true,
get: function () {
const defaultValue = 0;
const value = Number(this.getAttribute("headingoffset"));
return !Number.isNaN(value) && value > 0 ? value : defaultValue;
},
set: function (headingOffset) {
if (
headingOffset === "" ||
headingOffset === null ||
headingOffset === undefined
) {
this.removeAttribute("headingoffset");
return;
}
this.setAttribute("headingoffset", headingOffset);
},
});
new MutationObserver(function (mutationList) {
for (const mutation of mutationList) {
if (mutation.type === "childList") {
for (const addedNode of mutation.addedNodes) {
// If a heading is added, apply its offset.
if (addedNode instanceof HTMLHeadingElement) {
applyHeadingOffset(addedNode);
}
// If a container with a 'headingoffset' attribute is added, apply offsets to its headings.
else if (
addedNode instanceof HTMLElement &&
addedNode.querySelector("[headingOffset], h1, h2, h3, h4, h5, h6")
) {
applyHeadingOffsets(addedNode);
}
}
}
// If a container’s 'headingoffset' attribute changes, reapply offsets.
else if (
mutation.type === "attributes" &&
mutation.target instanceof HTMLElement
) {
applyHeadingOffsets(mutation.target);
}
}
}).observe(document.documentElement, {
attributeFilter: ["headingoffset"],
childList: true,
subtree: true,
});
ariaLevelObserver.observe(document.documentElement, {
attributeFilter: ["aria-level"],
subtree: true,
});
applyHeadingOffsets(document.documentElement);
}
/**
* Applies offsets all headings in a container, where needed.
* @param {Element} container A container element.
*/
function applyHeadingOffsets(container) {
const headings = Array.from(
container.querySelectorAll("h1, h2, h3, h4, h5, h6")
);
for (const heading of headings) {
applyHeadingOffset(heading);
}
}
/** Track headings whose 'aria-level' is set by the polyfill. */
function isManagedHeading(heading) {
return managedHeadings.has(heading);
}
function startManagingHeading(heading) {
managedHeadings.add(heading);
}
function stopManagingHeading(heading) {
managedHeadings.delete(heading);
}
/**
* Applies an offset to a heading, if needed.
* @param {Element} heading A heading element.
*/
function applyHeadingOffset(heading) {
// If 'aria-level' is already set and the polyfill didn’t set it, don’t change it.
if (heading.hasAttribute("aria-level") && !isManagedHeading(heading)) {
return;
}
const level = Number(heading.tagName[1]);
const offset = getHeadingOffset(heading, 9 - level);
const ariaLevel = level + offset;
// If the level wouldn’t change, don’t set 'aria-level', and remove it if it’s redundant.
if (ariaLevel === level) {
heading.removeAttribute("aria-level");
return;
}
heading.setAttribute("aria-level", String(ariaLevel));
ariaLevelObserver.takeRecords();
startManagingHeading(heading);
}
/**
* Determines the number of levels to offset a heading’s level, by summing the 'headingoffset' attributes of all its parents.
* If the offset would be greater than the maximum offset, returns 0.
* @param {Element} heading A heading element.
* @returns {number} Number of levels (0 or above) to offset the heading.
*/
function getHeadingOffset(heading, maxOffset) {
let offset = 0;
let ancestor = heading.parentNode;
while (ancestor && ancestor instanceof HTMLElement) {
const ancestorHeadingOffset = Number(
ancestor.getAttribute("headingoffset")
);
// Don’t allow negative offsets.
if (!Number.isNaN(ancestorHeadingOffset) && ancestorHeadingOffset > 0)
offset += ancestorHeadingOffset;
ancestor = ancestor.parentNode;
}
return offset <= maxOffset ? offset : 0;
}