简言
利用canvas实现文本的输入和显示。
简易文本输入框
功能描述
- 点击后显示输入光标,然后显示输入内容
- 可以不同行内输入
- 光标在文本后面时可以删除文本
实现思路
- canvas是无法直接唤起键盘输入文字和题词框的,需要搭配input或可编辑的div元素,通过定位实现输入文字和题词框的显示。
- 文字输入时,要监听键盘输入事件和失去焦点事件,我的逻辑是在失去焦点后保存文本信息和位置信息。
- canvas显示通过处理保存的文字数据,利用requestAnimationFrame事件将文字信息显示出来。
源码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文本输入</title>
<style>
.box {
position: relative;
}
#canvas1 {
position: relative;
border: 1px solid #000;
}
.test3 {
position: absolute;
left: 0;
top: 0;
font: "16px/3 sans-serif";
vertical-align: middle;
margin: 0;
padding: 0;
white-space: nowrap;
overflow: hidden;
outline: none;
}
</style>
</head>
<body>
<div class="box">
<canvas id="canvas1">
</canvas>
<div class="test3" contenteditable="true"></div>
</div>
<script>
const width = 900
const height = 450
const padding = 20
const lineHeight = 30
const textArr = [{
line: 0,
data: []
}] // 存储文本
const canvas = document.getElementById('canvas1');
const ctx = canvas.getContext('2d');
const divDom = document.querySelector('.test3');
canvas.width = width;
canvas.height = height;
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
let nowX = 0 // 当前输入坐标x
let nowY = 0 // 当前输入坐标y
let nowLine = 0 // 当前行数
drawLine(ctx)
canvas.addEventListener('click', function (e) {
let line = 1 // 行数
if (e.offsetY > Math.floor((height - padding * 2) / lineHeight) * lineHeight + padding) { // 底部
line = Math.floor((height - padding * 2) / lineHeight)
} else if (e.offsetY < padding + lineHeight) { // 顶部
line = 1
} else {
line = Math.floor(((e.offsetY - padding) / lineHeight) + 1)
}
let endItem = textArr[line]?.data ? textArr[line].data[textArr[line].data.length - 1] : false
nowX = endItem ? endItem.x + endItem.info.width : padding
nowY = line * lineHeight
nowLine = line
divDom.style.left = nowX + 'px';
divDom.style.top = nowY + 'px';
divDom.style.width = 1 + 'px';
divDom.focus();
})
divDom.addEventListener('input', function (e) {
ctx.save()
ctx.font = "16px/3 sans-serif";
ctx.textBaseline = 'bottom'
const info = ctx.measureText(e.target.innerText).width
ctx.restore()
divDom.style.width = info + 'px';
})
divDom.addEventListener('keydown', function (e) {
console.log(e.keyCode);
if (e.keyCode == 13) {
e.preventDefault();
divDom.blur()
} else if (e.keyCode == 8) {
// 删除
if (divDom.innerText.length > 0) {
} else {
let index = textArr[nowLine].data.findIndex(item => item.x + item.info.width == nowX)
if (index !== -1) { // 定位到指定坐标
nowX = textArr[nowLine].data[index]?.x || 0
divDom.innerText = ''
divDom.style.left = nowX + 'px'
divDom.focus()
textArr[nowLine].data.splice(index, 1)
}
}
}
})
divDom.addEventListener('blur', function (e) {
if (divDom.innerText.length > 0) {
var inputContent = e.target.innerText;
let lineH = e.target.offsetHeight
if (inputContent !== null) {
if (!textArr[nowLine]) {
textArr[nowLine] = {
line: nowLine,
data: []
}
}
inputContent.split('').forEach(s => {
ctx.save()
ctx.font = "16px/3 sans-serif";
ctx.textBaseline = 'bottom'
const info = ctx.measureText(s)
ctx.fillText(s, nowX, nowY + lineH);
textArr[nowLine].data.push({
value: s,
x: nowX,
y: nowY + lineH,
info
})
ctx.restore()
nowX = nowX + info.width
divDom.style.left = nowX + 'px';
divDom.focus();
})
}
}
divDom.style.width = '0px';
divDom.innerText = ''
})
// 行线
function drawLine(ctx) {
ctx.save()
ctx.strokeStyle = 'red'
ctx.setLineDash([3, 3])
ctx.beginPath();
for (let y = padding; y <= height - padding; y += lineHeight) {
ctx.moveTo(padding, y)
ctx.fillText(y, 0, y)
ctx.lineTo(width - padding, y)
}
ctx.stroke()
ctx.restore()
}
let raf = null
function show(ctx) {
ctx.clearRect(0, 0, width, height)
// 画航线
drawLine(ctx)
// 显示文字数据
textArr.forEach((item, index) => {
if (item.data?.length > 0) {
item.data.forEach((v, i) => {
ctx.save()
ctx.font = "16px/3 sans-serif";
ctx.textBaseline = 'bottom'
ctx.fillText(v.value, v.x, v.y)
ctx.restore()
})
}
})
raf = window.requestAnimationFrame(() => show(ctx))
}
// 开启动画
raf = window.requestAnimationFrame(() => show(ctx))
</script>
</body>
</html>
预览
问题
- 输入文字的处理(简单处理)
- 文字位置的定位(不全面)
- 文字的样式保存(未实现)
- 当前行输入超出边界换行(未处理)
- 光标自定义定位,然后删除、插入(未实现)
- 回车自动聚焦下一行(未实现)
- 当前行删除完后自定删除上一行数据(未实现)
- 行高动态变化(固定了30px)
- 粘贴、复制、文字选中操作(未实现)
结语
结束了。