Browse Source

!87 自定义聊天输入框

Merge pull request !87 from 柒木/feature/fx_20240806_custom_editor
master
blue 2 years ago
committed by Gitee
parent
commit
565320a0c2
No known key found for this signature in database GPG Key ID: 173E9B9CA92EEF8F
  1. 1900
      im-web/src/components/chat/ChatBox.vue
  2. 665
      im-web/src/components/chat/ChatInput.vue
  3. 11
      im-web/src/components/chat/ChatMessageItem.vue

1900
im-web/src/components/chat/ChatBox.vue

File diff suppressed because it is too large

665
im-web/src/components/chat/ChatInput.vue

@ -0,0 +1,665 @@
<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="compositionFlag=false;updateRange()"
@input="onEditorInput"
@mousedown="onMousedown"
v-html="contentHtml"
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() {
const defaultContentHtml = '<div></div>';
return {
// contentList: [],
// content: null,
defaultContentHtml,
contentHtml: defaultContentHtml + '',
imageList: [],
fileList: [],
currentId: 0,
atSearchText: null,
compositionFlag: false,
history: [defaultContentHtml],
atIng: false,
isEmpty: true,
changeStored: true,
blurRange: null
}
}, methods: {
onPaste(e) {
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)
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 divElement = this.newLine();
let text = document.createTextNode('\u00A0');
divElement.appendChild(text);
let imageElement = document.createElement('img');
imageElement.className = 'chat-image no-text';
imageElement.src = imagePush.url;
imageElement.dataset.imgId = imagePush.fileId;
divElement.appendChild(imageElement);
let after = document.createTextNode('\u00A0');
divElement.appendChild(after);
this.selectElement(after, 1);
// range.insertNode(divElement);
} 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 text = document.createTextNode('\u00A0');
line.appendChild(text);
let fileElement = this.createFile(filePush);
line.appendChild(fileElement);
let after = document.createTextNode('\u00A0');
line.appendChild(after);
this.selectElement(after, 1);
// fileElement.insertAdjacentHTML('afterend', '\u00A0');
}
}
}
range.collapse();
},
selectElement(element, endOffset) {
let selection = window.getSelection();
// vuedom
setTimeout(() => {
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();
}
})
},
onKeydown(e) {
if (e.keyCode === 13) {
e.preventDefault();
e.stopPropagation();
if (this.atIng) {
console.log('选中at的人')
this.$refs.atBox.select();
return;
}
if (e.shiftKey) {
let divElement = this.newLine();
this.selectElement(divElement);
} else {
//
if (this.compositionFlag) {
return;
}
this.submit();
}
return;
}
if (e.keyCode === 90) {
// Ctrl+Zmaccommand+z
if (e.ctrlKey || e.metaKey) {
// ctrl+zctrl+zlow
e.preventDefault();
e.stopPropagation();
if (this.history.length <= 1) {
return;
}
if (this.changeStored) {
this.history.pop();
}
let last = this.history.pop();
// console.log('', last);
this.contentHtml = last;
if (this.history.length === 0) {
//
this.history.push(this.defaultContentHtml);
}
// dom
setTimeout(() => {
let t = this.$refs.content.lastElementChild.lastChild;
if (!t || !t.textContent) {
return;
}
this.selectElement(t, t.textContent.length);
})
}
}
//
if (e.keyCode === 8) {
// dom
setTimeout(() => {
let s = this.$refs.content.innerHTML.trim();
// domdom
if (s === '' || s === '<br>' || s === '<div><br></div>' || s === '<div><br/></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 startOffset = blurRange.endOffset - this.atSearchText.length - 1;
blurRange.setStart(blurRange.endContainer, startOffset);
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.aliasName}`
blurRange.insertNode(element)
//
blurRange.collapse()
//
let textNode = document.createTextNode('\u00A0');
blurRange.insertNode(textNode);
blurRange.collapse()
this.atSearchText = "";
this.selectElement(textNode, 1);
},
onEditorInput(e) {
// timeoutcompositionend
this.isEmpty = false;
this.changeStored = false;
setTimeout(() => {
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;
}
//
this.showAtBox(e)
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();
}
})
},
onBlur(e) {
this.updateRange();
},
onMousedown() {
if (this.atIng) {
this.$refs.atBox.close();
this.atIng = false;
}
},
insertEmoji(emojiText) {
let emojiElement = document.createElement('img');
emojiElement.className = 'chat-emoji 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 = 'file-icon';
icon.innerText = '?';
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;
let newText = endContainer.textContent.substring(range.endOffset).trim();
endContainer.textContent = endContainer.textContent.substring(0, range.endOffset);
divElement.innerHTML = newText || '';
// atparentbug
if (parentElement === this.$refs.content) {
this.$refs.content.append(divElement);
} else {
// div
parentElement.insertAdjacentElement('afterend', divElement);
}
this.isEmpty = false;
return divElement;
},
clear() {
this.empty();
this.imageList = [];
this.fileList = [];
},
empty() {
let emptyCount = Math.random() * 100 + 5;
let content = '';
for (let i = 0; i < emptyCount; i++) {
content += ' ';
}
this.contentHtml = this.defaultContentHtml + content;
},
showAtBox(e) {
this.atIng = true;
// showtext
// 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() {
// console.log(this.content)
let nodes = this.$refs.content.childNodes;
let textList = [];
let imageList = [];
let fileList = [];
let fullList = [];
let tempText = '';
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
})
}
fullList.push({
type: 'image',
content: this.imageList[imgId]
})
imageList.push(this.imageList[imgId]);
textList.push(text);
tempText = '';
} 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
})
}
fullList.push({
type: 'file',
content: this.fileList[fileId]
})
fileList.push(this.fileList[fileId]);
textList.push(text);
tempText = '';
} else {
tempText += '\n';
each(node.childNodes);
}
} else if (nodeName === 'span') {
let userId = node.dataset.id;
if (userId !== null && userId !== undefined) {
tempText += node.outerHTML;
}
} else {
console.warn('未处理的标签');
if (getComputedStyle(node).display === 'block') {
tempText += '\n';
} else {
tempText += ' ';
}
each(node.childNodes);
}
}
}
each(nodes)
let text = tempText.trim();
if (text !== '') {
fullList.push({
type: 'text',
content: text
})
textList.push(text);
}
// console.log(textList, imageList, fileList)
this.$emit('submit', fullList, textList, imageList, fileList);
},
focus() {
this.$refs.content.focus();
}
},
mounted() {
// console.log(this.$props.groupMembers)
// this.$refs.content.firstElementChild.focus();
this.selectElement(this.$refs.content.firstElementChild);
setInterval(() => {
if (!this.$refs.content) {
return;
}
//
if (this.compositionFlag) {
return;
}
let last = this.history[this.history.length - 1];
let newContent = this.$refs.content.innerHTML;
if (last !== newContent) {
this.history.push(newContent);
}
this.changeStored = true;
}, 1000);
}
}
</script>
<style lang="scss">
.chat-input-area {
width: 100%;
height: 100%;
position: relative;
.edit-chat-container {
//width: 100%;
//height: 100%;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 1px solid #c3c3c3;
outline: none;
padding: 10px 0;
line-height: 30px;
font-size: 16px;
text-align: left;
> div {
padding-left: 10px;
//width: 1px;
min-height: 30px;
}
// bug
> div:before {
content: "\00a0";
font-size: 14px;
position: absolute;
top: 0;
left: 0;
}
.chat-image {
max-width: 200px;
max-height: 100px;
border: 1px solid #e6e6e6;
cursor: pointer;
//margin-left: 10px;
}
.chat-emoji {
width: 30px;
height: 30px;
vertical-align: top;
cursor: pointer;
}
.chat-file-container {
width: 200px;
//height: 100px;
padding: 5px 7px;
border: 1px solid #4cd964;
display: flex;
.file-position-left {
width: 30%;
flex: 1;
justify-content: center;
align-items: center;
.file-icon {
width: 30px;
height: 30px;
text-align: center;
justify-content: center;
align-items: center;
vertical-align: center;
border: 1px solid #e6e6e6;
}
}
.file-position-right {
width: 70%;
flex: 3;
.file-name {
font-size: 14px;
color: #333;
}
.file-size {
font-size: 12px;
color: #666;
}
}
}
.chat-at-user {
background-color: #4cd964;
padding: 2px 5px;
color: white;
//border: 1px solid #c3c3c3;
border-radius: 3px;
}
}
.edit-chat-container > div:nth-of-type(1):empty:after {
content: '请输入消息(按Shift+Enter键换行)';
color: gray;
}
.edit-chat-container > div:nth-of-type(1):focus:after {
content: none;
}
.edit-chat-container.not-empty > div:nth-of-type(1):after {
content: none;
}
}
</style>

11
im-web/src/components/chat/ChatMessageItem.vue

@ -280,6 +280,7 @@
position: absolute;
left: -10px;
top: 13px;
z-index: -1;
width: 0;
height: 0;
border-style: solid dashed dashed;
@ -407,6 +408,14 @@
color: #329432;
}
}
.chat-at-user {
background-color: #4cd964;
padding: 2px 5px;
color: white;
//border: 1px solid #c3c3c3;
border-radius: 3px;
}
}
}
@ -442,7 +451,7 @@
background-color: rgb(88, 127, 240);
color: #fff;
vertical-align: top;
&:after {
left: auto;
right: -10px;

Loading…
Cancel
Save