| @@ -7,8 +7,49 @@ | ||
| 7 | 7 | */ |
| 8 | 8 | ( function () { |
| 9 | 9 | 'use strict'; |
| 10 | 10 | |
| 11 | + // ── Per-session submission guard ────────────────────────────────────── | |
| 12 | + // A visitor may submit a given campaign's form only once per browser | |
| 13 | + // session; the flag is stored in sessionStorage keyed by campaign id. | |
| 14 | + function sessionStore() { | |
| 15 | + try { return window.sessionStorage; } catch ( e ) { return null; } | |
| 16 | + } | |
| 17 | + function submittedKey( nxId ) { | |
| 18 | + return 'notificationx_form_submitted_' + nxId; | |
| 19 | + } | |
| 20 | + function hasSubmitted( nxId ) { | |
| 21 | + var store = sessionStore(); | |
| 22 | + try { return !! ( store && store.getItem( submittedKey( nxId ) ) ); } | |
| 23 | + catch ( e ) { return false; } | |
| 24 | + } | |
| 25 | + function markSubmitted( nxId ) { | |
| 26 | + var store = sessionStore(); | |
| 27 | + try { if ( store ) store.setItem( submittedKey( nxId ), '1' ); } | |
| 28 | + catch ( e ) {} | |
| 29 | + } | |
| 30 | + | |
| 31 | + // Disable the form's submit button and show an "already submitted" notice. | |
| 32 | + function lockForm( form, btn, msgEl ) { | |
| 33 | + if ( btn ) btn.disabled = true; | |
| 34 | + if ( msgEl && ! msgEl.textContent ) { | |
| 35 | + msgEl.classList.add( 'is-success' ); | |
| 36 | + msgEl.textContent = form.getAttribute( 'data-already' ) | |
| 37 | + || 'You have already submitted this form.'; | |
| 38 | + } | |
| 39 | + } | |
| 40 | + | |
| 41 | + // If the form is rendered inside an Exit Intent popup, ask the popup to | |
| 42 | + // close itself (handled in ExitIntentPopup.tsx) shortly after a success. | |
| 43 | + function closeExitIntent( form, nxId ) { | |
| 44 | + if ( ! form.closest || ! form.closest( '.nx-exit-intent-overlay' ) ) return; | |
| 45 | + setTimeout( function () { | |
| 46 | + document.dispatchEvent( new CustomEvent( 'nx:exit-intent-close', { | |
| 47 | + detail: { nxId: nxId }, | |
| 48 | + } ) ); | |
| 49 | + }, 1600 ); | |
| 50 | + } | |
| 51 | + | |
| 11 | 52 | function init( form ) { |
| 12 | 53 | if ( ! form || form.__nxBound ) return; |
| 13 | 54 | form.__nxBound = true; |
| 14 | 55 | |
| @@ -13,12 +54,24 @@ | ||
| 13 | 54 | form.__nxBound = true; |
| 14 | 55 | |
| 15 | 56 | var msgEl = form.querySelector( '.nx-form-message' ); |
| 16 | 57 | var btn = form.querySelector( '.nx-form-submit' ); |
| 58 | + var nxId = form.getAttribute( 'data-nx-id' ); | |
| 17 | 59 | |
| 60 | + // Already submitted this session? Lock the form up-front. | |
| 61 | + if ( nxId && hasSubmitted( nxId ) ) { | |
| 62 | + lockForm( form, btn, msgEl ); | |
| 63 | + } | |
| 64 | + | |
| 18 | 65 | form.addEventListener( 'submit', function ( ev ) { |
| 19 | 66 | ev.preventDefault(); |
| 20 | 67 | |
| 68 | + // Guard against repeat submissions within the same session. | |
| 69 | + if ( nxId && hasSubmitted( nxId ) ) { | |
| 70 | + lockForm( form, btn, msgEl ); | |
| 71 | + return; | |
| 72 | + } | |
| 73 | + | |
| 21 | 74 | if ( msgEl ) { |
| 22 | 75 | msgEl.textContent = ''; |
| 23 | 76 | msgEl.classList.remove( 'is-success', 'is-error' ); |
| 24 | 77 | } |
| @@ -28,10 +81,9 @@ | ||
| 28 | 81 | form.reportValidity(); |
| 29 | 82 | return; |
| 30 | 83 | } |
| 31 | 84 | |
| 32 | - var nxId = form.getAttribute( 'data-nx-id' ); | |
| 33 | - var url = form.getAttribute( 'data-rest-url' ); | |
| 85 | + var url = form.getAttribute( 'data-rest-url' ); | |
| 34 | 86 | if ( ! nxId || ! url ) return; |
| 35 | 87 | |
| 36 | 88 | var payload = { |
| 37 | 89 | nx_id: nxId, |
| @@ -68,14 +120,22 @@ | ||
| 68 | 120 | return { ok: res.ok, body: {} }; |
| 69 | 121 | } ); |
| 70 | 122 | } ) |
| 71 | 123 | .then( function ( result ) { |
| 72 | - if ( ! msgEl ) return; | |
| 73 | 124 | if ( result.ok ) { |
| 74 | - msgEl.classList.add( 'is-success' ); | |
| 75 | - msgEl.textContent = form.getAttribute( 'data-success' ) || 'Submitted.'; | |
| 125 | + // Remember the submission for the rest of the session and | |
| 126 | + // keep the form locked so it can't be sent again. | |
| 127 | + markSubmitted( nxId ); | |
| 128 | + if ( btn ) btn.disabled = true; | |
| 129 | + if ( msgEl ) { | |
| 130 | + msgEl.classList.add( 'is-success' ); | |
| 131 | + msgEl.textContent = form.getAttribute( 'data-success' ) || 'Submitted.'; | |
| 132 | + } | |
| 76 | 133 | form.reset(); |
| 77 | - } else { | |
| 134 | + // When the form lives inside an Exit Intent popup, dismiss it | |
| 135 | + // after the success message has had a moment to register. | |
| 136 | + closeExitIntent( form, nxId ); | |
| 137 | + } else if ( msgEl ) { | |
| 78 | 138 | msgEl.classList.add( 'is-error' ); |
| 79 | 139 | msgEl.textContent = ( result.body && result.body.message ) |
| 80 | 140 | || form.getAttribute( 'data-error' ) |
| 81 | 141 | || 'Something went wrong.'; |
| @@ -86,9 +146,11 @@ | ||
| 86 | 146 | msgEl.classList.add( 'is-error' ); |
| 87 | 147 | msgEl.textContent = form.getAttribute( 'data-error' ) || 'Something went wrong.'; |
| 88 | 148 | } ) |
| 89 | 149 | .finally( function () { |
| 90 | - if ( btn ) btn.disabled = false; | |
| 150 | + // Leave the button disabled once a submission has gone through; | |
| 151 | + // only re-enable it after a failed attempt. | |
| 152 | + if ( btn && ! ( nxId && hasSubmitted( nxId ) ) ) btn.disabled = false; | |
| 91 | 153 | } ); |
| 92 | 154 | } ); |
| 93 | 155 | } |
| 94 | 156 | |