123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230 |
- import React, { useRef, memo, useEffect } from 'react'
- import { useSelector } from 'state'
- import styled from 'styles'
- import { getShapeUtils } from 'state/shape-utils'
- import { getPage, getSelectedIds, isMobile } from 'utils'
- import useShapeEvents from 'hooks/useShapeEvents'
- import { Shape as _Shape } from 'types'
- import vec from 'utils/vec'
- import { getShapeStyle } from 'state/shape-styles'
-
- const isMobileDevice = isMobile()
-
- interface ShapeProps {
- id: string
- isSelecting: boolean
- parentPoint: number[]
- }
-
- function Shape({ id, isSelecting, parentPoint }: ShapeProps): JSX.Element {
- const rGroup = useRef<SVGGElement>(null)
- const rFocusable = useRef<HTMLTextAreaElement>(null)
-
- const isEditing = useSelector((s) => s.data.editingId === id)
-
- const isSelected = useSelector((s) => getSelectedIds(s.data).has(id))
-
- const shape = useSelector((s) => getPage(s.data).shapes[id])
-
- const events = useShapeEvents(id, getShapeUtils(shape)?.isParent, rGroup)
-
- useEffect(() => {
- if (isEditing) {
- setTimeout(() => {
- const elm = rFocusable.current
- if (!elm) return
- elm.focus()
- }, 0)
- }
- }, [isEditing])
-
- // This is a problem with deleted shapes. The hooks in this component
- // may sometimes run before the hook in the Page component, which means
- // a deleted shape will still be pulled here before the page component
- // detects the change and pulls this component.
- if (!shape) return null
-
- const style = getShapeStyle(shape.style)
- const shapeUtils = getShapeUtils(shape)
-
- const { isShy, isParent, isForeignObject } = shapeUtils
-
- const bounds = shapeUtils.getBounds(shape)
- const center = shapeUtils.getCenter(shape)
- const rotation = shape.rotation * (180 / Math.PI)
-
- const transform = `
- translate(${vec.neg(parentPoint)})
- rotate(${rotation}, ${center})
- translate(${shape.point})
- `
-
- return (
- <StyledGroup
- id={id + '-group'}
- ref={rGroup}
- transform={transform}
- isSelected={isSelected}
- device={isMobileDevice ? 'mobile' : 'desktop'}
- {...events}
- >
- {isSelecting && !isShy && (
- <>
- {isForeignObject ? (
- <HoverIndicator
- as="rect"
- width={bounds.width}
- height={bounds.height}
- strokeWidth={1.5}
- variant={'ghost'}
- />
- ) : (
- <HoverIndicator
- as="use"
- href={'#' + id}
- strokeWidth={+style.strokeWidth + 5}
- variant={shapeUtils.canStyleFill ? 'filled' : 'hollow'}
- />
- )}
- </>
- )}
-
- {!shape.isHidden &&
- (isForeignObject ? (
- shapeUtils.render(shape, { isEditing, ref: rFocusable })
- ) : (
- <RealShape id={id} isParent={isParent} shape={shape} />
- ))}
-
- {isParent &&
- shape.children.map((shapeId) => (
- <Shape
- key={shapeId}
- id={shapeId}
- isSelecting={isSelecting}
- parentPoint={shape.point}
- />
- ))}
- </StyledGroup>
- )
- }
-
- interface RealShapeProps {
- id: string
- shape: _Shape
- isParent: boolean
- }
-
- const RealShape = memo(function RealShape({ id, isParent }: RealShapeProps) {
- return <StyledShape as="use" data-shy={isParent} href={'#' + id} />
- })
-
- const StyledShape = styled('path', {
- strokeLinecap: 'round',
- strokeLinejoin: 'round',
- pointerEvents: 'none',
- })
-
- const HoverIndicator = styled('path', {
- stroke: '$selected',
- strokeLinecap: 'round',
- strokeLinejoin: 'round',
- fill: 'transparent',
- filter: 'url(#expand)',
- variants: {
- variant: {
- ghost: {
- pointerEvents: 'all',
- filter: 'none',
- opacity: 0,
- },
- hollow: {
- pointerEvents: 'stroke',
- },
- filled: {
- pointerEvents: 'all',
- },
- },
- },
- })
-
- const StyledGroup = styled('g', {
- outline: 'none',
- [`& *[data-shy="true"]`]: {
- opacity: '0',
- },
- [`& ${HoverIndicator}`]: {
- opacity: '0',
- },
- variants: {
- device: {
- mobile: {},
- desktop: {},
- },
- isSelected: {
- true: {
- [`& *[data-shy="true"]`]: {
- opacity: '1',
- },
- [`& ${HoverIndicator}`]: {
- opacity: '0.2',
- },
- },
- false: {
- [`& ${HoverIndicator}`]: {
- opacity: '0',
- },
- },
- },
- },
- compoundVariants: [
- {
- device: 'desktop',
- isSelected: 'false',
- css: {
- [`&:hover ${HoverIndicator}`]: {
- opacity: '0.16',
- },
- [`&:hover *[data-shy="true"]`]: {
- opacity: '1',
- },
- },
- },
- {
- device: 'desktop',
- isSelected: 'true',
- css: {
- [`&:hover ${HoverIndicator}`]: {
- opacity: '0.25',
- },
- [`&:active ${HoverIndicator}`]: {
- opacity: '0.25',
- },
- },
- },
- ],
- })
-
- // function Label({ children }: { children: React.ReactNode }) {
- // return (
- // <text
- // y={4}
- // x={4}
- // fontSize={12}
- // fill="black"
- // stroke="none"
- // alignmentBaseline="text-before-edge"
- // pointerEvents="none"
- // >
- // {children}
- // </text>
- // )
- // }
-
- // function pp(n: number[]) {
- // return '[' + n.map((v) => v.toFixed(1)).join(', ') + ']'
- // }
-
- export { HoverIndicator }
-
- export default memo(Shape)
|