mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
44988: added reducer, angular animation, tests...
This commit is contained in:
@@ -1,10 +1,8 @@
|
||||
<div class="search-page">
|
||||
<ds-search-sidebar dsStick *ngIf="!(isMobileView | async)" class="col-3 sidebar-md-fixed" id="search-sidebar"
|
||||
resultCount="{{(results.pageInfo | async)?.totalElements}}"
|
||||
(toggleSidebar)="toggle()"></ds-search-sidebar>
|
||||
<div id="search-header" class="row">
|
||||
<ds-layout-controls class="col-md-3 d-none d-md-block sidebar-md-fixed"
|
||||
[isList]="isListView"
|
||||
(toggleList)="setListView($event)"></ds-layout-controls>
|
||||
|
||||
<ds-view-mode-switch></ds-view-mode-switch>
|
||||
<ds-search-form id="search-form" class="col-12 col-md-9 ml-md-auto"
|
||||
[query]="query"
|
||||
[scope]="scopeObject?.payload | async"
|
||||
@@ -13,20 +11,18 @@
|
||||
</ds-search-form>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div id="search-body" class="row-offcanvas row-offcanvas-left" [ngClass]="{'active': isSidebarActive}">
|
||||
<ds-search-sidebar class="col-12 col-md-3 sidebar-md-fixed" id="search-sidebar"
|
||||
<div id="search-body" class="row-offcanvas row-offcanvas-left" [@slideInOut]="(isSidebarCollapsed | async) ? 'collapsed' : 'expanded'">
|
||||
<ds-search-sidebar *ngIf="(isMobileView | async)" class="col-12" id="search-sidebar-xs"
|
||||
resultCount="{{(results.pageInfo | async)?.totalElements}}"
|
||||
(toggleSidebar)="setSidebarActive($event)"></ds-search-sidebar>
|
||||
(toggleSidebar)="toggle()"></ds-search-sidebar>
|
||||
<div id="search-content" class="col-12 col-md-9 ml-md-auto">
|
||||
<div class="d-block d-md-none search-controls clearfix">
|
||||
|
||||
<ds-view-mode-switch></ds-view-mode-switch>
|
||||
<ds-layout-controls [isList]="isListView"
|
||||
(toggleList)="setListView($event)"></ds-layout-controls>
|
||||
<button (click)="setSidebarActive(true)" aria-controls="#search-body"
|
||||
<button (click)="toggle()" aria-controls="#search-body"
|
||||
class="btn btn-outline-primary float-right"><i
|
||||
class="fa fa-sliders"></i> {{"search.sidebar.open" |
|
||||
translate}}
|
||||
class="fa fa-sliders"></i> {{"search.sidebar.open"
|
||||
| translate}}
|
||||
</button>
|
||||
</div>
|
||||
<ds-search-results [searchResults]="results"
|
||||
|
@@ -22,63 +22,36 @@
|
||||
}
|
||||
@include media-breakpoint-down(sm) {
|
||||
position: relative;
|
||||
-webkit-transition: all .25s ease-out;
|
||||
-o-transition: all .25s ease-out;
|
||||
transition: all .25s ease-out;
|
||||
|
||||
&.row-offcanvas {
|
||||
position: relative;
|
||||
-webkit-transition: all .25s ease-out;
|
||||
-o-transition: all .25s ease-out;
|
||||
transition: all .25s ease-out;
|
||||
}
|
||||
|
||||
&.row-offcanvas-right {
|
||||
right: 0;
|
||||
&.row-offcanvas-right #search-sidebar-xs {
|
||||
right: -100%;
|
||||
}
|
||||
|
||||
&.row-offcanvas-left {
|
||||
left: 0;
|
||||
&.row-offcanvas-left #search-sidebar-xs {
|
||||
left: -100%;
|
||||
}
|
||||
|
||||
&.row-offcanvas-right
|
||||
#search-sidebar {
|
||||
right: -100%; /* 12 columns */
|
||||
}
|
||||
|
||||
&.row-offcanvas-right.active
|
||||
#search-sidebar {
|
||||
right: -100%; /* 6 columns */
|
||||
}
|
||||
|
||||
&.row-offcanvas-left
|
||||
#search-sidebar {
|
||||
left: -100%; /* 12 columns */
|
||||
}
|
||||
|
||||
&.row-offcanvas-left.active
|
||||
#search-sidebar {
|
||||
left: -100%; /* 6 columns */
|
||||
}
|
||||
|
||||
&.row-offcanvas-right.active {
|
||||
right: 100%; /* 6 columns */
|
||||
}
|
||||
|
||||
&.row-offcanvas-left.active {
|
||||
left: 100%; /* 6 columns */
|
||||
}
|
||||
|
||||
#search-sidebar {
|
||||
#search-sidebar-xs {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%; /* 6 columns */
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-md-fixed {
|
||||
@include media-breakpoint-up(md) {
|
||||
position: absolute;
|
||||
margin-top: -$content-spacing;
|
||||
padding-top: $content-spacing;
|
||||
&.stick {
|
||||
top: 0;
|
||||
margin-top: 0px;
|
||||
position: fixed;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
@@ -8,8 +9,11 @@ import { SearchService } from './search-service/search.service';
|
||||
import { Community } from '../core/shared/community.model';
|
||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { SearchPageModule } from './search-page.module';
|
||||
import { Store } from '@ngrx/store';
|
||||
|
||||
describe('SearchPageComponent', () => {
|
||||
fdescribe('SearchPageComponent', () => {
|
||||
let comp: SearchPageComponent;
|
||||
let fixture: ComponentFixture<SearchPageComponent>;
|
||||
let searchServiceObject: SearchService;
|
||||
@@ -39,13 +43,14 @@ describe('SearchPageComponent', () => {
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
// imports: [ SearchPageModule ],
|
||||
declarations: [SearchPageComponent],
|
||||
imports: [ SearchPageModule ],
|
||||
// declarations: [SearchPageComponent],
|
||||
providers: [
|
||||
{ provide: SearchService, useValue: searchServiceStub },
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
{ provide: CommunityDataService, useValue: communityDataServiceStub },
|
||||
{ provide: Router, useClass: RouterStub }
|
||||
{ provide: Router, useClass: RouterStub },
|
||||
{ provide: Store, useClass: {} }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
|
@@ -6,11 +6,20 @@ import { SearchResult } from './search-result.model';
|
||||
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
||||
import { SortOptions } from '../core/cache/models/sort-options.model';
|
||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||
import { ViewModeSwitchComponent } from '../shared/view-mode-switch/view-mode-switch.component';
|
||||
import { SearchOptions } from './search-options.model';
|
||||
import { CommunityDataService } from '../core/data/community-data.service';
|
||||
import { isNotEmpty } from '../shared/empty.util';
|
||||
import { Community } from '../core/shared/community.model';
|
||||
import { createSelector, Store } from '@ngrx/store';
|
||||
import { AppState } from '../app.reducer';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { SearchSidebarState } from './search-sidebar/search-sidebar.reducer';
|
||||
import { SearchSidebarToggleAction } from './search-sidebar/search-sidebar.actions';
|
||||
import { slideInOut } from '../shared/animations/slide';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
|
||||
const sidebarStateSelector = (state: AppState) => state.searchSidebar;
|
||||
const sidebarCollapsedSelector = createSelector(sidebarStateSelector, (sidebar: SearchSidebarState) => sidebar.sidebarCollapsed);
|
||||
|
||||
/**
|
||||
* This component renders a simple item page.
|
||||
@@ -22,6 +31,7 @@ import { Community } from '../core/shared/community.model';
|
||||
selector: 'ds-search-page',
|
||||
styleUrls: ['./search-page.component.scss'],
|
||||
templateUrl: './search-page.component.html',
|
||||
animations: [slideInOut]
|
||||
})
|
||||
export class SearchPageComponent implements OnInit, OnDestroy {
|
||||
|
||||
@@ -34,14 +44,14 @@ export class SearchPageComponent implements OnInit, OnDestroy {
|
||||
currentParams = {};
|
||||
searchOptions: SearchOptions;
|
||||
scopeList: RemoteData<Community[]>;
|
||||
isSidebarActive = false;
|
||||
isListView = true;
|
||||
isSidebarCollapsed: Observable<boolean>;
|
||||
isMobileView: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
private service: SearchService,
|
||||
constructor(private service: SearchService,
|
||||
private route: ActivatedRoute,
|
||||
private communityService: CommunityDataService
|
||||
) {
|
||||
private communityService: CommunityDataService,
|
||||
private store: Store<AppState>,
|
||||
private hostWindowService: HostWindowService) {
|
||||
this.scopeList = communityService.findAll();
|
||||
// Initial pagination config
|
||||
const pagination: PaginationComponentOptions = new PaginationComponentOptions();
|
||||
@@ -50,9 +60,14 @@ export class SearchPageComponent implements OnInit, OnDestroy {
|
||||
pagination.pageSize = 10;
|
||||
const sort: SortOptions = new SortOptions();
|
||||
this.searchOptions = { pagination: pagination, sort: sort };
|
||||
this.isMobileView = Observable.combineLatest(
|
||||
this.hostWindowService.isXs(),
|
||||
this.hostWindowService.isSm(),
|
||||
(isXs, isSm) => isXs || isSm);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isSidebarCollapsed = this.store.select(sidebarCollapsedSelector);
|
||||
this.sub = this.route
|
||||
.queryParams
|
||||
.subscribe((params) => {
|
||||
@@ -93,11 +108,7 @@ export class SearchPageComponent implements OnInit, OnDestroy {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
|
||||
setSidebarActive(show: boolean) {
|
||||
this.isSidebarActive = show;
|
||||
}
|
||||
|
||||
setListView(isList: boolean) {
|
||||
this.isListView = isList;
|
||||
public toggle(): void {
|
||||
this.store.dispatch(new SearchSidebarToggleAction());
|
||||
}
|
||||
}
|
||||
|
@@ -19,8 +19,6 @@ import { LayoutControlsComponent } from './layout-controls/layout-controls.compo
|
||||
imports: [
|
||||
SearchPageRoutingModule,
|
||||
CommonModule,
|
||||
TranslateModule,
|
||||
RouterModule,
|
||||
SharedModule
|
||||
],
|
||||
declarations: [
|
||||
|
@@ -0,0 +1,40 @@
|
||||
import { Action } from '@ngrx/store';
|
||||
|
||||
import { type } from '../../shared/ngrx/type';
|
||||
|
||||
/**
|
||||
* For each action type in an action group, make a simple
|
||||
* enum object for all of this group's action types.
|
||||
*
|
||||
* The 'type' utility function coerces strings into string
|
||||
* literal types and runs a simple check to guarantee all
|
||||
* action types in the application are unique.
|
||||
*/
|
||||
export const SearchSidebarActionTypes = {
|
||||
COLLAPSE: type('dspace/search-sidebar/COLLAPSE'),
|
||||
EXPAND: type('dspace/search-sidebar/EXPAND'),
|
||||
TOGGLE: type('dspace/search-sidebar/TOGGLE')
|
||||
};
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
export class SearchSidebarCollapseAction implements Action {
|
||||
type = SearchSidebarActionTypes.COLLAPSE;
|
||||
}
|
||||
|
||||
export class SearchSidebarExpandAction implements Action {
|
||||
type = SearchSidebarActionTypes.EXPAND;
|
||||
}
|
||||
|
||||
export class SearchSidebarToggleAction implements Action {
|
||||
type = SearchSidebarActionTypes.TOGGLE;
|
||||
}
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
||||
/**
|
||||
* Export a type alias of all actions in this action group
|
||||
* so that reducers can easily compose action types
|
||||
*/
|
||||
export type SearchSidebarAction
|
||||
= SearchSidebarCollapseAction
|
||||
| SearchSidebarExpandAction
|
||||
| SearchSidebarToggleAction
|
@@ -7,6 +7,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div id="search-sidebar-content">
|
||||
<ds-view-mode-switch class="d-none d-md-block"></ds-view-mode-switch>
|
||||
Place filters and other search config here
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,23 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Effect, Actions } from '@ngrx/effects'
|
||||
import * as fromRouter from '@ngrx/router-store';
|
||||
|
||||
import { HostWindowActionTypes } from '../../shared/host-window.actions';
|
||||
import { SearchSidebarCollapseAction } from './search-sidebar.actions';
|
||||
|
||||
@Injectable()
|
||||
export class SearchSidebarEffects {
|
||||
|
||||
@Effect() resize$ = this.actions$
|
||||
.ofType(HostWindowActionTypes.RESIZE)
|
||||
.map(() => new SearchSidebarCollapseAction());
|
||||
|
||||
@Effect() routeChange$ = this.actions$
|
||||
.ofType(fromRouter.ROUTER_NAVIGATION)
|
||||
.map(() => new SearchSidebarCollapseAction());
|
||||
|
||||
constructor(private actions$: Actions) {
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,91 @@
|
||||
import * as deepFreeze from 'deep-freeze';
|
||||
|
||||
import { sidebarReducer } from './search-sidebar.reducer';
|
||||
import {
|
||||
SearchSidebarCollapseAction, SearchSidebarExpandAction,
|
||||
SearchSidebarToggleAction
|
||||
} from './search-sidebar.actions';
|
||||
import { async, TestBed } from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
class NullAction extends SearchSidebarCollapseAction {
|
||||
type = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
describe('sidebarReducer', () => {
|
||||
|
||||
it('should return the current state when no valid actions have been made', () => {
|
||||
const state = { sidebarCollapsed: false };
|
||||
const action = new NullAction();
|
||||
const newState = sidebarReducer(state, action);
|
||||
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
|
||||
it('should start with sidebarCollapsed = true', () => {
|
||||
const action = new NullAction();
|
||||
const initialState = sidebarReducer(undefined, action);
|
||||
|
||||
// The search sidebar starts collapsed
|
||||
expect(initialState.sidebarCollapsed).toEqual(true);
|
||||
});
|
||||
|
||||
it('should set sidebarCollapsed to true in response to the COLLAPSE action', () => {
|
||||
const state = { sidebarCollapsed: false };
|
||||
const action = new SearchSidebarCollapseAction();
|
||||
const newState = sidebarReducer(state, action);
|
||||
|
||||
expect(newState.sidebarCollapsed).toEqual(true);
|
||||
});
|
||||
|
||||
it('should perform the COLLAPSE action without affecting the previous state', () => {
|
||||
const state = { sidebarCollapsed: false };
|
||||
deepFreeze([state]);
|
||||
|
||||
const action = new SearchSidebarCollapseAction();
|
||||
sidebarReducer(state, action);
|
||||
|
||||
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||
// is mutated, and any uncaught exception will cause the test to fail
|
||||
});
|
||||
|
||||
it('should set sidebarCollapsed to false in response to the EXPAND action', () => {
|
||||
const state = { sidebarCollapsed: true };
|
||||
const action = new SearchSidebarExpandAction();
|
||||
const newState = sidebarReducer(state, action);
|
||||
|
||||
expect(newState.sidebarCollapsed).toEqual(false);
|
||||
});
|
||||
|
||||
it('should perform the EXPAND action without affecting the previous state', () => {
|
||||
const state = { sidebarCollapsed: true };
|
||||
deepFreeze([state]);
|
||||
|
||||
const action = new SearchSidebarExpandAction();
|
||||
sidebarReducer(state, action);
|
||||
});
|
||||
|
||||
it('should flip the value of sidebarCollapsed in response to the TOGGLE action', () => {
|
||||
const state1 = { sidebarCollapsed: true };
|
||||
const action = new SearchSidebarToggleAction();
|
||||
|
||||
const state2 = sidebarReducer(state1, action);
|
||||
const state3 = sidebarReducer(state2, action);
|
||||
|
||||
expect(state2.sidebarCollapsed).toEqual(false);
|
||||
expect(state3.sidebarCollapsed).toEqual(true);
|
||||
});
|
||||
|
||||
it('should perform the TOGGLE action without affecting the previous state', () => {
|
||||
const state = { sidebarCollapsed: true };
|
||||
deepFreeze([state]);
|
||||
|
||||
const action = new SearchSidebarToggleAction();
|
||||
sidebarReducer(state, action);
|
||||
});
|
||||
|
||||
});
|
@@ -0,0 +1,38 @@
|
||||
import { SearchSidebarAction, SearchSidebarActionTypes } from './search-sidebar.actions';
|
||||
|
||||
export interface SearchSidebarState {
|
||||
sidebarCollapsed: boolean;
|
||||
}
|
||||
|
||||
const initialState: SearchSidebarState = {
|
||||
sidebarCollapsed: true
|
||||
};
|
||||
|
||||
export function sidebarReducer(state = initialState, action: SearchSidebarAction): SearchSidebarState {
|
||||
switch (action.type) {
|
||||
|
||||
case SearchSidebarActionTypes.COLLAPSE: {
|
||||
return Object.assign({}, state, {
|
||||
sidebarCollapsed: true
|
||||
});
|
||||
}
|
||||
|
||||
case SearchSidebarActionTypes.EXPAND: {
|
||||
return Object.assign({}, state, {
|
||||
sidebarCollapsed: false
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
case SearchSidebarActionTypes.TOGGLE: {
|
||||
return Object.assign({}, state, {
|
||||
sidebarCollapsed: !state.sidebarCollapsed
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
@@ -62,12 +62,17 @@ export class AppComponent implements OnInit {
|
||||
const env: string = this.config.production ? 'Production' : 'Development';
|
||||
const color: string = this.config.production ? 'red' : 'green';
|
||||
console.info(`Environment: %c${env}`, `color: ${color}; font-weight: bold;`);
|
||||
this.dispatchWindowSize(this._window.nativeWindow.innerWidth, this._window.nativeWindow.innerHeight);
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
private onResize(event): void {
|
||||
this.dispatchWindowSize(event.target.innerWidth, event.target.innerHeight);
|
||||
}
|
||||
|
||||
private dispatchWindowSize(width, height): void {
|
||||
this.store.dispatch(
|
||||
new HostWindowResizeAction(event.target.innerWidth, event.target.innerHeight)
|
||||
new HostWindowResizeAction(width, height)
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -3,15 +3,21 @@ import * as fromRouter from '@ngrx/router-store';
|
||||
|
||||
import { headerReducer, HeaderState } from './header/header.reducer';
|
||||
import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
|
||||
import {
|
||||
SearchSidebarState,
|
||||
sidebarReducer
|
||||
} from './+search-page/search-sidebar/search-sidebar.reducer';
|
||||
|
||||
export interface AppState {
|
||||
router: fromRouter.RouterReducerState;
|
||||
hostWindow: HostWindowState;
|
||||
header: HeaderState;
|
||||
searchSidebar: SearchSidebarState;
|
||||
}
|
||||
|
||||
export const appReducers: ActionReducerMap<AppState> = {
|
||||
router: fromRouter.routerReducer,
|
||||
hostWindow: hostWindowReducer,
|
||||
header: headerReducer
|
||||
header: headerReducer,
|
||||
searchSidebar: sidebarReducer,
|
||||
};
|
||||
|
16
src/app/shared/animations/slide.ts
Normal file
16
src/app/shared/animations/slide.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { animate, state, transition, trigger, style } from '@angular/animations';
|
||||
|
||||
export const slideInOut = trigger('slideInOut', [
|
||||
|
||||
/*
|
||||
state('expanded', style({ right: '100%' }));
|
||||
|
||||
state('collapsed', style({ right: 0 }));
|
||||
*/
|
||||
|
||||
state('expanded', style({ left: '100%' })),
|
||||
|
||||
state('collapsed', style({ left: 0 })),
|
||||
|
||||
transition('expanded <=> collapsed', animate(250)),
|
||||
]);
|
@@ -16,4 +16,8 @@ export class MockHostWindowService {
|
||||
isXs(): Observable<boolean> {
|
||||
return Observable.of(this.width < 576);
|
||||
}
|
||||
|
||||
isSm(): Observable<boolean> {
|
||||
return Observable.of(this.width < 768);
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,7 @@ import { SearchResultListElementComponent } from '../object-list/search-result-l
|
||||
import { SearchFormComponent } from './search-form/search-form.component';
|
||||
import { WrapperListElementComponent } from '../object-list/wrapper-list-element/wrapper-list-element.component';
|
||||
import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component';
|
||||
import { ScrollAndStickDirective } from './utils/scroll-and-stick.directive';
|
||||
|
||||
const MODULES = [
|
||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||
@@ -66,6 +67,10 @@ const COMPONENTS = [
|
||||
ViewModeSwitchComponent
|
||||
];
|
||||
|
||||
const DIRECTIVES = [
|
||||
ScrollAndStickDirective,
|
||||
];
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
// put shared entry components (components that are created dynamically) here
|
||||
CollectionListElementComponent,
|
||||
@@ -81,12 +86,14 @@ const ENTRY_COMPONENTS = [
|
||||
declarations: [
|
||||
...PIPES,
|
||||
...COMPONENTS,
|
||||
...ENTRY_COMPONENTS
|
||||
...ENTRY_COMPONENTS,
|
||||
...DIRECTIVES
|
||||
],
|
||||
exports: [
|
||||
...MODULES,
|
||||
...PIPES,
|
||||
...COMPONENTS
|
||||
...COMPONENTS,
|
||||
...DIRECTIVES
|
||||
],
|
||||
entryComponents: [
|
||||
...ENTRY_COMPONENTS
|
||||
|
40
src/app/shared/utils/scroll-and-stick.directive.ts
Normal file
40
src/app/shared/utils/scroll-and-stick.directive.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
|
||||
import { NativeWindowRef, NativeWindowService } from '../window.service';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { AfterViewInit, Directive, ElementRef, Inject } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[dsStick]'
|
||||
})
|
||||
export class ScrollAndStickDirective implements AfterViewInit {
|
||||
|
||||
private initialY: number;
|
||||
|
||||
constructor(private _element: ElementRef, @Inject(NativeWindowService) private _window: NativeWindowRef) {
|
||||
this.subscribeForScrollEvent();
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.initialY = this._element.nativeElement.getBoundingClientRect().top;
|
||||
}
|
||||
|
||||
subscribeForScrollEvent() {
|
||||
|
||||
const obs = Observable.fromEvent(window, 'scroll');
|
||||
|
||||
obs.subscribe((e) => this.handleScrollEvent(e));
|
||||
}
|
||||
|
||||
handleScrollEvent(e) {
|
||||
|
||||
if (this._window.nativeWindow.pageYOffset >= this.initialY) {
|
||||
|
||||
this._element.nativeElement.classList.add('stick');
|
||||
|
||||
} else {
|
||||
|
||||
this._element.nativeElement.classList.remove('stick');
|
||||
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user