add Modal component

add Modal component
This commit is contained in:
梁灏 2016-09-29 15:47:19 +08:00
parent 39e6e96563
commit be966e9f49
20 changed files with 763 additions and 49 deletions

169
components/modal/confirm.js Normal file
View file

@ -0,0 +1,169 @@
import Vue from 'vue';
import Modal from './modal.vue';
import Icon from '../icon/icon.vue';
import Button from '../button/button.vue';
import { camelcaseToHyphen } from '../../utils/assist';
const prefixCls = 'ivu-modal-confirm';
Modal.newInstance = properties => {
const _props = properties || {};
let props = '';
Object.keys(_props).forEach(prop => {
props += ' :' + camelcaseToHyphen(prop) + '=' + prop;
});
const div = document.createElement('div');
div.innerHTML = `
<Modal${props} :visible.sync="visible" :width="width">
<div class="${prefixCls}">
<div class="${prefixCls}-head">
<div :class="iconTypeCls"><i :class="iconNameCls"></i></div>
<div class="${prefixCls}-head-title">{{{ title }}}</div>
</div>
<div class="${prefixCls}-body">
{{{ body }}}
</div>
<div class="${prefixCls}-footer">
<Button type="ghost" size="large" v-if="showCancel" @click="cancel">{{ cancelText }}</Button>
<Button type="primary" size="large" :loading="buttonLoading" @click="ok">{{ okText }}</Button>
</div>
</div>
</Modal>
`;
document.body.appendChild(div);
const modal = new Vue({
el: div,
components: { Modal, Button, Icon },
data: Object.assign(_props, {
visible: false,
width: 416,
title: '',
body: '',
iconType: '',
iconName: '',
okText: '确定',
cancelText: '取消',
showCancel: false,
loading: false,
buttonLoading: false
}),
computed: {
iconTypeCls () {
return [
`${prefixCls}-head-icon`,
`${prefixCls}-head-icon-${this.iconType}`
]
},
iconNameCls () {
return [
'ivu-icon',
`ivu-icon-${this.iconName}`
]
}
},
methods: {
cancel () {
this.visible = false;
this.buttonLoading = false;
this.onCancel();
this.remove();
},
ok () {
if (this.loading) {
this.buttonLoading = true;
} else {
this.visible = false;
this.remove();
}
this.onOk();
},
remove () {
setTimeout(() => {
this.destroy();
}, 300);
},
destroy () {
this.$destroy();
document.body.removeChild(div);
this.onRemove();
},
onOk () {},
onCancel () {},
onRemove () {}
}
}).$children[0];
return {
show (props) {
modal.$parent.showCancel = props.showCancel;
modal.$parent.iconType = props.icon;
switch (props.icon) {
case 'info':
modal.$parent.iconName = 'information-circled';
break;
case 'success':
modal.$parent.iconName = 'checkmark-circled';
break;
case 'warning':
modal.$parent.iconName = 'android-alert';
break;
case 'error':
modal.$parent.iconName = 'close-circled';
break;
case 'confirm':
modal.$parent.iconName = 'help-circled';
break;
}
if ('width' in props) {
modal.$parent.width = props.width;
}
if ('title' in props) {
modal.$parent.title = props.title;
}
if ('content' in props) {
modal.$parent.body = props.content;
}
if ('okText' in props) {
modal.$parent.okText = props.okText;
}
if ('cancelText' in props) {
modal.$parent.cancelText = props.cancelText;
}
if ('onCancel' in props) {
modal.$parent.onCancel = props.onCancel;
}
if ('onOk' in props) {
modal.$parent.onOk = props.onOk;
}
// async for ok
if ('loading' in props) {
modal.$parent.loading = props.loading;
}
// notice when component destroy
modal.$parent.onRemove = props.onRemove;
modal.visible = true;
},
remove () {
modal.visible = false;
modal.$parent.buttonLoading = false;
modal.$parent.remove();
},
component: modal
}
};
export default Modal;

65
components/modal/index.js Normal file
View file

@ -0,0 +1,65 @@
import Modal from './confirm';
let modalInstance;
function getModalInstance () {
modalInstance = modalInstance || Modal.newInstance({
closable: false,
maskClosable: false,
footerHide: true
});
return modalInstance;
}
function confirm (options) {
let instance = getModalInstance();
options.onRemove = function () {
modalInstance = null;
};
instance.show(options);
}
Modal.info = function (props = {}) {
props.icon = 'info';
props.showCancel = false;
return confirm(props);
};
Modal.success = function (props = {}) {
props.icon = 'success';
props.showCancel = false;
return confirm(props);
};
Modal.warning = function (props = {}) {
props.icon = 'warning';
props.showCancel = false;
return confirm(props);
};
Modal.error = function (props = {}) {
props.icon = 'error';
props.showCancel = false;
return confirm(props);
};
Modal.confirm = function (props = {}) {
props.icon = 'confirm';
props.showCancel = true;
return confirm(props);
};
Modal.remove = function () {
if (!modalInstance) { // at loading status, remove after Cancel
return false;
}
const instance = getModalInstance();
instance.remove();
};
export default Modal;

