Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Selection is null after editor loses focus #3412

Open
cboppert opened this issue Jan 8, 2020 · 37 comments
Open

Selection is null after editor loses focus #3412

cboppert opened this issue Jan 8, 2020 · 37 comments

Comments

@cboppert
Copy link

cboppert commented Jan 8, 2020

Do you want to request a feature or report a bug?

Bug

What's the current behavior?

Current behavior is that when you click into the toolbar or out of the document the editor's selection becomes null.

2020-01-08 13 25 51

https://codesandbox.io/s/fervent-bouman-ju71u?fontsize=14&hidenavigation=1&theme=dark

Tested on Firefox, Safari and Chrome although Firefox has different behavior. Firefox sometimes sets the cursor position to the end of the text in the editor depending on how you focus out of the editor.

Slate: 0.57.1
Browser: Chrome / Safari / Firefox
OS: Mac

What's the expected behavior?

Focus out shouldn't erase editor selection.

Changes to Example Site To Produce Behavior

In order to test this we forked the rich text example and made sure the toolbar style buttons did not disable on focus out. Then we used the ReactEditor.focus method in the MarkdownButton component's onMouseDown handler in the richtext.js file.

@Lalitj03
Copy link

Lalitj03 commented Jan 9, 2020

ezgif com-video-to-gif

Similar thing happened when I tried to use dialog box in link example instead of alert. While image upload as well the editor focus loses and the image gets appended at the last node instead of at the cursor location. Is there a way to control or change focus?

@Lalitj03
Copy link

Found a work around for this issue thanks to a kind developer on slack channel. Writing here in case anybody needs it. Store the selection value just before the editor loses focus. In my case it was when I clicked on input field, so I stored it just when dialog box opens. Similarly it can be applied to image upload, iframes or any action where editor loses focus.

const editorSelection = useRef(editor.selection);
useEffect(() => {
    if (openDialog) {
        editorSelection.current = editor.selection;
    }
}, [openDialog]);

@Morphexe
Copy link

Keeping on this, is there a way to actually keep it selected, as in highligting the text when losing focus ?

@Lalitj03
Copy link

Keeping on this, is there a way to actually keep it selected, as in highligting the text when losing focus ?

I don't know if this is a bug or expected behaviour. But here if you have value of selection with you (which is editorSelection.current ) you can pass it down to editor ( editor.selection = editorSelection.current ) before passing editor to Transforms.insertNode or anywhere else. If you want to show the selection, may be try Transforms.setSelection or Transforms.select.

@Morphexe
Copy link

Morphexe commented Jan 11, 2020

Keeping on this, is there a way to actually keep it selected, as in highligting the text when losing focus ?

I don't know if this is a bug or expected behaviour. But here if you have value of selection with you (which is editorSelection.current ) you can pass it down to editor ( editor.selection = editorSelection.current ) before passing editor to Transforms.insertNode or anywhere else. If you want to show the selection, may be try Transforms.setSelection or Transforms.select.

Yeah, I am currently trying to do Transforms.setSelection with the value I got from the onBlur event , cant seem to make it work. Setting the editor.selection does seem to work correctly.

I just need to find a way to highlight this and generalize the onBlur Event.

I have found that this is checking the onSelectChange event from the dom to unselect , so my next thing is going to try to disable the unselection there , I will report back on my findings.

So a quick work around, is to not allow the editor to be unselected - You can either run your own logic, or you can just "monkey patch" Transforms.deselect to be a empty function in the begginging of your app, this worked like a charm, and I can seem where this is actually being used on the internals of Slate apart from Focus/UnFocus. So so far this is my go to solution.
Better scenario would be to actually change Editable to not call deselect via a prop or something, and you woulc manually call deselect ( in case for multiple editors on the same page)

@ryanmitts
Copy link
Contributor

You can get around this issue by setting readOnly to true before opening the link input or focusing outside the editor, and then setting back to false when done.

However, this PR will need to be merged, since readOnly is broken right now.

https://github.com/ianstormtaylor/slate/pull/3388/files

@sunesimonsen
Copy link

I also had success with monkey patching Transforms.deselect while my dialog is open, but it feels wrong :-)

