refactor: useDrag & useDrop & useColResize
This commit is contained in:
parent
3c3ae5c348
commit
a7fd94affd
@ -23,6 +23,7 @@
|
|||||||
"@formily/react": "^2.0.0-beta.54",
|
"@formily/react": "^2.0.0-beta.54",
|
||||||
"ahooks": "^2.10.2",
|
"ahooks": "^2.10.2",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
|
"beautiful-react-hooks": "^0.35.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"react-dnd": "^14.0.2",
|
"react-dnd": "^14.0.2",
|
||||||
"react-dnd-html5-backend": "^14.0.0",
|
"react-dnd-html5-backend": "^14.0.0",
|
||||||
|
87
packages/client/src/blocks/grid/Col.tsx
Normal file
87
packages/client/src/blocks/grid/Col.tsx
Normal 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;
|
224
packages/client/src/blocks/grid/DND.tsx
Normal file
224
packages/client/src/blocks/grid/DND.tsx
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
21
packages/client/src/blocks/grid/Row.tsx
Normal file
21
packages/client/src/blocks/grid/Row.tsx
Normal 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;
|
109
packages/client/src/blocks/grid/demos/demo4.tsx
Normal file
109
packages/client/src/blocks/grid/demos/demo4.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
16
packages/client/src/blocks/grid/demos/demo5.less
Normal file
16
packages/client/src/blocks/grid/demos/demo5.less
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
packages/client/src/blocks/grid/demos/demo5.tsx
Normal file
19
packages/client/src/blocks/grid/demos/demo5.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -23,6 +23,14 @@ group:
|
|||||||
|
|
||||||
<code src="./demos/demo3.tsx"/>
|
<code src="./demos/demo3.tsx"/>
|
||||||
|
|
||||||
|
### useDrag & useDrop
|
||||||
|
|
||||||
|
<code src="./demos/demo4.tsx"/>
|
||||||
|
|
||||||
|
### useColResize
|
||||||
|
|
||||||
|
<code src="./demos/demo5.tsx"/>
|
||||||
|
|
||||||
## API 说明
|
## API 说明
|
||||||
|
|
||||||
### Grid
|
### Grid
|
||||||
@ -58,3 +66,10 @@ interface BlockOptions {
|
|||||||
### blocks2properties
|
### blocks2properties
|
||||||
|
|
||||||
原始 schema 需要至少 grid->row->col->block->custom 五层嵌套,写起来非常繁琐,`blocks2properties` 方法可以简化配置。
|
原始 schema 需要至少 grid->row->col->block->custom 五层嵌套,写起来非常繁琐,`blocks2properties` 方法可以简化配置。
|
||||||
|
|
||||||
|
### useDrag
|
||||||
|
|
||||||
|
### useDrop
|
||||||
|
|
||||||
|
### useColResize
|
||||||
|
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
export * from './Row';
|
||||||
|
export * from './Col';
|
||||||
|
export * from './DND';
|
Loading…
Reference in New Issue
Block a user