Port zibll hierarchical cat tabs, multi-metric sort with price toggle, skeleton/empty states, and REST archive payload (header/html/paginate) while reusing product-card template for AJAX HTML.
296 lines
11 KiB
JavaScript
296 lines
11 KiB
JavaScript
/**
|
|
* Product archive — cat filter + sort + AJAX load more (zibll ajax-next UX).
|
|
*
|
|
* Uses REST /shop/frontend/products/archive and optional AksrtAjaxList events.
|
|
*
|
|
* @package AKSRT
|
|
*/
|
|
(function () {
|
|
"use strict";
|
|
|
|
var root = document.querySelector("[data-aksrt-product-archive]");
|
|
if (!root) return;
|
|
|
|
function restBase() {
|
|
var site = window.aksrtData && aksrtData.siteUrl ? String(aksrtData.siteUrl).replace(/\/$/, "") : "";
|
|
return site + "/wp-json/aksrt/v1";
|
|
}
|
|
|
|
function toast(msg) {
|
|
if (typeof window.aksrtToast === "function") {
|
|
window.aksrtToast(msg);
|
|
return;
|
|
}
|
|
if (window.console) console.log(msg);
|
|
}
|
|
|
|
var grid = root.querySelector("[data-aksrt-product-grid], #aksrt-product-grid");
|
|
var pager = root.querySelector("[data-aksrt-ajaxpager], .ajaxpager");
|
|
var countEl = root.querySelector("[data-aksrt-archive-count]");
|
|
var headerHost = root.querySelector("[data-aksrt-ajax-header]");
|
|
var loadMoreBtn = root.querySelector("[data-aksrt-shop-load-more]");
|
|
var loader = root.querySelector("[data-aksrt-ajax-loader], .post_ajax_loader");
|
|
|
|
var state = {
|
|
taxonomy: root.getAttribute("data-taxonomy") || "",
|
|
termId: Number(root.getAttribute("data-term-id") || 0),
|
|
orderby: root.getAttribute("data-orderby") || "date",
|
|
order: (root.getAttribute("data-order") || "DESC").toUpperCase(),
|
|
page: Number(root.getAttribute("data-page") || 1),
|
|
maxPages: Number(root.getAttribute("data-max-pages") || 1),
|
|
paging: root.getAttribute("data-paging") || "number",
|
|
ias: root.getAttribute("data-ias") === "1",
|
|
iasMax: Number(root.getAttribute("data-ias-max") || 5),
|
|
loading: false,
|
|
autoLoads: 0,
|
|
};
|
|
|
|
function showLoader(on) {
|
|
if (loader) {
|
|
loader.hidden = !on;
|
|
loader.style.display = on ? "" : "none";
|
|
loader.classList.toggle("is-loading", !!on);
|
|
}
|
|
if (pager) pager.classList.toggle("is-ajax-loading", !!on);
|
|
}
|
|
|
|
function updateLoadMore(hasMore) {
|
|
var pag = root.querySelector(".ajax-pag");
|
|
if (loadMoreBtn) {
|
|
loadMoreBtn.style.display = hasMore ? "" : "none";
|
|
loadMoreBtn.disabled = false;
|
|
loadMoreBtn.textContent = hasMore ? "加载更多" : "没有更多了";
|
|
if (!hasMore) loadMoreBtn.classList.add("ajax-nomore");
|
|
else loadMoreBtn.classList.remove("ajax-nomore");
|
|
}
|
|
if (pag) pag.style.display = hasMore ? "" : "none";
|
|
}
|
|
|
|
function syncOrderbyActive() {
|
|
root.querySelectorAll("[data-aksrt-shop-orderby]").forEach(function (a) {
|
|
var key = a.getAttribute("data-aksrt-shop-orderby") || "";
|
|
var active = key === state.orderby;
|
|
a.classList.toggle("is-active", active);
|
|
a.classList.toggle("focus-color", active && key === "price");
|
|
});
|
|
}
|
|
|
|
function fetchArchive(page, orderby, order, replace, termId) {
|
|
if (state.loading) return Promise.resolve();
|
|
state.loading = true;
|
|
showLoader(true);
|
|
if (loadMoreBtn) {
|
|
loadMoreBtn.disabled = true;
|
|
loadMoreBtn.textContent = "加载中…";
|
|
}
|
|
|
|
var qs = new URLSearchParams();
|
|
qs.set("taxonomy", state.taxonomy);
|
|
qs.set("term_id", String(typeof termId === "number" ? termId : state.termId));
|
|
qs.set("page", String(page));
|
|
qs.set("orderby", orderby || state.orderby);
|
|
if ((orderby || state.orderby) === "price") {
|
|
qs.set("order", order || state.order || "ASC");
|
|
}
|
|
|
|
var container = pager || grid || root;
|
|
try {
|
|
container.dispatchEvent(
|
|
new CustomEvent("post_ajax.before", {
|
|
bubbles: true,
|
|
detail: { page: page, orderby: orderby || state.orderby, replace: !!replace },
|
|
})
|
|
);
|
|
} catch (e0) {}
|
|
|
|
return fetch(restBase() + "/shop/frontend/products/archive?" + qs.toString(), {
|
|
method: "GET",
|
|
credentials: "same-origin",
|
|
headers: {
|
|
Accept: "application/json",
|
|
"X-WP-Nonce": (window.aksrtData && aksrtData.restNonce) || "",
|
|
"X-AKSRT-Ajax": "list",
|
|
},
|
|
})
|
|
.then(function (r) {
|
|
return r.json();
|
|
})
|
|
.then(function (result) {
|
|
state.loading = false;
|
|
showLoader(false);
|
|
if (!result || !result.success || !result.data) {
|
|
toast((result && result.message) || "加载失败");
|
|
updateLoadMore(state.page < state.maxPages);
|
|
return;
|
|
}
|
|
var data = result.data;
|
|
state.page = Number(data.page || page);
|
|
state.maxPages = Number(data.max_pages || 1);
|
|
state.orderby = data.orderby || state.orderby;
|
|
state.order = String(data.order || state.order || "DESC").toUpperCase();
|
|
if (typeof termId === "number" && termId > 0) {
|
|
state.termId = termId;
|
|
}
|
|
if (data.term && data.term.id) {
|
|
state.termId = Number(data.term.id);
|
|
}
|
|
root.setAttribute("data-page", String(state.page));
|
|
root.setAttribute("data-max-pages", String(state.maxPages));
|
|
root.setAttribute("data-orderby", state.orderby);
|
|
root.setAttribute("data-order", state.order);
|
|
root.setAttribute("data-term-id", String(state.termId));
|
|
|
|
if (countEl && typeof data.total !== "undefined") {
|
|
countEl.textContent = "共 " + data.total + " 件商品";
|
|
}
|
|
var titleEl = root.querySelector("[data-aksrt-archive-title]");
|
|
if (titleEl && data.term && data.term.name) {
|
|
titleEl.textContent = data.term.name;
|
|
}
|
|
var descEl = root.querySelector("[data-aksrt-archive-desc]");
|
|
if (descEl && data.term) {
|
|
descEl.textContent = data.term.description || "";
|
|
descEl.style.display = data.term.description ? "" : "none";
|
|
}
|
|
|
|
if (headerHost && data.header_html) {
|
|
// Keep title/count; replace filter widget if present.
|
|
var existingFilter = headerHost.querySelector("[data-aksrt-shop-cat-filter], .shop-cat-filter");
|
|
var tmp = document.createElement("div");
|
|
tmp.innerHTML = data.header_html;
|
|
var nextFilter = tmp.querySelector("[data-aksrt-shop-cat-filter], .shop-cat-filter") || tmp.firstElementChild;
|
|
if (existingFilter && nextFilter) {
|
|
existingFilter.replaceWith(nextFilter);
|
|
} else if (!existingFilter && nextFilter) {
|
|
headerHost.appendChild(nextFilter);
|
|
}
|
|
}
|
|
|
|
var html = data.html || data.empty_html || "";
|
|
if (grid) {
|
|
if (replace) {
|
|
grid.innerHTML = html;
|
|
} else if (html) {
|
|
grid.insertAdjacentHTML("beforeend", html);
|
|
}
|
|
}
|
|
|
|
if (data.paginate_html) {
|
|
var oldPag = root.querySelector(".ajax-pag");
|
|
var wrap = document.createElement("div");
|
|
wrap.innerHTML = data.paginate_html;
|
|
var nextPag = wrap.firstElementChild;
|
|
if (oldPag && nextPag) oldPag.replaceWith(nextPag);
|
|
else if (!oldPag && nextPag && pager) pager.appendChild(nextPag);
|
|
loadMoreBtn = root.querySelector("[data-aksrt-shop-load-more]");
|
|
}
|
|
|
|
syncOrderbyActive();
|
|
updateLoadMore(!!data.has_more || state.page < state.maxPages);
|
|
|
|
try {
|
|
container.dispatchEvent(
|
|
new CustomEvent("post_ajax.ed", {
|
|
bubbles: true,
|
|
detail: { page: state.page, replace: !!replace, data: data },
|
|
})
|
|
);
|
|
document.dispatchEvent(
|
|
new CustomEvent("aksrt:posts:rendered", {
|
|
bubbles: true,
|
|
detail: { container: grid, replace: !!replace },
|
|
})
|
|
);
|
|
} catch (e1) {}
|
|
})
|
|
.catch(function () {
|
|
state.loading = false;
|
|
showLoader(false);
|
|
updateLoadMore(state.page < state.maxPages);
|
|
toast("网络错误");
|
|
});
|
|
}
|
|
|
|
function pushUrl(orderby, order, termUrl) {
|
|
try {
|
|
var u = termUrl ? new URL(termUrl, window.location.origin) : new URL(window.location.href);
|
|
if (orderby) u.searchParams.set("orderby", orderby);
|
|
else u.searchParams.delete("orderby");
|
|
u.searchParams.delete("shop_orderby");
|
|
if (orderby === "price" && order) u.searchParams.set("order", order);
|
|
else u.searchParams.delete("order");
|
|
u.searchParams.delete("paged");
|
|
window.history.replaceState({}, "", u.toString());
|
|
} catch (err) {}
|
|
}
|
|
|
|
root.addEventListener("click", function (e) {
|
|
// Category / sort links with ajax-next — intercept for REST (richer payload).
|
|
var termLink = e.target.closest("[data-aksrt-shop-term].ajax-next, a.ajax-next[data-aksrt-shop-term]");
|
|
if (termLink && root.contains(termLink)) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
var tid = Number(termLink.getAttribute("data-aksrt-shop-term") || 0);
|
|
var href = termLink.getAttribute("href") || "";
|
|
state.autoLoads = 0;
|
|
if (tid > 0) state.termId = tid;
|
|
pushUrl(state.orderby, state.order, href);
|
|
var rt = termLink.getAttribute("route-title");
|
|
if (rt) {
|
|
try {
|
|
document.title = rt;
|
|
} catch (e2) {}
|
|
}
|
|
fetchArchive(1, state.orderby, state.order, true, tid);
|
|
return;
|
|
}
|
|
|
|
var sort = e.target.closest("[data-aksrt-shop-orderby]");
|
|
if (sort && root.contains(sort) && sort.tagName === "A") {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
var orderby = sort.getAttribute("data-aksrt-shop-orderby") || "date";
|
|
var nextOrder = sort.getAttribute("data-aksrt-shop-order") || "";
|
|
if (orderby === "price") {
|
|
if (state.orderby === "price" && nextOrder) {
|
|
state.order = nextOrder.toUpperCase();
|
|
} else if (state.orderby !== "price") {
|
|
state.order = "ASC";
|
|
} else {
|
|
state.order = state.order === "ASC" ? "DESC" : "ASC";
|
|
}
|
|
}
|
|
if (orderby === state.orderby && orderby !== "price" && state.paging === "ajax") return;
|
|
state.orderby = orderby;
|
|
state.autoLoads = 0;
|
|
pushUrl(orderby, state.order);
|
|
fetchArchive(1, orderby, state.order, true);
|
|
return;
|
|
}
|
|
|
|
var more = e.target.closest("[data-aksrt-shop-load-more]");
|
|
if (more && root.contains(more)) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (state.page >= state.maxPages) return;
|
|
fetchArchive(state.page + 1, state.orderby, state.order, false);
|
|
}
|
|
});
|
|
|
|
if (state.paging === "ajax" && state.ias && "IntersectionObserver" in window && loadMoreBtn) {
|
|
var io = new IntersectionObserver(
|
|
function (entries) {
|
|
entries.forEach(function (entry) {
|
|
if (!entry.isIntersecting) return;
|
|
if (state.loading || state.page >= state.maxPages) return;
|
|
if (state.iasMax > 0 && state.autoLoads >= state.iasMax) return;
|
|
state.autoLoads += 1;
|
|
fetchArchive(state.page + 1, state.orderby, state.order, false);
|
|
});
|
|
},
|
|
{ rootMargin: "240px" }
|
|
);
|
|
io.observe(loadMoreBtn);
|
|
}
|
|
})();
|