繁体   English   中英

如何使用 Material UI 创建代码编辑器,如自动完成下拉菜单?

[英]How to create code editor like Autocomplete dropdown with Material UI?

我有一个相当具体的用例,我正在考虑如何在我正在开发的应用程序中实现。 该组件是类似编辑器的文本区域,应该填充 Material UI Chip 组件(类似于自动完成文本框中的标签),这些组件会生成某种表达式。 当用户开始在此文本区域内输入时,应该会弹出一个自动完成下拉菜单,向用户显示可能的选项。

我希望将此下拉列表放置在 IDE 中的此文本区域(类似于智能感知)内。

我正在尝试通过结合使用 Autocomplete 和某种自定义 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: {
      '& .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: {
      '& .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. 我怎样才能 position 这个下拉列表(波普尔)波纹管文本 cursor 在 textarea 内?
  2. 该组件还应该能够格式化创建的表达式(再次类似于代码编辑器格式化程序)。 你认为这是这个用例的正确方法,还是我应该使用其他一些库和/或 UI 组件?

谢谢。

警告:这将深入到 realm 的意见中......我最终选择了 Downshift 进行自定义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",

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM