PageSourceSearch

https://www.dataprivacynow.org/wp-content/themes/fight/js/app.js?v=1791165011

js dataprivacynow.org collected 2026-10-05 01:50:21 UTC 18,083 bytes, 608 lines download raw bytes

1
2function formValue(id) {
3  const el = document.getElementById(`form-${id}`)
4  return el ? el.value : null
5}
6
7function getMetaContent(property) {
8  const el = document.querySelector(`meta[property="${property}"]`)
9
10  if (el) {
11    return el.getAttribute('content')
12  }
13  else {
14    return ''
15  }
16}
17
18const app = Vue.createApp({
19  components: window.VUE_COMPONENTS || {},
20
21  data() {
22    return {
23      hasSigned: false,
24      hasCalled: false,
25      hasSubscribed: false,
26      anHasLoaded: false,
27      sendsToCongress: false,
28      sendsToHouse: false,
29      sendsToSenate: false,
30      sendsToBioguideIds: null,
31      congressEmailSubject: null,
32      firstName: null,
33      lastName: null,
34      email: null,
35      street: null,
36      zipCode: null,
37      country: null,
38      comments: null,
39      phone: null,
40      requiresOptIn: null,
41      sendsToRegulations: false,
42      regulationsDoc: null,
43      hideComment: false,
44      sendsToFCC: false,
45      fccDocket: null
46    }
47  },
48
49  watch: {
50    hasSigned(newValue) {
51      document.body.classList.toggle('has-signed', newValue)
52      if (this.street) {
53        document.dispatchEvent(new CustomEvent('address-entered', {
54          detail: {
55            street: this.street,
56            zipCode: this.zipCode,
57          },
58        }))
59      }
60    }
61  },
62
63  computed: {
64    fullName() {
65      return [
66        this.firstName || '',
67        this.lastName || ''
68      ].map(s => s.trim()).join(' ').trim()
69    },
70
71    usesCommentsAPI() {
72      return this.sendsToCongress || this.sendsToRegulations || this.sendsToFCC
73    },
74
75    sharingURL() {
76      const el = document.querySelector('meta[property="og:url"]')
77
78      if (el) {
79        return el.getAttribute('content')
80      }
81
82      return location.href
83    }
84  },
85
86  mounted() {
87    document.addEventListener('can_embed_loaded', this.anEmbedLoaded)
88    document.addEventListener('can_embed_submitted', this.anEmbedSubmitted)
89
90    if (location.search.match(/signed/)) {
91      this.hasSigned = true
92      this.showCallFormModal()
93    }
94    else if (location.search.match(/call=(1|true)/)) {
95      this.showCallFormModal()
96    }
97    else if (location.search.match(/hasCalled/)) {
98      this.hasCalled = true
99    }
100
101    // add search params to any link with this class
102    document.querySelectorAll('a.js-add-search-params').forEach(a => {
103      const sep = a.href.includes('?') ? '&' : '?'
104      a.href += window.location.search.replace('?', sep)
105    })
106
107    // set up airtable modal links
108    document.querySelectorAll('.js-airtable-modal').forEach(el => {
109      // let this work with WP button divs or actual links
110      const a = el.href ? el : el.querySelector('a')
111
112      if (a) {
113        a.setAttribute('data-bs-toggle', 'modal')
114        a.setAttribute('data-bs-target', '#airtable-modal')
115        a.addEventListener('click', event => {
116          const { href, dataset } = a
117          const embedSrc = href.match(/embed/) ? href : href.replace('https://airtable.com/', 'https://airtable.com/embed/')
118          const modalBody = document.querySelector(`${dataset.bsTarget} .modal-body`)
119
120          if (modalBody) {
121            modalBody.innerHTML = `
122              <iframe class="airtable-embed" src="${embedSrc}" frameborder="0" onmousewheel="" width="100%" height="533" style="background: transparent"></iframe>
123            `
124          }
125        })
126      }
127    })
128
129    document.querySelectorAll('.js-share-modal').forEach(el => {
130      // let this work with WP button divs or actual links
131      const a = el.href ? el : el.querySelector('a')
132
133      // just pick the first share modal on the page
134      const shareModal = document.querySelector('.share-modal')
135
136      if (a && shareModal) {
137        a.addEventListener('click', event => {
138          event.preventDefault()
139          this.share(shareModal.id)
140        })
141      }
142    })
143
144    const params = new URLSearchParams(window.location.search);
145    for (const [key, value] of params) {
146
147      const el = document.querySelector(`#${key.replace('-show','')}.d-none`)
148      if (el) {
149        el.classList.remove('d-none')
150      }
151    }
152
153  },
154
155  beforeDestroy() {
156    document.removeEventListener('can_embed_loaded', this.anEmbedLoaded)
157    document.removeEventListener('can_embed_submitted', this.anEmbedSubmitted)
158  },
159
160  methods: {
161    trackEvent(eventName) {
162      if (window.fathom) {
163        window.fathom.trackEvent(eventName)
164      }
165    },
166
167    anEmbedLoaded() {
168      console.log('AN Loaded')
169
170      if (this.anHasLoaded) {
171        return false
172      }
173
174      const form = document.querySelector('#new_signature,#new_delivery')
175
176      if (!form) {
177        console.error('Missing action netowrk form')
178        return false
179      }
180
181      // grab embedded data from DOM
182      const dataElement = form.closest('.js-petition-data')
183
184      if (dataElement) {
185        const { dataset } = dataElement
186
187        this.requiresOptIn = dataset.requiresOptIn
188        this.comments = dataset.defaultComment
189        this.hideComment = !!dataset.hideComment
190
191        if (dataset.sendsToCongress) {
192          this.sendsToCongress = true
193          this.sendsToHouse = !!dataset.sendsToHouse
194          this.sendsToSenate = !!dataset.sendsToSenate
195          this.sendsToBioguideIds = dataset.sendsToBioguideIds
196          this.congressEmailSubject = dataset.congressEmailSubject
197        }
198
199        if (dataset.sendsToRegulations) {
200          this.sendsToRegulations = true
201          this.regulationsDoc = dataset.regulationsDoc
202        }
203
204        if (dataset.sendsToFcc) {
205          this.sendsToFCC = true
206          this.fccDocket = dataset.fccDocket
207        }
208      }
209
210      // make country selector work with our custom CSS
211      // (these won't be here if the user is logged in to Action Network)
212      const countryLink = document.querySelector('.js-international_link')
213      const countryField = document.getElementById('form-country')
214
215      if (countryLink) {
216        countryLink.addEventListener('click', this.showCountrySelector)
217      }
218
219      if (countryField) {
220        countryField.addEventListener('change', this.handleCountryChange)
221
222        // also run this immediately in case a non-US country was pre-selected
223        this.handleCountryChange()
224      }
225
226      // add our privacy policy to the form
227      this.addPrivacyPolicy()
228      this.addSmsPolicies()
229
230      // save form data so we can use it for our after-actions
231      form.addEventListener('submit', event => {
232        this.firstName = formValue('first_name')
233        this.lastName = formValue('last_name')
234        this.email = formValue('email')
235        this.street = formValue('street')
236        this.zipCode = formValue('zip_code')
237        this.country = formValue('country')
238        this.comments = formValue('comments')
239      })
240
241      const commentBox = document.getElementById('form-comments')
242
243      if (commentBox) {
244        // load default comment
245        if (this.comments) {
246          commentBox.value = this.comments
247        }
248
249        // prevent urls in comments from crashing the submission
250        commentBox.addEventListener('change', event => {
251          commentBox.value = commentBox.value.replace(/https?:\/\//g, '')
252          commentBox.value = commentBox.value.replace(/\.(?=\S)/g, '. ')
253        })
254        commentBox.dispatchEvent(new Event('change'));
255
256        // hide comment if asked
257        if (this.hideComment) {
258          commentBox.style.display = 'none'
259        }
260      }
261
262      if (this.requiresOptIn) {
263        document.querySelectorAll('#d_sharing input[type="checkbox"]').forEach(box => {
264          box.checked = false
265        })
266      }
267
268      // Merge ?an_tags= / ?tags= from the page URL into the AN embed tag list,
269      // then mirror the combined tags onto after-action donate links
270      this.mergeUrlTagsIntoPetitionEmbed()
271      this.updateAfterActionDonateLinks()
272
273      this.anHasLoaded = true
274    },
275
276    getTagsFromUrl() {
277      const params = new URLSearchParams(window.location.search)
278      const raw = params.get('an_tags') || params.get('tags') || ''
279      if (!raw) return []
280
281      return raw
282        .split(',')
283        .map(tag => tag.trim())
284        .filter(Boolean)
285    },
286
287    mergeUrlTagsIntoPetitionEmbed() {
288      const tagInput = document.querySelector('#signature_tag_list, input.js-tag_list')
289      if (!tagInput) return
290
291      const urlTags = this.getTagsFromUrl()
292      if (!urlTags.length) return
293
294      let tags = []
295      if (tagInput.value) {
296        try {
297          const existing = JSON.parse(tagInput.value)
298          if (Array.isArray(existing)) {
299            tags = existing.map(tag =>
299 String(tag).trim()).filter(Boolean)
300          }
301        } catch (e) {
302          console.warn('Could not parse Action Network petition tags', e)
303        }
304      }
305
306      urlTags.forEach(tag => {
307        if (!tags.includes(tag)) {
308          tags.push(tag)
309        }
310      })
311
312      tagInput.value = JSON.stringify(tags)
313    },
314
315    updateAfterActionDonateLinks() {
316      const tagInput = document.querySelector('#signature_tag_list, input.js-tag_list')
317      const tags = ['after-action']
318
319      if (tagInput && tagInput.value) {
320        try {
321          const petitionTags = JSON.parse(tagInput.value)
322          if (Array.isArray(petitionTags)) {
323            petitionTags.forEach(tag => {
324              const cleaned = String(tag).trim()
325              if (cleaned && !tags.includes(cleaned)) {
326                tags.push(cleaned)
327              }
328            })
329          }
330        } catch (e) {
331          console.warn('Could not parse Action Network petition tags', e)
332        }
333      }
334
335      const anTags = tags
336        .map(tag => encodeURIComponent(tag).replace(/%20/g, '+'))
337        .join(',')
338
339      document.querySelectorAll('a.js-after-action-donate').forEach(a => {
340        // Keep commas literal (matches existing ?an_tags=a,b links); only encode within tags
341        a.href = a.href.replace(/([?&])an_tags=[^&]*/, `$1an_tags=${anTags}`)
342      })
343    },
344
345    anEmbedSubmitted() {
346      // if there's an airtable embed, prefill the name and email fields
347      if (this.$refs.airtableEmbed) {
348        this.$refs.airtableEmbed.src = `https://airtable.com/embed/${this.$refs.airtableEmbed.dataset.embedId}?backgroundColor=purple&prefill_Name=${encodeURIComponent(this.fullName || '')}&prefill_Email=${encodeURIComponent(this.email || '')}`
349      }
350
351      this.hasSigned = true
352      this.trackEvent('signPetition')
353
354      // if there's a call modal, show that next
355      if (this.$refs.callFormModal) {
356        this.showCallFormModal()
357      }
358      // if we're on desktop, show the share modal
359      else if (!navigator.share) {
360        this.share()
361      }
362
363      if (this.usesCommentsAPI) {
364        this.sendToCommentsAPI()
365      }
366    },
367
368    addPrivacyPolicy() {
369      document.querySelectorAll('#d_sharing li label').forEach(label => {
370        // add privacy policy link to Fight for the Future box
371        if (label.innerText.match(/Fight for the Future/i)) {
372          const el = document.createElement('span')
373          el.innerHTML = '<a class="privacy-policy" href="https://www.fightforthefuture.org/privacy/" target="_blank">Privacy Policy</a>'
374          label.appendChild(el)
375        }
376
377        // remove FFTF Joint Actions from the list since it's not a user-facing group
378        if (label.innerText.match(/FFTF Joint Actions(, | and)/)) {
379          label.innerHTML = label.innerHTML.replace(/FFTF Joint Actions(, | and)/, '')
380        }
381      })
382    },
383
384    addSmsPolicies() {
385      document.querySelectorAll('input[name="sms_subscriber"').forEach(smsCheckbox => {
386        const label = smsCheckbox.parentNode
387        if (label.innerText.match(/Privacy Policy/i)) {
388          label.innerHTML = label.innerHTML.replace(/Privacy Policy/, '<a href="https://www.fightforthefuture.org/privacy/" target="_blank">Privacy Policy</a>')
389        }
390        if (label.innerText.match(/Terms of Service/i)) {
391          label.innerHTML = label.innerHTML.replace(/Terms of Service/, '<a href="https://www.fightforthefuture.org/sms/" target="_blank">Terms of Service</a>')
392        }
393      })
394    },
395
396    showCountrySelector() {
397      document.getElementById('can_embed_form').classList.toggle('is-international')
398    },
399
400    handleCountryChange() {
401      const country = document.getElementById('form-country').value
402
403      if (country !== 'US') {
404        document.getElementById('name_optin1').removeAttribute('checked')
405      }
406    },
407
408    handleDefendVpnsCountryChange(event) {
409      const preCheckedCountries = ['US', 'CA', 'AU']
410      if (!preCheckedCountries.includes(event.target.value)) {
411        const optIn = document.getElementById('fight_petition_opt_in')
412        if (optIn) {
413          optIn.checked = false
414        }
415      }
416    },
417
418    share(modalId) {
419      if (navigator.share) {
420        const title = getMetaContent('og:title') || document.title
421        const text = getMetaContent('og:description')
422        const url = getMetaContent('og:url') || location.href
423        navigator.share({ title, text, url })
424      }
425      else if (modalId) {
426        const shareModal = document.getElementById(modalId)
427
428        if (shareModal) {
429          new bootstrap.Modal(shareModal, {
430            keyboard: true,
431            focus: true
432          }).show()
433        }
434      }
435    },
436
437    showCallFormModal() {
438      if (this.$refs.callFormModal) {
439        new bootstrap.Modal(this.$refs.callFormModal, {
440          keyboard: true,
441          focus: true
442        }).show()
443      }
444    },
445
446    submitCallForm($event) {
447      // just assume it worked
448      this.hasCalled = true
449
450      // make AJAX request to CallPower API
451      const form = $event.target
452
453      fetch(form.action, {
454        method: form.method,
455        body: new URLSearchParams(new FormData(form))
456      })
457
458      this.trackEvent('makeCall')
459
460      const modal = form.closest('.modal');
461      if (modal) {
462        modal.addEventListener('hidden.bs.modal', event => {
463          this.hasCalled = false
464        })
465      }
466    },
467
468    async sendToCommentsAPI() {
469      // extract base domain name to use as source
470      let source = location.hostname.replace(/^www\./, '')
471
472      // if domain is fightforthefuture.org, add the path
473      if (source === 'fightforthefuture.org') {
474        source += location.pathname.replace(/\/$/, '')
475      }
476
477
478      const params = {
479        first_name: this.firstName,
480        last_name: this.lastName,
481        email: this.email,
482        address: this.street,
483        postal_code: this.zipCode,
484        country: this.country,
485        comment: this.comments,
486        destinations: [],
487        source
488      }
489
490      if (this.sendsToCongress) {
491        params.subject = this.congressEmailSubject
492
493        if (this.sendsToHouse) {
494          params.destinations.push('house')
495        }
496
497        if (this.sendsToSenate) {
498          params.destinations.push('senate')
499        }
500
501        if (this.sendsToBioguideIds) {
502          params.bioguide_ids = this.sendsToBioguideIds
503        }
504      }
505
506      if (this.sendsToRegulations) {
507        params.destinations.push('regulations')
508        params.regulations_doc = this.regulationsDoc
509      }
510
511      if (this.sendsToFCC) {
512        params.destinations.push('fcc')
513        params.fcc_docket = this.fccDocket
514      }
515
516      // temporary hack to add a disclaimer to the comment
517      // TODO: make this a block param
518      if (typeof (window.COMMENT_DISCLAIMER) !== 'undefined' && window.COMMENT_DISCLAIMER) {
519        params.comment += `\n\n(${window.COMMENT_DISCLAIMER})`
520      }
521
522      const response = await fetch('https://comments-api.fftf.cat/comments', {
523        method: 'POST',
524        body: JSON.stringify(params),
525        headers: {
526          'Content-Type': 'application/json'
527        }
528      })
529
530      if (response.ok) {
531        const data = await response.json()
532        console.log(data)
533      }
534    },
535
536    submitSubscribeForm($event) {
537      this.hasSubscribed = true
538
539      const form = $event.target
540
541      fetch(form.action, {
542        method: form.method,
543        headers: {
544          'Accept': 'application/json',
545          'Content-type': 'application/json'
546        },
547        body: JSON.stringify({
548          email: this.email,
549          phone: this.phone,
550          status: 'subscribed'
551        })
552      })
553    }
554  }
555})
556
557// Vue compiles everything inside #page as a template, so any server-rendered
558// remote/visitor text containing {{ }} would be evaluated (esc_html doesn't
559// help). Wrap that markup in an element with v-pre.
560const pageEl = document.getElementById('page')
561
562// Unicorn Studio / Gobt pixel canvases can't live inside the Vue root —
563// mount rebuilds #page and wipes them. Shortcodes still render in content;
564// hoist out first, then restore in-flow dividers after mount.
565if (pageEl) {
566  const gobtRestore = []
567  let gobtSlotSeq = 0
568
569  pageEl.querySelectorAll('.unicorn-studio, .gobt-pixels').forEach(function(el) {
570    if (el.classList.contains('unicorn-studio')) {
571      pageEl.parentNode.insertBefore(el, pageEl)
572      return
573    }
574
575    const type = el.getAttribute('data-gobt-pixels')
576    if (type === 'hero') {
577      pageEl.parentNode.insertBefore(el, pageEl)
578      return
579    }
580    if (type === 'footer') {
581      pageEl.parentNode.appendChild(el)
582      return
583    }
584
585    // In-content divider: leave a named slot Vue will re-create, park canvas
586    // outside #page, then re-query the slot after mount (old node refs die).
587    const slotId = 'gobt-slot-' + (++gobtSlotSeq)
588    const slot = document.createElement('div')
589    slot.className = 'gobt-pixels-slot'
590    slot.setAttribute('data-gobt-slot', slotId)
591    slot.setAttribute('aria-hidden', 'true')
592    el.parentNode.insertBefore(slot, el)
593    gobtRestore.push({ el: el, slotId: slotId })
594    document.body.appendChild(el)
595  })
596
597  app.mount(pageEl)
598
599  gobtRestore.forEach(function(item) {
600    const slot = document.querySelector('[data-gobt-slot="' + item.slotId + '"]')
601    if (slot && slot.parentNode) {
602      slot.parentNode.insertBefore(item.el, slot)
603      slot.remove()
604    }
605  })
606
607  document.dispatchEvent(new CustomEvent('fight:page-app-mounted'))
608}

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.