| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266 | import { uniqueId, isMobile } from 'utils/utils'
import vec from 'utils/vec'
import { TextShape, ShapeType, FontSize, SizeStyle } from 'types'
import { registerShapeUtils } from './index'
import { defaultStyle, getFontStyle, getShapeStyle } from 'lib/shape-styles'
import styled from 'styles'
import state from 'state'
import { useEffect, useRef } from 'react'
// A div used for measurement
if (document.getElementById('__textMeasure')) {
  document.getElementById('__textMeasure').remove()
}
// A div used for measurement
const mdiv = document.createElement('pre')
mdiv.id = '__textMeasure'
Object.assign(mdiv.style, {
  whiteSpace: 'pre',
  width: 'auto',
  border: '1px solid red',
  padding: '4px',
  margin: '0px',
  opacity: '0',
  position: 'absolute',
  top: '-500px',
  left: '0px',
  zIndex: '9999',
  pointerEvents: 'none',
})
mdiv.tabIndex = -1
document.body.appendChild(mdiv)
function normalizeText(text: string) {
  return text.replace(/\t/g, '        ').replace(/\r?\n|\r/g, '\n')
}
const text = registerShapeUtils<TextShape>({
  isForeignObject: true,
  canChangeAspectRatio: false,
  canEdit: true,
  boundsCache: new WeakMap([]),
  create(props) {
    return {
      id: uniqueId(),
      seed: Math.random(),
      type: ShapeType.Text,
      isGenerated: false,
      name: 'Text',
      parentId: 'page1',
      childIndex: 0,
      point: [0, 0],
      rotation: 0,
      isAspectRatioLocked: false,
      isLocked: false,
      isHidden: false,
      style: defaultStyle,
      text: '',
      scale: 1,
      size: 'auto',
      fontSize: FontSize.Medium,
      ...props,
    }
  },
  render(shape, { isEditing, ref }) {
    const { id, text, style } = shape
    const styles = getShapeStyle(style)
    const font = getFontStyle(shape.scale, shape.style)
    const bounds = this.getBounds(shape)
    function handleChange(e: React.ChangeEvent<HTMLTextAreaElement>) {
      state.send('EDITED_SHAPE', {
        change: { text: normalizeText(e.currentTarget.value) },
      })
    }
    function handleKeyDown(e: React.KeyboardEvent) {
      e.stopPropagation()
      if (e.key === 'Tab') {
        e.preventDefault()
      }
    }
    function handleBlur() {
      state.send('BLURRED_EDITING_SHAPE')
    }
    function handleFocus(e: React.FocusEvent<HTMLTextAreaElement>) {
      e.currentTarget.select()
      state.send('FOCUSED_EDITING_SHAPE')
    }
    return (
      <foreignObject
        id={id}
        x={0}
        y={0}
        width={bounds.width}
        height={bounds.height}
        pointerEvents="none"
      >
        {isEditing ? (
          <StyledTextArea
            ref={ref}
            style={{
              font,
              color: styles.stroke,
            }}
            value={text}
            tabIndex={0}
            autoComplete="false"
            autoCapitalize="false"
            autoCorrect="false"
            autoSave="false"
            placeholder=""
            name="text"
            autoFocus={isMobile() ? true : false}
            onFocus={handleFocus}
            onBlur={handleBlur}
            onKeyDown={handleKeyDown}
            onChange={handleChange}
          />
        ) : (
          <StyledText
            style={{
              font,
              color: styles.stroke,
            }}
          >
            {text}
          </StyledText>
        )}
      </foreignObject>
    )
  },
  getBounds(shape) {
    if (!this.boundsCache.has(shape)) {
      mdiv.innerHTML = shape.text + '‍'
      mdiv.style.font = getFontStyle(shape.scale, shape.style)
      const [minX, minY] = shape.point
      const [width, height] = [mdiv.offsetWidth, mdiv.offsetHeight]
      this.boundsCache.set(shape, {
        minX,
        maxX: minX + width,
        minY,
        maxY: minY + height,
        width,
        height,
      })
    }
    return this.boundsCache.get(shape)
  },
  hitTest(shape, test) {
    return true
  },
  transform(shape, bounds, { initialShape, transformOrigin, scaleX, scaleY }) {
    if (shape.rotation === 0 && !shape.isAspectRatioLocked) {
      shape.point = [bounds.minX, bounds.minY]
      shape.scale = initialShape.scale * Math.abs(scaleX)
    } else {
      shape.point = [bounds.minX, bounds.minY]
      shape.rotation =
        (scaleX < 0 && scaleY >= 0) || (scaleY < 0 && scaleX >= 0)
          ? -initialShape.rotation
          : initialShape.rotation
      shape.scale = initialShape.scale * Math.abs(Math.min(scaleX, scaleY))
    }
    return this
  },
  transformSingle(shape, bounds, { initialShape, scaleX }) {
    shape.point = [bounds.minX, bounds.minY]
    shape.scale = initialShape.scale * Math.abs(scaleX)
    return this
  },
  onBoundsReset(shape) {
    const center = this.getCenter(shape)
    this.boundsCache.delete(shape)
    shape.scale = 1
    const newCenter = this.getCenter(shape)
    shape.point = vec.add(shape.point, vec.sub(center, newCenter))
    return this
  },
  applyStyles(shape, style) {
    const center = this.getCenter(shape)
    this.boundsCache.delete(shape)
    Object.assign(shape.style, style)
    const newCenter = this.getCenter(shape)
    shape.point = vec.add(shape.point, vec.sub(center, newCenter))
    return this
  },
  shouldDelete(shape) {
    return shape.text.length === 0
  },
})
export default text
const StyledText = styled('div', {
  width: '100%',
  height: '100%',
  border: 'none',
  padding: '4px',
  whiteSpace: 'pre',
  minHeight: 1,
  minWidth: 1,
  outline: 0,
  backgroundColor: 'transparent',
  overflow: 'hidden',
  pointerEvents: 'none',
  userSelect: 'none',
  WebkitUserSelect: 'none',
  display: 'inline-block',
})
const StyledTextArea = styled('textarea', {
  zIndex: 1,
  width: '100%',
  height: '100%',
  border: 'none',
  padding: '4px',
  whiteSpace: 'pre',
  resize: 'none',
  minHeight: 1,
  minWidth: 1,
  outline: 0,
  backgroundColor: '$boundsBg',
  overflow: 'hidden',
  pointerEvents: 'all',
  backfaceVisibility: 'hidden',
  display: 'inline-block',
  userSelect: 'text',
  WebkitUserSelect: 'text',
  WebkitTouchCallout: 'none',
})
 |