react-mention-input 1.1.24 → 1.1.26
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/dist/MentionInput.d.ts +0 -1
- package/dist/MentionInput.js +126 -37
- package/package.json +1 -1
- package/src/MentionInput.tsx +158 -46
package/dist/MentionInput.d.ts
CHANGED
package/dist/MentionInput.js
CHANGED
|
@@ -34,12 +34,21 @@ 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
|
-
|
|
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) {
|
|
41
50
|
var _b;
|
|
42
|
-
var users = _a.users, _c = _a.placeholder, placeholder = _c === void 0 ? "Type a message... (or drag & drop an image)" : _c, containerClassName = _a.containerClassName, inputContainerClassName = _a.inputContainerClassName, inputClassName = _a.inputClassName, sendBtnClassName = _a.sendBtnClassName, suggestionListClassName = _a.suggestionListClassName, suggestionItemClassName = _a.suggestionItemClassName, imgClassName = _a.imgClassName, imgStyle = _a.imgStyle, attachedImageContainerClassName = _a.attachedImageContainerClassName, attachedImageContainerStyle = _a.attachedImageContainerStyle, sendButtonIcon = _a.sendButtonIcon, attachmentButtonIcon = _a.attachmentButtonIcon, onSendMessage = _a.onSendMessage, _d = _a.suggestionPosition, suggestionPosition = _d === void 0 ? '
|
|
51
|
+
var users = _a.users, _c = _a.placeholder, placeholder = _c === void 0 ? "Type a message... (or drag & drop an image)" : _c, containerClassName = _a.containerClassName, inputContainerClassName = _a.inputContainerClassName, inputClassName = _a.inputClassName, sendBtnClassName = _a.sendBtnClassName, suggestionListClassName = _a.suggestionListClassName, suggestionItemClassName = _a.suggestionItemClassName, imgClassName = _a.imgClassName, imgStyle = _a.imgStyle, attachedImageContainerClassName = _a.attachedImageContainerClassName, attachedImageContainerStyle = _a.attachedImageContainerStyle, sendButtonIcon = _a.sendButtonIcon, attachmentButtonIcon = _a.attachmentButtonIcon, onSendMessage = _a.onSendMessage, _d = _a.suggestionPosition, suggestionPosition = _d === void 0 ? 'top' : _d, onImageUpload = _a.onImageUpload;
|
|
43
52
|
var _e = useState(""), inputValue = _e[0], setInputValue = _e[1]; // Plain text
|
|
44
53
|
var _f = useState([]), suggestions = _f[0], setSuggestions = _f[1];
|
|
45
54
|
var _g = useState(false), showSuggestions = _g[0], setShowSuggestions = _g[1];
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
139
|
-
|
|
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
|
|
@@ -157,6 +229,7 @@ var MentionInput = function (_a) {
|
|
|
157
229
|
}
|
|
158
230
|
};
|
|
159
231
|
var renderSuggestions = function () {
|
|
232
|
+
var _a, _b, _c, _d, _e, _f;
|
|
160
233
|
if (!showSuggestions || !inputRef.current)
|
|
161
234
|
return null;
|
|
162
235
|
var getInitials = function (name) {
|
|
@@ -168,27 +241,32 @@ var MentionInput = function (_a) {
|
|
|
168
241
|
return initials;
|
|
169
242
|
};
|
|
170
243
|
var inputRect = inputRef.current.getBoundingClientRect();
|
|
244
|
+
var scrollLeft = (_c = (_b = (_a = window.scrollX) !== null && _a !== void 0 ? _a : window.pageXOffset) !== null && _b !== void 0 ? _b : document.documentElement.scrollLeft) !== null && _c !== void 0 ? _c : 0;
|
|
245
|
+
var scrollTop = (_f = (_e = (_d = window.scrollY) !== null && _d !== void 0 ? _d : window.pageYOffset) !== null && _e !== void 0 ? _e : document.documentElement.scrollTop) !== null && _f !== void 0 ? _f : 0;
|
|
171
246
|
var styles = {
|
|
172
247
|
position: 'absolute',
|
|
173
248
|
zIndex: 1000,
|
|
249
|
+
minWidth: inputRect.width,
|
|
174
250
|
};
|
|
175
251
|
// Use suggestionPosition prop to adjust tooltip position
|
|
176
252
|
switch (suggestionPosition) {
|
|
177
253
|
case 'top':
|
|
178
|
-
styles.left = "".concat(inputRect.left, "px");
|
|
179
|
-
styles.top = "".concat(inputRect.top -
|
|
254
|
+
styles.left = "".concat(inputRect.left + scrollLeft, "px");
|
|
255
|
+
styles.top = "".concat(inputRect.top + scrollTop - 8, "px");
|
|
256
|
+
styles.transform = 'translateY(-100%)';
|
|
180
257
|
break;
|
|
181
258
|
case 'bottom':
|
|
182
|
-
styles.left = "".concat(inputRect.left, "px");
|
|
183
|
-
styles.top = "".concat(inputRect.bottom, "px");
|
|
259
|
+
styles.left = "".concat(inputRect.left + scrollLeft, "px");
|
|
260
|
+
styles.top = "".concat(inputRect.bottom + scrollTop + 8, "px");
|
|
184
261
|
break;
|
|
185
262
|
case 'left':
|
|
186
|
-
styles.left = "".concat(inputRect.left -
|
|
187
|
-
styles.top = "".concat(inputRect.top, "px");
|
|
263
|
+
styles.left = "".concat(inputRect.left + scrollLeft - 8, "px");
|
|
264
|
+
styles.top = "".concat(inputRect.top + scrollTop, "px");
|
|
265
|
+
styles.transform = "".concat(styles.transform ? "".concat(styles.transform, " ") : '', "translateX(-100%)");
|
|
188
266
|
break;
|
|
189
267
|
case 'right':
|
|
190
|
-
styles.left = "".concat(inputRect.right, "px");
|
|
191
|
-
styles.top = "".concat(inputRect.top, "px");
|
|
268
|
+
styles.left = "".concat(inputRect.right + scrollLeft + 8, "px");
|
|
269
|
+
styles.top = "".concat(inputRect.top + scrollTop, "px");
|
|
192
270
|
break;
|
|
193
271
|
default:
|
|
194
272
|
break;
|
|
@@ -333,7 +411,6 @@ var MentionInput = function (_a) {
|
|
|
333
411
|
messageHTML: messageHTML,
|
|
334
412
|
userSelectListWithIds: userSelectListWithIdsRef.current,
|
|
335
413
|
userSelectListName: userSelectListRef.current,
|
|
336
|
-
tags: tagsListRef.current,
|
|
337
414
|
images: selectedImage ? [selectedImage] : [],
|
|
338
415
|
imageUrl: imageUrl
|
|
339
416
|
});
|
|
@@ -344,7 +421,6 @@ var MentionInput = function (_a) {
|
|
|
344
421
|
setImageUrl(null);
|
|
345
422
|
userSelectListRef.current = [];
|
|
346
423
|
userSelectListWithIdsRef.current = [];
|
|
347
|
-
tagsListRef.current = [];
|
|
348
424
|
}
|
|
349
425
|
}
|
|
350
426
|
};
|
|
@@ -352,6 +428,19 @@ var MentionInput = function (_a) {
|
|
|
352
428
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
353
429
|
event.preventDefault(); // Prevent newline in content-editable
|
|
354
430
|
handleSendMessage(); // Trigger the same function as the Send button
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (event.key === "Backspace") {
|
|
434
|
+
var removed = removeMentionToken("backward");
|
|
435
|
+
if (removed) {
|
|
436
|
+
event.preventDefault();
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (event.key === "Delete") {
|
|
440
|
+
var removed = removeMentionToken("forward");
|
|
441
|
+
if (removed) {
|
|
442
|
+
event.preventDefault();
|
|
443
|
+
}
|
|
355
444
|
}
|
|
356
445
|
};
|
|
357
446
|
return (React.createElement("div", { className: "mention-container ".concat(containerClassName || "") },
|
|
@@ -367,7 +456,7 @@ var MentionInput = function (_a) {
|
|
|
367
456
|
React.createElement("div", { className: "mention-input-wrapper" },
|
|
368
457
|
(!inputValue || !inputRef.current || ((_b = inputRef.current) === null || _b === void 0 ? void 0 : _b.innerText.trim()) === "") && (React.createElement("span", { className: "placeholder" }, placeholder)),
|
|
369
458
|
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 || "
|
|
459
|
+
React.createElement("button", { onClick: handleSendMessage, className: "send-button ".concat(sendBtnClassName || ""), "aria-label": "Send message" }, sendButtonIcon || "➤"),
|
|
371
460
|
React.createElement("input", { type: "file", ref: fileInputRef, accept: "image/*", onChange: handleImageSelect, style: { display: 'none' } }),
|
|
372
461
|
isUploading && (React.createElement("div", { className: "upload-loading" },
|
|
373
462
|
React.createElement("span", null, "Uploading...")))),
|
package/package.json
CHANGED
package/src/MentionInput.tsx
CHANGED
|
@@ -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;
|
|
@@ -51,7 +50,7 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
51
50
|
sendButtonIcon,
|
|
52
51
|
attachmentButtonIcon,
|
|
53
52
|
onSendMessage,
|
|
54
|
-
suggestionPosition = '
|
|
53
|
+
suggestionPosition = 'top',
|
|
55
54
|
onImageUpload,
|
|
56
55
|
}) => {
|
|
57
56
|
const [inputValue, setInputValue] = useState<string>(""); // Plain text
|
|
@@ -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
|
|
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 =
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
177
|
-
|
|
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
|
|
|
@@ -210,28 +302,35 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
210
302
|
};
|
|
211
303
|
|
|
212
304
|
const inputRect = inputRef.current.getBoundingClientRect();
|
|
305
|
+
const scrollLeft =
|
|
306
|
+
window.scrollX ?? window.pageXOffset ?? document.documentElement.scrollLeft ?? 0;
|
|
307
|
+
const scrollTop =
|
|
308
|
+
window.scrollY ?? window.pageYOffset ?? document.documentElement.scrollTop ?? 0;
|
|
213
309
|
const styles: React.CSSProperties = {
|
|
214
310
|
position: 'absolute',
|
|
215
311
|
zIndex: 1000,
|
|
312
|
+
minWidth: inputRect.width,
|
|
216
313
|
};
|
|
217
314
|
|
|
218
315
|
// Use suggestionPosition prop to adjust tooltip position
|
|
219
316
|
switch (suggestionPosition) {
|
|
220
317
|
case 'top':
|
|
221
|
-
styles.left = `${inputRect.left}px`;
|
|
222
|
-
styles.top = `${inputRect.top -
|
|
318
|
+
styles.left = `${inputRect.left + scrollLeft}px`;
|
|
319
|
+
styles.top = `${inputRect.top + scrollTop - 8}px`;
|
|
320
|
+
styles.transform = 'translateY(-100%)';
|
|
223
321
|
break;
|
|
224
322
|
case 'bottom':
|
|
225
|
-
styles.left = `${inputRect.left}px`;
|
|
226
|
-
styles.top = `${inputRect.bottom}px`;
|
|
323
|
+
styles.left = `${inputRect.left + scrollLeft}px`;
|
|
324
|
+
styles.top = `${inputRect.bottom + scrollTop + 8}px`;
|
|
227
325
|
break;
|
|
228
326
|
case 'left':
|
|
229
|
-
styles.left = `${inputRect.left -
|
|
230
|
-
styles.top = `${inputRect.top}px`;
|
|
327
|
+
styles.left = `${inputRect.left + scrollLeft - 8}px`;
|
|
328
|
+
styles.top = `${inputRect.top + scrollTop}px`;
|
|
329
|
+
styles.transform = `${styles.transform ? `${styles.transform} ` : ''}translateX(-100%)`;
|
|
231
330
|
break;
|
|
232
331
|
case 'right':
|
|
233
|
-
styles.left = `${inputRect.right}px`;
|
|
234
|
-
styles.top = `${inputRect.top}px`;
|
|
332
|
+
styles.left = `${inputRect.right + scrollLeft + 8}px`;
|
|
333
|
+
styles.top = `${inputRect.top + scrollTop}px`;
|
|
235
334
|
break;
|
|
236
335
|
default:
|
|
237
336
|
break;
|
|
@@ -395,7 +494,6 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
395
494
|
messageHTML,
|
|
396
495
|
userSelectListWithIds: userSelectListWithIdsRef.current,
|
|
397
496
|
userSelectListName: userSelectListRef.current,
|
|
398
|
-
tags: tagsListRef.current,
|
|
399
497
|
images: selectedImage ? [selectedImage] : [],
|
|
400
498
|
imageUrl: imageUrl
|
|
401
499
|
});
|
|
@@ -406,7 +504,6 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
406
504
|
setImageUrl(null);
|
|
407
505
|
userSelectListRef.current = [];
|
|
408
506
|
userSelectListWithIdsRef.current = [];
|
|
409
|
-
tagsListRef.current = [];
|
|
410
507
|
}
|
|
411
508
|
}
|
|
412
509
|
};
|
|
@@ -415,6 +512,21 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
415
512
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
416
513
|
event.preventDefault(); // Prevent newline in content-editable
|
|
417
514
|
handleSendMessage(); // Trigger the same function as the Send button
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (event.key === "Backspace") {
|
|
519
|
+
const removed = removeMentionToken("backward");
|
|
520
|
+
if (removed) {
|
|
521
|
+
event.preventDefault();
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (event.key === "Delete") {
|
|
526
|
+
const removed = removeMentionToken("forward");
|
|
527
|
+
if (removed) {
|
|
528
|
+
event.preventDefault();
|
|
529
|
+
}
|
|
418
530
|
}
|
|
419
531
|
};
|
|
420
532
|
|
|
@@ -478,7 +590,7 @@ const MentionInput: React.FC<MentionInputProps> = ({
|
|
|
478
590
|
className={`send-button ${sendBtnClassName || ""}`}
|
|
479
591
|
aria-label="Send message"
|
|
480
592
|
>
|
|
481
|
-
{sendButtonIcon || "
|
|
593
|
+
{sendButtonIcon || "➤"}
|
|
482
594
|
</button>
|
|
483
595
|
|
|
484
596
|
<input
|