fix(viewer): 松开关闭切图手势的「闩锁」——这解释了安卓上「纹丝不动 / 大多数切图失败」
真因(真机打点 518 条 + 源码逐行核对 + jsdom 复现三方对齐):
node_modules/viewerjs/src/js/others.js:216-223 change() 里
case ACTION_SWITCH: {
this.action = 'switched'; // ← 先上闩
const absoluteOffsetX = Math.abs(offsetX);
if (absoluteOffsetX > 1 && absoluteOffsetX > Math.abs(offsetY)) {
… this.view(index); // 过了判定才真切图
}
第一次 pointermove 进来就把 this.action 改成字符串 'switched',而外层 switch 没有这个分支
⇒ 之后每一次 pointermove 全部落空:不切图、不平移、不放大 = 用户看到的「纹丝不动」。
判定只看开头那次采样,而真手指刚落下天然带纵向抖动(dy ≥ dx 很常见)⇒ 大多数手势一开始
就被闩死,偶尔开头偏横向才成功 = 「大多数失败、少数成功、无规律」。iOS 上因为触摸路径不同
不触发,官方演示页也没有列表页那套前置条件,所以都正常。
修法(不改 viewer.min.js —— 压缩产物改了没法维护):从外面把闩松开。
手势期间用 document 捕获阶段监听 pointermove,发现 viewer.action 变成了那个闩值,
就恢复成手势开始时 Viewer 自己算出的动作值;后续采样继续参与方向判定(判据看的是最近一次
位移,所以横滑终究会切到)。首次遇到闩值时自动学出「切图动作」的真值,无需硬编码常量。
验证(用 Claude 提供的 jsdom 复现脚本,加/不加本补丁对比):
修复前:starts diagonal dy>dx, then horizontal → no switch ✗
修复后:starts diagonal dy>dx, then horizontal → SWITCHED ✓
对照组:vertical drag (should NOT switch) → 仍 no switch ✓(未被破坏)
其余 7 个用例修复前后都正常。jsdom 9 套 409 项全绿。
This commit is contained in:
1 parent
36245c583b
commit
5f3603658b
4 files changed
+82
-3
No files matched your search
+2
-2
@@ -165,7 +165,7 @@ RUN php -r "file_exists('.env') || copy('.env.example', '.env');" \
|
||||
RUN printf '%s\n' \
|
||||
'c0e513ec8e93fd81b34b3c6de5cf5eb8 ./public/js/context-js/context-js.js' \
|
||||
'c0e513ec8e93fd81b34b3c6de5cf5eb8 ./resources/js/context-js.js' \
|
||||
'b40ba5e968e70e7f6d5c9e00c03e25e1 ./resources/views/user/images.blade.php' \
|
||||
'e0b5b669408e8a5f3dee7f332ad21e97 ./resources/views/user/images.blade.php' \
|
||||
| md5sum -c - \
|
||||
&& grep -q "assetVersion('js/context-js/context-js.js')" ./resources/views/user/images.blade.php \
|
||||
&& grep -q 'isIOSWebKit' ./public/js/context-js/context-js.js \
|
||||
@@ -208,7 +208,7 @@ RUN APP_SRC_MD5=$(find app config routes -type f -print0 2>/dev/null | sort -z |
|
||||
"fork_sha=${FORK_SHA}" \
|
||||
"lsky_commit=${LSKY_COMMIT}" \
|
||||
"context_js_md5=c0e513ec8e93fd81b34b3c6de5cf5eb8" \
|
||||
"images_blade_md5=b40ba5e968e70e7f6d5c9e00c03e25e1" \
|
||||
"images_blade_md5=e0b5b669408e8a5f3dee7f332ad21e97" \
|
||||
"app_src_md5=${APP_SRC_MD5}" \
|
||||
"app_js_md5=${APP_JS_MD5}" \
|
||||
"app_css_md5=${APP_CSS_MD5}" \
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
# 三个文件各自的 md5(与 Dockerfile「自证 2」里的期望值一致):
|
||||
# c0e513ec8e93fd81b34b3c6de5cf5eb8 src/resources/js/context-js.js
|
||||
# c0e513ec8e93fd81b34b3c6de5cf5eb8 src/public/js/context-js/context-js.js
|
||||
# b40ba5e968e70e7f6d5c9e00c03e25e1 src/resources/views/user/images.blade.php
|
||||
# e0b5b669408e8a5f3dee7f332ad21e97 src/resources/views/user/images.blade.php
|
||||
#
|
||||
# 上游原文件 md5(改动前,供比对):
|
||||
# c8e57f6232848ca8277341ddf1a3a7a6 resources/js/context-js.js
|
||||
|
||||
@@ -535,6 +535,42 @@ class="relative inline-flex h-4 w-7 shrink-0 items-center rounded-full bg-brand
|
||||
};
|
||||
const viewer = new Viewer(document.getElementById('images-grid'), {url: 'data-original'});
|
||||
|
||||
/* 修 Viewer.js 的「闩锁」bug(真机打点 + 源码逐行核对 + jsdom 复现确认):
|
||||
* others.js:216-223 change() 里
|
||||
* case ACTION_SWITCH: {
|
||||
* this.action = 'switched'; // ← 第一次 pointermove 就先上闩
|
||||
* if (|offsetX| > 1 && |offsetX| > |offsetY|) { … this.view(index) }
|
||||
* 上闩之后 action 成了字符串 'switched',而外层 switch 没有这个分支 ⇒ 之后
|
||||
* 每一次 pointermove 全部落空:不切图、不平移、不放大 =「纹丝不动」。
|
||||
* 判据又只看开头那次采样,真手指刚落下天然带纵向抖动(dy ≥ dx)⇒ 大多数手势
|
||||
* 一开始就被闩死,偶尔开头偏横向才成功 —— 这就是「大多数失败、少数成功、无规律」。
|
||||
* 从外面把闩松开:手势期间发现 action 变成那个闩值,就恢复成刚才那个动作值,
|
||||
* 后续采样继续参与判定(切图判据看的是"最近一次位移",所以横滑一定能切到)。
|
||||
* 不改 viewer.min.js(压缩产物,改了没法维护)。 */
|
||||
(function () {
|
||||
let inViewer = false;
|
||||
let lastAction = null;
|
||||
let switchAction = null;
|
||||
const isViewer = (el) => el instanceof Element && !!el.closest('.viewer-container');
|
||||
document.addEventListener('pointerdown', (e) => {
|
||||
inViewer = isViewer(e.target);
|
||||
lastAction = null;
|
||||
}, true);
|
||||
document.addEventListener('pointermove', (e) => {
|
||||
if (!inViewer) return;
|
||||
const a = viewer.action;
|
||||
if (a === 'switched') {
|
||||
// 学到切图动作的真值后,把闩松开
|
||||
if (!switchAction && lastAction) switchAction = lastAction;
|
||||
if (switchAction && viewer.action !== switchAction) viewer.action = switchAction;
|
||||
} else if (a) {
|
||||
lastAction = a; // 手势开始时 Viewer 刚算出来的动作值
|
||||
}
|
||||
}, true);
|
||||
document.addEventListener('pointerup', () => { inViewer = false; }, true);
|
||||
document.addEventListener('pointercancel', () => { inViewer = false; }, true);
|
||||
})();
|
||||
|
||||
/* 安卓 Chromium:看图控件自己有"模拟双击"逻辑(viewerjs handlers.js:424-451)——
|
||||
* 两次落在**图片上**的抬手间隔 <500ms,它就 setTimeout 后自己派发一个 dblclick,
|
||||
* 于是放大。真机打点证实:老师连续滑图时凭空出现 dblclick×23 ⇒ 一放大,图片就不再
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
const {JSDOM}=require('jsdom');
|
||||
const fs=require('fs');
|
||||
function run(file, moves, label){
|
||||
const dom=new JSDOM('<!doctype html><body><ul id=g><li><img src="a.jpg" data-original="a.jpg"></li><li><img src="b.jpg" data-original="b.jpg"></li><li><img src="c.jpg" data-original="c.jpg"></li></ul></body>',{runScripts:'outside-only',pretendToBeVisual:true});
|
||||
const w=dom.window;
|
||||
w.ontouchstart=null;
|
||||
if(!w.PointerEvent){ w.PointerEvent=class extends w.MouseEvent{constructor(t,i={}){super(t,i);this.pointerId=i.pointerId||1;this.pointerType=i.pointerType||'touch';}}; }
|
||||
w.eval(fs.readFileSync(file,'utf8'));
|
||||
const v=new w.Viewer(w.document.getElementById('g'),{transition:false});
|
||||
v.show();
|
||||
// fake a loaded, fitted image
|
||||
v.viewed=true; v.viewing=false; v.showing=false;
|
||||
v.imageData={x:0,y:0,width:300,height:200}; v.viewerData={width:360,height:640};
|
||||
const idx0=v.index;
|
||||
const ev=(t,x,y)=>new w.PointerEvent(t,{bubbles:true,cancelable:true,pointerId:7,pointerType:'touch',clientX:x,clientY:y,view:w});
|
||||
const mk=(t,x,y)=>{const e=ev(t,x,y);Object.defineProperty(e,'pageX',{value:x});Object.defineProperty(e,'pageY',{value:y});return e;};
|
||||
const canvas=v.canvas;
|
||||
// ==== 阿罗娜的"松闩"补丁(从外面松开 Viewer 的 'switched' 闩锁)====
|
||||
let inV=false,lastA=null,swA=null;
|
||||
w.document.addEventListener('pointerdown',(e)=>{ inV=!!e.target.closest('.viewer-container'); lastA=null; },true);
|
||||
w.document.addEventListener('pointermove',()=>{
|
||||
if(!inV) return;
|
||||
const a=v.action;
|
||||
if(a==='switched'){ if(!swA&&lastA) swA=lastA; if(swA&&v.action!==swA) v.action=swA; }
|
||||
else if(a){ lastA=a; }
|
||||
},true);
|
||||
w.document.addEventListener('pointerup',()=>{ inV=false; },true);
|
||||
// ==== 补丁结束 ====
|
||||
canvas.dispatchEvent(mk('pointerdown',300,400));
|
||||
for(const [x,y] of moves) w.document.dispatchEvent(mk('pointermove',x,y));
|
||||
w.document.dispatchEvent(mk('pointerup',moves[moves.length-1][0],moves[moves.length-1][1]));
|
||||
console.log(label.padEnd(58),'index',idx0,'->',v.index, v.index!==idx0?'SWITCHED':'no switch');
|
||||
}
|
||||
const f=process.argv[2];
|
||||
run(f,[[250,402],[200,405],[150,410]],'first move big & horizontal');
|
||||
run(f,[[300,401],[250,402],[200,405],[150,410]],'first move dx=0,dy=1 (tiny, vertical noise)');
|
||||
run(f,[[299,402],[250,402],[200,405],[150,410]],'first move dx=-1,dy=2');
|
||||
run(f,[[298,403],[250,402],[200,405],[150,410]],'first move dx=-2,dy=3 (dy>dx)');
|
||||
run(f,[[296,401],[250,402],[200,405],[150,410]],'first move dx=-4,dy=1 (ok)');
|
||||
run(f,[[300,401],[300,402],[299,402],[250,402],[150,410]],'jitter 3 samples then swipe');
|
||||
run(f,[[300,420],[300,450],[301,500]],'vertical drag (should NOT switch)');
|
||||
run(f,[[299,405],[298,412],[250,415],[150,418]],'starts diagonal dy>dx, then horizontal');
|
||||
run(f,[[330,401],[360,403],[400,405]],'swipe right (should go prev -> stays 0 at index 0 / no loop)');
|
||||
Reference in new issue
Block a user