This commit is contained in:
chenos 2021-07-03 00:24:21 +08:00
parent 46e4089140
commit 8b34b84a5e
33 changed files with 1488 additions and 2811 deletions

View File

@ -0,0 +1,329 @@
import React, {
createContext,
useCallback,
useContext,
useMemo,
useState,
} from 'react';
import { createForm } from '@formily/core';
import {
Field,
ISchema,
observer,
Schema,
createSchemaField,
FormProvider,
useField,
useFieldSchema,
} from '@formily/react';
import { observable } from '@formily/reactive';
import { uid, clone } from '@formily/shared';
import { ArrayCollapse, ArrayTable, FormLayout } from '@formily/antd';
import { Space, Card } from 'antd';
import { Action, useLogin, useRegister, useSubmit } from '../action';
import { AddNew } from '../add-new';
import { Cascader } from '../cascader';
import { Checkbox } from '../checkbox';
import { ColorSelect } from '../color-select';
import { DatabaseField } from '../database-field';
import { DatePicker } from '../date-picker';
import { DrawerSelect } from '../drawer-select';
import { Filter } from '../filter';
import { Form } from '../form';
import { Grid } from '../grid';
import { IconPicker } from '../icon-picker';
import { Input } from '../input';
import { InputNumber } from '../input-number';
import { Markdown } from '../markdown';
import { Menu } from '../menu';
import { Password } from '../password';
import { Radio } from '../radio';
import { Select } from '../select';
import { Table } from '../table';
import { Tabs } from '../tabs';
import { TimePicker } from '../time-picker';
import { Upload } from '../upload';
import { FormItem } from '../form-item';
export const BlockContext = createContext({ dragRef: null });
const Div = (props) => <div {...props} />;
export const scope = {
useLogin,
useRegister,
useSubmit,
};
export const components = {
Div,
Space,
Card,
ArrayCollapse,
ArrayTable,
FormLayout,
FormItem,
Action,
AddNew,
Cascader,
Checkbox,
ColorSelect,
DatabaseField,
DatePicker,
DrawerSelect,
Filter,
Form,
Grid,
IconPicker,
Input,
InputNumber,
Markdown,
Menu,
Password,
Radio,
Select,
Table,
Tabs,
TimePicker,
Upload,
};
export function registerScope(scopes) {
Object.keys(scopes).forEach((key) => {
scope[key] = scopes[key];
});
}
export function registerComponents(values) {
Object.keys(values).forEach((key) => {
components[key] = values[key];
});
}
export interface DesignableContextProps {
schema: Schema;
refresh: () => void;
}
export const DesignableContext = createContext<DesignableContextProps>({
schema: null,
refresh: null,
});
export function pathToArray(path): string[] {
if (Array.isArray(path)) {
return [...path];
}
if (typeof path === 'string') {
return path.split('.');
}
}
export function findPropertyByPath(schema: Schema, path?: any): Schema {
if (!path) {
return schema;
}
const arr = pathToArray(path);
let property = schema;
while (arr.length) {
const name = arr.shift();
property = property.properties[name];
if (!property) {
console.error('property does not exist.');
break;
}
}
return property;
}
export function addPropertyBefore(target: Schema, data: ISchema) {
Object.keys(target.parent.properties).forEach((name) => {
if (name === target.name) {
target.parent.addProperty(data.name, data);
}
const property = target.parent.properties[name];
property.parent.removeProperty(property.name);
target.parent.addProperty(property.name, property.toJSON());
});
}
export function addPropertyAfter(target: Schema, data: ISchema) {
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(data.name, data);
}
});
}
export function useDesignable(path?: any) {
const { schema, refresh } = useContext(DesignableContext);
const schemaPath = path || useSchemaPath();
const currentSchema = findPropertyByPath(schema, schemaPath);
console.log('useDesignable', { schema, schemaPath, currentSchema });
return {
schema: currentSchema,
refresh,
appendChild: (property: ISchema, targetPath?: any): Schema => {
let target = currentSchema;
if (targetPath) {
target = findPropertyByPath(schema, targetPath);
}
if (!target) {
console.error('target schema does not exist.');
return;
}
if (!property.name) {
property.name = uid();
}
target.addProperty(property.name, property);
// BUG: 空 properties 时addProperty 无反应。
const tmp = { name: uid() };
addPropertyAfter(target, tmp);
target.parent.removeProperty(tmp.name);
refresh();
return target.properties[property.name];
},
insertAfter: (property: ISchema, targetPath?: any): Schema => {
let target = currentSchema;
if (targetPath) {
target = findPropertyByPath(schema, targetPath);
}
if (!target) {
console.error('target schema does not exist.');
return;
}
if (!property.name) {
property.name = uid();
}
addPropertyAfter(target, property);
refresh();
return target.parent.properties[property.name];
},
insertBefore(property: ISchema, targetPath?: any): Schema {
let target = currentSchema;
if (targetPath) {
target = findPropertyByPath(schema, targetPath);
}
if (!target) {
console.error('target schema does not exist.');
return;
}
if (!property.name) {
property.name = uid();
}
addPropertyBefore(target, property);
refresh();
return target.parent.properties[property.name];
},
remove(targetPath?: any) {
let target = currentSchema;
if (targetPath) {
target = findPropertyByPath(schema, targetPath);
}
if (!target) {
console.error('target schema does not exist.');
return;
}
target.parent.removeProperty(target.name);
refresh();
return target;
},
};
}
export function useSchemaPath() {
const schema = useFieldSchema();
const path = [schema.name];
let parent = schema.parent;
while (parent) {
if (!parent.name) {
break;
}
path.unshift(parent.name);
parent = parent.parent;
}
return [...path];
}
console.log({ scope, components });
export const createDesignableSchemaField = (options) => {
const SchemaField = createSchemaField(options);
const DesignableSchemaField = (props) => {
const schema = useMemo(() => new Schema(props.schema), [props.schema]);
const [, refresh] = useState(0);
if (props.designable === false) {
return <SchemaField schema={schema} />;
}
return (
<DesignableContext.Provider
value={{
schema,
refresh: () => {
refresh(Math.random());
props.onRefresh && props.onRefresh(schema);
},
}}
>
<SchemaField schema={schema} />
</DesignableContext.Provider>
);
};
return DesignableSchemaField;
};
export const DesignableSchemaField = createDesignableSchemaField({
scope,
components,
});
export interface SchemaRendererProps {
schema: ISchema;
form?: any;
designable?: boolean;
onRefresh?: any;
onlyRenderProperties?: boolean;
}
export const SchemaRenderer = (props: SchemaRendererProps) => {
const form = useMemo(() => props.form || createForm({}), []);
const schema = useMemo(() => {
let s = props.schema;
if (props.onlyRenderProperties) {
s = {
type: 'object',
properties: s.properties,
};
} else if (s.name) {
s = {
type: 'object',
properties: {
[s.name]: s,
},
};
}
return s;
}, []);
console.log('SchemaRenderer', schema, props.schema);
return (
<FormProvider form={form}>
<DesignableSchemaField
onRefresh={props.onRefresh}
designable={props.designable}
schema={schema}
/>
</FormProvider>
);
};

View File

