Skip to content

Commit

Permalink
Merge pull request #89 from Lemoncode/feature/#34-inline-edit-clean
Browse files Browse the repository at this point in the history
inline edit basics
  • Loading branch information
brauliodiez authored Jul 31, 2024
2 parents f2068cf + 5edf5b6 commit cf64a85
Show file tree
Hide file tree
Showing 14 changed files with 193 additions and 12 deletions.
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-konva": "^18.2.10",
"react-konva-utils": "^1.0.6",
"tiny-invariant": "^1.3.3",
"uuid": "^10.0.0"
},
Expand Down
4 changes: 2 additions & 2 deletions src/common/components/front-components/input-shape.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const getInputShapeSizeRestrictions = (): ShapeSizeRestrictions =>
inputShapeRestrictions;

export const InputShape = forwardRef<any, ShapeProps>(
({ x, y, width, height, id, onSelected, ...shapeProps }, ref) => {
({ x, y, width, height, id, onSelected, text, ...shapeProps }, ref) => {
const { width: restrictedWidth, height: restrictedHeight } =
fitSizeToShapeSizeRestrictions(inputShapeRestrictions, width, height);

Expand Down Expand Up @@ -46,7 +46,7 @@ export const InputShape = forwardRef<any, ShapeProps>(
y={20}
width={width - 10}
height={height - 20}
text="Input text..."
text={text}
fontFamily="Comic Sans MS, cursive"
fontSize={15}
fill="gray"
Expand Down
4 changes: 2 additions & 2 deletions src/common/components/front-components/label-shape.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const getLabelSizeRestrictions = (): ShapeSizeRestrictions =>
labelSizeRestrictions;

export const LabelShape = forwardRef<any, ShapeProps>(
({ x, y, width, height, id, onSelected, ...shapeProps }, ref) => {
({ x, y, width, height, id, onSelected, text, ...shapeProps }, ref) => {
const { width: restrictedWidth, height: restrictedHeight } =
fitSizeToShapeSizeRestrictions(labelSizeRestrictions, width, height);

Expand All @@ -36,7 +36,7 @@ export const LabelShape = forwardRef<any, ShapeProps>(
y={0}
width={restrictedWidth}
height={restrictedHeight}
text="Label"
text={text}
fontFamily="Comic Sans MS, cursive"
fontSize={15}
fill="black"
Expand Down
1 change: 1 addition & 0 deletions src/common/components/inline-edit/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './inline-edit';
103 changes: 103 additions & 0 deletions src/common/components/inline-edit/inline-edit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { Coord, Size } from '@/core/model';
import React, { useEffect, useRef, useState } from 'react';
import { Group } from 'react-konva';
import { Html } from 'react-konva-utils';

type EditType = 'input' | 'textarea';

interface Props {
coords: Coord;
size: Size;
isEditable: boolean;
editType: EditType;
text: string;
scale: number;
onTextSubmit: (text: string) => void;
children: React.ReactNode;
}

export const EditableComponent: React.FC<Props> = props => {
const { coords, size, isEditable, text, onTextSubmit, scale, children } =
props;
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(text);

const inputRef = useRef<HTMLInputElement>(null);

// handle click outside of the input when editing
useEffect(() => {
if (!isEditable) return;

const handleClickOutside = (event: MouseEvent) => {
if (
inputRef.current &&
!inputRef.current.contains(event.target as Node)
) {
setIsEditing(false);
onTextSubmit(editText);
}
};

if (isEditing) {
document.addEventListener('mousedown', handleClickOutside);
} else {
document.removeEventListener('mousedown', handleClickOutside);
}

return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isEditing, editText]);

const handleDoubleClick = () => {
if (isEditable) {
setIsEditing(true);
}
};

// TODO: this can be optimized using React.useCallback, issue #90
// https://github.com/Lemoncode/quickmock/issues/90
const calculateTextAreaXPosition = () => {
return `${coords.x * scale}px`;
};

const calculateTextAreaYPosition = () => {
return `${coords.y * scale}px`;
};

const calculateWidth = () => {
return `${size.width}px`;
};

const calculateHeight = () => {
return `${size.height}px`;
};

// TODO: Componentize this #91
// https://github.com/Lemoncode/quickmock/issues/91
return (
<>
<Group onDblClick={handleDoubleClick}>{children}</Group>
{isEditing ? (
<Html
divProps={{
style: {
position: 'absolute',
top: calculateTextAreaYPosition(),
left: calculateTextAreaXPosition(),
width: calculateWidth(),
height: calculateHeight(),
},
}}
>
<input
ref={inputRef}
style={{ width: '100%', height: '100%' }}
value={editText}
onChange={e => setEditText(e.target.value)}
/>
</Html>
) : null}
</>
);
};
2 changes: 2 additions & 0 deletions src/core/model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,6 @@ export interface ShapeModel {
width: number;
height: number;
type: ShapeType;
allowsInlineEdition: boolean;
text?: string;
}
1 change: 1 addition & 0 deletions src/core/providers/canvas/canvas.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface SelectionInfo {
selectedShapeId: string;
selectedShapeType: ShapeType | null;
setZIndexOnSelected: (action: ZIndexAction) => void;
updateTextOnSelected: (text: string) => void;
}

export interface CanvasContextModel {
Expand Down
1 change: 0 additions & 1 deletion src/core/providers/canvas/canvas.provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React from 'react';
import { ShapeModel } from '@/core/model';
import { CanvasContext } from './canvas.context';
import { useSelection } from './use-selection.hook';
import { ZIndexAction } from './canvas.model';

interface Props {
children: React.ReactNode;
Expand Down
9 changes: 9 additions & 0 deletions src/core/providers/canvas/use-selection.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ export const useSelection = (
);
};

const updateTextOnSelected = (text: string) => {
setShapes(prevShapes =>
prevShapes.map(shape =>
shape.id === selectedShapeId ? { ...shape, text } : shape
)
);
};

return {
transformerRef,
shapeRefs,
Expand All @@ -60,5 +68,6 @@ export const useSelection = (
selectedShapeId,
selectedShapeType,
setZIndexOnSelected,
updateTextOnSelected,
};
};
23 changes: 23 additions & 0 deletions src/pods/canvas/canvas.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,27 @@ const getDefaultSizeFromShape = (shapeType: ShapeType): Size => {
}
};

const doesShapeAllowInlineEdition = (shapeType: ShapeType): boolean => {
switch (shapeType) {
case 'input':
case 'label':
return true;
default:
return false;
}
};

const generateDefaultTextValue = (shapeType: ShapeType): string | undefined => {
switch (shapeType) {
case 'input':
return '';
case 'label':
return 'Label';
default:
return undefined;
}
};

// TODO: create interfaces to hold Coordination and Size
// coordinate: { x: number, y: number }
// size: { width: number, height: number }
Expand All @@ -101,5 +122,7 @@ export const createShape = (coord: Coord, shapeType: ShapeType): ShapeModel => {
width,
height,
type: shapeType,
allowsInlineEdition: doesShapeAllowInlineEdition(shapeType),
text: generateDefaultTextValue(shapeType),
};
};
28 changes: 21 additions & 7 deletions src/pods/canvas/canvas.pod.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { renderShapeComponent } from './shape-renderer';
import { useDropShape } from './use-drop-shape.hook';
import { useMonitorShape } from './use-monitor-shape.hook';
import classes from './canvas.pod.module.css';
import { ShapeModel } from '@/core/model';
import { EditableComponent } from '@/common/components/inline-edit';

export const CanvasPod = () => {
const { shapes, setShapes, scale, selectionInfo } = useCanvasContext();
Expand All @@ -20,6 +20,7 @@ export const CanvasPod = () => {
selectedShapeRef,
selectedShapeId,
selectedShapeType,
updateTextOnSelected,
} = selectionInfo;

const { isDraggedOver, dropRef } = useDropShape();
Expand Down Expand Up @@ -68,12 +69,25 @@ export const CanvasPod = () => {
shapeRefs.current[shape.id] = createRef();
}

return renderShapeComponent(shape, {
handleSelected,
shapeRefs,
handleDragEnd,
handleTransform,
});
return (
<EditableComponent
key={shape.id}
coords={{ x: shape.x, y: shape.y }}
size={{ width: shape.width, height: shape.height }}
isEditable={shape.allowsInlineEdition}
text={shape.text ?? ''}
onTextSubmit={updateTextOnSelected}
scale={scale}
editType="input"
>
{renderShapeComponent(shape, {
handleSelected,
shapeRefs,
handleDragEnd,
handleTransform,
})}
</EditableComponent>
);
})
}
<Transformer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export const renderInput = (
onDragEnd={handleDragEnd(shape.id)}
onTransform={handleTransform}
onTransformEnd={handleTransform}
isEditable={shape.allowsInlineEdition}
text={shape.text}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export const renderLabel = (
onDragEnd={handleDragEnd(shape.id)}
onTransform={handleTransform}
onTransformEnd={handleTransform}
isEditable={shape.allowsInlineEdition}
text={shape.text}
/>
);
};

0 comments on commit cf64a85

Please sign in to comment.