175 lines
6.1 KiB
JavaScript
175 lines
6.1 KiB
JavaScript
/**
|
||||
|
|
* 后台设置页:代码配色实时预览
|
|||
|
|
*
|
|||
|
|
* 做什么:监听「代码配色」下拉框,切换时立刻把预览卡片的配色换掉,
|
|||
|
|
* 不用保存、不用刷新。
|
|||
|
|
*
|
|||
|
|
* 🔴 关键实现:**删掉旧的 <link>、插入新的 <link>**,而不是把所有配色
|
|||
|
|
* CSS 都加载进来再靠 class 切换。
|
|||
|
|
* 原因有两个:
|
|||
|
|
* ① 20 套配色合计约 50KB,且它们的规则是"裸选择器"(.hljs / .hljs-keyword),
|
|||
|
|
* 全部同时在场时会互相压制 —— 后加载的赢,切换看起来像"没变"。
|
|||
|
|
* ② 前台就是只加载选中的一套(见 yang_hl_enqueue)。后台必须用同样的
|
|||
|
|
* 加载方式,才可能得到同样的结果。多套共存时你看到的不一定是前台效果。
|
|||
|
|
*
|
|||
|
|
* 🔴 换完 CSS 必须重新给 token 上色。时序上有个坑:
|
|||
|
|
* <link> 插入后**样式不是立刻可用**的,浏览器要异步加载。
|
|||
|
|
* 直接同步去读 getComputedStyle 拿到的是旧配色。
|
|||
|
|
* 正确做法是等 link.onload 之后再做高亮(或者干脆重新跑一次 hljs)。
|
|||
|
|
*/
|
|||
|
|
(function () {
|
|||
|
|
'use strict';
|
|||
|
|
|
|||
|
|
var cfg = window.YANG_HL_ADMIN || null;
|
|||
|
|
if (!cfg) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var LINK_ID = 'yang-hl-preview-style';
|
|||
|
|
var PAGE_SIZE = 1; /* link 只用一条,避免多条同 id 并存 */
|
|||
|
|
|
|||
|
|
/** 当前预览容器 */
|
|||
|
|
function previewBox() {
|
|||
|
|
return document.querySelector('.yang-hl-preview');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 「代码配色」下拉框 —— 框架给 select 的 id 就是选项 id */
|
|||
|
|
function themeSelect() {
|
|||
|
|
return document.getElementById('a_highlight_theme');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 把预览的配色换成 key 对应的那套
|
|||
|
|
*
|
|||
|
|
* @param {string} key 配色 key,如 'monokai'
|
|||
|
|
* @param {boolean} noRecolor 只换 CSS 不重新上色(初始化时用,避免多余计算)
|
|||
|
|
*/
|
|||
|
|
function applyTheme(key, noRecolor) {
|
|||
|
|
if (!key) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var box = previewBox();
|
|||
|
|
var sel = themeSelect();
|
|||
|
|
|
|||
|
|
/* 先移除旧的 link。必须真正从 DOM 上拿掉,不能只改 href ——
|
|||
|
|
* 改 href 也有效,但连续快速切换时浏览器可能复用已完成的旧请求,
|
|||
|
|
* 出现"切了三次只变了一次"的观感。移除重建最稳。 */
|
|||
|
|
var old = document.getElementById(LINK_ID);
|
|||
|
|
if (old && old.parentNode) {
|
|||
|
|
old.parentNode.removeChild(old);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var link = document.createElement('link');
|
|||
|
|
link.id = LINK_ID;
|
|||
|
|
link.rel = 'stylesheet';
|
|||
|
|
link.type = 'text/css';
|
|||
|
|
link.href = cfg.styleBase + key + '.min.css?v=' + encodeURIComponent(cfg.ver || '');
|
|||
|
|
|
|||
|
|
/* 配色深/浅决定卡片边框、顶栏的颜色(token 颜色由 CSS 自己管)。
|
|||
|
|
* 这个必须在这里切,否则会看到"Monokai 的深色字 + GitHub 的浅色顶栏"。 */
|
|||
|
|
var kind = (key.indexOf('dark') >= 0 || /^(monokai|night-owl|srcery|a11y-dark|tomorrow-night-blue|panda-syntax-dark)$/.test(key))
|
|||
|
|
? 'dark' : 'light';
|
|||
|
|
|
|||
|
|
if (box) {
|
|||
|
|
box.setAttribute('data-theme-key', key);
|
|||
|
|
var card = box.querySelector('.cb');
|
|||
|
|
if (card) {
|
|||
|
|
card.className = 'cb hl-' + key;
|
|||
|
|
card.setAttribute('data-theme', key);
|
|||
|
|
card.setAttribute('data-kind', kind);
|
|||
|
|
}
|
|||
|
|
var nameEl = box.querySelector('[data-theme-name]');
|
|||
|
|
if (nameEl && sel) {
|
|||
|
|
var opt = sel.options[sel.selectedIndex];
|
|||
|
|
if (opt) {
|
|||
|
|
/* 选项文案形如「Monokai(深色)」,预览里只显示前半段 */
|
|||
|
|
nameEl.textContent = String(opt.textContent || '').replace(/([^)]*)\s*$/, '').trim();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 等 CSS 真正生效后再重新上色。
|
|||
|
|
* 用 onload 而不是 setTimeout:网络快时不用白等,慢时也不会提前上色。 */
|
|||
|
|
link.onload = function () {
|
|||
|
|
if (!noRecolor) {
|
|||
|
|
recolor();
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
link.onerror = function () {
|
|||
|
|
/* 加载失败就让预览保持原样,不要清空 —— 清空会让人以为主题坏了 */
|
|||
|
|
if (window.console && console.warn) {
|
|||
|
|
console.warn('[yang-hl] 配色样式加载失败: ' + link.href);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
document.head.appendChild(link);
|
|||
|
|
|
|||
|
|
/* 兜底:onload 在某些缓存命中场景下不一定触发。
|
|||
|
|
* 这里再排一次微延时重上色,保证最终一定是一致的。
|
|||
|
|
* 两次 recolor 是幂等的(内部先清空再写),不会叠加出问题。 */
|
|||
|
|
setTimeout(function () {
|
|||
|
|
if (!noRecolor) {
|
|||
|
|
recolor();
|
|||
|
|
}
|
|||
|
|
}, 120);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 重新给预览里的代码上色
|
|||
|
|
*
|
|||
|
|
* 为什么不直接用 highlightElement:它会在已经处理过的元素上再处理一次,
|
|||
|
|
* 把已有的 token span 当成纯文本再套一层,颜色会变得很脏。
|
|||
|
|
* 正确做法是**从 textContent 取原文重新 highlight**,把结果整体替换回去。
|
|||
|
|
*
|
|||
|
|
* 🔴 为什么可以直接用 textContent、不需要额外存一份 data-raw:
|
|||
|
|
* hljs 输出的 HTML 转义是**无损**的(实测 392 字符原文,上色后
|
|||
|
|
* textContent 回读 392 字符、逐字符相同),所以每次都能从当前
|
|||
|
|
* DOM 拿到干净的原文,重算是幂等的。多存一份反而是冗余状态,
|
|||
|
|
* 两份一旦不同步就是 bug 源头。
|
|||
|
|
*/
|
|||
|
|
function recolor() {
|
|||
|
|
var code = document.querySelector('.yang-hl-preview code[data-sample]');
|
|||
|
|
if (!code || !window.hljs) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
var raw = code.textContent;
|
|||
|
|
var lang = code.getAttribute('data-sample') || 'php';
|
|||
|
|
var res;
|
|||
|
|
try {
|
|||
|
|
res = window.hljs.highlight(raw, { language: lang, ignoreIllegals: true });
|
|||
|
|
} catch (e) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
code.innerHTML = res.value;
|
|||
|
|
code.className = 'hljs language-' + lang;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** 初始化 */
|
|||
|
|
function init() {
|
|||
|
|
var sel = themeSelect();
|
|||
|
|
if (!sel) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 初始配色:页面已经由 PHP 输出了选中那套的 CSS(前台样式不在这里加载,
|
|||
|
|
* 所以这里要主动插一次;noRecolor=true 让首次不重复计算) */
|
|||
|
|
applyTheme(sel.value, true);
|
|||
|
|
|
|||
|
|
sel.addEventListener('change', function () {
|
|||
|
|
applyTheme(sel.value, false);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/* 有些场景下框架会程序化改 select.value(比如"恢复默认"),
|
|||
|
|
* change 事件不会触发 ⇒ 监听 click 兜底 */
|
|||
|
|
sel.addEventListener('click', function () {
|
|||
|
|
/* 点击展开下拉不算切换,交给 change 处理 */
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (document.readyState === 'loading') {
|
|||
|
|
document.addEventListener('DOMContentLoaded', init);
|
|||
|
|
} else {
|
|||
|
|
init();
|
|||
|
|
}
|
|||
|
|
})();
|