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>
253 lines
9.6 KiB
JavaScript
253 lines
9.6 KiB
JavaScript
/**
|
||
* 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);
|
||
})();
|