facebookarchive / draft-js

A React framework for building text editors.
https://draftjs.org/
MIT License
22.56k stars 2.63k forks source link

draft.js manage focus material ui select component #2177

Open Ravindersingh1526 opened 5 years ago

Ravindersingh1526 commented 5 years ago

Bug - I am working on draft.js editor with floating toolBar. I have design toolBar component with material UI components like Select, buttons.

What is the current behavior? case 1- If I select a text from the editor Then I will click on the toolBar select component the selection will be lost of the editor case 2- If I select a text from the editor Then I will click on the toolBar button component the selection will be lost(But the solution of this case is to used onmouseDown event and call e.preventDefault()) case 3- If I select a text from the editor Then I will click on the toolBar button component this button component will open dialog that has textBox for add link depend on user input to selected text in that case focus is lost.

Codesandbox simple code example https://codesandbox.io/s/texteditor-izj8l

What is the expected behavior?

I want the editor will not lose focus any material UI component click or HTML select element options click or select

jkhaui commented 5 years ago

I added some code to your TextEditor file to show how I would normally keep focus retained. However, it doesn't seem to work because the Material-UI selectbox isn't losing focus even after you click something like "bold". So I don't know if it helps, but here's some code which might work if you can fix the issue of the SelectBox not losing focus automatically.

import * as React from "react";
import Button from "@material-ui/core/Button";
import { Editor, EditorState, RichUtils, SelectionState } from "draft-js";
import Dialog from "@material-ui/core/Dialog";
import {
  DialogTitle,
  DialogContent,
  DialogContentText,
  DialogActions,
  Select,
  MenuItem
} from "@material-ui/core";
const TextEditor: React.FC = () => {
  const [state, setState] = React.useState(EditorState.createEmpty());
  const [selectedStyle, setSelectedStyle] = React.useState("NONE");
  const onChange = (editorState: EditorState) => {
    setState(editorState);
  };

  const myRef = React.useRef<Editor>(null);

  const handleSelectChange = (
    event: React.ChangeEvent<{
      name?: string;
      value: unknown;
    }>
  ) => {
    // We capture the current selection state, which includes the 
    // current focused block and the offset of the caret within this
    // block.
    const selectionState = state.getSelection()
    const currentBlockOffset = selectionState.getFocusOffset()
    const currentBlockKey = selectionState.getFocusKey()

    console.log(event.target.value);
    let newState = RichUtils.toggleInlineStyle(state, event.target
      .value as string);
    setState(newState);
    setSelectedStyle(event.target.value as string);

    const newSelectionState = state.getSelection().merge({
      anchorKey: currentBlockKey,
      anchorOffset: currentBlockOffset,
      focusOffset: currentBlockOffset,
      focusKey: currentBlockKey,
    })
    // @ts-ignore
    setState(EditorState.forceSelection(state, newSelectionState))
  };

  return (
    <div>
      <div>
        <Select value={selectedStyle} onChange={handleSelectChange}>
          <MenuItem value="NONE">--select style --</MenuItem>
          <MenuItem value="BOLD">bold</MenuItem>
          <MenuItem value="ITALIC">italic</MenuItem>
        </Select>
      </div>
      <div>
        <br />
        <br />
        <br />
        <div style={{ border: "1px solid black" }}>
          <Editor editorState={state} onChange={onChange} />
        </div>
      </div>
    </div>
  );
};

export default TextEditor;
Ravindersingh1526 commented 5 years ago

@jkhaui still not working

jkhaui commented 5 years ago

@jkhaui still not working

Ok I found another solution, it's working now. I'm not sure if my changes to your codesandbox persist, so I'll paste the code I modified here:

import * as React from "react";
import Button from "@material-ui/core/Button";
import { Editor, EditorState, RichUtils, SelectionState } from "draft-js";
import Dialog from "@material-ui/core/Dialog";
import {
  DialogTitle,
  DialogContent,
  DialogContentText,
  DialogActions,
  Select,
  MenuItem
} from "@material-ui/core";
const TextEditor: React.FC = () => {
  const [state, setState] = React.useState(EditorState.createEmpty());
  const [selectedStyle, setSelectedStyle] = React.useState("NONE");
  const onChange = (editorState: EditorState) => {
    setState(editorState);
  };

  const myRef = React.useRef<Editor>(null);

  const handleSelectChange = (
    event: React.ChangeEvent<{
      name?: string;
      value: unknown;
    }>
  ) => {
    console.log(event.target.value);
    let newState = RichUtils.toggleInlineStyle(state, event.target
      .value as string);
    setState(newState);
    setSelectedStyle(event.target.value as string);
};

  const handleExited = () => {
    myRef.current.focus();
  };

  return (
    <div>
      <div>
        <Select 
        value={selectedStyle} 
        onChange={handleSelectChange}
        // Pass custom handler to `MenuProps`
        MenuProps={{
          onExited: handleExited,
        }}
        >
          <MenuItem value="NONE">--select style --</MenuItem>
          <MenuItem value="BOLD">bold</MenuItem>
          <MenuItem value="ITALIC">italic</MenuItem>
        </Select>
      </div>
      <div>
        <br />
        <br />
        <br />
        <div style={{ border: "1px solid black" }}>
          <Editor 
            ref={myRef}
            editorState={state} 
            onChange={onChange} 
          />
        </div>
      </div>
    </div>
  );
};

