mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-11-30 02:08:12 +08:00
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:
parent
6a1880a8d2
commit
b01a6f4e81
@ -6,3 +6,5 @@ export default (app: App): void => {
|
||||
app.component(Button.name, Button)
|
||||
app.component(ButtonGroup.name, ButtonGroup)
|
||||
}
|
||||
|
||||
export { Button }
|
||||
|
35
packages/directives/__tests__/repeat-click.spec.ts
Normal file
35
packages/directives/__tests__/repeat-click.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
@ -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;
|
||||
|
||||
|
@ -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'
|
||||
|
24
packages/directives/repeat-click/index.ts
Normal file
24
packages/directives/repeat-click/index.ts
Normal 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)
|
||||
})
|
||||
},
|
||||
}
|
@ -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)
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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', () => {
|
||||
|
@ -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
|
||||
|
343
packages/time-picker/__tests__/time-picker.spec.ts
Normal file
343
packages/time-picker/__tests__/time-picker.spec.ts
Normal 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 ])
|
||||
})
|
||||
})
|
||||
|
5
packages/time-picker/index.ts
Normal file
5
packages/time-picker/index.ts
Normal 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)
|
||||
}
|
12
packages/time-picker/package.json
Normal file
12
packages/time-picker/package.json
Normal 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"
|
||||
}
|
||||
}
|
12
packages/time-picker/src/common/constant.ts
Normal file
12
packages/time-picker/src/common/constant.ts
Normal 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',
|
||||
}
|
577
packages/time-picker/src/common/picker.vue
Normal file
577
packages/time-picker/src/common/picker.vue
Normal 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>
|
362
packages/time-picker/src/time-picker-com/basic-time-spinner.vue
Normal file
362
packages/time-picker/src/time-picker-com/basic-time-spinner.vue
Normal 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>
|
224
packages/time-picker/src/time-picker-com/panel-time-pick.vue
Normal file
224
packages/time-picker/src/time-picker-com/panel-time-pick.vue
Normal 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>
|
340
packages/time-picker/src/time-picker-com/panel-time-range.vue
Normal file
340
packages/time-picker/src/time-picker-com/panel-time-range.vue
Normal 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>
|
63
packages/time-picker/src/time-picker-com/useTimePicker.ts
Normal file
63
packages/time-picker/src/time-picker-com/useTimePicker.ts
Normal 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,
|
||||
}
|
||||
}
|
29
packages/time-picker/src/time-picker.ts
Normal file
29
packages/time-picker/src/time-picker.ts
Normal 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),
|
||||
})
|
||||
},
|
||||
}
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
}]
|
||||
},
|
||||
|
@ -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);
|
||||
}
|
||||
}]
|
||||
},
|
||||
|
@ -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);
|
||||
}
|
||||
}]
|
||||
},
|
||||
|
@ -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);
|
||||
}
|
||||
}]
|
||||
},
|
||||
|
@ -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);
|
||||
}
|
||||
}]
|
||||
},
|
||||
|
@ -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);
|
||||
}
|
||||
}]
|
||||
},
|
||||
|
@ -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);
|
||||
}
|
||||
}]
|
||||
},
|
||||
|
@ -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);
|
||||
}
|
||||
}]
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user