Browse Source

Loading: add loading service

Leopoldthecoder 8 years ago
parent
commit
5fcd73cd85

+ 5 - 2
build/bin/build-entry.js

@@ -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, {

+ 27 - 2
examples/docs/en-US/loading.md

@@ -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 | — | — |

+ 27 - 1
examples/docs/zh-CN/loading.md

@@ -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 | — | — |

+ 1 - 1
examples/docs/zh-CN/message.md

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

+ 6 - 2
packages/loading/index.js

@@ -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
+};

+ 7 - 19
packages/loading/src/directive.js

@@ -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);
         }
       }
     }

+ 86 - 0
packages/loading/src/index.js

@@ -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;

+ 22 - 0
packages/loading/src/loading.vue

@@ -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>

+ 0 - 21
packages/loading/src/spinner.vue

@@ -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>

+ 8 - 12
packages/theme-default/src/loading.css

@@ -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);
-      }
-    }
   }
 }
 

+ 4 - 2
src/index.js

@@ -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,

+ 174 - 110
test/unit/specs/loading.spec.js

@@ -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
-        };
-      }
-    });
-    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);
+        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);
+      });
     });
-  });
 
-  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: `
+    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();
+        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: `
+    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();
+        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: `
+    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();
+        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: `
+    it('text', done => {
+      vm = createVue({
+        template: `
         <div v-loading="loading" element-loading-text="拼命加载中"></div>
       `,
 
-      data() {
-        return {
-          loading: true
-        };
-      }
-    }, true);
-    Vue.nextTick(() => {
+        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();
+      });
+    });
+  });
+
+  describe('as a service', () => {
+    it('create', () => {
+      loadingInstance = Loading();
+      expect(document.querySelector('.el-loading-mask')).to.exist;
+    });
+
+    it('close', () => {
+      loadingInstance = Loading();
+      loadingInstance.close();
+      expect(document.querySelector('.el-loading-mask')).to.not.exist;
+    });
+
+    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');
-      const text = mask.querySelector('.el-loading-text');
+      expect(mask.parentNode).to.equal(document.body);
+      expect(mask.classList.contains('is-fullscreen')).to.true;
+    });
+
+    it('lock', () => {
+      loadingInstance = Loading({ lock: true });
+      expect(document.body.style.overflow).to.equal('hidden');
+    });
+
+    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...');
     });
   });
 });