Prechádzať zdrojové kódy

Image: add image component (#15117)

Simona 6 rokov pred
rodič
commit
d9462ab771
67 zmenil súbory, kde vykonal 1041 pridanie a 2 odobranie
  1. 2 1
      components.json
  2. 78 0
      examples/demo-styles/image.scss
  3. 1 0
      examples/demo-styles/index.scss
  4. 132 0
      examples/docs/en-US/image.md
  5. 132 0
      examples/docs/es/image.md
  6. 132 0
      examples/docs/fr-FR/image.md
  7. 132 0
      examples/docs/zh-CN/image.md
  8. 16 0
      examples/nav.config.json
  9. 8 0
      packages/image/index.js
  10. 123 0
      packages/image/src/main.vue
  11. 32 0
      packages/theme-chalk/src/image.scss
  12. 1 0
      packages/theme-chalk/src/index.scss
  13. 4 1
      src/index.js
  14. 3 0
      src/locale/lang/af-ZA.js
  15. 3 0
      src/locale/lang/ar.js
  16. 3 0
      src/locale/lang/bg.js
  17. 3 0
      src/locale/lang/ca.js
  18. 3 0
      src/locale/lang/cs-CZ.js
  19. 3 0
      src/locale/lang/da.js
  20. 3 0
      src/locale/lang/de.js
  21. 3 0
      src/locale/lang/ee.js
  22. 3 0
      src/locale/lang/el.js
  23. 3 0
      src/locale/lang/en.js
  24. 3 0
      src/locale/lang/es.js
  25. 3 0
      src/locale/lang/eu.js
  26. 3 0
      src/locale/lang/fa.js
  27. 3 0
      src/locale/lang/fi.js
  28. 3 0
      src/locale/lang/fr.js
  29. 3 0
      src/locale/lang/he.js
  30. 3 0
      src/locale/lang/hr.js
  31. 3 0
      src/locale/lang/hu.js
  32. 3 0
      src/locale/lang/hy-AM.js
  33. 3 0
      src/locale/lang/id.js
  34. 3 0
      src/locale/lang/it.js
  35. 3 0
      src/locale/lang/ja.js
  36. 3 0
      src/locale/lang/kg.js
  37. 3 0
      src/locale/lang/km.js
  38. 3 0
      src/locale/lang/ko.js
  39. 3 0
      src/locale/lang/ku.js
  40. 3 0
      src/locale/lang/kz.js
  41. 3 0
      src/locale/lang/lt.js
  42. 3 0
      src/locale/lang/lv.js
  43. 3 0
      src/locale/lang/mn.js
  44. 3 0
      src/locale/lang/nb-NO.js
  45. 3 0
      src/locale/lang/nl.js
  46. 3 0
      src/locale/lang/pl.js
  47. 3 0
      src/locale/lang/pt-br.js
  48. 3 0
      src/locale/lang/pt.js
  49. 3 0
      src/locale/lang/ro.js
  50. 3 0
      src/locale/lang/ru-RU.js
  51. 3 0
      src/locale/lang/sk.js
  52. 3 0
      src/locale/lang/sl.js
  53. 3 0
      src/locale/lang/sr.js
  54. 3 0
      src/locale/lang/sv-SE.js
  55. 3 0
      src/locale/lang/ta.js
  56. 3 0
      src/locale/lang/th.js
  57. 3 0
      src/locale/lang/tk.js
  58. 3 0
      src/locale/lang/tr-TR.js
  59. 3 0
      src/locale/lang/ua.js
  60. 3 0
      src/locale/lang/ug-CN.js
  61. 3 0
      src/locale/lang/vi.js
  62. 3 0
      src/locale/lang/zh-CN.js
  63. 3 0
      src/locale/lang/zh-TW.js
  64. 53 0
      src/utils/dom.js
  65. 8 0
      src/utils/types.js
  66. 3 0
      test/unit/specs/image.spec.js
  67. 34 0
      types/image.d.ts

+ 2 - 1
components.json

@@ -70,5 +70,6 @@
   "footer": "./packages/footer/index.js",
   "timeline": "./packages/timeline/index.js",
   "timeline-item": "./packages/timeline-item/index.js",
-  "divider": "./packages/divider/index.js"
+  "divider": "./packages/divider/index.js",
+  "image": "./packages/image/index.js"
 }

+ 78 - 0
examples/demo-styles/image.scss

