1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | 'use strict'
|
7 |
|
8 | import React, { Component, PropTypes as types } from 'react'
|
9 | import ReactDOM from 'react-dom'
|
10 | import classnames from 'classnames'
|
11 |
|
12 |
|
13 | class ApText extends Component {
|
14 |
|
15 |
|
16 |
|
17 | constructor (props) {
|
18 | super(props)
|
19 | const s = this
|
20 | s.state = {
|
21 | suggesting: false,
|
22 | candidates: null,
|
23 | selectedCandidate: null
|
24 | }
|
25 | let methodsToBind = [
|
26 | 'handleFocus',
|
27 | 'handleKeyUp',
|
28 | 'handleChange',
|
29 | 'handleBlur',
|
30 | 'handleKeyDown',
|
31 | 'handleCandidate'
|
32 | ]
|
33 | for (let name of methodsToBind) {
|
34 | s[ name ] = s[ name ].bind(s)
|
35 | }
|
36 | }
|
37 |
|
38 | render () {
|
39 | const s = this
|
40 | let { state, props } = s
|
41 | let {
|
42 | id,
|
43 | name,
|
44 | placeholder,
|
45 | autoFocus,
|
46 | className,
|
47 | value,
|
48 | rows
|
49 | } = props
|
50 | let hasVal = !!value
|
51 |
|
52 | let multiline = rows && (rows > 1)
|
53 |
|
54 | let {
|
55 | candidates,
|
56 | selectedCandidate,
|
57 | suggesting
|
58 | } = state
|
59 |
|
60 | let textHandlers = {
|
61 | onFocus: s.handleFocus,
|
62 | onKeyUp: s.handleKeyUp,
|
63 | onChange: s.handleChange,
|
64 | onBlur: s.handleBlur,
|
65 | onKeyDown: s.handleKeyDown
|
66 | }
|
67 | let candidateHandlers = {
|
68 | onClick: s.handleCandidate
|
69 | }
|
70 |
|
71 | return (
|
72 | <span className={ classnames('ap-text-wrap', { 'ap-text-wrap-empty': !hasVal }) }>
|
73 | <ApText.Text { ...{ id, name, value, placeholder, className, autoFocus, multiline, rows } }
|
74 | handlers={ textHandlers }
|
75 | />
|
76 | <ApText.CandidateList { ...{ suggesting, candidates, selectedCandidate, multiline } }
|
77 | handlers={ candidateHandlers }
|
78 | />
|
79 | </span>
|
80 | )
|
81 | }
|
82 |
|
83 |
|
84 |
|
85 |
|
86 |
|
87 | handleCandidate (e) {
|
88 | const s = this
|
89 | let { props } = s
|
90 | e.target.value = e.target.value || e.target.dataset.value
|
91 | s.setState({ suggesting: false })
|
92 | if (props.onChange) {
|
93 | props.onChange(e)
|
94 | }
|
95 | }
|
96 |
|
97 | handleFocus (e) {
|
98 | const s = this
|
99 | let { props } = s
|
100 | s.setState({ suggesting: true })
|
101 | s.updateCandidates()
|
102 | if (props.onFocus) {
|
103 | props.onFocus(e)
|
104 | }
|
105 | }
|
106 |
|
107 | handleChange (e) {
|
108 | const s = this
|
109 | let { props } = s
|
110 | s.setState({ suggesting: true })
|
111 | if (props.onChange) {
|
112 | props.onChange(e)
|
113 | }
|
114 | }
|
115 |
|
116 | handleBlur (e) {
|
117 | const s = this
|
118 | let { props } = s
|
119 | if (props.onBlur) {
|
120 | props.onBlur(e)
|
121 | }
|
122 | }
|
123 |
|
124 | handleKeyUp (e) {
|
125 | const s = this
|
126 | let { props } = s
|
127 | s.updateCandidates()
|
128 | if (props.onKeyUp) {
|
129 | props.onKeyUp(e)
|
130 | }
|
131 | }
|
132 |
|
133 | handleKeyDown (e) {
|
134 | const s = this
|
135 | let { props } = s
|
136 | switch (e.keyCode) {
|
137 | case 38:
|
138 | s.moveCandidateIndex(-1)
|
139 | break
|
140 | case 40:
|
141 | s.moveCandidateIndex(+1)
|
142 | break
|
143 | case 13:
|
144 | s.enterCandidate()
|
145 | break
|
146 | default:
|
147 | s.setState({ suggesting: true })
|
148 | break
|
149 | }
|
150 | if (props.onKeyDown) {
|
151 | props.onKeyDown(e)
|
152 | }
|
153 | }
|
154 |
|
155 | moveCandidateIndex (amount) {
|
156 | const s = this
|
157 | let { candidates, selectedCandidate } = s.state
|
158 | if (!candidates) {
|
159 | return
|
160 | }
|
161 | let index = candidates.indexOf(selectedCandidate) + amount
|
162 | let over = (index === -1) || (index === candidates.length)
|
163 | if (over) {
|
164 | return
|
165 | }
|
166 | s.setState({
|
167 | selectedCandidate: candidates[ index ] || null
|
168 | })
|
169 | }
|
170 |
|
171 | updateCandidates () {
|
172 | const s = this
|
173 | let { props } = s
|
174 | let { value } = props
|
175 | let candidates = (props.candidates || [])
|
176 | .filter((candidate) => !!candidate)
|
177 | .map((candidate) => String(candidate).trim())
|
178 | .filter((candidate) => !value || candidate.match(value))
|
179 |
|
180 | let hit = (candidates.length === 1) && (candidates[ 0 ] === value)
|
181 | if (hit) {
|
182 | candidates = null
|
183 | }
|
184 | s.setState({ candidates })
|
185 | }
|
186 |
|
187 | enterCandidate () {
|
188 | const s = this
|
189 | let { props } = s
|
190 | let { candidates, selectedCandidate } = s.state
|
191 | let valid = candidates && !!~candidates.indexOf(selectedCandidate)
|
192 | if (valid) {
|
193 | let target = { value: selectedCandidate }
|
194 | if (props.onChange) {
|
195 | props.onChange({ target })
|
196 | }
|
197 | s.setState({ suggesting: false })
|
198 | }
|
199 | }
|
200 |
|
201 |
|
202 |
|
203 |
|
204 | static Text ({ id, name, value, placeholder, className, autoFocus, multiline, handlers, rows }) {
|
205 | if (multiline) {
|
206 | return (
|
207 | <textarea autoFocus={ autoFocus }
|
208 | id={ id }
|
209 | name={ name }
|
210 | rows={ rows }
|
211 | placeholder={ placeholder }
|
212 | className={ classnames('ap-text ap-text-multiple', className) }
|
213 | value={ value }
|
214 | { ...handlers }
|
215 | onFocus={ null }
|
216 | >
|
217 | </textarea>
|
218 | )
|
219 | } else {
|
220 | return (
|
221 | <input autoFocus={ autoFocus }
|
222 | id={ id }
|
223 | name={ name }
|
224 | placeholder={ placeholder }
|
225 | className={ classnames('ap-text', className)}
|
226 | value={ value }
|
227 | { ...handlers }
|
228 | type='text'
|
229 | />
|
230 | )
|
231 | }
|
232 | }
|
233 |
|
234 | static CandidateList ({ suggesting, candidates, selectedCandidate, multiline, handlers }) {
|
235 | if (!suggesting) {
|
236 | return null
|
237 | }
|
238 | if (multiline) {
|
239 | console.warn('[ApText] Can not use candidates with multiline input.')
|
240 | return null
|
241 | }
|
242 |
|
243 | if (!candidates) {
|
244 | return null
|
245 | }
|
246 |
|
247 | if (!candidates.length) {
|
248 | return null
|
249 | }
|
250 |
|
251 | return (
|
252 | <ul className='ap-text-candidate-list'>
|
253 | {
|
254 | candidates.map((candidate) =>
|
255 | <li key={ candidate }
|
256 | className={ classnames('ap-text-candidate-list-item', {
|
257 | 'ap-text-candidate-list-item-selected': candidate === selectedCandidate
|
258 | }) }>
|
259 | <a { ...handlers }
|
260 | data-value={ candidate }>{ candidate }</a>
|
261 | </li>
|
262 | )
|
263 | }
|
264 | </ul>
|
265 | )
|
266 | }
|
267 | }
|
268 |
|
269 | Object.assign(ApText, {
|
270 |
|
271 |
|
272 |
|
273 |
|
274 | propTypes: {
|
275 |
|
276 | name: types.string,
|
277 |
|
278 | value: types.string,
|
279 |
|
280 | placeholder: types.string,
|
281 |
|
282 | rows: types.number,
|
283 |
|
284 | candidates: types.arrayOf(types.string)
|
285 | },
|
286 |
|
287 | defaultProps: {
|
288 | name: '',
|
289 | value: '',
|
290 | placeholder: '',
|
291 | rows: 1,
|
292 | candidates: null
|
293 | }
|
294 |
|
295 | })
|
296 |
|
297 | export default ApText
|