今天分享一下tinymce富文本编辑器做评论区的全过程。
文章目录
- 一、介绍
- 1.最终效果
- 2.功能介绍
- 3.主要项目包版本介绍:
- 二、每个功能的实现
- 1.自定义toolbar的功能区
- ①对应的样式以及意义
- ②对应的代码实现【忽略了一切非实现该功能的代码】
- 2.展示、收起评论区
- ①对应的样式以及意义
- ②对应的代码实现【忽略了一切非实现该功能的代码】
- 3.选择文字然后添加评论
- ①对应的样式以及意义
- ②对应的代码实现【忽略了一切非实现该功能的代码】
- 4.取消添加评论
- 5.点击左侧富文本内容【有评论的元素】右侧评论区滚动到对应的评论
- 6.删除评论\编辑评论\回复评论\标记评论
- 三、完整的代码
- 1.公共代码
- ①Comment组件
- ②数据处理dataProcessor
- 2.富文本编辑器代码
- 四、结语
一、介绍
1.最终效果
2.功能介绍
- 自定义toolbar的功能区
- 展示、收起评论区
- 选择文字然后添加评论
- 取消添加评论
- 点击左侧富文本内容【有评论的元素】右侧评论区滚动到对应的评论
- 删除评论
- 编辑评论、回复评论、标记解决评论、艾特人这些属于基本的需求了,本文不做分享
3.主要项目包版本介绍:
"react": "^16.13.1",
"@tinymce/tinymce-react": "^3.14.0",
二、每个功能的实现
1.自定义toolbar的功能区
①对应的样式以及意义
首先加了一个自定义的icon - 追加评论用的
然后加了一个自定义的文字 - 显示隐藏评论区用的
②对应的代码实现【忽略了一切非实现该功能的代码】
// 你换成你自己想要追加图片的地址
import addCommentIcon from '@assets/imgs/Comment/add-comment.png';
<Editor
init={{
// 在toolbar中追加配置 addCommentButton showCommentArea ,这俩个都是我们在setup里面注册的按钮【其他加粗、字体大小那些在这里忽略了】
toolbar: 'addCommentButton showCommentArea',
setup: (editor) => {
// 追加自定义icon - addComment
editor.ui.registry.addIcon(
'addComment',
`<img src='${addCommentIcon}' />`,
);
// 在toolbar中追加ICON - 添加评论的按钮 - customCommentButton
editor.ui.registry.addButton('addCommentButton', {
type: 'contextformbutton',
icon: 'addComment', // 使用自定义的icon
onAction: () => {},
});
// 在toolbar中追加按钮 - 控制评论区的显示与否
editor.ui.registry.addButton('showCommentArea', {
type: 'contextformbutton',
text: 'Show/Hide comment',
onAction: () => {},
});
},
}}
/>
2.展示、收起评论区
①对应的样式以及意义
点击Show/Hide comment控制右侧评论区的显示隐藏【始终显示的话占用空间】
②对应的代码实现【忽略了一切非实现该功能的代码】
首先自己做一个评论区的区域
下方代码说明:
设置id是为了控制展示、收起【在tinymce的setup中获取dom元素、在那里去react的state会有问题】
style控制评论区的显示隐藏【笔者这里使用display会触发回流,你可以使用其他的隐藏元素的方法,比如改成定位移出可视区等等】
Card笔者用的是antd的组件,你可以自己搞一个样式【是否要loading可选】。
commentList是从后端获取到的comment列表
Comment是自己做的渲染的每一项的组件【在后续会有这个组件,这里先不写】
// 是否展示评论区
const [commentAreaIsShow, setCommentAreaIsShow] = useState(false);
<div
id="rich-editor-comment-wrapper"
data-show={JSON.stringify(commentAreaIsShow)}
style={{ display: commentAreaIsShow ? 'block' : 'none' }}
>
<Card bodyStyle={{ height: '60vh', overflowY: 'auto' }}>
{commentsLoading && <ContainerLoading />}
{/* view comment */}
{commentList.map((item) => (
<Comment key={item.commentId} {...item} {...commentPublicParams} />
))}
</Card>
</div>
然后我们改一下刚才注册的tinymce的setup里面的showCommentArea的onAction
特殊说明:在tinymce的setup里面取不到最新的state,想取得需要重新渲染编辑区,会导致富文本区域闪烁,所以这里通过获取dom的自定义属性获取值取反进行更改。
// 控制评论区的显示与否
editor.ui.registry.addButton('showCommentArea', {
type: 'contextformbutton',
text: 'Show/Hide comment',
onAction: () => {
const commentArea = document.getElementById(
'rich-editor-comment-wrapper',
);
const show = JSON.parse(commentArea.getAttribute('data-show'));
setCommentAreaIsShow(!show);
},
});
3.选择文字然后添加评论
①对应的样式以及意义
选中文字 -> 点击add Comment的那个icon然后就会看到右侧评论区加了一个添加评论项
②对应的代码实现【忽略了一切非实现该功能的代码】
定义一个addItemInfo、如果有的话那就显示添加comment的组件。
// 当前add的信息
const [addItemInfo, setAddItemInfo] = useState({});
// 在刚才的评论区的div里面加一个判断,如果有addItemInfo的话就显示新增评论的Comment元素。
// addComment固定显示在第一个
<Card bodyStyle={{ height: '60vh', overflowY: 'auto' }}>
{commentsLoading && <ContainerLoading />}
{/* 添加comment - 单独放在最上面 */}
{addItemInfo?.id && (
<Comment
{...addItemInfo}
setAddItemInfo={setAddItemInfo}
{...commentPublicParams}
/>
)}
{/* view comment */}
{commentList.map((item) => (
<Comment key={item.commentId} {...item} {...commentPublicParams} />
))}
</Card>
改一下刚才tinymce的setup里面的命令
// 追加自定义命令
editor.addCommand('addComment', (ui, v) => {
// 获取选中的内容
const selectionText = editor.selection.getContent();
if (selectionText) {
const uuid = uuid4NoDash(); // 你可以自己用其他方法生成一个uuid
// 把刚才选中的内容替换成新的内容:加了一个下划线标识,然后加了一个id
editor.insertContent(`
<span
id="${uuid}"
style="border-bottom: 1px solid orangered;"
>
${editor.selection.getContent({ format: 'text' })}
</span>`);
// 添加comment,id和text是后续调后端接口存数据库用的,type是给comment组件用的
setAddItemInfo({ id: uuid, type: 'add', text: selectionText });
// 这里把评论区固定展示出来
setCommentAreaIsShow(true);
} else {
// 这里用的antd的message,如果没选择文案的话这边抛个提示
message.warn('Please select a sentence to add a comment.');
}
});
// 添加评论的按钮
editor.ui.registry.addButton('customCommentButton', {
type: 'contextformbutton',
icon: 'addComment',
onAction: () => {
editor.editorManager.execCommand('addComment');
},
});
Comment组件代码详见下方全部代码当中的公共代码
4.取消添加评论
在AddComment里面的cancel按钮的点击事件进行如下处理,下方代码可在全部代码->公共组件Comment中找到
5.点击左侧富文本内容【有评论的元素】右侧评论区滚动到对应的评论
// 删除已有的高亮的comment标记样式
function removeMarkComment() {
// 将已有的高亮样式删除
const Ele = document.getElementsByClassName('current_selected_comment')[0];
if (Ele) {
Ele.classList.remove('current_selected_comment');
}
}
<Editor
onSelectionChange={(event, editor) => {
const currentEle = editor.selection.getNode();
const currentEleId = currentEle.getAttribute('id');
const targetEle = document.getElementById(
`comment_item_${currentEleId}`,
);
removeMarkComment();
if (targetEle) {
// 滚动到对应评论区的位置
targetEle.scrollIntoView({ behavior: 'smooth' });
// 追加类名,高亮对应区域
targetEle.classList.add('current_selected_comment');
}
}}
/>
6.删除评论\编辑评论\回复评论\标记评论
这四个属于具体业务逻辑,与富文本编辑器添加comment其实是没太大关系的,可自行在下发代码中查看对应的逻辑
三、完整的代码
1.公共代码
①Comment组件
/**
* @note
* 评论组件【添加、编辑、展示】
* @param {
* type : 'add' | 'edit' | 'view' - 添加、编辑、查看
* id : string - 每个评论的唯一id,id是前端生成的uuid
* content : string - 评论的内容
* date : string - 评论的日期
* user : string - 评论人
* replyList: [params] - 回复列表
* }
* @author Di Wu
* @returns ReactComponent
*/
import React, { useState } from 'react';
import { connect } from 'react-redux';
import { Checkbox, Badge } from 'antd';
import { cloneDeep } from 'lodash';
import { Button } from 'appkit-react';
import { PWCInput } from '@components';
import { ConfirmModal } from '@components/ConfirmModal';
import { formatDate } from '@utils';
import { API } from '@service';
import {
changeCurrentToNewMode,
excludeTypeIsEqualToReply,
handleAddReply,
cancelReply,
} from '../../dataProcessor';
import './index.scss';
import EditCommentIcon from '@assets/imgs/Comment/edit-comment.png';
import DeleteCommentIcon from '@assets/imgs/Comment/delete-comment.png';
// comment的header部分
function CommentHeader({
user,
date,
type,
email,
loginUserInfo = {},
channel,
commentId,
projectId,
setCommentsLoading,
commentList,
setCommentList,
getAgendaCommentData,
}) {
// 是否展示删除确认的modal
const [showModal, setShowModal] = useState(false);
return (
<div className="comment-item-header">
<div>
<Badge color="#B23F02" />
<span className="user-name">{user}</span>
<span className="date">{formatDate(date)}</span>
</div>
{/* 用这条内容的email和当前登录人的email判断是否一致,一致才展示编辑还有删除按钮 */}
{type === 'view' && loginUserInfo.email === email && (
<div>
<img
src={EditCommentIcon}
style={{ marginRight: 12 }}
onClick={() => {
// 点击edit的时候把所有的reply删除掉
const commentListV2 = excludeTypeIsEqualToReply({ commentList });
// 将comment变成编辑模式
const newCommentList = changeCurrentToNewMode({
commentList: commentListV2,
commentId,
newType: 'edit',
});
setCommentList(newCommentList);
}}
/>
<img src={DeleteCommentIcon} onClick={() => setShowModal(true)} />
</div>
)}
{/* 删除确认的modal */}
<ConfirmModal
handleClickOk={async () => {
setShowModal(false);
setCommentsLoading(true);
// 调删除接口
const res = await API.deleteAgendaComment({});
// 重新获取comment数据
getAgendaCommentData();
setCommentsLoading(false);
}}
handleClickCancel={() => setShowModal(false)}
modalVisible={showModal}
type="WARNING"
width={580}
// Do you want to delete this comment ?
content={<p>Do you want to delete this comment thread?</p>}
cancelText="CANCEL"
okText="DELETE"
/>
</div>
);
}
// 新增一个reply的输入框
function ReplyComment(props) {
const {
commentList,
setCommentList,
commentId,
getAgendaCommentData,
projectId,
userInfo,
component,
identifier,
channel,
setCommentsLoading,
} = props;
const [currentEditVal, setCurrentEditVal] = useState('');
return (
<>
<div className="comment-content">
<PWCInput
value={currentEditVal}
onChange={(e) => setCurrentEditVal(e.target.value)}
/>
</div>
<div className="comment-foother">
<div> </div>
<div className="btn-group">
<Button
className="cancel-btn"
kind="secondary"
onClick={() => {
// 删除这个reply
const newCommentList = cancelReply({ commentList, commentId });
setCommentList(newCommentList);
}}
>
Cancel
</Button>
<Button
className="create"
kind="primary"
onClick={async () => {
console.log(
'currentEditVal: ',
commentId,
'--',
currentEditVal,
props,
);
try {
setCommentsLoading(true);
// 调用reply接口,然后成功之后reload
const res = await API.createAgendaComment({});
getAgendaCommentData();
setCommentsLoading(false);
} catch (err) {
console.log('!!!!', err);
}
}}
>
Comment
</Button>
</div>
</div>
</>
);
}
// comment的reply部分的渲染
function renderReplyList({ replyList, ...props }) {
const renderReplyItemObj = ({ item }) =>
({
view: (
<>
<CommentHeader {...props} {...item} />
<div className="comment-content">{item.content}</div>
</>
),
edit: <EditComment noPedding needFoother={false} {...props} {...item} />,
reply: <ReplyComment {...props} {...item} />,
}[item.type]);
return replyList?.map?.((item) => (
<div key={item.commentId} className="reply-item-box">
{renderReplyItemObj({ item })}
</div>
));
}
// comment的foother部分
function CommentFoother(props) {
const {
commentList,
setCommentList,
showReply,
commentId,
projectId,
userInfo,
channel,
identifier,
getAgendaCommentData,
setCommentsLoading,
} = props;
return (
<div className="comment-foother">
<div>
<Checkbox
checked={false}
onChange={async () => {
try {
setCommentsLoading(true);
// 调用resolve接口,然后成功之后reload
const res = await API.editAgendaComment({});
getAgendaCommentData();
setCommentsLoading(false);
} catch (err) {
console.log('!!!!', err);
}
}}
/>{' '}
Mark as resolved
</div>
{/* 判断是否显示,如果这一组的最后一个的type==='reply'那就不显示 */}
{showReply && (
<span
className="reply-btn"
onClick={() => {
// 点击edit的时候把所有的reply删除掉
const commentListV2 = excludeTypeIsEqualToReply({ commentList });
// 给这组comment加一个reply
const newCommentList = handleAddReply({
commentId,
commentList: commentListV2,
});
setCommentList(newCommentList);
}}
>
Reply
</span>
)}
</div>
);
}
// 新增comment
function AddComment({
id,
user,
commentList,
setCommentList,
setAddItemInfo,
setCommentsLoading,
...props
}) {
const {
userInfo: loginUserInfo,
projectId,
text,
getAgendaCommentData,
} = props;
const date = formatDate(new Date());
const [currentEditVal, setCurrentEditVal] = useState('');
return (
<div className="comment-item-box">
<CommentHeader user={user} date={date} type="add" />
<div className="comment-content">
<PWCInput
value={currentEditVal}
onChange={(e) => setCurrentEditVal(e.target.value)}
/>
</div>
<div className="comment-foother">
<div> </div>
<div className="btn-group">
<Button
className="cancel-btn"
kind="secondary"
onClick={() => {
const dom = document
.getElementsByTagName('iframe')?.[0]
?.contentWindow?.document?.getElementById?.(id);
if (dom) {
dom.removeAttribute('id');
dom.removeAttribute('style');
dom.removeAttribute('data-mce-style');
setAddItemInfo({});
} else {
// catch error
console.log('系统出现了未知错误');
}
}}
>
Cancel
</Button>
<Button
className="create"
kind="primary"
onClick={async () => {
// TODO:调后端接口,然后从新刷 comment 区域,或者根据后端的返回的值去做set
setCommentsLoading(true);
const res = await API.createAgendaComment({});
setCurrentEditVal('');
setAddItemInfo({});
// 重新获取数据
await getAgendaCommentData();
setCommentsLoading(false);
}}
>
Comment
</Button>
</div>
</div>
</div>
);
}
// 查看comment
function ViewComment({ content, ...props }) {
const { replyList } = props;
const publicParams = {
type: 'view',
loginUserInfo: props.userInfo,
};
return (
<div className="comment-item-box">
<CommentHeader {...props} {...publicParams} />
<div className="comment-content">{content}</div>
{renderReplyList({
...props,
...publicParams,
})}
<CommentFoother
{...props}
showReply={replyList?.[replyList.length - 1]?.type !== 'reply'}
/>
</div>
);
}
// 编辑comment
function EditComment(props) {
const {
noPedding,
needFoother = true,
replyList,
content,
user,
commentList,
commentId,
userInfo,
setCommentList,
projectId,
identifier,
channel,
setCommentsLoading,
getAgendaCommentData,
} = props;
const publicParams = {
type: 'view',
loginUserInfo: userInfo,
};
const date = formatDate(new Date());
const [currentEditVal, setCurrentEditVal] = useState(content);
return (
<div className="comment-item-box" style={noPedding ? { padding: 0 } : {}}>
<CommentHeader user={user} date={date} {...props} />
<div className="comment-content">
<PWCInput
value={currentEditVal}
onChange={(e) => setCurrentEditVal(e.target.value)}
/>
</div>
<div className="comment-foother">
<div> </div>
<div className="btn-group">
<Button
className="cancel-btn"
kind="secondary"
onClick={() => {
// 将comment变回查看模式
const newCommentList = changeCurrentToNewMode({
commentList,
commentId,
newType: 'view',
});
setCommentList(newCommentList);
}}
>
Cancel
</Button>
<Button
className="create"
kind="primary"
onClick={async () => {
console.log(
'currentEditVal: ',
commentId,
'--',
currentEditVal,
props,
);
try {
setCommentsLoading(true);
// 调用edit接口,然后成功之后reload
const res = await API.editAgendaComment({});
getAgendaCommentData();
setCommentsLoading(false);
} catch (err) {
console.log('!!!!', err);
}
}}
>
Comment
</Button>
</div>
</div>
{renderReplyList({
...props,
...publicParams,
})}
{needFoother && (
<CommentFoother
{...props}
showReply={replyList?.[replyList.length - 1]?.type !== 'reply'}
/>
)}
</div>
);
}
function Comment(props) {
const { type, id } = props;
const returnDomByMode = {
add: <AddComment key={id} {...props} user={props?.userInfo?.name || ''} />,
edit: <EditComment {...props} />,
view: <ViewComment {...props} />,
};
return (
<div
id={`comment_item_${id}`}
style={{ marginBottom: 12 }}
onClick={() => {
const Ele = document.getElementsByClassName(
'current_selected_comment',
)[0];
if (Ele) {
Ele.classList.remove('current_selected_comment');
}
}}
>
{returnDomByMode[type]}
</div>
);
}
// 获取redux当中的登录用户的信息
const mapStateToProps = ({ login, common }) => ({
userInfo: login.userInfo,
});
export default connect(mapStateToProps, () => ({}))(React.memo(Comment));
②数据处理dataProcessor
import { cloneDeep } from 'lodash';
// 将后端返回的comment变成前端想要的格式
function returnNewObj(obj) {
return {
type: 'view',
...obj,
id: obj.identifier,
content: obj.comment,
date: obj.updatedAt, // 'Oct 14, 2022 04:15 PM'
user: obj.name,
commentId: obj.id,
};
}
// 由于这边后端格式不是很理想,所以固有此function
export function handleCommentData(commonServicesData = []) {
const resData = [];
const groupObj = {}; // 将每个channel的进行分组
// 把所有的resolved的filter掉
const data = commonServicesData.filter((item) => {
// 筛选的时候直接把每个分组找出来
// 需要满足条件:没有被resolved掉 + id等于分组id +
if (
!item.channelStatus &&
item.id === item.channel &&
!groupObj[item.channel]
) {
groupObj[item.channel] = item;
}
return !item.channelStatus;
});
// 追加reply list
data.forEach((item) => {
// 因为上面分组已经都找到了,那么如果没有对应的话就代表是reply
if (!groupObj[item.id]) {
if (!groupObj[item.channel].replyList) {
groupObj[item.channel].replyList = [];
}
groupObj[item.channel].replyList.push(returnNewObj(item));
}
});
Object.values(groupObj).forEach((item) => {
resData.push(returnNewObj(item));
});
// reply list排序
resData.forEach(item => {
if (item.replyList) {
item.replyList = item.replyList.sort(
(a, b) => +new Date(a.createdAt) - +new Date(b.createdAt),
)
}
})
return resData.sort(
(a, b) => +new Date(b.createdAt) - +new Date(a.createdAt),
);
}
// 找到对应的comment,然后改变他的mode
export function changeCurrentToNewMode({
commentList = [],
commentId,
newType,
}) {
const newCommentList = cloneDeep(commentList);
newCommentList.forEach((item) => {
if (item.commentId === commentId) {
item.type = newType;
} else {
item.type = 'view'; // 目前只允许同时编辑一个,所以这里将其他的都变成view模式
}
if (item.replyList) {
item.replyList.forEach((ite) => {
if (ite.commentId === commentId) {
ite.type = newType;
} else {
ite.type = 'view'; // 目前只允许同时编辑一个,所以这里将其他的都变成view模式
}
});
}
});
return newCommentList;
}
// 找到所有的type为reply的,然后filter掉
export function excludeTypeIsEqualToReply({ commentList = [] }) {
const newCommentList = cloneDeep(commentList);
newCommentList.forEach((item) => {
if (item.replyList) {
item.replyList = item.replyList.filter((ite) => ite.type !== 'reply');
}
});
return newCommentList;
}
// 添加reply
export function handleAddReply({ commentList = [], commentId }) {
const newCommentList = cloneDeep(commentList);
newCommentList.forEach((item) => {
if (item.commentId === commentId) {
if (!item.replyList) {
item.replyList = [];
}
item.replyList.push({
type: 'reply',
commentId,
});
}
});
return newCommentList;
}
// 取消reply
export function cancelReply({ commentList = [], commentId }) {
const newCommentList = cloneDeep(commentList);
newCommentList.forEach((item) => {
if (item.commentId === commentId) {
item.replyList = item.replyList.slice(0, item.replyList.length - 1);
}
});
return newCommentList;
}
2.富文本编辑器代码
import React, { useState } from 'react';
import { message } from 'antd';
import classNames from 'classnames';
import { Editor } from '@tinymce/tinymce-react';
import { uuid4NoDash } from '@utils/commonFunc';
import { Card, ContainerLoading } from '@components';
import { Comment } from './components/index';
import { API } from '@service';
import './textEditor.scss';
import addCommentIcon from '@assets/imgs/Comment/add-comment.png';
function TextEditor({
changedContent = {},
isReadOnly,
setChangedContent,
commentsLoading,
setCommentsLoading,
commentList,
setCommentList,
projectId,
getAgendaCommentData,
}) {
// 是否展示评论区
const [commentAreaIsShow, setCommentAreaIsShow] = useState(false);
// 当前add的信息
const [addItemInfo, setAddItemInfo] = useState({});
// 删除已有的高亮的comment标记样式
function removeMarkComment() {
// 将已有的高亮样式删除
const Ele = document.getElementsByClassName('current_selected_comment')[0];
if (Ele) {
Ele.classList.remove('current_selected_comment');
}
}
// 添加comment
function addComment({ id, text }) {
setAddItemInfo({ id, type: 'add', text });
}
// Comment组件的公共参数
const commentPublicParams = {
setCommentsLoading,
setCommentList,
commentList,
projectId,
getAgendaCommentData,
};
return (
<div
className={classNames('editor-container', {
'ready-only-style': isReadOnly,
'write-style': !isReadOnly,
})}
>
<Editor
apiKey="自己去申请"
value={changedContent}
disabled={isReadOnly}
init={{
height: 400,
menubar: false,
skin: window.matchMedia('(prefers-color-scheme: dark)').matches
? 'oxide-dark'
: 'oxide',
plugins: [
'autolink lists image charmap print preview anchor tinycomments',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste code help tabfocus spellchecker',
],
paste_data_images: true,
toolbar_sticky: true,
toolbar:
'formatselect | fontsizeselect | bold italic alignleft aligncenter alignright alignjustify | numlist bullist | insertfile image | forecolor backcolor | customCommentButton showCommentArea',
content_style: `body {font-family:Helvetica,Arial,sans-serif; font-size:14px;color:#dbdbdb; overflow-y: hidden;}`,
setup: (editor) => {
// 追加自定义icon
editor.ui.registry.addIcon(
'addComment',
`<img src='${addCommentIcon}' />`,
);
// 追加自定义命令
editor.addCommand('addComment', (ui, v) => {
const selectionText = editor.selection.getContent();
if (selectionText) {
const uuid = uuid4NoDash();
editor.insertContent(`
<span
id="${uuid}"
style="border-bottom: 1px solid orangered;"
>
${editor.selection.getContent({ format: 'text' })}
</span>`);
addComment({ id: uuid, text: selectionText });
setCommentAreaIsShow(true);
} else {
message.warn('Please select a sentence to add a comment.');
}
});
// 添加评论的按钮
editor.ui.registry.addButton('customCommentButton', {
type: 'contextformbutton',
icon: 'addComment',
onAction: () => {
editor.editorManager.execCommand('addComment');
},
});
// 控制评论区的显示与否
editor.ui.registry.addButton('showCommentArea', {
type: 'contextformbutton',
text: 'Show/Hide comment',
onAction: () => {
const commentArea = document.getElementById(
'rich-editor-comment-wrapper',
);
const show = JSON.parse(commentArea.getAttribute('data-show'));
setCommentAreaIsShow(!show);
},
});
},
}}
onSelectionChange={(event, editor) => {
const currentEle = editor.selection.getNode();
const currentEleId = currentEle.getAttribute('id');
const targetEle = document.getElementById(
`comment_item_${currentEleId}`,
);
if (targetEle) {
removeMarkComment();
// 滚动到对应评论区的位置
targetEle.scrollIntoView({ behavior: 'smooth' });
// 追加类名,高亮对应区域
targetEle.classList.add('current_selected_comment');
} else {
removeMarkComment();
}
}}
onEditorChange={onEditorChange}
/>
<div
id="rich-editor-comment-wrapper"
data-show={JSON.stringify(commentAreaIsShow)}
style={{ display: commentAreaIsShow ? 'block' : 'none' }}
>
<Card bodyStyle={{ height: '60vh', overflowY: 'auto' }}>
{commentsLoading && <ContainerLoading />}
{/* 添加comment */}
{addItemInfo?.id && (
<Comment
{...addItemInfo}
setAddItemInfo={setAddItemInfo}
{...commentPublicParams}
/>
)}
{/* view comment */}
{commentList.map((item) => (
<Comment key={item.commentId} {...item} {...commentPublicParams} />
))}
</Card>
</div>
</div>
);
}
export default TextEditor;
四、结语
分享不易,望大家一键三连作为笔者持续分享的动力~