@@ -0,0 +1,78 @@
+@keyframes dot {
+  0% { width: 0; margin-right: 1em; }
+  100% { width: 1em; margin-right: 0; }
+}
+
+.demo-image {
+  .block {
+    padding: 30px 0;
+    text-align: center;
+    border-right: solid 1px #eff2f6;
+    display: inline-block;
+    width: 20%;
+    box-sizing: border-box;
+    vertical-align: top;
+    &:last-child {
+      border-right: none;
+    }
+  }
+
+  .demonstration {
+    display: block;
+    color: #8492a6;
+    font-size: 14px;
+    margin-bottom: 20px;
+  }
+}
+
+.demo-image__placeholder, .demo-image__error {
+  @extend .demo-image;
+
+  .block {
+    width: 49%;
+  }
+
+  .el-image {
+    width: 300px;
+    height: 200px;
+  }
+
+  .image-slot {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 100%;
+    height: 100%;
+    background: #f5f7fa;
+    color: #909399;
+    font-size: 14px;
+  }
+}
+
+.demo-image__placeholder {
+  .dot {
+    animation: dot 2s infinite steps(3, start);
+    overflow: hidden;
+  }
+}
+
+.demo-image__error {
+  .image-slot {
+    font-size: 30px;
+  }
+}
+
+.demo-image__lazy {
+  height: 400px;
+  overflow-y: auto;
+
+  .el-image {
+    display: block;
+    min-height: 200px;
+    margin-bottom: 10px;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+}

+ 1 - 0
examples/demo-styles/index.scss

@@ -39,3 +39,4 @@
 @import "./typography.scss";
 @import "./upload.scss";
 @import "./divider.scss";
+@import "./image.scss";

+ 132 - 0
examples/docs/en-US/image.md

@@ -0,0 +1,132 @@
+## Image
+Besides the native features of img, support lazy load, custom placeholder and load failure, etc.
+
+### Basic Usage
+
+:::demo Indicate how the image should be resized to fit its container by `fit`, same as native [object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit)。
+```html
+<div class="demo-image">
+  <div class="block" v-for="fit in fits" :key="fit">
+    <span class="demonstration">{{ fit }}</span>
+    <el-image
+      style="width: 100px; height: 100px"
+      :src="url"
+      :fit="fit"></el-image>
+  </div>
+</div>
+
+<script>
+  export default {
+    data() {
+      return {
+        fits: ['fill', 'contain', 'cover', 'none', 'scale-down'],
+        url: 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg'
+      }
+    }
+  }
+</script>
+```
+:::
+
+### Placeholder
+
+:::demo Custom placeholder content when image hasn't loaded yet by `slot = placeholder`
+```html
+<div class="demo-image__placeholder">
+  <div class="block">
+    <span class="demonstration">Default</span>
+    <el-image :src="src"></el-image>
+  </div>
+  <div class="block">
+    <span class="demonstration">Custom</span>
+    <el-image :src="src">
+      <div slot="placeholder" class="image-slot">
+        Loading<span class="dot">...</span>
+      </div>
+    </el-image>
+  </div>
+</div>
+
+<script>
+  export default {
+    data() {
+      return {
+        src: 'https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg'
+      }
+    }
+  }
+</script>
+```
+:::
+
+### Load Failed
+
+:::demo Custom failed content when error occurs to image load by `slot = error`
+```html
+<div class="demo-image__error">
+  <div class="block">
+    <span class="demonstration">Default</span>
+    <el-image></el-image>
+  </div>
+  <div class="block">
+    <span class="demonstration">Custom</span>
+    <el-image>
+      <div slot="error" class="image-slot">
+        <i class="el-icon-picture-outline"></i>
+      </div>
+    </el-image>
+  </div>
+</div>
+```
+:::
+
+### Lazy Load
+
+:::demo Use lazy load by `lazy = true`. Image will load until scroll into view when set. You can indicate scroll container that adds scroll listener to by `scroll-container`. If undefined, will be the nearest parent container whose overflow property is auto or scroll.
+```html
+<div class="demo-image__lazy">
+  <el-image v-for="url in urls" :key="url" :src="url" lazy></el-image>
+</div>
+
+<script>
+  export default {
+    data() {
+      return {
+        urls: [
+          'https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg',
+          'https://fuss10.elemecdn.com/1/34/19aa98b1fcb2781c4fba33d850549jpeg.jpeg',
+          'https://fuss10.elemecdn.com/0/6f/e35ff375812e6b0020b6b4e8f9583jpeg.jpeg',
+          'https://fuss10.elemecdn.com/9/bb/e27858e973f5d7d3904835f46abbdjpeg.jpeg',
+          'https://fuss10.elemecdn.com/d/e6/c4d93a3805b3ce3f323f7974e6f78jpeg.jpeg',
+          'https://fuss10.elemecdn.com/3/28/bbf893f792f03a54408b3b7a7ebf0jpeg.jpeg',
+          'https://fuss10.elemecdn.com/2/11/6535bcfb26e4c79b48ddde44f4b6fjpeg.jpeg'
+        ]
+      }
+    }
+  }
+</script>
+```
+:::
+
+### Attributes
+| Attribute | Description | Type  | Accepted values | Default   |
+|---------- |-------- |---------- |-------------  |-------- |
+| src | Image source, same as native | string | — | - |
+| fit | Indicate how the image should be resized to fit its container, same as [object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) | fill / contain / cover / none / scale-down | — | - |
+| alt | Native alt | string | - | - |
+| lazy | Whether to use lazy load | boolean | — | false |
+| scroll-container | The container to add scroll listener when using lazy load | string / HTMLElement | — | The nearest parent container whose overflow property is auto or scroll |
+
+### Events
+| Event Name | Description | Parameters |
+|---------- |-------- |---------- |
+| load | Same as native load | (e: Event) |
+| error | Same as native error | (e: Error) |
+
+### Slots
+| Slot Name | Description |
+|---------|-------------|
+| placeholder | Triggers when image load |
+| error | Triggers when image load failed |
+
+

+ 132 - 0
examples/docs/es/image.md

@@ -0,0 +1,132 @@
+## Image
+Besides the native features of img, support lazy load, custom placeholder and load failure, etc.
+
+### Basic Usage
+
+:::demo Indicate how the image should be resized to fit its container by `fit`, same as native [object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit)。
+```html
+<div class="demo-image">
+  <div class="block" v-for="fit in fits" :key="fit">
+    <span class="demonstration">{{ fit }}</span>
+    <el-image
+      style="width: 100px; height: 100px"
+      :src="url"
+      :fit="fit"></el-image>
+  </div>
+</div>
+
+<script>
+  export default {
+    data() {
+      return {
+        fits: ['fill', 'contain', 'cover', 'none', 'scale-down'],
+        url: 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg'
+      }
+    }
+  }
+</script>
+```
+:::
+
+### Placeholder
+
+:::demo Custom placeholder content when image hasn't loaded yet by `slot = placeholder`
+```html
+<div class="demo-image__placeholder">
+  <div class="block">
+    <span class="demonstration">Default</span>
+    <el-image :src="src"></el-image>
+  </div>
+  <div class="block">
+    <span class="demonstration">Custom</span>
+    <el-image :src="src">
+      <div slot="placeholder" class="image-slot">
+        Loading<span class="dot">...</span>
+      </div>
+    </el-image>
+  </div>
+</div>
+
+<script>
+  export default {
+    data() {
+      return {
+        src: 'https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg'
+      }
+    }
+  }
+</script>
+```
+:::
+
+### Load Failed
+
+:::demo Custom failed content when error occurs to image load by `slot = error`
+```html
+<div class="demo-image__error">
+  <div class="block">
+    <span class="demonstration">Default</span>
+    <el-image></el-image>
+  </div>
+  <div class="block">
+    <span class="demonstration">Custom</span>
+    <el-image>
+      <div slot="error" class="image-slot">
+        <i class="el-icon-picture-outline"></i>
+      </div>
+    </el-image>
+  </div>
+</div>
+```
+:::
+
+### Lazy Load
+
+:::demo Use lazy load by `lazy = true`. Image will load until scroll into view when set. You can indicate scroll container that adds scroll listener to by `scroll-container`. If undefined, will be the nearest parent container whose overflow property is auto or scroll.
+```html
+<div class="demo-image__lazy">
+  <el-image v-for="url in urls" :key="url" :src="url" lazy></el-image>
+</div>
+
+<script>
+  export default {
+    data() {
+      return {
+        urls: [
+          'https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg',
+          'https://fuss10.elemecdn.com/1/34/19aa98b1fcb2781c4fba33d850549jpeg.jpeg',
+          'https://fuss10.elemecdn.com/0/6f/e35ff375812e6b0020b6b4e8f9583jpeg.jpeg',
+          'https://fuss10.elemecdn.com/9/bb/e27858e973f5d7d3904835f46abbdjpeg.jpeg',
+          'https://fuss10.elemecdn.com/d/e6/c4d93a3805b3ce3f323f7974e6f78jpeg.jpeg',
+          'https://fuss10.elemecdn.com/3/28/bbf893f792f03a54408b3b7a7ebf0jpeg.jpeg',
+          'https://fuss10.elemecdn.com/2/11/6535bcfb26e4c79b48ddde44f4b6fjpeg.jpeg'
+        ]
+      }
+    }
+  }
+</script>
+```
+:::
+
+### Attributes
+| Attribute | Description | Type  | Accepted values | Default   |
+|---------- |-------- |---------- |-------------  |-------- |
+| src | Image source, same as native | string | — | - |
+| fit | Indicate how the image should be resized to fit its container, same as [object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) | fill / contain / cover / none / scale-down | — | - |
+| alt | Native alt | string | - | - |
+| lazy | Whether to use lazy load | boolean | — | false |
+| scroll-container | The container to add scroll listener when using lazy load | string / HTMLElement | — | The nearest parent container whose overflow property is auto or scroll |
+
+### Events
+| Event Name | Description | Parameters |
+|---------- |-------- |---------- |
+| load | Same as native load | (e: Event) |
+| error | Same as native error | (e: Error) |
+
+### Slots
+| Slot Name | Description |
+|---------|-------------|
+| placeholder | Triggers when image load |
+| error | Triggers when image load failed |
+
+

+ 132 - 0
examples/docs/fr-FR/image.md

@@ -0,0 +1,132 @@
+## Image
+Besides the native features of img, support lazy load, custom placeholder and load failure, etc.
+
+### Basic Usage
+
+:::demo Indicate how the image should be resized to fit its container by `fit`, same as native [object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit)。
+```html
+<div class="demo-image">
+  <div class="block" v-for="fit in fits" :key="fit">
+    <span class="demonstration">{{ fit }}</span>
+    <el-image
+      style="width: 100px; height: 100px"
+      :src="url"
+      :fit="fit"></el-image>
+  </div>
+</div>
+
+<script>
+  export default {
+    data() {
+      return {
+        fits: ['fill', 'contain', 'cover', 'none', 'scale-down'],
+        url: 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg'
+      }
+    }
+  }
+</script>
+```
+:::
+
+### Placeholder
+
+:::demo Custom placeholder content when image hasn't loaded yet by `slot = placeholder`
+```html
+<div class="demo-image__placeholder">
+  <div class="block">
+    <span class="demonstration">Default</span>
+    <el-image :src="src"></el-image>
+  </div>
+  <div class="block">
+    <span class="demonstration">Custom</span>
+    <el-image :src="src">
+      <div slot="placeholder" class="image-slot">
+        Loading<span class="dot">...</span>
+      </div>
+    </el-image>
+  </div>
+</div>
+
+<script>
+  export default {
+    data() {
+      return {
+        src: 'https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg'
+      }
+    }
+  }
+</script>
+```
+:::
+
+### Load Failed
+
+:::demo Custom failed content when error occurs to image load by `slot = error`
+```html
+<div class="demo-image__error">
+  <div class="block">
+    <span class="demonstration">Default</span>
+    <el-image></el-image>
+  </div>
+  <div class="block">
+    <span class="demonstration">Custom</span>
+    <el-image>
+      <div slot="error" class="image-slot">
+        <i class="el-icon-picture-outline"></i>
+      </div>
+    </el-image>
+  </div>
+</div>
+```
+:::
+
+### Lazy Load
+
+:::demo Use lazy load by `lazy = true`. Image will load until scroll into view when set. You can indicate scroll container that adds scroll listener to by `scroll-container`. If undefined, will be the nearest parent container whose overflow property is auto or scroll.
+```html
+<div class="demo-image__lazy">
+  <el-image v-for="url in urls" :key="url" :src="url" lazy></el-image>
+</div>
+
+<script>
+  export default {
+    data() {
+      return {
+        urls: [
+          'https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg',
+          'https://fuss10.elemecdn.com/1/34/19aa98b1fcb2781c4fba33d850549jpeg.jpeg',
+          'https://fuss10.elemecdn.com/0/6f/e35ff375812e6b0020b6b4e8f9583jpeg.jpeg',
+          'https://fuss10.elemecdn.com/9/bb/e27858e973f5d7d3904835f46abbdjpeg.jpeg',
+          'https://fuss10.elemecdn.com/d/e6/c4d93a3805b3ce3f323f7974e6f78jpeg.jpeg',
+          'https://fuss10.elemecdn.com/3/28/bbf893f792f03a54408b3b7a7ebf0jpeg.jpeg',
+          'https://fuss10.elemecdn.com/2/11/6535bcfb26e4c79b48ddde44f4b6fjpeg.jpeg'
+        ]
+      }
+    }
+  }
+</script>
+```
+:::
+
+### Attributes
+| Attribute | Description | Type  | Accepted values | Default   |
+|---------- |-------- |---------- |-------------  |-------- |
+| src | Image source, same as native | string | — | - |
+| fit | Indicate how the image should be resized to fit its container, same as [object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) | fill / contain / cover / none / scale-down | — | - |
+| alt | Native alt | string | - | - |
+| lazy | Whether to use lazy load | boolean | — | false |
+| scroll-container | The container to add scroll listener when using lazy load | string / HTMLElement | — | The nearest parent container whose overflow property is auto or scroll |
+
+### Events
+| Event Name | Description | Parameters |
+|---------- |-------- |---------- |
+| load | Same as native load | (e: Event) |
+| error | Same as native error | (e: Error) |
+
+### Slots
+| Slot Name | Description |
+|---------|-------------|
+| placeholder | Triggers when image load |
+| error | Triggers when image load failed |
+
+

+ 132 - 0
examples/docs/zh-CN/image.md

@@ -0,0 +1,132 @@
+## Image 图片
+图片容器,在保留原生img的特性下,支持懒加载,自定义占位、加载失败等
+
+### 基础用法
+
+:::demo 可通过`fit`确定图片如何适应到容器框,同原生 [object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit)。
+```html
+<div class="demo-image">
+  <div class="block" v-for="fit in fits" :key="fit">
+    <span class="demonstration">{{ fit }}</span>
+    <el-image
+      style="width: 100px; height: 100px"
+      :src="url"
+      :fit="fit"></el-image>
+  </div>
+</div>
+
+<script>
+  export default {
+    data() {
+      return {
+        fits: ['fill', 'contain', 'cover', 'none', 'scale-down'],
+        url: 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg'
+      }
+    }
+  }
+</script>
+```
+:::
+
+### 占位内容
+
+:::demo 可通过`slot = placeholder`可自定义占位内容
+```html
+<div class="demo-image__placeholder">
+  <div class="block">
+    <span class="demonstration">默认</span>
+    <el-image :src="src"></el-image>
+  </div>
+  <div class="block">
+    <span class="demonstration">自定义</span>
+    <el-image :src="src">
+      <div slot="placeholder" class="image-slot">
+        加载中<span class="dot">...</span>
+      </div>
+    </el-image>
+  </div>
+</div>
+
+<script>
+  export default {
+    data() {
+      return {
+        src: 'https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg'
+      }
+    }
+  }
+</script>
+```
+:::
+
+### 加载失败
+
+:::demo 可通过`slot = error`可自定义加载失败内容
+```html
+<div class="demo-image__error">
+  <div class="block">
+    <span class="demonstration">默认</span>
+    <el-image></el-image>
+  </div>
+  <div class="block">
+    <span class="demonstration">自定义</span>
+    <el-image>
+      <div slot="error" class="image-slot">
+        <i class="el-icon-picture-outline"></i>
+      </div>
+    </el-image>
+  </div>
+</div>
+```
+:::
+
+### 懒加载
+
+:::demo 可通过`lazy`开启懒加载功能,当图片滚动到可视范围内才会加载。可通过`scroll-container`来设置滚动容器,若未定义,则为最近一个`overflow`值为`auto`或`scroll`的父元素。
+```html
+<div class="demo-image__lazy">
+  <el-image v-for="url in urls" :key="url" :src="url" lazy></el-image>
+</div>
+
+<script>
+  export default {
+    data() {
+      return {
+        urls: [
+          'https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg',
+          'https://fuss10.elemecdn.com/1/34/19aa98b1fcb2781c4fba33d850549jpeg.jpeg',
+          'https://fuss10.elemecdn.com/0/6f/e35ff375812e6b0020b6b4e8f9583jpeg.jpeg',
+          'https://fuss10.elemecdn.com/9/bb/e27858e973f5d7d3904835f46abbdjpeg.jpeg',
+          'https://fuss10.elemecdn.com/d/e6/c4d93a3805b3ce3f323f7974e6f78jpeg.jpeg',
+          'https://fuss10.elemecdn.com/3/28/bbf893f792f03a54408b3b7a7ebf0jpeg.jpeg',
+          'https://fuss10.elemecdn.com/2/11/6535bcfb26e4c79b48ddde44f4b6fjpeg.jpeg'
+        ]
+      }
+    }
+  }
+</script>
+```
+:::
+
+### Attributes
+| 参数      | 说明    | 类型      | 可选值       | 默认值   |
+|---------- |-------- |---------- |-------------  |-------- |
+| src | 图片源,同原生 | string | — | - |
+| fit | 确定图片如何适应容器框,同原生 [object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) | fill / contain / cover / none / scale-down | — | - |
+| alt | 原生 alt | string | - | - |
+| lazy | 是否开启懒加载 | boolean | — | false |
+| scroll-container | 开启懒加载后,监听 scroll 事件的容器 | string / HTMLElement | — | 最近一个 overflow 值为 auto 或 scroll 的父元素 |
+
+### Events
+| 事件名称      | 说明    | 回调参数      |
+|---------- |-------- |---------- |
+| load | 图片加载成功触发 | (e: Event) |
+| error | 图片加载失败触发 | (e: Error) |
+
+### Slots
+| 名称    | 说明         |
+|---------|-------------|
+| placeholder | 图片未加载的占位内容 |
+| error | 加载失败的内容 |
+
+

+ 16 - 0
examples/nav.config.json

@@ -255,6 +255,10 @@
             {
               "path": "/divider",
               "title": "Divider 分割线"
+            },
+            {
+              "path": "/image",
+              "title": "Image"
             }
           ]
         }
