Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add useBackgroundImage composable #1248

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ export default defineNuxtModule<ModuleOptions>({
from: resolver.resolve('runtime/composables')
})

addImports({
name: 'useBackgroundImage',
from: resolver.resolve('runtime/composables')
})

// Add components
addComponent({
name: 'NuxtImg',
Expand Down
71 changes: 68 additions & 3 deletions src/runtime/composables.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { $Img } from '../types'

import type { $Img, ImageSizesOptions } from '../types'
import { generateRandomString } from './utils'
import { createImage } from './image'
// @ts-expect-error virtual file
import { imageOptions } from '#build/image-options'
import { useNuxtApp, useRuntimeConfig } from '#imports'
import { useNuxtApp, useRuntimeConfig, useHead } from '#imports'

export const useImage = (): $Img => {
const config = useRuntimeConfig()
Expand All @@ -16,3 +16,68 @@ export const useImage = (): $Img => {
}
}))
}

export const useBackgroundImage = (
src: string,
options: Partial<ImageSizesOptions> & { preload?: boolean; nonce?: string }
) => {
const $img = useImage()
const { sizes: bgSizes, imagesizes, imagesrcset } = $img.getBgSizes(src, options)

// Use this to prevent different class names on client and server
const classStates = useState<Record<string, string>>('_nuxt-img-bg', () => ({}))
// TODO: handle Map item type
const toCSS = (bgs: any[]) => {
const imageSets = bgs.map((bg) => {
const density = bg.density ? `${bg.density}x` : ''
const type = bg.type ? `type("${bg.type}")` : ''
return `url('${bg.src}') ${density} ${type}`
})
return `background-image: url(${
bgs[0].src
});background-image: image-set(${imageSets.join(', ')});`
}
const placeholder = '[placeholder]'

let css = Array.from(bgSizes)
.reverse()
.map(([key, value]) => {
if (key === 'default') {
return value ? `.${placeholder}{${toCSS(value)}}` : ''
} else {
return `@media (max-width: ${key}px) { .${placeholder} { ${toCSS(value)} } }`
}
}).join(' ')

// use generated css as key
let cls = ''
if (classStates.value[css]) {
cls = classStates.value[css]
} else {
cls = 'nuxt-bg-' + generateRandomString()
classStates.value[css] = cls
}
css = css.replace(/\[placeholder\]/gm, cls)

if (options.preload) {
useHead({
link: [
{
rel: 'preload',
as: 'image',
nonce: options.nonce,
href: bgSizes.get('default')?.[0].src,
...(bgSizes.size > 1 ? { imagesizes, imagesrcset } : {})
}
]
})
}

useHead({
style: [
{ key: cls, innerHTML: css }
]
})

return cls
}
106 changes: 105 additions & 1 deletion src/runtime/image.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { defu } from 'defu'
import { hasProtocol, parseURL, joinURL, withLeadingSlash } from 'ufo'
import type { ImageOptions, ImageSizesOptions, CreateImageOptions, ResolvedImage, ImageCTX, $Img, ImageSizes, ImageSizesVariant } from '../types/image'
import type {
ImageOptions,
ImageSizesOptions,
CreateImageOptions,
ResolvedImage,
ImageCTX,
$Img,
ImageSizes,
ImageSizesVariant,
BgImageSizes
} from '../types/image'
import { imageMeta } from './utils/meta'
import { checkDensities, parseDensities, parseSize, parseSizes } from './utils'
import { prerenderStaticImages } from './utils/prerender'
Expand All @@ -20,6 +30,9 @@ export function createImage (globalOptions: CreateImageOptions) {

return image
}
const getBgSizes: $Img['getBgSizes'] = (input, options = {}) => {
return createBgSizes(ctx, input, options)
}

