Merge branch 'main' into resolve-#75

This commit is contained in:
Steve Ruiz 2021-09-06 12:50:12 +01:00
commit 146eb87fb1
26 changed files with 390 additions and 219 deletions

View file

@ -3,6 +3,8 @@
"include": ["src"],
"exclude": ["node_modules", "dist", "docs"],
"compilerOptions": {
"composite": true,
"emitDeclarationOnly": true,
"rootDir": "src",
"outDir": "./dist/types",
"baseUrl": "src",

View file

@ -18,7 +18,7 @@
"src"
],
"dependencies": {
"@tldraw/tldraw": "^0.0.64",
"@tldraw/tldraw": "*",
"idb": "^6.1.2",
"react": "^17.0.2",
"react-dom": "^17.0.2"
@ -34,4 +34,4 @@
"typescript": "4.2.3"
},
"gitHead": "a7dac0f83ad998e205c2aab58182cb4ba4e099a6"
}
}

View file

@ -1,5 +1,5 @@
import * as React from 'react'
import { TLDraw, TLDrawState } from '@tldraw/tldraw'
import { TLDraw, TLDrawShapeType, TLDrawState } from '@tldraw/tldraw'
export default function Editor(): JSX.Element {
const rTLDrawState = React.useRef<TLDrawState>()

View file

@ -3,8 +3,12 @@
"include": ["src"],
"exclude": ["node_modules", "**/*.test.ts", "**/*.spec.ts", "dist"],
"compilerOptions": {
"rootDir": "src",
"outDir": "./dist/types",
"baseUrl": "src"
}
"composite": true
},
"references": [
{
"path": "../../packages/tldraw"
},
{ "path": "../../packages/core" }
]
}

View file

@ -67,6 +67,6 @@
"@tldraw/core": "^0.0.79",
"perfect-freehand": "^0.5.3",
"react-hotkeys-hook": "^3.4.0",
"rko": "^0.5.20"
"rko": "^0.5.22"
}
}
}

View file

