1import { Controller } from "@hotwired/stimulus" 2 3const MAX_REQUIRED_VISIBLE_HEIGHT = 96 4 5export default class extends Controller { 6 static values = { 7 viewedUrl: String, 8 kind: String, 9 version: String, 10 viewed: { type: Boolean, default: false }, 11 } 12 13 connect() { 14 this.markingViewed = false 15 this.retryTimer = null 16 this.visibilityChanged = () => this.acknowledgeIfVisible() 17 document.addEventListener("visibilitychange", this.visibilityChanged) 18 19 if (this.viewedValue) return 20 this.responseVisible = this.responseIsInViewport() 21 22 if (!("IntersectionObserver" in window)) { 23 this.acknowledgeIfVisible() 24 return 25 } 26 27 this.observer = new IntersectionObserver((entries) => { 28 this.responseVisible = entries.some((entry) => this.responseEntryIsVisible(entry)) 29 this.acknowledgeIfVisible() 30 }, { threshold: this.requiredVisibleRatio() }) 31 this.observer.observe(this.element) 32 this.acknowledgeIfVisible() 33 } 34 35 disconnect() { 36 document.removeEventListener("visibilitychange", this.visibilityChanged) 37 this.observer?.disconnect() 38 this.clearRetryTimer() 39 } 40 41 acknowledgeIfVisible() { 42 if (this.viewedValue || this.markingViewed || document.hidden || !this.responseVisible) return 43 if (!this.hasViewedUrlValue || !this.hasKindValue || !this.hasVersionValue) return 44 45 const csrfToken = document.querySelector("meta[name='csrf-token']")?.content 46 const headers = { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" } 47 if (csrfToken) headers["X-CSRF-Token"] = csrfToken 48 49 this.clearRetryTimer() 50 this.markingViewed = true 51 fetch(this.viewedUrlValue, { 52 method: "PATCH", 53 credentials: "same-origin", 54 headers, 55 body: new URLSearchParams({ kind: this.kindValue, version: this.versionValue }), 56 }).then((response) => { 57 if (!response.ok) throw new Error("Website request response acknowledgement failed") 58 59 this.viewedValue = true 60 }).catch(() => { 61 this.retryTimer = window.setTimeout(() => this.acknowledgeIfVisible(), 15000) 62 }).finally(() => { 63 this.markingViewed = false 64 }) 65 } 66 67 responseIsInViewport() { 68 const bounds = this.element.getBoundingClientRect() 69 if (bounds.width <= 0 || bounds.height <= 0) return false 70 71 const visibleHeight = Math.min(bounds.bottom, window.innerHeight) - Math.max(bounds.top, 0) 72 const visibleWidth = Math.min(bounds.right, window.innerWidth) - Math.max(bounds.left, 0) 73 return visibleHeight >= this.requiredVisibleHeight(bounds.height) && visibleWidth > 0 74 } 75 76 responseEntryIsVisible(entry) { 77 return entry.isIntersecting && 78 entry.intersectionRect.height >= this.requiredVisibleHeight(entry.boundingClientRect.height) && 79 entry.intersectionRect.width > 0 80 } 81 82 requiredVisibleRatio() { 83 const height = this.element.getBoundingClientRect().height 84 return height > 0 ? this.requiredVisibleHeight(height) / height : 0.15 85 } 86 87 requiredVisibleHeight(height) { 88 return Math.min(height * 0.15, MAX_REQUIRED_VISIBLE_HEIGHT) 89 } 90 91 clearRetryTimer() { 92 if (!this.retryTimer) return 93 94 window.clearTimeout(this.retryTimer) 95 this.retryTimer = null 96 } 97}
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.