Date-picker 日曆選擇器實現

用於選擇日期javascript

Date-picer 組件的難點在於:
  • 獲取年月日,相關日期的邏輯以及 new Date 的 Api 使用;
  • 結合Functional Component,減小切換日期時 render 的代價;
  • 組件功能解耦。

1. 實例

最終效果

代碼html

<!-- 基礎用法 -->
<fat-date-picker v-model="date" />
<!-- 語言爲EN -->
<fat-date-picker lang="EN" v-model="secDate" />
複製代碼

實例地址:DatePicker 實例java

代碼地址:Github UI-Librarygit

2. 原理

基本結構以下github

最終效果
<template>
  <div class="date-picker-wrapper" ref="date-picker">
    <fat-input type="text" readonly :class="['picker-data', 'not-select', {'disabled': disabled}]" :value="selectValue | dateFormat('day', lang)" :placeholder="placeholder" @click="toggle" />
    <transition name="fade">
      <div class="picker-panel" v-show="UI.isOpen">
        <!-- 顯示日期 -->
      </div>
    </transition>
  </div>
</template>

<script> export default { props: { value: { type: [Date, String, Number] }, ... }, filters: { dateFormat(val, mode, lang) { // 用於 format 對應日期 } }, model: { prop: "value", event: "input" }, data() { return { date: { year: null, month: null, day: null }, UI: { isOpen: false }, selectValue: null, panelType: "day" }; }, computed: { ... }, watch: { ... value: { handler(newValue) { this.date = dateToObj(newValue ? new Date(newValue) : new Date()); this.selectValue = newValue ? new Date(newValue) : ""; }, immediate: true } }, methods: { ... toggle() { this.UI.isOpen = !this.UI.isOpen; if (this.UI.isOpen) { const datePicker = this.$refs["date-picker"]; const handler = event => { let dom = event.target; let flag = false; while (dom) { if (dom === datePicker) { flag = true; break; } dom = dom.parentNode; } if (!flag) this.UI.isOpen = flag; document.removeEventListener("click", handler, true); }; document.addEventListener("click", handler, true); } } } }; </script>
複製代碼

首先處理 Date-picker 的數據雙向綁定以及下拉框的展開與收縮json

  • 數據綁定,與以前 Select 組件一直,須要定義 v-model 的相關 prop 以及 event,經過watch prop的變化,具體邏輯以下
    value: {
      handler(newValue) {
          // 從 new Date() 中分離出當前的年月日,方便生成對應的年Table、月Table、日Table
          this.date = dateToObj(newValue ? new Date(newValue) : new Date());
          this.selectValue = newValue ? new Date(newValue) : "";
      },
      immediate: true
    }
    export const dateToObj = function (date) {
      return {
          year: date.getFullYear(),
          month: date.getMonth(),
          day: date.getDate()
      }
    }
    複製代碼
  • 下拉框的展開和收縮,與以前 Select 組件不一樣的是,因爲 Date-picker 的下拉框存在着多種狀態,並且後續提供輸入功能,因此 tabIndexdiv 添加 blur 事件的方案實現起來較爲複雜,因此採用比較常規的作法
    toggle() {
        this.UI.isOpen = !this.UI.isOpen;
        if (this.UI.isOpen) {
            const datePicker = this.$refs["date-picker"];
            const handler = event => {
                let dom = event.target;
                let flag = false;
    
            while (dom) {
                if (dom === datePicker) {
                    flag = true;
                    break;
                }
                dom = dom.parentNode;
            }
            if (!flag) this.UI.isOpen = flag;
            document.removeEventListener("click", handler, true);
        };
        document.addEventListener("click", handler, true);
      }
    }
    複製代碼
    當下拉框展開的時候,監聽 documentclick 事件,同時定義事件傳播模式爲 use capture,此時遍歷 Dom,判斷是否在 event.target 是否爲 Date-picker組件。

在處理數據的時候獲取到了當前的年、月、日,也就是 data 中的 date 對象bash

date: {
    year: null,
    month: null,
    day: null
}
複製代碼

利用該對象來生成相應的下拉框的數據:app

  • 年:date.year 來生成年份的數據,也就是當前年份--到--當前年份+12;dom

    yearList() {
        const {
            date: { year }
        } = this;
        return Array.from({ length: 12 }, (v = year, i) => ({
            type: "year",
            value: v + i
        })
      );
    }
    複製代碼
  • 月:區分中英文,當前路徑下維護了一份 CONST.json 用於防止靜態的中英文月份;函數

    monthList() {
        const { lang } = this;
        return CONST[lang].month;
    }
    複製代碼
  • 日:這一部分比較複雜,首先實現當前月份的總天數,以後依據本月一天的星期數以及下個月第一天的星期數來填充表格,如圖

    最終效果
    dayList() {
      const {
        date: { year, month },
        selectValue
      } = this;
      // 後去當前月份的天數
      let curMonthDays = new Date(year, month + 1, 0).getDate();
      // 第一天的星期數
      let firstDay = new Date(year, month, 1).getDay();
      // 下個月第一天的星期數
      let preMonthDays = new Date(year, month, 0).getDate();
      let days = Array.from(
        {
          length: curMonthDays
        },
        (val, index) => {
          let value = index + 1;
          let date = {
            year,
            month,
            day: value
          };
          // 選中日期高亮
          let type = isEqualDay(date, new Date(selectValue))
            ? "cur-month is-selected"
            : "cur-month";
          return {
            type,
            value
          };
        }
      );
      // 標識上一月以及下一個月,對應作樣式處理
      for (let index = 0; index < firstDay; index++) {
        days = [
          {
            type: "pre-month",
            value: preMonthDays--
          }
        ].concat(days);
      }
      for (let index = days.length, item = 1; index < 42; index++, item++) {
        days.push({
          type: "next-month",
          value: item
        });
      }
      return CONST[lang].day.concat(days);
    }
    複製代碼

下拉框主要分爲兩部分:操做欄、日期選擇框

最終效果
  • 操做欄

    <div class="picker-panel" v-show="UI.isOpen">
        <div class="panel-header">
            <div class="left-part">
                <fat-icon class="panel-header-btn" name="chevron_left" :size="20" @click.stop="handleClick('decYear')" />
                <fat-icon class="panel-header-btn" name="chevron_left" :size="20" @click.stop="handleClick('decMonth')" />
            </div>
            ...
            <div>
                <fat-icon class="panel-header-btn" name="chevron_right" :size="20" @click.stop="handleClick('addMonth')" />
                <fat-icon class="panel-header-btn" name="chevron_right" :size="20" @click.stop="handleClick('addYear')" />
            </div>
        </div>
     </div>
    複製代碼

    四個 icon 主要負責加減月份以及年份,因爲四個都屬於點擊事件,而且只修改了 data,利用適配器模式來處理

    handleClick(type) {
        const handlers = {
            addYear: () => ++this.date.year,
            decYear: () => --this.date.year,
            addMonth: () => ++this.date.month,
            decMonth: () => --this.date.month,
            year: () => (this.panelType = "year"),
            month: () => (this.panelType = "month")
        };
        handlers[type]();
    }
    複製代碼

    同時 watch 狀態 date ,完成相關年月的進位

    date: {
        handler(newValue) {
            let { month } = newValue;
            if (month > 11) {
                ++this.date.year;
                this.date.month = 0;
            } else if (month < 0) {
                --this.date.year;
                this.date.month = 11;
            } else {
                this.date.month = newValue.month;
            }
        },
        deep: true
    },
    複製代碼
  • 日期選擇框:這部分要實時變更,爲了省去模板解析的耗費,採用 Functional Component 來實現,也就是說這一部分是函數式組件。

    <date-panel class="panel-content" :type="panelType" :data="list" @select="panelClick" />
    複製代碼

    props 包含上述年、月、日的數據,一樣也採用適配器模式,依據 panelType 來區分展現的是那一部分數據

    import GeneratorRows from './basic'
    
    export default Vue.component('panel', {
        functional: true,
        render: function (_h, context) {
            // 獲取panel組件的props,包含數據data以及類型type
            const {
                data: list,
                type
            } = context.props
            let result = null
            // 若是展現的日,一行的數量爲7個,若是是年月則展現3個。
            let num = type === 'day' ? 7 : 3
            // 此處利用事件委託
            const clickHandler = (e) => {
                if (e.target.attributes.index) {
                    let value = e.target.attributes.index.value
                    let params = {
                        type,
                        value
                    }
    
                    type === 'day' && Object.assign(params, {
                        dateType: e.target.attributes.dateType.value
                    })
                    context.listeners.select(params)
                }
                e.stopPropagation()
            }
            // GeneratorRows爲自定義函數,用來生成對應的行
            result = _h('table', {
                attrs: {
                    class: context.data.staticClass,
                    cellspacing: 0,
                    cellpadding: 0
                },
                on: {
                    click: clickHandler
                }
            }, GeneratorRows(_h, type, list, num))
            return result
        }
    })
    複製代碼

    總體結構很是簡單,首先獲取該組件的 props ,從中獲得數據和類型,

    而後利用 GeneratorRows 函數去生成對應的 table,因爲 table 內項比較多,因此利用事件委託技術,監聽 tableclick 事件,

    若是觸發的話,獲取 e.target 對應的屬性值 e.target.attributes.index.value,結合以前的類型,構建參數 params,再觸發自定義事件 context.listeners.select(params)

    export default function GeneratorRows(_h, type, list, itemNum) {
        let rows = []
        let row = []
    
        list.forEach((elem, index) => {
            let dom = index < itemNum ? 'th' : 'td'
            let className = index < itemNum && type === 'day' ? 'head-item' : `data-item ${elem.type}`
            let label = elem.label || elem.value
    
            row.push(
                _h(
                    dom, {
                        attrs: {
                            class: className,
                            // 用於事件委託
                            dateType: elem.type,
                            index: elem.value,
                        }
                    },
                    label
                )
            )
            if (row.length % itemNum === 0 && row.length) {
                // 換行
                rows.push(
                    _h(
                        'tr', {
                            attrs: {
                                class: "panel-content-row"
                            }
                        },
                        row
                    )
                )
                row = []
            }
        })
        return rows
    }
    複製代碼

    GeneratorRows 函數就是遍歷上述 list ,而後依據規則生成對應 table

3. 總結

這個組件原始的邏輯比較複雜,經過組件化的拆分以及數據的整合,使得總體的邏輯比較明瞭,也是我寫這套組件庫的緣由。

原創聲明: 該文章爲原創文章,轉載請註明出處。

相關文章
相關標籤/搜索