refactor(components): refactor calendar (#4325)

Co-authored-by: Kevin <sxzz@sxzz.moe>
This commit is contained in:
Alan Wang 2021-12-10 02:41:46 +08:00 committed by GitHub
parent 42430106f3
commit 8600be1cd1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 159 additions and 135 deletions

View File

@ -1,6 +1,6 @@
import { nextTick } from 'vue'
import { mount } from '@vue/test-utils'
import Calendar from '../src/index.vue'
import Calendar from '../src/calendar.vue'
const _mount = (template: string, data?, otherObj?) =>
mount({

View File

@ -1,12 +1,7 @@
import Calendar from './src/index.vue'
import type { App } from 'vue'
import type { SFCWithInstall } from '@element-plus/utils/types'
import { withInstall } from '@element-plus/utils/with-install'
import Calendar from './src/calendar.vue'
Calendar.install = (app: App): void => {
app.component(Calendar.name, Calendar)
}
export const ElCalendar = withInstall(Calendar)
export default ElCalendar
const _Calendar = Calendar as SFCWithInstall<typeof Calendar>
export default _Calendar
export const ElCalendar = _Calendar
export * from './src/calendar'

View File

@ -0,0 +1,23 @@
import { buildProps, definePropType } from '@element-plus/utils/props'
import { UPDATE_MODEL_EVENT } from '@element-plus/utils/constants'
import type { ExtractPropTypes } from 'vue'
export const calendarProps = buildProps({
modelValue: {
type: Date,
},
range: {
type: definePropType<[Date, Date]>(Array),
validator: (range: unknown): range is [Date, Date] =>
Array.isArray(range) &&
range.length === 2 &&
range.every((item) => item instanceof Date),
},
} as const)
export type CalendarProps = ExtractPropTypes<typeof calendarProps>
export const calendarEmits = {
[UPDATE_MODEL_EVENT]: (value: Date) => value instanceof Date,
input: (value: Date) => value instanceof Date,
}
export type CalendarEmits = typeof calendarEmits

View File

@ -49,13 +49,13 @@
<script lang="ts">
import { ref, computed, defineComponent } from 'vue'
import dayjs from 'dayjs'
import ElButton from '@element-plus/components/button'
import { ElButton, ElButtonGroup } from '@element-plus/components/button'
import { useLocale } from '@element-plus/hooks'
import { debugWarn } from '@element-plus/utils/error'
import DateTable from './date-table.vue'
import type { PropType, ComputedRef } from 'vue'
import { calendarProps, calendarEmits } from './calendar'
import type { ComputedRef } from 'vue'
import type { Dayjs } from 'dayjs'
type DateType =
@ -65,7 +65,6 @@ type DateType =
| 'next-year'
| 'today'
const { ButtonGroup: ElButtonGroup } = ElButton
export default defineComponent({
name: 'ElCalendar',
@ -75,27 +74,12 @@ export default defineComponent({
ElButtonGroup,
},
props: {
modelValue: {
type: Date,
},
range: {
type: Array as PropType<Array<Date>>,
validator: (range: Date): boolean => {
if (Array.isArray(range)) {
return (
range.length === 2 && range.every((item) => item instanceof Date)
)
}
return false
},
},
},
props: calendarProps,
emits: calendarEmits,
emits: ['input', 'update:modelValue'],
setup(props, ctx) {
setup(props, { emit }) {
const { t, lang } = useLocale()
const selectedDay = ref(null)
const selectedDay = ref<Dayjs>()
const now = dayjs().locale(lang.value)
const prevMonthDayjs = computed(() => {
@ -122,17 +106,18 @@ export default defineComponent({
return `${date.value.year()} ${t('el.datepicker.year')} ${t(pickedMonth)}`
})
const realSelectedDay = computed({
const realSelectedDay = computed<Dayjs | undefined>({
get() {
if (!props.modelValue) return selectedDay.value
return date.value
},
set(val: Dayjs) {
set(val) {
if (!val) return
selectedDay.value = val
const result = val.toDate()
ctx.emit('input', result)
ctx.emit('update:modelValue', result)
emit('input', result)
emit('update:modelValue', result)
},
})
@ -152,9 +137,9 @@ export default defineComponent({
// 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.Dayjs,
endDayjs: dayjs.Dayjs
) => {
startDayjs: Dayjs,
endDayjs: Dayjs
): [Dayjs, Dayjs][] => {
const firstDay = startDayjs.startOf('week')
const lastDay = endDayjs.endOf('week')
const firstMonth = firstDay.get('month')

View File

@ -0,0 +1,26 @@
import { buildProps, definePropType } from '@element-plus/utils/props'
import { isObject } from '@element-plus/utils/util'
import type { ExtractPropTypes } from 'vue'
import type { Dayjs } from 'dayjs'
export const dateTableProps = buildProps({
selectedDay: {
type: definePropType<Dayjs>(Object),
},
range: {
type: definePropType<[Dayjs, Dayjs]>(Array),
},
date: {
type: definePropType<Dayjs>(Object),
required: true,
},
hideHeader: {
type: Boolean,
},
} as const)
export type DateTableProps = ExtractPropTypes<typeof dateTableProps>
export const dateTableEmits = {
pick: (value: Dayjs) => isObject(value),
}
export type DateTableEmits = typeof dateTableEmits

View File

@ -10,6 +10,7 @@
<thead v-if="!hideHeader">
<th v-for="day in weekDays" :key="day">{{ day }}</th>
</thead>
<tbody>
<tr
v-for="(row, index) in rows"
@ -23,7 +24,7 @@
v-for="(cell, key) in row"
:key="key"
:class="getCellClass(cell)"
@click="pickDay(cell)"
@click="handlePickDay(cell)"
>
<div class="el-calendar-day">
<slot name="dateCell" :data="getSlotData(cell)">
@ -42,13 +43,21 @@ import dayjs from 'dayjs'
import localeData from 'dayjs/plugin/localeData'
import { useLocale } from '@element-plus/hooks'
import { rangeArr } from '@element-plus/components/time-picker'
import { dateTableProps, dateTableEmits } from './date-table'
import type { Dayjs } from 'dayjs'
import type { PropType } from 'vue'
dayjs.extend(localeData)
export const getPrevMonthLastDays = (date: Dayjs, amount) => {
type CellType = 'next' | 'prev' | 'current'
interface Cell {
text: number
type: CellType
}
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(amount).map((_, index) => lastDay - (amount - index - 1))
return rangeArr(count).map((_, index) => lastDay - (count - index - 1))
}
export const getMonthDays = (date: Dayjs) => {
@ -56,117 +65,65 @@ export const getMonthDays = (date: Dayjs) => {
return rangeArr(days).map((_, index) => index + 1)
}
const toNestedArr = (days: Cell[]) =>
rangeArr(days.length / 7).map((index) => {
const start = index * 7
return days.slice(start, start + 7)
})
export default defineComponent({
props: {
selectedDay: {
type: Object as PropType<Dayjs>,
},
range: {
type: Array as PropType<Array<Dayjs>>,
},
date: {
type: Object as PropType<Dayjs>,
},
hideHeader: {
type: Boolean,
},
},
emits: ['pick'],
setup(props, ctx) {
props: dateTableProps,
emits: dateTableEmits,
setup(props, { emit }) {
const { t, lang } = useLocale()
const WEEK_DAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
const now = dayjs().locale(lang.value)
// todo better way to get Day.js locale object
const firstDayOfWeek = (now as any).$locale().weekStart || 0
const toNestedArr = (days) => {
return rangeArr(days.length / 7).map((_, index) => {
const start = index * 7
return days.slice(start, start + 7)
})
}
const firstDayOfWeek: number = (now as any).$locale().weekStart || 0
const getFormattedDate = (day, type): Dayjs => {
let result
if (type === 'prev') {
result = props.date.startOf('month').subtract(1, 'month').date(day)
} else if (type === 'next') {
result = props.date.startOf('month').add(1, 'month').date(day)
} else {
result = props.date.date(day)
}
return result
}
const getCellClass = ({ text, type }) => {
const classes = [type]
if (type === 'current') {
const date_ = getFormattedDate(text, type)
if (date_.isSame(props.selectedDay, 'day')) {
classes.push('is-selected')
}
if (date_.isSame(now, 'day')) {
classes.push('is-today')
}
}
return classes
}
const pickDay = ({ text, type }) => {
const date = getFormattedDate(text, type)
ctx.emit('pick', date)
}
const getSlotData = ({ text, type }) => {
const day = getFormattedDate(text, type)
return {
isSelected: day.isSame(props.selectedDay),
type: `${type}-month`,
day: day.format('YYYY-MM-DD'),
date: day.toDate(),
}
}
const isInRange = computed(() => {
return props.range && props.range.length
})
const isInRange = computed(() => !!props.range && !!props.range.length)
const rows = computed(() => {
let days = []
let days: Cell[] = []
if (isInRange.value) {
const [start, end] = props.range
const currentMonthRange = rangeArr(end.date() - start.date() + 1).map(
(_, index) => ({
text: start.date() + index,
type: 'current',
})
)
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 = rangeArr(remaining).map((_, index) => ({
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 = getPrevMonthLastDays(
const prevMonthDays: Cell[] = getPrevMonthLastDays(
props.date,
firstDay - firstDayOfWeek
).map((day) => ({
text: day,
type: 'prev',
}))
const currentMonthDays = getMonthDays(props.date).map((day) => ({
text: day,
type: 'current',
}))
const currentMonthDays: Cell[] = getMonthDays(props.date).map(
(day) => ({
text: day,
type: 'current',
})
)
days = [...prevMonthDays, ...currentMonthDays]
const nextMonthDays = rangeArr(42 - days.length).map((_, index) => ({
text: index + 1,
type: 'next',
}))
const nextMonthDays: Cell[] = rangeArr(42 - days.length).map(
(_, index) => ({
text: index + 1,
type: 'next',
})
)
days = days.concat(nextMonthDays)
}
return toNestedArr(days)
@ -183,12 +140,52 @@ export default defineComponent({
}
})
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('is-selected')
}
if (date.isSame(now, 'day')) {
classes.push('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,
pickDay,
handlePickDay,
getSlotData,
}
},

View File

@ -1,6 +1,4 @@
export const rangeArr = (n) => {
return Array.from(Array(n).keys())
}
export const rangeArr = (n: number) => Array.from(Array(n).keys())
export const extractDateFormat = (format: string) => {
return format