@zachleat/table-of-contents 1.0.1 → 2.0.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/README.md +7 -1
- package/package.json +2 -2
- package/table-of-contents.js +59 -10
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `<table-of-contents>`
|
|
2
2
|
|
|
3
|
-
A zero-dependency web component that client renders a table of contents from the headings on the page and highlights
|
|
3
|
+
A zero-dependency web component that progressively enhances (or optionally client renders) a table of contents from the headings on the page and highlights visible sections. This component includes no styles (all visuals are app-code controlled).
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -22,6 +22,8 @@ npm install @zachleat/table-of-contents
|
|
|
22
22
|
|
|
23
23
|
Child content is kept and the list is added after it. Wrap it in a labeled `<nav>` so assistive technology can find it. The element is hidden when no headings match.
|
|
24
24
|
|
|
25
|
+
Entries for headings hidden by CSS (e.g. `display: none` on an ancestor) get the `hidden` attribute and update when visibility changes. The element is hidden when every heading is hidden.
|
|
26
|
+
|
|
25
27
|
A child `<ol>` or `<ul>` (for example, server-rendered) is used instead of generating one, and its `#` links are highlighted for headings matching `selector`.
|
|
26
28
|
|
|
27
29
|
Add `?nodefine` to skip auto-registration and define it yourself:
|
|
@@ -41,3 +43,7 @@ TableOfContents.define("my-toc");
|
|
|
41
43
|
## Styling
|
|
42
44
|
|
|
43
45
|
No styles are included. Links for sections in view get the `toc-in-view` class.
|
|
46
|
+
|
|
47
|
+
# Changelog
|
|
48
|
+
|
|
49
|
+
- `v2.0.0` tracks display of heading elements (and hides links when display is toggled off)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zachleat/table-of-contents",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "A zero-dependency web component that client renders a table of contents from the headings on the page.",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "A zero-dependency web component that progressively enhances (or optionally client renders) a table of contents from the headings on the page and highlights visible sections.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "table-of-contents.js",
|
|
7
7
|
"exports": {
|
package/table-of-contents.js
CHANGED
|
@@ -26,7 +26,12 @@ export class TableOfContents extends HTMLElement {
|
|
|
26
26
|
return text.replace(/\s+/g, " ");
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
static
|
|
29
|
+
static isVisible(heading) {
|
|
30
|
+
return heading.checkVisibility?.() ?? true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// `items` (optional) receives each generated <li> mapped to its heading.
|
|
34
|
+
static generate(headings, items) {
|
|
30
35
|
let root = document.createElement("ol");
|
|
31
36
|
let stack = [{ level: this.getLevel(headings[0]), list: root }];
|
|
32
37
|
for(let heading of headings) {
|
|
@@ -65,6 +70,7 @@ export class TableOfContents extends HTMLElement {
|
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
top.list.append(top.item);
|
|
73
|
+
items?.set(top.item, heading);
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
return root;
|
|
@@ -80,6 +86,18 @@ export class TableOfContents extends HTMLElement {
|
|
|
80
86
|
|
|
81
87
|
disconnectedCallback() {
|
|
82
88
|
this.observer?.disconnect();
|
|
89
|
+
this.resizeObserver?.disconnect();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Hides the element without overriding an author-set `hidden`.
|
|
93
|
+
setAutoHidden(force) {
|
|
94
|
+
if(this.autoHidden) {
|
|
95
|
+
this.hidden = false;
|
|
96
|
+
}
|
|
97
|
+
this.autoHidden = force && !this.hidden;
|
|
98
|
+
if(force) {
|
|
99
|
+
this.hidden = true;
|
|
100
|
+
}
|
|
83
101
|
}
|
|
84
102
|
|
|
85
103
|
init() {
|
|
@@ -91,30 +109,61 @@ export class TableOfContents extends HTMLElement {
|
|
|
91
109
|
let headings = Array.from(document.querySelectorAll(selector)).filter(h => !this.contains(h));
|
|
92
110
|
|
|
93
111
|
this.observer?.disconnect();
|
|
112
|
+
this.resizeObserver?.disconnect();
|
|
94
113
|
this.generatedList?.remove();
|
|
95
114
|
this.generatedList = undefined;
|
|
96
115
|
|
|
97
116
|
// A child list (e.g. server-rendered) is used as-is.
|
|
98
117
|
this.list = this.querySelector(":scope > :is(ol, ul)");
|
|
99
118
|
|
|
100
|
-
// Hide when empty, without overriding an author-set `hidden`.
|
|
101
119
|
let empty = !this.list && headings.length === 0;
|
|
102
|
-
|
|
103
|
-
this.hidden = false;
|
|
104
|
-
}
|
|
105
|
-
this.hiddenWhenEmpty = empty && !this.hidden;
|
|
120
|
+
this.setAutoHidden(empty);
|
|
106
121
|
if(empty) {
|
|
107
|
-
this.hidden = true;
|
|
108
122
|
return;
|
|
109
123
|
}
|
|
110
124
|
|
|
111
|
-
|
|
112
|
-
if(
|
|
113
|
-
|
|
125
|
+
let items = new Map();
|
|
126
|
+
if(this.list) {
|
|
127
|
+
for(let heading of headings) {
|
|
128
|
+
if(heading.id) {
|
|
129
|
+
for(let link of this.list.querySelectorAll(`a[href="#${CSS.escape(heading.id)}"]`)) {
|
|
130
|
+
items.set(link.closest("li"), heading);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
items.delete(null);
|
|
135
|
+
} else {
|
|
136
|
+
// Existing child content is kept before the generated list.
|
|
137
|
+
this.list = this.generatedList = TableOfContents.generate(headings, items);
|
|
114
138
|
this.append(this.list);
|
|
115
139
|
}
|
|
116
140
|
|
|
117
141
|
this.observe(headings);
|
|
142
|
+
this.observeVisibility(headings, items);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Hides entries for headings hidden by CSS (e.g. `display: none` on an ancestor).
|
|
146
|
+
observeVisibility(headings, items) {
|
|
147
|
+
let update = () => {
|
|
148
|
+
let visible = new Map(headings.map(h => [h, TableOfContents.isVisible(h)]));
|
|
149
|
+
|
|
150
|
+
// Reverse order so nested entries are updated before their parents.
|
|
151
|
+
for(let li of Array.from(this.list.querySelectorAll("li")).reverse()) {
|
|
152
|
+
let heading = items.get(li);
|
|
153
|
+
if(heading) {
|
|
154
|
+
li.hidden = !visible.get(heading) && !li.querySelector("li:not([hidden])");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this.setAutoHidden(headings.length > 0 && !headings.some(h => visible.get(h)));
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
update();
|
|
162
|
+
|
|
163
|
+
this.resizeObserver = new ResizeObserver(update);
|
|
164
|
+
for(let heading of headings) {
|
|
165
|
+
this.resizeObserver.observe(heading);
|
|
166
|
+
}
|
|
118
167
|
}
|
|
119
168
|
|
|
120
169
|
// A heading is active while it or any content before the next heading is visible.
|