From 2dc22d004113831952438c4ce92553bfcaae3c63 Mon Sep 17 00:00:00 2001 From: Stephen Cameron Date: Tue, 5 May 2020 21:16:51 +0200 Subject: [PATCH] File uploads / e-mail attachments, responsive tweaks and UX improvements. WIP #3383 @9.75 --- wp-content/mu-plugins/cube/src/Forms/Base.php | 113 ++++++++++++++++-- .../cube/src/Forms/Builder/Fields/File.php | 36 ++++++ .../cube/src/Forms/Builder/Form.php | 23 ++++ .../mu-plugins/cube/src/Forms/Training.php | 6 +- .../assets/scripts/forms/file-upload.js | 26 ++++ .../resources/assets/scripts/forms/forms.js | 7 ++ .../assets/styles/common/spacing.styl | 2 + .../assets/styles/components/forms.styl | 39 ++++-- .../assets/styles/components/headings.styl | 2 + .../views/forms/consultation.blade.php | 28 ++--- .../resources/views/forms/training.blade.php | 45 ++++--- .../resources/views/partials/footer.blade.php | 2 +- wp-content/themes/CCV/tailwind.config.js | 1 + 13 files changed, 281 insertions(+), 49 deletions(-) create mode 100644 wp-content/mu-plugins/cube/src/Forms/Builder/Fields/File.php create mode 100644 wp-content/themes/CCV/resources/assets/scripts/forms/file-upload.js diff --git a/wp-content/mu-plugins/cube/src/Forms/Base.php b/wp-content/mu-plugins/cube/src/Forms/Base.php index c8671e4..69a13a4 100644 --- a/wp-content/mu-plugins/cube/src/Forms/Base.php +++ b/wp-content/mu-plugins/cube/src/Forms/Base.php @@ -120,14 +120,15 @@ class Base return $this->forms; } - public function get_form_data() { - $data = $_POST; + public function get_request_data() { + $data = $this->sanitize($_POST); - // Include successful file uploads in data - if (!empty($_FILES)) { - foreach ($_FILES as $field_name => $file) { - if ($file['error'] === UPLOAD_ERR_OK) { - $data[$field_name] = $file; + $files = $this->normalise_files($_FILES); + + foreach ($files as $field_name => $uploads) { + foreach ($uploads as $upload) { + if ($upload['error'] === UPLOAD_ERR_OK) { + $data[$field_name][] = $upload['name']; // Record the filenames of successful uploads } } } @@ -135,6 +136,54 @@ class Base return $data; } + // Normalise PHP's $_FILES array so the structure is consistent between single and multi-file uploads + // This also gives a more sensible structure than PHP's default because each upload is grouped + // Ref: https://www.php.net/manual/en/features.file-upload.post-method.php#118858 + protected function normalise_files($files = []) { + + if (empty($files)) return []; + + $normalised_array = []; + + foreach($files as $index => $file) { + + // Single upload only + if (!is_array($file['name'])) { + $normalised_array[$index][] = $file; + continue; + } + + // Multi-file upload + foreach($file['name'] as $idx => $name) { + $normalised_array[$index][$idx] = [ + 'name' => $name, + 'type' => $file['type'][$idx], + 'tmp_name' => $file['tmp_name'][$idx], + 'error' => $file['error'][$idx], + 'size' => $file['size'][$idx] + ]; + } + + } + return $normalised_array; + } + + // Temporarily hook directly into PHPMailer so we can attach files AND set their names + // Ref: https://stackoverflow.com/a/57778561 + public function attach_files(\PHPMailer $PHPMailer) { + + $files = $this->normalise_files($_FILES); + + foreach ($files as $upload) { + foreach ($upload as $file) { + if ($file['error'] === UPLOAD_ERR_OK) { + $PHPMailer->addAttachment($file['tmp_name'], $file['name']); + } + } + } + + } + public function handle_ajax_request() { // Here we handle the form ajax requests, ensuring they're valid and loading the appropriate sub-class @@ -170,7 +219,7 @@ class Base /* @var $form Base */ $form = new $this->forms[$form_name]; $form->init(); - $form->data = $this->get_form_data(); + $form->data = $form->get_request_data(); $form->config = $config; $form->set_destination($config['destination']); $form->process(); @@ -198,7 +247,6 @@ class Base // Gather filled fields into label/value pairs foreach ($this->fields as $field_name => $field) { - // TODO: handle file upload fields - should it be removed from HTML and just attached? Or include the filename reference? if ($value = $this->get_data($field_name)) { $data[$field->get_title()] = $value; } @@ -206,7 +254,10 @@ class Base $message = view('forms.common.email', compact('data', 'subject')); + // Hook directly into WPMailer temporarily so we can add attachments and set their proper filenames + add_action('phpmailer_init', [$this, 'attach_files']); $success = wp_mail($to, $subject, $message, $headers); + remove_action('phpmailer_init', [$this, 'attach_files']); if ($success) { wp_send_json([ @@ -285,4 +336,48 @@ class Base return $this->fields; } + /** + * Sanitize array with values before sending. Can be called recursively. + * + * @param mixed $value + * @return mixed + */ + public function sanitize( $value ) { + if ( is_string( $value ) ) { + // do nothing if empty string + if ( $value === '' ) { + return $value; + } + + // strip slashes + $value = stripslashes( $value ); + + // strip all whitespace + $value = trim( $value ); + + // convert & back to & + $value = html_entity_decode( $value, ENT_NOQUOTES ); + } elseif ( is_array( $value ) || is_object( $value ) ) { + $new_value = array(); + $vars = is_array( $value ) ? $value : get_object_vars( $value ); + + // do nothing if empty array or object + if ( count( $vars ) === 0 ) { + return $value; + } + + foreach ( $vars as $key => $sub_value ) { + // strip all whitespace & HTML from keys (!) + $key = trim( strip_tags( $key ) ); + + // sanitize sub value + $new_value[ $key ] = $this->sanitize( $sub_value ); + } + + $value = is_object( $value ) ? (object) $new_value : $new_value; + } + + return $value; + } + } diff --git a/wp-content/mu-plugins/cube/src/Forms/Builder/Fields/File.php b/wp-content/mu-plugins/cube/src/Forms/Builder/Fields/File.php new file mode 100644 index 0000000..f5d03f4 --- /dev/null +++ b/wp-content/mu-plugins/cube/src/Forms/Builder/Fields/File.php @@ -0,0 +1,36 @@ +multiple = $is_multiple; + return $this; + } + + public function is_multiple() { + return $this->multiple; + } + + public function render($settings) { + + // Handle multiple uploads if needed + $multiple_attribute = $this->is_multiple() ? 'multiple' : ''; + $field_name = $this->is_multiple() ? $this->get_name() .'[]' : $this->get_name(); // Give PHP array for multiple files + + $res = 'get_name() .'">'. $this->get_title() .''; + + return $res; + } +} diff --git a/wp-content/mu-plugins/cube/src/Forms/Builder/Form.php b/wp-content/mu-plugins/cube/src/Forms/Builder/Form.php index d26a4c4..74e47b2 100644 --- a/wp-content/mu-plugins/cube/src/Forms/Builder/Form.php +++ b/wp-content/mu-plugins/cube/src/Forms/Builder/Form.php @@ -11,6 +11,12 @@ class Form extends Base $this->fields = $fields; } + /** + * Populate missing settings with defaults + * @param Field $field + * @param array $settings + * @return array + */ public function process_settings($field, $settings = []) { $default_settings = [ @@ -68,6 +74,23 @@ class Form extends Base return '
'. $res .'
'; } + /** + * Get the bare field input without the wrappers + * @param $name + * @param array $settings + * @return bool|string + */ + public function input($name, $settings = []) { + + /* @var $field Field */ + $field = $this->get_field($name); + if (!$field) return false; + + $settings = $this->process_settings($field, $settings); + + return $field->render($settings); + } + /** * Generate field title * @param $name diff --git a/wp-content/mu-plugins/cube/src/Forms/Training.php b/wp-content/mu-plugins/cube/src/Forms/Training.php index 167f9af..ac7fa69 100644 --- a/wp-content/mu-plugins/cube/src/Forms/Training.php +++ b/wp-content/mu-plugins/cube/src/Forms/Training.php @@ -4,6 +4,7 @@ namespace Cube\Forms; use Cube\Forms\Builder\Fields\Date; use Cube\Forms\Builder\Fields\Email; +use Cube\Forms\Builder\Fields\File; use Cube\Forms\Builder\Fields\Radio; use Cube\Forms\Builder\Fields\Text; @@ -57,7 +58,10 @@ class Training extends Base //== TYPE OF TRAINING // Since this question has a complex layout, the options are set in the template (formation.blade.php) - Radio::field('training-type', __('Par quel type de formation au CCV Montpellier êtes-vous intéressé ?', 'ccv')) + Radio::field('training-type', __('Par quel type de formation au CCV Montpellier êtes-vous intéressé ?', 'ccv')), + File::field('attachments', __('Joindre vos documents', 'ccv')) + ->multiple(true) + ->required(false), ]); } diff --git a/wp-content/themes/CCV/resources/assets/scripts/forms/file-upload.js b/wp-content/themes/CCV/resources/assets/scripts/forms/file-upload.js new file mode 100644 index 0000000..089543d --- /dev/null +++ b/wp-content/themes/CCV/resources/assets/scripts/forms/file-upload.js @@ -0,0 +1,26 @@ +// Based on https://tympanus.net/codrops/2015/09/15/styling-customizing-file-inputs-smart-way/ +let inputs = document.querySelectorAll('.file-input'); +inputs.forEach(function(input) { + + let label = input.nextElementSibling; + let labelVal = label.innerHTML; + + input.addEventListener('change', function(e) { + let fileName = ''; + if (this.files && this.files.length > 1) + fileName = (this.getAttribute('data-multiple-caption') || '').replace('{count}', this.files.length); + else + fileName = e.target.value.split('\\').pop(); + + if (fileName) + label.querySelector('span').innerHTML = fileName; + else + label.innerHTML = labelVal; + }); + + // Firefox bug fix + input.addEventListener('focus', function() { input.classList.add('has-focus'); }); + input.addEventListener('blur', function() { input.classList.remove('has-focus'); }); +}); + +module.exports = inputs; diff --git a/wp-content/themes/CCV/resources/assets/scripts/forms/forms.js b/wp-content/themes/CCV/resources/assets/scripts/forms/forms.js index dcb7a0f..055a38e 100644 --- a/wp-content/themes/CCV/resources/assets/scripts/forms/forms.js +++ b/wp-content/themes/CCV/resources/assets/scripts/forms/forms.js @@ -2,6 +2,7 @@ // Inspired by HTMLForms: https://github.com/ibericode/html-forms/blob/master/assets/browserify/public.js import Loader from './form-loading-indicator'; +import './file-upload'; const config = window.cube_forms_config || {}; @@ -34,6 +35,12 @@ function addFormMessage (formEl, message, clear) { wrapperElement.appendChild(msgElement); wrapperElement.classList.remove('hidden'); // Unhide messages + + // Finally, scroll to the message in case it isn't in view + setTimeout(function() { + let wrapperOffsetTop = Math.max(0, wrapperElement.getBoundingClientRect().top + window.scrollY - 100); // Scroll just above it to give some space + window.scrollTo({...{top: wrapperOffsetTop}, ...{behavior: 'smooth'}}); + }, 50); } function handleSubmitEvents (e) { diff --git a/wp-content/themes/CCV/resources/assets/styles/common/spacing.styl b/wp-content/themes/CCV/resources/assets/styles/common/spacing.styl index b6aa4ab..1c4ed69 100644 --- a/wp-content/themes/CCV/resources/assets/styles/common/spacing.styl +++ b/wp-content/themes/CCV/resources/assets/styles/common/spacing.styl @@ -5,6 +5,8 @@ margin-top: 1.5em .spaced-lg > * + * margin-top: 2em + .spaced-none > * + * + margin-top: 0 .spaced-horizontal > * + * margin-left: 0.75em 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 2a5693a..c58c986 100644 --- a/wp-content/themes/CCV/resources/assets/styles/components/forms.styl +++ b/wp-content/themes/CCV/resources/assets/styles/components/forms.styl @@ -85,10 +85,16 @@ input[type="submit"] box-shadow: inset 0 0 0 4px #fff // Inner border for checkbox cursor: pointer - .form-label:before - margin-right: 0.5em - .form-label-reversed:after - margin-left: 0.5em + .form-label + display: flex + align-items: center + + &:before + flex-shrink: 0 + margin-right: 0.5rem + + &-reversed:after + margin-left: 0.5rem input:checked ~ .form-label:before, input:checked ~ .form-label-reversed:after @@ -101,6 +107,13 @@ input[type="submit"] transform: translateY(-50%) visibility: hidden +// File upload input label +.file-input-label-text + display: block + overflow: hidden + white-space: nowrap + text-overflow: ellipsis + cursor: pointer // Common form styling .form-field-title @@ -108,11 +121,15 @@ input[type="submit"] margin-bottom: 0.75em .form-field-input - > *:not(:first-child) - margin-left: 1.5em + > label:not(:last-child) + margin-right: 1.5em + margin-bottom: 0.25em label + display: inline-block white-space: nowrap // Ensure text stays beside input control + +below(500px) + white-space: normal .form-field-date &.date-field-compact input @@ -127,20 +144,26 @@ input[type="submit"] justify-content: space-between .form-cols-2 + // Offset the margins on children to avoid bigger gap at bottom + margin-bottom: -2em + > * flex-basis: 47% + margin-bottom: 2em +below(1024px) flex-basis: 100% - margin-bottom: 2em .form-cols-4 + // Offset the margins on children to avoid bigger gap at bottom + margin-bottom: -2em + > * flex-basis: 23% + margin-bottom: 2em +below(1400px) flex-basis: 47% - margin-bottom: 2em +below(600px) flex-basis: 100% diff --git a/wp-content/themes/CCV/resources/assets/styles/components/headings.styl b/wp-content/themes/CCV/resources/assets/styles/components/headings.styl index e472779..77dc684 100644 --- a/wp-content/themes/CCV/resources/assets/styles/components/headings.styl +++ b/wp-content/themes/CCV/resources/assets/styles/components/headings.styl @@ -11,6 +11,8 @@ h1, .h1 h2, .h2 @apply text-2xl + +below(500px) + @apply text-xl // Pink dash h1, .h1, h2, .h2, .decorated 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 a81b6e2..a251eeb 100644 --- a/wp-content/themes/CCV/resources/views/forms/consultation.blade.php +++ b/wp-content/themes/CCV/resources/views/forms/consultation.blade.php @@ -127,12 +127,12 @@ {{-- IMAGES FROM CD --}}
- +
-
+
{{ __('Vos images sont sur un CD ?', 'ccv') }}
-

+

{{ __("Envoyez-nous l'ensemble des fichiers contenus sur votre CD :", "ccv") }}

@@ -176,12 +176,12 @@ {{-- IMAGES ONLINE --}}
- +
-
+
{{ __('Vous avez reçu un lien pour consulter vos images en ligne ?', 'ccv') }}
-

+

{{ __('Collez votre lien ci-dessous ainsi que vos identifiant et mot de passe :', 'ccv') }}

@@ -191,12 +191,12 @@ {{-- IMAGES FROM PHONE --}}
- +
-
+
{{ __('Vous remplissez cette demande depuis votre téléphone ?', 'ccv') }}
-

+

{{ __('Prenez vos images en photo et envoyez-les directement depuis votre téléphone :', 'ccv') }}

{{ __('Parcourir') }} @@ -206,12 +206,12 @@ {{-- IMAGES SENT BY POST --}}
- +
-
+
{{ __('Vous pouvez aussi nous envoyer vos images par courrier :', 'ccv') }}
-

+

CCV MONTPELLIER
AVIS MEDICAL
Clinique du parc - 50 Rue Emile Combes,
@@ -259,9 +259,9 @@ 'placeholder' => '', 'field_after' => __('ans', 'ccv') ]) !!} - {!! $form->field('message', ['title_class' => 'font-light']) !!} + {!! $form->field('message', ['class' => 'mt-6', 'title_class' => 'font-light']) !!} -

+