Braft Editor backfill issue, misplaced mouse

After backfilling, enter 1234 and it becomes 4321

react HOOK
# install with npm
npm install craft-editor --save

# install using yarn
yarn add craft-editor

import React from 'react';
import BraftEditor from 'braft-editor';
import 'braft-editor/dist/index.css';


{
   const [contentData, setContentData] = useState<string>(null);

   const handleChange = (editorState: {
    isEmpty: () => any;
    toHTML: () => React. SetStateAction<string>;
  }) => {
    if (!editorState. isEmpty()) {
      const content = editorState.toHTML();
      setContentData(content);
    } else {
      setContentData('');
    }
  };
   useEffect(() => {
     ref?.current?.onChange(BraftEditor.createEditorState(res.data.content  null));
  }, []);
  
  <BraftEditor
                {...props}
                ref={ref}
                value={null}
                onChange={handleChange}
                contentStyle={<!-- -->{ height: 400 }}
                style={<!-- -->{ border: '1px solid #d9d9d9', marginBottom: '20px' }}
                placeholder={''}
              />


}
 // BraftEditor rich text "braft-editor": "^2.3.9",
import React, { useEffect, useRef, useState } from 'react';
import BraftEditor from 'braft-editor';
import { ContentUtils } from 'braft-utils';
import PictureUpdateOss from '@/components/UploadOss/PictureUploadOss';
import { Modal } from 'antd';
import 'braft-editor/dist/index.css';

const EditorDemo: React.FC<any> = (props: any) => {
  const ref = useRef(null);
  const [logoImage, setLogoImage] = useState<string>();
  const [modalEditVisible, setModalEditVisible] = useState(false);
  const [editorState, setEditorState] = useState(BraftEditor. createEditorState(''));

  const handleChange = (editorState: {
    isEmpty: () => any;
    toHTML: () => React. SetStateAction<string>;
  }) => {
    setEditorState(editorState);
    if (!editorState. isEmpty()) {
      const content = editorState.toHTML();
      props?. changeValue(content);
    } else {
    }
  };
  // response when pasting
  // const handlePastedText = (text, html, editorState) => {
  // console. log(text)
  // console. log(html)
  // console. log(editorState)
  // debugger
  // };
  const extendControls = [
    //Add upload image function
    {
      key: 'antd-uploader',
      type: 'component',
      component: (
        <button
          type="button"
          className="control-item button upload-button"
          data-title="insert picture"
          onClick={() => {
            setModalEditVisible(true);
          }}
        >
          insert picture
        </button>
      ),
    },
  ];
  const OssUped = (values: string[]) => {
    setLogoImage(values[0]);
  };
  const onOk = () => {
    setEditorState(
      ContentUtils.insertMedias(editorState, [
        {
          type: 'IMAGE',
          url: logoImage, // imgUrl is the url address returned by the background after the upload is successful
        },
      ]),
    );
    setModalEditVisible(false);
  };
  useEffect(() => {
    ref?.current?.onChange(BraftEditor.createEditorState(props.Initvalue  '<p></p>'));
  }, [props.Initvalue]);
  return (
    <>
      <BraftEditor
        {...props}
        ref={ref}
        value={editorState}
        // controls={controls}
        onChange={handleChange}
        contentStyle={<!-- -->{ height: 400 }}
        style={<!-- -->{ border: '1px solid #d9d9d9', marginBottom: '20px' }}
        placeholder={''}
        extendControls = {extendControls}
        // response when pasting
        // handlePastedText={(text, html, editorState) => {
        // handlePastedText(text, html, editorState);
        // }}
      />
      <Modal
        open={modalEditVisible}
        destroyOnClose
        maskClosable={false}
        title={'Choose a picture! '}
        onOk={() => {
          onOk();
        }}
        width={800}
        onCancel={() => {
          setModalEditVisible(false);
        }}
      >
        <PictureUpdateOss
          files={[]}
          maxCount={1}
          maxSize={10}
          onUpLoaded={OssUped}
          fullPath
          imgCropProps={<!-- -->{
            aspect: 1.5,
          }}
        />
      </Modal>
    </>
  );
};
export default EditorDemo;
// PictureUpdateOss
"ali-oss": "^6.13.2",
    "antd-img-crop": "^3.13.2", "@types/ali-oss": "^6.0.7",

