Преглед изворни кода

[improvement] Add basic support for copying text (#354)

* Add getSvgElement

* Update TextUtil.tsx

* Add sticky svg

* Fix bounds bug, improve text export

* Include fonts
main
Steve Ruiz пре 3 година
родитељ
комит
a2fff9dca7
No account linked to committer's email address

+ 1
- 1
packages/core/src/hooks/useBoundsEvents.tsx Прегледај датотеку

9
       if (!inputs.pointerIsValid(e)) return
9
       if (!inputs.pointerIsValid(e)) return
10
 
10
 
11
       if (e.button === 2) {
11
       if (e.button === 2) {
12
-        callbacks.onRightPointShape?.(inputs.pointerDown(e, 'bounds'), e)
12
+        callbacks.onRightPointBounds?.(inputs.pointerDown(e, 'bounds'), e)
13
         return
13
         return
14
       }
14
       }
15
 
15
 

+ 21
- 24
packages/tldraw/src/state/TldrawApp.ts Прегледај датотеку

1558
   copySvg = (ids = this.selectedIds, pageId = this.currentPageId) => {
1558
   copySvg = (ids = this.selectedIds, pageId = this.currentPageId) => {
1559
     if (ids.length === 0) ids = Object.keys(this.page.shapes)
1559
     if (ids.length === 0) ids = Object.keys(this.page.shapes)
1560
 
1560
 
1561
-    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
1562
-
1563
     const shapes = ids.map((id) => this.getShape(id, pageId))
1561
     const shapes = ids.map((id) => this.getShape(id, pageId))
1562
+    const commonBounds = Utils.getCommonBounds(shapes.map(TLDR.getRotatedBounds))
1563
+    const padding = 16
1564
 
1564
 
1565
-    function getSvgElementForShape(shape: TDShape) {
1566
-      const elm = document.getElementById(shape.id + '_svg')
1567
-
1568
-      if (!elm) return
1565
+    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
1566
+    const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs')
1567
+    const style = document.createElementNS('http://www.w3.org/2000/svg', 'style')
1569
 
1568
 
1570
-      // TODO: Create SVG elements for text
1569
+    style.textContent = `@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&family=Source+Code+Pro&family=Source+Sans+Pro&family=Source+Serif+Pro&display=swap');`
1570
+    defs.appendChild(style)
1571
+    svg.appendChild(defs)
1571
 
1572
 
1572
-      const element = elm?.cloneNode(true) as SVGElement
1573
+    function getSvgElementForShape(shape: TDShape) {
1574
+      const util = TLDR.getShapeUtil(shape)
1575
+      const element = util.getSvgElement(shape)
1576
+      const bounds = util.getBounds(shape)
1573
 
1577
 
1574
-      const bounds = TLDR.getShapeUtil(shape).getBounds(shape)
1578
+      if (!element) return
1575
 
1579
 
1576
       element.setAttribute(
1580
       element.setAttribute(
1577
         'transform',
1581
         'transform',
1578
-        `translate(${shape.point[0]}, ${shape.point[1]}) rotate(${
1579
-          ((shape.rotation || 0) * 180) / Math.PI
1580
-        }, ${bounds.width / 2}, ${bounds.height / 2})`
1582
+        `translate(${padding + shape.point[0] - commonBounds.minX}, ${
1583
+          padding + shape.point[1] - commonBounds.minY
1584
+        }) rotate(${((shape.rotation || 0) * 180) / Math.PI}, ${bounds.width / 2}, ${
1585
+          bounds.height / 2
1586
+        })`
1581
       )
1587
       )
1582
 
1588
 
1583
       return element
1589
       return element
1608
       }
1614
       }
1609
     })
1615
     })
1610
 
1616
 
1611
-    const bounds = Utils.getCommonBounds(shapes.map(TLDR.getRotatedBounds))
1612
-    const padding = 16
1613
-
1614
     // Resize the element to the bounding box
1617
     // Resize the element to the bounding box
1615
     svg.setAttribute(
1618
     svg.setAttribute(
1616
       'viewBox',
1619
       'viewBox',
1617
-      [
1618
-        bounds.minX - padding,
1619
-        bounds.minY - padding,
1620
-        bounds.width + padding * 2,
1621
-        bounds.height + padding * 2,
1622
-      ].join(' ')
1620
+      [0, 0, commonBounds.width + padding * 2, commonBounds.height + padding * 2].join(' ')
1623
     )
1621
     )
