Bläddra i källkod

Merge branch 'master' of zhangyuhan/element-ui into master

zhangyuhan 11 månader sedan
förälder
incheckning
deb1819a75

+ 20 - 6
README.md

@@ -36,12 +36,26 @@
   </a>
 </p>
 
-> @2.15.9-rc 剑鱼内网版本 element-ui
-Q:有什么不同?
-A: 因为当前2.x版本已经不是主要维护版本,pr的响应和合并不可预知,因此采用内网版本。
-  1. node-sass 替换为 sass
-  2. shadow dom scrollTop等属性获取兼容
-  2. popover、date-picker 系列组件的 shadow dom 事件兼容
+> @2.15.17-rc 剑鱼内网版本 element-ui
+
+> Q:有什么不同?
+
+> A: 因为当前2.x版本已经不是主要维护版本,pr的响应和合并不可预知,因此采用内网版本。
+
+
+> 内网版本变更记录:
+
+> v.2.15.18-rc
+1. 2.15.9 -> 2.15.14 pr bug 合并
+2. 支持 date-picker daterange 范围选择中`年、月`标签快捷选择年月,新增 props `range-label-click` 控制,默认启用。
+
+
+> v.2.15.16-rc
+1. node-sass 替换为 sass
+2. shadow dom scrollTop等属性获取兼容
+3. popover、date-picker 系列组件的 shadow dom 事件兼容
+4. 分页样式定制
+
 > A Vue.js 2.0 UI Toolkit for Web.
 
 Element will stay with Vue 2.x

+ 1 - 1
examples/versions.json

@@ -1 +1 @@
-{"1.4.13":"1.4","2.0.11":"2.0","2.1.0":"2.1","2.2.2":"2.2","2.3.9":"2.3","2.4.11":"2.4","2.5.4":"2.5","2.6.3":"2.6","2.7.2":"2.7","2.8.2":"2.8","2.9.2":"2.9","2.10.1":"2.10","2.11.1":"2.11","2.12.0":"2.12","2.13.2":"2.13","2.14.1":"2.14","2.15.15-rc":"2.15"}
+{"1.4.13":"1.4","2.0.11":"2.0","2.1.0":"2.1","2.2.2":"2.2","2.3.9":"2.3","2.4.11":"2.4","2.5.4":"2.5","2.6.3":"2.6","2.7.2":"2.7","2.8.2":"2.8","2.9.2":"2.9","2.10.1":"2.10","2.11.1":"2.11","2.12.0":"2.12","2.13.2":"2.13","2.14.1":"2.14","2.15.18-rc":"2.15"}

+ 1 - 4
package.json

@@ -1,6 +1,6 @@
 {
   "name": "element-ui",
-  "version": "2.15.15-rc",
+  "version": "2.15.18-rc",
   "description": "A Component Library for Vue.js.",
   "main": "lib/element-ui.common.js",
   "files": [
@@ -145,8 +145,5 @@
     "webpack-cli": "^3.0.8",
     "webpack-dev-server": "^3.1.11",
     "webpack-node-externals": "^1.7.2"
-  },
-  "resolutions": {
-    "**/**/fsevents": "^1.2.9"
   }
 }

+ 15 - 0
packages/date-picker/src/basic/month-table.vue

@@ -37,6 +37,14 @@
       return NaN;
     }
   };
+
+  // remove the first element that satisfies `pred` from arr
+  // return a new array if modification occurs
+  // return the original array otherwise
+  const removeFromArray = function(arr, pred) {
+    const idx = typeof pred === 'function' ? arrayFindIndex(arr, pred) : arr.indexOf(pred);
+    return idx >= 0 ? [...arr.slice(0, idx), ...arr.slice(idx + 1)] : arr;
+  };
   export default {
     props: {
       disabledDate: {},
@@ -205,6 +213,13 @@
             }
             this.rangeState.selecting = false;
           }
+        } else if (this.selectionMode === 'months') {
+          const value = this.value || [];
+          const year = this.date.getFullYear();
+          const newValue = arrayFindIndex(value, date => date.getFullYear() === year && date.getMonth() === month) >= 0
+            ? removeFromArray(value, date => date.getTime() === newDate.getTime())
+            : [...value, newDate];
+          this.$emit('pick', newValue);
         } else {
           this.$emit('pick', month);
         }

+ 52 - 42
packages/date-picker/src/basic/year-table.vue

@@ -44,58 +44,68 @@
 </template>
 
 <script type="text/babel">
-  import { hasClass } from 'element-ui/src/utils/dom';
-  import { isDate, range, nextDate, getDayCountOfYear } from 'element-ui/src/utils/date-util';
-  import { arrayFindIndex, coerceTruthyValueToArray } from 'element-ui/src/utils/util';
+import { hasClass } from 'element-ui/src/utils/dom';
+import { isDate, range, nextDate, getDayCountOfYear } from 'element-ui/src/utils/date-util';
+import { arrayFindIndex, coerceTruthyValueToArray } from 'element-ui/src/utils/util';
 
-  const datesInYear = year => {
-    const numOfDays = getDayCountOfYear(year);
-    const firstDay = new Date(year, 0, 1);
-    return range(numOfDays).map(n => nextDate(firstDay, n));
-  };
+const datesInYear = year => {
+  const numOfDays = getDayCountOfYear(year);
+  const firstDay = new Date(year, 0, 1);
+  return range(numOfDays).map(n => nextDate(firstDay, n));
+};
 
