222 lines
5.9 KiB
TypeScript
222 lines
5.9 KiB
TypeScript
|
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<LinkToScroll>();
|
||
|
|
||
|
_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<Link> = [];
|
||
|
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<Link> = [];
|
||
|
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;
|
||
|
}
|