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:
@@ -0,0 +1,325 @@
|
||||
/*!
|
||||
* 图片灯箱样式
|
||||
* Yongquan Yang <yangyq.net>
|
||||
*
|
||||
* ── 配色为什么做成两套 ──────────────────────────────────────
|
||||
* 深色底适合看照片:周围全黑时人眼对图里颜色的判断更准,也不会
|
||||
* 被白底反射光干扰。浅色底适合看截图和文档图:黑底会让白底截图的
|
||||
* 边缘发灰、像蒙了一层雾,而且深色底下一张 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;
|
||||
/* <a> 默认是 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
/*!
|
||||
* 图片灯箱(正文图片点击放大、多图轮播、键盘切换)
|
||||
* Yongquan Yang <yangyq.net>
|
||||
*
|
||||
* ── 设计原则:没有 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 // 当前展示的 <img>
|
||||
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 做比较,
|
||||
而且 <img src> 用绝对地址能避免相对路径在覆盖层里解析错。 */
|
||||
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 = '<svg viewBox="0 0 24 24" width="26" height="26" aria-hidden="true">'
|
||||
+ '<path d="M15 4 L7 12 L15 20" fill="none" stroke="currentColor" '
|
||||
+ 'stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>'
|
||||
|
||||
btnNext = el('button', 'yang-lb-nav yang-lb-next')
|
||||
btnNext.setAttribute('type', 'button')
|
||||
btnNext.setAttribute('aria-label', CFG.next || '下一张')
|
||||
btnNext.innerHTML = '<svg viewBox="0 0 24 24" width="26" height="26" aria-hidden="true">'
|
||||
+ '<path d="M9 4 L17 12 L9 20" fill="none" stroke="currentColor" '
|
||||
+ 'stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>'
|
||||
|
||||
var btnClose = el('button', 'yang-lb-close')
|
||||
btnClose.setAttribute('type', 'button')
|
||||
btnClose.setAttribute('aria-label', CFG.close || '关闭')
|
||||
btnClose.innerHTML = '<svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">'
|
||||
+ '<path d="M5 5 L19 19 M19 5 L5 19" fill="none" stroke="currentColor" '
|
||||
+ 'stroke-width="2" stroke-linecap="round"/></svg>'
|
||||
|
||||
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 }
|
||||
}
|
||||
})()
|
||||
Reference in New Issue
Block a user