Skip to content

Commit

Permalink
Refactor: change EditableText behavior, make more flexible and less bugs
Browse files Browse the repository at this point in the history
But this makes it more complex to use, maybe I can come up with another solution one day.
But currently, I think this is enough.
  • Loading branch information
Bill2015 committed Feb 22, 2024
1 parent a6c038b commit d71a409
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 37 deletions.
80 changes: 57 additions & 23 deletions src/components/display/EditableText.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,37 @@
import { useCallback, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Badge, Box, BoxProps, Text } from '@mantine/core';
import { Badge, Box, BoxProps, ElementProps, Text } from '@mantine/core';

import classes from './EditableText.module.scss';

export interface ContentEditableProps extends BoxProps, Omit<ElementProps<'div', keyof BoxProps>, 'onChange'> {
value: string;

onChange: (value: string) => void;
}

export function ContentEditable(props: ContentEditableProps) {
const { value, onChange, ...boxProps } = props;
const ref = useRef<HTMLDivElement>(null);

useEffect(() => {
if (ref && ref.current) {
ref.current.textContent = value;
}
}, [value]);

return (
<Box
// eslint-disable-next-line react/jsx-props-no-spreading
{...boxProps}
ref={ref}
onInput={(e) => onChange(e.currentTarget.textContent ?? '')}
contentEditable
suppressContentEditableWarning
/>
);
}

