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