feat(pages): 1:1 文章导航 posts-navs (zibll postsnavs)
Per-category full post lists, affix TOC shell, admin drag-sort settings modal + AJAX save (navs_show_cat_id / name / desc).
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* AKSRT 文章导航页 — zibll postsnavs shell + admin settings.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
/* header card */
|
||||
.page-template-posts-navs .content-wrap,
|
||||
main.page-template-posts-navs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
main.page-template-posts-navs .content-wrap {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
main.page-template-posts-navs .sidebar {
|
||||
flex: 0 0 260px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.posts-navs .title-theme h2 {
|
||||
margin: 0;
|
||||
font-size: 1.15em;
|
||||
}
|
||||
|
||||
.posts-navs__list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.posts-navs__item {
|
||||
margin: 0 0 10px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.posts-navs__item .text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.posts-navs__item .icon-circle {
|
||||
display: inline-block;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: var(--muted-3-color, #ccc);
|
||||
margin: 0 0.55em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.posts-navs__item a {
|
||||
color: var(--color-ink, inherit);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.posts-navs__item a:hover {
|
||||
color: var(--focus-color, var(--color-primary));
|
||||
}
|
||||
|
||||
.posts-navs .theme-box {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.posts-navs .box-body.notop {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
/* TOC sidebar filled by section-navs.js */
|
||||
.posts-nav-box .posts-nav-lists,
|
||||
.posts-nav-box .aksrt-section-nav {
|
||||
max-height: 400px;
|
||||
padding-left: 10px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.posts-nav-box .zib-widget {
|
||||
background: var(--main-bg-color, var(--surface-1, #fff));
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px var(--main-shadow, rgba(0, 0, 0, 0.06));
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.posts-nav-lists .bl {
|
||||
border-left: 1px solid var(--main-border-color, #eee);
|
||||
padding: 12px 10px 12px 0;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.posts-nav-lists li {
|
||||
position: relative;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.posts-nav-lists li a {
|
||||
margin: 0 10px;
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
margin-right: 20px;
|
||||
color: var(--muted-2-color, #666);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.posts-nav-lists li a:hover,
|
||||
.posts-nav-lists li.active a {
|
||||
background: var(--main-shadow, rgba(0, 0, 0, 0.04));
|
||||
color: var(--focus-color, var(--color-primary));
|
||||
}
|
||||
|
||||
.posts-nav-lists li.n-H1 a,
|
||||
.posts-nav-lists li.n-H2 a {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* settings modal */
|
||||
#post-navs-sz.modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1050;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
#post-navs-sz.modal.is-open,
|
||||
#post-navs-sz.modal.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#post-navs-sz .modal-dialog {
|
||||
width: min(640px, 100%);
|
||||
max-height: 90vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#post-navs-sz .modal-content {
|
||||
background: var(--main-bg-color, #fff);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
#post-navs-sz .modal-header,
|
||||
#post-navs-sz .modal-body,
|
||||
#post-navs-sz .modal-footer {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
#post-navs-sz .modal-header {
|
||||
border-bottom: 1px solid var(--main-border-color, #eee);
|
||||
}
|
||||
|
||||
#post-navs-sz .modal-footer {
|
||||
border-top: 1px solid var(--main-border-color, #eee);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#post-navs-sz .close {
|
||||
float: right;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#post-navs-sz .form-control {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--main-border-color, #ddd);
|
||||
border-radius: 6px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#post-navs-sz .form-group {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
#post-navs-sz .row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -8px;
|
||||
}
|
||||
|
||||
#post-navs-sz .col-xs-6,
|
||||
#post-navs-sz .col-sm-6 {
|
||||
width: 50%;
|
||||
padding: 0 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.cat-group {
|
||||
height: 200px;
|
||||
background: var(--body-bg-color, var(--surface-2, #f6f7f9));
|
||||
border: 1px solid var(--main-border-color, #ddd);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
padding: 5px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sortable-cat-item {
|
||||
background: var(--main-border-color, #e8e8e8);
|
||||
padding: 4px 8px;
|
||||
transition: 0.2s;
|
||||
border-radius: 4px;
|
||||
margin: 2px;
|
||||
display: inline-block;
|
||||
cursor: grab;
|
||||
width: calc(100% - 15px);
|
||||
min-width: 100px;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.sortable-cat-item .badge {
|
||||
margin-left: 4px;
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
color: inherit;
|
||||
border-radius: 10px;
|
||||
padding: 0 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#cat-sortable-ok .sortable-cat-item {
|
||||
background: rgba(73, 214, 253, 0.12);
|
||||
color: #069cf3;
|
||||
}
|
||||
|
||||
.sortable-cat-item.is-dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.sortable-cat-item.sortable-selected {
|
||||
background: rgba(251, 82, 82, 0.1) !important;
|
||||
color: #fb5454 !important;
|
||||
}
|
||||
|
||||
.cat-group.is-drop-target {
|
||||
outline: 2px dashed var(--focus-color, #f04494);
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
main.page-template-posts-navs .sidebar {
|
||||
flex: 1 1 100%;
|
||||
order: -1;
|
||||
}
|
||||
|
||||
#post-navs-sz .col-xs-6,
|
||||
#post-navs-sz .col-sm-6 {
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* AKSRT 文章导航页 — admin settings modal + HTML5 drag-sort (no SortableJS dependency).
|
||||
* TOC generation is handled by section-navs.js via [data-nav] + .posts-nav-box.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function qs(sel, root) {
|
||||
return (root || document).querySelector(sel);
|
||||
}
|
||||
|
||||
function qsa(sel, root) {
|
||||
return Array.prototype.slice.call((root || document).querySelectorAll(sel));
|
||||
}
|
||||
|
||||
function ajaxUrl() {
|
||||
if (window.aksrtData && window.aksrtData.ajaxUrl) return window.aksrtData.ajaxUrl;
|
||||
if (window.ajaxurl) return window.ajaxurl;
|
||||
return '/wp-admin/admin-ajax.php';
|
||||
}
|
||||
|
||||
function toast(msg, type) {
|
||||
if (window.aksrt && typeof window.aksrt.toast === 'function') {
|
||||
window.aksrt.toast(msg, type || 'info');
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-alert
|
||||
window.alert(msg);
|
||||
}
|
||||
|
||||
function initModal() {
|
||||
var modal = qs('#post-navs-sz');
|
||||
if (!modal) return;
|
||||
|
||||
function openModal(e) {
|
||||
if (e) e.preventDefault();
|
||||
modal.classList.add('is-open', 'show');
|
||||
modal.setAttribute('aria-hidden', 'false');
|
||||
}
|
||||
|
||||
function closeModal(e) {
|
||||
if (e) e.preventDefault();
|
||||
modal.classList.remove('is-open', 'show');
|
||||
modal.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
qsa('[data-toggle="modal"][data-target="#post-navs-sz"], a[href="#post-navs-sz"]').forEach(function (el) {
|
||||
el.addEventListener('click', openModal);
|
||||
});
|
||||
// Title [设置] uses data-target.
|
||||
qsa('[data-target="#post-navs-sz"]').forEach(function (el) {
|
||||
el.addEventListener('click', openModal);
|
||||
});
|
||||
qsa('[data-dismiss="modal"]', modal).forEach(function (btn) {
|
||||
btn.addEventListener('click', closeModal);
|
||||
});
|
||||
modal.addEventListener('click', function (e) {
|
||||
if (e.target === modal) closeModal(e);
|
||||
});
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && modal.classList.contains('is-open')) closeModal(e);
|
||||
});
|
||||
}
|
||||
|
||||
function initDragSort() {
|
||||
var left = qs('#cat-sortable');
|
||||
var right = qs('#cat-sortable-ok');
|
||||
if (!left || !right) return;
|
||||
|
||||
var dragEl = null;
|
||||
|
||||
function onDragStart(e) {
|
||||
dragEl = e.currentTarget;
|
||||
dragEl.classList.add('is-dragging');
|
||||
try {
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', dragEl.getAttribute('data-id') || '');
|
||||
} catch (err) { /* IE */ }
|
||||
}
|
||||
|
||||
function onDragEnd() {
|
||||
if (dragEl) dragEl.classList.remove('is-dragging');
|
||||
dragEl = null;
|
||||
qsa('.cat-group').forEach(function (g) {
|
||||
g.classList.remove('is-drop-target');
|
||||
});
|
||||
}
|
||||
|
||||
function bindItem(el) {
|
||||
el.setAttribute('draggable', 'true');
|
||||
el.addEventListener('dragstart', onDragStart);
|
||||
el.addEventListener('dragend', onDragEnd);
|
||||
}
|
||||
|
||||
qsa('.sortable-cat-item', left).forEach(bindItem);
|
||||
qsa('.sortable-cat-item', right).forEach(bindItem);
|
||||
|
||||
[left, right].forEach(function (group) {
|
||||
group.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
group.classList.add('is-drop-target');
|
||||
if (!dragEl) return;
|
||||
var after = getDragAfterElement(group, e.clientY);
|
||||
if (after == null) {
|
||||
group.appendChild(dragEl);
|
||||
} else {
|
||||
group.insertBefore(dragEl, after);
|
||||
}
|
||||
});
|
||||
group.addEventListener('dragleave', function () {
|
||||
group.classList.remove('is-drop-target');
|
||||
});
|
||||
group.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
group.classList.remove('is-drop-target');
|
||||
});
|
||||
});
|
||||
|
||||
function getDragAfterElement(container, y) {
|
||||
var els = qsa('.sortable-cat-item:not(.is-dragging)', container);
|
||||
var closest = { offset: Number.NEGATIVE_INFINITY, element: null };
|
||||
els.forEach(function (child) {
|
||||
var box = child.getBoundingClientRect();
|
||||
var offset = y - box.top - box.height / 2;
|
||||
if (offset < 0 && offset > closest.offset) {
|
||||
closest = { offset: offset, element: child };
|
||||
}
|
||||
});
|
||||
return closest.element;
|
||||
}
|
||||
}
|
||||
|
||||
function initSave() {
|
||||
var form = qs('#aksrt-posts-navs-settings');
|
||||
if (!form) return;
|
||||
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var ids = [];
|
||||
qsa('#cat-sortable-ok .sortable-cat-item').forEach(function (item) {
|
||||
var id = item.getAttribute('data-id');
|
||||
if (id) ids.push(id);
|
||||
});
|
||||
if (!ids.length) {
|
||||
toast('请选择需要显示的分类', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
var fd = new FormData(form);
|
||||
fd.set('action', 'aksrt_post_navs_settings');
|
||||
// PHP accepts array; append each.
|
||||
fd.delete('navs_show_cat_id');
|
||||
ids.forEach(function (id) {
|
||||
fd.append('navs_show_cat_id[]', id);
|
||||
});
|
||||
|
||||
var btn = qs('button[type="submit"]', form);
|
||||
if (btn) btn.disabled = true;
|
||||
|
||||
fetch(ajaxUrl(), {
|
||||
method: 'POST',
|
||||
body: fd,
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
if (!data || data.error) {
|
||||
toast((data && data.msg) || '保存失败', 'danger');
|
||||
return;
|
||||
}
|
||||
toast(data.msg || '保存成功', 'success');
|
||||
if (data.reload) {
|
||||
window.setTimeout(function () { window.location.reload(); }, 400);
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
toast('网络错误', 'danger');
|
||||
})
|
||||
.finally(function () {
|
||||
if (btn) btn.disabled = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build simple TOC into .posts-nav-box if section-navs hasn't already.
|
||||
* Uses category h2 headings inside .posts-navs.
|
||||
*/
|
||||
function initFallbackToc() {
|
||||
var box = qs('.posts-nav-box');
|
||||
var root = qs('.posts-navs[data-nav]');
|
||||
if (!box || !root) return;
|
||||
if (box.querySelector('.posts-nav-lists, .aksrt-section-nav, .toc-list')) return;
|
||||
|
||||
var headings = qsa('.title-theme h2, h2', root);
|
||||
if (headings.length < 1) return;
|
||||
|
||||
var title = box.getAttribute('data-title') || '导航目录';
|
||||
var list = '';
|
||||
headings.forEach(function (h, i) {
|
||||
if (!h.id) h.id = 'navs-sec-' + i;
|
||||
list += '<li class="n-H2"><a class="text-ellipsis" href="#' + h.id + '">' + (h.textContent || '').trim() + '</a></li>';
|
||||
});
|
||||
|
||||
box.innerHTML =
|
||||
'<div class="box-body notop"><div class="title-theme">' + title + '</div></div>' +
|
||||
'<div class="zib-widget"><div class="posts-nav-lists scroll-y mini-scrollbar list-unstyled">' +
|
||||
'<ul class="bl relative nav">' + list + '</ul></div></div>';
|
||||
|
||||
qsa('a[href^="#"]', box).forEach(function (a) {
|
||||
a.addEventListener('click', function (e) {
|
||||
var id = a.getAttribute('href');
|
||||
var el = id ? document.getElementById(id.replace(/^#/, '')) : null;
|
||||
if (!el) return;
|
||||
e.preventDefault();
|
||||
var top = el.getBoundingClientRect().top + window.pageYOffset - 90;
|
||||
window.scrollTo({ top: top, behavior: 'smooth' });
|
||||
});
|
||||
});
|
||||
|
||||
function highlight() {
|
||||
var y = window.pageYOffset + 120;
|
||||
var current = headings[0];
|
||||
headings.forEach(function (h) {
|
||||
if (h.offsetTop <= y) current = h;
|
||||
});
|
||||
qsa('li', box).forEach(function (li) {
|
||||
var a = li.querySelector('a');
|
||||
if (a && current && a.getAttribute('href') === '#' + current.id) {
|
||||
li.classList.add('active');
|
||||
} else {
|
||||
li.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
window.addEventListener('scroll', highlight, { passive: true });
|
||||
highlight();
|
||||
}
|
||||
|
||||
function init() {
|
||||
initModal();
|
||||
initDragSort();
|
||||
initSave();
|
||||
// Defer so section-navs.js can own TOC first.
|
||||
window.setTimeout(initFallbackToc, 50);
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user