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

ReactJS实现含可替换标签的自由文本输入模板方案问询

Hey, I’ve tackled exactly this kind of template input component before—total headache when you try to roll it all from scratch with a native <input> or <textarea>, right? Let me break down what worked for me, plus some approaches other devs swear by.

The Core Problem with Native Inputs

Native text inputs only handle plain text, so mixing editable text with non-editable "tag" elements forces you to rewrite half the browser’s default behavior (like selection, keydown, and click handling)—which is why you ended up with that tangled logic. The fix? Ditch native inputs and use a contenteditable container instead. It lets you mix regular text nodes and custom non-editable elements seamlessly.

My Go-To Implementation Approach

Here’s a stripped-down, working example that hits all your requirements:

1. Basic HTML/CSS Setup

We’ll use a contenteditable div as the input container, and style tags to look distinct and uneditable:

<div id="templateEditor" contenteditable="true" class="editor-container"></div>

<div class="tag-buttons">
  <button onclick="insertTag('player')">Add [player]</button>
  <button onclick="insertTag('activity')">Add [activity]</button>
  <button onclick="insertTag('day')">Add [day]</button>
</div>
.editor-container {
  border: 1px solid #ddd;
  padding: 10px;
  min-height: 50px;
  border-radius: 4px;
  font-size: 16px;
}

.tag {
  background-color: #e8f0fe;
  padding: 2px 6px;
  border-radius: 3px;
  margin: 0 3px;
  user-select: none;
  pointer-events: none; /* Block direct clicks/edits on the tag */
  color: #1967d2;
}

.tag-buttons {
  margin-top: 10px;
  gap: 8px;
  display: flex;
}

2. JavaScript Logic for Inserting Tags & Handling Events

We’ll use the browser’s Selection API to manage cursor position, and add guards to prevent editing tag content:

function insertTag(tagName) {
  const editor = document.getElementById('templateEditor');
  const selection = window.getSelection();
  
  // Exit if there's no valid selection (edge case)
  if (selection.rangeCount === 0) return;
  
  const range = selection.getRangeAt(0);
  
  // Create the tag element
  const tagSpan = document.createElement('span');
  tagSpan.className = 'tag';
  tagSpan.textContent = `[${tagName}]`;
  tagSpan.dataset.tag = tagName; // Store tag name for later replacement
  
  // Replace any selected text with the tag, then insert it
  range.deleteContents();
  range.insertNode(tagSpan);
  
  // Move the cursor right after the tag so users can keep typing
  range.setStartAfter(tagSpan);
  range.setEndAfter(tagSpan);
  selection.removeAllRanges();
  selection.addRange(range);
}

// Prevent editing or deleting parts of a tag
document.getElementById('templateEditor').addEventListener('keydown', (e) => {
  const selection = window.getSelection();
  const activeElement = selection.focusNode?.parentElement;
  
  // If cursor is inside a tag
  if (activeElement?.classList.contains('tag')) {
    // Allow deleting the entire tag if it's fully selected
    if ((e.key === 'Backspace' || e.key === 'Delete') && selection.toString() === activeElement.textContent) {
      activeElement.remove();
    } else {
      // Block all other edits to the tag
      e.preventDefault();
    }
  }
});

3. Extracting the Template & Replacing Tags

To generate your final content, you can either:

  • Extract a raw template string:
    function getRawTemplate() {
      return document.getElementById('templateEditor').textContent;
    }
    
  • Or get structured data (better for future editing):
    function getStructuredTemplate() {
      const editor = document.getElementById('templateEditor');
      const parts = [];
      
      for (const node of editor.childNodes) {
        if (node.nodeType === Node.TEXT_NODE) {
          parts.push({ type: 'text', content: node.textContent });
        } else if (node.classList.contains('tag')) {
          parts.push({ type: 'tag', name: node.dataset.tag, content: node.textContent });
        }
      }
      return parts;
    }
    

Then replace tags with values using a simple regex or structured replacement:

function replaceTemplateTags(template, replacements) {
  return template.replace(/\[(\w+)\]/g, (match, tag) => replacements[tag] || match);
}

// Example usage:
const rawTemplate = getRawTemplate(); // "I will make [activity] on [day]"
const finalContent = replaceTemplateTags(rawTemplate, {
  activity: 'sandwiches',
  day: 'Monday'
});
console.log(finalContent); // "I will make sandwiches on Monday"

Ready-Made Libraries to Avoid Reinventing the Wheel

If you don’t want to maintain this logic yourself, these tools have built-in support for this exact use case:

  • Quill.js: Define a custom "blot" (Quill’s term for custom elements) for your tags. It handles all cursor and event logic out of the box.
  • Slate.js: A block-based editor that lets you mix editable text blocks with non-editable tag blocks. Great for more complex template needs.
  • Draft.js: Facebook’s rich text editor, which supports custom entity types (your tags would be entities) that can’t be edited directly.

Quick Pro Tips

  • Always test cursor behavior across browsers—contenteditable has minor quirks, but nothing unmanageable.
  • If you need to save the template for later, use the structured data format instead of raw text. It makes re-rendering the editor with existing tags trivial.
  • Add hover effects to tags to make them feel interactive (even if users can’t edit them directly).

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.07 14:37:26