@ -47,13 +47,13 @@ export const BlockContext = createContext({ dragRef: null });
const Div = (props) => <div {...props} />;
const scope = {
export const scope = {
useLogin,
useRegister,
useSubmit,
};
const components = {
export const components = {
Div,
Space,
Card,

View File

@ -27,7 +27,7 @@ group:
* desc: 可以通过配置 `useAction` 来处理操作逻辑
*/
import React from 'react';
import { SchemaBlock, registerScope } from '../';
import { SchemaRenderer, registerScope } from '../';
function useCustomAction() {
return {
@ -52,7 +52,7 @@ const schema = {
};
export default () => {
return <SchemaBlock schema={schema} />
return <SchemaRenderer schema={schema} />
}
```
@ -60,7 +60,7 @@ export default () => {
```tsx
import React from 'react';
import { SchemaBlock } from '../';
import { SchemaRenderer } from '../';
const schema = {
type: 'void',
@ -73,7 +73,7 @@ const schema = {
};
export default () => {
return <SchemaBlock schema={schema} />
return <SchemaRenderer schema={schema} />
}
```
@ -81,7 +81,7 @@ export default () => {
```tsx
import React from 'react';
import { SchemaBlock } from '../';
import { SchemaRenderer } from '../';
const schema = {
type: 'void',
@ -94,7 +94,7 @@ const schema = {
};
export default () => {
return <SchemaBlock schema={schema} />
return <SchemaRenderer schema={schema} />
}
```
@ -102,7 +102,7 @@ export default () => {
```tsx
import React from 'react';
import { SchemaBlock } from '../';
import { SchemaRenderer } from '../';
const schema = {
type: 'void',
@ -110,7 +110,7 @@ const schema = {
title: '按钮',
'x-component': 'Action',
properties: {
drawer1: {
popover1: {
type: 'void',
title: '弹窗标题',
'x-component': 'Action.Popover',
@ -126,7 +126,7 @@ const schema = {
};
export default () => {
return <SchemaBlock schema={schema} />
return <SchemaRenderer schema={schema} />
}
```
@ -134,7 +134,7 @@ export default () => {
```tsx
import React from 'react';
import { SchemaBlock } from '../';
import { SchemaRenderer } from '../';
const schema = {
type: 'void',
@ -179,7 +179,7 @@ const schema = {
};
export default () => {
return <SchemaBlock schema={schema} />
return <SchemaRenderer schema={schema} />
}
```
@ -187,7 +187,7 @@ export default () => {
```tsx
import React from 'react';
import { SchemaBlock } from '../';
import { SchemaRenderer } from '../';
const schema = {
type: 'void',
@ -207,15 +207,16 @@ const schema = {
};
export default () => {
return <SchemaBlock schema={schema} />
return <SchemaRenderer schema={schema} />
}
```
## Action.Container - 指定容器内打开
```tsx
import React from 'react';
import { SchemaBlock } from '../';
import React, { useRef } from 'react';
import { SchemaRenderer } from '../';
import { ActionContext } from './';
const schema = {
type: 'void',
@ -223,17 +224,15 @@ const schema = {
title: '按钮',
'x-component': 'Action',
properties: {
drawer1: {
container1: {
type: 'void',
title: '页面标题',
'x-component': 'Action.Container',
'x-component-props': {
container: '#container'
},
properties: {
input: {
type: 'string',
title: '字段',
'x-designable-bar': 'FormItem.DesignableBar',
'x-decorator': 'FormItem',
'x-component': 'Input',
}
@ -243,11 +242,17 @@ const schema = {
};
export default () => {
const ref = useRef();
console.log('containerRef2222', ref)
return (
<div>
<SchemaBlock schema={schema} />
<ActionContext.Provider value={{
containerRef: ref,
}}>
<SchemaRenderer schema={schema} />
</ActionContext.Provider>
<div style={{padding: '8px 0'}}>目标容器:</div>
<div id={'container'} style={{ border: '1px dashed #ebedf1', background: '#fafafa', padding: 24 }}/>
<div ref={ref} id={'container'} style={{ border: '1px dashed #ebedf1', background: '#fafafa', padding: 24 }}/>
</div>
)
}
@ -257,7 +262,7 @@ export default () => {
```tsx
import React from 'react';
import { SchemaBlock } from '../';
import { SchemaRenderer } from '../';
const schema = {
type: 'void',
@ -303,7 +308,6 @@ const schema = {
};
export default () => {
return <SchemaBlock schema={schema} />
return <SchemaRenderer schema={schema} />
}
```

View File

@ -1,12 +1,4 @@
import React, { useContext, useState } from 'react';
import {
Input,
FormItem,
FormButtonGroup,
Submit,
Password,
} from '@formily/antd';
import { createForm } from '@formily/core';
import React, { createContext, useContext, useState } from 'react';
import {
useForm,
FormProvider,
@ -21,56 +13,16 @@ import {
import { Button, Dropdown, Menu, Popover, Space } from 'antd';
import { Link, useHistory, LinkProps } from 'react-router-dom';
import Drawer from '../../components/Drawer';
import { SchemaBlock } from '../';
import { SchemaRenderer, useDesignable } from '../';
import ReactDOM from 'react-dom';
import get from 'lodash/get';
import {
DesignableSchemaContext,
RefreshDesignableSchemaContext,
} from '../SchemaField';
import {
MenuOutlined,
GroupOutlined,
PlusOutlined,
LinkOutlined,
AppstoreAddOutlined,
EditOutlined,
DeleteOutlined,
ArrowRightOutlined,
SettingOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
LoadingOutlined,
} from '@ant-design/icons';
import { MenuOutlined } from '@ant-design/icons';
import classNames from 'classnames';
import { useMount } from 'ahooks';
import { uid } from '@formily/shared';
import './style.less';
export function useSchemaQuery(segments?: any[]) {
const context = useContext(DesignableSchemaContext);
const refresh = useContext(RefreshDesignableSchemaContext);
const fieldSchema = useFieldSchema();
const field = useField();
const getSchemaByPath = (path) => {
let s: Schema = context;
const names = [...path];
// names.shift();
while (s && names.length) {
const name = names.shift();
s = s.properties[name];
}
return s;
};
const schema = getSchemaByPath(segments || field.address.segments);
return {
refresh,
schema,
};
}
export function useDefaultAction() {
return {
run() {},
@ -121,6 +73,8 @@ export type ActionType = React.FC<ActionProps> & {
Link?: React.FC<LinkProps>;
URL?: React.FC<any>;
Page?: React.FC<any>;
Container?: React.FC<any>;
Popover?: React.FC<any>;
Drawer?: React.FC<any>;
Modal?: React.FC<any>;
Dropdown?: React.FC<any>;
@ -141,13 +95,40 @@ function useDesignableBar() {
};
}
export const ActionContext = createContext({ containerRef: null });
export const Action: ActionType = observer((props) => {
const { useAction = useDefaultAction, ...others } = props;
const { containerRef } = useContext(ActionContext);
const field = useField();
const { schema } = useSchemaQuery();
const schema = useFieldSchema();
const { run } = useAction();
const { DesignableBar } = useDesignableBar();
const renderContainer = () => {
let childSchema = null;
if (schema.properties) {
const key = Object.keys(schema.properties).shift();
const current = schema.properties[key];
childSchema = current;
}
if (childSchema && childSchema['x-component'] === 'Action.Container') {
containerRef &&
ReactDOM.render(
<div>
<SchemaRenderer schema={childSchema} onlyRenderProperties />
</div>,
containerRef.current,
);
}
};
useMount(() => {
renderContainer();
});
let childSchema = null;
if (schema.properties) {
const key = Object.keys(schema.properties).shift();
const current = schema.properties[key];
@ -162,7 +143,7 @@ export const Action: ActionType = observer((props) => {
{...childSchema['x-component-props']}
content={
<div>
<SchemaBlock schema={childSchema} onlyRenderProperties />
<SchemaRenderer schema={childSchema} onlyRenderProperties />
</div>
}
>
@ -170,6 +151,7 @@ export const Action: ActionType = observer((props) => {
</Popover>
);
}
return (
<Button
{...others}
@ -184,25 +166,14 @@ export const Action: ActionType = observer((props) => {
content: () => {
return (
<div>
<SchemaBlock schema={childSchema} onlyRenderProperties />
<SchemaRenderer schema={childSchema} onlyRenderProperties />
</div>
);
},
});
}
if (childSchema['x-component'] === 'Action.Container') {
const el = document.createElement('div');
const target = document.querySelector(
childSchema['x-component-props']?.['container'],
);
target.childNodes.forEach((child) => child.remove());
target.appendChild(el);
ReactDOM.render(
<div>
<SchemaBlock schema={childSchema} onlyRenderProperties />
</div>,
el,
);
renderContainer();
}
}}
>
@ -227,9 +198,26 @@ Action.URL = observer((props) => {
);
});
Action.Container = ({ children }) => {
return children;
};
Action.Popover = ({ children }) => {
return children;
};
Action.Drawer = ({ children }) => {
return children;
};
Action.Modal = ({ children }) => {
return children;
};
Action.DesignableBar = () => {
const field = useField();
const { schema, refresh } = useSchemaQuery();
const schema = useFieldSchema();
const { insertAfter } = useDesignable();
const [visible, setVisible] = useState(false);
return (
<div className={classNames('designable-bar', { active: visible })}>
@ -253,12 +241,21 @@ Action.DesignableBar = () => {
schema.title = '按钮文案被修改了';
field.setTitle('按钮文案被修改了');
schema.properties.drawer1.title = '抽屉标题文案被修改了';
refresh();
setVisible(false);
}}
>
</Menu.Item>
<Menu.Item
onClick={() => {
insertAfter({
name: uid(),
'x-component': 'Input',
});
}}
>
insertAfter
</Menu.Item>
</Menu>
}
>

View File

@ -44,327 +44,12 @@ import {
DatabaseOutlined,
} from '@ant-design/icons';
import { uid } from '@formily/shared';
import {
DesignableSchemaContext,
RefreshDesignableSchemaContext,
SchemaField,
} from '../SchemaField';
import { SchemaBlock } from '../';
import table from './table';
import markdown from './markdown';
import form from './form';
import { useRequest } from 'ahooks';
const row = (schema) => {
const component = schema['x-component'];
return {
type: 'void',
name: `gr_${uid()}`,
'x-component': 'Grid.Row',
properties: {
[`gc_${uid()}`]: {
type: 'void',
'x-component': 'Grid.Col',
'x-component-props': {
size: 1,
},
properties: {
[`gb_${uid()}`]: {
type: 'void',
'x-component': 'Grid.Block',
properties: {
[`gbn_${uid()}`]: 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 function useSchemaQuery(segments?: any[]) {
const context = useContext(DesignableSchemaContext);
const refresh = useContext(RefreshDesignableSchemaContext);
const fieldSchema = useFieldSchema();
const field = useField();
const getSchemaByPath = (path) => {
let s: Schema = context;
const names = [...path];
// names.shift();
while (s && names.length) {
const name = names.shift();
s = s.properties[name];
}
return s;
};
const schema = getSchemaByPath(segments || field.address.segments);
console.log({ context, schema });
return {
refresh,
schema,
appendChild(data) {
schema.addProperty(data.name, data);
refresh();
},
insertAfter(data) {
addPropertyAfter(schema, data);
refresh();
},
insertBefore(data) {
addPropertyBefore(schema, data);
refresh();
},
push(data) {
addPropertyBefore(schema, data);
},
remove() {
removeProperty(schema);
refresh();
},
};
}
export const AddNew: any = observer((props) => {
const field = useField();
const segments = [...field.address.segments];
segments.pop();
segments.pop();
segments.pop();
// segments.pop();
const { insertBefore, refresh, schema } = useSchemaQuery(segments);
console.log('field.address.segments', segments, schema);
const {
data = [],
loading,
mutate,
} = useRequest(() => {
return Promise.resolve([
{ title: '数据表1' },
{ title: '数据表2' },
{ title: '数据表3' },
]);
});
const [visible, setVisible] = useState(false);
const dbschema = {
type: 'object',
properties: {
layout: {
type: 'void',
'x-component': 'FormLayout',
'x-component-props': {
layout: 'vertical',
},
properties: {
input: {
type: 'string',
title: '数据表名称',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
array: {
type: 'array',
title: '数据表字段',
'x-component': 'ArrayCollapse',
'x-component-props': {
accordion: true,
},
// maxItems: 3,
'x-decorator': 'FormItem',
items: {
type: 'object',
'x-component': 'ArrayCollapse.CollapsePanel',
'x-component-props': {
header: '字段',
},
properties: {
index: {
type: 'void',
'x-component': 'ArrayCollapse.Index',
},
input: {
type: 'string',
'x-decorator': 'FormItem',
title: 'Input',
required: true,
'x-component': 'Input',
},
remove: {
type: 'void',
'x-component': 'ArrayCollapse.Remove',
},
moveUp: {
type: 'void',
'x-component': 'ArrayCollapse.MoveUp',
},
moveDown: {
type: 'void',
'x-component': 'ArrayCollapse.MoveDown',
},
},
},
properties: {
addition: {
type: 'void',
title: '添加字段',
'x-component': 'ArrayCollapse.Addition',
},
},
},
},
},
},
};
if (loading) {
return <Spin />;
}
console.log({ data });
return (
<Dropdown
overlay={
<Menu>
<Menu.SubMenu
key={`table-tables`}
style={{ minWidth: 150 }}
title={'新建表单'}
>
<Menu.ItemGroup key={`table-tables-itemgroup`} title={'所属数据表'}>
{data.map((item, index) => {
return (
<Menu.Item
key={`table-${index}`}
style={{ minWidth: 150 }}
onClick={() => {
const rowData = row(form());
insertBefore(rowData);
}}
>
{item.title}
</Menu.Item>
);
})}
</Menu.ItemGroup>
<Menu.Divider></Menu.Divider>
<Menu.Item
style={{ minWidth: 150 }}
onClick={() => {
FormDialog('新建数据表', () => {
return <SchemaField schema={dbschema} />;
})
.open({
initialValues: {
// aaa: '123',
},
})
.then(() => {
const items = [...data];
items.push({ title: '数据表5' });
mutate(items);
const rowData = row(form());
insertBefore(rowData);
});
// const rowData = row(table());
// insertBefore(rowData);
}}
>
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu
key={`form-tables`}
style={{ minWidth: 150 }}
title={'新建表格'}
>
<Menu.ItemGroup key={`form-tables-itemgroup`} title={'所属数据表'}>
{data.map((item, index) => {
return (
<Menu.Item
key={`form-${index}`}
style={{ minWidth: 150 }}
onClick={() => {
const rowData = row(table());
console.log({ rowData });
insertBefore(rowData);
}}
>
{item.title}
</Menu.Item>
);
})}
</Menu.ItemGroup>
<Menu.Divider></Menu.Divider>
<Menu.Item
style={{ minWidth: 150 }}
onClick={() => {
FormDialog('新建数据表', () => {
return <SchemaField schema={dbschema} />;
})
.open({
initialValues: {
// aaa: '123',
},
})
.then(() => {
const items = [...data];
items.push({ title: '数据表4' });
mutate(items);
const rowData = row(table());
insertBefore(rowData);
});
}}
>
</Menu.Item>
</Menu.SubMenu>
<Menu.Item
onClick={() => {
const rowData = row(markdown());
insertBefore(rowData);
}}
>
Markdown
</Menu.Item>
<Menu.Item> Markdown</Menu.Item>
</Menu>
}
>
@ -374,47 +59,11 @@ export const AddNew: any = observer((props) => {
});
AddNew.FormItem = observer((props) => {
const field = useField();
const segments = [...field.address.segments];
segments.pop();
segments.pop();
segments.pop();
const { insertBefore, refresh, schema } = useSchemaQuery(segments);
const insertBeforeHandle = () => {
const rowData = row({
type: 'string',
// required: true,
name: `f_${uid()}`,
title: `字段${uid()}`,
'x-decorator': 'FormItem',
'x-designable-bar': 'FormItem.DesignableBar',
'x-component': 'Input',
});
console.log({ rowData });
insertBefore(rowData);
};
return (
<Dropdown
overlay={
<Menu>
<Menu.Item onClick={insertBeforeHandle} style={{ minWidth: 150 }}>
1
</Menu.Item>
<Menu.Item onClick={insertBeforeHandle}>2</Menu.Item>
<Menu.Item onClick={insertBeforeHandle}>3</Menu.Item>
<Menu.Divider />
<Menu.SubMenu title={'新增字段'}>
<Menu.Item onClick={insertBeforeHandle} style={{ minWidth: 150 }}>
</Menu.Item>
<Menu.Item onClick={insertBeforeHandle}></Menu.Item>
<Menu.Item onClick={insertBeforeHandle}></Menu.Item>
<Menu.Item onClick={insertBeforeHandle}></Menu.Item>
<Menu.Item onClick={insertBeforeHandle}></Menu.Item>
</Menu.SubMenu>
<Menu.Item> 1</Menu.Item>
</Menu>
}
>

View File

@ -18,7 +18,7 @@ group:
* title: 省市区级联
*/
import React from 'react';
import { SchemaBlock } from '../';
import { SchemaRenderer } from '../';
const options = [
{
@ -94,7 +94,7 @@ const schema = {
export default () => {
return (
<SchemaBlock schema={schema} />
<SchemaRenderer schema={schema} />
);
};
```

View File

@ -20,7 +20,7 @@ group:
* title: 勾选
*/
import React from 'react';
import { SchemaBlock } from '../';
import { SchemaRenderer } from '../';
const schema = {
type: 'object',
@ -53,7 +53,7 @@ const schema = {
export default () => {
return (
<SchemaBlock schema={schema} />
<SchemaRenderer schema={schema} />
);
};
```
@ -65,7 +65,7 @@ export default () => {
* title: 组
*/
import React from 'react';
import { SchemaBlock } from '../';
import { SchemaRenderer } from '../';
const options = [
{
@ -111,7 +111,7 @@ const schema = {
export default () => {
return (
<SchemaBlock schema={schema} />
<SchemaRenderer schema={schema} />
);
};
```

View File

@ -16,7 +16,7 @@ group:
* title: 颜色选择器
*/
import React from 'react';
import { SchemaBlock } from '../';
import { SchemaRenderer } from '../';
const schema = {
type: 'object',
@ -59,7 +59,7 @@ const schema = {
export default () => {
return (
<SchemaBlock schema={schema} />
<SchemaRenderer schema={schema} />
);
};
```

View File

@ -13,7 +13,7 @@ group:
```tsx
import React from 'react';
import { SchemaBlock } from '../';
import { SchemaRenderer } from '../';
const schema = {
type: 'object',
@ -71,6 +71,6 @@ const schema = {
};
export default () => {
return <SchemaBlock schema={schema} />
return <SchemaRenderer schema={schema} />
}
```

View File

@ -20,7 +20,7 @@ group:
* title: 日期选择器
*/
import React from 'react';
import { SchemaBlock } from '../';
import { SchemaRenderer } from '../';
const schema = {
type: 'object',
@ -53,7 +53,7 @@ const schema = {
export default () => {
return (
<SchemaBlock schema={schema} />
<SchemaRenderer schema={schema} />
);
};
```
@ -65,7 +65,7 @@ export default () => {
* title: 日期时间选择
*/
import React from 'react';
import { SchemaBlock } from '../';
import { SchemaRenderer } from '../';
const schema = {
type: 'object',
@ -104,7 +104,7 @@ const schema = {
export default () => {
return (
<SchemaBlock schema={schema} />
<SchemaRenderer schema={schema} />
);
};
```

View File

@ -1,12 +1,13 @@
import React, { useContext, useState } from 'react';
import { connect, mapProps, mapReadPretty, SchemaOptionsContext, useField, useFieldSchema } from '@formily/react';
import { FormItem as FormilyFormItem } from '@formily/antd';
import { Dropdown, Menu } from 'antd';
import { Dropdown, Menu, Space } from 'antd';
import classNames from 'classnames';
import { MenuOutlined } from '@ant-design/icons';
import { MenuOutlined, DragOutlined } from '@ant-design/icons';
import './style.less';
import get from 'lodash/get';
import { BlockContext } from '../SchemaField';
import { GridBlockContext } from '../grid';
import { uid } from '@formily/shared';
function Blank() {
return null;
@ -24,11 +25,10 @@ function useDesignableBar() {
export const FormItem: any = connect((props) => {
const { DesignableBar } = useDesignableBar();
const { dragRef } = useContext(BlockContext);
return (
<div className={'designable-form-item'}>
<FormilyFormItem {...props} />
{dragRef && <div ref={dragRef}>Drag</div>}
{/* {dragRef && <div ref={dragRef}>Drag</div>} */}
<DesignableBar/>
</div>
);
@ -37,6 +37,7 @@ export const FormItem: any = connect((props) => {
FormItem.DesignableBar = () => {
const field = useField();
const [visible, setVisible] = useState(false);
const { dragRef } = useContext(GridBlockContext);
return (
<div className={classNames('designable-bar', { active: visible })}>
<span
@ -45,6 +46,8 @@ FormItem.DesignableBar = () => {
}}
className={classNames('designable-bar-actions', { active: visible })}
>
<Space size={'small'}>
<DragOutlined ref={dragRef} />
<Dropdown
trigger={['click']}
visible={visible}
@ -55,6 +58,7 @@ FormItem.DesignableBar = () => {
<Menu>
<Menu.Item
onClick={(e) => {
field.title = uid();
setVisible(false);
}}
>
@ -63,8 +67,9 @@ FormItem.DesignableBar = () => {
</Menu>
}
>
<MenuOutlined />
<MenuOutlined/>
</Dropdown>
</Space>
</span>
</div>
);

View File

@ -24,12 +24,19 @@
position: absolute;
right: 0;
line-height: 1rem;
background-color: #1890ff;
color: #fff;
z-index: 10;
padding: 0 3px;
.ant-space {
gap: 1px !important;
}
.anticon {
width: 16px;
height: 16px;
vertical-align: top;
line-height: 16px;
font-size: 10px;
background-color: #1890ff;
}
}
}

View File

@ -2,7 +2,6 @@ import React, { useState } from 'react';
import classNames from 'classnames';
import { useField, observer, RecursionField, Schema } from '@formily/react';
import { Dropdown, Menu } from 'antd';
import { useSchemaQuery } from '../grid';
import {
MenuOutlined,
ArrowUpOutlined,
@ -11,18 +10,11 @@ import {
} from '@ant-design/icons';
export const DesignableBar = (props) => {
const { addBlock, removeBlock } = useSchemaQuery();
const [active, setActive] = useState(false);
return (
<>
<Menu.Item
onClick={() => {
addBlock(
{},
{
insertBefore: true,
},
);
setActive(false);
}}
icon={<ArrowUpOutlined />}
@ -31,7 +23,6 @@ export const DesignableBar = (props) => {
</Menu.Item>
<Menu.Item
onClick={() => {
addBlock();
setActive(false);
}}
icon={<ArrowDownOutlined />}
@ -41,7 +32,6 @@ export const DesignableBar = (props) => {
<Menu.Divider />
<Menu.Item
onClick={() => {
removeBlock();
setActive(false);
}}
icon={<DeleteOutlined />}

View File

@ -1,141 +0,0 @@
import React, { useState, useContext, createContext } from 'react';
import { useDrag, mergeRefs, useDrop } from './DND';
import classNames from 'classnames';
import {
useField,
observer,
RecursionField,
Schema,
SchemaOptionsContext,
} from '@formily/react';
import { Dropdown, Menu } from 'antd';
import { useSchemaQuery } from '.';
import {
MenuOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
DeleteOutlined,
} from '@ant-design/icons';
import get from 'lodash/get';
import { BlockContext } from '../SchemaField';
export const Block = observer((props: any) => {
const { children, title, DesignableBar } = 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('ActionBar', DesignableBar);
const { isOver, onTopHalf, dropRef } = useDrop({
accept: 'grid',
data: {},
canDrop: !isDragging,
});
const options = useContext(SchemaOptionsContext);
const DesignableBarComponent = get(options.components, DesignableBar);
console.log({ DesignableBarComponent, DesignableBar });
const { addBlock } = useSchemaQuery();
console.log({ children });
return (
<div
data-type={'block'}
ref={mergeRefs([dropRef, previewRef])}
className={classNames('block', { 'top-half': onTopHalf, hover: isOver })}
// style={{ textAlign: 'center', lineHeight: '60px', background: '#f1f1f1' }}
>
<BlockContext.Provider value={{ dragRef }}>
{children}
</BlockContext.Provider>
{/* {DesignableBarComponent && (
<DefaultActionBar DesignableBarComponent={DesignableBarComponent} dragRef={dragRef} />
)} */}
</div>
);
});
// function overlay() {
// return <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>
// }
function Overlay(props) {
return (
<>
<Menu.Item>1</Menu.Item>
<Menu.Item>2</Menu.Item>
<Menu.Divider />
<Menu.Item></Menu.Item>
</>
);
}
const DefaultActionBar = ({ DesignableBarComponent, dragRef }) => {
const { addBlock, removeBlock } = useSchemaQuery();
const [active, setActive] = useState(false);
// return <MenuOutlined className={'draggable'} ref={dragRef} />;
return (
<div className={classNames('action-bar', { active })}>
<Dropdown
overlayStyle={{ minWidth: 200 }}
trigger={['click']}
visible={active}
onVisibleChange={setActive}
overlay={<Menu>{DesignableBarComponent ? <DesignableBarComponent /> : <Overlay />}</Menu>}
>
<MenuOutlined className={'draggable'} ref={dragRef} />
</Dropdown>
</div>
);
};
export default Block;

View File

@ -1,100 +0,0 @@
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 || {};
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) => {
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,
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 && 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 { isDragging, dragOffset, dragRef, columns };
}
export const Col: any = (props) => {
const { size, children, position = {}, isLast } = props;
return (
<>
<div data-type={'col'} className={'col'} style={{ width: `${size * 100}%` }}>
{children}
</div>
<Col.Divider />
</>
);
};
Col.Divider = (props) => {
const { onDragEnd, resizable = true } = props;
const { isDragging, dragRef } = useColResizer({ onDragEnd });
const { isOver, dropRef } = useDrop({
accept: 'grid',
data: { },
});
return (
<div
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,88 +0,0 @@
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 './';
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 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,
),
};
}
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);
}}
>
<Row.Divider style={{ marginTop: -24 }} />
{children}
</DragDropProvider>
</div>
);
};
Grid.Row = Row;
Grid.Col = Col;
Grid.Block = Block;
export default Grid;

View File

@ -1,46 +0,0 @@
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, position = {}, isLast } = props;
const { isOver, onTopHalf, dropRef } = useDrop({
accept: 'grid',
data: {},
shallow: true,
});
return (
<>
<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

@ -0,0 +1,120 @@
import React from 'react';
import { uid } from '@formily/shared';
import {
observer,
ISchema,
FormProvider,
useFieldSchema,
RecursionField,
useField,
} from '@formily/react';
import { Input } from '../../input';
import { FormItem } from '../../form-item';
import { createDesignableSchemaField } from '../../DesignableSchemaField';
import { createForm } from '@formily/core';
import { Grid } from '../';
const schema: ISchema = {
type: 'object',
properties: {
[uid()]: {
type: 'void',
'x-component': 'Grid',
properties: {
[`row_${uid()}`]: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
[`col_${uid()}`]: {
type: 'void',
'x-component': 'Grid.Col',
"x-component-props": {
width: 30,
},
properties: {
[`block_${uid()}`]: {
type: 'void',
'x-component': 'Grid.Block',
properties: {
[uid()]: {
type: 'string',
title: uid(),
'x-designable-bar': 'FormItem.DesignableBar',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
[`col_${uid()}`]: {
type: 'void',
'x-component': 'Grid.Col',
"x-component-props": {
width: 70,
},
properties: {
[`block_${uid()}`]: {
type: 'void',
'x-component': 'Grid.Block',
properties: {
[uid()]: {
type: 'string',
title: uid(),
'x-designable-bar': 'FormItem.DesignableBar',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
},
},
[`row_${uid()}`]: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
[`col_${uid()}`]: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
[`block_${uid()}`]: {
type: 'void',
'x-component': 'Grid.Block',
properties: {
[uid()]: {
type: 'string',
title: uid(),
'x-designable-bar': 'FormItem.DesignableBar',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
},
},
},
},
},
};
const DesignableSchemaField = createDesignableSchemaField({
components: {
Grid,
Input,
FormItem,
},
});
const form = createForm();
export default () => {
return (
<FormProvider form={form}>
<DesignableSchemaField schema={schema} />
</FormProvider>
);
};

View File

@ -1,91 +0,0 @@
import React, { createContext, useContext, useEffect, useRef } from 'react';
import { useDrag, useDrop, DragDropProvider, mergeRefs } from '../';
import { Button, Space } from 'antd';
function DropZone({ options, children }) {
const { isOver, dropRef } = useDrop(options);
return (
<div
ref={dropRef as any}
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 Dragable2() {
const { isDragging, dragRef, previewRef } = useDrag({
type: 'box2',
onDragStart() {
console.log('onDragStart');
},
onDragEnd(event) {
console.log('onDragEnd', event.data);
},
onDrag(event) {
// console.log('onDrag');
},
});
return <Button ref={mergeRefs<any>([dragRef, previewRef])}>2</Button>;
}
export default () => {
return (
<DragDropProvider>
<Space style={{ marginBottom: 12 }}>
<Dragable />
<Dragable2 />
</Space>
<DropZone
options={{
accept: 'box',
data: { a: 'a' },
shallow: true,
}}
>
Drop Zone1
<DropZone
options={{
accept: 'box',
data: { b: 'b' },
// shallow: true,
}}
>
Drop Zone2
</DropZone>
<DropZone
options={{
accept: 'box2',
data: { c: 'c' },
// shallow: true,
}}
>
Drop Zone3
</DropZone>
Drop Zone1
</DropZone>
</DragDropProvider>
);
};

View File

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

View File

@ -1,167 +0,0 @@
import React from 'react';
import { SchemaBlock } from '../../';
import { ISchema } from '@formily/json-schema';
const schema: ISchema = {
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-content': (
<div
style={{
padding: 24,
textAlign: 'center',
background: 'rgb(241, 241, 241)',
}}
>
block11
</div>
),
},
},
},
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',
},
'x-content': (
<div
style={{
padding: 24,
textAlign: 'center',
background: 'rgb(241, 241, 241)',
}}
>
block21
</div>
),
},
},
},
},
},
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-content': (
<div
style={{
padding: 24,
textAlign: 'center',
background: 'rgb(241, 241, 241)',
}}
>
block211
</div>
),
},
},
},
col22: {
type: 'void',
'x-component': 'Grid.Col',
'x-component-props': {
size: 2 / 3,
isLast: true,
},
properties: {
block221: {
type: 'void',
'x-component': 'Grid.Block',
'x-content': (
<div
style={{
padding: 24,
textAlign: 'center',
background: 'rgb(241, 241, 241)',
}}
>
block221
</div>
),
},
},
},
},
},
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-content': (
<div
style={{
padding: 24,
textAlign: 'center',
background: 'rgb(241, 241, 241)',
}}
>
block311
</div>
),
},
},
},
},
},
},
},
},
};
export default () => {
return <SchemaBlock schema={schema} />;
};

View File

@ -99,6 +99,7 @@ export function useDrag(options?: any) {
};
const wrap = document.createElement('div');
wrap.className = 'drag-container';
wrap.style.position = 'absolute';
wrap.style.pointerEvents = 'none';
wrap.style.opacity = '0.7';
@ -325,3 +326,70 @@ export function useDrop(options) {
dropRef,
};
}
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) => {
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,
prevWidth: prev.style.width,
nextWidth: next.style.width,
});
}
});
onMouseUp((event: React.MouseEvent) => {
if (!isDragging) {
return;
}
const parent = dragRef.current.parentElement;
const els = parent.querySelectorAll(':scope > .nb-grid-col');
const size = [];
els.forEach((el: HTMLDivElement) => {
const w = (100 * el.clientWidth) / parent.clientWidth;
const w2 =
(100 * (el.clientWidth + 24 + 24 / els.length)) / parent.clientWidth;
size.push(w2);
el.style.width = `${w}%`;
});
console.log({ size });
setIsDragging(false);
setInitial(null);
// @ts-ignore
event.data = { size };
onDragEnd && 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 { isDragging, dragOffset, dragRef, columns };
}

View File

@ -73,62 +73,4 @@ group:
- 100%
## 代码演示
### useDrag & useDrop
<code src="./demos/demo4.tsx"/>
### useColResize
<code src="./demos/demo5.tsx"/>
### Grid
<code src="./demos/demo6.tsx"/>
## API 说明
### Grid
只能在同一个 Grid 里拖拽布局
### Grid.Row
### Grid.Column
### Grid.Block
区块
### BlockOptions
```ts
interface BlockOptions {
rowOrder: number;
columnOrder: number;
blockOrder: number;
}
```
- rowOrder第几行
- columnOrder第几列
- blockOrder某单元格内部区块排序
### blocks2properties
原始 schema 需要至少 grid->row->col->block->custom 五层嵌套,写起来非常繁琐,`blocks2properties` 方法可以简化配置。
### useDrag & useDrop
拖拽 hooks
原生态的
### useDrop
### useColResize
<code src="./demos/demo1.tsx"/>

View File

@ -1,252 +1,237 @@
import React, { useContext, createContext, useState } from 'react';
import React, {
FC,
CSSProperties,
useRef,
createContext,
useContext,
useEffect,
} from 'react';
// import { DndProvider, useDrag, useDragDropManager } from 'react-dnd';
// import { HTML5Backend } from 'react-dnd-html5-backend';
import { uid } from '@formily/shared';
import {
Schema,
observer,
ISchema,
FormProvider,
useFieldSchema,
useForm,
RecursionField,
useField,
} from '@formily/react';
import { uid } from '@formily/shared';
import './style.less';
import cls from 'classnames';
import {
DesignableSchemaContext,
RefreshDesignableSchemaContext,
} from '../SchemaField';
import { useDesignable, useSchemaPath } from '../DesignableSchemaField';
import { useColResizer } from './hooks';
import { useDrag, useDrop, DragDropProvider, mergeRefs } from './hooks';
export function removeProperty(property: Schema) {
property.parent.removeProperty(property.name);
}
export const GridContext = createContext({
ref: null,
});
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());
const ColumnSizeContext = createContext(null);
export const GridBlockContext = createContext({
dragRef: null,
});
const RowDivider = ({ onDrop }) => {
const { isOver, dropRef } = useDrop({
accept: 'grid',
onDrop,
});
}
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;
return (
<div
ref={dropRef}
className={cls('nb-grid-row-divider', { hover: isOver })}
/>
);
};
export function useSchemaQuery() {
const context = useContext(DesignableSchemaContext);
const refresh = useContext(RefreshDesignableSchemaContext);
const fieldSchema = useFieldSchema();
const ColDivider = (props: any) => {
const { onDragEnd, resizable } = props;
const { isDragging, dragRef } = useColResizer({ onDragEnd });
const { isOver, dropRef } = useDrop({
accept: 'grid',
data: {},
});
return (
<div
data-type={'col-divider'}
ref={mergeRefs([dragRef, dropRef])}
style={{ width: 24 }}
className={cls('nb-grid-col-divider', {
resizable,
hover: isOver,
dragging: isDragging,
})}
></div>
);
};
export const Grid: any = observer((props) => {
const schema = useFieldSchema();
const { insertBefore, insertAfter, remove } = useDesignable();
const ref = useRef();
return (
<DragDropProvider>
<GridContext.Provider value={{ ref }}>
<div ref={ref} className={'nb-grid'}>
<RowDivider
onDrop={(e) => {
const blockSchema = e.dragItem.schema;
const path = [...e.dragItem.path];
path.pop();
remove(path);
insertBefore({
type: 'void',
"x-component": 'Grid.Row',
properties: {
[uid()]: {
type: 'void',
"x-component": 'Grid.Col',
properties: {
[blockSchema.name]: blockSchema,
},
},
},
});
}}
/>
{schema.mapProperties((property) => {
return (
<>
<div style={{ display: 'flex' }} className={'nb-grid-row'}>
<RecursionField name={property.name} schema={property} />
</div>
<RowDivider
onDrop={(e) => {
const blockSchema = e.dragItem.schema;
const path = [...e.dragItem.path];
path.pop();
remove(path);
insertAfter({
type: 'void',
"x-component": 'Grid.Row',
properties: {
[uid()]: {
type: 'void',
"x-component": 'Grid.Col',
properties: {
[blockSchema.name]: blockSchema,
},
},
},
});
}}
/>
</>
);
})}
</div>
</GridContext.Provider>
</DragDropProvider>
);
});
Grid.Row = observer((props) => {
const field = useField();
const form = useForm();
const getSchemaByPath = (path) => {
let s: Schema = context;
const names = [...path];
while (names.length) {
s = s.properties[names.shift()];
}
const schema = useFieldSchema();
const { schema: designableSchema, refresh } = useDesignable();
const len = Object.keys(schema.properties || {}).length;
return (
<ColumnSizeContext.Provider value={len}>
{schema.mapProperties((property, key, index) => {
return (
<>
<ColDivider
resizable={index > 0}
onDragEnd={(e) => {
schema.mapProperties((s, key, index) => {
field.query(`.${schema.name}.${key}`).take((f) => {
f.componentProps['width'] = e.data.size[index];
});
s['x-component-props'] = s['x-component-props'] || {};
s['x-component-props']['width'] = e.data.size[index];
return s;
};
});
designableSchema.mapProperties((s, key, index) => {
s['x-component-props'] = s['x-component-props'] || {};
s['x-component-props']['width'] = e.data.size[index];
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;
}
console.log('e.data', designableSchema);
}}
/>
<RecursionField name={property.name} schema={property} />
</>
);
})}
<ColDivider />
</ColumnSizeContext.Provider>
);
});
// 只有一列时,删除当前行
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();
},
};
}
Grid.Col = observer((props) => {
const field = useField();
const width = field.componentProps['width'];
const size = useContext(ColumnSizeContext);
return (
<div
style={{ width: `calc(${width || 100 / size}% - 24px / ${size})` }}
className={'nb-grid-col'}
>
{props.children}
</div>
);
});
export * from './DND';
export * from './Row';
export * from './Col';
export * from './Grid';
export * from './Block';
Grid.Block = observer((props) => {
const schema = useFieldSchema();
const ctx = useContext(GridContext);
const path = useSchemaPath();
const { isDragging, dragRef, previewRef } = useDrag({
type: 'grid',
onDragStart() {
console.log('onDragStart');
},
onDragEnd(event) {
console.log('onDragEnd', event.data);
},
onDrag(event) {
// console.log('onDrag');
},
item: {
path,
schema: schema.toJSON(),
},
});
const { isOver, onTopHalf, dropRef } = useDrop({
accept: 'grid',
data: {},
canDrop: !isDragging,
});
useEffect(() => {
if (ctx.ref && ctx.ref.current) {
(ctx.ref.current as HTMLElement).className = isDragging
? 'nb-grid dragging'
: 'nb-grid';
}
console.log('ctx.ref.current');
}, [isDragging]);
return (
<GridBlockContext.Provider value={{ dragRef }}>
<div
ref={mergeRefs([previewRef, dropRef])}
className={cls('nb-grid-block', {
'top-half': onTopHalf,
hover: isOver,
dragging: isDragging,
})}
>
{props.children}
</div>
</GridBlockContext.Provider>
);
});

View File

@ -1,114 +1,81 @@
.col-divider {
.nb-grid-col-divider {
width: 24px;
}
.nb-grid-row {
margin: 0px -24px;
}
.nb-grid {
.nb-grid-col-divider {
&.resizable {
cursor: col-resize;
}
}
}
.nb-grid.dragging {
position: relative;
&:last-child {
&.resizable:hover {
cursor: auto;
&::after {
display: none;
}
}
&.resizable.hover {
&::after {
display: block !important;
}
}
}
.nb-grid-col-divider {
// background-color: #ddd;
// opacity: 0.5;
&.resizable {
cursor: col-resize;
}
&.hover {
cursor: grab !important;
background: #e6f7ff;
}
&.resizable:hover,
}
.nb-grid-row-divider {
// background-color: #ddd;
height: 24px;
width: 100%;
position: absolute;
z-index: 20;
// opacity: 0.5;
transform: translateY(-100%);
&.hover {
background: #e6f7ff;
}
}
.designable-bar {
display: none;
}
.nb-grid-block {
position: relative;
&.hover {
&::after {
background: #e6f7ff;
content: '';
display: block;
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 12px;
z-index: 30;
}
&.top-half {
&::after {
top: 0;
left: 50%;
transform: translateX(-50%);
height: 100%;
width: 12px;
background: #e6f7ff;
}
}
}
.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;
z-index: 222;
}
}
.draggable {
cursor: grab !important;
.nb-grid-block.dragging {
> div > .designable-bar {
display: block !important;
}
}
.drag-container {
.nb-grid-block.dragging .designable-bar {
display: none !important;
}
}
.anticon-drag {
cursor: grab;
}

View File

@ -5,6 +5,8 @@ import { DesignableSchemaField, SchemaField } from './SchemaField';
export * from './SchemaField';
export { SchemaRenderer, useDesignable, registerComponents, registerScope } from './DesignableSchemaField';
export const SchemaBlock = ({ schema, onlyRenderProperties = false, designable = true }) => {
const form = useMemo(() => createForm(), []);

View File

@ -22,7 +22,7 @@ group:
* title: 横向菜单
*/
import React from 'react';
import { SchemaBlock } from '../';
import { SchemaRenderer } from '../';
const schema = {
type: 'object',
@ -33,6 +33,7 @@ const schema = {
'x-designable-bar': 'Menu.DesignableBar',
'x-component-props': {
mode: 'horizontal',
theme: 'dark',
},
properties: {
item1: {
@ -47,24 +48,53 @@ const schema = {
},
item3: {
type: 'void',
title: '菜单组',
title: '菜单组3',
'x-component': 'Menu.SubMenu',
properties: {
item5: {
type: 'void',
title: `子菜单5`,
'x-component': 'Menu.SubMenu',
properties: {
item8: {
type: 'void',
title: `子菜单8`,
'x-component': 'Menu.Item',
},
item9: {
type: 'void',
title: `子菜单9`,
'x-component': 'Menu.Item',
},
},
},
}
},
item4: {
type: 'void',
title: `子菜单1`,
title: '菜单组4',
'x-component': 'Menu.SubMenu',
properties: {
item6: {
type: 'void',
title: `子菜单6`,
'x-component': 'Menu.Item',
},
item7: {
type: 'void',
title: `子菜单7`,
'x-component': 'Menu.Item',
},
}
},
},
}
}
};
},
},
}
export default () => {
return (
<SchemaBlock schema={schema} />
<SchemaRenderer schema={schema} />
);
};
```
@ -76,7 +106,7 @@ export default () => {
* title: 竖向菜单
*/
import React from 'react';
import { SchemaBlock } from '../';
import { SchemaRenderer } from '../';
const schema = {
type: 'object',
@ -90,23 +120,7 @@ const schema = {
mode: 'inline',
},
properties: {
item3: {
type: 'void',
title: '菜单组',
'x-component': 'Menu.SubMenu',
properties: {
item4: {
type: 'void',
title: `子菜单1`,
'x-component': 'Menu.Item',
},
item5: {
type: 'void',
title: `子菜单2`,
'x-component': 'Menu.Item',
},
}
},
item1: {
type: 'void',
title: `菜单1`,
@ -117,6 +131,40 @@ const schema = {
title: `菜单2`,
'x-component': 'Menu.Item',
},
item3: {
type: 'void',
title: '菜单组3',
'x-component': 'Menu.SubMenu',
properties: {
item4: {
type: 'void',
title: `子菜单4`,
'x-component': 'Menu.Item',
},
item5: {
type: 'void',
title: `子菜单5`,
'x-component': 'Menu.Item',
},
}
},
item4: {
type: 'void',
title: '菜单组4',
'x-component': 'Menu.SubMenu',
properties: {
item6: {
type: 'void',
title: `子菜单6`,
'x-component': 'Menu.Item',
},
item7: {
type: 'void',
title: `子菜单7`,
'x-component': 'Menu.Item',
},
}
},
},
},
},
@ -124,7 +172,234 @@ const schema = {
export default () => {
return (
<div style={{width: 200}}><SchemaBlock schema={schema} /></div>
<div style={{width: 200}}><SchemaRenderer schema={schema} /></div>
);
};
```
### 混合菜单
```tsx
import React, { useRef, useState } from 'react';
import { SchemaRenderer } from '../';
import { MenuContainerContext } from './';
import { Layout } from 'antd';
export default () => {
const ref = useRef();
const [activeKey, setActiveKey] = useState('item3');
const schema = {
type: 'object',
properties: {
menu1: {
type: 'void',
'x-component': 'Menu',
'x-component-props': {
defaultSelectedKeys: [activeKey],
mode: 'mix',
theme: 'dark',
onSelect(info) {
setActiveKey(info.key);
console.log({ info })
},
},
properties: {
item1: {
type: 'void',
title: `菜单1`,
'x-component': 'Menu.Item',
},
item2: {
type: 'void',
title: `菜单2`,
'x-component': 'Menu.Item',
},
item3: {
type: 'void',
title: '菜单组3',
'x-component': 'Menu.SubMenu',
properties: {
item4: {
type: 'void',
title: `子菜单4`,
'x-component': 'Menu.Item',
},
item5: {
type: 'void',
title: `子菜单5`,
'x-component': 'Menu.SubMenu',
properties: {
item8: {
type: 'void',
title: `子菜单8`,
'x-component': 'Menu.Item',
},
item9: {
type: 'void',
title: `子菜单9`,
'x-component': 'Menu.Item',
},
},
},
}
},
item4: {
type: 'void',
title: '菜单组4',
'x-component': 'Menu.SubMenu',
properties: {
item6: {
type: 'void',
title: `子菜单6`,
'x-component': 'Menu.Item',
},
item7: {
type: 'void',
title: `子菜单7`,
'x-component': 'Menu.Item',
},
}
},
},
},
},
}
return (
<div>
<Layout>
<Layout.Header>
<MenuContainerContext.Provider value={{
sideMenuRef: ref,
}}>
<SchemaRenderer schema={schema} />
</MenuContainerContext.Provider>
</Layout.Header>
<Layout>
<Layout.Sider ref={ref} theme={'light'} width={200}>
</Layout.Sider>
<Layout.Content>
{activeKey}
</Layout.Content>
</Layout>
</Layout>
</div>
)
}
```
### 设计器模式
```tsx
import React, { useRef, useState } from 'react';
import { SchemaRenderer } from '../';
import { MenuContainerContext } from './';
import { Layout } from 'antd';
export default () => {
const ref = useRef();
const [activeKey, setActiveKey] = useState('item3');
const schema = {
type: 'object',
properties: {
menu1: {
type: 'void',
'x-component': 'Menu',
'x-designable-bar': 'Menu.DesignableBar',
'x-component-props': {
defaultSelectedKeys: [activeKey],
mode: 'mix',
theme: 'dark',
onSelect(info) {
setActiveKey(info.key);
console.log({ info })
},
},
properties: {
item1: {
type: 'void',
title: `菜单1`,
'x-component': 'Menu.Item',
},
item2: {
type: 'void',
title: `菜单2`,
'x-component': 'Menu.Item',
},
item3: {
type: 'void',
title: '菜单组3',
'x-component': 'Menu.SubMenu',
properties: {
item34: {
type: 'void',
title: `子菜单4`,
'x-component': 'Menu.Item',
},
item5: {
type: 'void',
title: `子菜单5`,
'x-component': 'Menu.SubMenu',
properties: {
item8: {
type: 'void',
title: `子菜单8`,
'x-component': 'Menu.Item',
},
item9: {
type: 'void',
title: `子菜单9`,
'x-component': 'Menu.Item',
},
},
},
}
},
item4: {
type: 'void',
title: '菜单组4',
'x-component': 'Menu.SubMenu',
properties: {
item6: {
type: 'void',
title: `子菜单6`,
'x-component': 'Menu.Item',
},
item7: {
type: 'void',
title: `子菜单7`,
'x-component': 'Menu.Item',
},
}
},
},
},
},
}
return (
<div>
<Layout>
<Layout.Header>
<MenuContainerContext.Provider value={{
sideMenuRef: ref,
}}>
<SchemaRenderer schema={schema} />
</MenuContainerContext.Provider>
</Layout.Header>
<Layout>
<Layout.Sider ref={ref} theme={'light'} width={200}>
</Layout.Sider>
<Layout.Content>
{activeKey}
</Layout.Content>
</Layout>
</Layout>
</div>
)
}
```

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,19 @@
.ant-menu-dark.ant-menu-horizontal {
.ant-menu-horizontal {
width: 100%;
}
.ant-menu-dark .ant-menu-item-active .designable-bar {
.ant-menu-item-active .designable-bar {
display: inline-block;
}
.ant-menu-dark .ant-menu-item-active:hover .designable-bar,
.ant-menu-dark .ant-menu-item-active.ant-menu-item-selected .designable-bar {
.ant-menu-item-active:hover .designable-bar,
.ant-menu-item-active.ant-menu-item-selected .designable-bar {
display: inline-block;
}
.ant-menu-light .ant-menu-submenu-active > .ant-menu-submenu-title .designable-bar,
.ant-menu-light .ant-menu-item-active .designable-bar {
.ant-menu-submenu-active > .ant-menu-submenu-title .designable-bar,
.ant-menu-item-active .designable-bar {
display: inline-block;
}

View File

@ -20,7 +20,7 @@ group:
* title: 日期选择器
*/
import React from 'react';
import { SchemaBlock } from '../';
import { SchemaRenderer } from '../';
const schema = {
type: 'object',
@ -51,7 +51,7 @@ const schema = {
export default () => {
return (
<SchemaBlock schema={schema} />
<SchemaRenderer schema={schema} />
);
};
```

View File

@ -20,7 +20,7 @@ import React from 'react';
import { Button } from 'antd'
import { UploadOutlined, InboxOutlined } from '@ant-design/icons'
import Upload from './';
import { SchemaBlock, registerComponents } from '../';
import { SchemaRenderer, registerComponents } from '../';
const NormalUpload = (props) => {
return (
@ -69,7 +69,7 @@ const schema = {
export default () => {
return (
<SchemaBlock schema={schema} />
<SchemaRenderer schema={schema} />
);
};
```

View File

@ -4,8 +4,10 @@ export default {
type: 'void',
name: `m_${uid()}`,
'x-component': 'Menu',
'x-decorator': 'Menu.Designable',
'x-designable-bar': 'Menu.DesignableBar',
'x-component-props': {
mode: 'mix',
theme: 'dark',
},
properties: {
item2: {

View File

@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react';
import React, { useContext, useEffect, useRef, useState } from 'react';
import {
Button,
Spin,
@ -22,11 +22,11 @@ import {
refreshGlobalAction,
RouteComponentContext,
} from '../../';
import { SchemaBlock } from '../../blocks';
import { SchemaBlock, SchemaRenderer } from '../../blocks';
import { useRequest } from 'ahooks';
import cloneDeep from 'lodash/cloneDeep';
import { Schema } from '@formily/react';
import { DesignableProvider } from '../../blocks/SchemaField';
import { DesignableContext } from '../../blocks/SchemaField';
import { uid } from '@formily/shared';
import {
DatabaseOutlined,
@ -36,6 +36,7 @@ import {
import { Tabs } from 'antd';
import '@formily/antd/esm/array-collapse/style';
import './style.less';
import { MenuContainerContext } from '../../blocks/menu';
function LogoutButton() {
const history = useHistory();
@ -163,242 +164,34 @@ function Database() {
);
}
function useMenuSchema({ schema, selectedKey }) {
const [activeTopKey, setActiveTopKey] = useState(selectedKey);
let topMenuSchema = new Schema(cloneDeep(schema.toJSON()));
topMenuSchema =
topMenuSchema.properties[Object.keys(topMenuSchema.properties)[0]];
const [activeKey, setActiveKey] = useState(selectedKey);
console.log({ activeKey, topMenuSchema });
topMenuSchema['x-component-props']['hideSubMenu'] = true;
topMenuSchema['x-component-props']['mode'] = 'horizontal';
topMenuSchema['x-component-props']['theme'] = 'dark';
function findLastSelected(activeKey) {
function find(schema: Schema) {
return schema.reduceProperties((selected, current) => {
if (current.name === activeKey) {
return [...selected, current];
}
if (current.properties) {
return [...selected, ...find(current)];
}
return [...selected];
}, []);
}
// const topMenuSchema = new Schema(cloneDeep(schema.toJSON()));
let selected = find(topMenuSchema).shift() as Schema;
console.log({ topMenuSchema, selected, schema });
if (selected && selected.properties) {
const findChild = (properties) => {
const keys = Object.keys(properties || {});
const firstKey = keys.shift();
if (firstKey) {
selected = properties[firstKey];
findChild(properties[firstKey].properties);
}
};
findChild(selected.properties);
}
return selected;
}
function find(schema: Schema) {
return schema.reduceProperties((selected, current) => {
if (current.name === activeKey) {
return [...selected, current];
}
if (current.properties) {
return [...selected, ...find(current)];
}
return [...selected];
}, []);
}
let selected = (find(topMenuSchema).shift() as Schema) || new Schema({});
const [pageTitle, setPageTitle] = useState(selected.title);
console.log({ selected, pageTitle }, selected.title);
useEffect(() => {
setPageTitle(selected.title);
}, [selected]);
useEffect(() => {
setActiveKey(selectedKey);
}, [selectedKey]);
let s = selected;
let properties = null;
const selectedKeys = [s.name];
let sideMenuKey = null;
function getAddress(schema: Schema) {
const segments = [];
segments.unshift(schema.name);
while (schema.parent) {
segments.unshift(schema.parent.name);
schema = schema.parent;
}
return segments.join('.');
}
while (s.parent) {
if (s.parent['x-component'] === 'Menu') {
sideMenuKey = getAddress(s);
if (s['x-component'] === 'Menu.SubMenu') {
properties = s.properties;
}
break;
}
selectedKeys.push(s.parent.name);
s = s.parent;
}
console.log({ selectedKeys });
if (properties && selectedKeys.length === 1) {
const findChild = (properties) => {
const keys = Object.keys(properties || {});
const firstKey = keys.shift();
if (firstKey) {
selectedKeys.push(firstKey);
findChild(properties[firstKey].properties);
}
};
findChild(properties);
selectedKey = selectedKeys[selectedKeys.length - 1];
}
topMenuSchema['x-component-props']['onSelect'] = (info) => {
console.log('onSelect', info.item.props.schema);
// setActiveSchema(info.item.props.schema || {});
// setPageTitle(info.item.props.schema.title);
setActiveTopKey(info.key);
// setActiveKey(info.key);
const selected = findLastSelected(info.key);
console.log('selected', selected.name);
setActiveKey(selected.name);
setPageTitle(selected.title);
};
let sideMenuSchema = null;
if (properties) {
properties['add_new'] = new Schema({
type: 'void',
name: `m_${uid()}`,
'x-component': 'Menu.AddNew',
});
sideMenuSchema = new Schema({
type: 'void',
name: sideMenuKey,
'x-component': 'Menu',
'x-component-props': {
mode: 'inline',
// selectedKeys,
defaultSelectedKeys: selectedKeys,
defaultOpenKeys: selectedKeys,
onSelect(info) {
console.log('onSelect', info.item.props.schema);
setPageTitle(info.item.props.schema.title);
function LayoutWithMenu({ schema }) {
const location = useLocation();
const ref = useRef();
const [activeKey, setActiveKey] = useState('item3');
schema['x-component-props']['defaultSelectedKeys'] = [activeKey];
schema['x-component-props']['onSelect'] = (info) => {
console.log('LayoutWithMenu', schema)
setActiveKey(info.key);
},
},
properties,
}).toJSON();
}
// const sideMenuSchema = properties
// ? new Schema({
// type: 'void',
// name: sideMenuKey,
// 'x-component': 'Menu',
// 'x-component-props': {
// mode: 'inline',
// // selectedKeys,
// defaultSelectedKeys: selectedKeys,
// defaultOpenKeys: selectedKeys,
// onSelect(info) {
// console.log('onSelect', info.item.props.schema);
// setPageTitle(info.item.props.schema.title);
// setActiveKey(info.key);
// },
// },
// properties,
// }).toJSON()
// : null;
topMenuSchema['x-component-props']['defaultSelectedKeys'] = selectedKeys;
topMenuSchema['x-component-props']['defaultOpenKeys'] = selectedKeys;
return {
pageTitle,
topMenuSchema,
sideMenuSchema,
selectedKeys,
activeKey,
};
}
function LayoutWithMenu({ schema, activeMenuItemKey }) {
const { activeKey, pageTitle, topMenuSchema, sideMenuSchema } = useMenuSchema(
{ schema, selectedKey: activeMenuItemKey },
);
const history = useHistory();
return (
<Layout>
<Layout.Header
style={{
height: '45px',
lineHeight: '45px',
padding: 0,
display: 'flex',
}}
>
<div
style={{
width: 200,
fontSize: 24,
fontWeight: 200,
letterSpacing: 3,
textAlign: 'center',
color: '#fff',
}}
>
NocoBase
</div>
<SchemaBlock designable={false} schema={topMenuSchema} />
<Database />
<Layout.Header>
<MenuContainerContext.Provider value={{
sideMenuRef: ref,
}}>
<SchemaRenderer schema={schema} />
</MenuContainerContext.Provider>
</Layout.Header>
<Layout>
{sideMenuSchema && (
<Layout.Sider theme={'light'} width={200}>
<SchemaBlock designable={false} schema={sideMenuSchema} />
<Layout.Sider ref={ref} theme={'light'} width={200}>
</Layout.Sider>
)}
<Layout.Content>
{pageTitle && <PageHeader title={pageTitle} ghost={false} />}
<div style={{ margin: 24 }}>
{/* {history.location.pathname} */}
<Content activeKey={activeKey} />
</div>
{location.pathname}
<Content activeKey={activeKey}/>
</Layout.Content>
</Layout>
</Layout>
);
)
}
function Content({ activeKey }) {
@ -414,7 +207,7 @@ function Content({ activeKey }) {
return <Spin />;
}
return <SchemaBlock schema={data} />;
return <SchemaRenderer schema={data} />;
}
export function AdminLayout({ route, children }: any) {
@ -432,29 +225,8 @@ export function AdminLayout({ route, children }: any) {
}
return (
<DesignableProvider
schema={
new Schema(
data.name
? {
type: 'object',
properties: {
[data.name]: data,
},
}
: data,
)
}
>
{(s) => {
// console.log('DesignableProvider', s.properties.item2.title);
return (
<LayoutWithMenu activeMenuItemKey={match.params.name} schema={s} />
<LayoutWithMenu schema={data}/>
);
}}
</DesignableProvider>
);
// return <LayoutWithMenu activeMenuItemKey={match.params.name} schema={data} />;
}
export default AdminLayout;