Проблема


Недавно я столкнулся с вполне, на мой взгляд, распространённой задачей: нужно обеспечить пользователю возможность загрузить на сервер любое число, скажем, картинок с комментарием к каждой из них в рамках одного интерфейса. В моём случае это было: фото товара, его описание и количество. Для наглядности прикладываю скриншот интерфейса:



Идея и алгоритм решения


Так как описания и фотографии для получения конечного результата можно изменять очень много раз, решено было осуществить следующую схему работы: фотографии загружаются на сервер на одной при клике на фото-иконку, при этом в случае успеха сервер возвращает имя картинки, а при неуспехе — «error». Соответственно, в случае успеха, фото-иконка заменяется на миниатюру загруженного фото, а в скрытое поле формы соответствующей строки сохраняется её имя, а при неуспехе мы получаем фото-иконку и пустое скрытое поле формы соответствующей строки, отвечающее за имя фото. Текстовая же информация при изменении любого поля формы отправляется на сервер вся в формате массив [имяФото, описаниеДетали, количествоШт] — это наиболее универсально: один и тот же метод отвечает за полное обновление списка товаров при их редактировании или удалении. Как известно, AJAX не умеет отправлять файлы, поэтому реализуем процедуру загрузки с помощью обычной формы, в качестве target которой укажем скрытый фрейм, который и будет перезагружаться вместо страницы.



Практическая реализация


Итак, в нашем распоряжении HTML, PHP и Javascript. Поехали:

1. Верстаем на странице форму для загрузки фото. Она содержит только один input, который мы спрячем с помощью css:

<form enctype="multipart/form-data" action="<?=site_url('otherdetails/uploadOthDetPhoto')?>" method="post" id="othdetphotoform" target="hiddenframe">
<input type="file" id="photoloader" name="photo"/>
</form>

2. Создадим на странице скрытый iframe, который и будет перезагружаться в результате отправки формы с файлом:

<iframe id="hiddenframe" name="hiddenframe" style="width:0px;height:0px;border:0px"></iframe>

3. Верстаем таблицу товаров, с которой и будет работать пользователь:

<table id="othdetails" class="table table-hover borderbottom">
    <thead>
        <tr>
            <th>№ п/п</th>
            <th>Фото</th>
            <th>Описание</th>
            <th>шт</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td class="col-xs-2 col-md-1" style="vertical-align:middle;"></td>
            <td style="width:110px;">
                <img src="photo_icon.png" alt="Загрузить фото" class="img-circle othdet_photo"/>
                <input class="form-control" type="hidden" name="dform_oth_details_photo[]" value=""/>
            </td>
            <td><textarea name="dform_oth_details_descr[]" class="form-control" rows="3" placeholder="Описание детали"></textarea></td>
            <td class="col-xs-2 col-md-1" style="vertical-align:middle;"><input name="dform_oth_details_cnt[]" type="number" class="form-control" value="1" placeholder="шт"/></td>
            <td style="vertical-align:middle;text-align:right;"><span class="glyphicon glyphicon-remove removebtn" aria-hidden="true"></span></td>
        </tr>
    </tbody>
</table>


В строке товара мы имеем:
  • <img src="photo_icon.png" alt="Загрузить фото" class="img-circle othdet_photo"/>
    
    — иконка, клик которой будет имитировать клик /> в форме загрузки фото,
  • <input class="form-control" type="hidden" name="dform_oth_details_photo[]" value=""/>
    — это наш скрытый input, отвечающий за имя фото,
  • <textarea name="dform_oth_details_descr[]" class="form-control" rows="3" placeholder="Описание детали"></textarea>
    
    — описание,
  • <input name="dform_oth_details_cnt[]" type="number" class="form-control" value="1" placeholder="шт"/>
    — количество штук.


3. Пишем PHP-код загрузки файла:

