@yfwu2020/dsh-selection-explain 0.10.1 → 0.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -3
- package/lib/client.js +40 -16
- package/package.json +1 -1
- package/scripts/smoke-panel.mjs +79 -1
- package/scripts/test-client.mjs +15 -9
- package/scripts/test-escape-focus.mjs +39 -4
- package/src/client/index.js +40 -16
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ DSH 中的翻译、上下文解读与语音追问。
|
|
|
8
8
|
|
|
9
9
|
[快速安装](#install) · [语音输入](#voice) · [功能一览](#read) · [使用与设置](#settings)
|
|
10
10
|
|
|
11
|
-
`DSH 插件` · `v0.10.
|
|
11
|
+
`DSH 插件` · `v0.10.3` · `MIT`
|
|
12
12
|
|
|
13
13
|
</div>
|
|
14
14
|
|
|
@@ -103,11 +103,11 @@ dsh plugin --profile web add @yfwu2020/dsh-selection-explain
|
|
|
103
103
|
<details>
|
|
104
104
|
<summary>安装 Release 构建包</summary>
|
|
105
105
|
|
|
106
|
-
将下载的 `.tgz` 放到当前目录,以 v0.10.
|
|
106
|
+
将下载的 `.tgz` 放到当前目录,以 v0.10.3 为例:
|
|
107
107
|
|
|
108
108
|
```bash
|
|
109
109
|
mkdir -p ~/.dsh/plugins/dsh-selection-explain
|
|
110
|
-
tar -xzf yfwu2020-dsh-selection-explain-0.10.
|
|
110
|
+
tar -xzf yfwu2020-dsh-selection-explain-0.10.3.tgz -C ~/.dsh/plugins/dsh-selection-explain --strip-components=1
|
|
111
111
|
dsh plugin --profile web add ~/.dsh/plugins/dsh-selection-explain
|
|
112
112
|
```
|
|
113
113
|
|
|
@@ -153,6 +153,16 @@ dsh plugin --profile web remove @yfwu2020/dsh-selection-explain
|
|
|
153
153
|
| 界面 | 语音卡片收尾、侧边栏网页划词与悬浮胶囊 |
|
|
154
154
|
| 数据 | 保留多少条最近解读 |
|
|
155
155
|
|
|
156
|
+
### 与完整视图、最近会话共存(0.10.2)
|
|
157
|
+
|
|
158
|
+
解读小窗使用自己的浮层,显示在最近会话列表和完整视图工作面板上方,空白区域仍可点击原界面。在完整视图里,解读插件自行隐藏悬浮胶囊;返回分栏后恢复。
|
|
159
|
+
|
|
160
|
+
聊天与解读可以同时打开。与 `dsh-full-view` **0.1.6**、`dsh-recent-sessions` **0.2.3** 配合时,`Esc` 先退出最后操作的小窗;点击两窗之外后先退解读,再按一次收起聊天。此次调整需一起更新两个插件,未改变 Harness 的公共浮层或设置。
|
|
161
|
+
|
|
162
|
+
### 快捷键与 Esc 层级(0.10.3)
|
|
163
|
+
|
|
164
|
+
模型菜单收起后不再被 DSH 识别为弹层,避免设置等快捷键失效。官方设置等模态浮层打开时,`Esc` 优先交给官方浮层处理,保留解读小窗、插件设置抽屉、模型菜单和语音卡;官方浮层关闭后,再按 `Esc` 继续退出插件层级。
|
|
165
|
+
|
|
156
166
|
## 数据与使用范围
|
|
157
167
|
|
|
158
168
|
选中文字、必要的前后文,以及开启的会话背景会交给你配置的模型。可在设置中关闭会话背景或工具调用。
|
package/lib/client.js
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* 交互:在页面任意位置选中文字 → 选区末端浮出「解读」按钮 → 点击弹出面板,
|
|
5
5
|
* 面板流式显示两节:① 专业中英翻译 ② 这段文字在当前上下文中的含义详解。
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* 官方 shell.overlay 槽管理挂载生命周期;界面由插件自己挂到 body 的独立根。
|
|
8
|
+
* 根层固定为 70,越过全屏工作面板,不改写宿主公共浮层。空白区域鼠标穿透。
|
|
9
|
+
* 浮标与面板继续用 position:fixed 定位到选区坐标。
|
|
10
10
|
*
|
|
11
11
|
* 本文件是 DSH 客户端 bundle 的源文件(ModuleLoader 懒加载 CJS 表格式):
|
|
12
12
|
* 顶层只注册工厂,真正的副作用(React 组件、DOM/CSS、监听)都在 factory 被
|
|
@@ -435,9 +435,10 @@ window.__ModuleLoader__.load({
|
|
|
435
435
|
// --sel-fill(-fg) 实心块:浮标按钮 / 发送键 / 结论条标签——块上文字另算一套,
|
|
436
436
|
// 否则青底配白字只有 3.6(不达标),浅色下换成深青 5.4、深色下反白 7.4
|
|
437
437
|
// 变量挂在浮层容器上 → 浮标与面板共用;深浅由**面板实际底色**决定(data-theme)。
|
|
438
|
-
'.dsh-sel-layer{
|
|
438
|
+
'.dsh-sel-layer{position:fixed;inset:0;z-index:70;pointer-events:none;--sel-a1:#0d9488;--sel-a1-text:#0f766e;--sel-a2:#0d9488;--sel-a2-text:#0f766e;',
|
|
439
439
|
'--sel-fill:#0f766e;--sel-fill-fg:#ffffff;',
|
|
440
440
|
'--sel-a2-soft:color-mix(in srgb,var(--sel-a2) 12%,transparent);--sel-a1-soft:color-mix(in srgb,var(--sel-a1) 10%,transparent)}',
|
|
441
|
+
'.dsh-sel-layer[data-full-view] .dsh-sel-pill{display:none!important}',
|
|
441
442
|
'.dsh-sel-layer[data-theme=dark]{--sel-a1:#4ecdc4;--sel-a1-text:#6fe3d8;--sel-a2:#4ecdc4;--sel-a2-text:#6fe3d8;',
|
|
442
443
|
'--sel-fill:#4ecdc4;--sel-fill-fg:#052b26;',
|
|
443
444
|
'--sel-a2-soft:color-mix(in srgb,var(--sel-a2) 16%,transparent);--sel-a1-soft:color-mix(in srgb,var(--sel-a1) 16%,transparent)}',
|
|
@@ -2492,9 +2493,10 @@ window.__ModuleLoader__.load({
|
|
|
2492
2493
|
styleEl.textContent = CSS
|
|
2493
2494
|
document.head.appendChild(styleEl)
|
|
2494
2495
|
|
|
2495
|
-
/**
|
|
2496
|
+
/** 自有浮层根:位于 body,公共父层的堆叠上下文不影响它。 */
|
|
2496
2497
|
var layer = el('div', 'dsh-sel-layer')
|
|
2497
2498
|
layer.style.pointerEvents = 'none'
|
|
2499
|
+
layer.setAttribute('data-dsh-floating-owner', 'explanation')
|
|
2498
2500
|
|
|
2499
2501
|
var button = el('button', 'dsh-sel-btn')
|
|
2500
2502
|
button.type = 'button'
|
|
@@ -2646,7 +2648,7 @@ window.__ModuleLoader__.load({
|
|
|
2646
2648
|
modelPill.appendChild(modelPillTier)
|
|
2647
2649
|
modelPill.appendChild(el('span', 'dsh-sel-picker-caret', '▼'))
|
|
2648
2650
|
var modelMenu = el('div', 'dsh-sel-pickermenu')
|
|
2649
|
-
|
|
2651
|
+
// role 随开合同步:DSH 按 [role="menu"] 判定弹层,不检查 CSS 可见性。
|
|
2650
2652
|
|
|
2651
2653
|
// ── 录音行(与主会话 ui-voice-input 同一套结构)──
|
|
2652
2654
|
// [✕ 取消] [实时波形 / 状态文案] [■ 停止 或 行内动作] …… [↑ 发送(原位不动)]
|
|
@@ -3758,10 +3760,27 @@ window.__ModuleLoader__.load({
|
|
|
3758
3760
|
layer.appendChild(historyList)
|
|
3759
3761
|
|
|
3760
3762
|
/**
|
|
3761
|
-
*
|
|
3762
|
-
*
|
|
3763
|
-
* React 不接管它们的子树(容器本身无 children,不会被 diff 清空)。
|
|
3763
|
+
* 官方槽仅保留不可见的生命周期锚点;界面根由本插件挂到 body。
|
|
3764
|
+
* 稳定 ref 避免 React 重渲染时摘挂根节点、丢失输入焦点。
|
|
3764
3765
|
*/
|
|
3766
|
+
function syncFullViewLayer() {
|
|
3767
|
+
var fullView = !!document.querySelector('[data-dsh-full-view]')
|
|
3768
|
+
if (fullView !== layer.hasAttribute('data-full-view')) {
|
|
3769
|
+
if (fullView) layer.setAttribute('data-full-view', '')
|
|
3770
|
+
else layer.removeAttribute('data-full-view')
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
function mountFloatingLayer(node) {
|
|
3774
|
+
if (node) {
|
|
3775
|
+
if (layer.parentNode !== document.body) document.body.appendChild(layer)
|
|
3776
|
+
syncFullViewLayer()
|
|
3777
|
+
} else if (layer.parentNode) layer.parentNode.removeChild(layer)
|
|
3778
|
+
}
|
|
3779
|
+
var fullViewObserver = null
|
|
3780
|
+
if (typeof MutationObserver === 'function') {
|
|
3781
|
+
fullViewObserver = new MutationObserver(syncFullViewLayer)
|
|
3782
|
+
fullViewObserver.observe(document.body, {subtree:true, childList:true, attributes:true, attributeFilter:['data-dsh-full-view']})
|
|
3783
|
+
}
|
|
3765
3784
|
/** 开窗后"还没聚焦过输入框"(见 focusComposerOnce)。 */
|
|
3766
3785
|
var askFocusPending = false
|
|
3767
3786
|
/** 调试:最后一次"没聚焦"卡在哪一步(-1 表示聚焦成功)。 */
|
|
@@ -3780,16 +3799,14 @@ window.__ModuleLoader__.load({
|
|
|
3780
3799
|
},
|
|
3781
3800
|
}, function SelectionExplainLayer() {
|
|
3782
3801
|
return React.createElement('div', {
|
|
3783
|
-
className: 'dsh-sel-
|
|
3784
|
-
style: { pointerEvents: 'none' },
|
|
3785
|
-
ref:
|
|
3786
|
-
if (node && layer.parentNode !== node) node.appendChild(layer)
|
|
3787
|
-
},
|
|
3802
|
+
className: 'dsh-sel-anchor',
|
|
3803
|
+
style: { display: 'none', pointerEvents: 'none' },
|
|
3804
|
+
ref: mountFloatingLayer,
|
|
3788
3805
|
})
|
|
3789
3806
|
})
|
|
3790
3807
|
})
|
|
3791
3808
|
} catch (error) {
|
|
3792
|
-
//
|
|
3809
|
+
// 槽注册失败仅报错,自有界面根保持未挂载,不影响其他插件。
|
|
3793
3810
|
console.error('[dsh-selection-explain] shell.overlay 槽注册失败:', error)
|
|
3794
3811
|
}
|
|
3795
3812
|
} else {
|
|
@@ -9467,6 +9484,7 @@ window.__ModuleLoader__.load({
|
|
|
9467
9484
|
*/
|
|
9468
9485
|
var offVoiceEscape = listen(document, 'keydown', function (event) {
|
|
9469
9486
|
if (event.key !== 'Escape' || !vcardState.open || event.defaultPrevented) return
|
|
9487
|
+
if (document.querySelector('[role="dialog"][aria-modal="true"]')) return
|
|
9470
9488
|
event.preventDefault()
|
|
9471
9489
|
event.stopPropagation()
|
|
9472
9490
|
vcardState.suppress = true
|
|
@@ -9919,11 +9937,13 @@ window.__ModuleLoader__.load({
|
|
|
9919
9937
|
|
|
9920
9938
|
function closeModelMenu() {
|
|
9921
9939
|
modelMenu.setAttribute('data-open', '0')
|
|
9940
|
+
modelMenu.removeAttribute('role')
|
|
9922
9941
|
modelPill.setAttribute('aria-expanded', 'false')
|
|
9923
9942
|
}
|
|
9924
9943
|
|
|
9925
9944
|
function openModelMenu() {
|
|
9926
9945
|
modelMenu.setAttribute('data-open', '1')
|
|
9946
|
+
modelMenu.setAttribute('role', 'menu')
|
|
9927
9947
|
modelPill.setAttribute('aria-expanded', 'true')
|
|
9928
9948
|
renderModelMenu()
|
|
9929
9949
|
loadModelCatalog(false).then(function () {
|
|
@@ -9945,7 +9965,7 @@ window.__ModuleLoader__.load({
|
|
|
9945
9965
|
event.stopPropagation() // 菜单内部点击不要触发"点到外面"
|
|
9946
9966
|
})
|
|
9947
9967
|
var offModelKeys = listen(modelMenu, 'keydown', function (event) {
|
|
9948
|
-
if (event.key === 'Escape') closeModelMenu()
|
|
9968
|
+
if (event.key === 'Escape' && !document.querySelector('[role="dialog"][aria-modal="true"]')) closeModelMenu()
|
|
9949
9969
|
})
|
|
9950
9970
|
|
|
9951
9971
|
// —— 网页模式开关 ——
|
|
@@ -10123,6 +10143,9 @@ window.__ModuleLoader__.load({
|
|
|
10123
10143
|
|
|
10124
10144
|
var offKeyDown = listen(document, 'keydown', function (event) {
|
|
10125
10145
|
if (event.key !== 'Escape' || !panelOpen || event.defaultPrevented || event.isComposing || event.keyCode === 229) return
|
|
10146
|
+
// 官方模态浮层的 Esc 在冒泡阶段处理;capture 不消费,先让官方设置等浮层退出。
|
|
10147
|
+
// 插件面板和设置抽屉是非模态 dialog,不带 aria-modal,不会命中这里。
|
|
10148
|
+
if (document.querySelector('[role="dialog"][aria-modal="true"]')) return
|
|
10126
10149
|
// 完整视图记录最后点击/操作的小窗;不能只看可能停在另一窗的键盘焦点。
|
|
10127
10150
|
var fullViewFrame = document.querySelector('[data-dsh-full-view]')
|
|
10128
10151
|
if (fullViewFrame) {
|
|
@@ -12127,6 +12150,7 @@ window.__ModuleLoader__.load({
|
|
|
12127
12150
|
}
|
|
12128
12151
|
pillObserver = null
|
|
12129
12152
|
}
|
|
12153
|
+
if (fullViewObserver) fullViewObserver.disconnect()
|
|
12130
12154
|
if (typeof offSlot === 'function') offSlot()
|
|
12131
12155
|
// 划词桥:摘观察者与消息监听,并把我们自己建的预览 blob 全部回收
|
|
12132
12156
|
if (bridgeScanId) clearTimeout(bridgeScanId)
|
package/package.json
CHANGED
package/scripts/smoke-panel.mjs
CHANGED
|
@@ -264,7 +264,19 @@ async function main() {
|
|
|
264
264
|
check('插件装载(槽注册 + apply)', !!registration && registration.options.id === '@yfwu2020/dsh-selection-explain', registration && registration.options.id)
|
|
265
265
|
registration.component().props.ref(mount)
|
|
266
266
|
hook = window.__dshSelectionExplain
|
|
267
|
-
check('
|
|
267
|
+
check('解读自有浮层已挂载', !!document.querySelector('.dsh-sel-layer'), '')
|
|
268
|
+
var ownedLayer = document.querySelector('.dsh-sel-layer')
|
|
269
|
+
check('解读使用 body 自有根,公开归属且空白穿透', ownedLayer.parentNode === document.body && ownedLayer.getAttribute('data-dsh-floating-owner') === 'explanation' && getComputedStyle(ownedLayer).pointerEvents === 'none', '')
|
|
270
|
+
check('解读独立根越过完整视图工作面板', Number(getComputedStyle(ownedLayer).zIndex) > 60, getComputedStyle(ownedLayer).zIndex)
|
|
271
|
+
var modeFrame = document.createElement('div')
|
|
272
|
+
document.body.appendChild(modeFrame)
|
|
273
|
+
modeFrame.setAttribute('data-dsh-full-view', '')
|
|
274
|
+
await sleep(40)
|
|
275
|
+
check('解读自身响应完整视图并隐藏胶囊', getComputedStyle(document.querySelector('.dsh-sel-pill')).display === 'none', '')
|
|
276
|
+
modeFrame.removeAttribute('data-dsh-full-view')
|
|
277
|
+
await sleep(40)
|
|
278
|
+
check('返回分栏由解读自身恢复胶囊', getComputedStyle(document.querySelector('.dsh-sel-pill')).display !== 'none', '')
|
|
279
|
+
modeFrame.remove()
|
|
268
280
|
} catch (error) {
|
|
269
281
|
check('插件装载(槽注册 + apply)', false, error && error.message)
|
|
270
282
|
finish()
|
|
@@ -1492,8 +1504,57 @@ async function main() {
|
|
|
1492
1504
|
hook.open('模型菜单勾选', '', '菜单冒烟')
|
|
1493
1505
|
await waitFor(function () { return hook.state().phase === 'done' }, 8000)
|
|
1494
1506
|
await hook.loadModels(true)
|
|
1507
|
+
var modelNodes = hook.modelNodes()
|
|
1508
|
+
// DSH 按属性判定弹层,不检查 CSS 可见性(Issue #1)。
|
|
1509
|
+
var modalSelector = '[role="dialog"][aria-modal="true"], [role="menu"]'
|
|
1510
|
+
function modelMenuOwnsShortcuts() {
|
|
1511
|
+
return Array.from(document.querySelectorAll(modalSelector)).includes(modelNodes.menu)
|
|
1512
|
+
}
|
|
1513
|
+
function checkModelMenuClosed(reason) {
|
|
1514
|
+
check(reason + ':模型菜单不再占用 DSH 快捷键',
|
|
1515
|
+
!modelMenuOwnsShortcuts() && modelNodes.menu.getAttribute('data-open') !== '1' &&
|
|
1516
|
+
modelNodes.pill.getAttribute('aria-expanded') === 'false')
|
|
1517
|
+
}
|
|
1518
|
+
checkModelMenuClosed('初始收起')
|
|
1519
|
+
// 官方 useModalLayer 在 document 冒泡阶段处理 Esc;插件 capture 必须让行。
|
|
1520
|
+
function checkHostEscapeFirst(target, label) {
|
|
1521
|
+
var hostSettings = document.createElement('div')
|
|
1522
|
+
hostSettings.setAttribute('role', 'dialog')
|
|
1523
|
+
hostSettings.setAttribute('aria-modal', 'true')
|
|
1524
|
+
hostSettings.setAttribute('data-shortcut-modal', 'settings')
|
|
1525
|
+
var hostInput = document.createElement('input')
|
|
1526
|
+
hostSettings.appendChild(hostInput)
|
|
1527
|
+
document.body.appendChild(hostSettings)
|
|
1528
|
+
function dismissHost(event) {
|
|
1529
|
+
if (event.key !== 'Escape' || event.defaultPrevented) return
|
|
1530
|
+
event.preventDefault()
|
|
1531
|
+
hostSettings.remove()
|
|
1532
|
+
}
|
|
1533
|
+
document.addEventListener('keydown', dismissHost)
|
|
1534
|
+
try {
|
|
1535
|
+
var event = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true })
|
|
1536
|
+
;(target || hostInput).dispatchEvent(event)
|
|
1537
|
+
check(label + ':Esc 到达官方浮层并只关闭官方设置',
|
|
1538
|
+
event.defaultPrevented && !hostSettings.isConnected && panel.style.display === 'flex')
|
|
1539
|
+
} finally {
|
|
1540
|
+
document.removeEventListener('keydown', dismissHost)
|
|
1541
|
+
hostSettings.remove()
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
var escapeModeFrame = document.createElement('div')
|
|
1545
|
+
escapeModeFrame.setAttribute('data-dsh-full-view', '')
|
|
1546
|
+
escapeModeFrame.setAttribute('data-dsh-active-floating-window', 'explanation')
|
|
1547
|
+
for (var fullViewMode of [false, true]) {
|
|
1548
|
+
if (fullViewMode) document.body.appendChild(escapeModeFrame)
|
|
1549
|
+
var modeLabel = fullViewMode ? '完整视图' : '普通分栏'
|
|
1550
|
+
checkHostEscapeFirst(null, modeLabel + '/焦点在官方设置')
|
|
1551
|
+
checkHostEscapeFirst(document.querySelector('.dsh-sel-askbox'), modeLabel + '/焦点仍在解读输入框')
|
|
1552
|
+
}
|
|
1553
|
+
escapeModeFrame.remove()
|
|
1495
1554
|
hook.openModel()
|
|
1496
1555
|
await sleep(120)
|
|
1556
|
+
check('模型菜单展开时占用 DSH 快捷键',
|
|
1557
|
+
modelMenuOwnsShortcuts() && modelNodes.pill.getAttribute('aria-expanded') === 'true')
|
|
1497
1558
|
var menu = hook.modelNodes().menu
|
|
1498
1559
|
var rows = Array.prototype.slice.call(menu.querySelectorAll('.dsh-sel-pickerrow'))
|
|
1499
1560
|
check('模型菜单列出 3 个模型(含同名 id 的另一 provider)', rows.length === 3, String(rows.length))
|
|
@@ -1517,7 +1578,24 @@ async function main() {
|
|
|
1517
1578
|
var rowBox = onRows[0] ? onRows[0].getBoundingClientRect() : null
|
|
1518
1579
|
check('打勾那一行仍是单行(高度不超过 30px,没折成两行)',
|
|
1519
1580
|
!!rowBox && rowBox.height <= 30, rowBox ? Math.round(rowBox.height) + 'px' : '没找到那一行')
|
|
1581
|
+
|
|
1582
|
+
checkHostEscapeFirst(modelNodes.menu, '模型菜单仍展开')
|
|
1583
|
+
check('官方设置关闭时保留插件模型菜单', modelNodes.menu.getAttribute('data-open') === '1')
|
|
1584
|
+
|
|
1585
|
+
clickOn(modelNodes.pill)
|
|
1586
|
+
checkModelMenuClosed('再次点击胶囊')
|
|
1587
|
+
hook.openModel()
|
|
1588
|
+
modelNodes.menu.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
|
|
1589
|
+
checkModelMenuClosed('按 Esc')
|
|
1590
|
+
hook.openModel()
|
|
1591
|
+
document.getElementById('docp').dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
|
|
1592
|
+
checkModelMenuClosed('点击菜单外部')
|
|
1593
|
+
hook.openModel()
|
|
1594
|
+
clickOn(modelNodes.menu.querySelector('.dsh-sel-pickerrow'))
|
|
1595
|
+
checkModelMenuClosed('选择模型')
|
|
1596
|
+
hook.openModel()
|
|
1520
1597
|
hook.close() // 关窗会顺手收起模型菜单
|
|
1598
|
+
checkModelMenuClosed('关闭解读面板')
|
|
1521
1599
|
await sleep(30)
|
|
1522
1600
|
}
|
|
1523
1601
|
|
package/scripts/test-client.mjs
CHANGED
|
@@ -103,6 +103,9 @@ class FakeEl {
|
|
|
103
103
|
removeAttribute(key) {
|
|
104
104
|
delete this.attrs[key]
|
|
105
105
|
}
|
|
106
|
+
hasAttribute(key) {
|
|
107
|
+
return this.getAttribute(key) !== null
|
|
108
|
+
}
|
|
106
109
|
getAttribute(key) {
|
|
107
110
|
return this.attrs[key] ?? null
|
|
108
111
|
}
|
|
@@ -1242,7 +1245,10 @@ const mount = new FakeEl('div')
|
|
|
1242
1245
|
const element = registration.component()
|
|
1243
1246
|
assert('浮层组件返回 React 元素', !!element && element.type === 'div')
|
|
1244
1247
|
element.props.ref(mount)
|
|
1245
|
-
|
|
1248
|
+
const floatingLayer = body.children.find((c) => c.className === 'dsh-sel-layer')
|
|
1249
|
+
assert('界面挂在 body 自有根,不提升公共槽', !!floatingLayer && floatingLayer.parentNode === body && mount.children.length === 0)
|
|
1250
|
+
assert('自有根声明公开的小窗归属', floatingLayer && floatingLayer.getAttribute('data-dsh-floating-owner') === 'explanation')
|
|
1251
|
+
assert('重渲染使用稳定挂载 ref', registration.component().props.ref === element.props.ref)
|
|
1246
1252
|
|
|
1247
1253
|
// ───────────────────────── 划词 → 浮标 ─────────────────────────
|
|
1248
1254
|
const container = new FakeEl('div')
|
|
@@ -1264,7 +1270,7 @@ const selection = {
|
|
|
1264
1270
|
windowStub.getSelection = () => selection
|
|
1265
1271
|
documentStub.dispatch('mouseup', { target: body })
|
|
1266
1272
|
await new Promise((r) => setTimeout(r, 20))
|
|
1267
|
-
const button =
|
|
1273
|
+
const button = floatingLayer.children.find((c) => c.className === 'dsh-sel-btn')
|
|
1268
1274
|
assert('选中文字后浮标出现', !!button && button.style.display === 'inline-flex', button && `left=${button.style.left} top=${button.style.top}`)
|
|
1269
1275
|
// 浮现动效:只在"隐藏 → 显示"那一次挂动效类;已可见时只平移;animationend 后自动摘掉
|
|
1270
1276
|
{
|
|
@@ -1335,7 +1341,7 @@ assert('选中文字后浮标出现', !!button && button.style.display === 'inli
|
|
|
1335
1341
|
// 引用(❝)的**离线**契约:浮标进了浮层、CSS 规则齐全(真实交互在宿主可达时另有一段)
|
|
1336
1342
|
{
|
|
1337
1343
|
const css = readFileSync(BUNDLE, 'utf8')
|
|
1338
|
-
const quoteBtn = Array.from(walk(
|
|
1344
|
+
const quoteBtn = Array.from(walk(floatingLayer)).find((n) => String(n.className).indexOf('dsh-sel-quotebtn') >= 0)
|
|
1339
1345
|
assert('浮层里有「❝ 引用」浮标,且默认藏着', !!quoteBtn && quoteBtn.style.display === 'none' && textOf(quoteBtn).indexOf('引用') >= 0, quoteBtn ? textOf(quoteBtn) : '未找到')
|
|
1340
1346
|
assert('CSS 契约:引用区默认收起、data-show 控制显隐', /\.dsh-sel-quotes\{display:none;flex-direction:column/.test(css) && /\.dsh-sel-quotes\[data-show="1"\]\{display:flex\}/.test(css), '')
|
|
1341
1347
|
assert('CSS 契约:引用卡片三段(来源 / 摘要 / 删除)', /\.dsh-sel-quotechip-src\{/.test(css) && /\.dsh-sel-quotechip-text\{/.test(css) && /\.dsh-sel-quotechip-x\{/.test(css), '')
|
|
@@ -1361,12 +1367,12 @@ try {
|
|
|
1361
1367
|
}
|
|
1362
1368
|
|
|
1363
1369
|
button.dispatch('click', { preventDefault() {}, stopPropagation() {} })
|
|
1364
|
-
const panel =
|
|
1370
|
+
const panel = floatingLayer.children.find((c) => c.className === 'dsh-sel-panel')
|
|
1365
1371
|
// 面板内的固定引用点(后续断言共用)
|
|
1366
1372
|
const sections = Array.from(walk(panel)).filter((node) => node.className.indexOf('dsh-sel-sec') >= 0)
|
|
1367
1373
|
const panelHead = Array.from(walk(panel)).find((node) => node.className === 'dsh-sel-head')
|
|
1368
1374
|
const histBtn = Array.from(walk(panelHead)).find((n) => n.className.indexOf('dsh-sel-action') >= 0 && textOf(n).indexOf('最近') >= 0)
|
|
1369
|
-
const histList =
|
|
1375
|
+
const histList = floatingLayer.children.find((n) => n.className === 'dsh-sel-history')
|
|
1370
1376
|
assert('「最近聊过的」是独立浮层,不在消息滚动区里(B 方案)', !!histList && histList.parentNode.className === 'dsh-sel-layer' && !panel.contains(histList), histList ? histList.parentNode.className : '未找到')
|
|
1371
1377
|
const detailCard = sections.find((node) => node.getAttribute('data-sec') === 'detail')
|
|
1372
1378
|
const expandBtn = Array.from(walk(panel)).find((node) => node.className === 'dsh-sel-expand')
|
|
@@ -1378,7 +1384,7 @@ assert('弹窗显示选中文字', !!panel && textOf(panel).indexOf('the migrati
|
|
|
1378
1384
|
// 指的是刚才解读的那段文字(解读要的文字与上下文在 mouseup 那刻就抓好了,见 openForSelection)
|
|
1379
1385
|
assert('点「解读」后本文档选区被收掉', clearedSelections.count >= 1, `removeAllRanges 调用 ${clearedSelections.count} 次`)
|
|
1380
1386
|
{
|
|
1381
|
-
const quoteBtnNode = Array.from(walk(
|
|
1387
|
+
const quoteBtnNode = Array.from(walk(floatingLayer)).find((n) => String(n.className).indexOf('dsh-sel-quotebtn') >= 0)
|
|
1382
1388
|
assert(
|
|
1383
1389
|
'点「解读」后引用浮标不出现(旧选区不该复活它)',
|
|
1384
1390
|
!quoteBtnNode || quoteBtnNode.style.display === 'none',
|
|
@@ -5316,7 +5322,7 @@ assert('Esc/关闭后隐藏', panel.style.display === 'none')
|
|
|
5316
5322
|
{
|
|
5317
5323
|
// 也认 setAttribute('class') —— SVG 元素的 className 是只读对象,插件只能用 setAttribute
|
|
5318
5324
|
const find = (cls) =>
|
|
5319
|
-
Array.from(walk(
|
|
5325
|
+
Array.from(walk(floatingLayer)).find(
|
|
5320
5326
|
(n) => n.className === cls || (n.getAttribute && n.getAttribute('class') === cls),
|
|
5321
5327
|
)
|
|
5322
5328
|
|
|
@@ -5434,7 +5440,7 @@ assert('Esc/关闭后隐藏', panel.style.display === 'none')
|
|
|
5434
5440
|
threadDot ? 'cx=' + String(threadDot.getAttribute('cx')) + ' cy=' + String(threadDot.getAttribute('cy')) : 'none',
|
|
5435
5441
|
)
|
|
5436
5442
|
|
|
5437
|
-
const merged = Array.from(walk(
|
|
5443
|
+
const merged = Array.from(walk(floatingLayer)).filter((n) => n.className === 'dsh-sel-uline')
|
|
5438
5444
|
assert(
|
|
5439
5445
|
'块级盒子被丢掉,两行各有一条下划线(不再整段只剩一条)',
|
|
5440
5446
|
merged.length === 2,
|
|
@@ -6154,7 +6160,7 @@ for (const dispose of disposers.reverse()) {
|
|
|
6154
6160
|
}
|
|
6155
6161
|
}
|
|
6156
6162
|
|
|
6157
|
-
assert('
|
|
6163
|
+
assert('清理后自有根摘除,保留宿主 DOM', floatingLayer.parentNode === null && mount.children.length === 0 && body.children.indexOf(container) >= 0)
|
|
6158
6164
|
assert('清理后划词桥自己建的预览 blob 被回收', bridgeOwnedBlob !== '' && !blobStore.has(bridgeOwnedBlob), bridgeOwnedBlob)
|
|
6159
6165
|
assert('清理后钩子移除', windowStub.__dshSelectionExplain === undefined)
|
|
6160
6166
|
console.log('\n=== 客户端集成测试结束 ===')
|
|
@@ -7,12 +7,14 @@ const start = source.indexOf(' var offKeyDown = listen(document,')
|
|
|
7
7
|
const end = source.indexOf('\n\n var offSettingsOpen', start)
|
|
8
8
|
assert.ok(start >= 0 && end > start)
|
|
9
9
|
const ask = {}, historyInput = {}, outside = {}, expandedChat = { closest: selector => selector.startsWith('[data-dsh-floating-chat]') ? {} : null }, compactChat = { closest: () => null }, nativeMenu = { closest: selector => selector.startsWith('[role=') ? {} : null }
|
|
10
|
-
let handler, fullView = true, calls = [], activeWindow = null, chatExpanded = true
|
|
10
|
+
let handler, fullView = true, calls = [], activeWindow = null, chatExpanded = true, hostModalOpen = false
|
|
11
11
|
const context = vm.createContext({
|
|
12
12
|
panelOpen: true, settingsOpen: false,
|
|
13
13
|
panel: { contains: node => node === ask },
|
|
14
14
|
historyList: { contains: node => node === historyInput },
|
|
15
|
-
document: { querySelector: selector => selector === '[
|
|
15
|
+
document: { querySelector: selector => selector === '[role="dialog"][aria-modal="true"]'
|
|
16
|
+
? hostModalOpen ? {} : null
|
|
17
|
+
: selector === '[data-dsh-full-view]' && fullView
|
|
16
18
|
? { getAttribute: () => activeWindow }
|
|
17
19
|
: selector.startsWith('[data-dsh-floating-chat]') && chatExpanded ? {} : null },
|
|
18
20
|
listen(target, type, callback, capture) { assert.equal(type, 'keydown'); assert.equal(capture, true); handler = callback },
|
|
@@ -27,7 +29,22 @@ function escape(target, extra = {}) {
|
|
|
27
29
|
const event = { key: 'Escape', target, ...extra, preventDefault() { calls.push('prevent') }, stopPropagation() { calls.push('stop') } }
|
|
28
30
|
handler(event)
|
|
29
31
|
}
|
|
30
|
-
function reset() { calls = []; activeWindow = null; chatExpanded = true; context.panelOpen = true; context.settingsOpen = false; context.settingsByKey = {}; context.captureRow.getAttribute = () => '0' }
|
|
32
|
+
function reset() { calls = []; activeWindow = null; chatExpanded = true; hostModalOpen = false; context.panelOpen = true; context.settingsOpen = false; context.settingsByKey = {}; context.captureRow.getAttribute = () => '0' }
|
|
33
|
+
for (const mode of [true, false]) {
|
|
34
|
+
for (const target of [ask, historyInput, outside, expandedChat]) {
|
|
35
|
+
reset(); fullView = mode; hostModalOpen = true; activeWindow = 'explanation'
|
|
36
|
+
escape(target)
|
|
37
|
+
assert.deepEqual(calls, [], '官方模态设置浮层优先消费 Esc,不受视图模式、焦点或最后操作窗口影响')
|
|
38
|
+
assert.equal(context.panelOpen, true)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
reset(); fullView = true; hostModalOpen = true; context.settingsOpen = true; escape(ask)
|
|
42
|
+
assert.deepEqual(calls, [], '官方设置打开时不关闭插件设置抽屉')
|
|
43
|
+
assert.equal(context.settingsOpen, true)
|
|
44
|
+
reset(); hostModalOpen = true; context.captureRow.getAttribute = () => '1'; escape(ask)
|
|
45
|
+
assert.deepEqual(calls, [], '官方设置打开时不取消插件录音')
|
|
46
|
+
hostModalOpen = false; escape(ask)
|
|
47
|
+
assert.deepEqual(calls, ['prevent', 'stop', 'voice'], '官方设置退出后,下一次 Esc 恢复插件的内层退出顺序')
|
|
31
48
|
reset(); activeWindow = 'chat'; escape(ask)
|
|
32
49
|
assert.deepEqual(calls, [], '最后点击聊天后,即使键盘仍在解读输入框,也让聊天先退出')
|
|
33
50
|
reset(); activeWindow = 'explanation'; escape(expandedChat)
|
|
@@ -63,4 +80,22 @@ for (const target of [ask, historyInput]) {
|
|
|
63
80
|
}
|
|
64
81
|
reset(); fullView = false; escape(outside)
|
|
65
82
|
assert.equal(context.panelOpen, false, '普通分栏保留解读原来的全局 Esc 行为')
|
|
66
|
-
|
|
83
|
+
|
|
84
|
+
// 语音卡另有一个更早注册的 capture 监听,也必须给官方浮层让出 Esc。
|
|
85
|
+
const panelHandler = handler
|
|
86
|
+
const voiceStart = source.indexOf(' var offVoiceEscape = listen(document,')
|
|
87
|
+
const voiceEnd = source.indexOf('\n }, true)', voiceStart) + '\n }, true)'.length
|
|
88
|
+
assert.ok(voiceStart >= 0 && voiceEnd > voiceStart)
|
|
89
|
+
context.vcardState = { open: true, suppress: false }
|
|
90
|
+
context.closeVCard = () => { calls.push('vcard'); context.vcardState.open = false }
|
|
91
|
+
new vm.Script(source.slice(voiceStart, voiceEnd)).runInContext(context)
|
|
92
|
+
const voiceHandler = handler
|
|
93
|
+
handler = panelHandler
|
|
94
|
+
reset(); hostModalOpen = true
|
|
95
|
+
voiceHandler({ key: 'Escape', preventDefault() { calls.push('prevent') }, stopPropagation() { calls.push('stop') } })
|
|
96
|
+
assert.deepEqual(calls, [], '官方设置打开时,语音卡不抢先消费 Esc')
|
|
97
|
+
assert.equal(context.vcardState.open, true)
|
|
98
|
+
hostModalOpen = false
|
|
99
|
+
voiceHandler({ key: 'Escape', preventDefault() { calls.push('prevent') }, stopPropagation() { calls.push('stop') } })
|
|
100
|
+
assert.deepEqual(calls, ['prevent', 'stop', 'vcard'], '官方设置退出后,Esc 可以关闭语音卡')
|
|
101
|
+
console.log('PASS focused Esc: host modals, independent windows, IME, nested settings, voice, history and split-mode behavior')
|
package/src/client/index.js
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* 交互:在页面任意位置选中文字 → 选区末端浮出「解读」按钮 → 点击弹出面板,
|
|
5
5
|
* 面板流式显示两节:① 专业中英翻译 ② 这段文字在当前上下文中的含义详解。
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* 官方 shell.overlay 槽管理挂载生命周期;界面由插件自己挂到 body 的独立根。
|
|
8
|
+
* 根层固定为 70,越过全屏工作面板,不改写宿主公共浮层。空白区域鼠标穿透。
|
|
9
|
+
* 浮标与面板继续用 position:fixed 定位到选区坐标。
|
|
10
10
|
*
|
|
11
11
|
* 本文件是 DSH 客户端 bundle 的源文件(ModuleLoader 懒加载 CJS 表格式):
|
|
12
12
|
* 顶层只注册工厂,真正的副作用(React 组件、DOM/CSS、监听)都在 factory 被
|
|
@@ -435,9 +435,10 @@ window.__ModuleLoader__.load({
|
|
|
435
435
|
// --sel-fill(-fg) 实心块:浮标按钮 / 发送键 / 结论条标签——块上文字另算一套,
|
|
436
436
|
// 否则青底配白字只有 3.6(不达标),浅色下换成深青 5.4、深色下反白 7.4
|
|
437
437
|
// 变量挂在浮层容器上 → 浮标与面板共用;深浅由**面板实际底色**决定(data-theme)。
|
|
438
|
-
'.dsh-sel-layer{
|
|
438
|
+
'.dsh-sel-layer{position:fixed;inset:0;z-index:70;pointer-events:none;--sel-a1:#0d9488;--sel-a1-text:#0f766e;--sel-a2:#0d9488;--sel-a2-text:#0f766e;',
|
|
439
439
|
'--sel-fill:#0f766e;--sel-fill-fg:#ffffff;',
|
|
440
440
|
'--sel-a2-soft:color-mix(in srgb,var(--sel-a2) 12%,transparent);--sel-a1-soft:color-mix(in srgb,var(--sel-a1) 10%,transparent)}',
|
|
441
|
+
'.dsh-sel-layer[data-full-view] .dsh-sel-pill{display:none!important}',
|
|
441
442
|
'.dsh-sel-layer[data-theme=dark]{--sel-a1:#4ecdc4;--sel-a1-text:#6fe3d8;--sel-a2:#4ecdc4;--sel-a2-text:#6fe3d8;',
|
|
442
443
|
'--sel-fill:#4ecdc4;--sel-fill-fg:#052b26;',
|
|
443
444
|
'--sel-a2-soft:color-mix(in srgb,var(--sel-a2) 16%,transparent);--sel-a1-soft:color-mix(in srgb,var(--sel-a1) 16%,transparent)}',
|
|
@@ -2492,9 +2493,10 @@ window.__ModuleLoader__.load({
|
|
|
2492
2493
|
styleEl.textContent = CSS
|
|
2493
2494
|
document.head.appendChild(styleEl)
|
|
2494
2495
|
|
|
2495
|
-
/**
|
|
2496
|
+
/** 自有浮层根:位于 body,公共父层的堆叠上下文不影响它。 */
|
|
2496
2497
|
var layer = el('div', 'dsh-sel-layer')
|
|
2497
2498
|
layer.style.pointerEvents = 'none'
|
|
2499
|
+
layer.setAttribute('data-dsh-floating-owner', 'explanation')
|
|
2498
2500
|
|
|
2499
2501
|
var button = el('button', 'dsh-sel-btn')
|
|
2500
2502
|
button.type = 'button'
|
|
@@ -2646,7 +2648,7 @@ window.__ModuleLoader__.load({
|
|
|
2646
2648
|
modelPill.appendChild(modelPillTier)
|
|
2647
2649
|
modelPill.appendChild(el('span', 'dsh-sel-picker-caret', '▼'))
|
|
2648
2650
|
var modelMenu = el('div', 'dsh-sel-pickermenu')
|
|
2649
|
-
|
|
2651
|
+
// role 随开合同步:DSH 按 [role="menu"] 判定弹层,不检查 CSS 可见性。
|
|
2650
2652
|
|
|
2651
2653
|
// ── 录音行(与主会话 ui-voice-input 同一套结构)──
|
|
2652
2654
|
// [✕ 取消] [实时波形 / 状态文案] [■ 停止 或 行内动作] …… [↑ 发送(原位不动)]
|
|
@@ -3758,10 +3760,27 @@ window.__ModuleLoader__.load({
|
|
|
3758
3760
|
layer.appendChild(historyList)
|
|
3759
3761
|
|
|
3760
3762
|
/**
|
|
3761
|
-
*
|
|
3762
|
-
*
|
|
3763
|
-
* React 不接管它们的子树(容器本身无 children,不会被 diff 清空)。
|
|
3763
|
+
* 官方槽仅保留不可见的生命周期锚点;界面根由本插件挂到 body。
|
|
3764
|
+
* 稳定 ref 避免 React 重渲染时摘挂根节点、丢失输入焦点。
|
|
3764
3765
|
*/
|
|
3766
|
+
function syncFullViewLayer() {
|
|
3767
|
+
var fullView = !!document.querySelector('[data-dsh-full-view]')
|
|
3768
|
+
if (fullView !== layer.hasAttribute('data-full-view')) {
|
|
3769
|
+
if (fullView) layer.setAttribute('data-full-view', '')
|
|
3770
|
+
else layer.removeAttribute('data-full-view')
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
function mountFloatingLayer(node) {
|
|
3774
|
+
if (node) {
|
|
3775
|
+
if (layer.parentNode !== document.body) document.body.appendChild(layer)
|
|
3776
|
+
syncFullViewLayer()
|
|
3777
|
+
} else if (layer.parentNode) layer.parentNode.removeChild(layer)
|
|
3778
|
+
}
|
|
3779
|
+
var fullViewObserver = null
|
|
3780
|
+
if (typeof MutationObserver === 'function') {
|
|
3781
|
+
fullViewObserver = new MutationObserver(syncFullViewLayer)
|
|
3782
|
+
fullViewObserver.observe(document.body, {subtree:true, childList:true, attributes:true, attributeFilter:['data-dsh-full-view']})
|
|
3783
|
+
}
|
|
3765
3784
|
/** 开窗后"还没聚焦过输入框"(见 focusComposerOnce)。 */
|
|
3766
3785
|
var askFocusPending = false
|
|
3767
3786
|
/** 调试:最后一次"没聚焦"卡在哪一步(-1 表示聚焦成功)。 */
|
|
@@ -3780,16 +3799,14 @@ window.__ModuleLoader__.load({
|
|
|
3780
3799
|
},
|
|
3781
3800
|
}, function SelectionExplainLayer() {
|
|
3782
3801
|
return React.createElement('div', {
|
|
3783
|
-
className: 'dsh-sel-
|
|
3784
|
-
style: { pointerEvents: 'none' },
|
|
3785
|
-
ref:
|
|
3786
|
-
if (node && layer.parentNode !== node) node.appendChild(layer)
|
|
3787
|
-
},
|
|
3802
|
+
className: 'dsh-sel-anchor',
|
|
3803
|
+
style: { display: 'none', pointerEvents: 'none' },
|
|
3804
|
+
ref: mountFloatingLayer,
|
|
3788
3805
|
})
|
|
3789
3806
|
})
|
|
3790
3807
|
})
|
|
3791
3808
|
} catch (error) {
|
|
3792
|
-
//
|
|
3809
|
+
// 槽注册失败仅报错,自有界面根保持未挂载,不影响其他插件。
|
|
3793
3810
|
console.error('[dsh-selection-explain] shell.overlay 槽注册失败:', error)
|
|
3794
3811
|
}
|
|
3795
3812
|
} else {
|
|
@@ -9467,6 +9484,7 @@ window.__ModuleLoader__.load({
|
|
|
9467
9484
|
*/
|
|
9468
9485
|
var offVoiceEscape = listen(document, 'keydown', function (event) {
|
|
9469
9486
|
if (event.key !== 'Escape' || !vcardState.open || event.defaultPrevented) return
|
|
9487
|
+
if (document.querySelector('[role="dialog"][aria-modal="true"]')) return
|
|
9470
9488
|
event.preventDefault()
|
|
9471
9489
|
event.stopPropagation()
|
|
9472
9490
|
vcardState.suppress = true
|
|
@@ -9919,11 +9937,13 @@ window.__ModuleLoader__.load({
|
|
|
9919
9937
|
|
|
9920
9938
|
function closeModelMenu() {
|
|
9921
9939
|
modelMenu.setAttribute('data-open', '0')
|
|
9940
|
+
modelMenu.removeAttribute('role')
|
|
9922
9941
|
modelPill.setAttribute('aria-expanded', 'false')
|
|
9923
9942
|
}
|
|
9924
9943
|
|
|
9925
9944
|
function openModelMenu() {
|
|
9926
9945
|
modelMenu.setAttribute('data-open', '1')
|
|
9946
|
+
modelMenu.setAttribute('role', 'menu')
|
|
9927
9947
|
modelPill.setAttribute('aria-expanded', 'true')
|
|
9928
9948
|
renderModelMenu()
|
|
9929
9949
|
loadModelCatalog(false).then(function () {
|
|
@@ -9945,7 +9965,7 @@ window.__ModuleLoader__.load({
|
|
|
9945
9965
|
event.stopPropagation() // 菜单内部点击不要触发"点到外面"
|
|
9946
9966
|
})
|
|
9947
9967
|
var offModelKeys = listen(modelMenu, 'keydown', function (event) {
|
|
9948
|
-
if (event.key === 'Escape') closeModelMenu()
|
|
9968
|
+
if (event.key === 'Escape' && !document.querySelector('[role="dialog"][aria-modal="true"]')) closeModelMenu()
|
|
9949
9969
|
})
|
|
9950
9970
|
|
|
9951
9971
|
// —— 网页模式开关 ——
|
|
@@ -10123,6 +10143,9 @@ window.__ModuleLoader__.load({
|
|
|
10123
10143
|
|
|
10124
10144
|
var offKeyDown = listen(document, 'keydown', function (event) {
|
|
10125
10145
|
if (event.key !== 'Escape' || !panelOpen || event.defaultPrevented || event.isComposing || event.keyCode === 229) return
|
|
10146
|
+
// 官方模态浮层的 Esc 在冒泡阶段处理;capture 不消费,先让官方设置等浮层退出。
|
|
10147
|
+
// 插件面板和设置抽屉是非模态 dialog,不带 aria-modal,不会命中这里。
|
|
10148
|
+
if (document.querySelector('[role="dialog"][aria-modal="true"]')) return
|
|
10126
10149
|
// 完整视图记录最后点击/操作的小窗;不能只看可能停在另一窗的键盘焦点。
|
|
10127
10150
|
var fullViewFrame = document.querySelector('[data-dsh-full-view]')
|
|
10128
10151
|
if (fullViewFrame) {
|
|
@@ -12127,6 +12150,7 @@ window.__ModuleLoader__.load({
|
|
|
12127
12150
|
}
|
|
12128
12151
|
pillObserver = null
|
|
12129
12152
|
}
|
|
12153
|
+
if (fullViewObserver) fullViewObserver.disconnect()
|
|
12130
12154
|
if (typeof offSlot === 'function') offSlot()
|
|
12131
12155
|
// 划词桥:摘观察者与消息监听,并把我们自己建的预览 blob 全部回收
|
|
12132
12156
|
if (bridgeScanId) clearTimeout(bridgeScanId)
|