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