stick(iness): improve note+highlighter stickiness behavior

pull/3243/head
Mime Čuvalo 2024-03-22 18:02:40 +00:00
rodzic 5e7848aa01
commit bf543332cc
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: BA84499022AC984D
11 zmienionych plików z 329 dodań i 46 usunięć

Wyświetl plik

@ -1599,6 +1599,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
canBind: <K>(_shape: Shape, _otherShape?: K | undefined) => boolean;
canCrop: TLShapeUtilFlag<Shape>;
canDropShapes(shape: Shape, shapes: TLShape[]): boolean;
canDropShapesOnlyWithinMaskedBounds: TLShapeUtilFlag<Shape>;
canEdit: TLShapeUtilFlag<Shape>;
canEditInReadOnly: TLShapeUtilFlag<Shape>;
canReceiveNewChildrenOfType(shape: Shape, type: TLShape['type']): boolean;
@ -1623,6 +1624,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
hideSelectionBoundsFg: TLShapeUtilFlag<Shape>;
abstract indicator(shape: Shape): any;
isAspectRatioLocked: TLShapeUtilFlag<Shape>;
isSticky: TLShapeUtilFlag<Shape>;
// (undocumented)
static migrations?: Migrations;
onBeforeCreate?: TLOnBeforeCreateHandler<Shape>;
@ -1634,10 +1636,10 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
onDoubleClick?: TLOnDoubleClickHandler<Shape>;
onDoubleClickEdge?: TLOnDoubleClickHandler<Shape>;
onDoubleClickHandle?: TLOnDoubleClickHandleHandler<Shape>;
onDragShapesOut?: TLOnDragHandler<Shape>;
onDragShapesOver?: TLOnDragHandler<Shape, {
onDragShapesOut(shape: Shape, shapes: TLShape[]): void;
onDragShapesOver(shape: Shape, shapes: TLShape[]): {
shouldHint: boolean;
}>;
} | undefined;
onDropShapesOver?: TLOnDragHandler<Shape>;
onEditEnd?: TLOnEditEndHandler<Shape>;
onHandleDrag?: TLOnHandleDragHandler<Shape>;

Wyświetl plik