@@ -517,6 +521,10 @@
             {
               "path": "/divider",
               "title": "Divider"
+            },
+            {
+              "path": "/image",
+              "title": "Image"
             }
           ]
         }
@@ -779,6 +787,10 @@
             {
               "path": "/divider",
               "title": "Divider"
+            },
+            {
+              "path": "/image",
+              "title": "Image"
             }
           ]
         }
@@ -1041,6 +1053,10 @@
             {
               "path": "/divider",
               "title": "Divider"
+            },
+            {
+              "path": "/image",
+              "title": "Image 图片"
             }
           ]
         }

+ 8 - 0
packages/image/index.js

@@ -0,0 +1,8 @@
+import Image from './src/main';
+
+/* istanbul ignore next */
+Image.install = function(Vue) {
+  Vue.component(Image.name, Image);
+};
+
+export default Image;

+ 123 - 0
packages/image/src/main.vue

@@ -0,0 +1,123 @@
+<template>
+  <div class="el-image">
+    <slot v-if="loading" name="placeholder">
+      <div class="el-image__placeholder"></div>
+    </slot>
+    <slot v-else-if="error" name="error">
+      <div class="el-image__error">{{ t('el.image.error') }}</div>
+    </slot>
+    <img
+      v-else
+      class="el-image__inner"
+      :src="src"
+      :alt="alt"
+      :style="{ 'object-fit': fit }">
+  </div>
+</template>
+
+<script>
+  import Locale from 'element-ui/src/mixins/locale';
+  import { on, off, getScrollContainer, isInContainer } from 'element-ui/src/utils/dom';
+  import { isString, isHtmlElement } from 'element-ui/src/utils/types';
+  import throttle from 'throttle-debounce/throttle';
+
+  export default {
+    name: 'ElImage',
+
+    mixins: [Locale],
+
+    props: {
+      src: String,
+      fit: String,
+      lazy: Boolean,
+      scrollContainer: [String, HTMLElement],
+      alt: String
+    },
+
+    data() {
+      return {
+        loading: true,
+        error: false,
+        show: !this.lazy
+      };
+    },
+
+    watch: {
+      src: {
+        handler(val) {
+          this.show && this.loadImage(val);
+        },
+        immediate: true
+      },
+      show(val) {
+        val && this.loadImage(this.src);
+      }
+    },
+
+    mounted() {
+      this.lazy && this.addLazyLoadListener();
+    },
+
+    beforeDestroy() {
+      this.lazy && this.removeLazyLoadListener();
+    },
+
+    methods: {
+      loadImage(val) {
+        // reset status
+        this.loading = true;
+        this.error = false;
+
+        const img = new Image();
+        img.onload = this.handleLoad.bind(this);
+        img.onerror = this.handleError.bind(this);
+        img.src = val;
+      },
+      handleLoad(e) {
+        this.loading = false;
+        this.$emit('load', e);
+      },
+      handleError(e) {
+        this.loading = false;
+        this.error = true;
+        this.$emit('error', e);
+      },
+      handleLazyLoad() {
+        if (isInContainer(this.$el, this._scrollContainer)) {
+          this.show = true;
+          this.removeLazyLoadListener();
+        }
+      },
+      addLazyLoadListener() {
+        if (this.$isServer) return;
+
+        const { scrollContainer } = this;
+        let _scrollContainer = null;
+
+        if (isHtmlElement(scrollContainer)) {
+          _scrollContainer = scrollContainer;
+        } else if (isString(scrollContainer)) {
+          _scrollContainer = document.querySelector(scrollContainer);
+        } else {
+          _scrollContainer = getScrollContainer(this.$el);
+        }
+
+        if (_scrollContainer) {
+          this._scrollContainer = _scrollContainer;
+          this._lazyLoadHandler = throttle(200, this.handleLazyLoad);
+          on(_scrollContainer, 'scroll', this._lazyLoadHandler);
+          this.handleLazyLoad();
+        }
+      },
+      removeLazyLoadListener() {
+        const { _scrollContainer, _lazyLoadHandler } = this;
+
+        if (this.$isServer || !_scrollContainer || !_lazyLoadHandler) return;
+
+        off(_scrollContainer, 'scroll', _lazyLoadHandler);
+        this._scrollContainer = null;
+        this._lazyLoadHandler = null;
+      }
+    }
+  };
+</script>

