2

I am trying to change the undo/redo behaviour of react-quill when implemented as a Field modification to Contentful's SDK. The implementation is described by the template at https://github.com/contentful/create-contentful-app/tree/master/template.

When implemented naively, the Ctrl-Z keyboard shortcut clears the react-quill editor entirely. Implementing userOnly: true in the history module fixes this, but I would like to use buttons in the toolbar to control the undo/redo behaviour by accessing react-quill's delta and history functionality directly.

I have modified https://github.com/contentful/create-contentful-app/blob/master/template/src/components/Field.tsx as follows:

import React, {useState, useEffect, useRef, createRef} from 'react';
import { Paragraph } from '@contentful/forma-36-react-components';
import { FieldExtensionSDK } from 'contentful-ui-extensions-sdk';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';

const formats = ['header','bold', 'italic', 'underline', 'strike', 'blockquote','list', 'bullet', 'indent','link', 'image', 'undo', 'redo'];

let icons = ReactQuill.Quill.import("ui/icons");

interface FieldProps { sdk: FieldExtensionSDK; }

let mainRef = React.createRef<ReactQuill>();

const Field = (props: FieldProps) => {
  const [value, setValue] = useState('');

  useEffect (()=> {
    // CMS update code here - removed for clarity
  }, [])

  const modules = {
    toolbar: {
      container: [
        [
          { 'header': [1, 2, false] }
        ],
        ['bold', 'italic'],
        [
          {'list': 'ordered'},
          {'list': 'bullet'}
        ],
        ['link'],
        ['image'],
        ['undo'],
        ['redo']
      ],
      handlers: {
        'undo': function () {
          console.log(mainRef.current)
        },
        'redo': function () {
          console.log(mainRef.current)
        }
      }
    },
    history: {
      delay: 200,
      maxStack: 500,
      userOnly: true
    }
  };

  return <div>
        <ReactQuill id='quillmain' modules={modules} formats={formats} theme="snow" value={value} onChange={setValue} ref={mainRef}/>
    </div>
  ;
};

export default Field;

The undo and redo handlers defined above will fire successfully - on clicking the undo or redo icons, I will get console output of the current state of the ReactQuill instance, as per the reference I've created:

console output

I would like to access the history module controls defined at https://quilljs.com/docs/modules/history/, so that I can effectively call something like mainRef.current.history.undo() and redo(). But if I try to call them in my modules const declaration, I run into trouble with the state of the ReactQuill instance:

console.log(mainRef.current.history)
>>
Object is possibly 'null'.  TS2531

Trying to force the issue doesn't help:

console.log(mainRef.current!.history)
>>
Property 'history' does not exist on type 'ReactQuill'.  TS2339

But as per the screenshot above, printing the current instance shows that the history property is available, I assume just not at the point the modules const is being declared. Moving the declaration outside the modules const does not help - the same TS errors are encountered.

This feels like it should be possible as per this answer: How to create undo/redo buttons in Quill JS (react-quill)? , but I feel as though the combination of being restricted to a functional component, typescript, and hooks, are all conspiring to prevent me from using react-quill in the way I need.

Can anyone suggest how I can access the ReactQuill instance so that I can call the undo() and redo() functions as needed, or alternatively, how to implement react-quill under these restrictions so that the undo functionality works as intended?

iLuvLogix
  • 5,920
  • 3
  • 26
  • 43
Rob R
  • 31
  • 3
  • I've made some progress with this, but it's still not solved. – Rob R Nov 15 '20 at 21:23
  • I can access the undo functionality in Quill via keyboard shortcut, but only if I remove inline functions from the modules const declaration. I thought this this was because I was updating the state in the inline function, i.e. calling setValue and causing the hook to loop back on itself, but the undo behaviour happens for the very simplest handler declaration: `handlers: {` `'undo': function () {` `console.log("Undo")` `}` `}` If I declare the function outside the module const, undo works: `'undo': undoFunc()` Inline funcs are the cause – Rob R Nov 15 '20 at 21:32

2 Answers2

1

The answer here is useCallback. As suspected, inline functions were causing the component to have issues with state, but useCallback allows the declaration of an inline function while maintaining the desired react-quill behaviour. The following code allows the controlled inline function to fire, while maintaining desired undo/redo behaviour:

const modules = {
  toolbar: {
    container: [
      [
        { 'header': [1, 2, false] }
      ],
      ['bold', 'italic'],
      [
        {'list': 'ordered'},
        {'list': 'bullet'}
      ],
      ['link'],
      ['image'],
      ['undo'],
      ['redo']
    ],
    handlers: {
      'undo': React.useCallback(undoFunc => {
        console.log(mainRef.current) // DOES NOT BREAK QUILL STATE!
      }, [])
      'redo': function () {
        console.log(mainRef.current) // BREAKS QUILL STATE!
      }
    }
  },
  history: {
    delay: 200,
    maxStack: 500,
    userOnly: true
  }
};

Declaring the modules const with useCallback rather than inline functions achieves the desired behaviour.

Rob R
  • 31
  • 3
0
import ReactQuill, { Quill } from 'react-quill';

let mainRef = React.createRef<ReactQuill>();
.....
....
..
.
const modules = {
  toolbar: {
    container: [
      [
        { 'header': [1, 2, false] }
      ],
      ['bold', 'italic'],
      [
        {'list': 'ordered'},
        {'list': 'bullet'}
      ],
      ['link'],
      ['image'],
      ['undo'],
      ['redo']
    ],
    handlers: {
      'undo': () => {
        let myEditor: any = mainRef?.current?.getEditor();
        return myEditor?.history?.undo();
      },
      'redo': function () {
        let myEditor: any = mainRef?.current?.getEditor();
        return myEditor?.history?.redo();
      }
    }
  },
  history: {
    delay: 200,
    maxStack: 500,
    userOnly: true
  }
};

return (
    <ReactQuill
      theme="snow"
      value={props.value}
      onChange={props.onChange}
      placeholder={'Write something awesome...'}
      modules={modules}
      ref={mainRef}
      formats={formats}
    />
  );

Try to do this, it works for me !!

Mild-TN
  • 71
  • 3