travis added a commit to travis/concept that referenced this issue Apr 23, 2020
In particular manage selection and focus after inserting links.

Once ianstormtaylor/slate#3412 is fixed we
should be able to remove the wonky setTimeout(..., 200) bit here.
@heyitsaamir
Copy link

I wrote up a simple HOC using what @Lalitj03 posted if anyone's interested:
https://gist.github.com/heyitsaamir/6089165fe6789eee170b46809cb61fc6

@robertying
Copy link

It took me hours debugging before I read this issue and found out setting editor.selection works but not calling editor.setSelection... The API name is confusing, yet the doc does not seem to mention this.

@DianaLease
Copy link
Contributor

DianaLease commented Jul 8, 2020

I am able to get around this issue by overriding Transforms.deselect with Transforms.deselect = () => {};
Similar to what I assume @Morphexe & @sunesimonsen are doing. However, is there any downside to doing this?

@hanford
Copy link
Contributor

hanford commented Jul 10, 2020

@DianaLease I ran into some issues where the editor could then have it's selection state become out of sync with the content which will end up throwing some hard to debug exceptions

@shvets-sergey
Copy link

Here is my solution to this problem:

  1. Put command buttons (like "link" on the gif in the first post) out of the <Slate> tag. This is required, so onBlur event will be fired before your button is clicked.
  2. Add "onBlur" handler to the <Editable> tag. In that handler save selection to some property on the editor. E.g. blurSelection.
  3. Slate sets selection to null after blur, so before executing a command on the editor, you need to set selection to the saved one. Do it with Transforms.select(editor, editor.blurSelection); (blurSelection is the name of a variable from step2.
  4. Run your regular command as if a selection was there. Everything will work exactly the same.
  5. Bonus: now, since we have selection we can use ReactEditor.focus(editor) to return focus, so users can just continue typing.

Works well for all the basic commands my editor has: lists, numbers, formatting, headers, etc.

@gsanta
Copy link

gsanta commented Jul 27, 2020

The solution of @Bearz works, but it still removes the selection visually, which is problematic for some use cases.

I want to change the font size for the selection via typing the font size into an input field, and for the user it is disturbing that the selection is no longer there visually, only "under the hood".

@tleunen
Copy link

tleunen commented Jul 27, 2020

still removes the selection visually

But that was also the case in the previous version?
It's not easy to keep the selection visually because it's the same document. You could have a custom "Selection" plugin which draws a background behind the selection to have a visual effect maybe?

@gsanta
Copy link

gsanta commented Jul 27, 2020

Sorry, I did not mean that it worked differently in the prev pervious, I just meant that it is still a problem for some use cases.
But I can understand that it's not easy, because that is how the browser works.

I think the safest solution is what you suggested, and draw a background explicitly when the focus is not there.

@shvets-sergey
Copy link

Step 5 returns focus for me. I can continue to type exactly from the same place. Make sure that your command doesn't modify selection under the hood. If you split nodes or change blocks that might be a case.

@vsakos
Copy link

vsakos commented Aug 26, 2020

You can make it work even when splitting nodes, here is a basic example that works for me:

saveSelection = () => {
  this.editor.savedSelection = this.editor.selection;
};

render () {
  return (
    <Slate editor={this.editor} value={this.state.value} onChange={this.handleChange}>
      <FontColorPicker editor={this.editor} defaultValue='rgba(0, 0, 0, 1)'/>
      <Editable renderLeaf={this.renderLeaf} onBlur={this.saveSelection}/>
    </Slate>
  );
}

In FontColorPicker:

handleChange = (color) => {
  if (this.props.editor.savedSelection) {
    Transforms.select(this.props.editor, this.props.editor.savedSelection);
  }

  Editor.addMark(this.props.editor, 'color', color);

  const sel = this.props.editor.selection;

  Transforms.deselect(this.props.editor);

  setTimeout(() => {
    Transforms.select(this.props.editor, sel);
    ReactEditor.focus(this.props.editor);
  }, 10);
};

For some reason just running ReactEditor.focus(this.props.editor); after Editor.addMark was putting the cursor back to the start even with a timeout, but deselect + select works.

@codeGun123
Copy link

codeGun123 commented Sep 18, 2020

Consider setting CSS for toolbars buttons outside the slate editing area: user-select: none;
<button onClick={handleClick} style={{userSelect:'none'}}>test</button>
Reference MDN: https://developer.mozilla.org/zh-CN/docs/Web/CSS/user-select
After setting, the selection will not be lost

@anhphung97
Copy link

@vsakos do you know why the cursor is put back to the start? I'm seeing this issue as well where inserting a new inline void element causes node splitting. I have to set my timeout pretty high (450ms) to make it work.

@onzag
Copy link

onzag commented Nov 15, 2020

I am getting the opposite problem, selection is not null when editor has lost focus.

@trevorr
Copy link

trevorr commented Nov 18, 2020

I needed to sort this out to make a Slate editor look and act like a textarea, which has a border div that can be clicked to focus the editor and needs to remember the selection. It's basically the same approach that @Bearz describes. Also important is the // @refresh reset comment, which prevents crashing on React Fast Refresh in newer versions of React.

import clsx from 'clsx';
import React from 'react';
import { createEditor, Editor, Node, Transforms } from 'slate';
import { withHistory } from 'slate-history';
import { Editable, ReactEditor, Slate, withReact } from 'slate-react';
import './EditorTextArea.scss';

export interface EditorTextAreaProps {
  value: Node[];
  onChange: (value: Node[]) => void;
}

// @refresh reset
const EditorTextArea: React.FC<EditorTextAreaProps> = ({ value, onChange }) => {
  const editor = React.useMemo(
    () => withHistory(withReact(createEditor())),
    []
  );

  const [focused, setFocused] = React.useState(false);
  const savedSelection = React.useRef(editor.selection);
  const onFocus = React.useCallback(() => {
    setFocused(true);
    if (!editor.selection) {
      Transforms.select(
        editor,
        savedSelection.current ?? Editor.end(editor, [])
      );
    }
  }, [editor]);
  const onBlur = React.useCallback(() => {
    setFocused(false);
    savedSelection.current = editor.selection;
  }, [editor]);

  const divRef = React.useRef<HTMLDivElement>(null);
  const focusEditor = React.useCallback(
    (e: React.MouseEvent) => {
      if (e.target === divRef.current) {
        ReactEditor.focus(editor);
        e.preventDefault();
      }
    },
    [editor]
  );

  return (
    <div
      ref={divRef}
      className={clsx('editor-textarea', { focused })}
      onMouseDown={focusEditor}>
      <Slate editor={editor} value={value} onChange={onChange}>
        <Editable onFocus={onFocus} onBlur={onBlur} />
      </Slate>
    </div>
  );
};

export default EditorTextArea;

@jansedlon
Copy link

Hey guys, what if you use onMouseDown instead of onClick? This doesn't reset focus for me.

@xitanggg
Copy link

Here is my solution to this problem:

  1. Put command buttons (like "link" on the gif in the first post) out of the <Slate> tag. This is required, so onBlur event will be fired before your button is clicked.
  2. Add "onBlur" handler to the <Editable> tag. In that handler save selection to some property on the editor. E.g. blurSelection.
  3. Slate sets selection to null after blur, so before executing a command on the editor, you need to set selection to the saved one. Do it with Transforms.select(editor, editor.blurSelection); (blurSelection is the name of a variable from step2.
  4. Run your regular command as if a selection was there. Everything will work exactly the same.
  5. Bonus: now, since we have selection we can use ReactEditor.focus(editor) to return focus, so users can just continue typing.

Works well for all the basic commands my editor has: lists, numbers, formatting, headers, etc.

Works like charm. Thanks for sharing!

Hey guys, what if you use onMouseDown instead of onClick? This doesn't reset focus for me.

onMouseDown doesn't lose highlight but lose focus. I am not sure why.

@cortopy
Copy link
Contributor

cortopy commented May 6, 2021

Like others in this thread, I was also caught by the unexpected behaviour of setSelect. I've created #4247 explaining the difference

basically,if you need to regain focus with the previous selection, use select instead of setSelect

@raitucarp
Copy link

Here is my solution to this problem:

  1. Put command buttons (like "link" on the gif in the first post) out of the <Slate> tag. This is required, so onBlur event will be fired before your button is clicked.
  2. Add "onBlur" handler to the <Editable> tag. In that handler save selection to some property on the editor. E.g. blurSelection.
  3. Slate sets selection to null after blur, so before executing a command on the editor, you need to set selection to the saved one. Do it with Transforms.select(editor, editor.blurSelection); (blurSelection is the name of a variable from step2.
  4. Run your regular command as if a selection was there. Everything will work exactly the same.
  5. Bonus: now, since we have selection we can use ReactEditor.focus(editor) to return focus, so users can just continue typing.

Works well for all the basic commands my editor has: lists, numbers, formatting, headers, etc.

For step 1. If I put the buttons outside , how can I use useSlate()?

@shvets-sergey
Copy link

@raitucarp I don't have a project set up to try it out, but most likely keeping the ref and passing it to a ButtonComponent as a prop should do a trick. Example code (may contain errors as I haven't written JSX in forever):

let  editorRef = React.useRef();
 return 
(<div>
     <ButtonComponent editor={editorRef} />
    <Slate>
        <Editable ref={editorRef}>
          ....
        </Editable>
    </Slate>)

@raitucarp
Copy link

@raitucarp I don't have a project set up to try it out, but most likely keeping the ref and passing it to a ButtonComponent as a prop should do a trick. Example code (may contain errors as I haven't written JSX in forever):

let  editorRef = React.useRef();
 return 
(<div>
     <ButtonComponent editor={editorRef} />
    <Slate>
        <Editable ref={editorRef}>
          ....
        </Editable>
    </Slate>)

Thanks, totally makes sense now.

My text editor is too way complicated, involves popover triggering on a button click, and causes my text editor to trigger get on blur but I want to keep my button change the icon when the block is active, sometimes I want to give up. But your post helps me to understand more.. Thank you...

@prakhar-ap
Copy link

Found a work around for this issue thanks to a kind developer on slack channel. Writing here in case anybody needs it. Store the selection value just before the editor loses focus. In my case it was when I clicked on input field, so I stored it just when dialog box opens. Similarly it can be applied to image upload, iframes or any action where editor loses focus.

const editorSelection = useRef(editor.selection);
useEffect(() => {
    if (openDialog) {
        editorSelection.current = editor.selection;
    }
}, [openDialog]);

Thank you so much!
I think you too feel the pain finding this solution.

@scott-schibli
Copy link

scott-schibli commented Sep 24, 2021

Has any one found a solution to keeping the previous 'selected' highlighted while pressing buttons outside of ? Finding this is a huge problem, as I would like to have user type in a font while the text is still selected -- 'highlighted'.

This helps with storing and then actually changing the selected but cannot keep highlighted:

const editorSelection = useRef(editor.selection);
useEffect(() => {
    if (openDialog) {
        editorSelection.current = editor.selection;
    }
}, [openDialog]);

And this causes the new 'selected' value to not be the point where the focus begins overriding the highlighted selected to the singular anchor where the focus started, (so I.e. I cannot click 'italics' button twice with the same selected text:
`

handleChange = (color) => {
  if (this.props.editor.savedSelection) {
    Transforms.select(this.props.editor, this.props.editor.savedSelection);
  }

  Editor.addMark(this.props.editor, 'color', color);

  const sel = this.props.editor.selection;

  Transforms.deselect(this.props.editor);

  setTimeout(() => {
    Transforms.select(this.props.editor, sel);
    ReactEditor.focus(this.props.editor);
  }, 10);
};

`

Anyone else still having this problem? Or have a good solution for similar use cases/issues? would be a life saver.

@xharris
Copy link

xharris commented Oct 1, 2021

Hey guys, what if you use onMouseDown instead of onClick? This doesn't reset focus for me.

I also added a preventDefault() at the end of the onMouseDown and it kept both selection and focus for me.

@AleksandrHovhannisyan
Copy link

AleksandrHovhannisyan commented Oct 11, 2021

One alternative solution: When focus leaves the editor, wrap the current selection in a fake custom selection node. Then, instead of relying on editor.selection being non-null, perform all operations and checks relative to that unique selection node. And instead of using editor.selection, you can now define a custom editor.getSelection method that returns either the path/location of that fake selection or falls back to editor.selection. Then remove the fake selection when the editor is re-focused.

We did this in our code base and it works well; it allows you to implement a floating toolbar and have inline inputs next to your editor. I'm hoping to write an article/tutorial soon to clarify how all of this works and what the code looks like. But that's the basic idea: Just like you have other inline nodes (hyperlinks and whatever else), you can also mock up a selection node that doesn't get serialized/deserialized and is only a run-time helper.

@dlqqq
Copy link

dlqqq commented Oct 19, 2021

I was able to keep the selected text highlighted when clicking on a toolbar button by using the following prop on Editable:

onBlur={(e) => e.preventDefault()}

That doesn't stop the editor from getting set to null when clicking in the toolbar div but outside of a button. Very confusing, have no idea why that happens.

@Dimanoid
Copy link

Dimanoid commented Jun 1, 2022

Still not fixed?

@aaronncfca
Copy link

aaronncfca commented Jun 1, 2022

I believe this has been fixed actually, though I'm not sure in which commit or release. See this comment on Slack, from when I asked related questions on the Slate Slack.

What may still be an issue for some users is that ReactEditor.focus() resets the selection to null, so you still end up with no selection after refocusing the editor if you aren't careful.

@davevilela
Copy link

One alternative solution: When focus leaves the editor, wrap the current selection in a fake custom selection node. Then, instead of relying on editor.selection being non-null, perform all operations and checks relative to that unique selection node. And instead of using editor.selection, you can now define a custom editor.getSelection method that returns either the path/location of that fake selection or falls back to editor.selection. Then remove the fake selection when the editor is re-focused.

We did this in our code base and it works well; it allows you to implement a floating toolbar and have inline inputs next to your editor. I'm hoping to write an article/tutorial soon to clarify how all of this works and what the code looks like. But that's the basic idea: Just like you have other inline nodes (hyperlinks and whatever else), you can also mock up a selection node that doesn't get serialized/deserialized and is only a run-time helper.

@AleksandrHovhannisyan
wow! that was a brilliant insight!
I'm trying to implement a floating toolbar so the user can input a URL and create a link element.
The idea to create a "fake selection" leaf/element is fantastic! Can you share any piece of code ? That would help a lot ! Thanks in advance!

@AleksandrHovhannisyan
Copy link

@davevilela Sorry for the tease, I know that's not a ton of info to go off of. I'm still hoping I can get around to writing a blog post about it one of these days.

I know Notion does something very similar in its editor; when you highlight some text and try to insert a hyperlink, they insert a fake blue wrapper span styled to look like a regular selection, and then they delete it afterwards.

@davevilela
Copy link

davevilela commented Jun 6, 2022

@AleksandrHovhannisyan hey, I put some time into researching and going through other people's code and came up with this prototype:

type EditorWithPrevSelection<V extends Value = Value> = PlateEditor<V> & {
  prevSelection: BaseRange | undefined;
};

export const createFakeSelectionPlugin = createPluginFactory({
  key: FAKE_SELECTION_MARK,
  isLeaf: true,
  handlers: {
    onBlur: (editor: EditorWithPrevSelection<Value>) => (event) => {
      event.preventDefault();
      const currentSelection = editor.selection;
      const hasSelection = !!currentSelection && isSelectionExpanded(editor);

      if (hasSelection) {
        setMarks(editor, { [FAKE_SELECTION_MARK]: true });
        editor.prevSelection = editor.selection;
      }
    },
    onFocus: (editor: EditorWithPrevSelection<Value>) => (event) => {
      event.preventDefault();
      const { prevSelection } = editor;
      if (prevSelection) {
        Transforms.select(editor as any, prevSelection);
        editor.prevSelection = undefined;

        removeMark(editor, {
          key: FAKE_SELECTION_MARK,
          shouldChange: false,
          mode: 'all',
        });
      }
    },
  },
  component: withProps(StyledLeaf, {
    as: FAKE_SELECTION_MARK,
    styles: {
      root: {
        background: 'var(--chakra-colors-textSelect)',
      },
    },
  }),
});

hope this helps anyone that comes across this issue!

edit: i'm using plate for my plugin system

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests