@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 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.46";
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.46";
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,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, HTMLAttributes }) {
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
- const attrs = {
23
- class: "hash-tag",
24
- "data-type": "hashTag",
25
- "data-id": id,
26
- "data-label": label,
27
- "data-hash-tag": "#",
28
- contenteditable: "false"
29
- };
30
- 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
+ ];
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
- * - 1. 若该位置的 # 已经触发过且未被删除,则不再重复触发(满足“单次触发制/光标静默”)。
85
+ * - 若该字符带有 consumedTrigger 标记,说明此前已触发过或用户选择忽略,则不再触发建议。
45
86
  */
46
87
  allow: ({ state, range }) => {
47
- if (spentHashTagPositions.includes(range.from)) {
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 = event.key === " " || event.key === "Space" || event.key === "Spacebar" || // @ts-ignore 兼容旧浏览器/环境的数值键码
88
- event.keyCode === 32 || // 兼容部分环境的 event.code 标识
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 editorRef = activeEditor;
109
- if (editorRef) {
110
- const sel = (_a = editorRef.state) == null ? void 0 : _a.selection;
111
- const from = (_b = sel == null ? void 0 : sel.from) != null ? _b : 0;
112
- 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 : "";
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
- if ((range == null ? void 0 : range.from) !== void 0) {
120
- spentHashTagPositions.push(range.from);
121
- }
135
+ markAsConsumed(editor, range, "#");
122
136
  activeRange = null;
123
137
  activeEditor = null;
124
- const payload = {
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
- // Video input rule extension for !video(url) syntax
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: ({ state, range, match, commands }) => {
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: ({ state, range, match, commands }) => {
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
- // TextStyle 是 Color 的依赖,必须添加
219
- TextStyle.configure({
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: ({ state, range, match, commands }) => {
212
+ handler: ({ range, match, commands }) => {
244
213
  const [, text, href] = match;
245
- const start = range.from;
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
- if (currentItem.type.name === "listItem") {
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
- const currentItem = $from.node(-1);
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 (spentMentionPositions.includes(range.from)) {
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, range, command, query }) {
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
- if (range.from !== void 0) {
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
@@ -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.46";
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.46";
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,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, HTMLAttributes }) {
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
- const attrs = {
55
- class: "hash-tag",
56
- "data-type": "hashTag",
57
- "data-id": id,
58
- "data-label": label,
59
- "data-hash-tag": "#",
60
- contenteditable: "false"
61
- };
62
- 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
+ ];
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
- * - 1. 若该位置的 # 已经触发过且未被删除,则不再重复触发(满足“单次触发制/光标静默”)。
117
+ * - 若该字符带有 consumedTrigger 标记,说明此前已触发过或用户选择忽略,则不再触发建议。
77
118
  */
78
119
  allow: ({ state, range }) => {
79
- if (spentHashTagPositions.includes(range.from)) {
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 = event.key === " " || event.key === "Space" || event.key === "Spacebar" || // @ts-ignore 兼容旧浏览器/环境的数值键码
120
- event.keyCode === 32 || // 兼容部分环境的 event.code 标识
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 editorRef = activeEditor;
141
- if (editorRef) {
142
- const sel = (_a = editorRef.state) == null ? void 0 : _a.selection;
143
- const from = (_b = sel == null ? void 0 : sel.from) != null ? _b : 0;
144
- 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 : "";
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
- if ((range == null ? void 0 : range.from) !== void 0) {
152
- spentHashTagPositions.push(range.from);
153
- }
167
+ markAsConsumed(editor, range, "#");
154
168
  activeRange = null;
155
169
  activeEditor = null;
156
- const payload = {
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
- // Video input rule extension for !video(url) syntax
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: ({ state, range, match, commands }) => {
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: ({ state, range, match, commands }) => {
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
- // TextStyle 是 Color 的依赖,必须添加
251
- import_extension_text_style.TextStyle.configure({
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: ({ state, range, match, commands }) => {
244
+ handler: ({ range, match, commands }) => {
276
245
  const [, text, href] = match;
277
- const start = range.from;
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
- if (currentItem.type.name === "listItem") {
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
- const currentItem = $from.node(-1);
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 (spentMentionPositions.includes(range.from)) {
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, range, command, query }) {
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
- if (range.from !== void 0) {
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cgboiler/biz-basic",
3
- "version": "1.0.47",
3
+ "version": "1.0.48",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.js",