首页 / 中文教程 / 教程

在 Bricks 中使用 IsotopeJS 创建筛选器(第四部分):AJAX 筛选与无限循环、排序、列表视图与禁用动画

本教程讲解在 Brickshttps://brickslabs.com/go/bricks 构建器里用 Isotope 实现:给无限滚动查询循环容器启用 AJAX 筛选、添加排序功能、添加列表视图切换器,以及禁用筛选时的…

Ray Chan·2026-08-12·约 9 分钟

本教程讲解在 Bricks 构建器里用 Isotope 实现:给无限滚动查询循环容器启用 AJAX 筛选、添加排序功能、添加列表视图切换器,以及禁用筛选时的 CSS 动画。

目录

环境要求

无限滚动查询循环容器的 AJAX 筛选

听起来很难实现,其实比看起来简单得多。功劳属于 Jenn Lee——她解释了怎么使用 Bricks 创建的 AJAX 端点,并找到了正确的 AJAX 函数,让 isotope 在新内容加载时被触发。

第一步:在查询循环容器里启用无限滚动设置,并给 posts_per_page 设一个上限:

作者在服务器上开启这个设置时遇到过几个报错——循环因为某些未知原因没有查询到正确的 post_id,无限循环返回了错误。所以务必先确认不开 isotope 时无限循环能正常工作,再进行下一步。

把下面的代码粘贴到 init.js 里以启用 AJAX 筛选(具体粘贴位置见文末的最终代码):

const open = window.XMLHttpRequest.prototype.open;

function XMLOpenReplacement() {
   this.addEventListener("load", function () {
      let current_url = new URL(this.responseURL);
      if (current_url.pathname.includes("/load_query_page")) {

         //reload the items
         iso.reloadItems();

         //set the padding
         if (wrapper.dataset.gutter) {
            isotopeSelector = wrapper.querySelectorAll('.isotope-selector');
            isotopeSelector.forEach(elm => elm.style.paddingBottom = isotopeGutter + 'px');
         }

         //rearrange the container
         iso.arrange();
      }
   });
   return open.apply(this, arguments);
}
window.XMLHttpRequest.prototype.open = XMLOpenReplacement;

简单说,这个函数利用了 Bricks 创建的名为 load_query_page 的端点。作者创建了一个 EventListener 函数,每次这个 AJAX 端点被加载时就触发 isotope 相关函数。

isotope 相关函数很基础:首先重载所有项目(reloadItems),让 isotope 容器包含所有选择器——包括新加载的;然后如果设置了 gutter,就给所有项目补上 gutter 内边距;最后重新排列(arrange)容器、修正布局。就这些。

禁用 CSS 动画

有一件事困扰着作者:每次通过 AJAX 加载新项目时,新项目会带一个 transform 动画,像是从顶部飞到底部的新位置。

如果这也让你不爽,可以在 isotopeOptions 变量里加一行,直接停掉所有选择器的动画:

var isotopeOptions = {
   transitionDuration: 0,
};

排序功能

不少人要求加排序功能,现在就做。

这次添加一个下拉选择框(select),选项是要应用的排序方式:按最新、最旧、A 到 Z、Z 到 A。把这段代码放进一个代码元素:

<select name="sorting" id="sorting">
  <option value="newest">Newest</option>
  <option value="oldest">Oldest</option>
  <option value="a_to_z">A to Z</option>
  <option value="z_to_a">Z to A</option>
</select>

注意作者稍微调整了 DOM 结构,把下拉框(以及网格/列表视图按钮)放到了 isotope 容器上方:

现在进入 init.js,把新输入存进变量:

var sorting = wrapper.querySelector('#sorting');
if (!sorting) {
   console.log('No Sorting input found. Make sure your sorting dropdown input has the ID "#sorting"');
}

需要在 isotopeOptions 变量里声明排序标准。本例添加 name(对应“A to Z”和“Z to A”)和 date(对应“Newest”和“Oldest”):

