preface

Recent company want to have a demand to achieve nailing calendar effect, used nailing calendar should all know the above can switch this week and month calendar calendar calendar click below the corresponding schedule when rolling, then rolling schedule when the calendar will be placed at the top of the agenda is ultimately selected above such a linkage effect, because the schedule list have access to the company’s business code, So this may only be the calendar component of the implementation process, is probably the following way to make a GIF for you to see 🌺

The preparatory work

NPM I-D DayJS package is used to process calendar rendering logic

Arrange a throttling function to be used later

const throttle = (fun: (val: any) => void, time: number) = > {
  let delay = 0
  return (. params: any) = > {
    const now = +new Date(a)if (now - delay > time) {
      fun.apply(this, params)
      delay = now
    }
  }
}
Copy the code

Since we are using TSX, we will paste all the types up in a moment and then use them directly

type weekType = {
  id: number.name: string
}
type toutchType = { x: number.y: number }
​
type dayjsType = Array<dayjs.Dayjs>
​
type generateWeekDataType = {
  currenWeekFirstDay: dayjs.Dayjs,
  generateWeekDateList: Array<dayjsType>
}
​
type generateMonthDataType = {
  currentMonthFirstDay: dayjs.Dayjs,
  generateMonthDateList: Array<dayjsType>
}
Copy the code

GenerateMonthData is used to generate the monthly calendar and generateWeekData is used to generate the weekly calendar

As an aside to the calendar generation logic, dayjsDate is used to get the current month, followed by startOf to get the first day of the first week

Because we need to render all the dates of a month including the dates of the previous month displayed in the current month and the dates of the next month displayed in the current month

GenerateMonthData returns three arrays each time to indicate that the current month has passed from the previous month to the next month and we render only the current month, Each time the calendar is then swiped, it will continue to generate the calendar of the current month and the week of the previous and next month based on the current month

generateMonthData

/ * * * *@param {*} DayjsDate DayJS object */
const generateMonthData = (dayjsDate: dayjs.Dayjs): generateMonthDataType= > {
  Return the first day of the current month
  const currentMonthFirstDay = dayjsDate.startOf('month')
  // Returns the first day of the first week of the month
  const currentMonthStartDay = currentMonthFirstDay.startOf('week')

  // Last month
  const prevMonthFirstDay = currentMonthFirstDay.subtract(1.'month')
  const prevMonthStartDay = prevMonthFirstDay.startOf('week')

  // Next month
  const nextMonthFirstDay = currentMonthFirstDay.add(1.'month')
  const nextMonthStartDay = nextMonthFirstDay.startOf('week')

  return {
    currentMonthFirstDay,
    generateMonthDateList: [
      new Array(42).fill(' ').map((_, index) = > prevMonthStartDay.add(index, 'day')),
      new Array(42).fill(' ').map((_, index) = > currentMonthStartDay.add(index, 'day')),
      new Array(42).fill(' ').map((_, index) = > nextMonthStartDay.add(index, 'day'))],}}Copy the code

generateWeekData

/ * * * *@param {*} DayjsDate DayJS object */
const generateWeekData = (dayjsDate: dayjs.Dayjs): generateWeekDataType= > {
  const currenWeekStartDay = dayjsDate.startOf('week')
  const prevWeekStartDay = currenWeekStartDay.subtract(1.'week')
  const nextWeekStartDay = currenWeekStartDay.add(1.'week')
  return {
    currenWeekFirstDay: currenWeekStartDay,
    generateWeekDateList: [
      new Array(7).fill(' ').map((_, index) = > prevWeekStartDay.add(index, 'day')),
      new Array(7).fill(' ').map((_, index) = > currenWeekStartDay.add(index, 'day')),
      new Array(7).fill(' ').map((_, index) = > nextWeekStartDay.add(index, 'day'))],}}Copy the code

To the chase

Because of the layout of our business, I have divided it into two parts, so far only the calendar component is left

const ScheduleList = () = > {
  return <>
    <div className='schedule_view_wrap'>
      <div className='schedule_list_section'>{/* Calendar component */}<CalendarComp />
      </div>
    </div>
  </>
}
export default ScheduleList
Copy the code

Look at the
concrete logic. The following declarations and logic code are all in this component, and most functions and variables are commented out

CalendarComp components

const CalendarComp = () => {
}
Copy the code

useRef

const { current } = useRef({
    currentDate: dayjs().format('YYYY-MM-DD'), // Get today
    isTouch: false.// Control the sliding animation
    touchStartX: 0.// Determine the slide position
    touchStartY: 0.calendarRef: { offsetWidth: 0 } // Determine the slide position
})
Copy the code

