import { uniqueId } from 'utils/utils' import vec from 'utils/vec' import { TextShape, ShapeType, FontSize } 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', }) 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({ 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.fontSize, shape.scale, shape.style) const bounds = this.getBounds(shape) function handleChange(e: React.ChangeEvent) { 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) { e.currentTarget.select() state.send('FOCUSED_EDITING_SHAPE') } return ( {isEditing ? ( ) : ( {text} )} ) }, getBounds(shape) { if (!this.boundsCache.has(shape)) { mdiv.innerHTML = shape.text + '‍' mdiv.style.font = getFontStyle(shape.fontSize, 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) { shape.size = 'auto' 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', display: 'inline-block', position: 'relative', }) const StyledTextArea = styled('textarea', { 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', })