Skip to content

Commit

Permalink
Merge pull request #144 from Lemoncode/feature/#91-componentize-inlin…
Browse files Browse the repository at this point in the history
…e-edit-to-avoid-complexity-on-HTML

#91 inline-edit HTML is componentized
  • Loading branch information
brauliodiez authored Aug 10, 2024
2 parents 79bf314 + 5066a7f commit 5d9de49
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 99 deletions.
45 changes: 45 additions & 0 deletions src/common/components/inline-edit/components/html-edit.widget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Html } from 'react-konva-utils';
import { forwardRef } from 'react';
import { EditType, StyleDivProps } from '../inline-edit.model';

interface Props {
divProps: StyleDivProps;
value: string;
editType: EditType;
onSetEditText: (e: string) => void;
}

export const HtmlEditWidget = forwardRef<any, Props>(
({ divProps, onSetEditText, value, editType }, ref) => {
return (
<Html
divProps={{
style: {
position: divProps.position,
top: divProps.top,
left: divProps.left,
width: divProps.width,
height: divProps.height,
},
}}
>
{editType === 'input' && (
<input
ref={ref}
style={{ width: '100%', height: '100%' }}
value={value}
onChange={e => onSetEditText(e.target.value)}
/>
)}
{editType === 'textarea' && (
<textarea
ref={ref}
style={{ width: '100%', height: '100%' }}
value={value}
onChange={e => onSetEditText(e.target.value)}
/>
)}
</Html>
);
}
);
1 change: 1 addition & 0 deletions src/common/components/inline-edit/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './html-edit.widget';
2 changes: 2 additions & 0 deletions src/common/components/inline-edit/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './use-submit-cancel-hook';
export * from './use-position-hook';
31 changes: 31 additions & 0 deletions src/common/components/inline-edit/hooks/use-position-hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useCallback } from 'react';
import { addPxSuffix, calculateCoordinateValue } from '../inline-edit.utils';
import { Coord, Size } from '@/core/model';

export const usePositionHook = (coords: Coord, size: Size, scale: number) => {
// TODO: this can be optimized using React.useCallback, issue #90
// https://github.com/Lemoncode/quickmock/issues/90
const calculateTextAreaXPosition = useCallback(
() => calculateCoordinateValue(coords.x, scale),
[coords.x, scale]
);
const calculateTextAreaYPosition = useCallback(
() => calculateCoordinateValue(coords.y, scale),
[coords.y, scale]
);
const calculateWidth = useCallback(
() => addPxSuffix(size.width),
[size.width]
);
const calculateHeight = useCallback(
() => addPxSuffix(size.height),
[size.height]
);

return {
calculateTextAreaXPosition,
calculateTextAreaYPosition,
calculateWidth,
calculateHeight,
};
};
74 changes: 74 additions & 0 deletions src/common/components/inline-edit/hooks/use-submit-cancel-hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useEffect, useRef, useState } from 'react';
import { EditType } from '../inline-edit.model';

interface Configuration {
editType: EditType | undefined;
isEditable: boolean;
text: string;
onTextSubmit: (text: string) => void;
}

export const useSubmitCancelHook = (
configuration: Configuration,
setEditText: React.Dispatch<React.SetStateAction<string>>
) => {
const { editType, isEditable, text, onTextSubmit } = configuration;
const inputRef = useRef<HTMLInputElement>(null);
const textAreaRef = useRef<HTMLTextAreaElement>(null);
const [isEditing, setIsEditing] = useState(false);

const getActiveInputRef = ():
| HTMLInputElement
| HTMLTextAreaElement
| null => (editType === 'input' ? inputRef.current : textAreaRef.current);

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

const handleClickOutside = (event: MouseEvent) => {
if (
getActiveInputRef() &&
!getActiveInputRef()?.contains(event.target as Node)
) {
setIsEditing(false);
onTextSubmit(getActiveInputRef()?.value || '');
}
};

const handleKeyDown = (event: KeyboardEvent) => {
if (isEditing && event.key === 'Escape') {
setIsEditing(false);
setEditText(text);
}

if (editType === 'input' && isEditable && event.key === 'Enter') {
setIsEditing(false);
onTextSubmit(getActiveInputRef()?.value || '');
}
};

if (isEditing) {
getActiveInputRef()?.focus();
getActiveInputRef()?.select();
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleKeyDown);
} else {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('keydown', handleKeyDown);
}

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

