@tldraw/editor 3.16.0-next.f9f54ec051f3 → 3.16.0-next.fe14f1b4181f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/index.d.ts +110 -9
- package/dist-cjs/index.js +3 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +8 -2
- package/dist-cjs/lib/TldrawEditor.js.map +2 -2
- package/dist-cjs/lib/components/MenuClickCapture.js +0 -5
- package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
- package/dist-cjs/lib/components/Shape.js +7 -10
- package/dist-cjs/lib/components/Shape.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js +4 -23
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js.map +1 -1
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultScribble.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultScribble.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +9 -1
- package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
- package/dist-cjs/lib/config/TLUserPreferences.js +9 -3
- package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +63 -24
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +9 -4
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js +13 -0
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
- package/dist-cjs/lib/exports/getSvgJsx.js +35 -16
- package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js +31 -25
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +4 -1
- package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
- package/dist-cjs/lib/{utils/nearestMultiple.js → hooks/useStateAttribute.js} +15 -14
- package/dist-cjs/lib/hooks/useStateAttribute.js.map +7 -0
- package/dist-cjs/lib/license/Watermark.js +6 -6
- package/dist-cjs/lib/license/Watermark.js.map +1 -1
- package/dist-cjs/lib/options.js +7 -0
- package/dist-cjs/lib/options.js.map +2 -2
- package/dist-cjs/lib/primitives/Box.js +3 -0
- package/dist-cjs/lib/primitives/Box.js.map +2 -2
- package/dist-cjs/lib/utils/EditorAtom.js +45 -0
- package/dist-cjs/lib/utils/EditorAtom.js.map +7 -0
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +110 -9
- package/dist-esm/index.mjs +3 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +8 -2
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/components/MenuClickCapture.mjs +0 -5
- package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
- package/dist-esm/lib/components/Shape.mjs +7 -10
- package/dist-esm/lib/components/Shape.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +4 -23
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs.map +1 -1
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultScribble.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultScribble.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +9 -1
- package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
- package/dist-esm/lib/config/TLUserPreferences.mjs +9 -3
- package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +63 -24
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +9 -4
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +13 -0
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/exports/getSvgJsx.mjs +36 -16
- package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +32 -26
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +4 -1
- package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useStateAttribute.mjs +15 -0
- package/dist-esm/lib/hooks/useStateAttribute.mjs.map +7 -0
- package/dist-esm/lib/license/Watermark.mjs +6 -6
- package/dist-esm/lib/license/Watermark.mjs.map +1 -1
- package/dist-esm/lib/options.mjs +7 -0
- package/dist-esm/lib/options.mjs.map +2 -2
- package/dist-esm/lib/primitives/Box.mjs +4 -1
- package/dist-esm/lib/primitives/Box.mjs.map +2 -2
- package/dist-esm/lib/utils/EditorAtom.mjs +25 -0
- package/dist-esm/lib/utils/EditorAtom.mjs.map +7 -0
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +301 -288
- package/package.json +14 -37
- package/src/index.ts +2 -0
- package/src/lib/TldrawEditor.tsx +13 -6
- package/src/lib/components/MenuClickCapture.tsx +0 -8
- package/src/lib/components/Shape.tsx +6 -12
- package/src/lib/components/default-components/DefaultCanvas.tsx +5 -22
- package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +1 -1
- package/src/lib/components/default-components/DefaultErrorFallback.tsx +1 -1
- package/src/lib/components/default-components/DefaultScribble.tsx +1 -1
- package/src/lib/components/default-components/DefaultShapeIndicator.tsx +5 -1
- package/src/lib/config/TLUserPreferences.ts +8 -1
- package/src/lib/editor/Editor.test.ts +12 -11
- package/src/lib/editor/Editor.ts +88 -47
- package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +15 -14
- package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +16 -15
- package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +49 -48
- package/src/lib/editor/managers/FontManager/FontManager.test.ts +24 -23
- package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +7 -6
- package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +12 -11
- package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +57 -50
- package/src/lib/editor/managers/TextManager/TextManager.test.ts +51 -26
- package/src/lib/editor/managers/TickManager/TickManager.test.ts +14 -13
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +34 -26
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +6 -1
- package/src/lib/editor/shapes/ShapeUtil.ts +14 -0
- package/src/lib/editor/types/misc-types.ts +54 -1
- package/src/lib/exports/getSvgJsx.test.ts +868 -0
- package/src/lib/exports/getSvgJsx.tsx +78 -21
- package/src/lib/hooks/useCanvasEvents.ts +45 -38
- package/src/lib/hooks/usePassThroughWheelEvents.ts +6 -1
- package/src/lib/hooks/useStateAttribute.ts +15 -0
- package/src/lib/license/LicenseManager.test.ts +3 -1
- package/src/lib/license/Watermark.test.tsx +2 -1
- package/src/lib/license/Watermark.tsx +6 -6
- package/src/lib/options.ts +8 -0
- package/src/lib/primitives/Box.test.ts +126 -0
- package/src/lib/primitives/Box.ts +10 -1
- package/src/lib/utils/EditorAtom.ts +37 -0
- package/src/lib/utils/sync/LocalIndexedDb.test.ts +2 -1
- package/src/lib/utils/sync/TLLocalSyncClient.test.ts +15 -15
- package/src/version.ts +3 -3
- package/dist-cjs/lib/utils/nearestMultiple.js.map +0 -7
- package/dist-esm/lib/utils/nearestMultiple.mjs +0 -14
- package/dist-esm/lib/utils/nearestMultiple.mjs.map +0 -7
- package/src/lib/utils/nearestMultiple.ts +0 -13
package/src/lib/editor/Editor.ts
CHANGED
|
@@ -176,6 +176,7 @@ import {
|
|
|
176
176
|
RequiredKeys,
|
|
177
177
|
TLCameraMoveOptions,
|
|
178
178
|
TLCameraOptions,
|
|
179
|
+
TLGetShapeAtPointOptions,
|
|
179
180
|
TLImageExportOptions,
|
|
180
181
|
TLSvgExportOptions,
|
|
181
182
|
TLUpdatePointerOptions,
|
|
@@ -5154,20 +5155,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5154
5155
|
*
|
|
5155
5156
|
* @returns The shape at the given point, or undefined if there is no shape at the point.
|
|
5156
5157
|
*/
|
|
5157
|
-
getShapeAtPoint(
|
|
5158
|
-
point: VecLike,
|
|
5159
|
-
opts = {} as {
|
|
5160
|
-
renderingOnly?: boolean
|
|
5161
|
-
margin?: number
|
|
5162
|
-
hitInside?: boolean
|
|
5163
|
-
hitLocked?: boolean
|
|
5164
|
-
// TODO: we probably need to rename this, we don't quite _always_
|
|
5165
|
-
// respect this esp. in the part below that does "Check labels first"
|
|
5166
|
-
hitLabels?: boolean
|
|
5167
|
-
hitFrameInside?: boolean
|
|
5168
|
-
filter?(shape: TLShape): boolean
|
|
5169
|
-
}
|
|
5170
|
-
): TLShape | undefined {
|
|
5158
|
+
getShapeAtPoint(point: VecLike, opts: TLGetShapeAtPointOptions = {}): TLShape | undefined {
|
|
5171
5159
|
const zoomLevel = this.getZoomLevel()
|
|
5172
5160
|
const viewportPageBounds = this.getViewportPageBounds()
|
|
5173
5161
|
const {
|
|
@@ -5179,6 +5167,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5179
5167
|
hitFrameInside = false,
|
|
5180
5168
|
} = opts
|
|
5181
5169
|
|
|
5170
|
+
const [innerMargin, outerMargin] = Array.isArray(margin) ? margin : [margin, margin]
|
|
5171
|
+
|
|
5182
5172
|
let inHollowSmallestArea = Infinity
|
|
5183
5173
|
let inHollowSmallestAreaHit: TLShape | null = null
|
|
5184
5174
|
|
|
@@ -5198,7 +5188,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5198
5188
|
return false
|
|
5199
5189
|
const pageMask = this.getShapeMask(shape)
|
|
5200
5190
|
if (pageMask && !pointInPolygon(point, pageMask)) return false
|
|
5201
|
-
if (filter
|
|
5191
|
+
if (filter && !filter(shape)) return false
|
|
5202
5192
|
return true
|
|
5203
5193
|
})
|
|
5204
5194
|
|
|
@@ -5224,13 +5214,18 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5224
5214
|
}
|
|
5225
5215
|
}
|
|
5226
5216
|
|
|
5227
|
-
if (this.isShapeOfType(shape, 'frame')) {
|
|
5217
|
+
if (this.isShapeOfType<TLFrameShape>(shape, 'frame')) {
|
|
5228
5218
|
// On the rare case that we've hit a frame (not its label), test again hitInside to be forced true;
|
|
5229
5219
|
// this prevents clicks from passing through the body of a frame to shapes behind it.
|
|
5230
5220
|
|
|
5231
5221
|
// If the hit is within the frame's outer margin, then select the frame
|
|
5232
|
-
const distance = geometry.distanceToPoint(pointInShapeSpace,
|
|
5233
|
-
if (
|
|
5222
|
+
const distance = geometry.distanceToPoint(pointInShapeSpace, hitFrameInside)
|
|
5223
|
+
if (
|
|
5224
|
+
hitFrameInside
|
|
5225
|
+
? (distance > 0 && distance <= outerMargin) ||
|
|
5226
|
+
(distance <= 0 && distance > -innerMargin)
|
|
5227
|
+
: distance > 0 && distance <= outerMargin
|
|
5228
|
+
) {
|
|
5234
5229
|
return inMarginClosestToEdgeHit || shape
|
|
5235
5230
|
}
|
|
5236
5231
|
|
|
@@ -5269,11 +5264,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5269
5264
|
// If the margin is zero and the geometry has a very small width or height,
|
|
5270
5265
|
// then check the actual distance. This is to prevent a bug where straight
|
|
5271
5266
|
// lines would never pass the broad phase (point-in-bounds) check.
|
|
5272
|
-
if (
|
|
5267
|
+
if (outerMargin === 0 && (geometry.bounds.w < 1 || geometry.bounds.h < 1)) {
|
|
5273
5268
|
distance = geometry.distanceToPoint(pointInShapeSpace, hitInside)
|
|
5274
5269
|
} else {
|
|
5275
5270
|
// Broad phase
|
|
5276
|
-
if (geometry.bounds.containsPoint(pointInShapeSpace,
|
|
5271
|
+
if (geometry.bounds.containsPoint(pointInShapeSpace, outerMargin)) {
|
|
5277
5272
|
// Narrow phase (actual distance)
|
|
5278
5273
|
distance = geometry.distanceToPoint(pointInShapeSpace, hitInside)
|
|
5279
5274
|
} else {
|
|
@@ -5288,7 +5283,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5288
5283
|
// the shape or negative if inside of the shape. If the distance
|
|
5289
5284
|
// is greater than the margin, then it's a miss. Otherwise...
|
|
5290
5285
|
|
|
5291
|
-
|
|
5286
|
+
// Are we close to the shape's edge?
|
|
5287
|
+
if (distance <= outerMargin || (hitInside && distance <= 0 && distance > -innerMargin)) {
|
|
5292
5288
|
if (geometry.isFilled || (isGroup && geometry.children[0].isFilled)) {
|
|
5293
5289
|
// If the shape is filled, then it's a hit. Remember, we're
|
|
5294
5290
|
// starting from the TOP-MOST shape in z-index order, so any
|
|
@@ -5298,11 +5294,21 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5298
5294
|
// If the shape is bigger than the viewport, then skip it.
|
|
5299
5295
|
if (this.getShapePageBounds(shape)!.contains(viewportPageBounds)) continue
|
|
5300
5296
|
|
|
5301
|
-
//
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5297
|
+
// If we're close to the edge of the shape, and if it's the closest edge among
|
|
5298
|
+
// all the edges that we've gotten close to so far, then we will want to hit the
|
|
5299
|
+
// shape unless we hit something else or closer in later iterations.
|
|
5300
|
+
if (
|
|
5301
|
+
hitInside
|
|
5302
|
+
? // On hitInside, the distance will be negative for hits inside
|
|
5303
|
+
// If the distance is positive, check against the outer margin
|
|
5304
|
+
(distance > 0 && distance <= outerMargin) ||
|
|
5305
|
+
// If the distance is negative, check against the inner margin
|
|
5306
|
+
(distance <= 0 && distance > -innerMargin)
|
|
5307
|
+
: // If hitInside is false, then sadly _we do not know_ whether the
|
|
5308
|
+
// point is inside or outside of the shape, so we check against
|
|
5309
|
+
// the max of the two margins
|
|
5310
|
+
Math.abs(distance) <= Math.max(innerMargin, outerMargin)
|
|
5311
|
+
) {
|
|
5306
5312
|
if (Math.abs(distance) < inMarginClosestToEdgeDistance) {
|
|
5307
5313
|
inMarginClosestToEdgeDistance = Math.abs(distance)
|
|
5308
5314
|
inMarginClosestToEdgeHit = shape
|
|
@@ -5324,6 +5330,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5324
5330
|
} else {
|
|
5325
5331
|
// For open shapes (e.g. lines or draw shapes) always use the margin.
|
|
5326
5332
|
// If the distance is less than the margin, return the shape as the hit.
|
|
5333
|
+
// Use the editor's configurable hit test margin.
|
|
5327
5334
|
if (distance < this.options.hitTestMargin / zoomLevel) {
|
|
5328
5335
|
return shape
|
|
5329
5336
|
}
|
|
@@ -6326,7 +6333,17 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6326
6333
|
|
|
6327
6334
|
this.createShapes(shapesToCreate)
|
|
6328
6335
|
this.createBindings(bindingsToCreate)
|
|
6329
|
-
|
|
6336
|
+
|
|
6337
|
+
this.setSelectedShapes(
|
|
6338
|
+
compact(
|
|
6339
|
+
ids.map((oldId) => {
|
|
6340
|
+
const newId = shapeIds.get(oldId)
|
|
6341
|
+
if (!newId) return null
|
|
6342
|
+
if (!this.getShape(newId)) return null
|
|
6343
|
+
return newId
|
|
6344
|
+
})
|
|
6345
|
+
)
|
|
6346
|
+
)
|
|
6330
6347
|
|
|
6331
6348
|
if (offset !== undefined) {
|
|
6332
6349
|
// If we've offset the duplicated shapes, check to see whether their new bounds is entirely
|
|
@@ -7380,7 +7397,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7380
7397
|
if (
|
|
7381
7398
|
!this.getShapeUtil(shape).canBeLaidOut?.(shape, {
|
|
7382
7399
|
type: 'stretch',
|
|
7383
|
-
shapes: shapesToStretchFirstPass,
|
|
7384
7400
|
})
|
|
7385
7401
|
) {
|
|
7386
7402
|
continue
|
|
@@ -7851,25 +7867,32 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7851
7867
|
) {
|
|
7852
7868
|
let parentId: TLParentId = this.getFocusedGroupId()
|
|
7853
7869
|
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7870
|
+
const isPositioned = partial.x !== undefined && partial.y !== undefined
|
|
7871
|
+
|
|
7872
|
+
// If the shape has been explicitly positioned, we'll try to find a parent at
|
|
7873
|
+
// that position. If not, we'll assume the user isn't deliberately placing the
|
|
7874
|
+
// shape and the positioning will be handled later by another system.
|
|
7875
|
+
if (isPositioned) {
|
|
7876
|
+
for (let i = currentPageShapesSorted.length - 1; i >= 0; i--) {
|
|
7877
|
+
const parent = currentPageShapesSorted[i]
|
|
7878
|
+
const util = this.getShapeUtil(parent)
|
|
7879
|
+
if (
|
|
7880
|
+
util.canReceiveNewChildrenOfType(parent, partial.type) &&
|
|
7881
|
+
!this.isShapeHidden(parent) &&
|
|
7882
|
+
this.isPointInShape(
|
|
7883
|
+
parent,
|
|
7884
|
+
// If no parent is provided, then we can treat the
|
|
7885
|
+
// shape's provided x/y as being in the page's space.
|
|
7886
|
+
{ x: partial.x ?? 0, y: partial.y ?? 0 },
|
|
7887
|
+
{
|
|
7888
|
+
margin: 0,
|
|
7889
|
+
hitInside: true,
|
|
7890
|
+
}
|
|
7891
|
+
)
|
|
7892
|
+
) {
|
|
7893
|
+
parentId = parent.id
|
|
7894
|
+
break
|
|
7895
|
+
}
|
|
7873
7896
|
}
|
|
7874
7897
|
}
|
|
7875
7898
|
|
|
@@ -9496,6 +9519,24 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
9496
9519
|
}
|
|
9497
9520
|
}
|
|
9498
9521
|
|
|
9522
|
+
/**
|
|
9523
|
+
* Get an exported image of the given shapes as a data URL.
|
|
9524
|
+
*
|
|
9525
|
+
* @param shapes - The shapes (or shape ids) to export.
|
|
9526
|
+
* @param opts - Options for the export.
|
|
9527
|
+
*
|
|
9528
|
+
* @returns A data URL of the image.
|
|
9529
|
+
* @public
|
|
9530
|
+
*/
|
|
9531
|
+
async toImageDataUrl(shapes: TLShapeId[] | TLShape[], opts: TLImageExportOptions = {}) {
|
|
9532
|
+
const { blob, width, height } = await this.toImage(shapes, opts)
|
|
9533
|
+
return {
|
|
9534
|
+
url: await FileHelpers.blobToDataUrl(blob),
|
|
9535
|
+
width,
|
|
9536
|
+
height,
|
|
9537
|
+
}
|
|
9538
|
+
}
|
|
9539
|
+
|
|
9499
9540
|
/* --------------------- Events --------------------- */
|
|
9500
9541
|
|
|
9501
9542
|
/**
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import { Mocked, vi } from 'vitest'
|
|
1
2
|
import { Editor } from '../../Editor'
|
|
2
3
|
import { TLClickEventInfo, TLPointerEventInfo } from '../../types/event-types'
|
|
3
4
|
import { ClickManager } from './ClickManager'
|
|
4
5
|
|
|
5
6
|
// Mock the Editor class
|
|
6
|
-
|
|
7
|
+
vi.mock('../../Editor')
|
|
7
8
|
|
|
8
9
|
describe('ClickManager', () => {
|
|
9
|
-
let editor:
|
|
10
|
+
let editor: Mocked<Editor>
|
|
10
11
|
let clickManager: ClickManager
|
|
11
12
|
let mockTimers: any
|
|
12
13
|
|
|
@@ -29,14 +30,14 @@ describe('ClickManager', () => {
|
|
|
29
30
|
})
|
|
30
31
|
|
|
31
32
|
beforeEach(() => {
|
|
32
|
-
|
|
33
|
+
vi.useFakeTimers()
|
|
33
34
|
mockTimers = {
|
|
34
|
-
setTimeout:
|
|
35
|
+
setTimeout: vi.fn((fn, delay) => setTimeout(fn, delay)),
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
editor = {
|
|
38
39
|
timers: mockTimers,
|
|
39
|
-
dispatch:
|
|
40
|
+
dispatch: vi.fn(),
|
|
40
41
|
options: {
|
|
41
42
|
doubleClickDurationMs: 300,
|
|
42
43
|
multiClickDurationMs: 300,
|
|
@@ -46,7 +47,7 @@ describe('ClickManager', () => {
|
|
|
46
47
|
inputs: {
|
|
47
48
|
currentScreenPoint: { x: 0, y: 0 },
|
|
48
49
|
},
|
|
49
|
-
getInstanceState:
|
|
50
|
+
getInstanceState: vi.fn(() => ({
|
|
50
51
|
isCoarsePointer: false,
|
|
51
52
|
})),
|
|
52
53
|
} as any
|
|
@@ -55,8 +56,8 @@ describe('ClickManager', () => {
|
|
|
55
56
|
})
|
|
56
57
|
|
|
57
58
|
afterEach(() => {
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
vi.useRealTimers()
|
|
60
|
+
vi.clearAllMocks()
|
|
60
61
|
})
|
|
61
62
|
|
|
62
63
|
describe('constructor and initial state', () => {
|
|
@@ -100,7 +101,7 @@ describe('ClickManager', () => {
|
|
|
100
101
|
clickManager.handlePointerEvent(pointerEvent)
|
|
101
102
|
expect(clickManager.clickState).toBe('pendingDouble')
|
|
102
103
|
|
|
103
|
-
|
|
104
|
+
vi.advanceTimersByTime(350)
|
|
104
105
|
|
|
105
106
|
expect(clickManager.clickState).toBe('idle')
|
|
106
107
|
})
|
|
@@ -141,7 +142,7 @@ describe('ClickManager', () => {
|
|
|
141
142
|
clickManager.handlePointerEvent(firstDown)
|
|
142
143
|
clickManager.handlePointerEvent(secondDown)
|
|
143
144
|
|
|
144
|
-
|
|
145
|
+
vi.advanceTimersByTime(350)
|
|
145
146
|
|
|
146
147
|
expect(editor.dispatch).toHaveBeenCalledWith(
|
|
147
148
|
expect.objectContaining({
|
|
@@ -235,7 +236,7 @@ describe('ClickManager', () => {
|
|
|
235
236
|
clickManager.handlePointerEvent(pointerDown) // second
|
|
236
237
|
clickManager.handlePointerEvent(pointerDown) // third
|
|
237
238
|
|
|
238
|
-
|
|
239
|
+
vi.advanceTimersByTime(350)
|
|
239
240
|
|
|
240
241
|
expect(editor.dispatch).toHaveBeenCalledWith(
|
|
241
242
|
expect.objectContaining({
|
|
@@ -255,7 +256,7 @@ describe('ClickManager', () => {
|
|
|
255
256
|
clickManager.handlePointerEvent(pointerDown) // third
|
|
256
257
|
clickManager.handlePointerEvent(pointerDown) // fourth
|
|
257
258
|
|
|
258
|
-
|
|
259
|
+
vi.advanceTimersByTime(350)
|
|
259
260
|
|
|
260
261
|
expect(editor.dispatch).toHaveBeenCalledWith(
|
|
261
262
|
expect.objectContaining({
|
|
@@ -277,7 +278,7 @@ describe('ClickManager', () => {
|
|
|
277
278
|
editor.options.doubleClickDurationMs
|
|
278
279
|
)
|
|
279
280
|
|
|
280
|
-
|
|
281
|
+
vi.clearAllMocks()
|
|
281
282
|
|
|
282
283
|
// Second click - should use multiClickDurationMs
|
|
283
284
|
clickManager.handlePointerEvent(pointerDown)
|
|
@@ -392,7 +393,7 @@ describe('ClickManager', () => {
|
|
|
392
393
|
clickManager.cancelDoubleClickTimeout()
|
|
393
394
|
|
|
394
395
|
// Advance time - should not dispatch settle event
|
|
395
|
-
|
|
396
|
+
vi.advanceTimersByTime(350)
|
|
396
397
|
|
|
397
398
|
expect(editor.dispatch).not.toHaveBeenCalled()
|
|
398
399
|
expect(clickManager.clickState).toBe('idle')
|
|
@@ -1,19 +1,20 @@
|
|
|
1
|
+
import { Mock, Mocked, vi } from 'vitest'
|
|
1
2
|
import { Box } from '../../../primitives/Box'
|
|
2
3
|
import { Vec } from '../../../primitives/Vec'
|
|
3
4
|
import { Editor } from '../../Editor'
|
|
4
5
|
import { EdgeScrollManager } from './EdgeScrollManager'
|
|
5
6
|
|
|
6
7
|
// Mock the Editor class
|
|
7
|
-
|
|
8
|
+
vi.mock('../../Editor')
|
|
8
9
|
|
|
9
10
|
describe('EdgeScrollManager', () => {
|
|
10
|
-
let editor:
|
|
11
|
+
let editor: Mocked<
|
|
11
12
|
Editor & {
|
|
12
|
-
user: { getEdgeScrollSpeed:
|
|
13
|
-
getCamera:
|
|
14
|
-
getCameraOptions:
|
|
15
|
-
getZoomLevel:
|
|
16
|
-
getViewportScreenBounds:
|
|
13
|
+
user: { getEdgeScrollSpeed: Mock }
|
|
14
|
+
getCamera: Mock
|
|
15
|
+
getCameraOptions: Mock
|
|
16
|
+
getZoomLevel: Mock
|
|
17
|
+
getViewportScreenBounds: Mock
|
|
17
18
|
}
|
|
18
19
|
>
|
|
19
20
|
let edgeScrollManager: EdgeScrollManager
|
|
@@ -33,33 +34,33 @@ describe('EdgeScrollManager', () => {
|
|
|
33
34
|
isPanning: false,
|
|
34
35
|
},
|
|
35
36
|
user: {
|
|
36
|
-
getEdgeScrollSpeed:
|
|
37
|
+
getEdgeScrollSpeed: vi.fn(() => 1),
|
|
37
38
|
},
|
|
38
|
-
getViewportScreenBounds:
|
|
39
|
-
getInstanceState:
|
|
39
|
+
getViewportScreenBounds: vi.fn(() => new Box(0, 0, 1000, 600)),
|
|
40
|
+
getInstanceState: vi.fn(
|
|
40
41
|
() =>
|
|
41
42
|
({
|
|
42
43
|
isCoarsePointer: false,
|
|
43
44
|
insets: [false, false, false, false], // [top, right, bottom, left]
|
|
44
45
|
}) as any
|
|
45
46
|
),
|
|
46
|
-
getCameraOptions:
|
|
47
|
+
getCameraOptions: vi.fn(() => ({
|
|
47
48
|
isLocked: false,
|
|
48
49
|
panSpeed: 1,
|
|
49
50
|
zoomSpeed: 1,
|
|
50
51
|
zoomSteps: [1],
|
|
51
52
|
wheelBehavior: 'pan' as const,
|
|
52
53
|
})),
|
|
53
|
-
getZoomLevel:
|
|
54
|
-
getCamera:
|
|
55
|
-
setCamera:
|
|
54
|
+
getZoomLevel: vi.fn(() => 1),
|
|
55
|
+
getCamera: vi.fn(() => new Vec(0, 0, 1)),
|
|
56
|
+
setCamera: vi.fn(),
|
|
56
57
|
} as any
|
|
57
58
|
|
|
58
59
|
edgeScrollManager = new EdgeScrollManager(editor as any)
|
|
59
60
|
})
|
|
60
61
|
|
|
61
62
|
afterEach(() => {
|
|
62
|
-
|
|
63
|
+
vi.clearAllMocks()
|
|
63
64
|
})
|
|
64
65
|
|
|
65
66
|
describe('constructor and initialization', () => {
|
|
@@ -1,58 +1,59 @@
|
|
|
1
|
+
import { Mock, Mocked, vi } from 'vitest'
|
|
1
2
|
import { Editor } from '../../Editor'
|
|
2
3
|
import { FocusManager } from './FocusManager'
|
|
3
4
|
|
|
4
5
|
// Mock the Editor class
|
|
5
|
-
|
|
6
|
+
vi.mock('../../Editor')
|
|
6
7
|
|
|
7
8
|
describe('FocusManager', () => {
|
|
8
|
-
let editor:
|
|
9
|
+
let editor: Mocked<
|
|
9
10
|
Editor & {
|
|
10
11
|
sideEffects: {
|
|
11
|
-
registerAfterChangeHandler:
|
|
12
|
+
registerAfterChangeHandler: Mock
|
|
12
13
|
}
|
|
13
|
-
getInstanceState:
|
|
14
|
-
updateInstanceState:
|
|
15
|
-
getContainer:
|
|
16
|
-
isIn:
|
|
17
|
-
getSelectedShapeIds:
|
|
18
|
-
complete:
|
|
14
|
+
getInstanceState: Mock
|
|
15
|
+
updateInstanceState: Mock
|
|
16
|
+
getContainer: Mock
|
|
17
|
+
isIn: Mock
|
|
18
|
+
getSelectedShapeIds: Mock
|
|
19
|
+
complete: Mock
|
|
19
20
|
}
|
|
20
21
|
>
|
|
21
22
|
let focusManager: FocusManager
|
|
22
23
|
let mockContainer: HTMLElement
|
|
23
|
-
let mockDispose:
|
|
24
|
+
let mockDispose: Mock
|
|
24
25
|
let originalAddEventListener: typeof document.body.addEventListener
|
|
25
26
|
let originalRemoveEventListener: typeof document.body.removeEventListener
|
|
26
27
|
|
|
27
28
|
beforeEach(() => {
|
|
28
29
|
// Create mock container element
|
|
29
30
|
mockContainer = document.createElement('div')
|
|
30
|
-
mockContainer.focus =
|
|
31
|
-
mockContainer.blur =
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
mockContainer.focus = vi.fn()
|
|
32
|
+
mockContainer.blur = vi.fn()
|
|
33
|
+
vi.spyOn(mockContainer.classList, 'add')
|
|
34
|
+
vi.spyOn(mockContainer.classList, 'remove')
|
|
34
35
|
|
|
35
36
|
// Create mock dispose function
|
|
36
|
-
mockDispose =
|
|
37
|
+
mockDispose = vi.fn()
|
|
37
38
|
|
|
38
39
|
// Mock editor
|
|
39
40
|
editor = {
|
|
40
41
|
sideEffects: {
|
|
41
|
-
registerAfterChangeHandler:
|
|
42
|
+
registerAfterChangeHandler: vi.fn(() => mockDispose),
|
|
42
43
|
},
|
|
43
|
-
getInstanceState:
|
|
44
|
-
updateInstanceState:
|
|
45
|
-
getContainer:
|
|
46
|
-
isIn:
|
|
47
|
-
getSelectedShapeIds:
|
|
48
|
-
complete:
|
|
44
|
+
getInstanceState: vi.fn(() => ({ isFocused: false })),
|
|
45
|
+
updateInstanceState: vi.fn(),
|
|
46
|
+
getContainer: vi.fn(() => mockContainer),
|
|
47
|
+
isIn: vi.fn(() => false),
|
|
48
|
+
getSelectedShapeIds: vi.fn(() => []),
|
|
49
|
+
complete: vi.fn(),
|
|
49
50
|
} as any
|
|
50
51
|
|
|
51
52
|
// Mock document.body event listeners
|
|
52
53
|
originalAddEventListener = document.body.addEventListener
|
|
53
54
|
originalRemoveEventListener = document.body.removeEventListener
|
|
54
|
-
document.body.addEventListener =
|
|
55
|
-
document.body.removeEventListener =
|
|
55
|
+
document.body.addEventListener = vi.fn()
|
|
56
|
+
document.body.removeEventListener = vi.fn()
|
|
56
57
|
})
|
|
57
58
|
|
|
58
59
|
afterEach(() => {
|
|
@@ -65,7 +66,7 @@ describe('FocusManager', () => {
|
|
|
65
66
|
focusManager.dispose()
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
vi.clearAllMocks()
|
|
69
70
|
})
|
|
70
71
|
|
|
71
72
|
describe('constructor', () => {
|
|
@@ -131,7 +132,7 @@ describe('FocusManager', () => {
|
|
|
131
132
|
const handler = handlerCall[1]
|
|
132
133
|
|
|
133
134
|
// Clear previous calls
|
|
134
|
-
|
|
135
|
+
vi.clearAllMocks()
|
|
135
136
|
|
|
136
137
|
// Simulate focus state change
|
|
137
138
|
const prev = { isFocused: false }
|
|
@@ -149,7 +150,7 @@ describe('FocusManager', () => {
|
|
|
149
150
|
const handlerCall = editor.sideEffects.registerAfterChangeHandler.mock.calls[0]
|
|
150
151
|
const handler = handlerCall[1]
|
|
151
152
|
|
|
152
|
-
|
|
153
|
+
vi.clearAllMocks()
|
|
153
154
|
|
|
154
155
|
// Simulate no focus state change
|
|
155
156
|
const prev = { isFocused: true }
|
|
@@ -170,7 +171,7 @@ describe('FocusManager', () => {
|
|
|
170
171
|
// Get the handler before clearing mocks
|
|
171
172
|
const handlerCall = editor.sideEffects.registerAfterChangeHandler.mock.calls[0]
|
|
172
173
|
handler = handlerCall[1]
|
|
173
|
-
|
|
174
|
+
vi.clearAllMocks()
|
|
174
175
|
})
|
|
175
176
|
|
|
176
177
|
it('should add focused class when editor is focused', () => {
|
|
@@ -205,11 +206,11 @@ describe('FocusManager', () => {
|
|
|
205
206
|
focusManager = new FocusManager(editor)
|
|
206
207
|
|
|
207
208
|
// Get the keydown handler that was registered
|
|
208
|
-
const addEventListenerCalls = (document.body.addEventListener as
|
|
209
|
-
const keydownCall = addEventListenerCalls.find((call) => call[0] === 'keydown')
|
|
210
|
-
keydownHandler = keydownCall[1]
|
|
209
|
+
const addEventListenerCalls = (document.body.addEventListener as Mock).mock.calls
|
|
210
|
+
const keydownCall = addEventListenerCalls.find((call: any) => call[0] === 'keydown')
|
|
211
|
+
keydownHandler = keydownCall![1]
|
|
211
212
|
|
|
212
|
-
|
|
213
|
+
vi.clearAllMocks()
|
|
213
214
|
})
|
|
214
215
|
|
|
215
216
|
it('should remove no-focus-ring class on Tab key', () => {
|
|
@@ -283,11 +284,11 @@ describe('FocusManager', () => {
|
|
|
283
284
|
focusManager = new FocusManager(editor)
|
|
284
285
|
|
|
285
286
|
// Get the mousedown handler that was registered
|
|
286
|
-
const addEventListenerCalls = (document.body.addEventListener as
|
|
287
|
-
const mousedownCall = addEventListenerCalls.find((call) => call[0] === 'mousedown')
|
|
288
|
-
mousedownHandler = mousedownCall[1]
|
|
287
|
+
const addEventListenerCalls = (document.body.addEventListener as Mock).mock.calls
|
|
288
|
+
const mousedownCall = addEventListenerCalls.find((call: any) => call[0] === 'mousedown')
|
|
289
|
+
mousedownHandler = mousedownCall![1]
|
|
289
290
|
|
|
290
|
-
|
|
291
|
+
vi.clearAllMocks()
|
|
291
292
|
})
|
|
292
293
|
|
|
293
294
|
it('should add no-focus-ring class on mouse down', () => {
|
|
@@ -326,7 +327,7 @@ describe('FocusManager', () => {
|
|
|
326
327
|
it('should complete before bluring', () => {
|
|
327
328
|
const callOrder: string[] = []
|
|
328
329
|
editor.complete.mockImplementation(() => callOrder.push('complete'))
|
|
329
|
-
mockContainer.blur =
|
|
330
|
+
mockContainer.blur = vi.fn(() => callOrder.push('blur'))
|
|
330
331
|
|
|
331
332
|
focusManager.blur()
|
|
332
333
|
|
|
@@ -337,7 +338,7 @@ describe('FocusManager', () => {
|
|
|
337
338
|
describe('dispose', () => {
|
|
338
339
|
beforeEach(() => {
|
|
339
340
|
focusManager = new FocusManager(editor)
|
|
340
|
-
|
|
341
|
+
vi.clearAllMocks()
|
|
341
342
|
})
|
|
342
343
|
|
|
343
344
|
it('should remove keyboard event listener', () => {
|
|
@@ -376,7 +377,7 @@ describe('FocusManager', () => {
|
|
|
376
377
|
const handlerCall = editor.sideEffects.registerAfterChangeHandler.mock.calls[0]
|
|
377
378
|
const handler = handlerCall[1]
|
|
378
379
|
|
|
379
|
-
|
|
380
|
+
vi.clearAllMocks()
|
|
380
381
|
|
|
381
382
|
// Rapid focus changes
|
|
382
383
|
editor.getInstanceState.mockReturnValue({ isFocused: true })
|
|
@@ -394,9 +395,9 @@ describe('FocusManager', () => {
|
|
|
394
395
|
|
|
395
396
|
it('should handle keyboard navigation while editing', () => {
|
|
396
397
|
focusManager = new FocusManager(editor)
|
|
397
|
-
const addEventListenerCalls = (document.body.addEventListener as
|
|
398
|
-
const keydownCall = addEventListenerCalls.find((call) => call[0] === 'keydown')
|
|
399
|
-
const keydownHandler = keydownCall[1]
|
|
398
|
+
const addEventListenerCalls = (document.body.addEventListener as Mock).mock.calls
|
|
399
|
+
const keydownCall = addEventListenerCalls.find((call: any) => call[0] === 'keydown')
|
|
400
|
+
const keydownHandler = keydownCall![1]
|
|
400
401
|
|
|
401
402
|
editor.isIn.mockReturnValue(true) // Editing mode
|
|
402
403
|
|
|
@@ -409,15 +410,15 @@ describe('FocusManager', () => {
|
|
|
409
410
|
|
|
410
411
|
it('should handle mouse and keyboard interaction sequence', () => {
|
|
411
412
|
focusManager = new FocusManager(editor)
|
|
412
|
-
const addEventListenerCalls = (document.body.addEventListener as
|
|
413
|
+
const addEventListenerCalls = (document.body.addEventListener as Mock).mock.calls
|
|
413
414
|
|
|
414
|
-
const mousedownCall = addEventListenerCalls.find((call) => call[0] === 'mousedown')
|
|
415
|
-
const keydownCall = addEventListenerCalls.find((call) => call[0] === 'keydown')
|
|
415
|
+
const mousedownCall = addEventListenerCalls.find((call: any) => call[0] === 'mousedown')
|
|
416
|
+
const keydownCall = addEventListenerCalls.find((call: any) => call[0] === 'keydown')
|
|
416
417
|
|
|
417
|
-
const mousedownHandler = mousedownCall[1]
|
|
418
|
-
const keydownHandler = keydownCall[1]
|
|
418
|
+
const mousedownHandler = mousedownCall![1]
|
|
419
|
+
const keydownHandler = keydownCall![1]
|
|
419
420
|
|
|
420
|
-
|
|
421
|
+
vi.clearAllMocks()
|
|
421
422
|
|
|
422
423
|
// Mouse down adds no-focus-ring
|
|
423
424
|
mousedownHandler()
|