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(/ /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.组件展示