From a85dba9ab0bfcadad363be36b0ead5899564da4c Mon Sep 17 00:00:00 2001 From: Alessandro Amantini Date: Thu, 24 Oct 2024 14:38:10 +0100 Subject: [PATCH] ISSUE #5198 - plain text works, but callout is not immidiately shaped --- frontend/src/v4/.eslintrc.js | 1 - .../handleCalloutDrawing.component.tsx | 76 +++++++------ .../editableText/editableText.component.tsx | 32 +++--- .../editableText/editableText.styles.ts | 8 +- .../typingCalloutHandler.component.tsx | 107 ++++++++++++++++++ .../typingHandler/typingHandler.component.tsx | 26 +---- 6 files changed, 174 insertions(+), 76 deletions(-) create mode 100644 frontend/src/v4/routes/components/screenshotDialog/components/typingHandler/typingCalloutHandler.component.tsx diff --git a/frontend/src/v4/.eslintrc.js b/frontend/src/v4/.eslintrc.js index 3d87b7e437e..e8766ddff83 100644 --- a/frontend/src/v4/.eslintrc.js +++ b/frontend/src/v4/.eslintrc.js @@ -79,7 +79,6 @@ module.exports = { 'brace-style': ['error', '1tbs'], 'block-scoped-var': 'error', 'comma-style': ['error', 'last'], - curly: 'error', eqeqeq: 'error', 'import/order': [ 'error', diff --git a/frontend/src/v4/routes/components/screenshotDialog/components/drawingHandler/handleCalloutDrawing/handleCalloutDrawing.component.tsx b/frontend/src/v4/routes/components/screenshotDialog/components/drawingHandler/handleCalloutDrawing/handleCalloutDrawing.component.tsx index ebbe17ae71d..ede7cbe2038 100644 --- a/frontend/src/v4/routes/components/screenshotDialog/components/drawingHandler/handleCalloutDrawing/handleCalloutDrawing.component.tsx +++ b/frontend/src/v4/routes/components/screenshotDialog/components/drawingHandler/handleCalloutDrawing/handleCalloutDrawing.component.tsx @@ -19,10 +19,9 @@ import { isEmpty } from 'lodash'; import { renderWhenTrue } from '../../../../../../helpers/rendering'; import { batchGroupBy } from '../../../../../../modules/canvasHistory/canvasHistory.helpers'; import { COLOR } from '../../../../../../styles'; -import { MODES } from '../../../markupStage/markupStage.helpers'; import { SHAPE_TYPES } from '../../shape/shape.constants'; -import { TypingHandler } from '../../typingHandler/typingHandler.component'; +import { TypingCalloutHandler } from '../../typingHandler/typingCalloutHandler.component'; import { createDrawnLine, createShape, getDrawFunction } from '../drawingHandler.helpers'; import { HandleBaseDrawing, IHandleBaseDrawingProps, IHandleBaseDrawingStates, @@ -171,37 +170,35 @@ export class HandleCalloutDrawing } public handleMouseMoveLine = () => { - if (this.state.isCurrentlyDrawn) { - if (isEmpty(this.lastShape)) { - this.layer.clearBeforeDraw(); - const { x, y } = this.pointerPosition; - - this.lastPointerPosition = this.initialPointerPosition = { - x, - y - }; - - const initialPositionProps = { - x: this.lastPointerPosition.x, - y: this.initialPointerPosition.y - }; - - const commonProps = { - stroke: this.props.color, - strokeWidth: this.props.size, - draggable: false, - fill: COLOR.WHITE, - }; - - this.lastShape = createShape(SHAPE_TYPES.RECTANGLE, commonProps, initialPositionProps); - this.layer.add(this.lastShape); - this.setState({ - lastShape : this.lastShape, - }); - } else { - this.lastLine.points(getLinePoints(this.shape, this.lastShape)); - this.layer.batchDraw(); - } + if (this.state.isCurrentlyDrawn && isEmpty(this.lastShape)) { + this.layer.clearBeforeDraw(); + const { x, y } = this.pointerPosition; + + this.lastPointerPosition = this.initialPointerPosition = { + x, + y + }; + + const initialPositionProps = { + x: this.lastPointerPosition.x, + y: this.initialPointerPosition.y + }; + + const commonProps = { + stroke: this.props.color, + strokeWidth: this.props.size, + draggable: false, + fill: COLOR.WHITE, + }; + + this.lastShape = createShape(SHAPE_TYPES.RECTANGLE, commonProps, initialPositionProps); + this.layer.add(this.lastShape); + this.setState({ + lastShape : this.lastShape, + }); + } else { + this.lastLine.points(getLinePoints(this.shape, this.lastShape)); + this.layer.batchDraw(); } } @@ -297,8 +294,9 @@ export class HandleCalloutDrawing } public renderEditableTextarea = renderWhenTrue(() => ( - + {console.log(this.lastShape)} + + )); public render() { - return this.renderEditableTextarea(this.state.calloutState === CalloutState.POSITIONING_TEXT_BOX); + return this.renderEditableTextarea( + this.state.calloutState === CalloutState.POSITIONING_TEXT_BOX && + this.props.stage && + !this.props.selected + ); } } diff --git a/frontend/src/v4/routes/components/screenshotDialog/components/editableText/editableText.component.tsx b/frontend/src/v4/routes/components/screenshotDialog/components/editableText/editableText.component.tsx index 7393a1ceb7c..84aa653ded2 100644 --- a/frontend/src/v4/routes/components/screenshotDialog/components/editableText/editableText.component.tsx +++ b/frontend/src/v4/routes/components/screenshotDialog/components/editableText/editableText.component.tsx @@ -22,25 +22,29 @@ import { TextBox } from './editableText.styles'; interface IProps { styles: CSSProperties; - onAddText: (newtext: string, width: number) => void; - onChange?: ({ width, height }) => void; + disabled?: boolean; + onAddText?: (newtext: string, width: number) => void; + onResize?: ({ width, height }) => void; onClick?: () => void; } const pxToNumber = (size: string) => +size.replaceAll('px', ''); -export const EditableText = ({ styles, onAddText, onChange, onClick }: IProps) => { +export const EditableText = ({ styles, disabled, onAddText, onClick, onResize }: IProps) => { const ref = useRef(null); const [size, setSize] = useState({ width: 0, height: 0 }); const updateSize = () => { + if (!ref.current) return; const compStyles = getComputedStyle(ref.current); - setSize({ + const newSize = { width: pxToNumber(compStyles.width), height: pxToNumber(compStyles.height), - }); + }; + onResize?.(newSize); + setSize(newSize); } - const saveText = () => onAddText(ref.current.innerText, size.width); + const saveText = () => onAddText?.(ref.current.innerText, size.width); const handleKeyDown = (e) => { if (e.keyCode === 13 && !e.shiftKey) { @@ -50,14 +54,12 @@ export const EditableText = ({ styles, onAddText, onChange, onClick }: IProps) = useEffect(() => { setTimeout(() => { - ref.current?.focus(); - updateSize(); + new ResizeObserver(updateSize).observe(ref.current); + if (!disabled) { + ref.current?.focus(); + } }); - }, []); - - useEffect(() => { - onChange?.(size); - }, [size]); + }, [disabled]); return ( @@ -67,9 +69,9 @@ export const EditableText = ({ styles, onAddText, onChange, onClick }: IProps) = $placeholder={EDITABLE_TEXTAREA_PLACEHOLDER} style={styles} onKeyDown={handleKeyDown} - onInput={updateSize} onClick={onClick} - contentEditable + contentEditable={!disabled} + disabled={disabled} /> ); diff --git a/frontend/src/v4/routes/components/screenshotDialog/components/editableText/editableText.styles.ts b/frontend/src/v4/routes/components/screenshotDialog/components/editableText/editableText.styles.ts index 34232c6a9c2..659b7f9f036 100644 --- a/frontend/src/v4/routes/components/screenshotDialog/components/editableText/editableText.styles.ts +++ b/frontend/src/v4/routes/components/screenshotDialog/components/editableText/editableText.styles.ts @@ -15,9 +15,9 @@ * along with this program. If not, see . */ -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; -export const TextBox = styled.div<{ $placeholder: string }>` +export const TextBox = styled.div<{ $placeholder: string; disabled?: boolean }>` font-family: 'Arial', sans-serif; line-height: 1; position: absolute; @@ -33,6 +33,10 @@ export const TextBox = styled.div<{ $placeholder: string }>` position: sticky; bottom: 0; + ${({ disabled }) => disabled && css` + pointer-events: none; + `} + &:focus { outline: none; } diff --git a/frontend/src/v4/routes/components/screenshotDialog/components/typingHandler/typingCalloutHandler.component.tsx b/frontend/src/v4/routes/components/screenshotDialog/components/typingHandler/typingCalloutHandler.component.tsx new file mode 100644 index 00000000000..8678394907a --- /dev/null +++ b/frontend/src/v4/routes/components/screenshotDialog/components/typingHandler/typingCalloutHandler.component.tsx @@ -0,0 +1,107 @@ +/** + * Copyright (C) 2024 3D Repo Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { useState, useEffect, CSSProperties } from 'react'; +import Konva from 'konva'; +import { isEmpty } from 'lodash'; +import { EditableText } from '../editableText/editableText.component'; + +interface IProps { + stage: Konva.Stage; + layer: Konva.Layer; + boxRef?: any; + fontSize: number; + size?: number; + color: string; + onRefreshDrawingLayer?: () => void; + onAddNewText: (position, text: string, width: number, updateState?: boolean) => void; +} + +export const TypingCalloutHandler = ({ + stage, layer, boxRef, fontSize, size, color, onRefreshDrawingLayer, onAddNewText +}: IProps) => { + const [isPositionLocked, setIsPositionLocked] = useState(false); + const [offset, setOffset] = useState({}); + const [maxSizes, setMaxSizes] = useState({ maxWidth: 0, maxHeight: 0 }); + + const setTextPosition = () => { + let left = 0, top = 0; + if (stage && layer) { + const position = stage.getPointerPosition(); + left = position.x - layer.x(); + top = position.y - layer.y(); + } + setOffset({ top, left }); + const { width, height } = stage.attrs; + setMaxSizes({ + maxWidth: width - left, + maxHeight: height - top, + }); + }; + + const onTextChange = (newText: string, width: number) => { + onAddNewText({ x: offset.left, y: offset.top }, newText, width); + setIsPositionLocked(false); + }; + + const redrawCalloutBox = ({ width, height }) => { + if (!isEmpty(boxRef) && onRefreshDrawingLayer) { + boxRef.width(width + Math.max(6, size * 2)); + boxRef.height(height + Math.max(6, size * 2)); + onRefreshDrawingLayer(); + } + } + + useEffect(() => { + if (isPositionLocked) return; + + const handleMouseMove = () => setTextPosition(); + const handleClick = () => setTimeout(() => setIsPositionLocked(true)); + + stage.on('mousemove touchmove', handleMouseMove); + stage.on('click touchstart', handleClick); + + return () => { + stage.off('mousemove touchmove', handleMouseMove); + stage.off('click touchstart', handleClick); + } + }, [isPositionLocked]); + + useEffect(() => { + if (!isEmpty(boxRef) && !isEmpty(offset) && onRefreshDrawingLayer) { + boxRef.x(Number(offset.left) - Math.max(3, size)); + boxRef.y(Number(offset.top) - Math.max(3, size)); + onRefreshDrawingLayer(); + } + }, [offset]); + + const styles = { + ...offset, + ...maxSizes, + color, + fontSize: `${fontSize}px`, + }; + + if (!isPositionLocked) return ; + + return ( + + ); +}; diff --git a/frontend/src/v4/routes/components/screenshotDialog/components/typingHandler/typingHandler.component.tsx b/frontend/src/v4/routes/components/screenshotDialog/components/typingHandler/typingHandler.component.tsx index 392a0875baa..c66fc82b0d9 100644 --- a/frontend/src/v4/routes/components/screenshotDialog/components/typingHandler/typingHandler.component.tsx +++ b/frontend/src/v4/routes/components/screenshotDialog/components/typingHandler/typingHandler.component.tsx @@ -14,7 +14,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -import { useState, useEffect, useCallback, CSSProperties, useRef } from 'react'; +import { useState, useEffect, useCallback, CSSProperties } from 'react'; import Konva from 'konva'; import { isEmpty } from 'lodash'; @@ -41,14 +41,11 @@ export const TypingHandler = ({ const [offset, setOffset] = useState({}); const [positionLocked, setPositionLocked] = useState(false); const [maxSizes, setMaxSizes] = useState({ maxWidth: 0, maxHeight: 0 }); - const isCallout = !!boxRef; useEffect(() => { - if (stage) { - if (mode === MODES.TEXT && !visible && !positionLocked && !selected) { - stage.on('mousemove touchmove', handleMouseMove); - stage.on('click touchstart', handleClick); - } + if (stage && mode === MODES.TEXT && !visible && !positionLocked && !selected) { + stage.on('mousemove touchmove', handleMouseMove); + stage.on('click touchstart', handleClick); return () => { stage.off('mousemove touchmove', handleMouseMove); @@ -72,14 +69,6 @@ export const TypingHandler = ({ }); }; - const redrawCalloutBox = ({ width, height }) => { - if (!isEmpty(boxRef) && onRefreshDrawingLayer) { - boxRef.width(width + Math.max(6, size * 2)); - boxRef.height(height + Math.max(6, size * 2)); - onRefreshDrawingLayer(); - } - } - const handleMouseMove = () => { if (!positionLocked) { setTextPosition(); @@ -106,20 +95,15 @@ export const TypingHandler = ({ setPositionLocked(false); }; - if (!visible) { - return null; - } + if (!visible) return null; return (