| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549 |
- <template>
- <div class="chat-input-area">
- <div :class="['edit-chat-container', isEmpty ? '' : 'not-empty']" contenteditable="true"
- @paste.prevent="onPaste" @keydown="onKeydown" @compositionstart="compositionFlag = true"
- @compositionend="onCompositionEnd" @input="onEditorInput" @mousedown="onMousedown" ref="content"
- @blur="onBlur">
- </div>
- <chat-at-box @select="onAtSelect" :search-text="atSearchText" ref="atBox" :ownerId="ownerId"
- :members="groupMembers"></chat-at-box>
- </div>
- </template>
- <script>
- import ChatAtBox from "./ChatAtBox";
- export default {
- name: "ChatInput",
- components: { ChatAtBox },
- props: {
- ownerId: {
- type: Number,
- },
- groupMembers: {
- type: Array,
- },
- },
- data() {
- return {
- imageList: [],
- fileList: [],
- currentId: 0,
- atSearchText: null,
- compositionFlag: false,
- atIng: false,
- isEmpty: true,
- changeStored: true,
- blurRange: null
- }
- },
- methods: {
- onPaste(e) {
- this.isEmpty = false;
- let txt = e.clipboardData.getData('Text')
- let range = window.getSelection().getRangeAt(0)
- if (range.startContainer !== range.endContainer || range.startOffset !== range.endOffset) {
- range.deleteContents();
- }
- // 粘贴图片和文件时,这里没有数据
- if (txt && typeof(txt) == 'string') {
- let textNode = document.createTextNode(txt);
- range.insertNode(textNode)
- range.collapse();
- return;
- }
- let items = (e.clipboardData || window.clipboardData).items
- if (items.length) {
- for (let i = 0; i < items.length; i++) {
- if (items[i].type.indexOf('image') !== -1) {
- let file = items[i].getAsFile();
- let imagePush = {
- fileId: this.generateId(),
- file: file,
- url: URL.createObjectURL(file)
- };
- this.imageList[imagePush.fileId] = (imagePush);
- let line = this.newLine();
- let imageElement = document.createElement('img');
- imageElement.className = 'chat-image no-text';
- imageElement.src = imagePush.url;
- imageElement.dataset.imgId = imagePush.fileId;
- line.appendChild(imageElement);
- let after = document.createTextNode('\u00A0');
- line.appendChild(after);
- this.selectElement(after, 1);
- } else {
- let asFile = items[i].getAsFile();
- if (!asFile) {
- continue;
- }
- let filePush = { fileId: this.generateId(), file: asFile };
- this.fileList[filePush.fileId] = (filePush)
- let line = this.newLine();
- let fileElement = this.createFile(filePush);
- line.appendChild(fileElement);
- let after = document.createTextNode('\u00A0');
- line.appendChild(after);
- this.selectElement(after, 1);
- }
- }
- }
- range.collapse();
- },
- selectElement(element, endOffset) {
- let selection = window.getSelection();
- // 插入元素可能不是立即执行的,vue可能会在插入元素后再更新dom
- this.$nextTick(() => {
- let t1 = document.createRange();
- t1.setStart(element, 0);
- t1.setEnd(element, endOffset || 0);
- if (element.firstChild) {
- t1.selectNodeContents(element.firstChild);
- }
- t1.collapse();
- selection.removeAllRanges();
- selection.addRange(t1);
- // 需要时自动聚焦
- if (element.focus) {
- element.focus();
- }
- })
- },
- onCompositionEnd(e) {
- this.compositionFlag = false;
- this.onEditorInput(e);
- },
- onKeydown(e) {
- if (e.keyCode === 13) {
- e.preventDefault();
- e.stopPropagation();
- if (this.atIng) {
- this.$refs.atBox.select();
- return;
- }
- if (e.ctrlKey) {
- let line = this.newLine();
- let after = document.createTextNode('\u00A0');
- line.appendChild(after);
- this.selectElement(line.childNodes[0], 0);
- } else {
- // 中文输入标记
- if (this.compositionFlag) {
- return;
- }
- this.submit();
- }
- return;
- }
- // 删除键
- if (e.keyCode === 8) {
- // 等待dom更新
- setTimeout(() => {
- let s = this.$refs.content.innerHTML.trim();
- // 空dom时,需要刷新dom
- if (s === '' || s === '<br>' || s === '<div> </div>') {
- // 拼接随机长度的空格,以刷新dom
- this.empty();
- this.isEmpty = true;
- this.selectElement(this.$refs.content);
- } else {
- this.isEmpty = false;
- }
- })
- }
- // at框打开时,上下键移动特殊处理
- if (this.atIng) {
- if (e.keyCode === 38) {
- e.preventDefault();
- e.stopPropagation();
- this.$refs.atBox.moveUp();
- }
- if (e.keyCode === 40) {
- e.preventDefault();
- e.stopPropagation();
- this.$refs.atBox.moveDown();
- }
- }
- },
- onAtSelect(member) {
- this.atIng = false;
- // 选中输入的 @xx 符
- let blurRange = this.blurRange;
- let endContainer = blurRange.endContainer
- let startOffset = endContainer.data.indexOf("@" + this.atSearchText);
- let endOffset = startOffset + this.atSearchText.length + 1;
- blurRange.setStart(blurRange.endContainer, startOffset);
- blurRange.setEnd(blurRange.endContainer, endOffset);
- blurRange.deleteContents()
- blurRange.collapse();
- this.focus();
- // 创建元素节点
- let element = document.createElement('SPAN')
- element.className = "chat-at-user";
- element.dataset.id = member.userId;
- element.contentEditable = 'false'
- element.innerText = `@${member.showNickName}`
- blurRange.insertNode(element)
- // 光标移动到末尾
- blurRange.collapse()
- // 插入空格
- let textNode = document.createTextNode('\u00A0');
- blurRange.insertNode(textNode);
- blurRange.collapse()
- this.atSearchText = "";
- this.selectElement(textNode, 1);
- },
- onEditorInput(e) {
- this.isEmpty = false;
- this.changeStored = false;
- if (this.$props.groupMembers && !this.compositionFlag) {
- let selection = window.getSelection()
- let range = selection.getRangeAt(0);
- // 截取@后面的名称作为过滤条件,并以空格结束
- let endContainer = range.endContainer;
- let endOffset = range.endOffset;
- let textContent = endContainer.textContent;
- let startIndex = -1;
- for (let i = endOffset; i >= 0; i--) {
- if (textContent[i] === '@') {
- startIndex = i;
- break;
- }
- }
- // 没有at符号,则关闭弹窗
- if (startIndex === -1) {
- this.$refs.atBox.close();
- return;
- }
- let endIndex = endOffset;
- for (let i = endOffset; i < textContent.length; i++) {
- if (textContent[i] === ' ') {
- endIndex = i;
- break;
- }
- }
- this.atSearchText = textContent.substring(startIndex + 1, endIndex).trim();
- // 打开选择弹窗
- if (this.atSearchText == '') {
- this.showAtBox(e)
- }
- }
- },
- onBlur(e) {
- if (!this.atIng) {
- this.updateRange();
- }
- },
- onMousedown() {
- if (this.atIng) {
- this.$refs.atBox.close();
- this.atIng = false;
- }
- },
- insertEmoji(emojiText) {
- let emojiElement = document.createElement('img');
- emojiElement.className = 'emoji-normal no-text';
- emojiElement.dataset.emojiCode = emojiText;
- emojiElement.src = this.$emo.textToUrl(emojiText);
- let blurRange = this.blurRange;
- if (!blurRange) {
- this.focus();
- this.updateRange();
- blurRange = this.blurRange;
- }
- if (blurRange.startContainer !== blurRange.endContainer || blurRange.startOffset !== blurRange.endOffset) {
- blurRange.deleteContents();
- }
- blurRange.insertNode(emojiElement);
- blurRange.collapse()
- let textNode = document.createTextNode('\u00A0');
- blurRange.insertNode(textNode)
- blurRange.collapse()
- this.selectElement(textNode);
- this.updateRange();
- this.isEmpty = false;
- },
- generateId() {
- return this.currentId++;
- },
- createFile(filePush) {
- let file = filePush.file;
- let fileId = filePush.fileId;
- let container = document.createElement('div');
- container.className = 'chat-file-container no-text';
- container.contentEditable = 'false';
- container.dataset.fileId = fileId;
- let left = document.createElement('div');
- left.className = 'file-position-left';
- container.appendChild(left);
- let icon = document.createElement('div');
- icon.className = 'el-icon-document';
- left.appendChild(icon);
- let right = document.createElement('div');
- right.className = 'file-position-right';
- container.appendChild(right);
- let fileName = document.createElement('div');
- fileName.className = 'file-name';
- fileName.innerText = file.name;
- let fileSize = document.createElement('div');
- fileSize.className = 'file-size';
- fileSize.innerText = this.sizeConvert(file.size);
- right.appendChild(fileName);
- right.appendChild(fileSize);
- return container;
- },
- sizeConvert(len) {
- if (len < 1024) {
- return len + 'B';
- } else if (len < 1024 * 1024) {
- return (len / 1024).toFixed(2) + 'KB';
- } else if (len < 1024 * 1024 * 1024) {
- return (len / 1024 / 1024).toFixed(2) + 'MB';
- } else {
- return (len / 1024 / 1024 / 1024).toFixed(2) + 'GB';
- }
- },
- updateRange() {
- let selection = window.getSelection();
- this.blurRange = selection.getRangeAt(0);
- },
- newLine() {
- let selection = window.getSelection();
- let range = selection.getRangeAt(0);
- let divElement = document.createElement('div');
- let endContainer = range.endContainer;
- let parentElement = endContainer.parentElement;
- if (parentElement.parentElement === this.$refs.content) {
- divElement.innerHTML = endContainer.textContent.substring(range.endOffset).trim();
- endContainer.textContent = endContainer.textContent.substring(0, range.endOffset);
- // 插入到当前div(当前行)后面
- parentElement.insertAdjacentElement('afterend', divElement);
- } else {
- divElement.innerHTML = '';
- this.$refs.content.append(divElement);
- }
- return divElement;
- },
- clear() {
- this.empty();
- this.imageList = [];
- this.fileList = [];
- this.$refs.atBox.close();
- },
- empty() {
- this.$refs.content.innerHTML = "";
- let line = this.newLine();
- let after = document.createTextNode('\u00A0');
- line.appendChild(after);
- this.$nextTick(() => this.selectElement(after));
- },
- showAtBox(e) {
- this.atIng = true;
- // show之后会自动更新当前搜索的text
- // this.atSearchText = "";
- let selection = window.getSelection()
- let range = selection.getRangeAt(0)
- // 光标所在坐标
- let pos = range.getBoundingClientRect();
- this.$refs.atBox.open({
- x: pos.x,
- y: pos.y
- })
- // 记录光标所在位置
- this.updateRange();
- },
- submit() {
- let nodes = this.$refs.content.childNodes;
- let fullList = [];
- let tempText = '';
- let atUserIds = [];
- let each = (nodes) => {
- for (let i = 0; i < nodes.length; i++) {
- let node = nodes[i];
- if (!node) {
- continue;
- }
- if (node.nodeType === 3) {
- tempText += node.textContent;
- continue;
- }
- let nodeName = node.nodeName.toLowerCase();
- if (nodeName === 'script') {
- continue;
- }
- let text = tempText.trim();
- if (nodeName === 'img') {
- let imgId = node.dataset.imgId;
- if (imgId) {
- if (text) {
- fullList.push({
- type: 'text',
- content: text,
- atUserIds: atUserIds
- })
- tempText = '';
- atUserIds = []
- }
- fullList.push({
- type: 'image',
- content: this.imageList[imgId]
- })
- } else {
- let emojiCode = node.dataset.emojiCode;
- tempText += emojiCode;
- }
- } else if (nodeName === 'div') {
- let fileId = node.dataset.fileId
- // 文件
- if (fileId) {
- if (text) {
- fullList.push({
- type: 'text',
- content: text,
- atUserIds: atUserIds
- })
- tempText = '';
- atUserIds = []
- }
- fullList.push({
- type: 'file',
- content: this.fileList[fileId]
- })
- } else {
- tempText += '\n';
- each(node.childNodes);
- }
- } else if (nodeName === 'span') {
- if (node.dataset.id) {
- tempText += node.innerHTML;
- atUserIds.push(node.dataset.id)
- } else if (node.outerHtml) {
- tempText += node.outerHtml;
- }
- }
- }
- }
- each(nodes)
- let text = tempText.trim();
- if (text !== '') {
- fullList.push({
- type: 'text',
- content: text,
- atUserIds: atUserIds
- })
- }
- this.$emit('submit', fullList);
- },
- focus() {
- this.$refs.content.focus();
- }
- }
- }
- </script>
- <style lang="scss">
- .chat-input-area {
- width: 100%;
- height: 100%;
- position: relative;
- .edit-chat-container {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- outline: none;
- padding: 5px;
- line-height: 26px;
- font-size: var(--im-font-size);
- text-align: left;
- overflow-y: auto;
- // 单独一行时,无法在前面输入的bug
- >div:before {
- content: "\00a0";
- font-size: 14px;
- position: absolute;
- top: 0;
- left: 0;
- }
- .chat-image {
- display: block;
- max-width: 200px;
- max-height: 100px;
- border: 1px solid #e6e6e6;
- cursor: pointer;
- }
- .chat-file-container {
- max-width: 65%;
- padding: 10px;
- border: 2px solid #587ff0;
- display: flex;
- background: #eeeC;
- border-radius: 10px;
- .file-position-left {
- display: flex;
- width: 80px;
- justify-content: center;
- align-items: center;
- .el-icon-document {
- font-size: 40px;
- text-align: center;
- color: #d42e07;
- }
- }
- .file-position-right {
- flex: 1;
- .file-name {
- font-size: 16px;
- font-weight: 600;
- color: #66b1ff;
- }
- .file-size {
- font-size: 14px;
- font-weight: 600;
- }
- }
- }
- .chat-at-user {
- color: #00f;
- border-radius: 3px;
- }
- }
- .edit-chat-container>div:nth-of-type(1):after {
- content: '请输入消息(按Ctrl+Enter键换行)';
- color: gray;
- }
- .edit-chat-container.not-empty>div:nth-of-type(1):after {
- content: none;
- }
- }
- </style>
|