import { PlusOutlined } from '@ant-design/icons';
import OSS from 'ali-oss';
import { message, Modal, Upload } from 'antd';
import ImgCrop from 'antd-img-crop';
import MD5 from 'crypto-js/md5';
import React, { useEffect, useState } from 'react';
import { useIntl } from 'umi';
import { createAliyunSts, getAliyunOss } from './service';

const middleass = + new Date();

const getBase64 = (file: Blob) => {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = () => resolve(reader.result);
    reader.onerror = (error) => reject(error);
  });
};
interface OssType {
  accessKeyId: string;
  accessKeySecret: string;
  stsToken: string;
  endpoint: string;
  bucket: string;
  docPath: string;
  imagePath: string;
}
interface FileType {
  uid: string;
  name: string;
  status?: 'error' | 'success' | 'done' | 'uploading' | 'removed';
  url?: string;
  percent?: number;
  thumbUrl?: string;
  size: number;
  type: string;
}
interface UpdateOssProps {
  maxCount: number;
  maxSize: number;
  files: string[];
  no_crop?: boolean; // remove crop function
  onUpLoaded: (value: string[]) => void;
  fullPath?: boolean; // Whether to use full path
  imgCropProps?: any; // image upload and crop parameters https://github.com/nanxiaobei/antd-img-crop/blob/main/README.zh-CN.md
}
const PictureUpdateOss: React.FC<UpdateOssProps> = (props) => {
  const [uploadData, setUploadData] = useState<OssType>();
  const [previewVisible, setPreviewVisible] = useState<boolean>(false);
  const [previewTitle, setPreviewTitle] = useState<string>();
  const [previewImage, setPreviewImage] = useState<string>();
  const [fileLists, setFileLists] = useState<FileType[]>([]);
  const { formatMessage } = useIntl();
  const handlePreview = async (file: any) => {
    if (!file.url & amp; & amp; !file.preview) {
      file.preview = await getBase64(file.originFileObj);
    }
    setPreviewImage(file.url || file.preview);
    setPreviewVisible(true);
    setPreviewTitle(file.name || file.url.substring(file.url.lastIndexOf('/') + 1));
  };
  // alioss get upload parameters
  const initFun = async () => {
    const stsRes = await createAliyunSts({
      resource_type: 'OSS',
    });
    const ossRes = await getAliyunOss();
    if (stsRes.code === 200 & amp; & amp; ossRes.code === 200) {
      setUploadData({
        accessKeyId: stsRes.data.access_key_id,
        accessKeySecret: stsRes.data.access_key_secret,
        stsToken: stsRes.data.security_token,
        endpoint: ossRes.data.end_point,
        bucket: ossRes.data.bucket,
        docPath: ossRes.data.doc_path,
        imagePath: ossRes.data.image_path,
      });
    }
  };
  const setFileListFun = () => {
    let imgHost = `http://${uploadData?.bucket}.${uploadData?.endpoint}/`;
    if (props.files & amp; & amp; props.files.length > 0 & amp; & amp; uploadData?.bucket) {
      let newFiles: FileType[] = [];
      props.files.forEach((item: string, index) => {
        if (item) {
          let imageType = item. split('.')[1];
          newFiles. push({
            uid: `-${index + 1}`,
            name: item,
            status: 'done',
            url: `${props.fullPath ? '' : imgHost}${item}`, // Full path does not need to spell prefix
            size: 10000,
            type: `image/${imageType}`,
          });
        }
      });
      setFileLists(newFiles);
    }
  };
  const client = (upData: OssType) => {
    return new OSS({
      accessKeyId: upData. accessKeyId,
      accessKeySecret: upData. accessKeySecret,
      stsToken: upData.stsToken,
      endpoint: upData.endpoint,
      bucket: upData. bucket,
    });
  };
  useEffect(() => {
    initFun();
  }, []);
  useEffect(() => {
    setFileListFun();
  }, [uploadData, props.files]);
  // Path method of uploading files MD5(xxx).toString()
  const uploadPath = (path: string | undefined, file: any) => {
    // return `${path}/${file.name.split('.')[0]}-${middleass}.${file.type.split('/')[1]}`;
    let filenameMd5 = MD5(`${file.name.split('.')[0]}-${middleass}`).toString();
    let filetype = file.type.split('/')[1];
    if (filetype === 'svg + xml') filetype = 'svg';
    return `${path}/${filenameMd5}.${filetype}`;
  };
  // Add to Alibaba Cloud
  const UploadToOss = (file: Blob) => {
    const url = uploadPath(uploadData?.imagePath, file);
    return new Promise<void | Blob | File>((resolve, reject) => {
      client(uploadData as OssType)
        .multipartUpload(url, file, {})
        .then(() => {
          resolve();
        })
        .catch((error) => {
          reject(error);
        });
    });
  };
  // Delete Alibaba Cloud Single item deletion
  const DeleteFromOss = (delName: string) => {
    return new Promise<void | Blob | File>((resolve, reject) => {
      client(uploadData as OssType)
        .delete(delName)
        .then(() => {
          resolve();
        })
        .catch((error) => {
          reject(error);
        });
    });
  };
  // Delete Alibaba Cloud Multiple deletions
  // const DeleteFromOssMulti = (delArr: string[]) => {
  // return new Promise<void | Blob | File>((resolve, reject) => {
  // client(uploadData as OssType).deleteMulti(delArr).then(data => {
  // resolve();
  // }).catch(error => {
  // reject(error)
  // })
  // })
  // }
  // Get the Alibaba Cloud file
  // const GetFromOss = async (name: string) => {
  // let result = await client(uploadData as OssType).get(name)
  // console.log('Get Alibaba Cloud file', result)
  // }
  const handleChange = ({ fileList }: { fileList: FileType[] }) => {
    console. log(fileList);
    setFileLists(fileList);
    let newFormImg: string[] = [];
    fileList. forEach((item) => {
      if (item. url) {
        newFormImg. push(item. url);
      } else {
        let type = item.type.split('/')[1];
        if (type === 'svg + xml') type = 'svg';
        let name = item.name.split('.')[0];
        let filenameMd5 = MD5(`${name}-${middleass}`).toString();
        let imgHost = `http://${uploadData?.bucket}.${uploadData?.endpoint}/`;
        newFormImg. push(
          `${props.fullPath ? imgHost : ''}${uploadData?.imagePath}/${filenameMd5}.${type}`,
        ); // Full path splicing pass
      }
    });
    props.onUpLoaded(newFormImg);
  };
  // core upload
  // eslint-disable-next-line consistent-return
  const beforeUpload = (file: Blob) => {
    const isPicture =
      file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/svg + xml';
    if (!isPicture) {
      message.error(formatMessage({ id: 'component.table.imageTypeTip' }));
      return Upload.LIST_IGNORE;
    }
    const isMaxsize = file.size / 1024 / 1024 < props.maxSize;
    if (!isMaxsize) {
      message.error(`${formatMessage({ id: 'component.table.imageSizeTip' })}${props.maxSize}MB!`);
      return Upload.LIST_IGNORE;
    }
    if (isPicture & amp; & amp; isMaxsize) {
      return UploadToOss(file);
    }
  };
  // delete logic
  const handleRemove = (file: any) => {
    const index = fileLists. indexOf(file);
    const newFileList = fileLists. slice();
    newFileList. splice(index, 1);
    setFileLists(newFileList);
    if (file. url) {
      DeleteFromOss(file.name);
    } else {
      let delName = uploadPath(uploadData?.imagePath, file);
      DeleteFromOss(delName);
    }
  };
  const uploadButton = (
    <div>
      <PlusOutlined />
      <div style={<!-- -->{ marginTop: 8 }}>Upload</div>
    </div>
  );
  return (
    <React. Fragment>
      {props. no_crop ? (
        <Upload
          listType="picture-card"
          fileList={fileLists}
          beforeUpload={beforeUpload}
          onChange={handleChange}
          onRemove={handleRemove}
          onPreview={handlePreview}
          maxCount={props. maxCount || 1}
        >
          {fileLists. length >= props. maxCount ? null : uploadButton}
        </Upload>
      ) : (
        <ImgCrop {...props.imgCropProps}>
          <Upload
            listType="picture-card"
            fileList={fileLists}
            beforeUpload={beforeUpload}
            onChange={handleChange}
            onRemove={handleRemove}
            onPreview={handlePreview}
            maxCount={props. maxCount || 1}
          >
            {fileLists. length >= props. maxCount ? null : uploadButton}
          </Upload>
        </ImgCrop>
      )}
      <Modal
        open={previewVisible}
        title={previewTitle}
        maskClosable={false}
        footer={null}
        onCancel={() => setPreviewVisible(false)}
      >
        <img alt="example" style={<!-- -->{ width: '100%' }} src={previewImage} />
      </Modal>
    </React. Fragment>
  );
};

