Răsfoiți Sursa

*-picker: refactor (#7367)

* Revert "Picker only emit user change (#6214)"

This reverts commit 1912c473ef0fed4fe4ac9cb68f73d6995604131a.

* picker/util: add helper methods

range: n => Array
modify{Date, Time}: Date => Date
clear{Time, Milliseconds}: Date => Date
limitTimeRange: Date => Date
timeWithinRange: Date, [Date] => Boolean

* time-spinner: refactory

* time-panel: refactory

* picker refactory

* date-panel, *-table: refactory

* time-select: refactory

* test: update time-picker

* test: update date-picker

* time-range: refactory

* date-range: refactory

* test: update time-select

* test: update form date-picker/time-picker

* docs: update date-picker
Jiewei Qian 7 ani în urmă
părinte
comite
f93798446e

+ 102 - 7
examples/docs/en-US/date-picker.md

@@ -57,7 +57,11 @@
         value4: '',
         value5: '',
         value6: '',
-        value7: ''
+        value7: '',
+        value8: '',
+        value9: '',
+        value10: '',
+        value11: ''
       };
     }
   };
@@ -275,25 +279,116 @@ Picking a date range is supported.
 
 :::
 
+###  Default Value
+
+If user hasn't picked a date, shows today's calendar by default. You can use `default-value` to set another date. Its value should be parsable by `new Date()`.
+
+If type is `daterange`, `default-value` sets the left side calendar.
+
+:::demo
+```html
+<template>
+  <div class="block">
+    <span class="demonstration">date</span>
+    <el-date-picker
+      v-model="value8"
+      type="date"
+      placeholder="Pick a date"
+      default-value="2010-10-01">
+    </el-date-picker>
+  </div>
+  <div class="block">
+    <span class="demonstration">daterange</span>
+    <el-date-picker
+      v-model="value9"
+      type="daterange"
+      start-placeholder="Start Date"
+      end-placeholder="End Date"
+      default-value="2010-10-01">
+    </el-date-picker>
+  </div>
+</template>
+
+<script>
+  export default {
+    data() {
+      return {
+        value8: '',
+        value9: ''
+      };
+    }
+  };
+</script>
+```
+:::
+
+###  Formatted Value
+
+By default, DatePicker emits `Date` object. You can use `value-format` to designate the format of emitted value, it accepts the same format string of `format` attribute.
+
+:::warning
+This feature is at alpha stage. Feedback welcome.
+:::
+
+:::demo
+```html
+<template>
+  <div class="block">
+    <span class="demonstration">Emits Date object</span>
+    <div class="demonstration">Value: {{ value10 }}</div>
+    <el-date-picker
+      v-model="value10"
+      type="date"
+      placeholder="Pick a Date"
+      format="yyyy/MM/dd">
+    </el-date-picker>
+  </div>
+  <div class="block">
+    <span class="demonstration">Emits formatted date</span>
+    <div class="demonstration">Value: {{ value11 }}</div>
+    <el-date-picker
+      v-model="value11"
+      type="date"
+      placeholder="Pick a Date"
+      format="yyyy/MM/dd"
+      value-format="yyyy-MM-dd">
+    </el-date-picker>
+  </div>
+</template>
+
+<script>
+  export default {
+    data() {
+      return {
+        value10: '',
+        value11: '',
+      };
+    }
+  };
+</script>
+```
+:::
+
 ### Attributes
 | Attribute      | Description          | Type      | Accepted Values       | Default  |
 |---------- |-------------- |---------- |--------------------------------  |-------- |
 | readonly | whether DatePicker is read only | boolean | — | false |
 | disabled | whether DatePicker is disabled | boolean | — | false |
-|size | size of Input | string | large/small/mini | — |
+| size | size of Input | string | large/small/mini | — |
 | editable | whether the input is editable | boolean | — | true |
 | clearable | Whether to show clear button | boolean | — | true |
 | placeholder | placeholder in non-range mode | string | — | — |
 | start-placeholder | placeholder for the start date in range mode | string | — | — |
 | end-placeholder | placeholder for the end date in range mode | string | — | — |
 | type | type of the picker | string | year/month/date/datetime/ week/datetimerange/daterange | date |
-| format | format of the picker | string | year `yyyy` month `MM` day `dd`, hour `HH`, minute `mm`, second `ss` | yyyy-MM-dd |
+| format | format of the input box | string | year `yyyy`, month `MM`, day `dd`, hour `HH`, minute `mm`, second `ss` | yyyy-MM-dd |
 | align | alignment | left/center/right | left |
 | popper-class | custom class name for DatePicker's dropdown | string | — | — |
 | picker-options | additional options, check the table below | object | — | {} |
-| range-separator | range separator | string | - | '-' |
-| default-value | optional default time of the picker | Date | anything accepted by `new Date()` | - |
-|name | same as `name` in native input | string | — | — |
+| range-separator | range separator | string | — | '-' |
+| default-value | optional, default date of the calendar | Date | anything accepted by `new Date()` | — |
+| value-format | optional, format of bounded value | string | year `yyyy`, month `MM`, day `dd`, hour `HH`, minute `mm`, second `ss` | — |
+| name | same as `name` in native input | string | — | — |
 
 ### Picker Options
 | Attribute      | Description          | Type      | Accepted Values       | Default  |
@@ -313,7 +408,7 @@ Picking a date range is supported.
 ### Events
 | Event Name | Description | Parameters |
 |---------|--------|---------|
-| change | triggers when input value changes | formatted value |
+| change | triggers when user confirms the value | component's bounded value |
 | blur | triggers when Input blurs | (event: Event) |
 | focus | triggers when Input focuses | (event: Event) |
 

+ 4 - 2
examples/docs/en-US/datetime-picker.md

@@ -251,7 +251,9 @@ DateTimePicker is derived from DatePicker and TimePicker. For a more detailed ex
 | popper-class | custom class name for DateTimePicker's dropdown | string | — | — |
 | picker-options | additional options, check the table below | object | — | {} |
 | range-separator | range separator | string | - | '-' |
-|name | same as `name` in native input | string | — | — |
+| default-value | optional, default date of the calendar | Date | anything accepted by `new Date()` | — |
+| value-format | optional, format of bounded value | string | year `yyyy`, month `MM`, day `dd`, hour `HH`, minute `mm`, second `ss` | — |
+| name | same as `name` in native input | string | — | — |
 
 ### Picker Options
 | Attribute      | Description          | Type      | Accepted Values       | Default  |
@@ -269,7 +271,7 @@ DateTimePicker is derived from DatePicker and TimePicker. For a more detailed ex
 ### Events
 | Event Name | Description | Parameters |
 |---------|--------|---------|
-| change | triggers when input value changes | formatted value |
+| change | triggers when user confirms the value | component's bounded value |
 | blur | triggers when Input blurs | (event: Event) |
 | focus | triggers when Input focuses | (event: Event) |
 

+ 5 - 3
examples/docs/en-US/time-picker.md

@@ -161,12 +161,14 @@ Can pick an arbitrary time range.
 | placeholder | placeholder in non-range mode | string | — | — |
 | start-placeholder | placeholder for the start time in range mode | string | — | — |
 | end-placeholder | placeholder for the end time in range mode | string | — | — |
-| value | value of the picker | date for Time Picker, and string for Time Select | hour `HH`, minute `mm`, second `ss` | HH:mm:ss |
+| value | value of the picker | Date for Time Picker, and string for Time Select | hour `HH`, minute `mm`, second `ss` | HH:mm:ss |
 | align | alignment | left / center / right | left |
 | popper-class | custom class name for TimePicker's dropdown | string | — | — |
 | picker-options | additional options, check the table below | object | — | {} |
 | range-separator | range separator | string | - | '-' |
-|name | same as `name` in native input | string | — | — |
+| default-value | optional, default date of the calendar | Date for TimePicker, string for TimeSelect | anything accepted by `new Date()` for TimePicker, selectable value for TimeSelect | — |
+| value-format | optional, only for TimePicker, format of bounded value | string | hour `HH`, minute `mm`, second `ss` | — |
+| name | same as `name` in native input | string | — | — |
 
 ### Time Select Options
 | Attribute      | Description          | Type      | Accepted Values       | Default  |
@@ -187,6 +189,6 @@ Can pick an arbitrary time range.
 ### Events
 | Event Name | Description | Parameters |
 |---------|--------|---------|
-| change | triggers when input value changes | formatted value |
+| change | triggers when user confirms the value | component's bounded value |
 | blur | triggers when Input blurs | (event: Event) |
 | focus | triggers when Input focuses | (event: Event) |

+ 101 - 6
examples/docs/zh-CN/date-picker.md

@@ -65,7 +65,11 @@
         value4: '',
         value5: '',
         value6: '',
-        value7: ''
+        value7: '',
+        value8: '',
+        value9: '',
+        value10: '',
+        value11: ''
       };
     }
   };
