Home > other >  element.focus() doesn't work on rerendered contentEditable div element
element.focus() doesn't work on rerendered contentEditable div element

Time:01-16

Edit little-platform-3291w

import React, { useEffect, useState } from "react";

import "./styles.css";

const RichText = () => {
  const [value, setValue] = useState("");

  useEffect(() => {
    const div = document.getElementById("textarea");

    if (div) {
      setTimeout(() => {
        div.focus();
      }, 0);
    }
  });

  return (
    <div
      className="rich-text"
      onInput={(e) => {
        setValue(e.target.innerText);
      }}
      contentEditable
      id="textarea"
      dangerouslySetInnerHTML={{
        __html: value
      }}
    />
  );
};

export default RichText;

I want to implement rich text component, the idea , is that you can type text inside some field , you can style this text ( make it bold, italic, underlined , etc) . I want to same text value inside on value state variable , and then wrap it somehow inside of html tags <p>Hello <b> Andrew</b></p> , and show it in real time inside the same field , styled. For showing html tags inside of div , contentEditable field, I need to use dangerouslySetInnerHTML , and this is the main problem. That on each button press , I update value, component then rerendered , focus goes at the start of field , but I want it to be at the end in time you typing new text . I've tried to make it by ref => ref.current.focus() , it doesn't work, in code above you can see that I also try to make it by vanilla js with using timeout , it also doesn't work , autoFocus - can be used only on input, textarea, etc , div can be used with this property . I have save it to ref , but then I can't show wrapped html inside of div . Tried a lot of cases , but it is seemles . Any ideas how to make it ?

CodePudding user response:

The issue is when using useState hook with contentEditable and dangerouslySetInnerHTML to sync value. When you type something in the div it rerenders again and brings the cursor back to the start.

You can use instance variables in the function component (useRef to update value) to get rid of the issue.

And you should use innerHTML instead of innerText to get the HTML string saved

Try like below

import React, { useRef } from "react";

import "./styles.css";

const RichText = () => {
  const editableRef = useRef(null);
  const { current: value } = useRef(
    '<div style="color:green">my initial content</div>'
  );

  const setValue = (e) => {
    value.current = e.target.innerHTML;
  };

  const keepFocus = () => {
    const { current } = editableRef;
    if (current) {
      current.focus();
    }
  };

  return (
    <div
      className="rich-text"
      onInput={setValue}
      contentEditable
      id="textarea"
      ref={editableRef}
      onBlur={keepFocus}
      dangerouslySetInnerHTML={{
        __html: value
      }}
    />
  );
};

export default RichText;

Code Sandbox

CodePudding user response:

import React from "react";

import "./styles.css";

class TextInput extends React.Component {
  constructor(props) {
    super();
    this.state = { value: "" };
  }

  shouldComponentUpdate() {
    return true;
  }

  componentDidUpdate() {
    const el = document.getElementById("textarea");

    if (el) {
      console.log(el.selectionStart, el.selectionEnd);

      var range, selection;
      if (document.createRange) {
        //Firefox, Chrome, Opera, Safari, IE 9 
        range = document.createRange(); //Create a range (a range is a like the selection but invisible)
        range.selectNodeContents(el); //Select the entire contents of the element with the range
        range.collapse(false); //collapse the range to the end point. false means collapse to end rather than the start
        selection = window.getSelection(); //get the selection object (allows you to change selection)
        selection.removeAllRanges(); //remove any selections already made
        selection.addRange(range); //make the range you have just created the visible selection
      } else if (document.selection) {
        //IE 8 and lower
        range = document.body.createTextRange(); //Create a range (a range is a like the selection but invisible)
        range.moveToElementText(el); //Select the entire contents of the element with the range
        range.collapse(false); //collapse the range to the end point. false means collapse to end rather than the start
        range.select(); //Select the range (make it the visible selection
      }
    }
  }

  update(value) {
    this.setState({ value });
  }

  render() {
    console.log(this.state.value);
    return (
      <div
        className="rich-text"
        onInput={(e) => {
          this.update(e.target.innerText);
        }}
        contentEditable
        id="textarea"
        dangerouslySetInnerHTML={{
          __html: `<b>${this.state.value}</b>`
        }}
      />
    );
  }
}

export default TextInput;
  •  Tags:  
  • Related