当前位置: 首页 > news >正文

通过contenteditable实现仿豆包智能输入框

随着各种ai大模型如雨后春笋般冒出,一些中大企业都将目光投向了ai领域。各种ai自建应用,ai数据私域化系统也开始成为开发的主流。因此最近也在了解一下关于ai的大模型知识及相关ai前端组件库。后面会再出几期关于ai组件库相关的博文。今天要谈论的是关于豆包中富文本智能输入框的内容。

1.豆包智能输入框

        关于豆包的智能输入,通过和一些开发人员的沟通交流,初步认定是通过富文本实现的。但是在lz分析了一下的dom结构之后又推翻了这种看法。豆包的智能输入主要有三种效果:文字高亮,下拉选择;预输入提示当然,如果你要觉得很简单,那就大错特错了。不信你就去试试吧,一写一个不吱声。

        毕竟豆包的网页是编译后的代码,在分析dom时看到了一个contenteditable属性,经过了解,发现这个属性可不得了

 

 2.contenteditable属性

        contenteditable 是 HTML5 提供的一个全局属性,它可以让元素的内容变得可编辑。这个属性在现代 Web 开发中有广泛的应用,特别是在富文本编辑器和实时协作应用中。而目前主流的富文本编辑器,如 TinyMCE、CKEditor 等底层都使用了这个属性。使用方式也很简单。

<div contenteditable="true">这个区域的内容可以被用户编辑
</div>

3.仿豆包智能输入

        了解contenteditable属性后,就是开始尝试使用了。刚开始也是觉得豆包的输入不过实现了三种特效而已,应该是很简单的。但是在真正实操后发发现。事情并没有想象中看起来的那么简单。因为只是简单的尝试,想用纯原生js实现效果,所以对页面美观性就不要要求太多了, 最终展示效果如下:

当然,瑕疵也有。毕竟只是基于纯原生js的牛刀小试。也不追求什么尽善尽美了。初步效果能看到的就是:文字高亮,下拉选择,预输入提示。bug和问题也不少。后面看有没有更好的替代方案吧。

4.源码分享

        老规矩,一个单html文件,方便拿来主义的拿来精神能贯彻到底。

