4

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.

AmerllicA
  • 29,059
  • 15
  • 130
  • 154
Pete
  • 10,720
  • 25
  • 94
  • 139
  • Can you create a sandbox that re-produce the issue, that would help addressing your issue. – ROOT May 15 '20 at 05:05
  • @Pete, even the official demo of slateEditor doesn't handle the selection on dropdown. I added in a codeSandbox: https://codesandbox.io/s/serene-sunset-ilc44?file=/src/App.js – Shubham Khatri May 15 '20 at 05:34
  • btw, which slate-editor library do you use – Shubham Khatri May 15 '20 at 05:54
  • Oh, it's not even officially supported? But how is any user supposed to have a good experience, when the selected text to which they want to apply a change just isnt highlighted any more?! We're using "slate": "0.47.4" and "slate-react": "0.22.4", – Pete May 15 '20 at 06:55
  • Hmm, that' sounds like it's been a headache. You have my sympathies. I'd consider disabling standard text selection (or overriding the css to hide what normally happens) and then putting in your own text select function. That would let you bypass the hoop jumping and give you control of the highlighted text style in a more granular way. This way you could maintain what text is "styled as highlighted" in react state. Something like this perhaps but modified toward react? : https://stackoverflow.com/a/987376/5544119 – SethGoodluck May 18 '20 at 06:22
  • That was also my second alternative but then I thought handling selection on my own will cause a lot more headaches – Pete May 19 '20 at 09:33

1 Answers1

4

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

Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
  • Thanks for your input. Like I said, I've been trying along those lines already and have made it work. Just the minor drawback of the selection flickering during menu open but that's still better than not seeing the selection. – Pete May 19 '20 at 09:31
  • @Pete Well editors, no matter which one you choose has some or the other missing scenarios. Also internally they also handle things in the same manner by preserving and applying selection on toolbar button. The case for toolbar buttons is a little straightforward for them though. If at all you are exploring editors still, you could checkout CKEditor too – Shubham Khatri May 19 '20 at 09:42