feat(PowerInput): show variable suggestions when focused
This commit is contained in:
@@ -15,25 +15,19 @@ import type { IStep } from '@automatisch/types';
|
|||||||
const ListItemText = styled(MuiListItemText)``;
|
const ListItemText = styled(MuiListItemText)``;
|
||||||
|
|
||||||
type SuggestionsProps = {
|
type SuggestionsProps = {
|
||||||
query?: string | null;
|
data: any[];
|
||||||
index: number;
|
|
||||||
data: any;
|
|
||||||
onSuggestionClick: (variable: any) => void;
|
onSuggestionClick: (variable: any) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SHORT_LIST_LENGTH = 4;
|
const SHORT_LIST_LENGTH = 4;
|
||||||
const LIST_HEIGHT = 256;
|
const LIST_HEIGHT = 256;
|
||||||
|
|
||||||
const getPartialFilteredArray = (array: any[], query = '', length = array.length) => {
|
const getPartialArray = (array: any[], length = array.length) => {
|
||||||
return array
|
return array.slice(0, length);
|
||||||
.filter((suboption: any) => suboption.name.includes(query))
|
|
||||||
.slice(0, length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Suggestions = (props: SuggestionsProps) => {
|
const Suggestions = (props: SuggestionsProps) => {
|
||||||
const {
|
const {
|
||||||
query = '',
|
|
||||||
index: focusIndex,
|
|
||||||
data,
|
data,
|
||||||
onSuggestionClick = () => null,
|
onSuggestionClick = () => null,
|
||||||
} = props;
|
} = props;
|
||||||
@@ -79,13 +73,13 @@ const Suggestions = (props: SuggestionsProps) => {
|
|||||||
|
|
||||||
<Collapse in={current === index} timeout="auto" unmountOnExit>
|
<Collapse in={current === index} timeout="auto" unmountOnExit>
|
||||||
<List component="div" disablePadding sx={{ maxHeight: LIST_HEIGHT, overflowY: 'auto' }}>
|
<List component="div" disablePadding sx={{ maxHeight: LIST_HEIGHT, overflowY: 'auto' }}>
|
||||||
{getPartialFilteredArray(option.output as any || [], query as string, listLength)
|
{getPartialArray(option.output as any || [], listLength)
|
||||||
.map((suboption: any, index: number) => (
|
.map((suboption: any, index: number) => (
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
sx={{ pl: 4 }}
|
sx={{ pl: 4 }}
|
||||||
divider
|
divider
|
||||||
onClick={() => onSuggestionClick(suboption)}
|
onClick={() => onSuggestionClick(suboption)}
|
||||||
selected={focusIndex === index}>
|
>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={suboption.name}
|
primary={suboption.name}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
|
@@ -56,9 +56,9 @@ const PowerInput = (props: PowerInputProps) => {
|
|||||||
const editorRef = React.useRef<HTMLDivElement | null>(null);
|
const editorRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
const [target, setTarget] = React.useState<Range | null>(null);
|
const [target, setTarget] = React.useState<Range | null>(null);
|
||||||
const [index, setIndex] = React.useState(0);
|
const [index, setIndex] = React.useState(0);
|
||||||
const [search, setSearch] = React.useState<string | null>(null);
|
|
||||||
const renderElement = React.useCallback(props => <Element {...props} />, []);
|
const renderElement = React.useCallback(props => <Element {...props} />, []);
|
||||||
const [editor] = React.useState(() => customizeEditor(createEditor()));
|
const [editor] = React.useState(() => customizeEditor(createEditor()));
|
||||||
|
const [showVariableSuggestions, setShowVariableSuggestions] = React.useState(false);
|
||||||
|
|
||||||
const stepsWithVariables = React.useMemo(() => {
|
const stepsWithVariables = React.useMemo(() => {
|
||||||
return processStepWithExecutions(priorStepsWithExecutions);
|
return processStepWithExecutions(priorStepsWithExecutions);
|
||||||
@@ -70,46 +70,9 @@ const PowerInput = (props: PowerInputProps) => {
|
|||||||
|
|
||||||
const handleVariableSuggestionClick = React.useCallback(
|
const handleVariableSuggestionClick = React.useCallback(
|
||||||
(variable: Pick<VariableElement, "name" | "value">) => {
|
(variable: Pick<VariableElement, "name" | "value">) => {
|
||||||
if (target) {
|
|
||||||
Transforms.select(editor, target);
|
|
||||||
insertVariable(editor, variable, stepsWithVariables);
|
insertVariable(editor, variable, stepsWithVariables);
|
||||||
setTarget(null);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[index, target, stepsWithVariables]
|
[index, stepsWithVariables]
|
||||||
);
|
|
||||||
|
|
||||||
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], stepsWithVariables);
|
|
||||||
setTarget(null);
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'Escape': {
|
|
||||||
event.preventDefault();
|
|
||||||
setTarget(null);
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[index, search, target, stepsWithVariables]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -125,42 +88,12 @@ const PowerInput = (props: PowerInputProps) => {
|
|||||||
value={deserialize(value, stepsWithVariables)}
|
value={deserialize(value, stepsWithVariables)}
|
||||||
onChange={value => {
|
onChange={value => {
|
||||||
controllerOnChange(serialize(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)}>
|
<ClickAwayListener
|
||||||
|
mouseEvent="onMouseDown"
|
||||||
|
onClickAway={() => { setShowVariableSuggestions(false); }}
|
||||||
|
>
|
||||||
{/* ref-able single child for ClickAwayListener */}
|
{/* ref-able single child for ClickAwayListener */}
|
||||||
<div style={{ width: '100%' }}>
|
<div style={{ width: '100%' }}>
|
||||||
<FakeInput disabled={disabled}>
|
<FakeInput disabled={disabled}>
|
||||||
@@ -179,7 +112,9 @@ const PowerInput = (props: PowerInputProps) => {
|
|||||||
readOnly={disabled}
|
readOnly={disabled}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
renderElement={renderElement}
|
renderElement={renderElement}
|
||||||
onKeyDown={onKeyDown}
|
onFocus={() => {
|
||||||
|
setShowVariableSuggestions(true);
|
||||||
|
}}
|
||||||
onBlur={() => { controllerOnBlur(); handleBlur(value); }}
|
onBlur={() => { controllerOnBlur(); handleBlur(value); }}
|
||||||
/>
|
/>
|
||||||
</FakeInput>
|
</FakeInput>
|
||||||
@@ -192,18 +127,12 @@ const PowerInput = (props: PowerInputProps) => {
|
|||||||
{description}
|
{description}
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
|
|
||||||
<Popper
|
<SuggestionsPopper
|
||||||
open={target !== null && search !== null}
|
open={showVariableSuggestions}
|
||||||
anchorEl={editorRef.current}
|
anchorEl={editorRef.current}
|
||||||
style={{ width: editorRef.current?.clientWidth, zIndex: 1, }}
|
data={stepsWithVariables}
|
||||||
>
|
onSuggestionClick={handleVariableSuggestionClick}
|
||||||
<Suggestions
|
/>
|
||||||
query={search}
|
|
||||||
index={index}
|
|
||||||
data={stepsWithVariables}
|
|
||||||
onSuggestionClick={handleVariableSuggestionClick}
|
|
||||||
/>
|
|
||||||
</Popper>
|
|
||||||
</div>
|
</div>
|
||||||
</ClickAwayListener>
|
</ClickAwayListener>
|
||||||
</Slate>
|
</Slate>
|
||||||
@@ -212,6 +141,37 @@ const PowerInput = (props: PowerInputProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SuggestionsPopper = (props: any) => {
|
||||||
|
const {
|
||||||
|
open,
|
||||||
|
anchorEl,
|
||||||
|
data,
|
||||||
|
onSuggestionClick,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popper
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
style={{ width: anchorEl?.clientWidth, zIndex: 1, }}
|
||||||
|
modifiers={[
|
||||||
|
{
|
||||||
|
name: 'flip',
|
||||||
|
enabled: false,
|
||||||
|
options: {
|
||||||
|
altBoundary: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Suggestions
|
||||||
|
data={data}
|
||||||
|
onSuggestionClick={onSuggestionClick}
|
||||||
|
/>
|
||||||
|
</Popper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const Element = (props: any) => {
|
const Element = (props: any) => {
|
||||||
const { attributes, children, element } = props;
|
const { attributes, children, element } = props;
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
|
Reference in New Issue
Block a user