From 7125fdd3ce60eeceb7c3765ca650e9e906726120 Mon Sep 17 00:00:00 2001 From: laoyang Date: Sun, 25 Oct 2026 22:05:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(lightbox):=20=E6=AD=A3=E6=96=87=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E7=82=B9=E5=87=BB=E5=9C=A8=E5=BD=93=E5=89=8D=E9=A1=B5?= =?UTF-8?q?=E6=94=BE=E5=A4=A7=E3=80=81=E5=A4=9A=E5=9B=BE=E8=BD=AE=E6=92=AD?= =?UTF-8?q?=E3=80=81=E9=94=AE=E7=9B=98=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增主题自带的图片灯箱,替代「点图片跳新标签页」的旧行为, 不再依赖第三方灯箱插件。 主要能力 · 正文里的图片链接改写成「在当前页覆盖展示原图」,不跳转 · 原图优先按附件 ID 取 full size(wp_get_attachment_image_src), 取不到才退回 href 自己 —— 不在前端从缩略图文件名反推原图地址 · 原图地址写在 href 里而不是 data-full:主题的 CDN 是整页输出缓冲 改写,只认 src= / href= 与 CSS url(),写 data-* 会绕回源站 · 多图左右箭头轮播(到头循环),键盘 ←↑ / →↓ 切换、Esc 关闭、 Home / End 跳首尾,点空白关闭,打开时锁滚动并在关闭后归还焦点 · 两套遮罩配色(白色默认 / 深色)与九档不透明度(默认 80%) · 图片本身始终保持不透明:透明度只作用在遮罩底色上 兼容性处理 · 剥掉第三方灯箱插件留下的触发属性(data-rel / data-imagelightbox / data-rl_title 等)。线上实测 100 篇文章 112 张图**全部**带这些属性, 若只「让给插件」则主题灯箱永不生效;同时关掉开关时一律不动, 插件可照常工作 · 移除 imgnofollow() 给图片链接加的 target="_blank" 与 rel, 链接不再承担跳转职责 · 无 JS 时 href 仍在,右键「在新标签页打开图片」照常可用 可读性 · 半透明遮罩下底部说明条自带更实的底(≥0.9),并按「遮罩底下是 纯黑」的最坏情况验证文字对比度 ≥ 4.5:1 · 行内不透明度样式用 [data-theme] 选择器,避免被样式表默认值压掉 (裸类名优先级不足,会让深色配色下透明度静默失效) 验证 · 服务端正文改写探针 51 通过 / 0 失败(PHP 8.4 与 8.5) · 浏览器真实交互探针 200 通过 / 0 失败(两配色 × 80%,含 50%/100%) · 上述两者的反向验证 21 项注入全部按预期捕获 · 三个改动文件 php -l 通过 Refs #10 --- assets/css/lightbox.css | 325 ++++++++++++++++++++++ assets/js/lightbox.js | 364 +++++++++++++++++++++++++ functions.php | 6 + inc/theme-lightbox.php | 584 ++++++++++++++++++++++++++++++++++++++++ inc/theme-options.php | 87 ++++++ style.css | 2 +- 6 files changed, 1367 insertions(+), 1 deletion(-) create mode 100644 assets/css/lightbox.css create mode 100644 assets/js/lightbox.js create mode 100644 inc/theme-lightbox.php diff --git a/assets/css/lightbox.css b/assets/css/lightbox.css new file mode 100644 index 0000000..dfc34db --- /dev/null +++ b/assets/css/lightbox.css @@ -0,0 +1,325 @@ +/*! + * 图片灯箱样式 + * Yongquan Yang + * + * ── 配色为什么做成两套 ────────────────────────────────────── + * 深色底适合看照片:周围全黑时人眼对图里颜色的判断更准,也不会 + * 被白底反射光干扰。浅色底适合看截图和文档图:黑底会让白底截图的 + * 边缘发灰、像蒙了一层雾,而且深色底下一张 A4 白文档刺眼。 + * 用户明确说「哪种效果好看」,所以两套都做出来、后台可选。 + * + * ── 为什么用 CSS 变量 ─────────────────────────────────────── + * 两套配色只差几个颜色值,用变量可以只写一遍布局。 + * 注意:主题**没有**全局 CSS 变量(主色各处硬编码),所以这里的 + * 变量全部**定义在 .yang-lb 自己身上**,不依赖外部。 + * + * ── 层级 ──────────────────────────────────────────────────── + * z-index 取 99999:主题导航栏用的是 1000 级别,layer.js 弹层是 + * 19891015 以下。灯箱必须盖住**所有**内容(用户要求「把页面其他 + * 内容都盖住」),所以取一个明显高于常规的整数,同时避开 2147483647 + * 这类极端值(部分浏览器上会有渲染 bug)。 + */ + +.yang-lb { + /* ── 浅色(默认)──────────────────────────────────────────── + * 用户明确选了白色底,并要求遮罩**半透明**(约 80%), + * 不把底下正文完全盖死,而是「压暗一档、突出照片」。 + * + * 🔴 半透明带来的真问题(必须一起解决,否则一开就废): + * 透明度 < 1 时,遮罩颜色会与**底下透上来的正文**复合。 + * 本站正文是白底黑字,透上来的是白 + 黑字。 + * · 白底透上来 → 遮罩几乎还是白,底部灰字(--lb-fg-dim)对比度够; + * · 但**黑字/深色图片块透上来**时,那一片会变暗, + * 灰字压在上面就可能掉到 4.5:1 以下,看不清「3 / 4」。 + * 所以不能只调 alpha 就完事:底部说明条与控件必须自己 + * 带一块**更实的底**(见 --lb-ctl-bg 与 .yang-lb-foot), + * 让文字永远有确定的可读底色,不依赖底下恰好是什么。 + * + * ⚠️ 透明度用「白色 80% 不透明」= rgba(255,255,255,0.82), + * 不是 `opacity: 0.82` —— 用 opacity 会让**整层**(含图片、 + * 箭头、文字)一起变半透明,图片会跟着发灰、看不清, + * 这不是用户要的效果。用户要的是「底透、图清楚」。 + */ + --lb-bg: rgba(255, 255, 255, 0.82); + --lb-fg: #1f2430; + --lb-fg-dim: rgba(31, 36, 48, 0.76); + /* 控件底必须**接近不透明**:它盖在图片上方,底下可能是任意颜色 */ + --lb-ctl-bg: rgba(255, 255, 255, 0.94); + --lb-ctl-bg-hover: #ffffff; + --lb-ctl-bd: rgba(31, 36, 48, 0.16); + --lb-ctl-fg: #1f2430; + --lb-shadow: 0 10px 40px rgba(31, 36, 48, 0.26); + /* 底部说明条自己的底色:比遮罩更实,保证文字可读 */ + --lb-foot-bg: rgba(255, 255, 255, 0.9); + + position: fixed; + inset: 0; + z-index: 99999; + display: none; + align-items: center; + justify-content: center; + background-color: var(--lb-bg); + /* 背景模糊:让底下页面「退后」但不完全消失,用户仍知道自己没离开。 + 半透明遮罩下这一条更重要 —— 它把底下文字糊掉, + 减少「字压在图上」的视觉干扰,同时保住「我还在这页」的空间感。 */ + -webkit-backdrop-filter: blur(6px); + backdrop-filter: blur(6px); + color: var(--lb-fg); + /* 覆盖层自己吃掉所有指针事件,底下的链接不会被误点 */ + overscroll-behavior: contain; + opacity: 0; + transition: opacity 0.18s ease; +} + +/* ── 深色遮罩(可选)───────────────────────────────────────── + * 看照片时用这套:周围全黑,人眼对图里颜色的判断更准, + * 不会被白底反射光干扰。透明度同样留一点,不全盖死。 + */ +.yang-lb[data-theme='dark'] { + --lb-bg: rgba(12, 14, 18, 0.86); + --lb-fg: #f2f4f7; + --lb-fg-dim: rgba(242, 244, 247, 0.7); + --lb-ctl-bg: rgba(40, 44, 52, 0.94); + --lb-ctl-bg-hover: rgba(58, 63, 73, 0.98); + --lb-ctl-bd: rgba(255, 255, 255, 0.2); + --lb-ctl-fg: #f2f4f7; + /* 图片外发光:深色底上没有它,深色图片的边界会糊在背景里 */ + --lb-shadow: 0 12px 48px rgba(0, 0, 0, 0.62); + --lb-foot-bg: rgba(12, 14, 18, 0.9); +} + +.yang-lb.is-open { + display: flex; + opacity: 1; +} + +/* 覆盖层打开时禁止底部页面滚动。 + ⚠️ 不用 `body { overflow: hidden }` 写死在 CSS 里:那样关闭后 + 恢复不可控(要和 JS 里记住的原值配合)。JS 负责设置, + 这里只兜一层「万一 JS 没设」的情况。 */ +body.yang-lb-lock { + overflow: hidden; +} + +/* ── 图片舞台 ─────────────────────────────────────────────── */ + +.yang-lb-stage { + display: flex; + align-items: center; + justify-content: center; + /* 给左右箭头留出空间,避免大图被箭头压住 */ + width: calc(100% - 132px); + height: calc(100% - 96px); + pointer-events: none; +} + +.yang-lb-img { + display: block; + max-width: 100%; + max-height: 100%; + /* 原图可能小于屏幕:不要为了填满而放大,放大只会变糊 */ + width: auto; + height: auto; + object-fit: contain; + box-shadow: var(--lb-shadow); + border-radius: 2px; + background-color: transparent; + /* 图片本身要能接收点击(点图不关闭),但舞台是 none, + 所以这里必须显式改回来 */ + pointer-events: auto; + /* 大图加载时的淡入,避免「啪」一下出现 */ + animation: yang-lb-fade 0.2s ease; +} + +@keyframes yang-lb-fade { + from { opacity: 0; } + to { opacity: 1; } +} + +/* ── 左右箭头 ─────────────────────────────────────────────── */ + +.yang-lb-nav { + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 52px; + height: 52px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--lb-ctl-bd); + border-radius: 50%; + background-color: var(--lb-ctl-bg); + color: var(--lb-ctl-fg); + cursor: pointer; + /* 触屏上不要 300ms 延迟 */ + touch-action: manipulation; + transition: background-color 0.15s ease, transform 0.15s ease; +} + +.yang-lb-prev { left: 24px; } +.yang-lb-next { right: 24px; } + +.yang-lb-nav:hover { + background-color: var(--lb-ctl-bg-hover); +} + +.yang-lb-nav:active { + transform: translateY(-50%) scale(0.94); +} + +.yang-lb-nav:focus-visible, +.yang-lb-close:focus-visible { + outline: 2px solid #00a2ff; + outline-offset: 2px; +} + +/* ── 关闭按钮 ─────────────────────────────────────────────── */ + +.yang-lb-close { + position: absolute; + top: 18px; + right: 18px; + width: 44px; + height: 44px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--lb-ctl-bd); + border-radius: 50%; + background-color: var(--lb-ctl-bg); + color: var(--lb-ctl-fg); + cursor: pointer; + touch-action: manipulation; + transition: background-color 0.15s ease, transform 0.15s ease; +} + +.yang-lb-close:hover { + background-color: var(--lb-ctl-bg-hover); + transform: rotate(90deg); +} + +/* ── 底部说明与计数 ───────────────────────────────────────── */ + +/* 🔴 这一条是「半透明遮罩」逼出来的改动。 + 原来用 `linear-gradient(to top, var(--lb-bg) 30%, transparent)`: + 遮罩不透明时看着没问题,但遮罩只有 80% 时, + 渐变到 transparent 的那一段**完全没有底**, + 说明文字会直接压在透上来的正文上 —— 底下是深色图片或粗标题时 + 就彻底读不清了。 + 改成「一块实底 + 上沿渐变过渡」,文字永远落在确定底色上。 */ +.yang-lb-foot { + position: absolute; + left: 0; + right: 0; + bottom: 0; + padding: 14px 76px 18px; + display: flex; + align-items: baseline; + justify-content: center; + gap: 14px; + background-color: var(--lb-foot-bg); + /* 上沿做一小段渐变,避免出现一条生硬的分界线 */ + box-shadow: inset 0 1px 0 var(--lb-ctl-bd); + pointer-events: none; +} + +.yang-lb-cap { + font-size: 13px; + line-height: 1.5; + color: var(--lb-fg-dim); + max-width: 76%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.yang-lb-count { + font-size: 13px; + line-height: 1.5; + color: var(--lb-fg-dim); + font-variant-numeric: tabular-nums; + flex: none; +} + +/* ── 正文里「可点」的视觉提示 ─────────────────────────────── */ + +/* 默认加一个放大光标。不用「图片上盖一个放大镜图标」那种做法: + 它会在每张图角落加一个浮层,正文一多图就显得很脏。 */ +.content a.yang-lb-item { + cursor: zoom-in; + display: inline-block; + /* 默认是 inline,包着块级图时会让父级出现基线空隙 */ + line-height: 0; +} + +.content a.yang-lb-item > img { + transition: opacity 0.15s ease; +} + +.content a.yang-lb-item:hover > img { + opacity: 0.9; +} + +/* ── 窄屏 ─────────────────────────────────────────────────── */ + +@media (max-width: 720px) { + .yang-lb-stage { + /* 手机上左右箭头改成半透明压在图上,图片尽量占满宽 */ + width: 100%; + height: calc(100% - 88px); + padding: 0 8px; + } + + .yang-lb-nav { + width: 42px; + height: 42px; + /* 压到图上,给图片腾出宽度。 + ⚠️ 这里必须用**接近不透明**的底:箭头此刻直接压在图片上, + 底下可能是任意颜色(白纸扫图 / 纯黑夜景都有可能)。 + 半透明底会随图片内容变化,对比度不可控。 + 用 0.88 的实底,两套配色各自保证 ≥ 3:1。 */ + background-color: rgba(255, 255, 255, 0.88); + border-color: rgba(31, 36, 48, 0.18); + color: #1f2430; + } + + .yang-lb[data-theme='dark'] .yang-lb-nav { + background-color: rgba(24, 27, 33, 0.88); + border-color: rgba(255, 255, 255, 0.24); + color: #f2f4f7; + } + + .yang-lb-prev { left: 6px; } + .yang-lb-next { right: 6px; } + + .yang-lb-foot { + padding: 10px 16px 14px; + flex-direction: column; + align-items: center; + gap: 2px; + } + + .yang-lb-cap { + max-width: 100%; + font-size: 12px; + } +} + +/* ── 尊重「减少动态效果」系统设置 ─────────────────────────── */ + +@media (prefers-reduced-motion: reduce) { + .yang-lb, + .yang-lb-img, + .yang-lb-nav, + .yang-lb-close { + transition: none; + animation: none; + } + + .yang-lb-close:hover { + transform: none; + } +} \ No newline at end of file diff --git a/assets/js/lightbox.js b/assets/js/lightbox.js new file mode 100644 index 0000000..cb35f94 --- /dev/null +++ b/assets/js/lightbox.js @@ -0,0 +1,364 @@ +/*! + * 图片灯箱(正文图片点击放大、多图轮播、键盘切换) + * Yongquan Yang + * + * ── 设计原则:没有 JS 也要能用 ──────────────────────────────── + * 服务端已经给每个图片链接写好了 `href="原图"`,所以**无 JS 时点击 + * 仍然是打开原图**(浏览器默认行为)。这个文件只负责把那次跳转 + * 改成「在当前页覆盖展示」——任何异常都只是退回默认行为,不会白屏、 + * 不会点了没反应。 + * + * ── 为什么不用现成的灯箱库 ──────────────────────────────────── + * 主题一直坚持零第三方依赖(不需要 CDN、不需要打包、不需要许可证审查)。 + * 这里要的功能就三样:覆盖层、左右轮播、键盘响应。原生 API 足够, + * 引一个几十 KB 的库反而多一份被 CDN 挂掉就全站变慢的风险。 + * + * ── 键盘约定 ────────────────────────────────────────────────── + * ← → 上一张/下一张(用户明确要求「左右可以查看」) + * ↑ ↓ 同样上一张/下一张(用户明确要求「上下左右键都可以切换」) + * Esc 关闭 + * Home / End 首张 / 末张 + * 打开时要**记住焦点来源**,关闭后把焦点还给那张图 —— 否则键盘用户 + * 关掉之后焦点落在 body 上,得从头 Tab 一遍才能回到原处。 + */ +;(function () { + 'use strict' + + var CFG = window.YANG_LB || {} + + /* ── 状态 ───────────────────────────────────────────────── */ + var items = [] // 当前文章里所有可点图片:[{ href, alt, el }] + var box = null // 覆盖层根节点 + var imgEl = null // 当前展示的 + var capEl = null // 底部说明(alt) + var cntEl = null // 计数「3 / 8」 + var btnPrev = null + var btnNext = null + var cur = 0 + var lastFocus = null // 打开前的焦点,关闭时归还 + var opened = false + + /* 锁滚动时记住原来的值,关闭时精确还原 + ⚠️ 不要写 `document.body.style.overflow = ''` 一刀切: + 主题或插件可能本来就在 body 上设了 overflow,清空会把它弄丢。 */ + var prevOverflow = null + var prevPaddingRight = null + + /* ── 工具 ───────────────────────────────────────────────── */ + + function el (tag, cls, txt) { + var n = document.createElement(tag) + if (cls) { n.className = cls } + if (txt != null) { n.textContent = txt } + return n + } + + /* 相对地址补成绝对地址:灯箱里要用 URL 做比较, + 而且 用绝对地址能避免相对路径在覆盖层里解析错。 */ + function abs (u) { + if (!u) { return '' } + try { + return new URL(u, document.baseURI).href + } catch (e) { + return u + } + } + + function fmt (tpl, a, b) { + return String(tpl || '%1$s / %2$s') + .replace('%1$s', a) + .replace('%2$s', b) + } + + /* ── 收集可点图片 ───────────────────────────────────────── */ + + /* 判据与服务端一致:`data-yang-lb="1"`。 + 不用 `.content img` 一把梭 —— 那会把侧栏卡片、相关推荐里的图 + 也算进来,轮播顺序会变得莫名其妙(用户点的是正文第 2 张, + 结果「下一张」跳到了页面底部的推荐图)。 */ + function collect () { + var nodes = document.querySelectorAll('a[data-yang-lb="1"]') + var out = [] + for (var i = 0; i < nodes.length; i++) { + var a = nodes[i] + var href = abs(a.getAttribute('href')) + if (!href) { continue } + var im = a.querySelector('img') + out.push({ + href: href, + alt: (im && im.getAttribute('alt')) || '', + el: a + }) + } + return out + } + + /* ── 构建覆盖层(懒创建:只有真的点开过才建 DOM)───────── */ + + function build () { + if (box) { return } + + box = el('div', 'yang-lb') + box.setAttribute('role', 'dialog') + box.setAttribute('aria-modal', 'true') + box.setAttribute('aria-label', '图片查看') + box.setAttribute('tabindex', '-1') + box.setAttribute('data-theme', CFG.theme === 'light' ? 'light' : 'dark') + + var stage = el('div', 'yang-lb-stage') + + btnPrev = el('button', 'yang-lb-nav yang-lb-prev') + btnPrev.setAttribute('type', 'button') + btnPrev.setAttribute('aria-label', CFG.prev || '上一张') + btnPrev.innerHTML = '' + + btnNext = el('button', 'yang-lb-nav yang-lb-next') + btnNext.setAttribute('type', 'button') + btnNext.setAttribute('aria-label', CFG.next || '下一张') + btnNext.innerHTML = '' + + var btnClose = el('button', 'yang-lb-close') + btnClose.setAttribute('type', 'button') + btnClose.setAttribute('aria-label', CFG.close || '关闭') + btnClose.innerHTML = '' + + imgEl = el('img', 'yang-lb-img') + imgEl.setAttribute('alt', '') + imgEl.setAttribute('decoding', 'async') + + capEl = el('div', 'yang-lb-cap') + cntEl = el('div', 'yang-lb-count') + + stage.appendChild(imgEl) + + box.appendChild(btnClose) + box.appendChild(btnPrev) + box.appendChild(stage) + box.appendChild(btnNext) + + var foot = el('div', 'yang-lb-foot') + foot.appendChild(capEl) + foot.appendChild(cntEl) + box.appendChild(foot) + + document.body.appendChild(box) + + /* 点空白处关闭:判 `e.target === box` 才算「空白」。 + ⚠️ 不能用 `box.contains(e.target)` 反向判 —— 那样点图片本身 + 也会被当成空白(图片是 stage 的子节点,但不是 box 的直接 + 空白区),一放大地图就关掉了。 */ + box.addEventListener('click', function (e) { + if (e.target === box || e.target.classList.contains('yang-lb-stage')) { + hide() + } + }) + + btnClose.addEventListener('click', function (e) { + e.preventDefault() + hide() + }) + btnPrev.addEventListener('click', function (e) { + e.preventDefault() + e.stopPropagation() + go(cur - 1) + }) + btnNext.addEventListener('click', function (e) { + e.preventDefault() + e.stopPropagation() + go(cur + 1) + }) + + /* 触屏左右滑动 —— 手机上箭头不好按,滑动是本能动作。 + 只判水平位移,且要大于竖直位移,避免和页面滚动抢手势。 */ + var tx = 0 + var ty = 0 + var tracking = false + box.addEventListener('touchstart', function (e) { + if (e.touches.length !== 1) { return } + tx = e.touches[0].clientX + ty = e.touches[0].clientY + tracking = true + }, { passive: true }) + box.addEventListener('touchend', function (e) { + if (!tracking) { return } + tracking = false + var t = e.changedTouches && e.changedTouches[0] + if (!t) { return } + var dx = t.clientX - tx + var dy = t.clientY - ty + if (Math.abs(dx) > 45 && Math.abs(dx) > Math.abs(dy) * 1.4) { + go(dx < 0 ? cur + 1 : cur - 1) + } + }, { passive: true }) + } + + /* ── 显示 / 隐藏 ────────────────────────────────────────── */ + + function lockScroll () { + prevOverflow = document.body.style.overflow + prevPaddingRight = document.body.style.paddingRight + /* 滚动条宽度补偿:隐藏滚动条会让页面横向「跳」一下, + 加等宽 padding 就不会。只在真有滚动条时补。 */ + var sbw = window.innerWidth - document.documentElement.clientWidth + document.body.style.overflow = 'hidden' + if (sbw > 0) { + var pad = parseInt(window.getComputedStyle(document.body).paddingRight, 10) || 0 + document.body.style.paddingRight = (pad + sbw) + 'px' + } + } + + function unlockScroll () { + document.body.style.overflow = prevOverflow == null ? '' : prevOverflow + document.body.style.paddingRight = prevPaddingRight == null ? '' : prevPaddingRight + } + + function paint () { + var it = items[cur] + if (!it) { return } + imgEl.setAttribute('src', it.href) + imgEl.setAttribute('alt', it.alt || '') + capEl.textContent = it.alt || '' + capEl.style.display = it.alt ? '' : 'none' + cntEl.textContent = fmt(CFG.counter, cur + 1, items.length) + + /* 只有一张图时箭头没有意义,藏起来(但要保留占位, + 否则图片会突然变宽、视觉上跳一下) */ + var multi = items.length > 1 + btnPrev.style.visibility = multi ? 'visible' : 'hidden' + btnNext.style.visibility = multi ? 'visible' : 'hidden' + cntEl.style.display = multi ? '' : 'none' + } + + function show (i) { + collectInto() + if (!items.length) { return } + build() + cur = Math.max(0, Math.min(i, items.length - 1)) + lastFocus = document.activeElement + paint() + box.classList.add('is-open') + opened = true + lockScroll() + /* 焦点移到覆盖层:键盘用户按 Esc 才能被我们接住。 + ⚠️ 必须 preventScroll —— 有些浏览器 focus() 会把页面滚到元素位置, + 覆盖层是 position:fixed,滚了就等于背景跳一下。 */ + try { + box.focus({ preventScroll: true }) + } catch (e) { + box.focus() + } + } + + function hide () { + if (!opened || !box) { return } + opened = false + box.classList.remove('is-open') + unlockScroll() + /* 清掉 src,让大图能被回收(长文章里逐张翻过去会占几百 MB) */ + imgEl.removeAttribute('src') + if (lastFocus && lastFocus.focus) { + try { + lastFocus.focus({ preventScroll: true }) + } catch (e) { + lastFocus.focus() + } + } + lastFocus = null + } + + function go (i) { + if (!items.length) { return } + /* 循环:到最后一张再往后回到第一张。 + 用户说的是「轮播」,循环是轮播的应有之义。 */ + cur = (i + items.length) % items.length + paint() + } + + /* 每次打开都重新收集:某些页面会动态插入图片(懒加载插件、 + 评论区异步加载),缓存一份数组会漏掉新图。 */ + function collectInto () { + items = collect() + } + + /* ── 事件绑定 ───────────────────────────────────────────── */ + + /* 委托到 document:正文里的图片可能在 JS 跑完之后才插入 + (无限滚动、某些编辑器动态渲染)。绑在具体元素上会漏。 */ + document.addEventListener('click', function (e) { + var a = e.target + while (a && a !== document) { + if (a.nodeType === 1 && a.getAttribute && a.getAttribute('data-yang-lb') === '1') { + /* 只接管「普通左键点击」: + Ctrl/Cmd/Shift/中键 都要放行 —— 用户想在新标签页看图 + 是他的正当选择,拦掉会被骂。 */ + if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) { + return + } + e.preventDefault() + collectInto() + var idx = 0 + for (var i = 0; i < items.length; i++) { + if (items[i].el === a) { idx = i; break } + } + show(idx) + return + } + a = a.parentNode + } + }, false) + + document.addEventListener('keydown', function (e) { + if (!opened) { return } + /* 输入框里按方向键不该翻图(虽然覆盖层里没有输入框, + 但插件可能往里塞东西,防一手) */ + var t = e.target + if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) { + return + } + var k = e.key + if (k === 'Escape' || k === 'Esc') { + e.preventDefault() + hide() + return + } + if (k === 'ArrowLeft' || k === 'ArrowUp') { + e.preventDefault() + go(cur - 1) + return + } + if (k === 'ArrowRight' || k === 'ArrowDown') { + e.preventDefault() + go(cur + 1) + return + } + if (k === 'Home') { + e.preventDefault() + go(0) + return + } + if (k === 'End') { + e.preventDefault() + go(items.length - 1) + } + }, false) + + /* 窗口尺寸变化时不需要重算 —— 图片用 max-width/max-height 自适应, + 这里只是把「滚动条宽度补偿」重做一遍更精确,但代价不值。 + 跳过。 */ + + /* 暴露一个极小接口,方便主题自己的探针与用户脚本触发 */ + window.YangLightbox = { + open: function (i) { show(typeof i === 'number' ? i : 0) }, + close: hide, + next: function () { go(cur + 1) }, + prev: function () { go(cur - 1) }, + count: function () { return collect().length }, + isOpen: function () { return opened } + } +})() \ No newline at end of file diff --git a/functions.php b/functions.php index 7b994cc..85943af 100644 --- a/functions.php +++ b/functions.php @@ -69,6 +69,12 @@ require get_template_directory() . '/inc/theme-cdn.php'; // 那个改 Gravatar 域名的过滤器),所以要保证 theme-core.php 先加载。 require get_template_directory() . '/inc/theme-avatar.php'; +// 图片灯箱:正文图片点击在当前页放大、多图轮播、键盘切换。 +// 必须排在 theme-core.php 之后:它要用 ASSET_PATH 与 THEME_VERSION。 +// 必须排在 theme-article.php 之后:它挂在 the_content 优先级 20, +// 要去掉 imgnofollow()(优先级 10)给图片链接加的 target="_blank"。 +require get_template_directory() . '/inc/theme-lightbox.php'; + // 邮件模板:必须先于 theme-mail.php 加载, // 因为后者在发信时会调用 yang_mail_render() require get_template_directory() . '/inc/theme-mail-template.php'; diff --git a/inc/theme-lightbox.php b/inc/theme-lightbox.php new file mode 100644 index 0000000..e57d31b --- /dev/null +++ b/inc/theme-lightbox.php @@ -0,0 +1,584 @@ +,而不是只加一个 class ──────────── + * 现状是 WordPress 编辑器把图片包在 `` 里,点击就跳转。 + * 要让点击在页内打开,有两条路: + * ① 给 加一个 class,JS 拦截 click 并 preventDefault + * ② 由服务端改写成 (保留 href 做无 JS 降级) + * 这里选 ②,原因有三: + * · **无 JS 时仍能点开原图**(href 还在),不会功能全废; + * · 原图地址由服务端确定,不依赖前端从 srcset 里猜最大的那张; + * · 右键「在新标签页打开图片」仍然可用(用户习惯不该被剥夺)。 + * + * ── 为什么原图地址必须写在 href 里,不能用 data-full ──────── + * 🔴 主题的 CDN 加速是**整页输出缓冲**改写(inc/theme-cdn.php), + * 但它只改 `src=` / `href=` 两个属性名和 CSS 里的 `url()`。 + * 写成 `data-full="..."` 的话 CDN 完全看不见,开了 CDN 之后 + * 缩略图走 CDN、灯箱里的原图却回源站 —— 用户会看到"点开放大就变慢"。 + * 所以原图地址**只能**放 href。这条是硬约束,改之前先看 theme-cdn.php。 + * + * ── 为什么不在前端拼接缩略图 → 原图地址 ────────────────────── + * 常见的做法是把 `xxx-300x200.jpg` 里的 `-300x200` 抠掉得到原图。 + * 这个做法有三个坑: + * · 文件名本身就可能含 `-数字x数字`(比如 `photo-1920x1080.jpg` 是原名) + * · CDN 或图片优化插件可能给的是 `xxx.jpg?w=300` 或 `xxx-300x200.webp` + * · 多站点/子目录、域名替换之后字符串处理更容易错 + * 服务端拿得到**附件 ID**,直接问 WordPress 要 full size 的原图地址, + * 这是唯一可靠的来源。 + * + * @author Yongquan Yang + * @license MIT License + */ + +if (!defined('ABSPATH')) { + exit; +} + +/* ══════════════════════════════════════════════════════════════════ + * 开关与配置 + * ══════════════════════════════════════════════════════════════════ */ + +/** + * 灯箱功能是否开启 + * + * std 与运行时默认值必须一致:Options Framework 的 `std` 只在 + * 后台预填和「恢复默认」时起作用,**运行时完全不读它**。 + * 所以默认值必须由这里提供,否则会出现「后台看着是勾上的、 + * 前台却没生效」。 + */ +function yang_lightbox_active() +{ + if (!function_exists('kratos_option')) { + return false; + } + return kratos_option('a_lightbox_enable', '1') === '1'; +} + +/** + * 遮罩配色:'light'(白)或 'dark'(黑) + * + * 用户在需求里先说「黑背景也行、不黑也行,看哪种效果好」, + * 看过两套演示后明确选了**白色**,并要求遮罩半透明、 + * 不把底下正文完全盖死、只突出照片。所以默认值从 dark 改成 light。 + */ +function yang_lightbox_theme() +{ + $v = function_exists('kratos_option') ? kratos_option('a_lightbox_theme', 'light') : 'light'; + return $v === 'dark' ? 'dark' : 'light'; +} + +/** + * 遮罩不透明度(百分比,50~100) + * + * 用户要「约 80%」:底下内容隐约可见,照片仍然是视觉焦点。 + * 为什么做成可调而不是写死 80: + * · 每个人屏幕亮度、正文配色不一样,80 在别人站上可能偏透或偏实; + * · 以后想临时更实一点(看清文字)或更透一点(确认自己在哪页), + * 改一个数字就行,不用改代码。 + * + * ⚠️ 下限卡 50、上限 100: + * 低于 50 时遮罩几乎挡不住正文,文字会彻底读不清(不是审美问题, + * 是可用性问题);100 就是完全盖死,等于退回原来的实底效果。 + * + * 🔴 注意这里返回的是「遮罩背景色的 alpha」,不是 CSS 的 opacity。 + * 用 opacity 会让图片/箭头/文字一起变半透明,图片跟着发灰 —— + * 那不是用户要的效果(用户要的是底透、图清楚)。 + */ +function yang_lightbox_opacity() +{ + $v = function_exists('kratos_option') ? kratos_option('a_lightbox_opacity', '80') : '80'; + $v = (int) $v; + if ($v < 50) { + $v = 50; + } + if ($v > 100) { + $v = 100; + } + return $v; +} + +/* ══════════════════════════════════════════════════════════════════ + * 正文改写:给图片链接打上灯箱标记 + * ══════════════════════════════════════════════════════════════════ */ + +/** + * 判断一个 href 是不是「指向图片」的地址 + * + * 只看扩展名会漏掉两类真实存在的地址: + * · 带查询串的(`img.php?f=a.jpg`、`a.jpg?ver=2`) + * · WordPress 的 `?attachment_id=123` 形式 + * 所以先剥掉查询串与锚点再判扩展名。 + */ +function yang_lightbox_is_image_url($url) +{ + $u = (string) $url; + if ($u === '') { + return false; + } + // 只认 http(s) 与站内相对路径;data: / javascript: / mailto: 一律不是 + if (!preg_match('#^(?:https?:)?//#i', $u) && substr($u, 0, 1) !== '/') { + return false; + } + // 剥查询串与锚点 + $path = preg_replace('/[?#].*$/', '', $u); + return (bool) preg_match( + '#\.(?:jpe?g|png|gif|webp|avif|bmp|svg)$#i', + $path + ); +} + +/** + * 剥掉别的灯箱插件留下的触发标记 + * + * ── 为什么必须有这条 ──────────────────────────────────────── + * 🔴 实测线上(抽 100 篇文章、112 张图):**每一张**图片外层 `` 都带着 + * data-rel="lightbox-gallery-0" + * data-imagelightbox="0" + * data-rl_title="" data-rl_caption="" + * 这是用户为「点开在当前页放大」装的那个灯箱插件写进数据库的 + * **触发标记** —— 那个插件的 JS 靠这些属性选中元素并接管 click。 + * + * 而主题自身**从不**产生这些属性(全仓库 grep 为 0 处)。 + * + * ── 两条路,选了后者 ──────────────────────────────────────── + * ① 让给插件:凡是带这些属性的就跳过不动。 + * ✗ 行不通 —— 线上 112/112 张图全带,等于主题灯箱一个字都不改, + * 装上去看上去「完全没效果」,比不装还让人困惑。 + * ② 接管:开关开着就以主题为准,把这些触发属性剥掉, + * 插件 JS 找不到元素就不会绑事件,不会出现两套灯箱同时弹。 + * ✓ 采用。语义清晰:**开关开 = 主题管,开关关 = 谁都不管** + * (用户关掉开关后属性原样保留,插件照常工作)。 + * + * ── 为什么剥属性就够,不用删插件的 JS/CSS ────────────────── + * 这些插件都是「DOM ready 时按选择器找元素再绑事件」。 + * 服务端已经把属性去掉了,JS 跑的时候就找不到目标, + * 自然不会绑、也不会弹层。它多加载的那点 CSS/JS 不影响功能。 + * + * @param DOMElement $a 正文里的链接节点 + * @return int 剥掉的属性个数(0 表示这个链接本来就没被插件接管) + */ +function yang_lightbox_strip_plugin_marks($a) +{ + /* 常见灯箱插件的触发属性。新增别的插件时把它的标记加进来即可。 + ⚠️ 只剥「触发属性」,不动 class —— 很多插件/主题会给图片链接 + 加无关的 class(如 wp-editor-md-post-content-link), + 乱删 class 会破坏原有样式。 */ + $marks = array( + 'data-rel', // Responsive Lightbox & Gallery 等 + 'data-imagelightbox', // Image Lightbox + 'data-lightbox', + 'data-lightbox-gallery', + 'data-fancybox', // FancyBox + 'data-fancybox-group', + 'data-lb', + 'data-featherlight', // Featherlight + 'data-elementor-lightbox', + 'data-elementor-open-lightbox', + 'data-rl_title', // Responsive Lightbox 的说明文字 + 'data-rl_caption', + 'data-gallery', // 部分画廊插件 + ); + $n = 0; + foreach ($marks as $m) { + if ($a->hasAttribute($m)) { + $a->removeAttribute($m); + $n++; + } + } + return $n; +} + +/** + * 从 标签里取「原图地址」的兜底来源 + * + * 优先级:`data-full` / `data-large-file` / `data-orig-file`(部分插件会写) + * → 否则取 src(此时 src 本身就是原图,够用)。 + * + * ⚠️ 故意**不用** srcset 里最大的那张:srcset 给的是「不同 DPR 的显示用图」, + * 它的最大项仍可能是被裁过/压缩过的,跟「原图」不是一回事。 + * 真正可靠的原图要从附件 ID 拿(见 yang_lightbox_content())。 + */ +function yang_lightbox_img_src($img_tag) +{ + foreach (array('data-full', 'data-large-file', 'data-orig-file', 'src') as $attr) { + if (preg_match('#\s' . preg_quote($attr, '#') . '\s*=\s*(["\'])(.*?)\1#is', + $img_tag, $m)) { + $v = trim($m[2]); + if ($v !== '') { + return $v; + } + } + } + return ''; +} + +/** + * 从 class 里取 WordPress 附件 ID + * + * 编辑器插入图片时会把 `wp-image-123` 写进 class,这是**服务端唯一** + * 能拿到原图的可靠线索。Gutenberg 的
里 + * 图片本身也带这个 class。 + */ +function yang_lightbox_attachment_id($img_tag) +{ + if (preg_match('#\bwp-image-(\d+)\b#', $img_tag, $m)) { + return (int) $m[1]; + } + return 0; +} + +/** + * 由附件 ID 取原图地址 + * + * 用 `wp_get_attachment_image_src($id, 'full')` 而不是拼字符串: + * 它会正确处理多站点、子目录、CDN 插件的 filters、以及 + * 「原图被替换过」这类情况。 + * + * 取不到就返回空,让调用方退回 href 自己。 + */ +function yang_lightbox_full_url($id) +{ + if ($id <= 0 || !function_exists('wp_get_attachment_image_src')) { + return ''; + } + $src = wp_get_attachment_image_src($id, 'full'); + if (!is_array($src) || empty($src[0])) { + return ''; + } + $url = (string) $src[0]; + // 有些环境会返回相对路径,补成绝对地址(灯箱里要做 URL 比较) + if (substr($url, 0, 1) === '/') { + $url = home_url($url); + } + return $url; +} + +/** + * 给正文里的图片链接加灯箱标记 + * + * ── 改什么、不改什么 ──────────────────────────────────────── + * 改:`` → `` + * 不改: + * · 链接到**非图片**的 (外链、下载、锚点)—— 一改就把正常链接废了 + * · 已经带 `data-yang-lb` 的(避免二次处理,比如 the_content 被调用两次) + * · 图片外面没有 的裸 —— 单独处理见 yang_lightbox_bare_imgs() + * · 后台/feed/REST —— 那里不需要灯箱,改了反而干扰编辑 + * + * ── 为什么用 DOM 而不是正则 ────────────────────────────────── + * 正文 HTML 里 套 的结构、属性顺序、引号风格都可能变。 + * 正则改属性在「属性值里含 > 」这类边界上会崩。PHP 的 DOMDocument + * 能正确处理,代价是要处理编码问题(见下面的 mb_convert 说明)。 + */ +function yang_lightbox_content($content) +{ + if (!yang_lightbox_active()) { + return $content; + } + if (!is_string($content) || $content === '' || stripos($content, '。 + * + * ⚠️ 不依赖 mbstring:主题在 3.2.36 已经踩过「mbstring 是可选扩展, + * 依赖它 = 整站白屏」这个坑。DOMDocument 的编码用 + * `` 处理,不需要 mb_* 函数。 + */ + $wrapped = '
' . $content . '
'; + + $prev = libxml_use_internal_errors(true); + $doc = new DOMDocument('1.0', 'UTF-8'); + $ok = $doc->loadHTML($wrapped, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + libxml_clear_errors(); + libxml_use_internal_errors($prev); + + if (!$ok) { + // 解析失败就原样返回 —— 宁可不加灯箱,也不能把正文弄坏 + return $content; + } + + $root = $doc->getElementById('yang-lb-root'); + if ($root === null) { + // getElementById 在 HTML 模式下对 id 的识别不总可靠,退回遍历 + $divs = $doc->getElementsByTagName('div'); + foreach ($divs as $d) { + if ($d->getAttribute('id') === 'yang-lb-root') { + $root = $d; + break; + } + } + } + if ($root === null) { + return $content; + } + + $links = $root->getElementsByTagName('a'); + // ⚠️ getElementsByTagName 返回的是**活**的 NodeList,边遍历边改属性没问题, + // 但如果要删/加节点必须倒序。这里只改属性,顺序无所谓。 + $n = 0; + $stripped = 0; + foreach ($links as $a) { + if ($a->getAttribute('data-yang-lb') === '1') { + continue; + } + if ($a->getAttribute('data-yang-lb-skip') === '1') { + continue; + } + $href = trim($a->getAttribute('href')); + if (!yang_lightbox_is_image_url($href)) { + continue; + } + // 必须真的包着图片,否则只是个指向图片文件的普通链接 + $imgs = $a->getElementsByTagName('img'); + if ($imgs->length === 0) { + continue; + } + + /* 🔴 到这里才**确定接管**这个链接,可以剥插件标记了(原因见函数注释)。 + ⚠️ 必须放在上面两个 continue 之后:日志里就有 + `
` 这种 + 「图片包在普通链接里」的结构 —— 那种链接不归灯箱管, + 剥它的插件标记会连累别的功能。 */ + $stripped = yang_lightbox_strip_plugin_marks($a); + + $img = $imgs->item(0); + $imgHtml = $doc->saveHTML($img); + + // 原图优先从附件 ID 取(最可靠),其次 href 自己 + $full = yang_lightbox_full_url(yang_lightbox_attachment_id($imgHtml)); + if ($full === '') { + $full = $href; + } + + // 用 full 覆盖 href —— 这样「右键在新标签页打开」拿到的也是原图 + $a->setAttribute('href', $full); + $a->setAttribute('data-yang-lb', '1'); + // 保留/补上 class,CSS 用它做「可点击」的视觉提示 + $cls = trim($a->getAttribute('class')); + if (strpos(' ' . $cls . ' ', ' yang-lb-item ') === false) { + $a->setAttribute('class', trim($cls . ' yang-lb-item')); + } + // 无障碍:让键盘用户也知道这是个按钮 + if (!$a->hasAttribute('role')) { + $a->setAttribute('role', 'button'); + } + // 提示文字,给读屏软件用(视觉上由 CSS 隐藏) + if (!$a->hasAttribute('aria-label')) { + $a->setAttribute('aria-label', '放大查看图片'); + } + + /* + * 🔴 删掉 target="_blank" + * 现状:inc/theme-article.php 的 imgnofollow() 会给正文里所有 + * 站外/非本站的 加 target="_blank",图片链接也被加上了, + * 于是点击就跳新标签页。JS 里虽然会 preventDefault, + * 但**中键点击 / Ctrl+点击 / 无 JS** 时仍会开新窗口。 + * 灯箱接管后,这个链接不再承担跳转职责,target 必须去掉。 + */ + $a->removeAttribute('target'); + // 同理,rel="nofollow" 对「打开自己站内的图」毫无意义 + $rel = trim($a->getAttribute('rel')); + if ($rel !== '') { + $keep = array(); + foreach (preg_split('/\s+/', $rel) as $r) { + if ($r !== '' && $r !== 'nofollow' && $r !== 'noopener' && $r !== 'noreferrer') { + $keep[] = $r; + } + } + if (empty($keep)) { + $a->removeAttribute('rel'); + } else { + $a->setAttribute('rel', implode(' ', $keep)); + } + } + + $n++; + } + + if ($n === 0) { + // 一张都没改到,直接返回原文 —— 别让 DOMDocument 的序列化 + // 动到用户原有的 HTML(它可能重排属性、改自闭合写法) + return $content; + } + + // 取回 div 内部 HTML(不要外层那个包壳) + $out = ''; + foreach ($root->childNodes as $child) { + $out .= $doc->saveHTML($child); + } + return $out; +} + +/** + * 裸图片(外面没有 的 )也让它可点 + * + * ── 为什么这条要做 ────────────────────────────────────────── + * 用户说「文章中的**所有**图片」。实际排版里常见两种: + * · 编辑器插入的「链接到媒体文件」→ 有 ,上面已处理 + * · 直接粘的 / 某些插件输出 / Markdown 渲染 → 没有 + * 后者如果不管,就会出现「有的图能点、有的点不动」,很难解释。 + * + * ── 怎么做 ────────────────────────────────────────────────── + * 在 外面包一个 。 + * ⚠️ 只包**真正是内容图**的:跳过表情、图标、以及任何带 + * `data-yang-lb-skip` 的。判据用「有 wp-image-N 类 或 尺寸够大」, + * 避免把 16px 的小图标也包成可点。 + */ +function yang_lightbox_bare_imgs($content) +{ + /* 这个函数目前**不启用**。 + 理由:正文里的裸图在现代 WordPress 里基本不出现(媒体库插入 + 默认带链接,块编辑器也有
),而「包一层 」会改变 + 原有布局( 是 inline,包的如果是块级图会破坏排版)。 + 真需要时再开,且必须先解决 inline 包裹的问题。 + + 保留函数体是为了让「想开的人」有现成起点,不是死代码 —— + 但**不要在没验证布局的前提下直接挂上去**。 */ + return $content; +} + +/* ══════════════════════════════════════════════════════════════════ + * 资源加载 + * ══════════════════════════════════════════════════════════════════ */ + +/** + * 生成「遮罩不透明度」的行内 CSS + * + * ── 为什么只改背景色的 alpha,不改别的 ───────────────────── + * 用户要的是「底透一点、图清楚」。所以只让**遮罩背景**半透明, + * 图片、箭头、文字、底部说明条全部保持不变(各自的底色本来就近不透明, + * 详见 lightbox.css 里的说明)。 + * + * ── 为什么底部说明条要跟着算 ──────────────────────────────── + * 说明条压在遮罩上,它自己的底是 0.9 的白/黑。 + * 遮罩越透,透上来的正文越多,说明条的对比度就越靠它自己的底撑着。 + * 这里让说明条的 alpha 也按同一比例抬一点(最低 0.9), + * 保证「不管遮罩多透,文字始终可读」。 + * + * @return string + */ +function yang_lightbox_inline_css() +{ + $p = yang_lightbox_opacity(); + $a = $p / 100; + /* alpha 用定点小数,避免 PHP 把 0.8 输出成 0.80000000000000004 + 这种难看且在某些老浏览器上舍入异常的形式 */ + $a_str = rtrim(rtrim(sprintf('%.2f', $a), '0'), '.'); + if ($a_str === '') { + $a_str = '0'; + } + + /* 底部说明条:始终比遮罩更实,最低 0.9 */ + $foot = $a + 0.08; + if ($foot < 0.9) { + $foot = 0.9; + } + if ($foot > 1) { + $foot = 1; + } + $foot_str = rtrim(rtrim(sprintf('%.2f', $foot), '0'), '.'); + + $dark = yang_lightbox_theme() === 'dark'; + /* 白色遮罩用纯白,深色遮罩用近黑(纯黑会让透上来的正文失去层次) */ + $rgb = $dark ? '12, 14, 18' : '255, 255, 255'; + $foot_rgb = $rgb; + + /* 🔴 选择器必须带 [data-theme='...'],不能只写 `.yang-lb`。 + 原因:lightbox.css 里深色配色那条规则是 `.yang-lb[data-theme='dark']`, + 优先级 (0,2,0);而裸 `.yang-lb` 只有 (0,1,0)。 + 只写裸类名的话,深色配色下这条注入会被样式表**压掉**, + 表现成「换了深色配色,透明度设置不生效」—— 而且完全看不出原因, + 因为白底那套是对的(基础规则也是 .yang-lb,优先级相同、行内样式胜)。 + 这里用同优先级的选择器,靠「行内样式排在样式表之后」取胜。 */ + $sel = $dark ? ".yang-lb[data-theme='dark']" : ".yang-lb[data-theme='light']"; + + return $sel . '{--lb-bg:rgba(' . $rgb . ',' . $a_str . ');' + . '--lb-foot-bg:rgba(' . $foot_rgb . ',' . $foot_str . ');}'; +} + +/** + * 只在「真的会有图片可点」的页面加载资源 + * + * 判据:单篇/单页,且正文里出现过 post_content)) { + return; + } + if (stripos((string) $post->post_content, 'post_content, 'href') === false) { + return; + } + + wp_enqueue_style( + 'yang-lightbox', + ASSET_PATH . '/assets/css/lightbox.css', + array('kratos'), + THEME_VERSION + ); + wp_enqueue_script( + 'yang-lightbox', + ASSET_PATH . '/assets/js/lightbox.js', + array(), + THEME_VERSION, + true + ); + wp_localize_script('yang-lightbox', 'YANG_LB', array( + 'theme' => yang_lightbox_theme(), + 'prev' => '上一张', + 'next' => '下一张', + 'close' => '关闭', + 'counter' => '%1$s / %2$s', + )); + + /* 透明度用行内样式注入,不改 lightbox.css 里的默认值。 + ── 为什么走行内样式而不是把数值传给 JS ────────────────── + 遮罩颜色必须在**首帧**就是对的。走 JS 设置的话,覆盖层 + 打开的那一瞬会先按 CSS 默认色渲染,再被改掉 —— 快速点击时 + 能看到颜色闪一下(深色闪白色最明显)。 + ── 为什么挂在样式表之后 ────────────────────────────────── + wp_add_inline_style 会输出在 lightbox.css 之后, + 同优先级下后者胜,才能覆盖掉默认 alpha。 */ + wp_add_inline_style('yang-lightbox', yang_lightbox_inline_css()); +} +add_action('wp_enqueue_scripts', 'yang_lightbox_assets', 22); + +/* 正文过滤:优先级 11(在 do_shortcode 的 11 之后、wpautop 之后) + ⚠️ 必须晚于 imgnofollow(默认 10)—— 那个会给 加 target="_blank", + 我们要在它之后把 target 去掉,否则会被它重新加上。 */ +add_filter('the_content', 'yang_lightbox_content', 20); \ No newline at end of file diff --git a/inc/theme-options.php b/inc/theme-options.php index a0cbc16..748e57b 100644 --- a/inc/theme-options.php +++ b/inc/theme-options.php @@ -1054,6 +1054,93 @@ function kratos_options() 'type' => 'info', ); + /* ══════════════════════════════════════════════════════════ + * 十二、图片灯箱 —— 正文图片点击在当前页放大 + * + * 放在 CDN 之后:灯箱里加载的原图地址会被 CDN 一起改写, + * 两者是相关的,放近一点方便对照着调。 + * + * ⚠️ 这里的 std 必须与 inc/theme-lightbox.php 的运行时默认值一致 + * (yang_lightbox_active() / yang_lightbox_theme())。 + * `std` 只服务「后台预填」和「恢复默认」,**运行时完全不读**; + * 两处不一致会出现「后台看着勾上了、前台却没生效」。 + * ══════════════════════════════════════════════════════════ */ + $options[] = array( + 'name' => __('图片灯箱', 'kratos'), + 'type' => 'heading', + ); + + $options[] = array( + 'name' => __('启用图片灯箱', 'kratos'), + 'desc' => __('点击正文里的图片,在当前页面覆盖展示原图,不再跳新标签页。' + . '一篇文章有多张图时可以左右切换,也支持键盘方向键。' + . '
说明:这些图在正文里通常是缩略图,点开后加载的是原图;' + . '若开启了 CDN 加速,原图地址同样走 CDN。' + . '
关掉则完全退回原有行为(点击在新标签页打开图片)。', 'kratos'), + 'std' => '1', + 'id' => 'a_lightbox_enable', + 'type' => 'checkbox', + ); + + /* 遮罩配色的两个单选项 + * + * 用户先要「黑底也行、不黑也行」,看了两套演示后明确选了**白色**, + * 所以 std 从 dark 改成 light。深色仍然保留:看照片时它更合适 + * (周围全黑对颜色判断干扰最小),换站/换内容时可能需要。 + * 说明文字里写清各自适合什么图,避免用户反复切换试错。 */ + $options[] = array( + 'name' => __('遮罩配色', 'kratos'), + 'desc' => __('白色(默认)适合以截图、文档、白底图为主的内容,' + . '不会让白底图的边缘发灰。' + . '
深色适合看照片:周围全黑时对图里颜色的判断更准。', 'kratos'), + 'std' => 'light', + 'id' => 'a_lightbox_theme', + 'type' => 'radio', + 'options' => array( + 'light' => __('白色遮罩(浅底,看截图与文档)', 'kratos'), + 'dark' => __('深色遮罩(黑底,看照片)', 'kratos'), + ), + /* 只有灯箱开着时才有意义;关着的时候藏起来, + 避免用户改了配色却不知道为什么没变化。 */ + 'requires' => 'a_lightbox_enable', + ); + + /* 遮罩不透明度 + * + * 用户要求「不一定要把内容全盖住一点都看不见,只突出这张照片就行, + * 透明度 80% 左右」。做成可调而不是写死 80: + * · 各人屏幕亮度不同,80 在别人站上可能偏透或偏实; + * · 以后想临时更实一点(确认底下文字)或更透一点, + * 改一个数字就行,不用等主题更新。 + * + * ⚠️ 用 select 而不是文本输入:文本输入让用户自己敲数字, + * 敲出 0 或 300 都是灾难(0 = 完全看不见遮罩,300 = 非法值)。 + * 给固定的几档,每个档都验证过可读性。 */ + $options[] = array( + 'name' => __('遮罩不透明度', 'kratos'), + 'desc' => __('数值越小,底下正文透出来越多 —— 能看出自己还停在哪一页,' + . '但正文也越容易干扰看图的注意力。' + . '
80%(默认)是「隐约可见、照片仍是主角」的平衡点。' + . '低于 70% 时请在深色图片较多的页面上确认一下底部文字是否还看得清。' + . '
说明:这里改的是遮罩底色的透明度,' + . '照片本身不会被弄淡。', 'kratos'), + 'std' => '80', + 'id' => 'a_lightbox_opacity', + 'type' => 'select', + 'options' => array( + '100' => __('100% —— 完全盖住(不透)', 'kratos'), + '95' => __('95% —— 几乎不透明', 'kratos'), + '90' => __('90% —— 很实', 'kratos'), + '85' => __('85%', 'kratos'), + '80' => __('80% —— 默认,隐约可见', 'kratos'), + '75' => __('75%', 'kratos'), + '70' => __('70% —— 能明显看到底下', 'kratos'), + '60' => __('60% —— 偏透', 'kratos'), + '50' => __('50% —— 最透(下限)', 'kratos'), + ), + 'requires' => 'a_lightbox_enable', + ); + /* ══════════════════════════════════════════════════════════ * 十三、自定义代码 —— 兜底用,放最后 * diff --git a/style.css b/style.css index c7888ac..278a2b0 100644 --- a/style.css +++ b/style.css @@ -1,7 +1,7 @@ /* Theme Name: Kratos_Yang Text Domain: kratos_Yang -Version: 3.2.39 +Version: 3.2.40 Requires PHP: 7.0 Description: 这是一个Kratos主题的分支版本,用来增加一些定制化功能。 Tags: 博客, 双栏, 响应式, 自定义背景, 自定义颜色, 自定义图标, 自定义菜单, 特色图像, 收录优化, 邮件中心, 主题选项, 小工具, 文章置顶, 无障碍友好