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

* Add getSvgElement

* Update TextUtil.tsx

* Add sticky svg

* Fix bounds bug, improve text export

* Include fonts
This commit is contained in:
Steve Ruiz 2021-11-22 16:15:51 +00:00 committed by GitHub
parent f8dc5b3248
commit a2fff9dca7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 101 additions and 29 deletions

View file

@ -9,7 +9,7 @@ export function useBoundsEvents() {
if (!inputs.pointerIsValid(e)) return if (!inputs.pointerIsValid(e)) return
if (e.button === 2) { if (e.button === 2) {
callbacks.onRightPointShape?.(inputs.pointerDown(e, 'bounds'), e) callbacks.onRightPointBounds?.(inputs.pointerDown(e, 'bounds'), e)
return return
} }

View file

@ -1558,26 +1558,32 @@ export class TldrawApp extends StateManager<TDSnapshot> {
copySvg = (ids = this.selectedIds, pageId = this.currentPageId) => { copySvg = (ids = this.selectedIds, pageId = this.currentPageId) => {
if (ids.length === 0) ids = Object.keys(this.page.shapes) if (ids.length === 0) ids = Object.keys(this.page.shapes)
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
const shapes = ids.map((id) => this.getShape(id, pageId)) const shapes = ids.map((id) => this.getShape(id, pageId))
const commonBounds = Utils.getCommonBounds(shapes.map(TLDR.getRotatedBounds))
const padding = 16
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs')
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style')
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');`
defs.appendChild(style)
svg.appendChild(defs)
function getSvgElementForShape(shape: TDShape) { function getSvgElementForShape(shape: TDShape) {
const elm = document.getElementById(shape.id + '_svg') const util = TLDR.getShapeUtil(shape)
const element = util.getSvgElement(shape)
const bounds = util.getBounds(shape)
if (!elm) return if (!element) return
// TODO: Create SVG elements for text
const element = elm?.cloneNode(true) as SVGElement
const bounds = TLDR.getShapeUtil(shape).getBounds(shape)
element.setAttribute( element.setAttribute(
'transform', 'transform',
`translate(${shape.point[0]}, ${shape.point[1]}) rotate(${ `translate(${padding + shape.point[0] - commonBounds.minX}, ${
((shape.rotation || 0) * 180) / Math.PI padding + shape.point[1] - commonBounds.minY
}, ${bounds.width / 2}, ${bounds.height / 2})` }) rotate(${((shape.rotation || 0) * 180) / Math.PI}, ${bounds.width / 2}, ${
bounds.height / 2
})`
) )
return element return element
@ -1608,23 +1614,14 @@ export class TldrawApp extends StateManager<TDSnapshot> {
} }
}) })
const bounds = Utils.getCommonBounds(shapes.map(TLDR.getRotatedBounds))
const padding = 16
// Resize the element to the bounding box // Resize the element to the bounding box
svg.setAttribute( svg.setAttribute(
'viewBox', 'viewBox',
[ [0, 0, commonBounds.width + padding * 2, commonBounds.height + padding * 2].join(' ')
bounds.minX - padding,
bounds.minY - padding,
bounds.width + padding * 2,
bounds.height + padding * 2,
].join(' ')
) )
svg.setAttribute('width', String(bounds.width)) svg.setAttribute('width', String(commonBounds.width))
svg.setAttribute('height', String(commonBounds.height))
svg.setAttribute('height', String(bounds.height))
const s = new XMLSerializer() const s = new XMLSerializer()

View file

@ -202,6 +202,6 @@ Array [
] ]
`; `;
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>"`; 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>"`;
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\\"/>"`; 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>"`;

View file

@ -10,6 +10,7 @@ import { styled } from '~styles'
import { Vec } from '@tldraw/vec' import { Vec } from '@tldraw/vec'
import { GHOSTED_OPACITY } from '~constants' import { GHOSTED_OPACITY } from '~constants'
import { TLDR } from '~state/TLDR' import { TLDR } from '~state/TLDR'
import { getTextSvgElement } from '../shared/getTextSvgElement'
type T = StickyShape type T = StickyShape
type E = HTMLDivElement type E = HTMLDivElement
@ -232,6 +233,24 @@ export class StickyUtil extends TDShapeUtil<T, E> {
transformSingle = (shape: T): Partial<T> => { transformSingle = (shape: T): Partial<T> => {
return shape return shape
} }
getSvgElement = (shape: T): SVGElement | void => {
const bounds = this.getBounds(shape)
const textElm = getTextSvgElement(shape, bounds)
const style = getStickyShapeStyle(shape.style)
textElm.setAttribute('fill', style.color)
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
rect.setAttribute('width', bounds.width + '')
rect.setAttribute('height', bounds.height + '')
rect.setAttribute('fill', style.fill)
g.appendChild(rect)
g.appendChild(textElm)
return g
}
} }
/* -------------------------------------------------- */ /* -------------------------------------------------- */

