徒手擼UI之DatePicker

馬蹄疾發表於2018-03-22

QingUI是一個UI元件庫
目前擁有的元件:DatePicker, TimePicker, Paginator, Tree, Cascader, Checkbox, Radio, Switch, InputNumber, Input
ES6語法編寫,無依賴
原生模組化,Chrome63以上支援,請開啟靜態伺服器預覽效果,靜態伺服器傳送門
採用CSS變數配置樣式
辛苦造輪子,歡迎來github倉庫star:github.com/veedrin/qin…
四月份找工作,求內推,座標深圳

寫在前面

去年年底專案中嘗試著寫過一個分頁的Angular元件,然後就有了寫QingUI的想法

過程還是非常有意思的

接下來我會用幾篇文章分別介紹每個元件的大概思路,請大家耐心等待

這一篇介紹DatePicker日期選擇器

最重要的,求star,求fork,求內推

github.com/veedrin/qin…

少廢話,先上圖

img failed

實現一個類

ES6的類語法糖非常順手,用ES6寫程式碼就像吃德芙巧克力一樣哈哈

然後DatePicker和TimePicker有一些公共的方法,我用一個公共類,讓它們倆繼承

extends就是繼承關鍵字

super是用來繼承父類的this物件的,它必須首先呼叫,否則無法找到this

主要是為了照顧對ES6不熟悉的人

class Common {
    constructor() {}
}

class DatePicker extends Common {
    constructor() {
        super();
    }
}
複製程式碼

日曆

不知道大家觀察過沒有,每一頁日曆都是按星期來排的,展示當月的所有日期

所以,如果該月的1號不是星期一或者星期天呢?那還要從上個月抓幾天過來,把空補齊,月底也一樣

為了方便,下面都以星期一為一週起始日

當月所有日期容易,獲取當月有多少天,for迴圈加到模板裡

那月首補齊的呢?我要知道這個月1號是星期幾,還要知道上個月最後一天是多少號,然後少幾天,往前推幾天就好了

月底補齊的就簡單一點,我要知道這個月最後一天是星期幾,然後少幾天,從1號往後加幾天

const daysCountThisMonth = this.daysCountThisMonth();
const daysCountLastMonth = this.daysCountThisMonth(-1);
// weekie指的是星期幾(自創)
const weekieFirstDay = this.weekieOfSomedayThisMonth(1);
const weekieLastDay = this.weekieOfSomedayThisMonth(daysCountThisMonth);

if (weekieFirstDay > 1) {
    for (let i = daysCountLastMonth - weekieFirstDay + 2; i <= daysCountLastMonth; i++) {
        tpl += `<span class="day-disable">${i}</span>`;
    }
}
for (let i = 1; i <= daysCountThisMonth; i++) {
    tpl += `<span class="day">${i}</span>`;
}
if (weekieLastDay < 7) {
    for (let i = 1; i <= 7 - weekieLastDay; i++) {
        tpl += `<span class="day-disable">${i}</span>`;
    }
}
複製程式碼

這個月有多少天怎麼算?

月份引數傳入下個月,日期引數傳入0,就可以獲得當月最後一天是多少號了

我這裡this.M就是當月,而程式的月份是要減1的,所以就相當於下個月了

某一天是星期幾好求,我就不列出來了

function daysCountThisMonth(num = 0) {
    return new Date(this.Y, this.M + num, 0).getDate();
}
複製程式碼

至此,加上flex佈局,一個靜態的日曆就做出來了

還有一個小細節,我選中的日期要高亮,當天也要有一個背景色,它們倆在初始的時候還應該是重合的

當天嘛,我快取的年月和實時獲取的年月相等,再把實時獲取的日期和i比對,就是當天,加個today的class

而如果實時獲取的日期和快取的日期還相等,那麼這天不僅是當天,還是使用者選中的日子,加個today active的class

剩下的就是使用者選中的不是當天,和完全普通的日子

由於選擇年月的時候,日曆都要重新渲染,所以我們只能根據這些條件來判斷,看起來確實有些複雜

for (let i = 1; i <= daysCountThisMonth; i++) {
    if (this.Y === Y && this.M === M && i === D && i !== this.D) {
        tpl += `<span class="day today">${i}</span>`;
    } else if (this.Y === Y && this.M === M && i === D && i === this.D) {
        tpl += `<span class="day today active">${i}</span>`;
    } else if (i === this.D) {
        tpl += `<span class="day active">${i}</span>`;
    } else {
        tpl += `<span class="day">${i}</span>`;
    }
}
複製程式碼

我之前以為例項化時把當前年月日快取起來,然後再把使用者選中的年月日快取起來,兩者一對比,就可以渲染了

