Skip to content

Commit

Permalink
feat: 添加拖拽事件
Browse files Browse the repository at this point in the history
  • Loading branch information
TheBeard30 committed Mar 16, 2024
1 parent 5cf256d commit 7365ae4
Show file tree
Hide file tree
Showing 25 changed files with 678 additions and 34 deletions.
4 changes: 3 additions & 1 deletion src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }]
};
17 changes: 14 additions & 3 deletions src/app/components/container/designer.component.ts
Original file line number Diff line number Diff line change
@@ -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: `
<div [ngClass]="classNameList">
<ng-content></ng-content>
<app-ghost></app-ghost>
</div>
`,
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();
Expand All @@ -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];
}
Expand Down
14 changes: 12 additions & 2 deletions src/app/components/container/workspace.component.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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);
}
}
57 changes: 46 additions & 11 deletions src/app/components/widgets/ghost/ghost.widget.ts
Original file line number Diff line number Diff line change
@@ -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) {
<div class="{{ prefix }}">
<div class="{{ prefix }}" #containerRef>
<span style="white-space: nowrap">
<app-node-title-widget></app-node-title-widget>
<app-node-title-widget [node]="firstNode"></app-node-title-widget>
{{ movingNodes?.length > 1 ? '...' : '' }}
</span>
</div>
}
`,
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;
});
}
}
10 changes: 6 additions & 4 deletions src/app/components/widgets/node-title/node-title.widget.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}

Expand Down
1 change: 1 addition & 0 deletions src/app/core/drivers/drag-drop-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export class DragDropDriver extends EventDriver<Engine> {
startEvent: MouseEvent;

onMouseDown = (e: MouseEvent) => {
console.log('onMouseDown>>>', e);
if (e.button !== 0 || e.ctrlKey || e.metaKey) {
return false;
}
Expand Down
2 changes: 2 additions & 0 deletions src/app/core/drivers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './drag-drop-driver';
export * from './mouse-move-driver';
35 changes: 35 additions & 0 deletions src/app/core/drivers/mouse-move-driver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { EventDriver } from '../../shared/event';
import { Engine } from '../models';
import { MouseMoveEvent } from '../events/cursor';

export class MouseMoveDriver extends EventDriver<Engine> {
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'
});
}
}
2 changes: 2 additions & 0 deletions src/app/core/effects/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './useDragDropEffect';
export * from './useCursorEffect';
57 changes: 57 additions & 0 deletions src/app/core/effects/useCursorEffect.ts
Original file line number Diff line number Diff line change
@@ -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();
}
});
};
10 changes: 10 additions & 0 deletions src/app/core/events/workbench/AbstractWorkspaceEvent.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
5 changes: 5 additions & 0 deletions src/app/core/events/workbench/AddWorkspaceEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { AbstractWorkspaceEvent } from './AbstractWorkspaceEvent';
import { ICustomEvent } from '../../../shared/event';
export class AddWorkspaceEvent extends AbstractWorkspaceEvent implements ICustomEvent {
type = 'add:workspace';
}
1 change: 1 addition & 0 deletions src/app/core/events/workbench/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './AddWorkspaceEvent';
21 changes: 17 additions & 4 deletions src/app/core/externals.ts
Original file line number Diff line number Diff line change
@@ -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<IBehaviorCreator | IBehaviorCreator[]>): 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;
Expand Down
21 changes: 21 additions & 0 deletions src/app/core/models/cursor.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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() {
Expand Down
Loading

0 comments on commit 7365ae4

Please sign in to comment.