From 02de449a0474765a4796fa607e7e3922252f574f Mon Sep 17 00:00:00 2001 From: =?utf8?q?=E5=B4=96=E9=B9=B0?= Date: Fri, 13 Nov 2015 11:33:48 +0800 Subject: [PATCH] release 0.1.0 --- HISTORY.md | 9 ++ assets/index.less | 22 +++++ assets/index/Combobox.less | 4 + assets/index/Header.less | 54 ++++++++++++ assets/index/Picker.less | 4 + assets/index/Select.less | 50 ++++++++++++ assets/index/TimePanel.less | 16 ++++ examples/pick-time.jsx | 36 ++++++++ index.js | 2 + package.json | 66 +++++++++++++++ src/Picker.jsx | 158 ++++++++++++++++++++++++++++++++++++ src/TimePanel.jsx | 121 +++++++++++++++++++++++++++ src/locale/en_US.js | 9 ++ src/locale/zh_CN.js | 9 ++ src/mixin/CommonMixin.js | 46 +++++++++++ src/module/Combobox.jsx | 95 ++++++++++++++++++++++ src/module/Header.jsx | 139 +++++++++++++++++++++++++++++++ src/module/Select.jsx | 88 ++++++++++++++++++++ src/util/index.js | 8 ++ src/util/placements.js | 35 ++++++++ 20 files changed, 971 insertions(+) create mode 100644 HISTORY.md create mode 100644 assets/index.less create mode 100644 assets/index/Combobox.less create mode 100644 assets/index/Header.less create mode 100644 assets/index/Picker.less create mode 100644 assets/index/Select.less create mode 100644 assets/index/TimePanel.less create mode 100644 examples/pick-time.jsx create mode 100644 index.js create mode 100644 package.json create mode 100644 src/Picker.jsx create mode 100644 src/TimePanel.jsx create mode 100644 src/locale/en_US.js create mode 100644 src/locale/zh_CN.js create mode 100644 src/mixin/CommonMixin.js create mode 100644 src/module/Combobox.jsx create mode 100644 src/module/Header.jsx create mode 100644 src/module/Select.jsx create mode 100644 src/util/index.js create mode 100644 src/util/placements.js diff --git a/HISTORY.md b/HISTORY.md new file mode 100644 index 0000000..e2ad063 --- /dev/null +++ b/HISTORY.md @@ -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 index 0000000..2ab247b --- /dev/null +++ b/assets/index.less @@ -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 index 0000000..a796d0a --- /dev/null +++ b/assets/index/Combobox.less @@ -0,0 +1,4 @@ +.@{prefixClass} { + &-combobox { + } +} 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 @@ +.@{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 index 0000000..769c4b7 --- /dev/null +++ b/assets/index/Picker.less @@ -0,0 +1,4 @@ +.@{prefixClass} { + &-picker { + } +} 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 @@ +.@{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 index 0000000..574605f --- /dev/null +++ b/assets/index/TimePanel.less @@ -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 index 0000000..33c5d07 --- /dev/null +++ b/examples/pick-time.jsx @@ -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 = ( + +); + +ReactDom.render( + + { + ({value}) => { + return ; + } + } + , + document.getElementById('react-content') +); diff --git a/index.js b/index.js new file mode 100644 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 index 0000000..3f7b765 --- /dev/null +++ b/package.json @@ -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 index 0000000..f15434a --- /dev/null +++ b/src/Picker.jsx @@ -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 ( + + + {children(state, props)} + + + ); + }, +}); + +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 @@ +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 ( +
+
+ +
+ ); + }, +}); + +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 @@ +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 index 0000000..709cfb4 --- /dev/null +++ b/src/locale/zh_CN.js @@ -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 index 0000000..4203a9e --- /dev/null +++ b/src/mixin/CommonMixin.js @@ -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 index 0000000..e6fe5ed --- /dev/null +++ b/src/module/Combobox.jsx @@ -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 ( + formatOption(option))} + selectedIndex={minuteOptions.indexOf(minute)} + type="minute" + onSelect={this.onItemChange} + /> + ); + }, + + getSectionSelect(second) { + const { prefixCls, secondOptions, showSecond } = this.props; + if (!showSecond) { + return null; + } + return ( + ; + }, + + 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 ( +
+ {this.getInput()} + {this.getClearButton()} +
+ ); + }, +}); + +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 @@ +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
  • {item}
  • ; + }); + }, + + 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 ( +
    +
      {this.getOptions()}
    +
    + ); + }, +}); + +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 @@ +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 index 0000000..2574da1 --- /dev/null +++ b/src/util/placements.js @@ -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; -- 2.41.0