本教程提供 PHP 与 JS 代码,让你在 Bricks 构建器中实现:根据某个分类法(taxonomy)的术语(term)来过滤地图上的标记。
目录
前置要求
- 本地图系列 PART 1、PART 2 和 PART 3 的全部代码已经跑通。
- 建一个自定义分类法(本示例用
store_categories),并给每篇自定义文章分配一个术语(本示例用mobile和internet)。
Bricks 结构
既然要按术语过滤标记,第一步是创建一组按钮,每个按钮对应一个术语名:
实现方式:在搜索输入框代码元素后面创建一个按钮容器,容器里放一个静态按钮——「ALL」按钮(用于清除过滤)——再加一个查询循环 div,遍历所有术语并为每个术语输出一个按钮。
按钮容器要加 filterbtn-wrapper 类:
「ALL」按钮上添加两个类:.filterbtn 和 .filterbtn--active:
.filterbtn 类被所有按钮共用(包括术语按钮)。.filterbtn--active 有两个作用:
- 让「ALL」按钮在页面加载时默认处于激活状态
- 让我们能在构建器里按自己的意愿样式化激活态
再把链接类型改为外部链接(External URL),值填 #:
并添加一个 data-filter 属性,值为 *:
接下来进入 Terms 查询循环,填写如下设置:
再处理最后一个新元素:查询循环里的按钮。按钮文本由 term_name 函数动态生成,同样加上 filterbtn 类,外部链接设为 #:
并添加一个 data-filter 属性,值用 term_name 函数:
过滤按钮全部就绪后,还要给每篇自定义文章加上过滤条件。回到 Stores 查询循环元素(isotope wrapper 下面那个):
给它也加一个 data-filter 属性,值用一个自定义函数(稍后在 functions.php 里创建),输出每篇自定义文章用到的所有术语:
一切就绪,开始写代码!
PHP
上一期教程的代码不需要改动,只需要加上我们刚才用到的自定义函数——把一篇自定义文章的所有术语输出为字符串列表。把下面代码复制到 functions.php:
function bl_get_terms_from_post() {
$term_obj_list = get_the_terms( $post->ID, 'store_categories' );
$terms_string = join( ', ', wp_list_pluck( $term_obj_list, 'name' ) );
return $terms_string;
}
注意 store_categories 是我们分类法的名称,记得换成你自己的分类法名称。
JavaScript
先贴出需要整体替换的完整代码,然后再讲重点改动。最终代码如下:
window.addEventListener('load', () => {
const canvas = document.querySelector('#map');
if (!document.body.classList.contains('bricks-is-frontend') || canvas === null) {
return;
}
var mbAttr = 'Map data © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Imagery © <a href="https://www.mapbox.com/">Mapbox</a>';
var mbUrl = 'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw';
var streets = L.tileLayer(mbUrl, {
id: 'mapbox/streets-v11',
tileSize: 512,
zoomOffset: -1,
attribution: mbAttr
});
var osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
});
var satellite = L.tileLayer(mbUrl, {
id: 'mapbox/satellite-v9',
tileSize: 512,
zoomOffset: -1,
attribution: mbAttr
});
var map = L.map('map', {
center: [canvas.dataset.leafletLat, canvas.dataset.leafletLong],
zoom: canvas.dataset.leafletZoom,
maxZoom: canvas.dataset.leafletMaxZoom,
layers: [osm, cities]
});
var baseLayers = {
'OpenStreetMap': osm,
'Streets': streets,
'Satellite': satellite
};
var layerControl = L.control.layers(baseLayers).addTo(map);
// Jump to map on click
const storeHeadersLink = document.querySelectorAll('.store-header-link');
if (storeHeadersLink.length > 0) {
storeHeadersLink.forEach((link) => {
link.addEventListener('click', (e) => {
e.preventDefault();
let lat = e.target.dataset.leafletLat;
let long = e.target.dataset.leafletLong;
map.flyTo([lat, long], 17);
});
});
};
// Filter search input with IsotopeJS
// quick search regex
var qsRegex;
var filterSelector = "*";
// init Isotope
var iso = new Isotope('.isotope-wrapper', {
itemSelector: '.store-wrapper',
layoutMode: 'fitRows',
filter: function(itemElem) {
var search = qsRegex ? itemElem.textContent.match(qsRegex) : true;
var filterRes = filterSelector != '*' ? itemElem.dataset.filter.includes(filterSelector) : true;
return search && filterRes;
},
});
var mapRendering = () => {
boundsList = [];
var filteredItems = iso.filteredItems;
if (filteredItems < 1) {
return;
}
filteredItems.forEach(elm => {
elmt = elm.element;
let header = elmt.querySelector('.store-header')
let storeId = header.dataset.store;
let lat = header.dataset.leafletLat;
let long = header.dataset.leafletLong;
stores[storeId].addTo(cities);
boundsList.push(new L.LatLng(lat, long));
});
map.fitBounds(boundsList, {
padding: [50, 50]
});
}
// use value of search field to filter
var quicksearch = document.querySelector('.quicksearch');
quicksearch.addEventListener('keyup', debounce(function() {
qsRegex = new RegExp(quicksearch.value, 'gi');
// delete the current markers
cities.clearLayers();
//filter the store list
iso.arrange();
//add the new filtered markers to the map
mapRendering();
}, 200));
// debounce so filtering doesn't happen every millisecond
function 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);
};
};
// Filter terms
// bind filter button click
var filtersElem = document.querySelectorAll(".filterbtn-wrapper .filterbtn");
if (filtersElem.length > 0) {
filtersElem.forEach(elem => elem.addEventListener("click", function(event) {
event.preventDefault();
var filterValue = event.target.getAttribute("data-filter");
filterSelector = filterValue;
// delete the current markers
cities.clearLayers();
//filter the store list
iso.arrange();
//add the new filtered markers to the map
mapRendering();
}));
};
// change filterbtn--active class on buttons
for (var i = 0, len = filtersElem.length; i < len; i++) {
var buttonGroup = filtersElem[i];
radioButtonGroup(buttonGroup);
}
function radioButtonGroup(buttonGroup) {
buttonGroup.addEventListener("click", function(event) {
filtersElem.forEach(btn => btn.classList.remove("filterbtn--active"));
event.target.classList.add("filterbtn--active");
});
}
});
第一个改动:给 Isotope 初始化增加第二种过滤方式:
// init Isotope
var iso = new Isotope('.isotope-wrapper', {
itemSelector: '.store-wrapper',
layoutMode: 'fitRows',
filter: function(itemElem) {
var search = qsRegex ? itemElem.textContent.match(qsRegex) : true;
var filterRes = filterSelector != '*' ? itemElem.dataset.filter.includes(filterSelector) : true;
return search && filterRes;
},
});
注意这里返回的是搜索过滤和按钮过滤的「与」结果(AND,不是 OR)。也就是说,两种过滤方式可以同时生效。
第二个重要改动:把标记过滤相关的代码抽成一个独立函数 mapRendering(),这样无论是搜索框触发还是按钮触发的过滤,都调用它:
// Populate the filtered markers
var mapRendering = () => {
boundsList = [];
var filteredItems = iso.filteredItems;
if (filteredItems < 1){
return;
}
filteredItems.forEach(elm => {
elmt = elm.element;
let header = elmt.querySelector('.store-header')
let storeId = header.dataset.store;
let lat = header.dataset.leafletLat;
let long = header.dataset.leafletLong;
stores[storeId].addTo(cities);
boundsList.push(new L.LatLng(lat, long) );
} );
map.fitBounds(boundsList, {padding: [50, 50]});
}
我们给过滤按钮加了 eventListener:每次点击按钮,脚本取出按钮对应的术语,对列表和地图同时应用过滤:
// bind filter button click
var filtersElem = document.querySelectorAll(".filterbtn-wrapper .filterbtn");
if (filtersElem.length > 0) {
filtersElem.forEach ( elem => elem.addEventListener("click", function(event) {
event.preventDefault();
var filterValue = event.target.getAttribute("data-filter");
filterSelector = filterValue;
// delete the current markers
cities.clearLayers();
//filter the store list
iso.arrange();
//add the new filtered markers to the map
mapRendering();
}));
};
代码最后一部分负责激活类 .filterbtn--active 的切换——点击某个过滤按钮时,把它加到该按钮上(仅用于样式):
// change filterbtn--active class on buttons
for (var i = 0, len = filtersElem.length; i < len; i++) {
var buttonGroup = filtersElem[i];
radioButtonGroup(buttonGroup);
}
function radioButtonGroup(buttonGroup) {
buttonGroup.addEventListener("click", function(event) {
filtersElem.forEach(btn => btn.classList.remove("filterbtn--active"));
event.target.classList.add("filterbtn--active");
});
}
就这样!
结论
希望一切就位,你现在可以在前端测试新的术语过滤了:
本期到此为止!希望你喜欢这个教程,下期见!