Фильтр и быстрый фильтр
Быстрые фильтры значительно ускоряют работу фильтров в интернет-магазине, позволяют в фильтре рассчитать и показать количество отфильтрованных и подлежащих отображению элементов. Быстрые фильтры работают "прозрачно" для администратора и при отключении быстрой фильтрации для магазина, фильтр будет работать через обычную фильтрацию.

Как начать использовать?
Включается быстрый фильтр в опциях магазина. Быстрый фильтр строится на основании дополнительных свойств магазина и цены товаров. После включения фильтра необходимо выполнить построение таблицы, для этого нажмите на пиктограмму фильтра и дождитесь окончания построения индекса. При редактировании, импорте товаров, индекс перестраивается автоматически.

Перестроение быстрого фильтра
Для перестроение быстрого фильтра выбранного магазина нажмите на пиктограмму в столбце с фильтром, затем дождитесь окончания перестроения фильтра.
ЧПУ для фильтра
Фильтр использует ЧПУ, после пути к группе указывается название опции фильтра, затем значения (если тип фильтра это предполагает), например, /shop/price-1000-5000/diagonal/42/50/colors/Черный/
При использовании ЧПУ для фильтра, отдельно обрабатывать фильтр не требуется, условия будут применены в parserUrl() контроллера показа.
Запрет на индексацию фильтра, построенного через ЧПУ
Страницы с результатами фильтрации могут быть проиндексированы поисковыми системами, если вы не хотите, чтобы страницы фильтра попадали в поисковый индекс, внесите в макет в секцию <head> следующий код:
<?php
if (is_object(Core_Page::instance()->object)
&& Core_Page::instance()->object instanceof Shop_Controller_Show
// Нет примененного SEO-фильтра
&& is_null(Core_Page::instance()->object->filterSeo)
// Условия фильтра заданы по свойствам, ценам или производителю
&& (count(Core_Page::instance()->object->getFilterProperties())
|| count(Core_Page::instance()->object->getFilterPrices())
|| Core_Page::instance()->object->producer
)
)
{
// Запрет индексации страниц фильтра
?> <meta name="robots" content="noindex, follow"><?php
echo PHP_EOL;
}
?>
Опции фильтра в шаблонах мета-тегов
Для вывода в шаблонах мета-тегов значений, заданных фильтром, используйте подстановку {this.seoFilter ": " ", "}.
Применение фильтров
Указание опций фильтрации осуществляется методом ->addFilter().
Ограничение по свойству 17, где значение равно 33:
->addFilter('property', 17, '=', 33)
Фильтрация по дополнительным свойствам работает с существующими значениями свойства, при отсутствии значения свойства такой товар считается подходящим под ограничения, изменить это поведение можно опцией filterStrictMode(TRUE), которая указывает фильтровать только по существующим значениям (кроме списков и checkbox), отсутствие значения считать неверным значением. Если аргументом указан массив с идентификаторами свойств, то только для них будет использоваться строгий режим, по умолчанию FALSE.
Ограничение по цене, где цена больше 100:
->addFilter('price', '>', 100)
Ограничение по основным свойствам ('length', 'width', 'height', 'weight'), пример, где длина больше или равно 50:
->addFilter('length', '>=', 50)
Указание поля и направления сортировки
Указание направления сортировки возможно напрямую контроллеру показа методом ->orderBy($column, $direction = 'ASC'), при сортировке по цене вы можете указывать price или absolute_price (синонимы), при этом расчет absolute_price будет добавлен автоматически.
Подсчет количества соответствующих свойству значений в текущей группе
По умолчанию подсчет количества соответствующих свойству значений не производится. Для включения, необходимо контроллеру показа магазина добавить параметр ->filterCounts(TRUE). После чего в XML пойдет тег <filter_counts> с рассчитанным по текущим параметрам фильтра значением для каждого свойства.
Если требуется убрать отсутствующие значения, то необходимо внести изменения в темплейт <xsl:template match="list/list_item"> в XSL-шаблоне фильтр. После строки вносится условие в которое помещается все остальное тело темплейта.
Для свойств типа число при выбранном способе отображения "от .. до" будут рассчитаны минимальное и максимальное значение, которые помещаются в теги <min> и <max> * с версии 6.9.2
Переход на быстрые фильтры при обновлении до версии 6.9.1
С версии 6.9.1 внесены следующие изменения и улучшения в работу фильтров:
Для совместимости с предыдущей версией фильтра, когда опции задаются через GET-параметры, реализованы два метода, устанавливающие опции фильтрации по цене и свойствам:
// Prices $Shop_Controller_Show->setFilterPricesConditions($_GET); // Additional Properties $Shop_Controller_Show->setFilterPropertiesConditions($_GET);
Таким образом общий блок фильтрации в коде ТДС с фильтрацией через GET-параметры будет сокращен до следующего кода:
if (Core_Array::getGet('filter') || Core_Array::getGet('sorting'))
{
$Shop_Controller_Show->addEntity(
Core::factory('Core_Xml_Entity')
->name('filter')->value(1)
);
// Sorting
$sorting = Core_Array::getGet('sorting', 0, 'int');
($sorting == 1 || $sorting == 2)
&& $Shop_Controller_Show->orderBy('absolute_price', $sorting == 1 ? 'ASC' : 'DESC');
$sorting == 3 && $Shop_Controller_Show->orderBy('shop_items.name', 'ASC');
$Shop_Controller_Show->addEntity(
Core::factory('Core_Xml_Entity')
->name('sorting')->value($sorting)
);
// Prices
$Shop_Controller_Show->setFilterPricesConditions($_GET);
// Additional properties
$Shop_Controller_Show->setFilterPropertiesConditions($_GET);
}
Ограничения базы данных
Таблицы имеют ограничение в 64 вторичных индекса, в связи с чем по дополнительным свойствам доступно построение максимум 59 индексов.
Клиентская интеграция
Рассмотрим интеграцию фильтра на примере стандартного шаблона системы: http://demoshop.hostcms.ru/shop/clothes/woman/dresses/
Макет
Интеграцию фильтра в макете необходимо начать с блока вызова фильтра в макете.
$oShop = Core_Entity::factory('Shop', Core_Page::instance()->libParams['shopId']);
$Shop_Controller_Show = new Shop_Controller_Show($oShop);
$Shop_Controller_Show
->xsl(
Core_Entity::factory('Xsl')->getByName('МагазинФильтр')
)
->groupsMode('tree')
->limit(0)
->itemsProperties(TRUE);
if (is_object(Core_Page::instance()->object)
&& get_class(Core_Page::instance()->object) == 'Shop_Controller_Show')
{
$Shop_Controller_Show->group(Core_Page::instance()->object->group);
$mCurrentShopGroup = Core_Page::instance()->object->group;
$Shop_Controller_Show->setFilterProperties(Core_Page::instance()->object->getFilterProperties());
$Shop_Controller_Show->setFilterPrices(Core_Page::instance()->object->getFilterPrices());
// Основные свойства, с версии 6.9.5
$Shop_Controller_Show->setFilterMainProperties(Core_Page::instance()->object->getFilterMainProperties());
Core_Page::instance()->object->producer
&& $Shop_Controller_Show->producer(Core_Page::instance()->object->producer);
}
else
{
$mCurrentShopGroup = 0;
}
$Shop_Controller_Show
->group($mCurrentShopGroup)
->applyGroupCondition();
if ($Shop_Controller_Show->group == 0)
{
$Shop_Controller_Show->group(FALSE);
}
//Sorting
if (Core_Array::getGet('sorting'))
{
$sorting = intval(Core_Array::getGet('sorting'));
$Shop_Controller_Show->addEntity(
Core::factory('Core_Xml_Entity')
->name('sorting')->value($sorting)
);
$Shop_Controller_Show->addCacheSignature('sorting=' . $sorting);
}
/* Количество */
$on_page = intval(Core_Array::getGet('on_page'));
if ($on_page > 0 && $on_page < 150)
{
$Shop_Controller_Show->addEntity(
Core::factory('Core_Xml_Entity')
->name('on_page')->value($on_page)
);
}
$Shop_Controller_Show
//Фильтровать по ярлыкам
->filterShortcuts(TRUE)
->modificationsList(TRUE)
->favorite(FALSE)
->viewed(FALSE)
->addProducers()
->filterCounts(TRUE)
->addMinMaxPrice()
->show();
и внесением для макета на вкладку JS следующего кода:
function applyFilter()
{
var jForm = $('.filter').closest('form'),
path = jForm.attr('action'),
producerOption = jForm.find('select[name = producer_id] option:selected'),
priceFrom = jForm.find('input[name = price_from]').val(),
priceTo = jForm.find('input[name = price_to]').val(),
priceFromOriginal = jForm.find('input[name = price_from_original]').val(),
priceToOriginal = jForm.find('input[name = price_to_original]').val(),
sortingOption = jForm.find('select[name = sorting] option:selected');
if (parseInt(producerOption.attr('value')))
{
path += producerOption.data('producer') + '/';
}
if (typeof priceFrom !== 'undefined' && typeof priceTo !== 'undefined'
&& (priceFrom !== priceFromOriginal || priceTo !== priceToOriginal)
)
{
path += 'price-' + priceFrom + '-' + priceTo + '/';
}
var inputs = jForm.find('*[data-property]:not(div)'),
tag_name = null;
$.each(inputs, function (index, value) {
var type = this.type || this.tagName.toLowerCase(),
jObject = $(this),
value = null,
setValue = false;
if (typeof jObject.attr('name') !== 'undefined' && jObject.attr('name').indexOf('_to') !== -1)
{
return;
}
switch (type)
{
case 'checkbox':
case 'radio':
value = +jObject.is(':checked');
setValue = type != 'checkbox' ? true : jObject.attr('name').indexOf('[]') !== -1;
break;
case 'option':
value = +jObject.is(':selected');
setValue = true;
break;
case 'text':
case 'hidden':
value = jObject.val();
setValue = true;
break;
}
if (value && jObject.data('property') !== tag_name)
{
tag_name = jObject.data('property');
if (typeof jObject.attr('name') !== 'undefined' && jObject.attr('name').indexOf('_from') !== -1)
{
path += '';
}
else
{
path += tag_name + '/';
}
}
if (setValue && value)
{
if (typeof jObject.attr('name') !== 'undefined' && jObject.attr('name').indexOf('_from') !== -1)
{
path += tag_name + '-' + jObject.val() + '-' + jObject.nextAll('input').eq(0).val() + '/';
}
else
{
path += typeof jObject.data('value') !== 'undefined'
? jObject.data('value') + '/'
: value + '/';
}
}
});
if (parseInt(sortingOption.attr('value')))
{
path += '?sorting=' + sortingOption.val();
}
// console.log(path);
window.location.href = path;
}
function fastFilter(form)
{
this._timerId = false;
this._form = form;
this.filterChanged = function(obj) {
if (this._timerId)
{
clearTimeout(this._timerId);
}
var $this = this;
this._timerId = setTimeout(function() {
$this._loadJson(obj);
}, 1500);
return this;
}
this._loadJson = function(obj) {
var data = this._serializeObject();
$.loadingScreen('show');
$.ajax({
url: './',
type: "POST",
data: data,
dataType: 'json',
success: function (result) {
$.loadingScreen('hide');
if (typeof result.count !== 'undefined')
{
var jParent = obj.parents('.property-list').length
? obj.parents('.property-list')
: obj.parent();
$('.popup-filter').remove();
jParent.css('position', 'relative');
jParent.append('<div class="popup-filter"><div>Найдено: ' + result.count + '</div><br/><div><button class="filter-apply full-width" onclick="applyFilter(); return false;">Применить</button></div></div>');
setTimeout(function() {
$('.popup-filter').remove();
}, 5000);
}
}
});
}
this._serializeObject = function () {
var o = {fast_filter: 1};
var a = this._form.serializeArray();
$.each(a, function () {
if (o[this.name] !== undefined) {
if (!o[this.name].push) {
o[this.name] = [o[this.name]];
}
o[this.name].push(this.value || '');
} else {
o[this.name] = this.value || '';
}
});
return o;
};
}
Далее можно приступать к интеграции XSL-шаблона.
XSL-шаблон
1. В самое начало XSL-шаблона, перед <xsl:apply-templates select="/shop"/> необходимо добавить скрипт. Он будет обрабатывать события фильтра:
<script type="text/javascript">
<xsl:comment>
<xsl:text disable-output-escaping="yes">
<![CDATA[
$(function() {
var jForm = $('.filter').closest('form');
mainFastFilter = new fastFilter(jForm);
jForm.find(':input:not(:hidden):not(button)').on('change', function() {
mainFastFilter.filterChanged($(this));
});
$('.filter-color').on('click', function(){
var bg = $(this).css('background-color'),
className = $(this).attr('class');
if (className == 'filter-color')
{
$('.filter-color').each(function(index, value) {
$(this).removeClass('active');
});
$(this).addClass('active');
$('.color-input').remove();
var property_id = $(this).data('id'),
list_item_id = $(this).data('item-id');
$(this).append('<input type="hidden" class="color-input" id="property_' + property_id + '_' + list_item_id +'" name="property_' + property_id + '" data-property="' + $(this).data('property') + '" data-value="' + $(this).data('value') + '" value="' + list_item_id + '" />');
}
else
{
$('.filter-color').each(function(index, value) {
$(this).removeClass('active');
});
$('.color-input').remove();
}
mainFastFilter.filterChanged($(this));
});
jForm.on('submit', function(e) {
e.preventDefault();
applyFilter();
});
});
]]>
</xsl:text>
</xsl:comment>
</script>
2. Если выводится список производителей, то необходимо в темплейте <xsl:template match="shop_producer"> для каждого <option value="{@id}"> добавить data-атрибут data-producer="{name}". Должно получиться:
<xsl:template match="shop_producer">
<option value="{@id}" data-producer="{name}">
...
</option>
</xsl:template>
3. Следующий этап - добавление data-атрибутов для дополнительных свойств. В темплейте <xsl:template match="property" mode="propertyList"> необходимо всем input-ам, кроме блока переключателей с filter = 3, добавить атрибут data-property="{tag_name}".
4. Далее добавим атрибуты оставшимся свойствам, постоенным на списках. Для этого в темплейте <xsl:template match="list/list_item"> необходимо каждому <option> и <input> добавить два(!) атрибута data-property="{../../tag_name}" data-value="{value}".
5. Для всех свойств типа "Флажок" необходимо указать value="1", если он не указан.
Список цветов

