-
Notifications
You must be signed in to change notification settings - Fork 272
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
feat: support densities
for devices with DevicePixelRatio
> 1
#769
Conversation
👷 Deploy request for nuxt-image pending review.Visit the deploys page to approve it
|
✅ Live Preview ready!
|
…(revert default files)
I could really use this feature. Would love to see this merged and released! |
# Conflicts: # src/types/image.ts
Thank you! Fixed the conflict so that you can easily merge |
Big thanks for this pr, i write something similar #803, but your solution more cleanest
<nuxt-img
src="/logos/nuxt.png"
sizes="sm:200px lg:400px"
densities="1x 2x 3x"
/>
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you be able to have a look at the failing tests?
I also think we can default to [1, 2]
as default densities.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All proposed code changes should fix the implementation, I also have adjusted tests, not sure how to propose changes via PR suggestions when the files have not been changed yet..
src/runtime/image.ts
Outdated
|
||
function getDensitySet (ctx: ImageCTX, input: string, opts: ImageSizesOptions): string|undefined { | ||
const srcSet :{ density: string, src: string }[] = [] | ||
let densities = opts.densities ? parseDensities(opts.densities) : ctx.options.densities |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let densities = opts.densities ? parseDensities(opts.densities) : ctx.options.densities | |
// let densities = opts.densities ? parseDensities(opts.densities) : ctx.options.densities | |
let densities = opts.densities ? parseDensities(opts.densities) : (ctx.options.densities.length ? ctx.options.densities : [1, 2]) // TODO: pls help, default should come from `ctx.options.densities` ?!? |
also to fix existing tests, I have made following changes:
import { fileURLToPath } from 'node:url'
import { describe, it, expect } from 'vitest'
import { setup, useTestContext } from '@nuxt/test-utils'
import { useNuxt } from '@nuxt/kit'
import { resolve } from 'pathe'
import { globby } from 'globby'
await setup({
rootDir: fileURLToPath(new URL('../../playground', import.meta.url)),
build: true,
nuxtConfig: {
image: {
inject: false
},
hooks: {
'modules:before' () {
const nuxt = useNuxt()
nuxt.options.nitro.prerender = { routes: ['/provider/ipx'] }
}
}
}
})
describe('ipx provider', () => {
it('generates static files', async () => {
const ctx = useTestContext()
const outputDir = resolve(ctx.nuxt!.options.nitro.output?.dir || '', 'public/_ipx')
const files = await globby(outputDir)
expect(files.sort().map(f => f.replace(outputDir, '/_ipx'))).toMatchInlineSnapshot(`
[
"/_ipx/_/images/nuxt.png",
"/_ipx/s_300x300/images/colors.jpg",
"/_ipx/s_300x300/images/everest.jpg",
"/_ipx/s_300x300/images/tacos.svg",
"/_ipx/s_300x300/unsplash/photo-1606112219348-204d7d8b94ee",
"/_ipx/s_600x600/images/colors.jpg",
"/_ipx/s_600x600/images/everest.jpg",
"/_ipx/s_600x600/images/tacos.svg",
"/_ipx/s_600x600/unsplash/photo-1606112219348-204d7d8b94ee",
]
`)
})
})
// @vitest-environment nuxt
import { beforeEach, describe, it, expect } from 'vitest'
import { ComponentMountingOptions, VueWrapper, mount } from '@vue/test-utils'
import { NuxtImg } from '#components'
describe('Renders simple image', () => {
let wrapper: VueWrapper<any>
const src = '/image.png'
beforeEach(() => {
wrapper = mountImage({
width: 200,
height: 200,
sizes: '200,500:500,900:900',
src
})
})
it('Matches snapshot', () => {
expect(wrapper.html()).toMatchInlineSnapshot('"<img src=\\"/_ipx/s_1800x1800/image.png\\" width=\\"200\\" height=\\"200\\" data-nuxt-img=\\"\\" sizes=\\"(max-width: 500px) 500px, 900px\\" srcset=\\"/_ipx/s_500x500/image.png 500w, /_ipx/s_900x900/image.png 900w, /_ipx/s_1000x1000/image.png 1000w, /_ipx/s_1800x1800/image.png 1800w\\">"')
})
it('props.src is picked up by getImage()', () => {
const domSrc = wrapper.element.getAttribute('src')
expect(domSrc).toMatchInlineSnapshot('"/_ipx/s_1800x1800/image.png"')
})
it('props.src is reactive', async () => {
const newSource = '/image.jpeg'
wrapper.setProps({ src: newSource })
await nextTick()
const domSrc = wrapper.find('img').element.getAttribute('src')
expect(domSrc).toMatchInlineSnapshot('"/_ipx/s_1800x1800/image.jpeg"')
})
it('sizes', () => {
const sizes = wrapper.find('img').element.getAttribute('sizes')
expect(sizes).toBe('(max-width: 500px) 500px, 900px')
})
it('encodes characters', () => {
const img = mountImage({
width: 200,
height: 200,
sizes: '200,500:500,900:900',
src: '/汉字.png'
})
expect(img.html()).toMatchInlineSnapshot('"<img src=\\"/_ipx/s_1800x1800/%E6%B1%89%E5%AD%97.png\\" width=\\"200\\" height=\\"200\\" data-nuxt-img=\\"\\" sizes=\\"(max-width: 500px) 500px, 900px\\" srcset=\\"/_ipx/s_500x500/%E6%B1%89%E5%AD%97.png 500w, /_ipx/s_900x900/%E6%B1%89%E5%AD%97.png 900w, /_ipx/s_1000x1000/%E6%B1%89%E5%AD%97.png 1000w, /_ipx/s_1800x1800/%E6%B1%89%E5%AD%97.png 1800w\\">"')
})
it('correctly sets crop', () => {
const img = mountImage({
src: '/image.png',
width: 1000,
height: 2000,
sizes: 'xs:100vw sm:100vw md:300px lg:350px xl:350px 2xl:350px'
})
expect(img.html()).toMatchInlineSnapshot('"<img src=\\"/_ipx/s_1280x2560/image.png\\" width=\\"1000\\" height=\\"2000\\" data-nuxt-img=\\"\\" sizes=\\"(max-width: 320px) 100vw, (max-width: 640px) 100vw, (max-width: 768px) 300px, (max-width: 1024px) 350px, (max-width: 1280px) 350px, 350px\\" srcset=\\"/_ipx/s_300x600/image.png 300w, /_ipx/s_320x640/image.png 320w, /_ipx/s_350x700/image.png 350w, /_ipx/s_600x1200/image.png 600w, /_ipx/s_640x1280/image.png 640w, /_ipx/s_700x1400/image.png 700w, /_ipx/s_1280x2560/image.png 1280w\\">"')
})
})
const mountImage = (props: ComponentMountingOptions<typeof NuxtImg>['props']) => mount(NuxtImg, { props })
// @vitest-environment nuxt
import { beforeEach, describe, it, expect } from 'vitest'
import { VueWrapper, mount } from '@vue/test-utils'
import { NuxtPicture } from '#components'
describe('Renders simple image', () => {
let wrapper: VueWrapper<any>
const src = '/image.png'
const observer = {
wasAdded: false,
wasDestroyed: false
}
beforeEach(() => {
window.IntersectionObserver = class IntersectionObserver {
root: any
rootMargin: any
thresholds: any
takeRecords: any
observe (_target: Element) {
observer.wasAdded = true
}
disconnect () {
observer.wasDestroyed = true
}
unobserve () {
observer.wasDestroyed = true
}
}
wrapper = mount(NuxtPicture, {
propsData: {
loading: 'lazy',
width: 200,
height: 200,
sizes: '200,500:500,900:900',
src
}
})
})
it('Matches snapshot', () => {
expect(wrapper.html()).toMatchInlineSnapshot(`
"<picture>
<source type=\\"image/webp\\" sizes=\\"(max-width: 500px) 500px, 900px\\" srcset=\\"/_ipx/f_webp&s_500x500/image.png 500w, /_ipx/f_webp&s_900x900/image.png 900w, /_ipx/f_webp&s_1000x1000/image.png 1000w, /_ipx/f_webp&s_1800x1800/image.png 1800w\\"><img width=\\"200\\" height=\\"200\\" data-nuxt-pic=\\"\\" src=\\"/_ipx/f_png&s_1800x1800/image.png\\" sizes=\\"(max-width: 500px) 500px, 900px\\" srcset=\\"/_ipx/f_png&s_500x500/image.png 500w, /_ipx/f_png&s_900x900/image.png 900w, /_ipx/f_png&s_1000x1000/image.png 1000w, /_ipx/f_png&s_1800x1800/image.png 1800w\\">
</picture>"
`)
})
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('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('"<picture><img data-nuxt-pic=\\"\\" src=\\"/image.svg\\"></picture>"')
})
it('encodes characters', () => {
const img = mount(NuxtPicture, {
propsData: {
loading: 'lazy',
width: 200,
height: 200,
sizes: '200,500:500,900:900',
src: '/汉字.png'
}
})
expect(img.html()).toMatchInlineSnapshot(`
"<picture>
<source type=\\"image/webp\\" sizes=\\"(max-width: 500px) 500px, 900px\\" srcset=\\"/_ipx/f_webp&s_500x500/%E6%B1%89%E5%AD%97.png 500w, /_ipx/f_webp&s_900x900/%E6%B1%89%E5%AD%97.png 900w, /_ipx/f_webp&s_1000x1000/%E6%B1%89%E5%AD%97.png 1000w, /_ipx/f_webp&s_1800x1800/%E6%B1%89%E5%AD%97.png 1800w\\"><img width=\\"200\\" height=\\"200\\" data-nuxt-pic=\\"\\" src=\\"/_ipx/f_png&s_1800x1800/%E6%B1%89%E5%AD%97.png\\" sizes=\\"(max-width: 500px) 500px, 900px\\" srcset=\\"/_ipx/f_png&s_500x500/%E6%B1%89%E5%AD%97.png 500w, /_ipx/f_png&s_900x900/%E6%B1%89%E5%AD%97.png 900w, /_ipx/f_png&s_1000x1000/%E6%B1%89%E5%AD%97.png 1000w, /_ipx/f_png&s_1800x1800/%E6%B1%89%E5%AD%97.png 1800w\\">
</picture>"
`)
})
}) |
Co-authored-by: Hartmut <10864443+hartmut-co-uk@users.noreply.github.com>
+ updates docs accordingly
densities
for devices with DevicePixelRatio
> 1
- deep comparison of array elements using `JSON.stringify`
- deep comparison of array elements using `JSON.stringify`
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you all for amazing work on this. ❤️
Merging but please don't consider the API stabilised until next RC - @pi0 has yet to review and may have some helpful changes to make.
Add densities param for issues #216
Add:
resolves #216
closes #803