Merge pull request #3643 from SergioCrisostomo/datepicker-keyboard

Datepicker keyboard
This commit is contained in:
Aresn 2018-05-21 15:27:09 +08:00 committed by GitHub
commit bdb26ef7c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 494 additions and 77 deletions

View file

@ -249,14 +249,17 @@
<template> <template>
<div style="width: 500px;margin: 100px;"> <div style="width: 500px;margin: 100px;">
<Row> <p><input type="text"></p>
<Col span="12">
<DatePicker type="date" show-week-numbers placeholder="Select date" style="width: 200px"></DatePicker> <DatePicker type="month" show-week-numbers placeholder="Select date" style="width: 200px"></DatePicker>
</Col> <DatePicker type="year" show-week-numbers placeholder="Select date" style="width: 200px"></DatePicker>
<Col span="12">
<DatePicker type="daterange" show-week-numbers placement="bottom-end" placeholder="Select date" style="width: 200px"></DatePicker> <DatePicker type="date" transfer show-week-numbers placeholder="Select date" style="width: 400px"></DatePicker>
</Col> <DatePicker type="datetime" show-week-numbers confirm placeholder="Select date" style="width: 400px"></DatePicker>
</Row>
<DatePicker type="daterange" transfer show-week-numbers placeholder="Select date" style="width: 400px"></DatePicker>
<DatePicker type="datetimerange" transfer show-week-numbers placeholder="Select date" style="width: 400px"></DatePicker>
<Time-Picker :steps="[1, 1, 15]" :value="new Date()"></Time-Picker>
</div> </div>
</template> </template>
<script> <script>

View file

@ -1,37 +1,47 @@
<template> <template>
<div :class="[prefixCls + '-confirm']"> <div :class="[prefixCls + '-confirm']" @keydown.tab.capture="handleTab">
<span :class="timeClasses" v-if="showTime" @click="handleToggleTime"> <i-button :class="timeClasses" size="small" type="text" :disabled="timeDisabled" v-if="showTime" @click="handleToggleTime">
<template v-if="isTime">{{ t('i.datepicker.selectDate') }}</template> {{labels.time}}
<template v-else>{{ t('i.datepicker.selectTime') }}</template> </i-button>
</span> <i-button size="small" type="ghost" @click.native="handleClear" @keydown.enter.native="handleClear">
<i-button size="small" type="text" @click.native="handleClear">{{ t('i.datepicker.clear') }}</i-button> {{labels.clear}}
<i-button size="small" type="primary" @click.native="handleSuccess">{{ t('i.datepicker.ok') }}</i-button> </i-button>
<i-button size="small" type="primary" @click.native="handleSuccess" @keydown.enter.native="handleSuccess">
{{labels.ok}}
</i-button>
</div> </div>
</template> </template>
<script> <script>
import iButton from '../../button/button.vue'; import iButton from '../../button/button.vue';
import Locale from '../../../mixins/locale'; import Locale from '../../../mixins/locale';
import Emitter from '../../../mixins/emitter';
const prefixCls = 'ivu-picker'; const prefixCls = 'ivu-picker';
export default { export default {
mixins: [ Locale ], mixins: [Locale, Emitter],
components: { iButton }, components: {iButton},
props: { props: {
showTime: false, showTime: false,
isTime: false, isTime: false,
timeDisabled: false timeDisabled: false
}, },
data () { data() {
return { return {
prefixCls: prefixCls prefixCls: prefixCls
}; };
}, },
computed: { computed: {
timeClasses () { timeClasses () {
return { return `${prefixCls}-confirm-time`;
[`${prefixCls}-confirm-time-disabled`]: this.timeDisabled },
}; labels(){
const labels = ['time', 'clear', 'ok'];
const values = [(this.isTime ? 'selectDate' : 'selectTime'), 'clear', 'ok'];
return labels.reduce((obj, key, i) => {
obj[key] = this.t('i.datepicker.' + values[i]);
return obj;
}, {});
} }
}, },
methods: { methods: {
@ -44,6 +54,17 @@
handleToggleTime () { handleToggleTime () {
if (this.timeDisabled) return; if (this.timeDisabled) return;
this.$emit('on-pick-toggle-time'); this.$emit('on-pick-toggle-time');
this.dispatch('CalendarPicker', 'focus-input');
},
handleTab(e) {
const tabbables = [...this.$el.children];
const expectedFocus = tabbables[e.shiftKey ? 'shift' : 'pop']();
if (document.activeElement === expectedFocus) {
e.preventDefault();
e.stopPropagation();
this.dispatch('CalendarPicker', 'focus-input');
}
} }
} }
}; };

View file

@ -7,9 +7,9 @@
</div> </div>
<span <span
:class="getCellCls(cell)" :class="getCellCls(cell)"
v-for="(cell, i) in readCells" v-for="(cell, i) in cells"
:key="String(cell.date) + i" :key="String(cell.date) + i"
@click="handleClick(cell)" @click="handleClick(cell, $event)"
@mouseenter="handleMouseMove(cell)" @mouseenter="handleMouseMove(cell)"
> >
<em>{{ cell.desc }}</em> <em>{{ cell.desc }}</em>
@ -61,7 +61,7 @@
const weekDays = translatedDays.splice(weekStartDay, 7 - weekStartDay).concat(translatedDays.splice(0, weekStartDay)); const weekDays = translatedDays.splice(weekStartDay, 7 - weekStartDay).concat(translatedDays.splice(0, weekStartDay));
return this.showWeekNumbers ? [''].concat(weekDays) : weekDays; return this.showWeekNumbers ? [''].concat(weekDays) : weekDays;
}, },
readCells () { cells () {
const tableYear = this.tableDate.getFullYear(); const tableYear = this.tableDate.getFullYear();
const tableMonth = this.tableDate.getMonth(); const tableMonth = this.tableDate.getMonth();
const today = clearHours(new Date()); // timestamp of today const today = clearHours(new Date()); // timestamp of today
@ -99,7 +99,9 @@
[`${prefixCls}-cell-prev-month`]: cell.type === 'prevMonth', [`${prefixCls}-cell-prev-month`]: cell.type === 'prevMonth',
[`${prefixCls}-cell-next-month`]: cell.type === 'nextMonth', [`${prefixCls}-cell-next-month`]: cell.type === 'nextMonth',
[`${prefixCls}-cell-week-label`]: cell.type === 'weekLabel', [`${prefixCls}-cell-week-label`]: cell.type === 'weekLabel',
[`${prefixCls}-cell-range`]: cell.range && !cell.start && !cell.end [`${prefixCls}-cell-range`]: cell.range && !cell.start && !cell.end,
[`${prefixCls}-focused`]: clearHours(cell.date) === clearHours(this.focusedDate)
} }
]; ];
}, },

