'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();
$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();
private $forms = [ // All available forms
'consultation' => Consultation::class,
'training' => Training::class,
+ 'contact' => Contact::class,
];
private $fields = [];
+ private $config;
private $data;
public $form_title = '';
//=== 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(
//=== 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)
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
// 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;
}
$data = [];
$to = $this->destination;
- $from = 'CCV <no-reply@ccv-montpellier.fr';
+ $from = 'CCV <no-reply@ccv-montpellier.fr>';
$subject = $this->form_title;
$content_type = 'text/html';
$charset = get_bloginfo('charset');
$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();
/**
* HTML (submit) button
- * @param $text Button label
+ * @param string $text Button label
* @param array $settings
* @return string
*/
$default_settings = [
'type' => 'submit',
'class' => 'btn',
+ 'loading_text' => '',
];
$settings = array_merge($default_settings, $settings);
- return '<button type="'. $settings['type'] .'" class="'. $settings['class'] .'">'. $text .'</button>';
+ return '<button type="'. $settings['type'] .'" class="'. $settings['class'] .'" data-loading-text="'. $settings['loading_text'] .'">'. $text .'</button>';
}
--- /dev/null
+<?php
+
+namespace Cube\Forms;
+
+class Contact extends Base
+{
+ public function __construct() {
+ $this->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);
+ }
+}
<?php wp_body_open(); ?>
<?php do_action('get_header'); ?>
-<div id="app">
+<div id="app" class="flex flex-col min-h-screen">
<?php echo \Roots\view(\Roots\app('sage.view'), \Roots\app('sage.data'))->render(); ?>
</div>
+++ /dev/null
-//=== 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)
--- /dev/null
+'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;
--- /dev/null
+//=== 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)
--- /dev/null
+// 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);
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
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
left: 0
bottom: -2em
padding: 0
- margin-left: 0 !important
font-size: 0.7em
white-space: nowrap
<input type="hidden" name="config" value="{{ $config }}">
- @includeIf("forms/$form_name")
+ <div class="cube-form-content">
+ @includeIf("forms/$form_name")
+ </div>
+
+ {{-- Placeholder for form messages / response --}}
+ <div class="cube-form-messages hidden @stack('message_class')"></div>
</form>
]) !!}
{!! $form->field('message', ['title_class' => 'font-light']) !!}
- <div class="mt-4">
+ <div class="mt-3">
<div class="custom-checkbox mb-1">
<label>
<input type="checkbox" name="send-to-team" value="{{ __('Oui') }}" checked class="ml-2">
</div>
- {!! $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'),
+ ])
+ !!}
</div>
+
+{{-- Custom classes for form message container --}}
+@push('message_class')
+ py-2v pl-4v pr-3v xs:px-2v
+@endpush
--- /dev/null
+{{-- CONTACT FORM --}}
+@php /* @var $form \Cube\Forms\Contact */ @endphp
+
+<div class="spaced">
+ {!! $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']) !!}
+</div>
</div>
- {!! $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'),
+ ])
+ !!}
</div>
+
+{{-- Custom classes for form message container --}}
+@push('message_class')
+ py-2v pl-4v pr-3v xs:px-2v
+@endpush
@include('partials.header')
-<div class="wrapper flex flex-col min-h-screen">
- <main class="flex-grow min-h-full bg-white">
+<div class="wrapper w-full flex-grow">
+ <main>
@yield('content')
</main>
-<footer class="site-footer bg-purple-dark text-white antialiased pt-2v pb-1v">
- <div class="container">
- <div class="site-footer-cols">
- <div class="site-footer-col">
- @php(dynamic_sidebar('sidebar-footer-1'))
- </div>
- <div class="site-footer-col">
- @php(dynamic_sidebar('sidebar-footer-2'))
- </div>
- <div class="site-footer-col">
- @php(dynamic_sidebar('sidebar-footer-3'))
- </div>
+<footer class="site-footer container bg-purple-dark text-white antialiased pt-2v pb-1v">
+ <div class="site-footer-cols">
+ <div class="site-footer-col">
+ @php(dynamic_sidebar('sidebar-footer-1'))
</div>
-
- {{-- Copyright message and links --}}
- <div class="pl-4v sm:pl-2v pr-2v text-sm" style="color:#9796a0">
- @php(dynamic_sidebar('sidebar-footer-copyright'))
+ <div class="site-footer-col">
+ @php(dynamic_sidebar('sidebar-footer-2'))
+ </div>
+ <div class="site-footer-col">
+ @php(dynamic_sidebar('sidebar-footer-3'))
</div>
</div>
+
+ {{-- Copyright message and links --}}
+ <div class="pl-4v sm:pl-2v pr-2v text-sm" style="color:#9796a0">
+ @php(dynamic_sidebar('sidebar-footer-copyright'))
+ </div>
</footer>
open: false,
});
-// Styles
-mix.stylus(src`styles/app.styl`, 'styles', stylusConfig).options(stylusOptions).tailwind();
-
// Separate resources for Flatpickr since it is only needed on specific pages
mix.stylus(src`styles/flatpickr.styl`, 'styles');
mix.js(src`scripts/flatpickr.js`, 'scripts/flatpickr/trigger.js'); // Trigger function (separated so locales can load first)
['ar', 'fr', 'ru']
.forEach(locale => mix.copy(`node_modules/flatpickr/dist/l10n/${locale}.js`, publicPath`scripts/flatpickr/locale`));
-// JavaScript
-mix.js(src`scripts/app.js`, 'scripts')
- .js(src`scripts/forms.js`, 'scripts')
- .js(src`scripts/consultation.js`, 'scripts')
- .js(src`scripts/header-slideshow.js`, 'scripts')
- .js(src`scripts/link-carousel.js`, 'scripts')
- .js(src`scripts/testimonial-carousel.js`, 'scripts')
- .js(src`scripts/customizer.js`, 'scripts')
- .extract(['mmenu-light']); // Extract any libraries that will rarely change to a vendor.js file
+// Form handling
+mix.js(src`scripts/forms/forms.js`, 'scripts/forms');
// ParsleyJS validator
-mix.copy('node_modules/parsleyjs/dist/parsley.min.js', publicPath`scripts/parsley`);
+const parsleyPath = publicPath`scripts/forms/parsley`;
+mix.copy('node_modules/parsleyjs/dist/parsley.min.js', parsleyPath);
+mix.js(src`scripts/forms/parsley-setup.js`, parsleyPath);
// Set which locales to copy
['ar', 'fr', 'ru']
- .forEach(locale => mix.copy(`node_modules/parsleyjs/dist/i18n/${locale}.js`, publicPath`scripts/parsley/locale`));
+ .forEach(locale => mix.copy(`node_modules/parsleyjs/dist/i18n/${locale}.js`, `${parsleyPath}/locale`));
// Lity lightbox
mix.copy('node_modules/lity/dist/lity.js', publicPath`scripts`);
mix.stylus(src`styles/components/lity-lightbox.styl`, 'styles/lity.css', stylusConfig).options(stylusOptions).tailwind();
+//==================
+
+// Main JavaScript
+mix.js(src`scripts/app.js`, 'scripts')
+ .js(src`scripts/consultation.js`, 'scripts')
+ .js(src`scripts/header-slideshow.js`, 'scripts')
+ .js(src`scripts/link-carousel.js`, 'scripts')
+ .js(src`scripts/testimonial-carousel.js`, 'scripts')
+ .js(src`scripts/customizer.js`, 'scripts')
+ .extract(['mmenu-light']); // Extract any libraries that will rarely change to a vendor.js file
+
+// Main Styles
+mix.stylus(src`styles/app.styl`, 'styles', stylusConfig).options(stylusOptions).tailwind();
+
// Assets
mix.copyDirectory(src`images`, publicPath`images`)
.copyDirectory(src`fonts`, publicPath`fonts`);
extractorPattern: /[A-Za-z0-9-_:!/]+/g, // Tailwind patterns to include classes with special characters like !/:
globs: [
+ path.join(__dirname, './*.php'), // Process root PHP files like index.php and functions.php
path.join(__dirname, '../../mu-plugins/cube/**/*.php'), // Some classes (eg. for widgets) might be present only here
],