Skip to content

Commit

Permalink
feat(layout): add scroll & ruler services
Browse files Browse the repository at this point in the history
  • Loading branch information
nnixaa committed Jul 20, 2018
1 parent 657147b commit 043050a
Show file tree
Hide file tree
Showing 12 changed files with 665 additions and 2 deletions.
32 changes: 32 additions & 0 deletions docs/structure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,38 @@ export const structure = [
},
],
},
{
type: 'page',
name: 'LayoutScrollService',
children: [
{
type: 'block',
block: 'component',
source: 'NbLayoutScrollService',
},
{
type: 'block',
block: 'component',
source: 'NbScrollPosition',
},
],
},
{
type: 'page',
name: 'LayoutRulerService',
children: [
{
type: 'block',
block: 'component',
source: 'NbLayoutRulerService',
},
{
type: 'block',
block: 'component',
source: 'NbLayoutDimensions',
},
],
},
],
},
{
Expand Down
109 changes: 107 additions & 2 deletions src/framework/theme/components/layout/layout.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { convertToBoolProperty } from '../helpers';
import { NbThemeService } from '../../services/theme.service';
import { NbSpinnerService } from '../../services/spinner.service';
import { NbLayoutDirectionService } from '../../services/direction.service';
import { NbScrollPosition, NbLayoutScrollService } from '../../services/scroll.service';
import { NbLayoutDimensions, NbLayoutRulerService } from '../../services/ruler.service';
import { NB_WINDOW, NB_DOCUMENT } from '../../theme.options';

/**
Expand Down Expand Up @@ -248,7 +250,7 @@ export class NbLayoutFooterComponent {
styleUrls: ['./layout.component.scss'],
template: `
<ng-template #layoutTopDynamicArea></ng-template>
<div class="scrollable-container" #scrollableContainer>
<div class="scrollable-container" #scrollableContainer (scroll)="onScroll($event)">
<div class="layout">
<ng-content select="nb-layout-header:not([subheader])"></ng-content>
<div class="layout-container">
Expand Down Expand Up @@ -332,6 +334,8 @@ export class NbLayoutComponent implements AfterViewInit, OnInit, OnDestroy {
@Inject(NB_DOCUMENT) protected document,
@Inject(PLATFORM_ID) protected platformId: Object,
protected layoutDirectionService: NbLayoutDirectionService,
protected scrollService: NbLayoutScrollService,
protected rulerService: NbLayoutRulerService,
) {

this.themeService.onThemeChange()
Expand Down Expand Up @@ -371,6 +375,24 @@ export class NbLayoutComponent implements AfterViewInit, OnInit, OnDestroy {
}));
this.spinnerService.load();

this.rulerService.onGetDimensions()
.pipe(
takeWhile(() => this.alive),
)
.subscribe(({ listener }) => {
listener.next(this.getDimensions());
listener.complete();
});

this.scrollService.onGetPosition()
.pipe(
takeWhile(() => this.alive),
)
.subscribe(({ listener }) => {
listener.next(this.getScrollPosition());
listener.complete();
});

if (isPlatformBrowser(this.platformId)) {
// trigger first time so that after the change we have the initial value
this.themeService.changeWindowWidth(this.window.innerWidth);
Expand Down Expand Up @@ -403,6 +425,10 @@ export class NbLayoutComponent implements AfterViewInit, OnInit, OnDestroy {
this.renderer.setProperty(this.document, 'dir', direction);
});

this.scrollService.onManualScroll()
.pipe(takeWhile(() => this.alive))
.subscribe(({ x, y }: NbScrollPosition) => this.scroll(x, y));

this.afterViewInit$.next(true);
}

Expand All @@ -415,19 +441,98 @@ export class NbLayoutComponent implements AfterViewInit, OnInit, OnDestroy {
this.alive = false;
}

@HostListener('window:scroll', ['$event'])
onScroll($event) {
this.scrollService.fireScrollChange($event);
}

@HostListener('window:resize', ['$event'])
onResize(event) {
this.themeService.changeWindowWidth(event.target.innerWidth);
}

/**
* Returns scroll and client height/width
*
* Depending on the current scroll mode (`withScroll=true`) returns sizes from the body element
* or from the `.scrollable-container`
* @returns {NbLayoutDimensions}
*/
getDimensions(): NbLayoutDimensions {
let clientWidth, clientHeight, scrollWidth, scrollHeight = 0;
if (this.withScrollValue) {
const container = this.scrollableContainerRef.nativeElement;
clientWidth = container.clientWidth;
clientHeight = container.clientHeight;
scrollWidth = container.scrollWidth;
scrollHeight = container.scrollHeight;
} else {
const { documentElement, body } = this.document;
clientWidth = documentElement.clientWidth || body.clientWidth;
clientHeight = documentElement.clientHeight || body.clientHeight;
scrollWidth = documentElement.scrollWidth || body.scrollWidth;
scrollHeight = documentElement.scrollHeight || body.scrollHeight;
}

return {
clientWidth,
clientHeight,
scrollWidth,
scrollHeight,
};
}

/**
* Returns scroll position of current scroll container.
*
* If `withScroll` = true, returns scroll position of the `.scrollable-container` element,
* otherwise - of the scrollable element of the window (which may be different depending of a browser)
*
* @returns {NbScrollPosition}
*/
getScrollPosition(): NbScrollPosition {
if (this.withScrollValue) {
const container = this.scrollableContainerRef.nativeElement;
return { x: container.scrollLeft, y: container.scrollTop };
}

const documentRect = this.document.documentElement.getBoundingClientRect();

const x = -documentRect.left || this.document.body.scrollLeft || this.window.scrollX ||
this.document.documentElement.scrollLeft || 0;

const y = -documentRect.top || this.document.body.scrollTop || this.window.scrollY ||
this.document.documentElement.scrollTop || 0;


return { x, y };
}

private initScrollTop() {
this.router.events
.pipe(
takeWhile(() => this.alive),
filter(event => event instanceof NavigationEnd),
)
.subscribe(() => {
this.scrollableContainerRef.nativeElement.scrollTo && this.scrollableContainerRef.nativeElement.scrollTo(0, 0);
this.scroll(0, 0);
});
}

private scroll(x: number, y: number) {
if (!isPlatformBrowser(this.platformId)) {
return;
}
if (this.withScrollValue) {
const scrollable = this.scrollableContainerRef.nativeElement;
if (scrollable.scrollTo) {
scrollable.scrollTo(x, y);
} else {
scrollable.scrollLeft = x;
scrollable.scrollTop = y;
}
} else {
this.window.scrollTo(x, y);
}
}
}
2 changes: 2 additions & 0 deletions src/framework/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export * from './services/spinner.service';
export * from './services/breakpoints.service';
export * from './services/color.helper';
export * from './services/direction.service';
export * from './services/scroll.service';
export * from './services/ruler.service';
export * from './components/card/card.module';
export * from './components/layout/layout.module';
export * from './components/menu/menu.module';
Expand Down
133 changes: 133 additions & 0 deletions src/framework/theme/services/ruler.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { Component, ElementRef, ViewChild } from '@angular/core';
import { APP_BASE_HREF } from '@angular/common';
import { RouterModule } from '@angular/router';
import { TestBed, ComponentFixture, fakeAsync, tick, async, inject } from '@angular/core/testing';
import { NbLayoutRulerService, NbLayoutDimensions } from './ruler.service';
import { NbLayoutModule } from '../components/layout/layout.module';
import { NbThemeService } from './theme.service';
import { NbThemeModule } from '../theme.module';
import { NB_DOCUMENT } from '../theme.options';

