├── Block ├── AjaxScript.php └── LayeredNavigation │ ├── RenderLayered.php │ └── SliderRenderer.php ├── Controller └── Router.php ├── Model ├── Layer │ ├── Filter │ │ ├── Attribute.php │ │ ├── Category.php │ │ ├── Decimal.php │ │ ├── Item.php │ │ ├── Price.php │ │ └── SliderTrait.php │ └── ItemCollectionProvider.php ├── ResourceModel │ └── Fulltext │ │ └── Collection.php ├── Search │ └── Dynamic │ │ └── Algorithm.php └── Url │ ├── Builder.php │ ├── Hydrator.php │ └── Translit.php ├── Plugin ├── CategoryAggregation.php ├── CategoryView.php ├── CategoryViewBlock.php ├── FilterRenderer.php ├── Preprocessor.php ├── SearchView.php └── State.php ├── README.md ├── composer.json ├── etc ├── adminhtml │ └── system.xml ├── config.xml ├── di.xml ├── frontend │ └── di.xml └── module.xml ├── registration.php └── view └── frontend ├── layout ├── catalog_category_view.xml └── catalogsearch_result_index.xml ├── requirejs-config.js ├── templates ├── ajax.phtml └── slider.phtml └── web ├── js ├── navigation.js └── slider.js └── styles.css /Block/AjaxScript.php: -------------------------------------------------------------------------------- 1 | !$this->getIsAjax(), 22 | 'filtersContainer' => '#layered-filter-block', 23 | 'productsContainer' => '.' . \Niks\LayeredNavigation\Plugin\CategoryViewBlock::PRODUCT_LIST_WRAPPER 24 | ]; 25 | return json_encode($config); 26 | } 27 | 28 | /** 29 | * Get ajax option 30 | * 31 | * @return string 32 | */ 33 | protected function getIsAjax() 34 | { 35 | return $this->_scopeConfig->getValue( 36 | 'niks_layered_navigation/general/ajax', 37 | \Magento\Store\Model\ScopeInterface::SCOPE_STORE, 38 | $this->_storeManager->getStore()->getId() 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Block/LayeredNavigation/RenderLayered.php: -------------------------------------------------------------------------------- 1 | _urlBuilder->getFilterUrl( 21 | $this->filter->getRequestVar(), 22 | $optionId, 23 | [] 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Block/LayeredNavigation/SliderRenderer.php: -------------------------------------------------------------------------------- 1 | getFilter()->getCurrentValue(); 24 | if (isset($currentValues[0])) { 25 | return $currentValues[0]; 26 | } 27 | return $this->getFilter()->getMin(); 28 | } 29 | 30 | public function getTo() 31 | { 32 | $currentValues = $this->getFilter()->getCurrentValue(); 33 | if (isset($currentValues[1])) { 34 | return $currentValues[1]; 35 | } 36 | return $this->getFilter()->getMax(); 37 | } 38 | 39 | public function getPriceRangeUrlTemplate() 40 | { 41 | return $this->_urlBuilder->getFilterUrl( 42 | $this->getFilter()->getRequestVar(), 43 | '{{from}}-{{to}}', 44 | [], 45 | true 46 | ); 47 | } 48 | 49 | public function getCurrencySymbol() 50 | { 51 | return $this->_storeManager->getStore()->getCurrentCurrency()->getCurrencySymbol(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Controller/Router.php: -------------------------------------------------------------------------------- 1 | urlHydrator = $urlHydrator; 41 | $this->registry = $registry; 42 | parent::__construct($actionFactory, $url, $storeManager, $response, $urlFinder); 43 | } 44 | 45 | /** 46 | * Match corresponding navigation URL and modify request 47 | * 48 | * @param \Magento\Framework\App\RequestInterface $request 49 | * @return \Magento\Framework\App\ActionInterface|null 50 | */ 51 | public function match(\Magento\Framework\App\RequestInterface $request) 52 | { 53 | $parentMatch = parent::match($request); 54 | if ($parentMatch !== null) { 55 | $request->setAlias( 56 | Builder::REWRITE_NAVIGATION_PATH_ALIAS, 57 | ltrim($request->getOriginalPathInfo(), '/') 58 | ); 59 | return $parentMatch; 60 | } 61 | 62 | $filterString = '/' . $this->urlHydrator->getFilterString($request->getPathInfo()); 63 | $originalPath = preg_replace('%' . $filterString . '(?!.*' . $filterString . '.*)%', '', $request->getPathInfo()); 64 | 65 | $rewrite = $this->getRewrite($originalPath, $this->storeManager->getStore()->getId()); 66 | if ($rewrite === null) { 67 | return null; 68 | } 69 | if ($rewrite->getRedirectType()) { 70 | return $this->processRedirect($request, $rewrite); 71 | } 72 | 73 | $this->registry->register('current_category_id', $rewrite->getEntityId()); 74 | $filterParams = $this->urlHydrator->extract($request->getPathInfo()); 75 | if (empty($filterParams)) { 76 | return null; 77 | } 78 | $request->setParam('navigation_filters', $filterParams); 79 | $request->setAlias(UrlInterface::REWRITE_REQUEST_PATH_ALIAS, ltrim($request->getPathInfo(), '/')); 80 | $request->setAlias(Builder::REWRITE_NAVIGATION_PATH_ALIAS, $rewrite->getRequestPath()); 81 | $request->setPathInfo('/' . $rewrite->getTargetPath()); 82 | return $this->actionFactory->create(\Magento\Framework\App\Action\Forward::class); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Model/Layer/Filter/Attribute.php: -------------------------------------------------------------------------------- 1 | tagFilter = $tagFilter; 52 | $this->urlBuilder = $urlBuilder; 53 | $this->collectionProvider = $collectionProvider; 54 | } 55 | 56 | /** 57 | * Apply attribute option filter to product collection 58 | * 59 | * @param \Magento\Framework\App\RequestInterface $request 60 | * @return $this 61 | * @throws \Magento\Framework\Exception\LocalizedException 62 | */ 63 | public function apply(\Magento\Framework\App\RequestInterface $request) 64 | { 65 | $values = $this->urlBuilder->getValuesFromUrl($this->_requestVar); 66 | if (!$values) { 67 | return $this; 68 | } 69 | 70 | /** @var \Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection $productCollection */ 71 | $productCollection = $this->getLayer() 72 | ->getProductCollection(); 73 | $this->applyToCollection($productCollection); 74 | 75 | foreach ($values as $value) { 76 | $label = $this->getOptionText($value); 77 | $this->getLayer() 78 | ->getState() 79 | ->addFilter($this->_createItem($label, $value)); 80 | } 81 | return $this; 82 | } 83 | 84 | /** 85 | * Apply current filter to collection 86 | * 87 | * @return Attribute 88 | */ 89 | public function applyToCollection($collection) 90 | { 91 | $attribute = $this->getAttributeModel(); 92 | $attributeValue = $this->urlBuilder->getValuesFromUrl($this->_requestVar); 93 | if (empty($attributeValue)) { 94 | return $this; 95 | } 96 | $collection->addFieldToFilter($attribute->getAttributeCode(), array('in' => $attributeValue)); 97 | } 98 | 99 | /** 100 | * Get data array for building attribute filter items 101 | * 102 | * @return array 103 | * @throws \Magento\Framework\Exception\LocalizedException 104 | */ 105 | protected function _getItemsData() 106 | { 107 | $values = $this->urlBuilder->getValuesFromUrl($this->_requestVar); 108 | if (!$values) { 109 | return parent::_getItemsData(); 110 | } 111 | 112 | /** @var \Niks\LayeredNavigation\Model\ResourceModel\Fulltext\Collection $productCollection */ 113 | $productCollection = $this->getLayer() 114 | ->getProductCollection(); 115 | 116 | /** @var \Niks\LayeredNavigation\Model\ResourceModel\Fulltext\Collection $collection */ 117 | $collection = $this->collectionProvider->getCollection($this->getLayer()->getCurrentCategory()); 118 | $collection->updateSearchCriteriaBuilder(); 119 | $this->getLayer()->prepareProductCollection($collection); 120 | foreach ($productCollection->getAddedFilters() as $field => $condition) { 121 | if ($this->getAttributeModel()->getAttributeCode() == $field) { 122 | continue; 123 | } 124 | $collection->addFieldToFilter($field, $condition); 125 | } 126 | 127 | $attribute = $this->getAttributeModel(); 128 | 129 | $optionsFacetedData = $collection->getFacetedData($attribute->getAttributeCode()); 130 | 131 | if ($attribute->getFrontendInput() == 'multiselect') { 132 | $originalFacetedData = $productCollection->getFacetedData($attribute->getAttributeCode()); 133 | foreach ($originalFacetedData as $key => $optionData) { 134 | $optionsFacetedData[$key]['count'] -= $optionData['count']; 135 | if ($optionsFacetedData[$key]['count'] <= 0) { 136 | unset($optionsFacetedData[$key]['count']); 137 | } 138 | } 139 | } 140 | 141 | $options = $attribute->getFrontend() 142 | ->getSelectOptions(); 143 | 144 | foreach ($options as $option) { 145 | if (empty($option['value']) || in_array($option['value'], $values)) { 146 | continue; 147 | } 148 | // Check filter type 149 | if (empty($optionsFacetedData[$option['value']]['count'])) { 150 | continue; 151 | } 152 | $this->itemDataBuilder->addItemData( 153 | $this->tagFilter->filter($option['label']), 154 | $option['value'], 155 | isset($optionsFacetedData[$option['value']]['count']) ? '+' . $optionsFacetedData[$option['value']]['count'] : 0 156 | ); 157 | } 158 | 159 | return $this->itemDataBuilder->build(); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /Model/Layer/Filter/Category.php: -------------------------------------------------------------------------------- 1 | escaper = $escaper; 63 | $this->dataProvider = $categoryDataProviderFactory->create(['layer' => $this->getLayer()]); 64 | $this->urlBuilder = $urlBuilder; 65 | $this->collectionProvider = $collectionProvider; 66 | } 67 | 68 | /** 69 | * Apply category filter to product collection 70 | * 71 | * @param \Magento\Framework\App\RequestInterface $request 72 | * @return $this 73 | */ 74 | public function apply(\Magento\Framework\App\RequestInterface $request) 75 | { 76 | $values = $this->urlBuilder->getValuesFromUrl($this->_requestVar); 77 | if (!$values) { 78 | return $this; 79 | } 80 | 81 | /** @var \Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection $productCollection */ 82 | $productCollection = $this->getLayer() 83 | ->getProductCollection(); 84 | $this->applyToCollection($productCollection); 85 | 86 | /** @var \Magento\Catalog\Model\ResourceModel\Category\Collection $categoryCollection */ 87 | $categoryCollection = ObjectManager::getInstance() 88 | ->create(\Magento\Catalog\Model\ResourceModel\Category\Collection::class); 89 | $categoryCollection->addAttributeToFilter('entity_id', ['in' => $values])->addAttributeToSelect('name'); 90 | $categoryItems = $categoryCollection->getItems(); 91 | 92 | foreach ($values as $value) { 93 | if (isset($categoryItems[$value])) { 94 | $category = $categoryItems[$value]; 95 | $label = $category->getName(); 96 | $this->getLayer() 97 | ->getState() 98 | ->addFilter($this->_createItem($label, $value)); 99 | } 100 | } 101 | return $this; 102 | } 103 | 104 | /** 105 | * Get data array for building category filter items 106 | * 107 | * @return array 108 | */ 109 | protected function _getItemsData() 110 | { 111 | $values = $this->urlBuilder->getValuesFromUrl($this->_requestVar); 112 | if (!$values) { 113 | return parent::_getItemsData(); 114 | } 115 | 116 | /** @var \Niks\LayeredNavigation\Model\ResourceModel\Fulltext\Collection $productCollection */ 117 | $productCollection = $this->getLayer()->getProductCollection(); 118 | 119 | /** @var \Niks\LayeredNavigation\Model\ResourceModel\Fulltext\Collection $collection */ 120 | $collection = $this->collectionProvider->getCollection($this->getLayer()->getCurrentCategory()); 121 | $collection->updateSearchCriteriaBuilder(); 122 | $this->getLayer()->prepareProductCollection($collection); 123 | foreach ($productCollection->getAddedFilters() as $field => $condition) { 124 | if ($field === 'category_ids') { 125 | $collection->addFieldToFilter($field, $this->getLayer()->getCurrentCategory()->getId()); 126 | continue; 127 | } 128 | $collection->addFieldToFilter($field, $condition); 129 | } 130 | 131 | $optionsFacetedData = $collection->getFacetedData('category'); 132 | $category = $this->dataProvider->getCategory(); 133 | $categories = $category->getChildrenCategories(); 134 | 135 | if ($category->getIsActive()) { 136 | foreach ($categories as $category) { 137 | if ($category->getIsActive() 138 | && isset($optionsFacetedData[$category->getId()]) 139 | && !in_array($category->getId(), $values) 140 | ) { 141 | $this->itemDataBuilder->addItemData( 142 | $this->escaper->escapeHtml($category->getName()), 143 | $category->getId(), 144 | isset($optionsFacetedData[$category->getId()]['count']) ? '+' . $optionsFacetedData[$category->getId()]['count'] : 0 145 | ); 146 | } 147 | } 148 | } 149 | return $this->itemDataBuilder->build(); 150 | } 151 | 152 | /** 153 | * Apply current filter to collection 154 | * 155 | * @param \Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection $collection 156 | * @return $this 157 | */ 158 | public function applyToCollection($collection) 159 | { 160 | $values = $this->urlBuilder->getValuesFromUrl($this->_requestVar); 161 | if (empty($values)) { 162 | return $this; 163 | } 164 | $collection->addCategoriesFilter(['in' => $values]); 165 | return $this; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /Model/Layer/Filter/Decimal.php: -------------------------------------------------------------------------------- 1 | urlBuilder = $urlBuilder; 43 | $this->collectionProvider = $collectionProvider; 44 | } 45 | 46 | /** 47 | * Apply current filter to collection 48 | * 49 | * @return Decimal 50 | */ 51 | public function applyToCollection($collection, $addFilter = false) 52 | { 53 | $values = $this->urlBuilder->getValuesFromUrl($this->_requestVar); 54 | $filter = false; 55 | if ($values) { 56 | $filter = $values[0]; 57 | } 58 | if (!$filter || is_array($filter)) { 59 | return $this; 60 | } 61 | 62 | list($from, $to) = explode('-', $filter); 63 | 64 | $collection->addFieldToFilter( 65 | $this->getAttributeModel()->getAttributeCode(), 66 | ['from' => $from, 'to' => $to] 67 | ); 68 | 69 | if ($addFilter) { 70 | $this->getLayer()->getState()->addFilter( 71 | $this->_createItem($this->renderRangeLabel(empty($from) ? 0 : $from, $to), $filter) 72 | ); 73 | } 74 | return $this; 75 | } 76 | 77 | public function getCurrentValue() 78 | { 79 | $values = $this->urlBuilder->getValuesFromUrl($this->_requestVar); 80 | $filter = false; 81 | if ($values) { 82 | $filter = $values[0]; 83 | } 84 | $filterParams = explode('-', $filter); 85 | return $filterParams; 86 | } 87 | 88 | public function getMin() 89 | { 90 | return $this->getCollectionWithoutFilter()->getMin($this->_requestVar); 91 | } 92 | 93 | public function getMax() 94 | { 95 | return $this->getCollectionWithoutFilter()->getMax($this->_requestVar); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Model/Layer/Filter/Item.php: -------------------------------------------------------------------------------- 1 | _url->getRemoveFilterUrl( 14 | $this->getFilter()->getRequestVar(), 15 | $this->getValue(), 16 | [$this->_htmlPagerBlock->getPageVarName() => null] 17 | ); 18 | } 19 | 20 | /** 21 | * Get filter item url 22 | * 23 | * @return string 24 | */ 25 | public function getUrl() 26 | { 27 | $isSingle = false; 28 | $filter = $this->getFilter(); 29 | if ($filter->hasAttributeModel() && $filter->getAttributeModel()->getBackendType() == 'decimal') { 30 | $isSingle = true; 31 | } 32 | return $this->_url->getFilterUrl( 33 | $this->getFilter()->getRequestVar(), 34 | $this->getValue(), 35 | [$this->_htmlPagerBlock->getPageVarName() => null], 36 | $isSingle 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Model/Layer/Filter/Price.php: -------------------------------------------------------------------------------- 1 | dataProvider = $dataProviderFactory->create(['layer' => $this->getLayer()]); 72 | $this->urlBuilder = $urlBuilder; 73 | $this->collectionProvider = $collectionProvider; 74 | } 75 | 76 | /** 77 | * Apply current filter to collection 78 | * 79 | * @return Attribute 80 | */ 81 | public function applyToCollection($collection, $addFilter = false) 82 | { 83 | $values = $this->urlBuilder->getValuesFromUrl($this->_requestVar); 84 | $filter = false; 85 | if ($values) { 86 | $filter = $values[0]; 87 | } 88 | 89 | $filterParams = explode(',', $filter); 90 | $filter = $this->getCurrentValue(); 91 | if (!$filter) { 92 | return $this; 93 | } 94 | 95 | if ($addFilter) { 96 | $this->dataProvider->setInterval($filter); 97 | $priorFilters = $this->dataProvider->getPriorFilters($filterParams); 98 | if ($priorFilters) { 99 | $this->dataProvider->setPriorIntervals($priorFilters); 100 | } 101 | } 102 | 103 | list($from, $to) = $filter; 104 | 105 | $collection->addFieldToFilter( 106 | 'price', 107 | ['from' => $from, 'to' => empty($to) || $from == $to ? $to : $to - self::PRICE_DELTA] 108 | ); 109 | 110 | if ($addFilter) { 111 | $this->getLayer()->getState()->addFilter( 112 | $this->_createItem($this->_renderRangeLabel(empty($from) ? 0 : $from, $to), $filter) 113 | ); 114 | } 115 | return $this; 116 | } 117 | 118 | /** 119 | * Get applied values 120 | * 121 | * @return array|bool 122 | */ 123 | public function getCurrentValue() 124 | { 125 | $values = $this->urlBuilder->getValuesFromUrl($this->_requestVar); 126 | $filter = false; 127 | if ($values) { 128 | $filter = $values[0]; 129 | } 130 | $filterParams = explode(',', $filter); 131 | return $this->dataProvider->validateFilter($filterParams[0]); 132 | } 133 | 134 | /** 135 | * Get max value 136 | * 137 | * @return float 138 | */ 139 | public function getMax() 140 | { 141 | return $this->getCollectionWithoutFilter()->getMaxPrice(); 142 | } 143 | 144 | /** 145 | * Get min value 146 | * 147 | * @return float 148 | */ 149 | public function getMin() 150 | { 151 | return $this->getCollectionWithoutFilter()->getMinPrice(); 152 | } 153 | 154 | /** 155 | * @param float $from 156 | * @return float 157 | */ 158 | protected function getTo($from) 159 | { 160 | $to = ''; 161 | $interval = $this->dataProvider->getInterval(); 162 | if ($interval && is_numeric($interval[1]) && $interval[1] > $from) { 163 | $to = $interval[1]; 164 | } 165 | return $to; 166 | } 167 | 168 | /** 169 | * @param float $from 170 | * @return float 171 | */ 172 | protected function getFrom($from) 173 | { 174 | $to = ''; 175 | $interval = $this->dataProvider->getInterval(); 176 | if ($interval && is_numeric($interval[0]) && $interval[0] < $from) { 177 | $to = $interval[0]; 178 | } 179 | return $to; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /Model/Layer/Filter/SliderTrait.php: -------------------------------------------------------------------------------- 1 | applyToCollection($this->getLayer()->getProductCollection(), true); 22 | return $this; 23 | } 24 | 25 | /** 26 | * Get collection without current filter 27 | * 28 | * @return \Niks\LayeredNavigation\Model\ResourceModel\Fulltext\Collection 29 | */ 30 | protected function getCollectionWithoutFilter() 31 | { 32 | if (!$this->_skipFilterCollection) { 33 | /** @var \Niks\LayeredNavigation\Model\ResourceModel\Fulltext\Collection $productCollection */ 34 | $productCollection = $this->getLayer() 35 | ->getProductCollection(); 36 | 37 | /** @var \Niks\LayeredNavigation\Model\ResourceModel\Fulltext\Collection $collection */ 38 | $this->_skipFilterCollection = $this->collectionProvider->getCollection($this->getLayer()->getCurrentCategory()); 39 | $this->_skipFilterCollection->updateSearchCriteriaBuilder(); 40 | $this->getLayer()->prepareProductCollection($this->_skipFilterCollection); 41 | foreach ($productCollection->getAddedFilters() as $field => $condition) { 42 | if ($this->getAttributeModel()->getAttributeCode() == $field) { 43 | continue; 44 | } 45 | $this->_skipFilterCollection->addFieldToFilter($field, $condition); 46 | } 47 | } 48 | return $this->_skipFilterCollection; 49 | } 50 | 51 | /** 52 | * Mock items for slider 53 | * 54 | * @return mixed 55 | */ 56 | protected function _getItemsData() 57 | { 58 | if ($this->isSliderEnabled()) { 59 | $this->itemDataBuilder->addItemData( 60 | true, 61 | true, 62 | true 63 | ); 64 | return $this->itemDataBuilder->build(); 65 | } 66 | return parent::_getItemsData(); 67 | } 68 | 69 | /** 70 | * Check is slider enabled 71 | * 72 | * @return bool 73 | */ 74 | protected function isSliderEnabled() 75 | { 76 | $scope = ObjectManager::getInstance()->get(\Magento\Framework\App\Config\ScopeConfigInterface::class); 77 | $storeManager = ObjectManager::getInstance()->get(StoreManagerInterface::class); 78 | 79 | return $scope->getValue( 80 | 'niks_layered_navigation/general/slider', 81 | \Magento\Store\Model\ScopeInterface::SCOPE_STORE, 82 | $storeManager->getStore()->getId() 83 | ); 84 | } 85 | } -------------------------------------------------------------------------------- /Model/Layer/ItemCollectionProvider.php: -------------------------------------------------------------------------------- 1 | collectionFactory = $collectionFactory; 25 | } 26 | 27 | /** 28 | * @param \Magento\Catalog\Model\Category $category 29 | * @return \Magento\Catalog\Model\ResourceModel\Product\Collection 30 | */ 31 | public function getCollection(\Magento\Catalog\Model\Category $category) 32 | { 33 | /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $collection */ 34 | if ($category->getParentId() == 1) { 35 | $collection = $this->collectionFactory->create(['searchRequestName' => 'quick_search_container']); 36 | } else { 37 | $collection = $this->collectionFactory->create(); 38 | $collection->addCategoryFilter($category); 39 | } 40 | return $collection; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Model/ResourceModel/Fulltext/Collection.php: -------------------------------------------------------------------------------- 1 | _addedFilters[$field] = $condition; 27 | } 28 | return parent::addFieldToFilter($field, $condition); 29 | } 30 | 31 | /** 32 | * Filter Product by Categories 33 | * 34 | * @param array $categoriesFilter 35 | * @return $this 36 | */ 37 | public function addCategoriesFilter(array $categoriesFilter) 38 | { 39 | $this->addFieldToFilter('category_ids', $categoriesFilter); 40 | return $this; 41 | } 42 | 43 | /** 44 | * Get applied filters 45 | * 46 | * @return array 47 | */ 48 | public function getAddedFilters() 49 | { 50 | return $this->_addedFilters; 51 | } 52 | 53 | public function updateSearchCriteriaBuilder() 54 | { 55 | $searchCriteriaBuilder = ObjectManager::getInstance() 56 | ->create(\Magento\Framework\Api\Search\SearchCriteriaBuilder::class); 57 | $this->setSearchCriteriaBuilder($searchCriteriaBuilder); 58 | return $this; 59 | 60 | } 61 | 62 | protected function _prepareStatisticsData() 63 | { 64 | $this->_renderFilters(); 65 | return parent::_prepareStatisticsData(); 66 | } 67 | 68 | 69 | public function getMax($code) 70 | { 71 | $data = $this->prepareDecimalData($code); 72 | return isset($data['max']) ? $data['max'] : false; 73 | } 74 | 75 | public function getMin($code) 76 | { 77 | $data = $this->prepareDecimalData($code); 78 | return isset($data['min']) ? $data['min'] : false; 79 | } 80 | 81 | protected $decimalData = []; 82 | 83 | protected function prepareDecimalData($code) 84 | { 85 | if (!isset($this->decimalData[$code])) { 86 | $this->joinAttribute($code, 'catalog_product/' . $code, 'entity_id'); 87 | $fieldName = $this->_getAttributeFieldName($code); 88 | $sqlEndPart = ') * ' . $this->getCurrencyRate() . ', 2)'; 89 | $select = clone $this->getSelect(); 90 | $select->reset(\Magento\Framework\DB\Select::COLUMNS); 91 | $select->columns( 92 | [ 93 | 'max' => 'ROUND(MAX(' . $fieldName . $sqlEndPart, 94 | 'min' => 'ROUND(MIN(' . $fieldName . $sqlEndPart, 95 | ] 96 | ); 97 | $row = $this->getConnection()->fetchRow($select, $this->_bindParams, \Zend_Db::FETCH_NUM); 98 | $this->decimalData[$code] = [ 99 | 'min' => $row[1], 100 | 'max' => $row[0] 101 | ]; 102 | } 103 | return $this->decimalData[$code]; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Model/Search/Dynamic/Algorithm.php: -------------------------------------------------------------------------------- 1 | _lastValueLimiter = [null, 0]; 15 | return parent::calculateSeparators($interval); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Model/Url/Builder.php: -------------------------------------------------------------------------------- 1 | isSeoUrlsEnabled()) { 29 | return parent::_getRoutePath($routeParams); 30 | } 31 | if (!$this->hasData('route_path')) { 32 | $routePath = $this->_getRequest()->getAlias(self::REWRITE_NAVIGATION_PATH_ALIAS); 33 | if (!empty($routeParams['_use_rewrite']) && $routePath !== null && isset($routeParams['_navigation_filters'])) { 34 | if ($routeParams['_navigation_filters']) { 35 | $suffix = $this->getUrlHydrator()->getSuffix(); 36 | $routePath = preg_replace('/' . preg_quote($suffix, '/') . '$/', '', $routePath) . '/' . $routeParams['_navigation_filters'] . $suffix; 37 | } 38 | $this->setData('route_path', $routePath); 39 | return $routePath; 40 | } 41 | } 42 | return parent::_getRoutePath($routeParams); 43 | } 44 | 45 | /** 46 | * Get filter item url 47 | * 48 | * @param string $code 49 | * @param string $value 50 | * @param array $query 51 | * @return string 52 | */ 53 | public function getFilterUrl($code, $value, $query = [], $singleValue = false) 54 | { 55 | $params = ['_current' => true, '_use_rewrite' => true, '_query' => $query]; 56 | $values = []; 57 | if (!$singleValue) { 58 | $values = $this->getValuesFromUrl($code); 59 | } 60 | $values[] = $value; 61 | 62 | if ($this->isSeoUrlsEnabled()) { 63 | $allFilters = $this->_getRequest()->getParam('navigation_filters', []); 64 | $allFilters[$code] = $values; 65 | $filterUrlPart = $this->getUrlHydrator()->hydrate($allFilters); 66 | $params['_navigation_filters'] = $filterUrlPart; 67 | return $this->getUrl('*/*/*', $params); 68 | } 69 | 70 | $values = implode('_', $values); 71 | $params['_query'][$code] = $values; 72 | return $this->getUrl('*/*/*', $params); 73 | } 74 | 75 | /** 76 | * Get remove filter item url 77 | * 78 | * @param string $code 79 | * @param string $value 80 | * @param array $query 81 | * @return string 82 | */ 83 | public function getRemoveFilterUrl($code, $value, $query = []) 84 | { 85 | $params = ['_current' => true, '_use_rewrite' => true, '_query' => $query, '_escape' => true]; 86 | $values = $this->getValuesFromUrl($code); 87 | $key = array_search($value, $values); 88 | unset($values[$key]); 89 | 90 | if ($this->isSeoUrlsEnabled()) { 91 | $allFilters = $this->_getRequest()->getParam('navigation_filters', []); 92 | if (!$values && isset($allFilters[$code])) { 93 | unset($allFilters[$code]); 94 | } else { 95 | $allFilters[$code] = $values; 96 | } 97 | 98 | $filterUrlPart = $this->getUrlHydrator()->hydrate($allFilters); 99 | $params['_navigation_filters'] = $filterUrlPart; 100 | return $this->getUrl('*/*/*', $params); 101 | } 102 | 103 | $params['_query'][$code] = $values ? implode('_', $values) : null; 104 | return $this->getUrl('*/*/*', $params); 105 | } 106 | 107 | /** 108 | * Get array of filter values 109 | * 110 | * @param string $code 111 | * @return array 112 | */ 113 | public function getValuesFromUrl($code) 114 | { 115 | $paramValue = []; 116 | if ($this->isSeoUrlsEnabled()) { 117 | $filters = $this->_getRequest()->getParam('navigation_filters'); 118 | if (is_array($filters) && isset($filters[$code])) { 119 | $paramValue = $filters[$code]; 120 | } 121 | } else { 122 | $paramValue = array_filter(explode('_', $this->_getRequest()->getParam($code))); 123 | } 124 | return $paramValue; 125 | } 126 | 127 | /** 128 | * Chek is seo URLs opyion enabled 129 | * 130 | * @return bool 131 | */ 132 | public function isSeoUrlsEnabled() 133 | { 134 | if ($this->_getRequest()->getModuleName() != 'catalog') { 135 | return false; 136 | } 137 | $storeManager = ObjectManager::getInstance() 138 | ->get(StoreManagerInterface::class); 139 | return $this->_scopeConfig->getValue( 140 | 'niks_layered_navigation/general/friendly_urls', 141 | \Magento\Store\Model\ScopeInterface::SCOPE_STORE, 142 | $storeManager->getStore()->getId() 143 | ); 144 | } 145 | 146 | /** 147 | * Remove ajax option and build url 148 | * 149 | * @param null $routePath 150 | * @param null $routeParams 151 | * @return string 152 | */ 153 | public function getUrl($routePath = null, $routeParams = null) 154 | { 155 | if (isset($routeParams['_query'])) { 156 | $routeParams['_query']['niksAjax'] = null; 157 | } 158 | return parent::getUrl($routePath, $routeParams); 159 | } 160 | 161 | protected function getUrlHydrator() 162 | { 163 | if (!$this->urlHydrator) { 164 | $this->urlHydrator = ObjectManager::getInstance() 165 | ->get(Hydrator::class); 166 | } 167 | return $this->urlHydrator; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /Model/Url/Hydrator.php: -------------------------------------------------------------------------------- 1 | _attrOptionCollectionFactory = $attrOptionCollectionFactory; 60 | $this->categoryCollectionFactory = $categoryCollectionFactory; 61 | $this->attributeList = $attributeList; 62 | $this->scopeConfig = $scopeConfig; 63 | $this->storeManager = $storeManager; 64 | $this->registry = $registry; 65 | $this->translitFilter = $translitFilter; 66 | } 67 | 68 | /** 69 | * Extract filter params from URL part 70 | * 71 | * @param string $url 72 | * @return array 73 | */ 74 | public function extract($url) 75 | { 76 | $byAttribute = explode(self::SEO_FILTERS_DELIMITER, $this->getFilterString($url)); 77 | $data = []; 78 | foreach ($byAttribute as $attributeString) { 79 | preg_match('/[^-]*/', $attributeString, $match); 80 | if (empty($match)) { 81 | continue; 82 | } 83 | $attributeCode = $match[0]; 84 | $attributeValues = explode( 85 | self::SEO_FILTER_VALUES_DELIMITER, 86 | preg_replace('/' . $attributeCode . self::SEO_FILTER_CODE_DELIMITER . '/', '', $attributeString, 1) 87 | ); 88 | $attribute = $this->getAttribute($attributeCode); 89 | if (!$attribute && $attributeCode != 'cat') { 90 | continue; 91 | } 92 | $options = $this->getOptions($attributeCode); 93 | $data[$attributeCode] = []; 94 | foreach ($attributeValues as $value) { 95 | if ($attribute && $attribute->getBackendType() == 'decimal') { 96 | $data[$attributeCode][] = str_replace('_', '-', $value); 97 | continue; 98 | } 99 | $id = array_search($value, $options); 100 | if ($id !== false) { 101 | $data[$attributeCode][] = $id; 102 | } 103 | } 104 | } 105 | return $data; 106 | } 107 | 108 | /** 109 | * Hydrate filter params to url string 110 | * 111 | * @param array $data 112 | * @return string 113 | */ 114 | public function hydrate(array $data) 115 | { 116 | $stringParts = []; 117 | foreach ($data as $attributeCode => $values) { 118 | $attributeParts = []; 119 | $attribute = $this->getAttribute($attributeCode); 120 | $options = $this->getOptions($attributeCode); 121 | 122 | foreach ($values as $value) { 123 | if ($attribute && $attribute->getBackendType() == 'decimal') { 124 | $attributeParts[] = str_replace('-', '_', $value); 125 | continue; 126 | } 127 | $attributeParts[] = $options[$value] ?? false; 128 | } 129 | $stringParts[] = $attributeCode . self::SEO_FILTER_CODE_DELIMITER . implode(self::SEO_FILTER_VALUES_DELIMITER, $attributeParts); 130 | } 131 | return implode(self::SEO_FILTERS_DELIMITER, $stringParts); 132 | } 133 | 134 | /** 135 | * Get filter url part 136 | * 137 | * @param string $url 138 | * @return string 139 | */ 140 | public function getFilterString($url) 141 | { 142 | $suffix = preg_quote($this->getSuffix(), '/'); 143 | preg_match('/[^\/]*' . $suffix . '$/', $url, $match); 144 | $string = ''; 145 | if (count($match)) { 146 | $string = $match[0]; 147 | } 148 | return preg_replace('/' . $suffix . '$/', '', $string); 149 | } 150 | 151 | /** 152 | * Get filterable attributes options 153 | * 154 | * @param null|string $attribute 155 | * @return array 156 | */ 157 | protected function getOptions($attribute = null) 158 | { 159 | if (!$this->_optionsByCode) { 160 | $attributeIds = $this->attributeList->getList()->getAllIds(); 161 | 162 | $optionsCollection = $this->_attrOptionCollectionFactory->create() 163 | ->addFieldToFilter('main_table.attribute_id', ['in' => $attributeIds]) 164 | ->setStoreFilter($this->storeManager->getStore()->getId()) 165 | ; 166 | $optionsCollection->getSelect()->joinLeft( 167 | ['attr_table' => $optionsCollection->getTable('eav_attribute')], 168 | 'attr_table.attribute_id = main_table.attribute_id', 169 | ['attribute_code'] 170 | ); 171 | 172 | $this->_optionsByCode = []; 173 | foreach ($optionsCollection as $option) { 174 | if (!isset($this->_optionsByCode[$option->getAttributeCode()])) { 175 | $this->_optionsByCode[$option->getAttributeCode()] = []; 176 | } 177 | $this->_optionsByCode[$option->getAttributeCode()][$option->getOptionId()] = $this->prepareOptionLabel($option->getValue()); 178 | } 179 | 180 | $currentCategoryId = $this->registry->registry('current_category_id'); 181 | if ($currentCategoryId) { 182 | $this->_optionsByCode['cat'] = $this->getCategoryOptions($currentCategoryId); 183 | } 184 | } 185 | 186 | if (!isset($this->_optionsByCode['cat'])) { 187 | $currentCategory = $this->registry->registry('current_category'); 188 | $this->_optionsByCode['cat'] = $this->getCategoryOptions($currentCategory->getId()); 189 | } 190 | return isset($this->_optionsByCode[$attribute]) ? $this->_optionsByCode[$attribute] : []; 191 | } 192 | 193 | /** 194 | * Get attribute model by code 195 | * 196 | * @param string $code 197 | * @return bool|\Magento\Eav\Model\Entity\Attribute 198 | */ 199 | protected function getAttribute($code) 200 | { 201 | if (!$this->_attributes) { 202 | $this->_attributes = []; 203 | foreach ($this->attributeList->getList() as $attribute) { 204 | $this->_attributes[$attribute->getAttributeCode()] = $attribute; 205 | } 206 | } 207 | return isset($this->_attributes[$code]) ? $this->_attributes[$code] : false; 208 | } 209 | 210 | /** 211 | * Get category options 212 | * 213 | * @param int $parentId 214 | * @return array 215 | */ 216 | protected function getCategoryOptions($parentId) 217 | { 218 | $options = []; 219 | $categories = $this->categoryCollectionFactory->create()->addAttributeToSelect( 220 | 'name' 221 | )->addAttributeToSelect( 222 | 'is_active' 223 | )->addAttributeToFilter('parent_id', $parentId) 224 | ->setStoreId( 225 | $this->storeManager->getStore()->getId() 226 | ); 227 | foreach ($categories as $category) { 228 | $options[$category->getId()] = $this->prepareOptionLabel($category->getName()); 229 | } 230 | return $options; 231 | } 232 | 233 | /** 234 | * Prepare option label for url 235 | * 236 | * @param string $label 237 | * @return string 238 | */ 239 | protected function prepareOptionLabel($label) 240 | { 241 | return $this->translitFilter->filter($label); 242 | } 243 | 244 | /** 245 | * Get category url suffix 246 | * 247 | * @return string 248 | */ 249 | public function getSuffix() 250 | { 251 | return $this->scopeConfig->getValue( 252 | \Magento\CatalogUrlRewrite\Model\CategoryUrlPathGenerator::XML_PATH_CATEGORY_URL_SUFFIX, 253 | \Magento\Store\Model\ScopeInterface::SCOPE_STORE, 254 | $this->storeManager->getStore()->getId() 255 | ); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /Model/Url/Translit.php: -------------------------------------------------------------------------------- 1 | categoryRepository = $categoryRepository; 33 | $this->storeManager = $storeManager; 34 | } 35 | 36 | public function aroundIsApplicable( 37 | \Magento\CatalogSearch\Model\Adapter\Aggregation\Checker\Query\CatalogView $subject, 38 | \Closure $proceed, 39 | RequestInterface $request 40 | ) 41 | { 42 | if ($request->getName() === 'catalog_view_container') { 43 | return $this->hasAnchorCategory($request); 44 | } 45 | 46 | return $proceed($request); 47 | } 48 | 49 | /** 50 | * Check whether category is anchor. 51 | * 52 | * Proceeds with request and check whether at least one of categories is anchor. 53 | * 54 | * @param RequestInterface $request 55 | * @return bool 56 | */ 57 | private function hasAnchorCategory(RequestInterface $request) 58 | { 59 | $queryType = $request->getQuery()->getType(); 60 | $result = false; 61 | 62 | if ($queryType === QueryInterface::TYPE_BOOL) { 63 | $categories = $this->getCategoriesFromQuery($request->getQuery()); 64 | 65 | /** @var \Magento\Catalog\Api\Data\CategoryInterface $category */ 66 | foreach ($categories as $category) { 67 | // It's no need to render LN filters for non anchor categories 68 | if ($category && $category->getIsAnchor()) { 69 | $result = true; 70 | break; 71 | } 72 | } 73 | } 74 | 75 | return $result; 76 | } 77 | 78 | /** 79 | * Get categories based on query filter data. 80 | * 81 | * Get categories from query will allow to check if category is anchor 82 | * And proceed with attribute aggregation if it's not 83 | * 84 | * @param QueryInterface $queryExpression 85 | * @return \Magento\Catalog\Api\Data\CategoryInterface[]|[] 86 | */ 87 | private function getCategoriesFromQuery(QueryInterface $queryExpression) 88 | { 89 | /** @var BoolExpression $queryExpression */ 90 | $categoryIds = $this->getCategoryIdsFromQuery($queryExpression); 91 | $categories = []; 92 | 93 | foreach ($categoryIds as $categoryId) { 94 | try { 95 | $categories[] = $this->categoryRepository 96 | ->get($categoryId, $this->storeManager->getStore()->getId()); 97 | } catch (NoSuchEntityException $e) { 98 | // do nothing if category is not found by id 99 | } 100 | } 101 | 102 | return $categories; 103 | } 104 | 105 | /** 106 | * Get Category Ids from search query. 107 | * 108 | * Get Category Ids from Must and Should search queries. 109 | * 110 | * @param QueryInterface $queryExpression 111 | * @return array 112 | */ 113 | private function getCategoryIdsFromQuery(QueryInterface $queryExpression) 114 | { 115 | $queryFilterArray = []; 116 | /** @var BoolExpression $queryExpression */ 117 | $queryFilterArray[] = $queryExpression->getMust(); 118 | $queryFilterArray[] = $queryExpression->getShould(); 119 | $categoryIds = []; 120 | 121 | foreach ($queryFilterArray as $item) { 122 | if (!empty($item) && isset($item['category'])) { 123 | $queryFilter = $item['category']; 124 | /** @var \Magento\Framework\Search\Request\Query\Filter $queryFilter */ 125 | $values = $queryFilter->getReference()->getValue(); 126 | if (is_array($values)) { 127 | $categoryIds = array_merge($categoryIds, $values['in']); 128 | } else { 129 | $categoryIds[] = $values; 130 | } 131 | } 132 | } 133 | return $categoryIds; 134 | } 135 | } -------------------------------------------------------------------------------- /Plugin/CategoryView.php: -------------------------------------------------------------------------------- 1 | getRequest()->isXmlHttpRequest()) { 10 | $subject->getResponse()->setHeader('Content-Type', 'application/json', true); 11 | $navigationBlock = $result->getLayout()->getBlock('catalog.leftnav'); 12 | $productsBlock = $result->getLayout()->getBlock('category.products'); 13 | if ($navigationBlock) { 14 | return $subject->getResponse()->setBody(json_encode(['products' => $productsBlock->toHtml(), 'leftnav' => $navigationBlock->toHtml()])); 15 | } 16 | } 17 | return $result; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Plugin/CategoryViewBlock.php: -------------------------------------------------------------------------------- 1 | getNameInLayout() == 'category.products.list' || $subject->getNameInLayout() == 'search_result_list') { 22 | $result = '
' . $result . '
'; 23 | } 24 | return $result; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Plugin/FilterRenderer.php: -------------------------------------------------------------------------------- 1 | hasAttributeModel()) { 35 | if ($this->swatchHelper->isSwatchAttribute($filter->getAttributeModel())) { 36 | return $this->layout 37 | ->createBlock($this->block) 38 | ->setSwatchFilter($filter) 39 | ->toHtml(); 40 | } 41 | } 42 | 43 | if ($this->isSliderEnabled() && $filter->hasAttributeModel() && $filter->getAttributeModel()->getBackendType() == 'decimal') { 44 | return $this->layout 45 | ->createBlock(\Niks\LayeredNavigation\Block\LayeredNavigation\SliderRenderer::class) 46 | ->setFilter($filter) 47 | ->toHtml(); 48 | } 49 | return $proceed($filter); 50 | } 51 | 52 | protected function isSliderEnabled() 53 | { 54 | $scope = ObjectManager::getInstance()->get(\Magento\Framework\App\Config\ScopeConfigInterface::class); 55 | $storeManager = ObjectManager::getInstance()->get(StoreManagerInterface::class); 56 | 57 | return $scope->getValue( 58 | 'niks_layered_navigation/general/slider', 59 | \Magento\Store\Model\ScopeInterface::SCOPE_STORE, 60 | $storeManager->getStore()->getId() 61 | ); 62 | } 63 | 64 | 65 | } 66 | -------------------------------------------------------------------------------- /Plugin/Preprocessor.php: -------------------------------------------------------------------------------- 1 | getField() === 'category_ids' && is_array($filter->getValue()) && isset($filter->getValue()['in'])) { 24 | return 'category_ids_index.category_id IN (' . implode(',', $filter->getValue()['in']) . ')'; 25 | } 26 | return $proceed($filter, $isNegation, $query); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Plugin/SearchView.php: -------------------------------------------------------------------------------- 1 | _view = $context->getView(); 15 | } 16 | 17 | public function afterExecute(\Magento\CatalogSearch\Controller\Result\Index $subject) 18 | { 19 | $layout = $this->_view->getLayout(); 20 | if ($subject->getRequest()->isXmlHttpRequest()) { 21 | $subject->getResponse()->setHeader('Content-Type', 'application/json', true); 22 | $navigationBlock = $layout->getBlock('catalogsearch.leftnav'); 23 | $productsBlock = $layout->getBlock('search_result_list'); 24 | if ($navigationBlock) { 25 | return $subject->getResponse()->setBody(json_encode(['products' => $productsBlock->toHtml(), 'leftnav' => $navigationBlock->toHtml()])); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Plugin/State.php: -------------------------------------------------------------------------------- 1 | getActiveFilters() as $item) { 18 | $filterState[$item->getFilter()->getRequestVar()] = $item->getFilter()->getCleanValue(); 19 | } 20 | $params['_navigation_filters'] = ''; 21 | $params['_current'] = true; 22 | $params['_use_rewrite'] = true; 23 | $params['_escape'] = true; 24 | $params['_query'] = $filterState; 25 | return $subject->getUrl('*/*/*', $params); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multiple Layered Navigation for Magento 2 2 | 3 | This extension gives you an ability to choose a few options of one filterable attribute 4 | 5 | New beta features in 0.1.0 release (disabled in admin panel by default): 6 | 7 | - Ajax page update 8 | - Seo friendly URLs 9 | - Price slider 10 | 11 | ## Installation: 12 | 13 | First add repository to composer configuration: 14 | ```bash 15 | composer config repositories.niks-multiple-layered-navigation vcs git@github.com:NikZh/magento2-multiple-layered-navigation.git 16 | ``` 17 | 18 | Require new package with composer: 19 | ```bash 20 | composer require niks/multiple-layered-navigation 21 | ``` 22 | 23 | Enable module 24 | ```bash 25 | php bin/magento module:enable Niks_LayeredNavigation 26 | ``` 27 | 28 | Upgrade setup: 29 | ```bash 30 | php bin/magento setup:upgrade 31 | ``` 32 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "niks/multiple-layered-navigation", 3 | "description": "N/A", 4 | "require": { 5 | "php": "~5.5.0|~5.6.0|7.0.2|7.0.4|~7.0.6|~7.1.0" 6 | }, 7 | "type": "magento2-module", 8 | "version": "0.1.3", 9 | "license": [ 10 | "OSL-3.0", 11 | "AFL-3.0" 12 | ], 13 | "authors": [ 14 | { 15 | "name": "NikZh", 16 | "homepage": "https://github.com/NikZh", 17 | "role": "Developer" 18 | } 19 | ], 20 | "autoload": { 21 | "files": [ 22 | "registration.php" 23 | ], 24 | "psr-4": { 25 | "Niks\\LayeredNavigation\\": "" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /etc/adminhtml/system.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | niks_extensions 16 | Niks_LayeredNavigation::layered_navigation 17 | 18 | 19 | 20 | 21 | Magento\Config\Model\Config\Source\Yesno 22 | 23 | 24 | 25 | Magento\Config\Model\Config\Source\Yesno 26 | 27 | 28 | 29 | Magento\Config\Model\Config\Source\Yesno 30 | 31 | 32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 0 13 | 0 14 | 0 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /etc/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Niks\LayeredNavigation\Model\Layer\Filter\Attribute 7 | Niks\LayeredNavigation\Model\Layer\Filter\Price 8 | Niks\LayeredNavigation\Model\Layer\Filter\Decimal 9 | Niks\LayeredNavigation\Model\Layer\Filter\Category 10 | 11 | 12 | 13 | 14 | 15 | 16 | Niks\LayeredNavigation\Model\Layer\Filter\Attribute 17 | Niks\LayeredNavigation\Model\Layer\Filter\Price 18 | Niks\LayeredNavigation\Model\Layer\Filter\Decimal 19 | Niks\LayeredNavigation\Model\Layer\Filter\Category 20 | 21 | 22 | 23 | 24 | 25 | Niks\LayeredNavigation\Model\Layer\ItemCollectionProvider 26 | 27 | 28 | 29 | 30 | Niks\LayeredNavigation\Model\Layer\ItemCollectionProvider 31 | 32 | 33 | 34 | 35 | Niks\LayeredNavigation\Model\Url\Builder 36 | 37 | 38 | 39 | 40 | categoryViewContext 41 | 42 | 43 | 44 | 45 | categoryViewContext 46 | 47 | 48 | 49 | 50 | categoryViewContext 51 | 52 | 53 | 54 | 55 | 56 | categoryViewContext 57 | 58 | 59 | 60 | 61 | categoryViewContext 62 | 63 | 64 | 65 | 66 | 67 | Niks\LayeredNavigation\Model\Url\Builder 68 | 69 | 70 | 71 | 72 | Niks\LayeredNavigation\Model\Layer\Filter\Item 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | Niks\LayeredNavigation\Model\Search\Dynamic\Algorithm 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /etc/frontend/di.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Niks\LayeredNavigation\Controller\Router 8 | false 9 | 30 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /etc/module.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /registration.php: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /view/frontend/layout/catalogsearch_result_index.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /view/frontend/requirejs-config.js: -------------------------------------------------------------------------------- 1 | var config = { 2 | map: { 3 | '*': { 4 | niksNavigation: 'Niks_LayeredNavigation/js/navigation', 5 | niksSlider: 'Niks_LayeredNavigation/js/slider' 6 | } 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /view/frontend/templates/ajax.phtml: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /view/frontend/templates/slider.phtml: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |
6 | - 7 | getCurrencySymbol() ?> 8 |
9 |
10 |
11 | 12 | 22 | -------------------------------------------------------------------------------- /view/frontend/web/js/navigation.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "jquery" 3 | ], function ($) { 4 | "use strict"; 5 | var module = { 6 | _create: function () { 7 | if (this.options.disabled) { 8 | return; 9 | } 10 | this._initState(); 11 | var self = this; 12 | $('#layered-filter-block, .pages-items').off('click', 'a').on('click', 'a', function (e) { 13 | e.preventDefault(); 14 | var url = $(this).attr('href'); 15 | if (url) { 16 | self.updateContent(url, true); 17 | } 18 | }); 19 | $(window).unbind('popstate').bind('popstate', this.updateContent.bind(this)); 20 | }, 21 | 22 | _initState: function () { 23 | var self = this; 24 | if (!window.history.state) { 25 | $(document).ready(function () { 26 | self._saveState(document.location.href); 27 | }); 28 | } 29 | 30 | }, 31 | 32 | _saveState: function (url) { 33 | window.history.pushState({url: url}, '', url); 34 | }, 35 | 36 | updateContent: function (url, updateState) { 37 | if (updateState) { 38 | this._saveState(url); 39 | } 40 | if (url instanceof Object) { 41 | url = url.originalEvent.state.url; 42 | } 43 | $('body').loader('show'); 44 | var self = this; 45 | $.ajax({ 46 | url: url, 47 | cache: true, 48 | type: 'GET', 49 | data: {niksAjax: true}, 50 | success: function (resp) { 51 | if (resp instanceof Object) { 52 | $(self.options.filtersContainer).replaceWith(resp.leftnav); 53 | $(self.options.productsContainer).replaceWith(resp.products); 54 | $.mage.init(); 55 | $('html, body').animate({ 56 | scrollTop: $('#maincontent').offset().top 57 | }, 400); 58 | self._create(); 59 | $('body').loader('hide'); 60 | } 61 | } 62 | }); 63 | }, 64 | 65 | init: function (options) { 66 | if (!module.options) { 67 | module.options = options; 68 | module._create(); 69 | } 70 | return { 71 | updateContent: module.updateContent.bind(module) 72 | }; 73 | } 74 | }; 75 | 76 | return module.init; 77 | }); 78 | -------------------------------------------------------------------------------- /view/frontend/web/js/slider.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "jquery", 3 | "niksNavigation", 4 | "jquery/ui" 5 | ], function ($, nav) { 6 | "use strict"; 7 | 8 | $.widget('niks.priceSlider', { 9 | _create: function () { 10 | var self = this, 11 | slider = $('#slider-' + this.options.code + '-range'), 12 | fromInput = $('#' + self.options.code + '-from'), 13 | toInput = $('#' + self.options.code + '-to'); 14 | this.options.urlTemplate = decodeURI(this.options.urlTemplate); 15 | slider.slider({ 16 | range: true, 17 | min: this.options.min, 18 | max: this.options.max, 19 | values: [this.options.from, this.options.to], 20 | slide: function (event, ui) { 21 | fromInput.val(ui.values[0]); 22 | toInput.val(ui.values[1]); 23 | }, 24 | stop: function () { 25 | self.processPrice(slider); 26 | } 27 | }); 28 | 29 | fromInput.val(slider.slider('values', 0)); 30 | toInput.val(slider.slider('values', 1)); 31 | 32 | fromInput.change(function () { 33 | slider.slider('values', 0, $(this).val()); 34 | self.processPrice(slider); 35 | }); 36 | 37 | toInput.change(function () { 38 | slider.slider('values', 1, $(this).val()); 39 | self.processPrice(slider); 40 | }); 41 | }, 42 | 43 | processPrice: function (slider) { 44 | var from = slider.slider('values', 0), 45 | to = slider.slider('values', 1), 46 | url = this.options.urlTemplate.replace('{{from}}', from).replace('{{to}}', to); 47 | nav().updateContent(url, true); 48 | } 49 | }); 50 | return $.niks.priceSlider; 51 | }); 52 | -------------------------------------------------------------------------------- /view/frontend/web/styles.css: -------------------------------------------------------------------------------- 1 | .range-attribute-filter .ui-slider-handle { 2 | height: 10px; 3 | width: 10px; 4 | -webkit-border-radius: 10px; 5 | border-radius: 10px; 6 | background: #003eff; 7 | display: block; 8 | padding: 0px; 9 | margin-left: 0px; 10 | } 11 | 12 | .range-attribute-filter .ui-slider-handle:nth-of-type(2) { 13 | margin-left: -10px; 14 | } 15 | 16 | .range-attribute-filter .ui-state-active, 17 | .range-attribute-filter .ui-widget-content .ui-state-active, 18 | .range-attribute-filter .ui-widget-header .ui-state-active, 19 | .range-attribute-filter a.ui-button:active, 20 | .range-attribute-filter .ui-button:active, 21 | .range-attribute-filter .ui-button.ui-state-active:hover { 22 | background: #114ec3; 23 | color: #114ec3; 24 | } 25 | 26 | .range-attribute-filter .ui-icon-background, 27 | .range-attribute-filter .ui-state-active .ui-icon-background { 28 | border: #114ec3; 29 | background-color: #114ec3; 30 | } 31 | 32 | .range-attribute-filter .ui-state-active a, 33 | .range-attribute-filter .ui-state-active a:link, 34 | .range-attribute-filter .ui-state-active a:visited { 35 | color: #114ec3; 36 | text-decoration: none; 37 | } 38 | 39 | .range-attribute-filter .ui-state-hover, 40 | .range-attribute-filter .ui-widget-content .ui-state-hover, 41 | .range-attribute-filter .ui-widget-header .ui-state-hover, 42 | .range-attribute-filter .ui-button:hover { 43 | 44 | background: #114ec3; 45 | font-weight: normal; 46 | color: #114ec3; 47 | } 48 | .range-attribute-filter .range { 49 | padding-top:10px; 50 | text-align: center; 51 | } 52 | .range-attribute-filter .range input { 53 | max-width: 35px; 54 | border-radius: 5px; 55 | } 56 | 57 | .range-attribute-filter .range .range-wrapper{ 58 | padding-left: 10px; 59 | float: left; 60 | max-width: 45%; 61 | } 62 | --------------------------------------------------------------------------------