feat(lightbox): 正文图片点击在当前页放大、多图轮播、键盘切换

新增主题自带的图片灯箱,替代「点图片跳新标签页」的旧行为,
不再依赖第三方灯箱插件。

主要能力
· 正文里的图片链接改写成「在当前页覆盖展示原图」,不跳转
· 原图优先按附件 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
This commit is contained in:
2026-10-25 22:05:24 +08:00
parent 203721b1fe
commit 7125fdd3ce
6 changed files with 1367 additions and 1 deletions
+584
View File
@@ -0,0 +1,584 @@
<?php
/**
* 图片灯箱(正文图片点击放大)
*
* ── 做什么 ────────────────────────────────────────────────────
* 文章正文里的图片多是缩略图,点一下在**当前页面**覆盖展示原图,
* 不再跳新标签页。一篇文章有多张图时左右箭头可轮播,键盘方向键切换。
*
* ── 为什么必须重写 <a href>,而不是只加一个 class ────────────
* 现状是 WordPress 编辑器把图片包在 `<a href="原图">` 里,点击就跳转。
* 要让点击在页内打开,有两条路:
* ① 给 <a> 加一个 class,JS 拦截 click 并 preventDefault
* ② 由服务端改写成 <a href="原图" class="...">(保留 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 <yangyq.net>
* @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 张图):**每一张**图片外层 `<a>` 都带着
* 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;
}
/**
* 从 <img> 标签里取「原图地址」的兜底来源
*
* 优先级:`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 的 <figure class="wp-block-image"> 里
* 图片本身也带这个 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;
}
/**
* 给正文里的图片链接加灯箱标记
*
* ── 改什么、不改什么 ────────────────────────────────────────
* 改:`<a href="图片"><img ...></a>` → `<a class="yang-lb-item" href="原图" data-yang-lb="1">`
* 不改:
* · 链接到**非图片**的 <a>(外链、下载、锚点)—— 一改就把正常链接废了
* · 已经带 `data-yang-lb` 的(避免二次处理,比如 the_content 被调用两次)
* · 图片外面没有 <a> 的裸 <img> —— 单独处理见 yang_lightbox_bare_imgs()
* · 后台/feed/REST —— 那里不需要灯箱,改了反而干扰编辑
*
* ── 为什么用 DOM 而不是正则 ──────────────────────────────────
* 正文 HTML 里 <a> 套 <img> 的结构、属性顺序、引号风格都可能变。
* 正则改属性在「属性值里含 > 」这类边界上会崩。PHP 的 DOMDocument
* 能正确处理,代价是要处理编码问题(见下面的 mb_convert 说明)。
*/
function yang_lightbox_content($content)
{
if (!yang_lightbox_active()) {
return $content;
}
if (!is_string($content) || $content === '' || stripos($content, '<img') === false) {
return $content;
}
// 后台、feed、REST 里不处理
if (is_admin() || (function_exists('is_feed') && is_feed())) {
return $content;
}
/*
* ⚠️ DOMDocument 会把非 ASCII 当成 Latin-1,中文标题/alt 会被转成乱码。
* 标准做法是:处理前声明 UTF-8、处理后按 UTF-8 输出。
* 这里用「加 XML 声明 + 前缀包一层」的方式避免它自动补 <html><body>。
*
* ⚠️ 不依赖 mbstring:主题在 3.2.36 已经踩过「mbstring 是可选扩展,
* 依赖它 = 整站白屏」这个坑。DOMDocument 的编码用
* `<?xml encoding="UTF-8">` 处理,不需要 mb_* 函数。
*/
$wrapped = '<?xml encoding="UTF-8"><div id="yang-lb-root">' . $content . '</div>';
$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 之后:日志里就有
`<a href=".../wp-login.php"><img src=".../avatar.jpg"></a>` 这种
「图片包在普通链接里」的结构 —— 那种链接不归灯箱管,
剥它的插件标记会连累别的功能。 */
$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() 会给正文里所有
* 站外/非本站的 <a> 加 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;
}
/**
* 裸图片(外面没有 <a> 的 <img>)也让它可点
*
* ── 为什么这条要做 ──────────────────────────────────────────
* 用户说「文章中的**所有**图片」。实际排版里常见两种:
* · 编辑器插入的「链接到媒体文件」→ 有 <a>,上面已处理
* · 直接粘的 <img> / 某些插件输出 / Markdown 渲染 → 没有 <a>
* 后者如果不管,就会出现「有的图能点、有的点不动」,很难解释。
*
* ── 怎么做 ──────────────────────────────────────────────────
* 在 <img> 外面包一个 <a class="yang-lb-item" href="原图">。
* ⚠️ 只包**真正是内容图**的:跳过表情、图标、以及任何带
* `data-yang-lb-skip` 的。判据用「有 wp-image-N 类 或 尺寸够大」,
* 避免把 16px 的小图标也包成可点。
*/
function yang_lightbox_bare_imgs($content)
{
/* 这个函数目前**不启用**。
理由:正文里的裸图在现代 WordPress 里基本不出现(媒体库插入
默认带链接,块编辑器也有 <figure>),而「包一层 <a>」会改变
原有布局(<a> 是 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 . ');}';
}
/**
* 只在「真的会有图片可点」的页面加载资源
*
* 判据:单篇/单页,且正文里出现过 <img。
*
* ⚠️ 为什么不用 `is_singular()` 一把梭:列表页也可能有图,但灯箱在
* 列表页体验很差(图是摘要的一部分,点开就离开了浏览节奏)。
* 而且列表页少加载一份 CSS+JS 对首屏有实际好处。
*
* ⚠️ 为什么判 $post 用「是对象且有 post_content」而不是 `instanceof WP_Post`:
* 有些插件会把全局 $post 换成自己的对象(页面构建器、会员插件都干过),
* 那种对象往往也有 post_content。用 instanceof 会把它们一刀切排除,
* 表现成「某些页面灯箱就是不出现」,而且完全看不出原因。
* 这里要的只是「能读到正文」,不需要它是哪个类。
*/
function yang_lightbox_assets()
{
if (!yang_lightbox_active()) {
return;
}
if (!is_singular()) {
return;
}
$post = isset($GLOBALS['post']) ? $GLOBALS['post'] : null;
if (!is_object($post) || !isset($post->post_content)) {
return;
}
if (stripos((string) $post->post_content, '<img') === false) {
return;
}
/* 正文里一张「带链接的图」都没有时也不用加载:
只有裸图的话前端收集不到可点项,加载了也是白搭。 */
if (stripos((string) $post->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)—— 那个会给 <a> 加 target="_blank",
我们要在它之后把 target 去掉,否则会被它重新加上。 */
add_filter('the_content', 'yang_lightbox_content', 20);
+87
View File
@@ -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' => __('点击正文里的图片,<b>在当前页面</b>覆盖展示原图,不再跳新标签页。'
. '一篇文章有多张图时可以左右切换,也支持键盘方向键。'
. '<br><b>说明:</b>这些图在正文里通常是缩略图,点开后加载的是<b>原图</b>;'
. '若开启了 CDN 加速,原图地址同样走 CDN。'
. '<br>关掉则完全退回原有行为(点击在新标签页打开图片)。', 'kratos'),
'std' => '1',
'id' => 'a_lightbox_enable',
'type' => 'checkbox',
);
/* 遮罩配色的两个单选项
*
* 用户先要「黑底也行、不黑也行」,看了两套演示后明确选了**白色**,
* 所以 std 从 dark 改成 light。深色仍然保留:看照片时它更合适
* (周围全黑对颜色判断干扰最小),换站/换内容时可能需要。
* 说明文字里写清各自适合什么图,避免用户反复切换试错。 */
$options[] = array(
'name' => __('遮罩配色', 'kratos'),
'desc' => __('<b>白色</b>(默认)适合以截图、文档、白底图为主的内容,'
. '不会让白底图的边缘发灰。'
. '<br><b>深色</b>适合看照片:周围全黑时对图里颜色的判断更准。', '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' => __('数值越小,底下正文透出来越多 —— 能看出自己还停在哪一页,'
. '但正文也越容易干扰看图的注意力。'
. '<br><b>80%(默认)</b>是「隐约可见、照片仍是主角」的平衡点。'
. '低于 70% 时请在深色图片较多的页面上确认一下底部文字是否还看得清。'
. '<br><b>说明:</b>这里改的是<b>遮罩底色</b>的透明度,'
. '照片本身不会被弄淡。', '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',
);
/* ══════════════════════════════════════════════════════════
* 十三、自定义代码 —— 兜底用,放最后
*