Fork me on GitHub

自适应高度文本框 react contenteditable

import React, { Component } from 'react';
import PropTypes from 'prop-types';

const reduceTargetKeys = (target, keys, predicate) => Object.keys(target).reduce(predicate, {});

const omit = (target = {}, keys = []) =>
    reduceTargetKeys(target, keys, (acc, key) => keys.some(omitKey => omitKey === key) ? acc : { ...acc, [key]: target[key] });

const pick = (target = {}, keys = []) =>
    reduceTargetKeys(target, keys, (acc, key) => keys.some(pickKey => pickKey === key) ? { ...acc, [key]: target[key] } : acc);

const isEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b);

const propTypes = {
    content: PropTypes.string,
    editable: PropTypes.bool,
    focus: PropTypes.bool,
    maxLength: PropTypes.number,
    multiLine: PropTypes.bool,
    sanitise: PropTypes.bool,
    caretPosition: PropTypes.oneOf(['start', 'end']),
    tagName: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), // The element to make contenteditable. Takes an element string ('div', 'span', 'h1') or a styled component
    innerRef: PropTypes.func,
    onBlur: PropTypes.func,
    onFocus: PropTypes.func,
    onKeyDown: PropTypes.func,
    onPaste: PropTypes.func,
    onChange: PropTypes.func,
    styled: PropTypes.bool, // If element is a styled component (uses innerRef instead of ref)
};

const defaultProps = {
    content: '',
    editable: true,
    focus: false,
    maxLength: Infinity,
    multiLine: false,
    sanitise: true,
    caretPosition: null,
    tagName: 'div',
    innerRef: () => { },
    onBlur: () => { },
    onFocus: () => { },
    onKeyDown: () => { },
    onPaste: () => { },
    onChange: () => { },
    styled: false,
};

class ContentEditable extends Component {
    constructor(props) {
        super();

        this.state = {
            value: props.content,
            isFocused: false,
        };
    }

    componentDidMount() {
        this.setFocus();
        this.setCaret();
    }

    componentWillReceiveProps(nextProps) {
        if (nextProps.content !== this.sanitiseValue(this.state.value)) {
            this.setState({ value: nextProps.content }, () => {
                if (!this.state.isFocused) this.forceUpdate();
            });
        }
    }

    shouldComponentUpdate(nextProps) {
        const propKeys = Object.keys(nextProps).filter(key => key !== 'content');
        return !isEqual(pick(nextProps, propKeys), pick(this.props, propKeys));
    }

    componentDidUpdate() {
        this.setFocus();
        this.setCaret();
    }

    setFocus = () => {
        if (this.props.focus && this._element) {
            this._element.focus();
        }
    };

    setCaret = () => {
        const { caretPosition } = this.props;

        if (caretPosition && this._element) {
            const { value } = this.state;
            const offset = value.length && caretPosition === 'end' ? 1 : 0;
            const range = document.createRange();
            const selection = window.getSelection();
            range.setStart(this._element, offset);
            range.collapse(true);

            selection.removeAllRanges();
            selection.addRange(range);
        }
    };

    sanitiseValue(val) {
        const { maxLength, multiLine, sanitise } = this.props;

        if (!sanitise) {
            return val;
        }

        // replace encoded spaces
        let value = val.replace(/ /, ' ').replace(/[\u00a0\u2000-\u200b\u2028-\u2029\u202e-\u202f\u3000]/g, ' ');

        if (multiLine) {
            // replace any 2+ character whitespace (other than new lines) with a single space
            value = value.replace(/[\t\v\f\r ]+/g, ' ');
        } else {
            value = value.replace(/\s+/g, ' ');
        }

        return value
            .split('\n')
            .map(line => line.trim())
            .join('\n')
            .replace(/\n{3,}/g, '\n\n') // replace 3+ line breaks with two
            .trim()
            .substr(0, maxLength);
    }

    _onChange = ev => {
        const { sanitise } = this.props;
        const rawValue = this._element.innerText;
        const value = sanitise ? this.sanitiseValue(rawValue) : rawValue;

        if (this.state.value !== value) {
            this.setState({ value: rawValue }, () => {
                this.props.onChange(ev, value);
            });
        }
    };

    _onPaste = ev => {
        const { maxLength } = this.props;

        ev.preventDefault();
        const text = ev.clipboardData.getData('text').substr(0, maxLength);
        document.execCommand('insertText', false, text);

        this.props.onPaste(ev);
    };

    _onBlur = ev => {
        const { sanitise } = this.props;
        const rawValue = this._element.innerText;
        const value = sanitise ? this.sanitiseValue(rawValue) : rawValue;

        // We finally set the state to the sanitised version (rather than the `rawValue`) because we're blurring the field.
        this.setState({
            value,
            isFocused: false,
        }, () => {
            this.props.onChange(ev, value);
            this.forceUpdate();
        });

        this.props.onBlur(ev);
    };

    _onFocus = ev => {
        this.setState({
            isFocused: true,
        });
        this.props.onFocus(ev);
    };

    _onKeyDown = ev => {
        const { maxLength, multiLine } = this.props;
        const value = this._element.innerText;

        // return key
        if (!multiLine && ev.keyCode === 13) {
            ev.preventDefault();
            ev.currentTarget.blur();
            // Call onKeyUp directly as ev.preventDefault() means that it will not be called
            this._onKeyUp(ev);
        }

        // Ensure we don't exceed `maxLength` (keycode 8 === backspace)
        if (maxLength && !ev.metaKey && ev.which !== 8 && value.replace(/\s\s/g, ' ').length >= maxLength) {
            ev.preventDefault();
            // Call onKeyUp directly as ev.preventDefault() means that it will not be called
            this._onKeyUp(ev);
        }
    };

    _onKeyUp = ev => {
        // Call prop.onKeyDown callback from the onKeyUp event to mitigate both of these issues:
        // Access to Synthetic event: https://github.com/ashleyw/react-sane-contenteditable/issues/14
        // Current value onKeyDown: https://github.com/ashleyw/react-sane-contenteditable/pull/6
        // this._onKeyDown can't be moved in it's entirety to onKeyUp as we lose the opportunity to preventDefault
        this.props.onKeyDown(ev, this._element.innerText);
    };

    render() {
        const { tagName: Element, content, editable, styled, ...props } = this.props;

        return (
            <Element
                {...omit(props, Object.keys(propTypes))}
                {...(styled
                    ? {
                        innerRef: c => {
                            this._element = c;
                            props.innerRef(c);
                        },
                    }
                    : {
                        ref: c => {
                            this._element = c;
                            props.innerRef(c);
                        },
                    })}
                style={{ minHeight: '0.28rem', minWidth: '100%', display: 'inline-block', whiteSpace: 'pre-wrap', wordWrap: 'break-word', wordBreak: 'break-all', ...props.style }}
                contentEditable={editable}
                key={Date()}
                dangerouslySetInnerHTML={{ __html: this.state.value }}
                onBlur={this._onBlur}
                onFocus={this._onFocus}
                onInput={this._onChange}
                onKeyDown={this._onKeyDown}
                onKeyUp={this._onKeyUp}
                onPaste={this._onPaste}
            />
        );
    }
}

ContentEditable.propTypes = propTypes;
ContentEditable.defaultProps = defaultProps;

export default ContentEditable;

 

posted @ 2019-10-23 16:03  思考的大腿  阅读(1061)  评论(0编辑  收藏  举报