
sandBox.vue
<template>
<div class="content">
<div
ref="divRef"
class="editor"
contenteditable
@keyup="handkeKeyUp"
@keydown="handleKeyDown"
/>
<sandText
v-if="showDialog"
:visible="showDialog"
:position="position"
:query-string="queryString"
@onPickUser="handlePickUser"
@onHide="handleHide"
@onShow="handleShow"
/>
<el-checkbox v-model="checked">附订单号</el-checkbox>
<el-button type="primary" @click="logA">主要按钮</el-button>
<el-button ref="btn" type="text" @click="handleIn">@</el-button>
</div>
</template>
<script>
import sandText from './sandText'
export default {
name: 'SandBox',
components: { sandText },
data() {
return {
userList: [],
checked: '',
node: '',
user: '',
endIndex: '',
queryString: '',
showDialog: false,
position: {
x: 0,
y: 0
}
}
},
mounted() {
this.handleIn()
},
methods: {
distinct1(arr, key) {
var newobj = {}
var newArr = []
for (var i = 0; i < arr.length; i++) {
var item = arr[i]
if (!newobj[item[key]]) {
newobj[item[key]] = newArr.push(item)
}
}
return newArr
},
handleIn() {
this.$refs.divRef.focus()
document.execCommand('selectAll', false, null)
document.getSelection().collapseToEnd()
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
},
logA() {
const text = this.$refs.divRef.innerHTML
const text2 = this.escape2Html(text)
let list = []
for (let index = 0; index < this.userList.length; index++) {
if (text2.includes(`"id":"${this.userList[index].id}"`)) {
list.push(this.userList[index])
}
}
list = this.distinct1(list, 'id')
console.log(this.$refs.divRef.innerText, list)
},
escape2Html(str) {
var arrEntities = { lt: '<', gt: '>', nbsp: ' ', amp: '&', quot: '"' }
return str.replace(/&(lt|gt|nbsp|amp|quot);/gi, function(all, t) {
return arrEntities[t]
})
},
getCursorIndex() {
const selection = window.getSelection()
return selection.focusOffset
},
getRangeNode() {
const selection = window.getSelection()
return selection.focusNode
},
getRangeRect() {
const selection = window.getSelection()
const range = selection.getRangeAt(0)
const rect = range.getClientRects()[0]
const LINE_HEIGHT = 30
if (rect === undefined) {
this.$refs.divRef.innerHTML = '\u200b'
const rect = range.getClientRects()[0]
console.log(rect)
}
return {
x: rect.x,
y: rect.y + LINE_HEIGHT
}
},
showAt() {
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.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)
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() {
if (this.showAt()) {
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
}
if (this.$refs.divRef.innerText.length > 100) {
this.$refs.divRef.innerText = this.$refs.divRef.innerText.substr(
0,
100
)
document.execCommand('selectAll', false, null)
document.getSelection().collapseToEnd()
}
},
handleKeyDown(e) {
if (this.showDialog) {
if (
e.code === 'ArrowUp' ||
e.code === 'ArrowDown' ||
e.code === 'Enter'
) {
e.preventDefault()
}
}
},
handlePickUser(user) {
this.userList.push(user)
this.replaceAtUser(user)
this.user = user
this.showDialog = false
},
handleHide() {
this.showDialog = false
},
handleShow() {
this.showDialog = true
}
}
}
</script>
<style scoped lang="scss">
.content {
width: 100%;
font-family: sans-serif;
h1 {
text-align: center;
}
}
.editor {
margin: 0 auto;
width: 100%;
height: 150px;
background: #fff;
border: 1px solid #ccc;
border-radius: 5px;
text-align: left;
padding: 10px;
overflow: auto;
line-height: 30px;
&:focus {
outline: none;
}
}
</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"
ref="usersRef"
class="item"
:class="{'active': i === index}"
@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: 'CSS1' },
{ name: 'Java', id: 'Java1' },
{ name: 'JavaScript', id: 'JavaScript1' }
]
export default {
name: 'SandText',
props: {
visible: {
type: Boolean,
default: true
},
queryString: {
type: String,
default: ''
},
position: {
type: Object,
default: () => {
return {}
}
}
},
data() {
return {
users: [],
index: -1,
mockList: mockData
}
},
watch: {
queryString(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) {
if (e.code === 'Escape') {
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;
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;
z-index: 100;
}
.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;
}
}
&: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>