View file

@ -2,6 +2,7 @@
import {clearHours} from '../util'; import {clearHours} from '../util';
export default { export default {
name: 'PanelTable',
props: { props: {
tableDate: { tableDate: {
type: Date, type: Date,
@ -26,7 +27,10 @@ export default {
selecting: false selecting: false
}) })
}, },
focusedDate: {
type: Date,
required: true,
}
}, },
computed: { computed: {
dates(){ dates(){

View file

@ -38,14 +38,16 @@
const tableYear = this.tableDate.getFullYear(); const tableYear = this.tableDate.getFullYear();
const selectedDays = this.dates.filter(Boolean).map(date => clearHours(new Date(date.getFullYear(), date.getMonth(), 1))); const selectedDays = this.dates.filter(Boolean).map(date => clearHours(new Date(date.getFullYear(), date.getMonth(), 1)));
const focusedDate = clearHours(new Date(this.focusedDate.getFullYear(), this.focusedDate.getMonth(), 1));
for (let i = 0; i < 12; i++) { for (let i = 0; i < 12; i++) {
const cell = deepCopy(cell_tmpl); const cell = deepCopy(cell_tmpl);
cell.date = new Date(tableYear, i, 1); cell.date = new Date(tableYear, i, 1);
cell.text = this.tCell(i + 1); cell.text = this.tCell(i + 1);
const time = clearHours(cell.date); const day = clearHours(cell.date);
cell.disabled = typeof this.disabledDate === 'function' && this.disabledDate(cell.date) && this.selectionMode === 'month'; cell.disabled = typeof this.disabledDate === 'function' && this.disabledDate(cell.date) && this.selectionMode === 'month';
cell.selected = selectedDays.includes(time); cell.selected = selectedDays.includes(day);
cell.focused = day === focusedDate;
cells.push(cell); cells.push(cell);
} }
@ -59,6 +61,7 @@
{ {
[`${prefixCls}-cell-selected`]: cell.selected, [`${prefixCls}-cell-selected`]: cell.selected,
[`${prefixCls}-cell-disabled`]: cell.disabled, [`${prefixCls}-cell-disabled`]: cell.disabled,
[`${prefixCls}-cell-focused`]: cell.focused,
[`${prefixCls}-cell-range`]: cell.range && !cell.start && !cell.end [`${prefixCls}-cell-range`]: cell.range && !cell.start && !cell.end
} }
]; ];

View file

@ -22,8 +22,10 @@
import { deepCopy, scrollTop, firstUpperCase } from '../../../utils/assist'; import { deepCopy, scrollTop, firstUpperCase } from '../../../utils/assist';
const prefixCls = 'ivu-time-picker-cells'; const prefixCls = 'ivu-time-picker-cells';
const timeParts = ['hours', 'minutes', 'seconds'];
export default { export default {
name: 'TimeSpinner',
mixins: [Options], mixins: [Options],
props: { props: {
hours: { hours: {
@ -51,7 +53,9 @@
return { return {
spinerSteps: [1, 1, 1].map((one, i) => Math.abs(this.steps[i]) || one), spinerSteps: [1, 1, 1].map((one, i) => Math.abs(this.steps[i]) || one),
prefixCls: prefixCls, prefixCls: prefixCls,
compiled: false compiled: false,
focusedColumn: -1, // which column inside the picker
focusedTime: [0, 0, 0] // the values array into [hh, mm, ss]
}; };
}, },
computed: { computed: {
@ -66,6 +70,7 @@
hoursList () { hoursList () {
let hours = []; let hours = [];
const step = this.spinerSteps[0]; const step = this.spinerSteps[0];
const focusedHour = this.focusedColumn === 0 && this.focusedTime[0];
const hour_tmpl = { const hour_tmpl = {
text: 0, text: 0,
selected: false, selected: false,
@ -76,6 +81,7 @@
for (let i = 0; i < 24; i += step) { for (let i = 0; i < 24; i += step) {
const hour = deepCopy(hour_tmpl); const hour = deepCopy(hour_tmpl);
hour.text = i; hour.text = i;
hour.focused = i === focusedHour;
if (this.disabledHours.length && this.disabledHours.indexOf(i) > -1) { if (this.disabledHours.length && this.disabledHours.indexOf(i) > -1) {
hour.disabled = true; hour.disabled = true;
@ -90,6 +96,7 @@
minutesList () { minutesList () {
let minutes = []; let minutes = [];
const step = this.spinerSteps[1]; const step = this.spinerSteps[1];
const focusedMinute = this.focusedColumn === 1 && this.focusedTime[1];
const minute_tmpl = { const minute_tmpl = {
text: 0, text: 0,
selected: false, selected: false,
@ -100,6 +107,7 @@
for (let i = 0; i < 60; i += step) { for (let i = 0; i < 60; i += step) {
const minute = deepCopy(minute_tmpl); const minute = deepCopy(minute_tmpl);
minute.text = i; minute.text = i;
minute.focused = i === focusedMinute;
if (this.disabledMinutes.length && this.disabledMinutes.indexOf(i) > -1) { if (this.disabledMinutes.length && this.disabledMinutes.indexOf(i) > -1) {
minute.disabled = true; minute.disabled = true;
@ -113,6 +121,7 @@
secondsList () { secondsList () {
let seconds = []; let seconds = [];
const step = this.spinerSteps[2]; const step = this.spinerSteps[2];
const focusedMinute = this.focusedColumn === 2 && this.focusedTime[2];
const second_tmpl = { const second_tmpl = {
text: 0, text: 0,
selected: false, selected: false,
@ -123,6 +132,7 @@
for (let i = 0; i < 60; i += step) { for (let i = 0; i < 60; i += step) {
const second = deepCopy(second_tmpl); const second = deepCopy(second_tmpl);
second.text = i; second.text = i;
second.focused = i === focusedMinute;
if (this.disabledSeconds.length && this.disabledSeconds.indexOf(i) > -1) { if (this.disabledSeconds.length && this.disabledSeconds.indexOf(i) > -1) {
second.disabled = true; second.disabled = true;
@ -141,15 +151,32 @@
`${prefixCls}-cell`, `${prefixCls}-cell`,
{ {
[`${prefixCls}-cell-selected`]: cell.selected, [`${prefixCls}-cell-selected`]: cell.selected,
[`${prefixCls}-cell-focused`]: cell.focused,
[`${prefixCls}-cell-disabled`]: cell.disabled [`${prefixCls}-cell-disabled`]: cell.disabled
} }
]; ];
}, },
chooseValue(values){
const changes = timeParts.reduce((obj, part, i) => {
const value = values[i];
if (this[part] === value) return obj;
return {
...obj,
[part]: value
};
}, {});
if (Object.keys(changes).length > 0) {
this.emitChange(changes);
}
},
handleClick (type, cell) { handleClick (type, cell) {
if (cell.disabled) return; if (cell.disabled) return;
const data = {}; const data = {[type]: cell.text};
data[type] = cell.text; this.emitChange(data);
this.$emit('on-change', data); },
emitChange(changes){
this.$emit('on-change', changes);
this.$emit('on-pick-click'); this.$emit('on-pick-click');
}, },
scroll (type, index) { scroll (type, index) {
@ -168,15 +195,19 @@
return index; return index;
}, },
updateScroll () { updateScroll () {
const times = ['hours', 'minutes', 'seconds'];
this.$nextTick(() => { this.$nextTick(() => {
times.forEach(type => { timeParts.forEach(type => {
this.$refs[type].scrollTop = 24 * this[`${type}List`].findIndex(obj => obj.text == this[type]); this.$refs[type].scrollTop = 24 * this[`${type}List`].findIndex(obj => obj.text == this[type]);
}); });
}); });
}, },
formatTime (text) { formatTime (text) {
return text < 10 ? '0' + text : text; return text < 10 ? '0' + text : text;
},
updateFocusedTime(col, time) {
this.focusedColumn = col;
this.focusedTime = time.slice();
} }
}, },
watch: { watch: {
@ -191,6 +222,13 @@
seconds (val) { seconds (val) {
if (!this.compiled) return; if (!this.compiled) return;
this.scroll('seconds', this.secondsList.findIndex(obj => obj.text == val)); this.scroll('seconds', this.secondsList.findIndex(obj => obj.text == val));
},
focusedTime(updated, old){
timeParts.forEach((part, i) => {
if (updated[i] === old[i] || typeof updated[i] === 'undefined') return;
const valueIndex = this[`${part}List`].findIndex(obj => obj.text === updated[i]);
this.scroll(part, valueIndex);
});
} }
}, },
mounted () { mounted () {

View file

@ -39,13 +39,15 @@
}; };
const selectedDays = this.dates.filter(Boolean).map(date => clearHours(new Date(date.getFullYear(), 0, 1))); const selectedDays = this.dates.filter(Boolean).map(date => clearHours(new Date(date.getFullYear(), 0, 1)));
const focusedDate = clearHours(new Date(this.focusedDate.getFullYear(), 0, 1));
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
const cell = deepCopy(cell_tmpl); const cell = deepCopy(cell_tmpl);
cell.date = new Date(this.startYear + i, 0, 1); cell.date = new Date(this.startYear + i, 0, 1);
cell.disabled = typeof this.disabledDate === 'function' && this.disabledDate(cell.date) && this.selectionMode === 'year'; cell.disabled = typeof this.disabledDate === 'function' && this.disabledDate(cell.date) && this.selectionMode === 'year';
const time = clearHours(cell.date); const day = clearHours(cell.date);
cell.selected = selectedDays.includes(time); cell.selected = selectedDays.includes(day);
cell.focused = day === focusedDate;
cells.push(cell); cells.push(cell);
} }
@ -59,6 +61,7 @@
{ {
[`${prefixCls}-cell-selected`]: cell.selected, [`${prefixCls}-cell-selected`]: cell.selected,
[`${prefixCls}-cell-disabled`]: cell.disabled, [`${prefixCls}-cell-disabled`]: cell.disabled,
[`${prefixCls}-cell-focused`]: cell.focused,
[`${prefixCls}-cell-range`]: cell.range && !cell.start && !cell.end [`${prefixCls}-cell-range`]: cell.range && !cell.start && !cell.end
} }
]; ];

View file

@ -46,6 +46,10 @@ export default {
pickerType: { pickerType: {
type: String, type: String,
require: true require: true
},
focusedDate: {
type: Date,
required: true,
} }
}, },
computed: { computed: {

View file

@ -41,6 +41,8 @@
:range-state="rangeState" :range-state="rangeState"
:show-week-numbers="showWeekNumbers" :show-week-numbers="showWeekNumbers"
:value="preSelecting.left ? [dates[0]] : dates" :value="preSelecting.left ? [dates[0]] : dates"
:focused-date="focusedDate"
@on-change-range="handleChangeRange" @on-change-range="handleChangeRange"
@on-pick="panelPickerHandlers.left" @on-pick="panelPickerHandlers.left"
@on-pick-click="handlePickClick" @on-pick-click="handlePickClick"
@ -80,6 +82,8 @@
:disabled-date="disabledDate" :disabled-date="disabledDate"
:show-week-numbers="showWeekNumbers" :show-week-numbers="showWeekNumbers"
:value="preSelecting.right ? [dates[dates.length - 1]] : dates" :value="preSelecting.right ? [dates[dates.length - 1]] : dates"
:focused-date="focusedDate"
@on-change-range="handleChangeRange" @on-change-range="handleChangeRange"
@on-pick="panelPickerHandlers.right" @on-pick="panelPickerHandlers.right"
@on-pick-click="handlePickClick"></component> @on-pick-click="handlePickClick"></component>
@ -178,7 +182,7 @@
[prefixCls + '-body-time']: this.showTime, [prefixCls + '-body-time']: this.showTime,
[prefixCls + '-body-date']: !this.showTime, [prefixCls + '-body-date']: !this.showTime,
} }
] ];
}, },
leftDatePanelLabel(){ leftDatePanelLabel(){
return this.panelLabelConfig('left'); return this.panelLabelConfig('left');
@ -224,10 +228,7 @@
// set panels positioning // set panels positioning
const leftPanelDate = this.startDate || this.dates[0] || new Date(); this.setPanelDates(this.startDate || this.dates[0] || new Date());
this.leftPanelDate = leftPanelDate;
const rightPanelDate = new Date(leftPanelDate.getFullYear(), leftPanelDate.getMonth() + 1, leftPanelDate.getDate());
this.rightPanelDate = this.splitPanels ? new Date(Math.max(this.dates[1], rightPanelDate)) : rightPanelDate;
}, },
currentView(currentView){ currentView(currentView){
const leftMonth = this.leftPanelDate.getMonth(); const leftMonth = this.leftPanelDate.getMonth();
@ -246,6 +247,9 @@
}, },
selectionMode(type){ selectionMode(type){
this.currentView = type || 'range'; this.currentView = type || 'range';
},
focusedDate(date){
this.setPanelDates(date || new Date());
} }
}, },
methods: { methods: {
@ -254,6 +258,11 @@
this.leftPickerTable = `${this.currentView}-table`; this.leftPickerTable = `${this.currentView}-table`;
this.rightPickerTable = `${this.currentView}-table`; this.rightPickerTable = `${this.currentView}-table`;
}, },
setPanelDates(leftPanelDate){
this.leftPanelDate = leftPanelDate;
const rightPanelDate = new Date(leftPanelDate.getFullYear(), leftPanelDate.getMonth() + 1, leftPanelDate.getDate());
this.rightPanelDate = this.splitPanels ? new Date(Math.max(this.dates[1], rightPanelDate)) : rightPanelDate;
},
panelLabelConfig (direction) { panelLabelConfig (direction) {
const locale = this.t('i.locale'); const locale = this.t('i.locale');
const datePanelLabel = this.t('i.datepicker.datePanelLabel'); const datePanelLabel = this.t('i.datepicker.datePanelLabel');

View file

@ -39,6 +39,8 @@
:value="dates" :value="dates"
:selection-mode="selectionMode" :selection-mode="selectionMode"
:disabled-date="disabledDate" :disabled-date="disabledDate"
:focused-date="focusedDate"
@on-pick="panelPickerHandlers" @on-pick="panelPickerHandlers"
@on-pick-click="handlePickClick" @on-pick-click="handlePickClick"
></component> ></component>
@ -51,6 +53,8 @@
:format="format" :format="format"
:time-disabled="timeDisabled" :time-disabled="timeDisabled"
:disabled-date="disabledDate" :disabled-date="disabledDate"
:focused-date="focusedDate"
v-bind="timePickerOptions" v-bind="timePickerOptions"
@on-pick="handlePick" @on-pick="handlePick"
@on-pick-click="handlePickClick" @on-pick-click="handlePickClick"
@ -150,7 +154,6 @@
}, },
currentView (currentView) { currentView (currentView) {
this.$emit('on-selection-mode-change', currentView); this.$emit('on-selection-mode-change', currentView);
this.pickertable = this.getTableType(currentView);
if (this.currentView === 'time') { if (this.currentView === 'time') {
this.$nextTick(() => { this.$nextTick(() => {
@ -162,6 +165,13 @@
selectionMode(type){ selectionMode(type){
this.currentView = type; this.currentView = type;
this.pickerTable = this.getTableType(type); this.pickerTable = this.getTableType(type);
},
focusedDate(date){
const isDifferentYear = date.getFullYear() !== this.panelDate.getFullYear();
const isDifferentMonth = isDifferentYear || date.getMonth() !== this.panelDate.getMonth();
if (isDifferentYear || isDifferentMonth){
this.panelDate = date;
}
} }
}, },
methods: { methods: {

View file

@ -1,5 +1,9 @@
<template> <template>
<div :class="[prefixCls]" v-clickoutside="handleClose"> <div
:class="wrapperClasses"
v-click-outside:mousedown.capture="handleClose"
v-click-outside.capture="handleClose"
>
<div ref="reference" :class="[prefixCls + '-rel']"> <div ref="reference" :class="[prefixCls + '-rel']">
<slot> <slot>
<i-input <i-input
@ -12,10 +16,14 @@
:placeholder="placeholder" :placeholder="placeholder"
:value="visualValue" :value="visualValue"
:name="name" :name="name"
ref="input"
@on-input-change="handleInputChange" @on-input-change="handleInputChange"
@on-focus="handleFocus" @on-focus="handleFocus"
@on-blur="handleBlur" @on-blur="handleBlur"
@on-click="handleIconClick" @on-click="handleIconClick"
@click.native="handleFocus"
@keydown.native="handleKeydown"
@mouseenter.native="handleInputMouseenter" @mouseenter.native="handleInputMouseenter"
@mouseleave.native="handleInputMouseleave" @mouseleave.native="handleInputMouseleave"
@ -48,6 +56,7 @@
:show-week-numbers="showWeekNumbers" :show-week-numbers="showWeekNumbers"
:picker-type="type" :picker-type="type"
:multiple="multiple" :multiple="multiple"
:focused-date="focusedDate"
:time-picker-options="timePickerOptions" :time-picker-options="timePickerOptions"
@ -69,21 +78,49 @@
import iInput from '../../components/input/input.vue'; import iInput from '../../components/input/input.vue';
import Drop from '../../components/select/dropdown.vue'; import Drop from '../../components/select/dropdown.vue';
import clickoutside from '../../directives/clickoutside'; import vClickOutside from 'v-click-outside-x/index';
import TransferDom from '../../directives/transfer-dom'; import TransferDom from '../../directives/transfer-dom';
import { oneOf } from '../../utils/assist'; import { oneOf } from '../../utils/assist';
import { DEFAULT_FORMATS, RANGE_SEPARATOR, TYPE_VALUE_RESOLVER_MAP } from './util'; import { DEFAULT_FORMATS, RANGE_SEPARATOR, TYPE_VALUE_RESOLVER_MAP, getDayCountOfMonth } from './util';
import {findComponentsDownward} from '../../utils/assist';
import Emitter from '../../mixins/emitter'; import Emitter from '../../mixins/emitter';
const prefixCls = 'ivu-date-picker'; const prefixCls = 'ivu-date-picker';
const pickerPrefixCls = 'ivu-picker';
const isEmptyArray = val => val.reduce((isEmpty, str) => isEmpty && !str || (typeof str === 'string' && str.trim() === ''), true); const isEmptyArray = val => val.reduce((isEmpty, str) => isEmpty && !str || (typeof str === 'string' && str.trim() === ''), true);
const keyValueMapper = {
40: 'up',
39: 'right',
38: 'down',
37: 'left',
};
const mapPossibleValues = (key, horizontal, vertical) => {
if (key === 'left') return horizontal * -1;
if (key === 'right') return horizontal * 1;
if (key === 'up') return vertical * 1;
if (key === 'down') return vertical * -1;
};
const pulseElement = (el) => {
const pulseClass = 'ivu-date-picker-btn-pulse';
el.classList.add(pulseClass);
setTimeout(() => el.classList.remove(pulseClass), 200);
};
const extractTime = date => {
if (!date) return [0, 0, 0];
return [
date.getHours(), date.getMinutes(), date.getSeconds()
];
};
export default { export default {
name: 'CalendarPicker',
mixins: [ Emitter ], mixins: [ Emitter ],
components: { iInput, Drop }, components: { iInput, Drop },
directives: { clickoutside, TransferDom }, directives: { clickOutside: vClickOutside.directive, TransferDom },
props: { props: {
format: { format: {
type: String type: String
@ -172,6 +209,7 @@
const isRange = this.type.includes('range'); const isRange = this.type.includes('range');
const emptyArray = isRange ? [null, null] : [null]; const emptyArray = isRange ? [null, null] : [null];
const initialValue = isEmptyArray((isRange ? this.value : [this.value]) || []) ? emptyArray : this.parseDate(this.value); const initialValue = isEmptyArray((isRange ? this.value : [this.value]) || []) ? emptyArray : this.parseDate(this.value);
const focusedTime = initialValue.map(extractTime);
return { return {
prefixCls: prefixCls, prefixCls: prefixCls,
@ -181,10 +219,24 @@
disableClickOutSide: false, // fixed when click a date,trigger clickoutside to close picker disableClickOutSide: false, // fixed when click a date,trigger clickoutside to close picker
disableCloseUnderTransfer: false, // transfer Drop, disableCloseUnderTransfer: false, // transfer Drop,
selectionMode: this.onSelectionModeChange(this.type), selectionMode: this.onSelectionModeChange(this.type),
forceInputRerender: 1 forceInputRerender: 1,
isFocused: false,
focusedDate: initialValue[0] || this.startDate || new Date(),
focusedTime: {
column: 0, // which column inside the picker
picker: 0, // which picker
time: focusedTime, // the values array into [hh, mm, ss],
active: false
},
internalFocus: false,
}; };
}, },
computed: { computed: {
wrapperClasses(){
return [prefixCls, {
[prefixCls + '-focused']: this.isFocused
}];
},
publicVModelValue(){ publicVModelValue(){
if (this.multiple){ if (this.multiple){
return this.internalValue.slice(); return this.internalValue.slice();
@ -232,32 +284,254 @@
handleTransferClick () { handleTransferClick () {
if (this.transfer) this.disableCloseUnderTransfer = true; if (this.transfer) this.disableCloseUnderTransfer = true;
}, },
handleClose () { handleClose (e) {
if (this.disableCloseUnderTransfer) { if (this.disableCloseUnderTransfer) {
this.disableCloseUnderTransfer = false; this.disableCloseUnderTransfer = false;
return false; return false;
} }
if (this.open !== null) return;
this.visible = false; if (e && e.type === 'mousedown' && this.visible) {
e.preventDefault();
e.stopPropagation();
return;
}
if (this.visible) {
const pickerPanel = this.$refs.pickerPanel && this.$refs.pickerPanel.$el;
if (e && pickerPanel && pickerPanel.contains(e.target)) return; // its a click inside own component, lets ignore it.
this.visible = false;
e && e.preventDefault();
e && e.stopPropagation();
return;
}
this.isFocused = false;
this.disableClickOutSide = false; this.disableClickOutSide = false;
}, },
handleFocus () { handleFocus (e) {
if (this.readonly) return; if (this.readonly) return;
this.isFocused = true;
if (e && e.type === 'focus') return; // just focus, don't open yet
this.visible = true; this.visible = true;
this.$refs.pickerPanel.onToggleVisibility(true);
}, },
handleBlur () { handleBlur (e) {
this.visible = false; if (this.internalFocus){
this.internalFocus = false;
return;
}
if (this.visible) {
e.preventDefault();
return;
}
this.isFocused = false;
this.onSelectionModeChange(this.type); this.onSelectionModeChange(this.type);
this.internalValue = this.internalValue.slice(); // trigger panel watchers to reset views this.internalValue = this.internalValue.slice(); // trigger panel watchers to reset views
this.reset(); this.reset();
this.$refs.pickerPanel.onToggleVisibility(false); this.$refs.pickerPanel.onToggleVisibility(false);
}, },
handleKeydown(e){
const keyCode = e.keyCode;
// handle "tab" key
if (keyCode === 9){
if (this.visible){
e.stopPropagation();
e.preventDefault();
if (this.isConfirm){
const selector = `.${pickerPrefixCls}-confirm > *`;
const tabbable = this.$refs.drop.$el.querySelectorAll(selector);
this.internalFocus = true;
const element = [...tabbable][e.shiftKey ? 'pop' : 'shift']();
element.focus();
} else {
this.handleClose();
}
} else {
this.focused = false;
}
}
// open the panel
const arrows = [37, 38, 39, 40];
if (!this.visible && arrows.includes(keyCode)){
this.visible = true;
return;
}
// close on "esc" key
if (keyCode === 27){
if (this.visible) {
e.stopPropagation();
this.handleClose();
}
}
// select date, "Enter" key
if (keyCode === 13){
const timePickers = findComponentsDownward(this, 'TimeSpinner');
if (timePickers.length > 0){
const columnsPerPicker = timePickers[0].showSeconds ? 3 : 2;
const pickerIndex = Math.floor(this.focusedTime.column / columnsPerPicker);
const value = this.focusedTime.time[pickerIndex];
timePickers[pickerIndex].chooseValue(value);
return;
}
if (this.type.match(/range/)){
this.$refs.pickerPanel.handleRangePick(this.focusedDate, 'date');
} else {
const panels = findComponentsDownward(this, 'PanelTable');
const compareDate = (d) => {
const sliceIndex = ['year', 'month', 'date'].indexOf((this.type)) + 1;
return [d.getFullYear(), d.getMonth(), d.getDate()].slice(0, sliceIndex).join('-');
};
const dateIsValid = panels.find(({cells}) => {
return cells.find(({date, disabled}) => compareDate(date) === compareDate(this.focusedDate) && !disabled);
});
if (dateIsValid) this.onPick(this.focusedDate, false, 'date');
}
}
if (!arrows.includes(keyCode)) return; // ignore rest of keys
// navigate times and dates
if (this.focusedTime.active) e.preventDefault(); // to prevent cursor from moving
this.navigateDatePanel(keyValueMapper[keyCode], e.shiftKey);
},
reset(){ reset(){
this.$refs.pickerPanel.reset && this.$refs.pickerPanel.reset(); this.$refs.pickerPanel.reset && this.$refs.pickerPanel.reset();
}, },
navigateTimePanel(direction){
this.focusedTime.active = true;
const horizontal = direction.match(/left|right/);
const vertical = direction.match(/up|down/);
const timePickers = findComponentsDownward(this, 'TimeSpinner');
const maxNrOfColumns = (timePickers[0].showSeconds ? 3 : 2) * timePickers.length;
const column = (currentColumn => {
const incremented = currentColumn + (horizontal ? (direction === 'left' ? -1 : 1) : 0);
return (incremented + maxNrOfColumns) % maxNrOfColumns;
})(this.focusedTime.column);
const columnsPerPicker = maxNrOfColumns / timePickers.length;
const pickerIndex = Math.floor(column / columnsPerPicker);
const col = column % columnsPerPicker;
if (horizontal){
const time = this.internalValue.map(extractTime);
this.focusedTime = {
...this.focusedTime,
column: column,
time: time
};
timePickers.forEach((instance, i) => {
if (i === pickerIndex) instance.updateFocusedTime(col, time[pickerIndex]);
else instance.updateFocusedTime(-1, instance.focusedTime);
});
}
if (vertical){
const increment = direction === 'up' ? 1 : -1;
const timeParts = ['hours', 'minutes', 'seconds'];
const pickerPossibleValues = timePickers[pickerIndex][`${timeParts[col]}List`];
const nextIndex = pickerPossibleValues.findIndex(({text}) => this.focusedTime.time[pickerIndex][col] === text) + increment;
const nextValue = pickerPossibleValues[nextIndex % pickerPossibleValues.length].text;
const times = this.focusedTime.time.map((time, i) => {
if (i !== pickerIndex) return time;
time[col] = nextValue;
return time;
});
this.focusedTime = {
...this.focusedTime,
time: times
};
timePickers.forEach((instance, i) => {
if (i === pickerIndex) instance.updateFocusedTime(col, times[i]);
else instance.updateFocusedTime(-1, instance.focusedTime);
});
}
},
navigateDatePanel(direction, shift){
const timePickers = findComponentsDownward(this, 'TimeSpinner');
if (timePickers.length > 0) {
// we are in TimePicker mode
this.navigateTimePanel(direction, shift, timePickers);
return;
}
if (shift){
if (this.type === 'year'){
this.focusedDate = new Date(
this.focusedDate.getFullYear() + mapPossibleValues(direction, 0, 10),
this.focusedDate.getMonth(),
this.focusedDate.getDate()
);
} else {
this.focusedDate = new Date(
this.focusedDate.getFullYear() + mapPossibleValues(direction, 0, 1),
this.focusedDate.getMonth() + mapPossibleValues(direction, 1, 0),
this.focusedDate.getDate()
);
}
const position = direction.match(/left|down/) ? 'prev' : 'next';
const double = direction.match(/up|down/) ? '-double' : '';
// pulse button
const button = this.$refs.drop.$el.querySelector(`.ivu-date-picker-${position}-btn-arrow${double}`);
if (button) pulseElement(button);
return;
}
const initialDate = this.focusedDate || (this.internalValue && this.internalValue[0]) || new Date();
const focusedDate = new Date(initialDate);
if (this.type.match(/^date/)){
const lastOfMonth = getDayCountOfMonth(initialDate.getFullYear(), initialDate.getMonth());
const startDay = initialDate.getDate();
const nextDay = focusedDate.getDate() + mapPossibleValues(direction, 1, 7);
if (nextDay < 1) {
if (direction.match(/left|right/)) {
focusedDate.setMonth(focusedDate.getMonth() + 1);
focusedDate.setDate(nextDay);
} else {
focusedDate.setDate(startDay + Math.floor((lastOfMonth - startDay) / 7) * 7);
}
} else if (nextDay > lastOfMonth){
if (direction.match(/left|right/)) {
focusedDate.setMonth(focusedDate.getMonth() - 1);
focusedDate.setDate(nextDay);
} else {
focusedDate.setDate(startDay % 7);
}
} else {
focusedDate.setDate(nextDay);
}
}
if (this.type.match(/^month/)) {
focusedDate.setMonth(focusedDate.getMonth() + mapPossibleValues(direction, 1, 3));
}
if (this.type.match(/^year/)) {
focusedDate.setFullYear(focusedDate.getFullYear() + mapPossibleValues(direction, 1, 3));
}
this.focusedDate = focusedDate;
},
handleInputChange (event) { handleInputChange (event) {
const isArrayValue = this.type.includes('range') || this.multiple; const isArrayValue = this.type.includes('range') || this.multiple;
const oldValue = this.visualValue; const oldValue = this.visualValue;
@ -377,6 +651,12 @@
this.internalValue = Array.isArray(dates) ? dates : [dates]; this.internalValue = Array.isArray(dates) ? dates : [dates];
} }
this.focusedDate = this.internalValue[0];
this.focusedTime = {
...this.focusedTime,
time: this.internalValue.map(extractTime)
};
if (!this.isConfirm) this.onSelectionModeChange(this.type); // reset the selectionMode if (!this.isConfirm) this.onSelectionModeChange(this.type); // reset the selectionMode
if (!this.isConfirm) this.visible = visible; if (!this.isConfirm) this.visible = visible;
this.emitChange(type); this.emitChange(type);
@ -384,22 +664,23 @@
onPickSuccess(){ onPickSuccess(){
this.visible = false; this.visible = false;
this.$emit('on-ok'); this.$emit('on-ok');
this.focus();
this.reset(); this.reset();
}, },
focus() {
this.$refs.input.focus();
}
}, },
watch: { watch: {
visible (state) { visible (state) {
if (state === false){ if (state === false){
this.$refs.drop.destroy(); this.$refs.drop.destroy();
const input = this.$el.querySelector('input');
if (input) input.blur();
} }
this.$refs.drop.update(); this.$refs.drop.update();
this.$emit('on-open-change', state); this.$emit('on-open-change', state);
}, },
value(val) { value(val) {
this.internalValue = this.parseDate(val); this.internalValue = this.parseDate(val);
}, },
open (val) { open (val) {
this.visible = val === true; this.visible = val === true;
@ -421,6 +702,9 @@
this.$emit('input', this.publicVModelValue); // to update v-model this.$emit('input', this.publicVModelValue); // to update v-model
} }
if (this.open !== null) this.visible = this.open; if (this.open !== null) this.visible = this.open;
// to handle focus from confirm buttons
this.$on('focus-input', () => this.focus());
} }
}; };
</script> </script>

View file

@ -5,6 +5,7 @@ import RangeDatePickerPanel from '../panel/Date/date-range.vue';
import { oneOf } from '../../../utils/assist'; import { oneOf } from '../../../utils/assist';
export default { export default {
name: 'CalendarPicker',
mixins: [Picker], mixins: [Picker],
props: { props: {
type: { type: {

View file

@ -3,7 +3,7 @@ import TimePickerPanel from '../panel/Time/time.vue';
import RangeTimePickerPanel from '../panel/Time/time-range.vue'; import RangeTimePickerPanel from '../panel/Time/time-range.vue';
import Options from '../time-mixins'; import Options from '../time-mixins';
import { oneOf } from '../../../utils/assist'; import { findComponentsDownward, oneOf } from '../../../utils/assist';
export default { export default {
mixins: [Picker, Options], mixins: [Picker, Options],
@ -30,4 +30,14 @@ export default {
}; };
} }
}, },
watch: {
visible(visible){
if (visible) {
this.$nextTick(() => {
const spinners = findComponentsDownward(this, 'TimeSpinner');
spinners.forEach(instance => instance.updateScroll());
});
}
}
}
}; };

View file

@ -44,17 +44,23 @@
margin: 2px; margin: 2px;
color: @btn-disable-color; color: @btn-disable-color;
} }
&-cell:hover{
em{
background: @date-picker-cell-hover-bg;
}
}
&-focused{
em{
box-shadow: 0 0 0 1px @primary-color inset;
}
}
&-cell{ &-cell{
span&{ span&{
width: 28px; width: 28px;
height: 28px; height: 28px;
cursor: pointer; cursor: pointer;
} }
&:hover{
em{
background: @date-picker-cell-hover-bg;
}
}
&-prev-month,&-next-month{ &-prev-month,&-next-month{
em{ em{
color: @btn-disable-color; color: @btn-disable-color;
@ -154,6 +160,11 @@
margin: 0; margin: 0;
} }
} }
.@{date-picker-prefix-cls}-cells-cell-focused{
background-color: tint(@primary-color, 80%);
}
} }
&-header{ &-header{
@ -169,6 +180,11 @@
} }
} }
} }
&-btn-pulse{
background-color: tint(@primary-color, 80%) !important;
border-radius: @border-radius-small;
transition: background-color @transition-time @ease-in-out;
}
&-prev-btn{ &-prev-btn{
float: left; float: left;
&-arrow-double{ &-arrow-double{
@ -216,6 +232,10 @@
max-height: none; max-height: none;
width: auto; width: auto;
} }
&-focused input{
.active();
}
} }
.@{picker-prefix-cls} { .@{picker-prefix-cls} {
@ -289,9 +309,9 @@
color: @link-active-color; color: @link-active-color;
} }
} }
& > span&-time-disabled{
color: @btn-disable-color; &-time{
cursor: @cursor-disabled; float: left;
} }
} }
} }

View file

@ -70,6 +70,9 @@
color: @primary-color; color: @primary-color;
background: @background-color-select-hover; background: @background-color-select-hover;
} }
&-focused{
background-color: tint(@primary-color, 80%);
}
} }
} }
@ -165,4 +168,4 @@
} }
} }
} }
} }

View file

@ -116,7 +116,7 @@ describe('DatePicker.vue', () => {
`); `);
const picker = vm.$children[0]; const picker = vm.$children[0];
picker.handleIconClick(); picker.handleFocus({type: 'focus'});
vm.$nextTick(() => { vm.$nextTick(() => {
const displayField = vm.$el.querySelector('.ivu-input'); const displayField = vm.$el.querySelector('.ivu-input');
const clickableCells = vm.$el.querySelectorAll('.ivu-date-picker-cells-cell'); const clickableCells = vm.$el.querySelectorAll('.ivu-date-picker-cells-cell');
@ -169,7 +169,7 @@ describe('DatePicker.vue', () => {
}); });
const picker = vm.$children[0]; const picker = vm.$children[0];
picker.handleIconClick(); picker.handleFocus({type: 'focus'});
vm.$nextTick(() => { vm.$nextTick(() => {
const panel = vm.$el.querySelector('.ivu-picker-panel-content'); const panel = vm.$el.querySelector('.ivu-picker-panel-content');
const dayPanel = panel.querySelector('[class="ivu-date-picker-cells"]'); const dayPanel = panel.querySelector('[class="ivu-date-picker-cells"]');
@ -243,7 +243,7 @@ describe('DatePicker.vue', () => {
`); `);
const picker = vm.$children[0]; const picker = vm.$children[0];
picker.handleIconClick(); picker.handleFocus({type: 'focus'});
vm.$nextTick(() => { vm.$nextTick(() => {
const displayField = vm.$el.querySelector('.ivu-input'); const displayField = vm.$el.querySelector('.ivu-input');
const clickableCells = vm.$el.querySelectorAll('.ivu-date-picker-cells-cell'); const clickableCells = vm.$el.querySelectorAll('.ivu-date-picker-cells-cell');
@ -266,9 +266,11 @@ describe('DatePicker.vue', () => {
// it should be closed by now // it should be closed by now
expect(picker.visible).to.equal(false); expect(picker.visible).to.equal(false);
// open picker again // open picker again
picker.handleIconClick(); picker.handleFocus({type: 'focus'});
picker.visible = true;
vm.$nextTick(() => {
vm.$nextTick(() => {
expect(picker.visible).to.equal(true); expect(picker.visible).to.equal(true);
expect(JSON.stringify(picker.internalValue)).to.equal('[null,null]'); expect(JSON.stringify(picker.internalValue)).to.equal('[null,null]');
expect(displayField.value).to.equal(''); expect(displayField.value).to.equal('');
@ -355,7 +357,7 @@ describe('DatePicker.vue', () => {
`); `);
const picker = vm.$children[0]; const picker = vm.$children[0];
picker.handleIconClick(); picker.handleFocus({type: 'focus'});
vm.$nextTick(() => { vm.$nextTick(() => {
const now = new Date(); const now = new Date();
const labels = vm.$el.querySelectorAll('.ivu-picker-panel-body .ivu-date-picker-header-label'); const labels = vm.$el.querySelectorAll('.ivu-picker-panel-body .ivu-date-picker-header-label');

View file

@ -11,7 +11,7 @@ describe('TimePicker.vue', () => {
<Time-Picker></Time-Picker> <Time-Picker></Time-Picker>
`); `);
const picker = vm.$children[0]; const picker = vm.$children[0];
picker.handleIconClick(); // open the picker panels picker.handleFocus({type: 'focus'}); // open the picker panels
vm.$nextTick(() => { vm.$nextTick(() => {
const spiners = picker.$el.querySelectorAll('.ivu-time-picker-cells-list'); const spiners = picker.$el.querySelectorAll('.ivu-time-picker-cells-list');
@ -28,7 +28,7 @@ describe('TimePicker.vue', () => {
<Time-Picker format="HH:mm"></Time-Picker> <Time-Picker format="HH:mm"></Time-Picker>
`); `);
const picker = vm.$children[0]; const picker = vm.$children[0];
picker.handleIconClick(); // open the picker panels picker.handleFocus({type: 'focus'}); // open the picker panels
vm.$nextTick(() => { vm.$nextTick(() => {
const spiners = picker.$el.querySelectorAll('.ivu-time-picker-cells-list'); const spiners = picker.$el.querySelectorAll('.ivu-time-picker-cells-list');
@ -44,7 +44,7 @@ describe('TimePicker.vue', () => {
<Time-Picker :steps="[1, 15]"></Time-Picker> <Time-Picker :steps="[1, 15]"></Time-Picker>
`); `);
const picker = vm.$children[0]; const picker = vm.$children[0];
picker.handleIconClick(); // open the picker panels picker.handleFocus({type: 'focus'}); // open the picker panels
vm.$nextTick(() => { vm.$nextTick(() => {
const spiners = picker.$el.querySelectorAll('.ivu-time-picker-cells-list'); const spiners = picker.$el.querySelectorAll('.ivu-time-picker-cells-list');