From 02de449a0474765a4796fa607e7e3922252f574f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B4=96=E9=B9=B0?= Date: Fri, 13 Nov 2015 11:33:48 +0800 Subject: release 0.1.0 --- 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 +++++++++++ 10 files changed, 708 insertions(+) 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 (limited to 'src') 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 ( +
    + +
    + ); + }, +}); + +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; -- cgit v1.2.3