+ 32 - 0
packages/theme-chalk/src/image.scss

@@ -0,0 +1,32 @@
+@import "mixins/mixins";
+@import "common/var";
+
+%size {
+  width: 100%;
+  height: 100%;
+}
+
+@include b(image) {
+  display: inline-block;
+
+  @include e(inner) {
+    @extend %size;
+    vertical-align: top;
+  }
+
+  @include e(placeholder) {
+    @extend %size;
+    background: $--background-color-base;
+  }
+
+  @include e(error) {
+    @extend %size;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    font-size: 14px;
+    background: $--background-color-base;
+    color: $--color-text-placeholder;
+    vertical-align: middle;
+  }
+}

+ 1 - 0
packages/theme-chalk/src/index.scss

@@ -68,3 +68,4 @@
 @import "./timeline.scss";
 @import "./timeline-item.scss";
 @import "./divider.scss";
+@import "./image.scss";

+ 4 - 1
src/index.js

@@ -72,6 +72,7 @@ import Footer from '../packages/footer/index.js';
 import Timeline from '../packages/timeline/index.js';
 import TimelineItem from '../packages/timeline-item/index.js';
 import Divider from '../packages/divider/index.js';
+import Image from '../packages/image/index.js';
 import locale from 'element-ui/src/locale';
 import CollapseTransition from 'element-ui/src/transitions/collapse-transition';
 
