1import {Controller} from '@hotwired/stimulus'; 2 3/** 4 * Floating toast: anchors the toast to the bottom of the viewport (or, when a 5 * footer is present, to 60px above the footer's top edge as it scrolls into 6 * view) and auto-dismisses after `duration` milliseconds. 7 */ 8export default class extends Controller { 9 static baseOffsetPx = 60; 10 11 static values = { 12 duration: Number, 13 }; 14 15 connect() { 16 this.footer = document.querySelector('footer'); 17 this.reposition(); 18 19 if (this.footer) { 20 window.addEventListener('scroll', this.scheduleReposition, {passive: true}); 21 window.addEventListener('resize', this.scheduleReposition, {passive: true}); 22 } 23 24 if (this.durationValue > 0) { 25 this.dismissTimeout = setTimeout(() => this.dismiss(), this.durationValue); 26 } 27 } 28 29 disconnect() { 30 clearTimeout(this.dismissTimeout); 31 cancelAnimationFrame(this.frame); 32 window.removeEventListener('scroll', this.scheduleReposition); 33 window.removeEventListener('resize', this.scheduleReposition); 34 } 35 36 scheduleReposition = () => { 37 cancelAnimationFrame(this.frame); 38 this.frame = requestAnimationFrame(() => this.reposition()); 39 }; 40 41 reposition() { 42 const base = this.constructor.baseOffsetPx; 43 let bottom = base; 44 45 if (this.footer) { 46 const footerTop = this.footer.getBoundingClientRect().top; 47 bottom = Math.max(base, window.innerHeight - footerTop + base); 48 } 49 50 this.element.style.bottom = `${bottom}px`; 51 } 52 53 dismiss() { 54 this.element.classList.add('is-leaving'); 55 56 const transitionMs = parseFloat(getComputedStyle(this.element).transitionDuration) || 0; 57 if (transitionMs > 0) { 58 this.element.addEventListener('transitionend', () => this.element.remove(), {once: true}); 59 } else { 60 this.element.remove(); 61 } 62 } 63}
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.