react-mention-input 1.1.11 → 1.1.13

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,4 +1,4 @@
1
- import React, { useState, useRef, useEffect, ReactNode } from "react";
1
+ import React, { useState, useRef, ReactNode } from "react";
2
2
  import ReactDOM from "react-dom";
3
3
  import "./MentionInput.css";
4
4
 
@@ -16,27 +16,46 @@ interface MentionInputProps {
16
16
  sendBtnClassName?: string;
17
17
  suggestionListClassName?: string;
18
18
  suggestionItemClassName?: string;
19
+ imgClassName?: string; // New prop for image CSS class
20
+ imgStyle?: React.CSSProperties; // New prop for inline image styles
19
21
  sendButtonIcon?: ReactNode; // Button icon (MUI icon or image path)
20
- onSendMessage?: (obj:{messageText: string, messageHTML: string,userSelectListWithIds:{ id: number; name: string }[],userSelectListName:string[]}) => void;
22
+ attachmentButtonIcon?: ReactNode; // New prop for attachment button icon
23
+ onSendMessage?: (obj: {
24
+ messageText: string;
25
+ messageHTML: string;
26
+ userSelectListWithIds: { id: number; name: string }[];
27
+ userSelectListName: string[];
28
+ images?: File[];
29
+ imageUrl?: string | null;
30
+ }) => void;
21
31
  suggestionPosition?: 'top' | 'bottom' | 'left' | 'right'; // New prop for tooltip position
32
+ onImageUpload?: (file: File) => Promise<string>; // New prop for image upload
22
33
  }
23
34
 