public function uploadOthDetPhoto()
    {
        $this->isDformSet();
        $config['upload_path'] = 'public/uploads/othdet/';
        $config['allowed_types'] = 'gif|jpg|png';
        $config['max_size'] = '10240';
        $config['encrypt_name'] = true;
        $this->load->library('upload', $config);
        $this->upload->initialize($config);
        $this->upload->do_upload('photo');
        $arrErrors = $this->upload->display_errors();
        if (!empty($arrErrors) > 0){
            echo 'error';
        }else{
            $arrPhotoData = $this->upload->data();
            $strFileName = $arrPhotoData['file_name'];
            echo $strFileName;
            $intMaxWidth = 800;
            if ($arrPhotoData['image_width'] > $intMaxWidth){
                unset($config);
                $config['image_library'] = 'gd2';
                $config['source_image']	= $arrPhotoData['full_path'];
                $config['width'] = $intMaxWidth;
                $config['height'] = $intMaxWidth*$arrPhotoData['image_height']/$arrPhotoData['image_width'];
                $config['maintain_ratio'] = TRUE;
                $this->load->library('image_lib', $config); 
                $this->image_lib->resize();
            }
        }
    }

Мой проект на CodeIgniter, поэтому код вот такой, но в целом, суть в следующем: мы просто загружаем и переименовываем полученный из формы файл, если всё проходит успешно, выводим в наш iframe его имя, если нет — «error».

4. Пишем Javascript, который будет контролировать весь процесс:

$(function(){
    function rownumbers(){
        $('#othdetails tbody tr').each(function(i) {
            var number = i + 1;
            $(this).find('td:first').text(number);
        });
    }
    
    function update(){
        var url = '<?=site_url('otherdetails/updateOthDet')?>';
        var postData = $('.form-control').serialize();
        $.post(url, postData, function(){}, 'json');
    }
    
    var curimg = '';
    var intervalID = '';
    
    function checkphotoname() {
        var linkedFrame = document.getElementById('hiddenframe');
        var content = linkedFrame.contentWindow.document.body.innerHTML;
        var completed = false;
        if (content === 'error'){
            curimg.attr('src','<?=base_url()?>public/img/photo_icon.png');
            $('#error').show(200).delay(6000).hide(200);
            $('#error').html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> Фото не было загружено. Максимальный размер фото - 10 Мб, форматы: jpg,png,gif.');
            completed = true;
        }
        else
        if (content !== ''){
            curimg.parent().children('.form-control').val(content);
            curimg.attr('src','<?=base_url()?>public/uploads/othdet/' + content);    
            completed = true;
        }
        if (completed === true){
            update();
            clearInterval(intervalID);
            $('#hiddenframe').contents().find('body').html('');
        }
    }
    
    rownumbers();
    
    $(document).on('click', '.othdet_photo', function(){
        $('#hiddenframe').contents().find('body').html('');
        curimg = $(this);
        curimg.attr('src','<?=base_url()?>public/img/photo_icon.png');
        curimg.parent().children('.form-control').val('');
        update();
        $('#photoloader').click();
    });
    
    $('#photoloader').change(function(){
        curimg.attr('src','<?=base_url()?>public/img/indicator.gif');
        $('#othdetphotoform').submit();
        $('#photoloader').val('');
    });
    
    $('#othdetphotoform').submit(function(){
        intervalID = setInterval(checkphotoname, 500);
    });
    
    $(document).on('change', '.form-control', function(){
        update();
    });
    
    $("#addnewdet").click(function(){
        var emp = 0;
        $(".form-control[name!='dform_oth_details_photo[]']").each(function(indx){
            if ($(this).val() === ''){
                emp = 1;
                $(this).focus();
             }    
        });    
        if (emp === 1){
            $('#error').show(200).delay(2000).hide(200);
            $('#error').html('<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span> Пожалуйста, заполните все поля');
        }else{
            var row = '<tr><td class="col-xs-2 col-md-1" style="vertical-align:middle;"></td><td style="width:110px;"><img src="<?=base_url()?>public/img/photo_icon.png" alt="Загрузить фото" class="img-circle othdet_photo"/><input class="form-control" type="hidden" name="dform_oth_details_photo[]" value=""/></td><td><textarea name="dform_oth_details_descr[]" class="form-control" rows="3" placeholder="Описание детали"></textarea></td><td class="col-xs-2 col-md-1" style="vertical-align:middle;"><input name="dform_oth_details_cnt[]" type="number" class="form-control" value="1" placeholder="шт"/></td><td style="vertical-align:middle;text-align:right;"><span class="glyphicon glyphicon-remove removebtn" aria-hidden="true"></span></td></tr>';
            $("#othdetails tbody").append(row);
            rownumbers();
            $('[name="dform_oth_details_descr[]"]').last().focus();
        }    
    });
    
    $(document).on('click', '.removebtn', function(){
        $(this).parent().parent().remove();
        update();
        rownumbers();
    });
    
});

