🆕 Bricks 2.4 已发布:查询循环性能提升 40%,迁移教程同步更新 →

首页 / 中文教程 / 教程

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

实战技巧:在 Bricks 中使用 IsotopeJS 创建筛选器(第四部分):AJAX 筛选与无限循环、排序、列表视图与禁用动画——animation 实操步骤+踩坑提醒,适合外贸独立站与 WordPress 开发者。

Ray ChanRay Chan·2026-08-18·约 10 分钟
目录
  1. 1.目录
  2. 2.环境要求
  3. 3.无限滚动查询循环容器的 AJAX 筛选
  4. 4.禁用 CSS 动画
  5. 5.排序功能
  6. 6.列表视图
  7. 7.最终代码
  8. 8.结论

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

  • 无限滚动查询循环容器的 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。把这段代码放进一个代码元素: ``` 注意作者稍微调整了 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-view` 和 `list-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;

});

});




>



- [Bricks 中使用 Core Framework](/blog/core-framework-in-bricks/)



- [Bricks 内容切换器:纯 CSS 方案(radio + :has 实现标签切换)](/blog/a-css-approach-to-content-switcher-in-bricks/)



- [Bricks 用 ACF Checkbox 字段渲染自定义 SVG 图标列表](/blog/acf-checkbox-svg-bullets/)



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

## 常见误区与性能提示

几个坑:一是 AJAX 筛选每次都重查全量数据,量大时卡,配合缓存或只在前端过滤已取到的数据;二是无限循环(infinite scroll)和分页混用,爬虫只抓第一屏、内页不收录,要收录就保留分页或做结构化加载;三是禁用动画没关干净,切换时仍闪。Isotope 适合「纯前端、不引第三方插件」的智能筛选,但数据量大、要 SEO 收录时,得权衡是否上服务端筛选。

## 延伸阅读

- 想要现成外贸独立站,从零搭出能收询盘的站?看 [铲子铺 chanzipu 的外贸建站教程](https://chanzipu.com/)(同用 Bricks)

## 常见问题(FAQ)

**AJAX 筛选卡顿?** 量大时缓存查询结果,或只在已取到的数据里前端过滤,别每次打全量请求。

**无限滚动影响收录吗?** 会,爬虫只抓第一屏,要收录就保留分页或做可被爬取的结构化加载。

**动画关不掉?** 检查禁用动画选项是否真正生效,切换仍闪就重新初始化 Isotope。

**Isotope 和服务器端筛选怎么选?** 数据小、纯前端交互用 Isotope;要 SEO 收录、数据大就上服务端筛选更稳。
- 相关阅读:[Bricks Setup Guide](/blog/bricks-setup-guide/)

## 相关阅读

- [Bricks Query Loop 接入外部 API 数据](/blog/bricks-query-loop-api-data/)
- [Bricks 查询循环的分面筛选(Faceted Search)](/blog/bricks-query-filters-faceted-search/)
- [Bricks Form Builder 完整指南](/blog/bricks-form-builder-complete-guide/)
Ray Chan

站长

Ray Chan

WordPress Developer & Bricks Specialist

WordPress developer with 10+ years of client builds. Switched to Bricks in 2023 — now builds fast WordPress sites and migrates legacy Elementor/Divi projects.

延伸阅读