@@ -285,6 +289,96 @@
 ```
 :::
 
+###  默认显示日期
+
+未选择日期时,默认显示今天的日历。使用`default-value`可以指定其他日期,该值需要能够被`new Date()`解析。
+类型为`daterange`时,指定左侧日历的日期。
+
+:::demo
+```html
+<template>
+  <div class="block">
+    <span class="demonstration">date</span>
+    <el-date-picker
+      v-model="value8"
+      type="date"
+      placeholder="选择日期"
+      default-value="2010-10-01">
+    </el-date-picker>
+  </div>
+  <div class="block">
+    <span class="demonstration">daterange</span>
+    <el-date-picker
+      v-model="value9"
+      type="daterange"
+      start-placeholder="开始日期"
+      end-placeholder="结束日期"
+      default-value="2010-10-01">
+    </el-date-picker>
+  </div>
+</template>
+
+<script>
+  export default {
+    data() {
+      return {
+        value8: '',
+        value9: ''
+      };
+    }
+  };
+</script>
+```
+:::
+
+###  返回值格式
+
+默认情况下,组件接受并返回`Date`对象。
+使用`value-format`指定返回值的格式,支持的格式与`format`相同。
+
+:::warning
+该功能处于测试阶段,欢迎提供反馈。
+:::
+
+:::demo
+```html
+<template>
+  <div class="block">
+    <span class="demonstration">默认为 Date 对象</span>
+    <div class="demonstration">组件值:{{ value10 }}</div>
+    <el-date-picker
+      v-model="value10"
+      type="date"
+      placeholder="选择日期"
+      format="yyyy 年 MM 月 dd 日">
+    </el-date-picker>
+  </div>
+  <div class="block">
+    <span class="demonstration">使用 value-format 进行格式化</span>
+    <div class="demonstration">组件值:{{ value11 }}</div>
+    <el-date-picker
+      v-model="value11"
+      type="date"
+      placeholder="选择日期"
+      format="yyyy 年 MM 月 dd 日"
+      value-format="yyyy-MM-dd">
+    </el-date-picker>
+  </div>
+</template>
+
+<script>
+  export default {
+    data() {
+      return {
+        value10: '',
+        value11: '',
+      };
+    }
+  };
+</script>
+```
+:::
+
 ### Attributes
 | 参数      | 说明          | 类型      | 可选值                           | 默认值  |
 |---------- |-------------- |---------- |--------------------------------  |-------- |
@@ -297,12 +391,13 @@
 | start-placeholder | 范围选择时开始日期的占位内容 | string | — | — |
 | end-placeholder | 范围选择时结束日期的占位内容 | string | — | — |
 | type | 显示类型 | string | year/month/date/week/ datetime/datetimerange/daterange | date |
-| format | 时间日期格式 | string | 年 `yyyy`,月 `MM`,日 `dd`,小时 `HH`,分 `mm`,秒 `ss` | yyyy-MM-dd |
+| format | 输入框的时间日期格式 | string | 年 `yyyy`,月 `MM`,日 `dd`,小时 `HH`,分 `mm`,秒 `ss` | yyyy-MM-dd |
 | align | 对齐方式 | string | left, center, right | left |
 | popper-class | DatePicker 下拉框的类名 | string | — | — |
-|picker-options | 当前时间日期选择器特有的选项参考下表 | object |  — | {} |
-| range-separator | 选择范围时的分隔符 | string | - | '-' |
-| default-value | 可选,DatePicker打开时默认显示的时间 | Date | 可被new Date()解析 | - |
+| picker-options | 当前时间日期选择器特有的选项参考下表 | object |  — | {} |
+| range-separator | 选择范围时的分隔符 | string | — | '-' |
+| default-value | 可选,选择器打开时默认显示的时间 | Date | 可被`new Date()`解析 | — |
+| value-format | 可选,绑定值的格式 | string | 年 `yyyy`,月 `MM`,日 `dd`,小时 `HH`,分 `mm`,秒 `ss` | — |
 | name | 原生属性 | string | — | — |
 
 ### Picker Options
@@ -322,7 +417,7 @@
 ### Events
 | 事件名称      | 说明    | 回调参数      |
 |---------|--------|---------|
-| change | 当 input 的值改变时触发,返回值和文本框一致 | 格式化后的值 |
+| change | 用户确认选定的值时触发 | 组件绑定值 |
 | blur | 当 input 失去焦点时触发 | (event: Event) |
 | focus | 当 input 获得焦点时触发 | (event: Event) |
 

+ 3 - 1
examples/docs/zh-CN/datetime-picker.md

@@ -250,6 +250,8 @@ DateTimePicker 由 DatePicker 和 TimePicker 派生,`Picker Options` 或者其
 | popper-class | DateTimePicker 下拉框的类名 | string | — | — |
 | picker-options | 当前时间日期选择器特有的选项参考下表 | object |  — | {} |
 | range-separator | 选择范围时的分隔符 | string | - | '-' |
+| default-value | 可选,选择器打开时默认显示的时间 | Date | 可被`new Date()`解析 | — |
+| value-format | 可选,绑定值的格式 | string | 年 `yyyy`,月 `MM`,日 `dd`,小时 `HH`,分 `mm`,秒 `ss` | — |
 | name | 原生属性 | string | — | — |
 
 ### Picker Options
@@ -268,7 +270,7 @@ DateTimePicker 由 DatePicker 和 TimePicker 派生,`Picker Options` 或者其
 ### Events
 | Event Name | Description | Parameters |
 |---------|--------|---------|
-| change | 当 input 的值改变时触发,返回值和文本框一致 | formatted value |
+| change | 用户确认选定的值时触发 | 组件绑定值 |
 | blur | 当 input 失去焦点时触发 | (event: Event) |
 | focus | 当 input 获得焦点时触发 | (event: Event) |
 

+ 3 - 1
examples/docs/zh-CN/time-picker.md

@@ -166,6 +166,8 @@
 | popper-class | TimePicker 下拉框的类名 | string | — | — |
 | picker-options | 当前时间日期选择器特有的选项参考下表 | object | — | {} |
 | range-separator | 选择范围时的分隔符 | string | - | '-' |
+| value-format | 可选,仅TimePicker时可用,绑定值的格式,同DatePicker | string | 小时 `HH`,分 `mm`,秒 `ss` | — |
+| default-value | 可选,选择器打开时默认显示的时间 | Date(TimePicker) / string(TimeSelect) | 可被`new Date()`解析(TimePicker) / 可选值(TimeSelect) | — |
 | name | 原生属性 | string | — | — |
 
 ### Time Select Options
@@ -186,7 +188,7 @@
 ### Events
 | 事件名 | 说明 | 参数 |
 |---------|--------|---------|
-| change | 当 input 的值改变时触发,返回值和文本框一致 | formatted value |
+| change | 用户确认选定的值时触发 | 组件绑定值 |
 | blur | 当 input 失去焦点时触发 | (event: Event) |
 | focus | 当 input 获得焦点时触发 | (event: Event) |
 

+ 31 - 13
packages/date-picker/src/basic/date-table.vue

@@ -30,7 +30,7 @@
 </template>
 
 <script>
-  import { getFirstDayOfMonth, getDayCountOfMonth, getWeekNumber, getStartDateOfMonth, DAY_DURATION } from '../util';
+  import { getFirstDayOfMonth, getDayCountOfMonth, getWeekNumber, getStartDateOfMonth, DAY_DURATION, isDate } from '../util';
   import { hasClass } from 'element-ui/src/utils/dom';
   import Locale from 'element-ui/src/mixins/locale';
 
@@ -51,13 +51,16 @@
         validator: val => val >= 1 && val <= 7
       },
 
-      date: {},
-
-      year: {},
+      value: {},
 
-      month: {},
+      defaultValue: {
+        validator(val) {
+          // either: null, valid Date object, Array of valid Date objects
+          return val === null || isDate(val) || (Array.isArray(val) && val.every(isDate));
+        }
+      },
 
-      week: {},
+      date: {},
 
       selectionMode: {
         default: 'day'
@@ -98,8 +101,12 @@
         return WEEKS.concat(WEEKS).slice(week, week + 7);
       },
 
-      monthDate() {
-        return this.date.getDate();
+      year() {
+        return this.date.getFullYear();
+      },
+
+      month() {
+        return this.date.getMonth();
       },
 
       startDate() {
@@ -107,6 +114,7 @@
       },
 
       rows() {
+        // TODO: refactory rows / getCellClasses
         const date = new Date(this.year, this.month, 1);
         let day = getFirstDayOfMonth(date); // day of first day
         const dateCountOfMonth = getDayCountOfMonth(date.getFullYear(), date.getMonth());
@@ -220,7 +228,7 @@
           this.$emit('pick', {
             minDate: this.minDate,
             maxDate: this.maxDate
-          }, true, false);
+          });
         }
       }
     },
@@ -232,9 +240,16 @@
     },
 
     methods: {
+      cellMatchesDate(cell, date) {
+        const value = new Date(date);
+        return this.year === value.getFullYear() &&
+          this.month === value.getMonth() &&
+          Number(cell.text) === value.getDate();
+      },
+
       getCellClasses(cell) {
         const selectionMode = this.selectionMode;
-        const monthDate = this.monthDate;
+        const defaultValue = this.defaultValue ? Array.isArray(this.defaultValue) ? this.defaultValue : [this.defaultValue] : [];
 
         let classes = [];
         if ((cell.type === 'normal' || cell.type === 'today') && !cell.disabled) {
@@ -246,8 +261,11 @@
           classes.push(cell.type);
         }
 
-        if (selectionMode === 'day' && (cell.type === 'normal' || cell.type === 'today') &&
-          Number(this.year) === this.date.getFullYear() && this.month === this.date.getMonth() && monthDate === Number(cell.text)) {
+        if (cell.type === 'normal' && defaultValue.some(date => this.cellMatchesDate(cell, date))) {
+          classes.push('default');
+        }
+
+        if (selectionMode === 'day' && (cell.type === 'normal' || cell.type === 'today') && this.cellMatchesDate(cell, this.value)) {
           classes.push('current');
         }
 
@@ -307,7 +325,7 @@
 
         newDate.setDate(parseInt(cell.text, 10));
 
-        return getWeekNumber(newDate) === this.week;
+        return getWeekNumber(newDate) === getWeekNumber(this.date);
       },
 
       markRange(maxDate) {

+ 26 - 28
packages/date-picker/src/basic/month-table.vue

@@ -49,45 +49,43 @@
 
 <script type="text/babel">
   import Locale from 'element-ui/src/mixins/locale';
+  import { isDate, range, getDayCountOfMonth } from '../util';
   import { hasClass } from 'element-ui/src/utils/dom';
 
+  const datesInMonth = (year, month) => {
+    const numOfDays = getDayCountOfMonth(year, month);
+    const firstDay = new Date(year, month, 1);
+    const ONE_DAY = 8.64e7;
+    return range(numOfDays).map(n => new Date(firstDay.getTime() + n * ONE_DAY));
+  };
+
   export default {
     props: {
       disabledDate: {},
-      date: {},
-      month: {
-        type: Number
-      }
+      value: {},
+      defaultValue: {
+        validator(val) {
+          // null or valid Date Object
+          return val === null || (val instanceof Date && isDate(val));
+        }
+      },
+      date: {}
     },
     mixins: [Locale],
     methods: {
       getCellStyle(month) {
         const style = {};
+        const year = this.date.getFullYear();
+        const today = new Date();
 
-        var year = this.date.getFullYear();
-        var date = new Date(0);
-        date.setFullYear(year);
-        date.setMonth(month, 1);
-        date.setHours(0);
-        var nextMonth = new Date(date);
-        nextMonth.setMonth(month + 1);
-
-        var flag = false;
-        if (typeof this.disabledDate === 'function') {
-
-          while (date < nextMonth) {
-            if (this.disabledDate(date)) {
-              date = new Date(date.getTime() + 8.64e7);
-              flag = true;
-            } else {
-              flag = false;
-              break;
-            }
-          }
-        }
-
-        style.disabled = flag;
-        style.current = this.month === month;
+        style.disabled = typeof this.disabledDate === 'function'
+          ? datesInMonth(year, month).every(this.disabledDate)
+          : false;
+        style.current = this.value.getFullYear() === year && this.value.getMonth() === month;
+        style.today = today.getFullYear() === year && today.getMonth() === month;
+        style.default = this.defaultValue &&
+          this.defaultValue.getFullYear() === year &&
+          this.defaultValue.getMonth() === month;
 
         return style;
       },

+ 57 - 90
packages/date-picker/src/basic/time-spinner.vue

@@ -7,9 +7,9 @@
       view-class="el-time-spinner__list"
       noresize
       tag="ul"
-      ref="hour">
+      ref="hours">
       <li
-        @click="handleClick('hours', { value: hour, disabled: disabled }, true)"
+        @click="handleClick('hours', { value: hour, disabled: disabled })"
         v-for="(disabled, hour) in hoursList"
         track-by="hour"
         class="el-time-spinner__item"
@@ -22,9 +22,9 @@
       view-class="el-time-spinner__list"
       noresize
       tag="ul"
-      ref="minute">
+      ref="minutes">
       <li
-        @click="handleClick('minutes', key, true)"
+        @click="handleClick('minutes', { value: key, disabled: false })"
         v-for="(minute, key) in 60"
         class="el-time-spinner__item"
         :class="{ 'active': key === minutes }">{{ ('0' + key).slice(-2) }}</li>
@@ -37,9 +37,9 @@
       view-class="el-time-spinner__list"
       noresize
       tag="ul"
-      ref="second">
+      ref="seconds">
       <li
-        @click="handleClick('seconds', key, true)"
+        @click="handleClick('seconds', { value: key, disabled: false })"
         v-for="(second, key) in 60"
         class="el-time-spinner__item"
         :class="{ 'active': key === seconds }">{{ ('0' + key).slice(-2) }}</li>
@@ -48,83 +48,38 @@
 </template>
 
 <script type="text/babel">
-  import { getRangeHours } from '../util';
+  import { getRangeHours, modifyTime } from '../util';
   import ElScrollbar from 'element-ui/packages/scrollbar';
 
   export default {
     components: { ElScrollbar },
 
     props: {
-      hours: {
-        type: Number,
-        default: 0
-      },
-
-      minutes: {
-        type: Number,
-        default: 0
-      },
-
-      seconds: {
-        type: Number,
-        default: 0
-      },
-
+      date: {},
+      defaultValue: {},  // reserved for future use
       showSeconds: {
         type: Boolean,
         default: true
       }
     },
 
-    watch: {
-      hoursPrivate(newVal, oldVal) {
-        if (!(newVal >= 0 && newVal <= 23)) {
-          this.hoursPrivate = oldVal;
-        }
-        this.adjustElTop('hour', newVal);
-        this.$emit('change', { hours: newVal });
-      },
-
-      minutesPrivate(newVal, oldVal) {
-        if (!(newVal >= 0 && newVal <= 59)) {
-          this.minutesPrivate = oldVal;
-        }
-        this.adjustElTop('minute', newVal);
-        this.$emit('change', { minutes: newVal });
-      },
-
-      secondsPrivate(newVal, oldVal) {
-        if (!(newVal >= 0 && newVal <= 59)) {
-          this.secondsPrivate = oldVal;
-        }
-        this.adjustElTop('second', newVal);
-        this.$emit('change', { seconds: newVal });
-      }
-    },
-
     computed: {
-      hoursList() {
-        return getRangeHours(this.selectableRange);
+      hours() {
+        return this.date.getHours();
       },
-
-      hourEl() {
-        return this.$refs.hour.wrap;
+      minutes() {
+        return this.date.getMinutes();
       },
-
-      minuteEl() {
-        return this.$refs.minute.wrap;
+      seconds() {
+        return this.date.getSeconds();
       },
-
-      secondEl() {
-        return this.$refs.second.wrap;
+      hoursList() {
+        return getRangeHours(this.selectableRange);
       }
     },
 
     data() {
       return {
-        hoursPrivate: 0,
-        minutesPrivate: 0,
-        secondsPrivate: 0,
         selectableRange: [],
         currentScrollbar: null
       };
@@ -137,59 +92,71 @@
     },
 
     methods: {
-      handleClick(type, value, disabled) {
-        if (value.disabled) {
-          return;
+      modifyDateField(type, value) {
+        switch (type) {
+          case 'hours': this.$emit('change', modifyTime(this.date, value, this.minutes, this.seconds)); break;
+          case 'minutes': this.$emit('change', modifyTime(this.date, this.hours, value, this.seconds)); break;
+          case 'seconds': this.$emit('change', modifyTime(this.date, this.hours, this.minutes, value)); break;
         }
+      },
 
-        this[type + 'Private'] = value.value >= 0 ? value.value : value;
-
-        this.emitSelectRange(type);
+      handleClick(type, {value, disabled}) {
+        if (!disabled) {
+          this.modifyDateField(type, value);
+          this.emitSelectRange(type);
+          this.adjustSpinner(type, value);
+        }
       },
 
       emitSelectRange(type) {
         if (type === 'hours') {
           this.$emit('select-range', 0, 2);
-          this.adjustElTop('minute', this.minutes);
-          this.adjustElTop('second', this.seconds);
+          this.adjustSpinner('minutes', this.minutes);
+          this.adjustSpinner('seconds', this.seconds);
         } else if (type === 'minutes') {
           this.$emit('select-range', 3, 5);
-          this.adjustElTop('hour', this.hours);
-          this.adjustElTop('second', this.seconds);
+          this.adjustSpinner('hours', this.hours);
+          this.adjustSpinner('seconds', this.seconds);
         } else if (type === 'seconds') {
           this.$emit('select-range', 6, 8);
-          this.adjustElTop('minute', this.minutes);
-          this.adjustElTop('hour', this.hours);
+          this.adjustSpinner('minutes', this.minutes);
+          this.adjustSpinner('hours', this.hours);
         }
         this.currentScrollbar = type;
       },
 
       bindScrollEvent() {
         const bindFuntion = (type) => {
-          this[`${type}El`].onscroll = (e) => {
+          this.$refs[type].wrap.onscroll = (e) => {
+            // TODO: scroll is emitted when set scrollTop programatically
+            // should find better solutions in the future!
             this.handleScroll(type, e);
           };
         };
-        bindFuntion('hour');
-        bindFuntion('minute');
-        bindFuntion('second');
+        bindFuntion('hours');
+        bindFuntion('minutes');
+        bindFuntion('seconds');
       },
 
       handleScroll(type) {
-        const adjust = {};
-        adjust[`${type}s`] = Math.min(Math.floor((this[`${type}El`].scrollTop - 80) / 32 + 3), (`${type}` === 'hour' ? 23 : 59));
-        this.$emit('change', adjust);
+        const value = Math.min(Math.floor((this.$refs[type].wrap.scrollTop - 80) / 32 + 3), (type === 'hours' ? 23 : 59));
+        this.modifyDateField(type, value);
       },
 
-      adjustScrollTop() {
-        this.adjustElTop('hour', this.hours);
-        this.adjustElTop('minute', this.minutes);
-        this.adjustElTop('second', this.seconds);
+      // NOTE: used by datetime / date-range panel
+      //       renamed from adjustScrollTop
+      //       should try to refactory it
+      adjustSpinners() {
+        this.adjustSpinner('hours', this.hours);
+        this.adjustSpinner('minutes', this.minutes);
+        this.adjustSpinner('seconds', this.seconds);
       },
 
-      adjustElTop(type, value) {
-        if (!this[`${type}El`]) return;
-        this[`${type}El`].scrollTop = Math.max(0, (value - 2.5) * 32 + 80);
+      adjustSpinner(type, value) {
+        const el = this.$refs[type].wrap;
+        if (el) {
+          el.scrollTop = Math.max(0, (value - 2.5) * 32 + 80);
+        }
       },
 
       scrollDown(step) {
@@ -217,8 +184,8 @@
           now = (now + step + 60) % 60;
         }
 
-        this.$emit('change', { [label]: now });
-        this.adjustElTop(label.slice(0, -1), now);
+        this.modifyDateField(label, now);
+        this.adjustSpinner(label, now);
       }
     }
   };

+ 27 - 32
packages/date-picker/src/basic/year-table.vue

@@ -45,62 +45,57 @@
 
 <script type="text/babel">
   import { hasClass } from 'element-ui/src/utils/dom';
+  import { isDate, range } from '../util';
+
+  const isLeapYear = year => year % 400 === 0 || (year % 100 !== 0 && year % 4 === 0);
+
+  const datesInYear = year => {
+    const numOfDays = isLeapYear(year) ? 366 : 365;
+    const firstDay = new Date(year, 0, 1);
+    const ONE_DAY = 8.64e7;
+    return range(numOfDays).map(n => new Date(firstDay.getTime() + ONE_DAY));
+  };
 
   export default {
     props: {
       disabledDate: {},
-      date: {},
-      year: {}
+      value: {},
+      defaultValue: {
+        validator(val) {
+          // null or valid Date Object
+          return val === null || (val instanceof Date && isDate(val));
+        }
+      },
+      date: {}
     },
 
     computed: {
       startYear() {
-        return Math.floor(this.year / 10) * 10;
+        return Math.floor(this.date.getFullYear() / 10) * 10;
       }
     },
 
     methods: {
       getCellStyle(year) {
         const style = {};
+        const today = new Date();
 
-        var date = new Date(year, 0, 1, 0);
-        var nextYear = new Date(date);
-        nextYear.setFullYear(year + 1);
-
-        var flag = false;
-        if (typeof this.disabledDate === 'function') {
-
-          while (date < nextYear) {
-            if (this.disabledDate(date)) {
-              date = new Date(date.getTime() + 8.64e7);
-            } else {
-              break;
-            }
-          }
-          if ((date - nextYear) === 0) flag = true;
-
-        }
-
-        style.disabled = flag;
-        style.current = Number(this.year) === year;
+        style.disabled = typeof this.disabledDate === 'function'
+          ? datesInYear(year).every(this.disabledDate)
+          : false;
+        style.current = this.value.getFullYear() === year;
+        style.today = today.getFullYear() === year;
+        style.default = this.defaultValue && this.defaultValue.getFullYear() === year;
 
         return style;
       },
 
-      nextTenYear() {
-        this.$emit('pick', Number(this.year) + 10, false, true);
-      },
-
-      prevTenYear() {
-        this.$emit('pick', Number(this.year) - 10, false, true);
-      },
-
       handleYearTableClick(event) {
         const target = event.target;
         if (target.tagName === 'A') {
           if (hasClass(target.parentNode, 'disabled')) return;
           const year = target.textContent || target.innerText;
-          this.$emit('pick', Number(year), true, true);
+          this.$emit('pick', Number(year));
         }
       }
     }

+ 155 - 117
packages/date-picker/src/panel/date-range.vue

@@ -39,7 +39,6 @@
                   @change.native="handleTimeChange($event, 'min')" />
                 <time-picker
                   ref="minTimePicker"
-                  :date="minDate"
                   @pick="handleMinTimePick"
                   :visible="minTimePickerVisible"
                   @mounted="$refs.minTimePicker.format=timeFormat">
@@ -70,7 +69,6 @@
                   @change.native="handleTimeChange($event, 'max')" />
                 <time-picker
                   ref="maxTimePicker"
-                  :date="maxDate"
                   @pick="handleMaxTimePick"
                   :visible="maxTimePickerVisible"
                   @mounted="$refs.maxTimePicker.format=timeFormat">
@@ -92,9 +90,8 @@
             </div>
             <date-table
               selection-mode="range"
-              :date="date"
-              :year="leftYear"
-              :month="leftMonth"
+              :date="leftDate"
+              :default-value="defaultValue"
               :min-date="minDate"
               :max-date="maxDate"
               :range-state="rangeState"
@@ -119,8 +116,7 @@
             <date-table
               selection-mode="range"
               :date="rightDate"
-              :year="rightYear"
-              :month="rightMonth"
+              :default-value="defaultValue"
               :min-date="minDate"
               :max-date="maxDate"
               :range-state="rangeState"
@@ -147,17 +143,31 @@
 </template>
 
 <script type="text/babel">
-  import { nextMonth, prevMonth, toDate, formatDate, parseDate } from '../util';
+  import {
+    formatDate,
+    parseDate,
+    isDate,
+    modifyDate,
+    modifyTime,
+    prevMonth,
+    nextMonth
+  } from '../util';
   import Locale from 'element-ui/src/mixins/locale';
   import TimePicker from './time';
   import DateTable from '../basic/date-table';
   import ElInput from 'element-ui/packages/input';
 
-  const calcDefaultValue = defaultValue => {
+  const advanceDate = (date, amount) => {
+    return new Date(new Date(date).getTime() + amount);
+  };
+
+  const calcDefaultValue = (defaultValue) => {
     if (Array.isArray(defaultValue)) {
-      return defaultValue[0] ? new Date(defaultValue[0]) : new Date();
+      return [new Date(defaultValue[0]), new Date(defaultValue[1])];
+    } else if (defaultValue) {
+      return [new Date(defaultValue), advanceDate(defaultValue, 24 * 60 * 60 * 1000)];
     } else {
-      return new Date(defaultValue);
+      return [new Date(), advanceDate(Date.now, 24 * 60 * 60 * 1000)];
     }
   };
 
@@ -170,7 +180,7 @@
       },
 
       leftLabel() {
-        return this.date.getFullYear() + ' ' + this.t('el.datepicker.year') + ' ' + this.t(`el.datepicker.month${ this.date.getMonth() + 1 }`);
+        return this.leftDate.getFullYear() + ' ' + this.t('el.datepicker.year') + ' ' + this.t(`el.datepicker.month${ this.leftDate.getMonth() + 1 }`);
       },
 
       rightLabel() {
@@ -178,11 +188,15 @@
       },
 
       leftYear() {
-        return this.date.getFullYear();
+        return this.leftDate.getFullYear();
       },
 
       leftMonth() {
-        return this.date.getMonth();
+        return this.leftDate.getMonth();
+      },
+
+      leftMonthDate() {
+        return this.leftDate.getDate();
       },
 
       rightYear() {
@@ -193,6 +207,10 @@
         return this.rightDate.getMonth();
       },
 
+      rightMonthDate() {
+        return this.rightDate.getDate();
+      },
+
       minVisibleDate() {
         return this.minDate ? formatDate(this.minDate) : '';
       },
@@ -209,18 +227,12 @@
         return (this.maxDate || this.minDate) ? formatDate(this.maxDate || this.minDate, 'HH:mm:ss') : '';
       },
 
-      rightDate() {
-        const newDate = new Date(this.date);
-        const month = newDate.getMonth();
-        newDate.setDate(1);
-
-        if (month === 11) {
-          newDate.setFullYear(newDate.getFullYear() + 1);
-          newDate.setMonth(0);
+      dateFormat() {
+        if (this.format) {
+          return this.format.replace('HH:mm', '').replace(':ss', '').trim();
         } else {
-          newDate.setMonth(month + 1);
+          return 'yyyy-MM-dd';
         }
-        return newDate;
       },
 
       timeFormat() {
@@ -235,9 +247,12 @@
     data() {
       return {
         popperClass: '',
-        date: this.$options.defaultValue ? calcDefaultValue(this.$options.defaultValue) : new Date(),
+        value: [],
+        defaultValue: null,
         minDate: '',
         maxDate: '',
+        leftDate: new Date(),
+        rightDate: nextMonth(new Date()),
         rangeState: {
           endDate: null,
           selecting: false,
@@ -246,7 +261,6 @@
         },
         showTime: false,
         shortcuts: '',
-        value: '',
         visible: '',
         disabledDate: '',
         firstDayOfWeek: 7,
@@ -257,11 +271,10 @@
     },
 
     watch: {
-      minDate() {
+      minDate(val) {
         this.$nextTick(() => {
-          if (this.maxDate && this.maxDate < this.minDate) {
+          if (this.$refs.maxTimePicker && this.maxDate && this.maxDate < this.minDate) {
             const format = 'HH:mm:ss';
-
             this.$refs.maxTimePicker.selectableRange = [
               [
                 parseDate(formatDate(this.minDate, format), format),
@@ -270,14 +283,39 @@
             ];
           }
         });
+        if (val && this.$refs.minTimePicker) {
+          this.$refs.minTimePicker.date = val;
+          this.$refs.minTimePicker.value = val;
+          this.$refs.minTimePicker.adjustSpinners();
+        }
+      },
+
+      maxDate(val) {
+        if (val && this.$refs.maxTimePicker) {
+          this.$refs.maxTimePicker.date = val;
+          this.$refs.maxTimePicker.value = val;
+          this.$refs.maxTimePicker.adjustSpinners();
+        }
       },
 
       minTimePickerVisible(val) {
-        if (val) this.$nextTick(() => this.$refs.minTimePicker.adjustScrollTop());
+        if (val) {
+          this.$nextTick(() => {
+            this.$refs.minTimePicker.date = this.minDate;
+            this.$refs.minTimePicker.value = this.minDate;
+            this.$refs.minTimePicker.adjustSpinners();
+          });
+        }
       },
 
       maxTimePickerVisible(val) {
-        if (val) this.$nextTick(() => this.$refs.maxTimePicker.adjustScrollTop());
+        if (val) {
+          this.$nextTick(() => {
+            this.$refs.maxTimePicker.date = this.maxDate;
+            this.$refs.maxTimePicker.value = this.maxDate;
+            this.$refs.maxTimePicker.adjustSpinners();
+          });
+        }
       },
 
       value(newVal) {
@@ -285,10 +323,24 @@
           this.minDate = null;
           this.maxDate = null;
         } else if (Array.isArray(newVal)) {
-          this.minDate = newVal[0] ? toDate(newVal[0]) : null;
-          this.maxDate = newVal[1] ? toDate(newVal[1]) : null;
-          if (this.minDate) this.date = new Date(this.minDate);
-          this.handleConfirm(true, false);
+          this.minDate = isDate(newVal[0]) ? new Date(newVal[0]) : null;
+          this.maxDate = isDate(newVal[1]) ? new Date(newVal[1]) : null;
+          // NOTE: currently, maxDate = minDate + 1 month
+          //       should allow them to be set individually in the future
+          if (this.minDate) {
+            this.leftDate = this.minDate;
+            this.rightDate = nextMonth(this.leftDate);
+          } else {
+            this.leftDate = calcDefaultValue(this.defaultValue)[0];
+            this.rightDate = nextMonth(this.leftDate);
+          }
+        }
+      },
+
+      defaultValue(val) {
+        if (!Array.isArray(this.value)) {
+          this.leftDate = calcDefaultValue(val)[0];
+          this.rightDate = nextMonth(this.leftDate);
         }
       }
     },
@@ -297,52 +349,52 @@
       handleClear() {
         this.minDate = null;
         this.maxDate = null;
-        this.date = this.$options.defaultValue ? calcDefaultValue(this.$options.defaultValue) : new Date();
+        this.leftDate = calcDefaultValue(this.defaultValue)[0];
+        this.rightDate = nextMonth(this.leftDate);
         this.handleConfirm(false);
       },
 
+      handleChangeRange(val) {
+        this.minDate = val.minDate;
+        this.maxDate = val.maxDate;
+        this.rangeState = val.rangeState;
+      },
+
       handleDateInput(event, type) {
         const value = event.target.value;
-        const parsedValue = parseDate(value, 'yyyy-MM-dd');
+        if (value.length !== this.dateFormat.length) return;
+        const parsedValue = parseDate(value, this.dateFormat);
 
         if (parsedValue) {
           if (typeof this.disabledDate === 'function' &&
             this.disabledDate(new Date(parsedValue))) {
             return;
           }
-          const target = new Date(type === 'min' ? this.minDate : this.maxDate);
-          if (target) {
-            target.setFullYear(parsedValue.getFullYear());
-            target.setMonth(parsedValue.getMonth(), parsedValue.getDate());
+          if (type === 'min') {
+            this.minDate = new Date(parsedValue);
+            this.leftDate = new Date(parsedValue);
+            this.rightDate = nextMonth(this.leftDate);
+          } else {
+            this.maxDate = new Date(parsedValue);
+            this.leftDate = prevMonth(parsedValue);
+            this.rightDate = new Date(parsedValue);
           }
         }
       },
 
-      handleChangeRange(val) {
-        this.minDate = val.minDate;
-        this.maxDate = val.maxDate;
-        this.rangeState = val.rangeState;
-      },
-
       handleDateChange(event, type) {
         const value = event.target.value;
-        const parsedValue = parseDate(value, 'yyyy-MM-dd');
+        const parsedValue = parseDate(value, this.dateFormat);
         if (parsedValue) {
-          const target = new Date(type === 'min' ? this.minDate : this.maxDate);
-          if (target) {
-            target.setFullYear(parsedValue.getFullYear());
-            target.setMonth(parsedValue.getMonth(), parsedValue.getDate());
-          }
           if (type === 'min') {
-            if (target < this.maxDate) {
-              this.minDate = new Date(target.getTime());
+            this.minDate = modifyDate(this.minDate, parsedValue.getFullYear(), parsedValue.getMonth(), parsedValue.getDate());
+            if (this.minDate > this.maxDate) {
+              this.maxDate = this.minDate;
             }
           } else {
-            if (target > this.minDate) {
-              this.maxDate = new Date(target.getTime());
-              if (this.minDate && this.minDate > this.maxDate) {
-                this.minDate = null;
-              }
+            this.maxDate = modifyDate(this.maxDate, parsedValue.getFullYear(), parsedValue.getMonth(), parsedValue.getDate());
+            if (this.maxDate < this.minDate) {
+              this.minDate = this.maxDate;
             }
           }
         }
@@ -350,29 +402,27 @@
 
       handleTimeChange(event, type) {
         const value = event.target.value;
-        const parsedValue = parseDate(value, 'HH:mm:ss');
+        const parsedValue = parseDate(value, this.timeFormat);
         if (parsedValue) {
-          const target = new Date(type === 'min' ? this.minDate : this.maxDate);
-          if (target) {
-            target.setHours(parsedValue.getHours());
-            target.setMinutes(parsedValue.getMinutes());
-            target.setSeconds(parsedValue.getSeconds());
-          }
           if (type === 'min') {
-            if (target < this.maxDate) {
-              this.minDate = new Date(target.getTime());
+            this.minDate = modifyTime(this.minDate, parsedValue.getHours(), parsedValue.getMinutes(), parsedValue.getSeconds());
+            if (this.minDate > this.maxDate) {
+              this.maxDate = this.minDate;
             }
+            this.$refs.minTimePicker.value = this.minDate;
+            this.minTimePickerVisible = false;
           } else {
-            if (target > this.minDate) {
-              this.maxDate = new Date(target.getTime());
+            this.maxDate = modifyTime(this.maxDate, parsedValue.getHours(), parsedValue.getMinutes(), parsedValue.getSeconds());
+            if (this.maxDate < this.minDate) {
+              this.minDate = this.maxDate;
             }
+            this.$refs.maxTimePicker.value = this.minDate;
+            this.maxTimePickerVisible = false;
           }
-          this.$refs[type + 'TimePicker'].value = target;
-          this[type + 'TimePickerVisible'] = false;
         }
       },
 
-      handleRangePick(val, close = true, user = true) {
+      handleRangePick(val, close = true) {
         if (this.maxDate === val.maxDate && this.minDate === val.minDate) {
           return;
         }
@@ -380,11 +430,7 @@
         this.maxDate = val.maxDate;
         this.minDate = val.minDate;
         if (!close || this.showTime) return;
-        this.handleConfirm(false, user);
-      },
-
-      changeToToday() {
-        this.date = new Date();
+        this.handleConfirm();
       },
 
       handleShortcutClick(shortcut) {
@@ -393,76 +439,68 @@
         }
       },
 
-      resetView() {
-        this.minTimePickerVisible = false;
-        this.maxTimePickerVisible = false;
-      },
-
-      setTime(date, value) {
-        let oldDate = new Date(date.getTime());
-        let hour = value.getHours();
-        let minute = value.getMinutes();
-        let second = value.getSeconds();
-        oldDate.setHours(hour);
-        oldDate.setMinutes(minute);
-        oldDate.setSeconds(second);
-        return new Date(oldDate.getTime());
-      },
-
-      handleMinTimePick(value, visible, user, first) {
+      handleMinTimePick(value, visible, first) {
         this.minDate = this.minDate || new Date();
         if (value) {
-          this.minDate = this.setTime(this.minDate, value);
+          this.minDate = modifyTime(this.minDate, value.getHours(), value.getMinutes(), value.getSeconds());
         }
 
         if (!first) {
           this.minTimePickerVisible = visible;
         }
-      },
 
-      handleMaxTimePick(value, visible, user, first) {
-        if (!this.maxDate) {
-          const now = new Date();
-          if (now >= this.minDate) {
-            this.maxDate = new Date();
-          }
+        if (this.maxDate && this.maxDate.getTime() < this.minDate.getTime()) {
+          this.maxDate = new Date(this.minDate);
         }
+      },
 
+      handleMaxTimePick(value, visible, first) {
         if (this.maxDate && value) {
-          this.maxDate = this.setTime(this.maxDate, value);
+          this.maxDate = modifyTime(this.maxDate, value.getHours(), value.getMinutes(), value.getSeconds());
         }
 
         if (!first) {
           this.maxTimePickerVisible = visible;
         }
+
+        if (this.maxDate && this.minDate && this.minDate.getTime() > this.maxDate.getTime()) {
+          this.minDate = new Date(this.maxDate);
+        }
       },
 
       prevMonth() {
-        this.date = prevMonth(this.date);
+        this.leftDate = prevMonth(this.leftDate);
+        this.rightDate = nextMonth(this.leftDate);
       },
 
       nextMonth() {
-        this.date = nextMonth(this.date);
+        this.leftDate = nextMonth(this.leftDate);
+        this.rightDate = nextMonth(this.leftDate);
       },
 
       nextYear() {
-        const date = this.date;
-        date.setFullYear(date.getFullYear() + 1);
-        this.resetDate();
+        this.leftDate = modifyDate(this.leftDate, this.leftYear + 1, this.leftMonth, this.leftMonthDate);
+        this.rightDate = nextMonth(this.leftDate);
       },
 
       prevYear() {
-        const date = this.date;
-        date.setFullYear(date.getFullYear() - 1);
-        this.resetDate();
+        this.leftDate = modifyDate(this.leftDate, this.leftYear - 1, this.leftMonth, this.leftMonthDate);
+        this.rightDate = nextMonth(this.leftDate);
       },
 
-      handleConfirm(visible = false, user = true) {
-        this.$emit('pick', [this.minDate, this.maxDate], visible, user);
+      handleConfirm(visible = false) {
+        this.$emit('pick', [this.minDate, this.maxDate], visible);
       },
 
-      resetDate() {
-        this.date = new Date(this.date);
+      isValidValue(value) {
+        return Array.isArray(value) &&
+          value && value[0] && value[1] &&
+          isDate(value[0]) && isDate(value[1]) &&
+          value[0].getTime() <= value[1].getTime() && (
+            typeof this.disabledDate === 'function'
+            ? !this.disabledDate(value[0]) && !this.disabledDate(value[1])
+            : true
+          );
       }
     },
 

+ 162 - 168
packages/date-picker/src/panel/date.vue

@@ -23,7 +23,7 @@
                 :placeholder="t('el.datepicker.selectDate')"
                 :value="visibleDate"
                 size="small"
-                @change.native="visibleDate = $event.target.value" />
+                @change.native="handleVisibleDateChange" />
             </span>
             <span class="el-date-picker__editor-wrap">
               <el-input
@@ -32,7 +32,7 @@
                 :placeholder="t('el.datepicker.selectTime')"
                 :value="visibleTime"
                 size="small"
-                @change.native="visibleTime = $event.target.value" />
+                @change.native="handleVisibleTimeChange" />
               <time-picker
                 ref="timepicker"
                 :date="date"
@@ -82,27 +82,27 @@
             <date-table
               v-show="currentView === 'date'"
               @pick="handleDatePick"
-              :year="year"
-              :month="month"
-              :date="date"
-              :week="week"
               :selection-mode="selectionMode"
               :first-day-of-week="firstDayOfWeek"
+              :value="new Date(value)"
+              :default-value="defaultValue ? new Date(defaultValue) : null"
+              :date="date"
               :disabled-date="disabledDate">
             </date-table>
             <year-table
-              ref="yearTable"
-              :year="year"
-              :date="date"
               v-show="currentView === 'year'"
               @pick="handleYearPick"
+              :value="new Date(value)"
+              :default-value="defaultValue ? new Date(defaultValue) : null"
+              :date="date"
               :disabled-date="disabledDate">
             </year-table>
             <month-table
-              :month="month"
-              :date="date"
               v-show="currentView === 'month'"
               @pick="handleMonthPick"
+              :value="new Date(value)"
+              :default-value="defaultValue ? new Date(defaultValue) : null"
+              :date="date"
               :disabled-date="disabledDate">
             </month-table>
           </div>
@@ -126,7 +126,20 @@
 </template>
 
 <script type="text/babel">
-  import { formatDate, parseDate, getWeekNumber } from '../util';
+  import {
+    formatDate,
+    parseDate,
+    getWeekNumber,
+    isDate,
+    modifyDate,
+    modifyTime,
+    clearMilliseconds,
+    clearTime,
+    prevYear,
+    nextYear,
+    prevMonth,
+    nextMonth
+  } from '../util';
   import Locale from 'element-ui/src/mixins/locale';
   import ElInput from 'element-ui/packages/input';
   import TimePicker from './time';
@@ -138,23 +151,33 @@
     mixins: [Locale],
 
     watch: {
-      value(newVal) {
-        if (!newVal) return;
-        newVal = new Date(newVal);
-        if (!isNaN(newVal)) {
-          if (typeof this.disabledDate === 'function' &&
-            this.disabledDate(new Date(newVal))) {
-            return;
+      showTime(val) {
+        /* istanbul ignore if */
+        if (!val) return;
+        this.$nextTick(_ => {
+          const inputElm = this.$refs.input.$el;
+          if (inputElm) {
+            this.pickerWidth = inputElm.getBoundingClientRect().width + 10;
           }
-          this.date = newVal;
-          this.year = newVal.getFullYear();
-          this.month = newVal.getMonth();
-          this.$emit('pick', newVal, false, false);
+        });
+      },
+
+      value(val) {
+        if (isDate(val)) {
+          this.date = new Date(val);
+        } else {
+          this.date = this.defaultValue ? new Date(this.defaultValue) : new Date();
+        }
+      },
+
+      defaultValue(val) {
+        if (!isDate(this.value)) {
+          this.date = val ? new Date(val) : new Date();
         }
       },
 
       timePickerVisible(val) {
-        if (val) this.$nextTick(() => this.$refs.timepicker.adjustScrollTop());
+        if (val) this.$nextTick(() => this.$refs.timepicker.adjustSpinners());
       },
 
       selectionMode(newVal) {
@@ -166,25 +189,31 @@
         } else if (newVal === 'week') {
           this.week = getWeekNumber(this.date);
         }
-      },
-
-      date(newVal) {
-        this.year = newVal.getFullYear();
-        this.month = newVal.getMonth();
-        if (this.selectionMode === 'week') this.week = getWeekNumber(newVal);
       }
     },
 
     methods: {
       handleClear() {
-        this.date = this.$options.defaultValue ? new Date(this.$options.defaultValue) : new Date();
+        this.date = this.defaultValue ? new Date(this.defaultValue) : new Date();
         this.$emit('pick');
       },
 
-      resetDate() {
-        this.date = new Date(this.date);
+      emit(value, ...args) {
+        if (!value) {
+          this.emit('pick', value, ...args);
+          return;
+        }
+        if (this.showTime) {
+          this.$emit('pick', clearMilliseconds(value), ...args);
+        } else {
+          this.$emit('pick', clearTime(value), ...args);
+        }
       },
 
+      // resetDate() {
+      //   this.date = new Date(this.date);
+      // },
+
       showMonthPicker() {
         this.currentView = 'month';
       },
@@ -203,38 +232,26 @@
       // },
 
       prevMonth() {
-        this.month--;
-        if (this.month < 0) {
-          this.month = 11;
-          this.year--;
-        }
+        this.date = prevMonth(this.date);
       },
 
       nextMonth() {
-        this.month++;
-        if (this.month > 11) {
-          this.month = 0;
-          this.year++;
-        }
+        this.date = nextMonth(this.date);
       },
 
-      nextYear() {
+      prevYear() {
         if (this.currentView === 'year') {
-          this.$refs.yearTable.nextTenYear();
+          this.date = prevYear(this.date, 10);
         } else {
-          this.year++;
-          this.date.setFullYear(this.year);
-          this.resetDate();
+          this.date = prevYear(this.date);
         }
       },
 
-      prevYear() {
+      nextYear() {
         if (this.currentView === 'year') {
-          this.$refs.yearTable.prevTenYear();
+          this.date = nextYear(this.date, 10);
         } else {
-          this.year--;
-          this.date.setFullYear(this.year);
-          this.resetDate();
+          this.date = nextYear(this.date);
         }
       },
 
@@ -244,80 +261,61 @@
         }
       },
 
-      handleTimePick(picker, visible) {
-        if (picker) {
-          let oldDate = new Date(this.date.getTime());
-          let hour = picker.getHours();
-          let minute = picker.getMinutes();
-          let second = picker.getSeconds();
-          oldDate.setHours(hour);
-          oldDate.setMinutes(minute);
-          oldDate.setSeconds(second);
-          if (typeof this.disabledDate === 'function' && this.disabledDate(oldDate)) {
-            this.$refs.timepicker.disabled = true;
-            return;
-          }
-          this.$refs.timepicker.disabled = false;
-          this.date = new Date(oldDate.getTime());
+      handleTimePick(value, visible, first) {
+        const newDate = modifyTime(this.date, value.getHours(), value.getMinutes(), value.getSeconds());
+        if (typeof this.disabledDate === 'function' && this.disabledDate(newDate)) {
+          this.$refs.timepicker.disabled = true;
+          return;
         }
+        this.$refs.timepicker.disabled = false;
+        this.date = newDate;
+        this.emit(this.date, true);
 
-        this.timePickerVisible = visible;
+        if (!first) {
+          this.timePickerVisible = visible;
+        }
       },
 
       handleMonthPick(month) {
-        this.month = month;
-        const selectionMode = this.selectionMode;
-        if (selectionMode !== 'month') {
-          this.date.setMonth(month);
-          this.currentView = 'date';
-          this.resetDate();
+        if (this.selectionMode === 'month') {
+          this.date = modifyDate(this.date, this.year, month, 1);
+          this.emit(this.date);
         } else {
-          this.date.setMonth(month);
-          this.year && this.date.setFullYear(this.year);
-          this.resetDate();
-          const value = new Date(this.date.getFullYear(), month, 1);
-          this.$emit('pick', value);
+          this.date = modifyDate(this.date, this.year, month, this.monthDate);
+          // TODO: should emit intermediate value ??
+          // this.emit(this.date);
+          this.currentView = 'date';
         }
       },
 
-      handleDatePick(value, close, user = true) {
+      handleDatePick(value) {
         if (this.selectionMode === 'day') {
-          if (!this.showTime) {
-            this.$emit('pick', new Date(value.getTime()), false, user);
-          }
-          this.date.setFullYear(value.getFullYear());
-          this.date.setMonth(value.getMonth(), value.getDate());
+          this.date = modifyDate(this.date, value.getFullYear(), value.getMonth(), value.getDate());
+          this.emit(this.date, this.showTime);
         } else if (this.selectionMode === 'week') {
-          this.week = value.week;
-          this.$emit('pick', value.date, false, user);
+          this.emit(value.date);
         }
-
-        this.resetDate();
       },
 
-      handleYearPick(year, close = true, user) {
-        this.year = year;
-        if (!close) return;
-
-        this.date.setFullYear(year);
+      handleYearPick(year) {
         if (this.selectionMode === 'year') {
-          this.$emit('pick', new Date(year, 0, 1), false, user);
+          this.date = modifyDate(this.date, year, 0, 1);
+          this.emit(this.date);
         } else {
+          this.date = modifyDate(this.date, year, this.month, this.monthDate);
+          // TODO: should emit intermediate value ??
+          // this.emit(this.date, true);
           this.currentView = 'month';
         }
-
-        this.resetDate();
       },
 
       changeToNow() {
-        this.date.setTime(+new Date());
-        this.$emit('pick', new Date(this.date.getTime()));
-        this.resetDate();
+        this.date = new Date();
+        this.emit(this.date);
       },
 
       confirm() {
-        this.date.setMilliseconds(0);
-        this.$emit('pick', this.date);
+        this.emit(this.date);
       },
 
       resetView() {
@@ -328,24 +326,18 @@
         } else {
           this.currentView = 'date';
         }
-
-        if (this.selectionMode !== 'week') {
-          this.year = this.date.getFullYear();
-          this.month = this.date.getMonth();
-        }
       },
 
       handleEnter() {
-        document.body.addEventListener('keydown', this.handleKeyDown);
+        document.body.addEventListener('keydown', this.handleKeydown);
       },
 
       handleLeave() {
-        this.$refs.timepicker && this.$refs.timepicker.$emit('pick');
         this.$emit('dodestory');
-        document.body.removeEventListener('keydown', this.handleKeyDown);
+        document.body.removeEventListener('keydown', this.handleKeydown);
       },
 
-      handleKeyDown(e) {
+      handleKeydown(e) {
         const keyCode = e.keyCode;
         const list = [38, 40, 37, 39];
         if (this.visible && !this.timePickerVisible) {
@@ -354,11 +346,8 @@
             event.stopPropagation();
             event.preventDefault();
           }
-
-          if (keyCode === 13) {
-            this.confirm();
-            event.stopPropagation();
-            event.preventDefault();
+          if (keyCode === 13) {    // Enter
+            this.$emit('pick', this.date, false);
           }
         }
       },
@@ -389,8 +378,39 @@
             continue;
           }
           this.date = newDate;
+          this.$emit('pick', newDate, true);
           break;
         }
+      },
+
+      handleVisibleTimeChange(event) {
+        const time = parseDate(event.target.value, this.timeFormat);
+        if (time) {
+          this.date = modifyDate(time, this.year, this.month, this.monthDate);
+          this.$refs.timepicker.value = this.date;
+          this.timePickerVisible = false;
+          this.$emit('pick', this.date, true);
+        }
+      },
+
+      handleVisibleDateChange(event) {
+        const date = parseDate(event.target.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.resetView();
+          this.$emit('pick', this.date, true);
+        }
+      },
+
+      isValidValue(value) {
+        return value && !isNaN(value) && (
+          typeof this.disabledDate === 'function'
+          ? !this.disabledDate(value)
+          : true
+        );
       }
     },
 
@@ -398,18 +418,12 @@
       TimePicker, YearTable, MonthTable, DateTable, ElInput
     },
 
-    mounted() {
-      if (this.date && !this.year) {
-        this.year = this.date.getFullYear();
-        this.month = this.date.getMonth();
-      }
-    },
-
     data() {
       return {
         popperClass: '',
-        date: this.$options.defaultValue ? new Date(this.$options.defaultValue) : new Date(),
+        date: new Date(),
         value: '',
+        defaultValue: null,
         showTime: false,
         selectionMode: 'day',
         shortcuts: '',
@@ -417,9 +431,6 @@
         currentView: 'date',
         disabledDate: '',
         firstDayOfWeek: 7,
-        year: null,
-        month: null,
-        week: null,
         showWeekNumber: false,
         timePickerVisible: false,
         format: ''
@@ -427,57 +438,40 @@
     },
 
     computed: {
+      year() {
+        return this.date.getFullYear();
+      },
+
+      month() {
+        return this.date.getMonth();
+      },
+
+      week() {
+        return getWeekNumber(this.date);
+      },
+
+      monthDate() {
+        return this.date.getDate();
+      },
+
       footerVisible() {
         return this.showTime;
       },
 
-      visibleTime: {
-        get() {
-          return formatDate(this.date, this.timeFormat);
-        },
-
-        set(val) {
-          if (val) {
-            const date = parseDate(val, this.timeFormat);
-            if (date) {
-              date.setFullYear(this.date.getFullYear());
-              date.setMonth(this.date.getMonth());
-              date.setDate(this.date.getDate());
-              this.date = date;
-              this.$refs.timepicker.value = date;
-              this.timePickerVisible = false;
-            }
-          }
-        }
+      visibleTime() {
+        const date = this.value || this.defaultValue;
+        return date ? formatDate(date, this.timeFormat) : '';
       },
 
-      visibleDate: {
-        get() {
-          return formatDate(this.date, this.dateFormat);
-        },
-
-        set(val) {
-          const date = parseDate(val, this.dateFormat);
-          if (!date) {
-            return;
-          }
-          if (typeof this.disabledDate === 'function' && this.disabledDate(date)) {
-            return;
-          }
-          date.setHours(this.date.getHours());
-          date.setMinutes(this.date.getMinutes());
-          date.setSeconds(this.date.getSeconds());
-          this.date = date;
-          this.resetView();
-        }
+      visibleDate() {
+        const date = this.value || this.defaultValue;
+        return date ? formatDate(date, this.dateFormat) : '';
       },
 
       yearLabel() {
-        const year = this.year;
-        if (!year) return '';
         const yearTranslation = this.t('el.datepicker.year');
         if (this.currentView === 'year') {
-          const startYear = Math.floor(year / 10) * 10;
+          const startYear = Math.floor(this.year / 10) * 10;
           if (yearTranslation) {
             return startYear + ' ' + yearTranslation + ' - ' + (startYear + 9) + ' ' + yearTranslation;
           }

+ 93 - 108
packages/date-picker/src/panel/time-range.vue

@@ -1,7 +1,6 @@
 <template>
   <transition
     name="el-zoom-in-top"
-    @before-enter="panelCreated"
     @after-leave="$emit('dodestroy')">
     <div
       v-show="visible"
@@ -18,9 +17,7 @@
               :show-seconds="showSeconds"
               @change="handleMinChange"
               @select-range="setMinSelectionRange"
-              :hours="minHours"
-              :minutes="minMinutes"
-              :seconds="minSeconds">
+              :date="minDate">
             </time-spinner>
           </div>
         </div>
@@ -34,9 +31,7 @@
               :show-seconds="showSeconds"
               @change="handleMaxChange"
               @select-range="setMaxSelectionRange"
-              :hours="maxHours"
-              :minutes="maxMinutes"
-              :seconds="maxSeconds">
+              :date="maxDate">
             </time-spinner>
           </div>
         </div>
@@ -57,27 +52,30 @@
 </template>
 
 <script type="text/babel">
-  import { parseDate, limitRange } from '../util';
+  import {
+    parseDate,
+    limitTimeRange,
+    modifyDate,
+    clearMilliseconds,
+    timeWithinRange
+  } from '../util';
   import Locale from 'element-ui/src/mixins/locale';
   import TimeSpinner from '../basic/time-spinner';
 
   const MIN_TIME = parseDate('00:00:00', 'HH:mm:ss');
   const MAX_TIME = parseDate('23:59:59', 'HH:mm:ss');
-  const isDisabled = function(minTime, maxTime) {
-    const minValue = minTime.getHours() * 3600 + minTime.getMinutes() * 60 + minTime.getSeconds();
-    const maxValue = maxTime.getHours() * 3600 + maxTime.getMinutes() * 60 + maxTime.getSeconds();
 
-    return minValue > maxValue;
+  const minTimeOfDay = function(date) {
+    return modifyDate(MIN_TIME, date.getFullYear(), date.getMonth(), date.getDate());
+  };
+  
+  const maxTimeOfDay = function(date) {
+    return modifyDate(MAX_TIME, date.getFullYear(), date.getMonth(), date.getDate());
   };
-  const clacTime = function(time) {
-    time = Array.isArray(time) ? time : [time];
-    const minTime = time[0] || new Date();
-    const date = new Date();
-    date.setHours(date.getHours() + 1);
-    const maxTime = time[1] || date;
 
-    if (minTime > maxTime) return clacTime();
-    return { minTime, maxTime };
+  // increase time by amount of milliseconds, but within the range of day
+  const advanceTime = function(date, amount) {
+    return new Date(Math.min(date.getTime() + amount, maxTimeOfDay(date).getTime()));
   };
 
   export default {
@@ -96,25 +94,21 @@
 
       spinner() {
         return this.selectionRange[0] < this.offset ? this.$refs.minSpinner : this.$refs.maxSpinner;
+      },
+
+      btnDisabled() {
+        return this.minDate.getTime() > this.maxDate.getTime();
       }
     },
 
-    props: ['value'],
-
     data() {
-      const time = clacTime(this.$options.defaultValue);
-
       return {
         popperClass: '',
-        minTime: time.minTime,
-        maxTime: time.maxTime,
-        btnDisabled: isDisabled(time.minTime, time.maxTime),
-        maxHours: time.maxTime.getHours(),
-        maxMinutes: time.maxTime.getMinutes(),
-        maxSeconds: time.maxTime.getSeconds(),
-        minHours: time.minTime.getHours(),
-        minMinutes: time.minTime.getMinutes(),
-        minSeconds: time.minTime.getSeconds(),
+        minDate: new Date(),
+        maxDate: new Date(),
+        value: [],
+        oldValue: [new Date(), new Date()],
+        defaultValue: null,
         format: 'HH:mm:ss',
         visible: false,
         selectionRange: [0, 2]
@@ -122,87 +116,60 @@
     },
 
     watch: {
-      value(newVal) {
-        this.panelCreated();
-        this.$nextTick(_ => this.adjustScrollTop());
+      value(value) {
+        if (Array.isArray(value)) {
+          this.minDate = new Date(value[0]);
+          this.maxDate = new Date(value[1]);
+        } else {
+          if (Array.isArray(this.defaultValue)) {
+            this.minDate = new Date(this.defaultValue[0]);
+            this.maxDate = new Date(this.defaultValue[1]);
+          } else if (this.defaultValue) {
+            this.minDate = new Date(this.defaultValue);
+            this.maxDate = advanceTime(new Date(this.defaultValue), 60 * 60 * 1000);
+          } else {
+            this.minDate = new Date();
+            this.maxDate = advanceTime(new Date(), 60 * 60 * 1000);
+          }
+        }
+        if (this.visible) {
+          this.$nextTick(_ => this.adjustSpinners());
+        }
       },
 
       visible(val) {
         if (val) {
+          this.oldValue = this.value;
           this.$nextTick(() => this.$refs.minSpinner.emitSelectRange('hours'));
         }
       }
     },
 
     methods: {
-      panelCreated() {
-        const time = clacTime(this.value);
-        if (time.minTime === this.minTime && time.maxTime === this.maxTime) {
-          return;
-        }
-
-        this.handleMinChange({
-          hours: time.minTime.getHours(),
-          minutes: time.minTime.getMinutes(),
-          seconds: time.minTime.getSeconds()
-        }, true);
-        this.handleMaxChange({
-          hours: time.maxTime.getHours(),
-          minutes: time.maxTime.getMinutes(),
-          seconds: time.maxTime.getSeconds()
-        }, true);
-      },
-
       handleClear() {
-        this.handleCancel();
+        this.$emit('pick', []);
       },
 
       handleCancel() {
-        this.$emit('pick');
+        this.$emit('pick', this.oldValue);
       },
 
-      handleChange(notUser) {
-        if (this.minTime > this.maxTime) return;
-        MIN_TIME.setFullYear(this.minTime.getFullYear());
-        MIN_TIME.setMonth(this.minTime.getMonth(), this.minTime.getDate());
-        MAX_TIME.setFullYear(this.maxTime.getFullYear());
-        MAX_TIME.setMonth(this.maxTime.getMonth(), this.maxTime.getDate());
-        this.$refs.minSpinner.selectableRange = [[MIN_TIME, this.maxTime]];
-        this.$refs.maxSpinner.selectableRange = [[this.minTime, MAX_TIME]];
-        this.handleConfirm(true, false, notUser);
+      handleMinChange(date) {
+        this.minDate = clearMilliseconds(date);
+        this.handleChange();
       },
 
-      handleMaxChange(date, notUser) {
-        if (date.hours !== undefined) {
-          this.maxTime.setHours(date.hours);
-          this.maxHours = this.maxTime.getHours();
-        }
-        if (date.minutes !== undefined) {
-          this.maxTime.setMinutes(date.minutes);
-          this.maxMinutes = this.maxTime.getMinutes();
-        }
-        if (date.seconds !== undefined) {
-          this.maxTime.setSeconds(date.seconds);
-          this.maxSeconds = this.maxTime.getSeconds();
-        }
+      handleMaxChange(date) {
+        this.maxDate = clearMilliseconds(date);
         this.handleChange();
       },
 
-      handleMinChange(date, notUser) {
-        if (date.hours !== undefined) {
-          this.minTime.setHours(date.hours);
-          this.minHours = this.minTime.getHours();
-        }
-        if (date.minutes !== undefined) {
-          this.minTime.setMinutes(date.minutes);
-          this.minMinutes = this.minTime.getMinutes();
+      handleChange() {
+        if (this.isValidValue([this.minDate, this.maxDate])) {
+          this.$refs.minSpinner.selectableRange = [[minTimeOfDay(this.minDate), this.maxDate]];
+          this.$refs.maxSpinner.selectableRange = [[this.minDate, maxTimeOfDay(this.maxDate)]];
+          this.$emit('pick', [this.minDate, this.maxDate], true);
         }
-        if (date.seconds !== undefined) {
-          this.minTime.setSeconds(date.seconds);
-          this.minSeconds = this.minTime.getSeconds();
-        }
-
-        this.handleChange();
       },
 
       setMinSelectionRange(start, end) {
@@ -215,24 +182,19 @@
         this.selectionRange = [start + this.offset, end + this.offset];
       },
 
-      handleConfirm(visible = false, first = false, notUser = false) {
+      handleConfirm(visible = false) {
         const minSelectableRange = this.$refs.minSpinner.selectableRange;
         const maxSelectableRange = this.$refs.maxSpinner.selectableRange;
 
-        this.minTime = limitRange(this.minTime, minSelectableRange);
-        this.maxTime = limitRange(this.maxTime, maxSelectableRange);
-
-        if (first) return;
-        this.$emit('pick', [this.minTime, this.maxTime], visible, !notUser);
-      },
+        this.minDate = limitTimeRange(this.minDate, minSelectableRange, this.format);
+        this.maxDate = limitTimeRange(this.maxDate, maxSelectableRange, this.format);
 
-      adjustScrollTop() {
-        this.$refs.minSpinner.adjustScrollTop();
-        this.$refs.maxSpinner.adjustScrollTop();
+        this.$emit('pick', [this.minDate, this.maxDate], visible);
       },
 
-      scrollDown(step) {
-        this.spinner.scrollDown(step);
+      adjustSpinners() {
+        this.$refs.minSpinner.adjustSpinners();
+        this.$refs.maxSpinner.adjustSpinners();
       },
 
       changeSelectionRange(step) {
@@ -246,11 +208,34 @@
         } else {
           this.$refs.maxSpinner.emitSelectRange(mapping[next - half]);
         }
-      }
-    },
+      },
 
-    mounted() {
-      this.$nextTick(() => this.handleConfirm(true, true));
+      isValidValue(date) {
+        return Array.isArray(date) &&
+          timeWithinRange(this.minDate, this.$refs.minSpinner.selectableRange) &&
+          timeWithinRange(this.maxDate, this.$refs.maxSpinner.selectableRange);
+      },
+
+      handleKeydown(event) {
+        const keyCode = event.keyCode;
+        const mapping = { 38: -1, 40: 1, 37: -1, 39: 1 };
+
+         // Left or Right
+        if (keyCode === 37 || keyCode === 39) {
+          const step = mapping[keyCode];
+          this.changeSelectionRange(step);
+          event.preventDefault();
+          return;
+        }
+
+        // Up or Down
+        if (keyCode === 38 || keyCode === 40) {
+          const step = mapping[keyCode];
+          this.spinner.scrollDown(step);
+          event.preventDefault();
+          return;
+        }
+      }
     }
   };
 </script>

+ 30 - 21
packages/date-picker/src/panel/time-select.vue

@@ -9,7 +9,7 @@
       <el-scrollbar noresize wrap-class="el-picker-panel__content">
         <div class="time-select-item"
           v-for="item in items"
-          :class="{ selected: value === item.value, disabled: item.disabled }"
+          :class="{ selected: value === item.value, disabled: item.disabled, default: item.value === defaultValue }"
           :disabled="item.disabled"
           @click="handleClick(item)">{{ item.value }}</div>
       </el-scrollbar>
@@ -78,11 +78,6 @@
     watch: {
       value(val) {
         if (!val) return;
-        if (this.minTime && compareTime(val, this.minTime) < 0) {
-          this.$emit('pick', '', false, false);
-        } else if (this.maxTime && compareTime(val, this.maxTime) > 0) {
-          this.$emit('pick', '', false, false);
-        }
         this.$nextTick(() => this.scrollToOption());
       }
     },
@@ -95,34 +90,47 @@
       },
 
       handleClear() {
-        this.$emit('pick', '', false, false);
+        this.$emit('pick');
       },
 
-      scrollToOption(className = 'selected') {
+      scrollToOption(selector = '.selected') {
         const menu = this.$refs.popper.querySelector('.el-picker-panel__content');
-        scrollIntoView(menu, menu.getElementsByClassName(className)[0]);
+        scrollIntoView(menu, menu.querySelector(selector));
       },
 
       handleMenuEnter() {
-        this.$nextTick(() => this.scrollToOption());
+        const selected = this.items.map(item => item.value).indexOf(this.value) !== -1;
+        const hasDefault = this.items.map(item => item.value).indexOf(this.defaultValue) !== -1;
+        const option = (selected && '.selected') || (hasDefault && '.default') || '.time-select-item:not(.disabled)';
+        this.$nextTick(() => this.scrollToOption(option));
       },
 
       scrollDown(step) {
         const items = this.items;
+        const length = items.length;
+        let total = items.length;
         let index = items.map(item => item.value).indexOf(this.value);
-        let length = items.length;
-        let total = Math.abs(step);
-        step = step > 0 ? 1 : -1;
-        while (length-- && total) {
-          index = (index + step + items.length) % items.length;
-          const item = items[index];
-          if (!item.disabled) {
-            total--;
+        while (total--) {
+          index = (index + step + length) % length;
+          if (!items[index].disabled) {
+            this.$emit('pick', items[index].value, true);
+            return;
           }
         }
-        if (!items[index].disabled) {
-          this.value = items[index].value;
-          this.$emit('pick', this.value, true);
+      },
+
+      isValidValue(date) {
+        return this.items.filter(item => !item.disabled).map(item => item.value).indexOf(date) !== -1;
+      },
+
+      handleKeydown(event) {
+        const keyCode = event.keyCode;
+        if (keyCode === 38 || keyCode === 40) {
+          const mapping = { 40: 1, 38: -1 };
+          const offset = mapping[keyCode.toString()];
+          this.scrollDown(offset);
+          event.stopPropagation();
+          return;
         }
       }
     },
@@ -134,6 +142,7 @@
         end: '18:00',
         step: '00:30',
         value: '',
+        defaultValue: '',
         visible: false,
         minTime: '',
         maxTime: '',

+ 51 - 76
packages/date-picker/src/panel/time.vue

@@ -1,7 +1,7 @@
 <template>
   <transition name="el-zoom-in-top" @after-leave="$emit('dodestroy')">
     <div
-      v-show="currentVisible"
+      v-show="visible"
       class="el-time-panel el-popper"
       :class="popperClass">
       <div class="el-time-panel__content" :class="{ 'has-seconds': showSeconds }">
@@ -10,9 +10,7 @@
           @change="handleChange"
           :show-seconds="showSeconds"
           @select-range="setSelectionRange"
-          :hours="hours"
-          :minutes="minutes"
-          :seconds="seconds">
+          :date="date">
         </time-spinner>
       </div>
       <div class="el-time-panel__footer">
@@ -31,7 +29,7 @@
 </template>
 
 <script type="text/babel">
-  import { limitRange } from '../util';
+  import { limitTimeRange, isDate, clearMilliseconds, timeWithinRange } from '../util';
   import Locale from 'element-ui/src/mixins/locale';
 
   export default {
@@ -42,21 +40,13 @@
     },
 
     props: {
-      date: {
-        default() {
-          return new Date();
-        }
-      },
       visible: Boolean
     },
 
     watch: {
       visible(val) {
-        this.currentVisible = val;
         if (val) {
-          this.oldHours = this.hours;
-          this.oldMinutes = this.minutes;
-          this.oldSeconds = this.seconds;
+          this.oldValue = this.value;
           this.$nextTick(() => this.$refs.spinner.emitSelectRange('hours'));
         }
       },
@@ -64,27 +54,25 @@
       value(newVal) {
         let date;
         if (newVal instanceof Date) {
-          date = limitRange(newVal, this.selectableRange);
+          date = limitTimeRange(newVal, this.selectableRange, this.format);
         } else if (!newVal) {
-          date = new Date();
+          date = this.defaultValue ? new Date(this.defaultValue) : new Date();
         }
 
-        this.handleChange({
-          hours: date.getHours(),
-          minutes: date.getMinutes(),
-          seconds: date.getSeconds()
-        }, true);
-        this.$nextTick(_ => this.adjustScrollTop());
+        this.date = date;
+        if (this.visible) {
+          this.$nextTick(_ => this.adjustSpinners());
+        }
       },
 
       selectableRange(val) {
         this.$refs.spinner.selectableRange = val;
       },
 
-      date(val) {
-        if (!val) return;
-        this.currentDate = val;
-        this.reinitDate();
+      defaultValue(val) {
+        if (!isDate(this.value)) {
+          this.date = val ? new Date(val) : new Date();
+        }
       }
     },
 
@@ -93,15 +81,10 @@
         popperClass: '',
         format: 'HH:mm:ss',
         value: '',
-        hours: 0,
-        minutes: 0,
-        seconds: 0,
-        oldHours: 0,
-        oldMinutes: 0,
-        oldSeconds: 0,
+        defaultValue: null,
+        date: new Date(),
+        oldValue: new Date(),
         selectableRange: [],
-        currentDate: this.$options.defaultValue || this.date || new Date(),
-        currentVisible: this.visible || false,
         selectionRange: [0, 2],
         disabled: false
       };
@@ -114,35 +97,16 @@
     },
 
     methods: {
-      handleClear() {
-        this.$emit('pick', '', false, true);
-      },
-
       handleCancel() {
-        this.currentDate.setHours(this.oldHours);
-        this.currentDate.setMinutes(this.oldMinutes);
-        this.currentDate.setSeconds(this.oldSeconds);
-        this.hours = this.currentDate.getHours();
-        this.minutes = this.currentDate.getMinutes();
-        this.seconds = this.currentDate.getSeconds();
-        const date = new Date(limitRange(this.currentDate, this.selectableRange, 'HH:mm:ss'));
-        this.$emit('pick', date, false, true);
+        this.$emit('pick', this.oldValue);
       },
 
-      handleChange(date, notUser) {
-        if (date.hours !== undefined) {
-          this.currentDate.setHours(date.hours);
-          this.hours = this.currentDate.getHours();
-        }
-        if (date.minutes !== undefined) {
-          this.currentDate.setMinutes(date.minutes);
-          this.minutes = this.currentDate.getMinutes();
+      handleChange(date) {
+        this.date = clearMilliseconds(date);
+        // if date is out of range, do not emit
+        if (this.isValidValue(this.date)) {
+          this.$emit('pick', this.date, true);
         }
-        if (date.seconds !== undefined) {
-          this.currentDate.setSeconds(date.seconds);
-          this.seconds = this.currentDate.getSeconds();
-        }
-        this.handleConfirm(true, null, notUser);
       },
 
       setSelectionRange(start, end) {
@@ -150,18 +114,39 @@
         this.selectionRange = [start, end];
       },
 
-      handleConfirm(visible = false, first, notUser = false) {
+      handleConfirm(visible = false, first) {
         if (first) return;
-        const date = new Date(limitRange(this.currentDate, this.selectableRange, 'HH:mm:ss'));
-        this.$emit('pick', date, visible, !notUser, false);
+        const date = clearMilliseconds(limitTimeRange(this.date, this.selectableRange, this.format));
+        this.$emit('pick', date, visible, first);
+      },
+
+      handleKeydown(event) {
+        const keyCode = event.keyCode;
+        const mapping = { 38: -1, 40: 1, 37: -1, 39: 1 };
+
+        // Left or Right
+        if (keyCode === 37 || keyCode === 39) {
+          const step = mapping[keyCode];
+          this.changeSelectionRange(step);
+          event.preventDefault();
+          return;
+        }
+
+        // Up or Down
+        if (keyCode === 38 || keyCode === 40) {
+          const step = mapping[keyCode];
+          this.$refs.spinner.scrollDown(step);
+          event.preventDefault();
+          return;
+        }
       },
 
-      adjustScrollTop() {
-        return this.$refs.spinner.adjustScrollTop();
+      isValidValue(date) {
+        return timeWithinRange(date, this.selectableRange, this.format);
       },
 
-      scrollDown(step) {
-        this.$refs.spinner.scrollDown(step);
+      adjustSpinners() {
+        return this.$refs.spinner.adjustSpinners();
       },
 
       changeSelectionRange(step) {
@@ -170,19 +155,9 @@
         const index = list.indexOf(this.selectionRange[0]);
         const next = (index + step + list.length) % list.length;
         this.$refs.spinner.emitSelectRange(mapping[next]);
-      },
-
-      reinitDate() {
-        this.hours = this.currentDate.getHours();
-        this.minutes = this.currentDate.getMinutes();
-        this.seconds = this.currentDate.getSeconds();
       }
     },
 
-    created() {
-      this.reinitDate();
-    },
-
     mounted() {
       this.$nextTick(() => this.handleConfirm(true, true));
       this.$emit('mounted');

+ 239 - 90
packages/date-picker/src/picker.vue

@@ -13,9 +13,10 @@
     @blur="handleBlur"
     @keydown.native="handleKeydown"
     :value="displayValue"
+    @input="value => userInput = value"
     @mouseenter.native="handleMouseEnter"
     @mouseleave.native="showClose = false"
-    @change.native="displayValue = $event.target.value"
+    @change.native="handleChange"
     :validateEvent="false"
     :prefix-icon="triggerClass"
     ref="reference">
@@ -36,6 +37,7 @@
     @click="handleRangeClick"
     @mouseenter="handleMouseEnter"
     @mouseleave="showClose = false"
+    @keydown="handleKeydown"
     ref="reference"
     v-clickoutside="handleClose"
     v-else>
@@ -43,15 +45,17 @@
     <input
       :placeholder="startPlaceholder"
       :value="displayValue && displayValue[0]"
-      @keydown="handleKeydown"
+      @input="handleStartInput"
       @change="handleStartChange"
+      @focus="handleFocus"
       class="el-range-input">
     <span class="el-range-separator">{{ rangeSeparator }}</span>
     <input
       :placeholder="endPlaceholder"
       :value="displayValue && displayValue[1]"
-      @keydown="handleKeydown"
+      @input="handleEndInput"
       @change="handleEndChange"
+      @focus="handleFocus"
       class="el-range-input">
     <i
       @click="handleClickIcon"
@@ -65,7 +69,7 @@
 <script>
 import Vue from 'vue';
 import Clickoutside from 'element-ui/src/utils/clickoutside';
-import { formatDate, parseDate, getWeekNumber, equalDate, isDate } from './util';
+import { formatDate, parseDate, isDate, getWeekNumber } from './util';
 import Popper from 'element-ui/src/utils/vue-popper';
 import Emitter from 'element-ui/src/mixins/emitter';
 import Focus from 'element-ui/src/mixins/focus';
@@ -225,6 +229,26 @@ const PLACEMENT_MAP = {
   right: 'bottom-end'
 };
 
+const parseAsFormatAndType = (value, cutsomFormat, type, rangeSeparator = '-') => {
+  if (!value) return null;
+  const parser = (
+    TYPE_VALUE_RESOLVER_MAP[type] ||
+    TYPE_VALUE_RESOLVER_MAP['default']
+  ).parser;
+  const format = cutsomFormat || DEFAULT_FORMATS[type];
+  return parser(value, format, rangeSeparator);
+};
+
+const formatAsFormatAndType = (value, cutsomFormat, type) => {
+  if (!value) return null;
+  const formatter = (
+    TYPE_VALUE_RESOLVER_MAP[type] ||
+    TYPE_VALUE_RESOLVER_MAP['default']
+  ).formatter;
+  const format = cutsomFormat || DEFAULT_FORMATS[type];
+  return formatter(value, format);
+};
+
 // only considers date-picker's value: Date or [Date, Date]
 const valueEquals = function(a, b) {
   const aIsArray = a instanceof Array;
@@ -245,6 +269,7 @@ export default {
   props: {
     size: String,
     format: String,
+    valueFormat: String,
     readonly: Boolean,
     placeholder: String,
     startPlaceholder: String,
@@ -280,33 +305,44 @@ export default {
     return {
       pickerVisible: false,
       showClose: false,
-      currentValue: '',
+      userInput: null,
+      valueOnOpen: null,  // value when picker opens, used to determine whether to emit change
       unwatchPickerOptions: null
     };
   },
 
   watch: {
     pickerVisible(val) {
-      if (!val) this.dispatch('ElFormItem', 'el.form.blur');
       if (this.readonly || this.disabled) return;
-      val ? this.showPicker() : this.hidePicker();
-    },
-    currentValue(val) {
-      if (val) return;
-      if (this.picker && typeof this.picker.handleClear === 'function') {
-        this.picker.handleClear();
+      if (val) {
+        this.showPicker();
+        this.valueOnOpen = this.value;
       } else {
-        this.$emit('input');
+        this.hidePicker();
+        this.emitChange(this.value);
+        // flush user input if it is parsable
+        // this.displayValue here is not a typo, it merges text for both panels in range mode
+        const parsedValue = this.parseString(this.displayValue);
+        if (this.userInput && parsedValue && this.isValidValue(parsedValue)) {
+          this.userInput = null;
+        }
+        this.dispatch('ElFormItem', 'el.form.blur');
+        this.blur();
       }
     },
-    value: {
+    parsedValue: {
       immediate: true,
       handler(val) {
-        this.currentValue = isDate(val) ? new Date(val) : val;
+        if (this.picker) {
+          this.picker.value = val;
+        }
       }
     },
-    displayValue(val) {
-      this.dispatch('ElFormItem', 'el.form.change');
+    defaultValue(val) {
+      // NOTE: should eventually move to jsx style picker + panel ?
+      if (this.picker) {
+        this.picker.defaultValue = val;
+      }
     }
   },
 
@@ -328,7 +364,7 @@ export default {
     },
 
     valueIsEmpty() {
-      const val = this.currentValue;
+      const val = this.value;
       if (Array.isArray(val)) {
         for (let i = 0, len = val.length; i < len; i++) {
           if (val[i]) {
@@ -366,36 +402,24 @@ export default {
       return HAVE_TRIGGER_TYPES.indexOf(this.type) !== -1;
     },
 
-    displayValue: {
-      get() {
-        const value = this.currentValue;
-        if (!value) return;
-        const formatter = (
-          TYPE_VALUE_RESOLVER_MAP[this.type] ||
-          TYPE_VALUE_RESOLVER_MAP['default']
-        ).formatter;
-        const format = DEFAULT_FORMATS[this.type];
-
-        return formatter(value, this.format || format);
-      },
+    displayValue() {
+      const formattedValue = formatAsFormatAndType(this.parsedValue, this.format, this.type, this.rangeSeparator);
+      if (Array.isArray(this.userInput)) {
+        return [
+          this.userInput[0] || (formattedValue && formattedValue[0]) || '',
+          this.userInput[1] || (formattedValue && formattedValue[1]) || ''
+        ];
+      } else {
+        return this.userInput !== null ? this.userInput : formattedValue || '';
+      }
+    },
 
-      set(value) {
-        if (value) {
-          const type = this.type;
-          const parser = (
-            TYPE_VALUE_RESOLVER_MAP[type] ||
-            TYPE_VALUE_RESOLVER_MAP['default']
-          ).parser;
-          const parsedValue = parser(value, this.format || DEFAULT_FORMATS[type]);
-
-          if (parsedValue && this.picker) {
-            this.picker.value = parsedValue;
-          }
-        } else {
-          this.$emit('input', value);
-          this.picker.value = value;
-        }
-        this.$forceUpdate();
+    parsedValue() {
+      const isParsed = isDate(this.value) || (Array.isArray(this.value) && this.value.every(isDate));
+      if (this.valueFormat && !isParsed) {
+        return parseAsFormatAndType(this.value, this.valueFormat, this.type, this.rangeSeparator) || this.value;
+      } else {
+        return this.value;
       }
     }
   },
@@ -410,6 +434,41 @@ export default {
   },
 
   methods: {
+    blur() {
+      this.refInput.forEach(input => input.blur());
+    },
+
+    // {parse, formatTo} Value deals maps component value with internal Date
+    // parseValue validates value according to panel, requires picker to be mounted
+    parseValue(value, customFormat) {
+      if (!value || (!Array.isArray(value) || !value.every(val => val))) {
+        return null;
+      }
+      const format = customFormat || this.valueFormat;
+      const parsedValue = parseAsFormatAndType(value, format, this.type, this.rangeSeparator);
+      return this.isValidValue(parsedValue) ? parsedValue : null;
+    },
+
+    formatToValue(date, customFormat) {
+      if (this.valueFormat && (isDate(date) || Array.isArray(date))) {
+        const format = customFormat || this.valueFormat;
+        return formatAsFormatAndType(date, format, this.type, this.rangeSeparator);
+      } else {
+        return date;
+      }
+    },
+
+    // {parse, formatTo} String deals with user input
+    parseString(value) {
+      const type = Array.isArray(value) ? this.type : this.type.replace('range', '');
+      return parseAsFormatAndType(value, this.format, type);
+    },
+
+    formatToString(value) {
+      const type = Array.isArray(value) ? this.type : this.type.replace('range', '');
+      return formatAsFormatAndType(value, this.format, type);
+    },
+
     handleMouseEnter() {
       if (this.readonly || this.disabled) return;
       if (!this.valueIsEmpty && this.clearable) {
@@ -417,45 +476,74 @@ export default {
       }
     },
 
-    handleStartChange(event) {
-      if (this.displayValue && this.displayValue[1]) {
-        this.displayValue = [event.target.value, this.displayValue[1]];
+    handleChange() {
+      if (this.userInput) {
+        const value = this.parseString(this.displayValue);
+        if (value) {
+          this.picker.value = value;
+          if (this.isValidValue(value)) {
+            this.emitInput(value);
+            this.userInput = null;
+          }
+        }
+      }
+    },
+
+    handleStartInput(event) {
+      if (this.userInput) {
+        this.userInput = [event.target.value, this.userInput[1]];
       } else {
-        this.displayValue = [event.target.value, event.target.value];
+        this.userInput = [event.target.value, null];
       }
     },
 
-    handleEndChange(event) {
-      if (this.displayValue && this.displayValue[0]) {
-        this.displayValue = [this.displayValue[0], event.target.value];
+    handleEndInput(event) {
+      if (this.userInput) {
+        this.userInput = [this.userInput[0], event.target.value];
       } else {
-        this.displayValue = [event.target.value, event.target.value];
+        this.userInput = [null, event.target.value];
+      }
+    },
+
+    handleStartChange(event) {
+      const value = this.parseString(this.userInput && this.userInput[0]);
+      if (value) {
+        this.userInput = [this.formatToString(value), this.displayValue[1]];
+        const newValue = [value, this.picker.value && this.picker.value[1]];
+        this.picker.value = newValue;
+        if (this.isValidValue(newValue)) {
+          this.emitInput(newValue);
+          this.userInput = null;
+        }
+      }
+    },
+
+    handleEndChange(event) {
+      const value = this.parseString(this.userInput && this.userInput[1]);
+      if (value) {
+        this.userInput = [this.displayValue[0], this.formatToString(value)];
+        const newValue = [this.picker.value && this.picker.value[0], value];
+        this.picker.value = newValue;
+        if (this.isValidValue(newValue)) {
+          this.emitInput(newValue);
+          this.userInput = null;
+        }
       }
     },
 
     handleClickIcon(event) {
       if (this.readonly || this.disabled) return;
       if (this.showClose) {
-        this.currentValue = this.$options.defaultValue || '';
-        this.showClose = false;
         event.stopPropagation();
-      } else {
-        this.pickerVisible = !this.pickerVisible;
-      }
-    },
-
-    dateChanged(dateA, dateB) {
-      if (Array.isArray(dateA)) {
-        let len = dateA.length;
-        if (!dateB) return true;
-        while (len--) {
-          if (!equalDate(dateA[len], dateB[len])) return true;
+        this.emitInput(null);
+        this.emitChange(null);
+        this.showClose = false;
+        if (this.picker && typeof this.picker.handleClear === 'function') {
+          this.picker.handleClear();
         }
       } else {
-        if (!equalDate(dateA, dateB)) return true;
+        this.pickerVisible = !this.pickerVisible;
       }
-
-      return false;
     },
 
     handleClose() {
@@ -481,10 +569,54 @@ export default {
     handleKeydown(event) {
       const keyCode = event.keyCode;
 
-      // TAB or ESC
-      if (keyCode === 9 || keyCode === 27) {
+      // ESC
+      if (keyCode === 27) {
         this.pickerVisible = false;
         event.stopPropagation();
+        return;
+      }
+
+      // Tab
+      if (keyCode === 9) {
+        if (!this.ranged) {
+          this.handleChange();
+          this.pickerVisible = this.picker.visible = false;
+          this.blur();
+          event.stopPropagation();
+        } else {
+          // user may change focus between two input
+          setTimeout(() => {
+            if (this.refInput.indexOf(document.activeElement) === -1) {
+              this.pickerVisible = false;
+              this.blur();
+              event.stopPropagation();
+            }
+          }, 0);
+        }
+        return;
+      }
+
+      // Enter
+      if (keyCode === 13 && this.displayValue) {
+        const value = this.parseString(this.displayValue);
+        if (this.isValidValue(value)) {
+          this.handleChange();
+          this.pickerVisible = this.picker.visible = false;
+          this.blur();
+        }
+        event.stopPropagation();
+        return;
+      }
+
+      // if user is typing, do not let picker handle key input
+      if (this.userInput) {
+        event.stopPropagation();
+        return;
+      }
+
+      // delegate other keys to panel
+      if (this.picker && this.picker.handleKeydown) {
+        this.picker.handleKeydown(event);
       }
     },
 
@@ -514,22 +646,17 @@ export default {
 
       this.updatePopper();
 
-      if (this.currentValue instanceof Date) {
-        this.picker.date = new Date(this.currentValue.getTime());
-      } else {
-        this.picker.value = this.currentValue;
-      }
+      this.picker.value = this.parsedValue;
       this.picker.resetView && this.picker.resetView();
 
       this.$nextTick(() => {
-        this.picker.adjustScrollTop && this.picker.adjustScrollTop();
+        this.picker.adjustSpinners && this.picker.adjustSpinners();
       });
     },
 
     mountPicker() {
-      const defaultValue = this.defaultValue || this.currentValue;
-      const panel = merge({}, this.panel, { defaultValue });
-      this.picker = new Vue(panel).$mount();
+      this.picker = new Vue(this.panel).$mount();
+      this.picker.defaultValue = this.defaultValue;
       this.picker.popperClass = this.popperClass;
       this.popperElm = this.picker.$el;
       this.picker.width = this.reference.getBoundingClientRect().width;
@@ -566,15 +693,10 @@ export default {
       this.picker.resetView && this.picker.resetView();
 
       this.picker.$on('dodestroy', this.doDestroy);
-      this.picker.$on('pick', (date = '', visible = false, user = true) => {
-        // do not emit if values are same
-        if (!valueEquals(this.value, date)) {
-          this.$emit('input', date);
-          if (user && this.value !== date) {
-            this.$nextTick(() => this.$emit('change', this.displayValue));
-          };
-        }
+      this.picker.$on('pick', (date = '', visible = false) => {
+        this.userInput = null;
         this.pickerVisible = this.picker.visible = visible;
+        this.emitInput(date);
         this.picker.resetView && this.picker.resetView();
       });
 
@@ -599,6 +721,33 @@ export default {
         }
         this.picker.$el.parentNode.removeChild(this.picker.$el);
       }
+    },
+
+    emitChange(val) {
+      const formatted = this.formatToValue(val);
+      if (!valueEquals(this.valueOnOpen, formatted)) {
+        this.$emit('change', formatted);
+        this.dispatch('ElFormItem', 'el.form.change', formatted);
+        this.valueOnOpen = formatted;
+      }
+    },
+
+    emitInput(val) {
+      const formatted = this.formatToValue(val);
+      if (!valueEquals(this.value, formatted)) {
+        this.$emit('input', formatted);
+      }
+    },
+
+    isValidValue(value) {
+      if (!this.picker) {
+        this.mountPicker();
+      }
+      if (this.picker.isValidValue) {
+        return value && this.picker.isValidValue(value);
+      } else {
+        return true;
+      }
     }
   }
 };

+ 0 - 30
packages/date-picker/src/picker/date-picker.js

@@ -35,35 +35,5 @@ export default {
 
   created() {
     this.panel = getPanel(this.type);
-  },
-
-  methods: {
-    handleKeydown(event) {
-      const keyCode = event.keyCode;
-
-      // TAB or ESC or Enter
-      if (keyCode === 9 || keyCode === 27 || keyCode === 13) {
-        !this.ranged && (this.pickerVisible = false);
-        event.stopPropagation();
-        this.picker.confirm && this.picker.confirm();
-        !this.ranged && (this.currentValue = this.picker.date);
-        if (this.$refs.reference.$refs) {
-          this.$refs.reference.$refs.input.blur();
-        } else {
-          [].slice.call(this.$refs.reference.querySelectorAll('input')).forEach(input => {
-            input.blur();
-          });
-        }
-        return;
-      }
-
-      const list = [38, 40, 37, 39];
-      if (list.indexOf(keyCode) !== -1) {
-        if (this.type === 'daterange' || this.type === 'datetimerange') return;
-        this.picker.handleKeyControl(keyCode);
-        event.stopPropagation();
-        return;
-      }
-    }
   }
 };

+ 0 - 44
packages/date-picker/src/picker/time-picker.js

@@ -34,49 +34,5 @@ export default {
   created() {
     this.type = this.isRange ? 'timerange' : 'time';
     this.panel = this.isRange ? TimeRangePanel : TimePanel;
-  },
-
-  methods: {
-    handleKeydown(event) {
-      const keyCode = event.keyCode;
-
-      // TAB or ESC
-      if (keyCode === 9 || keyCode === 27) {
-        this.pickerVisible = false;
-        event.stopPropagation();
-        return;
-      }
-
-      const mapping = { 38: -1, 40: 1, 37: -1, 39: 1 };
-
-      // Left or Right
-      if (keyCode === 37 || keyCode === 39) {
-        const step = mapping[keyCode];
-        this.picker.changeSelectionRange(step);
-        event.preventDefault();
-        return;
-      }
-
-      // Up or Down
-      if (keyCode === 38 || keyCode === 40) {
-        const step = mapping[keyCode];
-        this.picker.scrollDown(step);
-        event.preventDefault();
-        return;
-      }
-
-      if (keyCode === 13) {
-        !this.isRange && this.picker.handleConfirm();
-        if (this.$refs.reference.$refs) {
-          this.$refs.reference.$refs.input.blur();
-        } else {
-          [].slice.call(this.$refs.reference.querySelectorAll('input')).forEach(input => {
-            input.blur();
-          });
-        }
-        event.preventDefault();
-        return;
-      }
-    }
   }
 };

+ 0 - 30
packages/date-picker/src/picker/time-select.js

@@ -9,35 +9,5 @@ export default {
   beforeCreate() {
     this.type = 'time-select';
     this.panel = Panel;
-  },
-
-  methods: {
-    handleKeydown(event) {
-      const keyCode = event.keyCode;
-      // TAB or ESC or Enter
-      if (keyCode === 9 || keyCode === 27 || keyCode === 13) {
-        const input = this.$refs.reference;
-        const index = this.picker.items.map(v => v.value).indexOf(input.currentValue);
-        const exist = index !== -1;
-        if (!exist) {
-          input.currentValue = this.currentValue;
-        } else {
-          this.picker.handleClick(this.picker.items[index]);
-        }
-        this.pickerVisible = false;
-        input.$refs.input.blur();
-        event.stopPropagation();
-        return;
-      }
-
-      if (keyCode === 38 || keyCode === 40) {
-        const mapping = { 40: 1, 38: -1 };
-        const offset = mapping[keyCode.toString()];
-        this.picker.scrollDown(offset);
-        this.currentValue = this.picker.value;
-        event.stopPropagation();
-        return;
-      }
-    }
   }
 };

+ 95 - 58
packages/date-picker/src/util/index.js

@@ -21,10 +21,6 @@ const newArray = function(start, end) {
   return result;
 };
 
-export const equalDate = function(dateA, dateB) {
-  return dateA === dateB || new Date(dateA).getTime() === new Date(dateB).getTime();
-};
-
 export const toDate = function(date) {
   return isDate(date) ? new Date(date) : null;
 };
@@ -93,44 +89,6 @@ export const getWeekNumber = function(src) {
   return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000 - 3 + (week1.getDay() + 6) % 7) / 7);
 };
 
-export const prevMonth = function(src) {
-  const year = src.getFullYear();
-  const month = src.getMonth();
-  const date = src.getDate();
-
-  const newYear = month === 0 ? year - 1 : year;
-  const newMonth = month === 0 ? 11 : month - 1;
-
-  const newMonthDayCount = getDayCountOfMonth(newYear, newMonth);
-  if (newMonthDayCount < date) {
-    src.setDate(newMonthDayCount);
-  }
-
-  src.setMonth(newMonth);
-  src.setFullYear(newYear);
-
-  return new Date(src.getTime());
-};
-
-export const nextMonth = function(src) {
-  const year = src.getFullYear();
-  const month = src.getMonth();
-  const date = src.getDate();
-
-  const newYear = month === 11 ? year + 1 : year;
-  const newMonth = month === 11 ? 0 : month + 1;
-
-  const newMonthDayCount = getDayCountOfMonth(newYear, newMonth);
-  if (newMonthDayCount < date) {
-    src.setDate(newMonthDayCount);
-  }
-
-  src.setMonth(newMonth);
-  src.setFullYear(newYear);
-
-  return new Date(src.getTime());
-};
-
 export const getRangeHours = function(ranges) {
   const hours = [];
   let disabledHours = [];
@@ -154,26 +112,105 @@ export const getRangeHours = function(ranges) {
   return hours;
 };
 
-export const limitRange = function(date, ranges, format = 'yyyy-MM-dd HH:mm:ss') {
-  if (!ranges || !ranges.length) return date;
+export const range = function(n) {
+  // see https://stackoverflow.com/questions/3746725/create-a-javascript-array-containing-1-n
+  return Array.apply(null, {length: n}).map((_, n) => n);
+};
 
-  const len = ranges.length;
+export const modifyDate = function(date, y, m, d) {
+  return new Date(y, m, d, date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds());
+};
 
-  date = dateUtil.parse(dateUtil.format(date, format), format);
-  for (let i = 0; i < len; i++) {
-    const range = ranges[i];
-    if (date >= range[0] && date <= range[1]) {
-      return date;
-    }
-  }
+export const modifyTime = function(date, h, m, s) {
+  return new Date(date.getFullYear(), date.getMonth(), date.getDate(), h, m, s, date.getMilliseconds());
+};
+
+export const clearTime = function(date) {
+  return new Date(date.getFullYear(), date.getMonth(), date.getDate());
+};
+
+export const clearMilliseconds = function(date) {
+  return new Date(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds(), 0);
+};
 
-  let maxDate = ranges[0][0];
-  let minDate = ranges[0][0];
+export const limitTimeRange = function(date, ranges, format = 'HH:mm:ss') {
+  // TODO: refactory a more elegant solution
+  if (ranges.length === 0) return date;
+  const normalizeDate = date => dateUtil.parse(dateUtil.format(date, format), format);
+  const ndate = normalizeDate(date);
+  const nranges = ranges.map(range => range.map(normalizeDate));
+  if (nranges.some(nrange => ndate >= nrange[0] && ndate <= nrange[1])) return date;
 
-  ranges.forEach(range => {
-    minDate = new Date(Math.min(range[0], minDate));
-    maxDate = new Date(Math.max(range[1], maxDate));
+  let minDate = nranges[0][0];
+  let maxDate = nranges[0][0];
+
+  nranges.forEach(nrange => {
+    minDate = new Date(Math.min(nrange[0], minDate));
+    maxDate = new Date(Math.max(nrange[1], minDate));
   });
 
-  return date < minDate ? minDate : maxDate;
+  const ret = ndate < minDate ? minDate : maxDate;
+  // preserve Year/Month/Date
+  return modifyDate(
+    ret,
+    date.getFullYear(),
+    date.getMonth(),
+    date.getDate()
+  );
+};
+
+export const timeWithinRange = function(date, selectableRange, format) {
+  const limitedDate = limitTimeRange(date, selectableRange, format);
+  return limitedDate.getTime() === date.getTime();
+};
+
+export const prevMonth = function(date) {
+  let year = date.getFullYear();
+  let month = date.getMonth();
+  if (month === 0) {
+    year -= 1;
+    month = 11;
+  } else {
+    month -= 1;
+  }
+  const monthDate = Math.min(date.getDate(), getDayCountOfMonth(year, month));
+  return modifyDate(date, year, month, monthDate);
+};
+
+export const nextMonth = function(date) {
+  let year = date.getFullYear();
+  let month = date.getMonth();
+  if (month === 11) {
+    year += 1;
+    month = 0;
+  } else {
+    month += 1;
+  }
+  const monthDate = Math.min(date.getDate(), getDayCountOfMonth(year, month));
+  return modifyDate(date, year, month, monthDate);
+};
+
+// check for leap year Feburary
+export const prevYear = function(date, amount = 1) {
+  const year = date.getFullYear() - amount;
+  const month = date.getMonth();
+  const monthDate = Math.min(date.getDate(), getDayCountOfMonth(year, month));
+  return modifyDate(date, year, month, monthDate);
+};
+
+export const nextYear = function(date, amount = 1) {
+  const year = date.getFullYear() + amount;
+  const month = date.getMonth();
+  const monthDate = Math.min(date.getDate(), getDayCountOfMonth(year, month));
+  return modifyDate(date, year, month, monthDate);
+};
+
+// {prev, next} Date works for daylight saving time
+// add / subtract one day's duration does not work
+export const prevDate = function(date, amount = 1) {
+  return new Date(date.getFullYear(), date.getMonth(), date.getDate() - amount);
+};
+
+export const nextDate = function(date, amount = 1) {
+  return new Date(date.getFullYear(), date.getMonth(), date.getDate() + amount);
 };

+ 398 - 73
test/unit/specs/date-picker.spec.js

@@ -6,7 +6,18 @@ import {
 } from '../util';
 import DatePicker from 'packages/date-picker';
 
-const DELAY = 10;
+const DELAY = 50;
+
+const LEFT = 37;
+const ENTER = 13;
+const TAB = 9;
+
+const keyDown = (el, keyCode) => {
+  const evt = document.createEvent('Events');
+  evt.initEvent('keydown', true, true);
+  evt.keyCode = keyCode;
+  el.dispatchEvent(evt);
+};
 
 describe('DatePicker', () => {
   let vm;
@@ -177,19 +188,18 @@ describe('DatePicker', () => {
   });
 
   it('change event', done => {
-    let inputValue;
+    let onChangeValue;
 
     vm = createVue({
       template: `
         <el-date-picker
           ref="compo"
           v-model="value"
