refactor(components): [calendar] refactor (#6682)

* refactor(components): [calendar] refactor

* fix: extract constant & rename type
This commit is contained in:
bqy_fe
2022-04-01 15:28:41 +08:00
committed by GitHub
parent 5021214d77
commit 209f505a27
9 changed files with 520 additions and 526 deletions

View File

@@ -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/",

View File

@@ -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')
})
})

View 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')
})
})

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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]

View File

@@ -1,4 +1,4 @@
export * from './aria'
export * from './date-pick'
export * from './date'
export * from './event'
export * from './size'