Фильтр и быстрый фильтр

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

Фильтр товаров в HostCMS

Как начать использовать?

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

Перестроение быстрого фильтра

Для перестроение быстрого фильтра выбранного магазина нажмите на пиктограмму в столбце с фильтром, затем дождитесь окончания перестроения фильтра.

ЧПУ для фильтра

Фильтр использует ЧПУ, после пути к группе указывается название опции фильтра, затем значения (если тип фильтра это предполагает), например, /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)

Ограничение по цене, где цена больше 100:

->addFilter('price', '>', 100)

Ограничение по основным свойствам ('length', 'width', 'height', 'weight'), пример, где длина больше или равно 50: * с версии 6.9.5

->addFilter('length', '>=', 50)

Указание поля и направления сортировки

Указание направления сортировки возможно напрямую контроллеру показа методом ->orderBy($column, $direction = 'ASC'), при сортировке по цене вы можете указывать price или absolute_price (синонимы), при этом расчет absolute_price будет добавлен автоматически.

Подсчет количества соответствующих свойству значений в текущей группе

По умолчанию подсчет количества соответствующих свойству значений не производится. Для включения, необходимо контроллеру показа магазина добавить параметр ->filterCounts(TRUE). После чего в XML пойдет тег <filter_counts> с рассчитанным по текущим параметрам фильтра значением для каждого свойства.

Если требуется убрать отсутствующие значения, то необходимо внести изменения в темплейт <xsl:template match="list/list_item"> в XSL-шаблоне фильтр. После строки вносится условие <xsl:if test="../../filter_counts/count[@id = $list_item_id]/node()">...</xsl:if> в которое помещается все остальное тело темплейта.

Для свойств типа число при выбранном способе отображения "от .. до" будут рассчитаны минимальное и максимальное значение, которые помещаются в теги <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 = intval(Core_Array::getGet('sorting'));
		
		($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">property_<xsl:value-of select="@id"/></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: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: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-шаблоне фильтра.

  1. Для производителей в темплейте "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}"
  2. Для элементов списка в темплейте "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-фильтр.

Не нашли ответ на свой вопрос в документации? Направьте обращение в службу поддержки или онлайн чат.

Комментарии

  • Без темы

    Есть какое-либо решение для скрытия страниц быстрого фильтра из индекса?
    Когда товаров мало, куча пустых дублей получается со временем.
    Через robots.txt закрывать очень муторно получается.

    26.12.2022 09:53:13
    Barbaros
    Barbaros

    Без темы

    Прошу прощения, решение же выше описано!

    26.12.2022 11:23:34
    Barbaros
    Barbaros
  • Деактивация отсутствующи свойств

    Добрый день!
    Планируете ли сделать доработку чтобы на уровне примененных фильтров скрывать (даективировать) те свойства в фильтрах которые отсутствуют у товаров в текущей выборке? Очень нужная функция.

    28.10.2020 11:01:33
    iqsite
  • Запрет индексации только при незаполненном SEO фильтре

    Здравствуйте!
    Как правильнее запретить индексацию не всем страницам фильтра, а только тем которые не прописаны в SEO-фильтре?

    Такой кустарный способ работает. Правильно ли это или есть какой-то встроенный метод чтобы сразу проверить без перебора всех объектов страницы?

    $objects = Core_Page::instance()->object->getEntities();
    foreach($objects as $object) {
    if (get_class($object) == 'Shop_Filter_Seo_Model') $seo_filter = true;

    }
    ?>

    01.06.2020 05:16:58
    nikolajgromkov

    Без темы

    В версии 6.9.4 у контроллера показа магазина Shop_Controller_Show появился filterSeo, в котором будет объект Shop_Filter_Seo, если он был применен или NULL [доступно после вызова parseUrl()]
    Соответственно проверить можно на if (is_null($oShop_Controller_Show->filterSeo)) { .... }

    01.06.2020 17:03:14
    hostcms

    Круто!

    Спасибо большое!

    02.06.2020 05:54:27
    nikolajgromkov
  • Без темы

    список всех чпу страниц сформированных быстрым фильтром можно посмотреть где то?
    как их вычислить?проверить?

    06.12.2019 22:42:16
    Puma

    Без темы

    Быстрый фильтр не генерирует страницы, он обрабатывает URL, сформированный при задании пользователем опций фильтра.

    09.12.2019 09:02:37
    hostcms