Вот тут объясню подробнее:
  • function rownumbers()
    — просто расставляет порядковые номера в таблице,
  • function update()
    — отправляет на сервер все текстовые поля нашей таблицы товаров. Их обработку в рамках этой статьи упоминать смысла нет,
  • var curimg = '';
        var intervalID = '';
    — указываем переменные, которые будут содержать ссылку на текущее фото и таймер. Подробнее об этом чуть позже;
  • function checkphotoname() 
    — одна из основных функций: отвечает за обработку результата загрузки фото на основе содержимого нашего скрытого iframe. Эта функцию запускается с интервалом при отправке формы фото и заканчивает своё выполнение подстановкой миниатюры фото вместо фото иконки или выдачей сообщения об ошибке при обнаружении изменения содержимого iframe, то есть по окончании обработки фото сервером;
  • rownumbers();
    — просто расставляет порядковые номера в таблице при загрузке страницы — я не включил в статью то, что эта страница используется как при первом добавлении товаров в список, так и при редактировании этого списка, то есть таблица товаров может быть изначально не пустой;
  • $(document).on('click', '.othdet_photo', function(){ ...
    — эмулируем клик input file в форме загрузки изображения при клике фото-иконки (так как пользователь может передумать использовать ранее загруженное фото, очищаем его перед выбором следующего);
  • $('#photoloader').change(function(){ ...
    — отправляем форму загрузки фото на сервер при изменении поля фото;
  • $('#othdetphotoform').submit(function(){ ...
    — вызываем с интервалом вышеописанную функцию обработки результата загрузки фото на сервер;
  • $(document).on('change', '.form-control', function() ...
    — при изменении любого поля (в том числе и имени фото) в списке товаров отправляем его весь на сервер;
  • $("#addnewdet").click(function(){ ...
    — добавляет строку в таблицу товаров;
  • $(document).on('click', '.removebtn', function(){ ...
    — удаляет строку из таблицы товаров.


Вот, собственно, и весь процесс.

Если возникнет такая необходимость, сделаю демо процесса отдельным блоком и прикреплю сюда ссылку, а так же дам исходники.

Спасибо за прочтение!

Комментарии (7)


  1. zenn
    20.04.2015 20:15
    +8

    Возможно я слишком требователен, но по моему мнению таким статьям не место на хабре, в лучшем случае — где то в личном блоге, ведь в статье нет никаких инновационных методов или разработок, а подобного можно «нагуглить» over9999 материалов…


  1. neomoto
    20.04.2015 20:21
    +2

    Как известно, AJAX не умеет отправлять файлы

    Пользуйтесь. C полгода как перешел на formdata и с облегчением вздохнул.


  1. Epsil0neR
    20.04.2015 20:28
    +1

    Как известно, AJAX не умеет отправлять файлы

    Javascript может отправлять файлы используя объект типа FormData (Давно поддерживается большинством браузеров за исключением IE: начигая с 10 версии).


  1. Evgeny42
    20.04.2015 22:18
    -1

    Судя по тому что автор до сих пор пишет на CI и данным в статье, автор первый реальный пример путешествия во времени. Лет эдак на 5 вперед.


  1. yvm
    20.04.2015 23:11
    -4

    Что общего между habrahabr и comedy club? Училки русского языка — рулят.


  1. izac
    21.04.2015 02:47

    На будущее автору совет, код нужно прятать под спойлер.


  1. dcc0
    22.04.2015 11:27

    Интересно. Если говорить не о файлах, а только о фотографиях,
    мне нравится такой вариант, (с точки зрения удобства управления), исходим из пользовательских действий:
    1. отдельное поле ввода для изображения,
    2. если берем изображение с чужого сайта — щелчок правой кнопкой мыши на нем, «копировать адрес изображения, вставляем в поле». Регулярным выражением (preg_replace) приводим его к виду img src — в общем, форматируем. Все. Если с загрузкой с компьютера, тоже самое — стандартная опция для загрузки, после добавления в текстовом поле получаем (оформленную) ссылку, которую можно править.

    Т.е. все равно имеем два действия и оставляем пользователю возможность отредактировать ссылку на изображение (по желанию).