1import { Controller } from '@hotwired/stimulus' 2 3// Opens once for a visitor who has just clicked through from the property agent 4// promotion. The promotion sets the key below immediately before navigating; this 5// controller spends it on arrival, so the modal cannot reappear later in the session. 6// Counterpart: app/components/assistant/property_agent_promotion_component/controller.js 7const ARRIVED_KEY = 'jitty.assistantPromotion.welcomePending' 8const DISPLAY_DELAY = 500 9const CLOSE_TRANSITION_DURATION = 150 10 11export default class extends Controller { 12 static targets = ['dialog'] 13 14 connect() { 15 this.seenFired = false 16 this.dismissedFired = false 17 this.resetRestoredDialog() 18 19 // Turbo connects controllers on the cached preview shown before the real page. 20 if (document.documentElement.hasAttribute('data-turbo-preview')) return 21 if (!this.spendArrival()) return 22 23 this.timerId = window.setTimeout(() => this.open(), DISPLAY_DELAY) 24 } 25 26 disconnect() { 27 if (this.timerId) window.clearTimeout(this.timerId) 28 this.timerId = null 29 document.body.classList.remove('overflow-hidden') 30 if (this.dialogTarget.open) this.dialogTarget.close() 31 } 32 33 // Returns true only for the arrival that owns the key, and removes it so a later 34 // visit in the same session does not open the modal again. 35 spendArrival() { 36 try { 37 const storage = window.sessionStorage 38 if (storage.getItem(ARRIVED_KEY) !== '1') return false 39 40 storage.removeItem(ARRIVED_KEY) 41 return true 42 } catch { 43 return false 44 } 45 } 46 47 open() { 48 if (this.dialogTarget.open) return 49 50 this.previouslyFocusedElement = document.activeElement 51 this.dialogTarget.showModal() 52 document.body.classList.add('overflow-hidden') 53 54 requestAnimationFrame(() => { 55 this.dialogTarget.classList.remove('opacity-0') 56 this.dialogTarget.classList.add('opacity-100') 57 58 if (!this.seenFired) { 59 this.seenFired = true 60 this.emitAnalytics('assistant.welcome.seen') 61 } 62 }) 63 } 64 65 dismiss(event) { 66 this.dismissWithMethod(event.params.method) 67 } 68 69 backdropClicked(event) { 70 if (event.target === this.dialogTarget) this.dismissWithMethod('backdrop') 71 } 72 73 escapePressed(event) { 74 event.preventDefault() 75 this.dismissWithMethod('escape') 76 } 77 78 dismissWithMethod(method) { 79 if (!this.dialogTarget.open || this.dismissedFired) return 80 81 this.dismissedFired = true 82 this.emitAnalytics('assistant.welcome.dismissed', { dismissal_method: method }) 83 this.close() 84 } 85 86 close() { 87 this.dialogTarget.classList.remove('opacity-100') 88 this.dialogTarget.classList.add('opacity-0') 89 90 const finish = () => { 91 if (this.dialogTarget.open) this.dialogTarget.close() 92 document.body.classList.remove('overflow-hidden') 93 if (this.previouslyFocusedElement?.isConnected) this.previouslyFocusedElement.focus() 94 } 95 96 if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { 97 finish() 98 } else { 99 window.setTimeout(finish, CLOSE_TRANSITION_DURATION) 100 } 101 } 102 103 // Turbo caches the page before Stimulus disconnects, so a snapshot taken while the 104 // dialog was open restores it open but no longer modal, on a scroll locked page. 105 resetRestoredDialog() { 106 if (this.dialogTarget.open) this.dialogTarget.close() 107 this.dialogTarget.classList.add('opacity-0') 108 this.dialogTarget.classList.remove('opacity-100') 109 document.body.classList.remove('overflow-hidden') 110 } 111 112 emitAnalytics(name, detail = {}) { 113 this.dispatch('analytics', { 114 prefix: 'assistant-agent-welcome', 115 detail: { name, ...detail } 116 }) 117 } 118}
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.