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 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)
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-mention-input",
3
- "version": "1.1.30",
3
+ "version": "1.1.31",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "scripts": {
@@ -42,4 +42,4 @@
42
42
  "react": "^18.3.1",
43
43
  "react-dom": "^18.3.1"
44
44
  }
45
- }
45
+ }
@@ -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 &nbsp;
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>