No impure getters pt3 (#2203)

Follow up to #2189 and #2202 

### Change Type

- [x] `patch` — Bug fix

[^1]: publishes a `patch` release, for devDependencies use `internal`
[^2]: will not publish a new version
This commit is contained in:
David Sheldrick 2023-11-13 14:31:27 +00:00 committed by GitHub
parent 2ca2f81f2a
commit 7ffda2335c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 309 additions and 209 deletions

View file

@ -24,7 +24,7 @@ test.describe.skip('clipboard tests', () => {
await page.mouse.up() await page.mouse.up()
expect(await page.evaluate(() => editor.currentPageShapes.length)).toBe(1) expect(await page.evaluate(() => editor.currentPageShapes.length)).toBe(1)
expect(await page.evaluate(() => editor.selectedShapes.length)).toBe(1) expect(await page.evaluate(() => editor.getSelectedShapes().length)).toBe(1)
await page.keyboard.down('Control') await page.keyboard.down('Control')
await page.keyboard.press('KeyC') await page.keyboard.press('KeyC')
@ -33,7 +33,7 @@ test.describe.skip('clipboard tests', () => {
await page.keyboard.up('Control') await page.keyboard.up('Control')
expect(await page.evaluate(() => editor.currentPageShapes.length)).toBe(2) expect(await page.evaluate(() => editor.currentPageShapes.length)).toBe(2)
expect(await page.evaluate(() => editor.selectedShapes.length)).toBe(1) expect(await page.evaluate(() => editor.getSelectedShapes().length)).toBe(1)
}) })
test('copy and paste from main menu', async ({ page }) => { test('copy and paste from main menu', async ({ page }) => {
@ -43,7 +43,7 @@ test.describe.skip('clipboard tests', () => {
await page.mouse.up() await page.mouse.up()
expect(await page.evaluate(() => editor.currentPageShapes.length)).toBe(1) expect(await page.evaluate(() => editor.currentPageShapes.length)).toBe(1)
expect(await page.evaluate(() => editor.selectedShapes.length)).toBe(1) expect(await page.evaluate(() => editor.getSelectedShapes().length)).toBe(1)
await page.getByTestId('main.menu').click() await page.getByTestId('main.menu').click()
await page.getByTestId('menu-item.edit').click() await page.getByTestId('menu-item.edit').click()
@ -54,7 +54,7 @@ test.describe.skip('clipboard tests', () => {
await page.getByTestId('menu-item.paste').click() await page.getByTestId('menu-item.paste').click()
expect(await page.evaluate(() => editor.currentPageShapes.length)).toBe(2) expect(await page.evaluate(() => editor.currentPageShapes.length)).toBe(2)
expect(await page.evaluate(() => editor.selectedShapes.length)).toBe(1) expect(await page.evaluate(() => editor.getSelectedShapes().length)).toBe(1)
}) })
test('copy and paste from context menu', async ({ page }) => { test('copy and paste from context menu', async ({ page }) => {
@ -64,7 +64,7 @@ test.describe.skip('clipboard tests', () => {
await page.mouse.up() await page.mouse.up()
expect(await page.evaluate(() => editor.currentPageShapes.length)).toBe(1) expect(await page.evaluate(() => editor.currentPageShapes.length)).toBe(1)
expect(await page.evaluate(() => editor.selectedShapes.length)).toBe(1) expect(await page.evaluate(() => editor.getSelectedShapes().length)).toBe(1)
await page.mouse.click(100, 100, { button: 'right' }) await page.mouse.click(100, 100, { button: 'right' })
await page.getByTestId('menu-item.copy').click() await page.getByTestId('menu-item.copy').click()
@ -74,6 +74,6 @@ test.describe.skip('clipboard tests', () => {
await page.getByTestId('menu-item.paste').click() await page.getByTestId('menu-item.paste').click()
expect(await page.evaluate(() => editor.currentPageShapes.length)).toBe(2) expect(await page.evaluate(() => editor.currentPageShapes.length)).toBe(2)
expect(await page.evaluate(() => editor.selectedShapes.length)).toBe(1) expect(await page.evaluate(() => editor.getSelectedShapes().length)).toBe(1)
}) })
}) })

View file

@ -65,7 +65,7 @@ test.describe('smoke tests', () => {
expect(await getAllShapeTypes(page)).toEqual(['geo']) expect(await getAllShapeTypes(page)).toEqual(['geo'])
const getSelectedShapeColor = async () => const getSelectedShapeColor = async () =>
await page.evaluate(() => (editor.selectedShapes[0] as TLGeoShape).props.color) await page.evaluate(() => (editor.getSelectedShapes()[0] as TLGeoShape).props.color)
// change style // change style
expect(await getSelectedShapeColor()).toBe('black') expect(await getSelectedShapeColor()).toBe('black')

View file

@ -24,7 +24,7 @@ type ShapeWithMyMeta = TLShape & { meta: { label: string } }
export const ShapeLabelUiWithHelper = track(function ShapeLabelUiWithHelper() { export const ShapeLabelUiWithHelper = track(function ShapeLabelUiWithHelper() {
const editor = useEditor() const editor = useEditor()
const onlySelectedShape = editor.onlySelectedShape as ShapeWithMyMeta | null const onlySelectedShape = editor.getOnlySelectedShape() as ShapeWithMyMeta | null
if (!onlySelectedShape) { if (!onlySelectedShape) {
return null return null

View file

@ -103,7 +103,7 @@ class PointingState extends StateNode {
override onPointerMove: TLEventHandlers['onPointerUp'] = () => { override onPointerMove: TLEventHandlers['onPointerUp'] = () => {
if (this.editor.inputs.isDragging) { if (this.editor.inputs.isDragging) {
this.parent.transition('dragging', { shapes: [...this.editor.selectedShapes] }) this.parent.transition('dragging', { shapes: [...this.editor.getSelectedShapes()] })
} }
} }
} }

View file

@ -689,6 +689,7 @@ export class Editor extends EventEmitter<TLEventMap> {
getInitialMetaForShape(_shape: TLShape): JsonObject; getInitialMetaForShape(_shape: TLShape): JsonObject;
getInstanceState(): TLInstance; getInstanceState(): TLInstance;
getIsMenuOpen(): boolean; getIsMenuOpen(): boolean;
getOnlySelectedShape(): null | TLShape;
getOpenMenus(): string[]; getOpenMenus(): string[];
getOutermostSelectableShape(shape: TLShape | TLShapeId, filter?: (shape: TLShape) => boolean): TLShape; getOutermostSelectableShape(shape: TLShape | TLShapeId, filter?: (shape: TLShape) => boolean): TLShape;
getPage(page: TLPage | TLPageId): TLPage | undefined; getPage(page: TLPage | TLPageId): TLPage | undefined;
@ -698,6 +699,7 @@ export class Editor extends EventEmitter<TLEventMap> {
getPointInShapeSpace(shape: TLShape | TLShapeId, point: VecLike): Vec2d; getPointInShapeSpace(shape: TLShape | TLShapeId, point: VecLike): Vec2d;
getSelectedShapeAtPoint(point: VecLike): TLShape | undefined; getSelectedShapeAtPoint(point: VecLike): TLShape | undefined;
getSelectedShapeIds(): TLShapeId[]; getSelectedShapeIds(): TLShapeId[];
getSelectedShapes(): TLShape[];
getShape<T extends TLShape = TLShape>(shape: TLParentId | TLShape): T | undefined; getShape<T extends TLShape = TLShape>(shape: TLParentId | TLShape): T | undefined;
getShapeAncestors(shape: TLShape | TLShapeId, acc?: TLShape[]): TLShape[]; getShapeAncestors(shape: TLShape | TLShapeId, acc?: TLShape[]): TLShape[];
getShapeAndDescendantIds(ids: TLShapeId[]): Set<TLShapeId>; getShapeAndDescendantIds(ids: TLShapeId[]): Set<TLShapeId>;
@ -791,6 +793,7 @@ export class Editor extends EventEmitter<TLEventMap> {
mark(markId?: string, onUndo?: boolean, onRedo?: boolean): this; mark(markId?: string, onUndo?: boolean, onRedo?: boolean): this;
moveShapesToPage(shapes: TLShape[] | TLShapeId[], pageId: TLPageId): this; moveShapesToPage(shapes: TLShape[] | TLShapeId[], pageId: TLPageId): this;
nudgeShapes(shapes: TLShape[] | TLShapeId[], offset: VecLike, historyOptions?: TLCommandHistoryOptions): this; nudgeShapes(shapes: TLShape[] | TLShapeId[], offset: VecLike, historyOptions?: TLCommandHistoryOptions): this;
// @deprecated (undocumented)
get onlySelectedShape(): null | TLShape; get onlySelectedShape(): null | TLShape;
// @deprecated (undocumented) // @deprecated (undocumented)
get openMenus(): string[]; get openMenus(): string[];
@ -849,6 +852,7 @@ export class Editor extends EventEmitter<TLEventMap> {
selectAll(): this; selectAll(): this;
// @deprecated (undocumented) // @deprecated (undocumented)
get selectedShapeIds(): TLShapeId[]; get selectedShapeIds(): TLShapeId[];
// @deprecated (undocumented)
get selectedShapes(): TLShape[]; get selectedShapes(): TLShape[];
get selectionPageBounds(): Box2d | null; get selectionPageBounds(): Box2d | null;
get selectionRotatedPageBounds(): Box2d | undefined; get selectionRotatedPageBounds(): Box2d | undefined;

View file

@ -9068,7 +9068,7 @@
{ {
"kind": "Method", "kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#duplicateShapes:member(1)", "canonicalReference": "@tldraw/editor!Editor#duplicateShapes:member(1)",
"docComment": "/**\n * Duplicate shapes.\n *\n * @param shapes - The shapes (or shape ids) to duplicate.\n *\n * @param offset - The offset (in pixels) to apply to the duplicated shapes.\n *\n * @example\n * ```ts\n * editor.duplicateShapes(['box1', 'box2'], { x: 8, y: 8 })\n * editor.duplicateShapes(editor.selectedShapes, { x: 8, y: 8 })\n * ```\n *\n * @public\n */\n", "docComment": "/**\n * Duplicate shapes.\n *\n * @param shapes - The shapes (or shape ids) to duplicate.\n *\n * @param offset - The offset (in pixels) to apply to the duplicated shapes.\n *\n * @example\n * ```ts\n * editor.duplicateShapes(['box1', 'box2'], { x: 8, y: 8 })\n * editor.duplicateShapes(editor.getSelectedShapes(), { x: 8, y: 8 })\n * ```\n *\n * @public\n */\n",
"excerptTokens": [ "excerptTokens": [
{ {
"kind": "Content", "kind": "Content",
@ -10517,6 +10517,42 @@
"isAbstract": false, "isAbstract": false,
"name": "getIsMenuOpen" "name": "getIsMenuOpen"
}, },
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getOnlySelectedShape:member(1)",
"docComment": "/**\n * The app's only selected shape.\n *\n * @returns Null if there is no shape or more than one selected shape, otherwise the selected shape.\n *\n * @public @readonly\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "getOnlySelectedShape(): "
},
{
"kind": "Content",
"text": "null | "
},
{
"kind": "Reference",
"text": "TLShape",
"canonicalReference": "@tldraw/tlschema!TLShape:type"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 1,
"endIndex": 3
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [],
"isOptional": false,
"isAbstract": false,
"name": "getOnlySelectedShape"
},
{ {
"kind": "Method", "kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getOpenMenus:member(1)", "canonicalReference": "@tldraw/editor!Editor#getOpenMenus:member(1)",
@ -11045,6 +11081,42 @@
"isAbstract": false, "isAbstract": false,
"name": "getSelectedShapeIds" "name": "getSelectedShapeIds"
}, },
{
"kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getSelectedShapes:member(1)",
"docComment": "/**\n * An array containing all of the currently selected shapes.\n *\n * @public @readonly\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "getSelectedShapes(): "
},
{
"kind": "Reference",
"text": "TLShape",
"canonicalReference": "@tldraw/tlschema!TLShape:type"
},
{
"kind": "Content",
"text": "[]"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 1,
"endIndex": 3
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [],
"isOptional": false,
"isAbstract": false,
"name": "getSelectedShapes"
},
{ {
"kind": "Method", "kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#getShape:member(1)", "canonicalReference": "@tldraw/editor!Editor#getShape:member(1)",
@ -14029,7 +14101,7 @@
{ {
"kind": "Method", "kind": "Method",
"canonicalReference": "@tldraw/editor!Editor#nudgeShapes:member(1)", "canonicalReference": "@tldraw/editor!Editor#nudgeShapes:member(1)",
"docComment": "/**\n * Move shapes by a delta.\n *\n * @param shapes - The shapes (or shape ids) to move.\n *\n * @param direction - The direction in which to move the shapes.\n *\n * @param historyOptions - The history options for the change.\n *\n * @example\n * ```ts\n * editor.nudgeShapes(['box1', 'box2'], { x: 8, y: 8 })\n * editor.nudgeShapes(editor.selectedShapes, { x: 8, y: 8 }, { squashing: true })\n * ```\n *\n */\n", "docComment": "/**\n * Move shapes by a delta.\n *\n * @param shapes - The shapes (or shape ids) to move.\n *\n * @param direction - The direction in which to move the shapes.\n *\n * @param historyOptions - The history options for the change.\n *\n * @example\n * ```ts\n * editor.nudgeShapes(['box1', 'box2'], { x: 8, y: 8 })\n * editor.nudgeShapes(editor.getSelectedShapes(), { x: 8, y: 8 }, { squashing: true })\n * ```\n *\n */\n",
"excerptTokens": [ "excerptTokens": [
{ {
"kind": "Content", "kind": "Content",
@ -14125,7 +14197,7 @@
{ {
"kind": "Property", "kind": "Property",
"canonicalReference": "@tldraw/editor!Editor#onlySelectedShape:member", "canonicalReference": "@tldraw/editor!Editor#onlySelectedShape:member",
"docComment": "/**\n * The app's only selected shape.\n *\n * @returns Null if there is no shape or more than one selected shape, otherwise the selected shape.\n *\n * @example\n * ```ts\n * editor.onlySelectedShape\n * ```\n *\n * @public @readonly\n */\n", "docComment": "/**\n * @deprecated\n *\n * Use `getOnlySelectedShape` instead.\n */\n",
"excerptTokens": [ "excerptTokens": [
{ {
"kind": "Content", "kind": "Content",
@ -15775,7 +15847,7 @@
{ {
"kind": "Property", "kind": "Property",
"canonicalReference": "@tldraw/editor!Editor#selectedShapes:member", "canonicalReference": "@tldraw/editor!Editor#selectedShapes:member",
"docComment": "/**\n * An array containing all of the currently selected shapes.\n *\n * @example\n * ```ts\n * editor.selectedShapes\n * ```\n *\n * @public @readonly\n */\n", "docComment": "/**\n * @deprecated\n *\n * Use `getSelectedShapes` instead.\n */\n",
"excerptTokens": [ "excerptTokens": [
{ {
"kind": "Content", "kind": "Content",

View file

@ -214,7 +214,9 @@ function HandlesWrapper() {
const isCoarse = useValue('coarse pointer', () => editor.getInstanceState().isCoarsePointer, [ const isCoarse = useValue('coarse pointer', () => editor.getInstanceState().isCoarsePointer, [
editor, editor,
]) ])
const onlySelectedShape = useValue('onlySelectedShape', () => editor.onlySelectedShape, [editor]) const onlySelectedShape = useValue('onlySelectedShape', () => editor.getOnlySelectedShape(), [
editor,
])
const isChangingStyle = useValue( const isChangingStyle = useValue(
'isChangingStyle', 'isChangingStyle',
() => editor.getInstanceState().isChangingStyle, () => editor.getInstanceState().isChangingStyle,
@ -225,13 +227,24 @@ function HandlesWrapper() {
]) ])
const handles = useValue( const handles = useValue(
'handles', 'handles',
() => (editor.onlySelectedShape ? editor.getShapeHandles(editor.onlySelectedShape) : undefined), () => {
const onlySelectedShape = editor.getOnlySelectedShape()
if (onlySelectedShape) {
return editor.getShapeHandles(onlySelectedShape)
}
return undefined
},
[editor] [editor]
) )
const transform = useValue( const transform = useValue(
'transform', 'transform',
() => () => {
editor.onlySelectedShape ? editor.getShapePageTransform(editor.onlySelectedShape) : undefined, const onlySelectedShape = editor.getOnlySelectedShape()
if (onlySelectedShape) {
return editor.getShapePageTransform(onlySelectedShape)
}
return undefined
},
[editor] [editor]
) )

View file

@ -20,7 +20,7 @@ export const DefaultSelectionForeground: TLSelectionForegroundComponent = ({
const editor = useEditor() const editor = useEditor()
const rSvg = useRef<SVGSVGElement>(null) const rSvg = useRef<SVGSVGElement>(null)
const onlyShape = useValue('only selected shape', () => editor.onlySelectedShape, [editor]) const onlyShape = useValue('only selected shape', () => editor.getOnlySelectedShape(), [editor])
// if all shapes have an expandBy for the selection outline, we can expand by the l // if all shapes have an expandBy for the selection outline, we can expand by the l
const expandOutlineBy = onlyShape const expandOutlineBy = onlyShape

View file

@ -1002,7 +1002,7 @@ export class Editor extends EventEmitter<TLEventMap> {
}, },
extras: { extras: {
activeStateNode: this.root.path.get(), activeStateNode: this.root.path.get(),
selectedShapes: this.selectedShapes, selectedShapes: this.getSelectedShapes(),
editingShape: this.editingShapeId ? this.getShape(this.editingShapeId) : undefined, editingShape: this.editingShapeId ? this.getShape(this.editingShapeId) : undefined,
inputs: this.inputs, inputs: this.inputs,
}, },
@ -1481,19 +1481,21 @@ export class Editor extends EventEmitter<TLEventMap> {
/** /**
* An array containing all of the currently selected shapes. * An array containing all of the currently selected shapes.
* *
* @example
* ```ts
* editor.selectedShapes
* ```
*
* @public * @public
* @readonly * @readonly
*/ */
@computed get selectedShapes(): TLShape[] { @computed getSelectedShapes(): TLShape[] {
const { selectedShapeIds } = this.getCurrentPageState() const { selectedShapeIds } = this.getCurrentPageState()
return compact(selectedShapeIds.map((id) => this.store.get(id))) return compact(selectedShapeIds.map((id) => this.store.get(id)))
} }
/**
* @deprecated Use `getSelectedShapes` instead.
*/
get selectedShapes() {
return this.getSelectedShapes()
}
/** /**
* Select one or more shapes. * Select one or more shapes.
* *
@ -1652,22 +1654,24 @@ export class Editor extends EventEmitter<TLEventMap> {
/** /**
* The app's only selected shape. * The app's only selected shape.
* *
* @example
* ```ts
* editor.onlySelectedShape
* ```
*
* @returns Null if there is no shape or more than one selected shape, otherwise the selected * @returns Null if there is no shape or more than one selected shape, otherwise the selected
* shape. * shape.
* *
* @public * @public
* @readonly * @readonly
*/ */
@computed get onlySelectedShape(): TLShape | null { @computed getOnlySelectedShape(): TLShape | null {
const { selectedShapes } = this const selectedShapes = this.getSelectedShapes()
return selectedShapes.length === 1 ? selectedShapes[0] : null return selectedShapes.length === 1 ? selectedShapes[0] : null
} }
/**
* @deprecated Use `getOnlySelectedShape` instead.
*/
get onlySelectedShape() {
return this.getOnlySelectedShape()
}
/** /**
* The current page bounds of all the selected shapes. If the * The current page bounds of all the selected shapes. If the
* selection is rotated, then these bounds are the axis-aligned * selection is rotated, then these bounds are the axis-aligned
@ -5138,7 +5142,7 @@ export class Editor extends EventEmitter<TLEventMap> {
* @example * @example
* ```ts * ```ts
* editor.nudgeShapes(['box1', 'box2'], { x: 8, y: 8 }) * editor.nudgeShapes(['box1', 'box2'], { x: 8, y: 8 })
* editor.nudgeShapes(editor.selectedShapes, { x: 8, y: 8 }, { squashing: true }) * editor.nudgeShapes(editor.getSelectedShapes(), { x: 8, y: 8 }, { squashing: true })
* ``` * ```
* *
* @param shapes - The shapes (or shape ids) to move. * @param shapes - The shapes (or shape ids) to move.
@ -5201,7 +5205,7 @@ export class Editor extends EventEmitter<TLEventMap> {
* @example * @example
* ```ts * ```ts
* editor.duplicateShapes(['box1', 'box2'], { x: 8, y: 8 }) * editor.duplicateShapes(['box1', 'box2'], { x: 8, y: 8 })
* editor.duplicateShapes(editor.selectedShapes, { x: 8, y: 8 }) * editor.duplicateShapes(editor.getSelectedShapes(), { x: 8, y: 8 })
* ``` * ```
* *
* @param shapes - The shapes (or shape ids) to duplicate. * @param shapes - The shapes (or shape ids) to duplicate.
@ -7288,7 +7292,7 @@ export class Editor extends EventEmitter<TLEventMap> {
private _selectionSharedStyles = computed<ReadonlySharedStyleMap>( private _selectionSharedStyles = computed<ReadonlySharedStyleMap>(
'_selectionSharedStyles', '_selectionSharedStyles',
() => { () => {
const { selectedShapes } = this const selectedShapes = this.getSelectedShapes()
const sharedStyles = new SharedStyleMap() const sharedStyles = new SharedStyleMap()
for (const selectedShape of selectedShapes) { for (const selectedShape of selectedShapes) {
@ -7418,7 +7422,7 @@ export class Editor extends EventEmitter<TLEventMap> {
* @param historyOptions - The history options for the change. * @param historyOptions - The history options for the change.
*/ */
setOpacityForSelectedShapes(opacity: number, historyOptions?: TLCommandHistoryOptions): this { setOpacityForSelectedShapes(opacity: number, historyOptions?: TLCommandHistoryOptions): this {
const { selectedShapes } = this const selectedShapes = this.getSelectedShapes()
if (selectedShapes.length > 0) { if (selectedShapes.length > 0) {
const shapesToUpdate: TLShape[] = [] const shapesToUpdate: TLShape[] = []
@ -7505,7 +7509,7 @@ export class Editor extends EventEmitter<TLEventMap> {
value: T, value: T,
historyOptions?: TLCommandHistoryOptions historyOptions?: TLCommandHistoryOptions
): this { ): this {
const { selectedShapes } = this const selectedShapes = this.getSelectedShapes()
if (selectedShapes.length > 0) { if (selectedShapes.length > 0) {
const updates: { const updates: {
@ -7847,7 +7851,7 @@ export class Editor extends EventEmitter<TLEventMap> {
let lowestAncestors: TLShape[] = [] let lowestAncestors: TLShape[] = []
// Among the selected shapes, find the shape with the fewest ancestors and use its first ancestor. // Among the selected shapes, find the shape with the fewest ancestors and use its first ancestor.
for (const shape of this.selectedShapes) { for (const shape of this.getSelectedShapes()) {
if (lowestDepth === 0) break if (lowestDepth === 0) break
const isFrame = this.isShapeOfType<TLFrameShape>(shape, 'frame') const isFrame = this.isShapeOfType<TLFrameShape>(shape, 'frame')

View file

@ -286,7 +286,7 @@ export class SnapManager {
// This needs to be external from any expensive work // This needs to be external from any expensive work
@computed get currentCommonAncestor() { @computed get currentCommonAncestor() {
return this.editor.findCommonAncestor(this.editor.selectedShapes) return this.editor.findCommonAncestor(this.editor.getSelectedShapes())
} }
// Points which belong to snappable shapes // Points which belong to snappable shapes

View file

@ -7,11 +7,11 @@ import { Vec2d } from '../primitives/Vec2d'
/** @internal */ /** @internal */
export function getRotationSnapshot({ editor }: { editor: Editor }): TLRotationSnapshot | null { export function getRotationSnapshot({ editor }: { editor: Editor }): TLRotationSnapshot | null {
const selectedShapes = editor.getSelectedShapes()
const { const {
selectionRotation, selectionRotation,
selectionRotatedPageBounds: selectionBounds, selectionRotatedPageBounds: selectionBounds,
inputs: { originPagePoint }, inputs: { originPagePoint },
selectedShapes,
} = editor } = editor
// todo: this assumes we're rotating the selected shapes // todo: this assumes we're rotating the selected shapes

View file

@ -37,8 +37,8 @@ export const TldrawSelectionForeground: TLSelectionForegroundComponent = track(
!editor.getIsMenuOpen() && editor.getInstanceState().cursor.type === 'default' !editor.getIsMenuOpen() && editor.getInstanceState().cursor.type === 'default'
const isCoarsePointer = editor.getInstanceState().isCoarsePointer const isCoarsePointer = editor.getInstanceState().isCoarsePointer
const shapes = editor.selectedShapes const shapes = editor.getSelectedShapes()
const onlyShape = editor.onlySelectedShape const onlyShape = editor.getOnlySelectedShape()
const isLockedShape = onlyShape && editor.isShapeOrAncestorLocked(onlyShape) const isLockedShape = onlyShape && editor.isShapeOrAncestorLocked(onlyShape)
// if all shapes have an expandBy for the selection outline, we can expand by the l // if all shapes have an expandBy for the selection outline, we can expand by the l

View file

@ -476,7 +476,7 @@ function centerSelecitonAroundPoint(editor: Editor, position: VecLike) {
const offset = selectionPageBounds!.center.sub(position) const offset = selectionPageBounds!.center.sub(position)
editor.updateShapes( editor.updateShapes(
editor.selectedShapes.map((shape) => { editor.getSelectedShapes().map((shape) => {
const localRotation = editor.getShapeParentTransform(shape).decompose().rotation const localRotation = editor.getShapeParentTransform(shape).decompose().rotation
const localDelta = Vec2d.Rot(offset, -localRotation) const localDelta = Vec2d.Rot(offset, -localRotation)
return { return {

View file

@ -488,24 +488,24 @@ describe("an arrow's parents", () => {
editor.setCurrentTool('frame') editor.setCurrentTool('frame')
editor.pointerDown(0, 0).pointerMove(100, 100).pointerUp() editor.pointerDown(0, 0).pointerMove(100, 100).pointerUp()
frameId = editor.onlySelectedShape!.id frameId = editor.getOnlySelectedShape()!.id
editor.setCurrentTool('geo') editor.setCurrentTool('geo')
editor.pointerDown(10, 10).pointerMove(20, 20).pointerUp() editor.pointerDown(10, 10).pointerMove(20, 20).pointerUp()
boxAid = editor.onlySelectedShape!.id boxAid = editor.getOnlySelectedShape()!.id
editor.setCurrentTool('geo') editor.setCurrentTool('geo')
editor.pointerDown(10, 80).pointerMove(20, 90).pointerUp() editor.pointerDown(10, 80).pointerMove(20, 90).pointerUp()
boxBid = editor.onlySelectedShape!.id boxBid = editor.getOnlySelectedShape()!.id
editor.setCurrentTool('geo') editor.setCurrentTool('geo')
editor.pointerDown(110, 10).pointerMove(120, 20).pointerUp() editor.pointerDown(110, 10).pointerMove(120, 20).pointerUp()
boxCid = editor.onlySelectedShape!.id boxCid = editor.getOnlySelectedShape()!.id
}) })
it("are updated when the arrow's bound shapes change", () => { it("are updated when the arrow's bound shapes change", () => {
// draw arrow from a to empty space within frame, but don't pointer up yet // draw arrow from a to empty space within frame, but don't pointer up yet
editor.setCurrentTool('arrow') editor.setCurrentTool('arrow')
editor.pointerDown(15, 15).pointerMove(50, 50) editor.pointerDown(15, 15).pointerMove(50, 50)
const arrowId = editor.onlySelectedShape!.id const arrowId = editor.getOnlySelectedShape()!.id
expect(editor.getShape(arrowId)).toMatchObject({ expect(editor.getShape(arrowId)).toMatchObject({
props: { props: {
@ -540,7 +540,7 @@ describe("an arrow's parents", () => {
// draw arrow from a to b // draw arrow from a to b
editor.setCurrentTool('arrow') editor.setCurrentTool('arrow')
editor.pointerDown(15, 15).pointerMove(15, 85).pointerUp() editor.pointerDown(15, 15).pointerMove(15, 85).pointerUp()
const arrowId = editor.onlySelectedShape!.id const arrowId = editor.getOnlySelectedShape()!.id
expect(editor.getShape(arrowId)).toMatchObject({ expect(editor.getShape(arrowId)).toMatchObject({
parentId: frameId, parentId: frameId,
@ -564,7 +564,7 @@ describe("an arrow's parents", () => {
// draw arrow from a to c // draw arrow from a to c
editor.setCurrentTool('arrow') editor.setCurrentTool('arrow')
editor.pointerDown(15, 15).pointerMove(115, 15).pointerUp() editor.pointerDown(15, 15).pointerMove(115, 15).pointerUp()
const arrowId = editor.onlySelectedShape!.id const arrowId = editor.getOnlySelectedShape()!.id
expect(editor.getShape(arrowId)).toMatchObject({ expect(editor.getShape(arrowId)).toMatchObject({
parentId: editor.currentPageId, parentId: editor.currentPageId,
props: { props: {

View file

@ -487,7 +487,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
// Not a class component, but eslint can't tell that :( // Not a class component, but eslint can't tell that :(
// eslint-disable-next-line react-hooks/rules-of-hooks // eslint-disable-next-line react-hooks/rules-of-hooks
const theme = useDefaultColorTheme() const theme = useDefaultColorTheme()
const onlySelectedShape = this.editor.onlySelectedShape const onlySelectedShape = this.editor.getOnlySelectedShape()
const shouldDisplayHandles = const shouldDisplayHandles =
this.editor.isInAny( this.editor.isInAny(
'select.idle', 'select.idle',

View file

@ -18,7 +18,7 @@ export class Idle extends StateNode {
override onKeyUp: TLEventHandlers['onKeyUp'] = (info) => { override onKeyUp: TLEventHandlers['onKeyUp'] = (info) => {
if (info.key === 'Enter') { if (info.key === 'Enter') {
if (this.editor.getInstanceState().isReadonly) return null if (this.editor.getInstanceState().isReadonly) return null
const { onlySelectedShape } = this.editor const onlySelectedShape = this.editor.getOnlySelectedShape()
// If the only selected shape is editable, start editing it // If the only selected shape is editable, start editing it
if ( if (
onlySelectedShape && onlySelectedShape &&

View file

@ -103,7 +103,7 @@ describe('When in the idle state', () => {
expect(editor.currentPageShapes.length).toBe(2) expect(editor.currentPageShapes.length).toBe(2)
editor.selectAll() editor.selectAll()
expect(editor.selectedShapes.length).toBe(2) expect(editor.getSelectedShapes().length).toBe(2)
editor.keyUp('Enter') editor.keyUp('Enter')
editor.expectPathToBe('root.select.idle') editor.expectPathToBe('root.select.idle')

View file

@ -15,7 +15,7 @@ export class Idle extends StateNode {
if (info.key === 'Enter') { if (info.key === 'Enter') {
if (this.editor.getInstanceState().isReadonly) return null if (this.editor.getInstanceState().isReadonly) return null
const { onlySelectedShape } = this.editor const onlySelectedShape = this.editor.getOnlySelectedShape()
// If the only selected shape is editable, start editing it // If the only selected shape is editable, start editing it
if ( if (
onlySelectedShape && onlySelectedShape &&

View file

@ -86,7 +86,7 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
const isSelected = useValue( const isSelected = useValue(
'onlySelectedShape', 'onlySelectedShape',
() => shape.id === this.editor.onlySelectedShape?.id, () => shape.id === this.editor.getOnlySelectedShape()?.id,
[this.editor] [this.editor]
) )

View file

@ -24,7 +24,7 @@ export class Idle extends StateNode {
override onKeyDown: TLEventHandlers['onKeyDown'] = (info) => { override onKeyDown: TLEventHandlers['onKeyDown'] = (info) => {
if (info.key === 'Enter') { if (info.key === 'Enter') {
if (this.editor.getInstanceState().isReadonly) return null if (this.editor.getInstanceState().isReadonly) return null
const { onlySelectedShape } = this.editor const onlySelectedShape = this.editor.getOnlySelectedShape()
// If the only selected shape is editable, start editing it // If the only selected shape is editable, start editing it
if ( if (
onlySelectedShape && onlySelectedShape &&

View file

@ -11,7 +11,7 @@ export class Idle extends StateNode {
{ ephemeral: true } { ephemeral: true }
) )
const { onlySelectedShape } = this.editor const onlySelectedShape = this.editor.getOnlySelectedShape()
// well this fucking sucks. what the fuck. // well this fucking sucks. what the fuck.
// it's possible for a user to enter cropping, then undo // it's possible for a user to enter cropping, then undo

View file

@ -88,7 +88,7 @@ export class TranslatingCrop extends StateNode {
} }
private createSnapshot() { private createSnapshot() {
const shape = this.editor.onlySelectedShape as ShapeWithCrop const shape = this.editor.getOnlySelectedShape() as ShapeWithCrop
return { shape } return { shape }
} }

View file

@ -60,7 +60,7 @@ export class Cropping extends StateNode {
} }
private updateCursor() { private updateCursor() {
const selectedShape = this.editor.selectedShapes[0] const selectedShape = this.editor.getSelectedShapes()[0]
if (!selectedShape) return if (!selectedShape) return
const cursorType = CursorTypeMap[this.info.handle!] const cursorType = CursorTypeMap[this.info.handle!]
@ -229,7 +229,7 @@ export class Cropping extends StateNode {
inputs: { originPagePoint }, inputs: { originPagePoint },
} = this.editor } = this.editor
const shape = this.editor.onlySelectedShape as TLImageShape const shape = this.editor.getOnlySelectedShape() as TLImageShape
const selectionBounds = this.editor.selectionRotatedPageBounds! const selectionBounds = this.editor.selectionRotatedPageBounds!

View file

@ -67,8 +67,8 @@ export class Idle extends StateNode {
} }
const selectedShapeIds = this.editor.getSelectedShapeIds() const selectedShapeIds = this.editor.getSelectedShapeIds()
const onlySelectedShape = this.editor.getOnlySelectedShape()
const { const {
onlySelectedShape,
inputs: { currentPagePoint }, inputs: { currentPagePoint },
} = this.editor } = this.editor
@ -224,7 +224,7 @@ export class Idle extends StateNode {
case 'selection': { case 'selection': {
if (this.editor.getInstanceState().isReadonly) break if (this.editor.getInstanceState().isReadonly) break
const { onlySelectedShape } = this.editor const onlySelectedShape = this.editor.getOnlySelectedShape()
if (onlySelectedShape) { if (onlySelectedShape) {
const util = this.editor.getShapeUtil(onlySelectedShape) const util = this.editor.getShapeUtil(onlySelectedShape)
@ -346,8 +346,8 @@ export class Idle extends StateNode {
} }
const selectedShapeIds = this.editor.getSelectedShapeIds() const selectedShapeIds = this.editor.getSelectedShapeIds()
const onlySelectedShape = this.editor.getOnlySelectedShape()
const { const {
onlySelectedShape,
inputs: { currentPagePoint }, inputs: { currentPagePoint },
} = this.editor } = this.editor
@ -425,7 +425,7 @@ export class Idle extends StateNode {
override onKeyUp = (info: TLKeyboardEventInfo) => { override onKeyUp = (info: TLKeyboardEventInfo) => {
switch (info.code) { switch (info.code) {
case 'Enter': { case 'Enter': {
const { selectedShapes } = this.editor const selectedShapes = this.editor.getSelectedShapes()
// On enter, if every selected shape is a group, then select all of the children of the groups // On enter, if every selected shape is a group, then select all of the children of the groups
if ( if (
@ -438,7 +438,7 @@ export class Idle extends StateNode {
} }
// If the only selected shape is editable, then begin editing it // If the only selected shape is editable, then begin editing it
const { onlySelectedShape } = this.editor const onlySelectedShape = this.editor.getOnlySelectedShape()
if (onlySelectedShape && this.shouldStartEditingShape(onlySelectedShape)) { if (onlySelectedShape && this.shouldStartEditingShape(onlySelectedShape)) {
this.startEditingShape(onlySelectedShape, { this.startEditingShape(onlySelectedShape, {
...info, ...info,
@ -457,7 +457,9 @@ export class Idle extends StateNode {
} }
} }
private shouldStartEditingShape(shape: TLShape | null = this.editor.onlySelectedShape): boolean { private shouldStartEditingShape(
shape: TLShape | null = this.editor.getOnlySelectedShape()
): boolean {
if (!shape) return false if (!shape) return false
if (this.editor.isShapeOrAncestorLocked(shape) && shape.type !== 'embed') return false if (this.editor.isShapeOrAncestorLocked(shape) && shape.type !== 'embed') return false
if (!this.canInteractWithShapeInReadOnly(shape)) return false if (!this.canInteractWithShapeInReadOnly(shape)) return false

View file

@ -25,7 +25,7 @@ export class PointingCropHandle extends StateNode {
override onEnter = (info: TLPointingCropHandleInfo) => { override onEnter = (info: TLPointingCropHandleInfo) => {
this.info = info this.info = info
this.parent.currentToolIdMask = info.onInteractionEnd this.parent.currentToolIdMask = info.onInteractionEnd
const selectedShape = this.editor.selectedShapes[0] const selectedShape = this.editor.getSelectedShapes()[0]
if (!selectedShape) return if (!selectedShape) return
this.updateCursor(selectedShape) this.updateCursor(selectedShape)

View file

@ -32,7 +32,7 @@ export class PointingResizeHandle extends StateNode {
private info = {} as PointingResizeHandleInfo private info = {} as PointingResizeHandleInfo
private updateCursor() { private updateCursor() {
const selected = this.editor.selectedShapes const selected = this.editor.getSelectedShapes()
const cursorType = CursorTypeMap[this.info.handle!] const cursorType = CursorTypeMap[this.info.handle!]
this.editor.updateInstanceState({ this.editor.updateInstanceState({
cursor: { type: cursorType, rotation: selected.length === 1 ? selected[0].rotation : 0 }, cursor: { type: cursorType, rotation: selected.length === 1 ? selected[0].rotation : 0 },

View file

@ -63,7 +63,9 @@ export class Resizing extends StateNode {
} }
this.snapshot = this._createSnapshot() this.snapshot = this._createSnapshot()
this.markId = isCreating ? `creating:${this.editor.onlySelectedShape!.id}` : 'starting resizing' this.markId = isCreating
? `creating:${this.editor.getOnlySelectedShape()!.id}`
: 'starting resizing'
if (!isCreating) this.editor.mark(this.markId) if (!isCreating) this.editor.mark(this.markId)
@ -107,8 +109,9 @@ export class Resizing extends StateNode {
private complete() { private complete() {
this.handleResizeEnd() this.handleResizeEnd()
if (this.editAfterComplete && this.editor.onlySelectedShape) { const onlySelectedShape = this.editor.getOnlySelectedShape()
this.editor.setEditingShape(this.editor.onlySelectedShape.id) if (this.editAfterComplete && onlySelectedShape) {
this.editor.setEditingShape(onlySelectedShape.id)
this.editor.setCurrentTool('select.editing_shape') this.editor.setCurrentTool('select.editing_shape')
return return
} }

View file

@ -53,7 +53,7 @@ export class Translating extends StateNode {
this.isCreating = isCreating this.isCreating = isCreating
this.editAfterComplete = editAfterComplete this.editAfterComplete = editAfterComplete
this.markId = isCreating ? `creating:${this.editor.onlySelectedShape!.id}` : 'translating' this.markId = isCreating ? `creating:${this.editor.getOnlySelectedShape()!.id}` : 'translating'
this.editor.mark(this.markId) this.editor.mark(this.markId)
this.isCloning = false this.isCloning = false
this.info = info this.info = info
@ -166,7 +166,7 @@ export class Translating extends StateNode {
this.editor.setCurrentTool(this.info.onInteractionEnd) this.editor.setCurrentTool(this.info.onInteractionEnd)
} else { } else {
if (this.editAfterComplete) { if (this.editAfterComplete) {
const onlySelected = this.editor.onlySelectedShape const onlySelected = this.editor.getOnlySelectedShape()
if (onlySelected) { if (onlySelected) {
this.editor.setEditingShape(onlySelected.id) this.editor.setEditingShape(onlySelected.id)
this.editor.setCurrentTool('select.editing_shape') this.editor.setCurrentTool('select.editing_shape')

View file

@ -1,7 +1,7 @@
import { Editor } from '@tldraw/editor' import { Editor } from '@tldraw/editor'
export function getShouldEnterCropMode(editor: Editor): boolean { export function getShouldEnterCropMode(editor: Editor): boolean {
const { onlySelectedShape } = editor const onlySelectedShape = editor.getOnlySelectedShape()
return !!( return !!(
onlySelectedShape && onlySelectedShape &&
!editor.isShapeOrAncestorLocked(onlySelectedShape) && !editor.isShapeOrAncestorLocked(onlySelectedShape) &&

View file

@ -27,7 +27,7 @@ export const ContextMenu = function ContextMenu({ children }: { children: any })
const cb = useCallback( const cb = useCallback(
(isOpen: boolean) => { (isOpen: boolean) => {
if (!isOpen) { if (!isOpen) {
const { onlySelectedShape } = editor const onlySelectedShape = editor.getOnlySelectedShape()
if (onlySelectedShape && editor.isShapeOrAncestorLocked(onlySelectedShape)) { if (onlySelectedShape && editor.isShapeOrAncestorLocked(onlySelectedShape)) {
editor.setSelectedShapes([]) editor.setSelectedShapes([])
@ -35,8 +35,8 @@ export const ContextMenu = function ContextMenu({ children }: { children: any })
} else { } else {
// Weird route: selecting locked shapes on long press // Weird route: selecting locked shapes on long press
if (editor.getInstanceState().isCoarsePointer) { if (editor.getInstanceState().isCoarsePointer) {
const selectedShapes = editor.getSelectedShapes()
const { const {
selectedShapes,
inputs: { currentPagePoint }, inputs: { currentPagePoint },
} = editor } = editor
@ -45,7 +45,7 @@ export const ContextMenu = function ContextMenu({ children }: { children: any })
if ( if (
// if there are no selected shapes // if there are no selected shapes
!editor.selectedShapes.length || !editor.getSelectedShapes().length ||
// OR if none of the shapes at the point include the selected shape // OR if none of the shapes at the point include the selected shape
!shapesAtPoint.some((s) => selectedShapes.includes(s)) !shapesAtPoint.some((s) => selectedShapes.includes(s))
) { ) {

View file

@ -175,7 +175,7 @@ const DebugMenuContent = track(function DebugMenuContent({
return count return count
} }
const { selectedShapes } = editor const selectedShapes = editor.getSelectedShapes()
const shapes = selectedShapes.length === 0 ? editor.renderingShapes : selectedShapes const shapes = selectedShapes.length === 0 ? editor.renderingShapes : selectedShapes

View file

@ -23,7 +23,7 @@ type ShapeWithUrl = TLBaseShape<string, { url: string }>
export const EditLinkDialog = track(function EditLinkDialog({ onClose }: TLUiDialogProps) { export const EditLinkDialog = track(function EditLinkDialog({ onClose }: TLUiDialogProps) {
const editor = useEditor() const editor = useEditor()
const selectedShape = editor.onlySelectedShape const selectedShape = editor.getOnlySelectedShape()
if ( if (
!(selectedShape && 'url' in selectedShape.props && typeof selectedShape.props.url === 'string') !(selectedShape && 'url' in selectedShape.props && typeof selectedShape.props.url === 'string')
@ -89,7 +89,7 @@ export const EditLinkDialogInner = track(function EditLinkDialogInner({
}, []) }, [])
const handleClear = useCallback(() => { const handleClear = useCallback(() => {
const { onlySelectedShape } = editor const onlySelectedShape = editor.getOnlySelectedShape()
if (!onlySelectedShape) return if (!onlySelectedShape) return
editor.updateShapes([ editor.updateShapes([
{ id: onlySelectedShape.id, type: onlySelectedShape.type, props: { url: '' } }, { id: onlySelectedShape.id, type: onlySelectedShape.type, props: { url: '' } },
@ -98,7 +98,7 @@ export const EditLinkDialogInner = track(function EditLinkDialogInner({
}, [editor, onClose]) }, [editor, onClose])
const handleComplete = useCallback(() => { const handleComplete = useCallback(() => {
const { onlySelectedShape } = editor const onlySelectedShape = editor.getOnlySelectedShape()
if (!onlySelectedShape) return if (!onlySelectedShape) return

View file

@ -233,7 +233,8 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
trackEvent('toggle-auto-size', { source }) trackEvent('toggle-auto-size', { source })
editor.mark('toggling auto size') editor.mark('toggling auto size')
editor.updateShapes( editor.updateShapes(
editor.selectedShapes editor
.getSelectedShapes()
.filter( .filter(
(shape): shape is TLTextShape => (shape): shape is TLTextShape =>
editor.isShapeOfType<TLTextShape>(shape, 'text') && shape.props.autoSize === false editor.isShapeOfType<TLTextShape>(shape, 'text') && shape.props.autoSize === false
@ -299,7 +300,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
editor.batch(() => { editor.batch(() => {
trackEvent('convert-to-bookmark', { source }) trackEvent('convert-to-bookmark', { source })
const shapes = editor.selectedShapes const shapes = editor.getSelectedShapes()
const createList: TLShapePartial[] = [] const createList: TLShapePartial[] = []
const deleteList: TLShapeId[] = [] const deleteList: TLShapeId[] = []
@ -441,7 +442,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
if (mustGoBackToSelectToolFirst()) return if (mustGoBackToSelectToolFirst()) return
trackEvent('group-shapes', { source }) trackEvent('group-shapes', { source })
const { onlySelectedShape } = editor const onlySelectedShape = editor.getOnlySelectedShape()
if (onlySelectedShape && editor.isShapeOfType<TLGroupShape>(onlySelectedShape, 'group')) { if (onlySelectedShape && editor.isShapeOfType<TLGroupShape>(onlySelectedShape, 'group')) {
editor.mark('ungroup') editor.mark('ungroup')
editor.ungroupShapes(editor.getSelectedShapeIds()) editor.ungroupShapes(editor.getSelectedShapeIds())

View file

@ -76,7 +76,7 @@ export const TLUiContextMenuSchemaProvider = track(function TLUiContextMenuSchem
const allowUngroup = useAllowUngroup() const allowUngroup = useAllowUngroup()
const hasClipboardWrite = Boolean(window.navigator.clipboard?.write) const hasClipboardWrite = Boolean(window.navigator.clipboard?.write)
const showEditLink = useHasLinkShapeSelected() const showEditLink = useHasLinkShapeSelected()
const { onlySelectedShape } = editor const onlySelectedShape = editor.getOnlySelectedShape()
const isShapeLocked = onlySelectedShape && editor.isShapeOrAncestorLocked(onlySelectedShape) const isShapeLocked = onlySelectedShape && editor.isShapeOrAncestorLocked(onlySelectedShape)
const contextTLUiMenuSchema = useMemo<TLUiMenuSchema>(() => { const contextTLUiMenuSchema = useMemo<TLUiMenuSchema>(() => {

View file

@ -5,7 +5,7 @@ export function useHasLinkShapeSelected() {
return useValue( return useValue(
'hasLinkShapeSelected', 'hasLinkShapeSelected',
() => { () => {
const { selectedShapes } = editor const selectedShapes = editor.getSelectedShapes()
return ( return (
selectedShapes.length === 1 && selectedShapes.length === 1 &&
'url' in selectedShapes[0].props && 'url' in selectedShapes[0].props &&

View file

@ -84,7 +84,7 @@ export function TLUiMenuSchemaProvider({ overrides, children }: TLUiMenuSchemaPr
const oneEmbedSelected = useValue( const oneEmbedSelected = useValue(
'oneEmbedSelected', 'oneEmbedSelected',
() => { () => {
const { onlySelectedShape } = editor const onlySelectedShape = editor.getOnlySelectedShape()
if (!onlySelectedShape) return false if (!onlySelectedShape) return false
return !!( return !!(
editor.isShapeOfType<TLEmbedShape>(onlySelectedShape, 'embed') && editor.isShapeOfType<TLEmbedShape>(onlySelectedShape, 'embed') &&
@ -98,7 +98,7 @@ export function TLUiMenuSchemaProvider({ overrides, children }: TLUiMenuSchemaPr
const oneEmbeddableBookmarkSelected = useValue( const oneEmbeddableBookmarkSelected = useValue(
'oneEmbeddableBookmarkSelected', 'oneEmbeddableBookmarkSelected',
() => { () => {
const { onlySelectedShape } = editor const onlySelectedShape = editor.getOnlySelectedShape()
if (!onlySelectedShape) return false if (!onlySelectedShape) return false
return !!( return !!(
editor.isShapeOfType<TLBookmarkShape>(onlySelectedShape, 'bookmark') && editor.isShapeOfType<TLBookmarkShape>(onlySelectedShape, 'bookmark') &&

View file

@ -12,7 +12,7 @@ export function useOnlyFlippableShape() {
return useValue( return useValue(
'onlyFlippableShape', 'onlyFlippableShape',
() => { () => {
const { selectedShapes } = editor const selectedShapes = editor.getSelectedShapes()
return ( return (
selectedShapes.length === 1 && selectedShapes.length === 1 &&
selectedShapes.every( selectedShapes.every(

View file

@ -5,7 +5,7 @@ export function useShowAutoSizeToggle() {
return useValue( return useValue(
'showAutoSizeToggle', 'showAutoSizeToggle',
() => { () => {
const { selectedShapes } = editor const selectedShapes = editor.getSelectedShapes()
return ( return (
selectedShapes.length === 1 && selectedShapes.length === 1 &&
editor.isShapeOfType<TLTextShape>(selectedShapes[0], 'text') && editor.isShapeOfType<TLTextShape>(selectedShapes[0], 'text') &&

View file

@ -415,7 +415,7 @@ describe('When clicking and dragging', () => {
describe('Does not erase hollow shapes on click', () => { describe('Does not erase hollow shapes on click', () => {
it('Returns to select on cancel', () => { it('Returns to select on cancel', () => {
editor.selectAll().deleteShapes(editor.selectedShapes) editor.selectAll().deleteShapes(editor.getSelectedShapes())
expect(editor.currentPageShapes.length).toBe(0) expect(editor.currentPageShapes.length).toBe(0)
editor.createShape({ editor.createShape({
id: createShapeId(), id: createShapeId(),

View file

@ -19,6 +19,6 @@ describe('Highlight shape', () => {
editor.setCurrentTool('highlight').pointerDown(60, 60).pointerUp() editor.setCurrentTool('highlight').pointerDown(60, 60).pointerUp()
editor.setCurrentTool('select').pointerDown(70, 70).pointerUp() editor.setCurrentTool('select').pointerDown(70, 70).pointerUp()
expect(editor.selectedShapes).toHaveLength(1) expect(editor.getSelectedShapes()).toHaveLength(1)
}) })
}) })

View file

@ -94,7 +94,7 @@ describe('TLSelectTool.Translating', () => {
editor.pointerDown(150, 150, { target: 'shape', shape }) editor.pointerDown(150, 150, { target: 'shape', shape })
editor.pointerMove(150, 250) editor.pointerMove(150, 250)
editor.pointerUp() editor.pointerUp()
const box2Id = editor.onlySelectedShape!.id const box2Id = editor.getOnlySelectedShape()!.id
expect(editor.currentPageShapes.length).toStrictEqual(2) expect(editor.currentPageShapes.length).toStrictEqual(2)
expect(ids.box1).not.toEqual(box2Id) expect(ids.box1).not.toEqual(box2Id)
@ -318,11 +318,11 @@ describe('When editing shapes', () => {
// start editing the geo shape // start editing the geo shape
editor.doubleClick(50, 50, { target: 'shape', shape: editor.getShape(ids.geo1) }) editor.doubleClick(50, 50, { target: 'shape', shape: editor.getShape(ids.geo1) })
expect(editor.editingShapeId).toBe(ids.geo1) expect(editor.editingShapeId).toBe(ids.geo1)
expect(editor.onlySelectedShape?.id).toBe(ids.geo1) expect(editor.getOnlySelectedShape()?.id).toBe(ids.geo1)
// point the text shape // point the text shape
editor.pointerDown(50, 50, { target: 'shape', shape: editor.getShape(ids.text1) }) editor.pointerDown(50, 50, { target: 'shape', shape: editor.getShape(ids.text1) })
expect(editor.editingShapeId).toBe(null) expect(editor.editingShapeId).toBe(null)
expect(editor.onlySelectedShape?.id).toBe(ids.text1) expect(editor.getOnlySelectedShape()?.id).toBe(ids.text1)
}) })
// The behavior described here will only work end to end, not with the library, // The behavior described here will only work end to end, not with the library,
@ -334,12 +334,12 @@ describe('When editing shapes', () => {
// start editing the geo shape // start editing the geo shape
editor.doubleClick(50, 50, { target: 'shape', shape: editor.getShape(ids.geo1) }) editor.doubleClick(50, 50, { target: 'shape', shape: editor.getShape(ids.geo1) })
expect(editor.editingShapeId).toBe(ids.geo1) expect(editor.editingShapeId).toBe(ids.geo1)
expect(editor.onlySelectedShape?.id).toBe(ids.geo1) expect(editor.getOnlySelectedShape()?.id).toBe(ids.geo1)
// point the other geo shape // point the other geo shape
editor.pointerDown(50, 50, { target: 'shape', shape: editor.getShape(ids.geo2) }) editor.pointerDown(50, 50, { target: 'shape', shape: editor.getShape(ids.geo2) })
// that other shape should now be editing and selected! // that other shape should now be editing and selected!
expect(editor.editingShapeId).toBe(ids.geo2) expect(editor.editingShapeId).toBe(ids.geo2)
expect(editor.onlySelectedShape?.id).toBe(ids.geo2) expect(editor.getOnlySelectedShape()?.id).toBe(ids.geo2)
}) })
// This works but only end to end — the logic had to move to React // This works but only end to end — the logic had to move to React
@ -352,7 +352,7 @@ describe('When editing shapes', () => {
editor.pointerDown(50, 50, { target: 'shape', shape: editor.getShape(ids.text2) }) editor.pointerDown(50, 50, { target: 'shape', shape: editor.getShape(ids.text2) })
// that other shape should now be editing and selected! // that other shape should now be editing and selected!
expect(editor.editingShapeId).toBe(ids.text2) expect(editor.editingShapeId).toBe(ids.text2)
expect(editor.onlySelectedShape?.id).toBe(ids.text2) expect(editor.getOnlySelectedShape()?.id).toBe(ids.text2)
}) })
it('Double clicking the canvas creates a new text shape', () => { it('Double clicking the canvas creates a new text shape', () => {

View file

@ -18,12 +18,12 @@ describe('arrowBindingsIndex', () => {
editor.selectNone() editor.selectNone()
editor.setCurrentTool('arrow') editor.setCurrentTool('arrow')
editor.pointerDown(50, 50) editor.pointerDown(50, 50)
expect(editor.onlySelectedShape).toBe(null) expect(editor.getOnlySelectedShape()).toBe(null)
expect(editor.getArrowsBoundTo(ids.box1)).toEqual([]) expect(editor.getArrowsBoundTo(ids.box1)).toEqual([])
editor.pointerMove(50, 55) editor.pointerMove(50, 55)
expect(editor.onlySelectedShape).not.toBe(null) expect(editor.getOnlySelectedShape()).not.toBe(null)
const arrow = editor.onlySelectedShape! const arrow = editor.getOnlySelectedShape()!
expect(arrow.type).toBe('arrow') expect(arrow.type).toBe('arrow')
expect(editor.getArrowsBoundTo(ids.box1)).toEqual([ expect(editor.getArrowsBoundTo(ids.box1)).toEqual([
{ arrowId: arrow.id, handleId: 'start' }, { arrowId: arrow.id, handleId: 'start' },
@ -48,7 +48,7 @@ describe('arrowBindingsIndex', () => {
expect(editor.getArrowsBoundTo(ids.box1)).toEqual([]) expect(editor.getArrowsBoundTo(ids.box1)).toEqual([])
editor.pointerMove(250, 50) editor.pointerMove(250, 50)
const arrow1 = editor.onlySelectedShape! const arrow1 = editor.getOnlySelectedShape()!
expect(arrow1.type).toBe('arrow') expect(arrow1.type).toBe('arrow')
expect(editor.getArrowsBoundTo(ids.box1)).toEqual([{ arrowId: arrow1.id, handleId: 'start' }]) expect(editor.getArrowsBoundTo(ids.box1)).toEqual([{ arrowId: arrow1.id, handleId: 'start' }])
@ -62,7 +62,7 @@ describe('arrowBindingsIndex', () => {
// start at box 1 and end on the page // start at box 1 and end on the page
editor.setCurrentTool('arrow') editor.setCurrentTool('arrow')
editor.pointerDown(50, 50).pointerMove(50, -50).pointerUp(50, -50) editor.pointerDown(50, 50).pointerMove(50, -50).pointerUp(50, -50)
const arrow2 = editor.onlySelectedShape! const arrow2 = editor.getOnlySelectedShape()!
expect(arrow2.type).toBe('arrow') expect(arrow2.type).toBe('arrow')
expect(editor.getArrowsBoundTo(ids.box1)).toEqual([ expect(editor.getArrowsBoundTo(ids.box1)).toEqual([
@ -73,7 +73,7 @@ describe('arrowBindingsIndex', () => {
// start outside box 1 and end in box 1 // start outside box 1 and end in box 1
editor.setCurrentTool('arrow') editor.setCurrentTool('arrow')
editor.pointerDown(0, -50).pointerMove(50, 50).pointerUp(50, 50) editor.pointerDown(0, -50).pointerMove(50, 50).pointerUp(50, 50)
const arrow3 = editor.onlySelectedShape! const arrow3 = editor.getOnlySelectedShape()!
expect(arrow3.type).toBe('arrow') expect(arrow3.type).toBe('arrow')
expect(editor.getArrowsBoundTo(ids.box1)).toEqual([ expect(editor.getArrowsBoundTo(ids.box1)).toEqual([
@ -91,7 +91,7 @@ describe('arrowBindingsIndex', () => {
editor.expectToBeIn('arrow.pointing') editor.expectToBeIn('arrow.pointing')
editor.pointerMove(250, -50) editor.pointerMove(250, -50)
editor.expectToBeIn('select.dragging_handle') editor.expectToBeIn('select.dragging_handle')
const arrow4 = editor.onlySelectedShape! const arrow4 = editor.getOnlySelectedShape()!
expect(editor.getArrowsBoundTo(ids.box2)).toEqual([ expect(editor.getArrowsBoundTo(ids.box2)).toEqual([
{ arrowId: arrow1.id, handleId: 'end' }, { arrowId: arrow1.id, handleId: 'end' },
@ -110,7 +110,7 @@ describe('arrowBindingsIndex', () => {
// start outside box 2 and enter in box 2 // start outside box 2 and enter in box 2
editor.setCurrentTool('arrow') editor.setCurrentTool('arrow')
editor.pointerDown(250, -50).pointerMove(250, 50).pointerUp(250, 50) editor.pointerDown(250, -50).pointerMove(250, 50).pointerUp(250, 50)
const arrow5 = editor.onlySelectedShape! const arrow5 = editor.getOnlySelectedShape()!
expect(arrow5.type).toBe('arrow') expect(arrow5.type).toBe('arrow')
expect(editor.getArrowsBoundTo(ids.box1)).toEqual([ expect(editor.getArrowsBoundTo(ids.box1)).toEqual([
@ -150,23 +150,23 @@ describe('arrowBindingsIndex', () => {
// span both boxes // span both boxes
editor.setCurrentTool('arrow') editor.setCurrentTool('arrow')
editor.pointerDown(50, 50).pointerMove(250, 50).pointerUp(250, 50) editor.pointerDown(50, 50).pointerMove(250, 50).pointerUp(250, 50)
arrowAId = editor.onlySelectedShape!.id arrowAId = editor.getOnlySelectedShape()!.id
// start at box 1 and leave // start at box 1 and leave
editor.setCurrentTool('arrow') editor.setCurrentTool('arrow')
editor.pointerDown(50, 50).pointerMove(50, -50).pointerUp(50, -50) editor.pointerDown(50, 50).pointerMove(50, -50).pointerUp(50, -50)
arrowBId = editor.onlySelectedShape!.id arrowBId = editor.getOnlySelectedShape()!.id
// start outside box 1 and enter // start outside box 1 and enter
editor.setCurrentTool('arrow') editor.setCurrentTool('arrow')
editor.pointerDown(50, -50).pointerMove(50, 50).pointerUp(50, 50) editor.pointerDown(50, -50).pointerMove(50, 50).pointerUp(50, 50)
arrowCId = editor.onlySelectedShape!.id arrowCId = editor.getOnlySelectedShape()!.id
// start at box 2 and leave // start at box 2 and leave
editor.setCurrentTool('arrow') editor.setCurrentTool('arrow')
editor.pointerDown(250, 50).pointerMove(250, -50).pointerUp(250, -50) editor.pointerDown(250, 50).pointerMove(250, -50).pointerUp(250, -50)
arrowDId = editor.onlySelectedShape!.id arrowDId = editor.getOnlySelectedShape()!.id
// start outside box 2 and enter // start outside box 2 and enter
editor.setCurrentTool('arrow') editor.setCurrentTool('arrow')
editor.pointerDown(250, -50).pointerMove(250, 50).pointerUp(250, 50) editor.pointerDown(250, -50).pointerMove(250, 50).pointerUp(250, 50)
arrowEId = editor.onlySelectedShape!.id arrowEId = editor.getOnlySelectedShape()!.id
}) })
it('deletes the entry if you delete the bound shapes', () => { it('deletes the entry if you delete the bound shapes', () => {
expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3) expect(editor.getArrowsBoundTo(ids.box2)).toHaveLength(3)
@ -236,7 +236,8 @@ describe('arrowBindingsIndex', () => {
editor.selectAll() editor.selectAll()
editor.duplicateShapes(editor.getSelectedShapeIds()) editor.duplicateShapes(editor.getSelectedShapeIds())
const [box1Clone, box2Clone] = editor.selectedShapes const [box1Clone, box2Clone] = editor
.getSelectedShapes()
.filter((shape) => editor.isShapeOfType<TLGeoShape>(shape, 'geo')) .filter((shape) => editor.isShapeOfType<TLGeoShape>(shape, 'geo'))
.sort((a, b) => a.x - b.x) .sort((a, b) => a.x - b.x)

View file

@ -18,7 +18,7 @@ const ids = {
arrow3: createShapeId('arrow3'), arrow3: createShapeId('arrow3'),
} }
const arrow = () => editor.onlySelectedShape as TLArrowShape const arrow = () => editor.getOnlySelectedShape() as TLArrowShape
beforeEach(() => { beforeEach(() => {
editor = new TestEditor() editor = new TestEditor()

View file

@ -10,12 +10,12 @@ beforeEach(() => {
it('Sets shape meta by default to an empty object', () => { it('Sets shape meta by default to an empty object', () => {
const id = createShapeId() const id = createShapeId()
editor.createShapes([{ id, type: 'geo' }]).select(id) editor.createShapes([{ id, type: 'geo' }]).select(id)
expect(editor.onlySelectedShape!.meta).toStrictEqual({}) expect(editor.getOnlySelectedShape()!.meta).toStrictEqual({})
}) })
it('Sets shape meta', () => { it('Sets shape meta', () => {
editor.getInitialMetaForShape = (shape) => ({ firstThreeCharactersOfId: shape.id.slice(0, 3) }) editor.getInitialMetaForShape = (shape) => ({ firstThreeCharactersOfId: shape.id.slice(0, 3) })
const id = createShapeId() const id = createShapeId()
editor.createShapes([{ id, type: 'geo' }]).select(id) editor.createShapes([{ id, type: 'geo' }]).select(id)
expect(editor.onlySelectedShape!.meta).toStrictEqual({ firstThreeCharactersOfId: 'sha' }) expect(editor.getOnlySelectedShape()!.meta).toStrictEqual({ firstThreeCharactersOfId: 'sha' })
}) })

View file

@ -189,9 +189,9 @@ describe('arrows', () => {
editor.pointerMove(255, 255) editor.pointerMove(255, 255)
editor.pointerMove(450, 450) editor.pointerMove(450, 450)
editor.pointerUp(450, 450) editor.pointerUp(450, 450)
const arrow = editor.onlySelectedShape! const arrow = editor.getOnlySelectedShape()!
expect(editor.getShapePageBounds(editor.onlySelectedShape!)).toCloselyMatchObject({ expect(editor.getShapePageBounds(editor.getOnlySelectedShape()!)).toCloselyMatchObject({
// exiting at the bottom right corner of the first box // exiting at the bottom right corner of the first box
x: 300, x: 300,
y: 300, y: 300,
@ -224,7 +224,7 @@ describe('arrows', () => {
.pointerMove(255, 255) .pointerMove(255, 255)
.pointerMove(450, 450) .pointerMove(450, 450)
.pointerUp(450, 450) .pointerUp(450, 450)
const arrow = editor.onlySelectedShape! const arrow = editor.getOnlySelectedShape()!
expect(editor.getArrowsBoundTo(firstBox.id).length).toBe(1) expect(editor.getArrowsBoundTo(firstBox.id).length).toBe(1)
expect(editor.getArrowsBoundTo(secondBox.id).length).toBe(1) expect(editor.getArrowsBoundTo(secondBox.id).length).toBe(1)

View file

@ -50,7 +50,7 @@ describe('when multiple shapes are selected', () => {
it('stretches horizontally and preserves aspect ratio', () => { it('stretches horizontally and preserves aspect ratio', () => {
const videoA = createVideoShape() const videoA = createVideoShape()
editor.selectAll() editor.selectAll()
expect(editor.selectedShapes.length).toBe(4) expect(editor.getSelectedShapes().length).toBe(4)
editor.stretchShapes(editor.getSelectedShapeIds(), 'horizontal') editor.stretchShapes(editor.getSelectedShapeIds(), 'horizontal')
jest.advanceTimersByTime(1000) jest.advanceTimersByTime(1000)
const newHeight = (500 * 9) / 16 const newHeight = (500 * 9) / 16
@ -76,7 +76,7 @@ describe('when multiple shapes are selected', () => {
it('stretches vertically and preserves aspect ratio', () => { it('stretches vertically and preserves aspect ratio', () => {
const videoA = createVideoShape() const videoA = createVideoShape()
editor.selectAll() editor.selectAll()
expect(editor.selectedShapes.length).toBe(4) expect(editor.getSelectedShapes().length).toBe(4)
editor.stretchShapes(editor.getSelectedShapeIds(), 'vertical') editor.stretchShapes(editor.getSelectedShapeIds(), 'vertical')
jest.advanceTimersByTime(1000) jest.advanceTimersByTime(1000)
const newWidth = (500 * 16) / 9 const newWidth = (500 * 16) / 9

View file

@ -297,7 +297,7 @@ describe('When one shape is selected', () => {
editor.selectAll() editor.selectAll()
editor.groupShapes(editor.getSelectedShapeIds()) // this will also select the new group editor.groupShapes(editor.getSelectedShapeIds()) // this will also select the new group
const groupBefore = editor.selectedShapes[0] const groupBefore = editor.getSelectedShapes()[0]
editor.on('change', fn) editor.on('change', fn)
editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal') editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal')

View file

@ -20,8 +20,8 @@ describe('creating frames', () => {
it('can be done', () => { it('can be done', () => {
editor.setCurrentTool('frame') editor.setCurrentTool('frame')
editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200) editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200)
expect(editor.onlySelectedShape?.type).toBe('frame') expect(editor.getOnlySelectedShape()?.type).toBe('frame')
expect(editor.getShapePageBounds(editor.onlySelectedShape!)).toMatchObject({ expect(editor.getShapePageBounds(editor.getOnlySelectedShape()!)).toMatchObject({
x: 100, x: 100,
y: 100, y: 100,
w: 100, w: 100,
@ -31,9 +31,9 @@ describe('creating frames', () => {
it('will create with a default size if no dragging happens', () => { it('will create with a default size if no dragging happens', () => {
editor.setCurrentTool('frame') editor.setCurrentTool('frame')
editor.pointerDown(100, 100).pointerUp(100, 100) editor.pointerDown(100, 100).pointerUp(100, 100)
expect(editor.onlySelectedShape?.type).toBe('frame') expect(editor.getOnlySelectedShape()?.type).toBe('frame')
const { w, h } = editor.getShapeUtil<TLFrameShape>('frame').getDefaultProps() const { w, h } = editor.getShapeUtil<TLFrameShape>('frame').getDefaultProps()
expect(editor.getShapePageBounds(editor.onlySelectedShape!)).toMatchObject({ expect(editor.getShapePageBounds(editor.getOnlySelectedShape()!)).toMatchObject({
x: 100 - w / 2, x: 100 - w / 2,
y: 100 - h / 2, y: 100 - h / 2,
w, w,
@ -43,7 +43,7 @@ describe('creating frames', () => {
it('can be canceled while pointing', () => { it('can be canceled while pointing', () => {
editor.setCurrentTool('frame') editor.setCurrentTool('frame')
editor.pointerDown(100, 100).cancel().pointerUp(100, 100) editor.pointerDown(100, 100).cancel().pointerUp(100, 100)
expect(editor.onlySelectedShape?.type).toBe(undefined) expect(editor.getOnlySelectedShape()?.type).toBe(undefined)
expect(editor.currentPageShapes).toHaveLength(0) expect(editor.currentPageShapes).toHaveLength(0)
}) })
it('can be canceled while dragging', () => { it('can be canceled while dragging', () => {
@ -52,35 +52,35 @@ describe('creating frames', () => {
editor.expectPathToBe('root.select.resizing') editor.expectPathToBe('root.select.resizing')
editor.cancel() editor.cancel()
editor.pointerUp() editor.pointerUp()
expect(editor.onlySelectedShape?.type).toBe(undefined) expect(editor.getOnlySelectedShape()?.type).toBe(undefined)
expect(editor.currentPageShapes).toHaveLength(0) expect(editor.currentPageShapes).toHaveLength(0)
}) })
it('can be undone', () => { it('can be undone', () => {
editor.setCurrentTool('frame') editor.setCurrentTool('frame')
editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200) editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200)
expect(editor.onlySelectedShape?.type).toBe('frame') expect(editor.getOnlySelectedShape()?.type).toBe('frame')
expect(editor.currentPageShapes).toHaveLength(1) expect(editor.currentPageShapes).toHaveLength(1)
editor.undo() editor.undo()
expect(editor.onlySelectedShape?.type).toBe(undefined) expect(editor.getOnlySelectedShape()?.type).toBe(undefined)
expect(editor.currentPageShapes).toHaveLength(0) expect(editor.currentPageShapes).toHaveLength(0)
}) })
it('can be done inside other frames', () => { it('can be done inside other frames', () => {
editor.setCurrentTool('frame') editor.setCurrentTool('frame')
editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200) editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200)
const frameAId = editor.onlySelectedShape!.id const frameAId = editor.getOnlySelectedShape()!.id
editor.setCurrentTool('frame') editor.setCurrentTool('frame')
editor.pointerDown(125, 125).pointerMove(175, 175).pointerUp(175, 175) editor.pointerDown(125, 125).pointerMove(175, 175).pointerUp(175, 175)
expect(editor.currentPageShapes).toHaveLength(2) expect(editor.currentPageShapes).toHaveLength(2)
expect(editor.onlySelectedShape?.parentId).toEqual(frameAId) expect(editor.getOnlySelectedShape()?.parentId).toEqual(frameAId)
expect(editor.getShapePageBounds(editor.onlySelectedShape!)).toMatchObject({ expect(editor.getShapePageBounds(editor.getOnlySelectedShape()!)).toMatchObject({
x: 125, x: 125,
y: 125, y: 125,
w: 50, w: 50,
@ -91,7 +91,7 @@ describe('creating frames', () => {
editor.setCurrentTool('frame') editor.setCurrentTool('frame')
editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200) editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200)
const frameAId = editor.onlySelectedShape!.id const frameAId = editor.getOnlySelectedShape()!.id
editor.rotateSelection(Math.PI / 2) editor.rotateSelection(Math.PI / 2)
@ -100,9 +100,9 @@ describe('creating frames', () => {
expect(editor.currentPageShapes).toHaveLength(2) expect(editor.currentPageShapes).toHaveLength(2)
expect(editor.onlySelectedShape?.parentId).toEqual(frameAId) expect(editor.getOnlySelectedShape()?.parentId).toEqual(frameAId)
expect(editor.getShapePageBounds(editor.onlySelectedShape!)).toCloselyMatchObject({ expect(editor.getShapePageBounds(editor.getOnlySelectedShape()!)).toCloselyMatchObject({
x: 125, x: 125,
y: 125, y: 125,
w: 50, w: 50,
@ -118,7 +118,7 @@ describe('creating frames', () => {
editor.setCurrentTool('frame') editor.setCurrentTool('frame')
editor.pointerDown(100, 100).pointerMove(49, 149) editor.pointerDown(100, 100).pointerMove(49, 149)
expect(editor.getShapePageBounds(editor.onlySelectedShape!)).toMatchObject({ expect(editor.getShapePageBounds(editor.getOnlySelectedShape()!)).toMatchObject({
x: 49, x: 49,
y: 100, y: 100,
w: 51, w: 51,
@ -128,7 +128,7 @@ describe('creating frames', () => {
// x should snap // x should snap
editor.keyDown('Control') editor.keyDown('Control')
expect(editor.snaps.lines).toHaveLength(1) expect(editor.snaps.lines).toHaveLength(1)
expect(editor.getShapePageBounds(editor.onlySelectedShape!)).toMatchObject({ expect(editor.getShapePageBounds(editor.getOnlySelectedShape()!)).toMatchObject({
x: 50, x: 50,
y: 100, y: 100,
w: 50, w: 50,
@ -184,7 +184,7 @@ describe('frame shapes', () => {
editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200) editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200)
editor.resizeSelection({ scaleX: 0.5, scaleY: 0.5 }, 'bottom_right') editor.resizeSelection({ scaleX: 0.5, scaleY: 0.5 }, 'bottom_right')
expect(editor.getShapePageBounds(editor.onlySelectedShape!)).toCloselyMatchObject({ expect(editor.getShapePageBounds(editor.getOnlySelectedShape()!)).toCloselyMatchObject({
x: 100, x: 100,
y: 100, y: 100,
w: 50, w: 50,
@ -198,7 +198,7 @@ describe('frame shapes', () => {
editor.setCurrentTool('frame') editor.setCurrentTool('frame')
editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200) editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200)
editor.resizeSelection({ scaleX: 0.5, scaleY: 0.5 }, 'bottom_right', { altKey: true }) editor.resizeSelection({ scaleX: 0.5, scaleY: 0.5 }, 'bottom_right', { altKey: true })
expect(editor.getShapePageBounds(editor.onlySelectedShape!)).toCloselyMatchObject({ expect(editor.getShapePageBounds(editor.getOnlySelectedShape()!)).toCloselyMatchObject({
x: 125, x: 125,
y: 125, y: 125,
w: 50, w: 50,
@ -210,12 +210,12 @@ describe('frame shapes', () => {
editor.setCurrentTool('frame') editor.setCurrentTool('frame')
editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200) editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200)
const frameId = editor.onlySelectedShape!.id const frameId = editor.getOnlySelectedShape()!.id
editor.setCurrentTool('geo') editor.setCurrentTool('geo')
editor.pointerDown(125, 125).pointerMove(175, 175).pointerUp(175, 175) editor.pointerDown(125, 125).pointerMove(175, 175).pointerUp(175, 175)
const boxId = editor.onlySelectedShape!.id const boxId = editor.getOnlySelectedShape()!.id
editor.select(frameId) editor.select(frameId)
@ -239,42 +239,42 @@ describe('frame shapes', () => {
editor.setCurrentTool('frame') editor.setCurrentTool('frame')
editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200) editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200)
const frameId = editor.onlySelectedShape!.id const frameId = editor.getOnlySelectedShape()!.id
editor.createShapes([ editor.createShapes([
{ type: 'geo', id: ids.boxA, x: 250, y: 250, props: { w: 50, h: 50, fill: 'solid' } }, { type: 'geo', id: ids.boxA, x: 250, y: 250, props: { w: 50, h: 50, fill: 'solid' } },
]) ])
expect(editor.onlySelectedShape!.parentId).toBe(editor.currentPageId) expect(editor.getOnlySelectedShape()!.parentId).toBe(editor.currentPageId)
editor.setCurrentTool('select') editor.setCurrentTool('select')
editor.pointerDown(275, 275).pointerMove(150, 150) editor.pointerDown(275, 275).pointerMove(150, 150)
jest.advanceTimersByTime(300) jest.advanceTimersByTime(300)
expect(editor.onlySelectedShape!.id).toBe(ids.boxA) expect(editor.getOnlySelectedShape()!.id).toBe(ids.boxA)
expect(editor.onlySelectedShape!.parentId).toBe(frameId) expect(editor.getOnlySelectedShape()!.parentId).toBe(frameId)
editor.pointerMove(275, 275) editor.pointerMove(275, 275)
jest.advanceTimersByTime(250) jest.advanceTimersByTime(250)
expect(editor.onlySelectedShape!.parentId).toBe(editor.currentPageId) expect(editor.getOnlySelectedShape()!.parentId).toBe(editor.currentPageId)
editor.pointerMove(150, 150) editor.pointerMove(150, 150)
jest.advanceTimersByTime(250) jest.advanceTimersByTime(250)
expect(editor.onlySelectedShape!.parentId).toBe(frameId) expect(editor.getOnlySelectedShape()!.parentId).toBe(frameId)
editor.pointerUp(150, 150) editor.pointerUp(150, 150)
expect(editor.onlySelectedShape!.parentId).toBe(frameId) expect(editor.getOnlySelectedShape()!.parentId).toBe(frameId)
}) })
it('can have shapes dragged on top and dropped before the timeout fires', () => { it('can have shapes dragged on top and dropped before the timeout fires', () => {
editor.setCurrentTool('frame') editor.setCurrentTool('frame')
editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200) editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200)
const frameId = editor.onlySelectedShape!.id const frameId = editor.getOnlySelectedShape()!.id
// Create a new shape off of the frame // Create a new shape off of the frame
editor.createShapes([ editor.createShapes([
@ -282,37 +282,37 @@ describe('frame shapes', () => {
]) ])
// It should be a child of the page // It should be a child of the page
expect(editor.onlySelectedShape!.parentId).toBe(editor.currentPageId) expect(editor.getOnlySelectedShape()!.parentId).toBe(editor.currentPageId)
// Drag the shape on top of the frame // Drag the shape on top of the frame
editor.setCurrentTool('select') editor.setCurrentTool('select')
editor.pointerDown(275, 275, ids.boxA).pointerMove(150, 150) editor.pointerDown(275, 275, ids.boxA).pointerMove(150, 150)
// The timeout has not fired yet, so the shape is still a child of the current page // The timeout has not fired yet, so the shape is still a child of the current page
expect(editor.onlySelectedShape!.parentId).toBe(editor.currentPageId) expect(editor.getOnlySelectedShape()!.parentId).toBe(editor.currentPageId)
// On pointer up, the shape should be dropped into the frame // On pointer up, the shape should be dropped into the frame
editor.pointerUp() editor.pointerUp()
expect(editor.onlySelectedShape!.parentId).toBe(frameId) expect(editor.getOnlySelectedShape()!.parentId).toBe(frameId)
}) })
it('does not reparent shapes that are being dragged from within the frame', () => { it('does not reparent shapes that are being dragged from within the frame', () => {
editor.setCurrentTool('frame') editor.setCurrentTool('frame')
editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200) editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200)
const frameId = editor.onlySelectedShape!.id const frameId = editor.getOnlySelectedShape()!.id
// create a box within the frame // create a box within the frame
editor.setCurrentTool('geo') editor.setCurrentTool('geo')
editor.pointerDown(125, 125).pointerMove(175, 175).pointerUp(175, 175) editor.pointerDown(125, 125).pointerMove(175, 175).pointerUp(175, 175)
expect(editor.onlySelectedShape!.parentId).toBe(frameId) expect(editor.getOnlySelectedShape()!.parentId).toBe(frameId)
const boxAid = editor.onlySelectedShape!.id const boxAid = editor.getOnlySelectedShape()!.id
// create another box within the frame // create another box within the frame
editor.setCurrentTool('geo') editor.setCurrentTool('geo')
editor.pointerDown(130, 130).pointerMove(180, 180).pointerUp(180, 180) editor.pointerDown(130, 130).pointerMove(180, 180).pointerUp(180, 180)
expect(editor.onlySelectedShape!.parentId).toBe(frameId) expect(editor.getOnlySelectedShape()!.parentId).toBe(frameId)
const boxBid = editor.onlySelectedShape!.id const boxBid = editor.getOnlySelectedShape()!.id
// dragging box A around should not cause the index to change or the frame to be highlighted // dragging box A around should not cause the index to change or the frame to be highlighted
@ -325,8 +325,8 @@ describe('frame shapes', () => {
jest.advanceTimersByTime(2500) jest.advanceTimersByTime(2500)
expect(editor.onlySelectedShape!.id).toBe(boxAid) expect(editor.getOnlySelectedShape()!.id).toBe(boxAid)
expect(editor.onlySelectedShape!.parentId).toBe(frameId) expect(editor.getOnlySelectedShape()!.parentId).toBe(frameId)
expect(editor.hintingShapeIds).toHaveLength(0) expect(editor.hintingShapeIds).toHaveLength(0)
// box A should still be beneath box B // box A should still be beneath box B
expect(editor.getShape(boxAid)!.index.localeCompare(editor.getShape(boxBid)!.index)).toBe(-1) expect(editor.getShape(boxAid)!.index.localeCompare(editor.getShape(boxBid)!.index)).toBe(-1)
@ -355,12 +355,12 @@ describe('frame shapes', () => {
editor.setCurrentTool('geo') editor.setCurrentTool('geo')
editor.pointerDown(125, 125).pointerMove(175, 175).pointerUp(175, 175) editor.pointerDown(125, 125).pointerMove(175, 175).pointerUp(175, 175)
const innerBoxId = editor.onlySelectedShape!.id const innerBoxId = editor.getOnlySelectedShape()!.id
// make a shape outside the frame // make a shape outside the frame
editor.setCurrentTool('geo') editor.setCurrentTool('geo')
editor.pointerDown(275, 125).pointerMove(280, 130).pointerUp(280, 130) editor.pointerDown(275, 125).pointerMove(280, 130).pointerUp(280, 130)
expect(editor.getShapePageBounds(editor.onlySelectedShape!)).toMatchObject({ expect(editor.getShapePageBounds(editor.getOnlySelectedShape()!)).toMatchObject({
x: 275, x: 275,
y: 125, y: 125,
w: 5, w: 5,
@ -370,13 +370,13 @@ describe('frame shapes', () => {
// drag it a pixel up, it should not snap even though it's at the same y as the box inside the frame // drag it a pixel up, it should not snap even though it's at the same y as the box inside the frame
editor.setCurrentTool('select') editor.setCurrentTool('select')
editor editor
.pointerDown(277.5, 127.5, editor.onlySelectedShape!.id) .pointerDown(277.5, 127.5, editor.getOnlySelectedShape()!.id)
.pointerMove(287.5, 126.5) .pointerMove(287.5, 126.5)
.pointerMove(277.5, 126.5) .pointerMove(277.5, 126.5)
// now try to snap // now try to snap
editor.keyDown('Control') editor.keyDown('Control')
expect(editor.getShapePageBounds(editor.onlySelectedShape!)).toMatchObject({ expect(editor.getShapePageBounds(editor.getOnlySelectedShape()!)).toMatchObject({
x: 275, x: 275,
y: 124, y: 124,
w: 5, w: 5,
@ -388,7 +388,7 @@ describe('frame shapes', () => {
editor.pointerMove(287.5, 126.5).pointerMove(277.5, 126.5) editor.pointerMove(287.5, 126.5).pointerMove(277.5, 126.5)
expect(editor.snaps.lines).toHaveLength(1) expect(editor.snaps.lines).toHaveLength(1)
expect(editor.getShapePageBounds(editor.onlySelectedShape!)).toMatchObject({ expect(editor.getShapePageBounds(editor.getOnlySelectedShape()!)).toMatchObject({
x: 275, x: 275,
y: 125, y: 125,
w: 5, w: 5,
@ -399,17 +399,17 @@ describe('frame shapes', () => {
it('children of a frame will not snap to shapes outside the frame', () => { it('children of a frame will not snap to shapes outside the frame', () => {
editor.setCurrentTool('frame') editor.setCurrentTool('frame')
editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200) editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200)
const frameId = editor.onlySelectedShape!.id const frameId = editor.getOnlySelectedShape()!.id
// make a shape inside the frame // make a shape inside the frame
editor.setCurrentTool('geo') editor.setCurrentTool('geo')
editor.pointerDown(125, 125).pointerMove(175, 175).pointerUp(175, 175) editor.pointerDown(125, 125).pointerMove(175, 175).pointerUp(175, 175)
const innerBoxId = editor.onlySelectedShape!.id const innerBoxId = editor.getOnlySelectedShape()!.id
// make a shape outside the frame // make a shape outside the frame
editor.setCurrentTool('geo') editor.setCurrentTool('geo')
editor.pointerDown(275, 125).pointerMove(280, 130).pointerUp(280, 130) editor.pointerDown(275, 125).pointerMove(280, 130).pointerUp(280, 130)
const outerBoxId = editor.onlySelectedShape!.id const outerBoxId = editor.getOnlySelectedShape()!.id
editor.setCurrentTool('select') editor.setCurrentTool('select')
editor.pointerDown(150, 150, innerBoxId).pointerMove(150, 50).pointerMove(150, 148) editor.pointerDown(150, 150, innerBoxId).pointerMove(150, 50).pointerMove(150, 148)
@ -429,7 +429,7 @@ describe('frame shapes', () => {
editor.setCurrentTool('geo') editor.setCurrentTool('geo')
editor.pointerDown(150, 150).pointerMove(250, 250).pointerUp(250, 250) editor.pointerDown(150, 150).pointerMove(250, 250).pointerUp(250, 250)
expect(editor.getShapePageBounds(editor.onlySelectedShape!)).toMatchObject({ expect(editor.getShapePageBounds(editor.getOnlySelectedShape()!)).toMatchObject({
x: 150, x: 150,
y: 150, y: 150,
w: 100, w: 100,
@ -437,13 +437,13 @@ describe('frame shapes', () => {
}) })
// mask should be a 50px box around the top left corner // mask should be a 50px box around the top left corner
expect(editor.getShapeClipPath(editor.onlySelectedShape!.id)).toMatchInlineSnapshot( expect(editor.getShapeClipPath(editor.getOnlySelectedShape()!.id)).toMatchInlineSnapshot(
`"polygon(-50px -50px,50px -50px,50px 50px,-50px 50px)"` `"polygon(-50px -50px,50px -50px,50px 50px,-50px 50px)"`
) )
editor.reparentShapes([editor.onlySelectedShape!.id], editor.currentPageId) editor.reparentShapes([editor.getOnlySelectedShape()!.id], editor.currentPageId)
expect(editor.getShapeClipPath(editor.onlySelectedShape!.id)).toBeUndefined() expect(editor.getShapeClipPath(editor.getOnlySelectedShape()!.id)).toBeUndefined()
}) })
it('masks its nested children', () => { it('masks its nested children', () => {
@ -453,12 +453,12 @@ describe('frame shapes', () => {
editor.setCurrentTool('frame') editor.setCurrentTool('frame')
editor.pointerDown(150, 150).pointerMove(250, 250).pointerUp(250, 250) editor.pointerDown(150, 150).pointerMove(250, 250).pointerUp(250, 250)
const innerFrameId = editor.onlySelectedShape!.id const innerFrameId = editor.getOnlySelectedShape()!.id
editor.setCurrentTool('geo') editor.setCurrentTool('geo')
editor.pointerDown(100, 100).pointerMove(250, 250).pointerUp(250, 250) editor.pointerDown(100, 100).pointerMove(250, 250).pointerUp(250, 250)
const boxId = editor.onlySelectedShape!.id const boxId = editor.getOnlySelectedShape()!.id
editor.reparentShapes([boxId], innerFrameId) editor.reparentShapes([boxId], innerFrameId)
@ -471,12 +471,12 @@ describe('frame shapes', () => {
it('arrows started within the frame will bind to it and have the page as their parent', () => { it('arrows started within the frame will bind to it and have the page as their parent', () => {
editor.setCurrentTool('frame') editor.setCurrentTool('frame')
editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200) editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200)
const frameId = editor.onlySelectedShape!.id const frameId = editor.getOnlySelectedShape()!.id
editor.setCurrentTool('arrow') editor.setCurrentTool('arrow')
editor.pointerDown(150, 150).pointerMove(250, 250).pointerUp(250, 250) editor.pointerDown(150, 150).pointerMove(250, 250).pointerUp(250, 250)
const arrow = editor.onlySelectedShape! as TLArrowShape const arrow = editor.getOnlySelectedShape()! as TLArrowShape
expect(arrow.props.start).toMatchObject({ boundShapeId: frameId }) expect(arrow.props.start).toMatchObject({ boundShapeId: frameId })
expect(arrow.props.end).toMatchObject({ type: 'point' }) expect(arrow.props.end).toMatchObject({ type: 'point' })
@ -487,7 +487,7 @@ describe('frame shapes', () => {
it('arrows started within the frame can bind to a shape within the frame ', () => { it('arrows started within the frame can bind to a shape within the frame ', () => {
editor.setCurrentTool('frame') editor.setCurrentTool('frame')
editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200) editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200)
const frameId = editor.onlySelectedShape!.id const frameId = editor.getOnlySelectedShape()!.id
editor.setCurrentTool('geo') editor.setCurrentTool('geo')
editor editor
@ -496,12 +496,12 @@ describe('frame shapes', () => {
.pointerUp(175, 175) .pointerUp(175, 175)
.setStyleForSelectedShapes(DefaultFillStyle, 'solid') .setStyleForSelectedShapes(DefaultFillStyle, 'solid')
.setStyleForNextShapes(DefaultFillStyle, 'solid') .setStyleForNextShapes(DefaultFillStyle, 'solid')
const boxId = editor.onlySelectedShape!.id const boxId = editor.getOnlySelectedShape()!.id
editor.setCurrentTool('arrow') editor.setCurrentTool('arrow')
editor.pointerDown(150, 150).pointerMove(190, 190).pointerUp(190, 190) editor.pointerDown(150, 150).pointerMove(190, 190).pointerUp(190, 190)
const arrow = editor.onlySelectedShape! as TLArrowShape const arrow = editor.getOnlySelectedShape()! as TLArrowShape
expect(arrow.props.start).toMatchObject({ boundShapeId: boxId }) expect(arrow.props.start).toMatchObject({ boundShapeId: boxId })
expect(arrow.props.end).toMatchObject({ boundShapeId: frameId }) expect(arrow.props.end).toMatchObject({ boundShapeId: frameId })
@ -513,7 +513,7 @@ describe('frame shapes', () => {
editor.setCurrentTool('frame') editor.setCurrentTool('frame')
editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200) editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200)
const frameId = editor.onlySelectedShape!.id const frameId = editor.getOnlySelectedShape()!.id
expect(editor.getSelectedShapeIds()[0]).toBe(frameId) expect(editor.getSelectedShapeIds()[0]).toBe(frameId)
expect(editor.getCurrentPageState().editingShapeId).toBe(null) expect(editor.getCurrentPageState().editingShapeId).toBe(null)
@ -529,7 +529,7 @@ describe('frame shapes', () => {
it('can be selected with box brushing only if the whole frame is selected', () => { it('can be selected with box brushing only if the whole frame is selected', () => {
editor.setCurrentTool('frame') editor.setCurrentTool('frame')
editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200) editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200)
const frameId = editor.onlySelectedShape!.id const frameId = editor.getOnlySelectedShape()!.id
// select from outside the frame // select from outside the frame
editor.setCurrentTool('select') editor.setCurrentTool('select')
@ -541,7 +541,7 @@ describe('frame shapes', () => {
editor.pointerMove(250, 250) editor.pointerMove(250, 250)
expect(editor.getSelectedShapeIds()).toHaveLength(1) expect(editor.getSelectedShapeIds()).toHaveLength(1)
expect(editor.onlySelectedShape!.id).toBe(frameId) expect(editor.getOnlySelectedShape()!.id).toBe(frameId)
}) })
it('can be selected with scribble brushing only if the drag starts outside the frame', () => { it('can be selected with scribble brushing only if the drag starts outside the frame', () => {
@ -561,12 +561,12 @@ describe('frame shapes', () => {
it('children of a frame will not be selected from outside of the frame', () => { it('children of a frame will not be selected from outside of the frame', () => {
editor.setCurrentTool('frame') editor.setCurrentTool('frame')
editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200) editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200)
editor.onlySelectedShape!.id editor.getOnlySelectedShape()!.id
// make a shape inside the frame that extends out of the frame // make a shape inside the frame that extends out of the frame
editor.setCurrentTool('geo') editor.setCurrentTool('geo')
editor.pointerDown(150, 150).pointerMove(400, 400).pointerUp(400, 400) editor.pointerDown(150, 150).pointerMove(400, 400).pointerUp(400, 400)
const innerBoxId = editor.onlySelectedShape!.id const innerBoxId = editor.getOnlySelectedShape()!.id
// select from outside the frame via box brushing // select from outside the frame via box brushing
editor.setCurrentTool('select') editor.setCurrentTool('select')
@ -581,7 +581,7 @@ describe('frame shapes', () => {
// Check if the inner box was selected // Check if the inner box was selected
expect(editor.getSelectedShapeIds()).toHaveLength(1) expect(editor.getSelectedShapeIds()).toHaveLength(1)
expect(editor.onlySelectedShape!.id).toBe(innerBoxId) expect(editor.getOnlySelectedShape()!.id).toBe(innerBoxId)
// Deselect everything // Deselect everything
editor.deselect() editor.deselect()
@ -600,7 +600,7 @@ describe('frame shapes', () => {
it('arrows will not bind to parts of shapes outside the frame', () => { it('arrows will not bind to parts of shapes outside the frame', () => {
editor.setCurrentTool('frame') editor.setCurrentTool('frame')
editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200) editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200)
editor.onlySelectedShape!.id editor.getOnlySelectedShape()!.id
// make a shape inside the frame that extends out of the frame // make a shape inside the frame that extends out of the frame
editor.setCurrentTool('geo') editor.setCurrentTool('geo')
@ -610,14 +610,14 @@ describe('frame shapes', () => {
.pointerUp(400, 400) .pointerUp(400, 400)
.setStyleForSelectedShapes(DefaultFillStyle, 'solid') .setStyleForSelectedShapes(DefaultFillStyle, 'solid')
.setStyleForNextShapes(DefaultFillStyle, 'solid') .setStyleForNextShapes(DefaultFillStyle, 'solid')
const innerBoxId = editor.onlySelectedShape!.id const innerBoxId = editor.getOnlySelectedShape()!.id
// Make an arrow that binds to the inner box's bottom right corner // Make an arrow that binds to the inner box's bottom right corner
editor.setCurrentTool('arrow') editor.setCurrentTool('arrow')
editor.pointerDown(500, 500).pointerMove(375, 375) editor.pointerDown(500, 500).pointerMove(375, 375)
// Check if the arrow's handles remain points // Check if the arrow's handles remain points
let arrow = editor.onlySelectedShape! as TLArrowShape let arrow = editor.getOnlySelectedShape()! as TLArrowShape
expect(arrow.props.start).toMatchObject({ expect(arrow.props.start).toMatchObject({
type: 'point', type: 'point',
x: 0, x: 0,
@ -633,7 +633,7 @@ describe('frame shapes', () => {
editor.pointerMove(175, 175).pointerUp(175, 175) editor.pointerMove(175, 175).pointerUp(175, 175)
// Check if arrow's end handle is bound to the inner box // Check if arrow's end handle is bound to the inner box
arrow = editor.onlySelectedShape! as TLArrowShape arrow = editor.getOnlySelectedShape()! as TLArrowShape
expect(arrow.props.end).toMatchObject({ boundShapeId: innerBoxId }) expect(arrow.props.end).toMatchObject({ boundShapeId: innerBoxId })
}) })
}) })
@ -655,7 +655,7 @@ test('arrows bound to a shape within a group within a frame are reparented if th
// └──────────────────────────┘ // └──────────────────────────┘
editor.setCurrentTool('frame') editor.setCurrentTool('frame')
editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200) editor.pointerDown(100, 100).pointerMove(200, 200).pointerUp(200, 200)
const frameId = editor.onlySelectedShape!.id const frameId = editor.getOnlySelectedShape()!.id
editor.setCurrentTool('geo') editor.setCurrentTool('geo')
editor editor
@ -664,7 +664,7 @@ test('arrows bound to a shape within a group within a frame are reparented if th
.pointerUp(120, 120) .pointerUp(120, 120)
.setStyleForSelectedShapes(DefaultFillStyle, 'solid') .setStyleForSelectedShapes(DefaultFillStyle, 'solid')
.setStyleForNextShapes(DefaultFillStyle, 'solid') .setStyleForNextShapes(DefaultFillStyle, 'solid')
const boxAId = editor.onlySelectedShape!.id const boxAId = editor.getOnlySelectedShape()!.id
editor.setCurrentTool('geo') editor.setCurrentTool('geo')
editor editor
@ -673,7 +673,7 @@ test('arrows bound to a shape within a group within a frame are reparented if th
.pointerUp(190, 120) .pointerUp(190, 120)
.setStyleForSelectedShapes(DefaultFillStyle, 'solid') .setStyleForSelectedShapes(DefaultFillStyle, 'solid')
.setStyleForNextShapes(DefaultFillStyle, 'solid') .setStyleForNextShapes(DefaultFillStyle, 'solid')
const boxBId = editor.onlySelectedShape!.id const boxBId = editor.getOnlySelectedShape()!.id
editor.setCurrentTool('geo') editor.setCurrentTool('geo')
editor editor
@ -682,16 +682,16 @@ test('arrows bound to a shape within a group within a frame are reparented if th
.pointerUp(170, 170) .pointerUp(170, 170)
.setStyleForSelectedShapes(DefaultFillStyle, 'solid') .setStyleForSelectedShapes(DefaultFillStyle, 'solid')
.setStyleForNextShapes(DefaultFillStyle, 'solid') .setStyleForNextShapes(DefaultFillStyle, 'solid')
const boxCId = editor.onlySelectedShape!.id const boxCId = editor.getOnlySelectedShape()!.id
editor.setCurrentTool('select') editor.setCurrentTool('select')
editor.select(boxBId, boxCId) editor.select(boxBId, boxCId)
editor.groupShapes(editor.getSelectedShapeIds()) editor.groupShapes(editor.getSelectedShapeIds())
const groupId = editor.onlySelectedShape!.id const groupId = editor.getOnlySelectedShape()!.id
editor.setCurrentTool('arrow') editor.setCurrentTool('arrow')
editor.pointerDown(115, 115).pointerMove(185, 115).pointerUp(185, 115) editor.pointerDown(115, 115).pointerMove(185, 115).pointerUp(185, 115)
const arrowId = editor.onlySelectedShape!.id const arrowId = editor.getOnlySelectedShape()!.id
expect(editor.getArrowsBoundTo(boxAId)).toHaveLength(1) expect(editor.getArrowsBoundTo(boxAId)).toHaveLength(1)
expect(editor.getArrowsBoundTo(boxBId)).toHaveLength(1) expect(editor.getArrowsBoundTo(boxBId)).toHaveLength(1)
@ -740,7 +740,7 @@ describe('When dragging a shape inside a group inside a frame', () => {
editor.pointerMove(100, 100).click().click() editor.pointerMove(100, 100).click().click()
expect(editor.onlySelectedShape?.id).toBe(ids.box1) expect(editor.getOnlySelectedShape()?.id).toBe(ids.box1)
editor.pointerMove(150, 150).pointerDown().pointerMove(140, 140) editor.pointerMove(150, 150).pointerDown().pointerMove(140, 140)
@ -760,7 +760,7 @@ describe('When dragging a shape inside a group inside a frame', () => {
editor.pointerMove(100, 100).click().click() editor.pointerMove(100, 100).click().click()
expect(editor.onlySelectedShape?.id).toBe(ids.box1) expect(editor.getOnlySelectedShape()?.id).toBe(ids.box1)
expect(editor.focusedGroupId).toBe(ids.group1) expect(editor.focusedGroupId).toBe(ids.group1)
editor editor

View file

@ -475,9 +475,9 @@ describe('When pasting into frames...', () => {
editor.setCamera({ x: -editor.viewportScreenBounds.w, y: -editor.viewportScreenBounds.h, z: 1 }) editor.setCamera({ x: -editor.viewportScreenBounds.w, y: -editor.viewportScreenBounds.h, z: 1 })
// paste the box // paste the box
editor.paste() editor.paste()
const boxId = editor.onlySelectedShape!.id const boxId = editor.getOnlySelectedShape()!.id
// it should be a child of the frame // it should be a child of the frame
expect(editor.onlySelectedShape?.parentId).toBe(ids.frame1) expect(editor.getOnlySelectedShape()?.parentId).toBe(ids.frame1)
// it should have pageBounds of 10x10 because it is not rotated relative to the viewport // it should have pageBounds of 10x10 because it is not rotated relative to the viewport
expect(editor.getShapePageBounds(boxId)).toMatchObject({ w: 10, h: 10 }) expect(editor.getShapePageBounds(boxId)).toMatchObject({ w: 10, h: 10 })
// it should be in the middle of the frame // it should be in the middle of the frame

View file

@ -15,7 +15,7 @@ describe(SelectTool, () => {
describe('pointer down while shape is being edited', () => { describe('pointer down while shape is being edited', () => {
it('captures the pointer down event if it is on the shape', () => { it('captures the pointer down event if it is on the shape', () => {
editor.setCurrentTool('geo').pointerDown(0, 0).pointerMove(100, 100).pointerUp(100, 100) editor.setCurrentTool('geo').pointerDown(0, 0).pointerMove(100, 100).pointerUp(100, 100)
const shapeId = editor.onlySelectedShape?.id const shapeId = editor.getOnlySelectedShape()?.id
editor._transformPointerDownSpy.mockRestore() editor._transformPointerDownSpy.mockRestore()
editor._transformPointerUpSpy.mockRestore() editor._transformPointerUpSpy.mockRestore()
editor.setCurrentTool('select') editor.setCurrentTool('select')
@ -42,7 +42,7 @@ describe(SelectTool, () => {
}) })
it('does not allow pressing undo to end up in the editing state', () => { it('does not allow pressing undo to end up in the editing state', () => {
editor.setCurrentTool('geo').pointerDown(0, 0).pointerMove(100, 100).pointerUp(100, 100) editor.setCurrentTool('geo').pointerDown(0, 0).pointerMove(100, 100).pointerUp(100, 100)
const shapeId = editor.onlySelectedShape?.id const shapeId = editor.getOnlySelectedShape()?.id
editor._transformPointerDownSpy.mockRestore() editor._transformPointerDownSpy.mockRestore()
editor._transformPointerUpSpy.mockRestore() editor._transformPointerUpSpy.mockRestore()
editor.setCurrentTool('select') editor.setCurrentTool('select')

View file

@ -75,9 +75,9 @@ describe('Hovering shapes', () => {
editor.pointerMove(50, 50) editor.pointerMove(50, 50)
editor.pointerDown() editor.pointerDown()
expect(editor.isIn('select.pointing_shape')).toBe(true) expect(editor.isIn('select.pointing_shape')).toBe(true)
expect(editor.selectedShapes.length).toBe(1) expect(editor.getSelectedShapes().length).toBe(1)
editor.pointerUp() editor.pointerUp()
expect(editor.selectedShapes.length).toBe(1) expect(editor.getSelectedShapes().length).toBe(1)
expect(editor.isIn('select.idle')).toBe(true) expect(editor.isIn('select.idle')).toBe(true)
}) })
@ -85,10 +85,10 @@ describe('Hovering shapes', () => {
editor.pointerMove(50, 50) editor.pointerMove(50, 50)
editor.pointerDown() editor.pointerDown()
expect(editor.isIn('select.pointing_canvas')).toBe(true) expect(editor.isIn('select.pointing_canvas')).toBe(true)
expect(editor.selectedShapes.length).toBe(0) expect(editor.getSelectedShapes().length).toBe(0)
editor.pointerUp() editor.pointerUp()
expect(editor.isIn('select.idle')).toBe(true) expect(editor.isIn('select.idle')).toBe(true)
expect(editor.selectedShapes.length).toBe(1) expect(editor.getSelectedShapes().length).toBe(1)
}) })
it('hovers the margins or inside of filled shapes', () => { it('hovers the margins or inside of filled shapes', () => {

View file

@ -182,7 +182,7 @@ describe('When cloning...', () => {
// Start cloning! // Start cloning!
editor.keyDown('Alt') editor.keyDown('Alt')
expect(editor.currentPageShapeIds.size).toBe(4) expect(editor.currentPageShapeIds.size).toBe(4)
const newShape = editor.selectedShapes[0] const newShape = editor.getSelectedShapes()[0]
expect(newShape.id).not.toBe(ids.box1) expect(newShape.id).not.toBe(ids.box1)
editor editor
@ -1792,19 +1792,19 @@ it('clones a single shape simply', () => {
.pointerMove(50, 50) .pointerMove(50, 50)
.click() .click()
expect(editor.onlySelectedShape).toBe(editor.currentPageShapes[0]) expect(editor.getOnlySelectedShape()).toBe(editor.currentPageShapes[0])
expect(editor.hoveredShape).toBe(editor.currentPageShapes[0]) expect(editor.hoveredShape).toBe(editor.currentPageShapes[0])
// click on the canvas to deselect // click on the canvas to deselect
editor.pointerMove(200, 50).click() editor.pointerMove(200, 50).click()
expect(editor.onlySelectedShape).toBe(null) expect(editor.getOnlySelectedShape()).toBe(null)
expect(editor.hoveredShape).toBe(undefined) expect(editor.hoveredShape).toBe(undefined)
// move back over the the shape // move back over the the shape
editor.pointerMove(50, 50) editor.pointerMove(50, 50)
expect(editor.onlySelectedShape).toBe(null) expect(editor.getOnlySelectedShape()).toBe(null)
expect(editor.hoveredShape).toBe(editor.currentPageShapes[0]) expect(editor.hoveredShape).toBe(editor.currentPageShapes[0])
// start dragging the shape // start dragging the shape
@ -1818,7 +1818,7 @@ it('clones a single shape simply', () => {
expect(editor.currentPageShapes).toHaveLength(2) expect(editor.currentPageShapes).toHaveLength(2)
const [, sticky2] = editor.currentPageShapes const [, sticky2] = editor.currentPageShapes
expect(editor.onlySelectedShape).toBe(sticky2) expect(editor.getOnlySelectedShape()).toBe(sticky2)
expect(editor.editingShape).toBe(undefined) expect(editor.editingShape).toBe(undefined)
expect(editor.hoveredShape).toBe(sticky2) expect(editor.hoveredShape).toBe(sticky2)
}) })