diff options
author | 崖鹰 <zhao.wuz@alipay.com> | 2015-11-13 11:33:48 +0800 |
---|---|---|
committer | 崖鹰 <zhao.wuz@alipay.com> | 2015-11-13 11:33:48 +0800 |
commit | 02de449a0474765a4796fa607e7e3922252f574f (patch) | |
tree | dc37faf2f610343112ea1fc3707ad188092bd031 | |
parent | 1f336fabc9135ac971e53d9c2ae407db69b8f096 (diff) | |
download | time-picker-02de449a0474765a4796fa607e7e3922252f574f.tar.gz time-picker-02de449a0474765a4796fa607e7e3922252f574f.tar.zst time-picker-02de449a0474765a4796fa607e7e3922252f574f.zip |
release 0.1.0
-rw-r--r-- | HISTORY.md | 9 | ||||
-rw-r--r-- | assets/index.less | 22 | ||||
-rw-r--r-- | assets/index/Combobox.less | 4 | ||||
-rw-r--r-- | assets/index/Header.less | 54 | ||||
-rw-r--r-- | assets/index/Picker.less | 4 | ||||
-rw-r--r-- | assets/index/Select.less | 50 | ||||
-rw-r--r-- | assets/index/TimePanel.less | 16 | ||||
-rw-r--r-- | examples/pick-time.jsx | 36 | ||||
-rw-r--r-- | index.js | 2 | ||||
-rw-r--r-- | package.json | 66 | ||||
-rw-r--r-- | src/Picker.jsx | 158 | ||||
-rw-r--r-- | src/TimePanel.jsx | 121 | ||||
-rw-r--r-- | src/locale/en_US.js | 9 | ||||
-rw-r--r-- | src/locale/zh_CN.js | 9 | ||||
-rw-r--r-- | src/mixin/CommonMixin.js | 46 | ||||
-rw-r--r-- | src/module/Combobox.jsx | 95 | ||||
-rw-r--r-- | src/module/Header.jsx | 139 | ||||
-rw-r--r-- | src/module/Select.jsx | 88 | ||||
-rw-r--r-- | src/util/index.js | 8 | ||||
-rw-r--r-- | src/util/placements.js | 35 |
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 @@ | |||
1 | History | ||
2 | ======= | ||
3 | |||
4 | --- | ||
5 | |||
6 | 0.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 @@ | |||
1 | import '../component/timepicker/assets/index.less'; | ||
2 | |||
3 | import React from 'react'; | ||
4 | import ReactDom from 'react-dom'; | ||
5 | import zhCn from 'gregorian-calendar/lib/locale/zh_CN'; | ||
6 | import GregorianCalendar from 'gregorian-calendar'; | ||
7 | |||
8 | import TimePicker from '../component/timepicker/src/Picker'; | ||
9 | import TimePanel from '../component/timepicker/src/TimePanel'; | ||
10 | import TimepickerLocale from '../component/timepicker/src/locale/zh_CN'; | ||
11 | import DateTimeFormat from 'gregorian-calendar-format'; | ||
12 | |||
13 | const formatter = new DateTimeFormat('HH:mm:ss'); | ||
14 | |||
15 | const now = new GregorianCalendar(zhCn); | ||
16 | now.setTime(Date.now()); | ||
17 | |||
18 | const timePanel = ( | ||
19 | <TimePanel | ||
20 | defaultValue={now} | ||
21 | locale={TimepickerLocale} | ||
22 | formatter={formatter} | ||
23 | minuteOptions={[0, 30]} | ||
24 | /> | ||
25 | ); | ||
26 | |||
27 | ReactDom.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 @@ | |||
1 | import TimePanel from './src/'; | ||
2 | export 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 @@ | |||
1 | import React, {PropTypes} from 'react'; | ||
2 | import ReactDOM from 'react-dom'; | ||
3 | import Trigger from 'rc-trigger'; | ||
4 | import {createChainedFunction} from 'rc-util'; | ||
5 | import placements from './util/placements'; | ||
6 | import CommonMixin from './mixin/CommonMixin'; | ||
7 | |||
8 | function noop() { | ||
9 | } | ||
10 | |||
11 | function refFn(field, component) { | ||
12 | this[field] = component; | ||
13 | } | ||
14 | |||
15 | const 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 | |||
158 | export 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 @@ | |||
1 | import React, {PropTypes} from 'react'; | ||
2 | import classnames from 'classnames'; | ||
3 | import CommonMixin from './mixin/CommonMixin'; | ||
4 | import Header from './module/Header'; | ||
5 | import Combobox from './module/Combobox'; | ||
6 | |||
7 | function noop() { | ||
8 | } | ||
9 | |||
10 | function generateOptions(length) { | ||
11 | return Array.apply(null, {length: length}).map((item, index) => { | ||
12 | return index; | ||
13 | }); | ||
14 | } | ||
15 | |||
16 | const 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 | |||
121 | export 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 @@ | |||
1 | import enUs from 'gregorian-calendar-format/lib/locale/en_US'; | ||
2 | |||
3 | export 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 @@ | |||
1 | import zhCn from 'gregorian-calendar-format/lib/locale/zh_CN'; | ||
2 | |||
3 | export 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 @@ | |||
1 | import {PropTypes} from 'react'; | ||
2 | import enUs from '../locale/en_US'; | ||
3 | import {getFormatter} from '../util/index'; | ||
4 | |||
5 | export 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 @@ | |||
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; | ||
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 @@ | |||
1 | import DateTimeFormat from 'gregorian-calendar-format'; | ||
2 | |||
3 | export 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 @@ | |||
1 | const autoAdjustOverflow = { | ||
2 | adjustX: 1, | ||
3 | adjustY: 1, | ||
4 | }; | ||
5 | |||
6 | const targetOffset = [0, 0]; | ||
7 | |||
8 | const 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 | |||
35 | export default placements; | ||