PageSourceSearch

https://survivalbrasil.org/assets/controllers/numiko/newsletter_signup_modal_controller-69ef2eb8.js

js survivalbrasil.org collected 2026-09-25 18:53:35 UTC 6,873 bytes, 206 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2import supportsInteractionObserver from "utils/supports_interaction_observer"
3
4// Connects to data-controller="numiko--newsletter-signup-modal"
5export default class extends Controller {
6  static targets = ["nameFields"]
7
8  connect () {
9    if (!supportsInteractionObserver()) { return; }
10
11    const configDiv = document.getElementById('newsletter-signup-config')
12    if (configDiv && configDiv.dataset.disableModal === 'true') {
13      this.element.remove()
14      return
15    }
16
17    // Mark this modal with its original order before moving to body
18    // so we can keep the first one when removing duplicates
19    if (!this.element.dataset.modalOrder) {
20      const allModals = document.querySelectorAll('#scroll-signup-modal')
21      this.element.dataset.modalOrder = Array.from(allModals).indexOf(this.element)
22    }
23
24    const modal = this.element;
25    if (modal && modal.parentNode !== document.body) document.body.appendChild(modal);
26    this.formSubmitted = false
27    this.#setupObserver()
28    this.#setupModalEvents()
29  }
30
31  disconnect () {
32    if (this.observer) {
33      this.observer.disconnect()
34    }
35    if (this.mutationObserver) {
36      this.mutationObserver.disconnect()
37    }
38    if (this.handleScroll) {
39      window.removeEventListener('scroll', this.handleScroll)
40    }
41  }
42
43  #setupObserver () {
44    this.modalShown = false
45
46    // Check if modal is disabled via config div to allow disabling of individual pages via the CMS
47    // <div id='newsletter-signup-config' data-disable-modal='true'>&nbsp;</div>
48    const configDiv = document.getElementById('newsletter-signup-config')
49    if (configDiv && configDiv.dataset.disableModal === 'true') return
50
51    if (this.getConfigValue('newsletter_modal_completed')) return
52
53    // Don't show the modal if it's already been closed without submission less than 5 minutes ago.
54    const dismissedTime = this.getConfigValue('newsletter_modal_dismissed')
55    if (dismissedTime && Date.now() - dismissedTime < 5 * 60 * 1000) return
56
57    // Ensure only one modal is left on the page if there are multiple
58    // Use a small delay to ensure all controllers have connected and marked their order
59    setTimeout(() => this.removeDuplicateModals(), 100)
60
61    // Scroll-based trigger setup (30% of page height)
62    this.handleScroll = () => {
63      if (this.modalShown) return
64
65      const scrollTop = document.documentElement.scrollTop
66      const documentHeight = document.documentElement.scrollHeight - window.innerHeight
67      const scrollPercentage = (scrollTop / documentHeight) * 100
68
69      if (scrollPercentage >= 30) {
70        const modal = new bootstrap.Modal(this.element)
71        modal.show()
72        this.modalShown = true
73
74        // Remove scroll listener once modal is shown
75        window.removeEventListener('scroll', this.handleScroll)
76      }
77    }
78
79    window.addEventListener('scroll', this.handleScroll)
80
81    // TODO: turbo:frame-load event doesn't seem to be firing so using MutationObserver instead
82    // document.addEventListener('turbo:frame-load', this.closeModal.bind(this))
83    this.frame = document.getElementById('signup-modal-frame')
84    if (this.frame) {
85      this.mutationObserver = new MutationObserver(() => {
86        // Only close the modal and set the cookie if the frame's been updated with the thanks message
87        if (document.getElementById('signup-completed')) {
88          this.formSubmitted = true
89          this.setNewsletterCookie()
90          this.fireGAEvent()
91        }
92      })
93      this.mutationObserver.observe(this.frame, { childList: true, subtree: true })
94    }
95  }
96
97  #setupModalEvents () {
98    // Listen for modal hide events to detect non-submission closure
99    this.element.addEventListener('hidden.bs.modal', () => {
100      if (!this.formSubmitted) {
101        this.setDismissalCookie()
102      }
103    })
104  }
105
106  closeModal (event) {
107    if (event.target.id === 'newsletter-signup-modal-frame') {
108      const modal = bootstrap.Modal.getInstance(this.element)
109      if (modal) modal.hide()
110    }
111  }
112
113  revealNameFields (event) {
114    if (this.validateEmail(event.target.value)) {
115      this.nameFieldsTarget.classList.add('visible')
116    } else {
117      this.nameFieldsTarget.classList.remove('visible')
118    }
119  }
120
121  fireGAEvent () {
122    window.dataLayer = window.dataLayer || []
123    window.dataLayer.push ({
124      event: 'newsletter_modal_signup_completed'
125    })
126  }
127
128  setNewsletterCookie () {
129    this.setConfigValue('newsletter_modal_completed', true)
130  }
131
132  setDismissalCookie () {
133    this.setConfigValue('newsletter_modal_dismissed', Date.now())
134  }
135
136  getCookie (name) {
137    return document.cookie.split('; ').find(row => row.startsWith(name + '=')) !== undefined
138  }
139
140  getCookieValue (name) {
141    const cookie = document.cookie.split('; ').find(row => row.startsWith(name + '='))
142    return cookie ? cookie.split('=')[1] : null
143  }
144
145  // Config-based cookie methods for storing multiple settings in JSON format
146  // TOOD: Extract into a generic utility if needed elsewhere as the config cookie
147  // has already been whitelisted in the cookie-banner so new values can just be added to this
148  getConfigValue (key) {
149    try {
150      const configCookie = this.getCookieValue('config')
151      if (!configCookie) return false
152      const config = JSON.parse(decodeURIComponent(configCookie))
153      return config[key] || false
154    } catch (e) {
155      return false
156    }
157  }
158
159  setConfigValue (key, value) {
160    try {
161      let config = {}
162      const existingConfig = this.getCookieValue('config')
163      if (existingConfig) {
164        config = JSON.parse(decodeURIComponent(existingConfig))
165      }
166
167      config[key] = value
168
169      const three_months = 60 * 60 * 24 * 90
170      const encodedConfig = encodeURIComponent(JSON.stringify(config))
171      document.cookie = `config=${encodedConfig}; path=/; max-age=${three_months}`
172    } catch (e) {
173      console.warn('Failed to set config value:', e)
174    }
175  }
176
177  removeDuplicateModals () {
178    const allModals = document.querySelectorAll('#scroll-signup-modal')
179    const allTriggers = document.querySelectorAll('#signup-modal')
180
181    // Keep the modal with the lowest order (the original first one)
182    if (allModals.length > 1) {
183      const modalArray = Array.from(allModals)
184      const sortedModals = modalArray.sort((a, b) => {
185        const orderA = parseInt(a.dataset.modalOrder || '0')
186        const orderB = parseInt(b.dataset.modalOrder || '0')
187        return orderA - orderB
188      })
189
190      // Remove all except the first (lowest order)
191      for (let i = 1; i < sortedModals.length; i++) {
192        sortedModals[i].remove()
193      }
194    }
195
196    if (allTriggers.length > 1) {
197      for (let i = 1; i < allTriggers.length; i++) {
198        allTriggers[i].remove()
199      }
200    }
201  }
202
203  validateEmail (email) {
204    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
205  }
206}

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.