react-mention-input 1.1.15 → 1.1.17

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/demo.tsx ADDED
@@ -0,0 +1,98 @@
1
+ import React, { useState } from 'react';
2
+ import { MentionInput, ShowMessageCard } from './src';
3
+
4
+ const Demo: React.FC = () => {
5
+ const [messages, setMessages] = useState<any[]>([
6
+ {
7
+ id: 1,
8
+ name: 'John Doe',
9
+ date: '2 hours ago',
10
+ comment: 'Updated the status to Draft. Need <span class="mention-highlight">@team-leads</span> review before proceeding. <span class="hashtag-highlight">#urgent</span> <span class="hashtag-highlight">#review</span>',
11
+ objectName: '26may_item001 (A)',
12
+ },
13
+ {
14
+ id: 2,
15
+ name: 'Mike Johnson',
16
+ date: '1 day ago',
17
+ comment: 'Revision A completed successfully. Ready for next phase. <span class="hashtag-highlight">#milestone</span>',
18
+ objectName: '26may_item001 (A)',
19
+ }
20
+ ]);
21
+
22
+ // Sample users for mentions
23
+ const users = [
24
+ { id: 1, name: 'John Doe' },
25
+ { id: 2, name: 'Jane Smith' },
26
+ { id: 3, name: 'Mike Johnson' },
27
+ { id: 4, name: 'Sarah Wilson' },
28
+ ];
29
+
30
+ const handleSendMessage = (messageData: {
31
+ messageText: string;
32
+ messageHTML: string;
33
+ userSelectListWithIds: { id: number; name: string }[];
34
+ userSelectListName: string[];
35
+ tags: string[];
36
+ images?: File[];
37
+ imageUrl?: string | null;
38
+ }) => {
39
+ console.log('Message Data:', messageData);
40
+ console.log('Extracted Tags:', messageData.tags);
41
+
42
+ // Create a new message for display
43
+ const newMessage = {
44
+ id: Date.now(),
45
+ name: 'You',
46
+ date: new Date().toLocaleTimeString(),
47
+ comment: messageData.messageHTML,
48
+ imageUrl: messageData.imageUrl,
49
+ tags: messageData.tags,
50
+ mentions: messageData.userSelectListName,
51
+ objectName: 'new_item001 (A)', // You can customize this or make it dynamic
52
+ };
53
+
54
+ setMessages([...messages, newMessage]);
55
+ };
56
+
57
+ return (
58
+ <div style={{ maxWidth: '600px', margin: '20px auto', padding: '20px' }}>
59
+ <h1>React Mention Input with Hashtag Support</h1>
60
+
61
+ <div style={{ marginBottom: '20px' }}>
62
+ <h3>Features:</h3>
63
+ <ul>
64
+ <li>Type <strong>@</strong> to mention users (e.g., @John Doe)</li>
65
+ <li>Type <strong>#</strong> to create hashtags (e.g., #urgent #review #milestone)</li>
66
+ <li>Links are automatically detected and highlighted</li>
67
+ <li>Hashtags are extracted and returned in the tags array</li>
68
+ </ul>
69
+ </div>
70
+
71
+ <div style={{ marginBottom: '20px' }}>
72
+ <MentionInput
73
+ users={users}
74
+ placeholder="Type a message with @mentions and #hashtags..."
75
+ onSendMessage={handleSendMessage}
76
+ suggestionPosition="bottom"
77
+ />
78
+ </div>
79
+
80
+ <div>
81
+ <h3>Messages:</h3>
82
+ <ShowMessageCard data={messages} />
83
+ </div>
84
+
85
+ <div style={{ marginTop: '20px', padding: '15px', backgroundColor: '#f5f5f5', borderRadius: '8px' }}>
86
+ <h4>Example Usage:</h4>
87
+ <p>Try typing these examples:</p>
88
+ <ul>
89
+ <li>"Project update complete. Ready for @John Doe review #milestone #completed"</li>
90
+ <li>"Need urgent help with deployment @Jane Smith @Mike Johnson #urgent #deployment #help"</li>
91
+ <li>"Meeting scheduled for tomorrow #meeting #planning"</li>
92
+ </ul>
93
+ </div>
94
+ </div>
95
+ );
96
+ };
97
+
98
+ export default Demo;
@@ -193,6 +193,14 @@
193
193
  font-weight: 500;
194
194
  }
195
195
 
