Add keyboard navigation to date|time picker
This commit is contained in:
parent
2bf3e04753
commit
75cb299868
16 changed files with 467 additions and 67 deletions
|
@ -1,5 +1,9 @@
|
|||
<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']">
|
||||
<slot>
|
||||
<i-input
|
||||
|
@ -12,10 +16,14 @@
|
|||
:placeholder="placeholder"
|
||||
:value="visualValue"
|
||||
:name="name"
|
||||
ref="input"
|
||||
|
||||
@on-input-change="handleInputChange"
|
||||
@on-focus="handleFocus"
|
||||
@on-blur="handleBlur"
|
||||
@on-click="handleIconClick"
|
||||
@click.native="handleFocus"
|
||||
@keydown.native="handleKeydown"
|
||||
@mouseenter.native="handleInputMouseenter"
|
||||
@mouseleave.native="handleInputMouseleave"
|
||||
|
||||
|
@ -48,6 +56,7 @@
|
|||
:show-week-numbers="showWeekNumbers"
|
||||
:picker-type="type"
|
||||
:multiple="multiple"
|
||||
:focused-date="focusedDate"
|
||||
|
||||
:time-picker-options="timePickerOptions"
|
||||
|
||||
|
@ -69,21 +78,49 @@
|
|||
|
||||
import iInput from '../../components/input/input.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 { 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';
|
||||
|
||||
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 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 {
|
||||
name: 'CalendarPicker',
|
||||
mixins: [ Emitter ],
|
||||
components: { iInput, Drop },
|
||||
directives: { clickoutside, TransferDom },
|
||||
directives: { clickOutside: vClickOutside.directive, TransferDom },
|
||||
props: {
|
||||
format: {
|
||||
type: String
|
||||
|
@ -172,6 +209,7 @@
|
|||
const isRange = this.type.includes('range');
|
||||
const emptyArray = isRange ? [null, null] : [null];
|
||||
const initialValue = isEmptyArray((isRange ? this.value : [this.value]) || []) ? emptyArray : this.parseDate(this.value);
|
||||
const focusedTime = initialValue.map(extractTime);
|
||||
|
||||
return {
|
||||
prefixCls: prefixCls,
|
||||
|
@ -181,10 +219,24 @@
|
|||
disableClickOutSide: false, // fixed when click a date,trigger clickoutside to close picker
|
||||
disableCloseUnderTransfer: false, // transfer 模式下,点击Drop也会触发关闭,
|
||||
selectionMode: this.onSelectionModeChange(this.type),
|
||||
forceInputRerender: 1
|
||||
forceInputRerender: 1,
|
||||
isFocused: false,
|
||||
focusedDate: initialValue[0] || 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: {
|
||||
wrapperClasses(){
|
||||
return [prefixCls, {
|
||||
[prefixCls + '-focused']: this.isFocused
|
||||
}];
|
||||
},
|
||||
publicVModelValue(){
|
||||
if (this.multiple){
|
||||
return this.internalValue.slice();
|
||||
|
@ -232,32 +284,246 @@
|
|||
handleTransferClick () {
|
||||
if (this.transfer) this.disableCloseUnderTransfer = true;
|
||||
},
|
||||
handleClose () {
|
||||
handleClose (e) {
|
||||
if (this.disableCloseUnderTransfer) {
|
||||
this.disableCloseUnderTransfer = 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;
|
||||
},
|
||||
handleFocus () {
|
||||
handleFocus (e) {
|
||||
if (this.readonly) return;
|
||||
this.isFocused = true;
|
||||
if (e && e.type === 'focus') return; // just focus, don't open yet
|
||||
this.visible = true;
|
||||
this.$refs.pickerPanel.onToggleVisibility(true);
|
||||
},
|
||||
handleBlur () {
|
||||
this.visible = false;
|
||||
handleBlur (e) {
|
||||
if (this.internalFocus){
|
||||
this.internalFocus = false;
|
||||
return;
|
||||
}
|
||||
if (this.visible) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
this.isFocused = false;
|
||||
this.onSelectionModeChange(this.type);
|
||||
this.internalValue = this.internalValue.slice(); // trigger panel watchers to reset views
|
||||
this.reset();
|
||||
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 {
|
||||
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(){
|
||||
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) {
|
||||
const isArrayValue = this.type.includes('range') || this.multiple;
|
||||
const oldValue = this.visualValue;
|
||||
|
@ -377,6 +643,12 @@
|
|||
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.visible = visible;
|
||||
this.emitChange(type);
|
||||
|
@ -384,22 +656,23 @@
|
|||
onPickSuccess(){
|
||||
this.visible = false;
|
||||
this.$emit('on-ok');
|
||||
this.focus();
|
||||
this.reset();
|
||||
},
|
||||
focus() {
|
||||
this.$refs.input.focus();
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible (state) {
|
||||
if (state === false){
|
||||
this.$refs.drop.destroy();
|
||||
const input = this.$el.querySelector('input');
|
||||
if (input) input.blur();
|
||||
}
|
||||
this.$refs.drop.update();
|
||||
this.$emit('on-open-change', state);
|
||||
},
|
||||
value(val) {
|
||||
this.internalValue = this.parseDate(val);
|
||||
|
||||
},
|
||||
open (val) {
|
||||
this.visible = val === true;
|
||||
|
@ -421,6 +694,9 @@
|
|||
this.$emit('input', this.publicVModelValue); // to update v-model
|
||||
}
|
||||
if (this.open !== null) this.visible = this.open;
|
||||
|
||||
// to handle focus from confirm buttons
|
||||
this.$on('focus-input', () => this.focus());
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue