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