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.