Due to recent iterations of the product, we need to add a comment function and support the insertion of custom emoticons. Textarea, like me, is the first thing that comes to mind, but textarea does not support image insertion. Our emojis are inserted into text as images, so here’s a new feature for HTML5, Contenteditable. Make the contents of div editable. The demo address
Take a look at the renderings:
Component functions:
- Support for inserting custom emoticons
- Character verification, automatic cutting of the excess part (calculated by characters, not by length, 2 characters for a Chinese, 1 character for a letter, 4 characters for an expression), because our company’s business is to use characters for word count, so the verification is to use characters, length can be modified by itself
- Insert emoticons exactly where the cursor is
- Support multiple sets of emojis
Problems encountered :(list problems first, then write solutions)
- If there are forced updates to the displayed content (such as inserting emojis, or cutting characters), the cursor moves to the front of the text
- Vue can not be directly used for two-way binding, and V-Model and V-HTML can not be used for two-way data binding
- Position the cursor. If you click on the emoticon while typing, insert it at the top of the cursor. If you don’t click on the emoticon while typing, insert it at the bottom of the text
- How are emoji placeholders guaranteed to be unique in the processing of text byte computations
- How to cut when the characters beyond the word limit are expressions
- Pressing the loop inserts the
tag, or wraps the text around the
tag - Pressing the space character yields The space character Will be treated as six characters, when in fact the space represents one
Compatibility issues:
- Internet Explorer 9 and some Safari CreatecontexTualFragments are not compatible with this method
- The compatibility of window.getSelection, window.getSelection().getrangeat and other methods. Since our business does not require compatibility with lower versions of IE, I did not make compatibility here
Begin to implement
The HTML part is relatively simple in the CSS part, but it is unnecessary to describe, first do not paste the code, at the end of the article to see the complete code or directly download the complete small demo run to see
parameter
Since it is encapsulated as a component, you need to receive data from the parent component
submitCmtLoading: { // If yes, there is no need to repeat the submission
type: Boolean.default: false
},
limtText: { // limit the number of characters, the default is 0,0 is not limited
type: Number.default: 0
},
iconWidth: { // Insert the width of the expression
type: Number.dafault: 24
},
iconHeight: { // Insert the expression height
type: Number.dafault: 24
},
cmt_show: { // Whether to display components
type: Boolean.default: true
},
iconList: { // List of emojis, in the form of an array and must be passed
type: Array.required: true
},
placeholder: { / / the clues
type: String.default: "Positive responses attract more comments."
},
info: { // Information, which depends on business requirements, can be passed if specific items need to be processed in child components
type: Object
}
Copy the code
As the HTML code has not been pasted, this.$refs.cmt_input appears below as input boxes, both elements with attributes contenteditable
data
data: function() {
return {
content: "".// Comment on the input
widFouce: "".// Used to locate the cursor position before the input box loses focus
rangeFouce: "".// Used to locate the cursor position before the input box loses focus
iconShow: false.// Whether to display the emoticon list
isSubmit: false If the input is empty, it will not be submitted
};
},
watch: {
cmt_show: { // When the component is displayed, you need to position the cursor to the end of the text, otherwise the first click on the emoticon will report an error
handler(value) {
if (value) {
this.$nextTick(() = > {
this.toLast(this.$refs.cmt_input); // Move the cursor to the end of the text}); }},immediate: true}},Copy the code
While the list of emojis is expanding, click on the other non-emojis area to make the emojis disappear.
mounted() {
const self = this;
document
.getElementsByClassName("g-doc") [0]
.addEventListener("click", self.setHideClick); // G-doc is the root node. You can also replace it with body, but the time is removed when the component is destroyed
self.$once("hook:beforeDestroy".() = > {
document.getElementsByClassName("g-doc") [0] &&
document
.getElementsByClassName("g-doc") [0]
.removeEventListener("click", self.setHideClick);
});
},
// the setHideClick method is placed in the method
setHideClick(event) {
let target = event.srcElement;
let nameList = ["icon_item"."icon_list"."icon_box"]; // These three class names are the entire emoji list
if(! nameList.includes(target.className) && ! nameList.includes(target.parentElement.className) ) {// Close the emoji list panel if it is not in the emoji list area
this.iconShow = false; }},Copy the code
methods
A few simple methods, but not much explanation
Problem: If there is a forced update to the displayed content (such as inserting emojis, or cutting characters), the cursor will move to the front of the text. Solution: After each update, call the toLast(obj) method to move the cursor back to the rear
toLast(obj) {
// Move the cursor to the end, obj is the input box node
if (window.getSelection) {
let winSel = window.getSelection(); winSel.selectAllChildren(obj); winSel.collapseToEnd(); }},blurInput() { // The out-of-focus method is triggered to save the cursor position when out of focus
this.getFouceInput();
},
getFouceInput() { // Save the cursor position
this.widFouce = window.getSelection();
this.rangeFouce = this.widFouce.getRangeAt(0);
},
showIconListPlane() { // Displays or closes the list of emojis
// Displays a list of emojis
if (!this.iconShow) { // If open, save the cursor position
this.getFouceInput();
}
this.iconShow = !this.iconShow;
},
submitCmt() {
// Submit comments
const self = this;
let text = this.$refs.cmt_input.innerHTML;
let length = this.getCharLen(this.paseText(text).text);
if(self.submitCmtLoading || ! self.isSubmit)return;
if (self.cmt_text == "") {
self.$emit("submitError"."Comments are empty"); // Method submitError is raised if the rule does not conform
return;
} else if (this.limtText && length > this.limtText) {
self.$emit("submitError"."Comments out of character"); // Method submitError is raised if the rule does not conform
return;
}
self.$emit("submitSuccess", text, self.info); // If the validation passes, the correct method is thrown, passing both text and information to the parent component
}
Copy the code
Verify the characters
Next, we will talk about the character validation function, because the cursor correlation logic is useful to part of the character validation, so we will first talk about the character correlation method. Judgment text accounts for how many characters, incoming text is treated here, because if not handled if take expressions in the text of the package, package is img tags in the text and expression that will occupy a lot of characters, so will do a placeholder with expression package, let a face pack only four characters, the back can speak this. Now let’s look at the way to determine the number of characters, because if it’s an input space it’s going to be So this is also going to be handled in this method
getCharLen(sSource) {
/ / space & have spent We count as one character, so we subtract 5 from each space
// Get the character length
var l = 0;
var schar;
for (var i = 0; (schar = sSource.charAt(i)); i++) {
l += schar.match(/[^\x00-\xff]/) != null ? 2 : 1;
}
let nbsp = sSource.match(/ /gi);
if (nbsp) {
let len = nbsp.length;
l = l - len * 5;
}
return l;
},
Copy the code
Use placeholders instead of emojis. Since an emoji is 4 bytes, 4 numbers are used to hold the space. A random number of 4 numbers is used to retrieve the timestamp, and then compared to the text, and recursively retrieved again until the 4 numbers are not present in the text. The return key adds div and br tags to the text. Since our comments don’t allow manual line breaks, replace both div and BR tags with two Spaces.
This solves all three of these problems
- How are emoji placeholders guaranteed to be unique in the processing of text byte computations
- Pressing the loop inserts the
tag, or wraps the text around the
tag - Pressing the space character yields The space character Will be treated as six characters, when in fact the space represents one
getRandomFour(str) {
// Take a random 4-digit key in the timestamp and recurse if the text contains a key
let timeStr = new Date().getTime() + "";
let result = "";
for (let i = 0; i < 4; i++) {
let nums = Math.floor(Math.random() * 13);
result = result + timeStr[nums];
}
return str.indexOf(result) == -1 ? result : this.getRandomFour(str);
},
paseText(str) {
// Parse the contents and replace all images with placeholders. Replace all images with two Spaces related to the newline (div, br)
let str1 = str.replace(/<div>|<\/div>|<br>/gi."");
let imgReg = /
]*src[=\'\"\s]+([^\"\']*)[\"\']? [^>]*>/gi
[^>;
let imgMatch = str1.match(imgReg);
let key = this.getRandomFour(str1);
let isReplace = false;
if (imgMatch) {
isReplace = true;
for (let i = 0; i < imgMatch.length; i++) { str1 = str1.replace(imgMatch[i], key); }}return {
isReplace, // If there is no placeholder replacement, there is no need to restore
key,
text: str1
};
},
reductionStr(sourceText, text, key) {
SourceText: original text: replaced text key: placeholder identifier
let imgReg = /
]*src[=\'\"\s]+([^\"\']*)[\"\']? [^>]*>/gi
[^>;
let imgMatch = sourceText.match(imgReg);
let result = text;
if (imgMatch) {
for (let i = 0; i < imgMatch.length; i++) { result = result.replace(key, imgMatch[i]); }}return result;
},
Copy the code
When cutting text beyond limits, note: If it is a placeholder images, to the need to be cut, not cut part, or it will produce the extra text Such as more than one character, but the final package for expression, and the expression package placeholders for four Numbers, when wanting to cut to remove four Numbers together, can not only remove one behind such as key values don’t match.
This will solve the problem mentioned above
- How to cut when the characters beyond the word limit are expressions
setCmtTextByLimit(sSource, key) {
let self = this;
// Text cutting is required if the text limit is exceeded
if (typeofsSource ! = ="string") return;
var str = changeLast(sSource, key); // Check whether the last one is an emoji
if (this.getCharLen(str) <= this.limtText) return str;
while (this.getCharLen(str) > this.limtText) {
str =
4 + str.lastIndexOf(key) == str.length
? str.substring(0, str.length - 4)
: str.substring(0, str.length - 1);
}
function changeLast(strl, key) {
// Cut until the last one is not an emoji or does not exceed the limit
while (
4 + strl.lastIndexOf(key) == strl.length &&
self.getCharLen(strl) > self.limtText
) {
strl = sSource.substring(0, sSource.length - 4);
}
return strl;
}
return str;
},
Copy the code
Input and cursor
The text should be transmitted to the parent component in real time when input, and the character judgment should be carried out when input, and the input cannot be exceeded. Of course, the verification should be made first. If no verification is required, the following steps should be omitted.
changeText(e) {
// Emoticon insertion does not trigger this method, only input
// To judge the number of words, change the custom emoji into a placeholder
let text = e.srcElement.innerHTML;
let emitText = text;
if (this.limtText) {
let textObj = this.paseText(text); // Text replacement
let len = this.getCharLen(textObj.text); // Get the character length
if (len > this.limtText) {
let str = this.setCmtTextByLimit(textObj.text, textObj.key); // Character cutting
emitText = textObj.isReplace
? this.reductionStr(text, str, textObj.key)
: str; // Restore the text if there is a substitution
e.srcElement.innerHTML = emitText;
this.toLast(e.srcElement); // Place the cursor at the end of the mandatory modification}}this.isSubmit = emitText.length ? true : false; // If the input field is not empty, it can be submitted
this.$emit("changeText", emitText);
},
Copy the code
When inserting an emoticon, it is necessary to determine whether adding four characters exceeds the character limit. If so, the emoticon will not be inserted. If the last cursor is in the input box, insert the expression at the cursor position and position the cursor behind the expression; if the cursor is not in the input box, insert the emoticons directly at the end of the text. For creating nodes createContextualFragment in Internet explorer 9 is not compatible with part of the safari browser, can change to createElement method to create, of course also can be written as createElement method, I’ll say createContextualFragment but I’m too lazy to change that hahaha.
Solve a problem:
- Position the cursor. If you click on the emoticon while typing, insert it at the top of the cursor. If you don’t click on the emoticon while typing, insert it at the bottom of the text
- Internet Explorer 9 and some Safari CreatecontexTualFragments are not compatible with this method
insertIcon(url) { // Insert emoticon, url is emoticon address
// Determine whether the number of words exceeded, if the number of words exceeded, do not insert
const self = this;
this.isSubmit || (this.isSubmit = true);
const length = this.getCharLen(
this.paseText(this.$refs.cmt_input.innerHTML).text,
);
if (this.limtText && length + 4 > this.limtText) return;
const img = `<img src='${url}' width=The ${this.iconWidth} height=The ${this.iconHeight}/ > `;
IE9 is not compatible with IE9, but it is convenient to make a judgment after flexible control
if (window.getSelection && window.getSelection().getRangeAt) {
const winSn = this.widFouce;
let range = this.rangeFouce;
// To determine the cursor status, if the cursor was not in the input box last time, you need to manually locate it in the input box
if( winSn.focusNode.className ! = ='content_edit'&& winSn.focusNode.parentElement.className ! = ='content_edit'
) {
winSn.selectAllChildren(self.$refs.cmt_input); // Select the element in the input box
winSn.collapseToEnd(); // Position the cursor to the end of the text
range = winSn.getRangeAt(0);
}
range.collapse(false);
let node;
if (range.createContextualFragment) {
// The img node is compatible with IE9 and Safari
node = range.createContextualFragment(img);
} else {
const tempDom = document.createElement('div');
tempDom.innerHTML = img;
node = tempDom;
}
const dom = node.firstChild;
range.insertNode(dom); // Add the emoji node to the text
const clRang = range.cloneRange(); // Copy the range object. Note that this is not a reference, so making changes to the copied object will not affect the original object
clRang.setStartAfter(dom); // Set the cursor behind the emoticons node, the emoticons in the text here are still selected, and the range is set here after cloning
winSn.removeAllRanges(); // Remove the selected state
winSn.addRange(clRang); // Add the cloned range
self.$emit('changeText', self.$refs.cmt_input.innerHTML); // We can't listen for emoticon input because we listen for input, so we need to throw another method to the parent component
} else {
console.log('incompatible with ~'); }},Copy the code
Another problem is that vUE cannot be directly used for two-way binding, and V-Model and V-HTML cannot be used for two-way data binding: Because input boxes are implemented through contenteditable, bi-directional binding of text content with v-Model is not possible, nor is bi-binding with V-HTML. So here is the way to pass up the latest text in real time by listening to the input method and inserting emojis.
The HTML code
<! -- Parent component -->
<Comment
:info="info"
:submitCmtLoading="submitCmtLoading"
:limtText="limtText"
:iconWidth="iconWidth"
:iconHeight="iconHeight"
:cmt_show="cmt_show"
:iconList="iconList"
@changeText="changeText"
@submitError="submitError"
@submitSuccess="submitSuccess"
/>
<! -- Subcomponent -->
<div v-if="cmt_show" class="cmt_box">
<div
ref="cmt_input"
class="content_edit"
contenteditable="true"
:placeholder="placeholder"
@focus="changeHandle"
@input="changeText"
@blur="blurInput"
v-html="content"
></div>
<div class="cmt_handle">
<img
id="emoticon_icon"
v-if="iconList.length"
src="//www1.pconline.com.cn/20200929/pgc/cmt/icon.png"
@click.stop="showIconListPlane"
/>
<div
@click="submitCmt()"
:class="['btn_submit', isSubmit ? 'btn_submit_y' : '']"
>release</div>
<div v-show="iconShow" class="icon_list">
<ul class="icon_box">
<li
class="icon_item"
v-for="(item, index) in iconList"
:key="`icon${index}`"
@click="insertIcon(item)"
>
<img :src="item" />
</li>
</ul>
</div>
</div>
</div>
Copy the code
Component complete code
<template>
<div v-if="cmt_show" class="cmt_box">
<div
ref="cmt_input"
class="content_edit"
contenteditable="true"
:placeholder="placeholder"
@focus="changeHandle"
@input="changeText"
@blur="blurInput"
@paste="pasteInput"
v-html="content"
></div>
<div class="cmt_handle">
<img
id="emoticon_icon"
v-if="iconList.length"
src="//www1.pconline.com.cn/20200929/pgc/cmt/icon.png"
@click.stop="showIconListPlane"
/>
<div
@click="submitCmt()"
:class="['btn_submit', isSubmit ? 'btn_submit_y' : '']"
>release</div>
<div v-show="iconShow" class="icon_list">
<div class="icon_tabs">
<div
v-for="(item, index) in iconList"
:key="`tab_${index}`"
:class="['tabs_item', iconIndex === index ? 'tabs_cur' : '']"
:style=" `width:${item.width ? item.width : 30}px; height:${ item.height ? item.height : 30 }px; `"
@click="changeIconIndex(index)"
>
<img :src="item.icon" />
</div>
</div>
<ul
class="icon_box"
v-for="(item, index) in iconList"
:key="`tab_${index}`"
v-show="iconIndex === index"
>
<li
class="icon_item"
v-for="(iconItem, iconIndex) in item.list"
:key="`icon${iconIndex}`"
@click="insertIcon(iconItem)"
>
<img :src="iconItem" />
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Comment",
props: {
submitCmtLoading: {
type: Boolean,
default: false
},
limtText: {
type: Number,
default: 0
},
iconWidth: {
type: Number,
dafault: 24
},
iconHeight: {
type: Number,
dafault: 24
},
cmt_show: {
type: Boolean,
default: true
},
iconList: {
type: Array,
required: true
},
placeholder: {
type: String,
default: "积极回复可吸引更多人评论"
},
filterPaste: {
type: Boolean,
default: true
},
info: {
type: Object
}
},
data: function() {
return {
content: "",
widFouce: "",
rangeFouce: "",
iconIndex: 0,
iconShow: false,
isSubmit: false
};
},
watch: {
content: function(val, oldVal) {
console.log(val, "-", oldVal);
},
cmt_show: {
handler(value) {
if (value) {
this.$nextTick(() => {
this.toLast(this.$refs.cmt_input);
});
}
},
immediate: true
}
},
mounted() {
const self = this;
document.addEventListener("click", self.setHideClick);
self.$once("hook:beforeDestroy", () => {
document.getElementById("app") &&
document
.getElementById("app")
.removeEventListener("click", self.setHideClick);
});
},
methods: {
setHideClick(event) {
let target = event.srcElement;
!this.isAncestorsDom(target, "icon_list") && (this.iconShow = false);
},
changeHandle() {
console.log("blur");
},
changeText(e) {
// 表情包插入时不触发该方法,只有输入时会触发
// 判断字数,要先把自定义表情改成占位符,一个自定义表情按俩2个字符算
let text = e.srcElement.innerHTML;
let emitText = text;
if (this.limtText) {
let textObj = this.paseText(text);
let len = this.getCharLen(textObj.text);
if (len > this.limtText) {
let str = this.setCmtTextByLimit(textObj.text, textObj.key);
emitText = textObj.isReplace
? this.reductionStr(text, str, textObj.key)
: str;
e.srcElement.innerHTML = emitText;
this.toLast(e.srcElement);
}
}
this.isSubmit = emitText.length ? true : false;
this.$emit("changeText", emitText);
},
paseText(str) {
// 解析内容,把图片全部用占位替换掉 跟换行相关(div,br)全部改成两个空格
let str1 = str.replace(/<div>|<\/div>|<br>/gi, " ");
// eslint-disable-next-line no-useless-escape
let imgReg = /<img[^>]*src[=\'\"\s]+([^\"\']*)[\"\']?[^>]*>/gi;
let imgMatch = str1.match(imgReg);
let key = this.getRandomFour(str1);
let isReplace = false;
if (imgMatch) {
isReplace = true;
for (let i = 0; i < imgMatch.length; i++) {
str1 = str1.replace(imgMatch[i], key);
}
}
return {
isReplace,
key,
text: str1
};
},
/**
* @description: 复原评论内容
*/
reductionStr(sourceText, text, key) {
// 解析内容,把图片全部用占位替换掉
// eslint-disable-next-line no-useless-escape
let imgReg = /<img[^>]*src[=\'\"\s]+([^\"\']*)[\"\']?[^>]*>/gi;
let imgMatch = sourceText.match(imgReg);
let result = text;
if (imgMatch) {
for (let i = 0; i < imgMatch.length; i++) {
result = result.replace(key, imgMatch[i]);
}
}
return result;
},
getRandomFour(str) {
// 在时间戳里取随机4位数作为key,且如果文本中包含key则递归
let timeStr = new Date().getTime() + "";
let result = "";
for (let i = 0; i < 4; i++) {
let nums = Math.floor(Math.random() * 13);
result = result + timeStr[nums];
}
return str.indexOf(result) == -1 ? result : this.getRandomFour(str);
},
getCharLen(sSource) {
// 空格 要当1个字符算,所以最后要给每个空格减去5
// 获取字符长度
var l = 0;
var schar;
for (var i = 0; (schar = sSource.charAt(i)); i++) {
// eslint-disable-next-line no-control-regex
l += schar.match(/[^\x00-\xff]/) != null ? 2 : 1;
}
let nbsp = sSource.match(/ /gi);
if (nbsp) {
let len = nbsp.length;
l = l - len * 5;
}
return l;
},
setCmtTextByLimit(sSource, key) {
let self = this;
// 文字切割 如果超出文字限制需要切割
if (typeof sSource !== "string") return;
var str = changeLast(sSource, key);
if (this.getCharLen(str) <= this.limtText) return str;
while (this.getCharLen(str) > this.limtText) {
str =
4 + str.lastIndexOf(key) == str.length
? str.substring(0, str.length - 4)
: str.substring(0, str.length - 1);
}
function changeLast(strl, key) {
// 一直切割到最后一个不为表情包或不超出限制为止
while (
4 + strl.lastIndexOf(key) == strl.length &&
self.getCharLen(strl) > self.limtText
) {
strl = sSource.substring(0, sSource.length - 4);
}
return strl;
}
return str;
},
showIconListPlane() {
// 显示表情包列表
if (!this.iconShow) {
this.getFouceInput();
}
this.iconShow = !this.iconShow;
// iconShow
},
getFouceInput() {
this.widFouce = window.getSelection();
this.rangeFouce = this.widFouce.getRangeAt(0);
},
toLast(obj) {
// 将光标移到最后
if (window.getSelection) {
let range = window.getSelection();
range.selectAllChildren(obj);
range.collapseToEnd();
}
},
insertIcon(url) {
// 判断是否超出字数,如果超出不给插入
const self = this;
this.isSubmit || (this.isSubmit = true);
let length = this.getCharLen(
this.paseText(this.$refs.cmt_input.innerHTML).text
);
if (this.limtText && length + 4 > this.limtText) return;
const img = `<img src='${url}' width=${this.iconWidth} height=${this.iconHeight} />`;
// 兼容性判断 如果不兼容不往下执行,虽然说不兼容IE9以下,但是还是做一下判断 方便后面灵活控制
if (window.getSelection && window.getSelection().getRangeAt) {
let winSn = this.widFouce,
range = this.rangeFouce;
// 要判断的光标状态
if (
winSn.focusNode.className !== "content_edit" &&
winSn.focusNode.parentElement.className !== "content_edit" &&
!this.isAncestorsDom(winSn.baseNode, "content_edit")
) {
winSn.selectAllChildren(self.$refs.cmt_input);
winSn.collapseToEnd();
range = winSn.getRangeAt(0);
}
range.collapse(false);
let node;
if (range.createContextualFragment) {
// 兼容IE9跟safari
node = range.createContextualFragment(img);
} else {
let tempDom = document.createElement("div");
tempDom.innerHTML = img;
node = tempDom;
}
let dom = node.firstChild;
range.insertNode(dom);
let clRang = range.cloneRange();
clRang.setStartAfter(dom);
winSn.removeAllRanges();
winSn.addRange(clRang);
self.$emit("changeText", self.$refs.cmt_input.innerHTML);
} else {
console.log("不兼容~");
}
},
blurInput() {
this.getFouceInput();
},
isAncestorsDom(dom, className) {
let tempDom = dom;
if (!tempDom || !className) return false;
if (
~tempDom.nodeName.toUpperCase().indexOf("DOCUMENT") ||
~tempDom.nodeName.toUpperCase().indexOf("HTML")
)
return false;
while (tempDom.nodeName.toUpperCase() !== "BODY") {
if (
tempDom.classList &&
Array.from(tempDom.classList).includes(className)
) {
return true;
}
tempDom = tempDom.parentElement;
}
return false;
},
submitCmt() {
// 提交评论
const self = this;
let text = this.$refs.cmt_input.innerHTML;
let length = this.getCharLen(this.paseText(text).text);
if (self.submitCmtLoading || !self.isSubmit) return;
if (self.cmt_text == "") {
self.$emit("submitError", "评论为空");
return;
} else if (this.limtText && length > this.limtText) {
self.$emit("submitError", "评论超出字数");
return;
}
self.$emit("submitSuccess", text, self.info);
},
changeIconIndex(index) {
this.iconIndex = index;
},
pasteInput(event) {
if (!this.filterPaste) return;
let text = event.clipboardData.getData("text");
const selection = window.getSelection();
if (!selection.rangeCount) return false;
selection.deleteFromDocument();
selection.getRangeAt(0).insertNode(document.createTextNode(text));
selection.getRangeAt(0).collapse(false);
event.preventDefault();
}
}
};
</script>
<style lang="scss" scoped>
.cmt_box {
width: 510px;
height: 180px;
background-color: #ffffff;
border-radius: 2px;
border: solid 1px #ececec;
padding: 14px;
box-sizing: border-box;
margin: auto;
.content_edit {
width: 100%;
height: 120px;
outline: none;
border: none;
text-align: left;
font-size: 14px;
&:empty::before {
color: #cccccc;
content: attr(placeholder);
}
}
.cmt_handle {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
#emoticon_icon {
width: 21px;
height: 21px;
display: block;
flex-shrink: 0;
cursor: pointer;
}
.btn_submit {
width: 68px;
height: 32px;
background-color: #cccccc;
border-radius: 16px;
text-align: center;
line-height: 32px;
color: #ffffff;
font-size: 14px;
cursor: pointer;
&.btn_submit_y {
background-color: #f95354;
}
}
.icon_list {
position: absolute;
top: 40px;
left: -10px;
min-width: 280px;
border-radius: 2px;
background: #fff;
box-shadow: 0 5px 18px 0 rgba(0, 0, 0, 0.16);
padding: 15px;
&::before {
content: "";
width: 0;
height: 0;
display: block;
padding: 0;
position: absolute;
top: -10px;
left: 10px;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-bottom: 10px solid #fff;
}
.icon_tabs {
border-bottom: solid 1px #ccc;
display: flex;
position: relative;
.tabs_item {
position: relative;
padding-bottom: 4px;
img {
width: 100%;
height: 100%;
}
& + .tabs_item {
margin-left: 10px;
}
&.tabs_cur::before {
content: "";
width: 100%;
height: 2px;
background-color: #f95354;
display: block;
position: absolute;
bottom: -1px;
}
}
}
.icon_box {
list-style: none;
display: flex;
justify-content: flex-start;
align-items: center;
flex-wrap: wrap;
padding: 10px 0;
margin: 0;
.icon_item {
padding: 5px;
text-align: center;
cursor: pointer;
> img {
width: 30px;
height: 30px;
}
}
}
}
}
}
</style>
Copy the code
Finally, the demo address is attached to my front primary school students, welcome to exchange corrections ~