Фильтр и быстрый фильтр
Быстрые фильтры значительно ускоряют работу фильтров в интернет-магазине, позволяют в фильтре рассчитать и показать количество отфильтрованных и подлежащих отображению элементов. Быстрые фильтры работают "прозрачно" для администратора и при отключении быстрой фильтрации для магазина, фильтр будет работать через обычную фильтрацию.
Как начать использовать?
Включается быстрый фильтр в опциях магазина. Быстрый фильтр строится на основании дополнительных свойств магазина и цены товаров. После включения фильтра необходимо выполнить построение таблицы, для этого нажмите на пиктограмму фильтра и дождитесь окончания построения индекса. При редактировании, импорте товаров, индекс перестраивается автоматически.
Перестроение быстрого фильтра
Для перестроение быстрого фильтра выбранного магазина нажмите на пиктограмму в столбце с фильтром, затем дождитесь окончания перестроения фильтра.
ЧПУ для фильтра
Фильтр использует ЧПУ, после пути к группе указывается название опции фильтра, затем значения (если тип фильтра это предполагает), например, /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-шаблоне фильтр. После строки
вносится условие
в которое помещается все остальное тело темплейта.
Для свойств типа число при выбранном способе отображения "от .. до" будут рассчитаны минимальное и максимальное значение, которые помещаются в теги <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-шаблоне фильтра.
- Для производителей в темплейте "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-фильтр.
Комментарии
-
Без темы
Есть какое-либо решение для скрытия страниц быстрого фильтра из индекса?
Когда товаров мало, куча пустых дублей получается со временем.
Через 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)) { .... }