PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / trunk
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More vtrunk
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
← All changes | square/js/frontend.js +295 -6 6.31 → trunk View file →
@@ -14,14 +14,15 @@
14 14 let cardGlobal;
15 15
16 16 const buyerTokens = {};
17 17
18 - // Track the state of each field in the card form
18 + // Track the state of each field in the card form.
19 + // postalCode is not included by default because Square hides it
20 + // for cards issued in countries that don't require it (e.g., Australia).
19 21 const cardFields = {
20 22 cardNumber: false,
21 23 expirationDate: false,
22 - cvv: false,
23 - postalCode: false
24 + cvv: false
24 25 };
25 26
26 27 async function initializeCard( payments ) {
27 28 const cardElement = document.querySelector( '.frm-card-element' );
@@ -30,16 +31,33 @@
30 31 }
31 32
32 33 const card = await payments.card();
33 34 const cardStyle = frmSquareVars.style;
35 +
36 + // Never attach while the card element is hidden (e.g. by conditional
37 + // logic). Square measures the container on attach, and a hidden
38 + // container measures as zero-size, so the card form renders with the
39 + // wrong height. Wait until the element is visible before attaching.
40 + await waitForVisibleCardElement( cardElement );
41 +
34 42 await card.attach( '.frm-card-element' );
35 43
36 44 card.configure( { style: cardStyle } );
37 45
46 + // Track when the postal code field is rendered by Square.
47 + // Square hides the postal code for cards issued in certain countries.
48 + card.addEventListener( 'focusClassAdded', event => {
49 + const { field } = event.detail;
50 + if ( field === 'postalCode' ) {
51 + cardFields.postalCode = event.detail.currentState.isCompletelyValid;
52 + }
53 + } );
54 +
38 55 // Add event listener to track when the card form is valid
39 56 card.addEventListener( 'focusClassRemoved', event => {
40 57 const { field } = event.detail;
41 58 const value = event.detail.currentState.isCompletelyValid;
59 +
42 60 cardFields[ field ] = value;
43 61
44 62 // Check if all fields are valid
45 63 squareCardElementIsComplete = Object.values( cardFields ).every( item => item === true );
@@ -48,8 +66,14 @@
48 66 if ( thisForm ) {
49 67 if ( squareCardElementIsComplete ) {
50 68 enableSubmit();
51 69 } else {
70 + if ( squareIsConditionallyDisabled( thisForm ) ) {
71 + running = 0;
72 + enableSubmit();
73 + return;
74 + }
75 +
52 76 disableSubmit( thisForm );
53 77 }
54 78 }
55 79 } );
@@ -66,9 +90,11 @@
66 90 cardFields.cvv = true;
67 91 cardFields.postalCode = true;
68 92 enableSubmit();
69 93 } else {
70 - disableSubmit();
94 + cardFields.postalCode = false;
95 + squareCardElementIsComplete = false;
96 + disableSubmit( thisForm );
71 97 }
72 98 } );
73 99
74 100 return card;
@@ -74,9 +100,92 @@
74 100 return card;
75 101 }
76 102
77 103 /**
104 + * Resolve once the card element is visible (has a layout box).
105 + * A width of zero means the element or one of its ancestors is hidden,
106 + * usually by conditional logic setting display: none.
107 + *
108 + * @since 6.34
109 + *
110 + * @param {HTMLElement} cardElement
111 + * @return {Promise<void>}
112 + */
113 + function waitForVisibleCardElement( cardElement ) {
114 + return new Promise( resolve => {
115 + if ( cardElement.getBoundingClientRect().width > 0 ) {
116 + resolve();
117 + return;
118 + }
119 +
120 + const form = cardElement.closest( 'form' );
121 + const observer = new MutationObserver( () => {
122 + if ( cardElement.getBoundingClientRect().width > 0 ) {
123 + observer.disconnect();
124 + resolve();
125 + }
126 + } );
127 +
128 + // Conditional logic toggles inline styles on field and section
129 + // containers, so watch the whole form for attribute changes and
130 + // re-check the card element's visibility on each change.
131 + observer.observe( form || document.body, {
132 + attributes: true,
133 + attributeFilter: [ 'style', 'class' ],
134 + subtree: true
135 + } );
136 + } );
137 + }
138 +
139 + /**
140 + * Check if a Square card element is conditionally hidden.
141 + * If it is, we should not be disabling the submit button.
142 + *
143 + * @since 6.34
144 + *
145 + * @param {HTMLElement} form
146 + *
147 + * @return {boolean} True if the field is conditionally hidden, false otherwise.
148 + */
149 + function squareIsConditionallyDisabled( form ) {
150 + const fieldContainer = getPaymentElementFieldContainer( form );
151 + if ( ! fieldContainer ) {
152 + return false;
153 + }
154 +
155 + // Field is conditionally hidden.
156 + if ( 'none' === fieldContainer.style.display ) {
157 + return true;
158 + }
159 +
160 + // Section parent is conditionally hidden.
161 + const parentSection = fieldContainer.closest( '.frm_section_heading' );
162 + return parentSection && 'none' === parentSection.style.display;
163 + }
164 +
165 + /**
166 + * Try to get the field container for a Square card payment element.
167 + * The field container is checked to determine if the field is conditionally hidden or not.
168 + *
169 + * @param {HTMLElement} form
170 + *
171 + * @return {HTMLElement|null} The field container element or null if not found.
172 + */
173 + function getPaymentElementFieldContainer( form ) {
174 + const paymentElement = form.querySelector( '.frm-card-element' );
175 + if ( ! paymentElement ) {
176 + return null;
177 + }
178 + return paymentElement.parentElement.closest( '.frm_form_field' );
179 + }
180 +
181 + /**
78 182 * Enable the submit button for the form.
183 + *
184 + * Square being ready is only half of what enables the button. When the submit
185 + * button has conditional logic of its own that is not satisfied, it stays
186 + * disabled no matter what state the card is in, so every caller is checked
187 + * here rather than at the individual call sites.
79 188 */
80 189 function enableSubmit() {
81 190 if ( running > 0 ) {
82 191 return;
@@ -81,8 +190,12 @@
81 190 if ( running > 0 ) {
82 191 return;
83 192 }
84 193
194 + if ( submitButtonIsConditionallyDisabled( getFormIdForForm( thisForm ) ) ) {
195 + return;
196 + }
197 +
85 198 thisForm.classList.add( 'frm_loading_form' );
86 199 frmFrontForm.removeSubmitLoading( jQuery( thisForm ), 'enable', 0 );
87 200
88 201 // Trigger custom event for other scripts to hook into
@@ -208,13 +321,22 @@
208 321 const cardContainer = document.querySelector( '.frm-card-element' );
209 322 if ( cardContainer ) {
210 323 thisForm = cardContainer.closest( 'form' );
211 324 if ( thisForm ) {
212 - // Initially disable the submit button until card is valid
213 - disableSubmit( thisForm );
325 + listenForFieldMutations( thisForm );
326 + listenForSubmitButtonMutations( thisForm );
214 327
328 + if ( ! squareIsConditionallyDisabled( thisForm ) ) {
329 + // Initially disable the submit button until card is valid
330 + disableSubmit( thisForm );
331 + }
332 +
215 333 // Add event listener for form submission
216 334 thisForm.addEventListener( 'submit', function( event ) {
335 + if ( squareIsConditionallyDisabled( thisForm ) ) {
336 + return;
337 + }
338 +
217 339 event.preventDefault();
218 340 event.stopPropagation();
219 341
220 342 if ( ! squareCardElementIsComplete ) {
@@ -300,8 +422,175 @@
300 422 }
301 423 displayPaymentFailure( e.message );
302 424 }
303 425 }
426 + }
427 +
428 + /**
429 + * Possibly toggle on and off the submit button when a Stripe Link payment field is conditionally shown or hidden.
430 + *
431 + * @since 6.34
432 + *
433 + * @param {HTMLElement} form
434 + * @return {void}
435 + */
436 + function listenForFieldMutations( form ) {
437 + const fieldContainer = getPaymentElementFieldContainer( form );
438 + if ( ! fieldContainer ) {
439 + return;
440 + }
441 +
442 + observeAttributeMutations( fieldContainer, handleMutation );
443 +
444 + const section = fieldContainer.closest( '.frm_section_heading' );
445 + if ( section ) {
446 + observeAttributeMutations( section, handleMutation );
447 + }
448 +
449 + /**
450 + * Handle a style attribute change for either a payment field container
451 + * or the field container of its parent section.
452 + *
453 + * @param {MutationRecord} mutation
454 + * @return {void}
455 + */
456 + function handleMutation( mutation ) {
457 + if ( mutation.attributeName !== 'style' ) {
458 + return;
459 + }
460 +
461 + const isFieldVisible = 'none' !== mutation.target.style.display;
462 +
463 + // If field is hidden, enable submit (field is conditionally not required)
464 + if ( ! isFieldVisible ) {
465 + thisForm = form;
466 + running = 0;
467 + enableSubmit();
468 + return;
469 + }
470 +
471 + // Field is now visible, recalculate size and check validation
472 + if ( cardGlobal ) {
473 + cardGlobal.recalculateSize();
474 + }
475 +
476 + const shouldEnable = squareCardElementIsComplete || squareIsConditionallyDisabled( form );
477 + if ( ! shouldEnable ) {
478 + disableSubmit( form );
479 + return;
480 + }
481 +
482 + thisForm = form;
483 + running = 0;
484 + enableSubmit();
485 + }
486 + }
487 +
488 + /**
489 + * Keep the submit button disabled while the Square card is not ready.
490 + *
491 + * Conditional logic on the submit button enables it as soon as its own
492 + * conditions are met, with no knowledge of the payment field. Watch for that
493 + * and disable it again until the card details are complete.
494 + *
495 + * @since 6.34
496 + *
497 + * @param {HTMLElement} form
498 + * @return {void}
499 + */
500 + function listenForSubmitButtonMutations( form ) {
501 + const submitButton = form.querySelector( '.frm_final_submit' );
502 + if ( ! submitButton ) {
503 + return;
504 + }
505 +
506 + observeAttributeMutations( submitButton, mutation => {
507 + if ( mutation.attributeName !== 'disabled' || submitButton.disabled ) {
508 + return;
509 + }
510 +
511 + if ( squareCardElementIsComplete || squareIsConditionallyDisabled( form ) ) {
512 + // Either the card is filled in or it is conditionally hidden, so
513 + // there is nothing left for Square to wait on.
514 + return;
515 + }
516 +
517 + disableSubmit( form );
518 + } );
519 + }
520 +
521 + /**
522 + * @param {HTMLElement} element
523 + * @param {Function} mutationHandler
524 + *
525 + * @return {void}
526 + */
527 + function observeAttributeMutations( element, mutationHandler ) {
528 + const observer = new MutationObserver(
529 + mutations => {
530 + mutations.forEach( mutationHandler );
531 + }
532 + );
533 + observer.observe(
534 + element,
535 + { attributes: true }
536 + );
537 + }
538 +
539 + /**
540 + * Check if the submit button is conditionally disabled.
541 + * This is required for Stripe link so the button does not get enabled at the wrong time after completing the Stripe elements.
542 + *
543 + * @since 6.34
544 + *
545 + * @param {string} formId
546 + *
547 + * @return {boolean} True if the submit button is conditionally disabled, false otherwise.
548 + */
549 + function submitButtonIsConditionallyDisabled( formId ) {
550 + if ( ! submitButtonIsConditionallyNotAvailable( formId ) ) {
551 + return false;
552 + }
553 +
554 + // __FRMRULES is only defined when conditional logic is on the page.
555 + const submitRules = typeof __FRMRULES === 'undefined' ? undefined : __FRMRULES[ `submit_${ formId }` ];
556 +
557 + return Boolean( submitRules ) && 'disable' === submitRules.hideDisable;
558 + }
559 +
560 + /**
561 + * Check submit button is conditionally "hidden". This is also used for the enabled check and is used in submitButtonIsConditionallyDisabled.
562 + *
563 + * @since 6.34
564 + *
565 + * @param {string} formId
566 + *
567 + * @return {boolean} True if the submit button is conditionally not available, false otherwise.
568 + */
569 + function submitButtonIsConditionallyNotAvailable( formId ) {
570 + const hideFields = document.getElementById( `frm_hide_fields_${ formId }` );
571 + if ( ! hideFields ) {
572 + return false;
573 + }
574 +
575 + // The value is a JSON array of every container conditional logic has
576 + // hidden, for example ["frm_field_25_container","frm_form_16_container
577 + // .frm_final_submit"], so match the quoted entry anywhere in it. Matching
578 + // the array brackets too would only find the submit button when it is the
579 + // single hidden entry, and it never is once the payment field has
580 + // conditional logic of its own.
581 + return hideFields.value.includes( `"frm_form_${ formId }_container .frm_final_submit"` );
582 + }
583 +
584 + /**
585 + * Check a form's form_id input for a form ID value.
586 + *
587 + * @param {HTMLElement} form
588 + *
589 + * @return {number} The form ID.
590 + */
591 + function getFormIdForForm( form ) {
592 + return parseInt( form.querySelector( '[name="form_id"]' ).value );
304 593 }
305 594
306 595 document.addEventListener( 'DOMContentLoaded', async function() {
307 596 if ( ! window.Square ) {