fix(ajax): show the reference skeleton on every load-more surface
Clicking 加载更多 in the 多栏目文章 widget showed no skeleton at all — the
list simply stayed still until the response arrived, then the new cards
appeared. The homepage stream, archives and comments already had a working
skeleton; the widget's AJAX load goes through a different client and never
found its loader.
Root cause was DOM placement, not visibility: ContentRenderer emitted
.post_ajax_loader as a *sibling* of .widget-ajaxpager, while the widget REST
client resolves it with container.querySelector('.post_ajax_loader'). The
node existed but could never match, so .is-loading was never added and the
skeleton stayed display:none. Inactive tab panes had the same shape, and the
direct-load branch of ajaxpager() emitted no loader at all.
Changes, all following the reference theme's post_ajax() contract in
zibll/js/main.js (beforeSend anchors the loader to the end of the list;
success only hides it, never removes it):
- ContentRenderer: nest the loader inside .widget-ajaxpager for both the
direct-load and inactive-tab branches, and drop the sibling append.
- widgets/frontend.js: anchorLoader() moves the loader to the end of the
list before .ajax-pag on every request; appended cards are inserted before
it; a replace preserves the node instead of wiping it with innerHTML; a
fallback loader is created when the DOM has none; loadItems() now returns
its promise; drop the unreachable is-loading auto-load branch.
- Message centre, user-home panels and the search page switch from spinner /
opacity feedback to the same .posts-item-shaped skeleton, so all AJAX
surfaces share one loading contract.
- Remove the second, never-wired skeleton system (.aksrt-skeleton-* in
core-ui.css) and the dead spinner stylesheets/rules (skeleton.css,
ajax-spinner.css, .load-more__*, --skeleton-base/--skeleton-shine) plus the
unregistered assets/js/waterfall.js.
- Extend the widget, message-centre, user-home and asset-ledger contract
tests to pin the loader inside the container, the class-only visibility
switch, and the absence of the deleted dead code.
Verified: 470 node tests, 136 PHP widget assertions, admin-contract and
widget-system audits all green; rebuilt public assets after the CSS/JS
removals.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
271e7f0518
commit
cf4554e7c8
25 files changed
+330
-981
No files matched your search
+25
-1
@@ -70,11 +70,18 @@
|
||||
currentList.querySelectorAll('[data-notification-id]').forEach(function (item) {
|
||||
existingIds.add(item.dataset.notificationId);
|
||||
});
|
||||
// 骨架常驻列表末尾:新条目一律插到它之前,形状与参考主题一致
|
||||
// (post_ajax 的 beforeSend 把 loader 挪到最后一张卡片之后)。
|
||||
var skeleton = currentList.querySelector(':scope > .post_ajax_loader');
|
||||
var firstAdded = null;
|
||||
incomingList.querySelectorAll('.message-item').forEach(function (item) {
|
||||
if (existingIds.has(item.dataset.notificationId)) return;
|
||||
if (!firstAdded) firstAdded = item;
|
||||
currentList.appendChild(item);
|
||||
if (skeleton) {
|
||||
currentList.insertBefore(item, skeleton);
|
||||
} else {
|
||||
currentList.appendChild(item);
|
||||
}
|
||||
});
|
||||
|
||||
var currentPagination = content.querySelector('.messages-pagination');
|
||||
@@ -93,6 +100,9 @@
|
||||
var settings = options || {};
|
||||
root.classList.add('is-loading');
|
||||
content.setAttribute('aria-busy', 'true');
|
||||
// 骨架只在 AJAX 请求期间展开(.is-loading),与文章列表翻页同一契约。
|
||||
var skeleton = content.querySelector('.messages-list > .post_ajax_loader');
|
||||
if (skeleton) skeleton.classList.add('is-loading');
|
||||
|
||||
return post('aksrt_load_message_center', {
|
||||
view: requested.view,
|
||||
@@ -111,7 +121,19 @@
|
||||
if (settings.append && state.paginateType === 'ajax_lists') {
|
||||
firstAdded = appendMessagePage(result.data.content);
|
||||
} else {
|
||||
// 替换时保留骨架容器(参考主题 success 后只 hide() 不删除),
|
||||
// 让后续翻页复用同一节点而不是回到无骨架状态。
|
||||
var keep = content.querySelector('.post_ajax_loader');
|
||||
content.innerHTML = result.data.content;
|
||||
var nextSkeleton = content.querySelector('.post_ajax_loader');
|
||||
if (keep && !nextSkeleton) {
|
||||
var list = content.querySelector('.messages-list');
|
||||
if (list) {
|
||||
list.appendChild(keep);
|
||||
} else {
|
||||
content.appendChild(keep);
|
||||
}
|
||||
}
|
||||
}
|
||||
updateHeaderBadge(result.data.unread);
|
||||
|
||||
@@ -131,6 +153,8 @@
|
||||
}).finally(function () {
|
||||
root.classList.remove('is-loading');
|
||||
content.removeAttribute('aria-busy');
|
||||
var loader = content.querySelector('.messages-list > .post_ajax_loader');
|
||||
if (loader) loader.classList.remove('is-loading');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
/**
|
||||
* AKSRT Waterfall — 瀑布流/异步列表
|
||||
*
|
||||
* 提供纯 CSS column-based 瀑布流布局和无限滚动加载能力。
|
||||
* 无外部依赖。
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Initialize waterfall layout on a container.
|
||||
*
|
||||
* @param {HTMLElement} container - The container element
|
||||
* @param {Object} options
|
||||
* @param {number} options.columns - Number of columns (default: auto from CSS)
|
||||
* @param {string} options.itemSelector - Child item selector (default: '.waterfall-item')
|
||||
* @param {number} options.gap - Gap between items in px (default: 16)
|
||||
*/
|
||||
function Waterfall(container, options) {
|
||||
if (!container) return;
|
||||
|
||||
this.container = container;
|
||||
this.options = Object.assign({
|
||||
columns: 0,
|
||||
itemSelector: '.waterfall-item',
|
||||
gap: 16,
|
||||
}, options || {});
|
||||
|
||||
this._resizeHandler = this.layout.bind(this);
|
||||
window.addEventListener('resize', this._resizeHandler);
|
||||
this.layout();
|
||||
}
|
||||
|
||||
Waterfall.prototype.layout = function () {
|
||||
var items = this.container.querySelectorAll(this.options.itemSelector);
|
||||
if (items.length === 0) return;
|
||||
|
||||
var cols = this.options.columns;
|
||||
if (cols <= 0) {
|
||||
var containerWidth = this.container.offsetWidth;
|
||||
cols = Math.max(1, Math.floor(containerWidth / 280));
|
||||
}
|
||||
|
||||
var gap = this.options.gap;
|
||||
var colWidth = (this.container.offsetWidth - gap * (cols - 1)) / cols;
|
||||
var colHeights = new Array(cols).fill(0);
|
||||
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var item = items[i];
|
||||
item.style.width = colWidth + 'px';
|
||||
item.style.position = 'absolute';
|
||||
|
||||
// Find shortest column
|
||||
var minHeight = Infinity;
|
||||
var minCol = 0;
|
||||
for (var c = 0; c < cols; c++) {
|
||||
if (colHeights[c] < minHeight) {
|
||||
minHeight = colHeights[c];
|
||||
minCol = c;
|
||||
}
|
||||
}
|
||||
|
||||
item.style.left = (minCol * (colWidth + gap)) + 'px';
|
||||
item.style.top = colHeights[minCol] + 'px';
|
||||
|
||||
colHeights[minCol] += item.offsetHeight + gap;
|
||||
}
|
||||
|
||||
// Set container height to tallest column
|
||||
var maxHeight = Math.max.apply(null, colHeights);
|
||||
this.container.style.height = maxHeight + 'px';
|
||||
this.container.style.position = 'relative';
|
||||
};
|
||||
|
||||
Waterfall.prototype.relayout = function () {
|
||||
this.layout();
|
||||
};
|
||||
|
||||
Waterfall.prototype.destroy = function () {
|
||||
window.removeEventListener('resize', this._resizeHandler);
|
||||
};
|
||||
|
||||
/**
|
||||
* Infinite scroll loader.
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {string} options.containerSelector - List container selector
|
||||
* @param {string} options.loadMoreSelector - Load-more button selector
|
||||
* @param {Function} options.loadMore - Async function that loads more items
|
||||
* @param {number} options.threshold - Distance from bottom to trigger (default: 200)
|
||||
*/
|
||||
function InfiniteScroll(options) {
|
||||
this.options = Object.assign({
|
||||
containerSelector: '.aksrt-infinite-list',
|
||||
loadMoreSelector: '.aksrt-load-more',
|
||||
loadMore: null,
|
||||
threshold: 200,
|
||||
}, options || {});
|
||||
|
||||
this.loading = false;
|
||||
this.hasMore = true;
|
||||
this._scrollHandler = this._check.bind(this);
|
||||
|
||||
window.addEventListener('scroll', this._scrollHandler);
|
||||
this._check();
|
||||
}
|
||||
|
||||
InfiniteScroll.prototype._check = function () {
|
||||
if (this.loading || !this.hasMore) return;
|
||||
|
||||
var loadMoreBtn = document.querySelector(this.options.loadMoreSelector);
|
||||
if (!loadMoreBtn) {
|
||||
this.hasMore = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var rect = loadMoreBtn.getBoundingClientRect();
|
||||
if (rect.top < window.innerHeight + this.options.threshold) {
|
||||
this._load();
|
||||
}
|
||||
};
|
||||
|
||||
InfiniteScroll.prototype._load = function () {
|
||||
if (!this.options.loadMore) return;
|
||||
|
||||
this.loading = true;
|
||||
var self = this;
|
||||
|
||||
this.options.loadMore().then(function (hasMore) {
|
||||
self.hasMore = hasMore !== false;
|
||||
self.loading = false;
|
||||
if (self.hasMore) {
|
||||
self._check();
|
||||
}
|
||||
}).catch(function () {
|
||||
self.loading = false;
|
||||
});
|
||||
};
|
||||
|
||||
InfiniteScroll.prototype.destroy = function () {
|
||||
window.removeEventListener('scroll', this._scrollHandler);
|
||||
};
|
||||
|
||||
// Auto-init
|
||||
var waterfallInstances = [];
|
||||
|
||||
function relayoutAll() {
|
||||
waterfallInstances.forEach(function (instance) {
|
||||
if (instance && typeof instance.relayout === 'function') {
|
||||
instance.relayout();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function autoInit() {
|
||||
document.querySelectorAll('[data-aksrt-waterfall]:not([data-aksrt-waterfall-bound])').forEach(function (el) {
|
||||
el.dataset.aksrtWaterfallBound = '1';
|
||||
var cols = parseInt(el.dataset.aksrtWaterfallColumns) || 0;
|
||||
var gap = parseInt(el.dataset.aksrtWaterfallGap) || 16;
|
||||
waterfallInstances.push(new Waterfall(el, { columns: cols, gap: gap }));
|
||||
});
|
||||
relayoutAll();
|
||||
}
|
||||
|
||||
jQuery(document).off('auto_fun.aksrt-waterfall').on('auto_fun.aksrt-waterfall', autoInit);
|
||||
jQuery(document).off('lazyloaded.aksrt-waterfall').on('lazyloaded.aksrt-waterfall', relayoutAll);
|
||||
|
||||
// Export to global
|
||||
window.AksrtWaterfall = Waterfall;
|
||||
window.AksrtInfiniteScroll = InfiniteScroll;
|
||||
})();
|
||||
Reference in new issue
Block a user