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