如下图所示,需求是完成一个填写周报的功能,功能点有:
- @ 选人和 # 选择相关的项目,并且变色
- 换行点击回车的时候 自动生成 序号,同时输入框变大一行
- 删除的时候 遇到 @/# 整体删除
- 删除的时候遇到 换行符\n 输入框变小一行
- 检测到输入的最后一个值是@/# 弹框选择
- 等等其他功能点
采用的是 textarea 绝对定位方案,大概原理 参考自 juejin.cn/post/724105… 同时还多了很多其他的细节点
一、template
<div style="position: relative" :style="cssVar">
<!--隐藏在下面,用于展示颜色-->
<p class="copyp" ref="copytext" v-html="copyVal" />
<!--绝对定位在上面-->
<p style="" class="textarea-wrap">
<textarea
ref="textarea"
class="textarea"
v-model="inputText"
:rows="rows"
@keydown="handleTextarea"
@input="handleInput"
@change="handleInput"
placeholder="请输入"
@scroll="onScroll"
></textarea>
</p>
</div>
二、js
data() {
return {
rows: 2,
copyVal: "",
inputText: "",
copypHeight: "100px",
}
},
computed: {
cssVar() {
return { "--copyp-height": this.copypHeight };
},
},
methods: {
}
handleInput 检测当前输入的最后一个字符是否是关键字
handleInput(event) {
const textarea = event.target;
const clcRows = ~~(textarea.scrollHeight / textarea.clientHeight);
const rowArr = (textarea.value || "").match(/\n/g) || [];
const rows = rowArr.length + 3;
this.rows = clcRows > rows ? clcRows : rows;
let val = textarea.value;
let len = val.length;
this.copypHeight = this.rows * 28 + "px";
if (val.substring(len - 1) === "\n") {
this.copypHeight = (this.rows - 1) * 28 + "px";
}
this.setCopyColor(val);
const cursorPosition = textarea.selectionStart;
const beforeCursor = val.substring(0, cursorPosition);
const afterCursor = val.substring(cursorPosition);
this.beforeCursor = beforeCursor;
this.afterCursor = afterCursor;
let lastWord = beforeCursor.substring(beforeCursor.length - 1);
if (lastWord === "@") {
this.selUserVisible = true;
}
if (lastWord === "#") {
this.selProjectVisible = true;
}
},
setCopyColor 这边是输入内容后获取所有内容进行一个正则匹配,然后再把对应的关键字加上样式
setCopyColor(val) {
this.$nextTick(() => {
this.copyVal = val;
let arr = val.match(/((@|#)\s*[^\s]+)/g) || [];
arr = arr.filter((v, i, arr) => arr.indexOf(v, 0) === i);
arr = arr.filter((v) => v.indexOf("\n") == -1);
arr = arr.sort((a, b) => b.length - a.length);
for (let i = 0; i < arr.length; i++) {
this.copyVal = this.copyVal.replaceAll(
arr[i],
`<span style='color:#409eff;position: relative;z-index: 9;font-size: 14px;'>${arr[i]}</span>`
);
}
this.$nextTick(() => {
this.$refs.copytext.scrollTop = this.$refs.textarea.scrollTop;
});
});
},
handleTextarea 监听 Enter Backspace 行为进行换行自动添加序号和删除一整块关键字的功能
handleTextarea(event) {
const textarea = event.target;
const cursorPosition = textarea.selectionStart;
const currentText = textarea.value;
const beforeCursor = currentText.substring(0, cursorPosition);
const afterCursor = currentText.substring(cursorPosition);
// Check if the key pressed is 'Enter'
if (event.key === "Enter") {
// Prevent default 'Enter' behavior which is inserting a newline
event.preventDefault();
// Find the last number before the cursor
const matches = beforeCursor.match(/(\d+)(\.|\n|$)/g);
let lastNumber = 0;
if (matches) {
const lastMatch = matches[matches.length - 1];
lastNumber = parseInt(lastMatch);
}
// Insert the next line number at the cursor position
// if (lastNumber == 0) {
// textarea.value =
// "1." + beforeCursor + "\n" + (lastNumber + 2) + "." + afterCursor;
// } else {
let str = "";
if (lastNumber) {
str = beforeCursor + "\n" + (lastNumber + 1) + "." + afterCursor;
} else {
str = beforeCursor + "\n" + afterCursor;
}
this.setVal(str);
// }
// Move the cursor to the next line after the line number
const nextCursorPosition =
cursorPosition + (lastNumber + 1).toString().length + 4;
textarea.setSelectionRange(nextCursorPosition, nextCursorPosition);
} else if (
event.key === "Backspace" &&
beforeCursor.match(/(^|\n)\d+\.$/)
) {
// Prevent default 'Backspace' behavior
event.preventDefault();
// Remove the current line number
const newBeforeCursor = beforeCursor.replace(/(\n|^)\d+\.$/, "$1");
let str = newBeforeCursor + afterCursor;
this.setVal(str);
if (this.rows > 2) this.rows--;
// Move the cursor to the end of the previous line
this.$nextTick(() => {
textarea.setSelectionRange(
newBeforeCursor.length,
newBeforeCursor.length
);
});
} else if (event.key === "Backspace") {
let arr = beforeCursor.match(/((@|#)\s*[^\s]+)/g) || [];
if (arr.length) {
let str = arr[arr.length - 1];
let start = beforeCursor.length - str.length;
if (beforeCursor.substring(start) == str) {
let val = beforeCursor.substring(0, start) + afterCursor;
this.inputText = val;
this.$nextTick(() => {
event.target.setSelectionRange(start, start);
});
}
}
}
// this.textVal = textarea.value;
},
监听 onScroll 可视内容滚动的时候,隐藏的那部分内容也需要一起滚动
// 解决滚动时 @/# 不随着滚动的问题
onScroll(event) {
const scrollTop = event.target.scrollTop;
this.$refs.copytext.scrollTop = scrollTop;
},
selUserConfirm 选完人员后的拼接
selUserConfirm(list) {
let allList = [...this.selUserList, ...list];
this.selUserList = JSON.parse(JSON.stringify(allList));
this.selUserClose();
this.beforeCursor = this.beforeCursor.substring(
0,
this.beforeCursor.length - 1
);
let str = "";
list.forEach((e, i) => {
str += ` @${e.name}`;
});
str += " ";
let val = this.beforeCursor + str + this.afterCursor;
this.setVal(val);
this.$nextTick().then(() => {
this.setCopyColor(val);
});
},
setVal(val) {
this.$nextTick(() => {
// let text = val.replace(/<[^>]*>/g, '') // 去除样式
let rowArr = (val || "").match(/\n/g) || [];
this.rows = Math.max(rowArr.length + 3, 4);
this.inputText = val;
this.copypHeight = this.rows * 28 + "px";
let len = val.length;
// 这里是因为 换行的时候textarea 的rows增加1,但是后面隐藏的那部分没有增加一行空行,导致可能带颜色的文字会错乱
if (val.substring(len - 1) === "\n") {
this.copypHeight = (this.rows - 1) * 28 + "px";
}
this.setCopyColor(val);
});
},
三、css
<style lang="scss" scoped>
.textarea-wrap {
position: absolute;
z-index: 1;
top: 0;
left: 0;
width: 100%;
.textarea {
color: #333;
caret-color: black;
background: transparent;
font-size: 14px;
width: 100%;
padding: 0;
border: none;
line-height: 28px;
resize: none;
}
}
.copyp {
white-space: pre-wrap;
line-height: 28px;
font-size: 14px;
height: var(--copyp-height);
color: #fff;
overflow: auto;
}
.textarea {
// width: 100%;
// padding: 0;
// border: none;
// line-height: 28px;
// resize: none;
}
/*修改textarea中placeholder颜色*/
textarea::-webkit-input-placeholder {
/* WebKit browsers */
color: #bbb;
}
textarea:-moz-placeholder {
/* Mozilla Firefox 4 to 18 */
color: #bbb;
}
textarea::-moz-placeholder {
/* Mozilla Firefox 19+ */
color: #bbb;
}
textarea:-ms-input-placeholder {
/* Internet Explorer 10+ */
color: #bbb;
}
</style>