From b8f0c16b56573a51f76b2c40b634b078587f75fd Mon Sep 17 00:00:00 2001 From: Murhaf Sousli Date: Mon, 28 Oct 2024 21:33:25 +0100 Subject: [PATCH] Update --- .../src/app/pages/lab/lab.component.html | 35 +-- .../src/app/pages/lab/lab.component.ts | 7 +- .../components/gallery-slider.component.ts | 10 +- .../src/lib/components/gallery-slider.scss | 19 +- .../src/lib/components/gallery.component.ts | 14 +- .../src/lib/components/gallery.scss | 12 +- .../src/lib/components/items/items.ts | 11 +- .../src/lib/components/slider/slider.ts | 3 +- .../lib/gestures/hammer-sliding.directive.ts | 11 +- .../src/lib/observers/auto-height.ts | 74 +++--- .../intersection-sensor.directive.ts | 1 + .../slider-resize-observer.directive.ts | 222 ------------------ .../src/lib/services/gallery-ref.ts | 8 +- .../src/lib/services/resize-sensor.ts | 15 +- .../src/lib/tests/auto-height.spec.ts | 58 +++++ projects/ng-gallery/src/lib/tests/common.ts | 11 + .../src/lib/tests/hammer-slider.spec.ts | 8 +- .../lib/tests/intersection-directive.spec.ts | 24 +- .../src/lib/tests/resize-directive.spec.ts | 13 +- .../src/lib/tests/smooth-scroll.spec.ts | 16 +- .../src/lib/utils/resize-observer.ts | 15 -- projects/ng-gallery/src/public-api.ts | 1 + 22 files changed, 247 insertions(+), 341 deletions(-) delete mode 100644 projects/ng-gallery/src/lib/observers/slider-resize-observer.directive.ts create mode 100644 projects/ng-gallery/src/lib/tests/auto-height.spec.ts delete mode 100644 projects/ng-gallery/src/lib/utils/resize-observer.ts diff --git a/projects/ng-gallery-demo/src/app/pages/lab/lab.component.html b/projects/ng-gallery-demo/src/app/pages/lab/lab.component.html index 7238acdd..20d7ee72 100644 --- a/projects/ng-gallery-demo/src/app/pages/lab/lab.component.html +++ b/projects/ng-gallery-demo/src/app/pages/lab/lab.component.html @@ -9,7 +9,6 @@

} - - + @if (thumbConfig.thumbs) { + + }
@@ -244,7 +244,8 @@

Centralize Slider

- Centralize Thumbnails + Centralize Thumbnails +
Detach thumbnails @@ -263,7 +264,9 @@

- Disable Thumb Scroll + Disable Thumb + Scroll +
@@ -271,7 +274,9 @@

