1.需求描述 根據項目需求,採用Antd組件庫需要封裝一個評論框,具有以下功能: 支持文字輸入 支持常用表情包選擇 支持發佈評論 支持自定義表情包 2.封裝代碼 ./InputComment.tsx 1 import React, { useState, useRef, useEffect, for ...
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 }
./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.組件展示