mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(ui): editable heading and text elements
This commit is contained in:
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user