@cgboiler/biz-basic 1.0.47 → 1.0.49

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.
@@ -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,139 @@ 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;
179
+ const CustomContent = import_extension_mention.default.extend({
180
+ name: "customContent",
181
+ addAttributes() {
182
+ return {
183
+ id: {
184
+ default: null,
185
+ parseHTML: (element) => element.getAttribute("data-id"),
186
+ renderHTML: (attributes) => {
187
+ if (!attributes.id) {
188
+ return {};
189
+ }
190
+ return {
191
+ "data-id": attributes.id
192
+ };
193
+ }
194
+ },
195
+ label: {
196
+ default: null,
197
+ parseHTML: (element) => element.getAttribute("file-label"),
198
+ renderHTML: (attributes) => {
199
+ if (!attributes.label) {
200
+ return {};
201
+ }
202
+ return {
203
+ "file-label": attributes.label
204
+ };
205
+ }
206
+ },
207
+ obj: {
208
+ default: null,
209
+ parseHTML: (element) => {
210
+ const objStr = element.getAttribute("obj");
211
+ return objStr ? JSON.parse(objStr) : null;
212
+ },
213
+ renderHTML: (attributes) => {
214
+ if (!attributes.obj) {
215
+ return {};
216
+ }
217
+ return {
218
+ obj: JSON.stringify(attributes.obj)
219
+ };
220
+ }
221
+ },
222
+ color: {
223
+ default: null,
224
+ parseHTML: (element) => element.getAttribute("data-color"),
225
+ renderHTML: (attributes) => {
226
+ if (!attributes.color)
227
+ return {};
228
+ return { "data-color": attributes.color };
229
+ }
230
+ },
231
+ hoverColor: {
232
+ default: null,
233
+ parseHTML: (element) => element.getAttribute("data-hover-color"),
234
+ renderHTML: (attributes) => {
235
+ if (!attributes.hoverColor)
236
+ return {};
237
+ return { "data-hover-color": attributes.hoverColor };
238
+ }
199
239
  }
200
- });
201
- spentMentionPositions = Array.from(new Set(spentMentionPositions));
240
+ };
241
+ },
242
+ renderHTML({ node }) {
243
+ var _a, _b, _c, _d, _e, _f;
244
+ const obj = (_b = (_a = node == null ? void 0 : node.attrs) == null ? void 0 : _a.obj) != null ? _b : {};
245
+ const label = (_d = (_c = node == null ? void 0 : node.attrs) == null ? void 0 : _c.label) != null ? _d : "";
246
+ const color = (_e = node == null ? void 0 : node.attrs) == null ? void 0 : _e.color;
247
+ const hoverColor = (_f = node == null ? void 0 : node.attrs) == null ? void 0 : _f.hoverColor;
248
+ const styleParts = [];
249
+ if (color)
250
+ styleParts.push(`--custom-content-color: ${color}`);
251
+ if (hoverColor)
252
+ styleParts.push(`--custom-content-hover-color: ${hoverColor}`);
253
+ return [
254
+ "span",
255
+ {
256
+ class: "custom-content",
257
+ "data-type": "custom-content",
258
+ obj: JSON.stringify(obj),
259
+ "file-label": label,
260
+ "data-color": color,
261
+ "data-hover-color": hoverColor,
262
+ style: styleParts.length ? styleParts.join(";") : void 0,
263
+ contenteditable: "false"
264
+ },
265
+ label
266
+ ];
202
267
  }
203
268
  });
