Bladeren bron

Adds copy to svg

main
Steve Ruiz 4 jaren geleden
bovenliggende
commit
fc2e3b3c4c

+ 1
- 2
components/canvas/canvas.tsx Bestand weergeven

30
       <MainSVG ref={rCanvas} {...events}>
30
       <MainSVG ref={rCanvas} {...events}>
31
         <Defs />
31
         <Defs />
32
         {isReady && (
32
         {isReady && (
33
-          <g ref={rGroup}>
33
+          <g ref={rGroup} id="shapes">
34
             <BoundsBg />
34
             <BoundsBg />
35
             <Page />
35
             <Page />
36
-            {/* <Selected /> */}
37
             <Bounds />
36
             <Bounds />
38
             <Handles />
37
             <Handles />
39
             <Brush />
38
             <Brush />

+ 8
- 0
components/canvas/context-menu/context-menu.tsx Bestand weergeven

208
               />
208
               />
209
             )}
209
             )}
210
             <MoveToPageMenu />
210
             <MoveToPageMenu />
211
+            <Button onSelect={() => state.send('COPIED_TO_SVG')}>
212
+              <span>Copy to SVG</span>
213
+              <kbd>
214
+                <span>{commandKey()}</span>
215
+                <span>⇧</span>
216
+                <span>C</span>
217
+              </kbd>
218
+            </Button>
211
             <StyledDivider />
219
             <StyledDivider />
212
             <Button onSelect={() => state.send('DELETED')}>
220
             <Button onSelect={() => state.send('DELETED')}>
213
               <span>Delete</span>
221
               <span>Delete</span>

+ 9
- 2
components/canvas/defs.tsx Bestand weergeven

1
+import { getShapeStyle } from 'lib/shape-styles'
1
 import { getShapeUtils } from 'lib/shape-utils'
2
 import { getShapeUtils } from 'lib/shape-utils'
2
-import { memo } from 'react'
3
+import React, { memo } from 'react'
3
 import { useSelector } from 'state'
4
 import { useSelector } from 'state'
4
 import { deepCompareArrays, getCurrentCamera, getPage } from 'utils/utils'
5
 import { deepCompareArrays, getCurrentCamera, getPage } from 'utils/utils'
5
 import { DotCircle, Handle } from './misc'
6
 import { DotCircle, Handle } from './misc'
33
   const shape = useSelector((s) => getPage(s.data).shapes[id])
34
   const shape = useSelector((s) => getPage(s.data).shapes[id])
34
 
35
 
35
   if (!shape) return null
36
   if (!shape) return null
36
-  return getShapeUtils(shape).render(shape, { isEditing: false })
37
+
38
+  const style = getShapeStyle(shape.style)
39
+
40
+  return React.cloneElement(
41
+    getShapeUtils(shape).render(shape, { isEditing: false }),
42
+    { ...style }
43
+  )
37
 })
44
 })

+ 8
- 7
components/canvas/selected.tsx Bestand weergeven

32
 }
32
 }
33
 
33
 
34
 export const ShapeOutline = memo(function ShapeOutline({ id }: { id: string }) {
34
 export const ShapeOutline = memo(function ShapeOutline({ id }: { id: string }) {
35
-  const rIndicator = useRef<SVGUseElement>(null)
35
+  // const rIndicator = useRef<SVGUseElement>(null)
36
 
36
 
37
   const shape = useSelector((s) => getPage(s.data).shapes[id])
37
   const shape = useSelector((s) => getPage(s.data).shapes[id])
38
 
38
 
39
-  const events = useShapeEvents(id, shape?.type === ShapeType.Group, rIndicator)
39
+  // const events = useShapeEvents(id, shape?.type === ShapeType.Group, rIndicator)
40
 
40
 
41
   if (!shape) return null
41
   if (!shape) return null
42
 
42
 
52
 
52
 
53
   return (
53
   return (
54
     <SelectIndicator
54
     <SelectIndicator
55
-      ref={rIndicator}
55
+      // ref={rIndicator}
56
       as="use"
56
       as="use"
57
       href={'#' + id}
57
       href={'#' + id}
58
       transform={transform}
58
       transform={transform}
59
       isLocked={shape.isLocked}
59
       isLocked={shape.isLocked}
60
-      {...events}
60
+      // {...events}
61
     />
61
     />
62
   )
62
   )
63
 })
63
 })
64
 
64
 
