tinymce富文本编辑器做评论区

news2024/11/25 0:39:27

今天分享一下tinymce富文本编辑器做评论区的全过程。

文章目录

  • 一、介绍
    • 1.最终效果
    • 2.功能介绍
    • 3.主要项目包版本介绍:
  • 二、每个功能的实现
    • 1.自定义toolbar的功能区
      • ①对应的样式以及意义
      • ②对应的代码实现【忽略了一切非实现该功能的代码】
    • 2.展示、收起评论区
      • ①对应的样式以及意义
      • ②对应的代码实现【忽略了一切非实现该功能的代码】
    • 3.选择文字然后添加评论
      • ①对应的样式以及意义
      • ②对应的代码实现【忽略了一切非实现该功能的代码】
    • 4.取消添加评论
    • 5.点击左侧富文本内容【有评论的元素】右侧评论区滚动到对应的评论
    • 6.删除评论\编辑评论\回复评论\标记评论
  • 三、完整的代码
    • 1.公共代码
      • ①Comment组件
      • ②数据处理dataProcessor
    • 2.富文本编辑器代码
  • 四、结语

一、介绍

1.最终效果

在这里插入图片描述

2.功能介绍

  1. 自定义toolbar的功能区
  2. 展示、收起评论区
  3. 选择文字然后添加评论
  4. 取消添加评论
  5. 点击左侧富文本内容【有评论的元素】右侧评论区滚动到对应的评论
  6. 删除评论
  7. 编辑评论、回复评论、标记解决评论、艾特人这些属于基本的需求了,本文不做分享

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;

四、结语

分享不易,望大家一键三连作为笔者持续分享的动力~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/48742.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ctf工具之:mitmproxy实践测试

1、安装居然使用的pip pip install mitmproxy 导入证书&#xff0c;密码为空 2、启用mitmweb pause 直接可以查看方式 搜索里输入login 对于http协议 直接看到了密码原文 3、后台日志方式 录入和回放 mitmdump -w baidu.txt pause 录制结束 mitmdump -nC baidu.txt paus…

如何设计可扩展架构

架构设计复杂度模型 业务复杂度和质量复杂度是正交的 业务复杂度 业务固有的复杂度&#xff0c;主要体现为难以理解、难以扩展&#xff0c;例如服务数量多、业务流程长、业务之间关系复杂 质量复杂度 高性能、高可用、成本、安全等质量属性的要求 架构复杂度应对之道 复杂…

MySQL备份与恢复

目录 一.数据备份的重要性 二.数据库备份的分类 2.1 物理备份 2.2 逻辑备份 2.3 完全备份&#xff08;只适合第一次&#xff09; 三.常见的备份方法 四.MySQL完全备份 4.1 MySQL完全备份优缺点 4.2 数据库完全备份分类 4.2.1 物理冷备份与恢复 五.完全备份 5.1 MySQ…

YOLO家族再度升级——阿里达摩院DAMO-YOLO重磅来袭

最近看到阿里达摩院发表了他们的最新研究成果&#xff0c;在YOLO系列上推出的新的模型DAMO-YOLO&#xff0c;还没有来得及去仔细了解一下&#xff0c;这里只是简单介绍下&#xff0c;后面有时间的话再详细研究下。 官方项目在这里&#xff0c;首页截图如下所示&#xff1a; 目…

ASEMI整流桥UD4KB100,UD4KB100体积,UD4KB100大小

编辑-Z ASEMI整流桥UD4KB100参数&#xff1a; 型号&#xff1a;UD4KB100 最大重复峰值反向电压&#xff08;VRRM&#xff09;&#xff1a;1000V 最大平均正向整流输出电流&#xff08;IF&#xff09;&#xff1a;4A 峰值正向浪涌电流&#xff08;IFSM&#xff09;&#xf…

堆(C语言实现)

文章目录&#xff1a;1.堆的概念2.堆的性质3.堆的结构4.接口实现4.1初始化堆4.2销毁堆4.3打印堆内元素4.4向上调整4.5向堆中插入数据4.6向下调整4.7删除堆顶元素4.8查看堆顶元素4.9统计堆内数据个数4.10判断堆是否为空4.11堆的构建1.堆的概念 如果有一个关键码的集合&#xff0…

【Redis】缓存更新策略

1. 缓存更新策略综述 内存淘汰 不用自己维护&#xff0c;利用 Redis 自己的内存淘汰机制 &#xff08;内存不足时&#xff0c;触发策略&#xff0c;默认开启&#xff0c;可自己配置&#xff09;&#xff0c;其可在一定程度上保持数据一致性 超时剔除 给数据添加 TTL&#x…

【电力运维】浅谈电力通信与泛在电力物联网技术的应用与发展