196
+ .hashtag-highlight {
197
+ background-color: rgba(255, 165, 0, 0.1);
198
+ color: #FF8C00;
199
+ border-radius: 4px;
200
+ padding: 1px 4px;
201
+ font-weight: 500;
202
+ }
203
+
196
204
  .link-highlight {
197
205
  color: #2684FF;
198
206
  text-decoration: none;
@@ -21,9 +21,25 @@
21
21
  .message-card-header {
22
22
  display: flex;
23
23
  align-items: center;
24
+ justify-content: space-between;
24
25
  margin-bottom: 8px; /* Space between header and body */
25
26
  }
26
27
 
28
+ .message-card-header-left {
29
+ display: flex;
30
+ align-items: center;
31
+ }
32
+
33
+ .message-card-item-name {
34
+ font-size: 14px;
35
+ color: #666;
36
+ font-weight: 500;
37
+ background-color: #f5f5f5;
38
+ padding: 4px 8px;
39
+ border-radius: 6px;
40
+ border: 1px solid #e0e0e0;
41
+ }
42
+
27
43
  .message-card-img,
28
44
  .message-card-initials {
29
45
  width: 48px;
@@ -72,4 +88,40 @@
72
88
  color: #007bff;
73
89
  padding: 2px 4px;
74
90
  border-radius: 4px;
91
+ }
92
+
93
+ .hashtag-highlight {
94
+ background-color: rgba(255, 165, 0, 0.15);
95
+ color: #FF8C00;
96
+ padding: 2px 4px;
97
+ border-radius: 4px;
98
+ font-weight: 500;
99
+ }
100
+
101
+ /* Tag chips styling */
102
+ .message-card-tags {
103
+ display: flex;
104
+ flex-wrap: wrap;
105
+ gap: 8px;
106
+ margin-top: 12px;
107
+ }
108
+
109
+ .tag-chip {
110
+ padding: 6px 12px;
111
+ border-radius: 16px;
112
+ font-size: 12px;
113
+ font-weight: 500;
114
+ border: none;
115
+ display: inline-block;
116
+ white-space: nowrap;
117
+ }
118
+
119
+ .hashtag-chip {
120
+ background-color: #D4A574;
121
+ color: #FFFFFF;
122
+ }
123
+
124
+ .mention-chip {
125
+ background-color: #2684FF;
126
+ color: #FFFFFF;
75
127
  }
package/dist/demo.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ declare const Demo: React.FC;
3
+ export default Demo;
package/dist/demo.js ADDED
@@ -0,0 +1,80 @@
1
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
2
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
3
+ if (ar || !(i in from)) {
4
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
5
+ ar[i] = from[i];
6
+ }
7
+ }
8
+ return to.concat(ar || Array.prototype.slice.call(from));
9
+ };
10
+ import React, { useState } from 'react';
11
+ import { MentionInput, ShowMessageCard } from './src';
12
+ var Demo = function () {
13
+ var _a = useState([
14
+ {
15
+ id: 1,
16
+ name: 'John Doe',
17
+ date: '2 hours ago',
18
+ comment: 'Updated the status to Draft. Need <span class="mention-highlight">@team-leads</span> review before proceeding. <span class="hashtag-highlight">#urgent</span> <span class="hashtag-highlight">#review</span>',
19
+ itemName: '26may_item001 (A)',
20
+ },
21
+ {
22
+ id: 2,
23
+ name: 'Mike Johnson',
24
+ date: '1 day ago',
25
+ comment: 'Revision A completed successfully. Ready for next phase. <span class="hashtag-highlight">#milestone</span>',
26
+ itemName: '26may_item001 (A)',
27
+ }
28
+ ]), messages = _a[0], setMessages = _a[1];
29
+ // Sample users for mentions
30
+ var users = [
31
+ { id: 1, name: 'John Doe' },
32
+ { id: 2, name: 'Jane Smith' },
33
+ { id: 3, name: 'Mike Johnson' },
34
+ { id: 4, name: 'Sarah Wilson' },
35
+ ];
36
+ var handleSendMessage = function (messageData) {
37
+ console.log('Message Data:', messageData);
38
+ console.log('Extracted Tags:', messageData.tags);
39
+ // Create a new message for display
40
+ var newMessage = {
41
+ id: Date.now(),
42
+ name: 'You',
43
+ date: new Date().toLocaleTimeString(),
44
+ comment: messageData.messageHTML,
45
+ imageUrl: messageData.imageUrl,
46
+ tags: messageData.tags,
47
+ mentions: messageData.userSelectListName,
48
+ itemName: 'new_item001 (A)', // You can customize this or make it dynamic
49
+ };
50
+ setMessages(__spreadArray(__spreadArray([], messages, true), [newMessage], false));
51
+ };
52
+ return (React.createElement("div", { style: { maxWidth: '600px', margin: '20px auto', padding: '20px' } },
53
+ React.createElement("h1", null, "React Mention Input with Hashtag Support"),
54
+ React.createElement("div", { style: { marginBottom: '20px' } },
55
+ React.createElement("h3", null, "Features:"),
56
+ React.createElement("ul", null,
57
+ React.createElement("li", null,
58
+ "Type ",
59
+ React.createElement("strong", null, "@"),
60
+ " to mention users (e.g., @John Doe)"),
61
+ React.createElement("li", null,
62
+ "Type ",
63
+ React.createElement("strong", null, "#"),
64
+ " to create hashtags (e.g., #urgent #review #milestone)"),
65
+ React.createElement("li", null, "Links are automatically detected and highlighted"),
66
+ React.createElement("li", null, "Hashtags are extracted and returned in the tags array"))),
67
+ React.createElement("div", { style: { marginBottom: '20px' } },
68
+ React.createElement(MentionInput, { users: users, placeholder: "Type a message with @mentions and #hashtags...", onSendMessage: handleSendMessage, suggestionPosition: "bottom" })),
69
+ React.createElement("div", null,
70
+ React.createElement("h3", null, "Messages:"),
71
+ React.createElement(ShowMessageCard, { data: messages })),
72
+ React.createElement("div", { style: { marginTop: '20px', padding: '15px', backgroundColor: '#f5f5f5', borderRadius: '8px' } },
73
+ React.createElement("h4", null, "Example Usage:"),
74
+ React.createElement("p", null, "Try typing these examples:"),
75
+ React.createElement("ul", null,
76
+ React.createElement("li", null, "\"Project update complete. Ready for @John Doe review #milestone #completed\""),
77
+ React.createElement("li", null, "\"Need urgent help with deployment @Jane Smith @Mike Johnson #urgent #deployment #help\""),
78
+ React.createElement("li", null, "\"Meeting scheduled for tomorrow #meeting #planning\"")))));
79
+ };
80
+ export default Demo;
package/dist/main.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/main.js ADDED
@@ -0,0 +1,5 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import Demo from './demo';
4
+ ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(React.StrictMode, null,
5
+ React.createElement(Demo, null)));
@@ -0,0 +1,38 @@
1
+ import React, { ReactNode } from "react";
2
+ import "./MentionInput.css";
3
+ interface User {
4
+ id: number;
5
+ name: string;
6
+ }
7
+ interface MentionInputProps {
8
+ users: User[];
9
+ placeholder?: string;
10
+ containerClassName?: string;
11
+ inputContainerClassName?: string;
12
+ inputClassName?: string;
13
+ sendBtnClassName?: string;
14
+ suggestionListClassName?: string;
15
+ suggestionItemClassName?: string;
16
+ attachedImageContainerClassName?: string;
17
+ attachedImageContainerStyle?: React.CSSProperties;
18
+ imgClassName?: string;
19
+ imgStyle?: React.CSSProperties;
20
+ sendButtonIcon?: ReactNode;
21
+ attachmentButtonIcon?: ReactNode;
22
+ onSendMessage?: (obj: {
23
+ messageText: string;
24
+ messageHTML: string;
25
+ userSelectListWithIds: {
26
+ id: number;
27
+ name: string;
28
+ }[];
29
+ userSelectListName: string[];
30
+ tags: string[];
31
+ images?: File[];
32
+ imageUrl?: string | null;
33
+ }) => void;
34
+ suggestionPosition?: 'top' | 'bottom' | 'left' | 'right';
35
+ onImageUpload?: (file: File) => Promise<string>;
36
+ }
37
+ declare const MentionInput: React.FC<MentionInputProps>;
38
+ export default MentionInput;
@@ -0,0 +1,376 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ var __generator = (this && this.__generator) || function (thisArg, body) {
11
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
12
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
13
+ function verb(n) { return function (v) { return step([n, v]); }; }
14
+ function step(op) {
15
+ if (f) throw new TypeError("Generator is already executing.");
16
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
17
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
18
+ if (y = 0, t) op = [op[0] & 2, t.value];
19
+ switch (op[0]) {
20
+ case 0: case 1: t = op; break;
21
+ case 4: _.label++; return { value: op[1], done: false };
22
+ case 5: _.label++; y = op[1]; op = [0]; continue;
23
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
24
+ default:
25
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
26
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
27
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
28
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
29
+ if (t[2]) _.ops.pop();
30
+ _.trys.pop(); continue;
31
+ }
32
+ op = body.call(thisArg, _);
33
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
34
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
35
+ }
36
+ };
37
+ import React, { useState, useRef } from "react";
38
+ import ReactDOM from "react-dom";
39
+ import "./MentionInput.css";
40
+ var MentionInput = function (_a) {
41
+ 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 ? 'bottom' : _d, onImageUpload = _a.onImageUpload;
43
+ var _e = useState(""), inputValue = _e[0], setInputValue = _e[1]; // Plain text
44
+ var _f = useState([]), suggestions = _f[0], setSuggestions = _f[1];
45
+ var _g = useState(false), showSuggestions = _g[0], setShowSuggestions = _g[1];
46
+ var _h = useState(null), selectedImage = _h[0], setSelectedImage = _h[1];
47
+ var _j = useState(null), imageUrl = _j[0], setImageUrl = _j[1];
48
+ var _k = useState(false), isUploading = _k[0], setIsUploading = _k[1];
49
+ var _l = useState(false), isDraggingOver = _l[0], setIsDraggingOver = _l[1];
50
+ var inputRef = useRef(null);
51
+ var suggestionListRef = useRef(null);
52
+ var caretOffsetRef = useRef(0);
53
+ var userSelectListRef = useRef([]); // Only unique names
54
+ var userSelectListWithIdsRef = useRef([]); // Unique IDs with names
55
+ var tagsListRef = useRef([]); // Store hashtags
56
+ var fileInputRef = useRef(null);
57
+ var highlightMentionsAndLinks = function (text) {
58
+ // Regular expression for detecting links
59
+ var linkRegex = /(https?:\/\/[^\s]+)/g;
60
+ // Regular expression for detecting hashtags
61
+ var hashtagRegex = /#[\w]+/g;
62
+ // Highlight links
63
+ 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
+ // Highlight mentions manually based on `userSelectListRef`
69
+ userSelectListRef === null || userSelectListRef === void 0 ? void 0 : userSelectListRef.current.forEach(function (userName) {
70
+ var mentionPattern = new RegExp("@".concat(userName, "(\\s|$)"), "g");
71
+ highlightedText = highlightedText.replace(mentionPattern, function (match) {
72
+ return "<span class=\"mention-highlight\">".concat(match.trim(), "</span>&nbsp;");
73
+ });
74
+ });
75
+ return highlightedText;
76
+ };
77
+ var restoreCaretPosition = function (node, caretOffset) {
78
+ var range = document.createRange();
79
+ var sel = window.getSelection();
80
+ var charCount = 0;
81
+ var findCaret = function (currentNode) {
82
+ var _a;
83
+ for (var _i = 0, _b = Array.from(currentNode.childNodes); _i < _b.length; _i++) {
84
+ var child = _b[_i];
85
+ if (child.nodeType === Node.TEXT_NODE) {
86
+ var textLength = ((_a = child.textContent) === null || _a === void 0 ? void 0 : _a.length) || 0;
87
+ if (charCount + textLength >= caretOffset) {
88
+ range.setStart(child, caretOffset - charCount);
89
+ range.collapse(true);
90
+ return true;
91
+ }
92
+ else {
93
+ charCount += textLength;
94
+ }
95
+ }
96
+ else if (child.nodeType === Node.ELEMENT_NODE) {
97
+ if (findCaret(child))
98
+ return true;
99
+ }
100
+ }
101
+ return false;
102
+ };
103
+ findCaret(node);
104
+ if (sel) {
105
+ sel.removeAllRanges();
106
+ sel.addRange(range);
107
+ }
108
+ };
109
+ var handleInputChange = function () {
110
+ if (!inputRef.current)
111
+ return;
112
+ // Store current selection before modifications
113
+ var selection = window.getSelection();
114
+ var range = selection === null || selection === void 0 ? void 0 : selection.getRangeAt(0);
115
+ var newCaretOffset = 0;
116
+ if (range && inputRef.current.contains(range.startContainer)) {
117
+ var preCaretRange = range.cloneRange();
118
+ preCaretRange.selectNodeContents(inputRef.current);
119
+ preCaretRange.setEnd(range.startContainer, range.startOffset);
120
+ newCaretOffset = preCaretRange.toString().length;
121
+ }
122
+ caretOffsetRef.current = newCaretOffset;
123
+ var plainText = inputRef.current.innerText;
124
+ setInputValue(plainText);
125
+ // Process for mention suggestions
126
+ var mentionMatch = plainText.slice(0, newCaretOffset).match(/@(\S*)$/);
127
+ if (mentionMatch) {
128
+ var query_1 = mentionMatch[1].toLowerCase();
129
+ var filteredUsers = query_1 === "" ? users : users.filter(function (user) {
130
+ return user.name.toLowerCase().includes(query_1);
131
+ });
132
+ setSuggestions(filteredUsers);
133
+ setShowSuggestions(filteredUsers.length > 0);
134
+ }
135
+ else {
136
+ setShowSuggestions(false);
137
+ }
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)) {
149
+ var currentHTML = inputRef.current.innerHTML;
150
+ var htmlWithHighlights = highlightMentionsAndLinks(plainText);
151
+ // Only update if the highlighted HTML is different to avoid cursor jumping
152
+ if (currentHTML !== htmlWithHighlights) {
153
+ inputRef.current.innerHTML = htmlWithHighlights;
154
+ // Restore cursor position after changing innerHTML
155
+ restoreCaretPosition(inputRef.current, newCaretOffset);
156
+ }
157
+ }
158
+ };
159
+ var renderSuggestions = function () {
160
+ if (!showSuggestions || !inputRef.current)
161
+ return null;
162
+ var getInitials = function (name) {
163
+ var nameParts = name.split(" ");
164
+ var initials = nameParts
165
+ .map(function (part) { var _a; return ((_a = part[0]) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || ""; })
166
+ .slice(0, 2)
167
+ .join("");
168
+ return initials;
169
+ };
170
+ var inputRect = inputRef.current.getBoundingClientRect();
171
+ var styles = {
172
+ position: 'absolute',
173
+ zIndex: 1000,
174
+ };
175
+ // Use suggestionPosition prop to adjust tooltip position
176
+ switch (suggestionPosition) {
177
+ case 'top':
178
+ styles.left = "".concat(inputRect.left, "px");
179
+ styles.top = "".concat(inputRect.top - 150, "px");
180
+ break;
181
+ case 'bottom':
182
+ styles.left = "".concat(inputRect.left, "px");
183
+ styles.top = "".concat(inputRect.bottom, "px");
184
+ break;
185
+ case 'left':
186
+ styles.left = "".concat(inputRect.left - 150, "px");
187
+ styles.top = "".concat(inputRect.top, "px");
188
+ break;
189
+ case 'right':
190
+ styles.left = "".concat(inputRect.right, "px");
191
+ styles.top = "".concat(inputRect.top, "px");
192
+ break;
193
+ default:
194
+ break;
195
+ }
196
+ return ReactDOM.createPortal(React.createElement("div", { className: "suggestion-container ".concat(suggestionListClassName || ''), style: styles },
197
+ React.createElement("ul", { className: "suggestion-list", ref: suggestionListRef }, suggestions.map(function (user) { return (React.createElement("li", { key: user.id, onClick: function () { return handleSuggestionClick(user); }, className: "suggestion-item ".concat(suggestionItemClassName || ''), role: "option", tabIndex: 0, "aria-selected": "false" },
198
+ React.createElement("div", { className: "user-icon" }, getInitials(user === null || user === void 0 ? void 0 : user.name)),
199
+ React.createElement("span", { className: "user-name" }, user.name))); }))), window.document.body);
200
+ };
201
+ var handleSuggestionClick = function (user) {
202
+ if (!inputRef.current)
203
+ return;
204
+ var plainText = inputValue;
205
+ var caretOffset = caretOffsetRef.current;
206
+ var mentionMatch = plainText.slice(0, caretOffset).match(/@(\S*)$/);
207
+ if (!userSelectListRef.current.includes(user.name)) {
208
+ userSelectListRef.current.push(user.name);
209
+ }
210
+ // Check if the ID is already stored
211
+ var isIdExists = userSelectListWithIdsRef.current.some(function (item) { return item.id === user.id; });
212
+ if (!isIdExists) {
213
+ userSelectListWithIdsRef.current.push(user);
214
+ }
215
+ if (!mentionMatch)
216
+ return;
217
+ var mentionIndex = plainText.slice(0, caretOffset).lastIndexOf("@");
218
+ // Append space after the mention
219
+ var newValue = plainText.substring(0, mentionIndex + 1) + user.name + " " + plainText.substring(caretOffset);
220
+ setInputValue(newValue);
221
+ inputRef.current.innerText = newValue;
222
+ // Highlight mentions and links with &nbsp;
223
+ var htmlWithHighlights = highlightMentionsAndLinks(newValue);
224
+ // Set highlighted content
225
+ inputRef.current.innerHTML = htmlWithHighlights;
226
+ setShowSuggestions(false);
227
+ // Adjust caret position after adding the mention and space
228
+ var mentionEnd = mentionIndex + user.name.length + 1;
229
+ restoreCaretPosition(inputRef.current, mentionEnd + 1); // +1 for the space
230
+ };
231
+ var handleImageSelect = function (event) { return __awaiter(void 0, void 0, void 0, function () {
232
+ var files, file;
233
+ return __generator(this, function (_a) {
234
+ switch (_a.label) {
235
+ case 0:
236
+ files = Array.from(event.target.files || []);
237
+ if (!(files.length > 0)) return [3 /*break*/, 2];
238
+ file = files[0];
239
+ if (!file.type.startsWith('image/')) return [3 /*break*/, 2];
240
+ return [4 /*yield*/, uploadImage(file)];
241
+ case 1:
242
+ _a.sent();
243
+ _a.label = 2;
244
+ case 2: return [2 /*return*/];
245
+ }
246
+ });
247
+ }); };
248
+ var handleDragOver = function (e) {
249
+ e.preventDefault();
250
+ e.stopPropagation();
251
+ // Only set dragging if files are being dragged
252
+ if (e.dataTransfer.types.includes('Files')) {
253
+ setIsDraggingOver(true);
254
+ }
255
+ };
256
+ var handleDragLeave = function (e) {
257
+ e.preventDefault();
258
+ e.stopPropagation();
259
+ // Check if we're leaving the container, not just moving between children
260
+ var rect = e.currentTarget.getBoundingClientRect();
261
+ var x = e.clientX;
262
+ var y = e.clientY;
263
+ if (x <= rect.left ||
264
+ x >= rect.right ||
265
+ y <= rect.top ||
266
+ y >= rect.bottom) {
267
+ setIsDraggingOver(false);
268
+ }
269
+ };
270
+ var handleDrop = function (e) { return __awaiter(void 0, void 0, void 0, function () {
271
+ var files, imageFiles;
272
+ return __generator(this, function (_a) {
273
+ switch (_a.label) {
274
+ case 0:
275
+ e.preventDefault();
276
+ e.stopPropagation();
277
+ setIsDraggingOver(false);
278
+ files = Array.from(e.dataTransfer.files);
279
+ if (!(files.length > 0)) return [3 /*break*/, 2];
280
+ imageFiles = files.filter(function (file) { return file.type.startsWith('image/'); });
281
+ if (!(imageFiles.length > 0)) return [3 /*break*/, 2];
282
+ return [4 /*yield*/, uploadImage(imageFiles[0])];
283
+ case 1:
284
+ _a.sent();
285
+ _a.label = 2;
286
+ case 2: return [2 /*return*/];
287
+ }
288
+ });
289
+ }); };
290
+ var uploadImage = function (file) { return __awaiter(void 0, void 0, void 0, function () {
291
+ var url, error_1;
292
+ return __generator(this, function (_a) {
293
+ switch (_a.label) {
294
+ case 0:
295
+ if (!onImageUpload) {
296
+ // If no upload function provided, store the file directly
297
+ setSelectedImage(file);
298
+ setImageUrl(URL.createObjectURL(file));
299
+ return [2 /*return*/];
300
+ }
301
+ _a.label = 1;
302
+ case 1:
303
+ _a.trys.push([1, 3, 4, 5]);
304
+ setIsUploading(true);
305
+ return [4 /*yield*/, onImageUpload(file)];
306
+ case 2:
307
+ url = _a.sent();
308
+ setSelectedImage(file);
309
+ setImageUrl(url);
310
+ return [3 /*break*/, 5];
311
+ case 3:
312
+ error_1 = _a.sent();
313
+ console.error('Error uploading image:', error_1);
314
+ return [3 /*break*/, 5];
315
+ case 4:
316
+ setIsUploading(false);
317
+ return [7 /*endfinally*/];
318
+ case 5: return [2 /*return*/];
319
+ }
320
+ });
321
+ }); };
322
+ var removeImage = function () {
323
+ setSelectedImage(null);
324
+ setImageUrl(null);
325
+ };
326
+ var handleSendMessage = function () {
327
+ if (inputRef.current) {
328
+ var messageText = inputRef.current.innerText.trim();
329
+ var messageHTML = inputRef.current.innerHTML.trim();
330
+ if ((messageText || selectedImage) && onSendMessage) {
331
+ onSendMessage({
332
+ messageText: messageText,
333
+ messageHTML: messageHTML,
334
+ userSelectListWithIds: userSelectListWithIdsRef.current,
335
+ userSelectListName: userSelectListRef.current,
336
+ tags: tagsListRef.current,
337
+ images: selectedImage ? [selectedImage] : [],
338
+ imageUrl: imageUrl
339
+ });
340
+ setInputValue("");
341
+ setShowSuggestions(false);
342
+ inputRef.current.innerText = "";
343
+ setSelectedImage(null);
344
+ setImageUrl(null);
345
+ userSelectListRef.current = [];
346
+ userSelectListWithIdsRef.current = [];
347
+ tagsListRef.current = [];
348
+ }
349
+ }
350
+ };
351
+ var handleKeyDown = function (event) {
352
+ if (event.key === "Enter" && !event.shiftKey) {
353
+ event.preventDefault(); // Prevent newline in content-editable
354
+ handleSendMessage(); // Trigger the same function as the Send button
355
+ }
356
+ };
357
+ return (React.createElement("div", { className: "mention-container ".concat(containerClassName || "") },
358
+ imageUrl && selectedImage && (React.createElement("div", { className: "image-preview-card ".concat(attachedImageContainerClassName || ""), style: attachedImageContainerStyle },
359
+ React.createElement("img", { src: imageUrl, alt: "Preview", className: imgClassName || "", style: imgStyle }),
360
+ React.createElement("button", { onClick: removeImage, className: "remove-image-btn", "aria-label": "Remove image" }, "\u00D7"))),
361
+ React.createElement("div", { className: "mention-input-container ".concat(inputContainerClassName || "", " ").concat(isDraggingOver ? 'dragging-over' : ''), onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDragEnd: function () { return setIsDraggingOver(false); }, onDrop: handleDrop },
362
+ isDraggingOver && (React.createElement("div", { className: "drag-overlay" },
363
+ React.createElement("div", { className: "drag-message" },
364
+ React.createElement("span", null, "Drop to upload")))),
365
+ React.createElement("button", { onClick: function () { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, className: "attachment-button", type: "button", "aria-label": "Attach image" },
366
+ React.createElement("span", { className: "attachment-icon" }, attachmentButtonIcon || "📷")),
367
+ React.createElement("div", { className: "mention-input-wrapper" },
368
+ (!inputValue || !inputRef.current || ((_b = inputRef.current) === null || _b === void 0 ? void 0 : _b.innerText.trim()) === "") && (React.createElement("span", { className: "placeholder" }, placeholder)),
369
+ 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 || "➤"),
371
+ React.createElement("input", { type: "file", ref: fileInputRef, accept: "image/*", onChange: handleImageSelect, style: { display: 'none' } }),
372
+ isUploading && (React.createElement("div", { className: "upload-loading" },
373
+ React.createElement("span", null, "Uploading...")))),
374
+ renderSuggestions()));
375
+ };
376
+ export default MentionInput;
@@ -0,0 +1,41 @@
1
+ import React, { CSSProperties, ReactNode } from "react";
2
+ import "./ShowMessageCard.css";
3
+ interface MessageCardProps {
4
+ [key: string]: any;
5
+ }
6
+ interface ShowMessageCardProps {
7
+ data: MessageCardProps[];
8
+ nameKey?: string;
9
+ dateKey?: string;
10
+ commentKey?: string;
11
+ imgSrcKey?: string;
12
+ imageUrlKey?: string;
13
+ itemNameKey?: string;
14
+ containerClassName?: string;
15
+ containerStyle?: CSSProperties;
16
+ cardClassName?: string;
17
+ cardStyle?: CSSProperties;
18
+ headerClassName?: string;
19
+ headerStyle?: CSSProperties;
20
+ imgClassName?: string;
21
+ imgStyle?: CSSProperties;
22
+ infoClassName?: string;
23
+ infoStyle?: CSSProperties;
24
+ nameClassName?: string;
25
+ nameStyle?: CSSProperties;
26
+ dateClassName?: string;
27
+ dateStyle?: CSSProperties;
28
+ bodyClassName?: string;
29
+ bodyStyle?: CSSProperties;
30
+ commentClassName?: string;
31
+ commentStyle?: CSSProperties;
32
+ attachedImageClassName?: string;
33
+ attachedImageStyle?: CSSProperties;
34
+ attachedImageContainerClassName?: string;
35
+ attachedImageContainerStyle?: CSSProperties;
36
+ itemNameClassName?: string;
37
+ itemNameStyle?: CSSProperties;
38
+ renderItem?: (element: MessageCardProps) => ReactNode;
39
+ }
40
+ export declare const ShowMessageCard: React.FC<ShowMessageCardProps>;
41
+ export {};
@@ -0,0 +1,71 @@
1
+ var __assign = (this && this.__assign) || function () {
2
+ __assign = Object.assign || function(t) {
3
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
4
+ s = arguments[i];
5
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
6
+ t[p] = s[p];
7
+ }
8
+ return t;
9
+ };
10
+ return __assign.apply(this, arguments);
11
+ };
12
+ import React, { useState } from "react";
13
+ import "./ShowMessageCard.css";
14
+ export var ShowMessageCard = function (_a) {
15
+ var data = _a.data, _b = _a.nameKey, nameKey = _b === void 0 ? "name" : _b, _c = _a.dateKey, dateKey = _c === void 0 ? "date" : _c, _d = _a.commentKey, commentKey = _d === void 0 ? "comment" : _d, _e = _a.imgSrcKey, imgSrcKey = _e === void 0 ? "imgSrc" : _e, _f = _a.imageUrlKey, imageUrlKey = _f === void 0 ? "imageUrl" : _f, // Default key for attached image
16
+ _g = _a.itemNameKey, // Default key for attached image
17
+ itemNameKey = _g === void 0 ? "name" : _g, // Default key for item identifier
18
+ 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, itemNameClassName = _a.itemNameClassName, itemNameStyle = _a.itemNameStyle, renderItem = _a.renderItem;
19
+ // State to manage initials for images that fail to load
20
+ var _h = useState({}), initialsState = _h[0], setInitialsState = _h[1];
21
+ // Handle image load failure
22
+ var handleImageError = function (id) {
23
+ setInitialsState(function (prevState) {
24
+ var _a;
25
+ return (__assign(__assign({}, prevState), (_a = {}, _a[id] = true, _a)));
26
+ });
27
+ };
28
+ // Helper function to generate initials from the name
29
+ var getInitials = function (name) {
30
+ var nameParts = name.split(" ");
31
+ var initials = nameParts
32
+ .map(function (part) { var _a; return ((_a = part[0]) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || ""; }) // Take the first letter of each part
33
+ .slice(0, 2) // Limit to 2 letters
34
+ .join("");
35
+ return initials;
36
+ };
37
+ // Helper function to extract hashtags and mentions from text
38
+ var extractTagsAndMentions = function (text) {
39
+ var plainText = text.replace(/<[^>]*>/g, ''); // Remove HTML tags to get plain text
40
+ var hashtags = plainText.match(/#[\w]+/g) || [];
41
+ var mentions = plainText.match(/@[\w\s-]+/g) || [];
42
+ return {
43
+ hashtags: Array.from(new Set(hashtags)), // Remove duplicates
44
+ mentions: Array.from(new Set(mentions.map(function (mention) { return mention.trim(); }))) // Remove duplicates and trim
45
+ };
46
+ };
47
+ return (React.createElement("div", { className: "message-card-container ".concat(containerClassName || ""), style: containerStyle }, data.map(function (item, index) {
48
+ if (renderItem !== undefined) {
49
+ // Use custom render function if provided
50
+ return (React.createElement(React.Fragment, { key: item.id || index }, renderItem(item)));
51
+ }
52
+ var showInitials = initialsState[item.id || index] || !item[imgSrcKey]; // Decide whether to show initials
53
+ // Extract tags and mentions from the comment
54
+ var _a = extractTagsAndMentions(item[commentKey] || ''), hashtags = _a.hashtags, mentions = _a.mentions;
55
+ return (React.createElement("div", { key: item.id || index, className: "message-card ".concat(cardClassName || ""), style: cardStyle },
56
+ React.createElement("div", { className: "message-card-header ".concat(headerClassName || ""), style: headerStyle },
57
+ React.createElement("div", { className: "message-card-header-left" },
58
+ 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); } })),
59
+ React.createElement("div", { className: "message-card-info ".concat(infoClassName || ""), style: infoStyle },
60
+ React.createElement("h3", { className: "message-card-name ".concat(nameClassName || ""), style: nameStyle }, item[nameKey]),
61
+ React.createElement("p", { className: "message-card-date ".concat(dateClassName || ""), style: dateStyle }, item[dateKey]))),
62
+ item[itemNameKey] && (React.createElement("div", { className: "message-card-item-name ".concat(itemNameClassName || ""), style: itemNameStyle }, item[itemNameKey]))),
63
+ React.createElement("div", { className: "message-card-body ".concat(bodyClassName || ""), style: bodyStyle },
64
+ React.createElement("p", { className: "message-card-comment ".concat(commentClassName || ""), style: commentStyle, dangerouslySetInnerHTML: { __html: item[commentKey] } }),
65
+ (item === null || item === void 0 ? void 0 : item[imageUrlKey]) && (React.createElement("div", { className: "message-card-attached-image-container ".concat(attachedImageContainerClassName || ""), style: attachedImageContainerStyle },
66
+ React.createElement("img", { src: item[imageUrlKey], alt: "Attached", className: "message-card-attached-image ".concat(attachedImageClassName || ""), style: attachedImageStyle }))),
67
+ (hashtags.length > 0 || mentions.length > 0) && (React.createElement("div", { className: "message-card-tags" },
68
+ hashtags.map(function (tag, tagIndex) { return (React.createElement("span", { key: "hashtag-".concat(tagIndex), className: "tag-chip hashtag-chip" }, tag)); }),
69
+ mentions.map(function (mention, mentionIndex) { return (React.createElement("span", { key: "mention-".concat(mentionIndex), className: "tag-chip mention-chip" }, mention)); }))))));
70
+ })));
71
+ };
@@ -0,0 +1,2 @@
1
+ export { default as MentionInput } from './MentionInput';
2
+ export { ShowMessageCard } from './ShowMessageCard';
@@ -0,0 +1,2 @@
1
+ export { default as MentionInput } from './MentionInput';
2
+ export { ShowMessageCard } from './ShowMessageCard';
@@ -0,0 +1,2 @@
1
+ declare const _default: import("vite").UserConfig;
2
+ export default _default;
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+ export default defineConfig({
4
+ plugins: [react()],
5
+ server: {
6
+ port: 3000,
7
+ open: true
8
+ }
9
+ });
package/index.html ADDED
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>React Mention Input Demo</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/main.tsx"></script>
11
+ </body>
12
+ </html>
package/main.tsx ADDED
@@ -0,0 +1,9 @@
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import Demo from './demo'
4
+
5
+ ReactDOM.createRoot(document.getElementById('root')!).render(
6
+ <React.StrictMode>
7
+ <Demo />
8
+ </React.StrictMode>,
9
+ )
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "react-mention-input",
3
- "version": "1.1.15",
3
+ "version": "1.1.17",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "scripts": {
7
7
  "test": "echo \"Error: no test specified\" && exit 1",
8
8
  "build": "tsc && npm run copy-css",
9
- "copy-css": "copyfiles -u 1 src/**/*.css dist"
9
+ "copy-css": "copyfiles -u 1 src/**/*.css dist",
10
+ "dev": "vite",
11
+ "demo": "vite"
10
12
  },
