Files
lsky-pro-docker/test
mole404 5151eff2e2 feat(menu): 点菜单外只关菜单(不再点穿)+ 触摸端二级菜单点击展开 + 滚动/缩放/Esc 关闭
老师在 iPhone 上提的两个交互问题,根因都在上游 context-js.js 里:

1) 点菜单之外会点穿:上游关闭逻辑只在 document 的冒泡阶段 fadeOut,不 preventDefault
   也不 stopPropagation → 点别的图片时"关菜单"和"开预览"同时发生(Viewer.js 的 click
   挂在图片网格上);手机上想关菜单只能去点地址栏,而地址栏又窄又难点。
2) 手机上二级菜单一点就没:二级菜单是 CSS :hover 驱动的,手机没光标 —— 点"复制链接"
   时浏览器补发的合成 mouseenter 让子菜单闪一下,紧接着那发 click 撞上关闭逻辑,整个
   菜单消失 → 复制链接里的 Url/Html/BBCode/Markdown 在手机上根本点不到。

改法(只在 overlay/context-js.js 里,不动主题 CSS):
- 菜单守卫装到 document 的捕获阶段:菜单打开时点菜单外 → 关菜单 + preventDefault +
  stopPropagation,事件永远到不了页面处理器;菜单内部点击照旧放行(复制/重命名/删除不变)。
- 手机上"手指按下"就收起来(更跟手),随后补发的 click 用 700ms 时间窗吞掉;
  拦截只打在 click 上、不拦 touchstart → 滚动/缩放不受影响。
- 刚打开的保护窗 MENU_OPEN_GRACE=800ms,只对"紧随一次触摸的 click"生效(lastTouchAt),
  鼠标点击不受影响:长按抬手补发的那发 click 只吞掉、不关菜单(否则菜单刚开就自己关)。
- 触摸设备(matchMedia('(hover: none)'))点带二级菜单的父项 → 加 touch-open 类 + 注入与
  hover 等价的 CSS,点击展开/收起、同层只留一个;有鼠标的设备继续 hover(桌面零改动)。
- 滚动/旋转缩放(含 #images-scroll 内部滚动)、Esc 关菜单;菜单自身滚动不误关。
- 新增 context.isMenuOpen()(上游没有)供测试与诊断观测菜单状态。

测试:test/longpress.test.mjs 25 → 56 个用例,覆盖上述每条分支,含三条回归
(抬手 click 不能关掉刚开的菜单 / 点菜单外必须吞点击 / 菜单关后点击要恢复正常)。
CI 新增 Behavior tests (jsdom) 步骤(构建前跑,npm ci + npm test),
镜像自证新增 installMenuGuard / MENU_OPEN_GRACE / touch-open / isMenuOpen 标记断言
与"Apache 实际吐出的 JS md5"端到端断言。

配套:blade 版本串 ios-longpress2 → ios-longpress3(击穿 Safari 启发式缓存)、
所有哈希断言同步(Dockerfile 自证 2 + .code-revision + workflow env)、
patches/ios-longpress.patch 重新生成(已 git apply --check 验证能干净打回上游并逐字节一致)、
README/test-README 文档更新、新增 .gitignore 与 test/package-lock.json。
2026-09-28 21:29:45 +08:00
..

补丁行为测试(node + jsdom)

没有 iPhone 也能先把补丁的每条分支跑一遍:用真实的 DOM + jQuery 事件复刻 resources/views/user/images.blade.php 的用法,验证

  1. iOS 长按:只在 iOS 生效、其它平台长按相关代码一行都不执行;
  2. 菜单收放:点菜单之外只关菜单、不点穿到页面(点别的图片不再顺手开预览),滚动/缩放/Esc 关闭;
  3. 触摸端二级菜单:点击展开/收起,不再"闪一下就整个菜单消失";有鼠标的设备继续走 hover。

运行

cd test
npm install
npm test                       # 测 ../overlay/context-js.js
node longpress.test.mjs /path/to/other/context-js.js   # 测指定的文件(例如未打补丁的原版)

覆盖到的用例

  • iPhone Safari(长按):长按 ≥250ms 弹出图床自己的菜单;内容包含"图片操作/复制链接";beforeOpen / afterOpen 回调照旧被调用并拿到元素与菜单节点;菜单用触摸坐标定位;注入 -webkit-touch-callout 抑制样式;抬手补发的 click 被吞掉(不会误开图片预览);且那一发 click 不会把刚打开的菜单关掉(回归:菜单"刚开就自己关"的陷阱)
  • iPhone Safari(菜单收放):菜单开着时点别的图片 → 只关菜单、不开预览;菜单关掉后正常点击照旧生效(不会一直吞点击)
  • iPhone Safari(二级菜单):长按菜单后点"复制链接" → 二级菜单展开、整个菜单不关闭、父项默认行为被阻止、没有点穿到页面;再点一次收起(菜单仍开着);点二级菜单里的叶子项(模拟 ClipboardJS 的 .copy)→ 复制逻辑照旧执行、菜单关闭;注入了与 hover 等价的触摸展开样式
  • 取消条件:手指移动 >10px(滚动/框选)、多指触摸、短按(<250ms 抬手)都不触发菜单;按住 160ms 抬手不出现、320ms 抬手出现(钉住 250ms 这个分界线)
  • 菜单收放(Windows):点图片(菜单外)→ 菜单关闭 + 页面收不到这发 click(预览没开)+ 没有冒泡到 document;菜单内部项点击照旧执行并关闭菜单;页面滚动 / 窗口缩放 / Esc / 内部滚动容器(#images-scroll)滚动都关菜单;菜单自身滚动不误关
  • 有鼠标的设备:即使 UA 是 Windows,只要 matchMedia('(hover: none)') 为假,点"复制链接"就不会展开(继续 hover),保持"点菜单项即关闭"的老行为
  • iPad 请求桌面网站(UA 伪装成 Mac、maxTouchPoints=5):仍走长按分支
  • Mac(含苹果 M 系列)(MacIntel 但 maxTouchPoints=0):不触发长按,右键 contextmenu 照旧
  • Android Chrome:长按不触发新分支、不注入 iOS 样式,contextmenu 照旧打开菜单且阻止浏览器默认菜单;长按抬手补发的 click 不会关掉刚打开的菜单;真手指点别处 → 只关菜单、不开预览
  • Windows 桌面:只有右键路径,连续右键仍能开菜单(去重窗口不误伤)

已知局限

  • jsdom 没有 matchMedia,代码里有能力检测兜底(没有 matchMedia 时退回 'ontouchstart' in window);测试里"有鼠标的设备"那条是显式 stub 出来的
  • jsdom 没有排版引擎(元素高度恒为 0,fadeIn/fadeOut 也不产生可见的 opacity/display 变化),所以只校验菜单的 left 与数值型 top;菜单开合状态用补丁新增的 context.isMenuOpen() 观测
  • 原生 callout 是否真的被压掉、touchend 的 preventDefault 是否真的省掉了原生 click、matchMedia 在真机上的实际取值,这些属于浏览器行为,必须真机验证(本测试只能证明代码路径与事件处理逻辑正确)