经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » React » 查看文章
React组件封装:文字、表情评论框
来源:cnblogs  作者:bk·ajiang  时间:2024/3/29 13:32:16  对本文有异议

1.需求描述

根据项目需求,采用Antd组件库需要封装一个评论框,具有以下功能:

  • 支持文字输入
  • 支持常用表情包选择
  • 支持发布评论
  • 支持自定义表情包

2.封装代码

 ./InputComment.tsx

  1. 1 import React, { useState, useRef, useEffect, forwardRef, useImperativeHandle } from 'react';
  2. 2 import { SmileOutlined } from '@ant-design/icons';
  3. 3 import { Row, Col, Button, Tooltip, message } from 'antd';
  4. 4
  5. 5 import styles from './index.less';
  6. 6
  7. 7 import {setCursorPostionEnd} from "./util";
  8. 8
  9. 9 const emojiPath = '/emojiImages/';
  10. 10 const emojiSuffix = '.png';
  11. 11 const emojiList = [...Array(15).keys()].map((_, index: number) => {
  12. 12 return { id: index + 1, path: emojiPath + (index + 1) + emojiSuffix };
  13. 13 });
  14. 14
  15. 15 type Props = {
  16. 16 uniqueId: string; // 唯一键
  17. 17 item?: object; // 携带参数
  18. 18 okClick: Function; // 发布
  19. 19 okText?: string;
  20. 20 };
  21. 21
  22. 22 const InputComment = forwardRef((props: Props, ref) => {
  23. 23 const { uniqueId: id, okClick, okText } = props;
  24. 24 const inputBoxRef = useRef<any>(null);
  25. 25 const [textCount, setTextCount] = useState(0);
  26. 26 let rangeOfInputBox: any;
  27. 27 const uniqueId = 'uniqueId_' + id;
  28. 28
  29. 29 const setCaretForEmoji = (target: any) => {
  30. 30 if (target?.tagName?.toLowerCase() === 'img') {
  31. 31 const range = new Range();
  32. 32 range.setStartBefore(target);
  33. 33 range.collapse(true);
  34. 34 // inputBoxRef?.current?.removeAllRanges();
  35. 35 // inputBoxRef?.current?.addRange(range);
  36. 36 const sel = window.getSelection();
  37. 37 sel?.removeAllRanges();
  38. 38 sel?.addRange(range);
  39. 39 }
  40. 40 };
  41. 41
  42. 42 /**
  43. 43 * 输入框点击
  44. 44 */
  45. 45 const inputBoxClick = (event: any) => {
  46. 46 const target = event.target;
  47. 47 setCaretForEmoji(target);
  48. 48 };
  49. 49
  50. 50 /**
  51. 51 * emoji点击
  52. 52 */
  53. 53 const emojiClick = (item: any) => {
  54. 54 const emojiEl = document.createElement('img');
  55. 55 emojiEl.src = item.path;
  56. 56 const dom = document.getElementById(uniqueId);
  57. 57 const html = dom?.innerHTML;
  58. 58
  59. 59 // rangeOfInputBox未定义并且存在内容时,将光标移动到末尾
  60. 60 if (!rangeOfInputBox && !!html) {
  61. 61 dom.innerHTML = html + `<img src="${item.path}"/>`;
  62. 62 setCursorPostionEnd(dom)
  63. 63 } else {
  64. 64 if (!rangeOfInputBox) {
  65. 65 rangeOfInputBox = new Range();
  66. 66 rangeOfInputBox.selectNodeContents(inputBoxRef.current);
  67. 67 }
  68. 68
  69. 69 if (rangeOfInputBox.collapsed) {
  70. 70 rangeOfInputBox.insertNode(emojiEl);
  71. 71 } else {
  72. 72 rangeOfInputBox.deleteContents();
  73. 73 rangeOfInputBox.insertNode(emojiEl);
  74. 74 }
  75. 75 rangeOfInputBox.collapse(false);
  76. 76
  77. 77 const sel = window.getSelection();
  78. 78 sel?.removeAllRanges();
  79. 79 sel?.addRange(rangeOfInputBox);
  80. 80 }
  81. 81 };
  82. 82
  83. 83 /**
  84. 84 * 选择变化事件
  85. 85 */
  86. 86 document.onselectionchange = (e) => {
  87. 87 if (inputBoxRef?.current) {
  88. 88 const element = inputBoxRef?.current;
  89. 89 const doc = element.ownerDocument || element.document;
  90. 90 const win = doc.defaultView || doc.parentWindow;
  91. 91 const selection = win.getSelection();
  92. 92
  93. 93 if (selection?.rangeCount > 0) {
  94. 94 const range = selection?.getRangeAt(0);
  95. 95 if (inputBoxRef?.current?.contains(range?.commonAncestorContainer)) {
  96. 96 rangeOfInputBox = range;
  97. 97 }
  98. 98 }
  99. 99 }
  100. 100 };
  101. 101
  102. 102 /**
  103. 103 * 获取内容长度
  104. 104 */
  105. 105 const getContentCount = (content: string) => {
  106. 106 return content
  107. 107 .replace(/&nbsp;/g, ' ')
  108. 108 .replace(/<br>/g, '')
  109. 109 .replace(/<\/?[^>]*>/g, '占位').length;
  110. 110 };
  111. 111
  112. 112 /**
  113. 113 * 发送
  114. 114 */
  115. 115 const okSubmit = () => {
  116. 116 const content = inputBoxRef.current.innerHTML;
  117. 117 if (!content) {
  118. 118 return message.warning('温馨提示:请填写评论内容!');
  119. 119 } else if (getContentCount(content) > 1000) {
  120. 120 return message.warning(`温馨提示:评论或回复内容小于1000字!`);
  121. 121 }
  122. 122
  123. 123 okClick(content);
  124. 124 };
  125. 125
  126. 126 /**
  127. 127 * 清空输入框内容
  128. 128 */
  129. 129 const clearInputBoxContent = () => {
  130. 130 inputBoxRef.current.innerHTML = '';
  131. 131 };
  132. 132
  133. 133 // 将子组件的方法 暴露给父组件
  134. 134 useImperativeHandle(ref, () => ({
  135. 135 clearInputBoxContent,
  136. 136 }));
  137. 137
  138. 138 // 监听变化
  139. 139 useEffect(() => {
  140. 140 const dom = document.getElementById(uniqueId);
  141. 141 const observer = new MutationObserver(() => {
  142. 142 const content = dom?.innerHTML ?? '';
  143. 143 // console.log('Content changed:', content);
  144. 144 setTextCount(getContentCount(content));
  145. 145 });
  146. 146
  147. 147 if (dom) {
  148. 148 observer.observe(dom, {
  149. 149 attributes: true,
  150. 150 childList: true,
  151. 151 characterData: true,
  152. 152 subtree: true,
  153. 153 });
  154. 154 }
  155. 155 }, []);
  156. 156
  157. 157 return (
  158. 158 <div style={{ marginTop: 10, marginBottom: 10 }} className={styles.inputComment}>
  159. 159 {textCount === 0 ? (
  160. 160 <div className="input-placeholder">
  161. 161 {okText === '确认' ? '回复' : '发布'}评论,内容小于1000字!
  162. 162 </div>
  163. 163 ) : null}
  164. 164
  165. 165 <div
  166. 166 ref={inputBoxRef}
  167. 167 id={uniqueId}
  168. 168 contentEditable={true}
  169. 169 placeholder="adsadadsa"
  170. 170 className="ant-input input-box"
  171. 171 onClick={inputBoxClick}
  172. 172 />
  173. 173 <div className="input-emojis">
  174. 174 <div className="input-count">{textCount}/1000</div>
  175. 175
  176. 176 <Row wrap={false}>
  177. 177 <Col flex="auto">
  178. 178 <Row wrap={true} gutter={[0, 10]} align="middle" style={{ userSelect: 'none' }}>
  179. 179 {emojiList.map((item, index: number) => {
  180. 180 return (
  181. 181 <Col
  182. 182 flex="none"
  183. 183 onClick={() => {
  184. 184 emojiClick(item);
  185. 185 }}
  186. 186
  187. 187 <Col flex="none" style={{ marginTop: 5 }}>
  188. 188 <Button
  189. 189 type="primary"
  190. 190 disabled={textCount === 0}
  191. 191 onClick={() => {
  192. 192 okSubmit();
  193. 193 }}
  194. 194 >
  195. 195 {okText || '发布'}
  196. 196 </Button>
  197. 197 </Col>
  198. 198 </Row>
  199. 199 </div>
  200. 200 </div>
  201. 201 );
  202. 202 });
  203. 203
  204. 204 export default InputComment;

./util.ts

  1. 1 /**
  2. 2 * 光标放到文字末尾(获取焦点时)
  3. 3 * @param el
  4. 4 */
  5. 5 export function setCursorPostionEnd(el:any) {
  6. 6 if (window.getSelection) {
  7. 7 // ie11 10 9 ff safari
  8. 8 el.focus() // 解决ff不获取焦点无法定位问题
  9. 9 const range = window.getSelection() // 创建range
  10. 10 range?.selectAllChildren(el) // range 选择obj下所有子内容
  11. 11 range?.collapseToEnd() // 光标移至最后
  12. 12 } else if (document?.selection) {
  13. 13 // ie10 9 8 7 6 5
  14. 14 const range = document?.selection?.createRange() // 创建选择对象
  15. 15 // var range = document.body.createTextRange();
  16. 16 range.moveToElementText(el) // range定位到obj
  17. 17 range.collapse(false) // 光标移至最后
  18. 18 range.select()
  19. 19 }
  20. 20 }

 

 3.问题解决

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

4.组件展示

 

原文链接:https://www.cnblogs.com/bk-ajiang/p/18103452

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号