mirror of
https://github.com/element-plus/element-plus.git
synced 2026-03-13 07:51:17 +08:00
refactor(components): [calendar] refactor (#6682)
* refactor(components): [calendar] refactor * fix: extract constant & rename type
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
"packages/components/breadcrumb-item/",
|
||||
"packages/components/button/",
|
||||
"packages/components/button-group/",
|
||||
"packages/components/calendar/",
|
||||
"packages/components/card/",
|
||||
"packages/components/carousel/",
|
||||
"packages/components/check-tag/",
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
import { nextTick } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Calendar from '../src/calendar.vue'
|
||||
|
||||
const _mount = (template: string, data?, otherObj?) =>
|
||||
mount({
|
||||
components: {
|
||||
'el-calendar': Calendar,
|
||||
},
|
||||
template,
|
||||
data,
|
||||
...otherObj,
|
||||
})
|
||||
|
||||
describe('Calendar.vue', () => {
|
||||
it('create', async () => {
|
||||
const wrapper = _mount(
|
||||
`
|
||||
<el-calendar v-model="value"></el-calendar>
|
||||
`,
|
||||
() => ({ value: new Date('2019-04-01') })
|
||||
)
|
||||
const titleEl = wrapper.find('.el-calendar__title')
|
||||
expect(
|
||||
/2019.*April/.test((titleEl.element as HTMLElement).innerHTML)
|
||||
).toBeTruthy()
|
||||
expect(wrapper.element.querySelectorAll('thead th').length).toBe(7)
|
||||
const rows = wrapper.element.querySelectorAll('.el-calendar-table__row')
|
||||
expect(rows.length).toBe(6)
|
||||
;(rows[5].firstElementChild as HTMLElement).click()
|
||||
|
||||
await nextTick()
|
||||
expect(
|
||||
/2019.*May/.test((titleEl.element as HTMLElement).innerHTML)
|
||||
).toBeTruthy()
|
||||
const vm = wrapper.vm as any
|
||||
const date = vm.value
|
||||
expect(date.getFullYear()).toBe(2019)
|
||||
expect(date.getMonth()).toBe(4)
|
||||
expect(
|
||||
(wrapper.find('.is-selected span').element as HTMLElement).innerHTML
|
||||
).toBe('5')
|
||||
})
|
||||
|
||||
it('range', () => {
|
||||
const wrapper = _mount(`
|
||||
<el-calendar :range="[new Date(2019, 2, 4), new Date(2019, 2, 24)]"></el-calendar>
|
||||
`)
|
||||
const titleEl = wrapper.find('.el-calendar__title')
|
||||
expect(
|
||||
/2019.*March/.test((titleEl.element as HTMLElement).innerHTML)
|
||||
).toBeTruthy()
|
||||
const rows = wrapper.element.querySelectorAll('.el-calendar-table__row')
|
||||
expect(rows.length).toBe(4)
|
||||
expect(
|
||||
wrapper.element.querySelector('.el-calendar__button-group')
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
// https://github.com/element-plus/element-plus/issues/3155
|
||||
it('range when the start date will be calculated to last month', () => {
|
||||
const wrapper = _mount(`
|
||||
<el-calendar :range="[new Date(2021, 1, 2), new Date(2021, 1, 28)]"></el-calendar>
|
||||
`)
|
||||
const titleEl = wrapper.find('.el-calendar__title')
|
||||
expect(
|
||||
/2021.*January/.test((titleEl.element as HTMLElement).innerHTML)
|
||||
).toBeTruthy()
|
||||
const rows = wrapper.element.querySelectorAll('.el-calendar-table__row')
|
||||
expect(rows.length).toBe(5)
|
||||
expect(
|
||||
wrapper.element.querySelector('.el-calendar__button-group')
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('range tow monthes', async () => {
|
||||
const wrapper = _mount(`
|
||||
<el-calendar :range="[new Date(2019, 3, 14), new Date(2019, 4, 18)]"></el-calendar>
|
||||
`)
|
||||
const titleEl = wrapper.find('.el-calendar__title')
|
||||
expect(
|
||||
/2019.*April/.test((titleEl.element as HTMLElement).innerHTML)
|
||||
).toBeTruthy()
|
||||
const dateTables = wrapper.element.querySelectorAll(
|
||||
'.el-calendar-table.is-range'
|
||||
)
|
||||
expect(dateTables.length).toBe(2)
|
||||
const rows = wrapper.element.querySelectorAll('.el-calendar-table__row')
|
||||
expect(rows.length).toBe(5)
|
||||
const cell = rows[rows.length - 1].firstElementChild
|
||||
;(cell as HTMLElement).click()
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(
|
||||
/2019.*May/.test((titleEl.element as HTMLElement).innerHTML)
|
||||
).toBeTruthy()
|
||||
expect(cell.classList.contains('is-selected')).toBeTruthy()
|
||||
})
|
||||
|
||||
// https://github.com/element-plus/element-plus/issues/3155
|
||||
it('range tow monthes when the start date will be calculated to last month', async () => {
|
||||
const wrapper = _mount(`
|
||||
<el-calendar :range="[new Date(2021, 1, 2), new Date(2021, 2, 21)]"></el-calendar>
|
||||
`)
|
||||
const titleEl = wrapper.find('.el-calendar__title')
|
||||
expect(
|
||||
/2021.*January/.test((titleEl.element as HTMLElement).innerHTML)
|
||||
).toBeTruthy()
|
||||
const dateTables = wrapper.element.querySelectorAll(
|
||||
'.el-calendar-table.is-range'
|
||||
)
|
||||
expect(dateTables.length).toBe(3)
|
||||
const rows = wrapper.element.querySelectorAll('.el-calendar-table__row')
|
||||
expect(rows.length).toBe(8)
|
||||
const cell = rows[rows.length - 1].firstElementChild
|
||||
;(cell as HTMLElement).click()
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(
|
||||
/2021.*March/.test((titleEl.element as HTMLElement).innerHTML)
|
||||
).toBeTruthy()
|
||||
expect(cell.classList.contains('is-selected')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('firstDayOfWeek', async () => {
|
||||
// default en locale, weekStart 0 Sunday
|
||||
const wrapper = _mount(
|
||||
`
|
||||
<el-calendar v-model="value"></el-calendar>
|
||||
`,
|
||||
() => ({ value: new Date('2019-04-01') })
|
||||
)
|
||||
const head = wrapper.element.querySelector('.el-calendar-table thead')
|
||||
expect((head.firstElementChild as HTMLElement).innerHTML).toBe('Sun')
|
||||
expect((head.lastElementChild as HTMLElement).innerHTML).toBe('Sat')
|
||||
const firstRow = wrapper.element.querySelector('.el-calendar-table__row')
|
||||
expect((firstRow.firstElementChild as HTMLElement).innerHTML).toContain(
|
||||
'31'
|
||||
)
|
||||
expect((firstRow.lastElementChild as HTMLElement).innerHTML).toContain('6')
|
||||
})
|
||||
|
||||
it('firstDayOfWeek in range mode', async () => {
|
||||
const wrapper = _mount(
|
||||
`
|
||||
<el-calendar v-model="value" :first-day-of-week="7" :range="[new Date(2019, 1, 3), new Date(2019, 2, 23)]"></el-calendar>
|
||||
`,
|
||||
() => ({ value: new Date('2019-03-04') })
|
||||
)
|
||||
const head = wrapper.element.querySelector('.el-calendar-table thead')
|
||||
expect((head.firstElementChild as HTMLElement).innerHTML).toBe('Sun')
|
||||
expect((head.lastElementChild as HTMLElement).innerHTML).toBe('Sat')
|
||||
const firstRow = wrapper.element.querySelector('.el-calendar-table__row')
|
||||
expect((firstRow.firstElementChild as HTMLElement).innerHTML).toContain('3')
|
||||
expect((firstRow.lastElementChild as HTMLElement).innerHTML).toContain('9')
|
||||
})
|
||||
|
||||
it('click previous month or next month', async () => {
|
||||
const wrapper = _mount(
|
||||
`
|
||||
<el-calendar v-model="value"></el-calendar>
|
||||
`,
|
||||
() => ({ value: new Date('2019-04-01') })
|
||||
)
|
||||
await nextTick()
|
||||
const btns = wrapper.findAll('.el-button')
|
||||
const prevBtn = btns.at(0)
|
||||
const nextBtn = btns.at(2)
|
||||
await prevBtn.trigger('click')
|
||||
expect(wrapper.find('.is-selected').text()).toBe('1')
|
||||
await nextBtn.trigger('click')
|
||||
expect(wrapper.find('.is-selected').text()).toBe('1')
|
||||
})
|
||||
})
|
||||
160
packages/components/calendar/__tests__/calendar.spec.tsx
Normal file
160
packages/components/calendar/__tests__/calendar.spec.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import { nextTick } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Calendar from '../src/calendar.vue'
|
||||
|
||||
describe('Calendar.vue', () => {
|
||||
it('create', async () => {
|
||||
const wrapper = mount({
|
||||
data: () => ({ value: new Date('2019-04-01') }),
|
||||
render() {
|
||||
return <Calendar v-model={this.value}></Calendar>
|
||||
},
|
||||
})
|
||||
const titleEl = wrapper.find('.el-calendar__title')
|
||||
expect(/2019.*April/.test(titleEl.element?.innerHTML)).toBeTruthy()
|
||||
expect(wrapper.element.querySelectorAll('thead th').length).toBe(7)
|
||||
const rows = wrapper.element.querySelectorAll('.el-calendar-table__row')
|
||||
expect(rows.length).toBe(6)
|
||||
;(rows[5].firstElementChild as HTMLElement).click()
|
||||
|
||||
await nextTick()
|
||||
expect(/2019.*May/.test(titleEl.element.innerHTML)).toBeTruthy()
|
||||
const vm = wrapper.vm
|
||||
const date = vm.value
|
||||
expect(date.getFullYear()).toBe(2019)
|
||||
expect(date.getMonth()).toBe(4)
|
||||
expect(wrapper.find('.is-selected span').element.innerHTML).toBe('5')
|
||||
})
|
||||
|
||||
it('range', () => {
|
||||
const wrapper = mount(() => (
|
||||
<Calendar
|
||||
range={[new Date(2019, 2, 4), new Date(2019, 2, 24)]}
|
||||
></Calendar>
|
||||
))
|
||||
const titleEl = wrapper.find('.el-calendar__title')
|
||||
expect(/2019.*March/.test(titleEl.element.innerHTML)).toBeTruthy()
|
||||
const rows = wrapper.element.querySelectorAll('.el-calendar-table__row')
|
||||
expect(rows.length).toBe(4)
|
||||
expect(
|
||||
wrapper.element.querySelector('.el-calendar__button-group')
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
// https://github.com/element-plus/element-plus/issues/3155
|
||||
it('range when the start date will be calculated to last month', () => {
|
||||
const wrapper = mount(() => (
|
||||
<Calendar
|
||||
range={[new Date(2021, 1, 2), new Date(2021, 1, 28)]}
|
||||
></Calendar>
|
||||
))
|
||||
const titleEl = wrapper.find('.el-calendar__title')
|
||||
expect(/2021.*January/.test(titleEl.element.innerHTML)).toBeTruthy()
|
||||
const rows = wrapper.element.querySelectorAll('.el-calendar-table__row')
|
||||
expect(rows.length).toBe(5)
|
||||
expect(
|
||||
wrapper.element.querySelector('.el-calendar__button-group')
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('range tow monthes', async () => {
|
||||
const wrapper = mount(() => (
|
||||
<Calendar
|
||||
range={[new Date(2019, 3, 14), new Date(2019, 4, 18)]}
|
||||
></Calendar>
|
||||
))
|
||||
const titleEl = wrapper.find('.el-calendar__title')
|
||||
expect(/2019.*April/.test(titleEl.element.innerHTML)).toBeTruthy()
|
||||
const dateTables = wrapper.element.querySelectorAll(
|
||||
'.el-calendar-table.is-range'
|
||||
)
|
||||
expect(dateTables.length).toBe(2)
|
||||
const rows = wrapper.element.querySelectorAll('.el-calendar-table__row')
|
||||
expect(rows.length).toBe(5)
|
||||
const cell = rows[rows.length - 1].firstElementChild as HTMLElement
|
||||
cell.click()
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(/2019.*May/.test(titleEl.element.innerHTML)).toBeTruthy()
|
||||
expect(cell?.classList.contains('is-selected')).toBeTruthy()
|
||||
})
|
||||
|
||||
// https://github.com/element-plus/element-plus/issues/3155
|
||||
it('range tow monthes when the start date will be calculated to last month', async () => {
|
||||
const wrapper = mount(() => (
|
||||
<Calendar
|
||||
range={[new Date(2021, 1, 2), new Date(2021, 2, 21)]}
|
||||
></Calendar>
|
||||
))
|
||||
const titleEl = wrapper.find('.el-calendar__title')
|
||||
expect(/2021.*January/.test(titleEl.element.innerHTML)).toBeTruthy()
|
||||
const dateTables = wrapper.element.querySelectorAll(
|
||||
'.el-calendar-table.is-range'
|
||||
)
|
||||
expect(dateTables.length).toBe(3)
|
||||
const rows = wrapper.element.querySelectorAll('.el-calendar-table__row')
|
||||
expect(rows.length).toBe(8)
|
||||
const cell = rows[rows.length - 1].firstElementChild as HTMLElement
|
||||
cell.click()
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(/2021.*March/.test(titleEl.element.innerHTML)).toBeTruthy()
|
||||
expect(cell?.classList.contains('is-selected')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('firstDayOfWeek', async () => {
|
||||
// default en locale, weekStart 0 Sunday
|
||||
const wrapper = mount({
|
||||
data: () => ({ value: new Date('2019-04-01') }),
|
||||
render() {
|
||||
return <Calendar v-model={this.value}></Calendar>
|
||||
},
|
||||
})
|
||||
const head = wrapper.element.querySelector('.el-calendar-table thead')
|
||||
expect(head?.firstElementChild?.innerHTML).toBe('Sun')
|
||||
expect(head?.lastElementChild?.innerHTML).toBe('Sat')
|
||||
const firstRow = wrapper.element.querySelector('.el-calendar-table__row')
|
||||
expect(firstRow?.firstElementChild?.innerHTML).toContain('31')
|
||||
expect(firstRow?.lastElementChild?.innerHTML).toContain('6')
|
||||
})
|
||||
|
||||
it('firstDayOfWeek in range mode', async () => {
|
||||
const wrapper = mount({
|
||||
data: () => ({ value: new Date('2019-03-04') }),
|
||||
render() {
|
||||
return (
|
||||
<Calendar
|
||||
v-model={this.value}
|
||||
first-day-of-week={7}
|
||||
range={[new Date(2019, 1, 3), new Date(2019, 2, 23)]}
|
||||
></Calendar>
|
||||
)
|
||||
},
|
||||
})
|
||||
const head = wrapper.element.querySelector('.el-calendar-table thead')
|
||||
expect(head?.firstElementChild?.innerHTML).toBe('Sun')
|
||||
expect(head?.lastElementChild?.innerHTML).toBe('Sat')
|
||||
const firstRow = wrapper.element.querySelector('.el-calendar-table__row')
|
||||
expect(firstRow?.firstElementChild?.innerHTML).toContain('3')
|
||||
expect(firstRow?.lastElementChild?.innerHTML).toContain('9')
|
||||
})
|
||||
|
||||
it('click previous month or next month', async () => {
|
||||
const wrapper = mount({
|
||||
data: () => ({ value: new Date('2019-04-01') }),
|
||||
render() {
|
||||
return <Calendar v-model={this.value}></Calendar>
|
||||
},
|
||||
})
|
||||
await nextTick()
|
||||
const btns = wrapper.findAll('.el-button')
|
||||
const prevBtn = btns.at(0)
|
||||
const nextBtn = btns.at(2)
|
||||
await prevBtn?.trigger('click')
|
||||
expect(wrapper.find('.is-selected').text()).toBe('1')
|
||||
await nextBtn?.trigger('click')
|
||||
expect(wrapper.find('.is-selected').text()).toBe('1')
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,14 @@
|
||||
import { buildProps, definePropType } from '@element-plus/utils'
|
||||
import { UPDATE_MODEL_EVENT } from '@element-plus/constants'
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import type Calendar from './calendar.vue'
|
||||
|
||||
export type CalendarDateType =
|
||||
| 'prev-month'
|
||||
| 'next-month'
|
||||
| 'prev-year'
|
||||
| 'next-year'
|
||||
| 'today'
|
||||
|
||||
export const calendarProps = buildProps({
|
||||
modelValue: {
|
||||
@@ -21,3 +29,5 @@ export const calendarEmits = {
|
||||
input: (value: Date) => value instanceof Date,
|
||||
}
|
||||
export type CalendarEmits = typeof calendarEmits
|
||||
|
||||
export type CalendarInstance = InstanceType<typeof Calendar>
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref } from 'vue'
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineExpose, ref } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { ElButton, ElButtonGroup } from '@element-plus/components/button'
|
||||
import { useLocale, useNamespace } from '@element-plus/hooks'
|
||||
@@ -52,218 +52,200 @@ import { debugWarn } from '@element-plus/utils'
|
||||
import DateTable from './date-table.vue'
|
||||
import { calendarEmits, calendarProps } from './calendar'
|
||||
|
||||
import type { CalendarDateType } from './calendar'
|
||||
import type { ComputedRef } from 'vue'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
|
||||
type DateType =
|
||||
| 'prev-month'
|
||||
| 'next-month'
|
||||
| 'prev-year'
|
||||
| 'next-year'
|
||||
| 'today'
|
||||
const COMPONENT_NAME = 'ElCalendar'
|
||||
|
||||
export default defineComponent({
|
||||
defineOptions({
|
||||
name: 'ElCalendar',
|
||||
})
|
||||
|
||||
components: {
|
||||
DateTable,
|
||||
ElButton,
|
||||
ElButtonGroup,
|
||||
const props = defineProps(calendarProps)
|
||||
const emit = defineEmits(calendarEmits)
|
||||
|
||||
const ns = useNamespace('calendar')
|
||||
|
||||
const { t, lang } = useLocale()
|
||||
const selectedDay = ref<Dayjs>()
|
||||
const now = dayjs().locale(lang.value)
|
||||
|
||||
const prevMonthDayjs = computed(() => {
|
||||
return date.value.subtract(1, 'month').date(1)
|
||||
})
|
||||
|
||||
const nextMonthDayjs = computed(() => {
|
||||
return date.value.add(1, 'month').date(1)
|
||||
})
|
||||
|
||||
const prevYearDayjs = computed(() => {
|
||||
return date.value.subtract(1, 'year').date(1)
|
||||
})
|
||||
|
||||
const nextYearDayjs = computed(() => {
|
||||
return date.value.add(1, 'year').date(1)
|
||||
})
|
||||
|
||||
const i18nDate = computed(() => {
|
||||
const pickedMonth = `el.datepicker.month${date.value.format('M')}`
|
||||
return `${date.value.year()} ${t('el.datepicker.year')} ${t(pickedMonth)}`
|
||||
})
|
||||
|
||||
const realSelectedDay = computed<Dayjs | undefined>({
|
||||
get() {
|
||||
if (!props.modelValue) return selectedDay.value
|
||||
return date.value
|
||||
},
|
||||
set(val) {
|
||||
if (!val) return
|
||||
selectedDay.value = val
|
||||
const result = val.toDate()
|
||||
|
||||
props: calendarProps,
|
||||
emits: calendarEmits,
|
||||
|
||||
setup(props, { emit }) {
|
||||
const ns = useNamespace('calendar')
|
||||
|
||||
const { t, lang } = useLocale()
|
||||
const selectedDay = ref<Dayjs>()
|
||||
const now = dayjs().locale(lang.value)
|
||||
|
||||
const prevMonthDayjs = computed(() => {
|
||||
return date.value.subtract(1, 'month').date(1)
|
||||
})
|
||||
const curMonthDatePrefix = computed(() => {
|
||||
return dayjs(date.value).locale(lang.value).format('YYYY-MM')
|
||||
})
|
||||
|
||||
const nextMonthDayjs = computed(() => {
|
||||
return date.value.add(1, 'month').date(1)
|
||||
})
|
||||
|
||||
const prevYearDayjs = computed(() => {
|
||||
return date.value.subtract(1, 'year').date(1)
|
||||
})
|
||||
|
||||
const nextYearDayjs = computed(() => {
|
||||
return date.value.add(1, 'year').date(1)
|
||||
})
|
||||
|
||||
const i18nDate = computed(() => {
|
||||
const pickedMonth = `el.datepicker.month${date.value.format('M')}`
|
||||
return `${date.value.year()} ${t('el.datepicker.year')} ${t(pickedMonth)}`
|
||||
})
|
||||
|
||||
const realSelectedDay = computed<Dayjs | undefined>({
|
||||
get() {
|
||||
if (!props.modelValue) return selectedDay.value
|
||||
return date.value
|
||||
},
|
||||
set(val) {
|
||||
if (!val) return
|
||||
selectedDay.value = val
|
||||
const result = val.toDate()
|
||||
|
||||
emit('input', result)
|
||||
emit('update:modelValue', result)
|
||||
},
|
||||
})
|
||||
|
||||
const date: ComputedRef<Dayjs> = computed(() => {
|
||||
if (!props.modelValue) {
|
||||
if (realSelectedDay.value) {
|
||||
return realSelectedDay.value
|
||||
} else if (validatedRange.value.length) {
|
||||
return validatedRange.value[0][0]
|
||||
}
|
||||
return now
|
||||
} else {
|
||||
return dayjs(props.modelValue).locale(lang.value)
|
||||
}
|
||||
})
|
||||
|
||||
// https://github.com/element-plus/element-plus/issues/3155
|
||||
// Calculate the validate date range according to the start and end dates
|
||||
const calculateValidatedDateRange = (
|
||||
startDayjs: Dayjs,
|
||||
endDayjs: Dayjs
|
||||
): [Dayjs, Dayjs][] => {
|
||||
const firstDay = startDayjs.startOf('week')
|
||||
const lastDay = endDayjs.endOf('week')
|
||||
const firstMonth = firstDay.get('month')
|
||||
const lastMonth = lastDay.get('month')
|
||||
|
||||
// Current mouth
|
||||
if (firstMonth === lastMonth) {
|
||||
return [[firstDay, lastDay]]
|
||||
}
|
||||
// Two adjacent months
|
||||
else if (firstMonth + 1 === lastMonth) {
|
||||
const firstMonthLastDay = firstDay.endOf('month')
|
||||
const lastMonthFirstDay = lastDay.startOf('month')
|
||||
|
||||
// Whether the last day of the first month and the first day of the last month is in the same week
|
||||
const isSameWeek = firstMonthLastDay.isSame(lastMonthFirstDay, 'week')
|
||||
const lastMonthStartDay = isSameWeek
|
||||
? lastMonthFirstDay.add(1, 'week')
|
||||
: lastMonthFirstDay
|
||||
|
||||
return [
|
||||
[firstDay, firstMonthLastDay],
|
||||
[lastMonthStartDay.startOf('week'), lastDay],
|
||||
]
|
||||
}
|
||||
// Three consecutive months (compatible: 2021-01-30 to 2021-02-28)
|
||||
else if (firstMonth + 2 === lastMonth) {
|
||||
const firstMonthLastDay = firstDay.endOf('month')
|
||||
const secondMonthFirstDay = firstDay.add(1, 'month').startOf('month')
|
||||
|
||||
// Whether the last day of the first month and the second month is in the same week
|
||||
const secondMonthStartDay = firstMonthLastDay.isSame(
|
||||
secondMonthFirstDay,
|
||||
'week'
|
||||
)
|
||||
? secondMonthFirstDay.add(1, 'week')
|
||||
: secondMonthFirstDay
|
||||
|
||||
const secondMonthLastDay = secondMonthStartDay.endOf('month')
|
||||
const lastMonthFirstDay = lastDay.startOf('month')
|
||||
|
||||
// Whether the last day of the second month and the last day of the last month is in the same week
|
||||
const lastMonthStartDay = secondMonthLastDay.isSame(
|
||||
lastMonthFirstDay,
|
||||
'week'
|
||||
)
|
||||
? lastMonthFirstDay.add(1, 'week')
|
||||
: lastMonthFirstDay
|
||||
|
||||
return [
|
||||
[firstDay, firstMonthLastDay],
|
||||
[secondMonthStartDay.startOf('week'), secondMonthLastDay],
|
||||
[lastMonthStartDay.startOf('week'), lastDay],
|
||||
]
|
||||
}
|
||||
// Other cases
|
||||
else {
|
||||
debugWarn(
|
||||
'ElCalendar',
|
||||
'start time and end time interval must not exceed two months'
|
||||
)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// if range is valid, we get a two-digit array
|
||||
const validatedRange = computed(() => {
|
||||
if (!props.range) return []
|
||||
const rangeArrDayjs = props.range.map((_) => dayjs(_).locale(lang.value))
|
||||
const [startDayjs, endDayjs] = rangeArrDayjs
|
||||
if (startDayjs.isAfter(endDayjs)) {
|
||||
debugWarn('ElCalendar', 'end time should be greater than start time')
|
||||
return []
|
||||
}
|
||||
if (startDayjs.isSame(endDayjs, 'month')) {
|
||||
// same month
|
||||
return calculateValidatedDateRange(startDayjs, endDayjs)
|
||||
} else {
|
||||
// two months
|
||||
if (startDayjs.add(1, 'month').month() !== endDayjs.month()) {
|
||||
debugWarn(
|
||||
'ElCalendar',
|
||||
'start time and end time interval must not exceed two months'
|
||||
)
|
||||
return []
|
||||
}
|
||||
return calculateValidatedDateRange(startDayjs, endDayjs)
|
||||
}
|
||||
})
|
||||
|
||||
const pickDay = (day: Dayjs) => {
|
||||
realSelectedDay.value = day
|
||||
}
|
||||
|
||||
const selectDate = (type: DateType) => {
|
||||
let day: Dayjs
|
||||
if (type === 'prev-month') {
|
||||
day = prevMonthDayjs.value
|
||||
} else if (type === 'next-month') {
|
||||
day = nextMonthDayjs.value
|
||||
} else if (type === 'prev-year') {
|
||||
day = prevYearDayjs.value
|
||||
} else if (type === 'next-year') {
|
||||
day = nextYearDayjs.value
|
||||
} else {
|
||||
day = now
|
||||
}
|
||||
|
||||
if (day.isSame(date.value, 'day')) return
|
||||
pickDay(day)
|
||||
}
|
||||
|
||||
return {
|
||||
selectedDay,
|
||||
curMonthDatePrefix,
|
||||
i18nDate,
|
||||
realSelectedDay,
|
||||
date,
|
||||
validatedRange,
|
||||
pickDay,
|
||||
selectDate,
|
||||
t,
|
||||
|
||||
ns,
|
||||
}
|
||||
emit('input', result)
|
||||
emit('update:modelValue', result)
|
||||
},
|
||||
})
|
||||
|
||||
const date: ComputedRef<Dayjs> = computed(() => {
|
||||
if (!props.modelValue) {
|
||||
if (realSelectedDay.value) {
|
||||
return realSelectedDay.value
|
||||
} else if (validatedRange.value.length) {
|
||||
return validatedRange.value[0][0]
|
||||
}
|
||||
return now
|
||||
} else {
|
||||
return dayjs(props.modelValue).locale(lang.value)
|
||||
}
|
||||
})
|
||||
|
||||
// https://github.com/element-plus/element-plus/issues/3155
|
||||
// Calculate the validate date range according to the start and end dates
|
||||
const calculateValidatedDateRange = (
|
||||
startDayjs: Dayjs,
|
||||
endDayjs: Dayjs
|
||||
): [Dayjs, Dayjs][] => {
|
||||
const firstDay = startDayjs.startOf('week')
|
||||
const lastDay = endDayjs.endOf('week')
|
||||
const firstMonth = firstDay.get('month')
|
||||
const lastMonth = lastDay.get('month')
|
||||
|
||||
// Current mouth
|
||||
if (firstMonth === lastMonth) {
|
||||
return [[firstDay, lastDay]]
|
||||
}
|
||||
// Two adjacent months
|
||||
else if (firstMonth + 1 === lastMonth) {
|
||||
const firstMonthLastDay = firstDay.endOf('month')
|
||||
const lastMonthFirstDay = lastDay.startOf('month')
|
||||
|
||||
// Whether the last day of the first month and the first day of the last month is in the same week
|
||||
const isSameWeek = firstMonthLastDay.isSame(lastMonthFirstDay, 'week')
|
||||
const lastMonthStartDay = isSameWeek
|
||||
? lastMonthFirstDay.add(1, 'week')
|
||||
: lastMonthFirstDay
|
||||
|
||||
return [
|
||||
[firstDay, firstMonthLastDay],
|
||||
[lastMonthStartDay.startOf('week'), lastDay],
|
||||
]
|
||||
}
|
||||
// Three consecutive months (compatible: 2021-01-30 to 2021-02-28)
|
||||
else if (firstMonth + 2 === lastMonth) {
|
||||
const firstMonthLastDay = firstDay.endOf('month')
|
||||
const secondMonthFirstDay = firstDay.add(1, 'month').startOf('month')
|
||||
|
||||
// Whether the last day of the first month and the second month is in the same week
|
||||
const secondMonthStartDay = firstMonthLastDay.isSame(
|
||||
secondMonthFirstDay,
|
||||
'week'
|
||||
)
|
||||
? secondMonthFirstDay.add(1, 'week')
|
||||
: secondMonthFirstDay
|
||||
|
||||
const secondMonthLastDay = secondMonthStartDay.endOf('month')
|
||||
const lastMonthFirstDay = lastDay.startOf('month')
|
||||
|
||||
// Whether the last day of the second month and the last day of the last month is in the same week
|
||||
const lastMonthStartDay = secondMonthLastDay.isSame(
|
||||
lastMonthFirstDay,
|
||||
'week'
|
||||
)
|
||||
? lastMonthFirstDay.add(1, 'week')
|
||||
: lastMonthFirstDay
|
||||
|
||||
return [
|
||||
[firstDay, firstMonthLastDay],
|
||||
[secondMonthStartDay.startOf('week'), secondMonthLastDay],
|
||||
[lastMonthStartDay.startOf('week'), lastDay],
|
||||
]
|
||||
}
|
||||
// Other cases
|
||||
else {
|
||||
debugWarn(
|
||||
COMPONENT_NAME,
|
||||
'start time and end time interval must not exceed two months'
|
||||
)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// if range is valid, we get a two-digit array
|
||||
const validatedRange = computed(() => {
|
||||
if (!props.range) return []
|
||||
const rangeArrDayjs = props.range.map((_) => dayjs(_).locale(lang.value))
|
||||
const [startDayjs, endDayjs] = rangeArrDayjs
|
||||
if (startDayjs.isAfter(endDayjs)) {
|
||||
debugWarn(COMPONENT_NAME, 'end time should be greater than start time')
|
||||
return []
|
||||
}
|
||||
if (startDayjs.isSame(endDayjs, 'month')) {
|
||||
// same month
|
||||
return calculateValidatedDateRange(startDayjs, endDayjs)
|
||||
} else {
|
||||
// two months
|
||||
if (startDayjs.add(1, 'month').month() !== endDayjs.month()) {
|
||||
debugWarn(
|
||||
COMPONENT_NAME,
|
||||
'start time and end time interval must not exceed two months'
|
||||
)
|
||||
return []
|
||||
}
|
||||
return calculateValidatedDateRange(startDayjs, endDayjs)
|
||||
}
|
||||
})
|
||||
|
||||
const pickDay = (day: Dayjs) => {
|
||||
realSelectedDay.value = day
|
||||
}
|
||||
|
||||
const selectDate = (type: CalendarDateType) => {
|
||||
let day: Dayjs
|
||||
if (type === 'prev-month') {
|
||||
day = prevMonthDayjs.value
|
||||
} else if (type === 'next-month') {
|
||||
day = nextMonthDayjs.value
|
||||
} else if (type === 'prev-year') {
|
||||
day = prevYearDayjs.value
|
||||
} else if (type === 'next-year') {
|
||||
day = nextYearDayjs.value
|
||||
} else {
|
||||
day = now
|
||||
}
|
||||
|
||||
if (day.isSame(date.value, 'day')) return
|
||||
pickDay(day)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
/** @description currently selected date */
|
||||
selectedDay: realSelectedDay,
|
||||
/** @description select a specific date */
|
||||
pickDay,
|
||||
/** @description select date */
|
||||
selectDate,
|
||||
/** @description Calculate the validate date range according to the start and end dates */
|
||||
calculateValidatedDateRange,
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
import { buildProps, definePropType, isObject } from '@element-plus/utils'
|
||||
import { rangeArr } from '@element-plus/components/time-picker'
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import type DateTable from './date-table.vue'
|
||||
|
||||
export type CalendarDateCellType = 'next' | 'prev' | 'current'
|
||||
export type CalendarDateCell = {
|
||||
text: number
|
||||
type: CalendarDateCellType
|
||||
}
|
||||
|
||||
export const getPrevMonthLastDays = (date: Dayjs, count: number) => {
|
||||
const lastDay = date.subtract(1, 'month').endOf('month').date()
|
||||
return rangeArr(count).map((_, index) => lastDay - (count - index - 1))
|
||||
}
|
||||
|
||||
export const getMonthDays = (date: Dayjs) => {
|
||||
const days = date.daysInMonth()
|
||||
return rangeArr(days).map((_, index) => index + 1)
|
||||
}
|
||||
|
||||
export const toNestedArr = (days: CalendarDateCell[]) =>
|
||||
rangeArr(days.length / 7).map((index) => {
|
||||
const start = index * 7
|
||||
return days.slice(start, start + 7)
|
||||
})
|
||||
|
||||
export const dateTableProps = buildProps({
|
||||
selectedDay: {
|
||||
@@ -23,3 +47,5 @@ export const dateTableEmits = {
|
||||
pick: (value: Dayjs) => isObject(value),
|
||||
}
|
||||
export type DateTableEmits = typeof dateTableEmits
|
||||
|
||||
export type DateTableInstance = InstanceType<typeof DateTable>
|
||||
|
||||
@@ -34,162 +34,142 @@
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue'
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import localeData from 'dayjs/plugin/localeData.js'
|
||||
import { useLocale, useNamespace } from '@element-plus/hooks'
|
||||
import { rangeArr } from '@element-plus/components/time-picker'
|
||||
import { dateTableEmits, dateTableProps } from './date-table'
|
||||
import { WEEK_DAYS } from '@element-plus/constants'
|
||||
import {
|
||||
dateTableEmits,
|
||||
dateTableProps,
|
||||
getMonthDays,
|
||||
getPrevMonthLastDays,
|
||||
toNestedArr,
|
||||
} from './date-table'
|
||||
import type { CalendarDateCell, CalendarDateCellType } from './date-table'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
|
||||
dayjs.extend(localeData)
|
||||
|
||||
type CellType = 'next' | 'prev' | 'current'
|
||||
interface Cell {
|
||||
text: number
|
||||
type: CellType
|
||||
defineOptions({
|
||||
name: 'DateTable',
|
||||
})
|
||||
|
||||
const props = defineProps(dateTableProps)
|
||||
const emit = defineEmits(dateTableEmits)
|
||||
|
||||
const { t, lang } = useLocale()
|
||||
const nsTable = useNamespace('calendar-table')
|
||||
const nsDay = useNamespace('calendar-day')
|
||||
|
||||
const now = dayjs().locale(lang.value)
|
||||
// todo better way to get Day.js locale object
|
||||
const firstDayOfWeek: number = (now as any).$locale().weekStart || 0
|
||||
|
||||
const isInRange = computed(() => !!props.range && !!props.range.length)
|
||||
|
||||
const rows = computed(() => {
|
||||
let days: CalendarDateCell[] = []
|
||||
if (isInRange.value) {
|
||||
const [start, end] = props.range!
|
||||
const currentMonthRange: CalendarDateCell[] = rangeArr(
|
||||
end.date() - start.date() + 1
|
||||
).map((index) => ({
|
||||
text: start.date() + index,
|
||||
type: 'current',
|
||||
}))
|
||||
|
||||
let remaining = currentMonthRange.length % 7
|
||||
remaining = remaining === 0 ? 0 : 7 - remaining
|
||||
const nextMonthRange: CalendarDateCell[] = rangeArr(remaining).map(
|
||||
(_, index) => ({
|
||||
text: index + 1,
|
||||
type: 'next',
|
||||
})
|
||||
)
|
||||
days = currentMonthRange.concat(nextMonthRange)
|
||||
} else {
|
||||
const firstDay = props.date.startOf('month').day() || 7
|
||||
const prevMonthDays: CalendarDateCell[] = getPrevMonthLastDays(
|
||||
props.date,
|
||||
firstDay - firstDayOfWeek
|
||||
).map((day) => ({
|
||||
text: day,
|
||||
type: 'prev',
|
||||
}))
|
||||
const currentMonthDays: CalendarDateCell[] = getMonthDays(props.date).map(
|
||||
(day) => ({
|
||||
text: day,
|
||||
type: 'current',
|
||||
})
|
||||
)
|
||||
days = [...prevMonthDays, ...currentMonthDays]
|
||||
const nextMonthDays: CalendarDateCell[] = rangeArr(42 - days.length).map(
|
||||
(_, index) => ({
|
||||
text: index + 1,
|
||||
type: 'next',
|
||||
})
|
||||
)
|
||||
days = days.concat(nextMonthDays)
|
||||
}
|
||||
return toNestedArr(days)
|
||||
})
|
||||
|
||||
const weekDays = computed(() => {
|
||||
const start = firstDayOfWeek
|
||||
if (start === 0) {
|
||||
return WEEK_DAYS.map((_) => t(`el.datepicker.weeks.${_}`))
|
||||
} else {
|
||||
return WEEK_DAYS.slice(start)
|
||||
.concat(WEEK_DAYS.slice(0, start))
|
||||
.map((_) => t(`el.datepicker.weeks.${_}`))
|
||||
}
|
||||
})
|
||||
|
||||
const getFormattedDate = (day: number, type: CalendarDateCellType): Dayjs => {
|
||||
switch (type) {
|
||||
case 'prev':
|
||||
return props.date.startOf('month').subtract(1, 'month').date(day)
|
||||
case 'next':
|
||||
return props.date.startOf('month').add(1, 'month').date(day)
|
||||
case 'current':
|
||||
return props.date.date(day)
|
||||
}
|
||||
}
|
||||
|
||||
const WEEK_DAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] as const
|
||||
|
||||
export const getPrevMonthLastDays = (date: Dayjs, count: number) => {
|
||||
const lastDay = date.subtract(1, 'month').endOf('month').date()
|
||||
return rangeArr(count).map((_, index) => lastDay - (count - index - 1))
|
||||
const getCellClass = ({ text, type }: CalendarDateCell) => {
|
||||
const classes: string[] = [type]
|
||||
if (type === 'current') {
|
||||
const date = getFormattedDate(text, type)
|
||||
if (date.isSame(props.selectedDay, 'day')) {
|
||||
classes.push(nsDay.is('selected'))
|
||||
}
|
||||
if (date.isSame(now, 'day')) {
|
||||
classes.push(nsDay.is('today'))
|
||||
}
|
||||
}
|
||||
return classes
|
||||
}
|
||||
|
||||
export const getMonthDays = (date: Dayjs) => {
|
||||
const days = date.daysInMonth()
|
||||
return rangeArr(days).map((_, index) => index + 1)
|
||||
const handlePickDay = ({ text, type }: CalendarDateCell) => {
|
||||
const date = getFormattedDate(text, type)
|
||||
emit('pick', date)
|
||||
}
|
||||
|
||||
const toNestedArr = (days: Cell[]) =>
|
||||
rangeArr(days.length / 7).map((index) => {
|
||||
const start = index * 7
|
||||
return days.slice(start, start + 7)
|
||||
})
|
||||
const getSlotData = ({ text, type }: CalendarDateCell) => {
|
||||
const day = getFormattedDate(text, type)
|
||||
return {
|
||||
isSelected: day.isSame(props.selectedDay),
|
||||
type: `${type}-month`,
|
||||
day: day.format('YYYY-MM-DD'),
|
||||
date: day.toDate(),
|
||||
}
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
props: dateTableProps,
|
||||
emits: dateTableEmits,
|
||||
|
||||
setup(props, { emit }) {
|
||||
const { t, lang } = useLocale()
|
||||
const nsTable = useNamespace('calendar-table')
|
||||
const nsDay = useNamespace('calendar-day')
|
||||
|
||||
const now = dayjs().locale(lang.value)
|
||||
// todo better way to get Day.js locale object
|
||||
const firstDayOfWeek: number = (now as any).$locale().weekStart || 0
|
||||
|
||||
const isInRange = computed(() => !!props.range && !!props.range.length)
|
||||
|
||||
const rows = computed(() => {
|
||||
let days: Cell[] = []
|
||||
if (isInRange.value) {
|
||||
const [start, end] = props.range!
|
||||
const currentMonthRange: Cell[] = rangeArr(
|
||||
end.date() - start.date() + 1
|
||||
).map((index) => ({
|
||||
text: start.date() + index,
|
||||
type: 'current',
|
||||
}))
|
||||
|
||||
let remaining = currentMonthRange.length % 7
|
||||
remaining = remaining === 0 ? 0 : 7 - remaining
|
||||
const nextMonthRange: Cell[] = rangeArr(remaining).map((_, index) => ({
|
||||
text: index + 1,
|
||||
type: 'next',
|
||||
}))
|
||||
days = currentMonthRange.concat(nextMonthRange)
|
||||
} else {
|
||||
const firstDay = props.date.startOf('month').day() || 7
|
||||
const prevMonthDays: Cell[] = getPrevMonthLastDays(
|
||||
props.date,
|
||||
firstDay - firstDayOfWeek
|
||||
).map((day) => ({
|
||||
text: day,
|
||||
type: 'prev',
|
||||
}))
|
||||
const currentMonthDays: Cell[] = getMonthDays(props.date).map(
|
||||
(day) => ({
|
||||
text: day,
|
||||
type: 'current',
|
||||
})
|
||||
)
|
||||
days = [...prevMonthDays, ...currentMonthDays]
|
||||
const nextMonthDays: Cell[] = rangeArr(42 - days.length).map(
|
||||
(_, index) => ({
|
||||
text: index + 1,
|
||||
type: 'next',
|
||||
})
|
||||
)
|
||||
days = days.concat(nextMonthDays)
|
||||
}
|
||||
return toNestedArr(days)
|
||||
})
|
||||
|
||||
const weekDays = computed(() => {
|
||||
const start = firstDayOfWeek
|
||||
if (start === 0) {
|
||||
return WEEK_DAYS.map((_) => t(`el.datepicker.weeks.${_}`))
|
||||
} else {
|
||||
return WEEK_DAYS.slice(start)
|
||||
.concat(WEEK_DAYS.slice(0, start))
|
||||
.map((_) => t(`el.datepicker.weeks.${_}`))
|
||||
}
|
||||
})
|
||||
|
||||
const getFormattedDate = (day: number, type: CellType): Dayjs => {
|
||||
switch (type) {
|
||||
case 'prev':
|
||||
return props.date.startOf('month').subtract(1, 'month').date(day)
|
||||
case 'next':
|
||||
return props.date.startOf('month').add(1, 'month').date(day)
|
||||
case 'current':
|
||||
return props.date.date(day)
|
||||
}
|
||||
}
|
||||
|
||||
const getCellClass = ({ text, type }: Cell) => {
|
||||
const classes: string[] = [type]
|
||||
if (type === 'current') {
|
||||
const date = getFormattedDate(text, type)
|
||||
if (date.isSame(props.selectedDay, 'day')) {
|
||||
classes.push(nsDay.is('selected'))
|
||||
}
|
||||
if (date.isSame(now, 'day')) {
|
||||
classes.push(nsDay.is('today'))
|
||||
}
|
||||
}
|
||||
return classes
|
||||
}
|
||||
|
||||
const handlePickDay = ({ text, type }: Cell) => {
|
||||
const date = getFormattedDate(text, type)
|
||||
emit('pick', date)
|
||||
}
|
||||
|
||||
const getSlotData = ({ text, type }: Cell) => {
|
||||
const day = getFormattedDate(text, type)
|
||||
return {
|
||||
isSelected: day.isSame(props.selectedDay),
|
||||
type: `${type}-month`,
|
||||
day: day.format('YYYY-MM-DD'),
|
||||
date: day.toDate(),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isInRange,
|
||||
weekDays,
|
||||
rows,
|
||||
getCellClass,
|
||||
handlePickDay,
|
||||
getSlotData,
|
||||
|
||||
nsTable,
|
||||
nsDay,
|
||||
}
|
||||
},
|
||||
defineExpose({
|
||||
/** @description toggle date panel */
|
||||
getFormattedDate,
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -9,4 +9,15 @@ export const datePickTypes = [
|
||||
'daterange',
|
||||
'monthrange',
|
||||
] as const
|
||||
|
||||
export const WEEK_DAYS = [
|
||||
'sun',
|
||||
'mon',
|
||||
'tue',
|
||||
'wed',
|
||||
'thu',
|
||||
'fri',
|
||||
'sat',
|
||||
] as const
|
||||
|
||||
export type DatePickType = typeof datePickTypes[number]
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './aria'
|
||||
export * from './date-pick'
|
||||
export * from './date'
|
||||
export * from './event'
|
||||
export * from './size'
|
||||
|
||||
Reference in New Issue
Block a user