Статья, в которой рассмотрим, как сделать всплывающую форму обратной связи для сайта на php с капчей и возможностью прикрепления к ней файлов.

Базой для разработки формы обратной связи, которая будет доступна пользователю внутри модального окна, является проект, который можно открыть по адресу: http://itchief.ru/lessons/php/feedback-form-for-website. Кроме этой ссылки, данный проект также доступен на гитхабе feedback-form.

Представленная в этом проекте форма имеет следующие характеристики:

  • работает на основе технологии ajax, т.е. без перезагрузки страницы;
  • оформление формы выполнено на основе фреймворка Bootstrap 3;
  • наличие капчи, а также прикрепления к форме любого количества файлов (по умолчанию 5);
  • проверка ошибок, как на стороне клиента, так и на стороне сервера;
  • серверная составляющая формы обратной связи работает на php;
  • отправка данных пользователя, включая файлы на почту (файлы можно прикрепить к письму в виде вложения, так и в теле письма с помощью ссылок);
  • сохранение информации, отставленной пользователем, в файл message.txt.

Для изменения проекта, а именно возможности отображения формы обратной связи во всплывающем окне, в него необходимо будет внести следующее:

  • добавить кнопку, предназначенную для открытия формы в модальном окне;
  • добавить в HTML код структуру модального окна из фреймворка Bootstrap;
  • перенести в тело модального окна HTML код формы обратной связи;
  • добавить в скрипт script.js код, который будет очищать успешную отправленную форму для того, чтобы можно было отправить ещё одну.

Т.е. изменения потребуется внести 2 файла: index.html и script.js.

HTML файл, содержащий форму обратной связи

Откроем файл index.html и внесём в него следующие изменения:

1. Кнопка, для вызова всплывающего окна:

<div class="text-center">
  <!-- Кнопка, для открытия модального окна -->
    <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#feedbackForm">
      Написать сообщение
  </button>
</div>

2. Выезжающее окно:

<!-- Форма обратной связи в модальном окне -->
  <div class="modal fade" id="feedbackForm" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
    <div class="modal-dialog" role="document">
      <div class="modal-content">
        <div class="modal-header">
          <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
          <h4 class="modal-title" id="myModalLabel">Форма обратной связи</h4>
        </div>
        <!-- Тело модального окна -->        
        <div class="modal-body">
          ...
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-default" data-dismiss="modal">Закрыть</button>
        </div>
      </div>
    </div>
  </div>

После этого перенесём код формы обратной связи в модальное окно.

В итоге файл index.html будет иметь следующее содержимое:

<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <title>Форма обратной связи</title>
  <link rel="stylesheet" href="/feedback/css/bootstrap.min.css">
  <style>
    input:-webkit-autofill {
        -webkit-box-shadow: 0 0 0 1000px white inset !important;
    }
  </style>
