aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Picker.jsx158
-rw-r--r--src/TimePanel.jsx121
-rw-r--r--src/locale/en_US.js9
-rw-r--r--src/locale/zh_CN.js9
-rw-r--r--src/mixin/CommonMixin.js46
-rw-r--r--src/module/Combobox.jsx95
-rw-r--r--src/module/Header.jsx139
-rw-r--r--src/module/Select.jsx88
-rw-r--r--src/util/index.js8
-rw-r--r--src/util/placements.js35
10 files changed, 708 insertions, 0 deletions
diff --git a/src/Picker.jsx b/src/Picker.jsx
new file mode 100644
index 0000000..f15434a
--- /dev/null
+++ b/src/Picker.jsx
@@ -0,0 +1,158 @@
1import React, {PropTypes} from 'react';
2import ReactDOM from 'react-dom';
3import Trigger from 'rc-trigger';
4import {createChainedFunction} from 'rc-util';
5import placements from './util/placements';
6import CommonMixin from './mixin/CommonMixin';
7
8function noop() {
9}
10
11function refFn(field, component) {
12 this[field] = component;
13}
14
15const Picker = React.createClass({
16 propTypes: {
17 prefixCls: PropTypes.string,
18 panel: PropTypes.element,
19 children: PropTypes.func,
20 disabled: PropTypes.bool,
21 value: PropTypes.object,
22 open: PropTypes.bool,
23 align: PropTypes.object,
24 placement: PropTypes.any,
25 transitionName: PropTypes.string,
26 onChange: PropTypes.func,
27 onOpen: PropTypes.func,
28 onClose: PropTypes.func,
29 },
30
31 mixins: [CommonMixin],
32
33 getDefaultProps() {
34 return {
35 open: false,
36 align: {},
37 placement: 'bottomLeft',
38 onChange: noop,
39 onOpen: noop,
40 onClose: noop,
41 };
42 },
43
44 getInitialState() {
45 this.savePanelRef = refFn.bind(this, 'panelInstance');
46 const { open, value } = this.props;
47 return { open, value };
48 },
49
50 componentWillMount() {
51 document.addEventListener('click', this.handleDocumentClick, false);
52 },
53
54 componentWillReceiveProps(nextProps) {
55 const { value, open } = nextProps;
56 if (value !== undefined) {
57 this.setState({value});
58 }
59 if (open !== undefined) {
60 this.setState({open});
61 }
62 },
63
64 componentWillUnmount() {
65 document.removeEventListener('click', this.handleDocumentClick, false);
66 },
67
68 onPanelChange(value) {
69 const props = this.props;
70 this.setState({
71 value: value,
72 });
73 props.onChange(value);
74 },
75
76 onPanelClear() {
77 this.setOpen(false, this.focus);
78 },
79
80 onVisibleChange(open) {
81 this.setOpen(open, () => {
82 if (open) {
83 ReactDOM.findDOMNode(this.panelInstance).focus();
84 }
85 });
86 },
87
88 getPanelElement() {
89 const panel = this.props.panel;
90 const extraProps = {
91 ref: this.savePanelRef,
92 defaultValue: this.state.value || panel.props.defaultValue,
93 onChange: createChainedFunction(panel.props.onChange, this.onPanelChange),
94 onClear: createChainedFunction(panel.props.onClear, this.onPanelClear),
95 };
96
97 return React.cloneElement(panel, extraProps);
98 },
99
100 setOpen(open, callback) {
101 const {onOpen, onClose} = this.props;
102 if (this.state.open !== open) {
103 this.setState({
104 open: open,
105 }, callback);
106 const event = {
107 open: open,
108 };
109 if (open) {
110 onOpen(event);
111 } else {
112 onClose(event);
113 }
114 }
115 },
116
117 handleDocumentClick(event) {
118 // hide popup when click outside
119 if (this.state.open && ReactDOM.findDOMNode(this.panelInstance).contains(event.target)) {
120 return;
121 }
122 this.setState({
123 open: false,
124 });
125 },
126
127 focus() {
128 if (!this.state.open) {
129 ReactDOM.findDOMNode(this).focus();
130 }
131 },
132
133 render() {
134 const state = this.state;
135 const props = this.props;
136 const { prefixCls, placement, align, disabled, transitionName, children } = props;
137 return (
138 <Trigger
139 prefixCls={prefixCls}
140 popup={this.getPanelElement()}
141 popupAlign={align}
142 builtinPlacements={placements}
143 popupPlacement={placement}
144 action={disabled ? [] : ['click']}
145 destroyPopupOnHide
146 popupTransitionName={transitionName}
147 popupVisible={state.open}
148 onPopupVisibleChange={this.onVisibleChange}
149 >
150 <span className={`${prefixCls}-picker`}>
151 {children(state, props)}
152 </span>
153 </Trigger>
154 );
155 },
156});
157
158export default Picker;
diff --git a/src/TimePanel.jsx b/src/TimePanel.jsx
new file mode 100644
index 0000000..dad8036
--- /dev/null
+++ b/src/TimePanel.jsx
@@ -0,0 +1,121 @@
1import React, {PropTypes} from 'react';
2import classnames from 'classnames';
3import CommonMixin from './mixin/CommonMixin';
4import Header from './module/Header';
5import Combobox from './module/Combobox';
6
7function noop() {
8}
9
10function generateOptions(length) {
11 return Array.apply(null, {length: length}).map((item, index) => {
12 return index;
13 });
14}
15
16const TimePanel = React.createClass({
17 propTypes: {
18 prefixCls: PropTypes.string,
19 defaultValue: PropTypes.object,
20 locale: PropTypes.object,
21 placeholder: PropTypes.string,
22 formatter: PropTypes.object,
23 hourOptions: PropTypes.array,
24 minuteOptions: PropTypes.array,
25 secondOptions: PropTypes.array,
26 onChange: PropTypes.func,
27 onClear: PropTypes.func,
28 },
29
30 mixins: [CommonMixin],
31
32 getDefaultProps() {
33 return {
34 hourOptions: generateOptions(24),
35 minuteOptions: generateOptions(60),
36 secondOptions: generateOptions(60),
37 onChange: noop,
38 onClear: noop,
39 };
40 },
41
42 getInitialState() {
43 return {
44 value: this.props.defaultValue,
45 };
46 },
47
48 componentWillMount() {
49 const formatter = this.props.formatter;
50 const pattern = formatter.originalPattern;
51 if (pattern === 'HH:mm') {
52 this.showSecond = false;
53 } else if (pattern === 'mm:ss') {
54 this.showHour = false;
55 }
56 },
57
58 onChange(newValue) {
59 this.setState({ value: newValue });
60 this.props.onChange(newValue);
61 },
62
63 onClear() {
64 this.props.onClear();
65 },
66
67 getPlaceholder(placeholder) {
68 if (placeholder) {
69 return placeholder;
70 }
71
72 const { locale } = this.props;
73 if (!this.showHour) {
74 return locale.placeholdermmss;
75 } else if (!this.showSecond) {
76 return locale.placeholderHHmm;
77 }
78 return locale.placeholderHHmmss;
79 },
80
81 showHour: true,
82 showSecond: true,
83
84 render() {
85 const { locale, prefixCls, defaultValue, placeholder, hourOptions, minuteOptions, secondOptions } = this.props;
86 const value = this.state.value || defaultValue;
87 const cls = classnames({ 'narrow': !this.showHour || !this.showSecond });
88
89 return (
90 <div className={`${prefixCls}-panel ${cls}`}>
91 <Header
92 prefixCls={prefixCls}
93 gregorianTimepickerLocale={defaultValue.locale}
94 locale={locale}
95 value={value}
96 formatter={this.getFormatter()}
97 placeholder={this.getPlaceholder(placeholder)}
98 hourOptions={hourOptions}
99 minuteOptions={minuteOptions}
100 secondOptions={secondOptions}
101 onChange={this.onChange}
102 onClear={this.onClear}
103 showClear
104 />
105 <Combobox
106 prefixCls={prefixCls}
107 value={value}
108 formatter={this.getFormatter()}
109 onChange={this.onChange}
110 showHour={this.showHour}
111 showSecond={this.showSecond}
112 hourOptions={hourOptions}
113 minuteOptions={minuteOptions}
114 secondOptions={secondOptions}
115 />
116 </div>
117 );
118 },
119});
120
121export default TimePanel;
diff --git a/src/locale/en_US.js b/src/locale/en_US.js
new file mode 100644
index 0000000..252d3d2
--- /dev/null
+++ b/src/locale/en_US.js
@@ -0,0 +1,9 @@
1import enUs from 'gregorian-calendar-format/lib/locale/en_US';
2
3export default {
4 placeholderHHmmss: 'HH:MM:SS',
5 placeholderHHmm: 'HH:MM',
6 placeholdermmss: 'MM:SS',
7 clear: 'Clear',
8 format: enUs,
9};
diff --git a/src/locale/zh_CN.js b/src/locale/zh_CN.js
new file mode 100644
index 0000000..709cfb4
--- /dev/null
+++ b/src/locale/zh_CN.js
@@ -0,0 +1,9 @@
1import zhCn from 'gregorian-calendar-format/lib/locale/zh_CN';
2
3export default {
4 placeholderHHmmss: '时:分:秒',
5 placeholderHHmm: '时:分',
6 placeholdermmss: '分:秒',
7 clear: '清除',
8 format: zhCn,
9};
diff --git a/src/mixin/CommonMixin.js b/src/mixin/CommonMixin.js
new file mode 100644
index 0000000..4203a9e
--- /dev/null
+++ b/src/mixin/CommonMixin.js
@@ -0,0 +1,46 @@
1import {PropTypes} from 'react';
2import enUs from '../locale/en_US';
3import {getFormatter} from '../util/index';
4
5export default {
6 propTypes: {
7 prefixCls: PropTypes.string,
8 locale: PropTypes.object,
9 },
10
11 getDefaultProps() {
12 return {
13 prefixCls: 'rc-timepicker',
14 locale: enUs,
15 };
16 },
17
18 getFormatter() {
19 const formatter = this.props.formatter;
20 const locale = this.props.locale;
21 if (formatter) {
22 if (formatter === this.lastFormatter) {
23 return this.normalFormatter;
24 }
25 this.normalFormatter = getFormatter(formatter, locale);
26 this.lastFormatter = formatter;
27 return this.normalFormatter;
28 }
29 if (!this.showSecond) {
30 if (!this.notShowSecondFormatter) {
31 this.notShowSecondFormatter = getFormatter('HH:mm', locale);
32 }
33 return this.notShowSecondFormatter;
34 }
35 if (!this.showHour) {
36 if (!this.notShowHourFormatter) {
37 this.notShowHourFormatter = getFormatter('mm:ss', locale);
38 }
39 return this.notShowHourFormatter;
40 }
41 if (!this.normalFormatter) {
42 this.normalFormatter = getFormatter('HH:mm:ss', locale);
43 }
44 return this.normalFormatter;
45 },
46};
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 @@
1import React, {PropTypes} from 'react';
2import Select from './Select';
3
4const formatOption = (option) => {
5 if (option < 10) {
6 return `0${option}`;
7 }
8 return `${option}`;
9};
10
11const 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
95export 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 @@
1import React, {PropTypes} from 'react';
2
3const 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
139export 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 @@
1import React, {PropTypes} from 'react';
2import ReactDom from 'react-dom';
3import classnames from 'classnames';
4
5const 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
21const 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
88export default Select;
diff --git a/src/util/index.js b/src/util/index.js
new file mode 100644
index 0000000..5bc0a78
--- /dev/null
+++ b/src/util/index.js
@@ -0,0 +1,8 @@
1import DateTimeFormat from 'gregorian-calendar-format';
2
3export function getFormatter(format, locale) {
4 if (typeof format === 'string') {
5 return new DateTimeFormat(format, locale.format);
6 }
7 return format;
8}
diff --git a/src/util/placements.js b/src/util/placements.js
new file mode 100644
index 0000000..2574da1
--- /dev/null
+++ b/src/util/placements.js
@@ -0,0 +1,35 @@
1const autoAdjustOverflow = {
2 adjustX: 1,
3 adjustY: 1,
4};
5
6const targetOffset = [0, 0];
7
8const placements = {
9 topLeft: {
10 points: ['tl', 'tl'],
11 overflow: autoAdjustOverflow,
12 offset: [0, -3],
13 targetOffset,
14 },
15 topRight: {
16 points: ['tr', 'tr'],
17 overflow: autoAdjustOverflow,
18 offset: [0, -3],
19 targetOffset,
20 },
21 bottomRight: {
22 points: ['br', 'br'],
23 overflow: autoAdjustOverflow,
24 offset: [0, 3],
25 targetOffset,
26 },
27 bottomLeft: {
28 points: ['bl', 'bl'],
29 overflow: autoAdjustOverflow,
30 offset: [0, 3],
31 targetOffset,
32 },
33};
34
35export default placements;