|
| 1 | +import React, { Component, PropTypes } from 'react'; |
| 2 | + |
| 3 | +export const LEFT_EXIT = 'LEFT_EXIT'; |
| 4 | +export const RIGHT_EXIT = 'RIGHT_EXIT'; |
| 5 | +export const ENTER_EXIT = 'ENTER_EXIT'; |
| 6 | +export const ESC_EXIT = 'ESC_EXIT'; |
| 7 | +export const TAB_EXIT = 'TAB_EXIT'; |
| 8 | +export const DEL_EXIT = 'DEL_EXIT'; |
| 9 | +export const BCSP_EXIT = 'BCSP_EXIT'; |
| 10 | + |
| 11 | +const HEIGHT_SHIFT = 8; |
| 12 | + |
| 13 | +const propTypes = { |
| 14 | + defaultValue: PropTypes.string, |
| 15 | + onSelect: PropTypes.func, |
| 16 | + handleFuncKeys: PropTypes.bool, |
| 17 | + hintText: PropTypes.string, |
| 18 | + hintColor: PropTypes.string, |
| 19 | + style: PropTypes.object, |
| 20 | + styleMouseOver: PropTypes.object, |
| 21 | + onMouseOver: PropTypes.func, |
| 22 | + onMouseOut: PropTypes.func, |
| 23 | + styleFocus: PropTypes.object, |
| 24 | + focus: PropTypes.number /* null*/, |
| 25 | + focusDelay: PropTypes.number /* null*/, |
| 26 | + onFocus: PropTypes.func, |
| 27 | + onBlur: PropTypes.func, |
| 28 | + onFuncKeyDown: PropTypes.func, |
| 29 | + onChange: PropTypes.func, |
| 30 | + onChangeHeight: PropTypes.func, |
| 31 | + onSelecting: PropTypes.func, |
| 32 | + disabled: PropTypes.bool, |
| 33 | +}; |
| 34 | + |
| 35 | +const defaultProps = { |
| 36 | + handleFuncKeys: false, |
| 37 | + hintText: 'hint text...', |
| 38 | + hintColor: 'rgba(57, 57, 57, 0.6)', |
| 39 | + style: {}, |
| 40 | + styleMouseOver: {}, |
| 41 | + onMouseOver: () => {}, |
| 42 | + onMouseOut: () => {}, |
| 43 | + styleFocus: {}, |
| 44 | + focus: null, /* =pos */ |
| 45 | + focusDelay: null, |
| 46 | + onFocus: () => {}, |
| 47 | + onBlur: () => {}, |
| 48 | + onFuncKeyDown: () => {}, |
| 49 | + onChange: () => {}, |
| 50 | + onChangeHeight: () => {}, |
| 51 | + onSelecting: () => {}, |
| 52 | +}; |
| 53 | + |
| 54 | +class SmartTextarea extends Component { |
| 55 | + constructor(props) { |
| 56 | + super(props); |
| 57 | + |
| 58 | + this.state = { |
| 59 | + text: props.defaultValue, |
| 60 | + height: 30, |
| 61 | + width: 100, |
| 62 | + onMouseOver: false, |
| 63 | + focusOn: false, |
| 64 | + }; |
| 65 | + this.shadow = null; |
| 66 | + this.input = null; |
| 67 | + this.ready = false; |
| 68 | + this.readyTrans = 1; |
| 69 | + |
| 70 | + this.styleComn = { |
| 71 | + width: '100%', |
| 72 | + font: 'inherit', |
| 73 | + padding: 0, |
| 74 | + border: 'none', |
| 75 | + resize: 'none', |
| 76 | + overflowY: 'hidden', |
| 77 | + outline: 'none', |
| 78 | + }; |
| 79 | + this.styleInput = { |
| 80 | + backgroundColor: 'transparent', |
| 81 | + color: 'rgb(66, 66, 66)', |
| 82 | + }; |
| 83 | + this.styleTransition = { transition: 'height 200ms cubic-bezier(0.23, 1, 0.32, 1) 0ms' }; |
| 84 | + |
| 85 | + this.selection = ''; |
| 86 | + |
| 87 | + this.focus = this.focus.bind(this); |
| 88 | + this.onmousemove = this.onmousemove.bind(this); |
| 89 | + this.updateComp = this.updateComp.bind(this); |
| 90 | + this.onkeyDown = this.onkeyDown.bind(this); |
| 91 | + this.onchange = this.onchange.bind(this); |
| 92 | + this.onfocus = this.onfocus.bind(this); |
| 93 | + this.onblur = this.onblur.bind(this); |
| 94 | + } |
| 95 | + |
| 96 | + componentDidMount() { |
| 97 | + this.updateComp(undefined, () => |
| 98 | + this.props.onChangeHeight(this.state.height + HEIGHT_SHIFT) |
| 99 | + ); |
| 100 | + } |
| 101 | + |
| 102 | + |
| 103 | + componentWillReceiveProps(nextProps) { |
| 104 | + if (this.props.focus !== null && this.props.focus !== undefined) { |
| 105 | + this.setState({ focusOn: true }); |
| 106 | + } |
| 107 | + this.updateComp((nextProps.defaultValue !== this.props.defaultValue) ? |
| 108 | + nextProps.defaultValue : null); |
| 109 | + } |
| 110 | + |
| 111 | + |
| 112 | + componentWillUpdate(nextProps, nextState) { |
| 113 | + if (nextState.height !== this.state.height) { |
| 114 | + this.props.onChangeHeight(nextState.height + HEIGHT_SHIFT); |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + componentDidUpdate(prevProps) { |
| 119 | + if (this.props.focus !== null && !prevProps.focus) { |
| 120 | + if (this.props.focusDelay) { |
| 121 | + setTimeout(() => {this.focus(this.props.focus);}, this.props.focusDelay); |
| 122 | + } else { |
| 123 | + this.focus(this.props.focus); |
| 124 | + } |
| 125 | + } |
| 126 | + } |
| 127 | + |
| 128 | + onkeyDown(event) { |
| 129 | + if (!this.props.handleFuncKeys) return; |
| 130 | + const stopit = () => {event.stopPropagation(); event.preventDefault(); }; |
| 131 | + |
| 132 | + const keyCode = event.keyCode; |
| 133 | + const selectionStart = this.input.selectionStart; |
| 134 | + const textLength = this.input.textLength; |
| 135 | + |
| 136 | + if ((keyCode === 37 || keyCode === 38) && selectionStart === 0) { |
| 137 | + stopit(); |
| 138 | + this.props.onFuncKeyDown(keyCode, LEFT_EXIT, selectionStart); |
| 139 | + } |
| 140 | + if ((keyCode === 39 || keyCode === 40) && selectionStart === textLength) { |
| 141 | + stopit(); |
| 142 | + this.props.onFuncKeyDown(keyCode, RIGHT_EXIT, selectionStart); |
| 143 | + } |
| 144 | + if (keyCode === 13) { |
| 145 | + stopit(); |
| 146 | + this.props.onFuncKeyDown(keyCode, ENTER_EXIT, selectionStart); |
| 147 | + } |
| 148 | + if (keyCode === 27) { |
| 149 | + stopit(); |
| 150 | + this.props.onFuncKeyDown(keyCode, ESC_EXIT, selectionStart); |
| 151 | + } |
| 152 | + if (keyCode === 46 && selectionStart === textLength) { |
| 153 | + stopit(); |
| 154 | + this.props.onFuncKeyDown(keyCode, DEL_EXIT, selectionStart); |
| 155 | + } |
| 156 | + if (keyCode === 8 && selectionStart === 0) { |
| 157 | + stopit(); |
| 158 | + this.props.onFuncKeyDown(keyCode, BCSP_EXIT, selectionStart); |
| 159 | + } |
| 160 | + if (keyCode === 9) { |
| 161 | + stopit(); |
| 162 | + this.props.onFuncKeyDown(keyCode, TAB_EXIT, selectionStart); |
| 163 | + } |
| 164 | + } |
| 165 | + |
| 166 | + onchange(event) { |
| 167 | + const input = event.target; |
| 168 | + this.updateComp(input.value); |
| 169 | + this.props.onChange(input.value); |
| 170 | + } |
| 171 | + |
| 172 | + onfocus(event) { |
| 173 | + this.setState({ focusOn: true }); |
| 174 | + this.props.onFocus(event); |
| 175 | + } |
| 176 | + |
| 177 | + onblur(event) { |
| 178 | + this.setState({ focusOn: false }); |
| 179 | + this.props.onBlur(event); |
| 180 | + } |
| 181 | + |
| 182 | + onmousemove() { |
| 183 | + const selection = this.input.value.slice(this.input.selectionStart, |
| 184 | + this.input.selectionEnd); |
| 185 | + if (this.selection !== selection) { |
| 186 | + this.selection = selection; |
| 187 | + this.props.onSelecting(this.selection); |
| 188 | + } |
| 189 | + } |
| 190 | + |
| 191 | + focus(pos) { |
| 192 | + if (this.input) { |
| 193 | + this.input.focus(); |
| 194 | + const position = (pos >= 0) ? pos : this.input.textLength + pos + 1; |
| 195 | + this.input.setSelectionRange(position, position); // fixme |
| 196 | + } |
| 197 | + } |
| 198 | + |
| 199 | + updateComp(text, callback) { |
| 200 | + let updText = text; |
| 201 | + if (updText == undefined) { |
| 202 | + updText = this.state.text; |
| 203 | + } |
| 204 | + this.shadow.value = updText; |
| 205 | + this.setState({ |
| 206 | + text: updText, |
| 207 | + height: this.shadow.scrollHeight, |
| 208 | + width: this.input.clientWidth, |
| 209 | + }, callback); |
| 210 | + } |
| 211 | + |
| 212 | + |
| 213 | + textareaInit(elem) { |
| 214 | + if (!elem) return; |
| 215 | + if (this.shadow && this.input) return; |
| 216 | + |
| 217 | + if (elem.name === 'shadow') { |
| 218 | + this.shadow = elem; |
| 219 | + } |
| 220 | + if (elem.name === 'input') { |
| 221 | + this.input = elem; |
| 222 | + } |
| 223 | + } |
| 224 | + |
| 225 | + |
| 226 | + render() { |
| 227 | + const styleMouseOver = this.state.onMouseOver ? this.props.styleMouseOver : {}; |
| 228 | + const styleFocus = this.state.focusOn ? this.props.styleFocus : {}; |
| 229 | + const styleTrans = this.readyTrans < 0 ? this.styleTransition : {}; |
| 230 | + const styleDiv = { |
| 231 | + marginTop: 0, |
| 232 | + marginLeft: 0, |
| 233 | + height: this.state.height + HEIGHT_SHIFT, |
| 234 | + ...styleTrans, |
| 235 | + ...this.props.style, |
| 236 | + ...styleMouseOver, |
| 237 | + ...styleFocus, |
| 238 | + }; |
| 239 | + const emptyText = this.state.focusOn ? '' : this.props.hintText; |
| 240 | + const emptyColr = this.state.focusOn ? '' : this.props.hintColor; |
| 241 | + this.readyTrans--; |
| 242 | + return ( |
| 243 | + <div |
| 244 | + name="SmartTextarea" |
| 245 | + className="smart-textarea" |
| 246 | + style={styleDiv} |
| 247 | + onMouseOver={(e) => this.setState({ onMouseOver: true }, this.props.onMouseOver(e))} |
| 248 | + onMouseOut ={(e) => this.setState({ onMouseOver: false }, this.props.onMouseOut(e))} |
| 249 | + onMouseMove={this.onmousemove} |
| 250 | + > |
| 251 | + <div style={{ margin: 0, height: this.state.height + HEIGHT_SHIFT, overflow: 'hidden' }}> |
| 252 | + |
| 253 | + <textarea |
| 254 | + name="input" |
| 255 | + className="smart-textarea-input" |
| 256 | + type="text" |
| 257 | + value={this.state.text || emptyText} |
| 258 | + onKeyDown={this.onkeyDown} |
| 259 | + onChange={this.onchange} |
| 260 | + onFocus={this.onfocus} |
| 261 | + onBlur ={this.onblur} |
| 262 | + rows="1" |
| 263 | + style={{ |
| 264 | + marginTop: 2, |
| 265 | + height: this.state.height + 0, |
| 266 | + ...this.styleComn, |
| 267 | + ...this.styleInput, |
| 268 | + color: this.state.text ? this.props.style.color : emptyColr, |
| 269 | + }} |
| 270 | + ref={this.textareaInit.bind(this)} |
| 271 | + disabled={this.props.disabled} |
| 272 | + onSelect={this.props.onSelect} |
| 273 | + /> |
| 274 | + |
| 275 | + <textarea |
| 276 | + name="shadow" |
| 277 | + type="text" |
| 278 | + value={this.state.text || emptyText} |
| 279 | + rows="1" |
| 280 | + onChange={() => (null)} |
| 281 | + style={{ visibility: 'hidden', height: 1, marginTop: -5, |
| 282 | + top: -15, position: 'relative', |
| 283 | + ...this.styleComn }} |
| 284 | + ref={this.textareaInit.bind(this)} |
| 285 | + /> |
| 286 | + |
| 287 | + </div> |
| 288 | + |
| 289 | + </div>); |
| 290 | + } |
| 291 | + |
| 292 | +} |
| 293 | + |
| 294 | + |
| 295 | +SmartTextarea.propTypes = propTypes; |
| 296 | +SmartTextarea.defaultProps = defaultProps; |
| 297 | + |
| 298 | +export default SmartTextarea; |
| 299 | + |
| 300 | +/* |
| 301 | +<div style={{'-webkit-user-modify':'read-write'}}>{this.state.text}</div> |
| 302 | +
|
| 303 | +*/ |
| 304 | + |
0 commit comments