@@ -144,6 +145,7 @@ const components = [
   Timeline,
   TimelineItem,
   Divider,
+  Image,
   CollapseTransition
 ];
 
@@ -254,5 +256,6 @@ export default {
   Footer,
   Timeline,
   TimelineItem,
-  Divider
+  Divider,
+  Image
 };

+ 3 - 0
src/locale/lang/af-ZA.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Voer sleutelwoord in',
       noCheckedFormat: '{total} items',
       hasCheckedFormat: '{checked}/{total} gekies'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/ar.js

@@ -103,6 +103,9 @@ export default {
       filterPlaceholder: 'ادخل كلمة',
       noCheckedFormat: '{total} عناصر',
       hasCheckedFormat: '{checked}/{total} مختار'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/bg.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Enter keyword', // to be translated
       noCheckedFormat: '{total} items', // to be translated
       hasCheckedFormat: '{checked}/{total} checked' // to be translated
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/ca.js

@@ -103,6 +103,9 @@ export default {
       filterPlaceholder: 'Introdueix la paraula clau',
       noCheckedFormat: '{total} ítems',
       hasCheckedFormat: '{checked}/{total} seleccionats'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/cs-CZ.js

@@ -106,6 +106,9 @@ export default {
       filterPlaceholder: 'Klíčové slovo',
       noCheckedFormat: '{total} položek',
       hasCheckedFormat: '{checked}/{total} vybráno'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/da.js

@@ -103,6 +103,9 @@ export default {
       filterPlaceholder: 'Indtast søgeord',
       noCheckedFormat: '{total} emner',
       hasCheckedFormat: '{checked}/{total} valgt'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/de.js

@@ -105,6 +105,9 @@ export default {
       filterPlaceholder: 'Einträge filtern',
       noCheckedFormat: '{total} Einträge',
       hasCheckedFormat: '{checked}/{total} ausgewählt'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/ee.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Sisesta märksõna',
       noCheckedFormat: '{total} objekti',
       hasCheckedFormat: '{checked}/{total} valitud'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/el.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Αναζήτηση',
       noCheckedFormat: '{total} Αντικείμενα',
       hasCheckedFormat: '{checked}/{total} επιλεγμένα'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/en.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Enter keyword', // to be translated
       noCheckedFormat: '{total} items', // to be translated
       hasCheckedFormat: '{checked}/{total} checked' // to be translated
+    },
+    image: {
+      error: 'FAILED'
     }
   }
 };

+ 3 - 0
src/locale/lang/es.js

@@ -103,6 +103,9 @@ export default {
       filterPlaceholder: 'Ingresar palabra clave',
       noCheckedFormat: '{total} artículos',
       hasCheckedFormat: '{checked}/{total} revisados'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/eu.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Sartu gako-hitza', // to be translated
       noCheckedFormat: '{total} elementu', // to be translated
       hasCheckedFormat: '{checked}/{total} hautatuta' // to be translated
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/fa.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'کلید واژه هارو وارد کن',
       noCheckedFormat: '{total} مورد',
       hasCheckedFormat: '{checked} مورد از {total} مورد انتخاب شده است'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/fi.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Syötä hakusana',
       noCheckedFormat: '{total} kohdetta',
       hasCheckedFormat: '{checked}/{total} valittu'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/fr.js

@@ -103,6 +103,9 @@ export default {
       filterPlaceholder: 'Entrer un mot clef',
       noCheckedFormat: '{total} elements',
       hasCheckedFormat: '{checked}/{total} coché(s)'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/he.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'הקלד',
       noCheckedFormat: 'פריטים {total}',
       hasCheckedFormat: ' אישור {checked}/{total}'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/hr.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Unesite ključnu riječ', // to be translated
       noCheckedFormat: '{total} stavki', // to be translated
       hasCheckedFormat: '{checked}/{total} checked' // to be translated
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/hu.js

@@ -103,6 +103,9 @@ export default {
       filterPlaceholder: 'Kulcsszó',
       noCheckedFormat: '{total} elem',
       hasCheckedFormat: '{checked}/{total} kiválasztva'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/hy-AM.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Մուտքագրեք բանալի բառ',
       noCheckedFormat: '{total} միաւոր',
       hasCheckedFormat: '{checked}/{total} ընտրուած է'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/id.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Masukan kata kunci',
       noCheckedFormat: '{total} butir',
       hasCheckedFormat: '{checked}/{total} terpilih'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/it.js

@@ -103,6 +103,9 @@ export default {
       filterPlaceholder: 'Inserisci filtro',
       noCheckedFormat: '{total} elementi',
       hasCheckedFormat: '{checked}/{total} selezionati'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/ja.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'キーワードを入力',
       noCheckedFormat: '総計 {total} 件',
       hasCheckedFormat: '{checked}/{total} を選択した'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/kg.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Сураныч, издөө кирет',
       noCheckedFormat: 'бүтүндөй {total} сан',
       hasCheckedFormat: 'Тандалган {checked}/{total} сан'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/km.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'បញ្ចូលពាក្យ',
       noCheckedFormat: '{total} ធាតុ',
       hasCheckedFormat: '{checked}/{total} បានគូសធីក'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/ko.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: ' 입력하세요',
       noCheckedFormat: '{total} 항목',
       hasCheckedFormat: '{checked}/{total} 선택됨'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/ku.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Binivîse',
       noCheckedFormat: '{total} lib',
       hasCheckedFormat: '{checked}/{total} bijartin'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/kz.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Кілт сөзді енгізіңіз',
       noCheckedFormat: '{total} элэмэнт',
       hasCheckedFormat: '{checked}/{total} құсбелгісі қойылды'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/lt.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Įvesk raktažodį',
       noCheckedFormat: 'Viso: {total}',
       hasCheckedFormat: 'Pažymėta {checked} iš {total}'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/lv.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Ievadīt atslēgvārdu',
       noCheckedFormat: '{total} vienības',
       hasCheckedFormat: '{checked}/{total} atzīmēti'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/mn.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Утга оруул',
       noCheckedFormat: '{total} өгөгдөл',
       hasCheckedFormat: '{checked}/{total} сонгосон'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/nb-NO.js

@@ -103,6 +103,9 @@ export default {
       filterPlaceholder: 'Enter keyword', // to be translated
       noCheckedFormat: '{total} items', // to be translated
       hasCheckedFormat: '{checked}/{total} checked' // to be translated
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/nl.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Geef zoekwoerd',
       noCheckedFormat: '{total} items',
       hasCheckedFormat: '{checked}/{total} geselecteerd'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/pl.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Wpisz szukaną frazę',
       noCheckedFormat: 'razem: {total}',
       hasCheckedFormat: 'wybranych: {checked}/{total}'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/pt-br.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Digite uma palavra-chave',
       noCheckedFormat: '{total} itens',
       hasCheckedFormat: '{checked}/{total} selecionados'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/pt.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Enter keyword', // to be translated
       noCheckedFormat: '{total} items', // to be translated
       hasCheckedFormat: '{checked}/{total} checked' // to be translated
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/ro.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Introduceți cuvântul cheie',
       noCheckedFormat: '{total} elemente',
       hasCheckedFormat: '{checked}/{total} verificate'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/ru-RU.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Введите ключевое слово',
       noCheckedFormat: '{total} пунктов',
       hasCheckedFormat: '{checked}/{total} выбрано'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/sk.js

@@ -106,6 +106,9 @@ export default {
       filterPlaceholder: 'Filtrovať podľa',
       noCheckedFormat: '{total} položiek',
       hasCheckedFormat: '{checked}/{total} označených'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/sl.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Vnesi ključno besedo',
       noCheckedFormat: '{total} elementov',
       hasCheckedFormat: '{checked}/{total} izbranih'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/sr.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Унеси кључну реч', // to be translated
       noCheckedFormat: '{total} ставки', // to be translated
       hasCheckedFormat: '{checked}/{total} обележених' // to be translated
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/sv-SE.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Enter keyword', // to be translated
       noCheckedFormat: '{total} items', // to be translated
       hasCheckedFormat: '{checked}/{total} checked' // to be translated
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/ta.js

@@ -103,6 +103,9 @@ export default {
       filterPlaceholder: 'சொல்லை உள்ளீடு செய்',
       noCheckedFormat: '{total} items', // to be translated
       hasCheckedFormat: '{checked}/{total} தேர்வு செய்யப்பட்டவைகள்'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/th.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Enter keyword', // to be translated
       noCheckedFormat: '{total} items', // to be translated
       hasCheckedFormat: '{checked}/{total} checked' // to be translated
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/tk.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Gözleg sözlerini giriziň',
       noCheckedFormat: '{total} sany',
       hasCheckedFormat: '{checked}/{total} saýlanan'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/tr-TR.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Anahtar kelimeleri gir',
       noCheckedFormat: '{total} adet',
       hasCheckedFormat: '{checked}/{total} seçildi'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/ua.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Введіть ключове слово',
       noCheckedFormat: '{total} пунктів',
       hasCheckedFormat: '{checked}/{total} вибрано'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/ug-CN.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'ئىزدىمەكچى بولغان مەزمۇننى كىرگۈزۈڭ',
       noCheckedFormat: 'جەمئىي {total} تۈر',
       hasCheckedFormat: 'تاللانغىنى {checked}/{total} تۈر'
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/vi.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Nhập từ khóa',
       noCheckedFormat: '{total} mục',
       hasCheckedFormat: '{checked}/{total} đã chọn '
+    },
+    image: {
+      error: 'FAILED' // to be translated
     }
   }
 };

+ 3 - 0
src/locale/lang/zh-CN.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: '请输入搜索内容',
       noCheckedFormat: '共 {total} 项',
       hasCheckedFormat: '已选 {checked}/{total} 项'
+    },
+    image: {
+      error: '加载失败'
     }
   }
 };

+ 3 - 0
src/locale/lang/zh-TW.js

@@ -104,6 +104,9 @@ export default {
       filterPlaceholder: 'Enter keyword', // to be translated
       noCheckedFormat: '{total} items', // to be translated
       hasCheckedFormat: '{checked}/{total} checked' // to be translated
+    },
+    image: {
+      error: '加載失敗'
     }
   }
 };

+ 53 - 0
src/utils/dom.js

@@ -172,3 +172,56 @@ export function setStyle(element, styleName, value) {
     }
   }
 };
