]> git.immae.eu Git - github/fretlink/time-picker.git/commitdiff
release 0.1.0
author崖鹰 <zhao.wuz@alipay.com>
Fri, 13 Nov 2015 03:33:48 +0000 (11:33 +0800)
committer崖鹰 <zhao.wuz@alipay.com>
Fri, 13 Nov 2015 03:33:48 +0000 (11:33 +0800)
20 files changed:
HISTORY.md [new file with mode: 0644]
assets/index.less [new file with mode: 0644]
assets/index/Combobox.less [new file with mode: 0644]
assets/index/Header.less [new file with mode: 0644]
assets/index/Picker.less [new file with mode: 0644]
assets/index/Select.less [new file with mode: 0644]
assets/index/TimePanel.less [new file with mode: 0644]
examples/pick-time.jsx [new file with mode: 0644]
index.js [new file with mode: 0644]
package.json [new file with mode: 0644]
src/Picker.jsx [new file with mode: 0644]
src/TimePanel.jsx [new file with mode: 0644]
src/locale/en_US.js [new file with mode: 0644]
src/locale/zh_CN.js [new file with mode: 0644]
src/mixin/CommonMixin.js [new file with mode: 0644]
src/module/Combobox.jsx [new file with mode: 0644]
src/module/Header.jsx [new file with mode: 0644]
src/module/Select.jsx [new file with mode: 0644]
src/util/index.js [new file with mode: 0644]
src/util/placements.js [new file with mode: 0644]

