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 }

 3.问题解决

  • 同一页面有多个评论框时,光标位置不准确?答:从组件外部传入唯一ID标识,进行区分。
  • 表情包存放位置?答:表情包存放在/public/emojiImages/**.png,命名规则1、2、3、4……

4.组件展示

React组件封装:文字、表情评论框的更多相关文章

  1. [RN] React Native 封装选择弹出框(ios&android)

    之前看到react-native-image-picker中自带了一个选择器,可以选择拍照还是图库,但我们的项目中有多处用到这个选择弹出框,所以就自己写了一下,最最重要的是ios和Android通用. ...

  2. React组件化开发

    环境搭建: 1.安装node.js 2.安装cnpm  # npm install -g cnpm --registry=https://registry.npm.taobao.org 3.全局安装c ...

  3. iOS开发之自定义表情键盘(组件封装与自动布局)

    下面的东西是编写自定义的表情键盘,话不多说,开门见山吧!下面主要用到的知识有MVC, iOS开发中的自动布局,自定义组件的封装与使用,Block回调,CoreData的使用.有的小伙伴可能会问写一个自 ...

  4. 带emoji表情弹出层的评论框,semantic+emoji picker,java.sql.SQLException: Incorrect string value: '\xF0\x9F..'

    在自己做一个项目玩时,在做评论的时候. 选中了semantic.js原型,这个在国内用的不是很多,但是在github上star数量很高,想当初我想找一个js框架是就在上面找的. semantic中文网 ...

  5. 封装react组件——三级联动

    思路: 数据设计:省份为一维数组,一级市为二维数组,二级市/区/县为三维数组.这样设计的好处在于根据数组索引实现数据的关联. UI组件: MUI的DropDownMenu组件或Select Field ...

  6. react第七单元(组件的高级用法-组件的组合(children的用法)-高阶组件-封装组件)

    第七单元(组件的高级用法-组件的组合(children的用法)-高阶组件-封装组件) #受控组件 简而言之,就是受到状态state控制的表单,表单的值改变则state值也改变,受控组件必须要搭配onc ...

  7. ReactNative之从HelloWorld中看环境搭建、组件封装、Props及State

    开篇呢,先给大家问个好,今天是中秋节,祝大家中秋节快乐!!虽然是中秋节,但是木有回家还是总结一下知识点写写博客吧,想着昨天总结一下的,但是昨天和几个同学小聚了一下,酒逢知己总是千杯少呢,喝的微醺不适合 ...

  8. React组件

    React组件 组件是React中的基本单位,在每个组件里面又封装了程序逻辑,通过reader标出界面片段或者回传一段描述,组件再通过React.renderComponent将组件展示在浏览器中.每 ...

  9. 编写React组件的最佳实践

    此文翻译自这里. 当我刚开始写React的时候,我看过很多写组件的方法.一百篇教程就有一百种写法.虽然React本身已经成熟了,但是如何使用它似乎还没有一个"正确"的方法.所以我( ...

  10. React组件之间通过Props传值的技巧(小案例,帮助体会理解props、state、受控组件和非受控组件等)

    本文重要是根据react小书上的一个很简单的例子改编的,加上自己的学习理解,希望可以通过实际案例让大家对概念有更清晰的理解,当然也希望能一块学习. import React,{Component} f ...

随机推荐

  1. PLSQL编译存储过程无响应

    解决方法如下: 1:查V$DB_OBJECT_CACHE SELECT * FROM V$DB_OBJECT_CACHE WHERE name='CRM_LASTCHGINFO_DAY' AND LO ...

  2. jenkins构建第一个maven项目

    1. 项目介绍 spring boot样例github项目. 大家可以访问:https://github.com/mudfish/springbootdemo 2. jenkins中新建maven任务 ...

  3. Innodb存储引擎的文件

    目录 概述 参数文件 日志文件 错误日志 慢查询日志 查询日志 二进制日志 binary log 二进制日志的配置 二进制日志的作用 二进制日志的保存 socket 套接字文件 pid文件 MySQL ...

  4. .Net Core Entity Framework Core 的基础封装

    上篇讲到  c# Unit of Work 知识分享时,对于创建DBContext 的封装没有讲到,这次分享跟大家 public interface IDbContextFactory { DbCon ...

  5. [Android 逆向]Xposed 破解 切水果大战原版.apk

    代码 public class Main implements IXposedHookLoadPackage { boolean flag = false; @Override public void ...

  6. OsgEarth开发笔记(四):Qt5.15.2在QtCreator集成Osg3.6.3+OsgEarth3.1+OsgQt的vs2019x64版本开发环境搭建

    前言   本篇非常麻烦,博主用QtCreator作为IDE,因为Osg3.6.3放弃对osgQt的支持,集成起来比较繁琐.   前提   基于前面三篇的基础上,才可以进行本篇.   Demo演示:Qt ...

  7. django学习第四天----mark_safe的用法,静态文件配置,用指令创建django项目应用注意点,ORM介绍,创建表执行命令,模板渲染补充(组件),inclusion_tag 自定义标签

    补充第三天跟safe差不多的一个方法 templatetags文件夹 自定义的py文件 需要先导入模块 from django.utils.safestring import mark_safe @r ...

  8. 【Azure 应用服务】使用Python Azure SDK 来获取 App Service的访问限制信息(Access Restrictions)

    问题描述 为Azure App Service添加访问限制,需要Python Azure SDK来实现的示例代码. 问题解答 查阅Azure App Service的官方资料,使用Python SDK ...

  9. 【Azure 应用服务】Azure Powershell Function 出错 The term 'Connect-AzAccount' is not recognized

    问题描述 在Azure Function中,执行Powershell的Function脚本时,先后出现 1:[Error] ERROR: The term 'Connect-AzAccount' is ...

  10. go语言实现扫雷

    源码如下 package main import ( "archive/zip" "bytes" "encoding/base64" &qu ...