Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#2870] VaImage intersectioning #2872

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/docs/src/locales/en/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,7 @@
"sizes": "Proxies the `sizes` attribute to the image [MDN](https://developer.mozilla.org/docs/Web/HTML/Element/img#using_the_srcset_and_sizes_attributes)[[target=_blank]].",
"srcset": "Proxies the `srcset` attribute to the image [MDN](https://developer.mozilla.org/docs/Web/HTML/Element/img#using_the_srcset_and_sizes_attributes)[[target=_blank]].",
"title": "Proxies the `title` attribute to the image [MDN](https://developer.mozilla.org/docs/Web/HTML/Element/img#the_title_attribute)[[target=_blank]].",
"lazy": "Enables lazy load for the image.",
"draggable": "Proxies the `draggable` attribute to the image [MDN](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/draggable)[[target=_blank]].",
"placeholderSrc": "`src` value for the placeholder image (can be replaced with `placeholder` slot)."
},
Expand Down Expand Up @@ -2635,6 +2636,10 @@
"title": "Object Fit",
"text": "Proxies `object-fit` CSS property. Available values are: `contain`, `fill`, `cover`, `scale-down`, `none`."
},
"lazy": {
"title": "Lazy Load",
"text": "The `lazy` prop allows you to use lazy-load behavior. In this case the image will be loaded only when it becomes visible (intersection)."
},
"ratio": {
"title": "Ratio",
"text": "The `ratio` prop changes image original ratio, showing part of image to fit new ratio."
Expand Down
5 changes: 5 additions & 0 deletions packages/docs/src/locales/ru/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@
"sizes": "Проксирует свойство `sizes` в изображение [MDN](https://developer.mozilla.org/docs/Web/HTML/Element/img#using_the_srcset_and_sizes_attributes)[[target=_blank]].",
"srcset": "Проксирует свойство `srcset` в изображение [MDN](https://developer.mozilla.org/docs/Web/HTML/Element/img#using_the_srcset_and_sizes_attributes)[[target=_blank]].",
"title": "Проксирует свойство `title` в изображение [MDN](https://developer.mozilla.org/docs/Web/HTML/Element/img#the_title_attribute)[[target=_blank]].",
"lazy": "Включает ленивую загрузку изображения.",
"draggable": "Проксирует свойство `draggable` в изображение [MDN](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/draggable)[[target=_blank]].",
"placeholderSrc": "Значение аттрибута `src` для тега `img` в плейсхолдере (который может быть перезаписан с помощью слота `placeholder`)."
},
Expand Down Expand Up @@ -2509,6 +2510,10 @@
"title": "Object Fit",
"text": "Проксирует значение CSS свойства `object-fit` в компонент. Допустимые значения: `contain`, `fill`, `cover`, `scale-down`, `none`."
},
"lazy": {
"title": "Ленивая загрузка",
"text": "Свойство `lazy` позволяет задать компоненту поведение ленивой загрузки. В этом случае изображение будет загружено лишь тогда, когда войдет в видимую область экрана пользователя."
},
"ratio": {
"title": "Соотношение сторон",
"text": "Свойство `ratio` изменяет исходное соотношение сторон изображения."
Expand Down
12 changes: 12 additions & 0 deletions packages/docs/src/page-configs/ui-elements/image/examples/Lazy.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<template>
<va-image
class="w-full md:w-1/2 lg:w-1/3"
src="https://picsum.photos/1500"
lazy
@loaded="consoleLog('lazy image was loaded only after intersection')"
/>
</template>

<script setup>
const consoleLog = (e) => console.log(e)
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ const config: ApiDocsBlock[] = [
'image.examples.srcSet.text',
'SrcSet',
),
...block.exampleBlock(
'image.examples.lazy.title',
'image.examples.lazy.text',
'Lazy',
),

block.subtitle('all.api'),
block.api(VaImage, apiOptions),
Expand Down
49 changes: 46 additions & 3 deletions packages/ui/src/components/va-image/VaImage.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<template>
<va-aspect-ratio
ref="root"
class="va-image"
v-bind="aspectRationAttributesComputed"
>
Expand All @@ -11,6 +12,7 @@
<slot v-if="$slots.sources" name="sources" />

<img
v-if="isReadyForRender"
ref="image"
v-bind="imgAttributesComputed"
@error="handleError"
Expand Down Expand Up @@ -55,12 +57,26 @@
</template>

<script lang="ts">
import { defineComponent, ref, computed, watch, nextTick, onBeforeUnmount, type PropType, onBeforeMount } from 'vue'
import {
defineComponent,
ref,
computed,
watch,
nextTick,
onBeforeMount,
onBeforeUnmount,
type PropType,
} from 'vue'

import { VaAspectRatio } from '../va-aspect-ratio'

import { useNativeImgAttributes, useNativeImgAttributesProps } from './hooks/useNativeImgAttributes'
import { useComponentPresetProp, useDeprecated } from '../../composables'
import {
useComponentPresetProp,
useIsMounted,
useDeprecated,
useIntersectionObserver,
} from '../../composables'

export default defineComponent({
name: 'VaImage',
Expand All @@ -87,6 +103,7 @@ export default defineComponent({
type: String as PropType<'contain' | 'fill' | 'cover' | 'scale-down' | 'none'>,
default: 'cover',
},
lazy: { type: Boolean, default: false },
placeholderSrc: { type: String, default: '' },
// TODO: delete in 1.7.0
contain: { type: Boolean, default: false },
Expand All @@ -96,7 +113,9 @@ export default defineComponent({
// TODO: delete in 1.7.0
useDeprecated(['contain'])

const root = ref<HTMLElement>()
const image = ref<HTMLImageElement>()

const renderedImage = ref()
const currentImage = computed(() => renderedImage.value || props.src)

Expand All @@ -107,6 +126,10 @@ export default defineComponent({
const isError = ref(false)

const handleLoad = () => {
isLoading.value = true

if (!isReadyForLoad.value) { return }

isLoading.value = false

renderedImage.value = image.value?.currentSrc
Expand All @@ -122,8 +145,23 @@ export default defineComponent({
emit('error', err || currentImage.value)
}

const isIntersecting = ref(false)
const handleIntersection = (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) { return }

isIntersecting.value = true
init()
observer.disconnect()
})
}
const { isIntersectionDisabled } = useIntersectionObserver(handleIntersection, undefined, root, props.lazy)
const isReadyForLoad = computed(() => isIntersectionDisabled.value || isIntersecting.value)
const isMounted = useIsMounted()
const isReadyForRender = computed(() => !props.lazy || (props.lazy && isMounted.value && isReadyForLoad.value))

const init = () => {
if (!props.src || isLoading.value) {
if (!props.src || (isLoading.value && isIntersectionDisabled.value) || !isReadyForLoad.value) {
return
}

Expand Down Expand Up @@ -184,11 +222,16 @@ export default defineComponent({

return {
fitComputed,

root,
image,

isLoading,
handleLoad,
isError,
handleError,
isReadyForRender,

isPlaceholderShown,
isSuccessfullyLoaded,
imgAttributesComputed,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const useNativeImgAttributesProps = {
title: { type: String, default: '' },
sizes: { type: String, default: '' },
srcset: { type: String, default: '' },
draggable: { type: Boolean, default: false },
draggable: { type: Boolean, default: true },
loading: {
type: String as PropType<'lazy' | 'eager'>,
},
Expand Down
43 changes: 19 additions & 24 deletions packages/ui/src/composables/useIntersectionObserver.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { onBeforeUnmount, ref, Ref, unref, watch } from 'vue'
import { ref, unref, computed, watch, onBeforeUnmount, type Ref } from 'vue'

import { extractHTMLElement } from './useHTMLElement'

type MaybeRef<T> = T | Ref<T>

export const useIntersectionObserver = <T extends HTMLElement | undefined>(
cb: (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => void,
options: MaybeRef<IntersectionObserverInit> = {},
targetsList: Ref<MaybeRef<T>[]> = ref([]),
options: Ref<IntersectionObserverInit> = ref({}),
target: Ref<MaybeRef<T>[] | T> = ref([]),
enabled = true,
) => {
const observer = ref<IntersectionObserver>()

Expand All @@ -14,41 +17,33 @@ export const useIntersectionObserver = <T extends HTMLElement | undefined>(
}

const observeTarget = (target: MaybeRef<T>) => {
const disclosedTarget = unref(target)
const disclosedTarget = extractHTMLElement(unref(target))
disclosedTarget && observer.value?.observe(disclosedTarget)
}

const observeAll = (targets: MaybeRef<MaybeRef<T>[]>) => {
const disclosedTargets = unref(targets)
disclosedTargets.forEach(observeTarget)
const observeAll = (targets: MaybeRef<T>[]) => {
targets.forEach(observeTarget)
}

const initObserver = () => {
observer.value = new IntersectionObserver(cb, unref(options))
observer.value = new IntersectionObserver(cb, options.value)
}

watch([targetsList, options], ([newList, newOptions], [oldList, oldOptions]) => {
disconnectObserver()
const isIntersectionDisabled = computed(() => !enabled || !(typeof window !== 'undefined' && 'IntersectionObserver' in window))

if (newOptions !== oldOptions) {
if (newList.length) {
initObserver()
observeAll(newList)
}
watch([target, options], ([newTarget]) => {
if (isIntersectionDisabled.value) { return }

disconnectObserver()

return
}
if (!newTarget) { return }

if (newList.length) {
if (!observer.value) {
initObserver()
}
initObserver()

observeAll(newList)
}
Array.isArray(newTarget) ? observeAll(newTarget) : observeTarget(newTarget)
}, { immediate: true })

onBeforeUnmount(disconnectObserver)

return observer
return { isIntersectionDisabled }
}