aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--HISTORY.md9
-rw-r--r--assets/index.less22
-rw-r--r--assets/index/Combobox.less4
-rw-r--r--assets/index/Header.less54
-rw-r--r--assets/index/Picker.less4
-rw-r--r--assets/index/Select.less50
-rw-r--r--assets/index/TimePanel.less16
-rw-r--r--examples/pick-time.jsx36
-rw-r--r--index.js2
-rw-r--r--package.json66
-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
20 files changed, 971 insertions, 0 deletions
diff --git a/HISTORY.md b/HISTORY.md
new file mode 100644
index 0000000..e2ad063
--- /dev/null
+++ b/HISTORY.md
@@ -0,0 +1,9 @@
1History
2=======
3
4---
5
60.1.0 / 2015-11-12
7------------------
8
9`new` [#305](https://github.com/ant-design/ant-design/issues/305#issuecomment-147027817) release 0.1.0 ([@wuzhao](https://github.com/wuzhao)\)
diff --git a/assets/index.less b/assets/index.less
new file mode 100644
index 0000000..2ab247b
--- /dev/null
+++ b/assets/index.less
@@ -0,0 +1,22 @@
1@prefixClass: rc-timepicker;
2
3.@{prefixClass} {
4 box-sizing: border-box;
5 * {
6 box-sizing: border-box;
7 }
8}
9
10@font-face {
11 font-family: 'anticon';
12 src: url('//at.alicdn.com/t/font_1434092639_4910953.eot');
13 /* IE9*/
14 src: url('//at.alicdn.com/t/font_1434092639_4910953.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ url('//at.alicdn.com/t/font_1434092639_4910953.woff') format('woff'), /* chrome、firefox */ url('//at.alicdn.com/t/font_1434092639_4910953.ttf') format('truetype'), /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/ url('//at.alicdn.com/t/font_1434092639_4910953.svg#iconfont') format('svg');
15 /* iOS 4.1- */
16}
17
18@import "./index/Picker";
19@import "./index/TimePanel";
20@import "./index/Header";
21@import "./index/Combobox";
22@import "./index/Select";
diff --git a/assets/index/Combobox.less b/assets/index/Combobox.less
new file mode 100644
index 0000000..a796d0a
--- /dev/null
+++ b/assets/index/Combobox.less
@@ -0,0 +1,4 @@
1.@{prefixClass} {
2 &-combobox {
3 }
4}
diff --git a/assets/index/Header.less b/assets/index/Header.less
new file mode 100644
index 0000000..ac3d662
--- /dev/null
+++ b/assets/index/Header.less
@@ -0,0 +1,54 @@
1.@{prefixClass} {
2 &-input {
3 margin: 0;
4 padding: 0;
5 border: 0;
6 width: 100%;
7 cursor: auto;
8 line-height: 1.5;
9 outline: 0;
10 border: 1px solid transparent;
11
12 &-wrap {
13 box-sizing: border-box;
14 position: relative;
15 padding: 6px;
16 border-bottom: 1px solid #e9e9e9;
17 }
18
19 &-invalid {
20 border-color: red;
21 }
22 }
23
24 &-clear-btn {
25 position: absolute;
26 right: 6px;
27 cursor: pointer;
28 overflow: hidden;
29 width: 20px;
30 height: 20px;
31 text-align: center;
32 line-height: 20px;
33 top: 6px;
34 margin: 0;
35 }
36
37 &-clear-btn:after {
38 content: "x";
39 font-size: 12px;
40 color: #aaa;
41 display: inline-block;
42 line-height: 1;
43 width: 20px;
44 transition: color 0.3s ease;
45 }
46
47 &-clear-btn:hover:after {
48 color: #666;
49 }
50}
51
52.narrow .@{prefixClass}-input-wrap {
53 max-width: 111px;
54}
diff --git a/assets/index/Picker.less b/assets/index/Picker.less
new file mode 100644
index 0000000..769c4b7
--- /dev/null
+++ b/assets/index/Picker.less
@@ -0,0 +1,4 @@
1.@{prefixClass} {
2 &-picker {
3 }
4}
diff --git a/assets/index/Select.less b/assets/index/Select.less
new file mode 100644
index 0000000..995d09e
--- /dev/null
+++ b/assets/index/Select.less
@@ -0,0 +1,50 @@
1.@{prefixClass}-select {
2 float: left;
3 overflow-y:auto;
4 font-size: 12px;
5 border: 1px solid #e9e9e9;
6 border-width: 0 1px;
7 margin-left: -1px;
8 box-sizing: border-box;
9 width: 56px;
10
11 &:first-child {
12 border-left: 0;
13 margin-left: 0;
14 }
15
16 &:last-child {
17 border-right: 0;
18 }
19
20 ul {
21 list-style: none;
22 box-sizing: border-box;
23 margin: 0;
24 padding: 0;
25 width: 100%;
26 max-height: 144px;
27 }
28
29 li {
30 list-style: none;
31 box-sizing: border-box;
32 margin: 0;
33 padding: 0 0 0 16px;
34 width: 100%;
35 height: 24px;
36 line-height: 24px;
37 text-align: left;
38 cursor: pointer;
39 user-select: none;
40
41 &.selected {
42 background: #edfaff;
43 color: #2db7f5;
44 }
45
46 &:hover {
47 background: #edfaff;
48 }
49 }
50}
diff --git a/assets/index/TimePanel.less b/assets/index/TimePanel.less
new file mode 100644
index 0000000..574605f
--- /dev/null
+++ b/assets/index/TimePanel.less
@@ -0,0 +1,16 @@
1.@{prefixClass}-panel {
2 display: inline-block;
3 position: relative;
4 outline: none;
5 font-family: Arial, "Hiragino Sans GB", "Microsoft Yahei", "Microsoft Sans Serif", "WenQuanYi Micro Hei", sans-serif;
6 border: 1px solid #ccc;
7 list-style: none;
8 font-size: 12px;
9 text-align: left;
10 background-color: #fff;
11 border-radius: 3px;
12 box-shadow: 0 1px 5px #ccc;
13 background-clip: padding-box;
14 border: 1px solid #ccc;
15 line-height: 1.5;
16}
diff --git a/examples/pick-time.jsx b/examples/pick-time.jsx
new file mode 100644
index 0000000..33c5d07
--- /dev/null
+++ b/examples/pick-time.jsx
@@ -0,0 +1,36 @@
1import '../component/timepicker/assets/index.less';
2
3import React from 'react';
4import ReactDom from 'react-dom';
5import zhCn from 'gregorian-calendar/lib/locale/zh_CN';
6import GregorianCalendar from 'gregorian-calendar';
7
8import TimePicker from '../component/timepicker/src/Picker';
9import TimePanel from '../component/timepicker/src/TimePanel';
10import TimepickerLocale from '../component/timepicker/src/locale/zh_CN';
11import DateTimeFormat from 'gregorian-calendar-format';
12
13const formatter = new DateTimeFormat('HH:mm:ss');
14
15const now = new GregorianCalendar(zhCn);
16now.setTime(Date.now());
17
18const timePanel = (
19 <TimePanel
20 defaultValue={now}
21 locale={TimepickerLocale}
22 formatter={formatter}
23 minuteOptions={[0, 30]}
24 />
25);
26
27ReactDom.render(
28 <TimePicker panel={timePanel} value={now}>
29 {
30 ({value}) => {
31 return <input type="text" placeholder="请选择时间" readOnly value={value && formatter.format(value)} />;
32 }
33 }
34 </TimePicker>,
35 document.getElementById('react-content')
36);
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..4570919
--- /dev/null
+++ b/index.js
@@ -0,0 +1,2 @@
1import TimePanel from './src/';
2export default TimePanel;
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..3f7b765
--- /dev/null
+++ b/package.json
@@ -0,0 +1,66 @@
1{
2 "name": "rc-timepicker",
3 "version": "0.1.0",
4 "description": "React Timepicker",
5 "keywords": [
6 "react",
7 "react-timepicker",
8 "react-component",
9 "timepicker",
10 "ui component",
11 "ui",
12 "component"
13 ],
14 "files": [
15 "lib",
16 "assets/*.css"
17 ],
18 "main": "lib/index",
19 "homepage": "http://github.com/react-component/timepicker",
20 "author": "wuzhao.mail@gmail.com",
21 "repository": {
22 "type": "git",
23 "url": "git@github.com:react-component/timepicker.git"
24 },
25 "bugs": {
26 "url": "http://github.com/react-component/timepicker/issues"
27 },
28 "licenses": "MIT",
29 "config": {
30 "port": 8001
31 },
32 "scripts": {
33 "build": "rc-tools run build",
34 "gh-pages": "rc-tools run gh-pages",
35 "start": "rc-server",
36 "pub": "rc-tools run pub",
37 "lint": "rc-tools run lint",
38 "karma": "rc-tools run karma",
39 "saucelabs": "rc-tools run saucelabs",
40 "browser-test": "rc-tools run browser-test",
41 "browser-test-cover": "rc-tools run browser-test-cover",
42 "validate": "npm ls"
43 },
44 "devDependencies": {
45 "async": "~0.9.0",
46 "bootstrap": "~3.3.2",
47 "expect.js": "~0.3.1",
48 "jquery": "~1.11.3",
49 "pre-commit": "1.x",
50 "rc-server": "3.x",
51 "rc-tools": "4.x",
52 "react": "0.14.x",
53 "react-addons-test-utils": "~0.14.0",
54 "react-dom": "0.14.x"
55 },
56 "pre-commit": [
57 "lint"
58 ],
59 "dependencies": {
60 "gregorian-calendar": "4.x",
61 "gregorian-calendar-format": "4.x",
62 "object-assign": "4.x",
63 "rc-trigger": "1.x",
64 "rc-util": "2.x"
65 }
66}
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;