diff --git a/package-lock.json b/package-lock.json index 35c009e4..29a76ea1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "^1.2.1", "@types/lodash.clonedeep": "^4.5.9", + "immer": "^10.1.1", "konva": "^9.3.12", "lodash.clonedeep": "^4.5.0", "react": "^18.3.1", @@ -3169,6 +3170,15 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", diff --git a/package.json b/package.json index d30ab31f..baaaad90 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "dependencies": { "@atlaskit/pragmatic-drag-and-drop": "^1.2.1", "@types/lodash.clonedeep": "^4.5.9", + "immer": "^10.1.1", "konva": "^9.3.12", "lodash.clonedeep": "^4.5.0", "react": "^18.3.1", diff --git a/src/common/components/front-components/button-shape.tsx b/src/common/components/front-components/button-shape.tsx index d5d114e7..9c5f1ca8 100644 --- a/src/common/components/front-components/button-shape.tsx +++ b/src/common/components/front-components/button-shape.tsx @@ -17,7 +17,10 @@ export const getButtonShapeSizeRestrictions = (): ShapeSizeRestrictions => buttonShapeRestrictions; export const ButtonShape = forwardRef( - ({ x, y, width, height, id, onSelected, text, ...shapeProps }, ref) => { + ( + { x, y, width, height, id, onSelected, text, otherProps, ...shapeProps }, + ref + ) => { const { width: restrictedWidth, height: restrictedHeight } = fitSizeToShapeSizeRestrictions(buttonShapeRestrictions, width, height); @@ -37,9 +40,9 @@ export const ButtonShape = forwardRef( width={restrictedWidth} height={restrictedHeight} cornerRadius={14} - stroke="black" + stroke={otherProps?.stroke ?? 'black'} strokeWidth={2} - fill="white" + fill={otherProps?.backgroundColor ?? 'white'} /> inputShapeRestrictions; export const InputShape = forwardRef( - ({ x, y, width, height, id, onSelected, text, ...shapeProps }, ref) => { + ( + { x, y, width, height, id, onSelected, text, otherProps, ...shapeProps }, + ref + ) => { const { width: restrictedWidth, height: restrictedHeight } = fitSizeToShapeSizeRestrictions(inputShapeRestrictions, width, height); + const stroke = useMemo( + () => otherProps?.stroke ?? 'black', + [otherProps?.stroke] + ); + + const fill = useMemo( + () => otherProps?.backgroundColor ?? 'white', + [otherProps?.backgroundColor] + ); + return ( ( width={restrictedWidth} height={restrictedHeight} cornerRadius={5} - stroke="black" + stroke={stroke} strokeWidth={2} - fill="white" + fill={fill} /> void; + otherProps?: OtherProps; } diff --git a/src/core/model/index.ts b/src/core/model/index.ts index dc2a5886..3c1a0d47 100644 --- a/src/core/model/index.ts +++ b/src/core/model/index.ts @@ -52,6 +52,11 @@ export interface Coord { y: number; } +export interface OtherProps { + stroke?: string; + backgroundColor?: string; +} + export interface ShapeModel { id: string; x: number; @@ -63,4 +68,5 @@ export interface ShapeModel { hasLateralTransformer: boolean; editType?: EditType; text?: string; + otherProps?: OtherProps; } diff --git a/src/core/providers/canvas/canvas.model.ts b/src/core/providers/canvas/canvas.model.ts index 8574dc41..ed30e1c7 100644 --- a/src/core/providers/canvas/canvas.model.ts +++ b/src/core/providers/canvas/canvas.model.ts @@ -1,4 +1,11 @@ -import { Coord, ShapeModel, ShapeRefs, ShapeType, Size } from '@/core/model'; +import { + Coord, + OtherProps, + ShapeModel, + ShapeRefs, + ShapeType, + Size, +} from '@/core/model'; import Konva from 'konva'; import { Node, NodeConfig } from 'konva/lib/Node'; @@ -16,8 +23,14 @@ export interface SelectionInfo { selectedShapeRef: React.MutableRefObject | null>; selectedShapeId: string; selectedShapeType: ShapeType | null; + getSelectedShapeData: () => ShapeModel | undefined; setZIndexOnSelected: (action: ZIndexAction) => void; updateTextOnSelected: (text: string) => void; + // TODO: Update, A. KeyOf B. Move To useSelectionInfo + updateOtherPropsOnSelected: ( + key: K, + value: OtherProps[K] + ) => void; } export interface CanvasContextModel { diff --git a/src/core/providers/canvas/use-selection.hook.ts b/src/core/providers/canvas/use-selection.hook.ts index 295ad52e..1f09686e 100644 --- a/src/core/providers/canvas/use-selection.hook.ts +++ b/src/core/providers/canvas/use-selection.hook.ts @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from 'react'; import Konva from 'konva'; -import { ShapeRefs, ShapeType } from '@/core/model'; +import { OtherProps, ShapeModel, ShapeRefs, ShapeType } from '@/core/model'; import { DocumentModel, SelectionInfo, ZIndexAction } from './canvas.model'; import { performZIndexAction } from './zindex.util'; @@ -69,6 +69,24 @@ export const useSelection = ( })); }; + // TODO: Rather implement this using immmer + + const updateOtherPropsOnSelected = ( + key: K, + value: OtherProps[K] + ) => { + setDocument(prevDocument => ({ + shapes: prevDocument.shapes.map(shape => + shape.id === selectedShapeId + ? { ...shape, otherProps: { ...shape.otherProps, [key]: value } } + : shape + ), + })); + }; + + const getSelectedShapeData = (): ShapeModel | undefined => + document.shapes.find(shape => shape.id === selectedShapeId); + return { transformerRef, shapeRefs, @@ -77,7 +95,9 @@ export const useSelection = ( selectedShapeRef, selectedShapeId, selectedShapeType, + getSelectedShapeData, setZIndexOnSelected, updateTextOnSelected, + updateOtherPropsOnSelected, }; }; diff --git a/src/pods/canvas/canvas.model.ts b/src/pods/canvas/canvas.model.ts index 4aeb73fb..08f83e1b 100644 --- a/src/pods/canvas/canvas.model.ts +++ b/src/pods/canvas/canvas.model.ts @@ -1,4 +1,11 @@ -import { Coord, ShapeType, Size, ShapeModel, EditType } from '@/core/model'; +import { + Coord, + ShapeType, + Size, + ShapeModel, + EditType, + OtherProps, +} from '@/core/model'; import { v4 as uuidv4 } from 'uuid'; import { @@ -233,6 +240,18 @@ const getShapeEditInlineType = (shapeType: ShapeType): EditType | undefined => { return result; }; +export const generateDefaultOtherProps = ( + shapeType: ShapeType +): OtherProps | undefined => { + switch (shapeType) { + case 'input': + case 'button': + return { stroke: '#000000', backgroundColor: '#FFFFFF' }; + default: + return undefined; + } +}; + // TODO: create interfaces to hold Coordination and Size // coordinate: { x: number, y: number } // size: { width: number, height: number } @@ -251,6 +270,7 @@ export const createShape = (coord: Coord, shapeType: ShapeType): ShapeModel => { hasLateralTransformer: doesShapeHaveLateralTransformer(shapeType), text: generateDefaultTextValue(shapeType), editType: getShapeEditInlineType(shapeType), + otherProps: generateDefaultOtherProps(shapeType), }; }; diff --git a/src/pods/canvas/shape-renderer/simple-component/button.renderer.tsx b/src/pods/canvas/shape-renderer/simple-component/button.renderer.tsx index 91998dbc..36627214 100644 --- a/src/pods/canvas/shape-renderer/simple-component/button.renderer.tsx +++ b/src/pods/canvas/shape-renderer/simple-component/button.renderer.tsx @@ -26,6 +26,7 @@ export const renderButton = ( onTransformEnd={handleTransform} isEditable={shape.allowsInlineEdition} text={shape.text} + otherProps={shape.otherProps} /> ); }; diff --git a/src/pods/canvas/shape-renderer/simple-component/input.renderer.tsx b/src/pods/canvas/shape-renderer/simple-component/input.renderer.tsx index 688a717c..2050368b 100644 --- a/src/pods/canvas/shape-renderer/simple-component/input.renderer.tsx +++ b/src/pods/canvas/shape-renderer/simple-component/input.renderer.tsx @@ -26,6 +26,7 @@ export const renderInput = ( onTransformEnd={handleTransform} isEditable={shape.allowsInlineEdition} text={shape.text} + otherProps={shape.otherProps} /> ); }; diff --git a/src/pods/properties/components/color-picker.component.tsx b/src/pods/properties/components/color-picker.component.tsx new file mode 100644 index 00000000..1aab5e23 --- /dev/null +++ b/src/pods/properties/components/color-picker.component.tsx @@ -0,0 +1,20 @@ +interface Props { + label: string; + color: string; + onChange: (color: string) => void; +} + +export const ColorPicker: React.FC = props => { + const { label, color, onChange } = props; + + return ( +
+ + onChange(e.target.value)} + /> +
+ ); +}; diff --git a/src/pods/properties/properties.pod.tsx b/src/pods/properties/properties.pod.tsx index 162402b9..fab1afda 100644 --- a/src/pods/properties/properties.pod.tsx +++ b/src/pods/properties/properties.pod.tsx @@ -1,9 +1,11 @@ import { useCanvasContext } from '@/core/providers'; import classes from './properties.pod.module.css'; import { ZIndexOptions } from './components/zindex/zindex-option.component'; +import { ColorPicker } from './components/color-picker.component'; export const PropertiesPod = () => { const { selectionInfo } = useCanvasContext(); + const { getSelectedShapeData, updateOtherPropsOnSelected } = selectionInfo; const selectedShapeID = selectionInfo?.selectedShapeRef.current ?? null; @@ -11,12 +13,30 @@ export const PropertiesPod = () => { return null; } + const selectedShapeData = getSelectedShapeData(); + return (

Properties

+ {selectedShapeData?.otherProps?.stroke && ( + updateOtherPropsOnSelected('stroke', color)} + /> + )} + {selectedShapeData?.otherProps?.backgroundColor && ( + + updateOtherPropsOnSelected('backgroundColor', color) + } + /> + )}
); };