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
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...