instructure-react / react-tinymce

React TinyMCE component
181 stars 110 forks source link

using file_picker_callback causes editor to reload with every change event that is fired #62

Open ghost opened 7 years ago

ghost commented 7 years ago

I am using admin-on-rest and react-tinymce. When I include file_picker_callbackin the tinymce config the editor will reload every time the onchange event fires.

This is an issue because users are not able to continually type (editor loses focus after reloading). I have tried manually setting the focus after it reloads, but it does not focus back to the same place the cursor was previously - so this is not a viable workaround.

If I remove the file_picker_callback, then everything works as expected.

Here is my code:

import React, { PropTypes, Component } from 'react'
import TinyMCE from 'react-tinymce'
import debounce from 'lodash.debounce';

class Editor extends Component {
    self;

    componentDidMount() {
        const {
            id,
            disabled,
            label,
            name,
            input: {
                value,
                onChange
            },
            config
        } = this.props;

        self = this;
    }

    onEditorChange = (content = window.tinyMCE.activeEditor.getContent()) => {
        if (self.props.input.value == content)
            return;

        self.props.input.onChange(content);
    }

    render() {
        return (
            <div>
                <input type="file" id="image-upload-tinymce" name="single-image" style={{ display: "none" }} accept="image/png, image/gif, image/jpeg, image/jpg, image/svg" />

                <TinyMCE
                    content={this.props.input.value}
                    config={{
                        plugins: [
                            'advlist autolink lists link image charmap print preview anchor',
                            'searchreplace wordcount visualblocks code',
                            'insertdatetime table contextmenu paste code'
                        ],
                        toolbar: 'insertfile undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image | code',
                        file_browser_callback_types: 'image',
                        file_picker_callback: (callback, value, meta) => {
                            if (meta.filetype == 'image') {

                                var input = document.getElementById('image-upload-tinymce');
                                input.click();

                                input.onchange = () => {
                                    var file = input.files[0];
                                    var reader = new FileReader();

                                    reader.onload = (e) => {
                                        var img = new Image();
                                        img.src = reader.result;

                                        callback(e.target.result, {
                                            alt: file.name
                                        });

                                        var delay = debounce(self.onEditorChange, 10000);

                                        delay();
                                    };

                                    reader.readAsDataURL(file);
                                };
                            }
                        },
                    }}
                    id={this.props.id}
                    onKeyup={e => {
                        e.stopPropagation();
                        e.preventDefault();

                        var delay = debounce(self.onEditorChange, 500);

                        delay();
                    }}

                />
            </div>
        )
    }

}

I have the following in my index.html: <script src="https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.5.6/tinymce.min.js"></script>

What I have noticed is that during an onchange event there is a get request issued for:

https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.5.6/skins/lightgray/content.min.css

and the call stack for this request is as follows:


