1import { Controller } from "@hotwired/stimulus"; 2 3// Quick-jump tab nav for the marketing /pricing page. 4// 5// All panels stay visible at all times. Tabs act as a scroll-to-section 6// nav (Stripe-style sub-nav). As the user scrolls, the tab corresponding 7// to the most-visible panel becomes active. 8// 9// Markup contract: 10// data-controller="marketing--pricing-tabs" 11// 12// tab buttons: data-marketing--pricing-tabs-target="tab" 13// data-product="<product-key>" 14// data-action="click->marketing--pricing-tabs#select" 15// panels: data-marketing--pricing-tabs-target="panel" 16// data-product="<product-key>" 17// id="<product-key>" (so #hash deep-links work) 18export default class extends Controller { 19 static targets = ["tab", "panel"]; 20 static values = { 21 navOffset: { type: Number, default: 90 }, 22 }; 23 24 connect() { 25 this.#setupIntersectionObserver(); 26 27 const hashKey = window.location.hash.slice(1); 28 const hashPanel = this.panelTargets.find((p) => p.dataset.product === hashKey); 29 30 if (hashPanel) { 31 // Defer one frame so the layout has settled before measuring. 32 requestAnimationFrame(() => this.#scrollToPanel(hashPanel, false)); 33 } else if (this.tabTargets.length > 0) { 34 this.#setActive(this.tabTargets[0].dataset.product); 35 } 36 } 37 38 disconnect() { 39 if (this.observer) this.observer.disconnect(); 40 } 41 42 select(event) { 43 event.preventDefault(); 44 const product = event.currentTarget.dataset.product; 45 if (!product) return; 46 const panel = this.panelTargets.find((p) => p.dataset.product === product); 47 if (!panel) return; 48 this.#scrollToPanel(panel, true); 49 history.replaceState(null, "", `#${product}`); 50 } 51 52 #setupIntersectionObserver() { 53 // Pick the panel whose top is closest to (but past) the offset line. 54 this.observer = new IntersectionObserver( 55 (entries) => { 56 entries.forEach((entry) => { 57 if (entry.isIntersecting) { 58 this.#setActive(entry.target.dataset.product); 59 } 60 }); 61 }, 62 { 63 // Activate as the panel crosses the line just under the sticky nav, 64 // and deactivate before it fully leaves through the bottom. 65 rootMargin: `-${this.navOffsetValue + 20}px 0px -55% 0px`, 66 threshold: 0, 67 }, 68 ); 69 this.panelTargets.forEach((p) => this.observer.observe(p)); 70 } 71 72 #setActive(product) { 73 if (!product) return; 74 this.tabTargets.forEach((t) => { 75 const on = t.dataset.product === product; 76 t.classList.toggle("on", on); 77 t.setAttribute("aria-selected", on ? "true" : "false"); 78 t.setAttribute("tabindex", on ? "0" : "-1"); 79 }); 80 } 81 82 #scrollToPanel(panel, smooth) { 83 const top = panel.getBoundingClientRect().top + window.scrollY - this.navOffsetValue; 84 window.scrollTo({ top, behavior: smooth ? "smooth" : "auto" }); 85 } 86};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.