Редактор изображения показывает новую фотографию, но после отправки формы карточка снова открывается со старой. Цена ошибки заметна не сразу: менеджер повторяет загрузку, а в файловом хранилище появляются лишние записи без понятной связи с товаром.
В такой legacy-форме я не пытаюсь заставить jQuery «запомнить картинку». Нужно решить, что именно передаёт форма: новый бинарный файл, ID уже существующего файла или команду удалить старый. Preview — только экранный сигнал. Сервер должен получить одно из этих состояний и вернуть результат, который форма может показать без догадок.
Фиксирую контракт формы до правки плагина
Перед работой я выписываю имена полей и один реальный POST. Старые шаблоны часто держат одновременно обычный input type=file, скрытый PHOTO_ID и HTML редактора. Если после Ajax-перерисовки в документе остаются два поля с одинаковым name, браузер отправит оба, а обработчик выберет не то значение. Поэтому в контракте у каждого поля одна роль.
| Поле или сигнал | Значение | Владелец | Что означает на сервере |
|---|---|---|---|
CATALOG_PREVIEW | бинарный файл из multipart POST | браузер до отправки | кандидат на новую картинку |
KEEP_PICTURE | 0 или 1 | форма и сценарий редактирования | сохраняем существующую картинку, если нового файла нет |
DELETE_PICTURE | 0 или 1 | явное действие пользователя | запрашиваем удаление, не выводим его из пустого DOM |
.js-photo-state | текст статуса | jQuery | не попадает в модель данных и не является ID файла |
Не прячу имена полей внутри редактора
Сначала оставляю контрол и обработчик читаемыми. В примере ниже FileInput рисует интерфейс для одного изображения, а вокруг него есть контейнер для статуса. Если конкретная версия Bitrix возвращает файл через свой формат, я проверяю его отдельным тестовым POST и передаю дальше как файловый массив. Не подменяю это знание строкой из скрытого поля.
<?php
use Bitrix\\Main\\UI\\FileInput;
$currentId = (int) $arResult['PREVIEW_PICTURE'];
?>
<form id="catalog-photo-form" method="post" enctype="multipart/form-data">
<div class="js-photo-editor">
<?php
echo FileInput::createInstance(array(
'id' => 'catalog_preview',
'name' => 'CATALOG_PREVIEW',
'upload' => true,
'allowUpload' => FileInput::UPLOAD_IMAGES,
'maxCount' => 1,
'maxSize' => 5 * 1024 * 1024,
'delete' => true,
))->show($currentId);
?>
</div>
<label><input type="checkbox" name="KEEP_PICTURE" value="1" checked> оставить текущую картинку</label>
<label><input type="checkbox" name="DELETE_PICTURE" value="1"> удалить картинку</label>
<p class="js-photo-state" aria-live="polite"></p>
<button type="submit">Сохранить</button>
</form>
Контрол FileInput доступен в D7-ядре и умеет формировать HTML и JavaScript. Но значение name — часть договора с PHP, а не косметика. После первой подстановки шаблона я смотрю исходный HTML и реальный Request Payload: имя, число file-полей, код ответа и наличие enctype=multipart/form-data. Если форма отправляется Ajax-ом, проверяю, что код строит FormData, а не сериализует только текстовые inputs.
jQuery показывает состояние, но не делает файл сохранённым
Устаревший шаблон может переотрисовать блок формы через .html(). Прямой обработчик на старом input после этого исчезнет, а повторная инициализация способна добавить второй обработчик. Я привязываю событие к стабильному контейнеру и использую namespace: перед повторной инициализацией снимаю именно свой обработчик. В браузере это даёт один статус выбора и не меняет серверную модель.
(function ($) {
function bindPhotoForm(root) {
var $root = $(root);
$root.off('change.photoWorkflow', 'input[type=file][name=CATALOG_PREVIEW]')
.on('change.photoWorkflow', 'input[type=file][name=CATALOG_PREVIEW]', function () {
var file = this.files && this.files[0];
var message = file
? 'Выбран файл: ' + file.name + '. Сохранение ещё не выполнено.'
: 'Новый файл не выбран.';
$root.find('.js-photo-state').text(message);
$root.find('input[name=KEEP_PICTURE]').prop('checked', !file);
$root.find('input[name=DELETE_PICTURE]').prop('checked', false);
});
}
$(function () {
bindPhotoForm(document);
});
}(jQuery));
Метод .on() с селектором делегирует событие от потомка к уже существующему контейнеру. Это подходит для полей, которые появятся после перерисовки. Обработчик выше намеренно не кладёт имя или data URL в PHOTO_ID: такой ID существует только после серверного шага. Если нужны размеры картинки для preview, их можно показать рядом, но финальную проверку и привязку оставляю обработчику.
На сервере выбираю ровно один путь
Для формы редактирования полезно свести три пользовательских действия к трём веткам. Новый файл важнее флага «оставить»; явное удаление нельзя смешивать с новым файлом в одном запросе. При конфликте возвращаю ошибку формы, а не выбираю вариант по порядку полей. Это проще объяснить оператору и проще проверить через один POST.
function updatePreviewPicture($elementId, array $post, array $files)
{
$hasNewFile = isset($files['CATALOG_PREVIEW'])
&& ($files['CATALOG_PREVIEW']['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK;
$deleteRequested = ($post['DELETE_PICTURE'] ?? '') === '1';
if ($hasNewFile && $deleteRequested) {
throw new RuntimeException('Choose a new picture or deletion, not both');
}
if (!$hasNewFile && !$deleteRequested) {
return; // карточка сохраняет текущую картинку
}
$fields = array();
if ($hasNewFile) {
$fields['PREVIEW_PICTURE'] = $files['CATALOG_PREVIEW'];
} else {
$fields['PREVIEW_PICTURE'] = array('del' => 'Y');
}
$element = new CIBlockElement();
if (!$element->Update((int) $elementId, $fields)) {
throw new RuntimeException($element->LAST_ERROR);
}
}
Это минимальный сценарий именно для поля изображения элемента. Если приложение сначала создаёт отдельную запись в b_file, я храню возвращённый ID в серверной сессии или черновике и при финальном сохранении снова проверяю права на элемент. Для повторного использования существующего файла документация Bitrix предлагает CFile::MakeFileArray(), принимающий и ID файла. Важно не смешивать эту ветку с «пользователь выбрал файл, но ещё не отправил форму».
Проверяю не только счастливый путь
- Открываю существующую карточку, фиксирую текущий ID изображения и отправляю форму без нового файла: ID должен остаться прежним.
- Выбираю небольшой тестовый файл, проверяю один POST с multipart-частью и убеждаюсь, что серверный ответ содержит успешный результат обновления.
- Перезагружаю страницу отдельным запросом; preview и URL картинки должны соответствовать новому значению элемента.
- Нажимаю «удалить» без нового файла и проверяю, что обработчик получает явный флаг, а не выводит удаление из пустого preview.
- Имитирую Ajax-перерисовку контейнера и ещё раз меняю файл: статус должен смениться один раз, без двойного обработчика.
- Отправляю новый файл вместе с удалением и ожидаю понятную ошибку формы, а не неявный выбор одной ветки.
Где legacy-интеграция чаще всего обманывает
Первый обман — вызывать serialize() для формы с файлом. Метод собирает текстовые поля и не переносит бинарное содержимое; для Ajax нужен FormData и правильные параметры запроса. Второй — считать, что серверный 200 подтвердил картинку: обработчик мог вернуть HTML ошибки или не обновить элемент. Третий — брать «последний созданный файл» из базы. В форме одновременно работают люди, поэтому связь должна идти из конкретного запроса и конкретного элемента.
Ещё один риск связан с повторной инициализацией. Namespace в change.photoWorkflow даёт узкую очистку только нашего события; нельзя заменять его общим off('change'), потому что так легко сломать чужой плагин на той же форме. В 2018 году это особенно важно для старых шаблонов, где порядок подключения JavaScript не документирован.
Ограничения и следующий шаг
Пример не определяет политику доступа, допустимые расширения и обработку больших изображений. Их надо связать с ролью пользователя, настройками PHP и правилами каталога на конкретной установке. Если FileInput уже загружает файл отдельным действием, не копируйте ветку с $_FILES: сначала посмотрите, какое значение возвращает контрол и кто владеет временным ID.
Критерий готовности простой: после новой загрузки форма делает один понятный запрос, Update() проходит, а свежая страница показывает новое изображение. После сохранения без файла старая картинка остаётся. Этого достаточно, чтобы следующая правка формы не превратила DOM-preview в ложное подтверждение данных.
Проверяемые источники
- 1С-Битрикс: FileInput — описание контрола, его методов createInstance, prepareFile и show, а также параметров загрузки
- 1С-Битрикс: CIBlockElement::Update — Update принимает массив полей, возвращает true или false, а текст ошибки остаётся в LAST_ERROR
- 1С-Битрикс: CFile::MakeFileArray — формирует файловый массив, в том числе по ID существующего файла
- jQuery API: .on() — делегированный обработчик работает на потомках существующего контейнера и может быть привязан с namespace
- jQuery API: .ready() — обработчик запускается, когда DOM готов к безопасному изменению
- PHP Manual: $_FILES — структура данных, переданных HTTP POST с файлом