+
+export const isScroll = (el, vertical) => {
+  if (isServer) return;
+
+  const determinedDirection = vertical !== null || vertical !== undefined;
+  const overflow = determinedDirection
+    ? vertical
+      ? getStyle(el, 'overflow-y')
+      : getStyle(el, 'overflow-x')
+    : getStyle(el, 'overflow');
+
+  return overflow.match(/(scroll|auto)/);
+};
+
+export const getScrollContainer = (el, vertical) => {
+  if (isServer) return;
+
+  let parent = el;
+  while (parent) {
+    if ([window, document, document.documentElement].includes(parent)) {
+      return window;
+    }
+    if (isScroll(parent, vertical)) {
+      return parent;
+    }
+    parent = parent.parentNode;
+  }
+
+  return parent;
+};
+
+export const isInContainer = (el, container) => {
+  if (isServer || !el || !container) return false;
+
+  const elRect = el.getBoundingClientRect();
+  let containerRect;
+
+  if ([window, document, document.documentElement, null, undefined].includes(container)) {
+    containerRect = {
+      top: 0,
+      right: window.innerWidth,
+      bottom: window.innerHeight,
+      left: 0
+    };
+  } else {
+    containerRect = container.getBoundingClientRect();
+  }
+
+  return elRect.top < containerRect.bottom &&
+    elRect.bottom > containerRect.top &&
+    elRect.right > containerRect.left &&
+    elRect.left < containerRect.right;
+};