@ -30401,6 +30401,41 @@
"isAbstract": false,
"name": "canDropShapes"
},
{
"kind": "Property",
"canonicalReference": "@tldraw/editor!ShapeUtil#canDropShapesOnlyWithinMaskedBounds:member",
"docComment": "/**\n * Whether the dropping a shape onto another behaves like a frame (true) where one has to drop the shape within the bounds of the frame, or like a stickies (false) where the shape is considered dropped when the bounding boxes collide.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "canDropShapesOnlyWithinMaskedBounds: "
},
{
"kind": "Reference",
"text": "TLShapeUtilFlag",
"canonicalReference": "@tldraw/editor!TLShapeUtilFlag:type"
},
{
"kind": "Content",
"text": "<Shape>"
},
{
"kind": "Content",
"text": ";"
}
],
"isReadonly": false,
"isOptional": false,
"releaseTag": "Public",
"name": "canDropShapesOnlyWithinMaskedBounds",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 3
},
"isStatic": false,
"isProtected": false,
"isAbstract": false
},
{
"kind": "Property",
"canonicalReference": "@tldraw/editor!ShapeUtil#canEdit:member",
@ -31249,6 +31284,41 @@
"isProtected": false,
"isAbstract": false
},
{
"kind": "Property",
"canonicalReference": "@tldraw/editor!ShapeUtil#isSticky:member",
"docComment": "/**\n * Whether the shape should adhere to other shapes: stickers, washi tape, sticky notes, highlighters, etc.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "isSticky: "
},
{
"kind": "Reference",
"text": "TLShapeUtilFlag",
"canonicalReference": "@tldraw/editor!TLShapeUtilFlag:type"
},
{
"kind": "Content",
"text": "<Shape>"
},
{
"kind": "Content",
"text": ";"
}
],
"isReadonly": false,
"isOptional": false,
"releaseTag": "Public",
"name": "isSticky",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 3
},
"isStatic": false,
"isProtected": false,
"isAbstract": false
},
{
"kind": "Property",
"canonicalReference": "@tldraw/editor!ShapeUtil.migrations:member",
@ -31526,74 +31596,142 @@
"isAbstract": false
},
{
"kind": "Property",
"canonicalReference": "@tldraw/editor!ShapeUtil#onDragShapesOut:member",
"kind": "Method",
"canonicalReference": "@tldraw/editor!ShapeUtil#onDragShapesOut:member(1)",
"docComment": "/**\n * A callback called when some other shapes are dragged out of this one.\n *\n * @param shape - The shape.\n *\n * @param shapes - The shapes that are being dragged out.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "onDragShapesOut?: "
},
{
"kind": "Reference",
"text": "TLOnDragHandler",
"canonicalReference": "@tldraw/editor!TLOnDragHandler:type"
"text": "onDragShapesOut(shape: "
},
{
"kind": "Content",
"text": "<Shape>"
"text": "Shape"
},
{
"kind": "Content",
"text": ", shapes: "
},
{
"kind": "Reference",
"text": "TLShape",
"canonicalReference": "@tldraw/tlschema!TLShape:type"
},
{
"kind": "Content",
"text": "[]"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Content",
"text": "void"
},
{
"kind": "Content",
"text": ";"
}
],
"isReadonly": false,
"isOptional": true,
"releaseTag": "Public",
"name": "onDragShapesOut",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 3
},
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 6,
"endIndex": 7
},
"releaseTag": "Public",
"isProtected": false,
"isAbstract": false
"overloadIndex": 1,
"parameters": [
{
"parameterName": "shape",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
},
{
"parameterName": "shapes",
"parameterTypeTokenRange": {
"startIndex": 3,
"endIndex": 5
},
"isOptional": false
}
],
"isOptional": false,
"isAbstract": false,
"name": "onDragShapesOut"
},
{
"kind": "Property",
"canonicalReference": "@tldraw/editor!ShapeUtil#onDragShapesOver:member",
"kind": "Method",
"canonicalReference": "@tldraw/editor!ShapeUtil#onDragShapesOver:member(1)",
"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",
"excerptTokens": [
{
"kind": "Content",
"text": "onDragShapesOver?: "
},
{
"kind": "Reference",
"text": "TLOnDragHandler",
"canonicalReference": "@tldraw/editor!TLOnDragHandler:type"
"text": "onDragShapesOver(shape: "
},
{
"kind": "Content",
"text": "<Shape, {\n shouldHint: boolean;\n }>"
"text": "Shape"
},
{
"kind": "Content",
"text": ", shapes: "
},
{
"kind": "Reference",
"text": "TLShape",
"canonicalReference": "@tldraw/tlschema!TLShape:type"
},
{
"kind": "Content",
"text": "[]"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Content",
"text": "{\n shouldHint: boolean;\n } | undefined"
},
{
"kind": "Content",
"text": ";"
}
],
"isReadonly": false,
"isOptional": true,
"releaseTag": "Public",
"name": "onDragShapesOver",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 3
},
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 6,
"endIndex": 7
},
"releaseTag": "Public",
"isProtected": false,
"isAbstract": false
"overloadIndex": 1,
"parameters": [
{
"parameterName": "shape",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
},
{
"parameterName": "shapes",
"parameterTypeTokenRange": {
"startIndex": 3,
"endIndex": 5
},
"isOptional": false
}
],
"isOptional": false,
"isAbstract": false,
"name": "onDragShapesOver"
},
{
"kind": "Property",

Wyświetl plik

@ -5023,16 +5023,27 @@ export class Editor extends EventEmitter<TLEventMap> {
}
// Only allow dropping into the masked page bounds of the shape, e.g. when a frame is
// partially clipped by its own parent frame
// partially clipped by its own parent frame.
const maskedPageBounds = this.getShapeMaskedPageBounds(shape.id)
if (
this.getShapeUtil(shape).canDropShapesOnlyWithinMaskedBounds(shape) &&
maskedPageBounds &&
maskedPageBounds.containsPoint(point) &&
this.getShapeGeometry(shape).hitTestPoint(this.getPointInShapeSpace(shape, point), 0, true)
) {
return shape
}
// Non-frames have different logic when dropping. They look at collisions between the bounding boxes.
const shapeBounds = this.getShapePageBounds(shape)
if (
shapeBounds &&
droppingShapes.some((droppingShape) =>
this.getShapePageBounds(droppingShape)?.collides(shapeBounds)
)
) {
return shape
}
}
}

