react-quill icon indicating copy to clipboard operation
react-quill copied to clipboard

Inserting Text into the editor with a custom button results in error on keystroke

Open the-simian opened this issue 7 years ago • 18 comments

Quill version

  • [x] master

Given a toolbar witha custom handler in the modules function

      toolbar: {
        container: [
          ['custom']
        ],
        handlers: {
          "custom": function () {
        
              this.quill.insertText(this.quill.getSelection().index, 'Hello', 'link', 'https://world.com');
              console.log(this.quill.getText())
          }
        }
      }

When you click the button it does correctly insert the text, but when you click in the editor you get: The given range isn't in document. Every time you try to type normally. Removing the item in the toolbar makes this stop

the-simian avatar Jul 21 '17 07:07 the-simian

Thanks for reporting. Are you defining the handlers inside the render method or elsewhere?

alexkrolick avatar Jul 21 '17 15:07 alexkrolick

I am defining them in a method I have called modules, and I call modules from the render method, like this:

This is in render,

      <div className={classes.frame}>
        <ReactQuill
          className={classes.rteParent}
          theme={`snow`}
          value={value || text}
          onChange={this.handleChange}
          modules={this.modules()}
          formats={this.formats()}
          >
            <div className={classes.rteEditZone}/>
          </ReactQuill>
      </div>

This is a separate method.

  modules() {


    return {
      toolbar: {
        container: [
          [
          'custom'
          ]
        ],
         handlers: {
           "custom": function () {
        
               this.quill.insertText(this.quill.getSelection().index, 'Hello', 'link', 'https://world.com');
               console.log(this.quill.getText())
           }
         }
      }
    }
  }

the-simian avatar Jul 24 '17 02:07 the-simian

If you are manipulating Quill imperatively you probably want the uncontrolled form of the component - use the defaultValue prop instead of value.

alexkrolick avatar Jul 24 '17 03:07 alexkrolick

thank you for the tip, I did not understand that would affect the component further into its lifecycle, I'll happily try :)

the-simian avatar Jul 24 '17 04:07 the-simian

Did this work for you?

alexkrolick avatar Jul 28 '17 17:07 alexkrolick

Sorry to reply late, I haven't had a chance to try this yet, because I've had to work on other things. I will know very soon and follow up. Very thankful for the tip, though

the-simian avatar Jul 28 '17 18:07 the-simian

Hello, unfortunately this results in errors on every keystroke when I use defaultValue, so unfortunately this does not result in a fix for me. I will close this, I suppose because I think I can solve my issues in a completely different way.

the-simian avatar Aug 10 '17 20:08 the-simian

I am encountering a similar issue - it seems this is only indirectly related to a custom button. From what I can reproduce it is related to the custom click handler @the-simian defined here - the important point being that we get a new reference to the function anytime the component which uses ReactQuill is rendered - this then triggers the error originally reported here.

Can be reproduced by trying the following for example:

class SomeModule
  constructor(quill, options) {
    quill.keyboard.addBinding({ key: 'a' }, () => options.onA());
  }
}

Quill.register('modules/some-module');

// later on in a render function
const modules = { 'some-module': { onA: () => console.log('typed a') } };
// and then pass these modules to ReactQuill

This will break because the onA handler we pass in is always a new reference.

Doing something like this will work fine:

// outside the render function
const onA = () => console.log('typed a');

// inside the render function
const modules = { 'some-module': { onA } };

This makes the deep equality check pass and everything is fine. There seems to be something wrong with the dirtyProps defined by ReactQuill perhaps which trigger a regeneration.

The workaround proposed is of course not really great, because it limits quite drastically what functions consumers can pass to quill modules.

Haven't investigated this further yet to really fix it.

LFDM avatar Sep 07 '17 13:09 LFDM

hey @LFDM , really appreciate the update, and that's incredibly useful information

edit: I'll likely be looking into this more this week. I'll post any findings.

the-simian avatar Sep 07 '17 19:09 the-simian

@the-simian @LFDM

This makes the deep equality check pass and everything is fine. There seems to be something wrong with the dirtyProps defined by ReactQuill perhaps which trigger a regeneration.

There's not really anything "wrong" with it, in fact the dirty check does a deep value check just to allow passing objects defined in render. However, creating a new function (as with an arrow function) each time will definitely cause a reinitialization of Quill. Technically this is because functions cannot be compared by value, but more importantly the functions in modules are passed directly to Quill and figuring out whether the new function is compatible with whatever is already registered is not practical.

https://github.com/zenoamaro/react-quill/blob/master/src/component.js#L242-L250

alexkrolick avatar Sep 18 '17 05:09 alexkrolick

The workaround proposed is of course not really great, because it limits quite drastically what functions consumers can pass to quill modules.

