improve a lot of details

This commit is contained in:
chenos 2021-06-06 18:21:04 +08:00
parent a7fd94affd
commit 5f752d28d7
15 changed files with 1278 additions and 1236 deletions

View File

@ -0,0 +1,102 @@
import React, { useState } from 'react';
import { useDrag, mergeRefs, useDrop } from './DND';
import classNames from 'classnames';
import { useField } from '@formily/react';
import { Dropdown, Menu } from 'antd';
import { useSchemaQuery } from '.';
import { MenuOutlined, ArrowUpOutlined, ArrowDownOutlined, DeleteOutlined } from '@ant-design/icons';
export const Block = (props) => {
const { children, title } = props;
const field = useField<Formily.Core.Models.Field>();
const { isDragging, dragRef, previewRef } = useDrag({
type: 'grid',
onDragStart() {
// console.log('onDragStart');
},
onDragEnd(event) {
console.log('onDragEnd', event);
},
onDrag(event) {
// console.log('onDrag');
},
item: {
path: field.address.segments,
},
});
console.log('useDrag', 'previewElement', field.address.segments);
const { isOver, onTopHalf, dropRef } = useDrop({
accept: 'grid',
data: {},
canDrop: !isDragging,
});
const { addBlock } = useSchemaQuery();
return (
<div
data-type={'block'}
ref={mergeRefs([dropRef, previewRef])}
className={classNames('block', { 'top-half': onTopHalf, hover: isOver })}
// style={{ textAlign: 'center', lineHeight: '60px', background: '#f1f1f1' }}
>
<ActionBar dragRef={dragRef}/>
<div
style={{ textAlign: 'center', lineHeight: '60px', background: '#f1f1f1' }}
>
{children} {field.path.entire}
</div>
</div>
);
};
const ActionBar = ({ dragRef }) => {
const { addBlock, removeBlock } = useSchemaQuery();
const [active, setActive] = useState(false);
return (
<div className={classNames('action-bar', { active })}>
<Dropdown
overlayStyle={{ minWidth: 200 }}
trigger={['click']}
visible={active}
onVisibleChange={setActive}
overlay={
<Menu>
<Menu.Item
onClick={() => {
addBlock({}, {
insertBefore: true,
});
setActive(false);
}}
icon={<ArrowUpOutlined />}
>
</Menu.Item>
<Menu.Item
onClick={() => {
addBlock();
setActive(false);
}}
icon={<ArrowDownOutlined />}
>
</Menu.Item>
<Menu.Divider />
<Menu.Item
onClick={() => {
removeBlock();
setActive(false);
}}
icon={<DeleteOutlined />}
>
</Menu.Item>
</Menu>
}
>
<MenuOutlined className={'draggable'} ref={dragRef} />
</Dropdown>
</div>
);
};
export default Block;

View File

