import { DOCUMENT } from '@angular/common'; import { AfterViewInit, Component, ElementRef, EventEmitter, Inject, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { fromEvent, Subject } from 'rxjs'; import { debounceTime, takeUntil } from 'rxjs/operators'; import { link } from 'fs'; interface Link { /* id of the section*/ id: string; /* header type h3/h4 */ type: string; /* If the anchor is in view of the page */ active: boolean; /* name of the anchor */ name: string; /* top offset px of the anchor */ top: number; page: number; section: number; } @Component({ selector: 'table-of-contents', styleUrls: ['./table-of-contents.scss'], templateUrl: './table-of-contents.html' }) export class TableOfContents implements OnInit, AfterViewInit, OnDestroy { @Input() links: Link[] = []; @Input() container: string; @Input() headerSelectors = '.toc-page-header, .toc-section-header, .toc-copositeField-header'; @Output() stepFound = new EventEmitter(); _rootUrl = this._router.url.split('#')[0]; private _scrollContainer: any; private _destroyed = new Subject(); private _urlFragment = ''; private selectedLinkId: string; constructor(private _router: Router, private _route: ActivatedRoute, private _element: ElementRef, @Inject(DOCUMENT) private _document: Document) { this._router.events.pipe(takeUntil(this._destroyed)).subscribe((event) => { if (event instanceof NavigationEnd) { const rootUrl = _router.url.split('#')[0]; if (rootUrl !== this._rootUrl) { this.links = this.createLinks(); this._rootUrl = rootUrl; } } }); this._route.fragment.pipe(takeUntil(this._destroyed)).subscribe(fragment => { this._urlFragment = fragment; const target = document.getElementById(this._urlFragment); if (target) { target.scrollIntoView(); } }); } ngOnInit(): void { // On init, the sidenav content element doesn't yet exist, so it's not possible // to subscribe to its scroll event until next tick (when it does exist). Promise.resolve().then(() => { this._scrollContainer = this.container ? this._document.querySelectorAll(this.container)[0] : window; if (this._scrollContainer) { fromEvent(this._scrollContainer, 'scroll').pipe( takeUntil(this._destroyed), debounceTime(10)) .subscribe(() => this.onScroll()); } }); } ngAfterViewInit() { this.updateScrollPosition(); } ngOnDestroy(): void { this._destroyed.next(); } updateScrollPosition(): void { this.links = this.createLinks(); const target = document.getElementById(this._urlFragment); if (target) { target.scrollIntoView(); } } /** Gets the scroll offset of the scroll container */ private getScrollOffset(): number | void { const { top } = this._element.nativeElement.getBoundingClientRect(); if (typeof this._scrollContainer.scrollTop !== 'undefined') { return this._scrollContainer.scrollTop + top; } else if (typeof this._scrollContainer.pageYOffset !== 'undefined') { return this._scrollContainer.pageYOffset + top; } } private createLinks(): Link[] { const links: Array = []; const headers = Array.from(this._document.querySelectorAll(this.headerSelectors)) as HTMLElement[]; if (headers.length) { for (const header of headers) { // const step = 0; // remove the 'link' icon name from the inner text const name = header.innerText.trim().replace(/^link/, ''); const { top } = header.getBoundingClientRect(); // links.push({ // name, // // step, // type: header.tagName.toLowerCase(), // top: top, // id: header.id, // active: false, // section: section // }); } } return links; } private onScroll(): void { // for (let i = 0; i < this.links.length; i++) { // this.links[i].active = this.isLinkActive(this.links[i], this.links[i + 1]); // } } // private isLinkActive(currentLink: any, nextLink: any): boolean { // A link is considered active if the page is scrolled passed the anchor without also // being scrolled passed the next link // const scrollOffset = this.getScrollOffset(); // return scrollOffset >= currentLink.top && !(nextLink && nextLink.top < scrollOffset); // } getLinks() { const links: Array = []; const headers = Array.from(this._document.querySelectorAll(this.headerSelectors)) as HTMLElement[]; if (headers.length) { let page; let section; for (const header of headers) { let name; let id; if (header.classList.contains('toc-page-header')) { name = header.innerText.trim().replace(/^link/, ''); id = header.id; page = header.id.split('_')[1]; section = undefined; } else if (header.classList.contains('toc-section-header')) { name = header.childNodes[0].childNodes[0].childNodes[0].childNodes[0].childNodes[0].nodeValue.trim().replace(/^link/, ''); id = header.id; page = header.id.split('.')[1]; section = header.id; } else if (header.classList.contains('toc-copositeField-header')) { name = (header.childNodes[0]).nodeValue.trim().replace(/^link/, ''); id = header.id; // id = header.parentElement.parentElement.parentElement.id; } const { top } = header.getBoundingClientRect(); links.push({ name, id, type: header.tagName.toLowerCase(), top: top, active: this.selectedLinkId == id ? true : false, page: page, section: section }); } } this.links = links; return links; } goToStep(link: Link) { this.selectedLinkId = link.id; this.stepFound.emit({ page: link.page, section: link.section }); setTimeout(() => { const target = document.getElementById(link.id); target.scrollIntoView(true); var scrolledY = window.scrollY; if (scrolledY) { window.scroll(0, scrolledY - 70); } }, 200); } } export interface LinkToScroll { page: number; section: number; }