簡體   English   中英

React.js:contentEditable 的 onChange 事件

[英]React.js: onChange event for contentEditable

如何收聽基於contentEditable的控件的更改事件?

var Number = React.createClass({
    render: function() {
        return <div>
            <span contentEditable={true} onChange={this.onChange}>
                {this.state.value}
            </span>
            =
            {this.state.value}
        </div>;
    },
    onChange: function(v) {
        // Doesn't fire :(
        console.log('changed', v);
    },
    getInitialState: function() {
        return {value: '123'}
    }    
});

React.renderComponent(<Number />, document.body);

http://jsfiddle.net/NV/kb3gN/1621/

編輯:請參閱Sebastien Lorber 的回答,它修復了我的實現中的錯誤。


使用 onInput 事件和可選的 onBlur 作為后備。 您可能希望保存以前的內容以防止發送額外的事件。

我個人會將此作為我的渲染功能。

var handleChange = function(event){
    this.setState({html: event.target.value});
}.bind(this);

return (<ContentEditable html={this.state.html} onChange={handleChange} />);

jsbin

它使用這個簡單的 contentEditable 包裝器。

var ContentEditable = React.createClass({
    render: function(){
        return <div 
            onInput={this.emitChange} 
            onBlur={this.emitChange}
            contentEditable
            dangerouslySetInnerHTML={{__html: this.props.html}}></div>;
    },
    shouldComponentUpdate: function(nextProps){
        return nextProps.html !== this.getDOMNode().innerHTML;
    },
    emitChange: function(){
        var html = this.getDOMNode().innerHTML;
        if (this.props.onChange && html !== this.lastHtml) {

            this.props.onChange({
                target: {
                    value: html
                }
            });
        }
        this.lastHtml = html;
    }
});

這是對我有用的最簡單的解決方案。

<div
  contentEditable='true'
  onInput={e => console.log('Text inside div', e.currentTarget.textContent)}
>
Text inside div
</div>

編輯 2015

有人用我的解決方案在 NPM 上做了一個項目: https ://github.com/lovasoa/react-contenteditable

編輯 06/2016:我剛剛遇到了一個新問題,當瀏覽器嘗試“重新格式化”你剛剛給他的 html 時會出現這個問題,導致組件總是重新渲染。

編輯 07/2016:這是我的生產內容可編輯實現。 它有一些您可能想要的超過react-contenteditable的附加選項,包括:

  • 鎖定
  • 允許嵌入 html 片段的命令式 API
  • 重新格式化內容的能力

概括:

FakeRainBrigand 的解決方案在我遇到新問題之前對我來說效果很好。 ContentEditables 很痛苦,而且處理 React 並不容易......

這個JSFiddle演示了這個問題。

如您所見,當您鍵入一些字符並單擊Clear時,內容並沒有被清除。 這是因為我們嘗試將 contenteditable 重置為最后一個已知的虛擬 dom 值。

所以看起來:

  • 您需要shouldComponentUpdate以防止插入符號位置跳轉
  • 如果你以這種方式使用shouldComponentUpdate ,你就不能依賴 React 的 VDOM 差異算法。

所以你需要一個額外的行,這樣只要shouldComponentUpdate返回 yes,你就可以確定 DOM 內容實際上已經更新了。

所以這里的版本增加了一個componentDidUpdate ,變成了:

var ContentEditable = React.createClass({
    render: function(){
        return <div id="contenteditable"
            onInput={this.emitChange} 
            onBlur={this.emitChange}
            contentEditable
            dangerouslySetInnerHTML={{__html: this.props.html}}></div>;
    },

    shouldComponentUpdate: function(nextProps){
        return nextProps.html !== this.getDOMNode().innerHTML;
    },

    componentDidUpdate: function() {
        if ( this.props.html !== this.getDOMNode().innerHTML ) {
           this.getDOMNode().innerHTML = this.props.html;
        }
    },

    emitChange: function(){
        var html = this.getDOMNode().innerHTML;
        if (this.props.onChange && html !== this.lastHtml) {
            this.props.onChange({
                target: {
                    value: html
                }
            });
        }
        this.lastHtml = html;
    }
});

