import { DOCUMENT } from '@angular/common'; import { Component, EventEmitter, Inject, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'; import { UntypedFormGroup } from '@angular/forms'; import { DescriptionTemplate, DescriptionTemplateField, DescriptionTemplateFieldSet, DescriptionTemplateSection } from '@app/core/model/description-template/description-template'; import { VisibilityRulesService } from '@app/ui/description/editor/description-form/visibility-rules/visibility-rules.service'; import { BaseComponent } from '@common/base/base.component'; import { Subject, Subscription, interval } from 'rxjs'; import { debounceTime, distinctUntilChanged, takeUntil } from 'rxjs/operators'; import { ToCEntry } from './models/toc-entry'; import { ToCEntryType } from './models/toc-entry-type.enum'; import { TableOfContentsInternal } from './table-of-contents-internal/table-of-contents-internal'; export 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; show: boolean; selected: boolean; } @Component({ selector: 'app-table-of-contents', styleUrls: ['./table-of-contents.component.scss'], templateUrl: './table-of-contents.component.html' }) export class TableOfContentsComponent extends BaseComponent implements OnInit, OnChanges { @ViewChild('internalTable') internalTable: TableOfContentsInternal; @Input() links: Link[]; container: string; headerSelectors = '.toc-page-header, .toc-section-header, .toc-compositeField-header'; @Output() stepFound = new EventEmitter(); @Output() currentLinks = new EventEmitter(); @Output() entrySelected = new EventEmitter(); subscription: Subscription; linksSubject: Subject = new Subject(); @Input() isActive: boolean; tocentries: ToCEntry[] = null; @Input() TOCENTRY_ID_PREFIX = ''; @Input() showErrors: boolean = false; @Input() selectedFieldsetId: string; private _tocentrySelected: ToCEntry = null; private isSelecting: boolean = false; private _intersectionObserver: IntersectionObserver; private _actOnObservation: boolean = true; public hiddenEntries: string[] = []; get tocentrySelected() { return this.hasFocus ? this._tocentrySelected : null; } set tocentrySelected(value) { this._tocentrySelected = value; } @Input() propertiesFormGroup: UntypedFormGroup; @Input() descriptionTemplate: DescriptionTemplate; @Input() hasFocus: boolean = false; @Input() visibilityRulesService: VisibilityRulesService; show: boolean = false; constructor( @Inject(DOCUMENT) private _document: Document, // public visibilityRulesService: VisibilityRulesService ) { super(); } ngOnInit(): void { if (this.descriptionTemplate) { this.tocentries = this.getTocEntries(this.descriptionTemplate); if (this.visibilityRulesService) { this.hiddenEntries = this._findHiddenEntries(this.tocentries); } } else { //emit value every 500ms const source = interval(500); this.subscription = source.subscribe(val => { const headers = Array.from(this._document.querySelectorAll(this.headerSelectors)) as HTMLElement[]; this.linksSubject.next(headers); }); // if (!this.links || this.links.length === 0) { // this.linksSubject.asObservable() // .pipe(distinctUntilChanged((p: HTMLElement[], q: HTMLElement[]) => JSON.stringify(p) == JSON.stringify(q))) // .subscribe(headers => { // const links: Array = []; // if (headers.length) { // let page; // let section; // let show // for (const header of headers) { // let name; // let id; // if (header.classList.contains('toc-page-header')) { // deprecated after removing stepper // name = header.innerText.trim().replace(/^link/, ''); // id = header.id; // page = header.id.split('_')[1]; // section = undefined; // show = true; // } else if (header.classList.contains('toc-section-header')) { // name = header.childNodes[0].childNodes[0].childNodes[0].childNodes[1].childNodes[0].nodeValue.trim().replace(/^link/, ''); // id = header.id; // page = header.id.split('.')[1]; // section = header.id; // if (header.id.split('.')[4]) { show = false; } // else { show = true; } // } else if (header.classList.contains('toc-compositeField-header')) { // name = (header.childNodes[0]).nodeValue.trim().replace(/^link/, ''); // id = header.id; // // id = header.parentElement.parentElement.parentElement.id; // show = false; // } // const { top } = header.getBoundingClientRect(); // links.push({ // name, // id, // type: header.tagName.toLowerCase(), // top: top, // active: false, // page: page, // section: section, // show: show, // selected: false // }); // } // } // this.links = links; // // Initialize selected for button next on dataset wizard component editor // this.links.length > 0 ? this.links[0].selected = true : null; // }) // } } } private _findHiddenEntries(tocentries: ToCEntry[]): string[] { if (!tocentries) return []; const invisibleEntries: string[] = [] tocentries.forEach(entry => { if (entry.type === ToCEntryType.FieldSet) { const isVisible = this.visibilityRulesService.isVisibleMap[entry.id.toString()]; if (!isVisible) { invisibleEntries.push(entry.id.toString()); } } else { const hiddenEntries = this._findHiddenEntries(entry.subEntries); if (entry.subEntries && (entry.subEntries.every(e => hiddenEntries.includes(e.id.toString())))) { //all children all hidden then hide parent node; invisibleEntries.push(entry.id.toString()); } else { invisibleEntries.push(...hiddenEntries); } } }) return invisibleEntries; } private _visibilityRulesSubscription: Subscription; ngOnChanges(changes: SimpleChanges) { if (this.selectedFieldsetId) { this.onToCentrySelected(this._findTocEntryById(this.selectedFieldsetId, this.tocentries), false); this._actOnObservation = false; setTimeout(() => { this._actOnObservation = true; }, 1000); } if (changes['hasFocus'] && changes.hasFocus.currentValue) { this._resetObserver(); } if (changes['descriptionTemplate'] && changes.descriptionTemplate != null) { this.tocentries = this.getTocEntries(this.descriptionTemplate); // if (this.visibilityRulesService) { // this.hiddenEntries = this._findHiddenEntries(this.tocentries); // } } if ('visibilityRulesService') { if (this._visibilityRulesSubscription) { this._visibilityRulesSubscription.unsubscribe(); this._visibilityRulesSubscription = null; } if (!this.visibilityRulesService) return; // this._visibilityRulesSubscription = this.visibilityRulesService.visibilityChange // .pipe(takeUntil(this._destroyed)) // .pipe(debounceTime(200)) // .subscribe(_ => { // if (this.hasFocus) { // this._resetObserver(); // // this.hiddenEntries = this._findHiddenEntries(this.tocentries); // } // }); // this.hiddenEntries = this._findHiddenEntries(this.tocentries); } // if (!this.isActive && this.links && this.links.length > 0) { // this.links.forEach(link => { // link.selected = false; // }) // this.links[0].selected = true; // } } private _resetObserver() { if (this._intersectionObserver) {//clean up this._intersectionObserver.disconnect(); this._intersectionObserver = null; } const options = { root: null, rootMargin: '-38% 0px -60% 0px', threshold: 0 } this._intersectionObserver = new IntersectionObserver((entries, observer) => { if (!this._actOnObservation) { return; } entries.forEach(ie => { if (ie.isIntersecting) { try { const target_id = ie.target.id.replace(this.TOCENTRY_ID_PREFIX, ''); if (this.visibilityRulesService.isVisibleMap[target_id] ?? true) { this.onToCentrySelected(this._findTocEntryById(target_id, this.tocentries)); } } catch { } } }) }, options); const fieldsetsEtries = this._getAllFieldSets(this.tocentries); fieldsetsEtries.forEach(e => { if (e.type === ToCEntryType.FieldSet) { try { const targetElement = document.getElementById(this.TOCENTRY_ID_PREFIX + e.id); this._intersectionObserver.observe(targetElement); } catch { console.log('element not found'); } } }); } goToStep(link: Link) { this.stepFound.emit({ page: link.page, section: link.section }); this.currentLinks.emit(this.links); setTimeout(() => { const target = document.getElementById(link.id); target.scrollIntoView(true); var scrolledY = window.scrollY; if (scrolledY) { window.scroll(0, scrolledY - 70); } }, 500); } toggle(headerLink: Link) { const headerPage = +headerLink.name.split(" ", 1); let innerPage; for (const link of this.links) { link.selected = false; if (link.type === 'mat-expansion-panel') { innerPage = +link.name.split(".", 1)[0]; if (isNaN(innerPage)) { innerPage = +link.name.split(" ", 1) } } else if (link.type === 'h5') { innerPage = +link.name.split(".", 1)[0]; } if (headerPage === innerPage && (link.type !== 'mat-expansion-panel' || (link.type === 'mat-expansion-panel' && link.id.split(".")[4]))) { link.show = !link.show; } } headerLink.selected = true; } // getIndex(link: Link): number { // return +link.id.split("_", 2)[1]; // } private _buildRecursivelySection(item: DescriptionTemplateSection): ToCEntry { if (!item) return null; const sections = item.sections; const fieldsets = item.fieldSets; const tempResult: ToCEntry[] = []; if (sections && sections.length) { sections.forEach(section => { tempResult.push(this._buildRecursivelySection(section)); }); } else if (fieldsets && fieldsets.length) { fieldsets.forEach(fieldset => { tempResult.push(this._buildRecursivelyFieldSet(fieldset)); }); } return { // form: form, id: item.id, label: item.title, numbering: '', subEntries: tempResult, subEntriesType: sections && sections.length ? ToCEntryType.Section : ToCEntryType.FieldSet, type: ToCEntryType.Section, ordinal: item.ordinal } } private _buildRecursivelyFieldSet(item: DescriptionTemplateFieldSet): ToCEntry { if (!item) return null; const tempResult: ToCEntry[] = []; if (item && item.fields.length > 0) { item.fields.forEach(field => { tempResult.push(this._buildRecursivelyField(field)); }); } return { // form: form, id: item.id, label: item.title, numbering: 's', subEntries: null, subEntriesType: ToCEntryType.Field, type: ToCEntryType.FieldSet, ordinal: item.ordinal }; } private _buildRecursivelyField(item: DescriptionTemplateField): ToCEntry { if (!item) return null; return { // form: form, id: item.id, label: null, numbering: 's', subEntries: null, subEntriesType: null, type: ToCEntryType.Field, ordinal: item.ordinal, hidden: false }; } private _sortByOrdinal(tocentries: ToCEntry[]) { if (!tocentries || !tocentries.length) return; tocentries.sort(this._customCompare); tocentries.forEach(entry => { this._sortByOrdinal(entry.subEntries); }); } private _customCompare(a, b) { return a.ordinal - b.ordinal; } private _calculateNumbering(tocentries: ToCEntry[], depth: number[] = []) { if (!tocentries || !tocentries.length) { return; } let prefixNumbering = depth.length ? depth.join('.') : ''; if (depth.length) prefixNumbering = prefixNumbering + "."; tocentries.forEach((entry, i) => { entry.numbering = prefixNumbering + (i + 1); this._calculateNumbering(entry.subEntries, [...depth, i + 1]) }); } getTocEntries(descriptionTemplate: DescriptionTemplate): ToCEntry[] { if (descriptionTemplate == null) { return []; } const result: ToCEntry[] = []; //build parent pages descriptionTemplate.definition.pages.forEach((pageElement, i) => { const tocEntry: ToCEntry = { // id: i + 'id', id: pageElement.id, label: pageElement.title, type: ToCEntryType.Page, // form: pageElement, numbering: (i + 1).toString(), subEntriesType: ToCEntryType.Section, subEntries: [], ordinal: pageElement.ordinal }; const sections = descriptionTemplate.definition.pages.find(x => x.id == pageElement.id)?.sections; sections.forEach(section => { const tempResults = this._buildRecursivelySection(section); tocEntry.subEntries.push(tempResults); }); result.push(tocEntry) }); this._sortByOrdinal(result); //calculate numbering this._calculateNumbering(result); return result; } onToCentrySelected(entry: ToCEntry = null, execute: boolean = true) { if (!this.isSelecting) { this.isSelecting = true; this.tocentrySelected = entry; this.entrySelected.emit({ entry: entry, execute: execute }); setTimeout(() => { this.isSelecting = false; }, 600); } } private _findTocEntryById(id: string, tocentries: ToCEntry[]): ToCEntry { if (!tocentries || !tocentries.length) { return null; } let tocEntryFound = tocentries.find(entry => entry.id === id); if (tocEntryFound) { return tocEntryFound; } for (let entry of tocentries) { const result = this._findTocEntryById(id, entry.subEntries); if (result) { tocEntryFound = result; break; } } return tocEntryFound ? tocEntryFound : null; } /** * Get all filedsets in a tocentry array; * @param entries Tocentries to search in * @returns The tocentries that are Fieldsets provided in the entries */ private _getAllFieldSets(entries: ToCEntry[]): ToCEntry[] { const fieldsets: ToCEntry[] = []; if (!entries || !entries.length) return fieldsets; entries.forEach(e => { if (e.type === ToCEntryType.FieldSet) { fieldsets.push(e); } else { fieldsets.push(...this._getAllFieldSets(e.subEntries)); } }); return fieldsets; } public hasVisibleInvalidFields(): boolean { if (!this.internalTable || !this.tocentries) { return false; } return this.tocentries.some(e => this.internalTable.invalidChildsVisible(e)); } protected readonly console = console; } export interface LinkToScroll { page: number; section: number; }