@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.
@@ -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,139 @@ 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;
147
+ const CustomContent = Mention.extend({
148
+ name: "customContent",
149
+ addAttributes() {
150
+ return {
151
+ id: {
152
+ default: null,
153
+ parseHTML: (element) => element.getAttribute("data-id"),
154
+ renderHTML: (attributes) => {
155
+ if (!attributes.id) {
156
+ return {};
157
+ }
158
+ return {
159
+ "data-id": attributes.id
160
+ };
161
+ }
162
+ },
163
+ label: {
164
+ default: null,
165
+ parseHTML: (element) => element.getAttribute("file-label"),
166
+ renderHTML: (attributes) => {
167
+ if (!attributes.label) {
168
+ return {};
169
+ }
170
+ return {
171
+ "file-label": attributes.label
172
+ };
173
+ }
174
+ },
175
+ obj: {
176
+ default: null,
177
+ parseHTML: (element) => {
178
+ const objStr = element.getAttribute("obj");
179
+ return objStr ? JSON.parse(objStr) : null;
180
+ },
181
+ renderHTML: (attributes) => {
182
+ if (!attributes.obj) {
183
+ return {};
184
+ }
185
+ return {
186
+ obj: JSON.stringify(attributes.obj)
187
+ };
188
+ }
189
+ },
190
+ color: {
191
+ default: null,
192
+ parseHTML: (element) => element.getAttribute("data-color"),
193
+ renderHTML: (attributes) => {
194
+ if (!attributes.color)
195
+ return {};
196
+ return { "data-color": attributes.color };
197
+ }
198
+ },
199
+ hoverColor: {
200
+ default: null,
201
+ parseHTML: (element) => element.getAttribute("data-hover-color"),
202
+ renderHTML: (attributes) => {
203
+ if (!attributes.hoverColor)
204
+ return {};
205
+ return { "data-hover-color": attributes.hoverColor };
206
+ }
167
207
  }
168
- });
169
- spentMentionPositions = Array.from(new Set(spentMentionPositions));
208
+ };
209
+ },
210
+ renderHTML({ node }) {
211
+ var _a, _b, _c, _d, _e, _f;
212
+ const obj = (_b = (_a = node == null ? void 0 : node.attrs) == null ? void 0 : _a.obj) != null ? _b : {};
213
+ const label = (_d = (_c = node == null ? void 0 : node.attrs) == null ? void 0 : _c.label) != null ? _d : "";
214
+ const color = (_e = node == null ? void 0 : node.attrs) == null ? void 0 : _e.color;
215
+ const hoverColor = (_f = node == null ? void 0 : node.attrs) == null ? void 0 : _f.hoverColor;
216
+ const styleParts = [];
217
+ if (color)
218
+ styleParts.push(`--custom-content-color: ${color}`);
219
+ if (hoverColor)
220
+ styleParts.push(`--custom-content-hover-color: ${hoverColor}`);
221
+ return [
222
+ "span",
223
+ {
224
+ class: "custom-content",
225
+ "data-type": "custom-content",
226
+ obj: JSON.stringify(obj),
227
+ "file-label": label,
228
+ "data-color": color,
229
+ "data-hover-color": hoverColor,
230
+ style: styleParts.length ? styleParts.join(";") : void 0,
231
+ contenteditable: "false"
232
+ },
233
+ label
234
+ ];
170
235
  }
171
236
  });