-          format="yyyy-MM"
           @change="handleChange" />`,
 
       methods: {
         handleChange(val) {
-          inputValue = val;
+          onChangeValue = val;
         }
       },
 
@@ -208,15 +218,17 @@ describe('DatePicker', () => {
     setTimeout(_ => {
       const picker = vm.$refs.compo.picker;
 
-      picker.$el.querySelector('td.available').click();
-      vm.$nextTick(_ => {
-        const date = picker.date;
-        let month = date.getMonth() + 1;
-        if (month < 10) month = '0' + month;
-
-        expect(inputValue).to.equal(`${date.getFullYear()}-${ month }`);
-        done();
-      });
+      // programatic modification of bound value does not emit cange
+      vm.value = new Date(2000, 9, 2);
+      setTimeout(_ => {
+        expect(onChangeValue).to.not.exist;
+        // user interaction does emit change
+        picker.$el.querySelector('td.available').click();
+        setTimeout(_ => {
+          expect(onChangeValue.getTime()).to.equal(vm.value.getTime());
+          done();
+        }, DELAY);
+      }, DELAY);
     }, DELAY);
   });
 
@@ -356,44 +368,212 @@ describe('DatePicker', () => {
     });
   });
 
-  it('default value', done => {
-    let defaultValue = '2000-01-01';
-    let expectValue = new Date(2000, 0, 1);
+  describe('value-format', () => {
+    it('emits', done => {
+      vm = createVue({
+        template: `
+          <el-date-picker
+            ref="compo"
+            v-model="value"
+            type="date"
+            @change="handleChange"
+            value-format="dd-MM-yyyy" />`,
+        data() {
+          return {
+            value: '',
+            handleChange: null
+          };
+        }
+      }, true);
+      const spy = sinon.spy();
+      vm.handleChange = spy;
+      vm.$refs.compo.$el.querySelector('input').focus();
+      setTimeout(_ => {
+        vm.$refs.compo.picker.$el.querySelector('.el-date-table td.available').click();
+        setTimeout(_ => {
+          const today = new Date();
+          const yyyy = today.getFullYear();
+          const MM = ('0' + (today.getMonth() + 1)).slice(-2);
+          const dd = '01';   // first available one should be first day of month
+          const expectValue = `${dd}-${MM}-${yyyy}`;
+          expect(vm.value).to.equal(expectValue);
+          expect(spy.calledOnce).to.be.true;
+          expect(spy.calledWith(expectValue)).to.be.true;
+          done();
+        }, DELAY);
+      }, DELAY);
+    });
 
-    vm = createVue({
-      template: `<el-date-picker v-model="value" ref="compo" default-value="${defaultValue}" />`,
-      data() {
-        return {
-          value: ''
-        };
-      }
-    }, true);
+    it('accepts', done => {
+      vm = createVue({
+        template: `
+          <el-date-picker
+            ref="compo"
+            v-model="value"
+            type="date"
+            value-format="dd-MM-yyyy" />`,
+        data() {
+          return {
+            value: '01-02-2000'
+          };
+        }
+      }, true);
+      vm.$refs.compo.$el.querySelector('input').focus();
+      setTimeout(_ => {
+        const date = vm.$refs.compo.picker.date;
+        expect(date.getFullYear()).to.equal(2000);
+        expect(date.getMonth()).to.equal(1);
+        expect(date.getDate()).to.equal(1);
+        done();
+      }, DELAY);
+    });
 
-    const input = vm.$el.querySelector('input');
+    it('translates format to value-format', done => {
+      vm = createVue({
+        template: `
+          <el-date-picker
+            ref="compo"
+            v-model="value"
+            type="date"
+            value-format="dd-MM-yyyy"
+            format="yyyy-MM-dd" />`,
+        data() {
+          return {
+            value: ''
+          };
+        }
+      }, true);
+      const input = vm.$refs.compo.$el.querySelector('input');
+      input.focus();
+      setTimeout(_ => {
+        input.value = '2000-10-01';
+        triggerEvent(input, 'input');
+        keyDown(input, ENTER);
+        setTimeout(_ => {
+          expect(vm.value).to.equal('01-10-2000');
+          done();
+        }, DELAY);
+      }, DELAY);
+    });
 
-    input.focus();
-    setTimeout(_ => {
-      const $el = vm.$refs.compo.picker.$el;
-      $el.querySelector('td.current').click();
+    it('works for daterange', done => {
+      vm = createVue({
+        template: `
+          <el-date-picker
+            ref="compo"
+            v-model="value"
+            type="daterange"
+            value-format="dd-MM-yyyy" />`,
+        data() {
+          return {
+            value: ''
+          };
+        }
+      }, true);
+      const inputs = vm.$refs.compo.$el.querySelectorAll('input');
+      inputs[0].focus();
       setTimeout(_ => {
-        expect(+vm.value).to.equal(+expectValue);
-        done();
-      }, 10);
-    }, 10);
+        inputs[0].value = '2000-10-01';
+        triggerEvent(inputs[0], 'input');
+        keyDown(inputs[0], TAB);
+        setTimeout(_ => {
+          inputs[1].focus();
+          inputs[1].value = '2000-10-02';
+          triggerEvent(inputs[1], 'input');
+          keyDown(inputs[0], ENTER);
+          setTimeout(_ => {
+            expect(vm.value).to.eql(['01-10-2000', '02-10-2000']);
+            done();
+          }, DELAY);
+        }, DELAY);
+      }, DELAY);
+    });
   });
 
-  describe('keydown', () => {
-    let input;
-    let keyDown = function(el, keyCode) {
-      const evt = document.createEvent('Events');
+  describe('default value', done => {
+    it('it works', done => {
+      let defaultValue = '2000-01-01';
+      let expectValue = new Date(2000, 0, 1);
 
-      evt.initEvent('keydown', true, true);
-      evt.keyCode = keyCode;
-      el.dispatchEvent(evt);
-    };
+      vm = createVue({
+        template: `<el-date-picker v-model="value" ref="compo" default-value="${defaultValue}" />`,
+        data() {
+          return {
+            value: ''
+          };
+        }
+      }, true);
+
+      vm.$el.querySelector('input').focus();
+      setTimeout(_ => {
+        const $el = vm.$refs.compo.picker.$el;
+        expect($el.querySelector('.el-month-table td.default')).to.exist;
+        expect($el.querySelector('.el-year-table td.default')).to.exist;
+        const defaultEls = $el.querySelectorAll('.el-date-table td.default');
+        expect(defaultEls.length).to.equal(1);
+        defaultEls[0].click();
+        setTimeout(_ => {
+          expect(vm.value).to.eql(expectValue);
+          done();
+        }, DELAY);
+      }, DELAY);
+    });
+
+    it('is reactive, works with clear', done => {
+      let defaultValue = '2000-01-01';
+      let expectValue = new Date(2000, 0, 1);
+
+      vm = createVue({
+        template: '<el-date-picker v-model="value" ref="compo" :default-value="defaultValue" />',
+        data() {
+          return {
+            value: new Date(),
+            defaultValue: null
+          };
+        }
+      }, true);
+
+      const input = vm.$el.querySelector('input');
+      input.focus();
+      setTimeout(_ => {
+        let $el = vm.$refs.compo.picker.$el;
+        expect($el.querySelector('.el-date-table td.current')).to.exist;
+        vm.defaultValue = defaultValue;
+        triggerEvent(vm.$refs.compo.$el, 'mouseenter');
+        setTimeout(_ => {
+          vm.$el.querySelector('.el-icon-circle-close').click();
+          setTimeout(_ => {
+            input.focus();
+            setTimeout(() => {
+              $el = vm.$refs.compo.picker.$el;
+              expect($el.querySelector('.el-date-table td.current')).to.not.exist;
+              expect($el.querySelector('.el-date-table td.default')).to.exist;
+              $el.querySelector('.el-date-table td.default').click();
+              setTimeout(() => {
+                expect(vm.value).to.eql(expectValue);
+                done();
+              }, DELAY);
+            }, DELAY);
+          }, DELAY);
+        }, DELAY);
+      }, DELAY);
+    });
+
+  });
+
+  describe('keydown', () => {
+    let input, datePicker;
 
     beforeEach(done => {
-      vm = createTest(DatePicker, true);
+      vm = createVue({
+        template: '<el-date-picker ref="compo" v-model="value"></el-date-picker>',
+        data() {
+          return {
+            value: ''
+          };
+        }
+      }, true);
+      datePicker = vm.$refs.compo;
       input = vm.$el.querySelector('input');
       input.blur();
       input.focus();
@@ -402,37 +582,131 @@ describe('DatePicker', () => {
 
     afterEach(() => destroyVM(vm));
 
-    it('tab', () => {
-      keyDown(input, 9);
-      expect(vm.pickerVisible).to.false;
+    it('tab', done => {
+      keyDown(input, TAB);
+      setTimeout(_ => {
+        expect(datePicker.pickerVisible).to.false;
+        done();
+      }, DELAY);
     });
 
     it('enter', done => {
       input.value = '2000-10-1';
-      triggerEvent(input, 'change', true);
-      setTimeout(_ => {
-        expect(vm.pickerVisible).to.false;
-        expect(vm.picker.date.getFullYear()).to.equal(2000);
-        expect(vm.picker.date.getMonth()).to.equal(9);
-        expect(vm.picker.date.getDate()).to.equal(1);
+      triggerEvent(input, 'input');
+      vm.$nextTick(_ => {
+        keyDown(input, ENTER);
+        setTimeout(_ => {
+          expect(datePicker.pickerVisible).to.false;
+          expect(datePicker.picker.date.getFullYear()).to.equal(2000);
+          expect(datePicker.picker.date.getMonth()).to.equal(9);
+          expect(datePicker.picker.date.getDate()).to.equal(1);
+          done();
+        }, DELAY);
+      });
+    });
+
+    it('arrow keys during typing does not navigate', done => {
+      const inputText = '2000-10-1';
+      input.value = inputText;
+      triggerEvent(input, 'input');
+      keyDown(input, LEFT);
+      vm.$nextTick(_ => {
+        expect(input.value).to.equal(inputText);
         done();
+      });
+    });
+
+    it('arrow keys navigates', done => {
+      const date = new Date(2000, 9, 1);
+      const prevDate = new Date(2000, 9, 0);
+      vm.value = date;
+      vm.$nextTick(_ => {
+        keyDown(input, LEFT);
+        setTimeout(_ => {
+          expect(datePicker.pickerVisible).to.true;
+          expect(datePicker.picker.date.getFullYear()).to.equal(prevDate.getFullYear());
+          expect(datePicker.picker.date.getMonth()).to.equal(prevDate.getMonth());
+          expect(datePicker.picker.date.getDate()).to.equal(prevDate.getDate());
+          done();
+        }, DELAY);
+      });
+    });
+  });
+
+  describe('nagivation', _ => {
+    const click = (el, cbk = () => {}) => {
+      el.click();
+      setTimeout(cbk, DELAY);
+    };
+
+    let prevMonth, prevYear, nextMonth, nextYear, getYearLabel, getMonthLabel;
+
+    const navigationTest = (value, cbk) => {
+      vm = createVue({
+        template: '<el-date-picker v-model="value" ref="compo" />',
+        data() {
+          return {
+            value
+          };
+        }
+      }, true);
+      vm.$refs.compo.$el.querySelector('input').focus();
+      setTimeout(_ => {
+        const $el = vm.$refs.compo.picker.$el;
+        prevMonth = $el.querySelector('button.el-icon-arrow-left');
+        prevYear = $el.querySelector('button.el-icon-d-arrow-left');
+        nextMonth = $el.querySelector('button.el-icon-arrow-right');
+        nextYear = $el.querySelector('button.el-icon-d-arrow-right');
+        getYearLabel = () => $el.querySelectorAll('.el-date-picker__header-label')[0].textContent;
+        getMonthLabel = () => $el.querySelectorAll('.el-date-picker__header-label')[1].textContent;
+        cbk();
       }, DELAY);
+    };
+
+    it('month, year', done => {
+      navigationTest(new Date(2000, 0, 1), _ => {
+        expect(getYearLabel()).to.include('2000');
+        expect(getMonthLabel()).to.include('1');
+        click(prevMonth, _ => {
+          expect(getYearLabel()).to.include('1999');
+          expect(getMonthLabel()).to.include('12');
+          click(prevYear, _ => {
+            expect(getYearLabel()).to.include('1998');
+            expect(getMonthLabel()).to.include('12');
+            click(nextMonth, _ => {
+              expect(getYearLabel()).to.include('1999');
+              expect(getMonthLabel()).to.include('1');
+              click(nextYear, _ => {
+                expect(getYearLabel()).to.include('2000');
+                expect(getMonthLabel()).to.include('1');
+                done();
+              });
+            });
+          });
+        });
+      });
     });
 
-    it('left', () => {
-      input.value = '2000-10-1';
-      keyDown(input, 13);
-      input.focus();
-      keyDown(input, 37);
-      expect(input.selectionStart > 0).to.true;
+    it('month with fewer dates', done => {
+      // July has 31 days, June has 30
+      navigationTest(new Date(2000, 6, 31), _ => {
+        click(prevMonth, _ => {
+          expect(getYearLabel()).to.include('2000');
+          expect(getMonthLabel()).to.include('6');
+          done();
+        });
+      });
     });
 
-    it('right', () => {
-      input.value = '2000-10-1';
-      keyDown(input, 13);
-      input.focus();
-      keyDown(input, 39);
-      expect(input.selectionStart > 0).to.true;
+    it('year with fewer Feburary dates', done => {
+      // Feburary 2008 has 29 days, Feburary 2007 has 28
+      navigationTest(new Date(2008, 1, 29), _ => {
+        click(prevYear, _ => {
+          expect(getYearLabel()).to.include('2007');
+          expect(getMonthLabel()).to.include('2');
+          done();
+        });
+      });
     });
   });
 
@@ -567,6 +841,67 @@ describe('DatePicker', () => {
         }, DELAY);
       }, DELAY);
     });
+
+    describe('default value', () => {
+      it('single', done => {
+        let defaultValue = '2000-10-01';
+        let expectValue = [new Date(2000, 9, 1), new Date(2000, 9, 2)];
+
+        vm = createVue({
+          template: '<el-date-picker type="daterange" v-model="value" ref="compo" :default-value="defaultValue" />',
+          data() {
+            return {
+              value: '',
+              defaultValue
+            };
+          }
+        }, true);
+
+        vm.$el.querySelector('input').focus();
+        setTimeout(_ => {
+          const $el = vm.$refs.compo.picker.$el;
+          const defaultEls = $el.querySelectorAll('.el-date-table td.default');
+          expect(defaultEls.length).to.equal(1);
+          defaultEls[0].click();
+          setTimeout(_ => {
+            $el.querySelector('.el-date-table td.default + td').click();
+            setTimeout(_ => {
+              expect(vm.value).to.eql(expectValue);
+              done();
+            }, DELAY);
+          }, DELAY);
+        }, DELAY);
+      });
+
+      it('array', done => {
+        let defaultValue = ['2000-01-01', '2000-02-01'];
+        let expectValue = [new Date(2000, 0, 1), new Date(2000, 1, 1)];
+
+        vm = createVue({
+          template: '<el-date-picker type="daterange" v-model="value" ref="compo" :default-value="defaultValue" />',
+          data() {
+            return {
+              value: '',
+              defaultValue
+            };
+          }
+        }, true);
+
+        vm.$el.querySelector('input').focus();
+        setTimeout(_ => {
+          const defaultEls = vm.$refs.compo.picker.$el.querySelectorAll('.el-date-table td.default');
+          expect(defaultEls.length).to.equal(2);
+          defaultEls[0].click();
+          setTimeout(_ => {
+            defaultEls[1].click();
+            setTimeout(_ => {
+              expect(vm.value).to.eql(expectValue);
+              done();
+            }, DELAY);
+          }, DELAY);
+        }, DELAY);
+      });
+    });
   });
 
   describe('type:week', () => {
@@ -909,16 +1244,6 @@ describe('DatePicker', () => {
       expect(vm.picker.$el.querySelector('.disabled')).to.be.ok;
     });
 
-    it('set disabled value', done => {
-      const date = new Date(1999, 10, 10, 10, 10, 10);
-      vm.picker.value = date;
-
-      setTimeout(_ => {
-        expect(vm.picker.date > date).to.true;
-        done();
-      }, DELAY);
-    });
-
     it('set value', done => {
       const date = new Date(3000, 10, 10, 10, 10, 10);
       vm.picker.value = date;

+ 49 - 29
test/unit/specs/form.spec.js

@@ -1,5 +1,7 @@
 import { createVue, destroyVM } from '../util';
 
+const DELAY = 50;
+
 describe('Form', () => {
   let vm;
   afterEach(() => {
@@ -362,7 +364,7 @@ describe('Form', () => {
         template: `
           <el-form :model="form" :rules="rules" ref="form">
             <el-form-item label="记住密码" prop="date" ref="field">
