Проблема
Недавно я столкнулся с вполне, на мой взгляд, распространённой задачей: нужно обеспечить пользователю возможность загрузить на сервер любое число, скажем, картинок с комментарием к каждой из них в рамках одного интерфейса. В моём случае это было: фото товара, его описание и количество. Для наглядности прикладываю скриншот интерфейса:
Идея и алгоритм решения
Так как описания и фотографии для получения конечного результата можно изменять очень много раз, решено было осуществить следующую схему работы: фотографии загружаются на сервер на одной при клике на фото-иконку, при этом в случае успеха сервер возвращает имя картинки, а при неуспехе — «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(){ ...
— удаляет строку из таблицы товаров.
Вот, собственно, и весь процесс.
Если возникнет такая необходимость, сделаю демо процесса отдельным блоком и прикреплю сюда ссылку, а так же дам исходники.
Спасибо за прочтение!
Автор: BostonGeorge