react-mention-input 1.1.5 → 1.1.7

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,264 +1,290 @@
1
- import React, { useState, useRef, useEffect, ReactNode } from "react";
2
- import ReactDOM from "react-dom";
3
- import "./MentionInput.css";
4
-
5
- interface User {
6
- id: number;
7
- name: string; // Full name, e.g., "John Heart"
8
- }
9
-
10
- interface MentionInputProps {
11
- users: User[];
12
- placeholder?: string;
13
- containerClassName?: string;
14
- inputContainerClassName?: string;
15
- inputClassName?: string;
16
- sendBtnClassName?: string;
17
- suggestionListClassName?: string;
18
- suggestionItemClassName?: string;
19
- sendButtonIcon?: ReactNode; // Button icon (MUI icon or image path)
20
- onSendMessage?: (obj:{messageText: string, messageHTML: string}) => void;
21
- suggestionPosition?: 'top' | 'bottom' | 'left' | 'right'; // New prop for tooltip position
22
- }
23
-
24
- const MentionInput: React.FC<MentionInputProps> = ({
25
- users,
26
- placeholder = "Type a message...",
27
- containerClassName,
28
- inputContainerClassName,
29
- inputClassName,
30
- sendBtnClassName,
31
- suggestionListClassName,
32
- suggestionItemClassName,
33
- sendButtonIcon,
34
- onSendMessage,
35
- suggestionPosition = 'bottom', // Default position is bottom
36
- }) => {
37
- const [inputValue, setInputValue] = useState<string>(""); // Plain text
38
- const [suggestions, setSuggestions] = useState<User[]>([]);
39
- const [showSuggestions, setShowSuggestions] = useState<boolean>(false);
40
-
41
- const inputRef = useRef<HTMLDivElement>(null);
42
- const suggestionListRef = useRef<HTMLUListElement>(null);
43
- const caretOffsetRef = useRef<number>(0);
44
-
45
- const highlightMentionsAndLinks = (text: string): string => {
46
- // Regular expression for detecting links
47
- const linkRegex = /(https?:\/\/[^\s]+)/g;
48
-
49
- // Regular expression for mentions
50
- const mentionRegex = /@([^\s]+(?: [^\s]+)?)(?=\s|$)/g;
51
-
52
- // First, highlight mentions
53
- let highlightedText = text.replace(
54
- mentionRegex,
55
- '<span class="mention-highlight">@$1</span>'
56
- );
57
-
58
- // Then, highlight links
59
- highlightedText = highlightedText.replace(
60
- linkRegex,
61
- '<a href="$1" target="_blank" rel="noopener noreferrer" class="link-highlight">$1</a>'
62
- );
63
-
64
- return highlightedText;
65
- };
66
-
67
- const restoreCaretPosition = (node: HTMLElement, caretOffset: number) => {
68
- const range = document.createRange();
69
- const sel = window.getSelection();
70
- let charCount = 0;
71
-
72
- const findCaret = (currentNode: Node) => {
73
- for (const child of Array.from(currentNode.childNodes)) {
74
- if (child.nodeType === Node.TEXT_NODE) {
75
- const textLength = child.textContent?.length || 0;
76
- if (charCount + textLength >= caretOffset) {
77
- range.setStart(child, caretOffset - charCount);
78
- range.collapse(true);
79
- return true;
80
- } else {
81
- charCount += textLength;
82
- }
83
- } else if (child.nodeType === Node.ELEMENT_NODE) {
84
- if (findCaret(child)) return true;
85
- }
86
- }
87
- return false;
88
- };
89
-
90
- findCaret(node);
91
-
92
- if (sel) {
93
- sel.removeAllRanges();
94
- sel.addRange(range);
95
- }
96
- };
97
-
98
- const handleInputChange = () => {
99
- if (!inputRef.current) return;
100
-
101
- const selection = window.getSelection();
102
- const range = selection?.getRangeAt(0);
103
-
104
- let newCaretOffset = 0;
105
- if (range && inputRef.current.contains(range.startContainer)) {
106
- const preCaretRange = range.cloneRange();
107
- preCaretRange.selectNodeContents(inputRef.current);
108
- preCaretRange.setEnd(range.startContainer, range.startOffset);
109
- newCaretOffset = preCaretRange.toString().length;
110
- }
111
-
112
- caretOffsetRef.current = newCaretOffset;
113
-
114
- const plainText = inputRef.current.innerText;
115
- setInputValue(plainText);
116
-
117
- const mentionMatch = plainText.slice(0, newCaretOffset).match(/@(\S*)$/);
118
- if (mentionMatch) {
119
- const query = mentionMatch[1].toLowerCase();
120
- const filteredUsers = query === "" ? users : users.filter((user) => user.name.toLowerCase().startsWith(query));
121
-
122
- setSuggestions(filteredUsers);
123
- setShowSuggestions(filteredUsers.length > 0);
124
- } else {
125
- setShowSuggestions(false);
126
- }
127
-
128
- const previousHTML = inputRef.current.innerHTML;
129
- const htmlWithHighlights = highlightMentionsAndLinks(plainText); // Updated function
130
- if (previousHTML !== htmlWithHighlights) {
131
- inputRef.current.innerHTML = htmlWithHighlights;
132
- }
133
-
134
- restoreCaretPosition(inputRef.current, newCaretOffset);
135
- };
136
-
137
-
138
- const renderSuggestions = () => {
139
- if (!showSuggestions || !inputRef.current) return null;
140
-
141
- const inputRect = inputRef.current.getBoundingClientRect();
142
- const styles: React.CSSProperties = {
143
- position: 'absolute',
144
- zIndex: 1000,
145
- };
146
-
147
- // Use suggestionPosition prop to adjust tooltip position
148
- switch (suggestionPosition) {
149
- case 'top':
150
- styles.left = `${inputRect.left}px`;
151
- styles.top = `${inputRect.top - 150}px`;
152
- break;
153
- case 'bottom':
154
- styles.left = `${inputRect.left}px`;
155
- styles.top = `${inputRect.bottom}px`;
156
- break;
157
- case 'left':
158
- styles.left = `${inputRect.left - 150}px`;
159
- styles.top = `${inputRect.top}px`;
160
- break;
161
- case 'right':
162
- styles.left = `${inputRect.right}px`;
163
- styles.top = `${inputRect.top}px`;
164
- break;
165
- default:
166
- break;
167
- }
168
-
169
- return ReactDOM.createPortal(
170
- <ul
171
- className={`suggestion-list ${suggestionListClassName || ''}`}
172
- ref={suggestionListRef}
173
- style={styles}
174
- >
175
- {suggestions.map((user) => (
176
- <li
177
- key={user.id}
178
- onClick={() => handleSuggestionClick(user)}
179
- className={`suggestion-item ${suggestionItemClassName || ''}`}
180
- role="option"
181
- tabIndex={0}
182
- aria-selected="false"
183
- >
184
- {user.name}
185
- </li>
186
- ))}
187
- </ul>,
188
- window.document.body // Render in portal
189
- );
190
- };
191
-
192
- const handleSuggestionClick = (user: User) => {
193
- if (!inputRef.current) return;
194
-
195
- const plainText = inputValue;
196
- const caretOffset = caretOffsetRef.current;
197
- const mentionMatch = plainText.slice(0, caretOffset).match(/@(\S*)$/);
198
-
199
- if (!mentionMatch) return;
200
-
201
- const mentionIndex = plainText.slice(0, caretOffset).lastIndexOf("@");
202
-
203
- const newValue =
204
- plainText.substring(0, mentionIndex + 1) + user.name + plainText.substring(caretOffset);
205
-
206
- setInputValue(newValue);
207
- inputRef.current.innerText = newValue;
208
-
209
- const htmlWithHighlights = highlightMentionsAndLinks(newValue);
210
- inputRef.current.innerHTML = htmlWithHighlights;
211
-
212
- setShowSuggestions(false);
213
-
214
- const mentionEnd = mentionIndex + user.name.length + 1;
215
- restoreCaretPosition(inputRef.current, mentionEnd);
216
- };
217
-
218
- const handleSendMessage = () => {
219
- if (inputRef.current) {
220
- const messageText = inputRef.current.innerText.trim(); // Plain text
221
- const messageHTML = inputRef.current.innerHTML.trim(); // HTML with <span> highlighting
222
-
223
- if (messageText && onSendMessage) {
224
- onSendMessage({messageText, messageHTML}); // Pass both plain text and HTML
225
- setInputValue(""); // Clear state
226
- setShowSuggestions(false); // Hide suggestions
227
- inputRef.current.innerText = ""; // Clear input field
228
- }
229
- }
230
- };
231
-
232
- const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
233
- if (event.key === "Enter" && !event.shiftKey) {
234
- event.preventDefault(); // Prevent newline in content-editable
235
- handleSendMessage(); // Trigger the same function as the Send button
236
- }
237
- };
238
-
239
-
240
-
241
- return (
242
- <div className={`mention-container ${containerClassName || ""}`}>
243
- <div className={`mention-input-container ${inputContainerClassName || ""}`}>
244
- <div
245
- ref={inputRef}
246
- contentEditable
247
- suppressContentEditableWarning
248
- className={`mention-input ${inputClassName || ""}`}
249
- onInput={handleInputChange}
250
- onKeyDown={handleKeyDown} // Add keydown listener
251
- ></div>
252
- <button
253
- onClick={handleSendMessage}
254
- className={`send-button ${sendBtnClassName || ""}`}
255
- >
256
- {sendButtonIcon || "➤"}
257
- </button>
258
- </div>
259
- {renderSuggestions()}
260
- </div>
261
- );
262
- };
263
-
264
- export default MentionInput;
1
+ import React, { useState, useRef, useEffect, ReactNode } from "react";
2
+ import ReactDOM from "react-dom";
3
+ import "./MentionInput.css";
4
+
5
+ interface User {
6
+ id: number;
7
+ name: string; // Full name, e.g., "John Heart"
8
+ }
9
+
10
+ interface MentionInputProps {
11
+ users: User[];
12
+ placeholder?: string;
13
+ containerClassName?: string;
14
+ inputContainerClassName?: string;
15
+ inputClassName?: string;
16
+ sendBtnClassName?: string;
17
+ suggestionListClassName?: string;
18
+ suggestionItemClassName?: string;
19
+ sendButtonIcon?: ReactNode; // Button icon (MUI icon or image path)
20
+ onSendMessage?: (obj:{messageText: string, messageHTML: string,userSelectListWithIds:{ id: number; name: string }[],userSelectListName:string[]}) => void;
21
+ suggestionPosition?: 'top' | 'bottom' | 'left' | 'right'; // New prop for tooltip position
22
+ }
23
+
24
+ const MentionInput: React.FC<MentionInputProps> = ({
25
+ users,
26
+ placeholder = "Type a message...",
27
+ containerClassName,
28
+ inputContainerClassName,
29
+ inputClassName,
30
+ sendBtnClassName,
31
+ suggestionListClassName,
32
+ suggestionItemClassName,
33
+ sendButtonIcon,
34
+ onSendMessage,
35
+ suggestionPosition = 'bottom', // Default position is bottom
36
+ }) => {
37
+ const [inputValue, setInputValue] = useState<string>(""); // Plain text
38
+ const [suggestions, setSuggestions] = useState<User[]>([]);
39
+ const [showSuggestions, setShowSuggestions] = useState<boolean>(false);
40
+
41
+
42
+
43
+ const inputRef = useRef<HTMLDivElement>(null);
44
+ const suggestionListRef = useRef<HTMLUListElement>(null);
45
+ const caretOffsetRef = useRef<number>(0);
46
+ const userSelectListRef = useRef<string[]>([]); // Only unique names
47
+ const userSelectListWithIdsRef = useRef<{ id: number; name: string }[]>([]); // Unique IDs with names
48
+
49
+ const highlightMentionsAndLinks = (text: string): string => {
50
+ // Regular expression for detecting links
51
+ const linkRegex = /(https?:\/\/[^\s]+)/g;
52
+
53
+ // Highlight links
54
+ let highlightedText = text.replace(
55
+ linkRegex,
56
+ '<a href="$1" target="_blank" rel="noopener noreferrer" class="link-highlight">$1</a>'
57
+ );
58
+
59
+ // Highlight mentions manually based on `userSelectListRef`
60
+ userSelectListRef?.current.forEach((userName) => {
61
+ const mentionPattern = new RegExp(`@${userName}(\\s|$)`, "g");
62
+ highlightedText = highlightedText.replace(
63
+ mentionPattern,
64
+ (match, trailingSpace) => {
65
+ return `<span class="mention-highlight">${match.trim()}</span>&nbsp;`;
66
+ }
67
+ );
68
+ });
69
+
70
+ return highlightedText;
71
+ };
72
+
73
+
74
+ const restoreCaretPosition = (node: HTMLElement, caretOffset: number) => {
75
+ const range = document.createRange();
76
+ const sel = window.getSelection();
77
+ let charCount = 0;
78
+
79
+ const findCaret = (currentNode: Node) => {
80
+ for (const child of Array.from(currentNode.childNodes)) {
81
+ if (child.nodeType === Node.TEXT_NODE) {
82
+ const textLength = child.textContent?.length || 0;
83
+ if (charCount + textLength >= caretOffset) {
84
+ range.setStart(child, caretOffset - charCount);
85
+ range.collapse(true);
86
+ return true;
87
+ } else {
88
+ charCount += textLength;
89
+ }
90
+ } else if (child.nodeType === Node.ELEMENT_NODE) {
91
+ if (findCaret(child)) return true;
92
+ }
93
+ }
94
+ return false;
95
+ };
96
+
97
+ findCaret(node);
98
+
99
+ if (sel) {
100
+ sel.removeAllRanges();
101
+ sel.addRange(range);
102
+ }
103
+ };
104
+
105
+ const handleInputChange = () => {
106
+ if (!inputRef.current) return;
107
+
108
+ const selection = window.getSelection();
109
+ const range = selection?.getRangeAt(0);
110
+
111
+ let newCaretOffset = 0;
112
+ if (range && inputRef.current.contains(range.startContainer)) {
113
+ const preCaretRange = range.cloneRange();
114
+ preCaretRange.selectNodeContents(inputRef.current);
115
+ preCaretRange.setEnd(range.startContainer, range.startOffset);
116
+ newCaretOffset = preCaretRange.toString().length;
117
+ }
118
+
119
+ caretOffsetRef.current = newCaretOffset;
120
+
121
+ const plainText = inputRef.current.innerText;
122
+ setInputValue(plainText);
123
+
124
+ const mentionMatch = plainText.slice(0, newCaretOffset).match(/@(\S*)$/);
125
+ if (mentionMatch) {
126
+ const query = mentionMatch[1].toLowerCase();
127
+ const filteredUsers = query === "" ? users : users.filter((user) => user.name.toLowerCase().startsWith(query));
128
+
129
+ setSuggestions(filteredUsers);
130
+ setShowSuggestions(filteredUsers.length > 0);
131
+ } else {
132
+ setShowSuggestions(false);
133
+ }
134
+
135
+ const previousHTML = inputRef.current.innerHTML;
136
+ const htmlWithHighlights = highlightMentionsAndLinks(plainText); // Updated function
137
+ if (previousHTML !== htmlWithHighlights) {
138
+ inputRef.current.innerHTML = htmlWithHighlights;
139
+ }
140
+
141
+ restoreCaretPosition(inputRef.current, newCaretOffset);
142
+ };
143
+
144
+
145
+ const renderSuggestions = () => {
146
+ if (!showSuggestions || !inputRef.current) return null;
147
+
148
+ const inputRect = inputRef.current.getBoundingClientRect();
149
+ const styles: React.CSSProperties = {
150
+ position: 'absolute',
151
+ zIndex: 1000,
152
+ };
153
+
154
+ // Use suggestionPosition prop to adjust tooltip position
155
+ switch (suggestionPosition) {
156
+ case 'top':
157
+ styles.left = `${inputRect.left}px`;
158
+ styles.top = `${inputRect.top - 150}px`;
159
+ break;
160
+ case 'bottom':
161
+ styles.left = `${inputRect.left}px`;
162
+ styles.top = `${inputRect.bottom}px`;
163
+ break;
164
+ case 'left':
165
+ styles.left = `${inputRect.left - 150}px`;
166
+ styles.top = `${inputRect.top}px`;
167
+ break;
168
+ case 'right':
169
+ styles.left = `${inputRect.right}px`;
170
+ styles.top = `${inputRect.top}px`;
171
+ break;
172
+ default:
173
+ break;
174
+ }
175
+
176
+ return ReactDOM.createPortal(
177
+ <ul
178
+ className={`suggestion-list ${suggestionListClassName || ''}`}
179
+ ref={suggestionListRef}
180
+ style={styles}
181
+ >
182
+ {suggestions.map((user) => (
183
+ <li
184
+ key={user.id}
185
+ onClick={() => handleSuggestionClick(user)}
186
+ className={`suggestion-item ${suggestionItemClassName || ''}`}
187
+ role="option"
188
+ tabIndex={0}
189
+ aria-selected="false"
190
+ >
191
+ {user.name}
192
+ </li>
193
+ ))}
194
+ </ul>,
195
+ window.document.body // Render in portal
196
+ );
197
+ };
198
+
199
+ const handleSuggestionClick = (user: User) => {
200
+ if (!inputRef.current) return;
201
+
202
+ const plainText = inputValue;
203
+ const caretOffset = caretOffsetRef.current;
204
+ const mentionMatch = plainText.slice(0, caretOffset).match(/@(\S*)$/);
205
+
206
+ if (!userSelectListRef.current.includes(user.name)) {
207
+ userSelectListRef.current.push(user.name);
208
+ }
209
+
210
+ // Check if the ID is already stored
211
+ const isIdExists = userSelectListWithIdsRef.current.some(
212
+ (item) => item.id === user.id
213
+ );
214
+ if (!isIdExists) {
215
+ userSelectListWithIdsRef.current.push(user);
216
+ }
217
+
218
+ if (!mentionMatch) return;
219
+
220
+ const mentionIndex = plainText.slice(0, caretOffset).lastIndexOf("@");
221
+
222
+ // Append space after the mention
223
+ const newValue =
224
+ plainText.substring(0, mentionIndex + 1) + user.name + " " + plainText.substring(caretOffset);
225
+
226
+ setInputValue(newValue);
227
+ inputRef.current.innerText = newValue;
228
+
229
+ // Highlight mentions and links with &nbsp;
230
+ const htmlWithHighlights = highlightMentionsAndLinks(newValue);
231
+
232
+ // Set highlighted content
233
+ inputRef.current.innerHTML = htmlWithHighlights;
234
+
235
+ setShowSuggestions(false);
236
+
237
+ // Adjust caret position after adding the mention and space
238
+ const mentionEnd = mentionIndex + user.name.length + 1;
239
+ restoreCaretPosition(inputRef.current, mentionEnd + 1); // +1 for the space
240
+ };
241
+
242
+ const handleSendMessage = () => {
243
+ if (inputRef.current) {
244
+ const messageText = inputRef.current.innerText.trim(); // Plain text
245
+ const messageHTML = inputRef.current.innerHTML.trim(); // HTML with <span> highlighting
246
+
247
+ if (messageText && onSendMessage) {
248
+ onSendMessage({messageText, messageHTML,userSelectListWithIds:userSelectListWithIdsRef.current,userSelectListName:userSelectListRef.current }); // Pass both plain text and HTML
249
+ setInputValue(""); // Clear state
250
+ setShowSuggestions(false); // Hide suggestions
251
+ inputRef.current.innerText = ""; // Clear input field
252
+ userSelectListRef.current = []
253
+ userSelectListWithIdsRef.current = []
254
+ }
255
+ }
256
+ };
257
+
258
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
259
+ if (event.key === "Enter" && !event.shiftKey) {
260
+ event.preventDefault(); // Prevent newline in content-editable
261
+ handleSendMessage(); // Trigger the same function as the Send button
262
+ }
263
+ };
264
+
265
+
266
+
267
+ return (
268
+ <div className={`mention-container ${containerClassName || ""}`}>
269
+ <div className={`mention-input-container ${inputContainerClassName || ""}`}>
270
+ <div
271
+ ref={inputRef}
272
+ contentEditable
273
+ suppressContentEditableWarning
274
+ className={`mention-input ${inputClassName || ""}`}
275
+ onInput={handleInputChange}
276
+ onKeyDown={handleKeyDown} // Add keydown listener
277
+ ></div>
278
+ <button
279
+ onClick={handleSendMessage}
280
+ className={`send-button ${sendBtnClassName || ""}`}
281
+ >
282
+ {sendButtonIcon || "➤"}
283
+ </button>
284
+ </div>
285
+ {renderSuggestions()}
286
+ </div>
287
+ );
288
+ };
289
+
290
+ export default MentionInput;