dsh-drawai 0.2.3 → 0.2.4
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/lib/client.js +124 -25
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -1258,7 +1258,7 @@ window.__ModuleLoader__.load({
|
|
|
1258
1258
|
// `height:100%` 要求父级有**确定高度**,而宿主给的容器不保证这一点。
|
|
1259
1259
|
// inset:0 只要求父级是定位元素,而 .drawai-pane 已经是(我们自己的 CSS),
|
|
1260
1260
|
// 所以这条链子只依赖我们自己,不再依赖宿主怎么排。
|
|
1261
|
-
'.drawai-root{display:flex;flex-direction:column;position:absolute;top:0;left:0;right:0;bottom:0;min-height:0;font-size:12px;color:var(--dsw-alias-label-primary,#e6e6e6)}',
|
|
1261
|
+
'.drawai-root{display:flex;flex-direction:column;position:absolute;top:0;left:0;right:0;bottom:0;min-height:0;font-size:12px;color:var(--dsw-alias-label-primary,#e6e6e6);outline:none}',
|
|
1262
1262
|
'.drawai-head{display:flex;align-items:center;gap:6px;padding:6px 8px;border-bottom:1px solid var(--dsw-alias-border-l1,#333)}',
|
|
1263
1263
|
// 多画布标签条。放在工具条**下面**一行:工具条是"对这张画布做什么",
|
|
1264
1264
|
// 标签条是"现在看哪张画布",两者职责不同,混在一行会挤成一团(右栏本来就窄)。
|
|
@@ -5832,15 +5832,16 @@ window.__ModuleLoader__.load({
|
|
|
5832
5832
|
/**
|
|
5833
5833
|
* 画布该不该处理这次按键。
|
|
5834
5834
|
*
|
|
5835
|
-
* 画布的快捷键挂在 `window`
|
|
5836
|
-
*
|
|
5837
|
-
* 两条判据,任何一条成立就放手:
|
|
5835
|
+
* 画布的快捷键挂在 `window` 上,代价是**别的面板里的按键也会经过这里** ——
|
|
5836
|
+
* 而 DSH 的输入框就在同一个页面里。两条判据,任何一条成立就放手:
|
|
5838
5837
|
*
|
|
5839
5838
|
* 1. 事件目标或当前焦点是**输入宿主**(见 isTextEntry)→ 那是人家在打字;
|
|
5840
5839
|
* 2. 焦点已经落在**别的控件**上(另一个面板的按钮、链接、树节点…)→ 也不抢。
|
|
5841
|
-
* 只有"焦点没了"
|
|
5840
|
+
* 只有"焦点没了"(焦点回到 body)或"焦点就在本面板里"才算画布的。
|
|
5842
5841
|
*
|
|
5843
|
-
*
|
|
5842
|
+
* **DSH 0.2 起不能再假设"点过画布焦点就回 body"**:对话是 Lexical contenteditable,
|
|
5843
|
+
* 点 SVG/画布空白常常**抢不走它的焦点**,于是这里一直判 false —— Delete / Ctrl+C/V
|
|
5844
|
+
* 全无反应。所以 `onRootPointerDown` 会主动把焦点收到 `.drawai-root`(tabIndex=-1)。
|
|
5844
5845
|
*/
|
|
5845
5846
|
function canvasOwnsKeyboard(event, root, active) {
|
|
5846
5847
|
const target = event === null || event === undefined ? null : event.target
|
|
@@ -5855,6 +5856,29 @@ window.__ModuleLoader__.load({
|
|
|
5855
5856
|
return root.contains(active) === true
|
|
5856
5857
|
}
|
|
5857
5858
|
|
|
5859
|
+
/**
|
|
5860
|
+
* 把键盘焦点收到画布面板根上(不抢就地编辑 / 菜单输入框)。
|
|
5861
|
+
*
|
|
5862
|
+
* 见 canvasOwnsKeyboard:DSH 0.2 点画布后焦点常仍停在对话 contenteditable,
|
|
5863
|
+
* 不主动 focus 的话快捷键整组静默失效。
|
|
5864
|
+
*/
|
|
5865
|
+
function claimCanvasKeyboard(root, target) {
|
|
5866
|
+
if (root === null || root === undefined || typeof root.focus !== 'function') return
|
|
5867
|
+
if (isTextEntry(target)) return
|
|
5868
|
+
if (target !== null && target !== undefined && typeof target.closest === 'function') {
|
|
5869
|
+
if (target.closest('.drawai-edit') !== null) return
|
|
5870
|
+
}
|
|
5871
|
+
try {
|
|
5872
|
+
if (typeof root.focus === 'function') root.focus({ preventScroll: true })
|
|
5873
|
+
} catch (error) {
|
|
5874
|
+
try {
|
|
5875
|
+
root.focus()
|
|
5876
|
+
} catch (error2) {
|
|
5877
|
+
// 测试环境 / 隐藏节点:静默
|
|
5878
|
+
}
|
|
5879
|
+
}
|
|
5880
|
+
}
|
|
5881
|
+
|
|
5858
5882
|
/**
|
|
5859
5883
|
* 剪贴板:**模块级**(不属于某个标签页)—— 在一个标签页里复制、切到另一个标签页粘贴,
|
|
5860
5884
|
* 是画布的常规用法,做成每个标签页一份反而奇怪。
|
|
@@ -8992,6 +9016,18 @@ window.__ModuleLoader__.load({
|
|
|
8992
9016
|
if (count === 0) setSaveNote('锁定层上的单元移不动:先解锁图层')
|
|
8993
9017
|
}
|
|
8994
9018
|
|
|
9019
|
+
/**
|
|
9020
|
+
* 快捷键字母是否匹配(兼容 event.key 与 event.code)。
|
|
9021
|
+
* 某些布局 / IME 下 Ctrl+C 的 key 不是可靠的 'c',code 仍是 KeyC。
|
|
9022
|
+
*/
|
|
9023
|
+
function accelLetter(event, letter) {
|
|
9024
|
+
if (event === null || event === undefined) return false
|
|
9025
|
+
const upper = String(letter).toUpperCase()
|
|
9026
|
+
if (event.code === 'Key' + upper) return true
|
|
9027
|
+
const key = event.key
|
|
9028
|
+
return key === letter || key === upper
|
|
9029
|
+
}
|
|
9030
|
+
|
|
8995
9031
|
React.useEffect(() => {
|
|
8996
9032
|
function onKey(event) {
|
|
8997
9033
|
// 归属判定见 canvasOwnsKeyboard:**在别处打字时这里一律不动手**。
|
|
@@ -9009,44 +9045,51 @@ window.__ModuleLoader__.load({
|
|
|
9009
9045
|
// 半路踩过的坑:**画布里有选中元素时不能放手**。用户先在对话里划了字、再点画布上的
|
|
9010
9046
|
// 元素,那次点击不会清掉别处的选区,于是"外部有选区"成立、Ctrl+C 被让给浏览器,
|
|
9011
9047
|
// 画布里的元素就**复制不了**了。所以判据是两条同时成立:画布空着 **且** 外面有选中的字。
|
|
9012
|
-
if (accel && (event
|
|
9048
|
+
if (accel && (accelLetter(event, 'c') || accelLetter(event, 'x') || accelLetter(event, 'a'))) {
|
|
9013
9049
|
if (selectedIds.length === 0 && hasTextSelectionOutside(rootRef.current)) return
|
|
9014
9050
|
}
|
|
9015
|
-
if (accel && (event
|
|
9051
|
+
if (accel && accelLetter(event, 'z')) {
|
|
9016
9052
|
event.preventDefault()
|
|
9017
9053
|
if (event.shiftKey === true) redo()
|
|
9018
9054
|
else undo()
|
|
9019
9055
|
return
|
|
9020
9056
|
}
|
|
9021
|
-
if (accel && (event
|
|
9057
|
+
if (accel && accelLetter(event, 'y')) {
|
|
9022
9058
|
event.preventDefault()
|
|
9023
9059
|
redo()
|
|
9024
9060
|
return
|
|
9025
9061
|
}
|
|
9026
|
-
// 复制 /
|
|
9027
|
-
//
|
|
9028
|
-
if (accel && (event
|
|
9062
|
+
// 复制 / 剪切:keydown 是兜底;主通道是下面的 copy/cut 捕获监听
|
|
9063
|
+
// (DSH 0.2 有时在冒泡阶段先截走 Ctrl+C,keydown 冒泡听不到)。
|
|
9064
|
+
if (accel && accelLetter(event, 'c')) {
|
|
9029
9065
|
event.preventDefault()
|
|
9066
|
+
event.stopPropagation()
|
|
9030
9067
|
copySelection()
|
|
9031
9068
|
return
|
|
9032
9069
|
}
|
|
9033
|
-
if (accel && (event
|
|
9070
|
+
if (accel && accelLetter(event, 'x')) {
|
|
9034
9071
|
event.preventDefault()
|
|
9072
|
+
event.stopPropagation()
|
|
9035
9073
|
cutSelection()
|
|
9036
9074
|
return
|
|
9037
9075
|
}
|
|
9038
|
-
// Ctrl+V
|
|
9039
|
-
//
|
|
9040
|
-
if (accel && (event
|
|
9076
|
+
// Ctrl+V:有画布内剪贴板时直接粘贴(不要等 paste 事件 —— 焦点若在中间被抢走会丢)。
|
|
9077
|
+
// 剪贴板空时不 preventDefault,留给 paste 事件接系统图片。
|
|
9078
|
+
if (accel && accelLetter(event, 'v')) {
|
|
9079
|
+
if (clipboard !== null) {
|
|
9080
|
+
event.preventDefault()
|
|
9081
|
+
event.stopPropagation()
|
|
9082
|
+
pasteClipboard()
|
|
9083
|
+
}
|
|
9041
9084
|
return
|
|
9042
9085
|
}
|
|
9043
9086
|
// 全选:与框选同一套语义(节点与连线都选上),于是能整体拖动/换样式/一把删掉。
|
|
9044
|
-
if (accel && (event
|
|
9087
|
+
if (accel && accelLetter(event, 'a')) {
|
|
9045
9088
|
event.preventDefault()
|
|
9046
9089
|
selectAll()
|
|
9047
9090
|
return
|
|
9048
9091
|
}
|
|
9049
|
-
if (event.key === 'Delete' || event.key === 'Backspace') {
|
|
9092
|
+
if (event.key === 'Delete' || event.key === 'Backspace' || event.code === 'Delete' || event.code === 'Backspace') {
|
|
9050
9093
|
event.preventDefault()
|
|
9051
9094
|
deleteSelected()
|
|
9052
9095
|
return
|
|
@@ -9056,12 +9099,12 @@ window.__ModuleLoader__.load({
|
|
|
9056
9099
|
nudgeSelectionByKey(event.key, event.shiftKey === true)
|
|
9057
9100
|
return
|
|
9058
9101
|
}
|
|
9059
|
-
if (accel && (event
|
|
9102
|
+
if (accel && accelLetter(event, 'f')) {
|
|
9060
9103
|
event.preventDefault()
|
|
9061
9104
|
openFindPanel()
|
|
9062
9105
|
return
|
|
9063
9106
|
}
|
|
9064
|
-
if (event.key === 'Escape') {
|
|
9107
|
+
if (event.key === 'Escape' || event.code === 'Escape') {
|
|
9065
9108
|
setSelectedIds([])
|
|
9066
9109
|
setEditing(null)
|
|
9067
9110
|
setMenu(null)
|
|
@@ -9075,10 +9118,55 @@ window.__ModuleLoader__.load({
|
|
|
9075
9118
|
setDocMenuPos(null)
|
|
9076
9119
|
}
|
|
9077
9120
|
}
|
|
9078
|
-
|
|
9079
|
-
|
|
9121
|
+
// 捕获阶段:抢在 DSH / 其他插件的冒泡监听之前(Ctrl+C 曾被截走导致"能删不能复制")。
|
|
9122
|
+
window.addEventListener('keydown', onKey, true)
|
|
9123
|
+
return () => window.removeEventListener('keydown', onKey, true)
|
|
9080
9124
|
}, [selectedIds, editing, menu, placeTool, placeEdgeKind])
|
|
9081
9125
|
|
|
9126
|
+
// 复制 / 剪切的主通道:浏览器 `copy`/`cut` 事件(捕获)。
|
|
9127
|
+
// 比 keydown 更稳 —— 只要用户按了系统复制快捷键就会到这里,不依赖 event.key 字形。
|
|
9128
|
+
React.useEffect(() => {
|
|
9129
|
+
function ownsCopyCut(event) {
|
|
9130
|
+
if (!canvasOwnsKeyboard(event, rootRef.current, document.activeElement)) return false
|
|
9131
|
+
if (selectedIds.length === 0) return false
|
|
9132
|
+
// 画布有选中单元时,复制/剪切归画布(即使页面上还残留对话文字选区)。
|
|
9133
|
+
return true
|
|
9134
|
+
}
|
|
9135
|
+
function onCopy(event) {
|
|
9136
|
+
if (!ownsCopyCut(event)) return
|
|
9137
|
+
const clip = collectClipboard(docRef.current, selectedIds)
|
|
9138
|
+
if (clip === null) return
|
|
9139
|
+
event.preventDefault()
|
|
9140
|
+
event.stopPropagation()
|
|
9141
|
+
clipboard = clip
|
|
9142
|
+
setMenu(null)
|
|
9143
|
+
setSaveNote('已复制 ' + clip.nodes.length + ' 个节点' + (clip.edges.length > 0 ? ' / ' + clip.edges.length + ' 条连线' : ''))
|
|
9144
|
+
try {
|
|
9145
|
+
if (event.clipboardData !== null && event.clipboardData !== undefined) {
|
|
9146
|
+
event.clipboardData.setData('text/plain', 'DrawAI clipboard: ' + clip.nodes.length + ' node(s)')
|
|
9147
|
+
}
|
|
9148
|
+
} catch (error) {
|
|
9149
|
+
// 系统剪贴板写失败不影响画布内 clipboard
|
|
9150
|
+
}
|
|
9151
|
+
}
|
|
9152
|
+
function onCut(event) {
|
|
9153
|
+
if (!ownsCopyCut(event)) return
|
|
9154
|
+
const clip = collectClipboard(docRef.current, selectedIds)
|
|
9155
|
+
if (clip === null) return
|
|
9156
|
+
event.preventDefault()
|
|
9157
|
+
event.stopPropagation()
|
|
9158
|
+
clipboard = clip
|
|
9159
|
+
setSaveNote('已剪切 ' + clip.nodes.length + ' 个节点' + (clip.edges.length > 0 ? ' / ' + clip.edges.length + ' 条连线' : ''))
|
|
9160
|
+
deleteSelected()
|
|
9161
|
+
}
|
|
9162
|
+
document.addEventListener('copy', onCopy, true)
|
|
9163
|
+
document.addEventListener('cut', onCut, true)
|
|
9164
|
+
return () => {
|
|
9165
|
+
document.removeEventListener('copy', onCopy, true)
|
|
9166
|
+
document.removeEventListener('cut', onCut, true)
|
|
9167
|
+
}
|
|
9168
|
+
}, [selectedIds])
|
|
9169
|
+
|
|
9082
9170
|
// 系统剪贴板粘贴:有图片就落成 image 节点;否则回落到画布内剪贴板(节点/边)。
|
|
9083
9171
|
React.useEffect(() => {
|
|
9084
9172
|
function onPaste(event) {
|
|
@@ -9097,8 +9185,8 @@ window.__ModuleLoader__.load({
|
|
|
9097
9185
|
pasteClipboard()
|
|
9098
9186
|
}
|
|
9099
9187
|
}
|
|
9100
|
-
window.addEventListener('paste', onPaste)
|
|
9101
|
-
return () => window.removeEventListener('paste', onPaste)
|
|
9188
|
+
window.addEventListener('paste', onPaste, true)
|
|
9189
|
+
return () => window.removeEventListener('paste', onPaste, true)
|
|
9102
9190
|
}, [editing, selectedIds, activeLayerId])
|
|
9103
9191
|
|
|
9104
9192
|
React.useEffect(() => {
|
|
@@ -10970,6 +11058,9 @@ window.__ModuleLoader__.load({
|
|
|
10970
11058
|
// ③ 在画布上按下 = "我要操作画布了":把残留在别处(比如对话里)的文字选区清掉。
|
|
10971
11059
|
// 不清的话,画布的新元素会因为"外面还有选中文字"而复制不了(见 clearStaleTextSelection)。
|
|
10972
11060
|
clearStaleTextSelection(target, event === null || event === undefined ? undefined : event.button)
|
|
11061
|
+
// ④ 同时把焦点收到面板根:否则 DSH 0.2 下对话 contenteditable 不放焦点,
|
|
11062
|
+
// canvasOwnsKeyboard 一直 false,Delete / Ctrl+C/V 全部没反应。
|
|
11063
|
+
claimCanvasKeyboard(rootRef.current, target)
|
|
10973
11064
|
if (editing !== null && (closest === null || closest('.drawai-edit') === null)) commitEdit()
|
|
10974
11065
|
if (docMenu === null) return
|
|
10975
11066
|
if (closest !== null && closest('.drawai-menu') !== null) return
|
|
@@ -11325,7 +11416,14 @@ window.__ModuleLoader__.load({
|
|
|
11325
11416
|
|
|
11326
11417
|
return React.createElement(
|
|
11327
11418
|
'div',
|
|
11328
|
-
{
|
|
11419
|
+
{
|
|
11420
|
+
className: 'drawai-root',
|
|
11421
|
+
ref: rootRef,
|
|
11422
|
+
// 可聚焦但不进 Tab 序:点画布时 claimCanvasKeyboard 把焦点收到这里,
|
|
11423
|
+
// 快捷键才能通过 canvasOwnsKeyboard(见该函数注释)。
|
|
11424
|
+
tabIndex: -1,
|
|
11425
|
+
onPointerDownCapture: onRootPointerDown,
|
|
11426
|
+
},
|
|
11329
11427
|
// 1) 工作栏(文件/编辑/视图/导出)—— 层级在标签页**之上**
|
|
11330
11428
|
head,
|
|
11331
11429
|
// 2) 标签页:由 CanvasTabs 构造、只在活动窗格里渲染(保证 DOM 里只有一份)
|
|
@@ -11755,6 +11853,7 @@ window.__ModuleLoader__.load({
|
|
|
11755
11853
|
// 按键归属:谁在打字、这次按键算不算画布的 —— 纯函数,自测直接喂假 DOM 节点。
|
|
11756
11854
|
isTextEntry: isTextEntry,
|
|
11757
11855
|
canvasOwnsKeyboard: canvasOwnsKeyboard,
|
|
11856
|
+
claimCanvasKeyboard: claimCanvasKeyboard,
|
|
11758
11857
|
// 「你有没有选中画布之外的文字」——决定 Ctrl+C/X/A 放不放手让浏览器去复制。
|
|
11759
11858
|
hasTextSelectionOutside: hasTextSelectionOutside,
|
|
11760
11859
|
drawaiPaneOf: drawaiPaneOf,
|