</head>
<body>
  <h1 class="text-center">Форма обратной связи</h1>
  <hr>
  
  <div class="text-center">
    <!-- Кнопка, для открытия модального окна -->
    <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#feedbackForm">
      Написать сообщение
    </button>
  </div>

  <!-- Форма обратной связи в модальном окне -->
  <div class="modal fade" id="feedbackForm" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
    <div class="modal-dialog" role="document">
      <div class="modal-content">
        <div class="modal-header">
          <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
          <h4 class="modal-title" id="myModalLabel">Форма обратной связи</h4>
        </div>
        
        <div class="modal-body">

            <!-- Сообщение, отображаемое в случае успешной отправки данных -->
            <div class="alert alert-success hidden" role="alert" id="msgSubmit" style="margin-bottom: 0px;">
              <strong>Внимание!</strong> Ваше сообщение отправлено.
            </div>

            <!-- Форма обратной связи -->
            <form id="messageForm" enctype="multipart/form-data">
              <div class="row">
                <div id="error" class="col-sm-12" style="color: #ff0000; margin-top: 5px; margin-bottom: 5px;"></div>
                <!-- Имя и email пользователя -->
                <div class="col-sm-6">
                  <!-- Имя пользователя -->
                  <div class="form-group has-feedback">
                    <label for="name" class="control-label">Введите ваше имя:</label>
                    <input type="text" id="name" name="name" class="form-control" required="required" value="" placeholder="Например, Иван Иванович" minlength="2" maxlength="30">
                    <span class="glyphicon form-control-feedback"></span>
                  </div>
                </div>
                <div class="col-sm-6">
                  <!-- Email пользователя -->
                  <div class="form-group has-feedback">
                    <label for="email" class="control-label">Введите адрес email:</label>
                    <input type="email" id="email" name="email" class="form-control" required="required"  value="" placeholder="Например, ivan@mail.ru" maxlength="30">
                    <span class="glyphicon form-control-feedback"></span>
                  </div>
                </div>
              </div>
              <!-- Сообщение пользователя -->
              <div class="form-group has-feedback">
                <label for="message" class="control-label">Введите сообщение:</label>
                <textarea id="message" class="form-control" rows="3" placeholder="Введите сообщение, состоящее не менее чем из 20 символов и не более чем из 500" minlength="20" maxlength="500" required="required"></textarea>
              </div>
              <!-- Файлы, для прикрепления к форме -->
              <div class="form-group">
                <p style="font-weight: 700;">Прикрепить к сообщению файлы (максимум <span id="countFiles"></span>):</p>
                <!-- Файл -->
                <input type="file" name="images[]">
                <p style="margin-top: 3px; margin-bottom: 3px; color: #ff0000;"></p>
              </div>
              <hr style="margin-top: 3px; margin-bottom: 3px;">
              <!-- Капча к форме -->
              <!-- Изображение, содержащее код CAPTCHA-->		  
	            <img id="img-captcha" src="/feedback/captcha.php">
              <!--Элемент, запрашивающий новый код CAPTCHA-->
	            <div id="reload-captcha" class="btn btn-default"><i class="glyphicon glyphicon-refresh"></i> Обновить</div>
	            <!--Блок для ввода кода CAPTCHA-->
	            <div class="form-group has-feedback">
                <label id="label-captcha" for="captcha" class="control-label">Пожалуйста, введите указанный на изображении код:</label>
	              <input id="text-captcha" name="captcha" type="text" class="form-control" required="required" value="" minlength="6" maxlength="6" autocomplete="off">
	              <span class="glyphicon form-control-feedback"></span>
              </div>
              <!-- Кнопка, отправляющая форму по технологии AJAX -->  
              <button name="send-message" type="submit" class="btn btn-primary pull-right">Отправить сообщение</button>
            </form><!-- Конец формы -->
            <div class="clearfix"></div>

        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-default" data-dismiss="modal">Закрыть</button>
        </div>
      </div>
    </div>
  </div>

  <script src="/feedback/js/jquery-1.12.4.min.js"></script>
  <script src="/feedback/js/bootstrap.min.js"></script>
  <script src="/feedback/script.js"></script>

</body>
</html>  

JavaScript файл формы обратной связи

В файле script.js потребуется внести только одну поправку, которая будет после закрывания модального окна выполнять следующее действие:

// при закрытии модального окна
$('#feedbackForm').on('hidden.bs.modal', function () {
  // если форма обратной связи скрыта, то...
  if ($('#messageForm').is(':hidden')) {
    // отобразить форму обратной связи
    $('#messageForm').show();
    // добавить класс hidden к элементу, имеющего id=msgSubmit
    $('#msgSubmit').addClass('hidden');
    $('#msgSubmit').html('<strong>Внимание!</strong> Ваше сообщение отправлено.');
    var files = $('#messageForm').find('input[type="file"]');
    files.eq(0).val('');
    files.eq(0).next('p').text('');
    for (var i=1; i<files.length; i++) {
      files.eq(i).next('p').remove();
      files.eq(i).remove();
    }
    $('#text-captcha').val('');
    $('#img-captcha').attr('src', '/feedback/captcha.php?id=' + Math.random() + '');
    $('#message').val('');
    $('#name').val('');
    $('#email').val('');
    $('#messageForm input,#messageForm textarea').each(function () {
      //найти предков, имеющих класс .form-group (для удаления success/error)
      var formGroup = $(this).parents('.form-group');
      //найти glyphicon (иконка)
      var glyphicon = formGroup.find('.form-control-feedback');
      //удалить у элемента formGroup класс .has-success и .has-error
      formGroup.removeClass('has-success').removeClass('has-error');
      //удлаить у элемента glyphicon класс .glyphicon-ok и .glyphicon-remove
      glyphicon.removeClass('glyphicon-ok').removeClass('glyphicon-remove');
   });      

  }
});

