chushihua

This commit is contained in:
li
2026-01-26 23:20:48 +08:00
commit 7b5aab0206
22521 changed files with 3300090 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
import Alert from './src/main';
/* istanbul ignore next */
Alert.install = function(Vue) {
Vue.component(Alert.name, Alert);
};
export default Alert;
+87
View File
@@ -0,0 +1,87 @@
<template>
<transition name="el-alert-fade">
<div
class="el-alert"
:class="[typeClass, center ? 'is-center' : '']"
v-show="visible"
role="alert"
>
<i class="el-alert__icon" :class="[ iconClass, isBigIcon ]" v-if="showIcon"></i>
<div class="el-alert__content">
<span class="el-alert__title" :class="[ isBoldTitle ]" v-if="title || $slots.title">
<slot name="title">{{ title }}</slot>
</span>
<p class="el-alert__description" v-if="$slots.default && !description"><slot></slot></p>
<p class="el-alert__description" v-if="description && !$slots.default">{{ description }}</p>
<i class="el-alert__closebtn" :class="{ 'is-customed': closeText !== '', 'el-icon-close': closeText === '' }" v-show="closable" @click="close()">{{closeText}}</i>
</div>
</div>
</transition>
</template>
<script type="text/babel">
const TYPE_CLASSES_MAP = {
'success': 'el-icon-success',
'warning': 'el-icon-warning',
'error': 'el-icon-error'
};
export default {
name: 'ElAlert',
props: {
title: {
type: String,
default: ''
},
description: {
type: String,
default: ''
},
type: {
type: String,
default: 'info'
},
closable: {
type: Boolean,
default: true
},
closeText: {
type: String,
default: ''
},
showIcon: Boolean,
center: Boolean
},
data() {
return {
visible: true
};
},
methods: {
close() {
this.visible = false;
this.$emit('close');
}
},
computed: {
typeClass() {
return `el-alert--${ this.type }`;
},
iconClass() {
return TYPE_CLASSES_MAP[this.type] || 'el-icon-info';
},
isBigIcon() {
return this.description || this.$slots.default ? 'is-big' : '';
},
isBoldTitle() {
return this.description || this.$slots.default ? 'is-bold' : '';
}
}
};
</script>
+8
View File
@@ -0,0 +1,8 @@
import Aside from './src/main';
/* istanbul ignore next */
Aside.install = function(Vue) {
Vue.component(Aside.name, Aside);
};
export default Aside;
+20
View File
@@ -0,0 +1,20 @@
<template>
<aside class="el-aside" :style="{ width }">
<slot></slot>
</aside>
</template>
<script>
export default {
name: 'ElAside',
componentName: 'ElAside',
props: {
width: {
type: String,
default: '300px'
}
}
};
</script>
+8
View File
@@ -0,0 +1,8 @@
import ElAutocomplete from './src/autocomplete';
/* istanbul ignore next */
ElAutocomplete.install = function(Vue) {
Vue.component(ElAutocomplete.name, ElAutocomplete);
};
export default ElAutocomplete;
@@ -0,0 +1,76 @@
<template>
<transition name="el-zoom-in-top" @after-leave="doDestroy">
<div
v-show="showPopper"
class="el-autocomplete-suggestion el-popper"
:class="{ 'is-loading': !parent.hideLoading && parent.loading }"
:style="{ width: dropdownWidth }"
role="region">
<el-scrollbar
tag="ul"
wrap-class="el-autocomplete-suggestion__wrap"
view-class="el-autocomplete-suggestion__list">
<li v-if="!parent.hideLoading && parent.loading"><i class="el-icon-loading"></i></li>
<slot v-else>
</slot>
</el-scrollbar>
</div>
</transition>
</template>
<script>
import Popper from 'element-ui/src/utils/vue-popper';
import Emitter from 'element-ui/src/mixins/emitter';
import ElScrollbar from 'element-ui/packages/scrollbar';
export default {
components: { ElScrollbar },
mixins: [Popper, Emitter],
componentName: 'ElAutocompleteSuggestions',
data() {
return {
parent: this.$parent,
dropdownWidth: ''
};
},
props: {
options: {
default() {
return {
gpuAcceleration: false
};
}
},
id: String
},
methods: {
select(item) {
this.dispatch('ElAutocomplete', 'item-click', item);
}
},
updated() {
this.$nextTick(_ => {
this.popperJS && this.updatePopper();
});
},
mounted() {
this.$parent.popperElm = this.popperElm = this.$el;
this.referenceElm = this.$parent.$refs.input.$refs.input;
this.referenceList = this.$el.querySelector('.el-autocomplete-suggestion__list');
this.referenceList.setAttribute('role', 'listbox');
this.referenceList.setAttribute('id', this.id);
},
created() {
this.$on('visible', (val, inputWidth) => {
this.dropdownWidth = inputWidth + 'px';
this.showPopper = val;
});
}
};
</script>
+281
View File
@@ -0,0 +1,281 @@
<template>
<div
class="el-autocomplete"
v-clickoutside="close"
aria-haspopup="listbox"
role="combobox"
:aria-expanded="suggestionVisible"
:aria-owns="id"
>
<el-input
ref="input"
v-bind="[$props, $attrs]"
@input="handleChange"
@focus="handleFocus"
@blur="handleBlur"
@clear="handleClear"
@keydown.up.native.prevent="highlight(highlightedIndex - 1)"
@keydown.down.native.prevent="highlight(highlightedIndex + 1)"
@keydown.enter.native="handleKeyEnter"
@keydown.native.tab="close"
>
<template slot="prepend" v-if="$slots.prepend">
<slot name="prepend"></slot>
</template>
<template slot="append" v-if="$slots.append">
<slot name="append"></slot>
</template>
<template slot="prefix" v-if="$slots.prefix">
<slot name="prefix"></slot>
</template>
<template slot="suffix" v-if="$slots.suffix">
<slot name="suffix"></slot>
</template>
</el-input>
<el-autocomplete-suggestions
visible-arrow
:class="[popperClass ? popperClass : '']"
:popper-options="popperOptions"
:append-to-body="popperAppendToBody"
ref="suggestions"
:placement="placement"
:id="id">
<li
v-for="(item, index) in suggestions"
:key="index"
:class="{'highlighted': highlightedIndex === index}"
@click="select(item)"
:id="`${id}-item-${index}`"
role="option"
:aria-selected="highlightedIndex === index"
>
<slot :item="item">
{{ item[valueKey] }}
</slot>
</li>
</el-autocomplete-suggestions>
</div>
</template>
<script>
import debounce from 'throttle-debounce/debounce';
import ElInput from 'element-ui/packages/input';
import Clickoutside from 'element-ui/src/utils/clickoutside';
import ElAutocompleteSuggestions from './autocomplete-suggestions.vue';
import Emitter from 'element-ui/src/mixins/emitter';
import Migrating from 'element-ui/src/mixins/migrating';
import { generateId } from 'element-ui/src/utils/util';
import Focus from 'element-ui/src/mixins/focus';
export default {
name: 'ElAutocomplete',
mixins: [Emitter, Focus('input'), Migrating],
inheritAttrs: false,
componentName: 'ElAutocomplete',
components: {
ElInput,
ElAutocompleteSuggestions
},
directives: { Clickoutside },
props: {
valueKey: {
type: String,
default: 'value'
},
popperClass: String,
popperOptions: Object,
placeholder: String,
clearable: {
type: Boolean,
default: false
},
disabled: Boolean,
name: String,
size: String,
value: String,
maxlength: Number,
minlength: Number,
autofocus: Boolean,
fetchSuggestions: Function,
triggerOnFocus: {
type: Boolean,
default: true
},
customItem: String,
selectWhenUnmatched: {
type: Boolean,
default: false
},
prefixIcon: String,
suffixIcon: String,
label: String,
debounce: {
type: Number,
default: 300
},
placement: {
type: String,
default: 'bottom-start'
},
hideLoading: Boolean,
popperAppendToBody: {
type: Boolean,
default: true
},
highlightFirstItem: {
type: Boolean,
default: false
}
},
data() {
return {
activated: false,
suggestions: [],
loading: false,
highlightedIndex: -1,
suggestionDisabled: false
};
},
computed: {
suggestionVisible() {
const suggestions = this.suggestions;
let isValidData = Array.isArray(suggestions) && suggestions.length > 0;
return (isValidData || this.loading) && this.activated;
},
id() {
return `el-autocomplete-${generateId()}`;
}
},
watch: {
suggestionVisible(val) {
let $input = this.getInput();
if ($input) {
this.broadcast('ElAutocompleteSuggestions', 'visible', [val, $input.offsetWidth]);
}
}
},
methods: {
getMigratingConfig() {
return {
props: {
'custom-item': 'custom-item is removed, use scoped slot instead.',
'props': 'props is removed, use value-key instead.'
}
};
},
getData(queryString) {
if (this.suggestionDisabled) {
return;
}
this.loading = true;
this.fetchSuggestions(queryString, (suggestions) => {
this.loading = false;
if (this.suggestionDisabled) {
return;
}
if (Array.isArray(suggestions)) {
this.suggestions = suggestions;
this.highlightedIndex = this.highlightFirstItem ? 0 : -1;
} else {
console.error('[Element Error][Autocomplete]autocomplete suggestions must be an array');
}
});
},
handleChange(value) {
this.$emit('input', value);
this.suggestionDisabled = false;
if (!this.triggerOnFocus && !value) {
this.suggestionDisabled = true;
this.suggestions = [];
return;
}
this.debouncedGetData(value);
},
handleFocus(event) {
this.activated = true;
this.$emit('focus', event);
if (this.triggerOnFocus) {
this.debouncedGetData(this.value);
}
},
handleBlur(event) {
this.$emit('blur', event);
},
handleClear() {
this.activated = false;
this.$emit('clear');
},
close(e) {
this.activated = false;
},
handleKeyEnter(e) {
if (this.suggestionVisible && this.highlightedIndex >= 0 && this.highlightedIndex < this.suggestions.length) {
e.preventDefault();
this.select(this.suggestions[this.highlightedIndex]);
} else if (this.selectWhenUnmatched) {
this.$emit('select', {value: this.value});
this.$nextTick(_ => {
this.suggestions = [];
this.highlightedIndex = -1;
});
}
},
select(item) {
this.$emit('input', item[this.valueKey]);
this.$emit('select', item);
this.$nextTick(_ => {
this.suggestions = [];
this.highlightedIndex = -1;
});
},
highlight(index) {
if (!this.suggestionVisible || this.loading) { return; }
if (index < 0) {
this.highlightedIndex = -1;
return;
}
if (index >= this.suggestions.length) {
index = this.suggestions.length - 1;
}
const suggestion = this.$refs.suggestions.$el.querySelector('.el-autocomplete-suggestion__wrap');
const suggestionList = suggestion.querySelectorAll('.el-autocomplete-suggestion__list li');
let highlightItem = suggestionList[index];
let scrollTop = suggestion.scrollTop;
let offsetTop = highlightItem.offsetTop;
if (offsetTop + highlightItem.scrollHeight > (scrollTop + suggestion.clientHeight)) {
suggestion.scrollTop += highlightItem.scrollHeight;
}
if (offsetTop < scrollTop) {
suggestion.scrollTop -= highlightItem.scrollHeight;
}
this.highlightedIndex = index;
let $input = this.getInput();
$input.setAttribute('aria-activedescendant', `${this.id}-item-${this.highlightedIndex}`);
},
getInput() {
return this.$refs.input.getInput();
}
},
mounted() {
this.debouncedGetData = debounce(this.debounce, this.getData);
this.$on('item-click', item => {
this.select(item);
});
let $input = this.getInput();
$input.setAttribute('role', 'textbox');
$input.setAttribute('aria-autocomplete', 'list');
$input.setAttribute('aria-controls', 'id');
$input.setAttribute('aria-activedescendant', `${this.id}-item-${this.highlightedIndex}`);
},
beforeDestroy() {
this.$refs.suggestions.$destroy();
}
};
</script>
+8
View File
@@ -0,0 +1,8 @@
import Badge from './src/main';
/* istanbul ignore next */
Badge.install = function(Vue) {
Vue.component(Badge.name, Badge);
};
export default Badge;
+53
View File
@@ -0,0 +1,53 @@
<template>
<div class="el-badge">
<slot></slot>
<transition name="el-zoom-in-center">
<sup
v-show="!hidden && (content || content === 0 || isDot)"
v-text="content"
class="el-badge__content"
:class="[
'el-badge__content--' + type,
{
'is-fixed': $slots.default,
'is-dot': isDot
}
]">
</sup>
</transition>
</div>
</template>
<script>
export default {
name: 'ElBadge',
props: {
value: {},
max: Number,
isDot: Boolean,
hidden: Boolean,
type: {
type: String,
validator(val) {
return ['primary', 'success', 'warning', 'info', 'danger'].indexOf(val) > -1;
}
}
},
computed: {
content() {
if (this.isDot) return;
const value = this.value;
const max = this.max;
if (typeof value === 'number' && typeof max === 'number') {
return max < value ? `${max}+` : value;
}
return value;
}
}
};
</script>
+8
View File
@@ -0,0 +1,8 @@
import ElBreadcrumbItem from '../breadcrumb/src/breadcrumb-item';
/* istanbul ignore next */
ElBreadcrumbItem.install = function(Vue) {
Vue.component(ElBreadcrumbItem.name, ElBreadcrumbItem);
};
export default ElBreadcrumbItem;
+8
View File
@@ -0,0 +1,8 @@
import ElBreadcrumb from './src/breadcrumb';
/* istanbul ignore next */
ElBreadcrumb.install = function(Vue) {
Vue.component(ElBreadcrumb.name, ElBreadcrumb);
};
export default ElBreadcrumb;
+41
View File
@@ -0,0 +1,41 @@
<template>
<span class="el-breadcrumb__item">
<span
:class="['el-breadcrumb__inner', to ? 'is-link' : '']"
ref="link"
role="link">
<slot></slot>
</span>
<i v-if="separatorClass" class="el-breadcrumb__separator" :class="separatorClass"></i>
<span v-else class="el-breadcrumb__separator" role="presentation">{{separator}}</span>
</span>
</template>
<script>
export default {
name: 'ElBreadcrumbItem',
props: {
to: {},
replace: Boolean
},
data() {
return {
separator: '',
separatorClass: ''
};
},
inject: ['elBreadcrumb'],
mounted() {
this.separator = this.elBreadcrumb.separator;
this.separatorClass = this.elBreadcrumb.separatorClass;
const link = this.$refs.link;
link.setAttribute('role', 'link');
link.addEventListener('click', _ => {
const { to, $router } = this;
if (!to || !$router) return;
this.replace ? $router.replace(to) : $router.push(to);
});
}
};
</script>
+34
View File
@@ -0,0 +1,34 @@
<template>
<div class="el-breadcrumb" aria-label="Breadcrumb" role="navigation">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'ElBreadcrumb',
props: {
separator: {
type: String,
default: '/'
},
separatorClass: {
type: String,
default: ''
}
},
provide() {
return {
elBreadcrumb: this
};
},
mounted() {
const items = this.$el.querySelectorAll('.el-breadcrumb__item');
if (items.length) {
items[items.length - 1].setAttribute('aria-current', 'page');
}
}
};
</script>
+8
View File
@@ -0,0 +1,8 @@
import ElButtonGroup from '../button/src/button-group';
/* istanbul ignore next */
ElButtonGroup.install = function(Vue) {
Vue.component(ElButtonGroup.name, ElButtonGroup);
};
export default ElButtonGroup;
+8
View File
@@ -0,0 +1,8 @@
import ElButton from './src/button';
/* istanbul ignore next */
ElButton.install = function(Vue) {
Vue.component(ElButton.name, ElButton);
};
export default ElButton;
+10
View File
@@ -0,0 +1,10 @@
<template>
<div class="el-button-group">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'ElButtonGroup'
};
</script>
+78
View File
@@ -0,0 +1,78 @@
<template>
<button
class="el-button"
@click="handleClick"
:disabled="buttonDisabled || loading"
:autofocus="autofocus"
:type="nativeType"
:class="[
type ? 'el-button--' + type : '',
buttonSize ? 'el-button--' + buttonSize : '',
{
'is-disabled': buttonDisabled,
'is-loading': loading,
'is-plain': plain,
'is-round': round,
'is-circle': circle
}
]"
>
<i class="el-icon-loading" v-if="loading"></i>
<i :class="icon" v-if="icon && !loading"></i>
<span v-if="$slots.default"><slot></slot></span>
</button>
</template>
<script>
export default {
name: 'ElButton',
inject: {
elForm: {
default: ''
},
elFormItem: {
default: ''
}
},
props: {
type: {
type: String,
default: 'default'
},
size: String,
icon: {
type: String,
default: ''
},
nativeType: {
type: String,
default: 'button'
},
loading: Boolean,
disabled: Boolean,
plain: Boolean,
autofocus: Boolean,
round: Boolean,
circle: Boolean
},
computed: {
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
buttonSize() {
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
},
buttonDisabled() {
return this.disabled || (this.elForm || {}).disabled;
}
},
methods: {
handleClick(evt) {
this.$emit('click', evt);
}
}
};
</script>
+8
View File
@@ -0,0 +1,8 @@
import Card from './src/main';
/* istanbul ignore next */
Card.install = function(Vue) {
Vue.component(Card.name, Card);
};
export default Card;
+23
View File
@@ -0,0 +1,23 @@
<template>
<div class="el-card" :class="shadow ? 'is-' + shadow + '-shadow' : 'is-always-shadow'">
<div class="el-card__header" v-if="$slots.header || header">
<slot name="header">{{ header }}</slot>
</div>
<div class="el-card__body" :style="bodyStyle">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: 'ElCard',
props: {
header: {},
bodyStyle: {},
shadow: {
type: String
}
}
};
</script>
+8
View File
@@ -0,0 +1,8 @@
import ElCarouselItem from '../carousel/src/item';
/* istanbul ignore next */
ElCarouselItem.install = function(Vue) {
Vue.component(ElCarouselItem.name, ElCarouselItem);
};
export default ElCarouselItem;
+8
View File
@@ -0,0 +1,8 @@
import Carousel from './src/main';
/* istanbul ignore next */
Carousel.install = function(Vue) {
Vue.component(Carousel.name, Carousel);
};
export default Carousel;
+114
View File
@@ -0,0 +1,114 @@
<template>
<div
v-show="ready"
class="el-carousel__item"
:class="{
'is-active': active,
'el-carousel__item--card': $parent.type === 'card',
'is-in-stage': inStage,
'is-hover': hover,
'is-animating': animating
}"
@click="handleItemClick"
:style="{
msTransform: `translateX(${ translate }px) scale(${ scale })`,
webkitTransform: `translateX(${ translate }px) scale(${ scale })`,
transform: `translateX(${ translate }px) scale(${ scale })`
}">
<div
v-if="$parent.type === 'card'"
v-show="!active"
class="el-carousel__mask">
</div>
<slot></slot>
</div>
</template>
<script>
const CARD_SCALE = 0.83;
export default {
name: 'ElCarouselItem',
props: {
name: String,
label: {
type: [String, Number],
default: ''
}
},
data() {
return {
hover: false,
translate: 0,
scale: 1,
active: false,
ready: false,
inStage: false,
animating: false
};
},
methods: {
processIndex(index, activeIndex, length) {
if (activeIndex === 0 && index === length - 1) {
return -1;
} else if (activeIndex === length - 1 && index === 0) {
return length;
} else if (index < activeIndex - 1 && activeIndex - index >= length / 2) {
return length + 1;
} else if (index > activeIndex + 1 && index - activeIndex >= length / 2) {
return -2;
}
return index;
},
calculateTranslate(index, activeIndex, parentWidth) {
if (this.inStage) {
return parentWidth * ((2 - CARD_SCALE) * (index - activeIndex) + 1) / 4;
} else if (index < activeIndex) {
return -(1 + CARD_SCALE) * parentWidth / 4;
} else {
return (3 + CARD_SCALE) * parentWidth / 4;
}
},
translateItem(index, activeIndex, oldIndex) {
const parentWidth = this.$parent.$el.offsetWidth;
const length = this.$parent.items.length;
if (this.$parent.type !== 'card' && oldIndex !== undefined) {
this.animating = index === activeIndex || index === oldIndex;
}
if (index !== activeIndex && length > 2 && this.$parent.loop) {
index = this.processIndex(index, activeIndex, length);
}
if (this.$parent.type === 'card') {
this.inStage = Math.round(Math.abs(index - activeIndex)) <= 1;
this.active = index === activeIndex;
this.translate = this.calculateTranslate(index, activeIndex, parentWidth);
this.scale = this.active ? 1 : CARD_SCALE;
} else {
this.active = index === activeIndex;
this.translate = parentWidth * (index - activeIndex);
}
this.ready = true;
},
handleItemClick() {
const parent = this.$parent;
if (parent && parent.type === 'card') {
const index = parent.items.indexOf(this);
parent.setActiveItem(index);
}
}
},
created() {
this.$parent && this.$parent.updateItems();
},
destroyed() {
this.$parent && this.$parent.updateItems();
}
};
</script>
+266
View File
@@ -0,0 +1,266 @@
<template>
<div
class="el-carousel"
:class="{ 'el-carousel--card': type === 'card' }"
@mouseenter.stop="handleMouseEnter"
@mouseleave.stop="handleMouseLeave">
<div
class="el-carousel__container"
:style="{ height: height }">
<transition name="carousel-arrow-left">
<button
type="button"
v-if="arrow !== 'never'"
v-show="(arrow === 'always' || hover) && (loop || activeIndex > 0)"
@mouseenter="handleButtonEnter('left')"
@mouseleave="handleButtonLeave"
@click.stop="throttledArrowClick(activeIndex - 1)"
class="el-carousel__arrow el-carousel__arrow--left">
<i class="el-icon-arrow-left"></i>
</button>
</transition>
<transition name="carousel-arrow-right">
<button
type="button"
v-if="arrow !== 'never'"
v-show="(arrow === 'always' || hover) && (loop || activeIndex < items.length - 1)"
@mouseenter="handleButtonEnter('right')"
@mouseleave="handleButtonLeave"
@click.stop="throttledArrowClick(activeIndex + 1)"
class="el-carousel__arrow el-carousel__arrow--right">
<i class="el-icon-arrow-right"></i>
</button>
</transition>
<slot></slot>
</div>
<ul
class="el-carousel__indicators"
v-if="indicatorPosition !== 'none'"
:class="{ 'el-carousel__indicators--labels': hasLabel, 'el-carousel__indicators--outside': indicatorPosition === 'outside' || type === 'card' }">
<li
v-for="(item, index) in items"
class="el-carousel__indicator"
:class="{ 'is-active': index === activeIndex }"
@mouseenter="throttledIndicatorHover(index)"
@click.stop="handleIndicatorClick(index)">
<button class="el-carousel__button"><span v-if="hasLabel">{{ item.label }}</span></button>
</li>
</ul>
</div>
</template>
<script>
import throttle from 'throttle-debounce/throttle';
import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event';
export default {
name: 'ElCarousel',
props: {
initialIndex: {
type: Number,
default: 0
},
height: String,
trigger: {
type: String,
default: 'hover'
},
autoplay: {
type: Boolean,
default: true
},
interval: {
type: Number,
default: 3000
},
indicatorPosition: String,
indicator: {
type: Boolean,
default: true
},
arrow: {
type: String,
default: 'hover'
},
type: String,
loop: {
type: Boolean,
default: true
}
},
data() {
return {
items: [],
activeIndex: -1,
containerWidth: 0,
timer: null,
hover: false
};
},
computed: {
hasLabel() {
return this.items.some(item => item.label.toString().length > 0);
}
},
watch: {
items(val) {
if (val.length > 0) this.setActiveItem(this.initialIndex);
},
activeIndex(val, oldVal) {
this.resetItemPosition(oldVal);
this.$emit('change', val, oldVal);
},
autoplay(val) {
val ? this.startTimer() : this.pauseTimer();
},
loop() {
this.setActiveItem(this.activeIndex);
}
},
methods: {
handleMouseEnter() {
this.hover = true;
this.pauseTimer();
},
handleMouseLeave() {
this.hover = false;
this.startTimer();
},
itemInStage(item, index) {
const length = this.items.length;
if (index === length - 1 && item.inStage && this.items[0].active ||
(item.inStage && this.items[index + 1] && this.items[index + 1].active)) {
return 'left';
} else if (index === 0 && item.inStage && this.items[length - 1].active ||
(item.inStage && this.items[index - 1] && this.items[index - 1].active)) {
return 'right';
}
return false;
},
handleButtonEnter(arrow) {
this.items.forEach((item, index) => {
if (arrow === this.itemInStage(item, index)) {
item.hover = true;
}
});
},
handleButtonLeave() {
this.items.forEach(item => {
item.hover = false;
});
},
updateItems() {
this.items = this.$children.filter(child => child.$options.name === 'ElCarouselItem');
},
resetItemPosition(oldIndex) {
this.items.forEach((item, index) => {
item.translateItem(index, this.activeIndex, oldIndex);
});
},
playSlides() {
if (this.activeIndex < this.items.length - 1) {
this.activeIndex++;
} else if (this.loop) {
this.activeIndex = 0;
}
},
pauseTimer() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
},
startTimer() {
if (this.interval <= 0 || !this.autoplay || this.timer) return;
this.timer = setInterval(this.playSlides, this.interval);
},
setActiveItem(index) {
if (typeof index === 'string') {
const filteredItems = this.items.filter(item => item.name === index);
if (filteredItems.length > 0) {
index = this.items.indexOf(filteredItems[0]);
}
}
index = Number(index);
if (isNaN(index) || index !== Math.floor(index)) {
process.env.NODE_ENV !== 'production' &&
console.warn('[Element Warn][Carousel]index must be an integer.');
return;
}
let length = this.items.length;
const oldIndex = this.activeIndex;
if (index < 0) {
this.activeIndex = this.loop ? length - 1 : 0;
} else if (index >= length) {
this.activeIndex = this.loop ? 0 : length - 1;
} else {
this.activeIndex = index;
}
if (oldIndex === this.activeIndex) {
this.resetItemPosition(oldIndex);
}
},
prev() {
this.setActiveItem(this.activeIndex - 1);
},
next() {
this.setActiveItem(this.activeIndex + 1);
},
handleIndicatorClick(index) {
this.activeIndex = index;
},
handleIndicatorHover(index) {
if (this.trigger === 'hover' && index !== this.activeIndex) {
this.activeIndex = index;
}
}
},
created() {
this.throttledArrowClick = throttle(300, true, index => {
this.setActiveItem(index);
});
this.throttledIndicatorHover = throttle(300, index => {
this.handleIndicatorHover(index);
});
},
mounted() {
this.updateItems();
this.$nextTick(() => {
addResizeListener(this.$el, this.resetItemPosition);
if (this.initialIndex < this.items.length && this.initialIndex >= 0) {
this.activeIndex = this.initialIndex;
}
this.startTimer();
});
},
beforeDestroy() {
if (this.$el) removeResizeListener(this.$el, this.resetItemPosition);
this.pauseTimer();
}
};
</script>
+8
View File
@@ -0,0 +1,8 @@
import Cascader from './src/main';
/* istanbul ignore next */
Cascader.install = function(Vue) {
Vue.component(Cascader.name, Cascader);
};
export default Cascader;
+456
View File
@@ -0,0 +1,456 @@
<template>
<span
class="el-cascader"
:class="[
{
'is-opened': menuVisible,
'is-disabled': cascaderDisabled
},
cascaderSize ? 'el-cascader--' + cascaderSize : ''
]"
@click="handleClick"
@mouseenter="inputHover = true"
@focus="inputHover = true"
@mouseleave="inputHover = false"
@blur="inputHover = false"
ref="reference"
v-clickoutside="handleClickoutside"
@keydown="handleKeydown"
>
<el-input
ref="input"
:readonly="readonly"
:placeholder="currentLabels.length ? undefined : placeholder"
v-model="inputValue"
@input="debouncedInputChange"
@focus="handleFocus"
@blur="handleBlur"
@compositionstart.native="handleComposition"
@compositionend.native="handleComposition"
:validate-event="false"
:size="size"
:disabled="cascaderDisabled"
:class="{ 'is-focus': menuVisible }"
>
<template slot="suffix">
<i
key="1"
v-if="clearable && inputHover && currentLabels.length"
class="el-input__icon el-icon-circle-close el-cascader__clearIcon"
@click="clearValue"
></i>
<i
key="2"
v-else
class="el-input__icon el-icon-arrow-down"
:class="{ 'is-reverse': menuVisible }"
></i>
</template>
</el-input>
<span class="el-cascader__label" v-show="inputValue === '' && !isOnComposition">
<template v-if="showAllLevels">
<template v-for="(label, index) in currentLabels">
{{ label }}
<span v-if="index < currentLabels.length - 1" :key="index"> {{ separator }} </span>
</template>
</template>
<template v-else>
{{ currentLabels[currentLabels.length - 1] }}
</template>
</span>
</span>
</template>
<script>
import Vue from 'vue';
import ElCascaderMenu from './menu';
import ElInput from 'element-ui/packages/input';
import Popper from 'element-ui/src/utils/vue-popper';
import Clickoutside from 'element-ui/src/utils/clickoutside';
import emitter from 'element-ui/src/mixins/emitter';
import Locale from 'element-ui/src/mixins/locale';
import { t } from 'element-ui/src/locale';
import debounce from 'throttle-debounce/debounce';
import { generateId, escapeRegexpString, isIE, isEdge } from 'element-ui/src/utils/util';
const popperMixin = {
props: {
placement: {
type: String,
default: 'bottom-start'
},
appendToBody: Popper.props.appendToBody,
arrowOffset: Popper.props.arrowOffset,
offset: Popper.props.offset,
boundariesPadding: Popper.props.boundariesPadding,
popperOptions: Popper.props.popperOptions
},
methods: Popper.methods,
data: Popper.data,
beforeDestroy: Popper.beforeDestroy
};
export default {
name: 'ElCascader',
directives: { Clickoutside },
mixins: [popperMixin, emitter, Locale],
inject: {
elForm: {
default: ''
},
elFormItem: {
default: ''
}
},
components: {
ElInput
},
props: {
options: {
type: Array,
required: true
},
props: {
type: Object,
default() {
return {
children: 'children',
label: 'label',
value: 'value',
disabled: 'disabled'
};
}
},
value: {
type: Array,
default() {
return [];
}
},
separator: {
type: String,
default: '/'
},
placeholder: {
type: String,
default() {
return t('el.cascader.placeholder');
}
},
disabled: Boolean,
clearable: {
type: Boolean,
default: false
},
changeOnSelect: Boolean,
popperClass: String,
expandTrigger: {
type: String,
default: 'click'
},
filterable: Boolean,
size: String,
showAllLevels: {
type: Boolean,
default: true
},
debounce: {
type: Number,
default: 300
},
beforeFilter: {
type: Function,
default: () => (() => {})
},
hoverThreshold: {
type: Number,
default: 500
}
},
data() {
return {
currentValue: this.value || [],
menu: null,
debouncedInputChange() {},
menuVisible: false,
inputHover: false,
inputValue: '',
flatOptions: null,
id: generateId(),
needFocus: true,
isOnComposition: false
};
},
computed: {
labelKey() {
return this.props.label || 'label';
},
valueKey() {
return this.props.value || 'value';
},
childrenKey() {
return this.props.children || 'children';
},
disabledKey() {
return this.props.disabled || 'disabled';
},
currentLabels() {
let options = this.options;
let labels = [];
this.currentValue.forEach(value => {
const targetOption = options && options.filter(option => option[this.valueKey] === value)[0];
if (targetOption) {
labels.push(targetOption[this.labelKey]);
options = targetOption[this.childrenKey];
}
});
return labels;
},
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
cascaderSize() {
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
},
cascaderDisabled() {
return this.disabled || (this.elForm || {}).disabled;
},
readonly() {
return !this.filterable || (!isIE() && !isEdge() && !this.menuVisible);
}
},
watch: {
menuVisible(value) {
this.$refs.input.$refs.input.setAttribute('aria-expanded', value);
value ? this.showMenu() : this.hideMenu();
this.$emit('visible-change', value);
},
value(value) {
this.currentValue = value;
},
currentValue(value) {
this.dispatch('ElFormItem', 'el.form.change', [value]);
},
currentLabels(value) {
const inputLabel = this.showAllLevels ? value.join('/') : value[value.length - 1] ;
this.$refs.input.$refs.input.setAttribute('value', inputLabel);
},
options: {
deep: true,
handler(value) {
if (!this.menu) {
this.initMenu();
}
this.flatOptions = this.flattenOptions(this.options);
this.menu.options = value;
}
}
},
methods: {
initMenu() {
this.menu = new Vue(ElCascaderMenu).$mount();
this.menu.options = this.options;
this.menu.props = this.props;
this.menu.expandTrigger = this.expandTrigger;
this.menu.changeOnSelect = this.changeOnSelect;
this.menu.popperClass = this.popperClass;
this.menu.hoverThreshold = this.hoverThreshold;
this.popperElm = this.menu.$el;
this.menu.$refs.menus[0].setAttribute('id', `cascader-menu-${this.id}`);
this.menu.$on('pick', this.handlePick);
this.menu.$on('activeItemChange', this.handleActiveItemChange);
this.menu.$on('menuLeave', this.doDestroy);
this.menu.$on('closeInside', this.handleClickoutside);
},
showMenu() {
if (!this.menu) {
this.initMenu();
}
this.menu.value = this.currentValue.slice(0);
this.menu.visible = true;
this.menu.options = this.options;
this.$nextTick(_ => {
this.updatePopper();
this.menu.inputWidth = this.$refs.input.$el.offsetWidth - 2;
});
},
hideMenu() {
this.inputValue = '';
this.menu.visible = false;
if (this.needFocus) {
this.$refs.input.focus();
} else {
this.needFocus = true;
}
},
handleActiveItemChange(value) {
this.$nextTick(_ => {
this.updatePopper();
});
this.$emit('active-item-change', value);
},
handleKeydown(e) {
const keyCode = e.keyCode;
if (keyCode === 13) {
this.handleClick();
} else if (keyCode === 40) { // down
this.menuVisible = true; // 打开
setTimeout(() => {
const firstMenu = this.popperElm.querySelectorAll('.el-cascader-menu')[0];
firstMenu.querySelectorAll("[tabindex='-1']")[0].focus();
});
e.stopPropagation();
e.preventDefault();
} else if (keyCode === 27 || keyCode === 9) { // esc tab
this.inputValue = '';
if (this.menu) this.menu.visible = false;
}
},
handlePick(value, close = true) {
this.currentValue = value;
this.$emit('input', value);
this.$emit('change', value);
if (close) {
this.menuVisible = false;
} else {
this.$nextTick(this.updatePopper);
}
},
handleInputChange(value) {
if (!this.menuVisible) return;
const flatOptions = this.flatOptions;
if (!value) {
this.menu.options = this.options;
this.$nextTick(this.updatePopper);
return;
}
let filteredFlatOptions = flatOptions.filter(optionsStack => {
return optionsStack.some(option => new RegExp(escapeRegexpString(value), 'i')
.test(option[this.labelKey]));
});
if (filteredFlatOptions.length > 0) {
filteredFlatOptions = filteredFlatOptions.map(optionStack => {
return {
__IS__FLAT__OPTIONS: true,
value: optionStack.map(item => item[this.valueKey]),
label: this.renderFilteredOptionLabel(value, optionStack),
disabled: optionStack.some(item => item[this.disabledKey])
};
});
} else {
filteredFlatOptions = [{
__IS__FLAT__OPTIONS: true,
label: this.t('el.cascader.noMatch'),
value: '',
disabled: true
}];
}
this.menu.options = filteredFlatOptions;
this.$nextTick(this.updatePopper);
},
renderFilteredOptionLabel(inputValue, optionsStack) {
return optionsStack.map((option, index) => {
const label = option[this.labelKey];
const keywordIndex = label.toLowerCase().indexOf(inputValue.toLowerCase());
const labelPart = label.slice(keywordIndex, inputValue.length + keywordIndex);
const node = keywordIndex > -1 ? this.highlightKeyword(label, labelPart) : label;
return index === 0 ? node : [` ${this.separator} `, node];
});
},
highlightKeyword(label, keyword) {
const h = this._c;
return label.split(keyword)
.map((node, index) => index === 0 ? node : [
h('span', { class: { 'el-cascader-menu__item__keyword': true }}, [this._v(keyword)]),
node
]);
},
flattenOptions(options, ancestor = []) {
let flatOptions = [];
options.forEach((option) => {
const optionsStack = ancestor.concat(option);
if (!option[this.childrenKey]) {
flatOptions.push(optionsStack);
} else {
if (this.changeOnSelect) {
flatOptions.push(optionsStack);
}
flatOptions = flatOptions.concat(this.flattenOptions(option[this.childrenKey], optionsStack));
}
});
return flatOptions;
},
clearValue(ev) {
ev.stopPropagation();
this.handlePick([], true);
},
handleClickoutside(pickFinished = false) {
if (this.menuVisible && !pickFinished) {
this.needFocus = false;
}
this.menuVisible = false;
},
handleClick() {
if (this.cascaderDisabled) return;
this.$refs.input.focus();
if (this.filterable) {
this.menuVisible = true;
return;
}
this.menuVisible = !this.menuVisible;
},
handleFocus(event) {
this.$emit('focus', event);
},
handleBlur(event) {
this.$emit('blur', event);
},
handleComposition(event) {
this.isOnComposition = event.type !== 'compositionend';
}
},
created() {
this.debouncedInputChange = debounce(this.debounce, value => {
const before = this.beforeFilter(value);
if (before && before.then) {
this.menu.options = [{
__IS__FLAT__OPTIONS: true,
label: this.t('el.cascader.loading'),
value: '',
disabled: true
}];
before
.then(() => {
this.$nextTick(() => {
this.handleInputChange(value);
});
});
} else if (before !== false) {
this.$nextTick(() => {
this.handleInputChange(value);
});
}
});
},
mounted() {
this.flatOptions = this.flattenOptions(this.options);
}
};
</script>
+375
View File
@@ -0,0 +1,375 @@
<script>
import { isDef } from 'element-ui/src/utils/shared';
import scrollIntoView from 'element-ui/src/utils/scroll-into-view';
import { generateId } from 'element-ui/src/utils/util';
const copyArray = (arr, props) => {
if (!arr || !Array.isArray(arr) || !props) return arr;
const result = [];
const configurableProps = ['__IS__FLAT__OPTIONS', 'label', 'value', 'disabled'];
const childrenProp = props.children || 'children';
arr.forEach(item => {
const itemCopy = {};
configurableProps.forEach(prop => {
let name = props[prop];
let value = item[name];
if (value === undefined) {
name = prop;
value = item[name];
}
if (value !== undefined) itemCopy[name] = value;
});
if (Array.isArray(item[childrenProp])) {
itemCopy[childrenProp] = copyArray(item[childrenProp], props);
}
result.push(itemCopy);
});
return result;
};
export default {
name: 'ElCascaderMenu',
data() {
return {
inputWidth: 0,
options: [],
props: {},
visible: false,
activeValue: [],
value: [],
expandTrigger: 'click',
changeOnSelect: false,
popperClass: '',
hoverTimer: 0,
clicking: false,
id: generateId()
};
},
watch: {
visible(value) {
if (value) {
this.activeValue = this.value;
}
},
value: {
immediate: true,
handler(value) {
this.activeValue = value;
}
}
},
computed: {
activeOptions: {
get() {
const activeValue = this.activeValue;
const configurableProps = ['label', 'value', 'children', 'disabled'];
const formatOptions = options => {
options.forEach(option => {
if (option.__IS__FLAT__OPTIONS) return;
configurableProps.forEach(prop => {
const value = option[this.props[prop] || prop];
if (value !== undefined) option[prop] = value;
});
if (Array.isArray(option.children)) {
formatOptions(option.children);
}
});
};
const loadActiveOptions = (options, activeOptions = []) => {
const level = activeOptions.length;
activeOptions[level] = options;
let active = activeValue[level];
if (isDef(active)) {
options = options.filter(option => option.value === active)[0];
if (options && options.children) {
loadActiveOptions(options.children, activeOptions);
}
}
return activeOptions;
};
const optionsCopy = copyArray(this.options, this.props);
formatOptions(optionsCopy);
return loadActiveOptions(optionsCopy);
}
}
},
methods: {
select(item, menuIndex) {
if (item.__IS__FLAT__OPTIONS) {
this.activeValue = item.value;
} else if (menuIndex) {
this.activeValue.splice(menuIndex, this.activeValue.length - 1, item.value);
} else {
this.activeValue = [item.value];
}
this.$emit('pick', this.activeValue.slice());
},
handleMenuLeave() {
this.$emit('menuLeave');
},
activeItem(item, menuIndex) {
const len = this.activeOptions.length;
this.activeValue.splice(menuIndex, len, item.value);
this.activeOptions.splice(menuIndex + 1, len, item.children);
if (this.changeOnSelect) {
this.$emit('pick', this.activeValue.slice(), false);
} else {
this.$emit('activeItemChange', this.activeValue);
}
},
scrollMenu(menu) {
scrollIntoView(menu, menu.getElementsByClassName('is-active')[0]);
},
handleMenuEnter() {
this.$nextTick(() => this.$refs.menus.forEach(menu => this.scrollMenu(menu)));
}
},
render(h) {
const {
activeValue,
activeOptions,
visible,
expandTrigger,
popperClass,
hoverThreshold
} = this;
let itemId = null;
let itemIndex = 0;
let hoverMenuRefs = {};
const hoverMenuHandler = e => {
const activeMenu = hoverMenuRefs.activeMenu;
if (!activeMenu) return;
const offsetX = e.offsetX;
const width = activeMenu.offsetWidth;
const height = activeMenu.offsetHeight;
if (e.target === hoverMenuRefs.activeItem) {
clearTimeout(this.hoverTimer);
const {activeItem} = hoverMenuRefs;
const offsetY_top = activeItem.offsetTop;
const offsetY_Bottom = offsetY_top + activeItem.offsetHeight;
hoverMenuRefs.hoverZone.innerHTML = `
<path style="pointer-events: auto;" fill="transparent" d="M${offsetX} ${offsetY_top} L${width} 0 V${offsetY_top} Z" />
<path style="pointer-events: auto;" fill="transparent" d="M${offsetX} ${offsetY_Bottom} L${width} ${height} V${offsetY_Bottom} Z" />
`;
} else {
if (!this.hoverTimer) {
this.hoverTimer = setTimeout(() => {
hoverMenuRefs.hoverZone.innerHTML = '';
}, hoverThreshold);
}
}
};
const menus = this._l(activeOptions, (menu, menuIndex) => {
let isFlat = false;
const menuId = `menu-${this.id}-${ menuIndex}`;
const ownsId = `menu-${this.id}-${ menuIndex + 1 }`;
const items = this._l(menu, item => {
const events = {
on: {}
};
if (item.__IS__FLAT__OPTIONS) isFlat = true;
if (!item.disabled) {
// keydown up/down/left/right/enter
events.on.keydown = (ev) => {
const keyCode = ev.keyCode;
if ([37, 38, 39, 40, 13, 9, 27].indexOf(keyCode) < 0) {
return;
}
const currentEle = ev.target;
const parentEle = this.$refs.menus[menuIndex];
const menuItemList = parentEle.querySelectorAll("[tabindex='-1']");
const currentIndex = Array.prototype.indexOf.call(menuItemList, currentEle); // 当前索引
let nextIndex, nextMenu;
if ([38, 40].indexOf(keyCode) > -1) {
if (keyCode === 38) { // up键
nextIndex = currentIndex !== 0 ? (currentIndex - 1) : currentIndex;
} else if (keyCode === 40) { // down
nextIndex = currentIndex !== (menuItemList.length - 1) ? currentIndex + 1 : currentIndex;
}
menuItemList[nextIndex].focus();
} else if (keyCode === 37) { // left键
if (menuIndex !== 0) {
const previousMenu = this.$refs.menus[menuIndex - 1];
previousMenu.querySelector('[aria-expanded=true]').focus();
}
} else if (keyCode === 39) { // right
if (item.children) {
// 有子menu 选择子menu的第一个menuitem
nextMenu = this.$refs.menus[menuIndex + 1];
nextMenu.querySelectorAll("[tabindex='-1']")[0].focus();
}
} else if (keyCode === 13) {
if (!item.children) {
const id = currentEle.getAttribute('id');
parentEle.setAttribute('aria-activedescendant', id);
this.select(item, menuIndex);
this.$nextTick(() => this.scrollMenu(this.$refs.menus[menuIndex]));
}
} else if (keyCode === 9 || keyCode === 27) { // esc tab
this.$emit('closeInside');
}
};
if (item.children) {
let triggerEvent = {
click: 'click',
hover: 'mouseenter'
}[expandTrigger];
const triggerHandler = () => {
if (this.visible) {
this.activeItem(item, menuIndex);
this.$nextTick(() => {
// adjust self and next level
this.scrollMenu(this.$refs.menus[menuIndex]);
this.scrollMenu(this.$refs.menus[menuIndex + 1]);
});
}
};
events.on[triggerEvent] = triggerHandler;
if (triggerEvent === 'mouseenter' && this.changeOnSelect) {
events.on.click = () => {
if (this.activeValue.indexOf(item.value) !== -1) {
this.$emit('closeInside', true);
}
};
}
events.on['mousedown'] = () => {
this.clicking = true;
};
events.on['focus'] = () => { // focus 选中
if (this.clicking) {
this.clicking = false;
return;
}
triggerHandler();
};
} else {
events.on.click = () => {
this.select(item, menuIndex);
this.$nextTick(() => this.scrollMenu(this.$refs.menus[menuIndex]));
};
}
}
if (!item.disabled && !item.children) { // no children set id
itemId = `${menuId}-${itemIndex}`;
itemIndex++;
}
return (
<li
class={{
'el-cascader-menu__item': true,
'el-cascader-menu__item--extensible': item.children,
'is-active': item.value === activeValue[menuIndex],
'is-disabled': item.disabled
}}
ref={item.value === activeValue[menuIndex] ? 'activeItem' : null}
{...events}
tabindex= { item.disabled ? null : -1 }
role="menuitem"
aria-haspopup={ !!item.children }
aria-expanded={ item.value === activeValue[menuIndex] }
id = { itemId }
aria-owns = { !item.children ? null : ownsId }
>
<span>{item.label}</span>
</li>
);
});
let menuStyle = {};
if (isFlat) {
menuStyle.minWidth = this.inputWidth + 'px';
}
const isHoveredMenu = expandTrigger === 'hover' && activeValue.length - 1 === menuIndex;
const hoverMenuEvent = {
on: {
}
};
if (isHoveredMenu) {
hoverMenuEvent.on.mousemove = hoverMenuHandler;
menuStyle.position = 'relative';
}
return (
<ul
class={{
'el-cascader-menu': true,
'el-cascader-menu--flexible': isFlat
}}
{...hoverMenuEvent}
style={menuStyle}
refInFor
ref="menus"
role="menu"
id = { menuId }
>
{items}
{
isHoveredMenu
? (<svg
ref="hoverZone"
style={{
position: 'absolute',
top: 0,
height: '100%',
width: '100%',
left: 0,
pointerEvents: 'none'
}}
></svg>) : null
}
</ul>
);
});
if (expandTrigger === 'hover') {
this.$nextTick(() => {
const activeItem = this.$refs.activeItem;
if (activeItem) {
const activeMenu = activeItem.parentElement;
const hoverZone = this.$refs.hoverZone;
hoverMenuRefs = {
activeMenu,
activeItem,
hoverZone
};
} else {
hoverMenuRefs = {};
}
});
}
return (
<transition name="el-zoom-in-top" on-before-enter={this.handleMenuEnter} on-after-leave={this.handleMenuLeave}>
<div
v-show={visible}
class={[
'el-cascader-menus el-popper',
popperClass
]}
ref="wrapper"
>
<div x-arrow class="popper__arrow"></div>
{menus}
</div>
</transition>
);
}
};
</script>
+8
View File
@@ -0,0 +1,8 @@
import ElCheckboxButton from '../checkbox/src/checkbox-button.vue';
/* istanbul ignore next */
ElCheckboxButton.install = function(Vue) {
Vue.component(ElCheckboxButton.name, ElCheckboxButton);
};
export default ElCheckboxButton;
+8
View File
@@ -0,0 +1,8 @@
import ElCheckboxGroup from '../checkbox/src/checkbox-group.vue';
/* istanbul ignore next */
ElCheckboxGroup.install = function(Vue) {
Vue.component(ElCheckboxGroup.name, ElCheckboxGroup);
};
export default ElCheckboxGroup;
+8
View File
@@ -0,0 +1,8 @@
import ElCheckbox from './src/checkbox';
/* istanbul ignore next */
ElCheckbox.install = function(Vue) {
Vue.component(ElCheckbox.name, ElCheckbox);
};
export default ElCheckbox;
+191
View File
@@ -0,0 +1,191 @@
<template>
<label
class="el-checkbox-button"
:class="[
size ? 'el-checkbox-button--' + size : '',
{ 'is-disabled': isDisabled },
{ 'is-checked': isChecked },
{ 'is-focus': focus },
]"
role="checkbox"
:aria-checked="isChecked"
:aria-disabled="isDisabled"
>
<input
v-if="trueLabel || falseLabel"
class="el-checkbox-button__original"
type="checkbox"
:name="name"
:disabled="isDisabled"
:true-value="trueLabel"
:false-value="falseLabel"
v-model="model"
@change="handleChange"
@focus="focus = true"
@blur="focus = false">
<input
v-else
class="el-checkbox-button__original"
type="checkbox"
:name="name"
:disabled="isDisabled"
:value="label"
v-model="model"
@change="handleChange"
@focus="focus = true"
@blur="focus = false">
<span class="el-checkbox-button__inner"
v-if="$slots.default || label"
:style="isChecked ? activeStyle : null">
<slot>{{label}}</slot>
</span>
</label>
</template>
<script>
import Emitter from 'element-ui/src/mixins/emitter';
export default {
name: 'ElCheckboxButton',
mixins: [Emitter],
inject: {
elForm: {
default: ''
},
elFormItem: {
default: ''
}
},
data() {
return {
selfModel: false,
focus: false,
isLimitExceeded: false
};
},
props: {
value: {},
label: {},
disabled: Boolean,
checked: Boolean,
name: String,
trueLabel: [String, Number],
falseLabel: [String, Number]
},
computed: {
model: {
get() {
return this._checkboxGroup
? this.store : this.value !== undefined
? this.value : this.selfModel;
},
set(val) {
if (this._checkboxGroup) {
this.isLimitExceeded = false;
(this._checkboxGroup.min !== undefined &&
val.length < this._checkboxGroup.min &&
(this.isLimitExceeded = true));
(this._checkboxGroup.max !== undefined &&
val.length > this._checkboxGroup.max &&
(this.isLimitExceeded = true));
this.isLimitExceeded === false &&
this.dispatch('ElCheckboxGroup', 'input', [val]);
} else if (this.value !== undefined) {
this.$emit('input', val);
} else {
this.selfModel = val;
}
}
},
isChecked() {
if ({}.toString.call(this.model) === '[object Boolean]') {
return this.model;
} else if (Array.isArray(this.model)) {
return this.model.indexOf(this.label) > -1;
} else if (this.model !== null && this.model !== undefined) {
return this.model === this.trueLabel;
}
},
_checkboxGroup() {
let parent = this.$parent;
while (parent) {
if (parent.$options.componentName !== 'ElCheckboxGroup') {
parent = parent.$parent;
} else {
return parent;
}
}
return false;
},
store() {
return this._checkboxGroup ? this._checkboxGroup.value : this.value;
},
activeStyle() {
return {
backgroundColor: this._checkboxGroup.fill || '',
borderColor: this._checkboxGroup.fill || '',
color: this._checkboxGroup.textColor || '',
'box-shadow': '-1px 0 0 0 ' + this._checkboxGroup.fill
};
},
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
size() {
return this._checkboxGroup.checkboxGroupSize || this._elFormItemSize || (this.$ELEMENT || {}).size;
},
isDisabled() {
return this._checkboxGroup
? this._checkboxGroup.disabled || this.disabled || (this.elForm || {}).disabled
: this.disabled || (this.elForm || {}).disabled;
}
},
methods: {
addToStore() {
if (
Array.isArray(this.model) &&
this.model.indexOf(this.label) === -1
) {
this.model.push(this.label);
} else {
this.model = this.trueLabel || true;
}
},
handleChange(ev) {
if (this.isLimitExceeded) return;
let value;
if (ev.target.checked) {
value = this.trueLabel === undefined ? true : this.trueLabel;
} else {
value = this.falseLabel === undefined ? false : this.falseLabel;
}
this.$emit('change', value, ev);
this.$nextTick(() => {
if (this._checkboxGroup) {
this.dispatch('ElCheckboxGroup', 'change', [this._checkboxGroup.value]);
}
});
}
},
created() {
this.checked && this.addToStore();
}
};
</script>
+48
View File
@@ -0,0 +1,48 @@
<script>
import Emitter from 'element-ui/src/mixins/emitter';
export default {
name: 'ElCheckboxGroup',
componentName: 'ElCheckboxGroup',
mixins: [Emitter],
inject: {
elFormItem: {
default: ''
}
},
props: {
value: {},
disabled: Boolean,
min: Number,
max: Number,
size: String,
fill: String,
textColor: String
},
computed: {
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
checkboxGroupSize() {
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
}
},
watch: {
value(value) {
this.dispatch('ElFormItem', 'el.form.change', [value]);
}
}
};
</script>
<template>
<div class="el-checkbox-group" role="group" aria-label="checkbox-group">
<slot></slot>
</div>
</template>
+215
View File
@@ -0,0 +1,215 @@
<template>
<label
class="el-checkbox"
:class="[
border && checkboxSize ? 'el-checkbox--' + checkboxSize : '',
{ 'is-disabled': isDisabled },
{ 'is-bordered': border },
{ 'is-checked': isChecked }
]"
role="checkbox"
:aria-checked="indeterminate ? 'mixed': isChecked"
:aria-disabled="isDisabled"
:id="id"
>
<span class="el-checkbox__input"
:class="{
'is-disabled': isDisabled,
'is-checked': isChecked,
'is-indeterminate': indeterminate,
'is-focus': focus
}"
aria-checked="mixed"
>
<span class="el-checkbox__inner"></span>
<input
v-if="trueLabel || falseLabel"
class="el-checkbox__original"
type="checkbox"
aria-hidden="true"
:name="name"
:disabled="isDisabled"
:true-value="trueLabel"
:false-value="falseLabel"
v-model="model"
@change="handleChange"
@focus="focus = true"
@blur="focus = false">
<input
v-else
class="el-checkbox__original"
type="checkbox"
aria-hidden="true"
:disabled="isDisabled"
:value="label"
:name="name"
v-model="model"
@change="handleChange"
@focus="focus = true"
@blur="focus = false">
</span>
<span class="el-checkbox__label" v-if="$slots.default || label">
<slot></slot>
<template v-if="!$slots.default">{{label}}</template>
</span>
</label>
</template>
<script>
import Emitter from 'element-ui/src/mixins/emitter';
export default {
name: 'ElCheckbox',
mixins: [Emitter],
inject: {
elForm: {
default: ''
},
elFormItem: {
default: ''
}
},
componentName: 'ElCheckbox',
data() {
return {
selfModel: false,
focus: false,
isLimitExceeded: false
};
},
computed: {
model: {
get() {
return this.isGroup
? this.store : this.value !== undefined
? this.value : this.selfModel;
},
set(val) {
if (this.isGroup) {
this.isLimitExceeded = false;
(this._checkboxGroup.min !== undefined &&
val.length < this._checkboxGroup.min &&
(this.isLimitExceeded = true));
(this._checkboxGroup.max !== undefined &&
val.length > this._checkboxGroup.max &&
(this.isLimitExceeded = true));
this.isLimitExceeded === false &&
this.dispatch('ElCheckboxGroup', 'input', [val]);
} else {
this.$emit('input', val);
this.selfModel = val;
}
}
},
isChecked() {
if ({}.toString.call(this.model) === '[object Boolean]') {
return this.model;
} else if (Array.isArray(this.model)) {
return this.model.indexOf(this.label) > -1;
} else if (this.model !== null && this.model !== undefined) {
return this.model === this.trueLabel;
}
},
isGroup() {
let parent = this.$parent;
while (parent) {
if (parent.$options.componentName !== 'ElCheckboxGroup') {
parent = parent.$parent;
} else {
this._checkboxGroup = parent;
return true;
}
}
return false;
},
store() {
return this._checkboxGroup ? this._checkboxGroup.value : this.value;
},
isDisabled() {
return this.isGroup
? this._checkboxGroup.disabled || this.disabled || (this.elForm || {}).disabled
: this.disabled || (this.elForm || {}).disabled;
},
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
checkboxSize() {
const temCheckboxSize = this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
return this.isGroup
? this._checkboxGroup.checkboxGroupSize || temCheckboxSize
: temCheckboxSize;
}
},
props: {
value: {},
label: {},
indeterminate: Boolean,
disabled: Boolean,
checked: Boolean,
name: String,
trueLabel: [String, Number],
falseLabel: [String, Number],
id: String, /* 当indeterminate为真时,为controls提供相关连的checkbox的id,表明元素间的控制关系*/
controls: String, /* 当indeterminate为真时,为controls提供相关连的checkbox的id,表明元素间的控制关系*/
border: Boolean,
size: String
},
methods: {
addToStore() {
if (
Array.isArray(this.model) &&
this.model.indexOf(this.label) === -1
) {
this.model.push(this.label);
} else {
this.model = this.trueLabel || true;
}
},
handleChange(ev) {
if (this.isLimitExceeded) return;
let value;
if (ev.target.checked) {
value = this.trueLabel === undefined ? true : this.trueLabel;
} else {
value = this.falseLabel === undefined ? false : this.falseLabel;
}
this.$emit('change', value, ev);
this.$nextTick(() => {
if (this.isGroup) {
this.dispatch('ElCheckboxGroup', 'change', [this._checkboxGroup.value]);
}
});
}
},
created() {
this.checked && this.addToStore();
},
mounted() { // 为indeterminate元素 添加aria-controls 属性
if (this.indeterminate) {
this.$el.setAttribute('aria-controls', this.controls);
}
},
watch: {
value(value) {
this.dispatch('ElFormItem', 'el.form.change', value);
}
}
};
</script>
+9
View File
@@ -0,0 +1,9 @@
import ElCol from './src/col';
/* istanbul ignore next */
ElCol.install = function(Vue) {
Vue.component(ElCol.name, ElCol);
};
export default ElCol;
+71
View File
@@ -0,0 +1,71 @@
export default {
name: 'ElCol',
props: {
span: {
type: Number,
default: 24
},
tag: {
type: String,
default: 'div'
},
offset: Number,
pull: Number,
push: Number,
xs: [Number, Object],
sm: [Number, Object],
md: [Number, Object],
lg: [Number, Object],
xl: [Number, Object]
},
computed: {
gutter() {
let parent = this.$parent;
while (parent && parent.$options.componentName !== 'ElRow') {
parent = parent.$parent;
}
return parent ? parent.gutter : 0;
}
},
render(h) {
let classList = [];
let style = {};
if (this.gutter) {
style.paddingLeft = this.gutter / 2 + 'px';
style.paddingRight = style.paddingLeft;
}
['span', 'offset', 'pull', 'push'].forEach(prop => {
if (this[prop] || this[prop] === 0) {
classList.push(
prop !== 'span'
? `el-col-${prop}-${this[prop]}`
: `el-col-${this[prop]}`
);
}
});
['xs', 'sm', 'md', 'lg', 'xl'].forEach(size => {
if (typeof this[size] === 'number') {
classList.push(`el-col-${size}-${this[size]}`);
} else if (typeof this[size] === 'object') {
let props = this[size];
Object.keys(props).forEach(prop => {
classList.push(
prop !== 'span'
? `el-col-${size}-${prop}-${props[prop]}`
: `el-col-${size}-${props[prop]}`
);
});
}
});
return h(this.tag, {
class: ['el-col', classList],
style
}, this.$slots.default);
}
};
+8
View File
@@ -0,0 +1,8 @@
import ElCollapseItem from '../collapse/src/collapse-item.vue';
/* istanbul ignore next */
ElCollapseItem.install = function(Vue) {
Vue.component(ElCollapseItem.name, ElCollapseItem);
};
export default ElCollapseItem;
+9
View File
@@ -0,0 +1,9 @@
import ElCollapse from './src/collapse';
/* istanbul ignore next */
ElCollapse.install = function(Vue) {
Vue.component(ElCollapse.name, ElCollapse);
};
export default ElCollapse;
+113
View File
@@ -0,0 +1,113 @@
<template>
<div class="el-collapse-item" :class="{'is-active': isActive}">
<div
role="tab"
:aria-expanded="isActive"
:aria-controls="`el-collapse-content-${id}`"
:aria-describedby ="`el-collapse-content-${id}`"
>
<div
class="el-collapse-item__header"
@click="handleHeaderClick"
role="button"
:id="`el-collapse-head-${id}`"
tabindex="0"
@keyup.space.enter.stop="handleEnterClick"
:class="{
'focusing': focusing,
'is-active': isActive
}"
@focus="handleFocus"
@blur="focusing = false"
>
<slot name="title">{{title}}</slot>
<i
class="el-collapse-item__arrow el-icon-arrow-right"
:class="{'is-active': isActive}">
</i>
</div>
</div>
<el-collapse-transition>
<div
class="el-collapse-item__wrap"
v-show="isActive"
role="tabpanel"
:aria-hidden="!isActive"
:aria-labelledby="`el-collapse-head-${id}`"
:id="`el-collapse-content-${id}`"
>
<div class="el-collapse-item__content">
<slot></slot>
</div>
</div>
</el-collapse-transition>
</div>
</template>
<script>
import ElCollapseTransition from 'element-ui/src/transitions/collapse-transition';
import Emitter from 'element-ui/src/mixins/emitter';
import { generateId } from 'element-ui/src/utils/util';
export default {
name: 'ElCollapseItem',
componentName: 'ElCollapseItem',
mixins: [Emitter],
components: { ElCollapseTransition },
data() {
return {
contentWrapStyle: {
height: 'auto',
display: 'block'
},
contentHeight: 0,
focusing: false,
isClick: false
};
},
inject: ['collapse'],
props: {
title: String,
name: {
type: [String, Number],
default() {
return this._uid;
}
}
},
computed: {
isActive() {
return this.collapse.activeNames.indexOf(this.name) > -1;
},
id() {
return generateId();
}
},
methods: {
handleFocus() {
setTimeout(() => {
if (!this.isClick) {
this.focusing = true;
} else {
this.isClick = false;
}
}, 50);
},
handleHeaderClick() {
this.dispatch('ElCollapse', 'item-click', this);
this.focusing = false;
this.isClick = true;
},
handleEnterClick() {
this.dispatch('ElCollapse', 'item-click', this);
}
}
};
</script>
+73
View File
@@ -0,0 +1,73 @@
<template>
<div class="el-collapse" role="tablist" aria-multiselectable="true">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'ElCollapse',
componentName: 'ElCollapse',
props: {
accordion: Boolean,
value: {
type: [Array, String, Number],
default() {
return [];
}
}
},
data() {
return {
activeNames: [].concat(this.value)
};
},
provide() {
return {
collapse: this
};
},
watch: {
value(value) {
this.activeNames = [].concat(value);
}
},
methods: {
setActiveNames(activeNames) {
activeNames = [].concat(activeNames);
let value = this.accordion ? activeNames[0] : activeNames;
this.activeNames = activeNames;
this.$emit('input', value);
this.$emit('change', value);
},
handleItemClick(item) {
if (this.accordion) {
this.setActiveNames(
(this.activeNames[0] || this.activeNames[0] === 0) &&
this.activeNames[0] === item.name
? '' : item.name
);
} else {
let activeNames = this.activeNames.slice(0);
let index = activeNames.indexOf(item.name);
if (index > -1) {
activeNames.splice(index, 1);
} else {
activeNames.push(item.name);
}
this.setActiveNames(activeNames);
}
}
},
created() {
this.$on('item-click', this.handleItemClick);
}
};
</script>
+8
View File
@@ -0,0 +1,8 @@
import ColorPicker from './src/main';
/* istanbul ignore next */
ColorPicker.install = function(Vue) {
Vue.component(ColorPicker.name, ColorPicker);
};
export default ColorPicker;
+316
View File
@@ -0,0 +1,316 @@
const hsv2hsl = function(hue, sat, val) {
return [
hue,
(sat * val / ((hue = (2 - sat) * val) < 1 ? hue : 2 - hue)) || 0,
hue / 2
];
};
// Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
// <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
const isOnePointZero = function(n) {
return typeof n === 'string' && n.indexOf('.') !== -1 && parseFloat(n) === 1;
};
const isPercentage = function(n) {
return typeof n === 'string' && n.indexOf('%') !== -1;
};
// Take input from [0, n] and return it as [0, 1]
const bound01 = function(value, max) {
if (isOnePointZero(value)) value = '100%';
const processPercent = isPercentage(value);
value = Math.min(max, Math.max(0, parseFloat(value)));
// Automatically convert percentage into number
if (processPercent) {
value = parseInt(value * max, 10) / 100;
}
// Handle floating point rounding errors
if ((Math.abs(value - max) < 0.000001)) {
return 1;
}
// Convert into [0, 1] range if it isn't already
return (value % max) / parseFloat(max);
};
const INT_HEX_MAP = { 10: 'A', 11: 'B', 12: 'C', 13: 'D', 14: 'E', 15: 'F' };
const toHex = function({ r, g, b }) {
const hexOne = function(value) {
value = Math.min(Math.round(value), 255);
const high = Math.floor(value / 16);
const low = value % 16;
return '' + (INT_HEX_MAP[high] || high) + (INT_HEX_MAP[low] || low);
};
if (isNaN(r) || isNaN(g) || isNaN(b)) return '';
return '#' + hexOne(r) + hexOne(g) + hexOne(b);
};
const HEX_INT_MAP = { A: 10, B: 11, C: 12, D: 13, E: 14, F: 15 };
const parseHexChannel = function(hex) {
if (hex.length === 2) {
return (HEX_INT_MAP[hex[0].toUpperCase()] || +hex[0]) * 16 + (HEX_INT_MAP[hex[1].toUpperCase()] || +hex[1]);
}
return HEX_INT_MAP[hex[1].toUpperCase()] || +hex[1];
};
const hsl2hsv = function(hue, sat, light) {
sat = sat / 100;
light = light / 100;
let smin = sat;
const lmin = Math.max(light, 0.01);
let sv;
let v;
light *= 2;
sat *= (light <= 1) ? light : 2 - light;
smin *= lmin <= 1 ? lmin : 2 - lmin;
v = (light + sat) / 2;
sv = light === 0 ? (2 * smin) / (lmin + smin) : (2 * sat) / (light + sat);
return {
h: hue,
s: sv * 100,
v: v * 100
};
};
// `rgbToHsv`
// Converts an RGB color value to HSV
// *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1]
// *Returns:* { h, s, v } in [0,1]
const rgb2hsv = function(r, g, b) {
r = bound01(r, 255);
g = bound01(g, 255);
b = bound01(b, 255);
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h, s;
let v = max;
const d = max - min;
s = max === 0 ? 0 : d / max;
if (max === min) {
h = 0; // achromatic
} else {
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return { h: h * 360, s: s * 100, v: v * 100 };
};
// `hsvToRgb`
// Converts an HSV color value to RGB.
// *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100]
// *Returns:* { r, g, b } in the set [0, 255]
const hsv2rgb = function(h, s, v) {
h = bound01(h, 360) * 6;
s = bound01(s, 100);
v = bound01(v, 100);
const i = Math.floor(h);
const f = h - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
const mod = i % 6;
const r = [v, q, p, p, t, v][mod];
const g = [t, v, v, q, p, p][mod];
const b = [p, p, t, v, v, q][mod];
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
};
};
export default class Color {
constructor(options) {
this._hue = 0;
this._saturation = 100;
this._value = 100;
this._alpha = 100;
this.enableAlpha = false;
this.format = 'hex';
this.value = '';
options = options || {};
for (let option in options) {
if (options.hasOwnProperty(option)) {
this[option] = options[option];
}
}
this.doOnChange();
}
set(prop, value) {
if (arguments.length === 1 && typeof prop === 'object') {
for (let p in prop) {
if (prop.hasOwnProperty(p)) {
this.set(p, prop[p]);
}
}
return;
}
this['_' + prop] = value;
this.doOnChange();
}
get(prop) {
return this['_' + prop];
}
toRgb() {
return hsv2rgb(this._hue, this._saturation, this._value);
}
fromString(value) {
if (!value) {
this._hue = 0;
this._saturation = 100;
this._value = 100;
this.doOnChange();
return;
}
const fromHSV = (h, s, v) => {
this._hue = Math.max(0, Math.min(360, h));
this._saturation = Math.max(0, Math.min(100, s));
this._value = Math.max(0, Math.min(100, v));
this.doOnChange();
};
if (value.indexOf('hsl') !== -1) {
const parts = value.replace(/hsla|hsl|\(|\)/gm, '')
.split(/\s|,/g).filter((val) => val !== '').map((val, index) => index > 2 ? parseFloat(val) : parseInt(val, 10));
if (parts.length === 4) {
this._alpha = Math.floor(parseFloat(parts[3]) * 100);
} else if (parts.length === 3) {
this._alpha = 100;
}
if (parts.length >= 3) {
const { h, s, v } = hsl2hsv(parts[0], parts[1], parts[2]);
fromHSV(h, s, v);
}
} else if (value.indexOf('hsv') !== -1) {
const parts = value.replace(/hsva|hsv|\(|\)/gm, '')
.split(/\s|,/g).filter((val) => val !== '').map((val, index) => index > 2 ? parseFloat(val) : parseInt(val, 10));
if (parts.length === 4) {
this._alpha = Math.floor(parseFloat(parts[3]) * 100);
} else if (parts.length === 3) {
this._alpha = 100;
}
if (parts.length >= 3) {
fromHSV(parts[0], parts[1], parts[2]);
}
} else if (value.indexOf('rgb') !== -1) {
const parts = value.replace(/rgba|rgb|\(|\)/gm, '')
.split(/\s|,/g).filter((val) => val !== '').map((val, index) => index > 2 ? parseFloat(val) : parseInt(val, 10));
if (parts.length === 4) {
this._alpha = Math.floor(parseFloat(parts[3]) * 100);
} else if (parts.length === 3) {
this._alpha = 100;
}
if (parts.length >= 3) {
const { h, s, v } = rgb2hsv(parts[0], parts[1], parts[2]);
fromHSV(h, s, v);
}
} else if (value.indexOf('#') !== -1) {
const hex = value.replace('#', '').trim();
let r, g, b;
if (hex.length === 3) {
r = parseHexChannel(hex[0] + hex[0]);
g = parseHexChannel(hex[1] + hex[1]);
b = parseHexChannel(hex[2] + hex[2]);
} else if (hex.length === 6 || hex.length === 8) {
r = parseHexChannel(hex.substring(0, 2));
g = parseHexChannel(hex.substring(2, 4));
b = parseHexChannel(hex.substring(4, 6));
}
if (hex.length === 8) {
this._alpha = Math.floor(parseHexChannel(hex.substring(6)) / 255 * 100);
} else if (hex.length === 3 || hex.length === 6) {
this._alpha = 100;
}
const { h, s, v } = rgb2hsv(r, g, b);
fromHSV(h, s, v);
}
}
compare(color) {
return Math.abs(color._hue - this._hue) < 2 &&
Math.abs(color._saturation - this._saturation) < 1 &&
Math.abs(color._value - this._value) < 1 &&
Math.abs(color._alpha - this._alpha) < 1;
}
doOnChange() {
const { _hue, _saturation, _value, _alpha, format } = this;
if (this.enableAlpha) {
switch (format) {
case 'hsl':
const hsl = hsv2hsl(_hue, _saturation / 100, _value / 100);
this.value = `hsla(${ _hue }, ${ Math.round(hsl[1] * 100) }%, ${ Math.round(hsl[2] * 100) }%, ${ _alpha / 100})`;
break;
case 'hsv':
this.value = `hsva(${ _hue }, ${ Math.round(_saturation) }%, ${ Math.round(_value) }%, ${ _alpha / 100})`;
break;
default:
const { r, g, b } = hsv2rgb(_hue, _saturation, _value);
this.value = `rgba(${r}, ${g}, ${b}, ${ _alpha / 100 })`;
}
} else {
switch (format) {
case 'hsl':
const hsl = hsv2hsl(_hue, _saturation / 100, _value / 100);
this.value = `hsl(${ _hue }, ${ Math.round(hsl[1] * 100) }%, ${ Math.round(hsl[2] * 100) }%)`;
break;
case 'hsv':
this.value = `hsv(${ _hue }, ${ Math.round(_saturation) }%, ${ Math.round(_value) }%)`;
break;
case 'rgb':
const { r, g, b } = hsv2rgb(_hue, _saturation, _value);
this.value = `rgb(${r}, ${g}, ${b})`;
break;
default:
this.value = toHex(hsv2rgb(_hue, _saturation, _value));
}
}
}
};
@@ -0,0 +1,132 @@
<template>
<div class="el-color-alpha-slider" :class="{ 'is-vertical': vertical }">
<div class="el-color-alpha-slider__bar"
@click="handleClick"
ref="bar"
:style="{
background: background
}">
</div>
<div class="el-color-alpha-slider__thumb"
ref="thumb"
:style="{
left: thumbLeft + 'px',
top: thumbTop + 'px'
}">
</div>
</div>
</template>
<script>
import draggable from '../draggable';
export default {
name: 'el-color-alpha-slider',
props: {
color: {
required: true
},
vertical: Boolean
},
watch: {
'color._alpha'() {
this.update();
},
'color.value'() {
this.update();
}
},
methods: {
handleClick(event) {
const thumb = this.$refs.thumb;
const target = event.target;
if (target !== thumb) {
this.handleDrag(event);
}
},
handleDrag(event) {
const rect = this.$el.getBoundingClientRect();
const { thumb } = this.$refs;
if (!this.vertical) {
let left = event.clientX - rect.left;
left = Math.max(thumb.offsetWidth / 2, left);
left = Math.min(left, rect.width - thumb.offsetWidth / 2);
this.color.set('alpha', Math.round((left - thumb.offsetWidth / 2) / (rect.width - thumb.offsetWidth) * 100));
} else {
let top = event.clientY - rect.top;
top = Math.max(thumb.offsetHeight / 2, top);
top = Math.min(top, rect.height - thumb.offsetHeight / 2);
this.color.set('alpha', Math.round((top - thumb.offsetHeight / 2) / (rect.height - thumb.offsetHeight) * 100));
}
},
getThumbLeft() {
if (this.vertical) return 0;
const el = this.$el;
const alpha = this.color._alpha;
if (!el) return 0;
const thumb = this.$refs.thumb;
return Math.round(alpha * (el.offsetWidth - thumb.offsetWidth / 2) / 100);
},
getThumbTop() {
if (!this.vertical) return 0;
const el = this.$el;
const alpha = this.color._alpha;
if (!el) return 0;
const thumb = this.$refs.thumb;
return Math.round(alpha * (el.offsetHeight - thumb.offsetHeight / 2) / 100);
},
getBackground() {
if (this.color && this.color.value) {
const { r, g, b } = this.color.toRgb();
return `linear-gradient(to right, rgba(${r}, ${g}, ${b}, 0) 0%, rgba(${r}, ${g}, ${b}, 1) 100%)`;
}
return null;
},
update() {
this.thumbLeft = this.getThumbLeft();
this.thumbTop = this.getThumbTop();
this.background = this.getBackground();
}
},
data() {
return {
thumbLeft: 0,
thumbTop: 0,
background: null
};
},
mounted() {
const { bar, thumb } = this.$refs;
const dragConfig = {
drag: (event) => {
this.handleDrag(event);
},
end: (event) => {
this.handleDrag(event);
}
};
draggable(bar, dragConfig);
draggable(thumb, dragConfig);
this.update();
}
};
</script>
@@ -0,0 +1,123 @@
<template>
<div class="el-color-hue-slider" :class="{ 'is-vertical': vertical }">
<div class="el-color-hue-slider__bar" @click="handleClick" ref="bar"></div>
<div class="el-color-hue-slider__thumb"
:style="{
left: thumbLeft + 'px',
top: thumbTop + 'px'
}"
ref="thumb">
</div>
</div>
</template>
<script>
import draggable from '../draggable';
export default {
name: 'el-color-hue-slider',
props: {
color: {
required: true
},
vertical: Boolean
},
data() {
return {
thumbLeft: 0,
thumbTop: 0
};
},
computed: {
hueValue() {
const hue = this.color.get('hue');
return hue;
}
},
watch: {
hueValue() {
this.update();
}
},
methods: {
handleClick(event) {
const thumb = this.$refs.thumb;
const target = event.target;
if (target !== thumb) {
this.handleDrag(event);
}
},
handleDrag(event) {
const rect = this.$el.getBoundingClientRect();
const { thumb } = this.$refs;
let hue;
if (!this.vertical) {
let left = event.clientX - rect.left;
left = Math.min(left, rect.width - thumb.offsetWidth / 2);
left = Math.max(thumb.offsetWidth / 2, left);
hue = Math.round((left - thumb.offsetWidth / 2) / (rect.width - thumb.offsetWidth) * 360);
} else {
let top = event.clientY - rect.top;
top = Math.min(top, rect.height - thumb.offsetHeight / 2);
top = Math.max(thumb.offsetHeight / 2, top);
hue = Math.round((top - thumb.offsetHeight / 2) / (rect.height - thumb.offsetHeight) * 360);
}
this.color.set('hue', hue);
},
getThumbLeft() {
if (this.vertical) return 0;
const el = this.$el;
const hue = this.color.get('hue');
if (!el) return 0;
const thumb = this.$refs.thumb;
return Math.round(hue * (el.offsetWidth - thumb.offsetWidth / 2) / 360);
},
getThumbTop() {
if (!this.vertical) return 0;
const el = this.$el;
const hue = this.color.get('hue');
if (!el) return 0;
const thumb = this.$refs.thumb;
return Math.round(hue * (el.offsetHeight - thumb.offsetHeight / 2) / 360);
},
update() {
this.thumbLeft = this.getThumbLeft();
this.thumbTop = this.getThumbTop();
}
},
mounted() {
const { bar, thumb } = this.$refs;
const dragConfig = {
drag: (event) => {
this.handleDrag(event);
},
end: (event) => {
this.handleDrag(event);
}
};
draggable(bar, dragConfig);
draggable(thumb, dragConfig);
this.update();
}
};
</script>
@@ -0,0 +1,121 @@
<template>
<transition name="el-zoom-in-top" @after-leave="doDestroy">
<div
class="el-color-dropdown"
v-show="showPopper">
<div class="el-color-dropdown__main-wrapper">
<hue-slider ref="hue" :color="color" vertical style="float: right;"></hue-slider>
<sv-panel ref="sl" :color="color"></sv-panel>
</div>
<alpha-slider v-if="showAlpha" ref="alpha" :color="color"></alpha-slider>
<predefine v-if="predefine" :color="color" :colors="predefine"></predefine>
<div class="el-color-dropdown__btns">
<span class="el-color-dropdown__value">
<el-input
v-model="customInput"
@keyup.native.enter="handleConfirm"
@blur="handleConfirm"
:validate-event="false"
size="mini">
</el-input>
</span>
<el-button
size="mini"
type="text"
class="el-color-dropdown__link-btn"
@click="$emit('clear')">
{{ t('el.colorpicker.clear') }}
</el-button>
<el-button
plain
size="mini"
class="el-color-dropdown__btn"
@click="confirmValue">
{{ t('el.colorpicker.confirm') }}
</el-button>
</div>
</div>
</transition>
</template>
<script>
import SvPanel from './sv-panel';
import HueSlider from './hue-slider';
import AlphaSlider from './alpha-slider';
import Predefine from './predefine';
import Popper from 'element-ui/src/utils/vue-popper';
import Locale from 'element-ui/src/mixins/locale';
import ElInput from 'element-ui/packages/input';
import ElButton from 'element-ui/packages/button';
export default {
name: 'el-color-picker-dropdown',
mixins: [Popper, Locale],
components: {
SvPanel,
HueSlider,
AlphaSlider,
ElInput,
ElButton,
Predefine
},
props: {
color: {
required: true
},
showAlpha: Boolean,
predefine: Array
},
data() {
return {
customInput: ''
};
},
computed: {
currentColor() {
const parent = this.$parent;
return !parent.value && !parent.showPanelColor ? '' : parent.color.value;
}
},
methods: {
confirmValue() {
this.$emit('pick');
},
handleConfirm() {
this.color.fromString(this.customInput);
}
},
mounted() {
this.$parent.popperElm = this.popperElm = this.$el;
this.referenceElm = this.$parent.$el;
},
watch: {
showPopper(val) {
if (val === true) {
this.$nextTick(() => {
const { sl, hue, alpha } = this.$refs;
sl && sl.update();
hue && hue.update();
alpha && alpha.update();
});
}
},
currentColor: {
immediate: true,
handler(val) {
this.customInput = val;
}
}
}
};
</script>
@@ -0,0 +1,61 @@
<template>
<div class="el-color-predefine">
<div class="el-color-predefine__colors">
<div class="el-color-predefine__color-selector"
:class="{selected: item.selected, 'is-alpha': item._alpha < 100}"
v-for="(item, index) in rgbaColors"
:key="colors[index]"
@click="handleSelect(index)">
<div :style="{'background-color': item.value}">
</div>
</div>
</div>
</div>
</template>
<script>
import Color from '../color';
export default {
props: {
colors: { type: Array, required: true },
color: { required: true }
},
data() {
return {
rgbaColors: this.parseColors(this.colors, this.color)
};
},
methods: {
handleSelect(index) {
this.color.fromString(this.colors[index]);
},
parseColors(colors, color) {
return colors.map(value => {
const c = new Color();
c.enableAlpha = true;
c.format = 'rgba';
c.fromString(value);
c.selected = c.value === color.value;
return c;
});
}
},
watch: {
'$parent.currentColor'(val) {
const color = new Color();
color.fromString(val);
this.rgbaColors.forEach(item => {
item.selected = color.compare(item);
});
},
colors(newVal) {
this.rgbaColors = this.parseColors(newVal, this.color);
},
color(newVal) {
this.rgbaColors = this.parseColors(this.colors, newVal);
}
}
};
</script>
@@ -0,0 +1,100 @@
<template>
<div class="el-color-svpanel"
:style="{
backgroundColor: background
}">
<div class="el-color-svpanel__white"></div>
<div class="el-color-svpanel__black"></div>
<div class="el-color-svpanel__cursor"
:style="{
top: cursorTop + 'px',
left: cursorLeft + 'px'
}">
<div></div>
</div>
</div>
</template>
<script>
import draggable from '../draggable';
export default {
name: 'el-sl-panel',
props: {
color: {
required: true
}
},
computed: {
colorValue() {
const hue = this.color.get('hue');
const value = this.color.get('value');
return { hue, value };
}
},
watch: {
colorValue() {
this.update();
}
},
methods: {
update() {
const saturation = this.color.get('saturation');
const value = this.color.get('value');
const el = this.$el;
let { clientWidth: width, clientHeight: height } = el;
this.cursorLeft = saturation * width / 100;
this.cursorTop = (100 - value) * height / 100;
this.background = 'hsl(' + this.color.get('hue') + ', 100%, 50%)';
},
handleDrag(event) {
const el = this.$el;
const rect = el.getBoundingClientRect();
let left = event.clientX - rect.left;
let top = event.clientY - rect.top;
left = Math.max(0, left);
left = Math.min(left, rect.width);
top = Math.max(0, top);
top = Math.min(top, rect.height);
this.cursorLeft = left;
this.cursorTop = top;
this.color.set({
saturation: left / rect.width * 100,
value: 100 - top / rect.height * 100
});
}
},
mounted() {
draggable(this.$el, {
drag: (event) => {
this.handleDrag(event);
},
end: (event) => {
this.handleDrag(event);
}
});
this.update();
},
data() {
return {
cursorTop: 0,
cursorLeft: 0,
background: 'hsl(0, 100%, 50%)'
};
}
};
</script>
+36
View File
@@ -0,0 +1,36 @@
import Vue from 'vue';
let isDragging = false;
export default function(element, options) {
if (Vue.prototype.$isServer) return;
const moveFn = function(event) {
if (options.drag) {
options.drag(event);
}
};
const upFn = function(event) {
document.removeEventListener('mousemove', moveFn);
document.removeEventListener('mouseup', upFn);
document.onselectstart = null;
document.ondragstart = null;
isDragging = false;
if (options.end) {
options.end(event);
}
};
element.addEventListener('mousedown', function(event) {
if (isDragging) return;
document.onselectstart = function() { return false; };
document.ondragstart = function() { return false; };
document.addEventListener('mousemove', moveFn);
document.addEventListener('mouseup', upFn);
isDragging = true;
if (options.start) {
options.start(event);
}
});
}
+188
View File
@@ -0,0 +1,188 @@
<template>
<div
:class="[
'el-color-picker',
colorDisabled ? 'is-disabled' : '',
colorSize ? `el-color-picker--${ colorSize }` : ''
]"
v-clickoutside="hide">
<div class="el-color-picker__mask" v-if="colorDisabled"></div>
<div class="el-color-picker__trigger" @click="handleTrigger">
<span class="el-color-picker__color" :class="{ 'is-alpha': showAlpha }">
<span class="el-color-picker__color-inner"
:style="{
backgroundColor: displayedColor
}"></span>
<span class="el-color-picker__empty el-icon-close" v-if="!value && !showPanelColor"></span>
</span>
<span class="el-color-picker__icon el-icon-arrow-down" v-show="value || showPanelColor"></span>
</div>
<picker-dropdown
ref="dropdown"
:class="['el-color-picker__panel', popperClass || '']"
v-model="showPicker"
@pick="confirmValue"
@clear="clearValue"
:color="color"
:show-alpha="showAlpha"
:predefine="predefine">
</picker-dropdown>
</div>
</template>
<script>
import Color from './color';
import PickerDropdown from './components/picker-dropdown.vue';
import Clickoutside from 'element-ui/src/utils/clickoutside';
import Emitter from 'element-ui/src/mixins/emitter';
export default {
name: 'ElColorPicker',
mixins: [Emitter],
props: {
value: String,
showAlpha: Boolean,
colorFormat: String,
disabled: Boolean,
size: String,
popperClass: String,
predefine: Array
},
inject: {
elForm: {
default: ''
},
elFormItem: {
default: ''
}
},
directives: { Clickoutside },
computed: {
displayedColor() {
if (!this.value && !this.showPanelColor) {
return 'transparent';
}
return this.displayedRgb(this.color, this.showAlpha);
},
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
colorSize() {
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
},
colorDisabled() {
return this.disabled || (this.elForm || {}).disabled;
}
},
watch: {
value(val) {
if (!val) {
this.showPanelColor = false;
} else if (val && val !== this.color.value) {
this.color.fromString(val);
}
},
color: {
deep: true,
handler() {
this.showPanelColor = true;
}
},
displayedColor(val) {
if (!this.showPicker) return;
const currentValueColor = new Color({
enableAlpha: this.showAlpha,
format: this.colorFormat
});
currentValueColor.fromString(this.value);
const currentValueColorRgb = this.displayedRgb(currentValueColor, this.showAlpha);
if (val !== currentValueColorRgb) {
this.$emit('active-change', val);
}
}
},
methods: {
handleTrigger() {
if (this.colorDisabled) return;
this.showPicker = !this.showPicker;
},
confirmValue() {
const value = this.color.value;
this.$emit('input', value);
this.$emit('change', value);
this.dispatch('ElFormItem', 'el.form.change', value);
this.showPicker = false;
},
clearValue() {
this.$emit('input', null);
this.$emit('change', null);
if (this.value !== null) {
this.dispatch('ElFormItem', 'el.form.change', null);
}
this.showPanelColor = false;
this.showPicker = false;
this.resetColor();
},
hide() {
this.showPicker = false;
this.resetColor();
},
resetColor() {
this.$nextTick(_ => {
if (this.value) {
this.color.fromString(this.value);
} else {
this.showPanelColor = false;
}
});
},
displayedRgb(color, showAlpha) {
if (!(color instanceof Color)) {
throw Error('color should be instance of Color Class');
}
const { r, g, b } = color.toRgb();
return showAlpha
? `rgba(${ r }, ${ g }, ${ b }, ${ color.get('alpha') / 100 })`
: `rgb(${ r }, ${ g }, ${ b })`;
}
},
mounted() {
const value = this.value;
if (value) {
this.color.fromString(value);
}
this.popperElm = this.$refs.dropdown.$el;
},
data() {
const color = new Color({
enableAlpha: this.showAlpha,
format: this.colorFormat
});
return {
color,
showPicker: false,
showPanelColor: false
};
},
components: {
PickerDropdown
}
};
</script>
+8
View File
@@ -0,0 +1,8 @@
import Container from './src/main';
/* istanbul ignore next */
Container.install = function(Vue) {
Vue.component(Container.name, Container);
};
export default Container;
+33
View File
@@ -0,0 +1,33 @@
<template>
<section class="el-container" :class="{ 'is-vertical': isVertical }">
<slot></slot>
</section>
</template>
<script>
export default {
name: 'ElContainer',
componentName: 'ElContainer',
props: {
direction: String
},
computed: {
isVertical() {
if (this.direction === 'vertical') {
return true;
} else if (this.direction === 'horizontal') {
return false;
}
return this.$slots && this.$slots.default
? this.$slots.default.some(vnode => {
const tag = vnode.componentOptions && vnode.componentOptions.tag;
return tag === 'el-header' || tag === 'el-footer';
})
: false;
}
}
};
</script>
+8
View File
@@ -0,0 +1,8 @@
import DatePicker from './src/picker/date-picker';
/* istanbul ignore next */
DatePicker.install = function install(Vue) {
Vue.component(DatePicker.name, DatePicker);
};
export default DatePicker;
+441
View File
@@ -0,0 +1,441 @@
<template>
<table
cellspacing="0"
cellpadding="0"
class="el-date-table"
@click="handleClick"
@mousemove="handleMouseMove"
:class="{ 'is-week-mode': selectionMode === 'week' }">
<tbody>
<tr>
<th v-if="showWeekNumber">{{ t('el.datepicker.week') }}</th>
<th v-for="(week, key) in WEEKS" :key="key">{{ t('el.datepicker.weeks.' + week) }}</th>
</tr>
<tr
class="el-date-table__row"
v-for="(row, key) in rows"
:class="{ current: isWeekActive(row[1]) }"
:key="key">
<td
v-for="(cell, key) in row"
:class="getCellClasses(cell)"
:key="key">
<div>
<span>
{{ cell.text }}
</span>
</div>
</td>
</tr>
</tbody>
</table>
</template>
<script>
import { getFirstDayOfMonth, getDayCountOfMonth, getWeekNumber, getStartDateOfMonth, prevDate, nextDate, isDate, clearTime as _clearTime} from '../util';
import Locale from 'element-ui/src/mixins/locale';
import { arrayFindIndex, arrayFind, coerceTruthyValueToArray } from 'element-ui/src/utils/util';
const WEEKS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
const getDateTimestamp = function(time) {
if (typeof time === 'number' || typeof time === 'string') {
return _clearTime(new Date(time)).getTime();
} else if (time instanceof Date) {
return _clearTime(time).getTime();
} else {
return NaN;
}
};
// remove the first element that satisfies `pred` from arr
// return a new array if modification occurs
// return the original array otherwise
const removeFromArray = function(arr, pred) {
const idx = typeof pred === 'function' ? arrayFindIndex(arr, pred) : arr.indexOf(pred);
return idx >= 0 ? [...arr.slice(0, idx), ...arr.slice(idx + 1)] : arr;
};
export default {
mixins: [Locale],
props: {
firstDayOfWeek: {
default: 7,
type: Number,
validator: val => val >= 1 && val <= 7
},
value: {},
defaultValue: {
validator(val) {
// either: null, valid Date object, Array of valid Date objects
return val === null || isDate(val) || (Array.isArray(val) && val.every(isDate));
}
},
date: {},
selectionMode: {
default: 'day'
},
showWeekNumber: {
type: Boolean,
default: false
},
disabledDate: {},
minDate: {},
maxDate: {},
rangeState: {
default() {
return {
endDate: null,
selecting: false
};
}
}
},
computed: {
offsetDay() {
const week = this.firstDayOfWeek;
// 周日为界限,左右偏移的天数,3217654 例如周一就是 -1,目的是调整前两行日期的位置
return week > 3 ? 7 - week : -week;
},
WEEKS() {
const week = this.firstDayOfWeek;
return WEEKS.concat(WEEKS).slice(week, week + 7);
},
year() {
return this.date.getFullYear();
},
month() {
return this.date.getMonth();
},
startDate() {
return getStartDateOfMonth(this.year, this.month);
},
rows() {
// TODO: refactory rows / getCellClasses
const date = new Date(this.year, this.month, 1);
let day = getFirstDayOfMonth(date); // day of first day
const dateCountOfMonth = getDayCountOfMonth(date.getFullYear(), date.getMonth());
const dateCountOfLastMonth = getDayCountOfMonth(date.getFullYear(), (date.getMonth() === 0 ? 11 : date.getMonth() - 1));
day = (day === 0 ? 7 : day);
const offset = this.offsetDay;
const rows = this.tableRows;
let count = 1;
let firstDayPosition;
const startDate = this.startDate;
const disabledDate = this.disabledDate;
const selectedDate = this.selectionMode === 'dates' ? coerceTruthyValueToArray(this.value) : [];
const now = getDateTimestamp(new Date());
for (let i = 0; i < 6; i++) {
const row = rows[i];
if (this.showWeekNumber) {
if (!row[0]) {
row[0] = { type: 'week', text: getWeekNumber(nextDate(startDate, i * 7 + 1)) };
}
}
for (let j = 0; j < 7; j++) {
let cell = row[this.showWeekNumber ? j + 1 : j];
if (!cell) {
cell = { row: i, column: j, type: 'normal', inRange: false, start: false, end: false };
}
cell.type = 'normal';
const index = i * 7 + j;
const time = nextDate(startDate, index - offset).getTime();
cell.inRange = time >= getDateTimestamp(this.minDate) && time <= getDateTimestamp(this.maxDate);
cell.start = this.minDate && time === getDateTimestamp(this.minDate);
cell.end = this.maxDate && time === getDateTimestamp(this.maxDate);
const isToday = time === now;
if (isToday) {
cell.type = 'today';
}
if (i >= 0 && i <= 1) {
if (j + i * 7 >= (day + offset)) {
cell.text = count++;
if (count === 2) {
firstDayPosition = i * 7 + j;
}
} else {
cell.text = dateCountOfLastMonth - (day + offset - j % 7) + 1 + i * 7;
cell.type = 'prev-month';
}
} else {
if (count <= dateCountOfMonth) {
cell.text = count++;
if (count === 2) {
firstDayPosition = i * 7 + j;
}
} else {
cell.text = count++ - dateCountOfMonth;
cell.type = 'next-month';
}
}
let cellDate = new Date(time);
cell.disabled = typeof disabledDate === 'function' && disabledDate(cellDate);
cell.selected = arrayFind(selectedDate, date => date.getTime() === cellDate.getTime());
this.$set(row, this.showWeekNumber ? j + 1 : j, cell);
}
if (this.selectionMode === 'week') {
const start = this.showWeekNumber ? 1 : 0;
const end = this.showWeekNumber ? 7 : 6;
const isWeekActive = this.isWeekActive(row[start + 1]);
row[start].inRange = isWeekActive;
row[start].start = isWeekActive;
row[end].inRange = isWeekActive;
row[end].end = isWeekActive;
}
}
rows.firstDayPosition = firstDayPosition;
return rows;
}
},
watch: {
'rangeState.endDate'(newVal) {
this.markRange(this.minDate, newVal);
},
minDate(newVal, oldVal) {
if (getDateTimestamp(newVal) !== getDateTimestamp(oldVal)) {
this.markRange(this.minDate, this.maxDate);
}
},
maxDate(newVal, oldVal) {
if (getDateTimestamp(newVal) !== getDateTimestamp(oldVal)) {
this.markRange(this.minDate, this.maxDate);
}
}
},
data() {
return {
tableRows: [ [], [], [], [], [], [] ],
lastRow: null,
lastColumn: null
};
},
methods: {
cellMatchesDate(cell, date) {
const value = new Date(date);
return this.year === value.getFullYear() &&
this.month === value.getMonth() &&
Number(cell.text) === value.getDate();
},
getCellClasses(cell) {
const selectionMode = this.selectionMode;
const defaultValue = this.defaultValue ? Array.isArray(this.defaultValue) ? this.defaultValue : [this.defaultValue] : [];
let classes = [];
if ((cell.type === 'normal' || cell.type === 'today') && !cell.disabled) {
classes.push('available');
if (cell.type === 'today') {
classes.push('today');
}
} else {
classes.push(cell.type);
}
if (cell.type === 'normal' && defaultValue.some(date => this.cellMatchesDate(cell, date))) {
classes.push('default');
}
if (selectionMode === 'day' && (cell.type === 'normal' || cell.type === 'today') && this.cellMatchesDate(cell, this.value)) {
classes.push('current');
}
if (cell.inRange && ((cell.type === 'normal' || cell.type === 'today') || this.selectionMode === 'week')) {
classes.push('in-range');
if (cell.start) {
classes.push('start-date');
}
if (cell.end) {
classes.push('end-date');
}
}
if (cell.disabled) {
classes.push('disabled');
}
if (cell.selected) {
classes.push('selected');
}
return classes.join(' ');
},
getDateOfCell(row, column) {
const offsetFromStart = row * 7 + (column - (this.showWeekNumber ? 1 : 0)) - this.offsetDay;
return nextDate(this.startDate, offsetFromStart);
},
isWeekActive(cell) {
if (this.selectionMode !== 'week') return false;
const newDate = new Date(this.year, this.month, 1);
const year = newDate.getFullYear();
const month = newDate.getMonth();
if (cell.type === 'prev-month') {
newDate.setMonth(month === 0 ? 11 : month - 1);
newDate.setFullYear(month === 0 ? year - 1 : year);
}
if (cell.type === 'next-month') {
newDate.setMonth(month === 11 ? 0 : month + 1);
newDate.setFullYear(month === 11 ? year + 1 : year);
}
newDate.setDate(parseInt(cell.text, 10));
if (isDate(this.value)) {
const dayOffset = (this.value.getDay() - this.firstDayOfWeek + 7) % 7 - 1;
const weekDate = prevDate(this.value, dayOffset);
return weekDate.getTime() === newDate.getTime();
}
return false;
},
markRange(minDate, maxDate) {
minDate = getDateTimestamp(minDate);
maxDate = getDateTimestamp(maxDate) || minDate;
[minDate, maxDate] = [Math.min(minDate, maxDate), Math.max(minDate, maxDate)];
const startDate = this.startDate;
const rows = this.rows;
for (let i = 0, k = rows.length; i < k; i++) {
const row = rows[i];
for (let j = 0, l = row.length; j < l; j++) {
if (this.showWeekNumber && j === 0) continue;
const cell = row[j];
const index = i * 7 + j + (this.showWeekNumber ? -1 : 0);
const time = nextDate(startDate, index - this.offsetDay).getTime();
cell.inRange = minDate && time >= minDate && time <= maxDate;
cell.start = minDate && time === minDate;
cell.end = maxDate && time === maxDate;
}
}
},
handleMouseMove(event) {
if (!this.rangeState.selecting) return;
let target = event.target;
if (target.tagName === 'SPAN') {
target = target.parentNode.parentNode;
}
if (target.tagName === 'DIV') {
target = target.parentNode;
}
if (target.tagName !== 'TD') return;
const row = target.parentNode.rowIndex - 1;
const column = target.cellIndex;
// can not select disabled date
if (this.rows[row][column].disabled) return;
// only update rangeState when mouse moves to a new cell
// this avoids frequent Date object creation and improves performance
if (row !== this.lastRow || column !== this.lastColumn) {
this.lastRow = row;
this.lastColumn = column;
this.$emit('changerange', {
minDate: this.minDate,
maxDate: this.maxDate,
rangeState: {
selecting: true,
endDate: this.getDateOfCell(row, column)
}
});
}
},
handleClick(event) {
let target = event.target;
if (target.tagName === 'SPAN') {
target = target.parentNode.parentNode;
}
if (target.tagName === 'DIV') {
target = target.parentNode;
}
if (target.tagName !== 'TD') return;
const row = target.parentNode.rowIndex - 1;
const column = this.selectionMode === 'week' ? 1 : target.cellIndex;
const cell = this.rows[row][column];
if (cell.disabled || cell.type === 'week') return;
const newDate = this.getDateOfCell(row, column);
if (this.selectionMode === 'range') {
if (!this.rangeState.selecting) {
this.$emit('pick', {minDate: newDate, maxDate: null});
this.rangeState.selecting = true;
} else {
if (newDate >= this.minDate) {
this.$emit('pick', {minDate: this.minDate, maxDate: newDate});
} else {
this.$emit('pick', {minDate: newDate, maxDate: this.minDate});
}
this.rangeState.selecting = false;
}
} else if (this.selectionMode === 'day') {
this.$emit('pick', newDate);
} else if (this.selectionMode === 'week') {
const weekNumber = getWeekNumber(newDate);
const value = newDate.getFullYear() + 'w' + weekNumber;
this.$emit('pick', {
year: newDate.getFullYear(),
week: weekNumber,
value: value,
date: newDate
});
} else if (this.selectionMode === 'dates') {
const value = this.value || [];
const newValue = cell.selected
? removeFromArray(value, date => date.getTime() === newDate.getTime())
: [...value, newDate];
this.$emit('pick', newValue);
}
}
}
};
</script>
+105
View File
@@ -0,0 +1,105 @@
<template>
<table @click="handleMonthTableClick" class="el-month-table">
<tbody>
<tr>
<td :class="getCellStyle(0)">
<a class="cell">{{ t('el.datepicker.months.jan') }}</a>
</td>
<td :class="getCellStyle(1)">
<a class="cell">{{ t('el.datepicker.months.feb') }}</a>
</td>
<td :class="getCellStyle(2)">
<a class="cell">{{ t('el.datepicker.months.mar') }}</a>
</td>
<td :class="getCellStyle(3)">
<a class="cell">{{ t('el.datepicker.months.apr') }}</a>
</td>
</tr>
<tr>
<td :class="getCellStyle(4)">
<a class="cell">{{ t('el.datepicker.months.may') }}</a>
</td>
<td :class="getCellStyle(5)">
<a class="cell">{{ t('el.datepicker.months.jun') }}</a>
</td>
<td :class="getCellStyle(6)">
<a class="cell">{{ t('el.datepicker.months.jul') }}</a>
</td>
<td :class="getCellStyle(7)">
<a class="cell">{{ t('el.datepicker.months.aug') }}</a>
</td>
</tr>
<tr>
<td :class="getCellStyle(8)">
<a class="cell">{{ t('el.datepicker.months.sep') }}</a>
</td>
<td :class="getCellStyle(9)">
<a class="cell">{{ t('el.datepicker.months.oct') }}</a>
</td>
<td :class="getCellStyle(10)">
<a class="cell">{{ t('el.datepicker.months.nov') }}</a>
</td>
<td :class="getCellStyle(11)">
<a class="cell">{{ t('el.datepicker.months.dec') }}</a>
</td>
</tr>
</tbody>
</table>
</template>
<script type="text/babel">
import Locale from 'element-ui/src/mixins/locale';
import { isDate, range, getDayCountOfMonth, nextDate } from '../util';
import { hasClass } from 'element-ui/src/utils/dom';
import { arrayFindIndex, coerceTruthyValueToArray } from 'element-ui/src/utils/util';
const datesInMonth = (year, month) => {
const numOfDays = getDayCountOfMonth(year, month);
const firstDay = new Date(year, month, 1);
return range(numOfDays).map(n => nextDate(firstDay, n));
};
export default {
props: {
disabledDate: {},
value: {},
defaultValue: {
validator(val) {
// null or valid Date Object
return val === null || (val instanceof Date && isDate(val));
}
},
date: {}
},
mixins: [Locale],
methods: {
getCellStyle(month) {
const style = {};
const year = this.date.getFullYear();
const today = new Date();
style.disabled = typeof this.disabledDate === 'function'
? datesInMonth(year, month).every(this.disabledDate)
: false;
style.current = arrayFindIndex(coerceTruthyValueToArray(this.value), date => date.getFullYear() === year && date.getMonth() === month) >= 0;
style.today = today.getFullYear() === year && today.getMonth() === month;
style.default = this.defaultValue &&
this.defaultValue.getFullYear() === year &&
this.defaultValue.getMonth() === month;
return style;
},
handleMonthTableClick(event) {
const target = event.target;
if (target.tagName !== 'A') return;
if (hasClass(target.parentNode, 'disabled')) return;
const column = target.parentNode.cellIndex;
const row = target.parentNode.parentNode.rowIndex;
const month = row * 4 + column;
this.$emit('pick', month);
}
}
};
</script>
+303
View File
@@ -0,0 +1,303 @@
<template>
<div class="el-time-spinner" :class="{ 'has-seconds': showSeconds }">
<template v-if="!arrowControl">
<el-scrollbar
@mouseenter.native="emitSelectRange('hours')"
@mousemove.native="adjustCurrentSpinner('hours')"
class="el-time-spinner__wrapper"
wrap-style="max-height: inherit;"
view-class="el-time-spinner__list"
noresize
tag="ul"
ref="hours">
<li
@click="handleClick('hours', { value: hour, disabled: disabled })"
v-for="(disabled, hour) in hoursList"
class="el-time-spinner__item"
:key="hour"
:class="{ 'active': hour === hours, 'disabled': disabled }">{{ ('0' + (amPmMode ? (hour % 12 || 12) : hour )).slice(-2) }}{{ amPm(hour) }}</li>
</el-scrollbar>
<el-scrollbar
@mouseenter.native="emitSelectRange('minutes')"
@mousemove.native="adjustCurrentSpinner('minutes')"
class="el-time-spinner__wrapper"
wrap-style="max-height: inherit;"
view-class="el-time-spinner__list"
noresize
tag="ul"
ref="minutes">
<li
@click="handleClick('minutes', { value: key, disabled: false })"
v-for="(enabled, key) in minutesList"
:key="key"
class="el-time-spinner__item"
:class="{ 'active': key === minutes, disabled: !enabled }">{{ ('0' + key).slice(-2) }}</li>
</el-scrollbar>
<el-scrollbar
v-show="showSeconds"
@mouseenter.native="emitSelectRange('seconds')"
@mousemove.native="adjustCurrentSpinner('seconds')"
class="el-time-spinner__wrapper"
wrap-style="max-height: inherit;"
view-class="el-time-spinner__list"
noresize
tag="ul"
ref="seconds">
<li
@click="handleClick('seconds', { value: key, disabled: false })"
v-for="(second, key) in 60"
class="el-time-spinner__item"
:class="{ 'active': key === seconds }"
:key="key">{{ ('0' + key).slice(-2) }}</li>
</el-scrollbar>
</template>
<template v-if="arrowControl">
<div
@mouseenter="emitSelectRange('hours')"
class="el-time-spinner__wrapper is-arrow">
<i v-repeat-click="decrease" class="el-time-spinner__arrow el-icon-arrow-up"></i>
<i v-repeat-click="increase" class="el-time-spinner__arrow el-icon-arrow-down"></i>
<ul class="el-time-spinner__list" ref="hours">
<li
class="el-time-spinner__item"
:class="{ 'active': hour === hours, 'disabled': hoursList[hour] }"
v-for="(hour, key) in arrowHourList"
:key="key">{{ hour === undefined ? '' : ('0' + (amPmMode ? (hour % 12 || 12) : hour )).slice(-2) + amPm(hour) }}</li>
</ul>
</div>
<div
@mouseenter="emitSelectRange('minutes')"
class="el-time-spinner__wrapper is-arrow">
<i v-repeat-click="decrease" class="el-time-spinner__arrow el-icon-arrow-up"></i>
<i v-repeat-click="increase" class="el-time-spinner__arrow el-icon-arrow-down"></i>
<ul class="el-time-spinner__list" ref="minutes">
<li
class="el-time-spinner__item"
:class="{ 'active': minute === minutes }"
v-for="(minute, key) in arrowMinuteList"
:key="key">
{{ minute === undefined ? '' : ('0' + minute).slice(-2) }}
</li>
</ul>
</div>
<div
@mouseenter="emitSelectRange('seconds')"
class="el-time-spinner__wrapper is-arrow"
v-if="showSeconds">
<i v-repeat-click="decrease" class="el-time-spinner__arrow el-icon-arrow-up"></i>
<i v-repeat-click="increase" class="el-time-spinner__arrow el-icon-arrow-down"></i>
<ul class="el-time-spinner__list" ref="seconds">
<li
v-for="(second, key) in arrowSecondList"
class="el-time-spinner__item"
:class="{ 'active': second === seconds }"
:key="key">
{{ second === undefined ? '' : ('0' + second).slice(-2) }}
</li>
</ul>
</div>
</template>
</div>
</template>
<script type="text/babel">
import { getRangeHours, getRangeMinutes, modifyTime } from '../util';
import ElScrollbar from 'element-ui/packages/scrollbar';
import RepeatClick from 'element-ui/src/directives/repeat-click';
export default {
components: { ElScrollbar },
directives: {
repeatClick: RepeatClick
},
props: {
date: {},
defaultValue: {}, // reserved for future use
showSeconds: {
type: Boolean,
default: true
},
arrowControl: Boolean,
amPmMode: {
type: String,
default: '' // 'a': am/pm; 'A': AM/PM
}
},
computed: {
hours() {
return this.date.getHours();
},
minutes() {
return this.date.getMinutes();
},
seconds() {
return this.date.getSeconds();
},
hoursList() {
return getRangeHours(this.selectableRange);
},
minutesList() {
return getRangeMinutes(this.selectableRange, this.hours);
},
arrowHourList() {
const hours = this.hours;
return [
hours > 0 ? hours - 1 : undefined,
hours,
hours < 23 ? hours + 1 : undefined
];
},
arrowMinuteList() {
const minutes = this.minutes;
return [
minutes > 0 ? minutes - 1 : undefined,
minutes,
minutes < 59 ? minutes + 1 : undefined
];
},
arrowSecondList() {
const seconds = this.seconds;
return [
seconds > 0 ? seconds - 1 : undefined,
seconds,
seconds < 59 ? seconds + 1 : undefined
];
}
},
data() {
return {
selectableRange: [],
currentScrollbar: null
};
},
mounted() {
this.$nextTick(() => {
!this.arrowControl && this.bindScrollEvent();
});
},
methods: {
increase() {
this.scrollDown(1);
},
decrease() {
this.scrollDown(-1);
},
modifyDateField(type, value) {
switch (type) {
case 'hours': this.$emit('change', modifyTime(this.date, value, this.minutes, this.seconds)); break;
case 'minutes': this.$emit('change', modifyTime(this.date, this.hours, value, this.seconds)); break;
case 'seconds': this.$emit('change', modifyTime(this.date, this.hours, this.minutes, value)); break;
}
},
handleClick(type, {value, disabled}) {
if (!disabled) {
this.modifyDateField(type, value);
this.emitSelectRange(type);
this.adjustSpinner(type, value);
}
},
emitSelectRange(type) {
if (type === 'hours') {
this.$emit('select-range', 0, 2);
} else if (type === 'minutes') {
this.$emit('select-range', 3, 5);
} else if (type === 'seconds') {
this.$emit('select-range', 6, 8);
}
this.currentScrollbar = type;
},
bindScrollEvent() {
const bindFuntion = (type) => {
this.$refs[type].wrap.onscroll = (e) => {
// TODO: scroll is emitted when set scrollTop programatically
// should find better solutions in the future!
this.handleScroll(type, e);
};
};
bindFuntion('hours');
bindFuntion('minutes');
bindFuntion('seconds');
},
handleScroll(type) {
const value = Math.min(Math.floor((this.$refs[type].wrap.scrollTop - (this.scrollBarHeight(type) * 0.5 - 10) / this.typeItemHeight(type) + 3) / this.typeItemHeight(type)), (type === 'hours' ? 23 : 59));
this.modifyDateField(type, value);
},
// NOTE: used by datetime / date-range panel
// renamed from adjustScrollTop
// should try to refactory it
adjustSpinners() {
this.adjustSpinner('hours', this.hours);
this.adjustSpinner('minutes', this.minutes);
this.adjustSpinner('seconds', this.seconds);
},
adjustCurrentSpinner(type) {
this.adjustSpinner(type, this[type]);
},
adjustSpinner(type, value) {
if (this.arrowControl) return;
const el = this.$refs[type].wrap;
if (el) {
el.scrollTop = Math.max(0, value * this.typeItemHeight(type));
}
},
scrollDown(step) {
if (!this.currentScrollbar) {
this.emitSelectRange('hours');
}
const label = this.currentScrollbar;
const hoursList = this.hoursList;
let now = this[label];
if (this.currentScrollbar === 'hours') {
let total = Math.abs(step);
step = step > 0 ? 1 : -1;
let length = hoursList.length;
while (length-- && total) {
now = (now + step + hoursList.length) % hoursList.length;
if (hoursList[now]) {
continue;
}
total--;
}
if (hoursList[now]) return;
} else {
now = (now + step + 60) % 60;
}
this.modifyDateField(label, now);
this.adjustSpinner(label, now);
},
amPm(hour) {
let shouldShowAmPm = this.amPmMode.toLowerCase() === 'a';
if (!shouldShowAmPm) return '';
let isCapital = this.amPmMode === 'A';
let content = (hour < 12) ? ' am' : ' pm';
if (isCapital) content = content.toUpperCase();
return content;
},
typeItemHeight(type) {
return this.$refs[type].$el.querySelector('li').offsetHeight;
},
scrollBarHeight(type) {
return this.$refs[type].$el.offsetHeight;
}
}
};
</script>
+101
View File
@@ -0,0 +1,101 @@
<template>
<table @click="handleYearTableClick" class="el-year-table">
<tbody>
<tr>
<td class="available" :class="getCellStyle(startYear + 0)">
<a class="cell">{{ startYear }}</a>
</td>
<td class="available" :class="getCellStyle(startYear + 1)">
<a class="cell">{{ startYear + 1 }}</a>
</td>
<td class="available" :class="getCellStyle(startYear + 2)">
<a class="cell">{{ startYear + 2 }}</a>
</td>
<td class="available" :class="getCellStyle(startYear + 3)">
<a class="cell">{{ startYear + 3 }}</a>
</td>
</tr>
<tr>
<td class="available" :class="getCellStyle(startYear + 4)">
<a class="cell">{{ startYear + 4 }}</a>
</td>
<td class="available" :class="getCellStyle(startYear + 5)">
<a class="cell">{{ startYear + 5 }}</a>
</td>
<td class="available" :class="getCellStyle(startYear + 6)">
<a class="cell">{{ startYear + 6 }}</a>
</td>
<td class="available" :class="getCellStyle(startYear + 7)">
<a class="cell">{{ startYear + 7 }}</a>
</td>
</tr>
<tr>
<td class="available" :class="getCellStyle(startYear + 8)">
<a class="cell">{{ startYear + 8 }}</a>
</td>
<td class="available" :class="getCellStyle(startYear + 9)">
<a class="cell">{{ startYear + 9 }}</a>
</td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</template>
<script type="text/babel">
import { hasClass } from 'element-ui/src/utils/dom';
import { isDate, range, nextDate, getDayCountOfYear } from '../util';
import { arrayFindIndex, coerceTruthyValueToArray } from 'element-ui/src/utils/util';
const datesInYear = year => {
const numOfDays = getDayCountOfYear(year);
const firstDay = new Date(year, 0, 1);
return range(numOfDays).map(n => nextDate(firstDay, n));
};
export default {
props: {
disabledDate: {},
value: {},
defaultValue: {
validator(val) {
// null or valid Date Object
return val === null || (val instanceof Date && isDate(val));
}
},
date: {}
},
computed: {
startYear() {
return Math.floor(this.date.getFullYear() / 10) * 10;
}
},
methods: {
getCellStyle(year) {
const style = {};
const today = new Date();
style.disabled = typeof this.disabledDate === 'function'
? datesInYear(year).every(this.disabledDate)
: false;
style.current = arrayFindIndex(coerceTruthyValueToArray(this.value), date => date.getFullYear() === year) >= 0;
style.today = today.getFullYear() === year;
style.default = this.defaultValue && this.defaultValue.getFullYear() === year;
return style;
},
handleYearTableClick(event) {
const target = event.target;
if (target.tagName === 'A') {
if (hasClass(target.parentNode, 'disabled')) return;
const year = target.textContent || target.innerText;
this.$emit('pick', Number(year));
}
}
}
};
</script>
+679
View File
@@ -0,0 +1,679 @@
<template>
<transition name="el-zoom-in-top" @after-leave="$emit('dodestroy')">
<div
v-show="visible"
class="el-picker-panel el-date-range-picker el-popper"
:class="[{
'has-sidebar': $slots.sidebar || shortcuts,
'has-time': showTime
}, popperClass]">
<div class="el-picker-panel__body-wrapper">
<slot name="sidebar" class="el-picker-panel__sidebar"></slot>
<div class="el-picker-panel__sidebar" v-if="shortcuts">
<button
type="button"
class="el-picker-panel__shortcut"
v-for="(shortcut, key) in shortcuts"
:key="key"
@click="handleShortcutClick(shortcut)">{{shortcut.text}}</button>
</div>
<div class="el-picker-panel__body">
<div class="el-date-range-picker__time-header" v-if="showTime">
<span class="el-date-range-picker__editors-wrap">
<span class="el-date-range-picker__time-picker-wrap">
<el-input
size="small"
:disabled="rangeState.selecting"
ref="minInput"
:placeholder="t('el.datepicker.startDate')"
class="el-date-range-picker__editor"
:value="minVisibleDate"
@input="val => handleDateInput(val, 'min')"
@change="val => handleDateChange(val, 'min')" />
</span>
<span class="el-date-range-picker__time-picker-wrap" v-clickoutside="handleMinTimeClose">
<el-input
size="small"
class="el-date-range-picker__editor"
:disabled="rangeState.selecting"
:placeholder="t('el.datepicker.startTime')"
:value="minVisibleTime"
@focus="minTimePickerVisible = true"
@input="val => handleTimeInput(val, 'min')"
@change="val => handleTimeChange(val, 'min')" />
<time-picker
ref="minTimePicker"
@pick="handleMinTimePick"
:time-arrow-control="arrowControl"
:visible="minTimePickerVisible"
@mounted="$refs.minTimePicker.format=timeFormat">
</time-picker>
</span>
</span>
<span class="el-icon-arrow-right"></span>
<span class="el-date-range-picker__editors-wrap is-right">
<span class="el-date-range-picker__time-picker-wrap">
<el-input
size="small"
class="el-date-range-picker__editor"
:disabled="rangeState.selecting"
:placeholder="t('el.datepicker.endDate')"
:value="maxVisibleDate"
:readonly="!minDate"
@input="val => handleDateInput(val, 'max')"
@change="val => handleDateChange(val, 'max')" />
</span>
<span class="el-date-range-picker__time-picker-wrap" v-clickoutside="handleMaxTimeClose">
<el-input
size="small"
class="el-date-range-picker__editor"
:disabled="rangeState.selecting"
:placeholder="t('el.datepicker.endTime')"
:value="maxVisibleTime"
:readonly="!minDate"
@focus="minDate && (maxTimePickerVisible = true)"
@input="val => handleTimeInput(val, 'max')"
@change="val => handleTimeChange(val, 'max')" />
<time-picker
ref="maxTimePicker"
@pick="handleMaxTimePick"
:time-arrow-control="arrowControl"
:visible="maxTimePickerVisible"
@mounted="$refs.maxTimePicker.format=timeFormat">
</time-picker>
</span>
</span>
</div>
<div class="el-picker-panel__content el-date-range-picker__content is-left">
<div class="el-date-range-picker__header">
<button
type="button"
@click="leftPrevYear"
class="el-picker-panel__icon-btn el-icon-d-arrow-left"></button>
<button
type="button"
@click="leftPrevMonth"
class="el-picker-panel__icon-btn el-icon-arrow-left"></button>
<button
type="button"
@click="leftNextYear"
v-if="unlinkPanels"
:disabled="!enableYearArrow"
:class="{ 'is-disabled': !enableYearArrow }"
class="el-picker-panel__icon-btn el-icon-d-arrow-right"></button>
<button
type="button"
@click="leftNextMonth"
v-if="unlinkPanels"
:disabled="!enableMonthArrow"
:class="{ 'is-disabled': !enableMonthArrow }"
class="el-picker-panel__icon-btn el-icon-arrow-right"></button>
<div>{{ leftLabel }}</div>
</div>
<date-table
selection-mode="range"
:date="leftDate"
:default-value="defaultValue"
:min-date="minDate"
:max-date="maxDate"
:range-state="rangeState"
:disabled-date="disabledDate"
@changerange="handleChangeRange"
:first-day-of-week="firstDayOfWeek"
@pick="handleRangePick">
</date-table>
</div>
<div class="el-picker-panel__content el-date-range-picker__content is-right">
<div class="el-date-range-picker__header">
<button
type="button"
@click="rightPrevYear"
v-if="unlinkPanels"
:disabled="!enableYearArrow"
:class="{ 'is-disabled': !enableYearArrow }"
class="el-picker-panel__icon-btn el-icon-d-arrow-left"></button>
<button
type="button"
@click="rightPrevMonth"
v-if="unlinkPanels"
:disabled="!enableMonthArrow"
:class="{ 'is-disabled': !enableMonthArrow }"
class="el-picker-panel__icon-btn el-icon-arrow-left"></button>
<button
type="button"
@click="rightNextYear"
class="el-picker-panel__icon-btn el-icon-d-arrow-right"></button>
<button
type="button"
@click="rightNextMonth"
class="el-picker-panel__icon-btn el-icon-arrow-right"></button>
<div>{{ rightLabel }}</div>
</div>
<date-table
selection-mode="range"
:date="rightDate"
:default-value="defaultValue"
:min-date="minDate"
:max-date="maxDate"
:range-state="rangeState"
:disabled-date="disabledDate"
@changerange="handleChangeRange"
:first-day-of-week="firstDayOfWeek"
@pick="handleRangePick">
</date-table>
</div>
</div>
</div>
<div class="el-picker-panel__footer" v-if="showTime">
<el-button
size="mini"
type="text"
class="el-picker-panel__link-btn"
@click="handleClear">
{{ t('el.datepicker.clear') }}
</el-button>
<el-button
plain
size="mini"
class="el-picker-panel__link-btn"
:disabled="btnDisabled"
@click="handleConfirm(false)">
{{ t('el.datepicker.confirm') }}
</el-button>
</div>
</div>
</transition>
</template>
<script type="text/babel">
import {
formatDate,
parseDate,
isDate,
modifyDate,
modifyTime,
modifyWithTimeString,
prevYear,
nextYear,
prevMonth,
nextMonth,
extractDateFormat,
extractTimeFormat
} from '../util';
import Clickoutside from 'element-ui/src/utils/clickoutside';
import Locale from 'element-ui/src/mixins/locale';
import TimePicker from './time';
import DateTable from '../basic/date-table';
import ElInput from 'element-ui/packages/input';
import ElButton from 'element-ui/packages/button';
const advanceDate = (date, amount) => {
return new Date(new Date(date).getTime() + amount);
};
const calcDefaultValue = (defaultValue) => {
if (Array.isArray(defaultValue)) {
return [new Date(defaultValue[0]), new Date(defaultValue[1])];
} else if (defaultValue) {
return [new Date(defaultValue), advanceDate(defaultValue, 24 * 60 * 60 * 1000)];
} else {
return [new Date(), advanceDate(Date.now(), 24 * 60 * 60 * 1000)];
}
};
export default {
mixins: [Locale],
directives: { Clickoutside },
computed: {
btnDisabled() {
return !(this.minDate && this.maxDate && !this.selecting && this.isValidValue([this.minDate, this.maxDate]));
},
leftLabel() {
return this.leftDate.getFullYear() + ' ' + this.t('el.datepicker.year') + ' ' + this.t(`el.datepicker.month${ this.leftDate.getMonth() + 1 }`);
},
rightLabel() {
return this.rightDate.getFullYear() + ' ' + this.t('el.datepicker.year') + ' ' + this.t(`el.datepicker.month${ this.rightDate.getMonth() + 1 }`);
},
leftYear() {
return this.leftDate.getFullYear();
},
leftMonth() {
return this.leftDate.getMonth();
},
leftMonthDate() {
return this.leftDate.getDate();
},
rightYear() {
return this.rightDate.getFullYear();
},
rightMonth() {
return this.rightDate.getMonth();
},
rightMonthDate() {
return this.rightDate.getDate();
},
minVisibleDate() {
if (this.dateUserInput.min !== null) return this.dateUserInput.min;
if (this.minDate) return formatDate(this.minDate, this.dateFormat);
return '';
},
maxVisibleDate() {
if (this.dateUserInput.max !== null) return this.dateUserInput.max;
if (this.maxDate || this.minDate) return formatDate(this.maxDate || this.minDate, this.dateFormat);
return '';
},
minVisibleTime() {
if (this.timeUserInput.min !== null) return this.timeUserInput.min;
if (this.minDate) return formatDate(this.minDate, this.timeFormat);
return '';
},
maxVisibleTime() {
if (this.timeUserInput.max !== null) return this.timeUserInput.max;
if (this.maxDate || this.minDate) return formatDate(this.maxDate || this.minDate, this.timeFormat);
return '';
},
timeFormat() {
if (this.format) {
return extractTimeFormat(this.format);
} else {
return 'HH:mm:ss';
}
},
dateFormat() {
if (this.format) {
return extractDateFormat(this.format);
} else {
return 'yyyy-MM-dd';
}
},
enableMonthArrow() {
const nextMonth = (this.leftMonth + 1) % 12;
const yearOffset = this.leftMonth + 1 >= 12 ? 1 : 0;
return this.unlinkPanels && new Date(this.leftYear + yearOffset, nextMonth) < new Date(this.rightYear, this.rightMonth);
},
enableYearArrow() {
return this.unlinkPanels && this.rightYear * 12 + this.rightMonth - (this.leftYear * 12 + this.leftMonth + 1) >= 12;
}
},
data() {
return {
popperClass: '',
value: [],
defaultValue: null,
defaultTime: null,
minDate: '',
maxDate: '',
leftDate: new Date(),
rightDate: nextMonth(new Date()),
rangeState: {
endDate: null,
selecting: false,
row: null,
column: null
},
showTime: false,
shortcuts: '',
visible: '',
disabledDate: '',
firstDayOfWeek: 7,
minTimePickerVisible: false,
maxTimePickerVisible: false,
format: '',
arrowControl: false,
unlinkPanels: false,
dateUserInput: {
min: null,
max: null
},
timeUserInput: {
min: null,
max: null
}
};
},
watch: {
minDate(val) {
this.dateUserInput.min = null;
this.timeUserInput.min = null;
this.$nextTick(() => {
if (this.$refs.maxTimePicker && this.maxDate && this.maxDate < this.minDate) {
const format = 'HH:mm:ss';
this.$refs.maxTimePicker.selectableRange = [
[
parseDate(formatDate(this.minDate, format), format),
parseDate('23:59:59', format)
]
];
}
});
if (val && this.$refs.minTimePicker) {
this.$refs.minTimePicker.date = val;
this.$refs.minTimePicker.value = val;
}
},
maxDate(val) {
this.dateUserInput.max = null;
this.timeUserInput.max = null;
if (val && this.$refs.maxTimePicker) {
this.$refs.maxTimePicker.date = val;
this.$refs.maxTimePicker.value = val;
}
},
minTimePickerVisible(val) {
if (val) {
this.$nextTick(() => {
this.$refs.minTimePicker.date = this.minDate;
this.$refs.minTimePicker.value = this.minDate;
this.$refs.minTimePicker.adjustSpinners();
});
}
},
maxTimePickerVisible(val) {
if (val) {
this.$nextTick(() => {
this.$refs.maxTimePicker.date = this.maxDate;
this.$refs.maxTimePicker.value = this.maxDate;
this.$refs.maxTimePicker.adjustSpinners();
});
}
},
value(newVal) {
if (!newVal) {
this.minDate = null;
this.maxDate = null;
} else if (Array.isArray(newVal)) {
this.minDate = isDate(newVal[0]) ? new Date(newVal[0]) : null;
this.maxDate = isDate(newVal[1]) ? new Date(newVal[1]) : null;
if (this.minDate) {
this.leftDate = this.minDate;
if (this.unlinkPanels && this.maxDate) {
const minDateYear = this.minDate.getFullYear();
const minDateMonth = this.minDate.getMonth();
const maxDateYear = this.maxDate.getFullYear();
const maxDateMonth = this.maxDate.getMonth();
this.rightDate = minDateYear === maxDateYear && minDateMonth === maxDateMonth
? nextMonth(this.maxDate)
: this.maxDate;
} else {
this.rightDate = nextMonth(this.leftDate);
}
} else {
this.leftDate = calcDefaultValue(this.defaultValue)[0];
this.rightDate = nextMonth(this.leftDate);
}
}
},
defaultValue(val) {
if (!Array.isArray(this.value)) {
const [left, right] = calcDefaultValue(val);
this.leftDate = left;
this.rightDate = val && val[1] && this.unlinkPanels
? right
: nextMonth(this.leftDate);
}
}
},
methods: {
handleClear() {
this.minDate = null;
this.maxDate = null;
this.leftDate = calcDefaultValue(this.defaultValue)[0];
this.rightDate = nextMonth(this.leftDate);
this.$emit('pick', null);
},
handleChangeRange(val) {
this.minDate = val.minDate;
this.maxDate = val.maxDate;
this.rangeState = val.rangeState;
},
handleDateInput(value, type) {
this.dateUserInput[type] = value;
if (value.length !== this.dateFormat.length) return;
const parsedValue = parseDate(value, this.dateFormat);
if (parsedValue) {
if (typeof this.disabledDate === 'function' &&
this.disabledDate(new Date(parsedValue))) {
return;
}
if (type === 'min') {
this.minDate = modifyDate(this.minDate || new Date(), parsedValue.getFullYear(), parsedValue.getMonth(), parsedValue.getDate());
this.leftDate = new Date(parsedValue);
if (!this.unlinkPanels) {
this.rightDate = nextMonth(this.leftDate);
}
} else {
this.maxDate = modifyDate(this.maxDate || new Date(), parsedValue.getFullYear(), parsedValue.getMonth(), parsedValue.getDate());
this.rightDate = new Date(parsedValue);
if (!this.unlinkPanels) {
this.leftDate = prevMonth(parsedValue);
}
}
}
},
handleDateChange(value, type) {
const parsedValue = parseDate(value, this.dateFormat);
if (parsedValue) {
if (type === 'min') {
this.minDate = modifyDate(this.minDate, parsedValue.getFullYear(), parsedValue.getMonth(), parsedValue.getDate());
if (this.minDate > this.maxDate) {
this.maxDate = this.minDate;
}
} else {
this.maxDate = modifyDate(this.maxDate, parsedValue.getFullYear(), parsedValue.getMonth(), parsedValue.getDate());
if (this.maxDate < this.minDate) {
this.minDate = this.maxDate;
}
}
}
},
handleTimeInput(value, type) {
this.timeUserInput[type] = value;
if (value.length !== this.timeFormat.length) return;
const parsedValue = parseDate(value, this.timeFormat);
if (parsedValue) {
if (type === 'min') {
this.minDate = modifyTime(this.minDate, parsedValue.getHours(), parsedValue.getMinutes(), parsedValue.getSeconds());
this.$nextTick(_ => this.$refs.minTimePicker.adjustSpinners());
} else {
this.maxDate = modifyTime(this.maxDate, parsedValue.getHours(), parsedValue.getMinutes(), parsedValue.getSeconds());
this.$nextTick(_ => this.$refs.maxTimePicker.adjustSpinners());
}
}
},
handleTimeChange(value, type) {
const parsedValue = parseDate(value, this.timeFormat);
if (parsedValue) {
if (type === 'min') {
this.minDate = modifyTime(this.minDate, parsedValue.getHours(), parsedValue.getMinutes(), parsedValue.getSeconds());
if (this.minDate > this.maxDate) {
this.maxDate = this.minDate;
}
this.$refs.minTimePicker.value = this.minDate;
this.minTimePickerVisible = false;
} else {
this.maxDate = modifyTime(this.maxDate, parsedValue.getHours(), parsedValue.getMinutes(), parsedValue.getSeconds());
if (this.maxDate < this.minDate) {
this.minDate = this.maxDate;
}
this.$refs.maxTimePicker.value = this.minDate;
this.maxTimePickerVisible = false;
}
}
},
handleRangePick(val, close = true) {
const defaultTime = this.defaultTime || [];
const minDate = modifyWithTimeString(val.minDate, defaultTime[0]);
const maxDate = modifyWithTimeString(val.maxDate, defaultTime[1]);
if (this.maxDate === maxDate && this.minDate === minDate) {
return;
}
this.onPick && this.onPick(val);
this.maxDate = maxDate;
this.minDate = minDate;
// workaround for https://github.com/ElemeFE/element/issues/7539, should remove this block when we don't have to care about Chromium 55 - 57
setTimeout(() => {
this.maxDate = maxDate;
this.minDate = minDate;
}, 10);
if (!close || this.showTime) return;
this.handleConfirm();
},
handleShortcutClick(shortcut) {
if (shortcut.onClick) {
shortcut.onClick(this);
}
},
handleMinTimePick(value, visible, first) {
this.minDate = this.minDate || new Date();
if (value) {
this.minDate = modifyTime(this.minDate, value.getHours(), value.getMinutes(), value.getSeconds());
}
if (!first) {
this.minTimePickerVisible = visible;
}
if (!this.maxDate || this.maxDate && this.maxDate.getTime() < this.minDate.getTime()) {
this.maxDate = new Date(this.minDate);
}
},
handleMinTimeClose() {
this.minTimePickerVisible = false;
},
handleMaxTimePick(value, visible, first) {
if (this.maxDate && value) {
this.maxDate = modifyTime(this.maxDate, value.getHours(), value.getMinutes(), value.getSeconds());
}
if (!first) {
this.maxTimePickerVisible = visible;
}
if (this.maxDate && this.minDate && this.minDate.getTime() > this.maxDate.getTime()) {
this.minDate = new Date(this.maxDate);
}
},
handleMaxTimeClose() {
this.maxTimePickerVisible = false;
},
// leftPrev*, rightNext* need to take care of `unlinkPanels`
leftPrevYear() {
this.leftDate = prevYear(this.leftDate);
if (!this.unlinkPanels) {
this.rightDate = nextMonth(this.leftDate);
}
},
leftPrevMonth() {
this.leftDate = prevMonth(this.leftDate);
if (!this.unlinkPanels) {
this.rightDate = nextMonth(this.leftDate);
}
},
rightNextYear() {
if (!this.unlinkPanels) {
this.leftDate = nextYear(this.leftDate);
this.rightDate = nextMonth(this.leftDate);
} else {
this.rightDate = nextYear(this.rightDate);
}
},
rightNextMonth() {
if (!this.unlinkPanels) {
this.leftDate = nextMonth(this.leftDate);
this.rightDate = nextMonth(this.leftDate);
} else {
this.rightDate = nextMonth(this.rightDate);
}
},
// leftNext*, rightPrev* are called when `unlinkPanels` is true
leftNextYear() {
this.leftDate = nextYear(this.leftDate);
},
leftNextMonth() {
this.leftDate = nextMonth(this.leftDate);
},
rightPrevYear() {
this.rightDate = prevYear(this.rightDate);
},
rightPrevMonth() {
this.rightDate = prevMonth(this.rightDate);
},
handleConfirm(visible = false) {
if (this.isValidValue([this.minDate, this.maxDate])) {
this.$emit('pick', [this.minDate, this.maxDate], visible);
}
},
isValidValue(value) {
return Array.isArray(value) &&
value && value[0] && value[1] &&
isDate(value[0]) && isDate(value[1]) &&
value[0].getTime() <= value[1].getTime() && (
typeof this.disabledDate === 'function'
? !this.disabledDate(value[0]) && !this.disabledDate(value[1])
: true
);
},
resetView() {
// NOTE: this is a hack to reset {min, max}Date on picker open.
// TODO: correct way of doing so is to refactor {min, max}Date to be dependent on value and internal selection state
// an alternative would be resetView whenever picker becomes visible, should also investigate date-panel's resetView
this.minDate = this.value && isDate(this.value[0]) ? new Date(this.value[0]) : null;
this.maxDate = this.value && isDate(this.value[0]) ? new Date(this.value[1]) : null;
}
},
components: { TimePicker, DateTable, ElInput, ElButton }
};
</script>
+595
View File
@@ -0,0 +1,595 @@
<template>
<transition name="el-zoom-in-top" @after-enter="handleEnter" @after-leave="handleLeave">
<div
v-show="visible"
class="el-picker-panel el-date-picker el-popper"
:class="[{
'has-sidebar': $slots.sidebar || shortcuts,
'has-time': showTime
}, popperClass]">
<div class="el-picker-panel__body-wrapper">
<slot name="sidebar" class="el-picker-panel__sidebar"></slot>
<div class="el-picker-panel__sidebar" v-if="shortcuts">
<button
type="button"
class="el-picker-panel__shortcut"
v-for="(shortcut, key) in shortcuts"
:key="key"
@click="handleShortcutClick(shortcut)">{{ shortcut.text }}</button>
</div>
<div class="el-picker-panel__body">
<div class="el-date-picker__time-header" v-if="showTime">
<span class="el-date-picker__editor-wrap">
<el-input
:placeholder="t('el.datepicker.selectDate')"
:value="visibleDate"
size="small"
@input="val => userInputDate = val"
@change="handleVisibleDateChange" />
</span>
<span class="el-date-picker__editor-wrap" v-clickoutside="handleTimePickClose">
<el-input
ref="input"
@focus="timePickerVisible = true"
:placeholder="t('el.datepicker.selectTime')"
:value="visibleTime"
size="small"
@input="val => userInputTime = val"
@change="handleVisibleTimeChange" />
<time-picker
ref="timepicker"
:time-arrow-control="arrowControl"
@pick="handleTimePick"
:visible="timePickerVisible"
@mounted="proxyTimePickerDataProperties">
</time-picker>
</span>
</div>
<div
class="el-date-picker__header"
:class="{ 'el-date-picker__header--bordered': currentView === 'year' || currentView === 'month' }"
v-show="currentView !== 'time'">
<button
type="button"
@click="prevYear"
:aria-label="t(`el.datepicker.prevYear`)"
class="el-picker-panel__icon-btn el-date-picker__prev-btn el-icon-d-arrow-left">
</button>
<button
type="button"
@click="prevMonth"
v-show="currentView === 'date'"
:aria-label="t(`el.datepicker.prevMonth`)"
class="el-picker-panel__icon-btn el-date-picker__prev-btn el-icon-arrow-left">
</button>
<span
@click="showYearPicker"
role="button"
class="el-date-picker__header-label">{{ yearLabel }}</span>
<span
@click="showMonthPicker"
v-show="currentView === 'date'"
role="button"
class="el-date-picker__header-label"
:class="{ active: currentView === 'month' }">{{t(`el.datepicker.month${ month + 1 }`)}}</span>
<button
type="button"
@click="nextYear"
:aria-label="t(`el.datepicker.nextYear`)"
class="el-picker-panel__icon-btn el-date-picker__next-btn el-icon-d-arrow-right">
</button>
<button
type="button"
@click="nextMonth"
v-show="currentView === 'date'"
:aria-label="t(`el.datepicker.nextMonth`)"
class="el-picker-panel__icon-btn el-date-picker__next-btn el-icon-arrow-right">
</button>
</div>
<div class="el-picker-panel__content">
<date-table
v-show="currentView === 'date'"
@pick="handleDatePick"
:selection-mode="selectionMode"
:first-day-of-week="firstDayOfWeek"
:value="value"
:default-value="defaultValue ? new Date(defaultValue) : null"
:date="date"
:disabled-date="disabledDate">
</date-table>
<year-table
v-show="currentView === 'year'"
@pick="handleYearPick"
:value="value"
:default-value="defaultValue ? new Date(defaultValue) : null"
:date="date"
:disabled-date="disabledDate">
</year-table>
<month-table
v-show="currentView === 'month'"
@pick="handleMonthPick"
:value="value"
:default-value="defaultValue ? new Date(defaultValue) : null"
:date="date"
:disabled-date="disabledDate">
</month-table>
</div>
</div>
</div>
<div
class="el-picker-panel__footer"
v-show="footerVisible && currentView === 'date'">
<el-button
size="mini"
type="text"
class="el-picker-panel__link-btn"
@click="changeToNow"
v-show="selectionMode !== 'dates'">
{{ t('el.datepicker.now') }}
</el-button>
<el-button
plain
size="mini"
class="el-picker-panel__link-btn"
@click="confirm">
{{ t('el.datepicker.confirm') }}
</el-button>
</div>
</div>
</transition>
</template>
<script type="text/babel">
import {
formatDate,
parseDate,
getWeekNumber,
isDate,
modifyDate,
modifyTime,
modifyWithTimeString,
clearMilliseconds,
clearTime,
prevYear,
nextYear,
prevMonth,
nextMonth,
changeYearMonthAndClampDate,
extractDateFormat,
extractTimeFormat,
timeWithinRange
} from '../util';
import Clickoutside from 'element-ui/src/utils/clickoutside';
import Locale from 'element-ui/src/mixins/locale';
import ElInput from 'element-ui/packages/input';
import ElButton from 'element-ui/packages/button';
import TimePicker from './time';
import YearTable from '../basic/year-table';
import MonthTable from '../basic/month-table';
import DateTable from '../basic/date-table';
export default {
mixins: [Locale],
directives: { Clickoutside },
watch: {
showTime(val) {
/* istanbul ignore if */
if (!val) return;
this.$nextTick(_ => {
const inputElm = this.$refs.input.$el;
if (inputElm) {
this.pickerWidth = inputElm.getBoundingClientRect().width + 10;
}
});
},
value(val) {
if (this.selectionMode === 'dates' && this.value) return;
if (isDate(val)) {
this.date = new Date(val);
} else {
this.date = this.getDefaultValue();
}
},
defaultValue(val) {
if (!isDate(this.value)) {
this.date = val ? new Date(val) : new Date();
}
},
timePickerVisible(val) {
if (val) this.$nextTick(() => this.$refs.timepicker.adjustSpinners());
},
selectionMode(newVal) {
if (newVal === 'month') {
/* istanbul ignore next */
if (this.currentView !== 'year' || this.currentView !== 'month') {
this.currentView = 'month';
}
} else if (newVal === 'dates') {
this.currentView = 'date';
}
}
},
methods: {
proxyTimePickerDataProperties() {
const format = timeFormat => {this.$refs.timepicker.format = timeFormat;};
const value = value => {this.$refs.timepicker.value = value;};
const date = date => {this.$refs.timepicker.date = date;};
const selectableRange = selectableRange => {this.$refs.timepicker.selectableRange = selectableRange;};
this.$watch('value', value);
this.$watch('date', date);
this.$watch('selectableRange', selectableRange);
format(this.timeFormat);
value(this.value);
date(this.date);
selectableRange(this.selectableRange);
},
handleClear() {
this.date = this.getDefaultValue();
this.$emit('pick', null);
},
emit(value, ...args) {
if (!value) {
this.$emit('pick', value, ...args);
} else if (Array.isArray(value)) {
const dates = value.map(date => this.showTime ? clearMilliseconds(date) : clearTime(date));
this.$emit('pick', dates, ...args);
} else {
this.$emit('pick', this.showTime ? clearMilliseconds(value) : clearTime(value), ...args);
}
this.userInputDate = null;
this.userInputTime = null;
},
// resetDate() {
// this.date = new Date(this.date);
// },
showMonthPicker() {
this.currentView = 'month';
},
showYearPicker() {
this.currentView = 'year';
},
// XXX: 没用到
// handleLabelClick() {
// if (this.currentView === 'date') {
// this.showMonthPicker();
// } else if (this.currentView === 'month') {
// this.showYearPicker();
// }
// },
prevMonth() {
this.date = prevMonth(this.date);
},
nextMonth() {
this.date = nextMonth(this.date);
},
prevYear() {
if (this.currentView === 'year') {
this.date = prevYear(this.date, 10);
} else {
this.date = prevYear(this.date);
}
},
nextYear() {
if (this.currentView === 'year') {
this.date = nextYear(this.date, 10);
} else {
this.date = nextYear(this.date);
}
},
handleShortcutClick(shortcut) {
if (shortcut.onClick) {
shortcut.onClick(this);
}
},
handleTimePick(value, visible, first) {
if (isDate(value)) {
const newDate = this.value
? modifyTime(this.value, value.getHours(), value.getMinutes(), value.getSeconds())
: modifyWithTimeString(this.getDefaultValue(), this.defaultTime);
this.date = newDate;
this.emit(this.date, true);
} else {
this.emit(value, true);
}
if (!first) {
this.timePickerVisible = visible;
}
},
handleTimePickClose() {
this.timePickerVisible = false;
},
handleMonthPick(month) {
if (this.selectionMode === 'month') {
this.date = modifyDate(this.date, this.year, month, 1);
this.emit(this.date);
} else {
this.date = changeYearMonthAndClampDate(this.date, this.year, month);
// TODO: should emit intermediate value ??
// this.emit(this.date);
this.currentView = 'date';
}
},
handleDatePick(value) {
if (this.selectionMode === 'day') {
let newDate = this.value
? modifyDate(this.value, value.getFullYear(), value.getMonth(), value.getDate())
: modifyWithTimeString(value, this.defaultTime);
// change default time while out of selectableRange
if (!this.checkDateWithinRange(newDate)) {
newDate = modifyDate(this.selectableRange[0][0], value.getFullYear(), value.getMonth(), value.getDate());
}
this.date = newDate;
this.emit(this.date, this.showTime);
} else if (this.selectionMode === 'week') {
this.emit(value.date);
} else if (this.selectionMode === 'dates') {
this.emit(value, true); // set false to keep panel open
}
},
handleYearPick(year) {
if (this.selectionMode === 'year') {
this.date = modifyDate(this.date, year, 0, 1);
this.emit(this.date);
} else {
this.date = changeYearMonthAndClampDate(this.date, year, this.month);
// TODO: should emit intermediate value ??
// this.emit(this.date, true);
this.currentView = 'month';
}
},
changeToNow() {
// NOTE: not a permanent solution
// consider disable "now" button in the future
if ((!this.disabledDate || !this.disabledDate(new Date())) && this.checkDateWithinRange(new Date())) {
this.date = new Date();
this.emit(this.date);
}
},
confirm() {
if (this.selectionMode === 'dates') {
this.emit(this.value);
} else {
// value were emitted in handle{Date,Time}Pick, nothing to update here
// deal with the scenario where: user opens the picker, then confirm without doing anything
const value = this.value
? this.value
: modifyWithTimeString(this.getDefaultValue(), this.defaultTime);
this.date = new Date(value); // refresh date
this.emit(value);
}
},
resetView() {
if (this.selectionMode === 'month') {
this.currentView = 'month';
} else if (this.selectionMode === 'year') {
this.currentView = 'year';
} else {
this.currentView = 'date';
}
},
handleEnter() {
document.body.addEventListener('keydown', this.handleKeydown);
},
handleLeave() {
this.$emit('dodestroy');
document.body.removeEventListener('keydown', this.handleKeydown);
},
handleKeydown(event) {
const keyCode = event.keyCode;
const list = [38, 40, 37, 39];
if (this.visible && !this.timePickerVisible) {
if (list.indexOf(keyCode) !== -1) {
this.handleKeyControl(keyCode);
event.stopPropagation();
event.preventDefault();
}
if (keyCode === 13 && this.userInputDate === null && this.userInputTime === null) { // Enter
this.emit(this.date, false);
}
}
},
handleKeyControl(keyCode) {
const mapping = {
'year': {
38: -4, 40: 4, 37: -1, 39: 1, offset: (date, step) => date.setFullYear(date.getFullYear() + step)
},
'month': {
38: -4, 40: 4, 37: -1, 39: 1, offset: (date, step) => date.setMonth(date.getMonth() + step)
},
'week': {
38: -1, 40: 1, 37: -1, 39: 1, offset: (date, step) => date.setDate(date.getDate() + step * 7)
},
'day': {
38: -7, 40: 7, 37: -1, 39: 1, offset: (date, step) => date.setDate(date.getDate() + step)
}
};
const mode = this.selectionMode;
const year = 3.1536e10;
const now = this.date.getTime();
const newDate = new Date(this.date.getTime());
while (Math.abs(now - newDate.getTime()) <= year) {
const map = mapping[mode];
map.offset(newDate, map[keyCode]);
if (typeof this.disabledDate === 'function' && this.disabledDate(newDate)) {
continue;
}
this.date = newDate;
this.$emit('pick', newDate, true);
break;
}
},
handleVisibleTimeChange(value) {
const time = parseDate(value, this.timeFormat);
if (time && this.checkDateWithinRange(time)) {
this.date = modifyDate(time, this.year, this.month, this.monthDate);
this.userInputTime = null;
this.$refs.timepicker.value = this.date;
this.timePickerVisible = false;
this.emit(this.date, true);
}
},
handleVisibleDateChange(value) {
const date = parseDate(value, this.dateFormat);
if (date) {
if (typeof this.disabledDate === 'function' && this.disabledDate(date)) {
return;
}
this.date = modifyTime(date, this.date.getHours(), this.date.getMinutes(), this.date.getSeconds());
this.userInputDate = null;
this.resetView();
this.emit(this.date, true);
}
},
isValidValue(value) {
return value && !isNaN(value) && (
typeof this.disabledDate === 'function'
? !this.disabledDate(value)
: true
) && this.checkDateWithinRange(value);
},
getDefaultValue() {
// if default-value is set, return it
// otherwise, return now (the moment this method gets called)
return this.defaultValue ? new Date(this.defaultValue) : new Date();
},
checkDateWithinRange(date) {
return this.selectableRange.length > 0
? timeWithinRange(date, this.selectableRange, this.format || 'HH:mm:ss')
: true;
}
},
components: {
TimePicker, YearTable, MonthTable, DateTable, ElInput, ElButton
},
data() {
return {
popperClass: '',
date: new Date(),
value: '',
defaultValue: null, // use getDefaultValue() for time computation
defaultTime: null,
showTime: false,
selectionMode: 'day',
shortcuts: '',
visible: false,
currentView: 'date',
disabledDate: '',
selectableRange: [],
firstDayOfWeek: 7,
showWeekNumber: false,
timePickerVisible: false,
format: '',
arrowControl: false,
userInputDate: null,
userInputTime: null
};
},
computed: {
year() {
return this.date.getFullYear();
},
month() {
return this.date.getMonth();
},
week() {
return getWeekNumber(this.date);
},
monthDate() {
return this.date.getDate();
},
footerVisible() {
return this.showTime || this.selectionMode === 'dates';
},
visibleTime() {
if (this.userInputTime !== null) {
return this.userInputTime;
} else {
return formatDate(this.value || this.defaultValue, this.timeFormat);
}
},
visibleDate() {
if (this.userInputDate !== null) {
return this.userInputDate;
} else {
return formatDate(this.value || this.defaultValue, this.dateFormat);
}
},
yearLabel() {
const yearTranslation = this.t('el.datepicker.year');
if (this.currentView === 'year') {
const startYear = Math.floor(this.year / 10) * 10;
if (yearTranslation) {
return startYear + ' ' + yearTranslation + ' - ' + (startYear + 9) + ' ' + yearTranslation;
}
return startYear + ' - ' + (startYear + 9);
}
return this.year + ' ' + yearTranslation;
},
timeFormat() {
if (this.format) {
return extractTimeFormat(this.format);
} else {
return 'HH:mm:ss';
}
},
dateFormat() {
if (this.format) {
return extractDateFormat(this.format);
} else {
return 'yyyy-MM-dd';
}
}
}
};
</script>
+248
View File
@@ -0,0 +1,248 @@
<template>
<transition
name="el-zoom-in-top"
@after-leave="$emit('dodestroy')">
<div
v-show="visible"
class="el-time-range-picker el-picker-panel el-popper"
:class="popperClass">
<div class="el-time-range-picker__content">
<div class="el-time-range-picker__cell">
<div class="el-time-range-picker__header">{{ t('el.datepicker.startTime') }}</div>
<div
:class="{ 'has-seconds': showSeconds, 'is-arrow': arrowControl }"
class="el-time-range-picker__body el-time-panel__content">
<time-spinner
ref="minSpinner"
:show-seconds="showSeconds"
:am-pm-mode="amPmMode"
@change="handleMinChange"
:arrow-control="arrowControl"
@select-range="setMinSelectionRange"
:date="minDate">
</time-spinner>
</div>
</div>
<div class="el-time-range-picker__cell">
<div class="el-time-range-picker__header">{{ t('el.datepicker.endTime') }}</div>
<div
:class="{ 'has-seconds': showSeconds, 'is-arrow': arrowControl }"
class="el-time-range-picker__body el-time-panel__content">
<time-spinner
ref="maxSpinner"
:show-seconds="showSeconds"
:am-pm-mode="amPmMode"
@change="handleMaxChange"
:arrow-control="arrowControl"
@select-range="setMaxSelectionRange"
:date="maxDate">
</time-spinner>
</div>
</div>
</div>
<div class="el-time-panel__footer">
<button
type="button"
class="el-time-panel__btn cancel"
@click="handleCancel()">{{ t('el.datepicker.cancel') }}</button>
<button
type="button"
class="el-time-panel__btn confirm"
@click="handleConfirm()"
:disabled="btnDisabled">{{ t('el.datepicker.confirm') }}</button>
</div>
</div>
</transition>
</template>
<script type="text/babel">
import {
parseDate,
limitTimeRange,
modifyDate,
clearMilliseconds,
timeWithinRange
} from '../util';
import Locale from 'element-ui/src/mixins/locale';
import TimeSpinner from '../basic/time-spinner';
const MIN_TIME = parseDate('00:00:00', 'HH:mm:ss');
const MAX_TIME = parseDate('23:59:59', 'HH:mm:ss');
const minTimeOfDay = function(date) {
return modifyDate(MIN_TIME, date.getFullYear(), date.getMonth(), date.getDate());
};
const maxTimeOfDay = function(date) {
return modifyDate(MAX_TIME, date.getFullYear(), date.getMonth(), date.getDate());
};
// increase time by amount of milliseconds, but within the range of day
const advanceTime = function(date, amount) {
return new Date(Math.min(date.getTime() + amount, maxTimeOfDay(date).getTime()));
};
export default {
mixins: [Locale],
components: { TimeSpinner },
computed: {
showSeconds() {
return (this.format || '').indexOf('ss') !== -1;
},
offset() {
return this.showSeconds ? 11 : 8;
},
spinner() {
return this.selectionRange[0] < this.offset ? this.$refs.minSpinner : this.$refs.maxSpinner;
},
btnDisabled() {
return this.minDate.getTime() > this.maxDate.getTime();
},
amPmMode() {
if ((this.format || '').indexOf('A') !== -1) return 'A';
if ((this.format || '').indexOf('a') !== -1) return 'a';
return '';
}
},
data() {
return {
popperClass: '',
minDate: new Date(),
maxDate: new Date(),
value: [],
oldValue: [new Date(), new Date()],
defaultValue: null,
format: 'HH:mm:ss',
visible: false,
selectionRange: [0, 2],
arrowControl: false
};
},
watch: {
value(value) {
if (Array.isArray(value)) {
this.minDate = new Date(value[0]);
this.maxDate = new Date(value[1]);
} else {
if (Array.isArray(this.defaultValue)) {
this.minDate = new Date(this.defaultValue[0]);
this.maxDate = new Date(this.defaultValue[1]);
} else if (this.defaultValue) {
this.minDate = new Date(this.defaultValue);
this.maxDate = advanceTime(new Date(this.defaultValue), 60 * 60 * 1000);
} else {
this.minDate = new Date();
this.maxDate = advanceTime(new Date(), 60 * 60 * 1000);
}
}
},
visible(val) {
if (val) {
this.oldValue = this.value;
this.$nextTick(() => this.$refs.minSpinner.emitSelectRange('hours'));
}
}
},
methods: {
handleClear() {
this.$emit('pick', null);
},
handleCancel() {
this.$emit('pick', this.oldValue);
},
handleMinChange(date) {
this.minDate = clearMilliseconds(date);
this.handleChange();
},
handleMaxChange(date) {
this.maxDate = clearMilliseconds(date);
this.handleChange();
},
handleChange() {
if (this.isValidValue([this.minDate, this.maxDate])) {
this.$refs.minSpinner.selectableRange = [[minTimeOfDay(this.minDate), this.maxDate]];
this.$refs.maxSpinner.selectableRange = [[this.minDate, maxTimeOfDay(this.maxDate)]];
this.$emit('pick', [this.minDate, this.maxDate], true);
}
},
setMinSelectionRange(start, end) {
this.$emit('select-range', start, end, 'min');
this.selectionRange = [start, end];
},
setMaxSelectionRange(start, end) {
this.$emit('select-range', start, end, 'max');
this.selectionRange = [start + this.offset, end + this.offset];
},
handleConfirm(visible = false) {
const minSelectableRange = this.$refs.minSpinner.selectableRange;
const maxSelectableRange = this.$refs.maxSpinner.selectableRange;
this.minDate = limitTimeRange(this.minDate, minSelectableRange, this.format);
this.maxDate = limitTimeRange(this.maxDate, maxSelectableRange, this.format);
this.$emit('pick', [this.minDate, this.maxDate], visible);
},
adjustSpinners() {
this.$refs.minSpinner.adjustSpinners();
this.$refs.maxSpinner.adjustSpinners();
},
changeSelectionRange(step) {
const list = this.showSeconds ? [0, 3, 6, 11, 14, 17] : [0, 3, 8, 11];
const mapping = ['hours', 'minutes'].concat(this.showSeconds ? ['seconds'] : []);
const index = list.indexOf(this.selectionRange[0]);
const next = (index + step + list.length) % list.length;
const half = list.length / 2;
if (next < half) {
this.$refs.minSpinner.emitSelectRange(mapping[next]);
} else {
this.$refs.maxSpinner.emitSelectRange(mapping[next - half]);
}
},
isValidValue(date) {
return Array.isArray(date) &&
timeWithinRange(this.minDate, this.$refs.minSpinner.selectableRange) &&
timeWithinRange(this.maxDate, this.$refs.maxSpinner.selectableRange);
},
handleKeydown(event) {
const keyCode = event.keyCode;
const mapping = { 38: -1, 40: 1, 37: -1, 39: 1 };
// Left or Right
if (keyCode === 37 || keyCode === 39) {
const step = mapping[keyCode];
this.changeSelectionRange(step);
event.preventDefault();
return;
}
// Up or Down
if (keyCode === 38 || keyCode === 40) {
const step = mapping[keyCode];
this.spinner.scrollDown(step);
event.preventDefault();
return;
}
}
}
};
</script>
+178
View File
@@ -0,0 +1,178 @@
<template>
<transition name="el-zoom-in-top" @before-enter="handleMenuEnter" @after-leave="$emit('dodestroy')">
<div
ref="popper"
v-show="visible"
:style="{ width: width + 'px' }"
:class="popperClass"
class="el-picker-panel time-select el-popper">
<el-scrollbar noresize wrap-class="el-picker-panel__content">
<div class="time-select-item"
v-for="item in items"
:class="{ selected: value === item.value, disabled: item.disabled, default: item.value === defaultValue }"
:disabled="item.disabled"
:key="item.value"
@click="handleClick(item)">{{ item.value }}</div>
</el-scrollbar>
</div>
</transition>
</template>
<script type="text/babel">
import ElScrollbar from 'element-ui/packages/scrollbar';
import scrollIntoView from 'element-ui/src/utils/scroll-into-view';
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
};
}
/* istanbul ignore next */
return null;
};
const compareTime = function(time1, time2) {
const value1 = parseTime(time1);
const value2 = parseTime(time2);
const minutes1 = value1.minutes + value1.hours * 60;
const minutes2 = value2.minutes + value2.hours * 60;
if (minutes1 === minutes2) {
return 0;
}
return minutes1 > minutes2 ? 1 : -1;
};
const formatTime = function(time) {
return (time.hours < 10 ? '0' + time.hours : time.hours) + ':' + (time.minutes < 10 ? '0' + time.minutes : time.minutes);
};
const nextTime = function(time, step) {
const timeValue = parseTime(time);
const stepValue = parseTime(step);
const next = {
hours: timeValue.hours,
minutes: timeValue.minutes
};
next.minutes += stepValue.minutes;
next.hours += stepValue.hours;
next.hours += Math.floor(next.minutes / 60);
next.minutes = next.minutes % 60;
return formatTime(next);
};
export default {
components: { ElScrollbar },
watch: {
value(val) {
if (!val) return;
this.$nextTick(() => this.scrollToOption());
}
},
methods: {
handleClick(item) {
if (!item.disabled) {
this.$emit('pick', item.value);
}
},
handleClear() {
this.$emit('pick', null);
},
scrollToOption(selector = '.selected') {
const menu = this.$refs.popper.querySelector('.el-picker-panel__content');
scrollIntoView(menu, menu.querySelector(selector));
},
handleMenuEnter() {
const selected = this.items.map(item => item.value).indexOf(this.value) !== -1;
const hasDefault = this.items.map(item => item.value).indexOf(this.defaultValue) !== -1;
const option = (selected && '.selected') || (hasDefault && '.default') || '.time-select-item:not(.disabled)';
this.$nextTick(() => this.scrollToOption(option));
},
scrollDown(step) {
const items = this.items;
const length = items.length;
let total = items.length;
let index = items.map(item => item.value).indexOf(this.value);
while (total--) {
index = (index + step + length) % length;
if (!items[index].disabled) {
this.$emit('pick', items[index].value, true);
return;
}
}
},
isValidValue(date) {
return this.items.filter(item => !item.disabled).map(item => item.value).indexOf(date) !== -1;
},
handleKeydown(event) {
const keyCode = event.keyCode;
if (keyCode === 38 || keyCode === 40) {
const mapping = { 40: 1, 38: -1 };
const offset = mapping[keyCode.toString()];
this.scrollDown(offset);
event.stopPropagation();
return;
}
}
},
data() {
return {
popperClass: '',
start: '09:00',
end: '18:00',
step: '00:30',
value: '',
defaultValue: '',
visible: false,
minTime: '',
maxTime: '',
width: 0
};
},
computed: {
items() {
const start = this.start;
const end = this.end;
const step = this.step;
const result = [];
if (start && end && step) {
let current = start;
while (compareTime(current, end) <= 0) {
result.push({
value: current,
disabled: compareTime(current, this.minTime || '-1:-1') <= 0 ||
compareTime(current, this.maxTime || '100:100') >= 0
});
current = nextTime(current, step);
}
}
return result;
}
}
};
</script>
+186
View File
@@ -0,0 +1,186 @@
<template>
<transition name="el-zoom-in-top" @after-leave="$emit('dodestroy')">
<div
v-show="visible"
class="el-time-panel el-popper"
:class="popperClass">
<div class="el-time-panel__content" :class="{ 'has-seconds': showSeconds }">
<time-spinner
ref="spinner"
@change="handleChange"
:arrow-control="useArrow"
:show-seconds="showSeconds"
:am-pm-mode="amPmMode"
@select-range="setSelectionRange"
:date="date">
</time-spinner>
</div>
<div class="el-time-panel__footer">
<button
type="button"
class="el-time-panel__btn cancel"
@click="handleCancel">{{ t('el.datepicker.cancel') }}</button>
<button
type="button"
class="el-time-panel__btn"
:class="{confirm: !disabled}"
@click="handleConfirm()">{{ t('el.datepicker.confirm') }}</button>
</div>
</div>
</transition>
</template>
<script type="text/babel">
import { limitTimeRange, isDate, clearMilliseconds, timeWithinRange } from '../util';
import Locale from 'element-ui/src/mixins/locale';
import TimeSpinner from '../basic/time-spinner';
export default {
mixins: [Locale],
components: {
TimeSpinner
},
props: {
visible: Boolean,
timeArrowControl: Boolean
},
watch: {
visible(val) {
if (val) {
this.oldValue = this.value;
this.$nextTick(() => this.$refs.spinner.emitSelectRange('hours'));
} else {
this.needInitAdjust = true;
}
},
value(newVal) {
let date;
if (newVal instanceof Date) {
date = limitTimeRange(newVal, this.selectableRange, this.format);
} else if (!newVal) {
date = this.defaultValue ? new Date(this.defaultValue) : new Date();
}
this.date = date;
if (this.visible && this.needInitAdjust) {
this.$nextTick(_ => this.adjustSpinners());
this.needInitAdjust = false;
}
},
selectableRange(val) {
this.$refs.spinner.selectableRange = val;
},
defaultValue(val) {
if (!isDate(this.value)) {
this.date = val ? new Date(val) : new Date();
}
}
},
data() {
return {
popperClass: '',
format: 'HH:mm:ss',
value: '',
defaultValue: null,
date: new Date(),
oldValue: new Date(),
selectableRange: [],
selectionRange: [0, 2],
disabled: false,
arrowControl: false,
needInitAdjust: true
};
},
computed: {
showSeconds() {
return (this.format || '').indexOf('ss') !== -1;
},
useArrow() {
return this.arrowControl || this.timeArrowControl || false;
},
amPmMode() {
if ((this.format || '').indexOf('A') !== -1) return 'A';
if ((this.format || '').indexOf('a') !== -1) return 'a';
return '';
}
},
methods: {
handleCancel() {
this.$emit('pick', this.oldValue, false);
},
handleChange(date) {
// this.visible avoids edge cases, when use scrolls during panel closing animation
if (this.visible) {
this.date = clearMilliseconds(date);
// if date is out of range, do not emit
if (this.isValidValue(this.date)) {
this.$emit('pick', this.date, true);
}
}
},
setSelectionRange(start, end) {
this.$emit('select-range', start, end);
this.selectionRange = [start, end];
},
handleConfirm(visible = false, first) {
if (first) return;
const date = clearMilliseconds(limitTimeRange(this.date, this.selectableRange, this.format));
this.$emit('pick', date, visible, first);
},
handleKeydown(event) {
const keyCode = event.keyCode;
const mapping = { 38: -1, 40: 1, 37: -1, 39: 1 };
// Left or Right
if (keyCode === 37 || keyCode === 39) {
const step = mapping[keyCode];
this.changeSelectionRange(step);
event.preventDefault();
return;
}
// Up or Down
if (keyCode === 38 || keyCode === 40) {
const step = mapping[keyCode];
this.$refs.spinner.scrollDown(step);
event.preventDefault();
return;
}
},
isValidValue(date) {
return timeWithinRange(date, this.selectableRange, this.format);
},
adjustSpinners() {
return this.$refs.spinner.adjustSpinners();
},
changeSelectionRange(step) {
const list = [0, 3].concat(this.showSeconds ? [6] : []);
const mapping = ['hours', 'minutes'].concat(this.showSeconds ? ['seconds'] : []);
const index = list.indexOf(this.selectionRange[0]);
const next = (index + step + list.length) % list.length;
this.$refs.spinner.emitSelectRange(mapping[next]);
}
},
mounted() {
this.$nextTick(() => this.handleConfirm(true, true));
this.$emit('mounted');
}
};
</script>
+931
View File
@@ -0,0 +1,931 @@
<template>
<el-input
class="el-date-editor"
:class="'el-date-editor--' + type"
:readonly="!editable || readonly || type === 'dates'"
:disabled="pickerDisabled"
:size="pickerSize"
:name="name"
v-bind="firstInputId"
v-if="!ranged"
v-clickoutside="handleClose"
:placeholder="placeholder"
@focus="handleFocus"
@keydown.native="handleKeydown"
:value="displayValue"
@input="value => userInput = value"
@change="handleChange"
@mouseenter.native="handleMouseEnter"
@mouseleave.native="showClose = false"
:validateEvent="false"
ref="reference">
<i slot="prefix"
class="el-input__icon"
:class="triggerClass"
@click="handleFocus">
</i>
<i slot="suffix"
class="el-input__icon"
@click="handleClickIcon"
:class="[showClose ? '' + clearIcon : '']"
v-if="haveTrigger">
</i>
</el-input>
<div
class="el-date-editor el-range-editor el-input__inner"
:class="[
'el-date-editor--' + type,
pickerSize ? `el-range-editor--${ pickerSize }` : '',
pickerDisabled ? 'is-disabled' : '',
pickerVisible ? 'is-active' : ''
]"
@click="handleRangeClick"
@mouseenter="handleMouseEnter"
@mouseleave="showClose = false"
@keydown="handleKeydown"
ref="reference"
v-clickoutside="handleClose"
v-else>
<i :class="['el-input__icon', 'el-range__icon', triggerClass]"></i>
<input
autocomplete="off"
:placeholder="startPlaceholder"
:value="displayValue && displayValue[0]"
:disabled="pickerDisabled"
v-bind="firstInputId"
:readonly="!editable || readonly"
:name="name && name[0]"
@input="handleStartInput"
@change="handleStartChange"
@focus="handleFocus"
class="el-range-input">
<slot name="range-separator">
<span class="el-range-separator">{{ rangeSeparator }}</span>
</slot>
<input
autocomplete="off"
:placeholder="endPlaceholder"
:value="displayValue && displayValue[1]"
:disabled="pickerDisabled"
v-bind="secondInputId"
:readonly="!editable || readonly"
:name="name && name[1]"
@input="handleEndInput"
@change="handleEndChange"
@focus="handleFocus"
class="el-range-input">
<i
@click="handleClickIcon"
v-if="haveTrigger"
:class="[showClose ? '' + clearIcon : '']"
class="el-input__icon el-range__close-icon">
</i>
</div>
</template>
<script>
import Vue from 'vue';
import Clickoutside from 'element-ui/src/utils/clickoutside';
import { formatDate, parseDate, isDateObject, getWeekNumber } from './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';
import merge from 'element-ui/src/utils/merge';
const NewPopper = {
props: {
appendToBody: Popper.props.appendToBody,
offset: Popper.props.offset,
boundariesPadding: Popper.props.boundariesPadding,
arrowOffset: Popper.props.arrowOffset
},
methods: Popper.methods,
data() {
return merge({ visibleArrow: true }, Popper.data);
},
beforeDestroy: Popper.beforeDestroy
};
const DEFAULT_FORMATS = {
date: 'yyyy-MM-dd',
month: 'yyyy-MM',
datetime: 'yyyy-MM-dd HH:mm:ss',
time: 'HH:mm:ss',
week: 'yyyywWW',
timerange: 'HH:mm:ss',
daterange: 'yyyy-MM-dd',
datetimerange: 'yyyy-MM-dd HH:mm:ss',
year: 'yyyy'
};
const HAVE_TRIGGER_TYPES = [
'date',
'datetime',
'time',
'time-select',
'week',
'month',
'year',
'daterange',
'timerange',
'datetimerange',
'dates'
];
const DATE_FORMATTER = function(value, format) {
if (format === 'timestamp') return value.getTime();
return formatDate(value, format);
};
const DATE_PARSER = function(text, format) {
if (format === 'timestamp') return new Date(Number(text));
return parseDate(text, format);
};
const RANGE_FORMATTER = function(value, format) {
if (Array.isArray(value) && value.length === 2) {
const start = value[0];
const end = value[1];
if (start && end) {
return [DATE_FORMATTER(start, format), DATE_FORMATTER(end, format)];
}
}
return '';
};
const RANGE_PARSER = function(array, format, separator) {
if (!Array.isArray(array)) {
array = array.split(separator);
}
if (array.length === 2) {
const range1 = array[0];
const range2 = array[1];
return [DATE_PARSER(range1, format), DATE_PARSER(range2, format)];
}
return [];
};
const TYPE_VALUE_RESOLVER_MAP = {
default: {
formatter(value) {
if (!value) return '';
return '' + value;
},
parser(text) {
if (text === undefined || text === '') return null;
return text;
}
},
week: {
formatter(value, format) {
let week = getWeekNumber(value);
let month = value.getMonth();
const trueDate = new Date(value);
if (week === 1 && month === 11) {
trueDate.setHours(0, 0, 0, 0);
trueDate.setDate(trueDate.getDate() + 3 - (trueDate.getDay() + 6) % 7);
}
let date = formatDate(trueDate, format);
date = /WW/.test(date)
? date.replace(/WW/, week < 10 ? '0' + week : week)
: date.replace(/W/, week);
return date;
},
parser(text) {
const array = (text || '').split('w');
if (array.length === 2) {
const year = Number(array[0]);
const month = Number(array[1]);
if (!isNaN(year) && !isNaN(month) && month < 54) {
return text;
}
}
return null;
}
},
date: {
formatter: DATE_FORMATTER,
parser: DATE_PARSER
},
datetime: {
formatter: DATE_FORMATTER,
parser: DATE_PARSER
},
daterange: {
formatter: RANGE_FORMATTER,
parser: RANGE_PARSER
},
datetimerange: {
formatter: RANGE_FORMATTER,
parser: RANGE_PARSER
},
timerange: {
formatter: RANGE_FORMATTER,
parser: RANGE_PARSER
},
time: {
formatter: DATE_FORMATTER,
parser: DATE_PARSER
},
month: {
formatter: DATE_FORMATTER,
parser: DATE_PARSER
},
year: {
formatter: DATE_FORMATTER,
parser: DATE_PARSER
},
number: {
formatter(value) {
if (!value) return '';
return '' + value;
},
parser(text) {
let result = Number(text);
if (!isNaN(text)) {
return result;
} else {
return null;
}
}
},
dates: {
formatter(value, format) {
return value.map(date => DATE_FORMATTER(date, format));
},
parser(value, format) {
return (typeof value === 'string' ? value.split(', ') : value)
.map(date => date instanceof Date ? date : DATE_PARSER(date, format));
}
}
};
const PLACEMENT_MAP = {
left: 'bottom-start',
center: 'bottom',
right: 'bottom-end'
};
const parseAsFormatAndType = (value, customFormat, type, rangeSeparator = '-') => {
if (!value) return null;
const parser = (
TYPE_VALUE_RESOLVER_MAP[type] ||
TYPE_VALUE_RESOLVER_MAP['default']
).parser;
const format = customFormat || DEFAULT_FORMATS[type];
return parser(value, format, rangeSeparator);
};
const formatAsFormatAndType = (value, customFormat, type) => {
if (!value) return null;
const formatter = (
TYPE_VALUE_RESOLVER_MAP[type] ||
TYPE_VALUE_RESOLVER_MAP['default']
).formatter;
const format = customFormat || DEFAULT_FORMATS[type];
return formatter(value, format);
};
/*
* Considers:
* 1. Date object
* 2. date string
* 3. array of 1 or 2
*/
const valueEquals = function(a, b) {
// considers Date object and string
const dateEquals = function(a, b) {
const aIsDate = a instanceof Date;
const bIsDate = b instanceof Date;
if (aIsDate && bIsDate) {
return a.getTime() === b.getTime();
}
if (!aIsDate && !bIsDate) {
return a === b;
}
return false;
};
const aIsArray = a instanceof Array;
const bIsArray = b instanceof Array;
if (aIsArray && bIsArray) {
if (a.length !== b.length) {
return false;
}
return a.every((item, index) => dateEquals(item, b[index]));
}
if (!aIsArray && !bIsArray) {
return dateEquals(a, b);
}
return false;
};
const isString = function(val) {
return typeof val === 'string' || val instanceof String;
};
const validator = function(val) {
// either: String, Array of String, null / undefined
return (
val === null ||
val === undefined ||
isString(val) ||
(Array.isArray(val) && val.length === 2 && val.every(isString))
);
};
export default {
mixins: [Emitter, NewPopper],
inject: {
elForm: {
default: ''
},
elFormItem: {
default: ''
}
},
props: {
size: String,
format: String,
valueFormat: String,
readonly: Boolean,
placeholder: String,
startPlaceholder: String,
endPlaceholder: String,
prefixIcon: String,
clearIcon: {
type: String,
default: 'el-icon-circle-close'
},
name: {
default: '',
validator
},
disabled: Boolean,
clearable: {
type: Boolean,
default: true
},
id: {
default: '',
validator
},
popperClass: String,
editable: {
type: Boolean,
default: true
},
align: {
type: String,
default: 'left'
},
value: {},
defaultValue: {},
defaultTime: {},
rangeSeparator: {
default: '-'
},
pickerOptions: {},
unlinkPanels: Boolean,
validateEvent: {
type: Boolean,
default: true
}
},
components: { ElInput },
directives: { Clickoutside },
data() {
return {
pickerVisible: false,
showClose: false,
userInput: null,
valueOnOpen: null, // value when picker opens, used to determine whether to emit change
unwatchPickerOptions: null
};
},
watch: {
pickerVisible(val) {
if (this.readonly || this.pickerDisabled) return;
if (val) {
this.showPicker();
this.valueOnOpen = Array.isArray(this.value) ? [...this.value] : this.value;
} else {
this.hidePicker();
this.emitChange(this.value);
this.userInput = null;
if (this.validateEvent) {
this.dispatch('ElFormItem', 'el.form.blur');
}
this.$emit('blur', this);
this.blur();
}
},
parsedValue: {
immediate: true,
handler(val) {
if (this.picker) {
this.picker.value = val;
}
}
},
defaultValue(val) {
// NOTE: should eventually move to jsx style picker + panel ?
if (this.picker) {
this.picker.defaultValue = val;
}
},
value(val, oldVal) {
if (!valueEquals(val, oldVal) && !this.pickerVisible && this.validateEvent) {
this.dispatch('ElFormItem', 'el.form.change', val);
}
}
},
computed: {
ranged() {
return this.type.indexOf('range') > -1;
},
reference() {
const reference = this.$refs.reference;
return reference.$el || reference;
},
refInput() {
if (this.reference) {
return [].slice.call(this.reference.querySelectorAll('input'));
}
return [];
},
valueIsEmpty() {
const val = this.value;
if (Array.isArray(val)) {
for (let i = 0, len = val.length; i < len; i++) {
if (val[i]) {
return false;
}
}
} else {
if (val) {
return false;
}
}
return true;
},
triggerClass() {
return this.prefixIcon || (this.type.indexOf('time') !== -1 ? 'el-icon-time' : 'el-icon-date');
},
selectionMode() {
if (this.type === 'week') {
return 'week';
} else if (this.type === 'month') {
return 'month';
} else if (this.type === 'year') {
return 'year';
} else if (this.type === 'dates') {
return 'dates';
}
return 'day';
},
haveTrigger() {
if (typeof this.showTrigger !== 'undefined') {
return this.showTrigger;
}
return HAVE_TRIGGER_TYPES.indexOf(this.type) !== -1;
},
displayValue() {
const formattedValue = formatAsFormatAndType(this.parsedValue, this.format, this.type, this.rangeSeparator);
if (Array.isArray(this.userInput)) {
return [
this.userInput[0] || (formattedValue && formattedValue[0]) || '',
this.userInput[1] || (formattedValue && formattedValue[1]) || ''
];
} else if (this.userInput !== null) {
return this.userInput;
} else if (formattedValue) {
return this.type === 'dates'
? formattedValue.join(', ')
: formattedValue;
} else {
return '';
}
},
parsedValue() {
if (!this.value) return this.value; // component value is not set
if (this.type === 'time-select') return this.value; // time-select does not require parsing, this might change in next major version
const valueIsDateObject = isDateObject(this.value) || (Array.isArray(this.value) && this.value.every(isDateObject));
if (valueIsDateObject) {
return this.value;
}
if (this.valueFormat) {
return parseAsFormatAndType(this.value, this.valueFormat, this.type, this.rangeSeparator) || this.value;
}
// NOTE: deal with common but incorrect usage, should remove in next major version
// user might provide string / timestamp without value-format, coerce them into date (or array of date)
return Array.isArray(this.value) ? this.value.map(val => new Date(val)) : new Date(this.value);
},
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
pickerSize() {
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
},
pickerDisabled() {
return this.disabled || (this.elForm || {}).disabled;
},
firstInputId() {
const obj = {};
let id;
if (this.ranged) {
id = this.id && this.id[0];
} else {
id = this.id;
}
if (id) obj.id = id;
return obj;
},
secondInputId() {
const obj = {};
let id;
if (this.ranged) {
id = this.id && this.id[1];
}
if (id) obj.id = id;
return obj;
}
},
created() {
// vue-popper
this.popperOptions = {
boundariesPadding: 0,
gpuAcceleration: false
};
this.placement = PLACEMENT_MAP[this.align] || PLACEMENT_MAP.left;
this.$on('fieldReset', this.handleFieldReset);
},
methods: {
focus() {
if (!this.ranged) {
this.$refs.reference.focus();
} else {
this.handleFocus();
}
},
blur() {
this.refInput.forEach(input => input.blur());
},
// {parse, formatTo} Value deals maps component value with internal Date
parseValue(value) {
const isParsed = isDateObject(value) || (Array.isArray(value) && value.every(isDateObject));
if (this.valueFormat && !isParsed) {
return parseAsFormatAndType(value, this.valueFormat, this.type, this.rangeSeparator) || value;
} else {
return value;
}
},
formatToValue(date) {
const isFormattable = isDateObject(date) || (Array.isArray(date) && date.every(isDateObject));
if (this.valueFormat && isFormattable) {
return formatAsFormatAndType(date, this.valueFormat, this.type, this.rangeSeparator);
} else {
return date;
}
},
// {parse, formatTo} String deals with user input
parseString(value) {
const type = Array.isArray(value) ? this.type : this.type.replace('range', '');
return parseAsFormatAndType(value, this.format, type);
},
formatToString(value) {
const type = Array.isArray(value) ? this.type : this.type.replace('range', '');
return formatAsFormatAndType(value, this.format, type);
},
handleMouseEnter() {
if (this.readonly || this.pickerDisabled) return;
if (!this.valueIsEmpty && this.clearable) {
this.showClose = true;
}
},
handleChange() {
if (this.userInput) {
const value = this.parseString(this.displayValue);
if (value) {
this.picker.value = value;
if (this.isValidValue(value)) {
this.emitInput(value);
this.userInput = null;
}
}
}
if (this.userInput === '') {
this.emitInput(null);
this.emitChange(null);
this.userInput = null;
}
},
handleStartInput(event) {
if (this.userInput) {
this.userInput = [event.target.value, this.userInput[1]];
} else {
this.userInput = [event.target.value, null];
}
},
handleEndInput(event) {
if (this.userInput) {
this.userInput = [this.userInput[0], event.target.value];
} else {
this.userInput = [null, event.target.value];
}
},
handleStartChange(event) {
const value = this.parseString(this.userInput && this.userInput[0]);
if (value) {
this.userInput = [this.formatToString(value), this.displayValue[1]];
const newValue = [value, this.picker.value && this.picker.value[1]];
this.picker.value = newValue;
if (this.isValidValue(newValue)) {
this.emitInput(newValue);
this.userInput = null;
}
}
},
handleEndChange(event) {
const value = this.parseString(this.userInput && this.userInput[1]);
if (value) {
this.userInput = [this.displayValue[0], this.formatToString(value)];
const newValue = [this.picker.value && this.picker.value[0], value];
this.picker.value = newValue;
if (this.isValidValue(newValue)) {
this.emitInput(newValue);
this.userInput = null;
}
}
},
handleClickIcon(event) {
if (this.readonly || this.pickerDisabled) return;
if (this.showClose) {
this.valueOnOpen = this.value;
event.stopPropagation();
this.emitInput(null);
this.emitChange(null);
this.showClose = false;
if (this.picker && typeof this.picker.handleClear === 'function') {
this.picker.handleClear();
}
} else {
this.pickerVisible = !this.pickerVisible;
}
},
handleClose() {
if (!this.pickerVisible) return;
this.pickerVisible = false;
if (this.type === 'dates') {
// restore to former value
const oldValue = parseAsFormatAndType(this.valueOnOpen, this.valueFormat, this.type, this.rangeSeparator) || this.valueOnOpen;
this.emitInput(oldValue);
}
},
handleFieldReset(initialValue) {
this.userInput = initialValue === '' ? null : initialValue;
},
handleFocus() {
const type = this.type;
if (HAVE_TRIGGER_TYPES.indexOf(type) !== -1 && !this.pickerVisible) {
this.pickerVisible = true;
}
this.$emit('focus', this);
},
handleKeydown(event) {
const keyCode = event.keyCode;
// ESC
if (keyCode === 27) {
this.pickerVisible = false;
event.stopPropagation();
return;
}
// Tab
if (keyCode === 9) {
if (!this.ranged) {
this.handleChange();
this.pickerVisible = this.picker.visible = false;
this.blur();
event.stopPropagation();
} else {
// user may change focus between two input
setTimeout(() => {
if (this.refInput.indexOf(document.activeElement) === -1) {
this.pickerVisible = false;
this.blur();
event.stopPropagation();
}
}, 0);
}
return;
}
// Enter
if (keyCode === 13) {
if (this.userInput === '' || this.isValidValue(this.parseString(this.displayValue))) {
this.handleChange();
this.pickerVisible = this.picker.visible = false;
this.blur();
}
event.stopPropagation();
return;
}
// if user is typing, do not let picker handle key input
if (this.userInput) {
event.stopPropagation();
return;
}
// delegate other keys to panel
if (this.picker && this.picker.handleKeydown) {
this.picker.handleKeydown(event);
}
},
handleRangeClick() {
const type = this.type;
if (HAVE_TRIGGER_TYPES.indexOf(type) !== -1 && !this.pickerVisible) {
this.pickerVisible = true;
}
this.$emit('focus', this);
},
hidePicker() {
if (this.picker) {
this.picker.resetView && this.picker.resetView();
this.pickerVisible = this.picker.visible = false;
this.destroyPopper();
}
},
showPicker() {
if (this.$isServer) return;
if (!this.picker) {
this.mountPicker();
}
this.pickerVisible = this.picker.visible = true;
this.updatePopper();
this.picker.value = this.parsedValue;
this.picker.resetView && this.picker.resetView();
this.$nextTick(() => {
this.picker.adjustSpinners && this.picker.adjustSpinners();
});
},
mountPicker() {
this.picker = new Vue(this.panel).$mount();
this.picker.defaultValue = this.defaultValue;
this.picker.defaultTime = this.defaultTime;
this.picker.popperClass = this.popperClass;
this.popperElm = this.picker.$el;
this.picker.width = this.reference.getBoundingClientRect().width;
this.picker.showTime = this.type === 'datetime' || this.type === 'datetimerange';
this.picker.selectionMode = this.selectionMode;
this.picker.unlinkPanels = this.unlinkPanels;
this.picker.arrowControl = this.arrowControl || this.timeArrowControl || false;
this.$watch('format', (format) => {
this.picker.format = format;
});
const updateOptions = () => {
const options = this.pickerOptions;
if (options && options.selectableRange) {
let ranges = options.selectableRange;
const parser = TYPE_VALUE_RESOLVER_MAP.datetimerange.parser;
const format = DEFAULT_FORMATS.timerange;
ranges = Array.isArray(ranges) ? ranges : [ranges];
this.picker.selectableRange = ranges.map(range => parser(range, format, this.rangeSeparator));
}
for (const option in options) {
if (options.hasOwnProperty(option) &&
// 忽略 time-picker 的该配置项
option !== 'selectableRange') {
this.picker[option] = options[option];
}
}
// main format must prevail over undocumented pickerOptions.format
if (this.format) {
this.picker.format = this.format;
}
};
updateOptions();
this.unwatchPickerOptions = this.$watch('pickerOptions', () => updateOptions(), { deep: true });
this.$el.appendChild(this.picker.$el);
this.picker.resetView && this.picker.resetView();
this.picker.$on('dodestroy', this.doDestroy);
this.picker.$on('pick', (date = '', visible = false) => {
this.userInput = null;
this.pickerVisible = this.picker.visible = visible;
this.emitInput(date);
this.picker.resetView && this.picker.resetView();
});
this.picker.$on('select-range', (start, end, pos) => {
if (this.refInput.length === 0) return;
if (!pos || pos === 'min') {
this.refInput[0].setSelectionRange(start, end);
this.refInput[0].focus();
} else if (pos === 'max') {
this.refInput[1].setSelectionRange(start, end);
this.refInput[1].focus();
}
});
},
unmountPicker() {
if (this.picker) {
this.picker.$destroy();
this.picker.$off();
if (typeof this.unwatchPickerOptions === 'function') {
this.unwatchPickerOptions();
}
this.picker.$el.parentNode.removeChild(this.picker.$el);
}
},
emitChange(val) {
// determine user real change only
if (!valueEquals(val, this.valueOnOpen)) {
this.$emit('change', val);
this.valueOnOpen = val;
if (this.validateEvent) {
this.dispatch('ElFormItem', 'el.form.change', val);
}
}
},
emitInput(val) {
const formatted = this.formatToValue(val);
if (!valueEquals(this.value, formatted)) {
this.$emit('input', formatted);
}
},
isValidValue(value) {
if (!this.picker) {
this.mountPicker();
}
if (this.picker.isValidValue) {
return value && this.picker.isValidValue(value);
} else {
return true;
}
}
}
};
</script>
+40
View File
@@ -0,0 +1,40 @@
import Picker from '../picker';
import DatePanel from '../panel/date';
import DateRangePanel from '../panel/date-range';
const getPanel = function(type) {
if (type === 'daterange' || type === 'datetimerange') {
return DateRangePanel;
}
return DatePanel;
};
export default {
mixins: [Picker],
name: 'ElDatePicker',
props: {
type: {
type: String,
default: 'date'
},
timeArrowControl: Boolean
},
watch: {
type(type) {
if (this.picker) {
this.unmountPicker();
this.panel = getPanel(type);
this.mountPicker();
} else {
this.panel = getPanel(type);
}
}
},
created() {
this.panel = getPanel(this.type);
}
};
+39
View File
@@ -0,0 +1,39 @@
import Picker from '../picker';
import TimePanel from '../panel/time';
import TimeRangePanel from '../panel/time-range';
export default {
mixins: [Picker],
name: 'ElTimePicker',
props: {
isRange: Boolean,
arrowControl: Boolean
},
data() {
return {
type: ''
};
},
watch: {
isRange(isRange) {
if (this.picker) {
this.unmountPicker();
this.type = isRange ? 'timerange' : 'time';
this.panel = isRange ? TimeRangePanel : TimePanel;
this.mountPicker();
} else {
this.type = isRange ? 'timerange' : 'time';
this.panel = isRange ? TimeRangePanel : TimePanel;
}
}
},
created() {
this.type = this.isRange ? 'timerange' : 'time';
this.panel = this.isRange ? TimeRangePanel : TimePanel;
}
};
+21
View File
@@ -0,0 +1,21 @@
import Picker from '../picker';
import Panel from '../panel/time-select';
export default {
mixins: [Picker],
name: 'ElTimeSelect',
componentName: 'ElTimeSelect',
props: {
type: {
type: String,
default: 'time-select'
}
},
beforeCreate() {
this.panel = Panel;
}
};
+273
View File
@@ -0,0 +1,273 @@
import dateUtil 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 = [];
for (let i = start; i <= end; i++) {
result.push(i);
}
return result;
};
export const toDate = function(date) {
return isDate(date) ? new Date(date) : null;
};
export const isDate = function(date) {
if (date === null || date === undefined) return false;
if (isNaN(new Date(date).getTime())) return false;
if (Array.isArray(date)) return false; // deal with `new Date([ new Date() ]) -> new Date()`
return true;
};
export const isDateObject = function(val) {
return val instanceof Date;
};
export const formatDate = function(date, format) {
date = toDate(date);
if (!date) return '';
return dateUtil.format(date, format || 'yyyy-MM-dd', getI18nSettings());
};
export const parseDate = function(string, format) {
return dateUtil.parse(string, format || 'yyyy-MM-dd', getI18nSettings());
};
export const getDayCountOfMonth = function(year, month) {
if (month === 3 || month === 5 || month === 8 || month === 10) {
return 30;
}
if (month === 1) {
if (year % 4 === 0 && year % 100 !== 0 || year % 400 === 0) {
return 29;
} else {
return 28;
}
}
return 31;
};
export const getDayCountOfYear = function(year) {
const isLeapYear = year % 400 === 0 || (year % 100 !== 0 && year % 4 === 0);
return isLeapYear ? 366 : 365;
};
export const getFirstDayOfMonth = function(date) {
const temp = new Date(date.getTime());
temp.setDate(1);
return temp.getDay();
};
// see: https://stackoverflow.com/questions/3674539/incrementing-a-date-in-javascript
// {prev, next} Date should work for Daylight Saving Time
// Adding 24 * 60 * 60 * 1000 does not work in the above scenario
export const prevDate = function(date, amount = 1) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate() - amount);
};
export const nextDate = function(date, amount = 1) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate() + amount);
};
export const getStartDateOfMonth = function(year, month) {
const result = new Date(year, month, 1);
const day = result.getDay();
if (day === 0) {
return prevDate(result, 7);
} else {
return prevDate(result, day);
}
};
export const getWeekNumber = function(src) {
if (!isDate(src)) return null;
const date = new Date(src.getTime());
date.setHours(0, 0, 0, 0);
// Thursday in current week decides the year.
date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
// January 4 is always in week 1.
const week1 = new Date(date.getFullYear(), 0, 4);
// Adjust to Thursday in week 1 and count number of weeks from date to week 1.
// Rounding should be fine for Daylight Saving Time. Its shift should never be more than 12 hours.
return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000 - 3 + (week1.getDay() + 6) % 7) / 7);
};
export const getRangeHours = function(ranges) {
const hours = [];
let disabledHours = [];
(ranges || []).forEach(range => {
const value = range.map(date => date.getHours());
disabledHours = disabledHours.concat(newArray(value[0], value[1]));
});
if (disabledHours.length) {
for (let i = 0; i < 24; i++) {
hours[i] = disabledHours.indexOf(i) === -1;
}
} else {
for (let i = 0; i < 24; i++) {
hours[i] = false;
}
}
return hours;
};
function setRangeData(arr, start, end, value) {
for (let i = start; i < end; i++) {
arr[i] = value;
}
}
export const getRangeMinutes = function(ranges, hour) {
const minutes = new Array(60);
if (ranges.length > 0) {
ranges.forEach(range => {
const start = range[0];
const end = range[1];
const startHour = start.getHours();
const startMinute = start.getMinutes();
const endHour = end.getHours();
const endMinute = end.getMinutes();
if (startHour === hour && endHour !== hour) {
setRangeData(minutes, startMinute, 60, true);
} else if (startHour === hour && endHour === hour) {
setRangeData(minutes, startMinute, endMinute + 1, true);
} else if (startHour !== hour && endHour === hour) {
setRangeData(minutes, 0, endMinute + 1, true);
} else if (startHour < hour && endHour > hour) {
setRangeData(minutes, 0, 60, true);
}
});
} else {
setRangeData(minutes, 0, 60, true);
}
return minutes;
};
export const range = function(n) {
// see https://stackoverflow.com/questions/3746725/create-a-javascript-array-containing-1-n
return Array.apply(null, {length: n}).map((_, n) => n);
};
export const modifyDate = function(date, y, m, d) {
return new Date(y, m, d, date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds());
};
export const modifyTime = function(date, h, m, s) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), h, m, s, date.getMilliseconds());
};
export const modifyWithTimeString = (date, time) => {
if (date == null || !time) {
return date;
}
time = parseDate(time, 'HH:mm:ss');
return modifyTime(date, time.getHours(), time.getMinutes(), time.getSeconds());
};
export const clearTime = function(date) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
};
export const clearMilliseconds = function(date) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds(), 0);
};
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 ndate = normalizeDate(date);
const nranges = ranges.map(range => range.map(normalizeDate));
if (nranges.some(nrange => ndate >= nrange[0] && ndate <= nrange[1])) return date;
let minDate = nranges[0][0];
let maxDate = nranges[0][0];
nranges.forEach(nrange => {
minDate = new Date(Math.min(nrange[0], minDate));
maxDate = new Date(Math.max(nrange[1], minDate));
});
const ret = ndate < minDate ? minDate : maxDate;
// preserve Year/Month/Date
return modifyDate(
ret,
date.getFullYear(),
date.getMonth(),
date.getDate()
);
};
export const timeWithinRange = function(date, selectableRange, format) {
const limitedDate = limitTimeRange(date, selectableRange, format);
return limitedDate.getTime() === date.getTime();
};
export const changeYearMonthAndClampDate = function(date, year, month) {
// clamp date to the number of days in `year`, `month`
// eg: (2010-1-31, 2010, 2) => 2010-2-28
const monthDate = Math.min(date.getDate(), getDayCountOfMonth(year, month));
return modifyDate(date, year, month, monthDate);
};
export const prevMonth = function(date) {
const year = date.getFullYear();
const month = date.getMonth();
return month === 0
? changeYearMonthAndClampDate(date, year - 1, 11)
: changeYearMonthAndClampDate(date, year, month - 1);
};
export const nextMonth = function(date) {
const year = date.getFullYear();
const month = date.getMonth();
return month === 11
? changeYearMonthAndClampDate(date, year + 1, 0)
: changeYearMonthAndClampDate(date, year, month + 1);
};
export const prevYear = function(date, amount = 1) {
const year = date.getFullYear();
const month = date.getMonth();
return changeYearMonthAndClampDate(date, year - amount, month);
};
export const nextYear = function(date, amount = 1) {
const year = date.getFullYear();
const month = date.getMonth();
return changeYearMonthAndClampDate(date, year + amount, month);
};
export const extractDateFormat = function(format) {
return format
.replace(/\W?m{1,2}|\W?ZZ/g, '')
.replace(/\W?h{1,2}|\W?s{1,3}|\W?a/gi, '')
.trim();
};
export const extractTimeFormat = function(format) {
return format
.replace(/\W?D{1,2}|\W?Do|\W?d{1,4}|\W?M{1,4}|\W?y{2,4}/g, '')
.trim();
};
+8
View File
@@ -0,0 +1,8 @@
import ElDialog from './src/component';
/* istanbul ignore next */
ElDialog.install = function(Vue) {
Vue.component(ElDialog.name, ElDialog);
};
export default ElDialog;
+201
View File
@@ -0,0 +1,201 @@
<template>
<transition
name="dialog-fade"
@after-enter="afterEnter"
@after-leave="afterLeave">
<div class="el-dialog__wrapper" v-show="visible" @click.self="handleWrapperClick">
<div
role="dialog"
aria-modal="true"
:aria-label="title || 'dialog'"
class="el-dialog"
:class="[{ 'is-fullscreen': fullscreen, 'el-dialog--center': center }, customClass]"
ref="dialog"
:style="style">
<div class="el-dialog__header">
<slot name="title">
<span class="el-dialog__title">{{ title }}</span>
</slot>
<button
type="button"
class="el-dialog__headerbtn"
aria-label="Close"
v-if="showClose"
@click="handleClose">
<i class="el-dialog__close el-icon el-icon-close"></i>
</button>
</div>
<div class="el-dialog__body" v-if="rendered"><slot></slot></div>
<div class="el-dialog__footer" v-if="$slots.footer">
<slot name="footer"></slot>
</div>
</div>
</div>
</transition>
</template>
<script>
import Popup from 'element-ui/src/utils/popup';
import Migrating from 'element-ui/src/mixins/migrating';
import emitter from 'element-ui/src/mixins/emitter';
export default {
name: 'ElDialog',
mixins: [Popup, emitter, Migrating],
props: {
title: {
type: String,
default: ''
},
modal: {
type: Boolean,
default: true
},
modalAppendToBody: {
type: Boolean,
default: true
},
appendToBody: {
type: Boolean,
default: false
},
lockScroll: {
type: Boolean,
default: true
},
closeOnClickModal: {
type: Boolean,
default: true
},
closeOnPressEscape: {
type: Boolean,
default: true
},
showClose: {
type: Boolean,
default: true
},
width: String,
fullscreen: Boolean,
customClass: {
type: String,
default: ''
},
top: {
type: String,
default: '15vh'
},
beforeClose: Function,
center: {
type: Boolean,
default: false
}
},
data() {
return {
closed: false
};
},
watch: {
visible(val) {
if (val) {
this.closed = false;
this.$emit('open');
this.$el.addEventListener('scroll', this.updatePopper);
this.$nextTick(() => {
this.$refs.dialog.scrollTop = 0;
});
if (this.appendToBody) {
document.body.appendChild(this.$el);
}
} else {
this.$el.removeEventListener('scroll', this.updatePopper);
if (!this.closed) this.$emit('close');
}
}
},
computed: {
style() {
let style = {};
if (!this.fullscreen) {
style.marginTop = this.top;
if (this.width) {
style.width = this.width;
}
}
return style;
}
},
methods: {
getMigratingConfig() {
return {
props: {
'size': 'size is removed.'
}
};
},
handleWrapperClick() {
if (!this.closeOnClickModal) return;
this.handleClose();
},
handleClose() {
if (typeof this.beforeClose === 'function') {
this.beforeClose(this.hide);
} else {
this.hide();
}
},
hide(cancel) {
if (cancel !== false) {
this.$emit('update:visible', false);
this.$emit('close');
this.closed = true;
}
},
updatePopper() {
this.broadcast('ElSelectDropdown', 'updatePopper');
this.broadcast('ElDropdownMenu', 'updatePopper');
},
afterEnter() {
this.$emit('opened');
},
afterLeave() {
this.$emit('closed');
}
},
mounted() {
if (this.visible) {
this.rendered = true;
this.open();
if (this.appendToBody) {
document.body.appendChild(this.$el);
}
}
},
destroyed() {
// if appendToBody is true, remove DOM node after destroy
if (this.appendToBody && this.$el && this.$el.parentNode) {
this.$el.parentNode.removeChild(this.$el);
}
}
};
</script>
+8
View File
@@ -0,0 +1,8 @@
import ElDropdownItem from '../dropdown/src/dropdown-item';
/* istanbul ignore next */
ElDropdownItem.install = function(Vue) {
Vue.component(ElDropdownItem.name, ElDropdownItem);
};
export default ElDropdownItem;
+8
View File
@@ -0,0 +1,8 @@
import ElDropdownMenu from '../dropdown/src/dropdown-menu';
/* istanbul ignore next */
ElDropdownMenu.install = function(Vue) {
Vue.component(ElDropdownMenu.name, ElDropdownMenu);
};
export default ElDropdownMenu;
+8
View File
@@ -0,0 +1,8 @@
import ElDropdown from './src/dropdown';
/* istanbul ignore next */
ElDropdown.install = function(Vue) {
Vue.component(ElDropdown.name, ElDropdown);
};
export default ElDropdown;
+37
View File
@@ -0,0 +1,37 @@
<template>
<li
class="el-dropdown-menu__item"
:class="{
'is-disabled': disabled,
'el-dropdown-menu__item--divided': divided
}"
@click="handleClick"
:aria-disabled="disabled"
:tabindex="disabled ? null : -1"
>
<i :class="icon" v-if="icon"></i>
<slot></slot>
</li>
</template>
<script>
import Emitter from 'element-ui/src/mixins/emitter';
export default {
name: 'ElDropdownItem',
mixins: [Emitter],
props: {
command: {},
disabled: Boolean,
divided: Boolean,
icon: String
},
methods: {
handleClick(e) {
this.dispatch('ElDropdown', 'menu-item-click', [this.command, this]);
}
}
};
</script>
+60
View File
@@ -0,0 +1,60 @@
<template>
<transition name="el-zoom-in-top" @after-leave="doDestroy">
<ul class="el-dropdown-menu el-popper" :class="[size && `el-dropdown-menu--${size}`]" v-show="showPopper">
<slot></slot>
</ul>
</transition>
</template>
<script>
import Popper from 'element-ui/src/utils/vue-popper';
export default {
name: 'ElDropdownMenu',
componentName: 'ElDropdownMenu',
mixins: [Popper],
props: {
visibleArrow: {
type: Boolean,
default: true
},
arrowOffset: {
type: Number,
default: 0
}
},
data() {
return {
size: this.dropdown.dropdownSize
};
},
inject: ['dropdown'],
created() {
this.$on('updatePopper', () => {
if (this.showPopper) this.updatePopper();
});
this.$on('visible', val => {
this.showPopper = val;
});
},
mounted() {
this.$parent.popperElm = this.popperElm = this.$el;
this.referenceElm = this.$parent.$el;
},
watch: {
'dropdown.placement': {
immediate: true,
handler(val) {
this.currentPlacement = val;
}
}
}
};
</script>
+271
View File
@@ -0,0 +1,271 @@
<script>
import Clickoutside from 'element-ui/src/utils/clickoutside';
import Emitter from 'element-ui/src/mixins/emitter';
import Migrating from 'element-ui/src/mixins/migrating';
import ElButton from 'element-ui/packages/button';
import ElButtonGroup from 'element-ui/packages/button-group';
import { generateId } from 'element-ui/src/utils/util';
export default {
name: 'ElDropdown',
componentName: 'ElDropdown',
mixins: [Emitter, Migrating],
directives: { Clickoutside },
components: {
ElButton,
ElButtonGroup
},
provide() {
return {
dropdown: this
};
},
props: {
trigger: {
type: String,
default: 'hover'
},
type: String,
size: {
type: String,
default: ''
},
splitButton: Boolean,
hideOnClick: {
type: Boolean,
default: true
},
placement: {
type: String,
default: 'bottom-end'
},
visibleArrow: {
default: true
},
showTimeout: {
type: Number,
default: 250
},
hideTimeout: {
type: Number,
default: 150
}
},
data() {
return {
timeout: null,
visible: false,
triggerElm: null,
menuItems: null,
menuItemsArray: null,
dropdownElm: null,
focusing: false,
listId: `dropdown-menu-${generateId()}`
};
},
computed: {
dropdownSize() {
return this.size || (this.$ELEMENT || {}).size;
}
},
mounted() {
this.$on('menu-item-click', this.handleMenuItemClick);
this.initEvent();
this.initAria();
},
watch: {
visible(val) {
this.broadcast('ElDropdownMenu', 'visible', val);
this.$emit('visible-change', val);
},
focusing(val) {
const selfDefine = this.$el.querySelector('.el-dropdown-selfdefine');
if (selfDefine) { // 自定义
if (val) {
selfDefine.className += ' focusing';
} else {
selfDefine.className = selfDefine.className.replace('focusing', '');
}
}
}
},
methods: {
getMigratingConfig() {
return {
props: {
'menu-align': 'menu-align is renamed to placement.'
}
};
},
show() {
if (this.triggerElm.disabled) return;
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.visible = true;
}, this.trigger === 'click' ? 0 : this.showTimeout);
},
hide() {
if (this.triggerElm.disabled) return;
this.removeTabindex();
this.resetTabindex(this.triggerElm);
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.visible = false;
}, this.trigger === 'click' ? 0 : this.hideTimeout);
},
handleClick() {
if (this.triggerElm.disabled) return;
if (this.visible) {
this.hide();
} else {
this.show();
}
},
handleTriggerKeyDown(ev) {
const keyCode = ev.keyCode;
if ([38, 40].indexOf(keyCode) > -1) { // up/down
this.removeTabindex();
this.resetTabindex(this.menuItems[0]);
this.menuItems[0].focus();
ev.preventDefault();
ev.stopPropagation();
} else if (keyCode === 13) { // space enter选中
this.handleClick();
} else if ([9, 27].indexOf(keyCode) > -1) { // tab || esc
this.hide();
}
return;
},
handleItemKeyDown(ev) {
const keyCode = ev.keyCode;
const target = ev.target;
const currentIndex = this.menuItemsArray.indexOf(target);
const max = this.menuItemsArray.length - 1;
let nextIndex;
if ([38, 40].indexOf(keyCode) > -1) { // up/down
if (keyCode === 38) { // up
nextIndex = currentIndex !== 0 ? currentIndex - 1 : 0;
} else { // down
nextIndex = currentIndex < max ? currentIndex + 1 : max;
}
this.removeTabindex();
this.resetTabindex(this.menuItems[nextIndex]);
this.menuItems[nextIndex].focus();
ev.preventDefault();
ev.stopPropagation();
} else if (keyCode === 13) { // enter选中
this.triggerElm.focus();
target.click();
if (this.hideOnClick) { // click关闭
this.visible = false;
}
} else if ([9, 27].indexOf(keyCode) > -1) { // tab // esc
this.hide();
this.triggerElm.focus();
}
return;
},
resetTabindex(ele) { // 下次tab时组件聚焦元素
this.removeTabindex();
ele.setAttribute('tabindex', '0'); // 下次期望的聚焦元素
},
removeTabindex() {
this.triggerElm.setAttribute('tabindex', '-1');
this.menuItemsArray.forEach((item) => {
item.setAttribute('tabindex', '-1');
});
},
initAria() {
this.dropdownElm.setAttribute('id', this.listId);
this.triggerElm.setAttribute('aria-haspopup', 'list');
this.triggerElm.setAttribute('aria-controls', this.listId);
this.menuItems = this.dropdownElm.querySelectorAll("[tabindex='-1']");
this.menuItemsArray = Array.prototype.slice.call(this.menuItems);
if (!this.splitButton) { // 自定义
this.triggerElm.setAttribute('role', 'button');
this.triggerElm.setAttribute('tabindex', '0');
this.triggerElm.setAttribute('class', (this.triggerElm.getAttribute('class') || '') + ' el-dropdown-selfdefine'); // 控制
}
},
initEvent() {
let { trigger, show, hide, handleClick, splitButton, handleTriggerKeyDown, handleItemKeyDown } = this;
this.triggerElm = splitButton
? this.$refs.trigger.$el
: this.$slots.default[0].elm;
let dropdownElm = this.dropdownElm = this.$slots.dropdown[0].elm;
this.triggerElm.addEventListener('keydown', handleTriggerKeyDown); // triggerElm keydown
dropdownElm.addEventListener('keydown', handleItemKeyDown, true); // item keydown
// 控制自定义元素的样式
if (!splitButton) {
this.triggerElm.addEventListener('focus', () => {
this.focusing = true;
});
this.triggerElm.addEventListener('blur', () => {
this.focusing = false;
});
this.triggerElm.addEventListener('click', () => {
this.focusing = false;
});
}
if (trigger === 'hover') {
this.triggerElm.addEventListener('mouseenter', show);
this.triggerElm.addEventListener('mouseleave', hide);
dropdownElm.addEventListener('mouseenter', show);
dropdownElm.addEventListener('mouseleave', hide);
} else if (trigger === 'click') {
this.triggerElm.addEventListener('click', handleClick);
}
},
handleMenuItemClick(command, instance) {
if (this.hideOnClick) {
this.visible = false;
}
this.$emit('command', command, instance);
},
focus() {
this.triggerElm.focus && this.triggerElm.focus();
}
},
render(h) {
let { hide, splitButton, type, dropdownSize } = this;
const handleMainButtonClick = (event) => {
this.$emit('click', event);
hide();
};
let triggerElm = !splitButton
? this.$slots.default
: (<el-button-group>
<el-button type={type} size={dropdownSize} nativeOn-click={handleMainButtonClick}>
{this.$slots.default}
</el-button>
<el-button ref="trigger" type={type} size={dropdownSize} class="el-dropdown__caret-button">
<i class="el-dropdown__icon el-icon-arrow-down"></i>
</el-button>
</el-button-group>);
return (
<div class="el-dropdown" v-clickoutside={hide}>
{triggerElm}
{this.$slots.dropdown}
</div>
);
}
};
</script>
+8
View File
@@ -0,0 +1,8 @@
import Footer from './src/main';
/* istanbul ignore next */
Footer.install = function(Vue) {
Vue.component(Footer.name, Footer);
};
export default Footer;
+20
View File
@@ -0,0 +1,20 @@
<template>
<footer class="el-footer" :style="{ height }">
<slot></slot>
</footer>
</template>
<script>
export default {
name: 'ElFooter',
componentName: 'ElFooter',
props: {
height: {
type: String,
default: '60px'
}
}
};
</script>
+8
View File
@@ -0,0 +1,8 @@
import ElFormItem from '../form/src/form-item';
/* istanbul ignore next */
ElFormItem.install = function(Vue) {
Vue.component(ElFormItem.name, ElFormItem);
};
export default ElFormItem;
+8
View File
@@ -0,0 +1,8 @@
import ElForm from './src/form';
/* istanbul ignore next */
ElForm.install = function(Vue) {
Vue.component(ElForm.name, ElForm);
};
export default ElForm;
+290
View File
@@ -0,0 +1,290 @@
<template>
<div class="el-form-item" :class="[{
'el-form-item--feedback': elForm && elForm.statusIcon,
'is-error': validateState === 'error',
'is-validating': validateState === 'validating',
'is-success': validateState === 'success',
'is-required': isRequired || required,
'is-no-asterisk': elForm && elForm.hideRequiredAsterisk
},
sizeClass ? 'el-form-item--' + sizeClass : ''
]">
<label :for="labelFor" class="el-form-item__label" :style="labelStyle" v-if="label || $slots.label">
<slot name="label">{{label + form.labelSuffix}}</slot>
</label>
<div class="el-form-item__content" :style="contentStyle">
<slot></slot>
<transition name="el-zoom-in-top">
<slot
v-if="validateState === 'error' && showMessage && form.showMessage"
name="error"
:error="validateMessage">
<div
class="el-form-item__error"
:class="{
'el-form-item__error--inline': typeof inlineMessage === 'boolean'
? inlineMessage
: (elForm && elForm.inlineMessage || false)
}"
>
{{validateMessage}}
</div>
</slot>
</transition>
</div>
</div>
</template>
<script>
import AsyncValidator from 'async-validator';
import emitter from 'element-ui/src/mixins/emitter';
import objectAssign from 'element-ui/src/utils/merge';
import { noop, getPropByPath } from 'element-ui/src/utils/util';
export default {
name: 'ElFormItem',
componentName: 'ElFormItem',
mixins: [emitter],
provide() {
return {
elFormItem: this
};
},
inject: ['elForm'],
props: {
label: String,
labelWidth: String,
prop: String,
required: {
type: Boolean,
default: undefined
},
rules: [Object, Array],
error: String,
validateStatus: String,
for: String,
inlineMessage: {
type: [String, Boolean],
default: ''
},
showMessage: {
type: Boolean,
default: true
},
size: String
},
watch: {
error: {
immediate: true,
handler(value) {
this.validateMessage = value;
this.validateState = value ? 'error' : '';
}
},
validateStatus(value) {
this.validateState = value;
}
},
computed: {
labelFor() {
return this.for || this.prop;
},
labelStyle() {
const ret = {};
if (this.form.labelPosition === 'top') return ret;
const labelWidth = this.labelWidth || this.form.labelWidth;
if (labelWidth) {
ret.width = labelWidth;
}
return ret;
},
contentStyle() {
const ret = {};
const label = this.label;
if (this.form.labelPosition === 'top' || this.form.inline) return ret;
if (!label && !this.labelWidth && this.isNested) return ret;
const labelWidth = this.labelWidth || this.form.labelWidth;
if (labelWidth) {
ret.marginLeft = labelWidth;
}
return ret;
},
form() {
let parent = this.$parent;
let parentName = parent.$options.componentName;
while (parentName !== 'ElForm') {
if (parentName === 'ElFormItem') {
this.isNested = true;
}
parent = parent.$parent;
parentName = parent.$options.componentName;
}
return parent;
},
fieldValue() {
const model = this.form.model;
if (!model || !this.prop) { return; }
let path = this.prop;
if (path.indexOf(':') !== -1) {
path = path.replace(/:/, '.');
}
return getPropByPath(model, path, true).v;
},
isRequired() {
let rules = this.getRules();
let isRequired = false;
if (rules && rules.length) {
rules.every(rule => {
if (rule.required) {
isRequired = true;
return false;
}
return true;
});
}
return isRequired;
},
_formSize() {
return this.elForm.size;
},
elFormItemSize() {
return this.size || this._formSize;
},
sizeClass() {
return this.elFormItemSize || (this.$ELEMENT || {}).size;
}
},
data() {
return {
validateState: '',
validateMessage: '',
validateDisabled: false,
validator: {},
isNested: false
};
},
methods: {
validate(trigger, callback = noop) {
this.validateDisabled = false;
const rules = this.getFilteredRule(trigger);
if ((!rules || rules.length === 0) && this.required === undefined) {
callback();
return true;
}
this.validateState = 'validating';
const descriptor = {};
if (rules && rules.length > 0) {
rules.forEach(rule => {
delete rule.trigger;
});
}
descriptor[this.prop] = rules;
const validator = new AsyncValidator(descriptor);
const model = {};
model[this.prop] = this.fieldValue;
validator.validate(model, { firstFields: true }, (errors, invalidFields) => {
this.validateState = !errors ? 'success' : 'error';
this.validateMessage = errors ? errors[0].message : '';
callback(this.validateMessage, invalidFields);
this.elForm && this.elForm.$emit('validate', this.prop, !errors, this.validateMessage || null);
});
},
clearValidate() {
this.validateState = '';
this.validateMessage = '';
this.validateDisabled = false;
},
resetField() {
this.validateState = '';
this.validateMessage = '';
let model = this.form.model;
let value = this.fieldValue;
let path = this.prop;
if (path.indexOf(':') !== -1) {
path = path.replace(/:/, '.');
}
let prop = getPropByPath(model, path, true);
this.validateDisabled = true;
if (Array.isArray(value)) {
prop.o[prop.k] = [].concat(this.initialValue);
} else {
prop.o[prop.k] = this.initialValue;
}
this.broadcast('ElTimeSelect', 'fieldReset', this.initialValue);
},
getRules() {
let formRules = this.form.rules;
const selfRules = this.rules;
const requiredRule = this.required !== undefined ? { required: !!this.required } : [];
const prop = getPropByPath(formRules, this.prop || '');
formRules = formRules ? (prop.o[this.prop || ''] || prop.v) : [];
return [].concat(selfRules || formRules || []).concat(requiredRule);
},
getFilteredRule(trigger) {
const rules = this.getRules();
return rules.filter(rule => {
if (!rule.trigger || trigger === '') return true;
if (Array.isArray(rule.trigger)) {
return rule.trigger.indexOf(trigger) > -1;
} else {
return rule.trigger === trigger;
}
}).map(rule => objectAssign({}, rule));
},
onFieldBlur() {
this.validate('blur');
},
onFieldChange() {
if (this.validateDisabled) {
this.validateDisabled = false;
return;
}
this.validate('change');
}
},
mounted() {
if (this.prop) {
this.dispatch('ElForm', 'el.form.addField', [this]);
let initialValue = this.fieldValue;
if (Array.isArray(initialValue)) {
initialValue = [].concat(initialValue);
}
Object.defineProperty(this, 'initialValue', {
value: initialValue
});
let rules = this.getRules();
if (rules.length || this.required !== undefined) {
this.$on('el.form.blur', this.onFieldBlur);
this.$on('el.form.change', this.onFieldChange);
}
}
},
beforeDestroy() {
this.dispatch('ElForm', 'el.form.removeField', [this]);
}
};
</script>
+148
View File
@@ -0,0 +1,148 @@
<template>
<form class="el-form" :class="[
labelPosition ? 'el-form--label-' + labelPosition : '',
{ 'el-form--inline': inline }
]">
<slot></slot>
</form>
</template>
<script>
import objectAssign from 'element-ui/src/utils/merge';
export default {
name: 'ElForm',
componentName: 'ElForm',
provide() {
return {
elForm: this
};
},
props: {
model: Object,
rules: Object,
labelPosition: String,
labelWidth: String,
labelSuffix: {
type: String,
default: ''
},
inline: Boolean,
inlineMessage: Boolean,
statusIcon: Boolean,
showMessage: {
type: Boolean,
default: true
},
size: String,
disabled: Boolean,
validateOnRuleChange: {
type: Boolean,
default: true
},
hideRequiredAsterisk: {
type: Boolean,
default: false
}
},
watch: {
rules() {
if (this.validateOnRuleChange) {
this.validate(() => {});
}
}
},
data() {
return {
fields: []
};
},
created() {
this.$on('el.form.addField', (field) => {
if (field) {
this.fields.push(field);
}
});
/* istanbul ignore next */
this.$on('el.form.removeField', (field) => {
if (field.prop) {
this.fields.splice(this.fields.indexOf(field), 1);
}
});
},
methods: {
resetFields() {
if (!this.model) {
console.warn('[Element Warn][Form]model is required for resetFields to work.');
return;
}
this.fields.forEach(field => {
field.resetField();
});
},
clearValidate(props = []) {
const fields = props.length
? (typeof props === 'string'
? this.fields.filter(field => props === field.prop)
: this.fields.filter(field => props.indexOf(field.prop) > -1)
) : this.fields;
fields.forEach(field => {
field.clearValidate();
});
},
validate(callback) {
if (!this.model) {
console.warn('[Element Warn][Form]model is required for validate to work!');
return;
}
let promise;
// if no callback, return promise
if (typeof callback !== 'function' && window.Promise) {
promise = new window.Promise((resolve, reject) => {
callback = function(valid) {
valid ? resolve(valid) : reject(valid);
};
});
}
let valid = true;
let count = 0;
// 如果需要验证的fields为空,调用验证时立刻返回callback
if (this.fields.length === 0 && callback) {
callback(true);
}
let invalidFields = {};
this.fields.forEach(field => {
field.validate('', (message, field) => {
if (message) {
valid = false;
}
invalidFields = objectAssign({}, invalidFields, field);
if (typeof callback === 'function' && ++count === this.fields.length) {
callback(valid, invalidFields);
}
});
});
if (promise) {
return promise;
}
},
validateField(props, cb) {
props = [].concat(props);
const fields = this.fields.filter(field => props.indexOf(field.prop) !== -1);
if (!fields.length) {
console.warn('[Element Warn]please pass correct props!');
return;
}
fields.forEach(field => {
field.validate('', cb);
});
}
}
};
</script>
+8
View File
@@ -0,0 +1,8 @@
import Header from './src/main';
/* istanbul ignore next */
Header.install = function(Vue) {
Vue.component(Header.name, Header);
};
export default Header;
+20
View File
@@ -0,0 +1,20 @@
<template>
<header class="el-header" :style="{ height }">
<slot></slot>
</header>
</template>
<script>
export default {
name: 'ElHeader',
componentName: 'ElHeader',
props: {
height: {
type: String,
default: '60px'
}
}
};
</script>
+8
View File
@@ -0,0 +1,8 @@
import ElIcon from './src/icon.vue';
/* istanbul ignore next */
ElIcon.install = function(Vue) {
Vue.component(ElIcon.name, ElIcon);
};
export default ElIcon;
+13
View File
@@ -0,0 +1,13 @@
<template>
<i :class="'el-icon-' + name"></i>
</template>
<script>
export default {
name: 'ElIcon',
props: {
name: String
}
};
</script>
+8
View File
@@ -0,0 +1,8 @@
import ElInputNumber from './src/input-number';
/* istanbul ignore next */
ElInputNumber.install = function(Vue) {
Vue.component(ElInputNumber.name, ElInputNumber);
};
export default ElInputNumber;
+262
View File
@@ -0,0 +1,262 @@
<template>
<div
@dragstart.prevent
:class="[
'el-input-number',
inputNumberSize ? 'el-input-number--' + inputNumberSize : '',
{ 'is-disabled': inputNumberDisabled },
{ 'is-without-controls': !controls },
{ 'is-controls-right': controlsAtRight }
]">
<span
class="el-input-number__decrease"
role="button"
v-if="controls"
v-repeat-click="decrease"
:class="{'is-disabled': minDisabled}"
@keydown.enter="decrease">
<i :class="`el-icon-${controlsAtRight ? 'arrow-down' : 'minus'}`"></i>
</span>
<span
class="el-input-number__increase"
role="button"
v-if="controls"
v-repeat-click="increase"
:class="{'is-disabled': maxDisabled}"
@keydown.enter="increase">
<i :class="`el-icon-${controlsAtRight ? 'arrow-up' : 'plus'}`"></i>
</span>
<el-input
ref="input"
:value="displayValue"
:placeholder="placeholder"
:disabled="inputNumberDisabled"
:size="inputNumberSize"
:max="max"
:min="min"
:name="name"
:label="label"
@keydown.up.native.prevent="increase"
@keydown.down.native.prevent="decrease"
@blur="handleBlur"
@focus="handleFocus"
@input="handleInput"
@change="handleInputChange">
</el-input>
</div>
</template>
<script>
import ElInput from 'element-ui/packages/input';
import Focus from 'element-ui/src/mixins/focus';
import RepeatClick from 'element-ui/src/directives/repeat-click';
export default {
name: 'ElInputNumber',
mixins: [Focus('input')],
inject: {
elForm: {
default: ''
},
elFormItem: {
default: ''
}
},
directives: {
repeatClick: RepeatClick
},
components: {
ElInput
},
props: {
step: {
type: Number,
default: 1
},
max: {
type: Number,
default: Infinity
},
min: {
type: Number,
default: -Infinity
},
value: {},
disabled: Boolean,
size: String,
controls: {
type: Boolean,
default: true
},
controlsPosition: {
type: String,
default: ''
},
name: String,
label: String,
placeholder: String,
precision: {
type: Number,
validator(val) {
return val >= 0 && val === parseInt(val, 10);
}
}
},
data() {
return {
currentValue: 0,
userInput: null
};
},
watch: {
value: {
immediate: true,
handler(value) {
let newVal = value === undefined ? value : Number(value);
if (newVal !== undefined) {
if (isNaN(newVal)) {
return;
}
if (this.precision !== undefined) {
newVal = this.toPrecision(newVal, this.precision);
}
}
if (newVal >= this.max) newVal = this.max;
if (newVal <= this.min) newVal = this.min;
this.currentValue = newVal;
this.userInput = null;
this.$emit('input', newVal);
}
}
},
computed: {
minDisabled() {
return this._decrease(this.value, this.step) < this.min;
},
maxDisabled() {
return this._increase(this.value, this.step) > this.max;
},
numPrecision() {
const { value, step, getPrecision, precision } = this;
const stepPrecision = getPrecision(step);
if (precision !== undefined) {
if (stepPrecision > precision) {
console.warn('[Element Warn][InputNumber]precision should not be less than the decimal places of step');
}
return precision;
} else {
return Math.max(getPrecision(value), stepPrecision);
}
},
controlsAtRight() {
return this.controls && this.controlsPosition === 'right';
},
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
inputNumberSize() {
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
},
inputNumberDisabled() {
return this.disabled || (this.elForm || {}).disabled;
},
displayValue() {
if (this.userInput !== null) {
return this.userInput;
}
const currentValue = this.currentValue;
if (typeof currentValue === 'number' && this.precision !== undefined) {
return currentValue.toFixed(this.precision);
} else {
return currentValue;
}
}
},
methods: {
toPrecision(num, precision) {
if (precision === undefined) precision = this.numPrecision;
return parseFloat(Number(num).toFixed(precision));
},
getPrecision(value) {
if (value === undefined) return 0;
const valueString = value.toString();
const dotPosition = valueString.indexOf('.');
let precision = 0;
if (dotPosition !== -1) {
precision = valueString.length - dotPosition - 1;
}
return precision;
},
_increase(val, step) {
if (typeof val !== 'number' && val !== undefined) return this.currentValue;
const precisionFactor = Math.pow(10, this.numPrecision);
// Solve the accuracy problem of JS decimal calculation by converting the value to integer.
return this.toPrecision((precisionFactor * val + precisionFactor * step) / precisionFactor);
},
_decrease(val, step) {
if (typeof val !== 'number' && val !== undefined) return this.currentValue;
const precisionFactor = Math.pow(10, this.numPrecision);
return this.toPrecision((precisionFactor * val - precisionFactor * step) / precisionFactor);
},
increase() {
if (this.inputNumberDisabled || this.maxDisabled) return;
const value = this.value || 0;
const newVal = this._increase(value, this.step);
this.setCurrentValue(newVal);
},
decrease() {
if (this.inputNumberDisabled || this.minDisabled) return;
const value = this.value || 0;
const newVal = this._decrease(value, this.step);
this.setCurrentValue(newVal);
},
handleBlur(event) {
this.$emit('blur', event);
},
handleFocus(event) {
this.$emit('focus', event);
},
setCurrentValue(newVal) {
const oldVal = this.currentValue;
if (typeof newVal === 'number' && this.precision !== undefined) {
newVal = this.toPrecision(newVal, this.precision);
}
if (newVal >= this.max) newVal = this.max;
if (newVal <= this.min) newVal = this.min;
if (oldVal === newVal) return;
this.userInput = null;
this.$emit('input', newVal);
this.$emit('change', newVal, oldVal);
this.currentValue = newVal;
},
handleInput(value) {
this.userInput = value;
},
handleInputChange(value) {
const newVal = value === '' ? undefined : Number(value);
if (!isNaN(newVal) || value === '') {
this.setCurrentValue(newVal);
}
this.userInput = null;
},
select() {
this.$refs.input.select();
}
},
mounted() {
let innerInput = this.$refs.input.$refs.input;
innerInput.setAttribute('role', 'spinbutton');
innerInput.setAttribute('aria-valuemax', this.max);
innerInput.setAttribute('aria-valuemin', this.min);
innerInput.setAttribute('aria-valuenow', this.currentValue);
innerInput.setAttribute('aria-disabled', this.inputNumberDisabled);
},
updated() {
if (!this.$refs || !this.$refs.input) return;
const innerInput = this.$refs.input.$refs.input;
innerInput.setAttribute('aria-valuenow', this.currentValue);
}
};
</script>
+8
View File
@@ -0,0 +1,8 @@
import ElInput from './src/input';
/* istanbul ignore next */
ElInput.install = function(Vue) {
Vue.component(ElInput.name, ElInput);
};
export default ElInput;
+104
View File
@@ -0,0 +1,104 @@
let hiddenTextarea;
const HIDDEN_STYLE = `
height:0 !important;
visibility:hidden !important;
overflow:hidden !important;
position:absolute !important;
z-index:-1000 !important;
top:0 !important;
right:0 !important
`;
const CONTEXT_STYLE = [
'letter-spacing',
'line-height',
'padding-top',
'padding-bottom',
'font-family',
'font-weight',
'font-size',
'text-rendering',
'text-transform',
'width',
'text-indent',
'padding-left',
'padding-right',
'border-width',
'box-sizing'
];
function calculateNodeStyling(targetElement) {
const style = window.getComputedStyle(targetElement);
const boxSizing = style.getPropertyValue('box-sizing');
const paddingSize = (
parseFloat(style.getPropertyValue('padding-bottom')) +
parseFloat(style.getPropertyValue('padding-top'))
);
const borderSize = (
parseFloat(style.getPropertyValue('border-bottom-width')) +
parseFloat(style.getPropertyValue('border-top-width'))
);
const contextStyle = CONTEXT_STYLE
.map(name => `${name}:${style.getPropertyValue(name)}`)
.join(';');
return { contextStyle, paddingSize, borderSize, boxSizing };
}
export default function calcTextareaHeight(
targetElement,
minRows = 1,
maxRows = null
) {
if (!hiddenTextarea) {
hiddenTextarea = document.createElement('textarea');
document.body.appendChild(hiddenTextarea);
}
let {
paddingSize,
borderSize,
boxSizing,
contextStyle
} = calculateNodeStyling(targetElement);
hiddenTextarea.setAttribute('style', `${contextStyle};${HIDDEN_STYLE}`);
hiddenTextarea.value = targetElement.value || targetElement.placeholder || '';
let height = hiddenTextarea.scrollHeight;
const result = {};
if (boxSizing === 'border-box') {
height = height + borderSize;
} else if (boxSizing === 'content-box') {
height = height - paddingSize;
}
hiddenTextarea.value = '';
let singleRowHeight = hiddenTextarea.scrollHeight - paddingSize;
if (minRows !== null) {
let minHeight = singleRowHeight * minRows;
if (boxSizing === 'border-box') {
minHeight = minHeight + paddingSize + borderSize;
}
height = Math.max(minHeight, height);
result.minHeight = `${ minHeight }px`;
}
if (maxRows !== null) {
let maxHeight = singleRowHeight * maxRows;
if (boxSizing === 'border-box') {
maxHeight = maxHeight + paddingSize + borderSize;
}
height = Math.min(maxHeight, height);
}
result.height = `${ height }px`;
hiddenTextarea.parentNode && hiddenTextarea.parentNode.removeChild(hiddenTextarea);
hiddenTextarea = null;
return result;
};
+366
View File
@@ -0,0 +1,366 @@
<template>
<div :class="[
type === 'textarea' ? 'el-textarea' : 'el-input',
inputSize ? 'el-input--' + inputSize : '',
{
'is-disabled': inputDisabled,
'el-input-group': $slots.prepend || $slots.append,
'el-input-group--append': $slots.append,
'el-input-group--prepend': $slots.prepend,
'el-input--prefix': $slots.prefix || prefixIcon,
'el-input--suffix': $slots.suffix || suffixIcon || clearable || showPassword
}
]"
@mouseenter="hovering = true"
@mouseleave="hovering = false"
>
<template v-if="type !== 'textarea'">
<!-- 前置元素 -->
<div class="el-input-group__prepend" v-if="$slots.prepend">
<slot name="prepend"></slot>
</div>
<input
:tabindex="tabindex"
v-if="type !== 'textarea'"
class="el-input__inner"
v-bind="$attrs"
:type="showPassword ? (passwordVisible ? 'text': 'password') : type"
:disabled="inputDisabled"
:readonly="readonly"
:autocomplete="autoComplete || autocomplete"
:value="nativeInputValue"
ref="input"
@compositionstart="handleComposition"
@compositionupdate="handleComposition"
@compositionend="handleComposition"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@change="handleChange"
:aria-label="label"
>
<!-- 前置内容 -->
<span class="el-input__prefix" v-if="$slots.prefix || prefixIcon">
<slot name="prefix"></slot>
<i class="el-input__icon"
v-if="prefixIcon"
:class="prefixIcon">
</i>
</span>
<!-- 后置内容 -->
<span
class="el-input__suffix"
v-if="$slots.suffix || suffixIcon || showClear || showPassword || validateState && needStatusIcon">
<span class="el-input__suffix-inner">
<template v-if="!showClear || !showPwdVisible">
<slot name="suffix"></slot>
<i class="el-input__icon"
v-if="suffixIcon"
:class="suffixIcon">
</i>
</template>
<i v-if="showClear"
class="el-input__icon el-icon-circle-close el-input__clear"
@click="clear"
></i>
<i v-if="showPwdVisible"
class="el-input__icon el-icon-view el-input__clear"
@click="handlePasswordVisible"
></i>
</span>
<i class="el-input__icon"
v-if="validateState"
:class="['el-input__validateIcon', validateIcon]">
</i>
</span>
<!-- 后置元素 -->
<div class="el-input-group__append" v-if="$slots.append">
<slot name="append"></slot>
</div>
</template>
<textarea
v-else
:tabindex="tabindex"
class="el-textarea__inner"
:value="nativeInputValue"
@compositionstart="handleComposition"
@compositionupdate="handleComposition"
@compositionend="handleComposition"
@input="handleInput"
ref="textarea"
v-bind="$attrs"
:disabled="inputDisabled"
:readonly="readonly"
:autocomplete="autoComplete || autocomplete"
:style="textareaStyle"
@focus="handleFocus"
@blur="handleBlur"
@change="handleChange"
:aria-label="label"
>
</textarea>
</div>
</template>
<script>
import emitter from 'element-ui/src/mixins/emitter';
import Migrating from 'element-ui/src/mixins/migrating';
import calcTextareaHeight from './calcTextareaHeight';
import merge from 'element-ui/src/utils/merge';
export default {
name: 'ElInput',
componentName: 'ElInput',
mixins: [emitter, Migrating],
inheritAttrs: false,
inject: {
elForm: {
default: ''
},
elFormItem: {
default: ''
}
},
data() {
return {
textareaCalcStyle: {},
hovering: false,
focused: false,
isOnComposition: false,
passwordVisible: false
};
},
props: {
value: [String, Number],
size: String,
resize: String,
form: String,
disabled: Boolean,
readonly: Boolean,
type: {
type: String,
default: 'text'
},
autosize: {
type: [Boolean, Object],
default: false
},
autocomplete: {
type: String,
default: 'off'
},
/** @Deprecated in next major version */
autoComplete: {
type: String,
validator(val) {
process.env.NODE_ENV !== 'production' &&
console.warn('[Element Warn][Input]\'auto-complete\' property will be deprecated in next major version. please use \'autocomplete\' instead.');
return true;
}
},
validateEvent: {
type: Boolean,
default: true
},
suffixIcon: String,
prefixIcon: String,
label: String,
clearable: {
type: Boolean,
default: false
},
showPassword: {
type: Boolean,
default: false
},
tabindex: String
},
computed: {
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
validateState() {
return this.elFormItem ? this.elFormItem.validateState : '';
},
needStatusIcon() {
return this.elForm ? this.elForm.statusIcon : false;
},
validateIcon() {
return {
validating: 'el-icon-loading',
success: 'el-icon-circle-check',
error: 'el-icon-circle-close'
}[this.validateState];
},
textareaStyle() {
return merge({}, this.textareaCalcStyle, { resize: this.resize });
},
inputSize() {
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
},
inputDisabled() {
return this.disabled || (this.elForm || {}).disabled;
},
nativeInputValue() {
return this.value === null || this.value === undefined ? '' : this.value;
},
showClear() {
return this.clearable &&
!this.inputDisabled &&
!this.readonly &&
this.nativeInputValue &&
(this.focused || this.hovering);
},
showPwdVisible() {
return this.showPassword &&
!this.inputDisabled &&
!this.readonly &&
(!!this.nativeInputValue || this.focused);
}
},
watch: {
value(val) {
this.$nextTick(this.resizeTextarea);
if (this.validateEvent) {
this.dispatch('ElFormItem', 'el.form.change', [val]);
}
}
},
methods: {
focus() {
this.getInput().focus();
},
blur() {
this.getInput().blur();
},
getMigratingConfig() {
return {
props: {
'icon': 'icon is removed, use suffix-icon / prefix-icon instead.',
'on-icon-click': 'on-icon-click is removed.'
},
events: {
'click': 'click is removed.'
}
};
},
handleBlur(event) {
this.focused = false;
this.$emit('blur', event);
if (this.validateEvent) {
this.dispatch('ElFormItem', 'el.form.blur', [this.value]);
}
},
select() {
this.getInput().select();
},
resizeTextarea() {
if (this.$isServer) return;
const { autosize, type } = this;
if (type !== 'textarea') return;
if (!autosize) {
this.textareaCalcStyle = {
minHeight: calcTextareaHeight(this.$refs.textarea).minHeight
};
return;
}
const minRows = autosize.minRows;
const maxRows = autosize.maxRows;
this.textareaCalcStyle = calcTextareaHeight(this.$refs.textarea, minRows, maxRows);
},
handleFocus(event) {
this.focused = true;
this.$emit('focus', event);
},
handleComposition(event) {
if (event.type === 'compositionstart') {
this.isOnComposition = true;
}
if (event.type === 'compositionend') {
this.isOnComposition = false;
this.handleInput(event);
}
},
handleInput(event) {
if (this.isOnComposition) return;
// hack for https://github.com/ElemeFE/element/issues/8548
// should remove the following line when we don't support IE
if (event.target.value === this.nativeInputValue) return;
this.$emit('input', event.target.value);
// set input's value, in case parent refuses the change
// see: https://github.com/ElemeFE/element/issues/12850
this.$nextTick(() => {
let input = this.getInput();
input.value = this.value;
});
},
handleChange(event) {
this.$emit('change', event.target.value);
},
calcIconOffset(place) {
let elList = [].slice.call(this.$el.querySelectorAll(`.el-input__${place}`) || []);
if (!elList.length) return;
let el = null;
for (let i = 0; i < elList.length; i++) {
if (elList[i].parentNode === this.$el) {
el = elList[i];
break;
}
}
if (!el) return;
const pendantMap = {
suffix: 'append',
prefix: 'prepend'
};
const pendant = pendantMap[place];
if (this.$slots[pendant]) {
el.style.transform = `translateX(${place === 'suffix' ? '-' : ''}${this.$el.querySelector(`.el-input-group__${pendant}`).offsetWidth}px)`;
} else {
el.removeAttribute('style');
}
},
updateIconOffset() {
this.calcIconOffset('prefix');
this.calcIconOffset('suffix');
},
clear() {
this.$emit('input', '');
this.$emit('change', '');
this.$emit('clear');
},
handlePasswordVisible() {
this.passwordVisible = !this.passwordVisible;
this.focus();
},
getInput() {
return this.$refs.input || this.$refs.textarea;
}
},
created() {
this.$on('inputSelect', this.select);
},
mounted() {
this.resizeTextarea();
this.updateIconOffset();
},
updated() {
this.$nextTick(this.updateIconOffset);
}
};
</script>
+11
View File
@@ -0,0 +1,11 @@
import directive from './src/directive';
import service from './src/index';
export default {
install(Vue) {
Vue.use(directive);
Vue.prototype.$loading = service;
},
directive,
service
};
+129
View File
@@ -0,0 +1,129 @@
import Vue from 'vue';
import Loading from './loading.vue';
import { addClass, removeClass, getStyle } from 'element-ui/src/utils/dom';
import { PopupManager } from 'element-ui/src/utils/popup';
import afterLeave from 'element-ui/src/utils/after-leave';
const Mask = Vue.extend(Loading);
const loadingDirective = {};
loadingDirective.install = Vue => {
if (Vue.prototype.$isServer) return;
const toggleLoading = (el, binding) => {
if (binding.value) {
Vue.nextTick(() => {
if (binding.modifiers.fullscreen) {
el.originalPosition = getStyle(document.body, 'position');
el.originalOverflow = getStyle(document.body, 'overflow');
el.maskStyle.zIndex = PopupManager.nextZIndex();
addClass(el.mask, 'is-fullscreen');
insertDom(document.body, el, binding);
} else {
removeClass(el.mask, 'is-fullscreen');
if (binding.modifiers.body) {
el.originalPosition = getStyle(document.body, 'position');
['top', 'left'].forEach(property => {
const scroll = property === 'top' ? 'scrollTop' : 'scrollLeft';
el.maskStyle[property] = el.getBoundingClientRect()[property] +
document.body[scroll] +
document.documentElement[scroll] -
parseInt(getStyle(document.body, `margin-${ property }`), 10) +
'px';
});
['height', 'width'].forEach(property => {
el.maskStyle[property] = el.getBoundingClientRect()[property] + 'px';
});
insertDom(document.body, el, binding);
} else {
el.originalPosition = getStyle(el, 'position');
insertDom(el, el, binding);
}
}
});
} else {
afterLeave(el.instance, _ => {
el.domVisible = false;
const target = binding.modifiers.fullscreen || binding.modifiers.body
? document.body
: el;
removeClass(target, 'el-loading-parent--relative');
removeClass(target, 'el-loading-parent--hidden');
el.instance.hiding = false;
}, 300, true);
el.instance.visible = false;
el.instance.hiding = true;
}
};
const insertDom = (parent, el, binding) => {
if (!el.domVisible && getStyle(el, 'display') !== 'none' && getStyle(el, 'visibility') !== 'hidden') {
Object.keys(el.maskStyle).forEach(property => {
el.mask.style[property] = el.maskStyle[property];
});
if (el.originalPosition !== 'absolute' && el.originalPosition !== 'fixed') {
addClass(parent, 'el-loading-parent--relative');
}
if (binding.modifiers.fullscreen && binding.modifiers.lock) {
addClass(parent, 'el-loading-parent--hidden');
}
el.domVisible = true;
parent.appendChild(el.mask);
Vue.nextTick(() => {
if (el.instance.hiding) {
el.instance.$emit('after-leave');
} else {
el.instance.visible = true;
}
});
el.domInserted = true;
}
};
Vue.directive('loading', {
bind: function(el, binding, vnode) {
const textExr = el.getAttribute('element-loading-text');
const spinnerExr = el.getAttribute('element-loading-spinner');
const backgroundExr = el.getAttribute('element-loading-background');
const customClassExr = el.getAttribute('element-loading-custom-class');
const vm = vnode.context;
const mask = new Mask({
el: document.createElement('div'),
data: {
text: vm && vm[textExr] || textExr,
spinner: vm && vm[spinnerExr] || spinnerExr,
background: vm && vm[backgroundExr] || backgroundExr,
customClass: vm && vm[customClassExr] || customClassExr,
fullscreen: !!binding.modifiers.fullscreen
}
});
el.instance = mask;
el.mask = mask.$el;
el.maskStyle = {};
binding.value && toggleLoading(el, binding);
},
update: function(el, binding) {
el.instance.setText(el.getAttribute('element-loading-text'));
if (binding.oldValue !== binding.value) {
toggleLoading(el, binding);
}
},
unbind: function(el, binding) {
if (el.domInserted) {
el.mask &&
el.mask.parentNode &&
el.mask.parentNode.removeChild(el.mask);
toggleLoading(el, { value: false, modifiers: binding.modifiers });
}
el.instance && el.instance.$destroy();
}
});
};
export default loadingDirective;
+106
View File
@@ -0,0 +1,106 @@
import Vue from 'vue';
import loadingVue from './loading.vue';
import { addClass, removeClass, getStyle } from 'element-ui/src/utils/dom';
import { PopupManager } from 'element-ui/src/utils/popup';
import afterLeave from 'element-ui/src/utils/after-leave';
import merge from 'element-ui/src/utils/merge';
const LoadingConstructor = Vue.extend(loadingVue);
const defaults = {
text: null,
fullscreen: true,
body: false,
lock: false,
customClass: ''
};
let fullscreenLoading;
LoadingConstructor.prototype.originalPosition = '';
LoadingConstructor.prototype.originalOverflow = '';
LoadingConstructor.prototype.close = function() {
if (this.fullscreen) {
fullscreenLoading = undefined;
}
afterLeave(this, _ => {
const target = this.fullscreen || this.body
? document.body
: this.target;
removeClass(target, 'el-loading-parent--relative');
removeClass(target, 'el-loading-parent--hidden');
if (this.$el && this.$el.parentNode) {
this.$el.parentNode.removeChild(this.$el);
}
this.$destroy();
}, 300);
this.visible = false;
};
const addStyle = (options, parent, instance) => {
let maskStyle = {};
if (options.fullscreen) {
instance.originalPosition = getStyle(document.body, 'position');
instance.originalOverflow = getStyle(document.body, 'overflow');
maskStyle.zIndex = PopupManager.nextZIndex();
} else if (options.body) {
instance.originalPosition = getStyle(document.body, 'position');
['top', 'left'].forEach(property => {
let scroll = property === 'top' ? 'scrollTop' : 'scrollLeft';
maskStyle[property] = options.target.getBoundingClientRect()[property] +
document.body[scroll] +
document.documentElement[scroll] +
'px';
});
['height', 'width'].forEach(property => {
maskStyle[property] = options.target.getBoundingClientRect()[property] + 'px';
});
} else {
instance.originalPosition = getStyle(parent, 'position');
}
Object.keys(maskStyle).forEach(property => {
instance.$el.style[property] = maskStyle[property];
});
};
const Loading = (options = {}) => {
if (Vue.prototype.$isServer) return;
options = merge({}, defaults, options);
if (typeof options.target === 'string') {
options.target = document.querySelector(options.target);
}
options.target = options.target || document.body;
if (options.target !== document.body) {
options.fullscreen = false;
} else {
options.body = true;
}
if (options.fullscreen && fullscreenLoading) {
return fullscreenLoading;
}
let parent = options.body ? document.body : options.target;
let instance = new LoadingConstructor({
el: document.createElement('div'),
data: options
});
addStyle(options, parent, instance);
if (instance.originalPosition !== 'absolute' && instance.originalPosition !== 'fixed') {
addClass(parent, 'el-loading-parent--relative');
}
if (options.fullscreen && options.lock) {
addClass(parent, 'el-loading-parent--hidden');
}
parent.appendChild(instance.$el);
Vue.nextTick(() => {
instance.visible = true;
});
if (options.fullscreen) {
fullscreenLoading = instance;
}
return instance;
};
export default Loading;
+41
View File
@@ -0,0 +1,41 @@
<template>
<transition name="el-loading-fade" @after-leave="handleAfterLeave">
<div
v-show="visible"
class="el-loading-mask"
:style="{ backgroundColor: background || '' }"
:class="[customClass, { 'is-fullscreen': fullscreen }]">
<div class="el-loading-spinner">
<svg v-if="!spinner" class="circular" viewBox="25 25 50 50">
<circle class="path" cx="50" cy="50" r="20" fill="none"/>
</svg>
<i v-else :class="spinner"></i>
<p v-if="text" class="el-loading-text">{{ text }}</p>
</div>
</div>
</transition>
</template>
<script>
export default {
data() {
return {
text: null,
spinner: null,
background: null,
fullscreen: true,
visible: false,
customClass: ''
};
},
methods: {
handleAfterLeave() {
this.$emit('after-leave');
},
setText(text) {
this.text = text;
}
}
};
</script>
+8
View File
@@ -0,0 +1,8 @@
import Main from './src/main';
/* istanbul ignore next */
Main.install = function(Vue) {
Vue.component(Main.name, Main);
};
export default Main;
+12
View File
@@ -0,0 +1,12 @@
<template>
<main class="el-main">
<slot></slot>
</main>
</template>
<script>
export default {
name: 'ElMain',
componentName: 'ElMain'
};
</script>
+8
View File
@@ -0,0 +1,8 @@
import ElMenuItemGroup from '../menu/src/menu-item-group';
/* istanbul ignore next */
ElMenuItemGroup.install = function(Vue) {
Vue.component(ElMenuItemGroup.name, ElMenuItemGroup);
};
export default ElMenuItemGroup;
+8
View File
@@ -0,0 +1,8 @@
import ElMenuItem from '../menu/src/menu-item';
/* istanbul ignore next */
ElMenuItem.install = function(Vue) {
Vue.component(ElMenuItem.name, ElMenuItem);
};
export default ElMenuItem;
+8
View File
@@ -0,0 +1,8 @@
import ElMenu from './src/menu';
/* istanbul ignore next */
ElMenu.install = function(Vue) {
Vue.component(ElMenu.name, ElMenu);
};
export default ElMenu;
+45
View File
@@ -0,0 +1,45 @@
<template>
<li class="el-menu-item-group">
<div class="el-menu-item-group__title" :style="{paddingLeft: levelPadding + 'px'}">
<template v-if="!$slots.title">{{title}}</template>
<slot v-else name="title"></slot>
</div>
<ul>
<slot></slot>
</ul>
</li>
</template>
<script>
export default {
name: 'ElMenuItemGroup',
componentName: 'ElMenuItemGroup',
inject: ['rootMenu'],
props: {
title: {
type: String
}
},
data() {
return {
paddingLeft: 20
};
},
computed: {
levelPadding() {
let padding = 20;
let parent = this.$parent;
if (this.rootMenu.collapse) return 20;
while (parent && parent.$options.componentName !== 'ElMenu') {
if (parent.$options.componentName === 'ElSubmenu') {
padding += 20;
}
parent = parent.$parent;
}
return padding;
}
}
};
</script>
+112
View File
@@ -0,0 +1,112 @@
<template>
<li class="el-menu-item"
role="menuitem"
tabindex="-1"
:style="[paddingStyle, itemStyle, { backgroundColor }]"
:class="{
'is-active': active,
'is-disabled': disabled
}"
@click="handleClick"
@mouseenter="onMouseEnter"
@focus="onMouseEnter"
@blur="onMouseLeave"
@mouseleave="onMouseLeave"
>
<el-tooltip
v-if="parentMenu.$options.componentName === 'ElMenu' && rootMenu.collapse && $slots.title"
effect="dark"
placement="right">
<div slot="content"><slot name="title"></slot></div>
<div style="position: absolute;left: 0;top: 0;height: 100%;width: 100%;display: inline-block;box-sizing: border-box;padding: 0 20px;">
<slot></slot>
</div>
</el-tooltip>
<template v-else>
<slot></slot>
<slot name="title"></slot>
</template>
</li>
</template>
<script>
import Menu from './menu-mixin';
import ElTooltip from 'element-ui/packages/tooltip';
import Emitter from 'element-ui/src/mixins/emitter';
export default {
name: 'ElMenuItem',
componentName: 'ElMenuItem',
mixins: [Menu, Emitter],
components: { ElTooltip },
props: {
index: {
default: null,
validator: val => typeof val === 'string' || val === null
},
route: [String, Object],
disabled: Boolean
},
computed: {
active() {
return this.index === this.rootMenu.activeIndex;
},
hoverBackground() {
return this.rootMenu.hoverBackground;
},
backgroundColor() {
return this.rootMenu.backgroundColor || '';
},
activeTextColor() {
return this.rootMenu.activeTextColor || '';
},
textColor() {
return this.rootMenu.textColor || '';
},
mode() {
return this.rootMenu.mode;
},
itemStyle() {
const style = {
color: this.active ? this.activeTextColor : this.textColor
};
if (this.mode === 'horizontal' && !this.isNested) {
style.borderBottomColor = this.active
? (this.rootMenu.activeTextColor ? this.activeTextColor : '')
: 'transparent';
}
return style;
},
isNested() {
return this.parentMenu !== this.rootMenu;
}
},
methods: {
onMouseEnter() {
if (this.mode === 'horizontal' && !this.rootMenu.backgroundColor) return;
this.$el.style.backgroundColor = this.hoverBackground;
},
onMouseLeave() {
if (this.mode === 'horizontal' && !this.rootMenu.backgroundColor) return;
this.$el.style.backgroundColor = this.backgroundColor;
},
handleClick() {
if (!this.disabled) {
this.dispatch('ElMenu', 'item-click', this);
this.$emit('click', this);
}
}
},
mounted() {
this.parentMenu.addItem(this);
this.rootMenu.addItem(this);
},
beforeDestroy() {
this.parentMenu.removeItem(this);
this.rootMenu.removeItem(this);
}
};
</script>
+44
View File
@@ -0,0 +1,44 @@
export default {
inject: ['rootMenu'],
computed: {
indexPath() {
const path = [this.index];
let parent = this.$parent;
while (parent.$options.componentName !== 'ElMenu') {
if (parent.index) {
path.unshift(parent.index);
}
parent = parent.$parent;
}
return path;
},
parentMenu() {
let parent = this.$parent;
while (
parent &&
['ElMenu', 'ElSubmenu'].indexOf(parent.$options.componentName) === -1
) {
parent = parent.$parent;
}
return parent;
},
paddingStyle() {
if (this.rootMenu.mode !== 'vertical') return {};
let padding = 20;
let parent = this.$parent;
if (this.rootMenu.collapse) {
padding = 20;
} else {
while (parent && parent.$options.componentName !== 'ElMenu') {
if (parent.$options.componentName === 'ElSubmenu') {
padding += 20;
}
parent = parent.$parent;
}
}
return {paddingLeft: padding + 'px'};
}
}
};
+315
View File
@@ -0,0 +1,315 @@
<script type="text/jsx">
import emitter from 'element-ui/src/mixins/emitter';
import Migrating from 'element-ui/src/mixins/migrating';
import Menubar from 'element-ui/src/utils/menu/aria-menubar';
import { addClass, removeClass, hasClass } from 'element-ui/src/utils/dom';
export default {
name: 'ElMenu',
render (h) {
const component = (
<ul
role="menubar"
key={ +this.collapse }
style={{ backgroundColor: this.backgroundColor || '' }}
class={{
'el-menu--horizontal': this.mode === 'horizontal',
'el-menu--collapse': this.collapse,
"el-menu": true
}}
>
{ this.$slots.default }
</ul>
);
if (this.collapseTransition) {
return (
<el-menu-collapse-transition>
{ component }
</el-menu-collapse-transition>
);
} else {
return component;
}
},
componentName: 'ElMenu',
mixins: [emitter, Migrating],
provide() {
return {
rootMenu: this
};
},
components: {
'el-menu-collapse-transition': {
functional: true,
render(createElement, context) {
const data = {
props: {
mode: 'out-in'
},
on: {
beforeEnter(el) {
el.style.opacity = 0.2;
},
enter(el) {
addClass(el, 'el-opacity-transition');
el.style.opacity = 1;
},
afterEnter(el) {
removeClass(el, 'el-opacity-transition');
el.style.opacity = '';
},
beforeLeave(el) {
if (!el.dataset) el.dataset = {};
if (hasClass(el, 'el-menu--collapse')) {
removeClass(el, 'el-menu--collapse');
el.dataset.oldOverflow = el.style.overflow;
el.dataset.scrollWidth = el.clientWidth;
addClass(el, 'el-menu--collapse');
} else {
addClass(el, 'el-menu--collapse');
el.dataset.oldOverflow = el.style.overflow;
el.dataset.scrollWidth = el.clientWidth;
removeClass(el, 'el-menu--collapse');
}
el.style.width = el.scrollWidth + 'px';
el.style.overflow = 'hidden';
},
leave(el) {
addClass(el, 'horizontal-collapse-transition');
el.style.width = el.dataset.scrollWidth + 'px';
}
}
};
return createElement('transition', data, context.children);
}
}
},
props: {
mode: {
type: String,
default: 'vertical'
},
defaultActive: {
type: String,
default: ''
},
defaultOpeneds: Array,
uniqueOpened: Boolean,
router: Boolean,
menuTrigger: {
type: String,
default: 'hover'
},
collapse: Boolean,
backgroundColor: String,
textColor: String,
activeTextColor: String,
collapseTransition: {
type: Boolean,
default: true
}
},
data() {
return {
activeIndex: this.defaultActive,
openedMenus: (this.defaultOpeneds && !this.collapse) ? this.defaultOpeneds.slice(0) : [],
items: {},
submenus: {}
};
},
computed: {
hoverBackground() {
return this.backgroundColor ? this.mixColor(this.backgroundColor, 0.2) : '';
},
isMenuPopup() {
return this.mode === 'horizontal' || (this.mode === 'vertical' && this.collapse);
}
},
watch: {
defaultActive: 'updateActiveIndex',
defaultOpeneds(value) {
if (!this.collapse) {
this.openedMenus = value;
}
},
collapse(value) {
if (value) this.openedMenus = [];
this.broadcast('ElSubmenu', 'toggle-collapse', value);
}
},
methods: {
updateActiveIndex(val) {
const item = this.items[val] || this.items[this.activeIndex] || this.items[this.defaultActive];
if (item) {
this.activeIndex = item.index;
this.initOpenedMenu();
} else {
this.activeIndex = null;
}
},
getMigratingConfig() {
return {
props: {
'theme': 'theme is removed.'
}
};
},
getColorChannels(color) {
color = color.replace('#', '');
if (/^[0-9a-fA-F]{3}$/.test(color)) {
color = color.split('');
for (let i = 2; i >= 0; i--) {
color.splice(i, 0, color[i]);
}
color = color.join('');
}
if (/^[0-9a-fA-F]{6}$/.test(color)) {
return {
red: parseInt(color.slice(0, 2), 16),
green: parseInt(color.slice(2, 4), 16),
blue: parseInt(color.slice(4, 6), 16)
};
} else {
return {
red: 255,
green: 255,
blue: 255
};
}
},
mixColor(color, percent) {
let { red, green, blue } = this.getColorChannels(color);
if (percent > 0) { // shade given color
red *= 1 - percent;
green *= 1 - percent;
blue *= 1 - percent;
} else { // tint given color
red += (255 - red) * percent;
green += (255 - green) * percent;
blue += (255 - blue) * percent;
}
return `rgb(${ Math.round(red) }, ${ Math.round(green) }, ${ Math.round(blue) })`;
},
addItem(item) {
this.$set(this.items, item.index, item);
},
removeItem(item) {
delete this.items[item.index];
},
addSubmenu(item) {
this.$set(this.submenus, item.index, item);
},
removeSubmenu(item) {
delete this.submenus[item.index];
},
openMenu(index, indexPath) {
let openedMenus = this.openedMenus;
if (openedMenus.indexOf(index) !== -1) return;
// 将不在该菜单路径下的其余菜单收起
// collapse all menu that are not under current menu item
if (this.uniqueOpened) {
this.openedMenus = openedMenus.filter(index => {
return indexPath.indexOf(index) !== -1;
});
}
this.openedMenus.push(index);
},
closeMenu(index) {
const i = this.openedMenus.indexOf(index);
if (i !== -1) {
this.openedMenus.splice(i, 1);
}
},
handleSubmenuClick(submenu) {
const { index, indexPath } = submenu;
let isOpened = this.openedMenus.indexOf(index) !== -1;
if (isOpened) {
this.closeMenu(index);
this.$emit('close', index, indexPath);
} else {
this.openMenu(index, indexPath);
this.$emit('open', index, indexPath);
}
},
handleItemClick(item) {
const { index, indexPath } = item;
const oldActiveIndex = this.activeIndex;
const hasIndex = item.index !== null;
if (hasIndex) {
this.activeIndex = item.index;
}
this.$emit('select', index, indexPath, item);
if (this.mode === 'horizontal' || this.collapse) {
this.openedMenus = [];
}
if (this.router && hasIndex) {
this.routeToItem(item, (error) => {
this.activeIndex = oldActiveIndex;
if (error) console.error(error);
});
}
},
// 初始化展开菜单
// initialize opened menu
initOpenedMenu() {
const index = this.activeIndex;
const activeItem = this.items[index];
if (!activeItem || this.mode === 'horizontal' || this.collapse) return;
let indexPath = activeItem.indexPath;
// 展开该菜单项的路径上所有子菜单
// expand all submenus of the menu item
indexPath.forEach(index => {
let submenu = this.submenus[index];
submenu && this.openMenu(index, submenu.indexPath);
});
},
routeToItem(item, onError) {
let route = item.route || item.index;
try {
this.$router.push(route, () => {}, onError);
} catch (e) {
console.error(e);
}
},
open(index) {
const { indexPath } = this.submenus[index.toString()];
indexPath.forEach(i => this.openMenu(i, indexPath));
},
close(index) {
this.closeMenu(index);
}
},
mounted() {
this.initOpenedMenu();
this.$on('item-click', this.handleItemClick);
this.$on('submenu-click', this.handleSubmenuClick);
if (this.mode === 'horizontal') {
new Menubar(this.$el); // eslint-disable-line
}
this.$watch('items', this.updateActiveIndex);
}
};
</script>

Some files were not shown because too many files have changed in this diff Show More