first
This commit is contained in:
8
packages/alert/index.js
Normal file
8
packages/alert/index.js
Normal 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;
|
94
packages/alert/src/main.vue
Normal file
94
packages/alert/src/main.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<transition name="el-alert-fade">
|
||||
<div
|
||||
class="el-alert"
|
||||
:class="[typeClass, center ? 'is-center' : '', 'is-' + effect]"
|
||||
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,
|
||||
effect: {
|
||||
type: String,
|
||||
default: 'light',
|
||||
validator: function(value) {
|
||||
return ['light', 'dark'].indexOf(value) !== -1;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
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
packages/aside/index.js
Normal file
8
packages/aside/index.js
Normal 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
packages/aside/src/main.vue
Normal file
20
packages/aside/src/main.vue
Normal 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
packages/autocomplete/index.js
Normal file
8
packages/autocomplete/index.js
Normal 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;
|
76
packages/autocomplete/src/autocomplete-suggestions.vue
Normal file
76
packages/autocomplete/src/autocomplete-suggestions.vue
Normal file
@@ -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.$parent.$refs.input.$refs.textarea;
|
||||
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>
|
285
packages/autocomplete/src/autocomplete.vue
Normal file
285
packages/autocomplete/src/autocomplete.vue
Normal file
@@ -0,0 +1,285 @@
|
||||
<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="handleInput"
|
||||
@change="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');
|
||||
}
|
||||
});
|
||||
},
|
||||
handleInput(value) {
|
||||
this.$emit('input', value);
|
||||
this.suggestionDisabled = false;
|
||||
if (!this.triggerOnFocus && !value) {
|
||||
this.suggestionDisabled = true;
|
||||
this.suggestions = [];
|
||||
return;
|
||||
}
|
||||
this.debouncedGetData(value);
|
||||
},
|
||||
handleChange(value) {
|
||||
this.$emit('change', 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
packages/avatar/index.js
Normal file
8
packages/avatar/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import Avatar from './src/main';
|
||||
|
||||
/* istanbul ignore next */
|
||||
Avatar.install = function(Vue) {
|
||||
Vue.component(Avatar.name, Avatar);
|
||||
};
|
||||
|
||||
export default Avatar;
|
107
packages/avatar/src/main.vue
Normal file
107
packages/avatar/src/main.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'ElAvatar',
|
||||
|
||||
props: {
|
||||
size: {
|
||||
type: [Number, String],
|
||||
validator(val) {
|
||||
if (typeof val === 'string') {
|
||||
return ['large', 'medium', 'small'].includes(val);
|
||||
}
|
||||
return typeof val === 'number';
|
||||
}
|
||||
},
|
||||
shape: {
|
||||
type: String,
|
||||
default: 'circle',
|
||||
validator(val) {
|
||||
return ['circle', 'square'].includes(val);
|
||||
}
|
||||
},
|
||||
icon: String,
|
||||
src: String,
|
||||
alt: String,
|
||||
srcSet: String,
|
||||
error: Function,
|
||||
fit: {
|
||||
type: String,
|
||||
default: 'cover'
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isImageExist: true
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
avatarClass() {
|
||||
const { size, icon, shape } = this;
|
||||
let classList = ['el-avatar'];
|
||||
|
||||
if (size && typeof size === 'string') {
|
||||
classList.push(`el-avatar--${size}`);
|
||||
}
|
||||
|
||||
if (icon) {
|
||||
classList.push('el-avatar--icon');
|
||||
}
|
||||
|
||||
if (shape) {
|
||||
classList.push(`el-avatar--${shape}`);
|
||||
}
|
||||
|
||||
return classList.join(' ');
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleError() {
|
||||
const { error } = this;
|
||||
const errorFlag = error ? error() : undefined;
|
||||
if (errorFlag !== false) {
|
||||
this.isImageExist = false;
|
||||
}
|
||||
},
|
||||
renderAvatar() {
|
||||
const { icon, src, alt, isImageExist, srcSet, fit } = this;
|
||||
|
||||
if (isImageExist && src) {
|
||||
return <img
|
||||
src={src}
|
||||
onError={this.handleError}
|
||||
alt={alt}
|
||||
srcSet={srcSet}
|
||||
style={{ 'object-fit': fit }}/>;
|
||||
}
|
||||
|
||||
if (icon) {
|
||||
return (<i class={icon} />);
|
||||
}
|
||||
|
||||
return this.$slots.default;
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
const { avatarClass, size } = this;
|
||||
|
||||
const sizeStyle = typeof size === 'number' ? {
|
||||
height: `${size}px`,
|
||||
width: `${size}px`,
|
||||
lineHeight: `${size}px`
|
||||
} : {};
|
||||
|
||||
return (
|
||||
<span class={ avatarClass } style={ sizeStyle }>
|
||||
{
|
||||
this.renderAvatar()
|
||||
}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
8
packages/backtop/index.js
Normal file
8
packages/backtop/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import Backtop from './src/main';
|
||||
|
||||
/* istanbul ignore next */
|
||||
Backtop.install = function(Vue) {
|
||||
Vue.component(Backtop.name, Backtop);
|
||||
};
|
||||
|
||||
export default Backtop;
|
110
packages/backtop/src/main.vue
Normal file
110
packages/backtop/src/main.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<transition name="el-fade-in">
|
||||
<div
|
||||
v-if="visible"
|
||||
@click.stop="handleClick"
|
||||
:style="{
|
||||
'right': styleRight,
|
||||
'bottom': styleBottom
|
||||
}"
|
||||
class="el-backtop">
|
||||
<slot>
|
||||
<el-icon name="caret-top"></el-icon>
|
||||
</slot>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import throttle from 'throttle-debounce/throttle';
|
||||
|
||||
const cubic = value => Math.pow(value, 3);
|
||||
const easeInOutCubic = value => value < 0.5
|
||||
? cubic(value * 2) / 2
|
||||
: 1 - cubic((1 - value) * 2) / 2;
|
||||
|
||||
export default {
|
||||
name: 'ElBacktop',
|
||||
|
||||
props: {
|
||||
visibilityHeight: {
|
||||
type: Number,
|
||||
default: 200
|
||||
},
|
||||
target: [String],
|
||||
right: {
|
||||
type: Number,
|
||||
default: 40
|
||||
},
|
||||
bottom: {
|
||||
type: Number,
|
||||
default: 40
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
el: null,
|
||||
container: null,
|
||||
visible: false
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
styleBottom() {
|
||||
return `${this.bottom}px`;
|
||||
},
|
||||
styleRight() {
|
||||
return `${this.right}px`;
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.init();
|
||||
this.throttledScrollHandler = throttle(300, this.onScroll);
|
||||
this.container.addEventListener('scroll', this.throttledScrollHandler);
|
||||
},
|
||||
|
||||
methods: {
|
||||
init() {
|
||||
this.container = document;
|
||||
this.el = document.documentElement;
|
||||
if (this.target) {
|
||||
this.el = document.querySelector(this.target);
|
||||
if (!this.el) {
|
||||
throw new Error(`target is not existed: ${this.target}`);
|
||||
}
|
||||
this.container = this.el;
|
||||
}
|
||||
},
|
||||
onScroll() {
|
||||
const scrollTop = this.el.scrollTop;
|
||||
this.visible = scrollTop >= this.visibilityHeight;
|
||||
},
|
||||
handleClick(e) {
|
||||
this.scrollToTop();
|
||||
this.$emit('click', e);
|
||||
},
|
||||
scrollToTop() {
|
||||
const el = this.el;
|
||||
const beginTime = Date.now();
|
||||
const beginValue = el.scrollTop;
|
||||
const rAF = window.requestAnimationFrame || (func => setTimeout(func, 16));
|
||||
const frameFunc = () => {
|
||||
const progress = (Date.now() - beginTime) / 500;
|
||||
if (progress < 1) {
|
||||
el.scrollTop = beginValue * (1 - easeInOutCubic(progress));
|
||||
rAF(frameFunc);
|
||||
} else {
|
||||
el.scrollTop = 0;
|
||||
}
|
||||
};
|
||||
rAF(frameFunc);
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.container.removeEventListener('scroll', this.throttledScrollHandler);
|
||||
}
|
||||
};
|
||||
</script>
|
8
packages/badge/index.js
Normal file
8
packages/badge/index.js
Normal 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
packages/badge/src/main.vue
Normal file
53
packages/badge/src/main.vue
Normal 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: [String, Number],
|
||||
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
packages/breadcrumb-item/index.js
Normal file
8
packages/breadcrumb-item/index.js
Normal 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
packages/breadcrumb/index.js
Normal file
8
packages/breadcrumb/index.js
Normal 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
packages/breadcrumb/src/breadcrumb-item.vue
Normal file
41
packages/breadcrumb/src/breadcrumb-item.vue
Normal 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
packages/breadcrumb/src/breadcrumb.vue
Normal file
34
packages/breadcrumb/src/breadcrumb.vue
Normal 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
packages/button-group/index.js
Normal file
8
packages/button-group/index.js
Normal 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
packages/button/index.js
Normal file
8
packages/button/index.js
Normal 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
packages/button/src/button-group.vue
Normal file
10
packages/button/src/button-group.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<div class="el-button-group">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'ElButtonGroup'
|
||||
};
|
||||
</script>
|
78
packages/button/src/button.vue
Normal file
78
packages/button/src/button.vue
Normal 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
packages/calendar/index.js
Normal file
8
packages/calendar/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import Calendar from './src/main';
|
||||
|
||||
/* istanbul ignore next */
|
||||
Calendar.install = function(Vue) {
|
||||
Vue.component(Calendar.name, Calendar);
|
||||
};
|
||||
|
||||
export default Calendar;
|
199
packages/calendar/src/date-table.vue
Normal file
199
packages/calendar/src/date-table.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<script>
|
||||
import fecha from 'element-ui/src/utils/date';
|
||||
import { range as rangeArr, getFirstDayOfMonth, getPrevMonthLastDays, getMonthDays, getI18nSettings, validateRangeInOneMonth } from 'element-ui/src/utils/date-util';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
selectedDay: String, // formated date yyyy-MM-dd
|
||||
range: {
|
||||
type: Array,
|
||||
validator(val) {
|
||||
if (!(val && val.length)) return true;
|
||||
const [start, end] = val;
|
||||
return validateRangeInOneMonth(start, end);
|
||||
}
|
||||
},
|
||||
date: Date,
|
||||
hideHeader: Boolean,
|
||||
firstDayOfWeek: Number
|
||||
},
|
||||
|
||||
inject: ['elCalendar'],
|
||||
|
||||
methods: {
|
||||
toNestedArr(days) {
|
||||
return rangeArr(days.length / 7).map((_, index) => {
|
||||
const start = index * 7;
|
||||
return days.slice(start, start + 7);
|
||||
});
|
||||
},
|
||||
|
||||
getFormateDate(day, type) {
|
||||
if (!day || ['prev', 'current', 'next'].indexOf(type) === -1) {
|
||||
throw new Error('invalid day or type');
|
||||
}
|
||||
let prefix = this.curMonthDatePrefix;
|
||||
if (type === 'prev') {
|
||||
prefix = this.prevMonthDatePrefix;
|
||||
} else if (type === 'next') {
|
||||
prefix = this.nextMonthDatePrefix;
|
||||
}
|
||||
day = `00${day}`.slice(-2);
|
||||
return `${prefix}-${day}`;
|
||||
},
|
||||
|
||||
getCellClass({ text, type}) {
|
||||
const classes = [type];
|
||||
if (type === 'current') {
|
||||
const date = this.getFormateDate(text, type);
|
||||
if (date === this.selectedDay) {
|
||||
classes.push('is-selected');
|
||||
}
|
||||
if (date === this.formatedToday) {
|
||||
classes.push('is-today');
|
||||
}
|
||||
}
|
||||
return classes;
|
||||
},
|
||||
|
||||
pickDay({ text, type }) {
|
||||
const date = this.getFormateDate(text, type);
|
||||
this.$emit('pick', date);
|
||||
},
|
||||
|
||||
cellRenderProxy({ text, type }) {
|
||||
let render = this.elCalendar.$scopedSlots.dateCell;
|
||||
if (!render) return <span>{ text }</span>;
|
||||
|
||||
const day = this.getFormateDate(text, type);
|
||||
const date = new Date(day);
|
||||
const data = {
|
||||
isSelected: this.selectedDay === day,
|
||||
type: `${type}-month`,
|
||||
day
|
||||
};
|
||||
return render({ date, data });
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
WEEK_DAYS() {
|
||||
return getI18nSettings().dayNames;
|
||||
},
|
||||
prevMonthDatePrefix() {
|
||||
const temp = new Date(this.date.getTime());
|
||||
temp.setDate(0);
|
||||
return fecha.format(temp, 'yyyy-MM');
|
||||
},
|
||||
|
||||
curMonthDatePrefix() {
|
||||
return fecha.format(this.date, 'yyyy-MM');
|
||||
},
|
||||
|
||||
nextMonthDatePrefix() {
|
||||
const temp = new Date(this.date.getFullYear(), this.date.getMonth() + 1, 1);
|
||||
return fecha.format(temp, 'yyyy-MM');
|
||||
},
|
||||
|
||||
formatedToday() {
|
||||
return this.elCalendar.formatedToday;
|
||||
},
|
||||
|
||||
isInRange() {
|
||||
return this.range && this.range.length;
|
||||
},
|
||||
|
||||
rows() {
|
||||
let days = [];
|
||||
// if range exists, should render days in range.
|
||||
if (this.isInRange) {
|
||||
const [start, end] = this.range;
|
||||
const currentMonthRange = rangeArr(end.getDate() - start.getDate() + 1).map((_, index) => ({
|
||||
text: start.getDate() + index,
|
||||
type: 'current'
|
||||
}));
|
||||
let remaining = currentMonthRange.length % 7;
|
||||
remaining = remaining === 0 ? 0 : 7 - remaining;
|
||||
const nextMonthRange = rangeArr(remaining).map((_, index) => ({
|
||||
text: index + 1,
|
||||
type: 'next'
|
||||
}));
|
||||
days = currentMonthRange.concat(nextMonthRange);
|
||||
} else {
|
||||
const date = this.date;
|
||||
let firstDay = getFirstDayOfMonth(date);
|
||||
firstDay = firstDay === 0 ? 7 : firstDay;
|
||||
const firstDayOfWeek = typeof this.firstDayOfWeek === 'number' ? this.firstDayOfWeek : 1;
|
||||
const prevMonthDays = getPrevMonthLastDays(date, firstDay - firstDayOfWeek).map(day => ({
|
||||
text: day,
|
||||
type: 'prev'
|
||||
}));
|
||||
const currentMonthDays = getMonthDays(date).map(day => ({
|
||||
text: day,
|
||||
type: 'current'
|
||||
}));
|
||||
days = [...prevMonthDays, ...currentMonthDays];
|
||||
const nextMonthDays = rangeArr(42 - days.length).map((_, index) => ({
|
||||
text: index + 1,
|
||||
type: 'next'
|
||||
}));
|
||||
days = days.concat(nextMonthDays);
|
||||
}
|
||||
return this.toNestedArr(days);
|
||||
},
|
||||
|
||||
weekDays() {
|
||||
const start = this.firstDayOfWeek;
|
||||
const { WEEK_DAYS } = this;
|
||||
|
||||
if (typeof start !== 'number' || start === 0) {
|
||||
return WEEK_DAYS.slice();
|
||||
} else {
|
||||
return WEEK_DAYS.slice(start).concat(WEEK_DAYS.slice(0, start));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
const thead = this.hideHeader ? null : (<thead>
|
||||
{
|
||||
this.weekDays.map(day => <th key={day}>{ day }</th>)
|
||||
}
|
||||
</thead>);
|
||||
return (
|
||||
<table
|
||||
class={{
|
||||
'el-calendar-table': true,
|
||||
'is-range': this.isInRange
|
||||
}}
|
||||
cellspacing="0"
|
||||
cellpadding="0">
|
||||
{
|
||||
thead
|
||||
}
|
||||
<tbody>
|
||||
{
|
||||
this.rows.map((row, index) => <tr
|
||||
class={{
|
||||
'el-calendar-table__row': true,
|
||||
'el-calendar-table__row--hide-border': index === 0 && this.hideHeader
|
||||
}}
|
||||
key={index}>
|
||||
{
|
||||
row.map((cell, key) => <td key={key}
|
||||
class={ this.getCellClass(cell) }
|
||||
onClick={this.pickDay.bind(this, cell)}>
|
||||
<div class="el-calendar-day">
|
||||
{
|
||||
this.cellRenderProxy(cell)
|
||||
}
|
||||
</div>
|
||||
</td>)
|
||||
}
|
||||
</tr>)
|
||||
}
|
||||
</tbody>
|
||||
</table>);
|
||||
}
|
||||
};
|
||||
</script>
|
280
packages/calendar/src/main.vue
Normal file
280
packages/calendar/src/main.vue
Normal file
@@ -0,0 +1,280 @@
|
||||
<template>
|
||||
<div class="el-calendar">
|
||||
<div class="el-calendar__header">
|
||||
<div class="el-calendar__title">
|
||||
{{ i18nDate }}
|
||||
</div>
|
||||
<div
|
||||
class="el-calendar__button-group"
|
||||
v-if="validatedRange.length === 0">
|
||||
<el-button-group>
|
||||
<el-button
|
||||
type="plain"
|
||||
size="mini"
|
||||
@click="selectDate('prev-month')">
|
||||
{{ t('el.datepicker.prevMonth') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="plain"
|
||||
size="mini"
|
||||
@click="selectDate('today')">
|
||||
{{ t('el.datepicker.today') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="plain"
|
||||
size="mini"
|
||||
@click="selectDate('next-month')">
|
||||
{{ t('el.datepicker.nextMonth') }}
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="el-calendar__body"
|
||||
v-if="validatedRange.length === 0"
|
||||
key="no-range">
|
||||
<date-table
|
||||
:date="date"
|
||||
:selected-day="realSelectedDay"
|
||||
:first-day-of-week="realFirstDayOfWeek"
|
||||
@pick="pickDay" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="el-calendar__body"
|
||||
key="has-range">
|
||||
<date-table
|
||||
v-for="(range, index) in validatedRange"
|
||||
:key="index"
|
||||
:date="range[0]"
|
||||
:selected-day="realSelectedDay"
|
||||
:range="range"
|
||||
:hide-header="index !== 0"
|
||||
:first-day-of-week="realFirstDayOfWeek"
|
||||
@pick="pickDay" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Locale from 'element-ui/src/mixins/locale';
|
||||
import fecha from 'element-ui/src/utils/date';
|
||||
import ElButton from 'element-ui/packages/button';
|
||||
import ElButtonGroup from 'element-ui/packages/button-group';
|
||||
import DateTable from './date-table';
|
||||
import { validateRangeInOneMonth } from 'element-ui/src/utils/date-util';
|
||||
|
||||
const validTypes = ['prev-month', 'today', 'next-month'];
|
||||
const weekDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
const oneDay = 86400000;
|
||||
|
||||
export default {
|
||||
name: 'ElCalendar',
|
||||
|
||||
mixins: [Locale],
|
||||
|
||||
components: {
|
||||
DateTable,
|
||||
ElButton,
|
||||
ElButtonGroup
|
||||
},
|
||||
|
||||
props: {
|
||||
value: [Date, String, Number],
|
||||
range: {
|
||||
type: Array,
|
||||
validator(range) {
|
||||
if (Array.isArray(range)) {
|
||||
return range.length === 2 && range.every(
|
||||
item => typeof item === 'string' ||
|
||||
typeof item === 'number' ||
|
||||
item instanceof Date);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
},
|
||||
firstDayOfWeek: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
|
||||
provide() {
|
||||
return {
|
||||
elCalendar: this
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
pickDay(day) {
|
||||
this.realSelectedDay = day;
|
||||
},
|
||||
|
||||
selectDate(type) {
|
||||
if (validTypes.indexOf(type) === -1) {
|
||||
throw new Error(`invalid type ${type}`);
|
||||
}
|
||||
let day = '';
|
||||
if (type === 'prev-month') {
|
||||
day = `${this.prevMonthDatePrefix}-01`;
|
||||
} else if (type === 'next-month') {
|
||||
day = `${this.nextMonthDatePrefix}-01`;
|
||||
} else {
|
||||
day = this.formatedToday;
|
||||
}
|
||||
|
||||
if (day === this.formatedDate) return;
|
||||
this.pickDay(day);
|
||||
},
|
||||
|
||||
toDate(val) {
|
||||
if (!val) {
|
||||
throw new Error('invalid val');
|
||||
}
|
||||
return val instanceof Date ? val : new Date(val);
|
||||
},
|
||||
|
||||
rangeValidator(date, isStart) {
|
||||
const firstDayOfWeek = this.realFirstDayOfWeek;
|
||||
const expected = isStart ? firstDayOfWeek : (firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1);
|
||||
const message = `${isStart ? 'start' : 'end'} of range should be ${weekDays[expected]}.`;
|
||||
if (date.getDay() !== expected) {
|
||||
console.warn('[ElementCalendar]', message, 'Invalid range will be ignored.');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
prevMonthDatePrefix() {
|
||||
const temp = new Date(this.date.getTime());
|
||||
temp.setDate(0);
|
||||
return fecha.format(temp, 'yyyy-MM');
|
||||
},
|
||||
|
||||
curMonthDatePrefix() {
|
||||
return fecha.format(this.date, 'yyyy-MM');
|
||||
},
|
||||
|
||||
nextMonthDatePrefix() {
|
||||
const temp = new Date(this.date.getFullYear(), this.date.getMonth() + 1, 1);
|
||||
return fecha.format(temp, 'yyyy-MM');
|
||||
},
|
||||
|
||||
formatedDate() {
|
||||
return fecha.format(this.date, 'yyyy-MM-dd');
|
||||
},
|
||||
|
||||
i18nDate() {
|
||||
const year = this.date.getFullYear();
|
||||
const month = this.date.getMonth() + 1;
|
||||
return `${year} ${this.t('el.datepicker.year')} ${this.t('el.datepicker.month' + month)}`;
|
||||
},
|
||||
|
||||
formatedToday() {
|
||||
return fecha.format(this.now, 'yyyy-MM-dd');
|
||||
},
|
||||
|
||||
realSelectedDay: {
|
||||
get() {
|
||||
if (!this.value) return this.selectedDay;
|
||||
return this.formatedDate;
|
||||
},
|
||||
set(val) {
|
||||
this.selectedDay = val;
|
||||
const date = new Date(val);
|
||||
this.$emit('input', date);
|
||||
}
|
||||
},
|
||||
|
||||
date() {
|
||||
if (!this.value) {
|
||||
if (this.realSelectedDay) {
|
||||
const d = this.selectedDay.split('-');
|
||||
return new Date(d[0], d[1] - 1, d[2]);
|
||||
} else if (this.validatedRange.length) {
|
||||
return this.validatedRange[0][0];
|
||||
}
|
||||
return this.now;
|
||||
} else {
|
||||
return this.toDate(this.value);
|
||||
}
|
||||
},
|
||||
|
||||
// if range is valid, we get a two-digit array
|
||||
validatedRange() {
|
||||
let range = this.range;
|
||||
if (!range) return [];
|
||||
range = range.reduce((prev, val, index) => {
|
||||
const date = this.toDate(val);
|
||||
if (this.rangeValidator(date, index === 0)) {
|
||||
prev = prev.concat(date);
|
||||
}
|
||||
return prev;
|
||||
}, []);
|
||||
if (range.length === 2) {
|
||||
const [start, end] = range;
|
||||
if (start > end) {
|
||||
console.warn('[ElementCalendar]end time should be greater than start time');
|
||||
return [];
|
||||
}
|
||||
// start time and end time in one month
|
||||
if (validateRangeInOneMonth(start, end)) {
|
||||
return [
|
||||
[start, end]
|
||||
];
|
||||
}
|
||||
const data = [];
|
||||
let startDay = new Date(start.getFullYear(), start.getMonth() + 1, 1);
|
||||
const lastDay = this.toDate(startDay.getTime() - oneDay);
|
||||
if (!validateRangeInOneMonth(startDay, end)) {
|
||||
console.warn('[ElementCalendar]start time and end time interval must not exceed two months');
|
||||
return [];
|
||||
}
|
||||
// 第一个月的时间范围
|
||||
data.push([
|
||||
start,
|
||||
lastDay
|
||||
]);
|
||||
// 下一月的时间范围,需要计算一下该月的第一个周起始日
|
||||
const firstDayOfWeek = this.realFirstDayOfWeek;
|
||||
const nextMontFirstDay = startDay.getDay();
|
||||
let interval = 0;
|
||||
if (nextMontFirstDay !== firstDayOfWeek) {
|
||||
if (firstDayOfWeek === 0) {
|
||||
interval = 7 - nextMontFirstDay;
|
||||
} else {
|
||||
interval = firstDayOfWeek - nextMontFirstDay;
|
||||
interval = interval > 0 ? interval : 7 + interval;
|
||||
}
|
||||
}
|
||||
startDay = this.toDate(startDay.getTime() + interval * oneDay);
|
||||
if (startDay.getDate() < end.getDate()) {
|
||||
data.push([
|
||||
startDay,
|
||||
end
|
||||
]);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
|
||||
realFirstDayOfWeek() {
|
||||
if (this.firstDayOfWeek < 1 || this.firstDayOfWeek > 6) {
|
||||
return 0;
|
||||
}
|
||||
return Math.floor(this.firstDayOfWeek);
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
selectedDay: '',
|
||||
now: new Date()
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
8
packages/card/index.js
Normal file
8
packages/card/index.js
Normal 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
packages/card/src/main.vue
Normal file
23
packages/card/src/main.vue
Normal 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
packages/carousel-item/index.js
Normal file
8
packages/carousel-item/index.js
Normal 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
packages/carousel/index.js
Normal file
8
packages/carousel/index.js
Normal 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;
|
137
packages/carousel/src/item.vue
Normal file
137
packages/carousel/src/item.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<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="itemStyle">
|
||||
<div
|
||||
v-if="$parent.type === 'card'"
|
||||
v-show="!active"
|
||||
class="el-carousel__mask">
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { autoprefixer } from 'element-ui/src/utils/util';
|
||||
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;
|
||||
},
|
||||
|
||||
calcCardTranslate(index, activeIndex) {
|
||||
const parentWidth = this.$parent.$el.offsetWidth;
|
||||
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;
|
||||
}
|
||||
},
|
||||
|
||||
calcTranslate(index, activeIndex, isVertical) {
|
||||
const distance = this.$parent.$el[isVertical ? 'offsetHeight' : 'offsetWidth'];
|
||||
return distance * (index - activeIndex);
|
||||
},
|
||||
|
||||
translateItem(index, activeIndex, oldIndex) {
|
||||
const parentType = this.$parent.type;
|
||||
const parentDirection = this.parentDirection;
|
||||
const length = this.$parent.items.length;
|
||||
if (parentType !== 'card' && oldIndex !== undefined) {
|
||||
this.animating = index === activeIndex || index === oldIndex;
|
||||
}
|
||||
if (index !== activeIndex && length > 2 && this.$parent.loop) {
|
||||
index = this.processIndex(index, activeIndex, length);
|
||||
}
|
||||
if (parentType === 'card') {
|
||||
if (parentDirection === 'vertical') {
|
||||
console.warn('[Element Warn][Carousel]vertical direction is not supported in card mode');
|
||||
}
|
||||
this.inStage = Math.round(Math.abs(index - activeIndex)) <= 1;
|
||||
this.active = index === activeIndex;
|
||||
this.translate = this.calcCardTranslate(index, activeIndex);
|
||||
this.scale = this.active ? 1 : CARD_SCALE;
|
||||
} else {
|
||||
this.active = index === activeIndex;
|
||||
const isVertical = parentDirection === 'vertical';
|
||||
this.translate = this.calcTranslate(index, activeIndex, isVertical);
|
||||
}
|
||||
this.ready = true;
|
||||
},
|
||||
|
||||
handleItemClick() {
|
||||
const parent = this.$parent;
|
||||
if (parent && parent.type === 'card') {
|
||||
const index = parent.items.indexOf(this);
|
||||
parent.setActiveItem(index);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
parentDirection() {
|
||||
return this.$parent.direction;
|
||||
},
|
||||
|
||||
itemStyle() {
|
||||
const translateType = this.parentDirection === 'vertical' ? 'translateY' : 'translateX';
|
||||
const value = `${translateType}(${ this.translate }px) scale(${ this.scale })`;
|
||||
const style = {
|
||||
transform: value
|
||||
};
|
||||
return autoprefixer(style);
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.$parent && this.$parent.updateItems();
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
this.$parent && this.$parent.updateItems();
|
||||
}
|
||||
};
|
||||
</script>
|
304
packages/carousel/src/main.vue
Normal file
304
packages/carousel/src/main.vue
Normal file
@@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<div
|
||||
:class="carouselClasses"
|
||||
@mouseenter.stop="handleMouseEnter"
|
||||
@mouseleave.stop="handleMouseLeave">
|
||||
<div
|
||||
class="el-carousel__container"
|
||||
:style="{ height: height }">
|
||||
<transition
|
||||
v-if="arrowDisplay"
|
||||
name="carousel-arrow-left">
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
v-if="arrowDisplay"
|
||||
name="carousel-arrow-right">
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
v-if="indicatorPosition !== 'none'"
|
||||
:class="indicatorsClasses">
|
||||
<li
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
:class="[
|
||||
'el-carousel__indicator',
|
||||
'el-carousel__indicator--' + direction,
|
||||
{ '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
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: 'horizontal',
|
||||
validator(val) {
|
||||
return ['horizontal', 'vertical'].indexOf(val) !== -1;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
activeIndex: -1,
|
||||
containerWidth: 0,
|
||||
timer: null,
|
||||
hover: false
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
arrowDisplay() {
|
||||
return this.arrow !== 'never' && this.direction !== 'vertical';
|
||||
},
|
||||
|
||||
hasLabel() {
|
||||
return this.items.some(item => item.label.toString().length > 0);
|
||||
},
|
||||
|
||||
carouselClasses() {
|
||||
const classes = ['el-carousel', 'el-carousel--' + this.direction];
|
||||
if (this.type === 'card') {
|
||||
classes.push('el-carousel--card');
|
||||
}
|
||||
return classes;
|
||||
},
|
||||
|
||||
indicatorsClasses() {
|
||||
const classes = ['el-carousel__indicators', 'el-carousel__indicators--' + this.direction];
|
||||
if (this.hasLabel) {
|
||||
classes.push('el-carousel__indicators--labels');
|
||||
}
|
||||
if (this.indicatorPosition === 'outside' || this.type === 'card') {
|
||||
classes.push('el-carousel__indicators--outside');
|
||||
}
|
||||
return classes;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
items(val) {
|
||||
if (val.length > 0) this.setActiveItem(this.initialIndex);
|
||||
},
|
||||
|
||||
activeIndex(val, oldVal) {
|
||||
this.resetItemPosition(oldVal);
|
||||
if (oldVal > -1) {
|
||||
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) {
|
||||
if (this.direction === 'vertical') return;
|
||||
this.items.forEach((item, index) => {
|
||||
if (arrow === this.itemInStage(item, index)) {
|
||||
item.hover = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleButtonLeave() {
|
||||
if (this.direction === 'vertical') return;
|
||||
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)) {
|
||||
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
packages/cascader-panel/index.js
Normal file
8
packages/cascader-panel/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import CascaderPanel from './src/cascader-panel';
|
||||
|
||||
/* istanbul ignore next */
|
||||
CascaderPanel.install = function(Vue) {
|
||||
Vue.component(CascaderPanel.name, CascaderPanel);
|
||||
};
|
||||
|
||||
export default CascaderPanel;
|
138
packages/cascader-panel/src/cascader-menu.vue
Normal file
138
packages/cascader-panel/src/cascader-menu.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<script>
|
||||
import ElScrollbar from 'element-ui/packages/scrollbar';
|
||||
import CascaderNode from './cascader-node.vue';
|
||||
import Locale from 'element-ui/src/mixins/locale';
|
||||
import { generateId } from 'element-ui/src/utils/util';
|
||||
|
||||
export default {
|
||||
name: 'ElCascaderMenu',
|
||||
|
||||
mixins: [Locale],
|
||||
|
||||
inject: ['panel'],
|
||||
|
||||
components: {
|
||||
ElScrollbar,
|
||||
CascaderNode
|
||||
},
|
||||
|
||||
props: {
|
||||
nodes: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
index: Number
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
activeNode: null,
|
||||
hoverTimer: null,
|
||||
id: generateId()
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
isEmpty() {
|
||||
return !this.nodes.length;
|
||||
},
|
||||
menuId() {
|
||||
return `cascader-menu-${this.id}-${this.index}`;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleExpand(e) {
|
||||
this.activeNode = e.target;
|
||||
},
|
||||
handleMouseMove(e) {
|
||||
const { activeNode, hoverTimer } = this;
|
||||
const { hoverZone } = this.$refs;
|
||||
|
||||
if (!activeNode || !hoverZone) return;
|
||||
|
||||
if (activeNode.contains(e.target)) {
|
||||
clearTimeout(hoverTimer);
|
||||
|
||||
const { left } = this.$el.getBoundingClientRect();
|
||||
const startX = e.clientX - left;
|
||||
const { offsetWidth, offsetHeight } = this.$el;
|
||||
const top = activeNode.offsetTop;
|
||||
const bottom = top + activeNode.offsetHeight;
|
||||
|
||||
hoverZone.innerHTML = `
|
||||
<path style="pointer-events: auto;" fill="transparent" d="M${startX} ${top} L${offsetWidth} 0 V${top} Z" />
|
||||
<path style="pointer-events: auto;" fill="transparent" d="M${startX} ${bottom} L${offsetWidth} ${offsetHeight} V${bottom} Z" />
|
||||
`;
|
||||
} else if (!hoverTimer) {
|
||||
this.hoverTimer = setTimeout(this.clearHoverZone, this.panel.config.hoverThreshold);
|
||||
}
|
||||
},
|
||||
clearHoverZone() {
|
||||
const { hoverZone } = this.$refs;
|
||||
if (!hoverZone) return;
|
||||
hoverZone.innerHTML = '';
|
||||
},
|
||||
|
||||
renderEmptyText(h) {
|
||||
return (
|
||||
<div class="el-cascader-menu__empty-text">{ this.t('el.cascader.noData') }</div>
|
||||
);
|
||||
},
|
||||
renderNodeList(h) {
|
||||
const { menuId } = this;
|
||||
const { isHoverMenu } = this.panel;
|
||||
const events = { on: {} };
|
||||
|
||||
if (isHoverMenu) {
|
||||
events.on.expand = this.handleExpand;
|
||||
}
|
||||
|
||||
const nodes = this.nodes.map((node, index) => {
|
||||
const { hasChildren } = node;
|
||||
return (
|
||||
<cascader-node
|
||||
key={ node.uid }
|
||||
node={ node }
|
||||
node-id={ `${menuId}-${index}` }
|
||||
aria-haspopup={ hasChildren }
|
||||
aria-owns = { hasChildren ? menuId : null }
|
||||
{ ...events }></cascader-node>
|
||||
);
|
||||
});
|
||||
|
||||
return [
|
||||
...nodes,
|
||||
isHoverMenu ? <svg ref='hoverZone' class='el-cascader-menu__hover-zone'></svg> : null
|
||||
];
|
||||
}
|
||||
},
|
||||
|
||||
render(h) {
|
||||
const { isEmpty, menuId } = this;
|
||||
const events = { nativeOn: {} };
|
||||
|
||||
// optimize hover to expand experience (#8010)
|
||||
if (this.panel.isHoverMenu) {
|
||||
events.nativeOn.mousemove = this.handleMouseMove;
|
||||
// events.nativeOn.mouseleave = this.clearHoverZone;
|
||||
}
|
||||
|
||||
return (
|
||||
<el-scrollbar
|
||||
tag="ul"
|
||||
role="menu"
|
||||
id={ menuId }
|
||||
class="el-cascader-menu"
|
||||
wrap-class="el-cascader-menu__wrap"
|
||||
view-class={{
|
||||
'el-cascader-menu__list': true,
|
||||
'is-empty': isEmpty
|
||||
}}
|
||||
{ ...events }>
|
||||
{ isEmpty ? this.renderEmptyText(h) : this.renderNodeList(h) }
|
||||
</el-scrollbar>
|
||||
);
|
||||
}
|
||||
};
|
||||
</script>
|
246
packages/cascader-panel/src/cascader-node.vue
Normal file
246
packages/cascader-panel/src/cascader-node.vue
Normal file
@@ -0,0 +1,246 @@
|
||||
<script>
|
||||
import ElCheckbox from 'element-ui/packages/checkbox';
|
||||
import ElRadio from 'element-ui/packages/radio';
|
||||
import { isEqual } from 'element-ui/src/utils/util';
|
||||
|
||||
const stopPropagation = e => e.stopPropagation();
|
||||
|
||||
export default {
|
||||
inject: ['panel'],
|
||||
|
||||
components: {
|
||||
ElCheckbox,
|
||||
ElRadio
|
||||
},
|
||||
|
||||
props: {
|
||||
node: {
|
||||
required: true
|
||||
},
|
||||
nodeId: String
|
||||
},
|
||||
|
||||
computed: {
|
||||
config() {
|
||||
return this.panel.config;
|
||||
},
|
||||
isLeaf() {
|
||||
return this.node.isLeaf;
|
||||
},
|
||||
isDisabled() {
|
||||
return this.node.isDisabled;
|
||||
},
|
||||
checkedValue() {
|
||||
return this.panel.checkedValue;
|
||||
},
|
||||
isChecked() {
|
||||
return this.node.isSameNode(this.checkedValue);
|
||||
},
|
||||
inActivePath() {
|
||||
return this.isInPath(this.panel.activePath);
|
||||
},
|
||||
inCheckedPath() {
|
||||
if (!this.config.checkStrictly) return false;
|
||||
|
||||
return this.panel.checkedNodePaths
|
||||
.some(checkedPath => this.isInPath(checkedPath));
|
||||
},
|
||||
value() {
|
||||
return this.node.getValueByOption();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleExpand() {
|
||||
const { panel, node, isDisabled, config } = this;
|
||||
const { multiple, checkStrictly } = config;
|
||||
|
||||
if (!checkStrictly && isDisabled || node.loading) return;
|
||||
|
||||
if (config.lazy && !node.loaded) {
|
||||
panel.lazyLoad(node, () => {
|
||||
// do not use cached leaf value here, invoke this.isLeaf to get new value.
|
||||
const { isLeaf } = this;
|
||||
|
||||
if (!isLeaf) this.handleExpand();
|
||||
if (multiple) {
|
||||
// if leaf sync checked state, else clear checked state
|
||||
const checked = isLeaf ? node.checked : false;
|
||||
this.handleMultiCheckChange(checked);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
panel.handleExpand(node);
|
||||
}
|
||||
},
|
||||
|
||||
handleCheckChange() {
|
||||
const { panel, value, node } = this;
|
||||
panel.handleCheckChange(value);
|
||||
panel.handleExpand(node);
|
||||
},
|
||||
|
||||
handleMultiCheckChange(checked) {
|
||||
this.node.doCheck(checked);
|
||||
this.panel.calculateMultiCheckedValue();
|
||||
},
|
||||
|
||||
isInPath(pathNodes) {
|
||||
const { node } = this;
|
||||
const selectedPathNode = pathNodes[node.level - 1] || {};
|
||||
return selectedPathNode.uid === node.uid;
|
||||
},
|
||||
|
||||
renderPrefix(h) {
|
||||
const { isLeaf, isChecked, config } = this;
|
||||
const { checkStrictly, multiple } = config;
|
||||
|
||||
if (multiple) {
|
||||
return this.renderCheckbox(h);
|
||||
} else if (checkStrictly) {
|
||||
return this.renderRadio(h);
|
||||
} else if (isLeaf && isChecked) {
|
||||
return this.renderCheckIcon(h);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
renderPostfix(h) {
|
||||
const { node, isLeaf } = this;
|
||||
|
||||
if (node.loading) {
|
||||
return this.renderLoadingIcon(h);
|
||||
} else if (!isLeaf) {
|
||||
return this.renderExpandIcon(h);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
renderCheckbox(h) {
|
||||
const { node, config, isDisabled } = this;
|
||||
const events = {
|
||||
on: { change: this.handleMultiCheckChange },
|
||||
nativeOn: {}
|
||||
};
|
||||
|
||||
if (config.checkStrictly) { // when every node is selectable, click event should not trigger expand event.
|
||||
events.nativeOn.click = stopPropagation;
|
||||
}
|
||||
|
||||
return (
|
||||
<el-checkbox
|
||||
value={ node.checked }
|
||||
indeterminate={ node.indeterminate }
|
||||
disabled={ isDisabled }
|
||||
{ ...events }
|
||||
></el-checkbox>
|
||||
);
|
||||
},
|
||||
|
||||
renderRadio(h) {
|
||||
let { checkedValue, value, isDisabled } = this;
|
||||
|
||||
// to keep same reference if value cause radio's checked state is calculated by reference comparision;
|
||||
if (isEqual(value, checkedValue)) {
|
||||
value = checkedValue;
|
||||
}
|
||||
|
||||
return (
|
||||
<el-radio
|
||||
value={ checkedValue }
|
||||
label={ value }
|
||||
disabled={ isDisabled }
|
||||
onChange={ this.handleCheckChange }
|
||||
nativeOnClick={ stopPropagation }>
|
||||
{/* add an empty element to avoid render label */}
|
||||
<span></span>
|
||||
</el-radio>
|
||||
);
|
||||
},
|
||||
|
||||
renderCheckIcon(h) {
|
||||
return (
|
||||
<i class="el-icon-check el-cascader-node__prefix"></i>
|
||||
);
|
||||
},
|
||||
|
||||
renderLoadingIcon(h) {
|
||||
return (
|
||||
<i class="el-icon-loading el-cascader-node__postfix"></i>
|
||||
);
|
||||
},
|
||||
|
||||
renderExpandIcon(h) {
|
||||
return (
|
||||
<i class="el-icon-arrow-right el-cascader-node__postfix"></i>
|
||||
);
|
||||
},
|
||||
|
||||
renderContent(h) {
|
||||
const { panel, node } = this;
|
||||
const render = panel.renderLabelFn;
|
||||
const vnode = render
|
||||
? render({ node, data: node.data })
|
||||
: null;
|
||||
|
||||
return (
|
||||
<span class="el-cascader-node__label">{ vnode || node.label }</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
render(h) {
|
||||
const {
|
||||
inActivePath,
|
||||
inCheckedPath,
|
||||
isChecked,
|
||||
isLeaf,
|
||||
isDisabled,
|
||||
config,
|
||||
nodeId
|
||||
} = this;
|
||||
const { expandTrigger, checkStrictly, multiple } = config;
|
||||
const disabled = !checkStrictly && isDisabled;
|
||||
const events = { on: {} };
|
||||
|
||||
if (expandTrigger === 'click') {
|
||||
events.on.click = this.handleExpand;
|
||||
} else {
|
||||
events.on.mouseenter = e => {
|
||||
this.handleExpand();
|
||||
this.$emit('expand', e);
|
||||
};
|
||||
events.on.focus = e => {
|
||||
this.handleExpand();
|
||||
this.$emit('expand', e);
|
||||
};
|
||||
}
|
||||
if (isLeaf && !isDisabled && !checkStrictly && !multiple) {
|
||||
events.on.click = this.handleCheckChange;
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
role="menuitem"
|
||||
id={ nodeId }
|
||||
aria-expanded={ inActivePath }
|
||||
tabindex={ disabled ? null : -1 }
|
||||
class={{
|
||||
'el-cascader-node': true,
|
||||
'is-selectable': checkStrictly,
|
||||
'in-active-path': inActivePath,
|
||||
'in-checked-path': inCheckedPath,
|
||||
'is-active': isChecked,
|
||||
'is-disabled': disabled
|
||||
}}
|
||||
{...events}>
|
||||
{ this.renderPrefix(h) }
|
||||
{ this.renderContent(h) }
|
||||
{ this.renderPostfix(h) }
|
||||
</li>
|
||||
);
|
||||
}
|
||||
};
|
||||
</script>
|
383
packages/cascader-panel/src/cascader-panel.vue
Normal file
383
packages/cascader-panel/src/cascader-panel.vue
Normal file
@@ -0,0 +1,383 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'el-cascader-panel',
|
||||
border && 'is-bordered'
|
||||
]"
|
||||
@keydown="handleKeyDown">
|
||||
<cascader-menu
|
||||
ref="menu"
|
||||
v-for="(menu, index) in menus"
|
||||
:index="index"
|
||||
:key="index"
|
||||
:nodes="menu"></cascader-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CascaderMenu from './cascader-menu';
|
||||
import Store from './store';
|
||||
import merge from 'element-ui/src/utils/merge';
|
||||
import AriaUtils from 'element-ui/src/utils/aria-utils';
|
||||
import scrollIntoView from 'element-ui/src/utils/scroll-into-view';
|
||||
import {
|
||||
noop,
|
||||
coerceTruthyValueToArray,
|
||||
isEqual,
|
||||
isEmpty,
|
||||
valueEquals
|
||||
} from 'element-ui/src/utils/util';
|
||||
|
||||
const { keys: KeyCode } = AriaUtils;
|
||||
const DefaultProps = {
|
||||
expandTrigger: 'click', // or hover
|
||||
multiple: false,
|
||||
checkStrictly: false, // whether all nodes can be selected
|
||||
emitPath: true, // wether to emit an array of all levels value in which node is located
|
||||
lazy: false,
|
||||
lazyLoad: noop,
|
||||
value: 'value',
|
||||
label: 'label',
|
||||
children: 'children',
|
||||
leaf: 'leaf',
|
||||
disabled: 'disabled',
|
||||
hoverThreshold: 500
|
||||
};
|
||||
|
||||
const isLeaf = el => !el.getAttribute('aria-owns');
|
||||
|
||||
const getSibling = (el, distance) => {
|
||||
const { parentNode } = el;
|
||||
if (parentNode) {
|
||||
const siblings = parentNode.querySelectorAll('.el-cascader-node[tabindex="-1"]');
|
||||
const index = Array.prototype.indexOf.call(siblings, el);
|
||||
return siblings[index + distance] || null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getMenuIndex = (el, distance) => {
|
||||
if (!el) return;
|
||||
const pieces = el.id.split('-');
|
||||
return Number(pieces[pieces.length - 2]);
|
||||
};
|
||||
|
||||
const focusNode = el => {
|
||||
if (!el) return;
|
||||
el.focus();
|
||||
!isLeaf(el) && el.click();
|
||||
};
|
||||
|
||||
const checkNode = el => {
|
||||
if (!el) return;
|
||||
|
||||
const input = el.querySelector('input');
|
||||
if (input) {
|
||||
input.click();
|
||||
} else if (isLeaf(el)) {
|
||||
el.click();
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'ElCascaderPanel',
|
||||
|
||||
components: {
|
||||
CascaderMenu
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {},
|
||||
options: Array,
|
||||
props: Object,
|
||||
border: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
renderLabel: Function
|
||||
},
|
||||
|
||||
provide() {
|
||||
return {
|
||||
panel: this
|
||||
};
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
checkedValue: null,
|
||||
checkedNodePaths: [],
|
||||
store: [],
|
||||
menus: [],
|
||||
activePath: [],
|
||||
loadCount: 0
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
config() {
|
||||
return merge({ ...DefaultProps }, this.props || {});
|
||||
},
|
||||
multiple() {
|
||||
return this.config.multiple;
|
||||
},
|
||||
checkStrictly() {
|
||||
return this.config.checkStrictly;
|
||||
},
|
||||
leafOnly() {
|
||||
return !this.checkStrictly;
|
||||
},
|
||||
isHoverMenu() {
|
||||
return this.config.expandTrigger === 'hover';
|
||||
},
|
||||
renderLabelFn() {
|
||||
return this.renderLabel || this.$scopedSlots.default;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
options: {
|
||||
handler: function() {
|
||||
this.initStore();
|
||||
},
|
||||
immediate: true,
|
||||
deep: true
|
||||
},
|
||||
value() {
|
||||
this.syncCheckedValue();
|
||||
this.checkStrictly && this.calculateCheckedNodePaths();
|
||||
},
|
||||
checkedValue(val) {
|
||||
if (!isEqual(val, this.value)) {
|
||||
this.checkStrictly && this.calculateCheckedNodePaths();
|
||||
this.$emit('input', val);
|
||||
this.$emit('change', val);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (!isEmpty(this.value)) {
|
||||
this.syncCheckedValue();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
initStore() {
|
||||
const { config, options } = this;
|
||||
if (config.lazy && isEmpty(options)) {
|
||||
this.lazyLoad();
|
||||
} else {
|
||||
this.store = new Store(options, config);
|
||||
this.menus = [this.store.getNodes()];
|
||||
this.syncMenuState();
|
||||
}
|
||||
},
|
||||
syncCheckedValue() {
|
||||
const { value, checkedValue } = this;
|
||||
if (!isEqual(value, checkedValue)) {
|
||||
this.activePath = [];
|
||||
this.checkedValue = value;
|
||||
this.syncMenuState();
|
||||
}
|
||||
},
|
||||
syncMenuState() {
|
||||
const { multiple, checkStrictly } = this;
|
||||
this.syncActivePath();
|
||||
multiple && this.syncMultiCheckState();
|
||||
checkStrictly && this.calculateCheckedNodePaths();
|
||||
this.$nextTick(this.scrollIntoView);
|
||||
},
|
||||
syncMultiCheckState() {
|
||||
const nodes = this.getFlattedNodes(this.leafOnly);
|
||||
|
||||
nodes.forEach(node => {
|
||||
node.syncCheckState(this.checkedValue);
|
||||
});
|
||||
},
|
||||
syncActivePath() {
|
||||
const { store, multiple, activePath, checkedValue } = this;
|
||||
|
||||
if (!isEmpty(activePath)) {
|
||||
const nodes = activePath.map(node => this.getNodeByValue(node.getValue()));
|
||||
this.expandNodes(nodes);
|
||||
} else if (!isEmpty(checkedValue)) {
|
||||
const value = multiple ? checkedValue[0] : checkedValue;
|
||||
const checkedNode = this.getNodeByValue(value) || {};
|
||||
const nodes = (checkedNode.pathNodes || []).slice(0, -1);
|
||||
this.expandNodes(nodes);
|
||||
} else {
|
||||
this.activePath = [];
|
||||
this.menus = [store.getNodes()];
|
||||
}
|
||||
},
|
||||
expandNodes(nodes) {
|
||||
nodes.forEach(node => this.handleExpand(node, true /* silent */));
|
||||
},
|
||||
calculateCheckedNodePaths() {
|
||||
const { checkedValue, multiple } = this;
|
||||
const checkedValues = multiple
|
||||
? coerceTruthyValueToArray(checkedValue)
|
||||
: [ checkedValue ];
|
||||
this.checkedNodePaths = checkedValues.map(v => {
|
||||
const checkedNode = this.getNodeByValue(v);
|
||||
return checkedNode ? checkedNode.pathNodes : [];
|
||||
});
|
||||
},
|
||||
handleKeyDown(e) {
|
||||
const { target, keyCode } = e;
|
||||
|
||||
switch (keyCode) {
|
||||
case KeyCode.up:
|
||||
const prev = getSibling(target, -1);
|
||||
focusNode(prev);
|
||||
break;
|
||||
case KeyCode.down:
|
||||
const next = getSibling(target, 1);
|
||||
focusNode(next);
|
||||
break;
|
||||
case KeyCode.left:
|
||||
const preMenu = this.$refs.menu[getMenuIndex(target) - 1];
|
||||
if (preMenu) {
|
||||
const expandedNode = preMenu.$el.querySelector('.el-cascader-node[aria-expanded="true"]');
|
||||
focusNode(expandedNode);
|
||||
}
|
||||
break;
|
||||
case KeyCode.right:
|
||||
const nextMenu = this.$refs.menu[getMenuIndex(target) + 1];
|
||||
if (nextMenu) {
|
||||
const firstNode = nextMenu.$el.querySelector('.el-cascader-node[tabindex="-1"]');
|
||||
focusNode(firstNode);
|
||||
}
|
||||
break;
|
||||
case KeyCode.enter:
|
||||
checkNode(target);
|
||||
break;
|
||||
case KeyCode.esc:
|
||||
case KeyCode.tab:
|
||||
this.$emit('close');
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
},
|
||||
handleExpand(node, silent) {
|
||||
const { activePath } = this;
|
||||
const { level } = node;
|
||||
const path = activePath.slice(0, level - 1);
|
||||
const menus = this.menus.slice(0, level);
|
||||
|
||||
if (!node.isLeaf) {
|
||||
path.push(node);
|
||||
menus.push(node.children);
|
||||
}
|
||||
|
||||
this.activePath = path;
|
||||
this.menus = menus;
|
||||
|
||||
if (!silent) {
|
||||
const pathValues = path.map(node => node.getValue());
|
||||
const activePathValues = activePath.map(node => node.getValue());
|
||||
if (!valueEquals(pathValues, activePathValues)) {
|
||||
this.$emit('active-item-change', pathValues); // Deprecated
|
||||
this.$emit('expand-change', pathValues);
|
||||
}
|
||||
}
|
||||
},
|
||||
handleCheckChange(value) {
|
||||
this.checkedValue = value;
|
||||
},
|
||||
lazyLoad(node, onFullfiled) {
|
||||
const { config } = this;
|
||||
if (!node) {
|
||||
node = node || { root: true, level: 0 };
|
||||
this.store = new Store([], config);
|
||||
this.menus = [this.store.getNodes()];
|
||||
}
|
||||
node.loading = true;
|
||||
const resolve = dataList => {
|
||||
const parent = node.root ? null : node;
|
||||
dataList && dataList.length && this.store.appendNodes(dataList, parent);
|
||||
node.loading = false;
|
||||
node.loaded = true;
|
||||
|
||||
// dispose default value on lazy load mode
|
||||
if (Array.isArray(this.checkedValue)) {
|
||||
const nodeValue = this.checkedValue[this.loadCount++];
|
||||
const valueKey = this.config.value;
|
||||
const leafKey = this.config.leaf;
|
||||
|
||||
if (Array.isArray(dataList) && dataList.filter(item => item[valueKey] === nodeValue).length > 0) {
|
||||
const checkedNode = this.store.getNodeByValue(nodeValue);
|
||||
|
||||
if (!checkedNode.data[leafKey]) {
|
||||
this.lazyLoad(checkedNode, () => {
|
||||
this.handleExpand(checkedNode);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.loadCount === this.checkedValue.length) {
|
||||
this.$parent.computePresentText();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onFullfiled && onFullfiled(dataList);
|
||||
};
|
||||
config.lazyLoad(node, resolve);
|
||||
},
|
||||
|
||||
/**
|
||||
* public methods
|
||||
*/
|
||||
calculateMultiCheckedValue() {
|
||||
this.checkedValue = this.getCheckedNodes(this.leafOnly)
|
||||
.map(node => node.getValueByOption());
|
||||
},
|
||||
scrollIntoView() {
|
||||
if (this.$isServer) return;
|
||||
|
||||
const menus = this.$refs.menu || [];
|
||||
menus.forEach(menu => {
|
||||
const menuElement = menu.$el;
|
||||
if (menuElement) {
|
||||
const container = menuElement.querySelector('.el-scrollbar__wrap');
|
||||
const activeNode = menuElement.querySelector('.el-cascader-node.is-active') ||
|
||||
menuElement.querySelector('.el-cascader-node.in-active-path');
|
||||
scrollIntoView(container, activeNode);
|
||||
}
|
||||
});
|
||||
},
|
||||
getNodeByValue(val) {
|
||||
return this.store.getNodeByValue(val);
|
||||
},
|
||||
getFlattedNodes(leafOnly) {
|
||||
const cached = !this.config.lazy;
|
||||
return this.store.getFlattedNodes(leafOnly, cached);
|
||||
},
|
||||
getCheckedNodes(leafOnly) {
|
||||
const { checkedValue, multiple } = this;
|
||||
if (multiple) {
|
||||
const nodes = this.getFlattedNodes(leafOnly);
|
||||
return nodes.filter(node => node.checked);
|
||||
} else {
|
||||
return isEmpty(checkedValue)
|
||||
? []
|
||||
: [this.getNodeByValue(checkedValue)];
|
||||
}
|
||||
},
|
||||
clearCheckedNodes() {
|
||||
const { config, leafOnly } = this;
|
||||
const { multiple, emitPath } = config;
|
||||
if (multiple) {
|
||||
this.getCheckedNodes(leafOnly)
|
||||
.filter(node => !node.isDisabled)
|
||||
.forEach(node => node.doCheck(false));
|
||||
this.calculateMultiCheckedValue();
|
||||
} else {
|
||||
this.checkedValue = emitPath ? [] : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
166
packages/cascader-panel/src/node.js
Normal file
166
packages/cascader-panel/src/node.js
Normal file
@@ -0,0 +1,166 @@
|
||||
import { isEqual, capitalize } from 'element-ui/src/utils/util';
|
||||
import { isDef } from 'element-ui/src/utils/shared';
|
||||
|
||||
let uid = 0;
|
||||
|
||||
export default class Node {
|
||||
|
||||
constructor(data, config, parentNode) {
|
||||
this.data = data;
|
||||
this.config = config;
|
||||
this.parent = parentNode || null;
|
||||
this.level = !this.parent ? 1 : this.parent.level + 1;
|
||||
this.uid = uid++;
|
||||
|
||||
this.initState();
|
||||
this.initChildren();
|
||||
}
|
||||
|
||||
initState() {
|
||||
const { value: valueKey, label: labelKey } = this.config;
|
||||
|
||||
this.value = this.data[valueKey];
|
||||
this.label = this.data[labelKey];
|
||||
this.pathNodes = this.calculatePathNodes();
|
||||
this.path = this.pathNodes.map(node => node.value);
|
||||
this.pathLabels = this.pathNodes.map(node => node.label);
|
||||
|
||||
// lazy load
|
||||
this.loading = false;
|
||||
this.loaded = false;
|
||||
}
|
||||
|
||||
initChildren() {
|
||||
const { config } = this;
|
||||
const childrenKey = config.children;
|
||||
const childrenData = this.data[childrenKey];
|
||||
this.hasChildren = Array.isArray(childrenData);
|
||||
this.children = (childrenData || []).map(child => new Node(child, config, this));
|
||||
}
|
||||
|
||||
get isDisabled() {
|
||||
const { data, parent, config } = this;
|
||||
const disabledKey = config.disabled;
|
||||
const { checkStrictly } = config;
|
||||
return data[disabledKey] ||
|
||||
!checkStrictly && parent && parent.isDisabled;
|
||||
}
|
||||
|
||||
get isLeaf() {
|
||||
const { data, loaded, hasChildren, children } = this;
|
||||
const { lazy, leaf: leafKey } = this.config;
|
||||
if (lazy) {
|
||||
const isLeaf = isDef(data[leafKey])
|
||||
? data[leafKey]
|
||||
: (loaded ? !children.length : false);
|
||||
this.hasChildren = !isLeaf;
|
||||
return isLeaf;
|
||||
}
|
||||
return !hasChildren;
|
||||
}
|
||||
|
||||
calculatePathNodes() {
|
||||
const nodes = [this];
|
||||
let parent = this.parent;
|
||||
|
||||
while (parent) {
|
||||
nodes.unshift(parent);
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
getPath() {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
getValueByOption() {
|
||||
return this.config.emitPath
|
||||
? this.getPath()
|
||||
: this.getValue();
|
||||
}
|
||||
|
||||
getText(allLevels, separator) {
|
||||
return allLevels ? this.pathLabels.join(separator) : this.label;
|
||||
}
|
||||
|
||||
isSameNode(checkedValue) {
|
||||
const value = this.getValueByOption();
|
||||
return this.config.multiple && Array.isArray(checkedValue)
|
||||
? checkedValue.some(val => isEqual(val, value))
|
||||
: isEqual(checkedValue, value);
|
||||
}
|
||||
|
||||
broadcast(event, ...args) {
|
||||
const handlerName = `onParent${capitalize(event)}`;
|
||||
|
||||
this.children.forEach(child => {
|
||||
if (child) {
|
||||
// bottom up
|
||||
child.broadcast(event, ...args);
|
||||
child[handlerName] && child[handlerName](...args);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
emit(event, ...args) {
|
||||
const { parent } = this;
|
||||
const handlerName = `onChild${capitalize(event)}`;
|
||||
if (parent) {
|
||||
parent[handlerName] && parent[handlerName](...args);
|
||||
parent.emit(event, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
onParentCheck(checked) {
|
||||
if (!this.isDisabled) {
|
||||
this.setCheckState(checked);
|
||||
}
|
||||
}
|
||||
|
||||
onChildCheck() {
|
||||
const { children } = this;
|
||||
const validChildren = children.filter(child => !child.isDisabled);
|
||||
const checked = validChildren.length
|
||||
? validChildren.every(child => child.checked)
|
||||
: false;
|
||||
|
||||
this.setCheckState(checked);
|
||||
}
|
||||
|
||||
setCheckState(checked) {
|
||||
const totalNum = this.children.length;
|
||||
const checkedNum = this.children.reduce((c, p) => {
|
||||
const num = p.checked ? 1 : (p.indeterminate ? 0.5 : 0);
|
||||
return c + num;
|
||||
}, 0);
|
||||
|
||||
this.checked = checked;
|
||||
this.indeterminate = checkedNum !== totalNum && checkedNum > 0;
|
||||
}
|
||||
|
||||
syncCheckState(checkedValue) {
|
||||
const value = this.getValueByOption();
|
||||
const checked = this.isSameNode(checkedValue, value);
|
||||
|
||||
this.doCheck(checked);
|
||||
}
|
||||
|
||||
doCheck(checked) {
|
||||
if (this.checked !== checked) {
|
||||
if (this.config.checkStrictly) {
|
||||
this.checked = checked;
|
||||
} else {
|
||||
// bottom up to unify the calculation of the indeterminate state
|
||||
this.broadcast('check', checked);
|
||||
this.setCheckState(checked);
|
||||
this.emit('check');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
62
packages/cascader-panel/src/store.js
Normal file
62
packages/cascader-panel/src/store.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import Node from './node';
|
||||
import { coerceTruthyValueToArray, valueEquals } from 'element-ui/src/utils/util';
|
||||
|
||||
const flatNodes = (data, leafOnly) => {
|
||||
return data.reduce((res, node) => {
|
||||
if (node.isLeaf) {
|
||||
res.push(node);
|
||||
} else {
|
||||
!leafOnly && res.push(node);
|
||||
res = res.concat(flatNodes(node.children, leafOnly));
|
||||
}
|
||||
return res;
|
||||
}, []);
|
||||
};
|
||||
|
||||
export default class Store {
|
||||
|
||||
constructor(data, config) {
|
||||
this.config = config;
|
||||
this.initNodes(data);
|
||||
}
|
||||
|
||||
initNodes(data) {
|
||||
data = coerceTruthyValueToArray(data);
|
||||
this.nodes = data.map(nodeData => new Node(nodeData, this.config));
|
||||
this.flattedNodes = this.getFlattedNodes(false, false);
|
||||
this.leafNodes = this.getFlattedNodes(true, false);
|
||||
}
|
||||
|
||||
appendNode(nodeData, parentNode) {
|
||||
const node = new Node(nodeData, this.config, parentNode);
|
||||
const children = parentNode ? parentNode.children : this.nodes;
|
||||
|
||||
children.push(node);
|
||||
}
|
||||
|
||||
appendNodes(nodeDataList, parentNode) {
|
||||
nodeDataList = coerceTruthyValueToArray(nodeDataList);
|
||||
nodeDataList.forEach(nodeData => this.appendNode(nodeData, parentNode));
|
||||
}
|
||||
|
||||
getNodes() {
|
||||
return this.nodes;
|
||||
}
|
||||
|
||||
getFlattedNodes(leafOnly, cached = true) {
|
||||
const cachedNodes = leafOnly ? this.leafNodes : this.flattedNodes;
|
||||
return cached
|
||||
? cachedNodes
|
||||
: flatNodes(this.nodes, leafOnly);
|
||||
}
|
||||
|
||||
getNodeByValue(value) {
|
||||
if (value) {
|
||||
const nodes = this.getFlattedNodes(false, !this.config.lazy)
|
||||
.filter(node => (valueEquals(node.path, value) || node.value === value));
|
||||
return nodes && nodes.length ? nodes[0] : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
8
packages/cascader/index.js
Normal file
8
packages/cascader/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import Cascader from './src/cascader';
|
||||
|
||||
/* istanbul ignore next */
|
||||
Cascader.install = function(Vue) {
|
||||
Vue.component(Cascader.name, Cascader);
|
||||
};
|
||||
|
||||
export default Cascader;
|
650
packages/cascader/src/cascader.vue
Normal file
650
packages/cascader/src/cascader.vue
Normal file
@@ -0,0 +1,650 @@
|
||||
<template>
|
||||
<div
|
||||
ref="reference"
|
||||
:class="[
|
||||
'el-cascader',
|
||||
realSize && `el-cascader--${realSize}`,
|
||||
{ 'is-disabled': isDisabled }
|
||||
]"
|
||||
v-clickoutside="() => toggleDropDownVisible(false)"
|
||||
@mouseenter="inputHover = true"
|
||||
@mouseleave="inputHover = false"
|
||||
@click="() => toggleDropDownVisible(readonly ? undefined : true)"
|
||||
@keydown="handleKeyDown">
|
||||
|
||||
<el-input
|
||||
ref="input"
|
||||
v-model="multiple ? presentText : inputValue"
|
||||
:size="realSize"
|
||||
:placeholder="placeholder"
|
||||
:readonly="readonly"
|
||||
:disabled="isDisabled"
|
||||
:validate-event="false"
|
||||
:class="{ 'is-focus': dropDownVisible }"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@input="handleInput">
|
||||
<template slot="suffix">
|
||||
<i
|
||||
v-if="clearBtnVisible"
|
||||
key="clear"
|
||||
class="el-input__icon el-icon-circle-close"
|
||||
@click.stop="handleClear"></i>
|
||||
<i
|
||||
v-else
|
||||
key="arrow-down"
|
||||
:class="[
|
||||
'el-input__icon',
|
||||
'el-icon-arrow-down',
|
||||
dropDownVisible && 'is-reverse'
|
||||
]"
|
||||
@click.stop="toggleDropDownVisible()"></i>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<div v-if="multiple" class="el-cascader__tags">
|
||||
<el-tag
|
||||
v-for="(tag, index) in presentTags"
|
||||
:key="tag.key"
|
||||
type="info"
|
||||
:size="tagSize"
|
||||
:hit="tag.hitState"
|
||||
:closable="tag.closable"
|
||||
disable-transitions
|
||||
@close="deleteTag(index)">
|
||||
<span>{{ tag.text }}</span>
|
||||
</el-tag>
|
||||
<input
|
||||
v-if="filterable && !isDisabled"
|
||||
v-model.trim="inputValue"
|
||||
type="text"
|
||||
class="el-cascader__search-input"
|
||||
:placeholder="presentTags.length ? '' : placeholder"
|
||||
@input="e => handleInput(inputValue, e)"
|
||||
@click.stop="toggleDropDownVisible(true)"
|
||||
@keydown.delete="handleDelete">
|
||||
</div>
|
||||
|
||||
<transition name="el-zoom-in-top" @after-leave="handleDropdownLeave">
|
||||
<div
|
||||
v-show="dropDownVisible"
|
||||
ref="popper"
|
||||
:class="['el-popper', 'el-cascader__dropdown', popperClass]">
|
||||
<el-cascader-panel
|
||||
ref="panel"
|
||||
v-show="!filtering"
|
||||
v-model="checkedValue"
|
||||
:options="options"
|
||||
:props="config"
|
||||
:border="false"
|
||||
:render-label="$scopedSlots.default"
|
||||
@expand-change="handleExpandChange"
|
||||
@close="toggleDropDownVisible(false)"></el-cascader-panel>
|
||||
<el-scrollbar
|
||||
ref="suggestionPanel"
|
||||
v-if="filterable"
|
||||
v-show="filtering"
|
||||
tag="ul"
|
||||
class="el-cascader__suggestion-panel"
|
||||
view-class="el-cascader__suggestion-list"
|
||||
@keydown.native="handleSuggestionKeyDown">
|
||||
<template v-if="suggestions.length">
|
||||
<li
|
||||
v-for="(item, index) in suggestions"
|
||||
:key="item.uid"
|
||||
:class="[
|
||||
'el-cascader__suggestion-item',
|
||||
item.checked && 'is-checked'
|
||||
]"
|
||||
:tabindex="-1"
|
||||
@click="handleSuggestionClick(index)">
|
||||
<span>{{ item.text }}</span>
|
||||
<i v-if="item.checked" class="el-icon-check"></i>
|
||||
</li>
|
||||
</template>
|
||||
<slot v-else name="empty">
|
||||
<li class="el-cascader__empty-text">{{ t('el.cascader.noMatch') }}</li>
|
||||
</slot>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
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 Migrating from 'element-ui/src/mixins/migrating';
|
||||
import ElInput from 'element-ui/packages/input';
|
||||
import ElTag from 'element-ui/packages/tag';
|
||||
import ElScrollbar from 'element-ui/packages/scrollbar';
|
||||
import ElCascaderPanel from 'element-ui/packages/cascader-panel';
|
||||
import AriaUtils from 'element-ui/src/utils/aria-utils';
|
||||
import { t } from 'element-ui/src/locale';
|
||||
import { isEqual, isEmpty, kebabCase } from 'element-ui/src/utils/util';
|
||||
import { isUndefined, isFunction } from 'element-ui/src/utils/types';
|
||||
import { isDef } from 'element-ui/src/utils/shared';
|
||||
import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event';
|
||||
import debounce from 'throttle-debounce/debounce';
|
||||
|
||||
const { keys: KeyCode } = AriaUtils;
|
||||
const MigratingProps = {
|
||||
expandTrigger: {
|
||||
newProp: 'expandTrigger',
|
||||
type: String
|
||||
},
|
||||
changeOnSelect: {
|
||||
newProp: 'checkStrictly',
|
||||
type: Boolean
|
||||
},
|
||||
hoverThreshold: {
|
||||
newProp: 'hoverThreshold',
|
||||
type: Number
|
||||
}
|
||||
};
|
||||
|
||||
const PopperMixin = {
|
||||
props: {
|
||||
placement: {
|
||||
type: String,
|
||||
default: 'bottom-start'
|
||||
},
|
||||
appendToBody: Popper.props.appendToBody,
|
||||
visibleArrow: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
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
|
||||
};
|
||||
|
||||
const InputSizeMap = {
|
||||
medium: 36,
|
||||
small: 32,
|
||||
mini: 28
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'ElCascader',
|
||||
|
||||
directives: { Clickoutside },
|
||||
|
||||
mixins: [PopperMixin, Emitter, Locale, Migrating],
|
||||
|
||||
inject: {
|
||||
elForm: {
|
||||
default: ''
|
||||
},
|
||||
elFormItem: {
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
ElInput,
|
||||
ElTag,
|
||||
ElScrollbar,
|
||||
ElCascaderPanel
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {},
|
||||
options: Array,
|
||||
props: Object,
|
||||
size: String,
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: () => t('el.cascader.placeholder')
|
||||
},
|
||||
disabled: Boolean,
|
||||
clearable: Boolean,
|
||||
filterable: Boolean,
|
||||
filterMethod: Function,
|
||||
separator: {
|
||||
type: String,
|
||||
default: ' / '
|
||||
},
|
||||
showAllLevels: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
collapseTags: Boolean,
|
||||
debounce: {
|
||||
type: Number,
|
||||
default: 300
|
||||
},
|
||||
beforeFilter: {
|
||||
type: Function,
|
||||
default: () => (() => {})
|
||||
},
|
||||
popperClass: String
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
dropDownVisible: false,
|
||||
checkedValue: this.value || null,
|
||||
inputHover: false,
|
||||
inputValue: null,
|
||||
presentText: null,
|
||||
presentTags: [],
|
||||
checkedNodes: [],
|
||||
filtering: false,
|
||||
suggestions: [],
|
||||
inputInitialHeight: 0,
|
||||
pressDeleteCount: 0
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
realSize() {
|
||||
const _elFormItemSize = (this.elFormItem || {}).elFormItemSize;
|
||||
return this.size || _elFormItemSize || (this.$ELEMENT || {}).size;
|
||||
},
|
||||
tagSize() {
|
||||
return ['small', 'mini'].indexOf(this.realSize) > -1
|
||||
? 'mini'
|
||||
: 'small';
|
||||
},
|
||||
isDisabled() {
|
||||
return this.disabled || (this.elForm || {}).disabled;
|
||||
},
|
||||
config() {
|
||||
const config = this.props || {};
|
||||
const { $attrs } = this;
|
||||
|
||||
Object
|
||||
.keys(MigratingProps)
|
||||
.forEach(oldProp => {
|
||||
const { newProp, type } = MigratingProps[oldProp];
|
||||
let oldValue = $attrs[oldProp] || $attrs[kebabCase(oldProp)];
|
||||
if (isDef(oldProp) && !isDef(config[newProp])) {
|
||||
if (type === Boolean && oldValue === '') {
|
||||
oldValue = true;
|
||||
}
|
||||
config[newProp] = oldValue;
|
||||
}
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
multiple() {
|
||||
return this.config.multiple;
|
||||
},
|
||||
leafOnly() {
|
||||
return !this.config.checkStrictly;
|
||||
},
|
||||
readonly() {
|
||||
return !this.filterable || this.multiple;
|
||||
},
|
||||
clearBtnVisible() {
|
||||
if (!this.clearable || this.isDisabled || this.filtering || !this.inputHover) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.multiple
|
||||
? !!this.checkedNodes.filter(node => !node.isDisabled).length
|
||||
: !!this.presentText;
|
||||
},
|
||||
panel() {
|
||||
return this.$refs.panel;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
disabled() {
|
||||
this.computePresentContent();
|
||||
},
|
||||
value(val) {
|
||||
if (!isEqual(val, this.checkedValue)) {
|
||||
this.checkedValue = val;
|
||||
this.computePresentContent();
|
||||
}
|
||||
},
|
||||
checkedValue(val) {
|
||||
const { value, dropDownVisible } = this;
|
||||
const { checkStrictly, multiple } = this.config;
|
||||
|
||||
if (!isEqual(val, value) || isUndefined(value)) {
|
||||
this.computePresentContent();
|
||||
// hide dropdown when single mode
|
||||
if (!multiple && !checkStrictly && dropDownVisible) {
|
||||
this.toggleDropDownVisible(false);
|
||||
}
|
||||
|
||||
this.$emit('input', val);
|
||||
this.$emit('change', val);
|
||||
this.dispatch('ElFormItem', 'el.form.change', [val]);
|
||||
}
|
||||
},
|
||||
options: {
|
||||
handler: function() {
|
||||
this.$nextTick(this.computePresentContent);
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
presentText(val) {
|
||||
this.inputValue = val;
|
||||
},
|
||||
presentTags(val, oldVal) {
|
||||
if (this.multiple && (val.length || oldVal.length)) {
|
||||
this.$nextTick(this.updateStyle);
|
||||
}
|
||||
},
|
||||
filtering(val) {
|
||||
this.$nextTick(this.updatePopper);
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const { input } = this.$refs;
|
||||
if (input && input.$el) {
|
||||
this.inputInitialHeight = input.$el.offsetHeight || InputSizeMap[this.realSize] || 40;
|
||||
}
|
||||
|
||||
if (!isEmpty(this.value)) {
|
||||
this.computePresentContent();
|
||||
}
|
||||
|
||||
this.filterHandler = debounce(this.debounce, () => {
|
||||
const { inputValue } = this;
|
||||
|
||||
if (!inputValue) {
|
||||
this.filtering = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const before = this.beforeFilter(inputValue);
|
||||
if (before && before.then) {
|
||||
before.then(this.getSuggestions);
|
||||
} else if (before !== false) {
|
||||
this.getSuggestions();
|
||||
} else {
|
||||
this.filtering = false;
|
||||
}
|
||||
});
|
||||
|
||||
addResizeListener(this.$el, this.updateStyle);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
removeResizeListener(this.$el, this.updateStyle);
|
||||
},
|
||||
|
||||
methods: {
|
||||
getMigratingConfig() {
|
||||
return {
|
||||
props: {
|
||||
'expand-trigger': 'expand-trigger is removed, use `props.expandTrigger` instead.',
|
||||
'change-on-select': 'change-on-select is removed, use `props.checkStrictly` instead.',
|
||||
'hover-threshold': 'hover-threshold is removed, use `props.hoverThreshold` instead'
|
||||
},
|
||||
events: {
|
||||
'active-item-change': 'active-item-change is renamed to expand-change'
|
||||
}
|
||||
};
|
||||
},
|
||||
toggleDropDownVisible(visible) {
|
||||
if (this.isDisabled) return;
|
||||
|
||||
const { dropDownVisible } = this;
|
||||
const { input } = this.$refs;
|
||||
visible = isDef(visible) ? visible : !dropDownVisible;
|
||||
if (visible !== dropDownVisible) {
|
||||
this.dropDownVisible = visible;
|
||||
if (visible) {
|
||||
this.$nextTick(() => {
|
||||
this.updatePopper();
|
||||
this.panel.scrollIntoView();
|
||||
});
|
||||
}
|
||||
input.$refs.input.setAttribute('aria-expanded', visible);
|
||||
this.$emit('visible-change', visible);
|
||||
}
|
||||
},
|
||||
handleDropdownLeave() {
|
||||
this.filtering = false;
|
||||
this.inputValue = this.presentText;
|
||||
},
|
||||
handleKeyDown(event) {
|
||||
switch (event.keyCode) {
|
||||
case KeyCode.enter:
|
||||
this.toggleDropDownVisible();
|
||||
break;
|
||||
case KeyCode.down:
|
||||
this.toggleDropDownVisible(true);
|
||||
this.focusFirstNode();
|
||||
event.preventDefault();
|
||||
break;
|
||||
case KeyCode.esc:
|
||||
case KeyCode.tab:
|
||||
this.toggleDropDownVisible(false);
|
||||
break;
|
||||
}
|
||||
},
|
||||
handleFocus(e) {
|
||||
this.$emit('focus', e);
|
||||
},
|
||||
handleBlur(e) {
|
||||
this.$emit('blur', e);
|
||||
},
|
||||
handleInput(val, event) {
|
||||
!this.dropDownVisible && this.toggleDropDownVisible(true);
|
||||
|
||||
if (event && event.isComposing) return;
|
||||
if (val) {
|
||||
this.filterHandler();
|
||||
} else {
|
||||
this.filtering = false;
|
||||
}
|
||||
},
|
||||
handleClear() {
|
||||
this.presentText = '';
|
||||
this.panel.clearCheckedNodes();
|
||||
},
|
||||
handleExpandChange(value) {
|
||||
this.$nextTick(this.updatePopper.bind(this));
|
||||
this.$emit('expand-change', value);
|
||||
this.$emit('active-item-change', value); // Deprecated
|
||||
},
|
||||
focusFirstNode() {
|
||||
this.$nextTick(() => {
|
||||
const { filtering } = this;
|
||||
const { popper, suggestionPanel } = this.$refs;
|
||||
let firstNode = null;
|
||||
|
||||
if (filtering && suggestionPanel) {
|
||||
firstNode = suggestionPanel.$el.querySelector('.el-cascader__suggestion-item');
|
||||
} else {
|
||||
const firstMenu = popper.querySelector('.el-cascader-menu');
|
||||
firstNode = firstMenu.querySelector('.el-cascader-node[tabindex="-1"]');
|
||||
}
|
||||
|
||||
if (firstNode) {
|
||||
firstNode.focus();
|
||||
!filtering && firstNode.click();
|
||||
}
|
||||
});
|
||||
},
|
||||
computePresentContent() {
|
||||
// nextTick is required, because checked nodes may not change right now
|
||||
this.$nextTick(() => {
|
||||
if (this.config.multiple) {
|
||||
this.computePresentTags();
|
||||
this.presentText = this.presentTags.length ? ' ' : null;
|
||||
} else {
|
||||
this.computePresentText();
|
||||
}
|
||||
});
|
||||
},
|
||||
computePresentText() {
|
||||
const { checkedValue, config } = this;
|
||||
if (!isEmpty(checkedValue)) {
|
||||
const node = this.panel.getNodeByValue(checkedValue);
|
||||
if (node && (config.checkStrictly || node.isLeaf)) {
|
||||
this.presentText = node.getText(this.showAllLevels, this.separator);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.presentText = null;
|
||||
},
|
||||
computePresentTags() {
|
||||
const { isDisabled, leafOnly, showAllLevels, separator, collapseTags } = this;
|
||||
const checkedNodes = this.getCheckedNodes(leafOnly);
|
||||
const tags = [];
|
||||
|
||||
const genTag = node => ({
|
||||
node,
|
||||
key: node.uid,
|
||||
text: node.getText(showAllLevels, separator),
|
||||
hitState: false,
|
||||
closable: !isDisabled && !node.isDisabled
|
||||
});
|
||||
|
||||
if (checkedNodes.length) {
|
||||
const [first, ...rest] = checkedNodes;
|
||||
const restCount = rest.length;
|
||||
tags.push(genTag(first));
|
||||
|
||||
if (restCount) {
|
||||
if (collapseTags) {
|
||||
tags.push({
|
||||
key: -1,
|
||||
text: `+ ${restCount}`,
|
||||
closable: false
|
||||
});
|
||||
} else {
|
||||
rest.forEach(node => tags.push(genTag(node)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.checkedNodes = checkedNodes;
|
||||
this.presentTags = tags;
|
||||
},
|
||||
getSuggestions() {
|
||||
let { filterMethod } = this;
|
||||
|
||||
if (!isFunction(filterMethod)) {
|
||||
filterMethod = (node, keyword) => node.text.includes(keyword);
|
||||
}
|
||||
|
||||
const suggestions = this.panel.getFlattedNodes(this.leafOnly)
|
||||
.filter(node => {
|
||||
if (node.isDisabled) return false;
|
||||
node.text = node.getText(this.showAllLevels, this.separator) || '';
|
||||
return filterMethod(node, this.inputValue);
|
||||
});
|
||||
|
||||
if (this.multiple) {
|
||||
this.presentTags.forEach(tag => {
|
||||
tag.hitState = false;
|
||||
});
|
||||
} else {
|
||||
suggestions.forEach(node => {
|
||||
node.checked = isEqual(this.checkedValue, node.getValueByOption());
|
||||
});
|
||||
}
|
||||
|
||||
this.filtering = true;
|
||||
this.suggestions = suggestions;
|
||||
this.$nextTick(this.updatePopper);
|
||||
},
|
||||
handleSuggestionKeyDown(event) {
|
||||
const { keyCode, target } = event;
|
||||
switch (keyCode) {
|
||||
case KeyCode.enter:
|
||||
target.click();
|
||||
break;
|
||||
case KeyCode.up:
|
||||
const prev = target.previousElementSibling;
|
||||
prev && prev.focus();
|
||||
break;
|
||||
case KeyCode.down:
|
||||
const next = target.nextElementSibling;
|
||||
next && next.focus();
|
||||
break;
|
||||
case KeyCode.esc:
|
||||
case KeyCode.tab:
|
||||
this.toggleDropDownVisible(false);
|
||||
break;
|
||||
}
|
||||
},
|
||||
handleDelete() {
|
||||
const { inputValue, pressDeleteCount, presentTags } = this;
|
||||
const lastIndex = presentTags.length - 1;
|
||||
const lastTag = presentTags[lastIndex];
|
||||
this.pressDeleteCount = inputValue ? 0 : pressDeleteCount + 1;
|
||||
|
||||
if (!lastTag) return;
|
||||
|
||||
if (this.pressDeleteCount) {
|
||||
if (lastTag.hitState) {
|
||||
this.deleteTag(lastIndex);
|
||||
} else {
|
||||
lastTag.hitState = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
handleSuggestionClick(index) {
|
||||
const { multiple } = this;
|
||||
const targetNode = this.suggestions[index];
|
||||
|
||||
if (multiple) {
|
||||
const { checked } = targetNode;
|
||||
targetNode.doCheck(!checked);
|
||||
this.panel.calculateMultiCheckedValue();
|
||||
} else {
|
||||
this.checkedValue = targetNode.getValueByOption();
|
||||
this.toggleDropDownVisible(false);
|
||||
}
|
||||
},
|
||||
deleteTag(index) {
|
||||
const { checkedValue } = this;
|
||||
const val = checkedValue[index];
|
||||
this.checkedValue = checkedValue.filter((n, i) => i !== index);
|
||||
this.$emit('remove-tag', val);
|
||||
},
|
||||
updateStyle() {
|
||||
const { $el, inputInitialHeight } = this;
|
||||
if (this.$isServer || !$el) return;
|
||||
|
||||
const { suggestionPanel } = this.$refs;
|
||||
const inputInner = $el.querySelector('.el-input__inner');
|
||||
|
||||
if (!inputInner) return;
|
||||
|
||||
const tags = $el.querySelector('.el-cascader__tags');
|
||||
let suggestionPanelEl = null;
|
||||
|
||||
if (suggestionPanel && (suggestionPanelEl = suggestionPanel.$el)) {
|
||||
const suggestionList = suggestionPanelEl.querySelector('.el-cascader__suggestion-list');
|
||||
suggestionList.style.minWidth = inputInner.offsetWidth + 'px';
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
const { offsetHeight } = tags;
|
||||
const height = Math.max(offsetHeight + 6, inputInitialHeight) + 'px';
|
||||
inputInner.style.height = height;
|
||||
this.updatePopper();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* public methods
|
||||
*/
|
||||
getCheckedNodes(leafOnly) {
|
||||
return this.panel.getCheckedNodes(leafOnly);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
8
packages/checkbox-button/index.js
Normal file
8
packages/checkbox-button/index.js
Normal 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
packages/checkbox-group/index.js
Normal file
8
packages/checkbox-group/index.js
Normal 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
packages/checkbox/index.js
Normal file
8
packages/checkbox/index.js
Normal 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;
|
199
packages/checkbox/src/checkbox-button.vue
Normal file
199
packages/checkbox/src/checkbox-button.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<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;
|
||||
},
|
||||
|
||||
/* used to make the isDisabled judgment under max/min props */
|
||||
isLimitDisabled() {
|
||||
const { max, min } = this._checkboxGroup;
|
||||
return !!(max || min) &&
|
||||
(this.model.length >= max && !this.isChecked) ||
|
||||
(this.model.length <= min && this.isChecked);
|
||||
},
|
||||
|
||||
isDisabled() {
|
||||
return this._checkboxGroup
|
||||
? this._checkboxGroup.disabled || this.disabled || (this.elForm || {}).disabled || this.isLimitDisabled
|
||||
: 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
packages/checkbox/src/checkbox-group.vue
Normal file
48
packages/checkbox/src/checkbox-group.vue
Normal 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>
|
222
packages/checkbox/src/checkbox.vue
Normal file
222
packages/checkbox/src/checkbox.vue
Normal file
@@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<label
|
||||
class="el-checkbox"
|
||||
:class="[
|
||||
border && checkboxSize ? 'el-checkbox--' + checkboxSize : '',
|
||||
{ 'is-disabled': isDisabled },
|
||||
{ 'is-bordered': border },
|
||||
{ 'is-checked': isChecked }
|
||||
]"
|
||||
:id="id"
|
||||
>
|
||||
<span class="el-checkbox__input"
|
||||
:class="{
|
||||
'is-disabled': isDisabled,
|
||||
'is-checked': isChecked,
|
||||
'is-indeterminate': indeterminate,
|
||||
'is-focus': focus
|
||||
}"
|
||||
:tabindex="indeterminate ? 0 : false"
|
||||
:role="indeterminate ? 'checkbox' : false"
|
||||
:aria-checked="indeterminate ? 'mixed' : false"
|
||||
>
|
||||
<span class="el-checkbox__inner"></span>
|
||||
<input
|
||||
v-if="trueLabel || falseLabel"
|
||||
class="el-checkbox__original"
|
||||
type="checkbox"
|
||||
:aria-hidden="indeterminate ? 'true' : 'false'"
|
||||
: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="indeterminate ? 'true' : 'false'"
|
||||
: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;
|
||||
},
|
||||
|
||||
/* used to make the isDisabled judgment under max/min props */
|
||||
isLimitDisabled() {
|
||||
const { max, min } = this._checkboxGroup;
|
||||
return !!(max || min) &&
|
||||
(this.model.length >= max && !this.isChecked) ||
|
||||
(this.model.length <= min && this.isChecked);
|
||||
},
|
||||
|
||||
isDisabled() {
|
||||
return this.isGroup
|
||||
? this._checkboxGroup.disabled || this.disabled || (this.elForm || {}).disabled || this.isLimitDisabled
|
||||
: 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
packages/col/index.js
Normal file
9
packages/col/index.js
Normal 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
packages/col/src/col.js
Normal file
71
packages/col/src/col.js
Normal 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
packages/collapse-item/index.js
Normal file
8
packages/collapse-item/index.js
Normal 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
packages/collapse/index.js
Normal file
9
packages/collapse/index.js
Normal 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;
|
||||
|
114
packages/collapse/src/collapse-item.vue
Normal file
114
packages/collapse/src/collapse-item.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div class="el-collapse-item"
|
||||
:class="{'is-active': isActive, 'is-disabled': disabled }">
|
||||
<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="disabled ? undefined : 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,
|
||||
id: generateId()
|
||||
};
|
||||
},
|
||||
|
||||
inject: ['collapse'],
|
||||
|
||||
props: {
|
||||
title: String,
|
||||
name: {
|
||||
type: [String, Number],
|
||||
default() {
|
||||
return this._uid;
|
||||
}
|
||||
},
|
||||
disabled: Boolean
|
||||
},
|
||||
|
||||
computed: {
|
||||
isActive() {
|
||||
return this.collapse.activeNames.indexOf(this.name) > -1;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleFocus() {
|
||||
setTimeout(() => {
|
||||
if (!this.isClick) {
|
||||
this.focusing = true;
|
||||
} else {
|
||||
this.isClick = false;
|
||||
}
|
||||
}, 50);
|
||||
},
|
||||
handleHeaderClick() {
|
||||
if (this.disabled) return;
|
||||
this.dispatch('ElCollapse', 'item-click', this);
|
||||
this.focusing = false;
|
||||
this.isClick = true;
|
||||
},
|
||||
handleEnterClick() {
|
||||
this.dispatch('ElCollapse', 'item-click', this);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
73
packages/collapse/src/collapse.vue
Normal file
73
packages/collapse/src/collapse.vue
Normal 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
packages/color-picker/index.js
Normal file
8
packages/color-picker/index.js
Normal 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;
|
317
packages/color-picker/src/color.js
Normal file
317
packages/color-picker/src/color.js
Normal file
@@ -0,0 +1,317 @@
|
||||
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();
|
||||
if (!/^(?:[0-9a-fA-F]{3}){1,2}|[0-9a-fA-F]{8}$/.test(hex)) return;
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
132
packages/color-picker/src/components/alpha-slider.vue
Normal file
132
packages/color-picker/src/components/alpha-slider.vue
Normal file
@@ -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>
|
123
packages/color-picker/src/components/hue-slider.vue
Normal file
123
packages/color-picker/src/components/hue-slider.vue
Normal file
@@ -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>
|
121
packages/color-picker/src/components/picker-dropdown.vue
Normal file
121
packages/color-picker/src/components/picker-dropdown.vue
Normal file
@@ -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>
|
61
packages/color-picker/src/components/predefine.vue
Normal file
61
packages/color-picker/src/components/predefine.vue
Normal file
@@ -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>
|
100
packages/color-picker/src/components/sv-panel.vue
Normal file
100
packages/color-picker/src/components/sv-panel.vue
Normal file
@@ -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
packages/color-picker/src/draggable.js
Normal file
36
packages/color-picker/src/draggable.js
Normal 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
packages/color-picker/src/main.vue
Normal file
188
packages/color-picker/src/main.vue
Normal 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
packages/container/index.js
Normal file
8
packages/container/index.js
Normal 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
packages/container/src/main.vue
Normal file
33
packages/container/src/main.vue
Normal 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
packages/date-picker/index.js
Normal file
8
packages/date-picker/index.js
Normal 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
packages/date-picker/src/basic/date-table.vue
Normal file
441
packages/date-picker/src/basic/date-table.vue
Normal 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 'element-ui/src/utils/date-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: {},
|
||||
|
||||
cellClassName: {},
|
||||
|
||||
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;
|
||||
|
||||
const startDate = this.startDate;
|
||||
const disabledDate = this.disabledDate;
|
||||
const cellClassName = this.cellClassName;
|
||||
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) {
|
||||
const numberOfDaysFromPreviousMonth = day + offset < 0 ? 7 + day + offset : day + offset;
|
||||
|
||||
if (j + i * 7 >= numberOfDaysFromPreviousMonth) {
|
||||
cell.text = count++;
|
||||
} else {
|
||||
cell.text = dateCountOfLastMonth - (numberOfDaysFromPreviousMonth - j % 7) + 1 + i * 7;
|
||||
cell.type = 'prev-month';
|
||||
}
|
||||
} else {
|
||||
if (count <= dateCountOfMonth) {
|
||||
cell.text = count++;
|
||||
} 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());
|
||||
cell.customClass = typeof cellClassName === 'function' && cellClassName(cellDate);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
if (cell.customClass) {
|
||||
classes.push(cell.customClass);
|
||||
}
|
||||
|
||||
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>
|
254
packages/date-picker/src/basic/month-table.vue
Normal file
254
packages/date-picker/src/basic/month-table.vue
Normal file
@@ -0,0 +1,254 @@
|
||||
<template>
|
||||
<table @click="handleMonthTableClick" @mousemove="handleMouseMove" class="el-month-table">
|
||||
<tbody>
|
||||
<tr v-for="(row, key) in rows" :key="key">
|
||||
<td :class="getCellStyle(cell)" v-for="(cell, key) in row" :key="key">
|
||||
<div>
|
||||
<a class="cell">{{ t('el.datepicker.months.' + months[cell.text]) }}</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script type="text/babel">
|
||||
import Locale from 'element-ui/src/mixins/locale';
|
||||
import { isDate, range, getDayCountOfMonth, nextDate } from 'element-ui/src/utils/date-util';
|
||||
import { hasClass } from 'element-ui/src/utils/dom';
|
||||
import { arrayFindIndex, coerceTruthyValueToArray, arrayFind } from 'element-ui/src/utils/util';
|
||||
|
||||
const datesInMonth = (year, month) => {
|
||||
const numOfDays = getDayCountOfMonth(year, month);
|
||||
const firstDay = new Date(year, month, 1);
|
||||
return range(numOfDays).map(n => nextDate(firstDay, n));
|
||||
};
|
||||
|
||||
const clearDate = (date) => {
|
||||
return new Date(date.getFullYear(), date.getMonth());
|
||||
};
|
||||
|
||||
const getMonthTimestamp = function(time) {
|
||||
if (typeof time === 'number' || typeof time === 'string') {
|
||||
return clearDate(new Date(time)).getTime();
|
||||
} else if (time instanceof Date) {
|
||||
return clearDate(time).getTime();
|
||||
} else {
|
||||
return NaN;
|
||||
}
|
||||
};
|
||||
export default {
|
||||
props: {
|
||||
disabledDate: {},
|
||||
value: {},
|
||||
selectionMode: {
|
||||
default: 'month'
|
||||
},
|
||||
minDate: {},
|
||||
|
||||
maxDate: {},
|
||||
defaultValue: {
|
||||
validator(val) {
|
||||
// null or valid Date Object
|
||||
return val === null || isDate(val) || (Array.isArray(val) && val.every(isDate));
|
||||
}
|
||||
},
|
||||
date: {},
|
||||
rangeState: {
|
||||
default() {
|
||||
return {
|
||||
endDate: null,
|
||||
selecting: false
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mixins: [Locale],
|
||||
|
||||
watch: {
|
||||
'rangeState.endDate'(newVal) {
|
||||
this.markRange(this.minDate, newVal);
|
||||
},
|
||||
|
||||
minDate(newVal, oldVal) {
|
||||
if (getMonthTimestamp(newVal) !== getMonthTimestamp(oldVal)) {
|
||||
this.markRange(this.minDate, this.maxDate);
|
||||
}
|
||||
},
|
||||
|
||||
maxDate(newVal, oldVal) {
|
||||
if (getMonthTimestamp(newVal) !== getMonthTimestamp(oldVal)) {
|
||||
this.markRange(this.minDate, this.maxDate);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
months: ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'],
|
||||
tableRows: [ [], [], [] ],
|
||||
lastRow: null,
|
||||
lastColumn: null
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
cellMatchesDate(cell, date) {
|
||||
const value = new Date(date);
|
||||
return this.date.getFullYear() === value.getFullYear() && Number(cell.text) === value.getMonth();
|
||||
},
|
||||
getCellStyle(cell) {
|
||||
const style = {};
|
||||
const year = this.date.getFullYear();
|
||||
const today = new Date();
|
||||
const month = cell.text;
|
||||
const defaultValue = this.defaultValue ? Array.isArray(this.defaultValue) ? this.defaultValue : [this.defaultValue] : [];
|
||||
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 = defaultValue.some(date => this.cellMatchesDate(cell, date));
|
||||
|
||||
if (cell.inRange) {
|
||||
style['in-range'] = true;
|
||||
|
||||
if (cell.start) {
|
||||
style['start-date'] = true;
|
||||
}
|
||||
|
||||
if (cell.end) {
|
||||
style['end-date'] = true;
|
||||
}
|
||||
}
|
||||
return style;
|
||||
},
|
||||
getMonthOfCell(month) {
|
||||
const year = this.date.getFullYear();
|
||||
return new Date(year, month, 1);
|
||||
},
|
||||
markRange(minDate, maxDate) {
|
||||
minDate = getMonthTimestamp(minDate);
|
||||
maxDate = getMonthTimestamp(maxDate) || minDate;
|
||||
[minDate, maxDate] = [Math.min(minDate, maxDate), Math.max(minDate, maxDate)];
|
||||
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++) {
|
||||
|
||||
const cell = row[j];
|
||||
const index = i * 4 + j;
|
||||
const time = new Date(this.date.getFullYear(), index).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 === 'A') {
|
||||
target = target.parentNode.parentNode;
|
||||
}
|
||||
if (target.tagName === 'DIV') {
|
||||
target = target.parentNode;
|
||||
}
|
||||
if (target.tagName !== 'TD') return;
|
||||
|
||||
const row = target.parentNode.rowIndex;
|
||||
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.getMonthOfCell(row * 4 + column)
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
handleMonthTableClick(event) {
|
||||
let target = event.target;
|
||||
if (target.tagName === 'A') {
|
||||
target = target.parentNode.parentNode;
|
||||
}
|
||||
if (target.tagName === 'DIV') {
|
||||
target = target.parentNode;
|
||||
}
|
||||
if (target.tagName !== 'TD') return;
|
||||
if (hasClass(target, 'disabled')) return;
|
||||
const column = target.cellIndex;
|
||||
const row = target.parentNode.rowIndex;
|
||||
const month = row * 4 + column;
|
||||
const newDate = this.getMonthOfCell(month);
|
||||
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 {
|
||||
this.$emit('pick', month);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
rows() {
|
||||
// TODO: refactory rows / getCellClasses
|
||||
const rows = this.tableRows;
|
||||
const disabledDate = this.disabledDate;
|
||||
const selectedDate = [];
|
||||
const now = getMonthTimestamp(new Date());
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const row = rows[i];
|
||||
for (let j = 0; j < 4; j++) {
|
||||
let cell = row[j];
|
||||
if (!cell) {
|
||||
cell = { row: i, column: j, type: 'normal', inRange: false, start: false, end: false };
|
||||
}
|
||||
|
||||
cell.type = 'normal';
|
||||
|
||||
const index = i * 4 + j;
|
||||
const time = new Date(this.date.getFullYear(), index).getTime();
|
||||
cell.inRange = time >= getMonthTimestamp(this.minDate) && time <= getMonthTimestamp(this.maxDate);
|
||||
cell.start = this.minDate && time === getMonthTimestamp(this.minDate);
|
||||
cell.end = this.maxDate && time === getMonthTimestamp(this.maxDate);
|
||||
const isToday = time === now;
|
||||
|
||||
if (isToday) {
|
||||
cell.type = 'today';
|
||||
}
|
||||
cell.text = index;
|
||||
let cellDate = new Date(time);
|
||||
cell.disabled = typeof disabledDate === 'function' && disabledDate(cellDate);
|
||||
cell.selected = arrayFind(selectedDate, date => date.getTime() === cellDate.getTime());
|
||||
|
||||
this.$set(row, j, cell);
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
304
packages/date-picker/src/basic/time-spinner.vue
Normal file
304
packages/date-picker/src/basic/time-spinner.vue
Normal file
@@ -0,0 +1,304 @@
|
||||
<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 'element-ui/src/utils/date-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.round((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);
|
||||
this.$nextTick(() => this.emitSelectRange(this.currentScrollbar));
|
||||
},
|
||||
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
packages/date-picker/src/basic/year-table.vue
Normal file
101
packages/date-picker/src/basic/year-table.vue
Normal 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 'element-ui/src/utils/date-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>
|
680
packages/date-picker/src/panel/date-range.vue
Normal file
680
packages/date-picker/src/panel/date-range.vue
Normal file
@@ -0,0 +1,680 @@
|
||||
<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"
|
||||
:cell-class-name="cellClassName"
|
||||
@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"
|
||||
:cell-class-name="cellClassName"
|
||||
@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,
|
||||
nextDate,
|
||||
extractDateFormat,
|
||||
extractTimeFormat
|
||||
} from 'element-ui/src/utils/date-util';
|
||||
import Clickoutside from 'element-ui/src/utils/clickoutside';
|
||||
import Locale from 'element-ui/src/mixins/locale';
|
||||
import TimePicker from './time';
|
||||
import DateTable from '../basic/date-table';
|
||||
import ElInput from 'element-ui/packages/input';
|
||||
import ElButton from 'element-ui/packages/button';
|
||||
|
||||
const calcDefaultValue = (defaultValue) => {
|
||||
if (Array.isArray(defaultValue)) {
|
||||
return [new Date(defaultValue[0]), new Date(defaultValue[1])];
|
||||
} else if (defaultValue) {
|
||||
return [new Date(defaultValue), nextDate(new Date(defaultValue), 1)];
|
||||
} else {
|
||||
return [new Date(), nextDate(new Date(), 1)];
|
||||
}
|
||||
};
|
||||
|
||||
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: '',
|
||||
cellClassName: '',
|
||||
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
|
||||
if (this.minDate && this.maxDate == null) this.rangeState.selecting = false;
|
||||
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>
|
597
packages/date-picker/src/panel/date.vue
Normal file
597
packages/date-picker/src/panel/date.vue
Normal file
@@ -0,0 +1,597 @@
|
||||
<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"
|
||||
:cell-class-name="cellClassName"
|
||||
: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 'element-ui/src/utils/date-util';
|
||||
import Clickoutside from 'element-ui/src/utils/clickoutside';
|
||||
import Locale from 'element-ui/src/mixins/locale';
|
||||
import ElInput from 'element-ui/packages/input';
|
||||
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: '',
|
||||
cellClassName: '',
|
||||
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>
|
289
packages/date-picker/src/panel/month-range.vue
Normal file
289
packages/date-picker/src/panel/month-range.vue
Normal file
@@ -0,0 +1,289 @@
|
||||
<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
|
||||
}, 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-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"
|
||||
v-if="unlinkPanels"
|
||||
@click="leftNextYear"
|
||||
:disabled="!enableYearArrow"
|
||||
:class="{ 'is-disabled': !enableYearArrow }"
|
||||
class="el-picker-panel__icon-btn el-icon-d-arrow-right"></button>
|
||||
<div>{{ leftLabel }}</div>
|
||||
</div>
|
||||
<month-table
|
||||
selection-mode="range"
|
||||
:date="leftDate"
|
||||
:default-value="defaultValue"
|
||||
:min-date="minDate"
|
||||
:max-date="maxDate"
|
||||
:range-state="rangeState"
|
||||
:disabled-date="disabledDate"
|
||||
@changerange="handleChangeRange"
|
||||
@pick="handleRangePick">
|
||||
</month-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"
|
||||
v-if="unlinkPanels"
|
||||
@click="rightPrevYear"
|
||||
:disabled="!enableYearArrow"
|
||||
:class="{ 'is-disabled': !enableYearArrow }"
|
||||
class="el-picker-panel__icon-btn el-icon-d-arrow-left"></button>
|
||||
<button
|
||||
type="button"
|
||||
@click="rightNextYear"
|
||||
class="el-picker-panel__icon-btn el-icon-d-arrow-right"></button>
|
||||
<div>{{ rightLabel }}</div>
|
||||
</div>
|
||||
<month-table
|
||||
selection-mode="range"
|
||||
:date="rightDate"
|
||||
:default-value="defaultValue"
|
||||
:min-date="minDate"
|
||||
:max-date="maxDate"
|
||||
:range-state="rangeState"
|
||||
:disabled-date="disabledDate"
|
||||
@changerange="handleChangeRange"
|
||||
@pick="handleRangePick">
|
||||
</month-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script type="text/babel">
|
||||
import {
|
||||
isDate,
|
||||
modifyWithTimeString,
|
||||
prevYear,
|
||||
nextYear,
|
||||
nextMonth
|
||||
} from 'element-ui/src/utils/date-util';
|
||||
import Clickoutside from 'element-ui/src/utils/clickoutside';
|
||||
import Locale from 'element-ui/src/mixins/locale';
|
||||
import MonthTable from '../basic/month-table';
|
||||
import ElInput from 'element-ui/packages/input';
|
||||
import ElButton from 'element-ui/packages/button';
|
||||
|
||||
const calcDefaultValue = (defaultValue) => {
|
||||
if (Array.isArray(defaultValue)) {
|
||||
return [new Date(defaultValue[0]), new Date(defaultValue[1])];
|
||||
} else if (defaultValue) {
|
||||
return [new Date(defaultValue), nextMonth(new Date(defaultValue))];
|
||||
} else {
|
||||
return [new Date(), nextMonth(new Date())];
|
||||
}
|
||||
};
|
||||
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');
|
||||
},
|
||||
|
||||
rightLabel() {
|
||||
return this.rightDate.getFullYear() + ' ' + this.t('el.datepicker.year');
|
||||
},
|
||||
|
||||
leftYear() {
|
||||
return this.leftDate.getFullYear();
|
||||
},
|
||||
|
||||
rightYear() {
|
||||
return this.rightDate.getFullYear() === this.leftDate.getFullYear() ? this.leftDate.getFullYear() + 1 : this.rightDate.getFullYear();
|
||||
},
|
||||
|
||||
enableYearArrow() {
|
||||
return this.unlinkPanels && this.rightYear > this.leftYear + 1;
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
popperClass: '',
|
||||
value: [],
|
||||
defaultValue: null,
|
||||
defaultTime: null,
|
||||
minDate: '',
|
||||
maxDate: '',
|
||||
leftDate: new Date(),
|
||||
rightDate: nextYear(new Date()),
|
||||
rangeState: {
|
||||
endDate: null,
|
||||
selecting: false,
|
||||
row: null,
|
||||
column: null
|
||||
},
|
||||
shortcuts: '',
|
||||
visible: '',
|
||||
disabledDate: '',
|
||||
format: '',
|
||||
arrowControl: false,
|
||||
unlinkPanels: false
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
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 maxDateYear = this.maxDate.getFullYear();
|
||||
this.rightDate = minDateYear === maxDateYear
|
||||
? nextYear(this.maxDate)
|
||||
: this.maxDate;
|
||||
} else {
|
||||
this.rightDate = nextYear(this.leftDate);
|
||||
}
|
||||
} else {
|
||||
this.leftDate = calcDefaultValue(this.defaultValue)[0];
|
||||
this.rightDate = nextYear(this.leftDate);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
defaultValue(val) {
|
||||
if (!Array.isArray(this.value)) {
|
||||
const [left, right] = calcDefaultValue(val);
|
||||
this.leftDate = left;
|
||||
this.rightDate = val && val[1] && left.getFullYear() !== right.getFullYear() && this.unlinkPanels
|
||||
? right
|
||||
: nextYear(this.leftDate);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleClear() {
|
||||
this.minDate = null;
|
||||
this.maxDate = null;
|
||||
this.leftDate = calcDefaultValue(this.defaultValue)[0];
|
||||
this.rightDate = nextYear(this.leftDate);
|
||||
this.$emit('pick', null);
|
||||
},
|
||||
|
||||
handleChangeRange(val) {
|
||||
this.minDate = val.minDate;
|
||||
this.maxDate = val.maxDate;
|
||||
this.rangeState = val.rangeState;
|
||||
},
|
||||
|
||||
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) return;
|
||||
this.handleConfirm();
|
||||
},
|
||||
|
||||
handleShortcutClick(shortcut) {
|
||||
if (shortcut.onClick) {
|
||||
shortcut.onClick(this);
|
||||
}
|
||||
},
|
||||
|
||||
// leftPrev*, rightNext* need to take care of `unlinkPanels`
|
||||
leftPrevYear() {
|
||||
this.leftDate = prevYear(this.leftDate);
|
||||
if (!this.unlinkPanels) {
|
||||
this.rightDate = prevYear(this.rightDate);
|
||||
}
|
||||
},
|
||||
|
||||
rightNextYear() {
|
||||
if (!this.unlinkPanels) {
|
||||
this.leftDate = nextYear(this.leftDate);
|
||||
}
|
||||
this.rightDate = nextYear(this.rightDate);
|
||||
},
|
||||
|
||||
// leftNext*, rightPrev* are called when `unlinkPanels` is true
|
||||
leftNextYear() {
|
||||
this.leftDate = nextYear(this.leftDate);
|
||||
},
|
||||
|
||||
rightPrevYear() {
|
||||
this.rightDate = prevYear(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: { MonthTable, ElInput, ElButton }
|
||||
};
|
||||
</script>
|
248
packages/date-picker/src/panel/time-range.vue
Normal file
248
packages/date-picker/src/panel/time-range.vue
Normal 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 'element-ui/src/utils/date-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
packages/date-picker/src/panel/time-select.vue
Normal file
178
packages/date-picker/src/panel/time-select.vue
Normal 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
packages/date-picker/src/panel/time.vue
Normal file
186
packages/date-picker/src/panel/time.vue
Normal 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 'element-ui/src/utils/date-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
packages/date-picker/src/picker.vue
Normal file
931
packages/date-picker/src/picker.vue
Normal file
@@ -0,0 +1,931 @@
|
||||
<template>
|
||||
<el-input
|
||||
class="el-date-editor"
|
||||
:class="'el-date-editor--' + type"
|
||||
:readonly="!editable || readonly || type === 'dates' || type === 'week'"
|
||||
: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 'element-ui/src/utils/date-util';
|
||||
import Popper from 'element-ui/src/utils/vue-popper';
|
||||
import Emitter from 'element-ui/src/mixins/emitter';
|
||||
import ElInput from 'element-ui/packages/input';
|
||||
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',
|
||||
monthrange: 'yyyy-MM',
|
||||
datetimerange: 'yyyy-MM-dd HH:mm:ss',
|
||||
year: 'yyyy'
|
||||
};
|
||||
const HAVE_TRIGGER_TYPES = [
|
||||
'date',
|
||||
'datetime',
|
||||
'time',
|
||||
'time-select',
|
||||
'week',
|
||||
'month',
|
||||
'year',
|
||||
'daterange',
|
||||
'monthrange',
|
||||
'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, format) {
|
||||
// parse as if a normal date
|
||||
return TYPE_VALUE_RESOLVER_MAP.date.parser(text, format);
|
||||
}
|
||||
},
|
||||
date: {
|
||||
formatter: DATE_FORMATTER,
|
||||
parser: DATE_PARSER
|
||||
},
|
||||
datetime: {
|
||||
formatter: DATE_FORMATTER,
|
||||
parser: DATE_PARSER
|
||||
},
|
||||
daterange: {
|
||||
formatter: RANGE_FORMATTER,
|
||||
parser: RANGE_PARSER
|
||||
},
|
||||
monthrange: {
|
||||
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: {
|
||||
default: ['00:00:00.000', '23:59:59.999']
|
||||
},
|
||||
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>
|
43
packages/date-picker/src/picker/date-picker.js
Normal file
43
packages/date-picker/src/picker/date-picker.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import Picker from '../picker';
|
||||
import DatePanel from '../panel/date';
|
||||
import DateRangePanel from '../panel/date-range';
|
||||
import MonthRangePanel from '../panel/month-range';
|
||||
|
||||
const getPanel = function(type) {
|
||||
if (type === 'daterange' || type === 'datetimerange') {
|
||||
return DateRangePanel;
|
||||
} else if (type === 'monthrange') {
|
||||
return MonthRangePanel;
|
||||
}
|
||||
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
packages/date-picker/src/picker/time-picker.js
Normal file
39
packages/date-picker/src/picker/time-picker.js
Normal 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
packages/date-picker/src/picker/time-select.js
Normal file
21
packages/date-picker/src/picker/time-select.js
Normal 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;
|
||||
}
|
||||
};
|
8
packages/dialog/index.js
Normal file
8
packages/dialog/index.js
Normal 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;
|
262
packages/dialog/src/component.vue
Normal file
262
packages/dialog/src/component.vue
Normal file
@@ -0,0 +1,262 @@
|
||||
<template>
|
||||
<transition
|
||||
name="dialog-fade"
|
||||
@after-enter="afterEnter"
|
||||
@after-leave="afterLeave"
|
||||
>
|
||||
<div
|
||||
v-show="visible"
|
||||
class="el-dialog__wrapper"
|
||||
@click.self="handleWrapperClick"
|
||||
>
|
||||
<div
|
||||
role="dialog"
|
||||
:key="key"
|
||||
aria-modal="true"
|
||||
:aria-label="title || 'dialog'"
|
||||
:class="[
|
||||
'el-dialog',
|
||||
{ '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>
|
||||
<button
|
||||
type="button"
|
||||
class="el-dialog__headerbtn size"
|
||||
aria-label="Maximize"
|
||||
key="maximize"
|
||||
v-if="allowMaximize && !maximize"
|
||||
@click="maximize = true"
|
||||
>
|
||||
<i class="el-dialog__close el-icon el-dialog__maximize"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="el-dialog__headerbtn size"
|
||||
aria-label="Minimize"
|
||||
key="minimize"
|
||||
v-if="allowMaximize && maximize"
|
||||
@click="maximize = false"
|
||||
>
|
||||
<i class="el-dialog__close el-icon el-dialog__minimize"></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: false
|
||||
},
|
||||
|
||||
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
|
||||
},
|
||||
|
||||
destroyOnClose: Boolean,
|
||||
|
||||
allowMaximize: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
inject: ['dialogMaximizeSizeWrapper'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
closed: false,
|
||||
maximize: false,
|
||||
key: 0
|
||||
};
|
||||
},
|
||||
|
||||
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');
|
||||
if (this.destroyOnClose) {
|
||||
this.$nextTick(() => {
|
||||
this.key++;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
style() {
|
||||
let style = {};
|
||||
if (!this.fullscreen) {
|
||||
style.marginTop = this.top;
|
||||
if (this.width) {
|
||||
style.width = this.width;
|
||||
}
|
||||
}
|
||||
if (this.maximize) {
|
||||
style.marginTop = this.dialogMaximizeSize.top + 'px';
|
||||
style.marginBottom = this.dialogMaximizeSize.bottom + 'px';
|
||||
style.marginLeft = this.dialogMaximizeSize.left + 'px';
|
||||
style.marginRight = this.dialogMaximizeSize.right + 'px';
|
||||
style.height = `calc(100vh - ${this.dialogMaximizeSize.top +
|
||||
this.dialogMaximizeSize.bottom}px)`;
|
||||
style.width = 'auto';
|
||||
}
|
||||
return style;
|
||||
},
|
||||
dialogMaximizeSize() {
|
||||
return this.dialogMaximizeSizeWrapper
|
||||
? this.dialogMaximizeSizeWrapper.size
|
||||
: { top: 80, left: 180, bottom: 0, right: 0 };
|
||||
}
|
||||
},
|
||||
|
||||
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
packages/divider/index.js
Normal file
8
packages/divider/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import Divider from './src/main';
|
||||
|
||||
/* istanbul ignore next */
|
||||
Divider.install = function(Vue) {
|
||||
Vue.component(Divider.name, Divider);
|
||||
};
|
||||
|
||||
export default Divider;
|
37
packages/divider/src/main.vue
Normal file
37
packages/divider/src/main.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template functional>
|
||||
<div
|
||||
v-bind="data.attrs"
|
||||
v-on="listeners"
|
||||
:class="[data.staticClass, 'el-divider', `el-divider--${props.direction}`]"
|
||||
>
|
||||
<div
|
||||
v-if="slots().default && props.direction !== 'vertical'"
|
||||
:class="['el-divider__text', `is-${props.contentPosition}`]"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ElDivider',
|
||||
props: {
|
||||
direction: {
|
||||
type: String,
|
||||
default: 'horizontal',
|
||||
validator(val) {
|
||||
return ['horizontal', 'vertical'].indexOf(val) !== -1;
|
||||
}
|
||||
},
|
||||
contentPosition: {
|
||||
type: String,
|
||||
default: 'center',
|
||||
validator(val) {
|
||||
return ['left', 'center', 'right'].indexOf(val) !== -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
8
packages/drawer/index.js
Normal file
8
packages/drawer/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import Drawer from './src/main';
|
||||
|
||||
/* istanbul ignore next */
|
||||
Drawer.install = function(Vue) {
|
||||
Vue.component(Drawer.name, Drawer);
|
||||
};
|
||||
|
||||
export default Drawer;
|
197
packages/drawer/src/main.vue
Normal file
197
packages/drawer/src/main.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<transition
|
||||
name="el-drawer-fade"
|
||||
@after-enter="afterEnter"
|
||||
@after-leave="afterLeave">
|
||||
<div
|
||||
class="el-drawer__wrapper"
|
||||
tabindex="-1"
|
||||
v-show="visible">
|
||||
<div
|
||||
class="el-drawer__container"
|
||||
:class="visible && 'el-drawer__open'"
|
||||
@click.self="handleWrapperClick"
|
||||
role="document"
|
||||
tabindex="-1">
|
||||
<div
|
||||
aria-modal="true"
|
||||
aria-labelledby="el-drawer__title"
|
||||
:aria-label="title"
|
||||
class="el-drawer"
|
||||
:class="[direction, customClass]"
|
||||
:style="isHorizontal ? `width: ${drawerSize}` : `height: ${drawerSize}`"
|
||||
ref="drawer"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<header class="el-drawer__header" id="el-drawer__title" v-if="withHeader">
|
||||
<slot name="title">
|
||||
<span role="heading" :title="title">{{ title }}</span>
|
||||
</slot>
|
||||
<button
|
||||
:aria-label="`close ${title || 'drawer'}`"
|
||||
class="el-drawer__close-btn"
|
||||
type="button"
|
||||
v-if="showClose"
|
||||
@click="closeDrawer">
|
||||
<i class="el-dialog__close el-icon el-icon-close"></i>
|
||||
</button>
|
||||
</header>
|
||||
<section class="el-drawer__body" v-if="rendered">
|
||||
<slot></slot>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Popup from 'element-ui/src/utils/popup';
|
||||
import emitter from 'element-ui/src/mixins/emitter';
|
||||
|
||||
export default {
|
||||
name: 'ElDrawer',
|
||||
mixins: [Popup, emitter],
|
||||
props: {
|
||||
appendToBody: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
beforeClose: {
|
||||
type: Function
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
closeOnPressEscape: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
destroyOnClose: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
modal: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: 'rtl',
|
||||
validator(val) {
|
||||
return ['ltr', 'rtl', 'ttb', 'btt'].indexOf(val) !== -1;
|
||||
}
|
||||
},
|
||||
modalAppendToBody: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showClose: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
size: {
|
||||
type: [Number, String],
|
||||
default: '30%'
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
visible: {
|
||||
type: Boolean
|
||||
},
|
||||
wrapperClosable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
withHeader: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isHorizontal() {
|
||||
return this.direction === 'rtl' || this.direction === 'ltr';
|
||||
},
|
||||
drawerSize() {
|
||||
return typeof this.size === 'number' ? `${this.size}px` : this.size;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
closed: false,
|
||||
prevActiveElement: null
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
if (val) {
|
||||
this.closed = false;
|
||||
this.$emit('open');
|
||||
if (this.appendToBody) {
|
||||
document.body.appendChild(this.$el);
|
||||
}
|
||||
this.prevActiveElement = document.activeElement;
|
||||
} else {
|
||||
if (!this.closed) this.$emit('close');
|
||||
this.$nextTick(() => {
|
||||
if (this.prevActiveElement) {
|
||||
this.prevActiveElement.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
afterEnter() {
|
||||
this.$emit('opened');
|
||||
},
|
||||
afterLeave() {
|
||||
this.$emit('closed');
|
||||
},
|
||||
hide(cancel) {
|
||||
if (cancel !== false) {
|
||||
this.$emit('update:visible', false);
|
||||
this.$emit('close');
|
||||
if (this.destroyOnClose === true) {
|
||||
this.rendered = false;
|
||||
}
|
||||
this.closed = true;
|
||||
}
|
||||
},
|
||||
handleWrapperClick() {
|
||||
if (this.wrapperClosable) {
|
||||
this.closeDrawer();
|
||||
}
|
||||
},
|
||||
closeDrawer() {
|
||||
if (typeof this.beforeClose === 'function') {
|
||||
this.beforeClose(this.hide);
|
||||
} else {
|
||||
this.hide();
|
||||
}
|
||||
},
|
||||
handleClose() {
|
||||
// This method here will be called by PopupManger, when the `closeOnPressEscape` was set to true
|
||||
// pressing `ESC` will call this method, and also close the drawer.
|
||||
// This method also calls `beforeClose` if there was one.
|
||||
this.closeDrawer();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.visible) {
|
||||
this.rendered = true;
|
||||
this.open();
|
||||
}
|
||||
},
|
||||
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
packages/dropdown-item/index.js
Normal file
8
packages/dropdown-item/index.js
Normal 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
packages/dropdown-menu/index.js
Normal file
8
packages/dropdown-menu/index.js
Normal 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
packages/dropdown/index.js
Normal file
8
packages/dropdown/index.js
Normal 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
packages/dropdown/src/dropdown-item.vue
Normal file
37
packages/dropdown/src/dropdown-item.vue
Normal 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>
|
63
packages/dropdown/src/dropdown-menu.vue
Normal file
63
packages/dropdown/src/dropdown-menu.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<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.dropdown.popperElm = this.popperElm = this.$el;
|
||||
this.referenceElm = this.dropdown.$el;
|
||||
// compatible with 2.6 new v-slot syntax
|
||||
// issue link https://github.com/ElemeFE/element/issues/14345
|
||||
this.dropdown.initDomOperation();
|
||||
},
|
||||
|
||||
watch: {
|
||||
'dropdown.placement': {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
this.currentPlacement = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
279
packages/dropdown/src/dropdown.vue
Normal file
279
packages/dropdown/src/dropdown.vue
Normal file
@@ -0,0 +1,279 @@
|
||||
<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
|
||||
},
|
||||
tabindex: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
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();
|
||||
if (this.tabindex >= 0) {
|
||||
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();
|
||||
}
|
||||
},
|
||||
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.triggerElmFocus();
|
||||
target.click();
|
||||
if (this.hideOnClick) { // click关闭
|
||||
this.visible = false;
|
||||
}
|
||||
} else if ([9, 27].indexOf(keyCode) > -1) { // tab // esc
|
||||
this.hide();
|
||||
this.triggerElmFocus();
|
||||
}
|
||||
},
|
||||
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);
|
||||
|
||||
if (!this.splitButton) { // 自定义
|
||||
this.triggerElm.setAttribute('role', 'button');
|
||||
this.triggerElm.setAttribute('tabindex', this.tabindex);
|
||||
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.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);
|
||||
},
|
||||
triggerElmFocus() {
|
||||
this.triggerElm.focus && this.triggerElm.focus();
|
||||
},
|
||||
initDomOperation() {
|
||||
this.dropdownElm = this.popperElm;
|
||||
this.menuItems = this.dropdownElm.querySelectorAll("[tabindex='-1']");
|
||||
this.menuItemsArray = [].slice.call(this.menuItems);
|
||||
|
||||
this.initEvent();
|
||||
this.initAria();
|
||||
}
|
||||
},
|
||||
|
||||
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
packages/footer/index.js
Normal file
8
packages/footer/index.js
Normal 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
packages/footer/src/main.vue
Normal file
20
packages/footer/src/main.vue
Normal 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
packages/form-item/index.js
Normal file
8
packages/form-item/index.js
Normal 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
packages/form/index.js
Normal file
8
packages/form/index.js
Normal 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;
|
319
packages/form/src/form-item.vue
Normal file
319
packages/form/src/form-item.vue
Normal file
@@ -0,0 +1,319 @@
|
||||
<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-wrap
|
||||
:is-auto-width="labelStyle && labelStyle.width === 'auto'"
|
||||
:update-all="form.labelWidth === 'auto'">
|
||||
<label :for="labelFor" class="el-form-item__label" :style="labelStyle" v-if="label || $slots.label">
|
||||
<slot name="label">{{label + form.labelSuffix}}</slot>
|
||||
</label>
|
||||
</label-wrap>
|
||||
<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';
|
||||
import LabelWrap from './label-wrap';
|
||||
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
|
||||
},
|
||||
components: {
|
||||
// use this component to calculate auto width
|
||||
LabelWrap
|
||||
},
|
||||
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 === 'auto') {
|
||||
if (this.labelWidth === 'auto') {
|
||||
ret.marginLeft = this.computedLabelWidth;
|
||||
} else if (this.form.labelWidth === 'auto') {
|
||||
ret.marginLeft = this.elForm.autoLabelWidth;
|
||||
}
|
||||
} else {
|
||||
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,
|
||||
computedLabelWidth: ''
|
||||
};
|
||||
},
|
||||
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;
|
||||
}
|
||||
|
||||
// reset validateDisabled after onFieldChange triggered
|
||||
this.$nextTick(() => {
|
||||
this.validateDisabled = false;
|
||||
});
|
||||
|
||||
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');
|
||||
},
|
||||
updateComputedLabelWidth(width) {
|
||||
this.computedLabelWidth = width ? `${width}px` : '';
|
||||
},
|
||||
addValidateEvents() {
|
||||
const rules = this.getRules();
|
||||
|
||||
if (rules.length || this.required !== undefined) {
|
||||
this.$on('el.form.blur', this.onFieldBlur);
|
||||
this.$on('el.form.change', this.onFieldChange);
|
||||
}
|
||||
},
|
||||
removeValidateEvents() {
|
||||
this.$off();
|
||||
}
|
||||
},
|
||||
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
|
||||
});
|
||||
|
||||
this.addValidateEvents();
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.dispatch('ElForm', 'el.form.removeField', [this]);
|
||||
}
|
||||
};
|
||||
</script>
|
182
packages/form/src/form.vue
Normal file
182
packages/form/src/form.vue
Normal file
@@ -0,0 +1,182 @@
|
||||
<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() {
|
||||
// remove then add event listeners on form-item after form rules change
|
||||
this.fields.forEach(field => {
|
||||
field.removeValidateEvents();
|
||||
field.addValidateEvents();
|
||||
});
|
||||
|
||||
if (this.validateOnRuleChange) {
|
||||
this.validate(() => {});
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
autoLabelWidth() {
|
||||
if (!this.potentialLabelWidthArr.length) return 0;
|
||||
const max = Math.max(...this.potentialLabelWidthArr);
|
||||
return max ? `${max}px` : '';
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fields: [],
|
||||
potentialLabelWidthArr: [] // use this array to calculate auto width
|
||||
};
|
||||
},
|
||||
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);
|
||||
});
|
||||
},
|
||||
getLabelWidthIndex(width) {
|
||||
const index = this.potentialLabelWidthArr.indexOf(width);
|
||||
// it's impossible
|
||||
if (index === -1) {
|
||||
throw new Error('[ElementForm]unpected width ', width);
|
||||
}
|
||||
return index;
|
||||
},
|
||||
registerLabelWidth(val, oldVal) {
|
||||
if (val && oldVal) {
|
||||
const index = this.getLabelWidthIndex(oldVal);
|
||||
this.potentialLabelWidthArr.splice(index, 1, val);
|
||||
} else if (val) {
|
||||
this.potentialLabelWidthArr.push(val);
|
||||
}
|
||||
},
|
||||
deregisterLabelWidth(val) {
|
||||
const index = this.getLabelWidthIndex(val);
|
||||
this.potentialLabelWidthArr.splice(index, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
78
packages/form/src/label-wrap.vue
Normal file
78
packages/form/src/label-wrap.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
isAutoWidth: Boolean,
|
||||
updateAll: Boolean
|
||||
},
|
||||
|
||||
inject: ['elForm', 'elFormItem'],
|
||||
|
||||
render() {
|
||||
const slots = this.$slots.default;
|
||||
if (!slots) return null;
|
||||
if (this.isAutoWidth) {
|
||||
const autoLabelWidth = this.elForm.autoLabelWidth;
|
||||
const style = {};
|
||||
if (autoLabelWidth && autoLabelWidth !== 'auto') {
|
||||
const marginLeft = parseInt(autoLabelWidth, 10) - this.computedWidth;
|
||||
if (marginLeft) {
|
||||
style.marginLeft = marginLeft + 'px';
|
||||
}
|
||||
}
|
||||
return (<div class="el-form-item__label-wrap" style={style}>
|
||||
{ slots }
|
||||
</div>);
|
||||
} else {
|
||||
return slots[0];
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getLabelWidth() {
|
||||
if (this.$el && this.$el.firstElementChild) {
|
||||
const computedWidth = window.getComputedStyle(this.$el.firstElementChild).width;
|
||||
return Math.ceil(parseFloat(computedWidth));
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
updateLabelWidth(action = 'update') {
|
||||
if (this.$slots.default && this.isAutoWidth && this.$el.firstElementChild) {
|
||||
if (action === 'update') {
|
||||
this.computedWidth = this.getLabelWidth();
|
||||
} else if (action === 'remove') {
|
||||
this.elForm.deregisterLabelWidth(this.computedWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
computedWidth(val, oldVal) {
|
||||
if (this.updateAll) {
|
||||
this.elForm.registerLabelWidth(val, oldVal);
|
||||
this.elFormItem.updateComputedLabelWidth(val);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
computedWidth: 0
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.updateLabelWidth('update');
|
||||
},
|
||||
|
||||
updated() {
|
||||
this.updateLabelWidth('update');
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.updateLabelWidth('remove');
|
||||
}
|
||||
};
|
||||
</script>
|
8
packages/header/index.js
Normal file
8
packages/header/index.js
Normal 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
packages/header/src/main.vue
Normal file
20
packages/header/src/main.vue
Normal 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
packages/icon/index.js
Normal file
8
packages/icon/index.js
Normal 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
packages/icon/src/icon.vue
Normal file
13
packages/icon/src/icon.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<i :class="'el-icon-' + name"></i>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ElIcon',
|
||||
|
||||
props: {
|
||||
name: String
|
||||
}
|
||||
};
|
||||
</script>
|
8
packages/image/index.js
Normal file
8
packages/image/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import Image from './src/main';
|
||||
|
||||
/* istanbul ignore next */
|
||||
Image.install = function(Vue) {
|
||||
Vue.component(Image.name, Image);
|
||||
};
|
||||
|
||||
export default Image;
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user