-  export default {
-    props: {
-      disabledDate: {},
-      value: {},
-      defaultValue: {
-        validator(val) {
-          // null or valid Date Object
-          return val === null || (val instanceof Date && isDate(val));
-        }
-      },
-      date: {}
-    },
-
-    computed: {
-      startYear() {
-        return Math.floor(this.date.getFullYear() / 10) * 10;
+export default {
+  props: {
+    disabledDate: {},
+    value: {},
+    defaultValue: {
+      validator(val) {
+        // null or valid Date Object
+        return val === null || (val instanceof Date && isDate(val));
       }
     },
+    date: {},
+    selectionMode: {}
+  },
 
-    methods: {
-      getCellStyle(year) {
-        const style = {};
-        const today = new Date();
+  computed: {
+    startYear() {
+      return Math.floor(this.date.getFullYear() / 10) * 10;
+    }
+  },
+
+  methods: {
+    getCellStyle(year) {
+      const style = {};
+      const today = new Date();
 
-        style.disabled = typeof this.disabledDate === 'function'
-          ? datesInYear(year).every(this.disabledDate)
-          : false;
-        style.current = arrayFindIndex(coerceTruthyValueToArray(this.value), date => date.getFullYear() === year) >= 0;
-        style.today = today.getFullYear() === year;
-        style.default = this.defaultValue && this.defaultValue.getFullYear() === year;
+      style.disabled = typeof this.disabledDate === 'function'
+        ? datesInYear(year).every(this.disabledDate)
+        : false;
+      style.current = arrayFindIndex(coerceTruthyValueToArray(this.value), date => date.getFullYear() === year) >= 0;
+      style.today = today.getFullYear() === year;
+      style.default = this.defaultValue && this.defaultValue.getFullYear() === year;
 
-        return style;
-      },
+      return style;
+    },
 
-      handleYearTableClick(event) {
-        const target = event.target;
-        if (target.tagName === 'A') {
-          if (hasClass(target.parentNode, 'disabled')) return;
-          const year = target.textContent || target.innerText;
+    handleYearTableClick(event) {
+      const target = event.target;
+      if (target.tagName === 'A') {
+        if (hasClass(target.parentNode, 'disabled')) return;
+        const year = target.textContent || target.innerText;
+        if (this.selectionMode === 'years') {
+          const value = this.value || [];
+          const idx = arrayFindIndex(value, date => date.getFullYear() === Number(year));
+          const newValue = idx > -1
+            ? [...value.slice(0, idx), ...value.slice(idx + 1)]
+            : [...value, new Date(year)];
+          this.$emit('pick', newValue);
+        } else {
           this.$emit('pick', Number(year));
         }
       }
     }
-  };
+  }
+};
 </script>

+ 154 - 11
packages/date-picker/src/panel/date-range.vue

@@ -91,6 +91,7 @@
                 @click="leftPrevYear"
                 class="el-picker-panel__icon-btn el-icon-d-arrow-left"></button>
               <button
+                v-show="canShowLeftDatePanel"
                 type="button"
                 @click="leftPrevMonth"
                 class="el-picker-panel__icon-btn el-icon-arrow-left"></button>
@@ -104,13 +105,26 @@
               <button
                 type="button"
                 @click="leftNextMonth"
-                v-if="unlinkPanels"
+                v-if="unlinkPanels && canShowLeftDatePanel"
                 :disabled="!enableMonthArrow"
                 :class="{ 'is-disabled': !enableMonthArrow }"
                 class="el-picker-panel__icon-btn el-icon-arrow-right"></button>
-              <div>{{ leftLabel }}</div>
+              <div v-if="!rangeLabelClick">{{ leftLabel }}</div>
+              <div v-else>
+                <span
+                  @click="showYearPicker('left')"
+                  role="button"
+                  class="el-date-picker__header-label">{{ leftYearLabel }}</span>
+                <span
+                    @click="showMonthPicker('left')"
+                    v-show="canShowLeftDatePanel"
+                    role="button"
+                    class="el-date-picker__header-label"
+                    :class="{ active: currentView === 'left-month' }">{{t(`el.datepicker.month${ leftMonth + 1 }`)}}</span>
+              </div>
             </div>
             <date-table
+              v-show="canShowLeftDatePanel"
               selection-mode="range"
               :date="leftDate"
               :default-value="defaultValue"
@@ -123,6 +137,22 @@
               :first-day-of-week="firstDayOfWeek"
               @pick="handleRangePick">
             </date-table>
+            <year-table
+                v-show="currentView === 'left-year'"
+                @pick="handleYearPick($event, 'left')"
+                :selection-mode="selectionMode"
+                :default-value="defaultValue ? new Date(defaultValue) : null"
+                :date="leftDate"
+                :disabled-date="disabledDate">
+            </year-table>
+            <month-table
+                v-show="currentView === 'left-month'"
+                @pick="handleMonthPick($event, 'left')"
+                :selection-mode="selectionMode"
+                :default-value="defaultValue ? new Date(defaultValue) : null"
+                :date="leftDate"
+                :disabled-date="disabledDate">
+            </month-table>
           </div>
           <div class="el-picker-panel__content el-date-range-picker__content is-right">
             <div class="el-date-range-picker__header">
@@ -136,7 +166,7 @@
               <button
                 type="button"
                 @click="rightPrevMonth"
-                v-if="unlinkPanels"
+                v-if="unlinkPanels && canShowRightDatePanel"
                 :disabled="!enableMonthArrow"
                 :class="{ 'is-disabled': !enableMonthArrow }"
                 class="el-picker-panel__icon-btn el-icon-arrow-left"></button>
@@ -145,12 +175,26 @@
                 @click="rightNextYear"
                 class="el-picker-panel__icon-btn el-icon-d-arrow-right"></button>
               <button
+                v-show="canShowRightDatePanel"
                 type="button"
                 @click="rightNextMonth"
                 class="el-picker-panel__icon-btn el-icon-arrow-right"></button>
-              <div>{{ rightLabel }}</div>
+              <div v-if="!rangeLabelClick">{{ rightLabel }}</div>
+              <div v-else>
+                <span
+                    @click="showYearPicker('right')"
+                    role="button"
+                    class="el-date-picker__header-label">{{ rightYearLabel }}</span>
+                <span
+                    @click="showMonthPicker('right')"
+                    v-show="canShowRightDatePanel"
+                    role="button"
+                    class="el-date-picker__header-label"
+                    :class="{ active: currentView === 'right-month' }">{{t(`el.datepicker.month${ rightMonth + 1 }`)}}</span>
+              </div>
             </div>
             <date-table
+              v-show="canShowRightDatePanel"
               selection-mode="range"
               :date="rightDate"
               :default-value="defaultValue"
@@ -163,6 +207,22 @@
               :first-day-of-week="firstDayOfWeek"
               @pick="handleRangePick">
             </date-table>
+            <year-table
+                v-show="currentView === 'right-year'"
+                @pick="handleYearPick($event, 'right')"
+                :selection-mode="selectionMode"
+                :default-value="defaultValue ? new Date(defaultValue) : null"
+                :date="rightDate"
+                :disabled-date="disabledDate">
+            </year-table>
+            <month-table
+                v-show="currentView === 'right-month'"
+                @pick="handleMonthPick($event, 'right')"
+                :selection-mode="selectionMode"
+                :default-value="defaultValue ? new Date(defaultValue) : null"
+                :date="rightDate"
+                :disabled-date="disabledDate">
+            </month-table>
           </div>
         </div>
       </div>
@@ -200,12 +260,15 @@
     prevMonth,
     nextMonth,
     nextDate,
+    changeYearMonthAndClampDate,
     extractDateFormat,
     extractTimeFormat
   } from 'element-ui/src/utils/date-util';
   import Clickoutside from 'element-ui/src/utils/clickoutside';
   import Locale from 'element-ui/src/mixins/locale';
   import TimePicker from './time';
+  import YearTable from '../basic/year-table';
+  import MonthTable from '../basic/month-table';
   import DateTable from '../basic/date-table';
   import ElInput from 'element-ui/packages/input';
   import ElButton from 'element-ui/packages/button';
@@ -250,6 +313,26 @@
         return this.leftDate.getDate();
       },
 
+      leftYearLabel() {
+        return this.computedYearLabel(this.leftYear, !this.canShowLeftDatePanel && this.canShowYearPanel);
+      },
+
+      canShowLeftDatePanel() {
+        return this.currentView.indexOf('left') === -1;
+      },
+
+      canShowRightDatePanel() {
+        return this.currentView.indexOf('right') === -1;
+      },
+
+      canShowYearPanel() {
+        return this.currentView.indexOf('year') !== -1;
+      },
+
+      yearStep() {
+        return this.canShowYearPanel ? 10 : 1;
+      },
+
       rightYear() {
         return this.rightDate.getFullYear();
       },
@@ -262,6 +345,10 @@
         return this.rightDate.getDate();
       },
 
+      rightYearLabel() {
+        return this.computedYearLabel(this.rightYear, !this.canShowRightDatePanel && this.canShowYearPanel);
+      },
+
       minVisibleDate() {
         if (this.dateUserInput.min !== null) return this.dateUserInput.min;
         if (this.minDate) return formatDate(this.minDate, this.dateFormat);
@@ -340,6 +427,7 @@
         format: '',
         arrowControl: false,
         unlinkPanels: false,
+        rangeLabelClick: true,
         dateUserInput: {
           min: null,
           max: null
@@ -347,7 +435,9 @@
         timeUserInput: {
           min: null,
           max: null
-        }
+        },
+        currentView: 'date',
+        selectionMode: 'day'
       };
     },
 
@@ -440,6 +530,18 @@
     },
 
     methods: {
+      computedYearLabel(year, showYear = false) {
+        const yearTranslation = this.t('el.datepicker.year');
+        if (showYear) {
+          const startYear = Math.floor(year / 10) * 10;
+          if (yearTranslation) {
+            return startYear + ' ' + yearTranslation + ' - ' + (startYear + 9) + ' ' + yearTranslation;
+          }
+          return startYear + ' - ' + (startYear + 9);
+        }
+        return year + ' ' + yearTranslation;
+      },
+
       handleClear() {
         this.minDate = null;
         this.maxDate = null;
@@ -555,6 +657,38 @@
         this.handleConfirm();
       },
 
+      handleYearPick(year, direction) {
+        const pointVal = direction === 'left' ? 'leftDate' : 'rightDate';
+        const pointMonthVal = direction === 'left' ? this.leftMonth : this.rightMonth;
+        if (this.selectionMode === 'year') {
+          this[pointVal] = modifyDate(this[pointVal], year, 0, 1);
+          this.emit(this[pointVal]);
+        } else if (this.selectionMode === 'years') {
+          this.emit(year, true);
+        } else {
+          this[pointVal] = changeYearMonthAndClampDate(this[pointVal], year, pointMonthVal);
+          // TODO: should emit intermediate value ??
+          // this.emit(this.date, true);
+          this.currentView = 'month';
+        }
+      },
+
+      handleMonthPick(month, direction) {
+        const pointVal = direction === 'left' ? 'leftDate' : 'rightDate';
+        const pointYearVal = direction === 'left' ? this.leftYear : this.rightYear;
+        if (this.selectionMode === 'month') {
+          this[pointVal] = modifyDate(this[pointVal], pointYearVal, month, 1);
+          this.emit(this[pointVal]);
+        } else if (this.selectionMode === 'months') {
+          this.emit(month, true);
+        } else {
+          this[pointVal] = changeYearMonthAndClampDate(this[pointVal], pointYearVal, month);
+          // TODO: should emit intermediate value ??
+          // this.emit(this.date);
+          this.currentView = 'date';
+        }
+      },
+
       handleShortcutClick(shortcut) {
         if (shortcut.onClick) {
           shortcut.onClick(this);
@@ -600,7 +734,7 @@
 
       // leftPrev*, rightNext* need to take care of `unlinkPanels`
       leftPrevYear() {
-        this.leftDate = prevYear(this.leftDate);
+        this.leftDate = prevYear(this.leftDate, this.yearStep);
         if (!this.unlinkPanels) {
           this.rightDate = nextMonth(this.leftDate);
         }
@@ -615,10 +749,10 @@
 
       rightNextYear() {
         if (!this.unlinkPanels) {
-          this.leftDate = nextYear(this.leftDate);
+          this.leftDate = nextYear(this.leftDate, this.yearStep);
           this.rightDate = nextMonth(this.leftDate);
         } else {
-          this.rightDate = nextYear(this.rightDate);
+          this.rightDate = nextYear(this.rightDate, this.yearStep);
         }
       },
 
@@ -633,7 +767,7 @@
 
       // leftNext*, rightPrev* are called when `unlinkPanels` is true
       leftNextYear() {
-        this.leftDate = nextYear(this.leftDate);
+        this.leftDate = nextYear(this.leftDate, this.yearStep);
       },
 
       leftNextMonth() {
@@ -641,7 +775,7 @@
       },
 
       rightPrevYear() {
-        this.rightDate = prevYear(this.rightDate);
+        this.rightDate = prevYear(this.rightDate, this.yearStep);
       },
 
       rightPrevMonth() {
@@ -672,9 +806,18 @@
         if (this.minDate && this.maxDate == null) this.rangeState.selecting = false;
         this.minDate = this.value && isDate(this.value[0]) ? new Date(this.value[0]) : null;
         this.maxDate = this.value && isDate(this.value[0]) ? new Date(this.value[1]) : null;
+        this.currentView = 'date';
+      },
+
+      showMonthPicker(direction) {
+        this.currentView = direction + '-month';
+      },
+
+      showYearPicker(direction) {
+        this.currentView = direction + '-year';
       }
     },
 
-    components: { TimePicker, DateTable, ElInput, ElButton }
+    components: { TimePicker, YearTable, MonthTable, DateTable, ElInput, ElButton }
   };
 </script>

+ 511 - 499
packages/date-picker/src/panel/date.vue

@@ -1,9 +1,9 @@
 <template>
   <transition name="el-zoom-in-top" @after-enter="handleEnter" @after-leave="handleLeave">
     <div
-      v-show="visible"
-      class="el-picker-panel el-date-picker el-popper"
-      :class="[{
+        v-show="visible"
+        class="el-picker-panel el-date-picker el-popper"
+        :class="[{
         'has-sidebar': $slots.sidebar || shortcuts,
         'has-time': showTime
       }, popperClass]">