摘要&#xff1a;随着我国社会经济的快速发展&#xff0c;我国科技实力得到了巨大的提升&#xff0c;当前互联网通信技术在社会中得到了广泛的应用。随着电力通信技术的快速发展与更新&#xff0c;泛在电力物联网建设成为电力通讯发展的重要方向。本文已泛在电力物联网系统为核…

Docker使用

xshell和xftp软件下载 链接&#xff1a;https://pan.baidu.com/s/1G7DIw14UvOmTwU9SwtYILg 提取码&#xff1a;he18 --来自百度网盘超级会员V6的分享 docker相关资料&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1VcxvuJvBIKNKnUUHPlM3MA 提取码&#xff1a;6w5e …

一些常见的项目管理 KPI

本文将介绍一些常见的项目管理kpi&#xff0c;让大家更深刻的了解其作用及所存在的问题。 一、关键绩效指标的作用 在 GPS 和其他现代导航方法出现之前&#xff0c;水手和探险家们只能通过星星找到正确的方向。特别是在北半球&#xff0c;他们利用北极星找出真正的北方方位。…

[附源码]SSM计算机毕业设计医学季节性疾病筛查系统JAVA

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Mysql高频面试题(一)

文章目录1. Mysql如何实现的索引机制&#xff1f;2. InnoDB索引与MyISAM索引实现的区别是什么&#xff1f;3. 一个表中如果没有创建索引&#xff0c;那么还会创建B树吗&#xff1f;4. B树索引实现原理&#xff08;数据结构&#xff09;5. 聚簇索引与非聚簇索引的B树实现有什么区…

Vector源码分析

Vector源码分析 1 Vector基本介绍与类图 Vector 类实现了一个动态数组。和 ArrayList 很相似,但是两者是不同的: Vector 是同步访问的。Vector 包含了许多传统的方法,这些方法不属于集合框架。Vector 主要用在事先不知道数组的大小,或者只是需要一个可以改变大小的数组的…

pytest + yaml 框架 - 1.我们发布上线了!

前言 基于 httprunner 框架的用例结构&#xff0c;我自己开发了一个pytest yaml 的框架&#xff0c;那么是不是重复造轮子呢&#xff1f; 不可否认 httprunner 框架设计非常优秀&#xff0c;但是也有缺点&#xff0c;httprunner3.x的版本虽然也是基于pytest框架设计&#xff…

Spring中JDK与Cglib动态代理的区别

靠Spring吃饭的小伙伴一定经常听说动态代理这个词&#xff0c;没错&#xff0c;Aop就是靠它来实现的。Spring提供了两种代理模式&#xff1a;JDK动态代理、Cglib动态代理&#xff0c;供我们选择&#xff0c;那他们有啥区别呢&#xff1f;Sping为啥不自己从中挑选一个作为代理模…

IB物理的费曼图怎么考?

费曼图是用来描述基本粒子间相互作用的图形化表示&#xff0c;由诺贝尔物理学奖得主、著名物理学家理查德费曼&#xff08;Richard Feynman&#xff09;提出&#xff0c;十分清晰直观。虽然真正的费曼图可以用来做更深奥的数学计算&#xff0c;但是在IB物理中&#xff0c;考纲要…

那些惊艳一时的 CSS 属性

1.position: sticky 不知道大家平时业务开发中有没有碰到像上图一样的吸顶的需求&#xff1a;标题在滚动的时候&#xff0c;会一直贴着最顶上。 这种场景实际上很多&#xff1a;比如表格的标题栏、网站的导航栏、手机通讯录的人名首字母标题等等。如果让大家自己动手做的话&…

flink学习

Flink学习之路&#xff08;一&#xff09;Flink简介 - 走看看 Flink(一)-基本概念 - 知乎 Flink架构&#xff1a; Flink整个系统包含三个部分&#xff1a; 1、Client&#xff1a; 给用户提供向Flink系统提交用户任务&#xff08;流式作业&#xff09;的能力。用户提交一个F…

大型商场借力泛微,实现内外协同招商,合同、铺位、费用统一管理

对即将开业或是面临调整改造的购物中心来说&#xff0c;用什么样的方式才能快速地达成招商目的&#xff0c;实现资产价值的保值和增值&#xff0c;成为商业操盘手们共同面临的难题…… 行业需求 • 建立充足的品牌资源储备&#xff0c;拓宽招商渠道和线索&#xff0c;提高成交…

ElasticSearch-全文检索和分析引擎学习Day01

前言 学习谷粒商城基础片完结后便开启了高级部分的学习&#xff0c;高级部分的第一章节 Elasticsearch 搜索和分析引擎。文档地址&#xff1a;elasticsearch中文文档地址 一、Elasticsearch 简介 1.1 Elasticsearch 是什么&#xff1f; Elasticsearch 是一个分布式的免费开…