feat: Feature/timepicker && repeat-click directive (#289)

* feat: Feature/datepicker && repeat-click directive (#288)

* style: fix lint

* test: fix local test

* test: update test

* fix: update api to disabledHours

* chore: update

* chore: fix lint
This commit is contained in:
zazzaz 2020-09-16 14:49:21 +08:00 committed by GitHub
parent 6a1880a8d2
commit b01a6f4e81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 2059 additions and 23 deletions

View File

@ -6,3 +6,5 @@ export default (app: App): void => {
app.component(Button.name, Button)
app.component(ButtonGroup.name, ButtonGroup)
}
export { Button }

View File

@ -0,0 +1,35 @@
import { sleep } from '@element-plus/test-utils'
import { mount } from '@vue/test-utils'
import RepeatClick from '../repeat-click'
const handler = jest.fn()
const _mount = () => mount({
template: `
<div id="block" v-repeat-click="onClick">TEST</div>
`,
directives: {
repeatClick: RepeatClick,
},
methods: {
onClick() {
handler()
},
},
})
beforeEach(() => {
handler.mockClear()
})
describe('Directives.vue', () => {
test('Click test', async () => {
const wrapper = _mount()
const block = wrapper.find('#block')
block.trigger('mousedown')
const testTime = 330
await sleep(testTime)
block.trigger('mouseup')
const expectResult = Math.floor(testTime / 100)
expect(handler).toHaveBeenCalledTimes(expectResult)
})
})

View File

@ -1,7 +1,7 @@
import isServer from '@element-plus/utils/isServer'
import { on } from '@element-plus/utils/dom'
import isServer from '@element-plus/utils/isServer'
import type { ComponentPublicInstance, DirectiveBinding, ObjectDirective } from 'vue'
import type { DirectiveBinding, ObjectDirective, ComponentPublicInstance } from 'vue'
type DocumentHandler = <T extends MouseEvent>(mouseup: T, mousedown: T) => void;

View File

@ -1,2 +1,3 @@
export { default as ClickOutside } from './click-outside'
export { default as RepeatClick } from './repeat-click'
export { default as TrapFocus } from './trap-focus'

View File

@ -0,0 +1,24 @@
import { on, once } from '@element-plus/utils/dom'
export default {
beforeMount(el, binding) {
let interval = null
let startTime
const handler = () => binding.value && binding.value()
const clear = () => {
if (Date.now() - startTime < 100) {
handler()
}
clearInterval(interval)
interval = null
}
on(el, 'mousedown', e => {
if ((e as any).button !== 0) return
startTime = Date.now()
once(document as any, 'mouseup', clear)
clearInterval(interval)
interval = setInterval(handler, 100)
})
},
}

View File

@ -27,6 +27,7 @@ import ElScrollBar from '@element-plus/scrollbar'
import ElSteps from '@element-plus/steps'
import ElCollapse from '@element-plus/collapse'
import ElPopper from '@element-plus/popper'
import ElTimePicker from '@element-plus/time-picker'
import ElTabs from '@element-plus/tabs'
import ElTooltip from '@element-plus/tooltip'
import ElSlider from '@element-plus/slider'
@ -63,6 +64,7 @@ export {
ElSteps,
ElRadio,
ElCollapse,
ElTimePicker,
ElTabs,
ElTooltip,
ElSlider,
@ -101,6 +103,7 @@ const install = (app: App): void => {
ElRadio(app)
ElCollapse(app)
ElPopper(app)
ElTimePicker(app)
ElTabs(app)
ElTooltip(app)
ElSlider(app)

View File

@ -36,6 +36,7 @@
"@element-plus/steps": "^0.0.0",
"@element-plus/notification": "^0.0.0",
"@element-plus/collapse": "^0.0.0",
"@element-plus/time-picker": "^0.0.0",
"@element-plus/tabs": "^0.0.0"
}
}

View File

@ -10,7 +10,7 @@ describe('Locale', () => {
})
test('return key name if not defined', () => {
expect(t('el.popconfirm.someThing')).toBe('someThing')
expect(t('el.popconfirm.someThing')).toBeUndefined()
})
test('use', () => {

View File

@ -8,7 +8,7 @@ export const t = (path:string): string => {
let current = lang
for (let i = 0, j = array.length; i < j; i++) {
const property = array[i]
value = current[property] || property
value = current[property]
if (i === j - 1) return value
if (!value) return ''
current = value

View File

@ -0,0 +1,343 @@
import { mount } from '@vue/test-utils'
import { nextTick } from 'vue'
import TimePicker from '../src/time-picker'
const _mount = (template: string, data, otherObj?) => mount({
components: {
'el-time-picker': TimePicker,
},
template,
data,
...otherObj,
}, {
global: {
provide: {
elForm: {},
elFormItem: {},
},
},
})
const makeRange = (start, end) => {
const result = []
for (let i = start; i <= end; i++) {
result.push(i)
}
return result
}
const getSpinnerTextAsArray = (dom, selector) => {
return [].slice
.call(dom.querySelectorAll(selector))
.map(node => Number(node.textContent))
}
afterEach(() => {
document.documentElement.innerHTML = ''
})
describe('TimePicker', () => {
it('create', async () => {
const wrapper = _mount(`<el-time-picker
:placeholder="placeholder"
:readonly="readonly"
/>`, () => ({ placeholder: 'test_',
readonly: true }))
const input = wrapper.find('input')
expect(input.attributes('placeholder')).toBe('test_')
expect(input.attributes('readonly')).not.toBeUndefined()
})
it('set format && default value && set AM/PM spinner', async () => {
const wrapper = _mount(`<el-time-picker
:format="format"
v-model="value"
/>`, () => ({ format: 'hh-mm:ss A',
value: new Date(2016, 9, 10, 18, 40) }))
await nextTick()
const input = wrapper.find('input')
expect(input.element.value).toBe('06-40:00 PM') // format
input.trigger('blur')
input.trigger('focus')
await nextTick()
const list = document.querySelectorAll('.el-time-spinner__list')
const hoursEl = list[0]
const items = hoursEl.querySelectorAll('.el-time-spinner__item')
expect(items[0].textContent).toBe('12 AM') // am pm
expect(items[1].textContent).toBe('01 AM')
expect(items[12].textContent).toBe('12 PM')
expect(items[15].textContent).toBe('03 PM')
const times = document.querySelectorAll('.el-time-spinner__list .active')
expect(times[0].textContent).toBe('06 PM')
expect(times[1].textContent).toBe('40') // default value
expect(times[2].textContent).toBe('00')
})
it('select time', async () => {
const wrapper = _mount(`<el-time-picker
v-model="value"
/>`, () => ({ value: '' }))
const input = wrapper.find('input')
input.trigger('blur')
input.trigger('focus')
await nextTick()
const list = document.querySelectorAll('.el-time-spinner__list')
const hoursEl = list[0]
const minutesEl = list[1]
const secondsEl = list[2]
const hourEl = hoursEl.querySelectorAll('.el-time-spinner__item')[4] as any
const minuteEl = minutesEl.querySelectorAll('.el-time-spinner__item')[36] as any
const secondEl = secondsEl.querySelectorAll('.el-time-spinner__item')[20] as any
// click hour, minute, second one at a time.
hourEl.click()
await nextTick()
minuteEl.click()
await nextTick()
secondEl.click()
await nextTick()
const vm = wrapper.vm as any
const date = vm.value
expect(hourEl.classList.contains('active')).toBeTruthy()
expect(minuteEl.classList.contains('active')).toBeTruthy()
expect(secondEl.classList.contains('active')).toBeTruthy()
expect(date.getHours()).toBe(4)
expect(date.getMinutes()).toBe(36)
expect(date.getSeconds()).toBe(20)
})
it('click confirm / cancel button', async () => {
const wrapper = _mount(`<el-time-picker
v-model="value"
/>`, () => ({ value: '' }))
const input = wrapper.find('input')
input.trigger('blur')
input.trigger('focus')
await nextTick();
(document.querySelector('.el-time-panel__btn.cancel') as any).click()
const vm = wrapper.vm as any
expect(vm.value).toBe('')
input.trigger('blur')
input.trigger('focus')
await nextTick();
(document.querySelector('.el-time-panel__btn.confirm') as any).click()
expect(vm.value instanceof Date).toBeTruthy()
})
it('set format', async () => {
const wrapper = _mount(`<el-time-picker
v-model="value"
format='HH:mm'
/>`, () => ({ value: '' }))
const input = wrapper.find('input')
input.trigger('blur')
input.trigger('focus')
await nextTick()
const spinnerDom = document.querySelectorAll('.el-time-spinner__wrapper')
const minutesDom = spinnerDom[1]
const secondsDom = spinnerDom[2]
expect(minutesDom).not.toBeUndefined()
expect(secondsDom).toBeUndefined()
})
it('event change, focus, blur', async () => {
const changeHandler = jest.fn()
const focusHandler = jest.fn()
const blurHandler = jest.fn()
const wrapper = _mount(`<el-time-picker
v-model="value"
@change="onChange"
@focus="onFocus"
@blur="onBlur"
/>`, () => ({ value: new Date(2016, 9, 10, 18, 40) }), {
methods: {
onChange(e) {
return changeHandler(e)
},
onFocus(e) {
return focusHandler(e)
},
onBlur(e) {
return blurHandler(e)
},
},
})
const input = wrapper.find('input')
input.trigger('focus')
await nextTick()
expect(focusHandler).toHaveBeenCalledTimes(1)
const list = document.querySelectorAll('.el-time-spinner__list')
const hoursEl = list[0]
const hourEl = hoursEl.querySelectorAll('.el-time-spinner__item')[4] as any
hourEl.click()
await nextTick()
expect(changeHandler).toHaveBeenCalledTimes(1);
(document.querySelector('.el-time-panel__btn.cancel') as any).click()
await nextTick()
expect(blurHandler).toHaveBeenCalledTimes(1)
})
it('selectableRange ', async () => {
// ['17:30:00 - 18:30:00', '18:50:00 - 20:30:00', '21:00:00 - 22:00:00']
const disabledHoursArr = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,23]
const wrapper = _mount(`<el-time-picker
v-model="value"
:disabled-hours="disabledHours"
:disabled-minutes="disabledMinutes"
:disabled-seconds="disabledSeconds"
/>`, () => ({ value: '' }), {
methods: {
disabledHours() {
return disabledHoursArr
},
disabledMinutes (hour) {
// ['17:30:00 - 18:30:00', '18:50:00 - 20:30:00', '21:00:00 - 22:00:00']
if (hour === 17) {
return makeRange(0, 29)
}
if (hour === 18) {
return makeRange(31, 49)
}
if (hour === 20) {
return makeRange(31, 59)
}
if (hour === 22) {
return makeRange(1, 59)
}
},
disabledSeconds(hour, minute) {
if (hour === 18 && minute === 30) {
return makeRange(1, 59)
}
if (hour === 20 && minute === 30) {
return makeRange(1, 59)
}
if (hour === 22 && minute === 0) {
return makeRange(1, 59)
}
},
},
})
const input = wrapper.find('input')
input.trigger('focus')
await nextTick()
const list = document.querySelectorAll('.el-time-spinner__list')
const hoursEl = list[0]
const minutesEl = list[1]
const secondsEl = list[2]
const disabledHours = getSpinnerTextAsArray(hoursEl, '.disabled')
expect(disabledHours).toEqual(disabledHoursArr)
const hourSpinners = hoursEl.querySelectorAll('.el-time-spinner__item');
(hourSpinners[18] as any).click()
await nextTick()
const disabledMinutes = getSpinnerTextAsArray(minutesEl, '.disabled')
expect(disabledMinutes.every(t => t > 30 && t < 50)).toBeTruthy()
expect(disabledMinutes.length).toEqual(19);
(hourSpinners[22] as any).click()
await nextTick()
const enabledMinutes = getSpinnerTextAsArray(minutesEl, ':not(.disabled)')
const enabledSeconds = getSpinnerTextAsArray(secondsEl, ':not(.disabled)')
expect(enabledMinutes).toEqual([0])
expect(enabledSeconds).toEqual([0])
})
})
describe('TimePicker(range)', () => {
it('create', async () => {
const wrapper = _mount(`<el-time-picker
v-model="value"
size="mini"
:is-range="true"
/>`, () => ({ value: [new Date(2016, 9, 10, 18, 40), new Date(2016, 9, 10, 19, 40)] }))
expect(wrapper.find('.el-range-editor--mini').exists()).toBeTruthy()
const input = wrapper.find('input')
input.trigger('blur')
input.trigger('focus')
await nextTick()
const list = document.querySelectorAll('.el-time-spinner__list .el-time-spinner__item.active');
['18','40','00','19','40','00'].forEach((_, i) => {
expect(list[i].textContent).toBe(_)
})
})
it('default value', async() => {
const defaultValue = [new Date(2000, 9, 1, 10, 20, 0), new Date(2000, 9, 1, 11, 10, 0)]
const wrapper = _mount(`<el-time-picker
v-model="value"
:default-value="defaultValue"
:is-range="true"
/>`, () => ({ value: '',
defaultValue }))
const input = wrapper.find('input')
input.trigger('blur')
input.trigger('focus')
await nextTick()
const list = document.querySelectorAll('.el-time-spinner__list .el-time-spinner__item.active');
['10','20','00','11','10','00'].forEach((_, i) => {
expect(list[i].textContent).toBe(_)
})
})
it('cancel button', async () => {
const wrapper = _mount(`<el-time-picker
v-model="value"
is-range
/>`, () => ({ value: '' }))
const input = wrapper.find('input')
input.trigger('blur')
input.trigger('focus')
await nextTick();
(document.querySelector('.el-time-panel__btn.cancel') as any).click()
await nextTick()
const vm = wrapper.vm as any
expect(vm.value).toBe('')
input.trigger('blur')
input.trigger('focus')
await nextTick();
(document.querySelector('.el-time-panel__btn.confirm') as any).click()
expect(vm.value instanceof Array).toBeTruthy()
vm.value.forEach(_ => {
expect(_ instanceof Date).toBeTruthy()
})
})
it('selectableRange ', async () => {
// left ['08:00:00 - 12:59:59'] right ['11:00:00 - 16:59:59']
const wrapper = _mount(`<el-time-picker
v-model="value"
is-range
:disabled-hours="disabledHours"
/>`, () => ({ value: [new Date(2016, 9, 10, 9, 40), new Date(2016, 9, 10, 15, 40)] }), {
methods: {
disabledHours(role) {
if (role === 'start') {
return makeRange(0, 7).concat(makeRange(13, 23))
}
return makeRange(0, 10).concat(makeRange(17, 23))
},
},
})
const input = wrapper.find('input')
input.trigger('focus')
await nextTick()
const list = document.querySelectorAll('.el-time-spinner__list')
const leftHoursEl = list[0]
const leftEndbledHours = getSpinnerTextAsArray(leftHoursEl, ':not(.disabled)')
expect(leftEndbledHours).toEqual([ 8, 9, 10, 11, 12 ])
const rightHoursEl = list[3]
const rightEndbledHours = getSpinnerTextAsArray(rightHoursEl, ':not(.disabled)')
expect(rightEndbledHours).toEqual([ 11, 12, 13, 14, 15, 16 ]);
(leftHoursEl.querySelectorAll('.el-time-spinner__item')[12] as any).click()
await nextTick()
const NextRightEndbledHours = getSpinnerTextAsArray(rightHoursEl, ':not(.disabled)')
expect(NextRightEndbledHours).toEqual([ 12, 13, 14, 15, 16 ])
})
})

View File

@ -0,0 +1,5 @@
import { App } from 'vue'
import TimePicker from './src/time-picker'
export default (app: App): void => {
app.component(TimePicker.name, TimePicker)
}

View File

@ -0,0 +1,12 @@
{
"name": "@element-plus/time-picker",
"version": "0.0.0",
"main": "dist/index.js",
"license": "MIT",
"peerDependencies": {
"vue": "^3.0.0-rc.1"
},
"devDependencies": {
"@vue/test-utils": "^2.0.0-beta.0"
}
}

View File

@ -0,0 +1,12 @@
// daterange: 'YYYY-MM-DD',
// monthrange: 'YYYY-MM',
// datetimerange: 'yyyy-MM-DD HH:mm:ss',
export const DEFAULT_FORMATS_TIME = 'HH:mm:ss'
export const DEFAULT_FORMATS_DATE = 'YYYY-MM-DD'
export const DEFAULT_FORMATS_DATEPICKER = {
'date': DEFAULT_FORMATS_DATE,
'week': 'YYYYwWW',
'year': 'YYYY',
'month': 'YYYY-MM',
'datetime': 'YYYY-MM-DD HH:mm:ss',
}

View File

@ -0,0 +1,577 @@
<template>
<!-- todo popper props align left -->
<!-- todo popper custom popper-class -->
<!-- todo bug handleKeydown event twice -->
<el-popper
effect="light"
manual-mode
:value="pickerVisible"
pure
>
<template #trigger>
<el-input
v-if="!isRangeInput"
ref="refContainer"
v-clickoutside="onClickOutside"
:model-value="displayValue"
:name="name"
:size="pickerSize"
:disabled="pickerDisabled"
:placeholder="placeholder"
class="el-date-editor"
:class="'el-date-editor--' + type"
:readonly="!editable || readonly || type === 'dates' || type === 'week'"
@input="onUserInput"
@focus="handleFocus"
@keydown="handleKeydown"
@change="handleChange"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
>
<template #prefix>
<i
class="el-input__icon"
:class="triggerClass"
@click="handleFocus"
>
</i>
</template>
<template #suffix>
<i
class="el-input__icon"
:class="[showClose ? '' + clearIcon : '']"
@click="onClearIconClick"
>
</i>
</template>
</el-input>
<div
v-else
ref="refContainer"
v-clickoutside="onClickOutside"
class="el-date-editor el-range-editor el-input__inner"
:class="[
'el-date-editor--' + type,
pickerSize ? `el-range-editor--${ pickerSize }` : '',
pickerDisabled ? 'is-disabled' : '',
pickerVisible ? 'is-active' : ''
]"
@click="handleFocus"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
@keydown="handleKeydown"
>
<i :class="['el-input__icon', 'el-range__icon', triggerClass]"></i>
<input
autocomplete="off"
:name="name && name[0]"
:placeholder="startPlaceholder"
:value="displayValue && displayValue[0]"
:disabled="pickerDisabled"
:readonly="!editable || readonly"
class="el-range-input"
@input="handleStartInput"
@change="handleStartChange"
@focus="handleFocus"
>
<slot name="range-separator">
<span class="el-range-separator">{{ rangeSeparator }}</span>
</slot>
<input
autocomplete="off"
:name="name && name[1]"
:placeholder="endPlaceholder"
:value="displayValue && displayValue[1]"
:disabled="pickerDisabled"
:readonly="!editable || readonly"
class="el-range-input"
@focus="handleFocus"
@input="handleEndInput"
@change="handleEndChange"
>
<i
:class="[showClose ? '' + clearIcon : '']"
class="el-input__icon el-range__close-icon"
@click="onClearIconClick"
>
</i>
</div>
</template>
<template #default>
<slot
:visible="pickerVisible"
:parsed-value="parsedValue"
:format="format"
:type="type"
:default-value="defaultValue"
v-bind="$attrs"
@pick="onPick"
@select-range="setSelectionRange"
@mousedown.stop
></slot>
</template>
</el-popper>
</template>
<script lang='ts'>
import {
defineComponent,
ref,
computed,
inject,
PropType,
watch,
provide,
} from 'vue'
import dayjs from 'dayjs'
import { ClickOutside } from '@element-plus/directives'
import ElInput from '@element-plus/input/src/index.vue'
import { Popper as ElPopper } from '@element-plus/popper'
import { eventKeys } from '@element-plus/utils/aria'
import mitt from 'mitt'
// Date object and string
const dateEquals = function(a, b) {
const aIsDate = a instanceof Date
const bIsDate = b instanceof Date
if (aIsDate && bIsDate) {
return a.getTime() === b.getTime()
}
if (!aIsDate && !bIsDate) {
return a === b
}
return false
}
const valueEquals = function(a, b) {
const aIsArray = a instanceof Array
const bIsArray = b instanceof Array
if (aIsArray && bIsArray) {
if (a.length !== b.length) {
return false
}
return a.every((item, index) => dateEquals(item, b[index]))
}
if (!aIsArray && !bIsArray) {
return dateEquals(a, b)
}
return false
}
// todo element
const ELEMENT = {
size: '',
}
interface PickerOptions {
isValidValue: any
handleKeydown: any
parseUserInput: any
formatToString: any
getRangeAvaliableTime: any
panelReady: boolean
}
export default defineComponent({
name: 'Picker',
components: {
ElInput,
ElPopper,
},
directives: { clickoutside: ClickOutside },
props: {
name: {
type: [Array, String],
default: '',
},
format: {
type: String,
required: true,
},
type: {
type: String,
default: '',
},
clearable: {
type: Boolean,
default: true,
},
clearIcon: {
type: String,
default: 'el-icon-circle-close',
},
editable: {
type: Boolean,
default: true,
},
prefixIcon:{
type: String,
default: '',
},
size:{
type: String,
default: '',
},
readonly: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
placeholder: {
type: String,
default: '',
},
modelValue: {
type: [Date, Array, String] as PropType<string | Date | Date[]>,
default: '',
},
rangeSeparator: {
type: String,
default: '-',
},
startPlaceholder: String,
endPlaceholder: String,
defaultValue: {
type: [Date, Array] as PropType<Date | Date[]>,
default: new Date(),
},
isRange: {
type: Boolean,
default: false,
},
disabledHours: {
type: Function,
default: null,
},
disabledMinutes: {
type: Function,
default: null,
},
disabledSeconds: {
type: Function,
default: null,
},
},
emits: ['update:modelValue', 'change', 'focus', 'blur'],
setup(props, ctx) {
const oldValue = ref(props.modelValue)
const refContainer = ref(null)
const pickerVisible = ref(false)
const valueOnOpen = ref(null)
watch(pickerVisible, val => {
if (!val) {
userInput.value = null
ctx.emit('blur')
blurInput()
} else {
valueOnOpen.value = props.modelValue
}
})
const emitChange = val => {
// determine user real change only
if (!valueEquals(val, valueOnOpen.value)) {
ctx.emit('change', val)
}
}
const emitInput = val => {
if (!valueEquals(props.modelValue, val)) {
ctx.emit('update:modelValue', val)
}
}
const refInput = computed(() => {
if (refContainer.value) {
const _r = isRangeInput.value ? refContainer.value : refContainer.value.$el
return [].slice.call(_r.querySelectorAll('input'))
}
return []
})
const setSelectionRange = (start, end, pos) => {
const _inputs = refInput.value
if (!_inputs.length) return
if (!pos || pos === 'min') {
_inputs[0].setSelectionRange(start, end)
_inputs[0].focus()
} else if (pos === 'max') {
_inputs[1].setSelectionRange(start, end)
_inputs[1].focus()
}
}
const onPick = (date: any = '', visible = false, useOldValue = false) => {
pickerVisible.value = visible
let result
if (useOldValue) {
result = oldValue.value
} else {
if (Array.isArray(date)) {
result = date.map(_ => _.toDate())
} else {
result = date.toDate()
}
}
userInput.value = null
emitInput(result)
emitChange(result)
}
const handleFocus = e => {
if (props.readonly || pickerDisabled.value) return
pickerVisible.value = true
ctx.emit('focus', e)
}
const elForm = inject('elForm', {} as any)
const pickerDisabled = computed(() =>{
return props.disabled || elForm.disabled
})
const parsedValue = computed(() => {
let result
if (isRangeInput.value) {
if (!props.modelValue) {
if (Array.isArray(props.defaultValue)) {
result = (props.defaultValue as Array<Date>).map(_=> dayjs(_))
} else {
result = [
dayjs(props.defaultValue as Date),
dayjs(props.defaultValue as Date).add(60,'m'),
]
}
} else {
result = (props.modelValue as Array<Date>).map(_=> dayjs(_))
}
} else {
if (!props.modelValue) {
result = dayjs(props.defaultValue as Date)
} else {
result = dayjs(props.modelValue as Date)
}
}
if (pickerOptions.value.getRangeAvaliableTime) {
result = pickerOptions.value.getRangeAvaliableTime(result)
}
return result
})
const displayValue = computed(() => {
if (!pickerOptions.value.panelReady) return
if (!pickerVisible.value && !props.modelValue) return
const formattedValue = formatDayjsToString(parsedValue.value)
if (Array.isArray(userInput.value)) {
return [
userInput.value[0] || (formattedValue && formattedValue[0]) || '',
userInput.value[1] || (formattedValue && formattedValue[1]) || '',
]
} else if (userInput.value !== null) {
return userInput.value
}
if (formattedValue) {
return props.type === 'dates'
? (formattedValue as Array<string>).join(', ')
: formattedValue
}
return ''
})
const triggerClass = computed(() => {
return props.prefixIcon || (props.type.indexOf('time') !== -1 ? 'el-icon-time' : 'el-icon-date')
})
const showClose = ref(false)
const onClearIconClick = event =>{
if (props.readonly || pickerDisabled.value) return
if (showClose.value) {
event.stopPropagation()
emitInput(null)
emitChange(null)
showClose.value = false
pickerVisible.value = false
}
}
const valueIsEmpty = computed(() => {
return !props.modelValue
})
const onMouseEnter = () => {
if (props.readonly || pickerDisabled.value) return
if (!valueIsEmpty.value && props.clearable) {
showClose.value = true
}
}
const onMouseLeave = e => {
if (e.relatedTarget && e.relatedTarget.className.includes('icon')) {
// if not el-icon then close
return
}
showClose.value = false
}
const isRangeInput = computed(() => {
return props.type.indexOf('range') > -1
})
const elFormItem = inject('elFormItem', {} as any)
const elFormItemSize = computed(() => {
return elFormItem.elFormItemSize
})
const pickerSize = computed(() => {
return props.size || elFormItemSize.value || (ELEMENT || {}).size
})
const onClickOutside = () => {
if (!pickerVisible.value) return
pickerVisible.value = false
}
const userInput =ref(null)
const handleChange = () => {
if (userInput.value) {
const value = parseUserInputToDayjs(displayValue.value)
if (value) {
if (isValidValue(value)) {
emitInput(value)
userInput.value = null
}
}
}
if (userInput.value === '') {
emitInput(null)
emitChange(null)
userInput.value = null
}
}
const blurInput = () => {
refInput.value.forEach(input => input.blur())
}
const parseUserInputToDayjs = value => {
return pickerOptions.value.parseUserInput(value)
}
const formatDayjsToString = value => {
return pickerOptions.value.formatToString(value)
}
const isValidValue = value => {
return pickerOptions.value.isValidValue(value)
}
const handleKeydown = event => {
const keyCode = event.keyCode
if (keyCode === eventKeys.esc) {
pickerVisible.value = false
event.stopPropagation()
return
}
if (keyCode === eventKeys.tab) {
if (!isRangeInput.value) {
handleChange()
pickerVisible.value = false
event.stopPropagation()
} else {
// user may change focus between two input
setTimeout(() => {
if (refInput.value.indexOf(document.activeElement) === -1) {
pickerVisible.value = false
blurInput()
}
}, 0)
}
return
}
if (keyCode === eventKeys.enter) {
if (userInput.value === '' || isValidValue(parseUserInputToDayjs(displayValue.value))) {
handleChange()
pickerVisible.value = false
}
event.stopPropagation()
return
}
// if user is typing, do not let picker handle key input
if (userInput.value) {
event.stopPropagation()
return
}
if (pickerOptions.value.handleKeydown) {
pickerOptions.value.handleKeydown(event)
}
}
const onUserInput = e => {
userInput.value = e.target.value
}
const handleStartInput = event => {
if (userInput.value) {
userInput.value = [event.target.value, userInput.value[1]]
} else {
userInput.value = [event.target.value, null]
}
}
const handleEndInput = event => {
if (userInput.value) {
userInput.value = [userInput.value[0], event.target.value]
} else {
userInput.value = [null, event.target.value]
}
}
const handleStartChange = () => {
const value = parseUserInputToDayjs(userInput.value && userInput.value[0])
if (value) {
userInput.value = [formatDayjsToString(value), displayValue.value[1]]
const newValue = [value, parsedValue.value && parsedValue.value[1]]
if (isValidValue(newValue)) {
emitInput(newValue)
userInput.value = null
}
}
}
const handleEndChange = () => {
const value = parseUserInputToDayjs(userInput.value && userInput.value[1])
if (value) {
userInput.value = [displayValue.value[0], formatDayjsToString(value)]
const newValue = [parsedValue.value && parsedValue.value[0], value]
if (isValidValue(newValue)) {
emitInput(newValue)
userInput.value = null
}
}
}
const pickerOptions = ref({} as PickerOptions)
const pickerHub = mitt()
pickerHub.on('SetPickerOption', e => {
pickerOptions.value[e[0]] = e[1]
pickerOptions.value.panelReady = true
})
provide('EP_PICKER_BASE', {
hub: pickerHub,
props,
})
return {
handleEndChange,
handleStartChange,
handleStartInput,
handleEndInput,
onUserInput,
handleChange,
handleKeydown,
onClickOutside,
pickerSize,
isRangeInput,
onMouseLeave,
onMouseEnter,
onClearIconClick,
showClose,
triggerClass,
onPick,
handleFocus,
pickerVisible,
displayValue,
parsedValue,
setSelectionRange,
refContainer,
pickerDisabled,
}
},
})
</script>

View File

@ -0,0 +1,362 @@
<template>
<div class="el-time-spinner" :class="{ 'has-seconds': showSeconds }">
<template v-if="!arrowControl">
<el-scrollbar
v-for="item in spinnerItems"
:key="item"
:ref="getRefId(item)"
class="el-time-spinner__wrapper"
wrap-style="max-height: inherit;"
view-class="el-time-spinner__list"
noresize
tag="ul"
@mouseenter="emitSelectRange(item)"
@mousemove="adjustCurrentSpinner(item)"
>
<li
v-for="(disabled, key) in listMap[item].value"
:key="key"
class="el-time-spinner__item"
:class="{ 'active': key === timePartsMap[item].value, disabled }"
@click="handleClick(item, { value: key, disabled })"
>
<template v-if="item === 'hours'">
{{ ('0' + (amPmMode ? (key % 12 || 12) : key )).slice(-2) }}{{ getAmPmFlag(key) }}
</template>
<template v-else>
{{ ('0' + key).slice(-2) }}
</template>
</li>
</el-scrollbar>
</template>
<template v-if="arrowControl">
<div
v-for="item in spinnerItems"
:key="item"
class="el-time-spinner__wrapper is-arrow"
@mouseenter="emitSelectRange(item)"
>
<i v-repeat-click="onDecreaseClick" class="el-time-spinner__arrow el-icon-arrow-up"></i>
<i v-repeat-click="onIncreaseClick" class="el-time-spinner__arrow el-icon-arrow-down"></i>
<ul class="el-time-spinner__list">
<li
v-for="(time, key) in arrowListMap[item].value"
:key="key"
class="el-time-spinner__item"
:class="{ 'active': time === timePartsMap[item].value, 'disabled': listMap[item].value[time] }"
>
{{ time === undefined ? '' : ('0' + (amPmMode ? (time % 12 || 12) : time )).slice(-2) + getAmPmFlag(time) }}
</li>
</ul>
</div>
</template>
</div>
</template>
<script lang='ts'>
import {
defineComponent,
ref,
nextTick,
computed,
onMounted,
inject,
Ref,
PropType,
watch,
} from 'vue'
import { Dayjs } from 'dayjs'
import { RepeatClick } from '@element-plus/directives'
import ElScrollbar from '@element-plus/scrollbar/src'
import { getTimeLists } from './useTimePicker'
export default defineComponent({
directives: {
repeatClick: RepeatClick,
},
components: {
ElScrollbar,
},
props: {
role: {
type: String,
required: true,
},
spinnerDate: {
type: Dayjs as PropType<Dayjs>,
required: true,
},
showSeconds: {
type: Boolean,
default: true,
},
arrowControl: Boolean,
amPmMode: {
type: String,
default: '', // 'a': am/pm; 'A': AM/PM
},
},
emits: ['change', 'select-range'],
setup(props, ctx) {
// data
const currentScrollbar = ref(null)
const listHoursRef: Ref<Nullable<HTMLElement>> = ref(null)
const listMinutesRef: Ref<Nullable<HTMLElement>> = ref(null)
const listSecondsRef: Ref<Nullable<HTMLElement>> = ref(null)
const listRefsMap = {
hours: listHoursRef, minutes: listMinutesRef, seconds: listSecondsRef,
}
// computed
const spinnerItems = computed(() => {
const arr = ['hours', 'minutes', 'seconds']
return props.showSeconds ? arr : arr.slice(0, 2)
})
const hours = computed(() => {
return props.spinnerDate.hour()
})
const minutes = computed(() => {
return props.spinnerDate.minute()
})
const seconds = computed(() => {
return props.spinnerDate.second()
})
const timePartsMap = computed(() => ({
hours, minutes, seconds,
}))
const hoursList = computed(() =>{
return getHoursList(props.role)
})
const minutesList = computed(() =>{
return getMinutesList(hours.value, props.role)
})
const secondsList = computed(() =>{
return getSecondsList(hours.value, minutes.value, props.role)
})
const listMap = computed(() => ({
hours: hoursList,
minutes: minutesList,
seconds: secondsList,
}))
const arrowHourList = computed(() => {
const hour = hours.value
return [
hour > 0 ? hour - 1 : undefined,
hour,
hour < 23 ? hour + 1 : undefined,
]
})
const arrowMinuteList = computed(()=> {
const minute = minutes.value
return [
minute > 0 ? minute - 1 : undefined,
minute,
minute < 59 ? minute + 1 : undefined,
]
})
const arrowSecondList = computed(() =>{
const second = seconds.value
return [
second > 0 ? second - 1 : undefined,
second,
second < 59 ? second + 1 : undefined,
]
})
const arrowListMap = computed(() => ({
hours: arrowHourList,
minutes: arrowMinuteList,
seconds: arrowSecondList,
}))
const getAmPmFlag = hour => {
let shouldShowAmPm = !!props.amPmMode
if (!shouldShowAmPm) return ''
let isCapital = props.amPmMode === 'A'
// todo locale
let content = (hour < 12) ? ' am' : ' pm'
if (isCapital) content = content.toUpperCase()
return content
}
const emitSelectRange = type =>{
if (type === 'hours') {
ctx.emit('select-range', 0, 2)
} else if (type === 'minutes') {
ctx.emit('select-range', 3, 5)
} else if (type === 'seconds') {
ctx.emit('select-range', 6, 8)
}
currentScrollbar.value = type
}
const adjustCurrentSpinner = type =>{
adjustSpinner(type, timePartsMap.value[type].value)
}
// NOTE: used by datetime / date-range panel
// renamed from adjustScrollTop
// should try to refactory it
const adjustSpinners = () => {
adjustCurrentSpinner('hours')
adjustCurrentSpinner('minutes')
adjustCurrentSpinner('seconds')
}
const adjustSpinner = (type, value) => {
if (props.arrowControl) return
const el = listRefsMap[type]
if (el.value) {
el.value.$el.querySelector('.el-scrollbar__wrap').scrollTop = Math.max(0, value * typeItemHeight(type))
}
}
const typeItemHeight = type =>{
const el = listRefsMap[type]
return el.value.$el.querySelector('li').offsetHeight
}
const onIncreaseClick = () => {
scrollDown(1)
}
const onDecreaseClick = () => {
scrollDown(-1)
}
const scrollDown = step => {
if (!currentScrollbar.value) {
emitSelectRange('hours')
}
const label = currentScrollbar.value
let now = timePartsMap.value[label].value
const total = currentScrollbar.value === 'hours' ? 24 : 60
now = (now + step + total) % total
modifyDateField(label, now)
adjustSpinner(label, now)
nextTick(() => emitSelectRange(currentScrollbar.value))
}
const modifyDateField = (type, value) => {
const list = listMap.value[type].value
const isDisabled = list[value]
if (isDisabled) return
switch (type) {
case 'hours': ctx.emit('change',
props.spinnerDate
.hour(value)
.minute(minutes.value)
.second(seconds.value))
break
case 'minutes': ctx.emit('change',
props.spinnerDate
.hour(hours.value)
.minute(value)
.second(seconds.value))
break
case 'seconds': ctx.emit('change',
props.spinnerDate
.hour(hours.value)
.minute(minutes.value)
.second(value))
break
}
}
const handleClick = (type, { value, disabled }) => {
if (!disabled) {
modifyDateField(type, value)
emitSelectRange(type)
adjustSpinner(type, value)
}
}
const handleScroll = type => {
const value = Math.min(Math.round((listRefsMap[type].value.$el.querySelector('.el-scrollbar__wrap').scrollTop - (scrollBarHeight(type) * 0.5 - 10) / typeItemHeight(type) + 3) / typeItemHeight(type)), (type === 'hours' ? 23 : 59))
modifyDateField(type, value)
}
const scrollBarHeight = type => {
return listRefsMap[type].value.$el.offsetHeight
}
const bindScrollEvent = () => {
const bindFuntion = type => {
if (listRefsMap[type].value) {
listRefsMap[type].value.$el.querySelector('.el-scrollbar__wrap').onscroll = () => {
// TODO: scroll is emitted when set scrollTop programatically
// should find better solutions in the future!
handleScroll(type)
}
}
}
bindFuntion('hours')
bindFuntion('minutes')
bindFuntion('seconds')
}
onMounted(() => {
nextTick(() => {
!props.arrowControl && bindScrollEvent()
adjustSpinners()
// set selection on the first hour part
if (props.role === 'start') emitSelectRange('hours')
})
})
const getRefId = item => {
return `list${item.charAt(0).toUpperCase() + item.slice(1)}Ref`
}
const pickerPanel = inject('EP_TIMEPICK_PANEL') as any
pickerPanel.hub.emit('SetOption',[`${props.role}_scrollDown`, scrollDown])
pickerPanel.hub.emit('SetOption',[`${props.role}_emitSelectRange`, emitSelectRange])
const {
getHoursList,
getMinutesList,
getSecondsList,
} = getTimeLists(
pickerPanel.methods.disabledHours,
pickerPanel.methods.disabledMinutes,
pickerPanel.methods.disabledSeconds,
)
watch(() => props.spinnerDate, () => {
adjustSpinners()
})
return {
getRefId,
spinnerItems,
currentScrollbar,
hours,
minutes,
seconds,
hoursList,
minutesList,
arrowHourList,
arrowMinuteList,
arrowSecondList,
getAmPmFlag,
emitSelectRange,
adjustCurrentSpinner,
typeItemHeight,
listHoursRef,
listMinutesRef,
listSecondsRef,
onIncreaseClick,
onDecreaseClick,
handleClick,
secondsList,
timePartsMap,
arrowListMap,
listMap,
}
},
})
</script>

View File

@ -0,0 +1,224 @@
<template>
<transition name="el-zoom-in-top">
<div
v-if="visible"
class="el-time-panel"
>
<div class="el-time-panel__content" :class="{ 'has-seconds': showSeconds }">
<time-spinner
ref="spinner"
role="start"
:arrow-control="arrowControl"
:show-seconds="showSeconds"
:am-pm-mode="amPmMode"
:spinner-date="parsedValue"
@change="handleChange"
@select-range="setSelectionRange"
/>
</div>
<div class="el-time-panel__footer">
<button
type="button"
class="el-time-panel__btn cancel"
@click="handleCancel"
>
{{ t('el.datepicker.cancel') }}
</button>
<button
type="button"
class="el-time-panel__btn confirm"
@click="handleConfirm()"
>
{{ t('el.datepicker.confirm') }}
</button>
</div>
</div>
</transition>
</template>
<script lang="ts">
import {
defineComponent,
ref,
computed,
inject,
provide,
PropType,
} from 'vue'
import { eventKeys } from '@element-plus/utils/aria'
import { t } from '@element-plus/locale'
import mitt from 'mitt'
import TimeSpinner from './basic-time-spinner.vue'
import dayjs, { Dayjs } from 'dayjs'
import { getAvaliableArrs } from './useTimePicker'
export default defineComponent({
components: {
TimeSpinner,
},
props: {
visible: {
type: [Boolean],
default: false,
},
parsedValue: {
type: Dayjs as PropType<Dayjs>,
default: '',
},
arrowControl: {
type: [Boolean],
default: false,
},
pickerOptions: {
type: Object,
default: () => ({}),
},
format: {
type: String,
default: '',
},
},
emits: ['pick', 'select-range'],
setup(props, ctx) {
// data
const selectionRange = ref([0, 2])
// computed
const showSeconds = computed(() => {
return props.format.includes('ss')
})
const amPmMode = computed(() => {
if (props.format.includes('A')) return 'A'
if (props.format.includes('a')) return 'a'
return ''
})
// method
const isValidValue = _date => {
const parsedDate = dayjs(_date)
const result = getRangeAvaliableTime(parsedDate)
return parsedDate.isSame(result)
}
const handleCancel = () => {
ctx.emit('pick', '', false, true)
}
const handleConfirm = (visible = false, first) => {
if (first) return
ctx.emit('pick', props.parsedValue, visible)
}
const handleChange = (_date: Dayjs) => {
// visible avoids edge cases, when use scrolls during panel closing animation
if (!props.visible) { return }
const result = getRangeAvaliableTime(_date).millisecond(0)
ctx.emit('pick', result, true)
}
const setSelectionRange = (start, end) => {
ctx.emit('select-range', start, end)
selectionRange.value = [start, end]
}
const changeSelectionRange = step => {
const list = [0, 3].concat(showSeconds.value ? [6] : [])
const mapping = ['hours', 'minutes'].concat(showSeconds.value ? ['seconds'] : [])
const index = list.indexOf(selectionRange.value[0])
const next = (index + step + list.length) % list.length
timePickeOptions['start_emitSelectRange'](mapping[next])
}
const handleKeydown = event => {
const keyCode = event.keyCode
if (keyCode === eventKeys.left || keyCode === eventKeys.right) {
const step = (keyCode === eventKeys.left) ? -1 : 1
changeSelectionRange(step)
event.preventDefault()
return
}
if (keyCode === eventKeys.up || keyCode === eventKeys.down) {
const step = (keyCode === eventKeys.up) ? -1 : 1
timePickeOptions['min_scrollDown'](step)
event.preventDefault()
return
}
}
const getRangeAvaliableTime = (date: Dayjs) => {
const avaliableMap = {
hour: getAvaliableHours,
minute: getAvaliableMinutes,
second: getAvaliableSeconds,
}
let result = date;
['hour', 'minute', 'second'].forEach(_ => {
if (avaliableMap[_]) {
let avaliableArr
const method = avaliableMap[_]
if (_ === 'minute') {
avaliableArr = method(result.hour())
} else if (_ === 'second') {
avaliableArr = method(result.hour(), result.minute())
} else {
avaliableArr = method()
}
if (avaliableArr && avaliableArr.length && !avaliableArr.includes(result[_]())) {
result = result[_](avaliableArr[0])
}
}
})
return result
}
const parseUserInput = value => {
if (!value) return null
return dayjs(value, props.format)
}
const formatToString = value => {
if (!value) return null
return value.format(props.format)
}
const pickerBase = inject('EP_PICKER_BASE') as any
pickerBase.hub.emit('SetPickerOption', ['isValidValue', isValidValue])
pickerBase.hub.emit('SetPickerOption', ['formatToString', formatToString])
pickerBase.hub.emit('SetPickerOption', ['parseUserInput', parseUserInput])
pickerBase.hub.emit('SetPickerOption',['handleKeydown', handleKeydown])
pickerBase.hub.emit('SetPickerOption',['getRangeAvaliableTime', getRangeAvaliableTime])
const timePickeOptions = {} as any
const pickerHub = mitt()
pickerHub.on('SetOption', e => {
timePickeOptions[e[0]] = e[1]
})
const { disabledHours, disabledMinutes, disabledSeconds } = pickerBase.props
const {
getAvaliableHours,
getAvaliableMinutes,
getAvaliableSeconds,
} = getAvaliableArrs(disabledHours, disabledMinutes, disabledSeconds)
provide('EP_TIMEPICK_PANEL', {
hub: pickerHub,
methods: {
disabledHours, disabledMinutes, disabledSeconds,
},
})
return {
t,
handleConfirm,
handleChange,
setSelectionRange,
amPmMode,
showSeconds,
handleCancel,
}
},
})
</script>
<style scoped>
.el-time-panel {
position: relative;
}
</style>

View File

@ -0,0 +1,340 @@
<template>
<transition
name="el-zoom-in-top"
>
<div
v-if="visible"
class="el-time-range-picker el-picker-panel"
>
<div class="el-time-range-picker__content">
<div class="el-time-range-picker__cell">
<div class="el-time-range-picker__header">{{ t('el.datepicker.startTime') }}</div>
<div
:class="{ 'has-seconds': showSeconds, 'is-arrow': arrowControl }"
class="el-time-range-picker__body el-time-panel__content"
>
<time-spinner
ref="minSpinner"
role="start"
:show-seconds="showSeconds"
:am-pm-mode="amPmMode"
:arrow-control="arrowControl"
:spinner-date="minDate"
@change="handleMinChange"
@select-range="setMinSelectionRange"
/>
</div>
</div>
<div class="el-time-range-picker__cell">
<div class="el-time-range-picker__header">{{ t('el.datepicker.endTime') }}</div>
<div
:class="{ 'has-seconds': showSeconds, 'is-arrow': arrowControl }"
class="el-time-range-picker__body el-time-panel__content"
>
<time-spinner
ref="maxSpinner"
role="end"
:show-seconds="showSeconds"
:am-pm-mode="amPmMode"
:arrow-control="arrowControl"
:spinner-date="maxDate"
@change="handleMaxChange"
@select-range="setMaxSelectionRange"
/>
</div>
</div>
</div>
<div class="el-time-panel__footer">
<button
type="button"
class="el-time-panel__btn cancel"
@click="handleCancel()"
>
{{ t('el.datepicker.cancel') }}
</button>
<button
type="button"
class="el-time-panel__btn confirm"
:disabled="btnConfirmDisabled"
@click="handleConfirm()"
>
{{ t('el.datepicker.confirm') }}
</button>
</div>
</div>
</transition>
</template>
<script lang="ts">
import {
defineComponent,
ref,
computed,
PropType,
inject,
provide,
} from 'vue'
import dayjs, { Dayjs } from 'dayjs'
import mitt from 'mitt'
import union from 'lodash/union'
import { t } from '@element-plus/locale'
import { eventKeys } from '@element-plus/utils/aria'
import TimeSpinner from './basic-time-spinner.vue'
import { getAvaliableArrs } from './useTimePicker'
const makeSelectRange = (start, end) => {
const result = []
for (let i = start; i <= end; i++) {
result.push(i)
}
return result
}
export default defineComponent({
components: { TimeSpinner },
props: {
visible:{
type: [Boolean],
default: false,
},
arrowControl: {
type: [Boolean],
default: false,
},
parsedValue: {
type: Array as PropType<Array<Dayjs>>,
default: '',
},
format: {
type: String,
default: '',
},
},
emits: ['pick', 'select-range'],
setup(props, ctx) {
const minDate = computed(() => props.parsedValue[0])
const maxDate = computed(() => props.parsedValue[1])
const handleCancel = () =>{
ctx.emit('pick', null, null, true)
}
const showSeconds = computed(() => {
return props.format.includes('ss')
})
const amPmMode = computed(() => {
if (props.format.includes('A')) return 'A'
if (props.format.includes('a')) return 'a'
return ''
})
const minSelectableRange = ref([])
const maxSelectableRange = ref([])
const handleConfirm = (visible = false) => {
ctx.emit('pick', [minDate.value, maxDate.value], visible)
}
const handleMinChange = date => {
handleChange(date.millisecond(0), maxDate.value)
}
const handleMaxChange = date => {
handleChange(minDate.value, date.millisecond(0))
}
const isValidValue = _date => {
const parsedDate = _date.map(_=> dayjs(_))
const result = getRangeAvaliableTime(parsedDate)
return parsedDate[0].isSame(result[0]) && parsedDate[1].isSame(result[1])
}
const handleChange = (_minDate, _maxDate) => {
// todo getRangeAvaliableTime(_date).millisecond(0)
ctx.emit('pick', [_minDate, _maxDate], true)
}
const btnConfirmDisabled = computed(() => {
return minDate.value > maxDate.value
})
const selectionRange = ref([0,2])
const setMinSelectionRange = (start, end) => {
ctx.emit('select-range', start, end, 'min')
selectionRange.value = [start, end]
}
const offset = computed(() => showSeconds.value ? 11 : 8)
const setMaxSelectionRange = (start, end) => {
ctx.emit('select-range', start, end, 'max')
selectionRange.value = [start + offset.value, end + offset.value]
}
const changeSelectionRange = step => {
const list = showSeconds.value ? [0, 3, 6, 11, 14, 17] : [0, 3, 8, 11]
const mapping = ['hours', 'minutes'].concat(showSeconds.value ? ['seconds'] : [])
const index = list.indexOf(selectionRange.value[0])
const next = (index + step + list.length) % list.length
const half = list.length / 2
if (next < half) {
timePickeOptions['start_emitSelectRange'](mapping[next])
} else {
timePickeOptions['end_emitSelectRange'](mapping[next - half])
}
}
const handleKeydown = event => {
const keyCode = event.keyCode
if (keyCode === eventKeys.left || keyCode === eventKeys.right) {
const step = (keyCode === eventKeys.left) ? -1 : 1
changeSelectionRange(step)
event.preventDefault()
return
}
if (keyCode === eventKeys.up || keyCode === eventKeys.down) {
const step = (keyCode === eventKeys.up) ? -1 : 1
const role = selectionRange.value[0] < offset.value ? 'start' : 'end'
timePickeOptions[`${role}_scrollDown`](step)
event.preventDefault()
return
}
}
const disabledHours_ = (role, compare) => {
const defaultDisable = disabledHours ? disabledHours(role) : []
const isStart = role === 'start'
const compareDate = compare || (isStart ? maxDate.value : minDate.value)
const compareHour = compareDate.hour()
const nextDisable = isStart ? makeSelectRange(compareHour + 1, 23) : makeSelectRange(0, compareHour - 1)
return union(defaultDisable, nextDisable)
}
const disabledMinutes_ = (hour, role, compare) => {
const defaultDisable = disabledMinutes ? disabledMinutes(hour, role) : []
const isStart = role === 'start'
const compareDate = compare || (isStart ? maxDate.value : minDate.value)
const compareHour = compareDate.hour()
if (hour !== compareHour) {
return defaultDisable
}
const compareMinute = compareDate.minute()
const nextDisable = isStart ? makeSelectRange(compareMinute + 1, 59) : makeSelectRange(0, compareMinute - 1)
return union(defaultDisable, nextDisable)
}
const disabledSeconds_ = (hour, minute, role, compare) => {
const defaultDisable = disabledSeconds ? disabledSeconds(hour, minute, role) : []
const isStart = role === 'start'
const compareDate = compare || (isStart ? maxDate.value : minDate.value)
const compareHour = compareDate.hour()
const compareMinute = compareDate.minute()
if (hour !== compareHour || minute !== compareMinute) {
return defaultDisable
}
const compareSecond = compareDate.second()
const nextDisable = isStart ? makeSelectRange(compareSecond + 1, 59) : makeSelectRange(0, compareSecond - 1)
return union(defaultDisable, nextDisable)
}
const getRangeAvaliableTime = (dates: Array<Dayjs>) => {
return dates.map((_, index) => getRangeAvaliableTimeEach(dates[0], dates[1], index === 0 ? 'start' : 'end'))
}
const {
getAvaliableHours,
getAvaliableMinutes,
getAvaliableSeconds,
} = getAvaliableArrs(disabledHours_, disabledMinutes_, disabledSeconds_)
const getRangeAvaliableTimeEach = (startDate: Dayjs, endDate: Dayjs, role) => {
const avaliableMap = {
hour: getAvaliableHours,
minute: getAvaliableMinutes,
second: getAvaliableSeconds,
}
const isStart = role === 'start'
let result = isStart ? startDate : endDate
const compareDate = isStart ? endDate : startDate;
['hour', 'minute', 'second'].forEach(_ => {
if (avaliableMap[_]) {
let avaliableArr
const method = avaliableMap[_]
if (_ === 'minute') {
avaliableArr = method(result.hour(), role, compareDate)
} else if (_ === 'second') {
avaliableArr = method(result.hour(), result.minute(), role, compareDate)
} else {
avaliableArr = method(role, compareDate)
}
if (avaliableArr && avaliableArr.length && !avaliableArr.includes(result[_]())) {
const pos = isStart ? 0 : avaliableArr.length - 1
result = result[_](avaliableArr[pos])
}
}
})
return result
}
const parseUserInput = value => {
if (!value) return null
if (Array.isArray(value)) {
return value.map(_=> dayjs(_, props.format))
}
return dayjs(value, props.format)
}
const formatToString = value => {
if (!value) return null
if (Array.isArray(value)) {
return value.map(_=> _.format(props.format))
}
return value.format(props.format)
}
const pickerBase = inject('EP_PICKER_BASE') as any
pickerBase.hub.emit('SetPickerOption',['formatToString', formatToString])
pickerBase.hub.emit('SetPickerOption',['parseUserInput', parseUserInput])
pickerBase.hub.emit('SetPickerOption',['isValidValue', isValidValue])
pickerBase.hub.emit('SetPickerOption',['handleKeydown', handleKeydown])
pickerBase.hub.emit('SetPickerOption',['getRangeAvaliableTime', getRangeAvaliableTime])
const timePickeOptions = {} as any
const pickerHub = mitt()
pickerHub.on('SetOption', e => {
timePickeOptions[e[0]] = e[1]
})
const { disabledHours, disabledMinutes, disabledSeconds } = pickerBase.props
provide('EP_TIMEPICK_PANEL', {
hub: pickerHub,
methods: {
disabledHours: disabledHours_,
disabledMinutes: disabledMinutes_,
disabledSeconds: disabledSeconds_,
},
})
return {
setMaxSelectionRange,
setMinSelectionRange,
btnConfirmDisabled,
handleCancel,
handleConfirm,
t,
showSeconds,
minDate,
maxDate,
amPmMode,
handleMinChange,
handleMaxChange,
minSelectableRange,
maxSelectableRange,
}
},
})
</script>
<style scoped>
.el-time-range-picker__content {
z-index: 1
}
</style>

View File

@ -0,0 +1,63 @@
const makeList = (total, method, methodFunc) => {
const arr = []
const disabledArr = method && methodFunc()
for (let i = 0; i < total; i++) {
arr[i] = disabledArr ? disabledArr.includes(i) : false
}
return arr
}
const makeAvaliableArr = list => {
return list.map((_, index) => !_ ? index : _).filter(_ => _ !== true)
}
export const getTimeLists = (disabledHours, disabledMinutes, disabledSeconds) => {
const getHoursList = (role, compare?) => {
return makeList(24, disabledHours, () => disabledHours(role, compare))
}
const getMinutesList = (hour, role, compare?) => {
return makeList(60, disabledMinutes, () => disabledMinutes(hour, role, compare))
}
const getSecondsList = (hour, minute, role, compare?) => {
return makeList(60, disabledSeconds, () => disabledSeconds(hour, minute, role, compare))
}
return {
getHoursList,
getMinutesList,
getSecondsList,
}
}
export const getAvaliableArrs = (disabledHours, disabledMinutes, disabledSeconds) => {
const {
getHoursList,
getMinutesList,
getSecondsList,
} = getTimeLists(
disabledHours,
disabledMinutes,
disabledSeconds,
)
const getAvaliableHours = (role, compare?) => {
return makeAvaliableArr(getHoursList(role, compare))
}
const getAvaliableMinutes = (hour, role, compare?) => {
return makeAvaliableArr(getMinutesList(hour, role, compare))
}
const getAvaliableSeconds = (hour, minute, role, compare?) => {
return makeAvaliableArr(getSecondsList(hour, minute, role, compare))
}
return {
getAvaliableHours,
getAvaliableMinutes,
getAvaliableSeconds,
}
}

View File

@ -0,0 +1,29 @@
import dayjs from 'dayjs'
import customParseFormat from 'dayjs/plugin/customParseFormat'
import { h } from 'vue'
import { DEFAULT_FORMATS_TIME } from './common/constant'
import Picker from './common/picker.vue'
import TimePickPanel from './time-picker-com/panel-time-pick.vue'
import TimeRangePanel from './time-picker-com/panel-time-range.vue'
dayjs.extend(customParseFormat)
export default {
name: 'ElTimePicker',
props: {
isRange: {
type: Boolean,
default: false,
},
},
setup(props) {
const type = props.isRange ? 'timerange' : 'time'
const panel = props.isRange ? TimeRangePanel : TimePickPanel
return () => h(Picker, {
format: DEFAULT_FORMATS_TIME,
...props,
type,
},
{
default: scopedProps => h(panel, scopedProps),
})
},
}

View File

@ -79,7 +79,10 @@ export const escapeRegexpString = (value = ''): string =>
// Use native Array.find, Array.findIndex instead
// coerce truthy value to array
export const coerceTruthyValueToArray = castArray
export const coerceTruthyValueToArray = arr => {
if (!arr) { return [] }
return castArray(arr)
}
export const isIE = function(): boolean {
return !isServer && !isNaN(Number(document.DOCUMENT_NODE))

View File

@ -1,5 +1,5 @@
import ResizeObserver from 'resize-observer-polyfill'
import { NOOP } from '@vue/shared'
const isServer = typeof window === 'undefined'
// TODO: add hack prototype __resizeListeners__
@ -21,7 +21,7 @@ export const addResizeListener = function(element, fn) {
if (isServer) return
if (!element.__resizeListeners__) {
element.__resizeListeners__ = []
element.__ro__ = new ResizeObserver(() => {})
element.__ro__ = new ResizeObserver(NOOP)
element.__ro__.observe(element)
}
element.__resizeListeners__.push(fn)

View File

@ -48,14 +48,14 @@ Basic date picker measured by 'day'.
onClick(picker) {
const date = new Date();
date.setTime(date.getTime() - 3600 * 1000 * 24);
picker.$emit('pick', date);
picker.emit('pick', date);
}
}, {
text: 'A week ago',
onClick(picker) {
const date = new Date();
date.setTime(date.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', date);
picker.emit('pick', date);
}
}]
},

View File

@ -55,14 +55,14 @@ DateTimePicker is derived from DatePicker and TimePicker. For a more detailed ex
onClick(picker) {
const date = new Date();
date.setTime(date.getTime() - 3600 * 1000 * 24);
picker.$emit('pick', date);
picker.emit('pick', date);
}
}, {
text: 'A week ago',
onClick(picker) {
const date = new Date();
date.setTime(date.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', date);
picker.emit('pick', date);
}
}]
},

View File

@ -49,14 +49,14 @@ Date Picker básico por "día".
onClick(picker) {
const date = new Date();
date.setTime(date.getTime() - 3600 * 1000 * 24);
picker.$emit('pick', date);
picker.emit('pick', date);
}
}, {
text: 'A week ago',
onClick(picker) {
const date = new Date();
date.setTime(date.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', date);
picker.emit('pick', date);
}
}]
},

