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. 全流程点云机器学习(一)使用CloudCompare自制sharpNet数据集

    前言 这不是高支模项目需要嘛,他们用传统算法切那个横杆竖杆流程复杂耗时很长,所以想能不能用机器学习完成这些工作,所以我就来整这个工作了. 工欲善其事,必先利其器,在正式开始之前,我们先要搞懂如何切分数 ...

  2. Redis 缓存过期删除/淘汰策略分析

    Redis 缓存过期删除/淘汰策略分析 Redis 缓存删除 Redis 键过期删除,定期删除(主动)和惰性删除(被动) Redis 内存不足时,缓存淘汰策略 key 键过期删除 我们用 redis ...

  3. LayUI样式优化

    如下是LayUI框架中页面元素的CSS优化样式: /* 表单输入框宽度 */ .layui-form-item .layui-input-inline { width: 295px; } /* 下拉框 ...

  4. 本地配置静态ip和dns及虚拟机

  5. django中使用celery异步发送邮件

    申请163网易发送邮件权限 在django中settings配置文件 #配置邮件服务器 EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBac ...

  6. 【LeetCode哈希表#3】快乐数(set)

    快乐数 力扣题目链接(opens new window) 编写一个算法来判断一个数 n 是不是快乐数. 「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程 ...

  7. AI 让观众成为 3D 版《老友记》的导演了?

    <老友记>上线 3D 版了? 允许用户旋转镜头,且从近景切换到全景观看故事? 今年出炉的 3D 方向 AI 项目 SitCom3D,能够自动补齐<老友记>原剧中的三维拍摄空间, ...

  8. 无所不谈,百无禁忌,Win11本地部署无内容审查中文大语言模型CausalLM-14B

    目前流行的开源大语言模型大抵都会有内容审查机制,这并非是新鲜事,因为之前chat-gpt就曾经被"玩"坏过,如果没有内容审查,恶意用户可能通过精心设计的输入(prompt)来操纵L ...

  9. expect tcl 摘录

    目录 部分参考来源说明 例子 expect命令 核心命令有三个 spawn.expect.send 其他expect命令 expect命令的选项 变量 tcl摘录 数据类型 符号 命令 其他说明 部分 ...

  10. Choreographer原理

    Android 系统在 VSYNC 信号的指引下,有条不紊地进行者每一帧的渲染.合成操作,使我们可以享受稳定帧率的画面.引入 VSYNC 之前的 Android 版本,渲染一帧相关的 Message ...