Parcourir la source

Adds context menu, "move to page"

main
Steve Ruiz il y a 4 ans
Parent
révision
f2d3231315

+ 1
- 1
components/canvas/bounds/bounding-box.tsx Voir le fichier

@@ -51,7 +51,7 @@ export default function Bounds() {
51 51
 
52 52
   if (isSingleHandles) return null
53 53
 
54
-  const size = (isMobile().any ? 10 : 8) / zoom // Touch target size
54
+  const size = (isMobile() ? 10 : 8) / zoom // Touch target size
55 55
   const center = getBoundsCenter(bounds)
56 56
 
57 57
   return (

+ 7
- 4
components/canvas/bounds/bounds-bg.tsx Voir le fichier

@@ -5,15 +5,18 @@ import styled from 'styles'
5 5
 import { deepCompareArrays, getPage } from 'utils/utils'
6 6
 
7 7
 function handlePointerDown(e: React.PointerEvent<SVGRectElement>) {
8
-  if (e.buttons !== 1) return
9 8
   if (!inputs.canAccept(e.pointerId)) return
10 9
   e.stopPropagation()
11 10
   e.currentTarget.setPointerCapture(e.pointerId)
12
-  state.send('POINTED_BOUNDS', inputs.pointerDown(e, 'bounds'))
11
+
12
+  if (e.button === 0) {
13
+    state.send('POINTED_BOUNDS', inputs.pointerDown(e, 'bounds'))
14
+  } else if (e.button === 2) {
15
+    state.send('RIGHT_POINTED', inputs.pointerDown(e, 'bounds'))
16
+  }
13 17
 }
14 18
 
15 19
 function handlePointerUp(e: React.PointerEvent<SVGRectElement>) {
16
-  if (e.buttons !== 1) return
17 20
   if (!inputs.canAccept(e.pointerId)) return
18 21
   e.stopPropagation()
19 22
   e.currentTarget.releasePointerCapture(e.pointerId)
@@ -36,7 +39,7 @@ export default function BoundsBg() {
36 39
     if (selectedIds.length === 1) {
37 40
       const { shapes } = getPage(s.data)
38 41
       const selected = Array.from(s.values.selectedIds.values())[0]
39
-      return shapes[selected].rotation
42
+      return shapes[selected]?.rotation
40 43
     } else {
41 44
       return 0
42 45
     }

+ 3
- 1
components/canvas/bounds/handles.tsx Voir le fichier

@@ -21,10 +21,12 @@ export default function Handles() {
21 21
     s.isInAny('notPointing', 'pinching', 'translatingHandles')
22 22
   )
23 23
 
24
-  if (!shape.handles || !isSelecting) return null
24
+  if (!shape || !shape.handles || !isSelecting) return null
25 25
 
26 26
   const center = getShapeUtils(shape).getCenter(shape)
27 27
 
28
+  console.log(shape)
29
+
28 30
   return (
29 31
     <g transform={`rotate(${shape.rotation * (180 / Math.PI)},${center})`}>
30 32
       {Object.values(shape.handles).map((handle) => (

+ 17
- 15
components/canvas/canvas.tsx Voir le fichier

@@ -1,7 +1,6 @@
1 1
 import styled from 'styles'
2 2
 import state, { useSelector } from 'state'
3
-import inputs from 'state/inputs'
4
-import React, { useCallback, useRef } from 'react'
3
+import React, { useRef } from 'react'
5 4
 import useZoomEvents from 'hooks/useZoomEvents'
6 5
 import useCamera from 'hooks/useCamera'
7 6
 import Defs from './defs'
@@ -12,6 +11,7 @@ import BoundsBg from './bounds/bounds-bg'
12 11
 import Selected from './selected'
13 12
 import Handles from './bounds/handles'
14 13
 import useCanvasEvents from 'hooks/useCanvasEvents'
14
+import ContextMenu from 'components/context-menu'
15 15
 
16 16
 export default function Canvas() {
17 17
   const rCanvas = useRef<SVGSVGElement>(null)
@@ -26,19 +26,21 @@ export default function Canvas() {
26 26
   const isReady = useSelector((s) => s.isIn('ready'))
27 27
 
28 28
   return (
29
-    <MainSVG ref={rCanvas} {...events}>
30
-      <Defs />
31
-      {isReady && (
32
-        <g ref={rGroup}>
33
-          <BoundsBg />
34
-          <Page />
35
-          <Selected />
36
-          <Bounds />
37
-          <Handles />
38
-          <Brush />
39
-        </g>
40
-      )}
41
-    </MainSVG>
29
+    <ContextMenu>
30
+      <MainSVG ref={rCanvas} {...events}>
31
+        <Defs />
32
+        {isReady && (
33
+          <g ref={rGroup}>
34
+            <BoundsBg />
35
+            <Page />
36
+            <Selected />
37
+            <Bounds />
38
+            <Handles />
39
+            <Brush />
40
+          </g>
41
+        )}
42
+      </MainSVG>
43
+    </ContextMenu>
42 44
   )
43 45
 }
44 46
 

+ 2
- 0
components/canvas/shape.tsx Voir le fichier

@@ -7,6 +7,7 @@ import { ShapeStyles, ShapeType } from 'types'
7 7
 import useShapeEvents from 'hooks/useShapeEvents'
8 8
 import * as vec from 'utils/vec'
9 9
 import { getShapeStyle } from 'lib/shape-styles'
10
+import ContextMenu from 'components/context-menu'
10 11
 
11 12
 interface ShapeProps {
12 13
   id: string
@@ -51,6 +52,7 @@ function Shape({ id, isSelecting, parentPoint }: ShapeProps) {
51 52
           {...events}
52 53
         />
53 54
       )}
55
+
54 56
       {!shape.isHidden && <RealShape isGroup={isGroup} id={id} style={style} />}
55 57
       {isGroup &&
56 58
         shape.children.map((shapeId) => (

+ 297
- 0
components/context-menu.tsx Voir le fichier

@@ -0,0 +1,297 @@
1
+import * as _ContextMenu from '@radix-ui/react-context-menu'
2
+import * as _Dropdown from '@radix-ui/react-dropdown-menu'
3
+import styled from 'styles'
4
+import { RowButton } from './shared'
5
+import {
6
+  commandKey,
7
+  deepCompareArrays,
8
+  getSelectedShapes,
9
+  isMobile,
10
+} from 'utils/utils'
11
+import state, { useSelector } from 'state'
12
+import { MoveType, ShapeType } from 'types'
13
+import React, { useRef } from 'react'
14
+
15
+export default function ContextMenu({
16
+  children,
17
+}: {
18
+  children: React.ReactNode
19
+}) {
20
+  const selectedShapes = useSelector(
21
+    (s) => getSelectedShapes(s.data),
22
+    deepCompareArrays
23
+  )
24
+
25
+  const rContent = useRef<HTMLDivElement>(null)
26
+
27
+  const hasGroupSelectd = selectedShapes.some((s) => s.type === ShapeType.Group)
28
+  const hasMultipleSelected = selectedShapes.length > 1
29
+
30
+  return (
31
+    <_ContextMenu.Root>
32
+      <_ContextMenu.Trigger>{children}</_ContextMenu.Trigger>
33
+      <StyledContent ref={rContent} isMobile={isMobile()}>
34
+        {selectedShapes.length ? (
35
+          <>
36
+            {/* <Button onSelect={() => state.send('COPIED')}>
37
+              <span>Copy</span>
38
+              <kbd>
39
+                <span>{commandKey()}</span>
40
+                <span>C</span>
41
+              </kbd>
42
+            </Button>
43
+            <Button onSelect={() => state.send('CUT')}>
44
+              <span>Cut</span>
45
+              <kbd>
46
+                <span>{commandKey()}</span>
47
+                <span>X</span>
48
+              </kbd>
49
+            </Button>
50
+             */}
51
+            <Button onSelect={() => state.send('DUPLICATED')}>
52
+              <span>Duplicate</span>
53
+              <kbd>
54
+                <span>{commandKey()}</span>
55
+                <span>D</span>
56
+              </kbd>
57
+            </Button>
58
+            <StyledDivider />
59
+            <Button
60
+              onSelect={() =>
61
+                state.send('MOVED', {
62
+                  type: MoveType.ToFront,
63
+                })
64
+              }
65
+            >
66
+              <span>Move To Front</span>
67
+              <kbd>
68
+                <span>{commandKey()}</span>
69
+                <span>⇧</span>
70
+                <span>]</span>
71
+              </kbd>
72
+            </Button>
73
+
74
+            <Button
75
+              onSelect={() =>
76
+                state.send('MOVED', {
77
+                  type: MoveType.Forward,
78
+                })
79
+              }
80
+            >
81
+              <span>Move Forward</span>
82
+              <kbd>
83
+                <span>{commandKey()}</span>
84
+                <span>]</span>
85
+              </kbd>
86
+            </Button>
87
+            <Button
88
+              onSelect={() =>
89
+                state.send('MOVED', {
90
+                  type: MoveType.Backward,
91
+                })
92
+              }
93
+            >
94
+              <span>Move Backward</span>
95
+              <kbd>
96
+                <span>{commandKey()}</span>
97
+                <span>[</span>
98
+              </kbd>
99
+            </Button>
100
+            <Button
101
+              onSelect={() =>
102
+                state.send('MOVED', {
103
+                  type: MoveType.ToBack,
104
+                })
105
+              }
106
+            >
107
+              <span>Move to Back</span>
108
+              <kbd>
109
+                <span>{commandKey()}</span>
110
+                <span>⇧</span>
111
+                <span>[</span>
112
+              </kbd>
113
+            </Button>
114
+            {hasGroupSelectd ||
115
+              (hasMultipleSelected && (
116
+                <>
117
+                  <StyledDivider />
118
+                  {hasGroupSelectd && (
119
+                    <Button onSelect={() => state.send('UNGROUPED')}>
120
+                      <span>Ungroup</span>
121
+                      <kbd>
122
+                        <span>{commandKey()}</span>
123
+                        <span>⇧</span>
124
+                        <span>G</span>
125
+                      </kbd>
126
+                    </Button>
127
+                  )}
128
+                  {hasMultipleSelected && (
129
+                    <Button onSelect={() => state.send('GROUPED')}>
130
+                      <span>Group</span>
131
+                      <kbd>
132
+                        <span>{commandKey()}</span>
133
+                        <span>G</span>
134
+                      </kbd>
135
+                    </Button>
136
+                  )}
137
+                </>
138
+              ))}
139
+            <StyledDivider />
140
+
141
+            {/* <Button onSelect={() => state.send('MOVED_TO_PAGE')}> */}
142
+            <_ContextMenu.Item>
143
+              <MoveToPageDropDown>Move to Page</MoveToPageDropDown>
144
+            </_ContextMenu.Item>
145
+            {/* </Button> */}
146
+            <Button onSelect={() => state.send('DELETED')}>
147
+              <span>Delete</span>
148
+              <kbd>
149
+                <span>⌫</span>
150
+              </kbd>
151
+            </Button>
152
+          </>
153
+        ) : (
154
+          <>
155
+            <Button onSelect={() => state.send('UNDO')}>
156
+              <span>Undo</span>
157
+              <kbd>
158
+                <span>{commandKey()}</span>
159
+                <span>Z</span>
160
+              </kbd>
161
+            </Button>
162
+            <Button onSelect={() => state.send('REDO')}>
163
+              <span>Redo</span>
164
+              <kbd>
165
+                <span>{commandKey()}</span>
166
+                <span>⇧</span>
167
+                <span>Z</span>
168
+              </kbd>
169
+            </Button>
170
+          </>
171
+        )}
172
+      </StyledContent>
173
+    </_ContextMenu.Root>
174
+  )
175
+}
176
+
177
+const StyledContent = styled(_ContextMenu.Content, {
178
+  position: 'relative',
179
+  backgroundColor: '$panel',
180
+  borderRadius: '4px',
181
+  overflow: 'hidden',
182
+  pointerEvents: 'all',
183
+  userSelect: 'none',
184
+  zIndex: 200,
185
+  padding: 2,
186
+  border: '1px solid $panel',
187
+  boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
188
+  minWidth: 128,
189
+
190
+  '& kbd': {
191
+    marginLeft: '32px',
192
+    fontSize: '$1',
193
+    fontFamily: '$ui',
194
+  },
195
+
196
+  '& kbd > span': {
197
+    display: 'inline-block',
198
+    width: '12px',
199
+  },
200
+
201
+  variants: {
202
+    isMobile: {
203
+      true: {
204
+        '& kbd': {
205
+          display: 'none',
206
+        },
207
+      },
208
+    },
209
+  },
210
+})
211
+
212
+const StyledDivider = styled(_ContextMenu.Separator, {
213
+  backgroundColor: '$hover',
214
+  height: 1,
215
+  margin: '2px -2px',
216
+})
217
+
218
+function Button({
219
+  onSelect,
220
+  children,
221
+  disabled = false,
222
+}: {
223
+  onSelect: () => void
224
+  disabled?: boolean
225
+  children: React.ReactNode
226
+}) {
227
+  return (
228
+    <_ContextMenu.Item
229
+      as={RowButton}
230
+      disabled={disabled}
231
+      bp={{ '@initial': 'mobile', '@sm': 'small' }}
232
+      onSelect={onSelect}
233
+    >
234
+      {children}
235
+    </_ContextMenu.Item>
236
+  )
237
+}
238
+
239
+function MoveToPageDropDown({ children }: { children: React.ReactNode }) {
240
+  const documentPages = useSelector((s) => s.data.document.pages)
241
+  const currentPageId = useSelector((s) => s.data.currentPageId)
242
+
243
+  if (!documentPages[currentPageId]) return null
244
+
245
+  const sorted = Object.values(documentPages)
246
+    .sort((a, b) => a.childIndex - b.childIndex)
247
+    .filter((a) => a.id !== currentPageId)
248
+
249
+  return (
250
+    <_Dropdown.Root>
251
+      <_Dropdown.Trigger
252
+        as={RowButton}
253
+        bp={{ '@initial': 'mobile', '@sm': 'small' }}
254
+      >
255
+        {children}
256
+      </_Dropdown.Trigger>
257
+      <StyledDialogContent side="right" sideOffset={8}>
258
+        {sorted.map(({ id, name }) => (
259
+          <_Dropdown.Item
260
+            as={RowButton}
261
+            key={id}
262
+            bp={{ '@initial': 'mobile', '@sm': 'small' }}
263
+            disabled={id === currentPageId}
264
+            onSelect={() => state.send('MOVED_TO_PAGE', { id })}
265
+          >
266
+            <span>{name}</span>
267
+          </_Dropdown.Item>
268
+        ))}
269
+      </StyledDialogContent>
270
+    </_Dropdown.Root>
271
+  )
272
+}
273
+
274
+const StyledDialogContent = styled(_Dropdown.Content, {
275
+  // position: 'fixed',
276
+  // top: '50%',
277
+  // left: '50%',
278
+  // transform: 'translate(-50%, -50%)',
279
+  // minWidth: 200,
280
+  // maxWidth: 'fit-content',
281
+  // maxHeight: '85vh',
282
+  // marginTop: '-5vh',
283
+  minWidth: 128,
284
+  backgroundColor: '$panel',
285
+  borderRadius: '4px',
286
+  overflow: 'hidden',
287
+  pointerEvents: 'all',
288
+  userSelect: 'none',
289
+  zIndex: 200,
290
+  padding: 2,
291
+  border: '1px solid $panel',
292
+  boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
293
+
294
+  '&:focus': {
295
+    outline: 'none',
296
+  },
297
+})

+ 1
- 5
components/editor.tsx Voir le fichier

@@ -9,6 +9,7 @@ import StylePanel from './style-panel/style-panel'
9 9
 import { useSelector } from 'state'
10 10
 import styled from 'styles'
11 11
 import PagePanel from './page-panel/page-panel'
12
+import ContextMenu from './context-menu'
12 13
 
13 14
 export default function Editor() {
14 15
   useKeyboardEvents()
@@ -25,11 +26,6 @@ export default function Editor() {
25 26
       <Spacer />
26 27
       <StylePanel />
27 28
       <Canvas />
28
-
29
-      {/* <LeftPanels>
30
-        <CodePanel />
31
-        {hasControls && <ControlsPanel />}
32
-      </LeftPanels> */}
33 29
       <ToolsPanel />
34 30
       <StatusBar />
35 31
     </Layout>

+ 4
- 0
components/shared.tsx Voir le fichier

@@ -105,6 +105,10 @@ export const RowButton = styled('button', {
105 105
     zIndex: 1,
106 106
   },
107 107
 
108
+  '& :disabled': {
109
+    opacity: 0.5,
110
+  },
111
+
108 112
   variants: {
109 113
     bp: {
110 114
       mobile: {},

+ 7
- 1
hooks/useCanvasEvents.ts Voir le fichier

@@ -9,8 +9,14 @@ export default function useCanvasEvents(
9 9
 ) {
10 10
   const handlePointerDown = useCallback((e: React.PointerEvent) => {
11 11
     if (!inputs.canAccept(e.pointerId)) return
12
+
12 13
     rCanvas.current.setPointerCapture(e.pointerId)
13
-    state.send('POINTED_CANVAS', inputs.pointerDown(e, 'canvas'))
14
+
15
+    if (e.button === 0) {
16
+      state.send('POINTED_CANVAS', inputs.pointerDown(e, 'canvas'))
17
+    } else if (e.button === 2) {
18
+      state.send('RIGHT_POINTED', inputs.pointerDown(e, 'canvas'))
19
+    }
14 20
   }, [])
15 21
 
16 22
   const handleTouchStart = useCallback((e: React.TouchEvent) => {

+ 8
- 3
hooks/useShapeEvents.ts Voir le fichier

@@ -14,10 +14,15 @@ export default function useShapeEvents(
14 14
       e.stopPropagation()
15 15
       rGroup.current.setPointerCapture(e.pointerId)
16 16
 
17
-      if (inputs.isDoubleClick()) {
18
-        state.send('DOUBLE_POINTED_SHAPE', inputs.pointerDown(e, id))
17
+      if (e.button === 0) {
18
+        if (inputs.isDoubleClick()) {
19
+          state.send('DOUBLE_POINTED_SHAPE', inputs.pointerDown(e, id))
20
+        }
21
+
22
+        state.send('POINTED_SHAPE', inputs.pointerDown(e, id))
23
+      } else {
24
+        state.send('RIGHT_POINTED', inputs.pointerDown(e, id))
19 25
       }
20
-      state.send('POINTED_SHAPE', inputs.pointerDown(e, id))
21 26
     },
22 27
     [id]
23 28
   )

+ 7
- 5
state/commands/index.ts Voir le fichier

@@ -2,14 +2,17 @@ import align from './align'
2 2
 import arrow from './arrow'
3 3
 import changePage from './change-page'
4 4
 import createPage from './create-page'
5
-import deleteSelected from './delete-selected'
6 5
 import deletePage from './delete-page'
6
+import deleteSelected from './delete-selected'
7 7
 import direct from './direct'
8 8
 import distribute from './distribute'
9 9
 import draw from './draw'
10 10
 import duplicate from './duplicate'
11 11
 import generate from './generate'
12
+import group from './group'
13
+import handle from './handle'
12 14
 import move from './move'
15
+import moveToPage from './move-to-page'
13 16
 import nudge from './nudge'
14 17
 import rotate from './rotate'
15 18
 import rotateCcw from './rotate-ccw'
@@ -19,8 +22,6 @@ import toggle from './toggle'
19 22
 import transform from './transform'
20 23
 import transformSingle from './transform-single'
21 24
 import translate from './translate'
22
-import handle from './handle'
23
-import group from './group'
24 25
 import ungroup from './ungroup'
25 26
 
26 27
 const commands = {
@@ -35,7 +36,10 @@ const commands = {
35 36
   draw,
36 37
   duplicate,
37 38
   generate,
39
+  group,
40
+  handle,
38 41
   move,
42
+  moveToPage,
39 43
   nudge,
40 44
   rotate,
41 45
   rotateCcw,
@@ -45,8 +49,6 @@ const commands = {
45 49
   transform,
46 50
   transformSingle,
47 51
   translate,
48
-  handle,
49
-  group,
50 52
   ungroup,
51 53
 }
52 54
 

+ 107
- 0
state/commands/move-to-page.ts Voir le fichier

@@ -0,0 +1,107 @@
1
+import Command from './command'
2
+import history from '../history'
3
+import { Data } from 'types'
4
+import {
5
+  getDocumentBranch,
6
+  getPage,
7
+  getPageState,
8
+  getSelectedIds,
9
+  getSelectedShapes,
10
+  getTopParentId,
11
+  setToArray,
12
+  uniqueArray,
13
+} from 'utils/utils'
14
+import { getShapeUtils } from 'lib/shape-utils'
15
+import * as vec from 'utils/vec'
16
+import storage from 'state/storage'
17
+
18
+export default function nudgeCommand(data: Data, toPageId: string) {
19
+  const { currentPageId: fromPageId } = data
20
+  const selectedIds = setToArray(getSelectedIds(data))
21
+
22
+  const selectedParents = uniqueArray(
23
+    ...selectedIds.map((id) => getTopParentId(data, id))
24
+  )
25
+
26
+  history.execute(
27
+    data,
28
+    new Command({
29
+      name: 'set_direction',
30
+      category: 'canvas',
31
+      manualSelection: true,
32
+      do(data) {
33
+        // The page we're moving the shapes from
34
+        const fromPage = getPage(data, fromPageId)
35
+
36
+        // Get all of the selected shapes and their descendents
37
+        const shapesToMove = selectedParents.flatMap((id) =>
38
+          getDocumentBranch(data, id).map((id) => fromPage.shapes[id])
39
+        )
40
+
41
+        // Delete the shapes from the "from" page
42
+        shapesToMove.forEach((shape) => delete fromPage.shapes[shape.id])
43
+
44
+        // Clear the current page state's selected ids
45
+        getPageState(data, fromPageId).selectedIds.clear()
46
+
47
+        // Save the "from" page
48
+        storage.savePage(data, fromPageId)
49
+
50
+        // Load the "to" page
51
+        storage.loadPage(data, toPageId)
52
+
53
+        // The page we're moving the shapes to
54
+        const toPage = getPage(data, toPageId)
55
+
56
+        // Add all of the selected shapes to the "from" page. Any shapes that
57
+        // were children of the "from" page should become children of the "to"
58
+        // page. Grouped shapes should keep their same parent.
59
+
60
+        // What about shapes that were children of a group that we haven't moved?
61
+        shapesToMove.forEach((shape) => {
62
+          toPage.shapes[shape.id] = shape
63
+          if (shape.parentId === fromPageId) {
64
+            getShapeUtils(shape).setProperty(shape, 'parentId', toPageId)
65
+          }
66
+        })
67
+
68
+        console.log('from', getPage(data, fromPageId))
69
+        console.log('to', getPage(data, toPageId))
70
+
71
+        // Select the selected ids on the new page
72
+        getPageState(data, toPageId).selectedIds = new Set(selectedIds)
73
+
74
+        // Move to the new page
75
+        data.currentPageId = toPageId
76
+      },
77
+      undo(data) {
78
+        const toPage = getPage(data, fromPageId)
79
+
80
+        const shapesToMove = selectedParents.flatMap((id) =>
81
+          getDocumentBranch(data, id).map((id) => toPage.shapes[id])
82
+        )
83
+
84
+        shapesToMove.forEach((shape) => delete toPage.shapes[shape.id])
85
+
86
+        getPageState(data, toPageId).selectedIds.clear()
87
+
88
+        storage.savePage(data, toPageId)
89
+
90
+        storage.loadPage(data, fromPageId)
91
+
92
+        const fromPage = getPage(data, toPageId)
93
+
94
+        shapesToMove.forEach((shape) => {
95
+          fromPage.shapes[shape.id] = shape
96
+          if (shape.parentId === toPageId) {
97
+            getShapeUtils(shape).setProperty(shape, 'parentId', fromPageId)
98
+          }
99
+        })
100
+
101
+        getPageState(data, fromPageId).selectedIds = new Set(selectedIds)
102
+
103
+        data.currentPageId = fromPageId
104
+      },
105
+    })
106
+  )
107
+}

+ 2
- 9
state/sessions/brush-session.ts Voir le fichier

@@ -7,6 +7,7 @@ import {
7 7
   getPage,
8 8
   getPageState,
9 9
   getShapes,
10
+  getTopParentId,
10 11
   setSelectedIds,
11 12
   setToArray,
12 13
 } from 'utils/utils'
@@ -77,7 +78,7 @@ export function getBrushSnapshot(data: Data) {
77 78
   const { selectedIds } = getPageState(cData)
78 79
 
79 80
   const shapesToTest = getShapes(cData)
80
-    .filter((shape) => shape.type !== ShapeType.Group)
81
+    .filter((shape) => shape.type !== ShapeType.Group && !shape.isHidden)
81 82
     .filter(
82 83
       (shape) => !(selectedIds.has(shape.id) || selectedIds.has(shape.parentId))
83 84
     )
@@ -100,11 +101,3 @@ export function getBrushSnapshot(data: Data) {
100 101
 }
101 102
 
102 103
 export type BrushSnapshot = ReturnType<typeof getBrushSnapshot>
103
-
104
-function getTopParentId(data: Data, id: string): string {
105
-  const shape = getPage(data).shapes[id]
106
-  return shape.parentId === data.currentPageId ||
107
-    shape.parentId === data.currentParentId
108
-    ? id
109
-    : getTopParentId(data, shape.parentId)
110
-}

+ 39
- 0
state/state.ts Voir le fichier

@@ -188,6 +188,10 @@ const state = createState({
188 188
             CHANGED_CODE_CONTROL: 'updateControls',
189 189
             GENERATED_FROM_CODE: ['setCodeControls', 'setGeneratedShapes'],
190 190
             TOGGLED_TOOL_LOCK: 'toggleToolLock',
191
+            MOVED_TO_PAGE: {
192
+              if: 'hasSelection',
193
+              do: ['moveSelectionToPage', 'zoomCameraToSelectionActual'],
194
+            },
191 195
             MOVED: { if: 'hasSelection', do: 'moveSelection' },
192 196
             DUPLICATED: { if: 'hasSelection', do: 'duplicateSelection' },
193 197
             ROTATED_CCW: { if: 'hasSelection', do: 'rotateSelectionCcw' },
@@ -268,6 +272,25 @@ const state = createState({
268 272
                     to: 'pointingBounds',
269 273
                   },
270 274
                 ],
275
+                RIGHT_POINTED: [
276
+                  {
277
+                    if: 'isPointingCanvas',
278
+                    do: 'clearSelectedIds',
279
+                    else: {
280
+                      if: 'isPointingShape',
281
+                      then: [
282
+                        'setPointedId',
283
+                        {
284
+                          unless: 'isPointedShapeSelected',
285
+                          do: [
286
+                            'clearSelectedIds',
287
+                            'pushPointedIdToSelectedIds',
288
+                          ],
289
+                        },
290
+                      ],
291
+                    },
292
+                  },
293
+                ],
271 294
               },
272 295
             },
273 296
             pointingBounds: {
@@ -756,15 +779,28 @@ const state = createState({
756 779
     },
757 780
   },
758 781
   conditions: {
782
+    isPointingCanvas(data, payload: PointerInfo) {
783
+      return payload.target === 'canvas'
784
+    },
759 785
     isPointingBounds(data, payload: PointerInfo) {
760 786
       return getSelectedIds(data).size > 0 && payload.target === 'bounds'
761 787
     },
788
+    isPointingShape(data, payload: PointerInfo) {
789
+      return (
790
+        payload.target &&
791
+        payload.target !== 'canvas' &&
792
+        payload.target !== 'bounds'
793
+      )
794
+    },
762 795
     isReadOnly(data) {
763 796
       return data.isReadOnly
764 797
     },
765 798
     distanceImpliesDrag(data, payload: PointerInfo) {
766 799
       return vec.dist2(payload.origin, payload.point) > 8
767 800
     },
801
+    hasPointedTarget(data, payload: PointerInfo) {
802
+      return payload.target !== undefined
803
+    },
768 804
     isPointedShapeSelected(data) {
769 805
       return getSelectedIds(data).has(data.pointedId)
770 806
     },
@@ -1121,6 +1157,9 @@ const state = createState({
1121 1157
     moveSelection(data, payload: { type: MoveType }) {
1122 1158
       commands.move(data, payload.type)
1123 1159
     },
1160
+    moveSelectionToPage(data, payload: { id: string }) {
1161
+      commands.moveToPage(data, payload.id)
1162
+    },
1124 1163
     alignSelection(data, payload: { type: AlignType }) {
1125 1164
       commands.align(data, payload.type)
1126 1165
     },

+ 2
- 1
styles/stitches.config.ts Voir le fichier

@@ -11,7 +11,8 @@ const { styled, global, css, theme, getCssString } = createCss({
11 11
       hint: 'rgba(216, 226, 249, 1.000)',
12 12
       selected: 'rgba(66, 133, 244, 1.000)',
13 13
       bounds: 'rgba(65, 132, 244, 1.000)',
14
-      boundsBg: 'rgba(65, 132, 244, 0.050)',
14
+      boundsBg: 'rgba(65, 132, 244, 0.05)',
15
+      overlay: 'rgba(0, 0, 0, 0.15)',
15 16
       border: '#aaa',
16 17
       canvas: '#fafafa',
17 18
       panel: '#fefefe',

+ 11
- 1
todo.md Voir le fichier

@@ -24,4 +24,14 @@
24 24
 ## Pages
25 25
 
26 26
 - [x] Make selection part of page state
27
-- [ ] Allow only one page to be in the document at a time
27
+- [x] Allow only one page to be in the document at a time
28
+
29
+## Context Menu
30
+
31
+- [x] Create context Menu
32
+- [ ] Wire up events
33
+
34
+## Move to Page
35
+
36
+- [ ] Move to Page Command
37
+- [ ] Dialog

+ 18
- 1
utils/utils.ts Voir le fichier

@@ -1398,7 +1398,7 @@ export function getSelectedBounds(data: Data) {
1398 1398
 }
1399 1399
 
1400 1400
 export function isMobile() {
1401
-  return _isMobile()
1401
+  return _isMobile().any
1402 1402
 }
1403 1403
 
1404 1404
 export function getRotatedBounds(shape: Shape) {
@@ -1661,6 +1661,7 @@ export function getParentRotation(
1661 1661
 
1662 1662
 export function getDocumentBranch(data: Data, id: string): string[] {
1663 1663
   const shape = getPage(data).shapes[id]
1664
+
1664 1665
   if (shape.type !== ShapeType.Group) return [id]
1665 1666
 
1666 1667
   return [
@@ -1741,3 +1742,19 @@ export function pointsBetween(a: number[], b: number[], steps = 6) {
1741 1742
 export function shuffleArr<T>(arr: T[], offset: number): T[] {
1742 1743
   return arr.map((_, i) => arr[(i + offset) % arr.length])
1743 1744
 }
1745
+
1746
+export function commandKey() {
1747
+  return isDarwin() ? '⌘' : 'Ctrl'
1748
+}
1749
+
1750
+export function getTopParentId(data: Data, id: string): string {
1751
+  const shape = getPage(data).shapes[id]
1752
+  return shape.parentId === data.currentPageId ||
1753
+    shape.parentId === data.currentParentId
1754
+    ? id
1755
+    : getTopParentId(data, shape.parentId)
1756
+}
1757
+
1758
+export function uniqueArray<T extends string | number | Symbol>(...items: T[]) {
1759
+  return Array.from(new Set(items).values())
1760
+}

Chargement…
Annuler
Enregistrer