Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React contenteditable cursor jumps to beginning

I am using the module react-simple-contenteditable to enable editing of a fill in the blank worksheet. The reason I must use a content editable element instead of an input element is because I want the text of the problem to wrap. For example, if a problem has one blank, it divides the text into three sections the part before the blank, the blank, and the part after. If I were to represent the outer two as separate divs (or input fields), then the text would not wrap like a paragraph. Instead, I must have a single contenteditable div that contains an input field for the blank and free text on either side.

The text is wrapping like I want it, but when I type text in the contenteditable field, the cursor jumps to the beginning. I don't understand why because I tried the example on the module's github site and it works perfectly, and although my implementation is a bit more complicated, it works essentially the same.

Here is my render function that uses <ContentEditable /> :

render() {
    const textPieces =
      <div className='new-form-text-pieces'>
        {
          this.props.problem.textPieces.map( (textPiece, idx) => {
            if (textPiece.blank) {
              return (
                  <div data-blank={true} className='blank' key={ textPiece.id } style={{display: 'inline'}}>
                    <input
                      placeholder="Answer blank"
                      className='new-form-answer-input'
                      value={ this.props.problem.textPieces[idx].text }
                      onChange={ (event) => this.props.handleTextPiecesInput(this.props.problemIdx, idx, event.target.value) }
                    />
                    <button className='modify-blank remove-blank' onClick={ (event) => this.props.removeBlank(this.props.problemIdx, idx) }>-</button>

                  </div>
              );
            } else {
              let text = this.props.problem.textPieces[idx].text;
              const placeholder = idx === 0 ? 'Problem text' : '...continue text';
              // text = text === '' ? placeholder : text;
              if (text === '') {
                text = <span style={{color:'gray'}}>{placeholder}</span>;
              } else {

              }
              return (
                this.props.isTextSplit ?
                  <TextPiece
                    key={ textPiece.id }
                    problemIdx={this.props.problemIdx}
                    textPieceIdx={idx}
                    dropBlank={this.props.dropBlank}
                    moveBlank={this.props.moveBlank}
                  >
                    <div style={{display: 'inline-block', }}>{text}</div>
                  </TextPiece>
                : text

              );
            }
          })
        }
      </div>;



    return (
       this.props.isTextSplit ? textPieces :
        <ContentEditable
          html={ReactDOMServer.renderToStaticMarkup(textPieces)}
          className="my-class"
          tagName="div"
          onChange={ (event, value) => this.props.handleProblemChange(event, this.props.problemIdx, value) }
          contentEditable='plaintext-only'
        />
    );

  }

Here is the onChange function:

handleProblemChange(event, problemIdx) {
    const problems = cloneDeep(this.state.problems);
    event.target.children[0].childNodes.forEach( (textPieceNode, idx) => {
      if (textPieceNode.constructor === Text) {
        problems[problemIdx].textPieces[idx].text = textPieceNode.wholeText;
      } else {
        problems[problemIdx].textPieces[idx].text = textPieceNode.childNodes[0].value;
      }
    });
    this.setState({ problems });
  }

And here is the state it refers to, just to make thing clear:

this.state = {
  problems: [
    {
      id: shortid.generate(),
      textPieces: [
        {
          text : "Three days was simply not a(n)",
          blank : false,
          id: shortid.generate(),
        },
        {
          text : "acceptable",
          blank : true,
          id: shortid.generate(),
        },
        {
          text : "amount of time to complete such a lot of work.",
          blank : false,
          id: shortid.generate(),
        }
      ]
    }

Thanks so much

like image 261
Steven Anderson Avatar asked Nov 13 '17 05:11

Steven Anderson


Video Answer


1 Answers

Long story short, there is no easy way to do this. I have tried this myself and spent days trying. Basically you have to save the cursor position and reposition it yourself after the update. All of this can be achieved with window.getSelection()

https://developer.mozilla.org/en-US/docs/Web/API/Window/getSelection

But it can get really tricky depending on how much your content has changed.

I ended up using draftJS instead. Which is an abstraction over contenteditable div by facebook themselves.

https://draftjs.org/docs/overview.html#content

A bit longer to pick up but you will be able to do a lot more

like image 110
klugjo Avatar answered Oct 26 '22 06:10

klugjo