argos/dmp-frontend/src/app/ui/description/editor/table-of-contents/table-of-contents.component.ts

515 lines
14 KiB
TypeScript

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<LinkToScroll>();
@Output() currentLinks = new EventEmitter<Link[]>();
@Output() entrySelected = new EventEmitter<any>();
subscription: Subscription;
linksSubject: Subject<HTMLElement[]> = new Subject<HTMLElement[]>();
@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<Link> = [];
// 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;
}