521 lines
15 KiB
TypeScript
521 lines
15 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()] ?? true;
|
|
if (!isVisible) {
|
|
invisibleEntries.push(entry.id.toString());
|
|
} else {
|
|
//check field inputs
|
|
const oneFieldAtLeastIsVisible = entry.subEntries.some(field => this.visibilityRulesService.isVisibleMap[field.id.toString()] ?? true);
|
|
if (!oneFieldAtLeastIsVisible) {
|
|
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;
|
|
}
|