diff --git a/package.json b/package.json index 3e3057d..a1871b0 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@angular/platform-browser": "^17.0.0", "@angular/platform-browser-dynamic": "^17.0.0", "@angular/router": "^17.0.0", + "@formily/reactive": "^2.3.1", "jsonpath-plus": "^8.0.0", "lodash": "^4.17.21", "rxjs": "~7.8.0", @@ -37,6 +38,7 @@ "@angular/cli": "^17.0.7", "@angular/compiler-cli": "^17.0.0", "@types/jasmine": "~5.1.0", + "@types/lodash": "^4.14.202", "@typescript-eslint/eslint-plugin": "6.19.0", "@typescript-eslint/parser": "6.19.0", "eslint": "^8.56.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e84f3e0..1e0083f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ dependencies: '@angular/router': specifier: ^17.0.0 version: 17.2.2(@angular/common@17.2.2)(@angular/core@17.2.2)(@angular/platform-browser@17.2.2)(rxjs@7.8.1) + '@formily/reactive': + specifier: ^2.3.1 + version: 2.3.1 jsonpath-plus: specifier: ^8.0.0 version: 8.0.0 @@ -73,6 +76,9 @@ devDependencies: '@types/jasmine': specifier: ~5.1.0 version: 5.1.4 + '@types/lodash': + specifier: ^4.14.202 + version: 4.14.202 '@typescript-eslint/eslint-plugin': specifier: 6.19.0 version: 6.19.0(@typescript-eslint/parser@6.19.0)(eslint@8.57.0)(typescript@5.2.2) @@ -2227,6 +2233,11 @@ packages: engines: {node: '>=14'} dev: true + /@formily/reactive@2.3.1: + resolution: {integrity: sha512-IVHOZW7VBc+Gq9eB/gPldi7pEC3wDonDb99KvHlS8SmzsY6+a/iAdrw2mDagXXUficsC2gT4y4EcJ2f1ALMKtQ==} + engines: {npm: '>=3.0.0'} + dev: false + /@humanwhocodes/config-array@0.11.14: resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} engines: {node: '>=10.10.0'} @@ -2526,6 +2537,7 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] requiresBuild: true dev: true optional: true @@ -2535,6 +2547,7 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] requiresBuild: true dev: true optional: true @@ -2544,6 +2557,7 @@ packages: engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] requiresBuild: true dev: true optional: true @@ -2553,6 +2567,7 @@ packages: engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] requiresBuild: true dev: true optional: true @@ -2626,6 +2641,7 @@ packages: resolution: {integrity: sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA==} cpu: [arm64] os: [linux] + libc: [glibc] requiresBuild: true dev: true optional: true @@ -2634,6 +2650,7 @@ packages: resolution: {integrity: sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ==} cpu: [arm64] os: [linux] + libc: [musl] requiresBuild: true dev: true optional: true @@ -2642,6 +2659,7 @@ packages: resolution: {integrity: sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw==} cpu: [riscv64] os: [linux] + libc: [glibc] requiresBuild: true dev: true optional: true @@ -2650,6 +2668,7 @@ packages: resolution: {integrity: sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA==} cpu: [x64] os: [linux] + libc: [glibc] requiresBuild: true dev: true optional: true @@ -2658,6 +2677,7 @@ packages: resolution: {integrity: sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw==} cpu: [x64] os: [linux] + libc: [musl] requiresBuild: true dev: true optional: true @@ -2856,6 +2876,10 @@ packages: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true + /@types/lodash@4.14.202: + resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==} + dev: true + /@types/mime@1.3.5: resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} dev: true diff --git a/src/app/components/widgets/text/text.widget.ts b/src/app/components/widgets/text/text.widget.ts index 2cc21ad..a3055fd 100644 --- a/src/app/components/widgets/text/text.widget.ts +++ b/src/app/components/widgets/text/text.widget.ts @@ -1,5 +1,5 @@ import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; -import { RegistryService } from '@/app/services/registry.service'; +import { GlobalRegistry } from '@/app/core/registry'; import { IDesignerMiniLocales } from '@/app/core/types'; @Component({ @@ -12,7 +12,7 @@ export class TextWidget implements OnChanges { currentText: string; - constructor(private registry: RegistryService) {} + constructor() {} ngOnChanges(changes: SimpleChanges): void { if (changes.title && changes.title.currentValue) { @@ -22,13 +22,13 @@ export class TextWidget implements OnChanges { fixLocaleText(text: string | IDesignerMiniLocales) { if (typeof text === 'string') { - this.currentText = this.registry.getDesignerMessage(text); + this.currentText = GlobalRegistry.getDesignerMessage(text); } else { const takeLocale = (message: string | IDesignerMiniLocales) => { if (typeof message == 'string') return message; if (typeof message == 'object') { - const lang = this.registry.getDesignerLanguage(); - for (let key in message) { + const lang = GlobalRegistry.getDesignerLanguage(); + for (const key in message) { if (key.toLocaleLowerCase() === lang) return message[key]; } return ''; diff --git a/src/app/core/externals.ts b/src/app/core/externals.ts index ead7e25..311588a 100644 --- a/src/app/core/externals.ts +++ b/src/app/core/externals.ts @@ -1,4 +1,4 @@ -import { IResource, IResourceCreator } from './types'; +import { IBehavior, IBehaviorHost, IResource, IResourceCreator } from './types'; export const createDesigner = () => {}; @@ -18,3 +18,10 @@ export const createResource = (...sources: IResourceCreator[]): IResource[] => { export const isResourceList = (val: any): val is IResource[] => Array.isArray(val) && val.every(isResource); export const isResource = (val: any): val is IResource => val?.node; + +export const isBehavior = (val: any): val is IBehavior => + val?.name || val?.selector || val?.extends || val?.designerProps || val?.designerLocales; + +export const isBehaviorHost = (val: any): val is IBehaviorHost => val?.Behavior && isBehaviorList(val.Behavior); + +export const isBehaviorList = (val: any): val is IBehavior[] => Array.isArray(val) && val.every(isBehavior); diff --git a/src/app/core/internals.ts b/src/app/core/internals.ts new file mode 100644 index 0000000..f75e928 --- /dev/null +++ b/src/app/core/internals.ts @@ -0,0 +1,34 @@ +import { isPlainObj } from '../shared/types'; +import _ from 'lodash'; +import { globalThisPolyfill } from '../shared/globalThisPolyfill'; + +export const lowerSnake = (str: string) => { + return String(str).replace(/\s+/g, '_').toLocaleLowerCase(); +}; + +export const mergeLocales = (target: any, source: any) => { + if (isPlainObj(target) && isPlainObj(source)) { + _.each(source, function (value, key) { + const token = lowerSnake(key); + const messages = mergeLocales(target[key] || target[token], value); + target[token] = messages; + }); + return target; + } else if (isPlainObj(source)) { + const result = Array.isArray(source) ? [] : {}; + _.each(source, function (value, key) { + const messages = mergeLocales(undefined, value); + result[lowerSnake(key)] = messages; + }); + return result; + } + return source; +}; + +export const getBrowserLanguage = () => { + /* istanbul ignore next */ + if (!globalThisPolyfill.navigator) { + return 'en'; + } + return globalThisPolyfill.navigator['browserlanguage'] || globalThisPolyfill.navigator?.language || 'en'; +}; diff --git a/src/app/core/models/tree-node.ts b/src/app/core/models/tree-node.ts index 4881866..3efc6a7 100644 --- a/src/app/core/models/tree-node.ts +++ b/src/app/core/models/tree-node.ts @@ -1,4 +1,9 @@ import { uid } from '@/app/shared/uid'; +import { IDesignerControllerProps, IDesignerLocales, IDesignerProps } from '@/app/core/types'; +import { GlobalRegistry } from '@/app/core/registry'; +import { action, define, observable } from '@formily/reactive'; +import { isFn } from '@/app/shared/types'; +import { mergeLocales } from '@/app/core/internals'; export interface ITreeNode { componentName?: string; @@ -11,8 +16,17 @@ export interface ITreeNode { children?: ITreeNode[]; } +export interface INodeFinder { + (node: TreeNode): boolean; +} + const TreeNodes = new Map(); +const resolveDesignerProps = (node: TreeNode, props: IDesignerControllerProps) => { + if (isFn(props)) return props(node); + return props; +}; + export class TreeNode { parent: TreeNode; @@ -56,6 +70,44 @@ export class TreeNode { } } + makeObservable() { + define(this, { + componentName: observable.ref, + props: observable, + hidden: observable.ref, + children: observable.shallow, + designerProps: observable.computed, + designerLocales: observable.computed + // wrap: action, + // prepend: action, + // append: action, + // insertAfter: action, + // insertBefore: action, + // remove: action, + // setProps: action, + // setChildren: action, + // setComponentName: action + }); + } + + get designerProps(): IDesignerProps { + const behaviors = GlobalRegistry.getDesignerBehaviors(this); + return behaviors.reduce((buf, pattern) => { + if (!pattern.designerProps) return buf; + Object.assign(buf, resolveDesignerProps(this, pattern.designerProps)); + return buf; + }, {}); + } + + get designerLocales(): IDesignerLocales { + const behaviors = GlobalRegistry.getDesignerBehaviors(this); + return behaviors.reduce((buf, pattern) => { + if (!pattern.designerLocales) return buf; + mergeLocales(buf, pattern.designerLocales); + return buf; + }, {}); + } + get previous() { if (this.parent === this || !this.parent) return null; return this.parent.children[this.index - 1]; @@ -77,4 +129,671 @@ export class TreeNode { if (this.parent === this || !this.parent) return 0; return this.parent.children.indexOf(this); } + + get descendants(): TreeNode[] { + return this.children.reduce((buf, node) => { + return buf.concat(node).concat(node.descendants); + }, []); + } + + get isRoot() { + return this === this.root; + } + + get isInOperation() { + return !!this.operation; + } + + get lastChild() { + return this.children[this.children.length - 1]; + } + + get firstChild() { + return this.children[0]; + } + + get isSourceNode() { + return this.root.isSelfSourceNode; + } + + get operation() { + return this.root?.rootOperation; + } + + get viewport() { + return this.operation?.workspace?.viewport; + } + + get outline() { + return this.operation?.workspace?.outline; + } + + get moveLayout() { + return this.viewport?.getValidNodeLayout(this); + } + + getElement(area: 'viewport' | 'outline' = 'viewport') { + return this[area]?.findElementById(this.id); + } + getValidElement(area: 'viewport' | 'outline' = 'viewport') { + return this[area]?.getValidNodeElement(this); + } + + getElementRect(area: 'viewport' | 'outline' = 'viewport') { + return this[area]?.getElementRect(this.getElement(area)); + } + + getValidElementRect(area: 'viewport' | 'outline' = 'viewport') { + return this[area]?.getValidNodeRect(this); + } + + getElementOffsetRect(area: 'viewport' | 'outline' = 'viewport') { + return this[area]?.getElementOffsetRect(this.getElement(area)); + } + + getValidElementOffsetRect(area: 'viewport' | 'outline' = 'viewport') { + return this[area]?.getValidNodeOffsetRect(this); + } + + getPrevious(step = 1) { + return this.parent.children[this.index - step]; + } + + getAfter(step = 1) { + return this.parent.children[this.index + step]; + } + + getSibling(index = 0) { + return this.parent.children[index]; + } + + getParents(node?: TreeNode): TreeNode[] { + const _node = node || this; + return _node?.parent ? [_node.parent].concat(this.getParents(_node.parent)) : []; + } + + getParentByDepth(depth = 0) { + const parent = this.parent; + if (parent?.depth === depth) { + return parent; + } else { + return parent?.getParentByDepth(depth); + } + } + + // getMessage(token: string) { + // return GlobalRegistry.getDesignerMessage(token, this.designerLocales); + // } + + isMyAncestor(node: TreeNode) { + if (node === this || this.parent === node) return false; + return node.contains(this); + } + + isMyParent(node: TreeNode) { + return this.parent === node; + } + + isMyParents(node: TreeNode) { + if (node === this) return false; + return this.isMyParent(node) || this.isMyAncestor(node); + } + + isMyChild(node: TreeNode) { + return node.isMyParent(this); + } + + isMyChildren(node: TreeNode) { + return node.isMyParents(this); + } + + takeSnapshot(type?: string) { + this.operation?.snapshot(type); + } + + triggerMutation(event: any, callback?: () => T, defaults?: T): T { + if (this.operation) { + const result = this.operation.dispatch(event, callback) || defaults; + this.takeSnapshot(event?.type); + return result; + } else if (isFn(callback)) { + return callback(); + } + return null; + } + + find(finder: INodeFinder): TreeNode { + if (finder(this)) { + return this; + } else { + let result = undefined; + + this.eachChildren(node => { + if (finder(node)) { + result = node; + return false; + } + return null; + }); + return result; + } + } + + findAll(finder: INodeFinder): TreeNode[] { + const results = []; + if (finder(this)) { + results.push(this); + } + this.eachChildren(node => { + if (finder(node)) { + results.push(node); + } + }); + return results; + } + + distanceTo(node: TreeNode) { + if (this.root !== node.root) { + return Infinity; + } + if (this.parent !== node.parent) { + return Infinity; + } + return Math.abs(this.index - node.index); + } + + crossSiblings(node: TreeNode): TreeNode[] { + if (this.parent !== node.parent) return []; + const minIndex = Math.min(this.index, node.index); + const maxIndex = Math.max(this.index, node.index); + const results = []; + for (let i = minIndex + 1; i < maxIndex; i++) { + results.push(this.parent.children[i]); + } + return results; + } + + allowSibling(nodes: TreeNode[]) { + if (this.designerProps?.allowSiblings?.(this, nodes) === false) return false; + return this.parent?.allowAppend(nodes); + } + + allowDrop(parent: TreeNode) { + if (!isFn(this.designerProps.allowDrop)) return true; + return this.designerProps.allowDrop(parent); + } + + allowAppend(nodes: TreeNode[]) { + if (!this.designerProps?.droppable) return false; + if (this.designerProps?.allowAppend?.(this, nodes) === false) return false; + if (nodes.some(node => !node.allowDrop(this))) return false; + if (this.root === this) return true; + return true; + } + + allowClone() { + if (this === this.root) return false; + return this.designerProps.cloneable ?? true; + } + + allowDrag() { + if (this === this.root && !this.isSourceNode) return false; + return this.designerProps.draggable ?? true; + } + + allowResize(): false | Array<'x' | 'y'> { + if (this === this.root && !this.isSourceNode) return false; + const { resizable } = this.designerProps; + if (!resizable) return false; + if (resizable.width && resizable.height) return ['x', 'y']; + if (resizable.width) return ['x']; + return ['y']; + } + + allowRotate() {} + + allowRound() {} + + allowScale() {} + + allowTranslate(): boolean { + if (this === this.root && !this.isSourceNode) return false; + const { translatable } = this.designerProps; + if (translatable?.x && translatable?.y) return true; + return false; + } + + allowDelete() { + if (this === this.root) return false; + return this.designerProps.deletable ?? true; + } + + findById(id: string) { + if (!id) return null; + if (this.id === id) return this; + if (this.children?.length > 0) { + return TreeNodes.get(id); + } + return null; + } + + contains(...nodes: TreeNode[]) { + return nodes.every(node => { + if (node === this || node?.parent === this || node?.getParentByDepth(this.depth) === this) { + return true; + } + return false; + }); + } + + eachTree(callback?: (node: TreeNode) => void | boolean) { + if (isFn(callback)) { + callback(this.root); + this.root?.eachChildren(callback); + } + } + + eachChildren(callback?: (node: TreeNode) => void | boolean) { + if (isFn(callback)) { + for (let i = 0; i < this.children.length; i++) { + const node = this.children[i]; + if (callback(node) === false) return; + node.eachChildren(callback); + } + } + } + + // resetNodesParent(nodes: TreeNode[], parent: TreeNode) { + // return resetNodesParent( + // nodes.filter(node => node !== this), + // parent + // ); + // } + + // setProps(props?: any) { + // return this.triggerMutation( + // new UpdateNodePropsEvent({ + // target: this, + // source: null + // }), + // () => { + // Object.assign(this.props, props); + // } + // ); + // } + + setComponentName(componentName: string) { + this.componentName = componentName; + } + + // prepend(...nodes: TreeNode[]) { + // if (nodes.some(node => node.contains(this))) return []; + // const originSourceParents = nodes.map(node => node.parent); + // const newNodes = this.resetNodesParent(nodes, this); + // if (!newNodes.length) return []; + // return this.triggerMutation( + // new PrependNodeEvent({ + // originSourceParents, + // target: this, + // source: newNodes + // }), + // () => { + // this.children = newNodes.concat(this.children); + // return newNodes; + // }, + // [] + // ); + // } + + // append(...nodes: TreeNode[]) { + // if (nodes.some(node => node.contains(this))) return []; + // const originSourceParents = nodes.map(node => node.parent); + // const newNodes = this.resetNodesParent(nodes, this); + // if (!newNodes.length) return []; + // return this.triggerMutation( + // new AppendNodeEvent({ + // originSourceParents, + // target: this, + // source: newNodes + // }), + // () => { + // this.children = this.children.concat(newNodes); + // return newNodes; + // }, + // [] + // ); + // } + + // wrap(wrapper: TreeNode) { + // if (wrapper === this) return; + // const parent = this.parent; + // return this.triggerMutation( + // new WrapNodeEvent({ + // target: this, + // source: wrapper + // }), + // () => { + // resetParent(this, wrapper); + // resetParent(wrapper, parent); + // return wrapper; + // } + // ); + // } + + // insertAfter(...nodes: TreeNode[]) { + // const parent = this.parent; + // if (nodes.some(node => node.contains(this))) return []; + // if (parent?.children?.length) { + // const originSourceParents = nodes.map(node => node.parent); + // const newNodes = this.resetNodesParent(nodes, parent); + // if (!newNodes.length) return []; + // + // return this.triggerMutation( + // new InsertAfterEvent({ + // originSourceParents, + // target: this, + // source: newNodes + // }), + // () => { + // parent.children = parent.children.reduce((buf, node) => { + // if (node === this) { + // return buf.concat([node]).concat(newNodes); + // } else { + // return buf.concat([node]); + // } + // }, []); + // return newNodes; + // }, + // [] + // ); + // } + // return []; + // } + + // insertBefore(...nodes: TreeNode[]) { + // const parent = this.parent; + // if (nodes.some(node => node.contains(this))) return []; + // if (parent?.children?.length) { + // const originSourceParents = nodes.map(node => node.parent); + // const newNodes = this.resetNodesParent(nodes, parent); + // if (!newNodes.length) return []; + // return this.triggerMutation( + // new InsertBeforeEvent({ + // originSourceParents, + // target: this, + // source: newNodes + // }), + // () => { + // parent.children = parent.children.reduce((buf, node) => { + // if (node === this) { + // return buf.concat(newNodes).concat([node]); + // } else { + // return buf.concat([node]); + // } + // }, []); + // return newNodes; + // }, + // [] + // ); + // } + // return []; + // } + + // insertChildren(start: number, ...nodes: TreeNode[]) { + // if (nodes.some(node => node.contains(this))) return []; + // if (this.children?.length) { + // const originSourceParents = nodes.map(node => node.parent); + // const newNodes = this.resetNodesParent(nodes, this); + // if (!newNodes.length) return []; + // return this.triggerMutation( + // new InsertChildrenEvent({ + // originSourceParents, + // target: this, + // source: newNodes + // }), + // () => { + // this.children = this.children.reduce((buf, node, index) => { + // if (index === start) { + // return buf.concat(newNodes).concat([node]); + // } + // return buf.concat([node]); + // }, []); + // return newNodes; + // }, + // [] + // ); + // } + // return []; + // } + + // setChildren(...nodes: TreeNode[]) { + // const originSourceParents = nodes.map(node => node.parent); + // const newNodes = this.resetNodesParent(nodes, this); + // return this.triggerMutation( + // new UpdateChildrenEvent({ + // originSourceParents, + // target: this, + // source: newNodes + // }), + // () => { + // this.children = newNodes; + // return newNodes; + // }, + // [] + // ); + // } + + /** + * @deprecated + * please use `setChildren` + */ + // setNodeChildren(...nodes: TreeNode[]) { + // return this.setChildren(...nodes); + // } + + // remove() { + // return this.triggerMutation( + // new RemoveNodeEvent({ + // target: this, + // source: null + // }), + // () => { + // removeNode(this); + // TreeNodes.delete(this.id); + // } + // ); + // } + // + // clone(parent?: TreeNode) { + // const newNode = new TreeNode( + // { + // id: uid(), + // componentName: this.componentName, + // sourceName: this.sourceName, + // props: toJS(this.props), + // children: [] + // }, + // parent ? parent : this.parent + // ); + // newNode.children = resetNodesParent( + // this.children.map(child => { + // return child.clone(newNode); + // }), + // newNode + // ); + // return this.triggerMutation( + // new CloneNodeEvent({ + // target: this, + // source: newNode + // }), + // () => newNode + // ); + // } + // + // from(node?: ITreeNode) { + // if (!node) return; + // return this.triggerMutation( + // new FromNodeEvent({ + // target: this, + // source: node + // }), + // () => { + // if (node.id && node.id !== this.id) { + // TreeNodes.delete(this.id); + // TreeNodes.set(node.id, this); + // this.id = node.id; + // } + // if (node.componentName) { + // this.componentName = node.componentName; + // } + // this.props = node.props ?? {}; + // if (node.hidden) { + // this.hidden = node.hidden; + // } + // if (node.children) { + // this.children = + // node.children?.map?.(node => { + // return new TreeNode(node, this); + // }) || []; + // } + // } + // ); + // } + + serialize(): ITreeNode { + return { + id: this.id, + componentName: this.componentName, + sourceName: this.sourceName, + props: this.props, + hidden: this.hidden, + children: this.children.map(treeNode => { + return treeNode.serialize(); + }) + }; + } + + static create(node: ITreeNode, parent?: TreeNode) { + return new TreeNode(node, parent); + } + + static findById(id: string) { + return TreeNodes.get(id); + } + + // static remove(nodes: TreeNode[] = []) { + // for (let i = nodes.length - 1; i >= 0; i--) { + // const node = nodes[i]; + // if (node.allowDelete()) { + // const previous = node.previous; + // const next = node.next; + // node.remove(); + // node.operation?.selection.select(previous ? previous : next ? next : node.parent); + // node.operation?.hover.clear(); + // } + // } + // } + + static sort(nodes: TreeNode[] = []) { + return nodes.sort((before, after) => { + if (before.depth !== after.depth) return 0; + return before.index - after.index >= 0 ? 1 : -1; + }); + } + + // static clone(nodes: TreeNode[] = []) { + // const groups: { [parentId: string]: TreeNode[] } = {}; + // const lastGroupNode: { [parentId: string]: TreeNode } = {}; + // const filterNestedNode = TreeNode.sort(nodes).filter(node => { + // return !nodes.some(parent => { + // return node.isMyParents(parent); + // }); + // }); + // _.each(filterNestedNode, node => { + // if (node === node.root) return; + // if (!node.allowClone()) return; + // if (!node?.operation) return; + // groups[node?.parent?.id] = groups[node?.parent?.id] || []; + // groups[node?.parent?.id].push(node); + // if (lastGroupNode[node?.parent?.id]) { + // if (node.index > lastGroupNode[node?.parent?.id].index) { + // lastGroupNode[node?.parent?.id] = node; + // } + // } else { + // lastGroupNode[node?.parent?.id] = node; + // } + // }); + // const parents = new Map(); + // _.each(groups, (nodes, parentId) => { + // const lastNode = lastGroupNode[parentId]; + // let insertPoint = lastNode; + // _.each(nodes, node => { + // const cloned = node.clone(); + // if (!cloned) return; + // if (node.operation?.selection.has(node) && insertPoint.parent.allowAppend([cloned])) { + // insertPoint.insertAfter(cloned); + // insertPoint = insertPoint.next; + // } else if (node.operation.selection.length === 1) { + // const targetNode = node.operation?.tree.findById(node.operation.selection.first); + // let cloneNodes = parents.get(targetNode); + // if (!cloneNodes) { + // cloneNodes = []; + // parents.set(targetNode, cloneNodes); + // } + // if (targetNode && targetNode.allowAppend([cloned])) { + // cloneNodes.push(cloned); + // } + // } + // }); + // }); + // parents.forEach((nodes, target) => { + // if (!nodes.length) return; + // target.append(...nodes); + // }); + // } + + static filterResizable(nodes: TreeNode[] = []) { + return nodes.filter(node => node.allowResize()); + } + + static filterRotatable(nodes: TreeNode[] = []) { + return nodes.filter(node => node.allowRotate()); + } + + static filterScalable(nodes: TreeNode[] = []) { + return nodes.filter(node => node.allowScale()); + } + + static filterRoundable(nodes: TreeNode[] = []) { + return nodes.filter(node => node.allowRound()); + } + + static filterTranslatable(nodes: TreeNode[] = []) { + return nodes.filter(node => node.allowTranslate()); + } + + static filterDraggable(nodes: TreeNode[] = []) { + return nodes.reduce((buf, node) => { + if (!node.allowDrag()) return buf; + if (isFn(node?.designerProps?.getDragNodes)) { + const transformed = node.designerProps.getDragNodes(node); + return transformed ? buf.concat(transformed) : buf; + } + if (node.componentName === '$$ResourceNode$$') return buf.concat(node.children); + return buf.concat([node]); + }, []); + } + + // static filterDroppable(nodes: TreeNode[] = [], parent: TreeNode) { + // return nodes.reduce((buf, node) => { + // if (!node.allowDrop(parent)) return buf; + // if (isFn(node.designerProps?.getDropNodes)) { + // const cloned = node.isSourceNode ? node.clone(node.parent) : node; + // const transformed = node.designerProps.getDropNodes(cloned, parent); + // return transformed ? buf.concat(transformed) : buf; + // } + // if (node.componentName === '$$ResourceNode$$') return buf.concat(node.children); + // return buf.concat([node]); + // }, []); + // } } diff --git a/src/app/core/registry.ts b/src/app/core/registry.ts new file mode 100644 index 0000000..1e35c13 --- /dev/null +++ b/src/app/core/registry.ts @@ -0,0 +1,139 @@ +import _ from 'lodash'; +import { + IBehavior, + IBehaviorLike, + IDesignerBehaviors, + IDesignerBehaviorStore, + IDesignerIcons, + IDesignerIconsStore, + IDesignerLanguageStore, + IDesignerLocales, + IDesignerLocaleStore +} from './types'; +import { JSONPath } from 'jsonpath-plus'; +import icons from '../locales/icons'; +import panels from '../locales/panels'; +import operations from '../locales/operations'; +import global from '../locales/global'; + +import { observable } from '@formily/reactive'; +import { isBehaviorHost, isBehaviorList } from '@/app/core/externals'; +import { getBrowserLanguage, lowerSnake, mergeLocales } from '@/app/core/internals'; +import { TreeNode } from '@/app/core/models'; + +const reSortBehaviors = (target: IBehavior[], sources: IDesignerBehaviors) => { + const findTargetBehavior = (behavior: IBehavior) => target.includes(behavior); + const findSourceBehavior = (name: string) => { + for (const key in sources) { + const { Behavior } = sources[key]; + for (let i = 0; i < Behavior.length; i++) { + if (Behavior[i].name === name) return Behavior[i]; + } + } + return null; + }; + _.each(sources, item => { + if (!item) return; + if (!isBehaviorHost(item)) return; + const { Behavior } = item; + _.each(Behavior, behavior => { + if (findTargetBehavior(behavior)) return; + const name = behavior.name; + _.each(behavior.extends, dep => { + const behavior = findSourceBehavior(dep); + if (!behavior) throw new Error(`No ${dep} behavior that ${name} depends on`); + if (!findTargetBehavior(behavior)) { + target.unshift(behavior); + } + }); + target.push(behavior); + }); + }); +}; + +const getISOCode = (language: string) => { + let isoCode = DESIGNER_LANGUAGE_STORE.value; + const lang = lowerSnake(language); + if (DESIGNER_LOCALES_STORE.value[lang]) { + return lang; + } + _.each(DESIGNER_LOCALES_STORE.value, (_, key: string) => { + if (key.indexOf(lang) > -1 || String(lang).indexOf(key) > -1) { + isoCode = key; + return false; + } + return undefined; + }); + return isoCode; +}; + +const DESIGNER_BEHAVIORS_STORE: IDesignerBehaviorStore = observable.ref([]); + +const DESIGNER_ICONS_STORE: IDesignerIconsStore = observable.ref({}); + +const DESIGNER_LOCALES_STORE: IDesignerLocaleStore = observable.ref({}); + +const DESIGNER_LANGUAGE_STORE: IDesignerLanguageStore = observable.ref(getBrowserLanguage()); +export const GlobalRegistry = { + setDesignerLanguage: (lang: string) => { + DESIGNER_LANGUAGE_STORE.value = lang; + }, + + setDesignerBehaviors: (behaviors: IBehaviorLike[]) => { + DESIGNER_BEHAVIORS_STORE.value = behaviors.reduce((buf, behavior) => { + if (isBehaviorHost(behavior)) { + return buf.concat(behavior.Behavior); + } else if (isBehaviorList(behavior)) { + return buf.concat(behavior); + } + return buf; + }, []); + }, + + getDesignerBehaviors: (node: TreeNode) => { + return DESIGNER_BEHAVIORS_STORE.value.filter(pattern => pattern.selector(node)); + }, + + getDesignerIcon: (name: string) => { + return DESIGNER_ICONS_STORE[name]; + }, + + getDesignerLanguage: () => { + return getISOCode(DESIGNER_LANGUAGE_STORE.value); + }, + + getDesignerMessage: (token: string, locales?: IDesignerLocales) => { + const lang = getISOCode(DESIGNER_LANGUAGE_STORE.value); + const locale = locales ? locales[lang] : DESIGNER_LOCALES_STORE.value[lang]; + if (!locale) { + for (const key in DESIGNER_LOCALES_STORE.value) { + const message = JSONPath({ json: DESIGNER_LOCALES_STORE.value[key], path: lowerSnake(token) }); + if (message) return message; + } + return; + } + return JSONPath({ json: locale, path: lowerSnake(token) }); + }, + + registerDesignerIcons: (map: IDesignerIcons) => { + Object.assign(DESIGNER_ICONS_STORE, map); + }, + + registerDesignerLocales: (...packages: IDesignerLocales[]) => { + packages.forEach(locales => { + mergeLocales(DESIGNER_LOCALES_STORE.value, locales); + }); + }, + + registerDesignerBehaviors: (...packages: IDesignerBehaviors[]) => { + const results: IBehavior[] = []; + packages.forEach(sources => { + reSortBehaviors(results, sources); + }); + if (results.length) { + DESIGNER_BEHAVIORS_STORE.value = results; + } + } +}; + +GlobalRegistry.registerDesignerLocales(icons, panels, global, operations); diff --git a/src/app/core/types.ts b/src/app/core/types.ts index c399f53..0b34de9 100644 --- a/src/app/core/types.ts +++ b/src/app/core/types.ts @@ -1,4 +1,4 @@ -import { Engine } from '@/app/core/models'; +import { Engine, TreeNode } from '@/app/core/models'; export type IEngineContext = { workspace: any; @@ -36,3 +36,95 @@ export interface IResourceCreator { span?: number; elements?: any; } + +export type IResizable = { + width?: ( + node: TreeNode, + element: Element + ) => { + plus: () => void; + minus: () => void; + }; + height?: ( + node: TreeNode, + element: Element + ) => { + plus: () => void; + minus: () => void; + }; +}; + +export type ITranslate = { + x: ( + node: TreeNode, + element: HTMLElement, + diffX: string | number + ) => { + translate: () => void; + }; + y: ( + node: TreeNode, + element: HTMLElement, + diffY: string | number + ) => { + translate: () => void; + }; +}; + +export interface IDesignerProps { + package?: string; //npm包名 + registry?: string; //web npm注册平台地址 + version?: string; //semver版本号 + path?: string; //组件模块路径 + title?: string; //标题 + description?: string; //描述 + icon?: string; //icon + droppable?: boolean; //是否可作为拖拽容器,默认为true + draggable?: boolean; //是否可拖拽,默认为true + deletable?: boolean; //是否可删除,默认为true + cloneable?: boolean; //是否可拷贝,默认为true + resizable?: IResizable; + translatable?: ITranslate; // 自由布局 + inlineChildrenLayout?: boolean; //子节点内联,用于指定复杂布局容器,强制内联 + selfRenderChildren?: boolean; //是否自己渲染子节点 + propsSchema?: { [p: string]: any }; + defaultProps?: any; //默认属性 + getDragNodes?: (node: TreeNode) => TreeNode | TreeNode[]; //拦截转换Drag节点 + getDropNodes?: (node: TreeNode, parent: TreeNode) => TreeNode | TreeNode[]; //拦截转换Drop节点 + getComponentProps?: (node: TreeNode) => any; //拦截属性 + allowAppend?: (target: TreeNode, sources?: TreeNode[]) => boolean; + allowSiblings?: (target: TreeNode, sources?: TreeNode[]) => boolean; + allowDrop?: (target: TreeNode) => boolean; + [key: string]: any; +} + +export interface IDesignerStore

{ + value: P; +} + +export type IDesignerControllerProps = IDesignerProps | ((node: TreeNode) => IDesignerProps); + +export interface IBehavior { + name: string; + extends?: string[]; + selector: (node: TreeNode) => boolean; + designerProps?: IDesignerControllerProps; + designerLocales?: IDesignerLocales; +} + +export interface IBehaviorHost { + Behavior?: IBehavior[]; +} + +export interface IDesignerBehaviors { + [key: string]: IBehaviorHost; +} + +export type IBehaviorLike = IBehavior[] | IBehaviorHost; + +export type IDesignerIcons = Record; + +export type IDesignerIconsStore = IDesignerStore; +export type IDesignerLocaleStore = IDesignerStore; +export type IDesignerBehaviorStore = IDesignerStore; +export type IDesignerLanguageStore = IDesignerStore; diff --git a/src/app/pages/home/home.component.ts b/src/app/pages/home/home.component.ts index c8d49be..ac36eb2 100644 --- a/src/app/pages/home/home.component.ts +++ b/src/app/pages/home/home.component.ts @@ -10,9 +10,9 @@ import { import { WorkspacePanelComponent } from '@/app/components/panels/workspace-panel.component'; import { SettingPanelComponent } from '@/app/components/panels/setting-panel.component'; import { ResourceWidget } from '@/app/components/widgets/resource/resource.widget'; -import { RegistryService } from '@/app/services/registry.service'; +import { GlobalRegistry } from '@/app/core/registry'; import { createResource } from '@/app/core/externals'; -import { IResource, IResourceLike } from '@/app/core/types'; +import { IResourceLike } from '@/app/core/types'; @Component({ selector: 'app-home', @@ -33,7 +33,7 @@ import { IResource, IResourceLike } from '@/app/core/types'; }) export class HomeComponent implements OnInit { resourceList: IResourceLike[] = []; - constructor(private registryService: RegistryService) {} + constructor() {} ngOnInit(): void { this.registerLocales(); @@ -41,7 +41,7 @@ export class HomeComponent implements OnInit { } registerLocales() { - this.registryService.registerDesignerLocales({ + GlobalRegistry.registerDesignerLocales({ 'zh-CN': { sources: { Inputs: '输入控件', diff --git a/src/app/services/registry.service.ts b/src/app/services/registry.service.ts deleted file mode 100644 index cf7619f..0000000 --- a/src/app/services/registry.service.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { Injectable } from '@angular/core'; -import { globalThisPolyfill } from '../shared/globalThisPolyfill'; -import _ from 'lodash'; -import { IDesignerLocales } from '../core/types'; -import { JSONPath } from 'jsonpath-plus'; -import icons from '../locales/icons'; -import panels from '../locales/panels'; -import operations from '../locales/operations'; -import global from '../locales/global'; - -@Injectable({ - providedIn: 'root' -}) -export class RegistryService { - DESIGNER_LOCALES_STORE = {}; - - DESIGNER_LANGUAGE_STORE: string; - - constructor() { - this.registerDesignerLocales(icons, panels, global, operations); - this.DESIGNER_LANGUAGE_STORE = this.getBrowserLanguage(); - } - - lowerSnake = (str: string) => { - return String(str).replace(/\s+/g, '_').toLocaleLowerCase(); - }; - - getBrowserLanguage = () => { - /* istanbul ignore next */ - if (!globalThisPolyfill.navigator) { - return 'en'; - } - return globalThisPolyfill.navigator['browserlanguage'] || globalThisPolyfill.navigator?.language || 'en'; - }; - - getISOCode = (language: string) => { - let isoCode = this.DESIGNER_LOCALES_STORE; - const lang = this.lowerSnake(language); - if (this.DESIGNER_LOCALES_STORE[lang]) { - return lang; - } - _.each(this.DESIGNER_LOCALES_STORE, (_, key: string) => { - if (key.indexOf(lang) > -1 || String(lang).indexOf(key) > -1) { - isoCode = key; - } - }); - return isoCode; - }; - - registerDesignerLocales = (...packages: IDesignerLocales[]) => { - packages.forEach(locale => { - this.mergeLocales(this.DESIGNER_LOCALES_STORE, locale); - }); - }; - - setDesignerLanguage = (lang: string) => { - this.DESIGNER_LANGUAGE_STORE = lang; - }; - - getDesignerLanguage = () => { - return this.getISOCode(this.DESIGNER_LANGUAGE_STORE); - }; - - getDesignerMessage = (token: string, locales?: IDesignerLocales) => { - const language = this.getBrowserLanguage(); - - const lang = this.getISOCode(language) as any; - const locale = locales ? locales[lang] : this.DESIGNER_LOCALES_STORE[lang]; - if (!locale) { - for (const key in this.DESIGNER_LOCALES_STORE) { - const message = JSONPath({ json: this.DESIGNER_LOCALES_STORE[key], path: this.lowerSnake(token) }); - if (message) return message; - } - return; - } - return JSONPath({ json: locale, path: this.lowerSnake(token) }); - }; - - isPlainObj = (obj: any) => typeof obj == 'object'; - - mergeLocales = (target: any, source: any) => { - if (this.isPlainObj(target) && this.isPlainObj(source)) { - _.each(source, (value, key) => { - const token = this.lowerSnake(key); - target[token] = this.mergeLocales(target[key] || target[token], value); - }); - return target; - } else if (this.isPlainObj(source)) { - const result = Array.isArray(source) ? [] : {}; - _.each(source, (value, key) => { - result[this.lowerSnake(key)] = this.mergeLocales(undefined, value); - }); - return result; - } - return source; - }; -} diff --git a/src/app/shared/types.ts b/src/app/shared/types.ts new file mode 100644 index 0000000..0ed83e7 --- /dev/null +++ b/src/app/shared/types.ts @@ -0,0 +1,19 @@ +const isType = + (type: string | string[]) => + (obj: unknown): obj is T => + obj != null && (Array.isArray(type) ? type : [type]).some(t => getType(obj) === `[object ${t}]`); +export const getType = (obj: any) => Object.prototype.toString.call(obj); +export const isFn = isType<(...args: any[]) => any>(['Function', 'AsyncFunction', 'GeneratorFunction']); +export const isWindow = isType('Window'); +export const isHTMLElement = (obj: any): obj is HTMLElement => { + return obj?.['nodeName'] || obj?.['tagName']; +}; +export const isArr = Array.isArray; +export const isPlainObj = isType('Object'); +export const isStr = isType('String'); +export const isBool = isType('Boolean'); +export const isNum = isType('Number'); +export const isObj = (val: unknown): val is object => typeof val === 'object'; +export const isRegExp = isType('RegExp'); +export const isValid = (val: any) => val !== null && val !== undefined; +export const isValidNumber = (val: any): val is number => !isNaN(val) && isNum(val);