@thebuoyant-tsdev/mui-ts-library 3.18.0 → 3.20.0

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/README.de.md CHANGED
@@ -475,6 +475,20 @@ Dieses Projekt folgt [Semantic Versioning](https://semver.org/). In der Praxis b
475
475
 
476
476
  ## Changelog
477
477
 
478
+ ### [3.20.0] — 2026-07-09
479
+
480
+ **Hinzugefügt**
481
+ - `RichTextEditor`: neue Prop `onMentionInserted?: (item: MentionItem) => void` — feuert wenn der Nutzer ein Element aus dem `@`-Mention-Dropdown auswählt. Das vollständige `MentionItem` (`{ id, label }`) wird übergeben — kein HTML-Parsen nötig. Rein additiv. Siehe [Full Changelog](https://github.com/thebuoyant/mui-ts-library/blob/main/CHANGELOG.md) für Details.
482
+
483
+ ---
484
+
485
+ ### [3.19.0] — 2026-07-08
486
+
487
+ **Hinzugefügt**
488
+ - `RichTextEditor`: neue Prop `defaultValue?: string` für unkontrollierten Einsatz — setzt den initialen HTML-Inhalt einmalig beim Mount, kein externer State erforderlich. Analog zu MUI TextFields `defaultValue`. Wenn sowohl `value` als auch `defaultValue` angegeben werden, hat `value` Vorrang. Rein additiv. Siehe [Full Changelog](https://github.com/thebuoyant/mui-ts-library/blob/main/CHANGELOG.md) für Details.
489
+
490
+ ---
491
+
478
492
  ### [3.18.0] — 2026-07-08
479
493
 
480
494
  **Hinzugefügt**
package/README.md CHANGED
@@ -475,6 +475,20 @@ This project follows [Semantic Versioning](https://semver.org/). In practice:
475
475
 
476
476
  ## Changelog
477
477
 
478
+ ### [3.20.0] — 2026-07-09
479
+
480
+ **Added**
481
+ - `RichTextEditor`: new `onMentionInserted?: (item: MentionItem) => void` prop — fires when the user selects an item from the `@` mention dropdown. The full `MentionItem` (`{ id, label }`) is passed to the callback, so no HTML parsing is needed to track who was mentioned. Purely additive. See [Full Changelog](https://github.com/thebuoyant/mui-ts-library/blob/main/CHANGELOG.md) for details.
482
+
483
+ ---
484
+
485
+ ### [3.19.0] — 2026-07-08
486
+
487
+ **Added**
488
+ - `RichTextEditor`: new `defaultValue?: string` prop for uncontrolled usage — sets the initial HTML content once on mount, no external state required. Analogous to MUI TextField's `defaultValue`. When both `value` and `defaultValue` are provided, `value` takes precedence. Purely additive — no existing behaviour changes. See [Full Changelog](https://github.com/thebuoyant/mui-ts-library/blob/main/CHANGELOG.md) for details.
489
+
490
+ ---
491
+
478
492
  ### [3.18.0] — 2026-07-08
479
493
 
480
494
  **Added**
@@ -5,4 +5,4 @@ declare module "@tiptap/core" {
5
5
  }
6
6
  }
7
7
  import { type RichTextEditorProps } from "./RichTextEditor.types";
8
- export declare function RichTextEditor({ disabled, error, height, helperText, maxCharacters, mentionItems, mentionTriggerChar, name, onMentionSearch, placeholder, readonly, showCharacterCount, showToolbar, showWordCount, toolbarConfig, translation, value, width, onBlur, onChange, onFocus, onMarkdownChange, onSave, }: RichTextEditorProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function RichTextEditor({ defaultValue, disabled, error, height, helperText, maxCharacters, mentionItems, mentionTriggerChar, name, onMentionInserted, onMentionSearch, placeholder, readonly, showCharacterCount, showToolbar, showWordCount, toolbarConfig, translation, value, width, onBlur, onChange, onFocus, onMarkdownChange, onSave, }: RichTextEditorProps): import("react/jsx-runtime").JSX.Element;
@@ -1,40 +1,44 @@
1
1
  import { normalizeSize as e } from "../shared/normalizeSize.js";
2
2
  import { DEFAULT_RICH_TEXT_EDITOR_TOOLBAR_CONFIG as t, DEFAULT_RICH_TEXT_EDITOR_TRANSLATION as n } from "./RichTextEditor.types.js";
3
3
  import { buildMentionSuggestion as r } from "./RichTextEditorMentionSuggestion.js";
4
- import { RichTextEditorContent as i } from "./RichTextEditorContent.js";
5
- import { RichTextEditorToolbar as ee } from "./RichTextEditorToolbar.js";
4
+ import { RichTextEditorContent as ee } from "./RichTextEditorContent.js";
5
+ import { RichTextEditorToolbar as i } from "./RichTextEditorToolbar.js";
6
6
  import { RichTextEditorFooter as a } from "./RichTextEditorFooter.js";
7
7
  import { useEffect as o, useMemo as te, useRef as s, useState as c } from "react";
8
- import { Box as l, Divider as ne, Paper as re } from "@mui/material";
9
- import { Fragment as u, jsx as d, jsxs as f } from "react/jsx-runtime";
10
- import { useEditor as p } from "@tiptap/react";
11
- import { Mention as ie } from "@tiptap/extension-mention";
8
+ import { Box as l, Divider as u, Paper as ne } from "@mui/material";
9
+ import { Fragment as re, jsx as d, jsxs as f } from "react/jsx-runtime";
10
+ import { useEditor as ie } from "@tiptap/react";
11
+ import { Mention as p } from "@tiptap/extension-mention";
12
12
  import { StarterKit as ae } from "@tiptap/starter-kit";
13
13
  import { TextStyle as oe } from "@tiptap/extension-text-style";
14
14
  import { Color as se } from "@tiptap/extension-color";
15
15
  import { Highlight as ce } from "@tiptap/extension-highlight";
16
- import { Placeholder as m } from "@tiptap/extension-placeholder";
17
- import { CharacterCount as h } from "@tiptap/extension-character-count";
18
- import { TableKit as g } from "@tiptap/extension-table";
19
- import { Image as _ } from "@tiptap/extension-image";
20
- import { Markdown as v } from "tiptap-markdown";
16
+ import { Placeholder as le } from "@tiptap/extension-placeholder";
17
+ import { CharacterCount as m } from "@tiptap/extension-character-count";
18
+ import { TableKit as h } from "@tiptap/extension-table";
19
+ import { Image as g } from "@tiptap/extension-image";
20
+ import { Markdown as _ } from "tiptap-markdown";
21
21
  //#region src/components/rich-text-editor/RichTextEditor.tsx
22
- function y({ disabled: y = !1, error: b = !1, height: le, helperText: x, maxCharacters: S, mentionItems: C, mentionTriggerChar: w = "@", name: T, onMentionSearch: E, placeholder: D, readonly: O = !1, showCharacterCount: k = !1, showToolbar: A = !0, showWordCount: j = !1, toolbarConfig: M, translation: N, value: P, width: F, onBlur: I, onChange: L, onFocus: R, onMarkdownChange: z, onSave: B }) {
22
+ function v({ defaultValue: v, disabled: y = !1, error: b = !1, height: ue, helperText: x, maxCharacters: S, mentionItems: C, mentionTriggerChar: w = "@", name: T, onMentionInserted: E, onMentionSearch: D, placeholder: de, readonly: O = !1, showCharacterCount: k = !1, showToolbar: A = !0, showWordCount: j = !1, toolbarConfig: M, translation: N, value: P, width: F, onBlur: I, onChange: L, onFocus: R, onMarkdownChange: z, onSave: B }) {
23
23
  let V = {
24
24
  ...n,
25
25
  ...N
26
26
  }, H = {
27
27
  ...t,
28
28
  ...M
29
- }, [U, W] = c(!1), [G, ue] = c(!1), K = s(G), q = s(B), J = s(C ?? []);
29
+ }, [U, fe] = c(!1), [W, pe] = c(!1), G = s(W), K = s(B), q = s(C ?? []);
30
30
  o(() => {
31
- J.current = C ?? [];
32
- }, [C]), o(() => {
33
- K.current = G;
34
- }, [G]), o(() => {
35
- q.current = B;
31
+ q.current = C ?? [];
32
+ }, [C]);
33
+ let J = s(E);
34
+ o(() => {
35
+ J.current = E;
36
+ }, [E]), o(() => {
37
+ G.current = W;
38
+ }, [W]), o(() => {
39
+ K.current = B;
36
40
  }, [B]);
37
- let Y = e(le), de = e(F), X = Y === "auto", fe = X ? void 0 : Y ?? 200, Z = C !== void 0 || E !== void 0, Q = te(() => Z ? ie.configure({
41
+ let Y = e(ue), me = e(F), X = Y === "auto", he = X ? void 0 : Y ?? 200, Z = C !== void 0 || D !== void 0, Q = te(() => Z ? p.configure({
38
42
  HTMLAttributes: { class: "rte-mention" },
39
43
  renderHTML({ options: e, node: t }) {
40
44
  return [
@@ -48,39 +52,40 @@ function y({ disabled: y = !1, error: b = !1, height: le, helperText: x, maxChar
48
52
  ];
49
53
  },
50
54
  suggestion: r({
51
- getItems: () => J.current,
52
- onMentionSearch: E,
55
+ getItems: () => q.current,
56
+ onMentionSearch: D,
57
+ onMentionInserted: (e) => J.current?.(e),
53
58
  noResultsLabel: V.mentionNoResults ?? "No results"
54
59
  })
55
60
  }) : null, [
56
61
  Z,
57
62
  w,
58
- E
59
- ]), pe = S !== void 0 && S > 0 || k || j, $ = p({
63
+ D
64
+ ]), ge = S !== void 0 && S > 0 || k || j, $ = ie({
60
65
  shouldRerenderOnTransaction: !0,
61
66
  extensions: [
62
67
  ae.configure({ link: { openOnClick: !1 } }),
63
68
  oe,
64
69
  se,
65
70
  ce.configure({ multicolor: !0 }),
66
- v.configure({
71
+ _.configure({
67
72
  transformPastedText: !0,
68
73
  transformCopiedText: !1
69
74
  }),
70
- m.configure({ placeholder: D ?? "" }),
71
- ...H.showTableButton ? [g] : [],
72
- ...H.showImageButton ? [_.configure({
75
+ le.configure({ placeholder: de ?? "" }),
76
+ ...H.showTableButton ? [h] : [],
77
+ ...H.showImageButton ? [g.configure({
73
78
  inline: !1,
74
79
  allowBase64: !0
75
80
  })] : [],
76
81
  ...Q ? [Q] : [],
77
- ...pe ? S !== void 0 && S > 0 ? [h.configure({ limit: S })] : [h] : []
82
+ ...ge ? S !== void 0 && S > 0 ? [m.configure({ limit: S })] : [m] : []
78
83
  ],
79
- content: P ?? "",
84
+ content: P ?? v ?? "",
80
85
  editable: !y && !O,
81
86
  editorProps: {
82
87
  handlePaste(e, t) {
83
- if (!K.current) return !1;
88
+ if (!G.current) return !1;
84
89
  let n = t.clipboardData?.getData("text/plain");
85
90
  if (!n) return !1;
86
91
  t.preventDefault();
@@ -88,7 +93,7 @@ function y({ disabled: y = !1, error: b = !1, height: le, helperText: x, maxChar
88
93
  return e.dispatch(r.tr.insertText(n, r.selection.from, r.selection.to)), !0;
89
94
  },
90
95
  handleKeyDown(e, t) {
91
- return (t.ctrlKey || t.metaKey) && t.key === "s" ? (t.preventDefault(), q.current?.(), !0) : !1;
96
+ return (t.ctrlKey || t.metaKey) && t.key === "s" ? (t.preventDefault(), K.current?.(), !0) : !1;
92
97
  }
93
98
  },
94
99
  onUpdate({ editor: e }) {
@@ -110,7 +115,7 @@ function y({ disabled: y = !1, error: b = !1, height: le, helperText: x, maxChar
110
115
  y,
111
116
  O
112
117
  ]);
113
- let me = $?.storage.characterCount?.characters?.() ?? 0, he = $?.storage.characterCount?.words?.() ?? 0, ge = k || S !== void 0 && S > 0 || j || !!x;
118
+ let _e = $?.storage.characterCount?.characters?.() ?? 0, ve = $?.storage.characterCount?.words?.() ?? 0, ye = k || S !== void 0 && S > 0 || j || !!x;
114
119
  return /* @__PURE__ */ f(l, {
115
120
  sx: U ? {
116
121
  position: "fixed",
@@ -124,7 +129,7 @@ function y({ disabled: y = !1, error: b = !1, height: le, helperText: x, maxChar
124
129
  bgcolor: "background.default",
125
130
  p: 1
126
131
  } : {
127
- width: de ?? "100%",
132
+ width: me ?? "100%",
128
133
  ...X ? {
129
134
  display: "flex",
130
135
  flexDirection: "column",
@@ -132,43 +137,43 @@ function y({ disabled: y = !1, error: b = !1, height: le, helperText: x, maxChar
132
137
  } : {}
133
138
  },
134
139
  children: [
135
- /* @__PURE__ */ f(re, {
140
+ /* @__PURE__ */ f(ne, {
136
141
  variant: "outlined",
137
142
  sx: {
138
143
  display: "flex",
139
144
  flexDirection: "column",
140
145
  overflow: "hidden",
141
- ...U || X ? { flex: 1 } : { height: fe },
146
+ ...U || X ? { flex: 1 } : { height: he },
142
147
  borderColor: b ? "error.main" : void 0,
143
148
  "&:focus-within": {
144
149
  borderColor: b ? "error.main" : "primary.main",
145
150
  borderWidth: 2
146
151
  }
147
152
  },
148
- children: [A && !O && /* @__PURE__ */ f(u, { children: [/* @__PURE__ */ d(ee, {
153
+ children: [A && !O && /* @__PURE__ */ f(re, { children: [/* @__PURE__ */ d(i, {
149
154
  editor: $,
150
155
  toolbarConfig: H,
151
156
  translation: V,
152
157
  disabled: y,
153
158
  isFullscreen: U,
154
- onToggleFullscreen: () => W((e) => !e),
155
- pasteAsPlainText: G,
156
- onTogglePasteAsPlainText: () => ue((e) => !e)
157
- }), /* @__PURE__ */ d(ne, {})] }), /* @__PURE__ */ d(i, {
159
+ onToggleFullscreen: () => fe((e) => !e),
160
+ pasteAsPlainText: W,
161
+ onTogglePasteAsPlainText: () => pe((e) => !e)
162
+ }), /* @__PURE__ */ d(u, {})] }), /* @__PURE__ */ d(ee, {
158
163
  editor: $,
159
164
  error: b,
160
165
  disabled: y,
161
166
  readonly: O
162
167
  })]
163
168
  }),
164
- ge && /* @__PURE__ */ d(a, {
169
+ ye && /* @__PURE__ */ d(a, {
165
170
  helperText: x,
166
171
  error: b,
167
172
  showCharacterCount: k || S !== void 0 && S > 0,
168
- charCount: me,
173
+ charCount: _e,
169
174
  maxCharacters: S && S > 0 ? S : void 0,
170
175
  showWordCount: j,
171
- wordCount: he,
176
+ wordCount: ve,
172
177
  translation: V
173
178
  }),
174
179
  T && /* @__PURE__ */ d("input", {
@@ -180,4 +185,4 @@ function y({ disabled: y = !1, error: b = !1, height: le, helperText: x, maxChar
180
185
  });
181
186
  }
182
187
  //#endregion
183
- export { y as RichTextEditor };
188
+ export { v as RichTextEditor };
@@ -125,6 +125,13 @@ export type RichTextEditorProps = {
125
125
  toolbarConfig?: RichTextEditorToolbarConfig;
126
126
  translation?: Partial<RichTextEditorTranslation>;
127
127
  value?: string;
128
+ /**
129
+ * Initial HTML content for uncontrolled usage — set once on mount, never re-synced.
130
+ * Analogous to MUI TextField's `defaultValue`: the editor manages its own content
131
+ * after initialisation, no external state required.
132
+ * When both `value` and `defaultValue` are provided, `value` takes precedence.
133
+ */
134
+ defaultValue?: string;
128
135
  /** Breite des Editors. Zahlen → px. "auto" oder leer → 100% des Elternelements. */
129
136
  width?: number | string;
130
137
  /**
@@ -140,6 +147,13 @@ export type RichTextEditorProps = {
140
147
  * @since 3.14.0
141
148
  */
142
149
  onMentionSearch?: (query: string) => MentionItem[] | Promise<MentionItem[]>;
150
+ /**
151
+ * Called when the user selects a mention item from the autocomplete dropdown.
152
+ * Fires immediately after the mention node is inserted into the editor.
153
+ * Useful for tracking which people are mentioned without parsing the HTML output.
154
+ * @since 3.20.0
155
+ */
156
+ onMentionInserted?: (item: MentionItem) => void;
143
157
  /**
144
158
  * Character that triggers the mention autocomplete popup. Default: "@"
145
159
  * @since 3.14.0
@@ -3,7 +3,13 @@ import type { MentionItem } from "./RichTextEditor.types";
3
3
  type BuildSuggestionOptions = {
4
4
  getItems: () => MentionItem[];
5
5
  onMentionSearch?: (query: string) => MentionItem[] | Promise<MentionItem[]>;
6
+ onMentionInserted?: (item: MentionItem) => void;
6
7
  noResultsLabel: string;
7
8
  };
8
- export declare function buildMentionSuggestion({ getItems, onMentionSearch, noResultsLabel, }: BuildSuggestionOptions): Partial<SuggestionOptions<MentionItem>>;
9
+ /** Pure handler factory extracted for testability without mocking ReactRenderer. */
10
+ export declare function createMentionSelectHandler(command: (attrs: {
11
+ id: string;
12
+ label: string;
13
+ }) => void, onMentionInserted?: (item: MentionItem) => void): (item: MentionItem) => void;
14
+ export declare function buildMentionSuggestion({ getItems, onMentionSearch, onMentionInserted, noResultsLabel, }: BuildSuggestionOptions): Partial<SuggestionOptions<MentionItem>>;
9
15
  export {};
@@ -1,45 +1,50 @@
1
1
  import { RichTextEditorMentionList as e } from "./RichTextEditorMentionList.js";
2
2
  import { ReactRenderer as t } from "@tiptap/react";
3
3
  //#region src/components/rich-text-editor/RichTextEditorMentionSuggestion.ts
4
- function n({ getItems: n, onMentionSearch: r, noResultsLabel: i }) {
4
+ function n(e, t) {
5
+ return (n) => {
6
+ e({
7
+ id: n.id,
8
+ label: n.label
9
+ }), t?.(n);
10
+ };
11
+ }
12
+ function r({ getItems: r, onMentionSearch: i, onMentionInserted: a, noResultsLabel: o }) {
5
13
  return {
6
14
  items: async ({ query: e }) => {
7
- if (r) return r(e);
8
- let t = n(), i = e.toLowerCase();
9
- return t.filter((e) => e.label.toLowerCase().includes(i));
15
+ if (i) return i(e);
16
+ let t = r(), n = e.toLowerCase();
17
+ return t.filter((e) => e.label.toLowerCase().includes(n));
10
18
  },
11
19
  render() {
12
- let n = null;
20
+ let r = null;
13
21
  return {
14
- onStart(r) {
15
- n = new t(e, {
22
+ onStart(i) {
23
+ r = new t(e, {
16
24
  props: {
17
- items: r.items,
18
- noResultsLabel: i,
19
- clientRect: r.clientRect,
20
- onSelect: (e) => r.command({
21
- id: e.id,
22
- label: e.label
23
- })
25
+ items: i.items,
26
+ noResultsLabel: o,
27
+ clientRect: i.clientRect,
28
+ onSelect: n(i.command, a)
24
29
  },
25
- editor: r.editor
26
- }), document.body.appendChild(n.element);
30
+ editor: i.editor
31
+ }), document.body.appendChild(r.element);
27
32
  },
28
33
  onUpdate(e) {
29
- n?.updateProps({
34
+ r?.updateProps({
30
35
  items: e.items,
31
36
  clientRect: e.clientRect
32
37
  });
33
38
  },
34
39
  onKeyDown(e) {
35
- return e.event.key === "Escape" ? (n?.destroy(), n = null, !0) : n?.ref?.onKeyDown(e.event) ?? !1;
40
+ return e.event.key === "Escape" ? (r?.destroy(), r = null, !0) : r?.ref?.onKeyDown(e.event) ?? !1;
36
41
  },
37
42
  onExit() {
38
- n?.element.remove(), n?.destroy(), n = null;
43
+ r?.element.remove(), r?.destroy(), r = null;
39
44
  }
40
45
  };
41
46
  }
42
47
  };
43
48
  }
44
49
  //#endregion
45
- export { n as buildMentionSuggestion };
50
+ export { r as buildMentionSuggestion };