From a7fd94affd690e63e5568c197670f6b6d623338f Mon Sep 17 00:00:00 2001 From: chenos Date: Fri, 4 Jun 2021 11:20:11 +0800 Subject: [PATCH] refactor: useDrag & useDrop & useColResize --- packages/client/package.json | 1 + packages/client/src/blocks/grid/Col.tsx | 87 +++++++ packages/client/src/blocks/grid/DND.tsx | 224 ++++++++++++++++++ packages/client/src/blocks/grid/Row.tsx | 21 ++ .../client/src/blocks/grid/demos/demo4.tsx | 109 +++++++++ .../client/src/blocks/grid/demos/demo5.less | 16 ++ .../client/src/blocks/grid/demos/demo5.tsx | 19 ++ packages/client/src/blocks/grid/index.md | 17 +- packages/client/src/blocks/grid/index.tsx | 3 + 9 files changed, 496 insertions(+), 1 deletion(-) create mode 100644 packages/client/src/blocks/grid/Col.tsx create mode 100644 packages/client/src/blocks/grid/DND.tsx create mode 100644 packages/client/src/blocks/grid/Row.tsx create mode 100644 packages/client/src/blocks/grid/demos/demo4.tsx create mode 100644 packages/client/src/blocks/grid/demos/demo5.less create mode 100644 packages/client/src/blocks/grid/demos/demo5.tsx diff --git a/packages/client/package.json b/packages/client/package.json index bb6699f0db..1c03ae44a4 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -23,6 +23,7 @@ "@formily/react": "^2.0.0-beta.54", "ahooks": "^2.10.2", "axios": "^0.21.1", + "beautiful-react-hooks": "^0.35.0", "lodash": "^4.17.21", "react-dnd": "^14.0.2", "react-dnd-html5-backend": "^14.0.0", diff --git a/packages/client/src/blocks/grid/Col.tsx b/packages/client/src/blocks/grid/Col.tsx new file mode 100644 index 0000000000..cf6c6dbf71 --- /dev/null +++ b/packages/client/src/blocks/grid/Col.tsx @@ -0,0 +1,87 @@ +import React, { useRef, useState } from 'react'; +import { useMouseEvents } from 'beautiful-react-hooks'; + +export function useColResizer(options?: any) { + const { onDragStart, onDrag, onDragEnd } = options || {}; + const dragRef = useRef(); + const [dragOffset, setDragOffset] = useState({ left: 0, top: 0 }); + const { onMouseDown } = useMouseEvents(dragRef); + const { onMouseMove, onMouseUp } = useMouseEvents(); + const [isDragging, setIsDragging] = useState(false); + const [columns, setColumns] = useState(options.columns || []); + const [initial, setInitial] = useState(null); + + onMouseDown((event: React.MouseEvent) => { + setIsDragging(true); + const prev = dragRef.current.previousElementSibling as HTMLDivElement; + const next = dragRef.current.nextElementSibling as HTMLDivElement; + if (!initial) { + setInitial({ + offset: event.clientX, + prevWidth: prev.style.width, + nextWidth: next.style.width, + }); + } + }); + + onMouseUp((event: React.MouseEvent) => { + if (!isDragging) { + return; + } + const parent = dragRef.current.parentElement; + const els = parent.querySelectorAll('.col'); + const size = []; + els.forEach((el: HTMLDivElement) => { + const w = el.clientWidth / parent.clientWidth; + size.push(w); + el.style.width = `${100 * w}%`; + }); + console.log(size); + setIsDragging(false); + setInitial(null); + // @ts-ignore + event.data = { size }; + onDragEnd(event); + }); + + onMouseMove((event: React.MouseEvent) => { + if (!isDragging) { + return; + } + const offset = event.clientX - initial.offset; + // dragRef.current.style.transform = `translateX(${event.clientX - initialOffset}px)`; + const prev = dragRef.current.previousElementSibling as HTMLDivElement; + const next = dragRef.current.nextElementSibling as HTMLDivElement; + prev.style.width = `calc(${initial.prevWidth} + ${offset}px)`; + next.style.width = `calc(${initial.nextWidth} - ${offset}px)`; + // console.log('dragRef.current.nextSibling', prev.style.width); + }); + + return { dragOffset, dragRef, columns }; +} + +export const Col: any = (props) => { + const { size, children } = props; + return ( +
+ {children} +
+ ); +} + +Col.Divider = (props) => { + const { onDragEnd } = props; + const { dragRef } = useColResizer({ onDragEnd }); + return ( +
+ ); +} + +export default Col; diff --git a/packages/client/src/blocks/grid/DND.tsx b/packages/client/src/blocks/grid/DND.tsx new file mode 100644 index 0000000000..79c3cb2a85 --- /dev/null +++ b/packages/client/src/blocks/grid/DND.tsx @@ -0,0 +1,224 @@ +import React, { createContext, useContext, useEffect, useRef } from 'react'; +import { useState } from 'react'; +import { useMouseEvents } from 'beautiful-react-hooks'; + +export const DragDropManagerContext = createContext({ drag: null, drops: {} }); + +export function DragDropProvider({ children }) { + return ( + + {children} + + ); +} + +export function mergeRefs( + refs: Array | React.LegacyRef> +): React.RefCallback { + return (value) => { + refs.forEach((ref) => { + if (typeof ref === "function") { + ref(value); + } else if (ref != null) { + (ref as React.MutableRefObject).current = value; + } + }); + }; +} + +export function useDrag(options?: any) { + const { type, onDragStart, onDrag, onDragEnd } = options; + const dragRef = useRef(); + const previewRef = useRef(); + const [dragOffset, setDragOffset] = useState({ left: 0, top: 0 }); + const [previewOffset, setPreviewOffset] = useState({ left: 0, top: 0 }); + const { onMouseDown } = useMouseEvents(dragRef); + const { onMouseMove, onMouseUp } = useMouseEvents(); + const [previewElement, setPreviewElement] = useState(); + const [isDragging, setIsDragging] = useState(false); + const dragDropManager = useContext(DragDropManagerContext); + + onMouseDown((event: React.MouseEvent) => { + dragDropManager.drag = { type }; + setIsDragging(true); + + const offset = { + left: event.clientX - previewRef.current.offsetLeft, + top: event.clientY - previewRef.current.offsetTop, + }; + setDragOffset(offset); + + const offset2 = { + left: event.clientX - offset.left, + top: event.clientY - offset.top, + }; + setPreviewOffset(offset2); + + console.log('previewRef.current.clientWidth', previewRef.current.clientWidth); + + const wrap = document.createElement('div'); + wrap.style.position = 'absolute'; + wrap.style.pointerEvents = 'none'; + wrap.style.opacity = '0.7'; + wrap.style.left = `0px`; + wrap.style.top = `0px`; + wrap.style.zIndex = '9999'; + wrap.style.width = `${previewRef.current.clientWidth}px`; + wrap.style.transform = `translate(${offset2.left}px, ${offset2.top}px)`; + + setPreviewElement(wrap); + document.body.appendChild(wrap); + const el = document.createElement('div'); + wrap.appendChild(el); + el.outerHTML = previewRef.current.outerHTML; + onDragStart && onDragStart(event); + document.body.style.cursor = 'grab'; + + console.log('onMouseDown', dragDropManager); + }); + + onMouseUp((event: React.MouseEvent) => { + setIsDragging(false); + dragDropManager.drag = null; + if (!previewElement) { + return; + } + + previewElement.remove(); + document.body.style.cursor = null; + + if (type) { + let dropElement = document.elementFromPoint(event.clientX, event.clientY); + const dropIds = []; + while (dropElement) { + if (!dropElement.getAttribute) { + dropElement = dropElement.parentNode as Element; + continue; + } + const dropId = dropElement.getAttribute('data-drop-id'); + const dropContext = dropId ? dragDropManager.drops[dropId] : null; + if (dropContext && dropContext.accept === type) { + if ( + !dropContext.shallow || + (dropContext.shallow && dropIds.length === 0) + ) { + // @ts-ignore + event.data = dropContext.data; + onDragEnd && onDragEnd(event); + dropIds.push(dropId); + } + } + dropElement = dropElement.parentNode as Element; + } + } else { + onDragEnd && onDragEnd(event); + } + }); + + onMouseMove((event: React.MouseEvent) => { + if (!isDragging) { + return; + } + + if (!previewElement) { + return; + } + + const offset = { + left: event.clientX - dragOffset.left, + top: event.clientY - dragOffset.top, + }; + + setPreviewOffset(offset); + + previewElement.style.transform = `translate(${offset.left}px, ${offset.top}px)`; + + if (type) { + let dropElement = document.elementFromPoint(event.clientX, event.clientY); + const dropIds = []; + while (dropElement) { + if (!dropElement.getAttribute) { + dropElement = dropElement.parentNode as Element; + continue; + } + const dropId = dropElement.getAttribute('data-drop-id'); + const dropContext = dropId ? dragDropManager.drops[dropId] : null; + if (dropContext && dropContext.accept === type) { + if ( + !dropContext.shallow || + (dropContext.shallow && dropIds.length === 0) + ) { + dropIds.push(dropId); + } + // @ts-ignore + // event.data = dropContext.data; + } + dropElement = dropElement.parentNode as Element; + } + dragDropManager.drag = { type, dropIds }; + } + + onDrag && onDrag(event); + }); + + return { isDragging, previewOffset, dragOffset, dragRef, previewRef }; +} + +export function useDrop(options) { + const { accept, data, shallow } = options; + const dropRef = useRef(); + const { onMouseEnter, onMouseLeave, onMouseMove, onMouseUp } = + useMouseEvents(dropRef); + const [isOver, setIsOver] = useState(false); + const [dropId] = useState(`d${Math.random()}`); + const dragDropManager = useContext(DragDropManagerContext); + + useEffect(() => { + dragDropManager.drops[dropId] = { + accept, + data, + shallow, + }; + dropRef.current.setAttribute('data-drop-id', dropId); + }, [accept, data, shallow]); + + onMouseEnter((event) => { + console.log({ dragDropManager }); + if (!dragDropManager.drag || dragDropManager.drag.type !== accept) { + return; + } + setIsOver(true); + }); + + onMouseMove(() => { + if (!dragDropManager.drag || dragDropManager.drag.type !== accept) { + return; + } + if ( + dragDropManager.drag.dropIds && + dragDropManager.drag.dropIds.includes(dropId) + ) { + setIsOver(true); + } else { + setIsOver(false); + } + }); + + onMouseUp((event) => { + setIsOver(false); + }); + + onMouseLeave(() => { + setIsOver(false); + }); + + return { + isOver, + dropRef, + }; +} diff --git a/packages/client/src/blocks/grid/Row.tsx b/packages/client/src/blocks/grid/Row.tsx new file mode 100644 index 0000000000..5fa9a1fa9e --- /dev/null +++ b/packages/client/src/blocks/grid/Row.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { Col } from './Col'; + +export const Row = (props) => { + const { children, onColResize } = props; + const len = children.length; + return ( +
+ {children.map((child, index) => { + return ( + <> + {child} + {len > index + 1 && } + + ); + })} +
+ ); +} + +export default Row; diff --git a/packages/client/src/blocks/grid/demos/demo4.tsx b/packages/client/src/blocks/grid/demos/demo4.tsx new file mode 100644 index 0000000000..0106c1e656 --- /dev/null +++ b/packages/client/src/blocks/grid/demos/demo4.tsx @@ -0,0 +1,109 @@ +import React, { createContext, useContext, useEffect, useRef } from 'react'; +import { useDrag, useDrop, DragDropProvider } from '../'; +import { Button, Space } from 'antd'; + +function DropZone({ options, children }) { + const { isOver, dropRef } = useDrop(options); + return ( +
+ {children} +
+ ); +} + +function Dragable() { + const { isDragging, dragRef, previewRef } = useDrag({ + type: 'box', + onDragStart() { + console.log('onDragStart'); + }, + onDragEnd(event) { + console.log('onDragEnd', event.data); + }, + onDrag(event) { + // console.log('onDrag'); + }, + }); + return ( + + ); +} + +function mergeRefs( + refs: Array | React.LegacyRef> +): React.RefCallback { + return (value) => { + refs.forEach((ref) => { + if (typeof ref === "function") { + ref(value); + } else if (ref != null) { + (ref as React.MutableRefObject).current = value; + } + }); + }; +} + +function Dragable2() { + const { isDragging, dragRef, previewRef } = useDrag({ + type: 'box2', + onDragStart() { + console.log('onDragStart'); + }, + onDragEnd(event) { + console.log('onDragEnd', event.data); + }, + onDrag(event) { + // console.log('onDrag'); + }, + }); + return ( + + ); +} + +export default () => { + return ( + + + + + + + Drop Zone1 + + Drop Zone2 + + + Drop Zone3 + + Drop Zone1 + + + ); +}; diff --git a/packages/client/src/blocks/grid/demos/demo5.less b/packages/client/src/blocks/grid/demos/demo5.less new file mode 100644 index 0000000000..fb654a2faa --- /dev/null +++ b/packages/client/src/blocks/grid/demos/demo5.less @@ -0,0 +1,16 @@ +.col-divider { + position: relative; + &:hover { + &::after { + content: ''; + display: block; + position: absolute; + top: 0; + left: 50%; + transform: translateX(-50%); + height: 100%; + width: 12px; + background: #e6f7ff; + } + } +} diff --git a/packages/client/src/blocks/grid/demos/demo5.tsx b/packages/client/src/blocks/grid/demos/demo5.tsx new file mode 100644 index 0000000000..a8ab54b2a1 --- /dev/null +++ b/packages/client/src/blocks/grid/demos/demo5.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Row, Col } from '../'; +import './demo5.less'; + +export default () => { + return ( +
+ { + console.log(e.data); + }}> + {[1, 2, 3].map((index) => ( + +
col {index}
+ + ))} +
+
+ ); +}; diff --git a/packages/client/src/blocks/grid/index.md b/packages/client/src/blocks/grid/index.md index b1b070521b..e709636ba6 100644 --- a/packages/client/src/blocks/grid/index.md +++ b/packages/client/src/blocks/grid/index.md @@ -23,6 +23,14 @@ group: +### useDrag & useDrop + + + +### useColResize + + + ## API 说明 ### Grid @@ -57,4 +65,11 @@ interface BlockOptions { ### blocks2properties -原始 schema 需要至少 grid->row->col->block->custom 五层嵌套,写起来非常繁琐,`blocks2properties` 方法可以简化配置。 \ No newline at end of file +原始 schema 需要至少 grid->row->col->block->custom 五层嵌套,写起来非常繁琐,`blocks2properties` 方法可以简化配置。 + +### useDrag + +### useDrop + +### useColResize + diff --git a/packages/client/src/blocks/grid/index.tsx b/packages/client/src/blocks/grid/index.tsx index e69de29bb2..2fe3c77f17 100644 --- a/packages/client/src/blocks/grid/index.tsx +++ b/packages/client/src/blocks/grid/index.tsx @@ -0,0 +1,3 @@ +export * from './Row'; +export * from './Col'; +export * from './DND';