refactor: useDrag & useDrop & useColResize

This commit is contained in:
chenos 2021-06-04 11:20:11 +08:00
parent 3c3ae5c348
commit a7fd94affd
9 changed files with 496 additions and 1 deletions

View File

@ -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",

View File

@ -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<HTMLDivElement>();
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<any>(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 (
<div
className={'col'}
style={{ width: `${size * 100}%` }}
>
{children}
</div>
);
}
Col.Divider = (props) => {
const { onDragEnd } = props;
const { dragRef } = useColResizer({ onDragEnd });
return (
<div
className={'col-divider'}
style={{ width: '24px', cursor: 'col-resize' }}
ref={dragRef}
></div>
);
}
export default Col;

View File

@ -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 (
<DragDropManagerContext.Provider
value={{
drag: null,
drops: {},
}}
>
{children}
</DragDropManagerContext.Provider>
);
}
export function mergeRefs<T = any>(
refs: Array<React.MutableRefObject<T> | React.LegacyRef<T>>
): React.RefCallback<T> {
return (value) => {
refs.forEach((ref) => {
if (typeof ref === "function") {
ref(value);
} else if (ref != null) {
(ref as React.MutableRefObject<T | null>).current = value;
}
});
};
}
export function useDrag(options?: any) {
const { type, onDragStart, onDrag, onDragEnd } = options;
const dragRef = useRef<HTMLButtonElement>();
const previewRef = useRef<HTMLDivElement>();
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<HTMLDivElement>();
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<HTMLDivElement>();
const { onMouseEnter, onMouseLeave, onMouseMove, onMouseUp } =
useMouseEvents(dropRef);
const [isOver, setIsOver] = useState(false);
const [dropId] = useState<string>(`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,
};
}

View File

@ -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 (
<div style={{ display: 'flex' }}>
{children.map((child, index) => {
return (
<>
{child}
{len > index + 1 && <Col.Divider onDragEnd={onColResize} />}
</>
);
})}
</div>
);
}
export default Row;

View File

@ -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 (
<div
ref={dropRef}
style={{
textAlign: 'center',
lineHeight: '100px',
margin: 24,
border: isOver ? '1px solid red' : '1px solid #ddd',
}}
>
{children}
</div>
);
}
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 (
<Button ref={mergeRefs<any>([dragRef, previewRef])}>1</Button>
);
}
function mergeRefs<T = any>(
refs: Array<React.MutableRefObject<T> | React.LegacyRef<T>>
): React.RefCallback<T> {
return (value) => {
refs.forEach((ref) => {
if (typeof ref === "function") {
ref(value);
} else if (ref != null) {
(ref as React.MutableRefObject<T | null>).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 (
<Button ref={mergeRefs<any>([dragRef, previewRef])}>2</Button>
);
}
export default () => {
return (
<DragDropProvider>
<Space style={{marginBottom: 12}}>
<Dragable />
<Dragable2 />
</Space>
<DropZone
options={{
accept: 'box',
data: { a: 'a' },
shallow: true,
}}
>
Drop Zone1
<DropZone
options={{
accept: 'box',
data: { b: 'b' },
// shallow: true,
}}
>
Drop Zone2
</DropZone>
<DropZone
options={{
accept: 'box2',
data: { c: 'c' },
// shallow: true,
}}
>
Drop Zone3
</DropZone>
Drop Zone1
</DropZone>
</DragDropProvider>
);
};

View File

@ -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;
}
}
}

View File

@ -0,0 +1,19 @@
import React from 'react';
import { Row, Col } from '../';
import './demo5.less';
export default () => {
return (
<div>
<Row onColResize={(e) => {
console.log(e.data);
}}>
{[1, 2, 3].map((index) => (
<Col size={1 / 3}>
<div style={{textAlign: 'center', lineHeight: '60px', background: '#f1f1f1'}}>col {index}</div>
</Col>
))}
</Row>
</div>
);
};

View File

@ -23,6 +23,14 @@ group:
<code src="./demos/demo3.tsx"/>
### useDrag & useDrop
<code src="./demos/demo4.tsx"/>
### useColResize
<code src="./demos/demo5.tsx"/>
## API 说明
### Grid
@ -57,4 +65,11 @@ interface BlockOptions {
### blocks2properties
原始 schema 需要至少 grid->row->col->block->custom 五层嵌套,写起来非常繁琐,`blocks2properties` 方法可以简化配置。
原始 schema 需要至少 grid->row->col->block->custom 五层嵌套,写起来非常繁琐,`blocks2properties` 方法可以简化配置。
### useDrag
### useDrop
### useColResize

View File

@ -0,0 +1,3 @@
export * from './Row';
export * from './Col';
export * from './DND';