export default TextEditor;
Ravindersingh1526 commented 5 years ago

@jkhaui Thankyou so much for reply. But this isn't still working. Selected text retained focus after style change. I except when we select a text click outside editor the selection will not lose. In your case when we select text and click on select element selection blue color is lost and when we change style after selection is applied we will directly achieve this by using force selection method.

jkhaui commented 4 years ago

@jkhaui Thankyou so much for reply. But this isn't still working. Selected text retained focus after style change. I except when we select a text click outside editor the selection will not lose. In your case when we select text and click on select element selection blue color is lost and when we change style after selection is applied we will directly achieve this by using force selection method.

No worries - unfortunately, the Draft.js community doesn't seem too active in helping people with issues (with some exceptions) so I'll try help if I can

Ok so I'm not entirely sure of your goals, but this is how I understand them: 1) When some text is selected (i.e. selection state is not collapsed), and the user then selects an inline style (e.g. Bold), you expect the inline style to immediately be applied to the selected text, and the selected text should also remain selected. If this is correct, then it looks like we have this part solved. 2) When the cursor is placed at a specific point, such as the end of the text (i.e. selection state is collapsed), and the user selects an inline style, you expect focus to be retained at the same location. If this is your second goal, then the second code snippet I posted fixes this aspect 3) Continuing on from point 2), you also expect the selected inline style to be applied at the point where the cursor is focused. Having just tested it out, it seems that this is the one aspect that is not currently working.

Am I understanding correctly?

Another question is: how would the select box work for applying multiple inline styles? E.g. If a user wants to apply both bold and italic styles to some text, should they select bold from the dropdown menu first? And then italic? I mean, the UX/UI is your choice, but you wouldn't have this issue if you have Bold/Italic buttons which can be toggled on and off independently of one another

kei-0226 commented 4 years ago

Thank you for posting this issue. I have the same problem.

formatlos commented 4 years ago

After digging through the material-ui sources I believe there's no simple way to keep the focus on the input at least not with the <Select /> component. You could use the <Menu /> component to build your custom select element though, just make sure to disable all auto-focusing behaviour.

<Menu 
  disableAutoFocus={true} 
  disableEnforceFocus={true}  
  disableRestoreFocus={true}
  disableAutoFocusItem={true}
  autoFocus={false}
  ...
/>
CruizK commented 3 years ago

In case anyone stumbles across this like I did and did not find a solution, put a setTimeout for about 50ms after changing the state in the select's onChange function and then focusing. I found it while looking through the react-rte repo. This is what I'm using

<Select value={selectedStyle} style={{minWidth: 100, textAlign:'center'}}  
    onChange={e => {
      onChange(RichUtils.toggleBlockType(editorState, String(e.target.value)))
      console.log(e.target.value);
      setStyle(String(e.target.value))
      setTimeout(() => {
        focus()
      }, 50)

    }}>
      {options.map(item => <MenuItem value={item.style}>{item.label}</MenuItem>)}
    </Select>
devnax commented 2 years ago

jus update the state this way. for the dropdown, you can update the current editor state when you open the dropdown. like

setEditorState(EditorState.forceSelection(editorState, editorState.getSelection()))

const newEditorState =  RichUtils.toggleInlineStyle(editorState, 'BOLD')
setEditorState(EditorState.forceSelection(newEditorState, editorState.getSelection()))


function DraftJsEditor() {

    const [editorState, setEditorState] = useState(() => EditorState.createEmpty());
    const selection = editorState.getSelection()

    useEffect(() => {
        const isCollapsed = editorState.getSelection().isCollapsed()
        const isFocus = editorState.getSelection().getHasFocus()
        if (!isCollapsed && isFocus){
           setEditorState(EditorState.forceSelection(editorState, editorState.getSelection()))
       }
    }, [selection.isCollapsed(), selection.getHasFocus()])

    return (
        <div>
            <button onClick={() => {
                const newEditorState =  RichUtils.toggleInlineStyle(editorState, 'BOLD')
                setEditorState(EditorState.forceSelection(newEditorState, editorState.getSelection()))
            }}> B</button>

            <Editor
                editorState={editorState}
                onChange={setEditorState}
            />

        </div>
    )

}