fix: @ mention popup appears above cursor with correct positioning
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m47s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m47s
- Popup now appears above the cursor line (matching reference design) - Header changed to "可能@的内容" - @ character stays visible while popup is open, removed on mention select - All references shown in popup (no dedup filtering) - Fixed zero-rect fallback using temp marker for empty editor Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7b804df30f
commit
dd7b693c0b
@ -50,7 +50,7 @@
|
||||
color: rgba(0, 184, 230, 0.9);
|
||||
}
|
||||
|
||||
/* Mention popup */
|
||||
/* Mention popup — appears above cursor */
|
||||
.mentionPopup {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
@ -61,12 +61,13 @@
|
||||
min-width: 200px;
|
||||
max-width: 280px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
animation: fadeIn 0.12s ease;
|
||||
transform: translateY(-100%);
|
||||
animation: fadeInUp 0.12s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(-100%) translateY(4px); }
|
||||
to { opacity: 1; transform: translateY(-100%); }
|
||||
}
|
||||
|
||||
.mentionHeader {
|
||||
|
||||
@ -20,6 +20,7 @@ export function PromptInput() {
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
const [showMentionPopup, setShowMentionPopup] = useState(false);
|
||||
const [mentionPos, setMentionPos] = useState({ top: 0, left: 0 });
|
||||
const typedAtRef = useRef(false); // tracks if popup was triggered by typing @
|
||||
const [hoverRef, setHoverRef] = useState<UploadedFile | null>(null);
|
||||
const [hoverPos, setHoverPos] = useState({ top: 0, left: 0 });
|
||||
|
||||
@ -41,6 +42,7 @@ export function PromptInput() {
|
||||
useEffect(() => {
|
||||
if (insertAtTrigger === 0) return;
|
||||
if (references.length === 0) return;
|
||||
typedAtRef.current = false;
|
||||
openMentionPopup();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [insertAtTrigger]);
|
||||
@ -51,16 +53,33 @@ export function PromptInput() {
|
||||
el.focus();
|
||||
|
||||
const sel = window.getSelection();
|
||||
if (!sel || sel.rangeCount === 0) return;
|
||||
|
||||
const range = sel.getRangeAt(0);
|
||||
const rect = range.getBoundingClientRect();
|
||||
const editorRect = el.getBoundingClientRect();
|
||||
|
||||
setMentionPos({
|
||||
top: rect.bottom - editorRect.top + 4,
|
||||
left: Math.max(0, rect.left - editorRect.left),
|
||||
});
|
||||
// Position popup above cursor line
|
||||
let top = 0; // fallback: top of editor
|
||||
let left = 0;
|
||||
|
||||
if (sel && sel.rangeCount > 0) {
|
||||
const range = sel.getRangeAt(0);
|
||||
const rect = range.getBoundingClientRect();
|
||||
|
||||
if (rect.height > 0) {
|
||||
top = rect.top - editorRect.top - 8;
|
||||
left = Math.max(0, rect.left - editorRect.left);
|
||||
} else {
|
||||
// Collapsed range returns zero rect — use temp marker
|
||||
const marker = document.createElement('span');
|
||||
marker.textContent = '\u200B';
|
||||
range.insertNode(marker);
|
||||
const mRect = marker.getBoundingClientRect();
|
||||
top = mRect.top - editorRect.top - 8;
|
||||
left = Math.max(0, mRect.left - editorRect.left);
|
||||
marker.remove();
|
||||
el.normalize();
|
||||
}
|
||||
}
|
||||
|
||||
setMentionPos({ top, left });
|
||||
setShowMentionPopup(true);
|
||||
}, []);
|
||||
|
||||
@ -84,18 +103,8 @@ export function PromptInput() {
|
||||
const text = node.textContent || '';
|
||||
const offset = range.startOffset;
|
||||
if (offset > 0 && text[offset - 1] === '@' && references.length > 0) {
|
||||
// Remove the typed @
|
||||
const before = text.substring(0, offset - 1);
|
||||
const after = text.substring(offset);
|
||||
node.textContent = before + after;
|
||||
|
||||
// Restore cursor
|
||||
const newRange = document.createRange();
|
||||
newRange.setStart(node, before.length);
|
||||
newRange.collapse(true);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(newRange);
|
||||
|
||||
// Keep the @ visible, open popup above it
|
||||
typedAtRef.current = true;
|
||||
openMentionPopup();
|
||||
}
|
||||
}, [extractText, references.length, openMentionPopup]);
|
||||
@ -110,6 +119,23 @@ export function PromptInput() {
|
||||
if (!sel || sel.rangeCount === 0) return;
|
||||
|
||||
const range = sel.getRangeAt(0);
|
||||
|
||||
// If triggered by typing @, remove the @ character first
|
||||
if (typedAtRef.current) {
|
||||
typedAtRef.current = false;
|
||||
const node = range.startContainer;
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
const text = node.textContent || '';
|
||||
const offset = range.startOffset;
|
||||
const atIdx = text.lastIndexOf('@', offset - 1);
|
||||
if (atIdx >= 0) {
|
||||
node.textContent = text.substring(0, atIdx) + text.substring(offset);
|
||||
range.setStart(node, atIdx);
|
||||
range.collapse(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
range.deleteContents();
|
||||
|
||||
// Create mention span
|
||||
@ -184,21 +210,6 @@ export function PromptInput() {
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, [showMentionPopup]);
|
||||
|
||||
// Compute available (un-mentioned) references
|
||||
const getMentionedIds = () => {
|
||||
const ids = new Set<string>();
|
||||
const el = editorRef.current;
|
||||
if (el) {
|
||||
el.querySelectorAll('[data-ref-id]').forEach((span) => {
|
||||
const id = (span as HTMLElement).dataset.refId;
|
||||
if (id) ids.add(id);
|
||||
});
|
||||
}
|
||||
return ids;
|
||||
};
|
||||
const mentionedIds = getMentionedIds();
|
||||
const availableRefs = references.filter((r) => !mentionedIds.has(r.id));
|
||||
|
||||
const isEmpty = !prompt.trim() && !editorHtml;
|
||||
|
||||
return (
|
||||
@ -219,15 +230,15 @@ export function PromptInput() {
|
||||
/>
|
||||
|
||||
{/* Mention popup */}
|
||||
{showMentionPopup && availableRefs.length > 0 && (
|
||||
{showMentionPopup && references.length > 0 && (
|
||||
<div
|
||||
className={styles.mentionPopup}
|
||||
style={{ top: mentionPos.top, left: mentionPos.left }}
|
||||
>
|
||||
<div className={styles.mentionHeader}>选择引用素材</div>
|
||||
{availableRefs.map((ref) => (
|
||||
<div className={styles.mentionHeader}>可能@的内容</div>
|
||||
{references.map((ref, idx) => (
|
||||
<button
|
||||
key={ref.id}
|
||||
key={`${ref.id}-${idx}`}
|
||||
className={styles.mentionItem}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user