Умный фильтр Битрикс. Сниппеты catalog.smart.filter

Раскрывающиеся блоки свойств

Не путать с DROPDOWN.

Верстка (id, классы, data) максимально сохранена к шаблонному дефолтному варианту.

Кейс

  • По клику на свойство необходимо отобразить раскрывающийся блок.

  • При клике на другое свойство, текущий открытый блок должен закрыться.

  • При клике ВНЕ элементов фильтра, должны закрыться Все открытые блоки (если есть возможность открывать несколько блоков или один текущий).

Работаем с catalog.smart.filter/имя_шаблона/script.js

За открытие закрытие блока отвечает метод hideFilterProps

Анимация реализована через

new BX.easing({
    duration : 10,
    start : { opacity: 1,  height: filterBlock.offsetHeight },
    finish : { opacity: 0, height:0 },
    // transition : BX.easing.transitions.quart, // Эффект анимации. https://dev.1c-bitrix.ru/api_help/js_lib/animation/animation.php
    step : function(state){
        filterBlock.style.opacity = state.opacity;
        filterBlock.style.height = state.height + "px";
        filterBlock.style.overflow = 'hidden'; // Иногда при закрытии блока, содержимое вываливается за пределы. Исправляем.
    },
    complete : function() {
        filterBlock.setAttribute("style", "");
        BX.removeClass(obj, "bx-active");
    }
}).animate();

При инициализации получим все блоки со свойствами, которые у нас есть и запустим функцию прослушки клика.

function JCSmartFilter(ajaxURL, viewMode, params)
{
    // ...

    this.filterItemsContainer = document.querySelector('.filter-items')
    this.filterPropsDropdowns = this.filterItemsContainer.querySelectorAll('[data-role="bx_filter_block"]'); // Все блоки
    this.listenClickOutsideFilterProps(); // Клик

    // ...
}

hideFilterProps

JCSmartFilter.prototype.hideFilterProps = function(element)
{
    var obj = element.parentNode,
        filterBlock = obj.querySelector("[data-role='bx_filter_block']"),
        propAngle = obj.querySelector("[data-role='prop_angle']");

    this.closeOtherPropsDropdowns(filterBlock); // Наш метод, который закрывает все блоки, кроме того по которому кликнули.

    if(BX.hasClass(obj, "bx-active"))
    {
        new BX.easing({
            duration : 10,
            start : { opacity: 1,  height: filterBlock.offsetHeight },
            finish : { opacity: 0, height:0 },
            // transition : BX.easing.transitions.quart,
            step : function(state){
                filterBlock.style.opacity = state.opacity;
                filterBlock.style.height = state.height + "px";
                filterBlock.style.overflow = 'hidden';
            },
            complete : function() {
                filterBlock.setAttribute("style", "");
                BX.removeClass(obj, "bx-active");
            }
        }).animate();

        BX.addClass(propAngle, "fa-angle-down");
        BX.removeClass(propAngle, "fa-angle-up");

    }
    else
    {
        filterBlock.style.display = "block";
        filterBlock.style.opacity = 0;
        filterBlock.style.height = "auto";

        var obj_children_height = filterBlock.offsetHeight;
        filterBlock.style.height = 0;

        new BX.easing({
            duration : 10,
            start : { opacity: 0,  height: 0 },
            finish : { opacity: 1, height: obj_children_height },
            // transition : BX.easing.transitions.quart,
            step : function(state){
                filterBlock.style.opacity = state.opacity;
                filterBlock.style.height = state.height + "px";
            },
            complete : function() {
            }
        }).animate();

        BX.addClass(obj, "bx-active");
        BX.removeClass(propAngle, "fa-angle-down");
        BX.addClass(propAngle, "fa-angle-up");
    }
};

listenClickOutsideFilterProps

/**
 * Отслеживает клики ВНЕ блоков свойств фильтра.
 * Стартует событие из инициализации.
 */
JCSmartFilter.prototype.listenClickOutsideFilterProps = function()
{
    if(this.filterItemsContainer) {
        if(this.filterPropsDropdowns.length > 0) {
            document.addEventListener('click', (e) => {
                if (!e.composedPath().includes(this.filterItemsContainer)) {
                    this.closeAllPropsDropdowns()
                }
            })
        }
    }
};

closeOtherPropsDropdowns

/**
 * Закрывает все блоки свойств фильтра КРОМЕ того, который открыли.
 */
JCSmartFilter.prototype.closeOtherPropsDropdowns = function(filterBlock)
{
    if(this.filterPropsDropdowns.length > 1) {
        let allFilterBlocks = Array.from(this.filterPropsDropdowns);
        let otherFilterBlocks = allFilterBlocks.filter(item => item !== filterBlock)

        otherFilterBlocks.forEach(otherFilterBlock => {
            this.closePropDropdown(otherFilterBlock)
        })
    } else {

    }
};

