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

  • React组件封装:文字、表情评论框已关闭评论
  • 69 次浏览
  • A+
所属分类:Web前端
摘要

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

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 }

 ./index.less

 1 .inputComment {  2   position: relative;  3   4   :global {  5     .input-placeholder {  6       position: absolute;  7       top: 11px;  8       left: 13px;  9       z-index: 0; 10       color: #dddddd; 11     } 12  13     .input-box { 14       height: 100px; 15       padding: 10px; 16       overflow: auto; 17       background-color: transparent; 18       border: 1px solid #dddddd; 19       border-top-left-radius: 3px; 20       border-top-right-radius: 3px; 21       resize: vertical; 22  23       img { 24         height: 18px; 25         vertical-align: middle; 26       } 27     } 28  29     .input-emojis { 30       margin-top: -7px; 31       padding: 10px; 32       border: 1px solid #dddddd; 33       border-top: 0; 34       border-bottom-right-radius: 3px; 35       border-bottom-left-radius: 3px; 36     } 37  38     .input-count { 39       float: right; 40       margin-top: -30px; 41       color: #00000073; 42       font-size: 12px; 43     } 44   } 45 }

 

 3.问题解决

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

4.组件展示

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