uiwjs / react-md-editor

A simple markdown editor with preview, implemented with React.js and TypeScript.
https://uiwjs.github.io/react-md-editor
MIT License
2.17k stars 156 forks source link

contenteditable div #193

Closed stevemk14ebr closed 3 years ago

stevemk14ebr commented 3 years ago

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).

jaywcjlove commented 3 years ago

@stevemk14ebr

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

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

stevemk14ebr commented 3 years ago

That was fast thanks! I have issues when using a div instead of a textarea though: https://github.com/uiwjs/react-md-editor/blob/9bb6985e6b8c74f2cab91707398bc77c1d28301b/src/components/TextArea/handleKeyDown.tsx#L18

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?

jaywcjlove commented 3 years ago

@stevemk14ebr Change to handle it yourself?

stevemk14ebr commented 3 years ago

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.

jaywcjlove commented 3 years ago

https://github.com/uiwjs/react-md-editor/blob/7003b7546a3186f5be94abc447204c35a40330df/src/components/TextArea/index.tsx#L49-L66

<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

jaywcjlove commented 3 years ago

Example: https://codesandbox.io/embed/markdown-editor-for-react-rendertextarea-v6kt3?file=/index.js

stevemk14ebr commented 3 years ago

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

stevemk14ebr commented 3 years ago

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: https://github.com/uiwjs/react-md-editor/blob/1730e0f24ec26540d8b2fad5b7098fbb82166925/src/components/Toolbar/index.tsx#L44

The commandOrchestrator set here: https://github.com/uiwjs/react-md-editor/blob/1730e0f24ec26540d8b2fad5b7098fbb82166925/src/components/TextArea/index.tsx#L94 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.

stevemk14ebr commented 3 years ago

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 }}
                        />
                    )
                }}