NewHome/assets/js/article.js
2026-09-06 15:20:48 +08:00

273 lines
11 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.

/* article.js —— 文章库三栏阅读交互
* 功能:三栏(标题栏 / 段落栏 / 正文)宽度拖拽调节、分区栏与段落栏显示/隐藏;
* Markdown 正文 CDN 懒加载渲染、正文图片路径前缀补全、段落目录生成与平滑跳转。
*/
(function (global) {
'use strict';
function $(sel, root) { return (root || document).querySelector(sel); }
function $$(sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); }
var app = $('.js-art-app');
var frame = $('.js-art-frame');
if (!app || !frame) return;
var BODY_MIN = 22; // 正文列最小占比(%
var KEY = 'hp-art-layout';
var MEDIA_SMALL = '(max-width: 860px)';
/* ---------- 布局状态(宽度 / 显隐) ---------- */
function loadState() {
var isList = global.HP_ART_DATA && global.HP_ART_DATA.listMode ? true : false;
var st = { w1: 15, w2: 15, t1: true, t2: !isList }; // 列表页默认收起段落栏
try {
var saved = JSON.parse(localStorage.getItem(KEY) || 'null');
if (saved) {
st.w1 = clampPct(saved.w1, 6, 60);
st.w2 = clampPct(saved.w2, 6, 60);
if (typeof saved.t1 === 'boolean') st.t1 = saved.t1;
if (typeof saved.t2 === 'boolean') st.t2 = saved.t2;
} else if (window.matchMedia && window.matchMedia(MEDIA_SMALL).matches) {
// 窄屏首次进入:默认只显示正文,两栏以浮层形式随时唤出
st.t1 = false;
st.t2 = false;
}
} catch (e) { /* ignore */ }
return st;
}
function saveState(st) {
try { localStorage.setItem(KEY, JSON.stringify(st)); } catch (e) { /* ignore */ }
}
function clampPct(v, min, max) {
v = parseFloat(v);
if (isNaN(v)) return min;
return Math.min(max, Math.max(min, v));
}
var st = loadState();
function isSmall() {
return (window.matchMedia && window.matchMedia(MEDIA_SMALL).matches) ? true : false;
}
function applyLayout() {
frame.style.setProperty('--w-title', st.w1 + '%');
frame.style.setProperty('--w-toc', st.w2 + '%');
var small = isSmall();
var tTitle = $('.js-col-title');
var tToc = $('.js-col-toc');
var gTitle = $('.js-gutter[data-var="--w-title"]');
var gToc = $('.js-gutter[data-var="--w-toc"]');
if (!small) {
if (tTitle) { tTitle.classList.toggle('art-col-hidden', !st.t1); }
if (tToc) { tToc.classList.toggle('art-col-hidden', !st.t2); }
if (gTitle) gTitle.style.display = st.t1 ? '' : 'none';
if (gToc) gToc.style.display = st.t2 ? '' : 'none';
} else {
// 窄屏:默认隐藏,靠开关以浮层方式显示
if (tTitle) tTitle.classList.toggle('art-col-show-mobile', !!st.t1);
if (tToc) tToc.classList.toggle('art-col-show-mobile', !!st.t2);
if (gTitle) gTitle.style.display = 'none';
if (gToc) gToc.style.display = 'none';
}
updateToggleBtns();
}
function setColVisible(col, visible) {
if (col === 'title') st.t1 = visible;
if (col === 'toc') st.t2 = visible;
applyLayout();
saveState(st);
}
function updateToggleBtns() {
$$('.js-art-toggle').forEach(function (b) {
var c = b.getAttribute('data-col');
var visible = (c === 'title') ? st.t1 : st.t2;
b.classList.toggle('active', visible);
var inSmall = isSmall();
b.title = (inSmall
? (visible ? '收起' : '展开') + ((c === 'title') ? '文章标题栏' : '段落目录')
: '显示 / 隐藏' + ((c === 'title') ? '文章标题栏' : '段落目录'));
});
}
function bindToggles() {
document.addEventListener('click', function (ev) {
var b = ev.target && ev.target.closest ? ev.target.closest('.js-art-toggle') : null;
if (!b) return;
var c = b.getAttribute('data-col');
if (c !== 'title' && c !== 'toc') return;
var cur = (c === 'title') ? st.t1 : st.t2;
setColVisible(c, !cur);
});
}
/* ---------- 分隔条拖拽 ---------- */
function bindGutters() {
$$('.js-gutter', frame).forEach(function (g) {
g.addEventListener('mousedown', function (ev) {
ev.preventDefault();
var colVar = g.getAttribute('data-var'); // --w-title / --w-toc
var isTitle = colVar.indexOf('title') !== -1;
var startX = ev.clientX;
var frameW = frame.getBoundingClientRect().width || 1;
var startW = isTitle ? st.w1 : st.w2;
var other = isTitle ? st.w2 : st.w1;
var moved = false;
function onMove(e) {
var dx = e.clientX - startX;
var pct = startW + (dx / frameW) * 100;
var max = 100 - BODY_MIN - other; // 保证正文列至少 BODY_MIN%
pct = clampPct(pct, 6, Math.max(6, max));
if (Math.abs(pct - startW) > 0.2) moved = true;
if (isTitle) { st.w1 = pct; } else { st.w2 = pct; }
frame.style.setProperty(colVar, pct + '%');
g.classList.add('dragging');
}
function onUp() {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
g.classList.remove('dragging');
if (moved) {
if (isTitle) { st.w1 = clampPct(st.w1, 6, Math.max(6, 100 - BODY_MIN - st.w2)); }
else { st.w2 = clampPct(st.w2, 6, Math.max(6, 100 - BODY_MIN - st.w1)); }
saveState(st);
}
}
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
});
}
/* ---------- Markdown 库 CDN 懒加载marked v4 ---------- */
var MARKED_URLS = [
'https://cdn.jsdelivr.net/npm/marked@4.3.0/marked.min.js',
'https://unpkg.com/marked@4.3.0/marked.min.js'
];
var markedPromise = null;
function loadMarked() {
if (global.marked) return Promise.resolve(global.marked);
if (markedPromise) return markedPromise;
markedPromise = new Promise(function (resolve) {
var tryLoad = function (i) {
if (global.marked) { resolve(global.marked); return; }
if (i >= MARKED_URLS.length) { resolve(null); return; }
var s = document.createElement('script');
s.src = MARKED_URLS[i];
s.onload = function () { resolve(global.marked || null); };
s.onerror = function () { tryLoad(i + 1); };
document.head.appendChild(s);
};
tryLoad(0);
});
return markedPromise;
}
/* ---------- 正文渲染与目录 ---------- */
var rawEl = document.getElementById('artRaw');
var mdBox = document.getElementById('artMd');
var tocNav = $('.js-toc-nav');
var tocCol = $('.js-col-toc');
function fixRelativeUrls() {
// 正文图片/链接可能使用 data/articles/img/… 站点相对根路径(相对站点根),
// 页面位于 article/ 子目录,渲染后需补 ../ 前缀才能正确指向 data/articles/img/。
if (!mdBox) return;
var absPrefixes = ['#', 'http:', 'https:', '//', 'mailto:', 'tel:', 'data:', '/', '../'];
function apply(attr) {
mdBox.querySelectorAll('img, a').forEach(function (el) {
var v = el.getAttribute(attr);
if (!v) return;
var t = v.trim();
for (var i = 0; i < absPrefixes.length; i++) {
if (t.indexOf(absPrefixes[i]) === 0) return;
}
// 纯相对路径:视为相对站点根 → 相对当前 article/ 目录需 ../
el.setAttribute(attr, '../' + v);
});
}
apply('src');
apply('href');
}
function renderError() {
var tpl = document.getElementById('artErrTpl');
if (mdBox && tpl) mdBox.innerHTML = tpl.textContent;
}
function buildToc() {
if (!mdBox || !tocNav) return;
var heads = $$('#artMd h1, #artMd h2, #artMd h3, #artMd h4, #artMd h5');
tocNav.innerHTML = '';
if (!heads.length) {
tocNav.innerHTML = '<p class="tip" style="padding:10px">本文暂无段落标题</p>';
return;
}
var frag = document.createDocumentFragment();
heads.forEach(function (h, i) {
var id = 'art-sec-' + i;
h.id = id;
var lv = parseInt(h.tagName.charAt(1), 10) || 1;
var level = Math.min(4, Math.max(1, lv));
var a = document.createElement('a');
a.href = '#' + id;
a.className = 'art-toc-link lv' + level;
a.textContent = h.textContent;
a.addEventListener('click', function (e) {
e.preventDefault();
var target = document.getElementById(id);
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
frag.appendChild(a);
});
tocNav.appendChild(frag);
}
function renderArticle() {
if (!rawEl || !mdBox) {
if (tocNav) tocNav.innerHTML = '<p class="tip" style="padding:10px">选择左侧任意文章后,此处生成段落目录。</p>';
return;
}
loadMarked().then(function (marked) {
if (!marked) { renderError(); return; }
var md = rawEl.value || '';
var html = marked.parse(md, { gfm: true, breaks: false });
mdBox.innerHTML = html;
fixRelativeUrls();
buildToc();
// 高亮标题栏当前项
var curId = (global.HP_ART_DATA && global.HP_ART_DATA.curId) ? global.HP_ART_DATA.curId : 0;
if (curId) {
var link = $('.js-art-nav a[href="?id=' + curId + '"]');
if (link) {
$$('.js-art-nav a').forEach(function (a) { a.classList.remove('active'); });
link.classList.add('active');
}
}
});
}
/* ---------- 初始化 ---------- */
function init() {
applyLayout();
bindToggles();
bindGutters();
renderArticle();
// 窗口尺寸变化(跨断点时重新应用显隐策略)
var resizeTimer = null;
window.addEventListener('resize', function () {
if (resizeTimer) clearTimeout(resizeTimer);
resizeTimer = setTimeout(function () { applyLayout(); }, 120);
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})(window);