feat(ui): editable heading and text elements

This commit is contained in:
psychedelicious
2025-01-31 11:04:24 +11:00
parent 3baaefb0cc
commit 3ef3b97c58
3 changed files with 153 additions and 23 deletions

View File

@@ -0,0 +1,16 @@
import type { TextareaProps } from '@invoke-ai/ui-library';
import { chakra, forwardRef, typedMemo, useStyleConfig } from '@invoke-ai/ui-library';
import type { ComponentProps } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
const ChakraTextareaAutosize = chakra(TextareaAutosize);
export const AutosizeTextarea = typedMemo(
forwardRef<ComponentProps<typeof ChakraTextareaAutosize> & TextareaProps, typeof ChakraTextareaAutosize>(
({ variant, ...rest }, ref) => {
const styles = useStyleConfig('Textarea', { variant });
return <ChakraTextareaAutosize __css={styles} ref={ref} {...rest} />;
}
)
);
AutosizeTextarea.displayName = 'AutosizeTextarea';

View File

@@ -1,17 +1,19 @@
import { Flex, Heading } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { Flex, Text } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { AutosizeTextarea } from 'features/nodes/components/sidePanel/builder/AutosizeTextarea';
import { FormElementEditModeWrapper } from 'features/nodes/components/sidePanel/builder/FormElementEditModeWrapper';
import { selectWorkflowFormMode, useElement } from 'features/nodes/store/workflowSlice';
import { formElementHeadingDataChanged, selectWorkflowFormMode, useElement } from 'features/nodes/store/workflowSlice';
import type { HeadingElement } from 'features/nodes/types/workflow';
import { HEADING_CLASS_NAME, isHeadingElement } from 'features/nodes/types/workflow';
import { memo } from 'react';
import type { ChangeEvent, KeyboardEvent } from 'react';
import { memo, useCallback, useRef, useState } from 'react';
const LEVEL_TO_SIZE = {
1: 'xl',
2: 'lg',
3: 'md',
4: 'sm',
5: 'xs',
const LEVEL_TO_FONT_SIZE = {
1: '4xl',
2: '3xl',
3: '2xl',
4: 'xl',
5: 'lg',
} as const;
export const HeadingElementComponent = memo(({ id }: { id: string }) => {
@@ -38,7 +40,9 @@ export const HeadingElementComponentViewMode = memo(({ el }: { el: HeadingElemen
return (
<Flex id={id} className={HEADING_CLASS_NAME}>
<Heading size={LEVEL_TO_SIZE[level]}>{content}</Heading>
<Text fontWeight="bold" fontSize={LEVEL_TO_FONT_SIZE[level]}>
{content || 'Edit to add heading'}
</Text>
</Flex>
);
});
@@ -46,16 +50,70 @@ export const HeadingElementComponentViewMode = memo(({ el }: { el: HeadingElemen
HeadingElementComponentViewMode.displayName = 'HeadingElementComponentViewMode';
export const HeadingElementComponentEditMode = memo(({ el }: { el: HeadingElement }) => {
const { id, data } = el;
const { content, level } = data;
const { id } = el;
return (
<FormElementEditModeWrapper element={el}>
<Flex id={id} className={HEADING_CLASS_NAME}>
<Heading size={LEVEL_TO_SIZE[level]}>{content}</Heading>
<Flex id={id} className={HEADING_CLASS_NAME} w="full">
<EditableHeading el={el} />
</Flex>
</FormElementEditModeWrapper>
);
});
HeadingElementComponentEditMode.displayName = 'HeadingElementComponentEditMode';
export const EditableHeading = memo(({ el }: { el: HeadingElement }) => {
const dispatch = useAppDispatch();
const { id, data } = el;
const { content, level } = data;
const [localContent, setLocalContent] = useState(content);
const ref = useRef<HTMLTextAreaElement>(null);
const onChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
setLocalContent(e.target.value);
}, []);
const onBlur = useCallback(() => {
const trimmedContent = localContent.trim();
if (trimmedContent === content) {
return;
}
setLocalContent(trimmedContent);
dispatch(formElementHeadingDataChanged({ id, changes: { content: trimmedContent } }));
}, [localContent, content, id, dispatch]);
const onKeyDown = useCallback(
(e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter') {
onBlur();
} else if (e.key === 'Escape') {
setLocalContent(content);
}
},
[content, onBlur]
);
return (
<AutosizeTextarea
ref={ref}
placeholder="Heading"
value={localContent}
onChange={onChange}
onBlur={onBlur}
onKeyDown={onKeyDown}
variant="outline"
overflowWrap="anywhere"
w="full"
minRows={1}
maxRows={10}
resize="none"
p={2}
fontWeight="bold"
fontSize={LEVEL_TO_FONT_SIZE[level]}
/>
);
});
EditableHeading.displayName = 'EditableHeading';

View File

@@ -1,10 +1,12 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { AutosizeTextarea } from 'features/nodes/components/sidePanel/builder/AutosizeTextarea';
import { FormElementEditModeWrapper } from 'features/nodes/components/sidePanel/builder/FormElementEditModeWrapper';
import { selectWorkflowFormMode, useElement } from 'features/nodes/store/workflowSlice';
import { formElementTextDataChanged, selectWorkflowFormMode, useElement } from 'features/nodes/store/workflowSlice';
import type { TextElement } from 'features/nodes/types/workflow';
import { isTextElement, TEXT_CLASS_NAME } from 'features/nodes/types/workflow';
import { memo } from 'react';
import type { ChangeEvent, KeyboardEvent } from 'react';
import { memo, useCallback, useRef, useState } from 'react';
export const TextElementComponent = memo(({ id }: { id: string }) => {
const el = useElement(id);
@@ -29,22 +31,76 @@ export const TextElementComponentViewMode = memo(({ el }: { el: TextElement }) =
return (
<Flex id={id} className={TEXT_CLASS_NAME}>
<Text fontSize={fontSize}>{content}</Text>
<Text fontSize={fontSize} overflowWrap="anywhere">
{content || 'Edit to add text'}
</Text>
</Flex>
);
});
TextElementComponentViewMode.displayName = 'TextElementComponentViewMode';
export const TextElementComponentEditMode = memo(({ el }: { el: TextElement }) => {
const { id, data } = el;
const { content, fontSize } = data;
const { id } = el;
return (
<FormElementEditModeWrapper element={el}>
<Flex id={id} className={TEXT_CLASS_NAME}>
<Text fontSize={fontSize}>{content}</Text>
<Flex id={id} className={TEXT_CLASS_NAME} w="full">
<EditableText el={el} />
</Flex>
</FormElementEditModeWrapper>
);
});
TextElementComponentEditMode.displayName = 'TextElementComponentEditMode';
export const EditableText = memo(({ el }: { el: TextElement }) => {
const dispatch = useAppDispatch();
const { id, data } = el;
const { content, fontSize } = data;
const [localContent, setLocalContent] = useState(content);
const ref = useRef<HTMLTextAreaElement>(null);
const onChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
setLocalContent(e.target.value);
}, []);
const onBlur = useCallback(() => {
const trimmedContent = localContent.trim();
if (trimmedContent === content) {
return;
}
setLocalContent(trimmedContent);
dispatch(formElementTextDataChanged({ id, changes: { content: trimmedContent } }));
}, [localContent, content, id, dispatch]);
const onKeyDown = useCallback(
(e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter') {
onBlur();
} else if (e.key === 'Escape') {
setLocalContent(content);
}
},
[content, onBlur]
);
return (
<AutosizeTextarea
ref={ref}
placeholder="Text"
value={localContent}
onChange={onChange}
onBlur={onBlur}
onKeyDown={onKeyDown}
fontSize={fontSize}
variant="outline"
overflowWrap="anywhere"
w="full"
minRows={1}
maxRows={10}
resize="none"
p={2}
/>
);
});
EditableText.displayName = 'EditableText';