11
13
  "keywords": [
12
14
  "mention",
@@ -27,13 +29,15 @@
27
29
  "@types/node": "^22.9.0",
28
30
  "@types/react": "^18.3.12",
29
31
  "@types/react-dom": "^18.3.1",
32
+ "@vitejs/plugin-react": "^4.6.0",
30
33
  "copyfiles": "^2.4.1",
31
34
  "csstype": "^3.1.3",
32
35
  "sass": "^1.81.0",
33
- "typescript": "^5.6.3"
36
+ "typescript": "^5.6.3",
37
+ "vite": "^7.0.0"
34
38
  },
35
39
  "peerDependencies": {
36
- "react": "^17.0.0 || ^18.0.0",
37
- "react-dom": "^17.0.0 || ^18.0.0"
40
+ "react": "^18.3.1",
41
+ "react-dom": "^18.3.1"
38
42
  }
39
43
  }
@@ -193,6 +193,14 @@
193
193
  font-weight: 500;
194
194
  }
195
195
 
196
+ .hashtag-highlight {
197
+ background-color: rgba(255, 165, 0, 0.1);
198
+ color: #FF8C00;
199
+ border-radius: 4px;
200
+ padding: 1px 4px;
201
+ font-weight: 500;
202
+ }
203
+
196
204
  .link-highlight {
197
205
  color: #2684FF;
198
206
  text-decoration: none;
@@ -27,6 +27,7 @@ interface MentionInputProps {
27
27
  messageHTML: string;
28
28
  userSelectListWithIds: { id: number; name: string }[];
29
29
  userSelectListName: string[];
30
+ tags: string[];
30
31
  images?: File[];
31
32
  imageUrl?: string | null;
32
33
  }) => void;