其實我沒有注意到一個問題,就是使用日曆是有可能跨天的,理論上,如果永遠不關機,跨年都可以

如果我在午夜12點左右操作QingUI,那快取的日期就有可能不準確,因為已經跨天了

至於我是如何發現這個BUG的,你猜?

所以每次渲染都要實時獲取當前年月日

ES6的解構賦值可以讓這個操作非常優美

function nowDate() {
    const date = new Date();
    return [date.getFullYear(), date.getMonth() + 1, date.getDate()];
}

const [Y, M, D] = nowDate();
複製程式碼

話說就因為這個BUG,我幾乎重構了整個元件

最後,使用者選中高亮是通過新增class的方式實現的,高亮新的日子,然後把舊的日子高亮去掉

注意,這種操作是不需要重新渲染的,所以一般做法是,把所有日子迴圈一遍,去掉高亮,然後新增新的高亮

如果我們將上一個高亮的日子快取起來呢?是不是就不用每次for迴圈了,於是就有了this.oldD

三角選擇年月

選擇年月有兩種方式,一種是點開皮膚,直接選擇,一種是點選三角,每次增1或減1

點選三角的時候,我設定成1月再減1,就會變成年份減1,月份變成12月,也就是說月份是可以一直點的

// 置灰時獲取不到元素
if ($monthPrev) {
    $monthPrev.addEventListener('click', function(event) {
        event.stopPropagation();
        self.M--;
        if (self.M > 0) {
            self.yearAndMonthChange('month');
        } else {
            self.M = 12;
            self.Y--;
            self.yearAndMonthChange('both');
        }
    });
}
複製程式碼

可真的是這樣嗎?還記得有一個配置項yearRange嗎?

如果年份到了頭,就置灰不能再點了,這是我遇到的第二個坑

如果年份到了頭,年份的減三角就要置灰,但是月份的減三角還是可以點

直到月份變成1,那麼年份和月份的減三角都置灰了

加三角的邏輯也一樣

這個邏輯,你們理一理

const [left, right] = this.yearRange;

let tpl = `
    <div class="bar">
        <div class="bar-item">
            <span class="${this.Y > left ? 'angle year-prev' : 'angle disabled'}">◀</span>
            <span class="year-pop"></span>
            <span class="${this.Y < right ? 'angle year-next' : 'angle disabled'}">▶</span>
        </div>
        <div class="bar-item">
            <span class="${this.Y === left && this.M === 1 ? 'angle disabled' : 'angle month-prev'}">◀</span>
            <span class="month-pop"></span>
            <span class="${this.Y === right && this.M === 12 ? 'angle disabled' : 'angle month-next'}">▶</span>
        </div>
    </div>
`;
複製程式碼

皮膚選擇年月

選擇年份和月份皮膚,我之前是做成pop彈窗加滾動條的,發現體驗很糟糕,於是參考ElementUI做到了日曆皮膚上

這就帶來一個問題,顯示年份皮膚的時候,日曆實際上是清除了,選擇完以後再重新渲染日曆,繼續重構...

月份簡單,一個皮膚就顯示完了

年份有可能一個皮膚顯示不完,但再怎麼樣,也比年份和月份聯動的情況要簡單是吧

我把使用者選擇的年份快取起來,因為我希望把使用者選中過的年份放在比較中間的位置,他下次再選的時候,可以從這裡繼續

再結合yearRange,效果是這樣的

const [left, right] = this.yearRange;
const start = this.anchor - 4 > left ? this.anchor - 4 : left;
const end = this.anchor + 7 < right ? this.anchor + 7 : right;
const [Y, , ] = this.nowDate();
let tpl = `<div class="title">${this.lang === 'en' ? 'Choose a Year' : '選擇年份'}</div>`;
if (this.anchor - 4 > left) {
    tpl += '<div class="prev">◀</div>';
} else {
    tpl += '<div class="prev-disabled">◀</div>';
}
tpl += '<div class="year-wrap">';
for (let i = start; i <= end; i++) {
    if (i !== Y) {
        tpl += `<span class="year">${i}</span>`;
    } else {
        tpl += `<span class="year thisyear">${i}</span>`;
    }
}
tpl += '</div>';
if (this.anchor + 7 < right) {
    tpl += '<div class="next">▶</div>';
} else {
    tpl += '<div class="next-disabled">▶</div>';
}
複製程式碼

寫在後面

DatePicker比較核心的邏輯就在這裡了

400行左右的程式碼,每個人都可以嘗試著寫一遍,很有意思的

下一篇文章介紹TimePicker,敬請期待

最後,求star,求fork,求內推

github.com/veedrin/qin…

相關文章