— 本教程讲解在 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。把这段代码放进一个代码元素:
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/)
延伸阅读
Bricks 查询循环输出 ACF 图片字段的 Alt、Caption、标题数据
教程:Bricks 查询循环输出 ACF 图片字段的 Alt、Caption、标题数据——acf 附完整代码可直接复用,适合外贸独立站与 WordPress 开发者。
tutorialBricks 中输出 ACF Relationship 关联文章的数量
教程:Bricks 中输出 ACF Relationship 关联文章的数量——acf 附完整代码可直接复用,适合外贸独立站与 WordPress 开发者。
tutorialACF Relationship 字段 + Bricks 查询循环:显示关联文章
教程:ACF Relationship 字段 + Bricks 查询循环:显示关联文章——acf 附完整代码可直接复用,适合外贸独立站与 WordPress 开发者。
tutorial把任意自定义 WP_Query 循环接入 Bricks 查询循环
教程:把任意自定义 WP_Query 循环接入 Bricks 查询循环——Bricks 附完整代码可直接复用,适合外贸独立站与 WordPress 开发者。