+ 8 - 0
src/utils/types.js

@@ -1,3 +1,11 @@
+export function isString(obj) {
+  return Object.prototype.toString.call(obj) === '[object String]';
+}
+
 export function isObject(obj) {
   return Object.prototype.toString.call(obj) === '[object Object]';
 }
+
+export function isHtmlElement(node) {
+  return node && node.nodeType === Node.ELEMENT_NODE;
+}

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 3 - 0
test/unit/specs/image.spec.js


+ 34 - 0
types/image.d.ts

@@ -0,0 +1,34 @@
+import { VNode } from 'vue'
+import { ElementUIComponent } from './component'
+
+export type ObjectFit = 'fill' | 'contain' | 'cover' | 'none' | 'scale-down'
+
+export interface ImageSlots {
+  /** Placeholder content when image hasn't loaded yet */
+  placeholder: VNode[]
+
+  /** Error content when error occurs to image load */
+  error: VNode[]
+
+  [key: string]: VNode[]
+}
+
+/** Image Component */
+export declare class ElImage extends ElementUIComponent {
+  /** Image source */
+  src: string
+
+  /** Indicate how the image should be resized to fit its container, same as native 'object-fit' */
+  fit: ObjectFit
+
+  /** Whether to use lazy load */
+  lazy: boolean
+
+  /** Scroll container that to add scroll listener when using lazy load */
+  scrollContainer: string | HTMLElement
+
+  /** Native 'alt' attribute */
+  alt: string
+
+  $slots: ImageSlots
+}

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov