键盘打出@,出现人员列表进行选择
主要是利用div或input的contenteditable属性(即可开启该元素的编辑模式)
然后利用键盘抬起和按下事件,进行监听判断是否是@,若是则显示人员列表进行选择,实现@功能的实现
主页面代码
<!-- 留言区 -->
<div
style="
height: 100px;
background-color: #fff;
justify-content: space-between;
border: 1px solid #e6eaf0;
"
>
<div style="display: flex">
<div
style="
width: 20px;
height: 20px;
border-radius: 10px;
overflow: hidden;
margin: auto 10px;
"
>
<img :src="info.img" style="width: 100%; height: 100%" />
</div>
<At ref="At" class="editor"></At>
</div>
<div
style="
width: 100%;
padding: 0 10px;
display: flex;
justify-content: space-between;
"
>
<div style="margin: auto 0">
<img
src="@/assets/wenj.png"
style="width: 20px; height: 20px"
/>
<!-- <img
src="@/assets/aite.png"
style="width: 20px; height: 20px; margin-left: 10px"
/> -->
</div>
<div>
<el-button type="text" @click="AddComments">发送</el-button>
</div>
</div>
</div>
// 发送留言
async AddComments() {
this.textarea = this.$refs.At.text;
const res = await AddComments({
userid: this.info.id,
username: this.info.name,
dataid: this.Form.dataid,
text: this.textarea,
});
if (res.code === 200) {
this.$parent.GetFromData(this.Form.dataid); //调用父组件的方法,重新渲染
this.textarea = "";
}
},
组件At的代码
<template>
<div id="At">
<!-- 内容,主要是需要显示输入多少字数了 -->
<el-input
id="divRef"
ref="divRef"
type="textarea"
:rows="2"
maxlength="140"
show-word-limit
contenteditable
resize="none"
placeholder="留言"
v-model="text"
@keyup.native="handkeKeyUp"
@keydown.native="handleKeyDown"
>
</el-input>
<!-- 若是没有要求需要显示多少字数,就用这个 -->
<!-- <div
ref="divRef"
contenteditable
spellcheck="false"
placeholder="留言"
@keyup="handkeKeyUp"
@keydown="handleKeyDown"
@blur="blur"
></div> -->
<!-- 人员选择 -->
<AtDialog
ref="AtDialog"
v-if="showDialog"
:visible="showDialog"
:position="position"
:queryString="queryString"
:mockList="mockList"
@onPickUser="handlePickUser"
@onHide="handleHide"
@onShow="handleShow"
></AtDialog>
</div>
</template>
<script>
import { mapGetters } from "vuex";
import AtDialog from "./AtDialog.vue";
import { GetAllList } from "@/api/permission";
export default {
name: "At",
computed: {
...mapGetters(["userid", "name", "info", "btnsUrl"]),
},
components: {
AtDialog,
},
data() {
return {
text: "", //编辑的留言
node: "", // 获取到节点
user: "", // 选中项的内容
endIndex: "", // 光标最后停留位置
queryString: "", // 搜索值
showDialog: false, // 是否显示弹窗
mockList: [],
position: {
x: 0,
y: 0,
}, // 弹窗显示位置
cursorPosition: "",
};
},
watch: {
// 监听点击空白处,隐藏用户列表
showDialog: {
handler(newVal, olVal) {
if (newVal) {
setTimeout(() => {
document.addEventListener("click", this.checkClick);
}, 0);
} else {
document.removeEventListener("click", this.checkClick);
}
},
},
},
// mounted() {
// // 监听点击到@后面,出现用户列表
// window.addEventListener("mouseup", this.handleMousedown);
// },
unmounted() {
document.removeEventListener("click", checkClick);
// document.removeEventListener("mouseup", this.handleMousedown);
},
created() {
this.GetAllList();
},
methods: {
GetAllList() {
GetAllList().then((res) => {
this.mockList = res.data.userlist;
});
},
// 点击空白处,隐藏用户列表
checkClick(event) {
let dom = this.$refs.AtDialog; // 这里是你的下拉菜单元素
if (dom) {
this.showDialog = false;
document.removeEventListener("click", this.checkClick);
}
// if (!dom.contains(event.target)) {
// // 不在菜单范围,隐藏即可
// if (this.showDialog) {
// this.showDialog = false;
// document.removeEventListener("click", this.checkClick);
// }
// }
},
// // 监听点击到@后面,出现用户列表
// handleMousedown(e) {
// if (e.target.id === "divRef") {
// const field = this.getTextSelection();
// var field_val = field.value; //文本
// const reg = /@([^@\s]*)$/;
// const mat = reg.exec(
// field_val.slice(field.selectionStart - 1, field.selectionEnd)
// );
// if (mat && mat.length === 2) {
// this.node = field.value; //文本内容
// this.endIndex = field.selectionEnd; //文本长度
// this.position = this.getRangeRect(e);
// this.queryString = this.getAtUser() || "";
// this.showDialog = true;
// }
// }
// },
getText() {
this.$emit("getText", this.text);
},
// 失去焦点,获取内容(div的方法)
blur() {
this.text = this.setText(this.$refs.divRef.innerHTML);
},
// 去除标签,获取纯文本
setText(html) {
return html
.replace(/<(p|div)[^>]*>(<br\/?>| )<\/\1>/gi, "\n")
.replace(/<br\/?>/gi, "\n")
.replace(/<[^>/]+>/g, "")
.replace(/(\n)?<\/([^>]+)>/g, "")
.replace(/\u00a0/g, " ")
.replace(/ /g, " ")
.replace(/<img[^>]+src\\s*=\\s*['\"]([^'\"]+)['\"][^>]*>/g, "")
.replace(/<\/?(img|table)[^>]*>/g, "") // 去除图片和表格
.replace(/<\/?(a)[^>]*>/g, ""); // 去除a标签
},
// 获取光标位置
getCursorIndex() {
const selection = window.getSelection();
return selection.focusOffset; // 选择开始处 focusNode 的偏移量
},
// 获取节点(div的方法)
getRangeNode() {
const selection = window.getSelection();
return selection.focusNode; // 选择的结束节点
},
// 获取节点(输入框的方法)
getTextSelection() {
return document.getElementById("divRef");
},
// 弹窗出现的位置(输入框的方法)
getRangeRect(e) {
let p = e.target.getBoundingClientRect();
const LINE_HEIGHT = 150;
return {
x: p.x,
y: p.y - LINE_HEIGHT,
};
},
// 弹窗出现的位置(div的方法)
// getRangeRect() {
// const selection = window.getSelection();
// const range = selection.getRangeAt(0); // 是用于管理选择范围的通用对象
// const rect = range.getClientRects()[0]; // 择一些文本并将获得所选文本的范围
// const LINE_HEIGHT = 160;
// return {
// x: rect.x,
// y: rect.y - LINE_HEIGHT,
// };
// },
// 是否展示 @
showAt() {
// 输入框的方法
const field = this.getTextSelection();
// var startPos = field.selectionStart; //光标开始的位置
// var endPos = field.selectionEnd; //结束
var field_value = field.value; //文本
const regx = /@([^@\s]*)$/;
// 包含@
// const match = regx.exec(field_value.slice(0, field.selectionEnd));
// 获取全部文字,然后判断光标结束位置的前面一个字符是不是@
let match = "";
// 判断是在整个文本的最后加还是中途加@
if (field_value.length === field.selectionEnd) {
match = regx.exec(
field_value.slice(field_value.length - 1, field.selectionEnd)
);
return match && match.length === 2;
} else {
match = regx.exec(
field_value.slice(field.selectionStart - 1, field.selectionEnd)
);
return match && match.length === 2;
}
// div的方法
// const node = this.getRangeNode();
// if (!node || node.nodeType !== Node.TEXT_NODE) return false;
// const content = node.textContent || "";
// const regx = /@([^@\s]*)$/;
// const match = regx.exec(content.slice(0, this.getCursorIndex()));
// return match && match.length === 2;
},
// 获取 @ 用户
getAtUser() {
const content = this.getRangeNode().textContent || "";
const regx = /@([^@\s]*)$/;
const match = regx.exec(content.slice(0, this.getCursorIndex()));
if (match && match.length === 2) {
return match[1];
}
return undefined;
},
// 创建标签
createAtButton(user) {
const btn = document.createElement("span");
btn.style.display = "inline-block";
btn.dataset.user = JSON.stringify(user);
btn.className = "at-button";
btn.contentEditable = "false";
btn.textContent = `@${user.name}`;
const wrapper = document.createElement("span");
wrapper.style.display = "inline-block";
wrapper.contentEditable = "false";
const spaceElem = document.createElement("span");
spaceElem.style.whiteSpace = "pre";
spaceElem.textContent = "\u200b";
spaceElem.contentEditable = "false";
const clonedSpaceElem = spaceElem.cloneNode(true);
wrapper.appendChild(spaceElem);
wrapper.appendChild(btn);
wrapper.appendChild(clonedSpaceElem);
return wrapper;
},
replaceString(raw, replacer) {
return raw.replace(/@([^@\s]*)$/, replacer);
},
// 插入@标签
replaceAtUser(user) {
const node = this.node;
if (node && user) {
// 输入框的方法
const content = node || "";
let seleText = "";
// 判断是在整个文本的最后加还是中途加选择后的用户
if (node.length === this.endIndex) {
seleText = content + user.name;
this.text = seleText;
this.$nextTick(() => {
this.$refs.divRef.focus();
});
} else {
seleText =
content.substring(0, this.endIndex) +
user.name +
content.substring(this.endIndex);
this.text = seleText;
this.$nextTick(() => {
this.$refs.divRef.focus();
});
}
// // div的方法
// // const content = node.textContent || "";
// // const endIndex = this.endIndex;
// // const preSlice = this.replaceString(content.slice(0, endIndex), "");
// // const restSlice = content.slice(endIndex);
// // const parentNode = node.parentNode;
// // const nextNode = node.nextSibling;
// // const previousTextNode = new Text(preSlice);
// // const nextTextNode = new Text("\u200b" + restSlice); // 添加 0 宽字符
// // const atButton = this.createAtButton(user);
// // parentNode.removeChild(node);
// // // 插在文本框中
// // if (nextNode) {
// // parentNode.insertBefore(previousTextNode, nextNode);
// // parentNode.insertBefore(atButton, nextNode);
// // parentNode.insertBefore(nextTextNode, nextNode);
// // } else {
// // parentNode.appendChild(previousTextNode);
// // parentNode.appendChild(atButton);
// // parentNode.appendChild(nextTextNode);
// // }
// // // 重置光标的位置
// // const range = new Range();
// // const selection = window.getSelection();
// // range.setStart(nextTextNode, 0);
// // range.setEnd(nextTextNode, 0);
// // selection.removeAllRanges();
// // selection.addRange(range);
}
},
// 键盘抬起事件
handkeKeyUp(e) {
if (this.showAt()) {
// 输入框的方法
const field = this.getTextSelection();
this.node = field.value; //文本内容
this.endIndex = field.selectionEnd; //文本长度
this.position = this.getRangeRect(e);
this.queryString = this.getAtUser() || "";
this.showDialog = true;
// // div的方法
// const node = this.getRangeNode();
// const endIndex = this.getCursorIndex();
// this.node = node;
// this.endIndex = endIndex;
// this.position = this.getRangeRect();
// this.queryString = this.getAtUser() || "";
// this.showDialog = true;
} else {
this.showDialog = false;
}
},
// 键盘按下事件
handleKeyDown(e) {
if (this.showDialog) {
if (
e.code === "ArrowUp" ||
e.code === "ArrowDown" ||
e.code === "Enter"
) {
e.preventDefault();
}
}
},
// 插入标签后隐藏选择框
handlePickUser(user) {
this.replaceAtUser(user);
this.user = user;
this.showDialog = false;
},
// 隐藏选择框
handleHide() {
this.showDialog = false;
},
// 显示选择框
handleShow() {
this.showDialog = true;
},
},
};
</script>
<style lang="scss" scoped></style>
人员列表的弹窗
<template>
<div
class="wrapper"
:style="{
position: 'fixed',
top: position.y + 'px',
left: position.x + 'px',
}"
>
<div v-if="!mockList.length" class="empty">无搜索结果</div>
<div
v-for="(item, i) in mockList"
:key="item.id"
class="item"
:class="{ active: i === index }"
ref="usersRef"
@click="clickAt($event, item)"
@mouseenter="hoverAt(i)"
>
<div class="name">{{ item.name }}</div>
</div>
</div>
</template>
<script>
// const mockData = [
// { name: "HTML", id: "HTML" },
// { name: "CSS", id: "CSS" },
// { name: "Java", id: "Java" },
// { name: "JavaScript", id: "JavaScript" },
// ];
export default {
name: "AtDialog",
props: {
visible: Boolean,
position: Object,
queryString: String,
mockList: Array,
},
data() {
return {
users: [],
index: -1,
// mockList: mockData,
};
},
watch: {
queryString(val) {
// console.log(val);
// val
// ? (this.mockList = mockData.filter(({ name }) => name.startsWith(val)))
// : (this.mockList = mockData.slice(0));
},
},
mounted() {
document.addEventListener("keyup", this.keyDownHandler);
},
destroyed() {
document.removeEventListener("keyup", this.keyDownHandler);
},
methods: {
keyDownHandler(e) {
const field = this.$parent.getTextSelection();
if (e.code === "Escape") {
this.$emit("onHide");
return;
}
if (e.code === "Backspace") {
var field_value = field.value; //文本
const regx = /@([^@\s]*)$/;
// 获取全部文字,然后判断光标结束位置的前面一个字符是不是@
const match = regx.exec(
field_value.slice(field_value.length - 1, field.selectionEnd)
);
if (match && match.length === 2) {
this.$emit("handleShow");
} else {
this.$emit("onHide");
}
return;
}
// // 键盘按下 => ↓
// if (e.code === "ArrowDown") {
// if (this.index >= this.mockList.length - 1) {
// this.index = 0;
// } else {
// this.index = this.index + 1;
// }
// }
// // 键盘按下 => ↑
// if (e.code === "ArrowUp") {
// if (this.index <= 0) {
// this.index = this.mockList.length - 1;
// } else {
// this.index = this.index - 1;
// }
// }
// // 键盘按下 => 回车
// if (e.code === "Enter") {
// if (this.mockList.length) {
// const user = {
// name: this.mockList[this.index].name,
// id: this.mockList[this.index].id,
// };
// this.$emit("onPickUser", user);
// this.index = -1;
// }
// }
},
clickAt(e, item) {
const user = {
name: item.name,
id: item.id,
};
this.$emit("onPickUser", user);
this.index = -1;
},
hoverAt(index) {
// this.index = index;
},
},
};
</script>
<style scoped lang="scss">
.wrapper {
width: 238px;
height: 150px;
overflow: hidden;
overflow-y: scroll;
border: 1px solid #e4e7ed;
border-radius: 4px;
background-color: #fff;
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
box-sizing: border-box;
padding: 6px 0;
}
.empty {
font-size: 14px;
padding: 0 20px;
color: #999;
}
.item {
font-size: 14px;
padding: 0 20px;
line-height: 34px;
cursor: pointer;
color: #606266;
&.active {
background: #f5f7fa;
color: blue;
.id {
color: blue;
}
}
&:hover {
background: #f5f7fa;
color: blue;
.id {
color: blue;
}
}
&:first-child {
border-radius: 5px 5px 0 0;
}
&:last-child {
border-radius: 0 0 5px 5px;
}
.id {
font-size: 12px;
color: rgb(83, 81, 81);
}
}
</style>