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