1624
 
1622
 
1625
-    svg.setAttribute('width', String(bounds.width))
1626
-
1627
-    svg.setAttribute('height', String(bounds.height))
1623
+    svg.setAttribute('width', String(commonBounds.width))
1624
+    svg.setAttribute('height', String(commonBounds.height))
1628
 
1625
 
1629
     const s = new XMLSerializer()
1626
     const s = new XMLSerializer()
1630
 
1627
 

+ 2
- 2
packages/tldraw/src/state/__snapshots__/TLDrawApp.spec.ts.snap Прегледај датотеку

202
 ]
202
 ]
203
 `;
203
 `;
204
 
204
 
205
-exports[`TldrawTestApp When copying to SVG Copies grouped shapes.: copied svg with group 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"-16 -16 232 232\\" width=\\"200\\" height=\\"200\\"><g/></svg>"`;
205
+exports[`TldrawTestApp When copying to SVG Copies grouped shapes.: copied svg with group 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 232 232\\" width=\\"200\\" height=\\"200\\"><defs><style>@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&amp;family=Source+Code+Pro&amp;family=Source+Sans+Pro&amp;family=Source+Serif+Pro&amp;display=swap');</style></defs><g/></svg>"`;
206
 
206
 
207
-exports[`TldrawTestApp When copying to SVG Copies shapes.: copied svg 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"-20.741879096242684 -20.741879096242684 236.74 236.74\\" width=\\"204.74\\" height=\\"204.74\\"/>"`;
207
+exports[`TldrawTestApp When copying to SVG Copies shapes.: copied svg 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 236.74 236.74\\" width=\\"204.74\\" height=\\"204.74\\"><defs><style>@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&amp;family=Source+Code+Pro&amp;family=Source+Sans+Pro&amp;family=Source+Serif+Pro&amp;display=swap');</style></defs></svg>"`;

+ 19
- 0
packages/tldraw/src/state/shapes/StickyUtil/StickyUtil.tsx Прегледај датотеку

10
 import { Vec } from '@tldraw/vec'
10
 import { Vec } from '@tldraw/vec'
11
 import { GHOSTED_OPACITY } from '~constants'
11
 import { GHOSTED_OPACITY } from '~constants'
12
 import { TLDR } from '~state/TLDR'
12
 import { TLDR } from '~state/TLDR'
13
+import { getTextSvgElement } from '../shared/getTextSvgElement'
13
 
14
 
14
 type T = StickyShape
15
 type T = StickyShape
15
 type E = HTMLDivElement
16
 type E = HTMLDivElement
232
   transformSingle = (shape: T): Partial<T> => {
233
   transformSingle = (shape: T): Partial<T> => {
233
     return shape
234
     return shape
234
   }
235
   }
236
+
237
+  getSvgElement = (shape: T): SVGElement | void => {
238
+    const bounds = this.getBounds(shape)
239
+    const textElm = getTextSvgElement(shape, bounds)
240
+    const style = getStickyShapeStyle(shape.style)
241
+    textElm.setAttribute('fill', style.color)
242
+
243
+    const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
244
+    const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
245
+    rect.setAttribute('width', bounds.width + '')
246
+    rect.setAttribute('height', bounds.height + '')
247
+    rect.setAttribute('fill', style.fill)
248
+
249
+    g.appendChild(rect)
250
+    g.appendChild(textElm)
251
+
252
+    return g
253
+  }
235
 }
254
 }
236
 
255
 
237
 /* -------------------------------------------------- */
256
 /* -------------------------------------------------- */

+ 4
- 0
packages/tldraw/src/state/shapes/TDShapeUtil.tsx Прегледај датотеку

179
   onDoubleClickBoundsHandle?: (shape: T) => Partial<T> | void
179
   onDoubleClickBoundsHandle?: (shape: T) => Partial<T> | void
180
 
180
 
181
   onSessionComplete?: (shape: T) => Partial<T> | void
181
   onSessionComplete?: (shape: T) => Partial<T> | void
182
+
183
+  getSvgElement = (shape: T): SVGElement | void => {
184
+    return document.getElementById(shape.id + '_svg')?.cloneNode(true) as SVGElement
185
+  }
182
 }
186
 }

+ 9
- 1
packages/tldraw/src/state/shapes/TextUtil/TextUtil.tsx Прегледај датотеку

10
 import { Vec } from '@tldraw/vec'
10
 import { Vec } from '@tldraw/vec'
