update at 2026-03-04 13:25:26
This commit is contained in:
1
openclaw
Submodule
1
openclaw
Submodule
Submodule openclaw added at b10f438221
2
pxterm/find_css.cjs
Normal file
2
pxterm/find_css.cjs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
console.log(fs.readFileSync('node_modules/xterm/css/xterm.css', 'utf8').includes('xterm-cursor-blink'));
|
||||||
@@ -36,3 +36,30 @@
|
|||||||
1. **完美原生选区**:长按仍能顺畅唤起 iOS 文本放大镜与蓝色选框。
|
1. **完美原生选区**:长按仍能顺畅唤起 iOS 文本放大镜与蓝色选框。
|
||||||
2. **不断流的跟手滚动**:只要没有进入选区模式,手指在密集的字符网格上狂滑也不会触发浏览器原生手势劫持,配合 60fps 的 JS 帧动画达到了极致丝滑。
|
2. **不断流的跟手滚动**:只要没有进入选区模式,手指在密集的字符网格上狂滑也不会触发浏览器原生手势劫持,配合 60fps 的 JS 帧动画达到了极致丝滑。
|
||||||
3. **软键盘完整兼容**:没有使用全局焦点的强行剥夺,不阻碍日常的点击输入与键盘显隐。
|
3. **软键盘完整兼容**:没有使用全局焦点的强行剥夺,不阻碍日常的点击输入与键盘显隐。
|
||||||
|
## 5. 最后的幽灵 Bug:触控目标为 SPAN 时滑动冻结 (The Phantom Bug: "SPAN" Target Blocking)
|
||||||
|
尽管我们实现了精密的动态拦截,但在实际使用中偶尔仍会触发严重的“屏幕冻结”。
|
||||||
|
通过全局侦听所有触摸和指针事件,我们抓到了极其关键的日志:
|
||||||
|
**当手指正常滑动时**:目标(Target) 是每一行的 `DIV`,`touchmove` 和 `pointermove` 都能成对、连续触发,我们的 JS 动量接管运转完美。
|
||||||
|
**当发生冻结时**:目标(Target) 变成了渲染终端彩色字符的内联元素 `SPAN`,紧接着**所有的 `touchmove` 事件就会离奇消失**,只剩下 `pointermove` 在孤零零地触发。
|
||||||
|
|
||||||
|
### 为什么碰到 `SPAN` 就不动了?
|
||||||
|
这源于开启了 `-webkit-user-select: text` 后,iOS Safari / WebKit 独有的**文本触摸启发式逻辑 (Text Interaction Heuristic)**:
|
||||||
|
当手指触摸在一个内联文本节点(如 `SPAN`)并且试图开始移动时,iOS 极大概率会将其判定为“光标拖拽”或“准备框选”。为了不打断系统的文本交互,iOS 会在非常底层的级别**没收(Swallow)后续所有的 `touchmove` 事件**。由于 `touchmove` 消失了,我们在 Vue 中原本用于拦截默认行为和计算 `dy` 滑动的代码根本没有执行的机会。于是表现为彻底断流结冰。
|
||||||
|
|
||||||
|
### 解决方案:视觉层穿透 (CSS Pointer-Events)
|
||||||
|
我们不能取消文本选择,否则长按复制功能就没了。但其实用户长按选中某行字,并不需要精确点击到那个细小的 `SPAN`,只要点击到包围它的块级元素 `DIV` 即可(iOS 支持依靠容器进行坐标选取)。
|
||||||
|
|
||||||
|
因此,最有效的解法是对所有终端内容层的 `SPAN` 施加“触控隐身”:
|
||||||
|
```css
|
||||||
|
/* 规避 iOS 触摸事件吞噬问题:
|
||||||
|
当目标是 span 等特定内联元素时,iOS 容易在滑动中没收 touchmove。
|
||||||
|
将其鼠标事件穿透到 div (行) 上可以绕过引发吞没的 WebKit 内核特定文本触摸启发式逻辑
|
||||||
|
同时并不影响基于坐标的文本选择。 */
|
||||||
|
.terminal-container.native-touch-selection .xterm-screen span {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**影响评估:**
|
||||||
|
- **滑动修复**:用户手指摸到的永远只有块级的 `DIV`(行),iOS 将其视为普通面板,从而持续派发我们亟需的 `touchmove`。彻底解决偶发冻结问题。
|
||||||
|
- **选区保持**:包围层 `.xterm-rows > div` 仍具有 `user-select: text`,系统原生长按动作(包括双端拖拽把手)依靠父级空间坐标寻找文本,完全不受影响。
|
||||||
|
- **xterm 功能**:xterm.js 其内部功能(如链接点击)通过捕捉整层的视口相对坐标映射到网格系来处理,也不受丢失底层 `SPAN` `target` 的影响。
|
||||||
|
|||||||
@@ -186,6 +186,8 @@ let asciiFallbackTimer: number | null = null;
|
|||||||
let asciiFallbackText = "";
|
let asciiFallbackText = "";
|
||||||
let compositionGuardTimer: number | null = null;
|
let compositionGuardTimer: number | null = null;
|
||||||
let compositionStartedAt = 0;
|
let compositionStartedAt = 0;
|
||||||
|
let cursorBlinkTimer: number | null = null;
|
||||||
|
let isCursorVisible = true;
|
||||||
let lastKeyboardCommittedText = "";
|
let lastKeyboardCommittedText = "";
|
||||||
let lastKeyboardCommittedAt = 0;
|
let lastKeyboardCommittedAt = 0;
|
||||||
let keyboardAssistBridgeText = "";
|
let keyboardAssistBridgeText = "";
|
||||||
@@ -1224,6 +1226,7 @@ function initTerminal(): void {
|
|||||||
* 触屏端依赖 iOS 自身动量;桌面端保留平滑滚动。
|
* 触屏端依赖 iOS 自身动量;桌面端保留平滑滚动。
|
||||||
*/
|
*/
|
||||||
smoothScrollDuration: nativeTouchSelectionEnabled ? 0 : TERMINAL_SMOOTH_SCROLL_DURATION_MS,
|
smoothScrollDuration: nativeTouchSelectionEnabled ? 0 : TERMINAL_SMOOTH_SCROLL_DURATION_MS,
|
||||||
|
cursorBlink: false,
|
||||||
theme: {
|
theme: {
|
||||||
background: settingsStore.settings.shellBgColor,
|
background: settingsStore.settings.shellBgColor,
|
||||||
foreground: settingsStore.settings.shellTextColor,
|
foreground: settingsStore.settings.shellTextColor,
|
||||||
@@ -1249,6 +1252,25 @@ function initTerminal(): void {
|
|||||||
|
|
||||||
terminal.open(containerRef.value);
|
terminal.open(containerRef.value);
|
||||||
|
|
||||||
|
terminal.onKey(({ domEvent }) => {
|
||||||
|
if (domEvent.key === "Enter" && terminal) {
|
||||||
|
terminal.scrollToBottom();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cursorBlinkTimer !== null) {
|
||||||
|
window.clearInterval(cursorBlinkTimer);
|
||||||
|
}
|
||||||
|
isCursorVisible = true;
|
||||||
|
cursorBlinkTimer = window.setInterval(() => {
|
||||||
|
if (!terminal) return;
|
||||||
|
isCursorVisible = !isCursorVisible;
|
||||||
|
terminal.options.theme = {
|
||||||
|
...terminal.options.theme,
|
||||||
|
cursor: isCursorVisible ? settingsStore.settings.shellAccentColor : "rgba(0,0,0,0)"
|
||||||
|
};
|
||||||
|
}, 1800);
|
||||||
|
|
||||||
terminal.attachCustomKeyEventHandler((event) => {
|
terminal.attachCustomKeyEventHandler((event) => {
|
||||||
if (isCopyShortcut(event) && terminal?.hasSelection()) {
|
if (isCopyShortcut(event) && terminal?.hasSelection()) {
|
||||||
const selected = terminal.getSelection();
|
const selected = terminal.getSelection();
|
||||||
@@ -2002,6 +2024,10 @@ watch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
if (cursorBlinkTimer !== null) {
|
||||||
|
window.clearInterval(cursorBlinkTimer);
|
||||||
|
cursorBlinkTimer = null;
|
||||||
|
}
|
||||||
window.removeEventListener("resize", handleResize);
|
window.removeEventListener("resize", handleResize);
|
||||||
resizeObserver?.disconnect();
|
resizeObserver?.disconnect();
|
||||||
resizeObserver = null;
|
resizeObserver = null;
|
||||||
|
|||||||
@@ -1367,6 +1367,14 @@ select.input {
|
|||||||
-webkit-touch-callout: default;
|
-webkit-touch-callout: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 规避 iOS 触摸事件吞噬问题:
|
||||||
|
当目标是 span 等特定内联元素时,iOS 容易在滑动中没收 touchmove。
|
||||||
|
将其鼠标事件穿透到 div (行) 上可以绕过引发吞没的 WebKit 内核特定文本触摸启发式逻辑
|
||||||
|
同时并不影响基于坐标的文本选择。 */
|
||||||
|
.terminal-container.native-touch-selection .xterm-screen span {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* 移动端字体渲染优化 */
|
/* 移动端字体渲染优化 */
|
||||||
@media (pointer: coarse) {
|
@media (pointer: coarse) {
|
||||||
.terminal-container .xterm {
|
.terminal-container .xterm {
|
||||||
|
|||||||
13
pxterm/test.html
Normal file
13
pxterm/test.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<style>
|
||||||
|
div { user-select: text; -webkit-user-select: text; font-size: 30px;}
|
||||||
|
span { pointer-events: none; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div><span>Hello world with pointer events none</span></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -9,3 +9,9 @@ const DEV_PUBLIC_HOST = "shell.biboer.cn";
|
|||||||
const DEV_CERT_PATH = "/Users/gavin/.acme.sh/shell.biboer.cn_ecc/fullchain.cer";
|
const DEV_CERT_PATH = "/Users/gavin/.acme.sh/shell.biboer.cn_ecc/fullchain.cer";
|
||||||
const DEV_KEY_PATH = "/Users/gavin/.acme.sh/shell.biboer.cn_ecc/shell.biboer.cn.key";
|
const DEV_KEY_PATH = "/Users/gavin/.acme.sh/shell.biboer.cn_ecc/shell.biboer.cn.key";
|
||||||
|
|
||||||
|
|
||||||
|
todo:
|
||||||
|
1. 点语音输入框,弹出第三方语音输入法,按住输入法的语音键,会跳到获取权限,但实际上,在应用中.已经获取过语音输入权限了
|
||||||
|
2. 回车/ctrlc/命令, 光标行复位
|
||||||
|
3. paste 点击不要在弹paste了。
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user