From 50c8e66a38addec8e71f6b9782142ea434e88026 Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Sat, 17 Dec 2022 13:58:47 +0100 Subject: [PATCH 01/36] feat: :sparkles: Add logic to handle multiple formats --- playground/pages/picture.vue | 8 ++++- src/runtime/components/nuxt-picture.ts | 46 ++++++++++++-------------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/playground/pages/picture.vue b/playground/pages/picture.vue index 904bf6a60..5ba998be6 100644 --- a/playground/pages/picture.vue +++ b/playground/pages/picture.vue @@ -1,6 +1,12 @@ diff --git a/src/runtime/components/nuxt-picture.ts b/src/runtime/components/nuxt-picture.ts index 3be648c20..dce34baac 100644 --- a/src/runtime/components/nuxt-picture.ts +++ b/src/runtime/components/nuxt-picture.ts @@ -1,7 +1,7 @@ -import { h, defineComponent, computed } from 'vue' -import { useBaseImage, baseImageProps } from './_base' -import { useImage, useHead } from '#imports' +import { computed, defineComponent, h } from 'vue' +import { baseImageProps, useBaseImage } from './_base' import { getFileExtension } from '#image' +import { useHead, useImage } from '#imports' export const pictureProps = { ...baseImageProps, @@ -20,25 +20,25 @@ export default defineComponent({ const originalFormat = computed(() => getFileExtension(props.src)) - const format = computed(() => props.format || originalFormat.value === 'svg' ? 'svg' : 'webp') - const legacyFormat = computed(() => { if (props.legacyFormat) { return props.legacyFormat } - const formats: Record = { - webp: isTransparent.value ? 'png' : 'jpeg', - svg: 'png' - } - return formats[format.value] || originalFormat.value + return isTransparent.value ? 'png' : 'jpg' }) const nSources = computed>(() => { - if (format.value === 'svg') { + if (originalFormat.value === 'svg') { return [{ srcset: props.src }] } - const formats = legacyFormat.value !== format.value - ? [legacyFormat.value, format.value] - : [format.value] + const format = props.format || originalFormat.value + const formats = format.split(',') + + if (!formats.includes(legacyFormat.value)) { + formats.push(legacyFormat.value) + } else { + formats.splice(formats.indexOf(legacyFormat.value), 1) + formats.push(legacyFormat.value) + } return formats.map((format) => { const { srcset, sizes, src } = $img.getSizes(props.src, { @@ -70,19 +70,17 @@ export default defineComponent({ } return () => h('picture', { key: nSources.value[0].src }, [ - ...(nSources.value?.[1] - ? [h('source', { - type: nSources.value[1].type, - sizes: nSources.value[1].sizes, - srcset: nSources.value[1].srcset - })] - : []), + nSources.value.slice(0, -1).map(source => h('source', { + type: source.type, + sizes: source.sizes, + srcset: source.srcset + })), h('img', { ..._base.attrs.value, ...imgAttrs, - src: nSources.value[0].src, - sizes: nSources.value[0].sizes, - srcset: nSources.value[0].srcset + src: nSources.value.at(-1).src, + sizes: nSources.value.at(-1).sizes, + srcset: nSources.value.at(-1).srcset }) ]) } From 8980005d7f0086c8f8862ed94a78e97415211d2f Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Sat, 17 Dec 2022 14:09:34 +0100 Subject: [PATCH 02/36] feat: :memo: Add documentation --- docs/content/3.components/2.nuxt-picture.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/content/3.components/2.nuxt-picture.md b/docs/content/3.components/2.nuxt-picture.md index 26b29ffe7..6ba42fff6 100644 --- a/docs/content/3.components/2.nuxt-picture.md +++ b/docs/content/3.components/2.nuxt-picture.md @@ -14,6 +14,16 @@ Learn more about the [`` tag on MDN](https://developer.mozilla.org/en-U See props supported by[``](/components/nuxt-img#props) :: +### `format` + +Format on images can be used to serve images in a multiple formats. A legacy format will be generated automaticlly. So in this case avif, webp and jpg or png would be generated. They will be added in the same order they are added to the format attribute. + +```html + +``` + +Available formats are `webp`, `avif`, `jpeg`, `jpg`, `png`, `gif` and `svg`. If the format is not specified, it will respect the default image format. + ### `legacyFormat` Format used for fallback. Default is conditional: From 4679830b5661447063729e5f2b5d9f69c9f32600 Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Sat, 17 Dec 2022 14:19:52 +0100 Subject: [PATCH 03/36] feat: :rewind: This reverts some changes to work like the original code --- src/runtime/components/nuxt-picture.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/runtime/components/nuxt-picture.ts b/src/runtime/components/nuxt-picture.ts index dce34baac..215cafb7d 100644 --- a/src/runtime/components/nuxt-picture.ts +++ b/src/runtime/components/nuxt-picture.ts @@ -16,13 +16,13 @@ export default defineComponent({ const $img = useImage() const _base = useBaseImage(props) - const isTransparent = computed(() => ['png', 'webp', 'gif'].includes(originalFormat.value)) + const isTransparent = computed(() => ['png', 'webp', 'gif', 'svg'].includes(originalFormat.value)) const originalFormat = computed(() => getFileExtension(props.src)) const legacyFormat = computed(() => { if (props.legacyFormat) { return props.legacyFormat } - return isTransparent.value ? 'png' : 'jpg' + return isTransparent.value ? 'png' : 'jpeg' }) const nSources = computed>(() => { From 24df1709d9363a9d89a3f82874a693a9a620e8e0 Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Sat, 17 Dec 2022 14:23:51 +0100 Subject: [PATCH 04/36] feat: :rewind: svg should only have a srcset --- src/runtime/components/nuxt-picture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/components/nuxt-picture.ts b/src/runtime/components/nuxt-picture.ts index 215cafb7d..db3d8ac40 100644 --- a/src/runtime/components/nuxt-picture.ts +++ b/src/runtime/components/nuxt-picture.ts @@ -46,7 +46,7 @@ export default defineComponent({ sizes: props.sizes || $img.options.screens, modifiers: { ..._base.modifiers.value, format } }) - + if (format === 'svg') { return { srcset } } return { src, type: `image/${format}`, sizes, srcset } }) }) From cbe7367b703a46821d34a6baeb5452631cba5eb5 Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Sat, 17 Dec 2022 14:32:23 +0100 Subject: [PATCH 05/36] feat: :sparkles: Use src as srcset if the image is an svg --- src/runtime/components/nuxt-picture.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/runtime/components/nuxt-picture.ts b/src/runtime/components/nuxt-picture.ts index db3d8ac40..510743723 100644 --- a/src/runtime/components/nuxt-picture.ts +++ b/src/runtime/components/nuxt-picture.ts @@ -26,10 +26,6 @@ export default defineComponent({ }) const nSources = computed>(() => { - if (originalFormat.value === 'svg') { - return [{ srcset: props.src }] - } - const format = props.format || originalFormat.value const formats = format.split(',') @@ -46,7 +42,7 @@ export default defineComponent({ sizes: props.sizes || $img.options.screens, modifiers: { ..._base.modifiers.value, format } }) - if (format === 'svg') { return { srcset } } + if (format === 'svg') { return { srcset: src } } return { src, type: `image/${format}`, sizes, srcset } }) }) From 57e0b7a8d934f6345c1bade8b9ac4255c11bc571 Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Fri, 23 Dec 2022 18:53:27 +0100 Subject: [PATCH 06/36] docs: :memo: Update documentation --- docs/content/3.components/2.nuxt-picture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/3.components/2.nuxt-picture.md b/docs/content/3.components/2.nuxt-picture.md index 6ba42fff6..7a110f529 100644 --- a/docs/content/3.components/2.nuxt-picture.md +++ b/docs/content/3.components/2.nuxt-picture.md @@ -16,7 +16,7 @@ Learn more about the [`` tag on MDN](https://developer.mozilla.org/en-U ### `format` -Format on images can be used to serve images in a multiple formats. A legacy format will be generated automaticlly. So in this case avif, webp and jpg or png would be generated. They will be added in the same order they are added to the format attribute. +Format on pictures can be used to serve images in a multiple formats. A legacy format will be generated automaticlly. So in the example below avif, webp and png would be generated. They will be added in the same order they are added to the format attribute. ```html From 9f00f9a2eecb80150a3b0c0a99542263d83c3b00 Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer <40826752+marcelxpfeifer@users.noreply.github.com> Date: Sat, 11 Feb 2023 13:37:19 +0100 Subject: [PATCH 07/36] fix: :bug: Use all sources --- src/runtime/components/nuxt-picture.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/runtime/components/nuxt-picture.ts b/src/runtime/components/nuxt-picture.ts index a84a22c2c..fe54dd980 100644 --- a/src/runtime/components/nuxt-picture.ts +++ b/src/runtime/components/nuxt-picture.ts @@ -40,7 +40,7 @@ export default defineComponent({ const sources = computed(() => { const format = props.format || originalFormat.value; const formats = format.split(","); - if (format.value === "svg") { + if (format === "svg") { return [{ srcset: props.src }]; } @@ -104,15 +104,13 @@ export default defineComponent({ const lastSourceIndex = computed(() => sources.value.length - 1); return () => h("picture", { key: sources.value[0].src }, [ - ...(sources.value?.[1] - ? [ - h("source", { - type: sources.value[1].type, - sizes: sources.value[1].sizes, - srcset: sources.value[1].srcset, - }), - ] - : []), + sources.value.slice(0, -1).map((source) => + h("source", { + type: source.type, + sizes: source.sizes, + srcset: source.srcset, + }) + ), h("img", { ref: imgEl, ..._base.attrs.value, From c5c8e3a61c21e0cc82e4204d6e4390d0d96a7d39 Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer <40826752+marcelxpfeifer@users.noreply.github.com> Date: Sat, 11 Feb 2023 13:56:45 +0100 Subject: [PATCH 08/36] style: :bug: Use eslint config --- src/runtime/components/nuxt-picture.ts | 109 +++++++++++++------------ 1 file changed, 55 insertions(+), 54 deletions(-) diff --git a/src/runtime/components/nuxt-picture.ts b/src/runtime/components/nuxt-picture.ts index fe54dd980..143c2d4f4 100644 --- a/src/runtime/components/nuxt-picture.ts +++ b/src/runtime/components/nuxt-picture.ts @@ -1,35 +1,36 @@ -import { getFileExtension } from "#image"; -import { useHead, useImage } from "#imports"; -import { computed, defineComponent, h, onMounted, ref } from "vue"; -import { prerenderStaticImages } from "../utils/prerender"; -import { baseImageProps, useBaseImage } from "./_base"; + +import { computed, defineComponent, h, onMounted, ref } from 'vue' +import { prerenderStaticImages } from '../utils/prerender' +import { baseImageProps, useBaseImage } from './_base' +import { useHead, useImage } from '#imports' +import { getFileExtension } from '#image' export const pictureProps = { ...baseImageProps, legacyFormat: { type: String, default: null }, - imgAttrs: { type: Object, default: null }, -}; + imgAttrs: { type: Object, default: null } +} export default defineComponent({ - name: "NuxtPicture", + name: 'NuxtPicture', props: pictureProps, - emits: ["load"], + emits: ['load'], setup: (props, ctx) => { - const $img = useImage(); - const _base = useBaseImage(props); + const $img = useImage() + const _base = useBaseImage(props) const isTransparent = computed(() => - ["png", "webp", "gif", "svg"].includes(originalFormat.value) - ); + ['png', 'webp', 'gif', 'svg'].includes(originalFormat.value) + ) - const originalFormat = computed(() => getFileExtension(props.src)); + const originalFormat = computed(() => getFileExtension(props.src)) const legacyFormat = computed(() => { if (props.legacyFormat) { - return props.legacyFormat; + return props.legacyFormat } - return isTransparent.value ? "png" : "jpeg"; - }); + return isTransparent.value ? 'png' : 'jpeg' + }) type Source = { srcset: string; @@ -38,87 +39,87 @@ export default defineComponent({ sizes?: string; }; const sources = computed(() => { - const format = props.format || originalFormat.value; - const formats = format.split(","); - if (format === "svg") { - return [{ srcset: props.src }]; + const format = props.format || originalFormat.value + const formats = format.split(',') + if (format === 'svg') { + return [{ srcset: props.src }] } if (!formats.includes(legacyFormat.value)) { - formats.push(legacyFormat.value); + formats.push(legacyFormat.value) } else { - formats.splice(formats.indexOf(legacyFormat.value), 1); - formats.push(legacyFormat.value); + formats.splice(formats.indexOf(legacyFormat.value), 1) + formats.push(legacyFormat.value) } return formats.map((format: string) => { const { srcset, sizes, src } = $img.getSizes(props.src!, { ..._base.options.value, sizes: props.sizes || $img.options.screens, - modifiers: { ..._base.modifiers.value, format }, - }); + modifiers: { ..._base.modifiers.value, format } + }) - return { src, type: `image/${format}`, sizes, srcset }; - }); - }); + return { src, type: `image/${format}`, sizes, srcset } + }) + }) if (props.preload) { - const srcKey = sources.value?.[1] ? 1 : 0; + const srcKey = sources.value?.[1] ? 1 : 0 const link: any = { - rel: "preload", - as: "image", - imagesrcset: sources.value[srcKey].srcset, - }; + rel: 'preload', + as: 'image', + imagesrcset: sources.value[srcKey].srcset + } if (sources.value?.[srcKey]?.sizes) { - link.imagesizes = sources.value[srcKey].sizes; + link.imagesizes = sources.value[srcKey].sizes } - useHead({ link: [link] }); + useHead({ link: [link] }) } // Only passdown supported attributes - const imgAttrs = { ...props.imgAttrs }; + const imgAttrs = { ...props.imgAttrs } for (const key in ctx.attrs) { if (key in baseImageProps && !(key in imgAttrs)) { - imgAttrs[key] = ctx.attrs[key]; + imgAttrs[key] = ctx.attrs[key] } } - const imgEl = ref(); + const imgEl = ref() // Prerender static images if (process.server && process.env.prerender) { for (const src of sources.value as Source[]) { - prerenderStaticImages(src.src, src.srcset); + prerenderStaticImages(src.src, src.srcset) } } onMounted(() => { imgEl.value!.onload = (event) => { - ctx.emit("load", event); - }; - }); + ctx.emit('load', event) + } + }) - const lastSourceIndex = computed(() => sources.value.length - 1); + const lastSourceIndex = computed(() => sources.value.length - 1) return () => - h("picture", { key: sources.value[0].src }, [ - sources.value.slice(0, -1).map((source) => - h("source", { + h('picture', { key: sources.value[0].src }, [ + sources.value.slice(0, -1).map(source => + h('source', { type: source.type, sizes: source.sizes, - srcset: source.srcset, + srcset: source.srcset }) ), - h("img", { + h('img', { ref: imgEl, ..._base.attrs.value, ...imgAttrs, src: sources.value[lastSourceIndex.value].src, sizes: sources.value[lastSourceIndex.value].sizes, - srcset: sources.value[lastSourceIndex.value].srcset, - }), - ]); - }, -}); + srcset: sources.value[lastSourceIndex.value].srcset + }) + ]) + } +}) From 510b7863864fbd79a0aefd044d01a48fe5571647 Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Tue, 6 Jun 2023 23:31:03 +0200 Subject: [PATCH 09/36] feat: :twisted_rightwards_arrows: Merge rc1 --- docs/content/3.components/2.nuxt-picture.md | 10 +++ playground/pages/picture.vue | 2 +- src/runtime/components/nuxt-picture.ts | 71 +++++++++++---------- 3 files changed, 49 insertions(+), 34 deletions(-) diff --git a/docs/content/3.components/2.nuxt-picture.md b/docs/content/3.components/2.nuxt-picture.md index 26b29ffe7..7a110f529 100644 --- a/docs/content/3.components/2.nuxt-picture.md +++ b/docs/content/3.components/2.nuxt-picture.md @@ -14,6 +14,16 @@ Learn more about the [`` tag on MDN](https://developer.mozilla.org/en-U See props supported by[``](/components/nuxt-img#props) :: +### `format` + +Format on pictures can be used to serve images in a multiple formats. A legacy format will be generated automaticlly. So in the example below avif, webp and png would be generated. They will be added in the same order they are added to the format attribute. + +```html + +``` + +Available formats are `webp`, `avif`, `jpeg`, `jpg`, `png`, `gif` and `svg`. If the format is not specified, it will respect the default image format. + ### `legacyFormat` Format used for fallback. Default is conditional: diff --git a/playground/pages/picture.vue b/playground/pages/picture.vue index 9dbb492f8..64d32cfc4 100644 --- a/playground/pages/picture.vue +++ b/playground/pages/picture.vue @@ -1,6 +1,6 @@ diff --git a/src/runtime/components/nuxt-picture.ts b/src/runtime/components/nuxt-picture.ts index 53517fde9..e9765cda7 100644 --- a/src/runtime/components/nuxt-picture.ts +++ b/src/runtime/components/nuxt-picture.ts @@ -1,8 +1,8 @@ -import { h, defineComponent, ref, computed, onMounted } from 'vue' -import { prerenderStaticImages } from '../utils/prerender' -import { useBaseImage, baseImageProps } from './_base' -import { useImage, useHead, useNuxtApp } from '#imports' import { getFileExtension } from '#image' +import { useHead, useImage, useNuxtApp } from '#imports' +import { computed, defineComponent, h, onMounted, ref } from 'vue' +import { prerenderStaticImages } from '../utils/prerender' +import { baseImageProps, useBaseImage } from './_base' export const pictureProps = { ...baseImageProps, @@ -35,31 +35,35 @@ export default defineComponent({ type Source = { srcset: string, src?: string, type?: string, sizes?: string } const sources = computed(() => { - if (format.value === 'svg') { - return [{ src: props.src }] + const format = props.format || originalFormat.value + const formats = format.split(',') + if (format === 'svg') { + return [{ srcset: props.src }] } - const formats = legacyFormat.value !== format.value - ? [legacyFormat.value, format.value] - : [format.value] + if (!formats.includes(legacyFormat.value)) { + formats.push(legacyFormat.value) + } else { + formats.splice(formats.indexOf(legacyFormat.value), 1) + formats.push(legacyFormat.value) + } - return formats.map((format) => { + return formats.map((format: string) => { const { srcset, sizes, src } = $img.getSizes(props.src!, { ..._base.options.value, sizes: props.sizes || $img.options.screens, modifiers: { ..._base.modifiers.value, format } }) - return { src, type: `image/${format}`, sizes, srcset } + return { src, type: `image/${format}`, sizes, srcset } }) }) + const lastSourceIndex = computed(() => sources.value.length - 1) if (props.preload) { - const srcKey = sources.value?.[1] ? 1 : 0 + const link: any = { rel: 'preload', as: 'image', imagesrcset: sources.value[lastSourceIndex.value].srcset } - const link: any = { rel: 'preload', as: 'image', imagesrcset: sources.value[srcKey].srcset } - - if (sources.value?.[srcKey]?.sizes) { link.imagesizes = sources.value[srcKey].sizes } + if (sources.value?.[lastSourceIndex.value]?.sizes) { link.imagesizes = sources.value[lastSourceIndex.value].sizes } useHead({ link: [link] }) } @@ -94,23 +98,24 @@ export default defineComponent({ } }) - return () => h('picture', { key: sources.value[0].src }, [ - ...(sources.value?.[1] - ? [h('source', { - type: sources.value[1].type, - sizes: sources.value[1].sizes, - srcset: sources.value[1].srcset - })] - : []), - h('img', { - ref: imgEl, - ..._base.attrs.value, - ...process.server ? { onerror: 'this.setAttribute(\'data-error\', 1)' } : {}, - ...imgAttrs, - src: sources.value[0].src, - sizes: sources.value[0].sizes, - srcset: sources.value[0].srcset - }) - ]) + return () => + h('picture', { key: sources.value[0].src }, [ + ...sources.value.slice(0, -1).map((source) => { + return h('source', { + type: source.type, + sizes: source.sizes, + srcset: source.srcset + }) + }), + h('img', { + ref: imgEl, + ..._base.attrs.value, + ...(process.server ? { onerror: "this.setAttribute('data-error', 1)" } : {}), + ...imgAttrs, + src: sources.value[lastSourceIndex.value].src, + sizes: sources.value[lastSourceIndex.value].sizes, + srcset: sources.value[lastSourceIndex.value].srcset + }) + ]) } }) From 92731e84cad4aa0262e09d5b0ef06df047e88073 Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Tue, 6 Jun 2023 23:38:35 +0200 Subject: [PATCH 10/36] refactor: :rotating_light: Linting --- playground/pages/picture.vue | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/playground/pages/picture.vue b/playground/pages/picture.vue index c42a860dc..64d32cfc4 100644 --- a/playground/pages/picture.vue +++ b/playground/pages/picture.vue @@ -1,18 +1,12 @@ From 047d257f7dd06aa84175dd7d6347ac1e8934cbf7 Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Wed, 7 Jun 2023 09:32:34 +0200 Subject: [PATCH 11/36] refactor: Import order and remove type --- src/runtime/components/nuxt-picture.ts | 134 ++++++++++++++----------- 1 file changed, 76 insertions(+), 58 deletions(-) diff --git a/src/runtime/components/nuxt-picture.ts b/src/runtime/components/nuxt-picture.ts index f054893b4..74bb0d44c 100644 --- a/src/runtime/components/nuxt-picture.ts +++ b/src/runtime/components/nuxt-picture.ts @@ -1,35 +1,35 @@ -import { getFileExtension } from '#image' -import { useHead, useImage, useNuxtApp } from '#imports' -import { computed, defineComponent, h, onMounted, ref } from 'vue' -import { prerenderStaticImages } from '../utils/prerender' -import { baseImageProps, useBaseImage } from './_base' +import { getFileExtension } from "#image"; +import { useHead, useImage, useNuxtApp } from "#imports"; +import { computed, defineComponent, h, onMounted, ref } from "vue"; +import { prerenderStaticImages } from "../utils/prerender"; +import { baseImageProps, useBaseImage } from "./_base"; export const pictureProps = { ...baseImageProps, legacyFormat: { type: String, default: null }, - imgAttrs: { type: Object, default: null } -} + imgAttrs: { type: Object, default: null }, +}; export default defineComponent({ - name: 'NuxtPicture', + name: "NuxtPicture", props: pictureProps, - emits: ['load'], + emits: ["load"], setup: (props, ctx) => { - const $img = useImage() - const _base = useBaseImage(props) + const $img = useImage(); + const _base = useBaseImage(props); const isTransparent = computed(() => - ['png', 'webp', 'gif', 'svg'].includes(originalFormat.value) - ) + ["png", "webp", "gif", "svg"].includes(originalFormat.value) + ); - const originalFormat = computed(() => getFileExtension(props.src)) + const originalFormat = computed(() => getFileExtension(props.src)); const legacyFormat = computed(() => { if (props.legacyFormat) { - return props.legacyFormat + return props.legacyFormat; } - return isTransparent.value ? 'png' : 'jpeg' - }) + return isTransparent.value ? "png" : "jpeg"; + }); type Source = { srcset: string; @@ -38,87 +38,105 @@ export default defineComponent({ sizes?: string; }; const sources = computed(() => { - const format = props.format || (originalFormat.value === 'svg' ? 'svg' : 'webp') - const formats = format.split(',') - if (format === 'svg') { - return [{ srcset: props.src }] + const format = + props.format || (originalFormat.value === "svg" ? "svg" : "webp"); + const formats = format.split(","); + if (format === "svg") { + return [{ srcset: props.src }]; } if (!formats.includes(legacyFormat.value)) { - formats.push(legacyFormat.value) + formats.push(legacyFormat.value); } else { - formats.splice(formats.indexOf(legacyFormat.value), 1) - formats.push(legacyFormat.value) + formats.splice(formats.indexOf(legacyFormat.value), 1); + formats.push(legacyFormat.value); } - return formats.map((format: string) => { + return formats.map((format) => { const { srcset, sizes, src } = $img.getSizes(props.src!, { ..._base.options.value, sizes: props.sizes || $img.options.screens, - modifiers: { ..._base.modifiers.value, format } - }) + modifiers: { ..._base.modifiers.value, format }, + }); - return { src, type: `image/${format}`, sizes, srcset } - }) - }) - const lastSourceIndex = computed(() => sources.value.length - 1) + return { src, type: `image/${format}`, sizes, srcset }; + }); + }); + const lastSourceIndex = computed(() => sources.value.length - 1); if (props.preload) { - const link: any = { rel: 'preload', as: 'image', imagesrcset: sources.value[lastSourceIndex.value].srcset } - - if (sources.value?.[lastSourceIndex.value]?.sizes) { link.imagesizes = sources.value[lastSourceIndex.value].sizes } + const link: any = { + rel: "preload", + as: "image", + imagesrcset: sources.value[lastSourceIndex.value].srcset, + }; + + if (sources.value?.[lastSourceIndex.value]?.sizes) { + link.imagesizes = sources.value[lastSourceIndex.value].sizes; + } - useHead({ link: [link] }) + useHead({ link: [link] }); } // Only passdown supported attributes - const imgAttrs: Record = { ...props.imgAttrs, 'data-nuxt-pic': '' } + const imgAttrs: Record = { + ...props.imgAttrs, + "data-nuxt-pic": "", + }; for (const key in ctx.attrs) { if (key in baseImageProps && !(key in imgAttrs)) { - imgAttrs[key] = ctx.attrs[key] + imgAttrs[key] = ctx.attrs[key]; } } - const imgEl = ref() + const imgEl = ref(); // Prerender static images if (process.server && process.env.prerender) { for (const src of sources.value as Source[]) { - prerenderStaticImages(src.src, src.srcset) + prerenderStaticImages(src.src, src.srcset); } } - const nuxtApp = useNuxtApp() - const initialLoad = nuxtApp.isHydrating + const nuxtApp = useNuxtApp(); + const initialLoad = nuxtApp.isHydrating; onMounted(() => { - if (!imgEl.value) { return } + if (!imgEl.value) { + return; + } - if (imgEl.value.complete && initialLoad && !imgEl.value.getAttribute('data-error')) { - ctx.emit('load', new Event('load')) + if ( + imgEl.value.complete && + initialLoad && + !imgEl.value.getAttribute("data-error") + ) { + ctx.emit("load", new Event("load")); } imgEl.value.onload = (event) => { - ctx.emit('load', event) - } - }) + ctx.emit("load", event); + }; + }); return () => - h('picture', { key: sources.value[0].src }, [ + h("picture", { key: sources.value[0].src }, [ ...sources.value.slice(0, -1).map((source) => { - return h('source', { + return h("source", { type: source.type, sizes: source.sizes, - srcset: source.srcset - }) + srcset: source.srcset, + }); }), - h('img', { + h("img", { ref: imgEl, ..._base.attrs.value, - ...(process.server ? { onerror: "this.setAttribute('data-error', 1)" } : {}), + ...(process.server + ? { onerror: "this.setAttribute('data-error', 1)" } + : {}), ...imgAttrs, src: sources.value[lastSourceIndex.value].src, sizes: sources.value[lastSourceIndex.value].sizes, - srcset: sources.value[lastSourceIndex.value].srcset - }) - ]) - } -}) + srcset: sources.value[lastSourceIndex.value].srcset, + }), + ]); + }, +}); From 0ad807f6817b51797d7e9ba3108dfce59d4883a7 Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Wed, 7 Jun 2023 09:33:21 +0200 Subject: [PATCH 12/36] refactor: Import order --- src/runtime/components/nuxt-picture.ts | 132 +++++++++++-------------- 1 file changed, 57 insertions(+), 75 deletions(-) diff --git a/src/runtime/components/nuxt-picture.ts b/src/runtime/components/nuxt-picture.ts index 74bb0d44c..8b50fb76b 100644 --- a/src/runtime/components/nuxt-picture.ts +++ b/src/runtime/components/nuxt-picture.ts @@ -1,35 +1,35 @@ -import { getFileExtension } from "#image"; -import { useHead, useImage, useNuxtApp } from "#imports"; -import { computed, defineComponent, h, onMounted, ref } from "vue"; -import { prerenderStaticImages } from "../utils/prerender"; -import { baseImageProps, useBaseImage } from "./_base"; +import { getFileExtension } from '#image' +import { useHead, useImage, useNuxtApp } from '#imports' +import { computed, defineComponent, h, onMounted, ref } from 'vue' +import { prerenderStaticImages } from '../utils/prerender' +import { baseImageProps, useBaseImage } from './_base' export const pictureProps = { ...baseImageProps, legacyFormat: { type: String, default: null }, - imgAttrs: { type: Object, default: null }, -}; + imgAttrs: { type: Object, default: null } +} export default defineComponent({ - name: "NuxtPicture", + name: 'NuxtPicture', props: pictureProps, - emits: ["load"], + emits: ['load'], setup: (props, ctx) => { - const $img = useImage(); - const _base = useBaseImage(props); + const $img = useImage() + const _base = useBaseImage(props) const isTransparent = computed(() => - ["png", "webp", "gif", "svg"].includes(originalFormat.value) - ); + ['png', 'webp', 'gif', 'svg'].includes(originalFormat.value) + ) - const originalFormat = computed(() => getFileExtension(props.src)); + const originalFormat = computed(() => getFileExtension(props.src)) const legacyFormat = computed(() => { if (props.legacyFormat) { - return props.legacyFormat; + return props.legacyFormat } - return isTransparent.value ? "png" : "jpeg"; - }); + return isTransparent.value ? 'png' : 'jpeg' + }) type Source = { srcset: string; @@ -38,105 +38,87 @@ export default defineComponent({ sizes?: string; }; const sources = computed(() => { - const format = - props.format || (originalFormat.value === "svg" ? "svg" : "webp"); - const formats = format.split(","); - if (format === "svg") { - return [{ srcset: props.src }]; + const format = props.format || (originalFormat.value === 'svg' ? 'svg' : 'webp') + const formats = format.split(',') + if (format === 'svg') { + return [{ srcset: props.src }] } if (!formats.includes(legacyFormat.value)) { - formats.push(legacyFormat.value); + formats.push(legacyFormat.value) } else { - formats.splice(formats.indexOf(legacyFormat.value), 1); - formats.push(legacyFormat.value); + formats.splice(formats.indexOf(legacyFormat.value), 1) + formats.push(legacyFormat.value) } return formats.map((format) => { const { srcset, sizes, src } = $img.getSizes(props.src!, { ..._base.options.value, sizes: props.sizes || $img.options.screens, - modifiers: { ..._base.modifiers.value, format }, - }); + modifiers: { ..._base.modifiers.value, format } + }) - return { src, type: `image/${format}`, sizes, srcset }; - }); - }); - const lastSourceIndex = computed(() => sources.value.length - 1); + return { src, type: `image/${format}`, sizes, srcset } + }) + }) + const lastSourceIndex = computed(() => sources.value.length - 1) if (props.preload) { - const link: any = { - rel: "preload", - as: "image", - imagesrcset: sources.value[lastSourceIndex.value].srcset, - }; - - if (sources.value?.[lastSourceIndex.value]?.sizes) { - link.imagesizes = sources.value[lastSourceIndex.value].sizes; - } + const link: any = { rel: 'preload', as: 'image', imagesrcset: sources.value[lastSourceIndex.value].srcset } + + if (sources.value?.[lastSourceIndex.value]?.sizes) { link.imagesizes = sources.value[lastSourceIndex.value].sizes } - useHead({ link: [link] }); + useHead({ link: [link] }) } // Only passdown supported attributes - const imgAttrs: Record = { - ...props.imgAttrs, - "data-nuxt-pic": "", - }; + const imgAttrs: Record = { ...props.imgAttrs, 'data-nuxt-pic': '' } for (const key in ctx.attrs) { if (key in baseImageProps && !(key in imgAttrs)) { - imgAttrs[key] = ctx.attrs[key]; + imgAttrs[key] = ctx.attrs[key] } } - const imgEl = ref(); + const imgEl = ref() // Prerender static images if (process.server && process.env.prerender) { for (const src of sources.value as Source[]) { - prerenderStaticImages(src.src, src.srcset); + prerenderStaticImages(src.src, src.srcset) } } - const nuxtApp = useNuxtApp(); - const initialLoad = nuxtApp.isHydrating; + const nuxtApp = useNuxtApp() + const initialLoad = nuxtApp.isHydrating onMounted(() => { - if (!imgEl.value) { - return; - } + if (!imgEl.value) { return } - if ( - imgEl.value.complete && - initialLoad && - !imgEl.value.getAttribute("data-error") - ) { - ctx.emit("load", new Event("load")); + if (imgEl.value.complete && initialLoad && !imgEl.value.getAttribute('data-error')) { + ctx.emit('load', new Event('load')) } imgEl.value.onload = (event) => { - ctx.emit("load", event); - }; - }); + ctx.emit('load', event) + } + }) return () => - h("picture", { key: sources.value[0].src }, [ + h('picture', { key: sources.value[0].src }, [ ...sources.value.slice(0, -1).map((source) => { - return h("source", { + return h('source', { type: source.type, sizes: source.sizes, - srcset: source.srcset, - }); + srcset: source.srcset + }) }), - h("img", { + h('img', { ref: imgEl, ..._base.attrs.value, - ...(process.server - ? { onerror: "this.setAttribute('data-error', 1)" } - : {}), + ...(process.server ? { onerror: "this.setAttribute('data-error', 1)" } : {}), ...imgAttrs, src: sources.value[lastSourceIndex.value].src, sizes: sources.value[lastSourceIndex.value].sizes, - srcset: sources.value[lastSourceIndex.value].srcset, - }), - ]); - }, -}); + srcset: sources.value[lastSourceIndex.value].srcset + }) + ]) + } +}) From 453109ff793e7b5e3948f6b5a0c1faf869c611d3 Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Wed, 7 Jun 2023 09:35:18 +0200 Subject: [PATCH 13/36] refactor: import order --- src/runtime/components/nuxt-picture.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/runtime/components/nuxt-picture.ts b/src/runtime/components/nuxt-picture.ts index 8b50fb76b..a0a81cfb0 100644 --- a/src/runtime/components/nuxt-picture.ts +++ b/src/runtime/components/nuxt-picture.ts @@ -1,8 +1,8 @@ -import { getFileExtension } from '#image' -import { useHead, useImage, useNuxtApp } from '#imports' -import { computed, defineComponent, h, onMounted, ref } from 'vue' +import { h, defineComponent, ref, computed, onMounted } from 'vue' import { prerenderStaticImages } from '../utils/prerender' -import { baseImageProps, useBaseImage } from './_base' +import { useBaseImage, baseImageProps } from './_base' +import { useImage, useHead, useNuxtApp } from '#imports' +import { getFileExtension } from '#image' export const pictureProps = { ...baseImageProps, From 906c4da9d8139aaffaeef466a34af01444b977cb Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Wed, 7 Jun 2023 09:37:51 +0200 Subject: [PATCH 14/36] refactor: Linebreaks --- src/runtime/components/nuxt-picture.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/runtime/components/nuxt-picture.ts b/src/runtime/components/nuxt-picture.ts index a0a81cfb0..af4eedd0a 100644 --- a/src/runtime/components/nuxt-picture.ts +++ b/src/runtime/components/nuxt-picture.ts @@ -18,25 +18,16 @@ export default defineComponent({ const $img = useImage() const _base = useBaseImage(props) - const isTransparent = computed(() => - ['png', 'webp', 'gif', 'svg'].includes(originalFormat.value) - ) + const isTransparent = computed(() => ['png', 'webp', 'gif', 'svg'].includes(originalFormat.value)) const originalFormat = computed(() => getFileExtension(props.src)) const legacyFormat = computed(() => { - if (props.legacyFormat) { - return props.legacyFormat - } + if (props.legacyFormat) { return props.legacyFormat } return isTransparent.value ? 'png' : 'jpeg' }) - type Source = { - srcset: string; - src?: string; - type?: string; - sizes?: string; - }; + type Source = { srcset: string; src?: string; type?: string; sizes?: string; }; const sources = computed(() => { const format = props.format || (originalFormat.value === 'svg' ? 'svg' : 'webp') const formats = format.split(',') From 96c4103ce13206514dc629895b35af4763a6504f Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Wed, 7 Jun 2023 10:26:38 +0200 Subject: [PATCH 15/36] fix: :bug: svg setup --- src/runtime/components/nuxt-picture.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/runtime/components/nuxt-picture.ts b/src/runtime/components/nuxt-picture.ts index af4eedd0a..bd0b92107 100644 --- a/src/runtime/components/nuxt-picture.ts +++ b/src/runtime/components/nuxt-picture.ts @@ -27,12 +27,12 @@ export default defineComponent({ return isTransparent.value ? 'png' : 'jpeg' }) - type Source = { srcset: string; src?: string; type?: string; sizes?: string; }; + type Source = { srcset?: string, src: string, type?: string, sizes?: string, }; const sources = computed(() => { const format = props.format || (originalFormat.value === 'svg' ? 'svg' : 'webp') const formats = format.split(',') if (format === 'svg') { - return [{ srcset: props.src }] + return [{ src: props.src }] } if (!formats.includes(legacyFormat.value)) { @@ -43,6 +43,9 @@ export default defineComponent({ } return formats.map((format) => { + if (format === 'svg') { + return { src: props.src } + } const { srcset, sizes, src } = $img.getSizes(props.src!, { ..._base.options.value, sizes: props.sizes || $img.options.screens, From c627d18176419e958f6ec8865090851598de35da Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Wed, 7 Jun 2023 10:27:54 +0200 Subject: [PATCH 16/36] refactor: formatting --- src/runtime/components/nuxt-picture.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/components/nuxt-picture.ts b/src/runtime/components/nuxt-picture.ts index bd0b92107..e6e2c55f6 100644 --- a/src/runtime/components/nuxt-picture.ts +++ b/src/runtime/components/nuxt-picture.ts @@ -27,7 +27,7 @@ export default defineComponent({ return isTransparent.value ? 'png' : 'jpeg' }) - type Source = { srcset?: string, src: string, type?: string, sizes?: string, }; + type Source = { srcset?: string, src: string, type?: string, sizes?: string } const sources = computed(() => { const format = props.format || (originalFormat.value === 'svg' ? 'svg' : 'webp') const formats = format.split(',') From 9e5d845405a82918b8249334cdc64a285b1fd7c3 Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Wed, 7 Jun 2023 11:43:17 +0200 Subject: [PATCH 17/36] feat: Imporve svg handling --- src/runtime/components/nuxt-picture.ts | 148 +++++++++++++++---------- 1 file changed, 88 insertions(+), 60 deletions(-) diff --git a/src/runtime/components/nuxt-picture.ts b/src/runtime/components/nuxt-picture.ts index e6e2c55f6..db4438550 100644 --- a/src/runtime/components/nuxt-picture.ts +++ b/src/runtime/components/nuxt-picture.ts @@ -1,118 +1,146 @@ -import { h, defineComponent, ref, computed, onMounted } from 'vue' -import { prerenderStaticImages } from '../utils/prerender' -import { useBaseImage, baseImageProps } from './_base' -import { useImage, useHead, useNuxtApp } from '#imports' -import { getFileExtension } from '#image' +import { getFileExtension } from "#image"; +import { useHead, useImage, useNuxtApp } from "#imports"; +import { computed, defineComponent, h, onMounted, ref } from "vue"; +import { prerenderStaticImages } from "../utils/prerender"; +import { baseImageProps, useBaseImage } from "./_base"; export const pictureProps = { ...baseImageProps, legacyFormat: { type: String, default: null }, - imgAttrs: { type: Object, default: null } -} + imgAttrs: { type: Object, default: null }, +}; export default defineComponent({ - name: 'NuxtPicture', + name: "NuxtPicture", props: pictureProps, - emits: ['load'], + emits: ["load"], setup: (props, ctx) => { - const $img = useImage() - const _base = useBaseImage(props) + const $img = useImage(); + const _base = useBaseImage(props); - const isTransparent = computed(() => ['png', 'webp', 'gif', 'svg'].includes(originalFormat.value)) + const isTransparent = computed(() => + ["png", "webp", "gif", "svg"].includes(originalFormat.value) + ); - const originalFormat = computed(() => getFileExtension(props.src)) + const originalFormat = computed(() => getFileExtension(props.src)); const legacyFormat = computed(() => { - if (props.legacyFormat) { return props.legacyFormat } - return isTransparent.value ? 'png' : 'jpeg' - }) - - type Source = { srcset?: string, src: string, type?: string, sizes?: string } + if (props.legacyFormat) { + return props.legacyFormat; + } + return isTransparent.value ? "png" : "jpeg"; + }); + + type Source = { + srcset?: string; + src?: string; + type?: string; + sizes?: string; + }; const sources = computed(() => { - const format = props.format || (originalFormat.value === 'svg' ? 'svg' : 'webp') - const formats = format.split(',') - if (format === 'svg') { - return [{ src: props.src }] + const format = + props.format || (originalFormat.value === "svg" ? "svg" : "webp"); + const formats = format.split(","); + if (format === "svg") { + return [{ src: props.src }]; } if (!formats.includes(legacyFormat.value)) { - formats.push(legacyFormat.value) + formats.push(legacyFormat.value); } else { - formats.splice(formats.indexOf(legacyFormat.value), 1) - formats.push(legacyFormat.value) + formats.splice(formats.indexOf(legacyFormat.value), 1); + formats.push(legacyFormat.value); } return formats.map((format) => { - if (format === 'svg') { - return { src: props.src } + if (format === "svg") { + return { src: props.src }; } const { srcset, sizes, src } = $img.getSizes(props.src!, { ..._base.options.value, sizes: props.sizes || $img.options.screens, - modifiers: { ..._base.modifiers.value, format } - }) + modifiers: { ..._base.modifiers.value, format }, + }); - return { src, type: `image/${format}`, sizes, srcset } - }) - }) - const lastSourceIndex = computed(() => sources.value.length - 1) + return { src, type: `image/${format}`, sizes, srcset }; + }); + }); + const lastSourceIndex = computed(() => sources.value.length - 1); if (props.preload) { - const link: any = { rel: 'preload', as: 'image', imagesrcset: sources.value[lastSourceIndex.value].srcset } - - if (sources.value?.[lastSourceIndex.value]?.sizes) { link.imagesizes = sources.value[lastSourceIndex.value].sizes } + const link: any = { + rel: "preload", + as: "image", + imagesrcset: sources.value[lastSourceIndex.value].srcset, + }; + + if (sources.value?.[lastSourceIndex.value]?.sizes) { + link.imagesizes = sources.value[lastSourceIndex.value].sizes; + } - useHead({ link: [link] }) + useHead({ link: [link] }); } // Only passdown supported attributes - const imgAttrs: Record = { ...props.imgAttrs, 'data-nuxt-pic': '' } + const imgAttrs: Record = { + ...props.imgAttrs, + "data-nuxt-pic": "", + }; for (const key in ctx.attrs) { if (key in baseImageProps && !(key in imgAttrs)) { - imgAttrs[key] = ctx.attrs[key] + imgAttrs[key] = ctx.attrs[key]; } } - const imgEl = ref() + const imgEl = ref(); // Prerender static images if (process.server && process.env.prerender) { for (const src of sources.value as Source[]) { - prerenderStaticImages(src.src, src.srcset) + prerenderStaticImages(src.src, src.srcset); } } - const nuxtApp = useNuxtApp() - const initialLoad = nuxtApp.isHydrating + const nuxtApp = useNuxtApp(); + const initialLoad = nuxtApp.isHydrating; onMounted(() => { - if (!imgEl.value) { return } + if (!imgEl.value) { + return; + } - if (imgEl.value.complete && initialLoad && !imgEl.value.getAttribute('data-error')) { - ctx.emit('load', new Event('load')) + if ( + imgEl.value.complete && + initialLoad && + !imgEl.value.getAttribute("data-error") + ) { + ctx.emit("load", new Event("load")); } imgEl.value.onload = (event) => { - ctx.emit('load', event) - } - }) + ctx.emit("load", event); + }; + }); return () => - h('picture', { key: sources.value[0].src }, [ + h("picture", { key: sources.value[0].src }, [ ...sources.value.slice(0, -1).map((source) => { - return h('source', { + return h("source", { type: source.type, sizes: source.sizes, - srcset: source.srcset - }) + srcset: source.srcset, + src: source.src, + }); }), - h('img', { + h("img", { ref: imgEl, ..._base.attrs.value, - ...(process.server ? { onerror: "this.setAttribute('data-error', 1)" } : {}), + ...(process.server + ? { onerror: "this.setAttribute('data-error', 1)" } + : {}), ...imgAttrs, src: sources.value[lastSourceIndex.value].src, sizes: sources.value[lastSourceIndex.value].sizes, - srcset: sources.value[lastSourceIndex.value].srcset - }) - ]) - } -}) + srcset: sources.value[lastSourceIndex.value].srcset, + }), + ]); + }, +}); From 36b2ecbc5e7519c582207dd162ae54ad225c39f7 Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Wed, 7 Jun 2023 11:44:42 +0200 Subject: [PATCH 18/36] fix: Linitng --- src/runtime/components/nuxt-picture.ts | 147 ++++++++++--------------- 1 file changed, 60 insertions(+), 87 deletions(-) diff --git a/src/runtime/components/nuxt-picture.ts b/src/runtime/components/nuxt-picture.ts index db4438550..278c633fb 100644 --- a/src/runtime/components/nuxt-picture.ts +++ b/src/runtime/components/nuxt-picture.ts @@ -1,146 +1,119 @@ -import { getFileExtension } from "#image"; -import { useHead, useImage, useNuxtApp } from "#imports"; -import { computed, defineComponent, h, onMounted, ref } from "vue"; -import { prerenderStaticImages } from "../utils/prerender"; -import { baseImageProps, useBaseImage } from "./_base"; +import { h, defineComponent, ref, computed, onMounted } from 'vue' +import { prerenderStaticImages } from '../utils/prerender' +import { useBaseImage, baseImageProps } from './_base' +import { useImage, useHead, useNuxtApp } from '#imports' +import { getFileExtension } from '#image' export const pictureProps = { ...baseImageProps, legacyFormat: { type: String, default: null }, - imgAttrs: { type: Object, default: null }, -}; + imgAttrs: { type: Object, default: null } +} export default defineComponent({ - name: "NuxtPicture", + name: 'NuxtPicture', props: pictureProps, - emits: ["load"], + emits: ['load'], setup: (props, ctx) => { - const $img = useImage(); - const _base = useBaseImage(props); + const $img = useImage() + const _base = useBaseImage(props) - const isTransparent = computed(() => - ["png", "webp", "gif", "svg"].includes(originalFormat.value) - ); + const isTransparent = computed(() => ['png', 'webp', 'gif', 'svg'].includes(originalFormat.value)) - const originalFormat = computed(() => getFileExtension(props.src)); + const originalFormat = computed(() => getFileExtension(props.src)) const legacyFormat = computed(() => { - if (props.legacyFormat) { - return props.legacyFormat; - } - return isTransparent.value ? "png" : "jpeg"; - }); - - type Source = { - srcset?: string; - src?: string; - type?: string; - sizes?: string; - }; + if (props.legacyFormat) { return props.legacyFormat } + return isTransparent.value ? 'png' : 'jpeg' + }) + + type Source = { srcset?: string, src?: string, type?: string, sizes?: string } const sources = computed(() => { - const format = - props.format || (originalFormat.value === "svg" ? "svg" : "webp"); - const formats = format.split(","); - if (format === "svg") { - return [{ src: props.src }]; + const format = props.format || (originalFormat.value === 'svg' ? 'svg' : 'webp') + const formats = format.split(',') + if (format === 'svg') { + return [{ src: props.src }] } if (!formats.includes(legacyFormat.value)) { - formats.push(legacyFormat.value); + formats.push(legacyFormat.value) } else { - formats.splice(formats.indexOf(legacyFormat.value), 1); - formats.push(legacyFormat.value); + formats.splice(formats.indexOf(legacyFormat.value), 1) + formats.push(legacyFormat.value) } return formats.map((format) => { - if (format === "svg") { - return { src: props.src }; + if (format === 'svg') { + return { src: props.src } } const { srcset, sizes, src } = $img.getSizes(props.src!, { ..._base.options.value, sizes: props.sizes || $img.options.screens, - modifiers: { ..._base.modifiers.value, format }, - }); + modifiers: { ..._base.modifiers.value, format } + }) - return { src, type: `image/${format}`, sizes, srcset }; - }); - }); - const lastSourceIndex = computed(() => sources.value.length - 1); + return { src, type: `image/${format}`, sizes, srcset } + }) + }) + const lastSourceIndex = computed(() => sources.value.length - 1) if (props.preload) { - const link: any = { - rel: "preload", - as: "image", - imagesrcset: sources.value[lastSourceIndex.value].srcset, - }; - - if (sources.value?.[lastSourceIndex.value]?.sizes) { - link.imagesizes = sources.value[lastSourceIndex.value].sizes; - } + const link: any = { rel: 'preload', as: 'image', imagesrcset: sources.value[lastSourceIndex.value].srcset } + + if (sources.value?.[lastSourceIndex.value]?.sizes) { link.imagesizes = sources.value[lastSourceIndex.value].sizes } - useHead({ link: [link] }); + useHead({ link: [link] }) } // Only passdown supported attributes - const imgAttrs: Record = { - ...props.imgAttrs, - "data-nuxt-pic": "", - }; + const imgAttrs: Record = { ...props.imgAttrs, 'data-nuxt-pic': '' } for (const key in ctx.attrs) { if (key in baseImageProps && !(key in imgAttrs)) { - imgAttrs[key] = ctx.attrs[key]; + imgAttrs[key] = ctx.attrs[key] } } - const imgEl = ref(); + const imgEl = ref() // Prerender static images if (process.server && process.env.prerender) { for (const src of sources.value as Source[]) { - prerenderStaticImages(src.src, src.srcset); + prerenderStaticImages(src.src, src.srcset) } } - const nuxtApp = useNuxtApp(); - const initialLoad = nuxtApp.isHydrating; + const nuxtApp = useNuxtApp() + const initialLoad = nuxtApp.isHydrating onMounted(() => { - if (!imgEl.value) { - return; - } + if (!imgEl.value) { return } - if ( - imgEl.value.complete && - initialLoad && - !imgEl.value.getAttribute("data-error") - ) { - ctx.emit("load", new Event("load")); + if (imgEl.value.complete && initialLoad && !imgEl.value.getAttribute('data-error')) { + ctx.emit('load', new Event('load')) } imgEl.value.onload = (event) => { - ctx.emit("load", event); - }; - }); + ctx.emit('load', event) + } + }) return () => - h("picture", { key: sources.value[0].src }, [ + h('picture', { key: sources.value[0].src }, [ ...sources.value.slice(0, -1).map((source) => { - return h("source", { + return h('source', { type: source.type, sizes: source.sizes, srcset: source.srcset, - src: source.src, - }); + src: source.src + }) }), - h("img", { + h('img', { ref: imgEl, ..._base.attrs.value, - ...(process.server - ? { onerror: "this.setAttribute('data-error', 1)" } - : {}), + ...(process.server ? { onerror: "this.setAttribute('data-error', 1)" } : {}), ...imgAttrs, src: sources.value[lastSourceIndex.value].src, sizes: sources.value[lastSourceIndex.value].sizes, - srcset: sources.value[lastSourceIndex.value].srcset, - }), - ]); - }, -}); + srcset: sources.value[lastSourceIndex.value].srcset + }) + ]) + } +}) From 026c567def08572d38cbbdba71605753f1b137ff Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Wed, 7 Jun 2023 22:09:34 +0200 Subject: [PATCH 19/36] test: Add tests for multiple formats --- docs/content/3.components/2.nuxt-picture.md | 2 +- test/unit/picture.test.ts | 41 +++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/docs/content/3.components/2.nuxt-picture.md b/docs/content/3.components/2.nuxt-picture.md index 7a110f529..62512b0dd 100644 --- a/docs/content/3.components/2.nuxt-picture.md +++ b/docs/content/3.components/2.nuxt-picture.md @@ -22,7 +22,7 @@ Format on pictures can be used to serve images in a multiple formats. A legacy f ``` -Available formats are `webp`, `avif`, `jpeg`, `jpg`, `png`, `gif` and `svg`. If the format is not specified, it will respect the default image format. +Available formats are `webp`, `avif`, `jpeg`, `jpg`, `png` and `gif`. If the format is not specified, it will respect the default image format. ### `legacyFormat` diff --git a/test/unit/picture.test.ts b/test/unit/picture.test.ts index 77693f386..a4f88e12d 100644 --- a/test/unit/picture.test.ts +++ b/test/unit/picture.test.ts @@ -64,6 +64,47 @@ describe('Renders simple image', () => { expect(wrapper.find('[type="image/webp"]').exists()).toBe(true) }) + it('renders single format and fallback', () => { + const img = mount(NuxtPicture, { + propsData: { + width: 200, + height: 200, + format: 'avif', + src + } + }) + expect(img.find('[type="image/avif"]').exists()).toBe(true) + expect(img.find('[type="image/png"]').exists()).toBe(true) + }) + + it('renders avif, webp and fallback', () => { + const img = mount(NuxtPicture, { + propsData: { + width: 200, + height: 200, + format: 'avif,webp', + src + } + }) + expect(img.find('[type="image/avif"]').exists()).toBe(true) + expect(img.find('[type="image/webp"]').exists()).toBe(true) + expect(img.find('[type="image/png"]').exists()).toBe(true) + }) + + it('renders avif, gif and fallback', () => { + const img = mount(NuxtPicture, { + propsData: { + width: 200, + height: 200, + format: 'avif', + src + } + }) + expect(img.find('[type="image/avif"]').exists()).toBe(true) + expect(img.find('[type="image/gif"]').exists()).toBe(true) + expect(img.find('[type="image/png"]').exists()).toBe(true) + }) + it('props.src is reactive', async () => { const newSource = '/image.jpeg' wrapper.setProps({ src: newSource }) From 6c965d88fe4084f829595cccccfd058cc6c70b31 Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Wed, 7 Jun 2023 22:21:05 +0200 Subject: [PATCH 20/36] fix: --- playground/pages/picture.vue | 2 +- src/runtime/components/nuxt-picture.ts | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/playground/pages/picture.vue b/playground/pages/picture.vue index 64d32cfc4..c36c71f5e 100644 --- a/playground/pages/picture.vue +++ b/playground/pages/picture.vue @@ -1,6 +1,6 @@ diff --git a/src/runtime/components/nuxt-picture.ts b/src/runtime/components/nuxt-picture.ts index 278c633fb..808987b9b 100644 --- a/src/runtime/components/nuxt-picture.ts +++ b/src/runtime/components/nuxt-picture.ts @@ -43,9 +43,6 @@ export default defineComponent({ } return formats.map((format) => { - if (format === 'svg') { - return { src: props.src } - } const { srcset, sizes, src } = $img.getSizes(props.src!, { ..._base.options.value, sizes: props.sizes || $img.options.screens, From b8974d50ae45969c96953a77107616433cd22a2c Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Wed, 7 Jun 2023 22:21:48 +0200 Subject: [PATCH 21/36] fix: remove invalid format --- playground/pages/picture.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playground/pages/picture.vue b/playground/pages/picture.vue index c36c71f5e..64d32cfc4 100644 --- a/playground/pages/picture.vue +++ b/playground/pages/picture.vue @@ -1,6 +1,6 @@ From 43702c51789d98c70d2725145e7012ddf49c4b35 Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer <40826752+marcelxpfeifer@users.noreply.github.com> Date: Wed, 7 Jun 2023 22:46:56 +0200 Subject: [PATCH 22/36] Update docs/content/3.components/2.nuxt-picture.md Co-authored-by: Daniel Roe --- docs/content/3.components/2.nuxt-picture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/3.components/2.nuxt-picture.md b/docs/content/3.components/2.nuxt-picture.md index 62512b0dd..73c473f07 100644 --- a/docs/content/3.components/2.nuxt-picture.md +++ b/docs/content/3.components/2.nuxt-picture.md @@ -16,7 +16,7 @@ Learn more about the [`` tag on MDN](https://developer.mozilla.org/en-U ### `format` -Format on pictures can be used to serve images in a multiple formats. A legacy format will be generated automaticlly. So in the example below avif, webp and png would be generated. They will be added in the same order they are added to the format attribute. +Format on pictures can be used to serve images in multiple formats. A legacy format will be generated automatically. So in the example below avif, webp and png would be generated. They will be added in the same order they are added to the format attribute. ```html From d83f3ed5b35025a39f3811304e519f0330490c3d Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Wed, 7 Jun 2023 22:55:07 +0200 Subject: [PATCH 23/36] fix: preload most desired image --- src/runtime/components/nuxt-picture.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/runtime/components/nuxt-picture.ts b/src/runtime/components/nuxt-picture.ts index 808987b9b..4a33b33d2 100644 --- a/src/runtime/components/nuxt-picture.ts +++ b/src/runtime/components/nuxt-picture.ts @@ -55,9 +55,9 @@ export default defineComponent({ const lastSourceIndex = computed(() => sources.value.length - 1) if (props.preload) { - const link: any = { rel: 'preload', as: 'image', imagesrcset: sources.value[lastSourceIndex.value].srcset } + const link: any = { rel: 'preload', as: 'image', imagesrcset: sources.value[0].srcset } - if (sources.value?.[lastSourceIndex.value]?.sizes) { link.imagesizes = sources.value[lastSourceIndex.value].sizes } + if (sources.value?.[0]?.sizes) { link.imagesizes = sources.value[0].sizes } useHead({ link: [link] }) } From 9002dfe5661eeece22d8314051dd176aaab7b549 Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Wed, 7 Jun 2023 23:53:34 +0200 Subject: [PATCH 24/36] fix: remove src from source tag --- src/runtime/components/nuxt-picture.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/runtime/components/nuxt-picture.ts b/src/runtime/components/nuxt-picture.ts index 4a33b33d2..d1c366de6 100644 --- a/src/runtime/components/nuxt-picture.ts +++ b/src/runtime/components/nuxt-picture.ts @@ -98,8 +98,7 @@ export default defineComponent({ return h('source', { type: source.type, sizes: source.sizes, - srcset: source.srcset, - src: source.src + srcset: source.srcset }) }), h('img', { From 765388babddb10b48ec18b5f2ebb8c8c3eebfaea Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Thu, 8 Jun 2023 00:03:50 +0200 Subject: [PATCH 25/36] test: --- test/unit/picture.test.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/unit/picture.test.ts b/test/unit/picture.test.ts index a4f88e12d..e97fcbadb 100644 --- a/test/unit/picture.test.ts +++ b/test/unit/picture.test.ts @@ -64,7 +64,7 @@ describe('Renders simple image', () => { expect(wrapper.find('[type="image/webp"]').exists()).toBe(true) }) - it('renders single format and fallback', () => { + it('renders single format and fallback image tag', () => { const img = mount(NuxtPicture, { propsData: { width: 200, @@ -73,11 +73,11 @@ describe('Renders simple image', () => { src } }) - expect(img.find('[type="image/avif"]').exists()).toBe(true) - expect(img.find('[type="image/png"]').exists()).toBe(true) + expect(img.find('source[type="image/avif"]').exists()).toBe(true); + expect(img.find('img').exists()).toBe(true) }) - it('renders avif, webp and fallback', () => { + it('renders avif, webp and fallback image tag', () => { const img = mount(NuxtPicture, { propsData: { width: 200, @@ -86,23 +86,23 @@ describe('Renders simple image', () => { src } }) - expect(img.find('[type="image/avif"]').exists()).toBe(true) - expect(img.find('[type="image/webp"]').exists()).toBe(true) - expect(img.find('[type="image/png"]').exists()).toBe(true) + expect(img.find('source[type="image/avif"]').exists()).toBe(true); + expect(img.find('source[type="image/webp"]').exists()).toBe(true); + expect(img.find('img').exists()).toBe(true) }) - it('renders avif, gif and fallback', () => { + it('renders avif, gif and fallback image tag', () => { const img = mount(NuxtPicture, { propsData: { width: 200, height: 200, - format: 'avif', + format: 'avif,gif', src } }) - expect(img.find('[type="image/avif"]').exists()).toBe(true) - expect(img.find('[type="image/gif"]').exists()).toBe(true) - expect(img.find('[type="image/png"]').exists()).toBe(true) + expect(img.find('source[type="image/avif"]').exists()).toBe(true); + expect(img.find('source[type="image/gif"]').exists()).toBe(true); + expect(img.find('img').exists()).toBe(true) }) it('props.src is reactive', async () => { From 19b0c879eb39e57b725cac5f8acc12d23d169746 Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Thu, 8 Jun 2023 00:05:50 +0200 Subject: [PATCH 26/36] style: liniting error --- test/unit/picture.test.ts | 201 ++++++++++++++++++++------------------ 1 file changed, 104 insertions(+), 97 deletions(-) diff --git a/test/unit/picture.test.ts b/test/unit/picture.test.ts index e97fcbadb..434760961 100644 --- a/test/unit/picture.test.ts +++ b/test/unit/picture.test.ts @@ -1,150 +1,157 @@ // @vitest-environment nuxt -import { beforeEach, describe, it, expect } from 'vitest' -import { VueWrapper, mount } from '@vue/test-utils' -import { NuxtPicture } from '#components' +import { NuxtPicture } from "#components"; +import { VueWrapper, mount } from "@vue/test-utils"; +import { beforeEach, describe, expect, it } from "vitest"; -describe('Renders simple image', () => { - let wrapper: VueWrapper - const src = '/image.png' +describe("Renders simple image", () => { + let wrapper: VueWrapper; + const src = "/image.png"; const observer = { wasAdded: false, - wasDestroyed: false - } + wasDestroyed: false, + }; beforeEach(() => { window.IntersectionObserver = class IntersectionObserver { - root: any - rootMargin: any - thresholds: any - takeRecords: any + root: any; + rootMargin: any; + thresholds: any; + takeRecords: any; - observe (_target: Element) { - observer.wasAdded = true + observe(_target: Element) { + observer.wasAdded = true; } - disconnect () { - observer.wasDestroyed = true + disconnect() { + observer.wasDestroyed = true; } - unobserve () { - observer.wasDestroyed = true + unobserve() { + observer.wasDestroyed = true; } - } + }; wrapper = mount(NuxtPicture, { propsData: { - loading: 'lazy', + loading: "lazy", width: 200, height: 200, - sizes: '200,500:500,900:900', - src - } - }) - }) + sizes: "200,500:500,900:900", + src, + }, + }); + }); - it('Matches snapshot', () => { + it("Matches snapshot", () => { expect(wrapper.html()).toMatchInlineSnapshot(` " " - `) - }) - - it.todo('alt attribute is generated') - - it('props.src is picked up by getImage()', () => { - [['source', 'srcset', '/_ipx/f_webp&s_500x500/image.png'], ['img', 'src']].forEach(([element, attribute, customSrc]) => { - const domSrc = wrapper.find(element).element.getAttribute(attribute) - expect(domSrc).toContain(customSrc || src) - }) - }) - - it('renders webp image source', () => { - expect(wrapper.find('[type="image/webp"]').exists()).toBe(true) - }) - - it('renders single format and fallback image tag', () => { + `); + }); + + it.todo("alt attribute is generated"); + + it("props.src is picked up by getImage()", () => { + [ + ["source", "srcset", "/_ipx/f_webp&s_500x500/image.png"], + ["img", "src"], + ].forEach(([element, attribute, customSrc]) => { + const domSrc = wrapper.find(element).element.getAttribute(attribute); + expect(domSrc).toContain(customSrc || src); + }); + }); + + it("renders webp image source", () => { + expect(wrapper.find('[type="image/webp"]').exists()).toBe(true); + }); + + it("renders single format and fallback image tag", () => { const img = mount(NuxtPicture, { propsData: { width: 200, height: 200, - format: 'avif', - src - } - }) + format: "avif", + src, + }, + }); expect(img.find('source[type="image/avif"]').exists()).toBe(true); - expect(img.find('img').exists()).toBe(true) - }) + expect(img.find("img").exists()).toBe(true); + }); - it('renders avif, webp and fallback image tag', () => { + it("renders avif, webp and fallback image tag", () => { const img = mount(NuxtPicture, { propsData: { width: 200, height: 200, - format: 'avif,webp', - src - } - }) + format: "avif,webp", + src, + }, + }); expect(img.find('source[type="image/avif"]').exists()).toBe(true); expect(img.find('source[type="image/webp"]').exists()).toBe(true); - expect(img.find('img').exists()).toBe(true) - }) + expect(img.find("img").exists()).toBe(true); + }); - it('renders avif, gif and fallback image tag', () => { + it("renders avif, gif and fallback image tag", () => { const img = mount(NuxtPicture, { propsData: { width: 200, height: 200, - format: 'avif,gif', - src - } - }) + format: "avif,gif", + src, + }, + }); expect(img.find('source[type="image/avif"]').exists()).toBe(true); expect(img.find('source[type="image/gif"]').exists()).toBe(true); - expect(img.find('img').exists()).toBe(true) - }) - - it('props.src is reactive', async () => { - const newSource = '/image.jpeg' - wrapper.setProps({ src: newSource }) - - await nextTick() - - ;[['source', 'srcset', '/_ipx/f_webp&s_500x500/image.jpeg'], ['img', 'src']].forEach(([element, attribute, src]) => { - const domSrc = wrapper.find(element).element.getAttribute(attribute) - expect(domSrc).toContain(src || newSource) - }) - }) - - it('sizes', () => { - const sizes = wrapper.find('source').element.getAttribute('sizes') - expect(sizes).toBe('(max-width: 500px) 500px, 900px') - }) - - it('renders src when svg is passed', () => { + expect(img.find("img").exists()).toBe(true); + }); + + it("props.src is reactive", async () => { + const newSource = "/image.jpeg"; + wrapper.setProps({ src: newSource }); + + await nextTick(); + [ + ["source", "srcset", "/_ipx/f_webp&s_500x500/image.jpeg"], + ["img", "src"], + ].forEach(([element, attribute, src]) => { + const domSrc = wrapper.find(element).element.getAttribute(attribute); + expect(domSrc).toContain(src || newSource); + }); + }); + + it("sizes", () => { + const sizes = wrapper.find("source").element.getAttribute("sizes"); + expect(sizes).toBe("(max-width: 500px) 500px, 900px"); + }); + + it("renders src when svg is passed", () => { const wrapper = mount(NuxtPicture, { propsData: { - src: '/image.svg' - } - }) - expect(wrapper.html()).toMatchInlineSnapshot('""') - }) - - it('encodes characters', () => { + src: "/image.svg", + }, + }); + expect(wrapper.html()).toMatchInlineSnapshot( + '""' + ); + }); + + it("encodes characters", () => { const img = mount(NuxtPicture, { propsData: { - loading: 'lazy', + loading: "lazy", width: 200, height: 200, - sizes: '200,500:500,900:900', - src: '/汉字.png' - } - }) + sizes: "200,500:500,900:900", + src: "/汉字.png", + }, + }); expect(img.html()).toMatchInlineSnapshot(` " " - `) - }) -}) + `); + }); +}); From f0326587b2fe9928489ef0415793c693c122a036 Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Thu, 8 Jun 2023 00:06:32 +0200 Subject: [PATCH 27/36] style: linting --- test/unit/picture.test.ts | 215 ++++++++++++++++++-------------------- 1 file changed, 104 insertions(+), 111 deletions(-) diff --git a/test/unit/picture.test.ts b/test/unit/picture.test.ts index 434760961..4e4f7a396 100644 --- a/test/unit/picture.test.ts +++ b/test/unit/picture.test.ts @@ -1,157 +1,150 @@ // @vitest-environment nuxt -import { NuxtPicture } from "#components"; -import { VueWrapper, mount } from "@vue/test-utils"; -import { beforeEach, describe, expect, it } from "vitest"; +import { NuxtPicture } from '#components' +import { VueWrapper, mount } from '@vue/test-utils' +import { beforeEach, describe, expect, it } from 'vitest' -describe("Renders simple image", () => { - let wrapper: VueWrapper; - const src = "/image.png"; +describe('Renders simple image', () => { + let wrapper: VueWrapper + const src = '/image.png' const observer = { wasAdded: false, - wasDestroyed: false, - }; + wasDestroyed: false + } beforeEach(() => { window.IntersectionObserver = class IntersectionObserver { - root: any; - rootMargin: any; - thresholds: any; - takeRecords: any; + root: any + rootMargin: any + thresholds: any + takeRecords: any - observe(_target: Element) { - observer.wasAdded = true; + observe (_target: Element) { + observer.wasAdded = true } - disconnect() { - observer.wasDestroyed = true; + disconnect () { + observer.wasDestroyed = true } - unobserve() { - observer.wasDestroyed = true; + unobserve () { + observer.wasDestroyed = true } - }; + } wrapper = mount(NuxtPicture, { propsData: { - loading: "lazy", + loading: 'lazy', width: 200, height: 200, - sizes: "200,500:500,900:900", - src, - }, - }); - }); + sizes: '200,500:500,900:900', + src + } + }) + }) - it("Matches snapshot", () => { + it('Matches snapshot', () => { expect(wrapper.html()).toMatchInlineSnapshot(` " " - `); - }); - - it.todo("alt attribute is generated"); - - it("props.src is picked up by getImage()", () => { - [ - ["source", "srcset", "/_ipx/f_webp&s_500x500/image.png"], - ["img", "src"], - ].forEach(([element, attribute, customSrc]) => { - const domSrc = wrapper.find(element).element.getAttribute(attribute); - expect(domSrc).toContain(customSrc || src); - }); - }); - - it("renders webp image source", () => { - expect(wrapper.find('[type="image/webp"]').exists()).toBe(true); - }); - - it("renders single format and fallback image tag", () => { + `) + }) + + it.todo('alt attribute is generated') + + it('props.src is picked up by getImage()', () => { + [['source', 'srcset', '/_ipx/f_webp&s_500x500/image.png'], ['img', 'src']].forEach(([element, attribute, customSrc]) => { + const domSrc = wrapper.find(element).element.getAttribute(attribute) + expect(domSrc).toContain(customSrc || src) + }) + }) + + it('renders webp image source', () => { + expect(wrapper.find('[type="image/webp"]').exists()).toBe(true) + }) + + it('renders single format and fallback image tag', () => { const img = mount(NuxtPicture, { propsData: { width: 200, height: 200, - format: "avif", - src, - }, - }); - expect(img.find('source[type="image/avif"]').exists()).toBe(true); - expect(img.find("img").exists()).toBe(true); - }); - - it("renders avif, webp and fallback image tag", () => { + format: 'avif', + src + } + }) + expect(img.find('source[type="image/avif"]').exists()).toBe(true) + expect(img.find('img').exists()).toBe(true) + }) + + it('renders avif, webp and fallback image tag', () => { const img = mount(NuxtPicture, { propsData: { width: 200, height: 200, - format: "avif,webp", - src, - }, - }); - expect(img.find('source[type="image/avif"]').exists()).toBe(true); - expect(img.find('source[type="image/webp"]').exists()).toBe(true); - expect(img.find("img").exists()).toBe(true); - }); - - it("renders avif, gif and fallback image tag", () => { + format: 'avif,webp', + src + } + }) + expect(img.find('source[type="image/avif"]').exists()).toBe(true) + expect(img.find('source[type="image/webp"]').exists()).toBe(true) + expect(img.find('img').exists()).toBe(true) + }) + + it('renders avif, gif and fallback image tag', () => { const img = mount(NuxtPicture, { propsData: { width: 200, height: 200, - format: "avif,gif", - src, - }, - }); - expect(img.find('source[type="image/avif"]').exists()).toBe(true); - expect(img.find('source[type="image/gif"]').exists()).toBe(true); - expect(img.find("img").exists()).toBe(true); - }); - - it("props.src is reactive", async () => { - const newSource = "/image.jpeg"; - wrapper.setProps({ src: newSource }); - - await nextTick(); - [ - ["source", "srcset", "/_ipx/f_webp&s_500x500/image.jpeg"], - ["img", "src"], - ].forEach(([element, attribute, src]) => { - const domSrc = wrapper.find(element).element.getAttribute(attribute); - expect(domSrc).toContain(src || newSource); - }); - }); - - it("sizes", () => { - const sizes = wrapper.find("source").element.getAttribute("sizes"); - expect(sizes).toBe("(max-width: 500px) 500px, 900px"); - }); - - it("renders src when svg is passed", () => { + format: 'avif,gif', + src + } + }) + expect(img.find('source[type="image/avif"]').exists()).toBe(true) + expect(img.find('source[type="image/gif"]').exists()).toBe(true) + expect(img.find('img').exists()).toBe(true) + }) + + it('props.src is reactive', async () => { + const newSource = '/image.jpeg' + wrapper.setProps({ src: newSource }) + + await nextTick() + + ;[['source', 'srcset', '/_ipx/f_webp&s_500x500/image.jpeg'], ['img', 'src']].forEach(([element, attribute, src]) => { + const domSrc = wrapper.find(element).element.getAttribute(attribute) + expect(domSrc).toContain(src || newSource) + }) + }) + + it('sizes', () => { + const sizes = wrapper.find('source').element.getAttribute('sizes') + expect(sizes).toBe('(max-width: 500px) 500px, 900px') + }) + + it('renders src when svg is passed', () => { const wrapper = mount(NuxtPicture, { propsData: { - src: "/image.svg", - }, - }); - expect(wrapper.html()).toMatchInlineSnapshot( - '""' - ); - }); - - it("encodes characters", () => { + src: '/image.svg' + } + }) + expect(wrapper.html()).toMatchInlineSnapshot('""') + }) + + it('encodes characters', () => { const img = mount(NuxtPicture, { propsData: { - loading: "lazy", + loading: 'lazy', width: 200, height: 200, - sizes: "200,500:500,900:900", - src: "/汉字.png", - }, - }); + sizes: '200,500:500,900:900', + src: '/汉字.png' + } + }) expect(img.html()).toMatchInlineSnapshot(` " " - `); - }); -}); + `) + }) +}) From 714f3a276d42f1cc38ec91ee3e275d438d880c43 Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Thu, 8 Jun 2023 00:07:53 +0200 Subject: [PATCH 28/36] style: linting --- test/unit/picture.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/picture.test.ts b/test/unit/picture.test.ts index 4e4f7a396..5c0c7a80c 100644 --- a/test/unit/picture.test.ts +++ b/test/unit/picture.test.ts @@ -1,8 +1,8 @@ // @vitest-environment nuxt -import { NuxtPicture } from '#components' import { VueWrapper, mount } from '@vue/test-utils' import { beforeEach, describe, expect, it } from 'vitest' +import { NuxtPicture } from '#components' describe('Renders simple image', () => { let wrapper: VueWrapper From cdcd1a7f058471600c0f54a1ee9516fa74d39bfd Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Thu, 22 Jun 2023 00:18:26 +0200 Subject: [PATCH 29/36] merge --- src/runtime/components/nuxt-picture.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/runtime/components/nuxt-picture.ts b/src/runtime/components/nuxt-picture.ts index 5a96db624..afb446d08 100644 --- a/src/runtime/components/nuxt-picture.ts +++ b/src/runtime/components/nuxt-picture.ts @@ -18,14 +18,8 @@ export default defineComponent({ const $img = useImage() const _base = useBaseImage(props) -<<<<<<< HEAD - const isTransparent = computed(() => ['png', 'webp', 'gif', 'svg'].includes(originalFormat.value)) - - const originalFormat = computed(() => getFileExtension(props.src)) -======= const originalFormat = computed(() => getFileExtension(props.src)) const isTransparent = computed(() => ['png', 'webp', 'gif', 'svg'].includes(originalFormat.value)) ->>>>>>> upstream/main const legacyFormat = computed(() => { if (props.legacyFormat) { return props.legacyFormat } @@ -34,14 +28,8 @@ export default defineComponent({ type Source = { srcset?: string, src?: string, type?: string, sizes?: string } const sources = computed(() => { -<<<<<<< HEAD - const format = props.format || (originalFormat.value === 'svg' ? 'svg' : 'webp') - const formats = format.split(',') - if (format === 'svg') { -======= const formats = props.format?.split(',') || (originalFormat.value === 'svg' ? ['svg'] : ($img.options.format?.length ? [...$img.options.format] : ['webp'])) if (formats[0] === 'svg') { ->>>>>>> upstream/main return [{ src: props.src }] } @@ -103,11 +91,7 @@ export default defineComponent({ }) return () => -<<<<<<< HEAD - h('picture', { key: sources.value[0].src }, [ -======= h('picture', null, [ ->>>>>>> upstream/main ...sources.value.slice(0, -1).map((source) => { return h('source', { type: source.type, From bf1fae6bde70fa8c9a1209a33d65dabebcfe5b54 Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Thu, 22 Jun 2023 00:59:00 +0200 Subject: [PATCH 30/36] feat: :sparkles: Allow generation of images via array --- docs/content/3.components/2.nuxt-picture.md | 15 ++++++++++++--- src/runtime/components/nuxt-picture.ts | 3 ++- test/unit/picture.test.ts | 18 ++++++++++++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/docs/content/3.components/2.nuxt-picture.md b/docs/content/3.components/2.nuxt-picture.md index 73c473f07..ec55d8f99 100644 --- a/docs/content/3.components/2.nuxt-picture.md +++ b/docs/content/3.components/2.nuxt-picture.md @@ -16,13 +16,22 @@ Learn more about the [`` tag on MDN](https://developer.mozilla.org/en-U ### `format` -Format on pictures can be used to serve images in multiple formats. A legacy format will be generated automatically. So in the example below avif, webp and png would be generated. They will be added in the same order they are added to the format attribute. +You can use this option to configure the format of your images used by . The available formats are webp, avif, jpeg, jpg, png, and gif. The order of the formats is important, as the first format that is supported by the browser will be used. You can pass multiple values like ['avif', 'webp']. + +A legacy format will be generated automatically. They will be added in the order of the formats array, the legacy format will be added last. ```html - +// generates avif, webp and png + + +// generates avif, webp and jepg + + +// generates wepb and png + ``` -Available formats are `webp`, `avif`, `jpeg`, `jpg`, `png` and `gif`. If the format is not specified, it will respect the default image format. +If the format is not specified, it will respect the default image format. ### `legacyFormat` diff --git a/src/runtime/components/nuxt-picture.ts b/src/runtime/components/nuxt-picture.ts index afb446d08..f411f759a 100644 --- a/src/runtime/components/nuxt-picture.ts +++ b/src/runtime/components/nuxt-picture.ts @@ -6,6 +6,7 @@ import { getFileExtension } from '#image' export const pictureProps = { ...baseImageProps, + format: { type: [String, Array], default: undefined }, legacyFormat: { type: String, default: null }, imgAttrs: { type: Object, default: null } } @@ -28,7 +29,7 @@ export default defineComponent({ type Source = { srcset?: string, src?: string, type?: string, sizes?: string } const sources = computed(() => { - const formats = props.format?.split(',') || (originalFormat.value === 'svg' ? ['svg'] : ($img.options.format?.length ? [...$img.options.format] : ['webp'])) + const formats = (Array.isArray(props.format) ? props.format : props.format?.split(',') || (originalFormat.value === 'svg' ? ['svg'] : ($img.options.format?.length ? [...$img.options.format] : ['webp']))) as string[] if (formats[0] === 'svg') { return [{ src: props.src }] } diff --git a/test/unit/picture.test.ts b/test/unit/picture.test.ts index 1932ddecf..5c77db210 100644 --- a/test/unit/picture.test.ts +++ b/test/unit/picture.test.ts @@ -78,6 +78,7 @@ describe('Renders simple image', () => { } }) expect(img.find('source[type="image/avif"]').exists()).toBe(true) + expect(img.findAll('source').length).toBe(1) expect(img.find('img').exists()).toBe(true) }) @@ -92,6 +93,7 @@ describe('Renders simple image', () => { }) expect(img.find('source[type="image/avif"]').exists()).toBe(true) expect(img.find('source[type="image/webp"]').exists()).toBe(true) + expect(img.findAll('source').length).toBe(2) expect(img.find('img').exists()).toBe(true) }) @@ -106,6 +108,22 @@ describe('Renders simple image', () => { }) expect(img.find('source[type="image/avif"]').exists()).toBe(true) expect(img.find('source[type="image/gif"]').exists()).toBe(true) + expect(img.findAll('source').length).toBe(2) + expect(img.find('img').exists()).toBe(true) + }) + + it('checks that multiple formats can also be parsed as array', () => { + const img = mount(NuxtPicture, { + propsData: { + width: 200, + height: 200, + format: ['avif', 'webp'], + src + } + }) + expect(img.find('source[type="image/avif"]').exists()).toBe(true) + expect(img.find('source[type="image/webp"]').exists()).toBe(true) + expect(img.findAll('source').length).toBe(2) expect(img.find('img').exists()).toBe(true) }) From 7823b7243d079cb4be3a668a24157c8510501f2c Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Thu, 22 Jun 2023 01:02:51 +0200 Subject: [PATCH 31/36] feat: :sparkles: Update docs --- docs/content/3.components/2.nuxt-picture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/3.components/2.nuxt-picture.md b/docs/content/3.components/2.nuxt-picture.md index ec55d8f99..40bb58997 100644 --- a/docs/content/3.components/2.nuxt-picture.md +++ b/docs/content/3.components/2.nuxt-picture.md @@ -16,7 +16,7 @@ Learn more about the [`` tag on MDN](https://developer.mozilla.org/en-U ### `format` -You can use this option to configure the format of your images used by . The available formats are webp, avif, jpeg, jpg, png, and gif. The order of the formats is important, as the first format that is supported by the browser will be used. You can pass multiple values like ['avif', 'webp']. +You can use this option to configure the format of your images. The available formats are webp, avif, jpeg, jpg, png, and gif. The order of the formats is important, as the first format that is supported by the browser will be used. You can pass multiple values like ['avif', 'webp'] or as a comma-separated string like 'avif,webp'. A legacy format will be generated automatically. They will be added in the order of the formats array, the legacy format will be added last. From 16dffb37ba6ac9ccc786b0ab95c4d02b2a5eb08e Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Thu, 22 Jun 2023 09:40:31 +0200 Subject: [PATCH 32/36] feat: :label: Improve types --- playground/pages/picture.vue | 2 +- src/runtime/components/nuxt-picture.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/playground/pages/picture.vue b/playground/pages/picture.vue index 64d32cfc4..4607b47a8 100644 --- a/playground/pages/picture.vue +++ b/playground/pages/picture.vue @@ -1,6 +1,6 @@ diff --git a/src/runtime/components/nuxt-picture.ts b/src/runtime/components/nuxt-picture.ts index f411f759a..81d9233ff 100644 --- a/src/runtime/components/nuxt-picture.ts +++ b/src/runtime/components/nuxt-picture.ts @@ -6,7 +6,7 @@ import { getFileExtension } from '#image' export const pictureProps = { ...baseImageProps, - format: { type: [String, Array], default: undefined }, + format: { type: [String, Array], default: undefined }, legacyFormat: { type: String, default: null }, imgAttrs: { type: Object, default: null } } From 21a2ae9c6c69554d7ee842846566c7521d13460a Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Tue, 12 Sep 2023 10:53:35 +0200 Subject: [PATCH 33/36] feat: :sparkles: Update docs --- docs/content/2.usage/2.nuxt-picture.md | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/docs/content/2.usage/2.nuxt-picture.md b/docs/content/2.usage/2.nuxt-picture.md index b5a28742b..f0d577bf9 100644 --- a/docs/content/2.usage/2.nuxt-picture.md +++ b/docs/content/2.usage/2.nuxt-picture.md @@ -17,17 +17,34 @@ See props supported by[``](/usage/nuxt-img#props) ### `format` -Format on pictures can be used to serve images in multiple formats. A legacy format will be generated automatically. So in the example below avif, webp and png would be generated. They will be added in the same order they are added to the format attribute. +You can use this option to configure the format of your images. The available formats are webp, avif, jpeg, jpg, png, and gif. The order of the formats is important, as the first format that is supported by the browser will be used. You can pass multiple values like ['avif', 'webp'] or as a comma-separated string like 'avif,webp'. + +A legacy format will be generated automatically. They will be added in the order of the formats array, the legacy format will be added last. ```html - + +// generates avif, webp and jepg + + +// generates wepb and png + ``` -Available formats are `webp`, `avif`, `jpeg`, `jpg`, `png` and `gif`. If the format is not specified, it will respect the default image format. +If the format is not specified, it will respect the default image format. ### `legacyFormat` From ffdeb757200e3800897db3c9acbf8a18daed5ebd Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Tue, 12 Sep 2023 10:59:16 +0200 Subject: [PATCH 34/36] feat: :sparkles: Update tag --- docs/content/2.usage/2.nuxt-picture.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/content/2.usage/2.nuxt-picture.md b/docs/content/2.usage/2.nuxt-picture.md index f0d577bf9..2d4bf5ec9 100644 --- a/docs/content/2.usage/2.nuxt-picture.md +++ b/docs/content/2.usage/2.nuxt-picture.md @@ -23,21 +23,21 @@ A legacy format will be generated automatically. They will be added in the order ```html // generates avif, webp and png - // generates avif, webp and jepg - // generates wepb and png - Date: Tue, 12 Sep 2023 13:47:07 +0200 Subject: [PATCH 35/36] fix: :bug: Failing test --- src/runtime/components/_base.ts | 54 ++++++++++++++++++++++++++ src/runtime/components/nuxt-picture.ts | 5 +-- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/runtime/components/_base.ts b/src/runtime/components/_base.ts index fd6c9235d..362bb50b5 100644 --- a/src/runtime/components/_base.ts +++ b/src/runtime/components/_base.ts @@ -43,6 +43,11 @@ export const baseImageProps = { } } +export const basePictureProps = { + ...baseImageProps, + format: { type: [String, Array] as unknown as () => string | string[], default: undefined } +} + export interface BaseImageAttrs { width?: number height?: number @@ -66,6 +71,10 @@ export interface BaseImageModifiers { [key: string]: any } +export interface BasePictureModifiers extends Omit { + format?: string | string[]; +} + export const useBaseImage = (props: ExtractPropTypes) => { const options = computed(() => { return { @@ -109,3 +118,48 @@ export const useBaseImage = (props: ExtractPropTypes) => modifiers } } + + +export const useBasePicture = (props: ExtractPropTypes) => { + const options = computed(() => { + return { + provider: props.provider, + preset: props.preset + } + }) + + const attrs = computed(() => { + return { + width: parseSize(props.width), + height: parseSize(props.height), + alt: props.alt, + referrerpolicy: props.referrerpolicy, + usemap: props.usemap, + longdesc: props.longdesc, + ismap: props.ismap, + crossorigin: props.crossorigin === true ? 'anonymous' : props.crossorigin || undefined, + loading: props.loading, + decoding: props.decoding + } + }) + + const $img = useImage() + + const modifiers = computed(() => { + return { + ...props.modifiers, + width: parseSize(props.width), + height: parseSize(props.height), + format: props.format, + quality: props.quality || $img.options.quality, + background: props.background, + fit: props.fit + } + }) + + return { + options, + attrs, + modifiers + } +} \ No newline at end of file diff --git a/src/runtime/components/nuxt-picture.ts b/src/runtime/components/nuxt-picture.ts index 6657088f8..9a0cece6d 100644 --- a/src/runtime/components/nuxt-picture.ts +++ b/src/runtime/components/nuxt-picture.ts @@ -1,6 +1,6 @@ import { h, defineComponent, ref, computed, onMounted } from 'vue' import { prerenderStaticImages } from '../utils/prerender' -import { useBaseImage, baseImageProps } from './_base' +import { baseImageProps, useBasePicture } from './_base' import { useImage, useHead, useNuxtApp } from '#imports' import { getFileExtension } from '#image' @@ -17,7 +17,7 @@ export default defineComponent({ emits: ['load'], setup: (props, ctx) => { const $img = useImage() - const _base = useBaseImage(props) + const _base = useBasePicture(props) const originalFormat = computed(() => getFileExtension(props.src)) const isTransparent = computed(() => ['png', 'webp', 'gif', 'svg'].includes(originalFormat.value)) @@ -45,7 +45,6 @@ export default defineComponent({ const { srcset, sizes, src } = $img.getSizes(props.src!, { ..._base.options.value, sizes: props.sizes || $img.options.screens, - densities: props.densities, modifiers: { ..._base.modifiers.value, format } }) From 4f36dd4e01a112dfaa06737225c9d3eb3b344cf4 Mon Sep 17 00:00:00 2001 From: Marcel Pfeifer Date: Tue, 12 Sep 2023 13:47:39 +0200 Subject: [PATCH 36/36] fix: :lipstick: Linting --- src/runtime/components/_base.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/runtime/components/_base.ts b/src/runtime/components/_base.ts index 362bb50b5..4cd8b688f 100644 --- a/src/runtime/components/_base.ts +++ b/src/runtime/components/_base.ts @@ -119,7 +119,6 @@ export const useBaseImage = (props: ExtractPropTypes) => } } - export const useBasePicture = (props: ExtractPropTypes) => { const options = computed(() => { return { @@ -162,4 +161,4 @@ export const useBasePicture = (props: ExtractPropTypes) attrs, modifiers } -} \ No newline at end of file +}