{ "version": 3, "sources": ["src/app/features/lazy-image/components/lazy-picture/lazy-picture.component.ts", "src/app/features/lazy-image/components/lazy-picture/lazy-picture.component.html"], "sourcesContent": ["import { DOCUMENT, NgClass } from '@angular/common';\nimport {\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n Inject,\n Input,\n OnChanges,\n OnInit,\n SimpleChanges,\n} from '@angular/core';\nimport { ContentfulService } from '@core/services/contentful.service';\nimport { EnvironmentService } from '@core/services/environment.service';\nimport { fixAssetUrl } from '@features/contentful/helpers/assetHelpers';\nimport { LazysizesService } from '@features/lazy-image/services/lazysizes.service';\nimport { Asset } from '@generated/graphql';\n\nexport enum Stops {\n FALLBACK = 'FALLBACK',\n BASE = '0px',\n SM = '640px',\n MD = '768px',\n LG = '1024px',\n XL = '1280px',\n XXL = '1536px',\n}\n\nexport enum StopsMediaQuery {\n BASE = '(max-width: 639.9px)',\n SM = '(min-width: 640px) and (max-width: 767.9px)',\n MD = '(min-width: 768px) and (max-width: 1023.9px)',\n LG = '(min-width: 1024px) and (max-width: 1279.9px)',\n XL = '(min-width: 1280px) and (max-width: 1535.9px)',\n XXL = '(min-width: 1536px) ',\n}\n\nexport const StopsMediaQueryMapping = {\n [Stops.FALLBACK]: StopsMediaQuery.BASE,\n [Stops.BASE]: StopsMediaQuery.BASE,\n [Stops.SM]: StopsMediaQuery.SM,\n [Stops.MD]: StopsMediaQuery.MD,\n [Stops.LG]: StopsMediaQuery.LG,\n [Stops.XL]: StopsMediaQuery.XL,\n [Stops.XXL]: StopsMediaQuery.XXL,\n};\n\nexport type CropFocus =\n | 'center'\n | 'top'\n | 'right'\n | 'left'\n | 'bottom'\n | 'top_right'\n | 'top_left'\n | 'bottom_right'\n | 'bottom_left';\nexport type ImageFit = 'pad' | 'fill' | 'scale' | 'crop' | 'thumb';\n\nexport interface ImageConfiguration {\n image: Asset;\n ratioWidth: number;\n ratioHeight: number;\n widthLqip?: number;\n width: number;\n width2x: number;\n width3x?: number;\n quality?: number;\n fit?: ImageFit;\n focus?: CropFocus;\n}\nexport type Configurations = {\n [key in Stops]?: ImageConfiguration;\n};\n\nexport interface Source {\n srcSet: string;\n defaultSrc: string;\n mediaQuery: string;\n stop: Stops;\n}\n\n@Component({\n selector: 'app-lazy-picture',\n templateUrl: './lazy-picture.component.html',\n changeDetection: ChangeDetectionStrategy.OnPush,\n standalone: true,\n imports: [NgClass],\n})\nexport class LazyPictureComponent implements OnChanges, OnInit {\n @Input() configurations: Configurations;\n @Input() alt: string = '';\n @Input() additionalClasses: string[] | string = [];\n @Input() lazy: boolean = true;\n @Input() preload: boolean = false;\n\n sources: Source[] = [];\n sourcesMap: { [key in Stops]?: Source } = {};\n fallback: Source;\n stops = Stops;\n trackBy(index: number, item: Source) {\n return item.mediaQuery;\n }\n\n cssClasses: string[] = [];\n\n constructor(\n private contentfulService: ContentfulService,\n private cd: ChangeDetectorRef,\n private lazysizesService: LazysizesService,\n private environmentService: EnvironmentService,\n @Inject(DOCUMENT) private doc: Document\n ) {}\n\n generateSource(stop: Stops, configuration: ImageConfiguration): Source {\n let [srcSet, defaultSrc] = ['', ''];\n if (configuration.image) {\n [srcSet, defaultSrc] = this.generateSrcSet(configuration);\n }\n return {\n stop: stop,\n srcSet: srcSet,\n defaultSrc: defaultSrc,\n mediaQuery: StopsMediaQueryMapping[stop],\n };\n }\n\n generateSrcSet(configuration: ImageConfiguration): [string, string] {\n let imageBaseUrl = fixAssetUrl(configuration.image.url);\n if (imageBaseUrl.endsWith('.svg')) {\n return [imageBaseUrl, imageBaseUrl];\n }\n const options = [];\n\n if (configuration.quality) {\n options.push(`q=${configuration.quality}`);\n } else {\n options.push(`q=80`);\n }\n if (configuration.fit) {\n options.push(`fit=${configuration.fit}`);\n } else {\n options.push('fit=fill');\n }\n if (configuration.focus) {\n options.push(`f=${configuration.focus}`);\n } else {\n options.push('f=center');\n }\n const urlWithOptions = `${imageBaseUrl}?${options.join('&')}`;\n const width1x = this.generateWidth(\n configuration.width,\n configuration.ratioWidth,\n configuration.ratioHeight\n );\n const width2x = this.generateWidth(\n configuration.width2x,\n configuration.ratioWidth,\n configuration.ratioHeight\n );\n const width3x = this.generateWidth(\n configuration.width3x || configuration.width2x,\n configuration.ratioWidth,\n configuration.ratioHeight\n );\n let defaultSrc = `${urlWithOptions}&${width1x}`;\n if (configuration.widthLqip) {\n const widthLqip = this.generateWidth(\n configuration.widthLqip,\n configuration.ratioWidth,\n configuration.ratioHeight\n );\n defaultSrc = `${urlWithOptions}&${widthLqip}`;\n }\n return [\n `${urlWithOptions}&${width1x} 1x, ${urlWithOptions}&${width2x} 2x, ${urlWithOptions}&${width3x} 3x`,\n defaultSrc,\n ];\n }\n\n generateWidth(width: number, ratioWidth: number, ratioHeight: number) {\n if (ratioHeight && ratioWidth) {\n const ratio = ratioHeight / ratioWidth;\n return `w=${width}&h=${Math.round(width * ratio)}`;\n } else {\n return `w=${width}`;\n }\n }\n\n ngOnChanges(changes: SimpleChanges): void {\n if (changes.configurations && changes.configurations.currentValue) {\n const configurationStops = Object.keys(this.configurations);\n const fakeFallback = configurationStops.includes(Stops.FALLBACK)\n ? {}\n : {\n [Stops.FALLBACK]: {\n image: null,\n ratioWidth: 0,\n ratioHeight: 0,\n width: 0,\n width2x: 0,\n },\n };\n const { FALLBACK, ...configurations } = {\n ...fakeFallback,\n ...this.configurations,\n };\n this.fallback = this.generateSource(Stops.FALLBACK, FALLBACK);\n this.sources = Object.entries(configurations)\n .filter(([stop, configuration]) => !!configuration.image)\n .map(([stop, configuration]) =>\n this.generateSource(stop as Stops, configuration)\n );\n this.sources.sort(\n (a, b) => +b.stop.slice(0, -2) - +a.stop.slice(0, -2)\n );\n this.sourcesMap = Object.assign(\n {},\n ...this.sources.map(source => ({ [source.stop]: source }))\n );\n if (this.preload) {\n this.generatePreloadLinks();\n }\n this.cd.detectChanges();\n }\n if (changes.additionalClasses || changes.lazy) {\n this.cssClasses = (this.lazy ? ['lazyload'] : []).concat(\n this.additionalClasses\n );\n this.cd.detectChanges();\n }\n }\n\n generatePreloadLinks() {\n for (const value in Stops) {\n const source = this.sourcesMap[Stops[value]];\n if (source) {\n this.createPreloadLink(\n source.defaultSrc,\n source.srcSet,\n Stops[value],\n StopsMediaQuery[value]\n );\n }\n }\n }\n\n createPreloadLink(\n url: string,\n srcSet: string,\n key: string,\n mediaQuery: string\n ) {\n // only create preload links on the server, since it will be too late on the client side anyway.\n if (\n !this.doc.getElementById(key) &&\n this.environmentService.isServer()\n ) {\n const link: HTMLLinkElement = this.doc.createElement('link');\n link.setAttribute('rel', 'preload');\n link.setAttribute('href', url);\n link.setAttribute('id', key);\n link.setAttribute('media', mediaQuery);\n link.setAttribute('as', 'image');\n link.setAttribute('imagesrcset', srcSet);\n if (!this.lazy) {\n link.setAttribute('fetchpriority', 'high');\n }\n // preload lcp image as early as possible\n this.doc.getElementById('viewport-definition').after(link);\n }\n }\n\n ngOnInit(): void {\n this.cssClasses = (this.lazy ? ['lazyload'] : []).concat(\n this.additionalClasses\n );\n this.cd.detectChanges();\n }\n}\n", "@if (sources.length) {\n