- Disable Thumb Mouse Scroll + Disable Thumb + Mouse Scroll +
diff --git a/projects/ng-gallery-demo/src/app/pages/lab/lab.component.ts b/projects/ng-gallery-demo/src/app/pages/lab/lab.component.ts index 57653c04..b6a8e19a 100644 --- a/projects/ng-gallery-demo/src/app/pages/lab/lab.component.ts +++ b/projects/ng-gallery-demo/src/app/pages/lab/lab.component.ts @@ -14,7 +14,7 @@ import { GalleryBulletsComponent, GalleryCounterComponent, GalleryItemDef, - ImgRecognizer + ImgRecognizer, AutoHeight } from 'ng-gallery'; import { MatInputModule } from '@angular/material/input'; import { MatCheckboxModule } from '@angular/material/checkbox'; @@ -39,6 +39,7 @@ import { FooterComponent } from '../../shared/footer/footer.component'; changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ + AutoHeight, FlexLayoutModule, MatIconModule, NgIf, @@ -113,7 +114,7 @@ export class LabComponent implements OnInit { indexChange$ = new BehaviorSubject({ active: false }); constructor(pixabay: Pixabay, private _title: Title) { - this.photos$ = pixabay.getHDImages('jet fighter'); + this.photos$ = pixabay.getHDImages('tropical'); } ngOnInit() { @@ -130,7 +131,7 @@ export class LabComponent implements OnInit { autoplayInterval: 3000, loadingStrategy: LoadingStrategy.Preload, orientation: Orientation.Horizontal, - autoHeight: false, + autoHeight: true, itemAutosize: false, scrollBehavior: 'smooth', loadingAttr: 'lazy', diff --git a/projects/ng-gallery/src/lib/components/gallery-slider.component.ts b/projects/ng-gallery/src/lib/components/gallery-slider.component.ts index ea6a173c..15a9efe4 100644 --- a/projects/ng-gallery/src/lib/components/gallery-slider.component.ts +++ b/projects/ng-gallery/src/lib/components/gallery-slider.component.ts @@ -1,4 +1,4 @@ -import { Component, inject, output, OutputEmitterRef, ChangeDetectionStrategy } from '@angular/core'; +import { Component, inject, output, OutputEmitterRef, ChangeDetectionStrategy, viewChild } from '@angular/core'; import { GalleryError } from '../models/gallery.model'; import { GalleryRef } from '../services/gallery-ref'; import { SmoothScroll } from '../smooth-scroll'; @@ -8,7 +8,6 @@ import { GalleryItemComponent } from './gallery-item.component'; import { ScrollSnapType } from '../services/scroll-snap-type'; import { ResizeSensor } from '../services/resize-sensor'; import { SliderComponent } from './slider/slider'; -import { AutoHeight } from '../observers/auto-height'; @Component({ standalone: true, @@ -17,7 +16,6 @@ import { AutoHeight } from '../observers/auto-height'; = output(); diff --git a/projects/ng-gallery/src/lib/components/gallery-slider.scss b/projects/ng-gallery/src/lib/components/gallery-slider.scss index da72f365..2b0d24d3 100644 --- a/projects/ng-gallery/src/lib/components/gallery-slider.scss +++ b/projects/ng-gallery/src/lib/components/gallery-slider.scss @@ -19,6 +19,11 @@ height: var(--slider-height, 100%); width: var(--slider-width, 100%); + //&.g-resizing { + // // Changes the height of the slider to match the active item height + // height: var(--slider-auto-height, 100%); + //} + overflow: var(--slider-overflow); scroll-snap-type: var(--slider-scroll-snap-type); scroll-snap-stop: always; @@ -48,13 +53,13 @@ display: none; } - &.g-resizing { - ::ng-deep { - gallery-item { - visibility: hidden; - } - } - } + //&.g-resizing { + // ::ng-deep { + // gallery-item { + // visibility: hidden; + // } + // } + //} &.g-sliding, &.g-scrolling { // Disable mouse click on gallery items/thumbnails when the slider is being dragged using the mouse diff --git a/projects/ng-gallery/src/lib/components/gallery.component.ts b/projects/ng-gallery/src/lib/components/gallery.component.ts index 5ab64f9a..eff10771 100644 --- a/projects/ng-gallery/src/lib/components/gallery.component.ts +++ b/projects/ng-gallery/src/lib/components/gallery.component.ts @@ -14,7 +14,7 @@ import { TemplateRef, OutputEmitterRef, ChangeDetectionStrategy, - InputSignalWithTransform + InputSignalWithTransform, viewChild } from '@angular/core'; import { NgTemplateOutlet } from '@angular/common'; import { Directionality } from '@angular/cdk/bidi'; @@ -41,7 +41,7 @@ import { GallerySliderComponent } from './gallery-slider.component'; '[attr.dir]': 'dir.value', '[attr.debug]': 'debug()', '[attr.imageSize]': 'imageSize()', - '[attr.autoHeight]': 'autoHeight()', + // '[attr.autoHeight]': 'autoHeight()', '[attr.orientation]': 'orientation()', '[attr.itemAutosize]': 'itemAutosize()', '[attr.scrollDisabled]': 'disableScroll()' @@ -71,6 +71,8 @@ import { GallerySliderComponent } from './gallery-slider.component'; }) export class GalleryComponent { + slider = viewChild(GallerySliderComponent); + /** * The gallery reference instance */ @@ -128,9 +130,9 @@ export class GalleryComponent { /** * Automatically adjusts the gallery's height to fit the content */ - autoHeight: InputSignalWithTransform = input(this._config.autoHeight, { - transform: booleanAttribute - }); + // autoHeight: InputSignalWithTransform = input(this._config.autoHeight, { + // transform: booleanAttribute + // }); /** * Automatically cycle through items at time interval @@ -283,7 +285,7 @@ export class GalleryComponent { disableScroll: this.disableScroll(), disableMouseScroll: this.disableMouseScroll(), itemAutosize: this.itemAutosize(), - autoHeight: this.autoHeight() + // autoHeight: this.autoHeight() }; }); diff --git a/projects/ng-gallery/src/lib/components/gallery.scss b/projects/ng-gallery/src/lib/components/gallery.scss index 3d747fa1..4489e4e2 100644 --- a/projects/ng-gallery/src/lib/components/gallery.scss +++ b/projects/ng-gallery/src/lib/components/gallery.scss @@ -13,7 +13,7 @@ gap: var(--g-gutter-size); width: 100%; - width: 776px; + //width: 776px; height: 500px; min-height: 100%; max-height: 100%; @@ -33,6 +33,16 @@ --g-item-height: 100%; --g-item-max-height: var(--slider-height); + &.g-resizing { + // Changes the height of the slider to match the active item height + //--slider-height: var(--slider-auto-height) !important; + //::ng-deep { + // .g-slider { + // height: var(--slider-auto-height) !important; + // } + //} + } + &[gallerize] { --g-item-cursor: pointer; } diff --git a/projects/ng-gallery/src/lib/components/items/items.ts b/projects/ng-gallery/src/lib/components/items/items.ts index 9e1ff517..3f1d93e6 100644 --- a/projects/ng-gallery/src/lib/components/items/items.ts +++ b/projects/ng-gallery/src/lib/components/items/items.ts @@ -1,12 +1,13 @@ import { - computed, Directive, - ElementRef, - inject, Injector, + inject, + signal, + computed, input, - InputSignal, Signal, - signal, + Injector, + ElementRef, + InputSignal, WritableSignal } from '@angular/core'; import { toObservable } from '@angular/core/rxjs-interop'; diff --git a/projects/ng-gallery/src/lib/components/slider/slider.ts b/projects/ng-gallery/src/lib/components/slider/slider.ts index baa5edaf..3a9deb66 100644 --- a/projects/ng-gallery/src/lib/components/slider/slider.ts +++ b/projects/ng-gallery/src/lib/components/slider/slider.ts @@ -27,8 +27,7 @@ import { SliderItem } from '../items/items'; '[attr.autosize]': 'autosize()', }, selector: 'g-slider', - template: ` - `, + template: '', changeDetection: ChangeDetectionStrategy.OnPush }) export class SliderComponent { diff --git a/projects/ng-gallery/src/lib/gestures/hammer-sliding.directive.ts b/projects/ng-gallery/src/lib/gestures/hammer-sliding.directive.ts index 5def81c2..a9840340 100644 --- a/projects/ng-gallery/src/lib/gestures/hammer-sliding.directive.ts +++ b/projects/ng-gallery/src/lib/gestures/hammer-sliding.directive.ts @@ -9,7 +9,7 @@ import { ElementRef, InputSignal, WritableSignal, - EffectCleanupRegisterFn + EffectCleanupRegisterFn, InputSignalWithTransform, booleanAttribute } from '@angular/core'; import { DOCUMENT } from '@angular/common'; import { HammerGestureConfig } from '@angular/platform-browser'; @@ -53,6 +53,10 @@ export class HammerSliding { sliding: WritableSignal = signal(false); + isThumbs: InputSignalWithTransform = input(false, { + transform: booleanAttribute + }); + constructor() { if (this._platform.ANDROID || this._platform.IOS || !(this._document.defaultView as any).Hammer) return; @@ -90,6 +94,11 @@ export class HammerSliding { mc.on('panend', (e: any) => { this._document.onselectstart = null; + if (this.isThumbs()) { + this.sliding.set(false); + return; + } + const index: number = this.getIndexOnMouseUp(e, this.slider.adapter()); if (index !== -1) { this._zone.run(() => { diff --git a/projects/ng-gallery/src/lib/observers/auto-height.ts b/projects/ng-gallery/src/lib/observers/auto-height.ts index 89fe67a7..7a3d482b 100644 --- a/projects/ng-gallery/src/lib/observers/auto-height.ts +++ b/projects/ng-gallery/src/lib/observers/auto-height.ts @@ -4,15 +4,25 @@ import { inject, signal, untracked, - input, - ElementRef, - InputSignal, WritableSignal, EffectCleanupRegisterFn } from '@angular/core'; -import { Observable, Subscription, of, filter, fromEvent, switchMap, tap, take } from 'rxjs'; +import { + Observable, + Subscription, + of, + filter, + fromEvent, + switchMap, + tap, + take, + debounceTime, + animationFrameScheduler +} from 'rxjs'; import { ImgManager } from '../utils/img-manager'; import { GalleryRef } from '../services/gallery-ref'; +import { GalleryComponent } from '../components/gallery.component'; +import { ResizeSensor } from '../services/resize-sensor'; /** * Auto height feature prerequisites: @@ -22,57 +32,61 @@ import { GalleryRef } from '../services/gallery-ref'; @Directive({ standalone: true, - selector: '[autoHeight]' + selector: 'gallery[autoHeight]', + host: { + '[attr.autoHeight]': 'true', + '[class.g-resizing]': 'isResizing()', + '[style.--slider-auto-height.px]': 'height()', + } }) export class AutoHeight { - private readonly galleryRef: GalleryRef = inject(GalleryRef); + private readonly gallery: GalleryComponent = inject(GalleryComponent); private readonly manager: ImgManager = inject(ImgManager); - private readonly _viewport: HTMLElement = inject(ElementRef).nativeElement; - - private readonly _galleryCore: HTMLElement = this._viewport.parentElement.parentElement.parentElement; - readonly isResizing: WritableSignal = signal(false); - disabled: InputSignal = input(false, { alias: 'disableAutoHeight' }); + readonly height: WritableSignal = signal(0); constructor() { let sub$: Subscription; let afterHeightChanged$: Observable; - - // Check if height has transition for the auto-height feature - const transitionDuration: string = getComputedStyle(this._viewport).transitionDuration; - console.log(parseFloat(transitionDuration)) - if (!parseFloat(transitionDuration)) { - afterHeightChanged$ = of(null); - } else { - afterHeightChanged$ = fromEvent(this._viewport, 'transitionend'); - } - effect((onCleanup: EffectCleanupRegisterFn) => { - if (this.disabled()) return; + const resizeSensor: ResizeSensor = this.gallery.slider().resizeSensor(); + // Check if height has transition for the auto-height feature + const transitionDuration: string = getComputedStyle(resizeSensor.nativeElement).transitionDuration; + if (!parseFloat(transitionDuration)) { + afterHeightChanged$ = of({}); + } else { + console.log(transitionDuration) + afterHeightChanged$ = fromEvent(resizeSensor.nativeElement, 'transitionend'); + } + // if (!this.galleryRef.config().autoHeight) return; // const currIndex = this.galleryRef.currIndex(); untracked(() => { sub$ = this.manager.getActiveItem().pipe( - tap((img)=> { - console.log('🤡 getActiveItem!', img.height, this._viewport.clientHeight); + // Wait for item image to be rendered + debounceTime(0, animationFrameScheduler), + // Skip if img height is equal the slider height + filter((img: HTMLImageElement) => { + console.log('🦕', resizeSensor.nativeElement.clientHeight, img.height) + return img.height !== resizeSensor.nativeElement.clientHeight }), - // SKip if img height is equal the slider height - filter((img: HTMLImageElement) => img.height !== this._viewport.clientHeight), switchMap((img: HTMLImageElement) => { - // TODO: Check if even to execute this code if there is no transition duration set. - this._galleryCore.style.setProperty('--slider-height', `${ img.clientHeight }px`); + console.log('👽 Resize started! --slider-height', resizeSensor.nativeElement.clientHeight, img.height) + resizeSensor.disabled.set(true); this.isResizing.set(true); - console.log('👽 Resize started!') + + resizeSensor.nativeElement.style.setProperty('--slider-height', `${img.height}px`) return afterHeightChanged$.pipe( + debounceTime(0, animationFrameScheduler), tap(() => { + resizeSensor.disabled.set(false); this.isResizing.set(false); - console.log('🍄 Resize ended!') }), take(1) ); diff --git a/projects/ng-gallery/src/lib/observers/intersection-sensor.directive.ts b/projects/ng-gallery/src/lib/observers/intersection-sensor.directive.ts index 3d999d2a..0251d9bd 100644 --- a/projects/ng-gallery/src/lib/observers/intersection-sensor.directive.ts +++ b/projects/ng-gallery/src/lib/observers/intersection-sensor.directive.ts @@ -78,6 +78,7 @@ export class IntersectionSensor { } }); this.zone.run(() => { + this.galleryRef.afterItemsVisible.next(); this.slider.visibleEntries.set({ ...visibleItems }); }); }); diff --git a/projects/ng-gallery/src/lib/observers/slider-resize-observer.directive.ts b/projects/ng-gallery/src/lib/observers/slider-resize-observer.directive.ts deleted file mode 100644 index bd628a3d..00000000 --- a/projects/ng-gallery/src/lib/observers/slider-resize-observer.directive.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { - Directive, - output, - inject, - effect, - computed, - untracked, - input, - Signal, - InputSignal, - OnInit, - NgZone, - ElementRef, - DestroyRef, - OutputEmitterRef, - AfterViewChecked, - EffectCleanupRegisterFn, signal, WritableSignal -} from '@angular/core'; -import { - Observable, - Subscription, - of, - tap, - take, - filter, - fromEvent, - switchMap, - debounceTime, - firstValueFrom, - EMPTY, - animationFrameScheduler -} from 'rxjs'; -import { ImgManager } from '../utils/img-manager'; -import { resizeObservable } from '../utils/resize-observer'; -import { SliderAdapter } from '../components/adapters'; -import { GalleryRef } from '../services/gallery-ref'; -import { GalleryConfig } from '../models/config.model'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; - -@Directive({ - host: { - '[class.g-resizing]': 'isResizing()' - }, - standalone: true, - selector: '[sliderResizeObserver]' -}) -export class SliderResizeObserver implements AfterViewChecked, OnInit { - - readonly isResizing: WritableSignal = signal(false); - - private readonly _galleryRef: GalleryRef = inject(GalleryRef); - - private readonly _viewport: HTMLElement = inject(ElementRef).nativeElement; - - private readonly _galleryCore: HTMLElement = this._viewport.parentElement.parentElement.parentElement; - - private readonly _zone: NgZone = inject(NgZone); - - private readonly _imgManager: ImgManager = inject(ImgManager); - - private _resizeObserver: ResizeObserver; - - private _shouldSkip: boolean; - - // Stream that emits after the transition to the new height is completed - private _afterHeightChanged$: Observable; - - // private _isAutoHeight: Signal = computed(() => { - // const config: GalleryConfig = this._galleryRef.config(); - // return config.autoHeight && - // !config.itemAutosize && - // config.orientation === 'horizontal' && - // (config.thumbPosition === 'top' || config.thumbPosition === 'bottom'); - // }); - - adapter: InputSignal = input(); - - isResizingChange: OutputEmitterRef = output(); - - constructor() { - let resizeSubscription$: Subscription; - let _autoHeightSubscription: Subscription; - const destroyRef = inject(DestroyRef); - - effect((onCleanup: EffectCleanupRegisterFn) => { - const config = this._galleryRef.config(); - // const isAutoHeight = this._isAutoHeight(); - - untracked(() => { - this._zone.runOutsideAngular(() => { - // Detect if the size of the slider has changed detecting current index on scroll - resizeSubscription$ = resizeObservable(this._viewport, (observer: ResizeObserver) => this._resizeObserver = observer).pipe( - takeUntilDestroyed(destroyRef), - // Check if resize should skip due to re-observing the slider - filter(() => !this._shouldSkip || !(this._shouldSkip = false)), - // Immediately set visibility to hidden to avoid changing the active item caused by appearance of other items when size is expanded - tap(() => this.setResizingState()), - debounceTime(config.resizeDebounceTime, animationFrameScheduler), - tap(async (entry: ResizeObserverEntry) => { - // Update CSS variables with the proper values - this.updateSliderSize(); - - // if (isAutoHeight) { - const img: HTMLImageElement = await firstValueFrom(this._imgManager.getActiveItem()); - // If img height is identical to the viewport height then skip - if (img.height === this._viewport.clientHeight) { - this.resetResizingState(); - } else { - // Unobserve the slider while the height is being changed - this.setResizingState({ unobserve: true }); - // Change the height - this._galleryCore.style.setProperty('--slider-height', `${ img.height }px`); - // Wait until height transition ends - await firstValueFrom(this._afterHeightChanged$); - this.resetResizingState({ - // Mark to skip first emit after re-observing the slider if height content rect height and client height are identical - shouldSkip: entry.contentRect.height === this._viewport.clientHeight, - observe: true - }); - } - // } else { - // requestAnimationFrame(() => this.resetResizingState({ shouldSkip: true })); - // } - }) - ).subscribe(); - }); - - onCleanup(() => resizeSubscription$?.unsubscribe()); - }); - }); - - - // effect((onCleanup: EffectCleanupRegisterFn) => { - // const isAutoHeight = this._isAutoHeight(); - // - // untracked(() => { - // this._shouldSkip = false; - // if (isAutoHeight) { - // this._zone.runOutsideAngular(() => { - // _autoHeightSubscription = this._imgManager.getActiveItem().pipe( - // takeUntilDestroyed(destroyRef), - // switchMap((img: HTMLImageElement) => { - // this.setResizingState({ unobserve: true }); - // this._galleryCore.style.setProperty('--slider-height', `${ img.clientHeight }px`); - // - // // Check if the new item height is equal to the current height, there will be no transition, - // // So reset resizing state - // if (img.height === this._viewport.clientHeight) { - // this.resetResizingState({ shouldSkip: true, observe: true }); - // return EMPTY; - // } - // return this._afterHeightChanged$.pipe( - // tap(() => this.resetResizingState({ shouldSkip: true, observe: true })), - // take(1) - // ); - // }) - // ).subscribe(); - // }); - // } - // - // onCleanup(() => resizeSubscription$?.unsubscribe()); - // }); - // }); - } - - ngOnInit(): void { - // Check if height has transition for the auto-height feature - // const transitionDuration: string = getComputedStyle(this._viewport).getPropertyValue('transition-duration'); - // if (parseFloat(transitionDuration) === 0) { - // this._afterHeightChanged$ = of(null); - // } else { - // this._afterHeightChanged$ = fromEvent(this._viewport, 'transitionend'); - // } - } - - ngAfterViewChecked(): void { - // this.updateSliderSize(); - } - - private updateSliderSize(): void { - // Update slider width and height CSS variables - this._galleryCore.style.setProperty('--slider-width', `${ this._viewport.clientWidth }px`); - - // Only update height if auto-height is false, because when it's true, another function will take care of it - if (!this._galleryRef.config().autoHeight) { - this._galleryCore.style.setProperty('--slider-height', `${ this._viewport.clientHeight }px`); - } - - this.updateCentralizeCSSVariables(); - } - - private updateCentralizeCSSVariables(): void { - if (this._galleryRef.config().itemAutosize) { - this._galleryCore.style.setProperty('--slider-centralize-start-size', `${ this.adapter().getCentralizerStartSize() }px`); - this._galleryCore.style.setProperty('--slider-centralize-end-size', `${ this.adapter().getCentralizerEndSize() }px`); - } - } - - private setResizingState({ unobserve }: { unobserve?: boolean } = {}): void { - this._zone.run(() => { - this.isResizing.set(true); - this.isResizingChange.emit(true); - }) - // this._viewport.classList.add('g-resizing'); - if (unobserve) { - // Unobserve the slider while the height is being changed - this._resizeObserver.unobserve(this._viewport); - } - } - - private resetResizingState({ shouldSkip, observe }: { shouldSkip?: boolean, observe?: boolean } = {}): void { - this._zone.run(() => { - this.isResizing.set(false); - this.isResizingChange.emit(false); - }) - // this._viewport.classList.remove('g-resizing'); - this._shouldSkip = shouldSkip; - if (observe) { - this._resizeObserver.observe(this._viewport); - } - } -} diff --git a/projects/ng-gallery/src/lib/services/gallery-ref.ts b/projects/ng-gallery/src/lib/services/gallery-ref.ts index b2dfde8f..eb3cc791 100644 --- a/projects/ng-gallery/src/lib/services/gallery-ref.ts +++ b/projects/ng-gallery/src/lib/services/gallery-ref.ts @@ -20,6 +20,8 @@ import { IndexChange } from '../models/slider.model'; @Injectable() export class GalleryRef { + readonly afterItemsVisible: Subject = new Subject(); + /** Stream that emits on item click */ readonly itemClick: Subject = new Subject(); @@ -29,9 +31,6 @@ export class GalleryRef { /** Stream that emits when items is changed (items loaded, item added, item removed) */ readonly itemsChanged: Subject = new Subject(); - /** Stream that emits when current index is changed */ - readonly indexChanged: Subject = new Subject(); - /** Stream that emits on an error occurs */ readonly error: Subject = new Subject(); @@ -55,6 +54,9 @@ export class GalleryRef { readonly indexChange: Subject = new Subject(); + /** Stream that emits when current index is changed */ + readonly indexChanged: Observable = toObservable(this.currIndex); + /** Config signal */ readonly config: WritableSignal = signal(inject(GALLERY_CONFIG)); diff --git a/projects/ng-gallery/src/lib/services/resize-sensor.ts b/projects/ng-gallery/src/lib/services/resize-sensor.ts index 6295486c..fae83982 100644 --- a/projects/ng-gallery/src/lib/services/resize-sensor.ts +++ b/projects/ng-gallery/src/lib/services/resize-sensor.ts @@ -8,7 +8,7 @@ import { NgZone, Signal, WritableSignal, - EffectCleanupRegisterFn + EffectCleanupRegisterFn, ElementRef } from '@angular/core'; import { SharedResizeObserver } from '@angular/cdk/observers/private'; import { Subscription, animationFrameScheduler, throttleTime, combineLatest } from 'rxjs'; @@ -27,15 +27,17 @@ import { SliderComponent } from '../components/slider/slider'; } }) export class ResizeSensor { - // TODO: This directive is used in both slider and thumbs, maybe we can only observe the root element once - private readonly sharedResizeObserver: SharedResizeObserver = inject(SharedResizeObserver) - private readonly slider: SliderComponent = inject(SliderComponent, { self: true }); + nativeElement:HTMLElement = inject(ElementRef).nativeElement; + + private readonly sharedResizeObserver: SharedResizeObserver = inject(SharedResizeObserver); private readonly zone: NgZone = inject(NgZone); private readonly galleryRef: GalleryRef = inject(GalleryRef); + private readonly slider: SliderComponent = inject(SliderComponent, { self: true }); + readonly slideSize: WritableSignal = signal(null); readonly contentSize: WritableSignal = signal(null); @@ -50,6 +52,8 @@ export class ResizeSensor { return this.slider.adapter()?.getCentralizerEndSize(); }); + disabled: WritableSignal = signal(false); + constructor() { let resizeSubscription$: Subscription; @@ -57,7 +61,7 @@ export class ResizeSensor { const config: GalleryConfig = this.galleryRef.config(); // Make sure items are rendered - if (!this.slider.items().length) return; + if (!this.slider.items().length || this.disabled()) return; untracked(() => { this.zone.runOutsideAngular(() => { @@ -74,6 +78,7 @@ export class ResizeSensor { if (!sliderEntries || !contentEntries) return; if (sliderEntries[0].contentRect.height) { + console.log('🔥', sliderEntries[0].contentRect.height) this.slideSize.set(sliderEntries[0].contentRect); } diff --git a/projects/ng-gallery/src/lib/tests/auto-height.spec.ts b/projects/ng-gallery/src/lib/tests/auto-height.spec.ts new file mode 100644 index 00000000..f1b287ee --- /dev/null +++ b/projects/ng-gallery/src/lib/tests/auto-height.spec.ts @@ -0,0 +1,58 @@ +// import { ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/core/testing'; +// import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +// import { By } from '@angular/platform-browser'; +// import { DebugElement } from '@angular/core'; +// import { GalleryRef } from 'ng-gallery'; +// import { getObservableFromContext, TestComponent } from './common'; +// import { filter, firstValueFrom, fromEvent, Observable } from 'rxjs'; +// import { AutoHeight } from '../observers/auto-height'; +// import { ImgManager } from '../utils/img-manager'; +// +// fdescribe('Auto-height directive', () => { +// let fixture: ComponentFixture; +// let autoHeightDirective: AutoHeight; +// let galleryRef: GalleryRef; +// let manager: ImgManager; +// let autoHeightDebugElement: DebugElement; +// +// beforeEach(() => { +// TestBed.configureTestingModule({ +// imports: [ +// NoopAnimationsModule, +// TestComponent +// ], +// providers: [ +// { provide: ComponentFixtureAutoDetect, useValue: true } +// ] +// }).compileComponents(); +// +// fixture = TestBed.createComponent(TestComponent); +// autoHeightDebugElement = fixture.debugElement.query(By.directive(AutoHeight)); +// autoHeightDirective = autoHeightDebugElement.injector.get(AutoHeight); +// galleryRef = autoHeightDebugElement.injector.get(GalleryRef); +// manager = autoHeightDebugElement.injector.get(ImgManager); +// fixture.detectChanges(); +// }); +// +// it('should create [autoHeight] directive', () => { +// expect(autoHeightDirective).toBeTruthy(); +// }); +// +// fit('should observe when items become visible as soon as possible', async () => { +// TestBed.flushEffects(); +// await firstValueFrom(galleryRef.afterItemsVisible); +// +// galleryRef.next('smooth'); +// +// const transitionEnd$ = fromEvent(autoHeightDebugElement.nativeElement, 'transitionend'); +// +// expect(autoHeightDirective.isResizing()).toBeTrue(); +// +// // const img: HTMLImageElement = await firstValueFrom(manager.getActiveItem()); +// // const el: HTMLElement = autoHeightDebugElement.nativeElement; +// // +// // await firstValueFrom(transitionEnd$); +// // expect(autoHeightDirective.isResizing()).toBeFalse(); +// // expect(el.parentElement.parentElement.parentElement.clientHeight).toBe(img.naturalHeight); +// }); +// }); diff --git a/projects/ng-gallery/src/lib/tests/common.ts b/projects/ng-gallery/src/lib/tests/common.ts index 6a49243c..62690952 100644 --- a/projects/ng-gallery/src/lib/tests/common.ts +++ b/projects/ng-gallery/src/lib/tests/common.ts @@ -1,5 +1,8 @@ import { Component, Signal, viewChild } from '@angular/core'; import { GalleryComponent, GalleryItem, GalleryItemDef, ImageItem, ImgRecognizer } from 'ng-gallery'; +import { Observable } from 'rxjs'; +import { TestBed } from '@angular/core/testing'; +import { toObservable } from '@angular/core/rxjs-interop'; @Component({ standalone: true, @@ -34,3 +37,11 @@ export async function afterTimeout(timeout: number): Promise { // Use await with a setTimeout promise await new Promise((resolve) => setTimeout(resolve, timeout)); } + +export function getObservableFromContext(signal: Signal): Observable { + let obs; + TestBed.runInInjectionContext(() => { + obs = toObservable(signal); + }); + return obs; +} diff --git a/projects/ng-gallery/src/lib/tests/hammer-slider.spec.ts b/projects/ng-gallery/src/lib/tests/hammer-slider.spec.ts index 0dd70fe1..71f695f3 100644 --- a/projects/ng-gallery/src/lib/tests/hammer-slider.spec.ts +++ b/projects/ng-gallery/src/lib/tests/hammer-slider.spec.ts @@ -2,17 +2,14 @@ import { ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; -import { GalleryRef } from 'ng-gallery'; -import { afterTimeout, TestComponent } from './common'; +import { TestComponent } from './common'; import { HammerSliding } from '../gestures/hammer-sliding.directive'; import 'hammerjs'; describe('Hammer slider directive', () => { let fixture: ComponentFixture; - let nativeElement: HTMLElement; let hammerSliderElement: DebugElement let hammerSliderDirective: HammerSliding; - let galleryRef: GalleryRef; beforeEach(() => { TestBed.configureTestingModule({ @@ -30,9 +27,6 @@ describe('Hammer slider directive', () => { hammerSliderElement = fixture.debugElement.query(By.directive(HammerSliding)); hammerSliderDirective = hammerSliderElement.injector.get(HammerSliding); - nativeElement = hammerSliderElement.nativeElement; - - galleryRef = hammerSliderElement.injector.get(GalleryRef); }); it('should create [hammerSlider] directive', () => { diff --git a/projects/ng-gallery/src/lib/tests/intersection-directive.spec.ts b/projects/ng-gallery/src/lib/tests/intersection-directive.spec.ts index a3364df5..35a7c967 100644 --- a/projects/ng-gallery/src/lib/tests/intersection-directive.spec.ts +++ b/projects/ng-gallery/src/lib/tests/intersection-directive.spec.ts @@ -3,8 +3,9 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; import { GalleryRef } from 'ng-gallery'; -import { afterTimeout, TestComponent } from './common'; +import { getObservableFromContext, TestComponent } from './common'; import { IntersectionSensor } from '../observers/intersection-sensor.directive'; +import { filter, firstValueFrom, Observable } from 'rxjs'; describe('Intersection directive', () => { let fixture: ComponentFixture; @@ -35,7 +36,7 @@ describe('Intersection directive', () => { }); it('should observe when items become visible as soon as possible', async () => { - await afterTimeout(16); + await firstValueFrom(galleryRef.afterItemsVisible); const visibleItems: Record = galleryRef.visibleItems(); const element: Element = visibleItems[0].target; @@ -48,11 +49,18 @@ describe('Intersection directive', () => { }); it('should detect when next item becomes visible on scroll then detect the previous leave after scroll', async () => { - await afterTimeout(16); + await firstValueFrom(galleryRef.afterItemsVisible); + expect(galleryRef.currIndex()).toBe(0); galleryRef.next(); - await afterTimeout(200); + // Wait for scroll starts and the next item is detected, at this point both previous and next items are visible + + const visibleItemIsTwo$: Observable = getObservableFromContext(galleryRef.visibleItems).pipe( + filter((obj: Record) => Object.keys(obj).length === 2) + ); + await firstValueFrom(visibleItemIsTwo$); + const visibleItems: Record = galleryRef.visibleItems(); const queryElements: DebugElement[] = fixture.debugElement.queryAll(By.css('gallery-item.g-item-highlight')); @@ -60,7 +68,13 @@ describe('Intersection directive', () => { expect(visibleItems[0].target).toBe(queryElements[0].nativeElement); expect(visibleItems[1].target).toBe(queryElements[1].nativeElement); - await afterTimeout(300); + // Wait until scroll is ended and the new active item is set + + const arrivedToNextItem$: Observable = galleryRef.indexChanged.pipe( + filter((currIndex: number) => currIndex === 1) + ); + await firstValueFrom(arrivedToNextItem$); + const visibleItemsAfter: Record = galleryRef.visibleItems(); const queryElementsAfter: DebugElement[] = fixture.debugElement.queryAll(By.css('gallery-item.g-item-highlight')); diff --git a/projects/ng-gallery/src/lib/tests/resize-directive.spec.ts b/projects/ng-gallery/src/lib/tests/resize-directive.spec.ts index a3e7493a..dd03d127 100644 --- a/projects/ng-gallery/src/lib/tests/resize-directive.spec.ts +++ b/projects/ng-gallery/src/lib/tests/resize-directive.spec.ts @@ -2,6 +2,8 @@ import { ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; +import { GalleryRef } from 'ng-gallery'; +import { firstValueFrom } from 'rxjs'; import { afterTimeout, TestComponent } from './common'; import { SliderComponent } from '../components/slider/slider'; import { ResizeSensor } from '../services/resize-sensor'; @@ -11,6 +13,7 @@ describe('Resize sensor directive', () => { let component: TestComponent; let resizeSensorDirective: ResizeSensor; let sliderComponent: SliderComponent; + let galleryRef: GalleryRef; beforeEach(() => { TestBed.configureTestingModule({ @@ -29,6 +32,7 @@ describe('Resize sensor directive', () => { const resizeSensorElement: DebugElement = fixture.debugElement.query(By.directive(ResizeSensor)); resizeSensorDirective = resizeSensorElement.injector.get(ResizeSensor); + galleryRef = resizeSensorElement.injector.get(GalleryRef); const sliderComponentElement: DebugElement = fixture.debugElement.query(By.directive(SliderComponent)); sliderComponent = sliderComponentElement.componentInstance; @@ -39,7 +43,7 @@ describe('Resize sensor directive', () => { }); it('should compute "centralizeStart" size when content >= viewport', async () => { - await afterTimeout(16); + await firstValueFrom(galleryRef.afterItemsVisible); expect(resizeSensorDirective.centralizeStart()).toBe(0); expect(resizeSensorDirective.centralizeStart()).toBe(0); expect(sliderComponent.nativeElement.style.getPropertyValue('--centralize-start-size')).toBe('0px'); @@ -52,7 +56,8 @@ describe('Resize sensor directive', () => { centralized: true }); component.width = 800; - component.height = 200; + component.height = 200 + // TODO: Find a promise that resolves when all items are loaded and displayed await afterTimeout(200); @@ -63,7 +68,7 @@ describe('Resize sensor directive', () => { }); it('should compute "centralizeStart" size when content >= viewport', async () => { - await afterTimeout(16); + await firstValueFrom(galleryRef.afterItemsVisible); expect(resizeSensorDirective.centralizeStart()).toBe(0); expect(resizeSensorDirective.centralizeStart()).toBe(0); expect(sliderComponent.nativeElement.style.getPropertyValue('--centralize-start-size')).toBe('0px'); @@ -71,7 +76,7 @@ describe('Resize sensor directive', () => { }); it('should update the size signal when component size changes', async () => { - await afterTimeout(16); + await firstValueFrom(galleryRef.afterItemsVisible); expect(resizeSensorDirective.slideSize().width).toBe(500); expect(resizeSensorDirective.slideSize().height).toBe(300); expect(sliderComponent.nativeElement.style.getPropertyValue('--slider-width')).toBe('500px'); diff --git a/projects/ng-gallery/src/lib/tests/smooth-scroll.spec.ts b/projects/ng-gallery/src/lib/tests/smooth-scroll.spec.ts index 24c9bfe7..58e12248 100644 --- a/projects/ng-gallery/src/lib/tests/smooth-scroll.spec.ts +++ b/projects/ng-gallery/src/lib/tests/smooth-scroll.spec.ts @@ -5,6 +5,7 @@ import { DebugElement } from '@angular/core'; import { GalleryRef } from 'ng-gallery'; import { afterTimeout, TestComponent } from './common'; import { SmoothScroll, SmoothScrollOptions } from '../smooth-scroll'; +import { filter, firstValueFrom, Observable } from 'rxjs'; describe('Smooth scroll directive', () => { let fixture: ComponentFixture; @@ -54,14 +55,17 @@ describe('Smooth scroll directive', () => { it('should scroll instantly to target item on gallery index changes', async () => { const scrollToSpy: jasmine.Spy = spyOn(smoothScrollDirective, 'scrollTo').and.callThrough(); - await afterTimeout(16); + await firstValueFrom(galleryRef.afterItemsVisible); // Trigger index change galleryRef.set(1, 'auto'); expect(smoothScrollDirective.scrolling()).toBe(true); - await afterTimeout(50); + const arrivedToNextItem$: Observable = galleryRef.indexChanged.pipe( + filter((currIndex: number) => currIndex === 1) + ); + await firstValueFrom(arrivedToNextItem$); const pos: SmoothScrollOptions = { start: 500, @@ -75,14 +79,17 @@ describe('Smooth scroll directive', () => { it('should scroll smoothly to target item on gallery index changes', async () => { const scrollToSpy: jasmine.Spy = spyOn(smoothScrollDirective, 'scrollTo').and.callThrough(); - await afterTimeout(16); + await firstValueFrom(galleryRef.afterItemsVisible); // Trigger index change galleryRef.set(2, 'smooth'); expect(smoothScrollDirective.scrolling()).toBe(true); - await afterTimeout(500); + const arrivedToNextItem$: Observable = galleryRef.indexChanged.pipe( + filter((currIndex: number) => currIndex === 2) + ); + await firstValueFrom(arrivedToNextItem$); const pos: SmoothScrollOptions = { start: 1000, @@ -92,5 +99,4 @@ describe('Smooth scroll directive', () => { expect(galleryRef.currIndex()).toBe(2); expect(smoothScrollDirective.scrolling()).toBe(false); }); - }); diff --git a/projects/ng-gallery/src/lib/utils/resize-observer.ts b/projects/ng-gallery/src/lib/utils/resize-observer.ts deleted file mode 100644 index 5a319c76..00000000 --- a/projects/ng-gallery/src/lib/utils/resize-observer.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Observable, Subscriber, mergeMap } from 'rxjs'; - -export function resizeObservable(el: HTMLElement, setter?: (ref: ResizeObserver) => void): Observable { - return new Observable((subscriber: Subscriber) => { - const resizeObserver: ResizeObserver = new ResizeObserver((entries: ResizeObserverEntry[]) => subscriber.next(entries)); - resizeObserver.observe(el); - if (setter) { - setter(resizeObserver); - } - return () => resizeObserver.disconnect(); - }).pipe( - mergeMap((entries: ResizeObserverEntry[]) => entries) - ); -} - diff --git a/projects/ng-gallery/src/public-api.ts b/projects/ng-gallery/src/public-api.ts index 83af4246..cbac9ce9 100644 --- a/projects/ng-gallery/src/public-api.ts +++ b/projects/ng-gallery/src/public-api.ts @@ -5,6 +5,7 @@ export * from './lib/components/gallery-thumbs.component'; export * from './lib/components/gallery-bullets.component'; export * from './lib/components/gallery-counter.component'; export * from './lib/utils/img-recognizer'; +export * from './lib/observers/auto-height'; export * from './lib/components/gallery.component'; export * from './lib/components/templates/items.model'; export * from './lib/components/templates/gallery-iframe.component';