-              <el-date-picker type="date" placeholder="选择日期" v-model="form.date" style="width: 100%;"></el-date-picker>
+              <el-date-picker type="date" ref="picker" placeholder="选择日期" v-model="form.date" style="width: 100%;"></el-date-picker>
             </el-form-item>
           </el-form>
         `,
@@ -377,26 +379,38 @@ describe('Form', () => {
               ]
             }
           };
-        },
-        methods: {
-          setValue(value) {
-            this.form.date = value;
-          }
         }
       }, true);
       vm.$refs.form.validate(valid => {
         let field = vm.$refs.field;
         expect(valid).to.not.true;
-        vm.$refs.form.$nextTick(_ => {
+        setTimeout(_ => {
           expect(field.validateMessage).to.equal('请选择日期');
-
-          vm.setValue(new Date());
-
-          vm.$refs.form.$nextTick(_ => {
-            expect(field.validateMessage).to.equal('');
-            done();
-          });
-        });
+          // programatic modification does not trigger change
+          vm.value = new Date();
+          setTimeout(_ => {
+            expect(field.validateMessage).to.equal('请选择日期');
+            vm.value = '';
+            // user modification triggers change
+            const input = vm.$refs.picker.$el.querySelector('input');
+            input.blur();
+            input.focus();
+            setTimeout(_ => {
+              const keyDown = (el, keyCode) => {
+                const evt = document.createEvent('Events');
+                evt.initEvent('keydown', true, true);
+                evt.keyCode = keyCode;
+                el.dispatchEvent(evt);
+              };
+              keyDown(input, 37);
+              keyDown(input, 13);
+              setTimeout(_ => {
+                expect(field.validateMessage).to.equal('');
+                done();
+              }, DELAY);
+            }, DELAY);
+          }, DELAY);
+        }, DELAY);
       });
     });
     it('timepicker', done => {
@@ -404,7 +418,7 @@ describe('Form', () => {
         template: `
           <el-form :model="form" :rules="rules" ref="form">
             <el-form-item label="记住密码" prop="date" ref="field">
