简介:群聊@他人的输入框实现。使用h5新增的contenteditable属性,使标签拥有可供输入的能力。对输入框的光标位置获取使用window.getSelection()实现,可查看文档。@弹框支持键盘上下按键对列表进行滚动选择,支持输入文字对列表进行数据筛选。按下enter确认选中后,依赖canvas将选择的名字进行绘画,生成一张base64的图片以供预览。
注:键盘上下操作滚动列表可参考另一篇文章,键盘上下选择列表项
html
<!-- 编辑区域 -->
<div class="container">
<div class="textBox" contenteditable></div>
<div class="textBoxAssist"></div>
</div>
<!-- @弹框 -->
<ul class="popUpBox">
<li id="0" class="highlight">张无忌</li>
<li id="1">张三</li>
<li id="2">李四</li>
<li id="3">王五</li>
<li id="4">赵六</li>
<li id="5">张三丰</li>
<li id="6">老王</li>
<li id="7">李小小</li>
<li id="8">李小二</li>
<li id="9">李小三</li>
</ul>
<div id="shade"></div>
<canvas id="canvas"></canvas>
css
<style>
* {
margin: 0;
padding: 0;
}
html, body {
/* height: ; */
font-size: 16px;
font-family: arial;
}
/* 编辑区域 */
.container {
position: relative;
height: 400px;
/* background-color: red; */
}
.container .textBox, .container .textBoxAssist {
width: 500px;
height: 300px;
background-color: skyblue;
outline: none;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
z-index: 1;
}
.container .textBox img, .container .textBoxAssist img {
vertical-align: bottom;
}
.container .textBoxAssist {
z-index: 0;
word-break: break-all;
/* margin-left: 100px; */
}
/* 弹框区域 */
.popUpBox {
width: 100px;
height: 100px;
overflow: auto;
border: 1px solid #000;
background-color: #fff;
position: absolute;
top: 0;
left: 0;
z-index: 9999;
visibility: hidden;
}
.popUpBox .highlight {
background-color: #ccc;
}
.popUpBox li {
list-style: none;
line-height: 25px;
}
.popUpBox li:hover {
background-color: #ccc;
}
#shade {
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
background-color: #ccc;
opacity: 0;
z-index: 9998;
display: none;
}
#canvas {
border: 1px solid #ccc;
}
</style>
js
注:art-template.js用于生成html的结构,可在官网进行代码导出,复制到自己文件夹下面
<script src="./art-template.js"></script>
<script id="temp" type="text/html">
{{each}}
<li id="{{$index}}" class="{{$index===0 ? 'highlight': ''}}">{{$value}}</li>
{{/each}}
</script>
<script>
// 编辑dom
let textBox = document.querySelector('.textBox')
let textBoxSel = {} // 编辑dom中的选区对象
let textBoxRange = {} // 输入@时的选区对象
let newRange = {} // @后其他文本的选区对象
// 获取光标位置的辅助dom
let textBoxAssist = document.querySelector('.textBoxAssist')
// 弹框domx
let popUpBox = document.querySelector('.popUpBox')
let liArr = ['张三', '李四', '王五', '赵六', '张三丰', '老王', '李小小', '李小二', '李小三']
let filterArr = []
let popUpBoxLis = [];
let highlightIndex = 0 // 高亮索引
let isVisity = false
let searchStr = ''
// 遮罩
let shade = document.getElementById('shade')
// 输入文字
textBox.onkeydown = function(e) {
// console.log(e)
// 13: enter
if(isVisity && e.keyCode === 13) {
e.preventDefault()
createImg()
} else if(e.shiftKey && e.code === 'Digit2') {
// Digit2: @按键
// 需要一个定时器延时一下,等待输入框值文本变化了才执行
setTimeout(() => {
initPopUpBox()
}, 100);
} else if(isVisity && e.keyCode === 38) {
// 38: 键盘上
e.preventDefault()
if (highlightIndex > 0) {
highlightIndex--;
// 当前列的位置 < 滚动出去的距离,说明当前列在可视区域上面,上滚动
if(popUpBoxLis[highlightIndex].offsetTop < popUpBox.scrollTop) {
popUpBox.scrollTop = popUpBoxLis[highlightIndex].offsetTop
} else if(popUpBoxLis[highlightIndex].offsetTop > popUpBox.scrollTop + 75) {
popUpBox.scrollTop = popUpBoxLis[highlightIndex].offsetTop - 75
}
}
// 重新渲染@弹框背景色
updateLisBgc()
} else if(isVisity && e.keyCode === 40) {
// 40: 键盘下
e.preventDefault()
if (highlightIndex < popUpBoxLis.length - 1) {
highlightIndex++;
// 当前列的位置 > 上滚动出去的距离 + 可视区域的大小,表示当前列在可视区域下面,下滚动
if(popUpBoxLis[highlightIndex].offsetTop > popUpBox.scrollTop + 75) {
popUpBox.scrollTop = popUpBoxLis[highlightIndex].offsetTop - 75
} else if(popUpBoxLis[highlightIndex].offsetTop < popUpBox.scrollTop) {
popUpBox.scrollTop = popUpBoxLis[highlightIndex].offsetTop
}
}
// 重新渲染@弹框背景色
updateLisBgc()
} else if(isVisity && [37, 39].includes(e.keyCode)) {
// 键盘左右
// 1. 隐藏弹框
popUpBox.style.cssText += `visibility: hidden`
isVisity = false
// 2. 遮罩
isShowShade()
} else if(isVisity) {
// 弹框出现,开始获取查询字符串
setTimeout(() => {
newRange = window.getSelection().getRangeAt(0)
if(textBoxRange.startContainer.data?.includes('@')) {
// 得到@后面输入的文本
searchStr = textBoxRange.startContainer.data?.slice(textBoxRange.startOffset, newRange.startOffset)
getSelectPosition()
// console.log(searchStr)
filterLiTag()
if(filterArr.length === 0) {
popUpBox.style.cssText += `visibility: hidden`
} else {
popUpBox.style.cssText += `visibility: block`
}
} else {
// 1. 隐藏弹框
popUpBox.style.cssText += `visibility: hidden`
isVisity = false
// 2. 遮罩
isShowShade()
}
}, 100);
}
}
// 过滤li标签
function filterLiTag() {
filterArr = liArr.filter(item => {
return item.includes(searchStr)
})
let resHtml = template('temp', filterArr)
// console.log(resHtml)
popUpBox.innerHTML = resHtml
popUpBoxLis = document.querySelectorAll(".popUpBox li")
highlightIndex = 0
}
// 手动选择@弹框中的项
popUpBox.onclick = function(e) {
highlightIndex = e.target.id
createImg()
}
// 初始化弹框
function initPopUpBox() {
// 1. Selection对象用于获取用户文本选区(选区被压缩至一点时,也就是光标位置)
textBoxSel = window.getSelection()
// 2. Range: 当前选区对象, 部分浏览器按住ctrl可以选择不连续的文本区域
textBoxRange = textBoxSel.getRangeAt(0)
newRange = textBoxSel.getRangeAt(0)
// 3. 调整@弹框在光标的位置显示
getSelectPosition()
// 4. 显示@弹框
isVisity = true
highlightIndex = 0
popUpBox.scrollTop = 0
searchStr = ''
// 5. 渲染li标签
filterLiTag()
// 6. li标签背景色
updateLisBgc()
// 7. 遮罩
isShowShade()
}
// 调整@弹框在光标的位置显示
function getSelectPosition() {
let textBoxRange = window.getSelection().getRangeAt(0)
// console.log(textBoxRange)
// 1. 选中当前光标前面的所有内容
textBoxRange.setStart(textBox, 0)
// 2. 获取选中的节点的文档片段
let cloneContents = textBoxRange.cloneContents()
// 3. 片段中增加辅助的span标签
let span = document.createElement('span')
span.className = 'spanAssist'
// console.log(cloneContents.lastElementChild)
if(cloneContents.lastElementChild && cloneContents.lastElementChild.tagName !== 'IMG') {
cloneContents.lastElementChild.appendChild(span)
}else {
cloneContents.appendChild(span)
}
// 4. 片段添加到辅助dom中
textBoxAssist.innerHTML = ''
textBoxAssist.appendChild(cloneContents)
// 5. 将第一步选中的选区恢复
// false: 选区折叠到结束位置
textBoxRange.collapse(false)
// 6. 获取辅助dom中的辅助span标签在浏览器中的位置
let spanAssist = textBoxAssist.querySelector('.spanAssist')
let { left: screenLeft, top: screenTop } = spanAssist.getBoundingClientRect()
popUpBox.style.cssText += `;left: ${screenLeft}px; top: ${screenTop > popUpBox.offsetHeight ? screenTop - popUpBox.offsetHeight : screenTop + spanAssist.offsetHeight}px; visibility: visible`
}
// 更新li标签的背景颜色
function updateLisBgc() {
popUpBoxLis.forEach((item, index) => {
if (index === highlightIndex) {
popUpBoxLis[index].className = "highlight"
} else {
popUpBoxLis[index].className = ""
}
})
}
// 创建图片标签
function createImg() {
let name = popUpBoxLis[highlightIndex].innerText
let { url, width, height } = createNameImg('@' + name)
let img = document.createElement('img')
img.src = url
img.style.width = width + 'px'
img.style.height = height + 'px'
// let span = document.createElement('span')
// span.appendChild(img)
// 插入图片
myInnerHtml(img)
}
// 插入图片
function myInnerHtml(html) {
// console.log(newRange)
// 1. 将当前光标所在位置的前一个内容进行拖蓝,也就是选中输入的"@"
newRange.setStart(newRange.startContainer, newRange.startOffset - searchStr.length - 1);
// 2. 删除选中的"@"符号 + 文本
newRange.deleteContents();
// 3. 插入新的元素节点
newRange.insertNode(html);
// 4. 将光标移动到新节点的后面,手动点击li标签会导致Range选区失去焦点,所以该对象的属性无效
// textBoxRange.collapse(false)
// 4. 重新设置光标位置
// textBoxSel.collapse(html, 1);
textBoxSel.collapse(newRange.endContainer, newRange.endOffset);
// 5. 隐藏弹框
popUpBox.style.cssText += `visibility: hidden`
isVisity = false
// 6. 遮罩
isShowShade()
}
// 遮罩
shade.onclick = function() {
popUpBox.style.cssText += `visibility: hidden`
isVisity = false
isShowShade()
}
function isShowShade() {
shade.style.display = isVisity ? 'block' : 'none'
}
// 依赖canvas将名字转为图片
let canvas = document.getElementById('canvas')
let ctx = canvas.getContext('2d')
let fontSize = Number.parseInt(window.getComputedStyle(document.documentElement).fontSize)
function createNameImg(name) {
// createNameImg('张三张三张三')
let width = name.length * fontSize + 10
let height = fontSize + 5
canvas.width = width
canvas.height = height
ctx.font = `${fontSize}px arial`
ctx.fillText(name, 5, fontSize) // 参数: text, x, y
return {
url: canvas.toDataURL(), // base64的图片格式
width,
height: height
}
}