mentionize 0.0.1
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/LICENSE +21 -0
- package/README.md +188 -0
- package/dist/cjs/MentionDropdown.d.ts +16 -0
- package/dist/cjs/MentionHighlighter.d.ts +12 -0
- package/dist/cjs/MentionInput.d.ts +3 -0
- package/dist/cjs/index.d.ts +6 -0
- package/dist/cjs/index.js +881 -0
- package/dist/cjs/index.js.map +15 -0
- package/dist/cjs/types.d.ts +77 -0
- package/dist/cjs/useCaretPosition.d.ts +5 -0
- package/dist/cjs/useMentionEngine.d.ts +33 -0
- package/dist/cjs/utils.d.ts +1 -0
- package/dist/esm/MentionDropdown.d.ts +16 -0
- package/dist/esm/MentionHighlighter.d.ts +12 -0
- package/dist/esm/MentionInput.d.ts +3 -0
- package/dist/esm/index.d.ts +6 -0
- package/dist/esm/index.js +844 -0
- package/dist/esm/index.js.map +15 -0
- package/dist/esm/types.d.ts +77 -0
- package/dist/esm/useCaretPosition.d.ts +5 -0
- package/dist/esm/useMentionEngine.d.ts +33 -0
- package/dist/esm/utils.d.ts +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
// src/MentionInput.tsx
|
|
2
|
+
import {
|
|
3
|
+
forwardRef,
|
|
4
|
+
useCallback as useCallback4,
|
|
5
|
+
useEffect as useEffect4,
|
|
6
|
+
useImperativeHandle,
|
|
7
|
+
useLayoutEffect as useLayoutEffect2,
|
|
8
|
+
useRef as useRef5,
|
|
9
|
+
useState as useState2
|
|
10
|
+
} from "react";
|
|
11
|
+
|
|
12
|
+
// src/MentionDropdown.tsx
|
|
13
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
14
|
+
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
15
|
+
var MentionDropdown = ({
|
|
16
|
+
items,
|
|
17
|
+
trigger,
|
|
18
|
+
highlightedIndex,
|
|
19
|
+
onHighlight,
|
|
20
|
+
onSelect,
|
|
21
|
+
onLoadMore,
|
|
22
|
+
loading,
|
|
23
|
+
position,
|
|
24
|
+
width,
|
|
25
|
+
className
|
|
26
|
+
}) => {
|
|
27
|
+
const listRef = useRef(null);
|
|
28
|
+
const sentinelRef = useRef(null);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!onLoadMore || !sentinelRef.current)
|
|
31
|
+
return;
|
|
32
|
+
const sentinel = sentinelRef.current;
|
|
33
|
+
const observer = new IntersectionObserver((entries) => {
|
|
34
|
+
if (entries[0]?.isIntersecting) {
|
|
35
|
+
onLoadMore();
|
|
36
|
+
}
|
|
37
|
+
}, { root: listRef.current, threshold: 0.1 });
|
|
38
|
+
observer.observe(sentinel);
|
|
39
|
+
return () => observer.disconnect();
|
|
40
|
+
}, [onLoadMore]);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const container = listRef.current;
|
|
43
|
+
if (!container)
|
|
44
|
+
return;
|
|
45
|
+
const highlighted = container.querySelector(`[data-mentionize-option-index="${highlightedIndex}"]`);
|
|
46
|
+
if (highlighted) {
|
|
47
|
+
highlighted.scrollIntoView({ block: "nearest" });
|
|
48
|
+
}
|
|
49
|
+
}, [highlightedIndex]);
|
|
50
|
+
const maxHeight = 240;
|
|
51
|
+
const gap = 4;
|
|
52
|
+
const spaceBelow = window.innerHeight - position.top - gap;
|
|
53
|
+
const flipAbove = spaceBelow < maxHeight && position.top > maxHeight;
|
|
54
|
+
const style = {
|
|
55
|
+
position: "fixed",
|
|
56
|
+
width,
|
|
57
|
+
maxHeight,
|
|
58
|
+
overflowY: "auto",
|
|
59
|
+
zIndex: 50,
|
|
60
|
+
...flipAbove ? { bottom: window.innerHeight - position.top + gap, left: position.left } : { top: position.top + gap, left: position.left }
|
|
61
|
+
};
|
|
62
|
+
const handleMouseDown = useCallback((e) => {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
}, []);
|
|
65
|
+
if (!items.length && !loading)
|
|
66
|
+
return null;
|
|
67
|
+
return /* @__PURE__ */ jsxDEV("div", {
|
|
68
|
+
ref: listRef,
|
|
69
|
+
role: "listbox",
|
|
70
|
+
className,
|
|
71
|
+
style,
|
|
72
|
+
"data-mentionize-dropdown": "",
|
|
73
|
+
onMouseDown: handleMouseDown,
|
|
74
|
+
children: [
|
|
75
|
+
items.map((item, i) => {
|
|
76
|
+
const isHighlighted = i === highlightedIndex;
|
|
77
|
+
if (trigger.renderOption) {
|
|
78
|
+
return /* @__PURE__ */ jsxDEV("div", {
|
|
79
|
+
role: "option",
|
|
80
|
+
"aria-selected": isHighlighted,
|
|
81
|
+
"data-mentionize-option-index": i,
|
|
82
|
+
"data-mentionize-option-highlighted": isHighlighted || undefined,
|
|
83
|
+
onMouseEnter: () => onHighlight(i),
|
|
84
|
+
onClick: () => onSelect(item),
|
|
85
|
+
children: trigger.renderOption(item, isHighlighted)
|
|
86
|
+
}, i, false, undefined, this);
|
|
87
|
+
}
|
|
88
|
+
return /* @__PURE__ */ jsxDEV("div", {
|
|
89
|
+
role: "option",
|
|
90
|
+
"aria-selected": isHighlighted,
|
|
91
|
+
"data-mentionize-option-index": i,
|
|
92
|
+
"data-mentionize-option": "",
|
|
93
|
+
"data-mentionize-option-highlighted": isHighlighted || undefined,
|
|
94
|
+
onMouseEnter: () => onHighlight(i),
|
|
95
|
+
onClick: () => onSelect(item),
|
|
96
|
+
children: trigger.displayText(item)
|
|
97
|
+
}, i, false, undefined, this);
|
|
98
|
+
}),
|
|
99
|
+
loading && /* @__PURE__ */ jsxDEV("div", {
|
|
100
|
+
"data-mentionize-loading": "",
|
|
101
|
+
children: "Loading..."
|
|
102
|
+
}, undefined, false, undefined, this),
|
|
103
|
+
onLoadMore && !loading && /* @__PURE__ */ jsxDEV("div", {
|
|
104
|
+
ref: sentinelRef,
|
|
105
|
+
style: { height: 1 },
|
|
106
|
+
"aria-hidden": true
|
|
107
|
+
}, undefined, false, undefined, this)
|
|
108
|
+
]
|
|
109
|
+
}, undefined, true, undefined, this);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// src/MentionHighlighter.tsx
|
|
113
|
+
import React2, { useEffect as useEffect2, useLayoutEffect, useMemo, useRef as useRef2 } from "react";
|
|
114
|
+
function textToNodes(text) {
|
|
115
|
+
const parts = text.split(`
|
|
116
|
+
`);
|
|
117
|
+
const nodes = [];
|
|
118
|
+
for (let i = 0;i < parts.length; i++) {
|
|
119
|
+
if (i > 0)
|
|
120
|
+
nodes.push(React2.createElement("br", { key: `br-${i}` }));
|
|
121
|
+
if (parts[i])
|
|
122
|
+
nodes.push(parts[i]);
|
|
123
|
+
}
|
|
124
|
+
return nodes;
|
|
125
|
+
}
|
|
126
|
+
var MentionHighlighter = ({
|
|
127
|
+
visible,
|
|
128
|
+
mentions,
|
|
129
|
+
triggers,
|
|
130
|
+
textareaRef,
|
|
131
|
+
className,
|
|
132
|
+
style
|
|
133
|
+
}) => {
|
|
134
|
+
const ref = useRef2(null);
|
|
135
|
+
useEffect2(() => {
|
|
136
|
+
const textarea = textareaRef.current;
|
|
137
|
+
const highlighter = ref.current;
|
|
138
|
+
if (!textarea || !highlighter)
|
|
139
|
+
return;
|
|
140
|
+
const onScroll = () => {
|
|
141
|
+
highlighter.scrollTop = textarea.scrollTop;
|
|
142
|
+
highlighter.scrollLeft = textarea.scrollLeft;
|
|
143
|
+
};
|
|
144
|
+
textarea.addEventListener("scroll", onScroll);
|
|
145
|
+
return () => textarea.removeEventListener("scroll", onScroll);
|
|
146
|
+
}, [textareaRef]);
|
|
147
|
+
const children = useMemo(() => {
|
|
148
|
+
const sorted = mentions.slice().sort((a, b) => a.start - b.start);
|
|
149
|
+
if (!sorted.length)
|
|
150
|
+
return textToNodes(visible);
|
|
151
|
+
const classMap = new Map;
|
|
152
|
+
for (const t of triggers) {
|
|
153
|
+
classMap.set(t.trigger, t.mentionClassName ?? "mentionize-mention");
|
|
154
|
+
}
|
|
155
|
+
const nodes = [];
|
|
156
|
+
let last = 0;
|
|
157
|
+
for (let i = 0;i < sorted.length; i++) {
|
|
158
|
+
const m = sorted[i];
|
|
159
|
+
if (last < m.start) {
|
|
160
|
+
nodes.push(...textToNodes(visible.slice(last, m.start)));
|
|
161
|
+
}
|
|
162
|
+
const mentionText = visible.slice(m.start, m.end);
|
|
163
|
+
const cls = classMap.get(m.trigger) ?? "mentionize-mention";
|
|
164
|
+
nodes.push(React2.createElement("span", {
|
|
165
|
+
key: `mention-${i}`,
|
|
166
|
+
className: cls,
|
|
167
|
+
"data-mentionize-trigger": m.trigger,
|
|
168
|
+
"data-mentionize-key": m.key
|
|
169
|
+
}, mentionText));
|
|
170
|
+
last = m.end;
|
|
171
|
+
}
|
|
172
|
+
if (last < visible.length) {
|
|
173
|
+
nodes.push(...textToNodes(visible.slice(last)));
|
|
174
|
+
}
|
|
175
|
+
return nodes;
|
|
176
|
+
}, [visible, mentions, triggers]);
|
|
177
|
+
useLayoutEffect(() => {
|
|
178
|
+
const el = ref.current;
|
|
179
|
+
if (!el)
|
|
180
|
+
return;
|
|
181
|
+
const spans = el.querySelectorAll("[data-mentionize-trigger]");
|
|
182
|
+
for (let i = 0;i < spans.length; i++) {
|
|
183
|
+
const span = spans[i];
|
|
184
|
+
span.style.marginLeft = "";
|
|
185
|
+
span.style.marginRight = "";
|
|
186
|
+
const cs = getComputedStyle(span);
|
|
187
|
+
const extraLeft = parseFloat(cs.paddingLeft) + parseFloat(cs.borderLeftWidth) + parseFloat(cs.marginLeft);
|
|
188
|
+
const extraRight = parseFloat(cs.paddingRight) + parseFloat(cs.borderRightWidth) + parseFloat(cs.marginRight);
|
|
189
|
+
if (extraLeft)
|
|
190
|
+
span.style.marginLeft = `${-extraLeft}px`;
|
|
191
|
+
if (extraRight)
|
|
192
|
+
span.style.marginRight = `${-extraRight}px`;
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
return React2.createElement("div", {
|
|
196
|
+
ref,
|
|
197
|
+
className,
|
|
198
|
+
style: {
|
|
199
|
+
position: "absolute",
|
|
200
|
+
inset: 0,
|
|
201
|
+
pointerEvents: "none",
|
|
202
|
+
overflow: "auto",
|
|
203
|
+
...style
|
|
204
|
+
},
|
|
205
|
+
"aria-hidden": true,
|
|
206
|
+
"data-mentionize-highlighter": ""
|
|
207
|
+
}, ...children);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// src/useCaretPosition.ts
|
|
211
|
+
import { useCallback as useCallback2, useRef as useRef3 } from "react";
|
|
212
|
+
var SHARED_STYLE_PROPS = [
|
|
213
|
+
"whiteSpace",
|
|
214
|
+
"overflowWrap",
|
|
215
|
+
"wordBreak",
|
|
216
|
+
"padding",
|
|
217
|
+
"paddingTop",
|
|
218
|
+
"paddingRight",
|
|
219
|
+
"paddingBottom",
|
|
220
|
+
"paddingLeft",
|
|
221
|
+
"fontFamily",
|
|
222
|
+
"fontSize",
|
|
223
|
+
"fontWeight",
|
|
224
|
+
"lineHeight",
|
|
225
|
+
"letterSpacing",
|
|
226
|
+
"tabSize",
|
|
227
|
+
"boxSizing",
|
|
228
|
+
"borderWidth",
|
|
229
|
+
"borderStyle"
|
|
230
|
+
];
|
|
231
|
+
function useCaretPosition(dropdownWidth) {
|
|
232
|
+
const mirrorRef = useRef3(null);
|
|
233
|
+
const getCaretPosition = useCallback2((textarea, caretIndex, textOverride) => {
|
|
234
|
+
const mirror = mirrorRef.current;
|
|
235
|
+
if (!mirror)
|
|
236
|
+
return null;
|
|
237
|
+
const caret = caretIndex ?? textarea.selectionStart;
|
|
238
|
+
const source = textOverride ?? textarea.value;
|
|
239
|
+
const before = source.slice(0, caret);
|
|
240
|
+
const computed = getComputedStyle(textarea);
|
|
241
|
+
for (const prop of SHARED_STYLE_PROPS) {
|
|
242
|
+
mirror.style[prop] = computed[prop];
|
|
243
|
+
}
|
|
244
|
+
mirror.style.width = `${textarea.offsetWidth}px`;
|
|
245
|
+
mirror.textContent = before;
|
|
246
|
+
const span = document.createElement("span");
|
|
247
|
+
span.textContent = "";
|
|
248
|
+
mirror.appendChild(span);
|
|
249
|
+
mirror.scrollTop = textarea.scrollTop;
|
|
250
|
+
const spanRect = span.getBoundingClientRect();
|
|
251
|
+
const mirrorRect = mirror.getBoundingClientRect();
|
|
252
|
+
const textareaRect = textarea.getBoundingClientRect();
|
|
253
|
+
const top = textareaRect.top + (spanRect.top - mirrorRect.top) - textarea.scrollTop + span.offsetHeight;
|
|
254
|
+
let left = textareaRect.left + (spanRect.left - mirrorRect.left);
|
|
255
|
+
if (left + dropdownWidth > window.innerWidth - 8) {
|
|
256
|
+
left = window.innerWidth - dropdownWidth - 8;
|
|
257
|
+
}
|
|
258
|
+
if (left < 8)
|
|
259
|
+
left = 8;
|
|
260
|
+
mirror.innerHTML = "";
|
|
261
|
+
return {
|
|
262
|
+
top: Math.min(Math.max(top, 8), window.innerHeight - 8),
|
|
263
|
+
left
|
|
264
|
+
};
|
|
265
|
+
}, [dropdownWidth]);
|
|
266
|
+
return { mirrorRef, getCaretPosition };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/useMentionEngine.ts
|
|
270
|
+
import { useCallback as useCallback3, useEffect as useEffect3, useMemo as useMemo2, useRef as useRef4, useState } from "react";
|
|
271
|
+
|
|
272
|
+
// src/utils.ts
|
|
273
|
+
function escapeRegex(s) {
|
|
274
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/useMentionEngine.ts
|
|
278
|
+
function detectMentionsInText(visibleText, triggers, getCache) {
|
|
279
|
+
const all = [];
|
|
280
|
+
for (const t of triggers) {
|
|
281
|
+
const triggerChar = t.trigger;
|
|
282
|
+
if (!visibleText.includes(triggerChar))
|
|
283
|
+
continue;
|
|
284
|
+
const cache = getCache(triggerChar);
|
|
285
|
+
const knownItems = [];
|
|
286
|
+
if (t.options) {
|
|
287
|
+
for (const item of t.options) {
|
|
288
|
+
knownItems.push({
|
|
289
|
+
displayText: t.displayText(item),
|
|
290
|
+
key: getItemKey(t, item),
|
|
291
|
+
item
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
for (const [key, item] of cache.entries()) {
|
|
296
|
+
if (item !== null) {
|
|
297
|
+
const dt = t.displayText(item);
|
|
298
|
+
if (!knownItems.some((k) => k.key === key)) {
|
|
299
|
+
knownItems.push({ displayText: dt, key, item });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (!knownItems.length)
|
|
304
|
+
continue;
|
|
305
|
+
const compiled = knownItems.map((ki) => {
|
|
306
|
+
const pat = "^" + escapeRegex(ki.displayText).replace(/\s+/g, "\\s+");
|
|
307
|
+
return { ...ki, re: new RegExp(pat, "i") };
|
|
308
|
+
});
|
|
309
|
+
const positions = [];
|
|
310
|
+
let idx = visibleText.indexOf(triggerChar);
|
|
311
|
+
while (idx !== -1) {
|
|
312
|
+
positions.push(idx);
|
|
313
|
+
idx = visibleText.indexOf(triggerChar, idx + 1);
|
|
314
|
+
}
|
|
315
|
+
const candidates = [];
|
|
316
|
+
for (const pos of positions) {
|
|
317
|
+
const after = visibleText.slice(pos + triggerChar.length);
|
|
318
|
+
for (const c of compiled) {
|
|
319
|
+
const match = c.re.exec(after);
|
|
320
|
+
if (!match)
|
|
321
|
+
continue;
|
|
322
|
+
const matched = match[0];
|
|
323
|
+
const end = pos + triggerChar.length + matched.length;
|
|
324
|
+
const next = visibleText[end];
|
|
325
|
+
if (next && !/[\s,.:;!?)}\]]/.test(next))
|
|
326
|
+
continue;
|
|
327
|
+
candidates.push({
|
|
328
|
+
trigger: triggerChar,
|
|
329
|
+
displayText: matched,
|
|
330
|
+
key: c.key,
|
|
331
|
+
start: pos,
|
|
332
|
+
end
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
candidates.sort((a, b) => a.start !== b.start ? a.start - b.start : b.end - b.start - (a.end - a.start));
|
|
337
|
+
for (const c of candidates) {
|
|
338
|
+
const overlaps = all.some((f) => Math.max(f.start, c.start) < Math.min(f.end, c.end));
|
|
339
|
+
if (!overlaps)
|
|
340
|
+
all.push(c);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return all;
|
|
344
|
+
}
|
|
345
|
+
function mentionsEqual(a, b) {
|
|
346
|
+
if (a.length !== b.length)
|
|
347
|
+
return false;
|
|
348
|
+
for (let i = 0;i < a.length; i++) {
|
|
349
|
+
const ai = a[i];
|
|
350
|
+
const bi = b[i];
|
|
351
|
+
if (ai.start !== bi.start || ai.end !== bi.end || ai.key !== bi.key)
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
function useMentionEngine(options) {
|
|
357
|
+
const {
|
|
358
|
+
triggers,
|
|
359
|
+
value: controlledValue,
|
|
360
|
+
defaultValue,
|
|
361
|
+
onChange,
|
|
362
|
+
onMentionsChange
|
|
363
|
+
} = options;
|
|
364
|
+
const lastRawRef = useRef4(controlledValue ?? defaultValue ?? "");
|
|
365
|
+
const suppressEmitRef = useRef4(false);
|
|
366
|
+
const caretPosRef = useRef4(null);
|
|
367
|
+
const prevMentionsRef = useRef4([]);
|
|
368
|
+
const cacheRef = useRef4(new Map);
|
|
369
|
+
function getCache(triggerChar) {
|
|
370
|
+
let map = cacheRef.current.get(triggerChar);
|
|
371
|
+
if (!map) {
|
|
372
|
+
map = new Map;
|
|
373
|
+
cacheRef.current.set(triggerChar, map);
|
|
374
|
+
}
|
|
375
|
+
return map;
|
|
376
|
+
}
|
|
377
|
+
useEffect3(() => {
|
|
378
|
+
for (const t of triggers) {
|
|
379
|
+
if (t.options) {
|
|
380
|
+
const cache = getCache(t.trigger);
|
|
381
|
+
for (const item of t.options) {
|
|
382
|
+
const serialized = t.serialize(item);
|
|
383
|
+
const re = new RegExp(t.pattern.source, t.pattern.flags.replace("g", ""));
|
|
384
|
+
const m = re.exec(serialized);
|
|
385
|
+
if (m) {
|
|
386
|
+
const { key } = t.parseMatch(m);
|
|
387
|
+
cache.set(key, item);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}, [triggers]);
|
|
393
|
+
const rawToVisible = useCallback3((raw) => {
|
|
394
|
+
let result = raw;
|
|
395
|
+
for (const t of triggers) {
|
|
396
|
+
const globalRe = new RegExp(t.pattern.source, t.pattern.flags.includes("g") ? t.pattern.flags : t.pattern.flags + "g");
|
|
397
|
+
const parts = [];
|
|
398
|
+
let lastIndex = 0;
|
|
399
|
+
let m;
|
|
400
|
+
globalRe.lastIndex = 0;
|
|
401
|
+
while ((m = globalRe.exec(result)) !== null) {
|
|
402
|
+
parts.push(result.slice(lastIndex, m.index));
|
|
403
|
+
const parsed = t.parseMatch(m);
|
|
404
|
+
const cache = getCache(t.trigger);
|
|
405
|
+
if (!cache.has(parsed.key)) {
|
|
406
|
+
cache.set(parsed.key, null);
|
|
407
|
+
}
|
|
408
|
+
parts.push(t.trigger + parsed.displayText);
|
|
409
|
+
lastIndex = m.index + m[0].length;
|
|
410
|
+
if (!t.pattern.flags.includes("g"))
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
parts.push(result.slice(lastIndex));
|
|
414
|
+
result = parts.join("");
|
|
415
|
+
}
|
|
416
|
+
return result;
|
|
417
|
+
}, [triggers]);
|
|
418
|
+
const [visible, setVisible] = useState(() => rawToVisible(controlledValue ?? defaultValue ?? ""));
|
|
419
|
+
const mentions = useMemo2(() => {
|
|
420
|
+
const newMentions = detectMentionsInText(visible, triggers, getCache);
|
|
421
|
+
if (mentionsEqual(prevMentionsRef.current, newMentions)) {
|
|
422
|
+
return prevMentionsRef.current;
|
|
423
|
+
}
|
|
424
|
+
prevMentionsRef.current = newMentions;
|
|
425
|
+
return newMentions;
|
|
426
|
+
}, [visible, triggers]);
|
|
427
|
+
const visibleToRaw = useCallback3((vis, mentionsList) => {
|
|
428
|
+
if (!mentionsList.length)
|
|
429
|
+
return vis;
|
|
430
|
+
const ordered = mentionsList.slice().sort((a, b) => a.start - b.start);
|
|
431
|
+
let raw = "";
|
|
432
|
+
let last = 0;
|
|
433
|
+
for (const m of ordered) {
|
|
434
|
+
raw += vis.slice(last, m.start);
|
|
435
|
+
const t = triggers.find((tr) => tr.trigger === m.trigger);
|
|
436
|
+
if (t) {
|
|
437
|
+
const cache = getCache(t.trigger);
|
|
438
|
+
const item = cache.get(m.key);
|
|
439
|
+
if (item !== null && item !== undefined) {
|
|
440
|
+
raw += t.serialize(item);
|
|
441
|
+
} else {
|
|
442
|
+
raw += vis.slice(m.start, m.end);
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
raw += vis.slice(m.start, m.end);
|
|
446
|
+
}
|
|
447
|
+
last = m.end;
|
|
448
|
+
}
|
|
449
|
+
raw += vis.slice(last);
|
|
450
|
+
return raw;
|
|
451
|
+
}, [triggers]);
|
|
452
|
+
const emitSync = useCallback3((newVisible) => {
|
|
453
|
+
const newMentions = detectMentionsInText(newVisible, triggers, getCache);
|
|
454
|
+
if (!mentionsEqual(prevMentionsRef.current, newMentions)) {
|
|
455
|
+
prevMentionsRef.current = newMentions;
|
|
456
|
+
}
|
|
457
|
+
const raw = visibleToRaw(newVisible, prevMentionsRef.current);
|
|
458
|
+
if (raw !== lastRawRef.current) {
|
|
459
|
+
lastRawRef.current = raw;
|
|
460
|
+
onChange?.(raw);
|
|
461
|
+
}
|
|
462
|
+
onMentionsChange?.(prevMentionsRef.current);
|
|
463
|
+
}, [triggers, visibleToRaw, onChange, onMentionsChange]);
|
|
464
|
+
useEffect3(() => {
|
|
465
|
+
if (controlledValue === undefined)
|
|
466
|
+
return;
|
|
467
|
+
if (controlledValue === lastRawRef.current)
|
|
468
|
+
return;
|
|
469
|
+
lastRawRef.current = controlledValue;
|
|
470
|
+
suppressEmitRef.current = true;
|
|
471
|
+
setVisible(rawToVisible(controlledValue));
|
|
472
|
+
}, [controlledValue, rawToVisible]);
|
|
473
|
+
const [activeTrigger, setActiveTrigger] = useState(null);
|
|
474
|
+
const [searchState, setSearchState] = useState({
|
|
475
|
+
items: [],
|
|
476
|
+
page: 0,
|
|
477
|
+
hasMore: false,
|
|
478
|
+
loading: false
|
|
479
|
+
});
|
|
480
|
+
const [highlightIndex, setHighlightIndex] = useState(0);
|
|
481
|
+
const searchAbortRef = useRef4(null);
|
|
482
|
+
const detectActiveTrigger = useCallback3((text, caretPos) => {
|
|
483
|
+
const prefix = text.slice(0, caretPos);
|
|
484
|
+
for (const t of triggers) {
|
|
485
|
+
const triggerChar = t.trigger;
|
|
486
|
+
const re = new RegExp(escapeRegex(triggerChar) + "([^\\n" + escapeRegex(triggerChar) + "]*)$");
|
|
487
|
+
const match = re.exec(prefix);
|
|
488
|
+
if (match && match[1] !== undefined) {
|
|
489
|
+
const query = match[1];
|
|
490
|
+
if (/\s/.test(query))
|
|
491
|
+
continue;
|
|
492
|
+
return {
|
|
493
|
+
trigger: t,
|
|
494
|
+
query,
|
|
495
|
+
startPos: match.index
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return null;
|
|
500
|
+
}, [triggers]);
|
|
501
|
+
const filteredOptions = useMemo2(() => {
|
|
502
|
+
if (!activeTrigger)
|
|
503
|
+
return [];
|
|
504
|
+
const t = activeTrigger.trigger;
|
|
505
|
+
const q = activeTrigger.query.toLowerCase();
|
|
506
|
+
if (t.onSearch) {
|
|
507
|
+
return searchState.items;
|
|
508
|
+
}
|
|
509
|
+
if (t.options) {
|
|
510
|
+
if (!q)
|
|
511
|
+
return t.options;
|
|
512
|
+
return t.options.filter((item) => t.displayText(item).toLowerCase().includes(q));
|
|
513
|
+
}
|
|
514
|
+
return [];
|
|
515
|
+
}, [activeTrigger, searchState.items]);
|
|
516
|
+
useEffect3(() => {
|
|
517
|
+
if (!activeTrigger?.trigger.onSearch)
|
|
518
|
+
return;
|
|
519
|
+
const t = activeTrigger.trigger;
|
|
520
|
+
const query = activeTrigger.query;
|
|
521
|
+
searchAbortRef.current?.abort();
|
|
522
|
+
const controller = new AbortController;
|
|
523
|
+
searchAbortRef.current = controller;
|
|
524
|
+
setSearchState((s) => ({ ...s, loading: true, page: 0 }));
|
|
525
|
+
const timer = setTimeout(async () => {
|
|
526
|
+
try {
|
|
527
|
+
const result = await t.onSearch(query, 0);
|
|
528
|
+
if (controller.signal.aborted)
|
|
529
|
+
return;
|
|
530
|
+
setSearchState({
|
|
531
|
+
items: result.items,
|
|
532
|
+
page: 0,
|
|
533
|
+
hasMore: result.hasMore,
|
|
534
|
+
loading: false
|
|
535
|
+
});
|
|
536
|
+
} catch {
|
|
537
|
+
if (controller.signal.aborted)
|
|
538
|
+
return;
|
|
539
|
+
setSearchState((s) => ({ ...s, loading: false }));
|
|
540
|
+
}
|
|
541
|
+
}, 150);
|
|
542
|
+
return () => {
|
|
543
|
+
clearTimeout(timer);
|
|
544
|
+
controller.abort();
|
|
545
|
+
};
|
|
546
|
+
}, [activeTrigger?.trigger, activeTrigger?.query]);
|
|
547
|
+
const loadMore = useCallback3(async () => {
|
|
548
|
+
if (!activeTrigger?.trigger.onSearch || searchState.loading || !searchState.hasMore)
|
|
549
|
+
return;
|
|
550
|
+
const t = activeTrigger.trigger;
|
|
551
|
+
const nextPage = searchState.page + 1;
|
|
552
|
+
setSearchState((s) => ({ ...s, loading: true }));
|
|
553
|
+
try {
|
|
554
|
+
const result = await t.onSearch(activeTrigger.query, nextPage);
|
|
555
|
+
setSearchState((s) => ({
|
|
556
|
+
items: [...s.items, ...result.items],
|
|
557
|
+
page: nextPage,
|
|
558
|
+
hasMore: result.hasMore,
|
|
559
|
+
loading: false
|
|
560
|
+
}));
|
|
561
|
+
} catch {
|
|
562
|
+
setSearchState((s) => ({ ...s, loading: false }));
|
|
563
|
+
}
|
|
564
|
+
}, [activeTrigger, searchState]);
|
|
565
|
+
const handleTextChange = useCallback3((newText, caretPos) => {
|
|
566
|
+
caretPosRef.current = caretPos;
|
|
567
|
+
setVisible(newText);
|
|
568
|
+
emitSync(newText);
|
|
569
|
+
const detected = detectActiveTrigger(newText, caretPos);
|
|
570
|
+
if (detected) {
|
|
571
|
+
setActiveTrigger(detected);
|
|
572
|
+
setHighlightIndex(0);
|
|
573
|
+
} else {
|
|
574
|
+
setActiveTrigger(null);
|
|
575
|
+
}
|
|
576
|
+
}, [detectActiveTrigger, emitSync]);
|
|
577
|
+
const selectOption = useCallback3((item, textarea) => {
|
|
578
|
+
if (!activeTrigger)
|
|
579
|
+
return;
|
|
580
|
+
const t = activeTrigger.trigger;
|
|
581
|
+
const cache = getCache(t.trigger);
|
|
582
|
+
const key = getItemKey(t, item);
|
|
583
|
+
cache.set(key, item);
|
|
584
|
+
const displayText = t.displayText(item);
|
|
585
|
+
const mentionText = t.trigger + displayText;
|
|
586
|
+
const before = visible.slice(0, activeTrigger.startPos);
|
|
587
|
+
const after = visible.slice(textarea.selectionStart);
|
|
588
|
+
const newVis = before + mentionText + " " + after;
|
|
589
|
+
const pos = before.length + mentionText.length + 1;
|
|
590
|
+
caretPosRef.current = pos;
|
|
591
|
+
setVisible(newVis);
|
|
592
|
+
emitSync(newVis);
|
|
593
|
+
setActiveTrigger(null);
|
|
594
|
+
}, [activeTrigger, visible, emitSync]);
|
|
595
|
+
const closeSuggestions = useCallback3(() => {
|
|
596
|
+
setActiveTrigger(null);
|
|
597
|
+
setSearchState({ items: [], page: 0, hasMore: false, loading: false });
|
|
598
|
+
}, []);
|
|
599
|
+
const handleKeyDown = useCallback3((e, textarea) => {
|
|
600
|
+
if (!activeTrigger)
|
|
601
|
+
return false;
|
|
602
|
+
const len = filteredOptions.length;
|
|
603
|
+
if (!len && !searchState.loading)
|
|
604
|
+
return false;
|
|
605
|
+
if (e.key === "ArrowDown") {
|
|
606
|
+
e.preventDefault();
|
|
607
|
+
const next = (highlightIndex + 1) % Math.max(len, 1);
|
|
608
|
+
setHighlightIndex(next);
|
|
609
|
+
if (len - 1 - next <= 3 && searchState.hasMore && !searchState.loading) {
|
|
610
|
+
loadMore();
|
|
611
|
+
}
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
if (e.key === "ArrowUp") {
|
|
615
|
+
e.preventDefault();
|
|
616
|
+
setHighlightIndex((highlightIndex - 1 + Math.max(len, 1)) % Math.max(len, 1));
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
if (e.key === "Enter") {
|
|
620
|
+
e.preventDefault();
|
|
621
|
+
const item = filteredOptions[highlightIndex];
|
|
622
|
+
if (item)
|
|
623
|
+
selectOption(item, textarea);
|
|
624
|
+
return true;
|
|
625
|
+
}
|
|
626
|
+
if (e.key === "Escape") {
|
|
627
|
+
e.preventDefault();
|
|
628
|
+
closeSuggestions();
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
return false;
|
|
632
|
+
}, [
|
|
633
|
+
activeTrigger,
|
|
634
|
+
filteredOptions,
|
|
635
|
+
highlightIndex,
|
|
636
|
+
searchState,
|
|
637
|
+
selectOption,
|
|
638
|
+
closeSuggestions,
|
|
639
|
+
loadMore
|
|
640
|
+
]);
|
|
641
|
+
return {
|
|
642
|
+
visible,
|
|
643
|
+
setVisible,
|
|
644
|
+
mentions,
|
|
645
|
+
activeTrigger,
|
|
646
|
+
filteredOptions,
|
|
647
|
+
highlightIndex,
|
|
648
|
+
setHighlightIndex,
|
|
649
|
+
searchLoading: searchState.loading,
|
|
650
|
+
searchHasMore: searchState.hasMore,
|
|
651
|
+
handleTextChange,
|
|
652
|
+
handleKeyDown,
|
|
653
|
+
selectOption,
|
|
654
|
+
closeSuggestions,
|
|
655
|
+
loadMore,
|
|
656
|
+
rawToVisible,
|
|
657
|
+
visibleToRaw,
|
|
658
|
+
caretPosRef
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
function getItemKey(trigger, item) {
|
|
662
|
+
const serialized = trigger.serialize(item);
|
|
663
|
+
const re = new RegExp(trigger.pattern.source, trigger.pattern.flags.replace("g", ""));
|
|
664
|
+
const m = re.exec(serialized);
|
|
665
|
+
if (m) {
|
|
666
|
+
return trigger.parseMatch(m).key;
|
|
667
|
+
}
|
|
668
|
+
return serialized;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// src/MentionInput.tsx
|
|
672
|
+
import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
|
|
673
|
+
var SHARED_STYLE = {
|
|
674
|
+
whiteSpace: "pre-wrap",
|
|
675
|
+
overflowWrap: "anywhere",
|
|
676
|
+
wordBreak: "break-word",
|
|
677
|
+
padding: "0.5rem 0.75rem",
|
|
678
|
+
fontFamily: "inherit",
|
|
679
|
+
fontSize: "inherit",
|
|
680
|
+
lineHeight: "inherit",
|
|
681
|
+
letterSpacing: "normal",
|
|
682
|
+
boxSizing: "border-box"
|
|
683
|
+
};
|
|
684
|
+
var MentionInput = forwardRef(({
|
|
685
|
+
triggers,
|
|
686
|
+
value,
|
|
687
|
+
defaultValue,
|
|
688
|
+
onChange,
|
|
689
|
+
onMentionsChange,
|
|
690
|
+
placeholder,
|
|
691
|
+
disabled,
|
|
692
|
+
rows = 4,
|
|
693
|
+
className,
|
|
694
|
+
inputClassName,
|
|
695
|
+
highlighterClassName,
|
|
696
|
+
dropdownClassName,
|
|
697
|
+
dropdownWidth = 250,
|
|
698
|
+
renderDropdown,
|
|
699
|
+
"aria-label": ariaLabel,
|
|
700
|
+
"aria-describedby": ariaDescribedBy
|
|
701
|
+
}, ref) => {
|
|
702
|
+
const textareaRef = useRef5(null);
|
|
703
|
+
useImperativeHandle(ref, () => textareaRef.current);
|
|
704
|
+
const engine = useMentionEngine({
|
|
705
|
+
triggers,
|
|
706
|
+
value,
|
|
707
|
+
defaultValue,
|
|
708
|
+
onChange,
|
|
709
|
+
onMentionsChange
|
|
710
|
+
});
|
|
711
|
+
useLayoutEffect2(() => {
|
|
712
|
+
const pos = engine.caretPosRef.current;
|
|
713
|
+
const ta = textareaRef.current;
|
|
714
|
+
if (pos !== null && ta && document.activeElement === ta) {
|
|
715
|
+
ta.setSelectionRange(pos, pos);
|
|
716
|
+
engine.caretPosRef.current = null;
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
const { mirrorRef, getCaretPosition } = useCaretPosition(dropdownWidth);
|
|
720
|
+
const [dropdownPos, setDropdownPos] = useState2(null);
|
|
721
|
+
useEffect4(() => {
|
|
722
|
+
if (engine.activeTrigger && textareaRef.current) {
|
|
723
|
+
requestAnimationFrame(() => {
|
|
724
|
+
const pos = getCaretPosition(textareaRef.current);
|
|
725
|
+
if (pos)
|
|
726
|
+
setDropdownPos(pos);
|
|
727
|
+
});
|
|
728
|
+
} else {
|
|
729
|
+
setDropdownPos(null);
|
|
730
|
+
}
|
|
731
|
+
}, [engine.activeTrigger, engine.visible, getCaretPosition]);
|
|
732
|
+
const handleChange = useCallback4((e) => {
|
|
733
|
+
engine.handleTextChange(e.target.value, e.target.selectionStart);
|
|
734
|
+
}, [engine.handleTextChange]);
|
|
735
|
+
const handleKeyDown = useCallback4((e) => {
|
|
736
|
+
if (textareaRef.current) {
|
|
737
|
+
engine.handleKeyDown(e, textareaRef.current);
|
|
738
|
+
}
|
|
739
|
+
}, [engine.handleKeyDown]);
|
|
740
|
+
const handlePaste = useCallback4((e) => {
|
|
741
|
+
const ta = textareaRef.current;
|
|
742
|
+
if (!ta)
|
|
743
|
+
return;
|
|
744
|
+
const txt = e.clipboardData.getData("text");
|
|
745
|
+
const start = ta.selectionStart;
|
|
746
|
+
const end = ta.selectionEnd;
|
|
747
|
+
const newText = engine.visible.slice(0, start) + txt + engine.visible.slice(end);
|
|
748
|
+
e.preventDefault();
|
|
749
|
+
engine.handleTextChange(newText, start + txt.length);
|
|
750
|
+
}, [engine.visible, engine.handleTextChange]);
|
|
751
|
+
const handleBlur = useCallback4(() => {
|
|
752
|
+
setTimeout(() => engine.closeSuggestions(), 150);
|
|
753
|
+
}, [engine.closeSuggestions]);
|
|
754
|
+
const handleSelect = useCallback4((item) => {
|
|
755
|
+
if (textareaRef.current) {
|
|
756
|
+
engine.selectOption(item, textareaRef.current);
|
|
757
|
+
}
|
|
758
|
+
}, [engine.selectOption]);
|
|
759
|
+
const showDropdown = engine.activeTrigger !== null && dropdownPos !== null;
|
|
760
|
+
return /* @__PURE__ */ jsxDEV2("div", {
|
|
761
|
+
className,
|
|
762
|
+
style: { position: "relative" },
|
|
763
|
+
"data-mentionize-container": "",
|
|
764
|
+
children: [
|
|
765
|
+
/* @__PURE__ */ jsxDEV2(MentionHighlighter, {
|
|
766
|
+
visible: engine.visible,
|
|
767
|
+
mentions: engine.mentions,
|
|
768
|
+
triggers,
|
|
769
|
+
textareaRef,
|
|
770
|
+
className: highlighterClassName,
|
|
771
|
+
style: SHARED_STYLE
|
|
772
|
+
}, undefined, false, undefined, this),
|
|
773
|
+
/* @__PURE__ */ jsxDEV2("textarea", {
|
|
774
|
+
ref: textareaRef,
|
|
775
|
+
value: engine.visible,
|
|
776
|
+
onChange: handleChange,
|
|
777
|
+
onKeyDown: handleKeyDown,
|
|
778
|
+
onPaste: handlePaste,
|
|
779
|
+
onBlur: handleBlur,
|
|
780
|
+
rows,
|
|
781
|
+
placeholder,
|
|
782
|
+
disabled,
|
|
783
|
+
"aria-label": ariaLabel,
|
|
784
|
+
"aria-describedby": ariaDescribedBy,
|
|
785
|
+
"aria-autocomplete": "list",
|
|
786
|
+
"aria-expanded": showDropdown,
|
|
787
|
+
className: inputClassName,
|
|
788
|
+
style: {
|
|
789
|
+
...SHARED_STYLE,
|
|
790
|
+
position: "relative",
|
|
791
|
+
width: "100%",
|
|
792
|
+
resize: "vertical",
|
|
793
|
+
background: "transparent",
|
|
794
|
+
color: "transparent",
|
|
795
|
+
caretColor: "CanvasText",
|
|
796
|
+
zIndex: 10
|
|
797
|
+
},
|
|
798
|
+
"data-mentionize-input": ""
|
|
799
|
+
}, undefined, false, undefined, this),
|
|
800
|
+
/* @__PURE__ */ jsxDEV2("div", {
|
|
801
|
+
ref: mirrorRef,
|
|
802
|
+
"aria-hidden": true,
|
|
803
|
+
style: {
|
|
804
|
+
position: "absolute",
|
|
805
|
+
top: 0,
|
|
806
|
+
left: -9999,
|
|
807
|
+
visibility: "hidden",
|
|
808
|
+
...SHARED_STYLE
|
|
809
|
+
},
|
|
810
|
+
"data-mentionize-mirror": ""
|
|
811
|
+
}, undefined, false, undefined, this),
|
|
812
|
+
showDropdown && engine.activeTrigger && (renderDropdown ? renderDropdown({
|
|
813
|
+
items: engine.filteredOptions,
|
|
814
|
+
highlightedIndex: engine.highlightIndex,
|
|
815
|
+
onSelect: handleSelect,
|
|
816
|
+
onHighlight: engine.setHighlightIndex,
|
|
817
|
+
loading: engine.searchLoading,
|
|
818
|
+
onLoadMore: engine.searchHasMore ? engine.loadMore : undefined
|
|
819
|
+
}) : /* @__PURE__ */ jsxDEV2(MentionDropdown, {
|
|
820
|
+
items: engine.filteredOptions,
|
|
821
|
+
trigger: engine.activeTrigger.trigger,
|
|
822
|
+
highlightedIndex: engine.highlightIndex,
|
|
823
|
+
onHighlight: engine.setHighlightIndex,
|
|
824
|
+
onSelect: handleSelect,
|
|
825
|
+
onLoadMore: engine.searchHasMore ? engine.loadMore : undefined,
|
|
826
|
+
loading: engine.searchLoading,
|
|
827
|
+
position: dropdownPos,
|
|
828
|
+
width: dropdownWidth,
|
|
829
|
+
className: dropdownClassName
|
|
830
|
+
}, undefined, false, undefined, this))
|
|
831
|
+
]
|
|
832
|
+
}, undefined, true, undefined, this);
|
|
833
|
+
});
|
|
834
|
+
MentionInput.displayName = "MentionInput";
|
|
835
|
+
export {
|
|
836
|
+
useMentionEngine,
|
|
837
|
+
useCaretPosition,
|
|
838
|
+
MentionInput,
|
|
839
|
+
MentionHighlighter,
|
|
840
|
+
MentionDropdown
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
//# debugId=436A2E507826B04564756E2164756E21
|
|
844
|
+
//# sourceMappingURL=index.js.map
|