react-mention-input 1.1.24 → 1.1.25

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.
@@ -27,7 +27,6 @@ interface MentionInputProps {
27
27
  name: string;
28
28
  }[];
29
29
  userSelectListName: string[];
30
- tags: string[];
31
30
  images?: File[];
32
31
  imageUrl?: string | null;
33
32
  }) => void;
@@ -34,7 +34,16 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
34
34
  if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
35
35
  }
36
36
  };
37
- import React, { useState, useRef } from "react";
37
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
38
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
39
+ if (ar || !(i in from)) {
40
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
41
+ ar[i] = from[i];
42
+ }
43
+ }
44
+ return to.concat(ar || Array.prototype.slice.call(from));
45
+ };
46
+ import React, { useState, useRef, useEffect } from "react";
38
47
  import ReactDOM from "react-dom";
39
48
  import "./MentionInput.css";
40
49
  var MentionInput = function (_a) {
@@ -52,19 +61,12 @@ var MentionInput = function (_a) {
52
61
  var caretOffsetRef = useRef(0);
53
62
  var userSelectListRef = useRef([]); // Only unique names
54
63
  var userSelectListWithIdsRef = useRef([]); // Unique IDs with names
55
- var tagsListRef = useRef([]); // Store hashtags
56
64
  var fileInputRef = useRef(null);
57
65
  var highlightMentionsAndLinks = function (text) {
58
66
  // Regular expression for detecting links
59
67
  var linkRegex = /(https?:\/\/[^\s]+)/g;
60
- // Regular expression for detecting hashtags
61
- var hashtagRegex = /#[\w]+/g;
62
68
  // Highlight links
63
69
  var highlightedText = text.replace(linkRegex, '<a href="$1" target="_blank" rel="noopener noreferrer" class="link-highlight">$1</a>');
64
- // Highlight hashtags
65
- highlightedText = highlightedText.replace(hashtagRegex, function (match) {
66
- return "<span class=\"hashtag-highlight\">".concat(match, "</span>");
67
- });
68
70
  // Highlight mentions manually based on `userSelectListRef`
69
71
  userSelectListRef === null || userSelectListRef === void 0 ? void 0 : userSelectListRef.current.forEach(function (userName) {
70
72
  var mentionPattern = new RegExp("@".concat(userName, "(\\s|$)"), "g");
@@ -74,6 +76,24 @@ var MentionInput = function (_a) {
74
76
  });
75
77
  return highlightedText;
76
78
  };
79
+ useEffect(function () {
80
+ var handleClickOutside = function (event) {
81
+ var target = event.target;
82
+ if (showSuggestions &&
83
+ inputRef.current &&
84
+ !inputRef.current.contains(target) &&
85
+ suggestionListRef.current &&
86
+ !suggestionListRef.current.contains(target)) {
87
+ setShowSuggestions(false);
88
+ }
89
+ };
90
+ if (showSuggestions) {
91
+ document.addEventListener("mousedown", handleClickOutside);
92
+ }
93
+ return function () {
94
+ document.removeEventListener("mousedown", handleClickOutside);
95
+ };
96
+ }, [showSuggestions]);
77
97
  var restoreCaretPosition = function (node, caretOffset) {
78
98
  var range = document.createRange();
79
99
  var sel = window.getSelection();
@@ -106,19 +126,80 @@ var MentionInput = function (_a) {
106
126
  sel.addRange(range);
107
127
  }
108
128
  };
109
- var handleInputChange = function () {
129
+ var getCurrentCaretOffset = function () {
110
130
  if (!inputRef.current)
111
- return;
112
- // Store current selection before modifications
131
+ return 0;
113
132
  var selection = window.getSelection();
114
- var range = selection === null || selection === void 0 ? void 0 : selection.getRangeAt(0);
115
- var newCaretOffset = 0;
133
+ var range = selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
116
134
  if (range && inputRef.current.contains(range.startContainer)) {
117
135
  var preCaretRange = range.cloneRange();
118
136
  preCaretRange.selectNodeContents(inputRef.current);
119
137
  preCaretRange.setEnd(range.startContainer, range.startOffset);
120
- newCaretOffset = preCaretRange.toString().length;
138
+ return preCaretRange.toString().length;
139
+ }
140
+ return caretOffsetRef.current;
141
+ };
142
+ var findMentionAtOffset = function (plainText, caretOffset, direction) {
143
+ if (!userSelectListRef.current.length)
144
+ return null;
145
+ var names = __spreadArray([], userSelectListRef.current, true).sort(function (a, b) { return b.length - a.length; });
146
+ for (var _i = 0, names_1 = names; _i < names_1.length; _i++) {
147
+ var name_1 = names_1[_i];
148
+ var pattern = "@".concat(name_1);
149
+ var searchIndex = plainText.indexOf(pattern);
150
+ while (searchIndex !== -1) {
151
+ var endIndex = searchIndex + pattern.length;
152
+ if (plainText[endIndex] === " " ||
153
+ plainText[endIndex] === "\u00a0") {
154
+ endIndex += 1;
155
+ }
156
+ var isCaretInsideMention = direction === "backward"
157
+ ? caretOffset > searchIndex && caretOffset <= endIndex
158
+ : caretOffset >= searchIndex && caretOffset < endIndex;
159
+ if (isCaretInsideMention) {
160
+ return { name: name_1, start: searchIndex, end: endIndex };
161
+ }
162
+ searchIndex = plainText.indexOf(pattern, searchIndex + 1);
163
+ }
164
+ }
165
+ return null;
166
+ };
167
+ var removeMentionToken = function (direction) {
168
+ if (!inputRef.current)
169
+ return false;
170
+ var plainText = inputRef.current.innerText;
171
+ var caretOffset = getCurrentCaretOffset();
172
+ var mentionInfo = findMentionAtOffset(plainText, caretOffset, direction);
173
+ if (!mentionInfo)
174
+ return false;
175
+ var newText = plainText.slice(0, mentionInfo.start) + plainText.slice(mentionInfo.end);
176
+ var hasLinks = !!newText.match(/(https?:\/\/[^\s]+)/g);
177
+ if (userSelectListRef.current.length > 0 || hasLinks) {
178
+ var htmlWithHighlights = highlightMentionsAndLinks(newText);
179
+ inputRef.current.innerHTML = htmlWithHighlights;
180
+ }
181
+ else {
182
+ inputRef.current.innerText = newText;
121
183
  }
184
+ setInputValue(newText);
185
+ setShowSuggestions(false);
186
+ var newCaretOffset = mentionInfo.start;
187
+ caretOffsetRef.current = newCaretOffset;
188
+ if (inputRef.current) {
189
+ restoreCaretPosition(inputRef.current, newCaretOffset);
190
+ }
191
+ if (!newText.includes("@".concat(mentionInfo.name))) {
192
+ userSelectListRef.current = userSelectListRef.current.filter(function (storedName) { return storedName !== mentionInfo.name; });
193
+ userSelectListWithIdsRef.current =
194
+ userSelectListWithIdsRef.current.filter(function (user) { return user.name !== mentionInfo.name; });
195
+ }
196
+ return true;
197
+ };
198
+ var handleInputChange = function () {
199
+ if (!inputRef.current)
200
+ return;
201
+ // Store current selection before modifications
202
+ var newCaretOffset = getCurrentCaretOffset();
122
203
  caretOffsetRef.current = newCaretOffset;
123
204
  var plainText = inputRef.current.innerText;
124
205
  setInputValue(plainText);
@@ -135,17 +216,8 @@ var MentionInput = function (_a) {
135
216
  else {
136
217
  setShowSuggestions(false);
137
218
  }
138
- // Extract and store hashtags
139
- var hashtagMatches = plainText.match(/#[\w]+/g);
140
- if (hashtagMatches) {
141
- var uniqueTags = Array.from(new Set(hashtagMatches));
142
- tagsListRef.current = uniqueTags;
143
- }
144
- else {
145
- tagsListRef.current = [];
146
- }
147
- // Only apply highlighting if we have mentions, hashtags, or links to highlight
148
- if (userSelectListRef.current.length > 0 || plainText.match(/(https?:\/\/[^\s]+)/g) || plainText.match(/#[\w]+/g)) {
219
+ // Only apply highlighting if we have mentions or links to highlight
220
+ if (userSelectListRef.current.length > 0 || plainText.match(/(https?:\/\/[^\s]+)/g)) {
149
221
  var currentHTML = inputRef.current.innerHTML;
150
222
  var htmlWithHighlights = highlightMentionsAndLinks(plainText);
151
223
  // Only update if the highlighted HTML is different to avoid cursor jumping
@@ -333,7 +405,6 @@ var MentionInput = function (_a) {
333
405
  messageHTML: messageHTML,
334
406
  userSelectListWithIds: userSelectListWithIdsRef.current,
335
407
  userSelectListName: userSelectListRef.current,
336
- tags: tagsListRef.current,
337
408
  images: selectedImage ? [selectedImage] : [],
338
409
  imageUrl: imageUrl
339
410
  });
@@ -344,7 +415,6 @@ var MentionInput = function (_a) {
344
415
  setImageUrl(null);
345
416
  userSelectListRef.current = [];
346
417
  userSelectListWithIdsRef.current = [];
347
- tagsListRef.current = [];
348
418
  }
349
419
  }
350
420
  };
@@ -352,6 +422,19 @@ var MentionInput = function (_a) {
352
422
  if (event.key === "Enter" && !event.shiftKey) {
353
423
  event.preventDefault(); // Prevent newline in content-editable
354
424
  handleSendMessage(); // Trigger the same function as the Send button
425
+ return;
426
+ }
427
+ if (event.key === "Backspace") {
428
+ var removed = removeMentionToken("backward");
429
+ if (removed) {
430
+ event.preventDefault();
431
+ }
432
+ }
433
+ if (event.key === "Delete") {
434
+ var removed = removeMentionToken("forward");
435
+ if (removed) {
436
+ event.preventDefault();
437
+ }
355
438
  }
356
439
  };
357
440
  return (React.createElement("div", { className: "mention-container ".concat(containerClassName || "") },
@@ -367,7 +450,7 @@ var MentionInput = function (_a) {
367
450
  React.createElement("div", { className: "mention-input-wrapper" },
368
451
  (!inputValue || !inputRef.current || ((_b = inputRef.current) === null || _b === void 0 ? void 0 : _b.innerText.trim()) === "") && (React.createElement("span", { className: "placeholder" }, placeholder)),
369
452
  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'); } })),
370
- React.createElement("button", { onClick: handleSendMessage, className: "send-button ".concat(sendBtnClassName || ""), "aria-label": "Send message" }, sendButtonIcon || ""),
453
+ React.createElement("button", { onClick: handleSendMessage, className: "send-button ".concat(sendBtnClassName || ""), "aria-label": "Send message" }, sendButtonIcon || ""),
371
454
  React.createElement("input", { type: "file", ref: fileInputRef, accept: "image/*", onChange: handleImageSelect, style: { display: 'none' } }),
372
455
  isUploading && (React.createElement("div", { className: "upload-loading" },
373
456
  React.createElement("span", null, "Uploading...")))),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-mention-input",
3
- "version": "1.1.24",
3
+ "version": "1.1.25",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "scripts": {
@@ -1,4 +1,4 @@
1
- import React, { useState, useRef, ReactNode } from "react";
1
+ import React, { useState, useRef, ReactNode, useEffect } from "react";
2
2
  import ReactDOM from "react-dom";
3
3
  import "./MentionInput.css";
4
4
 
@@ -27,7 +27,6 @@ interface MentionInputProps {
27
27
  messageHTML: string;
28
28
  userSelectListWithIds: { id: number; name: string }[];
29
29
  userSelectListName: string[];
30
- tags: string[];
31
30
  images?: File[];
32
31
  imageUrl?: string | null;
33
32
  }) => void;
@@ -69,29 +68,17 @@ const MentionInput: React.FC<MentionInputProps> = ({
69
68
  const caretOffsetRef = useRef<number>(0);
70
69
  const userSelectListRef = useRef<string[]>([]); // Only unique names
71
70
  const userSelectListWithIdsRef = useRef<{ id: number; name: string }[]>([]); // Unique IDs with names
72
- const tagsListRef = useRef<string[]>([]); // Store hashtags
73
71
  const fileInputRef = useRef<HTMLInputElement>(null);
74
72
 
75
73
  const highlightMentionsAndLinks = (text: string): string => {
76
74
  // Regular expression for detecting links
77
75
  const linkRegex = /(https?:\/\/[^\s]+)/g;
78
-
79
- // Regular expression for detecting hashtags
80
- const hashtagRegex = /#[\w]+/g;
81
76
 
82
77
  // Highlight links
83
78
  let highlightedText = text.replace(
84
79
  linkRegex,
85
80
  '<a href="$1" target="_blank" rel="noopener noreferrer" class="link-highlight">$1</a>'
86
81
  );
87
-
88
- // Highlight hashtags
89
- highlightedText = highlightedText.replace(
90
- hashtagRegex,
91
- (match) => {
92
- return `<span class="hashtag-highlight">${match}</span>`;
93
- }
94
- );
95
82
 
96
83
  // Highlight mentions manually based on `userSelectListRef`
97
84
  userSelectListRef?.current.forEach((userName) => {
@@ -106,6 +93,29 @@ const MentionInput: React.FC<MentionInputProps> = ({
106
93
 
107
94
  return highlightedText;
108
95
  };
96
+
97
+ useEffect(() => {
98
+ const handleClickOutside = (event: MouseEvent) => {
99
+ const target = event.target as Node;
100
+ if (
101
+ showSuggestions &&
102
+ inputRef.current &&
103
+ !inputRef.current.contains(target) &&
104
+ suggestionListRef.current &&
105
+ !suggestionListRef.current.contains(target)
106
+ ) {
107
+ setShowSuggestions(false);
108
+ }
109
+ };
110
+
111
+ if (showSuggestions) {
112
+ document.addEventListener("mousedown", handleClickOutside);
113
+ }
114
+
115
+ return () => {
116
+ document.removeEventListener("mousedown", handleClickOutside);
117
+ };
118
+ }, [showSuggestions]);
109
119
 
110
120
 
111
121
  const restoreCaretPosition = (node: HTMLElement, caretOffset: number) => {
@@ -139,21 +149,112 @@ const MentionInput: React.FC<MentionInputProps> = ({
139
149
  }
140
150
  };
141
151
 
142
- const handleInputChange = () => {
143
- if (!inputRef.current) return;
144
-
145
- // Store current selection before modifications
152
+ const getCurrentCaretOffset = (): number => {
153
+ if (!inputRef.current) return 0;
146
154
  const selection = window.getSelection();
147
- const range = selection?.getRangeAt(0);
148
-
149
- let newCaretOffset = 0;
155
+ const range =
156
+ selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
157
+
150
158
  if (range && inputRef.current.contains(range.startContainer)) {
151
159
  const preCaretRange = range.cloneRange();
152
160
  preCaretRange.selectNodeContents(inputRef.current);
153
161
  preCaretRange.setEnd(range.startContainer, range.startOffset);
154
- newCaretOffset = preCaretRange.toString().length;
162
+ return preCaretRange.toString().length;
155
163
  }
156
-
164
+
165
+ return caretOffsetRef.current;
166
+ };
167
+
168
+ const findMentionAtOffset = (
169
+ plainText: string,
170
+ caretOffset: number,
171
+ direction: "backward" | "forward"
172
+ ): { name: string; start: number; end: number } | null => {
173
+ if (!userSelectListRef.current.length) return null;
174
+
175
+ const names = [...userSelectListRef.current].sort(
176
+ (a, b) => b.length - a.length
177
+ );
178
+
179
+ for (const name of names) {
180
+ const pattern = `@${name}`;
181
+ let searchIndex = plainText.indexOf(pattern);
182
+
183
+ while (searchIndex !== -1) {
184
+ let endIndex = searchIndex + pattern.length;
185
+ if (
186
+ plainText[endIndex] === " " ||
187
+ plainText[endIndex] === "\u00a0"
188
+ ) {
189
+ endIndex += 1;
190
+ }
191
+
192
+ const isCaretInsideMention =
193
+ direction === "backward"
194
+ ? caretOffset > searchIndex && caretOffset <= endIndex
195
+ : caretOffset >= searchIndex && caretOffset < endIndex;
196
+
197
+ if (isCaretInsideMention) {
198
+ return { name, start: searchIndex, end: endIndex };
199
+ }
200
+
201
+ searchIndex = plainText.indexOf(pattern, searchIndex + 1);
202
+ }
203
+ }
204
+
205
+ return null;
206
+ };
207
+
208
+ const removeMentionToken = (direction: "backward" | "forward"): boolean => {
209
+ if (!inputRef.current) return false;
210
+
211
+ const plainText = inputRef.current.innerText;
212
+ const caretOffset = getCurrentCaretOffset();
213
+ const mentionInfo = findMentionAtOffset(plainText, caretOffset, direction);
214
+
215
+ if (!mentionInfo) return false;
216
+
217
+ const newText =
218
+ plainText.slice(0, mentionInfo.start) + plainText.slice(mentionInfo.end);
219
+
220
+ const hasLinks = !!newText.match(/(https?:\/\/[^\s]+)/g);
221
+
222
+ if (userSelectListRef.current.length > 0 || hasLinks) {
223
+ const htmlWithHighlights = highlightMentionsAndLinks(newText);
224
+ inputRef.current.innerHTML = htmlWithHighlights;
225
+ } else {
226
+ inputRef.current.innerText = newText;
227
+ }
228
+
229
+ setInputValue(newText);
230
+ setShowSuggestions(false);
231
+
232
+ const newCaretOffset = mentionInfo.start;
233
+ caretOffsetRef.current = newCaretOffset;
234
+
235
+ if (inputRef.current) {
236
+ restoreCaretPosition(inputRef.current, newCaretOffset);
237
+ }
238
+
239
+ if (!newText.includes(`@${mentionInfo.name}`)) {
240
+ userSelectListRef.current = userSelectListRef.current.filter(
241
+ (storedName) => storedName !== mentionInfo.name
242
+ );
243
+ userSelectListWithIdsRef.current =
244
+ userSelectListWithIdsRef.current.filter(
245
+ (user) => user.name !== mentionInfo.name
246
+ );
247
+ }
248
+
249
+ return true;
250
+ };
251
+
252
+ const handleInputChange = () => {
253
+ if (!inputRef.current) return;
254
+
255
+ // Store current selection before modifications
256
+ const newCaretOffset = getCurrentCaretOffset();
257
+
157
258
  caretOffsetRef.current = newCaretOffset;
158
259
 
159
260
  const plainText = inputRef.current.innerText;
@@ -173,17 +274,8 @@ const MentionInput: React.FC<MentionInputProps> = ({
173
274
  setShowSuggestions(false);
174
275
  }
175
276
 
176
- // Extract and store hashtags
177
- const hashtagMatches = plainText.match(/#[\w]+/g);
178
- if (hashtagMatches) {
179
- const uniqueTags = Array.from(new Set(hashtagMatches));
180
- tagsListRef.current = uniqueTags;
181
- } else {
182
- tagsListRef.current = [];
183
- }
184
-
185
- // Only apply highlighting if we have mentions, hashtags, or links to highlight
186
- if (userSelectListRef.current.length > 0 || plainText.match(/(https?:\/\/[^\s]+)/g) || plainText.match(/#[\w]+/g)) {
277
+ // Only apply highlighting if we have mentions or links to highlight
278
+ if (userSelectListRef.current.length > 0 || plainText.match(/(https?:\/\/[^\s]+)/g)) {
187
279
  const currentHTML = inputRef.current.innerHTML;
188
280
  const htmlWithHighlights = highlightMentionsAndLinks(plainText);
189
281
 
@@ -395,7 +487,6 @@ const MentionInput: React.FC<MentionInputProps> = ({
395
487
  messageHTML,
396
488
  userSelectListWithIds: userSelectListWithIdsRef.current,
397
489
  userSelectListName: userSelectListRef.current,
398
- tags: tagsListRef.current,
399
490
  images: selectedImage ? [selectedImage] : [],
400
491
  imageUrl: imageUrl
401
492
  });
@@ -406,7 +497,6 @@ const MentionInput: React.FC<MentionInputProps> = ({
406
497
  setImageUrl(null);
407
498
  userSelectListRef.current = [];
408
499
  userSelectListWithIdsRef.current = [];
409
- tagsListRef.current = [];
410
500
  }
411
501
  }
412
502
  };
@@ -415,6 +505,21 @@ const MentionInput: React.FC<MentionInputProps> = ({
415
505
  if (event.key === "Enter" && !event.shiftKey) {
416
506
  event.preventDefault(); // Prevent newline in content-editable
417
507
  handleSendMessage(); // Trigger the same function as the Send button
508
+ return;
509
+ }
510
+
511
+ if (event.key === "Backspace") {
512
+ const removed = removeMentionToken("backward");
513
+ if (removed) {
514
+ event.preventDefault();
515
+ }
516
+ }
517
+
518
+ if (event.key === "Delete") {
519
+ const removed = removeMentionToken("forward");
520
+ if (removed) {
521
+ event.preventDefault();
522
+ }
418
523
  }
419
524
  };
420
525
 
@@ -478,7 +583,7 @@ const MentionInput: React.FC<MentionInputProps> = ({
478
583
  className={`send-button ${sendBtnClassName || ""}`}
479
584
  aria-label="Send message"
480
585
  >
481
- {sendButtonIcon || ""}
586
+ {sendButtonIcon || ""}
482
587
  </button>
483
588
 
484
589
  <input