var isotopeOptions = {
   getSortData: {
      name: (el) => {
         return el.querySelector('.post-title').textContent;
      },
      date: (el) => {
         return el.querySelector('.post-date').textContent;
      }
   },
};

可以看到两个函数都查询了元素内部的子元素并抓取其文本内容:name 取文章标题(Post Title),date 取文章日期(Post Date)。记得给 Bricks 元素加上 .post-title.post-date 类。

最后一步是创建 EventListener 函数,每次下拉框的值改变时就触发排序:

// Event Listener for sorting 
if (sorting) {
   sorting.addEventListener('change', (e) => {

      switch (e.target.value) {
         case "newest":
            iso.arrange({
               sortBy: 'date',
               sortAscending: false
            });
            break;
         case "oldest":
            iso.arrange({
               sortBy: 'date',
               sortAscending: true
            });
            break;
         case "a_to_z":
            iso.arrange({
               sortBy: 'name',
               sortAscending: true
            });
            break;
         case "z_to_a":
            iso.arrange({
               sortBy: 'name',
               sortAscending: false
            });
            break;
         default:
            return;
      }
   })
}

每次下拉框值改变时,switch 函数会检查对应的情况。如果值匹配某个 case,就会用 sortBy 条件和通过 sortAscending 指定的升序/降序重新排列 isotope 容器。

列表视图

这个很直接。在新的顶栏容器里加两个按钮,分别设置 ID 为 grid-viewlist-view

init.js 里查询这两个按钮:

var gridView = wrapper.querySelector('#grid-view');
// Show a message in console if no grid-view buttons have been found
if (!gridView) {
   console.log('No grid-view button found. Make sure that your grid-view button has the ID "#grid-view"')
}

var listView = wrapper.querySelector('#list-view');
// Show a message in console if no list-view buttons have been found
if (!listView) {
   console.log('No list-view button found. Make sure that your list-view button has the ID "#grid-view"')
}

然后给它们各加一个 EventListener 函数:

//Event Listerner for grid-view
if (gridView) {
   gridView.addEventListener('click', (e) => {
      e.preventDefault();
      gridView.classList.add('filterbtn--active');
      listView.classList.remove('filterbtn--active');
      isotopeContainer.classList.remove('list');
      iso.arrange();
   })
}

//Event Listerner for list-view
if (listView) {
   listView.addEventListener('click', (e) => {
      e.preventDefault();
      listView.classList.add('filterbtn--active');
      gridView.classList.remove('filterbtn--active');
      isotopeContainer.classList.add('list');
      iso.arrange();
   })
}

脚本会在按钮被点击时切换 filterbtn--active 类。注意这里复用了第二部分创建的激活类,你也可以创建自己的专属类并在 Bricks 构建器里设置样式。

但最重要的是:在 isotope 容器上切换一个 list 类。现在只需在页面设置里插入下面的 CSS:

.isotope-container.list {
    --col: 1;
}

只改 CSS 变量 --col,就重新计算了 isotope 容器的全部尺寸。

可选:可以通过 .isotope-container.list 类随心所欲地设置列表视图样式。不过 Bricks 目前还不支持这个功能,CSS 得手动写。

作者示例里加的 CSS 如下:

@media screen and (min-width: 1200px) {

	/* ARTICLE */
	.isotope-container.list article {
		flex-direction: row !important;
		align-items: stretch;
		gap: 20px;
	}

	/* LEFT COL */
	.isotope-container.list article>a {
		max-width: 45%;
		align-self: stretch !important;
		flex-basis: 100%;
	}

	/*RIGHT COL */
	.isotope-container.list article>div {
		flex-basis: 100%;
	}

	/* FEATURE IMAGE */
	.isotope-container.list article>a>img {
		aspect-ratio: unset;
		position: absolute;
		top: 50%;
		left: 50%;
		bottom: 0;
		right: 0;
		width: 100%;
		height: 100%;
		transform: translate(-50%, -50%);
	}

	/* READ MORE BUTTONS */
	.isotope-container.list article>div>a {
		margin: 0;
		align-self: end;
	}

	.isotope-container.list article>div>a>div {
		border-radius: 10px 0 10px 0 !important;
	}
}