View file

@ -179,4 +179,8 @@ export abstract class TDShapeUtil<T extends TDShape, E extends Element = any> ex
onDoubleClickBoundsHandle?: (shape: T) => Partial<T> | void onDoubleClickBoundsHandle?: (shape: T) => Partial<T> | void
onSessionComplete?: (shape: T) => Partial<T> | void onSessionComplete?: (shape: T) => Partial<T> | void
getSvgElement = (shape: T): SVGElement | void => {
return document.getElementById(shape.id + '_svg')?.cloneNode(true) as SVGElement
}
} }

View file

@ -10,6 +10,7 @@ import { styled } from '~styles'
import { Vec } from '@tldraw/vec' import { Vec } from '@tldraw/vec'
import { TLDR } from '~state/TLDR' import { TLDR } from '~state/TLDR'
import { getTextAlign } from '../shared/getTextAlign' import { getTextAlign } from '../shared/getTextAlign'
import { getTextSvgElement } from '../shared/getTextSvgElement'
type T = TextShape type T = TextShape
type E = HTMLDivElement type E = HTMLDivElement
@ -311,6 +312,13 @@ export class TextUtil extends TDShapeUtil<T, E> {
point: Vec.round(Vec.add(shape.point, Vec.sub(center, newCenter))), point: Vec.round(Vec.add(shape.point, Vec.sub(center, newCenter))),
} }
} }
getSvgElement = (shape: T): SVGElement | void => {
const bounds = this.getBounds(shape)
const elm = getTextSvgElement(shape, bounds)
elm.setAttribute('fill', getShapeStyle(shape.style).stroke)
return elm
}
} }
/* -------------------------------------------------- */ /* -------------------------------------------------- */
@ -332,7 +340,7 @@ function getMeasurementDiv() {
Object.assign(pre.style, { Object.assign(pre.style, {
whiteSpace: 'pre', whiteSpace: 'pre',
width: 'auto', width: 'auto',
border: '1px solid red', border: '1px solid transparent',
padding: '4px', padding: '4px',
margin: '0px', margin: '0px',
letterSpacing: `${LETTER_SPACING}px`, letterSpacing: `${LETTER_SPACING}px`,

View file

@ -0,0 +1,43 @@
import type { TLBounds } from '@tldraw/core'
import { AlignStyle, StickyShape, TextShape } from '~types'
import { getFontFace, getFontSize } from './shape-styles'
import { getTextAlign } from './getTextAlign'
export function getTextSvgElement(shape: TextShape | StickyShape, bounds: TLBounds) {
const { text, style } = shape
const fontSize = getFontSize(shape.style.size, shape.style.font)
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
const LINE_HEIGHT = fontSize * 1.3
const textLines = text.split('\n').map((line, i) => {
const textElm = document.createElementNS('http://www.w3.org/2000/svg', 'text')
textElm.textContent = line
textElm.setAttribute('font-family', getFontFace(style.font))
textElm.setAttribute('font-size', fontSize + 'px')
textElm.setAttribute('text-anchor', 'start')
textElm.setAttribute('alignment-baseline', 'central')
textElm.setAttribute('text-align', getTextAlign(style.textAlign))
textElm.setAttribute('y', LINE_HEIGHT * (0.5 + i) + '')
g.appendChild(textElm)
return textElm
})
if (style.textAlign === AlignStyle.Middle) {
textLines.forEach((textElm) => {
textElm.setAttribute('x', bounds.width / 2 + '')
textElm.setAttribute('text-align', 'center')
textElm.setAttribute('text-anchor', 'middle')
})
} else if (style.textAlign === AlignStyle.End) {
textLines.forEach((textElm) => {
textElm.setAttribute('x', bounds.width + '')
textElm.setAttribute('text-align', 'right')
textElm.setAttribute('text-anchor', 'end')
})
}
return g
}

View file

@ -551,6 +551,7 @@ export class SelectTool extends BaseTool<Status> {
onRightPointShape: TLPointerEventHandler = (info) => { onRightPointShape: TLPointerEventHandler = (info) => {
if (!this.app.isSelected(info.target)) { if (!this.app.isSelected(info.target)) {
console.log(info.target)
this.app.select(info.target) this.app.select(info.target)
} }
} }