Files
wordpress-theme-aksrt/assets/js/admin-messages.js
T
kate522andClaude Fable 5 fe3ef82718 feat(admin): upgrade message management and broadcast interactions
Add message details, per-row actions, selection-aware bulk controls and
confirmation dialogs. Merge filters into the list card and link overview
statistics to the matching views with site-timezone today filtering.

Improve the compose dialog with Unicode character counters, recipient mode
segments, role counts and explicit broadcast confirmation verified against
the resolved role and recipient count on submission. Split rendering into
focused sibling traits and cover rendering, handlers and UI interactions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-30 17:24:54 +08:00

228 lines
9.7 KiB
JavaScript

/** 消息管理:详情、选择与批量操作、发送计数及角色群发确认。仅本页入队。 */
(function () {
'use strict';
var root = document.querySelector('.aksrt-messages-admin');
if (!root) return;
var compose = document.getElementById('aksrt-msg-compose');
var detail = document.getElementById('aksrt-msg-detail');
var confirmDialog = document.getElementById('aksrt-msg-confirm');
if (!compose || !detail || !confirmDialog || typeof compose.showModal !== 'function') return;
function text(element, key, values) {
var value = element.getAttribute('data-i18n-' + key) || '';
return value.replace(/\{(\w+)\}/g, function (match, name) {
return values && values[name] !== undefined ? String(values[name]) : match;
});
}
function syncScroll() {
document.documentElement.classList.toggle('aksrt-msg-modal-open', !!root.querySelector('dialog[open]'));
}
function open(dialog) {
if (!dialog.open) dialog.showModal();
syncScroll();
}
root.querySelectorAll('.aksrt-msg-dialog').forEach(function (dialog) {
var outsideStart = false;
function outside(event) {
var rect = dialog.getBoundingClientRect();
return event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom;
}
dialog.querySelectorAll('[data-aksrt-dialog-close]').forEach(function (button) {
button.addEventListener('click', function () { dialog.close(); });
});
dialog.addEventListener('pointerdown', function (event) {
outsideStart = event.target === dialog && outside(event);
});
dialog.addEventListener('click', function (event) {
if (outsideStart && event.target === dialog && outside(event)) dialog.close();
outsideStart = false;
});
dialog.addEventListener('close', syncScroll);
});
var pending = null;
var confirmButton = confirmDialog.querySelector('[data-aksrt-msg-confirm]');
function ask(title, message, label, danger, callback) {
pending = callback;
confirmDialog.querySelector('[data-aksrt-dialog-title]').textContent = title;
confirmDialog.querySelector('#aksrt-msg-confirm-text').textContent = message;
confirmButton.textContent = label;
confirmButton.classList.toggle('aksrt-messages-admin__danger', danger);
confirmButton.classList.toggle('button-primary', !danger);
open(confirmDialog);
confirmDialog.querySelector('[data-aksrt-dialog-close][autofocus]').focus();
}
confirmDialog.addEventListener('cancel', function () { pending = null; });
confirmDialog.addEventListener('close', function () { pending = null; });
confirmButton.addEventListener('click', function () {
var callback = pending;
pending = null;
confirmDialog.close();
if (callback) callback();
});
root.querySelectorAll('[data-aksrt-msg-view]').forEach(function (button) {
button.addEventListener('click', function () {
var template = document.getElementById('aksrt-msg-detail-' + button.getAttribute('data-aksrt-msg-view'));
if (!template) return;
detail.querySelector('[data-aksrt-msg-detail-body]').replaceChildren(template.content.cloneNode(true));
open(detail);
});
});
var list = root.querySelector('[data-aksrt-msg-list]');
var listSubmitting = false;
var syncSelection;
if (list) {
var checks = Array.from(list.querySelectorAll('[data-aksrt-msg-check]'));
var checkAll = list.querySelector('[data-aksrt-msg-check-all]');
var selection = list.querySelector('[data-aksrt-msg-selection]');
var bulkButtons = list.querySelectorAll('[data-aksrt-msg-action]:not([data-id])');
function selected() { return checks.filter(function (check) { return check.checked; }); }
syncSelection = function () {
var count = selected().length;
selection.textContent = selection.getAttribute('data-label').replace('%d', String(count));
checkAll.checked = count > 0 && count === checks.length;
checkAll.indeterminate = count > 0 && count < checks.length;
checkAll.disabled = listSubmitting || checks.length === 0;
bulkButtons.forEach(function (button) { button.disabled = listSubmitting || count === 0; });
checks.forEach(function (check) { check.closest('tr').classList.toggle('is-selected', check.checked); });
};
checkAll.addEventListener('change', function () {
checks.forEach(function (check) { check.checked = checkAll.checked; });
syncSelection();
});
checks.forEach(function (check) { check.addEventListener('change', syncSelection); });
function mutate(op, id) {
if (listSubmitting) return;
list.elements.bulk_action.value = op;
list.elements.message_id.value = id || '';
list.requestSubmit();
}
list.querySelectorAll('[data-aksrt-msg-action]').forEach(function (button) {
button.addEventListener('click', function () {
if (listSubmitting) return;
var op = button.getAttribute('data-aksrt-msg-action');
var id = button.getAttribute('data-id');
var count = id ? 1 : selected().length;
if (!count) return;
if (op === 'delete') {
ask(text(confirmDialog, 'delete-title'), text(confirmDialog, 'delete-text', { count: count }), text(confirmDialog, 'delete-send'), true, function () { mutate(op, id); });
} else {
mutate(op, id);
}
});
});
list.addEventListener('submit', function (event) {
if (listSubmitting || !list.elements.bulk_action.value || (!list.elements.message_id.value && !selected().length)) {
event.preventDefault();
return;
}
listSubmitting = true;
// 只锁操作按钮,不禁用复选框,否则 ids[] 不会随表单提交。
list.querySelectorAll('button').forEach(function (button) { button.disabled = true; });
syncSelection();
});
syncSelection();
}
var form = compose.querySelector('[data-aksrt-msg-compose]');
var send = form.querySelector('[data-aksrt-msg-send]');
var role = form.elements.role;
var counters = form.querySelectorAll('[data-count-limit]');
var sending = false;
var approved = '';
function clearApproval() {
approved = '';
form.elements.confirmed_role.value = '';
form.elements.confirmed_count.value = '';
}
function syncCounters() {
counters.forEach(function (field) {
// 与 PHP mb_strlen 一致按 Unicode 码点计数,emoji 不占两个字。
var count = Array.from(field.value).length;
var limit = Number(field.getAttribute('data-count-limit'));
var counter = form.querySelector('[data-count-for="' + field.name + '"]');
counter.textContent = count + ' / ' + limit;
counter.classList.toggle('is-over', count > limit);
field.setCustomValidity(count > limit ? text(form, 'count-error', { count: limit }) : '');
});
}
function syncRole() {
var option = role.options[role.selectedIndex];
var count = option ? Number(option.getAttribute('data-count')) : 0;
form.querySelector('[data-aksrt-msg-role-hint]').textContent = count > 0 && !option.disabled ? text(form, 'role-hint', { count: count }) : '';
}
function mode() { return form.querySelector('input[name="recipient_mode"]:checked').value; }
function syncMode() {
clearApproval();
form.querySelectorAll('[data-aksrt-msg-pane]').forEach(function (pane) {
var active = pane.getAttribute('data-aksrt-msg-pane') === mode();
pane.hidden = !active;
pane.querySelectorAll('input, select, textarea').forEach(function (field) { field.disabled = !active; });
});
syncRole();
}
form.querySelectorAll('input[name="recipient_mode"]').forEach(function (radio) { radio.addEventListener('change', syncMode); });
role.addEventListener('change', function () { clearApproval(); syncRole(); });
form.addEventListener('input', function () { clearApproval(); syncCounters(); });
function snapshot() {
return JSON.stringify([role.value, role.options[role.selectedIndex].getAttribute('data-count'), form.elements.title.value, form.elements.content.value, form.elements.target_url.value]);
}
form.addEventListener('submit', function (event) {
syncCounters();
if (sending || !form.reportValidity()) {
event.preventDefault();
return;
}
if (mode() === 'role' && approved !== snapshot()) {
event.preventDefault();
var option = role.options[role.selectedIndex];
if (!option || option.disabled || !role.value) return;
var count = Number(option.getAttribute('data-count'));
var current = snapshot();
ask(text(form, 'role-title'), text(form, 'role-confirm', { role: option.getAttribute('data-label'), count: count }), text(form, 'role-send'), false, function () {
if (mode() !== 'role' || current !== snapshot()) return;
approved = current;
form.elements.confirmed_role.value = role.value;
form.elements.confirmed_count.value = String(count);
form.requestSubmit(send);
});
return;
}
sending = true;
send.disabled = true;
});
root.querySelectorAll('[data-aksrt-dialog-open]').forEach(function (button) {
button.addEventListener('click', function () {
open(compose);
var field = mode() === 'role' ? role : form.elements.users;
field.focus();
});
});
window.addEventListener('pageshow', function () {
sending = false;
send.disabled = false;
clearApproval();
if (list) {
listSubmitting = false;
list.elements.bulk_action.value = '';
list.elements.message_id.value = '';
list.querySelectorAll('button[data-id], button[data-aksrt-msg-view]').forEach(function (button) { button.disabled = false; });
syncSelection();
}
syncCounters();
});
syncMode();
syncCounters();
if (compose.hasAttribute('data-aksrt-dialog-autoopen')) open(compose);
})();