@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 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 the sections currently in view.
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": "1.0.0",
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": {
@@ -26,7 +26,12 @@ export class TableOfContents extends HTMLElement {
26
26
  return text.replace(/\s+/g, " ");
27
27
  }
28
28
 
29
- static generate(headings) {
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
- // Hide when empty, without overriding an author-set `hidden`.
99
- if(this.hiddenWhenEmpty) {
100
- this.hidden = false;
101
- }
102
- this.hiddenWhenEmpty = headings.length === 0 && !this.hidden;
103
- if(headings.length === 0) {
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
- this.list = TableOfContents.generate(headings);
109
- this.append(this.list);
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.