172
237
  const extensions = [
173
238
  StarterKit,
174
239
  HtmlBlock,
175
- // Video input rule extension for !video(url) syntax
240
+ ConsumedTrigger,
241
+ // Video input rule extension
176
242
  Extension.create({
177
243
  name: "videoInputRule",
178
244
  addInputRules() {
179
245
  return [
180
246
  new InputRule({
181
247
  find: /!video\(([^)]+)\)\s/,
182
- handler: ({ state, range, match, commands }) => {
248
+ handler: ({ range, match, commands }) => {
183
249
  const [, url] = match;
184
250
  if (url) {
185
251
  const html = `<video src="${url}" autoplay="" loop="" muted="" controls="" playsinline=""></video>`;
@@ -200,7 +266,7 @@ function useExtensions({ props, emit }) {
200
266
  return [
201
267
  new InputRule({
202
268
  find: /!audio\(([^)]+)\)\s/,
203
- handler: ({ state, range, match, commands }) => {
269
+ handler: ({ range, match, commands }) => {
204
270
  const [, url] = match;
205
271
  if (url) {
206
272
  const html = `<audio src="${url}" controls="" playsinline=""></audio>`;
@@ -215,14 +281,8 @@ function useExtensions({ props, emit }) {
215
281
  ];
216
282
  }
217
283
  }),
218
- // TextStyle 是 Color 的依赖,必须添加
219
- TextStyle.configure({
220
- // 如果你还想支持其他 style 属性,可以在这里配置
221
- // HTMLAttributes: { class: 'my-custom-class' }
222
- }),
223
- Color.configure({
224
- // types: ['textStyle'] 默认就是这个,一般不需要改
225
- }),
284
+ TextStyle,
285
+ Color,
226
286
  Markdown.configure({
227
287
  html: true,
228
288
  tightLists: true,
@@ -233,27 +293,19 @@ function useExtensions({ props, emit }) {
233
293
  transformPastedText: true,
234
294
  transformCopiedText: true
235
295
  }),
236
- // 自定义扩展来支持 Markdown 链接语法
237
296
  Extension.create({
238
297
  name: "markdownLinkInputRule",
239
298
  addInputRules() {
240
299
  return [
241
300
  new InputRule({
242
301
  find: /\[([^\]]+)\]\(([^\)]+)\)\s/,
243
- handler: ({ state, range, match, commands }) => {
302
+ handler: ({ range, match, commands }) => {
244
303
  const [, text, href] = match;
245
- const start = range.from;
246
- const end = range.to;
247
- commands.deleteRange({ from: start, to: end });
304
+ commands.deleteRange({ from: range.from, to: range.to });
248
305
  commands.insertContent({
249
306
  type: "text",
250
307
  text,
251
- marks: [
252
- {
253
- type: "link",
254
- attrs: { href }
255
- }
256
- ]
308
+ marks: [{ type: "link", attrs: { href } }]
257
309
  });
258
310
  }
259
311
  })
@@ -265,9 +317,7 @@ function useExtensions({ props, emit }) {
265
317
  table: { resizable: true }
266
318
  }),
267
319
  Placeholder.configure({
268
- placeholder: () => {
269
- return props.placeholder;
270
- }
320
+ placeholder: () => props.placeholder
271
321
  }),
272
322
  ListKit.extend({
273
323
  addKeyboardShortcuts() {
@@ -275,18 +325,11 @@ function useExtensions({ props, emit }) {
275
325
  Tab: ({ editor }) => {
276
326
  const { $from } = editor.state.selection;
277
327
  const currentItem = $from.node(-1);
278
- if (currentItem.type.name === "listItem") {
279
- return true;
280
- }
281
- return false;
328
+ return currentItem.type.name === "listItem";
282
329
  },
283
330
  "Shift-Tab": ({ editor }) => {
284
331
  const { $from } = editor.state.selection;
285
- const currentItem = $from.node(-1);
286
- if (currentItem.type.name === "doc") {
287
- return true;
288
- }
289
- return false;
332
+ return $from.node(-1).type.name === "doc";
290
333
  }
291
334
  };
292
335
  }
@@ -300,32 +343,23 @@ function useExtensions({ props, emit }) {
300
343
  char: "@",
301
344
  allowedPrefixes: null,
302
345
  allowSpaces: false,
303
- /**
304
- * allow
305
- * - 功能:判断是否允许触发建议态。
306
- * - 1. 当@ 前面一个字符是数字/英文时,不触发,防止用户是想要输入邮箱。
307
- * - 2. 触发以后如果包含数字,则收起(通过返回 false 销毁建议态)。
308
- */
309
346
  allow: ({ state, range }) => {
310
- if (spentMentionPositions.includes(range.from)) {
347
+ if (checkConsumed(state, range))
311
348
  return false;
312
- }
313
349
  const doc = state.doc;
314
350
  const $from = doc.resolve(range.from);
315
351
  const isAtStart = range.from === $from.start();
316
352
  const prevChar = isAtStart ? "" : doc.textBetween(range.from - 1, range.from);
317
- if (/[a-zA-Z0-9]/.test(prevChar)) {
353
+ if (/[a-zA-Z0-9]/.test(prevChar))
318
354
  return false;
319
- }
320
355
  const query = doc.textBetween(range.from + 1, range.to);
321
- if (/[0-9]/.test(query)) {
356
+ if (/[0-9]/.test(query))
322
357
  return false;
323
- }
324
358
  return true;
325
359
  },
326
360
  render() {
327
361
  return {
328
- onStart({ editor, range, command, query }) {
362
+ onStart({ editor, command, query }) {
329
363
  activeEditor = editor;
330
364
  emit("mention-triggered", (data) => {
331
365
  command({
@@ -339,23 +373,16 @@ function useExtensions({ props, emit }) {
339
373
  activeEditor = editor;
340
374
  emit("mention-input", query != null ? query : "");
341
375
  },
342
- onExit({ range }) {
343
- if (range.from !== void 0) {
344
- spentMentionPositions.push(range.from);
345
- }
376
+ onExit({ editor, range }) {
377
+ markAsConsumed(editor, range, "@");
346
378
  emit("mention-exit");
347
379
  }
348
380
  };
349
381
  }
350
382
  }
351
383
  }),
352
- /**
353
- * 辅助扩展:追踪记录已失活的触发点
354
- */
355
- MentionTracker,
356
- HashTagTracker,
357
- // HashTag 扩展:支持 # 触发、实时输入同步、完成后插入为 “#+文字”
358
- HashTag
384
+ HashTag,
385
+ CustomContent
359
386
  ];
360
387
  return extensions;
361
388
  }
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.48";
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.48";
40
40
  function install(app) {
41
41
  const components = [
42
42
  import_rich_text_editor.default
@@ -16,7 +16,7 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
16
16
  type: BooleanConstructor;
17
17
  default: boolean;
18
18
  };
19
- }>, () => import("vue/jsx-runtime").JSX.Element, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("blur" | "focus" | "hashTag-triggered" | "hashTag-input" | "hashTag-exit" | "mention-triggered" | "mention-input" | "mention-exit" | "update:modelValue" | "mention-clicked" | "hashTag-clicked")[], "blur" | "focus" | "hashTag-triggered" | "hashTag-input" | "hashTag-exit" | "mention-triggered" | "mention-input" | "mention-exit" | "update:modelValue" | "mention-clicked" | "hashTag-clicked", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
19
+ }>, () => import("vue/jsx-runtime").JSX.Element, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, ("blur" | "focus" | "hashTag-triggered" | "hashTag-input" | "hashTag-exit" | "mention-triggered" | "mention-input" | "mention-exit" | "update:modelValue" | "mention-clicked" | "hashTag-clicked" | "customContent-triggered" | "customContent-input" | "customContent-exit" | "customContent-clicked")[], "blur" | "focus" | "hashTag-triggered" | "hashTag-input" | "hashTag-exit" | "mention-triggered" | "mention-input" | "mention-exit" | "update:modelValue" | "mention-clicked" | "hashTag-clicked" | "customContent-triggered" | "customContent-input" | "customContent-exit" | "customContent-clicked", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
20
20
  modelValue: {
21
21
  type: StringConstructor;
22
22
  default: string;
@@ -44,6 +44,10 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
44
44
  "onUpdate:modelValue"?: ((...args: any[]) => any) | undefined;
45
45
  "onMention-clicked"?: ((...args: any[]) => any) | undefined;
46
46
  "onHashTag-clicked"?: ((...args: any[]) => any) | undefined;
47
+ "onCustomContent-triggered"?: ((...args: any[]) => any) | undefined;
48
+ "onCustomContent-input"?: ((...args: any[]) => any) | undefined;
49
+ "onCustomContent-exit"?: ((...args: any[]) => any) | undefined;
50
+ "onCustomContent-clicked"?: ((...args: any[]) => any) | undefined;
47
51
  }>, {
48
52
  modelValue: string;
49
53
  placeholder: string;
@@ -63,7 +63,7 @@ var import_index = require("./index.css");
63
63
  var stdin_default = (0, import_vue2.defineComponent)({
64
64
  name: "RichTextEditor",
65
65
  props: import_types.richTextEditorProps,
66
- emits: ["update:modelValue", "mention-triggered", "mention-input", "mention-exit", "mention-clicked", "hashTag-triggered", "hashTag-input", "hashTag-exit", "hashTag-clicked", "blur", "focus"],
66
+ emits: ["update:modelValue", "mention-triggered", "mention-input", "mention-exit", "mention-clicked", "hashTag-triggered", "hashTag-input", "hashTag-exit", "hashTag-clicked", "customContent-triggered", "customContent-input", "customContent-exit", "customContent-clicked", "blur", "focus"],
67
67
  setup(props, {
68
68
  emit,
69
69
  expose
@@ -131,10 +131,23 @@ var stdin_default = (0, import_vue2.defineComponent)({
131
131
  }
132
132
  });
133
133
  };
134
+ const insertCustomContent = (data) => {
135
+ editor.value.commands.insertContent({
136
+ type: "customContent",
137
+ attrs: {
138
+ id: data.id || `custom-${Date.now()}`,
139
+ label: data.label,
140
+ obj: data.obj,
141
+ color: data.color,
142
+ hoverColor: data.hoverColor
143
+ }
144
+ });
145
+ };
134
146
  expose({
135
147
  getEditor: () => editor.value,
136
148
  insertMention,
137
- insertHashTag
149
+ insertHashTag,
150
+ insertCustomContent
138
151
  });
139
152
  (0, import_vue2.watch)(() => props.modelValue, (newValue) => {
140
153
  var _a, _b;
@@ -217,6 +230,26 @@ var stdin_default = (0, import_vue2.defineComponent)({
217
230
  return;
218
231
  }
219
232
  };
233
+ const customContentClickHandler = (event) => {
234
+ const target = event.target;
235
+ const tagEl = target.closest(".custom-content") || null;
236
+ if (tagEl) {
237
+ const objStr = tagEl.getAttribute("obj") || "{}";
238
+ const label = tagEl.getAttribute("file-label") || "";
239
+ try {
240
+ const obj = JSON.parse(objStr);
241
+ emit("customContent-clicked", {
242
+ obj,
243
+ label
244
+ });
245
+ } catch (e) {
246
+ console.error("Failed to parse custom content obj:", e);
247
+ }
248
+ event.stopPropagation();
249
+ event.preventDefault();
250
+ return;
251
+ }
252
+ };
220
253
  const HISTORY_METADATA_KEY = "rich_text_editor_history_metadata";
221
254
  const MAX_HISTORY_ITEMS = 10;
222
255
  let saveTimeoutId = null;
@@ -270,6 +303,7 @@ var stdin_default = (0, import_vue2.defineComponent)({
270
303
  editorElement.addEventListener("click", imageClickHandler);
271
304
  editorElement.addEventListener("click", hashTagClickHandler);
272
305
  editorElement.addEventListener("click", mentionClickHandler);
306
+ editorElement.addEventListener("click", customContentClickHandler);
273
307
  }
274
308
  });
275
309
  });
@@ -283,6 +317,7 @@ var stdin_default = (0, import_vue2.defineComponent)({
283
317
  editorElement.removeEventListener("click", imageClickHandler);
284
318
  editorElement.removeEventListener("click", hashTagClickHandler);
285
319
  editorElement.removeEventListener("click", mentionClickHandler);
320
+ editorElement.removeEventListener("click", customContentClickHandler);
286
321
  }
287
322
  (_c = editor.value) == null ? void 0 : _c.destroy();
288
323
  });
@@ -1 +1 @@
1
- body .ProseMirror{flex:1;overflow:auto;outline:none;line-height:1.6;font-size:var(--font-base);--white: #fff;--black: #2e2b29;--gray-1: rgba(61, 37, 20, .05);--gray-2: rgba(61, 37, 20, .08);--gray-3: rgba(61, 37, 20, .12);--gray-4: rgba(53, 38, 28, .3);--gray-5: rgba(28, 25, 23, .6);--green: #22c55e;--purple: #6a00f5;--purple-contrast: #5800cc;--purple-light: rgba(88, 5, 255, .05);--yellow-contrast: #facc15;--yellow: rgba(250, 204, 21, .4);--yellow-light: #fffae5;--red: #ff5c33;--red-light: #ffebe5;--shadow: 0px 12px 33px 0px rgba(0, 0, 0, .06), 0px 3.618px 9.949px 0px rgba(0, 0, 0, .04)}body .ProseMirror :first-child{margin-top:0}body .ProseMirror video{width:100%}body .ProseMirror ol{list-style:auto}body .ProseMirror ol ol,body .ProseMirror ul{list-style:disc}body .ProseMirror ol,body .ProseMirror ul{padding-left:1.5em;margin:0 0 12px}body .ProseMirror ol li p,body .ProseMirror ul li p{margin-top:.25em;margin-bottom:.25em}body .ProseMirror li::marker{text-align:start!important}body .ProseMirror h1,body .ProseMirror h2,body .ProseMirror h3,body .ProseMirror h4,body .ProseMirror h5,body .ProseMirror h6{line-height:1.1;margin-top:2.5rem;text-wrap:pretty}body .ProseMirror h1,body .ProseMirror h2{margin-top:1rem;margin-bottom:1rem}body .ProseMirror h1{font-size:1.4rem}body .ProseMirror h2{font-size:1.2rem}body .ProseMirror h3{font-size:1.1rem}body .ProseMirror h4,body .ProseMirror h5,body .ProseMirror h6{font-size:1rem}body .ProseMirror a,body .ProseMirror .editor-link{color:var(--purple);text-decoration:underline;cursor:pointer;transition:color .2s ease}body .ProseMirror a:hover,body .ProseMirror .editor-link:hover{color:var(--purple-contrast);text-decoration:underline}body .ProseMirror code{background-color:#ffe5e8;border-radius:.4rem;color:#c02537;padding:2px 4px}body .ProseMirror pre{border-radius:.5rem;font-family:JetBrainsMono,monospace;margin:1.5rem 0;padding:.75rem 1rem}body .ProseMirror pre code{background:none;color:inherit;font-size:.8rem;padding:0}body .ProseMirror blockquote{border-left:3px solid var(--gray-3);margin:1.5rem 0;padding-left:1rem}body .ProseMirror hr{border:none;border-top:1px solid var(--gray-2);margin:2rem 0}body .ProseMirror p.is-editor-empty:first-child:before{color:var(--gray-4);content:attr(data-placeholder);float:left;height:0;pointer-events:none}body .ProseMirror .is-empty:before{color:var(--gray-4);content:attr(data-placeholder);float:left;height:0;pointer-events:none}body .ProseMirror table{border-collapse:collapse;margin:0;overflow:hidden;table-layout:fixed;width:100%}body .ProseMirror table td,body .ProseMirror table th{border:1px solid var(--gray-3);box-sizing:border-box;min-width:1em;padding:6px 8px;position:relative;vertical-align:top}body .ProseMirror table td>*,body .ProseMirror table th>*{margin-bottom:0}body .ProseMirror table th{background-color:var(--gray-1);font-weight:700;text-align:left}body .ProseMirror table .selectedCell:after{background:var(--gray-2);content:"";left:0;right:0;top:0;bottom:0;pointer-events:none;position:absolute;z-index:2}body .ProseMirror table .column-resize-handle{background-color:var(--purple);bottom:-2px;pointer-events:none;position:absolute;right:-2px;top:0;width:4px}body .ProseMirror .tableWrapper{margin:1.5rem 0;overflow-x:auto}body .ProseMirror.resize-cursor{cursor:ew-resize;cursor:col-resize}body .ProseMirror img{max-width:100%}body .ProseMirror .mention,body .ProseMirror .hash-tag{color:#c02537;padding:0 .3em}.animation-indent--right{animation:indent-right .5s cubic-bezier(.68,-.55,.27,1.55) 1 alternate-reverse}@keyframes indent-right{0%{transform:translate(0)}to{transform:translate(12px)}}
1
+ body .ProseMirror{flex:1;overflow:auto;outline:none;line-height:1.6;font-size:var(--font-base);--white: #fff;--black: #2e2b29;--gray-1: rgba(61, 37, 20, .05);--gray-2: rgba(61, 37, 20, .08);--gray-3: rgba(61, 37, 20, .12);--gray-4: rgba(53, 38, 28, .3);--gray-5: rgba(28, 25, 23, .6);--green: #22c55e;--purple: #6a00f5;--purple-contrast: #5800cc;--purple-light: rgba(88, 5, 255, .05);--yellow-contrast: #facc15;--yellow: rgba(250, 204, 21, .4);--yellow-light: #fffae5;--red: #ff5c33;--red-light: #ffebe5;--shadow: 0px 12px 33px 0px rgba(0, 0, 0, .06), 0px 3.618px 9.949px 0px rgba(0, 0, 0, .04)}body .ProseMirror :first-child{margin-top:0}body .ProseMirror video{width:100%}body .ProseMirror ol{list-style:auto}body .ProseMirror ol ol,body .ProseMirror ul{list-style:disc}body .ProseMirror ol,body .ProseMirror ul{padding-left:1.5em;margin:0 0 12px}body .ProseMirror ol li p,body .ProseMirror ul li p{margin-top:.25em;margin-bottom:.25em}body .ProseMirror li::marker{text-align:start!important}body .ProseMirror h1,body .ProseMirror h2,body .ProseMirror h3,body .ProseMirror h4,body .ProseMirror h5,body .ProseMirror h6{line-height:1.1;margin-top:2.5rem;text-wrap:pretty}body .ProseMirror h1,body .ProseMirror h2{margin-top:1rem;margin-bottom:1rem}body .ProseMirror h1{font-size:1.4rem}body .ProseMirror h2{font-size:1.2rem}body .ProseMirror h3{font-size:1.1rem}body .ProseMirror h4,body .ProseMirror h5,body .ProseMirror h6{font-size:1rem}body .ProseMirror a,body .ProseMirror .editor-link{color:var(--purple);text-decoration:underline;cursor:pointer;transition:color .2s ease}body .ProseMirror a:hover,body .ProseMirror .editor-link:hover{color:var(--purple-contrast);text-decoration:underline}body .ProseMirror code{background-color:#ffe5e8;border-radius:.4rem;color:#c02537;padding:2px 4px}body .ProseMirror pre{border-radius:.5rem;font-family:JetBrainsMono,monospace;margin:1.5rem 0;padding:.75rem 1rem}body .ProseMirror pre code{background:none;color:inherit;font-size:.8rem;padding:0}body .ProseMirror blockquote{border-left:3px solid var(--gray-3);margin:1.5rem 0;padding-left:1rem}body .ProseMirror hr{border:none;border-top:1px solid var(--gray-2);margin:2rem 0}body .ProseMirror p.is-editor-empty:first-child:before{color:var(--gray-4);content:attr(data-placeholder);float:left;height:0;pointer-events:none}body .ProseMirror .is-empty:before{color:var(--gray-4);content:attr(data-placeholder);float:left;height:0;pointer-events:none}body .ProseMirror table{border-collapse:collapse;margin:0;overflow:hidden;table-layout:fixed;width:100%}body .ProseMirror table td,body .ProseMirror table th{border:1px solid var(--gray-3);box-sizing:border-box;min-width:1em;padding:6px 8px;position:relative;vertical-align:top}body .ProseMirror table td>*,body .ProseMirror table th>*{margin-bottom:0}body .ProseMirror table th{background-color:var(--gray-1);font-weight:700;text-align:left}body .ProseMirror table .selectedCell:after{background:var(--gray-2);content:"";left:0;right:0;top:0;bottom:0;pointer-events:none;position:absolute;z-index:2}body .ProseMirror table .column-resize-handle{background-color:var(--purple);bottom:-2px;pointer-events:none;position:absolute;right:-2px;top:0;width:4px}body .ProseMirror .tableWrapper{margin:1.5rem 0;overflow-x:auto}body .ProseMirror.resize-cursor{cursor:ew-resize;cursor:col-resize}body .ProseMirror img{max-width:100%}body .ProseMirror .mention,body .ProseMirror .hash-tag{color:#c02537;padding:0 .3em}body .ProseMirror .custom-content{color:var(--custom-content-color, #000);padding:0 .3em;cursor:pointer;transition:color .2s ease}body .ProseMirror .custom-content:hover{color:var(--custom-content-hover-color, #c02537)}.animation-indent--right{animation:indent-right .5s cubic-bezier(.68,-.55,.27,1.55) 1 alternate-reverse}@keyframes indent-right{0%{transform:translate(0)}to{transform:translate(12px)}}
@@ -218,11 +218,22 @@ body .ProseMirror {
218
218
  img {
219
219
  max-width: 100%;
220
220
  }
221
- // @人的样式 #话题的样式
221
+ // @人的样式 #话题的样式 自定义内容的样式
222
222
  .mention, .hash-tag {
223
223
  color: #c02537;
224
224
  padding: 0 0.3em;
225
225
  }
226
+
227
+ .custom-content {
228
+ color: var(--custom-content-color, #000);
229
+ padding: 0 0.3em;
230
+ cursor: pointer;
231
+ transition: color 0.2s ease;
232
+
233
+ &:hover {
234
+ color: var(--custom-content-hover-color, #c02537);
235
+ }
236
+ }
226
237
  }
227
238
 
228
239
  // animation
@@ -26,6 +26,21 @@ export interface HashTagData {
26
26
  hashTagId: string;
27
27
  name: string;
28
28
  }
29
+ /**
30
+ * CustomContentData
31
+ * - 功能:自定义内容的数据结构。
32
+ * - 字段说明:
33
+ * - label: string 显示标签(如 )。
34
+ * - obj: 任意JSON对象,内容由用户自定义。
35
+ * - id: 可选及其他扩展字段。
36
+ */
37
+ export interface CustomContentData {
38
+ label: string;
39
+ obj: Record<string, any>;
40
+ color?: string;
41
+ hoverColor?: string;
42
+ [key: string]: any;
43
+ }
29
44
  /**
30
45
  * HashTagExitEvent
31
46
  * - 功能:在 `hashTag-exit` 事件中传递退出原因与是否为非连续输入。