import styles from './ReactVisualEditor.less'; import { createVisualBlock, ReactVisualEditorBlock, ReactVisualEditorComponent, ReactVisualEditorConfig, ReactVisualEditorValue, } from './ReactVisualEditor.utils'; import React, { useMemo, useRef } from 'react'; import ReactDom from 'react-dom'; import { ReactVisualBlock } from './ReactVisualBlock'; import { useCallbackRef } from './../hook/useCallbackRef'; import { useVisualCommand } from './ReactVisualEditor.command'; import { createEvent } from './../plugin/event'; import classNames from 'classnames'; import { $$dialog } from './../service/dialog/$$dialog'; import { notification, Radio, Modal, Input, Collapse, Space } from 'antd'; import { $$dropdown, DropdownItem } from './../service/dropdown/$$dropdown'; import useState from 'react-usestateref'; import Drawer, { PlacementInfo } from './Drawer'; const { Panel } = Collapse; import { ReactVisualBlockResize, ReactVisualBlockResizeDirection } from './ReactVisualBlockResize'; import deepcopy from 'deepcopy'; import { ReactVisualOperator } from './ReactVisualOperator'; import { setLocalStorage, getLocalStorage, TEMPLATE_KEY } from './../localStorage'; import { visualConfig } from './visual.config'; import defaultData from './../defaultData'; import { getData } from './../requests/example'; export interface Fs { name: string; age: number; } export const ReactVisualEditor: React.FC<{ value?: ReactVisualEditorValue; containerSize?: { height: string; width: string; }; }> = (props) => { const [preview, setPreview] = useState(false); // 当前是否处于预览状态 const [editing, setEditing] = useState(true); // 当前是否处于编辑状态 const [selectIndex, setSelectIndex] = useState(-1); // 当前选中的block的索引 const [editorValue, setEditorValue] = useState( props.value || defaultData, ); const [dragstart] = useState(() => createEvent()); const [dragend] = useState(() => createEvent()); const [menuVisible, setMenuVisible] = useState(false); //菜单栏是否展开 const [operatorVisible, setOperatorVisible] = useState(false); //属性栏是否展开 const selectBlock = useMemo( () => editorValue.blocks[selectIndex] as ReactVisualEditorBlock | undefined, [editorValue.blocks, selectIndex], ); /** * container dom对象的引用 */ const containerRef = useRef({} as HTMLDivElement); /** * body dom对象的引用 */ const bodyRef = useRef({} as HTMLDivElement); /** * container dom对象的样式 */ const containerStyles = useMemo(() => { return { height: props.containerSize?.height || `1200px`, width: props.containerSize?.width || `2550px`, backgroundSize: 'cover', overflow: 'hidden', background: editorValue.container.backgroundImage ? `url('${editorValue.container.backgroundImage}')` : editorValue.container.background, }; }, [ editorValue.container.height, editorValue.container.width, editorValue.container.backgroundImage, editorValue.container.themeColor, props.containerSize ]); const classes = useMemo( () => classNames([styles.reactVisualEditor, preview && styles.reactVisualEditorPreview]), [preview], ); /** * 计算当前编辑的数据中,那些block元素是选中的,那些未选中的 */ const focusData = useMemo(() => { const focus: ReactVisualEditorBlock[] = []; const unFocus: ReactVisualEditorBlock[] = []; editorValue.blocks.forEach((block) => { (block.focus ? focus : unFocus).push(block); }); return { focus, unFocus }; }, [editorValue.blocks]); /** * 对外暴露的方法 */ const methods = { updateValue: (value: ReactVisualEditorValue) => { setEditorValue({ ...value }); }, /** * 更新blocks,触发重新渲染 */ updateBlocks: (blocks: ReactVisualEditorBlock[]) => { setEditorValue({ ...editorValue, blocks: [...blocks] }); }, /** * 清空选中的元素 */ clearFocus: (external?: ReactVisualEditorBlock) => { (!!external ? focusData.focus.filter((item) => item !== external) : focusData.focus).forEach( (block) => { block.focus = false; }, ); methods.updateBlocks(editorValue.blocks); }, /** * 显示(导出)block数据 */ showBlockData: (block: ReactVisualEditorBlock) => { $$dialog.textarea(JSON.stringify(block), { editReadonly: true, title: '节点数据' }); }, /** * 导入block数据 */ importBlockData: async (block: ReactVisualEditorBlock) => { const text = await $$dialog.textarea('', { title: '请输入导入的节点内容JSON字符串' }); try { const data = JSON.parse(text || ''); commander.updateBlock(data, block); } catch (e) { console.error(e); notification.open({ message: '导入失败!', description: '导入的数据格式不正常,请检查!', }); } }, }; /** * 拖拽处理逻辑,处理从menu菜单中拖拽预定义的组件到容器中 */ const menuDraggier = (() => { const dragData = useRef({ dragComponent: null as null | ReactVisualEditorComponent, }); const block = { dragstart: useCallbackRef( (e: React.DragEvent, dragComponent: ReactVisualEditorComponent) => { containerRef.current.addEventListener('dragenter', container.dragenter); containerRef.current.addEventListener('dragover', container.dragover); containerRef.current.addEventListener('dragleave', container.dragleave); containerRef.current.addEventListener('drop', container.drop); dragData.current.dragComponent = dragComponent; dragstart.emit(); }, ), dragend: useCallbackRef((e: React.DragEvent) => { containerRef.current.removeEventListener('dragenter', container.dragenter); containerRef.current.removeEventListener('dragover', container.dragover); containerRef.current.removeEventListener('dragleave', container.dragleave); containerRef.current.removeEventListener('drop', container.drop); }), }; const container = { dragenter: useCallbackRef((e: DragEvent) => { e.dataTransfer!.dropEffect = 'move'; }), dragover: useCallbackRef((e: DragEvent) => { e.preventDefault(); }), dragleave: useCallbackRef((e: DragEvent) => { e.dataTransfer!.dropEffect = 'none'; }), drop: useCallbackRef((e: DragEvent) => { methods.updateBlocks([ ...editorValue.blocks, createVisualBlock({ top: e.offsetY, left: e.offsetX, component: dragData.current.dragComponent!, }), ]); setTimeout(dragend.emit); }), }; return block; })(); /** * 处理block元素的选中事件 */ const focusHandler = (() => { const block = ( e: React.MouseEvent, block: ReactVisualEditorBlock, index: number, ) => { if (preview) return; if (e.button === 2) { /*右键不做任何处理*/ return; } if (e.shiftKey) { /*如果摁住了shift键,如果此时没有选中的block,就选中这个block,否则令这个block的选中状态取反*/ if (focusData.focus.length <= 1) { block.focus = true; } else { block.focus = !block.focus; } methods.updateBlocks(editorValue.blocks); } else { /*如果点击的这个block没有被选中,才清空这个其他选中的block,否则不做任何事情。放置拖拽多个block,取消其他block的选中状态*/ if (!block.focus) { block.focus = true; methods.clearFocus(block); } } setSelectIndex(block.focus ? index : -1); setTimeout(() => blockDraggier.mousedown(e, block)); }; const container = (e: React.MouseEvent) => { if (preview) return; if (e.target !== e.currentTarget) { return; } if (!e.shiftKey) { methods.clearFocus(); setSelectIndex(-1); } }; return { block, container, }; })(); /** * 处理block元素在container容器内的拖拽动作 */ const blockDraggier = (() => { const [mark, setMark] = useState({ x: null as null | number, y: null as null | number }); const dragData = useRef({ startX: 0, // 拖拽开始时,鼠标的left startY: 0, // 拖拽开始的时候,鼠标的top值 moveX: 0, // 拖拽过程中鼠标的left值 moveY: 0, // 拖拽过程中,鼠标的top值 shiftKey: false, // 当前是否摁住了shift键 body: { startScrollTop: 0, // 拖拽开始时,body的scrollTop值 moveScrollTop: 0, // 拖拽过程中,固定body的scrollTop值 }, startLeft: 0, // 拖拽开始的时候,拖拽的block的left startTop: 0, // 拖拽开始的时候,拖拽的block的top startPosArray: [] as { top: number; left: number }[], // 拖拽开始的时候,所有选中的block元素的top值以及left值 dragging: false, // 当前是否处于拖拽状态 /** * markLines 拖拽元素的时候,计算当前未选中的数据中,与拖拽元素之间应该显示的辅助线 */ markLines: { /*横向辅助线*/ x: [] as { left: number; showLeft: number }[], /*纵向辅助线*/ y: [] as { top: number; showTop: number }[], }, }); const handleMove = useCallbackRef(() => { if (!dragData.current.dragging) { dragData.current.dragging = true; dragstart.emit(); } let { moveX, moveY, body, shiftKey, startX, startY, startPosArray, markLines, startLeft, startTop, } = dragData.current; moveY = moveY + (body.moveScrollTop - body.startScrollTop); if (shiftKey) { if (Math.abs(moveX - startX) > Math.abs(moveY - startY)) { moveY = startY; } else { moveX = startX; } } const now = { mark: { x: null as null | number, y: null as null | number, }, top: startTop + moveY - startY, left: startLeft + moveX - startX, }; for (let i = 0; i < markLines.y.length; i++) { const { top, showTop } = markLines.y[i]; if (Math.abs(now.top - top) < 5) { moveY = top + startY - startTop; now.mark.y = showTop; } } for (let i = 0; i < markLines.x.length; i++) { const { left, showLeft } = markLines.x[i]; if (Math.abs(now.left - left) < 5) { moveX = left + startX - startLeft; now.mark.x = showLeft; } } const durX = moveX - startX, durY = moveY - startY; console.log('e.clientX', durX); focusData.focus.forEach((block, index) => { const { left, top } = startPosArray[index]; block.top = top + durY; block.left = left + durX; }); methods.updateBlocks(editorValue.blocks); setMark(now.mark); }); const mousedown = useCallbackRef( (e: React.MouseEvent, block: ReactVisualEditorBlock) => { e.stopPropagation(); document.addEventListener('mouseup', mouseup); document.addEventListener('mousemove', mousemove); bodyRef.current.addEventListener('scroll', scroll); dragData.current = { startX: e.clientX, startY: e.clientY, moveX: e.clientX, moveY: e.clientY, shiftKey: e.shiftKey, body: { startScrollTop: bodyRef.current.scrollTop, moveScrollTop: bodyRef.current.scrollTop, }, startLeft: block.left, startTop: block.top, startPosArray: focusData.focus.map(({ top, left }) => ({ top, left })), dragging: false, markLines: (() => { const x: { left: number; showLeft: number }[] = []; const y: { top: number; showTop: number }[] = []; const { unFocus } = focusData; unFocus.forEach((b) => { y.push({ top: b.top, showTop: b.top }); // 顶部对顶部 y.push({ top: b.top + b.height / 2 - block.height / 2, showTop: b.top + b.height / 2, }); // 中间对中间 y.push({ top: b.top + b.height - block.height, showTop: b.top + b.height }); // 底部对齐底部 y.push({ top: b.top - block.height, showTop: b.top }); // 底部对齐顶部 y.push({ top: b.top + b.height, showTop: b.top + b.height }); // 顶部对齐底部 x.push({ left: b.left, showLeft: b.left }); // 左侧对齐左侧 x.push({ left: b.left + b.width / 2 - block.width / 2, showLeft: b.left + b.width / 2, }); // 中间对中间 x.push({ left: b.left + b.width - block.width, showLeft: b.left + b.width }); // 右侧对齐右侧 x.push({ left: b.left - block.width, showLeft: b.left }); // 右侧对齐左侧 x.push({ left: b.left + b.width, showLeft: b.left + b.width }); // 左侧对齐右侧 }); return { x, y }; })(), }; }, ); const mousemove = useCallbackRef((e: MouseEvent) => { console.log('mousemove'); dragData.current.moveX = e.clientX; dragData.current.moveY = e.clientY; handleMove(); }); const mouseup = useCallbackRef((e: MouseEvent) => { console.log('mouseup'); document.removeEventListener('mousemove', mousemove); document.removeEventListener('mouseup', mouseup); bodyRef.current.removeEventListener('scroll', scroll); setMark({ x: null, y: null }); if (dragData.current.dragging) { dragend.emit(); } }); const scroll = useCallbackRef((e: Event) => { dragData.current.body.moveScrollTop = (e.target as HTMLDivElement).scrollTop; handleMove(); }); return { mousedown, mark, }; })(); /** * 处理block元素拖拽调整大小的拖拽动作 */ const resizeDraggier = (() => { const dragData = useRef({ block: {} as ReactVisualEditorBlock, // 拖拽开始的时候,拖拽的block元素对象应用 startX: 0, // 拖拽开始的时候,鼠标的left startY: 0, // 拖拽开始的时候,鼠标的top值 direction: { // 拖拽的指示点,是什么方向的点,如果是偏顶部,左侧的点,在拖拽的时候,差值要取反,并且还需要修改block的top或者left值 horizontal: ReactVisualBlockResizeDirection.start, vertical: ReactVisualBlockResizeDirection.start, }, startBlock: { top: 0, // 拖拽开始的时候,block的top值 left: 0, // 拖拽开始的时候,block的left值 height: 0, // 拖拽开始的时候,block的高度 width: 0, // 拖拽开始的时候,block的宽度 }, dragging: false, // 第一次move的时候才需要派发dragstart事件 }); const mousedown = useCallbackRef( ( e: React.MouseEvent, direction: { horizontal: ReactVisualBlockResizeDirection; vertical: ReactVisualBlockResizeDirection; }, block: ReactVisualEditorBlock, ) => { e.stopPropagation(); document.addEventListener('mousemove', mousemove); document.addEventListener('mouseup', mouseup); dragData.current = { block, startX: e.clientX, startY: e.clientY, direction, startBlock: { ...deepcopy(block) }, dragging: false, }; }, ); const mousemove = useCallbackRef((e: MouseEvent) => { if (!dragData.current.dragging) { dragData.current.dragging = true; dragstart.emit(); } let { clientX: moveX, clientY: moveY } = e; const { startX, startY, startBlock, direction, block } = dragData.current; if (direction.horizontal === ReactVisualBlockResizeDirection.center) { moveX = startX; } if (direction.vertical === ReactVisualBlockResizeDirection.center) { moveY = startY; } let durX = moveX - startX; let durY = moveY - startY; if (direction.vertical === ReactVisualBlockResizeDirection.start) { durY = -durY; block!.top = startBlock.top - durY; } if (direction.horizontal === ReactVisualBlockResizeDirection.start) { durX = -durX; block!.left = startBlock.left - durX; } const width = startBlock.width + durX; const height = startBlock.height + durY; block!.width = width; block!.height = height; block!.hasResize = true; methods.updateBlocks(editorValue.blocks); }); const mouseup = useCallbackRef((e: MouseEvent) => { document.removeEventListener('mousemove', mousemove); document.removeEventListener('mouseup', mouseup); if (dragData.current.dragging) { setTimeout(dragend.emit); } }); return { mousedown, }; })(); /** * 命令管理对象 */ const commander = useVisualCommand({ value: editorValue, focusData, updateBlocks: methods.updateBlocks, updateValue: methods.updateValue, dragstart, dragend, }); const ListMode: React.FC<{ list: Array<{ name: string; obj: {} }>; isSave?: boolean }> = ({ isSave = false, ...prop }) => { const [show, setShow] = useState(true); const [addValue, setAddValue, refAddValue] = useState('1'); const [value, setValue, refValue] = useState( prop.list.length > 0 ? prop.list[0].name : refAddValue.current, ); const onChange = (ev: any) => { setValue(ev.target.value); }; return ( setShow(false)} onOk={() => { if (isSave) { if (value && value !== '1' && value !== '') { setLocalStorage(value, JSON.stringify(editorValue)); setShow(false); } else { notification.warning({ message: '提示', description: '请输入模板名称' }); } } else { for (let i = 0; i < prop.list.length; i++) { if (prop.list[i].name === value) { commander.updateValue(JSON.parse(prop.list[i].obj) as ReactVisualEditorValue); break; } } setShow(false); } }} > {prop.list.map((item, index) => { return ( {item.name} ); })} {isSave && ( { if (refAddValue.current === refValue.current) { setValue(v.target.value); } setAddValue(v.target.value); }} placeholder={'增加新模板'} /> )} ); }; const handler = { onContextMenuBlock: (e: React.MouseEvent, block: ReactVisualEditorBlock) => { e.preventDefault(); e.stopPropagation(); $$dropdown({ reference: e.nativeEvent, render: () => ( <> 置顶节点 置底节点 删除节点 methods.showBlockData(block)}> 查看数据 methods.importBlockData(block)}> 导入数据 ), }); }, }; const buttons: { label: string | (() => string); icon: string | (() => string); tip?: string | (() => string); handler: () => void; }[] = [ { label: '撤销', icon: 'icon-back', handler: commander.undo, tip: 'ctrl+z' }, { label: '重做', icon: 'icon-forward', handler: commander.redo, tip: 'ctrl+y, ctrl+shift+z' }, { label: () => (preview ? '预览模式' : '编辑模式'), icon: () => (preview ? 'icon-edit' : 'icon-browse'), handler: () => { if (!preview) { methods.clearFocus(); } setPreview(!preview); }, }, { label: () => (menuVisible ? '关闭菜单' : '打开菜单'), icon: () => (preview ? 'icon-edit' : 'icon-browse'), handler: () => { setMenuVisible(!menuVisible); }, }, { label: '选择模板', icon: 'icon-import', handler: async () => { const childApen = document.createElement('div'); const list = getLocalStorage(TEMPLATE_KEY); ReactDom.render(, childApen); }, }, { label: '导入', icon: 'icon-import', handler: async () => { const text = await $$dialog.textarea('', { title: '请输入导入的JSON字符串' }); try { const data = JSON.parse(text || ''); commander.updateValue(data); } catch (e) { console.error(e); notification.open({ message: '导入失败!', description: '导入的数据格式不正常,请检查!', }); } }, }, { label: '保存', icon: 'icon-export', handler: () => { const childApen = document.createElement('div'); const list = getLocalStorage(TEMPLATE_KEY); ReactDom.render(, childApen); }, }, { label: '置顶', icon: 'icon-place-top', handler: () => commander.placeTop(), tip: 'ctrl+up' }, { label: '置底', icon: 'icon-place-bottom', handler: () => commander.placeBottom(), tip: 'ctrl+down', }, { label: '删除', icon: 'icon-delete', handler: commander.delete, tip: 'ctrl+d, backspace, delete', }, { label: '清空', icon: 'icon-reset', handler: commander.clear }, { label: () => (operatorVisible ? '关闭属性栏' : '展开属性栏'), icon: 'icon-close', handler: () => { setOperatorVisible(!operatorVisible); }, }, ]; /** * * @returns 分类菜单栏组件 */ const previewElement = () => { const dataArr: any = []; visualConfig.componentArray.map((mapItem) => { if (dataArr.length == 0) { dataArr.push({ type: mapItem.type, List: [mapItem] }); } else { let res = dataArr.some((item: any) => { if (item.type === mapItem.type) { item.List.push(mapItem); return true; } }); if (!res) { //如果没找相同的类型的组添加一个新对象 dataArr.push({ type: mapItem.type, List: [mapItem] }); } } }); return dataArr; }; return (
{ setMenuVisible(false); }} visible={menuVisible} >
{previewElement().map((item: any, index: any) => { return ( {item.type}
} key={index} > {item.List.map((component: any, index1: any) => { return (
menuDraggier.dragstart(e, component)} onDragEnd={menuDraggier.dragend} >
{component.name}
{component.preview()}
); })} ); })}
{buttons.map((btn, index) => { const label = typeof btn.label === 'function' ? btn.label() : btn.label; const icon = typeof btn.icon === 'function' ? btn.icon() : btn.icon; return (
{label}
); })}
{ setOperatorVisible(false); }} visible={operatorVisible} >
{editorValue.blocks.map((block, index) => ( focusHandler.block(e, block, index)} onContextMenu={(e) => handler.onContextMenuBlock(e, block)} click={(pro:any)=>{ preview&&pro&&pro['jumpUrl']&& getData(pro['jumpUrl']).then((res)=>{ if(res){ setEditorValue({...res}) } }) }} > {block.focus && !!visualConfig.componentMap[block.componentKey] && !!visualConfig.componentMap[block.componentKey].resize && (visualConfig.componentMap[block.componentKey].resize!.width || visualConfig.componentMap[block.componentKey].resize!.height) && ( resizeDraggier.mousedown(e, direction, block)} /> )} ))} {blockDraggier.mark.x != null && (
)} {blockDraggier.mark.y != null && (
)}
); };