Commit bdb26ef7c80f44e30ebdc33de2ddd60a3cc19c1f

Authored by Aresn
Committed by GitHub
2 parents 3dc7ac4f cdc79b9a

Merge pull request #3643 from SergioCrisostomo/datepicker-keyboard

Datepicker keyboard
examples/routers/date.vue
... ... @@ -249,14 +249,17 @@
249 249  
250 250 <template>
251 251 <div style="width: 500px;margin: 100px;">
252   - <Row>
253   - <Col span="12">
254   - <DatePicker type="date" show-week-numbers placeholder="Select date" style="width: 200px"></DatePicker>
255   - </Col>
256   - <Col span="12">
257   - <DatePicker type="daterange" show-week-numbers placement="bottom-end" placeholder="Select date" style="width: 200px"></DatePicker>
258   - </Col>
259   - </Row>
  252 + <p><input type="text"></p>
  253 +
  254 + <DatePicker type="month" show-week-numbers placeholder="Select date" style="width: 200px"></DatePicker>
  255 + <DatePicker type="year" show-week-numbers placeholder="Select date" style="width: 200px"></DatePicker>
  256 +
  257 + <DatePicker type="date" transfer show-week-numbers placeholder="Select date" style="width: 400px"></DatePicker>
  258 + <DatePicker type="datetime" show-week-numbers confirm placeholder="Select date" style="width: 400px"></DatePicker>
  259 +
  260 + <DatePicker type="daterange" transfer show-week-numbers placeholder="Select date" style="width: 400px"></DatePicker>
  261 + <DatePicker type="datetimerange" transfer show-week-numbers placeholder="Select date" style="width: 400px"></DatePicker>
  262 + <Time-Picker :steps="[1, 1, 15]" :value="new Date()"></Time-Picker>
260 263 </div>
261 264 </template>
262 265 <script>
... ...
src/components/date-picker/base/confirm.vue
1 1 <template>
2   - <div :class="[prefixCls + '-confirm']">
3   - <span :class="timeClasses" v-if="showTime" @click="handleToggleTime">
4   - <template v-if="isTime">{{ t('i.datepicker.selectDate') }}</template>
5   - <template v-else>{{ t('i.datepicker.selectTime') }}</template>
6   - </span>
7   - <i-button size="small" type="text" @click.native="handleClear">{{ t('i.datepicker.clear') }}</i-button>
8   - <i-button size="small" type="primary" @click.native="handleSuccess">{{ t('i.datepicker.ok') }}</i-button>
  2 + <div :class="[prefixCls + '-confirm']" @keydown.tab.capture="handleTab">
  3 + <i-button :class="timeClasses" size="small" type="text" :disabled="timeDisabled" v-if="showTime" @click="handleToggleTime">
  4 + {{labels.time}}
  5 + </i-button>
  6 + <i-button size="small" type="ghost" @click.native="handleClear" @keydown.enter.native="handleClear">
  7 + {{labels.clear}}
  8 + </i-button>
  9 + <i-button size="small" type="primary" @click.native="handleSuccess" @keydown.enter.native="handleSuccess">
  10 + {{labels.ok}}
  11 + </i-button>
9 12 </div>
10 13 </template>
11 14 <script>
12 15 import iButton from '../../button/button.vue';
13 16 import Locale from '../../../mixins/locale';
  17 + import Emitter from '../../../mixins/emitter';
