@8btc/mditor 0.0.24 → 0.0.26
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/css/content-theme/dark.css +10 -0
- package/dist/css/content-theme/light.css +10 -0
- package/dist/index.css +58 -2
- package/dist/index.js +371 -32
- package/dist/index.min.js +1 -1
- package/dist/js/lute/lute.min.js +29 -1
- package/dist/method.js +267 -4
- package/dist/method.min.js +1 -1
- package/dist/ts/ir/index.d.ts +2 -0
- package/dist/ts/markdown/customRender.d.ts +73 -0
- package/dist/ts/preview/index.d.ts +2 -0
- package/dist/ts/sv/index.d.ts +2 -0
- package/dist/ts/util/mathSelection.d.ts +14 -0
- package/dist/ts/wysiwyg/index.d.ts +2 -1
- package/dist/types/index.d.ts +16 -0
- package/package.json +1 -1
- package/src/assets/less/_content.less +12 -0
- package/src/assets/less/_ir.less +12 -0
- package/src/assets/less/_sv.less +12 -0
- package/src/assets/less/_wysiwyg.less +12 -0
- package/src/assets/less/index.less +5 -0
- package/src/ts/ir/index.ts +43 -0
- package/src/ts/markdown/customRender.ts +203 -0
- package/src/ts/markdown/previewRender.ts +82 -0
- package/src/ts/preview/index.ts +14 -0
- package/src/ts/sv/index.ts +12 -0
- package/src/ts/util/Options.ts +1 -0
- package/src/ts/util/hasClosest.ts +29 -8
- package/src/ts/util/mathSelection.ts +59 -0
- package/src/ts/wysiwyg/index.ts +61 -33
|
@@ -14,6 +14,7 @@ declare class WYSIWYG {
|
|
|
14
14
|
commentIds: string[];
|
|
15
15
|
private vditor;
|
|
16
16
|
private scrollListener;
|
|
17
|
+
private mathSelectionCleanup;
|
|
17
18
|
constructor(vditor: IVditor);
|
|
18
19
|
getComments(vditor: IVditor, getData?: boolean): ICommentsData[];
|
|
19
20
|
triggerRemoveComment(vditor: IVditor): void;
|
|
@@ -39,7 +40,7 @@ declare class WYSIWYG {
|
|
|
39
40
|
/**
|
|
40
41
|
* 复制事件处理
|
|
41
42
|
* - 代码块与链接:按原有规则格式化复制
|
|
42
|
-
* -
|
|
43
|
+
* - 数学公式选区:复制公式源码(与 sv 模式一致)
|
|
43
44
|
* - 其他内容:转换为 Markdown 文本复制
|
|
44
45
|
*/
|
|
45
46
|
private copy;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -610,6 +610,13 @@ interface IPreviewOptions {
|
|
|
610
610
|
theme?: IPreviewTheme;
|
|
611
611
|
icon?: "ant" | "material" | undefined;
|
|
612
612
|
render?: IPreviewRender;
|
|
613
|
+
/** 自定义组件配置 */
|
|
614
|
+
customComponents?: {
|
|
615
|
+
[key: string]: {
|
|
616
|
+
renderHTML: (props: Record<string, any>) => string;
|
|
617
|
+
displayName?: string;
|
|
618
|
+
};
|
|
619
|
+
};
|
|
613
620
|
|
|
614
621
|
transform?(html: string): string;
|
|
615
622
|
|
|
@@ -836,6 +843,15 @@ interface IOptions {
|
|
|
836
843
|
language: string;
|
|
837
844
|
render: (element: HTMLElement, vditor: IVditor) => void;
|
|
838
845
|
}[];
|
|
846
|
+
/** 自定义组件配置,用于渲染自定义标签如 <read-button label="文献解读"> */
|
|
847
|
+
customComponents?: {
|
|
848
|
+
[key: string]: {
|
|
849
|
+
/** HTML 渲染函数,用于生成 HTML 字符串 */
|
|
850
|
+
renderHTML: (props: Record<string, any>) => string;
|
|
851
|
+
/** 组件显示名称 */
|
|
852
|
+
displayName?: string;
|
|
853
|
+
};
|
|
854
|
+
};
|
|
839
855
|
|
|
840
856
|
/** 编辑器异步渲染完成后的回调方法 */
|
|
841
857
|
after?(): void;
|
package/package.json
CHANGED
|
@@ -58,6 +58,18 @@
|
|
|
58
58
|
img:not(.emoji) {
|
|
59
59
|
cursor: pointer;
|
|
60
60
|
}
|
|
61
|
+
|
|
62
|
+
// 数学公式选中高亮样式
|
|
63
|
+
.language-math {
|
|
64
|
+
user-select: all;
|
|
65
|
+
cursor: text;
|
|
66
|
+
transition: background-color 0.1s ease;
|
|
67
|
+
|
|
68
|
+
&.vditor-math--selected {
|
|
69
|
+
background-color: highlight !important;
|
|
70
|
+
color: highlighttext !important;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
61
73
|
}
|
|
62
74
|
|
|
63
75
|
&-devtools {
|
package/src/assets/less/_ir.less
CHANGED
|
@@ -263,6 +263,18 @@
|
|
|
263
263
|
.vditor-reset > :is(p, h1, h2, h3, h4, h5, h6, ul, ol, code, pre):has(+ .vditor-ir__node[data-type="math-block"]) {
|
|
264
264
|
margin-bottom: 0 !important;
|
|
265
265
|
}
|
|
266
|
+
|
|
267
|
+
// 数学公式选中高亮样式
|
|
268
|
+
.language-math {
|
|
269
|
+
user-select: all;
|
|
270
|
+
cursor: text;
|
|
271
|
+
transition: background-color 0.1s ease;
|
|
272
|
+
|
|
273
|
+
&.vditor-math--selected {
|
|
274
|
+
background-color: highlight !important;
|
|
275
|
+
color: highlighttext !important;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
266
278
|
}
|
|
267
279
|
|
|
268
280
|
@media screen and (max-width: @max-width) {
|
package/src/assets/less/_sv.less
CHANGED
|
@@ -129,4 +129,16 @@
|
|
|
129
129
|
font-weight: bold;
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
|
+
|
|
133
|
+
// 数学公式选中高亮样式
|
|
134
|
+
.language-math {
|
|
135
|
+
user-select: all;
|
|
136
|
+
cursor: text;
|
|
137
|
+
transition: background-color 0.1s ease;
|
|
138
|
+
|
|
139
|
+
&.vditor-math--selected {
|
|
140
|
+
background-color: highlight !important;
|
|
141
|
+
color: highlighttext !important;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
132
144
|
}
|
|
@@ -90,6 +90,18 @@
|
|
|
90
90
|
font-size: 100% !important;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
// 数学公式选中高亮样式
|
|
94
|
+
.language-math {
|
|
95
|
+
user-select: all;
|
|
96
|
+
cursor: text;
|
|
97
|
+
transition: background-color 0.1s ease;
|
|
98
|
+
|
|
99
|
+
&.vditor-math--selected {
|
|
100
|
+
background-color: highlight !important;
|
|
101
|
+
color: highlighttext !important;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
93
105
|
& > .vditor-reset > h1:before,
|
|
94
106
|
& > .vditor-reset > h2:before,
|
|
95
107
|
& > .vditor-reset > h3:before,
|
package/src/ts/ir/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { highlightToolbarIR } from "./highlightToolbarIR";
|
|
|
21
21
|
import { input } from "./input";
|
|
22
22
|
import { processAfterRender, processHint } from "./process";
|
|
23
23
|
import { hidePanel } from "../toolbar/setToolbar";
|
|
24
|
+
import { bindMathSelectionListener } from "../util/mathSelection";
|
|
24
25
|
|
|
25
26
|
class IR {
|
|
26
27
|
public range: Range;
|
|
@@ -29,6 +30,7 @@ class IR {
|
|
|
29
30
|
public hlToolbarTimeoutId: number;
|
|
30
31
|
public composingLock: boolean = false;
|
|
31
32
|
public preventInput: boolean;
|
|
33
|
+
private mathSelectionCleanup: () => void;
|
|
32
34
|
|
|
33
35
|
constructor(vditor: IVditor) {
|
|
34
36
|
const divElement = document.createElement("div");
|
|
@@ -52,6 +54,16 @@ class IR {
|
|
|
52
54
|
dropEvent(vditor, this.element);
|
|
53
55
|
copyEvent(vditor, this.element, this.copy);
|
|
54
56
|
cutEvent(vditor, this.element, this.copy);
|
|
57
|
+
|
|
58
|
+
// 绑定数学公式选中高亮监听
|
|
59
|
+
this.mathSelectionCleanup = bindMathSelectionListener(this.element);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public unbindListener() {
|
|
63
|
+
// 清理数学公式选中监听
|
|
64
|
+
if (this.mathSelectionCleanup) {
|
|
65
|
+
this.mathSelectionCleanup();
|
|
66
|
+
}
|
|
55
67
|
}
|
|
56
68
|
|
|
57
69
|
private copy(event: ClipboardEvent, vditor: IVditor) {
|
|
@@ -62,8 +74,39 @@ class IR {
|
|
|
62
74
|
event.stopPropagation();
|
|
63
75
|
event.preventDefault();
|
|
64
76
|
|
|
77
|
+
// 检查选区是否在数学公式元素内
|
|
78
|
+
let mathElement = hasClosestByClassName(
|
|
79
|
+
range.startContainer,
|
|
80
|
+
"language-math"
|
|
81
|
+
) as HTMLElement;
|
|
82
|
+
const mathEndElement = hasClosestByClassName(
|
|
83
|
+
range.endContainer,
|
|
84
|
+
"language-math"
|
|
85
|
+
) as HTMLElement;
|
|
86
|
+
|
|
87
|
+
// 如果选区的起点和终点都在同一个数学公式元素内,复制源码
|
|
88
|
+
if (mathElement && mathEndElement && mathElement.isSameNode(mathEndElement)) {
|
|
89
|
+
const mathSource = mathElement.getAttribute("data-math") || range.toString();
|
|
90
|
+
event.clipboardData.setData("text/plain", mathSource);
|
|
91
|
+
event.clipboardData.setData("text/html", "");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 处理包含多个公式的混合内容
|
|
65
96
|
const tempElement = document.createElement("div");
|
|
66
97
|
tempElement.appendChild(range.cloneContents());
|
|
98
|
+
|
|
99
|
+
const mathElements = tempElement.querySelectorAll(".language-math");
|
|
100
|
+
if (mathElements.length > 0) {
|
|
101
|
+
// 替换所有数学公式为源码
|
|
102
|
+
mathElements.forEach((mathEl: HTMLElement) => {
|
|
103
|
+
const mathSource = mathEl.getAttribute("data-math");
|
|
104
|
+
if (mathSource) {
|
|
105
|
+
const textNode = document.createTextNode(mathSource);
|
|
106
|
+
mathEl.parentNode.replaceChild(textNode, mathEl);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
67
110
|
|
|
68
111
|
event.clipboardData.setData("text/plain", vditor.lute.VditorIRDOM2Md(tempElement.innerHTML).trim());
|
|
69
112
|
event.clipboardData.setData("text/html", "");
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 自定义组件渲染模块
|
|
3
|
+
* 支持将 HTML 标签(如 <read-button label="文献解读">)转换为自定义 HTML
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 解析 HTML 属性字符串
|
|
8
|
+
* @param str 属性字符串,如 'label="文献解读" id="test"'
|
|
9
|
+
* @returns 属性对象
|
|
10
|
+
*/
|
|
11
|
+
export function parseAttributes(str: string): Record<string, string> {
|
|
12
|
+
const attributes: Record<string, string> = {};
|
|
13
|
+
if (!str) return attributes;
|
|
14
|
+
|
|
15
|
+
const attrRegex = /(\w+)="([^"]*)"/g;
|
|
16
|
+
let match;
|
|
17
|
+
while ((match = attrRegex.exec(str)) !== null) {
|
|
18
|
+
attributes[match[1]] = match[2];
|
|
19
|
+
}
|
|
20
|
+
return attributes;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 从 HTML 字符串中提取自定义标签
|
|
25
|
+
* @param html HTML 字符串
|
|
26
|
+
* @param tagName 标签名称,如 'read-button'
|
|
27
|
+
* @returns 匹配的标签信息数组
|
|
28
|
+
*/
|
|
29
|
+
export function extractCustomTags(
|
|
30
|
+
html: string,
|
|
31
|
+
tagName: string
|
|
32
|
+
): Array<{
|
|
33
|
+
fullTag: string;
|
|
34
|
+
attributes: Record<string, string>;
|
|
35
|
+
content: string;
|
|
36
|
+
}> {
|
|
37
|
+
const results: Array<{
|
|
38
|
+
fullTag: string;
|
|
39
|
+
attributes: Record<string, string>;
|
|
40
|
+
content: string;
|
|
41
|
+
}> = [];
|
|
42
|
+
|
|
43
|
+
// 匹配自闭合标签和非自闭合标签
|
|
44
|
+
const selfClosingRegex = new RegExp(`<${tagName}\\s+([^>]*)\\s*/>`, "g");
|
|
45
|
+
const openCloseRegex = new RegExp(
|
|
46
|
+
`<${tagName}\\s+([^>]*)>([^<]*)</${tagName}>`,
|
|
47
|
+
"g"
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
let match;
|
|
51
|
+
|
|
52
|
+
// 处理自闭合标签
|
|
53
|
+
while ((match = selfClosingRegex.exec(html)) !== null) {
|
|
54
|
+
results.push({
|
|
55
|
+
fullTag: match[0],
|
|
56
|
+
attributes: parseAttributes(match[1]),
|
|
57
|
+
content: "",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 处理开闭合标签
|
|
62
|
+
while ((match = openCloseRegex.exec(html)) !== null) {
|
|
63
|
+
results.push({
|
|
64
|
+
fullTag: match[0],
|
|
65
|
+
attributes: parseAttributes(match[1]),
|
|
66
|
+
content: match[2],
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return results;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 自定义组件配置类型
|
|
75
|
+
*/
|
|
76
|
+
export interface ICustomComponent {
|
|
77
|
+
/**
|
|
78
|
+
* HTML 渲染函数 - 用于生成 HTML 字符串
|
|
79
|
+
* @param props 组件属性对象
|
|
80
|
+
* @returns HTML 字符串
|
|
81
|
+
*/
|
|
82
|
+
renderHTML: (props: Record<string, any>) => string;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 组件显示名称
|
|
86
|
+
*/
|
|
87
|
+
displayName?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 自定义组件集合
|
|
92
|
+
*/
|
|
93
|
+
export type ICustomComponents = Record<string, ICustomComponent>;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 使用 HTML 字符串渲染自定义标签
|
|
97
|
+
* @param html HTML 字符串
|
|
98
|
+
* @param tagName 标签名称
|
|
99
|
+
* @param component 自定义组件配置
|
|
100
|
+
* @returns 修改后的 HTML 字符串
|
|
101
|
+
*/
|
|
102
|
+
export function renderCustomComponentHTML(
|
|
103
|
+
html: string,
|
|
104
|
+
tagName: string,
|
|
105
|
+
component: ICustomComponent
|
|
106
|
+
): string {
|
|
107
|
+
const tags = extractCustomTags(html, tagName);
|
|
108
|
+
let result = html;
|
|
109
|
+
|
|
110
|
+
// 反向遍历以保持位置正确性
|
|
111
|
+
for (let i = tags.length - 1; i >= 0; i--) {
|
|
112
|
+
const tag = tags[i];
|
|
113
|
+
const htmlString = component.renderHTML(tag.attributes);
|
|
114
|
+
result = result.replace(tag.fullTag, htmlString);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 在 DOM 元素中查找并渲染自定义组件
|
|
122
|
+
* @param element DOM 元素
|
|
123
|
+
* @param customComponents 自定义组件集合
|
|
124
|
+
*/
|
|
125
|
+
export function renderCustomComponents(
|
|
126
|
+
element: HTMLElement,
|
|
127
|
+
customComponents: ICustomComponents
|
|
128
|
+
): void {
|
|
129
|
+
Object.keys(customComponents).forEach((tagName) => {
|
|
130
|
+
const component = customComponents[tagName];
|
|
131
|
+
const customElements = element.querySelectorAll(tagName);
|
|
132
|
+
console.log(element, customComponents, "customComponents");
|
|
133
|
+
// 反向遍历以避免 DOM 修改时的索引问题
|
|
134
|
+
Array.from(customElements)
|
|
135
|
+
.reverse()
|
|
136
|
+
.forEach((el) => {
|
|
137
|
+
const attributes: Record<string, string> = {};
|
|
138
|
+
el.getAttributeNames().forEach((name) => {
|
|
139
|
+
attributes[name] = el.getAttribute(name) || "";
|
|
140
|
+
});
|
|
141
|
+
// 使用 innerHTML 保留内部 HTML 内容,而不仅仅是纯文本
|
|
142
|
+
const content = el.innerHTML || "";
|
|
143
|
+
|
|
144
|
+
// 使用 HTML 渲染
|
|
145
|
+
const htmlString = component.renderHTML({
|
|
146
|
+
...attributes,
|
|
147
|
+
children: content,
|
|
148
|
+
});
|
|
149
|
+
el.outerHTML = htmlString;
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 批量处理自定义组件 - 用于预处理 HTML 字符串
|
|
156
|
+
* @param html HTML 字符串
|
|
157
|
+
* @param customComponents 自定义组件集合
|
|
158
|
+
* @returns 处理后的 HTML 字符串
|
|
159
|
+
*/
|
|
160
|
+
export function processCustomComponents(
|
|
161
|
+
html: string,
|
|
162
|
+
customComponents: ICustomComponents
|
|
163
|
+
): string {
|
|
164
|
+
let result = html;
|
|
165
|
+
|
|
166
|
+
Object.keys(customComponents).forEach((tagName) => {
|
|
167
|
+
const component = customComponents[tagName];
|
|
168
|
+
result = renderCustomComponentHTML(result, tagName, component);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 处理 markdown 中的自定义组件
|
|
176
|
+
* 将 markdown 中的自定义标签转换为对应的 HTML
|
|
177
|
+
* @param markdown markdown 字符串
|
|
178
|
+
* @param customComponents 自定义组件集合
|
|
179
|
+
* @returns 处理后的 markdown 字符串
|
|
180
|
+
*/
|
|
181
|
+
export function processMarkdownCustomComponents(
|
|
182
|
+
markdown: string,
|
|
183
|
+
customComponents: ICustomComponents
|
|
184
|
+
): string {
|
|
185
|
+
let result = markdown;
|
|
186
|
+
|
|
187
|
+
Object.keys(customComponents).forEach((tagName) => {
|
|
188
|
+
const component = customComponents[tagName];
|
|
189
|
+
result = renderCustomComponentHTML(result, tagName, component);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 导出默认函数和其他公共 API
|
|
197
|
+
*/
|
|
198
|
+
export default function customRender(
|
|
199
|
+
element: HTMLElement,
|
|
200
|
+
customComponents: ICustomComponents
|
|
201
|
+
): void {
|
|
202
|
+
renderCustomComponents(element, customComponents);
|
|
203
|
+
}
|
|
@@ -24,6 +24,8 @@ import { plantumlRender } from "./plantumlRender";
|
|
|
24
24
|
import { setLute } from "./setLute";
|
|
25
25
|
import { speechRender } from "./speechRender";
|
|
26
26
|
import { selectionRender } from "./selectionRender";
|
|
27
|
+
import { bindMathSelectionListener } from "../util/mathSelection";
|
|
28
|
+
import customRender from "./customRender";
|
|
27
29
|
|
|
28
30
|
const mergeOptions = (options?: IPreviewOptions) => {
|
|
29
31
|
const defaultOption: IPreviewOptions = {
|
|
@@ -172,6 +174,10 @@ export const previewRender = async (
|
|
|
172
174
|
);
|
|
173
175
|
}
|
|
174
176
|
|
|
177
|
+
console.log(previewElement, "previewElement");
|
|
178
|
+
|
|
179
|
+
customRender(previewElement, mergedOptions.customComponents || {});
|
|
180
|
+
|
|
175
181
|
setContentTheme(mergedOptions.theme.current, mergedOptions.theme.path);
|
|
176
182
|
if (mergedOptions.anchor === 1) {
|
|
177
183
|
previewElement.classList.add("vditor-reset--anchor");
|
|
@@ -207,6 +213,82 @@ export const previewRender = async (
|
|
|
207
213
|
if (mergedOptions.lazyLoadImage) {
|
|
208
214
|
lazyLoadImageRender(previewElement);
|
|
209
215
|
}
|
|
216
|
+
// 绑定数学公式选中高亮监听
|
|
217
|
+
bindMathSelectionListener(previewElement);
|
|
218
|
+
|
|
219
|
+
// 绑定复制事件,处理数学公式源码复制
|
|
220
|
+
previewElement.addEventListener("copy", (event: ClipboardEvent) => {
|
|
221
|
+
const selection = window.getSelection();
|
|
222
|
+
if (!selection || selection.isCollapsed || selection.rangeCount === 0) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const range = selection.getRangeAt(0);
|
|
227
|
+
|
|
228
|
+
// 检查选区是否只包含单个数学公式
|
|
229
|
+
let mathElement = hasClosestByClassName(
|
|
230
|
+
range.startContainer,
|
|
231
|
+
"language-math"
|
|
232
|
+
) as HTMLElement;
|
|
233
|
+
const mathEndElement = hasClosestByClassName(
|
|
234
|
+
range.endContainer,
|
|
235
|
+
"language-math"
|
|
236
|
+
) as HTMLElement;
|
|
237
|
+
|
|
238
|
+
// 如果选区的起点和终点都在同一个数学公式元素内,复制源码
|
|
239
|
+
if (
|
|
240
|
+
mathElement &&
|
|
241
|
+
mathEndElement &&
|
|
242
|
+
mathElement.isSameNode(mathEndElement)
|
|
243
|
+
) {
|
|
244
|
+
event.stopPropagation();
|
|
245
|
+
event.preventDefault();
|
|
246
|
+
const mathSource =
|
|
247
|
+
mathElement.getAttribute("data-math") || range.toString();
|
|
248
|
+
event.clipboardData.setData("text/plain", mathSource);
|
|
249
|
+
event.clipboardData.setData("text/html", "");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 处理包含多个公式的混合内容
|
|
254
|
+
const fragment = range.cloneContents();
|
|
255
|
+
const mathElements = fragment.querySelectorAll(".language-math");
|
|
256
|
+
|
|
257
|
+
if (mathElements.length > 0) {
|
|
258
|
+
event.stopPropagation();
|
|
259
|
+
event.preventDefault();
|
|
260
|
+
|
|
261
|
+
// 创建临时容器来处理内容
|
|
262
|
+
const tempDiv = document.createElement("div");
|
|
263
|
+
tempDiv.appendChild(fragment);
|
|
264
|
+
|
|
265
|
+
// 替换所有数学公式为源码
|
|
266
|
+
tempDiv
|
|
267
|
+
.querySelectorAll(".language-math")
|
|
268
|
+
.forEach((mathEl: HTMLElement) => {
|
|
269
|
+
const mathSource = mathEl.getAttribute("data-math");
|
|
270
|
+
console.log(mathEl.tagName, "mathEl");
|
|
271
|
+
if (mathSource) {
|
|
272
|
+
if (mathEl.tagName === "SPAN") {
|
|
273
|
+
const textNode = document.createTextNode(
|
|
274
|
+
`$${mathSource}$`
|
|
275
|
+
);
|
|
276
|
+
mathEl.parentNode.replaceChild(textNode, mathEl);
|
|
277
|
+
} else {
|
|
278
|
+
const textNode = document.createTextNode(
|
|
279
|
+
`$$${mathSource}$$`
|
|
280
|
+
);
|
|
281
|
+
mathEl.parentNode.replaceChild(textNode, mathEl);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// 获取纯文本内容
|
|
287
|
+
const plainText = tempDiv.textContent || tempDiv.innerText || "";
|
|
288
|
+
event.clipboardData.setData("text/plain", plainText);
|
|
289
|
+
event.clipboardData.setData("text/html", "");
|
|
290
|
+
}
|
|
291
|
+
});
|
|
210
292
|
|
|
211
293
|
previewElement.addEventListener(
|
|
212
294
|
"click",
|
package/src/ts/preview/index.ts
CHANGED
|
@@ -19,11 +19,13 @@ import {
|
|
|
19
19
|
import { setSelectionFocus } from "../util/selection";
|
|
20
20
|
import { selectionRender } from "../markdown/selectionRender";
|
|
21
21
|
import { previewImage } from "./image";
|
|
22
|
+
import { bindMathSelectionListener } from "../util/mathSelection";
|
|
22
23
|
|
|
23
24
|
export class Preview {
|
|
24
25
|
public element: HTMLElement;
|
|
25
26
|
public previewElement: HTMLElement;
|
|
26
27
|
private mdTimeoutId: number;
|
|
28
|
+
private mathSelectionCleanup: () => void;
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
31
|
* 构造预览区域容器,注册复制与点击事件。
|
|
@@ -98,6 +100,18 @@ export class Preview {
|
|
|
98
100
|
}
|
|
99
101
|
);
|
|
100
102
|
this.element.appendChild(this.previewElement);
|
|
103
|
+
|
|
104
|
+
// 绑定数学公式选中高亮监听
|
|
105
|
+
this.mathSelectionCleanup = bindMathSelectionListener(
|
|
106
|
+
this.previewElement
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public unbindListener() {
|
|
111
|
+
// 清理数学公式选中监听
|
|
112
|
+
if (this.mathSelectionCleanup) {
|
|
113
|
+
this.mathSelectionCleanup();
|
|
114
|
+
}
|
|
101
115
|
}
|
|
102
116
|
|
|
103
117
|
/**
|
package/src/ts/sv/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { paste } from "../util/fixBrowserBehavior";
|
|
|
12
12
|
import { getSelectText } from "../util/getSelectText";
|
|
13
13
|
import { inputEvent } from "./inputEvent";
|
|
14
14
|
import { processAfterRender } from "./process";
|
|
15
|
+
import { bindMathSelectionListener } from "../util/mathSelection";
|
|
15
16
|
|
|
16
17
|
class Editor {
|
|
17
18
|
public range: Range;
|
|
@@ -20,6 +21,7 @@ class Editor {
|
|
|
20
21
|
public processTimeoutId: number;
|
|
21
22
|
public hlToolbarTimeoutId: number;
|
|
22
23
|
public preventInput: boolean;
|
|
24
|
+
private mathSelectionCleanup: () => void;
|
|
23
25
|
|
|
24
26
|
constructor(vditor: IVditor) {
|
|
25
27
|
this.element = document.createElement("pre");
|
|
@@ -40,6 +42,16 @@ class Editor {
|
|
|
40
42
|
dropEvent(vditor, this.element);
|
|
41
43
|
copyEvent(vditor, this.element, this.copy);
|
|
42
44
|
cutEvent(vditor, this.element, this.copy);
|
|
45
|
+
|
|
46
|
+
// 绑定数学公式选中高亮监听
|
|
47
|
+
this.mathSelectionCleanup = bindMathSelectionListener(this.element);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public unbindListener() {
|
|
51
|
+
// 清理数学公式选中监听
|
|
52
|
+
if (this.mathSelectionCleanup) {
|
|
53
|
+
this.mathSelectionCleanup();
|
|
54
|
+
}
|
|
43
55
|
}
|
|
44
56
|
|
|
45
57
|
private copy(event: ClipboardEvent, vditor: IVditor) {
|
package/src/ts/util/Options.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {hasClosestByTag} from "./hasClosestByHeadings";
|
|
1
|
+
import { hasClosestByTag } from "./hasClosestByHeadings";
|
|
2
2
|
|
|
3
3
|
export const hasTopClosestByClassName = (element: Node, className: string) => {
|
|
4
4
|
let closest = hasClosestByClassName(element, className);
|
|
@@ -15,12 +15,20 @@ export const hasTopClosestByClassName = (element: Node, className: string) => {
|
|
|
15
15
|
return closest || false;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
export const hasTopClosestByAttribute = (
|
|
18
|
+
export const hasTopClosestByAttribute = (
|
|
19
|
+
element: Node,
|
|
20
|
+
attr: string,
|
|
21
|
+
value: string
|
|
22
|
+
) => {
|
|
19
23
|
let closest = hasClosestByAttribute(element, attr, value);
|
|
20
24
|
let parentClosest: boolean | HTMLElement = false;
|
|
21
25
|
let findTop = false;
|
|
22
26
|
while (closest && !closest.classList.contains("vditor-reset") && !findTop) {
|
|
23
|
-
parentClosest = hasClosestByAttribute(
|
|
27
|
+
parentClosest = hasClosestByAttribute(
|
|
28
|
+
closest.parentElement,
|
|
29
|
+
attr,
|
|
30
|
+
value
|
|
31
|
+
);
|
|
24
32
|
if (parentClosest) {
|
|
25
33
|
closest = parentClosest;
|
|
26
34
|
} else {
|
|
@@ -49,13 +57,20 @@ export const getTopList = (element: Node) => {
|
|
|
49
57
|
const topUlElement = hasTopClosestByTag(element, "UL");
|
|
50
58
|
const topOlElement = hasTopClosestByTag(element, "OL");
|
|
51
59
|
let topListElement = topUlElement;
|
|
52
|
-
if (
|
|
60
|
+
if (
|
|
61
|
+
topOlElement &&
|
|
62
|
+
(!topUlElement || (topUlElement && topOlElement.contains(topUlElement)))
|
|
63
|
+
) {
|
|
53
64
|
topListElement = topOlElement;
|
|
54
65
|
}
|
|
55
66
|
return topListElement;
|
|
56
67
|
};
|
|
57
68
|
|
|
58
|
-
export const hasClosestByAttribute = (
|
|
69
|
+
export const hasClosestByAttribute = (
|
|
70
|
+
element: Node,
|
|
71
|
+
attr: string,
|
|
72
|
+
value: string
|
|
73
|
+
) => {
|
|
59
74
|
if (!element) {
|
|
60
75
|
return false;
|
|
61
76
|
}
|
|
@@ -84,13 +99,18 @@ export const hasClosestBlock = (element: Node) => {
|
|
|
84
99
|
let e = element as HTMLElement;
|
|
85
100
|
let isClosest = false;
|
|
86
101
|
|
|
87
|
-
const blockElement = hasClosestByAttribute(
|
|
102
|
+
const blockElement = hasClosestByAttribute(
|
|
103
|
+
element as HTMLElement,
|
|
104
|
+
"data-block",
|
|
105
|
+
"0"
|
|
106
|
+
);
|
|
88
107
|
if (blockElement) {
|
|
89
108
|
return blockElement;
|
|
90
109
|
}
|
|
91
110
|
|
|
92
111
|
while (e && !isClosest && !e.classList.contains("vditor-reset")) {
|
|
93
|
-
if (
|
|
112
|
+
if (
|
|
113
|
+
e.tagName === "H1" ||
|
|
94
114
|
e.tagName === "H2" ||
|
|
95
115
|
e.tagName === "H3" ||
|
|
96
116
|
e.tagName === "H4" ||
|
|
@@ -99,7 +119,8 @@ export const hasClosestBlock = (element: Node) => {
|
|
|
99
119
|
e.tagName === "P" ||
|
|
100
120
|
e.tagName === "BLOCKQUOTE" ||
|
|
101
121
|
e.tagName === "OL" ||
|
|
102
|
-
e.tagName === "UL"
|
|
122
|
+
e.tagName === "UL"
|
|
123
|
+
) {
|
|
103
124
|
isClosest = true;
|
|
104
125
|
} else {
|
|
105
126
|
e = e.parentElement;
|