2
0
mirror of https://github.com/tenrok/vue2-datepicker.git synced 2026-05-17 04:39:40 +03:00

refactor: 2.0

This commit is contained in:
mxie
2018-06-16 10:11:13 +08:00
parent d28d307def
commit 96fffcab04
39 changed files with 10988 additions and 2969 deletions
+17 -1
View File
@@ -7,5 +7,21 @@
}
],
"stage-3"
]
],
"plugins": [
"transform-vue-jsx",
"jsx-v-model"
],
"env": {
"test": {
"presets": [
["env", { "target": { "node": "current" }}],
"stage-3"
],
"plugins": [
"transform-vue-jsx",
"jsx-v-model"
]
}
}
}
+6
View File
@@ -0,0 +1,6 @@
/build/
/config/
/dist/
/*.js
/__test__/
/test/
+43
View File
@@ -0,0 +1,43 @@
// https://eslint.org/docs/user-guide/configuring
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint'
},
env: {
browser: true,
},
extends: [
// https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
// consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
'plugin:vue/essential',
// https://github.com/standard/standard/blob/master/docs/RULES-en.md
'standard'
],
// required to lint *.vue files
plugins: [
'vue'
],
// add your custom rules here
rules: {
// allow async-await
'generator-star-spacing': 'off',
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'camelcase': ['off', { properties: 'never' }],
// "vue/max-attributes-per-line": [2, {
// "singleline": 1,
// "multiline": {
// "max": 1,
// "allowFirstLine": true
// }
// }],
"vue/html-indent": ["error", 2, {
"attribute": 1,
"closeBracket": 0,
"alignAttributesVertically": false,
"ignores": []
}]
}
}
+2
View File
@@ -1,4 +1,6 @@
.DS_Store
node_modules/
lib
npm-debug.log
yarn-error.log
coverage
+5
View File
@@ -0,0 +1,5 @@
language: node_js
node_js:
- "node"
- "8"
script: "npm run test:push"
+53 -19
View File
@@ -1,11 +1,21 @@
# vue2-datepicker
[中文版](https://github.com/mengxiong10/vue2-datepicker/blob/master/README_CN.md)
[中文版](https://github.com/mengxiong10/vue2-datepicker/blob/master/README.zh-CN.md)
> A Datepicker Component For Vue2
<a href="https://travis-ci.org/mengxiong10/vue2-datepicker">
<img src="https://travis-ci.org/mengxiong10/vue2-datepicker.svg?branch=master" alt="build:passed">
</a>
<a href="https://coveralls.io/github/mengxiong10/vue2-datepicker">
<img src="https://coveralls.io/repos/github/mengxiong10/vue2-datepicker/badge.svg?branch=master" alt="Badge">
</a>
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-MIT-yellow.svg">
</a>
## Demo
<https://mengxiong10.github.io/vue2-datepicker/>
<https://mengxiong10.github.io/vue2-datepicker/demo>
![image](https://github.com/mengxiong10/vue2-datepicker/raw/master/screenshot/demo.PNG)
@@ -27,13 +37,19 @@ export default {
return {
time1: '',
time2: '',
time3: '',
shortcuts: [
{
text: 'Today',
start: new Date(),
end: new Date()
}
]
],
timePickerOptions:{
start: '00:00',
step: '00:30',
end: '23:30'
}
}
}
}
@@ -42,50 +58,66 @@ export default {
<template>
<div>
<date-picker v-model="time1" :first-day-of-week="1"></date-picker>
<date-picker v-model="time2" range :shortcuts="shortcuts"></date-picker>
<date-picker v-model="time2" type="datetime" :time-picker-options="timePickerOptions"></date-picker>
<date-picker v-model="time3" range :shortcuts="shortcuts"></date-picker>
</div>
</template>
```
### Attributes
### Props
| Prop | Type | Default | Description |
|---------------------|---------------|-------------|-----------------------------------------------------|
| type | String | 'date' | select datepicker or datetimepicker(date/datetime) |
| range | Boolean | false | if true, the type is daterange or datetimerange |
| format | String | yyyy-MM-dd | Date formatting string |
| custom-formatter | function | null | custom Date display |
| format | String | YYYY-MM-DD | The parsing tokens are similar to the moment.js |
| lang | String/Object | zh | Translation (en/zh/es/pt-br/fr/ru/de/it/cs)(custom) |
| clearable | Boolean | true | if false, don't show the clear icon |
| confirm | Boolean | false | if true, need click the button to change the value |
| editable | Boolean | true | if false, user cann't type it |
| disabled | Boolean | false | Disable the component |
| editable | Boolean | false | if true, user can type it(only the range is false) |
| placeholder | String | | input placeholder text |
| width | String/Number | 210 | input size |
| disabled-days | Array | [] | Days in YYYY-MM-DD format to disable |
| not-before | String/Date | '' | Disable all dates before new Date(not-before) |
| not-after | String/Date | '' | Disable all dates after new Date(not-after) |
| disabled-days | Array/function| [] | Disable Days |
| shortcuts | Boolean/Array | true | the shortcuts for the range picker |
| time-picker-options | Object | {} | set timePickerOptions(start, step, end) |
| minute-step | Number | 0 | if > 0 don't show the second picker(0 - 60) |
| first-day-of-week | Number | 7 | set the first day of week (1-7) |
| input-class | String | 'mx-input' | the input class name |
| input-name | String | 'date' | the input name attr |
| confirm-text | String | 'OK' | the default text to display on confirm button |
| range-separator | String | '~' | the range separator text |
#### lang
* String (en/zh/es/pt-br/fr/ru/de/it/cs)
* Object
* Object (custom)
```JavaScript
{
days: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
months: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
pickers: ['next 7 days', 'next 30 days', 'previous 7 days', 'previous 30 days'],
placeholder: {
date: 'Select Date',
dateRange: 'Select Date Range'
```html
<script>
export default {
data() {
return {
value: '',
lang: {
days: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
months: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
pickers: ['next 7 days', 'next 30 days', 'previous 7 days', 'previous 30 days'],
placeholder: {
date: 'Select Date',
dateRange: 'Select Date Range'
}
}
}
}
}
</script>
<template>
<date-picker v-model="value" :lang="lang"></date-picker>
</template>
```
#### shortcuts
@@ -112,8 +144,10 @@ export default {
### Events
| Name | Description | Callback Arguments |
|-----------------|------------------------------|------------------------|
| change | When user select date | the currentValue |
| change | When the value change | the currentValue |
| input | When the value change | the currentValue |
| confirm | When user click 'OK' button | the currentValue |
| input-error | When user type a invalid Date| the input value |
## License
+126
View File
@@ -0,0 +1,126 @@
# vue2-datepicker
[English Version](https://github.com/mengxiong10/vue2-datepicker/blob/master/README_CN.md)
> 一个基于Vue2.x的日期时间选择组件
<a href="https://travis-ci.org/mengxiong10/vue2-datepicker">
<img src="https://travis-ci.org/mengxiong10/vue2-datepicker.svg?branch=master" alt="build:passed">
</a>
<a href="https://coveralls.io/github/mengxiong10/vue2-datepicker">
<img src="https://coveralls.io/repos/github/mengxiong10/vue2-datepicker/badge.svg?branch=master" alt="Badge">
</a>
<a href="LICENSE">
<img src="https://img.shields.io/badge/License-MIT-yellow.svg">
</a>
## 线上Demo
<https://mengxiong10.github.io/vue2-datepicker/demo>
![image](https://github.com/mengxiong10/vue2-datepicker/raw/master/screenshot/demo.PNG)
## 安装
```bash
$ npm install vue2-datepicker --save
```
## 用法
```html
<script>
import DatePicker from 'vue2-datepicker'
export default {
components: { DatePicker },
data() {
return {
time1: '',
time2: '',
time3: '',
shortcuts: [
{
text: 'Today',
start: new Date(),
end: new Date()
}
],
timePickerOptions:{
start: '00:00',
step: '00:30',
end: '23:30'
}
}
}
}
</script>
<template>
<div>
<date-picker v-model="time1" :first-day-of-week="1"></date-picker>
<date-picker v-model="time2" type="datetime" :time-picker-options="timePickerOptions"></date-picker>
<date-picker v-model="time3" range :shortcuts="shortcuts"></date-picker>
</div>
</template>
```
### Props
| Prop | Type | Default | Description
|---------------------|---------------|-------------|-----------------------------------------------------
| type | String | 'date' | 选择日期或日期时间(可选:date,datetime)
| range | Boolean | false | 如果是true, 显示日历范围选择
| format | String | YYYY-MM-DD | 格式化显示日期 api类似moment.js
| lang | String/Object | zh | 选择语言或自定义 (en/zh/es/pt-br/fr/ru/de/it/cs)(custom)
| clearable | Boolean | true | 如果设置false, 不显示清除图标
| confirm | Boolean | false | 如果是true, 显示确认按钮且需要确认才更新时间
| editable | Boolean | true | 如果是false, 用户不能手动输入更新日期
| disabled | Boolean | false | 禁用组件
| placeholder | String | | 输入框placeholder
| width | String/Number | 210 | 设置宽度
| not-before | String/Date | '' | 禁止选择这个时间之前的时间
| not-after | String/Date | '' | 禁止选择这个时间之前=后的时间
| disabled-days | Array/function| [] | 自定义禁止的日期
| shortcuts | Boolean/Array | true | 自定义范围选择的时候快捷选项(见下表)
| time-picker-options | Object | {} | 自定义时间选择的开始,结束,步进(见下表)
| minute-step | Number | 0 | 设置分钟的步进, 设置后不显示秒的选择
| first-day-of-week | Number | 7 | 设置日历星期几开头(1-7)
| input-class | String | 'mx-input' | 自定义输入框的类名
| input-name | String | 'date' | 自定义input 的 name 属性
| confirm-text | String | 'OK' | 确认按钮的名称
| range-separator | String | '~' | range 分隔符
#### shortcuts
* true - 显示默认快捷选择
* false - 隐藏快捷选择
* Object[] - 自定义快捷选择, 格式:[{text, start, end}]
| 名称 | 类型 | 说明 |
|-----------------|---------------|----------------|
| text | String | 显示文字 |
| start | Date | 开始日期 |
| end | Date | 结束日期 |
#### time-picker-options
* Object[] - 自定义时间选择, 格式:[{start, step, end}]
| 名称 | 类型 | 说明 |
|-----------------|---------------|-----------------------|
| start | String | 开始时间 (eg '00:00') |
| step | String | 步进时间 (eg '00:30') |
| end | String | 结束时间 (eg '23:30') |
### Events
| Name | 说明 | 回调参数 |
|-----------------|----------------------------- |----------------|
| change | 日期改变的时候触发 | 选择的日期 |
| input | 日期改变的时候触发 | 选择的日期 |
| confirm | 点击确认按钮触发的事件 | 选择的日期 |
| input-error | 当用户输入的值无效时候触发 | 用户输入的字符串 |
## 许可证
[MIT](https://github.com/mengxiong10/vue2-datepicker/blob/master/LICENSE)
Copyright (c) 2017-present xiemengxiong
-103
View File
@@ -1,103 +0,0 @@
# vue2-datepicker
[English Version](https://github.com/mengxiong10/vue2-datepicker/blob/master/README_CN.md)
> 一个基于Vue2.x的日期时间选择组件
## 线上Demo
<https://mengxiong10.github.io/vue2-datepicker/>
![image](https://github.com/mengxiong10/vue2-datepicker/raw/master/screenshot/demo.PNG)
## 安装
```bash
$ npm install vue2-datepicker --save
```
## 用法
```html
<script>
import DatePicker from 'vue2-datepicker'
export default {
components: { DatePicker },
data() {
return {
time1: '',
time2: '',
shortcuts: [
{
text: 'Today',
start: new Date(),
end: new Date()
}
]
}
}
}
</script>
<template>
<div>
<date-picker v-model="time1" :first-day-of-week="1"></date-picker>
<date-picker v-model="time2" range :shortcuts="shortcuts"></date-picker>
</div>
</template>
```
### Props
| 名称 | 类型 | 默认 | 说明
|---------------------|---------------|-------------|-------------------------------------------
| type | String | 'date' | 选择日期或日期时间(可选:date,datetime)
| range | Boolean | false | 如果是true, 显示日历范围选择
| confirm | Boolean | false | 如果是true, 显示确认按钮且需要确认才更新时间
| format | String | yyyy-MM-dd | 自定义显示在输入框上的格式(yyyy-MM-dd HH:mm:ss)
| lang | String | zh | 选择语言 (en/zh/es/pt-br/fr/ru/de/it/cs)
| placeholder | String | | placeholder的值
| width | String/Number | 210 | 输入框的width
| disabled-days | Array | [] | 禁止选择的日期 (['2018-1-1'])
| not-before | String/Date | '' | 禁止选择这个时间之前的时间
| not-after | String/Date | '' | 禁止选择这个时间之后的时间
| shortcuts | Boolean/Array | true | 自定义范围选择的时候快捷选项(见下表)
| time-picker-options | Object | {} | 自定义时间选择的开始,结束,步进(见下表)
| minute-step | Number | 0 | 分钟的步进,设置time-picker-options,这项无效
| first-day-of-week | Number | 7 | 设置日历星期几开头(1-7)
| input-class | String | 'mx-input' | 自定义输入框的类名
| confirm-text | String | 'OK' | 确认按钮的名称
| disabled | Boolean | false | 禁用组件
| editable | Boolean | false | 如果是true, 用户可以手动输入 (仅在range === false)
#### shortcuts
* true - 显示默认快捷选择
* false - 隐藏快捷选择
* Object[] - 自定义快捷选择, 格式:[{text, start, end}]
| 名称 | 类型 | 说明 |
|-----------------|---------------|----------------|
| text | String | 显示文字 |
| start | Date | 开始日期 |
| end | Date | 结束日期 |
#### time-picker-options
* Object[] - 自定义时间选择, 格式:[{start, step, end}]
| 名称 | 类型 | 说明 |
|-----------------|---------------|-----------------------|
| start | String | 开始时间 (eg '00:00') |
| step | String | 步进时间 (eg '00:30') |
| end | String | 结束时间 (eg '23:30') |
### Events
| Name | 说明 | 回调参数 |
|-----------------|------------------------------|-------------|
| change | 选择的时候触发 | 选择的日期 |
| confirm | 点击确认按钮触发的事件 | 选择的日期 |
## 许可证
[MIT](https://github.com/mengxiong10/vue2-datepicker/blob/master/LICENSE)
Copyright (c) 2017-present xiemengxiong
+48
View File
@@ -0,0 +1,48 @@
const path = require('path')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'@': path.join(__dirname, '..', 'src')
}
},
module: {
rules: [
{
test: /\.vue$/,
use: [
{
loader: 'vue-loader',
options: {
}
}
]
},
{
test:/\.scss$/,
use: ['vue-style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
},
{
test: /\.js$/,
use: ['babel-loader'],
exclude: /node_modules/
},
{
test: /\.(png|jpg|gif|svg)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]?[hash]'
}
}
]
}
]
},
plugins: [
new VueLoaderPlugin()
]
}
+13
View File
@@ -0,0 +1,13 @@
const merge = require('webpack-merge')
const devWebpackConfig = require('./webpack.dev.config.js')
const webpackConfig = merge(devWebpackConfig, {
devtool: 'source-map',
mode: 'production'
externals: {
'vue': 'Vue',
'@/index': 'DatePicker'
},
})
module.exports = webpackConfig
+50
View File
@@ -0,0 +1,50 @@
const path = require('path')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.config.js')
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const webpackConfig = merge(baseWebpackConfig, {
mode: 'production',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, '../lib'),
filename: 'index.js',
library: "DatePicker",
libraryTarget: "umd"
},
plugins: [
new OptimizeCSSPlugin({
cssProcessorOptions: { safe: true }
})
]
})
const webpackConfigExtractCss = merge(baseWebpackConfig, {
mode: 'production',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, '../lib'),
filename: 'datepicker.js',
library: "DatePicker",
libraryTarget: "umd"
},
module: {
rules: [
{
test:/\.scss$/,
use: [ MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader']
},
]
},
plugins: [
new MiniCssExtractPlugin({
filename: 'datepicker.css'
}),
new OptimizeCSSPlugin({
cssProcessorOptions: { safe: true }
})
]
})
module.exports = [ webpackConfig, webpackConfigExtractCss]
+30
View File
@@ -0,0 +1,30 @@
const path = require('path')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.config.js')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const webpackConfig = merge(baseWebpackConfig, {
mode: 'development',
entry: './demo/index.js',
output: {
path: path.resolve(__dirname, '../demo'),
filename: 'build.js'
},
externals: {
'vue': 'Vue'
},
devServer: {
historyApiFallback: {
index: './demo/index.html'
},
noInfo: true,
port: 9000
},
performance: {
hints: false
},
devtool: 'cheap-module-eval-source-map'
})
module.exports = webpackConfig
-676
View File
@@ -1,676 +0,0 @@
<template>
<div class="mx-calendar">
<div class="mx-calendar-header" v-if="currentPanel === 'time'">
<a @click="currentPanel = 'date'">{{now.toLocaleDateString()}}</a>
</div>
<div class="mx-calendar-header" v-else>
<a class="mx-calendar__prev-icon" @click="changeYear(-1)">&laquo;</a>
<a v-show="currentPanel === 'date'" class="mx-calendar__prev-icon" @click="changeMonth(-1)">&lsaquo;</a>
<a class="mx-calendar__next-icon" @click="changeYear(1)">&raquo;</a>
<a v-show="currentPanel === 'date'" class="mx-calendar__next-icon" @click="changeMonth(1)">&rsaquo;</a>
<a @click="showMonths">{{months[currentMonth]}}</a>
<a @click="showYears">{{currentYear}}</a>
</div>
<div class="mx-calendar-content">
<table class="mx-calendar-table" v-show="currentPanel === 'date'">
<thead>
<tr>
<th v-for="(day, index) in days" :key="index">{{day}}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in dates">
<td v-for="cell in row" :title="cell.title" :class="getDateClasses(cell)" @click="selectDate(cell)">{{cell.day}}</td>
</tr>
</tbody>
</table>
<div class="mx-calendar-year" v-show="currentPanel === 'years'">
<a v-for="year in years"
@click="selectYear(year)"
:class="{'current': currentYear === year, 'disabled': isDisabledYear(year)}">{{year}}</a>
</div>
<div class="mx-calendar-month" v-show="currentPanel === 'months'">
<a v-for="(month, index) in months"
@click="selectMonth(index)"
:class="{'current': currentMonth === index, 'disabled': isDisabledMonth(index)}">{{month}}</a>
</div>
<div class="mx-calendar-time"
v-show="currentPanel === 'time'" >
<div v-if="timeSelectOptions.length" class="mx-time-list-wrapper">
<ul class="mx-time-list">
<li class="mx-time-item mx-time-picker-item"
:class="getTimeClasses(item.value.hours * 60 + item.value.minutes, -1)"
@click="pickTime(item.value)"
v-for="item in timeSelectOptions">
{{item.label}}
</li>
</ul>
</div>
<div v-else class="mx-time-list-wrapper"
:style="{width: 100 / times.length + '%' }"
v-for="(time, index) in times"
:key="index">
<ul class="mx-time-list">
<li class="mx-time-item"
v-for="num in time"
:class="getTimeClasses(num, index)"
:key="num"
@click="selectTime(num, index)"
>{{num | timeText}}</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
const getTimeArray = function (len, step = 1) {
const length = parseInt(len / step)
return Array.apply(null, { length }).map((v, i) => i * step)
}
const parseTime = function(time) {
const values = (time || '').split(':');
if (values.length >= 2) {
const hours = parseInt(values[0], 10);
const minutes = parseInt(values[1], 10);
return {
hours,
minutes
}
}
return null;
}
const formatTime = function(time, type="24") {
let hours = time.hours
hours = (type === '24') ? hours : (hours % 12 || 12)
hours = hours < 10 ? '0' + hours : hours
let minutes = time.minutes < 10 ? '0' + time.minutes : time.minutes
let result = hours + ':' + minutes
if (type === '12') {
result += time.hours >= 12 ? ' pm' : ' am'
}
return result
}
export default {
props: {
startAt: null,
endAt: null,
value: null,
show: Boolean
},
data() {
const translation = this.$parent.translation
const minuteStep = this.$parent.minuteStep
const times = [getTimeArray(24, 1), getTimeArray(60, minuteStep || 1)]
if (minuteStep === 0) {
times.push(getTimeArray(60, 1))
}
return {
months: translation.months,
dates: [], // 日历面板
years: [], // 年代面板
now: new Date(), // calendar-header 显示的时间, 用于切换日历
currentPanel: 'date',
times: times
}
},
computed: {
// 日历显示头
days() {
const days = this.$parent.translation.days
const firstday = +this.$parent.firstDayOfWeek
return days.concat(days).slice(firstday, firstday + 7)
},
timeType () {
return /h+/.test(this.$parent.format) ? '12' : '24'
},
timeSelectOptions () {
const result = []
const options = this.$parent.timePickerOptions
if (!options) {
return []
}
if (typeof options === 'function') {
return options() || []
}
const start = parseTime(options.start)
const end = parseTime(options.end)
const step = parseTime(options.step)
if (start && end && step) {
const startMinutes = start.minutes + start.hours * 60;
const endMinutes = end.minutes + end.hours * 60;
const stepMinutes = step.minutes + step.hours * 60
const len = Math.floor((endMinutes - startMinutes) / stepMinutes)
for (let i = 0; i <= len; i++) {
let timeMinutes = startMinutes + i * stepMinutes
let hours = Math.floor(timeMinutes/60)
let minutes = timeMinutes % 60
let value = {
hours, minutes
}
result.push({
value,
label: formatTime(value, this.timeType)
})
}
}
return result
},
currentYear() {
return this.now.getFullYear()
},
currentMonth() {
return this.now.getMonth()
},
curHour() {
return this.now.getHours()
},
curMinute() {
return this.now.getMinutes()
},
curSecond() {
return this.now.getSeconds()
}
},
created() {
this.updateCalendar()
},
watch: {
show(val) {
if (val) {
this.currentPanel = 'date'
this.updateNow()
}
},
value: {
handler: 'updateNow',
immediate: true
},
now: 'updateCalendar'
},
filters: {
timeText(value) {
return ('00' + value).slice(String(value).length)
}
},
methods: {
updateNow() {
this.now = this.value ? new Date(this.value) : new Date()
},
// 更新面板选择时间
updateCalendar() {
function getCalendar(time, firstday, length, classes) {
return Array.apply(null, { length }).map((v, i) => {
// eslint-disable-line
let day = firstday + i
const date = new Date(
time.getFullYear(),
time.getMonth(),
day,
0,
0,
0
)
date.setDate(day)
return {
title: date.toLocaleDateString(),
date,
day,
classes
}
})
}
const firstDayOfWeek = this.$parent.firstDayOfWeek
const time = new Date(this.now)
time.setDate(0) // 把时间切换到上个月最后一天
const lastMonthLength = (time.getDay() + 7 - firstDayOfWeek) % 7 + 1 // time.getDay() 0是星期天, 1是星期一 ...
const lastMonthfirst = time.getDate() - (lastMonthLength - 1)
const lastMonth = getCalendar(
time,
lastMonthfirst,
lastMonthLength,
'lastMonth'
)
time.setMonth(time.getMonth() + 2, 0) // 切换到这个月最后一天
const curMonthLength = time.getDate()
const curMonth = getCalendar(time, 1, curMonthLength, 'curMonth')
time.setMonth(time.getMonth() + 1, 1)
const nextMonthLength = 42 - (lastMonthLength + curMonthLength)
const nextMonth = getCalendar(time, 1, nextMonthLength, 'nextMonth')
// 分割数组
let index = 0
let resIndex = 0
const arr = lastMonth.concat(curMonth, nextMonth)
const result = new Array(6)
while (index < 42) {
result[resIndex++] = arr.slice(index, (index += 7))
}
this.dates = result
},
isDisabled (date) {
const now = new Date(date).getTime()
if (
this.$parent.disabledDays.some(v => new Date(v).setHours(0, 0, 0, 0) === now) ||
this.$parent.notBefore !== '' && now < new Date(this.$parent.notBefore).setHours(0, 0, 0, 0) ||
this.$parent.notAfter !== '' && now > new Date(this.$parent.notAfter).setHours(0, 0, 0, 0) ||
this.startAt && now < new Date(this.startAt).setHours(0, 0, 0, 0) ||
this.endAt && now > new Date(this.endAt).setHours(0, 0, 0, 0)
) {
return true
}
return false
},
getDateClasses(cell) {
const classes = []
const cellTime = new Date(cell.date).setHours(0, 0, 0, 0)
const cellEndTime = new Date(cell.date).setHours(23, 59, 59, 999)
const curTime = this.value ? new Date(this.value).setHours(0, 0, 0, 0) : 0
const startTime = this.startAt
? new Date(this.startAt).setHours(0, 0, 0, 0)
: 0
const endTime = this.endAt ? new Date(this.endAt).setHours(0, 0, 0, 0) : 0
const today = new Date().setHours(0, 0, 0, 0)
if (this.isDisabled(cellTime)) {
return 'disabled'
}
classes.push(cell.classes)
if (cellTime === today) {
classes.push('today')
}
// range classes
if (curTime) {
if (cellTime === curTime) {
classes.push('current')
} else if (startTime && cellTime <= curTime) {
classes.push('inrange')
} else if (endTime && cellTime >= curTime) {
classes.push('inrange')
}
}
return classes.join(' ')
},
getTimeClasses(value, index) {
let curValue
let cellTime
const startTime = this.startAt ? new Date(this.startAt) : 0
const endTime = this.endAt ? new Date(this.endAt) : 0
const classes = []
switch (index) {
case -1:
curValue = this.curHour * 60 + this.curMinute
cellTime = new Date(this.now).setHours(Math.floor(value / 60), value % 60, 0)
break
case 0:
curValue = this.curHour
cellTime = new Date(this.now).setHours(value)
break
case 1:
curValue = this.curMinute
cellTime = new Date(this.now).setMinutes(value)
break
case 2:
curValue = this.curSecond
cellTime = new Date(this.now).setSeconds(value)
break
}
if (
(this.$parent.notBefore !== '' &&
cellTime < new Date(this.$parent.notBefore).getTime()) ||
(this.$parent.notAfter !== '' &&
cellTime > new Date(this.$parent.notAfter).getTime())
) {
return 'disabled'
}
if (value === curValue) {
classes.push('cur-time')
} else if (startTime) {
if (cellTime < startTime) {
classes.push('disabled')
}
} else if (endTime) {
if (cellTime > endTime) {
classes.push('disabled')
}
}
return classes.join(' ')
},
showMonths() {
if (this.currentPanel === 'months') {
this.currentPanel = 'date'
} else {
this.currentPanel = 'months'
}
},
showYears() {
// 当前年代
if (this.currentPanel === 'years') {
this.currentPanel = 'date'
} else {
let firstYear = Math.floor(this.now.getFullYear() / 10) * 10
let years = []
for (let i = 0; i < 10; i++) {
years.push(firstYear + i)
}
this.years = years
this.currentPanel = 'years'
}
},
// 前进或后退一年
changeYear(flag) {
if (this.currentPanel === 'years') {
this.years = this.years.map(v => v + flag * 10)
} else {
const now = new Date(this.now)
now.setFullYear(now.getFullYear() + flag, now.getMonth(), 1)
this.now = now
}
},
changeMonth(flag) {
const now = new Date(this.now)
now.setMonth(now.getMonth() + flag, 1)
this.now = now
},
scrollIntoView(container, selected) {
if (!selected) {
container.scrollTop = 0
return
}
const top = selected.offsetTop
const bottom = selected.offsetTop + selected.offsetHeight
const viewRectTop = container.scrollTop
const viewRectBottom = viewRectTop + container.clientHeight
if (top < viewRectTop) {
container.scrollTop = top
} else if (bottom > viewRectBottom) {
container.scrollTop = bottom - container.clientHeight
}
},
selectDate(cell) {
const classes = this.getDateClasses(cell)
if (classes.indexOf('disabled') !== -1) {
return
}
let date = new Date(cell.date)
// datetime 跳转到 timepicker
if (this.$parent.type === 'datetime') {
// 保留时分秒
if (this.value instanceof Date) {
date.setHours(
this.value.getHours(),
this.value.getMinutes(),
this.value.getSeconds()
)
}
if (this.startAt && date.getTime() < new Date(this.startAt).getTime()) {
date = new Date(this.startAt)
} else if (
this.endAt &&
date.getTime() > new Date(this.endAt).getTime()
) {
date = new Date(this.endAt)
}
this.currentPanel = 'time'
this.$nextTick(() => {
Array.prototype.forEach.call(
this.$el.querySelectorAll('.mx-time-list-wrapper'),
(el) => {
this.scrollIntoView(el, el.querySelector('.cur-time'))
}
)
})
}
this.now = date
this.$emit('input', date)
this.$emit('select')
},
isDisabledYear (year) {
if (this.value) {
const now = new Date(this.now).setFullYear(year)
return this.isDisabled(now)
}
return false
},
isDisabledMonth (month) {
if (this.value) {
const now = new Date(this.now).setMonth(month)
return this.isDisabled(now)
}
return false
},
selectYear(year) {
if (this.isDisabledYear(year)) {
return
}
const now = new Date(this.now)
now.setFullYear(year)
this.now = now
if (this.value) {
this.$emit('input', now)
this.$emit('select', true)
}
this.currentPanel = 'months'
},
selectMonth(month) {
if (this.isDisabledMonth(month)) {
return
}
const now = new Date(this.now)
now.setMonth(month)
this.now = now
if (this.value) {
this.$emit('input', now)
this.$emit('select', true)
}
this.currentPanel = 'date'
},
selectTime(value, index) {
const classes = this.getTimeClasses(value, index)
if (classes.indexOf('disabled') !== -1) {
return
}
const date = new Date(this.now)
if (index === 0) {
date.setHours(value)
} else if (index === 1) {
date.setMinutes(value)
} else if (index === 2) {
date.setSeconds(value)
}
this.now = date
this.$emit('input', date)
this.$emit('select')
},
pickTime (value) {
const classes = this.getTimeClasses(value.hours * 60 + value.minutes, -1)
if (classes.indexOf('disabled') !== -1) {
return
}
const date = new Date(this.now)
date.setHours(value.hours, value.minutes, 0)
this.now = date
this.$emit('input', date)
this.$emit('select')
}
}
}
</script>
<style lang="scss">
.mx-calendar {
float: left;
width: 100%;
padding: 6px 12px;
a {
color: inherit;
text-decoration: none;
cursor: pointer;
}
}
.mx-calendar-header {
line-height: 34px;
text-align: center;
& > a:hover {
color: #1284e7;
}
}
.mx-calendar__next-icon,
.mx-calendar__prev-icon {
font-size: 20px;
padding: 0 6px;
}
.mx-calendar__prev-icon {
float: left;
}
.mx-calendar__next-icon {
float: right;
}
.mx-calendar-content {
height: 224px;
overflow: hidden;
}
.mx-calendar-table {
width: 100%;
font-size: 12px;
table-layout: fixed;
border-collapse: collapse;
border-spacing: 0;
td {
cursor: pointer;
}
.today {
color: #20a0ff;
}
.lastMonth,
.nextMonth {
color: #ddd;
}
}
.mx-calendar-table td,
.mx-calendar-table th {
width: 32px;
height: 32px;
text-align: center;
}
.mx-calendar-table td {
cursor: pointer;
}
.mx-calendar-table td.inrange,
.mx-calendar-table td:hover,
.mx-calendar-year > a:hover,
.mx-calendar-month > a:hover {
background-color: #eaf8fe;
}
.mx-calendar-table td.current,
.mx-calendar-year > a.current,
.mx-calendar-month > a.current {
color: #fff;
background-color: #1284e7;
}
.mx-calendar-table td.disabled,
.mx-time-item.disabled,
.mx-calendar-year a.disabled,
.mx-calendar-month a.disabled {
cursor: not-allowed;
color: #ccc;
background-color: #f3f3f3;
}
.mx-calendar-year,
.mx-calendar-month,
.mx-calendar-time {
width: 100%;
height: 100%;
padding: 7px 0;
text-align: center;
}
.mx-calendar-year > a {
display: inline-block;
width: 40%;
margin: 1px 5%;
line-height: 40px;
}
.mx-calendar-month > a {
display: inline-block;
width: 30%;
line-height: 40px;
margin: 8px 1.5%;
}
.mx-time-list-wrapper {
position: relative;
display: inline-block;
width: 100%;
height: 100%;
border-top: 1px solid rgba(0, 0, 0, 0.05);
border-left: 1px solid rgba(0, 0, 0, 0.05);
box-sizing: border-box;
overflow-y: auto;
}
.mx-time-list-wrapper::-webkit-scrollbar {
width: 8px;
height: 8px;
}
/* 滚动条滑块 */
.mx-time-list-wrapper::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.05);
border-radius: 10px;
box-shadow: inset 1px 1px 0 rgba(0, 0, 0, 0.1);
}
.mx-time-list-wrapper:hover::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.2);
}
.mx-time-list-wrapper:first-child {
border-left: 0;
}
.mx-time-picker-item {
text-align: left;
padding-left: 10px;
}
.mx-time-list {
margin: 0;
padding: 0;
list-style: none;
}
.mx-time-item {
width: 100%;
font-size: 12px;
height: 30px;
line-height: 30px;
cursor: pointer;
}
.mx-time-item:hover {
background-color: #eaf8fe;
}
.mx-time-item.cur-time {
color: #fff;
background-color: #1284e7;
}
</style>
-583
View File
@@ -1,583 +0,0 @@
<template>
<div class="mx-datepicker"
:class="{'disabled': disabled}"
:style="{'width': computedWidth,'min-width':range ? (type === 'datetime' ? '320px' : '210px') : '140px'}"
v-clickoutside="closePopup">
<input :name="inputName"
:disabled="disabled"
:class="inputClass"
:value="text"
:readonly="!editable || range"
:placeholder="innerPlaceholder"
ref="input"
@mouseenter="hoverIcon"
@mouseleave="hoverIcon"
@click="togglePopup"
@input="handleInput"
@change="handleChange"
@mousedown="$event.preventDefault()">
<i class="mx-input-icon"
:class="showCloseIcon ? 'mx-input-icon__close' : 'mx-input-icon__calendar'"
@mouseenter="hoverIcon"
@mouseleave="hoverIcon"
@click="clickIcon"></i>
<div class="mx-datepicker-popup"
:class="{'range':range}"
:style="position"
ref="calendar"
v-show="showPopup">
<calendar-panel v-if="!range"
v-model="currentValue"
@select="selectDate"
:show="showPopup"></calendar-panel>
<div v-else
style="overflow:hidden">
<div class="mx-datepicker-top"
v-if="ranges.length">
<span v-for="range in ranges"
@click="selectRange(range)">{{range.text}}</span>
</div>
<calendar-panel style="width:50%;box-shadow:1px 0 rgba(0, 0, 0, .1)"
v-model="currentValue[0]"
:end-at="currentValue[1]"
@select="selectDate"
:show="showPopup"></calendar-panel>
<calendar-panel style="width:50%;"
v-model="currentValue[1]"
:start-at="currentValue[0]"
@select="selectDate"
:show="showPopup"></calendar-panel>
</div>
<div class="mx-datepicker-footer"
v-if="confirm">
<button type="button"
class="mx-datepicker-btn mx-datepicker-btn-confirm"
@click="confirmDate"> {{ confirmText }}</button>
</div>
</div>
</div>
</template>
<script>
import CalendarPanel from './calendar-panel.vue'
import Languages from './languages.js'
const isObject = function (obj) {
return obj !== null && typeof obj === 'object'
}
export default {
name: 'DatePicker',
components: { CalendarPanel },
props: {
value: null,
format: {
type: String,
default: 'yyyy-MM-dd'
},
customFormatter: {
type: Function
},
range: {
type: Boolean,
default: false
},
type: {
type: String,
default: 'date' // ['date', 'datetime']
},
width: {
type: [String, Number],
default: 210
},
placeholder: String,
lang: {
type: [String, Object],
default: 'zh'
},
shortcuts: {
type: [Boolean, Array],
default: true
},
disabledDays: {
type: Array,
default: function () {
return []
}
},
notBefore: {
default: ''
},
notAfter: {
default: ''
},
firstDayOfWeek: {
default: 7,
type: Number,
validator: val => val >= 1 && val <= 7
},
minuteStep: {
type: Number,
default: 0,
validator: val => val >= 0 && val <= 60
},
timePickerOptions: {
type: [Object, Function],
default () {
return null
}
},
confirm: {
type: Boolean,
default: false
},
inputClass: {
type: String,
default: 'mx-input'
},
confirmText: {
type: String,
default: 'OK'
},
disabled: {
type: Boolean,
default: false
},
editable: {
type: Boolean,
default: false
},
rangeSeparator: {
type: String,
default: '~'
},
inputName:{
type: String,
default: 'date'
}
},
data () {
return {
showPopup: false,
showCloseIcon: false,
currentValue: this.value,
position: null,
userInput: null,
ranges: [] // 快捷选项
}
},
watch: {
value: {
handler (val) {
if (!this.range) {
this.currentValue = this.isValidDate(val) ? val : undefined
} else {
this.currentValue = this.isValidRange(val)
? val.slice(0, 2)
: [undefined, undefined]
}
},
immediate: true
},
showPopup (val) {
if (val) {
this.$nextTick(this.displayPopup)
} else {
this.userInput = null
}
}
},
computed: {
translation () {
if (isObject(this.lang)) {
return { ...Languages['en'], ...this.lang }
}
return Languages[this.lang] || Languages['en']
},
innerPlaceholder () {
return (
this.placeholder ||
(this.range
? this.translation.placeholder.dateRange
: this.translation.placeholder.date)
)
},
text () {
if (!this.range && this.isValidDate(this.value)) {
return this.userInput !== null ? this.userInput : this.stringify(this.value)
}
if (this.range && this.isValidRange(this.value)) {
return (
this.stringify(this.value[0]) + ` ${this.rangeSeparator} ` + this.stringify(this.value[1])
)
}
return ''
},
computedWidth () {
if ((typeof this.width === 'string' && /^\d+$/.test(this.width)) || typeof this.width === 'number') {
return this.width + 'px'
}
return this.width
}
},
methods: {
handleInput (event) {
this.userInput = event.target.value
},
handleChange (event) {
const value = event.target.value
const date = this.parseDate(value, this.format)
if (date && this.editable && !this.range) {
if (this.notBefore && date < new Date(this.notBefore)) {
return
}
if (this.notAfter && date > new Date(this.notAfter)) {
return
}
for (let i = 0, len = this.disabledDays.length; i < len; i++) {
if (date.getTime() === new Date(this.disabledDays[i]).getTime()) {
return
}
}
this.$emit('input', date)
this.$emit('change', date)
this.closePopup()
}
},
updateDate () {
const val = this.currentValue
if ((!this.range && val) || (this.range && val[0] && val[1])) {
this.$emit('input', val)
this.$emit('change', val)
}
},
confirmDate () {
this.updateDate()
this.closePopup()
this.$emit('confirm', this.currentValue)
},
selectDate (show = false) {
if (!this.confirm && !this.disabled) {
this.updateDate()
if (!show && this.type === 'date' && !this.range) {
this.closePopup()
}
}
},
closePopup () {
this.showPopup = false
},
togglePopup () {
if (this.showPopup) {
this.$refs.input.blur()
this.showPopup = false
} else {
this.$refs.input.focus()
this.showPopup = true
}
},
hoverIcon (e) {
if (this.disabled) {
return
}
if (e.type === 'mouseenter' && this.text) {
this.showCloseIcon = true
}
if (e.type === 'mouseleave') {
this.showCloseIcon = false
}
},
clickIcon () {
if (this.disabled) {
return
}
if (this.showCloseIcon) {
this.$emit('input', '')
this.$emit('change', '')
} else {
this.togglePopup()
}
},
parseDate (str, fmt = 'yyyy-MM-dd') {
let isValid = true
const obj = { y: 0, M: 1, d: 0, H: 0, h: 0, m: 0, s: 0 }
fmt.replace(/([^yMdHhms]*?)(([yMdHhms])\3*)([^yMdHhms]*?)/g, function (m, $1, $2, $3, $4, idx, old) {
const rgs = new RegExp($1 + '(\\d{' + $2.length + '})' + $4)
const index = str.search(rgs)
if (index === -1) {
isValid = false
} else {
str = str.replace(rgs, function (_m, _$1) {
obj[$3] = parseInt(_$1);
return ''
});
}
return ''
});
if (!isValid) {
return false
}
obj.M--
return new Date(obj.y, obj.M, obj.d, obj.H || obj.h, obj.m, obj.s)
},
formatDate (date, fmt = 'yyyy-MM-dd HH:mm:ss') {
const hour = date.getHours()
const map = {
'M+': date.getMonth() + 1, // 月份
'[Dd]+': date.getDate(), // 日
'H+': hour, // 小时
'h+': (hour % 12) || 12, // 小时
'm+': date.getMinutes(), // 分
's+': date.getSeconds(), // 秒
'q+': Math.floor((date.getMonth() + 3) / 3), // 季度
S: date.getMilliseconds(), // 毫秒
'a': hour >= 12 ? 'pm' : 'am',
'A': hour >= 12 ? 'PM' : 'AM'
}
let str = fmt.replace(/[Yy]+/g, function (str) {
return ('' + date.getFullYear()).slice(4 - str.length)
})
Object.keys(map).forEach(key => {
str = str.replace(new RegExp(key), function (str) {
const value = '' + map[key]
return str.length === 1 ? value : ('00' + value).slice(value.length)
})
})
return str
},
stringify (date) {
if (typeof this.customFormatter === 'function') {
return this.customFormatter(new Date(date))
}
return this.formatDate(new Date(date), this.format)
},
isValidDate (date) {
return !!new Date(date).getTime()
},
isValidRange (date) {
return (
Array.isArray(date) &&
date.length === 2 &&
this.isValidDate(date[0]) &&
this.isValidDate(date[1])
)
},
selectRange (range) {
this.$emit('input', [range.start, range.end])
this.$emit('change', [range.start, range.end])
},
initRanges () {
if (Array.isArray(this.shortcuts)) {
this.ranges = this.shortcuts
} else if (this.shortcuts) {
this.ranges = [
{
text: '未来7天',
start: new Date(),
end: new Date(Date.now() + 3600 * 1000 * 24 * 7)
},
{
text: '未来30天',
start: new Date(),
end: new Date(Date.now() + 3600 * 1000 * 24 * 30)
},
{
text: '最近7天',
start: new Date(Date.now() - 3600 * 1000 * 24 * 7),
end: new Date()
},
{
text: '最近30天',
start: new Date(Date.now() - 3600 * 1000 * 24 * 30),
end: new Date()
}
]
this.ranges.forEach((v, i) => {
v.text = this.translation.pickers[i]
})
} else {
this.ranges = []
}
},
displayPopup () {
if (this.disabled) {
return
}
const dw = document.documentElement.clientWidth
const dh = document.documentElement.clientHeight
const InputRect = this.$el.getBoundingClientRect()
const PopupRect = this.$refs.calendar.getBoundingClientRect()
this.position = {}
if (
dw - InputRect.left < PopupRect.width &&
InputRect.right < PopupRect.width
) {
this.position.left = 1 - InputRect.left + 'px'
} else if (InputRect.left + InputRect.width / 2 <= dw / 2) {
this.position.left = 0
} else {
this.position.right = 0
}
if (
InputRect.top <= PopupRect.height + 1 &&
dh - InputRect.bottom <= PopupRect.height + 1
) {
this.position.top = dh - InputRect.top - PopupRect.height - 1 + 'px'
} else if (InputRect.top + InputRect.height / 2 <= dh / 2) {
this.position.top = '100%'
} else {
this.position.bottom = '100%'
}
}
},
created () {
this.initRanges()
},
directives: {
clickoutside: {
bind (el, binding, vnode) {
el['@clickoutside'] = e => {
if (
!el.contains(e.target) &&
binding.expression &&
vnode.context[binding.expression]
) {
binding.value()
}
}
document.addEventListener('click', el['@clickoutside'], true)
},
unbind (el) {
document.removeEventListener('click', el['@clickoutside'], true)
}
}
}
}
</script>
<style lang="scss">
.mx-datepicker {
position: relative;
display: inline-block;
color: #73879c;
font: 14px/1.5 'Helvetica Neue', Helvetica, Arial, 'Microsoft Yahei',
sans-serif;
* {
box-sizing: border-box;
}
&.disabled {
opacity: 0.7;
cursor: not-allowed;
}
}
.mx-datepicker-popup {
position: absolute;
width: 250px;
margin-top: 1px;
margin-bottom: 1px;
border: 1px solid #d9d9d9;
background-color: #fff;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
z-index: 1000;
&.range {
width: 496px;
}
}
.mx-input {
display: inline-block;
width: 100%;
height: 34px;
padding: 6px 30px 6px 10px;
font-size: 14px;
line-height: 1.4;
color: #555;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
&:disabled,
&.disabled {
opacity: 0.7;
cursor: not-allowed;
}
&:focus {
outline: none;
}
}
.mx-input-icon {
top: 0;
right: 0;
position: absolute;
width: 30px;
height: 100%;
color: #888;
text-align: center;
font-style: normal;
&::after {
content: '';
display: inline-block;
width: 0;
height: 100%;
vertical-align: middle;
}
}
.mx-input-icon__calendar {
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA00lEQVQ4T72SzQ2CQBCF54UGKIES6EAswQq0BS/A3PQ0hAt0oKVQgiVYAkcuZMwSMOyCyRKNe9uf+d6b2Qf6csGtL8sy7vu+Zebn/E5EoiAIwjRNH/PzBUBEGiJqmPniAMw+YeZkFSAiJwA3j45aVT0wsxGitwOjDGDnASBVvU4OLQARRURk9e4CAcSqWn8CLHp3Ae6MXAe/B4yzUeMkz/P9ZgdFUQzFIwD/B4yKgwMTos0OtvzCHcDRJ0gAzlmW1VYSq6oKu66LfQBTjC2AT+Hamxcml5IRpPq3VQAAAABJRU5ErkJggg==);
background-position: center;
background-repeat: no-repeat;
}
.mx-input-icon__close::before {
content: '\2716';
vertical-align: middle;
}
.mx-datepicker-top {
text-align: left;
padding: 0 12px;
line-height: 34px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
& > span {
white-space: nowrap;
cursor: pointer;
&:hover {
color: #1284e7;
}
&:after {
content: '|';
margin: 0 10px;
color: #48576a;
}
}
}
.mx-datepicker-footer {
padding: 4px;
clear: both;
text-align: right;
border-top: 1px solid rgba(0, 0, 0, 0.05);
}
.mx-datepicker-btn {
font-size: 12px;
line-height: 1;
padding: 7px 15px;
margin: 0 5px;
cursor: pointer;
background-color: transparent;
outline: none;
border: none;
border-radius: 3px;
}
.mx-datepicker-btn-confirm {
border: 1px solid rgba(0, 0, 0, 0.1);
color: #73879c;
&:hover {
color: #1284e7;
border-color: #1284e7;
}
}
</style>
-136
View File
@@ -1,136 +0,0 @@
<template>
<div id="app">
<div class="example">
<section class="demo">
<span class="label">default:</span>
<date-picker v-model="value1" lang="en" editable></date-picker>
</section>
<section class="demo">
<span class="label">range:</span>
<date-picker v-model="value2" range lang="en"></date-picker>
</section>
<pre class="pre">{{demo1}}</pre>
</div>
<div class="example">
<section class="demo">
<span class="label">datetime:</span>
<date-picker v-model="value3" lang="en" type="datetime" format="yyyy-MM-dd HH:mm:ss"></date-picker>
</section>
<section class="demo">
<span class="label">datetime with minute-step picker:</span>
<date-picker v-model="value4" lang="en" type="datetime" format="yyyy-MM-dd hh:mm:ss a"
:time-picker-options="{
start: '00:00',
step: '00:30',
end: '23:30'
}"></date-picker>
</section>
<section class="demo">
<span class="label">datetime range:</span>
<date-picker v-model="value5" range type="datetime" lang="en" format="yyyy-MM-dd HH:mm:ss"></date-picker>
</section>
<blockquote class="tips">
if you use the datetime, you should set the format which default is 'yyyy-MM-dd'
</blockquote>
<pre class="pre">{{demo2}}</pre>
</div>
<div class="example">
<section class="demo">
<span class="label">with confirm:</span>
<date-picker
v-model="value6"
format="yyyy-MM-dd"
lang="en"
confirm></date-picker>
</section>
<section class="demo">
<span class="label">datetime with confirm:</span>
<date-picker
v-model="value7"
type="datetime"
lang="en"
format="yyyy-MM-dd hh:mm:ss"
confirm></date-picker>
</section>
<section class="demo">
<span class="label">range with confirm:</span>
<date-picker
v-model="value8"
range
lang="en"
format="yyyy-MM-dd"
@confirm="confirm"
confirm></date-picker>
</section>
<blockquote class="tips">
Recommend to use the confirm option when the type is 'datetime' or range is true
</blockquote>
<pre class="pre">{{demo3}}</pre>
</div>
</div>
</template>
<script>
import DatePicker from '../index'
export default {
name: 'app',
components: { DatePicker },
data () {
return {
value1: new Date(),
value2: '',
value3: new Date(),
value4: '',
value5: '',
value6: '',
value7: '',
value8: '',
demo1: '<date-picker v-model="value1" editable lang="en"></date-picker>\n<date-picker v-model="value3" range lang="en"></date-picker>',
demo2: `<date-picker v-model="value3" type="datetime" format="yyyy-MM-dd HH:mm:ss" lang="en"></date-picker>\n<date-picker v-model="value4" type="datetime" format="yyyy-MM-dd hh:mm:ss a" :time-picker-options="{start: '00:00',step: '00:30',end: '23:30'}" lang="en"></date-picker>\n<date-picker v-model="value4" range type="datetime" format="yyyy-MM-dd HH:mm:ss" lang="en"></date-picker>`,
demo3: '<date-picker v-model="value6" format="yyyy-MM-dd" lang="en" confirm></date-picker>\n<date-picker v-model="value7" lang="en" type="datetime" format="yyyy-MM-dd hh:mm:ss" confirm></date-picker>\n<date-picker v-model="value8" lang="en" range format="yyyy-MM-dd" confirm></date-picker>'
}
},
methods: {
confirm (val) {
console.log(val)
}
}
}
</script>
<style>
.demo {
margin:20px;
}
.label{
display: inline-block;
margin-right: 1em;
}
.pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #f6f8fa;
border-radius: 3px;
}
.example {
padding: 45px;
word-wrap: break-word;
background-color: #fff;
border: 1px solid #ddd;
border-bottom-right-radius: 3px;
border-bottom-left-radius: 3px;
}
.example > .demo {
display: inline-block;
}
.tips {
margin: 0;
padding: 0 1em;
color: #6a737d;
border-left: 0.25em solid #dfe2e5;
}
</style>
+48
View File
@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>vue2-datepicker</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/github.min.css" rel="stylesheet" />
<style>
.example {
padding: 20px;
word-wrap: break-word;
background-color: #fff;
border: 1px solid #ddd;
border-bottom-right-radius: 3px;
border-bottom-left-radius: 3px;
}
.source {
display: inline-block;
margin:20px;
}
.label{
display: inline-block;
margin-right: 1em;
}
.pre {
padding: 8px;
overflow: auto;
font-size: 85%;
line-height: 1.4;
background-color: #f6f8fa;
border-radius: 3px;
}
.tips {
margin: 0;
padding: 0 1em;
color: #6a737d;
border-left: 0.25em solid #dfe2e5;
}
</style>
</head>
<body>
<div id="app"></div>
<script src="https://unpkg.com/vue@2.5.16/dist/vue.js"></script>
<script src="https://unpkg.com/vue2-datepicker/dist/build.js"></script>
<script src="./build.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script>
<script>window.hljs.initHighlightingOnLoad();</script>
</body>
</html>
+134
View File
@@ -0,0 +1,134 @@
import Vue from 'vue'
import DatePicker from '@/index'
Vue.use(DatePicker)
new Vue({ // eslint-disable-line
el: '#app',
data () {
return {
value1: new Date(),
value2: '',
value3: new Date(),
value4: '',
value5: '',
value6: '',
value7: '',
value8: '',
value9: ''
}
},
methods: {
getSource (obj) {
return Object.keys(obj).map(key => {
const value = obj[key]
return (
<section class="source">
<label class="label">{key} : </label>
{Vue.compile(value).render.call(this)}
</section>
)
})
},
getPre (obj) {
return Object.keys(obj).map(key => {
const value = obj[key].replace(/\n/g, '').replace(/\s+/g, ' ')
return (
<pre class="pre">
<code class="language-html">{value}</code>
</pre>
)
})
}
},
render (h) {
const example1 = {
'base': '<date-picker v-model="value1" lang="en" :not-before="new Date()"></date-picker>',
'range': '<date-picker v-model="value2" range lang="en"></date-picker>'
}
const example2 = {
'datetime': `
<date-picker
v-model="value3"
lang="en"
type="datetime"
format="YYYY-MM-DD HH:mm:ss"></date-picker>`,
'datetime with time-picker-options': `
<date-picker
v-model="value4"
lang="en"
type="datetime"
format="YYYY-MM-DD hh:mm:ss a"
:time-picker-options="{
start: '00:00',
step: '00:30',
end: '23:30'
}"></date-picker>`,
'datetime with minute-step': `
<date-picker
v-model="value9"
lang="en"
type="datetime"
format="YYYY-MM-DD hh:mm:ss a"
:minute-step="10"
></date-picker>`,
'datetime range': `
<date-picker
v-model="value5"
range
type="datetime"
lang="en"
format="YYYY-MM-DD HH:mm:ss"></date-picker>`
}
const example3 = {
'with confirm': `
<date-picker
v-model="value6"
format="YYYY-MM-DD"
lang="en"
confirm></date-picker>`,
'datetime with confirm': `
<date-picker
v-model="value7"
type="datetime"
lang="en"
format="YYYY-MM-DD hh:mm:ss"
confirm></date-picker>`,
'range width confirm': `
<date-picker
v-model="value8"
range
lang="en"
format="YYYY-MM-DD"
confirm></date-picker>`
}
const arr = [
{
exam: example1
},
{
exam: example2,
tips: 'if you use the datetime, you should set the format to "YYYY-MM-DD HH:mm:ss" which default is "YYY-MM-DD'
},
{
exam: example3,
tips: 'Recommend to use the confirm option when the type is "datetime" or "range" is true'
}
]
return (
<div id="app">
{arr.map(obj => (
<div class="example">
{this.getSource(obj.exam)}
{
obj.tips
? <blockquote class="tips">{obj.tips}</blockquote>
: ''
}
{this.getPre(obj.exam)}
</div>
))}
</div>
)
}
})
-7
View File
@@ -1,7 +0,0 @@
import Vue from 'vue'
import App from './App.vue'
new Vue({ // eslint-disable-line
el: '#app',
render: h => h(App)
})
-1
View File
File diff suppressed because one or more lines are too long
-11
View File
@@ -1,11 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>vue2-datepicker</title>
</head>
<body>
<div id="app"></div>
<script src="demo/build.js"></script>
</body>
</html>
+8313 -1299
View File
File diff suppressed because it is too large Load Diff
+67 -28
View File
@@ -1,12 +1,31 @@
{
"name": "vue2-datepicker",
"description": "A Datepicker Component For Vue2",
"main": "dist/build.js",
"main": "lib/index.js",
"files": [
"lib",
"src"
],
"version": "1.9.8",
"scripts": {
"dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot",
"demo": "cross-env NODE_ENV=production webpack --progress --hide-modules --config webpack.demo.config.js",
"deploy": "cross-env NODE_ENV=production webpack --progress --hide-modules --config webpack.deploy.config.js"
"dev": "cross-env NODE_ENV=development webpack-dev-server --hot --open --config build/webpack.dev.config.js",
"demo": "cross-env NODE_ENV=production webpack --progress --hide-modules --config build/webpack.demo.config.js",
"deploy": "cross-env NODE_ENV=production webpack --progress --hide-modules --config build/webpack.deploy.config.js",
"test:push": "jest --coverage --coverageReporters=text-lcov | coveralls",
"test": "jest"
},
"jest": {
"moduleFileExtensions": [
"js",
"vue"
],
"transform": {
"^.+\\.js$": "<rootDir>/node_modules/babel-jest",
".*\\.(vue)$": "<rootDir>/node_modules/vue-jest"
},
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/$1"
}
},
"repository": {
"type": "git",
@@ -23,34 +42,54 @@
},
"homepage": "https://github.com/mengxiong10/vue2-datepicker#readme",
"dependencies": {
"vue": "^2.2.1"
"fecha": "^2.3.3"
},
"devDependencies": {
"autoprefixer": "^7.1.6",
"babel-core": "^6.0.0",
"babel-eslint": "^8.0.2",
"babel-loader": "^7.0.0",
"babel-preset-env": "^1.6.0",
"@vue/test-utils": "^1.0.0-beta.18",
"autoprefixer": "^7.2.6",
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.3",
"babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-jest": "^23.0.1",
"babel-loader": "^7.1.4",
"babel-plugin-jsx-v-model": "^2.0.3",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-vue-jsx": "^3.7.0",
"babel-preset-env": "^1.7.0",
"babel-preset-stage-3": "^6.24.1",
"cross-env": "^5.0.0",
"coveralls": "^3.0.1",
"cross-env": "^5.1.6",
"css-loader": "^0.25.0",
"eslint": "^4.12.0",
"eslint-config-standard": "^10.2.1",
"cz-conventional-changelog": "^2.1.0",
"eslint": "^4.19.1",
"eslint-config-standard": "^11.0.0",
"eslint-friendly-formatter": "^3.0.0",
"eslint-loader": "^1.9.0",
"eslint-plugin-html": "^4.0.1",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-node": "^5.2.1",
"eslint-plugin-promise": "^3.6.0",
"eslint-plugin-standard": "^3.0.1",
"file-loader": "^0.9.0",
"node-sass": "^4.7.2",
"postcss-loader": "^2.0.9",
"sass-loader": "^6.0.6",
"vue-loader": "^13.0.5",
"vue-template-compiler": "^2.2.1",
"webpack": "^2.2.0",
"webpack-dev-server": "^2.2.0",
"webpack-merge": "^4.1.1"
"eslint-loader": "^2.0.0",
"eslint-plugin-import": "^2.12.0",
"eslint-plugin-node": "^6.0.1",
"eslint-plugin-promise": "^3.8.0",
"eslint-plugin-standard": "^3.1.0",
"eslint-plugin-vue": "^4.5.0",
"file-loader": "^1.1.11",
"highlight.js": "^9.12.0",
"jest": "^23.0.1",
"mini-css-extract-plugin": "^0.4.0",
"node-sass": "^4.9.0",
"optimize-css-assets-webpack-plugin": "^4.0.2",
"postcss-loader": "^2.1.5",
"sass-loader": "^6.0.7",
"vue": "^2.5.16",
"vue-jest": "^2.6.0",
"vue-loader": "^15.2.1",
"vue-template-compiler": "^2.5.16",
"webpack": "^4.9.1",
"webpack-cli": "^2.1.4",
"webpack-dev-server": "^3.1.4",
"webpack-merge": "^4.1.2"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
}
+302
View File
@@ -0,0 +1,302 @@
<template>
<div class="mx-calendar">
<div class="mx-calendar-header">
<a
v-show="panel !== 'TIME'"
class="mx-icon-last-year"
@click="handleIconYear(-1)">&laquo;</a>
<a
v-show="panel === 'DATE'"
class="mx-icon-last-month"
@click="handleIconMonth(-1)">&lsaquo;</a>
<a
v-show="panel !== 'TIME'"
class="mx-icon-next-year"
@click="handleIconYear(1)">&raquo;</a>
<a
v-show="panel === 'DATE'"
class="mx-icon-next-month"
@click="handleIconMonth(1)">&rsaquo;</a>
<a
v-show="panel !== 'TIME'"
class="mx-current-month"
@click="handleBtnMonth">{{months[calendarMonth]}}</a>
<a
v-show="panel !== 'TIME'"
class="mx-current-year"
@click="handleBtnYear">{{calendarYear}}</a>
<a
v-show="panel === 'TIME'"
class="mx-time-header"
@click="showPanelDate">{{timeHeader}}</a>
</div>
<div class="mx-calendar-content">
<panel-date
v-show="panel === 'DATE'"
:value="value"
:calendar-month="calendarMonth"
:calendar-year="calendarYear"
:start-at="startAt"
:end-at="endAt"
:first-day-of-week="firstDayOfWeek"
:disabled-date="isDisabledDate"
@select="selectDate"/>
<panel-year
v-show="panel === 'YEAR'"
:value="value"
:first-year="firstYear"
@select="selectYear" />
<panel-month
v-show="panel === 'MONTH'"
:value="value"
:calendar-year="calendarYear"
@select="selectMonth" />
<panel-time
v-show="panel === 'TIME'"
:minute-step="minuteStep"
:time-picker-options="timePickerOptions"
:value="value"
:disabled-time="isDisabledTime"
@select="selectTime" />
</div>
</div>
</template>
<script>
import { isValidDate, isDateObejct } from '@/utils/index'
import { t } from '@/locale/index'
import scrollIntoView from '@/utils/scroll-into-view'
import PanelDate from '@/panel/date'
import PanelYear from '@/panel/year'
import PanelMonth from '@/panel/month'
import PanelTime from '@/panel/time'
export default {
name: 'CalendarPanel',
components: { PanelDate, PanelYear, PanelMonth, PanelTime },
props: {
value: {
default: null,
validator: function (val) {
return val === null || isValidDate(val)
}
},
startAt: null,
endAt: null,
visible: {
type: Boolean,
default: false
},
// below user set
type: {
type: String,
default: 'date' // ['date', 'datetime']
},
firstDayOfWeek: {
default: 7,
type: Number,
validator: val => val >= 1 && val <= 7
},
notBefore: {
default: null,
validator: function (val) {
return !val || isValidDate(val)
}
},
notAfter: {
default: null,
validator: function (val) {
return !val || isValidDate(val)
}
},
disabledDays: {
type: [Array, Function],
default: function () {
return []
}
},
minuteStep: {
type: Number,
default: 0,
validator: val => val >= 0 && val <= 60
},
timePickerOptions: {
type: [Object, Function],
default () {
return null
}
}
},
data () {
const now = new Date()
const calendarYear = now.getFullYear()
const calendarMonth = now.getMonth()
const firstYear = Math.floor(calendarYear / 10) * 10
const months = t('months')
return {
panel: 'DATE',
dates: [],
months,
calendarMonth,
calendarYear,
firstYear
}
},
computed: {
now: {
get () {
return new Date(this.calendarYear, this.calendarMonth).getTime()
},
set (val) {
const now = new Date(val)
this.calendarYear = now.getFullYear()
this.calendarMonth = now.getMonth()
}
},
timeHeader () {
return this.value && new Date(this.value).toLocaleDateString()
}
},
watch: {
value: {
immediate: true,
handler: 'updateNow'
},
visible: {
immediate: true,
handler: 'init'
},
panel: {
immediate: true,
handler: 'handelPanelChange'
}
},
methods: {
handelPanelChange (panel) {
if (panel === 'YEAR') {
this.firstYear = Math.floor(this.calendarYear / 10) * 10
} else if (panel === 'TIME') {
this.$nextTick(() => {
[...this.$el.querySelectorAll('.mx-panel-time .mx-time-list')].forEach(el => {
scrollIntoView(el, el.querySelector('.actived'))
})
})
}
},
init () {
this.panel = 'DATE'
this.updateNow(this.value)
},
// 根据value更新日历
updateNow (value) {
this.now = value ? new Date(value) : new Date()
},
isDisabledTime (date, startAt, endAt) {
const time = new Date(date).getTime()
const notBefore = this.notBefore && (time < new Date(this.notBefore))
const notAfter = this.notAfter && (time > new Date(this.notAfter))
startAt = startAt === undefined ? this.startAt : startAt
startAt = startAt && (time < new Date(startAt))
endAt = endAt === undefined ? this.endAt : endAt
endAt = endAt && (time > new Date(endAt))
return notBefore || notAfter || startAt || endAt
},
isDisabledDate (date, startAt, endAt) {
const time = new Date(date).getTime()
const notBefore = this.notBefore && (time < new Date(this.notBefore).setHours(0, 0, 0, 0))
const notAfter = this.notAfter && (time > new Date(this.notAfter).setHours(0, 0, 0, 0))
startAt = startAt === undefined ? this.startAt : startAt
startAt = startAt && (time < new Date(startAt).setHours(0, 0, 0, 0))
endAt = endAt === undefined ? this.endAt : endAt
endAt = endAt && (time > new Date(endAt).setHours(0, 0, 0, 0))
let disabledDays = false
if (Array.isArray(this.disabledDays)) {
disabledDays = this.disabledDays.some(v => new Date(v).setHours(0, 0, 0, 0) === time)
} else if (typeof this.disabledDays === 'function') {
disabledDays = this.disabledDays(new Date(date))
}
return notBefore || notAfter || disabledDays || startAt || endAt
},
selectDate (date) {
if (this.type === 'datetime') {
let time = new Date(date)
if (isDateObejct(this.value)) {
time.setHours(
this.value.getHours(),
this.value.getMinutes(),
this.value.getSeconds()
)
}
if (this.isDisabledTime(time)) {
time.setHours(0, 0, 0, 0)
if (this.notBefore && time.getTime() < new Date(this.notBefore).getTime()) {
time = new Date(this.notBefore)
}
if (this.startAt && time.getTime() < new Date(this.startAt).getTime()) {
time = new Date(this.startAt)
}
}
this.$emit('select-time', time)
this.panel = 'TIME'
return
}
this.$emit('select-date', date)
},
selectYear (year) {
this.changeCalendarYear(year)
this.showPanelMonth()
},
selectMonth (month) {
this.changeCalendarMonth(month)
this.showPanelDate()
},
selectTime (time) {
this.$emit('select-time', time)
},
changeCalendarYear (year) {
this.now = new Date(year, this.calendarMonth)
},
changeCalendarMonth (month) {
this.now = new Date(this.calendarYear, month)
},
handleIconMonth (flag) {
this.changeCalendarMonth(this.calendarMonth + flag)
},
handleIconYear (flag) {
if (this.panel === 'YEAR') {
this.changePanelYears(flag)
} else {
this.changeCalendarYear(this.calendarYear + flag)
}
},
handleBtnYear () {
if (this.panel === 'YEAR') {
this.showPanelDate()
} else {
this.showPanelYear()
}
},
handleBtnMonth () {
if (this.panel === 'MONTH') {
this.showPanelDate()
} else {
this.showPanelMonth()
}
},
changePanelYears (flag) {
this.firstYear = this.firstYear + flag * 10
},
showPanelDate () {
this.panel = 'DATE'
},
showPanelYear () {
this.panel = 'YEAR'
},
showPanelMonth () {
this.panel = 'MONTH'
}
}
}
</script>
+18
View File
@@ -0,0 +1,18 @@
export default {
bind (el, binding, vnode) {
el['@clickoutside'] = e => {
if (
!el.contains(e.target) &&
binding.expression &&
vnode.context[binding.expression]
) {
binding.value()
}
}
document.addEventListener('click', el['@clickoutside'], true)
},
unbind (el) {
document.removeEventListener('click', el['@clickoutside'], true)
}
}
+2 -1
View File
@@ -1,4 +1,5 @@
import DatePicker from './datepicker/index.vue'
import DatePicker from './index.vue'
import './index.scss'
DatePicker.install = function (Vue) {
Vue.component(DatePicker.name, DatePicker)
+319
View File
@@ -0,0 +1,319 @@
$default-color: #73879c;
$primary-color: #1284e7;
.mx-datepicker {
position: relative;
display: inline-block;
width: 210px;
color: $default-color;
font: 14px/1.5 'Helvetica Neue', Helvetica, Arial, 'Microsoft Yahei', sans-serif;
* {
box-sizing: border-box;
}
&.disabled {
opacity: 0.7;
cursor: not-allowed;
}
}
.mx-datepicker-range {
width: 320px;
}
.mx-datepicker-popup {
position: absolute;
margin-top: 1px;
margin-bottom: 1px;
border: 1px solid #d9d9d9;
background-color: #fff;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
z-index: 1000;
}
.mx-input-wrapper {
position: relative;
.mx-clear-wrapper {
display: none;
}
&:hover {
.mx-clear-wrapper {
display: block;
}
}
}
.mx-input {
display: inline-block;
width: 100%;
height: 34px;
padding: 6px 30px;
padding-left: 10px;
font-size: 14px;
line-height: 1.4;
color: #555;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
&:disabled,
&.disabled {
opacity: 0.7;
cursor: not-allowed;
}
&:focus {
outline: none;
}
}
.mx-input-append {
position: absolute;
top: 0;
right: 0;
width: 30px;
height: 100%;
padding: 6px;
background-color: #fff;
background-clip: content-box;
}
.mx-input-icon {
display: inline-block;
width: 100%;
height: 100%;
font-style: normal;
color: #555;
text-align: center;
cursor: pointer;
}
.mx-calendar-icon {
width: 100%;
height: 100%;
color: #555;
stroke-width: 8px;
stroke: currentColor;
fill: currentColor;
}
.mx-clear-icon {
&::before {
display: inline-block;
content: '\2716';
vertical-align: middle;
}
&::after {
content: '';
display: inline-block;
width: 0;
height: 100%;
vertical-align: middle;
}
}
.mx-range-wrapper {
width: 248px * 2;
overflow: hidden;
}
.mx-shortcuts-wrapper {
text-align: left;
padding: 0 12px;
line-height: 34px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
.mx-shortcuts {
white-space: nowrap;
cursor: pointer;
&:hover {
color: mix(#fff, $primary-color, 20%);
}
&:after {
content: '|';
margin: 0 10px;
color: #48576a;
}
}
}
.mx-datepicker-footer {
padding: 4px;
clear: both;
text-align: right;
border-top: 1px solid rgba(0, 0, 0, 0.05);
}
.mx-datepicker-btn {
font-size: 12px;
line-height: 1;
padding: 7px 15px;
margin: 0 5px;
cursor: pointer;
background-color: transparent;
outline: none;
border: none;
border-radius: 3px;
}
.mx-datepicker-btn-confirm {
border: 1px solid rgba(0, 0, 0, 0.1);
color: #73879c;
&:hover {
color: #1284e7;
border-color: #1284e7;
}
}
/* 日历组件 */
.mx-calendar {
float: left;
color: $default-color;
padding: 6px 12px;
font: 14px/1.5 Helvetica Neue,Helvetica,Arial,Microsoft Yahei,sans-serif;
* {
box-sizing: border-box;
}
}
.mx-calendar-header {
padding: 0 4px;
height: 34px;
line-height: 34px;
text-align: center;
overflow: hidden;
> a {
color: inherit;
text-decoration: none;
cursor: pointer;
&:hover {
color: mix(#fff, $primary-color, 20%);
}
}
@at-root {
.mx-icon-last-month,
.mx-icon-next-month {
padding: 0 6px;
font-size: 20px;
line-height: 30px;
}
.mx-icon-last-month {
float: left;
}
.mx-icon-next-month {
float: right;
}
.mx-icon-last-year {
@extend .mx-icon-last-month
}
.mx-icon-next-year {
@extend .mx-icon-next-month
}
}
}
.mx-calendar-content {
width: 32px * 7;
height: 32px * 7;
.cell {
cursor: pointer;
&:hover {
background-color: #eaf8fe;
}
&.actived {
color: #fff;
background-color: $primary-color;
}
&.inrange {
background-color: #eaf8fe;
}
&.disabled {
cursor: not-allowed;
color: #ccc;
background-color: #f3f3f3;
}
}
}
.mx-panel {
width: 100%;
height: 100%;
text-align: center;
}
.mx-panel-date {
table-layout: fixed;
border-collapse: collapse;
border-spacing: 0;
td, th {
font-size: 12px;
width: 32px;
height: 32px;
padding: 0;
overflow: hidden;
text-align: center;
}
td {
&.today {
color: mix(#fff, $primary-color, 10%);
}
&.last-month,
&.next-month {
color: #ddd;
}
}
}
.mx-panel-year {
padding: 7px 0;
.cell {
display: inline-block;
width: 40%;
margin: 1px 5%;
line-height: 40px;
}
}
.mx-panel-month {
.cell {
display: inline-block;
width: 30%;
line-height: 40px;
margin: 8px 1.5%;
}
}
.mx-time-list {
position: relative; // 定位 offsetParent
float: left;
margin: 0;
padding: 0;
list-style: none;
width: 100%;
height: 100%;
border-top: 1px solid rgba(0,0,0,.05);
border-left: 1px solid rgba(0,0,0,.05);
overflow-y: auto;
.mx-time-picker-item {
display: block;
text-align: left;
padding-left: 10px;
}
&:first-child {
border-left: 0;
}
.cell {
width: 100%;
font-size: 12px;
height: 30px;
line-height: 30px;
}
/* 滚动条滑块 */
&::-webkit-scrollbar {
width: 8px;
height: 8px;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.05);
border-radius: 10px;
box-shadow: inset 1px 1px 0 rgba(0, 0, 0, 0.1);
}
&:hover::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.2);
}
}
+409
View File
@@ -0,0 +1,409 @@
<template>
<div
class="mx-datepicker"
:class="{
'mx-datepicker-range': range,
'disabled': disabled
}"
:style="{
'width': computedWidth
}"
v-clickoutside="closePopup">
<div class="mx-input-wrapper"
@click="showPopup">
<input
:class="inputClass"
ref="input"
type="text"
:name="inputName"
:disabled="disabled"
:readonly="!editable"
:value="text"
:placeholder="innerPlaceholder"
@input="handleInput"
@change="handleChange">
<span class="mx-input-append">
<slot name="calendar-icon">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 200 200" class="mx-calendar-icon">
<rect x="13" y="29" rx="14" ry="14" width="174" height="158" fill="transparent" />
<line x1="46" x2="46" y1="8" y2="50" />
<line x1="154" x2="154" y1="8" y2="50" />
<line x1="13" x2="187" y1="70" y2="70" />
<text x="50%" y="135" font-size="90" stroke-width="1" text-anchor="middle" dominant-baseline="middle">{{new Date().getDate()}}</text>
</svg>
</slot>
</span>
<span
v-if="showClearIcon"
class="mx-input-append mx-clear-wrapper"
@click.stop="clearDate">
<slot name="mx-clear-icon">
<i class="mx-input-icon mx-clear-icon"></i>
</slot>
</span>
</div>
<div class="mx-datepicker-popup"
:style="position"
v-show="popupVisible"
ref="calendar">
<slot name="header">
<div class="mx-shortcuts-wrapper"
v-if="range && innnerShortcuts.length">
<span
class="mx-shortcuts"
v-for="(range, index) in innnerShortcuts"
:key="index"
@click="selectRange(range)">{{range.text}}</span>
</div>
</slot>
<calendar-panel
v-if="!range"
v-bind="$attrs"
:value="currentValue"
:visible="popupVisible"
@select-date="selectDate"
@select-time="selectTime"></calendar-panel>
<div class="mx-range-wrapper"
v-else>
<calendar-panel
style="box-shadow:1px 0 rgba(0, 0, 0, .1)"
v-bind="$attrs"
:value="currentValue[0]"
:end-at="currentValue[1]"
:start-at="null"
:visible="popupVisible"
@select-date="selectStartDate"
@select-time="selectStartTime"></calendar-panel>
<calendar-panel
v-bind="$attrs"
:value="currentValue[1]"
:start-at="currentValue[0]"
:end-at="null"
:visible="popupVisible"
@select-date="selectEndDate"
@select-time="selectEndTime"></calendar-panel>
</div>
<slot name="footer">
<div class="mx-datepicker-footer"
v-if="confirm">
<button type="button"
class="mx-datepicker-btn mx-datepicker-btn-confirm"
@click="confirmDate">{{ confirmText }}</button>
</div>
</slot>
</div>
</div>
</template>
<script>
import fecha from 'fecha'
import clickoutside from '@/directives/clickoutside'
import { isValidDate, isValidRange, isDateObejct } from '@/utils/index'
import { use, t } from '@/locale/index'
import CalendarPanel from './calendar.vue'
export default {
name: 'DatePicker',
components: { CalendarPanel },
directives: {
clickoutside
},
props: {
value: null,
placeholder: {
type: String,
default: null
},
lang: {
type: [String, Object],
default: 'zh'
},
format: {
type: String,
default: 'YYYY-MM-DD'
},
range: {
type: Boolean,
default: false
},
rangeSeparator: {
type: String,
default: '~'
},
width: {
type: [String, Number],
default: null
},
confirmText: {
type: String,
default: 'OK'
},
confirm: {
type: Boolean,
default: false
},
editable: {
type: Boolean,
default: true
},
disabled: {
type: Boolean,
default: false
},
clearable: {
type: Boolean,
default: true
},
shortcuts: {
type: [Boolean, Array],
default: true
},
inputName: {
type: String,
default: 'date'
},
inputClass: {
type: [String, Array],
default: 'mx-input'
}
},
data () {
return {
currentValue: this.range ? [null, null] : null,
userInput: null,
popupVisible: false,
position: {}
}
},
watch: {
value: {
immediate: true,
handler: 'handleValueChange'
},
popupVisible (val) {
if (val) {
this.initCalendar()
} else {
this.userInput = null
}
}
},
computed: {
innerPlaceholder () {
if (typeof this.placeholder === 'string') {
return this.placeholder
}
return this.range ? t('placeholder.dateRange') : t('placeholder.date')
},
text () {
if (this.userInput !== null) {
return this.userInput
}
if (!this.range) {
return isValidDate(this.value) ? this.stringify(this.value) : ''
}
return isValidRange(this.value)
? `${this.stringify(this.value[0])} ${this.rangeSeparator} ${this.stringify(this.value[1])}`
: ''
},
computedWidth () {
if (typeof this.width === 'number' || (typeof this.width === 'string' && /^\d+$/.test(this.width))) {
return this.width + 'px'
}
return this.width
},
showClearIcon () {
return !this.disabled && this.clearable && (this.range ? isValidRange(this.value) : isValidDate(this.value))
},
innnerShortcuts () {
if (Array.isArray(this.shortcuts)) {
return this.shortcuts
}
if (this.shortcuts === false) {
return []
}
const pickers = t('pickers')
const arr = [
{
text: pickers[0],
start: new Date(),
end: new Date(Date.now() + 3600 * 1000 * 24 * 7)
},
{
text: pickers[1],
start: new Date(),
end: new Date(Date.now() + 3600 * 1000 * 24 * 30)
},
{
text: pickers[2],
start: new Date(Date.now() - 3600 * 1000 * 24 * 7),
end: new Date()
},
{
text: pickers[3],
start: new Date(Date.now() - 3600 * 1000 * 24 * 30),
end: new Date()
}
]
return arr
}
},
created () {
use(this.lang)
},
methods: {
initCalendar () {
this.handleValueChange(this.value)
this.displayPopup()
},
stringify (date, format) {
format = format || this.format
return fecha.format(date, format)
},
parseDate (value, format) {
try {
format = format || this.format
return fecha.parse(value, format)
} catch (e) {
return false
}
},
dateEqual (a, b) {
return isDateObejct(a) && isDateObejct(b) && a.getTime() === b.getTime()
},
rangeEqual (a, b) {
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, index) => this.dateEqual(item, b[index]))
},
selectRange (range) {
this.currentValue = [ new Date(range.start), new Date(range.end) ]
this.updateDate(true)
},
clearDate () {
const date = this.range ? [null, null] : null
this.currentValue = date
this.updateDate(true)
},
confirmDate () {
const valid = this.range ? isValidRange(this.currentValue) : isValidDate(this.currentValue)
if (valid) {
this.updateDate(true)
}
this.$emit('confirm', this.currentValue)
this.closePopup()
},
updateDate (confirm = false) {
if ((this.confirm && !confirm) || this.disabled) {
return false
}
const equal = this.range ? this.rangeEqual(this.value, this.currentValue) : this.dateEqual(this.value, this.currentValue)
if (equal) {
return false
}
this.$emit('input', this.currentValue)
this.$emit('change', this.currentValue)
return true
},
handleValueChange (value) {
if (!this.range) {
this.currentValue = isValidDate(value) ? new Date(value) : null
} else {
this.currentValue = isValidRange(value) ? [new Date(value[0]), new Date(value[1])] : [null, null]
}
},
selectDate (date) {
this.currentValue = date
this.updateDate() && this.closePopup()
},
selectStartDate (date) {
this.$set(this.currentValue, 0, date)
if (this.currentValue[1]) {
this.updateDate()
}
},
selectEndDate (date) {
this.$set(this.currentValue, 1, date)
if (this.currentValue[0]) {
this.updateDate()
}
},
selectTime (time) {
this.currentValue = time
this.updateDate()
},
selectStartTime (time) {
this.selectStartDate(time)
},
selectEndTime (time) {
this.selectEndDate(time)
},
showPopup () {
if (this.disabled) {
return
}
this.popupVisible = true
},
closePopup () {
this.popupVisible = false
},
displayPopup () {
const dw = document.documentElement.clientWidth
const dh = document.documentElement.clientHeight
const InputRect = this.$el.getBoundingClientRect()
const PopupRect = this.$refs.calendar.getBoundingClientRect()
this.position = {}
if (
dw - InputRect.left < PopupRect.width &&
InputRect.right < PopupRect.width
) {
this.position.left = 1 - InputRect.left + 'px'
} else if (InputRect.left + InputRect.width / 2 <= dw / 2) {
this.position.left = 0
} else {
this.position.right = 0
}
if (
InputRect.top <= PopupRect.height + 1 &&
dh - InputRect.bottom <= PopupRect.height + 1
) {
this.position.top = dh - InputRect.top - PopupRect.height - 1 + 'px'
} else if (InputRect.top + InputRect.height / 2 <= dh / 2) {
this.position.top = '100%'
} else {
this.position.bottom = '100%'
}
},
handleInput (event) {
this.userInput = event.target.value
},
handleChange (event) {
const value = event.target.value
if (this.editable && this.userInput !== null) {
const calendar = this.$children[0]
const checkDate = calendar.type === 'date' ? calendar.isDisabledDate : calendar.isDisabledTime
if (this.range) {
const range = value.split(` ${this.rangeSeparator} `)
if (range.length === 2) {
const start = this.parseDate(range[0], this.format)
const end = this.parseDate(range[1], this.format)
if (start && end && !checkDate(start, null, end) && !checkDate(end, start, null)) {
this.currentValue = [ start, end ]
this.updateDate(true)
this.closePopup()
return
}
}
} else {
const date = this.parseDate(value, this.format)
if (date && !checkDate(date, null, null)) {
this.currentValue = date
this.updateDate(true)
this.closePopup()
return
}
}
this.$emit('input-error', value)
}
}
}
}
</script>
+30
View File
@@ -0,0 +1,30 @@
import Languages from './languages'
import { isPlainObject } from '@/utils/index'
let lang = 'zh'
export function use (target) {
if (isPlainObject(target)) {
lang = { ...Languages.en, ...target }
} else {
lang = Languages[target] || Languages.en
}
}
export function t (path) {
const arr = path.split('.')
let current = lang
let value
for (let i = 0, len = arr.length; i < len; i++) {
const prop = arr[i]
value = current[prop]
if (i === len - 1) {
return value
}
if (!value) {
return ''
}
current = value
}
return ''
}
+140
View File
@@ -0,0 +1,140 @@
import { t } from '@/locale/index'
export default {
name: 'panelDate',
props: {
value: null,
startAt: null,
endAt: null,
calendarMonth: {
default: new Date().getMonth()
},
calendarYear: {
default: new Date().getFullYear()
},
firstDayOfWeek: {
default: 7,
type: Number,
validator: val => val >= 1 && val <= 7
},
disabledDate: {
type: Function,
default: () => {
return false
}
}
},
methods: {
selectDate ({ year, month, day }) {
const date = new Date(year, month, day)
if (this.disabledDate(date)) {
return
}
this.$emit('select', date)
},
getDays (firstDayOfWeek) {
const days = t('days')
const firstday = parseInt(firstDayOfWeek, 10)
return days.concat(days).slice(firstday, firstday + 7)
},
getDates (year, month, firstDayOfWeek) {
const arr = []
const time = new Date(year, month)
time.setDate(0) // 把时间切换到上个月最后一天
const lastMonthLength = (time.getDay() + 7 - firstDayOfWeek) % 7 + 1 // time.getDay() 0是星期天, 1是星期一 ...
const lastMonthfirst = time.getDate() - (lastMonthLength - 1)
for (let i = 0; i < lastMonthLength; i++) {
arr.push({ year, month: month - 1, day: lastMonthfirst + i })
}
time.setMonth(time.getMonth() + 2, 0) // 切换到这个月最后一天
const curMonthLength = time.getDate()
for (let i = 0; i < curMonthLength; i++) {
arr.push({ year, month, day: 1 + i })
}
time.setMonth(time.getMonth() + 1, 1) // 切换到下个月第一天
const nextMonthLength = 42 - (lastMonthLength + curMonthLength)
for (let i = 0; i < nextMonthLength; i++) {
arr.push({ year, month: month + 1, day: 1 + i })
}
return arr
},
getCellClasses ({ year, month, day }) {
const classes = []
const cellTime = new Date(year, month, day).getTime()
const today = new Date().setHours(0, 0, 0, 0)
const curTime = this.value && new Date(this.value).setHours(0, 0, 0, 0)
const startTime = this.startAt && new Date(this.startAt).setHours(0, 0, 0, 0)
const endTime = this.endAt && new Date(this.endAt).setHours(0, 0, 0, 0)
if (month < this.calendarMonth) {
classes.push('last-month')
} else if (month > this.calendarMonth) {
classes.push('next-month')
} else {
classes.push('cur-month')
}
if (cellTime === today) {
classes.push('today')
}
if (this.disabledDate(cellTime)) {
classes.push('disabled')
}
if (curTime) {
if (cellTime === curTime) {
classes.push('actived')
} else if (startTime && cellTime <= curTime) {
classes.push('inrange')
} else if (endTime && cellTime >= curTime) {
classes.push('inrange')
}
}
return classes
},
getCellTitle ({ year, month, day }) {
return new Date(year, month, day).toLocaleDateString()
}
},
render (h) {
const ths = this.getDays(this.firstDayOfWeek).map(day => {
return <th>{day}</th>
})
const dates = this.getDates(this.calendarYear, this.calendarMonth, this.firstDayOfWeek)
const tbody = Array.apply(null, { length: 6 }).map((week, i) => {
const tds = dates.slice(7 * i, 7 * i + 7).map(date => {
const attrs = {
class: this.getCellClasses(date)
}
return (
<td
class="cell"
{...attrs}
title={this.getCellTitle(date)}
onClick={this.selectDate.bind(this, date)}>
{date.day}
</td>
)
})
return <tr>{tds}</tr>
})
return (
<table class="mx-panel mx-panel-date">
<thead>
<tr>{ths}</tr>
</thead>
<tbody>
{tbody}
</tbody>
</table>
)
}
}
+32
View File
@@ -0,0 +1,32 @@
import { t } from '@/locale/index'
export default {
name: 'panelMonth',
props: {
value: null,
calendarYear: {
default: new Date().getFullYear()
}
},
methods: {
selectMonth (month) {
this.$emit('select', month)
}
},
render (h) {
let months = t('months')
const currentYear = this.value && new Date(this.value).getFullYear()
const currentMonth = this.value && new Date(this.value).getMonth()
months = months.map((v, i) => {
return <span
class={{
'cell': true,
'actived': currentYear === this.calendarYear && currentMonth === i
}}
onClick={this.selectMonth.bind(this, i)}>
{v}
</span>
})
return <div class="mx-panel mx-panel-month">{months}</div>
}
}
+162
View File
@@ -0,0 +1,162 @@
import { formatTime, parseTime } from '@/utils/index'
export default {
name: 'panelTime',
props: {
timePickerOptions: {
type: [Object, Function],
default () {
return null
}
},
minuteStep: {
type: Number,
default: 0,
validator: val => val >= 0 && val <= 60
},
value: null,
disabledTime: Function
},
computed: {
currentHours () {
return new Date(this.value).getHours()
},
currentMinutes () {
return new Date(this.value).getMinutes()
},
currentSeconds () {
return new Date(this.value).getSeconds()
}
},
methods: {
stringifyText (value) {
return ('00' + value).slice(String(value).length)
},
selectTime (time) {
if (typeof this.disabledTime === 'function' && this.disabledTime(time)) {
return
}
this.$emit('select', new Date(time))
},
getTimeSelectOptions () {
const result = []
const options = this.timePickerOptions
if (!options) {
return []
}
if (typeof options === 'function') {
return options() || []
}
const start = parseTime(options.start)
const end = parseTime(options.end)
const step = parseTime(options.step)
if (start && end && step) {
const startMinutes = start.minutes + start.hours * 60
const endMinutes = end.minutes + end.hours * 60
const stepMinutes = step.minutes + step.hours * 60
const len = Math.floor((endMinutes - startMinutes) / stepMinutes)
for (let i = 0; i <= len; i++) {
let timeMinutes = startMinutes + i * stepMinutes
let hours = Math.floor(timeMinutes / 60)
let minutes = timeMinutes % 60
let value = {
hours, minutes
}
result.push({
value,
label: formatTime(value)
})
}
}
return result
}
},
render (h) {
const date = new Date(this.value)
const disabledTime = typeof this.disabledTime === 'function' && this.disabledTime
let pickers = this.getTimeSelectOptions()
if (Array.isArray(pickers) && pickers.length) {
pickers = pickers.map(picker => {
const pickHours = picker.value.hours
const pickMinutes = picker.value.minutes
const time = new Date(date).setHours(pickHours, pickMinutes, 0)
return (
<li
class={{
'mx-time-picker-item': true,
'cell': true,
'actived': pickHours === this.currentHours && pickMinutes === this.currentMinutes,
'disabled': disabledTime && disabledTime(time)
}}
onClick={this.selectTime.bind(this, time)}>{picker.label}</li>
)
})
return (
<div class="mx-panel mx-panel-time">
<ul class="mx-time-list">
{pickers}
</ul>
</div>
)
}
const hours = Array.apply(null, { length: 24 }).map((_, i) => {
const time = new Date(date).setHours(i)
return <li
class={{
'cell': true,
'actived': i === this.currentHours,
'disabled': disabledTime && disabledTime(time)
}}
onClick={this.selectTime.bind(this, time)}
>{this.stringifyText(i)}</li>
})
const step = this.minuteStep || 1
const length = parseInt(60 / step)
const minutes = Array.apply(null, { length }).map((_, i) => {
const value = i * step
const time = new Date(date).setMinutes(value)
return <li
class={{
'cell': true,
'actived': value === this.currentMinutes,
'disabled': disabledTime && disabledTime(time)
}}
onClick={this.selectTime.bind(this, time)}
>{this.stringifyText(value)}</li>
})
const seconds = Array.apply(null, { length: 60 }).map((_, i) => {
const time = new Date(date).setSeconds(i)
return <li
class={{
'cell': true,
'actived': i === this.currentSeconds,
'disabled': disabledTime && disabledTime(time)
}}
onClick={this.selectTime.bind(this, time)}
>{this.stringifyText(i)}</li>
})
let times = [hours, minutes]
if (this.minuteStep === 0) {
times.push(seconds)
}
times = times.map(list => (
<ul class="mx-time-list"
style={{ width: 100 / times.length + '%' }}>
{list}
</ul>
))
return (
<div class="mx-panel mx-panel-time">
{times}
</div>
)
}
}
+28
View File
@@ -0,0 +1,28 @@
export default {
name: 'panelYear',
props: {
value: null,
firstYear: Number
},
methods: {
selectYear (year) {
this.$emit('select', year)
}
},
render (h) {
// 当前年代
const firstYear = Math.floor(this.firstYear / 10) * 10
const currentYear = this.value && new Date(this.value).getFullYear()
const years = Array.apply(null, { length: 10 }).map((_, i) => {
const year = firstYear + i
return <span
class={{
'cell': true,
'actived': currentYear === year
}}
onClick={this.selectYear.bind(this, year)}
>{year}</span>
})
return <div class="mx-panel mx-panel-year">{years}</div>
}
}
+49
View File
@@ -0,0 +1,49 @@
export function isPlainObject (obj) {
return Object.prototype.toString.call(obj) === '[object Object]'
}
export function isDateObejct (value) {
return value instanceof Date
}
export function isValidDate (date) {
if (date === null || date === undefined) {
return false
}
return !!new Date(date).getTime()
}
export function isValidRange (date) {
return (
Array.isArray(date) &&
date.length === 2 &&
isValidDate(date[0]) &&
isValidDate(date[1]) &&
(new Date(date[1]).getTime() >= new Date(date[0]).getTime())
)
}
export function parseTime (time) {
const values = (time || '').split(':')
if (values.length >= 2) {
const hours = parseInt(values[0], 10)
const minutes = parseInt(values[1], 10)
return {
hours,
minutes
}
}
return null
}
export function formatTime (time, type = '24') {
let hours = time.hours
hours = (type === '24') ? hours : (hours % 12 || 12)
hours = hours < 10 ? '0' + hours : hours
let minutes = time.minutes < 10 ? '0' + time.minutes : time.minutes
let result = hours + ':' + minutes
if (type === '12') {
result += time.hours >= 12 ? ' pm' : ' am'
}
return result
}
+23
View File
@@ -0,0 +1,23 @@
export default function scrollIntoView (container, selected) {
if (!selected) {
container.scrollTop = 0
return
}
const offsetParents = []
let pointer = selected.offsetParent
while (pointer && container !== pointer && container.contains(pointer)) {
offsetParents.push(pointer)
pointer = pointer.offsetParent
}
const top = selected.offsetTop + offsetParents.reduce((prev, curr) => (prev + curr.offsetTop), 0)
const bottom = top + selected.offsetHeight
const viewRectTop = container.scrollTop
const viewRectBottom = viewRectTop + container.clientHeight
if (top < viewRectTop) {
container.scrollTop = top
} else if (bottom > viewRectBottom) {
container.scrollTop = bottom - container.clientHeight
}
}
+519
View File
@@ -0,0 +1,519 @@
import Vue from 'vue'
import { mount, shallowMount } from '@vue/test-utils'
import { use } from '../src/locale/index'
import DatePicker from '../src/index.vue'
import CalendarPanel from '../src/calendar.vue'
import DatePanel from '../src/panel/date'
import TimePanel from '../src/panel/time'
import YearPanel from '../src/panel/year'
use('zh')
let wrapper
afterEach(() => {
wrapper.destroy()
})
describe('datepicker', () => {
it('click: pick date', () => {
wrapper = mount(DatePicker, {
propsData: {
value: new Date(2018, 4, 2)
}
})
// 2018-05-03
const vm = wrapper.vm
let td = wrapper.find('.mx-panel-date td:nth-child(5)')
expect(td.classes()).not.toContain('actived')
expect(vm.text).toBe('2018-05-02')
const time = new Date(2018, 4, 3).getTime()
td.trigger('click')
const emitted = wrapper.emitted()
expect(emitted.input[0][0].getTime()).toBe(time)
expect(emitted.change[0][0].getTime()).toBe(time)
wrapper.setProps({ value: emitted.input[0][0] })
td = wrapper.find('.mx-panel-date td:nth-child(5)')
expect(td.classes()).toContain('actived')
expect(vm.text).toBe('2018-05-03')
})
it('click: clear icon', () => {
wrapper = shallowMount(DatePicker, {
propsData: {
value: new Date(2018, 4, 2)
}
})
const vm = wrapper.vm
const clear = wrapper.find('.mx-clear-wrapper')
clear.trigger('click')
const emitted = wrapper.emitted()
expect(emitted.input[0][0]).toBe(null)
wrapper.setProps({ value: null })
expect(vm.text).toBe('')
})
it('prop: range', (done) => {
wrapper = mount(DatePicker, {
propsData: {
range: true,
value: [],
confirm: false
},
sync: false // sync bug
})
const calendars = wrapper.findAll(CalendarPanel)
const calendar1 = calendars.at(0)
const calendar2 = calendars.at(1)
const td1 = calendar1.findAll('.mx-panel-date tbody td')
const td2 = calendar2.findAll('.mx-panel-date tbody td')
td1.at(14).trigger('click')
Vue.nextTick(() => {
let emitted = wrapper.emittedByOrder()
expect(emitted).toHaveLength(0)
expect(td1.at(14).classes()).toContain('actived')
expect(td2.at(13).classes()).toContain('disabled')
expect(td2.at(14).classes()).not.toContain('disabled')
const date1 = new Date(td1.at(14).element.title)
td2.at(16).trigger('click')
Vue.nextTick(() => {
emitted = wrapper.emittedByOrder()
const date2 = new Date(td2.at(16).element.title)
expect(td2.at(16).classes()).toContain('actived')
expect(td1.at(15).classes()).toContain('inrange')
expect(td1.at(16).classes()).toContain('inrange')
expect(td1.at(17).classes()).toContain('disabled')
expect(emitted).toHaveLength(2)
expect(emitted[0].args[0]).toEqual([date1, date2])
done()
})
})
})
it('prop: rangeSeparator', () => {
wrapper = shallowMount(DatePicker, {
propsData: {
range: true,
value: [new Date('2018-06-01'), new Date('2018-06-10')],
rangeSeparator: '至'
}
})
const vm = wrapper.vm
expect(vm.text).toBe('2018-06-01 至 2018-06-10')
})
it('prop: confirm', () => {
wrapper = shallowMount(DatePicker, {
propsData: {
confirm: true
}
})
const vm = wrapper.vm
const btn = wrapper.find('.mx-datepicker-btn-confirm')
expect(btn.exists()).toBe(true)
// click the date expect popup don't close
wrapper.setData({ popupVisible: true })
vm.selectDate(new Date(2018, 5, 5))
expect(vm.popupVisible).toBe(true)
expect(wrapper.emittedByOrder()).toHaveLength(0)
btn.trigger('click')
expect(wrapper.emittedByOrder()).toHaveLength(3)
expect(vm.popupVisible).toBe(false)
})
it('prop: confirmText', () => {
wrapper = shallowMount(DatePicker, {
propsData: {
confirm: true,
confirmText: '确定'
}
})
const btn = wrapper.find('.mx-datepicker-btn-confirm')
expect(btn.text()).toBe('确定')
})
it('prop: width', () => {
wrapper = shallowMount(DatePicker, {
propsData: {
width: 300
}
})
const el = wrapper.find('.mx-datepicker').element
let width = el.style.width
expect(width).toBe('300px')
wrapper.setProps({ width: '100%' })
width = el.style.width
expect(width).toBe('100%')
})
it('prop: disabled', () => {
wrapper = shallowMount(DatePicker, {
propsData: {
disabled: true,
value: new Date(2018, 4, 5),
clearable: true
}
})
const vm = wrapper.vm
// don't show the clearIocn
expect(vm.showClearIcon).toBe(false)
// don't show the popup
vm.showPopup()
expect(vm.popupVisible).toBe(false)
})
it('prop: input attribte - inputName inputClass placeholder', () => {
wrapper = shallowMount(DatePicker, {
propsData: {
value: new Date(2018, 4, 5),
inputName: 'datepicker',
inputClass: ['mx-input', 'mx-my'],
placeholder: 'hehe'
}
})
const el = wrapper.find('.mx-input').element
expect(el.className).toBe('mx-input mx-my')
expect(el.getAttribute('name')).toBe('datepicker')
expect(el.getAttribute('placeholder')).toBe('hehe')
})
it('prop: lang', () => {
wrapper = shallowMount(DatePicker, {
propsData: {
lang: 'en'
}
})
const el = wrapper.find('.mx-input').element
expect(el.getAttribute('placeholder')).toBe('Select Date')
})
it('prop: shortcuts', () => {
const today = new Date()
wrapper = shallowMount(DatePicker, {
propsData: {
shortcuts: [
{
text: 'Today',
start: today,
end: today
}
],
range: true
}
})
let shortcuts = wrapper.findAll('.mx-shortcuts')
expect(shortcuts).toHaveLength(1)
shortcuts = shortcuts.at(0)
shortcuts.trigger('click')
expect(wrapper.emitted()).toEqual({
input: [[[today, today]]],
change: [[[today, today]]]
})
wrapper.setProps({
shortcuts: false
})
shortcuts = wrapper.find('.mx-shortcuts-wrapper')
expect(shortcuts.exists()).toBe(false)
})
})
describe('calendar-panel', () => {
it('click: prev/next month', () => {
wrapper = mount(CalendarPanel)
const nextBtn = wrapper.find('.mx-icon-next-month')
const lastBtn = wrapper.find('.mx-icon-last-month')
const vm = wrapper.vm
let count = 12
while (count--) {
const oldYear = vm.calendarYear
const oldMonth = vm.calendarMonth
nextBtn.trigger('click')
const newYear = vm.calendarYear
const newMonth = vm.calendarMonth
if (oldMonth === 11) {
expect(newMonth).toBe(0)
expect(newYear).toBe(oldYear + 1)
} else {
expect(newMonth).toBe(oldMonth + 1)
expect(newYear).toBe(oldYear)
}
}
count = 12
while (count--) {
const oldYear = vm.calendarYear
const oldMonth = vm.calendarMonth
lastBtn.trigger('click')
const newYear = vm.calendarYear
const newMonth = vm.calendarMonth
if (oldMonth === 0) {
expect(newMonth).toBe(11)
expect(newYear).toBe(oldYear - 1)
} else {
expect(newMonth).toBe(oldMonth - 1)
expect(newYear).toBe(oldYear)
}
}
})
it('click: prev/next year', () => {
wrapper = mount(CalendarPanel, {
value: new Date(2018,4,5)
})
const nextBtn = wrapper.find('.mx-icon-next-year')
const lastBtn = wrapper.find('.mx-icon-last-year')
const yearBtn = wrapper.find('.mx-current-year')
const vm = wrapper.vm
const oldYear = vm.calendarYear
expect(oldYear).toBe(2018)
nextBtn.trigger('click')
let newYear = vm.calendarYear
expect(newYear).toBe(2019)
lastBtn.trigger('click')
newYear = vm.calendarYear
expect(newYear).toBe(oldYear)
// 年视图测试
yearBtn.trigger('click')
expect(vm.panel).toBe('YEAR')
expect(vm.firstYear).toBe(2010)
nextBtn.trigger('click')
expect(vm.firstYear).toBe(2020)
lastBtn.trigger('click')
lastBtn.trigger('click')
expect(vm.firstYear).toBe(2000)
})
it('prop: notBefore/notAfter', () => {
wrapper = mount(CalendarPanel, {
propsData: {
value: new Date(2018, 4, 2),
notBefore: new Date(2018, 4, 1, 12),
notAfter: new Date(2018, 4, 31, 12)
}
})
const tds = wrapper.findAll('.mx-panel-date td')
for (let i = 0; i < 42; i++) {
const td = tds.at(i)
const classes = td.classes()
if (i < 2 || i > 32) {
expect(classes).toContain('disabled')
} else {
expect(classes).not.toContain('disabled')
}
}
})
it('prop: disabledDays(Array)', () => {
const disabledDays = ['2018-05-01', new Date(2018, 4, 3)]
wrapper = mount(CalendarPanel, {
propsData: {
value: new Date(2018, 4, 2),
disabledDays
}
})
const tds = wrapper.findAll('.mx-panel-date td.disabled')
expect(tds.length).toBe(disabledDays.length)
for (let i = 0, len = tds.length; i < len; i++) {
const tdDate = new Date(tds.at(i).element.title).getTime()
const expectDate = new Date(disabledDays[i]).setHours(0, 0, 0, 0)
expect(tdDate).toBe(expectDate)
}
})
it('prop: disabledDays(Function)', () => {
const disabledDays = function (date) {
return date < new Date(2018, 4, 1) || date > new Date(2018, 4, 31)
}
wrapper = mount(CalendarPanel, {
propsData: {
value: new Date(2018, 4, 4),
disabledDays
}
})
const tds = wrapper.findAll('.mx-panel-date td')
for (let i = 0; i < 42; i++) {
const td = tds.at(i)
const classes = td.classes()
if (i < 2 || i > 32) {
expect(classes).toContain('disabled')
} else {
expect(classes).not.toContain('disabled')
}
}
})
it('feat: when the time panel show, scroll to the right position', () => {
wrapper = mount(CalendarPanel, {
propsData: {
value: new Date(2018, 4, 4),
type: 'datetime'
}
})
wrapper.setData({
panel: 'TIME'
})
const list = wrapper.find('.mx-time-list').element
list.scrollTop = 200
wrapper.setData({
panel: 'DATE'
})
wrapper.setData({
panel: 'TIME'
})
setTimeout(() => {
expect(list.scrollTop).toBe(0)
}, 0)
})
})
describe('date-panel', () => {
const testRenderCalendar = (i) => it(`feat: render the corrent date panel firstDayOfWeek: ${i}`, () => {
wrapper = mount(DatePanel, {
propsData: {
value: new Date(2018, 4, 1),
calendarMonth: 4,
calendarYear: 2018,
firstDayOfWeek: i
}
})
const vm = wrapper.vm
const lastMonth = new Date(2018, 3, 30)
const lastMonthDay = 30
const lastMonthLength = (lastMonth.getDay() + 7 - i) % 7 + 1
const currentMonthLength = 31
const tds = wrapper.findAll('.mx-panel-date td')
for (let i = 0; i < 42; i++) {
const td = tds.at(i)
const text = parseInt(td.text(), 10)
const classes = td.classes()
if (i < lastMonthLength) {
expect(classes).toContain('last-month')
expect(text).toBe(lastMonthDay - lastMonthLength + 1 + i)
} else if (i < lastMonthLength + currentMonthLength) {
expect(text).toBe(i - lastMonthLength + 1)
expect(classes).toContain('cur-month')
if (text === 1) {
expect(classes).toContain('actived')
}
} else {
expect(text).toBe(i - lastMonthLength - currentMonthLength + 1)
expect(classes).toContain('next-month')
}
}
const week = ['一', '二', '三', '四', '五', '六', '日']
const firstWeek = wrapper.find('tr th').text()
expect(firstWeek).toBe(week[i - 1])
})
for (let i = 1; i <= 7; i++) {
testRenderCalendar(i)
}
})
describe('year-panel', () => {
it('feat: render the corrent year panel', () => {
wrapper = mount(YearPanel, {
propsData: {
value: new Date(2018, 4, 1),
firstYear: 2010
}
})
const cells = wrapper.findAll('.cell')
for (let i = 0, len = cells.length; i < len; i++) {
const cell = cells.at(i)
expect(parseInt(cell.text())).toBe(2010 + i)
if (i === 8) {
expect(cell.classes()).toContain('actived')
}
}
wrapper.setProps({ firstYear: 2020 })
for (let i = 0, len = cells.length; i < len; i++) {
const cell = cells.at(i)
expect(parseInt(cell.text())).toBe(2020 + i)
}
})
})
describe('time-panel', () => {
it('click: pick time emitted the select event', () => {
wrapper = mount(TimePanel, {
propsData: {
value: new Date(2018, 5, 5)
}
})
const list = wrapper.findAll('.mx-time-list')
expect(list).toHaveLength(3)
const hours = list.at(0).findAll('.cell')
const minutes = list.at(1).findAll('.cell')
const seconds = list.at(2).findAll('.cell')
expect(hours).toHaveLength(24)
expect(minutes).toHaveLength(60)
expect(seconds).toHaveLength(60)
hours.at(1).trigger('click')
minutes.at(1).trigger('click')
seconds.at(1).trigger('click')
expect(wrapper.emitted()).toEqual({
select: [[new Date(2018, 5, 5, 1)], [new Date(2018, 5, 5, 0, 1)], [new Date(2018, 5, 5, 0, 0, 1)]]
})
})
it('prop: minuteStep', () => {
wrapper = mount(TimePanel, {
propsData: {
value: new Date(2018, 5, 5),
minuteStep: 30
}
})
const list = wrapper.findAll('.mx-time-list')
expect(list).toHaveLength(2)
const minutes = list.at(1).findAll('.cell')
expect(minutes).toHaveLength(2)
expect(minutes.at(0).text()).toBe('00')
expect(minutes.at(1).text()).toBe('30')
})
it('prop: timePickerOptions', () => {
wrapper = mount(TimePanel, {
propsData: {
value: new Date(2018, 5, 5),
timePickerOptions: { start: '01:00', step: '00:30', end: '23:00' }
}
})
const list = wrapper.findAll('.mx-time-list')
expect(list).toHaveLength(1)
const cells = list.at(0).findAll('.cell')
expect(cells).toHaveLength(45)
expect(cells.at(0).text()).toBe('01:00')
expect(cells.at(44).text()).toBe('23:00')
cells.at(0).trigger('click')
const emitted = wrapper.emitted()
expect(emitted).toEqual({
select: [[new Date(2018, 5, 5, 1)]]
})
})
})
-45
View File
@@ -1,45 +0,0 @@
var path = require('path')
var webpack = require('webpack')
module.exports = {
entry: './demo/main.js',
output: {
path: path.resolve(__dirname, './demo'),
publicPath: '/demo/',
filename: 'build.js'
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
loaders: {
}
// other vue-loader options go here
}
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: 'file-loader',
options: {
name: '[name].[ext]?[hash]'
}
}
]
},
devServer: {
historyApiFallback: true,
noInfo: true,
port: 9000
},
performance: {
hints: false
},
devtool: '#eval-source-map'
}
-25
View File
@@ -1,25 +0,0 @@
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.config.js')
const webpack = require('webpack')
const webpackConfig = merge(baseWebpackConfig, {
devtool: 'source-map',
plugins: [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"'
}
}),
new webpack.optimize.UglifyJsPlugin({
sourceMap: false,
compress: {
warnings: false
}
}),
new webpack.LoaderOptionsPlugin({
minimize: true
})
]
})
module.exports = webpackConfig
-34
View File
@@ -1,34 +0,0 @@
const path = require('path')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.config.js')
const webpack = require('webpack')
const webpackConfig = merge(baseWebpackConfig, {
entry: './index.js',
output: {
path: path.resolve(__dirname, './dist'),
publicPath: '/dist/',
filename: 'build.js',
library: "DatePicker",
libraryTarget: "umd"
},
devtool: 'source-map',
plugins: [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"'
}
}),
new webpack.optimize.UglifyJsPlugin({
sourceMap: false,
compress: {
warnings: false
}
}),
new webpack.LoaderOptionsPlugin({
minimize: true
})
]
})
module.exports = webpackConfig