355 lines
9.5 KiB
TypeScript
355 lines
9.5 KiB
TypeScript
import { ISchema, Schema, SchemaOptionsContext, useField, useFieldSchema } from '@formily/react';
|
||
import { uid } from '@formily/shared';
|
||
import get from 'lodash/get';
|
||
import set from 'lodash/set';
|
||
import React, { useContext } from 'react';
|
||
import { SchemaComponentContext } from '../context';
|
||
|
||
interface CreateDesignableProps {
|
||
current: Schema;
|
||
}
|
||
|
||
export function createDesignable(options: CreateDesignableProps) {
|
||
return new Designable(options);
|
||
}
|
||
|
||
/**
|
||
*
|
||
*/
|
||
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();
|
||
}
|
||
Object.keys(s.properties || {}).forEach((key) => {
|
||
generateUid(s.properties[key]);
|
||
});
|
||
};
|
||
|
||
const defaultWrap = (s: ISchema) => s;
|
||
|
||
export class Designable {
|
||
current: Schema;
|
||
|
||
events = {};
|
||
|
||
constructor({ current }) {
|
||
this.current = current;
|
||
}
|
||
|
||
prepareProperty(schema: ISchema) {
|
||
if (!schema.type) {
|
||
schema.type = 'void';
|
||
}
|
||
if (!schema.name) {
|
||
schema.name = uid();
|
||
}
|
||
// x-uid 仅用于后端查询 schema,如果 current 没有 x-uid 不处理
|
||
if (!this.current['x-uid']) {
|
||
return;
|
||
}
|
||
// if (Schema.isSchemaInstance(schema)) {
|
||
// return;
|
||
// }
|
||
generateUid(schema);
|
||
}
|
||
|
||
on(name: 'afterInsertAdjacent' | 'afterRemove', listener: any) {
|
||
if (!this.events[name]) {
|
||
this.events[name] = [];
|
||
}
|
||
this.events[name].push(listener);
|
||
}
|
||
|
||
emit(name: 'afterInsertAdjacent' | 'afterRemove', ...args) {
|
||
if (!this.events[name]) {
|
||
return;
|
||
}
|
||
this.events[name].forEach((fn) => fn.bind(this)(...args));
|
||
}
|
||
|
||
insertAdjacent(position: Position, schema: ISchema, options: InsertAdjacentOptions = {}) {
|
||
switch (position) {
|
||
case 'beforeBegin':
|
||
return this.insertBeforeBegin(schema, options);
|
||
case 'afterBegin':
|
||
return this.insertAfterBegin(schema, options);
|
||
case 'beforeEnd':
|
||
return this.insertBeforeEnd(schema, options);
|
||
case 'afterEnd':
|
||
return this.insertAfterEnd(schema, options);
|
||
}
|
||
}
|
||
|
||
removeIfChildrenEmpty(schema?: Schema) {
|
||
if (!schema) {
|
||
return;
|
||
}
|
||
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);
|
||
const count = Object.keys(s.parent.properties || {}).length;
|
||
if (count > 0) {
|
||
break;
|
||
}
|
||
s = s.parent;
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
if (!Schema.isSchemaInstance(schema)) {
|
||
return;
|
||
}
|
||
if (this.current.parent !== schema.parent) {
|
||
return;
|
||
}
|
||
let fromIndex = 0;
|
||
let toIndex = 0;
|
||
this.current.parent.mapProperties((property, key, index) => {
|
||
if (this.current.name === key) {
|
||
toIndex = index;
|
||
}
|
||
if (schema.name === key) {
|
||
fromIndex = index;
|
||
}
|
||
});
|
||
return fromIndex > toIndex ? this.insertBeforeBegin(schema, options) : this.insertAfterEnd(schema, options);
|
||
}
|
||
|
||
/**
|
||
* Before the current schema itself.
|
||
*/
|
||
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);
|
||
if (removeEmptyParents) {
|
||
this.removeIfChildrenEmpty(schema.parent);
|
||
}
|
||
}
|
||
|
||
this.current.parent.mapProperties((property, key) => {
|
||
if (key === this.current.name) {
|
||
start = true;
|
||
}
|
||
if (start) {
|
||
properties[key] = property;
|
||
this.current.parent.removeProperty(key);
|
||
}
|
||
});
|
||
|
||
this.prepareProperty(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);
|
||
}
|
||
|
||
/**
|
||
* Just inside the current schema, before its first child.
|
||
*
|
||
* @param schema
|
||
* @returns
|
||
*/
|
||
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);
|
||
if (removeEmptyParents) {
|
||
this.removeIfChildrenEmpty(schema.parent);
|
||
}
|
||
}
|
||
const properties = {};
|
||
this.current.mapProperties((schema, key) => {
|
||
properties[key] = schema;
|
||
});
|
||
this.current.properties = {};
|
||
this.prepareProperty(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);
|
||
}
|
||
|
||
/**
|
||
* Just inside the targetElement, after its last child.
|
||
*
|
||
* @param schema
|
||
* @returns
|
||
*/
|
||
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);
|
||
if (removeEmptyParents) {
|
||
this.removeIfChildrenEmpty(schema.parent);
|
||
}
|
||
}
|
||
this.prepareProperty(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, 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);
|
||
console.log('insertAfterEnd', removeEmptyParents);
|
||
if (removeEmptyParents) {
|
||
this.removeIfChildrenEmpty(schema.parent);
|
||
}
|
||
schema.parent = null;
|
||
}
|
||
|
||
this.current.parent.mapProperties((property, key) => {
|
||
if (key === this.current.name) {
|
||
start = true;
|
||
}
|
||
if (start && key !== this.current.name) {
|
||
properties[key] = property;
|
||
this.current.parent.removeProperty(key);
|
||
}
|
||
});
|
||
|
||
this.prepareProperty(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);
|
||
}
|
||
}
|
||
|
||
// TODO
|
||
export function useDesignable() {
|
||
const { designable, setDesignable, refresh, reset } = useContext(SchemaComponentContext);
|
||
const { components } = useContext(SchemaOptionsContext);
|
||
const DesignableBar = () => {
|
||
return <></>;
|
||
};
|
||
const field = useField();
|
||
const fieldSchema = useFieldSchema();
|
||
const dn = createDesignable({ current: fieldSchema });
|
||
dn.on('afterInsertAdjacent', refresh);
|
||
dn.on('afterRemove', refresh);
|
||
return {
|
||
designable,
|
||
reset,
|
||
refresh,
|
||
setDesignable,
|
||
DesignableBar,
|
||
findComponent(component: any) {
|
||
if (!component) {
|
||
return null;
|
||
}
|
||
if (typeof component !== 'string') {
|
||
return component;
|
||
}
|
||
return get(components, component);
|
||
},
|
||
on: dn.on.bind(dn),
|
||
// TODO
|
||
patch: (key: ISchema | string, value?: any) => {
|
||
const update = (obj: any) => {
|
||
Object.keys(obj).forEach((k) => {
|
||
const val = obj[k];
|
||
if (k === 'title') {
|
||
field.title = val;
|
||
fieldSchema['title'] = val;
|
||
}
|
||
if (k === 'x-component-props') {
|
||
Object.keys(val).forEach((i) => {
|
||
field.componentProps[i] = val[i];
|
||
fieldSchema['x-component-props'][i] = val[i];
|
||
});
|
||
}
|
||
});
|
||
};
|
||
if (typeof key === 'string') {
|
||
const obj = {};
|
||
set(obj, key, value);
|
||
return update(obj);
|
||
}
|
||
update(key);
|
||
refresh();
|
||
},
|
||
remove() {
|
||
dn.remove();
|
||
},
|
||
insertAdjacent(position: Position, schema: ISchema, options: InsertAdjacentOptions = {}) {
|
||
dn.insertAdjacent(position, schema, options);
|
||
},
|
||
insertBeforeBegin(schema: ISchema) {
|
||
dn.insertBeforeBegin(schema);
|
||
},
|
||
insertAfterBegin(schema: ISchema) {
|
||
dn.insertAfterBegin(schema);
|
||
},
|
||
insertBeforeEnd(schema: ISchema) {
|
||
dn.insertBeforeEnd(schema);
|
||
},
|
||
insertAfterEnd(schema: ISchema) {
|
||
dn.insertAfterEnd(schema);
|
||
},
|
||
};
|
||
}
|