PHP-обработчик, который отдаёт элементы постранично (с фильтрами), и jQuery-скрипт, который подгружает их (кнопкой «Ещё» или бесконечной прокруткой). Всё на стандартном CIBlockElement.
1) Подключаем jQuery и наш скрипт
В шаблоне (обычно /local/templates/ВАШ_ШАБЛОН/header.php):
php
1) Подключаем jQuery и наш скрипт
В шаблоне (обычно /local/templates/ВАШ_ШАБЛОН/header.php):
2) HTML-разметка страницы
В нужном шаблоне/странице выведи контейнер и фильтры:
php
<?php
// можно прокинуть ID инфоблока/раздела как data-атрибуты
$IBLOCK_ID = 10;
$SECTION_ID = 123;
?>
<div id="catalog" class="catalog"
data-iblock="<?=$IBLOCK_ID?>"
data-section="<?=$SECTION_ID?>">
<form id="catalog-filters" class="catalog__filters">
<input type="text" name="q" placeholder="Поиск по названию" />
<select name="color">
<option value="">Цвет (любой)</option>
<option value="red">Красный</option>
<option value="blue">Синий</option>
</select>
<input type="number" name="price_min" placeholder="Цена от" min="0" step="1" />
<input type="number" name="price_max" placeholder="Цена до" min="0" step="1" />
<button type="submit">Найти</button>
</form>
<div id="catalog-items" class="catalog__items"></div>
<button id="catalog-more" class="catalog__more" hidden>Загрузить ещё</button>
<div id="catalog-loader" class="catalog__loader" hidden>Загрузка…</div>
<div id="catalog-empty" class="catalog__empty" hidden>Ничего не найдено</div>
</div>
3) AJAX-обработчик /local/ajax/items.php
Создай файл /local/ajax/items.php:
php
<?php
// /local/ajax/items.php
define('NO_KEEP_STATISTIC', true);
define('STOP_STATISTICS', true);
define('NO_AGENT_STATISTIC', true);
define('DisableEventsCheck', true);
require $_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/prolog_before.php';
use Bitrix\Main\Context;
use Bitrix\Main\Loader;
header('Content-Type: application/json; charset=UTF-8');
if (!Loader::includeModule('iblock')) {
echo json_encode(['error' => 'iblock module not loaded']); die();
}
$request = Context::getCurrent()->getRequest();
// входные параметры
$iblockId = (int)$request->get('iblockId');
$sectionId = (int)$request->get('sectionId');
$page = max(1, (int)$request->get('page'));
$pageSize = min(60, max(1, (int)$request->get('pageSize') ?: 12));
$includeSub = $request->get('includeSub') === 'Y';
// фильтры
$q = trim((string)$request->get('q'));
$color = trim((string)$request->get('color'));
$priceMin = $request->get('price_min') !== null ? (float)$request->get('price_min') : null;
$priceMax = $request->get('price_max') !== null ? (float)$request->get('price_max') : null;
$filter = [
'IBLOCK_ID' => $iblockId,
'ACTIVE' => 'Y',
];
if ($sectionId > 0) {
$filter['SECTION_ID'] = $sectionId;
if ($includeSub) {
$filter['INCLUDE_SUBSECTIONS'] = 'Y';
}
}
if ($q !== '') {
// поиск по имени (можно заменить на full-text, если нужно)
$filter['?NAME'] = $q;
}
if ($color !== '') {
$filter['PROPERTY_COLOR'] = $color;
}
if ($priceMin !== null) {
$filter['>=PROPERTY_PRICE'] = $priceMin;
}
if ($priceMax !== null) {
$filter['<=PROPERTY_PRICE'] = $priceMax;
}
$select = [
'ID','IBLOCK_ID','NAME','DETAIL_PAGE_URL','PREVIEW_PICTURE',
'PROPERTY_COLOR','PROPERTY_PRICE'
];
$nav = [
'nPageSize' => $pageSize,
'iNumPage' => $page,
'checkOutOfRange' => true,
'bShowAll' => false,
];
$res = CIBlockElement::GetList(
['SORT' => 'ASC', 'ID' => 'DESC'],
$filter,
false,
$nav,
$select
);
// навигационные данные
$pageCount = (int)$res->NavPageCount;
$total = (int)$res->NavRecordCount;
$items = [];
while ($ob = $res->GetNextElement()) {
$f = $ob->GetFields();
$p = $ob->GetProperties();
$img = null;
if ((int)$f['PREVIEW_PICTURE'] > 0) {
$pic = CFile::ResizeImageGet(
$f['PREVIEW_PICTURE'],
['width' => 400, 'height' => 400],
BX_RESIZE_IMAGE_PROPORTIONAL,
true
);
$img = $pic['src'];
}
$items[] = [
'id' => (int)$f['ID'],
'name' => $f['~NAME'],
'url' => $f['DETAIL_PAGE_URL'],
'img' => $img,
'color' => $p['COLOR']['VALUE'] ?? null,
'price' => $p['PRICE']['VALUE'] ?? null,
];
}
echo json_encode([
'items' => $items,
'page' => $page,
'pageCount' => $pageCount,
'total' => $total,
], JSON_UNESCAPED_UNICODE);
- jQuery-скрипт /local/templates/.../js/catalog.js
javascript
jQuery(function($){
const $root = $('#catalog');
if (!$root.length) return;
const $items = $('#catalog-items');
const $form = $('#catalog-filters');
const $more = $('#catalog-more');
const $loader = $('#catalog-loader');
const $empty = $('#catalog-empty');
const iblockId = parseInt($root.data('iblock'), 10);
const sectionId = parseInt($root.data('section'), 10) || 0;
const state = {
page: 1,
pageCount: 1,
loading: false,
pageSize: 12,
lastQueryKey: ''
};
function queryParams(extra={}) {
const fd = new FormData($form[0]);
const params = {
iblockId,
sectionId,
includeSub: 'Y',
page: state.page,
pageSize: state.pageSize
};
for (const [k,v] of fd.entries()) {
if (v !== '') params[k] = v;
}
return Object.assign(params, extra);
}
function renderCard(it){
const img = it.img ? `<img src="${it.img}" alt="${escapeHtml(it.name)}" loading="lazy">` : '';
const price = it.price ? `<div class="card__price">${it.price}</div>` : '';
const color = it.color ? `<div class="card__color">Цвет: ${escapeHtml(it.color)}</div>` : '';
return `
<a class="card" href="${it.url}">
<div class="card__img">${img}</div>
<div class="card__body">
<div class="card__title">${escapeHtml(it.name)}</div>
<div class="card__meta">
${price}
${color}
</div>
</div>
</a>
`;
}
function escapeHtml(s){
return String(s).replace(/[&<>"']/g, m => ({
'&':'&','<':'<','>':'>','"':'"',"'":'''
}[m]));
}
function setLoading(flag){
state.loading = !!flag;
$loader.prop('hidden', !flag);
$more.prop('disabled', !!flag);
}
function updateMoreVisibility(){
const hasMore = state.page < state.pageCount;
$more.prop('hidden', !hasMore);
}
function fetchItems({append=false} = {}){
if (state.loading) return;
setLoading(true);
$empty.prop('hidden', true);
const params = queryParams();
// ключ запроса для предотвращения гонок
const queryKey = JSON.stringify(Object.assign({}, params, { page: append ? state.page : 1 }));
state.lastQueryKey = queryKey;
$.ajax({
url: '/local/ajax/items.php',
method: 'GET',
data: params,
dataType: 'json'
}).done(function(resp){
// защита от «переехавших» ответов
if (state.lastQueryKey !== queryKey) return;
const list = Array.isArray(resp.items) ? resp.items : [];
state.page = resp.page || 1;
state.pageCount = resp.pageCount || 1;
if (!append) $items.empty();
if (list.length) {
const html = list.map(renderCard).join('');
$items.append(html);
}
if (!list.length && !append && state.pageCount === 1) {
$empty.prop('hidden', false);
}
updateMoreVisibility();
}).fail(function(){
// здесь можно показать toast/ошибку
console.error('Ошибка загрузки элементов');
}).always(function(){
setLoading(false);
});
}
// Первичная загрузка
fetchItems();
// Сабмит фильтров — начинаем с первой страницы
$form.on('submit', function(e){
e.preventDefault();
state.page = 1;
fetchItems({append:false});
});
// Изменение фильтров — дебаунс
let t;
$form.on('input change', function(){
clearTimeout(t);
t = setTimeout(function(){
state.page = 1;
fetchItems({append:false});
}, 300);
});
// Кнопка «Загрузить ещё»
$more.on('click', function(){
if (state.loading) return;
if (state.page >= state.pageCount) return;
state.page += 1;
fetchItems({append:true});
});
// Бесконечная прокрутка (опционально)
let scrollT;
$(window).on('scroll', function(){
if (state.loading) return;
if (state.page >= state.pageCount) return;
clearTimeout(scrollT);
scrollT = setTimeout(function(){
const nearBottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 300;
if (nearBottom) {
state.page += 1;
fetchItems({append:true});
}
}, 100);
});
});
- Простейшие стили /local/templates/.../css/catalog.css (по желанию)
css
.catalog__filters { display:flex; gap:8px; flex-wrap:wrap; margin:16px 0; }
.catalog__items { display:grid; grid-template-columns: repeat(auto-fill,minmax(220px,1fr)); gap:16px; }
.card { display:block; border:1px solid #eee; border-radius:12px; overflow:hidden; text-decoration:none; color:inherit; }
.card__img { aspect-ratio: 1 / 1; display:flex; align-items:center; justify-content:center; background:#fafafa; }
.card__img img { max-width:100%; max-height:100%; display:block; }
.card__body { padding:12px; }
.card__title { font-weight:600; margin-bottom:6px; }
.card__meta { font-size:14px; opacity:.8; display:flex; gap:8px; flex-wrap:wrap; }
.catalog__more { display:block; margin:16px auto; padding:10px 16px; }
.catalog__loader, .catalog__empty { text-align:center; margin:16px 0; }
Как это работает
- PHP отдаёт JSON с элементами и числами страниц (page, pageCount).
- jQuery:
- рендерит карточки;
- применяет фильтры (поиск, цвет, цена);
- подгружает следующую страницу кнопкой или при прокрутке;
- показывает «Ничего не найдено», если пусто.
Хочешь — добавлю сортировку (селект «Цена ↑/↓», «По имени», «По дате») и серверную обработку ($order в GetList). Или интегрирую сюда свойства-списки/множественные и фильтр по разделам с «включая под-разделы».