PageSourceSearch

https://littlebirdelectronics.com.au/assets/controllers/ecommerce/hover_card_controller-08b85624.js

js littlebirdelectronics.com.au collected 2026-09-25 14:10:36 UTC 2,806 bytes, 80 lines download raw bytes

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.