You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在Draft JS中实现无触发符标签自动补全及弹窗位置优化

Let's break down your popup positioning issues first, then move on to better autocomplete patterns for your tag editor.


Fixing Popup Positioning Problems

1. Sync with Draft.js's Native Selection Tools

Your current code uses the browser's native window.getSelection(), which can get out of sync with Draft.js's internal state—especially on the first input, when the DOM hasn't fully updated to reflect the new text. Instead, use Draft.js's built-in getVisibleSelectionRect utility, which is designed to accurately capture the editor's selection position:

import getVisibleSelectionRect from 'draft-js/lib/getVisibleSelectionRect';

// Replace your native selection logic in onChange with this:
const visibleRect = getVisibleSelectionRect(window);
if (this.state.autoComplete.search !== categorySearch && visibleRect) {
  this.setState((prevState: StateType) => {
    const state = {
      autoComplete: {
        active: true,
        search: categorySearch,
        searchMeta: {
          begin: wordMeta.begin,
          end: wordMeta.end,
        },
      },
      selectionRect: prevState.selectionRect,
    };
    if (prevState.autoComplete.active === false) {
      // Use Draft.js's visible rect directly—it includes top/left/width/height
      state.selectionRect = visibleRect;
    }
    return state;
  });
}

2. Wait for DOM Updates Before Measuring

On first input, Draft.js might not have finished rendering the new text to the DOM yet, leading to an incorrect (or missing) selection rect. Add a small delay using requestAnimationFrame to ensure the DOM is up-to-date:

if (prevState.autoComplete.active === false) {
  requestAnimationFrame(() => {
    const updatedRect = getVisibleSelectionRect(window);
    if (updatedRect) {
      this.setState({ selectionRect: updatedRect });
    }
  });
}

3. Fix getWordAt Edge Cases

Your current getWordAt function can miscalculate word boundaries after spaces, which causes the popup to shift right. Adjust the regex logic to more reliably find word edges:

function getWordAt(text: string, pos: number): WordMeta {
  // Find the start of the current word (left to nearest space/start of text)
  const leftMatch = text.slice(0, pos).match(/\S*$/);
  const left = leftMatch ? leftMatch.index : 0;
  
  // Find the end of the current word (right to nearest space/end of text)
  const rightMatch = text.slice(pos).match(/\s|$/);
  const right = rightMatch ? pos + rightMatch.index : text.length;

  return {
    word: text.slice(left, right),
    begin: left,
    end: right,
  };
}

Better Autocomplete Patterns for Tag Editors

1. Use React Portal for Popup Placement

Mount your autocomplete popup to the <body> using React Portal. This avoids positioning conflicts with parent elements that have overflow: hidden or custom positioning, and ensures the popup stays visible even if the editor scrolls:

import ReactDOM from 'react-dom';

// In your component's render method:
{this.state.autoComplete.active && this.state.selectionRect && ReactDOM.createPortal(
  <div 
    className="autocomplete-popup"
    style={{
      position: 'absolute',
      top: `${this.state.selectionRect.bottom}px`,
      left: `${this.state.selectionRect.left}px`,
      zIndex: 1000,
      // Add your styling here
    }}
  >
    {/* Your autocomplete options go here */}
  </div>,
  document.body
)}

2. Debounce Search Updates

Prevent excessive state updates and potential API calls by adding a debounce to your autocomplete logic. This waits until the user stops typing for a short period before updating the popup:

import debounce from 'lodash/debounce';

// In your component constructor:
this.debouncedUpdateAutocomplete = debounce((categorySearch, rect, wordMeta) => {
  this.setState({
    autoComplete: {
      active: true,
      search: categorySearch,
      searchMeta: { begin: wordMeta.begin, end: wordMeta.end },
    },
    selectionRect: rect,
  });
}, 300);

// In your onChange function, replace the setState call with:
if (!existingEntity && matches) {
  const categorySearch = matches[1];
  const visibleRect = getVisibleSelectionRect(window);
  if (this.state.autoComplete.search !== categorySearch && visibleRect) {
    this.debouncedUpdateAutocomplete(categorySearch, visibleRect, wordMeta);
  }
}

3. Centralize Logic with Custom Hooks (If Using Functional Components)

If you're using functional components, extract the autocomplete logic into a custom Hook to keep your code clean and reusable:

import { useState, useEffect } from 'react';
import getVisibleSelectionRect from 'draft-js/lib/getVisibleSelectionRect';

function useTagAutocomplete(editorState) {
  const [autoComplete, setAutoComplete] = useState({ active: false, search: '', searchMeta: null });
  const [selectionRect, setSelectionRect] = useState(null);

  useEffect(() => {
    const content = editorState.getCurrentContent();
    const selection = editorState.getSelection();
    
    if (selection.isCollapsed()) {
      const currentBlock = content.getBlockForKey(selection.getAnchorKey());
      const blockText = currentBlock.getText();
      const wordMeta = getWordAt(blockText, selection.getAnchorOffset());
      const matches = wordMeta.word.match(/([\w]*)/);
      const existingEntity = currentBlock.getEntityAt(wordMeta.begin);

      if (!existingEntity && matches) {
        const categorySearch = matches[1];
        const visibleRect = getVisibleSelectionRect(window);
        setAutoComplete({ active: true, search: categorySearch, searchMeta: wordMeta });
        setSelectionRect(visibleRect);
      } else {
        setAutoComplete(prev => ({ ...prev, active: false }));
      }
    } else {
      setAutoComplete(prev => ({ ...prev, active: false }));
    }
  }, [editorState]);

  return { autoComplete, selectionRect };
}

4. Properly Manage Draft.js Entities

When a user selects an autocomplete option, create a Draft.js Entity to mark the tag as a completed entity. This prevents the autocomplete from triggering again on the same text:

function insertTag(editorState, tagName) {
  const content = editorState.getCurrentContent();
  const selection = editorState.getSelection();
  const currentBlock = content.getBlockForKey(selection.getAnchorKey());
  const wordMeta = getWordAt(currentBlock.getText(), selection.getAnchorOffset());

  // Create a tag entity
  const entityKey = content.createEntity('TAG', 'IMMUTABLE', { name: tagName });
  // Replace the current word with the selected tag
  const newContent = content.replaceText(
    selection.merge({ anchorOffset: wordMeta.begin, focusOffset: wordMeta.end }),
    tagName,
    null,
    entityKey
  );

  return EditorState.push(editorState, newContent, 'replace');
}

内容的提问来源于stack exchange,提问作者Victor

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 07:18:16