输入内容的时候,输入斜杠展示一个弹窗并且选中弹窗的内容可以追加到当前输入区的功能实现
先上效果图:
它也支持根据点击的位置以及键盘方向键的移动判断当光标处于斜杠后的时候弹出弹窗
代码如下:
import { defineComponent, ref, nextTick, onMounted, onUnmounted } from "vue"; import { List, ListItem } from "@arco-design/web-vue"; import "./index.less"; export default defineComponent({ name: "MyComponent", setup() { const popupVisible = ref(false); const textareaRef = ref<HTMLElement | null>(null); let lastCaretPosition: any = null; // 记录光标位置 /** * 更新光标位置 */ const updateCaretPosition = () => { const selection = window.getSelection(); if (selection && selection.rangeCount > 0) { lastCaretPosition = selection.getRangeAt(0); // 记录光标位置 } }; /** * 设置弹窗位置 * @param {HTMLElement} popover - 弹窗元素 * @param {HTMLElement} textarea - 输入框元素 */ const setPopupPosition = (popover: HTMLElement, textarea: HTMLElement) => { if (popover && textarea) { const cursorPos = getCaretPosition(); const rect = textarea.getBoundingClientRect(); const offset = 10; // 光标和弹窗之间的额外空间 popover.style.position = "absolute"; // 调整垂直位置 if ( cursorPos.top + cursorPos.height + popover.offsetHeight > window.innerHeight ) { popover.style.top = `${ rect.top + cursorPos.top - cursorPos.height - popover.offsetHeight + window.scrollY - offset }px`; } else { popover.style.top = `${ rect.top + cursorPos.top + cursorPos.height + window.scrollY + offset }px`; } // 调整水平位置 if (cursorPos.left + popover.offsetWidth > window.innerWidth) { popover.style.left = `${ rect.left + cursorPos.left - popover.offsetWidth }px`; } else { popover.style.left = `${rect.left + cursorPos.left}px`; } } }; /** * 处理输入事件 * @param {InputEvent} inputValue - 输入事件对象 */ const handleInput = async (inputValue: InputEvent) => { const lastChar = (inputValue.data || "").slice(-1); if (lastChar === "/") { popupVisible.value = true; await nextTick(); const textarea = textareaRef.value; if (textarea) { updateCaretPosition(); const popover = document.querySelector<HTMLElement>(".instruction-list"); if (popover) { setPopupPosition(popover, textarea); } } } else { popupVisible.value = false; } }; /** * 处理键盘事件 * @param {KeyboardEvent} event - 键盘事件对象 */ const handleKeyDown = (event: KeyboardEvent) => { if ( ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(event.key) ) { setTimeout(() => { checkCaretPosition(); updateCaretPosition(); }, 0); } }; /** * 处理点击事件 */ const handleClick = async () => { await nextTick(); checkCaretPosition(); updateCaretPosition(); }; const createNode = (text: string) => { // 创建新的文本内容 const newText = `<span class="highlight">${text}</span>`; const tmpSpan = document.createElement("span"); tmpSpan.innerHTML = newText; tmpSpan.setAttribute("contentEditable", "false"); return tmpSpan; }; /** * 处理选择项点击事件 * @param {string} item - 选择的项 */ const handleItemClick = (item: string) => { const selection = window.getSelection(); // 如果之前保存了光标位置,则恢复它 if (lastCaretPosition) { selection?.removeAllRanges(); selection?.addRange(lastCaretPosition); } const range: any = selection?.getRangeAt(0); // 获取当前选区的第一个 Range 对象 const startOffset = Math.max(range.startOffset - 1, 0); // 计算起始偏移量,确保它不会小于 0 const endOffset = range.endOffset; // 获取结束偏移量 if (range.startContainer.nodeType === Node.TEXT_NODE) { // 确保起始容器是文本节点 range.setStart(range.startContainer, startOffset); // 设置 Range 的起始位置为调整后的起始偏移量 range.setEnd(range.endContainer, endOffset); // 设置 Range 的结束位置 range.deleteContents(); // 删除 Range 内的内容 const spanNode = createNode(item); // 创建包含选项文本的新节点 range.insertNode(spanNode); // 将新节点插入到 Range 内 range.setStartAfter(spanNode); // 将 Range 的起始位置设置到新节点之后 range.setEndAfter(spanNode); // 将 Range 的结束位置设置到新节点之后 selection?.removeAllRanges(); // 清空当前的所有 Range selection?.addRange(range); // 将新的 Range 添加到选区 popupVisible.value = false; // 隐藏弹窗 textareaRef.value?.focus(); // 重新聚焦到输入框 } }; /** * 检查光标位置 */ const checkCaretPosition = () => { const textarea = textareaRef.value; if (textarea) { const selection = window.getSelection(); if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0); const text = range.startContainer.textContent; const caretPos = range.startOffset; if (text && text[caretPos - 1] === "/" && caretPos > 0) { popupVisible.value = true; const popover = document.querySelector<HTMLElement>(".instruction-list"); if (popover) { setPopupPosition(popover, textarea); } } else { popupVisible.value = false; } } } }; /** * 获取光标位置 * @returns {Object} 光标位置对象,包含 left、top 和 height 属性 */ const getCaretPosition = () => { const selection = window.getSelection(); if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0); const rect = range.getBoundingClientRect(); return { left: rect.left, top: rect.top, height: rect.height, }; } return { left: 0, top: 0, height: 0 }; }; onMounted(() => { const textarea = textareaRef.value; if (textarea) { textarea.addEventListener("keydown", handleKeyDown); textarea.addEventListener("click", handleClick); } }); onUnmounted(() => { const textarea = textareaRef.value; if (textarea) { textarea.removeEventListener("keydown", handleKeyDown); textarea.removeEventListener("click", handleClick); } }); return () => ( <div style={{ position: "relative" }}> <div placeholder="请输入内容" contenteditable class="editor" allow-clear ref={textareaRef} onInput={handleInput} /> <div id="popover" v-show={popupVisible.value}> <List class="instruction-list"> <ListItem tabindex="0" onClick={() => handleItemClick("北京1科技有限公司")} > 北京1科技有限公司 </ListItem> <ListItem tabindex="0" onClick={() => handleItemClick("北京2科技有限公司")} > 北京2科技有限公司 </ListItem> <ListItem tabindex="0" onClick={() => handleItemClick("北京3科技有限公司")} > 北京3科技有限公司 </ListItem> <ListItem tabindex="0" onClick={() => handleItemClick("北京4科技有限公司")} > 北京4科技有限公司 </ListItem> <ListItem tabindex="0" onClick={() => handleItemClick("北京5科技有限公司")} > 北京5科技有限公司 </ListItem> <ListItem tabindex="0" onClick={() => handleItemClick("北京6科技有限公司")} > 北京6科技有限公司 </ListItem> <ListItem tabindex="0" onClick={() => handleItemClick("北京7科技有限公司")} > 北京7科技有限公司 </ListItem> <ListItem tabindex="0" onClick={() => handleItemClick("北京8科技有限公司")} > 北京8科技有限公司 </ListItem> <ListItem tabindex="0" onClick={() => handleItemClick("北京9科技有限公司")} > 北京9科技有限公司 </ListItem> <ListItem tabindex="0" onClick={() => handleItemClick("北京0科技有限公司")} > 北京0科技有限公司 </ListItem> <ListItem tabindex="0" onClick={() => handleItemClick("北京11科技有限公司")} > 北京11科技有限公司 </ListItem> <ListItem tabindex="0" onClick={() => handleItemClick("北京12科技有限公司")} > 北京12科技有限公司 </ListItem> <ListItem tabindex="0" onClick={() => handleItemClick("北京13科技有限公司")} > 北京13科技有限公司 </ListItem> <ListItem tabindex="0" onClick={() => handleItemClick("北京14科技有限公司")} > 北京14科技有限公司 </ListItem> <ListItem tabindex="0" onClick={() => handleItemClick("北京15科技有限公司")} > 北京15科技有限公司 </ListItem> </List> </div> </div> ); }, });
CSS如下:
.instruction-list { height: 200px; z-index: 1000; background-color: #fff; box-shadow: 0 0 0 2px rgba(72, 5, 255, 0.06); .arco-list-content { height: 200px; overflow: auto; &::-webkit-scrollbar { background: transparent; width: 0.25rem; height: 0.2rem; } &::-webkit-scrollbar-thumb { /*滚动条里面小方块*/ background: #856dfc; opacity: 0.12; border-radius: 0.125rem; } &::-webkit-scrollbar-track { /*滚动条里面轨道*/ background: transparent; } } } .editor { margin: 0 auto; width: 100%; height: 100px; background: #fff; border: 1px solid #d9d9d9; border-radius: 5px; text-align: left; padding: 10px; overflow: auto; line-height: 30px; color: #333; &:hover { border-color: #856dfc; } &:focus { outline: none; border-color: #856dfc; box-shadow: 0 0 0 2px rgba(72, 5, 255, 0.06); border-inline-end-width: 1px; } } .highlight { display: inline-block; border: 1px solid purple; border-radius: 5px; padding: 2px 4px; }
积累小的知识,才能成就大的智慧,希望网上少一些复制多一些原创有用的答案
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
2020-01-06 id4的数据库持久化写法
2016-01-06 远程文件拷贝(fastcopy为例)