return {
isEditing,
setIsEditing,
inputRef,
textAreaRef,
getActiveInputRef,
};
};
11 changes: 11 additions & 0 deletions src/common/components/inline-edit/inline-edit.model.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export type EditType = 'input' | 'textarea';

type PositionType = 'absolute' | 'relative' | 'fixed' | 'static';

export interface StyleDivProps {
position: PositionType;
top: string | number;
left: string | number;
width: string | number;
height: string | number;
}
132 changes: 33 additions & 99 deletions src/common/components/inline-edit/inline-edit.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Coord, EditType, Size } from '@/core/model';
import React, { useEffect, useRef, useState } from 'react';
import React, { useState } from 'react';
import { Group } from 'react-konva';
import { Html } from 'react-konva-utils';
import { addPxSuffix, calculateCoordinateValue } from './inline-edit.utils';
import { Coord, Size } from '@/core/model';
import { HtmlEditWidget } from './components';
import { EditType } from './inline-edit.model';
import { useSubmitCancelHook, usePositionHook } from './hooks';

interface Props {
coords: Coord;
Expand All @@ -26,116 +27,49 @@ export const EditableComponent: React.FC<Props> = props => {
children,
editType,
} = props;
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(text);

const inputRef = useRef<HTMLInputElement>(null);
const textAreaRef = useRef<HTMLTextAreaElement>(null);

const getActiveInputRef = ():
| HTMLInputElement
| HTMLTextAreaElement
| null => (editType === 'input' ? inputRef.current : textAreaRef.current);

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

const handleClickOutside = (event: MouseEvent) => {
if (
getActiveInputRef() &&
!getActiveInputRef()?.contains(event.target as Node)
) {
setIsEditing(false);
onTextSubmit(getActiveInputRef()?.value || '');
}
};

const handleKeyDown = (event: KeyboardEvent) => {
if (isEditing && event.key === 'Escape') {
setIsEditing(false);
setEditText(text);
}

if (editType === 'input' && isEditable && event.key === 'Enter') {
setIsEditing(false);
onTextSubmit(getActiveInputRef()?.value || '');
}
};

if (isEditing) {
getActiveInputRef()?.focus();
getActiveInputRef()?.select();
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleKeyDown);
} else {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('keydown', handleKeyDown);
}

return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('keydown', handleKeyDown);
};
}, [isEditing]);
const { inputRef, textAreaRef, isEditing, setIsEditing } =
useSubmitCancelHook(
{
editType,
isEditable,
text,
onTextSubmit,
},
setEditText
);

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 = React.useCallback(
() => calculateCoordinateValue(coords.x, scale),
[coords.x, scale]
);
const calculateTextAreaYPosition = React.useCallback(
() => calculateCoordinateValue(coords.y, scale),
[coords.y, scale]
);
const calculateWidth = React.useCallback(
() => addPxSuffix(size.width),
[size.width]
);
const calculateHeight = React.useCallback(
() => addPxSuffix(size.height),
[size.height]
);
// TODO: Componentize this #91
// https://github.com/Lemoncode/quickmock/issues/91
const {
calculateTextAreaXPosition,
calculateTextAreaYPosition,
calculateWidth,
calculateHeight,
} = usePositionHook(coords, size, scale);

return (
<>
<Group onDblClick={handleDoubleClick}>{children}</Group>
{isEditing ? (
<Html
<HtmlEditWidget
divProps={{
style: {
position: 'absolute',
top: calculateTextAreaYPosition(),
left: calculateTextAreaXPosition(),
width: calculateWidth(),
height: calculateHeight(),
},
position: 'absolute',
top: calculateTextAreaYPosition(),
left: calculateTextAreaXPosition(),
width: calculateWidth(),
height: calculateHeight(),
}}
>
{editType === 'input' ? (
<input
ref={inputRef}
style={{ width: '100%', height: '100%' }}
value={editText}
onChange={e => setEditText(e.target.value)}
/>
) : (
<textarea
ref={textAreaRef}
style={{ width: '100%', height: '100%' }}
value={editText}
onChange={e => setEditText(e.target.value)}
></textarea>
)}
</Html>
ref={editType === 'input' ? inputRef : textAreaRef}
value={editText}
onSetEditText={setEditText}
editType={editType ?? 'input'}
/>
) : null}
</>
);
Expand Down

0 comments on commit 5d9de49

Please sign in to comment.