feat: improve draggable api

This commit is contained in:
chenos 2022-02-05 23:59:33 +08:00
parent 5b4dd1800e
commit de25f56a79
11 changed files with 309 additions and 139 deletions

View File

@ -1,12 +1,13 @@
import React from 'react';
import { useDesignable } from '../..';
import { SortableItem } from '../sortable-item';
export const BlockItem: React.FC<any> = (props) => {
const { DesignableBar } = useDesignable();
return (
<div className="nb-block-item">
<SortableItem>
{props.children}
<DesignableBar />
</div>
</SortableItem>
);
};

View File

@ -1,55 +1,15 @@
import React from 'react';
import { observer, useFieldSchema } from '@formily/react';
import { uid } from '@formily/shared';
import { observer, useField, useFieldSchema } from '@formily/react';
import { useDraggable, useDroppable } from '@dnd-kit/core';
import { SchemaComponent, SchemaComponentProvider, BlockItem, DndContext } from '@nocobase/client';
function Draggable(props) {
const { attributes, listeners, setNodeRef, transform } = useDraggable({
id: props.id,
data: props.data,
});
const style = transform
? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
}
: undefined;
return (
<button ref={setNodeRef} style={style} {...listeners} {...attributes}>
{props.children}
</button>
);
}
function Droppable(props) {
const { isOver, setNodeRef } = useDroppable({
id: props.id,
data: props.data,
});
const style = {
color: isOver ? 'green' : undefined,
};
return (
<div ref={setNodeRef} style={style}>
{props.children}
</div>
);
}
import { BlockItem, DndContext, DragHandler, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
import React from 'react';
const Block = observer((props) => {
const fieldSchema = useFieldSchema();
const field = useField();
return (
<Droppable id={field.address.toString()} data={{ schema: fieldSchema }}>
<div style={{ marginBottom: 20, padding: '0 20px', height: 50, lineHeight: '50px', background: '#f1f1f1' }}>
Block {fieldSchema.name}
<Draggable id={field.address.toString()} data={{ schema: fieldSchema }}>
Drag
</Draggable>
</div>
</Droppable>
<div style={{ marginBottom: 20, padding: '0 20px', height: 50, lineHeight: '50px', background: '#f1f1f1' }}>
Block {fieldSchema.name}
<DragHandler />
</div>
);
});

View File

@ -1 +1,2 @@
export * from './BlockItem';

View File

@ -10,6 +10,9 @@ const useDragEnd = () => {
console.log({ active, over });
const activeSchema = active?.data?.current?.schema;
const overSchema = over?.data?.current?.schema;
const insertAdjacent = over?.data?.current?.insertAdjacent;
const wrapSchema = over?.data?.current?.wrapSchema;
const onInsertAdjacent = over?.data?.current?.onInsertAdjacent;
if (!activeSchema || !overSchema) {
return;
@ -20,7 +23,25 @@ const useDragEnd = () => {
});
dn.on('afterInsertAdjacent', refresh);
dn.insertBeforeBeginOrAfterEnd(activeSchema);
dn.on('afterRemove', refresh);
if (activeSchema.parent === overSchema.parent) {
return dn.insertBeforeBeginOrAfterEnd(activeSchema);
}
if (insertAdjacent) {
console.log('removeIfChildrenEmpty', activeSchema)
dn.insertAdjacent(insertAdjacent, activeSchema, {
wrap: wrapSchema,
removeEmptyParents: true,
});
// onInsertAdjacent && onInsertAdjacent({
// dn,
// orginDraggedParentSchema,
// draggedSchema: activeSchema,
// });
return;
}
};
};

View File