204
269
  const extensions = [
205
270
  import_starter_kit.default,
206
271
  import_HtmlBlock.default,
207
- // Video input rule extension for !video(url) syntax
272
+ ConsumedTrigger,
273
+ // Video input rule extension
208
274
  import_core.Extension.create({
209
275
  name: "videoInputRule",
210
276
  addInputRules() {
211
277
  return [
212
278
  new import_core.InputRule({
213
279
  find: /!video\(([^)]+)\)\s/,
214
- handler: ({ state, range, match, commands }) => {
280
+ handler: ({ range, match, commands }) => {
215
281
  const [, url] = match;
216
282
  if (url) {
217
283
  const html = `<video src="${url}" autoplay="" loop="" muted="" controls="" playsinline=""></video>`;
@@ -232,7 +298,7 @@ function useExtensions({ props, emit }) {
232
298
  return [
233
299
  new import_core.InputRule({
234
300
  find: /!audio\(([^)]+)\)\s/,
235
- handler: ({ state, range, match, commands }) => {
301
+ handler: ({ range, match, commands }) => {
236
302
  const [, url] = match;
237
303
  if (url) {
238
304
  const html = `<audio src="${url}" controls="" playsinline=""></audio>`;
@@ -247,14 +313,8 @@ function useExtensions({ props, emit }) {
247
313
  ];
248
314
  }
249
315
  }),
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
- }),
316
+ import_extension_text_style.TextStyle,
317
+ import_extension_color.Color,
258
318
  import_tiptap_markdown.Markdown.configure({
259
319
  html: true,
260
320
  tightLists: true,
@@ -265,27 +325,19 @@ function useExtensions({ props, emit }) {
265
325
  transformPastedText: true,
266
326
  transformCopiedText: true
267
327
  }),
268
- // 自定义扩展来支持 Markdown 链接语法
269
328
  import_core.Extension.create({
270
329
  name: "markdownLinkInputRule",
271
330
  addInputRules() {
272
331
  return [
273
332
  new import_core.InputRule({
274
333
  find: /\[([^\]]+)\]\(([^\)]+)\)\s/,
275
- handler: ({ state, range, match, commands }) => {
334
+ handler: ({ range, match, commands }) => {
276
335
  const [, text, href] = match;
277
- const start = range.from;
278
- const end = range.to;
279
- commands.deleteRange({ from: start, to: end });
336
+ commands.deleteRange({ from: range.from, to: range.to });
280
337
  commands.insertContent({
281
338
  type: "text",
282
339
  text,
283
- marks: [
284
- {
285
- type: "link",
286
- attrs: { href }
287
- }
288
- ]
340
+ marks: [{ type: "link", attrs: { href } }]
289
341
  });
290
342
  }
291
343
  })
@@ -297,9 +349,7 @@ function useExtensions({ props, emit }) {
297
349
  table: { resizable: true }
298
350
  }),
299
351
  import_extensions.Placeholder.configure({
300
- placeholder: () => {
301
- return props.placeholder;
302
- }
352
+ placeholder: () => props.placeholder
303
353
  }),
304
354
  import_extension_list.ListKit.extend({
305
355
  addKeyboardShortcuts() {
@@ -307,18 +357,11 @@ function useExtensions({ props, emit }) {
307
357
  Tab: ({ editor }) => {
308
358
  const { $from } = editor.state.selection;
309
359
  const currentItem = $from.node(-1);
310
- if (currentItem.type.name === "listItem") {
311
- return true;
312
- }
313
- return false;
360
+ return currentItem.type.name === "listItem";
314
361
  },
315
362
  "Shift-Tab": ({ editor }) => {
316
363
  const { $from } = editor.state.selection;
317
- const currentItem = $from.node(-1);
318
- if (currentItem.type.name === "doc") {
319
- return true;
320
- }
321
- return false;
364
+ return $from.node(-1).type.name === "doc";
322
365
  }
323
366
  };
324
367
  }
@@ -332,32 +375,23 @@ function useExtensions({ props, emit }) {
332
375
  char: "@",
333
376
  allowedPrefixes: null,
334
377
  allowSpaces: false,
335
- /**
336
- * allow
337
- * - 功能:判断是否允许触发建议态。
338
- * - 1. 当@ 前面一个字符是数字/英文时,不触发,防止用户是想要输入邮箱。
339
- * - 2. 触发以后如果包含数字,则收起(通过返回 false 销毁建议态)。
340
- */
341
378
  allow: ({ state, range }) => {
342
- if (spentMentionPositions.includes(range.from)) {
379
+ if (checkConsumed(state, range))
343
380
  return false;
344
- }
345
381
  const doc = state.doc;
346
382
  const $from = doc.resolve(range.from);
347
383
  const isAtStart = range.from === $from.start();
348
384
  const prevChar = isAtStart ? "" : doc.textBetween(range.from - 1, range.from);
349
- if (/[a-zA-Z0-9]/.test(prevChar)) {
385
+ if (/[a-zA-Z0-9]/.test(prevChar))
350
386
  return false;
351
- }
352
387
  const query = doc.textBetween(range.from + 1, range.to);
353
- if (/[0-9]/.test(query)) {
388
+ if (/[0-9]/.test(query))
354
389
  return false;
355
- }
356
390
  return true;
357
391
  },
358
392
  render() {
359
393
  return {
360
- onStart({ editor, range, command, query }) {
394
+ onStart({ editor, command, query }) {
361
395
  activeEditor = editor;
362
396
  emit("mention-triggered", (data) => {
363
397
  command({
@@ -371,23 +405,16 @@ function useExtensions({ props, emit }) {
371
405
  activeEditor = editor;
372
406
  emit("mention-input", query != null ? query : "");
373
407
  },
374
- onExit({ range }) {
375
- if (range.from !== void 0) {
376
- spentMentionPositions.push(range.from);
377
- }
408
+ onExit({ editor, range }) {
409
+ markAsConsumed(editor, range, "@");
378
410
  emit("mention-exit");
379
411
  }
380
412
  };
381
413
  }
382
414
  }
383
415
  }),
384
- /**
385
- * 辅助扩展:追踪记录已失活的触发点
386
- */
387
- MentionTracker,
388
- HashTagTracker,
389
- // HashTag 扩展:支持 # 触发、实时输入同步、完成后插入为 “#+文字”
390
- HashTag
416
+ HashTag,
417
+ CustomContent
391
418
  ];
392
419
  return extensions;
393
420
  }
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.49",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.js",