uiwjs/react-md-editor

contenteditable div

stevemk14ebr opened this issue ยท 11 comments

Please allow the user to replace the textarea element with a user-given html element. This is required for drag and drop interfaces that want to drop into the mdeditor. Since textarea's cannot have children html elements other than text, the necessary hacky logic of putting spans between words to find the drop cursor location cannot be performed. With a contenteditable div the text can be mixed with html elements who's client position can be known (completing drag and drop logic).

@stevemk14ebr

<MDEditor
  highlightEnable={false}
  renderTextarea={(props, { dispatch, onChange }) => {
    return (
      <textarea {...props} onChange={(e) => {
        dispatch && dispatch({ markdown: e.target.value });
        onChange && onChange(e.target.value);
      }}/>
    )
  }}
/>
.w-md-editor-text-input {
  ....
  /* overflow: hidden; */
  /* -webkit-text-fill-color: transparent; */
}

@stevemk14ebr Upgrade @uiw/react-md-editor v3.4.0

That was fast thanks! I have issues when using a div instead of a textarea though:

const target = e.target as HTMLTextAreaElement;

throws with error 'can't access property "substr", target.value is undefined'

renderTextarea={(props, { dispatch, onChange }) => {
                    return (
                        <div {...props} spellcheck={true} contentEditable={true} onChange={(e) => {
                            dispatch && dispatch({ markdown: e.target.value });
                            onChange && onChange(e.target.value);
                        }}
                        />
                    )
                }}

I also think that the textareaProps may not be being passed down to the renderTextarea component correctly. I had to put my props directly on the div here to get them to be there. Maybe the cloneElement loses them?

@stevemk14ebr Change to handle it yourself?

It going to take me some time to figure this out, the new version doesn't throw but typing is broken entirely and the content never changes, im not sure how the MDEditor, onChange, and onKeyDown all interact at the moment.

{renderTextarea ? (
renderTextarea(
{
...otherProps,
value: markdown,
autoComplete: 'off',
autoCorrect: 'off',
spellCheck: 'false',
autoCapitalize: 'off',
className: `${prefixCls}-text-input`,
style: {
WebkitTextFillColor: 'inherit',
overflow: 'auto',
},
},
{ dispatch, onChange },
)
) : (

<MDEditor
  renderTextarea={(props, { dispatch, onChange}) => {
    return (
      <textarea {...props} onChange={(e) => {
        dispatch && dispatch({ markdown: e.target.value });
        onChange && onChange(e.target.value);
      }}/>
    )
  }}
/>

Add a new API, to be forward compatible.
@stevemk14ebr

Ok I will update this PR to use the new interfaces. We can keep the pre and div overlays, I've figured out the CSS issues! Here is an example of how the CSS should be, the font must be explicitly provided so that the div and pre render the same. In the old version a hardcoded padding is used and the wrong positioning is used too.

Correct: https://codesandbox.io/s/happy-feather-hdz1p?file=/src/App.js

In the current latest version if you click an item on the toolbar the application breaks because the commands assume they are interacting with a textarea, instead of the div. This starts here:

commandOrchestrator && commandOrchestrator.executeCommand(command);

The commandOrchestrator set here:

useContext: { commands, extraCommands, commandOrchestrator: executeRef.current },
must be configurable by the user so that they can override how the orchestrator gets the current text and finds the selection:

It's probably not worth the effort to generalize all the commands that already exist, instead just let the user override the toolbar onClick behaviors entirely.

I'm going to close this, i think it's smarter for a future user to start with a framework like draft-js and just use the MDEditor.Preview component. I'm finding that contenteditable is really finicky with cursor positions and positioning depending on child content.

I think the prop you added is still useful though, and should stay. The full current version I use right now is:

 // cursor class from https://stackoverflow.com/a/41034697/3480193
 renderTextarea={(props, { dispatch, onChange, shortcuts, useContext }) => {
                    let { value, ...otherProps } = props;
                    let texts = value.split("\n").map((item) => {
                        // each line is enclosed with a span already, each space subdivides from the middle
                        let split = item.replaceAll(" ", "</span><span> ");
                        return "<span>" + split + "\n</span>";
                    }).join("");
                    return (
                        <div {...otherProps} style={{ whiteSpace: 'pre', WebkitTextFillColor: 'inherit', overflowY: 'auto', whiteSpace: 'pre-wrap', overflowWrap: 'break-word', wordWrap: 'break-word' }} spellCheck={true} contentEditable={true} suppressContentEditableWarning={true}
                            onInput={(e) => {
                                let pos = Cursor.getCurrentCursorPosition(e.target);
                                setCaretPos(pos);
                                onChange(e.target.innerText);
                                dispatch(setEditorText(e.target.innerText));
                            }}
                            onKeyDown={(e) => {
                                // the way we store/reset cursor doesn't handle newlines (and i can't fix it due to browser issues)
                                if (e.keyCode === 13) {
                                    // on chrome this moves to the next line, firefox has a bug?
                                    var sel = window.getSelection();
                                    sel.modify("move", "forward", "character");
                                }
                            }}
                            dangerouslySetInnerHTML={{ __html: texts }}
                        />
                    )
                }}