initContentBody @   tinymce.min.js:11
init    @   tinymce.min.js:11
(anonymous) @   tinymce.min.js:11
(anonymous) @   tinymce.min.js:4
n   @   tinymce.min.js:3
v   @   tinymce.min.js:4
loadScripts @   tinymce.min.js:4
loadQueue   @   tinymce.min.js:4
t   @   tinymce.min.js:11
render  @   tinymce.min.js:11
a   @   tinymce.min.js:12
(anonymous) @   tinymce.min.js:12
n   @   tinymce.min.js:3
d   @   tinymce.min.js:12
t.bind  @   tinymce.min.js:2
bind    @   tinymce.min.js:4
init    @   tinymce.min.js:12
_init   @   TinyMCE.js:128
TinyMCE_componentWillReceiveProps   @   TinyMCE.js:66
(anonymous) @   ReactCompositeComponent.js:611
measureLifeCyclePerf    @   ReactCompositeComponent.js:75
updateComponent @   ReactCompositeComponent.js:610
receiveComponent    @   ReactCompositeComponent.js:547
receiveComponent    @   ReactReconciler.js:125
updateChildren  @   ReactChildReconciler.js:109
_reconcilerUpdateChildren   @   ReactMultiChild.js:208
_updateChildren @   ReactMultiChild.js:312
updateChildren  @   ReactMultiChild.js:299
_updateDOMChildren  @   ReactDOMComponent.js:936
updateComponent @   ReactDOMComponent.js:754
receiveComponent    @   ReactDOMComponent.js:716
receiveComponent    @   ReactReconciler.js:125
_updateRenderedComponent    @   ReactCompositeComponent.js:754
_performComponentUpdate @   ReactCompositeComponent.js:724
updateComponent @   ReactCompositeComponent.js:645
receiveComponent    @   ReactCompositeComponent.js:547
receiveComponent    @   ReactReconciler.js:125
updateChildren  @   ReactChildReconciler.js:109
_reconcilerUpdateChildren   @   ReactMultiChild.js:208
_updateChildren @   ReactMultiChild.js:312
updateChildren  @   ReactMultiChild.js:299
_updateDOMChildren  @   ReactDOMComponent.js:936
updateComponent @   ReactDOMComponent.js:754
receiveComponent    @   ReactDOMComponent.js:716
receiveComponent    @   ReactReconciler.js:125
_updateRenderedComponent    @   ReactCompositeComponent.js:754
_performComponentUpdate @   ReactCompositeComponent.js:724
updateComponent @   ReactCompositeComponent.js:645
receiveComponent    @   ReactCompositeComponent.js:547
receiveComponent    @   ReactReconciler.js:125
_updateRenderedComponent    @   ReactCompositeComponent.js:754
_performComponentUpdate @   ReactCompositeComponent.js:724
updateComponent @   ReactCompositeComponent.js:645
receiveComponent    @   ReactCompositeComponent.js:547
receiveComponent    @   ReactReconciler.js:125
_updateRenderedComponent    @   ReactCompositeComponent.js:754
_performComponentUpdate @   ReactCompositeComponent.js:724
updateComponent @   ReactCompositeComponent.js:645
receiveComponent    @   ReactCompositeComponent.js:547
receiveComponent    @   ReactReconciler.js:125
_updateRenderedComponent    @   ReactCompositeComponent.js:754
_performComponentUpdate @   ReactCompositeComponent.js:724
updateComponent @   ReactCompositeComponent.js:645
performUpdateIfNecessary    @   ReactCompositeComponent.js:561
performUpdateIfNecessary    @   ReactReconciler.js:157
runBatchedUpdates   @   ReactUpdates.js:150
perform @   Transaction.js:140
perform @   Transaction.js:140
perform @   ReactUpdates.js:89
flushBatchedUpdates @   ReactUpdates.js:172
closeAll    @   Transaction.js:206
perform @   Transaction.js:153
batchedUpdates  @   ReactDefaultBatchingStrategy.js:62
enqueueUpdate   @   ReactUpdates.js:200
enqueueUpdate   @   ReactUpdateQueue.js:24
enqueueSetState @   ReactUpdateQueue.js:219
ReactComponent.setState @   ReactComponent.js:63
handleChange    @   connect.js:302
dispatch    @   createStore.js:186
(anonymous) @   middleware.js:22
(anonymous) @   middleware.js:80
boundChange @   createFieldProps.js:97
(anonymous) @   createOnChange.js:37
Editor._this.onEditorChange @   editor.js:29
invokeFunc  @   index.js:160
trailingEdge    @   index.js:207
timerExpired    @   index.js:195

From the call stack, line 66 of TinyMCE.js is:

componentWillReceiveProps: function componentWillReceiveProps(nextProps) {
    if (!(0, _lodashLangIsEqual2['default'])(this.props.config, nextProps.config)) {
      this._init(nextProps.config, nextProps.content);//line 66 here
    }

line 128 is tinymce.init(config);

I believe this is a bug with react-tinymce (and not admin-on-rest) - but I am not very familiar with react, so I could be wrong.

Any help or insight would be appreciated. I have spent 2 days debugging this and have made no progress

Arkine commented 7 years ago

I also am having the problem with this callback.

Every time I press enter in the textarea, it submits the editor and reloads the page. When I remove the callback, I can press enter without the textarea submitting.

SimonChris commented 7 years ago

I am having exactly the same problem. Did either of you manage to solve this?

In my case, the page even reloads when entering text into a different input component on the same page. It seems like react-tinymce is picking up on events that happens elsewhere as well.

SimonChris commented 7 years ago

I have found the cause of this issue: Whenever ComponentWillReceiveProps() is called, react-tinymce uses Lodash.isEqual() to determine whether the component should be rerendered. isEqual compares functions by strict equality. This means that any function declared inside the "config" object is considered to be a new function, causing the editor to rerender whenever anything at all changes on the page.

The solution is to move the function declaration outside the component, so that the same function object is reused every time:

function filePickerCallback(callback, value, meta) {
...
}
class Editor extends Component {
...
    render() {
        return (
            <div>
                <input type="file" id="image-upload-tinymce" name="single-image" style={{ display: "none" }} accept="image/png, image/gif, image/jpeg, image/jpg, image/svg" />
                <TinyMCE
                    content={this.props.input.value}
                    config={{
                        plugins: [
                            'advlist autolink lists link image charmap print preview anchor',
                            'searchreplace wordcount visualblocks code',
                            'insertdatetime table contextmenu paste code'
                        ],
                        toolbar: 'insertfile undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image | code',
                        file_browser_callback_types: 'image',
                        file_picker_callback: filePickerCallback,
                    }}
                    id={this.props.id}
                    onKeyup={e => {
                        e.stopPropagation();
                        e.preventDefault();
                        var delay = debounce(self.onEditorChange, 500);
                        delay();
                    }}
                />
            </div>
        )
    }
}

See also #43 and #47.