react-mention-input 1.1.23 → 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.
- package/dist/MentionInput.d.ts +0 -1
- package/dist/MentionInput.js +111 -28
- package/dist/ShowMessageCard.d.ts +0 -1
- package/dist/ShowMessageCard.js +12 -18
- package/package.json +1 -1
- package/src/MentionInput.tsx +142 -37
- package/src/ShowMessageCard.tsx +34 -56
package/dist/MentionInput.d.ts
CHANGED
package/dist/MentionInput.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
@@ -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/dist/ShowMessageCard.js
CHANGED
|
@@ -28,12 +28,10 @@ export var ShowMessageCard = function (_a) {
|
|
|
28
28
|
revisionKey = _h === void 0 ? "revision" : _h, // Default key for revision
|
|
29
29
|
_j = _a.objectTypeIconKey, // Default key for revision
|
|
30
30
|
objectTypeIconKey = _j === void 0 ? "object_type_icon" : _j, // Default key for object type icon
|
|
31
|
-
_k = _a.relatedObjectKey, // Default key for object type icon
|
|
32
|
-
relatedObjectKey = _k === void 0 ? "relatedObject" : _k, // Default key for related object
|
|
33
31
|
containerClassName = _a.containerClassName, containerStyle = _a.containerStyle, cardClassName = _a.cardClassName, cardStyle = _a.cardStyle, headerClassName = _a.headerClassName, headerStyle = _a.headerStyle, imgClassName = _a.imgClassName, imgStyle = _a.imgStyle, infoClassName = _a.infoClassName, infoStyle = _a.infoStyle, nameClassName = _a.nameClassName, nameStyle = _a.nameStyle, dateClassName = _a.dateClassName, dateStyle = _a.dateStyle, bodyClassName = _a.bodyClassName, bodyStyle = _a.bodyStyle, commentClassName = _a.commentClassName, commentStyle = _a.commentStyle, attachedImageClassName = _a.attachedImageClassName, attachedImageStyle = _a.attachedImageStyle, attachedImageContainerClassName = _a.attachedImageContainerClassName, attachedImageContainerStyle = _a.attachedImageContainerStyle, objectNameClassName = _a.objectNameClassName, objectNameStyle = _a.objectNameStyle, revisionClassName = _a.revisionClassName, revisionStyle = _a.revisionStyle, objectChipRender = _a.objectChipRender, // Custom render function for object chip
|
|
34
32
|
renderItem = _a.renderItem;
|
|
35
33
|
// State to manage initials for images that fail to load
|
|
36
|
-
var
|
|
34
|
+
var _k = useState({}), initialsState = _k[0], setInitialsState = _k[1];
|
|
37
35
|
// Handle image load failure
|
|
38
36
|
var handleImageError = function (id) {
|
|
39
37
|
setInitialsState(function (prevState) {
|
|
@@ -87,26 +85,22 @@ export var ShowMessageCard = function (_a) {
|
|
|
87
85
|
showInitials ? (React.createElement("div", { className: "message-card-initials ".concat(imgClassName || ""), style: imgStyle }, getInitials(item[nameKey]))) : (React.createElement("img", { src: item[imgSrcKey], alt: item[nameKey], className: "message-card-img ".concat(imgClassName || ""), style: imgStyle, onError: function () { return handleImageError(item.id || index); } })),
|
|
88
86
|
React.createElement("div", { className: "message-card-info ".concat(infoClassName || ""), style: infoStyle },
|
|
89
87
|
React.createElement("h3", { className: "message-card-name ".concat(nameClassName || ""), style: nameStyle }, item[nameKey]),
|
|
90
|
-
React.createElement("p", { className: "message-card-date ".concat(dateClassName || ""), style: dateStyle }, item[dateKey]))),
|
|
91
|
-
(item[objectNameKey] || item[revisionKey] || item[objectTypeIconKey]) && (React.createElement(React.Fragment, null, objectChipRender ? (objectChipRender({
|
|
92
|
-
objectName: item[objectNameKey],
|
|
93
|
-
revision: item[revisionKey],
|
|
94
|
-
objectTypeIcon: item[objectTypeIconKey],
|
|
95
|
-
item: item
|
|
96
|
-
})) : (React.createElement("div", { className: "message-card-item-name ".concat(objectNameClassName || ""), style: objectNameStyle },
|
|
97
|
-
item[objectTypeIconKey] && (React.createElement("span", { className: "object-type-icon", style: { marginRight: '6px' } }, item[objectTypeIconKey])),
|
|
98
|
-
item[objectNameKey] && (React.createElement("span", { className: "object-name-text" }, item[objectNameKey])),
|
|
99
|
-
item[revisionKey] && (React.createElement("span", { className: "revision-text ".concat(revisionClassName || ""), style: revisionStyle }, item[objectNameKey] ? " (".concat(item[revisionKey], ")") : "(".concat(item[revisionKey], ")")))))))),
|
|
88
|
+
React.createElement("p", { className: "message-card-date ".concat(dateClassName || ""), style: dateStyle }, item[dateKey])))),
|
|
100
89
|
React.createElement("div", { className: "message-card ".concat(cardClassName || ""), style: cardStyle },
|
|
101
90
|
React.createElement("div", { className: "message-card-body ".concat(bodyClassName || ""), style: bodyStyle },
|
|
102
91
|
React.createElement("p", { className: "message-card-comment ".concat(commentClassName || ""), style: commentStyle, dangerouslySetInnerHTML: { __html: item[commentKey] } }),
|
|
103
92
|
(item === null || item === void 0 ? void 0 : item[imageUrlKey]) && (React.createElement("div", { className: "message-card-attached-image-container ".concat(attachedImageContainerClassName || ""), style: attachedImageContainerStyle },
|
|
104
93
|
React.createElement("img", { src: item[imageUrlKey], alt: "Attached", className: "message-card-attached-image ".concat(attachedImageClassName || ""), style: attachedImageStyle }))),
|
|
105
|
-
|
|
106
|
-
hashtags.map(function (tag, tagIndex) { return (React.createElement("span", { key: "hashtag-".concat(tagIndex), className: "tag-chip hashtag-chip" }, tag)); }),
|
|
107
|
-
mentions.map(function (mention, mentionIndex) { return (React.createElement("span", { key: "mention-".concat(mentionIndex), className: "tag-chip mention-chip" }, mention)); }))),
|
|
108
|
-
item[relatedObjectKey] && (React.createElement("div", { className: "message-card-related" },
|
|
94
|
+
item[objectNameKey] && (React.createElement("div", { className: "message-card-related" },
|
|
109
95
|
React.createElement("span", { className: "related-label" }, "Related: "),
|
|
110
|
-
React.createElement(
|
|
96
|
+
(item[objectNameKey] || item[revisionKey] || item[objectTypeIconKey]) && (React.createElement(React.Fragment, null, objectChipRender ? (objectChipRender({
|
|
97
|
+
objectName: item[objectNameKey],
|
|
98
|
+
revision: item[revisionKey],
|
|
99
|
+
objectTypeIcon: item[objectTypeIconKey],
|
|
100
|
+
item: item
|
|
101
|
+
})) : (React.createElement("div", { className: "message-card-item-name ".concat(objectNameClassName || ""), style: objectNameStyle },
|
|
102
|
+
item[objectTypeIconKey] && (React.createElement("span", { className: "object-type-icon", style: { marginRight: '6px' } }, item[objectTypeIconKey])),
|
|
103
|
+
item[objectNameKey] && (React.createElement("span", { className: "object-name-text" }, item[objectNameKey])),
|
|
104
|
+
item[revisionKey] && (React.createElement("span", { className: "revision-text ".concat(revisionClassName || ""), style: revisionStyle }, item[objectNameKey] ? " (".concat(item[revisionKey], ")") : "(".concat(item[revisionKey], ")")))))))))))));
|
|
111
105
|
})));
|
|
112
106
|
};
|
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;
|
|
@@ -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
|
|
|
@@ -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
|
package/src/ShowMessageCard.tsx
CHANGED
|
@@ -15,7 +15,6 @@ interface ShowMessageCardProps {
|
|
|
15
15
|
objectNameKey?: string; // Custom key for object identifier (top-right)
|
|
16
16
|
revisionKey?: string; // Custom key for revision (top-right)
|
|
17
17
|
objectTypeIconKey?: string; // Custom key for object type icon (top-right)
|
|
18
|
-
relatedObjectKey?: string; // Custom key for related object (bottom-left)
|
|
19
18
|
containerClassName?: string; // Class for the outermost container
|
|
20
19
|
containerStyle?: CSSProperties; // Style for the outermost container
|
|
21
20
|
cardClassName?: string; // Class for the card
|
|
@@ -56,7 +55,6 @@ export const ShowMessageCard: React.FC<ShowMessageCardProps> = ({
|
|
|
56
55
|
objectNameKey = "objectName", // Default key for object identifier
|
|
57
56
|
revisionKey = "revision", // Default key for revision
|
|
58
57
|
objectTypeIconKey = "object_type_icon", // Default key for object type icon
|
|
59
|
-
relatedObjectKey = "relatedObject", // Default key for related object
|
|
60
58
|
containerClassName,
|
|
61
59
|
containerStyle,
|
|
62
60
|
cardClassName,
|
|
@@ -199,39 +197,6 @@ export const ShowMessageCard: React.FC<ShowMessageCardProps> = ({
|
|
|
199
197
|
</p>
|
|
200
198
|
</div>
|
|
201
199
|
</div>
|
|
202
|
-
|
|
203
|
-
{/* Object identifier with revision in top-right corner */}
|
|
204
|
-
{(item[objectNameKey] || item[revisionKey] || item[objectTypeIconKey]) && (
|
|
205
|
-
<>
|
|
206
|
-
{objectChipRender ? (
|
|
207
|
-
objectChipRender({
|
|
208
|
-
objectName: item[objectNameKey],
|
|
209
|
-
revision: item[revisionKey],
|
|
210
|
-
objectTypeIcon: item[objectTypeIconKey],
|
|
211
|
-
item: item
|
|
212
|
-
})
|
|
213
|
-
) : (
|
|
214
|
-
<div
|
|
215
|
-
className={`message-card-item-name ${objectNameClassName || ""}`}
|
|
216
|
-
style={objectNameStyle}
|
|
217
|
-
>
|
|
218
|
-
{item[objectTypeIconKey] && (
|
|
219
|
-
<span className="object-type-icon" style={{ marginRight: '6px' }}>
|
|
220
|
-
{item[objectTypeIconKey]}
|
|
221
|
-
</span>
|
|
222
|
-
)}
|
|
223
|
-
{item[objectNameKey] && (
|
|
224
|
-
<span className="object-name-text">{item[objectNameKey]}</span>
|
|
225
|
-
)}
|
|
226
|
-
{item[revisionKey] && (
|
|
227
|
-
<span className={`revision-text ${revisionClassName || ""}`} style={revisionStyle}>
|
|
228
|
-
{item[objectNameKey] ? ` (${item[revisionKey]})` : `(${item[revisionKey]})`}
|
|
229
|
-
</span>
|
|
230
|
-
)}
|
|
231
|
-
</div>
|
|
232
|
-
)}
|
|
233
|
-
</>
|
|
234
|
-
)}
|
|
235
200
|
</div>
|
|
236
201
|
|
|
237
202
|
{/* Card bubble */}
|
|
@@ -264,31 +229,44 @@ export const ShowMessageCard: React.FC<ShowMessageCardProps> = ({
|
|
|
264
229
|
</div>
|
|
265
230
|
)}
|
|
266
231
|
|
|
267
|
-
{/* Display hashtags and mentions as chips */}
|
|
268
|
-
{(hashtags.length > 0 || mentions.length > 0) && (
|
|
269
|
-
<div className="message-card-tags">
|
|
270
|
-
{hashtags.map((tag, tagIndex) => (
|
|
271
|
-
<span key={`hashtag-${tagIndex}`} className="tag-chip hashtag-chip">
|
|
272
|
-
{tag}
|
|
273
|
-
</span>
|
|
274
|
-
))}
|
|
275
|
-
{mentions.map((mention, mentionIndex) => (
|
|
276
|
-
<span key={`mention-${mentionIndex}`} className="tag-chip mention-chip">
|
|
277
|
-
{mention}
|
|
278
|
-
</span>
|
|
279
|
-
))}
|
|
280
|
-
</div>
|
|
281
|
-
)}
|
|
282
|
-
|
|
283
232
|
{/* Display related object at bottom-left */}
|
|
284
|
-
{item[
|
|
233
|
+
{item[objectNameKey] && (
|
|
285
234
|
<div className="message-card-related">
|
|
286
235
|
<span className="related-label">Related: </span>
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
236
|
+
{/* Object identifier with revision in top-right corner */}
|
|
237
|
+
{(item[objectNameKey] || item[revisionKey] || item[objectTypeIconKey]) && (
|
|
238
|
+
<>
|
|
239
|
+
{objectChipRender ? (
|
|
240
|
+
objectChipRender({
|
|
241
|
+
objectName: item[objectNameKey],
|
|
242
|
+
revision: item[revisionKey],
|
|
243
|
+
objectTypeIcon: item[objectTypeIconKey],
|
|
244
|
+
item: item
|
|
245
|
+
})
|
|
246
|
+
) : (
|
|
247
|
+
<div
|
|
248
|
+
className={`message-card-item-name ${objectNameClassName || ""}`}
|
|
249
|
+
style={objectNameStyle}
|
|
250
|
+
>
|
|
251
|
+
{item[objectTypeIconKey] && (
|
|
252
|
+
<span className="object-type-icon" style={{ marginRight: '6px' }}>
|
|
253
|
+
{item[objectTypeIconKey]}
|
|
254
|
+
</span>
|
|
255
|
+
)}
|
|
256
|
+
{item[objectNameKey] && (
|
|
257
|
+
<span className="object-name-text">{item[objectNameKey]}</span>
|
|
258
|
+
)}
|
|
259
|
+
{item[revisionKey] && (
|
|
260
|
+
<span className={`revision-text ${revisionClassName || ""}`} style={revisionStyle}>
|
|
261
|
+
{item[objectNameKey] ? ` (${item[revisionKey]})` : `(${item[revisionKey]})`}
|
|
262
|
+
</span>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
266
|
+
</>
|
|
267
|
+
)}
|
|
290
268
|
</div>
|
|
291
|
-
|
|
269
|
+
)}
|
|
292
270
|
</div>
|
|
293
271
|
</div>
|
|
294
272
|
</div>
|