@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 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,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": "1.0.1",
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() {
@@ -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
- if(this.hiddenWhenEmpty) {
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
- // Existing child content is kept before the generated list.
112
- if(!this.list) {
113
- this.list = this.generatedList = TableOfContents.generate(headings);
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.