react-mention-input 1.1.6 → 1.1.8

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,290 +1,300 @@
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;
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
+ const getInitials = (name: string) => {
155
+ const nameParts = name.split(" ");
156
+ const initials = nameParts
157
+ .map((part) => part[0]?.toUpperCase() || "") // Take the first letter of each part
158
+ .slice(0, 2) // Limit to 2 letters
159
+ .join("");
160
+ return initials;
161
+ };
162
+
163
+ // Use suggestionPosition prop to adjust tooltip position
164
+ switch (suggestionPosition) {
165
+ case 'top':
166
+ styles.left = `${inputRect.left}px`;
167
+ styles.top = `${inputRect.top - 150}px`;
168
+ break;
169
+ case 'bottom':
170
+ styles.left = `${inputRect.left}px`;
171
+ styles.top = `${inputRect.bottom}px`;
172
+ break;
173
+ case 'left':
174
+ styles.left = `${inputRect.left - 150}px`;
175
+ styles.top = `${inputRect.top}px`;
176
+ break;
177
+ case 'right':
178
+ styles.left = `${inputRect.right}px`;
179
+ styles.top = `${inputRect.top}px`;
180
+ break;
181
+ default:
182
+ break;
183
+ }
184
+
185
+ return ReactDOM.createPortal(
186
+ <ul
187
+ className={`suggestion-list ${suggestionListClassName || ''}`}
188
+ ref={suggestionListRef}
189
+ style={styles}
190
+ >
191
+ {suggestions.map((user) => (
192
+ <li
193
+ key={user.id}
194
+ onClick={() => handleSuggestionClick(user)}
195
+ className={`suggestion-item ${suggestionItemClassName || ''}`}
196
+ role="option"
197
+ tabIndex={0}
198
+ aria-selected="false"
199
+ >
200
+ <div className="user-icon">{getInitials(user?.name)}</div>
201
+ {user.name}
202
+ </li>
203
+ ))}
204
+ </ul>,
205
+ window.document.body // Render in portal
206
+ );
207
+ };
208
+
209
+ const handleSuggestionClick = (user: User) => {
210
+ if (!inputRef.current) return;
211
+
212
+ const plainText = inputValue;
213
+ const caretOffset = caretOffsetRef.current;
214
+ const mentionMatch = plainText.slice(0, caretOffset).match(/@(\S*)$/);
215
+
216
+ if (!userSelectListRef.current.includes(user.name)) {
217
+ userSelectListRef.current.push(user.name);
218
+ }
219
+
220
+ // Check if the ID is already stored
221
+ const isIdExists = userSelectListWithIdsRef.current.some(
222
+ (item) => item.id === user.id
223
+ );
224
+ if (!isIdExists) {
225
+ userSelectListWithIdsRef.current.push(user);
226
+ }
227
+
228
+ if (!mentionMatch) return;
229
+
230
+ const mentionIndex = plainText.slice(0, caretOffset).lastIndexOf("@");
231
+
232
+ // Append space after the mention
233
+ const newValue =
234
+ plainText.substring(0, mentionIndex + 1) + user.name + " " + plainText.substring(caretOffset);
235
+
236
+ setInputValue(newValue);
237
+ inputRef.current.innerText = newValue;
238
+
239
+ // Highlight mentions and links with &nbsp;
240
+ const htmlWithHighlights = highlightMentionsAndLinks(newValue);
241
+
242
+ // Set highlighted content
243
+ inputRef.current.innerHTML = htmlWithHighlights;
244
+
245
+ setShowSuggestions(false);
246
+
247
+ // Adjust caret position after adding the mention and space
248
+ const mentionEnd = mentionIndex + user.name.length + 1;
249
+ restoreCaretPosition(inputRef.current, mentionEnd + 1); // +1 for the space
250
+ };
251
+
252
+ const handleSendMessage = () => {
253
+ if (inputRef.current) {
254
+ const messageText = inputRef.current.innerText.trim(); // Plain text
255
+ const messageHTML = inputRef.current.innerHTML.trim(); // HTML with <span> highlighting
256
+
257
+ if (messageText && onSendMessage) {
258
+ onSendMessage({messageText, messageHTML,userSelectListWithIds:userSelectListWithIdsRef.current,userSelectListName:userSelectListRef.current }); // Pass both plain text and HTML
259
+ setInputValue(""); // Clear state
260
+ setShowSuggestions(false); // Hide suggestions
261
+ inputRef.current.innerText = ""; // Clear input field
262
+ userSelectListRef.current = []
263
+ userSelectListWithIdsRef.current = []
264
+ }
265
+ }
266
+ };
267
+
268
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
269
+ if (event.key === "Enter" && !event.shiftKey) {
270
+ event.preventDefault(); // Prevent newline in content-editable
271
+ handleSendMessage(); // Trigger the same function as the Send button
272
+ }
273
+ };
274
+
275
+
276
+
277
+ return (
278
+ <div className={`mention-container ${containerClassName || ""}`}>
279
+ <div className={`mention-input-container ${inputContainerClassName || ""}`}>
280
+ <div
281
+ ref={inputRef}
282
+ contentEditable
283
+ suppressContentEditableWarning
284
+ className={`mention-input ${inputClassName || ""}`}
285
+ onInput={handleInputChange}
286
+ onKeyDown={handleKeyDown} // Add keydown listener
287
+ ></div>
288
+ <button
289
+ onClick={handleSendMessage}
290
+ className={`send-button ${sendBtnClassName || ""}`}
291
+ >
292
+ {sendButtonIcon || "➤"}
293
+ </button>
294
+ </div>
295
+ {renderSuggestions()}
296
+ </div>
297
+ );
298
+ };
299
+
300
+ export default MentionInput;