Adds copy to svg
This commit is contained in:
parent
124b70b262
commit
fc2e3b3c4c
9 changed files with 148 additions and 14 deletions
|
@ -30,10 +30,9 @@ export default function Canvas() {
|
|||
<MainSVG ref={rCanvas} {...events}>
|
||||
<Defs />
|
||||
{isReady && (
|
||||
<g ref={rGroup}>
|
||||
<g ref={rGroup} id="shapes">
|
||||
<BoundsBg />
|
||||
<Page />
|
||||
{/* <Selected /> */}
|
||||
<Bounds />
|
||||
<Handles />
|
||||
<Brush />
|
||||
|
|
|
@ -208,6 +208,14 @@ export default function ContextMenu({
|
|||
/>
|
||||
)}
|
||||
<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 />
|
||||
<Button onSelect={() => state.send('DELETED')}>
|
||||
<span>Delete</span>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { getShapeStyle } from 'lib/shape-styles'
|
||||
import { getShapeUtils } from 'lib/shape-utils'
|
||||
import { memo } from 'react'
|
||||
import React, { memo } from 'react'
|
||||
import { useSelector } from 'state'
|
||||
import { deepCompareArrays, getCurrentCamera, getPage } from 'utils/utils'
|
||||
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])
|
||||
|
||||
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 }
|
||||
)
|
||||
})
|
||||
|
|
|
@ -32,11 +32,11 @@ export default function Selected() {
|
|||
}
|
||||
|
||||
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 events = useShapeEvents(id, shape?.type === ShapeType.Group, rIndicator)
|
||||
// const events = useShapeEvents(id, shape?.type === ShapeType.Group, rIndicator)
|
||||
|
||||
if (!shape) return null
|
||||
|
||||
|
@ -52,23 +52,24 @@ export const ShapeOutline = memo(function ShapeOutline({ id }: { id: string }) {
|
|||
|
||||
return (
|
||||
<SelectIndicator
|
||||
ref={rIndicator}
|
||||
// ref={rIndicator}
|
||||
as="use"
|
||||
href={'#' + id}
|
||||
transform={transform}
|
||||
isLocked={shape.isLocked}
|
||||
{...events}
|
||||
// {...events}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
const SelectIndicator = styled('path', {
|
||||
zStrokeWidth: 2,
|
||||
// zStrokeWidth: 2,
|
||||
strokeLineCap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: '$selected',
|
||||
stroke: 'red',
|
||||
strokeWidth: '10',
|
||||
pointerEvents: 'none',
|
||||
fill: 'transparent',
|
||||
fill: 'red',
|
||||
|
||||
variants: {
|
||||
isLocked: {
|
||||
|
|
|
@ -3,7 +3,7 @@ import state, { useSelector } from 'state'
|
|||
import styled from 'styles'
|
||||
import { getShapeUtils } from 'lib/shape-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 vec from 'utils/vec'
|
||||
import { getShapeStyle } from 'lib/shape-styles'
|
||||
|
@ -60,6 +60,7 @@ function Shape({ id, isSelecting, parentPoint }: ShapeProps) {
|
|||
|
||||
return (
|
||||
<StyledGroup
|
||||
id={id + '-group'}
|
||||
ref={rGroup}
|
||||
transform={transform}
|
||||
device={isMobileDevice ? 'mobile' : 'desktop'}
|
||||
|
@ -94,6 +95,7 @@ function Shape({ id, isSelecting, parentPoint }: ShapeProps) {
|
|||
<RealShape
|
||||
isParent={isParent}
|
||||
id={id}
|
||||
shape={shape}
|
||||
style={style}
|
||||
isEditing={isEditing}
|
||||
/>
|
||||
|
@ -116,15 +118,17 @@ interface RealShapeProps {
|
|||
id: string
|
||||
style: Partial<React.SVGProps<SVGUseElement>>
|
||||
isParent: boolean
|
||||
shape: _Shape
|
||||
isEditing: boolean
|
||||
}
|
||||
|
||||
const RealShape = memo(function RealShape({
|
||||
id,
|
||||
shape,
|
||||
style,
|
||||
isParent,
|
||||
}: RealShapeProps) {
|
||||
return <StyledShape as="use" data-shy={isParent} href={'#' + id} {...style} />
|
||||
return <StyledShape as="use" data-shy={isParent} href={'#' + id} />
|
||||
})
|
||||
|
||||
const StyledShape = styled('path', {
|
||||
|
|
|
@ -192,7 +192,11 @@ export default function useKeyboardEvents() {
|
|||
}
|
||||
case 'c': {
|
||||
if (metaKey(e)) {
|
||||
if (e.shiftKey) {
|
||||
state.send('COPIED_TO_SVG', getKeyboardEventInfo(e))
|
||||
} else {
|
||||
state.send('COPIED', getKeyboardEventInfo(e))
|
||||
}
|
||||
} else {
|
||||
state.send('SELECTED_ELLIPSE_TOOL', getKeyboardEventInfo(e))
|
||||
}
|
||||
|
|
16
pages/shhh.tsx
Normal file
16
pages/shhh.tsx
Normal 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 />
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
import { getShapeUtils } from 'lib/shape-utils'
|
||||
import { Data, Shape } from 'types'
|
||||
import { getCommonBounds, getSelectedIds, getSelectedShapes } from 'utils/utils'
|
||||
import state from './state'
|
||||
|
||||
class Clipboard {
|
||||
|
@ -43,6 +45,94 @@ class Clipboard {
|
|||
clear = () => {
|
||||
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()
|
||||
|
|
|
@ -226,6 +226,7 @@ const state = createState({
|
|||
ZOOMED_IN: 'zoomIn',
|
||||
ZOOMED_OUT: 'zoomOut',
|
||||
RESET_CAMERA: 'resetCamera',
|
||||
COPIED_TO_SVG: 'copyToSvg',
|
||||
},
|
||||
initial: 'notPointing',
|
||||
states: {
|
||||
|
@ -1640,6 +1641,10 @@ const state = createState({
|
|||
|
||||
/* ---------------------- Data ---------------------- */
|
||||
|
||||
copyToSvg(data) {
|
||||
clipboard.copySelectionToSvg(data)
|
||||
},
|
||||
|
||||
copyToClipboard(data) {
|
||||
clipboard.copy(getSelectedShapes(data))
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue