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.
- package/README.md +1 -1
- package/dist/MentionInput.css +106 -106
- package/dist/MentionInput.d.ts +5 -0
- package/dist/MentionInput.js +28 -9
- package/dist/ShowMessageCard.css +74 -74
- package/package.json +1 -1
- package/src/MentionInput.css +106 -106
- package/src/MentionInput.tsx +290 -264
- package/src/ShowMessageCard.css +74 -74
- package/src/ShowMessageCard.tsx +167 -167
- package/src/index.ts +2 -2
- package/tsconfig.json +14 -14
package/src/MentionInput.tsx
CHANGED
|
@@ -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
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
styles.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
styles.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
{user.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
|
|
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> `;
|
|
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
|
|
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;
|