input.html

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>智能输入by流情</title><meta name="author" content="流情"><link rel="canonical" href="https://liuqingwushui.top"><style>.smart-input {min-height: 100px;border: 1px solid #ddd;padding: 10px;line-height: 1.5;display: flex;flex-wrap: wrap;
}.highlight {background-color: #e0f0ff;border-radius: 3px;padding: 0 2px;cursor: pointer;min-width: 20px;
}
/* 占位符样式 */
.highlight .placeholder {font-style: normal;color: #999;pointer-events: none;
}
/* 已填写内容样式 */
.highlight:not(:has(em)) {background-color: #c1e0ff;
}
.dropdown-menu {position: absolute;border: 1px solid #eee;background: white;box-shadow: 0 2px 8px rgba(0,0,0,0.1);display: none;z-index: 1000;
}.dropdown-item {padding: 8px 12px;cursor: pointer;
}
.smart-input span{display: inline-block;height: 100%;}
.dropdown-item:hover {background: #f5f5f5;
}</style>
</head>
<body class="bg-gray-50 min-h-screen"><div class="smart-input" contenteditable="true"><span>我是一名</span><span contenteditable="true" style="display:flex;min-width: 55px;padding: 0 4px;" class="highlight" data-placeholder="公众号博主"><span class="input" style="padding-left: 1px;">公众号博主&#xFEFF;</span><span contenteditable="false" class="placeholder" style="display:none;pointer-events: none;user-select: none;opacity: 0.7;">公众号博主</span></span><span>需要写一篇关于</span><span class="highlight" data-type="topic" data-value="[主题]" contenteditable="false">[主题]</span> 的<span class="highlight" data-type="format" data-value="文章" contenteditable="false">文章</span>。<span>面向</span> <span contenteditable="true" style="display:flex;padding: 0 4px;" class="highlight" data-placeholder="[人群]"><span class="input" style="padding-left: 1px;">&#xFEFF;</span><span contenteditable="false" class="placeholder" style="display:inline-block;pointer-events: none;user-select: none;opacity: 0.7;">[人群]</span></span><span>宣传产品。打造品牌效益</span></div><!-- 下拉菜单容器 --><span id="dropdown-menu" class="dropdown-menu"></div>
</body>
<script>// 配置选项数据
const OPTIONS = {topic: ['科技', '教育', '健康'],format: ['文章', '报告', '论文']
};
// 监听高亮文本点击
document.querySelector('.smart-input').addEventListener('click', (e) => {const target = e.target.closest('.highlight');if (!target) return;console.log(11,target)if(target.dataset.placeholder){const inputSpan = target.querySelector(".input");const placeholderSpan = target.querySelector(".placeholder");// 初始化:确保至少有一个文本节点if (!inputSpan.firstChild || inputSpan.firstChild.nodeType !== 3) {inputSpan.innerHTML = "";inputSpan.appendChild(document.createTextNode(""));}// 防循环标志let isProgrammaticChange = false;const ensureInputElement = () => {if (!inputSpan.isConnected) {// 如果元素被意外删除,重新创建(极端情况)const newSpan = document.createElement("span");newSpan.className = "input";newSpan.appendChild(document.createTextNode(""));target.insertBefore(newSpan, placeholderSpan);return newSpan;}return inputSpan;};const handleChange = () => {if (isProgrammaticChange) return;console.log("清空",e)const currentSpan = ensureInputElement();const hasContent = currentSpan.textContent.trim() !== "";if (!hasContent) {isProgrammaticChange = true;currentSpan.innerHTML = ""; // 清空但保留元素const textNode = document.createTextNode("\uFEFF");currentSpan.appendChild(textNode);// 移动光标到零宽空格后const range = document.createRange();range.setStart(textNode, 1);range.collapse(true);const sel = window.getSelection();sel.removeAllRanges();sel.addRange(range);setTimeout(() => isProgrammaticChange = false, 0);}placeholderSpan.style.display = hasContent ? "none" : "inline";};// 使用更安全的MutationObserver配置const observer = new MutationObserver((mutations) => {if (!isProgrammaticChange) handleChange();});observer.observe(inputSpan, {childList: true,subtree: true,characterData: true});// 关键事件监听const events = ["keydown", "input", "paste", "cut", "blur"];events.forEach(evt => inputSpan.addEventListener(evt, (e) => {console.log("监听事件触发",e);if (e.type === "keydown" && (e.key === "Backspace" || e.key === "Delete")) {// 对删除操作做特殊处理setTimeout(handleChange, 0);} else {handleChange();}}));// 初始状态设置handleChange();}if(target.dataset.type){showDropdown(target, target.dataset.type);}
});// 显示下拉菜单
function showDropdown(target, type) {const menu = document.getElementById('dropdown-menu');menu.innerHTML = '';// 生成选项OPTIONS[type].forEach(item => {const div = document.createElement('div');div.className = 'dropdown-item';div.textContent = item;div.onclick = () => {target.textContent = item;target.dataset.value = item;menu.style.display = 'none';};menu.appendChild(div);});// 定位菜单const rect = target.getBoundingClientRect();menu.style.display = 'block';menu.style.top = `${rect.bottom + window.scrollY}px`;menu.style.left = `${rect.left + window.scrollX}px`;
}// 点击其他地方关闭菜单
document.addEventListener('click', (e) => {if (!e.target.closest('.highlight')) {document.getElementById('dropdown-menu').style.display = 'none';}
});
</script>
</html>

相关文章:

  • 生成模型——PixelRNN与PixelCNN
  • 常见算法题目2 - 给定一个字符串,找出其中最长的不重复子串
  • MySQL慢日志——动态开启
  • (15)关于窗体的右键菜单的学习与使用,这关系到了信号与事件 event
  • 《C 语言内存函数超详细讲解:从 memcpy 到 memcmp 的原理与实战》
  • vue——v-pre的使用
  • 安装openEuler操作系统
  • 强者的本质是什么?
  • Leetcode 2792. 计算足够大的节点数
  • mysql语句执行流程
  • Femap许可证升级说明
  • LTSPICE仿真电路:(二十九)T型反馈比例器
  • VirtualBox 4.3.10 经典版安装教程 - Windows 7/10 下载与设置指南
  • ASUS华硕ROG枪神9P笔记本G815LP(G615LW,G635LR,G835LX)原装出厂Win11系统,开箱状态oem系统
  • CAU人工智能class4 批次归一化
  • leetcode3434. 子数组操作后的最大频率-medium
  • JavaSE核心知识点03高级特性03-01(集合框架)
  • docker镜像操作
  • 什么是Express
  • LeRobot 框架的开发指南 (上)
  • 手机网站发布页电脑版/长沙网站推广 下拉通推广
  • 怎样弄一个自己的网站/德州seo优化
  • 门户网站建设理由/aso排名
  • 建设一个商城网站需要多少钱/最新经济新闻
  • wordpress 文章数据表/石家庄百度快速排名优化
  • 用dw如何做网站链接/新冠疫情最新情况最新消息