@ -1,3 +1,4 @@
import { FormLayout } from '@formily/antd';
import { createForm, FormPath } from '@formily/core';
import {
FieldContext,
@ -72,7 +73,7 @@ const useDefaultValues = (props: any = {}, opts: any = {}) => {
};
export const Form: ComposedForm = observer((props) => {
const { useValues = useDefaultValues } = props;
const { request, initialValue, useValues = useDefaultValues, ...others } = props;
const decorator = useFormDecorator();
const fieldSchema = useFieldSchema();
const form = useMemo(() => createForm(), []);
@ -83,7 +84,13 @@ export const Form: ComposedForm = observer((props) => {
form.setValues(data?.data);
},
});
return <Spin spinning={loading}>{decorator ? <FormDecorator form={form} /> : <FormComponent form={form} />}</Spin>;
return (
<Spin spinning={loading}>
<FormLayout layout={'vertical'} {...others}>
{decorator ? <FormDecorator form={form} /> : <FormComponent form={form} />}
</FormLayout>
</Spin>
);
});
Form.__NOCOBASE_FORM = true;

View File

@ -1,115 +1,90 @@
import React from 'react';
import { uid } from '@formily/shared';
import { observer, useFieldSchema } from '@formily/react';
import { useDraggable, useDroppable } from '@dnd-kit/core';
import { SchemaComponent, SchemaComponentProvider, Grid } from '@nocobase/client';
function Draggable(props) {
const { attributes, listeners, setNodeRef, transform } = useDraggable({
id: props.id,
data: props.data,
});
const style = transform
? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
}
: undefined;
return (
<button ref={setNodeRef} style={style} {...listeners} {...attributes}>
{props.children}
</button>
);
}
function Droppable(props) {
const { isOver, setNodeRef } = useDroppable({
id: props.id,
data: props.data,
});
const style = {
color: isOver ? 'green' : undefined,
};
return (
<div ref={setNodeRef} style={style}>
{props.children}
</div>
);
}
import { uid } from '@formily/shared';
import { BlockItem, DragHandler, Grid, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
import React from 'react';
const Block = observer((props) => {
const fieldSchema = useFieldSchema();
return (
<Droppable id={fieldSchema.name} data={{ schema: fieldSchema }}>
<div style={{ marginBottom: 20, padding: '0 20px', height: 50, lineHeight: '50px', background: '#f1f1f1' }}>
Block {fieldSchema.name}
<Draggable id={fieldSchema.name} data={{ schema: fieldSchema }}>
Drag
</Draggable>
</div>
</Droppable>
<div style={{ marginBottom: 20, padding: '0 20px', height: 50, lineHeight: '50px', background: '#f1f1f1' }}>
Block {fieldSchema.title}
<DragHandler />
</div>
);
});
export default function App() {
return (
<SchemaComponentProvider components={{ Grid, Block }}>
<SchemaComponentProvider components={{ Grid, Block, BlockItem }}>
<SchemaComponent
schema={{
type: 'void',
name: 'page',
name: 'grid1',
'x-component': 'Grid',
'x-uid': uid(),
properties: {
[uid()]: {
row1: {
type: 'void',
'x-component': 'Grid.Row',
'x-uid': uid(),
properties: {
[uid()]: {
col11: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
[uid()]: {
block1: {
type: 'void',
title: '1',
'x-decorator': 'BlockItem',
'x-component': 'Block',
},
block2: {
type: 'void',
title: '2',
'x-decorator': 'BlockItem',
'x-component': 'Block',
},
},
},
[uid()]: {
col12: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
[uid()]: {
block3: {
type: 'void',
title: '3',
'x-decorator': 'BlockItem',
'x-component': 'Block',
},
},
},
},
},
[uid()]: {
row2: {
type: 'void',
'x-component': 'Grid.Row',
'x-uid': uid(),
properties: {
[uid()]: {
col21: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
[uid()]: {
block4: {
type: 'void',
title: '4',
'x-decorator': 'BlockItem',
'x-component': 'Block',
},
},
},
[uid()]: {
col22: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
[uid()]: {
block5: {
type: 'void',
title: '5',
'x-decorator': 'BlockItem',
'x-component': 'Block',
},
},

View File

@ -1,5 +1,9 @@
import { useDroppable } from '@dnd-kit/core';
import { css } from '@emotion/css';
import { observer, RecursionField, Schema, useField, useFieldSchema } from '@formily/react';
import { uid } from '@formily/shared';
import cls from 'classnames';
import React from 'react';
import { observer } from '@formily/react';
import { DndContext } from '../dnd-context';
export const Grid: any = observer((props) => {
@ -10,10 +14,72 @@ export const Grid: any = observer((props) => {
);
});
const ColDivider = (props) => {
const { isOver, setNodeRef } = useDroppable({
id: props.id,
data: props.data,
});
const droppableStyle = {
backgroundColor: isOver ? 'green' : undefined,
};
return <div ref={setNodeRef} style={{ width: 24, ...droppableStyle }}></div>;
};
Grid.Row = observer((props) => {
return <div>{props.children}</div>;
const field = useField();
const fieldSchema = useFieldSchema();
const addr = field.address.toString();
const wrapSchema = (schema: Schema) => {
const s = new Schema({
type: 'void',
name: `col_${uid()}`,
'x-uid': uid(),
'x-component': 'Grid.Col',
});
// parent 更新了,需要重新指定
if (Schema.isSchemaInstance(schema)) {
schema.parent = s;
}
s.addProperty(schema.name, schema);
return s;
};
const onInsertAdjacent = ({ dn, draggedSchema }) => {
dn.remove(draggedSchema, {
removeEmptyParents: true,
});
};
console.log('fieldSchema', fieldSchema.toJSON());
return (
<div
className={cls(
'nb-grid-row',
css`
margin: 0 -24px;
display: flex;
`,
)}
>
<ColDivider
id={`${addr}_0`}
data={{ wrapSchema, insertAdjacent: 'afterBegin', schema: fieldSchema, onInsertAdjacent }}
/>
{fieldSchema.mapProperties((schema, key, index) => {
return (
<React.Fragment key={key}>
<RecursionField name={key} schema={schema} />
<ColDivider
id={`${addr}_${index + 1}`}
data={{ wrapSchema, insertAdjacent: 'afterEnd', schema, onInsertAdjacent }}
/>
</React.Fragment>
);
})}
</div>
);
});
Grid.Col = observer((props) => {
return <div>{props.children}</div>;
return <div className={cls('nb-grid-col')}>{props.children}</div>;
});

View File

@ -25,8 +25,10 @@ export * from './radio';
export * from './record-picker';
export * from './row-selection';
export * from './select';
export * from './sortable-item';
export * from './tabs';
export * from './time-picker';
export * from './tree-select';
export * from './upload';
export * from './void-table';

View File

@ -0,0 +1,9 @@
---
nav:
path: /client
group:
path: /schema-components
---
# SortableItem

View File

@ -0,0 +1,61 @@
import { useDraggable, useDroppable } from '@dnd-kit/core';
import { observer, useField, useFieldSchema } from '@formily/react';
import React, { createContext, useContext } from 'react';
export const DraggableContext = createContext(null);
export const Sortable = (props: any) => {
const draggable = useDraggable({
id: props.id,
data: props.data,
});
const { isOver, setNodeRef } = useDroppable({
id: props.id,
data: props.data,
});
const droppableStyle = {
color: isOver ? 'green' : undefined,
};
return (
<DraggableContext.Provider value={draggable}>
<div ref={setNodeRef} style={droppableStyle}>
{props.children}
</div>
</DraggableContext.Provider>
);
};
export const SortableItem = observer((props) => {
const field = useField();
const fieldSchema = useFieldSchema();
const onInsertAdjacent = ({ dn, orginDraggedParentSchema }) => {
dn.removeIfChildrenEmpty(orginDraggedParentSchema, {
removeEmptyParents: true,
});
};
return (
<Sortable
id={field.address.toString()}
data={{ insertAdjacent: 'afterEnd', onInsertAdjacent, schema: fieldSchema }}
>
{props.children}
</Sortable>
);
});
export const DragHandler = () => {
const { attributes, listeners, setNodeRef, transform } = useContext(DraggableContext);
const style = transform
? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
}
: undefined;
return (
<div ref={setNodeRef} style={{ ...style, display: 'inline-block' }} {...listeners} {...attributes}>
Drag
</div>
);
};

View File

@ -1,8 +1,8 @@
import React, { useContext } from 'react';
import { ISchema, Schema, useField, useFieldSchema } from '@formily/react';
import { SchemaComponentContext } from '../context';
import set from 'lodash/set';
import { uid } from '@formily/shared';
import set from 'lodash/set';
import React, { useContext } from 'react';
import { SchemaComponentContext } from '../context';
interface CreateDesignableProps {
current: Schema;
@ -17,6 +17,11 @@ export function createDesignable(options: CreateDesignableProps) {
*/
type Position = 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd';
interface InsertAdjacentOptions {
wrap?: (s: ISchema) => ISchema;
removeEmptyParents?: boolean;
}
const generateUid = (s: ISchema) => {
if (!s['x-uid']) {
s['x-uid'] = uid();
@ -26,6 +31,8 @@ const generateUid = (s: ISchema) => {
});
};
const defaultWrap = (s: ISchema) => s;
export class Designable {
current: Schema;
@ -66,25 +73,63 @@ export class Designable {
this.events[name].forEach((fn) => fn.bind(this)(...args));
}
insertAdjacent(position: Position, schema: ISchema) {
insertAdjacent(position: Position, schema: ISchema, options: InsertAdjacentOptions = {}) {
switch (position) {
case 'beforeBegin':
return this.insertBeforeBegin(schema);
return this.insertBeforeBegin(schema, options);
case 'afterBegin':
return this.insertAfterBegin(schema);
return this.insertAfterBegin(schema, options);
case 'beforeEnd':
return this.insertBeforeEnd(schema);
return this.insertBeforeEnd(schema, options);
case 'afterEnd':
return this.insertAfterEnd(schema);
return this.insertAfterEnd(schema, options);
}
}
remove() {
const s = this.current.parent.removeProperty(this.current.name);
this.emit('afterRemove', s);
removeIfChildrenEmpty(schema?: Schema, options: { recursive?: boolean } = {}) {
if (!schema) {
return;
}
const { recursive } = options;
let s = schema;
const count = Object.keys(s.properties || {}).length;
console.log('removeIfChildrenEmpty', count, s.properties);
if (count > 0) {
return;
}
let removed;
while (s.parent) {
removed = s.parent.removeProperty(s.name);
if (!recursive) {
break;
}
const count = Object.keys(s.parent.properties || {}).length;
if (count > 0) {
break;
}
s = s.parent;
}
}
insertBeforeBeginOrAfterEnd(schema: ISchema) {
remove(schema?: Schema, options: { removeEmptyParents?: boolean } = {}) {
const { removeEmptyParents } = options;
let s = schema || this.current;
let removed;
while (s.parent) {
removed = s.parent.removeProperty(s.name);
if (!removeEmptyParents) {
break;
}
const count = Object.keys(s.parent.properties || {}).length;
if (count > 0) {
break;
}
s = s.parent;
}
this.emit('afterRemove', removed);
}
insertBeforeBeginOrAfterEnd(schema: ISchema, options: InsertAdjacentOptions = {}) {
if (!Schema.isSchemaInstance(this.current)) {
return;
}
@ -104,22 +149,25 @@ export class Designable {
fromIndex = index;
}
});
return fromIndex > toIndex ? this.insertBeforeBegin(schema) : this.insertAfterEnd(schema);
return fromIndex > toIndex ? this.insertBeforeBegin(schema, options) : this.insertAfterEnd(schema, options);
}
/**
* Before the current schema itself.
*/
insertBeforeBegin(schema: ISchema) {
insertBeforeBegin(schema: ISchema, options: InsertAdjacentOptions = {}) {
if (!Schema.isSchemaInstance(this.current)) {
return;
}
const { wrap = defaultWrap, removeEmptyParents } = options;
const properties = {};
let start = false;
if (Schema.isSchemaInstance(schema)) {
schema.parent.removeProperty(schema.name);
schema.parent = this.current.parent;
if (removeEmptyParents) {
this.removeIfChildrenEmpty(schema.parent);
}
}
this.current.parent.mapProperties((property, key) => {
@ -133,7 +181,9 @@ export class Designable {
});
this.prepareProperty(schema);
const s = this.current.parent.addProperty(schema.name, schema);
const wrapped = wrap(schema);
const s = this.current.parent.addProperty(wrapped.name, wrapped);
s.parent = this.current.parent;
this.current.parent.setProperties(properties);
this.emit('afterInsertAdjacent', 'beforeBegin', s);
}
@ -144,13 +194,16 @@ export class Designable {
* @param schema
* @returns
*/
insertAfterBegin(schema: ISchema) {
insertAfterBegin(schema: ISchema, options: InsertAdjacentOptions = {}) {
if (!Schema.isSchemaInstance(this.current)) {
return;
}
const { wrap = defaultWrap, removeEmptyParents } = options;
if (Schema.isSchemaInstance(schema)) {
schema.parent.removeProperty(schema.name);
schema.parent = this.current;
if (removeEmptyParents) {
this.removeIfChildrenEmpty(schema.parent);
}
}
const properties = {};
this.current.mapProperties((schema, key) => {
@ -158,7 +211,9 @@ export class Designable {
});
this.current.properties = {};
this.prepareProperty(schema);
const s = this.current.addProperty(schema.name, schema);
const wrapped = wrap(schema);
const s = this.current.addProperty(wrapped.name, wrapped);
s.parent = this.current;
this.current.setProperties(properties);
this.emit('afterInsertAdjacent', 'afterBegin', s);
}
@ -169,32 +224,40 @@ export class Designable {
* @param schema
* @returns
*/
insertBeforeEnd(schema: ISchema) {
insertBeforeEnd(schema: ISchema, options: InsertAdjacentOptions = {}) {
if (!Schema.isSchemaInstance(this.current)) {
return;
}
const { wrap = defaultWrap, removeEmptyParents } = options;
if (Schema.isSchemaInstance(schema)) {
schema.parent.removeProperty(schema.name);
schema.parent = this.current;
if (removeEmptyParents) {
this.removeIfChildrenEmpty(schema.parent);
}
}
this.prepareProperty(schema);
const s = this.current.addProperty(schema.name || uid(), schema);
const wrapped = wrap(schema);
const s = this.current.addProperty(wrapped.name || uid(), wrapped);
s.parent = this.current;
this.emit('afterInsertAdjacent', 'beforeEnd', s);
}
/**
* After the current schema itself.
*/
insertAfterEnd(schema: ISchema) {
insertAfterEnd(schema: ISchema, options: InsertAdjacentOptions = {}) {
if (!Schema.isSchemaInstance(this.current)) {
return;
}
const properties = {};
let start = false;
const { wrap = defaultWrap, removeEmptyParents } = options;
if (Schema.isSchemaInstance(schema)) {
schema.parent.removeProperty(schema.name);
schema.parent = this.current.parent;
if (removeEmptyParents) {
this.removeIfChildrenEmpty(schema.parent);
}
schema.parent = null;
}
this.current.parent.mapProperties((property, key) => {
@ -208,7 +271,11 @@ export class Designable {
});
this.prepareProperty(schema);
const s = this.current.parent.addProperty(schema.name || uid(), schema);
const wrapped = wrap(schema);
const s = this.current.parent.addProperty(wrapped.name || uid(), wrapped);
s.parent = this.current.parent;
this.current.parent.setProperties(properties);
this.emit('afterInsertAdjacent', 'afterEnd', s);
}