diff --git a/README.md b/README.md
index 642e3277..f6e493dc 100644
--- a/README.md
+++ b/README.md
@@ -233,7 +233,7 @@ http://react-component.github.io/calendar/examples/index.html
mode |
- enum('time', 'date', 'month', 'year', 'decade') |
+ enum('time', 'date', 'month', 'quarter', 'year', 'decade') |
'date' |
control which kind of panel should be shown |
@@ -411,7 +411,7 @@ http://react-component.github.io/calendar/examples/index.html
mode |
- enum('date', 'month', 'year', 'decade')[] |
+ enum('date', 'month', 'quarter', 'year', 'decade')[] |
['date', 'date'] |
control which kind of panels should be shown |
@@ -536,6 +536,100 @@ http://react-component.github.io/calendar/examples/index.html
+### rc-calendar/lib/QuarterCalendar props
+
+
+
+
+ name |
+ type |
+ default |
+ description |
+
+
+
+
+ prefixCls |
+ String |
+ |
+ prefixCls of this component |
+
+
+ className |
+ String |
+ |
+ additional css class of root dom node |
+
+
+ style |
+ Object |
+ |
+ additional style of root dom node |
+
+
+ value |
+ moment |
+ |
+ current value like input's value |
+
+
+ defaultValue |
+ moment |
+ |
+ defaultValue like input's defaultValue |
+
+
+ locale |
+ Object |
+ import from 'rc-calendar/lib/locale/en_US' |
+ calendar locale |
+
+
+ disabledDate |
+ Function(current:moment):Boolean |
+ |
+ whether to disable select of current quarter |
+
+
+ onSelect |
+ Function(date: moment) |
+ |
+ called when a date is selected from calendar |
+
+
+ quarterCellRender |
+ function |
+ |
+ Custom quarter cell render method |
+
+
+ dateCellRender |
+ function |
+ |
+ Custom date cell render method |
+
+
+ quarterCellContentRender |
+ function |
+ |
+ Custom quarter cell content render method,the content will be appended to the cell. |
+
+
+
+ onChange |
+ Function(date: moment) |
+ |
+ called when a date is changed inside calendar (next year/next month/keyboard) |
+
+
+ renderFooter |
+ () => React.Node |
+ |
+ extra footer |
+
+
+
+
### rc-calendar/lib/Picker props
diff --git a/assets/index.less b/assets/index.less
index 89e11059..ecfc1b15 100644
--- a/assets/index.less
+++ b/assets/index.less
@@ -4,6 +4,7 @@
@import "index/Time";
@import "index/TimePanel";
@import "index/MonthPanel";
+@import "index/QuarterPanel";
@import "index/YearPanel";
@import "index/DecadePanel";
@import "common/RangeCalendar";
diff --git a/assets/index/QuarterPanel.less b/assets/index/QuarterPanel.less
new file mode 100644
index 00000000..3928895b
--- /dev/null
+++ b/assets/index/QuarterPanel.less
@@ -0,0 +1,140 @@
+.@{prefixClass}-quarter-panel {
+ left: 0;
+ top:0;
+ bottom: 0;
+ right: 0;
+ background: #ffffff;
+ z-index: 10;
+ position: absolute;
+ outline: none;
+}
+
+.@{prefixClass}-quarter-panel-hidden {
+ display: none;
+}
+
+.@{prefixClass}-quarter-panel-header {
+ padding: 0 10px;
+ height: 34px;
+ line-height: 30px;
+ position: relative;
+ text-align: center;
+ user-select: none;
+ -webkit-user-select: none;
+ border-bottom: 1px solid #ccc;
+
+ > a {
+ font-weight: bold;
+ display: inline-block;
+ padding: 4px 5px;
+ text-align: center;
+ width: 30px;
+
+ &:hover {
+ cursor: pointer;
+ color: #23c0fa;
+ }
+ }
+}
+
+.@{prefixClass}-quarter-panel-prev-year-btn, .@{prefixClass}-quarter-panel-next-year-btn {
+ position: absolute;
+ top: 0;
+}
+
+.@{prefixClass}-quarter-panel-next-year-btn {
+ &:after {
+ content: '»'
+ }
+}
+
+.@{prefixClass}-quarter-panel-prev-year-btn {
+ user-select: none;
+ left: 0;
+
+ &:after {
+ content: '«'
+ }
+}
+
+.@{prefixClass}-quarter-panel .@{prefixClass}-quarter-panel-year-select {
+ width: 180px;
+}
+
+.@{prefixClass}-quarter-panel-year-select-arrow {
+ display: none;
+}
+
+.@{prefixClass}-quarter-panel-next-year-btn {
+ user-select: none;
+ right: 0;
+}
+
+.@{prefixClass}-quarter-panel-body {
+ padding: 9px 10px 10px;
+ position: absolute;
+ top: 34px;
+ bottom: 0;
+}
+
+.@{prefixClass}-quarter-panel-table {
+ table-layout: fixed;
+ width: 100%;
+ height: 100%;
+ border-collapse: separate;
+}
+
+.@{prefixClass}-quarter-panel-cell {
+ text-align: center;
+
+
+
+ .@{prefixClass}-quarter-panel-quarter {
+ display: block;
+ width: 46px;
+ margin: 0 auto;
+ color: #666;
+ border-radius: 4px 4px;
+ height: 36px;
+ padding: 0;
+ background: transparent;
+ line-height: 36px;
+ text-align: center;
+
+ &:hover {
+ background: #ebfaff;
+ cursor: pointer;
+ }
+ }
+
+ &-disabled{
+ .@{prefixClass}-quarter-panel-quarter {
+ color: #bfbfbf;
+
+ &:hover {
+ background: white;
+ cursor: not-allowed;
+ }
+ }
+ }
+}
+
+.@{prefixClass}-quarter-panel-selected-cell .@{prefixClass}-quarter-panel-quarter {
+ background: #3fc7fa;
+ color: #fff;
+
+ &:hover {
+ background: #3fc7fa;
+ color: #fff;
+ }
+}
+
+.@{prefixClass}-quarter-header-wrap {
+ position: relative;
+ height: 108px;
+}
+
+.@{prefixClass}-quarter-year-header-wrap {
+ position: relative;
+ min-height: 308px;
+}
\ No newline at end of file
diff --git a/examples/antd-month-calendar.js b/examples/antd-month-calendar.js
index 15ed13f2..e68d413d 100644
--- a/examples/antd-month-calendar.js
+++ b/examples/antd-month-calendar.js
@@ -63,6 +63,7 @@ class Demo extends React.Component {
render() {
const state = this.state;
+ console.log(cn);
const calendar = ( {
+ console.log(`DatePicker change: ${value && value.format(format)}`);
+ this.setState({
+ value,
+ });
+ }
+
+ onShowTimeChange = (e) => {
+ this.setState({
+ showTime: e.target.checked,
+ });
+ }
+
+ toggleDisabled = () => {
+ this.setState({
+ disabled: !this.state.disabled,
+ });
+ }
+
+ render() {
+ const state = this.state;
+ const calendar = ();
+ return ();
+ }
+}
+
+function onStandaloneSelect(value) {
+ console.log('quarter-calendar select', (value && value.format(format)));
+}
+
+function onStandaloneChange(value) {
+ console.log('quarter-calendar change', (value && value.format(format)));
+}
+
+function disabledDate(value) {
+ return value.year() > now.year() ||
+ value.year() === now.year() && value.quarter() > now.quarter();
+}
+
+function onQuarterCellContentRender(value) {
+ // console.log('month-calendar onMonthCellContentRender', (value && value.format(format)));
+ return `${value.quarter()}季度`;
+}
+
+ReactDOM.render(
+ (
+
'extra footer'}
+ />
+
+
+
+
+ )
+ , document.getElementById('__react-content'));
diff --git a/src/QuarterCalendar.jsx b/src/QuarterCalendar.jsx
new file mode 100644
index 00000000..416a5ace
--- /dev/null
+++ b/src/QuarterCalendar.jsx
@@ -0,0 +1,126 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import KeyCode from 'rc-util/lib/KeyCode';
+import CalendarHeader from './calendar/CalendarHeader';
+import CalendarFooter from './calendar/CalendarFooter';
+import {
+ calendarMixinWrapper,
+ calendarMixinPropTypes,
+ calendarMixinDefaultProps,
+} from './mixin/CalendarMixin';
+import { commonMixinWrapper, propType, defaultProp } from './mixin/CommonMixin';
+import moment from 'moment';
+
+class QuarterCalendar extends React.Component {
+ static propTypes = {
+ ...calendarMixinPropTypes,
+ ...propType,
+ quarterCellRender: PropTypes.func,
+ dateCellRender: PropTypes.func,
+ value: PropTypes.object,
+ defaultValue: PropTypes.object,
+ selectedValue: PropTypes.object,
+ defaultSelectedValue: PropTypes.object,
+ disabledDate: PropTypes.func,
+ }
+
+ static defaultProps = Object.assign({}, defaultProp, calendarMixinDefaultProps);
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ isShowYear: false,
+ mode: 'quarter',
+ value: props.value || props.defaultValue || moment(),
+ selectedValue: props.selectedValue || props.defaultSelectedValue,
+ };
+ }
+
+ onKeyDown = (event) => {
+ const keyCode = event.keyCode;
+ const ctrlKey = event.ctrlKey || event.metaKey;
+ const stateValue = this.state.value;
+ const { disabledDate } = this.props;
+ let value = stateValue;
+ switch (keyCode) {
+ case KeyCode.DOWN:
+ value = stateValue.clone();
+ value.add(0, 'quarters');
+ break;
+ case KeyCode.UP:
+ value = stateValue.clone();
+ value.add(0, 'quarters');
+ break;
+ case KeyCode.LEFT:
+ value = stateValue.clone();
+ if (ctrlKey) {
+ value.add(-1, 'years');
+ } else {
+ value.add(-1, 'quarters');
+ }
+ break;
+ case KeyCode.RIGHT:
+ value = stateValue.clone();
+ if (ctrlKey) {
+ value.add(1, 'years');
+ } else {
+ value.add(1, 'quarters');
+ }
+ break;
+ case KeyCode.ENTER:
+ if (!disabledDate || !disabledDate(stateValue)) {
+ this.onSelect(stateValue);
+ }
+ event.preventDefault();
+ return 1;
+ default:
+ return undefined;
+ }
+ if (value !== stateValue) {
+ this.setValue(value);
+ event.preventDefault();
+ return 1;
+ }
+ }
+
+ handlePanelChange = (_, mode) => {
+ if (mode !== 'date') {
+ this.setState({ mode });
+ }
+ }
+
+ render() {
+ const { props, state } = this;
+ const { mode, value, isShowYear } = state;
+ const children = (
+
+
+ this.setState({ isShowYear: bol })}
+ locale={props.locale}
+ disabledQuarter={props.disabledDate}
+ quarterCellRender={props.quarterCellRender}
+ quarterCellContentRender={props.quarterCellContentRender}
+ onQuarterSelect={this.onSelect}
+ onValueChange={this.setValue}
+ onPanelChange={this.handlePanelChange}
+ />
+
+
+
+ );
+ return this.renderRoot({
+ className: `${props.prefixCls}-quarter-calendar`,
+ children,
+ });
+ }
+}
+
+export default calendarMixinWrapper(commonMixinWrapper(QuarterCalendar));
diff --git a/src/calendar/CalendarHeader.jsx b/src/calendar/CalendarHeader.jsx
index a7eccfc1..0704cf69 100644
--- a/src/calendar/CalendarHeader.jsx
+++ b/src/calendar/CalendarHeader.jsx
@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import toFragment from 'rc-util/lib/Children/mapSelf';
import MonthPanel from '../month/MonthPanel';
+import QuarterPanel from '../quarter/QuarterPanel';
import YearPanel from '../year/YearPanel';
import DecadePanel from '../decade/DecadePanel';
@@ -26,6 +27,7 @@ export default class CalendarHeader extends React.Component {
prefixCls: PropTypes.string,
value: PropTypes.object,
onValueChange: PropTypes.func,
+ showYear: PropTypes.func,
showTimePicker: PropTypes.bool,
onPanelChange: PropTypes.func,
locale: PropTypes.object,
@@ -34,6 +36,7 @@ export default class CalendarHeader extends React.Component {
disabledMonth: PropTypes.func,
renderFooter: PropTypes.func,
onMonthSelect: PropTypes.func,
+ onQuarterSelect: PropTypes.func,
}
static defaultProps = {
@@ -63,11 +66,23 @@ export default class CalendarHeader extends React.Component {
}
}
+ onQuarterSelect = (value) => {
+ this.props.onPanelChange(value, 'date');
+ if (this.props.onQuarterSelect) {
+ this.props.onQuarterSelect(value);
+ } else {
+ this.props.onValueChange(value);
+ }
+ }
+
onYearSelect = (value) => {
const referer = this.state.yearPanelReferer;
this.setState({ yearPanelReferer: null });
this.props.onPanelChange(value, referer);
this.props.onValueChange(value);
+ if (this.props.showYear) {
+ this.props.showYear(false);
+ }
}
onDecadeSelect = (value) => {
@@ -136,6 +151,9 @@ export default class CalendarHeader extends React.Component {
showYearPanel = (referer) => {
this.setState({ yearPanelReferer: referer });
this.props.onPanelChange(null, 'year');
+ if (this.props.showYear) {
+ this.props.showYear(true);
+ }
}
showDecadePanel = () => {
@@ -153,6 +171,7 @@ export default class CalendarHeader extends React.Component {
enableNext,
enablePrev,
disabledMonth,
+ disabledQuarter,
renderFooter,
} = props;
@@ -173,6 +192,21 @@ export default class CalendarHeader extends React.Component {
/>
);
}
+ if (mode === 'quarter') {
+ panel = (
+ this.showYearPanel('quarter')}
+ disabledDate={disabledQuarter}
+ cellRender={props.quarterCellRender}
+ contentRender={props.quarterCellContentRender}
+ changeYear={this.changeYear}
+ />
+ );
+ }
if (mode === 'year') {
panel = (
{
+ this.setValue(value);
+ this.props.onSelect(value);
+ }
+
+ setValue(value) {
+ if (!('value' in this.props)) {
+ this.setState({
+ value,
+ });
+ }
+ }
+
+ render() {
+ const props = this.props;
+ const value = this.state.value;
+ const cellRender = props.cellRender;
+ const contentRender = props.contentRender;
+ const { locale } = props;
+ const year = value.year();
+ const prefixCls = this.prefixCls;
+
+ return (
+
+ );
+ }
+}
+
+QuarterPanel.defaultProps = {
+ onChange: noop,
+ onSelect: noop,
+};
+
+QuarterPanel.propTypes = {
+ value: PropTypes.object,
+ defaultValue: PropTypes.object,
+ rootPrefixCls: PropTypes.string,
+ onChange: PropTypes.func,
+ disabledDate: PropTypes.func,
+ onSelect: PropTypes.func,
+};
+
+export default QuarterPanel;
diff --git a/src/quarter/QuarterTable.js b/src/quarter/QuarterTable.js
new file mode 100644
index 00000000..dd8e0afc
--- /dev/null
+++ b/src/quarter/QuarterTable.js
@@ -0,0 +1,140 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import classnames from 'classnames';
+import { getTodayTime, getQuarterName } from '../util/index';
+
+const ROW = 1;
+const COL = 4;
+
+function chooseQuarter(quarter) {
+ const next = this.state.value.clone();
+ next.quarter(quarter);
+ this.setAndSelectValue(next);
+}
+
+function noop() {
+
+}
+
+class QuarterTable extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ value: props.value,
+ };
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if ('value' in nextProps) {
+ this.setState({
+ value: nextProps.value,
+ });
+ }
+ }
+
+ setAndSelectValue(value) {
+ this.setState({
+ value,
+ });
+ this.props.onSelect(value);
+ }
+
+ quarters() {
+ const value = this.state.value;
+ const current = value.clone();
+ const quarters = [];
+ let index = 1;
+ for (let rowIndex = 0; rowIndex < ROW; rowIndex++) {
+ quarters[rowIndex] = [];
+ for (let colIndex = 0; colIndex < COL; colIndex++) {
+ current.quarters(index);
+ const content = getQuarterName(current);
+ quarters[rowIndex][colIndex] = {
+ value: index,
+ content,
+ title: content,
+ };
+ index++;
+ }
+ }
+ return quarters;
+ }
+
+ render() {
+ const props = this.props;
+ const value = this.state.value;
+ const today = getTodayTime(value);
+ const quarters = this.quarters();
+ const currentQuarter = value.quarter();
+ const { prefixCls, locale, contentRender, cellRender } = props;
+ const quarterEls = quarters.map((quarter, index) => {
+ const tds = quarter.map(quarterData => {
+ let disabled = false;
+ if (props.disabledDate) {
+ const testValue = value.clone();
+ testValue.quarter(quarterData.value);
+ disabled = props.disabledDate(testValue);
+ }
+ const classNameMap = {
+ [`${prefixCls}-cell`]: 1,
+ [`${prefixCls}-cell-disabled`]: disabled,
+ [`${prefixCls}-selected-cell`]: quarterData.value === currentQuarter,
+ [`${prefixCls}-current-cell`]: today.year() === value.year() &&
+ quarterData.value === today.quarter(),
+ };
+ let cellEl;
+ if (cellRender) {
+ const currentValue = value.clone();
+ currentValue.quarter(quarterData.value);
+ cellEl = cellRender(currentValue, locale);
+ } else {
+ let content;
+ if (contentRender) {
+ const currentValue = value.clone();
+ currentValue.quarter(quarterData.value);
+ content = contentRender(currentValue, locale);
+ } else {
+ content = quarterData.content;
+ }
+ cellEl = (
+
+ {content}
+
+ );
+ }
+ return (
+
+ {cellEl}
+ | );
+ });
+ return ({tds}
);
+ });
+
+ return (
+
+ );
+ }
+}
+
+QuarterTable.defaultProps = {
+ onSelect: noop,
+};
+QuarterTable.propTypes = {
+ onSelect: PropTypes.func,
+ cellRender: PropTypes.func,
+ prefixCls: PropTypes.string,
+ value: PropTypes.object,
+};
+
+export default QuarterTable;
diff --git a/src/util/index.js b/src/util/index.js
index 260f3ba8..80703712 100644
--- a/src/util/index.js
+++ b/src/util/index.js
@@ -12,6 +12,8 @@ const defaultDisabledTime = {
},
};
+const quarterTitle = [['1季度', '2季度', '3季度', '4季度'], ['Q1', 'Q2', 'Q3', 'Q4']];
+
export function getTodayTime(value) {
const today = moment();
today.locale(value.locale()).utcOffset(value.utcOffset());
@@ -33,6 +35,12 @@ export function getMonthName(month) {
return localeData[locale === 'zh-cn' ? 'months' : 'monthsShort'](month);
}
+export function getQuarterName(quarterM) {
+ const locale = quarterM.locale();
+ const quarterN = quarterM.quarter();
+ return quarterTitle[locale === 'zh-cn' ? 0 : 1][quarterN - 1];
+}
+
export function syncTime(from, to) {
if (!moment.isMoment(from) || !moment.isMoment(to)) return;
to.hour(from.hour());
diff --git a/tests/QuarterCalendar.spec.js b/tests/QuarterCalendar.spec.js
new file mode 100644
index 00000000..f567e435
--- /dev/null
+++ b/tests/QuarterCalendar.spec.js
@@ -0,0 +1,108 @@
+/* eslint-disable no-undef */
+import React from 'react';
+import { mount } from 'enzyme';
+import keyCode from 'rc-util/lib/KeyCode';
+import moment from 'moment';
+import QuarterCalendar from '../src/QuarterCalendar';
+
+describe('QuarterCalendar', () => {
+ it('year or decade panel work correctly', () => {
+ const format = 'YYYY-Q';
+ const wrapper = mount();
+ wrapper.find('.rc-calendar-quarter-panel-year-select').simulate('click');
+ wrapper.find('.rc-calendar-year-panel-decade-select').simulate('click');
+ wrapper.find('.rc-calendar-decade-panel-selected-cell').simulate('click');
+ wrapper.find('.rc-calendar-year-panel-selected-cell').simulate('click');
+ wrapper.find('.rc-calendar-quarter-panel-selected-cell').simulate('click');
+ expect(wrapper.state('selectedValue').format(format)).toBe('2010-1');
+ });
+ describe('keyboard', () => {
+ let wrapper;
+ beforeEach(() => {
+ const selected = moment().add(1, 'Q');
+ wrapper = mount();
+ });
+
+ it('enter to select works', () => {
+ const onSelect = jest.fn();
+ wrapper = mount();
+ wrapper.simulate('keydown', {
+ keyCode: keyCode.ENTER,
+ });
+ expect(onSelect).toHaveBeenCalledWith(wrapper.state('value'), undefined);
+ });
+
+ it('enter not to select disabled quarter', () => {
+ const onSelect = jest.fn();
+ function disabledDate(current) {
+ if (!current) {
+ return false;
+ }
+ return true;
+ }
+
+ wrapper = mount();
+ wrapper.simulate('keydown', {
+ keyCode: keyCode.LEFT,
+ });
+ wrapper.simulate('keydown', {
+ keyCode: keyCode.ENTER,
+ });
+ expect(onSelect).not.toHaveBeenCalled();
+ });
+
+ it('DOWN', () => {
+ wrapper.simulate('keydown', {
+ keyCode: keyCode.DOWN,
+ });
+ expect(wrapper.state().value.quarter()).toBe(2);
+ });
+
+ it('UP', () => {
+ wrapper.simulate('keydown', {
+ keyCode: keyCode.UP,
+ });
+ expect(wrapper.state().value.quarter()).toBe(2);
+ });
+
+ it('LEFT', () => {
+ wrapper.simulate('keydown', {
+ keyCode: keyCode.LEFT,
+ });
+ expect(wrapper.state().value.quarter()).toBe(1);
+ });
+
+ it('RIGHT', () => {
+ wrapper.simulate('keydown', {
+ keyCode: keyCode.RIGHT,
+ });
+ expect(wrapper.state().value.quarter()).toBe(3);
+ });
+
+ it('CTRL + LEFT', () => {
+ wrapper.simulate('keydown', {
+ keyCode: keyCode.LEFT,
+ ctrlKey: 1,
+ });
+ expect(wrapper.state().value.quarter()).toBe(2);
+ expect(wrapper.state().value.year()).toBe(2016);
+ });
+
+ it('CTRL + RIGHT', () => {
+ wrapper.simulate('keydown', {
+ keyCode: keyCode.RIGHT,
+ ctrlKey: 1,
+ });
+ expect(wrapper.state().value.quarter()).toBe(2);
+ expect(wrapper.state().value.year()).toBe(2018);
+ });
+
+ it('ignore other keys', () => {
+ wrapper.simulate('keydown', {
+ keyCode: keyCode.A,
+ });
+ expect(wrapper.state().value.quarter()).toBe(2);
+ expect(wrapper.state().value.year()).toBe(2017);
+ });
+ });
+});