-              <el-time-picker type="fixed-time" placeholder="选择时间" v-model="form.date" style="width: 100%;"></el-time-picker>
+              <el-time-picker type="fixed-time" ref="picker" placeholder="选择时间" v-model="form.date" style="width: 100%;"></el-time-picker>
             </el-form-item>
           </el-form>
         `,
@@ -419,25 +433,31 @@ describe('Form', () => {
               ]
             }
           };
-        },
-        methods: {
-          setValue(value) {
-            this.form.date = value;
-          }
         }
       }, true);
       vm.$refs.form.validate(valid => {
         let field = vm.$refs.field;
         expect(valid).to.not.true;
-        vm.$refs.form.$nextTick(_ => {
+        setTimeout(_ => {
           expect(field.validateMessage).to.equal('请选择时间');
-          vm.setValue(new Date());
-
-          vm.$refs.form.$nextTick(_ => {
-            expect(field.validateMessage).to.equal('');
-            done();
-          });
-        });
+          // programatic modification does not trigger change
+          vm.value = new Date();
+          setTimeout(_ => {
+            expect(field.validateMessage).to.equal('请选择时间');
+            vm.value = '';
+            // user modification triggers change
+            const input = vm.$refs.picker.$el.querySelector('input');
+            input.blur();
+            input.focus();
+            setTimeout(_ => {
+              vm.$refs.picker.picker.$el.querySelector('.confirm').click();
+              setTimeout(_ => {
+                expect(field.validateMessage).to.equal('');
+                done();
+              }, DELAY);
+            }, DELAY);
+          }, DELAY);
+        }, DELAY);
       });
     });
     it('checkbox group', done => {

+ 115 - 75
test/unit/specs/time-picker.spec.js

@@ -1,6 +1,7 @@
 import { createTest, destroyVM, createVue } from '../util';
 import TimePicker from 'packages/time-picker';
-import Vue from 'vue';
+
+const DELAY = 100;
 
 describe('TimePicker', () => {
   let vm;
@@ -15,7 +16,6 @@ describe('TimePicker', () => {
     });
     expect(vm.$el.querySelector('input').getAttribute('placeholder')).to.equal('test');
     expect(vm.$el.querySelector('input').getAttribute('readonly')).to.ok;
-    destroyVM(vm);
   });
 
   it('format', () => {
@@ -24,7 +24,6 @@ describe('TimePicker', () => {
       value: new Date(2016, 9, 10, 18, 40)
     });
     expect(vm.$el.querySelector('input').value).to.equal('18-40-00');
-    destroyVM(vm);
   });
 
   it('default value', done => {
@@ -44,20 +43,28 @@ describe('TimePicker', () => {
       expect(times[0].textContent).to.equal('18');
       expect(times[1].textContent).to.equal('40');
       expect(times[2].textContent).to.equal('00');
-      destroyVM(vm);
       done();
-    }, 100);
+    }, DELAY);
   });
 
   it('select time', done => {
-    vm = createTest(TimePicker, true);
-    const input = vm.$el.querySelector('input');
+    vm = createVue({
+      template: '<el-time-picker ref="compo" v-model="value"></el-time-picker>',
+      data() {
+        return {
+          value: ''
+        };
+      }
+    }, true);
+    const timePicker = vm.$refs.compo;
+    const input = timePicker.$el.querySelector('input');
 
     input.blur();
     input.focus();
 
-    Vue.nextTick(_ => {
-      const list = vm.picker.$el.querySelectorAll('.el-time-spinner__list');
+    setTimeout(_ => {
+      const list = timePicker.picker.$el.querySelectorAll('.el-time-spinner__list');
+
       const hoursEl = list[0];
       const minutesEl = list[1];
       const secondsEl = list[2];
@@ -65,57 +72,75 @@ describe('TimePicker', () => {
       const minuteEl = minutesEl.querySelectorAll('.el-time-spinner__item')[36];
       const secondEl = secondsEl.querySelectorAll('.el-time-spinner__item')[20];
 
+      // click hour, minute, second one at a time.
       hourEl.click();
-      minuteEl.click();
-      secondEl.click();
-
-      Vue.nextTick(_ => {
-        const date = vm.picker.currentDate;
-
-        expect(hourEl.classList.contains('active')).to.true;
-        expect(minuteEl.classList.contains('active')).to.true;
-        expect(secondEl.classList.contains('active')).to.true;
-        expect(date.getHours()).to.equal(4);
-        expect(date.getMinutes()).to.equal(36);
-        expect(date.getSeconds()).to.equal(20);
-        destroyVM(vm);
-        done();
+      vm.$nextTick(_ => {
+        minuteEl.click();
+        vm.$nextTick(_ => {
+          secondEl.click();
+          setTimeout(_ => {
+            const date = timePicker.picker.date;
+            expect(hourEl.classList.contains('active')).to.true;
+            expect(minuteEl.classList.contains('active')).to.true;
+            expect(secondEl.classList.contains('active')).to.true;
+            expect(date.getHours()).to.equal(4);
+            expect(date.getMinutes()).to.equal(36);
+            expect(date.getSeconds()).to.equal(20);
+            done();
+          }, DELAY);
+        });
       });
-    });
+    }, DELAY);
   });
 
   it('click cancel button', done => {
-    vm = createTest(TimePicker, true);
-    const input = vm.$el.querySelector('input');
+    vm = createVue({
+      template: '<el-time-picker ref="compo" v-model="value"></el-time-picker>',
+      data() {
+        return {
+          value: ''
+        };
+      }
+    }, true);
+    const timePicker = vm.$refs.compo;
+    const input = timePicker.$el.querySelector('input');
 
     input.blur();
     input.focus();
 
-    Vue.nextTick(_ => {
-      vm.picker.$el.querySelector('.el-time-panel__btn.cancel').click();
+    setTimeout(_ => {
+      timePicker.picker.$el.querySelector('.el-time-panel__btn.cancel').click();
 
-      Vue.nextTick(_ => {
-        expect(vm.picker.currentDate).to.empty;
+      setTimeout(_ => {
+        expect(vm.value).to.equal('');
         done();
-      });
-    });
+      }, DELAY);
+    }, DELAY);
   });
 
   it('click confirm button', done => {
-    vm = createTest(TimePicker, true);
-    const input = vm.$el.querySelector('input');
+    vm = createVue({
+      template: '<el-time-picker ref="compo" v-model="value"></el-time-picker>',
+      data() {
+        return {
+          value: ''
+        };
+      }
+    }, true);
+    const timePicker = vm.$refs.compo;
+    const input = timePicker.$el.querySelector('input');
 
     input.blur();
     input.focus();
 
-    Vue.nextTick(_ => {
-      vm.picker.$el.querySelector('.el-time-panel__btn.confirm').click();
+    setTimeout(_ => {
+      timePicker.picker.$el.querySelector('.el-time-panel__btn.confirm').click();
 
-      Vue.nextTick(_ => {
-        expect(vm.picker.currentDate).to.exist;
+      setTimeout(_ => {
+        expect(vm.value.toISOString()).to.exist;
         done();
-      });
-    });
+      }, DELAY);
+    }, DELAY);
   });
 
   it('set format', done => {
@@ -130,7 +155,6 @@ describe('TimePicker', () => {
 
     setTimeout(_ => {
       expect(vm.picker.$el.querySelectorAll('.el-time-spinner__wrapper')[2].style.display).to.equal('none');
-      destroyVM(vm);
       done();
     }, 20);
   });
@@ -147,9 +171,8 @@ describe('TimePicker', () => {
 
     setTimeout(_ => {
       expect(vm.picker.$el.querySelectorAll('.el-time-spinner__wrapper')[2].style.display).to.equal('none');
-      destroyVM(vm);
       done();
-    }, 20);
+    }, DELAY);
   });
 
   it('selectableRange', done => {
@@ -172,9 +195,8 @@ describe('TimePicker', () => {
 
       hoursEl.querySelectorAll('.disabled')[0].click();
       expect(disabledHours).to.not.include.members([18, 19, 20]);
-      destroyVM(vm);
       done();
-    }, 20);
+    }, DELAY);
   });
 
   it('event focus and blur', done => {
@@ -229,48 +251,66 @@ describe('TimePicker', () => {
 
 describe('TimePicker(range)', () => {
   let vm;
-  beforeEach(done => {
+
+  afterEach(() => destroyVM(vm));
+
+  it('create', done => {
     vm = createTest(TimePicker, {
       isRange: true,
       value: [new Date(2016, 9, 10, 18, 40), new Date(2016, 9, 10, 19, 40)]
     }, true);
-    const input = vm.$el.querySelector('input');
-
-    input.click();
-    setTimeout(done, 20);
-  });
-
-  afterEach(() => destroyVM(vm));
 
-  it('create', () => {
-    expect(vm.picker.$el.querySelectorAll('.el-time-range-picker__cell')).to.length(2);
-  });
+    vm.$el.querySelector('input').click();
 
-  it('default value', () => {
-    expect(vm.picker.minTime.getTime()).to.equal(new Date(2016, 9, 10, 18, 40).getTime());
-    expect(vm.picker.maxTime.getTime()).to.equal(new Date(2016, 9, 10, 19, 40).getTime());
+    setTimeout(_ => {
+      expect(vm.picker.$el.querySelectorAll('.el-time-range-picker__cell')).to.length(2);
+      expect(vm.picker.minDate.getTime()).to.equal(new Date(2016, 9, 10, 18, 40).getTime());
+      expect(vm.picker.maxDate.getTime()).to.equal(new Date(2016, 9, 10, 19, 40).getTime());
+      done();
+    }, DELAY);
   });
 
-  it('minTime < maxTime', done => {
-    const vm2 = createTest(TimePicker, {
-      isRange: true,
-      value: [new Date(2016, 9, 10, 23, 40), new Date(2016, 9, 10, 10, 40)]
+  it('default value', done => {
+    const defaultValue = [new Date(2000, 9, 1, 10, 0, 0), new Date(2000, 9, 1, 11, 0, 0)];
+    vm = createVue({
+      template: '<el-time-picker ref="compo" is-range v-model="value" :default-value="defaultValue"></el-time-picker>',
+      data() {
+        return {
+          value: '',
+          defaultValue
+        };
+      }
     }, true);
-    const input = vm2.$el.querySelector('input');
 
-    input.click();
-    setTimeout(() => {
-      expect(vm2.picker.maxTime >= vm2.picker.minTime).to.true;
-      destroyVM(vm2);
+    const timePicker = vm.$refs.compo;
+    timePicker.$el.querySelector('input').click();
+
+    setTimeout(_ => {
+      expect(timePicker.picker.minDate.getTime()).to.equal(defaultValue[0].getTime());
+      expect(timePicker.picker.maxDate.getTime()).to.equal(defaultValue[1].getTime());
       done();
-    }, 100);
+    }, DELAY);
   });
 
-  it('click cancel button', done => {
-    vm.picker.$el.querySelector('.el-time-panel__btn.cancel').click();
-    Vue.nextTick(_ => {
-      expect(vm.picker.currentDate).to.empty;
-      done();
-    });
+  it('cancel button', done => {
+    vm = createVue({
+      template: '<el-time-picker ref="compo" is-range v-model="value"></el-time-picker>',
+      data() {
+        return {
+          value: ''
+        };
+      }
+    }, true);
+
+    const timePicker = vm.$refs.compo;
+    timePicker.$el.querySelector('input').click();
+    setTimeout(_ => {
+      timePicker.picker.$el.querySelector('.cancel').click();
+      setTimeout(_ => {
+        expect(timePicker.picker.visible).to.false;
+        expect(vm.value).to.equal('');
+        done();
+      }, DELAY);
+    }, DELAY);
   });
 });

+ 2 - 10
test/unit/specs/time-select.spec.js

@@ -27,7 +27,6 @@ describe('TimeSelect', () => {
       expect(vm.picker.end).to.equal('18:30');
       expect(vm.picker.step).to.equal('00:15');
       expect(vm.$el.querySelector('input').getAttribute('placeholder')).to.equal('test');
-      destroyVM(vm);
       done();
     });
   });
@@ -60,7 +59,6 @@ describe('TimeSelect', () => {
       target.click();
       Vue.nextTick(_ => {
         expect(vm.value).to.equal(time);
-        destroyVM(vm);
         done();
       });
     });
@@ -79,7 +77,6 @@ describe('TimeSelect', () => {
       expect(input.value).to.equal('14:30');
       expect(vm.picker.$el.querySelector('.selected')).to.be.ok;
       expect(vm.picker.$el.querySelector('.selected').textContent).to.equal('14:30');
-      destroyVM(vm);
       done();
     }, 50);
   });
@@ -104,7 +101,6 @@ describe('TimeSelect', () => {
       const elm = elms[elms.length - 1];
 
       expect(elm.textContent).to.equal('14:30');
-      destroyVM(vm);
       done();
     }, 50);
   });
@@ -134,8 +130,7 @@ describe('TimeSelect', () => {
       vm.value = '10:30';
 
       setTimeout(_ => {
-        expect(picker.picker.value).to.equal('09:30');
-        destroyVM(vm);
+        expect(picker.picker.value).to.equal('10:30');
         done();
       }, 50);
     }, 50);
@@ -161,7 +156,6 @@ describe('TimeSelect', () => {
       const elm = picker.picker.$el.querySelector('.disabled');
 
       expect(elm.textContent).to.equal('14:30');
-      destroyVM(vm);
       done();
     }, 50);
   });
@@ -191,8 +185,7 @@ describe('TimeSelect', () => {
       vm.value = '10:30';
 
       setTimeout(_ => {
-        expect(picker.picker.value).to.equal('09:30');
-        destroyVM(vm);
+        expect(picker.picker.value).to.equal('10:30');
         done();
       }, 50);
     }, 50);
@@ -242,7 +235,6 @@ describe('TimeSelect', () => {
 
     vm.$nextTick(_ => {
       expect(spy.calledOnce).to.be.true;
-      destroyVM(vm);
       done();
     });
   });