closeAllPropsDropdowns

/**
 * Закрывает все блоки свойств фильтра.
 */
JCSmartFilter.prototype.closeAllPropsDropdowns = function()
{
    // Наличие this.filterPropsDropdowns проверили Выше.
    this.filterPropsDropdowns.forEach(filterBlock => {
        this.closePropDropdown(filterBlock)
    })
};

closeOtherPropsDropdowns

/**
 * Закрывает все блоки свойств фильтра КРОМЕ того, который открыли.
 */
JCSmartFilter.prototype.closeOtherPropsDropdowns = function(filterBlock)
{
    if(this.filterPropsDropdowns.length > 1) {
        let allFilterBlocks = Array.from(this.filterPropsDropdowns);
        let otherFilterBlocks = allFilterBlocks.filter(item => item !== filterBlock)

        otherFilterBlocks.forEach(otherFilterBlock => {
            this.closePropDropdown(otherFilterBlock)
        })
    } else {

    }
};

closePropDropdown

/**
 * Закрывает один блок свойств фильтра.
 * Принимает filterBlock = div.data-role="bx_filter_block"
 */
JCSmartFilter.prototype.closePropDropdown = function(filterBlock)
{
    var obj = filterBlock.parentNode,
        propAngle = obj.querySelector("[data-role='prop_angle']");

    new BX.easing({
        duration : 10,
        start : { opacity: 1,  height: filterBlock.offsetHeight },
        finish : { opacity: 0, height:0 },
        // transition : BX.easing.transitions.quart,
        step : function(state){
            filterBlock.style.opacity = state.opacity;
            filterBlock.style.height = state.height + "px";
            filterBlock.style.overflow = 'hidden';
        },
        complete : function() {
            filterBlock.setAttribute("style", "");
            BX.removeClass(obj, "bx-active");
        }
    }).animate();

    BX.addClass(propAngle, "fa-angle-down");
    BX.removeClass(propAngle, "fa-angle-up");
};

Поиск по буквам в фильтре

catalog.smart.filter/имя_шаблона/script.js

/**
 * Поиск по буквам в фильтре.
 *
 * Обязательные атрибуты для поиска:
 * * data-search-item - контейнер с элементом.
 * * data-search-value - контейнер со значением, который ищем.
 */
JCSmartFilter.prototype.searchByLetters = function (element) {
    // Объявлять переменные.
    let input, filter, ul, li, a, i, txtValue;
    input = element
    filter = input.value.toUpperCase();
    ul = element.parentElement
    li = ul.querySelectorAll('[data-search-item]')
    // Скрываем все, что не соответствует запросу.
    for (i = 0; i < li.length; i++) {
        a = li[i].querySelector('[data-search-value]');
        txtValue = a.textContent || a.innerText;
        if (txtValue.toUpperCase().indexOf(filter) > -1) {
            li[i].style.display = "";
        } else {
            li[i].style.display = "none";
        }
    }

};

html

