react-mention-input 1.1.30 → 1.1.31
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/CHANGELOG.md +23 -0
- package/dist/MentionInput.js +28 -1
- package/package.json +2 -2
- package/src/MentionInput.tsx +72 -37
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.1.31] - 2026-02-02
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Font restriction enforcement**: Added paste event handler to strip all formatting (color, font-size, font-weight, font-family) from pasted content
|
|
12
|
+
- Pasted text now uses the component's standard CSS styling instead of preserving source formatting
|
|
13
|
+
- Prevents visual inconsistencies when users paste content from Word, websites, emails, or other formatted sources
|
|
14
|
+
|
|
15
|
+
### Technical Details
|
|
16
|
+
- Implemented `handlePaste` function using standard Clipboard API (`clipboardData.getData('text/plain')`)
|
|
17
|
+
- Preserves newlines, special characters, and cursor position
|
|
18
|
+
- Maintains full backward compatibility with existing features (mentions, links, images, keyboard shortcuts)
|
|
19
|
+
- Uses W3C standard DOM APIs (Selection and Range)
|
|
20
|
+
|
|
21
|
+
## [1.1.30] - Previous Release
|
|
22
|
+
|
|
23
|
+
(Previous versions not documented)
|
package/dist/MentionInput.js
CHANGED
|
@@ -444,6 +444,33 @@ var MentionInput = function (_a) {
|
|
|
444
444
|
}
|
|
445
445
|
}
|
|
446
446
|
};
|
|
447
|
+
var handlePaste = function (event) {
|
|
448
|
+
// Prevent default paste behavior to control formatting
|
|
449
|
+
event.preventDefault();
|
|
450
|
+
// Get plain text from clipboard (strips all HTML/formatting)
|
|
451
|
+
var text = event.clipboardData.getData('text/plain');
|
|
452
|
+
// If no text, do nothing
|
|
453
|
+
if (!text)
|
|
454
|
+
return;
|
|
455
|
+
// Get current selection
|
|
456
|
+
var selection = window.getSelection();
|
|
457
|
+
if (!selection || !selection.rangeCount)
|
|
458
|
+
return;
|
|
459
|
+
// Get the range where text will be inserted
|
|
460
|
+
var range = selection.getRangeAt(0);
|
|
461
|
+
// Delete any selected content first
|
|
462
|
+
range.deleteContents();
|
|
463
|
+
// Create a text node with the plain text (preserves newlines)
|
|
464
|
+
var textNode = document.createTextNode(text);
|
|
465
|
+
range.insertNode(textNode);
|
|
466
|
+
// Move cursor to end of inserted text
|
|
467
|
+
range.setStartAfter(textNode);
|
|
468
|
+
range.setEndAfter(textNode);
|
|
469
|
+
selection.removeAllRanges();
|
|
470
|
+
selection.addRange(range);
|
|
471
|
+
// Trigger input change to update state and apply mention/link highlighting
|
|
472
|
+
handleInputChange();
|
|
473
|
+
};
|
|
447
474
|
var handleKeyDown = function (event) {
|
|
448
475
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
449
476
|
event.preventDefault(); // Prevent newline in content-editable
|
|
@@ -475,7 +502,7 @@ var MentionInput = function (_a) {
|
|
|
475
502
|
React.createElement("span", { className: "attachment-icon" }, attachmentButtonIcon || "📷")),
|
|
476
503
|
React.createElement("div", { className: "mention-input-wrapper" },
|
|
477
504
|
(!inputValue || !inputRef.current || ((_b = inputRef.current) === null || _b === void 0 ? void 0 : _b.innerText.trim()) === "") && (React.createElement("span", { className: "placeholder" }, placeholder)),
|
|
478
|
-
React.createElement("div", { ref: inputRef, contentEditable: true, suppressContentEditableWarning: true, className: "mention-input ".concat(inputClassName || ""), onInput: handleInputChange, onKeyDown: handleKeyDown, onFocus: function () { return document.execCommand('styleWithCSS', false, 'false'); } })),
|
|
505
|
+
React.createElement("div", { ref: inputRef, contentEditable: true, suppressContentEditableWarning: true, className: "mention-input ".concat(inputClassName || ""), onInput: handleInputChange, onKeyDown: handleKeyDown, onPaste: handlePaste, onFocus: function () { return document.execCommand('styleWithCSS', false, 'false'); } })),
|
|
479
506
|
React.createElement("button", { onClick: handleSendMessage, className: "send-button ".concat(sendBtnClassName || ""), "aria-label": "Send message" }, sendButtonIcon || "➤"),
|
|
480
507
|
React.createElement("input", { type: "file", ref: fileInputRef, accept: "image/*", onChange: handleImageSelect, style: { display: 'none' } }),
|
|
481
508
|
isUploading && (React.createElement("div", { className: "upload-loading" },
|
package/package.json
CHANGED
package/src/MentionInput.tsx
CHANGED
|
@@ -65,14 +65,14 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
65
65
|
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
|
66
66
|
const [isUploading, setIsUploading] = useState<boolean>(false);
|
|
67
67
|
const [isDraggingOver, setIsDraggingOver] = useState<boolean>(false);
|
|
68
|
-
|
|
68
|
+
|
|
69
69
|
// Use protected image hook to handle authenticated image URLs
|
|
70
70
|
const displayImageUrl = useProtectedImage({
|
|
71
71
|
url: imageUrl,
|
|
72
72
|
isProtected: isProtectedUrl,
|
|
73
73
|
getAuthHeaders,
|
|
74
74
|
});
|
|
75
|
-
|
|
75
|
+
|
|
76
76
|
|
|
77
77
|
|
|
78
78
|
const inputRef = useRef<HTMLDivElement>(null);
|
|
@@ -85,13 +85,13 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
85
85
|
const highlightMentionsAndLinks = (text: string): string => {
|
|
86
86
|
// Regular expression for detecting links
|
|
87
87
|
const linkRegex = /(https?:\/\/[^\s]+)/g;
|
|
88
|
-
|
|
88
|
+
|
|
89
89
|
// Highlight links
|
|
90
90
|
let highlightedText = text.replace(
|
|
91
91
|
linkRegex,
|
|
92
92
|
'<a href="$1" target="_blank" rel="noopener noreferrer" class="link-highlight">$1</a>'
|
|
93
93
|
);
|
|
94
|
-
|
|
94
|
+
|
|
95
95
|
// Highlight mentions manually based on `userSelectListRef`
|
|
96
96
|
userSelectListRef?.current.forEach((userName) => {
|
|
97
97
|
const mentionPattern = new RegExp(`@${userName}(\\s|$)`, "g");
|
|
@@ -102,7 +102,7 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
102
102
|
}
|
|
103
103
|
);
|
|
104
104
|
});
|
|
105
|
-
|
|
105
|
+
|
|
106
106
|
return highlightedText;
|
|
107
107
|
};
|
|
108
108
|
|
|
@@ -128,7 +128,7 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
128
128
|
document.removeEventListener("mousedown", handleClickOutside);
|
|
129
129
|
};
|
|
130
130
|
}, [showSuggestions]);
|
|
131
|
-
|
|
131
|
+
|
|
132
132
|
|
|
133
133
|
const restoreCaretPosition = (node: HTMLElement, caretOffset: number) => {
|
|
134
134
|
const range = document.createRange();
|
|
@@ -268,29 +268,29 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
268
268
|
const newCaretOffset = getCurrentCaretOffset();
|
|
269
269
|
|
|
270
270
|
caretOffsetRef.current = newCaretOffset;
|
|
271
|
-
|
|
271
|
+
|
|
272
272
|
const plainText = inputRef.current.innerText;
|
|
273
273
|
setInputValue(plainText);
|
|
274
|
-
|
|
274
|
+
|
|
275
275
|
// Process for mention suggestions
|
|
276
276
|
const mentionMatch = plainText.slice(0, newCaretOffset).match(/@(\S*)$/);
|
|
277
277
|
if (mentionMatch) {
|
|
278
278
|
const query = mentionMatch[1].toLowerCase();
|
|
279
|
-
const filteredUsers = query === "" ? users : users.filter((user) =>
|
|
279
|
+
const filteredUsers = query === "" ? users : users.filter((user) =>
|
|
280
280
|
user.name.toLowerCase().includes(query)
|
|
281
281
|
);
|
|
282
|
-
|
|
282
|
+
|
|
283
283
|
setSuggestions(filteredUsers);
|
|
284
284
|
setShowSuggestions(filteredUsers.length > 0);
|
|
285
285
|
} else {
|
|
286
286
|
setShowSuggestions(false);
|
|
287
287
|
}
|
|
288
|
-
|
|
288
|
+
|
|
289
289
|
// Only apply highlighting if we have mentions or links to highlight
|
|
290
290
|
if (userSelectListRef.current.length > 0 || plainText.match(/(https?:\/\/[^\s]+)/g)) {
|
|
291
291
|
const currentHTML = inputRef.current.innerHTML;
|
|
292
292
|
const htmlWithHighlights = highlightMentionsAndLinks(plainText);
|
|
293
|
-
|
|
293
|
+
|
|
294
294
|
// Only update if the highlighted HTML is different to avoid cursor jumping
|
|
295
295
|
if (currentHTML !== htmlWithHighlights) {
|
|
296
296
|
inputRef.current.innerHTML = htmlWithHighlights;
|
|
@@ -299,7 +299,7 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
299
299
|
}
|
|
300
300
|
}
|
|
301
301
|
};
|
|
302
|
-
|
|
302
|
+
|
|
303
303
|
|
|
304
304
|
const renderSuggestions = () => {
|
|
305
305
|
if (!showSuggestions || !inputRef.current) return null;
|
|
@@ -312,7 +312,7 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
312
312
|
.join("");
|
|
313
313
|
return initials;
|
|
314
314
|
};
|
|
315
|
-
|
|
315
|
+
|
|
316
316
|
const inputRect = inputRef.current.getBoundingClientRect();
|
|
317
317
|
const scrollLeft =
|
|
318
318
|
window.scrollX ?? window.pageXOffset ?? document.documentElement.scrollLeft ?? 0;
|
|
@@ -349,7 +349,7 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
349
349
|
}
|
|
350
350
|
|
|
351
351
|
return ReactDOM.createPortal(
|
|
352
|
-
<div
|
|
352
|
+
<div
|
|
353
353
|
className={`suggestion-container ${suggestionListClassName || ''}`}
|
|
354
354
|
style={styles}
|
|
355
355
|
>
|
|
@@ -378,15 +378,15 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
378
378
|
|
|
379
379
|
const handleSuggestionClick = (user: User) => {
|
|
380
380
|
if (!inputRef.current) return;
|
|
381
|
-
|
|
381
|
+
|
|
382
382
|
const plainText = inputValue;
|
|
383
383
|
const caretOffset = caretOffsetRef.current;
|
|
384
384
|
const mentionMatch = plainText.slice(0, caretOffset).match(/@(\S*)$/);
|
|
385
|
-
|
|
385
|
+
|
|
386
386
|
if (!userSelectListRef.current.includes(user.name)) {
|
|
387
387
|
userSelectListRef.current.push(user.name);
|
|
388
388
|
}
|
|
389
|
-
|
|
389
|
+
|
|
390
390
|
// Check if the ID is already stored
|
|
391
391
|
const isIdExists = userSelectListWithIdsRef.current.some(
|
|
392
392
|
(item) => item.id === user.id
|
|
@@ -394,26 +394,26 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
394
394
|
if (!isIdExists) {
|
|
395
395
|
userSelectListWithIdsRef.current.push(user);
|
|
396
396
|
}
|
|
397
|
-
|
|
397
|
+
|
|
398
398
|
if (!mentionMatch) return;
|
|
399
|
-
|
|
399
|
+
|
|
400
400
|
const mentionIndex = plainText.slice(0, caretOffset).lastIndexOf("@");
|
|
401
|
-
|
|
401
|
+
|
|
402
402
|
// Append space after the mention
|
|
403
403
|
const newValue =
|
|
404
404
|
plainText.substring(0, mentionIndex + 1) + user.name + " " + plainText.substring(caretOffset);
|
|
405
|
-
|
|
405
|
+
|
|
406
406
|
setInputValue(newValue);
|
|
407
407
|
inputRef.current.innerText = newValue;
|
|
408
|
-
|
|
408
|
+
|
|
409
409
|
// Highlight mentions and links with
|
|
410
410
|
const htmlWithHighlights = highlightMentionsAndLinks(newValue);
|
|
411
|
-
|
|
411
|
+
|
|
412
412
|
// Set highlighted content
|
|
413
413
|
inputRef.current.innerHTML = htmlWithHighlights;
|
|
414
|
-
|
|
414
|
+
|
|
415
415
|
setShowSuggestions(false);
|
|
416
|
-
|
|
416
|
+
|
|
417
417
|
// Adjust caret position after adding the mention and space
|
|
418
418
|
const mentionEnd = mentionIndex + user.name.length + 1;
|
|
419
419
|
restoreCaretPosition(inputRef.current, mentionEnd + 1); // +1 for the space
|
|
@@ -449,7 +449,7 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
449
449
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
450
450
|
const x = e.clientX;
|
|
451
451
|
const y = e.clientY;
|
|
452
|
-
|
|
452
|
+
|
|
453
453
|
if (
|
|
454
454
|
x <= rect.left ||
|
|
455
455
|
x >= rect.right ||
|
|
@@ -532,6 +532,40 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
532
532
|
}
|
|
533
533
|
};
|
|
534
534
|
|
|
535
|
+
const handlePaste = (event: React.ClipboardEvent<HTMLDivElement>) => {
|
|
536
|
+
// Prevent default paste behavior to control formatting
|
|
537
|
+
event.preventDefault();
|
|
538
|
+
|
|
539
|
+
// Get plain text from clipboard (strips all HTML/formatting)
|
|
540
|
+
const text = event.clipboardData.getData('text/plain');
|
|
541
|
+
|
|
542
|
+
// If no text, do nothing
|
|
543
|
+
if (!text) return;
|
|
544
|
+
|
|
545
|
+
// Get current selection
|
|
546
|
+
const selection = window.getSelection();
|
|
547
|
+
if (!selection || !selection.rangeCount) return;
|
|
548
|
+
|
|
549
|
+
// Get the range where text will be inserted
|
|
550
|
+
const range = selection.getRangeAt(0);
|
|
551
|
+
|
|
552
|
+
// Delete any selected content first
|
|
553
|
+
range.deleteContents();
|
|
554
|
+
|
|
555
|
+
// Create a text node with the plain text (preserves newlines)
|
|
556
|
+
const textNode = document.createTextNode(text);
|
|
557
|
+
range.insertNode(textNode);
|
|
558
|
+
|
|
559
|
+
// Move cursor to end of inserted text
|
|
560
|
+
range.setStartAfter(textNode);
|
|
561
|
+
range.setEndAfter(textNode);
|
|
562
|
+
selection.removeAllRanges();
|
|
563
|
+
selection.addRange(range);
|
|
564
|
+
|
|
565
|
+
// Trigger input change to update state and apply mention/link highlighting
|
|
566
|
+
handleInputChange();
|
|
567
|
+
};
|
|
568
|
+
|
|
535
569
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
536
570
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
537
571
|
event.preventDefault(); // Prevent newline in content-editable
|
|
@@ -558,9 +592,9 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
558
592
|
<div className={`mention-container ${containerClassName || ""}`}>
|
|
559
593
|
{displayImageUrl && selectedImage && (
|
|
560
594
|
<div className={`image-preview-card ${attachedImageContainerClassName || ""}`} style={attachedImageContainerStyle}>
|
|
561
|
-
<img
|
|
562
|
-
src={displayImageUrl}
|
|
563
|
-
alt="Preview"
|
|
595
|
+
<img
|
|
596
|
+
src={displayImageUrl}
|
|
597
|
+
alt="Preview"
|
|
564
598
|
className={imgClassName || ""}
|
|
565
599
|
style={imgStyle}
|
|
566
600
|
/>
|
|
@@ -569,8 +603,8 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
569
603
|
</button>
|
|
570
604
|
</div>
|
|
571
605
|
)}
|
|
572
|
-
|
|
573
|
-
<div
|
|
606
|
+
|
|
607
|
+
<div
|
|
574
608
|
className={`mention-input-container ${inputContainerClassName || ""} ${isDraggingOver ? 'dragging-over' : ''}`}
|
|
575
609
|
onDragOver={handleDragOver}
|
|
576
610
|
onDragLeave={handleDragLeave}
|
|
@@ -584,7 +618,7 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
584
618
|
</div>
|
|
585
619
|
</div>
|
|
586
620
|
)}
|
|
587
|
-
|
|
621
|
+
|
|
588
622
|
<button
|
|
589
623
|
onClick={() => fileInputRef.current?.click()}
|
|
590
624
|
className="attachment-button"
|
|
@@ -593,7 +627,7 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
593
627
|
>
|
|
594
628
|
<span className="attachment-icon">{attachmentButtonIcon || "📷"}</span>
|
|
595
629
|
</button>
|
|
596
|
-
|
|
630
|
+
|
|
597
631
|
<div className="mention-input-wrapper">
|
|
598
632
|
{(!inputValue || !inputRef.current || inputRef.current?.innerText.trim() === "") && (
|
|
599
633
|
<span className="placeholder">{placeholder}</span>
|
|
@@ -605,10 +639,11 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
605
639
|
className={`mention-input ${inputClassName || ""}`}
|
|
606
640
|
onInput={handleInputChange}
|
|
607
641
|
onKeyDown={handleKeyDown}
|
|
642
|
+
onPaste={handlePaste}
|
|
608
643
|
onFocus={() => document.execCommand('styleWithCSS', false, 'false')}
|
|
609
644
|
/>
|
|
610
645
|
</div>
|
|
611
|
-
|
|
646
|
+
|
|
612
647
|
<button
|
|
613
648
|
onClick={handleSendMessage}
|
|
614
649
|
className={`send-button ${sendBtnClassName || ""}`}
|
|
@@ -616,7 +651,7 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
616
651
|
>
|
|
617
652
|
{sendButtonIcon || "➤"}
|
|
618
653
|
</button>
|
|
619
|
-
|
|
654
|
+
|
|
620
655
|
<input
|
|
621
656
|
type="file"
|
|
622
657
|
ref={fileInputRef}
|
|
@@ -624,7 +659,7 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
624
659
|
onChange={handleImageSelect}
|
|
625
660
|
style={{ display: 'none' }}
|
|
626
661
|
/>
|
|
627
|
-
|
|
662
|
+
|
|
628
663
|
{isUploading && (
|
|
629
664
|
<div className="upload-loading">
|
|
630
665
|
<span>Uploading...</span>
|