mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2024-11-21 03:15:49 +08:00
add cut paste
This commit is contained in:
parent
42e955b40d
commit
e24d97b0aa
@ -1,6 +1,6 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { css } from '@emotion/react';
|
||||
import React, { useRef } from 'react';
|
||||
import React from 'react';
|
||||
import { eventBus } from '../eventBus';
|
||||
import { genOperation } from '../operations';
|
||||
|
||||
@ -9,7 +9,6 @@ type Props = {
|
||||
};
|
||||
|
||||
export const KeyboardEventWrapper: React.FC<Props> = props => {
|
||||
const copyId = useRef('');
|
||||
const style = css`
|
||||
&:focus {
|
||||
outline: none;
|
||||
@ -44,18 +43,27 @@ export const KeyboardEventWrapper: React.FC<Props> = props => {
|
||||
// FIXME: detect os version and set redo/undo logic
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
// eventBus.send('copy', { componentId: props.selectedComponentId });
|
||||
copyId.current = props.selectedComponentId;
|
||||
}
|
||||
break;
|
||||
case 'x':
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
eventBus.send('cutComponent', {
|
||||
componentId: props.selectedComponentId,
|
||||
});
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('removeComponent', {
|
||||
componentId: props.selectedComponentId,
|
||||
})
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'v':
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('pasteComponent', {
|
||||
componentId: copyId.current,
|
||||
parentId: props.selectedComponentId,
|
||||
slot: 'content',
|
||||
})
|
||||
);
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
eventBus.send('paste', {
|
||||
componentId: props.selectedComponentId,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
@ -7,6 +7,8 @@ export type EventNames = {
|
||||
redo: undefined;
|
||||
undo: undefined;
|
||||
copy: { componentId: string };
|
||||
cutComponent: { componentId: string };
|
||||
paste: { componentId: string };
|
||||
// when switch app or module, current components refresh
|
||||
componentsRefresh: ApplicationComponent[];
|
||||
// components change by operation
|
||||
|
@ -1,15 +1,32 @@
|
||||
import { ApplicationComponent } from '@sunmao-ui/core';
|
||||
import { genOperation } from '.';
|
||||
import { eventBus } from '../eventBus';
|
||||
import { PasteManager } from './PasteManager';
|
||||
import { IUndoRedoManager, IOperation, OperationList } from './type';
|
||||
|
||||
export class AppModelManager implements IUndoRedoManager {
|
||||
components: ApplicationComponent[] = [];
|
||||
operationStack: OperationList<IOperation> = new OperationList();
|
||||
pasteManager = new PasteManager();
|
||||
|
||||
constructor() {
|
||||
eventBus.on('undo', () => this.undo());
|
||||
eventBus.on('redo', () => this.redo());
|
||||
eventBus.on('operation', o => this.do(o));
|
||||
eventBus.on('cutComponent', ({ componentId }) =>
|
||||
this.pasteManager.cutComponent(componentId, this.components)
|
||||
);
|
||||
eventBus.on('paste', ({ componentId }) => {
|
||||
console.log('componentId', componentId)
|
||||
eventBus.send(
|
||||
'operation',
|
||||
genOperation('pasteComponent', {
|
||||
parentId: componentId,
|
||||
slot: 'content',
|
||||
components: this.pasteManager.componentsCache,
|
||||
})
|
||||
);
|
||||
});
|
||||
eventBus.on('componentsRefresh', components => {
|
||||
this.components = components;
|
||||
this.operationStack = new OperationList();
|
||||
|
12
packages/editor/src/operations/PasteManager.ts
Normal file
12
packages/editor/src/operations/PasteManager.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { ApplicationComponent } from '@sunmao-ui/core';
|
||||
import { getComponentAndChildrens } from './util';
|
||||
|
||||
export class PasteManager {
|
||||
componentsCache: ApplicationComponent[] = [];
|
||||
|
||||
cutComponent(componentId: string, allComponents: ApplicationComponent[]) {
|
||||
const children = getComponentAndChildrens(componentId, allComponents);
|
||||
this.componentsCache = [...children];
|
||||
console.log('componentsCache', this.componentsCache)
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
import { ApplicationComponent } from '@sunmao-ui/core';
|
||||
import produce from 'immer';
|
||||
import {
|
||||
ModifyComponentPropertiesLeafOperation,
|
||||
CutComponentLeafOperation,
|
||||
} from '../leaf';
|
||||
import { BaseBranchOperation } from '../type';
|
||||
|
||||
export type CutComponentBranchOperationContext = {
|
||||
componentId: string;
|
||||
};
|
||||
|
||||
export class CutComponentBranchOperation extends BaseBranchOperation<CutComponentBranchOperationContext> {
|
||||
do(prev: ApplicationComponent[]): ApplicationComponent[] {
|
||||
// find component to remove
|
||||
const toRemove = prev.find(c => c.id === this.context.componentId);
|
||||
if (!toRemove) {
|
||||
console.warn('component not found');
|
||||
return prev;
|
||||
}
|
||||
// check if it has a slot trait (mean it is a child component of an slot-based component)
|
||||
let parentId = (
|
||||
toRemove.traits.find(t => t.type === 'core/v1/slot')?.properties as
|
||||
| { id: string }
|
||||
| undefined
|
||||
)?.id;
|
||||
prev.forEach(component => {
|
||||
if (component.id === parentId && component.type !== 'core/v1/grid_layout') {
|
||||
// only need to modified layout from grid_layout component
|
||||
parentId = undefined;
|
||||
}
|
||||
const slotTrait = component.traits.find(trait => trait.type === 'core/v1/slot');
|
||||
if (
|
||||
slotTrait &&
|
||||
(slotTrait.properties.container as { id: string }).id === this.context.componentId
|
||||
) {
|
||||
// for direct children of the element, use Remove operation to delete them, it will cause a cascade deletion
|
||||
this.operationStack.insert(
|
||||
new CutComponentBranchOperation({ componentId: component.id })
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (parentId) {
|
||||
// modify layout property of parent grid layout component
|
||||
this.operationStack.insert(
|
||||
new ModifyComponentPropertiesLeafOperation({
|
||||
componentId: parentId,
|
||||
properties: {
|
||||
layout: (prev: Array<ReactGridLayout.Layout>) => {
|
||||
return produce(prev, draft => {
|
||||
const removeIndex = draft.findIndex(
|
||||
item => item.i === this.context.componentId
|
||||
);
|
||||
if (removeIndex === -1) {
|
||||
console.warn("parent element doesn' contain specific child");
|
||||
}
|
||||
draft.splice(removeIndex, 1);
|
||||
});
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// free component from schema
|
||||
this.operationStack.insert(
|
||||
new CutComponentLeafOperation({ componentId: this.context.componentId })
|
||||
);
|
||||
|
||||
// do the operation in order
|
||||
return this.operationStack.reduce((prev, node) => {
|
||||
prev = node.do(prev);
|
||||
return prev;
|
||||
}, prev);
|
||||
}
|
||||
}
|
@ -1,80 +1,39 @@
|
||||
import { ApplicationComponent } from '@sunmao-ui/core';
|
||||
import { CreateComponentBranchOperation } from '../branch';
|
||||
import produce from 'immer';
|
||||
import { PasteComponentLeafOperation } from '../leaf/component/pasteComponentLeafOperation';
|
||||
import { BaseBranchOperation } from '../type';
|
||||
|
||||
export type PasteComponentBranchOperationContext = {
|
||||
parentId: string;
|
||||
slot: string;
|
||||
componentId: string;
|
||||
components: ApplicationComponent[];
|
||||
};
|
||||
|
||||
export class PasteComponentBranchOperation extends BaseBranchOperation<PasteComponentBranchOperationContext> {
|
||||
do(prev: ApplicationComponent[]): ApplicationComponent[] {
|
||||
console.log('paste', this.context)
|
||||
// find component to remove
|
||||
const targetComponent = prev.find(c => c.id === this.context.componentId);
|
||||
if (!targetComponent) {
|
||||
console.warn('paste component not found');
|
||||
return prev;
|
||||
}
|
||||
// TODO: Change slot to target Parent
|
||||
// this.operationStack.insert(
|
||||
// new CreateComponentBranchOperation({ componentId: component.id })
|
||||
// );
|
||||
|
||||
// TODO: O(n2) in worst case. Optimize in the future.
|
||||
const children = prev.filter(c => {
|
||||
const slotTrait = c.traits.find(t => t.type === 'core/v1/slot');
|
||||
return (
|
||||
slotTrait &&
|
||||
(slotTrait.properties.container as { id: string }).id === this.context.componentId
|
||||
const newComponents = produce(this.context.components, draft => {
|
||||
const newSlotTrait = {
|
||||
type: 'core/v1/slot',
|
||||
properties: { container: { id: this.context.parentId, slot: this.context.slot } },
|
||||
};
|
||||
const oldSlotIndex = draft[0].traits.findIndex(
|
||||
trait => trait.type === 'core/v1/slot'
|
||||
);
|
||||
if (oldSlotIndex !== -1) {
|
||||
draft[0].traits.push(newSlotTrait);
|
||||
} else {
|
||||
draft[0].traits[oldSlotIndex] = newSlotTrait;
|
||||
}
|
||||
});
|
||||
|
||||
children.forEach(component => {
|
||||
console.log('paste', newComponents)
|
||||
newComponents.forEach(c => {
|
||||
this.operationStack.insert(
|
||||
new CreateComponentBranchOperation({
|
||||
componentId: `${component.id}_copy`,
|
||||
componentType: component.type,
|
||||
parentId: `${this.context.componentId}_copy`,
|
||||
slot: this.context.slot,
|
||||
new PasteComponentLeafOperation({
|
||||
component: c,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// TODO: dont care about layout component
|
||||
// if (parentId) {
|
||||
// // modify layout property of parent grid layout component
|
||||
// this.operationStack.insert(
|
||||
// new ModifyComponentPropertiesLeafOperation({
|
||||
// componentId: parentId,
|
||||
// properties: {
|
||||
// layout: (prev: Array<ReactGridLayout.Layout>) => {
|
||||
// return produce(prev, draft => {
|
||||
// const removeIndex = draft.findIndex(
|
||||
// item => item.i === this.context.componentId
|
||||
// );
|
||||
// if (removeIndex === -1) {
|
||||
// console.warn("parent element doesn' contain specific child");
|
||||
// }
|
||||
// draft.splice(removeIndex, 1);
|
||||
// });
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
// );
|
||||
// }
|
||||
|
||||
// free component from schema
|
||||
this.operationStack.insert(
|
||||
new CreateComponentBranchOperation({
|
||||
componentId: `${this.context.componentId}_copy`,
|
||||
componentType: targetComponent.type,
|
||||
parentId: this.context.parentId,
|
||||
slot: this.context.slot,
|
||||
})
|
||||
);
|
||||
|
||||
// do the operation in order
|
||||
return this.operationStack.reduce((prev, node) => {
|
||||
prev = node.do(prev);
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
RemoveComponentBranchOperation,
|
||||
RemoveComponentBranchOperationContext,
|
||||
} from './branch';
|
||||
import { CutComponentBranchOperation } from './branch/cutComponentBranchOperation';
|
||||
import {
|
||||
PasteComponentBranchOperation,
|
||||
PasteComponentBranchOperationContext,
|
||||
@ -15,6 +16,7 @@ import {
|
||||
AdjustComponentOrderLeafOperationContext,
|
||||
CreateTraitLeafOperation,
|
||||
CreateTraitLeafOperationContext,
|
||||
CutComponentLeafOperationContext,
|
||||
ModifyComponentPropertiesLeafOperation,
|
||||
ModifyComponentPropertiesLeafOperationContext,
|
||||
ModifyTraitPropertiesLeafOperation,
|
||||
@ -40,6 +42,7 @@ const OperationConstructors: Record<
|
||||
modifyTraitProperty: ModifyTraitPropertiesLeafOperation,
|
||||
replaceApp: ReplaceAppLeafOperation,
|
||||
pasteComponent: PasteComponentBranchOperation,
|
||||
cutComponent: CutComponentBranchOperation,
|
||||
};
|
||||
|
||||
type OperationTypes = keyof OperationConfigMaps;
|
||||
@ -58,6 +61,10 @@ type OperationConfigMaps = {
|
||||
RemoveComponentBranchOperation,
|
||||
RemoveComponentBranchOperationContext
|
||||
>;
|
||||
cutComponent: OperationConfigMap<
|
||||
CutComponentBranchOperation,
|
||||
CutComponentLeafOperationContext
|
||||
>;
|
||||
modifyComponentProperty: OperationConfigMap<
|
||||
ModifyComponentPropertiesLeafOperation,
|
||||
ModifyComponentPropertiesLeafOperationContext
|
||||
|
@ -0,0 +1,41 @@
|
||||
import { ApplicationComponent } from '@sunmao-ui/core';
|
||||
import produce from 'immer';
|
||||
import { tryOriginal } from 'operations/util';
|
||||
import { BaseLeafOperation } from '../../type';
|
||||
|
||||
export type CutComponentLeafOperationContext = {
|
||||
componentId: string;
|
||||
};
|
||||
|
||||
export class CutComponentLeafOperation extends BaseLeafOperation<CutComponentLeafOperationContext> {
|
||||
private deletedComponent!: ApplicationComponent;
|
||||
// FIXME: index is not a good type to remember a deleted resource
|
||||
private deletedIndex = -1;
|
||||
|
||||
do(prev: ApplicationComponent[]): ApplicationComponent[] {
|
||||
this.deletedIndex = prev.findIndex(
|
||||
c => c.id === this.context.componentId
|
||||
);
|
||||
if (this.deletedIndex === -1) {
|
||||
console.warn('element not found');
|
||||
return prev;
|
||||
}
|
||||
return produce(prev, draft => {
|
||||
this.deletedComponent = tryOriginal(
|
||||
draft.splice(this.deletedIndex, 1)[0]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
redo(prev: ApplicationComponent[]): ApplicationComponent[] {
|
||||
return produce(prev, draft => {
|
||||
draft.splice(this.deletedIndex, 1);
|
||||
});
|
||||
}
|
||||
|
||||
undo(prev: ApplicationComponent[]): ApplicationComponent[] {
|
||||
return produce(prev, draft => {
|
||||
draft.splice(this.deletedIndex, 0, this.deletedComponent);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import { ApplicationComponent } from '@sunmao-ui/core';
|
||||
import produce from 'immer';
|
||||
import { BaseLeafOperation } from '../../type';
|
||||
|
||||
export type PasteComponentLeafOperationContext = {
|
||||
component: ApplicationComponent;
|
||||
};
|
||||
|
||||
export class PasteComponentLeafOperation extends BaseLeafOperation<PasteComponentLeafOperationContext> {
|
||||
private index!: number;
|
||||
private component!: ApplicationComponent;
|
||||
do(prev: ApplicationComponent[]): ApplicationComponent[] {
|
||||
this.component = this.context.component;
|
||||
this.index = prev.length;
|
||||
|
||||
return produce(prev, draft => {
|
||||
draft.push(this.context.component);
|
||||
});
|
||||
}
|
||||
|
||||
redo(prev: ApplicationComponent[]): ApplicationComponent[] {
|
||||
return produce(prev, draft => {
|
||||
draft.push(this.component);
|
||||
});
|
||||
}
|
||||
|
||||
undo(prev: ApplicationComponent[]): ApplicationComponent[] {
|
||||
return produce(prev, draft => {
|
||||
draft.splice(this.index, 1);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
export * from './component/removeComponentLeafOperation';
|
||||
export * from './component/cutComponentLeafOperation';
|
||||
export * from './component/adjustComponentOrderLeafOperation';
|
||||
export * from './component/createComponentLeafOperation';
|
||||
export * from './component/modifyComponentIdLeafOperation';
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { ApplicationComponent } from '@sunmao-ui/core';
|
||||
import { parseType } from '@sunmao-ui/runtime';
|
||||
import { isDraft, original } from 'immer';
|
||||
import { get } from 'lodash-es';
|
||||
import { registry } from '../setup';
|
||||
|
||||
export function genComponent(type: string, id: string): ApplicationComponent {
|
||||
@ -26,3 +27,21 @@ export function genId(componentType: string, components: ApplicationComponent[])
|
||||
export function tryOriginal<T>(val: T): T {
|
||||
return isDraft(val) ? (original(val) as T) : val;
|
||||
}
|
||||
|
||||
export function getComponentAndChildrens(
|
||||
componentId: string,
|
||||
allComponents: ApplicationComponent[]
|
||||
): ApplicationComponent[] {
|
||||
const target = allComponents.find(c => c.id === componentId);
|
||||
if (!target) {
|
||||
return [];
|
||||
}
|
||||
return allComponents.reduce<ApplicationComponent[]>((result, component) => {
|
||||
const slotTrait = component.traits.find(trait => trait.type === 'core/v1/slot');
|
||||
const slotId = get(slotTrait, 'properties.container.id');
|
||||
if (slotId === componentId) {
|
||||
return result.concat(getComponentAndChildrens(component.id, allComponents));
|
||||
}
|
||||
return result;
|
||||
}, [target]);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user