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
  • 前言 插件化的需求主要源於對軟體架構靈活性的追求,特別是在開發大型、複雜或需要不斷更新的軟體系統時,插件化可以提高軟體系統的可擴展性、可定製性、隔離性、安全性、可維護性、模塊化、易於升級和更新以及支持第三方開發等方面的能力,從而滿足不斷變化的業務需求和技術挑戰。 一、插件化探索 在WPF中我們想要開 ...
  • 歡迎ReaLTaiizor是一個用戶友好的、以設計為中心的.NET WinForms項目控制項庫,包含廣泛的組件。您可以使用不同的主題選項對項目進行個性化設置,並自定義用戶控制項,以使您的應用程式更加專業。 項目地址:https://github.com/Taiizor/ReaLTaiizor 步驟1: ...
  • EDP是一套集組織架構,許可權框架【功能許可權,操作許可權,數據訪問許可權,WebApi許可權】,自動化日誌,動態Interface,WebApi管理等基礎功能於一體的,基於.net的企業應用開發框架。通過友好的編碼方式實現數據行、列許可權的管控。 ...
  • Channel 是乾什麼的 The System.Threading.Channels namespace provides a set of synchronization data structures for passing data between producers and consume ...
  • efcore如何優雅的實現按年分庫按月分表 介紹 本文ShardinfCore版本 本期主角: ShardingCore 一款ef-core下高性能、輕量級針對分表分庫讀寫分離的解決方案,具有零依賴、零學習成本、零業務代碼入侵適配 距離上次發文.net相關的已經有很久了,期間一直在從事java相關的 ...
  • 前言 Spacesniffer 是一個免費的文件掃描工具,通過使用樹狀圖可視化佈局,可以立即瞭解大文件夾的位置,幫助用戶處理找到這些文件夾 當前系統C盤空間 清理後系統C盤空間 下載 Spacesniffer 下載地址:https://spacesniffer.en.softonic.com/dow ...
  • EDP是一套集組織架構,許可權框架【功能許可權,操作許可權,數據訪問許可權,WebApi許可權】,自動化日誌,動態Interface,WebApi管理等基礎功能於一體的,基於.net的企業應用開發框架。通過友好的編碼方式實現數據行、列許可權的管控。 ...
  • 一、ReZero簡介 ReZero是一款.NET中間件 : 全網唯一開源界面操作就能生成API , 可以集成到任何.NET6+ API項目,無破壞性,也可讓非.NET用戶使用exe文件 免費開源:MIT最寬鬆協議 , 一直從事開源事業十年,一直堅持開源 1.1 純ReZero開發 適合.Net Co ...
  • 一:背景 1. 講故事 停了一個月沒有更新文章了,主要是忙於寫 C#內功修煉系列的PPT,現在基本上接近尾聲,可以回頭繼續更新這段時間分析dump的一些事故報告,有朋友微信上找到我,說他們的系統出現了大量的http超時,程式不響應處理了,讓我幫忙看下怎麼回事,dump也抓到了。 二:WinDbg分析 ...
  • 開始做項目管理了(本人3年java,來到這邊之後真沒想到...),天天開會溝通整理需求,他們講話的時候忙裡偷閑整理一下常用的方法,其實語言還是有共通性的,基本上看到方法名就大概能猜出來用法。出去打水的時候看到外面太陽好好,真想在外面坐著曬太陽,回來的時候好兄弟三年前送給我的鍵盤D鍵不靈了,在打"等待 ...