const $img = ((input, modifiers = {}, options = {}) => {
return getImage(input, {
Expand All @@ -35,6 +48,7 @@ export function createImage (globalOptions: CreateImageOptions) {

$img.options = globalOptions
$img.getImage = getImage
$img.getBgSizes = getBgSizes
$img.getMeta = ((input: string, options?: ImageOptions) => getMeta(ctx, input, options)) as $Img['getMeta']
$img.getSizes = ((input: string, options: ImageSizesOptions) => getSizes(ctx, input, options)) as $Img['getSizes']

Expand Down Expand Up @@ -268,3 +282,93 @@ function finaliseSrcsetVariants (srcsetVariants: any[]) {
previousWidth = sizeVariant.width
}
}

export function createBgSizes (
ctx: ImageCTX,
input: string,
opts: Partial<ImageSizesOptions>
): { imagesrcset: string, imagesizes: string, sizes: BgImageSizes } {
const width = parseSize(opts.modifiers?.width)
const height = parseSize(opts.modifiers?.height)
const sizes = parseSizes(opts.sizes || {})
const hwRatio = width && height ? height / width : 0
const variants = Object.entries(sizes)
.map(([key, size]) => {
return getSizesVariant(key, String(size), height, hwRatio, ctx)
})
.filter((v) => {
return v !== undefined
}) as ImageSizesVariant[]

// sort by screenMaxWidth (ascending)
variants.sort((v1, v2) => v1.screenMaxWidth - v2.screenMaxWidth)

const densities = opts.densities?.trim()
? parseDensities(opts.densities.trim())
: ctx.options.densities
// sort densities ascending
densities.sort((a, b) => a - b)
checkDensities(densities)

const quality = opts.modifiers?.quality ? opts.modifiers.quality : ctx.options.quality

const result: BgImageSizes = new Map()
const imagesizesList: string[] = []

variants.forEach((variant, i) => {
const bpsWidth = String(variants[i + 1]?.screenMaxWidth || 'default')
if (result.has(bpsWidth)) {
return
}
if (bpsWidth === 'default') {
imagesizesList.push(`${variant._cWidth}px`)
} else {
imagesizesList.push(`(max-width: ${bpsWidth}px) ${variant._cWidth}px`)
}
result.set(
bpsWidth,
densities.map((d) => {
return {
src: getVariantSrc(ctx, input, {
...opts,
modifiers: {
...opts.modifiers,
quality
}
} as ImageSizesOptions, variant, d),
density: d.toString(),
type: opts.modifiers?.format ? `image/${opts.modifiers.format}` : '',
_cWidth: variant._cWidth * d
}
})
)
})
if (result.size === 0) {
result.set(
'default',
densities.map((d) => {
return {
src: ctx.$img!(
input,
{
...opts.modifiers,
quality,
width: width ? width * d : undefined,
height: height ? height * d : undefined
},
opts),
density: d.toString(),
type: opts.modifiers?.format ? `image/${opts.modifiers.format}` : '',
_cWidth: width ? width * d : undefined
}
})
)
}
// TODO: prerender static images: just add common version of current function

return {
imagesizes: imagesizesList.join(', '),
imagesrcset: Array.from(result).map(([_, value]) => value.map(v => `${v.src} ${v._cWidth}w`).join(', ')).join(', '),
sizes: result
}
}
9 changes: 9 additions & 0 deletions src/runtime/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,12 @@ export function parseSizes (input: Record<string, string | number> | string): Re
}
return sizes
}

export function generateRandomString (length: number = 6): string {
let result = ''
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length))
}
return result
}
8 changes: 8 additions & 0 deletions src/types/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,20 @@ export interface ImageSizes {
src: string
}

export type BgImageSizes = Map<string, {
src: string
density?: string
type?: string
_cWidth?: number
}[]>

export interface Img {
(source: string, modifiers?: ImageOptions['modifiers'], options?: ImageOptions): ResolvedImage['url']
options: CreateImageOptions
getImage: (source: string, options?: ImageOptions) => ResolvedImage
getSizes: (source: string, options?: ImageOptions, sizes?: string[]) => ImageSizes
getMeta: (source: string, options?: ImageOptions) => Promise<ImageInfo>
getBgSizes: (source: string, options?: Partial<ImageSizesOptions>) => { imagesrcset: string, imagesizes: string, sizes: BgImageSizes }
}

export type $Img = Img & {
Expand Down