14 18  
15 19 const prefixCls = 'ivu-picker';
16 20  
17 21 export default {
18   - mixins: [ Locale ],
19   - components: { iButton },
  22 + mixins: [Locale, Emitter],
  23 + components: {iButton},
20 24 props: {
21 25 showTime: false,
22 26 isTime: false,
23 27 timeDisabled: false
24 28 },
25   - data () {
  29 + data() {
26 30 return {
27 31 prefixCls: prefixCls
28 32 };
29 33 },
30 34 computed: {
31 35 timeClasses () {
32   - return {
33   - [`${prefixCls}-confirm-time-disabled`]: this.timeDisabled
34   - };
  36 + return `${prefixCls}-confirm-time`;
  37 + },
  38 + labels(){
  39 + const labels = ['time', 'clear', 'ok'];
  40 + const values = [(this.isTime ? 'selectDate' : 'selectTime'), 'clear', 'ok'];
  41 + return labels.reduce((obj, key, i) => {
  42 + obj[key] = this.t('i.datepicker.' + values[i]);
  43 + return obj;
  44 + }, {});
35 45 }
36 46 },
37 47 methods: {
... ... @@ -44,6 +54,17 @@
44 54 handleToggleTime () {
45 55 if (this.timeDisabled) return;
46 56 this.$emit('on-pick-toggle-time');
  57 + this.dispatch('CalendarPicker', 'focus-input');
  58 + },
  59 + handleTab(e) {
  60 + const tabbables = [...this.$el.children];
  61 + const expectedFocus = tabbables[e.shiftKey ? 'shift' : 'pop']();
  62 +
  63 + if (document.activeElement === expectedFocus) {
  64 + e.preventDefault();
  65 + e.stopPropagation();
  66 + this.dispatch('CalendarPicker', 'focus-input');
  67 + }
47 68 }
48 69 }
49 70 };
... ...
src/components/date-picker/base/date-table.vue
... ... @@ -7,9 +7,9 @@
7 7 </div>
8 8 <span
9 9 :class="getCellCls(cell)"
10   - v-for="(cell, i) in readCells"
  10 + v-for="(cell, i) in cells"
11 11 :key="String(cell.date) + i"
12   - @click="handleClick(cell)"
  12 + @click="handleClick(cell, $event)"
13 13 @mouseenter="handleMouseMove(cell)"
14 14 >
15 15 <em>{{ cell.desc }}</em>
... ... @@ -61,7 +61,7 @@
61 61 const weekDays = translatedDays.splice(weekStartDay, 7 - weekStartDay).concat(translatedDays.splice(0, weekStartDay));
62 62 return this.showWeekNumbers ? [''].concat(weekDays) : weekDays;
63 63 },
64   - readCells () {
  64 + cells () {
65 65 const tableYear = this.tableDate.getFullYear();
66 66 const tableMonth = this.tableDate.getMonth();
67 67 const today = clearHours(new Date()); // timestamp of today
... ... @@ -99,7 +99,9 @@
99 99 [`${prefixCls}-cell-prev-month`]: cell.type === 'prevMonth',
100 100 [`${prefixCls}-cell-next-month`]: cell.type === 'nextMonth',
101 101 [`${prefixCls}-cell-week-label`]: cell.type === 'weekLabel',
102   - [`${prefixCls}-cell-range`]: cell.range && !cell.start && !cell.end
  102 + [`${prefixCls}-cell-range`]: cell.range && !cell.start && !cell.end,
  103 + [`${prefixCls}-focused`]: clearHours(cell.date) === clearHours(this.focusedDate)
  104 +
103 105 }
104 106 ];
105 107 },
... ...
src/components/date-picker/base/mixin.js
... ... @@ -2,6 +2,7 @@
2 2 import {clearHours} from '../util';
3 3  
4 4 export default {
  5 + name: 'PanelTable',
5 6 props: {
6 7 tableDate: {
7 8 type: Date,
... ... @@ -26,7 +27,10 @@ export default {
26 27 selecting: false
27 28 })
28 29 },
29   -
  30 + focusedDate: {
  31 + type: Date,
  32 + required: true,
  33 + }
30 34 },
31 35 computed: {
32 36 dates(){
... ...
src/components/date-picker/base/month-table.vue
... ... @@ -38,14 +38,16 @@
38 38  
39 39 const tableYear = this.tableDate.getFullYear();
40 40 const selectedDays = this.dates.filter(Boolean).map(date => clearHours(new Date(date.getFullYear(), date.getMonth(), 1)));
  41 + const focusedDate = clearHours(new Date(this.focusedDate.getFullYear(), this.focusedDate.getMonth(), 1));
41 42  
42 43 for (let i = 0; i < 12; i++) {
43 44 const cell = deepCopy(cell_tmpl);
44 45 cell.date = new Date(tableYear, i, 1);
45 46 cell.text = this.tCell(i + 1);
46   - const time = clearHours(cell.date);
  47 + const day = clearHours(cell.date);
47 48 cell.disabled = typeof this.disabledDate === 'function' && this.disabledDate(cell.date) && this.selectionMode === 'month';
48   - cell.selected = selectedDays.includes(time);
  49 + cell.selected = selectedDays.includes(day);
  50 + cell.focused = day === focusedDate;
49 51 cells.push(cell);
50 52 }
51 53  
... ... @@ -59,6 +61,7 @@
59 61 {
60 62 [`${prefixCls}-cell-selected`]: cell.selected,
61 63 [`${prefixCls}-cell-disabled`]: cell.disabled,
  64 + [`${prefixCls}-cell-focused`]: cell.focused,
62 65 [`${prefixCls}-cell-range`]: cell.range && !cell.start && !cell.end
63 66 }
64 67 ];
... ...
src/components/date-picker/base/time-spinner.vue
... ... @@ -22,8 +22,10 @@
22 22 import { deepCopy, scrollTop, firstUpperCase } from '../../../utils/assist';
23 23  
24 24 const prefixCls = 'ivu-time-picker-cells';
  25 + const timeParts = ['hours', 'minutes', 'seconds'];
25 26  
26 27 export default {
  28 + name: 'TimeSpinner',
27 29 mixins: [Options],
28 30 props: {
29 31 hours: {
... ... @@ -51,7 +53,9 @@
51 53 return {
52 54 spinerSteps: [1, 1, 1].map((one, i) => Math.abs(this.steps[i]) || one),
53 55 prefixCls: prefixCls,
54   - compiled: false
  56 + compiled: false,
  57 + focusedColumn: -1, // which column inside the picker
  58 + focusedTime: [0, 0, 0] // the values array into [hh, mm, ss]
55 59 };
56 60 },
57 61 computed: {
... ... @@ -66,6 +70,7 @@
66 70 hoursList () {
67 71 let hours = [];
68 72 const step = this.spinerSteps[0];
  73 + const focusedHour = this.focusedColumn === 0 && this.focusedTime[0];
69 74 const hour_tmpl = {
70 75 text: 0,
71 76 selected: false,
... ... @@ -76,6 +81,7 @@
76 81 for (let i = 0; i < 24; i += step) {
77 82 const hour = deepCopy(hour_tmpl);
78 83 hour.text = i;
  84 + hour.focused = i === focusedHour;
79 85  
80 86 if (this.disabledHours.length && this.disabledHours.indexOf(i) > -1) {
81 87 hour.disabled = true;
... ... @@ -90,6 +96,7 @@
90 96 minutesList () {
91 97 let minutes = [];
92 98 const step = this.spinerSteps[1];
  99 + const focusedMinute = this.focusedColumn === 1 && this.focusedTime[1];
93 100 const minute_tmpl = {
94 101 text: 0,
95 102 selected: false,
... ... @@ -100,6 +107,7 @@
100 107 for (let i = 0; i < 60; i += step) {
101 108 const minute = deepCopy(minute_tmpl);
102 109 minute.text = i;
  110 + minute.focused = i === focusedMinute;
103 111  
104 112 if (this.disabledMinutes.length && this.disabledMinutes.indexOf(i) > -1) {
105 113 minute.disabled = true;
... ... @@ -113,6 +121,7 @@
113 121 secondsList () {
114 122 let seconds = [];
115 123 const step = this.spinerSteps[2];
  124 + const focusedMinute = this.focusedColumn === 2 && this.focusedTime[2];
116 125 const second_tmpl = {
117 126 text: 0,
118 127 selected: false,
... ... @@ -123,6 +132,7 @@
123 132 for (let i = 0; i < 60; i += step) {
124 133 const second = deepCopy(second_tmpl);
125 134 second.text = i;
  135 + second.focused = i === focusedMinute;
126 136  
127 137 if (this.disabledSeconds.length && this.disabledSeconds.indexOf(i) > -1) {
128 138 second.disabled = true;
... ... @@ -141,15 +151,32 @@
141 151 `${prefixCls}-cell`,
142 152 {
143 153 [`${prefixCls}-cell-selected`]: cell.selected,
  154 + [`${prefixCls}-cell-focused`]: cell.focused,
144 155 [`${prefixCls}-cell-disabled`]: cell.disabled
  156 +
145 157 }
146 158 ];
147 159 },
  160 + chooseValue(values){
  161 + const changes = timeParts.reduce((obj, part, i) => {
  162 + const value = values[i];
  163 + if (this[part] === value) return obj;
  164 + return {
  165 + ...obj,
  166 + [part]: value
  167 + };
  168 + }, {});
  169 + if (Object.keys(changes).length > 0) {
  170 + this.emitChange(changes);
  171 + }
  172 + },
148 173 handleClick (type, cell) {
149 174 if (cell.disabled) return;
150   - const data = {};
151   - data[type] = cell.text;
152   - this.$emit('on-change', data);
  175 + const data = {[type]: cell.text};
  176 + this.emitChange(data);
  177 + },
  178 + emitChange(changes){
  179 + this.$emit('on-change', changes);
153 180 this.$emit('on-pick-click');
154 181 },
155 182 scroll (type, index) {
... ... @@ -168,15 +195,19 @@
168 195 return index;
169 196 },
170 197 updateScroll () {
171   - const times = ['hours', 'minutes', 'seconds'];
172 198 this.$nextTick(() => {
173   - times.forEach(type => {
  199 + timeParts.forEach(type => {
174 200 this.$refs[type].scrollTop = 24 * this[`${type}List`].findIndex(obj => obj.text == this[type]);
175 201 });
176 202 });
177 203 },
178 204 formatTime (text) {
179 205 return text < 10 ? '0' + text : text;
  206 + },
  207 + updateFocusedTime(col, time) {
  208 + this.focusedColumn = col;
  209 + this.focusedTime = time.slice();
  210 +
180 211 }
181 212 },
182 213 watch: {
... ... @@ -191,6 +222,13 @@
191 222 seconds (val) {
192 223 if (!this.compiled) return;
193 224 this.scroll('seconds', this.secondsList.findIndex(obj => obj.text == val));
  225 + },
  226 + focusedTime(updated, old){
  227 + timeParts.forEach((part, i) => {
  228 + if (updated[i] === old[i] || typeof updated[i] === 'undefined') return;
  229 + const valueIndex = this[`${part}List`].findIndex(obj => obj.text === updated[i]);
  230 + this.scroll(part, valueIndex);
  231 + });
194 232 }
195 233 },
196 234 mounted () {
... ...
src/components/date-picker/base/year-table.vue
... ... @@ -39,13 +39,15 @@
39 39 };
40 40  
41 41 const selectedDays = this.dates.filter(Boolean).map(date => clearHours(new Date(date.getFullYear(), 0, 1)));
  42 + const focusedDate = clearHours(new Date(this.focusedDate.getFullYear(), 0, 1));
42 43  
43 44 for (let i = 0; i < 10; i++) {
44 45 const cell = deepCopy(cell_tmpl);
45 46 cell.date = new Date(this.startYear + i, 0, 1);
46 47 cell.disabled = typeof this.disabledDate === 'function' && this.disabledDate(cell.date) && this.selectionMode === 'year';
47   - const time = clearHours(cell.date);
48   - cell.selected = selectedDays.includes(time);
  48 + const day = clearHours(cell.date);
  49 + cell.selected = selectedDays.includes(day);
  50 + cell.focused = day === focusedDate;
49 51 cells.push(cell);
50 52 }
51 53  
... ... @@ -59,6 +61,7 @@
59 61 {
60 62 [`${prefixCls}-cell-selected`]: cell.selected,
61 63 [`${prefixCls}-cell-disabled`]: cell.disabled,
  64 + [`${prefixCls}-cell-focused`]: cell.focused,
62 65 [`${prefixCls}-cell-range`]: cell.range && !cell.start && !cell.end
63 66 }
64 67 ];
... ...
src/components/date-picker/panel/Date/date-panel-mixin.js
... ... @@ -46,6 +46,10 @@ export default {
46 46 pickerType: {
47 47 type: String,
48 48 require: true
  49 + },
  50 + focusedDate: {
  51 + type: Date,
  52 + required: true,
49 53 }
50 54 },
51 55 computed: {
... ...
src/components/date-picker/panel/Date/date-range.vue
... ... @@ -41,6 +41,8 @@
41 41 :range-state="rangeState"
42 42 :show-week-numbers="showWeekNumbers"
43 43 :value="preSelecting.left ? [dates[0]] : dates"
  44 + :focused-date="focusedDate"
  45 +
44 46 @on-change-range="handleChangeRange"
45 47 @on-pick="panelPickerHandlers.left"
46 48 @on-pick-click="handlePickClick"
... ... @@ -80,6 +82,8 @@
80 82 :disabled-date="disabledDate"
81 83 :show-week-numbers="showWeekNumbers"
82 84 :value="preSelecting.right ? [dates[dates.length - 1]] : dates"
  85 + :focused-date="focusedDate"
  86 +
83 87 @on-change-range="handleChangeRange"
84 88 @on-pick="panelPickerHandlers.right"
85 89 @on-pick-click="handlePickClick"></component>
... ... @@ -178,7 +182,7 @@
178 182 [prefixCls + '-body-time']: this.showTime,
179 183 [prefixCls + '-body-date']: !this.showTime,
180 184 }
181   - ]
  185 + ];
182 186 },
183 187 leftDatePanelLabel(){
184 188 return this.panelLabelConfig('left');
... ... @@ -224,10 +228,7 @@
224 228  
225 229  
226 230 // set panels positioning
227   - const leftPanelDate = this.startDate || this.dates[0] || new Date();
228   - this.leftPanelDate = leftPanelDate;
229   - const rightPanelDate = new Date(leftPanelDate.getFullYear(), leftPanelDate.getMonth() + 1, leftPanelDate.getDate());
230   - this.rightPanelDate = this.splitPanels ? new Date(Math.max(this.dates[1], rightPanelDate)) : rightPanelDate;
  231 + this.setPanelDates(this.startDate || this.dates[0] || new Date());
231 232 },
232 233 currentView(currentView){
233 234 const leftMonth = this.leftPanelDate.getMonth();
... ... @@ -246,6 +247,9 @@
246 247 },
247 248 selectionMode(type){
248 249 this.currentView = type || 'range';
  250 + },
  251 + focusedDate(date){
  252 + this.setPanelDates(date || new Date());
249 253 }
250 254 },
251 255 methods: {
... ... @@ -254,6 +258,11 @@
254 258 this.leftPickerTable = `${this.currentView}-table`;
255 259 this.rightPickerTable = `${this.currentView}-table`;
256 260 },
  261 + setPanelDates(leftPanelDate){
  262 + this.leftPanelDate = leftPanelDate;
  263 + const rightPanelDate = new Date(leftPanelDate.getFullYear(), leftPanelDate.getMonth() + 1, leftPanelDate.getDate());
  264 + this.rightPanelDate = this.splitPanels ? new Date(Math.max(this.dates[1], rightPanelDate)) : rightPanelDate;
  265 + },
257 266 panelLabelConfig (direction) {
258 267 const locale = this.t('i.locale');
259 268 const datePanelLabel = this.t('i.datepicker.datePanelLabel');
... ...
src/components/date-picker/panel/Date/date.vue
... ... @@ -39,6 +39,8 @@
39 39 :value="dates"
40 40 :selection-mode="selectionMode"
41 41 :disabled-date="disabledDate"
  42 + :focused-date="focusedDate"
  43 +
42 44 @on-pick="panelPickerHandlers"
43 45 @on-pick-click="handlePickClick"
44 46 ></component>
... ... @@ -51,6 +53,8 @@
51 53 :format="format"
52 54 :time-disabled="timeDisabled"
53 55 :disabled-date="disabledDate"
  56 + :focused-date="focusedDate"
  57 +
54 58 v-bind="timePickerOptions"
55 59 @on-pick="handlePick"
56 60 @on-pick-click="handlePickClick"
... ... @@ -150,7 +154,6 @@
150 154 },
151 155 currentView (currentView) {
152 156 this.$emit('on-selection-mode-change', currentView);
153   - this.pickertable = this.getTableType(currentView);
154 157  
155 158 if (this.currentView === 'time') {
156 159 this.$nextTick(() => {
... ... @@ -162,6 +165,13 @@
162 165 selectionMode(type){
163 166 this.currentView = type;
164 167 this.pickerTable = this.getTableType(type);
  168 + },
  169 + focusedDate(date){
  170 + const isDifferentYear = date.getFullYear() !== this.panelDate.getFullYear();
  171 + const isDifferentMonth = isDifferentYear || date.getMonth() !== this.panelDate.getMonth();
  172 + if (isDifferentYear || isDifferentMonth){
  173 + this.panelDate = date;
  174 + }
165 175 }
166 176 },
167 177 methods: {
... ...
src/components/date-picker/picker.vue
1 1 <template>
2   - <div :class="[prefixCls]" v-clickoutside="handleClose">
  2 + <div
  3 + :class="wrapperClasses"
  4 + v-click-outside:mousedown.capture="handleClose"
  5 + v-click-outside.capture="handleClose"
  6 + >
3 7 <div ref="reference" :class="[prefixCls + '-rel']">
4 8 <slot>
5 9 <i-input
... ... @@ -12,10 +16,14 @@
12 16 :placeholder="placeholder"
13 17 :value="visualValue"
14 18 :name="name"
  19 + ref="input"
  20 +
15 21 @on-input-change="handleInputChange"
16 22 @on-focus="handleFocus"
17 23 @on-blur="handleBlur"
18 24 @on-click="handleIconClick"
  25 + @click.native="handleFocus"
  26 + @keydown.native="handleKeydown"
19 27 @mouseenter.native="handleInputMouseenter"
20 28 @mouseleave.native="handleInputMouseleave"
21 29  
... ... @@ -48,6 +56,7 @@
48 56 :show-week-numbers="showWeekNumbers"
49 57 :picker-type="type"
50 58 :multiple="multiple"
  59 + :focused-date="focusedDate"
51 60  
52 61 :time-picker-options="timePickerOptions"
53 62  
... ... @@ -69,21 +78,49 @@
69 78  
70 79 import iInput from '../../components/input/input.vue';
71 80 import Drop from '../../components/select/dropdown.vue';
72   - import clickoutside from '../../directives/clickoutside';
  81 + import vClickOutside from 'v-click-outside-x/index';
73 82 import TransferDom from '../../directives/transfer-dom';
74 83 import { oneOf } from '../../utils/assist';
75   - import { DEFAULT_FORMATS, RANGE_SEPARATOR, TYPE_VALUE_RESOLVER_MAP } from './util';
  84 + import { DEFAULT_FORMATS, RANGE_SEPARATOR, TYPE_VALUE_RESOLVER_MAP, getDayCountOfMonth } from './util';
  85 + import {findComponentsDownward} from '../../utils/assist';
76 86 import Emitter from '../../mixins/emitter';
77 87  
78 88 const prefixCls = 'ivu-date-picker';
  89 + const pickerPrefixCls = 'ivu-picker';
79 90  
80 91 const isEmptyArray = val => val.reduce((isEmpty, str) => isEmpty && !str || (typeof str === 'string' && str.trim() === ''), true);
  92 + const keyValueMapper = {
  93 + 40: 'up',
  94 + 39: 'right',
  95 + 38: 'down',
  96 + 37: 'left',
  97 + };
  98 +
  99 + const mapPossibleValues = (key, horizontal, vertical) => {
  100 + if (key === 'left') return horizontal * -1;
  101 + if (key === 'right') return horizontal * 1;
  102 + if (key === 'up') return vertical * 1;
  103 + if (key === 'down') return vertical * -1;
  104 + };
  105 +
  106 + const pulseElement = (el) => {
  107 + const pulseClass = 'ivu-date-picker-btn-pulse';
  108 + el.classList.add(pulseClass);
  109 + setTimeout(() => el.classList.remove(pulseClass), 200);
  110 + };
  111 +
  112 + const extractTime = date => {
  113 + if (!date) return [0, 0, 0];
  114 + return [
  115 + date.getHours(), date.getMinutes(), date.getSeconds()
  116 + ];
  117 + };
  118 +
81 119  
82 120 export default {
83   - name: 'CalendarPicker',
84 121 mixins: [ Emitter ],
85 122 components: { iInput, Drop },
86   - directives: { clickoutside, TransferDom },
  123 + directives: { clickOutside: vClickOutside.directive, TransferDom },
87 124 props: {
88 125 format: {
89 126 type: String
... ... @@ -172,6 +209,7 @@
172 209 const isRange = this.type.includes('range');
173 210 const emptyArray = isRange ? [null, null] : [null];
174 211 const initialValue = isEmptyArray((isRange ? this.value : [this.value]) || []) ? emptyArray : this.parseDate(this.value);
  212 + const focusedTime = initialValue.map(extractTime);
175 213  
176 214 return {
177 215 prefixCls: prefixCls,
... ... @@ -181,10 +219,24 @@
181 219 disableClickOutSide: false, // fixed when click a date,trigger clickoutside to close picker
182 220 disableCloseUnderTransfer: false, // transfer ๆจกๅผไธ‹๏ผŒ็‚นๅ‡ปDropไนŸไผš่งฆๅ‘ๅ…ณ้—ญ,
183 221 selectionMode: this.onSelectionModeChange(this.type),
184   - forceInputRerender: 1
  222 + forceInputRerender: 1,
  223 + isFocused: false,
  224 + focusedDate: initialValue[0] || this.startDate || new Date(),
  225 + focusedTime: {
  226 + column: 0, // which column inside the picker
  227 + picker: 0, // which picker
  228 + time: focusedTime, // the values array into [hh, mm, ss],
  229 + active: false
  230 + },
  231 + internalFocus: false,
185 232 };
186 233 },
187 234 computed: {
  235 + wrapperClasses(){
  236 + return [prefixCls, {
  237 + [prefixCls + '-focused']: this.isFocused
  238 + }];
  239 + },
188 240 publicVModelValue(){
189 241 if (this.multiple){
190 242 return this.internalValue.slice();
... ... @@ -232,32 +284,254 @@
232 284 handleTransferClick () {
233 285 if (this.transfer) this.disableCloseUnderTransfer = true;
234 286 },
235   - handleClose () {
  287 + handleClose (e) {
236 288 if (this.disableCloseUnderTransfer) {
237 289 this.disableCloseUnderTransfer = false;
238 290 return false;
239 291 }
240   - if (this.open !== null) return;
241 292  
242   - this.visible = false;
  293 + if (e && e.type === 'mousedown' && this.visible) {
  294 + e.preventDefault();
  295 + e.stopPropagation();
  296 + return;
  297 + }
  298 +
  299 + if (this.visible) {
  300 + const pickerPanel = this.$refs.pickerPanel && this.$refs.pickerPanel.$el;
  301 + if (e && pickerPanel && pickerPanel.contains(e.target)) return; // its a click inside own component, lets ignore it.
  302 +
  303 + this.visible = false;
  304 + e && e.preventDefault();
  305 + e && e.stopPropagation();
  306 + return;
  307 + }
  308 +
  309 + this.isFocused = false;
243 310 this.disableClickOutSide = false;
244 311 },
245   - handleFocus () {
  312 + handleFocus (e) {
246 313 if (this.readonly) return;
  314 + this.isFocused = true;
  315 + if (e && e.type === 'focus') return; // just focus, don't open yet
247 316 this.visible = true;
248   - this.$refs.pickerPanel.onToggleVisibility(true);
249 317 },
250   - handleBlur () {
251   - this.visible = false;
  318 + handleBlur (e) {
  319 + if (this.internalFocus){
  320 + this.internalFocus = false;
  321 + return;
  322 + }
  323 + if (this.visible) {
  324 + e.preventDefault();
  325 + return;
  326 + }
  327 +
  328 + this.isFocused = false;
252 329 this.onSelectionModeChange(this.type);
253 330 this.internalValue = this.internalValue.slice(); // trigger panel watchers to reset views
254 331 this.reset();
255 332 this.$refs.pickerPanel.onToggleVisibility(false);
256 333  
257 334 },
  335 + handleKeydown(e){
  336 + const keyCode = e.keyCode;
  337 +
  338 + // handle "tab" key
  339 + if (keyCode === 9){
  340 + if (this.visible){
  341 + e.stopPropagation();
  342 + e.preventDefault();
  343 +
  344 + if (this.isConfirm){
  345 + const selector = `.${pickerPrefixCls}-confirm > *`;
  346 + const tabbable = this.$refs.drop.$el.querySelectorAll(selector);
  347 + this.internalFocus = true;
  348 + const element = [...tabbable][e.shiftKey ? 'pop' : 'shift']();
  349 + element.focus();
  350 + } else {
  351 + this.handleClose();
  352 + }
  353 + } else {
  354 + this.focused = false;
  355 + }
  356 + }
  357 +
  358 + // open the panel
  359 + const arrows = [37, 38, 39, 40];
  360 + if (!this.visible && arrows.includes(keyCode)){
  361 + this.visible = true;
  362 + return;
  363 + }
  364 +
  365 + // close on "esc" key
  366 + if (keyCode === 27){
  367 + if (this.visible) {
  368 + e.stopPropagation();
  369 + this.handleClose();
  370 + }
  371 + }
  372 +
  373 + // select date, "Enter" key
  374 + if (keyCode === 13){
  375 + const timePickers = findComponentsDownward(this, 'TimeSpinner');
  376 + if (timePickers.length > 0){
  377 + const columnsPerPicker = timePickers[0].showSeconds ? 3 : 2;
  378 + const pickerIndex = Math.floor(this.focusedTime.column / columnsPerPicker);
  379 + const value = this.focusedTime.time[pickerIndex];
  380 +
  381 + timePickers[pickerIndex].chooseValue(value);
  382 + return;
  383 + }
  384 +
  385 + if (this.type.match(/range/)){
  386 + this.$refs.pickerPanel.handleRangePick(this.focusedDate, 'date');
  387 + } else {
  388 + const panels = findComponentsDownward(this, 'PanelTable');
  389 + const compareDate = (d) => {
  390 + const sliceIndex = ['year', 'month', 'date'].indexOf((this.type)) + 1;
  391 + return [d.getFullYear(), d.getMonth(), d.getDate()].slice(0, sliceIndex).join('-');
  392 + };
  393 + const dateIsValid = panels.find(({cells}) => {
  394 + return cells.find(({date, disabled}) => compareDate(date) === compareDate(this.focusedDate) && !disabled);
  395 + });
  396 + if (dateIsValid) this.onPick(this.focusedDate, false, 'date');
  397 + }
  398 + }
  399 +
  400 + if (!arrows.includes(keyCode)) return; // ignore rest of keys
  401 +
  402 + // navigate times and dates
  403 + if (this.focusedTime.active) e.preventDefault(); // to prevent cursor from moving
  404 + this.navigateDatePanel(keyValueMapper[keyCode], e.shiftKey);
  405 + },
258 406 reset(){
259 407 this.$refs.pickerPanel.reset && this.$refs.pickerPanel.reset();
260 408 },
  409 + navigateTimePanel(direction){
  410 +
  411 + this.focusedTime.active = true;
  412 + const horizontal = direction.match(/left|right/);
  413 + const vertical = direction.match(/up|down/);
  414 + const timePickers = findComponentsDownward(this, 'TimeSpinner');
  415 +
  416 + const maxNrOfColumns = (timePickers[0].showSeconds ? 3 : 2) * timePickers.length;
  417 + const column = (currentColumn => {
  418 + const incremented = currentColumn + (horizontal ? (direction === 'left' ? -1 : 1) : 0);
  419 + return (incremented + maxNrOfColumns) % maxNrOfColumns;
  420 + })(this.focusedTime.column);
  421 +
  422 + const columnsPerPicker = maxNrOfColumns / timePickers.length;
  423 + const pickerIndex = Math.floor(column / columnsPerPicker);
  424 + const col = column % columnsPerPicker;
  425 +
  426 +
  427 + if (horizontal){
  428 + const time = this.internalValue.map(extractTime);
  429 +
  430 + this.focusedTime = {
  431 + ...this.focusedTime,
  432 + column: column,
  433 + time: time
  434 + };
  435 + timePickers.forEach((instance, i) => {
  436 + if (i === pickerIndex) instance.updateFocusedTime(col, time[pickerIndex]);
  437 + else instance.updateFocusedTime(-1, instance.focusedTime);
  438 + });
  439 + }
  440 +
  441 + if (vertical){
  442 + const increment = direction === 'up' ? 1 : -1;
  443 + const timeParts = ['hours', 'minutes', 'seconds'];
  444 +
  445 +
  446 + const pickerPossibleValues = timePickers[pickerIndex][`${timeParts[col]}List`];
  447 + const nextIndex = pickerPossibleValues.findIndex(({text}) => this.focusedTime.time[pickerIndex][col] === text) + increment;
  448 + const nextValue = pickerPossibleValues[nextIndex % pickerPossibleValues.length].text;
  449 + const times = this.focusedTime.time.map((time, i) => {
  450 + if (i !== pickerIndex) return time;
  451 + time[col] = nextValue;
  452 + return time;
  453 + });
  454 + this.focusedTime = {
  455 + ...this.focusedTime,
  456 + time: times
  457 + };
  458 +
  459 + timePickers.forEach((instance, i) => {
  460 + if (i === pickerIndex) instance.updateFocusedTime(col, times[i]);
  461 + else instance.updateFocusedTime(-1, instance.focusedTime);
  462 + });
  463 + }
  464 + },
  465 + navigateDatePanel(direction, shift){
  466 +
  467 + const timePickers = findComponentsDownward(this, 'TimeSpinner');
  468 + if (timePickers.length > 0) {
  469 + // we are in TimePicker mode
  470 + this.navigateTimePanel(direction, shift, timePickers);
  471 + return;
  472 + }
  473 +
  474 + if (shift){
  475 + if (this.type === 'year'){
  476 + this.focusedDate = new Date(
  477 + this.focusedDate.getFullYear() + mapPossibleValues(direction, 0, 10),
  478 + this.focusedDate.getMonth(),
  479 + this.focusedDate.getDate()
  480 + );
  481 + } else {
  482 + this.focusedDate = new Date(
  483 + this.focusedDate.getFullYear() + mapPossibleValues(direction, 0, 1),
  484 + this.focusedDate.getMonth() + mapPossibleValues(direction, 1, 0),
  485 + this.focusedDate.getDate()
  486 + );
  487 + }
  488 +
  489 + const position = direction.match(/left|down/) ? 'prev' : 'next';
  490 + const double = direction.match(/up|down/) ? '-double' : '';
  491 +
  492 + // pulse button
  493 + const button = this.$refs.drop.$el.querySelector(`.ivu-date-picker-${position}-btn-arrow${double}`);
  494 + if (button) pulseElement(button);
  495 + return;
  496 + }
  497 +
  498 + const initialDate = this.focusedDate || (this.internalValue && this.internalValue[0]) || new Date();
  499 + const focusedDate = new Date(initialDate);
  500 +
  501 + if (this.type.match(/^date/)){
  502 + const lastOfMonth = getDayCountOfMonth(initialDate.getFullYear(), initialDate.getMonth());
  503 + const startDay = initialDate.getDate();
  504 + const nextDay = focusedDate.getDate() + mapPossibleValues(direction, 1, 7);
  505 +
  506 + if (nextDay < 1) {
  507 + if (direction.match(/left|right/)) {
  508 + focusedDate.setMonth(focusedDate.getMonth() + 1);
  509 + focusedDate.setDate(nextDay);
  510 + } else {
  511 + focusedDate.setDate(startDay + Math.floor((lastOfMonth - startDay) / 7) * 7);
  512 + }
  513 + } else if (nextDay > lastOfMonth){
  514 + if (direction.match(/left|right/)) {
  515 + focusedDate.setMonth(focusedDate.getMonth() - 1);
  516 + focusedDate.setDate(nextDay);
  517 + } else {
  518 + focusedDate.setDate(startDay % 7);
  519 + }
  520 + } else {
  521 + focusedDate.setDate(nextDay);
  522 + }
  523 + }
  524 +
  525 + if (this.type.match(/^month/)) {
  526 + focusedDate.setMonth(focusedDate.getMonth() + mapPossibleValues(direction, 1, 3));
  527 + }
  528 +
  529 + if (this.type.match(/^year/)) {
  530 + focusedDate.setFullYear(focusedDate.getFullYear() + mapPossibleValues(direction, 1, 3));
  531 + }
  532 +
  533 + this.focusedDate = focusedDate;
  534 + },
261 535 handleInputChange (event) {
262 536 const isArrayValue = this.type.includes('range') || this.multiple;
263 537 const oldValue = this.visualValue;
... ... @@ -377,6 +651,12 @@
377 651 this.internalValue = Array.isArray(dates) ? dates : [dates];
378 652 }
379 653  
  654 + this.focusedDate = this.internalValue[0];
  655 + this.focusedTime = {
  656 + ...this.focusedTime,
  657 + time: this.internalValue.map(extractTime)
  658 + };
  659 +
380 660 if (!this.isConfirm) this.onSelectionModeChange(this.type); // reset the selectionMode
381 661 if (!this.isConfirm) this.visible = visible;
382 662 this.emitChange(type);
... ... @@ -384,22 +664,23 @@
384 664 onPickSuccess(){
385 665 this.visible = false;
386 666 this.$emit('on-ok');
  667 + this.focus();
387 668 this.reset();
388 669 },
  670 + focus() {
  671 + this.$refs.input.focus();
  672 + }
389 673 },
390 674 watch: {
391 675 visible (state) {
392 676 if (state === false){
393 677 this.$refs.drop.destroy();
394   - const input = this.$el.querySelector('input');
395   - if (input) input.blur();
396 678 }
397 679 this.$refs.drop.update();
398 680 this.$emit('on-open-change', state);
399 681 },
400 682 value(val) {
401 683 this.internalValue = this.parseDate(val);
402   -
403 684 },
404 685 open (val) {
405 686 this.visible = val === true;
... ... @@ -421,6 +702,9 @@
421 702 this.$emit('input', this.publicVModelValue); // to update v-model
422 703 }
423 704 if (this.open !== null) this.visible = this.open;
  705 +
  706 + // to handle focus from confirm buttons
  707 + this.$on('focus-input', () => this.focus());
424 708 }
425 709 };
426 710 </script>
... ...
src/components/date-picker/picker/date-picker.js
... ... @@ -5,6 +5,7 @@ import RangeDatePickerPanel from &#39;../panel/Date/date-range.vue&#39;;
5 5 import { oneOf } from '../../../utils/assist';
6 6  
7 7 export default {
  8 + name: 'CalendarPicker',
8 9 mixins: [Picker],
9 10 props: {
10 11 type: {
... ...
src/components/date-picker/picker/time-picker.js
... ... @@ -3,7 +3,7 @@ import TimePickerPanel from &#39;../panel/Time/time.vue&#39;;
3 3 import RangeTimePickerPanel from '../panel/Time/time-range.vue';
4 4 import Options from '../time-mixins';
5 5  
6   -import { oneOf } from '../../../utils/assist';
  6 +import { findComponentsDownward, oneOf } from '../../../utils/assist';
7 7  
8 8 export default {
9 9 mixins: [Picker, Options],
... ... @@ -30,4 +30,14 @@ export default {
30 30 };
31 31 }
32 32 },
  33 + watch: {
  34 + visible(visible){
  35 + if (visible) {
  36 + this.$nextTick(() => {
  37 + const spinners = findComponentsDownward(this, 'TimeSpinner');
  38 + spinners.forEach(instance => instance.updateScroll());
  39 + });
  40 + }
  41 + }
  42 + }
33 43 };
... ...
src/styles/components/date-picker.less
... ... @@ -44,17 +44,23 @@
44 44 margin: 2px;
45 45 color: @btn-disable-color;
46 46 }
  47 + &-cell:hover{
  48 + em{
  49 + background: @date-picker-cell-hover-bg;
  50 + }
  51 + }
  52 + &-focused{
  53 + em{
  54 + box-shadow: 0 0 0 1px @primary-color inset;
  55 + }
  56 + }
  57 +
47 58 &-cell{
48 59 span&{
49 60 width: 28px;
50 61 height: 28px;
51 62 cursor: pointer;
52 63 }
53   - &:hover{
54   - em{
55   - background: @date-picker-cell-hover-bg;
56   - }
57   - }
58 64 &-prev-month,&-next-month{
59 65 em{
60 66 color: @btn-disable-color;
... ... @@ -154,6 +160,11 @@
154 160 margin: 0;
155 161 }
156 162 }
  163 +
  164 + .@{date-picker-prefix-cls}-cells-cell-focused{
  165 + background-color: tint(@primary-color, 80%);
  166 + }
  167 +
157 168 }
158 169  
159 170 &-header{
... ... @@ -169,6 +180,11 @@
169 180 }
170 181 }
171 182 }
  183 + &-btn-pulse{
  184 + background-color: tint(@primary-color, 80%) !important;
  185 + border-radius: @border-radius-small;
  186 + transition: background-color @transition-time @ease-in-out;
  187 + }
172 188 &-prev-btn{
173 189 float: left;
174 190 &-arrow-double{
... ... @@ -216,6 +232,10 @@
216 232 max-height: none;
217 233 width: auto;
218 234 }
  235 +
  236 + &-focused input{
  237 + .active();
  238 + }
219 239 }
220 240  
221 241 .@{picker-prefix-cls} {
... ... @@ -289,9 +309,9 @@
289 309 color: @link-active-color;
290 310 }
291 311 }
292   - & > span&-time-disabled{
293   - color: @btn-disable-color;
294   - cursor: @cursor-disabled;
  312 +
  313 + &-time{
  314 + float: left;
295 315 }
296 316 }
297 317 }
... ...
src/styles/components/time-picker.less
... ... @@ -70,6 +70,9 @@
70 70 color: @primary-color;
71 71 background: @background-color-select-hover;
72 72 }
  73 + &-focused{
  74 + background-color: tint(@primary-color, 80%);
  75 + }
73 76 }
74 77 }
75 78  
... ... @@ -165,4 +168,4 @@
165 168 }
166 169 }
167 170 }
168   -}
169 171 \ No newline at end of file
  172 +}
... ...
test/unit/specs/date-picker.spec.js
... ... @@ -116,7 +116,7 @@ describe(&#39;DatePicker.vue&#39;, () =&gt; {
116 116 `);
117 117  
118 118 const picker = vm.$children[0];
119   - picker.handleIconClick();
  119 + picker.handleFocus({type: 'focus'});
120 120 vm.$nextTick(() => {
121 121 const displayField = vm.$el.querySelector('.ivu-input');
122 122 const clickableCells = vm.$el.querySelectorAll('.ivu-date-picker-cells-cell');
... ... @@ -169,7 +169,7 @@ describe(&#39;DatePicker.vue&#39;, () =&gt; {
169 169 });
170 170  
171 171 const picker = vm.$children[0];
172   - picker.handleIconClick();
  172 + picker.handleFocus({type: 'focus'});
173 173 vm.$nextTick(() => {
174 174 const panel = vm.$el.querySelector('.ivu-picker-panel-content');
175 175 const dayPanel = panel.querySelector('[class="ivu-date-picker-cells"]');
... ... @@ -243,7 +243,7 @@ describe(&#39;DatePicker.vue&#39;, () =&gt; {
243 243 `);
244 244  
245 245 const picker = vm.$children[0];
246   - picker.handleIconClick();
  246 + picker.handleFocus({type: 'focus'});
247 247 vm.$nextTick(() => {
248 248 const displayField = vm.$el.querySelector('.ivu-input');
249 249 const clickableCells = vm.$el.querySelectorAll('.ivu-date-picker-cells-cell');
... ... @@ -266,9 +266,11 @@ describe(&#39;DatePicker.vue&#39;, () =&gt; {
266 266 // it should be closed by now
267 267 expect(picker.visible).to.equal(false);
268 268 // open picker again
269   - picker.handleIconClick();
  269 + picker.handleFocus({type: 'focus'});
  270 + picker.visible = true;
270 271  
271   - vm.$nextTick(() => {
  272 +
  273 + vm.$nextTick(() => {
272 274 expect(picker.visible).to.equal(true);
273 275 expect(JSON.stringify(picker.internalValue)).to.equal('[null,null]');
274 276 expect(displayField.value).to.equal('');
... ... @@ -355,7 +357,7 @@ describe(&#39;DatePicker.vue&#39;, () =&gt; {
355 357 `);
356 358  
357 359 const picker = vm.$children[0];
358   - picker.handleIconClick();
  360 + picker.handleFocus({type: 'focus'});
359 361 vm.$nextTick(() => {
360 362 const now = new Date();
361 363 const labels = vm.$el.querySelectorAll('.ivu-picker-panel-body .ivu-date-picker-header-label');
... ...
test/unit/specs/time-spinner.spec.js
... ... @@ -11,7 +11,7 @@ describe(&#39;TimePicker.vue&#39;, () =&gt; {
11 11 <Time-Picker></Time-Picker>
12 12 `);
13 13 const picker = vm.$children[0];
14   - picker.handleIconClick(); // open the picker panels
  14 + picker.handleFocus({type: 'focus'}); // open the picker panels
15 15  
16 16 vm.$nextTick(() => {
17 17 const spiners = picker.$el.querySelectorAll('.ivu-time-picker-cells-list');
... ... @@ -28,7 +28,7 @@ describe(&#39;TimePicker.vue&#39;, () =&gt; {
28 28 <Time-Picker format="HH:mm"></Time-Picker>
29 29 `);
30 30 const picker = vm.$children[0];
31   - picker.handleIconClick(); // open the picker panels
  31 + picker.handleFocus({type: 'focus'}); // open the picker panels
32 32  
33 33 vm.$nextTick(() => {
34 34 const spiners = picker.$el.querySelectorAll('.ivu-time-picker-cells-list');
... ... @@ -44,7 +44,7 @@ describe(&#39;TimePicker.vue&#39;, () =&gt; {
44 44 <Time-Picker :steps="[1, 15]"></Time-Picker>
45 45 `);
46 46 const picker = vm.$children[0];
47   - picker.handleIconClick(); // open the picker panels
  47 + picker.handleFocus({type: 'focus'}); // open the picker panels
48 48  
49 49 vm.$nextTick(() => {
50 50 const spiners = picker.$el.querySelectorAll('.ivu-time-picker-cells-list');
... ...