@zachleat/table-of-contents 2.0.0 → 2.1.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
@@ -39,6 +39,7 @@ TableOfContents.define("my-toc");
39
39
  ## Attributes
40
40
 
41
41
  - `selector`: headings to include (default `main :is(h2, h3, h4, h5, h6)`). Headings without an `id` are listed without a link.
42
+ - `best-match`: when multiple sections are in view, highlight only the one taking up the most vertical space in the viewport.
42
43
 
43
44
  ## Styling
44
45
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zachleat/table-of-contents",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
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",
@@ -172,19 +172,36 @@ export class TableOfContents extends HTMLElement {
172
172
 
173
173
  let headingSet = new Set(headings);
174
174
  let targets = new Map();
175
- let visible = new Set();
175
+ let visible = new Map();
176
+ let bestMatch = this.hasAttribute("best-match");
176
177
 
177
178
  this.observer = new IntersectionObserver(entries => {
178
179
  for(let entry of entries) {
179
- visible[entry.isIntersecting ? "add" : "delete"](entry.target);
180
+ if(entry.isIntersecting) {
181
+ visible.set(entry.target, entry.intersectionRect.height);
182
+ } else {
183
+ visible.delete(entry.target);
184
+ }
185
+ }
186
+
187
+ let heights = new Map();
188
+ for(let [el, height] of visible) {
189
+ let id = targets.get(el);
190
+ heights.set(id, (heights.get(id) || 0) + height);
191
+ }
192
+
193
+ let activeIds = new Set(heights.keys());
194
+ if(bestMatch && activeIds.size > 1) {
195
+ // Ties go to the earlier heading.
196
+ let best = headings.map(h => h.id).filter(id => heights.has(id)).reduce((a, b) => heights.get(b) > heights.get(a) ? b : a);
197
+ activeIds = new Set([best]);
180
198
  }
181
199
 
182
- let activeIds = new Set(Array.from(visible, el => targets.get(el)));
183
200
  for(let link of this.list.querySelectorAll("a[href^='#']")) {
184
201
  let id = link.getAttribute("href").slice(1);
185
202
  link.classList.toggle(TableOfContents.defaults.activeClass, activeIds.has(id));
186
203
  }
187
- });
204
+ }, bestMatch ? { threshold: Array.from({ length: 21 }, (_, i) => i / 20) } : undefined);
188
205
 
189
206
  for(let heading of headings) {
190
207
  if(!heading.id) {