import { DOCUMENT } from '@angular/common'; import { Component, EventEmitter, Inject, OnInit, Output, Input, OnChanges, ViewChild } from '@angular/core'; import { BaseComponent } from '@common/base/base.component'; import { interval, Subject, Subscription } from 'rxjs'; import { debounceTime, distinctUntilChanged, takeUntil } from 'rxjs/operators'; import { type } from 'os'; import { SimpleChanges } from '@angular/core'; import { ToCEntry, ToCEntryType } from '../dataset-description.component'; import { FormArray, FormGroup } from '@angular/forms'; import { VisibilityRulesService } from '../visibility-rules/visibility-rules.service'; import { Rule } from '@app/core/model/dataset-profile-definition/rule'; 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: 'table-of-contents', styleUrls: ['./table-of-contents.scss'], templateUrl: './table-of-contents.html' }) export class TableOfContents 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(); subscription: Subscription; linksSubject: Subject = new Subject(); @Input() isActive: boolean; tocentries: ToCEntry[] = null; @Input() TOCENTRY_ID_PREFIX = ''; // visibilityRules:Rule[] = []; @Input() visibilityRules:Rule[] = []; @Input() showErrors: boolean = false; @Input() selectedFieldsetId:string; private _tocentrySelected:ToCEntry = null; 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() formGroup: FormGroup; @Input() hasFocus: boolean = false; @Input() visibilityRulesService: VisibilityRulesService; show: boolean = false; constructor( @Inject(DOCUMENT) private _document: Document, // public visibilityRulesService: VisibilityRulesService ) { super(); } ngOnInit(): void { // this.visibilityRulesService.visibilityChange // .pipe(takeUntil(this._destroyed)) // .pipe(debounceTime(200)) // .subscribe(_=>{ // if(this.hasFocus){ // this._resetObserver(); // this.hiddenEntries = this._findHiddenEntries(this.tocentries); // } // }) if(this.formGroup){ this.tocentries = this.getTocEntries(this.formGroup.get('datasetProfileDefinition')); const fg = this.formGroup.get('datasetProfileDefinition'); // this.visibilityRulesService.buildVisibilityRules(this.visibilityRules, fg); 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[0].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.checkElementVisibility(entry.id); if(!isVisible){ invisibleEntries.push(entry.id); }else{ //check field inputs const fields = entry.form.get('fields') as FormArray; const oneFieldAtLeastIsVisible = fields.controls.some(field=> this.visibilityRulesService.checkElementVisibility(field.get('id').value)); if(!oneFieldAtLeastIsVisible){ invisibleEntries.push(entry.id); } } }else{ const hiddenEntries = this._findHiddenEntries(entry.subEntries); if(entry.subEntries&& (entry.subEntries.every(e=> hiddenEntries.includes(e.id)))){ //all children all hidden then hide parent node; invisibleEntries.push(entry.id); }else{ invisibleEntries.push(...hiddenEntries); } } }) return invisibleEntries; } private _visibilityRulesSubscription:Subscription; ngOnChanges(changes: SimpleChanges) { if(this.selectedFieldsetId){ this.tocentrySelected = this._findTocEntryById(this.selectedFieldsetId,this.tocentries); this._actOnObservation = false; setTimeout(() => { this._actOnObservation = true; }, 1000); } if(changes['hasFocus'] && changes.hasFocus.currentValue){ this._resetObserver(); } 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.checkElementVisibility(target_id)){ this.tocentrySelected = 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 _buildRecursively(form: FormGroup,whatAmI:ToCEntryType):ToCEntry{ if(!form) return null; switch(whatAmI){ case ToCEntryType.Section: const sections = form.get('sections') as FormArray; const fieldsets = form.get('compositeFields') as FormArray; const tempResult:ToCEntry[] = []; if(sections &§ions.length){ sections.controls.forEach(section=>{ tempResult.push(this._buildRecursively(section as FormGroup, ToCEntryType.Section)); }); }else if(fieldsets && fieldsets.length){ fieldsets.controls.forEach(fieldset=>{ tempResult.push(this._buildRecursively(fieldset as FormGroup, ToCEntryType.FieldSet)); }); } return { form: form, id: form.get('id').value, label: form.get('title').value, numbering: '', subEntries:tempResult, subEntriesType: sections &§ions.length? ToCEntryType.Section: ToCEntryType.FieldSet, type: ToCEntryType.Section, ordinal: form.get('ordinal').value } case ToCEntryType.FieldSet: return { form: form, label:form.get('title').value, id: form.get('id').value, numbering:'s', subEntries:[], subEntriesType: ToCEntryType.Field, type: ToCEntryType.FieldSet, ordinal: form.get('ordinal').value } } } 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(form): ToCEntry[] { if (form == null) { return []; } const result: ToCEntry[] = []; //build parent pages (form.get('pages') as FormArray).controls.forEach((pageElement, i) => { result.push({ id: i+'id', label: pageElement.get('title').value, type: ToCEntryType.Page, form: pageElement, numbering: (i + 1).toString(), subEntriesType: ToCEntryType.Section, subEntries:[], ordinal: pageElement.get('ordinal').value } as ToCEntry) }); result.forEach((entry,i)=>{ const sections = entry.form.get('sections') as FormArray; sections.controls.forEach(section=>{ const tempResults = this._buildRecursively(section as FormGroup,ToCEntryType.Section); entry.subEntries.push(tempResults); }); }); this._sortByOrdinal(result); //calculate numbering this._calculateNumbering(result); return result; } onToCentrySelected(entry: ToCEntry){ this.tocentrySelected = entry; } public seekToFirstElement(){//only on tocentry mode if(this.tocentries && this.tocentries.length){ this.tocentrySelected = this.tocentries[0]; } } 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)); } } export interface LinkToScroll { page: number; section: number; }