diff --git a/src/app/app.component.html b/src/app/app.component.html index e2324427..2abe8c1e 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -4,7 +4,7 @@ - + diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 0ce5615f..78b67cfe 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -9,6 +9,7 @@ import { MarkdownModule } from 'ngx-markdown'; import { SharedModule } from './shared/shared.module'; import { HttpClientModule, HttpClient } from '@angular/common/http'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { ScrollingModule } from '@angular/cdk/scrolling'; @NgModule({ declarations: [AppComponent], @@ -21,6 +22,7 @@ import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; SharedModule, MarkdownModule.forRoot({ loader: HttpClient }), FontAwesomeModule, + ScrollingModule, ], providers: [], bootstrap: [AppComponent], diff --git a/src/app/shared/components/scroll-spy/scroll-spy.component.css b/src/app/shared/components/scroll-spy/scroll-spy.component.css new file mode 100644 index 00000000..39257af9 --- /dev/null +++ b/src/app/shared/components/scroll-spy/scroll-spy.component.css @@ -0,0 +1,40 @@ +.root { + padding: 4px; + padding-left: 8px; + display: inline-table; + position: sticky; + top: 20px; + border-left: 1px solid gray; + display: inline-table; +} + +.section { + display: flex; + align-items: center; + margin-bottom: 0.6em; + cursor: pointer; + transition: all 0.3s; +} + +.actual-section-indicator { + width: 0; + height: 10px; + margin-left: -17px; + margin-right: 15px; + border-radius: 100px; + background-color: white; + border: 1px solid white; + flex: none; + display: inline-block; + transition: all 0.3s; +} + +.section:hover { + color: rgb(148, 7, 78); +} + +.actual-section-indicator.active { + width: 16px; + margin-right: 5px; + border: 1px solid black; +} diff --git a/src/app/shared/components/scroll-spy/scroll-spy.component.html b/src/app/shared/components/scroll-spy/scroll-spy.component.html new file mode 100644 index 00000000..4078c7bb --- /dev/null +++ b/src/app/shared/components/scroll-spy/scroll-spy.component.html @@ -0,0 +1,13 @@ +
+ + + {{ section.name }} + +
diff --git a/src/app/shared/components/scroll-spy/scroll-spy.component.spec.ts b/src/app/shared/components/scroll-spy/scroll-spy.component.spec.ts new file mode 100644 index 00000000..a99f11b7 --- /dev/null +++ b/src/app/shared/components/scroll-spy/scroll-spy.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ScrollSpyComponent } from './scroll-spy.component'; + +describe('ScrollSpyComponent', () => { + let component: ScrollSpyComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ScrollSpyComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ScrollSpyComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/scroll-spy/scroll-spy.component.ts b/src/app/shared/components/scroll-spy/scroll-spy.component.ts new file mode 100644 index 00000000..2cd5db07 --- /dev/null +++ b/src/app/shared/components/scroll-spy/scroll-spy.component.ts @@ -0,0 +1,88 @@ +import { + Component, + AfterContentChecked, + ChangeDetectorRef, + OnInit, +} from '@angular/core'; +import { debounce, throttle, debounceTime } from 'rxjs/operators'; +import { ScrollDispatcher, CdkScrollable } from '@angular/cdk/overlay'; +import { interval } from 'rxjs'; + +@Component({ + selector: 'rxjs-scroll-spy', + templateUrl: './scroll-spy.component.html', + styleUrls: ['./scroll-spy.component.css'], +}) +export class ScrollSpyComponent implements AfterContentChecked, OnInit { + currentSection = ''; + sections = []; + private sectionsHeader: NodeListOf; + + constructor( + private scroll: ScrollDispatcher, + private changeDetector: ChangeDetectorRef + ) {} + + ngOnInit() { + this.scroll + .scrolled() + .pipe(throttle((ev) => interval(100))) + .subscribe((scroll: CdkScrollable) => this.onScroll(scroll)); + } + + ngAfterContentChecked(): void { + this.loadSections(); + } + + scrollTo(sectionId) { + document.querySelector('#' + sectionId).scrollIntoView(); + } + + isActualSection(sectionId: string) { + return sectionId === this.currentSection; + } + + private loadSections() { + this.sectionsHeader = document.querySelectorAll('h2'); + this.sections = []; + this.sectionsHeader.forEach((header: HTMLHeadingElement) => { + this.sections.push({ + name: header.textContent, + id: header.id, + }); + }); + } + + private onScroll(scroll: CdkScrollable) { + const scrollTop = scroll.getElementRef().nativeElement.scrollTop || 0; + const parentOffset = scroll.getElementRef().nativeElement.offsetTop; + const currentSection = this.getCurrentSection({ scrollTop, parentOffset }); + + if (!currentSection || currentSection.id === this.currentSection) { + return; + } + + this.currentSection = currentSection.id; + this.changeDetector.detectChanges(); + } + + private getCurrentSection({ scrollTop, parentOffset }) { + const sectionsCount = this.sectionsHeader.length; + let actualHeader; + for (let i = 0; i < sectionsCount; i++) { + const header = this.sectionsHeader[i]; + if (header.offsetTop - parentOffset <= scrollTop) { + actualHeader = header; + } + } + + return actualHeader; + + /* For some reason, this (and similar) DONT work. Only get first element + return [].find.call( + this.sectionsHeader, + (header: HTMLHeadingElement) => + header.offsetTop - parentOffset <= scrollTop + ); */ + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 9c8910ce..fcac3058 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -23,10 +23,16 @@ import { NgModule } from '@angular/core'; import { MatDividerModule } from '@angular/material/divider'; import { SidenavComponent } from './components/sidenav/sidenav.component'; import { RouterModule } from '@angular/router'; +import { ScrollSpyComponent } from './components/scroll-spy/scroll-spy.component'; const CORE_MODULES = [CommonModule, FormsModule, ReactiveFormsModule]; -const COMPONENTS = [FooterComponent, HeaderComponent, SidenavComponent]; +const COMPONENTS = [ + FooterComponent, + HeaderComponent, + SidenavComponent, + ScrollSpyComponent, +]; const MATERIAL_MODULES = [ MatInputModule, @@ -49,7 +55,7 @@ const MATERIAL_MODULES = [ ]; @NgModule({ - declarations: COMPONENTS, + declarations: [COMPONENTS, ScrollSpyComponent], imports: [MATERIAL_MODULES, CORE_MODULES, FlexLayoutModule], exports: [COMPONENTS, MATERIAL_MODULES, CORE_MODULES, FlexLayoutModule], }) diff --git a/src/app/views/home/content/content.component.css b/src/app/views/home/content/content.component.css index c22fa93e..c91e4da1 100644 --- a/src/app/views/home/content/content.component.css +++ b/src/app/views/home/content/content.component.css @@ -1,3 +1,12 @@ +.md-container { + display: flex; +} + +.md-container > * { + margin: 0 24px; + min-width: 0; /* Fix flex horizontal overflow */ +} + details { border-top: 2px solid #e3e7e7; margin: 50px 0 0; @@ -65,6 +74,10 @@ th { padding: 25px; } +.scrollspy-container { + max-width: 450px; +} + .page-heading { display: flex; justify-content: space-between; @@ -114,3 +127,10 @@ summary:hover { padding: 40px; } } + +@media only screen and (max-width: 700px) { + .scrollspy-container { + display: none; + } +} + diff --git a/src/app/views/home/content/content.component.html b/src/app/views/home/content/content.component.html index d85837d4..89942c31 100644 --- a/src/app/views/home/content/content.component.html +++ b/src/app/views/home/content/content.component.html @@ -1,3 +1,8 @@
- +
+ +
+
+ +