export interface EditableTextProps extends BoxProps {
value: string;

Expand All @@ -13,6 +41,10 @@ export interface EditableTextProps extends BoxProps {
/** when text change vent
* @param newValue new value of text */
onChange: (newValue: string) => void;

onEdit?: () => void;

onEditFinished?: (newValue: string, isEdited: boolean) => void;
}

/**
Expand All @@ -24,36 +56,44 @@ export interface EditableTextProps extends BoxProps {
* Because input can't warp the text when too long \
* And textarea can input newline, which is unnecessary in some fields */
export function EditableText(props: EditableTextProps) {
const { value, name, onChange, ...boxProps } = props;
const { value, name, onChange, onEdit, onEditFinished, ...boxProps } = props;
const { t } = useTranslation('common', { keyPrefix: 'Display.EditableText' });
const [inEdited, setInEdited] = useState<boolean>(false);
const [newValue, setNewValue] = useState<string>(value);
const edited = useRef<boolean>(false);

const handleClick = () => {
setInEdited(true);
setNewValue(value);
if (onEdit) {
onEdit();
}
};

const handleBlur = useCallback((newVal: string) => {
setNewValue(newVal);
setInEdited(false);
if (newVal !== value) {
onChange(newVal);
if (onEditFinished) {
onEditFinished(newVal, edited.current);
}
}, [onChange, value]);
edited.current = false;
}, [onEditFinished]);

if (inEdited) {
return (
<Box pos="relative">
<Box
contentEditable
<ContentEditable
value={value}
className={classes.input}
suppressContentEditableWarning
onMouseEnter={(e) => e.currentTarget.focus()}
onChange={(e) => {
onChange(e);
edited.current = true;
}}
onFocus={(e) => {
const textNode = e.currentTarget.firstChild!;
// prevent blur not being triggered
setTimeout(() => {
if (!textNode || !textNode.textContent) {
return;
}
const range = document.createRange();
range.setStart(textNode, 0);
range.setEnd(textNode, textNode.textContent!.length);
Expand All @@ -64,8 +104,8 @@ export function EditableText(props: EditableTextProps) {
}, 1);
}}
onBlur={(e) => {
const child = e.currentTarget.lastChild;
handleBlur(child ? child.textContent! : '');
const text = e.currentTarget.textContent;
handleBlur(text ?? '');
}}
onKeyDown={(e) => {
if (e.key === 'Escape' || e.key === 'Enter') {
Expand All @@ -74,15 +114,9 @@ export function EditableText(props: EditableTextProps) {
handleBlur(child ? child.textContent! : '');
}
}}
onPaste={(e) => {
e.preventDefault();
setNewValue(e.clipboardData.getData('text/plain'));
}}
// eslint-disable-next-line react/jsx-props-no-spreading
{...boxProps}
>
{newValue}
</Box>
/>
<Badge color="indigo" pos="absolute" right={0} variant="outline" style={{ zIndex: 99 }}>
{t('modifying')}
</Badge>
Expand All @@ -91,7 +125,7 @@ export function EditableText(props: EditableTextProps) {
}

// value is empty
if ((!value && !newValue) || ((value && !newValue))) {
if (!value) {
return (
// eslint-disable-next-line react/jsx-props-no-spreading
<Text className={classes.text} title={t('double_click_to_edit')} onDoubleClick={handleClick} {...boxProps}>
Expand All @@ -104,7 +138,7 @@ export function EditableText(props: EditableTextProps) {
return (
// eslint-disable-next-line react/jsx-props-no-spreading
<Text className={classes.text} title="double click to edit" onDoubleClick={handleClick} {...boxProps}>
{(value === newValue) ? value : newValue}
{value}
</Text>
);
}
17 changes: 11 additions & 6 deletions src/pages/resource-add/components/AttributePanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PropsWithChildren, useCallback, useMemo, useRef } from 'react';
import { PropsWithChildren, useCallback, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Flex, Group, Space, Stack, Text, UnstyledButton } from '@mantine/core';
import { RxCross2 } from 'react-icons/rx';
Expand Down Expand Up @@ -28,6 +28,8 @@ export function AttributePanel(props: AttributePanelProps) {
const { activeResource, updateResource, updateResourceTag, updateResourceIgnoreText } = useAddResourceContext();
const { getResourceSpecificTags } = useTextTagMapperContext();
const tagComboRef = useRef<TagComboSelectRef>(null);
const [name, setName] = useState<string>('');
const [description, setDescription] = useState<string>('');

const handleUpdate = useCallback((fieldName: keyof ResourceCreateDto, newValue: string) => {
updateResource(activeResource!.index, {
Expand Down Expand Up @@ -65,16 +67,19 @@ export function AttributePanel(props: AttributePanelProps) {
<Stack gap={0}>
<SubTitle>{t('name')}</SubTitle>
<EditableText
key={activeResource.data.name}
value={activeResource.data.name}
value={name || activeResource.data.name}
name={t('name')}
onChange={(val) => handleUpdate('name', val)}
onEdit={() => setName(activeResource.data.name)}
onChange={setName}
onEditFinished={(val) => handleUpdate('name', val)}
/>
<SubTitle>{t('description')}</SubTitle>
<EditableText
value={activeResource.data.description}
value={description || activeResource.data.description}
name={t('description')}
onChange={(val) => handleUpdate('description', val)}
onEdit={() => setDescription(activeResource.data.description)}
onChange={setDescription}
onEditFinished={(val) => handleUpdate('description', val)}
/>
<SubTitle>{t('auto_generate_tags')}</SubTitle>
<Flex gap={10} wrap="wrap">
Expand Down
34 changes: 26 additions & 8 deletions src/pages/resource-detail/ResourceDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export default function ResourcesDetailPage() {

// when new subject group was created, use for auto focus
const [newSubjectId, setNewSubjectId] = useState<string>('');
const [name, setName] = useState<string>('');
const [description, setDescription] = useState<string>('');

const updateResource = ResourceMutation.useUpdate();
const addResourceTag = ResourceMutation.useAddTag();
Expand All @@ -41,10 +43,14 @@ export default function ResourcesDetailPage() {

const handleResourceUpdate = useCallback(async (fieldName: keyof ResourceUpdateDto, newVal: string) => {
if (resourceId) {
updateResource.mutateAsync({ id: resourceId, [fieldName]: newVal })
.catch((e) => showNotification('Update Resource Failed', e.message, 'error'))
.then(() => resourceRefetch())
.then(() => showNotification('Update Resource Successful', '', 'success'));
await updateResource.mutateAsync({ id: resourceId, [fieldName]: newVal })
.then(() => {
showNotification('Update Resource Successful', '', 'success');
})
.catch((e) => {
showNotification('Update Resource Failed', e.message, 'error');
})
.finally(() => resourceRefetch());
}
}, [resourceId, updateResource, resourceRefetch]);

Expand Down Expand Up @@ -87,16 +93,28 @@ export default function ResourcesDetailPage() {
name="name"
fz="1.5rem"
fw="bold"
value={resourceData.name}
onChange={(val) => handleResourceUpdate('name', val)}
value={name || resourceData.name}
onEdit={() => setName(resourceData.name)}
onChange={setName}
onEditFinished={(newVal, isEdited) => {
if (isEdited) {
handleResourceUpdate('name', newVal);
}
}}
/>
<EditableText
name="description"
fz="1rem"
opacity="0.5"
fw="initial"
value={resourceData.description}
onChange={(val) => handleResourceUpdate('description', val)}
value={description || resourceData.description}
onEdit={() => setDescription(resourceData.description)}
onEditFinished={(newVal, isEdited) => {
if (isEdited) {
handleResourceUpdate('description', newVal);
}
}}
onChange={setDescription}
/>
<ResourceTagStack>
{resourceTagData.map(({ subjectId, subjectName, tags }) => (
Expand Down

0 comments on commit d71a409

Please sign in to comment.