24
35
  const MentionInput: React.FC<MentionInputProps> = ({
25
36
  users,
26
- placeholder = "Type a message...",
37
+ placeholder = "Type a message... (or drag & drop an image)",
27
38
  containerClassName,
28
39
  inputContainerClassName,
29
40
  inputClassName,
30
41
  sendBtnClassName,
31
42
  suggestionListClassName,
32
43
  suggestionItemClassName,
44
+ imgClassName,
45
+ imgStyle,
33
46
  sendButtonIcon,
47
+ attachmentButtonIcon,
34
48
  onSendMessage,
35
- suggestionPosition = 'bottom', // Default position is bottom
49
+ suggestionPosition = 'bottom',
50
+ onImageUpload,
36
51
  }) => {
37
52
  const [inputValue, setInputValue] = useState<string>(""); // Plain text
38
53
  const [suggestions, setSuggestions] = useState<User[]>([]);
39
54
  const [showSuggestions, setShowSuggestions] = useState<boolean>(false);
55
+ const [selectedImage, setSelectedImage] = useState<File | null>(null);
56
+ const [imageUrl, setImageUrl] = useState<string | null>(null);
57
+ const [isUploading, setIsUploading] = useState<boolean>(false);
58
+ const [isDraggingOver, setIsDraggingOver] = useState<boolean>(false);
40
59
 
41
60
 
42
61
 
@@ -45,6 +64,7 @@ const MentionInput: React.FC<MentionInputProps> = ({
45
64
  const caretOffsetRef = useRef<number>(0);
46
65
  const userSelectListRef = useRef<string[]>([]); // Only unique names
47
66
  const userSelectListWithIdsRef = useRef<{ id: number; name: string }[]>([]); // Unique IDs with names
67
+ const fileInputRef = useRef<HTMLInputElement>(null);
48
68
 
49
69
  const highlightMentionsAndLinks = (text: string): string => {
50
70
  // Regular expression for detecting links
@@ -61,7 +81,7 @@ const MentionInput: React.FC<MentionInputProps> = ({
61
81
  const mentionPattern = new RegExp(`@${userName}(\\s|$)`, "g");
62
82
  highlightedText = highlightedText.replace(
63
83
  mentionPattern,
64
- (match, trailingSpace) => {
84
+ (match) => {
65
85
  return `<span class="mention-highlight">${match.trim()}</span>&nbsp;`;
66
86
  }
67
87
  );
@@ -104,10 +124,11 @@ const MentionInput: React.FC<MentionInputProps> = ({
104
124
 
105
125
  const handleInputChange = () => {
106
126
  if (!inputRef.current) return;
107
-
127
+
128
+ // Store current selection before modifications
108
129
  const selection = window.getSelection();
109
130
  const range = selection?.getRangeAt(0);
110
-
131
+
111
132
  let newCaretOffset = 0;
112
133
  if (range && inputRef.current.contains(range.startContainer)) {
113
134
  const preCaretRange = range.cloneRange();
@@ -115,30 +136,38 @@ const MentionInput: React.FC<MentionInputProps> = ({
115
136
  preCaretRange.setEnd(range.startContainer, range.startOffset);
116
137
  newCaretOffset = preCaretRange.toString().length;
117
138
  }
118
-
139
+
119
140
  caretOffsetRef.current = newCaretOffset;
120
-
141
+
121
142
  const plainText = inputRef.current.innerText;
122
143
  setInputValue(plainText);
123
-
144
+
145
+ // Process for mention suggestions
124
146
  const mentionMatch = plainText.slice(0, newCaretOffset).match(/@(\S*)$/);
125
147
  if (mentionMatch) {
126
148
  const query = mentionMatch[1].toLowerCase();
127
- const filteredUsers = query === "" ? users : users.filter((user) => user.name.toLowerCase().startsWith(query));
128
-
149
+ const filteredUsers = query === "" ? users : users.filter((user) =>
150
+ user.name.toLowerCase().includes(query)
151
+ );
152
+
129
153
  setSuggestions(filteredUsers);
130
154
  setShowSuggestions(filteredUsers.length > 0);
131
155
  } else {
132
156
  setShowSuggestions(false);
133
157
  }
134
-
135
- const previousHTML = inputRef.current.innerHTML;
136
- const htmlWithHighlights = highlightMentionsAndLinks(plainText); // Updated function
137
- if (previousHTML !== htmlWithHighlights) {
138
- inputRef.current.innerHTML = htmlWithHighlights;
158
+
159
+ // Only apply highlighting if we have mentions or links to highlight
160
+ if (userSelectListRef.current.length > 0 || plainText.match(/(https?:\/\/[^\s]+)/g)) {
161
+ const currentHTML = inputRef.current.innerHTML;
162
+ const htmlWithHighlights = highlightMentionsAndLinks(plainText);
163
+
164
+ // Only update if the highlighted HTML is different to avoid cursor jumping
165
+ if (currentHTML !== htmlWithHighlights) {
166
+ inputRef.current.innerHTML = htmlWithHighlights;
167
+ // Restore cursor position after changing innerHTML
168
+ restoreCaretPosition(inputRef.current, newCaretOffset);
169
+ }
139
170
  }
140
-
141
- restoreCaretPosition(inputRef.current, newCaretOffset);
142
171
  };
143
172
 
144
173
 
@@ -148,8 +177,8 @@ const MentionInput: React.FC<MentionInputProps> = ({
148
177
  const getInitials = (name: string) => {
149
178
  const nameParts = name.split(" ");
150
179
  const initials = nameParts
151
- .map((part) => part[0]?.toUpperCase() || "") // Take the first letter of each part
152
- .slice(0, 2) // Limit to 2 letters
180
+ .map((part) => part[0]?.toUpperCase() || "")
181
+ .slice(0, 2)
153
182
  .join("");
154
183
  return initials;
155
184
  };
@@ -183,26 +212,30 @@ const MentionInput: React.FC<MentionInputProps> = ({
183
212
  }
184
213
 
185
214
  return ReactDOM.createPortal(
186
- <ul
187
- className={`suggestion-list ${suggestionListClassName || ''}`}
188
- ref={suggestionListRef}
215
+ <div
216
+ className={`suggestion-container ${suggestionListClassName || ''}`}
189
217
  style={styles}
190
218
  >
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
219
+ <ul
220
+ className="suggestion-list"
221
+ ref={suggestionListRef}
222
+ >
223
+ {suggestions.map((user) => (
224
+ <li
225
+ key={user.id}
226
+ onClick={() => handleSuggestionClick(user)}
227
+ className={`suggestion-item ${suggestionItemClassName || ''}`}
228
+ role="option"
229
+ tabIndex={0}
230
+ aria-selected="false"
231
+ >
232
+ <div className="user-icon">{getInitials(user?.name)}</div>
233
+ <span className="user-name">{user.name}</span>
234
+ </li>
235
+ ))}
236
+ </ul>
237
+ </div>,
238
+ window.document.body
206
239
  );
207
240
  };
208
241
 
@@ -249,18 +282,103 @@ const MentionInput: React.FC<MentionInputProps> = ({
249
282
  restoreCaretPosition(inputRef.current, mentionEnd + 1); // +1 for the space
250
283
  };
251
284
 
285
+ const handleImageSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
286
+ const files = Array.from(event.target.files || []);
287
+ if (files.length > 0) {
288
+ const file = files[0];
289
+ if (file.type.startsWith('image/')) {
290
+ await uploadImage(file);
291
+ }
292
+ }
293
+ };
294
+
295
+ const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
296
+ e.preventDefault();
297
+ e.stopPropagation();
298
+ // Only set dragging if files are being dragged
299
+ if (e.dataTransfer.types.includes('Files')) {
300
+ setIsDraggingOver(true);
301
+ }
302
+ };
303
+
304
+ const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
305
+ e.preventDefault();
306
+ e.stopPropagation();
307
+ // Check if we're leaving the container, not just moving between children
308
+ const rect = e.currentTarget.getBoundingClientRect();
309
+ const x = e.clientX;
310
+ const y = e.clientY;
311
+
312
+ if (
313
+ x <= rect.left ||
314
+ x >= rect.right ||
315
+ y <= rect.top ||
316
+ y >= rect.bottom
317
+ ) {
318
+ setIsDraggingOver(false);
319
+ }
320
+ };
321
+
322
+ const handleDrop = async (e: React.DragEvent<HTMLDivElement>) => {
323
+ e.preventDefault();
324
+ e.stopPropagation();
325
+ setIsDraggingOver(false);
326
+ const files = Array.from(e.dataTransfer.files);
327
+ if (files.length > 0) {
328
+ const imageFiles = files.filter(file => file.type.startsWith('image/'));
329
+ if (imageFiles.length > 0) {
330
+ await uploadImage(imageFiles[0]);
331
+ }
332
+ }
333
+ };
334
+
335
+ const uploadImage = async (file: File) => {
336
+ if (!onImageUpload) {
337
+ // If no upload function provided, store the file directly
338
+ setSelectedImage(file);
339
+ setImageUrl(URL.createObjectURL(file));
340
+ return;
341
+ }
342
+
343
+ try {
344
+ setIsUploading(true);
345
+ const url = await onImageUpload(file);
346
+ setSelectedImage(file);
347
+ setImageUrl(url);
348
+ } catch (error) {
349
+ console.error('Error uploading image:', error);
350
+ // You might want to show an error message to the user
351
+ } finally {
352
+ setIsUploading(false);
353
+ }
354
+ };
355
+
356
+ const removeImage = () => {
357
+ setSelectedImage(null);
358
+ setImageUrl(null);
359
+ };
360
+
252
361
  const handleSendMessage = () => {
253
362
  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 = []
363
+ const messageText = inputRef.current.innerText.trim();
364
+ const messageHTML = inputRef.current.innerHTML.trim();
365
+
366
+ if ((messageText || selectedImage) && onSendMessage) {
367
+ onSendMessage({
368
+ messageText,
369
+ messageHTML,
370
+ userSelectListWithIds: userSelectListWithIdsRef.current,
371
+ userSelectListName: userSelectListRef.current,
372
+ images: selectedImage ? [selectedImage] : [],
373
+ imageUrl: imageUrl
374
+ });
375
+ setInputValue("");
376
+ setShowSuggestions(false);
377
+ inputRef.current.innerText = "";
378
+ setSelectedImage(null);
379
+ setImageUrl(null);
380
+ userSelectListRef.current = [];
381
+ userSelectListWithIdsRef.current = [];
264
382
  }
265
383
  }
266
384
  };
@@ -272,28 +390,84 @@ const MentionInput: React.FC<MentionInputProps> = ({
272
390
  }
273
391
  };
274
392
 
393
+ console.log(inputValue, inputRef.current?.innerText.trim(), "inputValue====")
275
394
 
276
395
  return (
277
396
  <div className={`mention-container ${containerClassName || ""}`}>
278
- <div className={`mention-input-container ${inputContainerClassName || ""}`}>
279
- {(!inputValue || !inputRef.current || inputRef.current?.innerText.trim() === "") ? (
280
- <span className="placeholder">{placeholder}</span>
281
- ) : null}
282
- <div
283
- ref={inputRef}
284
- contentEditable
285
- suppressContentEditableWarning
286
- className={`mention-input ${inputClassName || ""}`}
287
- onInput={handleInputChange}
288
- onKeyDown={handleKeyDown} // Add keydown listener
397
+ {imageUrl && selectedImage && (
398
+ <div className="image-preview-card">
399
+ <img
400
+ src={imageUrl}
401
+ alt="Preview"
402
+ className={imgClassName || ""}
403
+ style={imgStyle}
404
+ />
405
+ <button onClick={removeImage} className="remove-image-btn" aria-label="Remove image">
406
+ ×
407
+ </button>
408
+ </div>
409
+ )}
410
+
411
+ <div
412
+ className={`mention-input-container ${inputContainerClassName || ""} ${isDraggingOver ? 'dragging-over' : ''}`}
413
+ onDragOver={handleDragOver}
414
+ onDragLeave={handleDragLeave}
415
+ onDragEnd={() => setIsDraggingOver(false)}
416
+ onDrop={handleDrop}
417
+ >
418
+ {isDraggingOver && (
419
+ <div className="drag-overlay">
420
+ <div className="drag-message">
421
+ <span>Drop to upload</span>
422
+ </div>
423
+ </div>
424
+ )}
425
+
426
+ <button
427
+ onClick={() => fileInputRef.current?.click()}
428
+ className="attachment-button"
429
+ type="button"
430
+ aria-label="Attach image"
289
431
  >
432
+ <span className="attachment-icon">{attachmentButtonIcon || "📷"}</span>
433
+ </button>
434
+
435
+ <div className="mention-input-wrapper">
436
+ {(!inputValue || !inputRef.current || inputRef.current?.innerText.trim() === "") && (
437
+ <span className="placeholder">{placeholder}</span>
438
+ )}
439
+ <div
440
+ ref={inputRef}
441
+ contentEditable
442
+ suppressContentEditableWarning
443
+ className={`mention-input ${inputClassName || ""}`}
444
+ onInput={handleInputChange}
445
+ onKeyDown={handleKeyDown}
446
+ onFocus={() => document.execCommand('styleWithCSS', false, 'false')}
447
+ />
290
448
  </div>
449
+
291
450
  <button
292
451
  onClick={handleSendMessage}
293
452
  className={`send-button ${sendBtnClassName || ""}`}
453
+ aria-label="Send message"
294
454
  >
295
455
  {sendButtonIcon || "➤"}
296
456
  </button>
457
+
458
+ <input
459
+ type="file"
460
+ ref={fileInputRef}
461
+ accept="image/*"
462
+ onChange={handleImageSelect}
463
+ style={{ display: 'none' }}
464
+ />
465
+
466
+ {isUploading && (
467
+ <div className="upload-loading">
468
+ <span>Uploading...</span>
469
+ </div>
470
+ )}
297
471
  </div>
298
472
  {renderSuggestions()}
299
473
  </div>