1import { Controller } from "@hotwired/stimulus" 2 3// GitHub-style hovercard: hover (or keyboard-focus) a product link and a small 4// preview appears near it. 5// 6// The card is positioned `fixed` and placed from JS rather than absolutely 7// inside the trigger's parent, because the cart's line-item container is 8// `overflow-hidden` â an absolutely positioned card would be clipped at the 9// card boundary. 10// 11// Hover is an enhancement, never the only route to the information: the trigger 12// is a real link to the product page, which is what touch devices get. 13export default class extends Controller { 14 static targets = ["trigger", "card"] 15 static values = { 16 openDelay: { type: Number, default: 300 }, 17 closeDelay: { type: Number, default: 180 }, 18 gap: { type: Number, default: 8 } 19 } 20 21 disconnect() { 22 clearTimeout(this.openTimer) 23 clearTimeout(this.closeTimer) 24 } 25 26 show() { 27 clearTimeout(this.closeTimer) 28 if (this.visible) return 29 this.openTimer = setTimeout(() => this.reveal(), this.openDelayValue) 30 } 31 32 scheduleHide() { 33 clearTimeout(this.openTimer) 34 this.closeTimer = setTimeout(() => this.hide(), this.closeDelayValue) 35 } 36 37 cancelHide() { 38 clearTimeout(this.closeTimer) 39 } 40 41 hide() { 42 clearTimeout(this.openTimer) 43 this.visible = false 44 this.cardTarget.classList.add("hidden", "opacity-0") 45 } 46 47 reveal() { 48 this.visible = true 49 // Unhide before measuring â a display:none element has no dimensions. 50 this.cardTarget.classList.remove("hidden") 51 this.position() 52 // Force a reflow rather than waiting on requestAnimationFrame: rAF is 53 // throttled (or never fires) in a background tab, which would leave the 54 // card present but permanently at opacity-0. Reading offsetWidth commits 55 // the un-hidden state so the transition still runs. Same trick as 56 // cart_bounce_controller. 57 void this.cardTarget.offsetWidth 58 this.cardTarget.classList.remove("opacity-0") 59 } 60 61 position() { 62 const anchor = this.triggerTarget.getBoundingClientRect() 63 const card = this.cardTarget.getBoundingClientRect() 64 const gap = this.gapValue 65 const margin = 8 66 67 // Below the trigger by default; flip above when the viewport bottom is 68 // closer than the card is tall. 69 const below = anchor.bottom + gap 70 const fitsBelow = below + card.height <= window.innerHeight - margin 71 const top = fitsBelow ? below : Math.max(margin, anchor.top - gap - card.height) 72 73 // Left-align to the trigger, then clamp so it never hangs off either edge. 74 const maxLeft = window.innerWidth - card.width - margin 75 const left = Math.min(Math.max(margin, anchor.left), Math.max(margin, maxLeft)) 76 77 this.cardTarget.style.top = `${Math.round(top)}px` 78 this.cardTarget.style.left = `${Math.round(left)}px` 79 } 80}
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.