@@ -11,130 +11,132 @@
         <slot name="sidebar" class="el-picker-panel__sidebar"></slot>
         <div class="el-picker-panel__sidebar" v-if="shortcuts">
           <button
-            type="button"
-            class="el-picker-panel__shortcut"
-            v-for="(shortcut, key) in shortcuts"
-            :key="key"
-            @click="handleShortcutClick(shortcut)">{{ shortcut.text }}</button>
+              type="button"
+              class="el-picker-panel__shortcut"
+              v-for="(shortcut, key) in shortcuts"
+              :key="key"
+              @click="handleShortcutClick(shortcut)">{{ shortcut.text }}</button>
         </div>
         <div class="el-picker-panel__body">
           <div class="el-date-picker__time-header" v-if="showTime">
             <span class="el-date-picker__editor-wrap">
               <el-input
-                :placeholder="t('el.datepicker.selectDate')"
-                :value="visibleDate"
-                size="small"
-                @input="val => userInputDate = val"
-                @change="handleVisibleDateChange" />
+                  :placeholder="t('el.datepicker.selectDate')"
+                  :value="visibleDate"
+                  size="small"
+                  @input="val => userInputDate = val"
+                  @change="handleVisibleDateChange" />
             </span>
             <span class="el-date-picker__editor-wrap" v-clickoutside="handleTimePickClose">
               <el-input
-                ref="input"
-                @focus="timePickerVisible = true"
-                :placeholder="t('el.datepicker.selectTime')"
-                :value="visibleTime"
-                size="small"
-                @input="val => userInputTime = val"
-                @change="handleVisibleTimeChange" />
+                  ref="input"
+                  @focus="timePickerVisible = true"
+                  :placeholder="t('el.datepicker.selectTime')"
+                  :value="visibleTime"
+                  size="small"
+                  @input="val => userInputTime = val"
+                  @change="handleVisibleTimeChange" />
               <time-picker
-                ref="timepicker"
-                :time-arrow-control="arrowControl"
-                @pick="handleTimePick"
-                :visible="timePickerVisible"
-                @mounted="proxyTimePickerDataProperties">
+                  ref="timepicker"
+                  :time-arrow-control="arrowControl"
+                  @pick="handleTimePick"
+                  :visible="timePickerVisible"
+                  @mounted="proxyTimePickerDataProperties">
               </time-picker>
             </span>
           </div>
           <div
-            class="el-date-picker__header"
-            :class="{ 'el-date-picker__header--bordered': currentView === 'year' || currentView === 'month' }"
-            v-show="currentView !== 'time'">
+              class="el-date-picker__header"
+              :class="{ 'el-date-picker__header--bordered': currentView === 'year' || currentView === 'month' }"
+              v-show="currentView !== 'time'">
             <button
-              type="button"
-              @click="prevYear"
-              :aria-label="t(`el.datepicker.prevYear`)"
-              class="el-picker-panel__icon-btn el-date-picker__prev-btn el-icon-d-arrow-left">
+                type="button"
+                @click="prevYear"
+                :aria-label="t(`el.datepicker.prevYear`)"
+                class="el-picker-panel__icon-btn el-date-picker__prev-btn el-icon-d-arrow-left">
             </button>
             <button
-              type="button"
-              @click="prevMonth"
-              v-show="currentView === 'date'"
-              :aria-label="t(`el.datepicker.prevMonth`)"
-              class="el-picker-panel__icon-btn el-date-picker__prev-btn el-icon-arrow-left">
+                type="button"
+                @click="prevMonth"
+                v-show="currentView === 'date'"
+                :aria-label="t(`el.datepicker.prevMonth`)"
+                class="el-picker-panel__icon-btn el-date-picker__prev-btn el-icon-arrow-left">
             </button>
             <span
-              @click="showYearPicker"
-              role="button"
-              class="el-date-picker__header-label">{{ yearLabel }}</span>
+                @click="showYearPicker"
+                role="button"
+                class="el-date-picker__header-label">{{ yearLabel }}</span>
             <span
-              @click="showMonthPicker"
-              v-show="currentView === 'date'"
-              role="button"
-              class="el-date-picker__header-label"
-              :class="{ active: currentView === 'month' }">{{t(`el.datepicker.month${ month + 1 }`)}}</span>
+                @click="showMonthPicker"
+                v-show="currentView === 'date'"
+                role="button"
+                class="el-date-picker__header-label"
+                :class="{ active: currentView === 'month' }">{{t(`el.datepicker.month${ month + 1 }`)}}</span>
             <button
-              type="button"
-              @click="nextYear"
-              :aria-label="t(`el.datepicker.nextYear`)"
-              class="el-picker-panel__icon-btn el-date-picker__next-btn el-icon-d-arrow-right">
+                type="button"
+                @click="nextYear"
+                :aria-label="t(`el.datepicker.nextYear`)"
+                class="el-picker-panel__icon-btn el-date-picker__next-btn el-icon-d-arrow-right">
             </button>
             <button
