yhhu 5 роки тому
батько
коміт
062aaf7a08

BIN
docs/datepicker.gif


+ 6 - 1
src/app.js

@@ -2,6 +2,7 @@
 import React from 'react'
 import ReactDOM from 'react-dom'
 import DatePicker from './index'
+import './styles/index.css'
 
 const { MonthPicker } = DatePicker
 
@@ -15,17 +16,21 @@ const { MonthPicker } = DatePicker
 
 const App = () => (
   <React.Fragment>
+    <p>日历模式</p>
     <DatePicker
-      // disable
+      inline
       // disabledDate={current => disabledDate(current)}
       defaultDate="2018-01-31"
       placeholder="please choose date"
       onSelectDate={day => console.log(day)}
     />
+    <p>月份选择模式</p>
     <MonthPicker
+      inline
       placeholder="Select month"
       year="2018"
       month="01"
+      onSelectMonth={month => console.log(month)}
     />
   </React.Fragment>
 )

+ 5 - 2
src/components/calendar/Header.js

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
 import Styles from './header.css'
 import { DateContext } from '../../context'
 import { withContext, formatMonthOrDay } from '../../utils'
+import { MONTH_DECADE_MODE, MONTH_MODE } from '../../const'
 
 const Header = ({
   context: {
@@ -12,14 +13,15 @@ const Header = ({
     onPrevYear,
     onNextMonth,
     onNextYear,
+    onMonthModalOpen,
   },
 }) => (
   <div className={Styles.wrapper}>
     <i className={Styles.prevYear} role="presentation" title="上一年" onClick={e => onPrevYear(e)} />
     <i className={Styles.prevMonth} role="presentation" title="上一月" onClick={e => onPrevMonth(e)} />
     <div className={Styles.text}>
-      <span className={Styles.link}>{`${year}年`}</span>
-      <span className={Styles.link}>{`${formatMonthOrDay(month)}月`}</span>
+      <span className={Styles.link} role="presentation" onClick={e => onMonthModalOpen(MONTH_DECADE_MODE, e)}>{`${year}年`}</span>
+      <span className={Styles.link} role="presentation" onClick={e => onMonthModalOpen(MONTH_MODE, e)}>{`${formatMonthOrDay(month)}月`}</span>
     </div>
     <i className={Styles.nextMonth} role="presentation" title="下一月" onClick={e => onNextMonth(e)} />
     <i className={Styles.nextYear} role="presentation" title="下一年" onClick={e => onNextYear(e)} />
@@ -34,6 +36,7 @@ Header.propTypes = {
     onPrevYear: PropTypes.func.isRequired,
     onNextMonth: PropTypes.func.isRequired,
     onNextYear: PropTypes.func.isRequired,
+    onMonthModalOpen: PropTypes.func.isRequired,
   }).isRequired,
 }
 

+ 6 - 3
src/components/delayUnmounting.js

@@ -1,3 +1,4 @@
+/* eslint-disable react/no-did-update-set-state */
 import React from 'react'
 
 export default function delayUnmounting(Component) {
@@ -11,11 +12,13 @@ export default function delayUnmounting(Component) {
     }
 
     componentDidUpdate(prevProps) {
-      const { isMounted, delayTime } = this.props
+      const { isMounted, delayTime, animation } = this.props
       if (prevProps.isMounted && !isMounted) {
-        setTimeout(() => this.setState({ shouldRender: false }), delayTime)
+        if (animation) {
+          setTimeout(() => this.setState({ shouldRender: false }), delayTime)
+        }
+        this.setState({ shouldRender: false })
       } else if (!prevProps.isMounted && isMounted) {
-        /* eslint-disable react/no-did-update-set-state */
         this.setState({ shouldRender: true })
       }
     }

+ 80 - 8
src/components/index/DatePicker.js

@@ -8,10 +8,17 @@ import {
   getCurrentMonth,
   isDateValid,
   getCurrentDate,
+  formatMonthOrDay,
 } from '../../utils'
 import Modal from '../modal/Modal'
 import {
-  CHINESE_MODEL, WESTERN_MODEL, _, INPUT_DEFAULT_PLACEHOLDER, noop,
+  CHINESE_MODEL,
+  WESTERN_MODEL,
+  _,
+  INPUT_DEFAULT_PLACEHOLDER,
+  noop,
+  MONTH_MODE,
+  MONTH_DECADE_MODE,
 } from '../../const'
 import { DateContext, initialData } from '../../context'
 import {
@@ -23,7 +30,6 @@ import {
   isInCurrentMonth,
   resetCalendarFromSpecialDay,
 } from '../../helper'
-import '../../utils/closest-polyfill'
 
 /* eslint-disable no-underscore-dangle */
 class DatePicker extends Component {
@@ -35,6 +41,10 @@ class DatePicker extends Component {
       month: month,
       value: defaultDate,
       showModal: false,
+      showMonthModal: false,
+      decade: year,
+      mode: MONTH_MODE,
+      monthValue: `${year}-${formatMonthOrDay(month)}`,
       ...initialData,
     }
   }
@@ -189,12 +199,68 @@ class DatePicker extends Component {
     this._onChangeYearOrMonth(+year + 1, month)
   }
 
+  onMonthModalOpen = mode => {
+    this.setState({ mode: mode, showMonthModal: true })
+  }
+
+  onMonthModalClose = () => {
+    this.setState({ showMonthModal: false })
+  }
+
+  onChangeMode = mode => {
+    if (mode === MONTH_MODE) {
+      this.setState({ mode: MONTH_DECADE_MODE })
+    }
+  }
+
+  onSelectYearOrMonth = val => {
+    const { mode } = this.state
+    if (mode === MONTH_DECADE_MODE) {
+      this.setState({ mode: MONTH_MODE, year: val, decade: val })
+    } else {
+      const { year } = this.state
+      this.setState({ month: val, monthValue: `${year}-${formatMonthOrDay(val)}` }, () => {
+        this._onChangeYearOrMonth(year, val)
+        this.onMonthModalClose()
+      })
+    }
+  }
+
+  onPrev = () => {
+    const { mode } = this.state
+    if (mode === MONTH_DECADE_MODE) {
+      const { decade } = this.state
+      this.setState({ decade: +decade - 10 })
+    } else {
+      const { year } = this.state
+      this.setState({ year: +year - 1, decade: +year - 1 })
+    }
+  }
+
+  onNext = () => {
+    const { mode } = this.state
+    if (mode === MONTH_DECADE_MODE) {
+      const { decade } = this.state
+      this.setState({ decade: +decade + 10 })
+    } else {
+      const { year } = this.state
+      this.setState({ year: +year + 1, decade: +year + 1 })
+    }
+  }
+
   _addGlobalClickListener() {
     this.globalClickListener = document.addEventListener('click', event => {
       if (event.target.closest('.picker-wrapper')) {
         return
       }
 
+      // hack
+      const cNames = event.target.className
+      const { mode } = this.state
+      if (cNames.includes('month__td') && mode === MONTH_MODE) {
+        return
+      }
+
       const { value } = this.state
       if (!isDateValid(value)) {
         this.onSelectToday(getDateFormatFromSepecificDate())
@@ -214,15 +280,12 @@ class DatePicker extends Component {
   }
 
   renderInput = () => {
-    const { inline, placeholder, disable } = this.props
+    const { placeholder, disable } = this.props
     const { value } = this.state
 
     return (
       <React.Fragment>
-        <div
-          className={Styles.container}
-          style={inline ? { display: 'inline-block' } : {}}
-        >
+        <div className={Styles.container}>
           <span className={Styles.inputWrapper}>
             <input
               type="text"
@@ -264,6 +327,11 @@ class DatePicker extends Component {
             onNextMonth: this.onNextMonth,
             onNextYear: this.onNextYear,
             onInputChange: this.onInputChange,
+            onChangeMode: this.onChangeMode,
+            onSelectYearOrMonth: this.onSelectYearOrMonth,
+            onPrev: this.onPrev,
+            onNext: this.onNext,
+            onMonthModalOpen: this.onMonthModalOpen,
           }
         }
       >
@@ -273,8 +341,12 @@ class DatePicker extends Component {
   }
 
   render() {
+    const { inline } = this.props
     return (
-      <div className={`picker-wrapper ${Styles.wrapper}`}>
+      <div
+        className={`picker-wrapper ${Styles.wrapper}`}
+        style={inline ? { display: 'inline-block' } : {}}
+      >
         { this.renderInput() }
         { this.renderModal() }
       </div>

+ 56 - 14
src/components/index/MonthPicker.js

@@ -7,7 +7,9 @@ import {
   MONTH_DEFAULT_PLACEHOLDER, noop, MONTH_MODE, MONTH_DECADE_MODE,
 } from '../../const'
 import MonthModal from '../modal/MonthModal'
-import { getCurrentYear, getCurrentMonth, formatMonthOrDay } from '../../utils'
+import {
+  getCurrentYear, getCurrentMonth, formatMonthOrDay,
+} from '../../utils'
 
 class MonthPicker extends React.Component {
   constructor(props) {
@@ -17,11 +19,20 @@ class MonthPicker extends React.Component {
       showModal: false,
       year: year,
       month: month,
+      decade: year,
       mode: MONTH_MODE,
       value: `${year}-${formatMonthOrDay(month)}`,
     }
   }
 
+  componentDidMount() {
+    this._addGlobalClickListener()
+  }
+
+  componentWillUnmount() {
+    this._removeGlobalClickListener()
+  }
+
   onModalOpen = () => {
     this.setState({ showModal: true })
   }
@@ -47,43 +58,67 @@ class MonthPicker extends React.Component {
   onSelectYearOrMonth = val => {
     const { mode } = this.state
     if (mode === MONTH_DECADE_MODE) {
-      this.setState({ mode: MONTH_MODE, year: val })
+      this.setState({ mode: MONTH_MODE, year: val, decade: val })
     } else {
       const { year } = this.state
-      this.setState({ month: val, value: `${year}-${formatMonthOrDay(val)}` }, () => this.onModalClose())
+      const { onSelectMonth } = this.props
+      this.setState({ month: val, value: `${year}-${formatMonthOrDay(val)}` }, () => {
+        onSelectMonth(`${year}-${formatMonthOrDay(val)}`)
+        this.onModalClose()
+      })
     }
   }
 
   onPrev = () => {
     const { mode } = this.state
     if (mode === MONTH_DECADE_MODE) {
-      console.log('MONTH_DECADE_MODE')
+      const { decade } = this.state
+      this.setState({ decade: +decade - 10 })
     } else {
       const { year } = this.state
-      this.setState({ year: +year - 1 })
+      this.setState({ year: +year - 1, decade: +year - 1 })
     }
   }
 
   onNext = () => {
     const { mode } = this.state
     if (mode === MONTH_DECADE_MODE) {
-      console.log('MONTH_DECADE_MODE')
+      const { decade } = this.state
+      this.setState({ decade: +decade + 10 })
     } else {
       const { year } = this.state
-      this.setState({ year: +year + 1 })
+      this.setState({ year: +year + 1, decade: +year + 1 })
     }
   }
 
+  _addGlobalClickListener() {
+    this.globalClickListener = document.addEventListener('click', event => {
+      if (event.target.closest('#monthPickerWrapper')) {
+        return
+      }
+
+      // hack
+      const cNames = event.target.className
+      const { mode } = this.state
+      if (cNames.includes('month__td') && mode === MONTH_MODE) {
+        return
+      }
+
+      this.onModalClose()
+    })
+  }
+
+  _removeGlobalClickListener() {
+    document.removeEventListener('click', this.globalClickListener)
+  }
+
   renderInput = () => {
-    const { inline, placeholder, disable } = this.props
+    const { placeholder, disable } = this.props
     const { value } = this.state
 
     return (
       <React.Fragment>
-        <div
-          className={Styles.container}
-          style={inline ? { display: 'inline-block' } : {}}
-        >
+        <div className={Styles.container}>
           <span className={Styles.inputWrapper}>
             <input
               type="text"
@@ -111,7 +146,7 @@ class MonthPicker extends React.Component {
 
   renderModal = () => {
     const {
-      showModal, year, month, mode,
+      showModal, year, month, mode, decade,
     } = this.state
     return (
       <MonthModal
@@ -119,6 +154,7 @@ class MonthPicker extends React.Component {
         delayTime={200}
         year={year}
         month={month}
+        decade={decade}
         title={this._getTitleByMode(mode)}
         mode={mode}
         onChangeMode={this.onChangeMode}
@@ -130,8 +166,13 @@ class MonthPicker extends React.Component {
   }
 
   render() {
+    const { inline } = this.props
     return (
-      <div className={`picker-wrapper ${Styles.wrapper}`}>
+      <div
+        id="monthPickerWrapper"
+        className={`${Styles.wrapper}`}
+        style={inline ? { display: 'inline-block' } : {}}
+      >
         { this.renderInput() }
         { this.renderModal() }
       </div>
@@ -153,6 +194,7 @@ MonthPicker.propTypes = {
   year: PropTypes.string,
   month: PropTypes.string,
   disable: PropTypes.bool,
+  onSelectMonth: PropTypes.func.isRequired,
 }
 
 export default MonthPicker

+ 40 - 1
src/components/modal/Modal.js

@@ -7,9 +7,23 @@ import Input from '../input/Input'
 import Header from '../calendar/Header'
 import Body from '../calendar/Index'
 import Footer from '../footer/Footer'
+import MonthModal from './MonthModal'
+import { DateContext } from '../../context'
+import { withContext } from '../../utils'
 
 const Modal = ({
   isMounted,
+  context: {
+    year,
+    month,
+    decade,
+    mode,
+    showMonthModal,
+    onChangeMode,
+    onSelectYearOrMonth,
+    onPrev,
+    onNext,
+  },
 }) => (
   <React.Fragment>
     <div className={classNames(Styles.container, {
@@ -21,6 +35,20 @@ const Modal = ({
       <div className={Styles.panel}>
         <div className={Styles.header}>
           <Header />
+          <MonthModal
+            isMounted={showMonthModal}
+            animation={false}
+            delayTime={200}
+            year={year}
+            month={month}
+            decade={decade}
+            title={year}
+            mode={mode}
+            onChangeMode={onChangeMode}
+            onSelectYearOrMonth={onSelectYearOrMonth}
+            onPrev={onPrev}
+            onNext={onNext}
+          />
         </div>
         <Body />
         <Footer />
@@ -35,6 +63,17 @@ Modal.defaultProps = {
 
 Modal.propTypes = {
   isMounted: PropTypes.bool,
+  context: PropTypes.shape({
+    year: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
+    month: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
+    decade: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
+    mode: PropTypes.string.isRequired,
+    showMonthModal: PropTypes.bool.isRequired,
+    onChangeMode: PropTypes.func.isRequired,
+    onSelectYearOrMonth: PropTypes.func.isRequired,
+    onPrev: PropTypes.func.isRequired,
+    onNext: PropTypes.func.isRequired,
+  }).isRequired,
 }
 
-export default delayUnmounting(Modal)
+export default withContext(DateContext, delayUnmounting(Modal))

+ 11 - 6
src/components/modal/MonthModal.js

@@ -11,11 +11,13 @@ import { getDecadeByGivenYear, getChineseMonth } from '../../helper'
 
 class MonthModal extends React.Component {
   _getRenderBody = () => {
-    const { year, month, mode } = this.props
+    const {
+      year, decade, month, mode,
+    } = this.props
     if (mode === MONTH_MODE) {
       return getChineseMonth(month)
     }
-    return getDecadeByGivenYear(year)
+    return getDecadeByGivenYear(decade, year)
   }
 
   _getPrevAndNextTitles = () => {
@@ -49,7 +51,7 @@ class MonthModal extends React.Component {
           <span
             role="presentation"
             className={classNames({ [HeaderStyles.link]: mode === MONTH_MODE })}
-            onClick={mode === MONTH_DECADE_MODE ? noop : e => onChangeMode(e)}
+            onClick={mode === MONTH_DECADE_MODE ? noop : e => onChangeMode(mode, e)}
           >
             {title}
           </span>
@@ -92,15 +94,15 @@ class MonthModal extends React.Component {
   }
 
   render() {
-    const { isMounted } = this.props
+    const { isMounted, animation } = this.props
 
     return (
       <React.Fragment>
         <div className={classNames(
           `${Styles.container} ${Styles.monthContainer}`,
           {
-            [Styles.in]: isMounted,
-            [Styles.out]: !isMounted,
+            [Styles.in]: animation && isMounted,
+            [Styles.out]: animation && !isMounted,
           },
         )}
         >
@@ -116,12 +118,15 @@ class MonthModal extends React.Component {
 
 MonthModal.defaultProps = {
   isMounted: false,
+  animation: true,
 }
 
 MonthModal.propTypes = {
   isMounted: PropTypes.bool,
+  animation: PropTypes.bool,
   title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
   year: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
+  decade: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
   month: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
   mode: PropTypes.string.isRequired,
   onPrev: PropTypes.func.isRequired,

+ 25 - 29
src/helper.js

@@ -194,46 +194,42 @@ export const resetCalendarFromSpecialDay = (originDays, date,
   return { afterDays }
 }
 
-export const getDecadeByGivenYear = year => {
-  const tempYear = +year
-  const factor = Math.floor(tempYear / 10)
-  const result = []
+export const getDecadeByGivenYear = (decade, year) => {
+  const tempDecade = +decade
+  const factor = Math.floor(tempDecade / 10)
+  const result = Array(12).fill({})
 
-  for (let i = -1; i <= 10; i++) {
-    const r = 10 * factor + i
+  result[0] = {
+    flag: 'prev',
+    value: 10 * factor - 1,
+    code: 10 * factor - 1,
+  }
 
-    if (i === -1) {
-      result.push({
-        flag: 'prev',
-        value: r,
-        code: r,
-      })
-    } else if (r < tempYear) {
-      result.push({
-        flag: 'normal',
-        value: r,
-        code: r,
-      })
-    } else if (r === tempYear) {
-      result.push({
+  result[11] = {
+    flag: 'next',
+    value: 10 * (factor + 1),
+    code: 10 * (factor + 1),
+  }
+
+  for (let i = 0; i <= 9; i++) {
+    const r = 10 * factor + i
+    /* eslint-disable eqeqeq */
+    if (r === tempDecade && year == tempDecade) {
+      /* eslint-enable eqeqeq */
+      result[i + 1] = {
         flag: 'current',
         value: r,
         code: r,
-      })
-    } else if (i === 10) {
-      result.push({
-        flag: 'next',
-        value: r,
-        code: r,
-      })
+      }
     } else {
-      result.push({
+      result[i + 1] = {
         flag: 'normal',
         value: r,
         code: r,
-      })
+      }
     }
   }
+
   return result
 }
 

+ 1 - 1
src/index.js

@@ -1,6 +1,6 @@
+import './utils/closest-polyfill'
 import DatePicker from './components/index/DatePicker'
 import MonthPicker from './components/index/MonthPicker'
-import './styles/index.css'
 
 DatePicker.MonthPicker = MonthPicker
 

+ 6 - 0
src/styles/index.css

@@ -1,3 +1,9 @@
 :global(.container) {
   width: 170px;
+  margin: 70px auto;
+}
+:global(.container > p) {
+  margin-top: 120px;
+  font-size: 16px;
+  font-family: "Chinese Quote", -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
 }