Calendar: add Calendar component (#14908)

pull/15256/head
hetech 2019-04-25 10:57:27 +08:00 committed by luckyCao
parent ac3aa99503
commit dc8bdc021e
29 changed files with 947 additions and 25 deletions

View File

@ -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"
}

View File

@ -0,0 +1,5 @@
.demo-calendar.demo-block {
.is-selected {
color: #1989FA;
}
}

View File

@ -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";

View File

@ -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 | — | — |

View File

@ -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 | — | — |

View File

@ -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 | — | — |

View File

@ -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包括 typeisSelectedday 属性)。详情解释参考下方的 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-monthcurrent-monthnext-month`isSelected` 标明该日期是否被选中;`day` 是格式化的日期,格式为 yyyy-MM-dd | Object | — | — |

View File

@ -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"

View File

@ -0,0 +1,8 @@
import Calendar from './src/main';
/* istanbul ignore next */
Calendar.install = function(Vue) {
Vue.component(Calendar.name, Calendar);
};
export default Calendar;

View File

@ -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>

View File

@ -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>

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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 => {

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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;
}
}
}

View File

@ -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
--------------------------*/

View File

@ -70,3 +70,4 @@
@import "./link.scss";
@import "./divider.scss";
@import "./image.scss";
@import "./calendar.scss";

View File

@ -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
};

View File

@ -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());
};

View File

@ -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;
});
});

12
types/calendar.d.ts vendored Normal file
View File

@ -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[]
}

View File

@ -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,