import { ISchema, Schema, SchemaOptionsContext, useField, useFieldSchema } from '@formily/react'; import { uid } from '@formily/shared'; import { message } from 'antd'; import get from 'lodash/get'; import set from 'lodash/set'; import React, { useContext } from 'react'; import { APIClient, useAPIClient } from '../../api-client'; import { SchemaComponentContext } from '../context'; interface CreateDesignableProps { current: Schema; api?: APIClient; refresh?: () => void; onSuccess?: any; } export function createDesignable(options: CreateDesignableProps) { return new Designable(options); } /** * */ type Position = 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd'; interface InsertAdjacentOptions { wrap?: (s: ISchema) => ISchema; removeParentsIfNoChildren?: boolean; breakRemoveOn?: ISchema | BreakFn; onSuccess?: any; } type BreakFn = (s: ISchema) => boolean; interface RemoveOptions { removeParentsIfNoChildren?: boolean; breakRemoveOn?: ISchema | BreakFn; } interface RecursiveRemoveOptions { breakRemoveOn?: ISchema | BreakFn; } 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; const matchSchema = (source: ISchema, target: ISchema) => { if (!source || !target) { return; } for (const key in target) { if (Object.prototype.hasOwnProperty.call(target, key)) { const value = target[key]; if (value !== source?.[key]) { return false; } } } return true; }; export const splitWrapSchema = (wrapped: Schema, schema: ISchema) => { if (wrapped['x-uid'] && wrapped['x-uid'] === schema['x-uid']) { return [null, wrapped.toJSON()]; } const wrappedJson: ISchema = wrapped.toJSON(); let schema1 = { ...wrappedJson, properties: {} }; let schema2 = null; const findSchema = (properties, parent) => { Object.keys(properties || {}).forEach((key) => { const current = properties[key]; if (current['x-uid'] === schema['x-uid']) { schema2 = properties[key]; return; } else { parent.properties[key] = { ...current, properties: {} }; findSchema(current?.properties, parent.properties[key]); } }); }; findSchema(wrappedJson.properties, schema1); return [schema1, schema2]; }; export class Designable { current: Schema; options: CreateDesignableProps; events = {}; constructor(options: CreateDesignableProps) { this.options = options; this.current = options.current; } loadAPIClientEvents() { const { refresh, api } = this.options; if (!api) { return; } this.on('insertAdjacent', async ({ onSuccess, current, position, schema, wrap, removed }) => { refresh(); if (!current['x-uid']) { return; } await api.request({ url: `/uiSchemas:insertAdjacent/${current['x-uid']}?position=${position}`, method: 'post', data: { schema, wrap, }, }); if (removed?.['x-uid']) { await api.request({ url: `/uiSchemas:remove/${removed['x-uid']}`, method: 'post', }); } onSuccess?.(); message.success('配置保存成功!', 0.2); }); this.on('patch', async ({ schema }) => { refresh(); if (!schema?.['x-uid']) { return; } await api.request({ url: `/uiSchemas:patch`, method: 'post', data: { ...schema, }, }); message.success('配置保存成功!', 0.2); }); this.on('remove', async ({ removed }) => { refresh(); if (!removed?.['x-uid']) { return; } await api.request({ url: `/uiSchemas:remove/${removed['x-uid']}`, method: 'post', }); message.success('配置保存成功!', 0.2); }); } 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: 'insertAdjacent' | 'remove' | 'error' | 'patch', listener: any) { if (!this.events[name]) { this.events[name] = []; } this.events[name].push(listener); } emit(name: 'insertAdjacent' | 'remove' | 'error' | 'patch', ...args) { if (!this.events[name]) { return; } const [opts, ...others] = args; this.events[name].forEach((fn) => fn.bind(this)({ current: this.current, ...opts }, ...others)); } parentsIn(schema: Schema) { if (!schema) { return false; } if (!Schema.isSchemaInstance(schema)) { return false; } let s = this.current; while (s?.parent) { if (s.parent === schema) { return true; } s = s.parent; } return false; } refresh() { const { refresh } = this.options; return refresh?.(); } 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); } } recursiveRemoveIfNoChildren(schema?: Schema, options?: RecursiveRemoveOptions) { if (!schema) { return; } let s = schema; let removed: Schema; const breakRemoveOn = options?.breakRemoveOn; while (s) { if (typeof breakRemoveOn === 'function') { if (breakRemoveOn(s)) { break; } } else { if (matchSchema(s, breakRemoveOn)) { break; } } const count = Object.keys(s.properties || {}).length; if (count > 0) { break; } if (s.parent) { removed = s.parent.removeProperty(s.name); } s = s.parent; } return removed; } remove(schema?: Schema, options: RemoveOptions = {}) { const { breakRemoveOn, removeParentsIfNoChildren } = options; let s = schema || this.current; let removed = s.parent.removeProperty(s.name); if (removeParentsIfNoChildren) { const parent = this.recursiveRemoveIfNoChildren(s.parent, { breakRemoveOn }); if (parent) { removed = parent; } } this.emit('remove', { removed }); } removeWithoutEmit(schema?: Schema, options: RemoveOptions = {}) { const { breakRemoveOn, removeParentsIfNoChildren } = options; let s = schema || this.current; let removed = s.parent.removeProperty(s.name); if (removeParentsIfNoChildren) { const parent = this.recursiveRemoveIfNoChildren(s.parent, { breakRemoveOn }); if (parent) { removed = parent; } } return 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 opts = { onSuccess: options.onSuccess }; const { wrap = defaultWrap, breakRemoveOn, removeParentsIfNoChildren } = options; if (Schema.isSchemaInstance(schema)) { if (this.parentsIn(schema)) { this.emit('error', { code: 'parent_is_not_allowed', schema, }); return; } schema.parent.removeProperty(schema.name); if (removeParentsIfNoChildren) { opts['removed'] = this.recursiveRemoveIfNoChildren(schema.parent, { breakRemoveOn }); } } const properties = {}; let start = false; let order = 0; let newOrder = 0; this.current.parent.mapProperties((property, key) => { if (key === this.current.name) { newOrder = order; start = true; ++order; } property['x-index'] = order; ++order; 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 || uid(), wrapped); s['x-index'] = newOrder; s.parent = this.current.parent; this.current.parent.setProperties(properties); const [schema1, schema2] = splitWrapSchema(s, schema); this.emit('insertAdjacent', { position: 'beforeBegin', schema: schema2, wrap: schema1, ...opts, }); } /** * Just inside the current schema, before its first child. * * @param schema * @returns */ insertAfterBegin(schema: ISchema, options: InsertAdjacentOptions = {}) { if (!Schema.isSchemaInstance(this.current)) { return; } const opts = { onSuccess: options.onSuccess }; const { wrap = defaultWrap, breakRemoveOn, removeParentsIfNoChildren } = options; if (Schema.isSchemaInstance(schema)) { if (this.parentsIn(schema)) { this.emit('error', { code: 'parent_is_not_allowed', schema, }); return; } schema.parent.removeProperty(schema.name); if (removeParentsIfNoChildren) { opts['removed'] = this.recursiveRemoveIfNoChildren(schema.parent, { breakRemoveOn }); } } const properties = {}; let order = 1; this.current.mapProperties((s, key) => { s['x-index'] = order; ++order; properties[key] = s; }); this.current.properties = {}; this.prepareProperty(schema); const wrapped = wrap(schema); const s = this.current.addProperty(wrapped.name || uid(), wrapped); s['x-index'] = 0; s.parent = this.current; this.current.setProperties(properties); const [schema1, schema2] = splitWrapSchema(s, schema); this.emit('insertAdjacent', { position: 'afterBegin', schema: schema2, wrap: schema1, ...opts, }); } /** * Just inside the targetElement, after its last child. * * @param schema * @returns */ insertBeforeEnd(schema: ISchema, options: InsertAdjacentOptions = {}) { if (!Schema.isSchemaInstance(this.current)) { return; } const opts = { onSuccess: options.onSuccess }; const { wrap = defaultWrap, breakRemoveOn, removeParentsIfNoChildren } = options; if (Schema.isSchemaInstance(schema)) { if (this.parentsIn(schema)) { this.emit('error', { code: 'parent_is_not_allowed', schema, }); return; } schema.parent.removeProperty(schema.name); if (removeParentsIfNoChildren) { opts['removed'] = this.recursiveRemoveIfNoChildren(schema.parent, { breakRemoveOn }); } } this.prepareProperty(schema); const wrapped = wrap(schema); const s = this.current.addProperty(wrapped.name || uid(), wrapped); s.parent = this.current; const [schema1, schema2] = splitWrapSchema(s, schema); this.emit('insertAdjacent', { position: 'beforeEnd', schema: schema2, wrap: schema1, ...opts, }); } /** * After the current schema itself. */ insertAfterEnd(schema: ISchema, options: InsertAdjacentOptions = {}) { if (!Schema.isSchemaInstance(this.current)) { return; } const opts = { onSuccess: options?.onSuccess }; const { wrap = defaultWrap, breakRemoveOn, removeParentsIfNoChildren } = options; if (Schema.isSchemaInstance(schema)) { if (this.parentsIn(schema)) { this.emit('error', { code: 'parent_is_not_allowed', schema, }); return; } schema.parent.removeProperty(schema.name); if (removeParentsIfNoChildren) { opts['removed'] = this.recursiveRemoveIfNoChildren(schema.parent, { breakRemoveOn }); } schema.parent = null; } let order = 0; let newOrder = 0; let start = false; const properties = {}; this.current.parent.mapProperties((property, key) => { property['x-index'] = order; if (key === this.current.name) { ++order; newOrder = order; start = true; } ++order; 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; s['x-index'] = newOrder; this.current.parent.setProperties(properties); const [schema1, schema2] = splitWrapSchema(s, schema); this.emit('insertAdjacent', { position: 'afterEnd', schema: schema2, wrap: schema1, ...opts, }); } } // 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 api = useAPIClient(); const dn = createDesignable({ api, refresh, current: fieldSchema }); dn.loadAPIClientEvents(); return { dn, 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(schema?: any, options?: RemoveOptions) { dn.remove(schema, options); }, 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); }, }; }