简体   繁体   中英

Text selection in React Slate no longer marked when focus changes to React Select

I don't know if this is a common issue or a mistake on our part but maybe someone has an idea: We're building an HTML editor with react and slate. The User can select a textbox and then change attributes. This works fine for simple buttons. However, when I open a dropdown (react-select) to for example change font size, the selected text is no longer marked. Slate keeps the selection so the changes take effect but it's a bad UX like that.

Imho this should be a slate feature to keep the text marked but maybe it's something I need to apply myself.

some snippets, don't know if they help:

The Editor component initializes the font style plugins and takes care of serialization.

class Editor extends React.Component {
  constructor(props) {
    super(props);

    this.config = {
      ...mergePluginConfig(PLUGIN_CONFIG, props),
      getEditor: () => this.editor,
      getValue: () => this.state.value,
    };
    this.plugins = initializePlugins(this.config);
    this.htmlSerializer = new HtmlSerializer({
      rules: getSerializationRulesFromPlugins(this.plugins),
    });
    this.schema = getSchemaFromPlugins(this.plugins);
    this.state = {
      value: this.htmlSerializer.deserialize(props.value)
    };



ref = editor => {
    this.editor = editor;
  };


render() {
    return (
      <div>
        <Toolbar>
            <div className="control">
                {renderToolbarElementWithPlugins(this.plugins, 'font-size')}
            </div>
        <!--- more tools --->
      <SlateEditor
            autoFocus={true}
            spellCheck={true}
            placeholder={this.props.placeholder}
            ref={this.ref}
            value={this.state.value}
            onChange={this.onChange}
            onKeyDown={this.onKeyDown}
            plugins={this.plugins}
            schema={this.schema}
       />


onChange = ({ value }) => {
    const {startInline, endInline, document, selection, fragment} = value;
    // holds the correct information
    console.log(fragment.text);
    // ...
    this.setState({ value });
    this.props.onChange(this.htmlSerializer.serialize(value));
 };

This is the font-size plugin that is initialized with the others and will be displayed in the toolbar:

export default function initializeFontSizePlugin(options) {
  // this takes care of detecting the current value and applying selected change to the value. 
  // it does not change selection
  const plugin = createStyleBasedMarkPlugin(...); 
  const fontSizeOptions = options.fontSizeOptions || [];

  const handleFontSizeChange = ({value}) => {
    plugin.reapplyFontSize({value: rendererFontSize(value)});
  };

  return {
    ...plugin,

    renderToolbarElement() {
      const {isMixed, fontSize} = plugin.detectFontSize();

      return <Select
        isCreatable={true}
        name='font-size'
        value={isMixed ? undefined : displayFontSize(fontSize)}
        onChange={handleFontSizeChange}
        options={fontSizeOptions}
      />;
    }
  };
}

My current solution is to focus slate as soon as select opens and then tell select to be open but that feels hackish and has drawbacks (see below)

const handleFontSizeChange = ({value}) => {
    plugin.reapplyFontSize({value: rendererFontSize(value)});
    handleMenuClose();
  };

  let menuIsOpen = false;
  let firstOpen = false;

  const handleMenuOpen = (editor) => {
    firstOpen = true;
    if(!menuIsOpen) {
      setTimeout(() => {
        if (editor) {
          editor.focus();
        }
        menuIsOpen = true;
      }, 1);
    }
  }
  const handleMenuClose = (editor) => {
    if(!firstOpen) {
      setTimeout(() => {
        if(menuIsOpen) {
          menuIsOpen = false;
          if (editor) {
            editor.focus();
          }
        }
      }, 1);
    } else {
      firstOpen = false;
    }  
  }

<Select
    onMenuOpen={handleMenuOpen.bind(this)}
    onMenuClose={handleMenuClose.bind(this)}
    menuIsOpen={menuIsOpen}

I have to use the timeout to get outside the react lifecycle and I have to add an additional flag since losing focus on the select component will also close it. Like I said that has drawbacks: - a little flicker on the selected text during the focus switch - the dropdown box in select has the wrong coloring (not focused obviously) - switching to another dropdown (like alignment) won't close the other since that already has no focus:

Additional info: We have to work with slate and slate-react at version 0.47 as higher versions are not supported by slate-html-serializer which we need. Maybe this has already been solved in a higher version?

So, I have a somewhat working version but I'd much more prefer a solution where slate takes care of the selection "natively" without me having to handle focus. It should be possible I think without the selected text flickering and off colors.

Slate doesn't hold the selection when you focus out of the editor due to a dropdown being opened. Now with buttons it is different as it reapplies the selection

Since you now have to manually apply and get selections a way to do this is to store editor selection in state when you are trying to open the menu from select. When menu is open, reapply selection using Transforms.setSelection and get the fontSize which you can store in state again to show the focussed value in dropdown

Now once you apply the change, you need to apply the selection again

You can follow the concept used in this PR

const [selection, setSelection] = useState();
const [menuIsOpen, setMenuIsOpen] = useState(false);
const [fontSize, setFontSize] = useState(plugins.detectFontSize());
const handleFontSizeChange = ({value}) => {
    plugin.reapplyFontSize({value: rendererFontSize(value)});
    handleMenuClose();
  };
}
const handleMenuOpen = (editor) => {
    setSelection(editor.selection);
    setMenuIsOpen(true);
    Transforms.setSelection() // pass in the required params here
    setFontSize(plugins.detectFontSize());
}
const handleMenuClose = (editor) => {
    setMenuIsOpen(false);
    Transforms.setSelection() // pass in the required params here based on selection state
}

<Select
    onMenuOpen={handleMenuOpen.bind(this)}
    onMenuClose={handleMenuClose.bind(this)}
    menuIsOpen={menuIsOpen}
    value={fontSize}
    options={options}
/>

Also have a look at this github issue regarding focus and selection

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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