export default PictureUpdateOss;
//tinymce "@tinymce/tinymce-react": "^3.12.6",
import { Editor } from '@tinymce/tinymce-react';
import { Image, message } from 'antd';
import React, { useEffect, useState } from 'react';
// import { uid } from '@formily/shared';
import OSS from 'ali-oss';
import MD5 from 'crypto-js/md5';
import styles from './index.less';
import { createAliyunSts, getAliyunOss } from './service';

interface OssType {
  accessKeyId: string;
  accessKeySecret: string;
  stsToken: string;
  endpoint: string;
  bucket: string;
  docPath: string;
  imagePath: string;
}

type RTProps = {
  disabled?: boolean; //whether to disable
  onChange: (values: any) => Promise<void | boolean>;
  style?: any;
  value?: any;
};

const RickText: React.FC<RTProps> = (props) => {
  const { disabled, style, value, onChange } = props;
  const [isShow, setIsShow] = useState<boolean>();
  const [previewImgUrl, setPreviewImgUrl] = useState<string>('');
  const [uploadData, setUploadData] = useState<OssType>();

  useEffect(() => {
    // eslint-disable-next-line @typescript-eslint/no-use-before-define
    initFun();
  }, []);
  const uid = () => {
    return Number(new Date());
  };
  useEffect(() => {
    setIsShow(true);
    return () => {
      setIsShow(false);
    };
  }, []);

  // rich text event
  function pcEditorChange(con: any) {
    onChange(con);
  }

  function richTextClick(event: any) {
    if (event.target.nodeName === 'IMG' || event.target.nodeName === 'img') {
      const img = event. target. currentSrc;
      setPreviewImgUrl(img);
    }
  }

  // alioss get upload parameters
  const initFun = async () => {
    const stsRes = await createAliyunSts({
      resource_type: 'OSS',
    });
    const ossRes = await getAliyunOss();
    if (stsRes.code === 200 & amp; & amp; ossRes.code === 200) {
      setUploadData({
        accessKeyId: stsRes.data.access_key_id,
        accessKeySecret: stsRes.data.access_key_secret,
        stsToken: stsRes.data.security_token,
        endpoint: ossRes.data.end_point,
        bucket: ossRes.data.bucket,
        docPath: ossRes.data.doc_path,
        imagePath: ossRes.data.image_path,
      });
    }
    console.log(uploadData);
  };

  // Path method of uploading files MD5(xxx).toString()
  const uploadPath = (path: string | undefined, file: any) => {
    let filenameMd5 = MD5(`${path}-${uid()}`).toString();
    return `${path}/${filenameMd5}.${file.type.split('/')[1]}`;
  };

  const client = (upData: OssType) => {
    return new OSS({
      accessKeyId: upData. accessKeyId,
      accessKeySecret: upData. accessKeySecret,
      stsToken: upData.stsToken,
      endpoint: upData.endpoint,
      bucket: upData. bucket,
    });
  };

  // Add to Alibaba Cloud
  const UploadToOss = (file: Blob) => {
    if (!uploadData) {
      return;
    }
    const url = uploadPath(uploadData?.imagePath, file);
    return new Promise<string>((resolve, reject) => {
      client(uploadData as OssType)
        .multipartUpload(url, file, {
          // headers: { 'x-oss-tagging': 'willbeclean=1' }
        })
        .then(() => {
          let imgHost = `https://${uploadData?.bucket}.${uploadData?.endpoint}/`;
          resolve(imgHost + url);
        })
        .catch((error) => {
          reject(error);
          message.error('Your network is unstable, please try again later');
        });
    });
  };

  function dataURLtoBlob(dataurl: string) {
    let arr = dataurl. split(','),
      mime = arr[0].match(/:(.*?);/)[1],
      bstr = atob(arr[1]),
      n = bstr. length,
      u8arr = new Uint8Array(n);
    while (n--) {
      u8arr[n] = bstr. charCodeAt(n);
    }
    return new Blob([u8arr], { type: mime });
  }

  return (
    <div style={style}>
      {isShow & &
        (disabled? (
          <div className={styles. richWrap}>
            <div
              onClick={richTextClick}
              dangerouslySetInnerHTML={<!-- -->{
                __html: value?.replace(/\<img/gi, '<img style="width:100%;" ') || '', // eslint-disable-line
              }}
            ></div>
          </div>
        ) : (
          <Editor
            disabled={disabled}
            value={value}
            onEditorChange={pcEditorChange}
            id={'pcEditor' + uid()}
            init={<!-- -->{
              branding: false,
              menubar: false,
              inline: false,
              toolbar:
                'print | preview | code | undo redo | fullscreen | formatselect alignleft aligncenter alignright alignjustify | image table | fontselect fontsizeselect forecolor backcolor | bold italic underline strikethrough | indent outdent | superscript subscript | removeformat |',
              toolbar_drawer: 'sliding',
              quickbars_selection_toolbar:
                'removeformat | bold italic underline strikethrough | fontsizeselect forecolor backcolor',
              plugins: 'powerpaste print preview code image table lists fullscreen quickbars',
              language: 'zh_CN', // localization setting
              powerpaste_word_import: 'propmt', // The parameter can be propmt, merge, clear, the effect can be switched and compared by itself
              powerpaste_html_import: 'propmt', // propmt, merge, clear
              powerpaste_allow_local_images: true,
              paste_data_images: true,
              end_container_on_empty_block: true,
              height: 300,
              paste_preprocess: function (_pluginApi: any, data: { content: any }) {
                const content = data. content;
                // const newContent = content.replace(/\<ul/gi, '<p').replace(/\<\/ul>/gi, '</p>').replace(/\<li/gi, '<p').replace(/\<\/li>/gi, '</p>');
                const newContent = content.replace(/\<ul/gi, '<ol').replace(/\<\/ul>/gi, '</ol>'); // eslint-disable-line
                data. content = newContent;
              },
              images_upload_handler: async function (blobInfo: any, succFun) {
                succFun(
                  await UploadToOss(dataURLtoBlob('data:image/png;base64,' + blobInfo.base64())),
                );
              },
            }}
          />
        ))}
      <Image
        style={<!-- -->{ display: 'none' }}
        preview={<!-- -->{
          visible: !!previewImgUrl,
          src: previewImgUrl,
          onVisibleChange: () => {
            setPreviewImgUrl('');
          },
        }}
      />
    </div>
  );
};

export default RickText;

Braft Editor | Strongly scalable React rich text editor based on DraftJS