<div class="bx-filter-parameters-box bx-active">
        <span class="bx-filter-container-modef"></span>
        <div class="bx-filter-parameters-box-title" onclick="smartFilter.hideFilterProps(this)">
                            <span class="bx-filter-parameters-box-hint">Входное напряжение, V                                                               
                                <i  data-role="prop_angle" class="fa fa-angle-down"></i>
                            </span>
        </div>

        <div class="bx-filter-block" data-role="bx_filter_block" style="">
            <div class="bx-filter-parameters-box-container">
                <input class="filter_search" type="text" autocomplete="off" placeholder="Быстрый поиск"
                       onkeyup="smartFilter.searchByLetters(this)" wfd-id="id23">
                <div class="checkbox" data-search-item="">
                    <label data-role="label_arrFilter_884_2537871056" class="bx-filter-param-label "
                           for="arrFilter_884_2537871056">
                        <input type="checkbox" value="Y" name="arrFilter_884_2537871056" id="arrFilter_884_2537871056"
                               onclick="smartFilter.click(this)" wfd-id="id24">
                        <span class="ui_checkbox"></span>
                        <span class="bx-filter-param-text" data-search-value="" title="AC 220 V">AC 220 V</span>

                    </label>
                </div>
                <div class="checkbox" data-search-item="">
                    <label data-role="label_arrFilter_884_3929219733" class="bx-filter-param-label "
                           for="arrFilter_884_3929219733">
                        <input type="checkbox" value="Y" name="arrFilter_884_3929219733" id="arrFilter_884_3929219733"
                               onclick="smartFilter.click(this)" wfd-id="id25">
                        <span class="ui_checkbox"></span>
                        <span class="bx-filter-param-text" data-search-value="" title="DC 5 V">DC 5 V</span>

                    </label>
                </div>
                <div class="checkbox" data-search-item="">
                    <label data-role="label_arrFilter_884_2227739503" class="bx-filter-param-label "
                           for="arrFilter_884_2227739503">
                        <input type="checkbox" value="Y" name="arrFilter_884_2227739503" id="arrFilter_884_2227739503"
                               onclick="smartFilter.click(this)" wfd-id="id26">
                        <span class="ui_checkbox"></span>
                        <span class="bx-filter-param-text" data-search-value="" title="AC 12 V">AC 12 V</span>

                    </label>
                </div>
                <div class="checkbox" data-search-item="">
                    <label data-role="label_arrFilter_884_2208653174" class="bx-filter-param-label "
                           for="arrFilter_884_2208653174">
                        <input type="checkbox" value="Y" name="arrFilter_884_2208653174" id="arrFilter_884_2208653174"
                               onclick="smartFilter.click(this)" wfd-id="id27">
                        <span class="ui_checkbox"></span>
                        <span class="bx-filter-param-text" data-search-value="" title="DC 12 V">DC 12 V</span>

                    </label>
                </div>
                <div class="checkbox" data-search-item="">
                    <label data-role="label_arrFilter_884_4104286176" class="bx-filter-param-label "
                           for="arrFilter_884_4104286176">
                        <input type="checkbox" value="Y" name="arrFilter_884_4104286176" id="arrFilter_884_4104286176"
                               onclick="smartFilter.click(this)" wfd-id="id28">
                        <span class="ui_checkbox"></span>
                        <span class="bx-filter-param-text" data-search-value="" title="DC 24 V">DC 24 V</span>

                    </label>
                </div>
                <div class="checkbox _hidden _isShown" data-search-item="">
                    <label data-role="label_arrFilter_884_1679651441" class="bx-filter-param-label "
                           for="arrFilter_884_1679651441">
                        <input type="checkbox" value="Y" name="arrFilter_884_1679651441" id="arrFilter_884_1679651441"
                               onclick="smartFilter.click(this)" wfd-id="id29">
                        <span class="ui_checkbox"></span>
                        <span class="bx-filter-param-text" data-search-value="" title="DC 220 V">DC 220 V</span>

                    </label>
                </div>
                <div class="checkbox _hidden _isShown" data-search-item="">
                    <label data-role="label_arrFilter_884_1943915266" class="bx-filter-param-label "
                           for="arrFilter_884_1943915266">
                        <input type="checkbox" value="Y" name="arrFilter_884_1943915266" id="arrFilter_884_1943915266"
                               onclick="smartFilter.click(this)" wfd-id="id30">
                        <span class="ui_checkbox"></span>
                        <span class="bx-filter-param-text" data-search-value="" title="12">12</span>

                    </label>
                </div>
                <div class="checkbox _hidden _isShown" data-search-item="">
                    <label data-role="label_arrFilter_884_3939801784" class="bx-filter-param-label "
                           for="arrFilter_884_3939801784">
                        <input type="checkbox" value="Y" name="arrFilter_884_3939801784" id="arrFilter_884_3939801784"
                               onclick="smartFilter.click(this)" wfd-id="id31">
                        <span class="ui_checkbox"></span>
                        <span class="bx-filter-param-text" data-search-value="" title="5">5</span>

                    </label>
                </div>
                <div class="checkbox _hidden _isShown" data-search-item="">
                    <label data-role="label_arrFilter_884_167867302" class="bx-filter-param-label "
                           for="arrFilter_884_167867302">
                        <input type="checkbox" value="Y" name="arrFilter_884_167867302" id="arrFilter_884_167867302"
                               onclick="smartFilter.click(this)" wfd-id="id32">
                        <span class="ui_checkbox"></span>
                        <span class="bx-filter-param-text" data-search-value="" title="24">24</span>

                    </label>
                </div>

            </div>
            <a class="show_all _active" onclick="smartFilter.showAll(this)" href="javascript:void(0);" rel="nofollow">
                Показать все <i data-role="prop_angle"></i>
            </a>
        </div>
    </div>


Показываем скрытые элементы при клике на кнопку "Показать еще".

/**
 * Показываем скрытые элементы при клике на кнопку "Показать еще".
 */
JCSmartFilter.prototype.showAll = function (element) {
    let parent = element.parentElement
    let hiddenElements = parent.querySelectorAll('._hidden')

    if (element.classList.contains('_active')) {
        this.showAllClose(hiddenElements, element, parent)
    } else {
        this.showAllOpen(hiddenElements, element, parent)
    }
};