If the declarative API of ReactQuill is limiting, you might want to try a simple wrapper (let's call it React-Quill Lite):

import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Quill from 'quill'

class ReactQuillLite extends Component {
  componentDidMount() {
    this.editor = new Quill(this.editorNode, {
      theme: this.props.theme,
      placeholder: this.props.placeholder,
    })
  }

  shouldComponentUpdate() {
     return false // never rerender
  }

  render() {
    return <div ref={el => this.editorNode = el}/>
  }
}

ReactQuillLite.propTypes = {
  theme: PropTypes.string,
  placeholder: PropTypes.string,
}

ReactQuillLite.defaultProps = {
  theme: 'snow',
  placeholder: 'Write something...'
}

alexkrolick avatar Sep 18 '17 05:09 alexkrolick

@alexkrolick I do understand the object reference problem, this is reasonable behavior, what I meant with "there is something wrong" is that even if one of the dirty props changes, the editor should probably not throw an exception and become unresponsive - the reinstantiation doesn't seem to work properly. (wouldn't have solved the initial problem, but it would have behaved more as specified)

LFDM avatar Sep 18 '17 11:09 LFDM

Ok... I can look into this more if you provide an editable example on Codepen, CodeSandbox, JSFiddle, etc.

alexkrolick avatar Sep 18 '17 17:09 alexkrolick

Use componentDidMount and ref, and remove handlers from modules:

modules() {
  return {
    toolbar: {
      container: [
        [
        'custom'
        ]
      ]
    }
  }
}

handleCustomToolbarButton() {
  const quill = this.quillRef.getEditor()

  quill.insertText( quill.getSelection().index, 'Hello', 'link', 'https://world.com' )
  
  console.log( quill.getText() )
}

componentDidMount() {
    const toolbar = this.quillRef.getEditor().getModule('toolbar')
    
    toolbar.addHandler( 'custom', ::this.handleCustomToolbarButton )
}

render() {
    return (
      <ReactQuill
        ref={(el) => this.quillRef = el}
        className={classes.rteParent}
        theme={`snow`}
        value={value || text}
        onChange={this.handleChange}
        modules={this.modules()}
        formats={this.formats()}
      >
          <div className={classes.rteEditZone}/>
      </ReactQuill>
    )
}

undermuz avatar Oct 12 '17 10:10 undermuz

in my case , it worked when i delete this from[ codepen example] (https://codepen.io/alexkrolick/pen/gmroPj?editors=0010) and added componentDidMount like @undermuz comment

 <div 
        key="editor"                     
        ref="editor"
        className="quill-contents"                     
/>

my complete code ( using server render)

import React from 'react';
import 'react-quill/dist/quill.snow.css';

const CustomToolbar = () => (
  <div id="toolbar">
    <select className="ql-header">
      <option value="1"></option>
      <option value="2"></option>
      <option selected></option>
    </select>
    <button className="ql-bold"></button>
    <button className="ql-italic"></button>
    <select className="ql-color">
      <option value="red"></option>
      <option value="green"></option>
      <option value="blue"></option>
      <option value="orange"></option>
      <option value="violet"></option>
      <option value="#d0d1d2"></option>
      <option selected></option>
    </select>
    <button className="ql-insertStar">
      <span className="octicon octicon-star">★</span>
    </button>
  </div>
);

function insertStar() {
  const cursorPosition = this.quill.getSelection().index;
  this.quill.insertText(cursorPosition, '★');
  this.quill.setSelection(cursorPosition + 1);
}


const modules = {
  toolbar: {
    container: '#toolbar',
    handlers: {
      'insertStar': insertStar,
    }
  }
};

const formats = [
  'header', 'font', 'size',
  'bold', 'italic', 'underline', 'strike', 'blockquote',
  'list', 'bullet', 'indent',
  'link', 'image', 'color',
];

class Editor extends React.Component {

  constructor(props) {
    super(props);
    this.state = {editorHtml: ''};
    if (typeof window !== 'undefined') {
      this.ReactQuill = require('react-quill');
    }
  }

  handleChange(html) {
    this.setState({editorHtml: html});
  }

  componentDidMount() {
    const toolbar = this.quillRef.getEditor().getModule('toolbar');
    toolbar.addHandler('insertStar', insertStar);
  }

  render() {
    const ReactQuill = this.ReactQuill;

    if (!ReactQuill) {
      return null;
    }

    return (
      <div className="text-editor">
        <CustomToolbar/>
        <ReactQuill
          ref={(el) => this.quillRef = el}
          onChange={this.handleChange.bind(this)}
          placeholder={this.props.placeholder}
          modules={modules}
          formats={formats}
          theme={'snow'}
        >
        </ReactQuill>
      </div>
    );
  }
}

export default Editor;

qskane avatar Nov 11 '17 02:11 qskane

I need it for a functional component

Irwin007-lab avatar Sep 14 '23 05:09 Irwin007-lab

@Irwin007-lab try using useCallback to make a stable function reference for your callbacks in a function component

alexkrolick avatar Sep 15 '23 00:09 alexkrolick