update the master branch to the latest
This commit is contained in:
parent
67d534df27
commit
23a0ba9831
611 changed files with 122648 additions and 0 deletions
144
src/components/affix/affix.vue
Normal file
144
src/components/affix/affix.vue
Normal file
|
@ -0,0 +1,144 @@
|
|||
<template>
|
||||
<div>
|
||||
<div ref="point" :class="classes" :style="styles">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div v-show="slot" :style="slotStyle"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { on, off } from '../../utils/dom';
|
||||
const prefixCls = 'ivu-affix';
|
||||
|
||||
function getScroll(target, top) {
|
||||
const prop = top ? 'pageYOffset' : 'pageXOffset';
|
||||
const method = top ? 'scrollTop' : 'scrollLeft';
|
||||
|
||||
let ret = target[prop];
|
||||
|
||||
if (typeof ret !== 'number') {
|
||||
ret = window.document.documentElement[method];
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function getOffset(element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
const scrollTop = getScroll(window, true);
|
||||
const scrollLeft = getScroll(window);
|
||||
|
||||
const docEl = window.document.body;
|
||||
const clientTop = docEl.clientTop || 0;
|
||||
const clientLeft = docEl.clientLeft || 0;
|
||||
|
||||
return {
|
||||
top: rect.top + scrollTop - clientTop,
|
||||
left: rect.left + scrollLeft - clientLeft
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'Affix',
|
||||
props: {
|
||||
offsetTop: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
offsetBottom: {
|
||||
type: Number
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
affix: false,
|
||||
styles: {},
|
||||
slot: false,
|
||||
slotStyle: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
offsetType () {
|
||||
let type = 'top';
|
||||
if (this.offsetBottom >= 0) {
|
||||
type = 'bottom';
|
||||
}
|
||||
|
||||
return type;
|
||||
},
|
||||
classes () {
|
||||
return [
|
||||
{
|
||||
[`${prefixCls}`]: this.affix
|
||||
}
|
||||
];
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
// window.addEventListener('scroll', this.handleScroll, false);
|
||||
// window.addEventListener('resize', this.handleScroll, false);
|
||||
on(window, 'scroll', this.handleScroll);
|
||||
on(window, 'resize', this.handleScroll);
|
||||
this.$nextTick(() => {
|
||||
this.handleScroll();
|
||||
});
|
||||
},
|
||||
beforeDestroy () {
|
||||
// window.removeEventListener('scroll', this.handleScroll, false);
|
||||
// window.removeEventListener('resize', this.handleScroll, false);
|
||||
off(window, 'scroll', this.handleScroll);
|
||||
off(window, 'resize', this.handleScroll);
|
||||
},
|
||||
methods: {
|
||||
handleScroll () {
|
||||
const affix = this.affix;
|
||||
const scrollTop = getScroll(window, true);
|
||||
const elOffset = getOffset(this.$el);
|
||||
const windowHeight = window.innerHeight;
|
||||
const elHeight = this.$el.getElementsByTagName('div')[0].offsetHeight;
|
||||
|
||||
// Fixed Top
|
||||
if ((elOffset.top - this.offsetTop) < scrollTop && this.offsetType == 'top' && !affix) {
|
||||
this.affix = true;
|
||||
this.slotStyle = {
|
||||
width: this.$refs.point.clientWidth + 'px',
|
||||
height: this.$refs.point.clientHeight + 'px'
|
||||
};
|
||||
this.slot = true;
|
||||
this.styles = {
|
||||
top: `${this.offsetTop}px`,
|
||||
left: `${elOffset.left}px`,
|
||||
width: `${this.$el.offsetWidth}px`
|
||||
};
|
||||
|
||||
this.$emit('on-change', true);
|
||||
} else if ((elOffset.top - this.offsetTop) > scrollTop && this.offsetType == 'top' && affix) {
|
||||
this.slot = false;
|
||||
this.slotStyle = {};
|
||||
this.affix = false;
|
||||
this.styles = null;
|
||||
|
||||
this.$emit('on-change', false);
|
||||
}
|
||||
|
||||
// Fixed Bottom
|
||||
if ((elOffset.top + this.offsetBottom + elHeight) > (scrollTop + windowHeight) && this.offsetType == 'bottom' && !affix) {
|
||||
this.affix = true;
|
||||
this.styles = {
|
||||
bottom: `${this.offsetBottom}px`,
|
||||
left: `${elOffset.left}px`,
|
||||
width: `${this.$el.offsetWidth}px`
|
||||
};
|
||||
|
||||
this.$emit('on-change', true);
|
||||
} else if ((elOffset.top + this.offsetBottom + elHeight) < (scrollTop + windowHeight) && this.offsetType == 'bottom' && affix) {
|
||||
this.affix = false;
|
||||
this.styles = null;
|
||||
|
||||
this.$emit('on-change', false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
2
src/components/affix/index.js
Normal file
2
src/components/affix/index.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
import Affix from './affix.vue';
|
||||
export default Affix;
|
110
src/components/alert/alert.vue
Normal file
110
src/components/alert/alert.vue
Normal file
|
@ -0,0 +1,110 @@
|
|||
<template>
|
||||
<transition name="fade">
|
||||
<div v-if="!closed" :class="wrapClasses">
|
||||
<span :class="iconClasses" v-if="showIcon">
|
||||
<slot name="icon">
|
||||
<Icon :type="iconType"></Icon>
|
||||
</slot>
|
||||
</span>
|
||||
<span :class="messageClasses"><slot></slot></span>
|
||||
<span :class="descClasses"><slot name="desc"></slot></span>
|
||||
<a :class="closeClasses" v-if="closable" @click="close">
|
||||
<slot name="close">
|
||||
<Icon type="ios-close"></Icon>
|
||||
</slot>
|
||||
</a>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
<script>
|
||||
import Icon from '../icon';
|
||||
import { oneOf } from '../../utils/assist';
|
||||
|
||||
const prefixCls = 'ivu-alert';
|
||||
|
||||
export default {
|
||||
name: 'Alert',
|
||||
components: { Icon },
|
||||
props: {
|
||||
type: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['success', 'info', 'warning', 'error']);
|
||||
},
|
||||
default: 'info'
|
||||
},
|
||||
closable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showIcon: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
banner: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
closed: false,
|
||||
desc: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
wrapClasses () {
|
||||
return [
|
||||
`${prefixCls}`,
|
||||
`${prefixCls}-${this.type}`,
|
||||
{
|
||||
[`${prefixCls}-with-icon`]: this.showIcon,
|
||||
[`${prefixCls}-with-desc`]: this.desc,
|
||||
[`${prefixCls}-with-banner`]: this.banner
|
||||
}
|
||||
];
|
||||
},
|
||||
messageClasses () {
|
||||
return `${prefixCls}-message`;
|
||||
},
|
||||
descClasses () {
|
||||
return `${prefixCls}-desc`;
|
||||
},
|
||||
closeClasses () {
|
||||
return `${prefixCls}-close`;
|
||||
},
|
||||
iconClasses () {
|
||||
return `${prefixCls}-icon`;
|
||||
},
|
||||
iconType () {
|
||||
let type = '';
|
||||
|
||||
switch (this.type) {
|
||||
case 'success':
|
||||
type = 'ios-checkmark-circle';
|
||||
break;
|
||||
case 'info':
|
||||
type = 'ios-information-circle';
|
||||
break;
|
||||
case 'warning':
|
||||
type = 'ios-alert';
|
||||
break;
|
||||
case 'error':
|
||||
type = 'ios-close-circle';
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.desc) type += '-outline';
|
||||
return type;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close (e) {
|
||||
this.closed = true;
|
||||
this.$emit('on-close', e);
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.desc = this.$slots.desc !== undefined;
|
||||
}
|
||||
};
|
||||
</script>
|
2
src/components/alert/index.js
Normal file
2
src/components/alert/index.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
import Alert from './alert.vue';
|
||||
export default Alert;
|
2
src/components/anchor-link/index.js
Normal file
2
src/components/anchor-link/index.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
import AnchorLink from '../anchor/anchor-link.vue';
|
||||
export default AnchorLink;
|
59
src/components/anchor/anchor-link.vue
Normal file
59
src/components/anchor/anchor-link.vue
Normal file
|
@ -0,0 +1,59 @@
|
|||
<template>
|
||||
<div :class="anchorLinkClasses">
|
||||
<a :class="linkTitleClasses" :href="href" :data-scroll-offset="scrollOffset" :data-href="href" @click.prevent="goAnchor" :title="title">{{ title }}</a>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'AnchorLink',
|
||||
inject: ['anchorCom'],
|
||||
props: {
|
||||
href: String,
|
||||
title: String,
|
||||
scrollOffset: {
|
||||
type: Number,
|
||||
default () {
|
||||
return this.anchorCom.scrollOffset;
|
||||
}
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
prefix: 'ivu-anchor-link'
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
anchorLinkClasses () {
|
||||
return [
|
||||
this.prefix,
|
||||
this.anchorCom.currentLink === this.href ? `${this.prefix}-active` : ''
|
||||
];
|
||||
},
|
||||
linkTitleClasses () {
|
||||
return [
|
||||
`${this.prefix}-title`
|
||||
];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goAnchor () {
|
||||
this.currentLink = this.href;
|
||||
this.anchorCom.handleHashChange();
|
||||
this.anchorCom.handleScrollTo();
|
||||
this.anchorCom.$emit('on-select', this.href);
|
||||
const isRoute = this.$router;
|
||||
if (isRoute) {
|
||||
this.$router.push(this.href);
|
||||
} else {
|
||||
window.location.href = this.href;
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$nextTick(() => {
|
||||
this.anchorCom.init();
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
200
src/components/anchor/anchor.vue
Normal file
200
src/components/anchor/anchor.vue
Normal file
|
@ -0,0 +1,200 @@
|
|||
<template>
|
||||
<component :is="wrapperComponent" :offset-top="offsetTop" :offset-bottom="offsetBottom" @on-change="handleAffixStateChange">
|
||||
<div :class="`${prefix}-wrapper`" :style="wrapperStyle">
|
||||
<div :class="`${prefix}`">
|
||||
<div :class="`${prefix}-ink`">
|
||||
<span v-show="showInk" :class="`${prefix}-ink-ball`" :style="{top: `${inkTop}px`}"></span>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
<script>
|
||||
import { scrollTop, findComponentsDownward, sharpMatcherRegx } from '../../utils/assist';
|
||||
import { on, off } from '../../utils/dom';
|
||||
export default {
|
||||
name: 'Anchor',
|
||||
provide () {
|
||||
return {
|
||||
anchorCom: this
|
||||
};
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
prefix: 'ivu-anchor',
|
||||
isAffixed: false, // current affixed state
|
||||
inkTop: 0,
|
||||
animating: false, // if is scrolling now
|
||||
currentLink: '', // current show link => #href -> currentLink = #href
|
||||
currentId: '', // current show title id => #href -> currentId = href
|
||||
scrollContainer: null,
|
||||
scrollElement: null,
|
||||
titlesOffsetArr: [],
|
||||
wrapperTop: 0,
|
||||
upperFirstTitle: true
|
||||
};
|
||||
},
|
||||
props: {
|
||||
affix: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
offsetTop: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
offsetBottom: Number,
|
||||
bounds: {
|
||||
type: Number,
|
||||
default: 5
|
||||
},
|
||||
// container: [String, HTMLElement], // HTMLElement 在 SSR 下不支持
|
||||
container: null,
|
||||
showInk: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
scrollOffset: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
wrapperComponent () {
|
||||
return this.affix ? 'Affix' : 'div';
|
||||
},
|
||||
wrapperStyle () {
|
||||
return {
|
||||
maxHeight: this.offsetTop ? `calc(100vh - ${this.offsetTop}px)` : '100vh'
|
||||
};
|
||||
},
|
||||
containerIsWindow () {
|
||||
return this.scrollContainer === window;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleAffixStateChange (state) {
|
||||
this.isAffixed = this.affix && state;
|
||||
},
|
||||
handleScroll (e) {
|
||||
this.upperFirstTitle = e.target.scrollTop < this.titlesOffsetArr[0].offset;
|
||||
if (this.animating) return;
|
||||
this.updateTitleOffset();
|
||||
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop || e.target.scrollTop;
|
||||
this.getCurrentScrollAtTitleId(scrollTop);
|
||||
},
|
||||
handleHashChange () {
|
||||
const url = window.location.href;
|
||||
const sharpLinkMatch = sharpMatcherRegx.exec(url);
|
||||
if (!sharpLinkMatch) return;
|
||||
this.currentLink = sharpLinkMatch[0];
|
||||
this.currentId = sharpLinkMatch[1];
|
||||
},
|
||||
handleScrollTo () {
|
||||
const anchor = document.getElementById(this.currentId);
|
||||
const currentLinkElementA = document.querySelector(`a[data-href="${this.currentLink}"]`);
|
||||
let offset = this.scrollOffset;
|
||||
if (currentLinkElementA) {
|
||||
offset = parseFloat(currentLinkElementA.getAttribute('data-scroll-offset'));
|
||||
}
|
||||
|
||||
if (!anchor) return;
|
||||
const offsetTop = anchor.offsetTop - this.wrapperTop - offset;
|
||||
this.animating = true;
|
||||
scrollTop(this.scrollContainer, this.scrollElement.scrollTop, offsetTop, 600, () => {
|
||||
this.animating = false;
|
||||
});
|
||||
this.handleSetInkTop();
|
||||
},
|
||||
handleSetInkTop () {
|
||||
const currentLinkElementA = document.querySelector(`a[data-href="${this.currentLink}"]`);
|
||||
if (!currentLinkElementA) return;
|
||||
const elementATop = currentLinkElementA.offsetTop;
|
||||
const top = (elementATop < 0 ? this.offsetTop : elementATop);
|
||||
this.inkTop = top;
|
||||
},
|
||||
updateTitleOffset () {
|
||||
const links = findComponentsDownward(this, 'AnchorLink').map(link => {
|
||||
return link.href;
|
||||
});
|
||||
const idArr = links.map(link => {
|
||||
return link.split('#')[1];
|
||||
});
|
||||
let offsetArr = [];
|
||||
idArr.forEach(id => {
|
||||
const titleEle = document.getElementById(id);
|
||||
if (titleEle) offsetArr.push({
|
||||
link: `#${id}`,
|
||||
offset: titleEle.offsetTop - this.scrollElement.offsetTop
|
||||
});
|
||||
});
|
||||
this.titlesOffsetArr = offsetArr;
|
||||
},
|
||||
getCurrentScrollAtTitleId (scrollTop) {
|
||||
let i = -1;
|
||||
let len = this.titlesOffsetArr.length;
|
||||
let titleItem = {
|
||||
link: '#',
|
||||
offset: 0
|
||||
};
|
||||
scrollTop += this.bounds;
|
||||
while (++i < len) {
|
||||
let currentEle = this.titlesOffsetArr[i];
|
||||
let nextEle = this.titlesOffsetArr[i + 1];
|
||||
if (scrollTop >= currentEle.offset && scrollTop < ((nextEle && nextEle.offset) || Infinity)) {
|
||||
titleItem = this.titlesOffsetArr[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.currentLink = titleItem.link;
|
||||
this.handleSetInkTop();
|
||||
},
|
||||
getContainer () {
|
||||
this.scrollContainer = this.container ? (typeof this.container === 'string' ? document.querySelector(this.container) : this.container) : window;
|
||||
this.scrollElement = this.container ? this.scrollContainer : (document.documentElement || document.body);
|
||||
},
|
||||
removeListener () {
|
||||
off(this.scrollContainer, 'scroll', this.handleScroll);
|
||||
off(window, 'hashchange', this.handleHashChange);
|
||||
},
|
||||
init () {
|
||||
// const anchorLink = findComponentDownward(this, 'AnchorLink');
|
||||
this.handleHashChange();
|
||||
this.$nextTick(() => {
|
||||
this.removeListener();
|
||||
this.getContainer();
|
||||
this.wrapperTop = this.containerIsWindow ? 0 : this.scrollElement.offsetTop;
|
||||
this.handleScrollTo();
|
||||
this.handleSetInkTop();
|
||||
this.updateTitleOffset();
|
||||
if (this.titlesOffsetArr[0]) {
|
||||
this.upperFirstTitle = this.scrollElement.scrollTop < this.titlesOffsetArr[0].offset;
|
||||
}
|
||||
on(this.scrollContainer, 'scroll', this.handleScroll);
|
||||
on(window, 'hashchange', this.handleHashChange);
|
||||
});
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route' () {
|
||||
this.handleHashChange();
|
||||
this.$nextTick(() => {
|
||||
this.handleScrollTo();
|
||||
});
|
||||
},
|
||||
container () {
|
||||
this.init();
|
||||
},
|
||||
currentLink (newHref, oldHref) {
|
||||
this.$emit('on-change', newHref, oldHref);
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.init();
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.removeListener();
|
||||
}
|
||||
};
|
||||
</script>
|
2
src/components/anchor/index.js
Normal file
2
src/components/anchor/index.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
import Anchor from './anchor.vue';
|
||||
export default Anchor;
|
173
src/components/auto-complete/auto-complete.vue
Normal file
173
src/components/auto-complete/auto-complete.vue
Normal file
|
@ -0,0 +1,173 @@
|
|||
<template>
|
||||
<i-select
|
||||
ref="select"
|
||||
class="ivu-auto-complete"
|
||||
:label="label"
|
||||
:disabled="disabled"
|
||||
:clearable="clearable"
|
||||
:placeholder="placeholder"
|
||||
:size="size"
|
||||
:placement="placement"
|
||||
:value="currentValue"
|
||||
filterable
|
||||
remote
|
||||
auto-complete
|
||||
:remote-method="remoteMethod"
|
||||
@on-change="handleChange"
|
||||
:transfer="transfer">
|
||||
<slot name="input">
|
||||
<i-input
|
||||
:element-id="elementId"
|
||||
ref="input"
|
||||
slot="input"
|
||||
v-model="currentValue"
|
||||
:name="name"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:size="size"
|
||||
:icon="inputIcon"
|
||||
@on-click="handleClear"
|
||||
@on-focus="handleFocus"
|
||||
@on-blur="handleBlur"></i-input>
|
||||
</slot>
|
||||
<slot>
|
||||
<i-option v-for="item in filteredData" :value="item" :key="item">{{ item }}</i-option>
|
||||
</slot>
|
||||
</i-select>
|
||||
</template>
|
||||
<script>
|
||||
import iSelect from '../select/select.vue';
|
||||
import iOption from '../select/option.vue';
|
||||
import iInput from '../input/input.vue';
|
||||
import { oneOf } from '../../utils/assist';
|
||||
import Emitter from '../../mixins/emitter';
|
||||
|
||||
export default {
|
||||
name: 'AutoComplete',
|
||||
mixins: [ Emitter ],
|
||||
components: { iSelect, iOption, iInput },
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
label: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
clearable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
placeholder: {
|
||||
type: String
|
||||
},
|
||||
size: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['small', 'large', 'default']);
|
||||
},
|
||||
default () {
|
||||
return !this.$IVIEW || this.$IVIEW.size === '' ? 'default' : this.$IVIEW.size;
|
||||
}
|
||||
},
|
||||
icon: {
|
||||
type: String
|
||||
},
|
||||
filterMethod: {
|
||||
type: [Function, Boolean],
|
||||
default: false
|
||||
},
|
||||
placement: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['top', 'bottom']);
|
||||
},
|
||||
default: 'bottom'
|
||||
},
|
||||
transfer: {
|
||||
type: Boolean,
|
||||
default () {
|
||||
return this.$IVIEW.transfer === '' ? false : this.$IVIEW.transfer;
|
||||
}
|
||||
},
|
||||
name: {
|
||||
type: String
|
||||
},
|
||||
elementId: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
currentValue: this.value,
|
||||
disableEmitChange: false // for Form reset
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
inputIcon () {
|
||||
let icon = '';
|
||||
if (this.clearable && this.currentValue) {
|
||||
icon = 'ios-close';
|
||||
} else if (this.icon) {
|
||||
icon = this.icon;
|
||||
}
|
||||
return icon;
|
||||
},
|
||||
filteredData () {
|
||||
if (this.filterMethod) {
|
||||
return this.data.filter(item => this.filterMethod(this.currentValue, item));
|
||||
} else {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value (val) {
|
||||
if(this.currentValue !== val){
|
||||
this.disableEmitChange = true;
|
||||
}
|
||||
this.currentValue = val;
|
||||
},
|
||||
currentValue (val) {
|
||||
this.$refs.select.setQuery(val);
|
||||
this.$emit('input', val);
|
||||
if (this.disableEmitChange) {
|
||||
this.disableEmitChange = false;
|
||||
return;
|
||||
}
|
||||
this.$emit('on-change', val);
|
||||
this.dispatch('FormItem', 'on-form-change', val);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
remoteMethod (query) {
|
||||
this.$emit('on-search', query);
|
||||
},
|
||||
handleChange (val) {
|
||||
if (val === undefined || val === null) return;
|
||||
this.currentValue = val;
|
||||
this.$refs.input.blur();
|
||||
this.$emit('on-select', val);
|
||||
},
|
||||
handleFocus (event) {
|
||||
this.$emit('on-focus', event);
|
||||
},
|
||||
handleBlur (event) {
|
||||
this.$emit('on-blur', event);
|
||||
},
|
||||
handleClear () {
|
||||
if (!this.clearable) return;
|
||||
this.currentValue = '';
|
||||
this.$refs.select.reset();
|
||||
this.$emit('on-clear');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
2
src/components/auto-complete/index.js
Normal file
2
src/components/auto-complete/index.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
import AutoComplete from './auto-complete.vue';
|
||||
export default AutoComplete;
|
104
src/components/avatar/avatar.vue
Normal file
104
src/components/avatar/avatar.vue
Normal file
|
@ -0,0 +1,104 @@
|
|||
<template>
|
||||
<span :class="classes">
|
||||
<img :src="src" v-if="src" @error="handleError">
|
||||
<Icon :type="icon" :custom="customIcon" v-else-if="icon || customIcon"></Icon>
|
||||
<span ref="children" :class="[prefixCls + '-string']" :style="childrenStyle" v-else><slot></slot></span>
|
||||
</span>
|
||||
</template>
|
||||
<script>
|
||||
import Icon from '../icon';
|
||||
import { oneOf } from '../../utils/assist';
|
||||
|
||||
const prefixCls = 'ivu-avatar';
|
||||
|
||||
export default {
|
||||
name: 'Avatar',
|
||||
components: { Icon },
|
||||
props: {
|
||||
shape: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['circle', 'square']);
|
||||
},
|
||||
default: 'circle'
|
||||
},
|
||||
size: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['small', 'large', 'default']);
|
||||
},
|
||||
default () {
|
||||
return !this.$IVIEW || this.$IVIEW.size === '' ? 'default' : this.$IVIEW.size;
|
||||
}
|
||||
},
|
||||
src: {
|
||||
type: String
|
||||
},
|
||||
icon: {
|
||||
type: String
|
||||
},
|
||||
customIcon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
prefixCls: prefixCls,
|
||||
scale: 1,
|
||||
childrenWidth: 0,
|
||||
isSlotShow: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
return [
|
||||
`${prefixCls}`,
|
||||
`${prefixCls}-${this.shape}`,
|
||||
`${prefixCls}-${this.size}`,
|
||||
{
|
||||
[`${prefixCls}-image`]: !!this.src,
|
||||
[`${prefixCls}-icon`]: !!this.icon || !!this.customIcon
|
||||
}
|
||||
];
|
||||
},
|
||||
childrenStyle () {
|
||||
let style = {};
|
||||
if (this.isSlotShow) {
|
||||
style = {
|
||||
msTransform: `scale(${this.scale})`,
|
||||
WebkitTransform: `scale(${this.scale})`,
|
||||
transform: `scale(${this.scale})`,
|
||||
position: 'absolute',
|
||||
display: 'inline-block',
|
||||
left: `calc(50% - ${Math.round(this.childrenWidth / 2)}px)`
|
||||
};
|
||||
}
|
||||
return style;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setScale () {
|
||||
this.isSlotShow = !this.src && !this.icon;
|
||||
if (this.$refs.children) {
|
||||
// set children width again to make slot centered
|
||||
this.childrenWidth = this.$refs.children.offsetWidth;
|
||||
const avatarWidth = this.$el.getBoundingClientRect().width;
|
||||
// add 4px gap for each side to get better performance
|
||||
if (avatarWidth - 8 < this.childrenWidth) {
|
||||
this.scale = (avatarWidth - 8) / this.childrenWidth;
|
||||
} else {
|
||||
this.scale = 1;
|
||||
}
|
||||
}
|
||||
},
|
||||
handleError (e) {
|
||||
this.$emit('on-error', e);
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.setScale();
|
||||
},
|
||||
updated () {
|
||||
this.setScale();
|
||||
}
|
||||
};
|
||||
</script>
|
2
src/components/avatar/index.js
Normal file
2
src/components/avatar/index.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
import Avatar from './avatar.vue';
|
||||
export default Avatar;
|
81
src/components/back-top/back-top.vue
Normal file
81
src/components/back-top/back-top.vue
Normal file
|
@ -0,0 +1,81 @@
|
|||
<template>
|
||||
<div :class="classes" :style="styles" @click="back">
|
||||
<slot>
|
||||
<div :class="innerClasses">
|
||||
<i class="ivu-icon ivu-icon-ios-arrow-up"></i>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { scrollTop } from '../../utils/assist';
|
||||
import { on, off } from '../../utils/dom';
|
||||
const prefixCls = 'ivu-back-top';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
height: {
|
||||
type: Number,
|
||||
default: 400
|
||||
},
|
||||
bottom: {
|
||||
type: Number,
|
||||
default: 30
|
||||
},
|
||||
right: {
|
||||
type: Number,
|
||||
default: 30
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 1000
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
backTop: false
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
// window.addEventListener('scroll', this.handleScroll, false);
|
||||
// window.addEventListener('resize', this.handleScroll, false);
|
||||
on(window, 'scroll', this.handleScroll);
|
||||
on(window, 'resize', this.handleScroll);
|
||||
},
|
||||
beforeDestroy () {
|
||||
// window.removeEventListener('scroll', this.handleScroll, false);
|
||||
// window.removeEventListener('resize', this.handleScroll, false);
|
||||
off(window, 'scroll', this.handleScroll);
|
||||
off(window, 'resize', this.handleScroll);
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
return [
|
||||
`${prefixCls}`,
|
||||
{
|
||||
[`${prefixCls}-show`]: this.backTop
|
||||
}
|
||||
];
|
||||
},
|
||||
styles () {
|
||||
return {
|
||||
bottom: `${this.bottom}px`,
|
||||
right: `${this.right}px`
|
||||
};
|
||||
},
|
||||
innerClasses () {
|
||||
return `${prefixCls}-inner`;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleScroll () {
|
||||
this.backTop = window.pageYOffset >= this.height;
|
||||
},
|
||||
back () {
|
||||
const sTop = document.documentElement.scrollTop || document.body.scrollTop;
|
||||
scrollTop(window, sTop, 0, this.duration);
|
||||
this.$emit('on-click');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
2
src/components/back-top/index.js
Normal file
2
src/components/back-top/index.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
import BackTop from './back-top.vue';
|
||||
export default BackTop;
|
121
src/components/badge/badge.vue
Normal file
121
src/components/badge/badge.vue
Normal file
|
@ -0,0 +1,121 @@
|
|||
<template>
|
||||
<span v-if="dot" :class="classes" ref="badge">
|
||||
<slot></slot>
|
||||
<sup :class="dotClasses" :style="styles" v-show="badge"></sup>
|
||||
</span>
|
||||
<span v-else-if="status" :class="classes" class="ivu-badge-status" ref="badge">
|
||||
<span :class="statusClasses"></span>
|
||||
<span class="ivu-badge-status-text">{{ text }}</span>
|
||||
</span>
|
||||
<span v-else :class="classes" ref="badge">
|
||||
<slot></slot>
|
||||
<sup v-if="hasCount" :style="styles" :class="countClasses" v-show="badge">{{ finalCount }}</sup>
|
||||
</span>
|
||||
</template>
|
||||
<script>
|
||||
import { oneOf } from '../../utils/assist';
|
||||
const prefixCls = 'ivu-badge';
|
||||
|
||||
export default {
|
||||
name: 'Badge',
|
||||
props: {
|
||||
count: Number,
|
||||
dot: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
overflowCount: {
|
||||
type: [Number, String],
|
||||
default: 99
|
||||
},
|
||||
className: String,
|
||||
showZero: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
status: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['success', 'processing', 'default', 'error', 'warning']);
|
||||
}
|
||||
},
|
||||
type: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['success', 'primary', 'normal', 'error', 'warning', 'info']);
|
||||
}
|
||||
},
|
||||
offset: {
|
||||
type: Array
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
return `${prefixCls}`;
|
||||
},
|
||||
dotClasses () {
|
||||
return `${prefixCls}-dot`;
|
||||
},
|
||||
countClasses () {
|
||||
return [
|
||||
`${prefixCls}-count`,
|
||||
{
|
||||
[`${this.className}`]: !!this.className,
|
||||
[`${prefixCls}-count-alone`]: this.alone,
|
||||
[`${prefixCls}-count-${this.type}`]: !!this.type
|
||||
}
|
||||
];
|
||||
},
|
||||
statusClasses () {
|
||||
return [
|
||||
`${prefixCls}-status-dot`,
|
||||
{
|
||||
[`${prefixCls}-status-${this.status}`]: !!this.status
|
||||
}
|
||||
];
|
||||
},
|
||||
styles () {
|
||||
const style = {};
|
||||
if (this.offset && this.offset.length === 2) {
|
||||
style['margin-top'] = `${this.offset[0]}px`;
|
||||
style['margin-right'] = `${this.offset[1]}px`;
|
||||
}
|
||||
return style;
|
||||
},
|
||||
finalCount () {
|
||||
if (this.text !== '') return this.text;
|
||||
return parseInt(this.count) >= parseInt(this.overflowCount) ? `${this.overflowCount}+` : this.count;
|
||||
},
|
||||
badge () {
|
||||
let status = false;
|
||||
|
||||
if (this.count) {
|
||||
status = !(parseInt(this.count) === 0);
|
||||
}
|
||||
|
||||
if (this.dot) {
|
||||
status = true;
|
||||
if (this.count !== null) {
|
||||
if (parseInt(this.count) === 0) {
|
||||
status = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.text !== '') status = true;
|
||||
|
||||
return status || this.showZero;
|
||||
},
|
||||
hasCount() {
|
||||
if(this.count || this.text !== '') return true;
|
||||
if(this.showZero && parseInt(this.count) === 0) return true;
|
||||
else return false;
|
||||
},
|
||||
alone () {
|
||||
return this.$slots.default === undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
2
src/components/badge/index.js
Normal file
2
src/components/badge/index.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
import Badge from './badge.vue';
|
||||
export default Badge;
|
83
src/components/base/collapse-transition.js
Normal file
83
src/components/base/collapse-transition.js
Normal file
|
@ -0,0 +1,83 @@
|
|||
import { addClass, removeClass } from '../../utils/assist';
|
||||
|
||||
const Transition = {
|
||||
beforeEnter(el) {
|
||||
addClass(el, 'collapse-transition');
|
||||
if (!el.dataset) el.dataset = {};
|
||||
|
||||
el.dataset.oldPaddingTop = el.style.paddingTop;
|
||||
el.dataset.oldPaddingBottom = el.style.paddingBottom;
|
||||
|
||||
el.style.height = '0';
|
||||
el.style.paddingTop = 0;
|
||||
el.style.paddingBottom = 0;
|
||||
},
|
||||
|
||||
enter(el) {
|
||||
el.dataset.oldOverflow = el.style.overflow;
|
||||
if (el.scrollHeight !== 0) {
|
||||
el.style.height = el.scrollHeight + 'px';
|
||||
el.style.paddingTop = el.dataset.oldPaddingTop;
|
||||
el.style.paddingBottom = el.dataset.oldPaddingBottom;
|
||||
} else {
|
||||
el.style.height = '';
|
||||
el.style.paddingTop = el.dataset.oldPaddingTop;
|
||||
el.style.paddingBottom = el.dataset.oldPaddingBottom;
|
||||
}
|
||||
|
||||
el.style.overflow = 'hidden';
|
||||
},
|
||||
|
||||
afterEnter(el) {
|
||||
// for safari: remove class then reset height is necessary
|
||||
removeClass(el, 'collapse-transition');
|
||||
el.style.height = '';
|
||||
el.style.overflow = el.dataset.oldOverflow;
|
||||
},
|
||||
|
||||
beforeLeave(el) {
|
||||
if (!el.dataset) el.dataset = {};
|
||||
el.dataset.oldPaddingTop = el.style.paddingTop;
|
||||
el.dataset.oldPaddingBottom = el.style.paddingBottom;
|
||||
el.dataset.oldOverflow = el.style.overflow;
|
||||
|
||||
el.style.height = el.scrollHeight + 'px';
|
||||
el.style.overflow = 'hidden';
|
||||
},
|
||||
|
||||
leave(el) {
|
||||
if (el.scrollHeight !== 0) {
|
||||
// for safari: add class after set height, or it will jump to zero height suddenly, weired
|
||||
addClass(el, 'collapse-transition');
|
||||
el.style.height = 0;
|
||||
el.style.paddingTop = 0;
|
||||
el.style.paddingBottom = 0;
|
||||
}
|
||||
},
|
||||
|
||||
afterLeave(el) {
|
||||
removeClass(el, 'collapse-transition');
|
||||
el.style.height = '';
|
||||
el.style.overflow = el.dataset.oldOverflow;
|
||||
el.style.paddingTop = el.dataset.oldPaddingTop;
|
||||
el.style.paddingBottom = el.dataset.oldPaddingBottom;
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'CollapseTransition',
|
||||
functional: true,
|
||||
props: {
|
||||
appear: Boolean
|
||||
},
|
||||
render(h, { children, props }) {
|
||||
const data = {
|
||||
on: Transition,
|
||||
props: {
|
||||
appear: props.appear
|
||||
}
|
||||
};
|
||||
|
||||
return h('transition', data, children);
|
||||
}
|
||||
};
|
36
src/components/base/notification/index.js
Normal file
36
src/components/base/notification/index.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
import Notification from './notification.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
Notification.newInstance = properties => {
|
||||
const _props = properties || {};
|
||||
|
||||
const Instance = new Vue({
|
||||
render (h) {
|
||||
return h(Notification, {
|
||||
props: _props
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const component = Instance.$mount();
|
||||
document.body.appendChild(component.$el);
|
||||
const notification = Instance.$children[0];
|
||||
|
||||
return {
|
||||
notice (noticeProps) {
|
||||
notification.add(noticeProps);
|
||||
},
|
||||
remove (name) {
|
||||
notification.close(name);
|
||||
},
|
||||
component: notification,
|
||||
destroy (element) {
|
||||
notification.closeAll();
|
||||
setTimeout(function() {
|
||||
document.body.removeChild(document.getElementsByClassName(element)[0]);
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default Notification;
|
172
src/components/base/notification/notice.vue
Normal file
172
src/components/base/notification/notice.vue
Normal file
|
@ -0,0 +1,172 @@
|
|||
<template>
|
||||
<transition :name="transitionName" @enter="handleEnter" @leave="handleLeave" appear>
|
||||
<div :class="classes" :style="styles">
|
||||
<template v-if="type === 'notice'">
|
||||
<div :class="contentClasses" ref="content" v-html="content"></div>
|
||||
<div :class="contentWithIcon">
|
||||
<render-cell
|
||||
:render="renderFunc"
|
||||
></render-cell>
|
||||
</div>
|
||||
<a :class="[baseClass + '-close']" @click="close" v-if="closable">
|
||||
<i class="ivu-icon ivu-icon-ios-close"></i>
|
||||
</a>
|
||||
</template>
|
||||
<template v-if="type === 'message'">
|
||||
<div :class="[baseClass + '-content']" ref="content">
|
||||
<div :class="[baseClass + '-content-text']" v-html="content"></div>
|
||||
<div :class="[baseClass + '-content-text']">
|
||||
<render-cell
|
||||
:render="renderFunc"
|
||||
></render-cell>
|
||||
</div>
|
||||
<a :class="[baseClass + '-close']" @click="close" v-if="closable">
|
||||
<i class="ivu-icon ivu-icon-ios-close"></i>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
<script>
|
||||
import RenderCell from '../render';
|
||||
export default {
|
||||
components: {
|
||||
RenderCell
|
||||
},
|
||||
props: {
|
||||
prefixCls: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 1.5
|
||||
},
|
||||
type: {
|
||||
type: String
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
withIcon: Boolean,
|
||||
render: {
|
||||
type: Function
|
||||
},
|
||||
hasTitle: Boolean,
|
||||
styles: {
|
||||
type: Object,
|
||||
default: function() {
|
||||
return {
|
||||
right: '50%'
|
||||
};
|
||||
}
|
||||
},
|
||||
closable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
className: {
|
||||
type: String
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
onClose: {
|
||||
type: Function
|
||||
},
|
||||
transitionName: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
withDesc: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
baseClass () {
|
||||
return `${this.prefixCls}-notice`;
|
||||
},
|
||||
renderFunc () {
|
||||
return this.render || function () {};
|
||||
},
|
||||
classes () {
|
||||
return [
|
||||
this.baseClass,
|
||||
{
|
||||
[`${this.className}`]: !!this.className,
|
||||
[`${this.baseClass}-closable`]: this.closable,
|
||||
[`${this.baseClass}-with-desc`]: this.withDesc
|
||||
}
|
||||
];
|
||||
},
|
||||
contentClasses () {
|
||||
return [
|
||||
`${this.baseClass}-content`,
|
||||
this.render !== undefined ? `${this.baseClass}-content-with-render` : ''
|
||||
];
|
||||
},
|
||||
contentWithIcon () {
|
||||
return [
|
||||
this.withIcon ? `${this.prefixCls}-content-with-icon` : '',
|
||||
!this.hasTitle && this.withIcon ? `${this.prefixCls}-content-with-render-notitle` : ''
|
||||
];
|
||||
},
|
||||
messageClasses () {
|
||||
return [
|
||||
`${this.baseClass}-content`,
|
||||
this.render !== undefined ? `${this.baseClass}-content-with-render` : ''
|
||||
];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clearCloseTimer () {
|
||||
if (this.closeTimer) {
|
||||
clearTimeout(this.closeTimer);
|
||||
this.closeTimer = null;
|
||||
}
|
||||
},
|
||||
close () {
|
||||
this.clearCloseTimer();
|
||||
this.onClose();
|
||||
this.$parent.close(this.name);
|
||||
},
|
||||
handleEnter (el) {
|
||||
if (this.type === 'message') {
|
||||
el.style.height = el.scrollHeight + 'px';
|
||||
}
|
||||
},
|
||||
handleLeave (el) {
|
||||
if (this.type === 'message') {
|
||||
// 优化一下,如果当前只有一个 Message,则不使用 js 过渡动画,这样更优美
|
||||
if (document.getElementsByClassName('ivu-message-notice').length !== 1) {
|
||||
el.style.height = 0;
|
||||
el.style.paddingTop = 0;
|
||||
el.style.paddingBottom = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.clearCloseTimer();
|
||||
|
||||
if (this.duration !== 0) {
|
||||
this.closeTimer = setTimeout(() => {
|
||||
this.close();
|
||||
}, this.duration * 1000);
|
||||
}
|
||||
|
||||
// check if with desc in Notice component
|
||||
if (this.prefixCls === 'ivu-notice') {
|
||||
let desc = this.$refs.content.querySelectorAll(`.${this.prefixCls}-desc`)[0];
|
||||
this.withDesc = this.render ? true : (desc ? desc.innerHTML !== '' : false);
|
||||
}
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.clearCloseTimer();
|
||||
}
|
||||
};
|
||||
</script>
|
114
src/components/base/notification/notification.vue
Normal file
114
src/components/base/notification/notification.vue
Normal file
|
@ -0,0 +1,114 @@
|
|||
<template>
|
||||
<div :class="classes" :style="wrapStyles">
|
||||
<Notice
|
||||
v-for="notice in notices"
|
||||
:key="notice.name"
|
||||
:prefix-cls="prefixCls"
|
||||
:styles="notice.styles"
|
||||
:type="notice.type"
|
||||
:content="notice.content"
|
||||
:duration="notice.duration"
|
||||
:render="notice.render"
|
||||
:has-title="notice.hasTitle"
|
||||
:withIcon="notice.withIcon"
|
||||
:closable="notice.closable"
|
||||
:name="notice.name"
|
||||
:transition-name="notice.transitionName"
|
||||
:on-close="notice.onClose">
|
||||
</Notice>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Notice from './notice.vue';
|
||||
|
||||
import { transferIndex, transferIncrease } from '../../../utils/transfer-queue';
|
||||
|
||||
const prefixCls = 'ivu-notification';
|
||||
let seed = 0;
|
||||
const now = Date.now();
|
||||
|
||||
function getUuid () {
|
||||
return 'ivuNotification_' + now + '_' + (seed++);
|
||||
}
|
||||
|
||||
export default {
|
||||
components: { Notice },
|
||||
props: {
|
||||
prefixCls: {
|
||||
type: String,
|
||||
default: prefixCls
|
||||
},
|
||||
styles: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return {
|
||||
top: '65px',
|
||||
left: '50%'
|
||||
};
|
||||
}
|
||||
},
|
||||
content: {
|
||||
type: String
|
||||
},
|
||||
className: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
notices: [],
|
||||
tIndex: this.handleGetIndex()
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
return [
|
||||
`${this.prefixCls}`,
|
||||
{
|
||||
[`${this.className}`]: !!this.className
|
||||
}
|
||||
];
|
||||
},
|
||||
wrapStyles () {
|
||||
let styles = Object.assign({}, this.styles);
|
||||
styles['z-index'] = 1010 + this.tIndex;
|
||||
|
||||
return styles;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add (notice) {
|
||||
const name = notice.name || getUuid();
|
||||
|
||||
let _notice = Object.assign({
|
||||
styles: {
|
||||
right: '50%'
|
||||
},
|
||||
content: '',
|
||||
duration: 1.5,
|
||||
closable: false,
|
||||
name: name
|
||||
}, notice);
|
||||
|
||||
this.notices.push(_notice);
|
||||
this.tIndex = this.handleGetIndex();
|
||||
},
|
||||
close (name) {
|
||||
const notices = this.notices;
|
||||
for (let i = 0; i < notices.length; i++) {
|
||||
if (notices[i].name === name) {
|
||||
this.notices.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
closeAll () {
|
||||
this.notices = [];
|
||||
},
|
||||
handleGetIndex () {
|
||||
transferIncrease();
|
||||
return transferIndex;
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
124
src/components/base/popper.js
Normal file
124
src/components/base/popper.js
Normal file
|
@ -0,0 +1,124 @@
|
|||
/**
|
||||
* https://github.com/freeze-component/vue-popper
|
||||
* */
|
||||
import Vue from 'vue';
|
||||
const isServer = Vue.prototype.$isServer;
|
||||
const Popper = isServer ? function() {} : require('popper.js/dist/umd/popper.js'); // eslint-disable-line
|
||||
|
||||
export default {
|
||||
props: {
|
||||
placement: {
|
||||
type: String,
|
||||
default: 'bottom'
|
||||
},
|
||||
boundariesPadding: {
|
||||
type: Number,
|
||||
default: 5
|
||||
},
|
||||
reference: Object,
|
||||
popper: Object,
|
||||
offset: {
|
||||
default: 0
|
||||
},
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
transition: String,
|
||||
options: {
|
||||
type: Object,
|
||||
default () {
|
||||
return {
|
||||
modifiers: {
|
||||
computeStyle:{
|
||||
gpuAcceleration: false,
|
||||
},
|
||||
preventOverflow :{
|
||||
boundariesElement: 'window'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
// visible: {
|
||||
// type: Boolean,
|
||||
// default: false
|
||||
// }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
visible: this.value
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
this.visible = val;
|
||||
this.$emit('input', val);
|
||||
}
|
||||
},
|
||||
visible(val) {
|
||||
if (val) {
|
||||
if (this.handleIndexIncrease) this.handleIndexIncrease(); // just use for Poptip
|
||||
this.updatePopper();
|
||||
this.$emit('on-popper-show');
|
||||
} else {
|
||||
this.$emit('on-popper-hide');
|
||||
}
|
||||
this.$emit('input', val);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
createPopper() {
|
||||
if (isServer) return;
|
||||
if (!/^(top|bottom|left|right)(-start|-end)?$/g.test(this.placement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = this.options;
|
||||
const popper = this.popper || this.$refs.popper;
|
||||
const reference = this.reference || this.$refs.reference;
|
||||
|
||||
if (!popper || !reference) return;
|
||||
|
||||
if (this.popperJS && this.popperJS.hasOwnProperty('destroy')) {
|
||||
this.popperJS.destroy();
|
||||
}
|
||||
|
||||
options.placement = this.placement;
|
||||
|
||||
if (!options.modifiers.offset) {
|
||||
options.modifiers.offset = {};
|
||||
}
|
||||
options.modifiers.offset.offset = this.offset;
|
||||
options.onCreate =()=>{
|
||||
this.$nextTick(this.updatePopper);
|
||||
this.$emit('created', this);
|
||||
};
|
||||
|
||||
this.popperJS = new Popper(reference, popper, options);
|
||||
|
||||
},
|
||||
updatePopper() {
|
||||
if (isServer) return;
|
||||
this.popperJS ? this.popperJS.update() : this.createPopper();
|
||||
},
|
||||
doDestroy() {
|
||||
if (isServer) return;
|
||||
if (this.visible) return;
|
||||
this.popperJS.destroy();
|
||||
this.popperJS = null;
|
||||
}
|
||||
},
|
||||
updated (){
|
||||
this.$nextTick(()=>this.updatePopper());
|
||||
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (isServer) return;
|
||||
if (this.popperJS) {
|
||||
this.popperJS.destroy();
|
||||
}
|
||||
}
|
||||
};
|
10
src/components/base/render.js
Normal file
10
src/components/base/render.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
export default {
|
||||
name: 'RenderCell',
|
||||
functional: true,
|
||||
props: {
|
||||
render: Function
|
||||
},
|
||||
render: (h, ctx) => {
|
||||
return ctx.props.render(h);
|
||||
}
|
||||
};
|
3
src/components/breadcrumb-item/index.js
Normal file
3
src/components/breadcrumb-item/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import BreadcrumbItem from '../breadcrumb/breadcrumb-item.vue';
|
||||
|
||||
export default BreadcrumbItem;
|
50
src/components/breadcrumb/breadcrumb-item.vue
Normal file
50
src/components/breadcrumb/breadcrumb-item.vue
Normal file
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<span>
|
||||
<a
|
||||
v-if="to"
|
||||
:href="linkUrl"
|
||||
:target="target"
|
||||
:class="linkClasses"
|
||||
@click.exact="handleCheckClick($event, false)"
|
||||
@click.ctrl="handleCheckClick($event, true)"
|
||||
@click.meta="handleCheckClick($event, true)">
|
||||
<slot></slot>
|
||||
</a>
|
||||
<span v-else :class="linkClasses">
|
||||
<slot></slot>
|
||||
</span>
|
||||
<span :class="separatorClasses" v-html="separator" v-if="!showSeparator"></span>
|
||||
<span :class="separatorClasses" v-else>
|
||||
<slot name="separator"></slot>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
<script>
|
||||
import mixinsLink from '../../mixins/link';
|
||||
const prefixCls = 'ivu-breadcrumb-item';
|
||||
|
||||
export default {
|
||||
name: 'BreadcrumbItem',
|
||||
mixins: [ mixinsLink ],
|
||||
props: {
|
||||
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
separator: '',
|
||||
showSeparator: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
linkClasses () {
|
||||
return `${prefixCls}-link`;
|
||||
},
|
||||
separatorClasses () {
|
||||
return `${prefixCls}-separator`;
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.showSeparator = this.$slots.separator !== undefined;
|
||||
}
|
||||
};
|
||||
</script>
|
43
src/components/breadcrumb/breadcrumb.vue
Normal file
43
src/components/breadcrumb/breadcrumb.vue
Normal file
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<div :class="classes">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
const prefixCls = 'ivu-breadcrumb';
|
||||
|
||||
export default {
|
||||
name: 'Breadcrumb',
|
||||
props: {
|
||||
separator: {
|
||||
type: String,
|
||||
default: '/'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
return `${prefixCls}`;
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.updateChildren();
|
||||
},
|
||||
updated () {
|
||||
this.$nextTick(() => {
|
||||
this.updateChildren();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
updateChildren () {
|
||||
this.$children.forEach((child) => {
|
||||
child.separator = this.separator;
|
||||
});
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
separator () {
|
||||
this.updateChildren();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
5
src/components/breadcrumb/index.js
Normal file
5
src/components/breadcrumb/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import Breadcrumb from './breadcrumb.vue';
|
||||
import BreadcrumbItem from './breadcrumb-item.vue';
|
||||
|
||||
Breadcrumb.Item = BreadcrumbItem;
|
||||
export default Breadcrumb;
|
3
src/components/button-group/index.js
Normal file
3
src/components/button-group/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import ButtonGroup from '../button/button-group.vue';
|
||||
|
||||
export default ButtonGroup;
|
45
src/components/button/button-group.vue
Normal file
45
src/components/button/button-group.vue
Normal file
|
@ -0,0 +1,45 @@
|
|||
<template>
|
||||
<div :class="classes">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { oneOf } from '../../utils/assist';
|
||||
|
||||
const prefixCls = 'ivu-btn-group';
|
||||
|
||||
export default {
|
||||
name: 'ButtonGroup',
|
||||
props: {
|
||||
size: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['small', 'large', 'default']);
|
||||
},
|
||||
default () {
|
||||
return !this.$IVIEW || this.$IVIEW.size === '' ? 'default' : this.$IVIEW.size;
|
||||
}
|
||||
},
|
||||
shape: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['circle', 'circle-outline']);
|
||||
}
|
||||
},
|
||||
vertical: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
return [
|
||||
`${prefixCls}`,
|
||||
{
|
||||
[`${prefixCls}-${this.size}`]: !!this.size,
|
||||
[`${prefixCls}-${this.shape}`]: !!this.shape,
|
||||
[`${prefixCls}-vertical`]: this.vertical
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
117
src/components/button/button.vue
Normal file
117
src/components/button/button.vue
Normal file
|
@ -0,0 +1,117 @@
|
|||
<template>
|
||||
<component :is="tagName" :class="classes" :disabled="disabled" @click="handleClickLink" v-bind="tagProps">
|
||||
<Icon class="ivu-load-loop" type="ios-loading" v-if="loading"></Icon>
|
||||
<Icon :type="icon" :custom="customIcon" v-if="(icon || customIcon) && !loading"></Icon>
|
||||
<span v-if="showSlot" ref="slot"><slot></slot></span>
|
||||
</component>
|
||||
</template>
|
||||
<script>
|
||||
import Icon from '../icon';
|
||||
import { oneOf } from '../../utils/assist';
|
||||
import mixinsLink from '../../mixins/link';
|
||||
|
||||
const prefixCls = 'ivu-btn';
|
||||
|
||||
export default {
|
||||
name: 'Button',
|
||||
mixins: [ mixinsLink ],
|
||||
components: { Icon },
|
||||
props: {
|
||||
type: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['default', 'primary', 'dashed', 'text', 'info', 'success', 'warning', 'error']);
|
||||
},
|
||||
default: 'default'
|
||||
},
|
||||
shape: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['circle', 'circle-outline']);
|
||||
}
|
||||
},
|
||||
size: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['small', 'large', 'default']);
|
||||
},
|
||||
default () {
|
||||
return !this.$IVIEW || this.$IVIEW.size === '' ? 'default' : this.$IVIEW.size;
|
||||
}
|
||||
},
|
||||
loading: Boolean,
|
||||
disabled: Boolean,
|
||||
htmlType: {
|
||||
default: 'button',
|
||||
validator (value) {
|
||||
return oneOf(value, ['button', 'submit', 'reset']);
|
||||
}
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
customIcon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
long: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
ghost: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showSlot: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
return [
|
||||
`${prefixCls}`,
|
||||
`${prefixCls}-${this.type}`,
|
||||
{
|
||||
[`${prefixCls}-long`]: this.long,
|
||||
[`${prefixCls}-${this.shape}`]: !!this.shape,
|
||||
[`${prefixCls}-${this.size}`]: this.size !== 'default',
|
||||
[`${prefixCls}-loading`]: this.loading != null && this.loading,
|
||||
[`${prefixCls}-icon-only`]: !this.showSlot && (!!this.icon || !!this.customIcon || this.loading),
|
||||
[`${prefixCls}-ghost`]: this.ghost
|
||||
}
|
||||
];
|
||||
},
|
||||
// Point out if it should render as <a> tag
|
||||
isHrefPattern() {
|
||||
const {to} = this;
|
||||
return !!to;
|
||||
},
|
||||
tagName() {
|
||||
const {isHrefPattern} = this;
|
||||
return isHrefPattern ? 'a' : 'button';
|
||||
},
|
||||
tagProps() {
|
||||
const {isHrefPattern} = this;
|
||||
if(isHrefPattern) {
|
||||
const {linkUrl,target}=this;
|
||||
return {href: linkUrl, target};
|
||||
} else {
|
||||
const {htmlType} = this;
|
||||
return {type: htmlType};
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// Ctrl or CMD and click, open in new window when use `to`
|
||||
handleClickLink (event) {
|
||||
this.$emit('click', event);
|
||||
const openInNewWindow = event.ctrlKey || event.metaKey;
|
||||
|
||||
this.handleCheckClick(event, openInNewWindow);
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.showSlot = this.$slots.default !== undefined;
|
||||
}
|
||||
};
|
||||
</script>
|
5
src/components/button/index.js
Normal file
5
src/components/button/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import Button from './button.vue';
|
||||
import ButtonGroup from './button-group.vue';
|
||||
|
||||
Button.Group = ButtonGroup;
|
||||
export default Button;
|
86
src/components/card/card.vue
Normal file
86
src/components/card/card.vue
Normal file
|
@ -0,0 +1,86 @@
|
|||
<template>
|
||||
<div :class="classes">
|
||||
<div :class="headClasses" v-if="showHead"><slot name="title">
|
||||
<p v-if="title">
|
||||
<Icon v-if="icon" :type="icon"></Icon>
|
||||
<span>{{title}}</span>
|
||||
</p>
|
||||
</slot></div>
|
||||
<div :class="extraClasses" v-if="showExtra"><slot name="extra"></slot></div>
|
||||
<div :class="bodyClasses" :style="bodyStyles"><slot></slot></div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
const prefixCls = 'ivu-card';
|
||||
const defaultPadding = 16;
|
||||
import Icon from '../icon/icon.vue';
|
||||
|
||||
export default {
|
||||
name: 'Card',
|
||||
components: { Icon },
|
||||
props: {
|
||||
bordered: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
disHover: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
shadow: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
padding: {
|
||||
type: Number,
|
||||
default: defaultPadding
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showHead: true,
|
||||
showExtra: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
return [
|
||||
`${prefixCls}`,
|
||||
{
|
||||
[`${prefixCls}-bordered`]: this.bordered && !this.shadow,
|
||||
[`${prefixCls}-dis-hover`]: this.disHover || this.shadow,
|
||||
[`${prefixCls}-shadow`]: this.shadow
|
||||
}
|
||||
];
|
||||
},
|
||||
headClasses () {
|
||||
return `${prefixCls}-head`;
|
||||
},
|
||||
extraClasses () {
|
||||
return `${prefixCls}-extra`;
|
||||
},
|
||||
bodyClasses () {
|
||||
return `${prefixCls}-body`;
|
||||
},
|
||||
bodyStyles () {
|
||||
if (this.padding !== defaultPadding) {
|
||||
return {
|
||||
padding: `${this.padding}px`
|
||||
};
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.showHead = this.title || this.$slots.title !== undefined;
|
||||
this.showExtra = this.$slots.extra !== undefined;
|
||||
}
|
||||
};
|
||||
</script>
|
2
src/components/card/index.js
Normal file
2
src/components/card/index.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
import Card from './card.vue';
|
||||
export default Card;
|
3
src/components/carousel-item/index.js
Normal file
3
src/components/carousel-item/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import CarouselItem from '../carousel/carousel-item.vue';
|
||||
|
||||
export default CarouselItem;
|
50
src/components/carousel/carousel-item.vue
Normal file
50
src/components/carousel/carousel-item.vue
Normal file
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<div :class="prefixCls" :style="styles"><slot></slot></div>
|
||||
</template>
|
||||
<script>
|
||||
const prefixCls = 'ivu-carousel-item';
|
||||
|
||||
export default {
|
||||
componentName: 'carousel-item',
|
||||
name: 'CarouselItem',
|
||||
data () {
|
||||
return {
|
||||
prefixCls: prefixCls,
|
||||
width: 0,
|
||||
height: 'auto',
|
||||
left: 0
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
styles () {
|
||||
return {
|
||||
width: `${this.width}px`,
|
||||
height: `${this.height}`,
|
||||
left: `${this.left}px`
|
||||
};
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$parent.slotChange();
|
||||
},
|
||||
watch: {
|
||||
width (val) {
|
||||
if (val && this.$parent.loop) {
|
||||
this.$nextTick(() => {
|
||||
this.$parent.initCopyTrackDom();
|
||||
});
|
||||
}
|
||||
},
|
||||
height (val) {
|
||||
if (val && this.$parent.loop) {
|
||||
this.$nextTick(() => {
|
||||
this.$parent.initCopyTrackDom();
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$parent.slotChange();
|
||||
}
|
||||
};
|
||||
</script>
|
328
src/components/carousel/carousel.vue
Normal file
328
src/components/carousel/carousel.vue
Normal file
|
@ -0,0 +1,328 @@
|
|||
<template>
|
||||
<div :class="classes">
|
||||
<button type="button" :class="arrowClasses" class="left" @click="arrowEvent(-1)">
|
||||
<Icon type="ios-arrow-back"></Icon>
|
||||
</button>
|
||||
<div :class="[prefixCls + '-list']">
|
||||
<div :class="[prefixCls + '-track', showCopyTrack ? '' : 'higher']" :style="trackStyles" ref="originTrack">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div :class="[prefixCls + '-track', showCopyTrack ? 'higher' : '']" :style="copyTrackStyles" ref="copyTrack" v-if="loop">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" :class="arrowClasses" class="right" @click="arrowEvent(1)">
|
||||
<Icon type="ios-arrow-forward"></Icon>
|
||||
</button>
|
||||
<ul :class="dotsClasses">
|
||||
<template v-for="n in slides.length">
|
||||
<li :class="[n - 1 === currentIndex ? prefixCls + '-active' : '']"
|
||||
@click="dotsEvent('click', n - 1)"
|
||||
@mouseover="dotsEvent('hover', n - 1)">
|
||||
<button type="button" :class="[radiusDot ? 'radius' : '']"></button>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Icon from '../icon/icon.vue';
|
||||
import { getStyle, oneOf } from '../../utils/assist';
|
||||
import { on, off } from '../../utils/dom';
|
||||
|
||||
const prefixCls = 'ivu-carousel';
|
||||
|
||||
export default {
|
||||
name: 'Carousel',
|
||||
components: { Icon },
|
||||
props: {
|
||||
arrow: {
|
||||
type: String,
|
||||
default: 'hover',
|
||||
validator (value) {
|
||||
return oneOf(value, ['hover', 'always', 'never']);
|
||||
}
|
||||
},
|
||||
autoplay: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
autoplaySpeed: {
|
||||
type: Number,
|
||||
default: 2000
|
||||
},
|
||||
loop: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
easing: {
|
||||
type: String,
|
||||
default: 'ease'
|
||||
},
|
||||
dots: {
|
||||
type: String,
|
||||
default: 'inside',
|
||||
validator (value) {
|
||||
return oneOf(value, ['inside', 'outside', 'none']);
|
||||
}
|
||||
},
|
||||
radiusDot: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
trigger: {
|
||||
type: String,
|
||||
default: 'click',
|
||||
validator (value) {
|
||||
return oneOf(value, ['click', 'hover']);
|
||||
}
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
height: {
|
||||
type: [String, Number],
|
||||
default: 'auto',
|
||||
validator (value) {
|
||||
return value === 'auto' || Object.prototype.toString.call(value) === '[object Number]';
|
||||
}
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
prefixCls: prefixCls,
|
||||
listWidth: 0,
|
||||
trackWidth: 0,
|
||||
trackOffset: 0,
|
||||
trackCopyOffset: 0,
|
||||
showCopyTrack: false,
|
||||
slides: [],
|
||||
slideInstances: [],
|
||||
timer: null,
|
||||
ready: false,
|
||||
currentIndex: this.value,
|
||||
trackIndex: this.value,
|
||||
copyTrackIndex: this.value,
|
||||
hideTrackPos: -1, // 默认左滑
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
return [
|
||||
`${prefixCls}`
|
||||
];
|
||||
},
|
||||
trackStyles () {
|
||||
return {
|
||||
width: `${this.trackWidth}px`,
|
||||
transform: `translate3d(${-this.trackOffset}px, 0px, 0px)`,
|
||||
transition: `transform 500ms ${this.easing}`
|
||||
};
|
||||
},
|
||||
copyTrackStyles () {
|
||||
return {
|
||||
width: `${this.trackWidth}px`,
|
||||
transform: `translate3d(${-this.trackCopyOffset}px, 0px, 0px)`,
|
||||
transition: `transform 500ms ${this.easing}`,
|
||||
position: 'absolute',
|
||||
top: 0
|
||||
};
|
||||
},
|
||||
arrowClasses () {
|
||||
return [
|
||||
`${prefixCls}-arrow`,
|
||||
`${prefixCls}-arrow-${this.arrow}`
|
||||
];
|
||||
},
|
||||
dotsClasses () {
|
||||
return [
|
||||
`${prefixCls}-dots`,
|
||||
`${prefixCls}-dots-${this.dots}`
|
||||
];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// find option component
|
||||
findChild (cb) {
|
||||
const find = function (child) {
|
||||
const name = child.$options.componentName;
|
||||
|
||||
if (name) {
|
||||
cb(child);
|
||||
} else if (child.$children.length) {
|
||||
child.$children.forEach((innerChild) => {
|
||||
find(innerChild, cb);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (this.slideInstances.length || !this.$children) {
|
||||
this.slideInstances.forEach((child) => {
|
||||
find(child);
|
||||
});
|
||||
} else {
|
||||
this.$children.forEach((child) => {
|
||||
find(child);
|
||||
});
|
||||
}
|
||||
},
|
||||
// copy trackDom
|
||||
initCopyTrackDom () {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.copyTrack.innerHTML = this.$refs.originTrack.innerHTML;
|
||||
});
|
||||
},
|
||||
updateSlides (init) {
|
||||
let slides = [];
|
||||
let index = 1;
|
||||
|
||||
this.findChild((child) => {
|
||||
slides.push({
|
||||
$el: child.$el
|
||||
});
|
||||
child.index = index++;
|
||||
|
||||
if (init) {
|
||||
this.slideInstances.push(child);
|
||||
}
|
||||
});
|
||||
|
||||
this.slides = slides;
|
||||
this.updatePos();
|
||||
},
|
||||
updatePos () {
|
||||
this.findChild((child) => {
|
||||
child.width = this.listWidth;
|
||||
child.height = typeof this.height === 'number' ? `${this.height}px` : this.height;
|
||||
});
|
||||
|
||||
this.trackWidth = (this.slides.length || 0) * this.listWidth;
|
||||
},
|
||||
// use when slot changed
|
||||
slotChange () {
|
||||
this.$nextTick(() => {
|
||||
this.slides = [];
|
||||
this.slideInstances = [];
|
||||
|
||||
this.updateSlides(true, true);
|
||||
this.updatePos();
|
||||
this.updateOffset();
|
||||
});
|
||||
},
|
||||
handleResize () {
|
||||
this.listWidth = parseInt(getStyle(this.$el, 'width'));
|
||||
this.updatePos();
|
||||
this.updateOffset();
|
||||
},
|
||||
updateTrackPos (index) {
|
||||
if (this.showCopyTrack) {
|
||||
this.trackIndex = index;
|
||||
} else {
|
||||
this.copyTrackIndex = index;
|
||||
}
|
||||
},
|
||||
updateTrackIndex (index) {
|
||||
if (this.showCopyTrack) {
|
||||
this.copyTrackIndex = index;
|
||||
} else {
|
||||
this.trackIndex = index;
|
||||
}
|
||||
this.currentIndex = index;
|
||||
},
|
||||
add (offset) {
|
||||
// 获取单个轨道的图片数
|
||||
let slidesLen = this.slides.length;
|
||||
// 如果是无缝滚动,需要初始化双轨道位置
|
||||
if (this.loop) {
|
||||
if (offset > 0) {
|
||||
// 初始化左滑轨道位置
|
||||
this.hideTrackPos = -1;
|
||||
} else {
|
||||
// 初始化右滑轨道位置
|
||||
this.hideTrackPos = slidesLen;
|
||||
}
|
||||
this.updateTrackPos(this.hideTrackPos);
|
||||
}
|
||||
// 获取当前展示图片的索引值
|
||||
const oldIndex = this.showCopyTrack ? this.copyTrackIndex : this.trackIndex;
|
||||
let index = oldIndex + offset;
|
||||
while (index < 0) index += slidesLen;
|
||||
if (((offset > 0 && index === slidesLen) || (offset < 0 && index === slidesLen - 1)) && this.loop) {
|
||||
// 极限值(左滑:当前索引为总图片张数, 右滑:当前索引为总图片张数 - 1)切换轨道
|
||||
this.showCopyTrack = !this.showCopyTrack;
|
||||
this.trackIndex += offset;
|
||||
this.copyTrackIndex += offset;
|
||||
} else {
|
||||
if (!this.loop) index = index % this.slides.length;
|
||||
this.updateTrackIndex(index);
|
||||
}
|
||||
this.currentIndex = index === this.slides.length ? 0 : index;
|
||||
this.$emit('on-change', oldIndex, this.currentIndex);
|
||||
this.$emit('input', this.currentIndex);
|
||||
},
|
||||
arrowEvent (offset) {
|
||||
this.setAutoplay();
|
||||
this.add(offset);
|
||||
},
|
||||
dotsEvent (event, n) {
|
||||
let curIndex = this.showCopyTrack ? this.copyTrackIndex : this.trackIndex;
|
||||
if (event === this.trigger && curIndex !== n) {
|
||||
this.updateTrackIndex(n);
|
||||
this.$emit('input', n);
|
||||
// Reset autoplay timer when trigger be activated
|
||||
this.setAutoplay();
|
||||
}
|
||||
},
|
||||
setAutoplay () {
|
||||
window.clearInterval(this.timer);
|
||||
if (this.autoplay) {
|
||||
this.timer = window.setInterval(() => {
|
||||
this.add(1);
|
||||
}, this.autoplaySpeed);
|
||||
}
|
||||
},
|
||||
updateOffset () {
|
||||
this.$nextTick(() => {
|
||||
/* hack: revise copyTrack offset (1px) */
|
||||
let ofs = this.copyTrackIndex > 0 ? -1 : 1;
|
||||
this.trackOffset = this.trackIndex * this.listWidth;
|
||||
this.trackCopyOffset = this.copyTrackIndex * this.listWidth + ofs;
|
||||
});
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
autoplay () {
|
||||
this.setAutoplay();
|
||||
},
|
||||
autoplaySpeed () {
|
||||
this.setAutoplay();
|
||||
},
|
||||
trackIndex () {
|
||||
this.updateOffset();
|
||||
},
|
||||
copyTrackIndex () {
|
||||
this.updateOffset();
|
||||
},
|
||||
height () {
|
||||
this.updatePos();
|
||||
},
|
||||
value (val) {
|
||||
// this.currentIndex = val;
|
||||
// this.trackIndex = val;
|
||||
this.updateTrackIndex(val);
|
||||
this.setAutoplay();
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.updateSlides(true);
|
||||
this.handleResize();
|
||||
this.setAutoplay();
|
||||
// window.addEventListener('resize', this.handleResize, false);
|
||||
on(window, 'resize', this.handleResize);
|
||||
},
|
||||
beforeDestroy () {
|
||||
// window.removeEventListener('resize', this.handleResize, false);
|
||||
off(window, 'resize', this.handleResize);
|
||||
}
|
||||
};
|
||||
</script>
|
5
src/components/carousel/index.js
Normal file
5
src/components/carousel/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import Carousel from './carousel.vue';
|
||||
import CarouselItem from './carousel-item.vue';
|
||||
|
||||
Carousel.Item = CarouselItem;
|
||||
export default Carousel;
|
444
src/components/cascader/cascader.vue
Normal file
444
src/components/cascader/cascader.vue
Normal file
|
@ -0,0 +1,444 @@
|
|||
<template>
|
||||
<div :class="classes" v-click-outside="handleClose">
|
||||
<div :class="[prefixCls + '-rel']" @click="toggleOpen" ref="reference">
|
||||
<input type="hidden" :name="name" :value="currentValue">
|
||||
<slot>
|
||||
<i-input
|
||||
:element-id="elementId"
|
||||
ref="input"
|
||||
:readonly="!filterable"
|
||||
:disabled="disabled"
|
||||
:value="displayInputRender"
|
||||
@on-change="handleInput"
|
||||
:size="size"
|
||||
:placeholder="inputPlaceholder"></i-input>
|
||||
<div
|
||||
:class="[prefixCls + '-label']"
|
||||
v-show="filterable && query === ''"
|
||||
@click="handleFocus">{{ displayRender }}</div>
|
||||
<Icon type="ios-close-circle" :class="[prefixCls + '-arrow']" v-show="showCloseIcon" @click.native.stop="clearSelect"></Icon>
|
||||
<Icon :type="arrowType" :custom="customArrowType" :size="arrowSize" :class="[prefixCls + '-arrow']"></Icon>
|
||||
</slot>
|
||||
</div>
|
||||
<transition name="transition-drop">
|
||||
<Drop
|
||||
v-show="visible"
|
||||
:class="{ [prefixCls + '-transfer']: transfer }"
|
||||
ref="drop"
|
||||
:data-transfer="transfer"
|
||||
:transfer="transfer"
|
||||
v-transfer-dom>
|
||||
<div>
|
||||
<Caspanel
|
||||
v-show="!filterable || (filterable && query === '')"
|
||||
ref="caspanel"
|
||||
:prefix-cls="prefixCls"
|
||||
:data="data"
|
||||
:disabled="disabled"
|
||||
:change-on-select="changeOnSelect"
|
||||
:trigger="trigger"></Caspanel>
|
||||
<div :class="[prefixCls + '-dropdown']" v-show="filterable && query !== '' && querySelections.length">
|
||||
<ul :class="[selectPrefixCls + '-dropdown-list']">
|
||||
<li
|
||||
:class="[selectPrefixCls + '-item', {
|
||||
[selectPrefixCls + '-item-disabled']: item.disabled
|
||||
}]"
|
||||
v-for="(item, index) in querySelections"
|
||||
@click="handleSelectItem(index)" v-html="item.display"></li>
|
||||
</ul>
|
||||
</div>
|
||||
<ul v-show="(filterable && query !== '' && !querySelections.length) || !data.length" :class="[prefixCls + '-not-found-tip']"><li>{{ localeNotFoundText }}</li></ul>
|
||||
</div>
|
||||
</Drop>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import iInput from '../input/input.vue';
|
||||
import Drop from '../select/dropdown.vue';
|
||||
import Icon from '../icon/icon.vue';
|
||||
import Caspanel from './caspanel.vue';
|
||||
import {directive as clickOutside} from 'v-click-outside-x';
|
||||
import TransferDom from '../../directives/transfer-dom';
|
||||
import { oneOf } from '../../utils/assist';
|
||||
import Emitter from '../../mixins/emitter';
|
||||
import Locale from '../../mixins/locale';
|
||||
|
||||
const prefixCls = 'ivu-cascader';
|
||||
const selectPrefixCls = 'ivu-select';
|
||||
|
||||
export default {
|
||||
name: 'Cascader',
|
||||
mixins: [ Emitter, Locale ],
|
||||
components: { iInput, Drop, Icon, Caspanel },
|
||||
directives: { clickOutside, TransferDom },
|
||||
props: {
|
||||
data: {
|
||||
type: Array,
|
||||
default () {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
default () {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
clearable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
placeholder: {
|
||||
type: String
|
||||
},
|
||||
size: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['small', 'large', 'default']);
|
||||
},
|
||||
default () {
|
||||
return !this.$IVIEW || this.$IVIEW.size === '' ? 'default' : this.$IVIEW.size;
|
||||
}
|
||||
},
|
||||
trigger: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['click', 'hover']);
|
||||
},
|
||||
default: 'click'
|
||||
},
|
||||
changeOnSelect: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
renderFormat: {
|
||||
type: Function,
|
||||
default (label) {
|
||||
return label.join(' / ');
|
||||
}
|
||||
},
|
||||
loadData: {
|
||||
type: Function
|
||||
},
|
||||
filterable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
notFoundText: {
|
||||
type: String
|
||||
},
|
||||
transfer: {
|
||||
type: Boolean,
|
||||
default () {
|
||||
return !this.$IVIEW || this.$IVIEW.transfer === '' ? false : this.$IVIEW.transfer;
|
||||
}
|
||||
},
|
||||
name: {
|
||||
type: String
|
||||
},
|
||||
elementId: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
prefixCls: prefixCls,
|
||||
selectPrefixCls: selectPrefixCls,
|
||||
visible: false,
|
||||
selected: [],
|
||||
tmpSelected: [],
|
||||
updatingValue: false, // to fix set value in changeOnSelect type
|
||||
currentValue: this.value,
|
||||
query: '',
|
||||
validDataStr: '',
|
||||
isLoadedChildren: false // #950
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
return [
|
||||
`${prefixCls}`,
|
||||
{
|
||||
[`${prefixCls}-show-clear`]: this.showCloseIcon,
|
||||
[`${prefixCls}-size-${this.size}`]: !!this.size,
|
||||
[`${prefixCls}-visible`]: this.visible,
|
||||
[`${prefixCls}-disabled`]: this.disabled,
|
||||
[`${prefixCls}-not-found`]: this.filterable && this.query !== '' && !this.querySelections.length
|
||||
}
|
||||
];
|
||||
},
|
||||
showCloseIcon () {
|
||||
return this.currentValue && this.currentValue.length && this.clearable && !this.disabled;
|
||||
},
|
||||
displayRender () {
|
||||
let label = [];
|
||||
for (let i = 0; i < this.selected.length; i++) {
|
||||
label.push(this.selected[i].label);
|
||||
}
|
||||
|
||||
return this.renderFormat(label, this.selected);
|
||||
},
|
||||
displayInputRender () {
|
||||
return this.filterable ? '' : this.displayRender;
|
||||
},
|
||||
localePlaceholder () {
|
||||
if (this.placeholder === undefined) {
|
||||
return this.t('i.select.placeholder');
|
||||
} else {
|
||||
return this.placeholder;
|
||||
}
|
||||
},
|
||||
inputPlaceholder () {
|
||||
return this.filterable && this.currentValue.length ? null : this.localePlaceholder;
|
||||
},
|
||||
localeNotFoundText () {
|
||||
if (this.notFoundText === undefined) {
|
||||
return this.t('i.select.noMatch');
|
||||
} else {
|
||||
return this.notFoundText;
|
||||
}
|
||||
},
|
||||
querySelections () {
|
||||
let selections = [];
|
||||
function getSelections (arr, label, value) {
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
let item = arr[i];
|
||||
item.__label = label ? label + ' / ' + item.label : item.label;
|
||||
item.__value = value ? value + ',' + item.value : item.value;
|
||||
|
||||
if (item.children && item.children.length) {
|
||||
getSelections(item.children, item.__label, item.__value);
|
||||
delete item.__label;
|
||||
delete item.__value;
|
||||
} else {
|
||||
selections.push({
|
||||
label: item.__label,
|
||||
value: item.__value,
|
||||
display: item.__label,
|
||||
item: item,
|
||||
disabled: !!item.disabled
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
getSelections(this.data);
|
||||
selections = selections.filter(item => {
|
||||
return item.label ? item.label.indexOf(this.query) > -1 : false;
|
||||
}).map(item => {
|
||||
item.display = item.display.replace(new RegExp(this.query, 'g'), `<span>${this.query}</span>`);
|
||||
return item;
|
||||
});
|
||||
return selections;
|
||||
},
|
||||
// 3.4.0, global setting customArrow 有值时,arrow 赋值空
|
||||
arrowType () {
|
||||
let type = 'ios-arrow-down';
|
||||
|
||||
if (this.$IVIEW) {
|
||||
if (this.$IVIEW.cascader.customArrow) {
|
||||
type = '';
|
||||
} else if (this.$IVIEW.cascader.arrow) {
|
||||
type = this.$IVIEW.cascader.arrow;
|
||||
}
|
||||
}
|
||||
return type;
|
||||
},
|
||||
// 3.4.0, global setting
|
||||
customArrowType () {
|
||||
let type = '';
|
||||
|
||||
if (this.$IVIEW) {
|
||||
if (this.$IVIEW.cascader.customArrow) {
|
||||
type = this.$IVIEW.cascader.customArrow;
|
||||
}
|
||||
}
|
||||
return type;
|
||||
},
|
||||
// 3.4.0, global setting
|
||||
arrowSize () {
|
||||
let size = '';
|
||||
|
||||
if (this.$IVIEW) {
|
||||
if (this.$IVIEW.cascader.arrowSize) {
|
||||
size = this.$IVIEW.cascader.arrowSize;
|
||||
}
|
||||
}
|
||||
return size;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clearSelect () {
|
||||
if (this.disabled) return false;
|
||||
const oldVal = JSON.stringify(this.currentValue);
|
||||
this.currentValue = this.selected = this.tmpSelected = [];
|
||||
this.handleClose();
|
||||
this.emitValue(this.currentValue, oldVal);
|
||||
// this.$broadcast('on-clear');
|
||||
this.broadcast('Caspanel', 'on-clear');
|
||||
},
|
||||
handleClose () {
|
||||
this.visible = false;
|
||||
},
|
||||
toggleOpen () {
|
||||
if (this.disabled) return false;
|
||||
if (this.visible) {
|
||||
if (!this.filterable) this.handleClose();
|
||||
} else {
|
||||
this.onFocus();
|
||||
}
|
||||
},
|
||||
onFocus () {
|
||||
this.visible = true;
|
||||
if (!this.currentValue.length) {
|
||||
this.broadcast('Caspanel', 'on-clear');
|
||||
}
|
||||
},
|
||||
updateResult (result) {
|
||||
this.tmpSelected = result;
|
||||
},
|
||||
updateSelected (init = false, changeOnSelectDataChange = false) {
|
||||
// #2793 changeOnSelectDataChange used for changeOnSelect when data changed and set value
|
||||
if (!this.changeOnSelect || init || changeOnSelectDataChange) {
|
||||
this.broadcast('Caspanel', 'on-find-selected', {
|
||||
value: this.currentValue
|
||||
});
|
||||
}
|
||||
},
|
||||
emitValue (val, oldVal) {
|
||||
if (JSON.stringify(val) !== oldVal) {
|
||||
this.$emit('on-change', this.currentValue, JSON.parse(JSON.stringify(this.selected)));
|
||||
this.$nextTick(() => {
|
||||
this.dispatch('FormItem', 'on-form-change', {
|
||||
value: this.currentValue,
|
||||
selected: JSON.parse(JSON.stringify(this.selected))
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
handleInput (event) {
|
||||
this.query = event.target.value;
|
||||
},
|
||||
handleSelectItem (index) {
|
||||
const item = this.querySelections[index];
|
||||
|
||||
if (item.item.disabled) return false;
|
||||
this.query = '';
|
||||
this.$refs.input.currentValue = '';
|
||||
const oldVal = JSON.stringify(this.currentValue);
|
||||
this.currentValue = item.value.split(',');
|
||||
// use setTimeout for #4786, can not use nextTick, because @on-find-selected use nextTick
|
||||
setTimeout(() => {
|
||||
this.emitValue(this.currentValue, oldVal);
|
||||
this.handleClose();
|
||||
}, 0);
|
||||
},
|
||||
handleFocus () {
|
||||
this.$refs.input.focus();
|
||||
},
|
||||
// 排除 loading 后的 data,避免重复触发 updateSelect
|
||||
getValidData (data) {
|
||||
function deleteData (item) {
|
||||
const new_item = Object.assign({}, item);
|
||||
if ('loading' in new_item) {
|
||||
delete new_item.loading;
|
||||
}
|
||||
if ('__value' in new_item) {
|
||||
delete new_item.__value;
|
||||
}
|
||||
if ('__label' in new_item) {
|
||||
delete new_item.__label;
|
||||
}
|
||||
if ('children' in new_item && new_item.children.length) {
|
||||
new_item.children = new_item.children.map(i => deleteData(i));
|
||||
}
|
||||
return new_item;
|
||||
}
|
||||
|
||||
return data.map(item => deleteData(item));
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.validDataStr = JSON.stringify(this.getValidData(this.data));
|
||||
this.$on('on-result-change', (params) => {
|
||||
// lastValue: is click the final val
|
||||
// fromInit: is this emit from update value
|
||||
const lastValue = params.lastValue;
|
||||
const changeOnSelect = params.changeOnSelect;
|
||||
const fromInit = params.fromInit;
|
||||
|
||||
if (lastValue || changeOnSelect) {
|
||||
const oldVal = JSON.stringify(this.currentValue);
|
||||
this.selected = this.tmpSelected;
|
||||
|
||||
let newVal = [];
|
||||
this.selected.forEach((item) => {
|
||||
newVal.push(item.value);
|
||||
});
|
||||
|
||||
if (!fromInit) {
|
||||
this.updatingValue = true;
|
||||
this.currentValue = newVal;
|
||||
this.emitValue(this.currentValue, oldVal);
|
||||
}
|
||||
}
|
||||
if (lastValue && !fromInit) {
|
||||
this.handleClose();
|
||||
}
|
||||
});
|
||||
},
|
||||
mounted () {
|
||||
this.updateSelected(true);
|
||||
},
|
||||
watch: {
|
||||
visible (val) {
|
||||
if (val) {
|
||||
if (this.currentValue.length) {
|
||||
this.updateSelected();
|
||||
}
|
||||
if (this.transfer) {
|
||||
this.$refs.drop.update();
|
||||
}
|
||||
this.broadcast('Drop', 'on-update-popper');
|
||||
} else {
|
||||
if (this.filterable) {
|
||||
this.query = '';
|
||||
this.$refs.input.currentValue = '';
|
||||
}
|
||||
if (this.transfer) {
|
||||
this.$refs.drop.destroy();
|
||||
}
|
||||
this.broadcast('Drop', 'on-destroy-popper');
|
||||
}
|
||||
this.$emit('on-visible-change', val);
|
||||
},
|
||||
value (val) {
|
||||
this.currentValue = val;
|
||||
if (!val.length) this.selected = [];
|
||||
},
|
||||
currentValue () {
|
||||
this.$emit('input', this.currentValue);
|
||||
if (this.updatingValue) {
|
||||
this.updatingValue = false;
|
||||
return;
|
||||
}
|
||||
this.updateSelected(true);
|
||||
},
|
||||
data: {
|
||||
deep: true,
|
||||
handler () {
|
||||
const validDataStr = JSON.stringify(this.getValidData(this.data));
|
||||
if (validDataStr !== this.validDataStr) {
|
||||
this.validDataStr = validDataStr;
|
||||
if (!this.isLoadedChildren) {
|
||||
this.$nextTick(() => this.updateSelected(false, this.changeOnSelect));
|
||||
}
|
||||
this.isLoadedChildren = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
72
src/components/cascader/casitem.vue
Normal file
72
src/components/cascader/casitem.vue
Normal file
|
@ -0,0 +1,72 @@
|
|||
<template>
|
||||
<li :class="classes">
|
||||
{{ data.label }}
|
||||
<Icon :type="arrowType" :custom="customArrowType" :size="arrowSize" v-if="showArrow" />
|
||||
<i v-if="showLoading" class="ivu-icon ivu-icon-ios-loading ivu-load-loop ivu-cascader-menu-item-loading"></i>
|
||||
</li>
|
||||
</template>
|
||||
<script>
|
||||
import Icon from '../icon/icon.vue';
|
||||
|
||||
export default {
|
||||
name: 'Casitem',
|
||||
components: { Icon },
|
||||
props: {
|
||||
data: Object,
|
||||
prefixCls: String,
|
||||
tmpItem: Object
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
return [
|
||||
`${this.prefixCls}-menu-item`,
|
||||
{
|
||||
[`${this.prefixCls}-menu-item-active`]: this.tmpItem.value === this.data.value,
|
||||
[`${this.prefixCls}-menu-item-disabled`]: this.data.disabled
|
||||
}
|
||||
];
|
||||
},
|
||||
showArrow () {
|
||||
return (this.data.children && this.data.children.length) || ('loading' in this.data && !this.data.loading);
|
||||
},
|
||||
showLoading () {
|
||||
return 'loading' in this.data && this.data.loading;
|
||||
},
|
||||
// 3.4.0, global setting customArrow 有值时,arrow 赋值空
|
||||
arrowType () {
|
||||
let type = 'ios-arrow-forward';
|
||||
|
||||
if (this.$IVIEW) {
|
||||
if (this.$IVIEW.cascader.customItemArrow) {
|
||||
type = '';
|
||||
} else if (this.$IVIEW.cascader.itemArrow) {
|
||||
type = this.$IVIEW.cascader.itemArrow;
|
||||
}
|
||||
}
|
||||
return type;
|
||||
},
|
||||
// 3.4.0, global setting
|
||||
customArrowType () {
|
||||
let type = '';
|
||||
|
||||
if (this.$IVIEW) {
|
||||
if (this.$IVIEW.cascader.customItemArrow) {
|
||||
type = this.$IVIEW.cascader.customItemArrow;
|
||||
}
|
||||
}
|
||||
return type;
|
||||
},
|
||||
// 3.4.0, global setting
|
||||
arrowSize () {
|
||||
let size = '';
|
||||
|
||||
if (this.$IVIEW) {
|
||||
if (this.$IVIEW.cascader.itemArrowSize) {
|
||||
size = this.$IVIEW.cascader.itemArrowSize;
|
||||
}
|
||||
}
|
||||
return size;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
173
src/components/cascader/caspanel.vue
Normal file
173
src/components/cascader/caspanel.vue
Normal file
|
@ -0,0 +1,173 @@
|
|||
<template>
|
||||
<span>
|
||||
<ul v-if="data && data.length" :class="[prefixCls + '-menu']">
|
||||
<Casitem
|
||||
v-for="item in data"
|
||||
:key="getKey()"
|
||||
:prefix-cls="prefixCls"
|
||||
:data="item"
|
||||
:tmp-item="tmpItem"
|
||||
@click.native.stop="handleClickItem(item)"
|
||||
@mouseenter.native.stop="handleHoverItem(item)"></Casitem>
|
||||
</ul><Caspanel v-if="sublist && sublist.length" :prefix-cls="prefixCls" :data="sublist" :disabled="disabled" :trigger="trigger" :change-on-select="changeOnSelect"></Caspanel>
|
||||
</span>
|
||||
</template>
|
||||
<script>
|
||||
import Casitem from './casitem.vue';
|
||||
import Emitter from '../../mixins/emitter';
|
||||
import { findComponentUpward, findComponentDownward } from '../../utils/assist';
|
||||
|
||||
let key = 1;
|
||||
|
||||
export default {
|
||||
name: 'Caspanel',
|
||||
mixins: [ Emitter ],
|
||||
components: { Casitem },
|
||||
props: {
|
||||
data: {
|
||||
type: Array,
|
||||
default () {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
disabled: Boolean,
|
||||
changeOnSelect: Boolean,
|
||||
trigger: String,
|
||||
prefixCls: String
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
tmpItem: {},
|
||||
result: [],
|
||||
sublist: []
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
data () {
|
||||
this.sublist = [];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClickItem (item) {
|
||||
if (this.trigger !== 'click' && item.children && item.children.length) return; // #1922
|
||||
this.handleTriggerItem(item, false, true);
|
||||
},
|
||||
handleHoverItem (item) {
|
||||
if (this.trigger !== 'hover' || !item.children || !item.children.length) return; // #1922
|
||||
this.handleTriggerItem(item, false, true);
|
||||
},
|
||||
handleTriggerItem (item, fromInit = false, fromUser = false) {
|
||||
if (item.disabled) return;
|
||||
|
||||
const cascader = findComponentUpward(this, 'Cascader');
|
||||
if (item.loading !== undefined && !item.children.length) {
|
||||
if (cascader && cascader.loadData) {
|
||||
cascader.loadData(item, () => {
|
||||
// todo
|
||||
if (fromUser) {
|
||||
cascader.isLoadedChildren = true;
|
||||
}
|
||||
if (item.children.length) {
|
||||
this.handleTriggerItem(item);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// return value back recursion // 向上递归,设置临时选中值(并非真实选中)
|
||||
const backItem = this.getBaseItem(item);
|
||||
// #5021 for this.changeOnSelect,加 if 是因为 #4472
|
||||
if (
|
||||
this.changeOnSelect ||
|
||||
(backItem.label !== this.tmpItem.label || backItem.value !== this.tmpItem.value) ||
|
||||
(backItem.label === this.tmpItem.label && backItem.value === this.tmpItem.value)
|
||||
) {
|
||||
this.tmpItem = backItem;
|
||||
this.emitUpdate([backItem]);
|
||||
}
|
||||
|
||||
if (item.children && item.children.length){
|
||||
this.sublist = item.children;
|
||||
this.dispatch('Cascader', 'on-result-change', {
|
||||
lastValue: false,
|
||||
changeOnSelect: this.changeOnSelect,
|
||||
fromInit: fromInit
|
||||
});
|
||||
|
||||
// #1553
|
||||
if (this.changeOnSelect) {
|
||||
const Caspanel = findComponentDownward(this, 'Caspanel');
|
||||
if (Caspanel) {
|
||||
Caspanel.$emit('on-clear', true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.sublist = [];
|
||||
this.dispatch('Cascader', 'on-result-change', {
|
||||
lastValue: true,
|
||||
changeOnSelect: this.changeOnSelect,
|
||||
fromInit: fromInit
|
||||
});
|
||||
}
|
||||
|
||||
if (cascader) {
|
||||
cascader.$refs.drop.update();
|
||||
}
|
||||
},
|
||||
updateResult (item) {
|
||||
this.result = [this.tmpItem].concat(item);
|
||||
this.emitUpdate(this.result);
|
||||
},
|
||||
getBaseItem (item) {
|
||||
let backItem = Object.assign({}, item);
|
||||
if (backItem.children) {
|
||||
delete backItem.children;
|
||||
}
|
||||
|
||||
return backItem;
|
||||
},
|
||||
emitUpdate (result) {
|
||||
if (this.$parent.$options.name === 'Caspanel') {
|
||||
this.$parent.updateResult(result);
|
||||
} else {
|
||||
this.$parent.$parent.updateResult(result);
|
||||
}
|
||||
},
|
||||
getKey () {
|
||||
return key++;
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$on('on-find-selected', (params) => {
|
||||
const val = params.value;
|
||||
let value = [...val];
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
for (let j = 0; j < this.data.length; j++) {
|
||||
if (value[i] === this.data[j].value) {
|
||||
this.handleTriggerItem(this.data[j], true);
|
||||
value.splice(0, 1);
|
||||
this.$nextTick(() => {
|
||||
this.broadcast('Caspanel', 'on-find-selected', {
|
||||
value: value
|
||||
});
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// deep for #1553
|
||||
this.$on('on-clear', (deep = false) => {
|
||||
this.sublist = [];
|
||||
this.tmpItem = {};
|
||||
if (deep) {
|
||||
const Caspanel = findComponentDownward(this, 'Caspanel');
|
||||
if (Caspanel) {
|
||||
Caspanel.$emit('on-clear', true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
2
src/components/cascader/index.js
Normal file
2
src/components/cascader/index.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
import Cascader from './cascader.vue';
|
||||
export default Cascader;
|
2
src/components/cell-group/index.js
Normal file
2
src/components/cell-group/index.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
import CellGroup from '../cell/cell-group.vue';
|
||||
export default CellGroup;
|
20
src/components/cell/cell-group.vue
Normal file
20
src/components/cell/cell-group.vue
Normal file
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<div class="ivu-cell-group">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'CellGroup',
|
||||
provide () {
|
||||
return {
|
||||
cellGroup: this
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleClick (name) {
|
||||
this.$emit('on-click', name);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
32
src/components/cell/cell-item.vue
Normal file
32
src/components/cell/cell-item.vue
Normal file
|
@ -0,0 +1,32 @@
|
|||
<template>
|
||||
<div class="ivu-cell-item">
|
||||
<div class="ivu-cell-icon">
|
||||
<slot name="icon"></slot>
|
||||
</div>
|
||||
<div class="ivu-cell-main">
|
||||
<div class="ivu-cell-title"><slot>{{ title }}</slot></div>
|
||||
<div class="ivu-cell-label"><slot name="label">{{ label }}</slot></div>
|
||||
</div>
|
||||
<div class="ivu-cell-footer">
|
||||
<span class="ivu-cell-extra"><slot name="extra">{{ extra }}</slot></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
extra: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
130
src/components/cell/cell.vue
Normal file
130
src/components/cell/cell.vue
Normal file
|
@ -0,0 +1,130 @@
|
|||
<template>
|
||||
<div :class="classes">
|
||||
<a
|
||||
v-if="to"
|
||||
:href="linkUrl"
|
||||
:target="target"
|
||||
class="ivu-cell-link"
|
||||
@click.exact="handleClickItem($event, false)"
|
||||
@click.ctrl="handleClickItem($event, true)"
|
||||
@click.meta="handleClickItem($event, true)">
|
||||
<CellItem :title="title" :label="label" :extra="extra">
|
||||
<slot name="icon" slot="icon"></slot>
|
||||
<slot slot="default"></slot>
|
||||
<slot name="extra" slot="extra"></slot>
|
||||
<slot name="label" slot="label"></slot>
|
||||
</CellItem>
|
||||
</a>
|
||||
<div class="ivu-cell-link" v-else @click="handleClickItem">
|
||||
<CellItem :title="title" :label="label" :extra="extra">
|
||||
<slot name="icon" slot="icon"></slot>
|
||||
<slot slot="default"></slot>
|
||||
<slot name="extra" slot="extra"></slot>
|
||||
<slot name="label" slot="label"></slot>
|
||||
</CellItem>
|
||||
</div>
|
||||
<div class="ivu-cell-arrow" v-if="to">
|
||||
<slot name="arrow">
|
||||
<Icon :type="arrowType" :custom="customArrowType" :size="arrowSize" />
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import CellItem from './cell-item.vue';
|
||||
import Icon from '../icon/icon.vue';
|
||||
import mixinsLink from '../../mixins/link';
|
||||
|
||||
const prefixCls = 'ivu-cell';
|
||||
|
||||
export default {
|
||||
name: 'Cell',
|
||||
inject: ['cellGroup'],
|
||||
mixins: [ mixinsLink ],
|
||||
components: { CellItem, Icon },
|
||||
props: {
|
||||
name: {
|
||||
type: [String, Number]
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
extra: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
prefixCls: prefixCls
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
return [
|
||||
`${prefixCls}`,
|
||||
{
|
||||
[`${prefixCls}-disabled`]: this.disabled,
|
||||
[`${prefixCls}-selected`]: this.selected,
|
||||
[`${prefixCls}-with-link`]: this.to
|
||||
}
|
||||
];
|
||||
},
|
||||
// 3.4.0, global setting customArrow 有值时,arrow 赋值空
|
||||
arrowType () {
|
||||
let type = 'ios-arrow-forward';
|
||||
|
||||
if (this.$IVIEW) {
|
||||
if (this.$IVIEW.cell.customArrow) {
|
||||
type = '';
|
||||
} else if (this.$IVIEW.cell.arrow) {
|
||||
type = this.$IVIEW.cell.arrow;
|
||||
}
|
||||
}
|
||||
return type;
|
||||
},
|
||||
// 3.4.0, global setting
|
||||
customArrowType () {
|
||||
let type = '';
|
||||
|
||||
if (this.$IVIEW) {
|
||||
if (this.$IVIEW.cell.customArrow) {
|
||||
type = this.$IVIEW.cell.customArrow;
|
||||
}
|
||||
}
|
||||
return type;
|
||||
},
|
||||
// 3.4.0, global setting
|
||||
arrowSize () {
|
||||
let size = '';
|
||||
|
||||
if (this.$IVIEW) {
|
||||
if (this.$IVIEW.cell.arrowSize) {
|
||||
size = this.$IVIEW.cell.arrowSize;
|
||||
}
|
||||
}
|
||||
return size;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClickItem (event, new_window) {
|
||||
this.cellGroup.handleClick(this.name);
|
||||
|
||||
this.handleCheckClick(event, new_window);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
5
src/components/cell/index.js
Normal file
5
src/components/cell/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import Cell from './cell.vue';
|
||||
import CellGroup from './cell-group.vue';
|
||||
|
||||
Cell.Group = CellGroup;
|
||||
export default Cell;
|
3
src/components/checkbox-group/index.js
Normal file
3
src/components/checkbox-group/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import CheckboxGroup from '../checkbox/checkbox-group.vue';
|
||||
|
||||
export default CheckboxGroup;
|
78
src/components/checkbox/checkbox-group.vue
Normal file
78
src/components/checkbox/checkbox-group.vue
Normal file
|
@ -0,0 +1,78 @@
|
|||
<template>
|
||||
<div :class="classes">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { findComponentsDownward, oneOf } from '../../utils/assist';
|
||||
import Emitter from '../../mixins/emitter';
|
||||
|
||||
const prefixCls = 'ivu-checkbox-group';
|
||||
|
||||
export default {
|
||||
name: 'CheckboxGroup',
|
||||
mixins: [ Emitter ],
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default () {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
size: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['small', 'large', 'default']);
|
||||
},
|
||||
default () {
|
||||
return !this.$IVIEW || this.$IVIEW.size === '' ? 'default' : this.$IVIEW.size;
|
||||
}
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
currentValue: this.value,
|
||||
childrens: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
return [
|
||||
`${prefixCls}`,
|
||||
{
|
||||
[`ivu-checkbox-${this.size}`]: !!this.size
|
||||
}
|
||||
];
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.updateModel(true);
|
||||
},
|
||||
methods: {
|
||||
updateModel (update) {
|
||||
this.childrens = findComponentsDownward(this, 'Checkbox');
|
||||
if (this.childrens) {
|
||||
const { value } = this;
|
||||
this.childrens.forEach(child => {
|
||||
child.model = value;
|
||||
|
||||
if (update) {
|
||||
child.currentValue = value.indexOf(child.label) >= 0;
|
||||
child.group = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
change (data) {
|
||||
this.currentValue = data;
|
||||
this.$emit('input', data);
|
||||
this.$emit('on-change', data);
|
||||
this.dispatch('FormItem', 'on-form-change', data);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value () {
|
||||
this.updateModel(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
171
src/components/checkbox/checkbox.vue
Normal file
171
src/components/checkbox/checkbox.vue
Normal file
|
@ -0,0 +1,171 @@
|
|||
<template>
|
||||
<label :class="wrapClasses">
|
||||
<span :class="checkboxClasses">
|
||||
<span :class="innerClasses"></span>
|
||||
<input
|
||||
v-if="group"
|
||||
type="checkbox"
|
||||
:class="inputClasses"
|
||||
:disabled="disabled"
|
||||
:value="label"
|
||||
v-model="model"
|
||||
:name="name"
|
||||
@change="change"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur">
|
||||
<input
|
||||
v-else
|
||||
type="checkbox"
|
||||
:class="inputClasses"
|
||||
:disabled="disabled"
|
||||
:checked="currentValue"
|
||||
:name="name"
|
||||
@change="change"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur">
|
||||
</span>
|
||||
<slot><span v-if="showSlot">{{ label }}</span></slot>
|
||||
</label>
|
||||
</template>
|
||||
<script>
|
||||
import { findComponentUpward, oneOf } from '../../utils/assist';
|
||||
import Emitter from '../../mixins/emitter';
|
||||
|
||||
const prefixCls = 'ivu-checkbox';
|
||||
|
||||
export default {
|
||||
name: 'Checkbox',
|
||||
mixins: [ Emitter ],
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
value: {
|
||||
type: [String, Number, Boolean],
|
||||
default: false
|
||||
},
|
||||
trueValue: {
|
||||
type: [String, Number, Boolean],
|
||||
default: true
|
||||
},
|
||||
falseValue: {
|
||||
type: [String, Number, Boolean],
|
||||
default: false
|
||||
},
|
||||
label: {
|
||||
type: [String, Number, Boolean]
|
||||
},
|
||||
indeterminate: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
size: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['small', 'large', 'default']);
|
||||
},
|
||||
default () {
|
||||
return !this.$IVIEW || this.$IVIEW.size === '' ? 'default' : this.$IVIEW.size;
|
||||
}
|
||||
},
|
||||
name: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
model: [],
|
||||
currentValue: this.value,
|
||||
group: false,
|
||||
showSlot: true,
|
||||
parent: findComponentUpward(this, 'CheckboxGroup'),
|
||||
focusInner: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
wrapClasses () {
|
||||
return [
|
||||
`${prefixCls}-wrapper`,
|
||||
{
|
||||
[`${prefixCls}-group-item`]: this.group,
|
||||
[`${prefixCls}-wrapper-checked`]: this.currentValue,
|
||||
[`${prefixCls}-wrapper-disabled`]: this.disabled,
|
||||
[`${prefixCls}-${this.size}`]: !!this.size
|
||||
}
|
||||
];
|
||||
},
|
||||
checkboxClasses () {
|
||||
return [
|
||||
`${prefixCls}`,
|
||||
{
|
||||
[`${prefixCls}-checked`]: this.currentValue,
|
||||
[`${prefixCls}-disabled`]: this.disabled,
|
||||
[`${prefixCls}-indeterminate`]: this.indeterminate
|
||||
}
|
||||
];
|
||||
},
|
||||
innerClasses () {
|
||||
return [
|
||||
`${prefixCls}-inner`,
|
||||
{
|
||||
[`${prefixCls}-focus`]: this.focusInner
|
||||
}
|
||||
];
|
||||
},
|
||||
inputClasses () {
|
||||
return `${prefixCls}-input`;
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.parent = findComponentUpward(this, 'CheckboxGroup');
|
||||
if (this.parent) {
|
||||
this.group = true;
|
||||
}
|
||||
|
||||
if (this.group) {
|
||||
this.parent.updateModel(true);
|
||||
} else {
|
||||
this.updateModel();
|
||||
this.showSlot = this.$slots.default !== undefined;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
change (event) {
|
||||
if (this.disabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const checked = event.target.checked;
|
||||
this.currentValue = checked;
|
||||
|
||||
const value = checked ? this.trueValue : this.falseValue;
|
||||
this.$emit('input', value);
|
||||
|
||||
if (this.group) {
|
||||
this.parent.change(this.model);
|
||||
} else {
|
||||
this.$emit('on-change', value);
|
||||
this.dispatch('FormItem', 'on-form-change', value);
|
||||
}
|
||||
},
|
||||
updateModel () {
|
||||
this.currentValue = this.value === this.trueValue;
|
||||
},
|
||||
onBlur () {
|
||||
this.focusInner = false;
|
||||
},
|
||||
onFocus () {
|
||||
this.focusInner = true;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value (val) {
|
||||
if (val === this.trueValue || val === this.falseValue) {
|
||||
this.updateModel();
|
||||
} else {
|
||||
throw 'Value should be trueValue or falseValue.';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
5
src/components/checkbox/index.js
Normal file
5
src/components/checkbox/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import Checkbox from './checkbox.vue';
|
||||
import CheckboxGroup from './checkbox-group.vue';
|
||||
|
||||
Checkbox.Group = CheckboxGroup;
|
||||
export default Checkbox;
|
118
src/components/circle/circle.vue
Normal file
118
src/components/circle/circle.vue
Normal file
|
@ -0,0 +1,118 @@
|
|||
<template>
|
||||
<div :style="circleSize" :class="wrapClasses">
|
||||
<svg viewBox="0 0 100 100">
|
||||
<path :d="pathString" :stroke="trailColor" :stroke-width="trailWidth" :fill-opacity="0" :style="trailStyle" />
|
||||
<path :d="pathString" :stroke-linecap="strokeLinecap" :stroke="strokeColor" :stroke-width="computedStrokeWidth" fill-opacity="0" :style="pathStyle" />
|
||||
</svg>
|
||||
<div :class="innerClasses">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { oneOf } from '../../utils/assist';
|
||||
|
||||
const prefixCls = 'ivu-chart-circle';
|
||||
|
||||
export default {
|
||||
name: 'iCircle',
|
||||
props: {
|
||||
percent: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 120
|
||||
},
|
||||
strokeWidth: {
|
||||
type: Number,
|
||||
default: 6
|
||||
},
|
||||
strokeColor: {
|
||||
type: String,
|
||||
default: '#2d8cf0'
|
||||
},
|
||||
strokeLinecap: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['square', 'round']);
|
||||
},
|
||||
default: 'round'
|
||||
},
|
||||
trailWidth: {
|
||||
type: Number,
|
||||
default: 5
|
||||
},
|
||||
trailColor: {
|
||||
type: String,
|
||||
default: '#eaeef2'
|
||||
},
|
||||
dashboard: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
circleSize () {
|
||||
return {
|
||||
width: `${this.size}px`,
|
||||
height: `${this.size}px`
|
||||
};
|
||||
},
|
||||
computedStrokeWidth () {
|
||||
return this.percent === 0 && this.dashboard ? 0 : this.strokeWidth;
|
||||
},
|
||||
radius () {
|
||||
return 50 - this.strokeWidth / 2;
|
||||
},
|
||||
pathString () {
|
||||
if (this.dashboard) {
|
||||
return `M 50,50 m 0,${this.radius}
|
||||
a ${this.radius},${this.radius} 0 1 1 0,-${2 * this.radius}
|
||||
a ${this.radius},${this.radius} 0 1 1 0,${2 * this.radius}`;
|
||||
} else {
|
||||
return `M 50,50 m 0,-${this.radius}
|
||||
a ${this.radius},${this.radius} 0 1 1 0,${2 * this.radius}
|
||||
a ${this.radius},${this.radius} 0 1 1 0,-${2 * this.radius}`;
|
||||
}
|
||||
},
|
||||
len () {
|
||||
return Math.PI * 2 * this.radius;
|
||||
},
|
||||
trailStyle () {
|
||||
let style = {};
|
||||
if (this.dashboard) {
|
||||
style = {
|
||||
'stroke-dasharray': `${this.len - 75}px ${this.len}px`,
|
||||
'stroke-dashoffset': `-${75 / 2}px`,
|
||||
'transition': 'stroke-dashoffset .3s ease 0s, stroke-dasharray .3s ease 0s, stroke .3s'
|
||||
};
|
||||
}
|
||||
return style;
|
||||
},
|
||||
pathStyle () {
|
||||
let style = {};
|
||||
if (this.dashboard) {
|
||||
style = {
|
||||
'stroke-dasharray': `${(this.percent / 100) * (this.len - 75)}px ${this.len}px`,
|
||||
'stroke-dashoffset': `-${75 / 2}px`,
|
||||
'transition': 'stroke-dashoffset .3s ease 0s, stroke-dasharray .6s ease 0s, stroke .6s, stroke-width .06s ease .6s'
|
||||
};
|
||||
} else {
|
||||
style = {
|
||||
'stroke-dasharray': `${this.len}px ${this.len}px`,
|
||||
'stroke-dashoffset': `${((100 - this.percent) / 100 * this.len)}px`,
|
||||
'transition': 'stroke-dashoffset 0.6s ease 0s, stroke 0.6s ease'
|
||||
};
|
||||
}
|
||||
return style;
|
||||
},
|
||||
wrapClasses () {
|
||||
return `${prefixCls}`;
|
||||
},
|
||||
innerClasses () {
|
||||
return `${prefixCls}-inner`;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
2
src/components/circle/index.js
Normal file
2
src/components/circle/index.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
import Circle from './circle.vue';
|
||||
export default Circle;
|
3
src/components/col/index.js
Normal file
3
src/components/col/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import Col from '../grid/col.vue';
|
||||
|
||||
export default Col;
|
110
src/components/collapse/collapse.vue
Normal file
110
src/components/collapse/collapse.vue
Normal file
|
@ -0,0 +1,110 @@
|
|||
<template>
|
||||
<div :class="classes">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
const prefixCls = 'ivu-collapse';
|
||||
|
||||
export default {
|
||||
name: 'Collapse',
|
||||
props: {
|
||||
accordion: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
value: {
|
||||
type: [Array, String]
|
||||
},
|
||||
simple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
currentValue: this.value
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
return [
|
||||
`${prefixCls}`,
|
||||
{
|
||||
[`${prefixCls}-simple`]: this.simple
|
||||
}
|
||||
];
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.setActive();
|
||||
},
|
||||
methods: {
|
||||
setActive () {
|
||||
const activeKey = this.getActiveKey();
|
||||
|
||||
this.$children.forEach((child, index) => {
|
||||
const name = child.name || index.toString();
|
||||
|
||||
child.isActive = activeKey.indexOf(name) > -1;
|
||||
child.index = index;
|
||||
});
|
||||
},
|
||||
getActiveKey () {
|
||||
let activeKey = this.currentValue || [];
|
||||
const accordion = this.accordion;
|
||||
|
||||
if (!Array.isArray(activeKey)) {
|
||||
activeKey = [activeKey];
|
||||
}
|
||||
|
||||
if (accordion && activeKey.length > 1) {
|
||||
activeKey = [activeKey[0]];
|
||||
}
|
||||
|
||||
for (let i = 0; i < activeKey.length; i++) {
|
||||
activeKey[i] = activeKey[i].toString();
|
||||
}
|
||||
|
||||
return activeKey;
|
||||
},
|
||||
toggle (data) {
|
||||
const name = data.name.toString();
|
||||
let newActiveKey = [];
|
||||
|
||||
if (this.accordion) {
|
||||
if (!data.isActive) {
|
||||
newActiveKey.push(name);
|
||||
}
|
||||
} else {
|
||||
let activeKey = this.getActiveKey();
|
||||
const nameIndex = activeKey.indexOf(name);
|
||||
|
||||
if (data.isActive) {
|
||||
if (nameIndex > -1) {
|
||||
activeKey.splice(nameIndex, 1);
|
||||
}
|
||||
} else {
|
||||
if (nameIndex < 0) {
|
||||
activeKey.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
newActiveKey = activeKey;
|
||||
}
|
||||
|
||||
this.currentValue = newActiveKey;
|
||||
this.$emit('input', newActiveKey);
|
||||
this.$emit('on-change', newActiveKey);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value (val) {
|
||||
this.currentValue = val;
|
||||
},
|
||||
currentValue () {
|
||||
this.setActive();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
5
src/components/collapse/index.js
Normal file
5
src/components/collapse/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import Collapse from './collapse.vue';
|
||||
import Panel from './panel.vue';
|
||||
|
||||
Collapse.Panel = Panel;
|
||||
export default Collapse;
|
69
src/components/collapse/panel.vue
Normal file
69
src/components/collapse/panel.vue
Normal file
|
@ -0,0 +1,69 @@
|
|||
<template>
|
||||
<div :class="itemClasses">
|
||||
<div :class="headerClasses" @click="toggle">
|
||||
<Icon type="ios-arrow-forward" v-if="!hideArrow"></Icon>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<collapse-transition v-if="mounted">
|
||||
<div :class="contentClasses" v-show="isActive">
|
||||
<div :class="boxClasses"><slot name="content"></slot></div>
|
||||
</div>
|
||||
</collapse-transition>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Icon from '../icon/icon.vue';
|
||||
import CollapseTransition from '../base/collapse-transition';
|
||||
const prefixCls = 'ivu-collapse';
|
||||
|
||||
export default {
|
||||
name: 'Panel',
|
||||
components: { Icon, CollapseTransition },
|
||||
props: {
|
||||
name: {
|
||||
type: String
|
||||
},
|
||||
hideArrow: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
index: 0, // use index for default when name is null
|
||||
isActive: false,
|
||||
mounted: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
itemClasses () {
|
||||
return [
|
||||
`${prefixCls}-item`,
|
||||
{
|
||||
[`${prefixCls}-item-active`]: this.isActive
|
||||
}
|
||||
];
|
||||
},
|
||||
headerClasses () {
|
||||
return `${prefixCls}-header`;
|
||||
},
|
||||
contentClasses () {
|
||||
return `${prefixCls}-content`;
|
||||
},
|
||||
boxClasses () {
|
||||
return `${prefixCls}-content-box`;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggle () {
|
||||
this.$parent.toggle({
|
||||
name: this.name || this.index,
|
||||
isActive: this.isActive
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.mounted = true;
|
||||
}
|
||||
};
|
||||
</script>
|
103
src/components/color-picker/alpha.vue
Normal file
103
src/components/color-picker/alpha.vue
Normal file
|
@ -0,0 +1,103 @@
|
|||
<template>
|
||||
<div
|
||||
:class="[prefixCls + '-alpha']"
|
||||
tabindex="0"
|
||||
@click="$el.focus()"
|
||||
@keydown.esc="handleEscape"
|
||||
@keydown.left="handleLeft"
|
||||
@keydown.right="handleRight"
|
||||
@keydown.up="handleUp"
|
||||
@keydown.down="handleDown"
|
||||
>
|
||||
<div :class="[prefixCls + '-alpha-checkboard-wrap']">
|
||||
<div :class="[prefixCls + '-alpha-checkerboard']"></div>
|
||||
</div>
|
||||
<div
|
||||
:style="gradientStyle"
|
||||
:class="[prefixCls + '-alpha-gradient']"></div>
|
||||
<div
|
||||
ref="container"
|
||||
:class="[prefixCls + '-alpha-container']"
|
||||
@mousedown="handleMouseDown"
|
||||
@touchmove="handleChange"
|
||||
@touchstart="handleChange">
|
||||
<div
|
||||
:style="{top: 0, left: `${value.a * 100}%`}"
|
||||
:class="[prefixCls + '-alpha-pointer']">
|
||||
<div :class="[prefixCls + '-alpha-picker']"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HSAMixin from './hsaMixin';
|
||||
import Prefixes from './prefixMixin';
|
||||
import {clamp, toRGBAString} from './utils';
|
||||
|
||||
export default {
|
||||
name: 'Alpha',
|
||||
|
||||
mixins: [HSAMixin, Prefixes],
|
||||
|
||||
data() {
|
||||
const normalStep = 1;
|
||||
const jumpStep = 10;
|
||||
|
||||
return {
|
||||
left: -normalStep,
|
||||
right: normalStep,
|
||||
up: jumpStep,
|
||||
down: -jumpStep,
|
||||
powerKey: 'shiftKey',
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
gradientStyle() {
|
||||
const {r, g, b} = this.value.rgba;
|
||||
const start = toRGBAString({r, g, b, a: 0});
|
||||
const finish = toRGBAString({r, g, b, a: 1});
|
||||
|
||||
return {background: `linear-gradient(to right, ${start} 0%, ${finish} 100%)`};
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
change(newAlpha) {
|
||||
const {h, s, l} = this.value.hsl;
|
||||
const {a} = this.value;
|
||||
|
||||
if (a !== newAlpha) {
|
||||
this.$emit('change', {h, s, l, a: newAlpha, source: 'rgba'});
|
||||
}
|
||||
},
|
||||
handleSlide(e, direction) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.change(clamp(e[this.powerKey] ? direction : Math.round(this.value.hsl.a * 100 + direction) / 100, 0, 1));
|
||||
},
|
||||
handleChange(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const left = this.getLeft(e);
|
||||
|
||||
if (left < 0) {
|
||||
this.change(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const {clientWidth} = this.$refs.container;
|
||||
|
||||
if (left > clientWidth) {
|
||||
this.change(1);
|
||||
return;
|
||||
}
|
||||
|
||||
this.change(Math.round(left * 100 / clientWidth) / 100);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
512
src/components/color-picker/color-picker.vue
Normal file
512
src/components/color-picker/color-picker.vue
Normal file
|
@ -0,0 +1,512 @@
|
|||
<template>
|
||||
<div
|
||||
v-click-outside="handleClose"
|
||||
:class="classes">
|
||||
<div
|
||||
ref="reference"
|
||||
:class="wrapClasses"
|
||||
@click="toggleVisible">
|
||||
<input
|
||||
:name="name"
|
||||
:value="currentValue"
|
||||
type="hidden">
|
||||
<Icon :type="arrowType" :custom="customArrowType" :size="arrowSize" :class="arrowClasses"></Icon>
|
||||
<div
|
||||
ref="input"
|
||||
:tabindex="disabled ? undefined : 0"
|
||||
:class="inputClasses"
|
||||
@keydown.tab="onTab"
|
||||
@keydown.esc="onEscape"
|
||||
@keydown.up="onArrow"
|
||||
@keydown.down="onArrow"
|
||||
>
|
||||
<div :class="[prefixCls + '-color']">
|
||||
<div
|
||||
v-show="value === '' && !visible"
|
||||
:class="[prefixCls + '-color-empty']">
|
||||
<i :class="[iconPrefixCls, iconPrefixCls + '-ios-close']"></i>
|
||||
</div>
|
||||
<div
|
||||
v-show="value || visible"
|
||||
:style="displayedColorStyle"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<transition name="transition-drop">
|
||||
<Drop
|
||||
v-transfer-dom
|
||||
v-show="visible"
|
||||
ref="drop"
|
||||
:placement="placement"
|
||||
:data-transfer="transfer"
|
||||
:transfer="transfer"
|
||||
:class="dropClasses"
|
||||
>
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="visible"
|
||||
:class="[prefixCls + '-picker']">
|
||||
<div :class="[prefixCls + '-picker-wrapper']">
|
||||
<div :class="[prefixCls + '-picker-panel']">
|
||||
<Saturation
|
||||
ref="saturation"
|
||||
v-model="saturationColors"
|
||||
:focused="visible"
|
||||
@change="childChange"
|
||||
@keydown.native.tab="handleFirstTab"
|
||||
></Saturation>
|
||||
</div>
|
||||
<div
|
||||
v-if="hue"
|
||||
:class="[prefixCls + '-picker-hue-slider']">
|
||||
<Hue
|
||||
v-model="saturationColors"
|
||||
@change="childChange"></Hue>
|
||||
</div>
|
||||
<div
|
||||
v-if="alpha"
|
||||
:class="[prefixCls + '-picker-alpha-slider']">
|
||||
<Alpha
|
||||
v-model="saturationColors"
|
||||
@change="childChange"></Alpha>
|
||||
</div>
|
||||
<recommend-colors
|
||||
v-if="colors.length"
|
||||
:list="colors"
|
||||
:class="[prefixCls + '-picker-colors']"
|
||||
@picker-color="handleSelectColor"></recommend-colors>
|
||||
<recommend-colors
|
||||
v-if="!colors.length && recommend"
|
||||
:list="recommendedColor"
|
||||
:class="[prefixCls + '-picker-colors']"
|
||||
@picker-color="handleSelectColor"></recommend-colors>
|
||||
</div>
|
||||
<div :class="[prefixCls + '-confirm']">
|
||||
<span :class="confirmColorClasses">
|
||||
<template v-if="editable">
|
||||
<i-input :value="formatColor" size="small" @on-enter="handleEditColor" @on-blur="handleEditColor"></i-input>
|
||||
</template>
|
||||
<template v-else>{{formatColor}}</template>
|
||||
</span>
|
||||
<i-button
|
||||
ref="clear"
|
||||
:tabindex="0"
|
||||
size="small"
|
||||
@click.native="handleClear"
|
||||
@keydown.enter="handleClear"
|
||||
@keydown.native.esc="closer"
|
||||
>{{t('i.datepicker.clear')}}</i-button>
|
||||
<i-button
|
||||
ref="ok"
|
||||
:tabindex="0"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click.native="handleSuccess"
|
||||
@keydown.native.tab="handleLastTab"
|
||||
@keydown.enter="handleSuccess"
|
||||
@keydown.native.esc="closer"
|
||||
>{{t('i.datepicker.ok')}}</i-button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</Drop>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tinycolor from 'tinycolor2';
|
||||
import {directive as clickOutside} from 'v-click-outside-x';
|
||||
import TransferDom from '../../directives/transfer-dom';
|
||||
import Drop from '../../components/select/dropdown.vue';
|
||||
import RecommendColors from './recommend-colors.vue';
|
||||
import Saturation from './saturation.vue';
|
||||
import Hue from './hue.vue';
|
||||
import Alpha from './alpha.vue';
|
||||
import iInput from '../input/input.vue';
|
||||
import iButton from '../button/button.vue';
|
||||
import Icon from '../icon/icon.vue';
|
||||
import Locale from '../../mixins/locale';
|
||||
import {oneOf} from '../../utils/assist';
|
||||
import Emitter from '../../mixins/emitter';
|
||||
import Prefixes from './prefixMixin';
|
||||
import {changeColor, toRGBAString} from './utils';
|
||||
|
||||
export default {
|
||||
name: 'ColorPicker',
|
||||
|
||||
components: {Drop, RecommendColors, Saturation, Hue, Alpha, iInput, iButton, Icon},
|
||||
|
||||
directives: {clickOutside, TransferDom},
|
||||
|
||||
mixins: [Emitter, Locale, Prefixes],
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
hue: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
alpha: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
recommend: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
format: {
|
||||
type: String,
|
||||
validator(value) {
|
||||
return oneOf(value, ['hsl', 'hsv', 'hex', 'rgb']);
|
||||
},
|
||||
default: undefined,
|
||||
},
|
||||
colors: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
size: {
|
||||
validator(value) {
|
||||
return oneOf(value, ['small', 'large', 'default']);
|
||||
},
|
||||
default () {
|
||||
return !this.$IVIEW || this.$IVIEW.size === '' ? 'default' : this.$IVIEW.size;
|
||||
}
|
||||
},
|
||||
hideDropDown: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
placement: {
|
||||
type: String,
|
||||
validator(value) {
|
||||
return oneOf(value, [
|
||||
'top',
|
||||
'top-start',
|
||||
'top-end',
|
||||
'bottom',
|
||||
'bottom-start',
|
||||
'bottom-end',
|
||||
'left',
|
||||
'left-start',
|
||||
'left-end',
|
||||
'right',
|
||||
'right-start',
|
||||
'right-end',
|
||||
]);
|
||||
},
|
||||
default: 'bottom',
|
||||
},
|
||||
transfer: {
|
||||
type: Boolean,
|
||||
default () {
|
||||
return !this.$IVIEW || this.$IVIEW.transfer === '' ? false : this.$IVIEW.transfer;
|
||||
}
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
val: changeColor(this.value),
|
||||
currentValue: this.value,
|
||||
dragging: false,
|
||||
visible: false,
|
||||
recommendedColor: [
|
||||
'#2d8cf0',
|
||||
'#19be6b',
|
||||
'#ff9900',
|
||||
'#ed4014',
|
||||
'#00b5ff',
|
||||
'#19c919',
|
||||
'#f9e31c',
|
||||
'#ea1a1a',
|
||||
'#9b1dea',
|
||||
'#00c2b1',
|
||||
'#ac7a33',
|
||||
'#1d35ea',
|
||||
'#8bc34a',
|
||||
'#f16b62',
|
||||
'#ea4ca3',
|
||||
'#0d94aa',
|
||||
'#febd79',
|
||||
'#5d4037',
|
||||
'#00bcd4',
|
||||
'#f06292',
|
||||
'#cddc39',
|
||||
'#607d8b',
|
||||
'#000000',
|
||||
'#ffffff',
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
arrowClasses() {
|
||||
return [
|
||||
`${this.inputPrefixCls}-icon`,
|
||||
`${this.inputPrefixCls}-icon-normal`,
|
||||
];
|
||||
},
|
||||
transition() {
|
||||
return oneOf(this.placement, ['bottom-start', 'bottom', 'bottom-end']) ? 'slide-up' : 'fade';
|
||||
},
|
||||
saturationColors: {
|
||||
get() {
|
||||
return this.val;
|
||||
},
|
||||
set(newVal) {
|
||||
this.val = newVal;
|
||||
this.$emit('on-active-change', this.formatColor);
|
||||
},
|
||||
},
|
||||
classes() {
|
||||
return [
|
||||
`${this.prefixCls}`,
|
||||
{
|
||||
[`${this.prefixCls}-transfer`]: this.transfer,
|
||||
},
|
||||
];
|
||||
},
|
||||
wrapClasses() {
|
||||
return [
|
||||
`${this.prefixCls}-rel`,
|
||||
`${this.prefixCls}-${this.size}`,
|
||||
`${this.inputPrefixCls}-wrapper`,
|
||||
`${this.inputPrefixCls}-wrapper-${this.size}`,
|
||||
{
|
||||
[`${this.prefixCls}-disabled`]: this.disabled,
|
||||
},
|
||||
];
|
||||
},
|
||||
inputClasses() {
|
||||
return [
|
||||
`${this.prefixCls}-input`,
|
||||
`${this.inputPrefixCls}`,
|
||||
`${this.inputPrefixCls}-${this.size}`,
|
||||
{
|
||||
[`${this.prefixCls}-focused`]: this.visible,
|
||||
[`${this.prefixCls}-disabled`]: this.disabled,
|
||||
},
|
||||
];
|
||||
},
|
||||
dropClasses() {
|
||||
return [
|
||||
`${this.transferPrefixCls}-no-max-height`,
|
||||
{
|
||||
[`${this.prefixCls}-transfer`]: this.transfer,
|
||||
[`${this.prefixCls}-hide-drop`]: this.hideDropDown,
|
||||
},
|
||||
];
|
||||
},
|
||||
displayedColorStyle() {
|
||||
return {backgroundColor: toRGBAString(this.visible ? this.saturationColors.rgba : tinycolor(this.value).toRgb())};
|
||||
},
|
||||
formatColor() {
|
||||
const {format, saturationColors} = this;
|
||||
|
||||
if (format) {
|
||||
if (format === 'hsl') {
|
||||
return tinycolor(saturationColors.hsl).toHslString();
|
||||
}
|
||||
|
||||
if (format === 'hsv') {
|
||||
return tinycolor(saturationColors.hsv).toHsvString();
|
||||
}
|
||||
|
||||
if (format === 'hex') {
|
||||
return saturationColors.hex;
|
||||
}
|
||||
|
||||
if (format === 'rgb') {
|
||||
return toRGBAString(saturationColors.rgba);
|
||||
}
|
||||
} else if (this.alpha) {
|
||||
return toRGBAString(saturationColors.rgba);
|
||||
}
|
||||
|
||||
return saturationColors.hex;
|
||||
},
|
||||
confirmColorClasses () {
|
||||
return [
|
||||
`${this.prefixCls}-confirm-color`,
|
||||
{
|
||||
[`${this.prefixCls}-confirm-color-editable`]: this.editable
|
||||
}
|
||||
];
|
||||
},
|
||||
// 3.4.0, global setting customArrow 有值时,arrow 赋值空
|
||||
arrowType () {
|
||||
let type = 'ios-arrow-down';
|
||||
|
||||
if (this.$IVIEW) {
|
||||
if (this.$IVIEW.colorPicker.customArrow) {
|
||||
type = '';
|
||||
} else if (this.$IVIEW.colorPicker.arrow) {
|
||||
type = this.$IVIEW.colorPicker.arrow;
|
||||
}
|
||||
}
|
||||
return type;
|
||||
},
|
||||
// 3.4.0, global setting
|
||||
customArrowType () {
|
||||
let type = '';
|
||||
|
||||
if (this.$IVIEW) {
|
||||
if (this.$IVIEW.colorPicker.customArrow) {
|
||||
type = this.$IVIEW.colorPicker.customArrow;
|
||||
}
|
||||
}
|
||||
return type;
|
||||
},
|
||||
// 3.4.0, global setting
|
||||
arrowSize () {
|
||||
let size = '';
|
||||
|
||||
if (this.$IVIEW) {
|
||||
if (this.$IVIEW.colorPicker.arrowSize) {
|
||||
size = this.$IVIEW.colorPicker.arrowSize;
|
||||
}
|
||||
}
|
||||
return size;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value(newVal) {
|
||||
this.val = changeColor(newVal);
|
||||
},
|
||||
visible(val) {
|
||||
this.val = changeColor(this.value);
|
||||
this.$refs.drop[val ? 'update' : 'destroy']();
|
||||
this.$emit('on-open-change', Boolean(val));
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$on('on-escape-keydown', this.closer);
|
||||
this.$on('on-dragging', this.setDragging);
|
||||
},
|
||||
|
||||
methods: {
|
||||
setDragging(value) {
|
||||
this.dragging = value;
|
||||
},
|
||||
handleClose(event) {
|
||||
if (this.visible) {
|
||||
if (this.dragging || event.type === 'mousedown') {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.transfer) {
|
||||
const {$el} = this.$refs.drop;
|
||||
if ($el === event.target || $el.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.closer(event);
|
||||
return;
|
||||
}
|
||||
|
||||
this.visible = false;
|
||||
},
|
||||
toggleVisible() {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.visible = !this.visible;
|
||||
this.$refs.input.focus();
|
||||
},
|
||||
childChange(data) {
|
||||
this.colorChange(data);
|
||||
},
|
||||
colorChange(data, oldHue) {
|
||||
this.oldHue = this.saturationColors.hsl.h;
|
||||
this.saturationColors = changeColor(data, oldHue || this.oldHue);
|
||||
},
|
||||
closer(event) {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
this.visible = false;
|
||||
this.$refs.input.focus();
|
||||
},
|
||||
handleButtons(event, value) {
|
||||
this.currentValue = value;
|
||||
this.$emit('input', value);
|
||||
this.$emit('on-change', value);
|
||||
this.dispatch('FormItem', 'on-form-change', value);
|
||||
this.closer(event);
|
||||
},
|
||||
handleSuccess(event) {
|
||||
this.handleButtons(event, this.formatColor);
|
||||
this.$emit('on-pick-success');
|
||||
},
|
||||
handleClear(event) {
|
||||
this.handleButtons(event, '');
|
||||
this.$emit('on-pick-clear');
|
||||
},
|
||||
handleSelectColor(color) {
|
||||
this.val = changeColor(color);
|
||||
this.$emit('on-active-change', this.formatColor);
|
||||
},
|
||||
handleEditColor (event) {
|
||||
const value = event.target.value;
|
||||
this.handleSelectColor(value);
|
||||
},
|
||||
handleFirstTab(event) {
|
||||
if (event.shiftKey) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.$refs.ok.$el.focus();
|
||||
}
|
||||
},
|
||||
handleLastTab(event) {
|
||||
if (!event.shiftKey) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.$refs.saturation.$el.focus();
|
||||
}
|
||||
},
|
||||
onTab(event) {
|
||||
if (this.visible) {
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
onEscape(event) {
|
||||
if (this.visible) {
|
||||
this.closer(event);
|
||||
}
|
||||
},
|
||||
onArrow(event) {
|
||||
if (!this.visible) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.visible = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
7
src/components/color-picker/handleEscapeMixin.js
Normal file
7
src/components/color-picker/handleEscapeMixin.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
methods: {
|
||||
handleEscape(e) {
|
||||
this.dispatch('ColorPicker', 'on-escape-keydown', e);
|
||||
},
|
||||
},
|
||||
};
|
78
src/components/color-picker/hsaMixin.js
Normal file
78
src/components/color-picker/hsaMixin.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
import Emitter from '../../mixins/emitter';
|
||||
import handleEscapeMixin from './handleEscapeMixin';
|
||||
import {getTouches} from './utils';
|
||||
import { on, off } from '../../utils/dom';
|
||||
|
||||
export default {
|
||||
mixins: [Emitter, handleEscapeMixin],
|
||||
|
||||
props: {
|
||||
focused: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
value: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.unbindEventListeners();
|
||||
},
|
||||
|
||||
created() {
|
||||
if (this.focused) {
|
||||
setTimeout(() => this.$el.focus(), 1);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleLeft(e) {
|
||||
this.handleSlide(e, this.left, 'left');
|
||||
},
|
||||
handleRight(e) {
|
||||
this.handleSlide(e, this.right, 'right');
|
||||
},
|
||||
handleUp(e) {
|
||||
this.handleSlide(e, this.up, 'up');
|
||||
},
|
||||
handleDown(e) {
|
||||
this.handleSlide(e, this.down, 'down');
|
||||
},
|
||||
handleMouseDown(e) {
|
||||
this.dispatch('ColorPicker', 'on-dragging', true);
|
||||
this.handleChange(e, true);
|
||||
// window.addEventListener('mousemove', this.handleChange, false);
|
||||
// window.addEventListener('mouseup', this.handleMouseUp, false);
|
||||
on(window, 'mousemove', this.handleChange);
|
||||
on(window, 'mouseup', this.handleMouseUp);
|
||||
},
|
||||
handleMouseUp() {
|
||||
this.unbindEventListeners();
|
||||
},
|
||||
unbindEventListeners() {
|
||||
// window.removeEventListener('mousemove', this.handleChange);
|
||||
// window.removeEventListener('mouseup', this.handleMouseUp);
|
||||
off(window, 'mousemove', this.handleChange);
|
||||
off(window, 'mouseup', this.handleMouseUp);
|
||||
// This timeout is required so that the click handler for click-outside
|
||||
// has the chance to run before the mouseup removes the dragging flag.
|
||||
setTimeout(() => this.dispatch('ColorPicker', 'on-dragging', false), 1);
|
||||
},
|
||||
getLeft(e) {
|
||||
const {container} = this.$refs;
|
||||
const xOffset = container.getBoundingClientRect().left + window.pageXOffset;
|
||||
const pageX = e.pageX || getTouches(e, 'PageX');
|
||||
|
||||
return pageX - xOffset;
|
||||
},
|
||||
getTop(e) {
|
||||
const {container} = this.$refs;
|
||||
const yOffset = container.getBoundingClientRect().top + window.pageYOffset;
|
||||
const pageY = e.pageY || getTouches(e, 'PageY');
|
||||
|
||||
return pageY - yOffset;
|
||||
},
|
||||
},
|
||||
};
|
101
src/components/color-picker/hue.vue
Normal file
101
src/components/color-picker/hue.vue
Normal file
|
@ -0,0 +1,101 @@
|
|||
<template>
|
||||
<div
|
||||
:class="[prefixCls + '-hue']"
|
||||
tabindex="0"
|
||||
@click="$el.focus()"
|
||||
@keydown.esc="handleEscape"
|
||||
@keydown.left="handleLeft"
|
||||
@keydown.right="handleRight"
|
||||
@keydown.up="handleUp"
|
||||
@keydown.down="handleDown"
|
||||
>
|
||||
<div
|
||||
ref="container"
|
||||
:class="[prefixCls + '-hue-container']"
|
||||
@mousedown="handleMouseDown"
|
||||
@touchmove="handleChange"
|
||||
@touchstart="handleChange">
|
||||
<div
|
||||
:style="{top: 0, left: `${percent}%`}"
|
||||
:class="[prefixCls + '-hue-pointer']">
|
||||
<div :class="[prefixCls + '-hue-picker']"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HASMixin from './hsaMixin';
|
||||
import Prefixes from './prefixMixin';
|
||||
import {clamp} from './utils';
|
||||
|
||||
export default {
|
||||
name: 'Hue',
|
||||
|
||||
mixins: [HASMixin, Prefixes],
|
||||
|
||||
data() {
|
||||
const normalStep = 1 / 360 * 25;
|
||||
const jumpStep = 20 * normalStep;
|
||||
|
||||
return {
|
||||
left: -normalStep,
|
||||
right: normalStep,
|
||||
up: jumpStep,
|
||||
down: -jumpStep,
|
||||
powerKey: 'shiftKey',
|
||||
percent: clamp(this.value.hsl.h * 100 / 360, 0, 100),
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
value () {
|
||||
this.percent = clamp(this.value.hsl.h * 100 / 360, 0, 100);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
change(percent) {
|
||||
this.percent = clamp(percent, 0, 100);
|
||||
|
||||
const {h, s, l, a} = this.value.hsl;
|
||||
const newHue = clamp(percent / 100 * 360, 0, 360);
|
||||
|
||||
if (h !== newHue) {
|
||||
this.$emit('change', {h: newHue, s, l, a, source: 'hsl'});
|
||||
}
|
||||
},
|
||||
handleSlide(e, direction) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (e[this.powerKey]) {
|
||||
this.change(direction < 0 ? 0 : 100);
|
||||
return;
|
||||
}
|
||||
|
||||
this.change(this.percent + direction);
|
||||
},
|
||||
handleChange(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const left = this.getLeft(e);
|
||||
|
||||
if (left < 0) {
|
||||
this.change(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const {clientWidth} = this.$refs.container;
|
||||
|
||||
if (left > clientWidth) {
|
||||
this.change(100);
|
||||
return;
|
||||
}
|
||||
|
||||
this.change(left * 100 / clientWidth);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
3
src/components/color-picker/index.js
Normal file
3
src/components/color-picker/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import ColorPicker from './color-picker.vue';
|
||||
|
||||
export default ColorPicker;
|
10
src/components/color-picker/prefixMixin.js
Normal file
10
src/components/color-picker/prefixMixin.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
export default {
|
||||
data() {
|
||||
return {
|
||||
prefixCls: 'ivu-color-picker',
|
||||
inputPrefixCls: 'ivu-input',
|
||||
iconPrefixCls: 'ivu-icon',
|
||||
transferPrefixCls: 'ivu-transfer',
|
||||
};
|
||||
},
|
||||
};
|
153
src/components/color-picker/recommend-colors.vue
Normal file
153
src/components/color-picker/recommend-colors.vue
Normal file
|
@ -0,0 +1,153 @@
|
|||
<template>
|
||||
<div
|
||||
ref="reference"
|
||||
tabindex="0"
|
||||
@click="handleClick"
|
||||
@keydown.esc="handleEscape"
|
||||
@keydown.enter="handleEnter"
|
||||
@keydown.left="handleArrow($event, 'x', left)"
|
||||
@keydown.right="handleArrow($event, 'x', right)"
|
||||
@keydown.up="handleArrow($event, 'y', up)"
|
||||
@keydown.down="handleArrow($event, 'y', down)"
|
||||
@blur="blurColor"
|
||||
@focus="focusColor"
|
||||
>
|
||||
<template v-for="(item, index) in list">
|
||||
<div
|
||||
:key="item + ':' + index"
|
||||
:class="[prefixCls + '-picker-colors-wrapper']">
|
||||
<div :data-color-id="index">
|
||||
<div
|
||||
:style="{background: item}"
|
||||
:class="[prefixCls + '-picker-colors-wrapper-color']"
|
||||
></div>
|
||||
<div
|
||||
:ref="'color-circle-' + index"
|
||||
:class="[prefixCls + '-picker-colors-wrapper-circle', hideClass]"></div>
|
||||
</div>
|
||||
</div>
|
||||
<br v-if="lineBreak(list, index)">
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Emitter from '../../mixins/emitter';
|
||||
import HandleEscapeMixin from './handleEscapeMixin';
|
||||
import Prefixes from './prefixMixin';
|
||||
import {clamp} from './utils';
|
||||
|
||||
export default {
|
||||
name: 'RecommendedColors',
|
||||
|
||||
mixins: [Emitter, HandleEscapeMixin, Prefixes],
|
||||
|
||||
props: {
|
||||
list: {
|
||||
type: Array,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
const columns = 12;
|
||||
const rows = Math.ceil(this.list.length / columns);
|
||||
const normalStep = 1;
|
||||
|
||||
return {
|
||||
left: -normalStep,
|
||||
right: normalStep,
|
||||
up: -normalStep,
|
||||
down: normalStep,
|
||||
powerKey: 'shiftKey',
|
||||
grid: {x: 1, y: 1},
|
||||
rows,
|
||||
columns,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
hideClass() {
|
||||
return `${this.prefixCls}-hide`;
|
||||
},
|
||||
linearIndex() {
|
||||
return this.getLinearIndex(this.grid);
|
||||
},
|
||||
currentCircle() {
|
||||
return this.$refs[`color-circle-${this.linearIndex}`][0];
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
getLinearIndex(grid) {
|
||||
return this.columns * (grid.y - 1) + grid.x - 1;
|
||||
},
|
||||
getMaxLimit(axis) {
|
||||
return axis === 'x' ? this.columns : this.rows;
|
||||
},
|
||||
handleArrow(e, axis, direction) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.blurColor();
|
||||
|
||||
const grid = {...this.grid};
|
||||
|
||||
if (e[this.powerKey]) {
|
||||
if (direction < 0) {
|
||||
grid[axis] = 1;
|
||||
} else {
|
||||
grid[axis] = this.getMaxLimit(axis);
|
||||
}
|
||||
} else {
|
||||
grid[axis] += direction;
|
||||
}
|
||||
|
||||
const index = this.getLinearIndex(grid);
|
||||
|
||||
if (index >= 0 && index < this.list.length) {
|
||||
this.grid[axis] = clamp(grid[axis], 1, this.getMaxLimit(axis));
|
||||
}
|
||||
|
||||
this.focusColor();
|
||||
},
|
||||
blurColor() {
|
||||
this.currentCircle.classList.add(this.hideClass);
|
||||
},
|
||||
focusColor() {
|
||||
this.currentCircle.classList.remove(this.hideClass);
|
||||
},
|
||||
handleEnter(e) {
|
||||
this.handleClick(e, this.currentCircle);
|
||||
},
|
||||
handleClick(e, circle) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.$refs.reference.focus();
|
||||
|
||||
const target = circle || e.target;
|
||||
const colorId = target.dataset.colorId || target.parentElement.dataset.colorId;
|
||||
|
||||
if (colorId) {
|
||||
this.blurColor();
|
||||
const id = Number(colorId) + 1;
|
||||
this.grid.x = id % this.columns || this.columns;
|
||||
this.grid.y = Math.ceil(id / this.columns);
|
||||
this.focusColor();
|
||||
this.$emit('picker-color', this.list[colorId]);
|
||||
this.$emit('change', {hex: this.list[colorId], source: 'hex'});
|
||||
}
|
||||
},
|
||||
lineBreak(list, index) {
|
||||
if (!index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const nextIndex = index + 1;
|
||||
|
||||
return nextIndex < list.length && nextIndex % this.columns === 0;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
101
src/components/color-picker/saturation.vue
Normal file
101
src/components/color-picker/saturation.vue
Normal file
|
@ -0,0 +1,101 @@
|
|||
<template>
|
||||
<div
|
||||
:class="[prefixCls + '-saturation-wrapper']"
|
||||
tabindex="0"
|
||||
@keydown.esc="handleEscape"
|
||||
@click="$el.focus()"
|
||||
@keydown.left="handleLeft"
|
||||
@keydown.right="handleRight"
|
||||
@keydown.up="handleUp"
|
||||
@keydown.down="handleDown"
|
||||
>
|
||||
<div
|
||||
ref="container"
|
||||
:style="bgColorStyle"
|
||||
:class="[prefixCls + '-saturation']"
|
||||
@mousedown="handleMouseDown">
|
||||
<div :class="[prefixCls + '-saturation--white']"></div>
|
||||
<div :class="[prefixCls + '-saturation--black']"></div>
|
||||
<div
|
||||
:style="pointerStyle"
|
||||
:class="[prefixCls + '-saturation-pointer']">
|
||||
<div :class="[prefixCls + '-saturation-circle']"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HSAMixin from './hsaMixin';
|
||||
import Prefixes from './prefixMixin';
|
||||
import {clamp, getIncrement} from './utils';
|
||||
import { on, off } from '../../utils/dom';
|
||||
|
||||
export default {
|
||||
name: 'Saturation',
|
||||
|
||||
mixins: [HSAMixin, Prefixes],
|
||||
|
||||
data() {
|
||||
const normalStep = 0.01;
|
||||
|
||||
return {
|
||||
left: -normalStep,
|
||||
right: normalStep,
|
||||
up: normalStep,
|
||||
down: -normalStep,
|
||||
multiplier: 10,
|
||||
powerKey: 'shiftKey',
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
bgColorStyle() {
|
||||
return {background: `hsl(${this.value.hsv.h}, 100%, 50%)`};
|
||||
},
|
||||
pointerStyle() {
|
||||
return {top: `${-(this.value.hsv.v * 100) + 1 + 100}%`, left: `${this.value.hsv.s * 100}%`};
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
change(h, s, v, a) {
|
||||
this.$emit('change', {h, s, v, a, source: 'hsva'});
|
||||
},
|
||||
handleSlide(e, direction, key) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const isPowerKey = e[this.powerKey];
|
||||
const increment = isPowerKey ? direction * this.multiplier : direction;
|
||||
const {h, s, v, a} = this.value.hsv;
|
||||
const saturation = clamp(s + getIncrement(key, ['left', 'right'], increment), 0, 1);
|
||||
const bright = clamp(v + getIncrement(key, ['up', 'down'], increment), 0, 1);
|
||||
|
||||
this.change(h, saturation, bright, a);
|
||||
},
|
||||
handleChange(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const {clientWidth, clientHeight} = this.$refs.container;
|
||||
const left = clamp(this.getLeft(e), 0, clientWidth);
|
||||
const top = clamp(this.getTop(e), 0, clientHeight);
|
||||
const saturation = left / clientWidth;
|
||||
const bright = clamp(1 - top / clientHeight, 0, 1);
|
||||
|
||||
this.change(this.value.hsv.h, saturation, bright, this.value.hsv.a);
|
||||
},
|
||||
handleMouseDown(e) {
|
||||
HSAMixin.methods.handleMouseDown.call(this, e);
|
||||
// window.addEventListener('mouseup', this.handleChange, false);
|
||||
on(window, 'mouseup', this.handleChange);
|
||||
},
|
||||
unbindEventListeners(e) {
|
||||
HSAMixin.methods.unbindEventListeners.call(this, e);
|
||||
// window.removeEventListener('mouseup', this.handleChange);
|
||||
off(window, 'mouseup', this.handleChange);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
118
src/components/color-picker/utils.js
Normal file
118
src/components/color-picker/utils.js
Normal file
|
@ -0,0 +1,118 @@
|
|||
import tinycolor from 'tinycolor2';
|
||||
import {oneOf} from '../../utils/assist';
|
||||
|
||||
function setAlpha(data, alpha) {
|
||||
const color = tinycolor(data);
|
||||
const {_a} = color;
|
||||
|
||||
if (_a === undefined || _a === null) {
|
||||
color.setAlpha(alpha || 1);
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
function getColor(data, colorData) {
|
||||
const alpha = colorData && colorData.a;
|
||||
|
||||
if (colorData) {
|
||||
// hsl is better than hex between conversions
|
||||
if (colorData.hsl) {
|
||||
return setAlpha(colorData.hsl, alpha);
|
||||
}
|
||||
|
||||
if (colorData.hex && colorData.hex.length > 0) {
|
||||
return setAlpha(colorData.hex, alpha);
|
||||
}
|
||||
}
|
||||
|
||||
return setAlpha(colorData, alpha);
|
||||
}
|
||||
|
||||
export function changeColor(data, oldHue) {
|
||||
const colorData = data === '' ? '#2d8cf0' : data;
|
||||
const color = getColor(data, colorData);
|
||||
const hsl = color.toHsl();
|
||||
const hsv = color.toHsv();
|
||||
|
||||
if (hsl.s === 0) {
|
||||
hsl.h = colorData.h || (colorData.hsl && colorData.hsl.h) || oldHue || 0;
|
||||
hsv.h = hsl.h;
|
||||
}
|
||||
|
||||
// when the hsv.v is less than 0.0164 (base on test)
|
||||
// because of possible loss of precision
|
||||
// the result of hue and saturation would be miscalculated
|
||||
if (hsv.v < 0.0164) {
|
||||
hsv.h = colorData.h || (colorData.hsv && colorData.hsv.h) || 0;
|
||||
hsv.s = colorData.s || (colorData.hsv && colorData.hsv.s) || 0;
|
||||
}
|
||||
|
||||
if (hsl.l < 0.01) {
|
||||
hsl.h = colorData.h || (colorData.hsl && colorData.hsl.h) || 0;
|
||||
hsl.s = colorData.s || (colorData.hsl && colorData.hsl.s) || 0;
|
||||
}
|
||||
|
||||
return {
|
||||
hsl,
|
||||
hex: color.toHexString().toUpperCase(),
|
||||
rgba: color.toRgb(),
|
||||
hsv,
|
||||
oldHue: colorData.h || oldHue || hsl.h,
|
||||
source: colorData.source,
|
||||
a: colorData.a || color.getAlpha(),
|
||||
};
|
||||
}
|
||||
|
||||
export function clamp(value, min, max) {
|
||||
if (value < min) {
|
||||
return min;
|
||||
}
|
||||
|
||||
if (value > max) {
|
||||
return max;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function getIncrement(key, keys, increment) {
|
||||
return oneOf(key, keys) ? increment : 0;
|
||||
}
|
||||
|
||||
export function getTouches(e, prop) {
|
||||
return e.touches ? e.touches[0][prop] : 0;
|
||||
}
|
||||
|
||||
export function toRGBAString(rgba) {
|
||||
const {r, g, b, a} = rgba;
|
||||
|
||||
return `rgba(${[r, g, b, a].join(',')})`;
|
||||
}
|
||||
|
||||
export function isValidHex(hex) {
|
||||
return tinycolor(hex).isValid();
|
||||
}
|
||||
|
||||
function checkIteratee(data, counts, letter) {
|
||||
let {checked, passed} = counts;
|
||||
const value = data[letter];
|
||||
|
||||
if (value) {
|
||||
checked += 1;
|
||||
|
||||
if (Number.isFinite(value)) {
|
||||
passed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return {checked, passed};
|
||||
}
|
||||
|
||||
const keysToCheck = ['r', 'g', 'b', 'a', 'h', 's', 'l', 'v'];
|
||||
|
||||
export function simpleCheckForValidColor(data) {
|
||||
const results = keysToCheck.reduce(checkIteratee.bind(null, data), {checked: 0, passed: 0});
|
||||
|
||||
return results.checked === results.passed ? data : undefined;
|
||||
}
|
3
src/components/content/index.js
Normal file
3
src/components/content/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import Content from '../layout/content.vue';
|
||||
|
||||
export default Content;
|
72
src/components/date-picker/base/confirm.vue
Normal file
72
src/components/date-picker/base/confirm.vue
Normal file
|
@ -0,0 +1,72 @@
|
|||
<template>
|
||||
<div :class="[prefixCls + '-confirm']" @keydown.tab.capture="handleTab">
|
||||
<i-button :class="timeClasses" size="small" type="text" :disabled="timeDisabled" v-if="showTime" @click="handleToggleTime">
|
||||
{{labels.time}}
|
||||
</i-button>
|
||||
<i-button size="small" @click.native="handleClear" @keydown.enter.native="handleClear">
|
||||
{{labels.clear}}
|
||||
</i-button>
|
||||
<i-button size="small" type="primary" @click.native="handleSuccess" @keydown.enter.native="handleSuccess">
|
||||
{{labels.ok}}
|
||||
</i-button>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import iButton from '../../button/button.vue';
|
||||
import Locale from '../../../mixins/locale';
|
||||
import Emitter from '../../../mixins/emitter';
|
||||
|
||||
const prefixCls = 'ivu-picker';
|
||||
|
||||
export default {
|
||||
mixins: [Locale, Emitter],
|
||||
components: {iButton},
|
||||
props: {
|
||||
showTime: false,
|
||||
isTime: false,
|
||||
timeDisabled: false
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
prefixCls: prefixCls
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
timeClasses () {
|
||||
return `${prefixCls}-confirm-time`;
|
||||
},
|
||||
labels(){
|
||||
const labels = ['time', 'clear', 'ok'];
|
||||
const values = [(this.isTime ? 'selectDate' : 'selectTime'), 'clear', 'ok'];
|
||||
return labels.reduce((obj, key, i) => {
|
||||
obj[key] = this.t('i.datepicker.' + values[i]);
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClear () {
|
||||
this.$emit('on-pick-clear');
|
||||
},
|
||||
handleSuccess () {
|
||||
this.$emit('on-pick-success');
|
||||
},
|
||||
handleToggleTime () {
|
||||
if (this.timeDisabled) return;
|
||||
this.$emit('on-pick-toggle-time');
|
||||
this.dispatch('CalendarPicker', 'focus-input');
|
||||
this.dispatch('CalendarPicker', 'update-popper');
|
||||
},
|
||||
handleTab(e) {
|
||||
const tabbables = [...this.$el.children];
|
||||
const expectedFocus = tabbables[e.shiftKey ? 'shift' : 'pop']();
|
||||
|
||||
if (document.activeElement === expectedFocus) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.dispatch('CalendarPicker', 'focus-input');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
114
src/components/date-picker/base/date-table.vue
Normal file
114
src/components/date-picker/base/date-table.vue
Normal file
|
@ -0,0 +1,114 @@
|
|||
<template>
|
||||
<div :class="classes">
|
||||
<div :class="[prefixCls + '-header']">
|
||||
<span v-for="day in headerDays" :key="day">
|
||||
{{day}}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
:class="getCellCls(cell)"
|
||||
v-for="(cell, i) in cells"
|
||||
:key="String(cell.date) + i"
|
||||
@click="handleClick(cell, $event)"
|
||||
@mouseenter="handleMouseMove(cell)"
|
||||
>
|
||||
<em>{{ cell.desc }}</em>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { clearHours, isInRange } from '../util';
|
||||
import Locale from '../../../mixins/locale';
|
||||
import jsCalendar from 'js-calendar';
|
||||
|
||||
import mixin from './mixin';
|
||||
import prefixCls from './prefixCls';
|
||||
|
||||
|
||||
export default {
|
||||
mixins: [ Locale, mixin ],
|
||||
|
||||
props: {
|
||||
/* more props in mixin */
|
||||
showWeekNumbers: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
prefixCls: prefixCls,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
return [
|
||||
`${prefixCls}`,
|
||||
{
|
||||
[`${prefixCls}-show-week-numbers`]: this.showWeekNumbers
|
||||
}
|
||||
];
|
||||
},
|
||||
calendar(){
|
||||
const weekStartDay = Number(this.t('i.datepicker.weekStartDay'));
|
||||
return new jsCalendar.Generator({onlyDays: !this.showWeekNumbers, weekStart: weekStartDay});
|
||||
},
|
||||
headerDays () {
|
||||
const weekStartDay = Number(this.t('i.datepicker.weekStartDay'));
|
||||
const translatedDays = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'].map(item => {
|
||||
return this.t('i.datepicker.weeks.' + item);
|
||||
});
|
||||
const weekDays = translatedDays.splice(weekStartDay, 7 - weekStartDay).concat(translatedDays.splice(0, weekStartDay));
|
||||
return this.showWeekNumbers ? [''].concat(weekDays) : weekDays;
|
||||
},
|
||||
cells () {
|
||||
const tableYear = this.tableDate.getFullYear();
|
||||
const tableMonth = this.tableDate.getMonth();
|
||||
const today = clearHours(new Date()); // timestamp of today
|
||||
const selectedDays = this.dates.filter(Boolean).map(clearHours); // timestamp of selected days
|
||||
const [minDay, maxDay] = this.dates.map(clearHours);
|
||||
const rangeStart = this.rangeState.from && clearHours(this.rangeState.from);
|
||||
const rangeEnd = this.rangeState.to && clearHours(this.rangeState.to);
|
||||
|
||||
const isRange = this.selectionMode === 'range';
|
||||
const disabledTestFn = typeof this.disabledDate === 'function' && this.disabledDate;
|
||||
|
||||
return this.calendar(tableYear, tableMonth, (cell) => {
|
||||
// normalize date offset from the dates provided by jsCalendar
|
||||
if (cell.date instanceof Date) cell.date.setTime(cell.date.getTime() + cell.date.getTimezoneOffset() * 60000);
|
||||
|
||||
const time = cell.date && clearHours(cell.date);
|
||||
const dateIsInCurrentMonth = cell.date && tableMonth === cell.date.getMonth();
|
||||
return {
|
||||
...cell,
|
||||
type: time === today ? 'today' : cell.type,
|
||||
selected: dateIsInCurrentMonth && selectedDays.includes(time),
|
||||
disabled: (cell.date && disabledTestFn) && disabledTestFn(new Date(time)),
|
||||
range: dateIsInCurrentMonth && isRange && isInRange(time, rangeStart, rangeEnd),
|
||||
start: dateIsInCurrentMonth && isRange && time === minDay,
|
||||
end: dateIsInCurrentMonth && isRange && time === maxDay
|
||||
};
|
||||
}).cells.slice(this.showWeekNumbers ? 8 : 0);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getCellCls (cell) {
|
||||
return [
|
||||
`${prefixCls}-cell`,
|
||||
{
|
||||
[`${prefixCls}-cell-selected`]: cell.selected || cell.start || cell.end,
|
||||
[`${prefixCls}-cell-disabled`]: cell.disabled,
|
||||
[`${prefixCls}-cell-today`]: cell.type === 'today',
|
||||
[`${prefixCls}-cell-prev-month`]: cell.type === 'prevMonth',
|
||||
[`${prefixCls}-cell-next-month`]: cell.type === 'nextMonth',
|
||||
[`${prefixCls}-cell-week-label`]: cell.type === 'weekLabel',
|
||||
[`${prefixCls}-cell-range`]: cell.range && !cell.start && !cell.end,
|
||||
[`${prefixCls}-focused`]: clearHours(cell.date) === clearHours(this.focusedDate)
|
||||
|
||||
}
|
||||
];
|
||||
},
|
||||
|
||||
}
|
||||
};
|
||||
</script>
|
57
src/components/date-picker/base/mixin.js
Normal file
57
src/components/date-picker/base/mixin.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
|
||||
import {clearHours} from '../util';
|
||||
|
||||
export default {
|
||||
name: 'PanelTable',
|
||||
props: {
|
||||
tableDate: {
|
||||
type: Date,
|
||||
required: true
|
||||
},
|
||||
disabledDate: {
|
||||
type: Function
|
||||
},
|
||||
selectionMode: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
rangeState: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
from: null,
|
||||
to: null,
|
||||
selecting: false
|
||||
})
|
||||
},
|
||||
focusedDate: {
|
||||
type: Date,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dates(){
|
||||
const {selectionMode, value, rangeState} = this;
|
||||
const rangeSelecting = selectionMode === 'range' && rangeState.selecting;
|
||||
return rangeSelecting ? [rangeState.from] : value;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleClick (cell) {
|
||||
if (cell.disabled || cell.type === 'weekLabel') return;
|
||||
const newDate = new Date(clearHours(cell.date));
|
||||
|
||||
this.$emit('on-pick', newDate);
|
||||
this.$emit('on-pick-click');
|
||||
},
|
||||
handleMouseMove (cell) {
|
||||
if (!this.rangeState.selecting) return;
|
||||
if (cell.disabled) return;
|
||||
const newDate = cell.date;
|
||||
this.$emit('on-change-range', newDate);
|
||||
},
|
||||
}
|
||||
};
|
74
src/components/date-picker/base/month-table.vue
Normal file
74
src/components/date-picker/base/month-table.vue
Normal file
|
@ -0,0 +1,74 @@
|
|||
<template>
|
||||
<div :class="classes">
|
||||
<span
|
||||
:class="getCellCls(cell)"
|
||||
v-for="cell in cells"
|
||||
@click="handleClick(cell)"
|
||||
@mouseenter="handleMouseMove(cell)"
|
||||
|
||||
>
|
||||
<em>{{ cell.text }}</em>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { clearHours } from '../util';
|
||||
import { deepCopy } from '../../../utils/assist';
|
||||
import Locale from '../../../mixins/locale';
|
||||
import mixin from './mixin';
|
||||
import prefixCls from './prefixCls';
|
||||
|
||||
export default {
|
||||
mixins: [ Locale, mixin ],
|
||||
props: {/* in mixin */},
|
||||
computed: {
|
||||
classes() {
|
||||
return [
|
||||
`${prefixCls}`,
|
||||
`${prefixCls}-month`
|
||||
];
|
||||
},
|
||||
cells () {
|
||||
let cells = [];
|
||||
const cell_tmpl = {
|
||||
text: '',
|
||||
selected: false,
|
||||
disabled: false
|
||||
};
|
||||
|
||||
const tableYear = this.tableDate.getFullYear();
|
||||
const selectedDays = this.dates.filter(Boolean).map(date => clearHours(new Date(date.getFullYear(), date.getMonth(), 1)));
|
||||
const focusedDate = clearHours(new Date(this.focusedDate.getFullYear(), this.focusedDate.getMonth(), 1));
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const cell = deepCopy(cell_tmpl);
|
||||
cell.date = new Date(tableYear, i, 1);
|
||||
cell.text = this.tCell(i + 1);
|
||||
const day = clearHours(cell.date);
|
||||
cell.disabled = typeof this.disabledDate === 'function' && this.disabledDate(cell.date) && this.selectionMode === 'month';
|
||||
cell.selected = selectedDays.includes(day);
|
||||
cell.focused = day === focusedDate;
|
||||
cells.push(cell);
|
||||
}
|
||||
|
||||
return cells;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getCellCls (cell) {
|
||||
return [
|
||||
`${prefixCls}-cell`,
|
||||
{
|
||||
[`${prefixCls}-cell-selected`]: cell.selected,
|
||||
[`${prefixCls}-cell-disabled`]: cell.disabled,
|
||||
[`${prefixCls}-cell-focused`]: cell.focused,
|
||||
[`${prefixCls}-cell-range`]: cell.range && !cell.start && !cell.end
|
||||
}
|
||||
];
|
||||
},
|
||||
tCell (nr) {
|
||||
return this.t(`i.datepicker.months.m${nr}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
2
src/components/date-picker/base/prefixCls.js
Normal file
2
src/components/date-picker/base/prefixCls.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
|
||||
export default 'ivu-date-picker-cells';
|
238
src/components/date-picker/base/time-spinner.vue
Normal file
238
src/components/date-picker/base/time-spinner.vue
Normal file
|
@ -0,0 +1,238 @@
|
|||
<template>
|
||||
<div :class="classes">
|
||||
<div :class="[prefixCls+ '-list']" ref="hours">
|
||||
<ul :class="[prefixCls + '-ul']">
|
||||
<li :class="getCellCls(item)" v-for="item in hoursList" v-show="!item.hide" @click="handleClick('hours', item)">{{ formatTime(item.text) }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div :class="[prefixCls+ '-list']" ref="minutes">
|
||||
<ul :class="[prefixCls + '-ul']">
|
||||
<li :class="getCellCls(item)" v-for="item in minutesList" v-show="!item.hide" @click="handleClick('minutes', item)">{{ formatTime(item.text) }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div :class="[prefixCls+ '-list']" v-show="showSeconds" ref="seconds">
|
||||
<ul :class="[prefixCls + '-ul']">
|
||||
<li :class="getCellCls(item)" v-for="item in secondsList" v-show="!item.hide" @click="handleClick('seconds', item)">{{ formatTime(item.text) }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Options from '../time-mixins';
|
||||
import { deepCopy, scrollTop, firstUpperCase } from '../../../utils/assist';
|
||||
|
||||
const prefixCls = 'ivu-time-picker-cells';
|
||||
const timeParts = ['hours', 'minutes', 'seconds'];
|
||||
|
||||
export default {
|
||||
name: 'TimeSpinner',
|
||||
mixins: [Options],
|
||||
props: {
|
||||
hours: {
|
||||
type: [Number, String],
|
||||
default: NaN
|
||||
},
|
||||
minutes: {
|
||||
type: [Number, String],
|
||||
default: NaN
|
||||
},
|
||||
seconds: {
|
||||
type: [Number, String],
|
||||
default: NaN
|
||||
},
|
||||
showSeconds: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
steps: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
spinerSteps: [1, 1, 1].map((one, i) => Math.abs(this.steps[i]) || one),
|
||||
prefixCls: prefixCls,
|
||||
compiled: false,
|
||||
focusedColumn: -1, // which column inside the picker
|
||||
focusedTime: [0, 0, 0] // the values array into [hh, mm, ss]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
return [
|
||||
`${prefixCls}`,
|
||||
{
|
||||
[`${prefixCls}-with-seconds`]: this.showSeconds
|
||||
}
|
||||
];
|
||||
},
|
||||
hoursList () {
|
||||
let hours = [];
|
||||
const step = this.spinerSteps[0];
|
||||
const focusedHour = this.focusedColumn === 0 && this.focusedTime[0];
|
||||
const hour_tmpl = {
|
||||
text: 0,
|
||||
selected: false,
|
||||
disabled: false,
|
||||
hide: false
|
||||
};
|
||||
|
||||
for (let i = 0; i < 24; i += step) {
|
||||
const hour = deepCopy(hour_tmpl);
|
||||
hour.text = i;
|
||||
hour.focused = i === focusedHour;
|
||||
|
||||
if (this.disabledHours.length && this.disabledHours.indexOf(i) > -1) {
|
||||
hour.disabled = true;
|
||||
if (this.hideDisabledOptions) hour.hide = true;
|
||||
}
|
||||
if (this.hours === i) hour.selected = true;
|
||||
hours.push(hour);
|
||||
}
|
||||
|
||||
return hours;
|
||||
},
|
||||
minutesList () {
|
||||
let minutes = [];
|
||||
const step = this.spinerSteps[1];
|
||||
const focusedMinute = this.focusedColumn === 1 && this.focusedTime[1];
|
||||
const minute_tmpl = {
|
||||
text: 0,
|
||||
selected: false,
|
||||
disabled: false,
|
||||
hide: false
|
||||
};
|
||||
|
||||
for (let i = 0; i < 60; i += step) {
|
||||
const minute = deepCopy(minute_tmpl);
|
||||
minute.text = i;
|
||||
minute.focused = i === focusedMinute;
|
||||
|
||||
if (this.disabledMinutes.length && this.disabledMinutes.indexOf(i) > -1) {
|
||||
minute.disabled = true;
|
||||
if (this.hideDisabledOptions) minute.hide = true;
|
||||
}
|
||||
if (this.minutes === i) minute.selected = true;
|
||||
minutes.push(minute);
|
||||
}
|
||||
return minutes;
|
||||
},
|
||||
secondsList () {
|
||||
let seconds = [];
|
||||
const step = this.spinerSteps[2];
|
||||
const focusedMinute = this.focusedColumn === 2 && this.focusedTime[2];
|
||||
const second_tmpl = {
|
||||
text: 0,
|
||||
selected: false,
|
||||
disabled: false,
|
||||
hide: false
|
||||
};
|
||||
|
||||
for (let i = 0; i < 60; i += step) {
|
||||
const second = deepCopy(second_tmpl);
|
||||
second.text = i;
|
||||
second.focused = i === focusedMinute;
|
||||
|
||||
if (this.disabledSeconds.length && this.disabledSeconds.indexOf(i) > -1) {
|
||||
second.disabled = true;
|
||||
if (this.hideDisabledOptions) second.hide = true;
|
||||
}
|
||||
if (this.seconds === i) second.selected = true;
|
||||
seconds.push(second);
|
||||
}
|
||||
|
||||
return seconds;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getCellCls (cell) {
|
||||
return [
|
||||
`${prefixCls}-cell`,
|
||||
{
|
||||
[`${prefixCls}-cell-selected`]: cell.selected,
|
||||
[`${prefixCls}-cell-focused`]: cell.focused,
|
||||
[`${prefixCls}-cell-disabled`]: cell.disabled
|
||||
|
||||
}
|
||||
];
|
||||
},
|
||||
chooseValue(values){
|
||||
const changes = timeParts.reduce((obj, part, i) => {
|
||||
const value = values[i];
|
||||
if (this[part] === value) return obj;
|
||||
return {
|
||||
...obj,
|
||||
[part]: value
|
||||
};
|
||||
}, {});
|
||||
if (Object.keys(changes).length > 0) {
|
||||
this.emitChange(changes);
|
||||
}
|
||||
},
|
||||
handleClick (type, cell) {
|
||||
if (cell.disabled) return;
|
||||
const data = {[type]: cell.text};
|
||||
this.emitChange(data);
|
||||
},
|
||||
emitChange(changes){
|
||||
this.$emit('on-change', changes);
|
||||
this.$emit('on-pick-click');
|
||||
},
|
||||
scroll (type, index) {
|
||||
const from = this.$refs[type].scrollTop;
|
||||
const to = 24 * this.getScrollIndex(type, index);
|
||||
scrollTop(this.$refs[type], from, to, 500);
|
||||
},
|
||||
getScrollIndex (type, index) {
|
||||
const Type = firstUpperCase(type);
|
||||
const disabled = this[`disabled${Type}`];
|
||||
if (disabled.length && this.hideDisabledOptions) {
|
||||
let _count = 0;
|
||||
disabled.forEach(item => item <= index ? _count++ : '');
|
||||
index -= _count;
|
||||
}
|
||||
return index;
|
||||
},
|
||||
updateScroll () {
|
||||
this.$nextTick(() => {
|
||||
timeParts.forEach(type => {
|
||||
this.$refs[type].scrollTop = 24 * this[`${type}List`].findIndex(obj => obj.text == this[type]);
|
||||
});
|
||||
});
|
||||
},
|
||||
formatTime (text) {
|
||||
return text < 10 ? '0' + text : text;
|
||||
},
|
||||
updateFocusedTime(col, time) {
|
||||
this.focusedColumn = col;
|
||||
this.focusedTime = time.slice();
|
||||
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
hours (val) {
|
||||
if (!this.compiled) return;
|
||||
this.scroll('hours', this.hoursList.findIndex(obj => obj.text == val));
|
||||
},
|
||||
minutes (val) {
|
||||
if (!this.compiled) return;
|
||||
this.scroll('minutes', this.minutesList.findIndex(obj => obj.text == val));
|
||||
},
|
||||
seconds (val) {
|
||||
if (!this.compiled) return;
|
||||
this.scroll('seconds', this.secondsList.findIndex(obj => obj.text == val));
|
||||
},
|
||||
focusedTime(updated, old){
|
||||
timeParts.forEach((part, i) => {
|
||||
if (updated[i] === old[i] || typeof updated[i] === 'undefined') return;
|
||||
const valueIndex = this[`${part}List`].findIndex(obj => obj.text === updated[i]);
|
||||
this.scroll(part, valueIndex);
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$nextTick(() => this.compiled = true);
|
||||
}
|
||||
};
|
||||
</script>
|
71
src/components/date-picker/base/year-table.vue
Normal file
71
src/components/date-picker/base/year-table.vue
Normal file
|
@ -0,0 +1,71 @@
|
|||
<template>
|
||||
<div :class="classes">
|
||||
<span
|
||||
:class="getCellCls(cell)"
|
||||
v-for="cell in cells"
|
||||
@click="handleClick(cell)"
|
||||
@mouseenter="handleMouseMove(cell)"
|
||||
>
|
||||
<em>{{ cell.date.getFullYear() }}</em>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { clearHours } from '../util';
|
||||
import { deepCopy } from '../../../utils/assist';
|
||||
import mixin from './mixin';
|
||||
import prefixCls from './prefixCls';
|
||||
|
||||
export default {
|
||||
mixins: [ mixin ],
|
||||
|
||||
props: {/* in mixin */},
|
||||
computed: {
|
||||
classes() {
|
||||
return [
|
||||
`${prefixCls}`,
|
||||
`${prefixCls}-year`
|
||||
];
|
||||
},
|
||||
startYear() {
|
||||
return Math.floor(this.tableDate.getFullYear() / 10) * 10;
|
||||
},
|
||||
cells () {
|
||||
let cells = [];
|
||||
const cell_tmpl = {
|
||||
text: '',
|
||||
selected: false,
|
||||
disabled: false
|
||||
};
|
||||
|
||||
const selectedDays = this.dates.filter(Boolean).map(date => clearHours(new Date(date.getFullYear(), 0, 1)));
|
||||
const focusedDate = clearHours(new Date(this.focusedDate.getFullYear(), 0, 1));
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const cell = deepCopy(cell_tmpl);
|
||||
cell.date = new Date(this.startYear + i, 0, 1);
|
||||
cell.disabled = typeof this.disabledDate === 'function' && this.disabledDate(cell.date) && this.selectionMode === 'year';
|
||||
const day = clearHours(cell.date);
|
||||
cell.selected = selectedDays.includes(day);
|
||||
cell.focused = day === focusedDate;
|
||||
cells.push(cell);
|
||||
}
|
||||
|
||||
return cells;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getCellCls (cell) {
|
||||
return [
|
||||
`${prefixCls}-cell`,
|
||||
{
|
||||
[`${prefixCls}-cell-selected`]: cell.selected,
|
||||
[`${prefixCls}-cell-disabled`]: cell.disabled,
|
||||
[`${prefixCls}-cell-focused`]: cell.focused,
|
||||
[`${prefixCls}-cell-range`]: cell.range && !cell.start && !cell.end
|
||||
}
|
||||
];
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
3
src/components/date-picker/index.js
Normal file
3
src/components/date-picker/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import DatePicker from './picker/date-picker';
|
||||
|
||||
export default DatePicker;
|
25
src/components/date-picker/panel/Date/date-panel-label.vue
Normal file
25
src/components/date-picker/panel/Date/date-panel-label.vue
Normal file
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<span>
|
||||
<span
|
||||
v-if="datePanelLabel"
|
||||
v-show="datePanelLabel.labels[0].type === 'year' || currentView === 'date'"
|
||||
:class="[datePrefixCls + '-header-label']"
|
||||
@click="datePanelLabel.labels[0].handler">{{ datePanelLabel.labels[0].label }}</span>
|
||||
<template v-if="datePanelLabel && currentView === 'date'">{{ datePanelLabel.separator }}</template>
|
||||
<span
|
||||
v-if="datePanelLabel"
|
||||
v-show="datePanelLabel.labels[1].type === 'year' || currentView === 'date'"
|
||||
:class="[datePrefixCls + '-header-label']"
|
||||
@click="datePanelLabel.labels[1].handler">{{ datePanelLabel.labels[1].label }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
datePanelLabel: Object,
|
||||
currentView: String,
|
||||
datePrefixCls: String
|
||||
}
|
||||
};
|
||||
</script>
|
65
src/components/date-picker/panel/Date/date-panel-mixin.js
Normal file
65
src/components/date-picker/panel/Date/date-panel-mixin.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
|
||||
import { oneOf } from '../../../../utils/assist';
|
||||
import {initTimeDate } from '../../util';
|
||||
|
||||
|
||||
export default {
|
||||
props: {
|
||||
showTime: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
format: {
|
||||
type: String,
|
||||
default: 'yyyy-MM-dd'
|
||||
},
|
||||
selectionMode: {
|
||||
type: String,
|
||||
validator (value) {
|
||||
return oneOf(value, ['year', 'month', 'date', 'time']);
|
||||
},
|
||||
default: 'date'
|
||||
},
|
||||
shortcuts: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
disabledDate: {
|
||||
type: Function,
|
||||
default: () => false
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => [initTimeDate(), initTimeDate()]
|
||||
},
|
||||
timePickerOptions: {
|
||||
default: () => ({}),
|
||||
type: Object,
|
||||
},
|
||||
showWeekNumbers: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
startDate: {
|
||||
type: Date
|
||||
},
|
||||
pickerType: {
|
||||
type: String,
|
||||
require: true
|
||||
},
|
||||
focusedDate: {
|
||||
type: Date,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isTime(){
|
||||
return this.currentView === 'time';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleToggleTime(){
|
||||
this.currentView = this.currentView === 'time' ? 'date' : 'time';
|
||||
},
|
||||
}
|
||||
};
|
378
src/components/date-picker/panel/Date/date-range.vue
Normal file
378
src/components/date-picker/panel/Date/date-range.vue
Normal file
|
@ -0,0 +1,378 @@
|
|||
<template>
|
||||
<div :class="classes" @mousedown.prevent>
|
||||
<div :class="[prefixCls + '-sidebar']" v-if="shortcuts.length">
|
||||
<div
|
||||
:class="[prefixCls + '-shortcut']"
|
||||
v-for="shortcut in shortcuts"
|
||||
@click="handleShortcutClick(shortcut)">{{ shortcut.text }}</div>
|
||||
</div>
|
||||
<div :class="panelBodyClasses">
|
||||
<div :class="[prefixCls + '-content', prefixCls + '-content-left']" v-show="!isTime">
|
||||
<div :class="[datePrefixCls + '-header']" v-show="currentView !== 'time'">
|
||||
<span
|
||||
:class="iconBtnCls('prev', '-double')"
|
||||
@click="prevYear('left')"><Icon type="ios-arrow-back"></Icon></span>
|
||||
<span
|
||||
v-if="leftPickerTable === 'date-table'"
|
||||
:class="iconBtnCls('prev')"
|
||||
@click="prevMonth('left')"
|
||||
v-show="currentView === 'date'"><Icon type="ios-arrow-back"></Icon></span>
|
||||
<date-panel-label
|
||||
:date-panel-label="leftDatePanelLabel"
|
||||
:current-view="leftDatePanelView"
|
||||
:date-prefix-cls="datePrefixCls"></date-panel-label>
|
||||
<span
|
||||
v-if="splitPanels || leftPickerTable !== 'date-table'"
|
||||
:class="iconBtnCls('next', '-double')"
|
||||
@click="nextYear('left')"><Icon type="ios-arrow-forward"></Icon></span>
|
||||
<span
|
||||
v-if="splitPanels && leftPickerTable === 'date-table'"
|
||||
:class="iconBtnCls('next')"
|
||||
@click="nextMonth('left')"
|
||||
v-show="currentView === 'date'"><Icon type="ios-arrow-forward"></Icon></span>
|
||||
</div>
|
||||
<component
|
||||
:is="leftPickerTable"
|
||||
ref="leftYearTable"
|
||||
v-if="currentView !== 'time'"
|
||||
:table-date="leftPanelDate"
|
||||
selection-mode="range"
|
||||
:disabled-date="disabledDate"
|
||||
:range-state="rangeState"
|
||||
:show-week-numbers="showWeekNumbers"
|
||||
:value="preSelecting.left ? [dates[0]] : dates"
|
||||
:focused-date="focusedDate"
|
||||
|
||||
@on-change-range="handleChangeRange"
|
||||
@on-pick="panelPickerHandlers.left"
|
||||
@on-pick-click="handlePickClick"
|
||||
></component>
|
||||
</div>
|
||||
<div :class="[prefixCls + '-content', prefixCls + '-content-right']" v-show="!isTime">
|
||||
<div :class="[datePrefixCls + '-header']" v-show="currentView !== 'time'">
|
||||
<span
|
||||
v-if="splitPanels || rightPickerTable !== 'date-table'"
|
||||
:class="iconBtnCls('prev', '-double')"
|
||||
@click="prevYear('right')"><Icon type="ios-arrow-back"></Icon></span>
|
||||
<span
|
||||
v-if="splitPanels && rightPickerTable === 'date-table'"
|
||||
:class="iconBtnCls('prev')"
|
||||
@click="prevMonth('right')"
|
||||
v-show="currentView === 'date'"><Icon type="ios-arrow-back"></Icon></span>
|
||||
<date-panel-label
|
||||
:date-panel-label="rightDatePanelLabel"
|
||||
:current-view="rightDatePanelView"
|
||||
:date-prefix-cls="datePrefixCls"></date-panel-label>
|
||||
<span
|
||||
:class="iconBtnCls('next', '-double')"
|
||||
@click="nextYear('right')"><Icon type="ios-arrow-forward"></Icon></span>
|
||||
<span
|
||||
v-if="rightPickerTable === 'date-table'"
|
||||
:class="iconBtnCls('next')"
|
||||
@click="nextMonth('right')"
|
||||
v-show="currentView === 'date'"><Icon type="ios-arrow-forward"></Icon></span>
|
||||
</div>
|
||||
<component
|
||||
:is="rightPickerTable"
|
||||
ref="rightYearTable"
|
||||
v-if="currentView !== 'time'"
|
||||
:table-date="rightPanelDate"
|
||||
selection-mode="range"
|
||||
:range-state="rangeState"
|
||||
:disabled-date="disabledDate"
|
||||
:show-week-numbers="showWeekNumbers"
|
||||
:value="preSelecting.right ? [dates[dates.length - 1]] : dates"
|
||||
:focused-date="focusedDate"
|
||||
|
||||
@on-change-range="handleChangeRange"
|
||||
@on-pick="panelPickerHandlers.right"
|
||||
@on-pick-click="handlePickClick"></component>
|
||||
</div>
|
||||
<div :class="[prefixCls + '-content']" v-show="isTime">
|
||||
<time-picker
|
||||
ref="timePicker"
|
||||
v-if="currentView === 'time'"
|
||||
:value="dates"
|
||||
:format="format"
|
||||
:time-disabled="timeDisabled"
|
||||
v-bind="timePickerOptions"
|
||||
@on-pick="handleRangePick"
|
||||
@on-pick-click="handlePickClick"
|
||||
@on-pick-clear="handlePickClear"
|
||||
@on-pick-success="handlePickSuccess"
|
||||
@on-pick-toggle-time="handleToggleTime"
|
||||
></time-picker>
|
||||
</div>
|
||||
<Confirm
|
||||
v-if="confirm"
|
||||
:show-time="showTime"
|
||||
:is-time="isTime"
|
||||
:time-disabled="timeDisabled"
|
||||
@on-pick-toggle-time="handleToggleTime"
|
||||
@on-pick-clear="handlePickClear"
|
||||
@on-pick-success="handlePickSuccess"></Confirm>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Icon from '../../../icon/icon.vue';
|
||||
import DateTable from '../../base/date-table.vue';
|
||||
import YearTable from '../../base/year-table.vue';
|
||||
import MonthTable from '../../base/month-table.vue';
|
||||
import TimePicker from '../Time/time-range.vue';
|
||||
import Confirm from '../../base/confirm.vue';
|
||||
|
||||
import { toDate, initTimeDate, formatDateLabels } from '../../util';
|
||||
import datePanelLabel from './date-panel-label.vue';
|
||||
|
||||
import Mixin from '../panel-mixin';
|
||||
import DateMixin from './date-panel-mixin';
|
||||
import Locale from '../../../../mixins/locale';
|
||||
|
||||
const prefixCls = 'ivu-picker-panel';
|
||||
const datePrefixCls = 'ivu-date-picker';
|
||||
|
||||
const dateSorter = (a, b) => {
|
||||
if (!a || !b) return 0;
|
||||
return a.getTime() - b.getTime();
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'RangeDatePickerPanel',
|
||||
mixins: [ Mixin, Locale, DateMixin ],
|
||||
components: { Icon, DateTable, YearTable, MonthTable, TimePicker, Confirm, datePanelLabel },
|
||||
props: {
|
||||
// more props in the mixin
|
||||
splitPanels: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
data(){
|
||||
const [minDate, maxDate] = this.value.map(date => date || initTimeDate());
|
||||
const leftPanelDate = this.startDate ? this.startDate : minDate;
|
||||
|
||||
return {
|
||||
prefixCls: prefixCls,
|
||||
datePrefixCls: datePrefixCls,
|
||||
dates: this.value,
|
||||
rangeState: {from: this.value[0], to: this.value[1], selecting: minDate && !maxDate},
|
||||
currentView: this.selectionMode || 'range',
|
||||
leftPickerTable: `${this.selectionMode}-table`,
|
||||
rightPickerTable: `${this.selectionMode}-table`,
|
||||
leftPanelDate: leftPanelDate,
|
||||
rightPanelDate: new Date(leftPanelDate.getFullYear(), leftPanelDate.getMonth() + 1, 1)
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
classes(){
|
||||
return [
|
||||
`${prefixCls}-body-wrapper`,
|
||||
`${datePrefixCls}-with-range`,
|
||||
{
|
||||
[`${prefixCls}-with-sidebar`]: this.shortcuts.length,
|
||||
[`${datePrefixCls}-with-week-numbers`]: this.showWeekNumbers
|
||||
}
|
||||
];
|
||||
},
|
||||
panelBodyClasses(){
|
||||
return [
|
||||
prefixCls + '-body',
|
||||
{
|
||||
[prefixCls + '-body-time']: this.showTime,
|
||||
[prefixCls + '-body-date']: !this.showTime,
|
||||
}
|
||||
];
|
||||
},
|
||||
leftDatePanelLabel(){
|
||||
return this.panelLabelConfig('left');
|
||||
},
|
||||
rightDatePanelLabel(){
|
||||
return this.panelLabelConfig('right');
|
||||
},
|
||||
leftDatePanelView(){
|
||||
return this.leftPickerTable.split('-').shift();
|
||||
},
|
||||
rightDatePanelView(){
|
||||
return this.rightPickerTable.split('-').shift();
|
||||
},
|
||||
timeDisabled(){
|
||||
return !(this.dates[0] && this.dates[1]);
|
||||
},
|
||||
preSelecting(){
|
||||
const tableType = `${this.currentView}-table`;
|
||||
|
||||
return {
|
||||
left: this.leftPickerTable !== tableType,
|
||||
right: this.rightPickerTable !== tableType,
|
||||
};
|
||||
},
|
||||
panelPickerHandlers(){
|
||||
return {
|
||||
left: this.preSelecting.left ? this.handlePreSelection.bind(this, 'left') : this.handleRangePick,
|
||||
right: this.preSelecting.right ? this.handlePreSelection.bind(this, 'right') : this.handleRangePick,
|
||||
};
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
const minDate = newVal[0] ? toDate(newVal[0]) : null;
|
||||
const maxDate = newVal[1] ? toDate(newVal[1]) : null;
|
||||
this.dates = [minDate, maxDate].sort(dateSorter);
|
||||
|
||||
this.rangeState = {
|
||||
from: this.dates[0],
|
||||
to: this.dates[1],
|
||||
selecting: false
|
||||
};
|
||||
|
||||
|
||||
// set panels positioning
|
||||
this.setPanelDates(this.startDate || this.dates[0] || new Date());
|
||||
},
|
||||
currentView(currentView){
|
||||
const leftMonth = this.leftPanelDate.getMonth();
|
||||
const rightMonth = this.rightPanelDate.getMonth();
|
||||
const isSameYear = this.leftPanelDate.getFullYear() === this.rightPanelDate.getFullYear();
|
||||
|
||||
if (currentView === 'date' && isSameYear && leftMonth === rightMonth){
|
||||
this.changePanelDate('right', 'Month', 1);
|
||||
}
|
||||
if (currentView === 'month' && isSameYear){
|
||||
this.changePanelDate('right', 'FullYear', 1);
|
||||
}
|
||||
if (currentView === 'year' && isSameYear){
|
||||
this.changePanelDate('right', 'FullYear', 10);
|
||||
}
|
||||
},
|
||||
selectionMode(type){
|
||||
this.currentView = type || 'range';
|
||||
},
|
||||
focusedDate(date){
|
||||
this.setPanelDates(date || new Date());
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
reset(){
|
||||
this.currentView = this.selectionMode;
|
||||
this.leftPickerTable = `${this.currentView}-table`;
|
||||
this.rightPickerTable = `${this.currentView}-table`;
|
||||
},
|
||||
setPanelDates(leftPanelDate){
|
||||
this.leftPanelDate = leftPanelDate;
|
||||
const rightPanelDate = new Date(leftPanelDate.getFullYear(), leftPanelDate.getMonth() + 1, 1);
|
||||
const splitRightPanelDate = this.dates[1]? this.dates[1].getTime() : this.dates[1];
|
||||
this.rightPanelDate = this.splitPanels ? new Date(Math.max(splitRightPanelDate, rightPanelDate.getTime())) : rightPanelDate;
|
||||
},
|
||||
panelLabelConfig (direction) {
|
||||
const locale = this.t('i.locale');
|
||||
const datePanelLabel = this.t('i.datepicker.datePanelLabel');
|
||||
const handler = type => {
|
||||
const fn = type == 'month' ? this.showMonthPicker : this.showYearPicker;
|
||||
return () => fn(direction);
|
||||
};
|
||||
|
||||
const date = this[`${direction}PanelDate`];
|
||||
const { labels, separator } = formatDateLabels(locale, datePanelLabel, date);
|
||||
|
||||
return {
|
||||
separator: separator,
|
||||
labels: labels.map(obj => ((obj.handler = handler(obj.type)), obj))
|
||||
};
|
||||
},
|
||||
prevYear (panel) {
|
||||
const increment = this.currentView === 'year' ? -10 : -1;
|
||||
this.changePanelDate(panel, 'FullYear', increment);
|
||||
},
|
||||
nextYear (panel) {
|
||||
const increment = this.currentView === 'year' ? 10 : 1;
|
||||
this.changePanelDate(panel, 'FullYear', increment);
|
||||
},
|
||||
prevMonth(panel){
|
||||
this.changePanelDate(panel, 'Month', -1);
|
||||
},
|
||||
nextMonth(panel){
|
||||
this.changePanelDate(panel, 'Month', 1);
|
||||
},
|
||||
changePanelDate(panel, type, increment, updateOtherPanel = true){
|
||||
const current = new Date(this[`${panel}PanelDate`]);
|
||||
current[`set${type}`](current[`get${type}`]() + increment);
|
||||
this[`${panel}PanelDate`] = current;
|
||||
|
||||
if (!updateOtherPanel) return;
|
||||
|
||||
if (this.splitPanels){
|
||||
// change other panel if dates overlap
|
||||
const otherPanel = panel === 'left' ? 'right' : 'left';
|
||||
if (panel === 'left' && this.leftPanelDate >= this.rightPanelDate){
|
||||
this.changePanelDate(otherPanel, type, 1);
|
||||
}
|
||||
if (panel === 'right' && this.rightPanelDate <= this.leftPanelDate){
|
||||
this.changePanelDate(otherPanel, type, -1);
|
||||
}
|
||||
} else {
|
||||
// keep the panels together
|
||||
const otherPanel = panel === 'left' ? 'right' : 'left';
|
||||
const currentDate = this[`${otherPanel}PanelDate`];
|
||||
const temp = new Date(currentDate);
|
||||
|
||||
if (type === 'Month') {
|
||||
const nextMonthLastDate = new Date(
|
||||
temp.getFullYear(), temp.getMonth() + increment + 1, 0
|
||||
).getDate();
|
||||
temp.setDate(Math.min(nextMonthLastDate, temp.getDate()));
|
||||
}
|
||||
|
||||
temp[`set${type}`](temp[`get${type}`]() + increment);
|
||||
this[`${otherPanel}PanelDate`] = temp;
|
||||
}
|
||||
},
|
||||
showYearPicker (panel) {
|
||||
this[`${panel}PickerTable`] = 'year-table';
|
||||
},
|
||||
showMonthPicker (panel) {
|
||||
this[`${panel}PickerTable`] = 'month-table';
|
||||
},
|
||||
handlePreSelection(panel, value){
|
||||
this[`${panel}PanelDate`] = value;
|
||||
const currentViewType = this[`${panel}PickerTable`];
|
||||
if (currentViewType === 'year-table') this[`${panel}PickerTable`] = 'month-table';
|
||||
else this[`${panel}PickerTable`] = `${this.currentView}-table`;
|
||||
|
||||
if (!this.splitPanels){
|
||||
const otherPanel = panel === 'left' ? 'right' : 'left';
|
||||
this[`${otherPanel}PanelDate`] = value;
|
||||
|
||||
const increment = otherPanel === 'left' ? -1 : 1; // #3973
|
||||
|
||||
this.changePanelDate(otherPanel, 'Month', increment, false);
|
||||
}
|
||||
},
|
||||
handleRangePick (val, type) {
|
||||
if (this.rangeState.selecting || this.currentView === 'time'){
|
||||
if (this.currentView === 'time'){
|
||||
this.dates = val;
|
||||
} else {
|
||||
const [minDate, maxDate] = [this.rangeState.from, val].sort(dateSorter);
|
||||
this.dates = [minDate, maxDate];
|
||||
this.rangeState = {
|
||||
from: minDate,
|
||||
to: maxDate,
|
||||
selecting: false
|
||||
};
|
||||
}
|
||||
this.handleConfirm(false, type || 'date');
|
||||
} else {
|
||||
this.rangeState = {
|
||||
from: val,
|
||||
to: null,
|
||||
selecting: true
|
||||
};
|
||||
}
|
||||
},
|
||||
handleChangeRange (val) {
|
||||
this.rangeState.to = val;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
213
src/components/date-picker/panel/Date/date.vue
Normal file
213
src/components/date-picker/panel/Date/date.vue
Normal file
|
@ -0,0 +1,213 @@
|
|||
<template>
|
||||
<div :class="classes" @mousedown.prevent>
|
||||
<div :class="[prefixCls + '-sidebar']" v-if="shortcuts.length">
|
||||
<div
|
||||
:class="[prefixCls + '-shortcut']"
|
||||
v-for="shortcut in shortcuts"
|
||||
@click="handleShortcutClick(shortcut)">{{ shortcut.text }}</div>
|
||||
</div>
|
||||
<div :class="[prefixCls + '-body']">
|
||||
<div :class="[datePrefixCls + '-header']" v-show="currentView !== 'time'">
|
||||
<span
|
||||
:class="iconBtnCls('prev', '-double')"
|
||||
@click="changeYear(-1)"><Icon type="ios-arrow-back"></Icon></span>
|
||||
<span
|
||||
v-if="pickerTable === 'date-table'"
|
||||
:class="iconBtnCls('prev')"
|
||||
@click="changeMonth(-1)"
|
||||
v-show="currentView === 'date'"><Icon type="ios-arrow-back"></Icon></span>
|
||||
<date-panel-label
|
||||
:date-panel-label="datePanelLabel"
|
||||
:current-view="pickerTable.split('-').shift()"
|
||||
:date-prefix-cls="datePrefixCls"></date-panel-label>
|
||||
<span
|
||||
:class="iconBtnCls('next', '-double')"
|
||||
@click="changeYear(+1)"><Icon type="ios-arrow-forward"></Icon></span>
|
||||
<span
|
||||
v-if="pickerTable === 'date-table'"
|
||||
:class="iconBtnCls('next')"
|
||||
@click="changeMonth(+1)"
|
||||
v-show="currentView === 'date'"><Icon type="ios-arrow-forward"></Icon></span>
|
||||
</div>
|
||||
<div :class="[prefixCls + '-content']">
|
||||
<component
|
||||
:is="pickerTable"
|
||||
ref="pickerTable"
|
||||
v-if="currentView !== 'time'"
|
||||
:table-date="panelDate"
|
||||
:show-week-numbers="showWeekNumbers"
|
||||
:value="dates"
|
||||
:selection-mode="selectionMode"
|
||||
:disabled-date="disabledDate"
|
||||
:focused-date="focusedDate"
|
||||
|
||||
@on-pick="panelPickerHandlers"
|
||||
@on-pick-click="handlePickClick"
|
||||
></component>
|
||||
</div>
|
||||
<div :class="[prefixCls + '-content']" v-show="isTime">
|
||||
<time-picker
|
||||
ref="timePicker"
|
||||
v-if="currentView === 'time'"
|
||||
:value="dates"
|
||||
:format="format"
|
||||
:time-disabled="timeDisabled"
|
||||
:disabled-date="disabledDate"
|
||||
:focused-date="focusedDate"
|
||||
|
||||
v-bind="timePickerOptions"
|
||||
@on-pick="handlePick"
|
||||
@on-pick-click="handlePickClick"
|
||||
@on-pick-clear="handlePickClear"
|
||||
@on-pick-success="handlePickSuccess"
|
||||
@on-pick-toggle-time="handleToggleTime"
|
||||
></time-picker>
|
||||
</div>
|
||||
<Confirm
|
||||
v-if="confirm"
|
||||
:show-time="showTime"
|
||||
:is-time="isTime"
|
||||
@on-pick-toggle-time="handleToggleTime"
|
||||
@on-pick-clear="handlePickClear"
|
||||
@on-pick-success="handlePickSuccess"
|
||||
></Confirm>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Icon from '../../../icon/icon.vue';
|
||||
import DateTable from '../../base/date-table.vue';
|
||||
import YearTable from '../../base/year-table.vue';
|
||||
import MonthTable from '../../base/month-table.vue';
|
||||
import TimePicker from '../Time/time.vue';
|
||||
import Confirm from '../../base/confirm.vue';
|
||||
import datePanelLabel from './date-panel-label.vue';
|
||||
|
||||
import Mixin from '../panel-mixin';
|
||||
import DateMixin from './date-panel-mixin';
|
||||
import Locale from '../../../../mixins/locale';
|
||||
|
||||
import { siblingMonth, formatDateLabels } from '../../util';
|
||||
|
||||
const prefixCls = 'ivu-picker-panel';
|
||||
const datePrefixCls = 'ivu-date-picker';
|
||||
|
||||
export default {
|
||||
name: 'DatePickerPanel',
|
||||
mixins: [ Mixin, Locale, DateMixin ],
|
||||
components: { Icon, DateTable, YearTable, MonthTable, TimePicker, Confirm, datePanelLabel },
|
||||
props: {
|
||||
// more props in the mixin
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
const {selectionMode, value} = this;
|
||||
|
||||
const dates = value.slice().sort();
|
||||
return {
|
||||
prefixCls: prefixCls,
|
||||
datePrefixCls: datePrefixCls,
|
||||
currentView: selectionMode || 'date',
|
||||
pickerTable: this.getTableType(selectionMode),
|
||||
dates: dates,
|
||||
panelDate: this.startDate || dates[0] || new Date()
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
return [
|
||||
`${prefixCls}-body-wrapper`,
|
||||
{
|
||||
[`${prefixCls}-with-sidebar`]: this.shortcuts.length
|
||||
}
|
||||
];
|
||||
},
|
||||
panelPickerHandlers(){
|
||||
return this.pickerTable === `${this.currentView}-table` ? this.handlePick : this.handlePreSelection;
|
||||
},
|
||||
datePanelLabel () {
|
||||
const locale = this.t('i.locale');
|
||||
const datePanelLabel = this.t('i.datepicker.datePanelLabel');
|
||||
const date = this.panelDate;
|
||||
const { labels, separator } = formatDateLabels(locale, datePanelLabel, date);
|
||||
|
||||
const handler = type => {
|
||||
return () => this.pickerTable = this.getTableType(type);
|
||||
};
|
||||
|
||||
return {
|
||||
separator: separator,
|
||||
labels: labels.map(obj => ((obj.handler = handler(obj.type)), obj))
|
||||
};
|
||||
},
|
||||
timeDisabled(){
|
||||
return !this.dates[0];
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value (newVal) {
|
||||
this.dates = newVal;
|
||||
const panelDate = this.multiple ? this.dates[this.dates.length - 1] : (this.startDate || this.dates[0]);
|
||||
this.panelDate = panelDate || new Date();
|
||||
},
|
||||
currentView (currentView) {
|
||||
this.$emit('on-selection-mode-change', currentView);
|
||||
|
||||
if (this.currentView === 'time') {
|
||||
this.$nextTick(() => {
|
||||
const spinner = this.$refs.timePicker.$refs.timeSpinner;
|
||||
spinner.updateScroll();
|
||||
});
|
||||
}
|
||||
},
|
||||
selectionMode(type){
|
||||
this.currentView = type;
|
||||
this.pickerTable = this.getTableType(type);
|
||||
},
|
||||
focusedDate(date){
|
||||
const isDifferentYear = date.getFullYear() !== this.panelDate.getFullYear();
|
||||
const isDifferentMonth = isDifferentYear || date.getMonth() !== this.panelDate.getMonth();
|
||||
if (isDifferentYear || isDifferentMonth){
|
||||
if (!this.multiple) this.panelDate = date;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
reset(){
|
||||
this.currentView = this.selectionMode;
|
||||
this.pickerTable = this.getTableType(this.currentView);
|
||||
},
|
||||
changeYear(dir){
|
||||
if (this.selectionMode === 'year' || this.pickerTable === 'year-table'){
|
||||
this.panelDate = new Date(this.panelDate.getFullYear() + dir * 10, 0, 1);
|
||||
} else {
|
||||
this.panelDate = siblingMonth(this.panelDate, dir * 12);
|
||||
}
|
||||
},
|
||||
getTableType(currentView){
|
||||
return currentView.match(/^time/) ? 'time-picker' : `${currentView}-table`;
|
||||
},
|
||||
changeMonth(dir){
|
||||
this.panelDate = siblingMonth(this.panelDate, dir);
|
||||
},
|
||||
handlePreSelection(value){
|
||||
this.panelDate = value;
|
||||
if (this.pickerTable === 'year-table') this.pickerTable = 'month-table';
|
||||
else this.pickerTable = this.getTableType(this.currentView);
|
||||
|
||||
},
|
||||
handlePick (value, type) {
|
||||
const {selectionMode, panelDate} = this;
|
||||
if (selectionMode === 'year') value = new Date(value.getFullYear(), 0, 1);
|
||||
else if (selectionMode === 'month') value = new Date(panelDate.getFullYear(), value.getMonth(), 1);
|
||||
else value = new Date(value);
|
||||
|
||||
this.dates = [value];
|
||||
this.$emit('on-pick', value, false, type || selectionMode);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
162
src/components/date-picker/panel/Time/time-range.vue
Normal file
162
src/components/date-picker/panel/Time/time-range.vue
Normal file
|
@ -0,0 +1,162 @@
|
|||
<template>
|
||||
<div :class="classes" @mousedown.prevent>
|
||||
<div :class="[prefixCls + '-body']">
|
||||
<div :class="[prefixCls + '-content', prefixCls + '-content-left']">
|
||||
<div :class="[timePrefixCls + '-header']">
|
||||
<template v-if="showDate">{{ leftDatePanelLabel }}</template>
|
||||
<template v-else>{{ t('i.datepicker.startTime') }}</template>
|
||||
</div>
|
||||
<time-spinner
|
||||
ref="timeSpinner"
|
||||
:steps="steps"
|
||||
:show-seconds="showSeconds"
|
||||
:hours="value[0] && dateStart.getHours()"
|
||||
:minutes="value[0] && dateStart.getMinutes()"
|
||||
:seconds="value[0] && dateStart.getSeconds()"
|
||||
:disabled-hours="disabledHours"
|
||||
:disabled-minutes="disabledMinutes"
|
||||
:disabled-seconds="disabledSeconds"
|
||||
:hide-disabled-options="hideDisabledOptions"
|
||||
@on-change="handleStartChange"
|
||||
@on-pick-click="handlePickClick"></time-spinner>
|
||||
</div>
|
||||
<div :class="[prefixCls + '-content', prefixCls + '-content-right']">
|
||||
<div :class="[timePrefixCls + '-header']">
|
||||
<template v-if="showDate">{{ rightDatePanelLabel }}</template>
|
||||
<template v-else>{{ t('i.datepicker.endTime') }}</template>
|
||||
</div>
|
||||
<time-spinner
|
||||
ref="timeSpinnerEnd"
|
||||
:steps="steps"
|
||||
:show-seconds="showSeconds"
|
||||
:hours="value[1] && dateEnd.getHours()"
|
||||
:minutes="value[1] && dateEnd.getMinutes()"
|
||||
:seconds="value[1] && dateEnd.getSeconds()"
|
||||
:disabled-hours="disabledHours"
|
||||
:disabled-minutes="disabledMinutes"
|
||||
:disabled-seconds="disabledSeconds"
|
||||
:hide-disabled-options="hideDisabledOptions"
|
||||
@on-change="handleEndChange"
|
||||
@on-pick-click="handlePickClick"></time-spinner>
|
||||
</div>
|
||||
<Confirm
|
||||
v-if="confirm"
|
||||
@on-pick-clear="handlePickClear"
|
||||
@on-pick-success="handlePickSuccess"></Confirm>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import TimeSpinner from '../../base/time-spinner.vue';
|
||||
import Confirm from '../../base/confirm.vue';
|
||||
import Options from '../../time-mixins';
|
||||
|
||||
|
||||
import Mixin from '../panel-mixin';
|
||||
import Locale from '../../../../mixins/locale';
|
||||
|
||||
import { initTimeDate, formatDateLabels } from '../../util';
|
||||
|
||||
const prefixCls = 'ivu-picker-panel';
|
||||
const timePrefixCls = 'ivu-time-picker';
|
||||
|
||||
const capitalize = (str) => str[0].toUpperCase() + str.slice(1);
|
||||
|
||||
export default {
|
||||
name: 'RangeTimePickerPanel',
|
||||
mixins: [ Mixin, Locale, Options ],
|
||||
components: { TimeSpinner, Confirm },
|
||||
props: {
|
||||
steps: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
format: {
|
||||
type: String,
|
||||
default: 'HH:mm:ss'
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
data () {
|
||||
const [dateStart, dateEnd] = this.value.slice();
|
||||
return {
|
||||
prefixCls: prefixCls,
|
||||
timePrefixCls: timePrefixCls,
|
||||
showDate: false,
|
||||
dateStart: dateStart || initTimeDate(),
|
||||
dateEnd: dateEnd || initTimeDate()
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
return [
|
||||
`${prefixCls}-body-wrapper`,
|
||||
`${timePrefixCls}-with-range`,
|
||||
{
|
||||
[`${timePrefixCls}-with-seconds`]: this.showSeconds
|
||||
}
|
||||
];
|
||||
},
|
||||
showSeconds () {
|
||||
return !(this.format || '').match(/mm$/);
|
||||
},
|
||||
leftDatePanelLabel () {
|
||||
return this.panelLabelConfig(this.date);
|
||||
},
|
||||
rightDatePanelLabel () {
|
||||
return this.panelLabelConfig(this.dateEnd);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value (dates) {
|
||||
const [dateStart, dateEnd] = dates.slice();
|
||||
this.dateStart = dateStart || initTimeDate();
|
||||
this.dateEnd = dateEnd || initTimeDate();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
panelLabelConfig (date) {
|
||||
const locale = this.t('i.locale');
|
||||
const datePanelLabel = this.t('i.datepicker.datePanelLabel');
|
||||
const { labels, separator } = formatDateLabels(locale, datePanelLabel, date || initTimeDate());
|
||||
return [labels[0].label, separator, labels[1].label].join('');
|
||||
},
|
||||
handleChange (start, end, emit = true) {
|
||||
|
||||
const dateStart = new Date(this.dateStart);
|
||||
let dateEnd = new Date(this.dateEnd);
|
||||
|
||||
// set dateStart
|
||||
Object.keys(start).forEach(type => {
|
||||
dateStart[`set${capitalize(type)}`](start[type]);
|
||||
});
|
||||
|
||||
// set dateEnd
|
||||
Object.keys(end).forEach(type => {
|
||||
dateEnd[`set${capitalize(type)}`](end[type]);
|
||||
});
|
||||
|
||||
// judge endTime > startTime?
|
||||
if (dateEnd < dateStart) dateEnd = dateStart;
|
||||
|
||||
if (emit) this.$emit('on-pick', [dateStart, dateEnd], 'time');
|
||||
},
|
||||
handleStartChange (date) {
|
||||
this.handleChange(date, {});
|
||||
},
|
||||
handleEndChange (date) {
|
||||
this.handleChange({}, date);
|
||||
},
|
||||
updateScroll () {
|
||||
this.$refs.timeSpinner.updateScroll();
|
||||
this.$refs.timeSpinnerEnd.updateScroll();
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.$parent && this.$parent.$options.name === 'DatePicker') this.showDate = true;
|
||||
}
|
||||
};
|
||||
</script>
|
145
src/components/date-picker/panel/Time/time.vue
Normal file
145
src/components/date-picker/panel/Time/time.vue
Normal file
|
@ -0,0 +1,145 @@
|
|||
<template>
|
||||
<div :class="[prefixCls + '-body-wrapper']" @mousedown.prevent>
|
||||
<div :class="[prefixCls + '-body']">
|
||||
<div :class="[timePrefixCls + '-header']" v-if="showDate">{{ visibleDate }}</div>
|
||||
<div :class="[prefixCls + '-content']">
|
||||
<time-spinner
|
||||
ref="timeSpinner"
|
||||
:show-seconds="showSeconds"
|
||||
:steps="steps"
|
||||
:hours="timeSlots[0]"
|
||||
:minutes="timeSlots[1]"
|
||||
:seconds="timeSlots[2]"
|
||||
:disabled-hours="disabledHMS.disabledHours"
|
||||
:disabled-minutes="disabledHMS.disabledMinutes"
|
||||
:disabled-seconds="disabledHMS.disabledSeconds"
|
||||
:hide-disabled-options="hideDisabledOptions"
|
||||
@on-change="handleChange"
|
||||
@on-pick-click="handlePickClick"></time-spinner>
|
||||
</div>
|
||||
<Confirm
|
||||
v-if="confirm"
|
||||
@on-pick-clear="handlePickClear"
|
||||
@on-pick-success="handlePickSuccess"></Confirm>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import TimeSpinner from '../../base/time-spinner.vue';
|
||||
import Confirm from '../../base/confirm.vue';
|
||||
import Options from '../../time-mixins';
|
||||
|
||||
|
||||
import Mixin from '../panel-mixin';
|
||||
import Locale from '../../../../mixins/locale';
|
||||
|
||||
import { initTimeDate } from '../../util';
|
||||
|
||||
const prefixCls = 'ivu-picker-panel';
|
||||
const timePrefixCls = 'ivu-time-picker';
|
||||
|
||||
const capitalize = (str) => str[0].toUpperCase() + str.slice(1);
|
||||
const mergeDateHMS = (date, hours, minutes, seconds) => {
|
||||
const newDate = new Date(date.getTime());
|
||||
newDate.setHours(hours);
|
||||
newDate.setMinutes(minutes);
|
||||
newDate.setSeconds(seconds);
|
||||
return newDate;
|
||||
};
|
||||
const unique = (el, i, arr) => arr.indexOf(el) === i;
|
||||
const returnFalse = () => false;
|
||||
|
||||
export default {
|
||||
name: 'TimePickerPanel',
|
||||
mixins: [ Mixin, Locale, Options ],
|
||||
components: { TimeSpinner, Confirm },
|
||||
props: {
|
||||
disabledDate: {
|
||||
type: Function,
|
||||
default: returnFalse
|
||||
},
|
||||
steps: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
format: {
|
||||
type: String,
|
||||
default: 'HH:mm:ss'
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
prefixCls: prefixCls,
|
||||
timePrefixCls: timePrefixCls,
|
||||
date: this.value[0] || initTimeDate(),
|
||||
showDate: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showSeconds () {
|
||||
return !(this.format || '').match(/mm$/);
|
||||
},
|
||||
visibleDate () { // TODO
|
||||
const date = this.date;
|
||||
const month = date.getMonth() + 1;
|
||||
const tYear = this.t('i.datepicker.year');
|
||||
const tMonth = this.t(`i.datepicker.month${month}`);
|
||||
return `${date.getFullYear()}${tYear} ${tMonth}`;
|
||||
},
|
||||
timeSlots(){
|
||||
if (!this.value[0]) return [];
|
||||
return ['getHours', 'getMinutes', 'getSeconds'].map(slot => this.date[slot]());
|
||||
},
|
||||
disabledHMS(){
|
||||
const disabledTypes = ['disabledHours', 'disabledMinutes', 'disabledSeconds'];
|
||||
if (this.disabledDate === returnFalse || !this.value[0]) {
|
||||
const disabled = disabledTypes.reduce(
|
||||
(obj, type) => (obj[type] = this[type], obj), {}
|
||||
);
|
||||
return disabled;
|
||||
} else {
|
||||
const slots = [24, 60, 60];
|
||||
const disabled = ['Hours', 'Minutes', 'Seconds'].map(type => this[`disabled${type}`]);
|
||||
const disabledHMS = disabled.map((preDisabled, j) => {
|
||||
const slot = slots[j];
|
||||
const toDisable = preDisabled;
|
||||
for (let i = 0; i < slot; i+= (this.steps[j] || 1)){
|
||||
const hms = this.timeSlots.map((slot, x) => x === j ? i : slot);
|
||||
const testDateTime = mergeDateHMS(this.date, ...hms);
|
||||
if (this.disabledDate(testDateTime, true)) toDisable.push(i);
|
||||
}
|
||||
return toDisable.filter(unique);
|
||||
});
|
||||
return disabledTypes.reduce(
|
||||
(obj, type, i) => (obj[type] = disabledHMS[i], obj), {}
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value (dates) {
|
||||
let newVal = dates[0] || initTimeDate();
|
||||
newVal = new Date(newVal);
|
||||
this.date = newVal;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleChange (date, emit = true) {
|
||||
|
||||
const newDate = new Date(this.date);
|
||||
Object.keys(date).forEach(
|
||||
type => newDate[`set${capitalize(type)}`](date[type])
|
||||
);
|
||||
|
||||
if (emit) this.$emit('on-pick', newDate, 'time');
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
if (this.$parent && this.$parent.$options.name === 'DatePicker') this.showDate = true;
|
||||
}
|
||||
};
|
||||
</script>
|
56
src/components/date-picker/panel/panel-mixin.js
Normal file
56
src/components/date-picker/panel/panel-mixin.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
const prefixCls = 'ivu-picker-panel';
|
||||
const datePrefixCls = 'ivu-date-picker';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
confirm: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
iconBtnCls (direction, type = '') {
|
||||
return [
|
||||
`${prefixCls}-icon-btn`,
|
||||
`${datePrefixCls}-${direction}-btn`,
|
||||
`${datePrefixCls}-${direction}-btn-arrow${type}`,
|
||||
];
|
||||
},
|
||||
handleShortcutClick (shortcut) {
|
||||
if (shortcut.value) this.$emit('on-pick', shortcut.value());
|
||||
if (shortcut.onClick) shortcut.onClick(this);
|
||||
},
|
||||
handlePickClear () {
|
||||
this.resetView();
|
||||
this.$emit('on-pick-clear');
|
||||
},
|
||||
handlePickSuccess () {
|
||||
this.resetView();
|
||||
this.$emit('on-pick-success');
|
||||
},
|
||||
handlePickClick () {
|
||||
this.$emit('on-pick-click');
|
||||
},
|
||||
resetView(){
|
||||
setTimeout(
|
||||
() => this.currentView = this.selectionMode,
|
||||
500 // 500ms so the dropdown can close before changing
|
||||
);
|
||||
},
|
||||
handleClear() {
|
||||
this.dates = this.dates.map(() => null);
|
||||
this.rangeState = {};
|
||||
this.$emit('on-pick', this.dates);
|
||||
this.handleConfirm();
|
||||
// if (this.showTime) this.$refs.timePicker.handleClear();
|
||||
},
|
||||
handleConfirm(visible, type) {
|
||||
this.$emit('on-pick', this.dates, visible, type || this.type);
|
||||
},
|
||||
onToggleVisibility(open){
|
||||
const {timeSpinner, timeSpinnerEnd} = this.$refs;
|
||||
if (open && timeSpinner) timeSpinner.updateScroll();
|
||||
if (open && timeSpinnerEnd) timeSpinnerEnd.updateScroll();
|
||||
}
|
||||
}
|
||||
};
|
798
src/components/date-picker/picker.vue
Normal file
798
src/components/date-picker/picker.vue
Normal file
|
@ -0,0 +1,798 @@
|
|||
<template>
|
||||
<div
|
||||
:class="wrapperClasses"
|
||||
v-click-outside:mousedown.capture="handleClose"
|
||||
v-click-outside:touchstart.capture="handleClose"
|
||||
v-click-outside.capture="handleClose"
|
||||
>
|
||||
<div ref="reference" :class="[prefixCls + '-rel']">
|
||||
<slot>
|
||||
<i-input
|
||||
:key="forceInputRerender"
|
||||
:element-id="elementId"
|
||||
:class="[prefixCls + '-editor']"
|
||||
:readonly="!editable || readonly"
|
||||
:disabled="disabled"
|
||||
:size="size"
|
||||
:placeholder="placeholder"
|
||||
:value="visualValue"
|
||||
:name="name"
|
||||
ref="input"
|
||||
|
||||
@on-input-change="handleInputChange"
|
||||
@on-focus="handleFocus"
|
||||
@on-blur="handleBlur"
|
||||
@click.native="handleFocus"
|
||||
@keydown.native="handleKeydown"
|
||||
@mouseenter.native="handleInputMouseenter"
|
||||
@mouseleave.native="handleInputMouseleave"
|
||||
>
|
||||
<Icon @click="handleIconClick" :type="arrowType" :custom="customArrowType" :size="arrowSize" slot="suffix" />
|
||||
</i-input>
|
||||
</slot>
|
||||
</div>
|
||||
<transition name="transition-drop">
|
||||
<Drop
|
||||
@click.native="handleTransferClick"
|
||||
v-show="opened"
|
||||
:class="{ [prefixCls + '-transfer']: transfer }"
|
||||
:placement="placement"
|
||||
ref="drop"
|
||||
:data-transfer="transfer"
|
||||
:transfer="transfer"
|
||||
v-transfer-dom>
|
||||
<div>
|
||||
<component
|
||||
:is="panel"
|
||||
ref="pickerPanel"
|
||||
:visible="visible"
|
||||
:showTime="type === 'datetime' || type === 'datetimerange'"
|
||||
:confirm="isConfirm"
|
||||
:selectionMode="selectionMode"
|
||||
:steps="steps"
|
||||
:format="format"
|
||||
:value="internalValue"
|
||||
:start-date="startDate"
|
||||
:split-panels="splitPanels"
|
||||
:show-week-numbers="showWeekNumbers"
|
||||
:picker-type="type"
|
||||
:multiple="multiple"
|
||||
:focused-date="focusedDate"
|
||||
|
||||
:time-picker-options="timePickerOptions"
|
||||
|
||||
v-bind="ownPickerProps"
|
||||
|
||||
@on-pick="onPick"
|
||||
@on-pick-clear="handleClear"
|
||||
@on-pick-success="onPickSuccess"
|
||||
@on-pick-click="disableClickOutSide = true"
|
||||
@on-selection-mode-change="onSelectionModeChange"
|
||||
></component>
|
||||
</div>
|
||||
</Drop>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
|
||||
import iInput from '../../components/input/input.vue';
|
||||
import Drop from '../../components/select/dropdown.vue';
|
||||
import Icon from '../../components/icon/icon.vue';
|
||||
import {directive as clickOutside} from 'v-click-outside-x';
|
||||
import TransferDom from '../../directives/transfer-dom';
|
||||
import { oneOf } from '../../utils/assist';
|
||||
import { DEFAULT_FORMATS, TYPE_VALUE_RESOLVER_MAP, getDayCountOfMonth } from './util';
|
||||
import {findComponentsDownward} from '../../utils/assist';
|
||||
import Emitter from '../../mixins/emitter';
|
||||
|
||||
const prefixCls = 'ivu-date-picker';
|
||||
const pickerPrefixCls = 'ivu-picker';
|
||||
|
||||
const isEmptyArray = val => val.reduce((isEmpty, str) => isEmpty && !str || (typeof str === 'string' && str.trim() === ''), true);
|
||||
const keyValueMapper = {
|
||||
40: 'up',
|
||||
39: 'right',
|
||||
38: 'down',
|
||||
37: 'left',
|
||||
};
|
||||
|
||||
const mapPossibleValues = (key, horizontal, vertical) => {
|
||||
if (key === 'left') return horizontal * -1;
|
||||
if (key === 'right') return horizontal * 1;
|
||||
if (key === 'up') return vertical * 1;
|
||||
if (key === 'down') return vertical * -1;
|
||||
};
|
||||
|
||||
const pulseElement = (el) => {
|
||||
const pulseClass = 'ivu-date-picker-btn-pulse';
|
||||
el.classList.add(pulseClass);
|
||||
setTimeout(() => el.classList.remove(pulseClass), 200);
|
||||
};
|
||||
|
||||
const extractTime = date => {
|
||||
if (!date) return [0, 0, 0];
|
||||
return [
|
||||
date.getHours(), date.getMinutes(), date.getSeconds()
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
export default {
|
||||
mixins: [ Emitter ],
|
||||
components: { iInput, Drop, Icon },
|
||||
directives: { clickOutside, TransferDom },
|
||||
props: {
|
||||
format: {
|
||||
type: String
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
clearable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
confirm: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
open: {
|
||||
type: Boolean,
|
||||
default: null
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
timePickerOptions: {
|
||||
default: () => ({}),
|
||||
type: Object,
|
||||
},
|
||||
splitPanels: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showWeekNumbers: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
startDate: {
|
||||
type: Date
|
||||
},
|
||||
size: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['small', 'large', 'default']);
|
||||
},
|
||||
default () {
|
||||
return !this.$IVIEW || this.$IVIEW.size === '' ? 'default' : this.$IVIEW.size;
|
||||
}
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
placement: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'left-start', 'left-end', 'right', 'right-start', 'right-end']);
|
||||
},
|
||||
default: 'bottom-start'
|
||||
},
|
||||
transfer: {
|
||||
type: Boolean,
|
||||
default () {
|
||||
return !this.$IVIEW || this.$IVIEW.transfer === '' ? false : this.$IVIEW.transfer;
|
||||
}
|
||||
},
|
||||
name: {
|
||||
type: String
|
||||
},
|
||||
elementId: {
|
||||
type: String
|
||||
},
|
||||
steps: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
value: {
|
||||
type: [Date, String, Array]
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
separator: {
|
||||
type: String,
|
||||
default: ' - '
|
||||
}
|
||||
},
|
||||
data(){
|
||||
const isRange = this.type.includes('range');
|
||||
const emptyArray = isRange ? [null, null] : [null];
|
||||
const initialValue = isEmptyArray((isRange ? this.value : [this.value]) || []) ? emptyArray : this.parseDate(this.value);
|
||||
const focusedTime = initialValue.map(extractTime);
|
||||
|
||||
return {
|
||||
prefixCls: prefixCls,
|
||||
showClose: false,
|
||||
visible: false,
|
||||
internalValue: initialValue,
|
||||
disableClickOutSide: false, // fixed when click a date,trigger clickoutside to close picker
|
||||
disableCloseUnderTransfer: false, // transfer 模式下,点击Drop也会触发关闭,
|
||||
selectionMode: this.onSelectionModeChange(this.type),
|
||||
forceInputRerender: 1,
|
||||
isFocused: false,
|
||||
focusedDate: initialValue[0] || this.startDate || new Date(),
|
||||
focusedTime: {
|
||||
column: 0, // which column inside the picker
|
||||
picker: 0, // which picker
|
||||
time: focusedTime, // the values array into [hh, mm, ss],
|
||||
active: false
|
||||
},
|
||||
internalFocus: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
wrapperClasses(){
|
||||
return [prefixCls, {
|
||||
[prefixCls + '-focused']: this.isFocused
|
||||
}];
|
||||
},
|
||||
publicVModelValue(){
|
||||
if (this.multiple){
|
||||
return this.internalValue.slice();
|
||||
} else {
|
||||
const isRange = this.type.includes('range');
|
||||
let val = this.internalValue.map(date => date instanceof Date ? new Date(date) : (date || ''));
|
||||
|
||||
if (this.type.match(/^time/)) val = val.map(this.formatDate);
|
||||
return (isRange || this.multiple) ? val : val[0];
|
||||
}
|
||||
},
|
||||
publicStringValue(){
|
||||
const {formatDate, publicVModelValue, type} = this;
|
||||
if (type.match(/^time/)) return publicVModelValue;
|
||||
if (this.multiple) return formatDate(publicVModelValue);
|
||||
return Array.isArray(publicVModelValue) ? publicVModelValue.map(formatDate) : formatDate(publicVModelValue);
|
||||
},
|
||||
opened () {
|
||||
return this.open === null ? this.visible : this.open;
|
||||
},
|
||||
transition () {
|
||||
const bottomPlaced = this.placement.match(/^bottom/);
|
||||
return bottomPlaced ? 'slide-up' : 'slide-down';
|
||||
},
|
||||
visualValue() {
|
||||
return this.formatDate(this.internalValue);
|
||||
},
|
||||
isConfirm(){
|
||||
return this.confirm || this.type === 'datetime' || this.type === 'datetimerange' || this.multiple;
|
||||
},
|
||||
// 3.4.0, global setting customArrow 有值时,arrow 赋值空
|
||||
arrowType () {
|
||||
let type = '';
|
||||
|
||||
if (this.type === 'time' || this.type === 'timerange') {
|
||||
type = 'ios-time-outline';
|
||||
|
||||
if (this.$IVIEW) {
|
||||
if (this.$IVIEW.timePicker.customIcon) {
|
||||
type = '';
|
||||
} else if (this.$IVIEW.timePicker.icon) {
|
||||
type = this.$IVIEW.timePicker.icon;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
type = 'ios-calendar-outline';
|
||||
|
||||
if (this.$IVIEW) {
|
||||
if (this.$IVIEW.datePicker.customIcon) {
|
||||
type = '';
|
||||
} else if (this.$IVIEW.datePicker.icon) {
|
||||
type = this.$IVIEW.datePicker.icon;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.showClose) type = 'ios-close-circle';
|
||||
|
||||
return type;
|
||||
},
|
||||
// 3.4.0, global setting
|
||||
customArrowType () {
|
||||
let type = '';
|
||||
|
||||
if (!this.showClose) {
|
||||
if (this.type === 'time' || this.type === 'timerange') {
|
||||
if (this.$IVIEW) {
|
||||
if (this.$IVIEW.timePicker.customIcon) {
|
||||
type = this.$IVIEW.timePicker.customIcon;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.$IVIEW) {
|
||||
if (this.$IVIEW.datePicker.customIcon) {
|
||||
type = this.$IVIEW.datePicker.customIcon;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return type;
|
||||
},
|
||||
// 3.4.0, global setting
|
||||
arrowSize () {
|
||||
let size = '';
|
||||
|
||||
if (!this.showClose) {
|
||||
if (this.type === 'time' || this.type === 'timerange') {
|
||||
if (this.$IVIEW) {
|
||||
if (this.$IVIEW.timePicker.iconSize) {
|
||||
size = this.$IVIEW.timePicker.iconSize;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.$IVIEW) {
|
||||
if (this.$IVIEW.datePicker.iconSize) {
|
||||
size = this.$IVIEW.datePicker.iconSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSelectionModeChange(type){
|
||||
if (type.match(/^date/)) type = 'date';
|
||||
this.selectionMode = oneOf(type, ['year', 'month', 'date', 'time']) && type;
|
||||
return this.selectionMode;
|
||||
},
|
||||
// 开启 transfer 时,点击 Drop 即会关闭,这里不让其关闭
|
||||
handleTransferClick () {
|
||||
if (this.transfer) this.disableCloseUnderTransfer = true;
|
||||
},
|
||||
handleClose (e) {
|
||||
if (this.disableCloseUnderTransfer) {
|
||||
this.disableCloseUnderTransfer = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e && e.type === 'mousedown' && this.visible) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.visible) {
|
||||
const pickerPanel = this.$refs.pickerPanel && this.$refs.pickerPanel.$el;
|
||||
if (e && pickerPanel && pickerPanel.contains(e.target)) return; // its a click inside own component, lets ignore it.
|
||||
|
||||
this.visible = false;
|
||||
e && e.preventDefault();
|
||||
e && e.stopPropagation();
|
||||
this.$emit('on-clickoutside', e);
|
||||
return;
|
||||
}
|
||||
|
||||
this.isFocused = false;
|
||||
this.disableClickOutSide = false;
|
||||
},
|
||||
handleFocus (e) {
|
||||
if (this.readonly) return;
|
||||
this.isFocused = true;
|
||||
if (e && e.type === 'focus') return; // just focus, don't open yet
|
||||
if(!this.disabled){
|
||||
this.visible = true;
|
||||
}
|
||||
},
|
||||
handleBlur (e) {
|
||||
if (this.internalFocus){
|
||||
this.internalFocus = false;
|
||||
return;
|
||||
}
|
||||
if (this.visible) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
this.isFocused = false;
|
||||
this.onSelectionModeChange(this.type);
|
||||
this.internalValue = this.internalValue.slice(); // trigger panel watchers to reset views
|
||||
this.reset();
|
||||
this.$refs.pickerPanel.onToggleVisibility(false);
|
||||
|
||||
},
|
||||
handleKeydown(e){
|
||||
const keyCode = e.keyCode;
|
||||
|
||||
// handle "tab" key
|
||||
if (keyCode === 9){
|
||||
if (this.visible){
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (this.isConfirm){
|
||||
const selector = `.${pickerPrefixCls}-confirm > *`;
|
||||
const tabbable = this.$refs.drop.$el.querySelectorAll(selector);
|
||||
this.internalFocus = true;
|
||||
const element = [...tabbable][e.shiftKey ? 'pop' : 'shift']();
|
||||
element.focus();
|
||||
} else {
|
||||
this.handleClose();
|
||||
}
|
||||
} else {
|
||||
this.focused = false;
|
||||
}
|
||||
}
|
||||
|
||||
// open the panel
|
||||
const arrows = [37, 38, 39, 40];
|
||||
if (!this.visible && arrows.includes(keyCode)){
|
||||
this.visible = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// close on "esc" key
|
||||
if (keyCode === 27){
|
||||
if (this.visible) {
|
||||
e.stopPropagation();
|
||||
this.handleClose();
|
||||
}
|
||||
}
|
||||
|
||||
// select date, "Enter" key
|
||||
if (keyCode === 13){
|
||||
const timePickers = findComponentsDownward(this, 'TimeSpinner');
|
||||
if (timePickers.length > 0){
|
||||
const columnsPerPicker = timePickers[0].showSeconds ? 3 : 2;
|
||||
const pickerIndex = Math.floor(this.focusedTime.column / columnsPerPicker);
|
||||
const value = this.focusedTime.time[pickerIndex];
|
||||
|
||||
timePickers[pickerIndex].chooseValue(value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.type.match(/range/)){
|
||||
this.$refs.pickerPanel.handleRangePick(this.focusedDate, 'date');
|
||||
} else {
|
||||
const panels = findComponentsDownward(this, 'PanelTable');
|
||||
const compareDate = (d) => {
|
||||
const sliceIndex = ['year', 'month', 'date'].indexOf((this.type)) + 1;
|
||||
return [d.getFullYear(), d.getMonth(), d.getDate()].slice(0, sliceIndex).join('-');
|
||||
};
|
||||
const dateIsValid = panels.find(({cells}) => {
|
||||
return cells.find(({date, disabled}) => compareDate(date) === compareDate(this.focusedDate) && !disabled);
|
||||
});
|
||||
if (dateIsValid) this.onPick(this.focusedDate, false, 'date');
|
||||
}
|
||||
}
|
||||
|
||||
if (!arrows.includes(keyCode)) return; // ignore rest of keys
|
||||
|
||||
// navigate times and dates
|
||||
if (this.focusedTime.active) e.preventDefault(); // to prevent cursor from moving
|
||||
this.navigateDatePanel(keyValueMapper[keyCode], e.shiftKey);
|
||||
},
|
||||
reset(){
|
||||
this.$refs.pickerPanel.reset && this.$refs.pickerPanel.reset();
|
||||
},
|
||||
navigateTimePanel(direction){
|
||||
|
||||
this.focusedTime.active = true;
|
||||
const horizontal = direction.match(/left|right/);
|
||||
const vertical = direction.match(/up|down/);
|
||||
const timePickers = findComponentsDownward(this, 'TimeSpinner');
|
||||
|
||||
const maxNrOfColumns = (timePickers[0].showSeconds ? 3 : 2) * timePickers.length;
|
||||
const column = (currentColumn => {
|
||||
const incremented = currentColumn + (horizontal ? (direction === 'left' ? -1 : 1) : 0);
|
||||
return (incremented + maxNrOfColumns) % maxNrOfColumns;
|
||||
})(this.focusedTime.column);
|
||||
|
||||
const columnsPerPicker = maxNrOfColumns / timePickers.length;
|
||||
const pickerIndex = Math.floor(column / columnsPerPicker);
|
||||
const col = column % columnsPerPicker;
|
||||
|
||||
|
||||
if (horizontal){
|
||||
const time = this.internalValue.map(extractTime);
|
||||
|
||||
this.focusedTime = {
|
||||
...this.focusedTime,
|
||||
column: column,
|
||||
time: time
|
||||
};
|
||||
timePickers.forEach((instance, i) => {
|
||||
if (i === pickerIndex) instance.updateFocusedTime(col, time[pickerIndex]);
|
||||
else instance.updateFocusedTime(-1, instance.focusedTime);
|
||||
});
|
||||
}
|
||||
|
||||
if (vertical){
|
||||
const increment = direction === 'up' ? 1 : -1;
|
||||
const timeParts = ['hours', 'minutes', 'seconds'];
|
||||
|
||||
|
||||
const pickerPossibleValues = timePickers[pickerIndex][`${timeParts[col]}List`];
|
||||
const nextIndex = pickerPossibleValues.findIndex(({text}) => this.focusedTime.time[pickerIndex][col] === text) + increment;
|
||||
const nextValue = pickerPossibleValues[nextIndex % pickerPossibleValues.length].text;
|
||||
const times = this.focusedTime.time.map((time, i) => {
|
||||
if (i !== pickerIndex) return time;
|
||||
time[col] = nextValue;
|
||||
return time;
|
||||
});
|
||||
this.focusedTime = {
|
||||
...this.focusedTime,
|
||||
time: times
|
||||
};
|
||||
|
||||
timePickers.forEach((instance, i) => {
|
||||
if (i === pickerIndex) instance.updateFocusedTime(col, times[i]);
|
||||
else instance.updateFocusedTime(-1, instance.focusedTime);
|
||||
});
|
||||
}
|
||||
},
|
||||
navigateDatePanel(direction, shift){
|
||||
|
||||
const timePickers = findComponentsDownward(this, 'TimeSpinner');
|
||||
if (timePickers.length > 0) {
|
||||
// we are in TimePicker mode
|
||||
this.navigateTimePanel(direction, shift, timePickers);
|
||||
return;
|
||||
}
|
||||
|
||||
if (shift){
|
||||
if (this.type === 'year'){
|
||||
this.focusedDate = new Date(
|
||||
this.focusedDate.getFullYear() + mapPossibleValues(direction, 0, 10),
|
||||
this.focusedDate.getMonth(),
|
||||
this.focusedDate.getDate()
|
||||
);
|
||||
} else {
|
||||
this.focusedDate = new Date(
|
||||
this.focusedDate.getFullYear() + mapPossibleValues(direction, 0, 1),
|
||||
this.focusedDate.getMonth() + mapPossibleValues(direction, 1, 0),
|
||||
this.focusedDate.getDate()
|
||||
);
|
||||
}
|
||||
|
||||
const position = direction.match(/left|down/) ? 'prev' : 'next';
|
||||
const double = direction.match(/up|down/) ? '-double' : '';
|
||||
|
||||
// pulse button
|
||||
const button = this.$refs.drop.$el.querySelector(`.ivu-date-picker-${position}-btn-arrow${double}`);
|
||||
if (button) pulseElement(button);
|
||||
return;
|
||||
}
|
||||
|
||||
const initialDate = this.focusedDate || (this.internalValue && this.internalValue[0]) || new Date();
|
||||
const focusedDate = new Date(initialDate);
|
||||
|
||||
if (this.type.match(/^date/)){
|
||||
const lastOfMonth = getDayCountOfMonth(initialDate.getFullYear(), initialDate.getMonth());
|
||||
const startDay = initialDate.getDate();
|
||||
const nextDay = focusedDate.getDate() + mapPossibleValues(direction, 1, 7);
|
||||
|
||||
if (nextDay < 1) {
|
||||
if (direction.match(/left|right/)) {
|
||||
focusedDate.setMonth(focusedDate.getMonth() + 1);
|
||||
focusedDate.setDate(nextDay);
|
||||
} else {
|
||||
focusedDate.setDate(startDay + Math.floor((lastOfMonth - startDay) / 7) * 7);
|
||||
}
|
||||
} else if (nextDay > lastOfMonth){
|
||||
if (direction.match(/left|right/)) {
|
||||
focusedDate.setMonth(focusedDate.getMonth() - 1);
|
||||
focusedDate.setDate(nextDay);
|
||||
} else {
|
||||
focusedDate.setDate(startDay % 7);
|
||||
}
|
||||
} else {
|
||||
focusedDate.setDate(nextDay);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.type.match(/^month/)) {
|
||||
focusedDate.setMonth(focusedDate.getMonth() + mapPossibleValues(direction, 1, 3));
|
||||
}
|
||||
|
||||
if (this.type.match(/^year/)) {
|
||||
focusedDate.setFullYear(focusedDate.getFullYear() + mapPossibleValues(direction, 1, 3));
|
||||
}
|
||||
|
||||
this.focusedDate = focusedDate;
|
||||
},
|
||||
handleInputChange (event) {
|
||||
const isArrayValue = this.type.includes('range') || this.multiple;
|
||||
const oldValue = this.visualValue;
|
||||
const newValue = event.target.value;
|
||||
const newDate = this.parseDate(newValue);
|
||||
const disabledDateFn =
|
||||
this.options &&
|
||||
typeof this.options.disabledDate === 'function' &&
|
||||
this.options.disabledDate;
|
||||
const valueToTest = isArrayValue ? newDate : newDate[0];
|
||||
const isDisabled = disabledDateFn && disabledDateFn(valueToTest);
|
||||
const isValidDate = newDate.reduce((valid, date) => valid && date instanceof Date, true);
|
||||
|
||||
if (newValue !== oldValue && !isDisabled && isValidDate) {
|
||||
this.emitChange(this.type);
|
||||
this.internalValue = newDate;
|
||||
} else {
|
||||
this.forceInputRerender++;
|
||||
}
|
||||
},
|
||||
handleInputMouseenter () {
|
||||
if (this.readonly || this.disabled) return;
|
||||
if (this.visualValue && this.clearable) {
|
||||
this.showClose = true;
|
||||
}
|
||||
},
|
||||
handleInputMouseleave () {
|
||||
this.showClose = false;
|
||||
},
|
||||
handleIconClick (e) {
|
||||
if (this.showClose) {
|
||||
if (e) e.stopPropagation();
|
||||
this.handleClear();
|
||||
} else if (!this.disabled) {
|
||||
this.handleFocus();
|
||||
}
|
||||
},
|
||||
handleClear () {
|
||||
this.visible = false;
|
||||
this.internalValue = this.internalValue.map(() => null);
|
||||
this.$emit('on-clear');
|
||||
this.dispatch('FormItem', 'on-form-change', '');
|
||||
this.emitChange(this.type);
|
||||
this.reset();
|
||||
|
||||
setTimeout(
|
||||
() => this.onSelectionModeChange(this.type),
|
||||
500 // delay to improve dropdown close visual effect
|
||||
);
|
||||
},
|
||||
emitChange (type) {
|
||||
this.$nextTick(() => {
|
||||
this.$emit('on-change', this.publicStringValue, type);
|
||||
this.dispatch('FormItem', 'on-form-change', this.publicStringValue);
|
||||
});
|
||||
},
|
||||
parseDate(val) {
|
||||
const isRange = this.type.includes('range');
|
||||
const type = this.type;
|
||||
const parser = (
|
||||
TYPE_VALUE_RESOLVER_MAP[type] ||
|
||||
TYPE_VALUE_RESOLVER_MAP['default']
|
||||
).parser;
|
||||
const format = this.format || DEFAULT_FORMATS[type];
|
||||
const multipleParser = TYPE_VALUE_RESOLVER_MAP['multiple'].parser;
|
||||
|
||||
if (val && type === 'time' && !(val instanceof Date)) {
|
||||
val = parser(val, format, this.separator);
|
||||
} else if (this.multiple && val) {
|
||||
val = multipleParser(val, format, this.separator);
|
||||
} else if (isRange) {
|
||||
if (!val){
|
||||
val = [null, null];
|
||||
} else {
|
||||
if (typeof val === 'string') {
|
||||
val = parser(val, format, this.separator);
|
||||
} else if (type === 'timerange') {
|
||||
val = parser(val, format, this.separator).map(v => v || '');
|
||||
} else {
|
||||
const [start, end] = val;
|
||||
if (start instanceof Date && end instanceof Date){
|
||||
val = val.map(date => new Date(date));
|
||||
} else if (typeof start === 'string' && typeof end === 'string'){
|
||||
val = parser(val.join(this.separator), format, this.separator);
|
||||
} else if (!start || !end){
|
||||
val = [null, null];
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (typeof val === 'string' && type.indexOf('time') !== 0){
|
||||
val = parser(val, format) || null;
|
||||
}
|
||||
|
||||
return (isRange || this.multiple) ? (val || []) : [val];
|
||||
},
|
||||
formatDate(value){
|
||||
const format = DEFAULT_FORMATS[this.type];
|
||||
|
||||
if (this.multiple) {
|
||||
const formatter = TYPE_VALUE_RESOLVER_MAP.multiple.formatter;
|
||||
return formatter(value, this.format || format, this.separator);
|
||||
} else {
|
||||
const {formatter} = (
|
||||
TYPE_VALUE_RESOLVER_MAP[this.type] ||
|
||||
TYPE_VALUE_RESOLVER_MAP['default']
|
||||
);
|
||||
return formatter(value, this.format || format, this.separator);
|
||||
}
|
||||
},
|
||||
onPick(dates, visible = false, type) {
|
||||
if (this.multiple){
|
||||
const pickedTimeStamp = dates.getTime();
|
||||
const indexOfPickedDate = this.internalValue.findIndex(date => date && date.getTime() === pickedTimeStamp);
|
||||
const allDates = [...this.internalValue, dates].filter(Boolean);
|
||||
const timeStamps = allDates.map(date => date.getTime()).filter((ts, i, arr) => arr.indexOf(ts) === i && i !== indexOfPickedDate); // filter away duplicates
|
||||
this.internalValue = timeStamps.map(ts => new Date(ts));
|
||||
} else {
|
||||
dates = this.parseDate(dates);
|
||||
this.internalValue = Array.isArray(dates) ? dates : [dates];
|
||||
}
|
||||
|
||||
if (this.internalValue[0]) this.focusedDate = this.internalValue[0];
|
||||
this.focusedTime = {
|
||||
...this.focusedTime,
|
||||
time: this.internalValue.map(extractTime)
|
||||
};
|
||||
|
||||
if (!this.isConfirm) this.onSelectionModeChange(this.type); // reset the selectionMode
|
||||
if (!this.isConfirm) this.visible = visible;
|
||||
this.emitChange(type);
|
||||
},
|
||||
onPickSuccess(){
|
||||
this.visible = false;
|
||||
this.$emit('on-ok');
|
||||
this.focus();
|
||||
this.reset();
|
||||
},
|
||||
focus() {
|
||||
this.$refs.input && this.$refs.input.focus();
|
||||
},
|
||||
updatePopper () {
|
||||
this.$refs.drop.update();
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible (state) {
|
||||
if (state === false){
|
||||
this.$refs.drop.destroy();
|
||||
}
|
||||
this.$refs.drop.update();
|
||||
this.$emit('on-open-change', state);
|
||||
},
|
||||
value(val) {
|
||||
this.internalValue = this.parseDate(val);
|
||||
},
|
||||
open (val) {
|
||||
this.visible = val === true;
|
||||
},
|
||||
type(type){
|
||||
this.onSelectionModeChange(type);
|
||||
},
|
||||
publicVModelValue(now, before){
|
||||
const newValue = JSON.stringify(now);
|
||||
const oldValue = JSON.stringify(before);
|
||||
const shouldEmitInput = newValue !== oldValue || typeof now !== typeof before;
|
||||
if (shouldEmitInput) this.$emit('input', now); // to update v-model
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
const initialValue = this.value;
|
||||
const parsedValue = this.publicVModelValue;
|
||||
if (typeof initialValue !== typeof parsedValue || JSON.stringify(initialValue) !== JSON.stringify(parsedValue)){
|
||||
this.$emit('input', this.publicVModelValue); // to update v-model
|
||||
}
|
||||
if (this.open !== null) this.visible = this.open;
|
||||
|
||||
// to handle focus from confirm buttons
|
||||
this.$on('focus-input', () => this.focus());
|
||||
this.$on('update-popper', () => this.updatePopper());
|
||||
}
|
||||
};
|
||||
</script>
|
28
src/components/date-picker/picker/date-picker.js
Normal file
28
src/components/date-picker/picker/date-picker.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import Picker from '../picker.vue';
|
||||
import DatePickerPanel from '../panel/Date/date.vue';
|
||||
import RangeDatePickerPanel from '../panel/Date/date-range.vue';
|
||||
|
||||
import { oneOf } from '../../../utils/assist';
|
||||
|
||||
export default {
|
||||
name: 'CalendarPicker',
|
||||
mixins: [Picker],
|
||||
props: {
|
||||
type: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['year', 'month', 'date', 'daterange', 'datetime', 'datetimerange']);
|
||||
},
|
||||
default: 'date'
|
||||
},
|
||||
},
|
||||
components: { DatePickerPanel, RangeDatePickerPanel },
|
||||
computed: {
|
||||
panel(){
|
||||
const isRange = this.type === 'daterange' || this.type === 'datetimerange';
|
||||
return isRange ? 'RangeDatePickerPanel' : 'DatePickerPanel';
|
||||
},
|
||||
ownPickerProps(){
|
||||
return this.options;
|
||||
}
|
||||
},
|
||||
};
|
43
src/components/date-picker/picker/time-picker.js
Normal file
43
src/components/date-picker/picker/time-picker.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
import Picker from '../picker.vue';
|
||||
import TimePickerPanel from '../panel/Time/time.vue';
|
||||
import RangeTimePickerPanel from '../panel/Time/time-range.vue';
|
||||
import Options from '../time-mixins';
|
||||
|
||||
import { findComponentsDownward, oneOf } from '../../../utils/assist';
|
||||
|
||||
export default {
|
||||
mixins: [Picker, Options],
|
||||
components: { TimePickerPanel, RangeTimePickerPanel },
|
||||
props: {
|
||||
type: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['time', 'timerange']);
|
||||
},
|
||||
default: 'time'
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
panel(){
|
||||
const isRange = this.type === 'timerange';
|
||||
return isRange ? 'RangeTimePickerPanel' : 'TimePickerPanel';
|
||||
},
|
||||
ownPickerProps(){
|
||||
return {
|
||||
disabledHours: this.disabledHours,
|
||||
disabledMinutes: this.disabledMinutes,
|
||||
disabledSeconds: this.disabledSeconds,
|
||||
hideDisabledOptions: this.hideDisabledOptions
|
||||
};
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(visible){
|
||||
if (visible) {
|
||||
this.$nextTick(() => {
|
||||
const spinners = findComponentsDownward(this, 'TimeSpinner');
|
||||
spinners.forEach(instance => instance.updateScroll());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
26
src/components/date-picker/time-mixins.js
Normal file
26
src/components/date-picker/time-mixins.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
export default {
|
||||
props: {
|
||||
disabledHours: {
|
||||
type: Array,
|
||||
default () {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
disabledMinutes: {
|
||||
type: Array,
|
||||
default () {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
disabledSeconds: {
|
||||
type: Array,
|
||||
default () {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
hideDisabledOptions: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
};
|
258
src/components/date-picker/util.js
Normal file
258
src/components/date-picker/util.js
Normal file
|
@ -0,0 +1,258 @@
|
|||
import dateUtil from '../../utils/date';
|
||||
|
||||
export const toDate = function(date) {
|
||||
let _date = new Date(date);
|
||||
// IE patch start (#1422)
|
||||
if (isNaN(_date.getTime()) && typeof date === 'string'){
|
||||
_date = date.split('-').map(Number);
|
||||
_date[1] += 1;
|
||||
_date = new Date(..._date);
|
||||
}
|
||||
// IE patch end
|
||||
|
||||
if (isNaN(_date.getTime())) return null;
|
||||
return _date;
|
||||
};
|
||||
|
||||
export const clearHours = function (time) {
|
||||
const cloneDate = new Date(time);
|
||||
cloneDate.setHours(0, 0, 0, 0);
|
||||
return cloneDate.getTime();
|
||||
};
|
||||
|
||||
export const isInRange = (time, a, b) => {
|
||||
if (!a || !b) return false;
|
||||
const [start, end] = [a, b].sort();
|
||||
return time >= start && time <= end;
|
||||
};
|
||||
|
||||
export const formatDate = function(date, format) {
|
||||
date = toDate(date);
|
||||
if (!date) return '';
|
||||
return dateUtil.format(date, format || 'yyyy-MM-dd');
|
||||
};
|
||||
|
||||
export const parseDate = function(string, format) {
|
||||
return dateUtil.parse(string, format || 'yyyy-MM-dd');
|
||||
};
|
||||
|
||||
export const getDayCountOfMonth = function(year, month) {
|
||||
return new Date(year, month + 1, 0).getDate();
|
||||
};
|
||||
|
||||
export const getFirstDayOfMonth = function(date) {
|
||||
const temp = new Date(date.getTime());
|
||||
temp.setDate(1);
|
||||
return temp.getDay();
|
||||
};
|
||||
|
||||
export const siblingMonth = function(src, diff) {
|
||||
const temp = new Date(src); // lets copy it so we don't change the original
|
||||
const newMonth = temp.getMonth() + diff;
|
||||
const newMonthDayCount = getDayCountOfMonth(temp.getFullYear(), newMonth);
|
||||
if (newMonthDayCount < temp.getDate()) {
|
||||
temp.setDate(newMonthDayCount);
|
||||
}
|
||||
temp.setMonth(newMonth);
|
||||
|
||||
return temp;
|
||||
};
|
||||
|
||||
export const prevMonth = function(src) {
|
||||
return siblingMonth(src, -1);
|
||||
};
|
||||
|
||||
export const nextMonth = function(src) {
|
||||
return siblingMonth(src, 1);
|
||||
};
|
||||
|
||||
export const initTimeDate = function() {
|
||||
const date = new Date();
|
||||
date.setHours(0);
|
||||
date.setMinutes(0);
|
||||
date.setSeconds(0);
|
||||
return date;
|
||||
};
|
||||
|
||||
export const formatDateLabels = (function() {
|
||||
/*
|
||||
Formats:
|
||||
yyyy - 4 digit year
|
||||
m - month, numeric, 1 - 12
|
||||
mm - month, numeric, 01 - 12
|
||||
mmm - month, 3 letters, as in `toLocaleDateString`
|
||||
Mmm - month, 3 letters, capitalize the return from `toLocaleDateString`
|
||||
mmmm - month, full name, as in `toLocaleDateString`
|
||||
Mmmm - month, full name, capitalize the return from `toLocaleDateString`
|
||||
*/
|
||||
|
||||
const formats = {
|
||||
yyyy: date => date.getFullYear(),
|
||||
m: date => date.getMonth() + 1,
|
||||
mm: date => ('0' + (date.getMonth() + 1)).slice(-2),
|
||||
mmm: (date, locale) => {
|
||||
const monthName = date.toLocaleDateString(locale, {
|
||||
month: 'long'
|
||||
});
|
||||
return monthName.slice(0, 3);
|
||||
},
|
||||
Mmm: (date, locale) => {
|
||||
const monthName = date.toLocaleDateString(locale, {
|
||||
month: 'long'
|
||||
});
|
||||
return (monthName[0].toUpperCase() + monthName.slice(1).toLowerCase()).slice(0, 3);
|
||||
},
|
||||
mmmm: (date, locale) =>
|
||||
date.toLocaleDateString(locale, {
|
||||
month: 'long'
|
||||
}),
|
||||
Mmmm: (date, locale) => {
|
||||
const monthName = date.toLocaleDateString(locale, {
|
||||
month: 'long'
|
||||
});
|
||||
return monthName[0].toUpperCase() + monthName.slice(1).toLowerCase();
|
||||
}
|
||||
};
|
||||
const formatRegex = new RegExp(['yyyy', 'Mmmm', 'mmmm', 'Mmm', 'mmm', 'mm', 'm'].join('|'), 'g');
|
||||
|
||||
return function(locale, format, date) {
|
||||
const componetsRegex = /(\[[^\]]+\])([^\[\]]+)(\[[^\]]+\])/;
|
||||
const components = format.match(componetsRegex).slice(1);
|
||||
const separator = components[1];
|
||||
const labels = [components[0], components[2]].map(component => {
|
||||
const label = component.replace(/\[[^\]]+\]/, str => {
|
||||
return str.slice(1, -1).replace(formatRegex, match => formats[match](date, locale));
|
||||
});
|
||||
return {
|
||||
label: label,
|
||||
type: component.indexOf('yy') != -1 ? 'year' : 'month'
|
||||
};
|
||||
});
|
||||
return {
|
||||
separator: separator,
|
||||
labels: labels
|
||||
};
|
||||
};
|
||||
})();
|
||||
|
||||
// Parsers and Formaters
|
||||
export const DEFAULT_FORMATS = {
|
||||
date: 'yyyy-MM-dd',
|
||||
month: 'yyyy-MM',
|
||||
year: 'yyyy',
|
||||
datetime: 'yyyy-MM-dd HH:mm:ss',
|
||||
time: 'HH:mm:ss',
|
||||
timerange: 'HH:mm:ss',
|
||||
daterange: 'yyyy-MM-dd',
|
||||
datetimerange: 'yyyy-MM-dd HH:mm:ss'
|
||||
};
|
||||
|
||||
// export const RANGE_SEPARATOR = ' - '; // use picker.vue prop separator
|
||||
|
||||
const DATE_FORMATTER = function(value, format) {
|
||||
return formatDate(value, format);
|
||||
};
|
||||
const DATE_PARSER = function(text, format) {
|
||||
return parseDate(text, format);
|
||||
};
|
||||
const RANGE_FORMATTER = function(value, format, RANGE_SEPARATOR) {
|
||||
if (Array.isArray(value) && value.length === 2) {
|
||||
const start = value[0];
|
||||
const end = value[1];
|
||||
|
||||
if (start && end) {
|
||||
return formatDate(start, format) + RANGE_SEPARATOR + formatDate(end, format);
|
||||
}
|
||||
} else if (!Array.isArray(value) && value instanceof Date){
|
||||
return formatDate(value, format);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
const RANGE_PARSER = function(text, format, RANGE_SEPARATOR) {
|
||||
const array = Array.isArray(text) ? text : text.split(RANGE_SEPARATOR);
|
||||
if (array.length === 2) {
|
||||
const range1 = array[0];
|
||||
const range2 = array[1];
|
||||
|
||||
return [
|
||||
range1 instanceof Date ? range1 : parseDate(range1, format),
|
||||
range2 instanceof Date ? range2 : parseDate(range2, format),
|
||||
];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const TYPE_VALUE_RESOLVER_MAP = {
|
||||
default: {
|
||||
formatter(value) {
|
||||
if (!value) return '';
|
||||
return '' + value;
|
||||
},
|
||||
parser(text) {
|
||||
if (text === undefined || text === '') return null;
|
||||
return text;
|
||||
}
|
||||
},
|
||||
date: {
|
||||
formatter: DATE_FORMATTER,
|
||||
parser: DATE_PARSER
|
||||
},
|
||||
datetime: {
|
||||
formatter: DATE_FORMATTER,
|
||||
parser: DATE_PARSER
|
||||
},
|
||||
daterange: {
|
||||
formatter: RANGE_FORMATTER,
|
||||
parser: RANGE_PARSER
|
||||
},
|
||||
datetimerange: {
|
||||
formatter: RANGE_FORMATTER,
|
||||
parser: RANGE_PARSER
|
||||
},
|
||||
timerange: {
|
||||
formatter: RANGE_FORMATTER,
|
||||
parser: RANGE_PARSER
|
||||
},
|
||||
time: {
|
||||
formatter: DATE_FORMATTER,
|
||||
parser: DATE_PARSER
|
||||
},
|
||||
month: {
|
||||
formatter: DATE_FORMATTER,
|
||||
parser: DATE_PARSER
|
||||
},
|
||||
year: {
|
||||
formatter: DATE_FORMATTER,
|
||||
parser: DATE_PARSER
|
||||
},
|
||||
multiple: {
|
||||
formatter: (value, format) => {
|
||||
return value.filter(Boolean).map(date => formatDate(date, format)).join(',');
|
||||
},
|
||||
parser: (value, format) => {
|
||||
const values = typeof value === 'string' ? value.split(',') : value;
|
||||
return values.map(value => {
|
||||
if (value instanceof Date) return value;
|
||||
if (typeof value === 'string') value = value.trim();
|
||||
else if (typeof value !== 'number' && !value) value = '';
|
||||
return parseDate(value, format);
|
||||
});
|
||||
}
|
||||
},
|
||||
number: {
|
||||
formatter(value) {
|
||||
if (!value) return '';
|
||||
return '' + value;
|
||||
},
|
||||
parser(text) {
|
||||
let result = Number(text);
|
||||
|
||||
if (!isNaN(text)) {
|
||||
return result;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
65
src/components/divider/divider.vue
Normal file
65
src/components/divider/divider.vue
Normal file
|
@ -0,0 +1,65 @@
|
|||
<template>
|
||||
<div :class="classes">
|
||||
<span v-if="hasSlot" :class="slotClasses">
|
||||
<slot></slot>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {oneOf} from '../../utils/assist';
|
||||
|
||||
const prefixCls = 'ivu-divider';
|
||||
|
||||
export default {
|
||||
name: 'Divider',
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'horizontal',
|
||||
validator (value) {
|
||||
return oneOf(value, ['horizontal', 'vertical']);
|
||||
}
|
||||
},
|
||||
orientation: {
|
||||
type: String,
|
||||
default: 'center',
|
||||
validator (value) {
|
||||
return oneOf(value, ['left', 'right', 'center']);
|
||||
}
|
||||
},
|
||||
dashed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
size: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['small', 'default']);
|
||||
},
|
||||
default: 'default'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasSlot() {
|
||||
return !!this.$slots.default;
|
||||
},
|
||||
classes() {
|
||||
return [
|
||||
`${prefixCls}`,
|
||||
`${prefixCls}-${this.type}`,
|
||||
`${prefixCls}-${this.size}`,
|
||||
{
|
||||
[`${prefixCls}-with-text`]: this.hasSlot && this.orientation === 'center',
|
||||
[`${prefixCls}-with-text-${this.orientation}`]: this.hasSlot,
|
||||
[`${prefixCls}-dashed`]: !!this.dashed
|
||||
}
|
||||
];
|
||||
},
|
||||
slotClasses() {
|
||||
return [
|
||||
`${prefixCls}-inner-text`,
|
||||
];
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
3
src/components/divider/index.js
Normal file
3
src/components/divider/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import Divider from './divider.vue';
|
||||
|
||||
export default Divider;
|
312
src/components/drawer/drawer.vue
Normal file
312
src/components/drawer/drawer.vue
Normal file
|
@ -0,0 +1,312 @@
|
|||
<template>
|
||||
<div v-transfer-dom :data-transfer="transfer">
|
||||
<transition name="fade">
|
||||
<div :class="maskClasses" :style="maskStyle" v-show="visible" v-if="mask" @click="handleMask"></div>
|
||||
</transition>
|
||||
<div :class="wrapClasses" @click="handleWrapClick">
|
||||
<transition :name="'move-' + placement">
|
||||
<div :class="classes" :style="mainStyles" v-show="visible">
|
||||
<div :class="contentClasses" ref="content">
|
||||
<a class="ivu-drawer-close" v-if="closable" @click="close">
|
||||
<slot name="close">
|
||||
<Icon type="ios-close"></Icon>
|
||||
</slot>
|
||||
</a>
|
||||
<div :class="[prefixCls + '-header']" v-if="showHead"><slot name="header"><div :class="[prefixCls + '-header-inner']">{{ title }}</div></slot></div>
|
||||
<div :class="[prefixCls + '-body']" :style="styles"><slot></slot></div>
|
||||
</div>
|
||||
<div class="ivu-drawer-drag" :class="{ 'ivu-drawer-drag-left': placement === 'left' }" v-if="draggable" @mousedown="handleTriggerMousedown">
|
||||
<slot name="trigger">
|
||||
<div class="ivu-drawer-drag-move-trigger">
|
||||
<div class="ivu-drawer-drag-move-trigger-point">
|
||||
<i></i><i></i><i></i><i></i><i></i>
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Icon from '../icon';
|
||||
import { oneOf, findBrothersComponents, findComponentsUpward } from '../../utils/assist';
|
||||
import TransferDom from '../../directives/transfer-dom';
|
||||
import Emitter from '../../mixins/emitter';
|
||||
import ScrollbarMixins from '../modal/mixins-scrollbar';
|
||||
|
||||
import { on, off } from '../../utils/dom';
|
||||
|
||||
const prefixCls = 'ivu-drawer';
|
||||
|
||||
export default {
|
||||
name: 'Drawer',
|
||||
mixins: [ Emitter, ScrollbarMixins ],
|
||||
components: { Icon },
|
||||
directives: { TransferDom },
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
title: {
|
||||
type: String
|
||||
},
|
||||
width: {
|
||||
type: [Number, String],
|
||||
default: 256
|
||||
},
|
||||
closable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
maskClosable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
mask: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
maskStyle: {
|
||||
type: Object
|
||||
},
|
||||
styles: {
|
||||
type: Object
|
||||
},
|
||||
scrollable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
placement: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['left', 'right']);
|
||||
},
|
||||
default: 'right'
|
||||
},
|
||||
zIndex: {
|
||||
type: Number,
|
||||
default: 1000
|
||||
},
|
||||
transfer: {
|
||||
type: Boolean,
|
||||
default () {
|
||||
return !this.$IVIEW || this.$IVIEW.transfer === '' ? true : this.$IVIEW.transfer;
|
||||
}
|
||||
},
|
||||
className: {
|
||||
type: String
|
||||
},
|
||||
inner: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// Whether drag and drop is allowed to adjust width
|
||||
draggable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
beforeClose: Function,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
prefixCls: prefixCls,
|
||||
visible: this.value,
|
||||
wrapShow: false,
|
||||
showHead: true,
|
||||
canMove: false,
|
||||
dragWidth: this.width,
|
||||
wrapperWidth: this.width,
|
||||
wrapperLeft: 0,
|
||||
minWidth: 256
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
wrapClasses () {
|
||||
return [
|
||||
`${prefixCls}-wrap`,
|
||||
{
|
||||
[`${prefixCls}-hidden`]: !this.wrapShow,
|
||||
[`${this.className}`]: !!this.className,
|
||||
[`${prefixCls}-no-mask`]: !this.mask,
|
||||
[`${prefixCls}-wrap-inner`]: this.inner,
|
||||
[`${prefixCls}-wrap-dragging`]: this.canMove
|
||||
}
|
||||
];
|
||||
},
|
||||
mainStyles () {
|
||||
let style = {};
|
||||
|
||||
const width = parseInt(this.dragWidth);
|
||||
|
||||
const styleWidth = {
|
||||
width: width <= 100 ? `${width}%` : `${width}px`
|
||||
};
|
||||
|
||||
Object.assign(style, styleWidth);
|
||||
|
||||
return style;
|
||||
},
|
||||
contentClasses () {
|
||||
return [
|
||||
`${prefixCls}-content`,
|
||||
{
|
||||
[`${prefixCls}-content-no-mask`]: !this.mask
|
||||
}
|
||||
];
|
||||
},
|
||||
classes () {
|
||||
return [
|
||||
`${prefixCls}`,
|
||||
`${prefixCls}-${this.placement}`,
|
||||
{
|
||||
[`${prefixCls}-no-header`]: !this.showHead,
|
||||
[`${prefixCls}-inner`]: this.inner
|
||||
}
|
||||
];
|
||||
},
|
||||
maskClasses () {
|
||||
return [
|
||||
`${prefixCls}-mask`,
|
||||
{
|
||||
[`${prefixCls}-mask-inner`]: this.inner
|
||||
}
|
||||
];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
if (!this.beforeClose) {
|
||||
return this.handleClose();
|
||||
}
|
||||
|
||||
const before = this.beforeClose();
|
||||
|
||||
if (before && before.then) {
|
||||
before.then(() => {
|
||||
this.handleClose();
|
||||
});
|
||||
} else {
|
||||
this.handleClose();
|
||||
}
|
||||
},
|
||||
handleClose () {
|
||||
this.visible = false;
|
||||
this.$emit('input', false);
|
||||
this.$emit('on-close');
|
||||
},
|
||||
handleMask () {
|
||||
if (this.maskClosable && this.mask) {
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
handleWrapClick (event) {
|
||||
// use indexOf,do not use === ,because ivu-modal-wrap can have other custom className
|
||||
const className = event.target.getAttribute('class');
|
||||
if (className && className.indexOf(`${prefixCls}-wrap`) > -1) this.handleMask();
|
||||
},
|
||||
handleMousemove (event) {
|
||||
if (!this.canMove || !this.draggable) return;
|
||||
// 更新容器宽度和距离左侧页面距离,如果是window则距左侧距离为0
|
||||
this.handleSetWrapperWidth();
|
||||
const left = event.pageX - this.wrapperLeft;
|
||||
// 如果抽屉方向为右边,宽度计算需用容器宽度减去left
|
||||
let width = this.placement === 'right' ? this.wrapperWidth - left : left;
|
||||
// 限定最小宽度
|
||||
width = Math.max(width, parseFloat(this.minWidth));
|
||||
event.atMin = width === parseFloat(this.minWidth);
|
||||
// 如果当前width不大于100,视为百分比
|
||||
if (width <= 100) width = (width / this.wrapperWidth) * 100;
|
||||
this.dragWidth = width;
|
||||
this.$emit('on-resize-width', parseInt(this.dragWidth));
|
||||
},
|
||||
handleSetWrapperWidth () {
|
||||
const {
|
||||
width,
|
||||
left
|
||||
} = this.$el.getBoundingClientRect();
|
||||
this.wrapperWidth = width;
|
||||
this.wrapperLeft = left;
|
||||
},
|
||||
handleMouseup () {
|
||||
if (!this.draggable) return;
|
||||
this.canMove = false;
|
||||
},
|
||||
handleTriggerMousedown () {
|
||||
this.canMove = true;
|
||||
// 防止鼠标选中抽屉中文字,造成拖动trigger触发浏览器原生拖动行为
|
||||
window.getSelection().removeAllRanges();
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
if (this.visible) {
|
||||
this.wrapShow = true;
|
||||
}
|
||||
|
||||
let showHead = true;
|
||||
|
||||
if (this.$slots.header === undefined && !this.title) {
|
||||
showHead = false;
|
||||
}
|
||||
|
||||
this.showHead = showHead;
|
||||
|
||||
on(document, 'mousemove', this.handleMousemove);
|
||||
on(document, 'mouseup', this.handleMouseup);
|
||||
this.handleSetWrapperWidth();
|
||||
},
|
||||
beforeDestroy () {
|
||||
off(document, 'mousemove', this.handleMousemove);
|
||||
off(document, 'mouseup', this.handleMouseup);
|
||||
this.removeScrollEffect();
|
||||
},
|
||||
watch: {
|
||||
value (val) {
|
||||
this.visible = val;
|
||||
},
|
||||
visible (val) {
|
||||
if (val === false) {
|
||||
this.timer = setTimeout(() => {
|
||||
this.wrapShow = false;
|
||||
// #4831 Check if there are any drawers left at the parent level
|
||||
const brotherDrawers = findBrothersComponents(this, 'Drawer') || [];
|
||||
const parentDrawers = findComponentsUpward(this, 'Drawer') || [];
|
||||
|
||||
const otherDrawers = [].concat(brotherDrawers).concat(parentDrawers);
|
||||
|
||||
const isScrollDrawer = otherDrawers.some(item => item.visible && !item.scrollable);
|
||||
|
||||
if (!isScrollDrawer) {
|
||||
this.removeScrollEffect();
|
||||
}
|
||||
}, 300);
|
||||
} else {
|
||||
if (this.timer) clearTimeout(this.timer);
|
||||
this.wrapShow = true;
|
||||
if (!this.scrollable) {
|
||||
this.addScrollEffect();
|
||||
}
|
||||
}
|
||||
this.broadcast('Table', 'on-visible-change', val);
|
||||
this.broadcast('Slider', 'on-visible-change', val); // #2852
|
||||
this.$emit('on-visible-change', val);
|
||||
},
|
||||
scrollable (val) {
|
||||
if (!val) {
|
||||
this.addScrollEffect();
|
||||
} else {
|
||||
this.removeScrollEffect();
|
||||
}
|
||||
},
|
||||
title (val) {
|
||||
if (this.$slots.header === undefined) {
|
||||
this.showHead = !!val;
|
||||
}
|
||||
},
|
||||
width (val) {
|
||||
this.dragWidth = val;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
3
src/components/drawer/index.js
Normal file
3
src/components/drawer/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import Drawer from './drawer.vue';
|
||||
|
||||
export default Drawer;
|
3
src/components/dropdown-item/index.js
Normal file
3
src/components/dropdown-item/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import DropdownItem from '../dropdown/dropdown-item.vue';
|
||||
|
||||
export default DropdownItem;
|
3
src/components/dropdown-menu/index.js
Normal file
3
src/components/dropdown-menu/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import DropdownMenu from '../dropdown/dropdown-menu.vue';
|
||||
|
||||
export default DropdownMenu;
|
58
src/components/dropdown/dropdown-item.vue
Normal file
58
src/components/dropdown/dropdown-item.vue
Normal file
|
@ -0,0 +1,58 @@
|
|||
<template>
|
||||
<li :class="classes" @click="handleClick"><slot></slot></li>
|
||||
</template>
|
||||
<script>
|
||||
const prefixCls = 'ivu-dropdown-item';
|
||||
import { findComponentUpward } from '../../utils/assist';
|
||||
export default {
|
||||
name: 'DropdownItem',
|
||||
props: {
|
||||
name: {
|
||||
type: [String, Number]
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
divided: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
return [
|
||||
`${prefixCls}`,
|
||||
{
|
||||
[`${prefixCls}-disabled`]: this.disabled,
|
||||
[`${prefixCls}-selected`]: this.selected,
|
||||
[`${prefixCls}-divided`]: this.divided
|
||||
}
|
||||
];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClick () {
|
||||
const $parent = findComponentUpward(this, 'Dropdown');
|
||||
const hasChildren = this.$parent && this.$parent.$options.name === 'Dropdown';
|
||||
|
||||
if (this.disabled) {
|
||||
this.$nextTick(() => {
|
||||
$parent.currentVisible = true;
|
||||
});
|
||||
} else if (hasChildren) {
|
||||
this.$parent.$emit('on-haschild-click');
|
||||
} else {
|
||||
if ($parent && $parent.$options.name === 'Dropdown') {
|
||||
$parent.$emit('on-hover-click');
|
||||
}
|
||||
}
|
||||
$parent.$emit('on-click', this.name);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
8
src/components/dropdown/dropdown-menu.vue
Normal file
8
src/components/dropdown/dropdown-menu.vue
Normal file
|
@ -0,0 +1,8 @@
|
|||
<template>
|
||||
<ul class="ivu-dropdown-menu"><slot></slot></ul>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'DropdownMenu'
|
||||
};
|
||||
</script>
|
200
src/components/dropdown/dropdown.vue
Normal file
200
src/components/dropdown/dropdown.vue
Normal file
|
@ -0,0 +1,200 @@
|
|||
<template>
|
||||
<div
|
||||
:class="[prefixCls]"
|
||||
v-click-outside="onClickoutside"
|
||||
@mouseenter="handleMouseenter"
|
||||
@mouseleave="handleMouseleave">
|
||||
<div :class="relClasses" ref="reference" @click="handleClick" @contextmenu.prevent="handleRightClick"><slot></slot></div>
|
||||
<transition name="transition-drop">
|
||||
<Drop
|
||||
:class="dropdownCls"
|
||||
v-show="currentVisible"
|
||||
:placement="placement"
|
||||
ref="drop"
|
||||
@mouseenter.native="handleMouseenter"
|
||||
@mouseleave.native="handleMouseleave"
|
||||
:data-transfer="transfer"
|
||||
:transfer="transfer"
|
||||
v-transfer-dom><slot name="list"></slot></Drop>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Drop from '../select/dropdown.vue';
|
||||
import {directive as clickOutside} from 'v-click-outside-x';
|
||||
import TransferDom from '../../directives/transfer-dom';
|
||||
import { oneOf, findComponentUpward } from '../../utils/assist';
|
||||
|
||||
const prefixCls = 'ivu-dropdown';
|
||||
|
||||
export default {
|
||||
name: 'Dropdown',
|
||||
directives: { clickOutside, TransferDom },
|
||||
components: { Drop },
|
||||
props: {
|
||||
trigger: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['click', 'hover', 'custom', 'contextMenu']);
|
||||
},
|
||||
default: 'hover'
|
||||
},
|
||||
placement: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'left-start', 'left-end', 'right', 'right-start', 'right-end']);
|
||||
},
|
||||
default: 'bottom'
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
transfer: {
|
||||
type: Boolean,
|
||||
default () {
|
||||
return !this.$IVIEW || this.$IVIEW.transfer === '' ? false : this.$IVIEW.transfer;
|
||||
}
|
||||
},
|
||||
transferClassName: {
|
||||
type: String
|
||||
},
|
||||
stopPropagation: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
transition () {
|
||||
return ['bottom-start', 'bottom', 'bottom-end'].indexOf(this.placement) > -1 ? 'slide-up' : 'fade';
|
||||
},
|
||||
dropdownCls () {
|
||||
return {
|
||||
[prefixCls + '-transfer']: this.transfer,
|
||||
[this.transferClassName]: this.transferClassName
|
||||
};
|
||||
},
|
||||
relClasses () {
|
||||
return [
|
||||
`${prefixCls}-rel`,
|
||||
{
|
||||
[`${prefixCls}-rel-user-select-none`]: this.trigger === 'contextMenu'
|
||||
}
|
||||
];
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
prefixCls: prefixCls,
|
||||
currentVisible: this.visible
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
visible (val) {
|
||||
this.currentVisible = val;
|
||||
},
|
||||
currentVisible (val) {
|
||||
if (val) {
|
||||
this.$refs.drop.update();
|
||||
} else {
|
||||
this.$refs.drop.destroy();
|
||||
}
|
||||
this.$emit('on-visible-change', val);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClick () {
|
||||
if (this.trigger === 'custom') return false;
|
||||
if (this.trigger !== 'click') {
|
||||
return false;
|
||||
}
|
||||
this.currentVisible = !this.currentVisible;
|
||||
},
|
||||
handleRightClick () {
|
||||
if (this.trigger === 'custom') return false;
|
||||
if (this.trigger !== 'contextMenu') {
|
||||
return false;
|
||||
}
|
||||
this.currentVisible = !this.currentVisible;
|
||||
},
|
||||
handleMouseenter () {
|
||||
if (this.trigger === 'custom') return false;
|
||||
if (this.trigger !== 'hover') {
|
||||
return false;
|
||||
}
|
||||
if (this.timeout) clearTimeout(this.timeout);
|
||||
this.timeout = setTimeout(() => {
|
||||
this.currentVisible = true;
|
||||
}, 250);
|
||||
},
|
||||
handleMouseleave () {
|
||||
if (this.trigger === 'custom') return false;
|
||||
if (this.trigger !== 'hover') {
|
||||
return false;
|
||||
}
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = setTimeout(() => {
|
||||
this.currentVisible = false;
|
||||
}, 150);
|
||||
}
|
||||
},
|
||||
onClickoutside (e) {
|
||||
this.handleClose();
|
||||
this.handleRightClose();
|
||||
if (this.currentVisible) this.$emit('on-clickoutside', e);
|
||||
},
|
||||
handleClose () {
|
||||
if (this.trigger === 'custom') return false;
|
||||
if (this.trigger !== 'click') {
|
||||
return false;
|
||||
}
|
||||
this.currentVisible = false;
|
||||
},
|
||||
handleRightClose () {
|
||||
if (this.trigger === 'custom') return false;
|
||||
if (this.trigger !== 'contextMenu') {
|
||||
return false;
|
||||
}
|
||||
this.currentVisible = false;
|
||||
},
|
||||
hasParent () {
|
||||
// const $parent = this.$parent.$parent.$parent;
|
||||
const $parent = findComponentUpward(this, 'Dropdown');
|
||||
if ($parent) {
|
||||
return $parent;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$on('on-click', (key) => {
|
||||
if (this.stopPropagation) return;
|
||||
const $parent = this.hasParent();
|
||||
if ($parent) $parent.$emit('on-click', key);
|
||||
});
|
||||
this.$on('on-hover-click', () => {
|
||||
const $parent = this.hasParent();
|
||||
if ($parent) {
|
||||
this.$nextTick(() => {
|
||||
if (this.trigger === 'custom') return false;
|
||||
this.currentVisible = false;
|
||||
});
|
||||
$parent.$emit('on-hover-click');
|
||||
} else {
|
||||
this.$nextTick(() => {
|
||||
if (this.trigger === 'custom') return false;
|
||||
this.currentVisible = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
this.$on('on-haschild-click', () => {
|
||||
this.$nextTick(() => {
|
||||
if (this.trigger === 'custom') return false;
|
||||
this.currentVisible = true;
|
||||
});
|
||||
const $parent = this.hasParent();
|
||||
if ($parent) $parent.$emit('on-haschild-click');
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
7
src/components/dropdown/index.js
Normal file
7
src/components/dropdown/index.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import Dropdown from './dropdown.vue';
|
||||
import DropdownMenu from './dropdown-menu.vue';
|
||||
import DropdownItem from './dropdown-item.vue';
|
||||
|
||||
Dropdown.Menu = DropdownMenu;
|
||||
Dropdown.Item = DropdownItem;
|
||||
export default Dropdown;
|
3
src/components/footer/index.js
Normal file
3
src/components/footer/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import Footer from '../layout/footer.vue';
|
||||
|
||||
export default Footer;
|
3
src/components/form-item/index.js
Normal file
3
src/components/form-item/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import FormItem from '../form/form-item.vue';
|
||||
|
||||
export default FormItem;
|
264
src/components/form/form-item.vue
Normal file
264
src/components/form/form-item.vue
Normal file
|
@ -0,0 +1,264 @@
|
|||
<template>
|
||||
<div :class="classes">
|
||||
<label :class="[prefixCls + '-label']" :for="labelFor" :style="labelStyles" v-if="label || $slots.label"><slot name="label">{{ label }}</slot></label>
|
||||
<div :class="[prefixCls + '-content']" :style="contentStyles">
|
||||
<slot></slot>
|
||||
<transition name="fade">
|
||||
<div :class="[prefixCls + '-error-tip']" v-if="validateState === 'error' && showMessage && form.showMessage">{{ validateMessage }}</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import AsyncValidator from 'async-validator';
|
||||
import Emitter from '../../mixins/emitter';
|
||||
|
||||
const prefixCls = 'ivu-form-item';
|
||||
|
||||
function getPropByPath(obj, path) {
|
||||
let tempObj = obj;
|
||||
path = path.replace(/\[(\w+)\]/g, '.$1');
|
||||
path = path.replace(/^\./, '');
|
||||
|
||||
let keyArr = path.split('.');
|
||||
let i = 0;
|
||||
|
||||
for (let len = keyArr.length; i < len - 1; ++i) {
|
||||
let key = keyArr[i];
|
||||
if (key in tempObj) {
|
||||
tempObj = tempObj[key];
|
||||
} else {
|
||||
throw new Error('[iView warn]: please transfer a valid prop path to form item!');
|
||||
}
|
||||
}
|
||||
return {
|
||||
o: tempObj,
|
||||
k: keyArr[i],
|
||||
v: tempObj[keyArr[i]]
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'FormItem',
|
||||
mixins: [ Emitter ],
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
labelWidth: {
|
||||
type: Number
|
||||
},
|
||||
prop: {
|
||||
type: String
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
rules: {
|
||||
type: [Object, Array]
|
||||
},
|
||||
error: {
|
||||
type: String
|
||||
},
|
||||
validateStatus: {
|
||||
type: Boolean
|
||||
},
|
||||
showMessage: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
labelFor: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
prefixCls: prefixCls,
|
||||
isRequired: false,
|
||||
validateState: '',
|
||||
validateMessage: '',
|
||||
validateDisabled: false,
|
||||
validator: {}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
error: {
|
||||
handler (val) {
|
||||
this.validateMessage = val;
|
||||
this.validateState = val ? 'error' : '';
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
validateStatus (val) {
|
||||
this.validateState = val;
|
||||
},
|
||||
rules (){
|
||||
this.setRules();
|
||||
}
|
||||
},
|
||||
inject: ['form'],
|
||||
computed: {
|
||||
classes () {
|
||||
return [
|
||||
`${prefixCls}`,
|
||||
{
|
||||
[`${prefixCls}-required`]: this.required || this.isRequired,
|
||||
[`${prefixCls}-error`]: this.validateState === 'error',
|
||||
[`${prefixCls}-validating`]: this.validateState === 'validating'
|
||||
}
|
||||
];
|
||||
},
|
||||
// form() {
|
||||
// let parent = this.$parent;
|
||||
// while (parent.$options.name !== 'iForm') {
|
||||
// parent = parent.$parent;
|
||||
// }
|
||||
// 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).v;
|
||||
},
|
||||
labelStyles () {
|
||||
let style = {};
|
||||
const labelWidth = this.labelWidth === 0 || this.labelWidth ? this.labelWidth : this.form.labelWidth;
|
||||
|
||||
if (labelWidth || labelWidth === 0) {
|
||||
style.width = `${labelWidth}px`;
|
||||
}
|
||||
return style;
|
||||
},
|
||||
contentStyles () {
|
||||
let style = {};
|
||||
const labelWidth = this.labelWidth === 0 || this.labelWidth ? this.labelWidth : this.form.labelWidth;
|
||||
|
||||
if (labelWidth || labelWidth === 0) {
|
||||
style.marginLeft = `${labelWidth}px`;
|
||||
}
|
||||
return style;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setRules() {
|
||||
let rules = this.getRules();
|
||||
if (rules.length&&this.required) {
|
||||
return;
|
||||
}else if (rules.length) {
|
||||
rules.every((rule) => {
|
||||
this.isRequired = rule.required;
|
||||
});
|
||||
}else if (this.required){
|
||||
this.isRequired = this.required;
|
||||
}
|
||||
this.$off('on-form-blur', this.onFieldBlur);
|
||||
this.$off('on-form-change', this.onFieldChange);
|
||||
this.$on('on-form-blur', this.onFieldBlur);
|
||||
this.$on('on-form-change', this.onFieldChange);
|
||||
},
|
||||
getRules () {
|
||||
let formRules = this.form.rules;
|
||||
const selfRules = this.rules;
|
||||
|
||||
formRules = formRules ? formRules[this.prop] : [];
|
||||
|
||||
return [].concat(selfRules || formRules || []);
|
||||
},
|
||||
getFilteredRule (trigger) {
|
||||
const rules = this.getRules();
|
||||
|
||||
return rules.filter(rule => !rule.trigger || rule.trigger.indexOf(trigger) !== -1);
|
||||
},
|
||||
validate(trigger, callback = function () {}) {
|
||||
let rules = this.getFilteredRule(trigger);
|
||||
if (!rules || rules.length === 0) {
|
||||
if (!this.required) {
|
||||
callback();
|
||||
return true;
|
||||
}else {
|
||||
rules = [{required: true}];
|
||||
}
|
||||
}
|
||||
|
||||
this.validateState = 'validating';
|
||||
|
||||
let descriptor = {};
|
||||
descriptor[this.prop] = rules;
|
||||
|
||||
const validator = new AsyncValidator(descriptor);
|
||||
let model = {};
|
||||
|
||||
model[this.prop] = this.fieldValue;
|
||||
|
||||
validator.validate(model, { firstFields: true }, errors => {
|
||||
this.validateState = !errors ? 'success' : 'error';
|
||||
this.validateMessage = errors ? errors[0].message : '';
|
||||
|
||||
callback(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);
|
||||
|
||||
// if (Array.isArray(value) && value.length > 0) {
|
||||
// this.validateDisabled = true;
|
||||
// prop.o[prop.k] = [];
|
||||
// } else if (value !== this.initialValue) {
|
||||
// this.validateDisabled = true;
|
||||
// prop.o[prop.k] = this.initialValue;
|
||||
// }
|
||||
if (Array.isArray(value)) {
|
||||
this.validateDisabled = true;
|
||||
prop.o[prop.k] = [].concat(this.initialValue);
|
||||
} else {
|
||||
this.validateDisabled = true;
|
||||
prop.o[prop.k] = this.initialValue;
|
||||
}
|
||||
},
|
||||
onFieldBlur() {
|
||||
this.validate('blur');
|
||||
},
|
||||
onFieldChange() {
|
||||
if (this.validateDisabled) {
|
||||
this.validateDisabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.validate('change');
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.prop) {
|
||||
this.dispatch('iForm', 'on-form-item-add', this);
|
||||
|
||||
Object.defineProperty(this, 'initialValue', {
|
||||
value: this.fieldValue
|
||||
});
|
||||
|
||||
this.setRules();
|
||||
}
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.dispatch('iForm', 'on-form-item-remove', this);
|
||||
}
|
||||
};
|
||||
</script>
|
110
src/components/form/form.vue
Normal file
110
src/components/form/form.vue
Normal file
|
@ -0,0 +1,110 @@
|
|||
<template>
|
||||
<form :class="classes" :autocomplete="autocomplete"><slot></slot></form>
|
||||
</template>
|
||||
<script>
|
||||
import { oneOf } from '../../utils/assist';
|
||||
|
||||
const prefixCls = 'ivu-form';
|
||||
|
||||
export default {
|
||||
name: 'iForm',
|
||||
props: {
|
||||
model: {
|
||||
type: Object
|
||||
},
|
||||
rules: {
|
||||
type: Object
|
||||
},
|
||||
labelWidth: {
|
||||
type: Number
|
||||
},
|
||||
labelPosition: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['left', 'right', 'top']);
|
||||
},
|
||||
default: 'right'
|
||||
},
|
||||
inline: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showMessage: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
autocomplete: {
|
||||
validator (value) {
|
||||
return oneOf(value, ['on', 'off']);
|
||||
},
|
||||
default: 'off'
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
return { form : this };
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
fields: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
return [
|
||||
`${prefixCls}`,
|
||||
`${prefixCls}-label-${this.labelPosition}`,
|
||||
{
|
||||
[`${prefixCls}-inline`]: this.inline
|
||||
}
|
||||
];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetFields() {
|
||||
this.fields.forEach(field => {
|
||||
field.resetField();
|
||||
});
|
||||
},
|
||||
validate(callback) {
|
||||
return new Promise(resolve => {
|
||||
let valid = true;
|
||||
let count = 0;
|
||||
this.fields.forEach(field => {
|
||||
field.validate('', errors => {
|
||||
if (errors) {
|
||||
valid = false;
|
||||
}
|
||||
if (++count === this.fields.length) {
|
||||
// all finish
|
||||
resolve(valid);
|
||||
if (typeof callback === 'function') {
|
||||
callback(valid);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
validateField(prop, cb) {
|
||||
const field = this.fields.filter(field => field.prop === prop)[0];
|
||||
if (!field) { throw new Error('[iView warn]: must call validateField with valid prop string!'); }
|
||||
|
||||
field.validate('', cb);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
rules() {
|
||||
this.validate();
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$on('on-form-item-add', (field) => {
|
||||
if (field) this.fields.push(field);
|
||||
return false;
|
||||
});
|
||||
this.$on('on-form-item-remove', (field) => {
|
||||
if (field.prop) this.fields.splice(this.fields.indexOf(field), 1);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue