Как создать редактор кода, подобный выпадающему списку автозаполнения, с помощью Material UI?

#javascript #reactjs #material-ui

#javascript #reactjs #материал-пользовательский интерфейс

Вопрос:

У меня есть довольно конкретный вариант использования, который я думаю о том, как реализовать в приложении, над которым я работаю. Компонент представляет собой текстовую область, похожую на редактор, которая должна быть заполнена компонентами чипа Material UI (что-то вроде тегов в текстовом поле автозаполнения), которые генерируют какое-то выражение. Когда пользователь начинает вводить текст внутри этой текстовой области, должен появиться выпадающий список автозаполнения, показывающий возможные варианты для пользователя.

Я хотел бы, чтобы этот выпадающий список располагался внутри этой текстовой области (аналогично intellisense) в IDEs.

Я пытаюсь реализовать этот компонент, используя комбинацию автозаполнения и какого-то пользовательского компонента Popper. Код выглядит примерно так (он все еще находится на стадии черновика):

 import { createStyles, makeStyles, Theme } from '@material-ui/core/styles';
import TextField from "@material-ui/core/TextField";
import Autocomplete from "@material-ui/lab/Autocomplete";
import Chip from '@material-ui/core/Chip';
import { Popper } from "@material-ui/core";

const targetingOptions = [
  { label: "(", type: "operator" },
  { label: ")", type: "operator" },
  { label: "OR", type: "operator" },
  { label: "AND", type: "operator" },
  { label: "Test Option 1", type: "option" },
  { label: "Test Option 2", type: "option" },
];




const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    root: {
      'amp; .MuiAutocomplete-inputRoot': {
        alignItems: 'start'
      }
    },
  }),
);


export default () => {
  const classes = useStyles();
  const [value, setValue] = React.useState<string[] | null>([]);

  const CustomPopper = function (props) {
    return <Popper {...props} style={{ width: 250, position: 'relative' }} />;
  };
  

  return (
    <div>
        <Autocomplete
        className={classes.root}
        multiple
        id="tags-filled"
        options={targetingOptions.map((option) => option.label)}
        freeSolo
        disableClearable
        PopperComponent={CustomPopper}
        renderTags={(value: string[], getTagProps) =>
          value.map((option: string, index: number) => (
            <Chip variant="outlined" label={option} {...getTagProps({ index })} />
          ))
        }
        renderInput={(params) => (
          <TextField {...params} variant="outlined" multiline={true} rows={20} />
        )}
      />
    </div>
  );
};

const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    root: {
      'amp; .MuiAutocomplete-inputRoot': {
        alignItems: 'start'
      }
    },
  }),
);

export default () => {
  const classes = useStyles();

  const CustomPopper = function (props) {
    return <Popper {...props} style={{ width: 250, position: 'relative' }} />;
  };
  

  return (
    <div>
        <Autocomplete
        className={classes.root}
        multiple
        id="tags-filled"
        options={targetingOptions.map((option) => option.label)}
        freeSolo
        disableClearable
        PopperComponent={CustomPopper}
        renderTags={(value: string[], getTagProps) =>
          value.map((option: string, index: number) => (
            <Chip variant="outlined" label={option} {...getTagProps({ index })} />
          ))
        }
        renderInput={(params) => (
          <TextField {...params} variant="outlined" multiline={true} rows={20} />
        )}
      />
    </div>
  );
};

 
  1. Как я могу расположить этот выпадающий список (Popper) под текстовым курсором внутри текстовой области?
  2. Этот компонент также должен иметь возможность форматировать созданное выражение (опять же, аналогично редактору форматирования кода). Как вы думаете, это правильный подход для данного варианта использования, или мне следует использовать какую-то другую библиотеку и / или компоненты пользовательского интерфейса?

Спасибо.

Ответ №1:

Предостережение: это будет глубоко в сфере мнений… В итоге я выбрал понижающую передачу для настройки npm install downshift

Этот код немного грязный (из моей ветки разработчиков), но он содержит настраиваемый выпадающий список, который вы можете редактировать

     import React from 'react'
import {render} from 'react-dom'
import Downshift from 'downshift'

import {
  MenuItem,
  Paper,
  TextField,
} from '@material-ui/core'

import {
  withStyles
} from '@material-ui/core/styles'

const items = [
  'apple',
  'pear',
  'orange',
  'grape',
  'banana',
]

