From ba33389c6df2aa59fe646230ae0024adb5e9bd69 Mon Sep 17 00:00:00 2001 From: locinus Date: Fri, 11 Feb 2022 23:05:27 +0100 Subject: [PATCH 1/6] feat(): previews template customization --- .../modal-gallery/modal-gallery.component.html | 1 + .../modal-gallery/modal-gallery.component.ts | 13 +++++++++++-- .../components/previews/previews.component.ts | 14 +++++++++++++- .../src/lib/components/previews/previews.html | 17 +++++++++++++++-- .../lib/model/modal-gallery-config.interface.ts | 8 ++++++++ .../modal-gallery/modal-gallery.component.ts | 12 +++++++++++- src/app/modal-gallery/modal-gallery.html | 13 +++++++++++++ src/app/modal-gallery/modal-gallery.scss | 12 ++++++++++++ 8 files changed, 84 insertions(+), 6 deletions(-) diff --git a/projects/ks89/angular-modal-gallery/src/lib/components/modal-gallery/modal-gallery.component.html b/projects/ks89/angular-modal-gallery/src/lib/components/modal-gallery/modal-gallery.component.html index 4a07ac9a..0dcbbb08 100644 --- a/projects/ks89/angular-modal-gallery/src/lib/components/modal-gallery/modal-gallery.component.html +++ b/projects/ks89/angular-modal-gallery/src/lib/components/modal-gallery/modal-gallery.component.html @@ -34,6 +34,7 @@ diff --git a/projects/ks89/angular-modal-gallery/src/lib/components/modal-gallery/modal-gallery.component.ts b/projects/ks89/angular-modal-gallery/src/lib/components/modal-gallery/modal-gallery.component.ts index 2edd9af8..534e4b91 100644 --- a/projects/ks89/angular-modal-gallery/src/lib/components/modal-gallery/modal-gallery.component.ts +++ b/projects/ks89/angular-modal-gallery/src/lib/components/modal-gallery/modal-gallery.component.ts @@ -1,5 +1,7 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, - HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID, SecurityContext, ViewChild } from '@angular/core'; +import { + ChangeDetectionStrategy, ChangeDetectorRef, Component, + HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID, SecurityContext, ViewChild, TemplateRef +} from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; import { DomSanitizer } from '@angular/platform-browser'; @@ -83,6 +85,12 @@ export class ModalGalleryComponent implements OnInit, OnDestroy { * Array of `InternalLibImage` representing the model of this library with all images, thumbs and so on. */ images: InternalLibImage[]; + + /** + * Optional template reference to use to render previews. + */ + customPreviewsTemplate?: TemplateRef; + /** * `Image` that is visible right now. */ @@ -123,6 +131,7 @@ export class ModalGalleryComponent implements OnInit, OnDestroy { this.images = (this.dialogContent as ModalGalleryConfig).images as InternalLibImage[]; this.currentImage = (this.dialogContent as ModalGalleryConfig).currentImage as InternalLibImage; this.libConfig = (this.dialogContent as ModalGalleryConfig).libConfig; + this.customPreviewsTemplate = (this.dialogContent as ModalGalleryConfig).previewsTemplate; this.configService.setConfig(this.id, this.libConfig); this.updateImagesSubscription = this.modalGalleryService.updateImages$.subscribe((images: Image[]) => { diff --git a/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.component.ts b/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.component.ts index cb1d1572..bf65690a 100644 --- a/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.component.ts +++ b/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.component.ts @@ -22,7 +22,7 @@ SOFTWARE. */ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChange, SimpleChanges } from '@angular/core'; +import { ChangeDetectionStrategy, Component, ContentChild, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChange, SimpleChanges, TemplateRef } from '@angular/core'; import { AccessibleComponent } from '../accessible.component'; @@ -67,6 +67,15 @@ export class PreviewsComponent extends AccessibleComponent implements OnInit, On @Input() images: InternalLibImage[] | undefined; + /** + * Optional template reference for the rendering of previews. + * Template may access following context variables: + * - preview: the `Image` object + * - defaultTemplate: the template used by default to render the preview (in case the need is to wrap it) + */ + @Input() + customTemplate?: TemplateRef; + /** * Output to emit the clicked preview. The payload contains the `ImageEvent` associated to the clicked preview. */ @@ -194,6 +203,9 @@ export class PreviewsComponent extends AccessibleComponent implements OnInit, On * @param Action action that triggered the source event or `Action.NORMAL` if not specified */ onImageEvent(preview: InternalLibImage, event: KeyboardEvent | MouseEvent, action: Action = Action.NORMAL): void { + // It's suggested to stop propagation of the event, so the + // Cdk background will not catch a click and close the modal (like it does on Windows Chrome/FF). + event?.stopPropagation(); if (!this.id || !this.images) { throw new Error('Internal library error - id and images must be defined'); } diff --git a/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.html b/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.html index 6628ff88..8a931e06 100644 --- a/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.html +++ b/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.html @@ -14,7 +14,9 @@ - + {{preview.modal.alt ? preview.modal.alt : ''}} + /> + + +
+ + +
+
diff --git a/projects/ks89/angular-modal-gallery/src/lib/model/modal-gallery-config.interface.ts b/projects/ks89/angular-modal-gallery/src/lib/model/modal-gallery-config.interface.ts index 690094b0..6a734bbd 100644 --- a/projects/ks89/angular-modal-gallery/src/lib/model/modal-gallery-config.interface.ts +++ b/projects/ks89/angular-modal-gallery/src/lib/model/modal-gallery-config.interface.ts @@ -22,6 +22,7 @@ SOFTWARE. */ +import { TemplateRef } from '@angular/core'; import { Image } from './image.class'; import { ModalLibConfig } from './lib-config.interface'; @@ -30,4 +31,11 @@ export interface ModalGalleryConfig { images: Image[]; currentImage: Image; libConfig?: ModalLibConfig; + /** + * Optional template reference for the rendering of previews. + * Template may access following context variables: + * - "preview": the `Image` object of the preview + * - "defaultTemplate": the template used by default to render the preview (in case the need is to augment it) + */ + previewsTemplate?: TemplateRef; } diff --git a/src/app/modal-gallery/modal-gallery.component.ts b/src/app/modal-gallery/modal-gallery.component.ts index 51c46c9c..e6176ae3 100644 --- a/src/app/modal-gallery/modal-gallery.component.ts +++ b/src/app/modal-gallery/modal-gallery.component.ts @@ -22,6 +22,8 @@ SOFTWARE. */ +import { TemplateRef } from '@angular/core'; +import { ViewChild } from '@angular/core'; import { Component, OnDestroy } from '@angular/core'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; @@ -46,6 +48,12 @@ import * as libConfigs from './libconfigs'; styleUrls: ['./modal-gallery.scss'] }) export class ModalGalleryExampleComponent implements OnDestroy { + /** + * A custom template to illustrate the customization of previews rendering. + */ + @ViewChild('previewsTemplate') + previewsTemplate?: TemplateRef; + imageIndex = 0; galleryId = 1; isPlaying = true; @@ -508,11 +516,13 @@ export class ModalGalleryExampleComponent implements OnDestroy { return; } const imageToShow: Image = imagesArrayToUse[imageIndex]; + const previewsTemplate = (id === 902 ? this.previewsTemplate : undefined); const dialogRef: ModalGalleryRef = this.modalGalleryService.open({ id, images: imagesArrayToUse, currentImage: imageToShow, - libConfig + libConfig, + previewsTemplate, } as ModalGalleryConfig) as ModalGalleryRef; } diff --git a/src/app/modal-gallery/modal-gallery.html b/src/app/modal-gallery/modal-gallery.html index 732107e1..f9f7c451 100644 --- a/src/app/modal-gallery/modal-gallery.html +++ b/src/app/modal-gallery/modal-gallery.html @@ -315,3 +315,16 @@

F1 - (id=900) - Experimental demo - infinite sliding with only one image to

F2 - (id=901) - Experimental demo - an array of Images with the same source file (different classes/ids and paths with appended '?imageIndex' to prevent caching issues)

+
+

F3 - (id=902) - Experimental demo - 'previews' rendering customization (via Angular templates)

+ +
+
{{preview?.modal?.description ?? ' '}}
+
+ +
+
+
+ +
+ diff --git a/src/app/modal-gallery/modal-gallery.scss b/src/app/modal-gallery/modal-gallery.scss index f95d327e..03b01023 100644 --- a/src/app/modal-gallery/modal-gallery.scss +++ b/src/app/modal-gallery/modal-gallery.scss @@ -163,3 +163,15 @@ button { .title { margin-top: 40px; } + +.preview-block { + margin-right: 10px; +} +.preview-description { + color: #fff; + margin-bottom: 3px; +} +.preview-description, +.preview-default { + text-align: center; +} \ No newline at end of file From 0f7a13cd08fde19942b25f6e0f11253d1529855b Mon Sep 17 00:00:00 2001 From: locinus Date: Sun, 3 Apr 2022 21:37:48 +0200 Subject: [PATCH 2/6] test(): previews template customization --- .../previews/previews.component.spec.ts | 137 ++++++++++++++++-- 1 file changed, 123 insertions(+), 14 deletions(-) diff --git a/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.component.spec.ts b/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.component.spec.ts index 2857dde3..48295062 100644 --- a/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.component.spec.ts +++ b/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.component.spec.ts @@ -14,26 +14,26 @@ * limitations under the License. */ -import 'hammerjs'; -import 'mousetrap'; - +import { Component, DebugElement, SimpleChanges, TemplateRef, ViewChild } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { DebugElement, SimpleChanges } from '@angular/core'; import { By } from '@angular/platform-browser'; - -import { AccessibilityConfig } from '../../model/accessibility.interface'; +import 'hammerjs'; +import 'mousetrap'; import { KS_DEFAULT_ACCESSIBILITY_CONFIG } from '../../components/accessibility-default'; +import { FallbackImageDirective } from '../../directives/fallback-image.directive'; +import { SizeDirective } from '../../directives/size.directive'; +import { AccessibilityConfig } from '../../model/accessibility.interface'; +import { Action } from '../../model/action.enum'; import { InternalLibImage } from '../../model/image-internal.class'; -import { PreviewsComponent } from './previews.component'; +import { ImageModalEvent, ModalImage, PlainImage } from '../../model/image.class'; import { PreviewConfig } from '../../model/preview-config.interface'; -import { SlideConfig } from '../../model/slide-config.interface'; import { Size } from '../../model/size.interface'; -import { ImageModalEvent, ModalImage, PlainImage } from '../../model/image.class'; -import { SizeDirective } from '../../directives/size.directive'; -import { KS_DEFAULT_SIZE } from '../upper-buttons/upper-buttons-default'; -import { Action } from '../../model/action.enum'; +import { SlideConfig } from '../../model/slide-config.interface'; import { ConfigService } from '../../services/config.service'; -import { FallbackImageDirective } from '../../directives/fallback-image.directive'; +import { KS_DEFAULT_SIZE } from '../upper-buttons/upper-buttons-default'; +import { PreviewsComponent } from './previews.component'; + + interface NavigationTestData { initial: { @@ -238,9 +238,45 @@ function checkPreviewStateAfterClick(previews: DebugElement[], prevValue: Intern expect(comp.previews).toEqual(IMAGES.slice(start, end)); } +/** + * A template-providing component to test the template-driven previews customization. + */ + @Component({ + template: ` + +
example
+
+`, +}) +class PreviewsTemplateComponent0 { + @ViewChild('template') templateRef?: TemplateRef; +} + +/** + * A template-providing component to test the template-driven previews customization (using default template). + */ +@Component({ + template: ` + +
+ +
+
+`, +}) +class PreviewsTemplateComponent1 { + @ViewChild('template') templateRef?: TemplateRef; +} + function initTestBed(): void { TestBed.configureTestingModule({ - declarations: [PreviewsComponent, SizeDirective, FallbackImageDirective] + declarations: [ + PreviewsComponent, + SizeDirective, + FallbackImageDirective, + PreviewsTemplateComponent0, + PreviewsTemplateComponent1, + ] }).overrideComponent(PreviewsComponent, { set: { providers: [ @@ -744,6 +780,79 @@ describe('PreviewsComponent', () => { checkPreviewStateAfterClick(previews, IMAGES[4], IMAGES[4], 2, 5, 5); })); }); + + it(`should use a custom template to render the previews`, () => { + const templateComponentFixture = TestBed.createComponent(PreviewsTemplateComponent0); + const templateComponent = templateComponentFixture.debugElement.componentInstance as PreviewsTemplateComponent0; + templateComponentFixture.detectChanges(); + const templateRef = templateComponent.templateRef; + const initialActiveImage = 0; + const numOfPreviews = 3; + const configService = fixture.debugElement.injector.get(ConfigService); + configService.setConfig(GALLERY_ID, { + previewConfig: PREVIEWS_CONFIG_VISIBLE, + accessibilityConfig: KS_DEFAULT_ACCESSIBILITY_CONFIG, + slideConfig: SLIDE_CONFIG + }); + comp.id = GALLERY_ID; + comp.currentImage = IMAGES[initialActiveImage]; + comp.images = IMAGES; + comp.customTemplate = templateRef; + comp.ngOnInit(); + fixture.detectChanges(); + + const element: DebugElement = fixture.debugElement; + + const arrows: DebugElement[] = element.queryAll(By.css('a')); + checkArrows(arrows, true, false); + + const previewsContainer: DebugElement = element.query(By.css('nav.previews-container')); + expect(previewsContainer.name).toBe('nav'); + expect(previewsContainer.attributes['aria-label']).toBe(KS_DEFAULT_ACCESSIBILITY_CONFIG.previewsContainerAriaLabel); + expect(previewsContainer.properties.title).toBe(KS_DEFAULT_ACCESSIBILITY_CONFIG.previewsContainerTitle); + const previews: DebugElement[] = element.queryAll(By.css('.my-own-template')); + expect(previews.length).toBe(numOfPreviews); + }); + + it(`should use a custom template to render the previews (using default template)`, () => { + const templateComponentFixture = TestBed.createComponent(PreviewsTemplateComponent1); + const templateComponent = templateComponentFixture.debugElement.componentInstance as PreviewsTemplateComponent1; + templateComponentFixture.detectChanges(); + const templateRef = templateComponent.templateRef; + const initialActiveImage = 0; + const numOfPreviews = 3; + const configService = fixture.debugElement.injector.get(ConfigService); + configService.setConfig(GALLERY_ID, { + previewConfig: PREVIEWS_CONFIG_VISIBLE, + accessibilityConfig: KS_DEFAULT_ACCESSIBILITY_CONFIG, + slideConfig: SLIDE_CONFIG + }); + comp.id = GALLERY_ID; + comp.currentImage = IMAGES[initialActiveImage]; + comp.images = IMAGES; + comp.customTemplate = templateRef; + comp.ngOnInit(); + fixture.detectChanges(); + + const element: DebugElement = fixture.debugElement; + + const arrows: DebugElement[] = element.queryAll(By.css('a')); + checkArrows(arrows, true, false); + + const previewsContainer: DebugElement = element.query(By.css('nav.previews-container')); + expect(previewsContainer.name).toBe('nav'); + expect(previewsContainer.attributes['aria-label']).toBe(KS_DEFAULT_ACCESSIBILITY_CONFIG.previewsContainerAriaLabel); + expect(previewsContainer.properties.title).toBe(KS_DEFAULT_ACCESSIBILITY_CONFIG.previewsContainerTitle); + + const previews: DebugElement[] = element.queryAll(By.css('img')); + expect(previews.length).toBe(numOfPreviews); + + const previewImages: InternalLibImage[] = IMAGES.slice(initialActiveImage, numOfPreviews); + + for (let i = 0; i < previewImages.length; i++) { + checkPreview(previews[i], previewImages[i], i === 0, DEFAULT_PREVIEW_SIZE); + } + }); }); describe('---NO---', () => { From c24cf423e76d9bfe4d03f95310dfac0d1c72d6ac Mon Sep 17 00:00:00 2001 From: locinus Date: Thu, 7 Apr 2022 02:47:41 +0200 Subject: [PATCH 3/6] fix(): previews template - css fix --- .../src/lib/components/previews/previews.html | 29 ++++++++-------- .../src/lib/components/previews/previews.scss | 33 ++++++++++--------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.html b/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.html index 8a931e06..e6cb8339 100644 --- a/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.html +++ b/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.html @@ -15,27 +15,26 @@ - - {{preview.modal.alt ? preview.modal.alt : ''}} - - -
+ + {{preview.modal.alt ? preview.modal.alt : ''}} +
diff --git a/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.scss b/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.scss index 4cca5a52..f1e2ef6c 100644 --- a/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.scss +++ b/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.scss @@ -108,27 +108,28 @@ $nav-side-margin: 10px; justify-content: center; margin-bottom: $container-margin-bottom; - > .preview-image { - cursor: pointer; + .preview-container { margin-left: $preview-image-side-margin; margin-right: $preview-image-side-margin; - opacity: $preview-image-opacity; - height: $preview-image-height; - //animation: fadein-semi-visible08 $preview-image-fade-in-time; - - &.active { - opacity: $preview-image-hover-opacity; - //animation: fadein-visible $preview-image-fade-in-time; - } - + cursor: pointer; &.unclickable { cursor: not-allowed; } - - &:hover { - opacity: $preview-image-active-opacity; - transition: all .5s ease; - transition-property: opacity; + .preview-image { + opacity: $preview-image-opacity; + height: $preview-image-height; + //animation: fadein-semi-visible08 $preview-image-fade-in-time; + + &.active { + opacity: $preview-image-hover-opacity; + //animation: fadein-visible $preview-image-fade-in-time; + } + + &:hover { + opacity: $preview-image-active-opacity; + transition: all .5s ease; + transition-property: opacity; + } } } From b3c56da0b4c9c8ef7d382442325ddbe37c59ad36 Mon Sep 17 00:00:00 2001 From: locinus Date: Thu, 7 Apr 2022 02:48:40 +0200 Subject: [PATCH 4/6] refactor(): previews template example --- .../modal-gallery/modal-gallery.component.ts | 21 +++++++++++++++++-- src/app/modal-gallery/modal-gallery.html | 2 +- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/app/modal-gallery/modal-gallery.component.ts b/src/app/modal-gallery/modal-gallery.component.ts index e6176ae3..5a833222 100644 --- a/src/app/modal-gallery/modal-gallery.component.ts +++ b/src/app/modal-gallery/modal-gallery.component.ts @@ -516,13 +516,11 @@ export class ModalGalleryExampleComponent implements OnDestroy { return; } const imageToShow: Image = imagesArrayToUse[imageIndex]; - const previewsTemplate = (id === 902 ? this.previewsTemplate : undefined); const dialogRef: ModalGalleryRef = this.modalGalleryService.open({ id, images: imagesArrayToUse, currentImage: imageToShow, libConfig, - previewsTemplate, } as ModalGalleryConfig) as ModalGalleryRef; } @@ -719,6 +717,25 @@ export class ModalGalleryExampleComponent implements OnDestroy { return item.id; } + openModalWithPreviewsTemplate(id: number, imagesArrayToUse: Image[], imageIndex: number, libConfig?: ModalLibConfig): void { + if(imagesArrayToUse.length === 0) { + console.error('Cannot open modal-gallery because images array cannot be empty'); + return; + } + if(imageIndex > imagesArrayToUse.length - 1) { + console.error('Cannot open modal-gallery because imageIndex must be valid'); + return; + } + const imageToShow: Image = imagesArrayToUse[imageIndex]; + const dialogRef: ModalGalleryRef = this.modalGalleryService.open({ + id, + images: imagesArrayToUse, + currentImage: imageToShow, + libConfig, + previewsTemplate: this.previewsTemplate, + } as ModalGalleryConfig) as ModalGalleryRef; + } + ngOnDestroy(): void { // release resources to prevent memory leaks and unexpected behaviours if (this.closeSubscription) { diff --git a/src/app/modal-gallery/modal-gallery.html b/src/app/modal-gallery/modal-gallery.html index f9f7c451..b9e8e6fe 100644 --- a/src/app/modal-gallery/modal-gallery.html +++ b/src/app/modal-gallery/modal-gallery.html @@ -325,6 +325,6 @@

F3 - (id=902) - Experimental demo - 'previews' rendering customization (via - + From 352598a02b9a798ce311977e159323da0d7651ed Mon Sep 17 00:00:00 2001 From: locinus Date: Wed, 13 Apr 2022 12:32:16 +0200 Subject: [PATCH 5/6] fix(): previews indexes calculation Simplifies and uniformizes the calculation of previews indexes, in the different navigation cases (changes of current images, uses of previews arrows). Provides fixes for: - clicking the right image arrow always shifts the displayed previews, even in cases where it shouldn't, for instance for n>=3 and current=0 (root cause of 'clipping' effects when navigating away from first/last preview) - the number of displayed previews sometimes oscillate between n (number of requested previews in config) and n-1/n+1 while navigating (for example for n=4 or n=5) - when opening the modal and navigating to the last preview by clicking on the right preview arrow, it's impossible to then click on the left preview arrow (same from last to first and clicking on the right preview arrow) --- .../previews/previews.component.spec.ts | 105 +++++++++- .../components/previews/previews.component.ts | 187 +++++------------- 2 files changed, 149 insertions(+), 143 deletions(-) diff --git a/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.component.spec.ts b/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.component.spec.ts index 48295062..feb6a3f8 100644 --- a/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.component.spec.ts +++ b/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.component.spec.ts @@ -738,7 +738,15 @@ describe('PreviewsComponent', () => { previews = element.queryAll(By.css('img')); - previews[1].nativeElement.click(); + // previews[1].nativeElement.click(); + comp.ngOnChanges({ + currentImage: { + previousValue: IMAGES[0], + currentValue: IMAGES[1], + firstChange: false, + isFirstChange: () => false + } + } as SimpleChanges); checkPreviewStateAfterClick(previews, IMAGES[0], IMAGES[1], 0, 3, 1); // images = IMAGES.slice(0, 3); // for (let i = 0; i < images.length; i++) { @@ -853,6 +861,101 @@ describe('PreviewsComponent', () => { checkPreview(previews[i], previewImages[i], i === 0, DEFAULT_PREVIEW_SIZE); } }); + + [4, 5].forEach((previewNumber: number) => { + it(`should display a constant number of previews (${previewNumber}), independent of current image index`, waitForAsync(() => { + const configService = fixture.debugElement.injector.get(ConfigService); + const previewConfig = Object.assign({}, PREVIEWS_CONFIG_VISIBLE, { number: previewNumber }) as PreviewConfig; + configService.setConfig(GALLERY_ID, { + previewConfig, + accessibilityConfig: KS_DEFAULT_ACCESSIBILITY_CONFIG, + slideConfig: SLIDE_CONFIG + }); + comp.id = GALLERY_ID; + comp.currentImage = IMAGES[0]; + comp.images = IMAGES; + comp.ngOnInit(); + expect(comp.previews.length).toBe(previewNumber); + + // click on the second picture + comp.currentImage = IMAGES[1]; // set component input + comp.ngOnChanges({ // trigger changes manually (not done automatically in tests) + currentImage: { + previousValue: IMAGES[0], + currentValue: IMAGES[1], + firstChange: false, + isFirstChange: () => false + } + } as SimpleChanges); + // at the time of writing this test, a change is detected within 'images', yet with the same value + comp.ngOnChanges({ + images: { + previousValue: IMAGES, + currentValue: IMAGES, + firstChange: false, + isFirstChange: () => false + } + } as SimpleChanges); + expect(comp.previews.length).toBe(previewNumber); + + // click on the third picture + comp.currentImage = IMAGES[2]; // set component input + comp.ngOnChanges({ // trigger changes manually (not done automatically in tests) + currentImage: { + previousValue: IMAGES[1], + currentValue: IMAGES[2], + firstChange: false, + isFirstChange: () => false + } + } as SimpleChanges); + // at the time of writing this test, a change is detected within 'images', yet with the same value + comp.ngOnChanges({ + images: { + previousValue: IMAGES, + currentValue: IMAGES, + firstChange: false, + isFirstChange: () => false + } + } as SimpleChanges); + expect(comp.previews.length).toBe(previewNumber); + })); + }); + + it('should allow to navigate previews from first to last and back', () => { + const configService = fixture.debugElement.injector.get(ConfigService); + configService.setConfig(GALLERY_ID, { + previewConfig: PREVIEWS_CONFIG_VISIBLE, + accessibilityConfig: KS_DEFAULT_ACCESSIBILITY_CONFIG, + slideConfig: SLIDE_CONFIG + }); + comp.id = GALLERY_ID; + comp.currentImage = IMAGES[0]; + comp.images = IMAGES; + comp.ngOnInit(); + fixture.detectChanges(); + const element: DebugElement = fixture.debugElement; + let previews: DebugElement[] = element.queryAll(By.css('img')); + + const leftArrow = element.query(By.css('a.nav-left')).nativeElement as HTMLAnchorElement; + const rightArrow = element.query(By.css('a.nav-right')).nativeElement as HTMLAnchorElement; + checkPreviewStateAfterClick(previews, IMAGES[0], IMAGES[0], 0, 3, 0); + + // click right until the last preview is reached + rightArrow.click(); + checkPreviewStateAfterClick(previews, IMAGES[0], IMAGES[0], 1, 4, 0); + rightArrow.click(); + checkPreviewStateAfterClick(previews, IMAGES[0], IMAGES[0], 2, 5, 0); + // click left until the first preview is reached + leftArrow.click(); + checkPreviewStateAfterClick(previews, IMAGES[0], IMAGES[0], 1, 4, 0); + leftArrow.click(); + checkPreviewStateAfterClick(previews, IMAGES[0], IMAGES[0], 0, 3, 0); + // click right again and check previews have changed accordingly + rightArrow.click(); + checkPreviewStateAfterClick(previews, IMAGES[0], IMAGES[0], 1, 4, 0); + + }); + }); describe('---NO---', () => { diff --git a/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.component.ts b/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.component.ts index bf65690a..05850af6 100644 --- a/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.component.ts +++ b/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.component.ts @@ -112,12 +112,12 @@ export class PreviewsComponent extends AccessibleComponent implements OnInit, On */ previews: InternalLibImage[] = []; /** - * Start index of the input images used to display previews. + * Start index (included) of the input images used to display previews. */ // @ts-ignore start: number; /** - * End index of the input images used to display previews. + * End index (excluded) of the input images used to display previews. */ // @ts-ignore end: number; @@ -170,28 +170,12 @@ export class PreviewsComponent extends AccessibleComponent implements OnInit, On * In particular, it's called when any data-bound property of a directive changes!!! */ ngOnChanges(changes: SimpleChanges): void { - const images: SimpleChange = changes.images; - const currentImage: SimpleChange = changes.currentImage; - let prev; - let current; + let currentImage = changes.currentImage?.currentValue ?? this.currentImage; + let images = changes.images?.currentValue ?? this.images; - if (currentImage) { - prev = currentImage.previousValue; - current = currentImage.currentValue; - } else { - current = this.currentImage; - } - - if (current && images && images.previousValue && images.currentValue) { - // I'm in this if statement, if input images are changed (for instance, because I removed one of them with the 'delete button', - // or because users changed the images array while modal gallery is still open). - // In this case, I have to re-init previews, because the input array of images is changed. - this.initPreviews(current, images.currentValue); - } - - if (prev && current && prev.id !== current.id) { - this.updatePreviews(prev, current); + if(this.previewConfig && currentImage && images) { + this.initPreviews( currentImage, images); } } @@ -251,80 +235,51 @@ export class PreviewsComponent extends AccessibleComponent implements OnInit, On * @param InternalLibImage[] images is the array of all images. */ private initPreviews(currentImage: InternalLibImage, images: InternalLibImage[]): void { - let index: number; - try { - index = getIndex(currentImage, images); - } catch (err) { - throw err; - } - switch (index) { - case 0: - // first image - this.setBeginningIndexesPreviews(); - break; - case images.length - 1: - // last image - this.setEndIndexesPreviews(); - break; - default: - // other images - this.setIndexesPreviews(); - break; - } + this.setIndexesPreviews(currentImage, images); this.previews = images.filter((img: InternalLibImage, i: number) => i >= this.start && i < this.end); } - /** - * Private method to init both `start` and `end` to the beginning. - */ - private setBeginningIndexesPreviews(): void { - if (!this.previewConfig || !this.images) { - throw new Error('Internal library error - previewConfig and images must be defined'); - } - this.start = 0; - this.end = Math.min(this.previewConfig.number as number, this.images.length); - } - - /** - * Private method to init both `start` and `end` to the end. - */ - private setEndIndexesPreviews(): void { - if (!this.previewConfig || !this.images) { - throw new Error('Internal library error - previewConfig and images must be defined'); - } - this.start = this.images.length - 1 - ((this.previewConfig.number as number) - 1); - this.end = this.images.length; - } - /** * Private method to update both `start` and `end` based on the currentImage. */ - private setIndexesPreviews(): void { - if (!this.previewConfig || !this.images || !this.currentImage) { + private setIndexesPreviews(currentImage: InternalLibImage, images: InternalLibImage[]): void { + if (!this.previewConfig || !images || !currentImage) { throw new Error('Internal library error - previewConfig, currentImage and images must be defined'); } - this.start = getIndex(this.currentImage, this.images) - Math.floor((this.previewConfig.number as number) / 2); - this.end = getIndex(this.currentImage, this.images) + Math.floor((this.previewConfig.number as number) / 2) + 1; + const previewsNumber = this.previewConfig.number as number; + let start = getIndex(currentImage, images) - Math.floor(previewsNumber / 2); + // start is, at a minimum, the first index + if(start < 0) start = 0; + // end index + let end = start + previewsNumber; + // end is, at a maximum, the last index + if(end > images.length) { + start -= end - images.length; + if(start < 0) start = 0; // start is, at a minimum, the first index + end = images.length; + } + this.start = start; + this.end = end; } /** * Private method to update the visible previews navigating to the right (next). */ private next(): void { - if (!this.images) { + if (!this.images || !this.previewConfig) { throw new Error('Internal library error - images must be defined'); } - // check if nextImage should be blocked - if (this.isPreventSliding(this.images.length - 1)) { - return; - } - - if (this.end === this.images.length) { - return; + if(this.end >= this.images.length) { + // check if nextImage should be blocked + const preventSliding = !!this.slideConfig && this.slideConfig.infinite === false; + if(preventSliding) { + return; + } + this.start = 0; + } else { + this.start++; } - - this.start++; - this.end = Math.min(this.end + 1, this.images.length); + this.end = this.start + Math.min((this.previewConfig.number as number), this.images.length); this.previews = this.images.filter((img: InternalLibImage, i: number) => i >= this.start && i < this.end); } @@ -333,74 +288,22 @@ export class PreviewsComponent extends AccessibleComponent implements OnInit, On * Private method to update the visible previews navigating to the left (previous). */ private previous(): void { - if (!this.images) { + if (!this.images || !this.previewConfig) { throw new Error('Internal library error - images must be defined'); } - // check if prevImage should be blocked - if (this.isPreventSliding(0)) { - return; - } - - if (this.start === 0) { - return; + if(this.start <= 0) { + // check if prevImage should be blocked + const preventSliding = !!this.slideConfig && this.slideConfig.infinite === false; + if(preventSliding) { + return; + } + this.end = this.images.length; + } else { + this.end--; } - - this.start = Math.max(this.start - 1, 0); - this.end = Math.min(this.end - 1, this.images.length); + this.start = this.end - Math.min((this.previewConfig.number as number), this.images.length); this.previews = this.images.filter((img: InternalLibImage, i: number) => i >= this.start && i < this.end); } - /** - * Private method to block/permit sliding between previews. - * @param number boundaryIndex is the first or the last index of `images` input array - * @returns boolean if true block sliding, otherwise not - */ - private isPreventSliding(boundaryIndex: number): boolean { - if (!this.images || !this.currentImage) { - throw new Error('Internal library error - images and currentImage must be defined'); - } - return !!this.slideConfig && this.slideConfig.infinite === false && getIndex(this.currentImage, this.images) === boundaryIndex; - } - - /** - * Private method to handle navigation changing the previews array and other variables. - */ - private updatePreviews(prev: InternalLibImage, current: InternalLibImage): void { - if (!this.images) { - throw new Error('Internal library error - images must be defined'); - } - // to manage infinite sliding I have to reset both `start` and `end` at the beginning - // to show again previews from the first image. - // This happens when you navigate over the last image to return to the first one - let prevIndex: number; - let currentIndex: number; - try { - prevIndex = getIndex(prev, this.images); - currentIndex = getIndex(current, this.images); - } catch (err) { - console.error('Cannot get previous and current image indexes in previews'); - throw err; - } - if (prevIndex === this.images.length - 1 && currentIndex === 0) { - // first image - this.setBeginningIndexesPreviews(); - this.previews = this.images.filter((img: InternalLibImage, i: number) => i >= this.start && i < this.end); - return; - } - // the same for the opposite case, when you navigate back from the fist image to go to the last one. - if (prevIndex === 0 && currentIndex === this.images.length - 1) { - // last image - this.setEndIndexesPreviews(); - this.previews = this.images.filter((img: InternalLibImage, i: number) => i >= this.start && i < this.end); - return; - } - - // otherwise manage standard scenarios - if (prevIndex > currentIndex) { - this.previous(); - } else if (prevIndex < currentIndex) { - this.next(); - } - } } From 13b82b62187c2507093402615f247174b2332e9c Mon Sep 17 00:00:00 2001 From: locinus Date: Wed, 13 Apr 2022 12:54:13 +0200 Subject: [PATCH 6/6] fix(): previews arrows in infinite mode In infinite sliding, the left and right preview arrows should be always visible (except if nbPreviews < nbImages) --- .../previews/previews.component.spec.ts | 35 +++++++++++++++++++ .../components/previews/previews.component.ts | 24 +++++++++++++ .../src/lib/components/previews/previews.html | 8 ++--- 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.component.spec.ts b/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.component.spec.ts index feb6a3f8..7ce0894a 100644 --- a/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.component.spec.ts +++ b/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.component.spec.ts @@ -956,6 +956,41 @@ describe('PreviewsComponent', () => { }); + it('should always display previews navigation arrows, in infinite sliding mode (nbPreviews < nbImages)', () => { + const configService = fixture.debugElement.injector.get(ConfigService); + configService.setConfig(GALLERY_ID, { + previewConfig: PREVIEWS_CONFIG_VISIBLE, + accessibilityConfig: KS_DEFAULT_ACCESSIBILITY_CONFIG, + slideConfig: SLIDE_CONFIG_INFINITE + }); + comp.id = GALLERY_ID; + comp.currentImage = IMAGES[0]; + comp.images = IMAGES; + comp.ngOnInit(); + fixture.detectChanges(); + const element: DebugElement = fixture.debugElement; + const leftArrow = element.query(By.css('a.nav-left')).nativeElement as HTMLAnchorElement; + const rightArrow = element.query(By.css('a.nav-right')).nativeElement as HTMLAnchorElement; + let leftArrowDiv = element.query(By.css('a.nav-left > div')).nativeElement as HTMLAnchorElement; + let rightArrowDiv = element.query(By.css('a.nav-right > div')).nativeElement as HTMLAnchorElement; + // check that arrows are initially visible + expect(leftArrowDiv.classList).toContain('left-arrow-preview-image'); + expect(rightArrowDiv.classList).toContain('right-arrow-preview-image'); + // click right until the last preview is reached, each time check that arrows are visible + rightArrow.click(); + fixture.detectChanges(); + leftArrowDiv = element.query(By.css('a.nav-left > div')).nativeElement as HTMLAnchorElement; + rightArrowDiv = element.query(By.css('a.nav-right > div')).nativeElement as HTMLAnchorElement; + expect(leftArrowDiv.classList).toContain('left-arrow-preview-image'); + expect(rightArrowDiv.classList).toContain('right-arrow-preview-image'); + rightArrow.click(); + fixture.detectChanges(); + leftArrowDiv = element.query(By.css('a.nav-left > div')).nativeElement as HTMLAnchorElement; + rightArrowDiv = element.query(By.css('a.nav-right > div')).nativeElement as HTMLAnchorElement; + expect(leftArrowDiv.classList).toContain('left-arrow-preview-image'); + expect(rightArrowDiv.classList).toContain('right-arrow-preview-image'); + }); + }); describe('---NO---', () => { diff --git a/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.component.ts b/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.component.ts index 05850af6..ff338713 100644 --- a/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.component.ts +++ b/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.component.ts @@ -228,6 +228,30 @@ export class PreviewsComponent extends AccessibleComponent implements OnInit, On return item.id; } + /** + * Indicates if the previews 'left arrow' should be displayed or not. + * @returns + */ + displayLeftPreviewsArrow(): boolean { + // Don't show arrows if requested previews number equals or is greated than total number of imgaes + if(this.previewConfig?.number !== undefined && this.images && this.previewConfig?.number >= this.images?.length) { + return false; + } + return (this.previewConfig?.arrows && this.start > 0) || !!this.slideConfig?.infinite; + } + + /** + * Indicates if the previews 'right arrow' should be displayed or not. + * @returns + */ + displayRightPreviewsArrow(): boolean { + // Don't show arrows if requested previews number equals or is greated than total number of imgaes + if(this.previewConfig?.number !== undefined && this.images && this.previewConfig?.number >= this.images?.length) { + return false; + } + return (this.previewConfig?.arrows && this.images && this.end < this.images.length) || !!this.slideConfig?.infinite; + } + /** * Private method to init previews based on the currentImage and the full array of images. * The current image in mandatory to show always the current preview (as highlighted). diff --git a/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.html b/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.html index e6cb8339..bda19729 100644 --- a/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.html +++ b/projects/ks89/angular-modal-gallery/src/lib/components/previews/previews.html @@ -6,9 +6,9 @@ - @@ -42,9 +42,9 @@ -