@zachleat/table-of-contents 1.0.0 → 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 +9 -1
- package/package.json +2 -2
- package/table-of-contents.js +69 -13
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,10 @@ 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
|
+
|
|
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`.
|
|
28
|
+
|
|
25
29
|
Add `?nodefine` to skip auto-registration and define it yourself:
|
|
26
30
|
|
|
27
31
|
```js
|
|
@@ -39,3 +43,7 @@ TableOfContents.define("my-toc");
|
|
|
39
43
|
## Styling
|
|
40
44
|
|
|
41
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() {
|
|
@@ -90,24 +108,62 @@ export class TableOfContents extends HTMLElement {
|
|
|
90
108
|
let selector = this.getAttribute("selector") || TableOfContents.defaults.selector;
|
|
91
109
|
let headings = Array.from(document.querySelectorAll(selector)).filter(h => !this.contains(h));
|
|
92
110
|
|
|
93
|
-
// Existing child content is kept before the list.
|
|
94
|
-
this.list?.remove();
|
|
95
|
-
this.list = undefined;
|
|
96
111
|
this.observer?.disconnect();
|
|
112
|
+
this.resizeObserver?.disconnect();
|
|
113
|
+
this.generatedList?.remove();
|
|
114
|
+
this.generatedList = undefined;
|
|
97
115
|
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
this.
|
|
103
|
-
if(
|
|
104
|
-
this.hidden = true;
|
|
116
|
+
// A child list (e.g. server-rendered) is used as-is.
|
|
117
|
+
this.list = this.querySelector(":scope > :is(ol, ul)");
|
|
118
|
+
|
|
119
|
+
let empty = !this.list && headings.length === 0;
|
|
120
|
+
this.setAutoHidden(empty);
|
|
121
|
+
if(empty) {
|
|
105
122
|
return;
|
|
106
123
|
}
|
|
107
124
|
|
|
108
|
-
|
|
109
|
-
|
|
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);
|
|
138
|
+
this.append(this.list);
|
|
139
|
+
}
|
|
140
|
+
|
|
110
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
|
+
}
|
|
111
167
|
}
|
|
112
168
|
|
|
113
169
|
// A heading is active while it or any content before the next heading is visible.
|