JCSmartFilter.prototype.showAllOpen = function (elements, button, parent) {
    parent.setAttribute('style', '')
    elements.forEach(el => {
        el.classList.add('_isShown')
    })
    button.classList.add('_active')

}

JCSmartFilter.prototype.showAllClose = function (elements, button, parent) {
    parent.setAttribute('style', '')
    elements.forEach(el => {
        el.classList.remove('_isShown')
    })
    button.classList.remove('_active')

}

Еще вариант

JCSmartFilter.prototype.showMore = function(button) {
    let container = button.parentNode;
    let elements = container.querySelectorAll('.checkbox');
    let isShowing = button.textContent === 'Показать еще';

    elements.forEach(element => {
        if (element.classList.contains('d-none') || element.classList.contains('show')) {
            if (isShowing) {
                element.classList.remove('d-none');
                element.classList.add('show');
            } else {
                element.classList.remove('show');
                element.classList.add('d-none');
            }
        }
    });

    button.textContent = isShowing ? 'Скрыть' : 'Показать еще';
};
<div onclick="JCSmartFilter.prototype.showMore(this)">Показать еще</div>

template.php

default://CHECKBOXES
                                    ?>
                                    <div class="col-xs-12">
                                        <? $i = 1;
                                        foreach($arItem["VALUES"] as $val => $ar){?>
                                            <div class="checkbox<?= $i > 7 ? ' d-none' : '' ?>">
                                                <label data-role="label_<?=$ar["CONTROL_ID"]?>" class="bx-filter-param-label <? echo $ar["DISABLED"] ? 'disabled': '' ?>" for="<? echo $ar["CONTROL_ID"] ?>">
                                                    <span class="bx-filter-input-checkbox">
                                                        <input
                                                            type="checkbox"
                                                            value="<? echo $ar["HTML_VALUE"] ?>"
                                                            name="<? echo $ar["CONTROL_NAME"] ?>"
                                                            id="<? echo $ar["CONTROL_ID"] ?>"
                                                            <? echo $ar["CHECKED"]? 'checked="checked"': '' ?>
                                                            onclick="smartFilter.click(this)"
                                                            data-label-text="<?=$ar["VALUE"]?>"
                                                        />
                                                        <span class="bx-filter-param-text" title="<?=$ar["VALUE"];?>"><?=$ar["VALUE"];?><?
                                                        if ($arParams["DISPLAY_ELEMENT_COUNT"] !== "N" && isset($ar["ELEMENT_COUNT"])):
                                                            ?>&nbsp;(<span data-role="count_<?=$ar["CONTROL_ID"]?>"><? echo $ar["ELEMENT_COUNT"]; ?></span>)<?
                                                        endif;?></span>
                                                    </span>
                                                </label>
                                            </div>
                                        <? $i++;
                                        } ?>
                                        <? if($i > 7) { ?>
                                            <div onclick="JCSmartFilter.prototype.showMore(this)">Показать еще</div>
                                        <? } ?>
                                    </div>
                            <?
                            }
                            ?>

Прячем лейбл с результатами фильтра через N сек

/**
 * Прячем лейбл с результатами фильтра.
 */
JCSmartFilter.prototype.hidePopupResult = function (modef) {

    setTimeout(() => {
        if (modef.style.display === 'inline-block') {
            modef.style.display = 'none';
        }
    }, 5000)
}

Вызываем в postHandler


Индикатор выбранного фильтра

  • При выборе свойства отображается индикатор.

  • При загрузке страницы отображается индикатор.

  • У примененного свойства (текущего) индикатор не убирается пока не сбросить фильтр

template.php

<div class="filter-item bx-filter-parameters-box<?= $arItem["DISPLAY_EXPANDED"] === "Y" ? ' _has_checked _current_filter' : ''?>">

JS

JCSmartFilter.prototype.toggleFilterCheckedIndicator = function (checkbox) {
    this.hasCheckedFilterProps(checkbox.closest('.bx-filter-parameters-box'))
};

JCSmartFilter.prototype.hasCheckedFilterProps = function (propContainer) {
    let hasCheckedProps = propContainer.querySelectorAll('.bx-filter-parameters-box-container input:checked').length > 0;
    let hasCurrentFilter = propContainer.classList.contains('_current_filter');

    if (!hasCurrentFilter) {
        propContainer.classList.toggle('_has_checked', hasCheckedProps);
    }
}

Смотрим что отрабатывает по клику на checkbox.

Вызываем наш обработчик в click()

JCSmartFilter.prototype.click = function (checkbox) {

    this.toggleFilterCheckedIndicator(checkbox);

    if (!!this.timer) {
        clearTimeout(this.timer);
    }

    this.timer = setTimeout(BX.delegate(function () {
        this.reload(checkbox);
    }, this), 500);
};