kopia lustrzana https://github.com/Tldraw/Tldraw
Stickies: parenting (of frames and stickies) (#3297)
This PR adds parenting behaviour to stickies, and also changes how parenting works on the whole. - Rescaled timings for drag and drop manager. It should feel tighter, closer to figma. https://github.com/tldraw/tldraw/assets/15892272/c36c5bba-9964-403e-a5cd-6dcb35e1fe27 - We now 'kick out' any occluded children from their parents. We manually do this after many interactions and actions. - We now only parent shapes if their geometry overlaps *as well* as your cursor dragging over. - We now hint parents when translating a child inside it. - We hide the hint if your selected shape will be kicked out of a frame/note when you let go. - We now *only* hint if a child will successfully drop inside a parent. - Removed `shouldHint` option from `onDragShapesOver`. The editor handles that now. I will add more gifs demonstrating all these cases. ### Change Type <!-- ❗ Please select a 'Scope' label ❗️ --> - [x] `sdk` — Changes the tldraw SDK - [x] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff <!-- ❗ Please select a 'Type' label ❗️ --> - [ ] `bugfix` — Bug fix - [x] `feature` — New feature - [x] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know ### Test Plan 1. Add a step-by-step description of how to test your PR here. 2. - [ ] Unit Tests - [ ] End to end tests ### Release Notes - Add a brief release note for your PR here. --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com>pull/3328/head
rodzic
df7e3c4d31
commit
3f6b385880
|
@ -814,7 +814,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
pointerVelocity: Vec;
|
||||
};
|
||||
interrupt(): this;
|
||||
isAncestorSelected(shape: TLShape | TLShapeId): boolean;
|
||||
isIn(path: string): boolean;
|
||||
isInAny(...paths: string[]): boolean;
|
||||
isPointInShape(shape: TLShape | TLShapeId, point: VecLike, opts?: {
|
||||
|
@ -1446,6 +1445,9 @@ export class Polygon2d extends Polyline2d {
|
|||
});
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export function polygonIntersectsPolyline(polygon: VecLike[], polyline: VecLike[]): boolean;
|
||||
|
||||
// @public (undocumented)
|
||||
export function polygonsIntersect(a: VecLike[], b: VecLike[]): boolean;
|
||||
|
||||
|
@ -1655,9 +1657,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
onDoubleClickEdge?: TLOnDoubleClickHandler<Shape>;
|
||||
onDoubleClickHandle?: TLOnDoubleClickHandleHandler<Shape>;
|
||||
onDragShapesOut?: TLOnDragHandler<Shape>;
|
||||
onDragShapesOver?: TLOnDragHandler<Shape, {
|
||||
shouldHint: boolean;
|
||||
}>;
|
||||
onDragShapesOver?: TLOnDragHandler<Shape>;
|
||||
onDropShapesOver?: TLOnDragHandler<Shape>;
|
||||
onEditEnd?: TLOnEditEndHandler<Shape>;
|
||||
onHandleDrag?: TLOnHandleDragHandler<Shape>;
|
||||
|
|
|
@ -14578,64 +14578,6 @@
|
|||
"isAbstract": false,
|
||||
"name": "interrupt"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#isAncestorSelected:member(1)",
|
||||
"docComment": "/**\n * Determine whether or not any of a shape's ancestors are selected.\n *\n * @param id - The id of the shape to check.\n *\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "isAncestorSelected(shape: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLShape",
|
||||
"canonicalReference": "@tldraw/tlschema!TLShape:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": " | "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLShapeId",
|
||||
"canonicalReference": "@tldraw/tlschema!TLShapeId:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "): "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "boolean"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 5,
|
||||
"endIndex": 6
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
"overloadIndex": 1,
|
||||
"parameters": [
|
||||
{
|
||||
"parameterName": "shape",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 4
|
||||
},
|
||||
"isOptional": false
|
||||
}
|
||||
],
|
||||
"isOptional": false,
|
||||
"isAbstract": false,
|
||||
"name": "isAncestorSelected"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#isIn:member(1)",
|
||||
|
@ -28079,6 +28021,77 @@
|
|||
},
|
||||
"implementsTokenRanges": []
|
||||
},
|
||||
{
|
||||
"kind": "Function",
|
||||
"canonicalReference": "@tldraw/editor!polygonIntersectsPolyline:function(1)",
|
||||
"docComment": "/**\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "export declare function polygonIntersectsPolyline(polygon: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "VecLike",
|
||||
"canonicalReference": "@tldraw/editor!VecLike:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "[]"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", polyline: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "VecLike",
|
||||
"canonicalReference": "@tldraw/editor!VecLike:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "[]"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "): "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "boolean"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"fileUrlPath": "packages/editor/src/lib/primitives/intersect.ts",
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 7,
|
||||
"endIndex": 8
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"overloadIndex": 1,
|
||||
"parameters": [
|
||||
{
|
||||
"parameterName": "polygon",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 3
|
||||
},
|
||||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "polyline",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 4,
|
||||
"endIndex": 6
|
||||
},
|
||||
"isOptional": false
|
||||
}
|
||||
],
|
||||
"name": "polygonIntersectsPolyline"
|
||||
},
|
||||
{
|
||||
"kind": "Function",
|
||||
"canonicalReference": "@tldraw/editor!polygonsIntersect:function(1)",
|
||||
|
@ -31678,7 +31691,7 @@
|
|||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "@tldraw/editor!ShapeUtil#onDragShapesOver:member",
|
||||
"docComment": "/**\n * A callback called when some other shapes are dragged over this one.\n *\n * @param shape - The shape.\n *\n * @param shapes - The shapes that are being dragged over this one.\n *\n * @returns An object specifying whether the shape should hint that it can receive the dragged shapes.\n *\n * @example\n * ```ts\n * onDragShapesOver = (shape, shapes) => {\n * \treturn { shouldHint: true }\n * }\n * ```\n *\n * @public\n */\n",
|
||||
"docComment": "/**\n * A callback called when some other shapes are dragged over this one.\n *\n * @param shape - The shape.\n *\n * @param shapes - The shapes that are being dragged over this one.\n *\n * @example\n * ```ts\n * onDragShapesOver = (shape, shapes) => {\n * \tthis.editor.reparentShapes(shapes, shape.id)\n * }\n * ```\n *\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -31691,7 +31704,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<Shape, {\n shouldHint: boolean;\n }>"
|
||||
"text": "<Shape>"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
|
|
@ -301,6 +301,7 @@ export {
|
|||
intersectPolygonBounds,
|
||||
intersectPolygonPolygon,
|
||||
linesIntersect,
|
||||
polygonIntersectsPolyline,
|
||||
polygonsIntersect,
|
||||
} from './lib/primitives/intersect'
|
||||
export {
|
||||
|
|
|
@ -1517,21 +1517,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Determine whether or not any of a shape's ancestors are selected.
|
||||
*
|
||||
* @param id - The id of the shape to check.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
isAncestorSelected(shape: TLShape | TLShapeId): boolean {
|
||||
const id = typeof shape === 'string' ? shape : shape?.id ?? null
|
||||
const _shape = this.getShape(id)
|
||||
if (!_shape) return false
|
||||
const selectedShapeIds = this.getSelectedShapeIds()
|
||||
return !!this.findShapeAncestor(_shape, (parent) => selectedShapeIds.includes(parent.id))
|
||||
}
|
||||
|
||||
/**
|
||||
* Select one or more shapes.
|
||||
*
|
||||
|
@ -4053,7 +4038,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
/** @internal */
|
||||
@computed private _getShapeMaskCache(): ComputedCache<Vec[], TLShape> {
|
||||
return this.store.createComputedCache('pageMaskCache', (shape) => {
|
||||
if (isPageId(shape.parentId)) return undefined
|
||||
// todo: Consider adding a flag for this hardcoded behaviour
|
||||
if (
|
||||
isPageId(shape.parentId) ||
|
||||
shape.type === 'note' ||
|
||||
this.findShapeAncestor(shape, (v) => v.type === 'note')
|
||||
)
|
||||
return undefined
|
||||
|
||||
const frameAncestors = this.getShapeAncestors(shape.id).filter((shape) =>
|
||||
this.isShapeOfType<TLFrameShape>(shape, 'frame')
|
||||
|
@ -5027,6 +5018,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
const shape = currentPageShapesSorted[i]
|
||||
|
||||
if (
|
||||
// don't allow dropping on selected shapes
|
||||
this.getSelectedShapeIds().includes(shape.id) ||
|
||||
// only allow shapes that can receive children
|
||||
!this.getShapeUtil(shape).canDropShapes(shape, droppingShapes) ||
|
||||
// don't allow dropping a shape on itself or one of it's children
|
||||
|
|
|
@ -334,16 +334,15 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
*
|
||||
* ```ts
|
||||
* onDragShapesOver = (shape, shapes) => {
|
||||
* return { shouldHint: true }
|
||||
* this.editor.reparentShapes(shapes, shape.id)
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param shape - The shape.
|
||||
* @param shapes - The shapes that are being dragged over this one.
|
||||
* @returns An object specifying whether the shape should hint that it can receive the dragged shapes.
|
||||
* @public
|
||||
*/
|
||||
onDragShapesOver?: TLOnDragHandler<Shape, { shouldHint: boolean }>
|
||||
onDragShapesOver?: TLOnDragHandler<Shape>
|
||||
|
||||
/**
|
||||
* A callback called when some other shapes are dragged out of this one.
|
||||
|
|
|
@ -316,3 +316,19 @@ export function polygonsIntersect(a: VecLike[], b: VecLike[]) {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function polygonIntersectsPolyline(polygon: VecLike[], polyline: VecLike[]) {
|
||||
let a: VecLike, b: VecLike, c: VecLike, d: VecLike
|
||||
for (let i = 0, n = polygon.length; i < n; i++) {
|
||||
a = polygon[i]
|
||||
b = polygon[(i + 1) % n]
|
||||
|
||||
for (let j = 1, m = polyline.length; j < m; j++) {
|
||||
c = polyline[j - 1]
|
||||
d = polyline[j]
|
||||
if (linesIntersect(a, b, c, d)) return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -94,7 +94,6 @@ import { TLOnBeforeUpdateHandler } from '@tldraw/editor';
|
|||
import { TLOnDoubleClickHandler } from '@tldraw/editor';
|
||||
import { TLOnEditEndHandler } from '@tldraw/editor';
|
||||
import { TLOnHandleDragHandler } from '@tldraw/editor';
|
||||
import { TLOnResizeEndHandler } from '@tldraw/editor';
|
||||
import { TLOnResizeHandler } from '@tldraw/editor';
|
||||
import { TLOnTranslateHandler } from '@tldraw/editor';
|
||||
import { TLOnTranslateStartHandler } from '@tldraw/editor';
|
||||
|
@ -659,14 +658,10 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
|
|||
// (undocumented)
|
||||
onDragShapesOut: (_shape: TLFrameShape, shapes: TLShape[]) => void;
|
||||
// (undocumented)
|
||||
onDragShapesOver: (frame: TLFrameShape, shapes: TLShape[]) => {
|
||||
shouldHint: boolean;
|
||||
};
|
||||
onDragShapesOver: (frame: TLFrameShape, shapes: TLShape[]) => void;
|
||||
// (undocumented)
|
||||
onResize: TLOnResizeHandler<any>;
|
||||
// (undocumented)
|
||||
onResizeEnd: TLOnResizeEndHandler<TLFrameShape>;
|
||||
// (undocumented)
|
||||
static props: {
|
||||
w: Validator<number>;
|
||||
h: Validator<number>;
|
||||
|
@ -974,9 +969,15 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
|
|||
// @public (undocumented)
|
||||
export function isGifAnimated(file: Blob): Promise<boolean>;
|
||||
|
||||
// @internal (undocumented)
|
||||
export function isShapeOccluded(editor: Editor, occluder: TLShape, shape: TLShapeId): boolean;
|
||||
|
||||
// @public (undocumented)
|
||||
export function KeyboardShortcutsMenuItem(): JSX_2.Element | null;
|
||||
|
||||
// @internal (undocumented)
|
||||
export function kickoutOccludedShapes(editor: Editor, shapeIds: TLShapeId[]): void;
|
||||
|
||||
// @public (undocumented)
|
||||
export const LABEL_FONT_SIZES: Record<TLDefaultSizeStyle, number>;
|
||||
|
||||
|
@ -1096,9 +1097,13 @@ export class NoteShapeTool extends StateNode {
|
|||
|
||||
// @public (undocumented)
|
||||
export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
||||
// (undocumented)
|
||||
canDropShapes: (shape: TLNoteShape, _shapes: TLShape[]) => boolean;
|
||||
// (undocumented)
|
||||
canEdit: () => boolean;
|
||||
// (undocumented)
|
||||
canReceiveNewChildrenOfType: (shape: TLNoteShape, type: string) => boolean;
|
||||
// (undocumented)
|
||||
component(shape: TLNoteShape): JSX_2.Element;
|
||||
// (undocumented)
|
||||
doesAutoEditOnKeyStroke: () => boolean;
|
||||
|
@ -1169,6 +1174,10 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
typeName: "shape";
|
||||
} | undefined;
|
||||
// (undocumented)
|
||||
onDragShapesOut: (note: TLNoteShape, shapes: TLShape[]) => void;
|
||||
// (undocumented)
|
||||
onDragShapesOver: (note: TLNoteShape, shapes: TLShape[]) => void;
|
||||
// (undocumented)
|
||||
onEditEnd: TLOnEditEndHandler<TLNoteShape>;
|
||||
// (undocumented)
|
||||
static props: {
|
||||
|
|
|
@ -7611,7 +7611,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "[]) => {\n shouldHint: boolean;\n }"
|
||||
"text": "[]) => void"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -7665,50 +7665,6 @@
|
|||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "tldraw!FrameShapeUtil#onResizeEnd:member",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "onResizeEnd: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLOnResizeEndHandler",
|
||||
"canonicalReference": "@tldraw/editor!TLOnResizeEndHandler:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<"
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLFrameShape",
|
||||
"canonicalReference": "@tldraw/tlschema!TLFrameShape:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ">"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isReadonly": false,
|
||||
"isOptional": false,
|
||||
"releaseTag": "Public",
|
||||
"name": "onResizeEnd",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 5
|
||||
},
|
||||
"isStatic": false,
|
||||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "tldraw!FrameShapeUtil.props:member",
|
||||
|
@ -12824,6 +12780,54 @@
|
|||
"name": "NoteShapeUtil",
|
||||
"preserveMemberOrder": false,
|
||||
"members": [
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "tldraw!NoteShapeUtil#canDropShapes:member",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "canDropShapes: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "(shape: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLNoteShape",
|
||||
"canonicalReference": "@tldraw/tlschema!TLNoteShape:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", _shapes: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLShape",
|
||||
"canonicalReference": "@tldraw/tlschema!TLShape:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "[]) => boolean"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isReadonly": false,
|
||||
"isOptional": false,
|
||||
"releaseTag": "Public",
|
||||
"name": "canDropShapes",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 6
|
||||
},
|
||||
"isStatic": false,
|
||||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "tldraw!NoteShapeUtil#canEdit:member",
|
||||
|
@ -12854,6 +12858,45 @@
|
|||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "tldraw!NoteShapeUtil#canReceiveNewChildrenOfType:member",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "canReceiveNewChildrenOfType: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "(shape: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLNoteShape",
|
||||
"canonicalReference": "@tldraw/tlschema!TLNoteShape:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", type: string) => boolean"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isReadonly": false,
|
||||
"isOptional": false,
|
||||
"releaseTag": "Public",
|
||||
"name": "canReceiveNewChildrenOfType",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 4
|
||||
},
|
||||
"isStatic": false,
|
||||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "tldraw!NoteShapeUtil#component:member(1)",
|
||||
|
@ -13435,6 +13478,102 @@
|
|||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "tldraw!NoteShapeUtil#onDragShapesOut:member",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "onDragShapesOut: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "(note: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLNoteShape",
|
||||
"canonicalReference": "@tldraw/tlschema!TLNoteShape:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", shapes: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLShape",
|
||||
"canonicalReference": "@tldraw/tlschema!TLShape:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "[]) => void"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isReadonly": false,
|
||||
"isOptional": false,
|
||||
"releaseTag": "Public",
|
||||
"name": "onDragShapesOut",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 6
|
||||
},
|
||||
"isStatic": false,
|
||||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "tldraw!NoteShapeUtil#onDragShapesOver:member",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "onDragShapesOver: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "(note: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLNoteShape",
|
||||
"canonicalReference": "@tldraw/tlschema!TLNoteShape:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", shapes: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLShape",
|
||||
"canonicalReference": "@tldraw/tlschema!TLShape:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "[]) => void"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isReadonly": false,
|
||||
"isOptional": false,
|
||||
"releaseTag": "Public",
|
||||
"name": "onDragShapesOver",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 6
|
||||
},
|
||||
"isStatic": false,
|
||||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "tldraw!NoteShapeUtil#onEditEnd:member",
|
||||
|
|
|
@ -41,6 +41,7 @@ export { EraserTool } from './lib/tools/EraserTool/EraserTool'
|
|||
export { HandTool } from './lib/tools/HandTool/HandTool'
|
||||
export { LaserTool } from './lib/tools/LaserTool/LaserTool'
|
||||
export { SelectTool } from './lib/tools/SelectTool/SelectTool'
|
||||
export { isShapeOccluded, kickoutOccludedShapes } from './lib/tools/SelectTool/selectHelpers'
|
||||
export { ZoomTool } from './lib/tools/ZoomTool/ZoomTool'
|
||||
// UI
|
||||
export { useEditableText } from './lib/shapes/shared/useEditableText'
|
||||
|
|
|
@ -306,15 +306,20 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
// If no bound shapes are in the selection, unbind any bound shapes
|
||||
|
||||
const selectedShapeIds = this.editor.getSelectedShapeIds()
|
||||
|
||||
if (
|
||||
(startBindingId &&
|
||||
(selectedShapeIds.includes(startBindingId) ||
|
||||
this.editor.isAncestorSelected(startBindingId))) ||
|
||||
(endBindingId &&
|
||||
(selectedShapeIds.includes(endBindingId) || this.editor.isAncestorSelected(endBindingId)))
|
||||
) {
|
||||
return
|
||||
const shapesToCheck = new Set<string>()
|
||||
if (startBindingId) {
|
||||
// Add shape and all ancestors to set
|
||||
shapesToCheck.add(startBindingId)
|
||||
this.editor.getShapeAncestors(startBindingId).forEach((a) => shapesToCheck.add(a.id))
|
||||
}
|
||||
if (endBindingId) {
|
||||
// Add shape and all ancestors to set
|
||||
shapesToCheck.add(endBindingId)
|
||||
this.editor.getShapeAncestors(endBindingId).forEach((a) => shapesToCheck.add(a.id))
|
||||
}
|
||||
// If any of the shapes are selected, return
|
||||
for (const id of selectedShapeIds) {
|
||||
if (shapesToCheck.has(id)) return
|
||||
}
|
||||
|
||||
let result = shape
|
||||
|
|
|
@ -6,10 +6,8 @@ import {
|
|||
SvgExportContext,
|
||||
TLFrameShape,
|
||||
TLGroupShape,
|
||||
TLOnResizeEndHandler,
|
||||
TLOnResizeHandler,
|
||||
TLShape,
|
||||
TLShapeId,
|
||||
canonicalizeRotation,
|
||||
frameShapeMigrations,
|
||||
frameShapeProps,
|
||||
|
@ -207,15 +205,10 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
|
|||
return !shape.isLocked
|
||||
}
|
||||
|
||||
override onDragShapesOver = (frame: TLFrameShape, shapes: TLShape[]): { shouldHint: boolean } => {
|
||||
override onDragShapesOver = (frame: TLFrameShape, shapes: TLShape[]) => {
|
||||
if (!shapes.every((child) => child.parentId === frame.id)) {
|
||||
this.editor.reparentShapes(
|
||||
shapes.map((shape) => shape.id),
|
||||
frame.id
|
||||
)
|
||||
return { shouldHint: true }
|
||||
this.editor.reparentShapes(shapes, frame.id)
|
||||
}
|
||||
return { shouldHint: false }
|
||||
}
|
||||
|
||||
override onDragShapesOut = (_shape: TLFrameShape, shapes: TLShape[]): void => {
|
||||
|
@ -232,24 +225,6 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
|
|||
}
|
||||
}
|
||||
|
||||
override onResizeEnd: TLOnResizeEndHandler<TLFrameShape> = (shape) => {
|
||||
const bounds = this.editor.getShapePageBounds(shape)!
|
||||
const children = this.editor.getSortedChildIdsForParent(shape.id)
|
||||
|
||||
const shapesToReparent: TLShapeId[] = []
|
||||
|
||||
for (const childId of children) {
|
||||
const childBounds = this.editor.getShapePageBounds(childId)!
|
||||
if (!bounds.includes(childBounds)) {
|
||||
shapesToReparent.push(childId)
|
||||
}
|
||||
}
|
||||
|
||||
if (shapesToReparent.length > 0) {
|
||||
this.editor.reparentShapes(shapesToReparent, this.editor.getCurrentPageId())
|
||||
}
|
||||
}
|
||||
|
||||
override onResize: TLOnResizeHandler<any> = (shape, info) => {
|
||||
return resizeBox(shape, info)
|
||||
}
|
||||
|
|
|
@ -204,17 +204,17 @@ describe('Grid placement helpers', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('Does note create a new sticky note in a sticky pit if a note is already there', () => {
|
||||
it('Does not create a new sticky note in a sticky pit if a note is already there', () => {
|
||||
editor
|
||||
.createShape({ type: 'note', x: 0, y: 0 })
|
||||
.createShape({ type: 'note', x: 228, y: 8 }) // make a shape kinda there already!
|
||||
.createShape({ type: 'note', x: 330, y: 8 }) // make a shape kinda there already!
|
||||
.setCurrentTool('note')
|
||||
.pointerMove(324, 104)
|
||||
.pointerMove(300, 104)
|
||||
.click()
|
||||
.expectShapeToMatch({
|
||||
...editor.getLastCreatedShape(),
|
||||
// outta da pit
|
||||
x: 224,
|
||||
x: 200,
|
||||
y: 4,
|
||||
})
|
||||
})
|
||||
|
|
|
@ -4,9 +4,11 @@ import {
|
|||
Rectangle2d,
|
||||
ShapeUtil,
|
||||
SvgExportContext,
|
||||
TLGroupShape,
|
||||
TLHandle,
|
||||
TLNoteShape,
|
||||
TLOnEditEndHandler,
|
||||
TLShape,
|
||||
TLShapeId,
|
||||
Vec,
|
||||
getDefaultColorTheme,
|
||||
|
@ -26,6 +28,7 @@ import { SvgTextLabel } from '../shared/SvgTextLabel'
|
|||
import { TextLabel } from '../shared/TextLabel'
|
||||
import { FONT_FAMILIES, LABEL_FONT_SIZES, TEXT_PROPS } from '../shared/default-shape-constants'
|
||||
import { getFontDefForExport } from '../shared/defaultStyleDefs'
|
||||
|
||||
import { useForceSolid } from '../shared/useForceSolid'
|
||||
import {
|
||||
ADJACENT_NOTE_MARGIN,
|
||||
|
@ -47,6 +50,36 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
override hideResizeHandles = () => true
|
||||
override hideSelectionBoundsFg = () => false
|
||||
|
||||
override canReceiveNewChildrenOfType = (shape: TLNoteShape, type: string) => {
|
||||
return !shape.isLocked && type !== 'frame'
|
||||
}
|
||||
|
||||
override canDropShapes = (shape: TLNoteShape, _shapes: TLShape[]): boolean => {
|
||||
return !shape.isLocked
|
||||
}
|
||||
|
||||
override onDragShapesOver = (note: TLNoteShape, shapes: TLShape[]) => {
|
||||
if (!shapes.every((child) => child.parentId === note.id)) {
|
||||
const shapesWithoutFrames = shapes.filter(
|
||||
(shape) => !this.editor.isShapeOfType(shape, 'frame')
|
||||
)
|
||||
this.editor.reparentShapes(shapesWithoutFrames, note.id)
|
||||
}
|
||||
}
|
||||
|
||||
override onDragShapesOut = (note: TLNoteShape, shapes: TLShape[]) => {
|
||||
const parent = this.editor.getShape(note.parentId)
|
||||
const isInGroup = parent && this.editor.isShapeOfType<TLGroupShape>(parent, 'group')
|
||||
|
||||
// If sticky is in a group, keep the shape in that group
|
||||
|
||||
if (isInGroup) {
|
||||
this.editor.reparentShapes(shapes, parent.id)
|
||||
} else {
|
||||
this.editor.reparentShapes(shapes, this.editor.getCurrentPageId())
|
||||
}
|
||||
}
|
||||
|
||||
getDefaultProps(): TLNoteShape['props'] {
|
||||
return {
|
||||
color: 'black',
|
||||
|
|
|
@ -183,7 +183,7 @@ it('Creates an adjacent note when dragging the clone handle', () => {
|
|||
|
||||
const handles = editor.getShapeHandles(shapeB.id)!
|
||||
|
||||
const handle = handles[1]
|
||||
const handle = handles[0]
|
||||
|
||||
editor.select(shapeB.id)
|
||||
editor.pointerDown(handle.x, handle.y, {
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
import { TLShapeId } from '@tldraw/editor'
|
||||
import { TestEditor } from '../../../test/TestEditor'
|
||||
|
||||
let editor: TestEditor
|
||||
|
||||
beforeEach(() => {
|
||||
editor = new TestEditor()
|
||||
})
|
||||
afterEach(() => {
|
||||
editor?.dispose()
|
||||
})
|
||||
|
||||
function clickCreate(tool: string, [x, y]: [number, number]): TLShapeId {
|
||||
editor.setCurrentTool('note')
|
||||
editor.pointerDown(x, y)
|
||||
editor.pointerUp(x, y)
|
||||
const shapes = editor.getSelectedShapes()
|
||||
const noteId = shapes[0].id
|
||||
return noteId
|
||||
}
|
||||
|
||||
function dragCreate(
|
||||
tool: string,
|
||||
{
|
||||
from,
|
||||
to,
|
||||
}: {
|
||||
from: [number, number]
|
||||
to: [number, number]
|
||||
}
|
||||
): TLShapeId {
|
||||
editor.setCurrentTool(tool)
|
||||
editor.pointerDown(...from)
|
||||
editor.pointerMove(...to)
|
||||
editor.pointerUp(...to)
|
||||
const shapes = editor.getSelectedShapes()
|
||||
const rectId = shapes[0].id
|
||||
return rectId
|
||||
}
|
||||
|
||||
describe('note parenting', () => {
|
||||
it('accepts shapes as children', () => {
|
||||
const noteId = clickCreate('note', [0, 0])
|
||||
const rectId = dragCreate('geo', { from: [-50, -50], to: [50, 50] })
|
||||
expect(editor.getShape(rectId)!.parentId).toBe(noteId)
|
||||
})
|
||||
|
||||
it("doesn't accept frames as children", () => {
|
||||
clickCreate('note', [0, 0])
|
||||
const frameId = dragCreate('frame', { from: [-50, -50], to: [50, 50] })
|
||||
expect(editor.getShape(frameId)!.parentId).toBe(editor.getCurrentPageId())
|
||||
})
|
||||
|
||||
it('parents shapes when you drag them onto the note', () => {
|
||||
const noteId = clickCreate('note', [0, 0])
|
||||
const rectId = dragCreate('geo', { from: [200, 200], to: [300, 300] })
|
||||
|
||||
expect(editor.getShape(rectId)!.parentId).toBe(editor.getCurrentPageId())
|
||||
editor.pointerDown(250, 250)
|
||||
editor.pointerMove(0, 0)
|
||||
jest.advanceTimersByTime(200)
|
||||
expect(editor.getShape(rectId)!.parentId).toBe(noteId)
|
||||
editor.pointerUp()
|
||||
})
|
||||
|
||||
it("doesn't parent frames when you drag them onto the note", () => {
|
||||
clickCreate('note', [0, 0])
|
||||
const frameId = dragCreate('frame', { from: [200, 200], to: [300, 300] })
|
||||
|
||||
expect(editor.getShape(frameId)!.parentId).toBe(editor.getCurrentPageId())
|
||||
editor.pointerDown(250, 250)
|
||||
editor.pointerMove(0, 0)
|
||||
jest.advanceTimersByTime(200)
|
||||
expect(editor.getShape(frameId)!.parentId).toBe(editor.getCurrentPageId())
|
||||
editor.pointerUp()
|
||||
})
|
||||
})
|
|
@ -1,6 +1,8 @@
|
|||
import { Editor, TLShape, TLShapeId, Vec, compact } from '@tldraw/editor'
|
||||
import { isShapeOccluded } from './selectHelpers'
|
||||
|
||||
const LAG_DURATION = 100
|
||||
const INITIAL_POINTER_LAG_DURATION = 20
|
||||
const FAST_POINTER_LAG_DURATION = 100
|
||||
|
||||
/** @public */
|
||||
export class DragAndDropManager {
|
||||
|
@ -16,6 +18,12 @@ export class DragAndDropManager {
|
|||
|
||||
updateDroppingNode(movingShapes: TLShape[], cb: () => void) {
|
||||
if (this.first) {
|
||||
this.editor.setHintingShapes(
|
||||
movingShapes
|
||||
.map((s) => this.editor.findShapeAncestor(s, (v) => v.type !== 'group'))
|
||||
.filter((s) => s) as TLShape[]
|
||||
)
|
||||
|
||||
this.prevDroppingShapeId =
|
||||
this.editor.getDroppingOverShape(this.editor.inputs.originPagePoint, movingShapes)?.id ??
|
||||
null
|
||||
|
@ -23,10 +31,10 @@ export class DragAndDropManager {
|
|||
}
|
||||
|
||||
if (this.droppingNodeTimer === null) {
|
||||
this.setDragTimer(movingShapes, LAG_DURATION * 10, cb)
|
||||
this.setDragTimer(movingShapes, INITIAL_POINTER_LAG_DURATION, cb)
|
||||
} else if (this.editor.inputs.pointerVelocity.len() > 0.5) {
|
||||
clearInterval(this.droppingNodeTimer)
|
||||
this.setDragTimer(movingShapes, LAG_DURATION, cb)
|
||||
this.setDragTimer(movingShapes, FAST_POINTER_LAG_DURATION, cb)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -46,6 +54,7 @@ export class DragAndDropManager {
|
|||
|
||||
// is the next dropping shape id different than the last one?
|
||||
if (nextDroppingShapeId === this.prevDroppingShapeId) {
|
||||
this.hintParents(movingShapes)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -64,24 +73,42 @@ export class DragAndDropManager {
|
|||
}
|
||||
|
||||
if (nextDroppingShape) {
|
||||
const res = this.editor
|
||||
this.editor
|
||||
.getShapeUtil(nextDroppingShape)
|
||||
.onDragShapesOver?.(nextDroppingShape, movingShapes)
|
||||
|
||||
if (res && res.shouldHint) {
|
||||
this.editor.setHintingShapes([nextDroppingShape.id])
|
||||
}
|
||||
} else {
|
||||
// If we're dropping onto the page, then clear hinting ids
|
||||
this.editor.setHintingShapes([])
|
||||
}
|
||||
|
||||
this.hintParents(movingShapes)
|
||||
cb?.()
|
||||
|
||||
// next -> curr
|
||||
this.prevDroppingShapeId = nextDroppingShapeId
|
||||
}
|
||||
|
||||
hintParents(movingShapes: TLShape[]) {
|
||||
// Group moving shapes by their ancestor
|
||||
const shapesGroupedByAncestor = new Map<TLShapeId, TLShape[]>()
|
||||
for (const shape of movingShapes) {
|
||||
const ancestor = this.editor.findShapeAncestor(shape, (v) => v.type !== 'group')
|
||||
if (!ancestor) continue
|
||||
const shapes = shapesGroupedByAncestor.get(ancestor.id) ?? []
|
||||
shapes.push(shape)
|
||||
shapesGroupedByAncestor.set(ancestor.id, shapes)
|
||||
}
|
||||
|
||||
// Only hint an ancestor if some shapes will drop into it on pointer up
|
||||
const hintingShapes = []
|
||||
for (const [ancestorId, shapes] of shapesGroupedByAncestor) {
|
||||
const ancestor = this.editor.getShape(ancestorId)
|
||||
if (!ancestor) continue
|
||||
if (shapes.some((shape) => !isShapeOccluded(this.editor, ancestor, shape.id))) {
|
||||
hintingShapes.push(ancestor.id)
|
||||
}
|
||||
}
|
||||
|
||||
this.editor.setHintingShapes(hintingShapes)
|
||||
}
|
||||
|
||||
dropShapes(shapes: TLShape[]) {
|
||||
const { prevDroppingShapeId } = this
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
Vec,
|
||||
structuredClone,
|
||||
} from '@tldraw/editor'
|
||||
import { kickoutOccludedShapes } from '../selectHelpers'
|
||||
import { MIN_CROP_SIZE } from './Crop/crop-constants'
|
||||
import { CursorTypeMap } from './PointingResizeHandle'
|
||||
|
||||
|
@ -206,6 +207,7 @@ export class Cropping extends StateNode {
|
|||
|
||||
private complete() {
|
||||
this.updateShapes()
|
||||
kickoutOccludedShapes(this.editor, [this.snapshot.shape.id])
|
||||
if (this.info.onInteractionEnd) {
|
||||
this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)
|
||||
} else {
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
sortByIndex,
|
||||
structuredClone,
|
||||
} from '@tldraw/editor'
|
||||
import { kickoutOccludedShapes } from '../selectHelpers'
|
||||
|
||||
export class DraggingHandle extends StateNode {
|
||||
static override id = 'dragging_handle'
|
||||
|
@ -203,6 +204,7 @@ export class DraggingHandle extends StateNode {
|
|||
|
||||
private complete() {
|
||||
this.editor.snaps.clearIndicators()
|
||||
kickoutOccludedShapes(this.editor, [this.shapeId])
|
||||
|
||||
const { onInteractionEnd } = this.info
|
||||
if (this.editor.getInstanceState().isToolLocked && onInteractionEnd) {
|
||||
|
|
|
@ -19,6 +19,7 @@ import { getHitShapeOnCanvasPointerDown } from '../../selection-logic/getHitShap
|
|||
import { getShouldEnterCropMode } from '../../selection-logic/getShouldEnterCropModeOnPointerDown'
|
||||
import { selectOnCanvasPointerUp } from '../../selection-logic/selectOnCanvasPointerUp'
|
||||
import { updateHoveredId } from '../../selection-logic/updateHoveredId'
|
||||
import { kickoutOccludedShapes } from '../selectHelpers'
|
||||
|
||||
const SKIPPED_KEYS_FOR_AUTO_EDITING = [
|
||||
'Delete',
|
||||
|
@ -268,6 +269,7 @@ export class Idle extends StateNode {
|
|||
if (change) {
|
||||
this.editor.mark('double click edge')
|
||||
this.editor.updateShapes([change])
|
||||
kickoutOccludedShapes(this.editor, [onlySelectedShape.id])
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,9 @@ export class PointingShape extends StateNode {
|
|||
|
||||
this.hitShape = info.shape
|
||||
const outermostSelectingShape = this.editor.getOutermostSelectableShape(info.shape)
|
||||
const selectedAncestor = this.editor.findShapeAncestor(outermostSelectingShape, (parent) =>
|
||||
selectedShapeIds.includes(parent.id)
|
||||
)
|
||||
|
||||
if (
|
||||
// If the shape has an onClick handler
|
||||
|
@ -33,7 +36,9 @@ export class PointingShape extends StateNode {
|
|||
outermostSelectingShape.id === focusedGroupId ||
|
||||
// ...or if the shape is within the selection
|
||||
selectedShapeIds.includes(outermostSelectingShape.id) ||
|
||||
this.editor.isAncestorSelected(outermostSelectingShape.id) ||
|
||||
// ...or if an ancestor of the shape is selected (except note shapes)...
|
||||
// todo: Consider adding a flag for this hardcoded behaviour
|
||||
(selectedAncestor && selectedAncestor.type !== 'note') ||
|
||||
// ...or if the current point is NOT within the selection bounds
|
||||
(selectedShapeIds.length > 1 && selectionBounds?.containsPoint(currentPagePoint))
|
||||
) {
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
compact,
|
||||
moveCameraWhenCloseToEdge,
|
||||
} from '@tldraw/editor'
|
||||
import { kickoutOccludedShapes } from '../selectHelpers'
|
||||
|
||||
type ResizingInfo = TLPointerEventInfo & {
|
||||
target: 'selection'
|
||||
|
@ -111,6 +112,8 @@ export class Resizing extends StateNode {
|
|||
}
|
||||
|
||||
private complete() {
|
||||
kickoutOccludedShapes(this.editor, this.snapshot.selectedShapeIds)
|
||||
|
||||
this.handleResizeEnd()
|
||||
|
||||
if (this.info.isCreating && this.info.onCreate) {
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
shortAngleDist,
|
||||
snapAngle,
|
||||
} from '@tldraw/editor'
|
||||
import { kickoutOccludedShapes } from '../selectHelpers'
|
||||
import { CursorTypeMap } from './PointingResizeHandle'
|
||||
|
||||
const ONE_DEGREE = Math.PI / 180
|
||||
|
@ -128,6 +129,10 @@ export class Rotating extends StateNode {
|
|||
snapshot: this.snapshot,
|
||||
stage: 'end',
|
||||
})
|
||||
kickoutOccludedShapes(
|
||||
this.editor,
|
||||
this.snapshot.shapeSnapshots.map((s) => s.shape.id)
|
||||
)
|
||||
if (this.info.onInteractionEnd) {
|
||||
this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)
|
||||
} else {
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
getAvailableNoteAdjacentPositions,
|
||||
} from '../../../shapes/note/noteHelpers'
|
||||
import { DragAndDropManager } from '../DragAndDropManager'
|
||||
import { kickoutOccludedShapes } from '../selectHelpers'
|
||||
|
||||
export class Translating extends StateNode {
|
||||
static override id = 'translating'
|
||||
|
@ -174,6 +175,10 @@ export class Translating extends StateNode {
|
|||
protected complete() {
|
||||
this.updateShapes()
|
||||
this.dragAndDropManager.dropShapes(this.snapshot.movingShapes)
|
||||
kickoutOccludedShapes(
|
||||
this.editor,
|
||||
this.snapshot.movingShapes.map((s) => s.id)
|
||||
)
|
||||
this.handleEnd()
|
||||
|
||||
if (this.editor.getInstanceState().isToolLocked && this.info.onInteractionEnd) {
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
import {
|
||||
Editor,
|
||||
TLShape,
|
||||
TLShapeId,
|
||||
polygonIntersectsPolyline,
|
||||
polygonsIntersect,
|
||||
} from '@tldraw/editor'
|
||||
|
||||
/** @internal */
|
||||
export function kickoutOccludedShapes(editor: Editor, shapeIds: TLShapeId[]) {
|
||||
const shapes = shapeIds.map((id) => editor.getShape(id)).filter((s) => s) as TLShape[]
|
||||
const effectedParents: TLShape[] = shapes
|
||||
.map((shape) => {
|
||||
const parent = editor.getShape(shape.parentId)
|
||||
if (!parent) return shape
|
||||
return parent
|
||||
})
|
||||
.filter((shape) => shape.type === 'frame' || shape.type === 'note')
|
||||
|
||||
const kickedOutChildren: TLShapeId[] = []
|
||||
for (const parent of effectedParents) {
|
||||
const childIds = editor.getSortedChildIdsForParent(parent.id)
|
||||
|
||||
// Get the bounds of the parent shape
|
||||
const parentPageBounds = editor.getShapePageBounds(parent)
|
||||
if (!parentPageBounds) continue
|
||||
|
||||
// For each child, check whether its bounds overlap with the parent's bounds
|
||||
for (const childId of childIds) {
|
||||
if (isShapeOccluded(editor, parent, childId)) {
|
||||
kickedOutChildren.push(childId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// now kick out the children
|
||||
// TODO: make this reparent to the parent's parent?
|
||||
editor.reparentShapes(kickedOutChildren, editor.getCurrentPageId())
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function isShapeOccluded(editor: Editor, occluder: TLShape, shape: TLShapeId) {
|
||||
const occluderPageBounds = editor.getShapePageBounds(occluder)
|
||||
if (!occluderPageBounds) return false
|
||||
|
||||
const shapePageBounds = editor.getShapePageBounds(shape)
|
||||
if (!shapePageBounds) return true
|
||||
|
||||
// If the shape's bounds are completely inside the occluder, it's not occluded
|
||||
if (occluderPageBounds.contains(shapePageBounds)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If the shape's bounds are completely outside the occluder, it's occluded
|
||||
if (!occluderPageBounds.includes(shapePageBounds)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// If we've made it this far, the shape's bounds must intersect the edge of the occluder
|
||||
// In this case, we need to look at the shape's geometry for a more fine-grained check
|
||||
const shapeGeometry = editor.getShapeGeometry(shape)
|
||||
const occluderCornersInShapeSpace = occluderPageBounds.corners.map((v) => {
|
||||
return editor.getPointInShapeSpace(shape, v)
|
||||
})
|
||||
|
||||
if (shapeGeometry.isClosed) {
|
||||
return !polygonsIntersect(occluderCornersInShapeSpace, shapeGeometry.vertices)
|
||||
}
|
||||
|
||||
return !polygonIntersectsPolyline(occluderCornersInShapeSpace, shapeGeometry.vertices)
|
||||
}
|
|
@ -22,6 +22,7 @@ import {
|
|||
} from '@tldraw/editor'
|
||||
import React from 'react'
|
||||
import { STYLES } from '../../../styles'
|
||||
import { kickoutOccludedShapes } from '../../../tools/SelectTool/selectHelpers'
|
||||
import { useUiEvents } from '../../context/events'
|
||||
import { useRelevantStyles } from '../../hooks/useRelevantStyles'
|
||||
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
|
||||
|
@ -101,6 +102,7 @@ export function CommonStylePickerSet({
|
|||
theme: TLDefaultColorTheme
|
||||
}) {
|
||||
const msg = useTranslation()
|
||||
const editor = useEditor()
|
||||
|
||||
const handleValueChange = useStyleChangeCallback()
|
||||
|
||||
|
@ -163,7 +165,13 @@ export function CommonStylePickerSet({
|
|||
style={DefaultSizeStyle}
|
||||
items={STYLES.size}
|
||||
value={size}
|
||||
onValueChange={handleValueChange}
|
||||
onValueChange={(style, value, squashing) => {
|
||||
handleValueChange(style, value, squashing)
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
if (selectedShapeIds.length > 0) {
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
}
|
||||
}}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
useEditor,
|
||||
} from '@tldraw/editor'
|
||||
import * as React from 'react'
|
||||
import { kickoutOccludedShapes } from '../../tools/SelectTool/selectHelpers'
|
||||
import { getEmbedInfo } from '../../utils/embeds/embeds'
|
||||
import { fitFrameToContent, removeFrame } from '../../utils/frames/frames'
|
||||
import { EditLinkDialog } from '../components/EditLinkDialog'
|
||||
|
@ -321,24 +322,28 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('toggle-auto-size', { source })
|
||||
editor.mark('toggling auto size')
|
||||
const shapes = editor
|
||||
.getSelectedShapes()
|
||||
.filter(
|
||||
(shape): shape is TLTextShape =>
|
||||
editor.isShapeOfType<TLTextShape>(shape, 'text') && shape.props.autoSize === false
|
||||
)
|
||||
editor.updateShapes(
|
||||
editor
|
||||
.getSelectedShapes()
|
||||
.filter(
|
||||
(shape): shape is TLTextShape =>
|
||||
editor.isShapeOfType<TLTextShape>(shape, 'text') && shape.props.autoSize === false
|
||||
)
|
||||
.map((shape) => {
|
||||
return {
|
||||
id: shape.id,
|
||||
type: shape.type,
|
||||
props: {
|
||||
...shape.props,
|
||||
w: 8,
|
||||
autoSize: true,
|
||||
},
|
||||
}
|
||||
})
|
||||
shapes.map((shape) => {
|
||||
return {
|
||||
id: shape.id,
|
||||
type: shape.type,
|
||||
props: {
|
||||
...shape.props,
|
||||
w: 8,
|
||||
autoSize: true,
|
||||
},
|
||||
}
|
||||
})
|
||||
)
|
||||
kickoutOccludedShapes(
|
||||
editor,
|
||||
shapes.map((shape) => shape.id)
|
||||
)
|
||||
},
|
||||
},
|
||||
|
@ -602,7 +607,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('align-shapes', { operation: 'left', source })
|
||||
editor.mark('align left')
|
||||
editor.alignShapes(editor.getSelectedShapeIds(), 'left')
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.alignShapes(selectedShapeIds, 'left')
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -619,7 +626,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('align-shapes', { operation: 'center-horizontal', source })
|
||||
editor.mark('align center horizontal')
|
||||
editor.alignShapes(editor.getSelectedShapeIds(), 'center-horizontal')
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.alignShapes(selectedShapeIds, 'center-horizontal')
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -633,7 +642,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('align-shapes', { operation: 'right', source })
|
||||
editor.mark('align right')
|
||||
editor.alignShapes(editor.getSelectedShapeIds(), 'right')
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.alignShapes(selectedShapeIds, 'right')
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -650,7 +661,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('align-shapes', { operation: 'center-vertical', source })
|
||||
editor.mark('align center vertical')
|
||||
editor.alignShapes(editor.getSelectedShapeIds(), 'center-vertical')
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.alignShapes(selectedShapeIds, 'center-vertical')
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -664,7 +677,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('align-shapes', { operation: 'top', source })
|
||||
editor.mark('align top')
|
||||
editor.alignShapes(editor.getSelectedShapeIds(), 'top')
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.alignShapes(selectedShapeIds, 'top')
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -678,7 +693,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('align-shapes', { operation: 'bottom', source })
|
||||
editor.mark('align bottom')
|
||||
editor.alignShapes(editor.getSelectedShapeIds(), 'bottom')
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.alignShapes(selectedShapeIds, 'bottom')
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -695,7 +712,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('distribute-shapes', { operation: 'horizontal', source })
|
||||
editor.mark('distribute horizontal')
|
||||
editor.distributeShapes(editor.getSelectedShapeIds(), 'horizontal')
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.distributeShapes(selectedShapeIds, 'horizontal')
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -712,7 +731,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('distribute-shapes', { operation: 'vertical', source })
|
||||
editor.mark('distribute vertical')
|
||||
editor.distributeShapes(editor.getSelectedShapeIds(), 'vertical')
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.distributeShapes(selectedShapeIds, 'vertical')
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -728,7 +749,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('stretch-shapes', { operation: 'horizontal', source })
|
||||
editor.mark('stretch horizontal')
|
||||
editor.stretchShapes(editor.getSelectedShapeIds(), 'horizontal')
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.stretchShapes(selectedShapeIds, 'horizontal')
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -744,7 +767,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('stretch-shapes', { operation: 'vertical', source })
|
||||
editor.mark('stretch vertical')
|
||||
editor.stretchShapes(editor.getSelectedShapeIds(), 'vertical')
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.stretchShapes(selectedShapeIds, 'vertical')
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -760,7 +785,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('flip-shapes', { operation: 'horizontal', source })
|
||||
editor.mark('flip horizontal')
|
||||
editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal')
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.flipShapes(selectedShapeIds, 'horizontal')
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -773,7 +800,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('flip-shapes', { operation: 'vertical', source })
|
||||
editor.mark('flip vertical')
|
||||
editor.flipShapes(editor.getSelectedShapeIds(), 'vertical')
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.flipShapes(selectedShapeIds, 'vertical')
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -786,7 +815,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('pack-shapes', { source })
|
||||
editor.mark('pack')
|
||||
editor.packShapes(editor.getSelectedShapeIds(), 16)
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.packShapes(selectedShapeIds, 16)
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -802,7 +833,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('stack-shapes', { operation: 'vertical', source })
|
||||
editor.mark('stack-vertical')
|
||||
editor.stackShapes(editor.getSelectedShapeIds(), 'vertical', 16)
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.stackShapes(selectedShapeIds, 'vertical', 16)
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -818,7 +851,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('stack-shapes', { operation: 'horizontal', source })
|
||||
editor.mark('stack-horizontal')
|
||||
editor.stackShapes(editor.getSelectedShapeIds(), 'horizontal', 16)
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.stackShapes(selectedShapeIds, 'horizontal', 16)
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -970,10 +1005,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
editor.mark('rotate-cw')
|
||||
const offset = editor.getSelectionRotation() % (HALF_PI / 2)
|
||||
const dontUseOffset = approximately(offset, 0) || approximately(offset, HALF_PI / 2)
|
||||
editor.rotateShapesBy(
|
||||
editor.getSelectedShapeIds(),
|
||||
HALF_PI / 2 - (dontUseOffset ? 0 : offset)
|
||||
)
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.rotateShapesBy(selectedShapeIds, HALF_PI / 2 - (dontUseOffset ? 0 : offset))
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -988,10 +1022,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
editor.mark('rotate-ccw')
|
||||
const offset = editor.getSelectionRotation() % (HALF_PI / 2)
|
||||
const offsetCloseToZero = approximately(offset, 0)
|
||||
editor.rotateShapesBy(
|
||||
editor.getSelectedShapeIds(),
|
||||
offsetCloseToZero ? -(HALF_PI / 2) : -offset
|
||||
)
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.rotateShapesBy(selectedShapeIds, offsetCloseToZero ? -(HALF_PI / 2) : -offset)
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
DefaultFillStyle,
|
||||
GeoShapeGeoStyle,
|
||||
TLArrowShape,
|
||||
TLFrameShape,
|
||||
TLShapeId,
|
||||
|
@ -275,8 +276,8 @@ describe('frame shapes', () => {
|
|||
expect(parentBefore).toBe(frameId)
|
||||
// resize the frame so the shape is partially out of bounds
|
||||
editor.pointerDown(100, 50, { target: 'selection', handle: 'right' })
|
||||
editor.pointerMove(70, 50)
|
||||
editor.pointerUp(70, 50)
|
||||
editor.pointerMove(80, 50)
|
||||
editor.pointerUp(80, 50)
|
||||
const parentAfter = editor.getShape(rectId)?.parentId
|
||||
expect(parentAfter).toBe(frameId)
|
||||
})
|
||||
|
@ -405,7 +406,7 @@ describe('frame shapes', () => {
|
|||
|
||||
expect(editor.getOnlySelectedShape()!.id).toBe(boxAid)
|
||||
expect(editor.getOnlySelectedShape()!.parentId).toBe(frameId)
|
||||
expect(editor.getHintingShapeIds()).toHaveLength(0)
|
||||
expect(editor.getHintingShapeIds()).toHaveLength(1)
|
||||
// box A should still be beneath box B
|
||||
expect(editor.getShape(boxAid)!.index.localeCompare(editor.getShape(boxBid)!.index)).toBe(-1)
|
||||
})
|
||||
|
@ -1024,6 +1025,65 @@ function dragCreateFrame({
|
|||
return frameId
|
||||
}
|
||||
|
||||
function dragCreateRect({
|
||||
down,
|
||||
move,
|
||||
up,
|
||||
}: {
|
||||
down: [number, number]
|
||||
move: [number, number]
|
||||
up: [number, number]
|
||||
}): TLShapeId {
|
||||
editor.setCurrentTool('geo')
|
||||
editor.pointerDown(...down)
|
||||
editor.pointerMove(...move)
|
||||
editor.pointerUp(...up)
|
||||
const shapes = editor.getSelectedShapes()
|
||||
const rectId = shapes[0].id
|
||||
return rectId
|
||||
}
|
||||
|
||||
function dragCreateTriangle({
|
||||
down,
|
||||
move,
|
||||
up,
|
||||
}: {
|
||||
down: [number, number]
|
||||
move: [number, number]
|
||||
up: [number, number]
|
||||
}): TLShapeId {
|
||||
editor.setCurrentTool('geo')
|
||||
const originalStyle = editor.getStyleForNextShape(GeoShapeGeoStyle)
|
||||
editor.setStyleForNextShapes(GeoShapeGeoStyle, 'triangle')
|
||||
editor.pointerDown(...down)
|
||||
editor.pointerMove(...move)
|
||||
editor.pointerUp(...up)
|
||||
const shapes = editor.getSelectedShapes()
|
||||
editor.selectNone()
|
||||
editor.setStyleForNextShapes(GeoShapeGeoStyle, originalStyle)
|
||||
const rectId = shapes[0].id
|
||||
editor.select(shapes[0].id)
|
||||
return rectId
|
||||
}
|
||||
|
||||
function dragCreateLine({
|
||||
down,
|
||||
move,
|
||||
up,
|
||||
}: {
|
||||
down: [number, number]
|
||||
move: [number, number]
|
||||
up: [number, number]
|
||||
}): TLShapeId {
|
||||
editor.setCurrentTool('line')
|
||||
editor.pointerDown(...down)
|
||||
editor.pointerMove(...move)
|
||||
editor.pointerUp(...up)
|
||||
const shapes = editor.getSelectedShapes()
|
||||
const lineId = shapes[0].id
|
||||
return lineId
|
||||
}
|
||||
|
||||
function createRect({ pos, size }: { pos: [number, number]; size: [number, number] }) {
|
||||
const rectId: TLShapeId = createShapeId()
|
||||
editor.createShapes([
|
||||
|
@ -1037,3 +1097,117 @@ function createRect({ pos, size }: { pos: [number, number]; size: [number, numbe
|
|||
])
|
||||
return rectId
|
||||
}
|
||||
|
||||
describe('Unparenting behavior', () => {
|
||||
it("unparents a shape when it's completely dragged out of a frame, even when the pointer doesn't move across the edge of the frame", () => {
|
||||
dragCreateFrame({ down: [0, 0], move: [100, 100], up: [100, 100] })
|
||||
dragCreateRect({ down: [80, 50], move: [120, 60], up: [120, 60] })
|
||||
const [frame, rect] = editor.getLastCreatedShapes(2)
|
||||
|
||||
expect(editor.getShape(rect.id)!.parentId).toBe(frame.id)
|
||||
editor.pointerDown(110, 50)
|
||||
editor.pointerMove(140, 50)
|
||||
expect(editor.getShape(rect.id)!.parentId).toBe(frame.id)
|
||||
editor.pointerUp(140, 50)
|
||||
expect(editor.getShape(rect.id)!.parentId).toBe(editor.getCurrentPageId())
|
||||
})
|
||||
|
||||
it("doesn't unparent a shape when it's partially dragged out of a frame, when the pointer doesn't move across the edge of the frame", () => {
|
||||
dragCreateFrame({ down: [0, 0], move: [100, 100], up: [100, 100] })
|
||||
dragCreateRect({ down: [80, 50], move: [120, 60], up: [120, 60] })
|
||||
const [frame, rect] = editor.getLastCreatedShapes(2)
|
||||
|
||||
expect(editor.getShape(rect.id)!.parentId).toBe(frame.id)
|
||||
editor.pointerDown(110, 50)
|
||||
editor.pointerMove(120, 50)
|
||||
expect(editor.getShape(rect.id)!.parentId).toBe(frame.id)
|
||||
editor.pointerUp(120, 50)
|
||||
expect(editor.getShape(rect.id)!.parentId).toBe(frame.id)
|
||||
})
|
||||
|
||||
it('unparents a shape when the pointer drags across the edge of a frame, even if its geometry overlaps with the frame', () => {
|
||||
dragCreateFrame({ down: [0, 0], move: [100, 100], up: [100, 100] })
|
||||
dragCreateRect({ down: [80, 50], move: [120, 60], up: [120, 60] })
|
||||
const [frame, rect] = editor.getLastCreatedShapes(2)
|
||||
|
||||
expect(editor.getShape(rect.id)!.parentId).toBe(frame.id)
|
||||
editor.pointerDown(90, 50)
|
||||
editor.pointerMove(110, 50)
|
||||
jest.advanceTimersByTime(200)
|
||||
expect(editor.getShape(rect.id)!.parentId).toBe(editor.getCurrentPageId())
|
||||
editor.pointerUp(110, 50)
|
||||
expect(editor.getShape(rect.id)!.parentId).toBe(editor.getCurrentPageId())
|
||||
})
|
||||
|
||||
it("unparents a shape when it's rotated out of a frame", () => {
|
||||
dragCreateFrame({ down: [0, 0], move: [100, 100], up: [100, 100] })
|
||||
dragCreateRect({ down: [95, 10], move: [200, 20], up: [200, 20] })
|
||||
const [frame, rect] = editor.getLastCreatedShapes(2)
|
||||
|
||||
expect(editor.getShape(rect.id)!.parentId).toBe(frame.id)
|
||||
editor.pointerDown(200, 20, {
|
||||
target: 'selection',
|
||||
handle: 'top_right_rotate',
|
||||
})
|
||||
editor.pointerMove(200, 200)
|
||||
expect(editor.getShape(rect.id)!.parentId).toBe(frame.id)
|
||||
editor.pointerUp(200, 200)
|
||||
expect(editor.getShape(rect.id)!.parentId).toBe(editor.getCurrentPageId())
|
||||
})
|
||||
|
||||
it("unparents shapes if they're resized out of a frame", () => {
|
||||
dragCreateFrame({ down: [0, 0], move: [100, 100], up: [100, 100] })
|
||||
dragCreateRect({ down: [10, 10], move: [20, 20], up: [20, 20] })
|
||||
dragCreateRect({ down: [80, 80], move: [90, 90], up: [90, 90] })
|
||||
const [frame, rect1, rect2] = editor.getLastCreatedShapes(3)
|
||||
|
||||
editor.select(rect1.id, rect2.id)
|
||||
editor.pointerDown(90, 90, { target: 'selection', handle: 'top_right' })
|
||||
expect(editor.getShape(rect2.id)!.parentId).toBe(frame.id)
|
||||
editor.pointerMove(200, 200)
|
||||
expect(editor.getShape(rect2.id)!.parentId).toBe(frame.id)
|
||||
editor.pointerUp(200, 200)
|
||||
expect(editor.getShape(rect2.id)!.parentId).toBe(editor.getCurrentPageId())
|
||||
})
|
||||
|
||||
it("unparents a shape if its geometry doesn't overlap with the frame", () => {
|
||||
dragCreateFrame({ down: [0, 0], move: [100, 100], up: [100, 100] })
|
||||
dragCreateTriangle({ down: [80, 80], move: [120, 120], up: [120, 120] })
|
||||
const [frame, triangle] = editor.getLastCreatedShapes(2)
|
||||
|
||||
expect(editor.getShape(triangle.id)!.parentId).toBe(frame.id)
|
||||
editor.pointerDown(85, 85)
|
||||
editor.pointerMove(95, 95)
|
||||
expect(editor.getShape(triangle.id)!.parentId).toBe(frame.id)
|
||||
editor.pointerUp(95, 95)
|
||||
expect(editor.getShape(triangle.id)!.parentId).toBe(editor.getCurrentPageId())
|
||||
})
|
||||
|
||||
it("only parents on pointer up if the shape's geometry overlaps with the frame", () => {
|
||||
dragCreateFrame({ down: [0, 0], move: [100, 100], up: [100, 100] })
|
||||
dragCreateTriangle({ down: [120, 120], move: [160, 160], up: [160, 160] })
|
||||
const [frame, triangle] = editor.getLastCreatedShapes(2)
|
||||
|
||||
expect(editor.getShape(triangle.id)!.parentId).toBe(editor.getCurrentPageId())
|
||||
editor.pointerDown(125, 125)
|
||||
editor.pointerMove(95, 95)
|
||||
jest.advanceTimersByTime(200)
|
||||
expect(editor.getShape(triangle.id)!.parentId).toBe(frame.id)
|
||||
expect(editor.getHintingShapeIds()).toHaveLength(0)
|
||||
editor.pointerUp(95, 95)
|
||||
expect(editor.getShape(triangle.id)!.parentId).toBe(editor.getCurrentPageId())
|
||||
})
|
||||
|
||||
it('unparents an occluded shape after dragging a handle out of a frame', () => {
|
||||
dragCreateFrame({ down: [0, 0], move: [100, 100], up: [100, 100] })
|
||||
dragCreateLine({ down: [90, 90], move: [120, 120], up: [120, 120] })
|
||||
const [frame, line] = editor.getLastCreatedShapes(2)
|
||||
|
||||
expect(editor.getShape(line.id)!.parentId).toBe(frame.id)
|
||||
editor.pointerDown(90, 90)
|
||||
editor.pointerMove(110, 110)
|
||||
expect(editor.getShape(line.id)!.parentId).toBe(frame.id)
|
||||
editor.pointerUp(110, 110)
|
||||
expect(editor.getShape(line.id)!.parentId).toBe(editor.getCurrentPageId())
|
||||
})
|
||||
})
|
||||
|
|
|
@ -2048,7 +2048,7 @@ describe('Note shape grid helper positions / pits', () => {
|
|||
it('Snaps multiple notes to the pit using the note under the cursor', () => {
|
||||
editor.createShape({ type: 'note' })
|
||||
editor.createShape({ type: 'note', x: 500, y: 500 })
|
||||
editor.createShape({ type: 'note', x: 700, y: 500 })
|
||||
editor.createShape({ type: 'note', x: 700, y: 500, parentId: editor.getCurrentPageId() })
|
||||
const [shapeB, shapeC] = editor.getLastCreatedShapes(2)
|
||||
|
||||
const pit = { x: 320, y: 100 } // right of shapeA
|
||||
|
@ -2093,6 +2093,9 @@ describe('Note shape grid helper positions / pits', () => {
|
|||
editor.createShape({ type: 'note', x: 501, y: 501 })
|
||||
const [shapeB, shapeC] = editor.getLastCreatedShapes(2)
|
||||
|
||||
// For the purposes of this test, let's leave the stickies unparented
|
||||
editor.reparentShapes([shapeC], editor.getCurrentPageId())
|
||||
|
||||
const pit = { x: 320, y: 100 } // right of shapeA
|
||||
|
||||
editor.select(shapeB, shapeC)
|
||||
|
|
Ładowanie…
Reference in New Issue