vditor 3.11.2 → 4.0.0

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.
Files changed (74) hide show
  1. package/README.md +12 -1
  2. package/dist/css/content-theme/ant-design.css +10 -0
  3. package/dist/css/content-theme/dark.css +10 -0
  4. package/dist/css/content-theme/light.css +13 -0
  5. package/dist/css/content-theme/wechat.css +10 -0
  6. package/dist/index.css +163 -81
  7. package/dist/index.js +3035 -2715
  8. package/dist/index.min.js +1 -1
  9. package/dist/js/i18n/ja_JP.js +2 -2
  10. package/dist/js/i18n/zh_TW.js +8 -8
  11. package/dist/js/lute/lute.min.js +75 -60
  12. package/dist/js/mermaid/mermaid.min.js +2027 -1047
  13. package/dist/js/wavedrom/LICENSE.WaveDrom +21 -0
  14. package/dist/js/wavedrom/wavedrom.min.js +9 -0
  15. package/dist/method.d.ts +2 -0
  16. package/dist/method.js +323 -9
  17. package/dist/method.min.js +1 -1
  18. package/dist/ts/constants.d.ts +4 -0
  19. package/dist/ts/markdown/adapterRender.d.ts +4 -0
  20. package/dist/ts/markdown/imageCaptionRender.d.ts +2 -0
  21. package/dist/ts/markdown/wavedromRender.d.ts +1 -0
  22. package/dist/ts/sv/index.d.ts +1 -5
  23. package/dist/ts/sv/process.d.ts +1 -3
  24. package/dist/ts/upload/getElement.d.ts +1 -1
  25. package/dist/ts/upload/index.d.ts +2 -0
  26. package/dist/ts/util/editorCommonEvent.d.ts +1 -1
  27. package/dist/ts/util/fixBrowserBehavior.d.ts +1 -0
  28. package/dist/types/index.d.ts +22 -5
  29. package/package.json +7 -4
  30. package/src/assets/less/_reset.less +103 -2
  31. package/src/assets/less/_sv.less +2 -98
  32. package/src/assets/less/_wysiwyg.less +30 -0
  33. package/src/assets/less/index.less +20 -0
  34. package/src/index.ts +48 -14
  35. package/src/method.ts +3 -0
  36. package/src/ts/constants.ts +5 -1
  37. package/src/ts/export/index.ts +2 -0
  38. package/src/ts/hint/index.ts +9 -4
  39. package/src/ts/ir/highlightToolbarIR.ts +4 -4
  40. package/src/ts/ir/index.ts +26 -0
  41. package/src/ts/ir/process.ts +24 -1
  42. package/src/ts/markdown/adapterRender.ts +4 -0
  43. package/src/ts/markdown/getHTML.ts +6 -3
  44. package/src/ts/markdown/getMarkdown.ts +3 -3
  45. package/src/ts/markdown/imageCaptionRender.ts +175 -0
  46. package/src/ts/markdown/mermaidRender.ts +1 -1
  47. package/src/ts/markdown/previewRender.ts +7 -1
  48. package/src/ts/markdown/setLute.ts +3 -0
  49. package/src/ts/markdown/wavedromRender.ts +54 -0
  50. package/src/ts/preview/index.ts +5 -0
  51. package/src/ts/sv/index.ts +37 -76
  52. package/src/ts/sv/process.ts +48 -164
  53. package/src/ts/sv/processKeydown.ts +19 -186
  54. package/src/ts/toolbar/EditMode.ts +3 -10
  55. package/src/ts/toolbar/Emoji.ts +8 -1
  56. package/src/ts/toolbar/Record.ts +10 -2
  57. package/src/ts/toolbar/index.ts +3 -0
  58. package/src/ts/ui/initUI.ts +7 -3
  59. package/src/ts/undo/index.ts +32 -17
  60. package/src/ts/upload/index.ts +42 -10
  61. package/src/ts/util/Options.ts +1 -1
  62. package/src/ts/util/editorCommonEvent.ts +18 -9
  63. package/src/ts/util/fixBrowserBehavior.ts +197 -51
  64. package/src/ts/util/processCode.ts +3 -0
  65. package/src/ts/util/selection.ts +11 -3
  66. package/src/ts/wysiwyg/afterRenderEvent.ts +2 -0
  67. package/src/ts/wysiwyg/highlightToolbarWYSIWYG.ts +58 -2
  68. package/src/ts/wysiwyg/input.ts +5 -0
  69. package/src/ts/wysiwyg/processKeydown.ts +47 -0
  70. package/src/ts/wysiwyg/setHeading.ts +110 -6
  71. package/dist/ts/sv/combineFootnote.d.ts +0 -6
  72. package/dist/ts/sv/inputEvent.d.ts +0 -1
  73. package/src/ts/sv/combineFootnote.ts +0 -27
  74. package/src/ts/sv/inputEvent.ts +0 -188
@@ -52,7 +52,8 @@ class Undo {
52
52
  }
53
53
 