11
 import { TLDR } from '~state/TLDR'
11
 import { TLDR } from '~state/TLDR'
12
 import { getTextAlign } from '../shared/getTextAlign'
12
 import { getTextAlign } from '../shared/getTextAlign'
13
+import { getTextSvgElement } from '../shared/getTextSvgElement'
13
 
14
 
14
 type T = TextShape
15
 type T = TextShape
15
 type E = HTMLDivElement
16
 type E = HTMLDivElement
311
       point: Vec.round(Vec.add(shape.point, Vec.sub(center, newCenter))),
312
       point: Vec.round(Vec.add(shape.point, Vec.sub(center, newCenter))),
312
     }
313
     }
313
   }
314
   }
315
+
316
+  getSvgElement = (shape: T): SVGElement | void => {
317
+    const bounds = this.getBounds(shape)
318
+    const elm = getTextSvgElement(shape, bounds)
319
+    elm.setAttribute('fill', getShapeStyle(shape.style).stroke)
320
+    return elm
321
+  }
314
 }
322
 }
315
 
323
 
316
 /* -------------------------------------------------- */
324
 /* -------------------------------------------------- */
332
   Object.assign(pre.style, {
340
   Object.assign(pre.style, {
333
     whiteSpace: 'pre',
341
     whiteSpace: 'pre',
334
     width: 'auto',
342
     width: 'auto',
335
-    border: '1px solid red',
343
+    border: '1px solid transparent',
336
     padding: '4px',
344
     padding: '4px',
337
     margin: '0px',
345
     margin: '0px',
338
     letterSpacing: `${LETTER_SPACING}px`,
346
     letterSpacing: `${LETTER_SPACING}px`,

+ 43
- 0
packages/tldraw/src/state/shapes/shared/getTextSvgElement.ts Прегледај датотеку

1
+import type { TLBounds } from '@tldraw/core'
2
+import { AlignStyle, StickyShape, TextShape } from '~types'
3
+import { getFontFace, getFontSize } from './shape-styles'
4
+import { getTextAlign } from './getTextAlign'
5
+
6
+export function getTextSvgElement(shape: TextShape | StickyShape, bounds: TLBounds) {
7
+  const { text, style } = shape
8
+  const fontSize = getFontSize(shape.style.size, shape.style.font)
9
+
10
+  const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
11
+
12
+  const LINE_HEIGHT = fontSize * 1.3
13
+
14
+  const textLines = text.split('\n').map((line, i) => {
15
+    const textElm = document.createElementNS('http://www.w3.org/2000/svg', 'text')
16
+    textElm.textContent = line
17
+    textElm.setAttribute('font-family', getFontFace(style.font))
18
+    textElm.setAttribute('font-size', fontSize + 'px')
19
+    textElm.setAttribute('text-anchor', 'start')
20
+    textElm.setAttribute('alignment-baseline', 'central')
21
+    textElm.setAttribute('text-align', getTextAlign(style.textAlign))
22
+    textElm.setAttribute('y', LINE_HEIGHT * (0.5 + i) + '')
23
+    g.appendChild(textElm)
24
+
25
+    return textElm
26
+  })
27
+
28
+  if (style.textAlign === AlignStyle.Middle) {
29
+    textLines.forEach((textElm) => {
30
+      textElm.setAttribute('x', bounds.width / 2 + '')
31
+      textElm.setAttribute('text-align', 'center')
32
+      textElm.setAttribute('text-anchor', 'middle')
33
+    })
34
+  } else if (style.textAlign === AlignStyle.End) {
35
+    textLines.forEach((textElm) => {
36
+      textElm.setAttribute('x', bounds.width + '')
37
+      textElm.setAttribute('text-align', 'right')
38
+      textElm.setAttribute('text-anchor', 'end')
39
+    })
40
+  }
41
+
42
+  return g
43
+}

+ 1
- 0
packages/tldraw/src/state/tools/SelectTool/SelectTool.ts Прегледај датотеку

551
 
551
 
552
   onRightPointShape: TLPointerEventHandler = (info) => {
552
   onRightPointShape: TLPointerEventHandler = (info) => {
553
     if (!this.app.isSelected(info.target)) {
553
     if (!this.app.isSelected(info.target)) {
554
+      console.log(info.target)
554
       this.app.select(info.target)
555
       this.app.select(info.target)
555
     }
556
     }
556
   }
557
   }

Loading…
Откажи
Сачувај