Adds copy to svg

This commit is contained in:
Steve Ruiz 2021-06-20 23:01:40 +01:00
parent 124b70b262
commit fc2e3b3c4c
9 changed files with 148 additions and 14 deletions

View file

@ -30,10 +30,9 @@ export default function Canvas() {
<MainSVG ref={rCanvas} {...events}> <MainSVG ref={rCanvas} {...events}>
<Defs /> <Defs />
{isReady && ( {isReady && (
<g ref={rGroup}> <g ref={rGroup} id="shapes">
<BoundsBg /> <BoundsBg />
<Page /> <Page />
{/* <Selected /> */}
<Bounds /> <Bounds />
<Handles /> <Handles />
<Brush /> <Brush />

View file

@ -208,6 +208,14 @@ export default function ContextMenu({
/> />
)} )}
<MoveToPageMenu /> <MoveToPageMenu />
<Button onSelect={() => state.send('COPIED_TO_SVG')}>
<span>Copy to SVG</span>
<kbd>
<span>{commandKey()}</span>
<span></span>
<span>C</span>
</kbd>
</Button>
<StyledDivider /> <StyledDivider />
<Button onSelect={() => state.send('DELETED')}> <Button onSelect={() => state.send('DELETED')}>
<span>Delete</span> <span>Delete</span>

View file

@ -1,5 +1,6 @@
import { getShapeStyle } from 'lib/shape-styles'
import { getShapeUtils } from 'lib/shape-utils' import { getShapeUtils } from 'lib/shape-utils'
import { memo } from 'react' import React, { memo } from 'react'
import { useSelector } from 'state' import { useSelector } from 'state'
import { deepCompareArrays, getCurrentCamera, getPage } from 'utils/utils' import { deepCompareArrays, getCurrentCamera, getPage } from 'utils/utils'
import { DotCircle, Handle } from './misc' import { DotCircle, Handle } from './misc'
@ -33,5 +34,11 @@ const Def = memo(function Def({ id }: { id: string }) {
const shape = useSelector((s) => getPage(s.data).shapes[id]) const shape = useSelector((s) => getPage(s.data).shapes[id])
if (!shape) return null if (!shape) return null
return getShapeUtils(shape).render(shape, { isEditing: false })
const style = getShapeStyle(shape.style)
return React.cloneElement(
getShapeUtils(shape).render(shape, { isEditing: false }),
{ ...style }
)
}) })

View file

@ -32,11 +32,11 @@ export default function Selected() {
} }
export const ShapeOutline = memo(function ShapeOutline({ id }: { id: string }) { export const ShapeOutline = memo(function ShapeOutline({ id }: { id: string }) {
const rIndicator = useRef<SVGUseElement>(null) // const rIndicator = useRef<SVGUseElement>(null)
const shape = useSelector((s) => getPage(s.data).shapes[id]) const shape = useSelector((s) => getPage(s.data).shapes[id])
const events = useShapeEvents(id, shape?.type === ShapeType.Group, rIndicator) // const events = useShapeEvents(id, shape?.type === ShapeType.Group, rIndicator)
if (!shape) return null if (!shape) return null
@ -52,23 +52,24 @@ export const ShapeOutline = memo(function ShapeOutline({ id }: { id: string }) {
return ( return (
<SelectIndicator <SelectIndicator
ref={rIndicator} // ref={rIndicator}
as="use" as="use"
href={'#' + id} href={'#' + id}
transform={transform} transform={transform}
isLocked={shape.isLocked} isLocked={shape.isLocked}
{...events} // {...events}
/> />
) )
}) })
const SelectIndicator = styled('path', { const SelectIndicator = styled('path', {
zStrokeWidth: 2, // zStrokeWidth: 2,
strokeLineCap: 'round', strokeLineCap: 'round',
strokeLinejoin: 'round', strokeLinejoin: 'round',
stroke: '$selected', stroke: 'red',
strokeWidth: '10',
pointerEvents: 'none', pointerEvents: 'none',
fill: 'transparent', fill: 'red',
variants: { variants: {
isLocked: { isLocked: {

View file

@ -3,7 +3,7 @@ import state, { useSelector } from 'state'
import styled from 'styles' import styled from 'styles'
import { getShapeUtils } from 'lib/shape-utils' import { getShapeUtils } from 'lib/shape-utils'
import { getBoundsCenter, getPage, isMobile } from 'utils/utils' import { getBoundsCenter, getPage, isMobile } from 'utils/utils'
import { ShapeStyles, ShapeType } from 'types' import { ShapeStyles, ShapeType, Shape as _Shape } from 'types'
import useShapeEvents from 'hooks/useShapeEvents' import useShapeEvents from 'hooks/useShapeEvents'
import vec from 'utils/vec' import vec from 'utils/vec'
import { getShapeStyle } from 'lib/shape-styles' import { getShapeStyle } from 'lib/shape-styles'
@ -60,6 +60,7 @@ function Shape({ id, isSelecting, parentPoint }: ShapeProps) {
return ( return (
<StyledGroup <StyledGroup
id={id + '-group'}
ref={rGroup} ref={rGroup}
transform={transform} transform={transform}
device={isMobileDevice ? 'mobile' : 'desktop'} device={isMobileDevice ? 'mobile' : 'desktop'}
@ -94,6 +95,7 @@ function Shape({ id, isSelecting, parentPoint }: ShapeProps) {
<RealShape <RealShape
isParent={isParent} isParent={isParent}
id={id} id={id}
shape={shape}
style={style} style={style}
isEditing={isEditing} isEditing={isEditing}
/> />
@ -116,15 +118,17 @@ interface RealShapeProps {
id: string id: string
style: Partial<React.SVGProps<SVGUseElement>> style: Partial<React.SVGProps<SVGUseElement>>
isParent: boolean isParent: boolean
shape: _Shape
isEditing: boolean isEditing: boolean
} }
const RealShape = memo(function RealShape({ const RealShape = memo(function RealShape({
id, id,
shape,
style, style,
isParent, isParent,
}: RealShapeProps) { }: RealShapeProps) {
return <StyledShape as="use" data-shy={isParent} href={'#' + id} {...style} /> return <StyledShape as="use" data-shy={isParent} href={'#' + id} />
}) })
const StyledShape = styled('path', { const StyledShape = styled('path', {

View file

@ -192,7 +192,11 @@ export default function useKeyboardEvents() {
} }
case 'c': { case 'c': {
if (metaKey(e)) { if (metaKey(e)) {
if (e.shiftKey) {
state.send('COPIED_TO_SVG', getKeyboardEventInfo(e))
} else {
state.send('COPIED', getKeyboardEventInfo(e)) state.send('COPIED', getKeyboardEventInfo(e))
}
} else { } else {
state.send('SELECTED_ELLIPSE_TOOL', getKeyboardEventInfo(e)) state.send('SELECTED_ELLIPSE_TOOL', getKeyboardEventInfo(e))
} }

16
pages/shhh.tsx Normal file
View file

@ -0,0 +1,16 @@
// import Editor from "components/editor"
import Head from 'next/head'
import dynamic from 'next/dynamic'
const Editor = dynamic(() => import('components/editor'), { ssr: false })
export default function Home() {
return (
<>
<Head>
<title>tldraw</title>
</Head>
<Editor />
</>
)
}

View file

@ -1,4 +1,6 @@
import { getShapeUtils } from 'lib/shape-utils'
import { Data, Shape } from 'types' import { Data, Shape } from 'types'
import { getCommonBounds, getSelectedIds, getSelectedShapes } from 'utils/utils'
import state from './state' import state from './state'
class Clipboard { class Clipboard {
@ -43,6 +45,94 @@ class Clipboard {
clear = () => { clear = () => {
this.current = undefined this.current = undefined
} }
copySelectionToSvg(data: Data) {
const shapes = getSelectedShapes(data)
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
shapes
.sort((a, b) => a.childIndex - b.childIndex)
.forEach((shape) => {
const group = document.getElementById(shape.id + '-group')
const node = document.getElementById(shape.id)
const groupClone = group.cloneNode()
groupClone.appendChild(node.cloneNode(true))
svg.appendChild(groupClone)
})
const bounds = getCommonBounds(
...shapes.map((shape) => getShapeUtils(shape).getBounds(shape))
)
// No content
if (!bounds) return
const padding = 16
// Resize the element to the bounding box
svg.setAttribute(
'viewBox',
[
bounds.minX - padding,
bounds.minY - padding,
bounds.width + padding * 2,
bounds.height + padding * 2,
].join(' ')
)
svg.setAttribute('width', String(bounds.width))
svg.setAttribute('height', String(bounds.height))
// Take a snapshot of the element
const s = new XMLSerializer()
const svgString = s.serializeToString(svg)
// Copy to clipboard!
try {
navigator.clipboard.writeText(svgString)
} catch (e) {
Clipboard.copyStringToClipboard(svgString)
}
}
static copyStringToClipboard(string: string) {
let textarea: HTMLTextAreaElement
let result: boolean | null
textarea = document.createElement('textarea')
textarea.setAttribute('position', 'fixed')
textarea.setAttribute('top', '0')
textarea.setAttribute('readonly', 'true')
textarea.setAttribute('contenteditable', 'true')
textarea.style.position = 'fixed'
textarea.value = string
document.body.appendChild(textarea)
textarea.focus()
textarea.select()
try {
const range = document.createRange()
range.selectNodeContents(textarea)
const sel = window.getSelection()
sel.removeAllRanges()
sel.addRange(range)
textarea.setSelectionRange(0, textarea.value.length)
result = document.execCommand('copy')
} catch (err) {
result = null
} finally {
document.body.removeChild(textarea)
}
if (!result) return false
return true
}
} }
export default new Clipboard() export default new Clipboard()

View file

@ -226,6 +226,7 @@ const state = createState({
ZOOMED_IN: 'zoomIn', ZOOMED_IN: 'zoomIn',
ZOOMED_OUT: 'zoomOut', ZOOMED_OUT: 'zoomOut',
RESET_CAMERA: 'resetCamera', RESET_CAMERA: 'resetCamera',
COPIED_TO_SVG: 'copyToSvg',
}, },
initial: 'notPointing', initial: 'notPointing',
states: { states: {
@ -1640,6 +1641,10 @@ const state = createState({
/* ---------------------- Data ---------------------- */ /* ---------------------- Data ---------------------- */
copyToSvg(data) {
clipboard.copySelectionToSvg(data)
},
copyToClipboard(data) { copyToClipboard(data) {
clipboard.copy(getSelectedShapes(data)) clipboard.copy(getSelectedShapes(data))
}, },