useState

  const dayjsDate = dayjs(current.currentDate) // Convert the date of the day into a dayJS object ready to get the rendered monthly calendar
  const { currentMonthFirstDay, generateMonthDateList = [] } = generateMonthData(dayjsDate)
  const { currenWeekFirstDay, generateWeekDateList = [] } = generateWeekData(dayjsDate)

  const [mounthFirstDay, setMounthFirstDay] = useState<dayjs.Dayjs>(currentMonthFirstDay) // The first day of the month
  const [mountDateList, setMountDateList] = useState<Array<dayjsType>>(generateMonthDateList)// The month calendar needs to display dates including the previous month and the next month
  const [weekFirstDay, setWeekFirstDay] = useState<dayjs.Dayjs>(currenWeekFirstDay)
  const [weekDateList, setWeekDateList] = useState<Array<dayjsType>>(generateWeekDateList)// The week calendar needs to display dates including the previous week, the current week and the next week
  const [selectDate, setSelectDate] = useState<string>(current.currentDate) // Set the style of the selected day
  const [moveIndex, setMoveIndex] = useState<number> (0) // Record the position of the calendar slide
  const [touch, setTouch] = useState<toutchType>({ x: 0.y: 0 }) // The position of the finger slide
  const [isMountView, setIsMountView] = useState<boolean> (false) // True/weekly calendar false/ monthly calendar
  const [weekInd, selectWeekInd] = useState<number> (0) // Record the selected index of the weekly calendar
Copy the code

weekList

const weekList: Array<weekType> = [{
    id: 0.name: 'day'
  }, {
    id: 1.name: '一'
  }, {
    id: 2.name: '二'
  }, {
    id: 3.name: '三'
  }, {
    id: 4.name: 'four'
  }, {
    id: 5.name: 'five'
  }, {
    id: 6.name: '六'
  }]
Copy the code

Core business structure

