diff --git a/src/app/app.config.ts b/src/app/app.config.ts index e0365cb..16edece 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -5,6 +5,8 @@ import { routes } from './app.routes'; import { Engine } from '@/app/core/models'; import { createDesigner } from '@/app/core/externals'; +const engine = createDesigner(); + export const appConfig: ApplicationConfig = { - providers: [provideRouter(routes), { provide: Engine, useFactory: createDesigner }] + providers: [provideRouter(routes), { provide: Engine, useValue: engine }] }; diff --git a/src/app/components/container/designer.component.ts b/src/app/components/container/designer.component.ts index 0c3a22e..2cf232e 100644 --- a/src/app/components/container/designer.component.ts +++ b/src/app/components/container/designer.component.ts @@ -1,26 +1,33 @@ -import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; import { SharedModule } from '@/app/shared/shared.module'; import { APP_PREFIX } from '@/app/constant/constant'; +import { Engine } from '@/app/core/models'; +import { GhostWidget } from '@/app/components/widgets/ghost/ghost.widget'; @Component({ selector: 'app-designer', standalone: true, - imports: [SharedModule], + imports: [SharedModule, GhostWidget], template: `
+
`, styles: [``], changeDetection: ChangeDetectionStrategy.OnPush }) -export class DesignerComponent implements OnInit, OnChanges { +export class DesignerComponent implements OnInit, OnChanges, OnDestroy { @Input() prefixClass = APP_PREFIX; @Input() theme: 'light' | 'dark' = 'light'; classNameList: string[] = []; + constructor(private engine: Engine) { + this.engine.mount(); + } + ngOnChanges(changes: SimpleChanges): void { if (changes.prefixClass && changes.prefixClass.currentValue) { this.createClass(); @@ -34,6 +41,10 @@ export class DesignerComponent implements OnInit, OnChanges { this.createClass(); } + ngOnDestroy(): void { + this.engine.unmount(); + } + private createClass() { this.classNameList = [this.prefixClass + 'app', this.prefixClass + this.theme]; } diff --git a/src/app/components/container/workspace.component.ts b/src/app/components/container/workspace.component.ts index 50cee01..685741c 100644 --- a/src/app/components/container/workspace.component.ts +++ b/src/app/components/container/workspace.component.ts @@ -1,4 +1,6 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { uid } from '@/app/shared/uid'; +import { Engine } from '@/app/core/models'; @Component({ selector: 'app-workspace', @@ -12,4 +14,12 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; styles: [``], changeDetection: ChangeDetectionStrategy.OnPush }) -export class WorkspaceComponent {} +export class WorkspaceComponent implements OnInit { + constructor(private designer: Engine) {} + ngOnInit(): void { + const workspace = { + id: uid() + }; + this.designer.workbench.ensureWorkspace(workspace); + } +} diff --git a/src/app/components/widgets/ghost/ghost.widget.ts b/src/app/components/widgets/ghost/ghost.widget.ts index c0c48de..a6cc0bd 100644 --- a/src/app/components/widgets/ghost/ghost.widget.ts +++ b/src/app/components/widgets/ghost/ghost.widget.ts @@ -1,30 +1,65 @@ -import { ChangeDetectionStrategy, Component, Optional } from '@angular/core'; -import { usePrefix } from '../../../utils'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, OnInit, ViewChild } from '@angular/core'; +import { usePrefix } from '@/app/utils'; import { NodeTitleWidget } from '../node-title/node-title.widget'; -import { Engine } from '../../../core/models'; -import { Cursor, CursorStatus } from '../../../core/models/cursor'; +import { Engine, TreeNode } from '@/app/core/models'; +import { Cursor, CursorStatus } from '@/app/core/models/cursor'; +import { autorun } from '@formily/reactive'; @Component({ selector: 'app-ghost', standalone: true, template: ` @if (cursor.status === CursorStatus.Dragging) { -
+
- + + {{ movingNodes?.length > 1 ? '...' : '' }}
} `, imports: [NodeTitleWidget], + styleUrls: ['./ghost.widget.less'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class GhostWidget { +export class GhostWidget implements OnInit { + @ViewChild('containerRef') containerRef: ElementRef; + prefix = usePrefix('ghost'); - constructor( - @Optional() public designer: Engine, - @Optional() public cursor: Cursor - ) {} protected readonly CursorStatus = CursorStatus; + + cursor: Cursor; + + movingNodes: TreeNode[]; + + firstNode: TreeNode; + + constructor( + public designer: Engine, + private cdr: ChangeDetectorRef + ) { + this.cursor = this.designer.cursor; + } + + ngOnInit(): void { + window.addEventListener('mousedown', () => { + setTimeout(() => { + this.movingNodes = this.designer.findMovingNodes(); + console.log(this.movingNodes); + this.firstNode = this.movingNodes[0]; + console.log(this.firstNode); + }, 200); + }); + autorun(() => { + // console.log('cursor status>>>', this.cursor.status); + this.cdr.markForCheck(); + const transform = `perspective(1px) translate3d(${ + this.cursor.position?.topClientX - 18 + }px,${this.cursor.position?.topClientY - 12}px,0) scale(0.8)`; + if (!this.containerRef) return; + // console.log('autorun', this.containerRef); + this.containerRef.nativeElement.style.transform = transform; + }); + } } diff --git a/src/app/components/widgets/node-title/node-title.widget.ts b/src/app/components/widgets/node-title/node-title.widget.ts index df406e1..1153736 100644 --- a/src/app/components/widgets/node-title/node-title.widget.ts +++ b/src/app/components/widgets/node-title/node-title.widget.ts @@ -1,20 +1,22 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } from '@angular/core'; -import { TreeNode } from '../../../core/models'; +import { TreeNode } from '@/app/core/models'; @Component({ selector: 'app-node-title-widget', standalone: true, - template: ` {{ node.getMessage('title') || node.componentName }} `, + template: ` {{ currentTitle }} `, changeDetection: ChangeDetectionStrategy.OnPush }) export class NodeTitleWidget implements OnChanges { @Input() node: TreeNode; - currentNode: TreeNode; + currentTitle: string; ngOnChanges(changes: SimpleChanges): void { if (changes.node && changes.node.currentValue) { - this.currentNode = this.takeNode(this.node); + const node = this.takeNode(this.node); + const message = node.getMessage('title'); + this.currentTitle = message ? message : node.componentName; } } diff --git a/src/app/core/drivers/drag-drop-driver.ts b/src/app/core/drivers/drag-drop-driver.ts index 59fc4d3..5efc9b1 100644 --- a/src/app/core/drivers/drag-drop-driver.ts +++ b/src/app/core/drivers/drag-drop-driver.ts @@ -15,6 +15,7 @@ export class DragDropDriver extends EventDriver { startEvent: MouseEvent; onMouseDown = (e: MouseEvent) => { + console.log('onMouseDown>>>', e); if (e.button !== 0 || e.ctrlKey || e.metaKey) { return false; } diff --git a/src/app/core/drivers/index.ts b/src/app/core/drivers/index.ts new file mode 100644 index 0000000..ac0d9ce --- /dev/null +++ b/src/app/core/drivers/index.ts @@ -0,0 +1,2 @@ +export * from './drag-drop-driver'; +export * from './mouse-move-driver'; diff --git a/src/app/core/drivers/mouse-move-driver.ts b/src/app/core/drivers/mouse-move-driver.ts new file mode 100644 index 0000000..8cc48f8 --- /dev/null +++ b/src/app/core/drivers/mouse-move-driver.ts @@ -0,0 +1,35 @@ +import { EventDriver } from '../../shared/event'; +import { Engine } from '../models'; +import { MouseMoveEvent } from '../events/cursor'; + +export class MouseMoveDriver extends EventDriver { + request = null; + + onMouseMove = (e: MouseEvent) => { + this.request = requestAnimationFrame(() => { + cancelAnimationFrame(this.request); + this.dispatch( + new MouseMoveEvent({ + clientX: e.clientX, + clientY: e.clientY, + pageX: e.pageX, + pageY: e.pageY, + target: e.target, + view: e.view + }) + ); + }); + }; + + override attach() { + this.addEventListener('mousemove', this.onMouseMove, { + mode: 'onlyOne' + }); + } + + override detach() { + this.removeEventListener('mouseover', this.onMouseMove, { + mode: 'onlyOne' + }); + } +} diff --git a/src/app/core/effects/index.ts b/src/app/core/effects/index.ts new file mode 100644 index 0000000..12a1362 --- /dev/null +++ b/src/app/core/effects/index.ts @@ -0,0 +1,2 @@ +export * from './useDragDropEffect'; +export * from './useCursorEffect'; diff --git a/src/app/core/effects/useCursorEffect.ts b/src/app/core/effects/useCursorEffect.ts new file mode 100644 index 0000000..703fe07 --- /dev/null +++ b/src/app/core/effects/useCursorEffect.ts @@ -0,0 +1,57 @@ +import { Engine } from '../models'; +import { DragMoveEvent, DragStartEvent, DragStopEvent, MouseMoveEvent } from '../events/cursor'; +import { CursorStatus } from '../models/cursor'; +import { requestIdle } from '../../shared/request-idle'; + +export const useCursorEffect = (engine: Engine) => { + engine.subscribeTo(MouseMoveEvent, event => { + engine.cursor.setStatus( + engine.cursor.status === CursorStatus.Dragging || engine.cursor.status === CursorStatus.DragStart + ? engine.cursor.status + : CursorStatus.Normal + ); + if (engine.cursor.status === CursorStatus.Dragging) return; + engine.cursor.setPosition(event.data); + }); + engine.subscribeTo(DragStartEvent, event => { + engine.cursor.setStatus(CursorStatus.DragStart); + engine.cursor.setDragStartPosition(event.data); + }); + engine.subscribeTo(DragMoveEvent, event => { + engine.cursor.setStatus(CursorStatus.Dragging); + engine.cursor.setPosition(event.data); + }); + engine.subscribeTo(DragStopEvent, event => { + engine.cursor.setStatus(CursorStatus.DragStop); + engine.cursor.setDragEndPosition(event.data); + engine.cursor.setDragStartPosition(null); + requestIdle(() => { + engine.cursor.setStatus(CursorStatus.Normal); + }); + }); + engine.subscribeTo(MouseMoveEvent, event => { + const currentWorkspace = event?.context?.workspace; + if (!currentWorkspace) return; + const operation = currentWorkspace.operation; + if (engine.cursor.status !== CursorStatus.Normal) { + operation.hover.clear(); + return; + } + const target = event.data.target as HTMLElement; + const el = target?.closest?.(` + *[${engine.props.nodeIdAttrName}], + *[${engine.props.outlineNodeIdAttrName}] + `); + if (!el?.getAttribute) { + return; + } + const nodeId = el.getAttribute(engine.props.nodeIdAttrName); + const outlineNodeId = el.getAttribute(engine.props.outlineNodeIdAttrName); + const node = operation.tree.findById(nodeId || outlineNodeId); + if (node) { + operation.hover.setHover(node); + } else { + operation.hover.clear(); + } + }); +}; diff --git a/src/app/core/events/workbench/AbstractWorkspaceEvent.ts b/src/app/core/events/workbench/AbstractWorkspaceEvent.ts new file mode 100644 index 0000000..0e6c4a2 --- /dev/null +++ b/src/app/core/events/workbench/AbstractWorkspaceEvent.ts @@ -0,0 +1,10 @@ +import { IEngineContext } from '../../types'; +import { Workspace } from '../../models/workspace'; + +export class AbstractWorkspaceEvent { + data: Workspace; + context: IEngineContext; + constructor(data: Workspace) { + this.data = data; + } +} diff --git a/src/app/core/events/workbench/AddWorkspaceEvent.ts b/src/app/core/events/workbench/AddWorkspaceEvent.ts new file mode 100644 index 0000000..8f811b0 --- /dev/null +++ b/src/app/core/events/workbench/AddWorkspaceEvent.ts @@ -0,0 +1,5 @@ +import { AbstractWorkspaceEvent } from './AbstractWorkspaceEvent'; +import { ICustomEvent } from '../../../shared/event'; +export class AddWorkspaceEvent extends AbstractWorkspaceEvent implements ICustomEvent { + type = 'add:workspace'; +} diff --git a/src/app/core/events/workbench/index.ts b/src/app/core/events/workbench/index.ts new file mode 100644 index 0000000..272ab30 --- /dev/null +++ b/src/app/core/events/workbench/index.ts @@ -0,0 +1 @@ +export * from './AddWorkspaceEvent'; diff --git a/src/app/core/externals.ts b/src/app/core/externals.ts index c34c66a..02dee90 100644 --- a/src/app/core/externals.ts +++ b/src/app/core/externals.ts @@ -1,20 +1,33 @@ -import { IBehavior, IBehaviorHost, IResource, IResourceCreator } from './types'; -import { Engine } from '@/app/core/models'; +import { IBehavior, IBehaviorCreator, IBehaviorHost, IResource, IResourceCreator } from './types'; +import { Engine, TreeNode } from '@/app/core/models'; import { DEFAULT_DRIVERS, DEFAULT_EFFECTS } from '@/app/core/presets'; +import { isArr } from '@/app/shared/types'; export const createResource = (...sources: IResourceCreator[]): IResource[] => { return sources.reduce((buf, source) => { return buf.concat({ ...source, - node: { + node: new TreeNode({ componentName: '$$ResourceNode$$', isSourceNode: true, children: source.elements || [] - } + }) }); }, []); }; +export const createBehavior = (...behaviors: Array): IBehavior[] => { + return behaviors.reduce((buf: any[], behavior) => { + if (isArr(behavior)) return buf.concat(createBehavior(...behavior)); + const { selector } = behavior || {}; + if (!selector) return buf; + if (typeof selector === 'string') { + behavior.selector = node => node.componentName === selector; + } + return buf.concat(behavior); + }, []); +}; + export const isResourceList = (val: any): val is IResource[] => Array.isArray(val) && val.every(isResource); export const isResource = (val: any): val is IResource => val?.node; diff --git a/src/app/core/models/cursor.ts b/src/app/core/models/cursor.ts index 66534dc..55804aa 100644 --- a/src/app/core/models/cursor.ts +++ b/src/app/core/models/cursor.ts @@ -1,6 +1,7 @@ import { Engine } from './engine'; import { isValidNumber } from '../../shared/types'; import { globalThisPolyfill } from '../../shared/globalThisPolyfill'; +import { action, define, observable } from '@formily/reactive'; export enum CursorStatus { Normal = 'NORMAL', @@ -112,6 +113,26 @@ export class Cursor { constructor(engine: Engine) { this.engine = engine; + this.makeObservable(); + } + + makeObservable() { + define(this, { + type: observable.ref, + dragType: observable.ref, + status: observable.ref, + position: observable.ref, + dragStartPosition: observable.ref, + dragEndPosition: observable.ref, + dragAtomDelta: observable.ref, + dragStartToCurrentDelta: observable.ref, + dragStartToEndDelta: observable.ref, + view: observable.ref, + setStyle: action, + setPosition: action, + setStatus: action, + setType: action + }); } get speed() { diff --git a/src/app/core/models/engine.ts b/src/app/core/models/engine.ts index 7182639..ea0cff5 100644 --- a/src/app/core/models/engine.ts +++ b/src/app/core/models/engine.ts @@ -5,6 +5,7 @@ import { Screen, ScreenType } from '@/app/core/models/screen'; import { uid } from '@/app/shared/uid'; import { TreeNode } from '@/app/core/models/tree-node'; import { Event } from '@/app/shared/event'; +import { globalThisPolyfill } from '@/app/shared/globalThisPolyfill'; /** * 设计器引擎 @@ -64,6 +65,14 @@ export class Engine extends Event { return results; } + mount() { + this.attachEvents(globalThisPolyfill); + } + + unmount() { + this.detachEvents(); + } + static defaultProps: IEngineProps = { shortcuts: [], effects: [], diff --git a/src/app/core/models/history.ts b/src/app/core/models/history.ts index d35a4e3..31bb3f3 100644 --- a/src/app/core/models/history.ts +++ b/src/app/core/models/history.ts @@ -24,4 +24,6 @@ export class History { updateTimer = null; maxSize = 100; locking = false; + + push(type?: string) {} } diff --git a/src/app/core/models/operation.ts b/src/app/core/models/operation.ts index 062657f..b4609a8 100644 --- a/src/app/core/models/operation.ts +++ b/src/app/core/models/operation.ts @@ -7,6 +7,7 @@ import { TransformHelper } from './transform-helper'; import { MoveHelper } from './move-helper'; import { ICustomEvent } from '@/app/shared/event'; import { isFn } from '@/app/shared/types'; +import { cancelIdle, requestIdle } from '@/app/shared/request-idle'; export interface IOperation { tree?: ITreeNode; @@ -32,6 +33,28 @@ export class Operation { snapshot: null }; + constructor(workspace: Workspace) { + this.engine = workspace.engine; + this.workspace = workspace; + this.tree = new TreeNode({ + componentName: this.engine.props.rootComponentName, + ...this.engine.props.defaultComponentTree, + operation: this + }); + + this.moveHelper = new MoveHelper({ + operation: this + }); + } + + snapshot(type?: string) { + cancelIdle(this.requests.snapshot); + if (!this.workspace || !this.workspace.history || this.workspace.history.locking) return; + this.requests.snapshot = requestIdle(() => { + this.workspace.history.push(type); + }); + } + dispatch(event: ICustomEvent, callback?: () => void) { if (this.workspace.dispatch(event) === false) return; if (isFn(callback)) return callback(); diff --git a/src/app/core/models/viewport.ts b/src/app/core/models/viewport.ts index ef642a1..5424769 100644 --- a/src/app/core/models/viewport.ts +++ b/src/app/core/models/viewport.ts @@ -4,6 +4,9 @@ import { globalThisPolyfill } from '@/app/shared/globalThisPolyfill'; import { calcBoundingRect, IPoint, isPointInRect, Rect } from '@/app/shared/coordinate'; import { TreeNode } from '@/app/core/models/tree-node'; import { calcElementLayout } from '@/app/shared/element'; +import { action, define, observable } from '@formily/reactive'; +import { isHTMLElement } from '@/app/shared/types'; +import { cancelIdle, requestIdle } from '@/app/shared/request-idle'; export interface IViewportProps { engine: Engine; @@ -55,6 +58,64 @@ export class Viewport { nodeElementsStore: Record = {}; + constructor(props: IViewportProps) { + this.workspace = props.workspace; + this.engine = props.engine; + this.moveSensitive = props.moveSensitive ?? false; + this.moveInsertionType = props.moveInsertionType ?? 'all'; + this.viewportElement = props.viewportElement; + this.contentWindow = props.contentWindow; + this.nodeIdAttrName = props.nodeIdAttrName; + this.digestViewport(); + this.makeObservable(); + this.attachEvents(); + } + + attachEvents() { + const engine = this.engine; + cancelIdle(this.attachRequest); + this.attachRequest = requestIdle(() => { + if (!engine) return; + if (this.isIframe) { + this.workspace.attachEvents(this.contentWindow, this.contentWindow); + } else if (isHTMLElement(this.viewportElement)) { + this.workspace.attachEvents(this.viewportElement, this.contentWindow); + } + }); + } + + makeObservable() { + define(this, { + scrollX: observable.ref, + scrollY: observable.ref, + width: observable.ref, + height: observable.ref, + digestViewport: action, + viewportElement: observable.ref, + contentWindow: observable.ref + }); + } + + digestViewport() { + Object.assign(this, this.getCurrentData()); + } + + getCurrentData() { + const data: IViewportData = {}; + if (this.isIframe) { + data.scrollX = this.contentWindow?.scrollX || 0; + data.scrollY = this.contentWindow?.scrollY || 0; + data.width = this.contentWindow?.innerWidth || 0; + data.height = this.contentWindow?.innerHeight || 0; + } else if (this.viewportElement) { + data.scrollX = this.viewportElement?.scrollLeft || 0; + data.scrollY = this.viewportElement?.scrollTop || 0; + data.width = this.viewportElement?.clientWidth || 0; + data.height = this.viewportElement?.clientHeight || 0; + } + return data; + } + cacheElements() { this.nodeElementsStore = {}; this.viewportRoot?.querySelectorAll(`*[${this.nodeIdAttrName}]`).forEach((element: HTMLElement) => { diff --git a/src/app/core/models/workbench.ts b/src/app/core/models/workbench.ts index 3c4eacc..ea42c8b 100644 --- a/src/app/core/models/workbench.ts +++ b/src/app/core/models/workbench.ts @@ -1,7 +1,8 @@ import { WorkbenchTypes } from '../types'; import { Engine } from './engine'; -import { Workspace } from './workspace'; +import { IWorkspaceProps, Workspace } from './workspace'; import { action, define, observable } from '@formily/reactive'; +import { AddWorkspaceEvent } from '@/app/core/events/workbench'; export class Workbench { workspaces: Workspace[]; @@ -26,15 +27,37 @@ export class Workbench { currentWorkspace: observable.ref, workspaces: observable.shallow, activeWorkspace: observable.ref, - type: observable.ref + type: observable.ref, // switchWorkspace: action, - // addWorkspace: action, + addWorkspace: action // removeWorkspace: action, // setActiveWorkspace: action, // setWorkbenchType: action }); } + addWorkspace(props: IWorkspaceProps) { + const finded = this.findWorkspaceById(props.id); + if (!finded) { + this.currentWorkspace = new Workspace(this.engine, props); + this.workspaces.push(this.currentWorkspace); + this.engine.dispatch(new AddWorkspaceEvent(this.currentWorkspace)); + return this.currentWorkspace; + } + return finded; + } + + ensureWorkspace(props: IWorkspaceProps = {}) { + const workspace = this.findWorkspaceById(props.id); + if (workspace) return workspace; + this.addWorkspace(props); + return this.currentWorkspace; + } + + findWorkspaceById(id: string) { + return this.workspaces.find(item => item.id === id); + } + eachWorkspace(callbackFn: (value: Workspace, index: number) => T) { this.workspaces.forEach(callbackFn); } diff --git a/src/app/core/models/workspace.ts b/src/app/core/models/workspace.ts index 6b1d7b3..05c6cfb 100644 --- a/src/app/core/models/workspace.ts +++ b/src/app/core/models/workspace.ts @@ -2,8 +2,9 @@ import { Engine } from '@/app/core/models/engine'; import { Viewport } from '@/app/core/models/viewport'; import { Operation } from '@/app/core/models/operation'; import { History } from '@/app/core/models/history'; -import { ICustomEvent } from '@/app/shared/event'; +import { EventContainer, ICustomEvent } from '@/app/shared/event'; import { IEngineContext } from '@/app/core/types'; +import { uid } from '@/app/shared/uid'; export interface IWorkspace { id?: string; @@ -42,6 +43,37 @@ export class Workspace { props: IWorkspaceProps; + constructor(engine: Engine, props: IWorkspaceProps) { + this.engine = engine; + this.props = props; + this.id = props.id || uid(); + this.title = props.title; + this.description = props.description; + this.viewport = new Viewport({ + engine: this.engine, + workspace: this, + viewportElement: props.viewportElement, + contentWindow: props.contentWindow, + nodeIdAttrName: this.engine.props.nodeIdAttrName, + moveSensitive: true, + moveInsertionType: 'all' + }); + this.outline = new Viewport({ + engine: this.engine, + workspace: this, + viewportElement: props.viewportElement, + contentWindow: props.contentWindow, + nodeIdAttrName: this.engine.props.outlineNodeIdAttrName, + moveSensitive: false, + moveInsertionType: 'block' + }); + this.operation = new Operation(this); + } + + attachEvents(container: EventContainer, contentWindow: Window) { + this.engine.attachEvents(container, contentWindow, this.getEventContext()); + } + getEventContext(): IEngineContext { return { workbench: this.engine.workbench, diff --git a/src/app/core/presets.ts b/src/app/core/presets.ts index 310c20c..8236b22 100644 --- a/src/app/core/presets.ts +++ b/src/app/core/presets.ts @@ -1,6 +1,6 @@ -import { useDragDropEffect } from './effects/useDragDropEffect'; -import { DragDropDriver } from '@/app/core/drivers/drag-drop-driver'; +import { useDragDropEffect, useCursorEffect } from '@/app/core/effects'; +import { DragDropDriver, MouseMoveDriver } from '@/app/core/drivers'; -export const DEFAULT_EFFECTS = [useDragDropEffect]; +export const DEFAULT_EFFECTS = [useDragDropEffect, useCursorEffect]; -export const DEFAULT_DRIVERS = [DragDropDriver]; +export const DEFAULT_DRIVERS = [DragDropDriver, MouseMoveDriver]; diff --git a/src/app/core/types.ts b/src/app/core/types.ts index 310dc17..3794533 100644 --- a/src/app/core/types.ts +++ b/src/app/core/types.ts @@ -112,6 +112,14 @@ export interface IBehavior { designerLocales?: IDesignerLocales; } +export interface IBehaviorCreator { + name: string; + extends?: string[]; + selector: string | ((node: TreeNode) => boolean); + designerProps?: IDesignerControllerProps; + designerLocales?: IDesignerLocales; +} + export interface IBehaviorHost { Behavior?: IBehavior[]; } diff --git a/src/app/pages/home/home.component.ts b/src/app/pages/home/home.component.ts index 15d8e84..c519a67 100644 --- a/src/app/pages/home/home.component.ts +++ b/src/app/pages/home/home.component.ts @@ -11,7 +11,7 @@ import { WorkspacePanelComponent } from '@/app/components/panels/workspace-panel import { SettingPanelComponent } from '@/app/components/panels/setting-panel.component'; import { ResourceWidget } from '@/app/components/widgets/resource/resource.widget'; import { GlobalRegistry } from '@/app/core/registry'; -import { createResource } from '@/app/core/externals'; +import { createBehavior, createResource } from '@/app/core/externals'; import { IResourceLike } from '@/app/core/types'; import { ToolbarPanelComponent } from '@/app/components/panels/toolbar-panel.component'; import { DesignerToolWidget } from '@/app/components/widgets/designer-tool/designer-tool.widget'; @@ -51,6 +51,7 @@ export class HomeComponent implements OnInit { constructor() {} ngOnInit(): void { + this.registerBehavior(); this.registerLocales(); this.createResources(); } @@ -81,6 +82,266 @@ export class HomeComponent implements OnInit { }); } + registerBehavior() { + const RootBehavior = createBehavior({ + name: 'Root', + selector: 'Root', + designerProps: { + droppable: true + }, + designerLocales: { + 'zh-CN': { + title: '根组件' + }, + 'en-US': { + title: 'Root' + }, + 'ko-KR': { + title: '루트' + } + } + }); + + const InputBehavior = createBehavior({ + name: 'Input', + selector: node => node.componentName === 'Field' && node.props['x-component'] === 'Input', + designerProps: { + propsSchema: { + type: 'object', + $namespace: 'Field', + properties: { + 'field-properties': { + type: 'void', + 'x-component': 'CollapseItem', + title: '字段属性', + properties: { + title: { + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Input' + }, + + hidden: { + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'Switch' + }, + default: { + 'x-decorator': 'FormItem', + 'x-component': 'ValueInput' + }, + test: { + type: 'void', + title: '测试', + 'x-decorator': 'FormItem', + 'x-component': 'DrawerSetter', + 'x-component-props': { + text: '打开抽屉' + }, + properties: { + test: { + type: 'string', + title: '测试输入', + 'x-decorator': 'FormItem', + 'x-component': 'Input' + } + } + } + } + }, + + 'component-styles': { + type: 'void', + title: '样式', + 'x-component': 'CollapseItem', + properties: { + 'style.width': { + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'SizeInput' + }, + 'style.height': { + type: 'string', + 'x-decorator': 'FormItem', + 'x-component': 'SizeInput' + }, + 'style.display': { + 'x-component': 'DisplayStyleSetter' + }, + 'style.background': { + 'x-component': 'BackgroundStyleSetter' + }, + 'style.boxShadow': { + 'x-component': 'BoxShadowStyleSetter' + }, + 'style.font': { + 'x-component': 'FontStyleSetter' + }, + 'style.margin': { + 'x-component': 'BoxStyleSetter' + }, + 'style.padding': { + 'x-component': 'BoxStyleSetter' + }, + 'style.borderRadius': { + 'x-component': 'BorderRadiusStyleSetter' + }, + 'style.border': { + 'x-component': 'BorderStyleSetter' + } + } + } + } + } + }, + designerLocales: { + 'zh-CN': { + title: '输入框', + settings: { + title: '标题', + hidden: '是否隐藏', + default: '默认值', + style: { + width: '宽度', + height: '高度', + display: '展示', + background: '背景', + boxShadow: '阴影', + font: '字体', + margin: '外边距', + padding: '内边距', + borderRadius: '圆角', + border: '边框' + } + } + }, + 'en-US': { + title: 'Input', + settings: { + title: 'Title', + hidden: 'Hidden', + default: 'Default Value', + style: { + width: 'Width', + height: 'Height', + display: 'Display', + background: 'Background', + boxShadow: 'Box Shadow', + font: 'Font', + margin: 'Margin', + padding: 'Padding', + borderRadius: 'Border Radius', + border: 'Border' + } + } + }, + 'ko-KR': { + title: '입력', + settings: { + title: '텍스트', + hidden: '숨김 여부', + default: '기본 설정 값', + style: { + width: '너비', + height: '높이', + display: '디스플레이', + background: '배경', + boxShadow: '그림자 박스', + font: '폰트', + margin: '마진', + padding: '패딩', + borderRadius: '테두리 굴곡', + border: '테두리' + } + } + } + } + }); + + const CardBehavior = createBehavior({ + name: 'Card', + selector: 'Card', + designerProps: { + droppable: true, + resizable: { + width(node, element) { + const width = Number(node.props?.style?.width ?? element.getBoundingClientRect().width); + return { + plus: () => { + node.props = node.props || {}; + node.props.style = node.props.style || {}; + node.props.style.width = width + 10; + }, + minus: () => { + node.props = node.props || {}; + node.props.style = node.props.style || {}; + node.props.style.width = width - 10; + } + }; + }, + height(node, element) { + const height = Number(node.props?.style?.height ?? element.getBoundingClientRect().height); + return { + plus: () => { + node.props = node.props || {}; + node.props.style = node.props.style || {}; + node.props.style.height = height + 10; + }, + minus: () => { + node.props = node.props || {}; + node.props.style = node.props.style || {}; + node.props.style.height = height - 10; + } + }; + } + }, + translatable: { + x(node, element, diffX) { + const left = parseInt(node.props?.style?.left ?? element?.style.left) || 0; + const rect = element.getBoundingClientRect(); + return { + translate: () => { + node.props = node.props || {}; + node.props.style = node.props.style || {}; + node.props.style.position = 'absolute'; + node.props.style.width = rect.width; + node.props.style.height = rect.height; + node.props.style.left = left + parseInt(String(diffX)) + 'px'; + } + }; + }, + y(node, element, diffY) { + const top = parseInt(node.props?.style?.top ?? element?.style.top) || 0; + const rect = element.getBoundingClientRect(); + return { + translate: () => { + node.props = node.props || {}; + node.props.style = node.props.style || {}; + node.props.style.position = 'absolute'; + node.props.style.width = rect.width; + node.props.style.height = rect.height; + node.props.style.top = top + parseInt(String(diffY)) + 'px'; + } + }; + } + } + }, + designerLocales: { + 'zh-CN': { + title: '卡片' + }, + 'en-US': { + title: 'Card' + }, + 'ko-KR': { + title: '카드' + } + } + }); + + GlobalRegistry.setDesignerBehaviors([RootBehavior, InputBehavior, CardBehavior]); + } + createResources() { const Input = createResource({ title: { diff --git a/src/app/shared/request-idle.ts b/src/app/shared/request-idle.ts new file mode 100644 index 0000000..6eaa50d --- /dev/null +++ b/src/app/shared/request-idle.ts @@ -0,0 +1,18 @@ +import { globalThisPolyfill } from './globalThisPolyfill'; + +export interface IIdleDeadline { + didTimeout: boolean; + timeRemaining: () => DOMHighResTimeStamp; +} + +export interface IdleCallbackOptions { + timeout?: number; +} + +export const requestIdle = (callback: (params: IIdleDeadline) => void, options?: IdleCallbackOptions): number => { + return globalThisPolyfill['requestIdleCallback'](callback, options); +}; + +export const cancelIdle = (id: number) => { + globalThisPolyfill['cancelIdleCallback'](id); +};