let currentDocument;
let fixture: ComponentFixture<RulerTestComponent>;
let componentInstance: RulerTestComponent;
let rulerService: NbLayoutRulerService;

@Component({
template: `
<nb-layout [withScroll]="localScroll" #layout>
<nb-layout-column>
<div #resize></div>
</nb-layout-column>
</nb-layout>
`,
})
class RulerTestComponent {

@ViewChild('resize', { read: ElementRef }) private resizeElement: ElementRef;
@ViewChild('layout', { read: ElementRef }) private layout: ElementRef;
localScroll = false;

setSize(width: string, height: string) {
this.resizeElement.nativeElement.style.width = width;
this.resizeElement.nativeElement.style.height = height;
}

useLocalScroll() {
this.localScroll = true;
}

useGlobalScroll() {
this.localScroll = false;
}

getScrollableElement() {
return this.layout.nativeElement.querySelector('.scrollable-container');
}
}

// This is rather a smoke test
describe('NbLayoutRulerService', () => {

beforeEach(() => {
fixture = TestBed.configureTestingModule({
imports: [ RouterModule.forRoot([]), NbThemeModule.forRoot({ name: 'default' }), NbLayoutModule ],
providers: [ NbLayoutRulerService, NbThemeService, { provide: APP_BASE_HREF, useValue: '/' } ],
declarations: [ RulerTestComponent ],
})
.createComponent(RulerTestComponent);

componentInstance = fixture.componentInstance;

fixture.detectChanges();
});

beforeEach(async(inject(
[NbLayoutRulerService, NB_DOCUMENT],
(_rulerService, _document) => {
rulerService = _rulerService;
currentDocument = _document;
},
)));

afterEach(fakeAsync(() => {
fixture.destroy();
tick();
fixture.nativeElement.remove();
}));

it('should get dimensions from document', (done) => {
fixture.detectChanges();
rulerService.getDimensions()
.subscribe((size: NbLayoutDimensions) => {
expect(size.clientHeight).toEqual(currentDocument.documentElement.clientHeight);
expect(size.clientWidth).toEqual(currentDocument.documentElement.clientWidth);
expect(size.scrollHeight).toEqual(currentDocument.documentElement.scrollHeight);
expect(size.scrollWidth).toEqual(currentDocument.documentElement.scrollWidth);
done();
})
});

it('should get dimensions from document when scrolls', (done) => {
componentInstance.setSize('10000px', '10000px');
fixture.detectChanges();
rulerService.getDimensions()
.subscribe((size: NbLayoutDimensions) => {
expect(size.clientHeight).toEqual(currentDocument.documentElement.clientHeight);
expect(size.clientWidth).toEqual(currentDocument.documentElement.clientWidth);
expect(size.scrollHeight).toEqual(currentDocument.documentElement.scrollHeight);
expect(size.scrollWidth).toEqual(currentDocument.documentElement.scrollWidth);
done();
})
});

it('should get dimensions from scrollable', (done) => {
componentInstance.useLocalScroll();
fixture.detectChanges();
const scrollable = componentInstance.getScrollableElement();
rulerService.getDimensions()
.subscribe((size: NbLayoutDimensions) => {
expect(size.clientHeight).toEqual(scrollable.clientHeight);
expect(size.clientWidth).toEqual(scrollable.clientWidth);
expect(size.scrollHeight).toEqual(scrollable.scrollHeight);
expect(size.scrollWidth).toEqual(scrollable.scrollWidth);
done();
})
});

it('should get dimensions from scrollable when scrolls', (done) => {
componentInstance.useLocalScroll();
componentInstance.setSize('10000px', '10000px');
fixture.detectChanges();
const scrollable = componentInstance.getScrollableElement();
rulerService.getDimensions()
.subscribe((size: NbLayoutDimensions) => {
expect(size.clientHeight).toEqual(scrollable.clientHeight);
expect(size.clientWidth).toEqual(scrollable.clientWidth);
expect(size.scrollHeight).toEqual(scrollable.scrollHeight);
expect(size.scrollWidth).toEqual(scrollable.scrollWidth);
done();
})
});

});
Loading

0 comments on commit 043050a

Please sign in to comment.