-              type="button"
-              @click="nextMonth"
-              v-show="currentView === 'date'"
-              :aria-label="t(`el.datepicker.nextMonth`)"
-              class="el-picker-panel__icon-btn el-date-picker__next-btn el-icon-arrow-right">
+                type="button"
+                @click="nextMonth"
+                v-show="currentView === 'date'"
+                :aria-label="t(`el.datepicker.nextMonth`)"
+                class="el-picker-panel__icon-btn el-date-picker__next-btn el-icon-arrow-right">
             </button>
           </div>
 
           <div class="el-picker-panel__content">
             <date-table
-              v-show="currentView === 'date'"
-              @pick="handleDatePick"
-              :selection-mode="selectionMode"
-              :first-day-of-week="firstDayOfWeek"
-              :value="value"
-              :default-value="defaultValue ? new Date(defaultValue) : null"
-              :date="date"
-              :cell-class-name="cellClassName"
-              :disabled-date="disabledDate">
+                v-show="currentView === 'date'"
+                @pick="handleDatePick"
+                :selection-mode="selectionMode"
+                :first-day-of-week="firstDayOfWeek"
+                :value="value"
+                :default-value="defaultValue ? new Date(defaultValue) : null"
+                :date="date"
+                :cell-class-name="cellClassName"
+                :disabled-date="disabledDate">
             </date-table>
             <year-table
-              v-show="currentView === 'year'"
-              @pick="handleYearPick"
-              :value="value"
-              :default-value="defaultValue ? new Date(defaultValue) : null"
-              :date="date"
-              :disabled-date="disabledDate">
+                v-show="currentView === 'year'"
+                @pick="handleYearPick"
+                :selection-mode="selectionMode"
+                :value="value"
+                :default-value="defaultValue ? new Date(defaultValue) : null"
+                :date="date"
+                :disabled-date="disabledDate">
             </year-table>
             <month-table
-              v-show="currentView === 'month'"
-              @pick="handleMonthPick"
-              :value="value"
-              :default-value="defaultValue ? new Date(defaultValue) : null"
-              :date="date"
-              :disabled-date="disabledDate">
+                v-show="currentView === 'month'"
+                @pick="handleMonthPick"
+                :selection-mode="selectionMode"
+                :value="value"
+                :default-value="defaultValue ? new Date(defaultValue) : null"
+                :date="date"
+                :disabled-date="disabledDate">
             </month-table>
           </div>
         </div>
       </div>
 
       <div
-        class="el-picker-panel__footer"
-        v-show="footerVisible && currentView === 'date'">
+          class="el-picker-panel__footer"
+          v-show="footerVisible && (currentView === 'date' || currentView === 'month' || currentView === 'year')">
         <el-button
-          size="mini"
-          type="text"
-          class="el-picker-panel__link-btn"
-          @click="changeToNow"
-          v-show="selectionMode !== 'dates'">
+            size="mini"
+            type="text"
+            class="el-picker-panel__link-btn"
+            @click="changeToNow"
+            v-show="selectionMode !== 'dates' && selectionMode !== 'months' && selectionMode !== 'years'">
           {{ t('el.datepicker.now') }}
         </el-button>
         <el-button
-          plain
-          size="mini"
-          class="el-picker-panel__link-btn"
-          @click="confirm">
+            plain
+            size="mini"
+            class="el-picker-panel__link-btn"
+            @click="confirm">
           {{ t('el.datepicker.confirm') }}
         </el-button>
       </div>
@@ -143,455 +145,465 @@
 </template>
 
 <script type="text/babel">
