Commit 75cb299868c5bee3978ea424da3c5145acefae82
1 parent
2bf3e047
Add keyboard navigation to date|time picker
Showing
16 changed files
with
467 additions
and
67 deletions
Show diff stats
src/components/date-picker/base/confirm.vue
1 | <template> | 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 | </div> | 12 | </div> |
10 | </template> | 13 | </template> |
11 | <script> | 14 | <script> |
12 | import iButton from '../../button/button.vue'; | 15 | import iButton from '../../button/button.vue'; |
13 | import Locale from '../../../mixins/locale'; | 16 | import Locale from '../../../mixins/locale'; |
17 | + import Emitter from '../../../mixins/emitter'; | ||
14 | 18 | ||
15 | const prefixCls = 'ivu-picker'; | 19 | const prefixCls = 'ivu-picker'; |
16 | 20 | ||
17 | export default { | 21 | export default { |
18 | - mixins: [ Locale ], | ||
19 | - components: { iButton }, | 22 | + mixins: [Locale, Emitter], |
23 | + components: {iButton}, | ||
20 | props: { | 24 | props: { |
21 | showTime: false, | 25 | showTime: false, |
22 | isTime: false, | 26 | isTime: false, |
23 | timeDisabled: false | 27 | timeDisabled: false |
24 | }, | 28 | }, |
25 | - data () { | 29 | + data() { |
26 | return { | 30 | return { |
27 | prefixCls: prefixCls | 31 | prefixCls: prefixCls |
28 | }; | 32 | }; |
29 | }, | 33 | }, |
30 | computed: { | 34 | computed: { |
31 | timeClasses () { | 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 | methods: { | 47 | methods: { |
@@ -44,6 +54,17 @@ | @@ -44,6 +54,17 @@ | ||
44 | handleToggleTime () { | 54 | handleToggleTime () { |
45 | if (this.timeDisabled) return; | 55 | if (this.timeDisabled) return; |
46 | this.$emit('on-pick-toggle-time'); | 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
@@ -9,7 +9,7 @@ | @@ -9,7 +9,7 @@ | ||
9 | :class="getCellCls(cell)" | 9 | :class="getCellCls(cell)" |
10 | v-for="(cell, i) in readCells" | 10 | v-for="(cell, i) in readCells" |
11 | :key="String(cell.date) + i" | 11 | :key="String(cell.date) + i" |
12 | - @click="handleClick(cell)" | 12 | + @click="handleClick(cell, $event)" |
13 | @mouseenter="handleMouseMove(cell)" | 13 | @mouseenter="handleMouseMove(cell)" |
14 | > | 14 | > |
15 | <em>{{ cell.desc }}</em> | 15 | <em>{{ cell.desc }}</em> |
@@ -99,7 +99,9 @@ | @@ -99,7 +99,9 @@ | ||
99 | [`${prefixCls}-cell-prev-month`]: cell.type === 'prevMonth', | 99 | [`${prefixCls}-cell-prev-month`]: cell.type === 'prevMonth', |
100 | [`${prefixCls}-cell-next-month`]: cell.type === 'nextMonth', | 100 | [`${prefixCls}-cell-next-month`]: cell.type === 'nextMonth', |
101 | [`${prefixCls}-cell-week-label`]: cell.type === 'weekLabel', | 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
src/components/date-picker/base/month-table.vue
@@ -38,14 +38,16 @@ | @@ -38,14 +38,16 @@ | ||
38 | 38 | ||
39 | const tableYear = this.tableDate.getFullYear(); | 39 | const tableYear = this.tableDate.getFullYear(); |
40 | const selectedDays = this.dates.filter(Boolean).map(date => clearHours(new Date(date.getFullYear(), date.getMonth(), 1))); | 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 | for (let i = 0; i < 12; i++) { | 43 | for (let i = 0; i < 12; i++) { |
43 | const cell = deepCopy(cell_tmpl); | 44 | const cell = deepCopy(cell_tmpl); |
44 | cell.date = new Date(tableYear, i, 1); | 45 | cell.date = new Date(tableYear, i, 1); |
45 | cell.text = this.tCell(i + 1); | 46 | cell.text = this.tCell(i + 1); |
46 | - const time = clearHours(cell.date); | 47 | + const day = clearHours(cell.date); |
47 | cell.disabled = typeof this.disabledDate === 'function' && this.disabledDate(cell.date) && this.selectionMode === 'month'; | 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 | cells.push(cell); | 51 | cells.push(cell); |
50 | } | 52 | } |
51 | 53 | ||
@@ -59,6 +61,7 @@ | @@ -59,6 +61,7 @@ | ||
59 | { | 61 | { |
60 | [`${prefixCls}-cell-selected`]: cell.selected, | 62 | [`${prefixCls}-cell-selected`]: cell.selected, |
61 | [`${prefixCls}-cell-disabled`]: cell.disabled, | 63 | [`${prefixCls}-cell-disabled`]: cell.disabled, |
64 | + [`${prefixCls}-cell-focused`]: cell.focused, | ||
62 | [`${prefixCls}-cell-range`]: cell.range && !cell.start && !cell.end | 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,8 +22,10 @@ | ||
22 | import { deepCopy, scrollTop, firstUpperCase } from '../../../utils/assist'; | 22 | import { deepCopy, scrollTop, firstUpperCase } from '../../../utils/assist'; |
23 | 23 | ||
24 | const prefixCls = 'ivu-time-picker-cells'; | 24 | const prefixCls = 'ivu-time-picker-cells'; |
25 | + const timeParts = ['hours', 'minutes', 'seconds']; | ||
25 | 26 | ||
26 | export default { | 27 | export default { |
28 | + name: 'TimeSpinner', | ||
27 | mixins: [Options], | 29 | mixins: [Options], |
28 | props: { | 30 | props: { |
29 | hours: { | 31 | hours: { |
@@ -51,7 +53,9 @@ | @@ -51,7 +53,9 @@ | ||
51 | return { | 53 | return { |
52 | spinerSteps: [1, 1, 1].map((one, i) => Math.abs(this.steps[i]) || one), | 54 | spinerSteps: [1, 1, 1].map((one, i) => Math.abs(this.steps[i]) || one), |
53 | prefixCls: prefixCls, | 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 | computed: { | 61 | computed: { |
@@ -66,6 +70,7 @@ | @@ -66,6 +70,7 @@ | ||
66 | hoursList () { | 70 | hoursList () { |
67 | let hours = []; | 71 | let hours = []; |
68 | const step = this.spinerSteps[0]; | 72 | const step = this.spinerSteps[0]; |
73 | + const focusedHour = this.focusedColumn === 0 && this.focusedTime[0]; | ||
69 | const hour_tmpl = { | 74 | const hour_tmpl = { |
70 | text: 0, | 75 | text: 0, |
71 | selected: false, | 76 | selected: false, |
@@ -76,6 +81,7 @@ | @@ -76,6 +81,7 @@ | ||
76 | for (let i = 0; i < 24; i += step) { | 81 | for (let i = 0; i < 24; i += step) { |
77 | const hour = deepCopy(hour_tmpl); | 82 | const hour = deepCopy(hour_tmpl); |
78 | hour.text = i; | 83 | hour.text = i; |
84 | + hour.focused = i === focusedHour; | ||
79 | 85 | ||
80 | if (this.disabledHours.length && this.disabledHours.indexOf(i) > -1) { | 86 | if (this.disabledHours.length && this.disabledHours.indexOf(i) > -1) { |
81 | hour.disabled = true; | 87 | hour.disabled = true; |
@@ -90,6 +96,7 @@ | @@ -90,6 +96,7 @@ | ||
90 | minutesList () { | 96 | minutesList () { |
91 | let minutes = []; | 97 | let minutes = []; |
92 | const step = this.spinerSteps[1]; | 98 | const step = this.spinerSteps[1]; |
99 | + const focusedMinute = this.focusedColumn === 1 && this.focusedTime[1]; | ||
93 | const minute_tmpl = { | 100 | const minute_tmpl = { |
94 | text: 0, | 101 | text: 0, |
95 | selected: false, | 102 | selected: false, |
@@ -100,6 +107,7 @@ | @@ -100,6 +107,7 @@ | ||
100 | for (let i = 0; i < 60; i += step) { | 107 | for (let i = 0; i < 60; i += step) { |
101 | const minute = deepCopy(minute_tmpl); | 108 | const minute = deepCopy(minute_tmpl); |
102 | minute.text = i; | 109 | minute.text = i; |
110 | + minute.focused = i === focusedMinute; | ||
103 | 111 | ||
104 | if (this.disabledMinutes.length && this.disabledMinutes.indexOf(i) > -1) { | 112 | if (this.disabledMinutes.length && this.disabledMinutes.indexOf(i) > -1) { |
105 | minute.disabled = true; | 113 | minute.disabled = true; |
@@ -113,6 +121,7 @@ | @@ -113,6 +121,7 @@ | ||
113 | secondsList () { | 121 | secondsList () { |
114 | let seconds = []; | 122 | let seconds = []; |
115 | const step = this.spinerSteps[2]; | 123 | const step = this.spinerSteps[2]; |
124 | + const focusedMinute = this.focusedColumn === 2 && this.focusedTime[2]; | ||
116 | const second_tmpl = { | 125 | const second_tmpl = { |
117 | text: 0, | 126 | text: 0, |
118 | selected: false, | 127 | selected: false, |
@@ -123,6 +132,7 @@ | @@ -123,6 +132,7 @@ | ||
123 | for (let i = 0; i < 60; i += step) { | 132 | for (let i = 0; i < 60; i += step) { |
124 | const second = deepCopy(second_tmpl); | 133 | const second = deepCopy(second_tmpl); |
125 | second.text = i; | 134 | second.text = i; |
135 | + second.focused = i === focusedMinute; | ||
126 | 136 | ||
127 | if (this.disabledSeconds.length && this.disabledSeconds.indexOf(i) > -1) { | 137 | if (this.disabledSeconds.length && this.disabledSeconds.indexOf(i) > -1) { |
128 | second.disabled = true; | 138 | second.disabled = true; |
@@ -141,15 +151,32 @@ | @@ -141,15 +151,32 @@ | ||
141 | `${prefixCls}-cell`, | 151 | `${prefixCls}-cell`, |
142 | { | 152 | { |
143 | [`${prefixCls}-cell-selected`]: cell.selected, | 153 | [`${prefixCls}-cell-selected`]: cell.selected, |
154 | + [`${prefixCls}-cell-focused`]: cell.focused, | ||
144 | [`${prefixCls}-cell-disabled`]: cell.disabled | 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 | handleClick (type, cell) { | 173 | handleClick (type, cell) { |
149 | if (cell.disabled) return; | 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 | this.$emit('on-pick-click'); | 180 | this.$emit('on-pick-click'); |
154 | }, | 181 | }, |
155 | scroll (type, index) { | 182 | scroll (type, index) { |
@@ -168,15 +195,19 @@ | @@ -168,15 +195,19 @@ | ||
168 | return index; | 195 | return index; |
169 | }, | 196 | }, |
170 | updateScroll () { | 197 | updateScroll () { |
171 | - const times = ['hours', 'minutes', 'seconds']; | ||
172 | this.$nextTick(() => { | 198 | this.$nextTick(() => { |
173 | - times.forEach(type => { | 199 | + timeParts.forEach(type => { |
174 | this.$refs[type].scrollTop = 24 * this[`${type}List`].findIndex(obj => obj.text == this[type]); | 200 | this.$refs[type].scrollTop = 24 * this[`${type}List`].findIndex(obj => obj.text == this[type]); |
175 | }); | 201 | }); |
176 | }); | 202 | }); |
177 | }, | 203 | }, |
178 | formatTime (text) { | 204 | formatTime (text) { |
179 | return text < 10 ? '0' + text : text; | 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 | watch: { | 213 | watch: { |
@@ -191,6 +222,13 @@ | @@ -191,6 +222,13 @@ | ||
191 | seconds (val) { | 222 | seconds (val) { |
192 | if (!this.compiled) return; | 223 | if (!this.compiled) return; |
193 | this.scroll('seconds', this.secondsList.findIndex(obj => obj.text == val)); | 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 | mounted () { | 234 | mounted () { |
src/components/date-picker/base/year-table.vue
@@ -39,13 +39,15 @@ | @@ -39,13 +39,15 @@ | ||
39 | }; | 39 | }; |
40 | 40 | ||
41 | const selectedDays = this.dates.filter(Boolean).map(date => clearHours(new Date(date.getFullYear(), 0, 1))); | 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 | for (let i = 0; i < 10; i++) { | 44 | for (let i = 0; i < 10; i++) { |
44 | const cell = deepCopy(cell_tmpl); | 45 | const cell = deepCopy(cell_tmpl); |
45 | cell.date = new Date(this.startYear + i, 0, 1); | 46 | cell.date = new Date(this.startYear + i, 0, 1); |
46 | cell.disabled = typeof this.disabledDate === 'function' && this.disabledDate(cell.date) && this.selectionMode === 'year'; | 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 | cells.push(cell); | 51 | cells.push(cell); |
50 | } | 52 | } |
51 | 53 | ||
@@ -59,6 +61,7 @@ | @@ -59,6 +61,7 @@ | ||
59 | { | 61 | { |
60 | [`${prefixCls}-cell-selected`]: cell.selected, | 62 | [`${prefixCls}-cell-selected`]: cell.selected, |
61 | [`${prefixCls}-cell-disabled`]: cell.disabled, | 63 | [`${prefixCls}-cell-disabled`]: cell.disabled, |
64 | + [`${prefixCls}-cell-focused`]: cell.focused, | ||
62 | [`${prefixCls}-cell-range`]: cell.range && !cell.start && !cell.end | 65 | [`${prefixCls}-cell-range`]: cell.range && !cell.start && !cell.end |
63 | } | 66 | } |
64 | ]; | 67 | ]; |
src/components/date-picker/panel/Date/date-panel-mixin.js
src/components/date-picker/panel/Date/date-range.vue
@@ -41,6 +41,8 @@ | @@ -41,6 +41,8 @@ | ||
41 | :range-state="rangeState" | 41 | :range-state="rangeState" |
42 | :show-week-numbers="showWeekNumbers" | 42 | :show-week-numbers="showWeekNumbers" |
43 | :value="preSelecting.left ? [dates[0]] : dates" | 43 | :value="preSelecting.left ? [dates[0]] : dates" |
44 | + :focused-date="focusedDate" | ||
45 | + | ||
44 | @on-change-range="handleChangeRange" | 46 | @on-change-range="handleChangeRange" |
45 | @on-pick="panelPickerHandlers.left" | 47 | @on-pick="panelPickerHandlers.left" |
46 | @on-pick-click="handlePickClick" | 48 | @on-pick-click="handlePickClick" |
@@ -80,6 +82,8 @@ | @@ -80,6 +82,8 @@ | ||
80 | :disabled-date="disabledDate" | 82 | :disabled-date="disabledDate" |
81 | :show-week-numbers="showWeekNumbers" | 83 | :show-week-numbers="showWeekNumbers" |
82 | :value="preSelecting.right ? [dates[dates.length - 1]] : dates" | 84 | :value="preSelecting.right ? [dates[dates.length - 1]] : dates" |
85 | + :focused-date="focusedDate" | ||
86 | + | ||
83 | @on-change-range="handleChangeRange" | 87 | @on-change-range="handleChangeRange" |
84 | @on-pick="panelPickerHandlers.right" | 88 | @on-pick="panelPickerHandlers.right" |
85 | @on-pick-click="handlePickClick"></component> | 89 | @on-pick-click="handlePickClick"></component> |
@@ -178,7 +182,7 @@ | @@ -178,7 +182,7 @@ | ||
178 | [prefixCls + '-body-time']: this.showTime, | 182 | [prefixCls + '-body-time']: this.showTime, |
179 | [prefixCls + '-body-date']: !this.showTime, | 183 | [prefixCls + '-body-date']: !this.showTime, |
180 | } | 184 | } |
181 | - ] | 185 | + ]; |
182 | }, | 186 | }, |
183 | leftDatePanelLabel(){ | 187 | leftDatePanelLabel(){ |
184 | return this.panelLabelConfig('left'); | 188 | return this.panelLabelConfig('left'); |
@@ -224,10 +228,7 @@ | @@ -224,10 +228,7 @@ | ||
224 | 228 | ||
225 | 229 | ||
226 | // set panels positioning | 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 | currentView(currentView){ | 233 | currentView(currentView){ |
233 | const leftMonth = this.leftPanelDate.getMonth(); | 234 | const leftMonth = this.leftPanelDate.getMonth(); |
@@ -246,6 +247,9 @@ | @@ -246,6 +247,9 @@ | ||
246 | }, | 247 | }, |
247 | selectionMode(type){ | 248 | selectionMode(type){ |
248 | this.currentView = type || 'range'; | 249 | this.currentView = type || 'range'; |
250 | + }, | ||
251 | + focusedDate(date){ | ||
252 | + this.setPanelDates(date || new Date()); | ||
249 | } | 253 | } |
250 | }, | 254 | }, |
251 | methods: { | 255 | methods: { |
@@ -254,6 +258,11 @@ | @@ -254,6 +258,11 @@ | ||
254 | this.leftPickerTable = `${this.currentView}-table`; | 258 | this.leftPickerTable = `${this.currentView}-table`; |
255 | this.rightPickerTable = `${this.currentView}-table`; | 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 | panelLabelConfig (direction) { | 266 | panelLabelConfig (direction) { |
258 | const locale = this.t('i.locale'); | 267 | const locale = this.t('i.locale'); |
259 | const datePanelLabel = this.t('i.datepicker.datePanelLabel'); | 268 | const datePanelLabel = this.t('i.datepicker.datePanelLabel'); |
src/components/date-picker/panel/Date/date.vue
@@ -39,6 +39,8 @@ | @@ -39,6 +39,8 @@ | ||
39 | :value="dates" | 39 | :value="dates" |
40 | :selection-mode="selectionMode" | 40 | :selection-mode="selectionMode" |
41 | :disabled-date="disabledDate" | 41 | :disabled-date="disabledDate" |
42 | + :focused-date="focusedDate" | ||
43 | + | ||
42 | @on-pick="panelPickerHandlers" | 44 | @on-pick="panelPickerHandlers" |
43 | @on-pick-click="handlePickClick" | 45 | @on-pick-click="handlePickClick" |
44 | ></component> | 46 | ></component> |
@@ -51,6 +53,8 @@ | @@ -51,6 +53,8 @@ | ||
51 | :format="format" | 53 | :format="format" |
52 | :time-disabled="timeDisabled" | 54 | :time-disabled="timeDisabled" |
53 | :disabled-date="disabledDate" | 55 | :disabled-date="disabledDate" |
56 | + :focused-date="focusedDate" | ||
57 | + | ||
54 | v-bind="timePickerOptions" | 58 | v-bind="timePickerOptions" |
55 | @on-pick="handlePick" | 59 | @on-pick="handlePick" |
56 | @on-pick-click="handlePickClick" | 60 | @on-pick-click="handlePickClick" |
@@ -150,7 +154,6 @@ | @@ -150,7 +154,6 @@ | ||
150 | }, | 154 | }, |
151 | currentView (currentView) { | 155 | currentView (currentView) { |
152 | this.$emit('on-selection-mode-change', currentView); | 156 | this.$emit('on-selection-mode-change', currentView); |
153 | - this.pickertable = this.getTableType(currentView); | ||
154 | 157 | ||
155 | if (this.currentView === 'time') { | 158 | if (this.currentView === 'time') { |
156 | this.$nextTick(() => { | 159 | this.$nextTick(() => { |
@@ -162,6 +165,13 @@ | @@ -162,6 +165,13 @@ | ||
162 | selectionMode(type){ | 165 | selectionMode(type){ |
163 | this.currentView = type; | 166 | this.currentView = type; |
164 | this.pickerTable = this.getTableType(type); | 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 | methods: { | 177 | methods: { |
src/components/date-picker/picker.vue
1 | <template> | 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 | <div ref="reference" :class="[prefixCls + '-rel']"> | 7 | <div ref="reference" :class="[prefixCls + '-rel']"> |
4 | <slot> | 8 | <slot> |
5 | <i-input | 9 | <i-input |
@@ -12,10 +16,14 @@ | @@ -12,10 +16,14 @@ | ||
12 | :placeholder="placeholder" | 16 | :placeholder="placeholder" |
13 | :value="visualValue" | 17 | :value="visualValue" |
14 | :name="name" | 18 | :name="name" |
19 | + ref="input" | ||
20 | + | ||
15 | @on-input-change="handleInputChange" | 21 | @on-input-change="handleInputChange" |
16 | @on-focus="handleFocus" | 22 | @on-focus="handleFocus" |
17 | @on-blur="handleBlur" | 23 | @on-blur="handleBlur" |
18 | @on-click="handleIconClick" | 24 | @on-click="handleIconClick" |
25 | + @click.native="handleFocus" | ||
26 | + @keydown.native="handleKeydown" | ||
19 | @mouseenter.native="handleInputMouseenter" | 27 | @mouseenter.native="handleInputMouseenter" |
20 | @mouseleave.native="handleInputMouseleave" | 28 | @mouseleave.native="handleInputMouseleave" |
21 | 29 | ||
@@ -48,6 +56,7 @@ | @@ -48,6 +56,7 @@ | ||
48 | :show-week-numbers="showWeekNumbers" | 56 | :show-week-numbers="showWeekNumbers" |
49 | :picker-type="type" | 57 | :picker-type="type" |
50 | :multiple="multiple" | 58 | :multiple="multiple" |
59 | + :focused-date="focusedDate" | ||
51 | 60 | ||
52 | :time-picker-options="timePickerOptions" | 61 | :time-picker-options="timePickerOptions" |
53 | 62 | ||
@@ -69,21 +78,49 @@ | @@ -69,21 +78,49 @@ | ||
69 | 78 | ||
70 | import iInput from '../../components/input/input.vue'; | 79 | import iInput from '../../components/input/input.vue'; |
71 | import Drop from '../../components/select/dropdown.vue'; | 80 | import Drop from '../../components/select/dropdown.vue'; |
72 | - import clickoutside from '../../directives/clickoutside'; | 81 | + import vClickOutside from 'v-click-outside-x/index'; |
73 | import TransferDom from '../../directives/transfer-dom'; | 82 | import TransferDom from '../../directives/transfer-dom'; |
74 | import { oneOf } from '../../utils/assist'; | 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 | import Emitter from '../../mixins/emitter'; | 86 | import Emitter from '../../mixins/emitter'; |
77 | 87 | ||
78 | const prefixCls = 'ivu-date-picker'; | 88 | const prefixCls = 'ivu-date-picker'; |
89 | + const pickerPrefixCls = 'ivu-picker'; | ||
79 | 90 | ||
80 | const isEmptyArray = val => val.reduce((isEmpty, str) => isEmpty && !str || (typeof str === 'string' && str.trim() === ''), true); | 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 | export default { | 120 | export default { |
83 | - name: 'CalendarPicker', | ||
84 | mixins: [ Emitter ], | 121 | mixins: [ Emitter ], |
85 | components: { iInput, Drop }, | 122 | components: { iInput, Drop }, |
86 | - directives: { clickoutside, TransferDom }, | 123 | + directives: { clickOutside: vClickOutside.directive, TransferDom }, |
87 | props: { | 124 | props: { |
88 | format: { | 125 | format: { |
89 | type: String | 126 | type: String |
@@ -172,6 +209,7 @@ | @@ -172,6 +209,7 @@ | ||
172 | const isRange = this.type.includes('range'); | 209 | const isRange = this.type.includes('range'); |
173 | const emptyArray = isRange ? [null, null] : [null]; | 210 | const emptyArray = isRange ? [null, null] : [null]; |
174 | const initialValue = isEmptyArray((isRange ? this.value : [this.value]) || []) ? emptyArray : this.parseDate(this.value); | 211 | const initialValue = isEmptyArray((isRange ? this.value : [this.value]) || []) ? emptyArray : this.parseDate(this.value); |
212 | + const focusedTime = initialValue.map(extractTime); | ||
175 | 213 | ||
176 | return { | 214 | return { |
177 | prefixCls: prefixCls, | 215 | prefixCls: prefixCls, |
@@ -181,10 +219,24 @@ | @@ -181,10 +219,24 @@ | ||
181 | disableClickOutSide: false, // fixed when click a date,trigger clickoutside to close picker | 219 | disableClickOutSide: false, // fixed when click a date,trigger clickoutside to close picker |
182 | disableCloseUnderTransfer: false, // transfer ๆจกๅผไธ๏ผ็นๅปDropไนไผ่งฆๅๅ ณ้ญ, | 220 | disableCloseUnderTransfer: false, // transfer ๆจกๅผไธ๏ผ็นๅปDropไนไผ่งฆๅๅ ณ้ญ, |
183 | selectionMode: this.onSelectionModeChange(this.type), | 221 | selectionMode: this.onSelectionModeChange(this.type), |
184 | - forceInputRerender: 1 | 222 | + forceInputRerender: 1, |
223 | + isFocused: false, | ||
224 | + focusedDate: initialValue[0] || 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 | computed: { | 234 | computed: { |
235 | + wrapperClasses(){ | ||
236 | + return [prefixCls, { | ||
237 | + [prefixCls + '-focused']: this.isFocused | ||
238 | + }]; | ||
239 | + }, | ||
188 | publicVModelValue(){ | 240 | publicVModelValue(){ |
189 | if (this.multiple){ | 241 | if (this.multiple){ |
190 | return this.internalValue.slice(); | 242 | return this.internalValue.slice(); |
@@ -232,32 +284,246 @@ | @@ -232,32 +284,246 @@ | ||
232 | handleTransferClick () { | 284 | handleTransferClick () { |
233 | if (this.transfer) this.disableCloseUnderTransfer = true; | 285 | if (this.transfer) this.disableCloseUnderTransfer = true; |
234 | }, | 286 | }, |
235 | - handleClose () { | 287 | + handleClose (e) { |
236 | if (this.disableCloseUnderTransfer) { | 288 | if (this.disableCloseUnderTransfer) { |
237 | this.disableCloseUnderTransfer = false; | 289 | this.disableCloseUnderTransfer = false; |
238 | return false; | 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 | this.disableClickOutSide = false; | 310 | this.disableClickOutSide = false; |
244 | }, | 311 | }, |
245 | - handleFocus () { | 312 | + handleFocus (e) { |
246 | if (this.readonly) return; | 313 | if (this.readonly) return; |
314 | + this.isFocused = true; | ||
315 | + if (e && e.type === 'focus') return; // just focus, don't open yet | ||
247 | this.visible = true; | 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 | this.onSelectionModeChange(this.type); | 329 | this.onSelectionModeChange(this.type); |
253 | this.internalValue = this.internalValue.slice(); // trigger panel watchers to reset views | 330 | this.internalValue = this.internalValue.slice(); // trigger panel watchers to reset views |
254 | this.reset(); | 331 | this.reset(); |
255 | this.$refs.pickerPanel.onToggleVisibility(false); | 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 | + this.onPick(this.focusedDate, false, 'date'); | ||
389 | + } | ||
390 | + } | ||
391 | + | ||
392 | + if (!arrows.includes(keyCode)) return; // ignore rest of keys | ||
393 | + | ||
394 | + // navigate times and dates | ||
395 | + if (this.focusedTime.active) e.preventDefault(); // to prevent cursor from moving | ||
396 | + this.navigateDatePanel(keyValueMapper[keyCode], e.shiftKey); | ||
397 | + }, | ||
258 | reset(){ | 398 | reset(){ |
259 | this.$refs.pickerPanel.reset && this.$refs.pickerPanel.reset(); | 399 | this.$refs.pickerPanel.reset && this.$refs.pickerPanel.reset(); |
260 | }, | 400 | }, |
401 | + navigateTimePanel(direction){ | ||
402 | + | ||
403 | + this.focusedTime.active = true; | ||
404 | + const horizontal = direction.match(/left|right/); | ||
405 | + const vertical = direction.match(/up|down/); | ||
406 | + const timePickers = findComponentsDownward(this, 'TimeSpinner'); | ||
407 | + | ||
408 | + const maxNrOfColumns = (timePickers[0].showSeconds ? 3 : 2) * timePickers.length; | ||
409 | + const column = (currentColumn => { | ||
410 | + const incremented = currentColumn + (horizontal ? (direction === 'left' ? -1 : 1) : 0); | ||
411 | + return (incremented + maxNrOfColumns) % maxNrOfColumns; | ||
412 | + })(this.focusedTime.column); | ||
413 | + | ||
414 | + const columnsPerPicker = maxNrOfColumns / timePickers.length; | ||
415 | + const pickerIndex = Math.floor(column / columnsPerPicker); | ||
416 | + const col = column % columnsPerPicker; | ||
417 | + | ||
418 | + | ||
419 | + if (horizontal){ | ||
420 | + const time = this.internalValue.map(extractTime); | ||
421 | + | ||
422 | + this.focusedTime = { | ||
423 | + ...this.focusedTime, | ||
424 | + column: column, | ||
425 | + time: time | ||
426 | + }; | ||
427 | + timePickers.forEach((instance, i) => { | ||
428 | + if (i === pickerIndex) instance.updateFocusedTime(col, time[pickerIndex]); | ||
429 | + else instance.updateFocusedTime(-1, instance.focusedTime); | ||
430 | + }); | ||
431 | + } | ||
432 | + | ||
433 | + if (vertical){ | ||
434 | + const increment = direction === 'up' ? 1 : -1; | ||
435 | + const timeParts = ['hours', 'minutes', 'seconds']; | ||
436 | + | ||
437 | + | ||
438 | + const pickerPossibleValues = timePickers[pickerIndex][`${timeParts[col]}List`]; | ||
439 | + const nextIndex = pickerPossibleValues.findIndex(({text}) => this.focusedTime.time[pickerIndex][col] === text) + increment; | ||
440 | + const nextValue = pickerPossibleValues[nextIndex % pickerPossibleValues.length].text; | ||
441 | + const times = this.focusedTime.time.map((time, i) => { | ||
442 | + if (i !== pickerIndex) return time; | ||
443 | + time[col] = nextValue; | ||
444 | + return time; | ||
445 | + }); | ||
446 | + this.focusedTime = { | ||
447 | + ...this.focusedTime, | ||
448 | + time: times | ||
449 | + }; | ||
450 | + | ||
451 | + timePickers.forEach((instance, i) => { | ||
452 | + if (i === pickerIndex) instance.updateFocusedTime(col, times[i]); | ||
453 | + else instance.updateFocusedTime(-1, instance.focusedTime); | ||
454 | + }); | ||
455 | + } | ||
456 | + }, | ||
457 | + navigateDatePanel(direction, shift){ | ||
458 | + | ||
459 | + const timePickers = findComponentsDownward(this, 'TimeSpinner'); | ||
460 | + if (timePickers.length > 0) { | ||
461 | + // we are in TimePicker mode | ||
462 | + this.navigateTimePanel(direction, shift, timePickers); | ||
463 | + return; | ||
464 | + } | ||
465 | + | ||
466 | + if (shift){ | ||
467 | + if (this.type === 'year'){ | ||
468 | + this.focusedDate = new Date( | ||
469 | + this.focusedDate.getFullYear() + mapPossibleValues(direction, 0, 10), | ||
470 | + this.focusedDate.getMonth(), | ||
471 | + this.focusedDate.getDate() | ||
472 | + ); | ||
473 | + } else { | ||
474 | + this.focusedDate = new Date( | ||
475 | + this.focusedDate.getFullYear() + mapPossibleValues(direction, 0, 1), | ||
476 | + this.focusedDate.getMonth() + mapPossibleValues(direction, 1, 0), | ||
477 | + this.focusedDate.getDate() | ||
478 | + ); | ||
479 | + } | ||
480 | + | ||
481 | + const position = direction.match(/left|down/) ? 'prev' : 'next'; | ||
482 | + const double = direction.match(/up|down/) ? '-double' : ''; | ||
483 | + | ||
484 | + // pulse button | ||
485 | + const button = this.$refs.drop.$el.querySelector(`.ivu-date-picker-${position}-btn-arrow${double}`); | ||
486 | + if (button) pulseElement(button); | ||
487 | + return; | ||
488 | + } | ||
489 | + | ||
490 | + const initialDate = this.focusedDate || (this.internalValue && this.internalValue[0]) || new Date(); | ||
491 | + const focusedDate = new Date(initialDate); | ||
492 | + | ||
493 | + if (this.type.match(/^date/)){ | ||
494 | + const lastOfMonth = getDayCountOfMonth(initialDate.getFullYear(), initialDate.getMonth()); | ||
495 | + const startDay = initialDate.getDate(); | ||
496 | + const nextDay = focusedDate.getDate() + mapPossibleValues(direction, 1, 7); | ||
497 | + | ||
498 | + if (nextDay < 1) { | ||
499 | + if (direction.match(/left|right/)) { | ||
500 | + focusedDate.setMonth(focusedDate.getMonth() + 1); | ||
501 | + focusedDate.setDate(nextDay); | ||
502 | + } else { | ||
503 | + focusedDate.setDate(startDay + Math.floor((lastOfMonth - startDay) / 7) * 7); | ||
504 | + } | ||
505 | + } else if (nextDay > lastOfMonth){ | ||
506 | + if (direction.match(/left|right/)) { | ||
507 | + focusedDate.setMonth(focusedDate.getMonth() - 1); | ||
508 | + focusedDate.setDate(nextDay); | ||
509 | + } else { | ||
510 | + focusedDate.setDate(startDay % 7); | ||
511 | + } | ||
512 | + } else { | ||
513 | + focusedDate.setDate(nextDay); | ||
514 | + } | ||
515 | + } | ||
516 | + | ||
517 | + if (this.type.match(/^month/)) { | ||
518 | + focusedDate.setMonth(focusedDate.getMonth() + mapPossibleValues(direction, 1, 3)); | ||
519 | + } | ||
520 | + | ||
521 | + if (this.type.match(/^year/)) { | ||
522 | + focusedDate.setFullYear(focusedDate.getFullYear() + mapPossibleValues(direction, 1, 3)); | ||
523 | + } | ||
524 | + | ||
525 | + this.focusedDate = focusedDate; | ||
526 | + }, | ||
261 | handleInputChange (event) { | 527 | handleInputChange (event) { |
262 | const isArrayValue = this.type.includes('range') || this.multiple; | 528 | const isArrayValue = this.type.includes('range') || this.multiple; |
263 | const oldValue = this.visualValue; | 529 | const oldValue = this.visualValue; |
@@ -377,6 +643,12 @@ | @@ -377,6 +643,12 @@ | ||
377 | this.internalValue = Array.isArray(dates) ? dates : [dates]; | 643 | this.internalValue = Array.isArray(dates) ? dates : [dates]; |
378 | } | 644 | } |
379 | 645 | ||
646 | + this.focusedDate = this.internalValue[0]; | ||
647 | + this.focusedTime = { | ||
648 | + ...this.focusedTime, | ||
649 | + time: this.internalValue.map(extractTime) | ||
650 | + }; | ||
651 | + | ||
380 | if (!this.isConfirm) this.onSelectionModeChange(this.type); // reset the selectionMode | 652 | if (!this.isConfirm) this.onSelectionModeChange(this.type); // reset the selectionMode |
381 | if (!this.isConfirm) this.visible = visible; | 653 | if (!this.isConfirm) this.visible = visible; |
382 | this.emitChange(type); | 654 | this.emitChange(type); |
@@ -384,22 +656,23 @@ | @@ -384,22 +656,23 @@ | ||
384 | onPickSuccess(){ | 656 | onPickSuccess(){ |
385 | this.visible = false; | 657 | this.visible = false; |
386 | this.$emit('on-ok'); | 658 | this.$emit('on-ok'); |
659 | + this.focus(); | ||
387 | this.reset(); | 660 | this.reset(); |
388 | }, | 661 | }, |
662 | + focus() { | ||
663 | + this.$refs.input.focus(); | ||
664 | + } | ||
389 | }, | 665 | }, |
390 | watch: { | 666 | watch: { |
391 | visible (state) { | 667 | visible (state) { |
392 | if (state === false){ | 668 | if (state === false){ |
393 | this.$refs.drop.destroy(); | 669 | this.$refs.drop.destroy(); |
394 | - const input = this.$el.querySelector('input'); | ||
395 | - if (input) input.blur(); | ||
396 | } | 670 | } |
397 | this.$refs.drop.update(); | 671 | this.$refs.drop.update(); |
398 | this.$emit('on-open-change', state); | 672 | this.$emit('on-open-change', state); |
399 | }, | 673 | }, |
400 | value(val) { | 674 | value(val) { |
401 | this.internalValue = this.parseDate(val); | 675 | this.internalValue = this.parseDate(val); |
402 | - | ||
403 | }, | 676 | }, |
404 | open (val) { | 677 | open (val) { |
405 | this.visible = val === true; | 678 | this.visible = val === true; |
@@ -421,6 +694,9 @@ | @@ -421,6 +694,9 @@ | ||
421 | this.$emit('input', this.publicVModelValue); // to update v-model | 694 | this.$emit('input', this.publicVModelValue); // to update v-model |
422 | } | 695 | } |
423 | if (this.open !== null) this.visible = this.open; | 696 | if (this.open !== null) this.visible = this.open; |
697 | + | ||
698 | + // to handle focus from confirm buttons | ||
699 | + this.$on('focus-input', () => this.focus()); | ||
424 | } | 700 | } |
425 | }; | 701 | }; |
426 | </script> | 702 | </script> |
src/components/date-picker/picker/date-picker.js
@@ -5,6 +5,7 @@ import RangeDatePickerPanel from '../panel/Date/date-range.vue'; | @@ -5,6 +5,7 @@ import RangeDatePickerPanel from '../panel/Date/date-range.vue'; | ||
5 | import { oneOf } from '../../../utils/assist'; | 5 | import { oneOf } from '../../../utils/assist'; |
6 | 6 | ||
7 | export default { | 7 | export default { |
8 | + name: 'CalendarPicker', | ||
8 | mixins: [Picker], | 9 | mixins: [Picker], |
9 | props: { | 10 | props: { |
10 | type: { | 11 | type: { |
src/components/date-picker/picker/time-picker.js
@@ -3,7 +3,7 @@ import TimePickerPanel from '../panel/Time/time.vue'; | @@ -3,7 +3,7 @@ import TimePickerPanel from '../panel/Time/time.vue'; | ||
3 | import RangeTimePickerPanel from '../panel/Time/time-range.vue'; | 3 | import RangeTimePickerPanel from '../panel/Time/time-range.vue'; |
4 | import Options from '../time-mixins'; | 4 | import Options from '../time-mixins'; |
5 | 5 | ||
6 | -import { oneOf } from '../../../utils/assist'; | 6 | +import { findComponentsDownward, oneOf } from '../../../utils/assist'; |
7 | 7 | ||
8 | export default { | 8 | export default { |
9 | mixins: [Picker, Options], | 9 | mixins: [Picker, Options], |
@@ -30,4 +30,14 @@ export default { | @@ -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,18 @@ | @@ -44,17 +44,18 @@ | ||
44 | margin: 2px; | 44 | margin: 2px; |
45 | color: @btn-disable-color; | 45 | color: @btn-disable-color; |
46 | } | 46 | } |
47 | + &-cell:hover, &-focused{ | ||
48 | + em{ | ||
49 | + background: @date-picker-cell-hover-bg; | ||
50 | + } | ||
51 | + } | ||
52 | + | ||
47 | &-cell{ | 53 | &-cell{ |
48 | span&{ | 54 | span&{ |
49 | width: 28px; | 55 | width: 28px; |
50 | height: 28px; | 56 | height: 28px; |
51 | cursor: pointer; | 57 | cursor: pointer; |
52 | } | 58 | } |
53 | - &:hover{ | ||
54 | - em{ | ||
55 | - background: @date-picker-cell-hover-bg; | ||
56 | - } | ||
57 | - } | ||
58 | &-prev-month,&-next-month{ | 59 | &-prev-month,&-next-month{ |
59 | em{ | 60 | em{ |
60 | color: @btn-disable-color; | 61 | color: @btn-disable-color; |
@@ -154,6 +155,11 @@ | @@ -154,6 +155,11 @@ | ||
154 | margin: 0; | 155 | margin: 0; |
155 | } | 156 | } |
156 | } | 157 | } |
158 | + | ||
159 | + .@{date-picker-prefix-cls}-cells-cell-focused{ | ||
160 | + background-color: tint(@primary-color, 80%); | ||
161 | + } | ||
162 | + | ||
157 | } | 163 | } |
158 | 164 | ||
159 | &-header{ | 165 | &-header{ |
@@ -169,6 +175,11 @@ | @@ -169,6 +175,11 @@ | ||
169 | } | 175 | } |
170 | } | 176 | } |
171 | } | 177 | } |
178 | + &-btn-pulse{ | ||
179 | + background-color: tint(@primary-color, 80%) !important; | ||
180 | + border-radius: @border-radius-small; | ||
181 | + transition: background-color @transition-time @ease-in-out; | ||
182 | + } | ||
172 | &-prev-btn{ | 183 | &-prev-btn{ |
173 | float: left; | 184 | float: left; |
174 | &-arrow-double{ | 185 | &-arrow-double{ |
@@ -216,6 +227,10 @@ | @@ -216,6 +227,10 @@ | ||
216 | max-height: none; | 227 | max-height: none; |
217 | width: auto; | 228 | width: auto; |
218 | } | 229 | } |
230 | + | ||
231 | + &-focused input{ | ||
232 | + .active(); | ||
233 | + } | ||
219 | } | 234 | } |
220 | 235 | ||
221 | .@{picker-prefix-cls} { | 236 | .@{picker-prefix-cls} { |
@@ -289,9 +304,9 @@ | @@ -289,9 +304,9 @@ | ||
289 | color: @link-active-color; | 304 | color: @link-active-color; |
290 | } | 305 | } |
291 | } | 306 | } |
292 | - & > span&-time-disabled{ | ||
293 | - color: @btn-disable-color; | ||
294 | - cursor: @cursor-disabled; | 307 | + |
308 | + &-time{ | ||
309 | + float: left; | ||
295 | } | 310 | } |
296 | } | 311 | } |
297 | } | 312 | } |
src/styles/components/time-picker.less
@@ -70,6 +70,9 @@ | @@ -70,6 +70,9 @@ | ||
70 | color: @primary-color; | 70 | color: @primary-color; |
71 | background: @background-color-select-hover; | 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,4 +168,4 @@ | ||
165 | } | 168 | } |
166 | } | 169 | } |
167 | } | 170 | } |
168 | -} | ||
169 | \ No newline at end of file | 171 | \ No newline at end of file |
172 | +} |
test/unit/specs/date-picker.spec.js
@@ -116,7 +116,7 @@ describe('DatePicker.vue', () => { | @@ -116,7 +116,7 @@ describe('DatePicker.vue', () => { | ||
116 | `); | 116 | `); |
117 | 117 | ||
118 | const picker = vm.$children[0]; | 118 | const picker = vm.$children[0]; |
119 | - picker.handleIconClick(); | 119 | + picker.handleFocus({type: 'focus'}); |
120 | vm.$nextTick(() => { | 120 | vm.$nextTick(() => { |
121 | const displayField = vm.$el.querySelector('.ivu-input'); | 121 | const displayField = vm.$el.querySelector('.ivu-input'); |
122 | const clickableCells = vm.$el.querySelectorAll('.ivu-date-picker-cells-cell'); | 122 | const clickableCells = vm.$el.querySelectorAll('.ivu-date-picker-cells-cell'); |
@@ -169,7 +169,7 @@ describe('DatePicker.vue', () => { | @@ -169,7 +169,7 @@ describe('DatePicker.vue', () => { | ||
169 | }); | 169 | }); |
170 | 170 | ||
171 | const picker = vm.$children[0]; | 171 | const picker = vm.$children[0]; |
172 | - picker.handleIconClick(); | 172 | + picker.handleFocus({type: 'focus'}); |
173 | vm.$nextTick(() => { | 173 | vm.$nextTick(() => { |
174 | const panel = vm.$el.querySelector('.ivu-picker-panel-content'); | 174 | const panel = vm.$el.querySelector('.ivu-picker-panel-content'); |
175 | const dayPanel = panel.querySelector('[class="ivu-date-picker-cells"]'); | 175 | const dayPanel = panel.querySelector('[class="ivu-date-picker-cells"]'); |
@@ -243,7 +243,7 @@ describe('DatePicker.vue', () => { | @@ -243,7 +243,7 @@ describe('DatePicker.vue', () => { | ||
243 | `); | 243 | `); |
244 | 244 | ||
245 | const picker = vm.$children[0]; | 245 | const picker = vm.$children[0]; |
246 | - picker.handleIconClick(); | 246 | + picker.handleFocus({type: 'focus'}); |
247 | vm.$nextTick(() => { | 247 | vm.$nextTick(() => { |
248 | const displayField = vm.$el.querySelector('.ivu-input'); | 248 | const displayField = vm.$el.querySelector('.ivu-input'); |
249 | const clickableCells = vm.$el.querySelectorAll('.ivu-date-picker-cells-cell'); | 249 | const clickableCells = vm.$el.querySelectorAll('.ivu-date-picker-cells-cell'); |
@@ -266,9 +266,11 @@ describe('DatePicker.vue', () => { | @@ -266,9 +266,11 @@ describe('DatePicker.vue', () => { | ||
266 | // it should be closed by now | 266 | // it should be closed by now |
267 | expect(picker.visible).to.equal(false); | 267 | expect(picker.visible).to.equal(false); |
268 | // open picker again | 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 | expect(picker.visible).to.equal(true); | 274 | expect(picker.visible).to.equal(true); |
273 | expect(JSON.stringify(picker.internalValue)).to.equal('[null,null]'); | 275 | expect(JSON.stringify(picker.internalValue)).to.equal('[null,null]'); |
274 | expect(displayField.value).to.equal(''); | 276 | expect(displayField.value).to.equal(''); |
@@ -355,7 +357,7 @@ describe('DatePicker.vue', () => { | @@ -355,7 +357,7 @@ describe('DatePicker.vue', () => { | ||
355 | `); | 357 | `); |
356 | 358 | ||
357 | const picker = vm.$children[0]; | 359 | const picker = vm.$children[0]; |
358 | - picker.handleIconClick(); | 360 | + picker.handleFocus({type: 'focus'}); |
359 | vm.$nextTick(() => { | 361 | vm.$nextTick(() => { |
360 | const now = new Date(); | 362 | const now = new Date(); |
361 | const labels = vm.$el.querySelectorAll('.ivu-picker-panel-body .ivu-date-picker-header-label'); | 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('TimePicker.vue', () => { | @@ -11,7 +11,7 @@ describe('TimePicker.vue', () => { | ||
11 | <Time-Picker></Time-Picker> | 11 | <Time-Picker></Time-Picker> |
12 | `); | 12 | `); |
13 | const picker = vm.$children[0]; | 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 | vm.$nextTick(() => { | 16 | vm.$nextTick(() => { |
17 | const spiners = picker.$el.querySelectorAll('.ivu-time-picker-cells-list'); | 17 | const spiners = picker.$el.querySelectorAll('.ivu-time-picker-cells-list'); |
@@ -28,7 +28,7 @@ describe('TimePicker.vue', () => { | @@ -28,7 +28,7 @@ describe('TimePicker.vue', () => { | ||
28 | <Time-Picker format="HH:mm"></Time-Picker> | 28 | <Time-Picker format="HH:mm"></Time-Picker> |
29 | `); | 29 | `); |
30 | const picker = vm.$children[0]; | 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 | vm.$nextTick(() => { | 33 | vm.$nextTick(() => { |
34 | const spiners = picker.$el.querySelectorAll('.ivu-time-picker-cells-list'); | 34 | const spiners = picker.$el.querySelectorAll('.ivu-time-picker-cells-list'); |
@@ -44,7 +44,7 @@ describe('TimePicker.vue', () => { | @@ -44,7 +44,7 @@ describe('TimePicker.vue', () => { | ||
44 | <Time-Picker :steps="[1, 15]"></Time-Picker> | 44 | <Time-Picker :steps="[1, 15]"></Time-Picker> |
45 | `); | 45 | `); |
46 | const picker = vm.$children[0]; | 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 | vm.$nextTick(() => { | 49 | vm.$nextTick(() => { |
50 | const spiners = picker.$el.querySelectorAll('.ivu-time-picker-cells-list'); | 50 | const spiners = picker.$el.querySelectorAll('.ivu-time-picker-cells-list'); |