54
54
  public undo(vditor: IVditor) {
55
- if (vditor[vditor.currentMode].element.getAttribute("contenteditable") === "false") {
55
+ if (vditor.currentMode === "sv" ? vditor.sv.element.disabled :
56
+ vditor[vditor.currentMode].element.getAttribute("contenteditable") === "false") {
56
57
  return;
57
58
  }
58
59
  if (this[vditor.currentMode].undoStack.length < 2) {
@@ -70,7 +71,8 @@ class Undo {
70
71
  }
71
72
 
72
73
  public redo(vditor: IVditor) {
73
- if (vditor[vditor.currentMode].element.getAttribute("contenteditable") === "false") {
74
+ if (vditor.currentMode === "sv" ? vditor.sv.element.disabled :
75
+ vditor[vditor.currentMode].element.getAttribute("contenteditable") === "false") {
74
76
  return;
75
77
  }
76
78
  const state = this[vditor.currentMode].redoStack.pop();
@@ -82,7 +84,7 @@ class Undo {
82
84
  }
83
85
 
84
86
  public recordFirstPosition(vditor: IVditor, event: KeyboardEvent) {
85
- if (getSelection().rangeCount === 0) {
87
+ if (vditor.currentMode !== "sv" && getSelection().rangeCount === 0) {
86
88
  return;
87
89
  }
88
90
  if (this[vditor.currentMode].undoStack.length !== 1 || this[vditor.currentMode].undoStack[0].length === 0 ||
@@ -147,8 +149,15 @@ class Undo {
147
149
  }
148
150
 
149
151
  this[vditor.currentMode].lastText = text;
150
- vditor[vditor.currentMode].element.innerHTML = text;
151
- if (vditor.currentMode !== "sv") {
152
+ if (vditor.currentMode === "sv") {
153
+ const caretIndex = text.indexOf("<wbr>");
154
+ vditor.sv.element.value = text.replace("<wbr>", "");
155
+ vditor.sv.element.focus();
156
+ if (caretIndex > -1) {
157
+ vditor.sv.element.setSelectionRange(caretIndex, caretIndex);
158
+ }
159
+ } else {
160
+ vditor[vditor.currentMode].element.innerHTML = text;
152
161
  vditor[vditor.currentMode].element.querySelectorAll(`.vditor-${vditor.currentMode}__preview`)
153
162
  .forEach((blockElement: HTMLElement) => {
154
163
  if (blockElement.parentElement.querySelector(".language-echarts")) {
@@ -163,17 +172,16 @@ class Undo {
163
172
  .forEach((blockElement: HTMLElement) => {
164
173
  processCodeRender(blockElement, vditor);
165
174
  });
166
- }
167
-
168
- if (!vditor[vditor.currentMode].element.querySelector("wbr")) {
169
- // Safari 第一次输入没有光标,需手动定位到结尾
170
- const range = getSelection().getRangeAt(0);
171
- range.setEndBefore(vditor[vditor.currentMode].element);
172
- range.collapse(false);
173
- } else {
174
- setRangeByWbr(
175
- vditor[vditor.currentMode].element, vditor[vditor.currentMode].element.ownerDocument.createRange());
176
- scrollCenter(vditor);
175
+ if (!vditor[vditor.currentMode].element.querySelector("wbr")) {
176
+ // Safari 第一次输入没有光标,需手动定位到结尾
177
+ const range = getSelection().getRangeAt(0);
178
+ range.setEndBefore(vditor[vditor.currentMode].element);
179
+ range.collapse(false);
180
+ } else {
181
+ setRangeByWbr(
182
+ vditor[vditor.currentMode].element, vditor[vditor.currentMode].element.ownerDocument.createRange());
183
+ scrollCenter(vditor);
184
+ }
177
185
  }
178
186
 
179
187
  renderToc(vditor);
@@ -183,7 +191,9 @@ class Undo {
183
191
  enableHint: false,
184
192
  enableInput: true,
185
193
  });
186
- highlightToolbar(vditor);
194
+ if (vditor.currentMode !== "sv") {
195
+ highlightToolbar(vditor);
196
+ }
187
197
 
188
198
  vditor[vditor.currentMode].element.querySelectorAll(`.vditor-${vditor.currentMode}__preview[data-render='2']`)
189
199
  .forEach((item: HTMLElement) => {
@@ -225,6 +235,11 @@ class Undo {
225
235
  }
226
236
 
227
237
  private addCaret(vditor: IVditor, setFocus = false) {
238
+ if (vditor.currentMode === "sv") {
239
+ const element = vditor.sv.element;
240
+ return element.value.substring(0, element.selectionStart) + "<wbr>" +
241
+ element.value.substring(element.selectionStart);
242
+ }
228
243
  let cloneRange: Range;
229
244
  if (getSelection().rangeCount !== 0 && !vditor[vditor.currentMode].element.querySelector("wbr")) {
230
245
  const range = getSelection().getRangeAt(0);
@@ -1,11 +1,14 @@
1
- import { getEditorRange, setSelectionFocus } from "../util/selection";
2
- import { getElement } from "./getElement";
3
- import { setHeaders } from "./setHeaders";
1
+ import {getEditorRange, setSelectionFocus} from "../util/selection";
2
+ import {processAfterRender as processSVAfterRender, processPaste} from "../sv/process";
3
+ import {getElement} from "./getElement";
4
+ import {setHeaders} from "./setHeaders";
4
5
 
5
6
  class Upload {
6
7
  public element: HTMLElement;
7
8
  public isUploading: boolean;
8
9
  public range: Range;
10
+ public selectionEnd: number;
11
+ public selectionStart: number;
9
12
 
10
13
  constructor() {
11
14
  this.isUploading = false;
@@ -146,9 +149,25 @@ const genUploadedLabel = (responseText: string, vditor: IVditor) => {
146
149
  }
147
150
  }
148
151
  });
149
- setSelectionFocus(vditor.upload.range);
150
- document.execCommand("insertHTML", false, succFileText);
151
- vditor.upload.range = getSelection().getRangeAt(0).cloneRange();
152
+ if (!succFileText) {
153
+ if (vditor.currentMode === "sv") {
154
+ vditor.sv.element.setSelectionRange(vditor.upload.selectionStart, vditor.upload.selectionEnd);
155
+ } else {
156
+ setSelectionFocus(vditor.upload.range);
157
+ }
158
+ return;
159
+ }
160
+ if (vditor.currentMode === "sv") {
161
+ vditor.sv.element.setSelectionRange(vditor.upload.selectionStart, vditor.upload.selectionEnd);
162
+ processPaste(vditor, succFileText);
163
+ processSVAfterRender(vditor);
164
+ vditor.upload.selectionStart = vditor.sv.element.selectionStart;
165
+ vditor.upload.selectionEnd = vditor.sv.element.selectionEnd;
166
+ } else {
167
+ setSelectionFocus(vditor.upload.range);
168
+ document.execCommand("insertHTML", false, succFileText);
169
+ vditor.upload.range = getSelection().getRangeAt(0).cloneRange();
170
+ }
152
171
  };
153
172
 
154
173
  const uploadFiles =
@@ -197,7 +216,12 @@ const uploadFiles =
197
216
  }
198
217
  const editorElement = getElement(vditor);
199
218
 
200
- vditor.upload.range = getEditorRange(vditor);
219
+ if (vditor.currentMode === "sv") {
220
+ vditor.upload.selectionStart = vditor.sv.element.selectionStart;
221
+ vditor.upload.selectionEnd = vditor.sv.element.selectionEnd;
222
+ } else {
223
+ vditor.upload.range = getEditorRange(vditor);
224
+ }
201
225
 
202
226
  const validateResult = validateFile(vditor, fileList);
203
227
  if (validateResult.length === 0) {
@@ -229,11 +253,19 @@ const uploadFiles =
229
253
  }
230
254
  setHeaders(vditor, xhr);
231
255
  vditor.upload.isUploading = true;
232
- editorElement.setAttribute("contenteditable", "false");
256
+ if (vditor.currentMode === "sv") {
257
+ vditor.sv.element.disabled = true;
258
+ } else {
259
+ editorElement.setAttribute("contenteditable", "false");
260
+ }
233
261
  xhr.onreadystatechange = () => {
234
262
  if (xhr.readyState === XMLHttpRequest.DONE) {
235
263
  vditor.upload.isUploading = false;
236
- editorElement.setAttribute("contenteditable", "true");
264
+ if (vditor.currentMode === "sv") {
265
+ vditor.sv.element.disabled = false;
266
+ } else {
267
+ editorElement.setAttribute("contenteditable", "true");
268
+ }
237
269
  if (xhr.status >= 200 && xhr.status < 300) {
238
270
  if (vditor.options.upload.success) {
239
271
  vditor.options.upload.success(editorElement, xhr.responseText);
@@ -270,4 +302,4 @@ const uploadFiles =
270
302
  xhr.send(formData);
271
303
  };
272
304
 
273
- export { Upload, uploadFiles };
305
+ export {Upload, uploadFiles};
@@ -290,7 +290,7 @@ export class Options {
290
290
  hotkey: "⇧⌘H",
291
291
  icon: '<svg><use xlink:href="#vditor-icon-line"></use></svg>',
292
292
  name: "line",
293
- prefix: "---",
293
+ prefix: "***",
294
294
  tipPosition: "n",
295
295
  },
296
296
  {
@@ -45,18 +45,22 @@ export const blurEvent = (vditor: IVditor, editorElement: HTMLElement) => {
45
45
  if (expandElement) {
46
46
  expandElement.classList.remove("vditor-ir__node--expand");
47
47
  }
48
- } else if (vditor.currentMode === "wysiwyg" &&
49
- !vditor.wysiwyg.selectPopover.contains(event.relatedTarget as HTMLElement)) {
50
- vditor.wysiwyg.hideComment();
48
+ vditor.ir.range = getEditorRange(vditor);
49
+ } else if (vditor.currentMode === "wysiwyg") {
50
+ if (!vditor.wysiwyg.selectPopover.contains(event.relatedTarget as HTMLElement)) {
51
+ vditor.wysiwyg.hideComment();
52
+ }
53
+ vditor.wysiwyg.range = getEditorRange(vditor);
51
54
  }
52
- vditor[vditor.currentMode].range = getEditorRange(vditor);
53
55
  if (vditor.options.blur) {
54
56
  vditor.options.blur(getMarkdown(vditor));
55
57
  }
56
58
  });
57
59
  };
58
60
 
59
- export const dropEvent = (vditor: IVditor, editorElement: HTMLElement) => {
61
+ export const dropEvent = (vditor: IVditor, editorElement: HTMLElement, pasteCode = (code: string) => {
62
+ document.execCommand("insertHTML", false, code);
63
+ }) => {
60
64
  editorElement.addEventListener("dragstart", (event) => {
61
65
  // 选中编辑器中的文字进行拖拽
62
66
  event.dataTransfer.setData(Constants.DROP_EDITOR, Constants.DROP_EDITOR);
@@ -69,9 +73,7 @@ export const dropEvent = (vditor: IVditor, editorElement: HTMLElement) => {
69
73
  } else if (event.dataTransfer.types.includes("Files") || event.dataTransfer.types.includes("text/html")) {
70
74
  // 外部文件拖入编辑器中或者编辑器内选中文字拖拽
71
75
  paste(vditor, event, {
72
- pasteCode: (code: string) => {
73
- document.execCommand("insertHTML", false, code);
74
- },
76
+ pasteCode,
75
77
  });
76
78
  }
77
79
  });
@@ -102,7 +104,14 @@ export const scrollCenter = (vditor: IVditor) => {
102
104
  return;
103
105
  }
104
106
  const editorElement = vditor[vditor.currentMode].element;
105
- const cursorTop = getCursorPosition(editorElement).top;
107
+ let cursorTop: number;
108
+ if (vditor.currentMode === "sv") {
109
+ const lineHeight = parseInt(getComputedStyle(vditor.sv.element).lineHeight, 10) || 22;
110
+ cursorTop = (vditor.sv.element.value.substring(0, vditor.sv.element.selectionStart).split("\n").length - 1) *
111
+ lineHeight;
112
+ } else {
113
+ cursorTop = getCursorPosition(editorElement).top;
114
+ }
106
115
  if (vditor.options.height === "auto" && !vditor.element.classList.contains("vditor--fullscreen")) {
107
116
  window.scrollTo(window.scrollX,
108
117
  cursorTop + vditor.element.offsetTop + vditor.toolbar.element.offsetHeight - window.innerHeight / 2 + 10);
@@ -79,6 +79,21 @@ export const fixCursorDownInlineMath = (range: Range, key: string) => {
79
79
  };
80
80
 
81
81
  export const insertEmptyBlock = (vditor: IVditor, position: InsertPosition) => {
82
+ if (vditor.currentMode === "sv") {
83
+ const element = vditor.sv.element;
84
+ if (position === "beforebegin") {
85
+ const lineStart = element.value.lastIndexOf("\n", element.selectionStart - 1) + 1;
86
+ element.setRangeText("\n", lineStart, lineStart, "end");
87
+ element.setSelectionRange(lineStart, lineStart);
88
+ } else {
89
+ const lineEndIndex = element.value.indexOf("\n", element.selectionEnd);
90
+ const lineEnd = lineEndIndex === -1 ? element.value.length : lineEndIndex;
91
+ element.setRangeText("\n", lineEnd, lineEnd, "end");
92
+ }
93
+ element.focus();
94
+ processSVAfterRender(vditor);
95
+ return;
96
+ }
82
97
  const range = getEditorRange(vditor);
83
98
  const blockElement = hasClosestBlock(range.startContainer);
84
99
  if (blockElement) {
@@ -177,10 +192,40 @@ export const insertBeforeBlock = (vditor: IVditor, event: KeyboardEvent, range:
177
192
 
178
193
  export const listToggle = (vditor: IVditor, range: Range, type: string, cancel = true) => {
179
194
  const itemElement = hasClosestByMatchTag(range.startContainer, "LI");
195
+ const selectedBlockElements: HTMLElement[] = [];
196
+ const startBlockElement = hasClosestByAttribute(range.startContainer, "data-block", "0");
197
+ const endBlockElement = hasClosestByAttribute(range.endContainer, "data-block", "0");
198
+ if (startBlockElement && endBlockElement &&
199
+ startBlockElement.parentElement.isSameNode(endBlockElement.parentElement)) {
200
+ let blockElement: Element = startBlockElement;
201
+ while (blockElement) {
202
+ selectedBlockElements.push(blockElement as HTMLElement);
203
+ if (blockElement.isSameNode(endBlockElement)) {
204
+ break;
205
+ }
206
+ blockElement = blockElement.nextElementSibling;
207
+ }
208
+ }
180
209
  vditor[vditor.currentMode].element.querySelectorAll("wbr").forEach((wbr) => {
181
210
  wbr.remove();
182
211
  });
183
- range.insertNode(document.createElement("wbr"));
212
+ const keepSelection = !range.collapsed;
213
+ if (keepSelection) {
214
+ // 列表转换会重建块元素,分别标记选区起止位置以便恢复原始选区
215
+ const endRange = range.cloneRange();
216
+ endRange.collapse(false);
217
+ const endElement = document.createElement("wbr");
218
+ endElement.setAttribute("data-type", "list-selection-end");
219
+ endRange.insertNode(endElement);
220
+
221
+ const startRange = range.cloneRange();
222
+ startRange.collapse(true);
223
+ const startElement = document.createElement("wbr");
224
+ startElement.setAttribute("data-type", "list-selection-start");
225
+ startRange.insertNode(startElement);
226
+ } else {
227
+ range.insertNode(document.createElement("wbr"));
228
+ }
184
229
 
185
230
  if (cancel && itemElement) {
186
231
  // 取消
@@ -197,25 +242,24 @@ export const listToggle = (vditor: IVditor, range: Range, type: string, cancel =
197
242
  } else {
198
243
  if (!itemElement) {
199
244
  // 添加
200
- let blockElement = hasClosestByAttribute(range.startContainer, "data-block", "0");
201
- if (!blockElement) {
245
+ if (selectedBlockElements.length === 0) {
202
246
  vditor[vditor.currentMode].element.querySelector("wbr").remove();
203
- blockElement = vditor[vditor.currentMode].element.querySelector("p");
247
+ const blockElement = vditor[vditor.currentMode].element.querySelector("p");
204
248
  blockElement.innerHTML = "<wbr>";
249
+ selectedBlockElements.push(blockElement);
205
250
  }
206
- if (type === "check") {
207
- blockElement.insertAdjacentHTML("beforebegin",
208
- `<ul data-block="0"><li class="vditor-task"><input type="checkbox" /> ${blockElement.innerHTML}</li></ul>`);
209
- blockElement.remove();
210
- } else if (type === "list") {
211
- blockElement.insertAdjacentHTML("beforebegin",
212
- `<ul data-block="0"><li>${blockElement.innerHTML}</li></ul>`);
213
- blockElement.remove();
214
- } else if (type === "ordered-list") {
215
- blockElement.insertAdjacentHTML("beforebegin",
216
- `<ol data-block="0"><li>${blockElement.innerHTML}</li></ol>`);
217
- blockElement.remove();
218
- }
251
+ let listHTML = "";
252
+ selectedBlockElements.forEach((blockElement) => {
253
+ if (type === "check") {
254
+ listHTML += `<li class="vditor-task"><input type="checkbox" /> ${blockElement.innerHTML}</li>`;
255
+ } else {
256
+ listHTML += `<li>${blockElement.innerHTML}</li>`;
257
+ }
258
+ });
259
+ const listTagName = type === "ordered-list" ? "ol" : "ul";
260
+ selectedBlockElements[0].insertAdjacentHTML("beforebegin",
261
+ `<${listTagName} data-block="0">${listHTML}</${listTagName}>`);
262
+ selectedBlockElements.forEach((blockElement) => blockElement.remove());
219
263
  } else {
220
264
  // 切换
221
265
  if (type === "check") {
@@ -227,7 +271,14 @@ export const listToggle = (vditor: IVditor, range: Range, type: string, cancel =
227
271
  } else {
228
272
  if (itemElement.querySelector("input")) {
229
273
  itemElement.parentElement.querySelectorAll("li").forEach((item) => {
230
- item.querySelector("input").remove();
274
+ const inputElement = item.querySelector("input");
275
+ if (inputElement) {
276
+ const nextSibling = inputElement.nextSibling;
277
+ inputElement.remove();
278
+ if (nextSibling && nextSibling.nodeType === 3 && nextSibling.textContent.startsWith(" ")) {
279
+ nextSibling.textContent = nextSibling.textContent.substring(1);
280
+ }
281
+ }
231
282
  item.classList.remove("vditor-task");
232
283
  });
233
284
  }
@@ -246,6 +297,18 @@ export const listToggle = (vditor: IVditor, range: Range, type: string, cancel =
246
297
  }
247
298
  }
248
299
  }
300
+ if (keepSelection) {
301
+ const editorElement = vditor[vditor.currentMode].element;
302
+ const startElement = editorElement.querySelector('[data-type="list-selection-start"]');
303
+ const endElement = editorElement.querySelector('[data-type="list-selection-end"]');
304
+ if (startElement && endElement) {
305
+ range.setStartAfter(startElement);
306
+ range.setEndBefore(endElement);
307
+ startElement.remove();
308
+ endElement.remove();
309
+ setSelectionFocus(range);
310
+ }
311
+ }
249
312
  };
250
313
 
251
314
  export const listIndent = (vditor: IVditor, liElement: HTMLElement, range: Range) => {
@@ -453,6 +516,41 @@ export const execAfterRender = (vditor: IVditor, options = {
453
516
  }
454
517
  };
455
518
 
519
+ const isEmptyListItem = (liElement: HTMLElement) => {
520
+ if (liElement.textContent.split(Constants.ZWSP).join("").trim() !== "") {
521
+ return false;
522
+ }
523
+ return !liElement.querySelector([
524
+ "audio", "canvas", "embed", "hr", "iframe", "img", "object", "ol", "pre", "svg", "table", "ul", "video",
525
+ "input:not([type='checkbox'])",
526
+ ].join(", "));
527
+ };
528
+
529
+ export const exitEmptyListItem = (liElement: HTMLElement) => {
530
+ const listElement = liElement.parentElement;
531
+ const listParent = listElement.parentElement;
532
+ const paragraphElement = document.createElement("p");
533
+ paragraphElement.setAttribute("data-block", "0");
534
+ paragraphElement.innerHTML = "<wbr>";
535
+
536
+ let trailingListElement: HTMLElement | undefined;
537
+ if (liElement.nextElementSibling) {
538
+ trailingListElement = listElement.cloneNode(false) as HTMLElement;
539
+ while (liElement.nextElementSibling) {
540
+ trailingListElement.appendChild(liElement.nextElementSibling);
541
+ }
542
+ listParent.insertBefore(trailingListElement, listElement.nextSibling);
543
+ }
544
+
545
+ liElement.remove();
546
+ if (listElement.childElementCount === 0) {
547
+ listParent.replaceChild(paragraphElement, listElement);
548
+ } else {
549
+ listParent.insertBefore(paragraphElement, trailingListElement || listElement.nextSibling);
550
+ }
551
+ return paragraphElement;
552
+ };
553
+
456
554
  export const fixList = (range: Range, vditor: IVditor, pElement: HTMLElement | false, event: KeyboardEvent) => {
457
555
  const startContainer = range.startContainer;
458
556
  const liElement = hasClosestByMatchTag(startContainer, "LI");
@@ -471,6 +569,28 @@ export const fixList = (range: Range, vditor: IVditor, pElement: HTMLElement | f
471
569
  return true;
472
570
  }
473
571
 
572
+ // 嵌套列表末尾的空列表项回车后转为父列表项中的段落
573
+ // https://github.com/Vanessa219/vditor/issues/939
574
+ if (!isCtrl(event) && !event.shiftKey && !event.altKey && event.key === "Enter" && range.collapsed &&
575
+ !liElement.nextElementSibling && liElement.parentElement.parentElement.tagName === "LI" &&
576
+ isEmptyListItem(liElement)) {
577
+ const paragraphElement = exitEmptyListItem(liElement);
578
+ setRangeByWbr(paragraphElement, range);
579
+ execAfterRender(vditor);
580
+ event.preventDefault();
581
+ return true;
582
+ }
583
+
584
+ // 空列表项删除标记后退出当前列表
585
+ if (!isCtrl(event) && !event.shiftKey && !event.altKey && event.key === "Backspace" &&
586
+ liElement.textContent.trim().replace(Constants.ZWSP, "") === "" && range.toString() === "") {
587
+ const paragraphElement = exitEmptyListItem(liElement);
588
+ setRangeByWbr(paragraphElement, range);
589
+ execAfterRender(vditor);
590
+ event.preventDefault();
591
+ return true;
592
+ }
593
+
474
594
  if (!isCtrl(event) && !event.shiftKey && !event.altKey && event.key === "Backspace" &&
475
595
  !liElement.previousElementSibling && range.toString() === "" &&
476
596
  getSelectPosition(liElement, vditor[vditor.currentMode].element, range).start === 0) {
@@ -488,20 +608,6 @@ export const fixList = (range: Range, vditor: IVditor, pElement: HTMLElement | f
488
608
  return true;
489
609
  }
490
610
 
491
- // 空列表删除后与上一级段落对齐
492
- if (!isCtrl(event) && !event.shiftKey && !event.altKey && event.key === "Backspace" &&
493
- liElement.textContent.trim().replace(Constants.ZWSP, "") === "" &&
494
- range.toString() === "" && liElement.previousElementSibling?.tagName === "LI") {
495
- liElement.previousElementSibling.insertAdjacentText("beforeend", "\n\n");
496
- range.selectNodeContents(liElement.previousElementSibling);
497
- range.collapse(false);
498
- liElement.remove();
499
- setRangeByWbr(vditor[vditor.currentMode].element, range);
500
- execAfterRender(vditor);
501
- event.preventDefault();
502
- return true;
503
- }
504
-
505
611
  if (!isCtrl(event) && !event.altKey && event.key === "Tab") {
506
612
  // 光标位于第一/零字符时,tab 用于列表的缩进
507
613
  let isFirst = false;
@@ -1021,6 +1127,27 @@ export const fixBlockquote = (vditor: IVditor, range: Range, event: KeyboardEven
1021
1127
  const startContainer = range.startContainer;
1022
1128
  const blockquoteElement = hasClosestByMatchTag(startContainer, "BLOCKQUOTE");
1023
1129
  if (blockquoteElement && range.toString() === "") {
1130
+ if (pElement && event.key === "Enter" && !isCtrl(event) && !event.shiftKey && !event.altKey &&
1131
+ pElement.parentElement.isSameNode(blockquoteElement) &&
1132
+ hasClosestByMatchTag(blockquoteElement.parentElement, "LI") &&
1133
+ (range.collapsed || !range.cloneContents().firstElementChild) &&
1134
+ pElement.innerHTML.replace(Constants.ZWSP, "") !== "\n" &&
1135
+ pElement.innerHTML.replace(Constants.ZWSP, "") !== "") {
1136
+ // 列表内的引用由浏览器处理回车时会新建列表项 https://github.com/Vanessa219/vditor/issues/1925
1137
+ const trailingRange = range.cloneRange();
1138
+ trailingRange.setEnd(pElement, pElement.childNodes.length);
1139
+ const paragraphElement = document.createElement("p");
1140
+ paragraphElement.setAttribute("data-block", "0");
1141
+ paragraphElement.appendChild(trailingRange.extractContents());
1142
+ pElement.insertAdjacentElement("afterend", paragraphElement);
1143
+ range.setStart(paragraphElement, 0);
1144
+ range.collapse(true);
1145
+ setSelectionFocus(range);
1146
+ execAfterRender(vditor);
1147
+ event.preventDefault();
1148
+ return true;
1149
+ }
1150
+
1024
1151
  if (event.key === "Backspace" && !isCtrl(event) && !event.shiftKey && !event.altKey &&
1025
1152
  getSelectPosition(blockquoteElement, vditor[vditor.currentMode].element, range).start === 0) {
1026
1153
  // Backspace: 光标位于引用中的第零个字符,仅删除引用标签
@@ -1068,9 +1195,15 @@ export const fixBlockquote = (vditor: IVditor, range: Range, event: KeyboardEven
1068
1195
  return true;
1069
1196
  }
1070
1197
 
1071
- if (insertAfterBlock(vditor, event, range, blockquoteElement, blockquoteElement)) {
1198
+ const itemElement = blockquoteElement.parentElement.tagName === "LI" ? blockquoteElement.parentElement : null;
1199
+ const afterBlockElement = !blockquoteElement.nextElementSibling && itemElement?.nextElementSibling?.tagName === "LI" ?
1200
+ itemElement : blockquoteElement;
1201
+ if (insertAfterBlock(vditor, event, range, blockquoteElement, afterBlockElement)) {
1072
1202
  return true;
1073
1203
  }
1204
+ if (event.key === "ArrowUp" && itemElement && !blockquoteElement.previousElementSibling) {
1205
+ return false;
1206
+ }
1074
1207
  if (insertBeforeBlock(vditor, event, range, blockquoteElement, blockquoteElement)) {
1075
1208
  return true;
1076
1209
  }
@@ -1258,7 +1391,8 @@ export const fixFirefoxArrowUpTable = (event: KeyboardEvent, blockElement: false
1258
1391
  export const paste = async (vditor: IVditor, event: (ClipboardEvent | DragEvent) & {target: HTMLElement}, callback: {
1259
1392
  pasteCode(code: string): void,
1260
1393
  }) => {
1261
- if (vditor[vditor.currentMode].element.getAttribute("contenteditable") !== "true") {
1394
+ if (vditor.currentMode === "sv" ? vditor.sv.element.disabled :
1395
+ vditor[vditor.currentMode].element.getAttribute("contenteditable") !== "true") {
1262
1396
  return;
1263
1397
  }
1264
1398
  event.stopPropagation();
@@ -1283,13 +1417,18 @@ export const paste = async (vditor: IVditor, event: (ClipboardEvent | DragEvent)
1283
1417
  HTML2VditorIRDOM?: ILuteRender,
1284
1418
  Md2VditorDOM?: ILuteRender,
1285
1419
  Md2VditorIRDOM?: ILuteRender,
1286
- Md2VditorSVDOM?: ILuteRender,
1287
1420
  } = {};
1288
1421
  const renderLinkDest: ILuteRenderCallback = (node, entering) => {
1289
1422
  if (!entering) {
1290
1423
  return ["", Lute.WalkContinue];
1291
1424
  }
1292
1425
 
1426
+ const parent = node.__internal_object__.Parent;
1427
+ // 链接引用的目标地址由定义节点提供,不在引用位置重复渲染
1428
+ if (parent.Type === 33 && parent.LinkType === 3) {
1429
+ return ["", Lute.WalkContinue];
1430
+ }
1431
+
1293
1432
  if (vditor.options.upload.renderLinkDest) {
1294
1433
  return vditor.options.upload.renderLinkDest(vditor, node, entering);
1295
1434
  }
@@ -1321,12 +1460,11 @@ export const paste = async (vditor: IVditor, event: (ClipboardEvent | DragEvent)
1321
1460
  }
1322
1461
  const original = responseJSON.data.originalURL;
1323
1462
  if (vditor.currentMode === "sv") {
1324
- vditor.sv.element.querySelectorAll(".vditor-sv__marker--link")
1325
- .forEach((item: HTMLElement) => {
1326
- if (item.textContent === original) {
1327
- item.textContent = responseJSON.data.url;
1328
- }
1329
- });
1463
+ const selectionStart = vditor.sv.element.selectionStart;
1464
+ const selectionEnd = vditor.sv.element.selectionEnd;
1465
+ vditor.sv.element.value = vditor.sv.element.value.split(original)
1466
+ .join(responseJSON.data.url);
1467
+ vditor.sv.element.setSelectionRange(selectionStart, selectionEnd);
1330
1468
  } else {
1331
1469
  const imgElement: HTMLImageElement =
1332
1470
  vditor[vditor.currentMode].element.querySelector(`img[src="${original}"]`);
@@ -1352,7 +1490,7 @@ export const paste = async (vditor: IVditor, event: (ClipboardEvent | DragEvent)
1352
1490
  } else if (vditor.currentMode === "wysiwyg") {
1353
1491
  return ["", Lute.WalkContinue];
1354
1492
  } else {
1355
- return [`<span class="vditor-sv__marker--link">${Lute.EscapeHTMLStr(src)}</span>`, Lute.WalkContinue];
1493
+ return ["", Lute.WalkContinue];
1356
1494
  }
1357
1495
  };
1358
1496
 
@@ -1378,7 +1516,13 @@ export const paste = async (vditor: IVditor, event: (ClipboardEvent | DragEvent)
1378
1516
  const codeElement = vditor.currentMode === "sv" ?
1379
1517
  hasClosestByAttribute(event.target, "data-type", "code-block") :
1380
1518
  hasClosestByMatchTag(event.target, "CODE");
1381
- if (codeElement) {
1519
+ const cellElement = vditor.currentMode === "wysiwyg" &&
1520
+ (hasClosestByMatchTag(getEditorRange(vditor).startContainer, "TD") ||
1521
+ hasClosestByMatchTag(getEditorRange(vditor).startContainer, "TH"));
1522
+ if (cellElement && /[\r\n]/.test(textPlain)) {
1523
+ // 表格单元格内的换行使用 br 表示,避免自旋时解析为新的表格行
1524
+ insertHTML(Lute.EscapeHTMLStr(textPlain).replace(/\r\n|\r|\n/g, "<br>"), vditor);
1525
+ } else if (codeElement) {
1382
1526
  // 粘贴在代码位置
1383
1527
  if (vditor.currentMode === "sv") {
1384
1528
  document.execCommand("insertHTML", false, textPlain.replace(/&/g, "&amp;").replace(/</g, "&lt;"));
@@ -1424,8 +1568,6 @@ export const paste = async (vditor: IVditor, event: (ClipboardEvent | DragEvent)
1424
1568
  vditor.lute.SetJSRenderers({renderers});
1425
1569
  insertHTML(vditor.lute.HTML2VditorDOM(tempElement.innerHTML), vditor);
1426
1570
  } else {
1427
- renderers.Md2VditorSVDOM = {renderLinkDest};
1428
- vditor.lute.SetJSRenderers({renderers});
1429
1571
  processPaste(vditor, vditor.lute.HTML2Md(tempElement.innerHTML).trimRight());
1430
1572
  }
1431
1573
  vditor.outline.render(vditor);
@@ -1451,14 +1593,20 @@ export const paste = async (vditor: IVditor, event: (ClipboardEvent | DragEvent)
1451
1593
  } else {
1452
1594
  imgHTML += `![${file.name}](${fileReader.result.toString()})\n`;
1453
1595
  }
1454
- document.execCommand("insertHTML", false, imgHTML);
1596
+ if (vditor.currentMode === "sv") {
1597
+ processPaste(vditor, imgHTML);
1598
+ execAfterRender(vditor);
1599
+ } else {
1600
+ document.execCommand("insertHTML", false, imgHTML);
1601
+ }
1455
1602
  };
1456
1603
  }
1457
1604
  }
1458
1605
  } else if (textPlain.trim() !== "" && files.length === 0) {
1459
- const range = getEditorRange(vditor);
1460
- if (range.toString() !== "" && vditor.lute.IsValidLinkDest(textPlain)) {
1461
- textPlain = `[${range.toString()}](${textPlain})`;
1606
+ const selectedText = vditor.currentMode === "sv" ? vditor.sv.element.value.substring(
1607
+ vditor.sv.element.selectionStart, vditor.sv.element.selectionEnd) : getEditorRange(vditor).toString();
1608
+ if (selectedText !== "" && vditor.lute.IsValidLinkDest(textPlain)) {
1609
+ textPlain = `[${selectedText}](${textPlain})`;
1462
1610
  }
1463
1611
  if (vditor.currentMode === "ir") {
1464
1612
  renderers.Md2VditorIRDOM = {renderLinkDest};
@@ -1469,8 +1617,6 @@ export const paste = async (vditor: IVditor, event: (ClipboardEvent | DragEvent)
1469
1617
  vditor.lute.SetJSRenderers({renderers});
1470
1618
  insertHTML(Lute.Sanitize(vditor.lute.Md2VditorDOM(textPlain)), vditor);
1471
1619
  } else {
1472
- renderers.Md2VditorSVDOM = {renderLinkDest};
1473
- vditor.lute.SetJSRenderers({renderers});
1474
1620
  processPaste(vditor, textPlain);
1475
1621
  }
1476
1622
  vditor.outline.render(vditor);
@@ -10,6 +10,7 @@ import {markmapRender} from "../markdown/markmapRender";
10
10
  import {mindmapRender} from "../markdown/mindmapRender";
11
11
  import {plantumlRender} from "../markdown/plantumlRender";
12
12
  import {SMILESRender} from "../markdown/SMILESRender";
13
+ import {wavedromRender} from "../markdown/wavedromRender";
13
14
 
14
15
  export const processPasteCode = (html: string, text: string, type = "sv") => {
15
16
  const tempElement = document.createElement("div");
@@ -82,6 +83,8 @@ export const processCodeRender = (previewPanel: HTMLElement, vditor: IVditor) =>
82
83
  plantumlRender(previewPanel, vditor.options.cdn);
83
84
  } else if (language === "graphviz") {
84
85
  graphvizRender(previewPanel, vditor.options.cdn);
86
+ } else if (language === "wavedrom") {
87
+ wavedromRender(previewPanel, vditor.options.cdn);
85
88
  } else if (language === "math") {
86
89
  mathRender(previewPanel, {cdn: vditor.options.cdn, math: vditor.options.preview.math});
87
90
  } else {