mirror of
https://gitee.com/element-plus/element-plus.git
synced 2024-11-29 17:58:08 +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
|
||||
|
||||
:::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.
|
||||
|
||||
image/lazy-load
|
||||
@ -51,11 +59,12 @@ image/image-preview
|
||||
|
||||
### Image Attributes
|
||||
|
||||
| Name | Description | Type | Default |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| Attribute | Description | Type | 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'` | — |
|
||||
| `hide-on-click-modal` | when enabling preview, use this flag to control whether clicking on backdrop can exit preview mode. | `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'` | — |
|
||||
| `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. |
|
||||
| `alt` | native attribute `alt`. | `string` | — |
|
||||
|
@ -115,6 +115,22 @@ describe('Image.vue', () => {
|
||||
).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 () => {
|
||||
const alt = 'this ia alt'
|
||||
const props: ElImageProps = {
|
||||
|
@ -21,6 +21,10 @@ export const imageProps = buildProps({
|
||||
values: ['', 'contain', 'cover', 'fill', 'none', 'scale-down'],
|
||||
default: '',
|
||||
},
|
||||
loading: {
|
||||
type: String,
|
||||
values: ['eager', 'lazy'],
|
||||
},
|
||||
lazy: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
|
@ -1,19 +1,22 @@
|
||||
<template>
|
||||
<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')" />
|
||||
</slot>
|
||||
<slot v-else-if="hasLoadError" name="error">
|
||||
<div :class="ns.e('error')">{{ t('el.image.error') }}</div>
|
||||
</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">
|
||||
<image-viewer
|
||||
v-if="showViewer"
|
||||
@ -72,14 +75,14 @@ const ns = useNamespace('image')
|
||||
const rawAttrs = useRawAttrs()
|
||||
const attrs = useAttrs()
|
||||
|
||||
const imageSrc = ref<string | undefined>()
|
||||
const hasLoadError = ref(false)
|
||||
const loading = ref(true)
|
||||
const imgWidth = ref(0)
|
||||
const imgHeight = ref(0)
|
||||
const isLoading = ref(true)
|
||||
const showViewer = ref(false)
|
||||
const container = ref<HTMLElement>()
|
||||
|
||||
const _scrollContainer = ref<HTMLElement | Window>()
|
||||
|
||||
const supportLoading = isClient && 'loading' in HTMLImageElement.prototype
|
||||
let stopScrollListener: (() => void) | undefined
|
||||
let stopWheelListener: (() => void) | undefined
|
||||
|
||||
@ -107,49 +110,27 @@ const imageIndex = computed(() => {
|
||||
return previewIndex
|
||||
})
|
||||
|
||||
const isManual = computed(() => {
|
||||
if (props.loading === 'eager') return false
|
||||
return (!supportLoading && props.loading === 'lazy') || props.lazy
|
||||
})
|
||||
|
||||
const loadImage = () => {
|
||||
if (!isClient) return
|
||||
|
||||
// reset status
|
||||
loading.value = true
|
||||
isLoading.value = true
|
||||
hasLoadError.value = false
|
||||
|
||||
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
|
||||
imageSrc.value = props.src
|
||||
}
|
||||
|
||||
function handleLoad(e: Event, img: HTMLImageElement) {
|
||||
imgWidth.value = img.width
|
||||
imgHeight.value = img.height
|
||||
loading.value = false
|
||||
function handleLoad() {
|
||||
isLoading.value = false
|
||||
hasLoadError.value = false
|
||||
}
|
||||
|
||||
function handleError(event: Event) {
|
||||
loading.value = false
|
||||
isLoading.value = false
|
||||
hasLoadError.value = true
|
||||
emit('error', event)
|
||||
}
|
||||
@ -235,9 +216,9 @@ function switchViewer(val: number) {
|
||||
watch(
|
||||
() => props.src,
|
||||
() => {
|
||||
if (props.lazy) {
|
||||
if (isManual.value) {
|
||||
// reset status
|
||||
loading.value = true
|
||||
isLoading.value = true
|
||||
hasLoadError.value = false
|
||||
removeLazyLoadListener()
|
||||
addLazyLoadListener()
|
||||
@ -248,7 +229,7 @@ watch(
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (props.lazy) {
|
||||
if (isManual.value) {
|
||||
addLazyLoadListener()
|
||||
} else {
|
||||
loadImage()
|
||||
|
@ -6,6 +6,12 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
%position {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
@include b(image) {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
@ -17,11 +23,13 @@
|
||||
}
|
||||
|
||||
@include e(placeholder) {
|
||||
@extend %position !optional;
|
||||
@extend %size !optional;
|
||||
background: getCssVar('fill-color', 'light');
|
||||
}
|
||||
|
||||
@include e(error) {
|
||||
@extend %position !optional;
|
||||
@extend %size !optional;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
Loading…
Reference in New Issue
Block a user