205
components/modal/modal.vue Normal file
View file

@ -0,0 +1,205 @@
<template>
<div :class="wrapClasses">
<div :class="maskClasses" v-show="visible" @click="mask" transition="fade"></div>
<div :class="classes" :style="styles" v-show="visible" transition="ease">
<div :class="[`${prefixCls}-content`]">
<a :class="[`${prefixCls}-close`]" v-if="closable" @click="close">
<slot name="close">
<Icon type="ios-close-empty"></Icon>
</slot>
</a>
<div :class="[`${prefixCls}-header`]" v-if="showHead" v-el:head><slot name="header"><p>{{ title }}</p></slot></div>
<div :class="[`${prefixCls}-body`]"><slot></slot></div>
<div :class="[`${prefixCls}-footer`]" v-if="!footerHide">
<slot name="footer">
<Button type="ghost" size="large" @click="cancel">{{ cancelText }}</Button>
<Button type="primary" size="large" :loading="buttonLoading" @click="ok">{{ okText }}</Button>
</slot>
</div>
</div>
</div>
</div>
</template>
<script>
import Icon from '../icon';
import Button from '../button';
import { getScrollBarSize } from '../../utils/assist';
const prefixCls = 'ivu-modal';
export default {
components: { Icon, Button },
props: {
visible: {
type: Boolean,
default: false
},
closable: {
type: Boolean,
default: true
},
maskClosable: {
type: Boolean,
default: true
},
title: {
type: String
},
width: {
type: [Number, String],
default: 520
},
okText: {
type: String,
default: '确定'
},
cancelText: {
type: String,
default: '取消'
},
loading: {
type: Boolean,
default: false
},
style: {
type: Object
},
className: {
type: String
},
// for instance
footerHide: {
type: Boolean,
default: false
}
},
data () {
return {
prefixCls: prefixCls,
wrapShow: false,
showHead: true,
buttonLoading: false
}
},
computed: {
wrapClasses () {
return [
`${prefixCls}-wrap`,
{
[`${prefixCls}-hidden`]: !this.wrapShow,
[`${this.className}`]: !!this.className
}
]
},
maskClasses () {
return `${prefixCls}-mask`;
},
classes () {
return `${prefixCls}`;
},
styles () {
let style = {};
const styleWidth = {
width: `${this.width}px`
};
const customStyle = !!this.style ? this.style : {};
Object.assign(style, styleWidth, customStyle);
return style;
}
},
methods: {
close () {
this.visible = false;
this.$emit('on-cancel');
},
mask () {
if (this.maskClosable) {
this.close();
}
},
cancel () {
this.close();
},
ok () {
if (this.loading) {
this.buttonLoading = true;
} else {
this.visible = false;
}
this.$emit('on-ok');
},
EscClose (e) {
if (this.visible && this.closable) {
if (e.keyCode === 27) {
this.close()
}
}
},
checkScrollBar () {
let fullWindowWidth = window.innerWidth;
if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8
const documentElementRect = document.documentElement.getBoundingClientRect();
fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left);
}
this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth;
if (this.bodyIsOverflowing) {
this.scrollBarWidth = getScrollBarSize();
}
},
setScrollBar () {
if (this.bodyIsOverflowing && this.scrollBarWidth !== undefined) {
document.body.style.paddingRight = `${this.scrollBarWidth}px`;
}
},
resetScrollBar () {
document.body.style.paddingRight = '';
},
addScrollEffect () {
this.checkScrollBar();
this.setScrollBar();
document.body.style.overflow = 'hidden';
},
removeScrollEffect() {
document.body.style.overflow = '';
this.resetScrollBar();
}
},
ready () {
if (this.visible) {
this.wrapShow = true;
}
let showHead = true;
if (this.$els.head.innerHTML == '<p></p>' && !this.title) {
showHead = false;
}
this.showHead = showHead;
// ESC close
document.addEventListener('keydown', this.EscClose);
},
beforeDestroy () {
document.removeEventListener('keydown', this.EscClose);
},
watch: {
visible (val) {
if (val === false) {
this.buttonLoading = false;
setTimeout(() => {
this.wrapShow = false;
}, 300);
this.removeScrollEffect();
} else {
this.wrapShow = true;
this.addScrollEffect();
}
}
}
}
</script>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -23,6 +23,7 @@ import Card from './components/card';
import Message from './components/message';
import Notice from './components/notice';
import LoadingBar from './components/loading-bar';
import Modal from './components/modal';
const iview = {
Button,
@ -50,7 +51,8 @@ const iview = {
Card,
Message,
Notice,
LoadingBar
LoadingBar,
Modal
};
module.exports = iview;

View file

@ -2,32 +2,59 @@
</style>
<template>
<div>welcome</div>
<Checkbox-group :model.sync="fruit">
<Checkbox value="香蕉"></Checkbox>
<Checkbox value="苹果"></Checkbox>
<Checkbox value="西瓜"></Checkbox>
</Checkbox-group>
{{ fruit | json }}
<Button @click="update">update fruit</Button>
<Button @click="info">info</Button>
<Button @click="success">success</Button>
<Button @click="warning">warning</Button>
<Button @click="error">error</Button>
<Button @click="confirm">confirm</Button>
</template>
<script>
import { Checkbox, Icon, Button } from 'iview';
const CheckboxGroup = Checkbox.Group;
import { Modal, Button, Message } from 'iview';
export default {
components: { Checkbox, CheckboxGroup, Icon, Button },
components: { Modal, Button },
props: {
},
data () {
return {
fruit: ['苹果']
}
},
methods: {
update () {
this.fruit = ['香蕉', '西瓜']
info () {
Modal.info({
title: '这是对话框标题',
content: `<p>这是对话框内容</p><p>这是对话框内容这是对话框内容这是对话框内容这是对话框内容这是对话框内容这是对话框内容这是对话框内容这是对话框内容这是对话框内容这是对话框内容</p>`
});
},
success () {
Modal.success();
},
warning () {
Modal.warning();
},
error () {
Modal.error();
},
confirm () {
Modal.confirm({
// okText: 'OK',
// cancelText: 'Cancel',
title: '删除提示',
content: '删除后将不可找回,您确定要删除吗?',
onCancel () {
Message.info('cancel it');
},
onOk () {
setTimeout(() => {
Modal.remove();
Message.success('OK!');
}, 2000);
},
loading: true
});
}
}
}

View file

@ -5,14 +5,25 @@
<Button @click="warning">warning</Button>
<Button @click="loading">手动消失</Button>
<Button @click="destroy">destroy</Button>
<Alert closable>消息提示文案</Alert>
<Alert type="success" show-icon closable>
成功提示文案
<span slot="desc">成功的提示描述文案成功的提示描述文案成功的提示描述文案成功的提示描述文案成功的提示描述文案</span>
</Alert>
<Card :bordered="false">
<p slot="title">无边框标题</p>
<p>无边框内容填充无边框内容填充无边框内容填充无边框内容填充无边框内容填充无边框内容填充无边框内容填充无边框内容填充无边框内容填充无边框内容填充无边框内容填充</p>
</Card>
</template>
<script>
import { Message, Button } from 'iview';
import { Message, Button, Alert, Card } from 'iview';
export default {
components: {
Message,
Button
Button,
Alert,
Card
},
props: {

View file

@ -1,6 +1,6 @@
{
"name": "iview",
"version": "0.9.1",
"version": "0.9.2",
"title": "iView",
"description": "A high quality UI components Library with Vue.js",
"homepage": "http://www.iviewui.com",

View file

@ -0,0 +1,36 @@
.ease-motion(@className, @keyframeName) {
.make-motion(@className, @keyframeName);
.@{className}-enter, .@{className}-appear {
opacity: 0;
animation-timing-function: linear;
animation-duration: @transition-time;
}
.@{className}-leave {
animation-timing-function: linear;
animation-duration: @transition-time;
}
}
.ease-motion(ease, ivuEase);
@keyframes ivuEaseIn {
0% {
opacity: 0;
transform: scale(0.9);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes ivuEaseOut {
0% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(0.9);
}
}

View file

@ -23,4 +23,5 @@
}
@import "fade";
@import "move";
@import "move";
@import "ease";

View file

@ -62,16 +62,7 @@
}
&-close {
font-size: 12px;
position: absolute;
right: 16px;
top: 8px;
overflow: hidden;
cursor: pointer;
.@{icon-prefix-cls}-ios-close-empty {
.close-base(-3px);
}
.content-close(-3px);
}
&-with-desc {

View file

@ -35,21 +35,7 @@
}
&-head {
border-bottom: 1px solid @border-color-split;
padding: 10px 16px;
line-height: 1;
p {
display: inline-block;
width: 100%;
height: 20px;
line-height: 20px;
font-size: 14px;
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.content-header;
}
&-extra {

View file

@ -18,4 +18,5 @@
@import "progress";
@import "timeline";
@import "page";
@import "steps";
@import "steps";
@import "modal";

View file

@ -0,0 +1,133 @@
@modal-prefix-cls: ~"@{css-prefix}modal";
@confirm-prefix-cls: ~"@{css-prefix}modal-confirm";
.@{modal-prefix-cls} {
width: auto;
margin: 0 auto;
position: relative;
outline: none;
top: 100px;
&-hidden {
display: none;
}
&-wrap {
position: fixed;
overflow: auto;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: @zindex-modal;
-webkit-overflow-scrolling: touch;
outline: 0;
}
&-wrap * {
box-sizing: border-box;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
&-mask {
.mask;
}
&-content {
position: relative;
background-color: #fff;
border: 0;
border-radius: @border-radius-base;
background-clip: padding-box;
}
&-header {
.content-header;
}
&-close {
.content-close;
}
&-body {
padding: 16px;
font-size: 12px;
line-height: 1.5;
}
&-footer {
border-top: 1px solid @border-color-split;
padding: 10px 18px 10px 10px;
text-align: right;
button + button {
margin-left: 8px;
margin-bottom: 0;
}
}
}
@media (max-width: 768px) {
.@{modal-prefix-cls} {
width: auto !important;
margin: 10px;
}
.vertical-center-modal {
.@{modal-prefix-cls} {
flex: 1;
}
}
}
.@{confirm-prefix-cls} {
padding: 10px 25px 20px;
&-head {
&-icon {
display: inline-block;
font-size: 28px;
margin-right: 5px;
padding: 0 1px;
position: relative;
top: 5px;
&-info {
color: @primary-color;
}
&-success {
color: @success-color;
}
&-warning {
color: @warning-color;
}
&-error {
color: @error-color;
}
&-confirm {
color: @warning-color;
}
}
&-title {
display: inline-block;
font-size: @font-size-base;
color: @text-color;
font-weight: 700;
}
}
&-body{
margin-left: 35px;
margin-top: 8px;
font-size: 12px;
color: @text-color;
}
&-footer{
margin-top: 20px;
text-align: right;
button + button {
margin-left: 8px;
margin-bottom: 0;
}
}
}

View file

@ -0,0 +1,32 @@
@icon-prefix-cls: ~"@{css-prefix}icon";
.content-header() {
border-bottom: 1px solid @border-color-split;
padding: 10px 16px;
line-height: 1;
p {
display: inline-block;
width: 100%;
height: 20px;
line-height: 20px;
font-size: 14px;
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.content-close(@top: 0) {
font-size: 12px;
position: absolute;
right: 16px;
top: 8px;
overflow: hidden;
cursor: pointer;
.@{icon-prefix-cls}-ios-close-empty {
.close-base(@top);
}
}

View file

@ -11,4 +11,6 @@
@import "close";
@import "checkbox";
@import "input";
@import "breadcrumb";
@import "breadcrumb";
@import "mask";
@import "content"; // card、modal

13
styles/mixins/mask.less Normal file
View file

@ -0,0 +1,13 @@
.mask() {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(55, 55, 55, 0.6);
height: 100%;
&-hidden {
display: none;
}
}

View file

@ -99,9 +99,10 @@
@tag-font-size : 12px;
// Z-index
@zindex-spin : 8;
@zindex-affix : 10;
@zindex-back-top : 10;
@zindex-spin : 8;
@zindex-modal : 1000;
@zindex-message : 1010;
@zindex-notification : 1010;
@zindex-loading-bar : 2000;

View file

@ -10,4 +10,43 @@ export function oneOf (value, validList) {
export function camelcaseToHyphen (str) {
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}
// For Modal scrollBar hidden
let cached;
export function getScrollBarSize (fresh) {
if (fresh || cached === undefined) {
const inner = document.createElement('div');
inner.style.width = '100%';
inner.style.height = '200px';
const outer = document.createElement('div');
const outerStyle = outer.style;
outerStyle.position = 'absolute';
outerStyle.top = 0;
outerStyle.left = 0;
outerStyle.pointerEvents = 'none';
outerStyle.visibility = 'hidden';
outerStyle.width = '200px';
outerStyle.height = '150px';
outerStyle.overflow = 'hidden';
outer.appendChild(inner);
document.body.appendChild(outer);
const widthContained = inner.offsetWidth;
outer.style.overflow = 'scroll';
let widthScroll = inner.offsetWidth;
if (widthContained === widthScroll) {
widthScroll = outer.clientWidth;
}
document.body.removeChild(outer);
cached = widthContained - widthScroll;
}
return cached;
}