From: Stephen Cameron Date: Thu, 30 Apr 2020 19:53:51 +0000 (+0200) Subject: Form processing, refactoring and bug fixes. WIP #3445 @9 X-Git-Url: http://git.cubedesigners.com/?a=commitdiff_plain;h=ef6f0564e5dc5422243618347ea449946c93a8b8;p=ccv-wordpress.git Form processing, refactoring and bug fixes. WIP #3445 @9 --- diff --git a/wp-content/mu-plugins/cube/src/Elementor/Widgets/Form.php b/wp-content/mu-plugins/cube/src/Elementor/Widgets/Form.php index a712206..ab75ec1 100644 --- a/wp-content/mu-plugins/cube/src/Elementor/Widgets/Form.php +++ b/wp-content/mu-plugins/cube/src/Elementor/Widgets/Form.php @@ -78,6 +78,19 @@ class Form extends _Base { 'placeholder' => $form_base->get_destination(), ] ); + + $this->add_control( + 'success_message', + [ + 'type' => Controls_Manager::WYSIWYG, + 'label' => __('Message après la soumission réussie', 'cube'), + 'label_block' => true, + 'default' => __('Nous vous remercions pour votre demande.', 'cube'), + 'dynamic' => [ + 'active' => false, + ], + ] + ); $this->end_controls_section(); @@ -94,10 +107,10 @@ class Form extends _Base { $form_name = $this->get_settings('form_name'); $destination = $this->get_settings('destination'); + $success_message = $this->get_settings('success_message'); // Serialize and encode configuration for use in hidden field - // Currently only the form name and destination address is set but this might be extended in the future - $config = base64_encode(serialize(compact('form_name', 'destination'))); + $config = base64_encode(serialize(compact('form_name', 'destination', 'success_message'))); $base_form = new BaseForm(); $forms = $base_form->get_forms(); diff --git a/wp-content/mu-plugins/cube/src/Forms/Base.php b/wp-content/mu-plugins/cube/src/Forms/Base.php index f81b069..694fbd7 100644 --- a/wp-content/mu-plugins/cube/src/Forms/Base.php +++ b/wp-content/mu-plugins/cube/src/Forms/Base.php @@ -13,9 +13,11 @@ class Base private $forms = [ // All available forms 'consultation' => Consultation::class, 'training' => Training::class, + 'contact' => Contact::class, ]; private $fields = []; + private $config; private $data; public $form_title = ''; @@ -52,7 +54,7 @@ class Base //=== Cube Forms // Base forms functionality - enqueued directly since it is always required - wp_enqueue_script('cube-forms', asset('scripts/forms.js'), ['jquery'], null, true); + wp_enqueue_script('cube-forms', asset('scripts/forms/forms.js'), ['jquery'], null, true); // JS variables wp_add_inline_script( @@ -67,7 +69,8 @@ class Base //=== Parsley JS Validation - wp_enqueue_script('parsleyjs', asset('scripts/parsley/parsley.min.js'), ['jquery'], null, true); + $parsley_path = 'scripts/forms/parsley'; // Relative to dist folder + wp_enqueue_script('parsleyjs', asset("$parsley_path/parsley.min.js"), ['jquery'], null, true); // Load localisation if available (English is default so not needed) $parsley_locales = ['ar', 'fr', 'ru']; // List of possible locales (see webpack.mix.js for which ones are copied) @@ -76,33 +79,12 @@ class Base if (in_array($current_locale, $parsley_locales)) { // Include it inline since it's small and to save an extra HTTP request - wp_add_inline_script('parsleyjs', asset("scripts/parsley/locale/{$current_locale}.js")->contents()); + wp_add_inline_script('parsleyjs', asset("$parsley_path/locale/$current_locale.js")->contents()); } } // Parsley JS initialisation and overrides - // Ref: https://stackoverflow.com/a/30122442 - wp_add_inline_script('parsleyjs', " - const parsleyConfig = { - classHandler: function(parsleyField) { - var fieldWrapper = parsleyField.\$element.parents('.form-field'); - return (fieldWrapper.length > 0) ? fieldWrapper : parsleyField; - }, - - errorsContainer: function(parsleyField) { - var inputWrapper = parsleyField.\$element.parents('.form-field-input'); - return (inputWrapper.length > 0) ? inputWrapper : parsleyField; - } - }; - - jQuery('.cube-form').parsley(parsleyConfig); - - // On validation errors, scroll to the first invalid input - jQuery.listen('parsley:field:error', function() { - window.scrollTo({...jQuery('.parsley-error').first().offset(), ...{behavior: 'smooth'}}); - }); - - "); + wp_add_inline_script('parsleyjs', asset("$parsley_path/parsley-setup.js")->contents()); //=== Flatpickr Calendar @@ -159,25 +141,38 @@ class Base // First, check that request is valid via nonce value if (check_ajax_referer($this->action_name, 'nonce', false) === false) { - wp_send_json_error(new \WP_Error('nonce', 'Failed security validation')); + wp_send_json([ + 'message' => [ + 'type' => 'error', + 'text' => __('Une erreur est survenue. Veuillez réessayer.', 'ccv'), + ], + 'debug' => 'Failed security validation (nonce)', + ]); } // Next, get the form configuration and decode + unserialize it $config = unserialize(base64_decode($_POST['config'])); - //check which form is being sent and if it is valid + // Check which form is being sent and if it is valid $form_name = $config['form_name'] ?? null; if (!($form_name && array_key_exists($form_name, $this->forms))) { - wp_send_json_error(new \WP_Error('invalid_form', "Unknown form ($form_name)")); + wp_send_json([ + 'message' => [ + 'type' => 'error', + 'text' => __('Une erreur est survenue. Veuillez réessayer.', 'ccv'), + ], + 'debug' => "Unknown form ($form_name)", + ]); } // Load the form and process it... /* @var $form Base */ $form = new $this->forms[$form_name]; $form->register_fields(); - $form->set_destination($config['destination']); $form->data = $this->get_form_data(); + $form->config = $config; + $form->set_destination($config['destination']); $form->process(); exit; } @@ -192,7 +187,7 @@ class Base $data = []; $to = $this->destination; - $from = 'CCV form_title; $content_type = 'text/html'; $charset = get_bloginfo('charset'); @@ -212,9 +207,20 @@ class Base $success = wp_mail($to, $subject, $message, $headers); if ($success) { - wp_send_json_success(__('Success !', 'ccv')); + wp_send_json([ + 'message' => [ + 'type' => 'success', + 'text' => $this->config['success_message'], + ], + 'hide_form' => true, + ]); } else { - wp_send_json_error(); + wp_send_json([ + 'message' => [ + 'type' => 'error', + 'text' => __("Erreur d'envoi du message. Veuillez réessayer.", "ccv"), + ] + ]); } $this->post_process(); @@ -470,7 +476,7 @@ class Base /** * HTML (submit) button - * @param $text Button label + * @param string $text Button label * @param array $settings * @return string */ @@ -478,11 +484,12 @@ class Base $default_settings = [ 'type' => 'submit', 'class' => 'btn', + 'loading_text' => '', ]; $settings = array_merge($default_settings, $settings); - return ''; + return ''; } diff --git a/wp-content/mu-plugins/cube/src/Forms/Contact.php b/wp-content/mu-plugins/cube/src/Forms/Contact.php new file mode 100644 index 0000000..9ba13ac --- /dev/null +++ b/wp-content/mu-plugins/cube/src/Forms/Contact.php @@ -0,0 +1,19 @@ +form_title = __('Formulaire de contact', 'ccv'); + } + + function register_fields() { + + parent::register_fields(); + + $this->add_field('last-name', _x('Nom', 'Nom de famille', 'ccv'), self::TEXT); + $this->add_field('first-name', __('Prénom', 'ccv'), self::TEXT); + $this->add_field('phone', __('Téléphone', 'ccv'), self::TEXT); + } +} diff --git a/wp-content/themes/CCV/index.php b/wp-content/themes/CCV/index.php index bf7ac5a..3ca57da 100644 --- a/wp-content/themes/CCV/index.php +++ b/wp-content/themes/CCV/index.php @@ -14,7 +14,7 @@ -
+
render(); ?>
diff --git a/wp-content/themes/CCV/resources/assets/scripts/forms.js b/wp-content/themes/CCV/resources/assets/scripts/forms.js deleted file mode 100644 index 7b2bb41..0000000 --- a/wp-content/themes/CCV/resources/assets/scripts/forms.js +++ /dev/null @@ -1,29 +0,0 @@ -//=== Cube Forms -// Inspired by HTMLForms: https://github.com/ibericode/html-forms/blob/master/assets/browserify/public.js - -const config = window.cube_forms_config || {}; - -function handleSubmitEvents (e) { - const formEl = e.target; - if (formEl.className.indexOf('cube-form') < 0) { - return - } - e.preventDefault(); // always prevent default because we only want to send via AJAX - submitForm(formEl) -} - -function submitForm(form) { - const formData = new FormData(form); - - formData.append('action', config.action); - formData.append('nonce', config.nonce); - - let request = new XMLHttpRequest(); - //request.onreadystatechange = createRequestHandler(form); - request.open('POST', config.ajax_url, true); - request.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); - request.send(formData); - request = null; -} - -document.addEventListener('submit', handleSubmitEvents, false); // useCapture=false to ensure we bubble upwards (and thus can cancel propagation) diff --git a/wp-content/themes/CCV/resources/assets/scripts/forms/form-loading-indicator.js b/wp-content/themes/CCV/resources/assets/scripts/forms/form-loading-indicator.js new file mode 100644 index 0000000..8fc6a8b --- /dev/null +++ b/wp-content/themes/CCV/resources/assets/scripts/forms/form-loading-indicator.js @@ -0,0 +1,66 @@ +'use strict'; + +function getButtonText (button) { + return button.innerHTML ? button.innerHTML : button.value; +} + +function setButtonText (button, text) { + button.innerHTML ? button.innerHTML = text : button.value = text; +} + +function Loader (formElement) { + this.form = formElement; + this.button = formElement.querySelector('input[type="submit"], button[type="submit"]'); + this.loadingInterval = 0; + this.character = '\u00B7'; // · middle dot character + + if (this.button) { + this.originalButton = this.button.cloneNode(true); + } +} + +Loader.prototype.setCharacter = function (c) { + this.character = c; +}; + +Loader.prototype.start = function () { + if (this.button) { + this.button.disabled = true; // Avoid multiple submits + + // loading text + const loadingText = this.button.getAttribute('data-loading-text'); + if (loadingText) { + setButtonText(this.button, loadingText); + return; + } + + // Show AJAX loader + const styles = window.getComputedStyle(this.button); + this.button.style.width = styles.width; + setButtonText(this.button, this.character); + this.loadingInterval = window.setInterval(this.tick.bind(this), 500); + } else { + this.form.style.opacity = '0.5'; + } +}; + +Loader.prototype.tick = function () { + // count chars, start over at 5 (3 dots + 2 spaces) + const text = getButtonText(this.button); + const loadingChar = this.character; + setButtonText(this.button, text.length >= 5 ? loadingChar : text + ' ' + loadingChar); +}; + +Loader.prototype.stop = function () { + if (this.button) { + this.button.disabled = false; + this.button.style.width = this.originalButton.style.width; + const text = getButtonText(this.originalButton); + setButtonText(this.button, text); + window.clearInterval(this.loadingInterval); + } else { + this.form.style.opacity = ''; + } +}; + +module.exports = Loader; diff --git a/wp-content/themes/CCV/resources/assets/scripts/forms/forms.js b/wp-content/themes/CCV/resources/assets/scripts/forms/forms.js new file mode 100644 index 0000000..dcb7a0f --- /dev/null +++ b/wp-content/themes/CCV/resources/assets/scripts/forms/forms.js @@ -0,0 +1,111 @@ +//=== Cube Forms +// Inspired by HTMLForms: https://github.com/ibericode/html-forms/blob/master/assets/browserify/public.js + +import Loader from './form-loading-indicator'; + +const config = window.cube_forms_config || {}; + +function clearFormMessages (formEl) { + const messageElements = formEl.querySelectorAll('.cube-form-message'); + [].forEach.call(messageElements, (el) => { + el.parentNode.removeChild(el); + }); +} + +function addFormMessage (formEl, message, clear) { + const msgElement = document.createElement('div'); + msgElement.className = 'cube-form-message cube-form-message-' + message.type; + msgElement.innerHTML = message.text; // uses innerHTML because we allow some HTML strings in the message settings + msgElement.role = 'alert'; + + let wrapperElement = formEl.querySelector('.cube-form-messages'); + + // If messages wrapper is missing, add it + if (!wrapperElement) { + wrapperElement = document.createElement('div'); + wrapperElement.className = 'cube-form-messages'; + formEl.appendChild(wrapperElement); + console.log('wrapper was missing...', wrapperElement); + } + + if (clear) { + wrapperElement.innerHTML = ''; + } + + wrapperElement.appendChild(msgElement); + wrapperElement.classList.remove('hidden'); // Unhide messages +} + +function handleSubmitEvents (e) { + const formEl = e.target; + if (formEl.className.indexOf('cube-form') < 0) { + return + } + e.preventDefault(); // always prevent default because we only want to send via AJAX + submitForm(formEl) +} + +function submitForm(form) { + const formData = new FormData(form); + + formData.append('action', config.action); + formData.append('nonce', config.nonce); + + let request = new XMLHttpRequest(); + request.onreadystatechange = createRequestHandler(form); + request.open('POST', config.ajax_url, true); + request.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + request.send(formData); + request = null; +} + +function createRequestHandler (formEl) { + const loader = new Loader(formEl); + loader.start(); + + return function () { + // Is the XHR request complete? https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState + if (this.readyState === 4) { + let response; + loader.stop(); + + if (this.status >= 200 && this.status < 400) { + + try { + response = JSON.parse(this.responseText); + } catch (error) { + console.log('Cube Forms: failed to parse AJAX response.\n\nError: "' + error + '"'); + return; + } + + // Show form message + if (response.message) { + let clearMessages = response.message.type === 'error'; + addFormMessage(formEl, response.message, clearMessages); + } + + // Should we hide form? + if (response.hide_form) { + formEl.querySelector('.cube-form-content').style.display = 'none' + } + + // Should we redirect? + if (response.redirect_url) { + window.location = response.redirect_url + } + + // Clear form + // if (!response.error) { + // formEl.reset() + // } + } else { + // Server error :( + console.log(this.responseText) + } + } + } +} + + + +document.addEventListener('submit', handleSubmitEvents, false); // useCapture=false to ensure we bubble upwards (and thus can cancel propagation) diff --git a/wp-content/themes/CCV/resources/assets/scripts/forms/parsley-setup.js b/wp-content/themes/CCV/resources/assets/scripts/forms/parsley-setup.js new file mode 100644 index 0000000..7ecda22 --- /dev/null +++ b/wp-content/themes/CCV/resources/assets/scripts/forms/parsley-setup.js @@ -0,0 +1,28 @@ +// ParsleyJS Setup - to be included after main library that will be copied from node_modules +// Ref: https://stackoverflow.com/a/30122442 +(function($) { + + const parsleyConfig = { + classHandler: function(parsleyField) { + let fieldWrapper = parsleyField.$element.parents('.form-field'); + return (fieldWrapper.length > 0) ? fieldWrapper : parsleyField; + }, + + errorsContainer: function(parsleyField) { + let inputWrapper = parsleyField.$element.parents('.form-field-input'); + return (inputWrapper.length > 0) ? inputWrapper : parsleyField; + } + }; + + $('.cube-form').parsley(parsleyConfig); + + // On validation errors, scroll to the first invalid input + $.listen('parsley:field:error', function() { + + let firstErrorOffset = $('.parsley-error').first().offset(); + firstErrorOffset.top = Math.max(0, firstErrorOffset.top - 50); // Scroll a bit above the element + + window.scrollTo({...firstErrorOffset, ...{behavior: 'smooth'}}); + }); + +})(jQuery); diff --git a/wp-content/themes/CCV/resources/assets/styles/components/forms.styl b/wp-content/themes/CCV/resources/assets/styles/components/forms.styl index 7b0bdec..f87cdc5 100644 --- a/wp-content/themes/CCV/resources/assets/styles/components/forms.styl +++ b/wp-content/themes/CCV/resources/assets/styles/components/forms.styl @@ -108,8 +108,8 @@ input[type="submit"] margin-bottom: 0.75em .form-field-input - > *:not(:first-child) - margin-left: 1.5em + > *:not(:last-child) + margin-right: 1.5em label white-space: nowrap // Ensure text stays beside input control @@ -145,16 +145,10 @@ input[type="submit"] flex-basis: 100% -// HTML Forms Plugin styling -.hf-message - margin: 2em 0 - border: 2px solid - padding: 1em - background-color: #ccc - - &-success - background-color: #e8f5e9 - color: #4caf50 +// Cube Forms +.cube-form-message + &-error + @apply text-red // ParsleyJS validation .parsley-errors-list @@ -164,7 +158,6 @@ input[type="submit"] left: 0 bottom: -2em padding: 0 - margin-left: 0 !important font-size: 0.7em white-space: nowrap diff --git a/wp-content/themes/CCV/resources/views/forms/common/wrapper.blade.php b/wp-content/themes/CCV/resources/views/forms/common/wrapper.blade.php index 3f46456..eecb3d4 100644 --- a/wp-content/themes/CCV/resources/views/forms/common/wrapper.blade.php +++ b/wp-content/themes/CCV/resources/views/forms/common/wrapper.blade.php @@ -9,6 +9,11 @@ - @includeIf("forms/$form_name") +
+ @includeIf("forms/$form_name") +
+ + {{-- Placeholder for form messages / response --}} + diff --git a/wp-content/themes/CCV/resources/views/forms/consultation.blade.php b/wp-content/themes/CCV/resources/views/forms/consultation.blade.php index b44d8c3..300b0c5 100644 --- a/wp-content/themes/CCV/resources/views/forms/consultation.blade.php +++ b/wp-content/themes/CCV/resources/views/forms/consultation.blade.php @@ -261,7 +261,7 @@ ]) !!} {!! $form->field('message', ['title_class' => 'font-light']) !!} -
+
- {!! $form->button(__('Envoyer votre demande', 'ccv'), ['class' => 'btn block mt-1v ml-auto']) !!} + {!! + $form->button(__('Envoyer votre demande', 'ccv'), [ + 'class' => 'btn block mt-1v ml-auto', + 'loading_text' => __('Envoi en cours...', 'ccv'), + ]) + !!}
+ +{{-- Custom classes for form message container --}} +@push('message_class') + py-2v pl-4v pr-3v xs:px-2v +@endpush diff --git a/wp-content/themes/CCV/resources/views/forms/contact.blade.php b/wp-content/themes/CCV/resources/views/forms/contact.blade.php new file mode 100644 index 0000000..4b94f05 --- /dev/null +++ b/wp-content/themes/CCV/resources/views/forms/contact.blade.php @@ -0,0 +1,9 @@ +{{-- CONTACT FORM --}} +@php /* @var $form \Cube\Forms\Contact */ @endphp + +
+ {!! $form->field('last-name', ['show_title' => false]) !!} + {!! $form->field('first-name', ['show_title' => false]) !!} + {!! $form->field('phone', ['show_title' => false]) !!} + {!! $form->button(__('Contactez-moi', 'ccv'), ['class' => 'btn mt-8']) !!} +
diff --git a/wp-content/themes/CCV/resources/views/forms/training.blade.php b/wp-content/themes/CCV/resources/views/forms/training.blade.php index d20c5d9..170bdea 100644 --- a/wp-content/themes/CCV/resources/views/forms/training.blade.php +++ b/wp-content/themes/CCV/resources/views/forms/training.blade.php @@ -127,6 +127,16 @@
- {!! $form->button(__('Envoyer votre demande', 'ccv'), ['class' => 'btn text-lg block mt-2v mx-auto']) !!} + {!! + $form->button(__('Envoyer votre demande', 'ccv'), [ + 'class' => 'btn text-lg block mt-2v mx-auto', + 'loading_text' => __('Envoi en cours...', 'ccv'), + ]) + !!}
+ +{{-- Custom classes for form message container --}} +@push('message_class') + py-2v pl-4v pr-3v xs:px-2v +@endpush diff --git a/wp-content/themes/CCV/resources/views/layouts/app.blade.php b/wp-content/themes/CCV/resources/views/layouts/app.blade.php index ebffaaf..c866f85 100644 --- a/wp-content/themes/CCV/resources/views/layouts/app.blade.php +++ b/wp-content/themes/CCV/resources/views/layouts/app.blade.php @@ -7,8 +7,8 @@ @include('partials.header') -
-
+
+
@yield('content')
diff --git a/wp-content/themes/CCV/resources/views/partials/footer.blade.php b/wp-content/themes/CCV/resources/views/partials/footer.blade.php index c2c5d48..7804bb0 100644 --- a/wp-content/themes/CCV/resources/views/partials/footer.blade.php +++ b/wp-content/themes/CCV/resources/views/partials/footer.blade.php @@ -1,20 +1,18 @@ -