diff --git a/square/js/frontend.js b/square/js/frontend.js index 4f49d997ac..a2267ce1d3 100644 --- a/square/js/frontend.js +++ b/square/js/frontend.js @@ -32,6 +32,13 @@ const card = await payments.card(); const cardStyle = frmSquareVars.style; + + // Never attach while the card element is hidden (e.g. by conditional + // logic). Square measures the container on attach, and a hidden + // container measures as zero-size, so the card form renders with the + // wrong height. Wait until the element is visible before attaching. + await waitForVisibleCardElement( cardElement ); + await card.attach( '.frm-card-element' ); card.configure( { style: cardStyle } ); @@ -60,6 +67,12 @@ if ( squareCardElementIsComplete ) { enableSubmit(); } else { + if ( squareIsConditionallyDisabled( thisForm ) ) { + running = 0; + enableSubmit(); + return; + } + disableSubmit( thisForm ); } } @@ -87,6 +100,84 @@ return card; } + /** + * Resolve once the card element is visible (has a layout box). + * A width of zero means the element or one of its ancestors is hidden, + * usually by conditional logic setting display: none. + * + * @since x.x + * + * @param {HTMLElement} cardElement + * @return {Promise} + */ + function waitForVisibleCardElement( cardElement ) { + return new Promise( resolve => { + if ( cardElement.getBoundingClientRect().width > 0 ) { + resolve(); + return; + } + + const form = cardElement.closest( 'form' ); + const observer = new MutationObserver( () => { + if ( cardElement.getBoundingClientRect().width > 0 ) { + observer.disconnect(); + resolve(); + } + } ); + + // Conditional logic toggles inline styles on field and section + // containers, so watch the whole form for attribute changes and + // re-check the card element's visibility on each change. + observer.observe( form || document.body, { + attributes: true, + attributeFilter: [ 'style', 'class' ], + subtree: true + } ); + } ); + } + + /** + * Check if a Square card element is conditionally hidden. + * If it is, we should not be disabling the submit button. + * + * @since x.x + * + * @param {HTMLElement} form + * + * @return {boolean} True if the field is conditionally hidden, false otherwise. + */ + function squareIsConditionallyDisabled( form ) { + const fieldContainer = getPaymentElementFieldContainer( form ); + if ( ! fieldContainer ) { + return false; + } + + // Field is conditionally hidden. + if ( 'none' === fieldContainer.style.display ) { + return true; + } + + // Section parent is conditionally hidden. + const parentSection = fieldContainer.closest( '.frm_section_heading' ); + return parentSection && 'none' === parentSection.style.display; + } + + /** + * Try to get the field container for a Square card payment element. + * The field container is checked to determine if the field is conditionally hidden or not. + * + * @param {HTMLElement} form + * + * @return {HTMLElement|null} The field container element or null if not found. + */ + function getPaymentElementFieldContainer( form ) { + const paymentElement = form.querySelector( '.frm-card-element' ); + if ( ! paymentElement ) { + return null; + } + return paymentElement.parentElement.closest( '.frm_form_field' ); + } + /** * Enable the submit button for the form. */ @@ -222,11 +313,19 @@ if ( cardContainer ) { thisForm = cardContainer.closest( 'form' ); if ( thisForm ) { - // Initially disable the submit button until card is valid - disableSubmit( thisForm ); + listenForFieldMutations( thisForm ); + + if ( ! squareIsConditionallyDisabled( thisForm ) ) { + // Initially disable the submit button until card is valid + disableSubmit( thisForm ); + } // Add event listener for form submission thisForm.addEventListener( 'submit', function( event ) { + if ( squareIsConditionallyDisabled( thisForm ) ) { + return; + } + event.preventDefault(); event.stopPropagation(); @@ -316,6 +415,129 @@ } } + /** + * Possibly toggle on and off the submit button when a Stripe Link payment field is conditionally shown or hidden. + * + * @since x.x + * + * @param {HTMLElement} form + * @return {void} + */ + function listenForFieldMutations( form ) { + const fieldContainer = getPaymentElementFieldContainer( form ); + if ( ! fieldContainer ) { + return; + } + + observeAttributeMutations( fieldContainer, handleMutation ); + + const section = fieldContainer.closest( '.frm_section_heading' ); + if ( section ) { + observeAttributeMutations( section, handleMutation ); + } + + const formId = getFormIdForForm( form ); + + /** + * Handle a style attribute change for either a payment field container + * or the field container of its parent section. + * + * @param {MutationRecord} mutation + * @return {void} + */ + function handleMutation( mutation ) { + if ( mutation.attributeName !== 'style' ) { + return; + } + + if ( submitButtonIsConditionallyDisabled( formId ) ) { + return; + } + + const isFieldVisible = 'none' !== mutation.target.style.display; + + // If field is hidden, enable submit (field is conditionally not required) + if ( ! isFieldVisible ) { + thisForm = form; + running = 0; + enableSubmit(); + return; + } + + // Field is now visible, recalculate size and check validation + if ( cardGlobal ) { + cardGlobal.recalculateSize(); + } + + const shouldEnable = squareCardElementIsComplete || squareIsConditionallyDisabled( form ); + if ( ! shouldEnable ) { + disableSubmit( form ); + return; + } + + thisForm = form; + running = 0; + enableSubmit(); + } + } + + /** + * @param {HTMLElement} element + * @param {function} mutationHandler + * + * @return {void} + */ + function observeAttributeMutations( element, mutationHandler ) { + const observer = new MutationObserver( + mutations => { + mutations.forEach( mutationHandler ); + } + ); + observer.observe( + element, + { attributes: true } + ); + } + + /** + * Check if the submit button is conditionally disabled. + * This is required for Stripe link so the button does not get enabled at the wrong time after completing the Stripe elements. + * + * @since x.x + * + * @param {string} formId + * + * @return {boolean} True if the submit button is conditionally disabled, false otherwise. + */ + function submitButtonIsConditionallyDisabled( formId ) { + return submitButtonIsConditionallyNotAvailable( formId ) && 'disable' === __FRMRULES[ `submit_${ formId }` ].hideDisable; + } + + /** + * Check submit button is conditionally "hidden". This is also used for the enabled check and is used in submitButtonIsConditionallyDisabled. + * + * @since x.x + * + * @param {string} formId + * + * @return {boolean} True if the submit button is conditionally not available, false otherwise. + */ + function submitButtonIsConditionallyNotAvailable( formId ) { + const hideFields = document.getElementById( `frm_hide_fields_${ formId }` ); + return hideFields && hideFields.value.includes( `["frm_form_${ formId }_container .frm_final_submit"]` ); + } + + /** + * Check a form's form_id input for a form ID value. + * + * @param {HTMLElement} form + * + * @return {number} The form ID. + */ + function getFormIdForForm( form ) { + return parseInt( form.querySelector( '[name="form_id"]' ).value ); + } + document.addEventListener( 'DOMContentLoaded', async function() { if ( ! window.Square ) { console.error( 'Square.js failed to load properly' );