dsh-plugin-table-zoom 0.3.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.2] - 2026-08-22
4
+
5
+ - README:新增「相关插件」互相引用段(列出同系列已发布插件,npm / GitHub 链接 + 一句话说明),互相引流。
6
+ - **修复:放大/溢出表格里拖动不再被平移劫持**——此前正文内容溢出
7
+ (缩放后或表超宽)时,左键按住正文任意位置拖动即进入平移,并主动清空
8
+ 选区,导致**无法在放大的表格里框选文字**。现在普通左键拖动保留浏览器
9
+ 原生行为,可直接框选表格文字(含放大后);滚动条 / 滚轮照常滚动。
10
+ - **平移改为显式手势**:表格超宽/超高时,**按住空格再按住正文拖动**
11
+ 才左右/上下滚动(grab 光标仅在按住空格时出现,拖拽中为 grabbing);
12
+ 触屏仍走原生滚动;拖动中松开空格即结束平移;关闭浮窗时清理空格监听。
13
+ - 测试:冒烟拆分/新增 2 项(未按空格左键拖动不启动平移 / 按住空格可平移
14
+ 且光标随空格状态切换),共 selfcheck 12 项 + 冒烟 13 项。
15
+
16
+ ## [0.3.1] - 2026-08-22
17
+
18
+ - **每次打开自适应,不再继承任何布局**:移除尺寸/位置/缩放的跨次记忆
19
+ (不再读写 localStorage),浮窗每次打开都按表格自然宽度定宽
20
+ (下限 320;窄表收缩不撑满、**超级大表铺满视口可用宽度**)、居中、
21
+ 缩放 100%;手动拖拽(改大小/移动/缩放)只影响当次打开,关掉再开即回到自适应。
22
+ - **修复:自适应此前从未生效**——旧实现返回值错误(无记忆时返回估算宽 960
23
+ 而非 0)导致自适应分支不执行,小表仍撑满大窗口。
24
+ - **缩放不再跨次继承**:Ctrl+滚轮缩放比例不写入 localStorage,
25
+ 每次打开浮窗都从 100% 开始(标题栏百分比同步复位)。
26
+ - 测试:冒烟调整/新增 3 项(自适应宽度:窄表收缩/宽表封顶/空表兜底;
27
+ 每次打开不继承上次尺寸;openPopup 走自适应下限),
28
+ 共 selfcheck 12 项 + 冒烟 12 项。
29
+
3
30
  ## [0.3.0] - 2026-08-22
4
31
 
5
32
  - **浮窗可拖动**:不再强制居中——按住标题栏(按钮除外)拖动浮窗位置,
package/README.md CHANGED
@@ -22,13 +22,17 @@ DeepSeek Harness (dsh) Web GUI 聊天表格浮窗插件。
22
22
  - 浮窗头部「复制为 Markdown」按钮一键复制整表(单元格内联换行折叠、管道符转义);
23
23
  - 浮窗里展示的是**原表格的克隆**,聊天里的表格保持不变,也不会被重复增强;
24
24
  - **拖拽改尺寸**:浮窗右下角手柄可自由调整大小(最小 320×200,最大到视口留白),
25
- 调整结果自动记忆,下次打开沿用;
25
+ 只影响当次打开;
26
+ - **每次打开自适应**:浮窗不记忆任何布局——宽度按表格内容自适应(窄表不撑满、
27
+ 超级大表直接铺满视口可用宽度,下限 320)、位置居中、缩放 100%;关掉再开即回到自适应;
26
28
  - **拖拽移动**:按住标题栏可把浮窗拖到任意位置(自动限制在页面内,不会拖出屏幕),
27
- 位置同样自动记忆;
29
+ 位置只影响当次打开;
28
30
  - **Ctrl+滚轮缩放**:浮窗内按住 Ctrl 滚动滚轮,表格字体 60%–250% 缩放
29
- (步进 10%),标题栏实时显示百分比;
30
- - **拖拽平移**:表格超宽/超高时,按住正文任意位置即可左右/上下拖动滚动
31
- (grab 光标提示;越过 4px 阈值才视为拖动,不干扰文本选择;触屏走原生滚动);
31
+ (步进 10%),标题栏实时显示百分比;**缩放不记忆,每次打开回到 100%**;
32
+ - **框选文字**:普通左键在表格(含放大后)里拖动即可框选文字,
33
+ 不会被平移劫持;
34
+ - **按住空格拖拽平移**:表格超宽/超高时,按住空格再按住正文拖动即可
35
+ 左右/上下滚动(grab 光标仅在按住空格时出现;触屏走原生滚动);
32
36
  - **滚动条清晰**:正文滚动条加深加粗,浅色主题下也能看清;
33
37
  - **列宽适配**:列宽上限与聊天内一致(`min(30vw, 320px)`),超宽列自动换行收缩,
34
38
  表格整体不再撑破浮窗。
@@ -108,6 +112,20 @@ npm run pack # 打包
108
112
  - 不影响其他插件:只认 `tableScroll` 容器内的表格,跳过浮窗与 image-tools
109
113
  lightbox 内的表格。
110
114
 
