Aether主题 开发备忘录 - modal.js 用法与回帖框Bug修复血泪史
Tillreetree 9小时前

modal.js 用法

view/vendor/modal.js 包含两套弹窗组件的全局函数:Modal 和 Offcanvas。

Modal(模态框)

基于 Pico.css 的模态框实现,使用 <dialog> 风格的 <article> 元素。

打开模态框:

openModal(modalElement)
  • 参数:DOM 元素(不是 ID)
  • 效果:添加 modal-is-openmodal-is-opening class,设置焦点陷阱,400ms 动画后聚焦第一个输入框

关闭模态框:

closeModal(modalElement)
  • 参数:DOM 元素(不是 ID)
  • 效果:添加 modal-is-closing class,400ms 动画后移除所有状态 class,恢复焦点到触发元素

切换模态框:

<button data-target="modal-id" onclick="toggleModal(event)">打开</button>

自动行为:

  • 点击模态框外部(<article> 以外)自动关闭
  • 按 Esc 键自动关闭
  • Tab 键焦点陷阱(循环聚焦模态框内的可交互元素)

Offcanvas(底部滑出面板)

自定义实现,不依赖 Bootstrap JS。用于回帖框等需要从底部滑出的面板。

打开面板:

showOffcanvas(id)
  • 参数:元素 ID 字符串,如 'S5_post_reply_modal'
  • 效果:添加 aether-offcanvas-show class,300ms 后聚焦第一个可交互元素

关闭面板:

hideOffcanvas(id)
  • 参数:元素 ID 字符串
  • 效果:移除 aether-offcanvas-show class,恢复焦点到触发元素

HTML 结构:

<aside class="aether-offcanvas aether-offcanvas-bottom" id="your_id">
    <div class="aether-offcanvas-backdrop" onclick="hideOffcanvas('your_id')"></div>
    <div class="aether-offcanvas-header">...</div>
    <div class="aether-offcanvas-body">...</div>
</aside>

触发方式:

<button onclick="showOffcanvas('your_id')">打开</button>
<button onclick="hideOffcanvas('your_id')">关闭</button>

关闭方式(只有这三种,不会意外关闭):

  1. 点击关闭按钮 → onclick="hideOffcanvas(...)"
  2. 点击遮罩层 → onclick="hideOffcanvas(...)"
  3. 代码调用 → hideOffcanvas('S5_post_reply_modal')

CSS 类名:

  • .aether-offcanvas — 基础样式(fixed 定位、transform 隐藏)
  • .aether-offcanvas-show — 显示状态(transform 归零)
  • .aether-offcanvas-backdrop — 遮罩层(仅 show 时可见)
  • body.aether-offcanvas-open — 阻止背景滚动

关键设计:不挂任何 resize / viewport 侦听器。 这不是偷懒,这是刻意为之——见下文。


回帖框 Bug 修复血泪史

问题描述

手机端,帖子详情页,点击回帖框后,输入法弹出,回帖框会自动关闭。用户无法输入任何内容。

就这么一个看似简单的问题,从 A8 版本开始,一直修到 B5 版本,跨越了将近四个月。

第一次修复(A8)

A8 版本的更新记录写着"再次修复了帖子详情回帖框点击后会收起的问题"。

怎么修的呢?我已经记不太清了,大概是给 Bootstrap Offcanvas 加了一些事件拦截之类的。反正修了,但没完全修好。

第二次修复(A9)

A9 版本的更新记录写着"再次修复了帖子详情回帖框点击后会收起的问题,但这次是点击回帖框外面的地方完全不会收起"。

注意这个措辞——"但这次是点击回帖框外面的地方完全不会收起"。这其实是一种妥协:既然无法区分"输入法导致的关闭"和"用户点击外部导致的关闭",那就干脆把"点击外部关闭"也禁用了。

这当然不是理想状态,但至少用户能打字了。

第三次修复(B3)

B3 版本的更新记录写着"修复:再次修复了帖子详情回帖框点击后会收起的问题,这次是专为TinyMCE编辑器设计的修复"。

