Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fastlane update shipping options & taxes when changing address (3541) #2665

Merged
merged 10 commits into from
Oct 7, 2024
8 changes: 6 additions & 2 deletions modules/ppcp-axo/resources/js/AxoManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -801,8 +801,6 @@ class AxoManager {
}

async onChangeEmail() {
this.clearData();

if ( ! this.status.active ) {
log( 'Email checking skipped, AXO not active.' );
return;
Expand All @@ -813,11 +811,17 @@ class AxoManager {
return;
}

if ( this.data.email === this.emailInput.value ) {
log( 'Email has not changed since last validation.' );
return;
}

log(
`Email changed: ${
this.emailInput ? this.emailInput.value : '<empty>'
}`
);
this.clearData();

this.emailInput.value = this.stripSpaces( this.emailInput.value );

Expand Down
233 changes: 197 additions & 36 deletions modules/ppcp-axo/resources/js/Components/FormFieldGroup.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,35 @@
class FormFieldGroup {
constructor( config ) {
this.data = {};

this.baseSelector = config.baseSelector;
this.contentSelector = config.contentSelector;
this.fields = config.fields || {};
this.template = config.template;
#stored;
#data = {};
#active = false;
#baseSelector;
#contentSelector;
#fields = {};
#template;

this.active = false;
constructor( config ) {
this.#baseSelector = config.baseSelector;
this.#contentSelector = config.contentSelector;
this.#fields = config.fields || {};
this.#template = config.template;
this.#stored = new Map();
}

setData( data ) {
this.data = data;
this.#data = data;
this.refresh();
}

dataValue( fieldKey ) {
if ( ! fieldKey || ! this.fields[ fieldKey ] ) {
if ( ! fieldKey || ! this.#fields[ fieldKey ] ) {
return '';
}

if ( typeof this.fields[ fieldKey ].valueCallback === 'function' ) {
return this.fields[ fieldKey ].valueCallback( this.data );
if ( typeof this.#fields[ fieldKey ].valueCallback === 'function' ) {
return this.#fields[ fieldKey ].valueCallback( this.#data );
}

const path = this.fields[ fieldKey ].valuePath;
const path = this.#fields[ fieldKey ].valuePath;

if ( ! path ) {
return '';
Expand All @@ -35,72 +40,230 @@ class FormFieldGroup {
.reduce(
( acc, key ) =>
acc && acc[ key ] !== undefined ? acc[ key ] : undefined,
this.data
this.#data
);
return value ? value : '';
}

/**
* Changes the value of the input field.
*
* @param {Element|null} field
* @param {string|boolean} value
* @return {boolean} True indicates that the previous value was different from the new value.
*/
#setFieldValue( field, value ) {
let oldVal;

const isValidOption = () => {
for ( let i = 0; i < field.options.length; i++ ) {
if ( field.options[ i ].value === value ) {
return true;
}
}
return false;
};

if ( ! field ) {
return false;
}

// If an invalid option is provided, do nothing.
if ( 'SELECT' === field.tagName && ! isValidOption() ) {
return false;
}

if ( 'checkbox' === field.type || 'radio' === field.type ) {
value = !! value;
oldVal = field.checked;
field.checked = value;
} else {
oldVal = field.value;
field.value = value;
}

return oldVal !== value;
}

/**
* Activate form group: Render a custom Fastlane UI to replace the WooCommerce form.
*
* Indicates: Ryan flow.
*/
activate() {
this.active = true;
this.#active = true;
this.storeFormData();
this.refresh();
}

/**
* Deactivate form group: Remove the custom Fastlane UI - either display the default
* WooCommerce checkout form or no form at all (when no email was provided yet).
*
* Indicates: Gary flow / no email provided / not using Fastlane.
*/
deactivate() {
this.active = false;
this.#active = false;
this.restoreFormData();
this.refresh();
}

toggle() {
this.active ? this.deactivate() : this.activate();
if ( this.#active ) {
this.deactivate();
} else {
this.activate();
}
}

refresh() {
const content = document.querySelector( this.contentSelector );
const content = document.querySelector( this.#contentSelector );

if ( ! content ) {
return;
}

content.innerHTML = '';

if ( ! this.active ) {
this.hideField( this.contentSelector );
if ( ! this.#active ) {
this.hideField( this.#contentSelector );
} else {
this.showField( this.contentSelector );
this.showField( this.#contentSelector );
}

Object.keys( this.fields ).forEach( ( key ) => {
const field = this.fields[ key ];

if ( this.active && ! field.showInput ) {
this.hideField( field.selector );
this.loopFields( ( { selector } ) => {
if ( this.#active /* && ! field.showInput */ ) {
this.hideField( selector );
} else {
this.showField( field.selector );
this.showField( selector );
}
} );

if ( typeof this.template === 'function' ) {
content.innerHTML = this.template( {
if ( typeof this.#template === 'function' ) {
content.innerHTML = this.#template( {
value: ( fieldKey ) => {
return this.dataValue( fieldKey );
},
isEmpty: () => {
let isEmpty = true;
Object.keys( this.fields ).forEach( ( fieldKey ) => {

this.loopFields( ( field, fieldKey ) => {
if ( this.dataValue( fieldKey ) ) {
isEmpty = false;
return false;
}
} );

return isEmpty;
},
} );
}
}

/**
* Invoke a callback on every field in the current group.
*
* @param {(field: object, key: string) => void} callback
*/
loopFields( callback ) {
for ( const [ key, field ] of Object.entries( this.#fields ) ) {
const { selector, inputName } = field;
const inputSelector = `${ selector } [name="${ inputName }"]`;

const fieldInfo = {
inputSelector: inputName ? inputSelector : '',
...field,
};

callback( fieldInfo, key );
}
}

/**
* Stores the current form data in an internal storage.
* This allows the original form to be restored later.
*/
storeFormData() {
const storeValue = ( field, name ) => {
if ( 'checkbox' === field.type || 'radio' === field.type ) {
this.#stored.set( name, field.checked );
this.#setFieldValue( field, this.dataValue( name ) );
} else {
this.#stored.set( name, field.value );
this.#setFieldValue( field, '' );
}
};

this.loopFields( ( { inputSelector }, fieldKey ) => {
if ( inputSelector && ! this.#stored.has( fieldKey ) ) {
const elInput = document.querySelector( inputSelector );

if ( elInput ) {
storeValue( elInput, fieldKey );
}
}
} );
}

/**
* Restores the form data to its initial state before the form group was activated.
* This function iterates through the stored form fields and resets their values or states.
*/
restoreFormData() {
let formHasChanged = false;

// Reset form fields to their initial state.
this.loopFields( ( { inputSelector }, fieldKey ) => {
if ( ! this.#stored.has( fieldKey ) ) {
return;
}

const elInput = inputSelector
? document.querySelector( inputSelector )
: null;
const oldValue = this.#stored.get( fieldKey );
this.#stored.delete( fieldKey );

if ( this.#setFieldValue( elInput, oldValue ) ) {
formHasChanged = true;
}
} );

if ( formHasChanged ) {
document.body.dispatchEvent( new Event( 'update_checkout' ) );
}
}

/**
* Syncs the internal field-data with the hidden checkout form fields.
*/
syncDataToForm() {
if ( ! this.#active ) {
return;
}

let formHasChanged = false;

// Push data to the (hidden) checkout form.
this.loopFields( ( { inputSelector }, fieldKey ) => {
const elInput = inputSelector
? document.querySelector( inputSelector )
: null;

if ( this.#setFieldValue( elInput, this.dataValue( fieldKey ) ) ) {
formHasChanged = true;
}
} );

// Tell WooCommerce about the changes.
if ( formHasChanged ) {
document.body.dispatchEvent( new Event( 'update_checkout' ) );
}
}

showField( selector ) {
const field = document.querySelector(
this.baseSelector + ' ' + selector
this.#baseSelector + ' ' + selector
);
if ( field ) {
field.classList.remove( 'ppcp-axo-field-hidden' );
Expand All @@ -109,15 +272,15 @@ class FormFieldGroup {

hideField( selector ) {
const field = document.querySelector(
this.baseSelector + ' ' + selector
this.#baseSelector + ' ' + selector
);
if ( field ) {
field.classList.add( 'ppcp-axo-field-hidden' );
}
}

inputElement( name ) {
const baseSelector = this.fields[ name ].selector;
const baseSelector = this.#fields[ name ].selector;

const select = document.querySelector( baseSelector + ' select' );
if ( select ) {
Expand All @@ -138,9 +301,7 @@ class FormFieldGroup {
}

toSubmitData( data ) {
Object.keys( this.fields ).forEach( ( fieldKey ) => {
const field = this.fields[ fieldKey ];

this.loopFields( ( field, fieldKey ) => {
if ( ! field.valuePath || ! field.selector ) {
return true;
}
Expand Down
Loading
Loading