-  import {
-    formatDate,
-    parseDate,
-    getWeekNumber,
-    isDate,
-    modifyDate,
-    modifyTime,
-    modifyWithTimeString,
-    clearMilliseconds,
-    clearTime,
-    prevYear,
-    nextYear,
-    prevMonth,
-    nextMonth,
-    changeYearMonthAndClampDate,
-    extractDateFormat,
-    extractTimeFormat,
-    timeWithinRange
-  } from 'element-ui/src/utils/date-util';
-  import Clickoutside from 'element-ui/src/utils/clickoutside';
-  import Locale from 'element-ui/src/mixins/locale';
-  import ElInput from 'element-ui/packages/input';
-  import ElButton from 'element-ui/packages/button';
-  import TimePicker from './time';
-  import YearTable from '../basic/year-table';
-  import MonthTable from '../basic/month-table';
-  import DateTable from '../basic/date-table';
-
-  export default {
-    mixins: [Locale],
-
-    directives: { Clickoutside },
-
-    watch: {
-      showTime(val) {
-        /* istanbul ignore if */
-        if (!val) return;
-        this.$nextTick(_ => {
-          const inputElm = this.$refs.input.$el;
-          if (inputElm) {
-            this.pickerWidth = inputElm.getBoundingClientRect().width + 10;
-          }
-        });
-      },
-
-      value(val) {
-        if (this.selectionMode === 'dates' && this.value) return;
-        if (isDate(val)) {
-          this.date = new Date(val);
-        } else {
-          this.date = this.getDefaultValue();
+import {
+  formatDate,
+  parseDate,
+  getWeekNumber,
+  isDate,
+  modifyDate,
+  modifyTime,
+  modifyWithTimeString,
+  clearMilliseconds,
+  clearTime,
+  prevYear,
+  nextYear,
+  prevMonth,
+  nextMonth,
+  changeYearMonthAndClampDate,
+  extractDateFormat,
+  extractTimeFormat,
+  timeWithinRange
+} from 'element-ui/src/utils/date-util';
+import Clickoutside from 'element-ui/src/utils/clickoutside';
+import Locale from 'element-ui/src/mixins/locale';
+import ElInput from 'element-ui/packages/input';
+import ElButton from 'element-ui/packages/button';
+import TimePicker from './time';
+import YearTable from '../basic/year-table';
+import MonthTable from '../basic/month-table';
+import DateTable from '../basic/date-table';
+
+export default {
+  mixins: [Locale],
+
+  directives: { Clickoutside },
+
+  watch: {
+    showTime(val) {
+      /* istanbul ignore if */
+      if (!val) return;
+      this.$nextTick(_ => {
+        const inputElm = this.$refs.input.$el;
+        if (inputElm) {
+          this.pickerWidth = inputElm.getBoundingClientRect().width + 10;
         }
-      },
+      });
+    },
 
-      defaultValue(val) {
-        if (!isDate(this.value)) {
-          this.date = val ? new Date(val) : new Date();
-        }
-      },
-
-      timePickerVisible(val) {
-        if (val) this.$nextTick(() => this.$refs.timepicker.adjustSpinners());
-      },
-
-      selectionMode(newVal) {
-        if (newVal === 'month') {
-          /* istanbul ignore next */
-          if (this.currentView !== 'year' || this.currentView !== 'month') {
-            this.currentView = 'month';
-          }
-        } else if (newVal === 'dates') {
-          this.currentView = 'date';
+    value(val) {
+      if (this.selectionMode === 'dates' && this.value) return;
+      if (this.selectionMode === 'months' && this.value) return;
+      if (this.selectionMode === 'years' && this.value) return;
+      if (isDate(val)) {
+        this.date = new Date(val);
+      } else {
+        this.date = this.getDefaultValue();
+      }
+    },
+
+    defaultValue(val) {
+      if (!isDate(this.value)) {
+        this.date = val ? new Date(val) : new Date();
+      }
+    },
+
+    timePickerVisible(val) {
+      if (val) this.$nextTick(() => this.$refs.timepicker.adjustSpinners());
+    },
+
+    selectionMode(newVal) {
+      if (newVal === 'month') {
+        /* istanbul ignore next */
+        if (this.currentView !== 'year' || this.currentView !== 'month') {
+          this.currentView = 'month';
         }
+      } else if (newVal === 'dates') {
+        this.currentView = 'date';
+      } else if (newVal === 'years') {
+        this.currentView = 'year';
+      } else if (newVal === 'months') {
+        this.currentView = 'month';
       }
+    }
+  },
+
+  methods: {
+    proxyTimePickerDataProperties() {
+      const format = timeFormat => {this.$refs.timepicker.format = timeFormat;};
+      const value = value => {this.$refs.timepicker.value = value;};
+      const date = date => {this.$refs.timepicker.date = date;};
+      const selectableRange = selectableRange => {this.$refs.timepicker.selectableRange = selectableRange;};
+
+      this.$watch('value', value);
+      this.$watch('date', date);
+      this.$watch('selectableRange', selectableRange);
+
+      format(this.timeFormat);
+      value(this.value);
+      date(this.date);
+      selectableRange(this.selectableRange);
+    },
+
+    handleClear() {
+      this.date = this.getDefaultValue();
+      this.$emit('pick', null);
     },
 
-    methods: {
-      proxyTimePickerDataProperties() {
-        const format = timeFormat => {this.$refs.timepicker.format = timeFormat;};
-        const value = value => {this.$refs.timepicker.value = value;};
-        const date = date => {this.$refs.timepicker.date = date;};
-        const selectableRange = selectableRange => {this.$refs.timepicker.selectableRange = selectableRange;};
+    emit(value, ...args) {
+      if (!value) {
+        this.$emit('pick', value, ...args);
+      } else if (Array.isArray(value)) {
+        const dates = value.map(date => this.showTime ? clearMilliseconds(date) : clearTime(date));
+        this.$emit('pick', dates, ...args);
+      } else {
+        this.$emit('pick', this.showTime ? clearMilliseconds(value) : clearTime(value), ...args);
+      }
+      this.userInputDate = null;
+      this.userInputTime = null;
+    },
 
-        this.$watch('value', value);
-        this.$watch('date', date);
-        this.$watch('selectableRange', selectableRange);
+    // resetDate() {
+    //   this.date = new Date(this.date);
+    // },
 
-        format(this.timeFormat);
-        value(this.value);
-        date(this.date);
-        selectableRange(this.selectableRange);
-      },
+    showMonthPicker() {
+      this.currentView = 'month';
+    },
 
-      handleClear() {
-        this.date = this.getDefaultValue();
-        this.$emit('pick', null);
-      },
-
-      emit(value, ...args) {
-        if (!value) {
-          this.$emit('pick', value, ...args);
-        } else if (Array.isArray(value)) {
-          const dates = value.map(date => this.showTime ? clearMilliseconds(date) : clearTime(date));
-          this.$emit('pick', dates, ...args);
-        } else {
-          this.$emit('pick', this.showTime ? clearMilliseconds(value) : clearTime(value), ...args);
-        }
-        this.userInputDate = null;
-        this.userInputTime = null;
-      },
+    showYearPicker() {
+      this.currentView = 'year';
+    },
+
+    // XXX: 没用到
+    // handleLabelClick() {
+    //   if (this.currentView === 'date') {
+    //     this.showMonthPicker();
+    //   } else if (this.currentView === 'month') {
+    //     this.showYearPicker();
+    //   }
+    // },
+
+    prevMonth() {
+      this.date = prevMonth(this.date);
+    },
+
+    nextMonth() {
+      this.date = nextMonth(this.date);
+    },
 
-      // resetDate() {
-      //   this.date = new Date(this.date);
-      // },
+    prevYear() {
+      if (this.currentView === 'year') {
+        this.date = prevYear(this.date, 10);
+      } else {
+        this.date = prevYear(this.date);
+      }
+    },
 
-      showMonthPicker() {
+    nextYear() {
+      if (this.currentView === 'year') {
+        this.date = nextYear(this.date, 10);
+      } else {
+        this.date = nextYear(this.date);
+      }
+    },
+
+    handleShortcutClick(shortcut) {
+      if (shortcut.onClick) {
+        shortcut.onClick(this);
+      }
+    },
+
+    handleTimePick(value, visible, first) {
+      if (isDate(value)) {
+        const newDate = this.value
+          ? modifyTime(this.value, value.getHours(), value.getMinutes(), value.getSeconds())
+          : modifyWithTimeString(this.getDefaultValue(), this.defaultTime);
+        this.date = newDate;
+        this.emit(this.date, true);
+      } else {
+        this.emit(value, true);
+      }
+      if (!first) {
+        this.timePickerVisible = visible;
+      }
+    },
+
+    handleTimePickClose() {
+      this.timePickerVisible = false;
+    },
+
+    handleMonthPick(month) {
+      if (this.selectionMode === 'month') {
+        this.date = modifyDate(this.date, this.year, month, 1);
+        this.emit(this.date);
+      } else if (this.selectionMode === 'months') {
+        this.emit(month, true);
+      } else {
+        this.date = changeYearMonthAndClampDate(this.date, this.year, month);
+        // TODO: should emit intermediate value ??
+        // this.emit(this.date);
+        this.currentView = 'date';
+      }
+    },
+
+    handleDatePick(value) {
+      if (this.selectionMode === 'day') {
+        let newDate = this.value
+          ? modifyDate(this.value, value.getFullYear(), value.getMonth(), value.getDate())
+          : modifyWithTimeString(value, this.defaultTime);
+        // change default time while out of selectableRange
+        if (!this.checkDateWithinRange(newDate)) {
+          newDate = modifyDate(this.selectableRange[0][0], value.getFullYear(), value.getMonth(), value.getDate());
+        }
+        this.date = newDate;
+        this.emit(this.date, this.showTime);
+      } else if (this.selectionMode === 'week') {
+        this.emit(value.date);
+      } else if (this.selectionMode === 'dates') {
+        this.emit(value, true); // set false to keep panel open
+      }
+    },
+
+    handleYearPick(year) {
+      if (this.selectionMode === 'year') {
+        this.date = modifyDate(this.date, year, 0, 1);
+        this.emit(this.date);
+      } else if (this.selectionMode === 'years') {
+        this.emit(year, true);
+      } else {
+        this.date = changeYearMonthAndClampDate(this.date, year, this.month);
+        // TODO: should emit intermediate value ??
+        // this.emit(this.date, true);
         this.currentView = 'month';
-      },
+      }
+    },
+
+    changeToNow() {
+      // NOTE: not a permanent solution
+      //       consider disable "now" button in the future
+      if ((!this.disabledDate || !this.disabledDate(new Date())) && this.checkDateWithinRange(new Date())) {
+        this.date = new Date();
+        this.emit(this.date);
+      }
+    },
+
+    confirm() {
+      if (this.selectionMode === 'dates' || this.selectionMode === 'months' || this.selectionMode === 'years') {
+        this.emit(this.value);
+      } else {
+        // value were emitted in handle{Date,Time}Pick, nothing to update here
+        // deal with the scenario where: user opens the picker, then confirm without doing anything
+        const value = this.value
+          ? this.value
+          : modifyWithTimeString(this.getDefaultValue(), this.defaultTime);
+        this.date = new Date(value); // refresh date
+        this.emit(value);
+      }
+    },
 
-      showYearPicker() {
+    resetView() {
+      if (this.selectionMode === 'month' || this.selectionMode === 'months') {
+        this.currentView = 'month';
+      } else if (this.selectionMode === 'year' || this.selectionMode === 'years') {
         this.currentView = 'year';
-      },
-
-      // XXX: 没用到
-      // handleLabelClick() {
-      //   if (this.currentView === 'date') {
-      //     this.showMonthPicker();
-      //   } else if (this.currentView === 'month') {
-      //     this.showYearPicker();
-      //   }
-      // },
-
-      prevMonth() {
-        this.date = prevMonth(this.date);
-      },
-
-      nextMonth() {
-        this.date = nextMonth(this.date);
-      },
-
-      prevYear() {
-        if (this.currentView === 'year') {
-          this.date = prevYear(this.date, 10);
-        } else {
-          this.date = prevYear(this.date);
-        }
-      },
+      } else {
+        this.currentView = 'date';
+      }
+    },
 
-      nextYear() {
-        if (this.currentView === 'year') {
-          this.date = nextYear(this.date, 10);
-        } else {
-          this.date = nextYear(this.date);
-        }
-      },
+    handleEnter() {
+      document.body.addEventListener('keydown', this.handleKeydown);
+    },
 
-      handleShortcutClick(shortcut) {
-        if (shortcut.onClick) {
-          shortcut.onClick(this);
-        }
-      },
-
-      handleTimePick(value, visible, first) {
-        if (isDate(value)) {
-          const newDate = this.value
-            ? modifyTime(this.value, value.getHours(), value.getMinutes(), value.getSeconds())
-            : modifyWithTimeString(this.getDefaultValue(), this.defaultTime);
-          this.date = newDate;
-          this.emit(this.date, true);
-        } else {
-          this.emit(value, true);
-        }
-        if (!first) {
-          this.timePickerVisible = visible;
-        }
-      },
+    handleLeave() {
+      this.$emit('dodestroy');
+      document.body.removeEventListener('keydown', this.handleKeydown);
+    },
 
-      handleTimePickClose() {
-        this.timePickerVisible = false;
-      },
-
-      handleMonthPick(month) {
-        if (this.selectionMode === 'month') {
-          this.date = modifyDate(this.date, this.year, month, 1);
-          this.emit(this.date);
-        } else {
-          this.date = changeYearMonthAndClampDate(this.date, this.year, month);
-          // TODO: should emit intermediate value ??
-          // this.emit(this.date);
-          this.currentView = 'date';
-        }
-      },
-
-      handleDatePick(value) {
-        if (this.selectionMode === 'day') {
-          let newDate = this.value
-            ? modifyDate(this.value, value.getFullYear(), value.getMonth(), value.getDate())
-            : modifyWithTimeString(value, this.defaultTime);
-          // change default time while out of selectableRange
-          if (!this.checkDateWithinRange(newDate)) {
-            newDate = modifyDate(this.selectableRange[0][0], value.getFullYear(), value.getMonth(), value.getDate());
-          }
-          this.date = newDate;
-          this.emit(this.date, this.showTime);
-        } else if (this.selectionMode === 'week') {
-          this.emit(value.date);
-        } else if (this.selectionMode === 'dates') {
-          this.emit(value, true); // set false to keep panel open
-        }
-      },
-
-      handleYearPick(year) {
-        if (this.selectionMode === 'year') {
-          this.date = modifyDate(this.date, year, 0, 1);
-          this.emit(this.date);
-        } else {
-          this.date = changeYearMonthAndClampDate(this.date, year, this.month);
-          // TODO: should emit intermediate value ??
-          // this.emit(this.date, true);
-          this.currentView = 'month';
+    handleKeydown(event) {
+      const keyCode = event.keyCode;
+      const list = [38, 40, 37, 39];
+      if (this.visible && !this.timePickerVisible) {
+        if (list.indexOf(keyCode) !== -1) {
+          this.handleKeyControl(keyCode);
+          event.stopPropagation();
+          event.preventDefault();
         }
-      },
-
-      changeToNow() {
-        // NOTE: not a permanent solution
-        //       consider disable "now" button in the future
-        if ((!this.disabledDate || !this.disabledDate(new Date())) && this.checkDateWithinRange(new Date())) {
-          this.date = new Date();
-          this.emit(this.date);
+        if (keyCode === 13 && this.userInputDate === null && this.userInputTime === null) { // Enter
+          this.emit(this.date, false);
         }
-      },
-
-      confirm() {
-        if (this.selectionMode === 'dates') {
-          this.emit(this.value);
-        } else {
-          // value were emitted in handle{Date,Time}Pick, nothing to update here
-          // deal with the scenario where: user opens the picker, then confirm without doing anything
-          const value = this.value
-            ? this.value
-            : modifyWithTimeString(this.getDefaultValue(), this.defaultTime);
-          this.date = new Date(value); // refresh date
-          this.emit(value);
-        }
-      },
+      }
+    },
 
-      resetView() {
-        if (this.selectionMode === 'month') {
-          this.currentView = 'month';
-        } else if (this.selectionMode === 'year') {
-          this.currentView = 'year';
-        } else {
-          this.currentView = 'date';
-        }
-      },
-
-      handleEnter() {
-        document.body.addEventListener('keydown', this.handleKeydown);
-      },
-
-      handleLeave() {
-        this.$emit('dodestroy');
-        document.body.removeEventListener('keydown', this.handleKeydown);
-      },
-
-      handleKeydown(event) {
-        const keyCode = event.keyCode;
-        const list = [38, 40, 37, 39];
-        if (this.visible && !this.timePickerVisible) {
-          if (list.indexOf(keyCode) !== -1) {
-            this.handleKeyControl(keyCode);
-            event.stopPropagation();
-            event.preventDefault();
-          }
-          if (keyCode === 13 && this.userInputDate === null && this.userInputTime === null) { // Enter
-            this.emit(this.date, false);
-          }
+    handleKeyControl(keyCode) {
+      const mapping = {
+        'year': {
+          38: -4, 40: 4, 37: -1, 39: 1, offset: (date, step) => date.setFullYear(date.getFullYear() + step)
+        },
+        'month': {
+          38: -4, 40: 4, 37: -1, 39: 1, offset: (date, step) => date.setMonth(date.getMonth() + step)
+        },
+        'week': {
+          38: -1, 40: 1, 37: -1, 39: 1, offset: (date, step) => date.setDate(date.getDate() + step * 7)
+        },
+        'day': {
+          38: -7, 40: 7, 37: -1, 39: 1, offset: (date, step) => date.setDate(date.getDate() + step)
         }
-      },
-
-      handleKeyControl(keyCode) {
-        const mapping = {
-          'year': {
-            38: -4, 40: 4, 37: -1, 39: 1, offset: (date, step) => date.setFullYear(date.getFullYear() + step)
-          },
-          'month': {
-            38: -4, 40: 4, 37: -1, 39: 1, offset: (date, step) => date.setMonth(date.getMonth() + step)
-          },
-          'week': {
-            38: -1, 40: 1, 37: -1, 39: 1, offset: (date, step) => date.setDate(date.getDate() + step * 7)
-          },
-          'day': {
-            38: -7, 40: 7, 37: -1, 39: 1, offset: (date, step) => date.setDate(date.getDate() + step)
-          }
-        };
-        const mode = this.selectionMode;
-        const year = 3.1536e10;
-        const now = this.date.getTime();
-        const newDate = new Date(this.date.getTime());
-        while (Math.abs(now - newDate.getTime()) <= year) {
-          const map = mapping[mode];
-          map.offset(newDate, map[keyCode]);
-          if (typeof this.disabledDate === 'function' && this.disabledDate(newDate)) {
-            continue;
-          }
-          this.date = newDate;
-          this.$emit('pick', newDate, true);
-          break;
-        }
-      },
-
-      handleVisibleTimeChange(value) {
-        const time = parseDate(value, this.timeFormat);
-        if (time && this.checkDateWithinRange(time)) {
-          this.date = modifyDate(time, this.year, this.month, this.monthDate);
-          this.userInputTime = null;
-          this.$refs.timepicker.value = this.date;
-          this.timePickerVisible = false;
-          this.emit(this.date, true);
+      };
+      const mode = this.selectionMode;
+      const year = 3.1536e10;
+      const now = this.date.getTime();
+      const newDate = new Date(this.date.getTime());
+      while (Math.abs(now - newDate.getTime()) <= year) {
+        const map = mapping[mode];
+        map.offset(newDate, map[keyCode]);
+        if (typeof this.disabledDate === 'function' && this.disabledDate(newDate)) {
+          continue;
         }
-      },
-
-      handleVisibleDateChange(value) {
-        const date = parseDate(value, this.dateFormat);
-        if (date) {
-          if (typeof this.disabledDate === 'function' && this.disabledDate(date)) {
-            return;
-          }
-          this.date = modifyTime(date, this.date.getHours(), this.date.getMinutes(), this.date.getSeconds());
-          this.userInputDate = null;
-          this.resetView();
-          this.emit(this.date, true);
+        this.date = newDate;
+        this.$emit('pick', newDate, true);
+        break;
+      }
+    },
+
+    handleVisibleTimeChange(value) {
+      const time = parseDate(value, this.timeFormat);
+      if (time && this.checkDateWithinRange(time)) {
+        this.date = modifyDate(time, this.year, this.month, this.monthDate);
+        this.userInputTime = null;
+        this.$refs.timepicker.value = this.date;
+        this.timePickerVisible = false;
+        this.emit(this.date, true);
+      }
+    },
+
+    handleVisibleDateChange(value) {
+      const date = parseDate(value, this.dateFormat);
+      if (date) {
+        if (typeof this.disabledDate === 'function' && this.disabledDate(date)) {
+          return;
         }
-      },
-
-      isValidValue(value) {
-        return value && !isNaN(value) && (
-          typeof this.disabledDate === 'function'
-            ? !this.disabledDate(value)
-            : true
-        ) && this.checkDateWithinRange(value);
-      },
-
-      getDefaultValue() {
-        // if default-value is set, return it
-        // otherwise, return now (the moment this method gets called)
-        return this.defaultValue ? new Date(this.defaultValue) : new Date();
-      },
-
-      checkDateWithinRange(date) {
-        return this.selectableRange.length > 0
-          ? timeWithinRange(date, this.selectableRange, this.format || 'HH:mm:ss')
-          : true;
+        this.date = modifyTime(date, this.date.getHours(), this.date.getMinutes(), this.date.getSeconds());
+        this.userInputDate = null;
+        this.resetView();
+        this.emit(this.date, true);
       }
     },
 
-    components: {
-      TimePicker, YearTable, MonthTable, DateTable, ElInput, ElButton
-    },
-
-    data() {
-      return {
-        popperClass: '',
-        date: new Date(),
-        value: '',
-        defaultValue: null, // use getDefaultValue() for time computation
-        defaultTime: null,
-        showTime: false,
-        selectionMode: 'day',
-        shortcuts: '',
-        visible: false,
-        currentView: 'date',
-        disabledDate: '',
-        cellClassName: '',
-        selectableRange: [],
-        firstDayOfWeek: 7,
-        showWeekNumber: false,
-        timePickerVisible: false,
-        format: '',
-        arrowControl: false,
-        userInputDate: null,
-        userInputTime: null
-      };
+    isValidValue(value) {
+      return value && !isNaN(value) && (
+        typeof this.disabledDate === 'function'
+          ? !this.disabledDate(value)
+          : true
+      ) && this.checkDateWithinRange(value);
     },
 
-    computed: {
-      year() {
-        return this.date.getFullYear();
-      },
+    getDefaultValue() {
+      // if default-value is set, return it
+      // otherwise, return now (the moment this method gets called)
+      return this.defaultValue ? new Date(this.defaultValue) : new Date();
+    },
 
-      month() {
-        return this.date.getMonth();
-      },
+    checkDateWithinRange(date) {
+      return this.selectableRange.length > 0
+        ? timeWithinRange(date, this.selectableRange, this.format || 'HH:mm:ss')
+        : true;
+    }
+  },
+
+  components: {
+    TimePicker, YearTable, MonthTable, DateTable, ElInput, ElButton
+  },
+
+  data() {
+    return {
+      popperClass: '',
+      date: new Date(),
+      value: '',
+      defaultValue: null, // use getDefaultValue() for time computation
+      defaultTime: null,
+      showTime: false,
+      selectionMode: 'day',
+      shortcuts: '',
+      visible: false,
+      currentView: 'date',
+      disabledDate: '',
+      cellClassName: '',
+      selectableRange: [],
+      firstDayOfWeek: 7,
+      showWeekNumber: false,
+      timePickerVisible: false,
+      format: '',
+      arrowControl: false,
+      userInputDate: null,
+      userInputTime: null
+    };
+  },
+
+  computed: {
+    year() {
+      return this.date.getFullYear();
+    },
 
-      week() {
-        return getWeekNumber(this.date);
-      },
+    month() {
+      return this.date.getMonth();
+    },
 
-      monthDate() {
-        return this.date.getDate();
-      },
+    week() {
+      return getWeekNumber(this.date);
+    },
 
-      footerVisible() {
-        return this.showTime || this.selectionMode === 'dates';
-      },
+    monthDate() {
+      return this.date.getDate();
+    },
 
-      visibleTime() {
-        if (this.userInputTime !== null) {
-          return this.userInputTime;
-        } else {
-          return formatDate(this.value || this.defaultValue, this.timeFormat);
-        }
-      },
+    footerVisible() {
+      return this.showTime || this.selectionMode === 'dates' || this.selectionMode === 'months' || this.selectionMode === 'years';
+    },
 
-      visibleDate() {
-        if (this.userInputDate !== null) {
-          return this.userInputDate;
-        } else {
-          return formatDate(this.value || this.defaultValue, this.dateFormat);
-        }
-      },
-
-      yearLabel() {
-        const yearTranslation = this.t('el.datepicker.year');
-        if (this.currentView === 'year') {
-          const startYear = Math.floor(this.year / 10) * 10;
-          if (yearTranslation) {
-            return startYear + ' ' + yearTranslation + ' - ' + (startYear + 9) + ' ' + yearTranslation;
-          }
-          return startYear + ' - ' + (startYear + 9);
-        }
-        return this.year + ' ' + yearTranslation;
-      },
-
-      timeFormat() {
-        if (this.format) {
-          return extractTimeFormat(this.format);
-        } else {
-          return 'HH:mm:ss';
-        }
-      },
+    visibleTime() {
+      if (this.userInputTime !== null) {
+        return this.userInputTime;
+      } else {
+        return formatDate(this.value || this.defaultValue, this.timeFormat);
+      }
+    },
+
+    visibleDate() {
+      if (this.userInputDate !== null) {
+        return this.userInputDate;
+      } else {
+        return formatDate(this.value || this.defaultValue, this.dateFormat);
+      }
+    },
 
-      dateFormat() {
-        if (this.format) {
-          return extractDateFormat(this.format);
-        } else {
-          return 'yyyy-MM-dd';
+    yearLabel() {
+      const yearTranslation = this.t('el.datepicker.year');
+      if (this.currentView === 'year') {
+        const startYear = Math.floor(this.year / 10) * 10;
+        if (yearTranslation) {
+          return startYear + ' ' + yearTranslation + ' - ' + (startYear + 9) + ' ' + yearTranslation;
         }
+        return startYear + ' - ' + (startYear + 9);
+      }
+      return this.year + ' ' + yearTranslation;
+    },
+
+    timeFormat() {
+      if (this.format) {
+        return extractTimeFormat(this.format);
+      } else {
+        return 'HH:mm:ss';
+      }
+    },
+
+    dateFormat() {
+      if (this.format) {
+        return extractDateFormat(this.format);
+      } else {
+        return 'yyyy-MM-dd';
       }
     }
-  };
+  }
+};
 </script>

+ 36 - 6
packages/date-picker/src/picker.vue

@@ -2,7 +2,7 @@
   <el-input
     class="el-date-editor"
     :class="'el-date-editor--' + type"
-    :readonly="!editable || readonly || type === 'dates' || type === 'week'"
+    :readonly="!editable || readonly || type === 'dates' || type === 'week' || type === 'years' || type === 'months'"
     :disabled="pickerDisabled"
     :size="pickerSize"
     :name="name"
@@ -98,7 +98,6 @@ const NewPopper = {
     offset: Popper.props.offset,
     boundariesPadding: Popper.props.boundariesPadding,
     arrowOffset: Popper.props.arrowOffset,
-    placement: Popper.props.placement,
     transformOrigin: Popper.props.transformOrigin
   },
   methods: Popper.methods,
@@ -111,6 +110,7 @@ const NewPopper = {
 const DEFAULT_FORMATS = {
   date: 'yyyy-MM-dd',
   month: 'yyyy-MM',
+  months: 'yyyy-MM',
   datetime: 'yyyy-MM-dd HH:mm:ss',
   time: 'HH:mm:ss',
   week: 'yyyywWW',
@@ -118,7 +118,8 @@ const DEFAULT_FORMATS = {
   daterange: 'yyyy-MM-dd',
   monthrange: 'yyyy-MM',
   datetimerange: 'yyyy-MM-dd HH:mm:ss',
-  year: 'yyyy'
+  year: 'yyyy',
+  years: 'yyyy'
 };
 const HAVE_TRIGGER_TYPES = [
   'date',
@@ -132,7 +133,9 @@ const HAVE_TRIGGER_TYPES = [
   'monthrange',
   'timerange',
   'datetimerange',
-  'dates'
+  'dates',
+  'months',
+  'years'
 ];
 const DATE_FORMATTER = function(value, format) {
   if (format === 'timestamp') return value.getTime();
@@ -256,6 +259,24 @@ const TYPE_VALUE_RESOLVER_MAP = {
       return (typeof value === 'string' ? value.split(', ') : value)
         .map(date => date instanceof Date ? date : DATE_PARSER(date, format));
     }
+  },
+  months: {
+    formatter(value, format) {
+      return value.map(date => DATE_FORMATTER(date, format));
+    },
+    parser(value, format) {
+      return (typeof value === 'string' ? value.split(', ') : value)
+        .map(date => date instanceof Date ? date : DATE_PARSER(date, format));
+    }
+  },
+  years: {
+    formatter(value, format) {
+      return value.map(date => DATE_FORMATTER(date, format));
+    },
+    parser(value, format) {
+      return (typeof value === 'string' ? value.split(', ') : value)
+        .map(date => date instanceof Date ? date : DATE_PARSER(date, format));
+    }
   }
 };
 const PLACEMENT_MAP = {
@@ -387,6 +408,10 @@ export default {
     },
     pickerOptions: {},
     unlinkPanels: Boolean,
+    rangeLabelClick: {
+      type: Boolean,
+      default: true
+    },
     validateEvent: {
       type: Boolean,
       default: true
@@ -491,6 +516,10 @@ export default {
         return 'year';
       } else if (this.type === 'dates') {
         return 'dates';
+      } else if (this.type === 'months') {
+        return 'months';
+      } else if (this.type === 'years') {
+        return 'years';
       }
 
       return 'day';
@@ -513,7 +542,7 @@ export default {
       } else if (this.userInput !== null) {
         return this.userInput;
       } else if (formattedValue) {
-        return this.type === 'dates'
+        return (this.type === 'dates' || this.type === 'years' || this.type === 'months')
           ? formattedValue.join(', ')
           : formattedValue;
       } else {
@@ -715,7 +744,7 @@ export default {
       if (!this.pickerVisible) return;
       this.pickerVisible = false;
 
-      if (this.type === 'dates') {
+      if (this.type === 'dates' || this.type === 'years' || this.type === 'months') {
         // restore to former value
         const oldValue = parseAsFormatAndType(this.valueOnOpen, this.valueFormat, this.type, this.rangeSeparator) || this.valueOnOpen;
         this.emitInput(oldValue);
@@ -832,6 +861,7 @@ export default {
       this.picker.showTime = this.type === 'datetime' || this.type === 'datetimerange';
       this.picker.selectionMode = this.selectionMode;
       this.picker.unlinkPanels = this.unlinkPanels;
+      this.picker.rangeLabelClick = this.rangeLabelClick;
       this.picker.arrowControl = this.arrowControl || this.timeArrowControl || false;
       this.$watch('format', (format) => {
         this.picker.format = format;

+ 8 - 1
packages/image/src/main.vue

@@ -62,7 +62,8 @@
       zIndex: {
         type: Number,
         default: 2000
-      }
+      },
+      initialIndex: Number
     },
 
     data() {
@@ -95,9 +96,15 @@
       },
       imageIndex() {
         let previewIndex = 0;
+        const initialIndex = this.initialIndex;
+        if (initialIndex >= 0) {
+          previewIndex = initialIndex;
+          return previewIndex;
+        }
         const srcIndex = this.previewSrcList.indexOf(this.src);
         if (srcIndex >= 0) {
           previewIndex = srcIndex;
+          return previewIndex;
         }
         return previewIndex;
       }

+ 1 - 1
packages/loading/src/directive.js

@@ -64,7 +64,7 @@ loadingDirective.install = Vue => {
         el.mask.style[property] = el.maskStyle[property];
       });
 
-      if (el.originalPosition !== 'absolute' && el.originalPosition !== 'fixed') {
+      if (el.originalPosition !== 'absolute' && el.originalPosition !== 'fixed' && el.originalPosition !== 'sticky') {
         addClass(parent, 'el-loading-parent--relative');
       }
       if (binding.modifiers.fullscreen && binding.modifiers.lock) {

+ 1 - 1
packages/loading/src/index.js

@@ -87,7 +87,7 @@ const Loading = (options = {}) => {
   });
 
   addStyle(options, parent, instance);
-  if (instance.originalPosition !== 'absolute' && instance.originalPosition !== 'fixed') {
+  if (instance.originalPosition !== 'absolute' && instance.originalPosition !== 'fixed' && instance.originalPosition !== 'sticky') {
     addClass(parent, 'el-loading-parent--relative');
   }
   if (options.fullscreen && options.lock) {

+ 4 - 3
packages/tree/src/model/node.js

@@ -23,7 +23,7 @@ export const getChildState = node => {
 };
 
 const reInitChecked = function(node) {
-  if (node.childNodes.length === 0) return;
+  if (node.childNodes.length === 0 || node.loading) return;
 
   const {all, none, half} = getChildState(node.childNodes);
   if (all) {
@@ -463,12 +463,13 @@ export default class Node {
       this.loading = true;
 
       const resolve = (children) => {
-        this.loaded = true;
-        this.loading = false;
         this.childNodes = [];
 
         this.doCreateChildren(children, defaultProps);
 
+        this.loaded = true;
+        this.loading = false;
+
         this.updateLeafState();
         if (callback) {
           callback.call(this, children);

+ 5 - 2
src/directives/repeat-click.js

@@ -1,12 +1,15 @@
 import { once, on } from 'element-ui/src/utils/dom';
+import { isMac } from 'element-ui/src/utils/util';
 
 export default {
   bind(el, binding, vnode) {
     let interval = null;
     let startTime;
+    const maxIntervals = isMac() ? 100 : 200;
+
     const handler = () => vnode.context[binding.expression].apply();
     const clear = () => {
-      if (Date.now() - startTime < 100) {
+      if (Date.now() - startTime < maxIntervals) {
         handler();
       }
       clearInterval(interval);
@@ -18,7 +21,7 @@ export default {
       startTime = Date.now();
       once(document, 'mouseup', clear);
       clearInterval(interval);
-      interval = setInterval(handler, 100);
+      interval = setInterval(handler, maxIntervals);
     });
   }
 };

+ 1 - 1
src/index.js

@@ -210,7 +210,7 @@ if (typeof window !== 'undefined' && window.Vue) {
 }
 
 export default {
-  version: '2.15.15-rc',
+  version: '2.15.18-rc',
   locale: locale.use,
   i18n: locale.i18n,
   install,

+ 2 - 12
src/utils/date-util.js

@@ -48,19 +48,9 @@ export const parseDate = function(string, format) {
 };
 
 export const getDayCountOfMonth = function(year, month) {
-  if (month === 3 || month === 5 || month === 8 || month === 10) {
-    return 30;
-  }
-
-  if (month === 1) {
-    if (year % 4 === 0 && year % 100 !== 0 || year % 400 === 0) {
-      return 29;
-    } else {
-      return 28;
-    }
-  }
+  if (isNaN(+month)) return 31;
 
-  return 31;
+  return new Date(year, +month + 1, 0).getDate();
 };
 
 export const getDayCountOfYear = function(year) {

+ 5 - 0
src/utils/util.js

@@ -289,3 +289,8 @@ export function calcShadowRootEvent(e) {
   }
   return event;
 }
+
+export const isMac = function() {
+  return !Vue.prototype.$isServer && /macintosh|mac os x/i.test(navigator.userAgent);
+};
+

+ 6 - 1
yarn.lock

@@ -4111,7 +4111,7 @@ fs.realpath@^1.0.0:
   resolved "http://192.168.3.42:4873/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
   integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
 
-fsevents@^1.0.0, fsevents@^1.2.7, fsevents@^1.2.9, fsevents@~2.3.2:
+fsevents@^1.0.0, fsevents@^1.2.7:
   version "1.2.13"
   resolved "http://192.168.3.42:4873/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38"
   integrity sha1-8yXLBFVZJCi88Rs4M3DvcOO/zDg=
@@ -4119,6 +4119,11 @@ fsevents@^1.0.0, fsevents@^1.2.7, fsevents@^1.2.9, fsevents@~2.3.2:
     bindings "^1.5.0"
     nan "^2.12.1"
 
+fsevents@~2.3.2:
+  version "2.3.3"
+  resolved "http://192.168.3.71:4873/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
+  integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
 function-bind@^1.1.1:
   version "1.1.1"
   resolved "http://192.168.3.42:4873/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"