这次我写了一个完整的"Offcanvas 抑制功能"(initOffcanvasSuppression),大概 100 行代码,做了这些事:

  1. 监听 resize 事件,检测虚拟键盘弹出/收起(通过判断高度变化是否超过 150px)
  2. 监听 hide.bs.offcanvas 事件,如果检测到键盘刚刚弹出(300ms 内),就 event.preventDefault() 阻止关闭
  3. 监听关闭按钮和背景点击,设置 allowHide 标志来区分"用户主动关闭"和"意外关闭"

听起来很完美对吧?但实际测试发现:完全没生效

为什么?因为 Bootstrap Offcanvas 内部的关闭逻辑不是通过 hide.bs.offcanvas 事件触发的——或者说,当 viewport 变化时,Bootstrap 的关闭走的是另一条代码路径,根本不经过可以被 preventDefault 拦截的事件。

就像你精心在门上装了一个门闩,但小偷是从窗户进来的。

第四次修复(B5)—— 彻底重写

在 B3 的修复被证实无效后,我意识到一个残酷的事实:

Bootstrap Offcanvas 的设计哲学与移动端输入法场景根本不兼容。

Bootstrap Offcanvas 的核心假设是:当 viewport 大小变化时,offcanvas 应该重新评估自己的状态。这在桌面端是合理的(窗口缩放时调整布局),但在移动端,输入法弹出导致的 viewport 变化完全不是用户意图,Bootstrap 无法区分这两者。

而且,如果你激进地解决这个问题(比如禁用所有 viewport 相关的监听),所有合理的关闭方式也会失效——因为 Bootstrap 内部的关闭机制和 viewport 监听是耦合在一起的。

所以,唯一的出路是:另起炉灶

方案设计

参考了项目中已有的 modal.js(基于 Pico.css 的模态框实现),它的设计哲学非常简洁:

  • 用 class 控制显示/隐藏
  • 用全局函数控制开关
  • 不挂任何自动侦听器(除了 Esc 键和点击外部)

我把同样的哲学应用到 Offcanvas 上:

  • 隐藏状态:transform: translateY(100%)(面板在视口下方)
  • 显示状态:添加 .aether-offcanvas-showtransform: translateY(0)
  • 不挂任何 resize / viewport 侦听器
  • 只通过 onclick 显式控制

就这么简单。没有 100 行的"抑制功能",没有 event.preventDefault(),没有 allowHide 标志,没有 300ms 的时间窗口判断。

两个函数,一个 class,完事。

function showOffcanvas(id) {
  var el = document.getElementById(id);
  if (!el) return;
  el.classList.add('aether-offcanvas-show');
  document.body.classList.add('aether-offcanvas-open');
}

function hideOffcanvas(id) {
  var el = document.getElementById(id);
  if (!el) return;
  el.classList.remove('aether-offcanvas-show');
  document.body.classList.remove('aether-offcanvas-open');
}

输入法弹出?viewport 变化?关我什么事,我又没在监听。

教训

  1. 当你发现自己在给一个框架打越来越多的补丁时,应该考虑换框架(或自己写)。从 A8 到 B3,我一直在 Bootstrap Offcanvas 的框架内修修补补,每次都觉得"这次应该能行了",每次都不行。直到 B5 彻底抛弃它,问题才真正解决。

  2. 简单性是可靠性前提。自定义 Offcanvas 的代码量不到 Bootstrap Offcanvas 的 1/10,但它不会出这个 bug,因为它根本就没有出 bug 的机会——它不监听 viewport 变化,所以 viewport 变化不会触发任何行为。

  3. "不做什么"比"做什么"更重要。这个 bug 的根因不是缺少了什么功能,而是多了一个不该有的行为(viewport 变化时自动关闭)。解决方案不是添加更多的逻辑来"抑制"这个行为,而是从一开始就不让这个行为存在。


写于 2026年5月12日。从A8到B5,四个月,四次修复,终于画上了句号。

最新回复 (0)
全部楼主
返回