Adds move to page
This commit is contained in:
parent
dbed5f6bf4
commit
8ecddcbdbc
13 changed files with 657 additions and 199 deletions
|
@ -179,11 +179,11 @@ export const ContextMenu = React.memo(({ children }: ContextMenuProps): JSX.Elem
|
|||
<Kbd variant="menu">#⇧[</Kbd>
|
||||
</ContextMenuButton>
|
||||
</ContextMenuSubMenu>
|
||||
<MoveToPageMenu />
|
||||
{hasTwoOrMore && (
|
||||
<AlignDistributeSubMenu hasTwoOrMore={hasTwoOrMore} hasThreeOrMore={hasThreeOrMore} />
|
||||
)}
|
||||
<ContextMenuDivider />
|
||||
{/* <MoveToPageMenu /> */}
|
||||
<ContextMenuButton onSelect={handleCopy}>
|
||||
<span>Copy</span>
|
||||
<Kbd variant="menu">#C</Kbd>
|
||||
|
@ -345,38 +345,42 @@ const StyledGrid = styled(MenuContent, {
|
|||
},
|
||||
})
|
||||
|
||||
// function MoveToPageMenu() {
|
||||
// const documentPages = useSelector((s) => s.data.document.pages)
|
||||
// const currentPageId = useSelector((s) => s.data.currentPageId)
|
||||
const currentPageIdSelector = (s: Data) => s.appState.currentPageId
|
||||
const documentPagesSelector = (s: Data) => s.document.pages
|
||||
|
||||
// if (!documentPages[currentPageId]) return null
|
||||
function MoveToPageMenu(): JSX.Element | null {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
const currentPageId = useSelector(currentPageIdSelector)
|
||||
const documentPages = useSelector(documentPagesSelector)
|
||||
|
||||
// const sorted = Object.values(documentPages)
|
||||
// .sort((a, b) => a.childIndex - b.childIndex)
|
||||
// .filter((a) => a.id !== currentPageId)
|
||||
const sorted = Object.values(documentPages)
|
||||
.sort((a, b) => (a.childIndex || 0) - (b.childIndex || 0))
|
||||
.filter((a) => a.id !== currentPageId)
|
||||
|
||||
// if (sorted.length === 0) return null
|
||||
if (sorted.length === 0) return null
|
||||
|
||||
// return (
|
||||
// <ContextMenuRoot>
|
||||
// <ContextMenuButton>
|
||||
// <span>Move To Page</span>
|
||||
// <IconWrapper size="small">
|
||||
// <ChevronRightIcon />
|
||||
// </IconWrapper>
|
||||
// </ContextMenuButton>
|
||||
// <MenuContent as={RadixContextMenu.Content} sideOffset={2} alignOffset={-2}>
|
||||
// {sorted.map(({ id, name }) => (
|
||||
// <ContextMenuButton
|
||||
// key={id}
|
||||
// disabled={id === currentPageId}
|
||||
// onSelect={() => state.send('MOVED_TO_PAGE', { id })}
|
||||
// >
|
||||
// <span>{name}</span>
|
||||
// </ContextMenuButton>
|
||||
// ))}
|
||||
// <ContextMenuArrow offset={13} />
|
||||
// </MenuContent>
|
||||
// </ContextMenuRoot>
|
||||
// )
|
||||
// }
|
||||
console.log(sorted)
|
||||
|
||||
return (
|
||||
<ContextMenuRoot>
|
||||
<RadixContextMenu.TriggerItem as={RowButton} bp={breakpoints}>
|
||||
<span>Move To Page</span>
|
||||
<IconWrapper size="small">
|
||||
<ChevronRightIcon />
|
||||
</IconWrapper>
|
||||
</RadixContextMenu.TriggerItem>
|
||||
<MenuContent as={RadixContextMenu.Content} sideOffset={2} alignOffset={-2}>
|
||||
{sorted.map(({ id, name }, i) => (
|
||||
<ContextMenuButton
|
||||
key={id}
|
||||
disabled={id === currentPageId}
|
||||
onSelect={() => tlstate.moveToPage(id)}
|
||||
>
|
||||
<span>{name || `Page ${i}`}</span>
|
||||
</ContextMenuButton>
|
||||
))}
|
||||
<ContextMenuArrow offset={13} />
|
||||
</MenuContent>
|
||||
</ContextMenuRoot>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import type { TLDrawShape, Data, TLDrawCommand } from '~types'
|
|||
|
||||
export function create(data: Data, shapes: TLDrawShape[]): TLDrawCommand {
|
||||
const { currentPageId } = data.appState
|
||||
|
||||
const beforeShapes: Record<string, Patch<TLDrawShape> | undefined> = {}
|
||||
const afterShapes: Record<string, Patch<TLDrawShape> | undefined> = {}
|
||||
|
||||
|
|
|
@ -90,9 +90,26 @@ describe('Delete command', () => {
|
|||
it('updates the group', () => {
|
||||
tlstate
|
||||
.loadDocument(mockDocument)
|
||||
.group(['rect1', 'rect2'], 'newGroup')
|
||||
.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()
|
||||
|
||||
expect(tlstate.getShape('rect1')).toBeUndefined()
|
||||
expect(tlstate.getShape('rect2')).toBeUndefined()
|
||||
expect(tlstate.getShape('newGroup')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { TLDR } from '~state/tldr'
|
||||
import type { Data, TLDrawCommand, PagePartial, TLDrawShape, GroupShape } from '~types'
|
||||
import type { Data, TLDrawCommand } from '~types'
|
||||
import { removeShapesFromPage } from '../utils/removeShapesFromPage'
|
||||
|
||||
// - [ ] Update parents and possibly delete parents
|
||||
|
||||
|
@ -8,82 +9,7 @@ export function deleteShapes(
|
|||
ids: string[],
|
||||
pageId = data.appState.currentPageId
|
||||
): TLDrawCommand {
|
||||
const before: PagePartial = {
|
||||
shapes: {},
|
||||
bindings: {},
|
||||
}
|
||||
|
||||
const after: PagePartial = {
|
||||
shapes: {},
|
||||
bindings: {},
|
||||
}
|
||||
|
||||
const parentsToUpdate: GroupShape[] = []
|
||||
|
||||
const deletedIds = [...ids]
|
||||
|
||||
// These are the shapes we're definitely going to delete
|
||||
|
||||
ids.forEach((id) => {
|
||||
const shape = TLDR.getShape(data, id, pageId)
|
||||
before.shapes[id] = shape
|
||||
after.shapes[id] = undefined
|
||||
|
||||
if (shape.parentId !== pageId) {
|
||||
parentsToUpdate.push(TLDR.getShape(data, shape.parentId, pageId))
|
||||
}
|
||||
})
|
||||
|
||||
parentsToUpdate.forEach((parent) => {
|
||||
if (ids.includes(parent.id)) return
|
||||
deletedIds.push(parent.id)
|
||||
before.shapes[parent.id] = { children: parent.children }
|
||||
after.shapes[parent.id] = { children: parent.children.filter((id) => !ids.includes(id)) }
|
||||
})
|
||||
|
||||
// Recursively check for empty parents?
|
||||
|
||||
const page = TLDR.getPage(data, pageId)
|
||||
|
||||
// We also need to delete bindings that reference the deleted shapes
|
||||
Object.values(page.bindings).forEach((binding) => {
|
||||
for (const id of [binding.toId, binding.fromId]) {
|
||||
// If the binding references a deleted shape...
|
||||
if (after.shapes[id] === undefined) {
|
||||
// Delete this binding
|
||||
before.bindings[binding.id] = binding
|
||||
after.bindings[binding.id] = undefined
|
||||
|
||||
// Let's also look each the bound shape...
|
||||
const shape = TLDR.getShape(data, id, pageId)
|
||||
|
||||
// If the bound shape has a handle that references the deleted binding...
|
||||
if (shape.handles) {
|
||||
Object.values(shape.handles)
|
||||
.filter((handle) => handle.bindingId === binding.id)
|
||||
.forEach((handle) => {
|
||||
// Save the binding reference in the before patch
|
||||
before.shapes[id] = {
|
||||
...before.shapes[id],
|
||||
handles: {
|
||||
...before.shapes[id]?.handles,
|
||||
[handle.id]: { bindingId: binding.id },
|
||||
},
|
||||
}
|
||||
|
||||
// Unless we're currently deleting the shape, remove the
|
||||
// binding reference from the after patch
|
||||
if (!deletedIds.includes(id)) {
|
||||
after.shapes[id] = {
|
||||
...after.shapes[id],
|
||||
handles: { ...after.shapes[id]?.handles, [handle.id]: { bindingId: undefined } },
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
const { before, after } = removeShapesFromPage(data, ids, pageId)
|
||||
|
||||
return {
|
||||
id: 'delete_shapes',
|
||||
|
|
|
@ -8,6 +8,7 @@ export * from './distribute'
|
|||
export * from './duplicate-page'
|
||||
export * from './duplicate'
|
||||
export * from './flip'
|
||||
export * from './group'
|
||||
export * from './move'
|
||||
export * from './rename-page'
|
||||
export * from './rotate'
|
||||
|
@ -17,4 +18,4 @@ export * from './toggle-decoration'
|
|||
export * from './toggle'
|
||||
export * from './translate'
|
||||
export * from './update'
|
||||
export * from './group'
|
||||
export * from './move-to-page'
|
||||
|
|
1
packages/tldraw/src/state/command/move-to-page/index.ts
Normal file
1
packages/tldraw/src/state/command/move-to-page/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './move-to-page.command'
|
|
@ -0,0 +1,250 @@
|
|||
import { TLDrawState } from '~state'
|
||||
import { mockDocument } from '~test'
|
||||
import { ArrowShape, TLDrawShapeType } from '~types'
|
||||
|
||||
describe('Move to page command', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
|
||||
/*
|
||||
Moving shapes to a new page should remove those shapes from the
|
||||
current page and add them to the specifed 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
|
||||
the new page.
|
||||
*/
|
||||
|
||||
it('does, undoes and redoes command', () => {
|
||||
tlstate
|
||||
.loadDocument(mockDocument)
|
||||
.createPage('page2')
|
||||
.changePage('page1')
|
||||
.select('rect1', 'rect2')
|
||||
.moveToPage('page2')
|
||||
|
||||
expect(tlstate.currentPageId).toBe('page2')
|
||||
expect(tlstate.getShape('rect1', 'page1')).toBeUndefined()
|
||||
expect(tlstate.getShape('rect1', 'page2')).toBeDefined()
|
||||
expect(tlstate.getShape('rect2', 'page1')).toBeUndefined()
|
||||
expect(tlstate.getShape('rect2', 'page2')).toBeDefined()
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect1', 'rect2'])
|
||||
|
||||
tlstate.undo()
|
||||
|
||||
expect(tlstate.getShape('rect1', 'page1')).toBeDefined()
|
||||
expect(tlstate.getShape('rect1', 'page2')).toBeUndefined()
|
||||
expect(tlstate.getShape('rect2', 'page1')).toBeDefined()
|
||||
expect(tlstate.getShape('rect2', 'page2')).toBeUndefined()
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect1', 'rect2'])
|
||||
expect(tlstate.currentPageId).toBe('page1')
|
||||
|
||||
tlstate.redo()
|
||||
|
||||
expect(tlstate.getShape('rect1', 'page1')).toBeUndefined()
|
||||
expect(tlstate.getShape('rect1', 'page2')).toBeDefined()
|
||||
expect(tlstate.getShape('rect2', 'page1')).toBeUndefined()
|
||||
expect(tlstate.getShape('rect2', 'page2')).toBeDefined()
|
||||
expect(tlstate.selectedIds).toStrictEqual(['rect1', 'rect2'])
|
||||
expect(tlstate.currentPageId).toBe('page2')
|
||||
})
|
||||
|
||||
describe('when moving shapes with bindings', () => {
|
||||
it('deletes bindings when only the bound-to shape is moved', () => {
|
||||
tlstate
|
||||
.loadDocument(mockDocument)
|
||||
.selectAll()
|
||||
.delete()
|
||||
.createShapes(
|
||||
{ type: TLDrawShapeType.Rectangle, id: 'target1', size: [100, 100] },
|
||||
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||
)
|
||||
.select('arrow1')
|
||||
.startHandleSession([200, 200], 'start')
|
||||
.updateHandleSession([50, 50])
|
||||
.completeSession()
|
||||
|
||||
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')
|
||||
|
||||
expect(
|
||||
tlstate.getShape<ArrowShape>('arrow1', 'page1').handles.start.bindingId
|
||||
).toBeUndefined()
|
||||
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeUndefined()
|
||||
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||
|
||||
tlstate.undo()
|
||||
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1', 'page1').handles.start.bindingId).toBe(
|
||||
bindingId
|
||||
)
|
||||
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeDefined()
|
||||
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||
|
||||
tlstate.redo()
|
||||
|
||||
expect(
|
||||
tlstate.getShape<ArrowShape>('arrow1', 'page1').handles.start.bindingId
|
||||
).toBeUndefined()
|
||||
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeUndefined()
|
||||
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||
})
|
||||
|
||||
it('deletes bindings when only the bound-from shape is moved', () => {
|
||||
tlstate
|
||||
.loadDocument(mockDocument)
|
||||
.selectAll()
|
||||
.delete()
|
||||
.createShapes(
|
||||
{ type: TLDrawShapeType.Rectangle, id: 'target1', size: [100, 100] },
|
||||
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||
)
|
||||
.select('arrow1')
|
||||
.startHandleSession([200, 200], 'start')
|
||||
.updateHandleSession([50, 50])
|
||||
.completeSession()
|
||||
|
||||
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')
|
||||
|
||||
expect(
|
||||
tlstate.getShape<ArrowShape>('arrow1', 'page2').handles.start.bindingId
|
||||
).toBeUndefined()
|
||||
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeUndefined()
|
||||
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||
|
||||
tlstate.undo()
|
||||
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1', 'page1').handles.start.bindingId).toBe(
|
||||
bindingId
|
||||
)
|
||||
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeDefined()
|
||||
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||
|
||||
tlstate.redo()
|
||||
|
||||
expect(
|
||||
tlstate.getShape<ArrowShape>('arrow1', 'page2').handles.start.bindingId
|
||||
).toBeUndefined()
|
||||
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeUndefined()
|
||||
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||
})
|
||||
|
||||
it('moves bindings when both shapes are moved', () => {
|
||||
tlstate
|
||||
.loadDocument(mockDocument)
|
||||
.selectAll()
|
||||
.delete()
|
||||
.createShapes(
|
||||
{ type: TLDrawShapeType.Rectangle, id: 'target1', size: [100, 100] },
|
||||
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||
)
|
||||
.select('arrow1')
|
||||
.startHandleSession([200, 200], 'start')
|
||||
.updateHandleSession([50, 50])
|
||||
.completeSession()
|
||||
|
||||
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')
|
||||
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1', 'page2').handles.start.bindingId).toBe(
|
||||
bindingId
|
||||
)
|
||||
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeUndefined()
|
||||
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeDefined()
|
||||
|
||||
tlstate.undo()
|
||||
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1', 'page1').handles.start.bindingId).toBe(
|
||||
bindingId
|
||||
)
|
||||
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeDefined()
|
||||
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeUndefined()
|
||||
|
||||
tlstate.redo()
|
||||
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1', 'page2').handles.start.bindingId).toBe(
|
||||
bindingId
|
||||
)
|
||||
expect(tlstate.document.pages['page1'].bindings[bindingId]).toBeUndefined()
|
||||
expect(tlstate.document.pages['page2'].bindings[bindingId]).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('when moving grouped shapes', () => {
|
||||
it('moves groups and their children', () => {
|
||||
tlstate
|
||||
.loadDocument(mockDocument)
|
||||
.createPage('page2')
|
||||
.changePage('page1')
|
||||
.group(['rect1', 'rect2'], 'groupA')
|
||||
.moveToPage('page2')
|
||||
|
||||
expect(tlstate.getShape('rect1', 'page1')).toBeUndefined()
|
||||
expect(tlstate.getShape('rect2', 'page1')).toBeUndefined()
|
||||
expect(tlstate.getShape('groupA', 'page1')).toBeUndefined()
|
||||
|
||||
expect(tlstate.getShape('rect1', 'page2')).toBeDefined()
|
||||
expect(tlstate.getShape('rect2', 'page2')).toBeDefined()
|
||||
expect(tlstate.getShape('groupA', 'page2')).toBeDefined()
|
||||
|
||||
tlstate.undo()
|
||||
|
||||
expect(tlstate.getShape('rect1', 'page2')).toBeUndefined()
|
||||
expect(tlstate.getShape('rect2', 'page2')).toBeUndefined()
|
||||
expect(tlstate.getShape('groupA', 'page2')).toBeUndefined()
|
||||
|
||||
expect(tlstate.getShape('rect1', 'page1')).toBeDefined()
|
||||
expect(tlstate.getShape('rect2', 'page1')).toBeDefined()
|
||||
expect(tlstate.getShape('groupA', 'page1')).toBeDefined()
|
||||
|
||||
tlstate.redo()
|
||||
|
||||
expect(tlstate.getShape('rect1', 'page1')).toBeUndefined()
|
||||
expect(tlstate.getShape('rect2', 'page1')).toBeUndefined()
|
||||
expect(tlstate.getShape('groupA', 'page1')).toBeUndefined()
|
||||
|
||||
expect(tlstate.getShape('rect1', 'page2')).toBeDefined()
|
||||
expect(tlstate.getShape('rect2', 'page2')).toBeDefined()
|
||||
expect(tlstate.getShape('groupA', 'page2')).toBeDefined()
|
||||
})
|
||||
|
||||
it('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')
|
||||
|
||||
expect(tlstate.getShape('rect1', 'page1')).toBeUndefined()
|
||||
expect(tlstate.getShape('rect1', 'page2')).toBeDefined()
|
||||
expect(tlstate.getShape('rect1', 'page2').parentId).toBe('page2')
|
||||
expect(tlstate.getShape('groupA', 'page1').children).toStrictEqual(['rect2', 'rect3'])
|
||||
|
||||
tlstate.undo()
|
||||
|
||||
expect(tlstate.getShape('rect1', 'page2')).toBeUndefined()
|
||||
expect(tlstate.getShape('rect1', 'page1').parentId).toBe('groupA')
|
||||
expect(tlstate.getShape('groupA', 'page1').children).toStrictEqual([
|
||||
'rect1',
|
||||
'rect2',
|
||||
'rect3',
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,211 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import type { ArrowShape, Data, PagePartial, TLDrawCommand, TLDrawShape } from '~types'
|
||||
import { TLDR } from '~state/tldr'
|
||||
import { Utils, Vec } from '@tldraw/core'
|
||||
|
||||
export function moveToPage(
|
||||
data: Data,
|
||||
ids: string[],
|
||||
fromPageId: string,
|
||||
toPageId: string
|
||||
): TLDrawCommand {
|
||||
const { currentPageId } = data.appState
|
||||
|
||||
const page = TLDR.getPage(data, currentPageId)
|
||||
|
||||
const fromPage: Record<string, PagePartial> = {
|
||||
before: {
|
||||
shapes: {},
|
||||
bindings: {},
|
||||
},
|
||||
after: {
|
||||
shapes: {},
|
||||
bindings: {},
|
||||
},
|
||||
}
|
||||
|
||||
const toPage: Record<string, PagePartial> = {
|
||||
before: {
|
||||
shapes: {},
|
||||
bindings: {},
|
||||
},
|
||||
after: {
|
||||
shapes: {},
|
||||
bindings: {},
|
||||
},
|
||||
}
|
||||
|
||||
// Collect all the shapes to move and their keys.
|
||||
const movingShapeIds = new Set<string>()
|
||||
const shapesToMove = new Set<TLDrawShape>()
|
||||
|
||||
ids
|
||||
.map((id) => TLDR.getShape(data, id, fromPageId))
|
||||
.forEach((shape) => {
|
||||
movingShapeIds.add(shape.id)
|
||||
shapesToMove.add(shape)
|
||||
if (shape.children !== undefined) {
|
||||
shape.children.forEach((childId) => {
|
||||
movingShapeIds.add(childId)
|
||||
shapesToMove.add(TLDR.getShape(data, childId, fromPageId))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Where should we put start putting shapes on the "to" page?
|
||||
const startingChildIndex = TLDR.getTopChildIndex(data, toPageId)
|
||||
|
||||
// Which shapes are we moving?
|
||||
const movingShapes = Array.from(shapesToMove.values())
|
||||
|
||||
movingShapes.forEach((shape, i) => {
|
||||
// Remove the shape from the fromPage
|
||||
fromPage.before.shapes[shape.id] = shape
|
||||
fromPage.after.shapes[shape.id] = undefined
|
||||
|
||||
// But the moved shape on the "to" page
|
||||
toPage.before.shapes[shape.id] = undefined
|
||||
toPage.after.shapes[shape.id] = shape
|
||||
|
||||
// If the shape's parent isn't moving too, reparent the shape to
|
||||
// the "to" page, at the top of the z stack
|
||||
if (!movingShapeIds.has(shape.parentId)) {
|
||||
toPage.after.shapes[shape.id] = {
|
||||
...shape,
|
||||
parentId: toPageId,
|
||||
childIndex: startingChildIndex + i,
|
||||
}
|
||||
|
||||
// If the shape was in a group, then pull the shape from the
|
||||
// parent's children array.
|
||||
if (shape.parentId !== fromPageId) {
|
||||
const parent = TLDR.getShape(data, shape.parentId, fromPageId)
|
||||
fromPage.before.shapes[parent.id] = {
|
||||
children: parent.children,
|
||||
}
|
||||
|
||||
fromPage.after.shapes[parent.id] = {
|
||||
children: parent.children!.filter((childId) => childId !== shape.id),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Handle bindings that effect duplicated shapes
|
||||
Object.values(page.bindings)
|
||||
.filter((binding) => movingShapeIds.has(binding.fromId) || movingShapeIds.has(binding.toId))
|
||||
.forEach((binding) => {
|
||||
// Always delete the binding from the from page
|
||||
|
||||
fromPage.before.bindings[binding.id] = binding
|
||||
fromPage.after.bindings[binding.id] = undefined
|
||||
|
||||
// Delete the reference from the binding's fromShape
|
||||
|
||||
const fromBoundShape = TLDR.getShape(data, binding.fromId, fromPageId)
|
||||
|
||||
// Will we be copying this binding to the new page?
|
||||
|
||||
const shouldCopy = movingShapeIds.has(binding.fromId) && movingShapeIds.has(binding.toId)
|
||||
|
||||
if (shouldCopy) {
|
||||
// Just move the binding to the new page
|
||||
toPage.before.bindings[binding.id] = undefined
|
||||
toPage.after.bindings[binding.id] = binding
|
||||
} else {
|
||||
if (movingShapeIds.has(binding.fromId)) {
|
||||
// If we are only moving the "from" shape, we need to delete
|
||||
// the binding reference from the "from" shapes handles
|
||||
const fromShape = TLDR.getShape(data, binding.fromId, fromPageId)
|
||||
const handle = Object.values(fromBoundShape.handles!).find(
|
||||
(handle) => handle.bindingId === binding.id
|
||||
)!
|
||||
|
||||
// Remove the handle from the shape on the toPage
|
||||
|
||||
const handleId = handle.id as keyof ArrowShape['handles']
|
||||
|
||||
const toPageShape = toPage.after.shapes[fromShape.id]!
|
||||
|
||||
toPageShape.handles = {
|
||||
...toPageShape.handles,
|
||||
[handleId]: {
|
||||
...toPageShape.handles![handleId],
|
||||
bindingId: undefined,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// If we are only moving the "to" shape, we need to delete
|
||||
// the binding reference from the "from" shape's handles
|
||||
const fromShape = TLDR.getShape(data, binding.fromId, fromPageId)
|
||||
const handle = Object.values(fromBoundShape.handles!).find(
|
||||
(handle) => handle.bindingId === binding.id
|
||||
)!
|
||||
|
||||
fromPage.before.shapes[fromShape.id] = {
|
||||
handles: { [handle.id]: { bindingId: binding.id } },
|
||||
}
|
||||
|
||||
fromPage.after.shapes[fromShape.id] = {
|
||||
handles: { [handle.id]: { bindingId: undefined } },
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Finally, center camera on selection
|
||||
|
||||
const toPageState = data.document.pageStates[toPageId]
|
||||
const bounds = Utils.getCommonBounds(movingShapes.map((shape) => TLDR.getBounds(shape)))
|
||||
const zoom = TLDR.getCameraZoom(
|
||||
window.innerWidth < window.innerHeight
|
||||
? (window.innerWidth - 128) / bounds.width
|
||||
: (window.innerHeight - 128) / bounds.height
|
||||
)
|
||||
const mx = (window.innerWidth - bounds.width * zoom) / 2 / zoom
|
||||
const my = (window.innerHeight - bounds.height * zoom) / 2 / zoom
|
||||
const point = Vec.round(Vec.add([-bounds.minX, -bounds.minY], [mx, my]))
|
||||
|
||||
return {
|
||||
id: 'move_to_page',
|
||||
before: {
|
||||
appState: {
|
||||
currentPageId: fromPageId,
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[fromPageId]: fromPage.before,
|
||||
[toPageId]: toPage.before,
|
||||
},
|
||||
pageStates: {
|
||||
[fromPageId]: { selectedIds: ids },
|
||||
[toPageId]: {
|
||||
selectedIds: toPageState.selectedIds,
|
||||
camera: toPageState.camera,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
after: {
|
||||
appState: {
|
||||
currentPageId: toPageId,
|
||||
},
|
||||
document: {
|
||||
pages: {
|
||||
[fromPageId]: fromPage.after,
|
||||
[toPageId]: toPage.after,
|
||||
},
|
||||
pageStates: {
|
||||
[fromPageId]: { selectedIds: [] },
|
||||
[toPageId]: {
|
||||
selectedIds: ids,
|
||||
camera: {
|
||||
zoom,
|
||||
point,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
import { TLDR } from '~state/tldr'
|
||||
import type { Data, GroupShape, PagePartial } from '~types'
|
||||
|
||||
export function removeShapesFromPage(data: Data, ids: string[], pageId: string) {
|
||||
const before: PagePartial = {
|
||||
shapes: {},
|
||||
bindings: {},
|
||||
}
|
||||
|
||||
const after: PagePartial = {
|
||||
shapes: {},
|
||||
bindings: {},
|
||||
}
|
||||
|
||||
const parentsToUpdate: GroupShape[] = []
|
||||
|
||||
const deletedIds = new Set()
|
||||
|
||||
// These are the shapes we're definitely going to delete
|
||||
|
||||
ids.forEach((id) => {
|
||||
deletedIds.add(id)
|
||||
const shape = TLDR.getShape(data, id, pageId)
|
||||
before.shapes[id] = shape
|
||||
after.shapes[id] = undefined
|
||||
|
||||
// Also delete the shape's children
|
||||
|
||||
if (shape.children !== undefined) {
|
||||
shape.children.forEach((childId) => {
|
||||
deletedIds.add(childId)
|
||||
const child = TLDR.getShape(data, childId, pageId)
|
||||
before.shapes[childId] = child
|
||||
after.shapes[childId] = undefined
|
||||
})
|
||||
}
|
||||
|
||||
if (shape.parentId !== pageId) {
|
||||
parentsToUpdate.push(TLDR.getShape(data, shape.parentId, pageId))
|
||||
}
|
||||
})
|
||||
|
||||
parentsToUpdate.forEach((parent) => {
|
||||
if (ids.includes(parent.id)) return
|
||||
deletedIds.add(parent.id)
|
||||
before.shapes[parent.id] = { children: parent.children }
|
||||
after.shapes[parent.id] = { children: parent.children.filter((id) => !ids.includes(id)) }
|
||||
})
|
||||
|
||||
// Recursively check for empty parents?
|
||||
|
||||
const page = TLDR.getPage(data, pageId)
|
||||
|
||||
// We also need to delete bindings that reference the deleted shapes
|
||||
Object.values(page.bindings).forEach((binding) => {
|
||||
for (const id of [binding.toId, binding.fromId]) {
|
||||
// If the binding references a deleted shape...
|
||||
if (after.shapes[id] === undefined) {
|
||||
// Delete this binding
|
||||
before.bindings[binding.id] = binding
|
||||
after.bindings[binding.id] = undefined
|
||||
|
||||
// Let's also look each the bound shape...
|
||||
const shape = TLDR.getShape(data, id, pageId)
|
||||
|
||||
// If the bound shape has a handle that references the deleted binding...
|
||||
if (shape.handles) {
|
||||
Object.values(shape.handles)
|
||||
.filter((handle) => handle.bindingId === binding.id)
|
||||
.forEach((handle) => {
|
||||
// Save the binding reference in the before patch
|
||||
before.shapes[id] = {
|
||||
...before.shapes[id],
|
||||
handles: {
|
||||
...before.shapes[id]?.handles,
|
||||
[handle.id]: { bindingId: binding.id },
|
||||
},
|
||||
}
|
||||
|
||||
// Unless we're currently deleting the shape, remove the
|
||||
// binding reference from the after patch
|
||||
if (!deletedIds.has(id)) {
|
||||
after.shapes[id] = {
|
||||
...after.shapes[id],
|
||||
handles: { ...after.shapes[id]?.handles, [handle.id]: { bindingId: undefined } },
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return { before, after }
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
import { TLDrawState } from '~state'
|
||||
import { mockDocument } from '~test'
|
||||
import { TLDR } from '~state/tldr'
|
||||
import { ArrowShape, TLDrawShape, TLDrawStatus } from '~types'
|
||||
import { ArrowShape, TLDrawShapeType, TLDrawStatus } from '~types'
|
||||
|
||||
describe('Arrow session', () => {
|
||||
const tlstate = new TLDrawState()
|
||||
|
@ -9,20 +8,9 @@ describe('Arrow session', () => {
|
|||
.loadDocument(mockDocument)
|
||||
.selectAll()
|
||||
.delete()
|
||||
.create(
|
||||
TLDR.getShapeUtils({ type: 'rectangle' } as TLDrawShape).create({
|
||||
id: 'target1',
|
||||
parentId: 'page1',
|
||||
point: [0, 0],
|
||||
size: [100, 100],
|
||||
})
|
||||
)
|
||||
.create(
|
||||
TLDR.getShapeUtils({ type: 'arrow' } as TLDrawShape).create({
|
||||
id: 'arrow1',
|
||||
parentId: 'page1',
|
||||
point: [200, 200],
|
||||
})
|
||||
.createShapes(
|
||||
{ type: TLDrawShapeType.Rectangle, id: 'target1', point: [0, 0], size: [100, 100] },
|
||||
{ type: TLDrawShapeType.Arrow, id: 'arrow1', point: [200, 200] }
|
||||
)
|
||||
|
||||
const restoreDoc = tlstate.document
|
||||
|
|
|
@ -317,71 +317,3 @@ export class ArrowSession implements Session {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findBinding(
|
||||
data: Data,
|
||||
shape: ArrowShape,
|
||||
handle: TLHandle,
|
||||
newBindingId: string,
|
||||
bindableShapeIds: string[],
|
||||
bindings: Record<string, TLDrawBinding | undefined>,
|
||||
altKey: boolean,
|
||||
metaKey: boolean
|
||||
) {
|
||||
let nextBinding: ArrowBinding | undefined = undefined
|
||||
let nextTarget: TLDrawShape | undefined = undefined
|
||||
|
||||
if (!altKey) {
|
||||
const oppositeHandle = shape.handles[handle.id === 'start' ? 'end' : 'start']
|
||||
|
||||
// Find the origin and direction of the handle
|
||||
const rayOrigin = Vec.add(oppositeHandle.point, shape.point)
|
||||
const rayPoint = Vec.add(handle.point, shape.point)
|
||||
const rayDirection = Vec.uni(Vec.sub(rayPoint, rayOrigin))
|
||||
|
||||
const oppositeBinding = oppositeHandle.bindingId
|
||||
? bindings[oppositeHandle.bindingId]
|
||||
: undefined
|
||||
|
||||
// From all bindable shapes on the page...
|
||||
for (const id of bindableShapeIds) {
|
||||
if (id === shape.id) continue
|
||||
if (id === shape.parentId) continue
|
||||
if (id === oppositeBinding?.toId) continue
|
||||
|
||||
const target = TLDR.getShape(data, id, data.appState.currentPageId)
|
||||
|
||||
const util = TLDR.getShapeUtils(target)
|
||||
|
||||
const bindingPoint = util.getBindingPoint(
|
||||
target,
|
||||
shape,
|
||||
rayPoint,
|
||||
rayOrigin,
|
||||
rayDirection,
|
||||
32,
|
||||
metaKey
|
||||
)
|
||||
|
||||
// Not all shapes will produce a binding point
|
||||
if (!bindingPoint) continue
|
||||
|
||||
// Stop at the first shape that will produce a binding point
|
||||
nextTarget = target
|
||||
|
||||
nextBinding = {
|
||||
id: newBindingId,
|
||||
type: 'arrow',
|
||||
fromId: shape.id,
|
||||
handleId: handle.id as keyof ArrowShape['handles'],
|
||||
toId: target.id,
|
||||
point: Vec.round(bindingPoint.point),
|
||||
distance: bindingPoint.distance,
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return { target: nextTarget, binding: nextBinding }
|
||||
}
|
||||
|
|
|
@ -871,6 +871,15 @@ export class TLDR {
|
|||
.reduce<TLDrawShape[]>((acc, shape) => [...acc, ...TLDR.flattenShape(data, shape)], [])
|
||||
}
|
||||
|
||||
static getTopChildIndex = (data: Data, pageId: string): number => {
|
||||
const shapes = TLDR.getShapes(data, pageId)
|
||||
return shapes.length === 0
|
||||
? 1
|
||||
: shapes
|
||||
.filter((shape) => shape.parentId === pageId)
|
||||
.sort((a, b) => b.childIndex - a.childIndex)[0].childIndex + 1
|
||||
}
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Assertions */
|
||||
/* -------------------------------------------------- */
|
||||
|
|
|
@ -996,7 +996,7 @@ export class TLDrawState extends StateManager<Data> {
|
|||
const bounds = Utils.getCommonBounds(Object.values(shapes).map(TLDR.getBounds))
|
||||
|
||||
const zoom = TLDR.getCameraZoom(
|
||||
bounds.width > bounds.height
|
||||
window.innerWidth < window.innerHeight
|
||||
? (window.innerWidth - 128) / bounds.width
|
||||
: (window.innerHeight - 128) / bounds.height
|
||||
)
|
||||
|
@ -1016,12 +1016,12 @@ export class TLDrawState extends StateManager<Data> {
|
|||
* @returns this
|
||||
*/
|
||||
zoomToSelection = (): this => {
|
||||
if (this.pageState.selectedIds.length === 0) return this
|
||||
if (this.selectedIds.length === 0) return this
|
||||
|
||||
const bounds = TLDR.getSelectedBounds(this.state)
|
||||
|
||||
const zoom = TLDR.getCameraZoom(
|
||||
bounds.width > bounds.height
|
||||
window.innerWidth < window.innerHeight
|
||||
? (window.innerWidth - 128) / bounds.width
|
||||
: (window.innerHeight - 128) / bounds.height
|
||||
)
|
||||
|
@ -1031,7 +1031,7 @@ export class TLDrawState extends StateManager<Data> {
|
|||
|
||||
return this.setCamera(
|
||||
Vec.round(Vec.add([-bounds.minX, -bounds.minY], [mx, my])),
|
||||
this.pageState.camera.zoom,
|
||||
zoom,
|
||||
`zoomed_to_selection`
|
||||
)
|
||||
}
|
||||
|
@ -1157,6 +1157,11 @@ export class TLDrawState extends StateManager<Data> {
|
|||
* @returns this
|
||||
*/
|
||||
select = (...ids: string[]): this => {
|
||||
ids.forEach((id) => {
|
||||
if (!this.page.shapes[id]) {
|
||||
throw Error(`That shape does not exist on page ${this.currentPageId}`)
|
||||
}
|
||||
})
|
||||
this.setSelectedIds(ids)
|
||||
this.addToSelectHistory(ids)
|
||||
return this
|
||||
|
@ -1644,6 +1649,24 @@ export class TLDrawState extends StateManager<Data> {
|
|||
return this.setState(Commands.flip(this.state, ids, FlipType.Vertical))
|
||||
}
|
||||
|
||||
/**
|
||||
* Move one or more shapes to a new page. Will also break or move bindings.
|
||||
* @param toPage The id of the page to move the shapes to.
|
||||
* @param fromPage The id of the page to move the shapes from
|
||||
*(defaults to current page).
|
||||
* @param ids The ids of the shapes to move (defaults to selection).
|
||||
* @returns this
|
||||
*/
|
||||
moveToPage = (
|
||||
toPageId: string,
|
||||
fromPageId = this.currentPageId,
|
||||
ids = this.selectedIds
|
||||
): this => {
|
||||
if (ids.length === 0) return this
|
||||
this.setState(Commands.moveToPage(this.state, ids, fromPageId, toPageId))
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Move one or more shapes to the back of the page.
|
||||
* @param ids The ids of the shapes to change (defaults to selection).
|
||||
|
|
Loading…
Reference in a new issue