@@ -68,17 +69,29 @@ const MentionInput: React.FC<MentionInputProps> = ({
68
69
  const caretOffsetRef = useRef<number>(0);
69
70
  const userSelectListRef = useRef<string[]>([]); // Only unique names
70
71
  const userSelectListWithIdsRef = useRef<{ id: number; name: string }[]>([]); // Unique IDs with names
72
+ const tagsListRef = useRef<string[]>([]); // Store hashtags
71
73
  const fileInputRef = useRef<HTMLInputElement>(null);
72
74
 
73
75
  const highlightMentionsAndLinks = (text: string): string => {
74
76
  // Regular expression for detecting links
75
77
  const linkRegex = /(https?:\/\/[^\s]+)/g;
78
+
79
+ // Regular expression for detecting hashtags
80
+ const hashtagRegex = /#[\w]+/g;
76
81
 
77
82
  // Highlight links
78
83
  let highlightedText = text.replace(
79
84
  linkRegex,
80
85
  '<a href="$1" target="_blank" rel="noopener noreferrer" class="link-highlight">$1</a>'
81
86
  );
87
+
88
+ // Highlight hashtags
89
+ highlightedText = highlightedText.replace(
90
+ hashtagRegex,
91
+ (match) => {
92
+ return `<span class="hashtag-highlight">${match}</span>`;
93
+ }
94
+ );
82
95
 
83
96
  // Highlight mentions manually based on `userSelectListRef`
84
97
  userSelectListRef?.current.forEach((userName) => {
@@ -160,8 +173,17 @@ const MentionInput: React.FC<MentionInputProps> = ({
160
173
  setShowSuggestions(false);
161
174
  }
162
175
 
163
- // Only apply highlighting if we have mentions or links to highlight
164
- if (userSelectListRef.current.length > 0 || plainText.match(/(https?:\/\/[^\s]+)/g)) {
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)) {
165
187
  const currentHTML = inputRef.current.innerHTML;
166
188
  const htmlWithHighlights = highlightMentionsAndLinks(plainText);
167
189
 
@@ -373,6 +395,7 @@ const MentionInput: React.FC<MentionInputProps> = ({
373
395
  messageHTML,
374
396
  userSelectListWithIds: userSelectListWithIdsRef.current,
375
397
  userSelectListName: userSelectListRef.current,
398
+ tags: tagsListRef.current,
376
399
  images: selectedImage ? [selectedImage] : [],
377
400
  imageUrl: imageUrl
378
401
  });
@@ -383,6 +406,7 @@ const MentionInput: React.FC<MentionInputProps> = ({
383
406
  setImageUrl(null);
384
407
  userSelectListRef.current = [];
385
408
  userSelectListWithIdsRef.current = [];
409
+ tagsListRef.current = [];
386
410
  }
387
411
  }
388
412
  };
@@ -394,8 +418,6 @@ const MentionInput: React.FC<MentionInputProps> = ({
394
418
  }
395
419
  };
396
420
 
397
- console.log(inputValue, inputRef.current?.innerText.trim(), "inputValue====")
398
-
399
421
  return (
400
422
  <div className={`mention-container ${containerClassName || ""}`}>
401
423
  {imageUrl && selectedImage && (
@@ -21,9 +21,25 @@
21
21
  .message-card-header {
22
22
  display: flex;
23
23
  align-items: center;
24
+ justify-content: space-between;
24
25
  margin-bottom: 8px; /* Space between header and body */
25
26
  }
26
27
 
28
+ .message-card-header-left {
29
+ display: flex;
30
+ align-items: center;
31
+ }
32
+
33
+ .message-card-item-name {
34
+ font-size: 14px;
35
+ color: #666;
36
+ font-weight: 500;
37
+ background-color: #f5f5f5;
38
+ padding: 4px 8px;
39
+ border-radius: 6px;
40
+ border: 1px solid #e0e0e0;
41
+ }
42
+
27
43
  .message-card-img,
28
44
  .message-card-initials {
29
45
  width: 48px;
@@ -72,4 +88,40 @@
72
88
  color: #007bff;
73
89
  padding: 2px 4px;
74
90
  border-radius: 4px;
91
+ }
92
+
93
+ .hashtag-highlight {
94
+ background-color: rgba(255, 165, 0, 0.15);
95
+ color: #FF8C00;
96
+ padding: 2px 4px;
97
+ border-radius: 4px;
98
+ font-weight: 500;
99
+ }
100
+
101
+ /* Tag chips styling */
102
+ .message-card-tags {
103
+ display: flex;
104
+ flex-wrap: wrap;
105
+ gap: 8px;
106
+ margin-top: 12px;
107
+ }
108
+
109
+ .tag-chip {
110
+ padding: 6px 12px;
111
+ border-radius: 16px;
112
+ font-size: 12px;
113
+ font-weight: 500;
114
+ border: none;
115
+ display: inline-block;
116
+ white-space: nowrap;
117
+ }
118
+
119
+ .hashtag-chip {
120
+ background-color: #D4A574;
121
+ color: #FFFFFF;
122
+ }
123
+
124
+ .mention-chip {
125
+ background-color: #2684FF;
126
+ color: #FFFFFF;
75
127
  }
@@ -12,6 +12,7 @@ interface ShowMessageCardProps {
12
12
  commentKey?: string; // Custom key for comment
13
13
  imgSrcKey?: string; // Custom key for image source
14
14
  imageUrlKey?: string; // Custom key for attached image URL
15
+ objectNameKey?: string; // Custom key for object identifier (top-right)
15
16
  containerClassName?: string; // Class for the outermost container
16
17
  containerStyle?: CSSProperties; // Style for the outermost container
17
18
  cardClassName?: string; // Class for the card
@@ -34,6 +35,8 @@ interface ShowMessageCardProps {
34
35
  attachedImageStyle?: CSSProperties; // Style for the attached image
35
36
  attachedImageContainerClassName?: string; // Class for the attached image container
36
37
  attachedImageContainerStyle?: CSSProperties; // Style for the attached image container
38
+ objectNameClassName?: string; // Class for the object name (top-right)
39
+ objectNameStyle?: CSSProperties; // Style for the object name (top-right)
37
40
  renderItem?: (element: MessageCardProps) => ReactNode; // Custom render function
38
41
  }
39
42
 
@@ -44,6 +47,7 @@ export const ShowMessageCard: React.FC<ShowMessageCardProps> = ({
44
47
  commentKey = "comment",
45
48
  imgSrcKey = "imgSrc",
46
49
  imageUrlKey = "imageUrl", // Default key for attached image
50
+ objectNameKey = "objectName", // Default key for object identifier
47
51
  containerClassName,
48
52
  containerStyle,
49
53
  cardClassName,
@@ -66,8 +70,12 @@ export const ShowMessageCard: React.FC<ShowMessageCardProps> = ({
66
70
  attachedImageStyle,
67
71
  attachedImageContainerClassName,
68
72
  attachedImageContainerStyle,
73
+ objectNameClassName,
74
+ objectNameStyle,
69
75
  renderItem, // Custom render function
70
76
  }) => {
77
+
78
+ console.log(data, "data==");
71
79
  // State to manage initials for images that fail to load
72
80
  const [initialsState, setInitialsState] = useState<{ [key: string]: boolean }>(
73
81
  {}
@@ -91,6 +99,18 @@ export const ShowMessageCard: React.FC<ShowMessageCardProps> = ({
91
99
  return initials;
92
100
  };
93
101
 
102
+ // Helper function to extract hashtags and mentions from text
103
+ const extractTagsAndMentions = (text: string) => {
104
+ const plainText = text.replace(/<[^>]*>/g, ''); // Remove HTML tags to get plain text
105
+ const hashtags = plainText.match(/#[\w]+/g) || [];
106
+ const mentions = plainText.match(/@[\w\s-]+/g) || [];
107
+
108
+ return {
109
+ hashtags: Array.from(new Set(hashtags)), // Remove duplicates
110
+ mentions: Array.from(new Set(mentions.map(mention => mention.trim()))) // Remove duplicates and trim
111
+ };
112
+ };
113
+
94
114
  return (
95
115
  <div
96
116
  className={`message-card-container ${containerClassName || ""}`}
@@ -107,6 +127,9 @@ export const ShowMessageCard: React.FC<ShowMessageCardProps> = ({
107
127
  }
108
128
 
109
129
  const showInitials = initialsState[item.id || index] || !item[imgSrcKey]; // Decide whether to show initials
130
+
131
+ // Extract tags and mentions from the comment
132
+ const { hashtags, mentions } = extractTagsAndMentions(item[commentKey] || '');
110
133
 
111
134
  return (
112
135
  <div
@@ -118,39 +141,51 @@ export const ShowMessageCard: React.FC<ShowMessageCardProps> = ({
118
141
  className={`message-card-header ${headerClassName || ""}`}
119
142
  style={headerStyle}
120
143
  >
121
- {showInitials ? (
144
+ <div className="message-card-header-left">
145
+ {showInitials ? (
146
+ <div
147
+ className={`message-card-initials ${imgClassName || ""}`}
148
+ style={imgStyle}
149
+ >
150
+ {getInitials(item[nameKey])}
151
+ </div>
152
+ ) : (
153
+ <img
154
+ src={item[imgSrcKey]}
155
+ alt={item[nameKey]}
156
+ className={`message-card-img ${imgClassName || ""}`}
157
+ style={imgStyle}
158
+ onError={() => handleImageError(item.id || index)} // Pass card id or index
159
+ />
160
+ )}
122
161
  <div
123
- className={`message-card-initials ${imgClassName || ""}`}
124
- style={imgStyle}
162
+ className={`message-card-info ${infoClassName || ""}`}
163
+ style={infoStyle}
125
164
  >
126
- {getInitials(item[nameKey])}
165
+ <h3
166
+ className={`message-card-name ${nameClassName || ""}`}
167
+ style={nameStyle}
168
+ >
169
+ {item[nameKey]}
170
+ </h3>
171
+ <p
172
+ className={`message-card-date ${dateClassName || ""}`}
173
+ style={dateStyle}
174
+ >
175
+ {item[dateKey]}
176
+ </p>
127
177
  </div>
128
- ) : (
129
- <img
130
- src={item[imgSrcKey]}
131
- alt={item[nameKey]}
132
- className={`message-card-img ${imgClassName || ""}`}
133
- style={imgStyle}
134
- onError={() => handleImageError(item.id || index)} // Pass card id or index
135
- />
136
- )}
137
- <div
138
- className={`message-card-info ${infoClassName || ""}`}
139
- style={infoStyle}
140
- >
141
- <h3
142
- className={`message-card-name ${nameClassName || ""}`}
143
- style={nameStyle}
144
- >
145
- {item[nameKey]}
146
- </h3>
147
- <p
148
- className={`message-card-date ${dateClassName || ""}`}
149
- style={dateStyle}
150
- >
151
- {item[dateKey]}
152
- </p>
153
178
  </div>
179
+
180
+ {/* Object identifier in top-right corner */}
181
+ {item[objectNameKey] && (
182
+ <div
183
+ className={`message-card-item-name ${objectNameClassName || ""}`}
184
+ style={objectNameStyle}
185
+ >
186
+ {item[objectNameKey]}
187
+ </div>
188
+ )}
154
189
  </div>
155
190
  <div
156
191
  className={`message-card-body ${bodyClassName || ""}`}
@@ -176,6 +211,22 @@ export const ShowMessageCard: React.FC<ShowMessageCardProps> = ({
176
211
  />
177
212
  </div>
178
213
  )}
214
+
215
+ {/* Display hashtags and mentions as chips */}
216
+ {(hashtags.length > 0 || mentions.length > 0) && (
217
+ <div className="message-card-tags">
218
+ {hashtags.map((tag, tagIndex) => (
219
+ <span key={`hashtag-${tagIndex}`} className="tag-chip hashtag-chip">
220
+ {tag}
221
+ </span>
222
+ ))}
223
+ {mentions.map((mention, mentionIndex) => (
224
+ <span key={`mention-${mentionIndex}`} className="tag-chip mention-chip">
225
+ {mention}
226
+ </span>
227
+ ))}
228
+ </div>
229
+ )}
179
230
  </div>
180
231
  </div>
181
232
  );
package/vite.config.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ port: 3000,
8
+ open: true
9
+ }
10
+ })