From 2144c7a3d97bc97ce10ebd9a5d9baeace4dce8f3 Mon Sep 17 00:00:00 2001 From: Sergey Andrievskiy Date: Wed, 26 Jun 2019 19:33:05 +0300 Subject: [PATCH] fix(layout): prevent layout scroll disappearing when overlay open --- .../layouts/one-column/one-column.layout.ts | 21 ++- .../window-mode-block-scroll.service.ts | 132 ++++++++++++++++++ src/app/@theme/styles/themes.scss | 4 + src/app/@theme/theme.module.ts | 2 + 4 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 src/app/@theme/services/window-mode-block-scroll.service.ts diff --git a/src/app/@theme/layouts/one-column/one-column.layout.ts b/src/app/@theme/layouts/one-column/one-column.layout.ts index 3397032a..eb8bc88e 100644 --- a/src/app/@theme/layouts/one-column/one-column.layout.ts +++ b/src/app/@theme/layouts/one-column/one-column.layout.ts @@ -1,4 +1,8 @@ -import { Component } from '@angular/core'; +import { AfterViewInit, Component, Inject, PLATFORM_ID, ViewChild } from '@angular/core'; +import { isPlatformBrowser } from '@angular/common'; +import { NbLayoutComponent } from '@nebular/theme'; + +import { WindowModeBlockScrollService } from '../../services/window-mode-block-scroll.service'; @Component({ selector: 'ngx-one-column-layout', @@ -23,5 +27,18 @@ import { Component } from '@angular/core'; `, }) -export class OneColumnLayoutComponent { +export class OneColumnLayoutComponent implements AfterViewInit { + + @ViewChild(NbLayoutComponent, { static: false }) layout: NbLayoutComponent; + + constructor( + @Inject(PLATFORM_ID) private platformId, + private windowModeBlockScrollService: WindowModeBlockScrollService, + ) {} + + ngAfterViewInit() { + if (isPlatformBrowser(this.platformId)) { + this.windowModeBlockScrollService.register(this.layout); + } + } } diff --git a/src/app/@theme/services/window-mode-block-scroll.service.ts b/src/app/@theme/services/window-mode-block-scroll.service.ts new file mode 100644 index 00000000..28b1d97b --- /dev/null +++ b/src/app/@theme/services/window-mode-block-scroll.service.ts @@ -0,0 +1,132 @@ +import { Inject, Injectable, OnDestroy } from '@angular/core'; +import { coerceCssPixelValue } from '@angular/cdk/coercion'; +import { + NB_WINDOW, + NbLayoutComponent, + NbLayoutDimensions, + NbLayoutRulerService, + NbLayoutScrollService, + NbViewportRulerAdapter, +} from '@nebular/theme'; +import { filter, map, take, takeUntil } from 'rxjs/operators'; +import { fromEvent as observableFromEvent, merge, Subject } from 'rxjs'; + +@Injectable() +export class WindowModeBlockScrollService implements OnDestroy { + + private destroy$ = new Subject(); + + private blockEnabled = false; + private unblock$ = new Subject(); + + private container: HTMLElement; + private content: HTMLElement; + + private previousScrollPosition: { top: number, left: number }; + private previousContainerStyles: { overflowY: string }; + private previousContentStyles: { left: string, top: string, width: string, overflow: string, position: string }; + + constructor( + private scrollService: NbLayoutScrollService, + private viewportRuler: NbViewportRulerAdapter, + private layout: NbLayoutRulerService, + @Inject(NB_WINDOW) private window, + ) {} + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + this.unblock$.complete(); + } + + register(layout: NbLayoutComponent) { + this.container = layout.scrollableContainerRef.nativeElement; + this.content = this.container.children[0] as HTMLElement; + + this.scrollService.onScrollableChange() + .pipe( + filter(() => layout.windowModeValue), + map((scrollable: boolean) => !scrollable), + takeUntil(this.destroy$), + ) + .subscribe((shouldBlock: boolean) => { + if (shouldBlock) { + this.blockScroll(); + } else { + this.unblockScroll(); + } + }); + } + + blockScroll() { + if (!this.canBeBlocked()) { + return; + } + + this.previousScrollPosition = this.viewportRuler.getViewportScrollPosition(); + this.backupStyles(); + + this.container.style.overflowY = 'scroll'; + this.content.style.overflow = 'hidden'; + this.content.style.position = 'fixed'; + this.updateContentSizeAndPosition(); + + observableFromEvent(this.window, 'resize') + .pipe( + takeUntil(merge(this.destroy$, this.unblock$).pipe(take(1))), + ) + .subscribe(() => this.updateContentSizeAndPosition()); + + this.blockEnabled = true; + } + + unblockScroll() { + if (this.blockEnabled) { + this.restoreStyles(); + this.scrollService.scrollTo(this.previousScrollPosition.left, this.previousScrollPosition.top); + this.unblock$.next(); + this.blockEnabled = false; + } + } + + private canBeBlocked(): boolean { + if (this.blockEnabled) { + return false; + } + + const { height: containerHeight } = this.viewportRuler.getViewportSize(); + return this.content.scrollHeight > containerHeight; + } + + private updateContentSizeAndPosition() { + const { top, left } = this.container.getBoundingClientRect(); + this.content.style.left = coerceCssPixelValue(-this.previousScrollPosition.left + left); + this.content.style.top = coerceCssPixelValue(-this.previousScrollPosition.top + top); + this.layout.getDimensions() + .pipe( + map(({ clientWidth }: NbLayoutDimensions) => coerceCssPixelValue(clientWidth)), + take(1), + ) + .subscribe((clientWidth: string) => this.content.style.width = clientWidth); + } + + private backupStyles() { + this.previousContainerStyles = { overflowY: this.container.style.overflowY }; + this.previousContentStyles = { + overflow: this.content.style.overflow, + position: this.content.style.position, + left: this.content.style.left, + top: this.content.style.top, + width: this.content.style.width, + }; + } + + private restoreStyles() { + this.container.style.overflowY = this.previousContainerStyles.overflowY; + this.content.style.overflow = this.previousContentStyles.overflow; + this.content.style.position = this.previousContentStyles.position; + this.content.style.left = this.previousContentStyles.left; + this.content.style.top = this.previousContentStyles.top; + this.content.style.width = this.previousContentStyles.width; + } +} diff --git a/src/app/@theme/styles/themes.scss b/src/app/@theme/styles/themes.scss index 796c53a6..2ee0b9e0 100644 --- a/src/app/@theme/styles/themes.scss +++ b/src/app/@theme/styles/themes.scss @@ -6,6 +6,7 @@ $nb-themes: nb-register-theme(( font-family-secondary: font-family-primary, layout-padding-top: 2.25rem, + layout-window-mode-padding-top: 0, menu-item-icon-margin: 0 0.5rem 0 0, @@ -28,6 +29,7 @@ $nb-themes: nb-register-theme(( $nb-themes: nb-register-theme(( font-family-secondary: font-family-primary, layout-padding-top: 2.25rem, + layout-window-mode-padding-top: 0, menu-item-icon-margin: 0 0.5rem 0 0, @@ -50,6 +52,7 @@ $nb-themes: nb-register-theme(( $nb-themes: nb-register-theme(( font-family-secondary: font-family-primary, layout-padding-top: 2.25rem, + layout-window-mode-padding-top: 0, menu-item-icon-margin: 0 0.5rem 0 0, @@ -72,6 +75,7 @@ $nb-themes: nb-register-theme(( $nb-themes: nb-register-theme(( font-family-secondary: font-family-primary, layout-padding-top: 2.25rem, + layout-window-mode-padding-top: 0, menu-item-icon-margin: 0 0.5rem 0 0, diff --git a/src/app/@theme/theme.module.ts b/src/app/@theme/theme.module.ts index 5ae45d8c..ec66139a 100644 --- a/src/app/@theme/theme.module.ts +++ b/src/app/@theme/theme.module.ts @@ -34,6 +34,7 @@ import { ThreeColumnsLayoutComponent, TwoColumnsLayoutComponent, } from './layouts'; +import { WindowModeBlockScrollService } from './services/window-mode-block-scroll.service'; import { DEFAULT_THEME } from './styles/theme.default'; import { COSMIC_THEME } from './styles/theme.cosmic'; import { CORPORATE_THEME } from './styles/theme.corporate'; @@ -86,6 +87,7 @@ export class ThemeModule { }, [ DEFAULT_THEME, COSMIC_THEME, CORPORATE_THEME, DARK_THEME ], ).providers, + WindowModeBlockScrollService, ], }; }