搞定!

最终代码

这是 init.js 文件的最终代码:

window.addEventListener('DOMContentLoaded', () => {

   const isotopeWrappers = document.querySelectorAll('.isotope-wrapper');

   // Stop the function if there is no isotope wrapper detected
   if (isotopeWrappers.length < 1) {
      return console.log('No isotope wrapper found. Make sure to add the ".isotope-wrapper" class to the main isotope wrapper');
   };

   // Default variable

   // buttons
   var filterRes = true;
   var filterSelector = "*";
   // search
   var filterSearch = true;
   var qsRegex
   // range
   var filterRange = true;
   var filterRangeValue = '*';
   // checkbox
   var filterCheckbox = true;
   var filterCheckboxValue = '*';
   // radio
   var filterRadio = true;
   var filterRadioValue = '*';

   // Loop inside each isotope wrapper
   isotopeWrappers.forEach(wrapper => {

      // Set variable and Error Handling

      var isotopeContainer = wrapper.querySelector('.isotope-container');
      if (!isotopeContainer) {
         return console.log('No isotope container found. Make sure to add the ".isotope-container" class to the container of your selectors');
      }

      var isotopeSelector = isotopeContainer.querySelectorAll('.isotope-container .isotope-selector');
      if (isotopeSelector.length < 1) {
         return console.log('No isotope selector found. Make sure to add the ".isotope-selector" class to all your selector');
      }

      var buttons = wrapper.querySelectorAll(".filterbtn-container .filterbtn");
      // Show a message in console if no buttons have been found
      if (buttons.length < 1) {
         console.log('No filter wrapper or filter buttons found. Make sure your filter wrapper has the class ".filterbtn-wrapper" and all your filter buttons have the class ".filterbtn"');
      }

      var quickSearch = wrapper.querySelector('#quicksearch');
      // Show a message in console if no search input have been found
      if (!quickSearch) {
         console.log('No QuickSearch found. Make sure your search input has the ID "#quicksearch"');
      }

      var range = wrapper.querySelector('#range');
      // Show a message in console if no range slider have been found
      if (!range) {
         console.log('No Range found. Make sure your range input has the ID "#range"');
      }

      var checkboxes = wrapper.querySelectorAll('.checkbox');
      // Show a message in console if no checkboxes have been found
      if (checkboxes.length < 1) {
         console.log('No checkbox found. Make sure your checkbox input has the class ".checkbox"');
      }

      var radios = wrapper.querySelectorAll('.radio');
      // Show a message in console if no radio buttons have been found
      if (checkboxes.length < 1) {
         console.log('No radio found. Make sure your radio input has the class ".radio"');
      }

      var sorting = wrapper.querySelector('#sorting');
      if (!sorting) {
         console.log('No Sorting found. Make sure your sorting dropdown input has the ID "#sorting"');
      }

      var gridView = wrapper.querySelector('#grid-view');
      // Show a message in console if no grid-view buttons have been found
      if (!gridView) {
         console.log('No grid-view button found. Make sure that your grid-view button has the ID "#grid-view"')
      }

      var listView = wrapper.querySelector('#list-view');
      // Show a message in console if no list-view buttons have been found
      if (!listView) {
         console.log('No list-view button found. Make sure that your list-view button has the ID "#grid-view"')
      }

      // Gutter Settings through data-gutter
      if (wrapper.dataset.gutter) {
         var isotopeGutter = parseInt(wrapper.dataset.gutter);
         wrapper.style.setProperty('--gutter', isotopeGutter + 'px');
         isotopeSelector.forEach(elm => elm.style.paddingBottom = isotopeGutter + 'px');
      } else {
         // Default option
         var isotopeGutter = 0;
         console.log('No data-gutter attribute has been found on your isotope container. Default set to 0.');
      };

      // Layout Settings through data-filter-layout
      if (wrapper.dataset.filterLayout) {
         var isotopeLayoutHelper = wrapper.dataset.filterLayout;
      } else {
         // Default option
         var isotopeLayoutHelper = 'fitRows';
         console.log('No data-filter-layout attribute has been found on your isotope container. Default set to "fitRows".');
      };

      // init Isotope

      var isotopeOptions = {
         itemSelector: '.isotope-selector',
         layoutMode: isotopeLayoutHelper,
         //transitionDuration: 0, /Uncomment to disable animations
         getSortData: {
            name: (el) => {
               return el.querySelector('.post-title').textContent;
            },
            date: (el) => {
               return el.querySelector('.post-date').textContent;
            }
         },
         filter: (itemElem1, itemElem2) => {
            const itemElem = itemElem1 || itemElem2;

            // tags/buttons
            if (buttons.length > 0) {
               filterRes = filterSelector != '*' ? itemElem.dataset.filter.includes(filterSelector) : true;
            }

            // quicksearch
            if (quickSearch) {
               filterSearch = qsRegex ? itemElem.textContent.match(qsRegex) : true;
            }

            // range
            if (range) {
               filterRange = filterRangeValue != '*' ? parseInt(itemElem.dataset.range) <= filterRangeValue : true;
            }

            // checkboxes
            if (checkboxes.length > 0) {
               filterCheckbox = filterCheckboxValue != '*' ? filterCheckboxValue.includes(itemElem.dataset.checkbox) : true;
            }

            // radio
            if (radios.length > 0) {
               filterRadio = filterRadioValue != '*' ? filterRadioValue.includes(itemElem.dataset.radio) : true;
            }

            return filterRes && filterSearch && filterRange && filterCheckbox && filterRadio;
         }
      };

      // Set the correct layout
      switch (isotopeLayoutHelper) {
         case 'fitRows':
            isotopeOptions.fitRows = {
               gutter: isotopeGutter
            };
            break;
         case 'masonry':
            isotopeOptions.masonry = {
               gutter: isotopeGutter
            };
            break;
      }

      var iso = new Isotope(isotopeContainer, isotopeOptions);

      // debounce so filtering doesn't happen every millisecond
      const debounce = (fn, threshold) => {
         var timeout;
         threshold = threshold || 200;
         return function debounced() {
            clearTimeout(timeout);
            var args = arguments;
            var _this = this;

            function delayed() {
               fn.apply(_this, args);
            }
            timeout = setTimeout(delayed, threshold);
         };
      };

      // Event Listener for buttons
      if (buttons.length > 0) {

         buttons.forEach(elem => elem.addEventListener("click", (event) => {

            event.preventDefault();

            // get the data-filter attribute from the filter button
            var filterValue = event.target.getAttribute("data-filter");
            filterSelector = filterValue;

            // filter results
            iso.arrange();
         }));
      };

      // Event Listener for search input
      if (quickSearch) {
         quickSearch.addEventListener('keyup', debounce((event) => {

            qsRegex = new RegExp(quickSearch.value, 'gi');

            //filter the store list
            iso.arrange();

         }, 200));
      }

      // Event Listener for range slider
      if (range) {
         const rangeFilterFN = (e) => {
            filterRangeValue = parseInt(e.target.value);

            //filter the store list
            iso.arrange();
         }

         range.addEventListener('input', debounce((e) => {

            rangeFilterFN(e);

         }, 200));

         range.addEventListener('keyup', debounce((e) => {

            rangeFilterFN(e);

         }, 200));
      }

      // Event Listener for checkboxes
      if (checkboxes) {
         checkboxes.forEach(checkbox => {
            checkbox.addEventListener('click', debounce(() => {
               arrCheckbox = [];

               checkboxes.forEach(cb => {
                  if (!cb.checked) {
                     return;
                  }
                  arrCheckbox.push(cb.value);

               })

               filterCheckboxValue = arrCheckbox;

               //filter the store list
               iso.arrange();

            }, 200));
         })
      }

      // Event Listener for radio buttons
      if (radios) {
         radios.forEach(radio => {
            radio.addEventListener('change', debounce(() => {
               arrRadio = [];

               radios.forEach(radio => {
                  if (!radio.checked) {
                     return;
                  }
                  arrRadio.push(radio.value);

               })

               filterRadioValue = arrRadio;

               //filter the store list
               iso.arrange();

            }, 200));
         })
      }

      // Event Listener for sorting 
      if (sorting) {
         sorting.addEventListener('change', (e) => {

            switch (e.target.value) {
               case "newest":
                  iso.arrange({
                     sortBy: 'date',
                     sortAscending: false
                  });
                  break;
               case "oldest":
                  iso.arrange({
                     sortBy: 'date',
                     sortAscending: true
                  });
                  break;
               case "a_to_z":
                  iso.arrange({
                     sortBy: 'name',
                     sortAscending: true
                  });
                  break;
               case "z_to_a":
                  iso.arrange({
                     sortBy: 'name',
                     sortAscending: false
                  });
                  break;
               default:
                  return;
            }
         })
      }

      //Event Listener for grid-view
      if (gridView) {
         gridView.addEventListener('click', (e) => {
            e.preventDefault();
            gridView.classList.add('filterbtn--active');
            listView.classList.remove('filterbtn--active');
            isotopeContainer.classList.remove('list');
            iso.arrange();
         })
      }

      // Event Listener for list-view
      if (listView) {
         listView.addEventListener('click', (e) => {
            e.preventDefault();
            listView.classList.add('filterbtn--active');
            gridView.classList.remove('filterbtn--active');
            isotopeContainer.classList.add('list');
            iso.arrange();
         })
      }

      // Event Listener for filter buttons
      const radioButtonGroup = (buttonGroup) => {

         buttonGroup.addEventListener("click", (event) => {
            buttons.forEach(btn => btn.classList.remove("filterbtn--active"));
            event.target.classList.add("filterbtn--active");
         });
      };

      // change is-checked class on buttons
      for (var i = 0, len = buttons.length; i < len; i++) {

         var buttonGroup = buttons[i];
         radioButtonGroup(buttonGroup);
      };
      setTimeout(() => {
         iso.arrange()
      }, 300);

      // AJAX FILTER
      const open = window.XMLHttpRequest.prototype.open;

      function XMLOpenReplacement() {
         this.addEventListener("load", function () {
            let current_url = new URL(this.responseURL);
            if (current_url.pathname.includes("/load_query_page")) {

               //reload the items
               iso.reloadItems();

               //set the padding
               if (wrapper.dataset.gutter) {
                  isotopeSelector = wrapper.querySelectorAll('.isotope-selector');
                  isotopeSelector.forEach(elm => elm.style.paddingBottom = isotopeGutter + 'px');
               }

               //rearrange the container
               iso.arrange();
            }
         });
         return open.apply(this, arguments);
      }
      window.XMLHttpRequest.prototype.open = XMLOpenReplacement;
   });

});

结论

除非社区再提出什么特殊需求,这篇教程就为 Isotope 系列画上句号了——教你如何在不引入任何第三方插件的情况下,把智能筛选整合进下一个 Bricks 项目。希望你喜欢!

需要帮忙?

用 Bricks 建站?我接客户项目。

从快速营销站到完整的 Bricks 建站,再到从 Elementor 迁移——我都做过。本站每篇教程都来自真实项目经验。告诉我你的需求,一个工作日内回复。

  • Bricks 建站与改版
  • Elementor / Divi → Bricks 迁移
  • Bricks → Astro / headless 性能升级
  • 速度优化,PageSpeed 95+ 目标