UNPKG

react-pencil

Version:

A React component that allows single and multiline in-place edits.

272 lines (229 loc) 5.82 kB
import PropTypes from 'prop-types'; import React from 'react'; import {render} from 'react-dom'; import autosizeInput from 'autosize-input'; const { Component } = React; const commonPropTypes = { finishEdit: PropTypes.func.isRequired, placeholder: PropTypes.string, tabIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), value: PropTypes.string }; const commonDefaultProps = {}; function moveCursorToEnd(el) { if (el && el.tagName.toLowerCase().match(/input|textarea/)) { el.focus(); if (el.setSelectionRange) { const len = el.value.length * 2; el.setSelectionRange(len, len); } else { el.value = el.value; } el.scrollTop = 999999; } else if (document.createRange) { const range = document.createRange(); range.selectNodeContents(el); range.collapse(false); const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); } } class Singleline extends Component { constructor({value}) { super(); this.state = {value}; this.onBlur = this.onBlur.bind(this); this.onKeyUp = this.onKeyUp.bind(this); this.onChange = this.onChange.bind(this); } componentDidMount() { this.autosize(); } componentDidUpdate() { this.autosize(); } componentWillUnmount() { if (this._delayedFocus) { window.clearTimeout(this._delayedFocus); } } componentWillReceiveProps({value}) { if (this.state.value !== value) { this.setState({value}); } } autosize() { try { autosizeInput(this.content); } catch (ignore) {} } focus() { this._delayedFocus = window.setTimeout(() => { moveCursorToEnd(this.content); this.content.focus(); }, 110); } blur() { this.content.blur(); } onKeyUp(e) { if (e.keyCode === 27 || e.keyCode === 13) { this.blur(); } } onBlur(e) { this.props.finishEdit(this.state.value); } onChange(e) { this.setState({value: e.target.value}); } render() { const {name, value, style, finishEdit, ...rest} = this.props; return ( <input type='text' ref={el => (this.content = el)} name={name} autoComplete='off' value={this.state.value} style={style} onBlur={this.onBlur} onKeyUp={this.onKeyUp} onChange={this.onChange} {...rest} /> ); } } Object.assign(Singleline, { propTypes: commonPropTypes, defaultProps: commonDefaultProps }); class Multiline extends Component { constructor(props) { super(props); this.onFocus= this.onFocus.bind(this); this.onBlur= this.onBlur.bind(this); this.onClick= this.onClick.bind(this); this.onKeyDown= this.onKeyDown.bind(this); } focus() { this._wasClicked = true; this.content.focus(); } blur() { this.content.blur(); } selectAll() { if (document && typeof document.execCommand === 'function') { // Mimic input behavior when navigating to element with TAB key. setTimeout(() => { if (!this._wasClicked) { document.execCommand('selectAll', false, null); } }, 50); } } onFocus() { this.selectAll(); moveCursorToEnd(this.content); } onClick() { this._wasClicked = true; this.focus(); } onKeyDown(e) { if (e.keyCode === 27 || e.keyCode === 13) { e.preventDefault(); this.blur(); } } onBlur(e) { this._wasClicked = false; this.props.finishEdit(e.target.innerText); } ensureEmptyContent() { if (!this.props.value) { this.content.innerHTML = ''; } } render() { const {value, style, finishEdit, ...rest} = this.props; return ( <span ref={el => (this.content = el)} contentEditable='true' style={style} onFocus={this.onFocus} onBlur={this.onBlur} onClick={this.onClick} onKeyDown={this.onKeyDown} dangerouslySetInnerHTML={{__html: value || null}} {...rest} > </span> ); } } Object.assign(Multiline, { propTypes: commonPropTypes, defaultProps: commonDefaultProps }); class ReactPencil extends Component { constructor(props) { super(props); this.finishEdit = this.finishEdit.bind(this) } focus() { this.editable.focus(); } finishEdit(newValue = '') { const {value, name, multiline} = this.props; newValue = newValue.trim(); if (newValue !== value) { this.props.onEditDone(name, newValue); } if (multiline) { this.editable.ensureEmptyContent(); } } renderPencilButton() { return ( <button className='pencil-button' onClick={() => this.focus()}> <i className='pencil-icon'></i> </button> ); } renderError(error) { return <div className='error-msg'>{error}</div> } render() { const {multiline, pencil, error, wrapperClassname, onEditDone, ...rest} = this.props; const Component = multiline ? Multiline : Singleline; return ( <div className={`react-pencil${wrapperClassname ? ' ' + wrapperClassname : ''}${error ? ' error' : ''}`}> <div className='input-field'> <Component ref={el => (this.editable = el)} {...rest} finishEdit={this.finishEdit}/> {pencil ? this.renderPencilButton() : null} </div> {error ? this.renderError(error) : null} </div> ); } } Object.assign(ReactPencil, { propTypes: { error: PropTypes.string, multiline: PropTypes.bool, name: PropTypes.string, onEditDone: PropTypes.func, value: PropTypes.string }, defaultProps: { value: '', pencil: true, onEditDone: () => {} } }); export default ReactPencil;