class DownshiftWrapper extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      value: props.value || '',
      backup: props.value || '',
      onChange: v => {console.log('changed', v)}
    }
  }

  _renderMenuItem(args) {
    const { key, index, itemProps, current, highlightedIndex, selectedItem, ...rest } = args
    const isSelected = key == current
    return (
      <MenuItem
        {...rest}
        key = { key }
        selected = { isSelected }
        component='div'
        style={{
          fontWeight: isSelected ? 500 : 400,
          padding: '2px 16px 2px 16px',
          borderBottom: '1px solid rgba(128,128,128,0.5)',
        }}
      >
        { key }
      </MenuItem>
    )
  }
  render() {
    const { classes, style } = this.props

    const _override = (incoming) => {
      console.log('override:', incoming)
      this.setState({
        ...this.state,
        value: incoming
      })

      if(this.props.onChange) {
        this.props.onChange(incoming)
      } else {
        console.log(`Downshift::onChange the onchange handler is missing. New value:${incoming}`)
      }      
    }

    return (
      <Downshift
        ref = { x => this.downshift = x}
        onSelect = { (selected) => {
          if(selected) {
            console.log('::onSelect', selected) 
            _override(selected)
          }
        } }
        onInputValueChange= { (inputValue, stateAndHelpers) => {
          console.log('::onInputValueChange', {
            ...stateAndHelpers,
            _val: inputValue,
          })
        } }
        // onStateChange={( state ) => {
        //   //return input.onChange(inputValue);
        //   let value = state.inputValue

        //   this.state.onChange(state.inputValue)
        //   console.log('old:state', state)
        //   console.log('value:', value)

        //   _override( state.inputValue )
        // }}
        onChange={ selection => { console.log(selection) }}
        itemToString={ item => {
          return item || ''
        } }
        //selectedItem={this.props.input.value}
      >
      {({
        getInputProps,
        getItemProps,
        getLabelProps,
        getMenuProps,
        isOpen,
        inputValue,
        highlightedIndex,
        selectedItem,
      }) => {
        const inputProps = getInputProps()
        let value = inputProps.value

        //FIXME add filtering options
        let filtered = this.props.items || items//.filter(item => !inputValue || item.includes(inputValue))

        return (
          <div className={classes.container}>
            <TextField 
              { ...inputProps } 
              style={
                style
              }
              label={this.props.label}
              placeholder={this.props.placeholder}
              
              value = { 
                this.state.value 
              }
              onFocus = { e => {
                this.downshift.openMenu()
                e.target.select()
              }}
              onBlur={ e => { 
                console.log(inputValue) 
                e.preventDefault()
                this.downshift.closeMenu()
              } }  
              onChange={ e => {
                inputProps.onChange(e)//pass to the logic
                _override(e.target.value)
              }}
              onKeyDown= { (e) => {
                const key = e.which || e.keyCode
                if(key == 27){
                  e.preventDefault()
                  e.target.blur()
                   //reset to default
                  _override(this.state.backup || '')
                } else if (key == 13){
                  e.preventDefault()
                  e.target.blur()
                  _override(e.target.value)
                }
              }}
            />
            {isOpen
              ? (
                <Paper 
                  className={classes.paper}
                  // style={{
                  //   backgroundColor: 'white',
                  // }}
                square>
                  { filtered
                      .map( (item, index) => {
                        const _props = {
                          ...getItemProps({ item: item }),
                          index: index,
                          key: item,
                          item: item,
                          current: this.state.value,
                        } 
                        return this._renderMenuItem(_props)  
                      } )
                  }
                </Paper>
              )
              : null}

            {/* <div style={{color: 'red'}}>{this.state.value || 'null'}</div> */}
          </div>
        )
        
      } }
      </Downshift>
    )
  }
}

class Integrated extends React.Component {

}

//Material UI Examples -> https://material-ui.com/demos/autocomplete/
const styles = theme => ({
  root: {
    flexGrow: 1,
    height: 250,
  },
  container: {
    flexGrow: 1,
    position: 'relative',
  },
  paper: {
    position: 'absolute',
    zIndex: 1,
    marginTop: theme.spacing.unit,
    left: 0,
    right: 0,
  },
  chip: {
    margin: `${theme.spacing.unit / 2}px ${theme.spacing.unit / 4}px`,
  },
  inputRoot: {
    flexWrap: 'wrap',
  },
})

export default withStyles(styles)(DownshiftWrapper)
 

используется (EditableSelect — это имя экспорта в моем проекте):

 return (
        <EditableSelect 
          //onFocus={e => this.onFocus(e) }
          //multiLine={true}
          //onKeyDown={ e=> this.keyHandler(e) }
          items={ options }
          value={ cue.spots[index][field] }
          hintText={T.get('spot'   field   'Hint')}
          placeholder={ T.get('spot'   field   'Hint') }
          ref={x => this[id] = x }
          style={{width: '90%' }}
          onChange={ val => this.updateSpotExplicit(val, index, field) } 
        />
      )
 

введите описание изображения здесь

Я не уверен, что вам нужно, но я обнаружил Autocomplete проблему, когда попытался ее настроить. В этом коде необходимо выполнить кучу очисток, но я могу убедиться, что он работает в нашей производственной среде. Это было лучшее решение, которое я нашел ~ 18 месяцев назад, и мы все еще используем его.

 "@material-ui/core": "^4.11.2",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.57",
"@material-ui/styles": "^4.11.2",
"downshift": "^2.0.10",
 

Комментарии:

1. Спасибо за хлопоты, я думаю, я также удалю автозаполнение и вручную создам компонент. По сути, это нетрадиционное требование к бизнесу, а не то, что я изобрел сам, и я просто хотел прояснить свои сомнения по этому поводу и посмотреть, были ли у кого-нибудь подобные проблемы в прошлом. Мне также нужно поддерживать настройку макета микросхем внутри текстового поля, и я не думаю, что это будет возможно с компонентом пользовательского интерфейса Material.