diff --git a/HISTORY.md b/HISTORY.md
new file mode 100644 (file)
index 0000000..e2ad063
--- /dev/null
@@ -0,0 +1,9 @@
+History
+=======
+
+---
+
+0.1.0 / 2015-11-12
+------------------
+
+`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 (file)
index 0000000..2ab247b
--- /dev/null
@@ -0,0 +1,22 @@
+@prefixClass: rc-timepicker;
+
+.@{prefixClass} {
+  box-sizing: border-box;
+  * {
+    box-sizing: border-box;
+  }
+}
+
+@font-face {
+  font-family: 'anticon';
+  src: url('//at.alicdn.com/t/font_1434092639_4910953.eot');
+  /* IE9*/
+  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');
+  /* iOS 4.1- */
+}
+
+@import "./index/Picker";
+@import "./index/TimePanel";
+@import "./index/Header";
+@import "./index/Combobox";
+@import "./index/Select";
diff --git a/assets/index/Combobox.less b/assets/index/Combobox.less
new file mode 100644 (file)
index 0000000..a796d0a
--- /dev/null
@@ -0,0 +1,4 @@
+.@{prefixClass} {
+  &-combobox {
+  }
+}
diff --git a/assets/index/Header.less b/assets/index/Header.less
new file mode 100644 (file)
index 0000000..ac3d662
--- /dev/null
@@ -0,0 +1,54 @@
+.@{prefixClass} {
+  &-input {
+    margin: 0;
+    padding: 0;
+    border: 0;
+    width: 100%;
+    cursor: auto;
+    line-height: 1.5;
+    outline: 0;
+    border: 1px solid transparent;
+
+    &-wrap {
+      box-sizing: border-box;
+      position: relative;
+      padding: 6px;
+      border-bottom: 1px solid #e9e9e9;
+    }
+
+    &-invalid {
+      border-color: red;
+    }
+  }
+
+  &-clear-btn {
+    position: absolute;
+    right: 6px;
+    cursor: pointer;
+    overflow: hidden;
+    width: 20px;
+    height: 20px;
+    text-align: center;
+    line-height: 20px;
+    top: 6px;
+    margin: 0;
+  }
+
+  &-clear-btn:after {
+    content: "x";
+    font-size: 12px;
+    color: #aaa;
+    display: inline-block;
+    line-height: 1;
+    width: 20px;
+    transition: color 0.3s ease;
+  }
+
+  &-clear-btn:hover:after {
+    color: #666;
+  }
+}
+
+.narrow .@{prefixClass}-input-wrap {
+  max-width: 111px;
+}
diff --git a/assets/index/Picker.less b/assets/index/Picker.less
new file mode 100644 (file)
index 0000000..769c4b7
--- /dev/null
@@ -0,0 +1,4 @@
+.@{prefixClass} {
+  &-picker {
+  }
+}
diff --git a/assets/index/Select.less b/assets/index/Select.less
new file mode 100644 (file)
index 0000000..995d09e
--- /dev/null
@@ -0,0 +1,50 @@
+.@{prefixClass}-select {
+  float: left;
+  overflow-y:auto;
+  font-size: 12px;
+  border: 1px solid #e9e9e9;
+  border-width: 0 1px;
+  margin-left: -1px;
+  box-sizing: border-box;
+  width: 56px;
+
+  &:first-child {
+    border-left: 0;
+    margin-left: 0;
+  }
+
+  &:last-child {
+    border-right: 0;
+  }
+
+  ul {
+    list-style: none;
+    box-sizing: border-box;
+    margin: 0;
+    padding: 0;
+    width: 100%;
+    max-height: 144px;
+  }
+
+  li {
+    list-style: none;
+    box-sizing: border-box;
+    margin: 0;
+    padding: 0 0 0 16px;
+    width: 100%;
+    height: 24px;
+    line-height: 24px;
+    text-align: left;
+    cursor: pointer;
+    user-select: none;
+
+    &.selected {
+      background: #edfaff;
+      color: #2db7f5;
+    }
+
+    &:hover {
+      background: #edfaff;
+    }
+  }
+}
diff --git a/assets/index/TimePanel.less b/assets/index/TimePanel.less
new file mode 100644 (file)
index 0000000..574605f
--- /dev/null
@@ -0,0 +1,16 @@
+.@{prefixClass}-panel {
+  display: inline-block;
+  position: relative;
+  outline: none;
+  font-family: Arial, "Hiragino Sans GB", "Microsoft Yahei", "Microsoft Sans Serif", "WenQuanYi Micro Hei", sans-serif;
+  border: 1px solid #ccc;
+  list-style: none;
+  font-size: 12px;
+  text-align: left;
+  background-color: #fff;
+  border-radius: 3px;
+  box-shadow: 0 1px 5px #ccc;
+  background-clip: padding-box;
+  border: 1px solid #ccc;
+  line-height: 1.5;
+}
diff --git a/examples/pick-time.jsx b/examples/pick-time.jsx
new file mode 100644 (file)
index 0000000..33c5d07
--- /dev/null
@@ -0,0 +1,36 @@
+import '../component/timepicker/assets/index.less';
+
+import React from 'react';
+import ReactDom from 'react-dom';
+import zhCn from 'gregorian-calendar/lib/locale/zh_CN';
+import GregorianCalendar from 'gregorian-calendar';
+
+import TimePicker from '../component/timepicker/src/Picker';
+import TimePanel from '../component/timepicker/src/TimePanel';
+import TimepickerLocale from '../component/timepicker/src/locale/zh_CN';
+import DateTimeFormat from 'gregorian-calendar-format';
+
+const formatter = new DateTimeFormat('HH:mm:ss');
+
+const now = new GregorianCalendar(zhCn);
+now.setTime(Date.now());
+
+const timePanel = (
+  <TimePanel
+    defaultValue={now}
+    locale={TimepickerLocale}
+    formatter={formatter}
+    minuteOptions={[0, 30]}
+  />
+);
+
+ReactDom.render(
+  <TimePicker panel={timePanel} value={now}>
+    {
+      ({value}) => {
+        return <input type="text" placeholder="请选择时间" readOnly value={value && formatter.format(value)} />;
+      }
+    }
+  </TimePicker>,
+  document.getElementById('react-content')
+);
diff --git a/index.js b/index.js
new file mode 100644 (file)
index 0000000..4570919
--- /dev/null
+++ b/index.js
@@ -0,0 +1,2 @@
+import TimePanel from './src/';
+export default TimePanel;
diff --git a/package.json b/package.json
new file mode 100644 (file)
index 0000000..3f7b765
--- /dev/null
@@ -0,0 +1,66 @@
+{
+  "name": "rc-timepicker",
+  "version": "0.1.0",
+  "description": "React Timepicker",
+  "keywords": [
+    "react",
+    "react-timepicker",
+    "react-component",
+    "timepicker",
+    "ui component",
+    "ui",
+    "component"
+  ],
+  "files": [
+    "lib",
+    "assets/*.css"
+  ],
+  "main": "lib/index",
+  "homepage": "http://github.com/react-component/timepicker",
+  "author": "wuzhao.mail@gmail.com",
+  "repository": {
+    "type": "git",
+    "url": "git@github.com:react-component/timepicker.git"
+  },
+  "bugs": {
+    "url": "http://github.com/react-component/timepicker/issues"
+  },
+  "licenses": "MIT",
+  "config": {
+    "port": 8001
+  },
+  "scripts": {
+    "build": "rc-tools run build",
+    "gh-pages": "rc-tools run gh-pages",
+    "start": "rc-server",
+    "pub": "rc-tools run pub",
+    "lint": "rc-tools run lint",
+    "karma": "rc-tools run karma",
+    "saucelabs": "rc-tools run saucelabs",
+    "browser-test": "rc-tools run browser-test",
+    "browser-test-cover": "rc-tools run browser-test-cover",
+    "validate": "npm ls"
+  },
+  "devDependencies": {
+    "async": "~0.9.0",
+    "bootstrap": "~3.3.2",
+    "expect.js": "~0.3.1",
+    "jquery": "~1.11.3",
+    "pre-commit": "1.x",
+    "rc-server": "3.x",
+    "rc-tools": "4.x",
+    "react": "0.14.x",
+    "react-addons-test-utils": "~0.14.0",
+    "react-dom": "0.14.x"
+  },
+  "pre-commit": [
+    "lint"
+  ],
+  "dependencies": {
+    "gregorian-calendar": "4.x",
+    "gregorian-calendar-format": "4.x",
+    "object-assign": "4.x",
+    "rc-trigger": "1.x",
+    "rc-util": "2.x"
+  }
+}
diff --git a/src/Picker.jsx b/src/Picker.jsx
new file mode 100644 (file)
index 0000000..f15434a
--- /dev/null
@@ -0,0 +1,158 @@
+import React, {PropTypes} from 'react';
+import ReactDOM from 'react-dom';
+import Trigger from 'rc-trigger';
+import {createChainedFunction} from 'rc-util';
+import placements from './util/placements';
+import CommonMixin from './mixin/CommonMixin';
+
+function noop() {
+}
+
+function refFn(field, component) {
+  this[field] = component;
+}
+
+const Picker = React.createClass({
+  propTypes: {
+    prefixCls: PropTypes.string,
+    panel: PropTypes.element,
+    children: PropTypes.func,
+    disabled: PropTypes.bool,
+    value: PropTypes.object,
+    open: PropTypes.bool,
+    align: PropTypes.object,
+    placement: PropTypes.any,
+    transitionName: PropTypes.string,
+    onChange: PropTypes.func,
+    onOpen: PropTypes.func,
+    onClose: PropTypes.func,
+  },
+
+  mixins: [CommonMixin],
+
+  getDefaultProps() {
+    return {
+      open: false,
+      align: {},
+      placement: 'bottomLeft',
+      onChange: noop,
+      onOpen: noop,
+      onClose: noop,
+    };
+  },
+
+  getInitialState() {
+    this.savePanelRef = refFn.bind(this, 'panelInstance');
+    const { open, value } = this.props;
+    return { open, value };
+  },
+
+  componentWillMount() {
+    document.addEventListener('click', this.handleDocumentClick, false);
+  },
+
+  componentWillReceiveProps(nextProps) {
+    const { value, open } = nextProps;
+    if (value !== undefined) {
+      this.setState({value});
+    }
+    if (open !== undefined) {
+      this.setState({open});
+    }
+  },
+
+  componentWillUnmount() {
+    document.removeEventListener('click', this.handleDocumentClick, false);
+  },
+
+  onPanelChange(value) {
+    const props = this.props;
+    this.setState({
+      value: value,
+    });
+    props.onChange(value);
+  },
+
+  onPanelClear() {
+    this.setOpen(false, this.focus);
+  },
+
+  onVisibleChange(open) {
+    this.setOpen(open, () => {
+      if (open) {
+        ReactDOM.findDOMNode(this.panelInstance).focus();
+      }
+    });
+  },
+
+  getPanelElement() {
+    const panel = this.props.panel;
+    const extraProps = {
+      ref: this.savePanelRef,
+      defaultValue: this.state.value || panel.props.defaultValue,
+      onChange: createChainedFunction(panel.props.onChange, this.onPanelChange),
+      onClear: createChainedFunction(panel.props.onClear, this.onPanelClear),
+    };
+
+    return React.cloneElement(panel, extraProps);
+  },
+
+  setOpen(open, callback) {
+    const {onOpen, onClose} = this.props;
+    if (this.state.open !== open) {
+      this.setState({
+        open: open,
+      }, callback);
+      const event = {
+        open: open,
+      };
+      if (open) {
+        onOpen(event);
+      } else {
+        onClose(event);
+      }
+    }
+  },
+
+  handleDocumentClick(event) {
+    // hide popup when click outside
+    if (this.state.open && ReactDOM.findDOMNode(this.panelInstance).contains(event.target)) {
+      return;
+    }
+    this.setState({
+      open: false,
+    });
+  },
+
+  focus() {
+    if (!this.state.open) {
+      ReactDOM.findDOMNode(this).focus();
+    }
+  },
+
+  render() {
+    const state = this.state;
+    const props = this.props;
+    const { prefixCls, placement, align, disabled, transitionName, children } = props;
+    return (
+      <Trigger
+        prefixCls={prefixCls}
+        popup={this.getPanelElement()}
+        popupAlign={align}
+        builtinPlacements={placements}
+        popupPlacement={placement}
+        action={disabled ? [] : ['click']}
+        destroyPopupOnHide
+        popupTransitionName={transitionName}
+        popupVisible={state.open}
+        onPopupVisibleChange={this.onVisibleChange}
+      >
+        <span className={`${prefixCls}-picker`}>
+          {children(state, props)}
+        </span>
+      </Trigger>
+    );
+  },
+});
+
+export default Picker;
diff --git a/src/TimePanel.jsx b/src/TimePanel.jsx
new file mode 100644 (file)
index 0000000..dad8036
--- /dev/null
@@ -0,0 +1,121 @@
+import React, {PropTypes} from 'react';
+import classnames from 'classnames';
+import CommonMixin from './mixin/CommonMixin';
+import Header from './module/Header';
+import Combobox from './module/Combobox';
+
+function noop() {
+}
+
+function generateOptions(length) {
+  return Array.apply(null, {length: length}).map((item, index) => {
+    return index;
+  });
+}
+
+const TimePanel = React.createClass({
+  propTypes: {
+    prefixCls: PropTypes.string,
+    defaultValue: PropTypes.object,
+    locale: PropTypes.object,
+    placeholder: PropTypes.string,
+    formatter: PropTypes.object,
+    hourOptions: PropTypes.array,
+    minuteOptions: PropTypes.array,
+    secondOptions: PropTypes.array,
+    onChange: PropTypes.func,
+    onClear: PropTypes.func,
+  },
+
+  mixins: [CommonMixin],
+
+  getDefaultProps() {
+    return {
+      hourOptions: generateOptions(24),
+      minuteOptions: generateOptions(60),
+      secondOptions: generateOptions(60),
+      onChange: noop,
+      onClear: noop,
+    };
+  },
+
+  getInitialState() {
+    return {
+      value: this.props.defaultValue,
+    };
+  },
+
+  componentWillMount() {
+    const formatter = this.props.formatter;
+    const pattern = formatter.originalPattern;
+    if (pattern === 'HH:mm') {
+      this.showSecond = false;
+    } else if (pattern === 'mm:ss') {
+      this.showHour = false;
+    }
+  },
+
+  onChange(newValue) {
+    this.setState({ value: newValue });
+    this.props.onChange(newValue);
+  },
+
+  onClear() {
+    this.props.onClear();
+  },
+
+  getPlaceholder(placeholder) {
+    if (placeholder) {
+      return placeholder;
+    }
+
+    const { locale } = this.props;
+    if (!this.showHour) {
+      return locale.placeholdermmss;
+    } else if (!this.showSecond) {
+      return locale.placeholderHHmm;
+    }
+    return locale.placeholderHHmmss;
+  },
+
+  showHour: true,
+  showSecond: true,
+
+  render() {
+    const { locale, prefixCls, defaultValue, placeholder, hourOptions, minuteOptions, secondOptions } = this.props;
+    const value = this.state.value || defaultValue;
+    const cls = classnames({ 'narrow': !this.showHour || !this.showSecond });
+
+    return (
+      <div className={`${prefixCls}-panel ${cls}`}>
+        <Header
+          prefixCls={prefixCls}
+          gregorianTimepickerLocale={defaultValue.locale}
+          locale={locale}
+          value={value}
+          formatter={this.getFormatter()}
+          placeholder={this.getPlaceholder(placeholder)}
+          hourOptions={hourOptions}
+          minuteOptions={minuteOptions}
+          secondOptions={secondOptions}
+          onChange={this.onChange}
+          onClear={this.onClear}
+          showClear
+        />
+        <Combobox
+          prefixCls={prefixCls}
+          value={value}
+          formatter={this.getFormatter()}
+          onChange={this.onChange}
+          showHour={this.showHour}
+          showSecond={this.showSecond}
+          hourOptions={hourOptions}
+          minuteOptions={minuteOptions}
+          secondOptions={secondOptions}
+        />
+      </div>
+    );
+  },
+});
+
+export default TimePanel;
diff --git a/src/locale/en_US.js b/src/locale/en_US.js
new file mode 100644 (file)
index 0000000..252d3d2
--- /dev/null
@@ -0,0 +1,9 @@
+import enUs from 'gregorian-calendar-format/lib/locale/en_US';
+
+export default {
+  placeholderHHmmss: 'HH:MM:SS',
+  placeholderHHmm: 'HH:MM',
+  placeholdermmss: 'MM:SS',
+  clear: 'Clear',
+  format: enUs,
+};
diff --git a/src/locale/zh_CN.js b/src/locale/zh_CN.js
new file mode 100644 (file)
index 0000000..709cfb4
--- /dev/null
@@ -0,0 +1,9 @@
+import zhCn from 'gregorian-calendar-format/lib/locale/zh_CN';
+
+export default {
+  placeholderHHmmss: '时:分:秒',
+  placeholderHHmm: '时:分',
+  placeholdermmss: '分:秒',
+  clear: '清除',
+  format: zhCn,
+};
diff --git a/src/mixin/CommonMixin.js b/src/mixin/CommonMixin.js
new file mode 100644 (file)
index 0000000..4203a9e
--- /dev/null
@@ -0,0 +1,46 @@
+import {PropTypes} from 'react';
+import enUs from '../locale/en_US';
+import {getFormatter} from '../util/index';
+
+export default {
+  propTypes: {
+    prefixCls: PropTypes.string,
+    locale: PropTypes.object,
+  },
+
+  getDefaultProps() {
+    return {
+      prefixCls: 'rc-timepicker',
+      locale: enUs,
+    };
+  },
+
+  getFormatter() {
+    const formatter = this.props.formatter;
+    const locale = this.props.locale;
+    if (formatter) {
+      if (formatter === this.lastFormatter) {
+        return this.normalFormatter;
+      }
+      this.normalFormatter = getFormatter(formatter, locale);
+      this.lastFormatter = formatter;
+      return this.normalFormatter;
+    }
+    if (!this.showSecond) {
+      if (!this.notShowSecondFormatter) {
+        this.notShowSecondFormatter = getFormatter('HH:mm', locale);
+      }
+      return this.notShowSecondFormatter;
+    }
+    if (!this.showHour) {
+      if (!this.notShowHourFormatter) {
+        this.notShowHourFormatter = getFormatter('mm:ss', locale);
+      }
+      return this.notShowHourFormatter;
+    }
+    if (!this.normalFormatter) {
+      this.normalFormatter = getFormatter('HH:mm:ss', locale);
+    }
+    return this.normalFormatter;
+  },
+};
diff --git a/src/module/Combobox.jsx b/src/module/Combobox.jsx
new file mode 100644 (file)
index 0000000..e6fe5ed
--- /dev/null
@@ -0,0 +1,95 @@
+import React, {PropTypes} from 'react';
+import Select from './Select';
+
+const formatOption = (option) => {
+  if (option < 10) {
+    return `0${option}`;
+  }
+  return `${option}`;
+};
+
+const Combobox = React.createClass({
+  propTypes: {
+    formatter: PropTypes.object,
+    prefixCls: PropTypes.string,
+    value: PropTypes.object,
+    onChange: PropTypes.func,
+    showHour: PropTypes.bool,
+    showSecond: PropTypes.bool,
+    hourOptions: PropTypes.array,
+    minuteOptions: PropTypes.array,
+    secondOptions: PropTypes.array,
+  },
+
+  onItemChange(type, itemValue) {
+    const { value, onChange } = this.props;
+    let index = 4;
+    if (type === 'minute') {
+      index = 5;
+    } else if (type === 'second') {
+      index = 6;
+    }
+    value.fields[index] = itemValue;
+    onChange(value);
+  },
+
+  getHourSelect(hour) {
+    const { prefixCls, hourOptions, showHour } = this.props;
+    if (!showHour) {
+      return null;
+    }
+    return (
+      <Select
+        prefixCls={prefixCls}
+        options={hourOptions.map(option => formatOption(option))}
+        selectedIndex={hourOptions.indexOf(hour)}
+        type="hour"
+        onSelect={this.onItemChange}
+      />
+    );
+  },
+
+  getMinuteSelect(minute) {
+    const { prefixCls, minuteOptions } = this.props;
+    return (
+      <Select
+        prefixCls={prefixCls}
+        options={minuteOptions.map(option => formatOption(option))}
+        selectedIndex={minuteOptions.indexOf(minute)}
+        type="minute"
+        onSelect={this.onItemChange}
+      />
+    );
+  },
+
+  getSectionSelect(second) {
+    const { prefixCls, secondOptions, showSecond } = this.props;
+    if (!showSecond) {
+      return null;
+    }
+    return (
+      <Select
+        prefixCls={prefixCls}
+        options={secondOptions.map(option => formatOption(option))}
+        selectedIndex={secondOptions.indexOf(second)}
+        type="second"
+        onSelect={this.onItemChange}
+      />
+    );
+  },
+
+  render() {
+    const { prefixCls, value } = this.props;
+    const timeFields = value.fields;
+
+    return (
+      <div className={`${prefixCls}-combobox`}>
+        {this.getHourSelect(timeFields[4])}
+        {this.getMinuteSelect(timeFields[5])}
+        {this.getSectionSelect(timeFields[6])}
+      </div>
+    );
+  },
+});
+
+export default Combobox;
diff --git a/src/module/Header.jsx b/src/module/Header.jsx
new file mode 100644 (file)
index 0000000..f7e443f
--- /dev/null
@@ -0,0 +1,139 @@
+import React, {PropTypes} from 'react';
+
+const Header = React.createClass({
+  propTypes: {
+    formatter: PropTypes.object,
+    prefixCls: PropTypes.string,
+    gregorianTimepickerLocale: PropTypes.object,
+    locale: PropTypes.object,
+    disabledDate: PropTypes.func,
+    placeholder: PropTypes.string,
+    value: PropTypes.object,
+    hourOptions: PropTypes.array,
+    minuteOptions: PropTypes.array,
+    secondOptions: PropTypes.array,
+    onChange: PropTypes.func,
+    onClear: PropTypes.func,
+    showClear: PropTypes.bool,
+  },
+
+  getInitialState() {
+    const value = this.props.value;
+    return {
+      str: value && this.props.formatter.format(value) || '',
+      invalid: false,
+    };
+  },
+
+  componentWillReceiveProps(nextProps) {
+    const value = this.formatValue(nextProps.value);
+    this.setState({
+      str: value && nextProps.formatter.format(value) || '',
+      invalid: false,
+    });
+  },
+
+  onInputChange(event) {
+    const str = event.target.value;
+    this.setState({
+      str,
+    });
+    let value = null;
+    const {formatter, gregorianTimepickerLocale, hourOptions, minuteOptions, secondOptions, onChange} = this.props;
+
+    if (str) {
+      const originalValue = this.props.value;
+      try {
+        value = formatter.parse(str, {
+          locale: gregorianTimepickerLocale,
+          obeyCount: true,
+        });
+        value = this.formatValue(value);
+      } catch (ex) {
+        this.setState({
+          invalid: true,
+        });
+        return;
+      }
+
+      if (value) {
+        if (
+          hourOptions.indexOf(value.fields[4]) < 0 ||
+          minuteOptions.indexOf(value.fields[5]) < 0 ||
+          secondOptions.indexOf(value.fields[6]) < 0
+        ) {
+          this.setState({
+            invalid: true,
+          });
+          return;
+        }
+
+        if (originalValue && value) {
+          if (
+            originalValue.fields[4] !== value.fields[4] ||
+            originalValue.fields[5] !== value.fields[5] ||
+            originalValue.fields[6] !== value.fields[6]
+          ) {
+            onChange(value);
+          }
+        } else if (originalValue !== value) {
+          onChange(value);
+        }
+      } else {
+        this.setState({
+          invalid: true,
+        });
+        return;
+      }
+    } else {
+      onChange(null);
+    }
+
+    this.setState({
+      invalid: false,
+    });
+  },
+
+  onClear() {
+    this.setState({str: ''});
+    this.props.onClear();
+  },
+
+  getClearButton() {
+    const { locale, prefixCls, showClear } = this.props;
+    if (!showClear) {
+      return null;
+    }
+    return <a className={`${prefixCls}-clear-btn`} role="button" title={locale.clear} onMouseDown={this.onClear} />;
+  },
+
+  getInput() {
+    const { prefixCls, placeholder } = this.props;
+    const { invalid, str } = this.state;
+    const invalidClass = invalid ? `${prefixCls}-input-invalid` : '';
+    return <input className={`${prefixCls}-input  ${invalidClass}`} value={str} placeholder={placeholder} onChange={this.onInputChange} />;
+  },
+
+  formatValue(value) {
+    const newValue = this.props.value.clone();
+    if (!value) {
+      return newValue;
+    }
+    newValue.fields[4] = value.fields[4];
+    newValue.fields[5] = value.fields[5];
+    newValue.fields[6] = value.fields[6];
+    return newValue;
+  },
+
+  render() {
+    const { prefixCls } = this.props;
+    return (
+      <div className={`${prefixCls}-input-wrap`}>
+        {this.getInput()}
+        {this.getClearButton()}
+      </div>
+    );
+  },
+});
+
+export default Header;
diff --git a/src/module/Select.jsx b/src/module/Select.jsx
new file mode 100644 (file)
index 0000000..2b69623
--- /dev/null
@@ -0,0 +1,88 @@
+import React, {PropTypes} from 'react';
+import ReactDom from 'react-dom';
+import classnames from 'classnames';
+
+const scrollTo = (element, to, duration) => {
+  // jump to target if duration zero
+  if (duration <= 0) {
+    element.scrollTop = to;
+    return;
+  }
+  const difference = to - element.scrollTop;
+  const perTick = difference / duration * 10;
+
+  setTimeout(() => {
+    element.scrollTop = element.scrollTop + perTick;
+    if (element.scrollTop === to) return;
+    scrollTo(element, to, duration - 10);
+  }, 10);
+};
+
+const Select = React.createClass({
+  propTypes: {
+    prefixCls: PropTypes.string,
+    options: PropTypes.array,
+    selectedIndex: PropTypes.number,
+    type: PropTypes.string,
+    onSelect: PropTypes.func,
+  },
+
+  componentDidMount() {
+    // jump to selected option
+    this.scrollToSelected(0);
+  },
+
+  componentDidUpdate() {
+    // smooth scroll to selected option
+    this.scrollToSelected(200);
+  },
+
+  onSelect(event) {
+    // do nothing when select selected option
+    if (event.target.getAttribute('class') === 'selected') {
+      return;
+    }
+    // change combobox selection
+    const { onSelect, type } = this.props;
+    const value = parseInt(event.target.innerHTML, 10);
+    onSelect(type, value);
+  },
+
+  getOptions() {
+    const { options, selectedIndex } = this.props;
+    return options.map((item, index) => {
+      const cls = classnames({ selected: selectedIndex === index});
+      const ref = selectedIndex === index ? 'selected' : null;
+      return <li ref={ref} className={cls} key={index} onClick={this.onSelect}>{item}</li>;
+    });
+  },
+
+  scrollToSelected(duration) {
+    // move to selected item
+    const select = ReactDom.findDOMNode(this);
+    const list = ReactDom.findDOMNode(this.refs.list);
+    let index = this.props.selectedIndex - 2;
+    if (index < 0) {
+      index = 0;
+    }
+    const topOption = list.children[index];
+    const to = topOption.offsetTop - select.offsetTop;
+    scrollTo(select, to, duration);
+  },
+
+  render() {
+    if (this.props.options.length === 0) {
+      return null;
+    }
+
+    const { prefixCls } = this.props;
+
+    return (
+      <div className={`${prefixCls}-select`}>
+        <ul ref="list">{this.getOptions()}</ul>
+      </div>
+    );
+  },
+});
+
+export default Select;
diff --git a/src/util/index.js b/src/util/index.js
new file mode 100644 (file)
index 0000000..5bc0a78
--- /dev/null
@@ -0,0 +1,8 @@
+import DateTimeFormat from 'gregorian-calendar-format';
+
+export function getFormatter(format, locale) {
+  if (typeof format === 'string') {
+    return new DateTimeFormat(format, locale.format);
+  }
+  return format;
+}
diff --git a/src/util/placements.js b/src/util/placements.js
new file mode 100644 (file)
index 0000000..2574da1
--- /dev/null
@@ -0,0 +1,35 @@
+const autoAdjustOverflow = {
+  adjustX: 1,
+  adjustY: 1,
+};
+
+const targetOffset = [0, 0];
+
+const placements = {
+  topLeft: {
+    points: ['tl', 'tl'],
+    overflow: autoAdjustOverflow,
+    offset: [0, -3],
+    targetOffset,
+  },
+  topRight: {
+    points: ['tr', 'tr'],
+    overflow: autoAdjustOverflow,
+    offset: [0, -3],
+    targetOffset,
+  },
+  bottomRight: {
+    points: ['br', 'br'],
+    overflow: autoAdjustOverflow,
+    offset: [0, 3],
+    targetOffset,
+  },
+  bottomLeft: {
+    points: ['bl', 'bl'],
+    overflow: autoAdjustOverflow,
+    offset: [0, 3],
+    targetOffset,
+  },
+};
+
+export default placements;