@cgboiler/biz-basic 1.0.47 → 1.0.48
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/es/index.d.ts +1 -1
- package/es/index.js +1 -1
- package/es/rich-text-editor/useExtensions.d.ts +2 -7
- package/es/rich-text-editor/useExtensions.js +88 -152
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/rich-text-editor/useExtensions.d.ts +2 -7
- package/lib/rich-text-editor/useExtensions.js +87 -151
- package/package.json +1 -1
package/es/index.d.ts
CHANGED
package/es/index.js
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
import { Extension } from '@tiptap/core';
|
|
1
|
+
import { Extension, Mark } from '@tiptap/core';
|
|
2
2
|
/**
|
|
3
3
|
* useExtensions
|
|
4
4
|
* - 功能:构建并返回 TipTap 编辑器使用的扩展集合。
|
|
5
|
-
* - 参数说明:
|
|
6
|
-
* - props: 组件属性对象,至少应包含占位符 `placeholder` 等。
|
|
7
|
-
* - emit: 事件发送函数,用于向父组件触发富文本相关事件(如 hashTag-triggered / hashTag-input / hashTag-exit)。
|
|
8
|
-
* - 返回值:Extension[] 扩展数组,用于初始化 TipTap 编辑器。
|
|
9
|
-
* - 异常:无显式抛出;若内部命令执行失败(例如插入内容时)可能由 TipTap 抛出异常,应在上层统一处理。
|
|
10
5
|
*/
|
|
11
|
-
export declare function useExtensions({ props, emit }: any): (import("@tiptap/core").Node<any, any> | import("@tiptap/core").Node<import("@tiptap/extension-mention").MentionOptions<any, import("@tiptap/extension-mention").MentionNodeAttrs>, any> | Extension<
|
|
6
|
+
export declare function useExtensions({ props, emit }: any): (import("@tiptap/core").Node<any, any> | Mark<any, any> | import("@tiptap/core").Node<import("@tiptap/extension-mention").MentionOptions<any, import("@tiptap/extension-mention").MentionNodeAttrs>, any> | Extension<import("@tiptap/starter-kit").StarterKitOptions, any> | Extension<any, any> | Mark<import("@tiptap/extension-text-style").TextStyleOptions, any> | Extension<import("@tiptap/extension-text-style").ColorOptions, any> | Extension<import("tiptap-markdown").MarkdownOptions, import("tiptap-markdown").MarkdownStorage> | import("@tiptap/core").Node<import("@tiptap/extension-image").ImageOptions, any> | Extension<import("@tiptap/extension-table").TableKitOptions, any> | Extension<import("@tiptap/extensions").PlaceholderOptions, any> | Extension<import("@tiptap/extension-list").ListKitOptions, any>)[];
|
|
@@ -6,33 +6,74 @@ import Mention from "@tiptap/extension-mention";
|
|
|
6
6
|
import { TextStyle } from "@tiptap/extension-text-style";
|
|
7
7
|
import { Color } from "@tiptap/extension-color";
|
|
8
8
|
import { Markdown } from "tiptap-markdown";
|
|
9
|
-
import { Extension, InputRule } from "@tiptap/core";
|
|
9
|
+
import { Extension, InputRule, Mark, mergeAttributes } from "@tiptap/core";
|
|
10
10
|
import HtmlBlock from "./extensions/HtmlBlock";
|
|
11
11
|
import { Placeholder } from "@tiptap/extensions";
|
|
12
12
|
function useExtensions({ props, emit }) {
|
|
13
|
-
let spentHashTagPositions = [];
|
|
14
|
-
let spentMentionPositions = [];
|
|
15
13
|
let activeEditor = null;
|
|
14
|
+
const ConsumedTrigger = Mark.create({
|
|
15
|
+
name: "consumedTrigger",
|
|
16
|
+
keepOnSplit: false,
|
|
17
|
+
// 保证在标记后继续输入时不延续标记
|
|
18
|
+
parseHTML() {
|
|
19
|
+
return [{ tag: "span[data-consumed-trigger]" }];
|
|
20
|
+
},
|
|
21
|
+
renderHTML({ HTMLAttributes }) {
|
|
22
|
+
return [
|
|
23
|
+
"span",
|
|
24
|
+
mergeAttributes(HTMLAttributes, {
|
|
25
|
+
"data-consumed-trigger": "true",
|
|
26
|
+
style: "color: inherit;"
|
|
27
|
+
}),
|
|
28
|
+
0
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
const checkConsumed = (state, range) => {
|
|
33
|
+
const { doc, schema } = state;
|
|
34
|
+
const markType = schema.marks.consumedTrigger;
|
|
35
|
+
if (!markType)
|
|
36
|
+
return false;
|
|
37
|
+
let isConsumed = false;
|
|
38
|
+
doc.nodesBetween(range.from, range.to, (node) => {
|
|
39
|
+
if (node.marks.some((m) => m.type === markType)) {
|
|
40
|
+
isConsumed = true;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return isConsumed;
|
|
44
|
+
};
|
|
45
|
+
const markAsConsumed = (editor, range, char) => {
|
|
46
|
+
if (!editor || !range || range.from === void 0)
|
|
47
|
+
return;
|
|
48
|
+
try {
|
|
49
|
+
const { doc } = editor.state;
|
|
50
|
+
const actualChar = doc.textBetween(range.from, range.from + 1);
|
|
51
|
+
if (actualChar === char) {
|
|
52
|
+
editor.chain().setMeta("addToHistory", false).setTextSelection({ from: range.from, to: range.from + 1 }).setMark("consumedTrigger").setTextSelection(editor.state.selection.to).unsetMark("consumedTrigger").run();
|
|
53
|
+
}
|
|
54
|
+
} catch (e) {
|
|
55
|
+
}
|
|
56
|
+
};
|
|
16
57
|
const HashTag = Mention.extend({
|
|
17
58
|
name: "hashTag",
|
|
18
|
-
renderHTML({ node
|
|
59
|
+
renderHTML({ node }) {
|
|
19
60
|
var _a, _b, _c, _d;
|
|
20
61
|
const id = (_b = (_a = node == null ? void 0 : node.attrs) == null ? void 0 : _a.id) != null ? _b : "";
|
|
21
62
|
const label = (_d = (_c = node == null ? void 0 : node.attrs) == null ? void 0 : _c.label) != null ? _d : "";
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
63
|
+
return [
|
|
64
|
+
"span",
|
|
65
|
+
{
|
|
66
|
+
class: "hash-tag",
|
|
67
|
+
"data-type": "hashTag",
|
|
68
|
+
"data-id": id,
|
|
69
|
+
"data-label": label,
|
|
70
|
+
"data-hash-tag": "#",
|
|
71
|
+
contenteditable: "false"
|
|
72
|
+
},
|
|
73
|
+
`#${label}`
|
|
74
|
+
];
|
|
31
75
|
}
|
|
32
76
|
}).configure({
|
|
33
|
-
HTMLAttributes: {
|
|
34
|
-
class: "hash-tag"
|
|
35
|
-
},
|
|
36
77
|
deleteTriggerWithBackspace: true,
|
|
37
78
|
suggestion: {
|
|
38
79
|
char: "#",
|
|
@@ -41,13 +82,10 @@ function useExtensions({ props, emit }) {
|
|
|
41
82
|
/**
|
|
42
83
|
* allow
|
|
43
84
|
* - 功能:判断是否允许触发建议态。
|
|
44
|
-
* -
|
|
85
|
+
* - 若该字符带有 consumedTrigger 标记,说明此前已触发过或用户选择忽略,则不再触发建议。
|
|
45
86
|
*/
|
|
46
87
|
allow: ({ state, range }) => {
|
|
47
|
-
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
return true;
|
|
88
|
+
return !checkConsumed(state, range);
|
|
51
89
|
},
|
|
52
90
|
render() {
|
|
53
91
|
let activeRange = null;
|
|
@@ -75,111 +113,49 @@ function useExtensions({ props, emit }) {
|
|
|
75
113
|
activeEditor = editor || activeEditor;
|
|
76
114
|
emit("hashTag-input", query != null ? query : "");
|
|
77
115
|
},
|
|
78
|
-
/**
|
|
79
|
-
* onKeyDown
|
|
80
|
-
* - 功能:在建议态期间侦测用户键盘行为并记录“非连续输入”的退出原因。
|
|
81
|
-
* - 参数说明:
|
|
82
|
-
* - event: KeyboardEvent 当前键盘事件对象。
|
|
83
|
-
* - 返回值:boolean 始终返回 false,交由默认逻辑处理(从而触发 onExit)。
|
|
84
|
-
* - 异常:无。
|
|
85
|
-
*/
|
|
86
116
|
onKeyDown({ event }) {
|
|
87
|
-
const isSpaceKey =
|
|
88
|
-
event.keyCode === 32 ||
|
|
89
|
-
event.code === "Space";
|
|
117
|
+
const isSpaceKey = [" ", "Space", "Spacebar"].includes(event.key) || // @ts-ignore
|
|
118
|
+
event.keyCode === 32 || event.code === "Space";
|
|
90
119
|
if (isSpaceKey) {
|
|
91
120
|
exitCause = "space";
|
|
92
|
-
return false;
|
|
93
121
|
}
|
|
94
122
|
return false;
|
|
95
123
|
},
|
|
96
|
-
|
|
97
|
-
* onExit
|
|
98
|
-
* - 功能:建议态结束时向外部发送退出事件,并尽可能准确地判断退出原因。
|
|
99
|
-
* - 设计考量:
|
|
100
|
-
* - 安卓设备上软键盘可能不触发标准 keydown 空格;此时通过检查当前选择位置前一个字符是否为空格来回溯判断。
|
|
101
|
-
* - 参数:无。
|
|
102
|
-
* - 返回值:无(通过 emit 派发事件)。
|
|
103
|
-
* - 异常:内部读取选择/文档时若发生异常,捕获后保持原有退出原因,不影响外部流程。
|
|
104
|
-
*/
|
|
105
|
-
onExit({ range }) {
|
|
124
|
+
onExit({ editor, range }) {
|
|
106
125
|
var _a, _b, _c, _d, _e;
|
|
107
126
|
try {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (prevChar === " ") {
|
|
114
|
-
exitCause = "space";
|
|
115
|
-
}
|
|
127
|
+
const sel = (_a = editor.state) == null ? void 0 : _a.selection;
|
|
128
|
+
const from = (_b = sel == null ? void 0 : sel.from) != null ? _b : 0;
|
|
129
|
+
const prevChar = (_e = (_d = (_c = editor.state) == null ? void 0 : _c.doc) == null ? void 0 : _d.textBetween(Math.max(from - 1, 0), from, "\0", "\0")) != null ? _e : "";
|
|
130
|
+
if (prevChar === " ") {
|
|
131
|
+
exitCause = "space";
|
|
116
132
|
}
|
|
117
133
|
} catch (e) {
|
|
118
134
|
}
|
|
119
|
-
|
|
120
|
-
spentHashTagPositions.push(range.from);
|
|
121
|
-
}
|
|
135
|
+
markAsConsumed(editor, range, "#");
|
|
122
136
|
activeRange = null;
|
|
123
137
|
activeEditor = null;
|
|
124
|
-
|
|
138
|
+
emit("hashTag-exit", {
|
|
125
139
|
reason: exitCause,
|
|
126
140
|
nonContiguous: exitCause === "space"
|
|
127
|
-
};
|
|
128
|
-
emit("hashTag-exit", payload);
|
|
141
|
+
});
|
|
129
142
|
}
|
|
130
143
|
};
|
|
131
144
|
}
|
|
132
145
|
}
|
|
133
146
|
});
|
|
134
|
-
const HashTagTracker = Extension.create({
|
|
135
|
-
name: "hashTagTracker",
|
|
136
|
-
onTransaction({ transaction }) {
|
|
137
|
-
if (spentHashTagPositions.length === 0)
|
|
138
|
-
return;
|
|
139
|
-
spentHashTagPositions = spentHashTagPositions.map((pos) => transaction.mapping.map(pos, -1));
|
|
140
|
-
spentHashTagPositions = spentHashTagPositions.filter((pos) => {
|
|
141
|
-
try {
|
|
142
|
-
if (pos < 0 || pos >= transaction.doc.content.size)
|
|
143
|
-
return false;
|
|
144
|
-
const char = transaction.doc.textBetween(pos, pos + 1);
|
|
145
|
-
return char === "#";
|
|
146
|
-
} catch (e) {
|
|
147
|
-
return false;
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
spentHashTagPositions = Array.from(new Set(spentHashTagPositions));
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
const MentionTracker = Extension.create({
|
|
154
|
-
name: "mentionTracker",
|
|
155
|
-
onTransaction({ transaction }) {
|
|
156
|
-
if (spentMentionPositions.length === 0)
|
|
157
|
-
return;
|
|
158
|
-
spentMentionPositions = spentMentionPositions.map((pos) => transaction.mapping.map(pos, -1));
|
|
159
|
-
spentMentionPositions = spentMentionPositions.filter((pos) => {
|
|
160
|
-
try {
|
|
161
|
-
if (pos < 0 || pos >= transaction.doc.content.size)
|
|
162
|
-
return false;
|
|
163
|
-
const char = transaction.doc.textBetween(pos, pos + 1);
|
|
164
|
-
return char === "@";
|
|
165
|
-
} catch (e) {
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
spentMentionPositions = Array.from(new Set(spentMentionPositions));
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
147
|
const extensions = [
|
|
173
148
|
StarterKit,
|
|
174
149
|
HtmlBlock,
|
|
175
|
-
|
|
150
|
+
ConsumedTrigger,
|
|
151
|
+
// Video input rule extension
|
|
176
152
|
Extension.create({
|
|
177
153
|
name: "videoInputRule",
|
|
178
154
|
addInputRules() {
|
|
179
155
|
return [
|
|
180
156
|
new InputRule({
|
|
181
157
|
find: /!video\(([^)]+)\)\s/,
|
|
182
|
-
handler: ({
|
|
158
|
+
handler: ({ range, match, commands }) => {
|
|
183
159
|
const [, url] = match;
|
|
184
160
|
if (url) {
|
|
185
161
|
const html = `<video src="${url}" autoplay="" loop="" muted="" controls="" playsinline=""></video>`;
|
|
@@ -200,7 +176,7 @@ function useExtensions({ props, emit }) {
|
|
|
200
176
|
return [
|
|
201
177
|
new InputRule({
|
|
202
178
|
find: /!audio\(([^)]+)\)\s/,
|
|
203
|
-
handler: ({
|
|
179
|
+
handler: ({ range, match, commands }) => {
|
|
204
180
|
const [, url] = match;
|
|
205
181
|
if (url) {
|
|
206
182
|
const html = `<audio src="${url}" controls="" playsinline=""></audio>`;
|
|
@@ -215,14 +191,8 @@ function useExtensions({ props, emit }) {
|
|
|
215
191
|
];
|
|
216
192
|
}
|
|
217
193
|
}),
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
// 如果你还想支持其他 style 属性,可以在这里配置
|
|
221
|
-
// HTMLAttributes: { class: 'my-custom-class' }
|
|
222
|
-
}),
|
|
223
|
-
Color.configure({
|
|
224
|
-
// types: ['textStyle'] 默认就是这个,一般不需要改
|
|
225
|
-
}),
|
|
194
|
+
TextStyle,
|
|
195
|
+
Color,
|
|
226
196
|
Markdown.configure({
|
|
227
197
|
html: true,
|
|
228
198
|
tightLists: true,
|
|
@@ -233,27 +203,19 @@ function useExtensions({ props, emit }) {
|
|
|
233
203
|
transformPastedText: true,
|
|
234
204
|
transformCopiedText: true
|
|
235
205
|
}),
|
|
236
|
-
// 自定义扩展来支持 Markdown 链接语法
|
|
237
206
|
Extension.create({
|
|
238
207
|
name: "markdownLinkInputRule",
|
|
239
208
|
addInputRules() {
|
|
240
209
|
return [
|
|
241
210
|
new InputRule({
|
|
242
211
|
find: /\[([^\]]+)\]\(([^\)]+)\)\s/,
|
|
243
|
-
handler: ({
|
|
212
|
+
handler: ({ range, match, commands }) => {
|
|
244
213
|
const [, text, href] = match;
|
|
245
|
-
|
|
246
|
-
const end = range.to;
|
|
247
|
-
commands.deleteRange({ from: start, to: end });
|
|
214
|
+
commands.deleteRange({ from: range.from, to: range.to });
|
|
248
215
|
commands.insertContent({
|
|
249
216
|
type: "text",
|
|
250
217
|
text,
|
|
251
|
-
marks: [
|
|
252
|
-
{
|
|
253
|
-
type: "link",
|
|
254
|
-
attrs: { href }
|
|
255
|
-
}
|
|
256
|
-
]
|
|
218
|
+
marks: [{ type: "link", attrs: { href } }]
|
|
257
219
|
});
|
|
258
220
|
}
|
|
259
221
|
})
|
|
@@ -265,9 +227,7 @@ function useExtensions({ props, emit }) {
|
|
|
265
227
|
table: { resizable: true }
|
|
266
228
|
}),
|
|
267
229
|
Placeholder.configure({
|
|
268
|
-
placeholder: () =>
|
|
269
|
-
return props.placeholder;
|
|
270
|
-
}
|
|
230
|
+
placeholder: () => props.placeholder
|
|
271
231
|
}),
|
|
272
232
|
ListKit.extend({
|
|
273
233
|
addKeyboardShortcuts() {
|
|
@@ -275,18 +235,11 @@ function useExtensions({ props, emit }) {
|
|
|
275
235
|
Tab: ({ editor }) => {
|
|
276
236
|
const { $from } = editor.state.selection;
|
|
277
237
|
const currentItem = $from.node(-1);
|
|
278
|
-
|
|
279
|
-
return true;
|
|
280
|
-
}
|
|
281
|
-
return false;
|
|
238
|
+
return currentItem.type.name === "listItem";
|
|
282
239
|
},
|
|
283
240
|
"Shift-Tab": ({ editor }) => {
|
|
284
241
|
const { $from } = editor.state.selection;
|
|
285
|
-
|
|
286
|
-
if (currentItem.type.name === "doc") {
|
|
287
|
-
return true;
|
|
288
|
-
}
|
|
289
|
-
return false;
|
|
242
|
+
return $from.node(-1).type.name === "doc";
|
|
290
243
|
}
|
|
291
244
|
};
|
|
292
245
|
}
|
|
@@ -300,32 +253,23 @@ function useExtensions({ props, emit }) {
|
|
|
300
253
|
char: "@",
|
|
301
254
|
allowedPrefixes: null,
|
|
302
255
|
allowSpaces: false,
|
|
303
|
-
/**
|
|
304
|
-
* allow
|
|
305
|
-
* - 功能:判断是否允许触发建议态。
|
|
306
|
-
* - 1. 当@ 前面一个字符是数字/英文时,不触发,防止用户是想要输入邮箱。
|
|
307
|
-
* - 2. 触发以后如果包含数字,则收起(通过返回 false 销毁建议态)。
|
|
308
|
-
*/
|
|
309
256
|
allow: ({ state, range }) => {
|
|
310
|
-
if (
|
|
257
|
+
if (checkConsumed(state, range))
|
|
311
258
|
return false;
|
|
312
|
-
}
|
|
313
259
|
const doc = state.doc;
|
|
314
260
|
const $from = doc.resolve(range.from);
|
|
315
261
|
const isAtStart = range.from === $from.start();
|
|
316
262
|
const prevChar = isAtStart ? "" : doc.textBetween(range.from - 1, range.from);
|
|
317
|
-
if (/[a-zA-Z0-9]/.test(prevChar))
|
|
263
|
+
if (/[a-zA-Z0-9]/.test(prevChar))
|
|
318
264
|
return false;
|
|
319
|
-
}
|
|
320
265
|
const query = doc.textBetween(range.from + 1, range.to);
|
|
321
|
-
if (/[0-9]/.test(query))
|
|
266
|
+
if (/[0-9]/.test(query))
|
|
322
267
|
return false;
|
|
323
|
-
}
|
|
324
268
|
return true;
|
|
325
269
|
},
|
|
326
270
|
render() {
|
|
327
271
|
return {
|
|
328
|
-
onStart({ editor,
|
|
272
|
+
onStart({ editor, command, query }) {
|
|
329
273
|
activeEditor = editor;
|
|
330
274
|
emit("mention-triggered", (data) => {
|
|
331
275
|
command({
|
|
@@ -339,22 +283,14 @@ function useExtensions({ props, emit }) {
|
|
|
339
283
|
activeEditor = editor;
|
|
340
284
|
emit("mention-input", query != null ? query : "");
|
|
341
285
|
},
|
|
342
|
-
onExit({ range }) {
|
|
343
|
-
|
|
344
|
-
spentMentionPositions.push(range.from);
|
|
345
|
-
}
|
|
286
|
+
onExit({ editor, range }) {
|
|
287
|
+
markAsConsumed(editor, range, "@");
|
|
346
288
|
emit("mention-exit");
|
|
347
289
|
}
|
|
348
290
|
};
|
|
349
291
|
}
|
|
350
292
|
}
|
|
351
293
|
}),
|
|
352
|
-
/**
|
|
353
|
-
* 辅助扩展:追踪记录已失活的触发点
|
|
354
|
-
*/
|
|
355
|
-
MentionTracker,
|
|
356
|
-
HashTagTracker,
|
|
357
|
-
// HashTag 扩展:支持 # 触发、实时输入同步、完成后插入为 “#+文字”
|
|
358
294
|
HashTag
|
|
359
295
|
];
|
|
360
296
|
return extensions;
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -36,7 +36,7 @@ __export(stdin_exports, {
|
|
|
36
36
|
module.exports = __toCommonJS(stdin_exports);
|
|
37
37
|
var import_rich_text_editor = __toESM(require("./rich-text-editor"));
|
|
38
38
|
__reExport(stdin_exports, require("./rich-text-editor"), module.exports);
|
|
39
|
-
const version = "1.0.
|
|
39
|
+
const version = "1.0.47";
|
|
40
40
|
function install(app) {
|
|
41
41
|
const components = [
|
|
42
42
|
import_rich_text_editor.default
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
import { Extension } from '@tiptap/core';
|
|
1
|
+
import { Extension, Mark } from '@tiptap/core';
|
|
2
2
|
/**
|
|
3
3
|
* useExtensions
|
|
4
4
|
* - 功能:构建并返回 TipTap 编辑器使用的扩展集合。
|
|
5
|
-
* - 参数说明:
|
|
6
|
-
* - props: 组件属性对象,至少应包含占位符 `placeholder` 等。
|
|
7
|
-
* - emit: 事件发送函数,用于向父组件触发富文本相关事件(如 hashTag-triggered / hashTag-input / hashTag-exit)。
|
|
8
|
-
* - 返回值:Extension[] 扩展数组,用于初始化 TipTap 编辑器。
|
|
9
|
-
* - 异常:无显式抛出;若内部命令执行失败(例如插入内容时)可能由 TipTap 抛出异常,应在上层统一处理。
|
|
10
5
|
*/
|
|
11
|
-
export declare function useExtensions({ props, emit }: any): (import("@tiptap/core").Node<any, any> | import("@tiptap/core").Node<import("@tiptap/extension-mention").MentionOptions<any, import("@tiptap/extension-mention").MentionNodeAttrs>, any> | Extension<
|
|
6
|
+
export declare function useExtensions({ props, emit }: any): (import("@tiptap/core").Node<any, any> | Mark<any, any> | import("@tiptap/core").Node<import("@tiptap/extension-mention").MentionOptions<any, import("@tiptap/extension-mention").MentionNodeAttrs>, any> | Extension<import("@tiptap/starter-kit").StarterKitOptions, any> | Extension<any, any> | Mark<import("@tiptap/extension-text-style").TextStyleOptions, any> | Extension<import("@tiptap/extension-text-style").ColorOptions, any> | Extension<import("tiptap-markdown").MarkdownOptions, import("tiptap-markdown").MarkdownStorage> | import("@tiptap/core").Node<import("@tiptap/extension-image").ImageOptions, any> | Extension<import("@tiptap/extension-table").TableKitOptions, any> | Extension<import("@tiptap/extensions").PlaceholderOptions, any> | Extension<import("@tiptap/extension-list").ListKitOptions, any>)[];
|
|
@@ -42,29 +42,70 @@ var import_core = require("@tiptap/core");
|
|
|
42
42
|
var import_HtmlBlock = __toESM(require("./extensions/HtmlBlock"));
|
|
43
43
|
var import_extensions = require("@tiptap/extensions");
|
|
44
44
|
function useExtensions({ props, emit }) {
|
|
45
|
-
let spentHashTagPositions = [];
|
|
46
|
-
let spentMentionPositions = [];
|
|
47
45
|
let activeEditor = null;
|
|
46
|
+
const ConsumedTrigger = import_core.Mark.create({
|
|
47
|
+
name: "consumedTrigger",
|
|
48
|
+
keepOnSplit: false,
|
|
49
|
+
// 保证在标记后继续输入时不延续标记
|
|
50
|
+
parseHTML() {
|
|
51
|
+
return [{ tag: "span[data-consumed-trigger]" }];
|
|
52
|
+
},
|
|
53
|
+
renderHTML({ HTMLAttributes }) {
|
|
54
|
+
return [
|
|
55
|
+
"span",
|
|
56
|
+
(0, import_core.mergeAttributes)(HTMLAttributes, {
|
|
57
|
+
"data-consumed-trigger": "true",
|
|
58
|
+
style: "color: inherit;"
|
|
59
|
+
}),
|
|
60
|
+
0
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
const checkConsumed = (state, range) => {
|
|
65
|
+
const { doc, schema } = state;
|
|
66
|
+
const markType = schema.marks.consumedTrigger;
|
|
67
|
+
if (!markType)
|
|
68
|
+
return false;
|
|
69
|
+
let isConsumed = false;
|
|
70
|
+
doc.nodesBetween(range.from, range.to, (node) => {
|
|
71
|
+
if (node.marks.some((m) => m.type === markType)) {
|
|
72
|
+
isConsumed = true;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
return isConsumed;
|
|
76
|
+
};
|
|
77
|
+
const markAsConsumed = (editor, range, char) => {
|
|
78
|
+
if (!editor || !range || range.from === void 0)
|
|
79
|
+
return;
|
|
80
|
+
try {
|
|
81
|
+
const { doc } = editor.state;
|
|
82
|
+
const actualChar = doc.textBetween(range.from, range.from + 1);
|
|
83
|
+
if (actualChar === char) {
|
|
84
|
+
editor.chain().setMeta("addToHistory", false).setTextSelection({ from: range.from, to: range.from + 1 }).setMark("consumedTrigger").setTextSelection(editor.state.selection.to).unsetMark("consumedTrigger").run();
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {
|
|
87
|
+
}
|
|
88
|
+
};
|
|
48
89
|
const HashTag = import_extension_mention.default.extend({
|
|
49
90
|
name: "hashTag",
|
|
50
|
-
renderHTML({ node
|
|
91
|
+
renderHTML({ node }) {
|
|
51
92
|
var _a, _b, _c, _d;
|
|
52
93
|
const id = (_b = (_a = node == null ? void 0 : node.attrs) == null ? void 0 : _a.id) != null ? _b : "";
|
|
53
94
|
const label = (_d = (_c = node == null ? void 0 : node.attrs) == null ? void 0 : _c.label) != null ? _d : "";
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
95
|
+
return [
|
|
96
|
+
"span",
|
|
97
|
+
{
|
|
98
|
+
class: "hash-tag",
|
|
99
|
+
"data-type": "hashTag",
|
|
100
|
+
"data-id": id,
|
|
101
|
+
"data-label": label,
|
|
102
|
+
"data-hash-tag": "#",
|
|
103
|
+
contenteditable: "false"
|
|
104
|
+
},
|
|
105
|
+
`#${label}`
|
|
106
|
+
];
|
|
63
107
|
}
|
|
64
108
|
}).configure({
|
|
65
|
-
HTMLAttributes: {
|
|
66
|
-
class: "hash-tag"
|
|
67
|
-
},
|
|
68
109
|
deleteTriggerWithBackspace: true,
|
|
69
110
|
suggestion: {
|
|
70
111
|
char: "#",
|
|
@@ -73,13 +114,10 @@ function useExtensions({ props, emit }) {
|
|
|
73
114
|
/**
|
|
74
115
|
* allow
|
|
75
116
|
* - 功能:判断是否允许触发建议态。
|
|
76
|
-
* -
|
|
117
|
+
* - 若该字符带有 consumedTrigger 标记,说明此前已触发过或用户选择忽略,则不再触发建议。
|
|
77
118
|
*/
|
|
78
119
|
allow: ({ state, range }) => {
|
|
79
|
-
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
return true;
|
|
120
|
+
return !checkConsumed(state, range);
|
|
83
121
|
},
|
|
84
122
|
render() {
|
|
85
123
|
let activeRange = null;
|
|
@@ -107,111 +145,49 @@ function useExtensions({ props, emit }) {
|
|
|
107
145
|
activeEditor = editor || activeEditor;
|
|
108
146
|
emit("hashTag-input", query != null ? query : "");
|
|
109
147
|
},
|
|
110
|
-
/**
|
|
111
|
-
* onKeyDown
|
|
112
|
-
* - 功能:在建议态期间侦测用户键盘行为并记录“非连续输入”的退出原因。
|
|
113
|
-
* - 参数说明:
|
|
114
|
-
* - event: KeyboardEvent 当前键盘事件对象。
|
|
115
|
-
* - 返回值:boolean 始终返回 false,交由默认逻辑处理(从而触发 onExit)。
|
|
116
|
-
* - 异常:无。
|
|
117
|
-
*/
|
|
118
148
|
onKeyDown({ event }) {
|
|
119
|
-
const isSpaceKey =
|
|
120
|
-
event.keyCode === 32 ||
|
|
121
|
-
event.code === "Space";
|
|
149
|
+
const isSpaceKey = [" ", "Space", "Spacebar"].includes(event.key) || // @ts-ignore
|
|
150
|
+
event.keyCode === 32 || event.code === "Space";
|
|
122
151
|
if (isSpaceKey) {
|
|
123
152
|
exitCause = "space";
|
|
124
|
-
return false;
|
|
125
153
|
}
|
|
126
154
|
return false;
|
|
127
155
|
},
|
|
128
|
-
|
|
129
|
-
* onExit
|
|
130
|
-
* - 功能:建议态结束时向外部发送退出事件,并尽可能准确地判断退出原因。
|
|
131
|
-
* - 设计考量:
|
|
132
|
-
* - 安卓设备上软键盘可能不触发标准 keydown 空格;此时通过检查当前选择位置前一个字符是否为空格来回溯判断。
|
|
133
|
-
* - 参数:无。
|
|
134
|
-
* - 返回值:无(通过 emit 派发事件)。
|
|
135
|
-
* - 异常:内部读取选择/文档时若发生异常,捕获后保持原有退出原因,不影响外部流程。
|
|
136
|
-
*/
|
|
137
|
-
onExit({ range }) {
|
|
156
|
+
onExit({ editor, range }) {
|
|
138
157
|
var _a, _b, _c, _d, _e;
|
|
139
158
|
try {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (prevChar === " ") {
|
|
146
|
-
exitCause = "space";
|
|
147
|
-
}
|
|
159
|
+
const sel = (_a = editor.state) == null ? void 0 : _a.selection;
|
|
160
|
+
const from = (_b = sel == null ? void 0 : sel.from) != null ? _b : 0;
|
|
161
|
+
const prevChar = (_e = (_d = (_c = editor.state) == null ? void 0 : _c.doc) == null ? void 0 : _d.textBetween(Math.max(from - 1, 0), from, "\0", "\0")) != null ? _e : "";
|
|
162
|
+
if (prevChar === " ") {
|
|
163
|
+
exitCause = "space";
|
|
148
164
|
}
|
|
149
165
|
} catch (e) {
|
|
150
166
|
}
|
|
151
|
-
|
|
152
|
-
spentHashTagPositions.push(range.from);
|
|
153
|
-
}
|
|
167
|
+
markAsConsumed(editor, range, "#");
|
|
154
168
|
activeRange = null;
|
|
155
169
|
activeEditor = null;
|
|
156
|
-
|
|
170
|
+
emit("hashTag-exit", {
|
|
157
171
|
reason: exitCause,
|
|
158
172
|
nonContiguous: exitCause === "space"
|
|
159
|
-
};
|
|
160
|
-
emit("hashTag-exit", payload);
|
|
173
|
+
});
|
|
161
174
|
}
|
|
162
175
|
};
|
|
163
176
|
}
|
|
164
177
|
}
|
|
165
178
|
});
|
|
166
|
-
const HashTagTracker = import_core.Extension.create({
|
|
167
|
-
name: "hashTagTracker",
|
|
168
|
-
onTransaction({ transaction }) {
|
|
169
|
-
if (spentHashTagPositions.length === 0)
|
|
170
|
-
return;
|
|
171
|
-
spentHashTagPositions = spentHashTagPositions.map((pos) => transaction.mapping.map(pos, -1));
|
|
172
|
-
spentHashTagPositions = spentHashTagPositions.filter((pos) => {
|
|
173
|
-
try {
|
|
174
|
-
if (pos < 0 || pos >= transaction.doc.content.size)
|
|
175
|
-
return false;
|
|
176
|
-
const char = transaction.doc.textBetween(pos, pos + 1);
|
|
177
|
-
return char === "#";
|
|
178
|
-
} catch (e) {
|
|
179
|
-
return false;
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
spentHashTagPositions = Array.from(new Set(spentHashTagPositions));
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
const MentionTracker = import_core.Extension.create({
|
|
186
|
-
name: "mentionTracker",
|
|
187
|
-
onTransaction({ transaction }) {
|
|
188
|
-
if (spentMentionPositions.length === 0)
|
|
189
|
-
return;
|
|
190
|
-
spentMentionPositions = spentMentionPositions.map((pos) => transaction.mapping.map(pos, -1));
|
|
191
|
-
spentMentionPositions = spentMentionPositions.filter((pos) => {
|
|
192
|
-
try {
|
|
193
|
-
if (pos < 0 || pos >= transaction.doc.content.size)
|
|
194
|
-
return false;
|
|
195
|
-
const char = transaction.doc.textBetween(pos, pos + 1);
|
|
196
|
-
return char === "@";
|
|
197
|
-
} catch (e) {
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
spentMentionPositions = Array.from(new Set(spentMentionPositions));
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
179
|
const extensions = [
|
|
205
180
|
import_starter_kit.default,
|
|
206
181
|
import_HtmlBlock.default,
|
|
207
|
-
|
|
182
|
+
ConsumedTrigger,
|
|
183
|
+
// Video input rule extension
|
|
208
184
|
import_core.Extension.create({
|
|
209
185
|
name: "videoInputRule",
|
|
210
186
|
addInputRules() {
|
|
211
187
|
return [
|
|
212
188
|
new import_core.InputRule({
|
|
213
189
|
find: /!video\(([^)]+)\)\s/,
|
|
214
|
-
handler: ({
|
|
190
|
+
handler: ({ range, match, commands }) => {
|
|
215
191
|
const [, url] = match;
|
|
216
192
|
if (url) {
|
|
217
193
|
const html = `<video src="${url}" autoplay="" loop="" muted="" controls="" playsinline=""></video>`;
|
|
@@ -232,7 +208,7 @@ function useExtensions({ props, emit }) {
|
|
|
232
208
|
return [
|
|
233
209
|
new import_core.InputRule({
|
|
234
210
|
find: /!audio\(([^)]+)\)\s/,
|
|
235
|
-
handler: ({
|
|
211
|
+
handler: ({ range, match, commands }) => {
|
|
236
212
|
const [, url] = match;
|
|
237
213
|
if (url) {
|
|
238
214
|
const html = `<audio src="${url}" controls="" playsinline=""></audio>`;
|
|
@@ -247,14 +223,8 @@ function useExtensions({ props, emit }) {
|
|
|
247
223
|
];
|
|
248
224
|
}
|
|
249
225
|
}),
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
// 如果你还想支持其他 style 属性,可以在这里配置
|
|
253
|
-
// HTMLAttributes: { class: 'my-custom-class' }
|
|
254
|
-
}),
|
|
255
|
-
import_extension_color.Color.configure({
|
|
256
|
-
// types: ['textStyle'] 默认就是这个,一般不需要改
|
|
257
|
-
}),
|
|
226
|
+
import_extension_text_style.TextStyle,
|
|
227
|
+
import_extension_color.Color,
|
|
258
228
|
import_tiptap_markdown.Markdown.configure({
|
|
259
229
|
html: true,
|
|
260
230
|
tightLists: true,
|
|
@@ -265,27 +235,19 @@ function useExtensions({ props, emit }) {
|
|
|
265
235
|
transformPastedText: true,
|
|
266
236
|
transformCopiedText: true
|
|
267
237
|
}),
|
|
268
|
-
// 自定义扩展来支持 Markdown 链接语法
|
|
269
238
|
import_core.Extension.create({
|
|
270
239
|
name: "markdownLinkInputRule",
|
|
271
240
|
addInputRules() {
|
|
272
241
|
return [
|
|
273
242
|
new import_core.InputRule({
|
|
274
243
|
find: /\[([^\]]+)\]\(([^\)]+)\)\s/,
|
|
275
|
-
handler: ({
|
|
244
|
+
handler: ({ range, match, commands }) => {
|
|
276
245
|
const [, text, href] = match;
|
|
277
|
-
|
|
278
|
-
const end = range.to;
|
|
279
|
-
commands.deleteRange({ from: start, to: end });
|
|
246
|
+
commands.deleteRange({ from: range.from, to: range.to });
|
|
280
247
|
commands.insertContent({
|
|
281
248
|
type: "text",
|
|
282
249
|
text,
|
|
283
|
-
marks: [
|
|
284
|
-
{
|
|
285
|
-
type: "link",
|
|
286
|
-
attrs: { href }
|
|
287
|
-
}
|
|
288
|
-
]
|
|
250
|
+
marks: [{ type: "link", attrs: { href } }]
|
|
289
251
|
});
|
|
290
252
|
}
|
|
291
253
|
})
|
|
@@ -297,9 +259,7 @@ function useExtensions({ props, emit }) {
|
|
|
297
259
|
table: { resizable: true }
|
|
298
260
|
}),
|
|
299
261
|
import_extensions.Placeholder.configure({
|
|
300
|
-
placeholder: () =>
|
|
301
|
-
return props.placeholder;
|
|
302
|
-
}
|
|
262
|
+
placeholder: () => props.placeholder
|
|
303
263
|
}),
|
|
304
264
|
import_extension_list.ListKit.extend({
|
|
305
265
|
addKeyboardShortcuts() {
|
|
@@ -307,18 +267,11 @@ function useExtensions({ props, emit }) {
|
|
|
307
267
|
Tab: ({ editor }) => {
|
|
308
268
|
const { $from } = editor.state.selection;
|
|
309
269
|
const currentItem = $from.node(-1);
|
|
310
|
-
|
|
311
|
-
return true;
|
|
312
|
-
}
|
|
313
|
-
return false;
|
|
270
|
+
return currentItem.type.name === "listItem";
|
|
314
271
|
},
|
|
315
272
|
"Shift-Tab": ({ editor }) => {
|
|
316
273
|
const { $from } = editor.state.selection;
|
|
317
|
-
|
|
318
|
-
if (currentItem.type.name === "doc") {
|
|
319
|
-
return true;
|
|
320
|
-
}
|
|
321
|
-
return false;
|
|
274
|
+
return $from.node(-1).type.name === "doc";
|
|
322
275
|
}
|
|
323
276
|
};
|
|
324
277
|
}
|
|
@@ -332,32 +285,23 @@ function useExtensions({ props, emit }) {
|
|
|
332
285
|
char: "@",
|
|
333
286
|
allowedPrefixes: null,
|
|
334
287
|
allowSpaces: false,
|
|
335
|
-
/**
|
|
336
|
-
* allow
|
|
337
|
-
* - 功能:判断是否允许触发建议态。
|
|
338
|
-
* - 1. 当@ 前面一个字符是数字/英文时,不触发,防止用户是想要输入邮箱。
|
|
339
|
-
* - 2. 触发以后如果包含数字,则收起(通过返回 false 销毁建议态)。
|
|
340
|
-
*/
|
|
341
288
|
allow: ({ state, range }) => {
|
|
342
|
-
if (
|
|
289
|
+
if (checkConsumed(state, range))
|
|
343
290
|
return false;
|
|
344
|
-
}
|
|
345
291
|
const doc = state.doc;
|
|
346
292
|
const $from = doc.resolve(range.from);
|
|
347
293
|
const isAtStart = range.from === $from.start();
|
|
348
294
|
const prevChar = isAtStart ? "" : doc.textBetween(range.from - 1, range.from);
|
|
349
|
-
if (/[a-zA-Z0-9]/.test(prevChar))
|
|
295
|
+
if (/[a-zA-Z0-9]/.test(prevChar))
|
|
350
296
|
return false;
|
|
351
|
-
}
|
|
352
297
|
const query = doc.textBetween(range.from + 1, range.to);
|
|
353
|
-
if (/[0-9]/.test(query))
|
|
298
|
+
if (/[0-9]/.test(query))
|
|
354
299
|
return false;
|
|
355
|
-
}
|
|
356
300
|
return true;
|
|
357
301
|
},
|
|
358
302
|
render() {
|
|
359
303
|
return {
|
|
360
|
-
onStart({ editor,
|
|
304
|
+
onStart({ editor, command, query }) {
|
|
361
305
|
activeEditor = editor;
|
|
362
306
|
emit("mention-triggered", (data) => {
|
|
363
307
|
command({
|
|
@@ -371,22 +315,14 @@ function useExtensions({ props, emit }) {
|
|
|
371
315
|
activeEditor = editor;
|
|
372
316
|
emit("mention-input", query != null ? query : "");
|
|
373
317
|
},
|
|
374
|
-
onExit({ range }) {
|
|
375
|
-
|
|
376
|
-
spentMentionPositions.push(range.from);
|
|
377
|
-
}
|
|
318
|
+
onExit({ editor, range }) {
|
|
319
|
+
markAsConsumed(editor, range, "@");
|
|
378
320
|
emit("mention-exit");
|
|
379
321
|
}
|
|
380
322
|
};
|
|
381
323
|
}
|
|
382
324
|
}
|
|
383
325
|
}),
|
|
384
|
-
/**
|
|
385
|
-
* 辅助扩展:追踪记录已失活的触发点
|
|
386
|
-
*/
|
|
387
|
-
MentionTracker,
|
|
388
|
-
HashTagTracker,
|
|
389
|
-
// HashTag 扩展:支持 # 触发、实时输入同步、完成后插入为 “#+文字”
|
|
390
326
|
HashTag
|
|
391
327
|
];
|
|
392
328
|
return extensions;
|