首页 > 其他分享 >React组件封装:文字、表情评论框

React组件封装:文字、表情评论框

时间:2024-03-29 13:44:59浏览次数:19  
标签:content 封装 current React rangeOfInputBox range inputBoxRef 组件 const

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

 

标签:content,封装,current,React,rangeOfInputBox,range,inputBoxRef,组件,const
From: https://www.cnblogs.com/bk-ajiang/p/18103452

相关文章

  • Vue:实现子组件和父组件数据的双向绑定案例和sync修饰符简化
    实现子组件和父组件数据的双向绑定(实现App.vue中的selectId和子组件选中的数据进行双向绑定)代码案例BaseSelect.vue<template><div><select:value="selectId"@change="selectCity"><optionvalue="101">北京</option><op......
  • 安装 Visual C++ 可再发行组件包的简单方法
    安装VisualC++RedistributablePackages的最佳方法安装对Wampserver(以及许多其他软件)至关重要的VC++可再发行组件的最简单、最简单、最不容易出错、最快的方法是使用一个程序,该程序通过单个可执行文件安装所需的所有内容。不,这不是乌托邦!它存在,它是名为VisualCppRedistA......
  • React — 原理面试题-持续更新
    1.什么是React事件,什么是原生事件?两者的区别在哪儿?React事件:React事件是经过封装和合成的,以保证在不同浏览器上的一致性。在使用React中的事件处理时,你会给JSX元素添加事件处理函数,比如onClick、onChange等,然后在事件处理函数中处理相应的逻辑。React事件的处理方式......
  • pytest框架的封装以及用例管理框架
    pytest框架的封装以及用例管理框架公共类统一封装requests_util02.pytest_api01.py自动化测试的基础自动化测试的介入点自动化测试和手工测试占比自动化实施过程pytest元素定位元素定位查找元素定位的方式通过ID定位通过Name定位通过ClassName定位通过TagName......
  • Ajax和django自带序列化组件
    Ajax和django自带序列化组件1.Ajax1.1Ajax介绍AJAX(AsynchronousJavascriptAndXML)翻译成中文就是“异步的Javascript和XML”。即使用Javascript语言与服务器进行异步交互,传输的数据为XML(当然,传输的数据不只是XML)。AJAX不是新的编程语言,而是一种使用现有标准的新方法......
  • forms组件
    forms组件1.form介绍form组件的主要功能如下:生成页面可用的HTML标签对用户提交的数据进行校验保留上次输入内容2.需求案例写一个注册功能获取用户名和密码,利用form表单提交数据在后端判断用户名和密码是否符合一定的条件用户名中不能包含啦啦啦密码不能少于三......
  • vue3 mitt事件总线,组件之间通信,通信范围不在局限于父子组件之间
    vue2使用的EventBus事件总线在vue3已经被弃用了;vue3使用的事件总线为mitt,可用父子组件,兄弟组件之间通信我使用的方法如下新建一个mitt.ts文件保存以下内容importmitts,{Emitter}from"mitt";//定义类型别名,因全局使用并且需要自定义事件名称,所以使用索引签名......
  • Vue 自定义组件库通过配置调整样式?
      在Vue自定义组件库中,通常可以通过配置来调整样式。为了实现这一点,你可以定义一组样式相关的配置项,并在组件内部使用这些配置项来动态地设置样式。以下是一个简单的示例,演示了如何通过配置调整组件的样式。自定义组件(CustomComponent.vue)<template><div:style......
  • ONLYOFFICE 文档 Vue 组件
    ONLYOFFICE文档Vue组件ONLYOFFICEDocsVue.js 组件 集成ONLYOFFICEDocs到 Vue.js 项目。先决条件此过程需要 Node.js(和npm)。使用ONLYOFFICE文档编辑器创建演示Vue.js应用程序此过程创建一个基本Vue.js应用程序 并在其中安装ONLYOFFICE文档编辑器。......
  • 基于Axios封装请求---防止接口重复请求解决方案
     一、引言前端接口防止重复请求的实现方案主要基于以下几个原因:用户体验:重复发送请求可能导致页面长时间无响应或加载缓慢,从而影响用户的体验。特别是在网络不稳定或请求处理时间较长的情况下,这个问题尤为突出。服务器压力:如果前端不限制重复请求,服务器可能会接收到大量......