tachybase_todo/packages/client/src/schema-component/hooks/useDesignable.tsx

355 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
},
};
}