React组件封装:文字、表情评论框

1.需求描述

根据项目需求,采用Antd组件库需要封装一个评论框,具有以下功能:

  • 支持文字输入
  • 支持常用表情包选择
  • 支持发布评论
  • 支持自定义表情包

2.封装代码

 ./InputComment.tsx

  1 import React, { useState, useRef, useEffect, forwardRef, useImperativeHandle } from 'react';
  2 import { SmileOutlined } from '@ant-design/icons';
  3 import { Row, Col, Button, Tooltip, message } from 'antd';
  4 
  5 import styles from './index.less';
  6 
  7 import {setCursorPostionEnd} from "./util";
  8 
  9 const emojiPath = '/emojiImages/';
 10 const emojiSuffix = '.png';
 11 const emojiList = [...Array(15).keys()].map((_, index: number) => {
 12   return { id: index + 1, path: emojiPath + (index + 1) + emojiSuffix };
 13 });
 14 
 15 type Props = {
 16   uniqueId: string; // 唯一键
 17   item?: object; // 携带参数
 18   okClick: Function; // 发布
 19   okText?: string;
 20 };
 21 
 22 const InputComment = forwardRef((props: Props, ref) => {
 23   const { uniqueId: id, okClick, okText } = props;
 24   const inputBoxRef = useRef<any>(null);
 25   const [textCount, setTextCount] = useState(0);
 26   let rangeOfInputBox: any;
 27   const uniqueId = 'uniqueId_' + id;
 28 
 29   const setCaretForEmoji = (target: any) => {
 30     if (target?.tagName?.toLowerCase() === 'img') {
 31       const range = new Range();
 32       range.setStartBefore(target);
 33       range.collapse(true);
 34       // inputBoxRef?.current?.removeAllRanges();
 35       // inputBoxRef?.current?.addRange(range);
 36       const sel = window.getSelection();
 37       sel?.removeAllRanges();
 38       sel?.addRange(range);
 39     }
 40   };
 41 
 42   /**
 43    * 输入框点击
 44    */
 45   const inputBoxClick = (event: any) => {
 46     const target = event.target;
 47     setCaretForEmoji(target);
 48   };
 49 
 50   /**
 51    * emoji点击
 52    */
 53   const emojiClick = (item: any) => {
 54     const emojiEl = document.createElement('img');
 55     emojiEl.src = item.path;
 56     const dom = document.getElementById(uniqueId);
 57     const html = dom?.innerHTML;
 58 
 59     // rangeOfInputBox未定义并且存在内容时,将光标移动到末尾
 60     if (!rangeOfInputBox && !!html) {
 61       dom.innerHTML = html + `<img src="${item.path}"/>`;
 62       setCursorPostionEnd(dom)
 63     } else {
 64       if (!rangeOfInputBox) {
 65         rangeOfInputBox = new Range();
 66         rangeOfInputBox.selectNodeContents(inputBoxRef.current);
 67       }
 68 
 69       if (rangeOfInputBox.collapsed) {
 70         rangeOfInputBox.insertNode(emojiEl);
 71       } else {
 72         rangeOfInputBox.deleteContents();
 73         rangeOfInputBox.insertNode(emojiEl);
 74       }
 75       rangeOfInputBox.collapse(false);
 76 
 77       const sel = window.getSelection();
 78       sel?.removeAllRanges();
 79       sel?.addRange(rangeOfInputBox);
 80     }
 81   };
 82 
 83   /**
 84    * 选择变化事件
 85    */
 86   document.onselectionchange = (e) => {
 87     if (inputBoxRef?.current) {
 88       const element = inputBoxRef?.current;
 89       const doc = element.ownerDocument || element.document;
 90       const win = doc.defaultView || doc.parentWindow;
 91       const selection = win.getSelection();
 92 
 93       if (selection?.rangeCount > 0) {
 94         const range = selection?.getRangeAt(0);
 95         if (inputBoxRef?.current?.contains(range?.commonAncestorContainer)) {
 96           rangeOfInputBox = range;
 97         }
 98       }
 99     }
100   };
101 
102   /**
103    * 获取内容长度
104    */
105   const getContentCount = (content: string) => {
106     return content
107       .replace(/&nbsp;/g, ' ')
108       .replace(/<br>/g, '')
109       .replace(/<\/?[^>]*>/g, '占位').length;
110   };
111 
112   /**
113    * 发送
114    */
115   const okSubmit = () => {
116     const content = inputBoxRef.current.innerHTML;
117     if (!content) {
118       return message.warning('温馨提示:请填写评论内容!');
119     } else if (getContentCount(content) > 1000) {
120       return message.warning(`温馨提示:评论或回复内容小于1000字!`);
121     }
122 
123     okClick(content);
124   };
125 
126   /**
127    * 清空输入框内容
128    */
129   const clearInputBoxContent = () => {
130     inputBoxRef.current.innerHTML = '';
131   };
132 
133   // 将子组件的方法 暴露给父组件
134   useImperativeHandle(ref, () => ({
135     clearInputBoxContent,
136   }));
137 
138   // 监听变化
139   useEffect(() => {
140     const dom = document.getElementById(uniqueId);
141     const observer = new MutationObserver(() => {
142       const content = dom?.innerHTML ?? '';
143       // console.log('Content changed:', content);
144       setTextCount(getContentCount(content));
145     });
146 
147     if (dom) {
148       observer.observe(dom, {
149         attributes: true,
150         childList: true,
151         characterData: true,
152         subtree: true,
153       });
154     }
155   }, []);
156 
157   return (
158     <div style={{ marginTop: 10, marginBottom: 10 }} className={styles.inputComment}>
159       {textCount === 0 ? (
160         <div className="input-placeholder">
161           {okText === '确认' ? '回复' : '发布'}评论,内容小于1000字!
162         </div>
163       ) : null}
164 
165       <div
166         ref={inputBoxRef}
167         id={uniqueId}
168         contentEditable={true}
169         placeholder="adsadadsa"
170         className="ant-input input-box"
171         onClick={inputBoxClick}
172       />
173       <div className="input-emojis">
174         <div className="input-count">{textCount}/1000</div>
175 
176         <Row wrap={false}>
177           <Col flex="auto">
178             <Row wrap={true} gutter={[0, 10]} align="middle" style={{ userSelect: 'none' }}>
179               {emojiList.map((item, index: number) => {
180                 return (
181                   <Col
182                     flex="none"
183                     onClick={() => {
184                       emojiClick(item);
185                     }}
186                  
187           <Col flex="none" style={{ marginTop: 5 }}>
188             <Button
189               type="primary"
190               disabled={textCount === 0}
191               onClick={() => {
192                 okSubmit();
193               }}
194             >
195               {okText || '发布'}
196             </Button>
197           </Col>
198         </Row>
199       </div>
200     </div>
201   );
202 });
203 
204 export default InputComment;

./util.ts

 1 /**
 2  * 光标放到文字末尾(获取焦点时)
 3  * @param el 
 4  */
 5 export function setCursorPostionEnd(el:any) {
 6   if (window.getSelection) {
 7     // ie11 10 9 ff safari
 8     el.focus() // 解决ff不获取焦点无法定位问题
 9     const range = window.getSelection() // 创建range
10     range?.selectAllChildren(el) // range 选择obj下所有子内容
11     range?.collapseToEnd() // 光标移至最后
12   } else if (document?.selection) {
13     // ie10 9 8 7 6 5
14     const range = document?.selection?.createRange() // 创建选择对象
15     // var range = document.body.createTextRange();
16     range.moveToElementText(el) // range定位到obj
17     range.collapse(false) // 光标移至最后
18     range.select()
19   }
20 }

 

 3.问题解决

  • 同一页面有多个评论框时,光标位置不准确?答:从组件外部传入唯一ID标识,进行区分。
  • 表情包存放位置?答:表情包存放在/public/emojiImages/**.png,命名规则1、2、3、4……

4.组件展示

 

上一篇:前后端分离开发【Yapi平台】【Swagger注解自动生成接口文档平台】


下一篇:无忧微服务:如何实现大流量下新版本的发布*