UNPKG

arch-editor

Version:

Rich text editor with a high degree of customization.

247 lines (222 loc) 7.51 kB
import React, { useState, useCallback, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames/bind'; import Tooltip from '@/components/Tooltip'; import { removeBlockEntity, updateBlockEntityData } from '@/utils/draftUtils'; import { useToolbar, EditToolbar } from '@/utils/atomic'; import IMAGE from '@/static/default-image.png'; import styles from './AtomicImage.less'; const cx = classNames.bind(styles); function fileToBase64(file, callback) { const fileReader = new FileReader(); fileReader.readAsDataURL(file); fileReader.onload = (e) => { callback(e.target.result); }; } export default function AtomicImage(props) { // props const { block, data = {}, readOnly, editorState, setEditorState, editing, setEditing } = props; // ref const fileRef = useRef(null); const snapshot = useRef(null); const atomicImage = useRef(null); // state const [imageObj, setImageObj] = useState({}); // effect useEffect(() => { // initial state: 初次创建处于编辑状态 let timer; if (data.initial) { setEditing(true, () => { timer = setTimeout(() => { if (atomicImage.current) { atomicImage.current.scrollIntoView(); } }, 0); }); } return () => { if (timer) clearTimeout(timer); }; }, [data, setEditing]); useEffect(() => { // 编辑手动初始化数据 setImageObj(data); snapshot.current = { [data.type]: data }; }, [data, editing]); // handler const execSubmitAction = useCallback(() => { const newEditorState = updateBlockEntityData(block, editorState, { ...imageObj, initial: false, }); setEditorState(newEditorState); }, [block, editorState, imageObj, setEditorState]); const execRemoveAction = useCallback(() => { const newEditorState = removeBlockEntity(block, editorState); setEditorState(newEditorState); }, [block, editorState, setEditorState]); const handleEdit = useCallback(() => { setEditing(true); }, [setEditing]); const handleSubmit = useCallback(() => { const { src } = imageObj; if (!src) return; setEditing(false, () => { execSubmitAction(); }); }, [execSubmitAction, imageObj, setEditing]); const handleDelete = useCallback(() => { if (editing) { setEditing(false, () => { execRemoveAction(); }); return; } execRemoveAction(); }, [editing, execRemoveAction, setEditing]); const handleCancel = useCallback(() => { setEditing(false); }, [setEditing]); const handleFileChange = (e) => { const file = e.target.files[0]; fileToBase64(file, (base64) => { setImageObj((v) => ({ ...v, src: base64 })); }); }; const handleUrlChange = (e) => { setImageObj((v) => ({ ...v, src: e.target.value })); }; const handleWidthChange = (e) => { setImageObj((v) => ({ ...v, width: e.target.value })); }; const handleHeightChange = (e) => { setImageObj((v) => ({ ...v, height: e.target.value })); }; const handleTypeChange = (e) => { // 切换type const nextType = e.target.value; const defaultImageData = { type: nextType, src: '', width: 280, height: '' }; setImageObj((v) => { // 记录当前快照 snapshot.current[v.type] = v; // 恢复之前快照 return snapshot.current[nextType] || defaultImageData; }); }; const toolbarEdit = useToolbar({ editing: false, onEdit: handleEdit, onDelete: handleDelete, }); const core = imageObj.src ? ( <img src={imageObj.src} width={imageObj.width} height={imageObj.height} alt={imageObj.src} /> ) : ( <img src={IMAGE} className={styles.defaultImage} alt="未选择图片" /> ); if (readOnly) { // 只读模式无法操作,只负责静态渲染 return <div className={styles.atomicImage}>{core}</div>; } if (editing) { return ( <div className={styles.atomicImageContainer} ref={atomicImage}> <div className={cx('atomicImage', 'active')}> {core} <div className={styles.imageToolbar}> <div className={styles.row} style={{ marginBottom: 5 }}> <label className={styles.radioButton}> <input name="imageType" type="radio" value="local" checked={imageObj.type === 'local'} onChange={handleTypeChange} /> <span>本地资源</span> </label> <label className={styles.radioButton}> <input name="imageType" type="radio" value="remote" checked={imageObj.type === 'remote'} onChange={handleTypeChange} /> <span>远程资源</span> </label> </div> <div className={styles.row}> <span className={styles.col}> <span>宽:</span> <input value={imageObj.width} className={styles.inputNumber} type="number" onChange={handleWidthChange} /> </span> <span className={styles.col}> <span>高:</span> <input value={imageObj.height} className={styles.inputNumber} type="number" onChange={handleHeightChange} /> </span> </div> {imageObj.type === 'local' && ( <div className={styles.row}> <span>地址:</span> <button className={styles.inputButton} type="button" title="base64" onClick={() => fileRef.current.click()} > {imageObj.src || '请选择本地图片'} <input ref={fileRef} type="file" accept="image/png,image/jpeg,image/gif" hidden onChange={handleFileChange} /> </button> </div> )} {imageObj.type === 'remote' && ( <div className={styles.row}> <span>地址:</span> <input value={imageObj.src} type="text" className={styles.inputText} onChange={handleUrlChange} placeholder="请输入图片资源地址" /> </div> )} </div> </div> <EditToolbar onCancel={handleCancel} onOk={handleSubmit} onDelete={handleDelete} /> </div> ); } return ( <Tooltip content={toolbarEdit}> <div className={styles.atomicImage}>{core}</div> </Tooltip> ); } AtomicImage.propTypes = { block: PropTypes.object, data: PropTypes.any, readOnly: PropTypes.bool, editorState: PropTypes.object, setEditorState: PropTypes.func, editing: PropTypes.bool, setEditing: PropTypes.func, };