Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): support alt key duplicate #WIK-15159 #851

Merged
merged 7 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/blue-melons-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@plait/common': minor
'@plait/core': minor
'@plait/draw': minor
'@plait/mind': minor
---

support alt key duplicate
2 changes: 1 addition & 1 deletion packages/common/src/plugins/with-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export function withGroup(board: PlaitBoard) {
};

board.getRelatedFragment = (elements: PlaitElement[], originData?: PlaitElement[]) => {
const groups = getSelectedGroups(board, originData);
const groups = getSelectedGroups(board, elements, originData);
return getRelatedFragment([...elements, ...groups], originData);
};

Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/interfaces/board.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ export interface PlaitBoard {
buildFragment: (
clipboardContext: WritableClipboardContext | null,
rectangle: RectangleClient | null,
type: 'copy' | 'cut'
type: 'copy' | 'cut',
elements?: PlaitElement[]
) => WritableClipboardContext | null;
insertFragment: (clipboardData: ClipboardData | null, targetPoint: Point) => void;
deleteFragment: (data: PlaitElement[]) => void;
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/plugins/create-board.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { PathRef, PathRefOptions } from '../interfaces/path-ref';
import { Path } from '../interfaces/path';
import { ThemeColorMode } from '../interfaces/theme';
import { CoreTransforms } from '../transforms/element';
import { WritableClipboardContext, drawEntireActiveRectangleG, setClipboardData } from '../utils';
import { WritableClipboardContext, drawEntireActiveRectangleG } from '../utils';
import { RectangleClient } from '../interfaces';

export function createBoard(children: PlaitElement[], options?: PlaitBoardOptions): PlaitBoard {
const board: PlaitBoard = {
Expand Down Expand Up @@ -95,7 +96,12 @@ export function createBoard(children: PlaitElement[], options?: PlaitBoardOption
globalKeyDown: (event: KeyboardEvent) => {},
keyUp: (event: KeyboardEvent) => {},
dblClick: (event: MouseEvent) => {},
buildFragment: (clipboardContext: WritableClipboardContext | null) => clipboardContext,
buildFragment: (
clipboardContext: WritableClipboardContext | null,
rectangle: RectangleClient | null,
type: 'copy' | 'cut',
elements?: PlaitElement[]
huanhuanwa marked this conversation as resolved.
Show resolved Hide resolved
) => clipboardContext,
insertFragment: () => {},
deleteFragment: (elements: PlaitElement[]) => {
CoreTransforms.removeElements(board, elements);
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/plugins/with-hotkey.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { isHotkey, isKeyHotkey } from 'is-hotkey';
import { Ancestor, PlaitBoard, PlaitElement, PlaitPluginKey } from '../interfaces';
import { Ancestor, PlaitBoard, PlaitElement, PlaitPluginKey, Point } from '../interfaces';
import { BoardTransforms, Transforms } from '../transforms';
import { deleteFragment, depthFirstRecursion, duplicateElements, getSelectedElements, hotkeys } from '../utils';
import { deleteFragment, depthFirstRecursion, duplicateElements, getRectangleByElements, getSelectedElements, hotkeys } from '../utils';
import { PlaitOptionsBoard } from './with-options';
import { WithPluginOptions } from './with-selection';

Expand Down Expand Up @@ -60,7 +60,7 @@ export const withHotkey = (board: PlaitBoard) => {
if (!PlaitBoard.isReadonly(board) && selectedElements.length > 0) {
if (isKeyHotkey('mod+d', event)) {
event.preventDefault();
duplicateElements(board, selectedElements);
duplicateElements(board);
return;
}
}
Expand Down
108 changes: 98 additions & 10 deletions packages/core/src/plugins/with-moving.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { PlaitBoard } from '../interfaces/board';
import { isInPlaitBoard } from '../utils/board';
import { isMainPointer } from '../utils/dom/common';
import { createG, isMainPointer } from '../utils/dom/common';
import { Point } from '../interfaces/point';
import { Transforms } from '../transforms';
import { PlaitElement } from '../interfaces/element';
Expand All @@ -20,15 +20,19 @@ import {
hotkeys,
getElementsInGroupByElement,
getRectangleByAngle,
getSelectionAngle
getSelectionAngle,
duplicateElements,
drawRectangle,
depthFirstRecursion
} from '../utils';
import { getSnapMovingRef } from '../utils/snap/snap-moving';
import { PlaitGroupElement, PlaitPointerType, RectangleClient } from '../interfaces';
import { PlaitGroupElement, PlaitPointerType, RectangleClient, SELECTION_BORDER_COLOR, SELECTION_FILL_COLOR } from '../interfaces';
import { ACTIVE_MOVING_CLASS_NAME, PRESS_AND_MOVE_BUFFER } from '../constants';
import { addSelectionWithTemporaryElements } from '../transforms/selection';
import { isKeyHotkey } from 'is-hotkey';

export function withMoving(board: PlaitBoard) {
const { pointerDown, pointerMove, globalPointerUp, globalPointerMove } = board;
const { pointerDown, pointerMove, globalPointerUp, globalPointerMove, globalKeyDown, keyUp } = board;

let offsetX = 0;
let offsetY = 0;
Expand All @@ -40,6 +44,36 @@ export function withMoving(board: PlaitBoard) {
let selectedTargetElements: PlaitElement[] | null = null;
let hitTargetElement: PlaitElement | undefined = undefined;
let isHitSelectedTarget: boolean | undefined = undefined;
let isAltKeyDown: boolean = false;
let pendingNodesG: SVGGElement | null = null;

board.globalKeyDown = (event: KeyboardEvent) => {
if (!PlaitBoard.isReadonly(board)) {
if (isKeyHotkey('option', event)) {
event.preventDefault();
isAltKeyDown = true;
huanhuanwa marked this conversation as resolved.
Show resolved Hide resolved
if (startPoint && activeElements.length && !PlaitBoard.hasBeenTextEditing(board)) {
pendingNodesG = drawPendingNodesG(board, activeElements, offsetX, offsetY);
pendingNodesG && PlaitBoard.getElementActiveHost(board).append(pendingNodesG);
}
}
}
globalKeyDown(event);
};

board.keyUp = (event: KeyboardEvent) => {
if (!PlaitBoard.isReadonly(board)) {
if (isAltKeyDown && pendingNodesG && startPoint && activeElements.length && !PlaitBoard.hasBeenTextEditing(board)) {
event.preventDefault();
const currentElements = updatePoints(board, activeElements, offsetX, offsetY);
PlaitBoard.getBoardContainer(board).classList.add('element-moving');
cacheMovingElements(board, currentElements as PlaitElement[]);
}
}
isAltKeyDown = false;
pendingNodesG?.remove();
keyUp(event);
};

board.pointerDown = (event: PointerEvent) => {
if (
Expand Down Expand Up @@ -87,6 +121,7 @@ export function withMoving(board: PlaitBoard) {
isPreventDefault = true;
}
snapG?.remove();
pendingNodesG?.remove();
const endPoint = toViewBoxPoint(board, toHostPoint(board, event.x, event.y));
offsetX = endPoint[0] - startPoint[0];
offsetY = endPoint[1] - startPoint[1];
Expand All @@ -107,7 +142,6 @@ export function withMoving(board: PlaitBoard) {
x: activeElementsRectangle.x + offsetX,
y: activeElementsRectangle.y + offsetY
};

const activeRectangle = getRectangleByAngle(newRectangle, getSelectionAngle(activeElements)) || newRectangle;
const ref = getSnapMovingRef(board, activeRectangle, activeElements);
offsetX += ref.deltaX;
Expand All @@ -116,9 +150,14 @@ export function withMoving(board: PlaitBoard) {
snapG.classList.add(ACTIVE_MOVING_CLASS_NAME);
PlaitBoard.getElementActiveHost(board).append(snapG);
handleTouchTarget(board);
const currentElements = updatePoints(board, activeElements, offsetX, offsetY);
PlaitBoard.getBoardContainer(board).classList.add('element-moving');
cacheMovingElements(board, currentElements as PlaitElement[]);
if (isAltKeyDown) {
pendingNodesG = drawPendingNodesG(board, activeElements, offsetX, offsetY);
pendingNodesG && PlaitBoard.getElementActiveHost(board).append(pendingNodesG);
} else {
const currentElements = updatePoints(board, activeElements, offsetX, offsetY);
PlaitBoard.getBoardContainer(board).classList.add('element-moving');
cacheMovingElements(board, currentElements as PlaitElement[]);
}
});
}
}
Expand All @@ -140,6 +179,11 @@ export function withMoving(board: PlaitBoard) {
};

board.globalPointerUp = event => {
if (isAltKeyDown && activeElements.length) {
const validElements = getValidElements(board, activeElements);
const rectangle = getRectangleByElements(board, activeElements, false);
duplicateElements(board, rectangle, [rectangle.x + offsetX, rectangle.y + offsetY], validElements);
}
isPreventDefault = false;
hitTargetElement = undefined;
selectedTargetElements = null;
Expand All @@ -153,6 +197,7 @@ export function withMoving(board: PlaitBoard) {

function cancelMove(board: PlaitBoard) {
snapG?.remove();
pendingNodesG?.remove();
startPoint = null;
activeElementsRectangle = null;
offsetX = 0;
Expand Down Expand Up @@ -221,10 +266,15 @@ export function getSelectedTargetElements(board: PlaitBoard) {
return targetElements;
}

export function updatePoints(board: PlaitBoard, targetElements: PlaitElement[], offsetX: number, offsetY: number) {
const validElements = targetElements.filter(
export function getValidElements(board: PlaitBoard, activeElements: PlaitElement[]) {
const validElements = [...activeElements].filter(
element => !PlaitGroupElement.isGroup(element) && board.children.findIndex(item => item.id === element.id) > -1
);
return validElements;
}

export function updatePoints(board: PlaitBoard, activeElements: PlaitElement[], offsetX: number, offsetY: number) {
const validElements = getValidElements(board, activeElements);
const currentElements = validElements.map(element => {
const points = element.points || [];
const newPoints = points.map(p => [p[0] + offsetX, p[1] + offsetY]) as Point[];
Expand All @@ -241,3 +291,41 @@ export function updatePoints(board: PlaitBoard, targetElements: PlaitElement[],
});
return currentElements;
}

export function drawPendingNodesG(board: PlaitBoard, activeElements: PlaitElement[], offsetX: number, offsetY: number) {
let pendingNodesG: SVGElement | null = null;
const elements: PlaitElement[] = [];
const validElements = getValidElements(board, activeElements);
validElements.forEach(element => {
depthFirstRecursion(
element,
node => {
elements.push(node);
},
() => true
);
});
elements.forEach(item => {
let rectangle = board.getRectangle(item);
if (rectangle) {
rectangle = {
x: rectangle.x + offsetX,
y: rectangle.y + offsetY,
width: rectangle.width,
height: rectangle.height
};
const movingG = drawRectangle(board, rectangle!, {
stroke: SELECTION_BORDER_COLOR,
strokeWidth: 1,
fill: SELECTION_FILL_COLOR,
fillStyle: 'solid'
});
if (!pendingNodesG) {
pendingNodesG = createG();
pendingNodesG.classList.add(ACTIVE_MOVING_CLASS_NAME);
}
pendingNodesG.append(movingG);
}
});
return pendingNodesG;
}
39 changes: 21 additions & 18 deletions packages/core/src/plugins/with-related-fragment.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
import { PlaitBoard, RectangleClient } from '../interfaces';
import {
WritableClipboardContext,
createClipboardContext,
WritableClipboardType,
addClipboardContext
} from '../utils';
import { PlaitBoard, PlaitElement, RectangleClient } from '../interfaces';
import { WritableClipboardContext, createClipboardContext, WritableClipboardType, addClipboardContext } from '../utils';

export function withRelatedFragment(board: PlaitBoard) {
const { buildFragment } = board;

board.buildFragment = (
clipboardContext: WritableClipboardContext | null,
rectangle: RectangleClient | null,
type: 'copy' | 'cut'
type: 'copy' | 'cut',
elements?: PlaitElement[]
) => {
const relatedFragment = board.getRelatedFragment([]);
if (!clipboardContext) {
clipboardContext = createClipboardContext(WritableClipboardType.elements, relatedFragment, '');
} else {
clipboardContext = addClipboardContext(clipboardContext, {
text: '',
type: WritableClipboardType.elements,
elements: relatedFragment
});
let relatedFragment = board.getRelatedFragment(elements || []);
if (relatedFragment) {
if (elements?.length) {
relatedFragment = relatedFragment.filter(item => !elements.map(element => element.id).includes(item.id));
}
if (relatedFragment.length) {
if (!clipboardContext) {
clipboardContext = createClipboardContext(WritableClipboardType.elements, relatedFragment, '');
} else {
clipboardContext = addClipboardContext(clipboardContext, {
text: '',
type: WritableClipboardType.elements,
elements: relatedFragment
});
}
}
}
return buildFragment(clipboardContext, rectangle, type);
return buildFragment(clipboardContext, rectangle, type, elements);
};

return board;
Expand Down
16 changes: 9 additions & 7 deletions packages/core/src/utils/fragment.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PlaitBoard, PlaitElement } from '../interfaces';
import { PlaitBoard, PlaitElement, Point, RectangleClient } from '../interfaces';
import { setClipboardData } from './clipboard/clipboard';
import { getRectangleByElements } from './element';
import { getSelectedElements } from './selected-element';
Expand All @@ -15,11 +15,13 @@ export const setFragment = (board: PlaitBoard, type: 'copy' | 'cut', clipboardDa
clipboardContext && setClipboardData(clipboardData, clipboardContext);
};


export const duplicateElements = (board: PlaitBoard, elements?: PlaitElement[]) => {
const selectedElements = elements || getSelectedElements(board);
const rectangle = getRectangleByElements(board, selectedElements, false);
const clipboardContext = board.buildFragment(null, rectangle, 'copy');
export const duplicateElements = (board: PlaitBoard, rectangle?: RectangleClient, point?: Point, element?: PlaitElement[]) => {
let targetRectangle = rectangle;
huanhuanwa marked this conversation as resolved.
Show resolved Hide resolved
if (!targetRectangle) {
const selectedElements = getSelectedElements(board);
targetRectangle = getRectangleByElements(board, selectedElements, false);
}
const clipboardContext = board.buildFragment(null, targetRectangle, 'copy', element);
const stringifiedContext = clipboardContext && JSON.stringify(clipboardContext);
const clonedContext = stringifiedContext && JSON.parse(stringifiedContext);
clonedContext &&
Expand All @@ -28,6 +30,6 @@ export const duplicateElements = (board: PlaitBoard, elements?: PlaitElement[])
...clonedContext,
text: undefined
},
[rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height / 2]
point || [targetRectangle.x + targetRectangle.width / 2, targetRectangle.y + targetRectangle.height / 2]
);
};
2 changes: 1 addition & 1 deletion packages/core/src/utils/group.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ const group2: PlaitElement[] = [
describe('group', () => {
let board: PlaitBoard;
beforeEach(() => {
board = createTestingBoard([], []);
board = createTestingBoard([], [...children, ...group1, ...group2]);
});

describe('getHighestSelectedElements', () => {
Expand Down
Loading
Loading