mirror of https://github.com/ElemeFE/element
Calendar: add Calendar component (#14908)
parent
ac3aa99503
commit
dc8bdc021e
|
@ -72,5 +72,6 @@
|
|||
"timeline-item": "./packages/timeline-item/index.js",
|
||||
"link": "./packages/link/index.js",
|
||||
"divider": "./packages/divider/index.js",
|
||||
"image": "./packages/image/index.js"
|
||||
"image": "./packages/image/index.js",
|
||||
"calendar": "./packages/calendar/index.js"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
.demo-calendar.demo-block {
|
||||
.is-selected {
|
||||
color: #1989FA;
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
@import "./badge.scss";
|
||||
@import "./border.scss";
|
||||
@import "./button.scss";
|
||||
@import "./calendar.scss";
|
||||
@import "./card.scss";
|
||||
@import "./carousel.scss";
|
||||
@import "./cascader.scss";
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
## Calendar
|
||||
|
||||
Display date.
|
||||
|
||||
### Basic
|
||||
|
||||
:::demo Set `value` to specify the currently displayed month. If `value` is not specified, current month is displayed. `value` supports two-way binding.
|
||||
```html
|
||||
<el-calendar v-model="value">
|
||||
</el-calendar>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
value: new Date()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
:::
|
||||
|
||||
### Custom Content
|
||||
|
||||
:::demo Customize what is displayed in the calendar cell by setting `scoped-slot` named `dateCell`. In `scoped-slot` you can get the date (the date of the current cell), data (including the type, isSelected, day attribute). For details, please refer to the API documentation below.
|
||||
```html
|
||||
<el-calendar>
|
||||
<!-- Use 2.5 slot syntax. If you use Vue 2.6, please use new slot syntax-->
|
||||
<template
|
||||
slot="dateCell"
|
||||
slot-scope="{date, data}">
|
||||
<p :class="data.isSelected ? 'is-selected' : ''">
|
||||
{{ data.day.split('-').slice(1).join('-') }} {{ data.isSelected ? '✔️' : ''}}
|
||||
</p>
|
||||
</template>
|
||||
</el-calendar>
|
||||
<style>
|
||||
.is-selected {
|
||||
color: #1989FA;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
:::
|
||||
|
||||
### Range
|
||||
|
||||
:::demo Set the `range` attribute to specify the display range of the calendar. Start time must be Monday, end time must be Sunday, and the time span cannot exceed two months.
|
||||
```html
|
||||
<el-calendar :range="['2019-03-04', '2019-03-24']">
|
||||
</el-calendar>
|
||||
```
|
||||
:::
|
||||
|
||||
### Attributes
|
||||
| Attribute | Description | Type | Accepted Values | Default |
|
||||
|-----------------|-------------- |---------- |---------------------- |--------- |
|
||||
| value / v-model | binding value | Date/string/number | — | — |
|
||||
| range | time range, including start time and end time. Start time must be Monday, end time must be Sunday, the time span cannot exceed two months | Array | — | — |
|
||||
|
||||
### dateCell scoped slot 参数
|
||||
| Attribute | Description | Type | Accepted Values | Default |
|
||||
|-----------------|-------------- |---------- |---------------------- |--------- |
|
||||
| date | date the cell represents | Date | — | — |
|
||||
| data | { type, isSelected, day}. `type` indicates which month the date belongs, optional values are prev-month, current-month, next-month; `isSelected` indicates whether the date is selected; `day` is the formatted date in the format yyyy-MM-dd | Object | — | — |
|
|
@ -0,0 +1,65 @@
|
|||
## Calendar
|
||||
|
||||
Display date.
|
||||
|
||||
### Basic
|
||||
|
||||
:::demo Set `value` to specify the currently displayed month. If `value` is not specified, current month is displayed. `value` supports two-way binding.
|
||||
```html
|
||||
<el-calendar v-model="value">
|
||||
</el-calendar>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
value: new Date()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
:::
|
||||
|
||||
### Custom Content
|
||||
|
||||
:::demo Customize what is displayed in the calendar cell by setting `scoped-slot` named `dateCell`. In `scoped-slot` you can get the date (the date of the current cell), data (including the type, isSelected, day attribute). For details, please refer to the API documentation below.
|
||||
```html
|
||||
<el-calendar>
|
||||
<!-- Use 2.5 slot syntax. If you use Vue 2.6, please use new slot syntax-->
|
||||
<template
|
||||
slot="dateCell"
|
||||
slot-scope="{date, data}">
|
||||
<p :class="data.isSelected ? 'is-selected' : ''">
|
||||
{{ data.day.split('-').slice(1).join('-') }} {{ data.isSelected ? '✔️' : ''}}
|
||||
</p>
|
||||
</template>
|
||||
</el-calendar>
|
||||
<style>
|
||||
.is-selected {
|
||||
color: #1989FA;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
:::
|
||||
|
||||
### Range
|
||||
|
||||
:::demo Set the `range` attribute to specify the display range of the calendar. Start time must be Monday, end time must be Sunday, and the time span cannot exceed two months.
|
||||
```html
|
||||
<el-calendar :range="['2019-03-04', '2019-03-24']">
|
||||
</el-calendar>
|
||||
```
|
||||
:::
|
||||
|
||||
### Attributes
|
||||
| Attribute | Description | Type | Accepted Values | Default |
|
||||
|-----------------|-------------- |---------- |---------------------- |--------- |
|
||||
| value / v-model | binding value | Date/string/number | — | — |
|
||||
| range | time range, including start time and end time. Start time must be Monday, end time must be Sunday, the time span cannot exceed two months | Array | — | — |
|
||||
|
||||
### dateCell scoped slot 参数
|
||||
| Attribute | Description | Type | Accepted Values | Default |
|
||||
|-----------------|-------------- |---------- |---------------------- |--------- |
|
||||
| date | date the cell represents | Date | — | — |
|
||||
| data | { type, isSelected, day}. `type` indicates which month the date belongs, optional values are prev-month, current-month, next-month; `isSelected` indicates whether the date is selected; `day` is the formatted date in the format yyyy-MM-dd | Object | — | — |
|
|
@ -0,0 +1,65 @@
|
|||
## Calendar
|
||||
|
||||
Display date.
|
||||
|
||||
### Basic
|
||||
|
||||
:::demo Set `value` to specify the currently displayed month. If `value` is not specified, current month is displayed. `value` supports two-way binding.
|
||||
```html
|
||||
<el-calendar v-model="value">
|
||||
</el-calendar>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
value: new Date()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
:::
|
||||
|
||||
### Custom Content
|
||||
|
||||
:::demo Customize what is displayed in the calendar cell by setting `scoped-slot` named `dateCell`. In `scoped-slot` you can get the date (the date of the current cell), data (including the type, isSelected, day attribute). For details, please refer to the API documentation below.
|
||||
```html
|
||||
<el-calendar>
|
||||
<!-- Use 2.5 slot syntax. If you use Vue 2.6, please use new slot syntax-->
|
||||
<template
|
||||
slot="dateCell"
|
||||
slot-scope="{date, data}">
|
||||
<p :class="data.isSelected ? 'is-selected' : ''">
|
||||
{{ data.day.split('-').slice(1).join('-') }} {{ data.isSelected ? '✔️' : ''}}
|
||||
</p>
|
||||
</template>
|
||||
</el-calendar>
|
||||
<style>
|
||||
.is-selected {
|
||||
color: #1989FA;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
:::
|
||||
|
||||
### Range
|
||||
|
||||
:::demo Set the `range` attribute to specify the display range of the calendar. Start time must be Monday, end time must be Sunday, and the time span cannot exceed two months.
|
||||
```html
|
||||
<el-calendar :range="['2019-03-04', '2019-03-24']">
|
||||
</el-calendar>
|
||||
```
|
||||
:::
|
||||
|
||||
### Attributes
|
||||
| Attribute | Description | Type | Accepted Values | Default |
|
||||
|-----------------|-------------- |---------- |---------------------- |--------- |
|
||||
| value / v-model | binding value | Date/string/number | — | — |
|
||||
| range | time range, including start time and end time. Start time must be Monday, end time must be Sunday, the time span cannot exceed two months | Array | — | — |
|
||||
|
||||
### dateCell scoped slot 参数
|
||||
| Attribute | Description | Type | Accepted Values | Default |
|
||||
|-----------------|-------------- |---------- |---------------------- |--------- |
|
||||
| date | date the cell represents | Date | — | — |
|
||||
| data | { type, isSelected, day}. `type` indicates which month the date belongs, optional values are prev-month, current-month, next-month; `isSelected` indicates whether the date is selected; `day` is the formatted date in the format yyyy-MM-dd | Object | — | — |
|
|
@ -0,0 +1,65 @@
|
|||
## Calendar calendar
|
||||
|
||||
显示日期
|
||||
|
||||
### 基本
|
||||
|
||||
:::demo 设置 `value` 来指定当前显示的月份。如果 `value` 未指定,则显示当月。`value` 支持 `v-model` 双向绑定。
|
||||
```html
|
||||
<el-calendar v-model="value">
|
||||
</el-calendar>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
value: new Date()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
:::
|
||||
|
||||
### 自定义内容
|
||||
|
||||
:::demo 通过设置名为 `dateCell` 的 `scoped-slot` 来自定义日历单元格中显示的内容。在 `scoped-slot` 可以获取到 date(当前单元格的日期), data(包括 type,isSelected,day 属性)。详情解释参考下方的 API 文档。
|
||||
```html
|
||||
<el-calendar>
|
||||
<!-- 这里使用的是 2.5 slot 语法,对于新项目请使用 2.6 slot 语法-->
|
||||
<template
|
||||
slot="dateCell"
|
||||
slot-scope="{date, data}">
|
||||
<p :class="data.isSelected ? 'is-selected' : ''">
|
||||
{{ data.day.split('-').slice(1).join('-') }} {{ data.isSelected ? '✔️' : ''}}
|
||||
</p>
|
||||
</template>
|
||||
</el-calendar>
|
||||
<style>
|
||||
.is-selected {
|
||||
color: #1989FA;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
:::
|
||||
|
||||
### 自定义范围
|
||||
|
||||
:::demo 设置 `range` 属性指定日历的显示范围。开始时间必须是周一,结束时间必须是周日,且时间跨度不能超过两个月。
|
||||
```html
|
||||
<el-calendar :range="['2019-03-04', '2019-03-24']">
|
||||
</el-calendar>
|
||||
```
|
||||
:::
|
||||
|
||||
### Attributes
|
||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||
|-----------------|-------------- |---------- |------------ |-------- |
|
||||
| value / v-model | 绑定值 | Date/string/number | — | — |
|
||||
| range | 时间范围,包括开始时间与结束时间。开始时间必须是周一,结束时间必须是周日,且时间跨度不能超过两个月。 | Array | — | — |
|
||||
|
||||
### dateCell scoped slot 参数
|
||||
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|
||||
|-----------------|-------------- |---------- |------------ |-------- |
|
||||
| date | 单元格代表的日期 | Date | — | — |
|
||||
| data | { type, isSelected, day},`type` 表示该日期的所属月份,可选值有 prev-month,current-month,next-month;`isSelected` 标明该日期是否被选中;`day` 是格式化的日期,格式为 yyyy-MM-dd | Object | — | — |
|
|
@ -260,6 +260,10 @@
|
|||
"path": "/divider",
|
||||
"title": "Divider 分割线"
|
||||
},
|
||||
{
|
||||
"path": "/calendar",
|
||||
"title": "Calendar 日历"
|
||||
},
|
||||
{
|
||||
"path": "/image",
|
||||
"title": "Image 图片"
|
||||
|
@ -530,6 +534,10 @@
|
|||
"path": "/divider",
|
||||
"title": "Divider"
|
||||
},
|
||||
{
|
||||
"path": "/calendar",
|
||||
"title": "Calendar"
|
||||
},
|
||||
{
|
||||
"path": "/image",
|
||||
"title": "Image"
|
||||
|
@ -800,6 +808,10 @@
|
|||
"path": "/divider",
|
||||
"title": "Divider"
|
||||
},
|
||||
{
|
||||
"path": "/calendar",
|
||||
"title": "Calendar"
|
||||
},
|
||||
{
|
||||
"path": "/image",
|
||||
"title": "Image"
|
||||
|
@ -1070,6 +1082,10 @@
|
|||
"path": "/divider",
|
||||
"title": "Divider"
|
||||
},
|
||||
{
|
||||
"path": "/calendar",
|
||||
"title": "Calendar"
|
||||
},
|
||||
{
|
||||
"path": "/image",
|
||||
"title": "Image"
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import Calendar from './src/main';
|
||||
|
||||
/* istanbul ignore next */
|
||||
Calendar.install = function(Vue) {
|
||||
Vue.component(Calendar.name, Calendar);
|
||||
};
|
||||
|
||||
export default Calendar;
|
|
@ -0,0 +1,189 @@
|
|||
<script>
|
||||
import fecha from 'element-ui/src/utils/date';
|
||||
import { range as rangeArr, getFirstDayOfMonth, getPrevMonthLastDays, getMonthDays, getI18nSettings, validateRangeInOneMonth } from 'element-ui/src/utils/date-util';
|
||||
export default {
|
||||
|
||||
props: {
|
||||
selectedDay: String, // formated date yyyy-MM-dd
|
||||
range: {
|
||||
type: Array,
|
||||
validator(val) {
|
||||
if (!(val && val.length)) return true;
|
||||
const [start, end] = val;
|
||||
return validateRangeInOneMonth(start, end);
|
||||
}
|
||||
},
|
||||
date: Date,
|
||||
hideHeader: Boolean
|
||||
},
|
||||
|
||||
inject: ['elCalendar'],
|
||||
|
||||
methods: {
|
||||
toNestedArr(days) {
|
||||
return rangeArr(days.length / 7).map((_, index) => {
|
||||
const start = index * 7;
|
||||
return days.slice(start, start + 7);
|
||||
});
|
||||
},
|
||||
|
||||
getFormateDate(day, type) {
|
||||
if (!day || ['prev', 'current', 'next'].indexOf(type) === -1) {
|
||||
throw new Error('invalid day or type');
|
||||
}
|
||||
let prefix = this.curMonthDatePrefix;
|
||||
if (type === 'prev') {
|
||||
prefix = this.prevMonthDatePrefix;
|
||||
} else if (type === 'next') {
|
||||
prefix = this.nextMonthDatePrefix;
|
||||
}
|
||||
day = `00${day}`.slice(-2);
|
||||
return `${prefix}-${day}`;
|
||||
},
|
||||
|
||||
getCellClass({ text, type}) {
|
||||
const classes = [type];
|
||||
if (type === 'current') {
|
||||
const date = this.getFormateDate(text, type);
|
||||
if (date === this.selectedDay) {
|
||||
classes.push('is-selected');
|
||||
}
|
||||
if (date === this.formatedToday) {
|
||||
classes.push('is-today');
|
||||
}
|
||||
}
|
||||
return classes;
|
||||
},
|
||||
|
||||
pickDay({ text, type }) {
|
||||
const date = this.getFormateDate(text, type);
|
||||
this.$emit('pick', date);
|
||||
},
|
||||
|
||||
cellRenderProxy({ text, type }) {
|
||||
let render = this.elCalendar.$scopedSlots.dateCell;
|
||||
if (!render) return <span>{ text }</span>;
|
||||
|
||||
const day = this.getFormateDate(text, type);
|
||||
const date = new Date(day);
|
||||
const data = {
|
||||
isSelected: this.selectedDay === day,
|
||||
type: `${type}-month`,
|
||||
day
|
||||
};
|
||||
return render({ date, data });
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
prevMonthDatePrefix() {
|
||||
const temp = new Date(this.date.getTime());
|
||||
temp.setDate(0);
|
||||
return fecha.format(temp, 'yyyy-MM');
|
||||
},
|
||||
|
||||
curMonthDatePrefix() {
|
||||
return fecha.format(this.date, 'yyyy-MM');
|
||||
},
|
||||
|
||||
nextMonthDatePrefix() {
|
||||
const temp = new Date(this.date.getFullYear(), this.date.getMonth() + 1, 1);
|
||||
return fecha.format(temp, 'yyyy-MM');
|
||||
},
|
||||
|
||||
formatedToday() {
|
||||
return this.elCalendar.formatedToday;
|
||||
},
|
||||
|
||||
isInRange() {
|
||||
return this.range && this.range.length;
|
||||
},
|
||||
|
||||
rows() {
|
||||
let days = [];
|
||||
// if range exists, should render days in range.
|
||||
if (this.isInRange) {
|
||||
const [start, end] = this.range;
|
||||
const currentMonthRange = rangeArr(end.getDate() - start.getDate() + 1).map((_, index) => ({
|
||||
text: start.getDate() + index,
|
||||
type: 'current'
|
||||
}));
|
||||
let remaining = currentMonthRange.length % 7;
|
||||
remaining = remaining === 0 ? 0 : 7 - remaining;
|
||||
const nextMonthRange = rangeArr(remaining).map((_, index) => ({
|
||||
text: index + 1,
|
||||
type: 'next'
|
||||
}));
|
||||
days = currentMonthRange.concat(nextMonthRange);
|
||||
} else {
|
||||
const date = this.date;
|
||||
const firstDay = getFirstDayOfMonth(date);
|
||||
const prevMonthDays = getPrevMonthLastDays(date, firstDay - 1).map(day => ({
|
||||
text: day,
|
||||
type: 'prev'
|
||||
}));
|
||||
const currentMonthDays = getMonthDays(date).map(day => ({
|
||||
text: day,
|
||||
type: 'current'
|
||||
}));
|
||||
days = [...prevMonthDays, ...currentMonthDays];
|
||||
const nextMonthDays = rangeArr(42 - days.length).map((_, index) => ({
|
||||
text: index + 1,
|
||||
type: 'next'
|
||||
}));
|
||||
days = days.concat(nextMonthDays);
|
||||
}
|
||||
return this.toNestedArr(days);
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
const dayNames = getI18nSettings().dayNames;
|
||||
return {
|
||||
DAYS: dayNames.slice(1).concat(dayNames[0])
|
||||
};
|
||||
},
|
||||
|
||||
render() {
|
||||
const thead = this.hideHeader ? null : (<thead>
|
||||
{
|
||||
this.DAYS.map(day => <th key={day}>{ day }</th>)
|
||||
}
|
||||
</thead>);
|
||||
return (
|
||||
<table
|
||||
class={{
|
||||
'el-calendar-table': true,
|
||||
'is-range': this.isInRange
|
||||
}}
|
||||
cellspacing="0"
|
||||
cellpadding="0">
|
||||
{
|
||||
thead
|
||||
}
|
||||
<tbody>
|
||||
{
|
||||
this.rows.map((row, index) => <tr
|
||||
class={{
|
||||
'el-calendar-table__row': true,
|
||||
'el-calendar-table__row--hide-border': index === 0 && this.hideHeader
|
||||
}}
|
||||
key={index}>
|
||||
{
|
||||
row.map((cell, key) => <td key={key}
|
||||
class={ this.getCellClass(cell) }
|
||||
onClick={this.pickDay.bind(this, cell)}>
|
||||
<div class="el-calendar-day">
|
||||
{
|
||||
this.cellRenderProxy(cell)
|
||||
}
|
||||
</div>
|
||||
</td>)
|
||||
}
|
||||
</tr>)
|
||||
}
|
||||
</tbody>
|
||||
</table>);
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,251 @@
|
|||
<template>
|
||||
<div class="el-calendar">
|
||||
<div class="el-calendar__header">
|
||||
<div class="el-calendar__title">
|
||||
{{ i18nDate }}
|
||||
</div>
|
||||
<div
|
||||
class="el-calendar__button-group"
|
||||
v-if="validatedRange.length === 0">
|
||||
<el-button-group>
|
||||
<el-button
|
||||
type="plain"
|
||||
size="mini"
|
||||
@click="selectDate('prev-month')">
|
||||
{{ t('el.datepicker.prevMonth') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="plain"
|
||||
size="mini"
|
||||
@click="selectDate('today')">
|
||||
{{ t('el.datepicker.today') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="plain"
|
||||
size="mini"
|
||||
@click="selectDate('next-month')">
|
||||
{{ t('el.datepicker.nextMonth') }}
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="el-calendar__body"
|
||||
v-if="validatedRange.length === 0"
|
||||
key="no-range">
|
||||
<date-table
|
||||
:date="date"
|
||||
:selected-day="realSelectedDay"
|
||||
@pick="pickDay" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="el-calendar__body"
|
||||
key="has-range">
|
||||
<date-table
|
||||
v-for="(range, index) in validatedRange"
|
||||
:key="index"
|
||||
:date="range[0]"
|
||||
:selected-day="realSelectedDay"
|
||||
:range="range"
|
||||
:hide-header="index !== 0"
|
||||
@pick="pickDay" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Locale from 'element-ui/src/mixins/locale';
|
||||
import fecha from 'element-ui/src/utils/date';
|
||||
import DateTable from './date-table';
|
||||
import { validateRangeInOneMonth } from 'element-ui/src/utils/date-util';
|
||||
|
||||
const validTypes = ['prev-month', 'today', 'next-month'];
|
||||
const oneDay = 86400000;
|
||||
|
||||
export default {
|
||||
name: 'ElCalendar',
|
||||
|
||||
mixins: [Locale],
|
||||
|
||||
components: {
|
||||
DateTable
|
||||
},
|
||||
|
||||
props: {
|
||||
value: [Date, String, Number],
|
||||
range: {
|
||||
type: Array,
|
||||
validator(range) {
|
||||
if (Array.isArray(range)) {
|
||||
return range.length === 2 && range.every(
|
||||
item => typeof item === 'string' ||
|
||||
typeof item === 'number' ||
|
||||
item instanceof Date);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
provide() {
|
||||
return {
|
||||
elCalendar: this
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
pickDay(day) {
|
||||
this.realSelectedDay = day;
|
||||
},
|
||||
|
||||
selectDate(type) {
|
||||
if (validTypes.indexOf(type) === -1) {
|
||||
throw new Error(`invalid type ${type}`);
|
||||
}
|
||||
let day = '';
|
||||
if (type === 'prev-month') {
|
||||
day = `${this.prevMonthDatePrefix}-01`;
|
||||
} else if (type === 'next-month') {
|
||||
day = `${this.nextMonthDatePrefix}-01`;
|
||||
} else {
|
||||
day = this.formatedToday;
|
||||
}
|
||||
|
||||
if (day === this.formatedDate) return;
|
||||
this.pickDay(day);
|
||||
},
|
||||
|
||||
toDate(val) {
|
||||
if (!val) {
|
||||
throw new Error('invalid val');
|
||||
}
|
||||
return val instanceof Date ? val : new Date(val);
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
prevMonthDatePrefix() {
|
||||
const temp = new Date(this.date.getTime());
|
||||
temp.setDate(0);
|
||||
return fecha.format(temp, 'yyyy-MM');
|
||||
},
|
||||
|
||||
curMonthDatePrefix() {
|
||||
return fecha.format(this.date, 'yyyy-MM');
|
||||
},
|
||||
|
||||
nextMonthDatePrefix() {
|
||||
const temp = new Date(this.date.getFullYear(), this.date.getMonth() + 1, 1);
|
||||
return fecha.format(temp, 'yyyy-MM');
|
||||
},
|
||||
|
||||
formatedDate() {
|
||||
return fecha.format(this.date, 'yyyy-MM-dd');
|
||||
},
|
||||
|
||||
i18nDate() {
|
||||
const year = this.formatedDate.slice(0, 4);
|
||||
const month = this.formatedDate.slice(5, 7).replace('0', '');
|
||||
return `${year} ${this.t('el.datepicker.year')} ${this.t('el.datepicker.month' + month)}`;
|
||||
},
|
||||
|
||||
formatedToday() {
|
||||
return fecha.format(this.now, 'yyyy-MM-dd');
|
||||
},
|
||||
|
||||
realSelectedDay: {
|
||||
get() {
|
||||
if (!this.value) return this.selectedDay;
|
||||
return this.formatedDate;
|
||||
},
|
||||
set(val) {
|
||||
this.selectedDay = val;
|
||||
const date = new Date(val);
|
||||
this.$emit('input', date);
|
||||
}
|
||||
},
|
||||
|
||||
date() {
|
||||
if (!this.value) {
|
||||
if (this.realSelectedDay) {
|
||||
return new Date(this.selectedDay);
|
||||
} else if (this.validatedRange.length) {
|
||||
return this.validatedRange[0][0];
|
||||
}
|
||||
return this.now;
|
||||
} else {
|
||||
return this.toDate(this.value);
|
||||
}
|
||||
},
|
||||
|
||||
// if range is valid, we get a two-digit array
|
||||
validatedRange() {
|
||||
let range = this.range;
|
||||
if (!range) return [];
|
||||
const expetedMap = {
|
||||
0: {
|
||||
value: 1,
|
||||
message: 'start of range should be Monday.'
|
||||
},
|
||||
1: {
|
||||
value: 0,
|
||||
message: 'end of range should be Sunday.'
|
||||
}
|
||||
};
|
||||
range = range.reduce((prev, val, index) => {
|
||||
const date = this.toDate(val);
|
||||
if (date.getDay() !== expetedMap[index].value) {
|
||||
console.warn('[ElementCalendar]', expetedMap[index].message, ' invalid range will be ignored');
|
||||
} else {
|
||||
prev = prev.concat(date);
|
||||
}
|
||||
return prev;
|
||||
}, []);
|
||||
if (range.length === 2) {
|
||||
const [start, end] = range;
|
||||
if (start > end) {
|
||||
console.warn('[ElementCalendar]end time should be greater than start time');
|
||||
return [];
|
||||
}
|
||||
// start time and end time in one month
|
||||
if (validateRangeInOneMonth(start, end)) {
|
||||
return [
|
||||
[start, end]
|
||||
];
|
||||
}
|
||||
const data = [];
|
||||
let startDay = new Date(start.getFullYear(), start.getMonth() + 1, 1);
|
||||
const lastDay = this.toDate(startDay.getTime() - oneDay);
|
||||
if (!validateRangeInOneMonth(startDay, end)) {
|
||||
console.warn('[ElementCalendar]start time and end time interval must not exceed two months');
|
||||
return [];
|
||||
}
|
||||
data.push([
|
||||
start,
|
||||
lastDay
|
||||
]);
|
||||
let interval = startDay.getDay();
|
||||
interval = interval <= 1 ? Math.abs(interval - 1) : (8 - interval);
|
||||
startDay = this.toDate(startDay.getTime() + interval * oneDay);
|
||||
if (startDay.getDate() < end.getDate()) {
|
||||
data.push([
|
||||
startDay,
|
||||
end
|
||||
]);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
selectedDay: '',
|
||||
now: new Date()
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -32,7 +32,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { getFirstDayOfMonth, getDayCountOfMonth, getWeekNumber, getStartDateOfMonth, prevDate, nextDate, isDate, clearTime as _clearTime} from '../util';
|
||||
import { getFirstDayOfMonth, getDayCountOfMonth, getWeekNumber, getStartDateOfMonth, prevDate, nextDate, isDate, clearTime as _clearTime} from 'element-ui/src/utils/date-util';
|
||||
import Locale from 'element-ui/src/mixins/locale';
|
||||
import { arrayFindIndex, arrayFind, coerceTruthyValueToArray } from 'element-ui/src/utils/util';
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
<script type="text/babel">
|
||||
import Locale from 'element-ui/src/mixins/locale';
|
||||
import { isDate, range, getDayCountOfMonth, nextDate } from '../util';
|
||||
import { isDate, range, getDayCountOfMonth, nextDate } from 'element-ui/src/utils/date-util';
|
||||
import { hasClass } from 'element-ui/src/utils/dom';
|
||||
import { arrayFindIndex, coerceTruthyValueToArray, arrayFind } from 'element-ui/src/utils/util';
|
||||
|
||||
|
|
|
@ -101,7 +101,7 @@
|
|||
</template>
|
||||
|
||||
<script type="text/babel">
|
||||
import { getRangeHours, getRangeMinutes, modifyTime } from '../util';
|
||||
import { getRangeHours, getRangeMinutes, modifyTime } from 'element-ui/src/utils/date-util';
|
||||
import ElScrollbar from 'element-ui/packages/scrollbar';
|
||||
import RepeatClick from 'element-ui/src/directives/repeat-click';
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
|
||||
<script type="text/babel">
|
||||
import { hasClass } from 'element-ui/src/utils/dom';
|
||||
import { isDate, range, nextDate, getDayCountOfYear } from '../util';
|
||||
import { isDate, range, nextDate, getDayCountOfYear } from 'element-ui/src/utils/date-util';
|
||||
import { arrayFindIndex, coerceTruthyValueToArray } from 'element-ui/src/utils/util';
|
||||
|
||||
const datesInYear = year => {
|
||||
|
|
|
@ -200,7 +200,7 @@
|
|||
nextDate,
|
||||
extractDateFormat,
|
||||
extractTimeFormat
|
||||
} from '../util';
|
||||
} from 'element-ui/src/utils/date-util';
|
||||
import Clickoutside from 'element-ui/src/utils/clickoutside';
|
||||
import Locale from 'element-ui/src/mixins/locale';
|
||||
import TimePicker from './time';
|
||||
|
|
|
@ -160,7 +160,7 @@
|
|||
extractDateFormat,
|
||||
extractTimeFormat,
|
||||
timeWithinRange
|
||||
} from '../util';
|
||||
} from 'element-ui/src/utils/date-util';
|
||||
import Clickoutside from 'element-ui/src/utils/clickoutside';
|
||||
import Locale from 'element-ui/src/mixins/locale';
|
||||
import ElInput from 'element-ui/packages/input';
|
||||
|
|
|
@ -84,7 +84,7 @@
|
|||
prevYear,
|
||||
nextYear,
|
||||
nextMonth
|
||||
} from '../util';
|
||||
} from 'element-ui/src/utils/date-util';
|
||||
import Clickoutside from 'element-ui/src/utils/clickoutside';
|
||||
import Locale from 'element-ui/src/mixins/locale';
|
||||
import MonthTable from '../basic/month-table';
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
modifyDate,
|
||||
clearMilliseconds,
|
||||
timeWithinRange
|
||||
} from '../util';
|
||||
} from 'element-ui/src/utils/date-util';
|
||||
import Locale from 'element-ui/src/mixins/locale';
|
||||
import TimeSpinner from '../basic/time-spinner';
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
</template>
|
||||
|
||||
<script type="text/babel">
|
||||
import { limitTimeRange, isDate, clearMilliseconds, timeWithinRange } from '../util';
|
||||
import { limitTimeRange, isDate, clearMilliseconds, timeWithinRange } from 'element-ui/src/utils/date-util';
|
||||
import Locale from 'element-ui/src/mixins/locale';
|
||||
import TimeSpinner from '../basic/time-spinner';
|
||||
|
||||
|
|
|
@ -86,7 +86,7 @@
|
|||
<script>
|
||||
import Vue from 'vue';
|
||||
import Clickoutside from 'element-ui/src/utils/clickoutside';
|
||||
import { formatDate, parseDate, isDateObject, getWeekNumber } from './util';
|
||||
import { formatDate, parseDate, isDateObject, getWeekNumber } from 'element-ui/src/utils/date-util';
|
||||
import Popper from 'element-ui/src/utils/vue-popper';
|
||||
import Emitter from 'element-ui/src/mixins/emitter';
|
||||
import ElInput from 'element-ui/packages/input';
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
@import "mixins/mixins";
|
||||
@import "common/var";
|
||||
|
||||
@include b(calendar) {
|
||||
background-color:#fff;
|
||||
|
||||
@include e(header) {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 12px 20px;
|
||||
border-bottom: $--table-border;
|
||||
}
|
||||
|
||||
@include e(title) {
|
||||
color: #000000;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
@include e(body) {
|
||||
padding: 12px 20px 35px;
|
||||
}
|
||||
}
|
||||
|
||||
@include b(calendar-table) {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
|
||||
thead th {
|
||||
padding: 12px 0;
|
||||
color: $--color-text-regular;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
&:not(.is-range) {
|
||||
td.prev,
|
||||
td.next {
|
||||
color: $--color-text-placeholder;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
border-bottom: $--calendar-border;
|
||||
border-right: $--calendar-border;
|
||||
vertical-align: top;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
@include when(selected) {
|
||||
background-color: $--calendar-selected-background-color;
|
||||
}
|
||||
|
||||
@include when(today) {
|
||||
color: $--color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
tr:first-child td {
|
||||
border-top: $--calendar-border;
|
||||
}
|
||||
|
||||
tr td:first-child {
|
||||
border-left: $--calendar-border;
|
||||
}
|
||||
|
||||
tr.el-calendar-table__row--hide-border td {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
@include b(calendar-day) {
|
||||
box-sizing: border-box;
|
||||
padding: 8px;
|
||||
height: $--calendar-cell-width;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: $--calendar-selected-background-color;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -934,6 +934,13 @@ $--link-warning-font-color: $--color-warning !default;
|
|||
$--link-danger-font-color: $--color-danger !default;
|
||||
/// color||Color|0
|
||||
$--link-info-font-color: $--color-info !default;
|
||||
/* Calendar
|
||||
--------------------------*/
|
||||
/// border||Other|4
|
||||
$--calendar-border: $--table-border !default;
|
||||
/// color||Other|4
|
||||
$--calendar-selected-background-color: #F2F8FE !default;
|
||||
$--calendar-cell-width: 85px !default;
|
||||
|
||||
/* Break-point
|
||||
--------------------------*/
|
||||
|
|
|
@ -70,3 +70,4 @@
|
|||
@import "./link.scss";
|
||||
@import "./divider.scss";
|
||||
@import "./image.scss";
|
||||
@import "./calendar.scss";
|
||||
|
|
|
@ -74,6 +74,7 @@ import TimelineItem from '../packages/timeline-item/index.js';
|
|||
import Link from '../packages/link/index.js';
|
||||
import Divider from '../packages/divider/index.js';
|
||||
import Image from '../packages/image/index.js';
|
||||
import Calendar from '../packages/calendar/index.js';
|
||||
import locale from 'element-ui/src/locale';
|
||||
import CollapseTransition from 'element-ui/src/transitions/collapse-transition';
|
||||
|
||||
|
@ -148,6 +149,7 @@ const components = [
|
|||
Link,
|
||||
Divider,
|
||||
Image,
|
||||
Calendar,
|
||||
CollapseTransition
|
||||
];
|
||||
|
||||
|
@ -260,5 +262,6 @@ export default {
|
|||
TimelineItem,
|
||||
Link,
|
||||
Divider,
|
||||
Image
|
||||
Image,
|
||||
Calendar
|
||||
};
|
||||
|
|
|
@ -1,17 +1,8 @@
|
|||
import dateUtil from 'element-ui/src/utils/date';
|
||||
import fecha from 'element-ui/src/utils/date';
|
||||
import { t } from 'element-ui/src/locale';
|
||||
|
||||
const weeks = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
||||
const months = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
|
||||
const getI18nSettings = () => {
|
||||
return {
|
||||
dayNamesShort: weeks.map(week => t(`el.datepicker.weeks.${ week }`)),
|
||||
dayNames: weeks.map(week => t(`el.datepicker.weeks.${ week }`)),
|
||||
monthNamesShort: months.map(month => t(`el.datepicker.months.${ month }`)),
|
||||
monthNames: months.map((month, index) => t(`el.datepicker.month${ index + 1 }`)),
|
||||
amPm: ['am', 'pm']
|
||||
};
|
||||
};
|
||||
|
||||
const newArray = function(start, end) {
|
||||
let result = [];
|
||||
|
@ -21,6 +12,16 @@ const newArray = function(start, end) {
|
|||
return result;
|
||||
};
|
||||
|
||||
export const getI18nSettings = () => {
|
||||
return {
|
||||
dayNamesShort: weeks.map(week => t(`el.datepicker.weeks.${ week }`)),
|
||||
dayNames: weeks.map(week => t(`el.datepicker.weeks.${ week }`)),
|
||||
monthNamesShort: months.map(month => t(`el.datepicker.months.${ month }`)),
|
||||
monthNames: months.map((month, index) => t(`el.datepicker.month${ index + 1 }`)),
|
||||
amPm: ['am', 'pm']
|
||||
};
|
||||
};
|
||||
|
||||
export const toDate = function(date) {
|
||||
return isDate(date) ? new Date(date) : null;
|
||||
};
|
||||
|
@ -39,11 +40,11 @@ export const isDateObject = function(val) {
|
|||
export const formatDate = function(date, format) {
|
||||
date = toDate(date);
|
||||
if (!date) return '';
|
||||
return dateUtil.format(date, format || 'yyyy-MM-dd', getI18nSettings());
|
||||
return fecha.format(date, format || 'yyyy-MM-dd', getI18nSettings());
|
||||
};
|
||||
|
||||
export const parseDate = function(string, format) {
|
||||
return dateUtil.parse(string, format || 'yyyy-MM-dd', getI18nSettings());
|
||||
return fecha.parse(string, format || 'yyyy-MM-dd', getI18nSettings());
|
||||
};
|
||||
|
||||
export const getDayCountOfMonth = function(year, month) {
|
||||
|
@ -131,6 +132,20 @@ export const getRangeHours = function(ranges) {
|
|||
return hours;
|
||||
};
|
||||
|
||||
export const getPrevMonthLastDays = (date, amount) => {
|
||||
if (amount <= 0) return [];
|
||||
const temp = new Date(date.getTime());
|
||||
temp.setDate(0);
|
||||
const lastDay = temp.getDate();
|
||||
return range(amount).map((_, index) => lastDay - (amount - index - 1));
|
||||
};
|
||||
|
||||
export const getMonthDays = (date) => {
|
||||
const temp = new Date(date.getFullYear(), date.getMonth() + 1, 0);
|
||||
const days = temp.getDate();
|
||||
return range(days).map((_, index) => index + 1);
|
||||
};
|
||||
|
||||
function setRangeData(arr, start, end, value) {
|
||||
for (let i = start; i < end; i++) {
|
||||
arr[i] = value;
|
||||
|
@ -196,7 +211,7 @@ export const clearMilliseconds = function(date) {
|
|||
export const limitTimeRange = function(date, ranges, format = 'HH:mm:ss') {
|
||||
// TODO: refactory a more elegant solution
|
||||
if (ranges.length === 0) return date;
|
||||
const normalizeDate = date => dateUtil.parse(dateUtil.format(date, format), format);
|
||||
const normalizeDate = date => fecha.parse(fecha.format(date, format), format);
|
||||
const ndate = normalizeDate(date);
|
||||
const nranges = ranges.map(range => range.map(normalizeDate));
|
||||
if (nranges.some(nrange => ndate >= nrange[0] && ndate <= nrange[1])) return date;
|
||||
|
@ -271,3 +286,7 @@ export const extractTimeFormat = function(format) {
|
|||
.replace(/\W?D{1,2}|\W?Do|\W?d{1,4}|\W?M{1,4}|\W?y{2,4}/g, '')
|
||||
.trim();
|
||||
};
|
||||
|
||||
export const validateRangeInOneMonth = function(start, end) {
|
||||
return (start.getMonth() === end.getMonth()) && (start.getFullYear() === end.getFullYear());
|
||||
};
|
|
@ -0,0 +1,70 @@
|
|||
import { createVue, destroyVM, waitImmediate } from '../util';
|
||||
|
||||
describe('Calendar', () => {
|
||||
let vm;
|
||||
afterEach(() => {
|
||||
destroyVM(vm);
|
||||
});
|
||||
|
||||
it('create', async() => {
|
||||
vm = createVue({
|
||||
template: `
|
||||
<el-calendar v-model="value"></el-calendar>
|
||||
`,
|
||||
data() {
|
||||
return {
|
||||
value: new Date('2019-04-01')
|
||||
};
|
||||
}
|
||||
}, true);
|
||||
const titleEl = vm.$el.querySelector('.el-calendar__title');
|
||||
expect(/2019.*4/.test(titleEl.innerText)).to.be.true;
|
||||
expect(vm.$el.querySelectorAll('thead th').length).to.equal(7);
|
||||
const rows = vm.$el.querySelectorAll('.el-calendar-table__row');
|
||||
expect(rows.length).to.equal(6);
|
||||
rows[5].firstElementChild.click();
|
||||
|
||||
await waitImmediate();
|
||||
|
||||
expect(/2019.*5/.test(titleEl.innerText)).to.be.true;
|
||||
const value = vm.value;
|
||||
expect(value.getFullYear()).to.be.equal(2019);
|
||||
expect(value.getMonth()).to.be.equal(4);
|
||||
expect(vm.$el.querySelector('.is-selected span').innerText).to.be.equal('6');
|
||||
});
|
||||
|
||||
it('range', () => {
|
||||
vm = createVue({
|
||||
template: `
|
||||
<el-calendar :range="['2019-03-04', '2019-03-24']"></el-calendar>
|
||||
`
|
||||
}, true);
|
||||
const titleEl = vm.$el.querySelector('.el-calendar__title');
|
||||
expect(/2019.*3/.test(titleEl.innerText)).to.be.true;
|
||||
const rows = vm.$el.querySelectorAll('.el-calendar-table__row');
|
||||
expect(rows.length).to.equal(3);
|
||||
expect(vm.$el.querySelector('.el-calendar__button-group')).to.be.a('null');
|
||||
});
|
||||
|
||||
it('range tow monthes', async() => {
|
||||
vm = createVue({
|
||||
template: `
|
||||
<el-calendar :range="['2019-04-15', '2019-05-19']"></el-calendar>
|
||||
`
|
||||
}, true);
|
||||
const titleEl = vm.$el.querySelector('.el-calendar__title');
|
||||
expect(/2019.*4/.test(titleEl.innerText)).to.be.true;
|
||||
const dateTables = vm.$el.querySelectorAll('.el-calendar-table.is-range');
|
||||
expect(dateTables.length).to.be.equal(2);
|
||||
const rows = vm.$el.querySelectorAll('.el-calendar-table__row');
|
||||
expect(rows.length).to.equal(5);
|
||||
const cell = rows[rows.length - 1].firstElementChild;
|
||||
cell.click();
|
||||
|
||||
await waitImmediate();
|
||||
|
||||
expect(/2019.*5/.test(titleEl.innerText)).to.be.true;
|
||||
expect(cell.classList.contains('is-selected')).to.be.true;
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { ElementUIComponent } from './component'
|
||||
|
||||
export type DateType = Date | String | Number
|
||||
|
||||
/** Calendar Component */
|
||||
export declare class ElCalendar extends ElementUIComponent {
|
||||
/** Binding value */
|
||||
value: DateType
|
||||
|
||||
/** Specify the display range of the calendar */
|
||||
range: DateType[]
|
||||
}
|
|
@ -70,6 +70,8 @@ import { ElTransfer } from './transfer'
|
|||
import { ElTree } from './tree'
|
||||
import { ElUpload } from './upload'
|
||||
import { ElDivider } from './divider'
|
||||
import { ElCalendar } from './calendar'
|
||||
import { ElImage } from './image'
|
||||
|
||||
export interface InstallationOptions {
|
||||
locale: any,
|
||||
|
|
Loading…
Reference in New Issue