diff options
Diffstat (limited to 'src/module')
-rw-r--r-- | src/module/Combobox.jsx | 95 | ||||
-rw-r--r-- | src/module/Header.jsx | 139 | ||||
-rw-r--r-- | src/module/Select.jsx | 88 |
3 files changed, 322 insertions, 0 deletions
diff --git a/src/module/Combobox.jsx b/src/module/Combobox.jsx new file mode 100644 index 0000000..e6fe5ed --- /dev/null +++ b/src/module/Combobox.jsx | |||
@@ -0,0 +1,95 @@ | |||
1 | import React, {PropTypes} from 'react'; | ||
2 | import Select from './Select'; | ||
3 | |||
4 | const formatOption = (option) => { | ||
5 | if (option < 10) { | ||
6 | return `0${option}`; | ||
7 | } | ||
8 | return `${option}`; | ||
9 | }; | ||
10 | |||
11 | const Combobox = React.createClass({ | ||
12 | propTypes: { | ||
13 | formatter: PropTypes.object, | ||
14 | prefixCls: PropTypes.string, | ||
15 | value: PropTypes.object, | ||
16 | onChange: PropTypes.func, | ||
17 | showHour: PropTypes.bool, | ||
18 | showSecond: PropTypes.bool, | ||
19 | hourOptions: PropTypes.array, | ||
20 | minuteOptions: PropTypes.array, | ||
21 | secondOptions: PropTypes.array, | ||
22 | }, | ||
23 | |||
24 | onItemChange(type, itemValue) { | ||
25 | const { value, onChange } = this.props; | ||
26 | let index = 4; | ||
27 | if (type === 'minute') { | ||
28 | index = 5; | ||
29 | } else if (type === 'second') { | ||
30 | index = 6; | ||
31 | } | ||
32 | value.fields[index] = itemValue; | ||
33 | onChange(value); | ||
34 | }, | ||
35 | |||
36 | getHourSelect(hour) { | ||
37 | const { prefixCls, hourOptions, showHour } = this.props; | ||
38 | if (!showHour) { | ||
39 | return null; | ||
40 | } | ||
41 | return ( | ||
42 | <Select | ||
43 | prefixCls={prefixCls} | ||
44 | options={hourOptions.map(option => formatOption(option))} | ||
45 | selectedIndex={hourOptions.indexOf(hour)} | ||
46 | type="hour" | ||
47 | onSelect={this.onItemChange} | ||
48 | /> | ||
49 | ); | ||
50 | }, | ||
51 | |||
52 | getMinuteSelect(minute) { | ||
53 | const { prefixCls, minuteOptions } = this.props; | ||
54 | return ( | ||
55 | <Select | ||
56 | prefixCls={prefixCls} | ||
57 | options={minuteOptions.map(option => formatOption(option))} | ||
58 | selectedIndex={minuteOptions.indexOf(minute)} | ||
59 | type="minute" | ||
60 | onSelect={this.onItemChange} | ||
61 | /> | ||
62 | ); | ||
63 | }, | ||
64 | |||
65 | getSectionSelect(second) { | ||
66 | const { prefixCls, secondOptions, showSecond } = this.props; | ||
67 | if (!showSecond) { | ||
68 | return null; | ||
69 | } | ||
70 | return ( | ||
71 | <Select | ||
72 | prefixCls={prefixCls} | ||
73 | options={secondOptions.map(option => formatOption(option))} | ||
74 | selectedIndex={secondOptions.indexOf(second)} | ||
75 | type="second" | ||
76 | onSelect={this.onItemChange} | ||
77 | /> | ||
78 | ); | ||
79 | }, | ||
80 | |||
81 | render() { | ||
82 | const { prefixCls, value } = this.props; | ||
83 | const timeFields = value.fields; | ||
84 | |||
85 | return ( | ||
86 | <div className={`${prefixCls}-combobox`}> | ||
87 | {this.getHourSelect(timeFields[4])} | ||
88 | {this.getMinuteSelect(timeFields[5])} | ||
89 | {this.getSectionSelect(timeFields[6])} | ||
90 | </div> | ||
91 | ); | ||
92 | }, | ||
93 | }); | ||
94 | |||
95 | export default Combobox; | ||
diff --git a/src/module/Header.jsx b/src/module/Header.jsx new file mode 100644 index 0000000..f7e443f --- /dev/null +++ b/src/module/Header.jsx | |||
@@ -0,0 +1,139 @@ | |||
1 | import React, {PropTypes} from 'react'; | ||
2 | |||
3 | const Header = React.createClass({ | ||
4 | propTypes: { | ||
5 | formatter: PropTypes.object, | ||
6 | prefixCls: PropTypes.string, | ||
7 | gregorianTimepickerLocale: PropTypes.object, | ||
8 | locale: PropTypes.object, | ||
9 | disabledDate: PropTypes.func, | ||
10 | placeholder: PropTypes.string, | ||
11 | value: PropTypes.object, | ||
12 | hourOptions: PropTypes.array, | ||
13 | minuteOptions: PropTypes.array, | ||
14 | secondOptions: PropTypes.array, | ||
15 | onChange: PropTypes.func, | ||
16 | onClear: PropTypes.func, | ||
17 | showClear: PropTypes.bool, | ||
18 | }, | ||
19 | |||
20 | getInitialState() { | ||
21 | const value = this.props.value; | ||
22 | return { | ||
23 | str: value && this.props.formatter.format(value) || '', | ||
24 | invalid: false, | ||
25 | }; | ||
26 | }, | ||
27 | |||
28 | componentWillReceiveProps(nextProps) { | ||
29 | const value = this.formatValue(nextProps.value); | ||
30 | this.setState({ | ||
31 | str: value && nextProps.formatter.format(value) || '', | ||
32 | invalid: false, | ||
33 | }); | ||
34 | }, | ||
35 | |||
36 | onInputChange(event) { | ||
37 | const str = event.target.value; | ||
38 | this.setState({ | ||
39 | str, | ||
40 | }); | ||
41 | let value = null; | ||
42 | const {formatter, gregorianTimepickerLocale, hourOptions, minuteOptions, secondOptions, onChange} = this.props; | ||
43 | |||
44 | if (str) { | ||
45 | const originalValue = this.props.value; | ||
46 | try { | ||
47 | value = formatter.parse(str, { | ||
48 | locale: gregorianTimepickerLocale, | ||
49 | obeyCount: true, | ||
50 | }); | ||
51 | value = this.formatValue(value); | ||
52 | } catch (ex) { | ||
53 | this.setState({ | ||
54 | invalid: true, | ||
55 | }); | ||
56 | return; | ||
57 | } | ||
58 | |||
59 | if (value) { | ||
60 | if ( | ||
61 | hourOptions.indexOf(value.fields[4]) < 0 || | ||
62 | minuteOptions.indexOf(value.fields[5]) < 0 || | ||
63 | secondOptions.indexOf(value.fields[6]) < 0 | ||
64 | ) { | ||
65 | this.setState({ | ||
66 | invalid: true, | ||
67 | }); | ||
68 | return; | ||
69 | } | ||
70 | |||
71 | if (originalValue && value) { | ||
72 | if ( | ||
73 | originalValue.fields[4] !== value.fields[4] || | ||
74 | originalValue.fields[5] !== value.fields[5] || | ||
75 | originalValue.fields[6] !== value.fields[6] | ||
76 | ) { | ||
77 | onChange(value); | ||
78 | } | ||
79 | } else if (originalValue !== value) { | ||
80 | onChange(value); | ||
81 | } | ||
82 | } else { | ||
83 | this.setState({ | ||
84 | invalid: true, | ||
85 | }); | ||
86 | return; | ||
87 | } | ||
88 | } else { | ||
89 | onChange(null); | ||
90 | } | ||
91 | |||
92 | this.setState({ | ||
93 | invalid: false, | ||
94 | }); | ||
95 | }, | ||
96 | |||
97 | onClear() { | ||
98 | this.setState({str: ''}); | ||
99 | this.props.onClear(); | ||
100 | }, | ||
101 | |||
102 | getClearButton() { | ||
103 | const { locale, prefixCls, showClear } = this.props; | ||
104 | if (!showClear) { | ||
105 | return null; | ||
106 | } | ||
107 | return <a className={`${prefixCls}-clear-btn`} role="button" title={locale.clear} onMouseDown={this.onClear} />; | ||
108 | }, | ||
109 | |||
110 | getInput() { | ||
111 | const { prefixCls, placeholder } = this.props; | ||
112 | const { invalid, str } = this.state; | ||
113 | const invalidClass = invalid ? `${prefixCls}-input-invalid` : ''; | ||
114 | return <input className={`${prefixCls}-input ${invalidClass}`} value={str} placeholder={placeholder} onChange={this.onInputChange} />; | ||
115 | }, | ||
116 | |||
117 | formatValue(value) { | ||
118 | const newValue = this.props.value.clone(); | ||
119 | if (!value) { | ||
120 | return newValue; | ||
121 | } | ||
122 | newValue.fields[4] = value.fields[4]; | ||
123 | newValue.fields[5] = value.fields[5]; | ||
124 | newValue.fields[6] = value.fields[6]; | ||
125 | return newValue; | ||
126 | }, | ||
127 | |||
128 | render() { | ||
129 | const { prefixCls } = this.props; | ||
130 | return ( | ||
131 | <div className={`${prefixCls}-input-wrap`}> | ||
132 | {this.getInput()} | ||
133 | {this.getClearButton()} | ||
134 | </div> | ||
135 | ); | ||
136 | }, | ||
137 | }); | ||
138 | |||
139 | export default Header; | ||
diff --git a/src/module/Select.jsx b/src/module/Select.jsx new file mode 100644 index 0000000..2b69623 --- /dev/null +++ b/src/module/Select.jsx | |||
@@ -0,0 +1,88 @@ | |||
1 | import React, {PropTypes} from 'react'; | ||
2 | import ReactDom from 'react-dom'; | ||
3 | import classnames from 'classnames'; | ||
4 | |||
5 | const scrollTo = (element, to, duration) => { | ||
6 | // jump to target if duration zero | ||
7 | if (duration <= 0) { | ||
8 | element.scrollTop = to; | ||
9 | return; | ||
10 | } | ||
11 | const difference = to - element.scrollTop; | ||
12 | const perTick = difference / duration * 10; | ||
13 | |||
14 | setTimeout(() => { | ||
15 | element.scrollTop = element.scrollTop + perTick; | ||
16 | if (element.scrollTop === to) return; | ||
17 | scrollTo(element, to, duration - 10); | ||
18 | }, 10); | ||
19 | }; | ||
20 | |||
21 | const Select = React.createClass({ | ||
22 | propTypes: { | ||
23 | prefixCls: PropTypes.string, | ||
24 | options: PropTypes.array, | ||
25 | selectedIndex: PropTypes.number, | ||
26 | type: PropTypes.string, | ||
27 | onSelect: PropTypes.func, | ||
28 | }, | ||
29 | |||
30 | componentDidMount() { | ||
31 | // jump to selected option | ||
32 | this.scrollToSelected(0); | ||
33 | }, | ||
34 | |||
35 | componentDidUpdate() { | ||
36 | // smooth scroll to selected option | ||
37 | this.scrollToSelected(200); | ||
38 | }, | ||
39 | |||
40 | onSelect(event) { | ||
41 | // do nothing when select selected option | ||
42 | if (event.target.getAttribute('class') === 'selected') { | ||
43 | return; | ||
44 | } | ||
45 | // change combobox selection | ||
46 | const { onSelect, type } = this.props; | ||
47 | const value = parseInt(event.target.innerHTML, 10); | ||
48 | onSelect(type, value); | ||
49 | }, | ||
50 | |||
51 | getOptions() { | ||
52 | const { options, selectedIndex } = this.props; | ||
53 | return options.map((item, index) => { | ||
54 | const cls = classnames({ selected: selectedIndex === index}); | ||
55 | const ref = selectedIndex === index ? 'selected' : null; | ||
56 | return <li ref={ref} className={cls} key={index} onClick={this.onSelect}>{item}</li>; | ||
57 | }); | ||
58 | }, | ||
59 | |||
60 | scrollToSelected(duration) { | ||
61 | // move to selected item | ||
62 | const select = ReactDom.findDOMNode(this); | ||
63 | const list = ReactDom.findDOMNode(this.refs.list); | ||
64 | let index = this.props.selectedIndex - 2; | ||
65 | if (index < 0) { | ||
66 | index = 0; | ||
67 | } | ||
68 | const topOption = list.children[index]; | ||
69 | const to = topOption.offsetTop - select.offsetTop; | ||
70 | scrollTo(select, to, duration); | ||
71 | }, | ||
72 | |||
73 | render() { | ||
74 | if (this.props.options.length === 0) { | ||
75 | return null; | ||
76 | } | ||
77 | |||
78 | const { prefixCls } = this.props; | ||
79 | |||
80 | return ( | ||
81 | <div className={`${prefixCls}-select`}> | ||
82 | <ul ref="list">{this.getOptions()}</ul> | ||
83 | </div> | ||
84 | ); | ||
85 | }, | ||
86 | }); | ||
87 | |||
88 | export default Select; | ||