Просмотр исходного кода

[fix] Ungrouping (#144)

* Adds auto ungroup if grouping only a grouped shape

* Adds test for multiple shape ungrouping
main
Steve Ruiz 4 лет назад
Родитель
Сommit
74a8a40af8
Аккаунт пользователя с таким Email не найден

+ 30
- 0
packages/tldraw/src/state/command/group/group.command.spec.ts Просмотреть файл

@@ -318,6 +318,36 @@ describe('Group command', () => {
318 318
       ])
319 319
     })
320 320
 
321
+    it('Ungroups if the only shape selected is a group', () => {
322
+      tlstate.resetDocument().createShapes(
323
+        {
324
+          id: 'rect1',
325
+          type: TLDrawShapeType.Rectangle,
326
+          childIndex: 1,
327
+        },
328
+        {
329
+          id: 'rect2',
330
+          type: TLDrawShapeType.Rectangle,
331
+          childIndex: 2,
332
+        },
333
+        {
334
+          id: 'rect3',
335
+          type: TLDrawShapeType.Rectangle,
336
+          childIndex: 3,
337
+        }
338
+      )
339
+
340
+      expect(tlstate.shapes.length).toBe(3)
341
+
342
+      tlstate.selectAll().group()
343
+
344
+      expect(tlstate.shapes.length).toBe(4)
345
+
346
+      tlstate.selectAll().group()
347
+
348
+      expect(tlstate.shapes.length).toBe(3)
349
+    })
350
+
321 351
     /*
322 352
       The layers should be in the same order as the original layers as
323 353
       they would have appeared on a layers tree (lowest child index

+ 45
- 2
packages/tldraw/src/state/command/ungroup/ungroup.command.spec.ts Просмотреть файл

@@ -37,13 +37,29 @@ describe('Ungroup command', () => {
37 37
         .loadDocument(mockDocument)
38 38
         .group(['rect1', 'rect2'], 'groupA')
39 39
         .createPage('page2')
40
-        .ungroup('groupA', 'page1')
40
+        .ungroup(['groupA'], 'page1')
41 41
 
42 42
       expect(tlstate.getShape('groupA', 'page1')).toBeUndefined()
43 43
       tlstate.undo()
44 44
       expect(tlstate.getShape('groupA', 'page1')).toBeDefined()
45 45
     })
46 46
 
47
+    it('Ungroups multiple selected groups', () => {
48
+      tlstate
49
+        .loadDocument(mockDocument)
50
+        .createShapes({
51
+          id: 'rect4',
52
+          type: TLDrawShapeType.Rectangle,
53
+        })
54
+        .group(['rect1', 'rect2'], 'groupA')
55
+        .group(['rect3', 'rect4'], 'groupB')
56
+        .selectAll()
57
+        .ungroup()
58
+
59
+      expect(tlstate.getShape('groupA', 'page1')).toBeUndefined()
60
+      expect(tlstate.getShape('groupB', 'page1')).toBeUndefined()
61
+    })
62
+
47 63
     it('Does not ungroup if a group shape is not selected', () => {
48 64
       tlstate.loadDocument(mockDocument).select('rect1')
49 65
       const before = tlstate.state
@@ -52,6 +68,33 @@ describe('Ungroup command', () => {
52 68
       expect(tlstate.state).toStrictEqual(before)
53 69
     })
54 70
 
71
+    it('Correctly selects children after ungrouping', () => {
72
+      const tlstate = new TLDrawState()
73
+        .createShapes(
74
+          {
75
+            id: 'rect1',
76
+            type: TLDrawShapeType.Rectangle,
77
+            childIndex: 1,
78
+          },
79
+          {
80
+            id: 'rect2',
81
+            type: TLDrawShapeType.Rectangle,
82
+            childIndex: 2,
83
+          },
84
+          {
85
+            id: 'rect3',
86
+            type: TLDrawShapeType.Rectangle,
87
+            childIndex: 3,
88
+          }
89
+        )
90
+        .group(['rect1', 'rect2'], 'groupA')
91
+        .selectAll()
92
+        .ungroup()
93
+
94
+      // State should not have changed
95
+      expect(tlstate.selectedIds).toStrictEqual(['rect3', 'rect1', 'rect2'])
96
+    })
97
+
55 98
     it('Reparents shapes to the page at the correct childIndex', () => {
56 99
       const tlstate = new TLDrawState()
57 100
         .createShapes(
@@ -80,7 +123,7 @@ describe('Ungroup command', () => {
80 123
       expect(tlstate.getShape('rect2').childIndex).toBe(2)
81 124
       expect(tlstate.getShape('rect3').childIndex).toBe(3)
82 125
 
83
-      tlstate.ungroup('groupA')
126
+      tlstate.ungroup()
84 127
 
85 128
       expect(tlstate.getShape('rect1').childIndex).toBe(1)
86 129
       expect(tlstate.getShape('rect2').childIndex).toBe(2)

+ 85
- 77
packages/tldraw/src/state/command/ungroup/ungroup.command.ts Просмотреть файл

@@ -3,100 +3,108 @@ import type { Data, TLDrawCommand } from '~types'
3 3
 import { TLDR } from '~state/tldr'
4 4
 import type { Patch } from 'rko'
5 5
 
6
-export function ungroup(data: Data, groupId: string, pageId: string): TLDrawCommand | undefined {
6
+export function ungroup(
7
+  data: Data,
8
+  selectedIds: string[],
9
+  groupShapes: GroupShape[],
10
+  pageId: string
11
+): TLDrawCommand | undefined {
7 12
   const beforeShapes: Record<string, Patch<TLDrawShape | undefined>> = {}
8 13
   const afterShapes: Record<string, Patch<TLDrawShape | undefined>> = {}
9 14
 
10 15
   const beforeBindings: Record<string, Patch<TLDrawBinding | undefined>> = {}
11 16
   const afterBindings: Record<string, Patch<TLDrawBinding | undefined>> = {}
12 17
 
13
-  // The group shape
14
-  const groupShape = TLDR.getShape<GroupShape>(data, groupId, pageId)
15
-
16
-  const idsToUngroup = groupShape.children
17
-  const shapesToUngroup: TLDrawShape[] = []
18
-  const deletedGroupIds: string[] = []
19
-
20
-  // Collect all of the shapes to group (and their ids)
21
-  for (const id of idsToUngroup) {
22
-    const shape = TLDR.getShape(data, id, pageId)
23
-    shapesToUngroup.push(shape)
24
-  }
18
+  const beforeSelectedIds = selectedIds
19
+  const afterSelectedIds = selectedIds.filter((id) => !groupShapes.find((shape) => shape.id === id))
25 20
 
26
-  // We'll start placing the shapes at this childIndex
27
-  const startingChildIndex = groupShape.childIndex
21
+  // The group shape
22
+  groupShapes.forEach((groupShape) => {
23
+    const shapesToReparent: TLDrawShape[] = []
24
+    const deletedGroupIds: string[] = []
25
+
26
+    // Remove the group shape in the next state
27
+    beforeShapes[groupShape.id] = groupShape
28
+    afterShapes[groupShape.id] = undefined
29
+
30
+    // Select its children in the next state
31
+    groupShape.children.forEach((id) => {
32
+      afterSelectedIds.push(id)
33
+      const shape = TLDR.getShape(data, id, pageId)
34
+      shapesToReparent.push(shape)
35
+    })
28 36
 
29
-  // And we'll need to fit them under this child index
30
-  const endingChildIndex = TLDR.getChildIndexAbove(data, groupShape.id, pageId)
37
+    // We'll start placing the shapes at this childIndex
38
+    const startingChildIndex = groupShape.childIndex
31 39
 
32
-  const step = (endingChildIndex - startingChildIndex) / shapesToUngroup.length
40
+    // And we'll need to fit them under this child index
41
+    const endingChildIndex = TLDR.getChildIndexAbove(data, groupShape.id, pageId)
33 42
 
34
-  // An array of shapes in order by their child index
35
-  const sortedShapes = shapesToUngroup.sort((a, b) => a.childIndex - b.childIndex)
43
+    const step = (endingChildIndex - startingChildIndex) / shapesToReparent.length
36 44
 
37
-  // Remove the group shape
38
-  beforeShapes[groupId] = groupShape
39
-  afterShapes[groupId] = undefined
45
+    // An array of shapes in order by their child index
46
+    const sortedShapes = shapesToReparent.sort((a, b) => a.childIndex - b.childIndex)
40 47
 
41
-  // Reparent shapes to the page
42
-  sortedShapes.forEach((shape, index) => {
43
-    beforeShapes[shape.id] = {
44
-      parentId: shape.parentId,
45
-      childIndex: shape.childIndex,
46
-    }
48
+    // Reparent shapes to the page
49
+    sortedShapes.forEach((shape, index) => {
50
+      beforeShapes[shape.id] = {
51
+        parentId: shape.parentId,
52
+        childIndex: shape.childIndex,
53
+      }
47 54
 
48
-    afterShapes[shape.id] = {
49
-      parentId: pageId,
50
-      childIndex: startingChildIndex + step * index,
51
-    }
52
-  })
55
+      afterShapes[shape.id] = {
56
+        parentId: pageId,
57
+        childIndex: startingChildIndex + step * index,
58
+      }
59
+    })
53 60
 
54
-  const page = TLDR.getPage(data, pageId)
55
-
56
-  // We also need to delete bindings that reference the deleted shapes
57
-  Object.values(page.bindings)
58
-    .filter((binding) => binding.toId === groupId || binding.fromId === groupId)
59
-    .forEach((binding) => {
60
-      for (const id of [binding.toId, binding.fromId]) {
61
-        // If the binding references the deleted group...
62
-        if (afterShapes[id] === undefined) {
63
-          // Delete the binding
64
-          beforeBindings[binding.id] = binding
65
-          afterBindings[binding.id] = undefined
66
-
67
-          // Let's also look each the bound shape...
68
-          const shape = TLDR.getShape(data, id, pageId)
69
-
70
-          // If the bound shape has a handle that references the deleted binding...
71
-          if (shape.handles) {
72
-            Object.values(shape.handles)
73
-              .filter((handle) => handle.bindingId === binding.id)
74
-              .forEach((handle) => {
75
-                // Save the binding reference in the before patch
76
-                beforeShapes[id] = {
77
-                  ...beforeShapes[id],
78
-                  handles: {
79
-                    ...beforeShapes[id]?.handles,
80
-                    [handle.id]: { bindingId: binding.id },
81
-                  },
82
-                }
83
-
84
-                // Unless we're currently deleting the shape, remove the
85
-                // binding reference from the after patch
86
-                if (!deletedGroupIds.includes(id)) {
87
-                  afterShapes[id] = {
88
-                    ...afterShapes[id],
61
+    const page = TLDR.getPage(data, pageId)
62
+
63
+    // We also need to delete bindings that reference the deleted shapes
64
+    Object.values(page.bindings)
65
+      .filter((binding) => binding.toId === groupShape.id || binding.fromId === groupShape.id)
66
+      .forEach((binding) => {
67
+        for (const id of [binding.toId, binding.fromId]) {
68
+          // If the binding references the deleted group...
69
+          if (afterShapes[id] === undefined) {
70
+            // Delete the binding
71
+            beforeBindings[binding.id] = binding
72
+            afterBindings[binding.id] = undefined
73
+
74
+            // Let's also look each the bound shape...
75
+            const shape = TLDR.getShape(data, id, pageId)
76
+
77
+            // If the bound shape has a handle that references the deleted binding...
78
+            if (shape.handles) {
79
+              Object.values(shape.handles)
80
+                .filter((handle) => handle.bindingId === binding.id)
81
+                .forEach((handle) => {
82
+                  // Save the binding reference in the before patch
83
+                  beforeShapes[id] = {
84
+                    ...beforeShapes[id],
89 85
                     handles: {
90
-                      ...afterShapes[id]?.handles,
91
-                      [handle.id]: { bindingId: undefined },
86
+                      ...beforeShapes[id]?.handles,
87
+                      [handle.id]: { bindingId: binding.id },
92 88
                     },
93 89
                   }
94
-                }
95
-              })
90
+
91
+                  // Unless we're currently deleting the shape, remove the
92
+                  // binding reference from the after patch
93
+                  if (!deletedGroupIds.includes(id)) {
94
+                    afterShapes[id] = {
95
+                      ...afterShapes[id],
96
+                      handles: {
97
+                        ...afterShapes[id]?.handles,
98
+                        [handle.id]: { bindingId: undefined },
99
+                      },
100
+                    }
101
+                  }
102
+                })
103
+            }
96 104
           }
97 105
         }
98
-      }
99
-    })
106
+      })
107
+  })
100 108
 
101 109
   return {
102 110
     id: 'ungroup',
@@ -110,7 +118,7 @@ export function ungroup(data: Data, groupId: string, pageId: string): TLDrawComm
110 118
         },
111 119
         pageStates: {
112 120
           [pageId]: {
113
-            selectedIds: [groupId],
121
+            selectedIds: beforeSelectedIds,
114 122
           },
115 123
         },
116 124
       },
@@ -125,7 +133,7 @@ export function ungroup(data: Data, groupId: string, pageId: string): TLDrawComm
125 133
         },
126 134
         pageStates: {
127 135
           [pageId]: {
128
-            selectedIds: idsToUngroup,
136
+            selectedIds: afterSelectedIds,
129 137
           },
130 138
         },
131 139
       },

+ 12
- 4
packages/tldraw/src/state/tlstate.ts Просмотреть файл

@@ -2225,7 +2225,12 @@ export class TLDrawState extends StateManager<Data> {
2225 2225
     groupId = Utils.uniqueId(),
2226 2226
     pageId = this.currentPageId
2227 2227
   ): this => {
2228
+    if (ids.length === 1 && this.getShape(ids[0], pageId).type === TLDrawShapeType.Group) {
2229
+      return this.ungroup(ids, pageId)
2230
+    }
2231
+
2228 2232
     if (ids.length < 2) return this
2233
+
2229 2234
     const command = Commands.group(this.state, ids, groupId, pageId)
2230 2235
     if (!command) return this
2231 2236
     return this.setState(command)
@@ -2235,11 +2240,14 @@ export class TLDrawState extends StateManager<Data> {
2235 2240
    * Ungroup the selected groups.
2236 2241
    * @todo
2237 2242
    */
2238
-  ungroup = (groupId = this.selectedIds[0], pageId = this.currentPageId): this => {
2239
-    const shape = this.getShape(groupId, pageId)
2240
-    if (shape.type !== TLDrawShapeType.Group) return this
2243
+  ungroup = (ids = this.selectedIds, pageId = this.currentPageId): this => {
2244
+    const groups = ids
2245
+      .map((id) => this.getShape(id, pageId))
2246
+      .filter((shape) => shape.type === TLDrawShapeType.Group)
2247
+
2248
+    if (groups.length === 0) return this
2241 2249
 
2242
-    const command = Commands.ungroup(this.state, groupId, pageId)
2250
+    const command = Commands.ungroup(this.state, ids, groups as GroupShape[], pageId)
2243 2251
     if (!command) return this
2244 2252
     return this.setState(command)
2245 2253
   }

Загрузка…
Отмена
Сохранить