PageSourceSearch

https://survivalbrasil.org/assets/controllers/numiko/link_preview_card_controller-51f4aab6.js

js survivalbrasil.org collected 2026-09-25 18:53:28 UTC 3,582 bytes, 116 lines download raw bytes

1// app/javascript/controllers/preview_controller.js
2import { Controller } from "@hotwired/stimulus"
3
4// Connects to data-controller="numiko--link-preview-card"
5
6/*
7  The anchor element requires the following attributes:
8  class="preview-cardable"
9  data-controller="numiko--link-preview-card"
10  data-action="mouseenter->numiko--link-preview-card#showPreview mouseleave->numiko--link-preview-card#hidePreview  mousemove->numiko--link-preview-card#trackCursor"
11*/
12export default class extends Controller {
13  static values = { previewDelay: { type: Number, default: 300 } }
14
15  initialize() {
16    this.hoverTimeout = null
17    this.cache = new Map()
18    this.previewCard = null
19    this.cursorX = 0
20    this.cursorY = 0
21    this.trackCursorBound = this.trackCursor.bind(this)
22  }
23
24  showPreview() {
25    if (window.innerWidth < 768) { return } // Don't show preview on mobile
26    this.hoverTimeout = setTimeout(() => {
27      const href = this.element.href
28      
29      if (this.cache.has(href)) {
30        this.#renderPreview(this.cache.get(href))
31        this.#setInitialCursorPosition()
32        return
33      }
34
35      fetch(href, {
36        headers: { 'Turbo-Frame': 'preview-frame' }
37      })
38        .then(r => r.text())
39        .then(html => {
40          this.cache.set(href, html)
41          this.#renderPreview(html)
42          this.#setInitialCursorPosition()
43        })
44        .catch(err => console.error('Preview failed:', err))
45    }, this.previewDelayValue)
46  }
47
48  trackCursor(event) {
49    this.cursorX = event.clientX
50    this.cursorY = event.clientY
51    
52    // Update position if card is visible
53    if (this.previewCard) { this.#updateCardPosition() }
54  }
55
56  #updateCardPosition() {
57    this.previewCard.style.left = (this.cursorX + 20) + 'px'
58    this.previewCard.style.top = this.cursorY + 'px'
59    this.previewCard.style.transform = 'translateY(-70%)'
60    this.#adjustForViewport()
61  }
62
63  #adjustForViewport() {
64    const cardRect = this.previewCard.getBoundingClientRect()
65    if (cardRect.right > window.innerWidth) {
66      this.previewCard.style.left = (window.innerWidth - cardRect.width - 20) + 'px'
67    }
68    if (cardRect.top < 0) {
69      this.previewCard.style.top = '20px'
70      this.previewCard.style.transform = 'translateY(0)'
71    } else if (cardRect.bottom > window.innerHeight) {
72      this.previewCard.style.top = (window.innerHeight - cardRect.height - 20) + 'px'
73      this.previewCard.style.transform = 'translateY(0)'
74    }
75  }
76
77  hidePreview() {
78    clearTimeout(this.hoverTimeout)
79    document.removeEventListener('mousemove', this.trackCursorBound)
80    this.previewCard?.remove()
81    this.previewCard = null
82  }
83
84  #renderPreview(html) {
85    const card = document.createElement('div')
86    card.innerHTML = html
87    card.style.position = 'fixed'
88    card.style.zIndex = 9999
89    card.style.pointerEvents = 'none' // Don't interfere with hover
90
91    // Ensure the fetched content contains the expected card element
92    if (!card.firstElementChild.classList.contains('link-preview-card')) { return; }
93    
94    document.body.appendChild(card)
95    this.previewCard = card
96    
97    this.#updateCardPosition()
98    document.addEventListener('mousemove', this.trackCursorBound)
99  }
100
101  disconnect() {
102    this.hidePreview()
103    this.#removeAllPreviewCards()
104  }
105
106  #setInitialCursorPosition() {
107    const rect = this.element.getBoundingClientRect();
108    this.cursorX = rect.left;
109    this.cursorY = rect.top;
110    this.#updateCardPosition();
111  }
112
113  #removeAllPreviewCards() {
114    document.querySelectorAll('.link-preview-card').forEach(card => card.remove())
115  }
116}

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.