@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 CHANGED
@@ -5,6 +5,6 @@ declare namespace _default {
5
5
  }
6
6
  export default _default;
7
7
  export function install(app: any): void;
8
- export const version: "1.0.45";
8
+ export const version: "1.0.47";
9
9
  import RichTextEditor from './rich-text-editor';
10
10
  export { RichTextEditor };
package/es/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import RichTextEditor from "./rich-text-editor";
2
- const version = "1.0.45";
2
+ const version = "1.0.47";
3
3
  function install(app) {
4
4
  const components = [
5
5
  RichTextEditor
@@ -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<any, any> | Extension<import("@tiptap/starter-kit").StarterKitOptions, any> | import("@tiptap/core").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
+ 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, HTMLAttributes }) {
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
- const attrs = {
20
- class: "hash-tag",
21
- "data-type": "hashTag",
22
- "data-id": id,
23
- "data-label": label,
24
- "data-hash-tag": "#",
25
- contenteditable: "false"
26
- };
27
- return ["span", attrs, `#${label}`];
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 = event.key === " " || event.key === "Space" || event.key === "Spacebar" || // @ts-ignore 兼容旧浏览器/环境的数值键码
73
- event.keyCode === 32 || // 兼容部分环境的 event.code 标识
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 editorRef = activeEditor;
94
- if (editorRef) {
95
- const sel = (_a = editorRef.state) == null ? void 0 : _a.selection;
96
- const from = (_b = sel == null ? void 0 : sel.from) != null ? _b : 0;
97
- const prevChar = (_e = (_d = (_c = editorRef.state) == null ? void 0 : _c.doc) == null ? void 0 : _d.textBetween(Math.max(from - 1, 0), from, "\0", "\0")) != null ? _e : "";
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
- const payload = {
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
- // Video input rule extension for !video(url) syntax
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: ({ state, range, match, commands }) => {
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: ({ state, range, match, commands }) => {
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
- // TextStyle 是 Color 的依赖,必须添加
184
- TextStyle.configure({
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: ({ state, range, match, commands }) => {
212
+ handler: ({ range, match, commands }) => {
209
213
  const [, text, href] = match;
210
- const start = range.from;
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
- if (currentItem.type.name === "listItem") {
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
- const currentItem = $from.node(-1);
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 (spentMentionPositions.includes(range.from)) {
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, range, command, query }) {
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
- if (range.from !== void 0) {
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
@@ -5,6 +5,6 @@ declare namespace _default {
5
5
  }
6
6
  export default _default;
7
7
  export function install(app: any): void;
8
- export const version: "1.0.45";
8
+ export const version: "1.0.47";
9
9
  import RichTextEditor from './rich-text-editor';
10
10
  export { RichTextEditor };
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.45";
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<any, any> | Extension<import("@tiptap/starter-kit").StarterKitOptions, any> | import("@tiptap/core").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
+ 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, HTMLAttributes }) {
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
- const attrs = {
52
- class: "hash-tag",
53
- "data-type": "hashTag",
54
- "data-id": id,
55
- "data-label": label,
56
- "data-hash-tag": "#",
57
- contenteditable: "false"
58
- };
59
- return ["span", attrs, `#${label}`];
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 = event.key === " " || event.key === "Space" || event.key === "Spacebar" || // @ts-ignore 兼容旧浏览器/环境的数值键码
105
- event.keyCode === 32 || // 兼容部分环境的 event.code 标识
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 editorRef = activeEditor;
126
- if (editorRef) {
127
- const sel = (_a = editorRef.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 = editorRef.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";
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
- const payload = {
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
- // Video input rule extension for !video(url) syntax
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: ({ state, range, match, commands }) => {
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: ({ state, range, match, commands }) => {
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
- // TextStyle 是 Color 的依赖,必须添加
216
- import_extension_text_style.TextStyle.configure({
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: ({ state, range, match, commands }) => {
244
+ handler: ({ range, match, commands }) => {
241
245
  const [, text, href] = match;
242
- const start = range.from;
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
- if (currentItem.type.name === "listItem") {
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
- const currentItem = $from.node(-1);
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 (spentMentionPositions.includes(range.from)) {
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, range, command, query }) {
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
- if (range.from !== void 0) {
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cgboiler/biz-basic",
3
- "version": "1.0.46",
3
+ "version": "1.0.48",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.js",