虛擬 dom 仍然過時,它可能不是最有效的代碼,但至少它確實有效 :)我的錯誤已解決


細節:

1)如果您放置 shouldComponentUpdate 以避免插入符號跳轉,則 contenteditable 永遠不會重新呈現(至少在擊鍵時)

2) 如果組件在擊鍵時從不重新渲染,那么 React 會為這個 contenteditable 保留一個過時的虛擬 dom。

3) 如果 React 在其虛擬 dom 樹中保留了一個過期版本的 contenteditable,那么如果你嘗試將 contenteditable 重置為虛擬 dom 中過期的值,那么在虛擬 dom diff 期間,React 將計算出沒有對適用於 DOM!

這主要發生在以下情況:

  • 你最初有一個空的 contenteditable (shouldComponentUpdate=true,prop="",previous vdom=N/A),
  • 用戶輸入一些文本,你阻止渲染(shouldComponentUpdate=false,prop=text,previous vdom="")
  • 用戶單擊驗證按鈕后,您要清空該字段(shouldComponentUpdate=false,prop="",previous vdom="")
  • 由於新生成的和舊的 vdom 都是 "",因此 React 不會觸及 dom。

因為當編輯完成時,元素的焦點總是丟失,您可以簡單地使用onBlur事件處理程序。

<div
  onBlur={e => {
    console.log(e.currentTarget.textContent);
  }} 
  contentEditable
  suppressContentEditableWarning={true}
>
  <p>Lorem ipsum dolor.</p>
</div>

這可能不完全是您正在尋找的答案,但是我自己也在為此苦苦掙扎並且對建議的答案有疑問,所以我決定讓它不受控制。

editable prop 為false時,我按原樣使用text prop,但當它為true時,我切換到text無效的編輯模式(但至少瀏覽器不會崩潰)。 在此期間, onChange由控件觸發。 最后,當我將editable改回false時,它​​會用text中傳遞的任何內容填充 HTML:

/** @jsx React.DOM */
'use strict';

var React = require('react'),
    escapeTextForBrowser = require('react/lib/escapeTextForBrowser'),
    { PropTypes } = React;

var UncontrolledContentEditable = React.createClass({
  propTypes: {
    component: PropTypes.func,
    onChange: PropTypes.func.isRequired,
    text: PropTypes.string,
    placeholder: PropTypes.string,
    editable: PropTypes.bool
  },

  getDefaultProps() {
    return {
      component: React.DOM.div,
      editable: false
    };
  },

  getInitialState() {
    return {
      initialText: this.props.text
    };
  },

  componentWillReceiveProps(nextProps) {
    if (nextProps.editable && !this.props.editable) {
      this.setState({
        initialText: nextProps.text
      });
    }
  },

  componentWillUpdate(nextProps) {
    if (!nextProps.editable && this.props.editable) {
      this.getDOMNode().innerHTML = escapeTextForBrowser(this.state.initialText);
    }
  },

  render() {
    var html = escapeTextForBrowser(this.props.editable ?
      this.state.initialText :
      this.props.text
    );

    return (
      <this.props.component onInput={this.handleChange}
                            onBlur={this.handleChange}
                            contentEditable={this.props.editable}
                            dangerouslySetInnerHTML={{__html: html}} />
    );
  },

  handleChange(e) {
    if (!e.target.textContent.trim().length) {
      e.target.innerHTML = '';
    }

    this.props.onChange(e);
  }
});

module.exports = UncontrolledContentEditable;

我建議使用 mutationObserver 來做到這一點。 它使您可以更好地控制正在發生的事情。 它還為您提供有關瀏覽器如何解釋所有擊鍵的更多詳細信息

在打字稿中

import * as React from 'react';

