273 lines
11 KiB
JavaScript
273 lines
11 KiB
JavaScript
/* 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);
|