View File

@ -56,14 +56,14 @@ DateTimePicker se deriva de DatePicker y TimePicker. Por una explicación más d
onClick(picker) {
const date = new Date();
date.setTime(date.getTime() - 3600 * 1000 * 24);
picker.$emit('pick', date);
picker.emit('pick', date);
}
}, {
text: 'A week ago',
onClick(picker) {
const date = new Date();
date.setTime(date.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', date);
picker.emit('pick', date);
}
}]
},

View File

@ -47,14 +47,14 @@ L'unité de base du DatePicker est le jour.
onClick(picker) {
const date = new Date();
date.setTime(date.getTime() - 3600 * 1000 * 24);
picker.$emit('pick', date);
picker.emit('pick', date);
}
}, {
text: 'Il y a une semaine',
onClick(picker) {
const date = new Date();
date.setTime(date.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', date);
picker.emit('pick', date);
}
}]
},

View File

@ -55,14 +55,14 @@ DateTimePicker est dérivé de DatePicker et TimePicker. Pour plus d'information
onClick(picker) {
const date = new Date();
date.setTime(date.getTime() - 3600 * 1000 * 24);
picker.$emit('pick', date);
picker.emit('pick', date);
}
}, {
text: 'Il y a une semaine',
onClick(picker) {
const date = new Date();
date.setTime(date.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', date);
picker.emit('pick', date);
}
}]
},

View File

@ -48,14 +48,14 @@
onClick(picker) {
const date = new Date();
date.setTime(date.getTime() - 3600 * 1000 * 24);
picker.$emit('pick', date);
picker.emit('pick', date);
}
}, {
text: '一周前',
onClick(picker) {
const date = new Date();
date.setTime(date.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', date);
picker.emit('pick', date);
}
}]
},

View File

@ -55,14 +55,14 @@ DateTimePicker 由 DatePicker 和 TimePicker 派生,`Picker Options` 或者其
onClick(picker) {
const date = new Date();
date.setTime(date.getTime() - 3600 * 1000 * 24);
picker.$emit('pick', date);
picker.emit('pick', date);
}
}, {
text: '一周前',
onClick(picker) {
const date = new Date();
date.setTime(date.getTime() - 3600 * 1000 * 24 * 7);
picker.$emit('pick', date);
picker.emit('pick', date);
}
}]
},