|
|
<template>
|
|
|
<div class="custom-at-box">
|
|
|
<div class="custom-textarea-box">
|
|
|
<div
|
|
|
ref="customInput"
|
|
|
:class="[
|
|
|
'custom-textarea custom-scroll',
|
|
|
{ 'show-word-limit': showWordLimit && maxlength },
|
|
|
{ 'custom-textarea-disabled': disabled },
|
|
|
]"
|
|
|
:contenteditable="!disabled"
|
|
|
:placeholder="placeholder"
|
|
|
@input="onInput($event)"
|
|
|
@keydown="onKeyDownInput($event)"
|
|
|
@paste="onPaste($event)"
|
|
|
@copy="onCopy($event)"
|
|
|
@click="showList(false)"
|
|
|
/>
|
|
|
</div>
|
|
|
<div :key="`customInput${taskPanelIsInFullScreen}`">
|
|
|
<el-popover
|
|
|
ref="popoverRef"
|
|
|
v-model="showPopover"
|
|
|
trigger="click"
|
|
|
class="custom-select-box"
|
|
|
:append-to-body="taskPanelPopoverAppendToBody"
|
|
|
:style="{ top: popoverOffset + 'px' }"
|
|
|
@hide="hidePoppver"
|
|
|
>
|
|
|
<div
|
|
|
ref="customSelectContent"
|
|
|
class="custom-select-content custom-scroll"
|
|
|
>
|
|
|
<div
|
|
|
v-for="(item, index) in dataList"
|
|
|
|
|
|
:key="index"
|
|
|
:class="[
|
|
|
'custom-select-item',
|
|
|
{ hoverItem: selectedIndex === index },
|
|
|
]"
|
|
|
@click="handleClickOperatorItem(item)"
|
|
|
>
|
|
|
<div class="custom-select-item-content">
|
|
|
{{ item }}
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</el-popover>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
<script>
|
|
|
import Vue from 'vue'
|
|
|
|
|
|
export default {
|
|
|
props: {
|
|
|
// 输入框placeholder
|
|
|
placeholder: {
|
|
|
type: String,
|
|
|
default: '请输入...'
|
|
|
},
|
|
|
// 是否显示输入字数统计
|
|
|
showWordLimit: {
|
|
|
type: Boolean,
|
|
|
default: true
|
|
|
},
|
|
|
// 是否禁用
|
|
|
disabled: {
|
|
|
type: Boolean,
|
|
|
default: false
|
|
|
},
|
|
|
// 最大输入长度
|
|
|
maxlength: {
|
|
|
type: [Number, String],
|
|
|
default: '3000'
|
|
|
},
|
|
|
// 输入框高度
|
|
|
height: {
|
|
|
type: String,
|
|
|
default: '100px'
|
|
|
},
|
|
|
setRefresh: {
|
|
|
type: Object,
|
|
|
default: () => {}
|
|
|
},
|
|
|
dataList: {
|
|
|
type: Array,
|
|
|
default: () => []
|
|
|
},
|
|
|
// 输入框输入的内容
|
|
|
value: {
|
|
|
type: String,
|
|
|
default: ''
|
|
|
}
|
|
|
},
|
|
|
data() {
|
|
|
return {
|
|
|
// 已输入内容的长度
|
|
|
inputValueLen: 0,
|
|
|
top: '',
|
|
|
left: '',
|
|
|
message: '',
|
|
|
startOffset: 0,
|
|
|
// @搜索人dom
|
|
|
searSpan: null,
|
|
|
|
|
|
// 筛选人数据加载状态
|
|
|
searchOperatorLoad: false,
|
|
|
// @插入位置
|
|
|
selectionIndex: 0,
|
|
|
// 当前编辑的dom
|
|
|
dom: null,
|
|
|
// 当前编辑dom的index
|
|
|
domIndex: 0,
|
|
|
// 当前编辑dom的childNodes的index
|
|
|
childDomIndex: 0,
|
|
|
// 编辑前dom内容
|
|
|
beforeDomVal: '',
|
|
|
// 筛选人选择框
|
|
|
showPopover: false,
|
|
|
// 筛选人选择框偏移量
|
|
|
popoverOffset: 0,
|
|
|
listInput: false,
|
|
|
listInputValue: '',
|
|
|
// 防抖
|
|
|
timer: null,
|
|
|
// 保存弹窗加载状态
|
|
|
addDataLoad: false,
|
|
|
|
|
|
// 鼠标选择人的索引
|
|
|
selectedIndex: 0
|
|
|
}
|
|
|
},
|
|
|
computed: {
|
|
|
// 计算属性,用于同步父组件的数据
|
|
|
model: {
|
|
|
get() {
|
|
|
return this.value
|
|
|
},
|
|
|
set(newValue) {
|
|
|
this.$emit('input', newValue)
|
|
|
if (this.$refs.customInput) {
|
|
|
this.$emit('inputText', this.$refs.customInput.textContent)
|
|
|
}
|
|
|
const nodeList = this.$refs.customInput.childNodes
|
|
|
const list = []
|
|
|
nodeList.forEach((e) => {
|
|
|
if (e.childNodes) {
|
|
|
e.childNodes.forEach(i => {
|
|
|
if (i.className === 'active-text') {
|
|
|
list.push({
|
|
|
jobNumber: i.getAttribute('data-id'),
|
|
|
name: i.textContent.replace(/{/g, '').replace(/\s/g, '')
|
|
|
})
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
if (e.className === 'active-text') {
|
|
|
list.push({
|
|
|
jobNumber: e.getAttribute('data-id'),
|
|
|
name: e.textContent.replace(/{/g, '').replace(/\s/g, '')
|
|
|
})
|
|
|
}
|
|
|
})
|
|
|
this.$emit('changeChosen', list)
|
|
|
}
|
|
|
},
|
|
|
taskPanelIsInFullScreen() {
|
|
|
return this.$store.getters.taskPanelIsInFullScreen
|
|
|
},
|
|
|
taskPanelPopoverAppendToBody() {
|
|
|
return this.$store.getters.taskPanelPopoverAppendToBody
|
|
|
}
|
|
|
},
|
|
|
mounted() {
|
|
|
this.setNativeInputValue()
|
|
|
},
|
|
|
methods: {
|
|
|
// 设置输入框的值
|
|
|
setNativeInputValue() {
|
|
|
if (this.$refs.customInput) {
|
|
|
if (this.value === this.$refs.customInput.innerHTML) return
|
|
|
this.$refs.customInput.innerHTML = this.value
|
|
|
this.inputValueLen = this.$refs.customInput.innerText.length
|
|
|
}
|
|
|
},
|
|
|
// 筛选人弹窗数据选择
|
|
|
handleClickOperatorItem(item) {
|
|
|
this.addData(JSON.parse(JSON.stringify(item)))
|
|
|
this.$refs.customSelectContent.scrollTop = 0
|
|
|
this.selectedIndex = 0
|
|
|
this.showPopover = false
|
|
|
this.listInput = false
|
|
|
this.listInputValue = ''
|
|
|
},
|
|
|
// 艾特人弹窗关闭
|
|
|
hidePoppver() {
|
|
|
this.$refs.customSelectContent.scrollTop = 0
|
|
|
this.selectedIndex = 0
|
|
|
this.showPopover = false
|
|
|
this.listInput = false
|
|
|
this.listInputValue = ''
|
|
|
},
|
|
|
// 创建艾特需要插入的元素
|
|
|
createAtDom(item) {
|
|
|
// 先判断剩余输入长度是否能够完整插入元素
|
|
|
const dom = document.createElement('span')
|
|
|
|
|
|
dom.classList.add('active-text')
|
|
|
// 这里的contenteditable属性设置为false,删除时可以整块删除
|
|
|
dom.setAttribute('contenteditable', 'false')
|
|
|
// 将id存储在dom元素的标签上,便于后续数据处理
|
|
|
dom.setAttribute('data-id', item)
|
|
|
|
|
|
dom.innerHTML = `{${item}} `
|
|
|
|
|
|
return dom
|
|
|
},
|
|
|
// 插入元素
|
|
|
addData(item) {
|
|
|
const spanElement = this.createAtDom(item)
|
|
|
|
|
|
const maxlength = Number(this.maxlength) || 3000
|
|
|
// 因为插入后需要删除之前输入的@,所以判断长度时需要减去这个1
|
|
|
if (maxlength - this.inputValueLen < spanElement.innerText.length - 1) {
|
|
|
this.$message('剩余字数不足')
|
|
|
return
|
|
|
}
|
|
|
this.$refs.customInput.focus()
|
|
|
|
|
|
// 获取当前光标位置的范围
|
|
|
const selection = window.getSelection()
|
|
|
const range = selection.getRangeAt(0)
|
|
|
|
|
|
// 找到要插入的节点
|
|
|
const nodes = Array.from(this.$refs.customInput.childNodes)
|
|
|
let insertNode = ''
|
|
|
// 是否是子元素
|
|
|
let domIsCustomInputChild = true
|
|
|
if (nodes[this.domIndex].nodeType === Node.TEXT_NODE) {
|
|
|
insertNode = nodes[this.domIndex]
|
|
|
} else {
|
|
|
const childNodeList = nodes[this.domIndex].childNodes
|
|
|
insertNode = childNodeList[this.childDomIndex]
|
|
|
domIsCustomInputChild = false
|
|
|
}
|
|
|
|
|
|
// 如果前一个节点是空的文本节点,@用户无法删除
|
|
|
// 添加判断条件:如果前一个节点是空的文本节点,则插入一个空的<span>节点
|
|
|
const html = insertNode.textContent
|
|
|
// 左边的节点
|
|
|
const textLeft = document.createTextNode(
|
|
|
html.substring(0, this.selectionIndex - 1) + ''
|
|
|
)
|
|
|
const emptySpan = document.createElement('span')
|
|
|
|
|
|
// 如果找到了要插入的节点,则在其前面插入新节点
|
|
|
if (insertNode) {
|
|
|
if (!textLeft.textContent) {
|
|
|
if (domIsCustomInputChild) {
|
|
|
this.$refs.customInput.insertBefore(emptySpan, insertNode)
|
|
|
} else {
|
|
|
nodes[this.domIndex].insertBefore(emptySpan, insertNode)
|
|
|
}
|
|
|
}
|
|
|
insertNode.parentNode.insertBefore(spanElement, insertNode.nextSibling)
|
|
|
// 删除多余的@以及搜索条件
|
|
|
const textContent = insertNode.textContent.slice(
|
|
|
0,
|
|
|
-(1 + this.listInputValue.length)
|
|
|
)
|
|
|
if (!textContent && insertNode.nodeName === '#text') {
|
|
|
insertNode.remove()
|
|
|
} else {
|
|
|
insertNode.textContent = textContent
|
|
|
}
|
|
|
} else {
|
|
|
// 如果未找到要插入的节点,则将新节点直接追加到末尾
|
|
|
this.$refs.customInput.appendChild(spanElement)
|
|
|
}
|
|
|
|
|
|
// 将光标移动到 span 元素之后
|
|
|
const nextNode = spanElement.nextSibling
|
|
|
range.setStart(
|
|
|
nextNode || spanElement.parentNode,
|
|
|
nextNode ? 0 : spanElement.parentNode.childNodes.length
|
|
|
)
|
|
|
range.setEnd(
|
|
|
nextNode || spanElement.parentNode,
|
|
|
nextNode ? 0 : spanElement.parentNode.childNodes.length
|
|
|
)
|
|
|
selection.removeAllRanges()
|
|
|
selection.addRange(range)
|
|
|
|
|
|
this.model = this.$refs.customInput.innerHTML
|
|
|
this.inputValueLen = this.$refs.customInput.innerText.length
|
|
|
this.showList(false)
|
|
|
},
|
|
|
// 检查是否发生了全选操作
|
|
|
isSelectAll() {
|
|
|
const selection = window.getSelection()
|
|
|
return selection.toString() === this.$refs.customInput.innerText
|
|
|
},
|
|
|
// 获取输入框是否选中文字
|
|
|
isSelect() {
|
|
|
try {
|
|
|
const selection = window.getSelection()
|
|
|
return selection.toString().length
|
|
|
} catch (error) {
|
|
|
return 0
|
|
|
}
|
|
|
},
|
|
|
// 输入事件
|
|
|
onKeyDownInput(event) {
|
|
|
// 获取当前输入框的长度
|
|
|
const currentLength = this.$refs.customInput.innerText.length
|
|
|
// 获取最大输入长度限制
|
|
|
const maxLength = Number(this.maxlength) || 3000
|
|
|
|
|
|
// 如果按下的键是非控制键并且当前长度已经达到了最大长度限制
|
|
|
if (currentLength >= maxLength) {
|
|
|
// 获取按键的 keyCode
|
|
|
var keyCode = event.keyCode || event.which
|
|
|
|
|
|
// 检查是否按下了 Ctrl 键
|
|
|
var ctrlKey = event.ctrlKey || event.metaKey // metaKey 用于 macOS 上的 Command 键
|
|
|
|
|
|
// 允许的按键:Backspace(8)、Delete(46)、方向键和
|
|
|
var allowedKeys = [8, 46, 37, 38, 39, 40]
|
|
|
|
|
|
// 允许的按键 Ctrl+A、Ctrl+C、Ctrl+V
|
|
|
const allowedCtrlKey = [65, 67, 86]
|
|
|
|
|
|
// 检查按键是否在允许列表中并且没有执行选中操作
|
|
|
if (!allowedKeys.includes(keyCode) && !this.isSelect()) {
|
|
|
if ((allowedCtrlKey.includes(keyCode) && ctrlKey)) {
|
|
|
return
|
|
|
}
|
|
|
// 阻止默认行为
|
|
|
event.preventDefault()
|
|
|
return false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (this.showPopover) {
|
|
|
const listElement = this.$refs.customSelectContent
|
|
|
const itemHeight = listElement.children[0].clientHeight
|
|
|
if (event.key === 'ArrowDown') {
|
|
|
// 防止光标移动
|
|
|
event.preventDefault()
|
|
|
// 移动选中索引
|
|
|
if (this.selectedIndex === this.dataList.length - 1) {
|
|
|
this.selectedIndex = 0 // 跳转到第一项
|
|
|
listElement.scrollTop = 0 // 滚动到列表顶部
|
|
|
} else {
|
|
|
this.selectedIndex++
|
|
|
const itemBottom = (this.selectedIndex + 1) * itemHeight
|
|
|
const scrollBottom = listElement.scrollTop + listElement.clientHeight
|
|
|
if (itemBottom > scrollBottom) {
|
|
|
listElement.scrollTop += itemHeight
|
|
|
}
|
|
|
}
|
|
|
} else if (event.key === 'ArrowUp') {
|
|
|
event.preventDefault()
|
|
|
if (this.selectedIndex === 0) {
|
|
|
this.selectedIndex = this.dataList.length - 1 // 跳转到最后一项
|
|
|
listElement.scrollTop = listElement.scrollHeight // 滚动到列表底部
|
|
|
} else {
|
|
|
this.selectedIndex--
|
|
|
const itemTop = this.selectedIndex * itemHeight
|
|
|
if (itemTop < listElement.scrollTop) {
|
|
|
listElement.scrollTop -= itemHeight
|
|
|
}
|
|
|
}
|
|
|
} else if (event.key === 'Enter') {
|
|
|
event.preventDefault()
|
|
|
if (!this.searchOperatorLoad) {
|
|
|
this.handleClickOperatorItem(
|
|
|
this.dataList[this.selectedIndex]
|
|
|
)
|
|
|
}
|
|
|
}
|
|
|
} else if (event.key === 'Backspace' && this.isSelectAll()) {
|
|
|
// 如果执行了全选操作并删除,清空输入框内容
|
|
|
this.$refs.customInput.innerText = ''
|
|
|
this.model = this.$refs.customInput.innerHTML
|
|
|
this.inputValueLen = 0
|
|
|
}
|
|
|
},
|
|
|
// 监听输入事件
|
|
|
onInput(e) {
|
|
|
this.inputValueLen = this.$refs.customInput.innerText.length
|
|
|
if (
|
|
|
['<div><br></div>', '<br>', '<span></span><br>'].includes(
|
|
|
this.$refs.customInput.innerHTML
|
|
|
)
|
|
|
) {
|
|
|
this.$refs.customInput.innerHTML = ''
|
|
|
this.inputValueLen = 0
|
|
|
} else if (e.data === '{') {
|
|
|
// 保存焦点位置
|
|
|
this.saveIndex()
|
|
|
this.showList()
|
|
|
this.listInput = true
|
|
|
} else if (this.showPopover) {
|
|
|
const { diffChars } = Vue.internal
|
|
|
const diffResult = diffChars(
|
|
|
this.beforeDomVal,
|
|
|
this.dom.textContent
|
|
|
)
|
|
|
let result = ''
|
|
|
// 遍历差异信息数组
|
|
|
for (let i = 0; i < diffResult.length; i++) {
|
|
|
const change = diffResult[i]
|
|
|
|
|
|
// 如果当前差异是添加或修改类型,则将其添加到结果字符串中
|
|
|
if (change.added) {
|
|
|
result += change.value
|
|
|
} else if (change.removed && change.value === '{') {
|
|
|
this.showList(false)
|
|
|
this.listInputValue = ''
|
|
|
}
|
|
|
}
|
|
|
if (this.timer) {
|
|
|
clearTimeout(this.timer)
|
|
|
}
|
|
|
this.listInputValue = result
|
|
|
this.timer = setTimeout(() => {
|
|
|
this.remoteMethod()
|
|
|
}, 300)
|
|
|
}
|
|
|
this.model = this.$refs.customInput.innerHTML
|
|
|
},
|
|
|
onPaste(event) {
|
|
|
event.preventDefault()
|
|
|
// 获取剪贴板中的 HTML 和文本内容
|
|
|
const html = (event.clipboardData || window.clipboardData).getData(
|
|
|
'text/html'
|
|
|
)
|
|
|
const text = (event.clipboardData || window.clipboardData).getData(
|
|
|
'text/plain'
|
|
|
)
|
|
|
|
|
|
// 设置最大输入限制
|
|
|
const maxLength = Number(this.maxlength) || 3000
|
|
|
|
|
|
// 此时加个条件 看鼠标选中的文本长度,剩余可输入长度加上选中文本长度
|
|
|
const selection1 = window.getSelection()
|
|
|
const range1 = selection1.getRangeAt(0)
|
|
|
const clonedSelection = range1.cloneContents()
|
|
|
let selectTextLen = 0
|
|
|
if (clonedSelection.textContent && clonedSelection.textContent.length) {
|
|
|
selectTextLen = clonedSelection.textContent.length
|
|
|
}
|
|
|
|
|
|
// 剩余可输入长度
|
|
|
const remainingLength = maxLength - this.inputValueLen + selectTextLen
|
|
|
|
|
|
// 过滤掉不可见字符
|
|
|
const cleanText = text.replace(/\s/g, '')
|
|
|
|
|
|
// 创建一个临时 div 用于处理粘贴的 HTML 内容
|
|
|
const tempDiv = document.createElement('div')
|
|
|
tempDiv.innerHTML = html
|
|
|
|
|
|
// 过滤掉不需要的内容,例如注释和换行符
|
|
|
const fragment = document.createDocumentFragment()
|
|
|
let totalLength = 0
|
|
|
|
|
|
if (cleanText) {
|
|
|
if (remainingLength >= cleanText.length) {
|
|
|
fragment.appendChild(document.createTextNode(cleanText))
|
|
|
} else {
|
|
|
const truncatedText = cleanText.substr(0, remainingLength)
|
|
|
fragment.appendChild(document.createTextNode(truncatedText))
|
|
|
}
|
|
|
} else {
|
|
|
Array.from(tempDiv.childNodes).forEach((node) => {
|
|
|
const regex = /<span class="active-text" contenteditable="false" data-id="(\d+)">{([^<]+)<\/span>/g
|
|
|
// 过滤注释和空白节点
|
|
|
if (
|
|
|
node.nodeType !== 8 &&
|
|
|
!(node.nodeType === 3 && !/\S/.test(node.textContent))
|
|
|
) {
|
|
|
const childText = node.textContent || ''
|
|
|
const childLength = childText.length
|
|
|
const childHtml = node.outerHTML || node.innerHTML
|
|
|
// 如果剩余空间足够,插入节点
|
|
|
if ((regex.exec(childHtml) !== null) && totalLength + childLength <= remainingLength) {
|
|
|
fragment.appendChild(node.cloneNode(true))
|
|
|
totalLength += childLength
|
|
|
} else if (remainingLength - totalLength > 0) {
|
|
|
// 如果还有剩余长度,不插入节点,插入文本内容
|
|
|
const lastNodeLength = remainingLength - totalLength
|
|
|
const truncatedText = childText.substr(0, lastNodeLength)
|
|
|
fragment.appendChild(document.createTextNode(truncatedText))
|
|
|
totalLength += truncatedText.length
|
|
|
} else {
|
|
|
// 如果添加当前节点的内容会超出剩余可插入长度,则结束循环
|
|
|
return
|
|
|
}
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
|
|
|
// 插入处理后的内容到光标位置
|
|
|
const selection = window.getSelection()
|
|
|
const range = selection.getRangeAt(0)
|
|
|
range.deleteContents()
|
|
|
range.insertNode(fragment)
|
|
|
|
|
|
// 更新输入框内容和长度
|
|
|
this.model = this.$refs.customInput.innerHTML
|
|
|
this.inputValueLen = this.$refs.customInput.innerText.length
|
|
|
|
|
|
// 设置光标位置为插入内容的后面一位
|
|
|
const newRange = document.createRange()
|
|
|
newRange.setStart(range.endContainer, range.endOffset)
|
|
|
newRange.collapse(true)
|
|
|
selection.removeAllRanges()
|
|
|
selection.addRange(newRange)
|
|
|
},
|
|
|
// 修改默认复制事件
|
|
|
onCopy(e) {
|
|
|
e.preventDefault()
|
|
|
const selection = window.getSelection()
|
|
|
const range = selection.getRangeAt(0)
|
|
|
const clonedSelection = range.cloneContents()
|
|
|
|
|
|
// 检查复制的内容是否包含符合条件的元素
|
|
|
const hasActiveText =
|
|
|
clonedSelection.querySelector(
|
|
|
'.active-text[contenteditable="false"][data-id]'
|
|
|
) !== null
|
|
|
|
|
|
const clipboardData = e.clipboardData || window.clipboardData
|
|
|
if (hasActiveText) {
|
|
|
const div = document.createElement('div')
|
|
|
div.appendChild(clonedSelection)
|
|
|
const selectedHtml = div.innerHTML
|
|
|
clipboardData.setData('text/html', selectedHtml)
|
|
|
} else {
|
|
|
clipboardData.setData('text/plain', clonedSelection.textContent || '')
|
|
|
}
|
|
|
},
|
|
|
// 保存焦点位置
|
|
|
async saveIndex() {
|
|
|
const selection = getSelection()
|
|
|
this.selectionIndex = selection.anchorOffset
|
|
|
const nodeList = this.$refs.customInput.childNodes
|
|
|
const range = selection.getRangeAt(0)
|
|
|
|
|
|
// 保存当前编辑的dom节点
|
|
|
for (const [index, value] of nodeList.entries()) {
|
|
|
// 这里第二个参数要配置成true,没配置有其他的一些小bug
|
|
|
// (range.startContainer.contains(value) && range.endContainer.contains(value)) 是为了处理兼容性问题
|
|
|
if (
|
|
|
selection.containsNode(value, true) ||
|
|
|
(range.startContainer.contains(value) &&
|
|
|
range.endContainer.contains(value))
|
|
|
) {
|
|
|
if (value.nodeType === Node.TEXT_NODE) {
|
|
|
this.dom = value
|
|
|
this.beforeDomVal = value.textContent
|
|
|
this.domIndex = index
|
|
|
const selection = window.getSelection()
|
|
|
const range = selection.getRangeAt(0)
|
|
|
this.startOffset = range.startOffset - 1
|
|
|
} else {
|
|
|
const childNodeList = value.childNodes
|
|
|
for (const [childIndex, childValue] of childNodeList.entries()) {
|
|
|
if (selection.containsNode(childValue, true)) {
|
|
|
this.dom = value
|
|
|
this.beforeDomVal = value.textContent
|
|
|
this.domIndex = index
|
|
|
this.childDomIndex = childIndex
|
|
|
const selection = window.getSelection()
|
|
|
const range = selection.getRangeAt(0)
|
|
|
this.startOffset = range.startOffset - 1
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
// 筛选人弹窗
|
|
|
showList(bool = true) {
|
|
|
this.showPopover = bool
|
|
|
if (bool) {
|
|
|
const offset =
|
|
|
this.getCursorDistanceFromDivBottom(this.$refs.customInput) || -1
|
|
|
if (offset < 0) {
|
|
|
this.popoverOffset = 0
|
|
|
} else {
|
|
|
this.popoverOffset = -(offset - 1)
|
|
|
}
|
|
|
}
|
|
|
if (!bool) {
|
|
|
this.listInputValue = ''
|
|
|
this.remoteMethod()
|
|
|
}
|
|
|
},
|
|
|
// 获取光标位置
|
|
|
getCursorDistanceFromDivBottom(editableDiv) {
|
|
|
// 获取选区
|
|
|
const selection = window.getSelection()
|
|
|
// 获取选区的范围
|
|
|
const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null
|
|
|
|
|
|
if (range) {
|
|
|
// 创建一个临时元素来标记范围的结束位置
|
|
|
const markerElement = document.createElement('span')
|
|
|
// 插入临时标记元素
|
|
|
range.insertNode(markerElement)
|
|
|
markerElement.appendChild(document.createTextNode('\u200B')) // 零宽空格
|
|
|
|
|
|
// 获取标记元素的位置信息
|
|
|
const markerOffsetTop = markerElement.offsetTop
|
|
|
const markerHeight = markerElement.offsetHeight
|
|
|
|
|
|
// 计算光标距离div底部的距离
|
|
|
const cursorDistanceFromBottom =
|
|
|
editableDiv.offsetHeight - (markerOffsetTop + markerHeight)
|
|
|
|
|
|
// 滚动条距顶部的高度
|
|
|
const scrollTop = editableDiv.scrollTop || 0
|
|
|
// 移除临时标记元素
|
|
|
markerElement.parentNode.removeChild(markerElement)
|
|
|
|
|
|
// 返回光标距离底部的距离
|
|
|
return cursorDistanceFromBottom + scrollTop
|
|
|
}
|
|
|
|
|
|
// 如果没有选区,则返回-1或者其他错误值
|
|
|
return -1
|
|
|
},
|
|
|
// 搜索筛选人
|
|
|
async remoteMethod() {
|
|
|
const query = this.listInputValue
|
|
|
this.searchOperatorLoad = true
|
|
|
|
|
|
this.searchOperatorLoad = false
|
|
|
},
|
|
|
handleNameShift(item) {
|
|
|
const name = item.realname || ''
|
|
|
if (!name) return '--'
|
|
|
if (name.length > 1) {
|
|
|
return name.slice(0, 1)
|
|
|
} else {
|
|
|
return name
|
|
|
}
|
|
|
},
|
|
|
// 按钮div点击 聚焦textarea
|
|
|
handleBtnBoxClick() {
|
|
|
this.$refs.customInput.focus()
|
|
|
},
|
|
|
getInnerText() {
|
|
|
const customInput = this.$refs.customInput
|
|
|
if (!customInput) return
|
|
|
return customInput.innerText
|
|
|
},
|
|
|
getJobId() {
|
|
|
const nodeList = this.$refs.customInput.childNodes
|
|
|
const list = []
|
|
|
nodeList.forEach((e) => {
|
|
|
if (e.className === 'active-text') {
|
|
|
list.push(e.getAttribute('data-id'))
|
|
|
}
|
|
|
})
|
|
|
return list
|
|
|
},
|
|
|
clearInput() {
|
|
|
this.$refs.customInput.innerText = ''
|
|
|
this.$refs.customInput.innerHTML = ''
|
|
|
this.inputValueLen = 0
|
|
|
this.$emit('input', '')
|
|
|
this.$emit('inputText', '')
|
|
|
this.$emit('changeChosen', [])
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
|
<style lang="scss" scoped>
|
|
|
.custom-textarea-btn {
|
|
|
position: absolute;
|
|
|
bottom: 1px;
|
|
|
right: 4px;
|
|
|
left: 4px;
|
|
|
text-align: right;
|
|
|
// background: #fff;
|
|
|
padding-bottom: 3px;
|
|
|
.el-button {
|
|
|
font-size: 12px;
|
|
|
padding: 4px 10px;
|
|
|
}
|
|
|
}
|
|
|
.custom-textarea-box {
|
|
|
position: relative;
|
|
|
}
|
|
|
.custom-at-limit {
|
|
|
position: absolute;
|
|
|
right: 12px;
|
|
|
bottom: 4px;
|
|
|
font-size: 12px;
|
|
|
color: #999;
|
|
|
line-height: 12px;
|
|
|
}
|
|
|
::v-deep.custom-textarea {
|
|
|
white-space: pre-wrap;
|
|
|
height: calc(100vh - 500px);
|
|
|
border: 1px solid #dcdfe6;
|
|
|
border-radius: 4px;
|
|
|
background-color: #ffffff;
|
|
|
padding: 5px 15px;
|
|
|
color: #606266;
|
|
|
overflow-y: auto;
|
|
|
line-height: 20px;
|
|
|
font-size: 14px;
|
|
|
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
|
|
|
position: relative;
|
|
|
word-break: break-all;
|
|
|
&.show-word-limit {
|
|
|
padding-bottom: 16px;
|
|
|
}
|
|
|
&.custom-textarea-disabled {
|
|
|
cursor: not-allowed;
|
|
|
background-color: #f5f7fa;
|
|
|
border-color: #e4e7ed;
|
|
|
color: #c0c4cc;
|
|
|
}
|
|
|
&:focus {
|
|
|
border-color: #f98600 !important;
|
|
|
}
|
|
|
&:empty::before {
|
|
|
content: attr(placeholder);
|
|
|
font-size: 14px;
|
|
|
color: #c0c4cc;
|
|
|
}
|
|
|
.active-text {
|
|
|
color: #909399;
|
|
|
// padding: 2px 6px;
|
|
|
// background: #f4f4f5;
|
|
|
margin-right: 4px;
|
|
|
// border-radius: 4px;
|
|
|
// font-size: 12px;
|
|
|
}
|
|
|
// &:focus::before {
|
|
|
// content: "";
|
|
|
// }
|
|
|
}
|
|
|
|
|
|
::v-deep.custom-select-box {
|
|
|
position: relative;
|
|
|
|
|
|
.el-popover {
|
|
|
padding: 0;
|
|
|
top: 0;
|
|
|
box-shadow: 0 4px 8px 0 rgba(89, 88, 88, 0.8);
|
|
|
}
|
|
|
.custom-select-content {
|
|
|
width: 259px;
|
|
|
padding: 8px;
|
|
|
max-height: 260px;
|
|
|
overflow-y: auto;
|
|
|
}
|
|
|
|
|
|
.custom-select-item {
|
|
|
// font-size: 14px;
|
|
|
// padding: 0 20px;
|
|
|
// position: relative;
|
|
|
// height: 34px;
|
|
|
// line-height: 34px;
|
|
|
// box-sizing: border-box;
|
|
|
display: flex;
|
|
|
padding: 8px 12px;
|
|
|
border-bottom: 1px solid #ebebeb;
|
|
|
align-items: center;
|
|
|
color: #606266;
|
|
|
cursor: pointer;
|
|
|
&:last-child {
|
|
|
border-bottom: none;
|
|
|
}
|
|
|
.avatar-box {
|
|
|
flex-shrink: 0;
|
|
|
.custom-select-item-avatar {
|
|
|
width: 24px;
|
|
|
height: 24px;
|
|
|
background-color: #ffb803;
|
|
|
border-radius: 50%;
|
|
|
text-align: center;
|
|
|
line-height: 24px;
|
|
|
color: #ffffff;
|
|
|
}
|
|
|
}
|
|
|
.custom-select-item-content {
|
|
|
flex: 1;
|
|
|
padding-left: 12px;
|
|
|
white-space: nowrap;
|
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
|
}
|
|
|
&:hover {
|
|
|
background-color: #f5f7fa;
|
|
|
}
|
|
|
&.hoverItem {
|
|
|
background-color: #dbdbdb;
|
|
|
}
|
|
|
}
|
|
|
.custom-select-empty {
|
|
|
padding: 10px 0;
|
|
|
text-align: center;
|
|
|
color: #999;
|
|
|
font-size: 14px;
|
|
|
|
|
|
&.load {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
}
|
|
|
}
|
|
|
.custom-scroll {
|
|
|
overflow: auto;
|
|
|
&::-webkit-scrollbar {
|
|
|
width: 8px;
|
|
|
height: 8px;
|
|
|
}
|
|
|
&::-webkit-scrollbar-thumb {
|
|
|
border-radius: 8px;
|
|
|
background-color: #b4b9bf;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
</style>
|