export default class Editor extends React.Component {
    private _root: HTMLDivElement; // Ref to the editable div
    private _mutationObserver: MutationObserver; // Modifications observer
    private _innerTextBuffer: string; // Stores the last printed value

    public componentDidMount() {
        this._root.contentEditable = "true";
        this._mutationObserver = new MutationObserver(this.onContentChange);
        this._mutationObserver.observe(this._root, {
            childList: true, // To check for new lines
            subtree: true, // To check for nested elements
            characterData: true // To check for text modifications
        });
    }

    public render() {
        return (
            <div ref={this.onRootRef}>
                Modify the text here ...
            </div>
        );
    }

    private onContentChange: MutationCallback = (mutations: MutationRecord[]) => {
        mutations.forEach(() => {
            // Get the text from the editable div
            // (Use innerHTML to get the HTML)
            const {innerText} = this._root; 

            // Content changed will be triggered several times for one key stroke
            if (!this._innerTextBuffer || this._innerTextBuffer !== innerText) {
                console.log(innerText); // Call this.setState or this.props.onChange here
                this._innerTextBuffer = innerText;
            }
        });
    }

    private onRootRef = (elt: HTMLDivElement) => {
        this._root = elt;
    }
}

這是一個包含 lovasoa 的大部分內容的組件: https ://github.com/lovasoa/react-contenteditable/blob/master/index.js

他在 emitChange 中填充事件

emitChange: function(evt){
    var html = this.getDOMNode().innerHTML;
    if (this.props.onChange && html !== this.lastHtml) {
        evt.target = { value: html };
        this.props.onChange(evt);
    }
    this.lastHtml = html;
}

我成功地使用了類似的方法

<div
    spellCheck="false"
    onInput={e => console.log("e: ", e.currentTarget.textContent}
    contentEditable="true"
    suppressContentEditableWarning={true}
    placeholder="Title"
    className="new-post-title"
/>

我嘗試使用上面來自 Saint Laurent 的示例

<div
  onBlur={e => {
    console.log(e.currentTarget.textContent);
  }}
  contentEditable
  suppressContentEditableWarning={true}
>
  <p>Lorem ipsum dolor.</p>
</div>

在我嘗試將此值設置為 state 之前,它運行良好。 當我使用調用setState(e.currentTarget.textContent)的功能組件時,我將currentTarget設為 null。 setState異步工作, currentTarget在那里不可用。

在 React 17.0.2 中對我有用的修復是使用e.target.innerText

<div
  onBlur={e => setState(e.target.innerText)}
  contentEditable
  suppressContentEditableWarning={true}
>
  <p>Lorem ipsum dolor.</p>
</div>

這是我基於https://stackoverflow.com/a/27255103的基於鈎子的版本

const noop = () => {};
const ContentEditable = ({
  html,
  onChange = noop,
}: {
  html: string;
  onChange?: (s: string) => any;
}) => {
  const ref = useRef<HTMLDivElement>(null);
  const lastHtml = useRef<string>('');

  const emitChange = () => {
    const curHtml = ref.current?.innerHTML || '';
    if (curHtml !== lastHtml.current) {
      onChange(curHtml);
    }
    lastHtml.current = html;
  };

  useEffect(() => {
    if (!ref.current) return;
    if (ref.current.innerHTML === html) return;
    ref.current.innerHTML = html;
  }, [html]);

  return (
    <div
      onInput={emitChange}
      contentEditable
      dangerouslySetInnerHTML={{ __html: html }}
      ref={ref}
    ></div>
  );
};
<div
    contentEditable={true}
    dangerouslySetInnerHTML={{ __html: str }}
  />

如果您正在尋找一種簡單而簡單的方法,那就是。

<div 
    contentEditable={true}
    onSelect={() => callMeOnSelect()}
    onChange={() => callMeOnChange()}
    onMouseUp={() => callMeOnMouseUp()}
    className="myClassNameHere"/>

如果您想在觸發選擇時獲取所選文本,您可以執行此操作。

let selection = window.getSelection().toString();

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM