如何实现输入框内可拖拽、不可编辑删除的标签功能
带可拖拽固定标签的输入组件实现方案
该组件无法直接基于原生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
相关产品推荐
相关产品推荐

