diff --git a/newIDE/app/src/CommandPalette/CommandsContext.js b/newIDE/app/src/CommandPalette/CommandsContext.js index c85d759f1e..19e552cec2 100644 --- a/newIDE/app/src/CommandPalette/CommandsContext.js +++ b/newIDE/app/src/CommandPalette/CommandsContext.js @@ -1,7 +1,7 @@ // @flow import * as React from 'react'; import CommandManager, { CommandManagerInterface } from './CommandManager'; -import useRefInit from './UseRefInitHook'; +import useValueWithInit from '../Utils/UseRefInitHook'; const CommandsContext = React.createContext( new CommandManager() @@ -12,7 +12,9 @@ type Props = { }; export const CommandsContextProvider = (props: Props) => { - const commandManager = useRefInit(() => new CommandManager()); + const commandManager = useValueWithInit( + () => new CommandManager() + ); return ( diff --git a/newIDE/app/src/CommandPalette/CommandsScopedContext.js b/newIDE/app/src/CommandPalette/CommandsScopedContext.js index b03c47c69a..669248b2c9 100644 --- a/newIDE/app/src/CommandPalette/CommandsScopedContext.js +++ b/newIDE/app/src/CommandPalette/CommandsScopedContext.js @@ -7,7 +7,7 @@ import { } from './CommandManager'; import { type CommandName } from './CommandsList'; import CommandsContext from './CommandsContext'; -import useRefInit from './UseRefInitHook'; +import useValueWithInit from '../Utils/UseRefInitHook'; class ScopedCommandManager implements CommandManagerInterface { _commands: { [CommandName]: Command }; @@ -72,7 +72,7 @@ type Props = {| const CommandsContextScopedProvider = (props: Props) => { const centralManager = React.useContext(CommandsContext); - const scopedManager = useRefInit( + const scopedManager = useValueWithInit( () => new ScopedCommandManager(centralManager) ); diff --git a/newIDE/app/src/CommandPalette/UseRefInitHook.js b/newIDE/app/src/CommandPalette/UseRefInitHook.js deleted file mode 100644 index eaddd6ce80..0000000000 --- a/newIDE/app/src/CommandPalette/UseRefInitHook.js +++ /dev/null @@ -1,15 +0,0 @@ -// @flow -import { useRef } from 'react'; - -const useRefInit = (init: () => T): T => { - const instanceRef = useRef(null); - - let instance = instanceRef.current; - if (instance !== null) return instance; - // Lazily create the ref object - let newInstance = init(); - instanceRef.current = newInstance; - return newInstance; -}; - -export default useRefInit; diff --git a/newIDE/app/src/InstancesEditor/InstancePropertiesEditor/index.js b/newIDE/app/src/InstancesEditor/InstancePropertiesEditor/index.js index 4ee4d191bb..91f7fefdaa 100644 --- a/newIDE/app/src/InstancesEditor/InstancePropertiesEditor/index.js +++ b/newIDE/app/src/InstancesEditor/InstancePropertiesEditor/index.js @@ -377,7 +377,7 @@ const InstancePropertiesEditor = ({ {object ? ( { )} Add your first object variable diff --git a/newIDE/app/src/UI/ErrorBoundary.js b/newIDE/app/src/UI/ErrorBoundary.js index 8d3ec0acca..2acd9bc8f0 100644 --- a/newIDE/app/src/UI/ErrorBoundary.js +++ b/newIDE/app/src/UI/ErrorBoundary.js @@ -23,6 +23,7 @@ type ErrorBoundaryScope = | 'mainframe' | 'list-search-result' | 'box-search-result' + | 'variables-list' | 'app'; const errorHandler = ( diff --git a/newIDE/app/src/UI/SimpleTextField.css b/newIDE/app/src/UI/SimpleTextField.css new file mode 100644 index 0000000000..7e29475074 --- /dev/null +++ b/newIDE/app/src/UI/SimpleTextField.css @@ -0,0 +1,55 @@ +/** + * CSS classes for a text field, inspired from Material UI, but lightweight + * and faster to render. + */ +.gd-simple-text-field { + position: relative; + width: 100%; +} + +.gd-simple-text-field input { + font-family: var(--gdevelop-modern-font-family); + font-size: 14px; + outline: none; + border: none; + padding: 0; + background-image: none; + background-color: transparent; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + color: inherit; + width: 100%; +} + +.gd-simple-text-field.gd-disabled input { + color: var(--theme-text-disabled-color); +} + +.gd-simple-text-field:before { + bottom: -1px; + left: 0; + right: 0; + content: '\00a0'; + border-bottom: 1px solid var(--theme-text-default-color); + display: block; + position: absolute; + pointer-events: none; +} +.gd-simple-text-field:hover::before { + border-width: 2px; +} +.gd-simple-text-field:focus-within::before { + border-width: 2px; +} + +.gd-simple-text-field.gd-disabled:before { + border-bottom-style: dotted; + border-color: var(--theme-text-disabled-color); +} +.gd-simple-text-field.gd-disabled:hover::before { + border-width: 1px; +} +.gd-simple-text-field.gd-disabled:focus-within::before { + border-width: 1px; +} diff --git a/newIDE/app/src/UI/SimpleTextField.js b/newIDE/app/src/UI/SimpleTextField.js new file mode 100644 index 0000000000..57d40b81fc --- /dev/null +++ b/newIDE/app/src/UI/SimpleTextField.js @@ -0,0 +1,136 @@ +// @flow +import * as React from 'react'; +import { shouldValidate } from './KeyboardShortcuts/InteractionKeys'; +import './SimpleTextField.css'; + +type SimpleTextFieldProps = {| + disabled: boolean, + type: 'number' | 'text', + onChange: (newValue: string, context: any) => void, + value: string, + id: string, + additionalContext?: any, + italic?: boolean, + + /** + * Only to be used in the exceptional case where any change + * must be immediately communicated to the parent. + */ + directlyStoreValueChangesWhileEditing?: boolean, +|}; + +type FocusOptions = {| + selectAll?: boolean, + caretPosition?: number, +|}; + +export type SimpleTextFieldInterface = {| + focus: (options: ?FocusOptions) => void, + forceSetSelection: (selectionStart: number, selectionEnd: number) => void, + getCaretPosition: () => number, +|}; + +const styles = { + italic: { + fontStyle: 'italic', + }, +}; + +const stopPropagation = e => e.stopPropagation(); + +/** + * A text field, inspired from Material UI, but lightweight + * and faster to render (2 DOM elements, uncontrolled, pure CSS styling). + */ +export const SimpleTextField = React.memo< + SimpleTextFieldProps, + SimpleTextFieldInterface +>( + React.forwardRef( + (props, ref) => { + const inputRef = React.useRef(null); + + React.useEffect( + () => { + // If the value passed changed, update the input. Otherwise, + // keep the input uncontrolled. + if (inputRef.current) inputRef.current.value = props.value; + }, + [props.value] + ); + + const focus = React.useCallback((options: ?FocusOptions) => { + const input = inputRef.current; + if (input) { + input.focus(); + + if (options && options.selectAll) { + input.select(); + } + + if (options && Number.isInteger(options.caretPosition)) { + const position = Number(options.caretPosition); + input.setSelectionRange(position, position); + } + } + }, []); + + const forceSetSelection = React.useCallback( + (selectionStart: number, selectionEnd: number) => { + if (inputRef.current) { + inputRef.current.selectionStart = selectionStart; + inputRef.current.selectionEnd = selectionEnd; + } + }, + [] + ); + + const getCaretPosition = React.useCallback(() => { + if (inputRef.current) return inputRef.current.selectionStart; + return 0; + }, []); + + React.useImperativeHandle(ref, () => ({ + focus, + forceSetSelection, + getCaretPosition, + })); + + return ( +
+ { + props.onChange(e.currentTarget.value, props.additionalContext); + }} + onChange={ + props.directlyStoreValueChangesWhileEditing + ? e => { + props.onChange( + e.currentTarget.value, + props.additionalContext + ); + } + : undefined + } + onKeyUp={e => { + if (shouldValidate(e)) { + props.onChange(e.currentTarget.value, props.additionalContext); + } + }} + style={props.italic ? styles.italic : undefined} + /> +
+ ); + } + ) +); diff --git a/newIDE/app/src/UI/Theme/CreateTheme.js b/newIDE/app/src/UI/Theme/CreateTheme.js index 8acf7c30b3..feba8eb413 100644 --- a/newIDE/app/src/UI/Theme/CreateTheme.js +++ b/newIDE/app/src/UI/Theme/CreateTheme.js @@ -13,6 +13,7 @@ export function getRootClassNames(theme: string) { eventsSheetRootClassName: theme, tableRootClassName: theme, markdownRootClassName: theme, + uiRootClassName: theme, }; } diff --git a/newIDE/app/src/Utils/UseRefInitHook.js b/newIDE/app/src/Utils/UseRefInitHook.js new file mode 100644 index 0000000000..e0e1df0953 --- /dev/null +++ b/newIDE/app/src/Utils/UseRefInitHook.js @@ -0,0 +1,29 @@ +// @flow +import { useRef } from 'react'; + +const useValueWithInit = (init: () => T): T => { + const instanceRef = useRef(null); + + let instance = instanceRef.current; + if (instance !== null) return instance; + // Lazily create the ref object. + let newInstance = init(); + instanceRef.current = newInstance; + return newInstance; +}; + +export default useValueWithInit; + +export const useRefWithInit = (init: () => T): {| current: T |} => { + const instanceRef = useRef(null); + + let instance = instanceRef.current; + // $FlowFixMe - we have the guarantee that T can't be null. + if (instance !== null) return instanceRef; + + // Lazily create the ref object. + let newInstance = init(); + instanceRef.current = newInstance; + // $FlowFixMe - we have the guarantee that T can't be null. + return instanceRef; +}; diff --git a/newIDE/app/src/Utils/VariablesUtils.js b/newIDE/app/src/Utils/VariablesUtils.js index 934d28f3c3..7aa70053ea 100644 --- a/newIDE/app/src/Utils/VariablesUtils.js +++ b/newIDE/app/src/Utils/VariablesUtils.js @@ -45,12 +45,18 @@ export const hasChildThatContainsStringInNameOrValue = ( export const insertInVariablesContainer = ( variablesContainer: gdVariablesContainer, name: string, - serializedVariable: ?any, - index: ?number + serializedVariable: any | null, + index: number, + inheritedVariablesContainer: ?gdVariablesContainer ): { name: string, variable: gdVariable } => { const newName = newNameGenerator( name, - name => variablesContainer.has(name), + name => { + return ( + variablesContainer.has(name) || + (!!inheritedVariablesContainer && inheritedVariablesContainer.has(name)) + ); + }, serializedVariable ? 'CopyOf' : undefined ); const newVariable = new gd.Variable(); @@ -60,11 +66,7 @@ export const insertInVariablesContainer = ( } else { newVariable.setString(''); } - const variable = variablesContainer.insert( - newName, - newVariable, - index || variablesContainer.count() - ); + const variable = variablesContainer.insert(newName, newVariable, index); newVariable.delete(); return { name: newName, variable }; }; diff --git a/newIDE/app/src/VariablesList/MultilineVariableEditorDialog.js b/newIDE/app/src/VariablesList/MultilineVariableEditorDialog.js new file mode 100644 index 0000000000..b4247b24f8 --- /dev/null +++ b/newIDE/app/src/VariablesList/MultilineVariableEditorDialog.js @@ -0,0 +1,48 @@ +// @flow +import * as React from 'react'; +import { Trans } from '@lingui/macro'; +import Dialog, { DialogPrimaryButton } from '../UI/Dialog'; +import SemiControlledTextField from '../UI/SemiControlledTextField'; + +type Props = {| + initialValue: string, + onClose: (newValue: string) => void, +|}; + +export const MultilineVariableEditorDialog = ({ + initialValue, + onClose, +}: Props) => { + const [value, setValue] = React.useState(initialValue); + + return ( + Ok} + primary + onClick={() => onClose(value)} + />, + ]} + maxWidth="md" + onRequestClose={() => onClose(value)} + onApply={() => onClose(value)} + > + Initial text of the variable} + value={value} + onChange={setValue} + rows={5} + rowsMax={10} + /> + + ); +}; diff --git a/newIDE/app/src/VariablesList/VariableToTreeNodeHandling.js b/newIDE/app/src/VariablesList/VariableToTreeNodeHandling.js index 78a1a38598..3d0688f47d 100644 --- a/newIDE/app/src/VariablesList/VariableToTreeNodeHandling.js +++ b/newIDE/app/src/VariablesList/VariableToTreeNodeHandling.js @@ -102,43 +102,6 @@ export const getVariableContextFromNodeId = ( }; }; -export const getExpandedNodeIdsFromVariables = ( - variables: { name: string, variable: gdVariable }[], - accumulator: string[], - parentNodeId: string = '' -): string[] => { - let newAccumulator = []; - for (const { name, variable } of variables) { - const nodeId = parentNodeId ? `${parentNodeId}${separator}${name}` : name; - if (!variable.isFolded() && variable.getChildrenCount() > 0) { - newAccumulator.push(nodeId); - } - if (variable.getType() === gd.Variable.Array) { - const children = mapFor(0, variable.getChildrenCount(), index => ({ - name: index.toString(), - variable: variable.getAtIndex(index), - })); - newAccumulator = [ - ...newAccumulator, - ...getExpandedNodeIdsFromVariables(children, newAccumulator, nodeId), - ]; - } else if (variable.getType() === gd.Variable.Structure) { - const children = variable - .getAllChildrenNames() - .toJSArray() - .map((childName, index) => ({ - variable: variable.getChild(childName), - name: childName, - })); - newAccumulator = [ - ...newAccumulator, - ...getExpandedNodeIdsFromVariables(children, newAccumulator, nodeId), - ]; - } - } - return newAccumulator; -}; - export const updateListOfNodesFollowingChangeName = ( list: string[], oldNodeId: string, @@ -164,38 +127,6 @@ export const updateListOfNodesFollowingChangeName = ( return newList; }; -export const getExpandedNodeIdsFromVariablesContainer = ( - variablesContainer: gdVariablesContainer, - isInherited: boolean = false -): string[] => { - const variables = []; - for (let index = 0; index < variablesContainer.count(); index += 1) { - variables.push({ - name: `${ - isInherited ? inheritedPrefix : '' - }${variablesContainer.getNameAt(index)}`, - variable: variablesContainer.getAt(index), - }); - } - return getExpandedNodeIdsFromVariables(variables, []); -}; - -export const foldNodesVariables = ( - variablesContainer: gdVariablesContainer, - nodes: string[], - fold: boolean -) => { - nodes.forEach(nodeId => { - const { variable } = getVariableContextFromNodeId( - nodeId, - variablesContainer - ); - if (variable) { - variable.setFolded(fold); - } - }); -}; - export const getMovementTypeWithinVariablesContainer = ( draggedVariableContext: VariableContext, targetVariableContext: VariableContext diff --git a/newIDE/app/src/VariablesList/VariableToTreeNodeHandling.spec.js b/newIDE/app/src/VariablesList/VariableToTreeNodeHandling.spec.js index c3830d897a..c47751b99d 100644 --- a/newIDE/app/src/VariablesList/VariableToTreeNodeHandling.spec.js +++ b/newIDE/app/src/VariablesList/VariableToTreeNodeHandling.spec.js @@ -2,7 +2,6 @@ import { generateListOfNodesMatchingSearchInVariable, generateListOfNodesMatchingSearchInVariablesContainer, - getExpandedNodeIdsFromVariablesContainer, getVariableContextFromNodeId, separator, updateListOfNodesFollowingChangeName, @@ -129,14 +128,6 @@ describe('VariableToTreeNodeHandling', () => { }); }); - describe('getExpandedNodeIdsFromVariablesContainer', () => { - test('List of unfolded nodes are returned', () => { - expect( - getExpandedNodeIdsFromVariablesContainer(variablesContainer) - ).toEqual(['parent', `parent2${separator}structureChild`]); - }); - }); - describe('updateListOfNodesFollowingChangeName', () => { test('Concerned variable node id is modified in the list', () => { expect( diff --git a/newIDE/app/src/VariablesList/VariableTypeSelector.js b/newIDE/app/src/VariablesList/VariableTypeSelector.js index a767c1f0af..7763e77bd8 100644 --- a/newIDE/app/src/VariablesList/VariableTypeSelector.js +++ b/newIDE/app/src/VariablesList/VariableTypeSelector.js @@ -15,9 +15,10 @@ const gd = global.gd; type Props = {| variableType: Variable_Type, - onChange: (newVariableType: string) => void, + onChange: (newVariableType: string, nodeId: string) => void, + nodeId: string, isHighlighted?: boolean, - disabled?: boolean, + readOnlyWithIcon?: boolean, id?: string, |}; @@ -80,7 +81,7 @@ const getVariableTypeToString = () => { return variableTypeToString; }; -const VariableTypeSelector = (props: Props) => { +const VariableTypeSelector = React.memo((props: Props) => { const gdevelopTheme = React.useContext(GDevelopThemeContext); const Icon = getVariableTypeToIcon()[props.variableType]; @@ -94,26 +95,33 @@ const VariableTypeSelector = (props: Props) => { : undefined } /> - - - props.onChange(getVariableTypeToString()[event.target.value]) - } - inputStyle={ - props.isHighlighted - ? { color: gdevelopTheme.listItem.selectedTextColor } - : undefined - } - disabled={props.disabled} - id={props.id} - > - {getOptions()} - + {!props.readOnlyWithIcon && ( + <> + + + props.onChange( + getVariableTypeToString()[event.target.value], + props.nodeId + ) + } + inputStyle={{ + fontSize: 14, + color: props.isHighlighted + ? gdevelopTheme.listItem.selectedTextColor + : undefined, + }} + id={props.id} + > + {getOptions()} + + + )} ); -}; +}); export default VariableTypeSelector; diff --git a/newIDE/app/src/VariablesList/VariablesEditorDialog.js b/newIDE/app/src/VariablesList/VariablesEditorDialog.js index 6037f99a98..73bf2d13e7 100644 --- a/newIDE/app/src/VariablesList/VariablesEditorDialog.js +++ b/newIDE/app/src/VariablesList/VariablesEditorDialog.js @@ -168,7 +168,6 @@ const VariablesEditorDialog = ({ )} ) => event.stopPropagation(); -const preventEventDefaultEffect = ( - event: SyntheticPointerEvent -) => event.preventDefault(); const styles = { inlineIcon: { padding: 0 }, handlePlaceholder: { width: 24 } }; @@ -106,68 +103,426 @@ type Props = {| emptyPlaceholderTitle?: React.Node, emptyPlaceholderDescription?: React.Node, helpPagePath?: ?string, - /** If set to false, it will commit changes to variables on each input change. It can be expensive, but useful when VariablesList can be unmounted at any time. */ - commitChangesOnBlur: boolean, + /** If set to true, it will commit changes to variables on each input change. It can be expensive, but useful when VariablesList can be unmounted at any time. */ + directlyStoreValueChangesWhileEditing?: boolean, /** If set to small, will collapse variable row by default. */ size?: 'small', onVariablesUpdated?: () => void, |}; -const StyledTreeItem = withStyles(theme => ({ - group: { - marginLeft: 7, - paddingLeft: 15, - }, - iconContainer: { - alignSelf: 'stretch', - alignItems: 'center', - }, - root: { - '&:focus:not(.Mui-selected)': { - '& > .MuiTreeItem-content': { - filter: 'brightness(1.15)', - }, - '& > .MuiTreeItem-content > .MuiTreeItem-label': { - backgroundColor: 'unset', - }, - }, - '&:hover:not(:focus)': { - '& > .MuiTreeItem-content:hover': { - filter: 'brightness(1.07)', - }, - }, - '&.Mui-selected:hover': { - '& > .MuiTreeItem-content:hover': { - filter: 'brightness(1.07)', - }, - }, - }, - label: { - padding: 0, - '&:hover': { - backgroundColor: 'unset', - }, - }, - content: { marginTop: 5 }, -}))(props => ); +const variableRowStyles = { + chevron: { width: 15 }, +}; -const VariablesList = ({ onComputeAllVariableNames, ...props }: Props) => { - const [expandedNodes, setExpandedNodes] = React.useState>( - getExpandedNodeIdsFromVariablesContainer(props.variablesContainer).concat( - props.inheritedVariablesContainer - ? getExpandedNodeIdsFromVariablesContainer( - props.inheritedVariablesContainer, - true +type VariableRowProps = {| + // Context: + depth: number, + isNarrow: boolean, + containerWidth: ?number, + shouldHideExpandIcons: boolean, + isExpanded: boolean, + onExpand: (shouldExpand: boolean, nodeId: string) => void, + draggedNodeId: { current: ?string }, + nodeId: string, + isInherited: boolean, + canDrop: string => boolean, + dropNode: string => void, + isSelected: boolean, + onSelect: (shouldMultiselect: boolean, nodeId: string) => void, + topLevelVariableNameInputRefs: {| + current: { [number]: SimpleTextFieldInterface }, + |}, + topLevelVariableValueInputRefs: {| + current: { [number]: SimpleTextFieldInterface }, + |}, + parentType: Variable_Type | null, + directlyStoreValueChangesWhileEditing: boolean, + + // Styling + gdevelopTheme: GDevelopTheme, + rowRightSideStyle: any, + + // Variable information: + onChangeName: (string, string) => void, + overwritesInheritedVariable: boolean | void, + name: string, + index: number, + isTopLevel: boolean, + type: Variable_Type, + onChangeType: (string, nodeId: string) => void, + valueAsString: string | null, + valueAsBool: boolean | null, + onChangeValue: (string, nodeId: string) => void, + isCollection: boolean, + variablePointer: number, + onAddChild: string => void, + editInheritedVariable: string => void, + deleteNode: string => void, +|}; + +const VariableRow = React.memo( + ({ + depth, + isNarrow, + containerWidth, + shouldHideExpandIcons, + isExpanded, + onExpand, + draggedNodeId, + nodeId, + isInherited, + canDrop, + dropNode, + isSelected, + onSelect, + gdevelopTheme, + topLevelVariableNameInputRefs, + topLevelVariableValueInputRefs, + parentType, + onChangeName, + overwritesInheritedVariable, + name, + index, + rowRightSideStyle, + isTopLevel, + type, + onChangeType, + valueAsString, + valueAsBool, + onChangeValue, + isCollection, + variablePointer, + onAddChild, + editInheritedVariable, + deleteNode, + directlyStoreValueChangesWhileEditing, + }: VariableRowProps) => { + const shouldWrap = + isNarrow || + (!containerWidth + ? false + : containerWidth <= 750 + ? depth >= 5 + : containerWidth <= 850 + ? depth >= 6 + : containerWidth <= 950 + ? depth >= 7 + : depth >= 8); + const [editInMultilineEditor, setEditInMultilineEditor] = React.useState( + false + ); + const forceUpdate = useForceUpdate(); + const hasLineBreaks = valueAsString + ? valueAsString.indexOf('\n') !== -1 + : false; + + return ( + { + draggedNodeId.current = nodeId; + return {}; + }} + canDrag={() => !isInherited} + canDrop={() => canDrop(nodeId)} + drop={() => { + dropNode(nodeId); + }} + > + {({ connectDragSource, connectDropTarget, isOver, canDrop }) => + connectDropTarget( +
{ + const shouldMultiSelect = event.metaKey || event.ctrlKey; + onSelect(shouldMultiSelect, nodeId); + }} + > + {isOver && } +
+ {shouldHideExpandIcons ? null : isCollection ? ( + onExpand(!isExpanded, nodeId)} + focusRipple + style={variableRowStyles.chevron} + > + {isExpanded ? : } + + ) : ( +
+ )} + + {isInherited ? ( + + ) : ( + connectDragSource( + + + + ) + )} + + + {shouldWrap ? null : } + { + if (depth === 0 && element) { + topLevelVariableNameInputRefs.current[ + variablePointer + ] = element; + } + }} + directlyStoreValueChangesWhileEditing={ + directlyStoreValueChangesWhileEditing + } + disabled={isInherited || parentType === gd.Variable.Array} + onChange={onChangeName} + additionalContext={JSON.stringify({ nodeId, depth })} + italic={!!overwritesInheritedVariable} + value={name} + id={`variable-${index}-name`} + /> + + +
+ + + + + + {type === gd.Variable.Boolean ? ( + + + + {valueAsBool ? ( + True + ) : ( + False + )} + + + {isInherited && !isTopLevel ? null : ( + <> + + { + onChangeValue( + !valueAsBool ? 'true' : 'false', + nodeId + ); + forceUpdate(); + }} + tooltip={ + !valueAsBool + ? t`Set to true` + : t`Set to false` + } + > + + + + )} + + ) : ( + { + if (depth === 0 && element) { + topLevelVariableValueInputRefs.current[ + variablePointer + ] = element; + } + }} + type={ + type === gd.Variable.Number ? 'number' : 'text' + } + directlyStoreValueChangesWhileEditing={ + directlyStoreValueChangesWhileEditing + } + key="value" + disabled={ + isCollection || + (isInherited && !isTopLevel) || + hasLineBreaks + } + onChange={onChangeValue} + value={ + // If line breaks are present, disable the field (as it's + // single line only) and make line breaks visible. + hasLineBreaks + ? (valueAsString || '').replace(/\n/g, '↵') + : valueAsString || '' + } + additionalContext={nodeId} + id={`variable-${index}-text-value`} + /> + )} + + {// Only show the large edit button for string variables, + // and not for those who are in an inherited structure or array. + type === gd.Variable.String && + !(isInherited && !isTopLevel) ? ( + { + stopEventPropagation(event); + setEditInMultilineEditor(true); + }} + > + + + ) : null} + {isCollection && !isInherited ? ( + { + stopEventPropagation(event); + onAddChild(nodeId); + }} + > + + + ) : null} + {isCollection && isInherited && isTopLevel ? ( + { + stopEventPropagation(event); + editInheritedVariable(nodeId); + }} + > + + + ) : null} + {overwritesInheritedVariable && isTopLevel ? ( + { + stopEventPropagation(event); + deleteNode(nodeId); + }} + > + + + ) : null} + +
+
+
+ {editInMultilineEditor && ( + { + onChangeValue(newValue, nodeId); + setEditInMultilineEditor(false); + forceUpdate(); + }} + /> + )} +
) - : [] - ) - ); - const [history, setHistory] = React.useState( + } + + ); + } +); + +const VariablesList = (props: Props) => { + const historyRef = useRefWithInit(() => getHistoryInitialState(props.variablesContainer, { historyMaxSize: 50, }) ); + const [searchText, setSearchText] = React.useState(''); + const { onComputeAllVariableNames } = props; const allVariablesNames = React.useMemo>( () => (onComputeAllVariableNames ? onComputeAllVariableNames() : null), [onComputeAllVariableNames] @@ -177,14 +532,11 @@ const VariablesList = ({ onComputeAllVariableNames, ...props }: Props) => { Array >([]); const [containerWidth, setContainerWidth] = React.useState(null); - const [nameErrors, setNameErrors] = React.useState<{ [number]: React.Node }>( - {} - ); const topLevelVariableNameInputRefs = React.useRef<{| - [number]: SemiControlledAutoCompleteInterface, + [number]: SimpleTextFieldInterface, |}>({}); const topLevelVariableValueInputRefs = React.useRef<{| - [number]: SemiControlledTextFieldInterface, + [number]: SimpleTextFieldInterface, |}>({}); // $FlowFixMe - Hard to fix issue regarding strict checking with interface. const refocusNameField = useRefocusField(topLevelVariableNameInputRefs); @@ -229,32 +581,6 @@ const VariablesList = ({ onComputeAllVariableNames, ...props }: Props) => { (props.inheritedVariablesContainer ? !hasVariablesContainerSubChildren(props.inheritedVariablesContainer) : true); - const useStylesForSelectedTreeItem = makeStyles(() => ({ - root: { - '& > .MuiTreeItem-content': { - backgroundColor: gdevelopTheme.list.itemsBackgroundColor, - }, - '&.Mui-selected > .MuiTreeItem-content': { - marginTop: 5, - backgroundColor: gdevelopTheme.listItem.selectedBackgroundColor, - }, - '&.Mui-selected > .MuiTreeItem-content > .MuiTreeItem-label': { - backgroundColor: 'unset', - }, - '&.isCollection > .MuiTreeItem-content > .MuiTreeItem-iconContainer': { - backgroundColor: gdevelopTheme.listItem.selectedBackgroundColor, - }, - '& > .MuiTreeItem-content > .MuiTreeItem-iconContainer': shouldHideExpandIcons - ? { - display: 'none', - } - : undefined, - }, - group: { - borderLeft: `1px solid ${gdevelopTheme.listItem.groupTextColor}`, - }, - })); - const selectedTreeItemClasses = useStylesForSelectedTreeItem(); const rowRightSideStyle = React.useMemo( () => ({ @@ -270,52 +596,54 @@ const VariablesList = ({ onComputeAllVariableNames, ...props }: Props) => { ); const undefinedVariableNames = allVariablesNames - ? allVariablesNames - .map(variableName => { - if (!props.variablesContainer.has(variableName)) { - return { text: variableName, value: variableName }; - } - return null; - }) - .filter(Boolean) + ? allVariablesNames.filter(variableName => { + return !props.variablesContainer.has(variableName); + }) : []; const { historyHandler, onVariablesUpdated, variablesContainer } = props; const _onChange = React.useCallback( () => { - historyHandler - ? historyHandler.saveToHistory() - : setHistory(saveToHistory(history, variablesContainer)); + if (historyHandler) historyHandler.saveToHistory(); + else + historyRef.current = saveToHistory( + historyRef.current, + variablesContainer + ); if (onVariablesUpdated) onVariablesUpdated(); }, - [history, historyHandler, onVariablesUpdated, variablesContainer] + [historyRef, historyHandler, onVariablesUpdated, variablesContainer] ); const _undo = React.useCallback( () => { - historyHandler - ? historyHandler.undo() - : setHistory(undo(history, props.variablesContainer)); + if (historyHandler) historyHandler.undo(); + else + historyRef.current = undo(historyRef.current, props.variablesContainer); setSelectedNodes([]); }, - [history, historyHandler, props.variablesContainer] + [historyRef, historyHandler, props.variablesContainer] ); const _redo = React.useCallback( () => { - historyHandler - ? historyHandler.redo() - : setHistory(redo(history, props.variablesContainer)); + if (historyHandler) historyHandler.redo(); + else + historyRef.current = redo(historyRef.current, props.variablesContainer); setSelectedNodes([]); }, - [history, historyHandler, props.variablesContainer] + [historyRef, historyHandler, props.variablesContainer] ); const _canUndo = (): boolean => - props.historyHandler ? props.historyHandler.canUndo() : canUndo(history); + props.historyHandler + ? props.historyHandler.canUndo() + : canUndo(historyRef.current); const _canRedo = (): boolean => - props.historyHandler ? props.historyHandler.canRedo() : canRedo(history); + props.historyHandler + ? props.historyHandler.canRedo() + : canRedo(historyRef.current); const keyboardShortcuts = new KeyboardShortcuts({ isActive: () => true, @@ -399,8 +727,10 @@ const VariablesList = ({ onComputeAllVariableNames, ...props }: Props) => { if (!name) return; const { name: newName } = insertInVariablesContainer( props.variablesContainer, - name, - serializedVariable + gd.Project.getSafeName(name), + serializedVariable, + props.variablesContainer.count(), + props.inheritedVariablesContainer ); newSelectedNodes.push(newName); } else { @@ -425,7 +755,8 @@ const VariablesList = ({ onComputeAllVariableNames, ...props }: Props) => { props.variablesContainer, name, serializedVariable, - props.variablesContainer.getPosition(targetVariableName) + 1 + props.variablesContainer.getPosition(targetVariableName) + 1, + props.inheritedVariablesContainer ); newSelectedNodes.push(newName); } else { @@ -471,7 +802,12 @@ const VariablesList = ({ onComputeAllVariableNames, ...props }: Props) => { _onChange(); setSelectedNodes(newSelectedNodes); }, - [_onChange, props.variablesContainer, selectedNodes] + [ + _onChange, + props.inheritedVariablesContainer, + props.variablesContainer, + selectedNodes, + ] ); const _deleteNode = React.useCallback( @@ -497,312 +833,339 @@ const VariablesList = ({ onComputeAllVariableNames, ...props }: Props) => { [props.variablesContainer] ); - const deleteNode = (nodeId: string): void => { - const success = _deleteNode(nodeId); - if (success) { - _onChange(); - } - }; + const deleteNode = React.useCallback( + (nodeId: string): void => { + const success = _deleteNode(nodeId); + if (success) { + _onChange(); + forceUpdate(); + } + }, + [_onChange, forceUpdate, _deleteNode] + ); const deleteSelection = React.useCallback( () => { - const deleteSuccesses = selectedNodes.map(_deleteNode); + // Take advantage of the node ids notation to sort them in + // descending lexicographical order, so we delete from "last" + // to "first". In the case of arrays, this avoids to change + // the index of the variables while deleting them, which would + // result in the wrong variables to be deleted if multiple of them + // are removed. + const deleteSuccesses = selectedNodes + .sort() + .reverse() + .map(_deleteNode); if (deleteSuccesses.some(Boolean)) { + _onChange(); setSelectedNodes([]); } }, - [_deleteNode, selectedNodes] + [_onChange, _deleteNode, selectedNodes] ); - const updateExpandedAndSelectedNodesFollowingNameChange = ( - oldNodeId: string, - newName: string - ) => { - setExpandedNodes( - updateListOfNodesFollowingChangeName(expandedNodes, oldNodeId, newName) - ); - setSelectedNodes( - updateListOfNodesFollowingChangeName(selectedNodes, oldNodeId, newName) - ); - if (!!searchText) { - setSearchMatchingNodes( - updateListOfNodesFollowingChangeName( - searchMatchingNodes, - oldNodeId, - newName - ) + const updateExpandedAndSelectedNodesFollowingNameChange = React.useCallback( + (oldNodeId: string, newName: string) => { + setSelectedNodes(selectedNodes => + updateListOfNodesFollowingChangeName(selectedNodes, oldNodeId, newName) ); - } - }; - - const updateExpandedAndSelectedNodesFollowingNodeMove = ( - oldNodeId: string, - newParentNodeId: string, - newName: string - ) => { - // TODO: Recompute list of selected nodes following a node move that changes all the values of an array. - setSelectedNodes([]); - const inheritedExpandedNodes = expandedNodes.filter(nodeId => - nodeId.startsWith(inheritedPrefix) - ); - setExpandedNodes([ - ...inheritedExpandedNodes, - ...getExpandedNodeIdsFromVariablesContainer(props.variablesContainer), - ]); - if (!!searchText) { - triggerSearch(); - forceUpdate(); - } - }; - - const canDrop = (nodeId: string): boolean => { - if (nodeId.startsWith(inheritedPrefix)) return false; - const { current } = draggedNodeId; - if (!current) return false; - - const targetVariableContext = getVariableContextFromNodeId( - nodeId, - props.variablesContainer - ); - const { lineage: targetLineage } = targetVariableContext; - - const draggedVariableContext = getVariableContextFromNodeId( - current, - props.variablesContainer - ); - const { variable: draggedVariable } = draggedVariableContext; - if (!draggedVariable) return false; - - if (isAnAncestryOf(draggedVariable, targetLineage)) return false; - - const movementType = getMovementTypeWithinVariablesContainer( - draggedVariableContext, - targetVariableContext - ); - - switch (movementType) { - case 'InsideTopLevel': - case 'TopLevelToStructure': - case 'StructureToTopLevel': - case 'FromStructureToAnotherStructure': - case 'FromArrayToAnotherArray': - case 'InsideSameArray': - return true; - case 'FromStructureToArray': - case 'FromArrayToStructure': - case 'ArrayToTopLevel': - case 'InsideSameStructure': - case 'TopLevelToArray': - default: - return false; - } - }; - - const dropNode = (nodeId: string): void => { - if (nodeId.startsWith(inheritedPrefix)) return; - const { current } = draggedNodeId; - if (!current) return; - - // TODO: Add logic to copy dragged variable instead of moving it if Alt/Opt key is pressed - // React-dnd keeps the focus when user is dragging so keyboard shortcut instance - // cannot detect if the key is pressed while dragging. React-dnd has issues to - // return event data about pressed keys when mouse is up. - - const targetVariableContext = getVariableContextFromNodeId( - nodeId, - props.variablesContainer - ); - const { lineage: targetLineage, name: targetName } = targetVariableContext; - const targetVariableParentVariable = getDirectParentVariable(targetLineage); - if (!targetName) return; - - const draggedVariableContext = getVariableContextFromNodeId( - current, - props.variablesContainer - ); - const { - variable: draggedVariable, - lineage: draggedLineage, - name: draggedName, - } = draggedVariableContext; - const draggedVariableParentVariable = getDirectParentVariable( - draggedLineage - ); - if (!draggedVariable || !draggedName) return; - - if (isAnAncestryOf(draggedVariable, targetLineage)) return; - - const movementType = getMovementTypeWithinVariablesContainer( - draggedVariableContext, - targetVariableContext - ); - let newName; - let draggedIndex; - let targetIndex; - let movementHasBeenMade = true; - let parentNodeId; - let targetParentNodeId; - - switch (movementType) { - case 'InsideTopLevel': - draggedIndex = props.variablesContainer.getPosition(draggedName); - targetIndex = props.variablesContainer.getPosition(targetName); - props.variablesContainer.move( - draggedIndex, - targetIndex > draggedIndex ? targetIndex - 1 : targetIndex - ); - break; - case 'TopLevelToStructure': - newName = newNameGenerator( - draggedName, - // $FlowFixMe - Regarding movement type, we are confident that the variable will exist - name => targetVariableParentVariable.hasChild(name), - 'CopyOf' - ); - - // $FlowFixMe - Regarding movement type, we are confident that the variable will exist - targetVariableParentVariable.insertChild(newName, draggedVariable); - - props.variablesContainer.remove(draggedName); - parentNodeId = getDirectParentNodeId(targetLineage); - if (parentNodeId) - updateExpandedAndSelectedNodesFollowingNodeMove( - current, - parentNodeId, + if (!!searchText) { + setSearchMatchingNodes(searchMatchingNodes => + updateListOfNodesFollowingChangeName( + searchMatchingNodes, + oldNodeId, newName - ); - break; - case 'StructureToTopLevel': - newName = newNameGenerator( - draggedName, - name => props.variablesContainer.has(name), - 'CopyOf' - ); - props.variablesContainer.insert( - newName, - draggedVariable, - props.variablesContainer.getPosition(targetName) + ) ); + } + }, + [searchText] + ); + + const updateExpandedAndSelectedNodesFollowingNodeMove = React.useCallback( + (oldNodeId: string, newParentNodeId: string, newName: string) => { + // TODO: Recompute list of selected nodes following a node move that changes all the values of an array. + setSelectedNodes([]); + if (!!searchText) { + triggerSearch(); + forceUpdate(); + } + }, + [forceUpdate, searchText, triggerSearch] + ); + + const canDrop = React.useCallback( + (nodeId: string): boolean => { + if (nodeId.startsWith(inheritedPrefix)) return false; + const { current } = draggedNodeId; + if (!current) return false; + + const targetVariableContext = getVariableContextFromNodeId( + nodeId, + props.variablesContainer + ); + const { lineage: targetLineage } = targetVariableContext; + + const draggedVariableContext = getVariableContextFromNodeId( + current, + props.variablesContainer + ); + const { variable: draggedVariable } = draggedVariableContext; + if (!draggedVariable) return false; + + if (isAnAncestryOf(draggedVariable, targetLineage)) return false; + + const movementType = getMovementTypeWithinVariablesContainer( + draggedVariableContext, + targetVariableContext + ); + + switch (movementType) { + case 'InsideTopLevel': + case 'TopLevelToStructure': + case 'StructureToTopLevel': + case 'FromStructureToAnotherStructure': + case 'FromArrayToAnotherArray': + case 'InsideSameArray': + return true; + case 'FromStructureToArray': + case 'FromArrayToStructure': + case 'ArrayToTopLevel': + case 'InsideSameStructure': + case 'TopLevelToArray': + default: + return false; + } + }, + [props.variablesContainer] + ); + + const dropNode = React.useCallback( + (nodeId: string): void => { + if (nodeId.startsWith(inheritedPrefix)) return; + const { current } = draggedNodeId; + if (!current) return; + + // TODO: Add logic to copy dragged variable instead of moving it if Alt/Opt key is pressed + // React-dnd keeps the focus when user is dragging so keyboard shortcut instance + // cannot detect if the key is pressed while dragging. React-dnd has issues to + // return event data about pressed keys when mouse is up. + + const targetVariableContext = getVariableContextFromNodeId( + nodeId, + props.variablesContainer + ); + const { + lineage: targetLineage, + name: targetName, + } = targetVariableContext; + const targetVariableParentVariable = getDirectParentVariable( + targetLineage + ); + if (!targetName) return; + + const draggedVariableContext = getVariableContextFromNodeId( + current, + props.variablesContainer + ); + const { + variable: draggedVariable, + lineage: draggedLineage, + name: draggedName, + } = draggedVariableContext; + const draggedVariableParentVariable = getDirectParentVariable( + draggedLineage + ); + if (!draggedVariable || !draggedName) return; + + if (isAnAncestryOf(draggedVariable, targetLineage)) return; + + const movementType = getMovementTypeWithinVariablesContainer( + draggedVariableContext, + targetVariableContext + ); + let newName; + let draggedIndex; + let targetIndex; + let movementHasBeenMade = true; + let parentNodeId; + let targetParentNodeId; + + switch (movementType) { + case 'InsideTopLevel': + draggedIndex = props.variablesContainer.getPosition(draggedName); + targetIndex = props.variablesContainer.getPosition(targetName); + props.variablesContainer.move( + draggedIndex, + targetIndex > draggedIndex ? targetIndex - 1 : targetIndex + ); + break; + case 'TopLevelToStructure': + newName = newNameGenerator( + draggedName, + // $FlowFixMe - Regarding movement type, we are confident that the variable will exist + name => targetVariableParentVariable.hasChild(name), + 'CopyOf' + ); - // $FlowFixMe - Regarding movement type, we are confident that the variable will exist - draggedVariableParentVariable.removeChild(draggedName); - updateExpandedAndSelectedNodesFollowingNodeMove(current, '', newName); - break; - case 'FromStructureToAnotherStructure': - newName = newNameGenerator( - draggedName, // $FlowFixMe - Regarding movement type, we are confident that the variable will exist - name => targetVariableParentVariable.hasChild(name), - 'CopyOf' - ); - // $FlowFixMe - Regarding movement type, we are confident that the variable will exist - targetVariableParentVariable.insertChild(newName, draggedVariable); + targetVariableParentVariable.insertChild(newName, draggedVariable); - // $FlowFixMe - Regarding movement type, we are confident that the variable will exist - draggedVariableParentVariable.removeChild(draggedName); - parentNodeId = getDirectParentNodeId(targetLineage); - if (parentNodeId) - updateExpandedAndSelectedNodesFollowingNodeMove( - current, - parentNodeId, - newName + props.variablesContainer.remove(draggedName); + parentNodeId = getDirectParentNodeId(targetLineage); + if (parentNodeId) + updateExpandedAndSelectedNodesFollowingNodeMove( + current, + parentNodeId, + newName + ); + break; + case 'StructureToTopLevel': + newName = newNameGenerator( + gd.Project.getSafeName(draggedName), + name => props.variablesContainer.has(name), + 'CopyOf' + ); + props.variablesContainer.insert( + newName, + draggedVariable, + props.variablesContainer.getPosition(targetName) ); - break; - case 'FromArrayToAnotherArray': - draggedIndex = parseInt(draggedName, 10); - targetIndex = parseInt(targetName, 10); - // $FlowFixMe - Regarding movement type, we are confident that the variable will exist - targetVariableParentVariable.insertAtIndex( - draggedVariable, - targetIndex - ); + // $FlowFixMe - Regarding movement type, we are confident that the variable will exist + draggedVariableParentVariable.removeChild(draggedName); + updateExpandedAndSelectedNodesFollowingNodeMove(current, '', newName); + break; + case 'FromStructureToAnotherStructure': + newName = newNameGenerator( + draggedName, + // $FlowFixMe - Regarding movement type, we are confident that the variable will exist + name => targetVariableParentVariable.hasChild(name), + 'CopyOf' + ); + // $FlowFixMe - Regarding movement type, we are confident that the variable will exist + targetVariableParentVariable.insertChild(newName, draggedVariable); - // $FlowFixMe - Regarding movement type, we are confident that the variable will exist - draggedVariableParentVariable.removeAtIndex(draggedIndex); - targetParentNodeId = getDirectParentNodeId(targetLineage); - if (targetParentNodeId) - updateExpandedAndSelectedNodesFollowingNodeMove( - current, - targetParentNodeId, - targetIndex.toString() + // $FlowFixMe - Regarding movement type, we are confident that the variable will exist + draggedVariableParentVariable.removeChild(draggedName); + parentNodeId = getDirectParentNodeId(targetLineage); + if (parentNodeId) + updateExpandedAndSelectedNodesFollowingNodeMove( + current, + parentNodeId, + newName + ); + break; + case 'FromArrayToAnotherArray': + draggedIndex = parseInt(draggedName, 10); + targetIndex = parseInt(targetName, 10); + + // $FlowFixMe - Regarding movement type, we are confident that the variable will exist + targetVariableParentVariable.insertAtIndex( + draggedVariable, + targetIndex ); - break; - case 'InsideSameArray': - draggedIndex = parseInt(draggedName, 10); - targetIndex = parseInt(targetName, 10); - const correctedTargetIndex = - targetIndex > draggedIndex ? targetIndex - 1 : targetIndex; - // $FlowFixMe - Regarding movement type, we are confident that the variable will exist - targetVariableParentVariable.moveChildInArray( - draggedIndex, - correctedTargetIndex + + // $FlowFixMe - Regarding movement type, we are confident that the variable will exist + draggedVariableParentVariable.removeAtIndex(draggedIndex); + targetParentNodeId = getDirectParentNodeId(targetLineage); + if (targetParentNodeId) + updateExpandedAndSelectedNodesFollowingNodeMove( + current, + targetParentNodeId, + targetIndex.toString() + ); + break; + case 'InsideSameArray': + draggedIndex = parseInt(draggedName, 10); + targetIndex = parseInt(targetName, 10); + const correctedTargetIndex = + targetIndex > draggedIndex ? targetIndex - 1 : targetIndex; + // $FlowFixMe - Regarding movement type, we are confident that the variable will exist + targetVariableParentVariable.moveChildInArray( + draggedIndex, + correctedTargetIndex + ); + parentNodeId = getDirectParentNodeId(targetLineage); + if (parentNodeId) { + updateExpandedAndSelectedNodesFollowingNodeMove( + current, + parentNodeId, + correctedTargetIndex.toString() + ); + } + break; + case 'FromStructureToArray': + case 'FromArrayToStructure': + case 'ArrayToTopLevel': + case 'InsideSameStructure': + default: + movementHasBeenMade = false; + } + if (movementHasBeenMade) { + _onChange(); + forceUpdate(); + } + }, + [ + _onChange, + forceUpdate, + props.variablesContainer, + updateExpandedAndSelectedNodesFollowingNodeMove, + ] + ); + + const onAddChild = React.useCallback( + (nodeId: string) => { + if (nodeId.startsWith(inheritedPrefix)) return; + const { variable } = getVariableContextFromNodeId( + nodeId, + props.variablesContainer + ); + if (!variable || !isCollectionVariable(variable)) return; + const type = variable.getType(); + + if (type === gd.Variable.Structure) { + const name = newNameGenerator('ChildVariable', name => + variable.hasChild(name) ); - parentNodeId = getDirectParentNodeId(targetLineage); - if (parentNodeId) { - updateExpandedAndSelectedNodesFollowingNodeMove( - current, - parentNodeId, - correctedTargetIndex.toString() - ); - } - break; - case 'FromStructureToArray': - case 'FromArrayToStructure': - case 'ArrayToTopLevel': - case 'InsideSameStructure': - default: - movementHasBeenMade = false; - } - if (movementHasBeenMade) { + variable.getChild(name).setString(''); + } else if (type === gd.Variable.Array) variable.pushNew(); _onChange(); + if (variable.isFolded()) variable.setFolded(false); forceUpdate(); - } - }; + }, + [_onChange, forceUpdate, props.variablesContainer] + ); - const onAddChild = (nodeId: string) => { - if (nodeId.startsWith(inheritedPrefix)) return; - const { variable } = getVariableContextFromNodeId( - nodeId, - props.variablesContainer - ); - if (!variable || !isCollectionVariable(variable)) return; - const type = variable.getType(); - - if (type === gd.Variable.Structure) { - const name = newNameGenerator('ChildVariable', name => - variable.hasChild(name) + const editInheritedVariable = React.useCallback( + (nodeId: string): void => { + if (!props.inheritedVariablesContainer) return; + const { + variable: inheritedVariable, + name: inheritedVariableName, + } = getVariableContextFromNodeId( + nodeId, + props.inheritedVariablesContainer ); - variable.getChild(name).setString(''); - } else if (type === gd.Variable.Array) variable.pushNew(); - _onChange(); - if (variable.isFolded()) variable.setFolded(false); - setExpandedNodes([...expandedNodes, nodeId]); - }; - - const editInheritedVariable = (nodeId: string): void => { - if (!props.inheritedVariablesContainer) return; - const { - variable: inheritedVariable, - name: inheritedVariableName, - } = getVariableContextFromNodeId(nodeId, props.inheritedVariablesContainer); - if (!inheritedVariable || !inheritedVariableName) return; - if (props.variablesContainer.has(inheritedVariableName)) return; - const newVariable = new gd.Variable(); - unserializeFromJSObject( - newVariable, - serializeToJSObject(inheritedVariable) - ); - props.variablesContainer.insert( - inheritedVariableName, - newVariable, - props.variablesContainer.count() - ); - _onChange(); - setSelectedNodes([inheritedVariableName]); - setExpandedNodes([...expandedNodes, inheritedVariableName]); - newVariable.delete(); - }; + if (!inheritedVariable || !inheritedVariableName) return; + if (props.variablesContainer.has(inheritedVariableName)) return; + const newVariable = new gd.Variable(); + unserializeFromJSObject( + newVariable, + serializeToJSObject(inheritedVariable) + ); + props.variablesContainer.insert( + inheritedVariableName, + newVariable, + props.variablesContainer.count() + ); + _onChange(); + setSelectedNodes([inheritedVariableName]); + newVariable.delete(); + }, + [_onChange, props.inheritedVariablesContainer, props.variablesContainer] + ); const onAdd = React.useCallback( () => { @@ -815,7 +1178,8 @@ const VariablesList = ({ onComputeAllVariableNames, ...props }: Props) => { props.variablesContainer, 'Variable', null, - props.variablesContainer.count() + props.variablesContainer.count(), + props.inheritedVariablesContainer ); _onChange(); setSelectedNodes([newName]); @@ -841,13 +1205,45 @@ const VariablesList = ({ onComputeAllVariableNames, ...props }: Props) => { props.variablesContainer, 'Variable', null, - position + position, + props.inheritedVariablesContainer ); _onChange(); setSelectedNodes([newName]); refocusNameField({ identifier: variable.ptr }); }, - [_onChange, props.variablesContainer, refocusNameField, selectedNodes] + [ + _onChange, + props.inheritedVariablesContainer, + props.variablesContainer, + refocusNameField, + selectedNodes, + ] + ); + + const onSelect = React.useCallback( + (shouldMultiSelect: boolean, nodeId: string) => { + setSelectedNodes(selectedNodes => { + const isAlreadySelected = selectedNodes.indexOf(nodeId) !== -1; + + if (shouldMultiSelect) { + if (isAlreadySelected) { + return selectedNodes.filter( + selectedNodeId => selectedNodeId !== nodeId + ); + } else { + return [...selectedNodes, nodeId]; + } + } else { + if (isAlreadySelected) { + return selectedNodes; + } else { + return [nodeId]; + } + } + }); + }, + [] ); const renderVariableAndChildrenRows = ( @@ -867,25 +1263,15 @@ const VariablesList = ({ onComputeAllVariableNames, ...props }: Props) => { index: number, |}, i18n: I18nType - ) => { + ): Array => { const isCollection = isCollectionVariable(variable); const type = variable.getType(); + const isExpanded = !variable.isFolded(); + const variablePointer = variable.ptr; const depth = parentNodeId ? parentNodeId.split(separator).length : 0; const isTopLevel = depth === 0; - const shouldWrap = - isNarrow || - (!containerWidth - ? false - : containerWidth <= 750 - ? depth >= 5 - : containerWidth <= 850 - ? depth >= 6 - : containerWidth <= 950 - ? depth >= 7 - : depth >= 8); - let parentType = null; let nodeId; if (!parentNodeId) { if (isInherited) { @@ -896,9 +1282,7 @@ const VariablesList = ({ onComputeAllVariableNames, ...props }: Props) => { } else { nodeId = `${parentNodeId}${separator}${name}`; } - if (!!parentVariable) { - parentType = parentVariable.getType(); - } + const parentType = parentVariable ? parentVariable.getType() : null; const isSelected = selectedNodes.includes(nodeId); const overwritesInheritedVariable = isTopLevel && @@ -920,536 +1304,322 @@ const VariablesList = ({ onComputeAllVariableNames, ...props }: Props) => { // - node is in the list of nodes matching search // - parent node is in the list of nodes matching search (to be able to edit direct children of searched structure) // - node is an ancestry of a node in the list of nodes matching search - return null; + return []; } } - const valueInputStyle = {}; - if (type === gd.Variable.String) { - // By default, Material-UI adds some padding on top and bottom of a multiline text field. - // Avoid this to prevent extra spaces that would make single line strings - // (for variable values) not aligned with the variable name - valueInputStyle.padding = 0; - } - if (isSelected) { - valueInputStyle.color = gdevelopTheme.listItem.selectedTextColor; - } + const valueAsString = isCollection + ? i18n._( + variable.getChildrenCount() === 0 + ? t`No children` + : variable.getChildrenCount() === 1 + ? t`1 child` + : t`${variable.getChildrenCount()} children` + ) + : type === gd.Variable.String + ? variable.getString() + : type === gd.Variable.Number + ? variable.getValue().toString() + : null; - return ( - { - draggedNodeId.current = nodeId; - return {}; - }} - canDrag={() => !isInherited} - canDrop={() => canDrop(nodeId)} - drop={() => { - dropNode(nodeId); - }} - > - {({ connectDragSource, connectDropTarget, isOver, canDrop }) => ( - 0 - ? 'isCollection' - : '' - } - classes={selectedTreeItemClasses} - label={connectDropTarget( -
- {isOver && } -
- {isInherited ? ( - - ) : ( - connectDragSource( - - - - ) - )} - - - {shouldWrap ? null : } - { - if (depth === 0 && element) { - topLevelVariableNameInputRefs.current[ - variable.ptr - ] = element; - } - }} - commitOnInputChange={!props.commitChangesOnBlur} - dataSource={isTopLevel ? undefinedVariableNames : []} - margin="none" - key="name" - disabled={ - isInherited || parentType === gd.Variable.Array - } - onClick={stopEventPropagation} - errorText={nameErrors[variable.ptr]} - onChange={newValue => { - onChangeName(nodeId, newValue, depth); - if (nameErrors[variable.ptr]) { - const newNameErrors = { ...nameErrors }; - delete newNameErrors[variable.ptr]; - setNameErrors(newNameErrors); - } - forceUpdate(); - }} - inputStyle={{ - color: isSelected - ? gdevelopTheme.listItem.selectedTextColor - : gdevelopTheme.listItem.textColor, - fontStyle: overwritesInheritedVariable - ? 'italic' - : undefined, - }} - value={name} - onBlur={event => { - onChangeName( - nodeId, - event.currentTarget.value, - depth - ); - if (nameErrors[variable.ptr]) { - const newNameErrors = { ...nameErrors }; - delete newNameErrors[variable.ptr]; - setNameErrors(newNameErrors); - } - forceUpdate(); - }} - id={`variable-${index}-name`} - /> - - -
- - - { - onChangeType(nodeId, newType); - forceUpdate(); - }} - isHighlighted={isSelected} - disabled={isInherited} - id={`variable-${index}-type`} - /> - - - {type === gd.Variable.Boolean ? ( - - - {variable.getBool() ? ( - True - ) : ( - False - )} - - {isInherited && !isTopLevel ? null : ( - <> - - { - onChangeValue( - nodeId, - !variable.getBool() ? 'true' : 'false' - ); - }} - tooltip={ - !variable.getBool() - ? t`Set to true` - : t`Set to false` - } - > - - - - )} - - ) : ( - { - if (depth === 0 && element) { - topLevelVariableValueInputRefs.current[ - variable.ptr - ] = element; - } - }} - type={ - type === gd.Variable.Number ? 'number' : 'text' - } - key="value" - onClick={stopEventPropagation} - multiline={type === gd.Variable.String} - inputStyle={valueInputStyle} - disabled={ - isCollection || (isInherited && !isTopLevel) - } - onChange={newValue => { - onChangeValue(nodeId, newValue); - forceUpdate(); - }} - value={ - isCollection - ? i18n._( - variable.getChildrenCount() === 0 - ? t`No children` - : variable.getChildrenCount() === 1 - ? t`1 child` - : t`${variable.getChildrenCount()} children` - ) - : type === gd.Variable.String - ? variable.getString() - : variable.getValue().toString() - } - commitOnBlur={props.commitChangesOnBlur} - id={`variable-${index}-text-value`} - /> - )} - - {isCollection && !isInherited ? ( - { - stopEventPropagation(event); - onAddChild(nodeId); - }} - > - - - ) : null} - {isCollection && isInherited && isTopLevel ? ( - { - stopEventPropagation(event); - editInheritedVariable(nodeId); - }} - > - - - ) : null} - {overwritesInheritedVariable && isTopLevel ? ( - { - stopEventPropagation(event); - deleteNode(nodeId); - }} - > - - - ) : null} - -
-
-
-
- )} - onLabelClick={preventEventDefaultEffect} - > - {!isCollection - ? null - : type === gd.Variable.Structure - ? variable - .getAllChildrenNames() - .toJSArray() - .map((childName, index) => { - const childVariable = variable.getChild(childName); - return renderVariableAndChildrenRows( - { - name: childName, - variable: childVariable, - parentNodeId: nodeId, - parentVariable: variable, - isInherited, - index, - }, - i18n - ); - }) - : mapFor(0, variable.getChildrenCount(), index => { - const childVariable = variable.getAtIndex(index); - return renderVariableAndChildrenRows( - { - name: index.toString(), - variable: childVariable, - parentNodeId: nodeId, - parentVariable: variable, - isInherited, - index, - }, - i18n - ); - })} -
- )} -
- ); - }; + const valueAsBool = + type === gd.Variable.Boolean ? variable.getBool() : null; - const onNodeToggle = (event, values) => { - // Inherited variables should not be modified - const instanceExpandedNodes = expandedNodes.filter( - node => !node.startsWith(inheritedPrefix) + const variableRow = ( + ); - const instanceNewExpandedNodes = values.filter( - node => !node.startsWith(inheritedPrefix) - ); - const foldedNodes = instanceExpandedNodes.filter( - node => !instanceNewExpandedNodes.includes(node) - ); - const unfoldedNodes = instanceNewExpandedNodes.filter( - node => !instanceExpandedNodes.includes(node) - ); - foldNodesVariables(props.variablesContainer, foldedNodes, true); - foldNodesVariables(props.variablesContainer, unfoldedNodes, false); - setExpandedNodes(values); - }; - const onChangeName = (nodeId: string, newName: ?string, depth: number) => { - const { variable, lineage, name } = getVariableContextFromNodeId( - nodeId, - props.variablesContainer - ); - if (name === null) return; - if (!!variable && !!newName) { - if (newName.startsWith(inheritedPrefix) || newName.includes(separator)) { - setNameErrors({ - ...nameErrors, - [variable.ptr]: ( - - Variables cannot have a name that includes {inheritedPrefix} or{' '} - {separator} - - ), - }); - return; - } - if (depth === 0 && !newName.match(/^[\p{L}_][\p{L}0-9_]*$/u)) { - setNameErrors({ - ...nameErrors, - [variable.ptr]: ( - - Top variable names can contain letters from any alphabet, digits - and "_" character and cannot start with a digit. - - ), - }); - return; - } - } - if (!newName) { - if (!!variable) { - setNameErrors({ - ...nameErrors, - [variable.ptr]: Variables cannot have empty names, - }); - } - return; - } - - if (newName === name) return; - - let hasBeenRenamed = false; - const parentVariable = getDirectParentVariable(lineage); - if (!parentVariable) { - hasBeenRenamed = props.variablesContainer.rename(name, newName); + if (type === gd.Variable.Structure) { + return [ + variableRow, + ...(isExpanded + ? variable + .getAllChildrenNames() + .toJSArray() + .map((childName, index) => { + const childVariable = variable.getChild(childName); + return renderVariableAndChildrenRows( + { + name: childName, + variable: childVariable, + parentNodeId: nodeId, + parentVariable: variable, + isInherited, + index, + }, + i18n + ); + }) + : []), + ]; + } else if (type === gd.Variable.Array) { + return [ + variableRow, + ...(isExpanded + ? mapFor(0, variable.getChildrenCount(), index => { + const childVariable = variable.getAtIndex(index); + return renderVariableAndChildrenRows( + { + name: index.toString(), + variable: childVariable, + parentNodeId: nodeId, + parentVariable: variable, + isInherited, + index, + }, + i18n + ); + }) + : []), + ]; } else { - hasBeenRenamed = parentVariable.renameChild(name, newName); - } - if (hasBeenRenamed) { - _onChange(); - updateExpandedAndSelectedNodesFollowingNameChange(nodeId, newName); - } else { - if (variable) - setNameErrors({ - ...nameErrors, - [variable.ptr]: ( - The variable name {newName} is already taken - ), - }); + return [variableRow]; } }; - const onChangeType = (nodeId: string, newType: string) => { - const { variable } = getVariableContextFromNodeId( - nodeId, - props.variablesContainer - ); - if (!variable) return; - variable.castTo(newType); - _onChange(); - }; + const onChangeName = React.useCallback( + (newName: string, additionalContext: any) => { + const parsedContext = JSON.parse(additionalContext); + const nodeId: string = parsedContext.nodeId; + const depth: number = parsedContext.number; - const onChangeValue = (nodeId: string, newValue: string) => { - const isInherited = nodeId.startsWith(inheritedPrefix); - let variable; - if (isInherited && props.inheritedVariablesContainer) { - // If user changes inherited variable, check if value is truly modified before - // duplicating the variable into the variables container - const { - variable: changedInheritedVariable, - name, - depth, - } = getVariableContextFromNodeId( - nodeId, - props.inheritedVariablesContainer - ); - if (!name || !changedInheritedVariable || depth > 0) return; - switch (changedInheritedVariable.getType()) { - case gd.Variable.String: - if (changedInheritedVariable.getString() === newValue) return; - break; - case gd.Variable.Number: - const newValueAsFloat = parseFloat(newValue); - if (newValueAsFloat === changedInheritedVariable.getValue()) return; - break; - case gd.Variable.Boolean: - const newBool = newValue === 'true'; - if (newBool === changedInheritedVariable.getBool()) return; - break; - default: - } - const newVariable = new gd.Variable(); - unserializeFromJSObject( - newVariable, - serializeToJSObject(changedInheritedVariable) - ); - variable = props.variablesContainer.insert(name, newVariable, 0); - const newSelectedNodes = [...selectedNodes]; - const isVariableSelected = newSelectedNodes.indexOf(nodeId) !== -1; - if (isVariableSelected) { - newSelectedNodes.splice(newSelectedNodes.indexOf(nodeId), 1, name); - setSelectedNodes(newSelectedNodes); - } else { - setSelectedNodes([...newSelectedNodes, name]); - } - const currentlyFocusedValueField = - topLevelVariableValueInputRefs.current[changedInheritedVariable.ptr]; - refocusValueField({ - identifier: variable.ptr, - caretPosition: currentlyFocusedValueField - ? currentlyFocusedValueField.getCaretPosition() - : null, - }); - newVariable.delete(); - } else { - const { variable: changedVariable } = getVariableContextFromNodeId( + const { variable, lineage, name } = getVariableContextFromNodeId( nodeId, props.variablesContainer ); - variable = changedVariable; - } - if (!variable) return; - switch (variable.getType()) { - case gd.Variable.String: - if (variable.getString() === newValue) return; - variable.setString(newValue); - break; - case gd.Variable.Number: - const newValueAsFloat = parseFloat(newValue); - if (newValueAsFloat === variable.getValue()) return; - variable.setValue(newValueAsFloat); - break; - case gd.Variable.Boolean: - const newBool = newValue === 'true'; - if (newBool === variable.getBool()) return; - variable.setBool(newBool); - break; - default: - console.error( - `Cannot set variable with type ${variable.getType()} - are you sure it's a primitive type?` + if (name === null || !variable || newName === name) return; + + const parentVariable = getDirectParentVariable(lineage); + + // In theory this cleaning is not necessary (a "safe name" is mandatory for root variables, + // but others should be able to have any name). In practice, + // this editor uses specific seperator that we forbid in names. + let cleanedName = newName.replace(inheritedPrefix, ''); + while (cleanedName.includes(separator)) { + cleanedName.replace(separator, ''); + } + + const safeAndUniqueNewName = newNameGenerator( + depth === 0 + ? // Root variables always use identifier safe names. + gd.Project.getSafeName(cleanedName) + : // Child variables of structures must "just" be not empty. + cleanedName || 'Unnamed', + tentativeNewName => { + if ( + (parentVariable && parentVariable.hasChild(tentativeNewName)) || + (!parentVariable && props.variablesContainer.has(tentativeNewName)) + ) { + return true; + } + + return false; + } + ); + + if (!parentVariable) { + props.variablesContainer.rename(name, safeAndUniqueNewName); + } else { + parentVariable.renameChild(name, safeAndUniqueNewName); + } + + _onChange(); + updateExpandedAndSelectedNodesFollowingNameChange( + nodeId, + safeAndUniqueNewName + ); + refocusNameField({ identifier: variable.ptr }); + }, + [ + _onChange, + props.variablesContainer, + updateExpandedAndSelectedNodesFollowingNameChange, + refocusNameField, + ] + ); + + const onChangeType = React.useCallback( + (newType: string, nodeId: string) => { + const { variable } = getVariableContextFromNodeId( + nodeId, + props.variablesContainer + ); + if (!variable) return; + variable.castTo(newType); + _onChange(); + forceUpdate(); + }, + [_onChange, forceUpdate, props.variablesContainer] + ); + + const onExpand = React.useCallback( + (expand: boolean, nodeId: string) => { + const isInherited = nodeId.startsWith(inheritedPrefix); + const { variable } = getVariableContextFromNodeId( + nodeId, + isInherited && props.inheritedVariablesContainer + ? props.inheritedVariablesContainer + : variablesContainer + ); + if (variable) { + variable.setFolded(!expand); + forceUpdate(); + } + }, + [props.inheritedVariablesContainer, variablesContainer, forceUpdate] + ); + + const onChangeValue = React.useCallback( + (newValue: string, nodeId: string) => { + const isInherited = nodeId.startsWith(inheritedPrefix); + let variable; + if (isInherited && props.inheritedVariablesContainer) { + // If user changes inherited variable, check if value is truly modified before + // duplicating the variable into the variables container + const { + variable: changedInheritedVariable, + name, + depth, + } = getVariableContextFromNodeId( + nodeId, + props.inheritedVariablesContainer ); - } - _onChange(); - forceUpdate(); - }; + if (!name || !changedInheritedVariable || depth > 0) return; + switch (changedInheritedVariable.getType()) { + case gd.Variable.String: + if (changedInheritedVariable.getString() === newValue) return; + break; + case gd.Variable.Number: + const newValueAsFloat = parseFloat(newValue); + if (newValueAsFloat === changedInheritedVariable.getValue()) return; + break; + case gd.Variable.Boolean: + const newBool = newValue === 'true'; + if (newBool === changedInheritedVariable.getBool()) return; + break; + default: + } + const newVariable = new gd.Variable(); + unserializeFromJSObject( + newVariable, + serializeToJSObject(changedInheritedVariable) + ); + variable = props.variablesContainer.insert(name, newVariable, 0); + + setSelectedNodes(selectedNodes => { + const newSelectedNodes = [...selectedNodes]; + const isVariableSelected = newSelectedNodes.indexOf(nodeId) !== -1; + if (isVariableSelected) { + newSelectedNodes.splice(newSelectedNodes.indexOf(nodeId), 1, name); + return newSelectedNodes; + } else { + return [...newSelectedNodes, name]; + } + }); + const currentlyFocusedValueField = + topLevelVariableValueInputRefs.current[changedInheritedVariable.ptr]; + refocusValueField({ + identifier: variable.ptr, + caretPosition: currentlyFocusedValueField + ? currentlyFocusedValueField.getCaretPosition() + : null, + }); + newVariable.delete(); + } else { + const { variable: changedVariable } = getVariableContextFromNodeId( + nodeId, + props.variablesContainer + ); + variable = changedVariable; + } + if (!variable) return; + switch (variable.getType()) { + case gd.Variable.String: + if (variable.getString() === newValue) return; + variable.setString(newValue); + break; + case gd.Variable.Number: + const newValueAsFloat = parseFloat(newValue); + if (newValueAsFloat === variable.getValue()) return; + variable.setValue(newValueAsFloat); + break; + case gd.Variable.Boolean: + const newBool = newValue === 'true'; + if (newBool === variable.getBool()) return; + variable.setBool(newBool); + break; + default: + console.error( + `Cannot set variable with type ${variable.getType()} - are you sure it's a primitive type?` + ); + } + _onChange(); + forceUpdate(); + }, + [ + _onChange, + forceUpdate, + props.inheritedVariablesContainer, + props.variablesContainer, + refocusValueField, + ] + ); const renderTree = (i18n: I18nType, isInherited: boolean = false) => { const variablesContainer = isInherited && props.inheritedVariablesContainer ? props.inheritedVariablesContainer : props.variablesContainer; - const containerVariablesTree = mapFor( - 0, - variablesContainer.count(), - index => { - const variable = variablesContainer.getAt(index); - const name = variablesContainer.getNameAt(index); - if (isInherited) { - if (props.variablesContainer.has(name)) { - return null; - } + const allRows = []; + mapFor(0, variablesContainer.count(), index => { + const variable = variablesContainer.getAt(index); + const name = variablesContainer.getNameAt(index); + if (isInherited) { + if (props.variablesContainer.has(name)) { + return null; } + } - return renderVariableAndChildrenRows( + allRows.push( + ...renderVariableAndChildrenRows( { name, variable, @@ -1457,10 +1627,10 @@ const VariablesList = ({ onComputeAllVariableNames, ...props }: Props) => { index, }, i18n - ); - } - ); - return containerVariablesTree; + ) + ); + }); + return allRows; }; const toolbar = ( @@ -1487,69 +1657,72 @@ const VariablesList = ({ onComputeAllVariableNames, ...props }: Props) => { ); return ( - - {({ i18n }) => ( - setSelectedNodes([])}> - { - setContainerWidth(contentRect.bounds.width); - }} - > - {({ contentRect, measureRef }) => ( -
- - {isNarrow ? null : toolbar} - {props.variablesContainer.count() === 0 && - (!props.inheritedVariablesContainer || - props.inheritedVariablesContainer.count() === 0) ? ( - - {props.emptyPlaceholderTitle && - props.emptyPlaceholderDescription ? ( - Add a variable} - helpPagePath={props.helpPagePath || undefined} - tutorialId="intermediate-advanced-variables" - onAction={onAdd} - actionButtonId="add-variable" - /> - ) : null} - - ) : ( - - } - defaultCollapseIcon={} - onNodeSelect={(event, values) => - setSelectedNodes(values) - } - onNodeToggle={onNodeToggle} - selected={selectedNodes} - expanded={expandedNodes} - > + + + {({ i18n }) => ( + setSelectedNodes([])}> + { + setContainerWidth(contentRect.bounds.width); + }} + > + {({ contentRect, measureRef }) => ( +
+ + {isNarrow ? null : toolbar} + {props.variablesContainer.count() === 0 && + (!props.inheritedVariablesContainer || + props.inheritedVariablesContainer.count() === 0) ? ( + + {props.emptyPlaceholderTitle && + props.emptyPlaceholderDescription ? ( + Add a variable} + helpPagePath={props.helpPagePath || undefined} + tutorialId="intermediate-advanced-variables" + onAction={onAdd} + actionButtonId="add-variable" + /> + ) : null} + + ) : ( + {props.inheritedVariablesContainer ? renderTree(i18n, true) : null} {renderTree(i18n)} - - - )} - {isNarrow ? toolbar : null} - -
- )} -
-
- )} -
+ {!!undefinedVariableNames.length && ( + + + + )} +
+ )} + {isNarrow ? toolbar : null} +
+
+ )} +
+
+ )} +
+ ); }; diff --git a/newIDE/app/src/VariablesList/useRefocusField.js b/newIDE/app/src/VariablesList/useRefocusField.js index 90ac57da90..0e89371afb 100644 --- a/newIDE/app/src/VariablesList/useRefocusField.js +++ b/newIDE/app/src/VariablesList/useRefocusField.js @@ -1,6 +1,11 @@ // @flow import React from 'react'; +type FocusOptions = {| + identifier: number, + caretPosition?: ?number, +|}; + const useRefocusField = (fieldRefs: {| current: {| [identifier: number]: {| @@ -8,23 +13,23 @@ const useRefocusField = (fieldRefs: {| |}, |}, |}) => { - const [fieldToFocus, setFieldToFocus] = React.useState(null); + const fieldToFocus = React.useRef(null); - React.useEffect( - () => { - if (fieldToFocus) { - const fieldRef = fieldRefs.current[fieldToFocus.identifier]; - if (fieldRef) { - fieldRef.focus({ caretPosition: fieldToFocus.caretPosition }); - setFieldToFocus(null); - } + const setFieldToFocus = React.useCallback((options: FocusOptions) => { + fieldToFocus.current = options; + }, []); + + React.useLayoutEffect(() => { + if (fieldToFocus.current) { + const fieldRef = fieldRefs.current[fieldToFocus.current.identifier]; + if (fieldRef) { + fieldRef.focus({ caretPosition: fieldToFocus.current.caretPosition }); } - }, - [fieldToFocus, fieldRefs] - ); + } + + fieldToFocus.current = null; + }); + return setFieldToFocus; }; diff --git a/newIDE/app/src/stories/componentStories/SimpleTextField.stories.js b/newIDE/app/src/stories/componentStories/SimpleTextField.stories.js new file mode 100644 index 0000000000..2bcd363bb0 --- /dev/null +++ b/newIDE/app/src/stories/componentStories/SimpleTextField.stories.js @@ -0,0 +1,50 @@ +// @flow +import * as React from 'react'; +import { action } from '@storybook/addon-actions'; + +import muiDecorator from '../ThemeDecorator'; + +import { SimpleTextField } from '../../UI/SimpleTextField'; +import { ColumnStackLayout } from '../../UI/Layout'; +import paperDecorator from '../PaperDecorator'; + +export default { + title: 'UI Building Blocks/SimpleTextField', + component: SimpleTextField, + decorators: [paperDecorator, muiDecorator], +}; + +export const Default = () => ( + + + + + + +); diff --git a/newIDE/app/src/stories/componentStories/VariablesList.stories.js b/newIDE/app/src/stories/componentStories/VariablesList.stories.js index 53614b9b37..453d82e50b 100644 --- a/newIDE/app/src/stories/componentStories/VariablesList.stories.js +++ b/newIDE/app/src/stories/componentStories/VariablesList.stories.js @@ -13,7 +13,6 @@ export const Default = () => ( (