diff --git a/.changeset/four-dodos-sell.md b/.changeset/four-dodos-sell.md new file mode 100644 index 000000000..40a4e7e3e --- /dev/null +++ b/.changeset/four-dodos-sell.md @@ -0,0 +1,6 @@ +--- +'@plait/common': minor +'@plait/core': minor +--- + +support transform z index diff --git a/packages/common/src/transforms/index.ts b/packages/common/src/transforms/index.ts index b586040dd..dcdbceb77 100644 --- a/packages/common/src/transforms/index.ts +++ b/packages/common/src/transforms/index.ts @@ -1,3 +1,4 @@ export * from './property'; export * from './align'; export * from './text'; +export * from './z-index'; \ No newline at end of file diff --git a/packages/common/src/transforms/z-index.ts b/packages/common/src/transforms/z-index.ts new file mode 100644 index 000000000..36ee0fa6d --- /dev/null +++ b/packages/common/src/transforms/z-index.ts @@ -0,0 +1,18 @@ +import { PlaitBoard } from '@plait/core'; +import { getOneMoveOptions, moveElementsToNewPath } from '../utils'; + +const moveToTop = () => {}; + +const moveToBottom = () => {}; + +const moveUp = (board: PlaitBoard) => { + const moveOptions = getOneMoveOptions(board, 'up'); + moveElementsToNewPath(board, moveOptions); +}; + +const moveDown = (board: PlaitBoard) => { + const moveOptions = getOneMoveOptions(board, 'down'); + moveElementsToNewPath(board, moveOptions); +}; + +export const ZIndexTransforms = { moveUp, moveDown, moveToTop, moveToBottom }; diff --git a/packages/common/src/utils/index.ts b/packages/common/src/utils/index.ts index 8c3ee1b14..6204bf299 100644 --- a/packages/common/src/utils/index.ts +++ b/packages/common/src/utils/index.ts @@ -11,3 +11,4 @@ export * from './memorize'; export * from './vector'; export * from './math'; export * from './drawing'; +export * from './z-index'; diff --git a/packages/common/src/utils/z-index.ts b/packages/common/src/utils/z-index.ts new file mode 100644 index 000000000..46eaf249a --- /dev/null +++ b/packages/common/src/utils/z-index.ts @@ -0,0 +1,167 @@ +import { + PlaitBoard, + getSelectedElements, + getSelectedGroups, + PlaitElement, + PlaitGroupElement, + findLastIndex, + getEditingGroup, + getGroupByElement, + PlaitGroup, + getElementsInGroup, + Transforms, + Path, + findIndex +} from '@plait/core'; + +export interface ZIndexMoveOption { + element: PlaitElement; + newPath: Path; +} + +export const moveElementsToNewPath = (board: PlaitBoard, zIndexMoveOption: ZIndexMoveOption[]) => { + zIndexMoveOption + .map(item => { + const path = PlaitBoard.findPath(board, item.element); + const ref = board.pathRef(path); + return () => { + ref.current && Transforms.moveNode(board, ref.current, item.newPath); + ref.unref(); + }; + }) + .forEach(action => { + action(); + }); +}; + +export const getOneMoveOptions = (board: PlaitBoard, direction: 'down' | 'up'): ZIndexMoveOption[] => { + const indicesToMove = getIndicesToMove(board); + let groupedIndices = toContiguousGroups(board, indicesToMove); + if (direction === 'up') { + groupedIndices = groupedIndices.reverse(); + } + let moveContents: ZIndexMoveOption[] = []; + groupedIndices.forEach((indices, i) => { + const leadingIndex = indices[0]; + const trailingIndex = indices[indices.length - 1]; + const boundaryIndex = direction === 'down' ? leadingIndex : trailingIndex; + const targetIndex = getTargetIndex(board, boundaryIndex, direction); + if (targetIndex === -1 || boundaryIndex === targetIndex) { + return; + } + if (direction === 'down') { + moveContents.push( + ...indices.reverse().map(path => { + return { + element: board.children[path], + newPath: [targetIndex] + }; + }) + ); + } else { + moveContents.push( + ...indices.map(path => { + return { + element: board.children[path], + newPath: [targetIndex] + }; + }) + ); + } + }); + + return moveContents; +}; + +const getIndicesToMove = (board: PlaitBoard) => { + const selectedElements = [...getSelectedElements(board), ...getSelectedGroups(board)]; + return selectedElements + .map(item => { + return board.children.indexOf(item); + }) + .sort((a, b) => { + return a - b; + }); +}; + +const toContiguousGroups = (board: PlaitBoard, array: number[]) => { + let cursor = 0; + return array.reduce((acc, value, index) => { + if (index > 0) { + const currentElement = board.children[value]; + const previousElement = board.children[array[index - 1]]; + const isInSameGroup = currentElement.groupId === previousElement.groupId; + const isContain = currentElement.id === previousElement.groupId || currentElement.groupId === previousElement.id; + if (array[index - 1] !== value - 1 || (!isInSameGroup && !isContain)) { + cursor = ++cursor; + } + } + (acc[cursor] || (acc[cursor] = [])).push(value); + return acc; + }, [] as number[][]); +}; + +/** + * Returns next candidate index that's available to be moved to. Currently that + * is a non-deleted element, and not inside a group (unless we're editing it). + */ +const getTargetIndex = (board: PlaitBoard, boundaryIndex: number, direction: 'down' | 'up') => { + if ((boundaryIndex === 0 && direction === 'down') || (boundaryIndex === board.children.length - 1 && direction === 'up')) { + return -1; + } + const indexFilter = (element: PlaitElement) => { + if (element.isDeleted || PlaitGroupElement.isGroup(element)) { + return false; + } + return true; + }; + const candidateIndex = + direction === 'down' + ? findLastIndex(board.children, el => indexFilter(el), Math.max(0, boundaryIndex - 1)) + : findIndex(board.children, el => indexFilter(el), boundaryIndex + 1); + + const nextElement = board.children[candidateIndex]; + if (!nextElement) { + return -1; + } + + if (!nextElement.groupId) { + return candidateIndex; + } + + const elements = [...board.children]; + const sourceElement = elements[boundaryIndex]; + const editingGroup = getEditingGroup(board, sourceElement); + const nextElementGroups = (getGroupByElement(board, nextElement, true) || []) as PlaitGroup[]; + // candidate element is a sibling in current editing group → return + if (editingGroup && sourceElement?.groupId !== nextElement?.groupId) { + // candidate element is outside current editing group → prevent + if (!(nextElementGroups as PlaitGroup[]).find(item => item.id === editingGroup.id)) { + return -1; + } + } + let siblingGroup: PlaitGroup; + if (editingGroup) { + siblingGroup = nextElementGroups[nextElementGroups.indexOf(editingGroup) - 1]; + } else { + siblingGroup = nextElementGroups[nextElementGroups.length - 1]; + } + if (siblingGroup) { + let elementsInSiblingGroup = getElementsInGroup(board, siblingGroup, true, true); + if (elementsInSiblingGroup.length) { + elementsInSiblingGroup = [...elementsInSiblingGroup, siblingGroup]; + elementsInSiblingGroup.sort((a, b) => { + const indexA = board.children.findIndex(child => child.id === a.id); + const indexB = board.children.findIndex(child => child.id === b.id); + return indexA - indexB; + }); + // assumes getElementsInGroup() returned elements are sorted + // by zIndex (ascending) + return direction === 'down' + ? elements.indexOf(elementsInSiblingGroup[0]) + : elements.indexOf(elementsInSiblingGroup[elementsInSiblingGroup.length - 1]); + } + } + + return candidateIndex; +}; diff --git a/packages/core/src/utils/group.ts b/packages/core/src/utils/group.ts index 53f0efd6a..902ff8243 100644 --- a/packages/core/src/utils/group.ts +++ b/packages/core/src/utils/group.ts @@ -233,3 +233,18 @@ export const canRemoveGroup = (board: PlaitBoard, elements?: PlaitElement[]) => const selectedElements = elements || getSelectedElements(board); return selectedElements.length > 0 && selectedGroups.length > 0; }; + + +export const getEditingGroup = (board: PlaitBoard, element: PlaitElement) => { + const groups = getGroupByElement(board, element, true) as PlaitGroup[]; + let editingGroup = null; + if (groups?.length) { + for (let i = 0; i < groups?.length; i++) { + if (!isSelectedAllElementsInGroup(board, groups[i])) { + editingGroup = groups[i]; + break; + } + } + } + return editingGroup; +}; \ No newline at end of file diff --git a/packages/core/src/utils/helper.ts b/packages/core/src/utils/helper.ts index e4f1f7dae..cebf1ff71 100644 --- a/packages/core/src/utils/helper.ts +++ b/packages/core/src/utils/helper.ts @@ -1,4 +1,4 @@ -import { PlaitElement } from "../interfaces"; +import { PlaitElement } from '../interfaces'; export function isNullOrUndefined(value: any) { return value === null || value === undefined; @@ -37,3 +37,40 @@ export function uniqueById(elements: PlaitElement[]) { return Array.from(uniqueMap.values()); } + +export const findLastIndex = ( + array: readonly T[], + cb: (element: T, index: number, array: readonly T[]) => boolean, + fromIndex: number = array.length - 1 +) => { + if (fromIndex < 0) { + fromIndex = array.length + fromIndex; + } + fromIndex = Math.min(array.length - 1, Math.max(fromIndex, 0)); + let index = fromIndex + 1; + while (--index > -1) { + if (cb(array[index], index, array)) { + return index; + } + } + return -1; +}; + +export const findIndex = ( + array: readonly T[], + cb: (element: T, index: number, array: readonly T[]) => boolean, + fromIndex: number = 0 +) => { + // fromIndex = 2 + if (fromIndex < 0) { + fromIndex = array.length + fromIndex; + } + fromIndex = Math.min(array.length, Math.max(fromIndex, 0)); + let index = fromIndex - 1; + while (++index < array.length) { + if (cb(array[index], index, array)) { + return index; + } + } + return -1; +}; diff --git a/src/app/editor/editor.component.html b/src/app/editor/editor.component.html index a33eefb71..7e9da2d3f 100644 --- a/src/app/editor/editor.component.html +++ b/src/app/editor/editor.component.html @@ -17,6 +17,8 @@
+
上移一层
+
下移一层
成组
取消成组
复制
diff --git a/src/app/editor/editor.component.ts b/src/app/editor/editor.component.ts index 23757f4b1..e63b74c82 100644 --- a/src/app/editor/editor.component.ts +++ b/src/app/editor/editor.component.ts @@ -37,7 +37,7 @@ import { withCommonPlugin } from '../plugins/with-common'; import { AppMenuComponent } from '../components/menu/menu.component'; import { NgIf } from '@angular/common'; import { mockTurningPointData } from './mock-turning-point-data'; -import { withGroup } from '@plait/common'; +import { withGroup, ZIndexTransforms } from '@plait/common'; const LOCAL_STORAGE_KEY = 'plait-board-data'; @@ -169,6 +169,18 @@ export class BasicEditorComponent implements OnInit { BoardTransforms.updateThemeColor(this.board, value as ThemeColorMode); } + moveUp(event: MouseEvent) { + event.stopPropagation(); + event.preventDefault(); + ZIndexTransforms.moveUp(this.board); + } + + moveDown(event: MouseEvent) { + event.stopPropagation(); + event.preventDefault(); + ZIndexTransforms.moveDown(this.board); + } + copy(event: MouseEvent) { event.stopPropagation(); event.preventDefault();