Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions .changeset/fresh-table-resize.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@plait/draw': patch
---

Adjust table cell edge resize behavior
243 changes: 176 additions & 67 deletions packages/draw/src/plugins/with-table-resize.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { PlaitBoard, Point, RectangleClient, Transforms, isSelectedElement, getSelectedElements, hasValidAngle } from '@plait/core';
import { PlaitBaseTable, PlaitTableBoard, PlaitTableCellWithPoints } from '../interfaces/table';
import { PlaitBoard, Point, RectangleClient, Transforms, getSelectedElements, hasValidAngle } from '@plait/core';
import { PlaitBaseTable, PlaitTableBoard, PlaitTableCell, PlaitTableCellWithPoints } from '../interfaces/table';
import {
getIndexByResizeHandle,
isCornerHandle,
RESIZE_HANDLE_DIAMETER,
getRectangleResizeHandleRefs,
ResizeOptions,
ResizeHandle,
ResizeRef,
Expand All @@ -11,9 +13,9 @@ import {
WithResizeOptions,
normalizeShapePoints
} from '@plait/common';
import { getCellsWithPoints, updateColumns, updateRows } from '../utils/table';
import { getHitRectangleResizeHandleRef } from '../utils/position/geometry';
import { getResizeOriginPointAndHandlePoint, getResizeZoom, movePointByZoomAndOriginPoint } from './with-draw-resize';
import { getCellsWithPoints, getCurrentRowOrColumnSize, ResizeEdge, updateColumns, updateRows } from '../utils/table';
import { getHitRectangleResizeHandleRef, ResizeHandleRef } from '../utils/position/geometry';
import { getResizeOriginPointAndHandlePoint } from './with-draw-resize';
import { getSnapResizingRef, getSnapResizingRefOptions } from '../utils/snap-resizing';
import { PlaitDrawElement } from '../interfaces';
import { isSingleSelectTable } from '../utils';
Expand All @@ -27,6 +29,14 @@ interface TableResizeOptions extends ResizeOptions {

const MIN_CELL_SIZE = 20;

interface TableCellResizeHit {
cell: PlaitTableCellWithPoints;
rectangle: RectangleClient;
handleRef: ResizeHandleRef;
targetIndex: number;
distance: number;
}

export function withTableResize(board: PlaitTableBoard) {
let snapG: SVGGElement | null;

Expand All @@ -41,37 +51,55 @@ export function withTableResize(board: PlaitTableBoard) {
const hitElement = selectedElements[0];
// debugGenerator.clear();
if (hitElement && PlaitDrawElement.isElementByTable(hitElement)) {
let rectangle = board.getRectangle(hitElement) as RectangleClient;
// debugGenerator.drawRectangle(board, rectangle);
const tableRectangle = board.getRectangle(hitElement) as RectangleClient;
// debugGenerator.drawRectangle(board, tableRectangle);
// debugGenerator.drawCircles(board, [point], 5);
let handleRef = getHitRectangleResizeHandleRef(board, rectangle, point, hitElement.angle);
if (handleRef) {
const selectElement = isSelectedElement(board, hitElement);
if ((selectElement && isSingleSelectTable(board)) || (!selectElement && !isCornerHandle(board, handleRef.handle))) {
return {
element: hitElement,
handle: handleRef.handle,
cursorClass: handleRef.cursorClass,
rectangle
};
}
const tableHandleRef = getHitRectangleResizeHandleRef(board, tableRectangle, point, hitElement.angle);
if (tableHandleRef && isCornerHandle(board, tableHandleRef.handle)) {
return {
element: hitElement,
handle: tableHandleRef.handle,
cursorClass: tableHandleRef.cursorClass,
rectangle: tableRectangle
};
}

const cells = getCellsWithPoints(board, hitElement);
const cellResizeHits: TableCellResizeHit[] = [];
for (let i = 0; i < cells.length; i++) {
rectangle = RectangleClient.getRectangleByPoints(cells[i].points);
handleRef = getHitRectangleResizeHandleRef(board, rectangle, point, 0);
if (handleRef && !isCornerHandle(board, handleRef.handle)) {
return {
element: hitElement,
handle: handleRef.handle,
cursorClass: handleRef.cursorClass,
rectangle,
options: {
cell: cells[i]
const rectangle = RectangleClient.getRectangleByPoints(cells[i].points);
const handleRef = getCellResizeHandleRef(board, rectangle, point);
if (handleRef) {
const handleIndex = getIndexByResizeHandle(handleRef.handle);
const edge = getResizeEdge(handleIndex);
if (edge) {
const isRow = isRowResizeHandle(handleIndex);
const targetIndex = getResizeTargetRowOrColumnIndex(hitElement, cells[i], edge, isRow);
if (targetIndex >= 0) {
cellResizeHits.push({
cell: cells[i],
rectangle,
handleRef,
targetIndex,
distance: getResizeHandleDistance(rectangle, handleIndex, point)
});
}
};
}
}
}

const cellResizeHit = getStableCellResizeHit(cellResizeHits);
if (cellResizeHit) {
return {
element: hitElement,
handle: cellResizeHit.handleRef.handle,
cursorClass: cellResizeHit.handleRef.cursorClass,
rectangle: cellResizeHit.rectangle,
options: {
cell: cellResizeHit.cell
}
};
}
}
return null;
},
Expand All @@ -80,26 +108,43 @@ export function withTableResize(board: PlaitTableBoard) {
const path = PlaitBoard.findPath(board, resizeRef.element);
if (resizeRef.options?.cell && resizeRef.rectangle) {
const handleIndex = getIndexByResizeHandle(resizeRef.handle);
const { originPoint, handlePoint } = getResizeOriginPointAndHandlePoint(board, handleIndex, resizeRef.rectangle!);
const resizePoints: [Point, Point] = [resizeState.startPoint, resizeState.endPoint];
const { xZoom, yZoom } = getResizeZoom(resizePoints, originPoint, handlePoint, false, false);
const originPoints = resizeRef.options?.cell.points;
const targetPoints = originPoints.map((p) => {
return movePointByZoomAndOriginPoint(p, originPoint, xZoom, yZoom);
}) as [Point, Point];
const offsetX = targetPoints[1][0] - originPoints[1][0];
const offsetY = targetPoints[1][1] - originPoints[1][1];
const width = targetPoints[1][0] - targetPoints[0][0];
const height = targetPoints[1][1] - targetPoints[0][1];
if (offsetX !== 0 && width >= MIN_CELL_SIZE) {
const { columns, points } = updateColumns(resizeRef.element, resizeRef.options?.cell.columnId, width, offsetX);
Transforms.setNode(board, { columns, points }, path);
} else if (offsetY !== 0 && height >= MIN_CELL_SIZE) {
const { rows, points } = updateRows(resizeRef.element, resizeRef.options?.cell.rowId, height, offsetY);
Transforms.setNode(board, { rows, points }, path);
const edge = getResizeEdge(handleIndex);
if (edge) {
const isRow = isRowResizeHandle(handleIndex);
const pointerOffset = isRow
? resizeState.endPoint[1] - resizeState.startPoint[1]
: resizeState.endPoint[0] - resizeState.startPoint[0];
const targetIndex = getResizeTargetRowOrColumnIndex(resizeRef.element, resizeRef.options.cell, edge, isRow);
if (targetIndex < 0) {
return;
}
const currentSize = getCurrentRowOrColumnSize(resizeRef.element, targetIndex, isRow);
const sizeOffset = edge === 'start' ? -pointerOffset : pointerOffset;
const targetSize = Math.max(MIN_CELL_SIZE, currentSize + sizeOffset);
const appliedOffset = targetSize - currentSize;
if (appliedOffset !== 0) {
if (isRow) {
const { rows, points } = updateRows(
resizeRef.element,
resizeRef.element.rows[targetIndex].id,
targetSize,
appliedOffset,
edge
);
Transforms.setNode(board, { rows, points }, path);
} else {
const { columns, points } = updateColumns(
resizeRef.element,
resizeRef.element.columns[targetIndex].id,
targetSize,
appliedOffset,
edge
);
Transforms.setNode(board, { columns, points }, path);
}
}
}
} else {
const isFromCorner = isCornerHandle(board, resizeRef.handle);
} else if (isCornerHandle(board, resizeRef.handle)) {
const isAspectRatio = resizeState.isShift;
const handleIndex = getIndexByResizeHandle(resizeRef.handle);
const { originPoint, handlePoint } = getResizeOriginPointAndHandlePoint(board, handleIndex, resizeRef.rectangle!);
Expand All @@ -112,7 +157,7 @@ export function withTableResize(board: PlaitTableBoard) {
handlePoint
},
isAspectRatio,
isFromCorner
true
);
const resizeSnapRef = getSnapResizingRef(board, [resizeRef.element], resizeSnapRefOptions);
snapG = resizeSnapRef.snapG;
Expand All @@ -126,26 +171,10 @@ export function withTableResize(board: PlaitTableBoard) {
let columns = [...resizeRef.element.columns];
let rows = [...resizeRef.element.rows];
if (offsetWidth !== 0) {
columns = columns.map((item) => {
if (item.width) {
return {
...item,
width: item.width + offsetWidth * (item.width / originRect.width)
};
}
return item;
});
columns = scaleRowsOrColumns(columns, offsetWidth, originRect.width, false);
}
if (offsetHeight !== 0) {
rows = rows.map((item) => {
if (item.height) {
return {
...item,
height: item.height + offsetHeight * (item.height / originRect.height)
};
}
return item;
});
rows = scaleRowsOrColumns(rows, offsetHeight, originRect.height, true);
}
Transforms.setNode(board, { points: normalizeShapePoints(points), columns, rows }, path);
}
Expand All @@ -160,3 +189,83 @@ export function withTableResize(board: PlaitTableBoard) {

return board;
}

function getCellResizeHandleRef(board: PlaitBoard, rectangle: RectangleClient, point: Point) {
const handleRef = getHitRectangleResizeHandleRef(board, rectangle, point, 0);
if (handleRef && !isCornerHandle(board, handleRef.handle)) {
return handleRef;
}
return getHitRectangleSideResizeHandleRef(board, rectangle, point);
}

function getHitRectangleSideResizeHandleRef(board: PlaitBoard, rectangle: RectangleClient, point: Point): ResizeHandleRef | undefined {
const resizeHandleRefs = getRectangleResizeHandleRefs(rectangle, RESIZE_HANDLE_DIAMETER);
return resizeHandleRefs.find((resizeHandleRef) => {
return (
!isCornerHandle(board, resizeHandleRef.handle) &&
RectangleClient.isHit(RectangleClient.getRectangleByPoints([point, point]), resizeHandleRef.rectangle)
);
});
}

function getStableCellResizeHit(cellResizeHits: TableCellResizeHit[]) {
if (!cellResizeHits.length) {
return null;
}
return cellResizeHits.sort((previous, next) => {
return previous.distance - next.distance || previous.targetIndex - next.targetIndex;
})[0];
}

function isRowResizeHandle(handleIndex: number) {
return [Number(ResizeHandle.n), Number(ResizeHandle.s)].includes(handleIndex);
}

function getResizeHandleDistance(rectangle: RectangleClient, handleIndex: number, point: Point) {
switch (handleIndex) {
case Number(ResizeHandle.n):
return Math.abs(point[1] - rectangle.y);
case Number(ResizeHandle.e):
return Math.abs(point[0] - (rectangle.x + rectangle.width));
case Number(ResizeHandle.s):
return Math.abs(point[1] - (rectangle.y + rectangle.height));
case Number(ResizeHandle.w):
return Math.abs(point[0] - rectangle.x);
default:
return 0;
}
}

function scaleRowsOrColumns(data: { id: string; width?: number; height?: number }[], offset: number, originSize: number, isRow: boolean) {
const dimension = isRow ? 'height' : 'width';
return data.map((item) => {
if (item[dimension]) {
return {
...item,
[dimension]: item[dimension]! + offset * (item[dimension]! / originSize)
};
}
return item;
});
}

function getResizeEdge(handleIndex: number): ResizeEdge | undefined {
if ([Number(ResizeHandle.s), Number(ResizeHandle.e)].includes(handleIndex)) {
return 'end';
}
if ([Number(ResizeHandle.n), Number(ResizeHandle.w)].includes(handleIndex)) {
return 'start';
}
return undefined;
}

function getResizeTargetRowOrColumnIndex(element: PlaitBaseTable, resizeCell: PlaitTableCell, edge: ResizeEdge, isRow: boolean) {
const data = isRow ? element.rows : element.columns;
const id = isRow ? resizeCell.rowId : resizeCell.columnId;
const span = isRow ? resizeCell.rowspan : resizeCell.colspan;
let index = data.findIndex((item) => item.id === id);
if (edge === 'end' && span && span !== 1) {
index += span - 1;
}
return index;
}
23 changes: 19 additions & 4 deletions packages/draw/src/utils/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,15 +116,30 @@ export function getTextManageByCell(board: PlaitBoard, cell: PlaitTableCell) {
return getTextManage(board, undefined, cell);
}

export const updateColumns = (table: PlaitBaseTable, columnId: string, width: number, offset: number) => {
export type ResizeEdge = 'start' | 'end';

export function getCurrentRowOrColumnSize(table: PlaitBaseTable, index: number, isRow: boolean) {
const rectangle = RectangleClient.getRectangleByPoints(table.points);
const data = isRow ? table.rows : table.columns;
const sizes = calculateCellsSize(data, isRow ? rectangle.height : rectangle.width, data.length, !isRow);
return sizes[index] || 0;
}

export const updateColumns = (table: PlaitBaseTable, columnId: string, width: number, offset: number, edge: ResizeEdge = 'end') => {
const columns = table.columns.map((item) => (item.id === columnId ? { ...item, width } : item));
const points = [table.points[0], [table.points[1][0] + offset, table.points[1][1]]] as Point[];
let points = [table.points[0], [table.points[1][0] + offset, table.points[1][1]]] as Point[];
if (edge === 'start') {
points = [[table.points[0][0] - offset, table.points[0][1]], table.points[1]] as Point[];
}
return { columns, points };
};

export const updateRows = (table: PlaitBaseTable, rowId: string, height: number, offset: number) => {
export const updateRows = (table: PlaitBaseTable, rowId: string, height: number, offset: number, edge: ResizeEdge = 'end') => {
const rows = table.rows.map((item) => (item.id === rowId ? { ...item, height } : item));
const points = [table.points[0], [table.points[1][0], table.points[1][1] + offset]] as Point[];
let points = [table.points[0], [table.points[1][0], table.points[1][1] + offset]] as Point[];
if (edge === 'start') {
points = [[table.points[0][0], table.points[0][1] - offset], table.points[1]] as Point[];
}
return { rows, points };
};

Expand Down
Loading