Wyświetl plik

@ -208,7 +208,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
* @public
*/
canReceiveNewChildrenOfType(shape: Shape, type: TLShape['type']) {
return false
return this.editor.getShapeUtil(type).isSticky(shape)
}
/**
@ -219,9 +219,28 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
* @public
*/
canDropShapes(shape: Shape, shapes: TLShape[]) {
return false
return shapes.every((droppedShape) =>
this.editor.getShapeUtil(droppedShape).isSticky(droppedShape)
)
}
/**
* Whether the dropping a shape onto another behaves like a frame (true) where one
* has to drop the shape within the bounds of the frame, or like a stickies (false) where
* the shape is considered dropped when the bounding boxes collide.
*
* @public
*/
canDropShapesOnlyWithinMaskedBounds: TLShapeUtilFlag<Shape> = () => true
/**
* Whether the shape should adhere to other shapes:
* stickers, washi tape, sticky notes, highlighters, etc.
*
* @public
*/
isSticky: TLShapeUtilFlag<Shape> = () => false
/**
* Get the shape as an SVG object.
*
@ -332,7 +351,12 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
* @returns An object specifying whether the shape should hint that it can receive the dragged shapes.
* @public
*/
onDragShapesOver?: TLOnDragHandler<Shape, { shouldHint: boolean }>
onDragShapesOver(shape: Shape, shapes: TLShape[]) {
if (this.canDropShapes(shape, shapes)) {
this.editor.reparentShapes(shapes, shape.id)
return { shouldHint: true }
}
}
/**
* A callback called when some other shapes are dragged out of this one.
@ -341,7 +365,11 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
* @param shapes - The shapes that are being dragged out.
* @public
*/
onDragShapesOut?: TLOnDragHandler<Shape>
onDragShapesOut(shape: Shape, shapes: TLShape[]) {
if (this.canDropShapes(shape, shapes)) {
this.editor.reparentShapes(shapes, this.editor.getCurrentPage().id)
}
}
/**
* A callback called when some other shapes are dropped over this one.

Wyświetl plik

@ -632,6 +632,8 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
// (undocumented)
canDropShapes: (shape: TLFrameShape, _shapes: TLShape[]) => boolean;
// (undocumented)
canDropShapesOnlyWithinMaskedBounds: () => boolean;
// (undocumented)
canEdit: () => boolean;
// (undocumented)
canReceiveNewChildrenOfType: (shape: TLShape, _type: TLShape['type']) => boolean;
@ -900,6 +902,8 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
// (undocumented)
indicator(shape: TLHighlightShape): JSX_2.Element;
// (undocumented)
isSticky: () => boolean;
// (undocumented)
static migrations: Migrations;
// (undocumented)
onResize: TLOnResizeHandler<TLHighlightShape>;
@ -1100,6 +1104,8 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
// (undocumented)
indicator(shape: TLNoteShape): JSX_2.Element;
// (undocumented)
isSticky: () => boolean;
// (undocumented)
static migrations: Migrations;
// (undocumented)
onBeforeCreate: (next: TLNoteShape) => {

Wyświetl plik

@ -7156,6 +7156,36 @@
"isProtected": false,
"isAbstract": false
},
{
"kind": "Property",
"canonicalReference": "tldraw!FrameShapeUtil#canDropShapesOnlyWithinMaskedBounds:member",
"docComment": "",
"excerptTokens": [
{
"kind": "Content",
"text": "canDropShapesOnlyWithinMaskedBounds: "
},
{
"kind": "Content",
"text": "() => boolean"
},
{
"kind": "Content",
"text": ";"
}
],
"isReadonly": false,
"isOptional": false,
"releaseTag": "Public",
"name": "canDropShapesOnlyWithinMaskedBounds",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isStatic": false,
"isProtected": false,
"isAbstract": false
},
{
"kind": "Property",
"canonicalReference": "tldraw!FrameShapeUtil#canEdit:member",
@ -10294,6 +10324,36 @@
"isAbstract": false,
"name": "indicator"
},
{
"kind": "Property",
"canonicalReference": "tldraw!HighlightShapeUtil#isSticky:member",
"docComment": "",
"excerptTokens": [
{
"kind": "Content",
"text": "isSticky: "
},
{
"kind": "Content",
"text": "() => boolean"
},
{
"kind": "Content",
"text": ";"
}
],
"isReadonly": false,
"isOptional": false,
"releaseTag": "Public",
"name": "isSticky",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isStatic": false,
"isProtected": false,
"isAbstract": false
},
{
"kind": "Property",
"canonicalReference": "tldraw!HighlightShapeUtil.migrations:member",
@ -13137,6 +13197,36 @@
"isAbstract": false,
"name": "indicator"
},
{
"kind": "Property",
"canonicalReference": "tldraw!NoteShapeUtil#isSticky:member",
"docComment": "",
"excerptTokens": [
{
"kind": "Content",
"text": "isSticky: "
},
{
"kind": "Content",
"text": "() => boolean"
},
{
"kind": "Content",
"text": ";"
}
],
"isReadonly": false,
"isOptional": false,
"releaseTag": "Public",
"name": "isSticky",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isStatic": false,
"isProtected": false,
"isAbstract": false
},
{
"kind": "Property",
"canonicalReference": "tldraw!NoteShapeUtil.migrations:member",

Wyświetl plik

@ -42,6 +42,8 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
override canEdit = () => true
override canDropShapesOnlyWithinMaskedBounds = () => true
override getDefaultProps(): TLFrameShape['props'] {
return { w: 160 * 2, h: 90 * 2, name: '' }
}

Wyświetl plik

@ -38,6 +38,7 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
override hideResizeHandles = (shape: TLHighlightShape) => getIsDot(shape)
override hideRotateHandle = (shape: TLHighlightShape) => getIsDot(shape)
override hideSelectionBoundsFg = (shape: TLHighlightShape) => getIsDot(shape)
override isSticky = () => true
override getDefaultProps(): TLHighlightShape['props'] {
return {

Wyświetl plik

@ -29,6 +29,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
override canEdit = () => true
override hideResizeHandles = () => true
override hideSelectionBoundsFg = () => true
override isSticky = () => true
getDefaultProps(): TLNoteShape['props'] {
return {

Wyświetl plik

@ -16,8 +16,13 @@ export class DragAndDropManager {
updateDroppingNode(movingShapes: TLShape[], cb: () => void) {
if (this.first) {
const ancestorsFromSingleMovingShape =
movingShapes.length === 1 ? this.editor.getShapeAncestors(movingShapes[0]) : null
const singleAncestorFromSingleMovingShape =
ancestorsFromSingleMovingShape?.length === 1 ? ancestorsFromSingleMovingShape[0] : null
this.prevDroppingShapeId =
this.editor.getDroppingOverShape(this.editor.inputs.originPagePoint, movingShapes)?.id ??
singleAncestorFromSingleMovingShape?.id ??
null
this.first = false
}

Wyświetl plik

@ -35,7 +35,6 @@ 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 the current point is NOT within the selection bounds
(selectedShapeIds.length > 1 && selectionBounds?.containsPoint(currentPagePoint))
) {