React組件封裝:文字、表情評論框

来源:https://www.cnblogs.com/bk-ajiang/p/18103452
-Advertisement-
Play Games

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

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前端技術棧+Vue筆記 ES6新特性 1.let 1)let聲明有嚴格的局部作用域 ​ 此時"console.log("job= "+job)"將報錯 { var name = "zy學習"; let job = "java工程師"; console.log("name= "+name) conso ...
  • 預覽 打字機動畫是一種常見的網頁效果,通常用於“我是某某”這樣的場景,比如個人簡介或者產品介紹,需要在多個辭彙之間切換。這篇博文將從頭開始製作類似的效果。它看起來像這樣: 我要成為賽馬娘 高手! 如果這裡的黃字不會動,可能是因為這裡的架構不支持,請到https://penghy.com/?p=typ ...
  • 快速入門 1. 創建第一個應用 由於該庫還不支持 src 引入,接下來的例子我將在 webpack 環境下演示,webpack 模板 已經配置完畢,可直接下載使用 創建一個應用可用通過 new 來創建實例或通過提供的 createApp 方法來創建下麵我將分別演示 通過 new 來創建 App , ...
  • 遍歷是指通過或遍歷節點樹 遍歷節點樹 通常,您想要迴圈一個 XML 文檔,例如:當您想要提取每個元素的值時。 這被稱為"遍歷節點樹"。 下麵的示例迴圈遍歷所有 <book> 的子節點,並顯示它們的名稱和值: <!DOCTYPE html> <html> <body> <p id="demo"></p ...
  • 一、是什麼 HTTP頭欄位(HTTP header fields),是指在超文本傳輸協議(HTTP)的請求和響應消息中的消息頭部分 它們定義了一個超文本傳輸協議事務中的操作參數 HTTP頭部欄位可以自己根據需要定義,因此可能在 Web伺服器和瀏覽器上發現非標準的頭欄位 下麵是一個HTTP請求的請求頭 ...
  • DOM(文檔對象模型)定義了一種訪問和操作文檔的標準。它是一個平臺和語言無關的介面,允許程式和腳本動態訪問和更新文檔的內容、結構和樣式。HTML DOM用於操作HTML文檔,而XML DOM用於操作XML文檔。 HTML DOM示例 通過ID獲取並修改HTML元素的值: <!DOCTYPE html ...
  • 一、是什麼 HTTP狀態碼(英語:HTTP Status Code),用以表示網頁伺服器超文本傳輸協議響應狀態的3位數字代碼 它由 RFC 2616規範定義的,並得到 RFC 2518、RFC 2817、RFC 2295、RFC 2774與 RFC 4918等規範擴展 簡單來講,http狀態碼的作用 ...
  • 1.簡介 開源 Web 富文本編輯器,開箱即用,配置簡單。一個產品的價值,就在於解決用戶的問題,提高效率、降低成本、增加穩定性和擴展性。wangEditor 不是為了做而做,也不是單純的模仿誰,而是經過上述問題分析之後,給出一個系統的解決方案。旨在真正去解決用戶的問題,產出自己的價值。更多資料見官網 ...
一周排行
    -Advertisement-
    Play Games
  • GoF之工廠模式 @目錄GoF之工廠模式每博一文案1. 簡單說明“23種設計模式”1.2 介紹工廠模式的三種形態1.3 簡單工廠模式(靜態工廠模式)1.3.1 簡單工廠模式的優缺點:1.4 工廠方法模式1.4.1 工廠方法模式的優缺點:1.5 抽象工廠模式1.6 抽象工廠模式的優缺點:2. 總結:3 ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 本章將和大家分享ES的數據同步方案和ES集群相關知識。廢話不多說,下麵我們直接進入主題。 一、ES數據同步 1、數據同步問題 Elasticsearch中的酒店數據來自於mysql資料庫,因此mysql數據發生改變時,Elasticsearch也必須跟著改變,這個就是Elasticsearch與my ...
  • 引言 在我們之前的文章中介紹過使用Bogus生成模擬測試數據,今天來講解一下功能更加強大自動生成測試數據的工具的庫"AutoFixture"。 什麼是AutoFixture? AutoFixture 是一個針對 .NET 的開源庫,旨在最大程度地減少單元測試中的“安排(Arrange)”階段,以提高 ...
  • 經過前面幾個部分學習,相信學過的同學已經能夠掌握 .NET Emit 這種中間語言,並能使得它來編寫一些應用,以提高程式的性能。隨著 IL 指令篇的結束,本系列也已經接近尾聲,在這接近結束的最後,會提供幾個可供直接使用的示例,以供大伙分析或使用在項目中。 ...
  • 當從不同來源導入Excel數據時,可能存在重覆的記錄。為了確保數據的準確性,通常需要刪除這些重覆的行。手動查找並刪除可能會非常耗費時間,而通過編程腳本則可以實現在短時間內處理大量數據。本文將提供一個使用C# 快速查找並刪除Excel重覆項的免費解決方案。 以下是實現步驟: 1. 首先安裝免費.NET ...
  • C++ 異常處理 C++ 異常處理機制允許程式在運行時處理錯誤或意外情況。它提供了捕獲和處理錯誤的一種結構化方式,使程式更加健壯和可靠。 異常處理的基本概念: 異常: 程式在運行時發生的錯誤或意外情況。 拋出異常: 使用 throw 關鍵字將異常傳遞給調用堆棧。 捕獲異常: 使用 try-catch ...
  • 優秀且經驗豐富的Java開發人員的特征之一是對API的廣泛瞭解,包括JDK和第三方庫。 我花了很多時間來學習API,尤其是在閱讀了Effective Java 3rd Edition之後 ,Joshua Bloch建議在Java 3rd Edition中使用現有的API進行開發,而不是為常見的東西編 ...
  • 框架 · 使用laravel框架,原因:tp的框架路由和orm沒有laravel好用 · 使用強制路由,方便介面多時,分多版本,分文件夾等操作 介面 · 介面開發註意欄位類型,欄位是int,查詢成功失敗都要返回int(對接java等強類型語言方便) · 查詢介面用GET、其他用POST 代碼 · 所 ...
  • 正文 下午找企業的人去鎮上做貸後。 車上聽同事跟那個司機對罵,火星子都快出來了。司機跟那同事更熟一些,連我在內一共就三個人,同事那一手指桑罵槐給我都聽愣了。司機也是老社會人了,馬上聽出來了,為那個無辜的企業經辦人辯護,實際上是為自己辯護。 “這個事情你不能怪企業。”“但他們總不能讓銀行的人全權負責, ...