add cut paste

This commit is contained in:
Bowen Tan 2021-12-03 15:44:18 +08:00
parent bbe97dc486
commit 5cc864db4d
11 changed files with 246 additions and 71 deletions

View File

@ -1,5 +1,5 @@
import { css } from '@emotion/react';
import React, { useRef } from 'react';
import React from 'react';
import { eventBus } from '../eventBus';
import { genOperation } from '../operations';
@ -8,7 +8,6 @@ type Props = {
};
export const KeyboardEventWrapper: React.FC<Props> = props => {
const copyId = useRef('');
const style = css`
&:focus {
outline: none;
@ -46,18 +45,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;
}
};

View File

@ -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

View File

@ -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();

View 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)
}
}

View File

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

View File

@ -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);

View File

@ -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

View File

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

View File

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

View File

@ -1,4 +1,5 @@
export * from './component/removeComponentLeafOperation';
export * from './component/cutComponentLeafOperation';
export * from './component/adjustComponentOrderLeafOperation';
export * from './component/createComponentLeafOperation';
export * from './component/modifyComponentIdLeafOperation';

View File

@ -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]);
}