65
 const SelectIndicator = styled('path', {
65
 const SelectIndicator = styled('path', {
66
-  zStrokeWidth: 2,
66
+  // zStrokeWidth: 2,
67
   strokeLineCap: 'round',
67
   strokeLineCap: 'round',
68
   strokeLinejoin: 'round',
68
   strokeLinejoin: 'round',
69
-  stroke: '$selected',
69
+  stroke: 'red',
70
+  strokeWidth: '10',
70
   pointerEvents: 'none',
71
   pointerEvents: 'none',
71
-  fill: 'transparent',
72
+  fill: 'red',
72
 
73
 
73
   variants: {
74
   variants: {
74
     isLocked: {
75
     isLocked: {

+ 6
- 2
components/canvas/shape.tsx Bestand weergeven

3
 import styled from 'styles'
3
 import styled from 'styles'
4
 import { getShapeUtils } from 'lib/shape-utils'
4
 import { getShapeUtils } from 'lib/shape-utils'
5
 import { getBoundsCenter, getPage, isMobile } from 'utils/utils'
5
 import { getBoundsCenter, getPage, isMobile } from 'utils/utils'
6
-import { ShapeStyles, ShapeType } from 'types'
6
+import { ShapeStyles, ShapeType, Shape as _Shape } from 'types'
7
 import useShapeEvents from 'hooks/useShapeEvents'
7
 import useShapeEvents from 'hooks/useShapeEvents'
8
 import vec from 'utils/vec'
8
 import vec from 'utils/vec'
9
 import { getShapeStyle } from 'lib/shape-styles'
9
 import { getShapeStyle } from 'lib/shape-styles'
60
 
60
 
61
   return (
61
   return (
62
     <StyledGroup
62
     <StyledGroup
63
+      id={id + '-group'}
63
       ref={rGroup}
64
       ref={rGroup}
64
       transform={transform}
65
       transform={transform}
65
       device={isMobileDevice ? 'mobile' : 'desktop'}
66
       device={isMobileDevice ? 'mobile' : 'desktop'}
94
           <RealShape
95
           <RealShape
95
             isParent={isParent}
96
             isParent={isParent}
96
             id={id}
97
             id={id}
98
+            shape={shape}
97
             style={style}
99
             style={style}
98
             isEditing={isEditing}
100
             isEditing={isEditing}
99
           />
101
           />
116
   id: string
118
   id: string
117
   style: Partial<React.SVGProps<SVGUseElement>>
119
   style: Partial<React.SVGProps<SVGUseElement>>
118
   isParent: boolean
120
   isParent: boolean
121
+  shape: _Shape
119
   isEditing: boolean
122
   isEditing: boolean
120
 }
123
 }
121
 
124
 
122
 const RealShape = memo(function RealShape({
125
 const RealShape = memo(function RealShape({
123
   id,
126
   id,
127
+  shape,
124
   style,
128
   style,
125
   isParent,
129
   isParent,
126
 }: RealShapeProps) {
130
 }: RealShapeProps) {
127
-  return <StyledShape as="use" data-shy={isParent} href={'#' + id} {...style} />
131
+  return <StyledShape as="use" data-shy={isParent} href={'#' + id} />
128
 })
132
 })
129
 
133
 
130
 const StyledShape = styled('path', {
134
 const StyledShape = styled('path', {

+ 5
- 1
hooks/useKeyboardEvents.ts Bestand weergeven

192
         }
192
         }
193
         case 'c': {
193
         case 'c': {
194
           if (metaKey(e)) {
194
           if (metaKey(e)) {
195
-            state.send('COPIED', getKeyboardEventInfo(e))
195
+            if (e.shiftKey) {
196
+              state.send('COPIED_TO_SVG', getKeyboardEventInfo(e))
197
+            } else {
198
+              state.send('COPIED', getKeyboardEventInfo(e))
199
+            }
196
           } else {
200
           } else {
197
             state.send('SELECTED_ELLIPSE_TOOL', getKeyboardEventInfo(e))
201
             state.send('SELECTED_ELLIPSE_TOOL', getKeyboardEventInfo(e))
198
           }
202
           }

+ 16
- 0
pages/shhh.tsx Bestand weergeven

1
+// import Editor from "components/editor"
2
+import Head from 'next/head'
3
+import dynamic from 'next/dynamic'
4
+
5
+const Editor = dynamic(() => import('components/editor'), { ssr: false })
6
+
7
+export default function Home() {
8
+  return (
9
+    <>
10
+      <Head>
11
+        <title>tldraw</title>
12
+      </Head>
13
+      <Editor />
14
+    </>
15
+  )
16
+}

+ 90
- 0
state/clipboard.ts Bestand weergeven

1
+import { getShapeUtils } from 'lib/shape-utils'
1
 import { Data, Shape } from 'types'
2
 import { Data, Shape } from 'types'
3
+import { getCommonBounds, getSelectedIds, getSelectedShapes } from 'utils/utils'
2
 import state from './state'
4
 import state from './state'
3
 
5
 
4
 class Clipboard {
6
 class Clipboard {
43
   clear = () => {
45
   clear = () => {
44
     this.current = undefined
46
     this.current = undefined
45
   }
47
   }
48
+
49
+  copySelectionToSvg(data: Data) {
50
+    const shapes = getSelectedShapes(data)
51
+
52
+    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
53
+
54
+    shapes
55
+      .sort((a, b) => a.childIndex - b.childIndex)
56
+      .forEach((shape) => {
57
+        const group = document.getElementById(shape.id + '-group')
58
+        const node = document.getElementById(shape.id)
59
+
60
+        const groupClone = group.cloneNode()
61
+        groupClone.appendChild(node.cloneNode(true))
62
+
63
+        svg.appendChild(groupClone)
64
+      })
65
+
66
+    const bounds = getCommonBounds(
67
+      ...shapes.map((shape) => getShapeUtils(shape).getBounds(shape))
68
+    )
69
+
70
+    // No content
71
+    if (!bounds) return
72
+
73
+    const padding = 16
74
+
75
+    // Resize the element to the bounding box
76
+    svg.setAttribute(
77
+      'viewBox',
78
+      [
79
+        bounds.minX - padding,
80
+        bounds.minY - padding,
81
+        bounds.width + padding * 2,
82
+        bounds.height + padding * 2,
83
+      ].join(' ')
84
+    )
85
+
86
+    svg.setAttribute('width', String(bounds.width))
87
+    svg.setAttribute('height', String(bounds.height))
88
+
89
+    // Take a snapshot of the element
90
+    const s = new XMLSerializer()
91
+    const svgString = s.serializeToString(svg)
92
+
93
+    // Copy to clipboard!
94
+    try {
95
+      navigator.clipboard.writeText(svgString)
96
+    } catch (e) {
97
+      Clipboard.copyStringToClipboard(svgString)
98
+    }
99
+  }
100
+
101
+  static copyStringToClipboard(string: string) {
102
+    let textarea: HTMLTextAreaElement
103
+    let result: boolean | null
104
+
105
+    textarea = document.createElement('textarea')
106
+    textarea.setAttribute('position', 'fixed')
107
+    textarea.setAttribute('top', '0')
108
+    textarea.setAttribute('readonly', 'true')
109
+    textarea.setAttribute('contenteditable', 'true')
110
+    textarea.style.position = 'fixed'
111
+    textarea.value = string
112
+    document.body.appendChild(textarea)
113
+    textarea.focus()
114
+    textarea.select()
115
+
116
+    try {
117
+      const range = document.createRange()
118
+      range.selectNodeContents(textarea)
119
+
120
+      const sel = window.getSelection()
121
+      sel.removeAllRanges()
122
+      sel.addRange(range)
123
+
124
+      textarea.setSelectionRange(0, textarea.value.length)
125
+      result = document.execCommand('copy')
126
+    } catch (err) {
127
+      result = null
128
+    } finally {
129
+      document.body.removeChild(textarea)
130
+    }
131
+
132
+    if (!result) return false
133
+
134
+    return true
135
+  }
46
 }
136
 }
47
 
137
 
48
 export default new Clipboard()
138
 export default new Clipboard()

+ 5
- 0
state/state.ts Bestand weergeven

226
             ZOOMED_IN: 'zoomIn',
226
             ZOOMED_IN: 'zoomIn',
227
             ZOOMED_OUT: 'zoomOut',
227
             ZOOMED_OUT: 'zoomOut',
228
             RESET_CAMERA: 'resetCamera',
228
             RESET_CAMERA: 'resetCamera',
229
+            COPIED_TO_SVG: 'copyToSvg',
229
           },
230
           },
230
           initial: 'notPointing',
231
           initial: 'notPointing',
231
           states: {
232
           states: {
1640
 
1641
 
1641
     /* ---------------------- Data ---------------------- */
1642
     /* ---------------------- Data ---------------------- */
1642
 
1643
 
1644
+    copyToSvg(data) {
1645
+      clipboard.copySelectionToSvg(data)
1646
+    },
1647
+
1643
     copyToClipboard(data) {
1648
     copyToClipboard(data) {
1644
       clipboard.copy(getSelectedShapes(data))
1649
       clipboard.copy(getSelectedShapes(data))
1645
     },
1650
     },

Laden…
Annuleren
Opslaan