@ -1,5 +1,7 @@
import React, { useRef, useState } from 'react';
import React, { cloneElement, useRef, useState } from 'react';
import { useMouseEvents } from 'beautiful-react-hooks';
import { mergeRefs, useDrop } from './DND';
import classNames from 'classnames';
export function useColResizer(options?: any) {
const { onDragStart, onDrag, onDragEnd } = options || {};
@ -12,9 +14,15 @@ export function useColResizer(options?: any) {
const [initial, setInitial] = useState<any>(null);
onMouseDown((event: React.MouseEvent) => {
setIsDragging(true);
if (event.button !== 0) {
return;
}
const prev = dragRef.current.previousElementSibling as HTMLDivElement;
const next = dragRef.current.nextElementSibling as HTMLDivElement;
if (!prev || !next) {
return;
}
setIsDragging(true);
if (!initial) {
setInitial({
offset: event.clientX,
@ -41,7 +49,7 @@ export function useColResizer(options?: any) {
setInitial(null);
// @ts-ignore
event.data = { size };
onDragEnd(event);
onDragEnd && onDragEnd(event);
});
onMouseMove((event: React.MouseEvent) => {
@ -57,31 +65,36 @@ export function useColResizer(options?: any) {
// console.log('dragRef.current.nextSibling', prev.style.width);
});
return { dragOffset, dragRef, columns };
return { isDragging, dragOffset, dragRef, columns };
}
export const Col: any = (props) => {
const { size, children } = props;
const { size, children, position = {}, isLast } = props;
return (
<div
className={'col'}
style={{ width: `${size * 100}%` }}
>
{children}
</div>
<>
<div data-type={'col'} className={'col'} style={{ width: `${size * 100}%` }}>
{children}
</div>
<Col.Divider />
</>
);
}
};
Col.Divider = (props) => {
const { onDragEnd } = props;
const { dragRef } = useColResizer({ onDragEnd });
const { onDragEnd, resizable = true } = props;
const { isDragging, dragRef } = useColResizer({ onDragEnd });
const { isOver, dropRef } = useDrop({
accept: 'grid',
data: { },
});
return (
<div
className={'col-divider'}
style={{ width: '24px', cursor: 'col-resize' }}
ref={dragRef}
data-type={'col-divider'}
className={classNames('col-divider', { hover: isOver, resizable })}
style={{ width: '24px' }}
ref={mergeRefs(resizable ? [dropRef, dragRef] : [dropRef])}
></div>
);
}
};
export default Col;

View File

@ -1,15 +1,24 @@
import React, { createContext, useContext, useEffect, useRef } from 'react';
import { useState } from 'react';
import { useMouseEvents } from 'beautiful-react-hooks';
import { useMouseEvents, useWillUnmount } from 'beautiful-react-hooks';
import { useField } from '@formily/react';
export const DragDropManagerContext = createContext({ drag: null, drops: {} });
export const DragDropManagerContext = createContext<any>({
drag: null,
drops: {},
name: `${Math.random()}`,
});
export function DragDropProvider({ children }) {
export function DragDropProvider(props) {
const { gridRef, onDrop, children } = props;
return (
<DragDropManagerContext.Provider
value={{
drag: null,
drops: {},
onDrop,
gridRef,
name: `${Math.random()}`,
}}
>
{children}
@ -18,11 +27,11 @@ export function DragDropProvider({ children }) {
}
export function mergeRefs<T = any>(
refs: Array<React.MutableRefObject<T> | React.LegacyRef<T>>
refs: Array<React.MutableRefObject<T> | React.LegacyRef<T>>,
): React.RefCallback<T> {
return (value) => {
refs.forEach((ref) => {
if (typeof ref === "function") {
if (typeof ref === 'function') {
ref(value);
} else if (ref != null) {
(ref as React.MutableRefObject<T | null>).current = value;
@ -32,34 +41,62 @@ export function mergeRefs<T = any>(
}
export function useDrag(options?: any) {
const { type, onDragStart, onDrag, onDragEnd } = options;
const dragRef = useRef<HTMLButtonElement>();
const { type, onDragStart, onDrag, onDragEnd, item = {} } = options;
const dragRef = useRef<HTMLDivElement>();
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 [mousePostionWithinPreview, setMousePostionWithinPreview] = useState({
left: 0,
top: 0,
});
const dragDropManager = useContext(DragDropManagerContext);
const field = useField();
console.log(
'init',
{ isDragging, previewElement },
field ? field.address.segments : null,
);
useWillUnmount(() => {
setIsDragging(false);
console.log(
'useWillUnmount',
{ isDragging, previewElement },
dragDropManager.previewElement,
field ? field.address.segments : null,
);
// @ts-ignore
window.__previewElement && window.__previewElement.remove();
// @ts-ignore
window.__previewElement = undefined;
dragDropManager.drag = null;
document.body.style.cursor = null;
document.body.style.userSelect = null;
})
onMouseDown((event: React.MouseEvent) => {
dragDropManager.drag = { type };
if (event.button !== 0) {
return;
}
dragDropManager.drag = { type, item };
dragDropManager.drop = { type, item };
setIsDragging(true);
const postion = {
left: event.clientX - previewRef.current.getBoundingClientRect().left,
top: event.clientY - previewRef.current.getBoundingClientRect().top,
};
setMousePostionWithinPreview(postion);
const offset = {
left: event.clientX - previewRef.current.offsetLeft,
top: event.clientY - previewRef.current.offsetTop,
left: event.pageX - postion.left,
top: event.pageY - postion.top,
};
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';
@ -69,73 +106,100 @@ export function useDrag(options?: any) {
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)`;
wrap.style.transform = `translate(${offset.left}px, ${offset.top}px)`;
setPreviewElement(wrap);
// @ts-ignore
window.__previewElement = wrap;
console.log('dragDropManager.previewElement', dragDropManager.previewElement)
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';
document.body.style.userSelect = 'none';
document.body.className = 'dragging';
console.log('onMouseDown', dragDropManager);
console.log(
'onMouseDown',
{ isDragging, previewElement },
dragDropManager.previewElement,
field ? field.address.segments : null,
);
// console.log('onMouseDown', event);
});
onMouseUp((event: React.MouseEvent) => {
setIsDragging(false);
dragDropManager.drag = null;
if (!previewElement) {
if (!isDragging || !previewElement) {
return;
}
previewElement.remove();
document.body.style.cursor = null;
console.log(
'onMouseUp',
{ isDragging, previewElement },
field ? field.address.segments : 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 {
// @ts-ignore
event.dragItem = item;
setIsDragging(false);
dragDropManager.drag = null;
previewElement.remove();
setPreviewElement(undefined);
document.body.style.cursor = null;
document.body.style.userSelect = null;
// @ts-ignore
window.__previewElement && window.__previewElement.remove();
// @ts-ignore
window.__previewElement = undefined;
if (!type) {
onDragEnd && onDragEnd(event);
}
let dropElement = document.elementFromPoint(event.clientX, event.clientY);
const dropIds = [];
while (dropElement) {
if (!dropElement.getAttribute) {
dropElement = dropElement.parentNode as HTMLElement;
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 HTMLElement;
}
});
onMouseMove((event: React.MouseEvent) => {
if (!isDragging) {
return;
}
if (!previewElement) {
if (!isDragging || !previewElement) {
return;
}
console.log(
'onMouseMove',
{ isDragging, previewElement },
dragDropManager.previewElement,
field ? field.address.segments : null,
);
// console.log({previewElement})
const offset = {
left: event.clientX - dragOffset.left,
top: event.clientY - dragOffset.top,
left: event.pageX - mousePostionWithinPreview.left,
top: event.pageY - mousePostionWithinPreview.top,
};
setPreviewOffset(offset);
previewElement.style.transform = `translate(${offset.left}px, ${offset.top}px)`;
if (type) {
@ -143,7 +207,7 @@ export function useDrag(options?: any) {
const dropIds = [];
while (dropElement) {
if (!dropElement.getAttribute) {
dropElement = dropElement.parentNode as Element;
dropElement = dropElement.parentNode as HTMLElement;
continue;
}
const dropId = dropElement.getAttribute('data-drop-id');
@ -158,7 +222,7 @@ export function useDrag(options?: any) {
// @ts-ignore
// event.data = dropContext.data;
}
dropElement = dropElement.parentNode as Element;
dropElement = dropElement.parentNode as HTMLElement;
}
dragDropManager.drag = { type, dropIds };
}
@ -166,15 +230,16 @@ export function useDrag(options?: any) {
onDrag && onDrag(event);
});
return { isDragging, previewOffset, dragOffset, dragRef, previewRef };
return { isDragging, dragRef, previewRef };
}
export function useDrop(options) {
const { accept, data, shallow } = options;
const { accept, data, shallow, onDrop, onHover, canDrop = true } = options;
const dropRef = useRef<HTMLDivElement>();
const { onMouseEnter, onMouseLeave, onMouseMove, onMouseUp } =
useMouseEvents(dropRef);
const [isOver, setIsOver] = useState(false);
const [onTopHalf, setOnTopHalf] = useState(null);
const [dropId] = useState<string>(`d${Math.random()}`);
const dragDropManager = useContext(DragDropManagerContext);
@ -188,14 +253,20 @@ export function useDrop(options) {
}, [accept, data, shallow]);
onMouseEnter((event) => {
console.log({ dragDropManager });
if (!canDrop) {
return;
}
// console.log({ dragDropManager });
if (!dragDropManager.drag || dragDropManager.drag.type !== accept) {
return;
}
setIsOver(true);
});
onMouseMove(() => {
onMouseMove((event: React.MouseEvent) => {
if (!canDrop) {
return;
}
if (!dragDropManager.drag || dragDropManager.drag.type !== accept) {
return;
}
@ -203,21 +274,53 @@ export function useDrop(options) {
dragDropManager.drag.dropIds &&
dragDropManager.drag.dropIds.includes(dropId)
) {
const top = event.clientY - dropRef.current.getBoundingClientRect().top;
const onTop = top < dropRef.current.clientHeight / 2;
setOnTopHalf(onTop);
// @ts-ignore
event.onTopHalf = onTop;
setIsOver(true);
onHover && onHover(event);
} else {
setIsOver(false);
}
});
onMouseUp((event) => {
onMouseUp((event: React.MouseEvent) => {
if (!canDrop) {
return;
}
if (event.button !== 0) {
return;
}
if (isOver) {
const top = event.clientY - dropRef.current.getBoundingClientRect().top;
const onTop = top < dropRef.current.clientHeight / 2;
setOnTopHalf(onTop);
// @ts-ignore
event.onTopHalf = onTop;
// @ts-ignore
event.data = data;
// @ts-ignore
event.dragItem = dragDropManager.drop.item;
// @ts-ignore
event.dropElement = dropRef.current;
onDrop && onDrop(event);
dragDropManager.onDrop && dragDropManager.onDrop(event);
dragDropManager.drop = null;
}
setIsOver(false);
});
onMouseLeave(() => {
if (!canDrop) {
return;
}
setIsOver(false);
});
return {
onTopHalf: isOver ? onTopHalf : null,
isOver,
dropRef,
};

View File

@ -1,150 +0,0 @@
import React, { useContext } from 'react';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import classNames from 'classnames';
import { useSchema } from '../../fields';
import { AcceptContext } from './Grid';
import { useField } from '@formily/react';
export function DropFirstRow() {
const field = useField();
const { schema } = useSchema();
const accept = useContext(AcceptContext);
const [{ canDrop, isOver }, drop] = useDrop(
() => ({
accept,
drop: () => ({
gridType: 'first-row',
schema,
segments: field.address.segments,
}),
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
}),
[schema],
);
const active = canDrop && isOver;
return (
<div
className={classNames('drop-row', 'first', { active })}
ref={drop}
></div>
);
}
export function DropRow() {
const { schema } = useSchema();
const field = useField();
const accept = useContext(AcceptContext);
const [{ canDrop, isOver }, drop] = useDrop(
() => ({
accept,
drop: () => ({
gridType: 'row',
schema,
segments: field.address.segments,
}),
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
}),
[schema],
);
const active = canDrop && isOver;
return <div className={classNames('drop-row', { active })} ref={drop}></div>;
}
export function DropColumn() {
const field = useField();
const { schema } = useSchema();
const accept = useContext(AcceptContext);
const [{ canDrop, isOver }, drop] = useDrop(
() => ({
accept,
drop: () => ({
gridType: 'column',
schema,
segments: field.address.segments,
}),
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
}),
[schema],
);
const active = canDrop && isOver;
return (
<div className={classNames('drop-column', { active })} ref={drop}></div>
);
}
export function DropLastColumn() {
const field = useField();
const { schema } = useSchema();
const accept = useContext(AcceptContext);
const [{ canDrop, isOver }, drop] = useDrop(
() => ({
accept,
drop: () => ({
gridType: 'last-column',
schema,
segments: field.address.segments,
}),
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
}),
[schema],
);
const active = canDrop && isOver;
return (
<div
className={classNames('drop-column', 'last', { active })}
ref={drop}
></div>
);
}
export function DropBlock({ canDrop }) {
const { schema } = useSchema();
const accept = useContext(AcceptContext);
const [{ isOver }, drop] = useDrop(
() => ({
accept,
drop: () => ({ schema }),
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
canDrop: () => canDrop,
}),
[canDrop],
);
console.log({ canDrop });
const active = canDrop && isOver;
return (
<div className={classNames('drop-block', { active })} ref={drop}></div>
);
}
export default {
DropRow,
DropColumn,
DropLastColumn,
DropBlock,
};

View File

@ -1,319 +1,88 @@
import React, { createContext, useContext, useState } from 'react';
import { Row, Col, Dropdown, Menu } from 'antd';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import {
DropFirstRow,
DropRow,
DropColumn,
DropBlock,
DropLastColumn,
} from './Drop';
import {
CloseOutlined,
DeleteOutlined,
MenuOutlined,
PlusOutlined,
ArrowDownOutlined,
ArrowUpOutlined,
} from '@ant-design/icons';
import {
getFullPaths,
SchemaDesignerContext,
useSchemaQuery,
useSchema,
} from '../../fields';
import { TouchBackend } from 'react-dnd-touch-backend';
import { usePreview } from 'react-dnd-preview';
import classNames from 'classnames';
import './style.less';
import { useField, useFieldSchema } from '@formily/react';
import React, { cloneElement, useRef } from 'react';
import { Col } from './Col';
import { Row } from './Row';
import { Block } from './Block';
import { DragDropProvider } from './DND';
import { useSchemaQuery } from './';
const Preview = () => {
const accept = useContext(AcceptContext);
const { item, style, display, itemType } = usePreview();
if (itemType !== accept) {
return null;
}
if (!display) {
return null;
}
if (!item.ref) {
return null;
}
if (!item.ref.current) {
return null;
}
const el = item.ref.current as HTMLDivElement;
console.log({ itemType });
type Event = React.MouseEvent & {
dropElement: HTMLElement;
onTopHalf?: boolean;
dragItem?: any;
};
export const Grid = (props) => {
const { children, onDrop } = props;
const ref = useRef();
const { schema, moveTo, refresh } = useSchemaQuery();
return (
<div
style={{
...style,
height: el.clientHeight,
width: el.clientWidth,
zIndex: 9999,
opacity: 0.8,
// left: `-${el.clientWidth}px`,
}}
>
<div
style={{
transform: 'translate(-90%, -5%)',
}}
dangerouslySetInnerHTML={{ __html: el.outerHTML }}
/>
</div>
);
};
export interface GridPorps {
children?: React.ReactNode;
}
export interface GridRowPorps {
children?: React.ReactNode;
rowOrder?: number;
}
export interface GridColumnPorps {
children?: React.ReactNode;
span?: any;
}
export interface GridBlockProps {
children?: React.ReactNode;
lastComponentType?: string;
}
export type GridComponent = React.FC<GridPorps> & {
Row?: React.FC<GridRowPorps>;
Column?: React.FC<GridColumnPorps>;
Block?: React.FC<GridBlockProps>;
};
export const AcceptContext = createContext(null);
export const Grid: GridComponent = (props) => {
const { children } = props;
const schema = useFieldSchema();
const field = useField();
console.log({ accept: schema.name, schema: schema.toJSON() });
return (
<div className={'grid'} style={{ marginTop: 24 }}>
<AcceptContext.Provider value={schema.name}>
<DndProvider
backend={TouchBackend}
options={{ enableMouseEvents: true }}
>
{children}
<Preview />
</DndProvider>
</AcceptContext.Provider>
</div>
);
};
Grid.Row = (props) => {
const { children, rowOrder } = props;
const { schema } = useSchema();
const field = useField();
const accept = useContext(AcceptContext);
console.log({ accept });
const [{ canDrop, isOverCurrent }, drop] = useDrop(() => ({
accept,
drop: (item, monitor) => {
const didDrop = monitor.didDrop();
if (didDrop) {
return;
}
return { gridType: 'row', schema, segments: field.address.segments };
},
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
isOverCurrent: monitor.isOver({ shallow: true }),
}),
}));
const active = canDrop && isOverCurrent;
return (
<div
ref={drop}
className={classNames('grid-row', `grid-row-order-${rowOrder}`, {
active,
})}
>
{rowOrder === 0 && <DropFirstRow />}
<Row gutter={24}>{children}</Row>
<DropLastColumn />
{/* <DropRow /> */}
</div>
);
};
Grid.Column = (props) => {
const { children, span } = props;
return (
<Col span={span}>
<DropColumn />
{children}
</Col>
);
};
interface DropResult {
[key: string]: any;
}
Grid.Block = (props) => {
const { children, lastComponentType } = props;
const { schema, refresh } = useSchema();
const fieldSchema = useFieldSchema();
const field = useField();
const accept = useContext(AcceptContext);
const context = useContext(SchemaDesignerContext);
const {
insertAfter,
insertAfterWithAddRow,
insertBeforeWithAddRow,
insertBeforeWithAddColumn,
appendToRowWithAddColumn,
} = useSchemaQuery();
const ref = React.useRef();
console.log({ accept });
const [{ opacity, isDragging }, drag, preview] = useDrag(
() => ({
type: accept,
item: {
ref,
preview,
schema,
},
collect: (monitor) => ({
isDragging: monitor.isDragging(),
opacity: monitor.isDragging() ? 0.9 : 1,
}),
end: (item, monitor) => {
const dropResult = monitor.getDropResult<DropResult>();
if (item && dropResult) {
if (dropResult.gridType === 'block') {
insertAfter(field.address.segments, dropResult.segments);
} else if (dropResult.gridType === 'row') {
insertAfterWithAddRow(field.address.segments, dropResult.segments);
} else if (dropResult.gridType === 'column') {
insertBeforeWithAddColumn(
field.address.segments,
dropResult.segments,
);
} else if (dropResult.gridType === 'last-column') {
appendToRowWithAddColumn(
field.address.segments,
dropResult.segments,
);
} else if (dropResult.gridType === 'first-row') {
insertBeforeWithAddRow(
field.address.segments,
dropResult.segments,
<div className={'grid'}>
<DragDropProvider
gridRef={ref}
onDrop={(event: Event) => {
const el = event.dropElement;
const type = el.getAttribute('data-type');
const getIndex = (el) => {
const type = el.getAttribute('data-type');
return Array.prototype.indexOf.call(
el.parentNode.querySelectorAll(`.${type}`),
el,
);
};
let position: any = { type };
if (type === 'row') {
// position.rowIndex = getIndex(el);
position = {
type: 'row-divider',
rowDividerIndex: getIndex(
event.onTopHalf ? el.previousSibling : el.nextSibling,
),
};
}
refresh();
}
},
}),
[field, schema],
);
const segments = field.address.segments;
const [{ canDrop, isOver }, drop] = useDrop(
() => ({
accept,
drop: () => {
console.log('source.segments', segments);
return { gridType: 'block', segments, schema };
},
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
// hover: (item, monitor) => {
// console.log(monitor.getSourceClientOffset());
// },
canDrop: () => !isDragging,
}),
[isDragging, schema],
);
const active = canDrop && isOver;
drop(ref);
console.log({ isDragging });
return (
<div
ref={ref}
style={{ opacity }}
className={classNames(`grid-block`, `grid-block--${lastComponentType}`, {
active,
})}
>
<ActionBar dragRef={drag} />
{children}
{/* <DropBlock canDrop={!isDragging} /> */}
</div>
);
};
const ActionBar = ({ dragRef }) => {
const { addBlock, removeBlock } = useSchemaQuery();
const [active, setActive] = useState(false);
return (
<div className={classNames('action-bar', { active })}>
<Dropdown
overlayStyle={{ minWidth: 200 }}
trigger={['click']}
visible={active}
onVisibleChange={setActive}
overlay={
<Menu>
<Menu.Item
onClick={() => {
addBlock({}, true);
setActive(false);
}}
icon={<ArrowUpOutlined />}
>
</Menu.Item>
<Menu.Item
onClick={() => {
addBlock({});
setActive(false);
}}
icon={<ArrowDownOutlined />}
>
</Menu.Item>
<Menu.Divider />
<Menu.Item
onClick={() => {
removeBlock();
setActive(false);
}}
icon={<DeleteOutlined />}
>
</Menu.Item>
</Menu>
}
if (type === 'row-divider') {
position.rowDividerIndex = getIndex(el);
}
if (type === 'col-divider') {
position.colDividerIndex = getIndex(el);
position.rowIndex = getIndex(el.parentNode);
}
if (type === 'block') {
const rowNode = el.parentNode.parentNode;
position.blockIndex = getIndex(el);
position.colIndex = getIndex(el.parentNode);
position.rowIndex = getIndex(rowNode);
const colsize = rowNode.querySelectorAll('.col').length;
if (colsize === 1) {
position = {
type: 'row-divider',
rowDividerIndex: getIndex(
event.onTopHalf
? rowNode.previousSibling
: rowNode.nextSibling,
),
};
} else {
position.type = 'block-divider';
position.blockDividerIndex = getIndex(el);
if (!event.onTopHalf) {
position.blockDividerIndex += 1
}
}
}
onDrop && onDrop(event);
moveTo(event.dragItem.path, position);
console.log('onDrop', position, event.dragItem);
}}
>
<MenuOutlined className={'draggable'} ref={dragRef} />
</Dropdown>
<Row.Divider style={{ marginTop: -24 }} />
{children}
</DragDropProvider>
</div>
);
};
Grid.Row = Row;
Grid.Col = Col;
Grid.Block = Block;
export default Grid;

View File

@ -1,21 +1,46 @@
import React from 'react';
import React, { cloneElement, useRef } from 'react';
import classNames from 'classnames';
import { Col } from './Col';
import { DragDropProvider, useDrop } from './DND';
import { useField } from '@formily/react';
export const Row = (props) => {
const { children, onColResize } = props;
const len = children.length;
const { children, onColResize, position = {}, isLast } = props;
const { isOver, onTopHalf, dropRef } = useDrop({
accept: 'grid',
data: {},
shallow: true,
});
return (
<div style={{ display: 'flex' }}>
{children.map((child, index) => {
return (
<>
{child}
{len > index + 1 && <Col.Divider onDragEnd={onColResize} />}
</>
);
})}
</div>
<>
<div
data-type={'row'}
ref={dropRef}
className={classNames('row', { hover: isOver, 'top-half': onTopHalf })}
style={{ margin: '0 -24px', display: 'flex' }}
>
<Col.Divider resizable={false} />
{children}
</div>
<Row.Divider />
</>
);
}
};
Row.Divider = (props) => {
const { style = {}, position } = props;
const { isOver, dropRef } = useDrop({
accept: 'grid',
data: { position },
});
return (
<div
data-type={'row-divider'}
ref={dropRef}
className={classNames('row-divider', { hover: isOver })}
style={{ ...style, height: '24px' }}
></div>
);
};
export default Row;

View File

@ -1,133 +0,0 @@
import React, { useMemo } from 'react';
import { FormProvider, FormConsumer, useField, useFieldSchema } from '@formily/react';
import { createForm } from '@formily/core';
import {
SchemaFieldWithDesigner,
registerFieldComponents,
useSchema,
} from '../../../fields';
import { grid, row, column, block } from '../utils';
import { blocks2properties } from '../utils';
function Designer(props) {
const form = useMemo(() => createForm({}), []);
const { schema } = props;
return (
<div>
<FormProvider form={form}>
<SchemaFieldWithDesigner schema={schema} />
{/* <FormConsumer>
{(form) => {
return <div>{JSON.stringify(form.values, null, 2)}</div>;
}}
</FormConsumer> */}
</FormProvider>
</div>
);
}
function Hello(props) {
const schema = useFieldSchema();
return (
<div style={{ marginBottom: 24, padding: '1rem', background: '#f9f9f9', minHeight: 50, lineHeight: '50px' }}>
Hello {schema.title}
</div>
);
}
registerFieldComponents({ Hello });
const blocks = [
{
type: 'string',
title: `Block 1`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 1,
columnOrder: 1,
blockOrder: 1,
},
{
type: 'string',
title: `Block 2`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 1,
columnOrder: 2,
blockOrder: 1,
},
{
type: 'string',
title: `Block 3`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 2,
columnOrder: 1,
blockOrder: 1,
},
{
type: 'string',
title: `Block 4`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 1,
columnOrder: 1,
blockOrder: 2,
},
{
type: 'string',
title: `Block 5`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 3,
columnOrder: 1,
blockOrder: 1,
},
{
type: 'string',
title: `Block 6`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 3,
columnOrder: 2,
blockOrder: 1,
},
];
const schema = blocks2properties(blocks);
export default () => {
console.log({schema});
return (
<div>
<Designer
schema={{
type: 'object',
properties: {
layout: {
type: 'void',
'x-component': 'FormLayout',
'x-component-props': {
layout: 'vertical',
},
properties: {
[schema.name]: schema,
},
},
},
}}
/>
{/* <pre>{JSON.stringify(schema, null, 2)}</pre> */}
</div>
);
};

View File

@ -1,200 +0,0 @@
import React, { useMemo } from 'react';
import { FormProvider, FormConsumer, useField, useFieldSchema } from '@formily/react';
import { createForm } from '@formily/core';
import {
SchemaFieldWithDesigner,
registerFieldComponents,
} from '../../../fields';
import { grid, row, column, block } from '../utils';
import { blocks2properties } from '../utils';
import { Card } from 'antd';
function Designer(props) {
const form = useMemo(() => createForm({}), []);
const { schema } = props;
return (
<div>
<FormProvider form={form}>
<SchemaFieldWithDesigner schema={schema} />
{/* <FormConsumer>
{(form) => {
return <div>{JSON.stringify(form.values, null, 2)}</div>;
}}
</FormConsumer> */}
</FormProvider>
</div>
);
}
function Hello(props) {
const schema = useFieldSchema();
return (
<div style={{ marginBottom: 24, padding: '1rem', background: '#f9f9f9', minHeight: 50, lineHeight: '50px' }}>
Hello {schema.title}
</div>
);
}
registerFieldComponents({ Hello, Designer, Card });
const nested = blocks2properties([
{
type: 'string',
title: `Nested Block 1`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 1,
columnOrder: 1,
blockOrder: 1,
},
{
type: 'string',
title: `Nested Block 2`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 1,
columnOrder: 2,
blockOrder: 1,
},
{
type: 'string',
title: `Nested Block 3`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 2,
columnOrder: 1,
blockOrder: 1,
},
]);
const blocks = [
{
type: 'string',
title: `Block 1`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 1,
columnOrder: 1,
blockOrder: 1,
},
{
type: 'string',
title: `Block 2`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 1,
columnOrder: 2,
blockOrder: 1,
},
{
type: 'string',
title: `Block 3`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 2,
columnOrder: 1,
blockOrder: 1,
},
{
type: 'string',
title: `Block 4`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 1,
columnOrder: 1,
blockOrder: 2,
},
{
type: 'string',
title: `Block 5`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 3,
columnOrder: 1,
blockOrder: 1,
},
{
type: 'string',
title: `Block 6`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 3,
columnOrder: 2,
blockOrder: 1,
},
{
type: 'string',
title: `Block 7`,
required: true,
'x-read-pretty': false,
'x-decorator': 'Card',
'x-decorator-props': {
title: '内嵌区块(只能在当前区域内部拖拽)',
},
'x-component': 'Designer',
'x-component-props': {
schema: {
type: 'object',
properties: {
layout: {
type: 'void',
'x-component': 'FormLayout',
'x-component-props': {
layout: 'vertical',
},
properties: {
[nested.name]: nested,
},
},
},
},
},
rowOrder: 4,
columnOrder: 1,
blockOrder: 1,
},
];
const schema = blocks2properties(blocks);
export default () => {
console.log({schema});
return (
<div>
<Designer
schema={{
type: 'object',
properties: {
layout: {
type: 'void',
'x-component': 'FormLayout',
'x-component-props': {
layout: 'vertical',
},
properties: {
[schema.name]: schema,
},
},
},
}}
/>
{/* <pre>{JSON.stringify(schema, null, 2)}</pre> */}
</div>
);
};

View File

@ -1,12 +1,12 @@
import React, { createContext, useContext, useEffect, useRef } from 'react';
import { useDrag, useDrop, DragDropProvider } from '../';
import { useDrag, useDrop, DragDropProvider, mergeRefs } from '../';
import { Button, Space } from 'antd';
function DropZone({ options, children }) {
const { isOver, dropRef } = useDrop(options);
return (
<div
ref={dropRef}
ref={dropRef as any}
style={{
textAlign: 'center',
lineHeight: '100px',
@ -32,23 +32,7 @@ function Dragable() {
// 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;
}
});
};
return <Button ref={mergeRefs<any>([dragRef, previewRef])}>1</Button>;
}
function Dragable2() {
@ -64,17 +48,15 @@ function Dragable2() {
// console.log('onDrag');
},
});
return (
<Button ref={mergeRefs<any>([dragRef, previewRef])}>2</Button>
);
return <Button ref={mergeRefs<any>([dragRef, previewRef])}>2</Button>;
}
export default () => {
return (
<DragDropProvider>
<Space style={{marginBottom: 12}}>
<Dragable />
<Dragable2 />
<Space style={{ marginBottom: 12 }}>
<Dragable />
<Dragable2 />
</Space>
<DropZone
options={{

View File

@ -1,6 +1,26 @@
.col-divider {
position: relative;
&:hover {
&:last-child {
&.resizable:hover {
cursor: auto;
&::after {
display: none;
}
}
&.resizable.hover {
&::after {
display: block !important;
}
}
}
&.resizable {
cursor: col-resize;
}
&.hover {
cursor: grab !important;
}
&.resizable:hover,
&.hover {
&::after {
content: '';
display: block;
@ -14,3 +34,80 @@
}
}
}
.row-divider {
position: relative;
&.hover {
&::after {
content: '';
display: block;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
height: 12px;
width: 100%;
background: #e6f7ff;
}
}
&:last-child {
margin-bottom: -24px;
}
}
.row {
position: relative;
&.hover {
&::after {
content: '';
display: block;
position: absolute;
left: 24px;
bottom: -18px;
height: 12px;
right: 24px;
background: #e6f7ff;
}
&.top-half {
&::after {
top: -18px;
bottom: auto;
}
}
}
}
.block {
position: relative;
margin-bottom: 24px;
&:last-child {
margin-bottom: 0;
}
&.hover {
&::after {
content: '';
display: block;
position: absolute;
left: 0;
bottom: -18px;
height: 12px;
width: 100%;
background: #e6f7ff;
}
&.top-half {
&::after {
top: -18px;
bottom: auto;
}
}
}
> .action-bar {
position: absolute;
top: 5px;
right: 8px;
}
}
.draggable {
cursor: grab !important;
}

View File

@ -0,0 +1,208 @@
import { Grid, Row, Col, Block, SchemaFieldWithDesigner } from '../';
import './demo5.less';
import { Card } from 'antd';
import classNames from 'classnames';
import React, { useMemo } from 'react';
import {
FormProvider,
FormConsumer,
useField,
useFieldSchema,
ISchema,
Schema,
} from '@formily/react';
import { createForm } from '@formily/core';
function Designer(props: { schema?: ISchema }) {
const form = useMemo(() => createForm({}), []);
const { schema } = props;
return (
<div>
<FormProvider form={form}>
<SchemaFieldWithDesigner schema={schema} />
{/* <FormConsumer>
{(form) => {
return <div>{JSON.stringify(form.values, null, 2)}</div>;
}}
</FormConsumer> */}
</FormProvider>
</div>
);
}
const schema = new Schema({
type: 'object',
properties: {
grid: {
type: 'void',
title: 'aa',
'x-component': 'Grid',
properties: {
row1: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
col1: {
type: 'void',
'x-component': 'Grid.Col',
'x-component-props': {
size: 1 / 2,
},
properties: {
block11: {
type: 'void',
'x-component': 'Grid.Block',
'x-component-props': {
title: 'block11',
},
},
},
},
col2: {
type: 'void',
'x-component': 'Grid.Col',
'x-component-props': {
size: 1 / 2,
isLast: true,
},
properties: {
block21: {
type: 'void',
'x-component': 'Grid.Block',
'x-component-props': {
title: 'block21',
},
},
},
},
},
},
row2: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
col21: {
type: 'void',
'x-component': 'Grid.Col',
'x-component-props': {
size: 1 / 3,
},
properties: {
block211: {
type: 'void',
'x-component': 'Grid.Block',
'x-component-props': {
title: 'block211',
},
},
},
},
col22: {
type: 'void',
'x-component': 'Grid.Col',
'x-component-props': {
size: 2 / 3,
isLast: true,
},
properties: {
block221: {
type: 'void',
'x-component': 'Grid.Block',
'x-component-props': {
title: 'block221',
},
},
},
},
},
},
row3: {
type: 'void',
'x-component': 'Grid.Row',
"x-component-props": {
isLast: true,
},
properties: {
col31: {
type: 'void',
'x-component': 'Grid.Col',
'x-component-props': {
size: 1,
isLast: true,
},
properties: {
block311: {
type: 'void',
'x-component': 'Grid.Block',
'x-component-props': {
title: 'block311',
},
},
},
},
},
},
},
},
},
})
export default () => {
return (
<Card>
<Designer
schema={schema}
/>
{/* <Grid
onDrop={(e) => {
console.log('onDrop', e, e.data);
}}
>
<Row
onColResize={(e) => {
console.log(e.data);
}}
>
{[1, 2, 3].map((index) => (
<Col size={1 / 3}>
<Block>col {index}</Block>
</Col>
))}
</Row>
<Row
onColResize={(e) => {
console.log(e.data);
}}
>
{[4, 5, 6].map((index) => (
<Col size={1 / 3}>
<Block>col {index}</Block>
</Col>
))}
</Row>
<Row
onColResize={(e) => {
console.log(e.data);
}}
>
{[7, 8].map((index) => (
<Col size={1 / 3}>
<Block>col {index}</Block>
</Col>
))}
</Row>
<Row
onColResize={(e) => {
console.log(e.data);
}}
>
{[9].map((index) => (
<Col size={1}>
<Block>col {index}</Block>
</Col>
))}
</Row>
</Grid> */}
</Card>
);
};

View File

@ -14,14 +14,14 @@ group:
基于行Row和列Col来定义区块Block的外部框架。
## 代码演示
<!--
### 基本用法
<code src="./demos/demo2.tsx"/>
### 内嵌区块
<code src="./demos/demo3.tsx"/>
<code src="./demos/demo3.tsx"/> -->
### useDrag & useDrop
@ -31,6 +31,10 @@ group:
<code src="./demos/demo5.tsx"/>
### Grid
<code src="./demos/demo6.tsx"/>
## API 说明
### Grid
@ -67,7 +71,11 @@ interface BlockOptions {
原始 schema 需要至少 grid->row->col->block->custom 五层嵌套,写起来非常繁琐,`blocks2properties` 方法可以简化配置。
### useDrag
### useDrag & useDrop
拖拽 hooks
原生态的
### useDrop

View File

@ -1,3 +1,271 @@
import React, { useContext, createContext, useState } from 'react';
import {
Schema,
ISchema,
useFieldSchema,
useForm,
useField,
} from '@formily/react';
import { uid } from '@formily/shared';
import { SchemaField } from '../../fields';
import set from 'lodash/set';
export const SchemaDesignerContext = createContext<Schema>(new Schema({}));
export const SchemaRefreshContext = createContext(null);
export function SchemaFieldWithDesigner(props: { schema?: ISchema }) {
function Container(props) {
const { schema } = props;
const [, refresh] = useState(0);
return (
<SchemaRefreshContext.Provider
value={() => {
refresh(Math.random());
}}
>
<SchemaDesignerContext.Provider value={schema}>
<SchemaField schema={schema} />
</SchemaDesignerContext.Provider>
<pre>{JSON.stringify(schema.toJSON(), null, 2)}</pre>
</SchemaRefreshContext.Provider>
);
}
return <Container schema={new Schema(props.schema)} />;
}
export function removeProperty(property: Schema) {
property.parent.removeProperty(property.name);
}
export function addPropertyBefore(target, prop) {
Object.keys(target.parent.properties).forEach((name) => {
if (name === target.name) {
target.parent.addProperty(prop.name, prop);
}
const property = target.parent.properties[name];
property.parent.removeProperty(property.name);
target.parent.addProperty(property.name, property.toJSON());
});
}
export function addPropertyAfter(target, prop) {
Object.keys(target.parent.properties).forEach((name) => {
const property = target.parent.properties[name];
property.parent.removeProperty(property.name);
target.parent.addProperty(property.name, property.toJSON());
if (name === target.name) {
target.parent.addProperty(prop.name, prop);
}
});
}
export const getSchemaAddressSegments = (schema: Schema) => {
if (!schema) {
return [];
}
const segments = [schema.name];
if (schema.parent && schema.parent.name) {
segments.unshift(...getSchemaAddressSegments(schema.parent));
}
return segments;
};
export function useSchemaQuery() {
const context = useContext(SchemaDesignerContext);
const refresh = useContext(SchemaRefreshContext);
const fieldSchema = useFieldSchema();
const field = useField();
const form = useForm();
const getSchemaByPath = (path) => {
let s: Schema = context;
const names = [...path];
while (names.length) {
s = s.properties[names.shift()];
}
return s;
};
const schema = getSchemaByPath(field.address.segments);
const getPropertyByPosition = (position) => {
if (position.type === 'row-divider') {
const names = Object.keys(schema.properties);
const isOver = position.rowDividerIndex > names.length - 1;
const index = isOver ? names.length - 1 : position.rowDividerIndex;
const name = names[index];
const property = schema.properties[name];
const addProperty = isOver ? addPropertyAfter : addPropertyBefore;
return (data) => {
return addProperty(property, {
type: 'void',
name: `r_${uid()}`,
'x-component': 'Grid.Row',
properties: {
[`c_${uid()}`]: {
type: 'void',
'x-component': 'Grid.Col',
'x-component-props': {
size: 1,
},
properties: {
[data.name]: data,
},
},
},
});
};
}
const rowNames = Object.keys(schema.properties);
const rowName = rowNames[position.rowIndex];
const row = schema.properties[rowName];
if (position.type === 'col-divider') {
const names = Object.keys(row.properties);
const isOver = position.colDividerIndex > names.length - 1;
const index = isOver ? names.length - 1 : position.colDividerIndex;
const name = names[index];
const property = row.properties[name];
const addProperty = isOver ? addPropertyAfter : addPropertyBefore;
const count = Object.keys(row.properties).length+1;
return (data) => {
const other = 1-(1/count);
Object.keys(row.properties).forEach(name => {
const prop = row.properties[name];
const segments = getSchemaAddressSegments(prop);
form.setFieldState(segments.join('.'), state => {
state.componentProps.size = other * state.componentProps.size;
console.log({state}, other * state.componentProps.size);
});
});
addProperty(property, {
type: 'void',
name: `c_${uid()}`,
'x-component': 'Grid.Col',
'x-component-props': {
size: 1/count,
},
properties: {
[data.name]: data,
},
});
};
}
const colNames = Object.keys(row.properties);
const colName = colNames[position.colIndex];
const col = row.properties[colName];
if (position.type === 'block-divider') {
const names = Object.keys(col.properties);
const isOver = position.blockDividerIndex > names.length - 1;
const index = isOver ? names.length - 1 : position.blockDividerIndex;
const name = names[index];
const property = col.properties[name];
const addProperty = isOver ? addPropertyAfter : addPropertyBefore;
return (data) => {
return addProperty(property, data);
};
}
};
return {
schema,
fieldSchema,
refresh,
removeBlock: () => {
if (Object.keys(schema.parent.parent.properties).length === 1) {
removeProperty(schema.parent.parent);
} else if (Object.keys(schema.parent.properties).length === 1) {
removeProperty(schema.parent);
const cols = [];
let allSize = 0;
Object.keys(schema.parent.parent.properties).forEach(name => {
const prop = schema.parent.parent.properties[name];
const segments = getSchemaAddressSegments(prop);
cols.push(segments);
form.setFieldState(segments.join('.'), state => {
allSize += state.componentProps.size;
});
return;
});
for (const segments of cols) {
form.setFieldState(segments.join('.'), state => {
state.componentProps.size = state.componentProps.size/allSize;
});
}
}
refresh();
},
addBlock: (data?: any, options?: any) => {
const { insertBefore = false } = options || {};
data = {
type: 'void',
name: `b_${uid()}`,
'x-component': 'Grid.Block',
};
const addProperty = insertBefore ? addPropertyBefore : addPropertyAfter;
if (Object.keys(schema.parent.parent.properties).length === 1) {
addProperty(schema.parent.parent, {
type: 'void',
name: `r_${uid()}`,
'x-component': 'Grid.Row',
properties: {
[`c_${uid()}`]: {
type: 'void',
'x-component': 'Grid.Col',
'x-component-props': {
size: 1,
},
properties: {
[data.name]: data,
},
},
},
})
} else {
addProperty(schema, data);
}
refresh();
},
moveTo: (path, position) => {
const source = getSchemaByPath(path);
const insert = getPropertyByPosition(position);
if (!insert) {
return;
}
// 只有一列时,删除当前行
if (Object.keys(source.parent.parent.properties).length === 1) {
source.parent.parent.parent.removeProperty(source.parent.parent.name);
}
// 某列只有一个区块时删除当前列
else if (Object.keys(source.parent.properties).length === 1) {
source.parent.parent.removeProperty(source.parent.name);
const cols = [];
let allSize = 0;
Object.keys(source.parent.parent.properties).forEach(name => {
const prop = source.parent.parent.properties[name];
const segments = getSchemaAddressSegments(prop);
cols.push(segments);
form.setFieldState(segments.join('.'), state => {
allSize += state.componentProps.size;
});
return;
});
for (const segments of cols) {
form.setFieldState(segments.join('.'), state => {
state.componentProps.size = state.componentProps.size/allSize;
});
}
} else {
source.parent.removeProperty(source.name);
}
insert(source.toJSON());
refresh();
},
};
}
export * from './DND';
export * from './Row';
export * from './Col';
export * from './DND';
export * from './Grid';
export * from './Block';

View File

@ -1,5 +1,267 @@
import set from 'lodash/set';
import React, { useContext, createContext, useState } from 'react';
import { Schema, useFieldSchema, useForm } from '@formily/react';
import { uid } from '@formily/shared';
import { SchemaField } from '../../fields';
import set from 'lodash/set';
export const SchemaDesignerContext = createContext<Schema>(new Schema({}));
export const SchemaRefreshContext = createContext(null);
export function SchemaFieldWithDesigner(props) {
function Container(props) {
const { schema } = props;
const [, refresh] = useState(0);
return (
<SchemaRefreshContext.Provider
value={() => {
refresh(Math.random());
}}
>
<SchemaDesignerContext.Provider value={schema}>
<SchemaField schema={schema} />
</SchemaDesignerContext.Provider>
</SchemaRefreshContext.Provider>
);
}
return <Container schema={new Schema(props.schema)} />;
}
export const getFullPaths = (schema: Schema) => {
if (!schema) {
return [];
}
const paths = [schema.name];
if (schema.parent && schema.parent.name) {
paths.unshift(...getFullPaths(schema.parent));
}
return paths;
};
export function useSchema() {
const context = useContext(SchemaDesignerContext);
const refresh = useContext(SchemaRefreshContext);
const fieldSchema = useFieldSchema();
const paths = getFullPaths(fieldSchema);
let schema: Schema = context;
const names = [...paths];
while (names.length) {
schema = schema.properties[names.shift()];
}
return { schema, fieldSchema, refresh };
}
export function getSchema(context) {
return (paths) => {
const fullPaths = Array.isArray(paths) ? paths : getFullPaths(paths);
let s: Schema = context;
const names = [...fullPaths];
while (names.length) {
s = s.properties[names.shift()];
}
return s;
};
}
export function addPropertyBefore(target, prop) {
Object.keys(target.parent.properties).forEach((name) => {
if (name === target.name) {
target.parent.addProperty(prop.name, prop);
}
const property = target.parent.properties[name];
property.parent.removeProperty(property.name);
target.parent.addProperty(property.name, property.toJSON());
});
}
export function addPropertyAfter(target, prop) {
Object.keys(target.parent.properties).forEach((name) => {
const property = target.parent.properties[name];
property.parent.removeProperty(property.name);
target.parent.addProperty(property.name, property.toJSON());
if (name === target.name) {
target.parent.addProperty(prop.name, prop);
}
});
}
export function useSchemaQuery() {
const context = useContext(SchemaDesignerContext);
const form = useForm();
const { schema, refresh } = useSchema();
return {
resizeColumn() {
function getPrevProperty() {
const names = Object.keys(schema.parent.properties);
const index = names.indexOf(schema.name as string);
return schema.parent.properties[names[index-1]];
}
console.log('onMouseDown', schema.name);
console.log('onMouseDown', schema.parent.properties);
console.log('onMouseDown', getFullPaths(getPrevProperty()));
form.setFieldState(getFullPaths(getPrevProperty()).join('.'), (state) => {
const span = state.componentProps.span;
let width = state.componentProps.width||50;
width -= 1;
state.componentProps = {
span: 1*span - 1,
width,
style: {
flex: `0 0 ${width}%`,
maxWidth: `${width}%`,
},
};
});
console.log('onMouseDown', getFullPaths(schema));
form.setFieldState(getFullPaths(schema).join('.'), (state) => {
const span = state.componentProps.span;
console.log({span})
let width = state.componentProps.width||50;
width += 1;
state.componentProps = {
span: 1*span + 1,
style: {
flex: `0 0 ${width}%`,
maxWidth: `${width}%`,
},
};
});
refresh();
},
removeBlock() {
schema.parent.removeProperty(schema.name);
refresh();
},
addBlock: (data, up = false) => {
const blockSchema = block({
type: 'string',
title: `Block ${uid()}`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 2,
columnOrder: 1,
blockOrder: 1,
});
if ('Grid.Column' === schema.parent['x-component']) {
if (Object.keys(schema.parent.parent.properties).length > 1) {
up
? addPropertyBefore(schema, blockSchema)
: addPropertyAfter(schema, blockSchema);
} else {
const rowSchema = row(column(blockSchema));
up
? addPropertyAfter(schema.parent.parent, rowSchema)
: addPropertyAfter(schema.parent.parent, rowSchema);
}
}
console.log('x-component', schema.parent['x-component']);
refresh();
// schema.parent['x-component']
},
insertAfter: (sourcePath, targetPath) => {
const source = getSchema(context)(sourcePath);
const target = getSchema(context)(targetPath);
if (!source || !target) {
return;
}
console.log({
sourcePath,
source,
target,
targetPath,
sourceParentproperties: source.parent.properties,
});
const names = [];
Object.keys(target.parent.properties).forEach((name) => {
// if (names.includes(source.name)) {
// return;
// }
// names.push(name);
const property = target.parent.properties[name];
property.parent.removeProperty(property.name);
target.parent.addProperty(property.name, property.toJSON());
if (name === target.name) {
// names.push(source.name);
source.parent.removeProperty(source.name);
console.log('source.parent.properties', source.parent.properties);
target.parent.addProperty(source.name, source.toJSON());
}
});
},
insertAfterWithAddRow: (sourcePath, targetPath) => {
const source = getSchema(context)(sourcePath);
const target = getSchema(context)(targetPath);
const rowSchema = row(column(source.toJSON()));
source.parent.removeProperty(source.name);
Object.keys(target.parent.properties).forEach((name) => {
const property = target.parent.properties[name];
property.parent.removeProperty(property.name);
target.parent.addProperty(property.name, property.toJSON());
if (name === target.name) {
target.parent.addProperty(rowSchema.name, rowSchema);
}
});
},
insertBeforeWithAddRow: (sourcePath, targetPath) => {
const source = getSchema(context)(sourcePath);
const target = getSchema(context)(targetPath);
const rowSchema = row(column(source.toJSON()));
source.parent.removeProperty(source.name);
Object.keys(target.parent.properties).forEach((name) => {
if (name === target.name) {
target.parent.addProperty(rowSchema.name, rowSchema);
}
const property = target.parent.properties[name];
property.parent.removeProperty(property.name);
target.parent.addProperty(property.name, property.toJSON());
});
},
appendToRowWithAddColumn: (sourcePath, targetPath) => {
const source = getSchema(context)(sourcePath);
const target = getSchema(context)(targetPath);
const colSchema = column(source.toJSON());
source.parent.removeProperty(source.name);
const len = Object.keys(target.properties).length + 1;
target.addProperty(colSchema.name, colSchema);
console.log('target.properties', target.properties);
Object.keys(target.properties).forEach((name) => {
const prop = target.properties[name];
form.setFieldState(getFullPaths(prop).join('.'), (state) => {
state.componentProps = {
span: 24 / len,
};
});
});
},
insertBeforeWithAddColumn: (sourcePath, targetPath) => {
const source = getSchema(context)(sourcePath);
const target = getSchema(context)(targetPath);
const colSchema = column(source.toJSON());
source.parent.removeProperty(source.name);
const len = Object.keys(target.parent.properties).length + 1;
console.log('x-component-props.span', 24 / len);
Object.keys(target.parent.properties).forEach((name) => {
if (name === target.name) {
target.parent.addProperty(colSchema.name, colSchema);
}
const property = target.parent.properties[name];
property.parent.removeProperty(property.name);
const json = property.toJSON();
target.parent.addProperty(property.name, json);
});
Object.keys(target.parent.properties).forEach((name) => {
const prop = target.parent.properties[name];
form.setFieldState(getFullPaths(prop).join('.'), (state) => {
state.componentProps = {
span: 24 / len,
};
});
});
},
};
}
export const grid = (...rows: any[]) => {
const rowProperties = {};
@ -59,6 +321,7 @@ export const block = (...fields: any[]) => {
const properties = {};
fields.forEach((item) => {
const name = item.name || `f_${uid()}`;
// item.title = `${item.title} ${name}`;
properties[name] = item;
});
const lastComponentType = fields[fields.length - 1]['x-component'];
@ -73,22 +336,3 @@ export const block = (...fields: any[]) => {
properties,
};
};
export function blocks2properties(blocks: any[]) {
const obj = {
rows: [],
};
blocks.forEach(block => {
const path = ['rows', block['rowOrder'], block['columnOrder'], block['blockOrder']];
console.log(path);
set(obj, path, block);
});
return grid(...obj.rows.filter(Boolean).map((cols) => {
return row(
...cols.filter(Boolean).map((items: any[]) => {
console.log({items: items.filter(Boolean)})
return column(...items.filter(Boolean).map(item => block(item)));
}),
);
}));
}

View File

@ -90,300 +90,6 @@ export const SchemaField = createSchemaField({
scope: fieldScope,
});
export const SchemaDesignerContext = createContext<Schema>(new Schema({}));
export const SchemaRefreshContext = createContext(null);
export function SchemaFieldWithDesigner(props) {
function Container(props) {
const { schema } = props;
const [, refresh] = useState(0);
return (
<SchemaRefreshContext.Provider
value={() => {
refresh(Math.random());
}}
>
<SchemaDesignerContext.Provider value={schema}>
<SchemaField schema={schema} />
</SchemaDesignerContext.Provider>
</SchemaRefreshContext.Provider>
);
}
return <Container schema={new Schema(props.schema)} />;
}
export const getFullPaths = (schema: Schema) => {
if (!schema) {
return [];
}
const paths = [schema.name];
if (schema.parent && schema.parent.name) {
paths.unshift(...getFullPaths(schema.parent));
}
return paths;
};
export function useSchema() {
const context = useContext(SchemaDesignerContext);
const refresh = useContext(SchemaRefreshContext);
const fieldSchema = useFieldSchema();
const paths = getFullPaths(fieldSchema);
let schema: Schema = context;
const names = [...paths];
while (names.length) {
schema = schema.properties[names.shift()];
}
return { schema, fieldSchema, refresh };
}
export function getSchema(context) {
return (paths) => {
const fullPaths = Array.isArray(paths) ? paths : getFullPaths(paths);
let s: Schema = context;
const names = [...fullPaths];
while (names.length) {
s = s.properties[names.shift()];
}
return s;
};
}
export function addPropertyBefore(target, prop) {
Object.keys(target.parent.properties).forEach((name) => {
if (name === target.name) {
target.parent.addProperty(prop.name, prop);
}
const property = target.parent.properties[name];
property.parent.removeProperty(property.name);
target.parent.addProperty(property.name, property.toJSON());
});
}
export function addPropertyAfter(target, prop) {
Object.keys(target.parent.properties).forEach((name) => {
const property = target.parent.properties[name];
property.parent.removeProperty(property.name);
target.parent.addProperty(property.name, property.toJSON());
if (name === target.name) {
target.parent.addProperty(prop.name, prop);
}
});
}
export function useSchemaQuery() {
const context = useContext(SchemaDesignerContext);
const form = useForm();
const { schema, refresh } = useSchema();
return {
removeBlock() {
schema.parent.removeProperty(schema.name);
refresh();
},
addBlock: (data, up = false) => {
const blockSchema = block({
type: 'string',
title: `Block ${uid()}`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 2,
columnOrder: 1,
blockOrder: 1,
});
if ('Grid.Column' === schema.parent['x-component']) {
if (Object.keys(schema.parent.parent.properties).length > 1) {
up
? addPropertyBefore(schema, blockSchema)
: addPropertyAfter(schema, blockSchema);
} else {
const rowSchema = row(column(blockSchema));
up
? addPropertyAfter(schema.parent.parent, rowSchema)
: addPropertyAfter(schema.parent.parent, rowSchema);
}
}
console.log('x-component', schema.parent['x-component']);
refresh();
// schema.parent['x-component']
},
insertAfter: (sourcePath, targetPath) => {
const source = getSchema(context)(sourcePath);
const target = getSchema(context)(targetPath);
if (!source || !target) {
return;
}
console.log({
sourcePath,
source,
target,
targetPath,
sourceParentproperties: source.parent.properties,
});
const names = [];
Object.keys(target.parent.properties).forEach((name) => {
// if (names.includes(source.name)) {
// return;
// }
// names.push(name);
const property = target.parent.properties[name];
property.parent.removeProperty(property.name);
target.parent.addProperty(property.name, property.toJSON());
if (name === target.name) {
// names.push(source.name);
source.parent.removeProperty(source.name);
console.log('source.parent.properties', source.parent.properties);
target.parent.addProperty(source.name, source.toJSON());
}
});
},
insertAfterWithAddRow: (sourcePath, targetPath) => {
const source = getSchema(context)(sourcePath);
const target = getSchema(context)(targetPath);
const rowSchema = row(column(source.toJSON()));
source.parent.removeProperty(source.name);
Object.keys(target.parent.properties).forEach((name) => {
const property = target.parent.properties[name];
property.parent.removeProperty(property.name);
target.parent.addProperty(property.name, property.toJSON());
if (name === target.name) {
target.parent.addProperty(rowSchema.name, rowSchema);
}
});
},
insertBeforeWithAddRow: (sourcePath, targetPath) => {
const source = getSchema(context)(sourcePath);
const target = getSchema(context)(targetPath);
const rowSchema = row(column(source.toJSON()));
source.parent.removeProperty(source.name);
Object.keys(target.parent.properties).forEach((name) => {
if (name === target.name) {
target.parent.addProperty(rowSchema.name, rowSchema);
}
const property = target.parent.properties[name];
property.parent.removeProperty(property.name);
target.parent.addProperty(property.name, property.toJSON());
});
},
appendToRowWithAddColumn: (sourcePath, targetPath) => {
const source = getSchema(context)(sourcePath);
const target = getSchema(context)(targetPath);
const colSchema = column(source.toJSON());
source.parent.removeProperty(source.name);
const len = Object.keys(target.properties).length + 1;
target.addProperty(colSchema.name, colSchema);
console.log('target.properties', target.properties);
Object.keys(target.properties).forEach((name) => {
const prop = target.properties[name];
form.setFieldState(getFullPaths(prop).join('.'), (state) => {
state.componentProps = {
span: 24 / len,
};
});
});
},
insertBeforeWithAddColumn: (sourcePath, targetPath) => {
const source = getSchema(context)(sourcePath);
const target = getSchema(context)(targetPath);
const colSchema = column(source.toJSON());
source.parent.removeProperty(source.name);
const len = Object.keys(target.parent.properties).length + 1;
console.log('x-component-props.span', 24 / len);
Object.keys(target.parent.properties).forEach((name) => {
if (name === target.name) {
target.parent.addProperty(colSchema.name, colSchema);
}
const property = target.parent.properties[name];
property.parent.removeProperty(property.name);
const json = property.toJSON();
target.parent.addProperty(property.name, json);
});
Object.keys(target.parent.properties).forEach((name) => {
const prop = target.parent.properties[name];
form.setFieldState(getFullPaths(prop).join('.'), (state) => {
state.componentProps = {
span: 24 / len,
};
});
});
},
};
}
export const grid = (...rows: any[]) => {
const rowProperties = {};
rows.forEach((row, index) => {
set(row, 'x-component-props.rowOrder', index);
rowProperties[row.name] = row;
});
const name = `g_${uid()}`;
return {
type: 'void',
name,
'x-component': 'Grid',
'x-component-props': {},
properties: rowProperties,
};
};
export const row = (...cols: any[]) => {
const rowName = `r_${uid()}`;
const colsProperties = {};
cols.forEach((col, index) => {
set(col, 'x-component-props.columnOrder', index);
set(col, 'x-component-props.span', 24 / cols.length);
colsProperties[col.name] = col;
});
return {
type: 'void',
name: rowName,
'x-component': 'Grid.Row',
'x-component-props': {},
properties: colsProperties,
};
};
export const column = (...blocks: any[]) => {
const colName = `c_${uid()}`;
const properties = {};
blocks.forEach((item) => {
properties[item.name] = item;
});
return {
name: colName,
type: 'void',
'x-component': 'Grid.Column',
'x-read-pretty': true,
'x-component-props': {
labelCol: 6,
wrapperCol: 10,
span: 24,
},
properties,
};
};
export const block = (...fields: any[]) => {
const blockName = `b_${uid()}`;
const properties = {};
fields.forEach((item) => {
const name = item.name || `f_${uid()}`;
// item.title = `${item.title} ${name}`;
properties[name] = item;
});
const lastComponentType = fields[fields.length - 1]['x-component'];
return {
name: blockName,
type: 'void',
'x-component': 'Grid.Block',
'x-component-props': {
lastComponentType,
},
'x-read-pretty': false,
properties,
};
};
export function parseEffects(effects: any, form?: any) {
if (!effects) {
return;