mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 12:40:50 +00:00
refactor: useDrag & useDrop & useColResize
This commit is contained in:
parent
3c3ae5c348
commit
a7fd94affd
@ -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",
|
||||
|
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"/>
|
||||
|
||||
### 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
|
||||
|
||||
|
@ -0,0 +1,3 @@
|
||||
export * from './Row';
|
||||
export * from './Col';
|
||||
export * from './DND';
|
Loading…
Reference in New Issue
Block a user