115
+ ## 相关插件
116
+
117
+ 本插件属于 **Pasumao 的 dsh 插件生态**,同系列已发布插件可搭配使用:
118
+
119
+ | 插件(npm) | GitHub | 说明 |
120
+ |---|---|---|
121
+ | [dsh-notify](https://www.npmjs.com/package/dsh-notify) | [GitHub 仓库](https://github.com/Pasumao/dsh-plugin-notify) | Windows 原生通知 + 系统托盘 |
122
+ | [dsh-plugin-choice-refresh](https://www.npmjs.com/package/dsh-plugin-choice-refresh) | [GitHub 仓库](https://github.com/Pasumao/dsh-plugin-choice-refresh) | 选择增强:重新生成选项 / 更多选项 |
123
+ | [dsh-plugin-dev-kb](https://www.npmjs.com/package/dsh-plugin-dev-kb) | [GitHub 仓库](https://github.com/Pasumao/dsh-plugin-dev-kb) | 插件开发知识库(官方文档完整镜像 + 技能) |
124
+ | [dsh-plugin-image-tools](https://www.npmjs.com/package/dsh-plugin-image-tools) | [GitHub 仓库](https://github.com/Pasumao/dsh-plugin-image-tools) | 图片选择卡 + 回复内嵌图片 + 盲模型收图 |
125
+ | [dsh-plugin-workbench](https://www.npmjs.com/package/dsh-plugin-workbench) | [GitHub 仓库](https://github.com/Pasumao/dsh-plugin-workbench) | VS Code 风格文件浏览器 + 可编辑预览 |
126
+
127
+ > 本系列其余插件见 [Pasumao · dsh 插件](https://github.com/Pasumao);觉得好用欢迎到 GitHub 点 ⭐。
128
+
111
129
  ## AI 生成声明
112
130
 
113
131
  代码与文档由 AI 辅助生成(DeepSeek Harness),均经人工审查与实机验证
package/lib/client.js CHANGED
@@ -203,11 +203,10 @@ window.__ModuleLoader__.load({
203
203
  }
204
204
 
205
205
  // ------------------------------------------------------------------
206
- // 浮窗布局:拖拽改大小 + 拖标题栏移动 + Ctrl+滚轮缩放 + 布局记忆
206
+ // 浮窗布局:拖拽改大小 + 拖标题栏移动 + Ctrl+滚轮缩放
207
+ // 不记忆任何布局:每次打开都按表格内容自适应宽度并居中,手动拖拽只影响当次。
207
208
  // ------------------------------------------------------------------
208
209
 
209
- /** localStorage 键:记住用户最后一次手动调整的浮窗布局(尺寸/位置/缩放)。 */
210
- var SIZE_KEY = 'dsh-plugin-table-zoom:popup-size';
211
210
  var MIN_POPUP_W = 320;
212
211
  var MIN_POPUP_H = 200;
213
212
  /** 浮窗距视口边缘的最小留白。 */
@@ -236,52 +235,25 @@ window.__ModuleLoader__.load({
236
235
  return 800;
237
236
  }
238
237
 
239
- /** 读取上次记忆的浮窗布局;无记录/损坏/隐私模式返回 null。 */
240
- function readSavedSize() {
241
- try {
242
- if (typeof localStorage === 'undefined') return null;
243
- var raw = localStorage.getItem(SIZE_KEY);
244
- if (raw === null || raw === '') return null;
245
- var v = JSON.parse(raw);
246
- if (v === null || typeof v !== 'object') return null;
247
- return {
248
- w: Number(v.w) || 0,
249
- h: Number(v.h) || 0,
250
- x: typeof v.x === 'number' ? v.x : null,
251
- y: typeof v.y === 'number' ? v.y : null,
252
- zoom: typeof v.zoom === 'number' && v.zoom > 0 ? v.zoom : 1
253
- };
254
- } catch (e) {
255
- return null;
256
- }
257
- }
258
-
259
238
  /**
260
- * 打开浮窗时套用上次记忆的布局(尺寸/位置/缩放,按当前视口收窄)。
261
- * 无记忆时尺寸用 CSS 默认、位置居中。返回本次生效的 {w,h,x,y,zoom}。
239
+ * 每次打开都按表格自然宽度自适应面板宽度
240
+ * (正文左右 padding 36 + 面板边框 2;下限 320,上限为视口减去留白——
241
+ * 超级大表直接铺满最大可用宽度)。
262
242
  */
263
- function applySavedLayout(panel) {
264
- var v = readSavedSize();
265
- var vw = viewportW();
266
- var vh = viewportH();
267
- var maxW = vw - POPUP_MARGIN;
268
- var maxH = vh - POPUP_MARGIN;
269
- if (maxW < MIN_POPUP_W || maxH < MIN_POPUP_H) return { w: 0, h: 0, x: 0, y: 0, zoom: 1 };
270
- var w = v !== null && v.w > 0 ? clamp(v.w, MIN_POPUP_W, maxW) : 0;
271
- var h = v !== null && v.h > 0 ? clamp(v.h, MIN_POPUP_H, maxH) : 0;
272
- if (w > 0) {
273
- panel.style.width = w + 'px';
274
- panel.style.maxHeight = 'none';
275
- }
276
- if (h > 0) panel.style.height = h + 'px';
277
- var pw = w > 0 ? w : Math.min(960, maxW);
278
- var ph = h > 0 ? h : Math.min(720, maxH);
279
- var x = v !== null && v.x !== null ? clamp(v.x, 0, Math.max(0, vw - pw)) : Math.round((vw - pw) / 2);
280
- var y = v !== null && v.y !== null ? clamp(v.y, 0, Math.max(0, vh - ph)) : Math.round((vh - ph) / 2);
281
- panel.style.left = x + 'px';
282
- panel.style.top = y + 'px';
283
- var zoom = v !== null ? clamp(v.zoom, ZOOM_MIN, ZOOM_MAX) : 1;
284
- return { w: pw, h: ph, x: x, y: y, zoom: zoom };
243
+ function applyAdaptiveWidth(panel, clone) {
244
+ var tableW = typeof clone.offsetWidth === 'number' && clone.offsetWidth > 0 ? clone.offsetWidth : 0;
245
+ var maxW = viewportW() - POPUP_MARGIN;
246
+ var w = clamp(tableW + 38, MIN_POPUP_W, Math.max(MIN_POPUP_W, maxW));
247
+ panel.style.width = w + 'px';
248
+ }
249
+
250
+ /** 把面板按实际尺寸重新居中(自适应改宽后调用;测不到尺寸时跳过)。 */
251
+ function centerPanel(panel) {
252
+ var w = panel.offsetWidth;
253
+ var h = panel.offsetHeight;
254
+ if (typeof w !== 'number' || w <= 0 || typeof h !== 'number' || h <= 0) return;
255
+ panel.style.left = Math.round((viewportW() - w) / 2) + 'px';
256
+ panel.style.top = Math.round((viewportH() - h) / 2) + 'px';
285
257
  }
286
258
 
287
259
  /** 把面板位置限制在视口内(完全可见,不越出页面)。 */
@@ -307,32 +279,19 @@ window.__ModuleLoader__.load({
307
279
  panel.style.top = y + 'px';
308
280
  }
309
281
 
310
- /** 拖拽结束后记忆当前布局(尺寸/位置/缩放;localStorage 不可用时静默跳过)。 */
311
- function saveLayout(panel, zoom) {
312
- try {
313
- if (typeof localStorage === 'undefined') return;
314
- var rect = panel.getBoundingClientRect();
315
- if (rect === null || typeof rect !== 'object') return;
316
- var prev = readSavedSize();
317
- var record = {
318
- w: Math.round(rect.width),
319
- h: Math.round(rect.height),
320
- x: typeof rect.left === 'number' ? Math.round(rect.left) : 0,
321
- y: typeof rect.top === 'number' ? Math.round(rect.top) : 0,
322
- zoom: typeof zoom === 'number' && zoom > 0 ? Math.round(zoom * 100) / 100 : (prev !== null && prev.zoom > 0 ? prev.zoom : 1)
323
- };
324
- localStorage.setItem(SIZE_KEY, JSON.stringify(record));
325
- } catch (e) {
326
- /* 隐私模式等场景忽略 */
327
- }
328
- }
329
-
330
- /** 内容溢出时给正文加 grab 光标提示(可拖拽平移)。 */
282
+ /**
283
+ * 内容溢出且按住空格(平移预备)时才给正文加 grab 光标提示。
284
+ * 空格状态由 attachPan 维护在 body._dstzSpaceArmed 上;
285
+ * 布局变化(打开/改尺寸/缩放)后调用本函数即可同步光标。
286
+ */
331
287
  function refreshGrabbable(body) {
332
288
  var overX = body.scrollWidth > body.clientWidth + 1;
333
289
  var overY = body.scrollHeight > body.clientHeight + 1;
334
- if (overX || overY) body.classList.add('dstz-grabbable');
335
- else body.classList.remove('dstz-grabbable');
290
+ if ((overX || overY) && body._dstzSpaceArmed === true) {
291
+ body.classList.add('dstz-grabbable');
292
+ } else {
293
+ body.classList.remove('dstz-grabbable');
294
+ }
336
295
  }
337
296
 
338
297
  /** 命中可交互元素(链接/按钮/输入框等)时让给默认行为,不启动拖拽。 */
@@ -343,7 +302,7 @@ window.__ModuleLoader__.load({
343
302
 
344
303
  /**
345
304
  * 给面板挂载「拖右下角改尺寸」:指针捕获 + _suppressClick 防止拖完
346
- * 松手落在遮罩上误触发关闭。尺寸写入 inline style 并记忆到 localStorage。
305
+ * 松手落在遮罩上误触发关闭。尺寸只影响当次打开,不持久化。
347
306
  * @param {object} panel - .dstz-panel 元素。
348
307
  * @param {object} overlay - .dstz-popup 元素(承载 _suppressClick)。
349
308
  */
@@ -382,7 +341,6 @@ window.__ModuleLoader__.load({
382
341
  state = null;
383
342
  panel.classList.remove('dstz-resizing');
384
343
  clampPanelPosition(panel);
385
- saveLayout(panel);
386
344
  window.setTimeout(function () { overlay._suppressClick = false; }, 0);
387
345
  };
388
346
  handle.addEventListener('pointerdown', begin);
@@ -394,7 +352,7 @@ window.__ModuleLoader__.load({
394
352
 
395
353
  /**
396
354
  * 给标题栏挂载「拖拽移动」:按住标题栏(按钮除外)拖动面板位置,
397
- * 位置限制在视口内(不越出页面);拖动结束记忆位置。
355
+ * 位置限制在视口内(不越出页面);位置不记忆,下次打开重新居中。
398
356
  * @param {object} panel - .dstz-panel 元素(children[0] 为标题栏)。
399
357
  * @param {object} overlay - .dstz-popup 元素(承载 _suppressClick)。
400
358
  */
@@ -431,32 +389,72 @@ window.__ModuleLoader__.load({
431
389
  var wasMoved = state.moved;
432
390
  state = null;
433
391
  header.classList.remove('dstz-dragging');
434
- if (wasMoved) {
435
- saveLayout(panel);
436
- window.setTimeout(function () { overlay._suppressClick = false; }, 0);
437
- }
392
+ if (wasMoved) window.setTimeout(function () { overlay._suppressClick = false; }, 0);
438
393
  };
439
394
  header.addEventListener('pointerup', end);
440
395
  header.addEventListener('pointercancel', end);
441
396
  }
442
397
 
443
398
  /**
444
- * 给正文挂载「拖拽平移」:内容溢出时按住表格左右/上下拖动即可滚动
445
- * (鼠标/触控笔;触屏保留原生滚动)。越过 4px 阈值才视为拖动,
446
- * 避免干扰文本选择;拖动结束松手落在遮罩上不会误关浮窗。
399
+ * 给正文挂载「按住空格拖动平移」:内容溢出时,按住空格再按住表格
400
+ * 左右/上下拖动即可滚动(鼠标/触控笔;触屏保留原生滚动)。
401
+ * 普通左键拖动不再被平移劫持,而是保留浏览器原生行为——可在表格
402
+ * (含放大后)里直接框选文字;只有按住空格时拖动才平移。
403
+ * grab 光标仅在按住空格悬停正文且内容溢出时出现;
404
+ * 拖动结束松手落在遮罩上不会误关浮窗。
447
405
  * @param {object} body - .dstz-body 元素。
448
- * @param {object} overlay - .dstz-popup 元素(承载 _suppressClick)。
406
+ * @param {object} overlay - .dstz-popup 元素(承载 _suppressClick / _cleanups)。
449
407
  */
450
408
  function attachPan(body, overlay) {
451
409
  var state = null;
410
+ var endPan = function () {
411
+ if (state === null) return;
412
+ state = null;
413
+ body.classList.remove('dstz-panning');
414
+ window.setTimeout(function () { overlay._suppressClick = false; }, 0);
415
+ };
416
+ var setArmed = function (on) {
417
+ body._dstzSpaceArmed = on === true;
418
+ refreshGrabbable(body);
419
+ };
420
+ var onKeyDown = function (e) {
421
+ if (e.key === ' ' || e.code === 'Space') setArmed(true);
422
+ };
423
+ var onKeyUp = function (e) {
424
+ if (e.key === ' ' || e.code === 'Space') {
425
+ setArmed(false);
426
+ if (state !== null) endPan(); // 拖动中松开空格:结束平移
427
+ }
428
+ };
429
+ var onBlur = function () {
430
+ setArmed(false);
431
+ if (state !== null) endPan();
432
+ };
433
+ if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
434
+ window.addEventListener('keydown', onKeyDown, true);
435
+ window.addEventListener('keyup', onKeyUp, true);
436
+ window.addEventListener('blur', onBlur);
437
+ if (Array.isArray(overlay._cleanups)) {
438
+ overlay._cleanups.push(function () {
439
+ window.removeEventListener('keydown', onKeyDown, true);
440
+ window.removeEventListener('keyup', onKeyUp, true);
441
+ window.removeEventListener('blur', onBlur);
442
+ setArmed(false);
443
+ });
444
+ }
445
+ }
452
446
  body.addEventListener('pointerdown', function (e) {
453
447
  if (e.button !== 0) return;
454
448
  if (e.pointerType !== undefined && e.pointerType !== 'mouse' && e.pointerType !== 'pen') return;
449
+ if (!body._dstzSpaceArmed) return; // 未按住空格:让给浏览器原生文本选择(框选文字)
455
450
  if (isInteractive(e.target)) return;
456
451
  var overX = body.scrollWidth > body.clientWidth + 1;
457
452
  var overY = body.scrollHeight > body.clientHeight + 1;
458
453
  if (!overX && !overY) return;
459
- state = { x: e.clientX, y: e.clientY, sl: body.scrollLeft, st: body.scrollTop, moved: false };
454
+ if (typeof e.preventDefault === 'function') e.preventDefault();
455
+ state = { x: e.clientX, y: e.clientY, sl: body.scrollLeft, st: body.scrollTop };
456
+ body.classList.add('dstz-panning');
457
+ overlay._suppressClick = true;
460
458
  if (typeof body.setPointerCapture === 'function') {
461
459
  try { body.setPointerCapture(e.pointerId); } catch (err) { /* ignore */ }
462
460
  }
@@ -465,28 +463,12 @@ window.__ModuleLoader__.load({
465
463
  if (state === null) return;
466
464
  var dx = e.clientX - state.x;
467
465
  var dy = e.clientY - state.y;
468
- if (!state.moved && Math.abs(dx) + Math.abs(dy) < 4) return;
469
- if (!state.moved) {
470
- state.moved = true;
471
- body.classList.add('dstz-panning');
472
- overlay._suppressClick = true;
473
- if (typeof document !== 'undefined' && typeof document.getSelection === 'function') {
474
- try { document.getSelection().removeAllRanges(); } catch (err) { /* ignore */ }
475
- }
476
- }
477
466
  body.scrollLeft = state.sl - dx;
478
467
  body.scrollTop = state.st - dy;
479
468
  if (typeof e.preventDefault === 'function') e.preventDefault();
480
469
  });
481
- var end = function () {
482
- if (state === null) return;
483
- var wasMoved = state.moved;
484
- state = null;
485
- body.classList.remove('dstz-panning');
486
- if (wasMoved) window.setTimeout(function () { overlay._suppressClick = false; }, 0);
487
- };
488
- body.addEventListener('pointerup', end);
489
- body.addEventListener('pointercancel', end);
470
+ body.addEventListener('pointerup', endPan);
471
+ body.addEventListener('pointercancel', endPan);
490
472
  }
491
473
 
492
474
  /**
@@ -559,7 +541,10 @@ window.__ModuleLoader__.load({
559
541
  panel.appendChild(body);
560
542
  overlay.appendChild(panel);
561
543
 
562
- // 拖拽改尺寸 + 拖标题栏移动 + 内容溢出拖拽平移 + 布局记忆 + Ctrl+滚轮缩放
544
+ // 清理登记(attachPan 等挂载的全局监听会 push 进来,关闭时统一移除)
545
+ overlay._cleanups = [];
546
+
547
+ // 拖拽改尺寸 + 拖标题栏移动 + 内容溢出拖拽平移 + Ctrl+滚轮缩放
563
548
  attachResize(panel, overlay);
564
549
  attachMove(panel, overlay);
565
550
  attachPan(body, overlay);
@@ -571,16 +556,14 @@ window.__ModuleLoader__.load({
571
556
  small.textContent = rowCount + ' ' + LABELS.rowsCols + ' ' + colCount
572
557
  + (Math.abs(zoom - 1) > 0.001 ? ' \u00b7 ' + Math.round(zoom * 100) + '%' : '');
573
558
  };
574
- var layout = applySavedLayout(panel);
575
- applyZoom(layout.zoom);
559
+ applyZoom(1); // 不继承上次的缩放,每次都从 100% 开始
576
560
  refreshGrabbable(body);
577
561
 
578
- // Ctrl+滚轮:缩放表格字体(阻止浏览器整页缩放)
562
+ // Ctrl+滚轮:缩放表格字体(阻止浏览器整页缩放;缩放不写入 localStorage)
579
563
  var onWheel = function (e) {
580
564
  if (!e.ctrlKey) return;
581
565
  if (typeof e.preventDefault === 'function') e.preventDefault();
582
566
  applyZoom(e.deltaY < 0 ? zoom + ZOOM_STEP : zoom - ZOOM_STEP);
583
- saveLayout(panel, zoom);
584
567
  };
585
568
  panel.addEventListener('wheel', onWheel, { passive: false });
586
569
 
@@ -596,7 +579,6 @@ window.__ModuleLoader__.load({
596
579
  clampPanelPosition(panel);
597
580
  refreshGrabbable(body);
598
581
  };
599
- overlay._cleanups = [];
600
582
  if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
601
583
  window.addEventListener('resize', onWindowResize);
602
584
  overlay._cleanups.push(function () {
@@ -623,6 +605,10 @@ window.__ModuleLoader__.load({
623
605
 
624
606
  activePopup = overlay;
625
607
  document.body.appendChild(overlay);
608
+ // 每次打开都按表格自然宽度自适应,并按实际尺寸重新居中(不继承上次的尺寸/位置)
609
+ applyAdaptiveWidth(panel, clone);
610
+ centerPanel(panel);
611
+ refreshGrabbable(body);
626
612
  closeBtn.focus();
627
613
  }
628
614
 
@@ -734,9 +720,7 @@ window.__ModuleLoader__.load({
734
720
  exports.attachResize = attachResize;
735
721
  exports.attachMove = attachMove;
736
722
  exports.attachPan = attachPan;
737
- exports.applySavedLayout = applySavedLayout;
738
- exports.saveLayout = saveLayout;
739
- exports.readSavedSize = readSavedSize;
723
+ exports.applyAdaptiveWidth = applyAdaptiveWidth;
740
724
  exports.clampPanelPosition = clampPanelPosition;
741
725
  exports.refreshGrabbable = refreshGrabbable;
742
726
  return module.exports;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-plugin-table-zoom",
3
3
  "description": "DSH 聊天表格浮窗插件:聊天里 markdown 表格太长(行多/超宽)看不完时,表格上方自动出现「浮窗查看」按钮,点击弹出可滚动的小浮窗完整显示表格,并支持一键复制为 Markdown。纯前端 DOM 增强,不改核心包、不注册工具、无服务端逻辑。",
4
- "version": "0.3.0",
4
+ "version": "0.3.2",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -7,9 +7,10 @@
7
7
  * 3. openPopup 创建浮窗(标题含行列数、表格克隆、关闭按钮),
8
8
  * closePopup 关闭并还原 body 滚动;
9
9
  * 4. 表格克隆在浮窗内不会被再次增强(跳过 .dstz-popup);
10
- * 5. 右下角改尺寸手柄可拖拽改面板大小并记忆到 localStorage,
11
- * 再次打开复用记忆布局;内容溢出时正文可拖拽平移;
12
- * 标题栏可拖拽移动浮窗(不越出视口);Ctrl+滚轮缩放表格字体。
10
+ * 5. 右下角改尺寸手柄可拖拽改面板大小;溢出时左键拖动保留原生文本
11
+ * 选择(不启动平移),按住空格拖动才平移;标题栏可拖拽移动浮窗
12
+ * (不越出视口);Ctrl+滚轮缩放表格字体;
13
+ * 每次打开都按内容自适应宽度并居中(不继承上次的尺寸/位置/缩放)。
13
14
  *
14
15
  * 运行:node scripts/smoke-client.mjs
15
16
  */
@@ -132,7 +133,22 @@ const documentEl = {
132
133
  removeEventListener() {},
133
134
  }
134
135
  globalThis.document = documentEl
135
- globalThis.window = { setTimeout: (fn) => fn() }
136
+ globalThis.window = {
137
+ setTimeout: (fn) => fn(),
138
+ _listeners: {},
139
+ addEventListener(type, fn) { (this._listeners[type] ??= []).push(fn) },
140
+ removeEventListener(type, fn) {
141
+ const arr = this._listeners[type]
142
+ if (!arr) return
143
+ const idx = arr.indexOf(fn)
144
+ if (idx >= 0) arr.splice(idx, 1)
145
+ },
146
+ dispatch(type, event = {}) {
147
+ const arr = this._listeners[type]
148
+ if (!arr) return
149
+ for (const fn of [...arr]) fn({ target: documentEl, stopPropagation() {}, ...event })
150
+ },
151
+ }
136
152
  Object.defineProperty(globalThis, 'navigator', {
137
153
  value: { clipboard: { writeText: async (t) => { globalThis.__copied = t } } },
138
154
  configurable: true,
@@ -254,17 +270,7 @@ ok('copyText 写入剪贴板', async () => {
254
270
  assert.equal(globalThis.__copied, '| a |\n| --- |')
255
271
  })
256
272
 
257
- // 5) 浮窗改尺寸 + 尺寸记忆 + 拖拽平移
258
- // 假 localStorage:验证尺寸记忆读写(真实环境缺失时各函数均已兜底)
259
- const sizeStore = new Map()
260
- Object.defineProperty(globalThis, 'localStorage', {
261
- value: {
262
- getItem: (k) => (sizeStore.has(k) ? sizeStore.get(k) : null),
263
- setItem: (k, v) => { sizeStore.set(k, String(v)) },
264
- removeItem: (k) => { sizeStore.delete(k) },
265
- },
266
- configurable: true,
267
- })
273
+ // 5) 浮窗改尺寸 + 自适应 + 拖拽平移(无布局记忆:每次打开都自适应并居中)
268
274
 
269
275
  ok('openPopup 挂载右下角改尺寸手柄并可拖拽', () => {
270
276
  mod.closePopup()
@@ -275,6 +281,9 @@ ok('openPopup 挂载右下角改尺寸手柄并可拖拽', () => {
275
281
  const panel = popup.children[0]
276
282
  const handle = panel.children.find((c) => c.className === 'dstz-resizeSE')
277
283
  assert.ok(handle !== undefined, 'resize handle not mounted')
284
+ // 假 DOM 无真实布局:显式给一个起始尺寸,验证拖拽按 delta 改大小
285
+ panel.style.width = '800px'
286
+ panel.style.height = '400px'
278
287
  panel.getBoundingClientRect = () => ({
279
288
  width: parseInt(panel.style.width, 10) || 800,
280
289
  height: parseInt(panel.style.height, 10) || 400,
@@ -286,31 +295,32 @@ ok('openPopup 挂载右下角改尺寸手柄并可拖拽', () => {
286
295
  assert.equal(panel.style.maxHeight, 'none')
287
296
  assert.ok(popup._suppressClick === true, 'drag should suppress overlay close')
288
297
  handle.dispatch('pointerup', {})
289
- const saved = JSON.parse(sizeStore.get('dsh-plugin-table-zoom:popup-size'))
290
- assert.equal(saved.w, 960)
291
- assert.equal(saved.h, 460)
292
298
  assert.ok(popup._suppressClick === false, 'suppress flag reset after drag')
293
299
  mod.closePopup()
294
300
  })
295
301
 
296
- ok('applySavedLayout 复用上次记忆的布局', () => {
302
+ ok('每次打开不继承上次尺寸,始终按内容自适应', () => {
297
303
  mod.closePopup()
298
304
  const table = makeEl('table')
299
305
  table.rows = []
300
306
  mod.openPopup(table)
301
- const popup = documentEl.body.children.find((c) => c.className === 'dstz-popup')
302
- const panel = popup.children[0]
303
- const clone = panel.children[1].children[0]
304
- assert.equal(panel.style.width, '960px')
305
- assert.equal(panel.style.height, '460px')
306
- assert.equal(panel.style.maxHeight, 'none')
307
- assert.equal(panel.style.left, '0px')
308
- assert.equal(panel.style.top, '0px')
309
- assert.equal(clone.style.zoom, '1')
307
+ let popup = documentEl.body.children.find((c) => c.className === 'dstz-popup')
308
+ let panel = popup.children[0]
309
+ assert.equal(panel.style.width, '320px', '首次打开走自适应(假 DOM 无布局 → 下限 320)')
310
+ // 模拟当次手动改大
311
+ panel.style.width = '800px'
312
+ panel.style.height = '500px'
313
+ mod.closePopup()
314
+ // 重新打开:应回到自适应宽度,而不是继承 800px
315
+ mod.openPopup(table)
316
+ popup = documentEl.body.children.find((c) => c.className === 'dstz-popup')
317
+ panel = popup.children[0]
318
+ assert.equal(panel.style.width, '320px', '再次打开仍自适应,不继承上次尺寸')
319
+ assert.notEqual(panel.style.height, '500px')
310
320
  mod.closePopup()
311
321
  })
312
322
 
313
- ok('溢出时正文可拖拽平移', () => {
323
+ ok('溢出时左键拖动不启动平移(保留原生文本选择)', () => {
314
324
  mod.closePopup()
315
325
  const table = makeEl('table')
316
326
  table.rows = []
@@ -324,28 +334,61 @@ ok('溢出时正文可拖拽平移', () => {
324
334
  body.scrollLeft = 300
325
335
  body.scrollTop = 100
326
336
  mod.refreshGrabbable(body)
327
- assert.ok(body.classList.contains('dstz-grabbable'), 'overflow should show grab cursor')
337
+ assert.ok(!body.classList.contains('dstz-grabbable'), '未按空格时不应显示 grab 光标')
338
+ // 未按空格:左键拖动不进入平移,也不吞掉遮罩点击(让给浏览器原生框选文字)
328
339
  body.dispatch('pointerdown', { button: 0, clientX: 10, clientY: 10, pointerId: 1 })
329
340
  body.dispatch('pointermove', { clientX: 110, clientY: 60 })
330
- assert.ok(body.classList.contains('dstz-panning'), 'panning class while dragging')
341
+ assert.ok(!body.classList.contains('dstz-panning'), '未按空格拖动不应进入平移')
342
+ assert.equal(body.scrollLeft, 300)
343
+ assert.equal(body.scrollTop, 100)
344
+ assert.ok(popup._suppressClick !== true, '未按空格拖动不应吞掉遮罩点击')
345
+ body.dispatch('pointerup', {})
346
+ mod.closePopup()
347
+ })
348
+
349
+ ok('按住空格拖动可平移溢出内容,光标随空格状态切换', () => {
350
+ mod.closePopup()
351
+ const table = makeEl('table')
352
+ table.rows = []
353
+ mod.openPopup(table)
354
+ const popup = documentEl.body.children.find((c) => c.className === 'dstz-popup')
355
+ const body = popup.children[0].children[1]
356
+ body.scrollWidth = 1200
357
+ body.clientWidth = 600
358
+ body.scrollHeight = 400
359
+ body.clientHeight = 300
360
+ body.scrollLeft = 300
361
+ body.scrollTop = 100
362
+ // 按住空格:内容溢出时出现 grab 光标
363
+ globalThis.window.dispatch('keydown', { key: ' ', code: 'Space' })
364
+ assert.ok(body.classList.contains('dstz-grabbable'), '按住空格且溢出时应显示 grab 光标')
365
+ body.dispatch('pointerdown', { button: 0, clientX: 10, clientY: 10, pointerId: 1 })
366
+ body.dispatch('pointermove', { clientX: 110, clientY: 60 })
367
+ assert.ok(body.classList.contains('dstz-panning'), '按住空格拖动应进入平移')
331
368
  assert.equal(body.scrollLeft, 200)
332
369
  assert.equal(body.scrollTop, 50)
333
- assert.ok(popup._suppressClick === true, 'pan should suppress overlay close')
370
+ assert.ok(popup._suppressClick === true, '平移应吞掉遮罩关闭')
334
371
  body.dispatch('pointerup', {})
335
- assert.ok(!body.classList.contains('dstz-panning'), 'panning class cleared on release')
336
- assert.ok(popup._suppressClick === false, 'suppress flag reset after pan')
372
+ assert.ok(!body.classList.contains('dstz-panning'), '松手后退出平移')
373
+ assert.ok(popup._suppressClick === false, '平移结束恢复遮罩点击')
374
+ // 松开空格:grab 光标消失
375
+ globalThis.window.dispatch('keyup', { key: ' ', code: 'Space' })
376
+ assert.ok(!body.classList.contains('dstz-grabbable'), '松开空格后 grab 光标消失')
337
377
  mod.closePopup()
338
378
  })
339
379
 
340
380
  ok('标题栏可拖拽移动浮窗(不越出视口)', () => {
341
381
  mod.closePopup()
342
- sizeStore.delete('dsh-plugin-table-zoom:popup-size') // 清掉记忆,用默认尺寸测移动
343
382
  const table = makeEl('table')
344
383
  table.rows = []
345
384
  mod.openPopup(table)
346
385
  const popup = documentEl.body.children.find((c) => c.className === 'dstz-popup')
347
386
  const panel = popup.children[0]
348
387
  const header = panel.children[0]
388
+ assert.equal(panel.style.width, '320px', '打开即自适应(假 DOM 无布局 → 下限 320)')
389
+ // 假 DOM 无真实布局:显式给起始位置,验证拖拽按 delta 移动
390
+ panel.style.left = '160px'
391
+ panel.style.top = '40px'
349
392
  panel.getBoundingClientRect = () => ({
350
393
  width: parseInt(panel.style.width, 10) || 800,
351
394
  height: parseInt(panel.style.height, 10) || 400,
@@ -386,4 +429,19 @@ ok('Ctrl+滚轮缩放表格字体', () => {
386
429
  mod.closePopup()
387
430
  })
388
431
 
432
+ ok('applyAdaptiveWidth 按表格自然宽度自适应(窄表收缩、宽表铺满视口)', () => {
433
+ const panel = makeEl('div')
434
+ const clone = makeEl('table')
435
+ clone.offsetWidth = 500
436
+ mod.applyAdaptiveWidth(panel, clone)
437
+ assert.equal(panel.style.width, '538px')
438
+ // 超级大表:铺满视口可用宽度(假 DOM 视口 1280 - 留白 48 = 1232)
439
+ clone.offsetWidth = 3000
440
+ mod.applyAdaptiveWidth(panel, clone)
441
+ assert.equal(panel.style.width, '1232px')
442
+ clone.offsetWidth = 0
443
+ mod.applyAdaptiveWidth(panel, clone)
444
+ assert.equal(panel.style.width, '320px')
445
+ })
446
+
389
447
  console.log(`[dsh-plugin-table-zoom] smoke-client: ${passed} passed`)
@@ -47,15 +47,21 @@ cordis.patch.yml 挂载一行,交 profile 的 dsh.profile.bundles 自
47
47
  - 正文:独立滚动(横竖),表格克隆带 `dstz-table` 类,样式全部走主题 CSS 变量;
48
48
  - **改尺寸**:右下角有拖拽手柄,按住可自由调整浮窗大小(最小 320×200,
49
49
  上限为视口减去遮罩留白);
50
+ - **每次打开自适应**:浮窗**不记忆任何布局**——尺寸按表格自然宽度自适应
51
+ (下限 320,窄表不撑满、超级大表铺满视口可用宽度)、位置居中、缩放 100%;
52
+ 手动拖拽(改大小/移动/缩放)只影响当次打开,关掉再开即回到自适应;
50
53
  - **拖拽移动**:按住标题栏(按钮除外)可拖动浮窗位置,位置限制在视口内、
51
- 不越出页面;尺寸与位置都记忆到 `localStorage`,下次打开自动复用;
54
+ 不越出页面;
52
55
  - **Ctrl+滚轮缩放**:浮窗内按住 Ctrl 滚动滚轮可放大/缩小表格字体
53
- (60%–250%,步进 10%),标题栏实时显示百分比,缩放比例随布局一起记忆;
56
+ (60%–250%,步进 10%),标题栏实时显示百分比;缩放不记忆,
57
+ 每次打开都从 100% 开始;
54
58
  - **滚动条**:正文滚动条加深加粗(WebKit 伪元素 + Firefox `scrollbar-color`),
55
59
  浅色主题下也清晰可见;
56
- - **拖拽平移**:表格内容溢出正文时,鼠标/触控笔按住表格即可左右/上下拖动
57
- 滚动(grab/grabbing 光标提示);越过 4px 阈值才视为拖动,避免干扰文本
58
- 选择;触屏保留浏览器原生滚动;拖完松手落在遮罩上不会误关浮窗;
60
+ - **框选文字优先**:普通左键拖动保留浏览器原生文本选择(表格放大后也能
61
+ 直接框选文字),不再被平移劫持;
62
+ - **按住空格拖拽平移**:表格内容溢出正文时,按住空格再按住表格即可
63
+ 左右/上下拖动滚动(grab 光标仅在按住空格时出现,拖拽中为 grabbing;
64
+ 触屏保留浏览器原生滚动);拖完松手落在遮罩上不会误关浮窗;
59
65
  - **列宽上限**:与核心渲染器 `tableScroll` 一致(`max-width:min(30vw,320px)`、
60
66
  `min-width:100px`),超宽列在浮窗内收缩换行,表整体不再超出浮窗。
61
67
 
@@ -64,4 +70,4 @@ cordis.patch.yml 挂载一行,交 profile 的 dsh.profile.bundles 自
64
70
  - 只认 `tableScroll` 容器内的表格;浮窗内克隆表与 image-tools lightbox 内的表格跳过;
65
71
  - 不提供服务端工具/路由/配置;无 React 依赖(零运行时依赖);
66
72
  - 复制为 Markdown 优先 clipboard API,失败时 execCommand 兜底;
67
- - 尺寸记忆依赖 localStorage,隐私模式等不可用场景静默跳过(浮窗仍可用默认尺寸)。
73
+ - 不写 localStorage:浮窗布局(尺寸/位置/缩放)不跨次记忆,每次打开都自适应。