You need to enable JavaScript to run this app. 如何实现输入框内可拖拽、不可编辑删除的标签功能
优惠活动
大模型
产品
解决方案
定价
更多

如何实现输入框内可拖拽、不可编辑删除的标签功能

带可拖拽固定标签的输入组件实现方案

该组件无法直接基于原生input/textarea实现,需要使用可编辑DOM容器配合事件监听完成,具体实现逻辑如下:

核心实现逻辑

用contenteditable=true的div作为输入容器,内部包含两类元素:

  • 普通文本节点:支持正常的输入、删除、编辑操作
  • 标签元素:设置contenteditable=false固定不可编辑,绑定拖拽事件支持位置调整

分步实现代码

1. 基础HTML结构

<div class="tag-input-container" contenteditable="true">
  <span class="tag-item" draggable="true" contenteditable="false">示例标签</span>
  请输入普通文本
</div>

2. 基础样式

.tag-input-container {
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  padding: 6px 12px;
  min-height: 32px;
  line-height: 24px;
  outline: none;
  font-size: 14px;
}
.tag-item {
  display: inline-flex;
  align-items: center;
  background: #f0f2f5;
  border: 1px solid #e5e7eb;
  border-radius: 4px;
  padding: 0 8px;
  margin: 0 4px;
  color: #1f2937;
  cursor: grab;
  user-select: none;
}
.tag-item:active {
  cursor: grabbing;
}
/* 禁止标签内部触发交互事件 */
.tag-item * {
  pointer-events: none;
}

3. 标签防修改/删除逻辑

监听输入容器的键盘事件,拦截针对标签的删除、修改操作:

const inputContainer = document.querySelector('.tag-input-container')
// 处理退格/删除按键,避免误删标签
inputContainer.addEventListener('keydown', (e) => {
  const selection = window.getSelection()
  if (!selection.rangeCount) return
  const range = selection.getRangeAt(0)
  
  // 退格时检测前一个节点是不是标签
  if (e.key === 'Backspace' && range.startOffset === 0) {
    const prevNode = getPreviousValidNode(range.startContainer)
    if (prevNode?.classList?.contains('tag-item')) {
      e.preventDefault()
      // 直接将光标移动到标签前面
      moveCursorBefore(prevNode)
    }
  }
  // 删除键时检测后一个节点是不是标签
  if (e.key === 'Delete' && range.startOffset === getTextLength(range.startContainer)) {
    const nextNode = getNextValidNode(range.startContainer)
    if (nextNode?.classList?.contains('tag-item')) {
      e.preventDefault()
      // 直接将光标移动到标签后面
      moveCursorAfter(nextNode)
    }
  }
})
// 辅助函数:获取前一个有效节点
function getPreviousValidNode(node) {
  let prev = node.previousSibling
  while (prev && prev.nodeType === Node.TEXT_NODE && prev.textContent.trim() === '') {
    prev = prev.previousSibling
  }
  return prev
}
// 辅助函数:获取后一个有效节点
function getNextValidNode(node) {
  let next = node.nextSibling
  while (next && next.nodeType === Node.TEXT_NODE && next.textContent.trim() === '') {
    next = next.nextSibling
  }
  return next
}
// 辅助函数:移动光标到节点前
function moveCursorBefore(node) {
  const range = document.createRange()
  const selection = window.getSelection()
  range.setStartBefore(node)
  range.collapse(true)
  selection.removeAllRanges()
  selection.addRange(range)
}
// 辅助函数:移动光标到节点后
function moveCursorAfter(node) {
  const range = document.createRange()
  const selection = window.getSelection()
  range.setStartAfter(node)
  range.collapse(true)
  selection.removeAllRanges()
  selection.addRange(range)
}

4. 标签拖拽调整逻辑

基于HTML5原生拖拽API实现标签位置调整:

let draggedTag = null
// 拖拽开始记录被拖拽的标签
inputContainer.addEventListener('dragstart', (e) => {
  if (e.target.classList.contains('tag-item')) {
    draggedTag = e.target
    e.dataTransfer.effectAllowed = 'move'
    // 加半透明效果提升体验
    setTimeout(() => draggedTag.style.opacity = '0.5', 0)
  }
})
// 拖拽过程中实时调整标签位置
inputContainer.addEventListener('dragover', (e) => {
  e.preventDefault()
  if (!draggedTag) return
  // 获取当前光标位置对应的插入范围
  const range = document.caretRangeFromPoint(e.clientX, e.clientY)
  if (range) {
    range.insertNode(draggedTag)
  }
})
// 拖拽结束恢复状态
inputContainer.addEventListener('dragend', () => {
  if (draggedTag) {
    draggedTag.style.opacity = '1'
    draggedTag = null
  }
})

5. 输入内容提取

需要提交内容时,按顺序拼接标签和普通文本即可:

function getInputContent() {
  const contentArr = []
  inputContainer.childNodes.forEach(node => {
    if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('tag-item')) {
      // 标签可以按业务需要自定义格式,比如存标签ID、名称等
      contentArr.push(`<TAG:${node.dataset.tagId || node.textContent}>`)
    } else if (node.nodeType === Node.TEXT_NODE) {
      contentArr.push(node.textContent)
    }
  })
  return contentArr.join('')
}

体验优化点

  • 粘贴内容时监听paste事件,过滤掉非法HTML,只保留纯文本和合法标签
  • 中文输入时监听compositionstart和compositionend事件,避免输入拼音过程中触发键盘拦截逻辑
  • 标签较多时可以设置容器自动换行,适配多行输入场景

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.15 14:45:03