Т.е. проверять, успешно ли отправлена форма, и если это так, то очищать её.

Суммарный код файла script.js:

//после загрузки веб-страницы
$(function () {

  // максимальное количество файлов 
  var countFiles = 5;
  // типы разрешённых файлов
  var typeFile = 'image.*';
  // максимльный размер
  var maxSizeFile = 524288; //512 Кбайт
  // отображаем на форме максимальное количество файлов
  $('#countFiles').text(countFiles);
  // при изменения значения элемента "Выбрать файл"
  $(document).on('change','input[name="images[]"]',function(e){
    // если выбран файл, то добавить ещё элемент "Выбрать файл"
    if ((e.target.files.length>0)&&($(this).next('p').next('input[name="images[]"]').length==0) && ($('input[name="images[]"]').length<countFiles)) {
      $(this).next('p').after('<input type="file" name="images[]"><p style="margin-top: 3px; margin-bottom: 3px; color: #ff0000;"></p>');
    }
    // если выбран файл, то..
    if (e.target.files.length>0) {
      // получить файл
      var file = e.target.files[0];
      // проверить размер файла
      if (file.size>maxSizeFile) {
        $(this).next('p').text('* Файл не будет отправлен, т.к. его размер больше 512Кбайт');
      }
      // проверить тип файла
      else if (!file.type.match(typeFile)) {
        $(this).next('p').text('* Файл не будет отправлен, т.к. его тип не соответствует разрешённому');
      }
      else {
        // убираем сообщение об ошибке
        if ($(this).next('p')) {
          $(this).next('p').text('');
        }
      }
    }
    else {
      // если после изменения файл не выбран, то сообщаем об этом пользователю
      $(this).next('p').text('* Файл не будет отправлен, т.к. он не выбран');
    }
  });

  // при нажатии на кнопку "Обновить", выведим новый код капчи
  $('#reload-captcha').click(function () {
    $('#img-captcha').attr('src', '/feedback/captcha.php?id=' + Math.random() + '');
  });

  // при отправке формы messageForm на сервер (id="messageForm")
  $('#messageForm').submit(function (event) {
    // отменим стандартное действие браузера
    event.preventDefault();
    // заведём переменную, которая будет говорить о том валидная форма или нет
    var formValid = true;

    // перебирём все элементы управления формы (input и textarea) 
    $('#messageForm input,#messageForm textarea').each(function () {

      //если этот элемент капча, то не проверять его
      if ($(this).attr('id') == 'text-captcha') { 
        return true;
      }
      //найти предков, имеющих класс .form-group (для установления success/error)
      var formGroup = $(this).parents('.form-group');
      //найти glyphicon (иконка успеха или ошибки)
      var glyphicon = formGroup.find('.form-control-feedback');
      //валидация данных с помощью HTML5 функции checkValidity
      if (this.checkValidity()) {
        //добавить к formGroup класс .has-success и удалить .has-error
        formGroup.addClass('has-success').removeClass('has-error');
        //добавить к glyphicon класс .glyphicon-ok и удалить .glyphicon-remove
        glyphicon.addClass('glyphicon-ok').removeClass('glyphicon-remove');
      } else {
        //добавить к formGroup класс .has-error и удалить .has-success
        formGroup.addClass('has-error').removeClass('has-success');
        //добавить к glyphicon класс glyphicon-remove и удалить glyphicon-ok
        glyphicon.addClass('glyphicon-remove').removeClass('glyphicon-ok');
        //если элемент не прошёл проверку, то отметить форму как не валидную 
        formValid = false;
      }
    });

    //проверяем элемент, содержащий код капчи
    //1. Получаем значение элемента input, содержащего код капчи
    var captcha = $("#text-captcha").val();
    //2. Если длина кода капчи, которой ввёл пользователь не равно 6,
    //   то сразу отмечаем капчу как невалидную (без отправки на сервер)
    if (captcha.length != 6) {
      // получаем элемент, содержащий капчу
      inputCaptcha = $("#text-captcha");
      //найти предка, имеющего класс .form-group (для установления success/error)
      formGroupCaptcha = inputCaptcha.parents('.form-group');
      //найти glyphicon (иконка успеха или ошибки)
      glyphiconCaptcha = formGroupCaptcha.find('.form-control-feedback');
      //добавить к formGroup класс .has-error и удалить .has-success
      formGroupCaptcha.addClass('has-error').removeClass('has-success');
      //добавить к glyphicon класс glyphicon-remove и удалить glyphicon-ok
      glyphiconCaptcha.addClass('glyphicon-remove').removeClass('glyphicon-ok');
    }

    // форма валидна и длина капчи равно 6 символам, то отправляем форму на сервер (AJAX)
    if ((formValid) && (captcha.length == 6)) {

      // получаем имя, которое ввёл пользователь	
      var name = $("#name").val();
      // получаем email, который ввёл пользователь
      var email = $("#email").val();
      // получаем сообщение, которое ввёл пользователь
      var message = $("#message").val();
      // получаем капчу, которую ввёл пользователь
      var captcha = $("#text-captcha").val();

      // объект, посредством которого будем кодировать форму перед отправкой её на сервер
      var formData = new FormData();
      // добавить в formData значение 'name'=значение_поля_name
      formData.append('name', name);
      // добавить в formData значение 'email'=значение_поля_email
      formData.append('email', email);
      // добавить в formData значение 'message'=значение_поля_message
      formData.append('message', message);
      // добавить в formData файлы
      // получить все элементы с атрибутом name="images[]"
      var images = document.getElementsByName("images[]");
      // перебрать все элементы images с помощью цикла
      for (var i = 0; i < images.length; i++) {
        // получить список файлов элемента input с type="file"
        var fileList = images[i].files;
        // если элемент не содержит файлов, то перейти к следующей
        if (fileList.length > 0) {
          // получить первый файл из списка
          var file = fileList[0];
          // проверить тип файла и размер
          if ((file.type.match('image.*')) && (file.size<524288)) {
            // добавить его (файл (file) с именем file.name) в formData
            formData.append('images[]', file, file.name);
            console.log(file);
          }
        }
      }
      // добавить в formData значение 'captcha'=значение_поля_captcha
      formData.append('captcha', captcha);

      // технология AJAX 
      $.ajax({
        //метод передачи запроса - POST
        type: "POST",
        //URL-адрес запроса 
        url: "/feedback/verify.php",
        //передаваемые данные - formData
        data: formData,
        // не устанавливать тип контента, т.к. используется FormData
        contentType: false,
        // не обрабатывать данные formData
        processData: false,
        // отключить кэширование результатов в браузере
        cache: false,
        //при успешном выполнении запроса
        success: function (data) {
          // разбираем строку JSON, полученную от сервера
          var $data =  JSON.parse(data);
          // устанавливаем элементу, содержащему текст ошибки, пустую строку
          $('#error').text('');

          // если сервер вернул ответ success, то значит двнные отправлены
          if ($data.result == "success") {
            // скрываем форму обратной связи
            $('#messageForm').hide();
            // удаляем у элемента, имеющего id=msgSubmit, класс hidden
            $('#msgSubmit').removeClass('hidden');
          }
          else if ($data.result == "invalidCaptcha") {
            // Если сервер вернул ответ invalidcaptcha, то делаем следующее...

            //получаем элемент, содержащий капчу
            inputCaptcha = $("#text-captcha");
            //найти предка, имеющего класс .form-group (для установления success/error)
            formGroupCaptcha = inputCaptcha.parents('.form-group');
            //найти glyphicon (иконка успеха или ошибки)
            glyphiconCaptcha = formGroupCaptcha.find('.form-control-feedback');
            //добавить к formGroup класс .has-error и удалить .has-success
            formGroupCaptcha.addClass('has-error').removeClass('has-success');
            //добавить к glyphicon класс glyphicon-remove и удалить glyphicon-ok
            glyphiconCaptcha.addClass('glyphicon-remove').removeClass('glyphicon-ok');
            //вывести новый код капча
            $('#img-captcha').attr('src', '/feedback/captcha.php?id=' + Math.random() + '');
            //установить полю ввода капчи пустое значение
            $("#text-captcha").val('');
          } else {
            // Если сервер вернул ответ error, то делаем следующее...
            $('#error').text('Произошли ошибки при отправке формы на сервер.');
            if ($data.files) {
              $('#error').html($('#error').text()+'<br>'+$data.files);
            }
          }
        },
        error: function (request) {
          $('#error').text('Произошла ошибка ' + request.responseText + ' при отправке данных.');
        }
      });
    }
  });

  // при закрытии модального окна
  $('#feedbackForm').on('hidden.bs.modal', function () {
    // если форма обратной связи скрыта, то...
    if ($('#messageForm').is(':hidden')) {
      // отобразить форму обратной связи
      $('#messageForm').show();
      // добавить класс hidden к элементу, имеющего id=msgSubmit
      $('#msgSubmit').addClass('hidden');
      $('#msgSubmit').html('<strong>Внимание!</strong> Ваше сообщение отправлено.');
      var files = $('#messageForm').find('input[type="file"]');
      files.eq(0).val('');
      files.eq(0).next('p').text('');
      for (var i=1; i<files.length; i++) {
        files.eq(i).next('p').remove();
        files.eq(i).remove();
      }
      $('#text-captcha').val('');
      $('#img-captcha').attr('src', '/feedback/captcha.php?id=' + Math.random() + '');
      $('#message').val('');
      $('#name').val('');
      $('#email').val('');
      $('#messageForm input,#messageForm textarea').each(function () {
        //найти предков, имеющих класс .form-group (для удаления success/error)
        var formGroup = $(this).parents('.form-group');
        //найти glyphicon (иконка)
        var glyphicon = formGroup.find('.form-control-feedback');
        //удалить у элемента formGroup класс .has-success и .has-error
        formGroup.removeClass('has-success').removeClass('has-error');
        //удлаить у элемента glyphicon класс .glyphicon-ok и .glyphicon-remove
        glyphicon.removeClass('glyphicon-ok').removeClass('glyphicon-remove');
     });      
    }
  });
});

Скачать форму обратной связи

Бесплатно скачать архив, содержащий форму обртаной связи, можно воспользовавшись следующей ссылкой:

Бесплатная форма обратной связи

На сайте работу формы feedback продемонстрируем с помощью следующих изображений:

1. Страница, содержащая кнопку, посредством которой вызвается выезжающая контактная форма:

Кнопка, вызывающая всплывающую форму обратной связи

2. Контактная форма в модальном окне (всплывающая):

Форма обратной связи в модальном окне

3. Пользователь неправильно ввёл капчу и нажал на кнопку "Отправить сообщение":

Неправильно заполнена капча в выезжающей форме обратной связи

4. Успешная отправка формы feedback:

Сообщение об успешной отправки формы обратной связи

5. Письмо, пришедшее менеджеру с формы feedback:

Письмо, пришедшее пользователю посредством формы обратной связи