feat: add PowerInput component

This commit is contained in:
Ali BARIN
2022-02-17 20:15:08 +01:00
committed by Ömer Faruk Aydın
parent c864a1062d
commit d06f21c927
30 changed files with 848 additions and 115 deletions

View File

@@ -0,0 +1,247 @@
import * as React from 'react';
import ClickAwayListener from '@mui/base/ClickAwayListener';
import Chip from '@mui/material/Chip';
import Popper from '@mui/material/Popper';
import TextField from '@mui/material/TextField';
import InputLabel from '@mui/material/InputLabel';
import FormHelperText from '@mui/material/FormHelperText';
import { Controller, Control, FieldValues } from 'react-hook-form';
import { Editor, Transforms, Range, createEditor } from 'slate';
import {
Slate,
Editable,
useSelected,
useFocused,
} from 'slate-react';
import {
serialize,
deserialize,
insertVariable,
customizeEditor,
} from './utils';
import Suggestions from './Suggestions';
import { StepExecutionsContext } from 'contexts/StepExecutions';
import { FakeInput, InputLabelWrapper } from './style';
import { VariableElement } from './types';
import { processStepWithExecutions } from './data';
type PowerInputProps = {
control?: Control<FieldValues>;
onChange?: (value: string) => void;
onBlur?: (value: string) => void;
defaultValue?: string;
name: string;
label?: string;
type?: string;
required?: boolean;
readOnly?: boolean;
description?: string;
docUrl?: string;
clickToCopy?: boolean;
}
const PowerInput = (props: PowerInputProps) => {
const {
control,
defaultValue = '',
onBlur,
name,
label,
required,
description,
} = props;
const priorStepsWithExecutions = React.useContext(StepExecutionsContext);
const editorRef = React.useRef<HTMLDivElement | null>(null);
const [target, setTarget] = React.useState<Range | null>(null);
const [index, setIndex] = React.useState(0);
const [search, setSearch] = React.useState<string | null>(null);
const renderElement = React.useCallback(props => <Element {...props} />, []);
const [editor] = React.useState(() => customizeEditor(createEditor()));
const stepsWithVariables = React.useMemo(() => {
return processStepWithExecutions(priorStepsWithExecutions);
}, [priorStepsWithExecutions])
const handleBlur = React.useCallback((value) => {
onBlur?.(value);
}, [onBlur]);
const handleVariableSuggestionClick = React.useCallback(
(variable: Pick<VariableElement, "name" | "value">) => {
if (target) {
Transforms.select(editor, target);
insertVariable(editor, variable);
setTarget(null);
}
},
[index, target]
);
const onKeyDown = React.useCallback(
event => {
if (target) {
switch (event.key) {
case 'ArrowDown': {
event.preventDefault();
setIndex((currentIndex) => currentIndex + 1);
break
}
case 'ArrowUp': {
event.preventDefault();
setIndex((currentIndex) => currentIndex - 1 < 0 ? 0 : currentIndex - 1);
break
}
case 'Tab':
case 'Enter': {
event.preventDefault();
Transforms.select(editor, target);
insertVariable(editor, stepsWithVariables[0].output[index]);
setTarget(null);
break
}
case 'Escape': {
event.preventDefault();
setTarget(null);
break
}
}
}
},
[index, search, target, stepsWithVariables]
);
return (
<Controller
rules={{ required }}
name={name}
control={control}
defaultValue={defaultValue}
shouldUnregister={false}
render={({ field: { value, ref, onChange: controllerOnChange, onBlur: controllerOnBlur, ...field } }) => (
<Slate
editor={editor}
value={deserialize(value)}
onChange={value => {
controllerOnChange(serialize(value));
const { selection } = editor
if (selection && Range.isCollapsed(selection)) {
const [start] = Range.edges(selection);
const lineBefore = Editor.before(editor, start, { unit: 'line' });
const before = lineBefore && Editor.before(editor, lineBefore);
const beforeRange = (before || lineBefore) && Editor.range(editor, before || lineBefore, start);
const beforeText = beforeRange && Editor.string(editor, beforeRange);
const variableMatch = beforeText && beforeText.match(/@([\w.]*?)$/);
if (variableMatch) {
const beginningOfVariable = Editor.before(
editor,
start,
{
unit: 'offset',
distance: (variableMatch[1].length || 0) + 1
}
);
if (beginningOfVariable) {
const newTarget = Editor.range(editor, beginningOfVariable, start);
if (newTarget) {
setTarget(newTarget);
}
}
setIndex(0);
setSearch(variableMatch[1]);
return;
}
}
setSearch(null);
}}
>
<ClickAwayListener onClickAway={() => setSearch(null)}>
{/* ref-able single child for ClickAwayListener */}
<div style={{ width: '100%' }}>
<FakeInput>
<InputLabelWrapper>
<InputLabel
shrink={true}
// focused
variant="outlined"
sx={{ bgcolor: 'white', display: 'inline-block', px: .75 }}
>
{label}
</InputLabel>
</InputLabelWrapper>
<Editable
style={{ width: '100%' }}
renderElement={renderElement}
onKeyDown={onKeyDown}
onBlur={() => { controllerOnBlur(); handleBlur(value); }}
/>
</FakeInput>
{/* ghost placer for the variables popover */}
<div ref={editorRef} style={{ width: '100%' }} />
<FormHelperText
variant="outlined"
>
{description}
</FormHelperText>
<Popper
open={target !== null && search !== null}
anchorEl={editorRef.current}
style={{ width: editorRef.current?.clientWidth }}
>
<Suggestions
query={search}
index={index}
data={stepsWithVariables}
onSuggestionClick={handleVariableSuggestionClick}
/>
</Popper>
</div>
</ClickAwayListener>
</Slate>
)}
/>
)
}
const Element = (props: any) => {
const { attributes, children, element } = props;
switch (element.type) {
case 'variable':
return <Variable {...props} />;
default:
return <p {...attributes}>{children}</p>;
}
}
const Variable = ({ attributes, children, element }: any) => {
const selected = useSelected();
const focused = useFocused();
const label = (
<>
{element.name}
{children}
</>
);
return (
<Chip
{...attributes}
component="span"
contentEditable={false}
style={{
boxShadow: selected && focused ? '0 0 0 2px #B4D5FF' : 'none',
}}
size="small"
label={label}
/>
)
}
export default PowerInput;