mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-12-01 18:58:10 +08:00
feat(components): [image] support native lazy loading (#7968)
This commit is contained in:
parent
19aa8ca424
commit
60cd22b890
12
breakings/2.2.3/image.yml
Normal file
12
breakings/2.2.3/image.yml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
- scope: 'component'
|
||||||
|
name: 'el-image'
|
||||||
|
type: 'props'
|
||||||
|
version: '2.2.3'
|
||||||
|
commit_hash: '7a48556'
|
||||||
|
description: |
|
||||||
|
Per [HTMLImageElement.loading Request](https://github.com/element-plus/element-plus/issues/7841),
|
||||||
|
Add [native loading](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-loading) support for Image components.
|
||||||
|
props:
|
||||||
|
- api: 'loading'
|
||||||
|
before: ''
|
||||||
|
after: '"eager" | "lazy"'
|
@ -33,6 +33,14 @@ image/load-failed
|
|||||||
|
|
||||||
## Lazy Load
|
## Lazy Load
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
|
||||||
|
Native `loading` has been supported since <VersionTag version="2.2.3" />, you can use `loading = "lazy"` to replace `lazy = true`.
|
||||||
|
|
||||||
|
If the current browser supports native lazy loading, the native lazy loading will be used first, otherwise will be implemented through scroll.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
:::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.
|
:::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.
|
||||||
|
|
||||||
image/lazy-load
|
image/lazy-load
|
||||||
@ -51,20 +59,21 @@ image/image-preview
|
|||||||
|
|
||||||
### Image Attributes
|
### Image Attributes
|
||||||
|
|
||||||
| Name | Description | Type | Default |
|
| Attribute | Description | Type | Default |
|
||||||
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------- |
|
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||||
| `src` | image source, same as native. | `string` | — |
|
| `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'` | — |
|
| `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'` | — |
|
||||||
| `hide-on-click-modal` | when enabling preview, use this flag to control whether clicking on backdrop can exit preview mode. | `boolean` | `false` |
|
| `hide-on-click-modal` | when enabling preview, use this flag to control whether clicking on backdrop can exit preview mode. | `boolean` | `false` |
|
||||||
| `lazy` | whether to use lazy load. | `boolean` | `false` |
|
| `loading` <VersionTag version="2.2.3" /> | Indicates how the browser should load the image, same as [native](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#attr-loading) | `'eager' \| 'lazy'` | — |
|
||||||
| `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. |
|
| `lazy` | whether to use lazy load. | `boolean` | `false` |
|
||||||
| `alt` | native attribute `alt`. | `string` | — |
|
| `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. |
|
||||||
| `referrer-policy` | native attribute `referrerPolicy`. | `string` | — |
|
| `alt` | native attribute `alt`. | `string` | — |
|
||||||
| `preview-src-list` | allow big image preview. | `string[]` | — |
|
| `referrer-policy` | native attribute `referrerPolicy`. | `string` | — |
|
||||||
| `z-index` | set image preview z-index. | `number` | — |
|
| `preview-src-list` | allow big image preview. | `string[]` | — |
|
||||||
| `initial-index` | initial preview image index, less than the length of `url-list`. | `number` | `0` |
|
| `z-index` | set image preview z-index. | `number` | — |
|
||||||
| `close-on-press-escape` | whether the image-viewer can be closed by pressing ESC | `boolean` | `true` |
|
| `initial-index` | initial preview image index, less than the length of `url-list`. | `number` | `0` |
|
||||||
| `preview-teleported` | whether to append image-viewer to body. A nested parent element attribute transform should have this attribute set to `true`. | `boolean` | `false` |
|
| `close-on-press-escape` | whether the image-viewer can be closed by pressing ESC | `boolean` | `true` |
|
||||||
|
| `preview-teleported` | whether to append image-viewer to body. A nested parent element attribute transform should have this attribute set to `true`. | `boolean` | `false` |
|
||||||
|
|
||||||
### Image Events
|
### Image Events
|
||||||
|
|
||||||
|
@ -115,6 +115,22 @@ describe('Image.vue', () => {
|
|||||||
).not.toContain('display: none')
|
).not.toContain('display: none')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('native loading attributes', async () => {
|
||||||
|
const wrapper = mount(Image, {
|
||||||
|
props: {
|
||||||
|
src: IMAGE_SUCCESS,
|
||||||
|
loading: 'eager',
|
||||||
|
} as ElImageProps,
|
||||||
|
})
|
||||||
|
|
||||||
|
await doubleWait()
|
||||||
|
expect(wrapper.find('img').exists()).toBe(true)
|
||||||
|
expect(wrapper.find('img').attributes('loading')).toBe('eager')
|
||||||
|
|
||||||
|
await wrapper.setProps({ loading: undefined })
|
||||||
|
expect(wrapper.find('img').attributes('loading')).toBe(undefined)
|
||||||
|
})
|
||||||
|
|
||||||
test('$attrs', async () => {
|
test('$attrs', async () => {
|
||||||
const alt = 'this ia alt'
|
const alt = 'this ia alt'
|
||||||
const props: ElImageProps = {
|
const props: ElImageProps = {
|
||||||
|
@ -21,6 +21,10 @@ export const imageProps = buildProps({
|
|||||||
values: ['', 'contain', 'cover', 'fill', 'none', 'scale-down'],
|
values: ['', 'contain', 'cover', 'fill', 'none', 'scale-down'],
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
loading: {
|
||||||
|
type: String,
|
||||||
|
values: ['eager', 'lazy'],
|
||||||
|
},
|
||||||
lazy: {
|
lazy: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
@ -1,19 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="container" :class="[ns.b(), $attrs.class]" :style="containerStyle">
|
<div ref="container" :class="[ns.b(), $attrs.class]" :style="containerStyle">
|
||||||
<slot v-if="loading" name="placeholder">
|
<img
|
||||||
|
v-if="imageSrc !== undefined && !hasLoadError"
|
||||||
|
v-bind="attrs"
|
||||||
|
:src="imageSrc"
|
||||||
|
:loading="loading"
|
||||||
|
:style="imageStyle"
|
||||||
|
:class="[ns.e('inner'), preview ? ns.e('preview') : '']"
|
||||||
|
@click="clickHandler"
|
||||||
|
@load="handleLoad"
|
||||||
|
@error="handleError"
|
||||||
|
/>
|
||||||
|
<slot v-if="isLoading" name="placeholder">
|
||||||
<div :class="ns.e('placeholder')" />
|
<div :class="ns.e('placeholder')" />
|
||||||
</slot>
|
</slot>
|
||||||
<slot v-else-if="hasLoadError" name="error">
|
<slot v-else-if="hasLoadError" name="error">
|
||||||
<div :class="ns.e('error')">{{ t('el.image.error') }}</div>
|
<div :class="ns.e('error')">{{ t('el.image.error') }}</div>
|
||||||
</slot>
|
</slot>
|
||||||
<img
|
|
||||||
v-else
|
|
||||||
v-bind="attrs"
|
|
||||||
:src="src"
|
|
||||||
:style="imageStyle"
|
|
||||||
:class="[ns.e('inner'), preview ? ns.e('preview') : '']"
|
|
||||||
@click="clickHandler"
|
|
||||||
/>
|
|
||||||
<template v-if="preview">
|
<template v-if="preview">
|
||||||
<image-viewer
|
<image-viewer
|
||||||
v-if="showViewer"
|
v-if="showViewer"
|
||||||
@ -72,14 +75,14 @@ const ns = useNamespace('image')
|
|||||||
const rawAttrs = useRawAttrs()
|
const rawAttrs = useRawAttrs()
|
||||||
const attrs = useAttrs()
|
const attrs = useAttrs()
|
||||||
|
|
||||||
|
const imageSrc = ref<string | undefined>()
|
||||||
const hasLoadError = ref(false)
|
const hasLoadError = ref(false)
|
||||||
const loading = ref(true)
|
const isLoading = ref(true)
|
||||||
const imgWidth = ref(0)
|
|
||||||
const imgHeight = ref(0)
|
|
||||||
const showViewer = ref(false)
|
const showViewer = ref(false)
|
||||||
const container = ref<HTMLElement>()
|
const container = ref<HTMLElement>()
|
||||||
|
|
||||||
const _scrollContainer = ref<HTMLElement | Window>()
|
const _scrollContainer = ref<HTMLElement | Window>()
|
||||||
|
|
||||||
|
const supportLoading = isClient && 'loading' in HTMLImageElement.prototype
|
||||||
let stopScrollListener: (() => void) | undefined
|
let stopScrollListener: (() => void) | undefined
|
||||||
let stopWheelListener: (() => void) | undefined
|
let stopWheelListener: (() => void) | undefined
|
||||||
|
|
||||||
@ -107,49 +110,27 @@ const imageIndex = computed(() => {
|
|||||||
return previewIndex
|
return previewIndex
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isManual = computed(() => {
|
||||||
|
if (props.loading === 'eager') return false
|
||||||
|
return (!supportLoading && props.loading === 'lazy') || props.lazy
|
||||||
|
})
|
||||||
|
|
||||||
const loadImage = () => {
|
const loadImage = () => {
|
||||||
if (!isClient) return
|
if (!isClient) return
|
||||||
|
|
||||||
// reset status
|
// reset status
|
||||||
loading.value = true
|
isLoading.value = true
|
||||||
hasLoadError.value = false
|
hasLoadError.value = false
|
||||||
|
imageSrc.value = props.src
|
||||||
const img = new Image()
|
|
||||||
const currentImageSrc = props.src
|
|
||||||
|
|
||||||
// load & error callbacks are only responsible for currentImageSrc
|
|
||||||
img.addEventListener('load', (e) => {
|
|
||||||
if (currentImageSrc !== props.src) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
handleLoad(e, img)
|
|
||||||
})
|
|
||||||
img.addEventListener('error', (e) => {
|
|
||||||
if (currentImageSrc !== props.src) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
handleError(e)
|
|
||||||
})
|
|
||||||
|
|
||||||
// bind html attrs
|
|
||||||
// so it can behave consistently
|
|
||||||
Object.entries(rawAttrs).forEach(([key, value]) => {
|
|
||||||
// avoid onload to be overwritten
|
|
||||||
if (key.toLowerCase() === 'onload') return
|
|
||||||
img.setAttribute(key, value as string)
|
|
||||||
})
|
|
||||||
img.src = currentImageSrc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLoad(e: Event, img: HTMLImageElement) {
|
function handleLoad() {
|
||||||
imgWidth.value = img.width
|
isLoading.value = false
|
||||||
imgHeight.value = img.height
|
|
||||||
loading.value = false
|
|
||||||
hasLoadError.value = false
|
hasLoadError.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleError(event: Event) {
|
function handleError(event: Event) {
|
||||||
loading.value = false
|
isLoading.value = false
|
||||||
hasLoadError.value = true
|
hasLoadError.value = true
|
||||||
emit('error', event)
|
emit('error', event)
|
||||||
}
|
}
|
||||||
@ -235,9 +216,9 @@ function switchViewer(val: number) {
|
|||||||
watch(
|
watch(
|
||||||
() => props.src,
|
() => props.src,
|
||||||
() => {
|
() => {
|
||||||
if (props.lazy) {
|
if (isManual.value) {
|
||||||
// reset status
|
// reset status
|
||||||
loading.value = true
|
isLoading.value = true
|
||||||
hasLoadError.value = false
|
hasLoadError.value = false
|
||||||
removeLazyLoadListener()
|
removeLazyLoadListener()
|
||||||
addLazyLoadListener()
|
addLazyLoadListener()
|
||||||
@ -248,7 +229,7 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.lazy) {
|
if (isManual.value) {
|
||||||
addLazyLoadListener()
|
addLazyLoadListener()
|
||||||
} else {
|
} else {
|
||||||
loadImage()
|
loadImage()
|
||||||
|
@ -6,6 +6,12 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
%position {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@include b(image) {
|
@include b(image) {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@ -17,11 +23,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@include e(placeholder) {
|
@include e(placeholder) {
|
||||||
|
@extend %position !optional;
|
||||||
@extend %size !optional;
|
@extend %size !optional;
|
||||||
background: getCssVar('fill-color', 'light');
|
background: getCssVar('fill-color', 'light');
|
||||||
}
|
}
|
||||||
|
|
||||||
@include e(error) {
|
@include e(error) {
|
||||||
|
@extend %position !optional;
|
||||||
@extend %size !optional;
|
@extend %size !optional;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
Loading…
Reference in New Issue
Block a user