return <div className='calendar_comp'>{/* display current month title */}<header>
      <span>{handelFormtDate(isMountView ? WeekFirstDay: mounthFirstDay, 'YYYY ')}</span>
    </header>{/* Week list */}<p className='week_list'>
      {
        weekList.map(item => {
          return <span key={item.id}>{item.name}</span>})}</p>{/* Monthly calendar view/weekly calendar view */}<div className={`calendar_comp_wrapThe ${isMountView ? 'pushHei' : 'pullHei` '}}ref={(e: any) = > current.calendarRef = e}
      onTouchStart={handleTouchStart}
      onTouchMove={handleTouchMove}
      onTouchEnd={handleTouchEnd}
    >
      <div className='calendar_comp_section' style={{
        transform: `translateX(${-moveIndex * 100} %) `,}} >
        {
          (isMountView ? weekDateList : mountDateList).map((item, index) => {
            return <ul key={index} className='calendar_list_day'
              style={{
                transform: `translateX(${(index - 1 + moveIndex + (current.isTouch ? touch.x : 0)) * 100`} %),transitionDuration:` ${current.isTouch ? 0 : 0.3}s`}} >
              {
                item.map((date, ind) => {
                  const isOtherMonthDay = isMountView || date.isSame(mounthFirstDay, 'month')
                  const formtDate = handelFormtDate(date, 'YYYY-MM-DD')
​
                  return <li
                    key={ind}
                    onClick={()= > {
                      handelSelectDate(formtDate, isOtherMonthDay, ind)
                    }}
                    style={{ color: isOtherMonthDay ? '#333333' : '#CCCCCC' }}
                  >
                    <span className={renderClassName(formtDate)}>{date.format('DD')}</span>
                  </li>})}</ul>})}</div>
    </div>{/* Calendar bottom operation close/expand button */}<span className='calendar_pull' onClick={()= >{ handelIsMountView() }}>{isMountView ? 'expand' : 'fold '}</span>
    <p className='current_day'>{handelFormtDate(selectDate, 'MM month DD day ')} {' week ${getWeekDate(selectDate)? .name}`}</p>
  </div >
Copy the code

The core business logic is presented in sections (all of which may look tiresome) and other functions are gradually introduced starting with useEffect

The first paragraph starts with useEffect

useEffect(() = > {
    handelSetWeekIndex(current.currentDate, weekDateList) // Set the selected position of the weekly calendar
    setSelectDate(current.currentDate) // Initialize the calendar on the selected day
}, [])

useEffect(() = > {
  // The first day of the next month is directly selected when the calendar switch is triggered
  if(! isMountView) {/ / month calendar
    let currentM = handelFormtDate(mounthFirstDay, 'YYYY-MM-DD')
    setSelectDate(currentM)
  }
}, [mounthFirstDay])

// Format the date function
const handelFormtDate = (date: dayjs.Dayjs | string.exp: string) :string= > {
  return dayjs(date).format(exp)
}

 // Set the selected position of the weekly calendar
const handelSetWeekIndex = (selectDate: string | dayjs.Dayjs, generateWeekDateList: Array<{ [key: string] :any} >) :void= > {
  const currentWeekDateList = generateWeekDateList[1]
  let weekIndex = 0
  // Set the location of the weekly calendar to be selected (for example, if it is currently on Wednesday when switching to weekly calendar, make sure it is still on Wednesday when switching to weekly calendar)
  currentWeekDateList.forEach((v: any, index: number) = > {
    if (handelFormtDate(v, 'YYYY-MM-DD') === selectDate) weekIndex = index
  })
  selectWeekInd(weekIndex)
}

// Weekly title list render logic
const getWeekDate = (cDate: string) :weekType= > {
  const day = new Date(cDate).getDay()
  return weekList[day];
}
Copy the code

The second paragraph starts with handleTouchStart

// Slide calendar to trigger
const handleTouchStart = (e: any) = > {
  e.stopPropagation()
  const touchs = e.touches[0]
  current.touchStartX = touchs.clientX
  current.touchStartY = touchs.clientY
  current.isTouch = true 
}

// Slide calendar trigger
const handleTouchMove = throttle((e: any) = > {
    e.stopPropagation()
    const touchs = e.touches[0]
    const moveX = touchs.clientX - current.touchStartX
    const moveY = touchs.clientY - current.touchStartY
    const calendarWidth = current.calendarRef.offsetWidth
    if (Math.abs(moveX) > Math.abs(moveY)) {
      // Slide left and right
      setTouch({ x: moveX / calendarWidth, y: 0})}},25)

// Slide calendar end trigger
 const handleTouchEnd = (e: any) = > {
    e.stopPropagation()
    current.isTouch = false
    const touchX = Math.abs(touch.x)
    const touchY = Math.abs(touch.y)
    const newTranslateIndex = touch.x > 0 ? moveIndex + 1 : moveIndex - 1

    if (touchX > touchY && touchX > 0.15) {

      if (isMountView) {
        // Weekly calendar goes logic
        const nextWeekFirstDay = weekFirstDay[touch.x > 0 ? 'subtract' : 'add'] (1.'week')
        const { currenWeekFirstDay = null, generateWeekDateList = [] } = generateWeekData(nextWeekFirstDay)
				
        // Update the weekly calendar view
        updateWeekView(newTranslateIndex, currenWeekFirstDay, generateWeekDateList)

        // Weekly calendar scrolling is selected by default
        const currentWeekDays = generateWeekDateList[1]
        const selectWeekDay = currentWeekDays[weekInd]
        const formtSelectWeekDay = handelFormtDate(selectWeekDay, 'YYYY-MM-DD')
        setSelectDate(formtSelectWeekDay)

      } else {
        // Monthly calendar goes logic
        const nextMonthFirstDay = mounthFirstDay[touch.x > 0 ? 'subtract' : 'add'] (1.'month')
        const { currentMonthFirstDay = null, generateMonthDateList = [] } = generateMonthData(nextMonthFirstDay)
      	
        // Update the monthly calendar view
        updateMounthView(newTranslateIndex, currentMonthFirstDay, generateMonthDateList)
      }
    }
    setTouch({ x: 0.y: 0})}// Update the weekly calendar view
 const updateWeekView = (index: number.currentFirstDay: any.dateList: Array<dayjsType>): void= > {
   setMoveIndex(index)
   setWeekFirstDay(currentFirstDay)
   setWeekDateList(dateList)
 }
 
 // Update the monthly calendar view
 const updateMounthView = (index: number.currentFirstDay: any.dateList: Array<dayjsType>): void= > {
   setMoveIndex(index)
   setMounthFirstDay(currentFirstDay)
   setMountDateList(dateList)
 }
Copy the code

The third paragraph starts with the handelSelectDate calendar selection logic

// Calendar select logic
/ * *@isOtherMonthDay Check whether the current day is in the current month. If it is not in the current month, the click will automatically switch to next month or last month **/
const handelSelectDate = (formtDate: string, isOtherMonthDay: boolean, index: number) = > {
  let selectM = handelFormtDate(formtDate, 'YYYYMM')
  let currentM = handelFormtDate(mounthFirstDay, 'YYYYMM')
  if(! isOtherMonthDay) {// Click last month or next month in the current month
    const { generateMonthDateList = [] } = generateMonthData(dayjs(formtDate))
    const newTranslateIndex = selectM < currentM ? moveIndex + 1 : moveIndex - 1
    // Update the monthly view
    updateMounthView(newTranslateIndex, dayjs(formtDate), generateMonthDateList)
  }
	
  // Ensure that the selected position in the calendar is correct when expanding and collapsing
  if (isMountView) {
    // Weekly calendar records the selected position
    selectWeekInd(index)
  }

  // Update the calendar selection style
  setSelectDate(formtDate)
}


// Render the selected styles. This is written as a function to distinguish the styles of the day from other selected styles
const renderClassName = (formtDate: string) :string= > {
  if (selectDate === formtDate) return 'selectDay'
  if (formtDate === current.currentDate) return 'currentDay'
  return ' '
}
Copy the code

Start with handelIsMountView by switching the weekly/monthly calendar

// Monthly calendar/weekly calendar switch
const handelIsMountView = () = > {
  // Generate the corresponding week calendar and month calendar based on the currently selected day
  const dayjsDate = dayjs(selectDate)
  const{ generateWeekDateList = [], generateMonthDateList = [] } = { ... generateMonthData(dayjsDate), ... generateWeekData(dayjsDate) }constflag = ! isMountViewif (flag) {
    // Update the weekly calendar
    setWeekFirstDay(dayjsDate)
    setWeekDateList(generateWeekDateList)
    // When updating the calendar, you need to record the current position
    handelSetWeekIndex(selectDate, generateWeekDateList)
  } else {
    // Update the monthly calendar
    setMounthFirstDay(dayjsDate)
    setMountDateList(generateMonthDateList)
  }
  setIsMountView(flag)
}
Copy the code

This is the main logical and business structure of the calendar component

Also paste the style

.schedule_view_wrap {
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    overflow: hidden;
    .schedule_list_section{
        height: 100%;
        overflow: hidden;
        display: flex;
        flex-direction: column; }}.calendar_comp {
    width: 100%;
    background: #fff;
    header {
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 37px 40px 23px 40px;
        background: #fff;
        span {
            font-size: 36px;
            font-family: PingFangSC-Medium, PingFang SC;
            font-weight: 500;
            color: # 000000;
        }
        img {
            width: 40px;
            height: 40px; }}.week_list {
        display: flex;
        span {
            width: calc(100%/7);
            height: 40px;
            text-align: center;
            line-height: 40px;
            font-size: 24px;
            font-family: PingFangSC-Regular, PingFang SC;
            color: # 999999; }}.calendar_comp_wrap {
        width: 100%;
        position: relative;
        &.pullHei {
            height: 480px;
            transition: 0.25 s;
        }
        &.pushHei {
            height: 80px;
            transition: 0.15 s;
        }
        .calendar_comp_section {
            width: 100%;
            display: flex;
            .calendar_list_day {
                display: flex;
                flex-wrap: wrap;
                padding-top: 8px;
                flex-shrink: 0;
                width: 100%;
                position: absolute;
                li {
                    margin-bottom: 32px;
                    width: calc(100%/7);
                    height: 48px;
                    text-align: center;
                    line-height: 48px;
                    font-size: 28px;
                    font-family: PingFangSC-Medium, PingFang SC;
                    font-weight: 500;
                    color: # 333333;
                    .currentDay {
                        background: rgba(255.142.34.0.1) ! important;
                        color: #FF8E22 ! important;
                        padding: 6px 10px;
                        border-radius: 8px;
                    }
                    .selectDay {
                        border-radius: 8px;
                        background: #FF8E22 ! important;
                        color: #FFFFFF ! important;
                        padding: 6px 10px;
                        &.selectDayActive{
                            padding: 6px 18px; }}.circle {
                        display: block;
                        width: 8px;
                        height: 8px;
                        background: #ccc;
                        border-radius: 50%;
                        margin: 0 auto;
                        margin-top: 10px;
                    }
                }
            }
        }
    }
    .calendar_pull {
        width: 48px;
        height: 8px;
        padding-bottom: 22px;
        padding-top: 10px;
        margin: 0 auto;
        display: block;
    }
    .current_day {
        width: 100%;
        text-align: center;
        padding-bottom: 10px;
        font-size: 28px;
        font-family: PingFangSC-Regular, PingFang SC;
        font-weight: 400;
        color: # 333333; }}Copy the code

The last

As mentioned above, because the schedule list has been connected to the company’s business, it is not convenient to post it. However, if you really need it, you can discuss it in the comment section and attach the git address: github.com/gaoxinxiao/…

Creation is not easy to comment on three generations of beauty, praise rich life 😁