From a163353de7c8479555ac25ec74a6dde01cde21bd Mon Sep 17 00:00:00 2001 From: Badisi Date: Fri, 21 Jun 2024 19:27:26 +0200 Subject: [PATCH] feat(overlay): first commit --- projects/overlay/package.json | 14 +- .../overlay/src/connection-position-pair.ts | 30 +++ .../src/example/example.component.spec.ts | 22 -- .../overlay/src/example/example.component.ts | 11 - .../src/example/example.service.spec.ts | 16 -- .../overlay/src/example/example.service.ts | 6 - projects/overlay/src/index.ts | 8 +- projects/overlay/src/overlay.component.html | 5 + projects/overlay/src/overlay.component.scss | 5 + projects/overlay/src/overlay.component.ts | 195 ++++++++++++++++++ projects/overlay/tsconfig.lib.json | 2 +- 11 files changed, 248 insertions(+), 66 deletions(-) create mode 100644 projects/overlay/src/connection-position-pair.ts delete mode 100644 projects/overlay/src/example/example.component.spec.ts delete mode 100644 projects/overlay/src/example/example.component.ts delete mode 100644 projects/overlay/src/example/example.service.spec.ts delete mode 100644 projects/overlay/src/example/example.service.ts create mode 100644 projects/overlay/src/overlay.component.html create mode 100644 projects/overlay/src/overlay.component.scss create mode 100644 projects/overlay/src/overlay.component.ts diff --git a/projects/overlay/package.json b/projects/overlay/package.json index b6c9e610..058f4fe6 100644 --- a/projects/overlay/package.json +++ b/projects/overlay/package.json @@ -11,7 +11,7 @@ ], "repository": { "type": "git", - "url": "https://github.com/dsi-hug/ngx-components.git" + "url": "git+https://github.com/dsi-hug/ngx-components.git" }, "keywords": [ "angular", @@ -24,11 +24,17 @@ "lint": "eslint . --fix", "test": "ng test overlay", "test:ci": "ng test overlay --watch=false --browsers=ChromeHeadless", - "build": "ng build overlay -c production" + "build:ng": "ng build overlay -c=production", + "build": "nx build:ng @hug/ngx-overlay --verbose", + "release": "nx release -p=@hug/ngx-overlay --yes --verbose", + "release:dry-run": "nx release -p=@hug/ngx-overlay --verbose --dry-run" }, "peerDependencies": { - "@angular/common": "^14.3.0", - "@angular/core": "^14.3.0" + "@angular/common": ">= 14", + "@angular/core": ">= 14", + "@angular/cdk": ">= 14", + "rxjs": ">= 7.0.0", + "@hug/ngx-core": "1.1.2" }, "dependencies": { "tslib": "^2.6.3" diff --git a/projects/overlay/src/connection-position-pair.ts b/projects/overlay/src/connection-position-pair.ts new file mode 100644 index 00000000..ee78043c --- /dev/null +++ b/projects/overlay/src/connection-position-pair.ts @@ -0,0 +1,30 @@ +import { ConnectionPositionPair, OriginConnectionPosition, OverlayConnectionPosition } from '@angular/cdk/overlay'; + +export class OverlayConnectionPositionPair extends ConnectionPositionPair { + public static parse(value: string): ConnectionPositionPair[] { + const values = value.trim().split(','); + const positions = new Array(); + values.forEach(pos => { + const poss = pos.trim().split(' '); + if (poss.length !== 4) { + throw new Error('Invalid positions property for DejaMenuComponent. String entry must be of type \'positions="start top end bottom"\''); + } + + const originPosition = { + originX: poss[0], + originY: poss[1] + } as OriginConnectionPosition; + + const overlayPosition = { + overlayX: poss[2], + overlayY: poss[3] + } as OverlayConnectionPosition; + + positions.push(new OverlayConnectionPositionPair(originPosition, overlayPosition)); + }); + + return positions; + } +} + +export const defaultConnectionPositionPair: ConnectionPositionPair[] = OverlayConnectionPositionPair.parse('start bottom start top,start top start bottom,end bottom end top,end top end bottom'); diff --git a/projects/overlay/src/example/example.component.spec.ts b/projects/overlay/src/example/example.component.spec.ts deleted file mode 100644 index 61022488..00000000 --- a/projects/overlay/src/example/example.component.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { ExampleComponent } from './example.component'; - -describe('ExampleComponent', () => { - let component: ExampleComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ExampleComponent] - }).compileComponents(); - - fixture = TestBed.createComponent(ExampleComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/projects/overlay/src/example/example.component.ts b/projects/overlay/src/example/example.component.ts deleted file mode 100644 index d6dc2085..00000000 --- a/projects/overlay/src/example/example.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; - -@Component({ - selector: 'deja-example', - standalone: true, - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [], - template: '

example works!

', - styles: [] -}) -export class ExampleComponent { } diff --git a/projects/overlay/src/example/example.service.spec.ts b/projects/overlay/src/example/example.service.spec.ts deleted file mode 100644 index 33a65090..00000000 --- a/projects/overlay/src/example/example.service.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { ExampleService } from './example.service'; - -describe('ExampleService', () => { - let service: ExampleService; - - beforeEach(() => { - TestBed.configureTestingModule({}); - service = TestBed.inject(ExampleService); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); -}); diff --git a/projects/overlay/src/example/example.service.ts b/projects/overlay/src/example/example.service.ts deleted file mode 100644 index 278d5bcf..00000000 --- a/projects/overlay/src/example/example.service.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable({ - providedIn: 'root' -}) -export class ExampleService { } diff --git a/projects/overlay/src/index.ts b/projects/overlay/src/index.ts index 020b9f72..067724cb 100644 --- a/projects/overlay/src/index.ts +++ b/projects/overlay/src/index.ts @@ -1,6 +1,2 @@ -/* - * Public API Surface of lib - */ - -export * from './example/example.service'; -export * from './example/example.component'; +export * from './overlay.component'; +export * from './connection-position-pair'; diff --git a/projects/overlay/src/overlay.component.html b/projects/overlay/src/overlay.component.html new file mode 100644 index 00000000..8f656048 --- /dev/null +++ b/projects/overlay/src/overlay.component.html @@ -0,0 +1,5 @@ + + + + + diff --git a/projects/overlay/src/overlay.component.scss b/projects/overlay/src/overlay.component.scss new file mode 100644 index 00000000..45089fa0 --- /dev/null +++ b/projects/overlay/src/overlay.component.scss @@ -0,0 +1,5 @@ +@media print { + .ngx-overlay-container { + display: none; + } +} diff --git a/projects/overlay/src/overlay.component.ts b/projects/overlay/src/overlay.component.ts new file mode 100644 index 00000000..feeda935 --- /dev/null +++ b/projects/overlay/src/overlay.component.ts @@ -0,0 +1,195 @@ +import { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion'; +import { CdkConnectedOverlay, CdkOverlayOrigin, OverlayContainer, OverlayModule } from '@angular/cdk/overlay'; +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild, ViewEncapsulation } from '@angular/core'; +import { MediaService } from '@hug/ngx-core'; +import { BehaviorSubject, combineLatestWith, delay, distinctUntilChanged, EMPTY, map, mergeWith, Observable, of, ReplaySubject, shareReplay, startWith, Subject, switchMap } from 'rxjs'; + +import { defaultConnectionPositionPair, OverlayConnectionPositionPair } from './connection-position-pair'; + +export interface ShowParams { + event?: MouseEvent; + offsetX?: number; + offsetY?: number; +} + +interface OverlayInfos { + offsetX: number; + offsetY: number; + origin: CdkOverlayOrigin; + width: string; +} + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + selector: 'overlay', + styleUrls: ['./overlay.component.scss'], + templateUrl: './overlay.component.html', + standalone: true, + imports: [ + CommonModule, + OverlayModule + ] +}) +export class OverlayComponent implements OnChanges { + @Input() public ownerElement!: HTMLElement; + + @Input() public width!: string; + + @Input() public widthForMobile = '100%'; + + @Input() public overlayBackdropClass = 'cdk-overlay-transparent-backdrop'; + + @Input() public overlayContainerClass?: string; + + @Input() public isMobile?: BooleanInput; + + /** Overlay pane containing the options. */ + @ViewChild(CdkConnectedOverlay, { static: true }) private overlay?: CdkConnectedOverlay; + + public readonly isVisible$: Observable; + + protected overlayInfos$: Observable; + + private show$ = new ReplaySubject(1); + private hide$ = new Subject(); + + /** Renvoie une valeur qui indique si le dialog est affiché. */ + private isMobileExt$ = new BehaviorSubject(undefined); + + private _hasBackdrop = true; + private ownerElement$ = new BehaviorSubject(undefined); + + @Input() public set hasBackdrop(value: BooleanInput) { + this._hasBackdrop = coerceBooleanProperty(value); + } + + public get hasBackdrop(): BooleanInput { + return this._hasBackdrop; + } + + private _positions = defaultConnectionPositionPair; + private _positionsForMobile?: OverlayConnectionPositionPair[]; + + public constructor(private elementRef: ElementRef, private overlayContainer: OverlayContainer, mediaService: MediaService) { + const containerElement = this.overlayContainer.getContainerElement(); + containerElement.addEventListener('contextmenu', (event: Event) => { + event.preventDefault(); + return false; + }); + + const isMobile$ = this.isMobileExt$.pipe( + switchMap(isMobileExt => { + if (isMobileExt !== undefined) { + return of(isMobileExt); + } + + return mediaService.isMobile$; + }), + startWith(false), + shareReplay({ bufferSize: 1, refCount: false }) + ); + + this.overlayInfos$ = this.show$.pipe( + mergeWith(this.hide$), + switchMap(showParams => { + if (!showParams) { + return of(undefined); + } + + containerElement.className = Array.from(containerElement.classList) + .filter(token => token.startsWith('cdk')) + .join(' '); + + containerElement.classList.add('ngx-overlay-container'); + if (this.overlayContainerClass) { + this.overlayContainerClass.split(' ').forEach(className => { + containerElement.classList.add(className); + }); + } + + const info$ = this.ownerElement$.pipe( + combineLatestWith(isMobile$), + map(([ownerElement, isMobile]) => ({ + offsetX: showParams.offsetX && +showParams.offsetX || 0, + offsetY: showParams.offsetY && +showParams.offsetY || 0, + origin: new CdkOverlayOrigin(new ElementRef((isMobile && document.body) ?? showParams.event?.target ?? ownerElement ?? this.elementRef.nativeElement)), + width: isMobile ? this.widthForMobile : this.width + } as OverlayInfos)) + ); + + const updatePosition$ = info$.pipe( + delay(1), + switchMap(() => { + this.overlay?.overlayRef?.updatePosition(); + return EMPTY; + }) + ); + + return info$.pipe( + mergeWith(updatePosition$) + ); + }), + shareReplay({ bufferSize: 1, refCount: false }) + ); + + + this.isVisible$ = this.overlayInfos$.pipe( + map(Boolean), + startWith(false), + distinctUntilChanged() + ); + } + + public ngOnChanges(changes: SimpleChanges): void { + if (changes['isMobile']) { + this.isMobileExt$.next(this.isMobile !== undefined ? coerceBooleanProperty(this.isMobile) : undefined); + } + + if (changes['ownerElement']) { + this.ownerElement$.next(this.ownerElement); + } + } + + public get positionPairs(): OverlayConnectionPositionPair[] { + return this.positions; + } + + public get positions(): OverlayConnectionPositionPair[] { + if (!this.isMobile) { + return this._positions; + } else if (this._positionsForMobile) { + return this._positionsForMobile; + } else { + return OverlayConnectionPositionPair.parse('start top start top'); + } + } + + @Input() + public set positions(value: OverlayConnectionPositionPair[] | string) { + this._positions = typeof value === 'string' ? OverlayConnectionPositionPair.parse(value) : value; + } + + /** Si pas null, sera utilisé quand isMobile est vrai. Si null et si isMobile est vrai, + * alors c'est la valeur 'start top start top' qui est utilisée. + * */ + @Input() + public set positionsForMobile(value: OverlayConnectionPositionPair[] | string) { + this._positionsForMobile = typeof value === 'string' ? OverlayConnectionPositionPair.parse(value) : value; + } + + /** Affiche le dialog. */ + public show(eventOrOffsetX?: MouseEvent | number, offsetY?: number): void { + if (typeof eventOrOffsetX === 'number') { + this.show$.next({ offsetX: eventOrOffsetX, offsetY }); + } else { + this.show$.next({ event: eventOrOffsetX, offsetY }); + } + } + + /** Ferme le dialog. */ + public close(): void { + this.hide$.next(); + } +} diff --git a/projects/overlay/tsconfig.lib.json b/projects/overlay/tsconfig.lib.json index 8f6c5298..599bb9f4 100644 --- a/projects/overlay/tsconfig.lib.json +++ b/projects/overlay/tsconfig.lib.json @@ -1,6 +1,6 @@ /* To learn more about this file see: https://angular.io/config/tsconfig. */ { - "extends": "../../tsconfig.json", + "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "../../out-tsc/lib", "declaration": true,