1import { Controller } from "@hotwired/stimulus" 2 3export default class extends Controller { 4 static targets = ["scroller", "bottomHint"] 5 6 connect() { 7 this.onScroll = this.onScroll.bind(this) 8 this.onResize = this.onResize.bind(this) 9 this.syncLayout = this.syncLayout.bind(this) 10 11 if (!this.hasScrollerTarget) return 12 13 this.scrollerTarget.addEventListener("scroll", this.onScroll, { passive: true }) 14 window.addEventListener("resize", this.onResize) 15 16 if ("ResizeObserver" in window) { 17 this.resizeObserver = new ResizeObserver(this.syncLayout) 18 this.resizeObserver.observe(this.scrollerTarget) 19 } 20 21 this.syncLayout() 22 requestAnimationFrame(() => this.syncLayout()) 23 this.settleTimeout = setTimeout(() => this.syncLayout(), 80) 24 } 25 26 disconnect() { 27 if (this.hasScrollerTarget) { 28 this.scrollerTarget.removeEventListener("scroll", this.onScroll) 29 this.scrollerTarget.classList.remove("is-scrolling") 30 } 31 32 window.removeEventListener("resize", this.onResize) 33 this.resizeObserver?.disconnect() 34 clearTimeout(this.settleTimeout) 35 clearTimeout(this.scrollIdleTimeout) 36 } 37 38 onScroll() { 39 this.syncHints() 40 this.flagScrolling() 41 } 42 43 flagScrolling() { 44 if (!this.hasScrollerTarget) return 45 46 this.scrollerTarget.classList.add("is-scrolling") 47 clearTimeout(this.scrollIdleTimeout) 48 this.scrollIdleTimeout = setTimeout(() => { 49 this.scrollerTarget.classList.remove("is-scrolling") 50 }, 800) 51 } 52 53 onResize() { 54 this.syncLayout() 55 } 56 57 syncLayout() { 58 this.syncHints() 59 this.revealActiveItem() 60 } 61 62 revealActiveItem() { 63 if (!this.hasScrollerTarget || this.scrollerTarget.clientHeight === 0) return 64 65 const activeItem = this.scrollerTarget.querySelector("[aria-current='page']") 66 if (!activeItem) return 67 68 const containerRect = this.scrollerTarget.getBoundingClientRect() 69 const itemRect = activeItem.getBoundingClientRect() 70 const margin = 20 71 72 const aboveFold = itemRect.top < containerRect.top + margin 73 const belowFold = itemRect.bottom > containerRect.bottom - margin 74 75 if (aboveFold || belowFold) { 76 const currentScrollTop = this.scrollerTarget.scrollTop 77 const itemTopWithinScroller = currentScrollTop + (itemRect.top - containerRect.top) 78 const targetTop = Math.max( 79 0, 80 itemTopWithinScroller - (this.scrollerTarget.clientHeight * 0.35) 81 ) 82 83 this.scrollerTarget.scrollTop = targetTop 84 this.syncHints() 85 } 86 } 87 88 syncHints() { 89 if (!this.hasScrollerTarget || this.scrollerTarget.clientHeight === 0) return 90 91 const { scrollTop, scrollHeight, clientHeight } = this.scrollerTarget 92 const overflow = scrollHeight - clientHeight > 12 93 const atBottom = scrollTop + clientHeight >= scrollHeight - 6 94 95 if (this.hasBottomHintTarget) { 96 this.bottomHintTarget.classList.toggle("hidden", !overflow || atBottom) 97 } 98 } 99}
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.