Files
kate522andClaude Fable 5 cf4554e7c8 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>
2026-10-03 22:22:15 +08:00

253 lines
9.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* AKSRT message center DOM navigation.
*
* @package AKSRT
*/
(function () {
'use strict';
var root = document.querySelector('[data-message-center]');
if (!root) return;
var nonce = typeof aksrtData !== 'undefined' ? aksrtData.nonce : '';
var ajaxUrl = typeof aksrtData !== 'undefined' ? aksrtData.ajaxUrl : '/wp-admin/admin-ajax.php';
var content = root.querySelector('[data-message-content]');
var nav = root.querySelector('[data-message-nav]');
var state = {
view: root.dataset.view || 'unread',
page: 1,
detail: 0,
paginateType: root.dataset.paginateType || 'ajax_lists',
};
var pollInterval = 60000;
function post(action, data) {
var body = new URLSearchParams({ action: action, nonce: nonce });
Object.keys(data || {}).forEach(function (key) {
var value = data[key];
if (Array.isArray(value)) {
value.forEach(function (item) { body.append(key + '[]', item); });
} else {
body.append(key, value);
}
});
return fetch(ajaxUrl, { method: 'POST', body: body, credentials: 'same-origin' })
.then(function (response) { return response.json(); });
}
function routeFor(view) {
var link = nav.querySelector('[data-message-view="' + view + '"]');
return link ? link.href : window.location.href;
}
function updateHeaderBadge(unread) {
var count = Math.max(0, Number(unread) || 0);
var headerBadge = document.querySelector('.aksrt-notification-badge');
var avatarDot = document.querySelector('.nav-user__dot');
if (headerBadge) {
headerBadge.textContent = String(count);
headerBadge.style.display = count > 0 ? '' : 'none';
}
if (avatarDot) avatarDot.style.display = count > 0 ? '' : 'none';
}
function renderError(message) {
content.innerHTML = '<div class="messages-empty"><span class="messages-empty__icon"><i class="fa fas fa-exclamation-circle"></i></span><p class="messages-empty__text"></p></div>';
content.querySelector('.messages-empty__text').textContent = message || '加载失败,请稍后重试';
}
function appendMessagePage(markup) {
var incoming = document.createElement('div');
incoming.innerHTML = markup;
var currentList = content.querySelector('.messages-list');
var incomingList = incoming.querySelector('.messages-list');
if (!currentList || !incomingList) {
content.innerHTML = markup;
return null;
}
var existingIds = new Set();
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;
if (skeleton) {
currentList.insertBefore(item, skeleton);
} else {
currentList.appendChild(item);
}
});
var currentPagination = content.querySelector('.messages-pagination');
var incomingPagination = incoming.querySelector('.messages-pagination');
if (currentPagination) currentPagination.remove();
if (incomingPagination) content.appendChild(incomingPagination);
return firstAdded;
}
function load(next, options) {
var requested = {
view: next.view || state.view,
page: next.page || 1,
detail: next.detail || 0,
};
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,
page: requested.page,
notification_id: requested.detail,
}).then(function (result) {
if (!result.success || !result.data) {
throw new Error(result.data && result.data.message ? result.data.message : '加载失败,请稍后重试');
}
requested.paginateType = result.data.paginate_type || state.paginateType;
state = requested;
root.dataset.view = state.view;
root.dataset.paginateType = state.paginateType;
nav.innerHTML = result.data.nav;
var firstAdded = null;
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);
if (settings.history === 'push') {
window.history.pushState(state, '', routeFor(state.view));
} else if (settings.history === 'replace') {
window.history.replaceState(state, '', routeFor(state.view));
}
if (firstAdded) {
firstAdded.setAttribute('tabindex', '-1');
firstAdded.focus({ preventScroll: true });
} else {
content.focus({ preventScroll: true });
}
}).catch(function (error) {
renderError(error.message);
}).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');
});
}
function reloadList() {
return load({ view: state.view, page: state.page, detail: 0 });
}
document.addEventListener('click', function (event) {
var tab = event.target.closest('[data-message-view]');
if (!tab || !root.contains(tab) || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
event.preventDefault();
load({ view: tab.dataset.messageView, page: 1, detail: 0 }, { history: 'push' });
});
document.addEventListener('click', function (event) {
var button = event.target.closest('.js-msg-detail');
if (!button || !root.contains(button)) return;
event.preventDefault();
load({ view: button.dataset.view || state.view, page: state.page, detail: button.dataset.id });
});
document.addEventListener('click', function (event) {
var button = event.target.closest('[data-message-back]');
if (!button || !root.contains(button)) return;
load({ view: button.dataset.view || state.view, page: state.page, detail: 0 });
});
document.addEventListener('click', function (event) {
var button = event.target.closest('[data-message-page]');
if (!button || !root.contains(button)) return;
event.preventDefault();
load({ view: state.view, page: button.dataset.messagePage, detail: 0 });
});
document.addEventListener('click', function (event) {
var button = event.target.closest('[data-message-next-page]');
if (!button || !root.contains(button)) return;
event.preventDefault();
load(
{ view: state.view, page: button.dataset.messageNextPage, detail: 0 },
{ append: true }
);
});
document.addEventListener('click', function (event) {
var button = event.target.closest('.js-msg-mark-read');
if (!button || !root.contains(button)) return;
post('aksrt_mark_notification_read', { notification_id: button.dataset.id }).then(function (result) {
if (result.success) reloadList();
});
});
document.addEventListener('click', function (event) {
var button = event.target.closest('.js-msg-delete');
if (!button || !root.contains(button)) return;
post('aksrt_delete_notification', { notification_id: button.dataset.id }).then(function (result) {
if (result.success) reloadList();
});
});
document.addEventListener('click', function (event) {
var button = event.target.closest('.js-msg-mark-all-read');
if (!button || !root.contains(button)) return;
post('aksrt_mark_notification_read', { mark_all: '1' }).then(function (result) {
if (result.success) reloadList();
});
});
document.addEventListener('click', function (event) {
var button = event.target.closest('.js-msg-clear-read');
if (!button || !root.contains(button)) return;
var ids = [];
root.querySelectorAll('.message-item:not(.is-unread)[data-notification-id]').forEach(function (item) {
if (/^\d+$/.test(item.dataset.notificationId)) ids.push(item.dataset.notificationId);
});
if (!ids.length) return;
post('aksrt_delete_notifications', { notification_ids: ids }).then(function (result) {
if (result.success) reloadList();
});
});
window.addEventListener('popstate', function (event) {
var previous = event.state;
if (previous && previous.view) {
load(previous);
}
});
window.history.replaceState(state, '', routeFor(state.view));
window.setInterval(function () {
post('aksrt_check_new_notifications', {}).then(function (result) {
if (result.success && result.data) updateHeaderBadge(result.data.unread);
}).catch(function () {});
}, pollInterval);
})();