diff --git a/projects/natural/src/lib/modules/common/common-module.ts b/projects/natural/src/lib/modules/common/common-module.ts index 3ec10fe8..8fc7180a 100644 --- a/projects/natural/src/lib/modules/common/common-module.ts +++ b/projects/natural/src/lib/modules/common/common-module.ts @@ -12,6 +12,7 @@ import {NaturalDefaultPipe} from './pipes/default.pipe'; import {MatFormFieldModule} from '@angular/material/form-field'; import {MatInputModule} from '@angular/material/input'; import {MatSelectModule} from '@angular/material/select'; +import {NaturalSrcDensityDirective} from './directives/src-density.directive'; const declarationsToExport = [ NaturalCapitalizePipe, @@ -21,6 +22,7 @@ const declarationsToExport = [ NaturalSwissDatePipe, ReactiveAsteriskDirective, NaturalHttpPrefixDirective, + NaturalSrcDensityDirective, NaturalLinkableTabDirective, ]; diff --git a/projects/natural/src/lib/modules/common/directives/src-density.directive.spec.ts b/projects/natural/src/lib/modules/common/directives/src-density.directive.spec.ts new file mode 100644 index 00000000..a7ecab11 --- /dev/null +++ b/projects/natural/src/lib/modules/common/directives/src-density.directive.spec.ts @@ -0,0 +1,69 @@ +import {Component, DebugElement} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {NaturalSrcDensityDirective} from './src-density.directive'; +import {By} from '@angular/platform-browser'; + +@Component({ + template: ` + + + + + + + `, +}) +class TestComponent {} + +describe('NaturalSrcDensityDirective', () => { + let fixture: ComponentFixture; + let elements: DebugElement[]; // the three elements w/ the directive + + const expectedSrc = 'https://example.com/image/123/200'; + const expectedSrcset = + 'https://example.com/image/123/200, https://example.com/image/123/300 1.5x, https://example.com/image/123/400 2x, https://example.com/image/123/600 3x, https://example.com/image/123/800 4x'; + beforeEach(() => { + fixture = TestBed.configureTestingModule({ + declarations: [NaturalSrcDensityDirective, TestComponent], + }).createComponent(TestComponent); + + fixture.detectChanges(); // initial binding + + // all elements with an attached NaturalSrcDensityDirective + elements = fixture.debugElement.queryAll(By.directive(NaturalSrcDensityDirective)); + }); + + it('should have 6 active elements', () => { + expect(elements.length).toBe(6); + }); + + it('1st should be kept empty and should not be used in real world', () => { + expect(elements[0].nativeElement.src).toBe(''); + expect(elements[0].nativeElement.srcset).toBe(''); + }); + + it('2nd should work', () => { + expect(elements[1].nativeElement.src).toBe(expectedSrc); + expect(elements[1].nativeElement.srcset).toBe(expectedSrcset); + }); + + it('3rd should work', () => { + expect(elements[2].nativeElement.src).toBe(expectedSrc); + expect(elements[2].nativeElement.srcset).toBe(expectedSrcset); + }); + + it('4th should keep naturalSrcDensity as-is without srcset', () => { + expect(elements[3].nativeElement.src).toBe('https://example.com/image/123.jpg'); + expect(elements[3].nativeElement.srcset).toBe(''); + }); + + it('5th will completely discard original src and srcset attributes', () => { + expect(elements[4].nativeElement.src).toBe(expectedSrc); + expect(elements[4].nativeElement.srcset).toBe(expectedSrcset); + }); + + it('6th will completely discard original src and srcset attributes while keeping naturalSrcDensity as-is', () => { + expect(elements[5].nativeElement.src).toBe('https://example.com/image/123.jpg'); + expect(elements[5].nativeElement.srcset).toBe(''); + }); +}); diff --git a/projects/natural/src/lib/modules/common/directives/src-density.directive.ts b/projects/natural/src/lib/modules/common/directives/src-density.directive.ts new file mode 100644 index 00000000..fe8b5f12 --- /dev/null +++ b/projects/natural/src/lib/modules/common/directives/src-density.directive.ts @@ -0,0 +1,55 @@ +import {Directive, ElementRef, Input} from '@angular/core'; + +@Directive({ + selector: 'img[naturalSrcDensity]', +}) +export class NaturalSrcDensityDirective { + /** + * Automatically apply image selection based on screen density. + * + * The given URL **MUST** be the normal density URL. And it **MUST** include + * the size as last path segment. That size will automatically be changed + * for other screen densities. That means that the server **MUST** be able to + * serve an image of the given size. + * + * Usage: + * + * ```html + * + * ``` + * + * Will generate something like: + * + * ```html + * + * ``` + * + * @see https://web.dev/codelab-density-descriptors/ + */ + @Input() + public set naturalSrcDensity(src: string) { + if (!src) { + return; + } + + const match = src.match(/^(.*\/)(\d+)$/); + const base = match?.[1]; + const size = +(match?.[2] ?? 0); + + let srcset = ''; + if (base && size) { + // This should cover most common densities according to https://www.mydevice.io/#tab1 + const size15 = size * 1.5; + const size2 = size * 2; + const size3 = size * 3; + const size4 = size * 4; + + srcset = `${base}${size}, ${base}${size15} 1.5x, ${base}${size2} 2x, ${base}${size3} 3x, ${base}${size4} 4x`; + } + + this.elementRef.nativeElement.src = src; + this.elementRef.nativeElement.srcset = srcset; + } + + constructor(private elementRef: ElementRef) {} +} diff --git a/projects/natural/src/lib/modules/common/public-api.ts b/projects/natural/src/lib/modules/common/public-api.ts index 41dc41e3..c80451b7 100644 --- a/projects/natural/src/lib/modules/common/public-api.ts +++ b/projects/natural/src/lib/modules/common/public-api.ts @@ -11,6 +11,7 @@ export * from './pipes/ellipsis.pipe'; export * from './pipes/enum.pipe'; export * from './pipes/swiss-date.pipe'; export * from './services/memory-storage'; +export {NaturalSrcDensityDirective} from './directives/src-density.directive'; export { NATURAL_SEO_CONFIG, NaturalSeoConfig,