@8btc/mditor 0.0.7 → 0.0.9
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/dist/index.css +56 -2
- package/dist/index.d.ts +9 -0
- package/dist/index.js +3289 -2841
- package/dist/index.min.js +1 -1
- package/dist/method.d.ts +14 -10
- package/dist/method.js +527 -3
- package/dist/method.min.js +1 -1
- package/dist/ts/util/editorCommonEvent.d.ts +3 -2
- package/dist/ts/wysiwyg/index.d.ts +29 -0
- package/dist/types/index.d.ts +18 -2
- package/package.json +1 -1
- package/src/assets/less/_line-number.less +6 -1
- package/src/assets/less/_selection-popover.less +148 -0
- package/src/assets/less/_selection-tag.less +38 -0
- package/src/assets/less/index.less +2 -0
- package/src/index.ts +58 -3
- package/src/method.ts +1 -1
- package/src/ts/ir/index.ts +1 -1
- package/src/ts/ir/process.ts +1 -1
- package/src/ts/markdown/selectionRender.ts +28 -0
- package/src/ts/preview/index.ts +6 -5
- package/src/ts/sv/index.ts +1 -1
- package/src/ts/sv/process.ts +1 -1
- package/src/ts/ui/initUI.ts +4 -2
- package/src/ts/util/Options.ts +8 -1
- package/src/ts/util/editorCommonEvent.ts +296 -184
- package/src/ts/util/processCode.ts +43 -24
- package/src/ts/wysiwyg/afterRenderEvent.ts +1 -1
- package/src/ts/wysiwyg/index.ts +509 -119
- package/src/ts/wysiwyg/renderDomByMd.ts +1 -1
package/src/ts/wysiwyg/index.ts
CHANGED
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
} from "../util/hasClosest";
|
|
22
22
|
import { hasClosestByHeadings } from "../util/hasClosestByHeadings";
|
|
23
23
|
import {
|
|
24
|
-
getCursorPosition,
|
|
25
24
|
getEditorRange,
|
|
26
25
|
getSelectPosition,
|
|
27
26
|
setRangeByWbr,
|
|
@@ -45,36 +44,93 @@ class WYSIWYG {
|
|
|
45
44
|
public element: HTMLPreElement;
|
|
46
45
|
public popover: HTMLDivElement;
|
|
47
46
|
public selectPopover: HTMLDivElement;
|
|
47
|
+
public popoverInput: HTMLTextAreaElement;
|
|
48
|
+
public popoverSendBtn: HTMLButtonElement;
|
|
49
|
+
public selectionContent: string;
|
|
50
|
+
public selectionLines: [number, number];
|
|
48
51
|
public afterRenderTimeoutId: number;
|
|
49
52
|
public hlToolbarTimeoutId: number;
|
|
50
53
|
public preventInput: boolean;
|
|
51
54
|
public composingLock = false;
|
|
52
55
|
public commentIds: string[] = [];
|
|
56
|
+
private vditor: IVditor;
|
|
53
57
|
private scrollListener: () => void;
|
|
54
58
|
|
|
55
59
|
constructor(vditor: IVditor) {
|
|
60
|
+
this.vditor = vditor;
|
|
56
61
|
const divElement = document.createElement("div");
|
|
57
62
|
divElement.className = "vditor-wysiwyg";
|
|
58
|
-
if (vditor.options.lineNumber) {
|
|
63
|
+
if (vditor.options.lineNumber?.enable) {
|
|
59
64
|
divElement.classList.add("vditor--linenumber");
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
divElement.innerHTML = `<pre class="vditor-reset" placeholder="${vditor.options.placeholder}"
|
|
63
68
|
contenteditable="true" spellcheck="false"></pre>
|
|
64
69
|
<div class="vditor-panel vditor-panel--none"></div>
|
|
65
|
-
<div class="vditor-
|
|
66
|
-
<
|
|
67
|
-
<
|
|
68
|
-
|
|
70
|
+
<div class="vditor-selection-popover">
|
|
71
|
+
<div class="vditor-selection-popover__actions">
|
|
72
|
+
<button type="button" data-action="ai" aria-label="AI编辑" class="vditor-selection-popover__btn" disabled>✨ AI编辑</button>
|
|
73
|
+
<button type="button" data-action="cut" aria-label="剪切" class="vditor-selection-popover__btn">剪切</button>
|
|
74
|
+
<button type="button" data-action="copy" aria-label="复制" class="vditor-selection-popover__btn">复制</button>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="vditor-selection-popover__input">
|
|
77
|
+
<textarea placeholder="请输入您想修改的内容"></textarea>
|
|
78
|
+
<button class="vditor-selection-popover__send">
|
|
79
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sparkles-icon lucide-sparkles"><path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/><path d="M20 2v4"/><path d="M22 4h-4"/><circle cx="4" cy="20" r="2"/></svg>
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
69
82
|
</div>`;
|
|
70
83
|
|
|
71
84
|
this.element = divElement.firstElementChild as HTMLPreElement;
|
|
72
85
|
this.popover = divElement.firstElementChild
|
|
73
86
|
.nextElementSibling as HTMLDivElement;
|
|
74
87
|
this.selectPopover = divElement.lastElementChild as HTMLDivElement;
|
|
88
|
+
this.popoverInput = this.selectPopover.querySelector(
|
|
89
|
+
"textarea"
|
|
90
|
+
) as HTMLTextAreaElement;
|
|
91
|
+
this.popoverSendBtn = this.selectPopover.querySelector(
|
|
92
|
+
".vditor-selection-popover__send"
|
|
93
|
+
) as HTMLButtonElement;
|
|
75
94
|
|
|
76
95
|
this.bindEvent(vditor);
|
|
77
96
|
|
|
97
|
+
// 绑定发送按钮事件
|
|
98
|
+
if (this.popoverSendBtn) {
|
|
99
|
+
this.popoverSendBtn.setAttribute("disabled", "disabled");
|
|
100
|
+
|
|
101
|
+
this.popoverSendBtn.onclick = (event) => {
|
|
102
|
+
event.stopPropagation();
|
|
103
|
+
if (vditor.options.ai) {
|
|
104
|
+
vditor.options.ai({
|
|
105
|
+
value: this.popoverInput.value,
|
|
106
|
+
content: this.selectionContent,
|
|
107
|
+
lines: this.selectionLines,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
this.hideSelectionPopover();
|
|
111
|
+
};
|
|
112
|
+
// 阻止点击输入区域时清除选区
|
|
113
|
+
this.selectPopover
|
|
114
|
+
.querySelector(".vditor-selection-popover__input")
|
|
115
|
+
?.addEventListener("mousedown", (event) => {
|
|
116
|
+
event.stopPropagation();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// 输入验证
|
|
120
|
+
if (this.popoverInput) {
|
|
121
|
+
this.popoverInput.addEventListener("input", () => {
|
|
122
|
+
if (this.popoverInput.value.trim().length > 0) {
|
|
123
|
+
this.popoverSendBtn.removeAttribute("disabled");
|
|
124
|
+
} else {
|
|
125
|
+
this.popoverSendBtn.setAttribute(
|
|
126
|
+
"disabled",
|
|
127
|
+
"disabled"
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
78
134
|
focusEvent(vditor, this.element);
|
|
79
135
|
dblclickEvent(vditor, this.element);
|
|
80
136
|
blurEvent(vditor, this.element);
|
|
@@ -84,121 +140,177 @@ class WYSIWYG {
|
|
|
84
140
|
copyEvent(vditor, this.element, this.copy);
|
|
85
141
|
cutEvent(vditor, this.element, this.copy);
|
|
86
142
|
|
|
143
|
+
// 选择浮窗按钮事件绑定
|
|
144
|
+
const aiBtn = this.selectPopover.querySelector(
|
|
145
|
+
'[data-action="ai"]'
|
|
146
|
+
) as HTMLButtonElement | null;
|
|
147
|
+
if (aiBtn) {
|
|
148
|
+
/**
|
|
149
|
+
* AI编辑按钮占位事件
|
|
150
|
+
* - 当前仅输出日志,不执行编辑逻辑
|
|
151
|
+
*/
|
|
152
|
+
aiBtn.onclick = () => {
|
|
153
|
+
console.log("[Selection AI Edit] clicked");
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const copyBtn = this.selectPopover.querySelector(
|
|
157
|
+
'[data-action="copy"]'
|
|
158
|
+
) as HTMLButtonElement | null;
|
|
159
|
+
if (copyBtn) {
|
|
160
|
+
/**
|
|
161
|
+
* 复制按钮点击处理
|
|
162
|
+
* - 优先使用 Clipboard API;降级为 execCommand('copy')
|
|
163
|
+
*/
|
|
164
|
+
copyBtn.onclick = () => {
|
|
165
|
+
this.copySelection(vditor);
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const cutBtn = this.selectPopover.querySelector(
|
|
169
|
+
'[data-action="cut"]'
|
|
170
|
+
) as HTMLButtonElement | null;
|
|
171
|
+
if (cutBtn) {
|
|
172
|
+
/**
|
|
173
|
+
* 剪切按钮点击处理
|
|
174
|
+
* - 将选区复制到剪贴板后删除,并接入撤销栈
|
|
175
|
+
*/
|
|
176
|
+
cutBtn.onclick = () => {
|
|
177
|
+
this.cutSelection(vditor);
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 评论按钮仅在启用时注册(避免选择浮窗结构变化导致报错)
|
|
87
182
|
if (vditor.options.comment.enable) {
|
|
88
|
-
this.selectPopover.querySelector(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
wrap =
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
"data-cmtids",
|
|
107
|
-
item.getAttribute("data-cmtids") + " " + id
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
if (wrap) {
|
|
111
|
-
if (
|
|
112
|
-
item.nodeType !== 3 &&
|
|
113
|
-
item.getAttribute("data-block") === "0" &&
|
|
114
|
-
index === 0 &&
|
|
115
|
-
rangeClone.startOffset > 0
|
|
116
|
-
) {
|
|
117
|
-
item.innerHTML = `<span class="vditor-comment" data-cmtids="${id}">${item.innerHTML}</span>`;
|
|
118
|
-
blockStartElement = item;
|
|
183
|
+
const commentBtn = this.selectPopover.querySelector(
|
|
184
|
+
'[data-action="comment"]'
|
|
185
|
+
) as HTMLButtonElement | null;
|
|
186
|
+
if (commentBtn) {
|
|
187
|
+
commentBtn.onclick = () => {
|
|
188
|
+
const id = Lute.NewNodeID();
|
|
189
|
+
const range = getSelection().getRangeAt(0);
|
|
190
|
+
const rangeClone = range.cloneRange();
|
|
191
|
+
const contents = range.extractContents();
|
|
192
|
+
let blockStartElement: HTMLElement;
|
|
193
|
+
let blockEndElement: HTMLElement;
|
|
194
|
+
let removeStart = false;
|
|
195
|
+
let removeEnd = false;
|
|
196
|
+
contents.childNodes.forEach(
|
|
197
|
+
(item: HTMLElement, index: number) => {
|
|
198
|
+
let wrap = false;
|
|
199
|
+
if (item.nodeType === 3) {
|
|
200
|
+
wrap = true;
|
|
119
201
|
} else if (
|
|
120
|
-
item.
|
|
121
|
-
item.getAttribute("data-block") === "0" &&
|
|
122
|
-
index === contents.childNodes.length - 1 &&
|
|
123
|
-
rangeClone.endOffset <
|
|
124
|
-
rangeClone.endContainer.textContent.length
|
|
202
|
+
!item.classList.contains("vditor-comment")
|
|
125
203
|
) {
|
|
126
|
-
|
|
127
|
-
blockEndElement = item;
|
|
204
|
+
wrap = true;
|
|
128
205
|
} else if (
|
|
129
|
-
item.
|
|
130
|
-
item.getAttribute("data-block") === "0"
|
|
206
|
+
item.classList.contains("vditor-comment")
|
|
131
207
|
) {
|
|
132
|
-
|
|
133
|
-
|
|
208
|
+
item.setAttribute(
|
|
209
|
+
"data-cmtids",
|
|
210
|
+
item.getAttribute("data-cmtids") + " " + id
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (wrap) {
|
|
214
|
+
if (
|
|
215
|
+
item.nodeType !== 3 &&
|
|
216
|
+
item.getAttribute("data-block") === "0" &&
|
|
217
|
+
index === 0 &&
|
|
218
|
+
rangeClone.startOffset > 0
|
|
219
|
+
) {
|
|
220
|
+
item.innerHTML = `<span class="vditor-comment" data-cmtids="${id}">${item.innerHTML}</span>`;
|
|
221
|
+
blockStartElement = item;
|
|
134
222
|
} else if (
|
|
135
|
-
|
|
136
|
-
|
|
223
|
+
item.nodeType !== 3 &&
|
|
224
|
+
item.getAttribute("data-block") === "0" &&
|
|
225
|
+
index === contents.childNodes.length - 1 &&
|
|
226
|
+
rangeClone.endOffset <
|
|
227
|
+
rangeClone.endContainer.textContent
|
|
228
|
+
.length
|
|
137
229
|
) {
|
|
138
|
-
|
|
230
|
+
item.innerHTML = `<span class="vditor-comment" data-cmtids="${id}">${item.innerHTML}</span>`;
|
|
231
|
+
blockEndElement = item;
|
|
232
|
+
} else if (
|
|
233
|
+
item.nodeType !== 3 &&
|
|
234
|
+
item.getAttribute("data-block") === "0"
|
|
235
|
+
) {
|
|
236
|
+
if (index === 0) {
|
|
237
|
+
removeStart = true;
|
|
238
|
+
} else if (
|
|
239
|
+
index ===
|
|
240
|
+
contents.childNodes.length - 1
|
|
241
|
+
) {
|
|
242
|
+
removeEnd = true;
|
|
243
|
+
}
|
|
244
|
+
item.innerHTML = `<span class="vditor-comment" data-cmtids="${id}">${item.innerHTML}</span>`;
|
|
245
|
+
} else {
|
|
246
|
+
const commentElement =
|
|
247
|
+
document.createElement("span");
|
|
248
|
+
commentElement.classList.add(
|
|
249
|
+
"vditor-comment"
|
|
250
|
+
);
|
|
251
|
+
commentElement.setAttribute(
|
|
252
|
+
"data-cmtids",
|
|
253
|
+
id
|
|
254
|
+
);
|
|
255
|
+
item.parentNode.insertBefore(
|
|
256
|
+
commentElement,
|
|
257
|
+
item
|
|
258
|
+
);
|
|
259
|
+
commentElement.appendChild(item);
|
|
139
260
|
}
|
|
140
|
-
item.innerHTML = `<span class="vditor-comment" data-cmtids="${id}">${item.innerHTML}</span>`;
|
|
141
|
-
} else {
|
|
142
|
-
const commentElement =
|
|
143
|
-
document.createElement("span");
|
|
144
|
-
commentElement.classList.add("vditor-comment");
|
|
145
|
-
commentElement.setAttribute("data-cmtids", id);
|
|
146
|
-
item.parentNode.insertBefore(
|
|
147
|
-
commentElement,
|
|
148
|
-
item
|
|
149
|
-
);
|
|
150
|
-
commentElement.appendChild(item);
|
|
151
261
|
}
|
|
152
262
|
}
|
|
263
|
+
);
|
|
264
|
+
const startElement = hasClosestBlock(
|
|
265
|
+
rangeClone.startContainer
|
|
266
|
+
);
|
|
267
|
+
if (startElement) {
|
|
268
|
+
if (blockStartElement) {
|
|
269
|
+
startElement.insertAdjacentHTML(
|
|
270
|
+
"beforeend",
|
|
271
|
+
blockStartElement.innerHTML
|
|
272
|
+
);
|
|
273
|
+
blockStartElement.remove();
|
|
274
|
+
} else if (
|
|
275
|
+
startElement.textContent
|
|
276
|
+
.trim()
|
|
277
|
+
.replace(Constants.ZWSP, "") === "" &&
|
|
278
|
+
removeStart
|
|
279
|
+
) {
|
|
280
|
+
startElement.remove();
|
|
281
|
+
}
|
|
153
282
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
const endElement = hasClosestBlock(rangeClone.endContainer);
|
|
173
|
-
if (endElement) {
|
|
174
|
-
if (blockEndElement) {
|
|
175
|
-
endElement.insertAdjacentHTML(
|
|
176
|
-
"afterbegin",
|
|
177
|
-
blockEndElement.innerHTML
|
|
178
|
-
);
|
|
179
|
-
blockEndElement.remove();
|
|
180
|
-
} else if (
|
|
181
|
-
endElement.textContent
|
|
182
|
-
.trim()
|
|
183
|
-
.replace(Constants.ZWSP, "") === "" &&
|
|
184
|
-
removeEnd
|
|
185
|
-
) {
|
|
186
|
-
endElement.remove();
|
|
283
|
+
const endElement = hasClosestBlock(rangeClone.endContainer);
|
|
284
|
+
if (endElement) {
|
|
285
|
+
if (blockEndElement) {
|
|
286
|
+
endElement.insertAdjacentHTML(
|
|
287
|
+
"afterbegin",
|
|
288
|
+
blockEndElement.innerHTML
|
|
289
|
+
);
|
|
290
|
+
blockEndElement.remove();
|
|
291
|
+
} else if (
|
|
292
|
+
endElement.textContent
|
|
293
|
+
.trim()
|
|
294
|
+
.replace(Constants.ZWSP, "") === "" &&
|
|
295
|
+
removeEnd
|
|
296
|
+
) {
|
|
297
|
+
endElement.remove();
|
|
298
|
+
}
|
|
187
299
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
300
|
+
range.insertNode(contents);
|
|
301
|
+
vditor.options.comment.add(
|
|
302
|
+
id,
|
|
303
|
+
range.toString(),
|
|
304
|
+
this.getComments(vditor, true)
|
|
305
|
+
);
|
|
306
|
+
afterRenderEvent(vditor, {
|
|
307
|
+
enableAddUndoStack: true,
|
|
308
|
+
enableHint: false,
|
|
309
|
+
enableInput: false,
|
|
310
|
+
});
|
|
311
|
+
this.hideSelectionPopover();
|
|
312
|
+
};
|
|
313
|
+
}
|
|
202
314
|
}
|
|
203
315
|
}
|
|
204
316
|
|
|
@@ -250,16 +362,141 @@ class WYSIWYG {
|
|
|
250
362
|
}
|
|
251
363
|
}
|
|
252
364
|
|
|
365
|
+
/**
|
|
366
|
+
* 显示选择浮窗(定位于选区右上)
|
|
367
|
+
* - 当未选中文本或选区不在编辑器内时不显示
|
|
368
|
+
*/
|
|
369
|
+
public showSelectionPopover() {
|
|
370
|
+
if (getSelection().rangeCount === 0) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const range = getSelection().getRangeAt(0);
|
|
374
|
+
if (range.toString().trim() === "") {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// 捕获选区内容(在选区丢失前)
|
|
379
|
+
const fragment = range.cloneContents();
|
|
380
|
+
const tempContainer = document.createElement("div");
|
|
381
|
+
tempContainer.appendChild(fragment);
|
|
382
|
+
this.selectionContent = this.vditor.lute
|
|
383
|
+
.VditorDOM2Md(tempContainer.innerHTML)
|
|
384
|
+
.trim();
|
|
385
|
+
if (!this.selectionContent) {
|
|
386
|
+
this.selectionContent = range.toString().trim();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// 捕获行号
|
|
390
|
+
const getBlockIndex = (node: Node, editor: HTMLElement): number => {
|
|
391
|
+
let target = node;
|
|
392
|
+
let directChild: Element | null = null;
|
|
393
|
+
|
|
394
|
+
// 向上遍历查找行号或直接子元素
|
|
395
|
+
while (target && target.parentElement !== editor) {
|
|
396
|
+
if (
|
|
397
|
+
target instanceof Element &&
|
|
398
|
+
target.hasAttribute("data-linenumber")
|
|
399
|
+
) {
|
|
400
|
+
return parseInt(target.getAttribute("data-linenumber"), 10);
|
|
401
|
+
}
|
|
402
|
+
target = target.parentNode;
|
|
403
|
+
if (target && target.parentElement === editor) {
|
|
404
|
+
directChild = target as Element;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// 检查直接子元素
|
|
409
|
+
if (directChild && directChild.hasAttribute("data-linenumber")) {
|
|
410
|
+
return parseInt(
|
|
411
|
+
directChild.getAttribute("data-linenumber"),
|
|
412
|
+
10
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// 回退到使用子元素索引
|
|
417
|
+
if (directChild) {
|
|
418
|
+
return Array.from(editor.children).indexOf(directChild) + 1;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return 0;
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const startLine = getBlockIndex(range.startContainer, this.element);
|
|
425
|
+
const endLine = getBlockIndex(range.endContainer, this.element);
|
|
426
|
+
this.selectionLines = [startLine, endLine];
|
|
427
|
+
|
|
428
|
+
const editorRect = this.element.getBoundingClientRect();
|
|
429
|
+
const rect = range.getBoundingClientRect();
|
|
430
|
+
|
|
431
|
+
// 计算浮窗尺寸
|
|
432
|
+
this.selectPopover.style.display = "flex";
|
|
433
|
+
this.selectPopover.style.opacity = "0";
|
|
434
|
+
const popoverHeight = this.selectPopover.offsetHeight;
|
|
435
|
+
const popoverWidth = this.selectPopover.offsetWidth;
|
|
436
|
+
|
|
437
|
+
// 理想位置:选区上方居中
|
|
438
|
+
let top = rect.top - editorRect.top - popoverHeight - 8;
|
|
439
|
+
let left =
|
|
440
|
+
rect.left - editorRect.left + rect.width / 2 - popoverWidth / 2;
|
|
441
|
+
|
|
442
|
+
// 边界检查:顶部超出则翻转到底部
|
|
443
|
+
if (top < 0) {
|
|
444
|
+
top = rect.bottom - editorRect.top + 8;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// 边界检查:左右限制
|
|
448
|
+
const maxLeft = this.element.clientWidth - popoverWidth;
|
|
449
|
+
left = Math.max(0, Math.min(left, maxLeft));
|
|
450
|
+
|
|
451
|
+
this.selectPopover.style.top = `${top}px`;
|
|
452
|
+
this.selectPopover.style.left = `${left}px`;
|
|
453
|
+
|
|
454
|
+
// 强制重排以触发过渡动画
|
|
455
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
456
|
+
this.selectPopover.offsetHeight;
|
|
457
|
+
|
|
458
|
+
this.selectPopover.style.opacity = "1";
|
|
459
|
+
this.selectPopover.style.transform = "scale(1)";
|
|
460
|
+
this.popover.setAttribute("data-top", top.toString());
|
|
461
|
+
this.selectPopover.setAttribute("data-top", top.toString());
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* 隐藏选择浮窗(淡出)
|
|
466
|
+
*/
|
|
467
|
+
public hideSelectionPopover() {
|
|
468
|
+
if (this.selectPopover.style.display !== "flex") {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
this.selectPopover.style.opacity = "0";
|
|
472
|
+
this.selectPopover.style.transform = "scale(0.95)";
|
|
473
|
+
window.setTimeout(() => {
|
|
474
|
+
this.selectPopover.setAttribute("style", "display:none");
|
|
475
|
+
// Reset state
|
|
476
|
+
if (this.popoverInput) {
|
|
477
|
+
this.popoverInput.value = "";
|
|
478
|
+
}
|
|
479
|
+
if (this.popoverSendBtn) {
|
|
480
|
+
this.popoverSendBtn.classList.remove(
|
|
481
|
+
"vditor-selection-popover__send--loading"
|
|
482
|
+
);
|
|
483
|
+
this.popoverSendBtn.setAttribute("disabled", "disabled");
|
|
484
|
+
}
|
|
485
|
+
}, 150);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* 兼容旧接口:显示评论面板(映射为选择浮窗)
|
|
490
|
+
*/
|
|
253
491
|
public showComment() {
|
|
254
|
-
|
|
255
|
-
this.selectPopover.setAttribute(
|
|
256
|
-
"style",
|
|
257
|
-
`left:${position.left}px;display:block;top:${Math.max(-8, position.top - 21)}px`
|
|
258
|
-
);
|
|
492
|
+
this.showSelectionPopover();
|
|
259
493
|
}
|
|
260
494
|
|
|
495
|
+
/**
|
|
496
|
+
* 兼容旧接口:隐藏评论面板(映射为选择浮窗隐藏)
|
|
497
|
+
*/
|
|
261
498
|
public hideComment() {
|
|
262
|
-
this.
|
|
499
|
+
this.hideSelectionPopover();
|
|
263
500
|
}
|
|
264
501
|
|
|
265
502
|
public unbindListener() {
|
|
@@ -344,9 +581,162 @@ class WYSIWYG {
|
|
|
344
581
|
"text/plain",
|
|
345
582
|
vditor.lute.VditorDOM2Md(tempElement.innerHTML).trim()
|
|
346
583
|
);
|
|
347
|
-
event.clipboardData.setData("text/html",
|
|
584
|
+
event.clipboardData.setData("text/html", tempElement.innerHTML);
|
|
348
585
|
}
|
|
349
586
|
|
|
587
|
+
/**
|
|
588
|
+
* 构建选区剪贴板内容(纯文本 + 富文本)
|
|
589
|
+
*/
|
|
590
|
+
private buildClipboardPayload(vditor: IVditor) {
|
|
591
|
+
const range = getSelection().getRangeAt(0);
|
|
592
|
+
const codeElement = hasClosestByMatchTag(range.startContainer, "CODE");
|
|
593
|
+
const codeEndElement = hasClosestByMatchTag(range.endContainer, "CODE");
|
|
594
|
+
if (
|
|
595
|
+
codeElement &&
|
|
596
|
+
codeEndElement &&
|
|
597
|
+
codeEndElement.isSameNode(codeElement)
|
|
598
|
+
) {
|
|
599
|
+
const codeText =
|
|
600
|
+
codeElement.parentElement.tagName === "PRE"
|
|
601
|
+
? range.toString()
|
|
602
|
+
: "`" + range.toString() + "`";
|
|
603
|
+
return { plainText: codeText, html: "" };
|
|
604
|
+
}
|
|
605
|
+
const aElement = hasClosestByMatchTag(range.startContainer, "A");
|
|
606
|
+
const aEndElement = hasClosestByMatchTag(range.endContainer, "A");
|
|
607
|
+
if (aElement && aEndElement && aEndElement.isSameNode(aElement)) {
|
|
608
|
+
let aTitle = aElement.getAttribute("title") || "";
|
|
609
|
+
if (aTitle) {
|
|
610
|
+
aTitle = ` "${aTitle}"`;
|
|
611
|
+
}
|
|
612
|
+
return {
|
|
613
|
+
plainText: `[${range.toString()}](${aElement.getAttribute("href")}${aTitle})`,
|
|
614
|
+
html: "",
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
const startPreview = hasClosestByClassName(
|
|
618
|
+
range.startContainer,
|
|
619
|
+
"vditor-wysiwyg__preview"
|
|
620
|
+
) as HTMLElement;
|
|
621
|
+
const endPreview = hasClosestByClassName(
|
|
622
|
+
range.endContainer,
|
|
623
|
+
"vditor-wysiwyg__preview"
|
|
624
|
+
) as HTMLElement;
|
|
625
|
+
const isMathPreview = (el: HTMLElement) => {
|
|
626
|
+
const first = el.firstElementChild as HTMLElement | null;
|
|
627
|
+
return !!first && first.classList.contains("language-math");
|
|
628
|
+
};
|
|
629
|
+
if (
|
|
630
|
+
startPreview &&
|
|
631
|
+
endPreview &&
|
|
632
|
+
startPreview.isSameNode(endPreview) &&
|
|
633
|
+
isMathPreview(startPreview)
|
|
634
|
+
) {
|
|
635
|
+
return { plainText: range.toString(), html: "" };
|
|
636
|
+
}
|
|
637
|
+
const tempElement = document.createElement("div");
|
|
638
|
+
tempElement.appendChild(range.cloneContents());
|
|
639
|
+
return {
|
|
640
|
+
plainText: vditor.lute.VditorDOM2Md(tempElement.innerHTML).trim(),
|
|
641
|
+
html: tempElement.innerHTML,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* 复制选区(按钮触发)
|
|
647
|
+
* - Clipboard API 优先,降级为 execCommand('copy') 调用原有 copy 逻辑
|
|
648
|
+
*/
|
|
649
|
+
private copySelection(vditor: IVditor) {
|
|
650
|
+
const range = getSelection().getRangeAt(0);
|
|
651
|
+
if (range.toString().trim() === "") {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
const payload = this.buildClipboardPayload(vditor);
|
|
655
|
+
const writeClipboard = async () => {
|
|
656
|
+
// Safari 兼容:ClipboardItem 可能不可用
|
|
657
|
+
const ClipboardItemCtor = (window as any)["ClipboardItem"];
|
|
658
|
+
const navClipboard = (navigator as any).clipboard as
|
|
659
|
+
| { write?(items: unknown[]): Promise<void> }
|
|
660
|
+
| undefined;
|
|
661
|
+
if (
|
|
662
|
+
navClipboard?.write &&
|
|
663
|
+
typeof ClipboardItemCtor !== "undefined"
|
|
664
|
+
) {
|
|
665
|
+
try {
|
|
666
|
+
const item = new ClipboardItemCtor({
|
|
667
|
+
"text/plain": new Blob([payload.plainText], {
|
|
668
|
+
type: "text/plain",
|
|
669
|
+
}),
|
|
670
|
+
"text/html": new Blob([payload.html], {
|
|
671
|
+
type: "text/html",
|
|
672
|
+
}),
|
|
673
|
+
});
|
|
674
|
+
await navClipboard.write([item]);
|
|
675
|
+
return true;
|
|
676
|
+
} catch {
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return false;
|
|
681
|
+
};
|
|
682
|
+
writeClipboard().then((ok) => {
|
|
683
|
+
if (!ok) {
|
|
684
|
+
document.execCommand("copy");
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
this.hideSelectionPopover();
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* 剪切选区(按钮触发)
|
|
692
|
+
* - 复制到剪贴板后删除选区,并加入撤销栈
|
|
693
|
+
*/
|
|
694
|
+
private cutSelection(vditor: IVditor) {
|
|
695
|
+
const range = getSelection().getRangeAt(0);
|
|
696
|
+
if (range.toString().trim() === "") {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const payload = this.buildClipboardPayload(vditor);
|
|
700
|
+
const writeClipboard = async () => {
|
|
701
|
+
const ClipboardItemCtor = (window as any)["ClipboardItem"];
|
|
702
|
+
const navClipboard = (navigator as any).clipboard as
|
|
703
|
+
| { write?(items: unknown[]): Promise<void> }
|
|
704
|
+
| undefined;
|
|
705
|
+
if (
|
|
706
|
+
navClipboard?.write &&
|
|
707
|
+
typeof ClipboardItemCtor !== "undefined"
|
|
708
|
+
) {
|
|
709
|
+
try {
|
|
710
|
+
const item = new ClipboardItemCtor({
|
|
711
|
+
"text/plain": new Blob([payload.plainText], {
|
|
712
|
+
type: "text/plain",
|
|
713
|
+
}),
|
|
714
|
+
"text/html": new Blob([payload.html], {
|
|
715
|
+
type: "text/html",
|
|
716
|
+
}),
|
|
717
|
+
});
|
|
718
|
+
await navClipboard.write([item]);
|
|
719
|
+
return true;
|
|
720
|
+
} catch {
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return false;
|
|
725
|
+
};
|
|
726
|
+
writeClipboard().then((ok) => {
|
|
727
|
+
if (!ok) {
|
|
728
|
+
document.execCommand("cut");
|
|
729
|
+
} else {
|
|
730
|
+
document.execCommand("delete");
|
|
731
|
+
}
|
|
732
|
+
afterRenderEvent(vditor, {
|
|
733
|
+
enableAddUndoStack: true,
|
|
734
|
+
enableHint: false,
|
|
735
|
+
enableInput: true,
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
this.hideSelectionPopover();
|
|
739
|
+
}
|
|
350
740
|
private bindEvent(vditor: IVditor) {
|
|
351
741
|
this.unbindListener();
|
|
352
742
|
window.addEventListener(
|
|
@@ -355,7 +745,7 @@ class WYSIWYG {
|
|
|
355
745
|
hidePanel(vditor, ["hint"]);
|
|
356
746
|
if (
|
|
357
747
|
this.popover.style.display !== "block" ||
|
|
358
|
-
this.selectPopover.style.display !== "
|
|
748
|
+
this.selectPopover.style.display !== "flex"
|
|
359
749
|
) {
|
|
360
750
|
return;
|
|
361
751
|
}
|
|
@@ -376,7 +766,7 @@ class WYSIWYG {
|
|
|
376
766
|
if (this.popover.style.display === "block") {
|
|
377
767
|
this.popover.style.top = popoverTop;
|
|
378
768
|
}
|
|
379
|
-
if (this.selectPopover.style.display === "
|
|
769
|
+
if (this.selectPopover.style.display === "flex") {
|
|
380
770
|
this.selectPopover.style.top = popoverTop;
|
|
381
771
|
}
|
|
382
772
|
}
|
|
@@ -392,7 +782,7 @@ class WYSIWYG {
|
|
|
392
782
|
if (this.popover.style.display === "block") {
|
|
393
783
|
this.popover.style.top = popoverTop1;
|
|
394
784
|
}
|
|
395
|
-
if (this.selectPopover.style.display === "
|
|
785
|
+
if (this.selectPopover.style.display === "flex") {
|
|
396
786
|
this.selectPopover.style.top = popoverTop1;
|
|
397
787
|
}
|
|
398
788
|
})
|