Если в фильтре используется списк с выбором цвета, то его можно вывести ввиде отдельных цветов. Для этого необходимо внести изменения в XSL-шаблон фильтра. Для примера, у нас есть список, который указан у свойства с tag_name = 'colors', каждый цвет задан в поле icon у элемента списка.
В теге <form>, в необходимом месте, добавим блок:
<xsl:if test="shop_item_properties//property[tag_name = 'colors']/node() and count(shop_item_properties//property[tag_name = 'colors']/list//list_item[icon != ''])">
<div class="property-list sidebar-list">
<h3><xsl:value-of select="shop_item_properties//property[tag_name = 'colors']/name"/></h3>
<xsl:for-each select="shop_item_properties//property[tag_name = 'colors']/list/list_item[icon != '']">
<xsl:variable name="color"><xsl:value-of select="icon" /></xsl:variable>
<xsl:variable name="nodename">property_<xsl:value-of select="../../@id" /></xsl:variable>
<div class="filter-color" style="background-color: {$color}" data-id="{../../@id}" data-property="{../../tag_name}" data-value="{value}" data-item-id="{@id}">
<xsl:if test="/shop/*[name()=$nodename]/node() and /shop/*[name()=$nodename] = @id">
<xsl:attribute name="class">filter-color active</xsl:attribute>
<input type="hidden" class="color-input" id="property_{../../@id}_{@id}" name="property_{../../@id}" data-property="{../../tag_name}" data-value="{value}" value="{@id}"/>
</xsl:if>
</div>
</xsl:for-each>
</div>
</xsl:if>
Чтобы не было дубля при выводе всех свойств, то добавим ограничение при показе:
<!-- Фильтр по дополнительным свойствам товара: --> <xsl:if test="count(shop_item_properties//property[filter != 0 and (type = 0 or type = 1 or type = 3 or type = 4 or type = 7 or type = 11) and tag_name != 'colors'])"> <xsl:apply-templates select="shop_item_properties//property[filter != 0 and (type = 0 or type = 1 or type = 3 or type = 4 or type = 7 or type = 11) and tag_name != 'colors']" mode="propertyList"/> </xsl:if>
Пагинация в каталоге товаров
Для корректной работы пагинации и быстрого фильтра необходимо внести изменения в темплейт <xsl:template name="for"> в XSL-шаблоне каталога товаров. Меняем переменную:
<xsl:variable name="filter"><xsl:if test="/shop/filter/node()">?filter=1&sorting=<xsl:value-of select="/shop/sorting"/>&price_from=<xsl:value-of select="/shop/price_from"/>&price_to=<xsl:value-of select="/shop/price_to"/><xsl:for-each select="/shop/*"><xsl:if test="starts-with(name(), 'property_')">&<xsl:value-of select="name()"/>[]=<xsl:value-of select="."/></xsl:if></xsl:for-each></xsl:if></xsl:variable>
на:
<xsl:variable name="filter"><xsl:if test="/shop/filter_path/node() and /shop/filter_path != ''"><xsl:value-of select="/shop/filter_path"/></xsl:if></xsl:variable>
Далее во всех ссылках в темплейте вызов переменной {$filter} переносится после {$group_link}
Типовая динамическая страница
В коде настроек ТДС необходимо добавить обработку запроса:
// Быстрый фильтр
if (Core_Array::getRequest('fast_filter'))
{
$aJson = array();
if ($oShop->filter)
{
// В корне выводим из всех групп
$Shop_Controller_Show->group == 0 && $Shop_Controller_Show->group(FALSE);
$aJson['count'] = $Shop_Controller_Show->getFastFilteredCount();
}
Core::showJson($aJson);
}
CSS
В стили макета необходимо добавить базовый блок стилей фильтра. Которые вы можете поправить так как требует внешний вид сайта.
.filter .popup-filter {
position: absolute;
top: 0;
min-width: 150px;
height: 100px;
background: #fff;
border: 1px solid #03a9f4;
right: -70%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
z-index: 10;
padding: 10px;
}
.filter .popup-filter > div:first-child {
margin-bottom: 5px;
color: #e91e63;
}
.filter .popup-filter > div:last-child {
width: 100%;
}
.filter .property-inline-wrapper > div:first-child {
display: inline-table;
width: 50%;
}
.filter .property-inline-wrapper .filter-label {
display: inline-table;
width: 50%;
text-align: right;
}
.filter .property-inline-wrapper .filter-label .label {
margin: 5px 0;
}
.filter .property-inline-wrapper {
white-space: nowrap;
}
.filter .filter-color {
width: 40px;
height: 40px;
display: inline-block;
cursor: pointer;
}
.filter .filter-color.active {
outline-offset: -5px;
outline: 1px solid #fff;
}
Выбранные значения фильтра для <H1>
При необходимости вывода выбранных значений фильтра, например, в заголовке страницы, добавьте формирование переменной h1filter в XSL-шаблоне:
<xsl:variable name="h1filter"> <xsl:for-each select="shop_item_properties//property[type != 2]"> <xsl:variable name="nodename"> <xsl:choose> <xsl:when test="type = 11">property_<xsl:value-of select="@id"/>_from</xsl:when> <xsl:otherwise>property_<xsl:value-of select="@id"/></xsl:otherwise> </xsl:choose> </xsl:variable> <xsl:variable name="property" select="."/> <xsl:for-each select="/shop/*[name()=$nodename]"> <xsl:variable name="value" select="."/> <xsl:if test="position() = 1"> <xsl:variable name="property_name"> <xsl:choose> <xsl:when test="$property/type = 11"><xsl:value-of select="$property/prefix"/></xsl:when> <xsl:otherwise><xsl:value-of select="$property/name"/></xsl:otherwise> </xsl:choose> </xsl:variable> <xsl:text> </xsl:text><xsl:value-of select="$property_name"/><xsl:text>: </xsl:text> </xsl:if> <xsl:choose> <xsl:when test="$property/type = 3"> <xsl:value-of select="$property/list/list_item[@id = $value]/value"/> </xsl:when> <xsl:when test="$property/type = 11"> <xsl:variable name="property_to">property_<xsl:value-of select="$property/@id"/>_to</xsl:variable> <xsl:value-of select="$value"/> - <xsl:value-of select="/shop/*[name()=$property_to]"/> </xsl:when> <xsl:otherwise> <xsl:value-of select="$value"/> </xsl:otherwise> </xsl:choose> <xsl:if test="position() != last()"> <xsl:text>, </xsl:text> </xsl:if> </xsl:for-each> </xsl:for-each> </xsl:variable>
Далее, переменную можно добавить в <h1>, например, <h1><xsl:value-of select="name"/><xsl:text> </xsl:text><xsl:value-of select="$h1filter"/></h1>
Переход на версию 6.9.4
В версии 6.9.4 добавлен выбор режима URL для фильтра. Для того чтобы обработать новый режим "Путь" необходимо внести изменения в формирование data-атрибутов для производителей и элементов списка в XSL-шаблоне фильтра.
- Для производителей в темплейте "shop_producer" добавляется новая переменная:
<xsl:variable name="name"> <xsl:choose> <xsl:when test="/shop/filter_mode = 0"> <xsl:value-of select="name" /> </xsl:when> <xsl:otherwise> <xsl:value-of select="path" /> </xsl:otherwise> </xsl:choose> </xsl:variable>
Далее нужно заменить data-producer="{name}" на data-producer="{$name}" - Для элементов списка в темплейте "list/list_item" добавляется новая переменная:
<xsl:variable name="value"> <xsl:choose> <xsl:when test="/shop/filter_mode = 1 and path != ''"> <xsl:value-of select="path" /> </xsl:when> <xsl:otherwise> <xsl:value-of select="value" /> </xsl:otherwise> </xsl:choose> </xsl:variable>
Далее нужно во всем темплейте заменить data-value="{value} на data-value="{$value}
SEO-фильтр
Управлять заголовками для страниц фильтрации можно через SEO-фильтр.
Комментарии
-
Быстрые фильтры и Метки
При выборе Метки и параметров фильтра всё работает, показываются результаты отбора, но только, если у товаров с Меткой есть свойства, соответствующие выбранным параметрам. Если же у товаров с Меткой нет такого свойства, то показывается страница 404. А хотелось бы, чтобы, показывалось сообщение, что нет товаров соответствующих запросу. Как это сделать или я что-то недоработал?
Без темы
В контроллере показа имеется условие, что если товары не найдены, при этом страница не первая или фильтрация по метке, то выводится страница 404 с кодом 410, это стандартное поведение контроллера.
-
Без темы
Есть какое-либо решение для скрытия страниц быстрого фильтра из индекса?
Когда товаров мало, куча пустых дублей получается со временем.
Через robots.txt закрывать очень муторно получается.
-
Деактивация отсутствующи свойств
Добрый день!
Планируете ли сделать доработку чтобы на уровне примененных фильтров скрывать (даективировать) те свойства в фильтрах которые отсутствуют у товаров в текущей выборке? Очень нужная функция.
-
Запрет индексации только при незаполненном SEO фильтре
Здравствуйте!
Как правильнее запретить индексацию не всем страницам фильтра, а только тем которые не прописаны в SEO-фильтре?
Такой кустарный способ работает. Правильно ли это или есть какой-то встроенный метод чтобы сразу проверить без перебора всех объектов страницы?
$objects = Core_Page::instance()->object->getEntities();
foreach($objects as $object) {
if (get_class($object) == 'Shop_Filter_Seo_Model') $seo_filter = true;
}
?>Без темы
В версии 6.9.4 у контроллера показа магазина Shop_Controller_Show появился filterSeo, в котором будет объект Shop_Filter_Seo, если он был применен или NULL [доступно после вызова parseUrl()]
Соответственно проверить можно на if (is_null($oShop_Controller_Show->filterSeo)) { .... }