@ -5,69 +5,73 @@ import { AlignType } from '~types'
describe('Align command', () => {
const tlstate = new TLDrawState()
it('does, undoes and redoes command', () => {
tlstate.loadDocument(mockDocument)
tlstate.selectAll()
tlstate.align(AlignType.Top)
describe('when less than two shapes are selected', () => {
it('does nothing', () => {
tlstate.loadDocument(mockDocument).select('rect2')
const initialState = tlstate.state
tlstate.align(AlignType.Top)
const currentState = tlstate.state
expect(tlstate.getShape('rect2').point).toEqual([100, 0])
tlstate.undo()
expect(tlstate.getShape('rect2').point).toEqual([100, 100])
tlstate.redo()
expect(tlstate.getShape('rect2').point).toEqual([100, 0])
expect(currentState).toEqual(initialState)
})
})
it('aligns top', () => {
tlstate.loadDocument(mockDocument)
tlstate.selectAll()
tlstate.align(AlignType.Top)
describe('when multiple shapes are selected', () => {
beforeEach(() => {
tlstate.loadDocument(mockDocument)
tlstate.selectAll()
})
expect(tlstate.getShape('rect2').point).toEqual([100, 0])
})
it('does, undoes and redoes command', () => {
tlstate.align(AlignType.Top)
it('aligns right', () => {
tlstate.loadDocument(mockDocument)
tlstate.selectAll()
tlstate.align(AlignType.Right)
expect(tlstate.getShape('rect2').point).toEqual([100, 0])
expect(tlstate.getShape('rect1').point).toEqual([100, 0])
})
tlstate.undo()
it('aligns bottom', () => {
tlstate.loadDocument(mockDocument)
tlstate.selectAll()
tlstate.align(AlignType.Bottom)
expect(tlstate.getShape('rect2').point).toEqual([100, 100])
expect(tlstate.getShape('rect1').point).toEqual([0, 100])
})
tlstate.redo()
it('aligns left', () => {
tlstate.loadDocument(mockDocument)
tlstate.selectAll()
tlstate.align(AlignType.Left)
expect(tlstate.getShape('rect2').point).toEqual([100, 0])
})
expect(tlstate.getShape('rect2').point).toEqual([0, 100])
})
it('aligns top', () => {
tlstate.align(AlignType.Top)
it('aligns center horizontal', () => {
tlstate.loadDocument(mockDocument)
tlstate.selectAll()
tlstate.align(AlignType.CenterHorizontal)
expect(tlstate.getShape('rect2').point).toEqual([100, 0])
})
expect(tlstate.getShape('rect1').point).toEqual([50, 0])
expect(tlstate.getShape('rect2').point).toEqual([50, 100])
})
it('aligns right', () => {
tlstate.align(AlignType.Right)
it('aligns center vertical', () => {
tlstate.loadDocument(mockDocument)
tlstate.selectAll()
tlstate.align(AlignType.CenterVertical)
expect(tlstate.getShape('rect1').point).toEqual([100, 0])
})
expect(tlstate.getShape('rect1').point).toEqual([0, 50])
expect(tlstate.getShape('rect2').point).toEqual([100, 50])
it('aligns bottom', () => {
tlstate.align(AlignType.Bottom)
expect(tlstate.getShape('rect1').point).toEqual([0, 100])
})
it('aligns left', () => {
tlstate.align(AlignType.Left)
expect(tlstate.getShape('rect2').point).toEqual([0, 100])
})
it('aligns center horizontal', () => {
tlstate.align(AlignType.CenterHorizontal)
expect(tlstate.getShape('rect1').point).toEqual([50, 0])
expect(tlstate.getShape('rect2').point).toEqual([50, 100])
})
it('aligns center vertical', () => {
tlstate.align(AlignType.CenterVertical)
expect(tlstate.getShape('rect1').point).toEqual([0, 50])
expect(tlstate.getShape('rect2').point).toEqual([100, 50])
})
})
})

View file

@ -4,8 +4,22 @@ import { mockDocument } from '~test'
describe('Create command', () => {
const tlstate = new TLDrawState()
it('does, undoes and redoes command', () => {
beforeEach(() => {
tlstate.loadDocument(mockDocument)
})
describe('when no shape is provided', () => {
it('does nothing', () => {
const initialState = tlstate.state
tlstate.create()
const currentState = tlstate.state
expect(currentState).toEqual(initialState)
})
})
it('does, undoes and redoes command', () => {
const shape = { ...tlstate.getShape('rect1'), id: 'rect4' }
tlstate.create(shape)

View file

@ -4,9 +4,22 @@ import { mockDocument } from '~test'
describe('Delete page', () => {
const tlstate = new TLDrawState()
it('does, undoes and redoes command', () => {
beforeEach(() => {
tlstate.loadDocument(mockDocument)
})
describe('when there are no pages in the current document', () => {
it('does nothing', () => {
tlstate.resetDocument()
const initialState = tlstate.state
tlstate.deletePage('page1')
const currentState = tlstate.state
expect(currentState).toEqual(initialState)
})
})
it('does, undoes and redoes command', () => {
const initialId = tlstate.currentPageId
tlstate.createPage()

View file

@ -6,8 +6,21 @@ import type { TLDrawShape } from '~types'
describe('Delete command', () => {
const tlstate = new TLDrawState()
it('does, undoes and redoes command', () => {
beforeEach(() => {
tlstate.loadDocument(mockDocument)
})
describe('when no shape is selected', () => {
it('does nothing', () => {
const initialState = tlstate.state
tlstate.delete()
const currentState = tlstate.state
expect(currentState).toEqual(initialState)
})
})
it('does, undoes and redoes command', () => {
tlstate.select('rect2')
tlstate.delete()
@ -26,7 +39,6 @@ describe('Delete command', () => {
})
it('deletes two shapes', () => {
tlstate.loadDocument(mockDocument)
tlstate.selectAll()
tlstate.delete()
@ -45,8 +57,6 @@ describe('Delete command', () => {
})
it('deletes bound shapes', () => {
tlstate.loadDocument(mockDocument)
expect(Object.values(tlstate.page.bindings)[0]).toBe(undefined)
tlstate
@ -86,26 +96,18 @@ describe('Delete command', () => {
expect(tlstate.getShape('arrow1').handles?.start.bindingId).toBe(undefined)
})
describe('when deleting grouped shapes', () => {
describe('when deleting shapes in a group', () => {
it('updates the group', () => {
tlstate
.loadDocument(mockDocument)
.group(['rect1', 'rect2', 'rect3'], 'newGroup')
.select('rect1')
.delete()
tlstate.group(['rect1', 'rect2', 'rect3'], 'newGroup').select('rect1').delete()
expect(tlstate.getShape('rect1')).toBeUndefined()
expect(tlstate.getShape('newGroup').children).toStrictEqual(['rect2', 'rect3'])
})
})
describe('when deleting shapes with children', () => {
it('also deletes the children', () => {
tlstate
.loadDocument(mockDocument)
.group(['rect1', 'rect2'], 'newGroup')
.select('newGroup')
.delete()
describe('when deleting a group', () => {
it('deletes all grouped shapes', () => {
tlstate.group(['rect1', 'rect2'], 'newGroup').select('newGroup').delete()
expect(tlstate.getShape('rect1')).toBeUndefined()
expect(tlstate.getShape('rect2')).toBeUndefined()

View file

@ -4,11 +4,26 @@ import { DistributeType } from '~types'
describe('Distribute command', () => {
const tlstate = new TLDrawState()
tlstate.loadDocument(mockDocument)
tlstate.selectAll()
beforeEach(() => {
tlstate.loadDocument(mockDocument)
})
describe('when less than three shapes are selected', () => {
it('does nothing', () => {
tlstate.select('rect1', 'rect2')
const initialState = tlstate.state
tlstate.distribute(DistributeType.Horizontal)
const currentState = tlstate.state
expect(currentState).toEqual(initialState)
})
})
it('does, undoes and redoes command', () => {
tlstate.selectAll()
tlstate.distribute(DistributeType.Horizontal)
expect(tlstate.getShape('rect3').point).toEqual([50, 20])
tlstate.undo()
expect(tlstate.getShape('rect3').point).toEqual([20, 20])
@ -17,9 +32,9 @@ describe('Distribute command', () => {
})
it('distributes vertically', () => {
tlstate.loadDocument(mockDocument)
tlstate.selectAll()
tlstate.distribute(DistributeType.Vertical)
expect(tlstate.getShape('rect3').point).toEqual([20, 50])
})
})

View file

@ -1,14 +1,28 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { TLDrawState } from '~state'
import { mockDocument } from '~test'
import { ArrowShape, GroupShape, TLDrawShapeType } from '~types'
import { ArrowShape, TLDrawShapeType } from '~types'
describe('Duplicate command', () => {
const tlstate = new TLDrawState()
tlstate.loadDocument(mockDocument)
tlstate.select('rect1')
beforeEach(() => {
tlstate.loadDocument(mockDocument)
})
describe('when no shape is selected', () => {
it('does nothing', () => {
const initialState = tlstate.state
tlstate.duplicate()
const currentState = tlstate.state
expect(currentState).toEqual(initialState)
})
})
it('does, undoes and redoes command', () => {
tlstate.select('rect1')
expect(Object.keys(tlstate.getPage().shapes).length).toBe(3)
tlstate.duplicate()
@ -121,7 +135,6 @@ describe('Duplicate command', () => {
})
it('duplicates groups', () => {
tlstate.loadDocument(mockDocument)
tlstate.group(['rect1', 'rect2'], 'newGroup').select('newGroup')
const beforeShapeIds = Object.keys(tlstate.page.shapes)
@ -140,7 +153,6 @@ describe('Duplicate command', () => {
})
it('duplicates grouped shapes', () => {
tlstate.loadDocument(mockDocument)
tlstate.group(['rect1', 'rect2'], 'newGroup').select('rect1')
const beforeShapeIds = Object.keys(tlstate.page.shapes)

View file

@ -9,6 +9,16 @@ describe('Flip command', () => {
tlstate.loadDocument(mockDocument)
})
describe('when no shape is selected', () => {
it('does nothing', () => {
const initialState = tlstate.state
tlstate.flipHorizontal()
const currentState = tlstate.state
expect(currentState).toEqual(initialState)
})
})
it('does, undoes and redoes command', () => {
tlstate.select('rect1', 'rect2')
tlstate.flipHorizontal()

View file

@ -6,8 +6,11 @@ import { GroupShape, TLDrawShape, TLDrawShapeType } from '~types'
describe('Group command', () => {
const tlstate = new TLDrawState()
it('does, undoes and redoes command', () => {
beforeEach(() => {
tlstate.loadDocument(mockDocument)
})
it('does, undoes and redoes command', () => {
tlstate.group(['rect1', 'rect2'], 'newGroup')
expect(tlstate.getShape<GroupShape>('newGroup')).toBeTruthy()
@ -23,7 +26,6 @@ describe('Group command', () => {
describe('when less than two shapes are selected', () => {
it('does nothing', () => {
tlstate.loadDocument(mockDocument)
tlstate.deselectAll()
// @ts-ignore
@ -49,8 +51,6 @@ describe('Group command', () => {
*/
it('creates a group with the correct props', () => {
tlstate.loadDocument(mockDocument)
tlstate.updateShapes(
{
id: 'rect1',
@ -74,8 +74,6 @@ describe('Group command', () => {
})
it('reparents the grouped shapes', () => {
tlstate.loadDocument(mockDocument)
tlstate.updateShapes(
{
id: 'rect1',
@ -114,9 +112,9 @@ describe('Group command', () => {
})
})
describe('when grouping shapes that are the child of another group', () => {
describe('when grouping shapes that already belong to a group', () => {
/*
Do not allow groups to nest. All groups should be the parent of
Do not allow groups to nest. All groups should be children of
the page: a group should never be the child of a different group.
This is a UX decision as much as a technical one.
*/
@ -125,7 +123,8 @@ describe('Group command', () => {
/*
When the selected shapes are the children of another group, and so
long as the children do not represent ALL of the group's children,
then a new group should be created that is a child of the parent group.
then a new group should be created from the selected shapes and the
original group be updated to only contain the remaining ones.
*/
tlstate.resetDocument().createShapes(
@ -206,7 +205,7 @@ describe('Group command', () => {
expect(tlstate.getShape<GroupShape>('newGroupB').children).toStrictEqual(['rect1', 'rect3'])
})
it('does not group shapes if shapes are all the groups children', () => {
it('does nothing if all shapes in the group are selected', () => {
/*
If the selected shapes represent ALL of the children of the a
group, then no effect should occur.
@ -269,7 +268,7 @@ describe('Group command', () => {
])
})
it('marges selected groups that no longer have children', () => {
it('merges selected groups that no longer have children', () => {
/*
If the user is creating a group while having selected other
groups, then the selected groups should be destroyed and a new

View file

@ -5,9 +5,23 @@ import { ArrowShape, TLDrawShapeType } from '~types'
describe('Move to page command', () => {
const tlstate = new TLDrawState()
beforeEach(() => {
tlstate.loadDocument(mockDocument).createPage('page2').changePage('page1')
})
describe('when no shape is selected', () => {
it('does nothing', () => {
const initialState = tlstate.state
tlstate.moveToPage('page2')
const currentState = tlstate.state
expect(currentState).toEqual(initialState)
})
})
/*
Moving shapes to a new page should remove those shapes from the
current page and add them to the specifed page. If bindings exist
current page and add them to the specified page. If bindings exist
that effect the moved shapes, then the bindings should be destroyed
on the old page and created on the new page only if both the "to"
and "from" shapes were moved. The app should then change pages to
@ -15,12 +29,7 @@ describe('Move to page command', () => {
*/
it('does, undoes and redoes command', () => {
tlstate
.loadDocument(mockDocument)
.createPage('page2')
.changePage('page1')
.select('rect1', 'rect2')
.moveToPage('page2')
tlstate.select('rect1', 'rect2').moveToPage('page2')
expect(tlstate.currentPageId).toBe('page2')
expect(tlstate.getShape('rect1', 'page1')).toBeUndefined()
@ -51,7 +60,6 @@ describe('Move to page command', () => {
describe('when moving shapes with bindings', () => {
it('deletes bindings when only the bound-to shape is moved', () => {
tlstate
.loadDocument(mockDocument)
.selectAll()
.delete()
.createShapes(
@ -66,7 +74,7 @@ describe('Move to page command', () => {
const bindingId = tlstate.bindings[0].id
expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBe(bindingId)
tlstate.createPage('page2').changePage('page1').select('target1').moveToPage('page2')
tlstate.select('target1').moveToPage('page2')
expect(
tlstate.getShape<ArrowShape>('arrow1', 'page1').handles.start.bindingId
@ -93,7 +101,6 @@ describe('Move to page command', () => {
it('deletes bindings when only the bound-from shape is moved', () => {
tlstate
.loadDocument(mockDocument)
.selectAll()
.delete()
.createShapes(
@ -108,7 +115,7 @@ describe('Move to page command', () => {
const bindingId = tlstate.bindings[0].id
expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBe(bindingId)
tlstate.createPage('page2').changePage('page1').select('arrow1').moveToPage('page2')
tlstate.select('arrow1').moveToPage('page2')
expect(
tlstate.getShape<ArrowShape>('arrow1', 'page2').handles.start.bindingId
@ -135,7 +142,6 @@ describe('Move to page command', () => {
it('moves bindings when both shapes are moved', () => {
tlstate
.loadDocument(mockDocument)
.selectAll()
.delete()
.createShapes(
@ -150,11 +156,7 @@ describe('Move to page command', () => {
const bindingId = tlstate.bindings[0].id
expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.bindingId).toBe(bindingId)
tlstate
.createPage('page2')
.changePage('page1')
.select('arrow1', 'target1')
.moveToPage('page2')
tlstate.select('arrow1', 'target1').moveToPage('page2')
expect(tlstate.getShape<ArrowShape>('arrow1', 'page2').handles.start.bindingId).toBe(
bindingId
@ -182,12 +184,7 @@ describe('Move to page command', () => {
describe('when moving grouped shapes', () => {
it('moves groups and their children', () => {
tlstate
.loadDocument(mockDocument)
.createPage('page2')
.changePage('page1')
.group(['rect1', 'rect2'], 'groupA')
.moveToPage('page2')
tlstate.group(['rect1', 'rect2'], 'groupA').moveToPage('page2')
expect(tlstate.getShape('rect1', 'page1')).toBeUndefined()
expect(tlstate.getShape('rect2', 'page1')).toBeUndefined()
@ -218,18 +215,10 @@ describe('Move to page command', () => {
expect(tlstate.getShape('groupA', 'page2')).toBeDefined()
})
it('deletes groups shapes if the groups children were all moved', () => {
// ...
})
it.todo('deletes groups shapes if the groups children were all moved')
it('reparents grouped shapes if the group is not moved', () => {
tlstate
.loadDocument(mockDocument)
.createPage('page2')
.changePage('page1')
.group(['rect1', 'rect2', 'rect3'], 'groupA')
.select('rect1')
.moveToPage('page2')
tlstate.group(['rect1', 'rect2', 'rect3'], 'groupA').select('rect1').moveToPage('page2')
expect(tlstate.getShape('rect1', 'page1')).toBeUndefined()
expect(tlstate.getShape('rect1', 'page2')).toBeDefined()

View file

@ -42,8 +42,22 @@ function getSortedIndices(data: Data) {
}
describe('Move command', () => {
it('does, undoes and redoes command', () => {
beforeEach(() => {
tlstate.loadDocument(doc)
})
describe('when no shape is selected', () => {
it('does nothing', () => {
const initialState = tlstate.state
tlstate.moveToBack()
const currentState = tlstate.state
expect(currentState).toEqual(initialState)
})
})
it('does, undoes and redoes command', () => {
tlstate.select('b')
tlstate.moveToBack()
expect(getSortedShapeIds(tlstate.state)).toBe('bacd')
@ -55,7 +69,6 @@ describe('Move command', () => {
describe('to back', () => {
it('moves a shape to back', () => {
tlstate.loadDocument(doc)
tlstate.select('b')
tlstate.moveToBack()
expect(getSortedShapeIds(tlstate.state)).toBe('bacd')
@ -63,7 +76,6 @@ describe('Move command', () => {
})
it('moves two adjacent siblings to back', () => {
tlstate.loadDocument(doc)
tlstate.select('b', 'c')
tlstate.moveToBack()
expect(getSortedShapeIds(tlstate.state)).toBe('bcad')
@ -71,7 +83,6 @@ describe('Move command', () => {
})
it('moves two non-adjacent siblings to back', () => {
tlstate.loadDocument(doc)
tlstate.select('b', 'd')
tlstate.moveToBack()
expect(getSortedShapeIds(tlstate.state)).toBe('bdac')
@ -81,7 +92,6 @@ describe('Move command', () => {
describe('backward', () => {
it('moves a shape backward', () => {
tlstate.loadDocument(doc)
tlstate.select('c')
tlstate.moveBackward()
expect(getSortedShapeIds(tlstate.state)).toBe('acbd')
@ -89,7 +99,6 @@ describe('Move command', () => {
})
it('moves a shape at first index backward', () => {
tlstate.loadDocument(doc)
tlstate.select('a')
tlstate.moveBackward()
expect(getSortedShapeIds(tlstate.state)).toBe('abcd')
@ -97,7 +106,6 @@ describe('Move command', () => {
})
it('moves two adjacent siblings backward', () => {
tlstate.loadDocument(doc)
tlstate.select('c', 'd')
tlstate.moveBackward()
expect(getSortedShapeIds(tlstate.state)).toBe('acdb')
@ -105,7 +113,6 @@ describe('Move command', () => {
})
it('moves two non-adjacent siblings backward', () => {
tlstate.loadDocument(doc)
tlstate.select('b', 'd')
tlstate.moveBackward()
expect(getSortedShapeIds(tlstate.state)).toBe('badc')
@ -113,7 +120,6 @@ describe('Move command', () => {
})
it('moves two adjacent siblings backward at zero index', () => {
tlstate.loadDocument(doc)
tlstate.select('a', 'b')
tlstate.moveBackward()
expect(getSortedShapeIds(tlstate.state)).toBe('abcd')
@ -123,7 +129,6 @@ describe('Move command', () => {
describe('forward', () => {
it('moves a shape forward', () => {
tlstate.loadDocument(doc)
tlstate.select('c')
tlstate.moveForward()
expect(getSortedShapeIds(tlstate.state)).toBe('abdc')
@ -131,7 +136,6 @@ describe('Move command', () => {
})
it('moves a shape forward at the top index', () => {
tlstate.loadDocument(doc)
tlstate.select('b')
tlstate.moveForward()
tlstate.moveForward()
@ -141,7 +145,6 @@ describe('Move command', () => {
})
it('moves two adjacent siblings forward', () => {
tlstate.loadDocument(doc)
tlstate.select('a', 'b')
tlstate.moveForward()
expect(getSortedShapeIds(tlstate.state)).toBe('cabd')
@ -149,7 +152,6 @@ describe('Move command', () => {
})
it('moves two non-adjacent siblings forward', () => {
tlstate.loadDocument(doc)
tlstate.select('a', 'c')
tlstate.moveForward()
expect(getSortedShapeIds(tlstate.state)).toBe('badc')
@ -157,7 +159,6 @@ describe('Move command', () => {
})
it('moves two adjacent siblings forward at top index', () => {
tlstate.loadDocument(doc)
tlstate.select('c', 'd')
tlstate.moveForward()
expect(getSortedShapeIds(tlstate.state)).toBe('abcd')
@ -167,7 +168,6 @@ describe('Move command', () => {
describe('to front', () => {
it('moves a shape to front', () => {
tlstate.loadDocument(doc)
tlstate.select('b')
tlstate.moveToFront()
expect(getSortedShapeIds(tlstate.state)).toBe('acdb')
@ -175,7 +175,6 @@ describe('Move command', () => {
})
it('moves two adjacent siblings to front', () => {
tlstate.loadDocument(doc)
tlstate.select('a', 'b')
tlstate.moveToFront()
expect(getSortedShapeIds(tlstate.state)).toBe('cdab')
@ -183,7 +182,6 @@ describe('Move command', () => {
})
it('moves two non-adjacent siblings to front', () => {
tlstate.loadDocument(doc)
tlstate.select('a', 'c')
tlstate.moveToFront()
expect(getSortedShapeIds(tlstate.state)).toBe('bdac')
@ -191,7 +189,6 @@ describe('Move command', () => {
})
it('moves siblings already at front to front', () => {
tlstate.loadDocument(doc)
tlstate.select('c', 'd')
tlstate.moveToFront()
expect(getSortedShapeIds(tlstate.state)).toBe('abcd')

View file

@ -3,10 +3,24 @@ import { mockDocument } from '~test'
describe('Rotate command', () => {
const tlstate = new TLDrawState()
tlstate.loadDocument(mockDocument)
tlstate.select('rect1')
beforeEach(() => {
tlstate.loadDocument(mockDocument)
})
describe('when no shape is selected', () => {
it('does nothing', () => {
const initialState = tlstate.state
tlstate.rotate()
const currentState = tlstate.state
expect(currentState).toEqual(initialState)
})
})
it('does, undoes and redoes command', () => {
tlstate.select('rect1')
expect(tlstate.getShape('rect1').rotation).toBe(undefined)
tlstate.rotate()

View file

@ -5,8 +5,22 @@ import { mockDocument } from '~test'
describe('Stretch command', () => {
const tlstate = new TLDrawState()
it('does, undoes and redoes command', () => {
beforeEach(() => {
tlstate.loadDocument(mockDocument)
})
describe('when less than two shapes are selected', () => {
it('does nothing', () => {
tlstate.select('rect2')
const initialState = tlstate.state
tlstate.stretch(StretchType.Horizontal)
const currentState = tlstate.state
expect(currentState).toEqual(initialState)
})
})
it('does, undoes and redoes command', () => {
tlstate.select('rect1', 'rect2')
tlstate.stretch(StretchType.Horizontal)
@ -31,7 +45,6 @@ describe('Stretch command', () => {
})
it('stretches horizontally', () => {
tlstate.loadDocument(mockDocument)
tlstate.select('rect1', 'rect2')
tlstate.stretch(StretchType.Horizontal)
@ -42,7 +55,6 @@ describe('Stretch command', () => {
})
it('stretches vertically', () => {
tlstate.loadDocument(mockDocument)
tlstate.select('rect1', 'rect2')
tlstate.stretch(StretchType.Vertical)

View file

@ -6,9 +6,32 @@ import { ArrowShape, Decoration, TLDrawShape } from '~types'
describe('Toggle decoration command', () => {
const tlstate = new TLDrawState()
beforeEach(() => {
tlstate.loadDocument(mockDocument)
})
describe('when no shape is selected', () => {
it('does nothing', () => {
const initialState = tlstate.state
tlstate.toggleDecoration('start')
const currentState = tlstate.state
expect(currentState).toEqual(initialState)
})
})
describe('when handle id is invalid', () => {
it('does nothing', () => {
const initialState = tlstate.state
tlstate.toggleDecoration('invalid')
const currentState = tlstate.state
expect(currentState).toEqual(initialState)
})
})
it('does, undoes and redoes command', () => {
tlstate
.loadDocument(mockDocument)
.create(
TLDR.getShapeUtils({ type: 'arrow' } as TLDrawShape).create({
id: 'arrow1',

View file

@ -5,38 +5,57 @@ import { mockDocument } from '~test'
describe('Toggle command', () => {
const tlstate = new TLDrawState()
it('does, undoes and redoes command', () => {
beforeEach(() => {
tlstate.loadDocument(mockDocument)
})
describe('when no shape is selected', () => {
it('does nothing', () => {
const initialState = tlstate.state
tlstate.toggleHidden()
const currentState = tlstate.state
expect(currentState).toEqual(initialState)
})
})
it('does, undoes and redoes command', () => {
tlstate.selectAll()
expect(tlstate.getShape('rect2').isAspectRatioLocked).toBe(undefined)
expect(tlstate.getShape('rect2').isLocked).toBe(undefined)
tlstate.toggleAspectRatioLocked()
tlstate.toggleLocked()
expect(tlstate.getShape('rect2').isAspectRatioLocked).toBe(true)
expect(tlstate.getShape('rect2').isLocked).toBe(true)
tlstate.undo()
expect(tlstate.getShape('rect2').isAspectRatioLocked).toBe(undefined)
expect(tlstate.getShape('rect2').isLocked).toBe(undefined)
tlstate.redo()
expect(tlstate.getShape('rect2').isAspectRatioLocked).toBe(true)
expect(tlstate.getShape('rect2').isLocked).toBe(true)
})
it('toggles on before off when mixed values', () => {
tlstate.loadDocument(mockDocument)
tlstate.select('rect2')
expect(tlstate.getShape<RectangleShape>('rect1').isAspectRatioLocked).toBe(undefined)
expect(tlstate.getShape<RectangleShape>('rect2').isAspectRatioLocked).toBe(undefined)
tlstate.toggleAspectRatioLocked()
expect(tlstate.getShape<RectangleShape>('rect1').isAspectRatioLocked).toBe(undefined)
expect(tlstate.getShape<RectangleShape>('rect2').isAspectRatioLocked).toBe(true)
tlstate.selectAll()
tlstate.toggleAspectRatioLocked()
expect(tlstate.getShape<RectangleShape>('rect1').isAspectRatioLocked).toBe(true)
expect(tlstate.getShape<RectangleShape>('rect1').isAspectRatioLocked).toBe(true)
tlstate.toggleAspectRatioLocked()
expect(tlstate.getShape<RectangleShape>('rect1').isAspectRatioLocked).toBe(false)
expect(tlstate.getShape<RectangleShape>('rect1').isAspectRatioLocked).toBe(false)
})

View file

@ -6,8 +6,21 @@ import { ArrowShape, TLDrawShapeType } from '~types'
describe('Translate command', () => {
const tlstate = new TLDrawState()
it('does, undoes and redoes command', () => {
beforeEach(() => {
tlstate.loadDocument(mockDocument)
})
describe('when no shape is selected', () => {
it('does nothing', () => {
const initialState = tlstate.state
tlstate.nudge([1, 2])
const currentState = tlstate.state
expect(currentState).toEqual(initialState)
})
})
it('does, undoes and redoes command', () => {
tlstate.selectAll()
tlstate.nudge([1, 2])
@ -23,7 +36,6 @@ describe('Translate command', () => {
})
it('major nudges', () => {
tlstate.loadDocument(mockDocument)
tlstate.selectAll()
tlstate.nudge([1, 2], true)
expect(tlstate.getShape('rect2').point).toEqual([110, 120])

View file

@ -1,19 +1,34 @@
import { TLDrawState } from '~state'
import { mockDocument } from '~test'
import { Utils } from '@tldraw/core'
const doc = Utils.deepClone(mockDocument)
describe('Update command', () => {
const tlstate = new TLDrawState()
beforeEach(() => {
tlstate.loadDocument(mockDocument)
})
describe('when no shape is selected', () => {
it('does nothing', () => {
const initialState = tlstate.state
tlstate.updateShapes()
const currentState = tlstate.state
expect(currentState).toEqual(initialState)
})
})
it('does, undoes and redoes command', () => {
tlstate.loadDocument(doc)
tlstate.updateShapes({ id: 'rect1', point: [100, 100] })
expect(tlstate.getShape('rect1').point).toStrictEqual([100, 100])
tlstate.undo()
expect(tlstate.getShape('rect1').point).toStrictEqual([0, 0])
tlstate.redo()
expect(tlstate.getShape('rect1').point).toStrictEqual([100, 100])
})
})

View file

@ -3,11 +3,14 @@
"include": ["src"],
"exclude": ["node_modules", "dist", "docs"],
"compilerOptions": {
"composite": true,
"emitDeclarationOnly": true,
"rootDir": "src",
"outDir": "./dist/types",
"baseUrl": "src",
"paths": {
"~*": ["./*"]
}
}
},
"references": [{ "path": "../../packages/core" }]
}

View file

@ -1,5 +1,6 @@
{
"compilerOptions": {
"composite": true,
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
@ -20,5 +21,11 @@
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
"exclude": ["node_modules"],
"references": [
{
"path": "../../packages/tldraw"
},
{ "path": "../../packages/core" }
]
}

View file

@ -1,31 +1,33 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
// For references
"declaration": true,
"declarationMap": true,
"sourceMap": true,
// Other
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"importsNotUsedAsValues": "error",
"stripInternal": true,
"incremental": true,
"importHelpers": true,
"importsNotUsedAsValues": "error",
"incremental": true,
"jsx": "preserve",
"lib": ["dom", "esnext"],
"module": "esnext",
"moduleResolution": "node",
"declarationMap": true,
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
"noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
"noUnusedLocals": false /* Report errors on unused locals. */,
"noUnusedParameters": false /* Report errors on unused parameters. */,
"skipLibCheck": true,
"sourceMap": true,
"strict": false,
"strictFunctionTypes": true /* Enable strict checking of function types. */,
"strictNullChecks": true /* Enable strict null checks. */,
"stripInternal": true,
"target": "es6",
"typeRoots": ["node_modules/@types", "node_modules/jest"],
"types": ["node", "jest"],
"jsx": "preserve",
"lib": ["dom", "esnext"],
"module": "esnext"
"types": ["node", "jest"]
}
}

View file

@ -1,6 +1,9 @@
{
"composite": true,
"extends": "./tsconfig.base.json",
"exclude": ["node_modules", "**/*.test.ts", "**/*.spec.ts"],
"files": [],
"references": [{ "path": "./packages/tldraw" }, { "path": "./packages/core" }],
"compilerOptions": {
"baseUrl": ".",
"paths": {

View file

@ -2914,9 +2914,9 @@
universal-user-agent "^6.0.0"
"@octokit/openapi-types@^10.1.0":
version "10.1.0"
resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-10.1.0.tgz#43c2ce1ad0ad711869f0a366dfa04d48e06f2fa2"
integrity sha512-Nq5TMBwijRXco+Bm/Rq1n5maxxXsHLwd/Cm3lyNeOxbjyzAOSD0qmr4TwKCD4TN4rHZ7lq/tARuqSv/WJHF7IA==
version "10.1.1"
resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-10.1.1.tgz#74607482d193e9c9cc7e23ecf04b1bde3eabb6d8"
integrity sha512-ygp/6r25Ezb1CJuVMnFfOsScEtPF0zosdTJDZ7mZ+I8IULl7DP1BS5ZvPRwglcarGPXOvS5sHdR0bjnVDDfQdQ==
"@octokit/plugin-enterprise-rest@^6.0.1":
version "6.0.1"
@ -3827,9 +3827,9 @@
"@types/react" "*"
"@types/react@*", "@types/react@^17.0.16", "@types/react@^17.0.19", "@types/react@^17.0.3":
version "17.0.19"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.19.tgz#8f2a85e8180a43b57966b237d26a29481dacc991"
integrity sha512-sX1HisdB1/ZESixMTGnMxH9TDe8Sk709734fEQZzCV/4lSu9kJCPbo2PbTRoZM+53Pp0P10hYVyReUueGwUi4A==
version "17.0.20"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.20.tgz#a4284b184d47975c71658cd69e759b6bd37c3b8c"
integrity sha512-wWZrPlihslrPpcKyCSlmIlruakxr57/buQN1RjlIeaaTWDLtJkTtRW429MoQJergvVKc4IWBpRhWw7YNh/7GVA==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
@ -4034,9 +4034,9 @@ acorn-walk@^7.1.1:
integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
acorn-walk@^8.1.1:
version "8.1.1"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.1.1.tgz#3ddab7f84e4a7e2313f6c414c5b7dac85f4e3ebc"
integrity sha512-FbJdceMlPHEAWJOILDk1fXD8lnTlEIWFkqtfk+MvmL5q/qlHfN7GEHcsFZWt/Tea9jRNPWUZG4G976nqAAmU9w==
version "8.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
acorn@^7.1.1, acorn@^7.4.0:
version "7.4.1"
@ -4044,9 +4044,9 @@ acorn@^7.1.1, acorn@^7.4.0:
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
acorn@^8.2.4, acorn@^8.4.1:
version "8.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c"
integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==
version "8.5.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.5.0.tgz#4512ccb99b3698c752591e9bb4472e38ad43cee2"
integrity sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==
add-stream@^1.0.0:
version "1.0.0"
@ -4745,13 +4745,13 @@ browserslist@4.16.6:
node-releases "^1.1.71"
browserslist@^4.16.6, browserslist@^4.16.8:
version "4.16.8"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.8.tgz#cb868b0b554f137ba6e33de0ecff2eda403c4fb0"
integrity sha512-sc2m9ohR/49sWEbPj14ZSSZqp+kbi16aLao42Hmn3Z8FpjuMaq2xCA2l4zl9ITfyzvnvyE0hcg62YkIGKxgaNQ==
version "4.17.0"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.17.0.tgz#1fcd81ec75b41d6d4994fb0831b92ac18c01649c"
integrity sha512-g2BJ2a0nEYvEFQC208q8mVAhfNwpZ5Mu8BwgtCdZKO3qx98HChmeg448fPdUzld8aFmfLgVh7yymqV+q1lJZ5g==
dependencies:
caniuse-lite "^1.0.30001251"
caniuse-lite "^1.0.30001254"
colorette "^1.3.0"
electron-to-chromium "^1.3.811"
electron-to-chromium "^1.3.830"
escalade "^3.1.1"
node-releases "^1.1.75"
@ -4992,10 +4992,10 @@ camelcase@^6.2.0:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809"
integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
caniuse-lite@^1.0.30001202, caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001228, caniuse-lite@^1.0.30001251:
version "1.0.30001254"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001254.tgz#974d45e8b7f6e3b63d4b1435e97752717612d4b9"
integrity sha512-GxeHOvR0LFMYPmFGA+NiTOt9uwYDxB3h154tW2yBYwfz2EMX3i1IBgr6gmJGfU0K8KQsqPa5XqLD8zVdP5lUzA==
caniuse-lite@^1.0.30001202, caniuse-lite@^1.0.30001219, caniuse-lite@^1.0.30001228, caniuse-lite@^1.0.30001254:
version "1.0.30001255"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001255.tgz#f3b09b59ab52e39e751a569523618f47c4298ca0"
integrity sha512-F+A3N9jTZL882f/fg/WWVnKSu6IOo3ueLz4zwaOPbPYHNmM/ZaDUyzyJwS1mZhX7Ex5jqTyW599Gdelh5PDYLQ==
caseless@~0.12.0:
version "0.12.0"
@ -5832,9 +5832,9 @@ dedent@^0.7.0:
integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=
deep-is@^0.1.3, deep-is@~0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
version "0.1.4"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
deepmerge@^4.2.2:
version "4.2.2"
@ -6083,7 +6083,7 @@ ejs@^2.6.1:
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba"
integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==
electron-to-chromium@^1.3.723, electron-to-chromium@^1.3.811:
electron-to-chromium@^1.3.723, electron-to-chromium@^1.3.830:
version "1.3.830"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.830.tgz#40e3144204f8ca11b2cebec83cf14c20d3499236"
integrity sha512-gBN7wNAxV5vl1430dG+XRcQhD4pIeYeak6p6rjdCtlz5wWNwDad8jwvphe5oi1chL5MV6RNRikfffBBiFuj+rQ==
@ -7120,9 +7120,9 @@ git-up@^4.0.0:
parse-url "^6.0.0"
git-url-parse@^11.1.2, git-url-parse@^11.4.4:
version "11.5.0"
resolved "https://registry.yarnpkg.com/git-url-parse/-/git-url-parse-11.5.0.tgz#acaaf65239cb1536185b19165a24bbc754b3f764"
integrity sha512-TZYSMDeM37r71Lqg1mbnMlOqlHd7BSij9qN7XwTkRqSAYFMihGLGhfHwgqQob3GUhEneKnV4nskN9rbQw2KGxA==
version "11.6.0"
resolved "https://registry.yarnpkg.com/git-url-parse/-/git-url-parse-11.6.0.tgz#c634b8de7faa66498a2b88932df31702c67df605"
integrity sha512-WWUxvJs5HsyHL6L08wOusa/IXYtMuCAhrMmnTjQPpBU0TTHyDhnOATNH3xNQz7YOQUsqIIPTGr4xiVti1Hsk5g==
dependencies:
git-up "^4.0.0"
@ -7491,11 +7491,11 @@ iconv-lite@^0.6.2:
safer-buffer ">= 2.1.2 < 3.0.0"
idb-keyval@^5.1.3:
version "5.1.3"
resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-5.1.3.tgz#6ef5dff371897c23f144322dc6374eadd6a345d9"
integrity sha512-N9HbCK/FaXSRVI+k6Xq4QgWxbcZRUv+SfG1y7HJ28JdV8yEJu6k+C/YLea7npGckX2DQJeEVuMc4bKOBeU/2LQ==
version "5.1.4"
resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-5.1.4.tgz#c46c7dfc93087ceb3ce0072d6c517e1544a061e3"
integrity sha512-uo43nnrb50yUzEIf1EVRDcHTSa38LQ1SQTkIupH16/FxzpftktV+bpAy5gvpZqvvA9inYKulLVxcE8W8bwi1DA==
dependencies:
safari-14-idb-fix "^1.0.4"
safari-14-idb-fix "^1.0.5"
idb@^6.0.0, idb@^6.1.2:
version "6.1.3"
@ -11557,10 +11557,10 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^3.0.0"
inherits "^2.0.1"
rko@^0.5.19, rko@^0.5.20:
version "0.5.20"
resolved "https://registry.yarnpkg.com/rko/-/rko-0.5.20.tgz#a0a374e9b8d6880f05a1d13f2efc277299644e19"
integrity sha512-u9V6n0rx9nHI+hO0e20LYdvKEHCDDwVRsD0vMxMe2A2sHWVb6qtmwMw3hifJTo0Tn3UJQDOiBy+uE7zxEyrS/g==
rko@^0.5.19, rko@^0.5.20, rko@^0.5.22:
version "0.5.22"
resolved "https://registry.yarnpkg.com/rko/-/rko-0.5.22.tgz#d5a563beefd97a9cfdda3c29c1fbe119d782b576"
integrity sha512-aNXCHTLshLgq6XuWZzb3XKgy5RyS5YY6/YzCnrBa+tn7NTPC2HysbNhJwyt6bow3u8whiD36GajhUYR6oIbJWw==
dependencies:
idb-keyval "^5.1.3"
zustand "^3.5.9"
@ -11608,10 +11608,10 @@ rxjs@^6.4.0, rxjs@^6.6.0, rxjs@^6.6.3:
dependencies:
tslib "^1.9.0"
safari-14-idb-fix@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/safari-14-idb-fix/-/safari-14-idb-fix-1.0.4.tgz#5c68ba63e2a8ae0d89a0aa1e13fe89e3aef7da19"
integrity sha512-4+Y2baQdgJpzu84d0QjySl70Kyygzf0pepVg8NVg4NnQEPpfC91fAn0baNvtStlCjUUxxiu0BOMiafa98fRRuA==
safari-14-idb-fix@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/safari-14-idb-fix/-/safari-14-idb-fix-1.0.5.tgz#0bc9b4433774d47da2394f37e2d1de19075294ee"
integrity sha512-NhqdtNr1aq4/+gmIbwcc0Odlh55niZDYHuCFqRTZVpLjIi+wXJg+yQ4mXXZBk5Z4h4hkZXv0N615YV5Np6qhgw==
safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0:
version "5.2.1"