@yfwu2020/dsh-selection-explain 0.10.1 → 0.10.2
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 +9 -3
- package/lib/client.js +32 -14
- package/package.json +1 -1
- package/scripts/smoke-panel.mjs +13 -1
- package/scripts/test-client.mjs +15 -9
- package/src/client/index.js +32 -14
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.2` · `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.2 为例:
|
|
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.2.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,12 @@ 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
|
+
|
|
156
162
|
## 数据与使用范围
|
|
157
163
|
|
|
158
164
|
选中文字、必要的前后文,以及开启的会话背景会交给你配置的模型。可在设置中关闭会话背景或工具调用。
|
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'
|
|
@@ -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 {
|
|
@@ -12127,6 +12144,7 @@ window.__ModuleLoader__.load({
|
|
|
12127
12144
|
}
|
|
12128
12145
|
pillObserver = null
|
|
12129
12146
|
}
|
|
12147
|
+
if (fullViewObserver) fullViewObserver.disconnect()
|
|
12130
12148
|
if (typeof offSlot === 'function') offSlot()
|
|
12131
12149
|
// 划词桥:摘观察者与消息监听,并把我们自己建的预览 blob 全部回收
|
|
12132
12150
|
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()
|
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=== 客户端集成测试结束 ===')
|
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'
|
|
@@ -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 {
|
|
@@ -12127,6 +12144,7 @@ window.__ModuleLoader__.load({
|
|
|
12127
12144
|
}
|
|
12128
12145
|
pillObserver = null
|
|
12129
12146
|
}
|
|
12147
|
+
if (fullViewObserver) fullViewObserver.disconnect()
|
|
12130
12148
|
if (typeof offSlot === 'function') offSlot()
|
|
12131
12149
|
// 划词桥:摘观察者与消息监听,并把我们自己建的预览 blob 全部回收
|
|
12132
12150
|
if (bridgeScanId) clearTimeout(bridgeScanId)
|