Merge pull request #1083 from Leopoldthecoder/loading-service

Loading: add loading service
pull/1099/head
baiyaaaaa 2016-11-16 10:48:59 +08:00 committed by GitHub
commit 9619be271b
12 changed files with 375 additions and 180 deletions

View File

@ -19,8 +19,9 @@ const install = function(Vue, opts = {}) {
{{install}}
Vue.use(Loading);
Vue.use(Loading.directive);
Vue.prototype.$loading = Loading.service;
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$alert = MessageBox.alert;
Vue.prototype.$confirm = MessageBox.confirm;
@ -38,6 +39,8 @@ module.exports = {
version: '{{version}}',
locale: locale.use,
install,
Loading: Loading.directive,
LoadingService: Loading.service,
{{list}}
};
`;
@ -65,7 +68,7 @@ ComponentNames.forEach(name => {
}));
}
listTemplate.push(` ${componentName}`);
if (componentName !== 'Loading') listTemplate.push(` ${componentName}`);
});
var template = render(MAIN_TEMPLATE, {

View File

@ -40,7 +40,7 @@ Show animation while loading data.
Displays animation in a container (such as a table) while loading data.
:::demo We provide a custom directive `v-loading`. You just need to bind a `boolean` value to it. By default, the loading mask will append to the element where the directive is used. Adding the `body` modifier makes the mask append to the body element.
:::demo Element provides two ways to invoke Loading: directive and service. For the custom directive `v-loading`, you just need to bind a `boolean` value to it. By default, the loading mask will append to the element where the directive is used. Adding the `body` modifier makes the mask append to the body element.
```html
<template>
@ -100,7 +100,7 @@ Displays animation in a container (such as a table) while loading data.
You can customize a text message.
:::demo
:::demo Add attribute `element-loading-text` to the element on which `v-loading` is bound, and its value will be displayed under the spinner.
```html
<template>
<el-table
@ -186,3 +186,28 @@ Show a full screen animation while loading data.
```
:::
### Service
You can also invoke Loading with a service. Import Loading service:
```javascript
import { LoadingService } from 'element-ui';
```
Invoke it:
```javascript
LoadingService(options);
```
The parameter `options` is the configuration of Loading, and its details can be found in the following table. `LoadingService` returns a Loading instance, and you can close it by invoking its `close` method:
```javascript
let loadingInstance = LoadingService(options);
loadingInstance.close();
```
If Element is imported entirely, a globally method `$loading` will be registered to Vue.prototype. You can invoke it like this: `this.$loading(options)`, and it also returns a Loading instance.
### Options
| Attribute | Description | Type | Accepted Values | Default |
|---------- |-------------- |---------- |-------------------------------- |-------- |
| target | the DOM node Loading needs to cover. Accepts a DOM object or a string. If it's a string, it will be passed to `document.querySelector` to get the corresponding DOM node | object/string | — | document.body |
| body | same as the `body` modifier of `v-loading` | boolean | — | false |
| fullscreen | same as the `fullscreen` modifier of `v-loading` | boolean | — | true |
| lock | same as the `lock` modifier of `v-loading` | boolean | — | false |
| text | loading text that displays under the spinner | string | — | — |
| customClass | custom class name for Loading | string | — | — |

View File

@ -45,7 +45,7 @@
在表格等容器中加载数据时显示。
:::demo 在 Loading 组件中Element 准备了自定义命令`v-loading`,只需要绑定`Boolean`即可。默认状况下Loading 遮罩会插入到绑定元素的子节点,通过添加`body`修饰符,可以使遮罩插入至 DOM 中的 body 上。
:::demo Element 提供了两种调用 Loading 的方法:指令和服务。对于自定义指令`v-loading`,只需要绑定`Boolean`即可。默认状况下Loading 遮罩会插入到绑定元素的子节点,通过添加`body`修饰符,可以使遮罩插入至 DOM 中的 body 上。
```html
<template>
<el-table
@ -189,3 +189,29 @@
</script>
```
:::
### 服务
Loading 还可以以服务的方式调用。引入 Loading 服务:
```javascript
import { LoadingService } from 'element-ui';
```
在需要调用时:
```javascript
LoadingService(options);
```
其中 `options` 参数为 Loading 的配置项,具体见下表。`LoadingService` 会返回一个 Loading 实例,可通过调用该实例的 `close` 方法来关闭它:
```javascript
let loadingInstance = LoadingService(options);
loadingInstance.close();
```
如果完整引入了 Element那么 Vue.prototype 上会有一个全局方法 `$loading`,它的调用方式为:`this.$loading(options)`,同样会返回一个 Loading 实例。
### Options
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|---------- |-------------- |---------- |-------------------------------- |-------- |
| target | Loading 需要覆盖的 DOM 节点。可传入一个 DOM 对象或字符串;<br>若传入字符串,则会将其作为参数传入 `document.querySelector`<br>以获取到对应 DOM 节点 | object/string | — | document.body |
| body | 同 `v-loading` 指令中的 `body` 修饰符 | boolean | — | false |
| fullscreen | 同 `v-loading` 指令中的 `fullscreen` 修饰符 | boolean | — | true |
| lock | 同 `v-loading` 指令中的 `lock` 修饰符 | boolean | — | false |
| text | 显示在加载图标下方的加载文案 | string | — | — |
| customClass | Loading 的自定义类名 | string | — | — |

View File

@ -180,7 +180,7 @@
### 全局方法
element 为 Vue.prototype 添加了全局方法 $message。因此在 vue instance 中可以采用本页面中的方式调用 `Message`
Element 为 Vue.prototype 添加了全局方法 $message。因此在 vue instance 中可以采用本页面中的方式调用 `Message`
### 单独引用

View File

@ -1,2 +1,6 @@
import Directive from './src/directive';
export default Directive;
import directive from './src/directive';
import service from './src/index';
export default {
directive,
service
};

View File

@ -1,6 +1,6 @@
import Vue from 'vue';
import { addClass, removeClass } from 'wind-dom/src/class';
let Spinner = Vue.extend(require('./spinner.vue'));
let Mask = Vue.extend(require('./loading.vue'));
exports.install = Vue => {
let toggleLoading = (el, binding) => {
@ -11,12 +11,9 @@ exports.install = Vue => {
el.originalOverflow = document.body.style.overflow;
addClass(el.mask, 'is-fullscreen');
addClass(el.spinner, 'is-fullscreen');
insertDom(document.body, el, binding);
} else {
removeClass(el.mask, 'is-fullscreen');
removeClass(el.spinner, 'is-fullscreen');
if (binding.modifiers.body) {
el.originalPosition = document.body.style.position;
@ -39,7 +36,6 @@ exports.install = Vue => {
} else {
if (el.domVisible) {
el.mask.style.display = 'none';
el.spinner.style.display = 'none';
el.domVisible = false;
if (binding.modifiers.fullscreen && el.originalOverflow !== 'hidden') {
@ -66,29 +62,25 @@ exports.install = Vue => {
parent.style.overflow = 'hidden';
}
directive.mask.style.display = 'block';
directive.spinner.style.display = 'inline-block';
directive.domVisible = true;
parent.appendChild(directive.mask);
directive.mask.appendChild(directive.spinner);
directive.domInserted = true;
}
};
Vue.directive('loading', {
bind: function(el, binding) {
el.mask = document.createElement('div');
el.mask.className = 'el-loading-mask';
el.maskStyle = {};
let spinner = new Spinner({
let mask = new Mask({
el: document.createElement('div'),
data: {
text: el.getAttribute('element-loading-text'),
fullScreen: !!binding.modifiers.fullscreen
fullscreen: !!binding.modifiers.fullscreen
}
});
spinner.$mount(el.mask);
el.spinner = spinner.$el;
el.mask = mask.$el;
el.maskStyle = {};
toggleLoading(el, binding);
},
@ -102,14 +94,10 @@ exports.install = Vue => {
if (el.domInserted) {
if (binding.modifiers.fullscreen || binding.modifiers.body) {
document.body.removeChild(el.mask);
el.mask.removeChild(el.spinner);
} else {
el.mask &&
el.mask.parentNode &&
el.mask.parentNode.removeChild(el.mask);
el.spinner &&
el.spinner.parentNode &&
el.spinner.parentNode.removeChild(el.spinner);
}
}
}

View File

@ -0,0 +1,86 @@
import Vue from 'vue';
import loadingVue from './loading.vue';
import merge from 'element-ui/src/utils/merge';
const LoadingConstructor = Vue.extend(loadingVue);
const defaults = {
text: null,
fullscreen: true,
body: false,
lock: false,
customClass: ''
};
let originalPosition, originalOverflow;
LoadingConstructor.prototype.close = function() {
if (this.fullscreen && originalOverflow !== 'hidden') {
document.body.style.overflow = originalOverflow;
}
if (this.fullscreen || this.body) {
document.body.style.position = originalPosition;
} else {
this.target.style.position = originalPosition;
}
this.$el &&
this.$el.parentNode &&
this.$el.parentNode.removeChild(this.$el);
this.$destroy();
};
const addStyle = (options, parent, element) => {
let maskStyle = {};
if (options.fullscreen) {
originalPosition = document.body.style.position;
originalOverflow = document.body.style.overflow;
} else if (options.body) {
originalPosition = document.body.style.position;
['top', 'left'].forEach(property => {
let scroll = property === 'top' ? 'scrollTop' : 'scrollLeft';
maskStyle[property] = options.target.getBoundingClientRect()[property] +
document.body[scroll] +
document.documentElement[scroll] +
'px';
});
['height', 'width'].forEach(property => {
maskStyle[property] = options.target.getBoundingClientRect()[property] + 'px';
});
} else {
originalPosition = parent.style.position;
}
Object.keys(maskStyle).forEach(property => {
element.style[property] = maskStyle[property];
});
};
const Loading = (options = {}) => {
options = merge({}, defaults, options);
if (typeof options.target === 'string') {
options.target = document.querySelector(options.target);
}
options.target = options.target || document.body;
if (options.target !== document.body) {
options.fullscreen = false;
} else {
options.body = true;
}
let parent = options.body ? document.body : options.target;
let instance = new LoadingConstructor({
el: document.createElement('div'),
data: options
});
addStyle(options, parent, instance.$el);
if (originalPosition !== 'absolute') {
parent.style.position = 'relative';
}
if (options.fullscreen && options.lock) {
parent.style.overflow = 'hidden';
}
parent.appendChild(instance.$el);
return instance;
};
export default Loading;

View File

@ -0,0 +1,22 @@
<template>
<div class="el-loading-mask" :class="[customClass, { 'is-fullscreen': fullscreen }]">
<div class="el-loading-spinner">
<svg class="circular" viewBox="25 25 50 50">
<circle class="path" cx="50" cy="50" r="20" fill="none"/>
</svg>
<p v-if="text" class="el-loading-text">{{ text }}</p>
</div>
</div>
</template>
<script>
export default {
data() {
return {
text: null,
fullscreen: true,
customClass: ''
};
}
};
</script>

View File

@ -1,21 +0,0 @@
<template>
<div
class="el-loading-spinner"
:class="{ 'is-full-screen': fullScreen }">
<svg class="circular" viewBox="25 25 50 50">
<circle class="path" cx="50" cy="50" r="20" fill="none"/>
</svg>
<p v-if="text" class="el-loading-text">{{ text }}</p>
</div>
</template>
<script>
export default {
data() {
return {
text: null,
fullScreen: false
};
}
};
</script>

View File

@ -14,6 +14,14 @@
@when fullscreen {
position: fixed;
.el-loading-spinner {
margin-top: calc(- var(--loading-fullscreen-spinner-size) / 2);
.circular {
width: var(--loading-fullscreen-spinner-size);
}
}
}
}
@ -24,10 +32,6 @@
text-align: center;
position: absolute;
@when fullscreen {
position: fixed;
}
.el-loading-text {
color: var(--color-primary);
margin: 3px 0;
@ -47,14 +51,6 @@
stroke: var(--color-primary);
stroke-linecap: round;
}
@when full-screen {
margin-top: calc(- var(--loading-fullscreen-spinner-size) / 2);
.circular {
width: var(--loading-fullscreen-spinner-size);
}
}
}
}

View File

@ -114,8 +114,9 @@ const install = function(Vue, opts = {}) {
Vue.component(Steps.name, Steps);
Vue.component(Step.name, Step);
Vue.use(Loading);
Vue.use(Loading.directive);
Vue.prototype.$loading = Loading.service;
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$alert = MessageBox.alert;
Vue.prototype.$confirm = MessageBox.confirm;
@ -133,6 +134,8 @@ module.exports = {
version: '1.0.0',
locale: locale.use,
install,
Loading: Loading.directive,
LoadingService: Loading.service,
Pagination,
Dialog,
Autocomplete,
@ -175,7 +178,6 @@ module.exports = {
Alert,
Notification,
Slider,
Loading,
Icon,
Row,
Col,

View File

@ -1,158 +1,222 @@
import { createVue, destroyVM } from '../util';
import Vue from 'vue';
import LoadingRaw from 'packages/loading';
const Loading = LoadingRaw.service;
describe('Loading', () => {
let vm;
let vm, loadingInstance;
afterEach(() => {
destroyVM(vm);
loadingInstance && loadingInstance.close();
});
it('create', done => {
vm = createVue({
template: `
describe('as a directive', () => {
it('create', done => {
vm = createVue({
template: `
<div v-loading="loading"></div>
`,
data() {
return {
loading: true
};
}
data() {
return {
loading: true
};
}
});
Vue.nextTick(() => {
const mask = vm.$el.querySelector('.el-loading-mask');
expect(mask).to.exist;
vm.loading = false;
setTimeout(() => {
expect(mask.style.display).to.equal('none');
done();
}, 100);
});
});
Vue.nextTick(() => {
const mask = vm.$el.querySelector('.el-loading-mask');
expect(mask).to.exist;
vm.loading = false;
setTimeout(() => {
expect(mask.style.display).to.equal('none');
done();
}, 100);
});
});
it('unbind', done => {
const vm1 = createVue({
template: `
it('unbind', done => {
const vm1 = createVue({
template: `
<div v-if="show" v-loading="loading"></div>
`,
data() {
return {
show: true,
loading: true
};
}
});
const vm2 = createVue({
template: `
data() {
return {
show: true,
loading: true
};
}
});
const vm2 = createVue({
template: `
<div v-if="show" v-loading.body="loading"></div>
`,
data() {
return {
show: true,
loading: true
};
}
});
Vue.nextTick(() => {
vm1.loading = false;
vm2.loading = false;
data() {
return {
show: true,
loading: true
};
}
});
Vue.nextTick(() => {
vm1.show = false;
vm2.show = false;
vm1.loading = false;
vm2.loading = false;
Vue.nextTick(() => {
expect(document.querySelector('.el-loading-mask')).to.not.exist;
done();
vm1.show = false;
vm2.show = false;
Vue.nextTick(() => {
expect(document.querySelector('.el-loading-mask')).to.not.exist;
done();
});
});
});
});
it('body', done => {
vm = createVue({
template: `
<div v-loading.body="loading"></div>
`,
data() {
return {
loading: true
};
}
}, true);
Vue.nextTick(() => {
const mask = document.querySelector('.el-loading-mask');
expect(mask.parentNode === document.body).to.true;
vm.loading = false;
document.body.removeChild(mask);
document.body.removeChild(vm.$el);
done();
});
});
it('fullscreen', done => {
vm = createVue({
template: `
<div v-loading.fullscreen="loading"></div>
`,
data() {
return {
loading: true
};
}
}, true);
Vue.nextTick(() => {
const mask = document.querySelector('.el-loading-mask');
expect(mask.parentNode === document.body).to.true;
expect(mask.classList.contains('is-fullscreen')).to.true;
vm.loading = false;
document.body.removeChild(mask);
document.body.removeChild(vm.$el);
done();
});
});
it('lock', done => {
vm = createVue({
template: `
<div v-loading.fullscreen.lock="loading"></div>
`,
data() {
return {
loading: true
};
}
}, true);
Vue.nextTick(() => {
expect(document.body.style.overflow).to.equal('hidden');
vm.loading = false;
document.body.removeChild(document.querySelector('.el-loading-mask'));
document.body.removeChild(vm.$el);
done();
});
});
it('text', done => {
vm = createVue({
template: `
<div v-loading="loading" element-loading-text="拼命加载中"></div>
`,
data() {
return {
loading: true
};
}
}, true);
Vue.nextTick(() => {
const mask = document.querySelector('.el-loading-mask');
const text = mask.querySelector('.el-loading-text');
expect(text).to.exist;
expect(text.textContent).to.equal('拼命加载中');
done();
});
});
});
it('body', done => {
vm = createVue({
template: `
<div v-loading.body="loading"></div>
`,
data() {
return {
loading: true
};
}
}, true);
Vue.nextTick(() => {
const mask = document.querySelector('.el-loading-mask');
expect(mask.parentNode === document.body).to.true;
vm.loading = false;
document.body.removeChild(mask);
document.body.removeChild(vm.$el);
done();
describe('as a service', () => {
it('create', () => {
loadingInstance = Loading();
expect(document.querySelector('.el-loading-mask')).to.exist;
});
});
it('fullscreen', done => {
vm = createVue({
template: `
<div v-loading.fullscreen="loading"></div>
`,
it('close', () => {
loadingInstance = Loading();
loadingInstance.close();
expect(document.querySelector('.el-loading-mask')).to.not.exist;
});
data() {
return {
loading: true
};
}
}, true);
Vue.nextTick(() => {
it('target', () => {
vm = createVue({
template: `
<div class="loading-container"></div>
`
}, true);
loadingInstance = Loading({ target: '.loading-container' });
let mask = document.querySelector('.el-loading-mask');
expect(mask).to.exist;
expect(mask.parentNode).to.equal(document.querySelector('.loading-container'));
});
it('body', () => {
vm = createVue({
template: `
<div class="loading-container"></div>
`
}, true);
loadingInstance = Loading({
target: '.loading-container',
body: true
});
let mask = document.querySelector('.el-loading-mask');
expect(mask).to.exist;
expect(mask.parentNode).to.equal(document.body);
});
it('fullscreen', () => {
loadingInstance = Loading({ fullScreen: true });
const mask = document.querySelector('.el-loading-mask');
expect(mask.parentNode === document.body).to.true;
expect(mask.parentNode).to.equal(document.body);
expect(mask.classList.contains('is-fullscreen')).to.true;
vm.loading = false;
document.body.removeChild(mask);
document.body.removeChild(vm.$el);
done();
});
});
it('lock', done => {
vm = createVue({
template: `
<div v-loading.fullscreen.lock="loading"></div>
`,
data() {
return {
loading: true
};
}
}, true);
Vue.nextTick(() => {
it('lock', () => {
loadingInstance = Loading({ lock: true });
expect(document.body.style.overflow).to.equal('hidden');
vm.loading = false;
document.body.removeChild(document.querySelector('.el-loading-mask'));
document.body.removeChild(vm.$el);
done();
});
});
it('text', done => {
vm = createVue({
template: `
<div v-loading="loading" element-loading-text="拼命加载中"></div>
`,
data() {
return {
loading: true
};
}
}, true);
Vue.nextTick(() => {
const mask = document.querySelector('.el-loading-mask');
const text = mask.querySelector('.el-loading-text');
it('text', () => {
loadingInstance = Loading({ text: 'Loading...' });
const text = document.querySelector('.el-loading-text');
expect(text).to.exist;
expect(text.textContent).to.equal('拼命加载中');
done();
expect(text.textContent).to.equal('Loading...');
});
});
});