当前位置: 首页 > 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>

http://www.xdnf.cn/news/619147.html

相关文章:

  • 解决PLSQL工具连接Oracle后无法使用ODBC导入器问题
  • 第三章、DQN(Deep Q-Network)
  • 【AS32X601驱动系列教程】PLIC_中断应用详解
  • PADS LAYOUT添加GND过孔
  • 小豆包api:claude-sonnet-4,Claude 最新模型
  • 卖家受益于WOOT推广的逻辑
  • 基于QuestionPicture的图片批量处理方法与实践
  • 2025 ICPC 南昌全国邀请赛暨江西省赛(8题题解)
  • 三格电子上新了——高频工业 RFID 读写器
  • 理解网卡RSS
  • 深入理解会话管理:Cookie、Session与JWT的对比与应用
  • Python图像处理基础(四)
  • 信号与系统05-复频域分析(拉普拉斯变换与Z变换)
  • 飞书知识问答深度体验:企业AI应用落地的典范产品
  • 可解释性学习指标综述_Machine Learning Interpretability: A Survey on Methods and Metrics
  • unity在urp管线中插入事件
  • Opixs: Fluxim推出的全新显示仿真模拟软件
  • 易境通专线散拼系统:全方位支持多种专线物流业务!
  • 推挽电路工作原理及仿真
  • 从elf文件动态加载的过程解释got,plt及got.plt,plt.sec
  • JavaScript运算符全解析:从基础到进阶实战指南
  • 2025年中级社会工作者备考精选练习题
  • HardFault_Handler调试及问题方法
  • vue——v-pre的使用
  • Redis 面经
  • 开发指南118-背景渐变特效
  • SOC-ESP32S3部分:8-GPIO输出LED控制
  • 如何做好一份技术文档?
  • JavaSE核心知识点03高级特性03-01(集合框架)
  • AbMole| MG132(133407-82-6,M1902,蛋白酶体抑制剂)