changed description editor table of contents

This commit is contained in:
Diamantis Tziotzios 2024-06-07 16:06:27 +03:00
parent 8a8aa40ac9
commit 8940394aa5
11 changed files with 207 additions and 651 deletions

View File

@ -107,7 +107,7 @@
<div class="row toc-pane-container" #boundary>
<div #spacer></div>
<div class="col-12">
<app-table-of-contents [visibilityRulesService]="visibilityRulesService" [selectedFieldsetId]="fieldsetIdWithFocus" #table0fContents [showErrors]="showtocentriesErrors" [hasFocus]="step > 0" [formGroup]="formGroup.get('properties')" [descriptionTemplate]="item.descriptionTemplate" *ngIf="formGroup" [links]="links" [boundary]="boundary" [spacer]="spacer" [isActive]="step !== 0" stickyThing (stepFound)="onStepFound($event)" (currentLinks)="getLinks($event)" (entrySelected)="changeStep($event.entry, $event.execute)" [pageToFieldSetMap]="pageToFieldSetMap"></app-table-of-contents>
<app-table-of-contents [visibilityRulesService]="visibilityRulesService" #table0fContents [showErrors]="showTocEntriesErrors" [hasFocus]="step > 0" [formGroup]="formGroup.get('properties')" [descriptionTemplate]="item.descriptionTemplate" *ngIf="formGroup" [links]="links" [boundary]="boundary" [spacer]="spacer" [isActive]="step !== 0" stickyThing (entrySelected)="changeStep($event.entry, $event.execute)" [pageToFieldSetMap]="pageToFieldSetMap"></app-table-of-contents>
</div>
</div>
</div>
@ -163,7 +163,6 @@
[validationErrorModel]="editorModel.validationErrorModel"
[isNew]="isNew || isCopy"
[canReview]="canReview"
(fieldsetFocusChange)="fieldsetIdWithFocus = $event"
></app-description-form>
</div>
</div>

View File

@ -46,7 +46,7 @@ import { DescriptionEditorService } from './description-editor.service';
import { PrefillDescriptionDialogComponent } from './prefill-description/prefill-description.component';
import { ToCEntry } from './table-of-contents/models/toc-entry';
import { ToCEntryType } from './table-of-contents/models/toc-entry-type.enum';
import { TableOfContentsValidationService } from './table-of-contents/services/table-of-contents-validation-service';
import { TableOfContentsService } from './table-of-contents/services/table-of-contents-service';
import { TableOfContentsComponent } from './table-of-contents/table-of-contents.component';
@Component({
@ -63,13 +63,13 @@ export class DescriptionEditorComponent extends BaseEditor<DescriptionEditorMode
canEdit = false;
canReview = false;
item: Description;
fieldsetIdWithFocus: string;
fileTransformerEntityTypeEnum = FileTransformerEntityType;
viewOnly = false;
lockStatus: Boolean;
descriptionIsOnceSaved = false;
isFinalized = false;
showTocEntriesErrors = false;
@ViewChild('table0fContents') table0fContents: TableOfContentsComponent;
step: number = 0;
@ -100,11 +100,10 @@ export class DescriptionEditorComponent extends BaseEditor<DescriptionEditorMode
private descriptionTemplateService: DescriptionTemplateService,
public visibilityRulesService: VisibilityRulesService,
public fileTransformerService: FileTransformerService,
public tocValidationService: TableOfContentsValidationService,
public titleService: Title,
private analyticsService: AnalyticsService,
private breadcrumbService: BreadcrumbService,
private changeDetectorRef: ChangeDetectorRef
private changeDetectorRef: ChangeDetectorRef,
private tableOfContentsService: TableOfContentsService
) {
const descriptionLabel: string = route.snapshot.data['entity']?.label;
if (descriptionLabel) {
@ -270,7 +269,6 @@ export class DescriptionEditorComponent extends BaseEditor<DescriptionEditorMode
this.getItem(this.editorModel.id, (data: Description) => {
this.prepareForm(data)
});
this.tocValidationService.validateForm();
}
refreshOnNavigateToData(id?: Guid): void {
@ -551,9 +549,13 @@ export class DescriptionEditorComponent extends BaseEditor<DescriptionEditorMode
public changeStep(selected: ToCEntry = null, execute: boolean = true) {
if (execute) {
if (selected) {
let fieldSet = this.getFirstFieldSet(selected);
let index = this.visibleFieldSets.findIndex(entry => entry.id === fieldSet.id);
this.step = index + (selected.type === ToCEntryType.FieldSet ? 1 : 0.5);
if (this.step == 0) {
this.step = 1;
}
const element = document.getElementById(selected.id);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
} else {
this.step = 0;
this.resetScroll();
@ -561,54 +563,44 @@ export class DescriptionEditorComponent extends BaseEditor<DescriptionEditorMode
}
}
getFirstFieldSet(entry: ToCEntry): ToCEntry {
if (entry.type === ToCEntryType.FieldSet && !this.table0fContents.internalTable.hiddenEntries.find(hiddenEntry => hiddenEntry === entry.id.toString())) {
return entry;
} else {
let subEntries = entry.subEntries.filter(subEntry => !this.table0fContents.internalTable.hiddenEntries.find(hiddenEntry => hiddenEntry === subEntry.id.toString()));
if (subEntries.length > 0) {
return this.getFirstFieldSet(subEntries[0]);
} else {
return null;
}
}
}
get maxStep() {
return this.visibleFieldSets.length;
return 0;
}
public nextStep() {
if (this.step < this.maxStep) {//view is changing
this.step = Math.floor(this.step + 1);
let entry = this.visibleFieldSets[this.step - 1];
const targetElement = document.getElementById(entry.id);
if (targetElement) {
this.table0fContents.onToCentrySelected(entry, false);
this.scroll(entry);
} else {
this.nextStep();
}
}
this.tableOfContentsService.nextClicked();
// if (this.step < this.maxStep) {//view is changing
// this.step = Math.floor(this.step + 1);
// let entry = this.visibleFieldSets[this.step - 1];
// const targetElement = document.getElementById(entry.id);
// if (targetElement) {
// this.table0fContents.onToCentrySelected(entry, false);
// this.scroll(entry);
// } else {
// this.nextStep();
// }
// }
}
public previousStep() {
if (this.step > 0) {
this.step = Math.ceil(this.step - 1);
if (this.step >= 1) {
let entry = this.visibleFieldSets[this.step - 1];
const targetElement = document.getElementById(entry.id);
if (targetElement) {
this.table0fContents.onToCentrySelected(entry, false);
this.scroll(entry);
} else {
this.previousStep();
}
} else {
this.table0fContents.onToCentrySelected(null, false);
this.resetScroll();
}
}
this.tableOfContentsService.nextClicked();
// if (this.step > 0) {
// this.step = Math.ceil(this.step - 1);
// if (this.step >= 1) {
// let entry = this.visibleFieldSets[this.step - 1];
// const targetElement = document.getElementById(entry.id);
// if (targetElement) {
// this.table0fContents.onToCentrySelected(entry, false);
// this.scroll(entry);
// } else {
// this.previousStep();
// }
// } else {
// this.table0fContents.onToCentrySelected(null, false);
// this.resetScroll();
// }
// }
}
private scroll(entry: ToCEntry) {
@ -619,19 +611,6 @@ export class DescriptionEditorComponent extends BaseEditor<DescriptionEditorMode
document.getElementById('description-editor-form').scrollTop = 0;
}
get visibleFieldSets(): ToCEntry[] {
let fieldSets = [];
let arrays = this.table0fContents ? this.table0fContents.tocentries.
filter(entry => !this.table0fContents.internalTable.hiddenEntries.find(hiddenEntry => hiddenEntry === entry.id.toString())).map(entry => {
return this.getEntryVisibleFieldSets(entry);
})
: [];
arrays.forEach(array => {
fieldSets = fieldSets.concat(array);
});
return fieldSets;
}
get countErrorsOfBaseInfoPage(): number {
if (this.formGroup == null) return 0;
@ -645,18 +624,6 @@ export class DescriptionEditorComponent extends BaseEditor<DescriptionEditorMode
return errorsCount;
}
getEntryVisibleFieldSets(entry: ToCEntry): ToCEntry[] {
let fieldSets = [];
if (entry.type === ToCEntryType.FieldSet && !this.table0fContents.internalTable.hiddenEntries.find(hiddenEntry => hiddenEntry === entry.id.toString())) {
fieldSets.push(entry);
} else if (entry.type !== ToCEntryType.FieldSet) {
entry.subEntries.forEach(subEntry => {
fieldSets = fieldSets.concat(this.getEntryVisibleFieldSets(subEntry));
});
}
return fieldSets;
}
registerFormListeners() {
this.formGroup.get('descriptionTemplateId').valueChanges
@ -667,10 +634,6 @@ export class DescriptionEditorComponent extends BaseEditor<DescriptionEditorMode
this.descriptionTemplateValueChanged(descriptionTemplateId);
}
});
this.formGroup.get('properties').valueChanges
.pipe(takeUntil(this._destroyed))
.subscribe(next => this.tocValidationService.validateForm());
}
descriptionTemplateValueChanged(descriptionTemplateId: Guid) {
@ -757,7 +720,7 @@ export class DescriptionEditorComponent extends BaseEditor<DescriptionEditorMode
this.formService.touchAllFormFields(this.formGroup);
this.formService.validateAllFormFields(this.formGroup);
this.tocValidationService.validateForm();
this.showTocEntriesErrors = true;
if (!this.isFormValid()) {
this.dialog.open(FormValidationErrorsDialogComponent, {
data: {

View File

@ -28,7 +28,6 @@ export class DescriptionFormComponent extends BaseComponent implements OnInit, O
@Input() validationErrorModel: ValidationErrorModel;
@Output() formChanged: EventEmitter<any> = new EventEmitter();
@Output() fieldsetFocusChange: EventEmitter<string> = new EventEmitter<string>();
constructor(
public descriptionFormAnnotationService: DescriptionFormAnnotationService,

View File

@ -1,4 +1,4 @@
import { Guid } from "@common/types/guid";
import { AbstractControl } from "@angular/forms";
import { ToCEntryType } from "./toc-entry-type.enum";
export interface ToCEntry {
@ -10,5 +10,12 @@ export interface ToCEntry {
// form: AbstractControl;
numbering: string;
ordinal: number;
hidden?: boolean
hidden?: boolean;
visibilityRuleKey: string;
validityAbstractControl: AbstractControl;
isLastEntry?: boolean;
isFirstEntry?: boolean;
previousEntry?: ToCEntry;
NextEntry?: ToCEntry;
}

View File

@ -0,0 +1,25 @@
import { EventEmitter, Injectable } from "@angular/core";
import { Observable } from "rxjs";
@Injectable()
export class TableOfContentsService {
private _nextClickedEventEmmiter: EventEmitter<any> = new EventEmitter<any>();
private _previousClickedEventEmmiter: EventEmitter<any> = new EventEmitter<any>();
getNextClickedEventObservable(): Observable<any> {
return this._nextClickedEventEmmiter.asObservable();
}
getPreviousEventObservable(): Observable<any> {
return this._previousClickedEventEmmiter.asObservable();
}
nextClicked(): void {
this._nextClickedEventEmmiter.emit();
}
previousClicked(): void {
this._previousClickedEventEmmiter.emit();
}
}

View File

@ -1,13 +0,0 @@
import { EventEmitter, Injectable } from "@angular/core";
@Injectable()
export class TableOfContentsValidationService {
private _validateFormEvent: EventEmitter<any> = new EventEmitter<any>();
get validateFormEvent(): EventEmitter<any> {
return this._validateFormEvent;
}
validateForm(): void {
this._validateFormEvent.emit();
}
}

View File

@ -1,56 +1,31 @@
<div *ngFor="let entry of tocentries; index as idx">
<!-- check if is visible -->
<!-- <ng-container *ngIf="!hiddenEntries.includes(entry.id)"> -->
<ng-container *ngIf="!hiddenEntries.includes(entry.id)">
<!-- Is fieldset and has no visible inputs -->
<!-- <ng-container *ngIf="!(entry.type === tocEntryTypeEnum.FieldSet && !visibilityRulesService.scanIfChildsOfCompositeFieldHasVisibleItems(entry.form))"> TODO: add this bellow -->
<!-- <ng-container *ngIf="!(entry.type === tocEntryTypeEnum.FieldSet)"> -->
<ng-container *ngIf="visibilityRulesService.isVisibleMap[entry.visibilityRuleKey] ?? true">
<ng-container >
<span class="table-entry"
(click)="toggleExpand(idx);navigateToFieldSet(entry, $event); onEntrySelected(entry);"
(click)="toggleExpand(idx); onEntrySelected(entry);"
[ngStyle]="calculateStyle(entry)"
[ngClass]="calculateClass(entry)"
>
<span [class.text-danger]="showErrors && propertiesFormGroup.get('entry.id')?.invalid && ( entry.type === tocEntryTypeEnum.FieldSet || (entry.type !== tocEntryTypeEnum.FieldSet && !expandChildren[idx])) && invalidChildsVisible(entry) ">
<span [class.text-danger]="!isTocEntryValid(entry) && showErrors">
{{entry.numbering}}. {{entry.label}}
</span>
<!-- <mat-icon style="transform: translateY(3px);" class="text-danger"
*ngIf="showErrors && entry.form.invalid && entry.type !== tocEntryTypeEnum.FieldSet && !expandChildren[idx]">
priority_high
</mat-icon> -->
<!-- <ng-container *ngIf="entry.subEntries && entry.subEntries.length && !expandChildren[idx]">
<small>
({{entry.subEntries.length}})
</small>
</ng-container> -->
</span>
<!-- <div class="table-entry-container">
</div> -->
<div class="internal-table">
<table-of-contents-internal
[tocentries]="entry.subEntries"
*ngIf="entry.subEntries && entry.subEntries.length && expandChildren[idx]"
(entrySelected)="onEntrySelected($event)"
[selected]="selected"
[showErrors]="showErrors"
[hiddenEntries]="hiddenEntries"
[visibilityRulesService]="visibilityRulesService"
[propertiesFormGroup]="propertiesFormGroup"
[parentId]="entry.id"
[parentMap]="updatedMap"
(scrollFinished)="onScrollFinished($event)"
(scrollStarted)="onScrollStarted($event)"
[showErrors]="showErrors"
>
</table-of-contents-internal>
</div>
</ng-container>
</ng-container>
</div>

View File

@ -1,50 +1,35 @@
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, QueryList, SimpleChanges, ViewChildren } from '@angular/core';
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core';
import { UntypedFormGroup } from '@angular/forms';
import { VisibilityRulesService } from '@app/ui/description/editor/description-form/visibility-rules/visibility-rules.service';
import { Guid } from '@common/types/guid';
import { BaseComponent } from '@common/base/base.component';
import { ToCEntry } from '../models/toc-entry';
import { ToCEntryType } from '../models/toc-entry-type.enum';
import { DescriptionFieldIndicator } from '../../description-editor.model';
import { Observable, Subscription, map } from 'rxjs';
import { TableOfContentsValidationService } from '../services/table-of-contents-validation-service';
import { TableOfContentsComponent } from '../table-of-contents.component';
@Component({
selector: 'table-of-contents-internal',
styleUrls: ['./table-of-contents-internal.scss'],
templateUrl: './table-of-contents-internal.html'
})
export class TableOfContentsInternal implements OnInit, OnDestroy {
export class TableOfContentsInternal extends BaseComponent implements OnInit, OnDestroy {
@Input() tocentries: ToCEntry[] = null;
@Input() selected: ToCEntry = null;
// @Input() visibilityRules:Rule[] = [];
@Output() entrySelected = new EventEmitter<ToCEntry>();
@Output() scrollStarted = new EventEmitter<ToCEntry>();
@Output() scrollFinished = new EventEmitter<ToCEntry>();
@Input() propertiesFormGroup: UntypedFormGroup;
expandChildren: boolean[];
tocEntryTypeEnum = ToCEntryType;
@Input() showErrors: boolean = false;
@Input() hiddenEntries: string[] = [];
@Input() visibilityRulesService: VisibilityRulesService;
@ViewChildren(TableOfContentsInternal) internalTables: QueryList<TableOfContentsInternal>;
@Input() parentId: string;
@Input() parentMap: Map<string, DescriptionFieldIndicator[]> = new Map<string, DescriptionFieldIndicator[]>();
@Input() updatedMap: Map<string, DescriptionFieldIndicator[]> = new Map<string, DescriptionFieldIndicator[]>();
tocEntriesStateSubscriptions: Subscription[] = [];
tocEntriesStateMap: Map<string, boolean> = new Map<string, boolean>();
constructor(private tocValidationService: TableOfContentsValidationService) { }
constructor() { super(); }
ngOnInit(): void {
// console.log('component created' + JSON.stringify(this.tocentries));
if (this.tocentries) {
this.expandChildren = this.tocentries.map(() => false);
if (this.selected) {
for (let i = 0; i < this.tocentries.length; i++) {
if (this._findTocEntryById(this.selected.id, this.tocentries[i].subEntries)) {
if (TableOfContentsComponent._findTocEntryById(this.selected.id, this.tocentries[i].subEntries)) {
if (this.expandChildren) {
this.expandChildren[i] = true;
}
@ -52,17 +37,13 @@ export class TableOfContentsInternal implements OnInit, OnDestroy {
}
}
}
if (this.parentMap) {
this.refreshErrorIndicators();
}
}
}
ngOnChanges(changes: SimpleChanges) {
if (changes.selected && this.selected) {
for (let i = 0; i < this.tocentries.length; i++) {
if (this._findTocEntryById(this.selected.id, this.tocentries[i].subEntries)) {
if (TableOfContentsComponent._findTocEntryById(this.selected.id, this.tocentries[i].subEntries)) {
if (this.expandChildren) {
this.expandChildren[i] = true;
}
@ -70,127 +51,24 @@ export class TableOfContentsInternal implements OnInit, OnDestroy {
}
}
}
if (changes.parentMap && this.parentMap) {
this.refreshErrorIndicators();
}
// if (!this.isActive && this.links && this.links.length > 0) {
// this.links.forEach(link => {
// link.selected = false;
// })
// this.links[0].selected = true;
// }
}
ngOnDestroy(): void {
this.tocEntriesStateSubscriptions.forEach((errorSubscription: Subscription) => {
errorSubscription.unsubscribe();
});
}
refreshErrorIndicators(): void {
this.updatedMap = this.updateMap(this.tocentries, this.parentMap);
for (let entry of this.tocentries) {
this.tocEntriesStateMap.set(entry.id, this.hasErrors(entry.id));
this.tocEntriesStateSubscriptions.push(
this.tocValidationService.validateFormEvent
.pipe(map(() => this.hasErrors(entry.id)))
.subscribe(next => {
this.tocEntriesStateMap.set(entry.id, next);
})
);
}
}
updateMap(entries: ToCEntry[], parentMap:Map<string, DescriptionFieldIndicator[]>): Map<string, DescriptionFieldIndicator[]> {
if (this.parentId == null) return parentMap;
let updatedMap = new Map<string, DescriptionFieldIndicator[]>();
parentMap.forEach((fields: DescriptionFieldIndicator[], parentId: string) => {
if (this.parentId === parentId) {
for (let entry of entries) {
let entryFields = fields.filter((field: DescriptionFieldIndicator) => field.sectionIds.includes(entry.id) || field.fieldSetId === entry.id || field.fieldId === entry.id )
updatedMap.set(entry.id, entryFields);
}
}
});
return updatedMap;
}
hasErrors(entryId: string): boolean {
if (this.updatedMap.size == 0) return false;
const fields: DescriptionFieldIndicator[] = this.updatedMap.get(entryId);
for (let field of fields) {
let formFieldName: string = `fieldSets.${field.fieldSetId}.items.0.fields.${field.fieldId}.${field.type}`;
if (this.isFormFieldValid(formFieldName) === false) {
return true;
}
}
return false;
}
isFormFieldValid(formFieldName: string):boolean {
const formField = this.propertiesFormGroup?.get(formFieldName);
if (formField == null) return true;
if (formField.dirty === false
&& formField.touched === false) return true;
return formField.valid;
super.ngOnDestroy();
}
toggleExpand(index) {
this.expandChildren[index] = !this.expandChildren[index];
// console.log(this.expandChildren);
}
navigateToFieldSet(entry: ToCEntry, event) {
if (entry.type === ToCEntryType.FieldSet) {
this.onScrollStarted(entry);
const fieldSetId = entry.id;
const element = document.getElementById(fieldSetId);
if (element) {
element.click();//open mat expansion panel
//scroll asyn in 200 ms so the expansion panel is expanded and the element coordinates are updated
setTimeout(() => {
const element = document.getElementById(fieldSetId);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
// element.focus({preventScroll: true});
setTimeout(() => {
this.scrollFinished.emit(entry);
}, 1000);
}
}, 300);
}
}
}
onEntrySelected(entry: ToCEntry) {
this.entrySelected.emit(entry);
}
onScrollStarted(entry: ToCEntry) {
this.scrollStarted.emit(entry);
}
onScrollFinished(entry: ToCEntry) {
this.scrollFinished.emit(entry);
}
calculateStyle(entry: ToCEntry) {
const style = {};
style['font-size'] = entry.type === this.tocEntryTypeEnum.FieldSet ? '.9em' : '1em';
style['font-size'] = entry.type === ToCEntryType.FieldSet ? '.9em' : '1em';
return style;
}
@ -201,64 +79,24 @@ export class TableOfContentsInternal implements OnInit, OnDestroy {
myClass['selected'] = true;
}
if (entry.type != this.tocEntryTypeEnum.FieldSet) {
if (entry.type != ToCEntryType.FieldSet) {
myClass['section'] = true;
}
if(this.tocEntriesStateMap?.get(entry.id) === true) {
myClass['text-danger'] = true;
}
return myClass;
}
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;
}
public invalidChildsVisible(entry: ToCEntry): boolean {
if (!entry || !this.visibilityRulesService) {
return false;
}
if (entry.type != this.tocEntryTypeEnum.FieldSet) {
return entry.subEntries.some(_ => this.invalidChildsVisible(_));
}
if (entry.type === this.tocEntryTypeEnum.FieldSet) {
const id = entry.id;
if (!(this.visibilityRulesService.isVisibleMap[id.toString()])) {
return false;
}
// const fieldsArray = entry.form.get('fields') as UntypedFormArray;
const hasError = !entry.subEntries.every(field => {//every invalid field should be invisible
//TODO: check this
// if (field.invalid) {
// const id = field.id;
// const isVisible = this.visibilityRulesService.isVisibleMap[id);
// if (isVisible) {
// return false;
// }
// }
isTocEntryValid(entry: ToCEntry): boolean {
if (entry == null) {
return true;
});
return hasError;
}
return false;
let currentValidity = entry.validityAbstractControl?.valid ?? true;
if (!currentValidity) return currentValidity;
entry.subEntries?.forEach(subEntry => {
currentValidity = currentValidity && this.isTocEntryValid(subEntry);
if (!currentValidity) return currentValidity;
});
return currentValidity;
}
}

View File

@ -1,11 +1,3 @@
<div *ngIf="links?.length" class="row docs-toc-container">
<div class="col-12 scroll-container">
<span *ngFor="let link of links; let i = index" (click)="toggle(link); goToStep(link)" class="docs-level-{{link.type}} docs-link mt-0" [class.docs-active]="link.active">
<span *ngIf="link.show" class="link-name"><span [class.selected]="link.selected && isActive">{{link.name}}</span></span>
</span>
</div>
</div>
<div *ngIf="tocentries" class="row docs-toc-container">
<div class="scroll-container col-12 internal-table">
@ -13,12 +5,8 @@
[showErrors]="showErrors"
(entrySelected)="onToCentrySelected($event)"
[selected]="tocentrySelected"
[hiddenEntries]="hiddenEntries"
[visibilityRulesService]="visibilityRulesService"
[propertiesFormGroup]="formGroup"
[parentMap]="pageToFieldSetMap"
(scrollFinished)="onScrollFinished($event)"
(scrollStarted)="onScrollStarted($event)"
>
</table-of-contents-internal>

View File

@ -1,32 +1,12 @@
import { DOCUMENT } from '@angular/common';
import { Component, EventEmitter, Inject, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core';
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
import { UntypedFormGroup } from '@angular/forms';
import { DescriptionTemplate, DescriptionTemplateField, DescriptionTemplateFieldSet, DescriptionTemplateSection } from '@app/core/model/description-template/description-template';
import { DescriptionTemplate, 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 { Observable, debounceTime, distinctUntilChanged, filter, mergeMap, take, takeUntil } from 'rxjs';
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';
import { DescriptionFieldIndicator } from '../description-editor.model';
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;
}
import { TableOfContentsService } from './services/table-of-contents-service';
@Component({
selector: 'app-table-of-contents',
@ -35,30 +15,18 @@ export interface Link {
})
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;
@Input() showErrors: boolean = false;
@Input() formGroup: UntypedFormGroup;
@Input() descriptionTemplate: DescriptionTemplate;
@Input() hasFocus: boolean = false;
@Input() visibilityRulesService: VisibilityRulesService;
tocentries: ToCEntry[] = null;
@Input() showErrors: boolean = false;
@Input() selectedFieldsetId: string;
private _intersectionObserver: IntersectionObserver;
private _tocentrySelected: ToCEntry = null;
private isSelecting: boolean = false;
private isScrolling: boolean = false;
private _intersectionObserver: IntersectionObserver;
private _actOnObservation: boolean = true;
public hiddenEntries: string[] = [];
get tocentrySelected() {
return this.hasFocus ? this._tocentrySelected : null;
}
@ -66,17 +34,9 @@ export class TableOfContentsComponent extends BaseComponent implements OnInit, O
this._tocentrySelected = value;
}
@Input() formGroup: UntypedFormGroup;
@Input() descriptionTemplate: DescriptionTemplate;
@Input() hasFocus: boolean = false;
@Input() visibilityRulesService: VisibilityRulesService;
show: boolean = false;
@Input() pageToFieldSetMap: Map<string, DescriptionFieldIndicator[]>;
constructor(
@Inject(DOCUMENT) private _document: Document,
// public visibilityRulesService: VisibilityRulesService
private tableOfContentsService: TableOfContentsService
) {
super();
}
@ -84,166 +44,45 @@ export class TableOfContentsComponent extends BaseComponent implements OnInit, O
ngOnInit(): void {
this.visibilityRulesService.getRulesChangedObservable().pipe(takeUntil(this._destroyed)).subscribe(data => {
this.hiddenEntries = this._findHiddenEntries(this.tocentries);
});
if (this.descriptionTemplate) {
this.tocentries = this.getTocEntries(this.descriptionTemplate);
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);
this.tableOfContentsService.getNextClickedEventObservable().pipe(takeUntil(this._destroyed)).subscribe(x => {
//TODO: implement function to find next element
// call onToCentrySelected()
});
// 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;
// })
// }
this.tableOfContentsService.getPreviousEventObservable().pipe(takeUntil(this._destroyed)).subscribe(x => {
//TODO: implement function to find previous element
// call onToCentrySelected()
});
}
}
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;
}
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);
this.hiddenEntries = this._findHiddenEntries(this.tocentries);
}
}
private _resetObserver() {
if (this._intersectionObserver) {//clean up
this._intersectionObserver.disconnect();
this._intersectionObserver = null;
}
new Observable(observer => {
const options = {
root: null,
rootMargin: '20% 0px -30% 0px',
threshold: 0
threshold: [0, 0.2, 0.5, 1]
}
this._intersectionObserver = new IntersectionObserver((entries, observer) => {
if (!this._actOnObservation) {
return;
}
entries.forEach(ie => {
if (ie.isIntersecting) {
try {
if(!this.isScrolling) {
const target_id = ie.target.id;
if (this.visibilityRulesService.isVisibleMap[target_id] ?? true) {
this.onToCentrySelected(this._findTocEntryById(target_id, this.tocentries));
}
}
} catch {
}
// setTimeout(() => {
// try {
// if(!this.isSelecting) {
// const target_id = ie.target.id;
// if (this.visibilityRulesService.isVisibleMap[target_id] ?? true) {
// this.onToCentrySelected(this._findTocEntryById(target_id, this.tocentries));
// }
// }
// } catch {
// }
// }, 1000)
}
})
this._intersectionObserver = new IntersectionObserver(entries => {
observer.next(entries);
}, options);
const fieldsetsEtries = this._getAllFieldSets(this.tocentries);
@ -258,48 +97,24 @@ export class TableOfContentsComponent extends BaseComponent implements OnInit, O
}
}
});
}
goToStep(link: Link) {
this.stepFound.emit({
page: link.page,
section: link.section
return () => { this._intersectionObserver.disconnect(); };
}).pipe(
takeUntil(this._destroyed),
mergeMap((entries: IntersectionObserverEntry[]) => entries),
filter(entry => entry.isIntersecting),
debounceTime(200),
distinctUntilChanged(),
).subscribe(x => {
if (x.isIntersecting) {
const target_id = x.target.id;
console.log(target_id);
if (this.visibilityRulesService.isVisibleMap[target_id] ?? true) {
this.tocentrySelected = TableOfContentsComponent._findTocEntryById(target_id, this.tocentries);
}
}
});
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;
@ -328,18 +143,14 @@ export class TableOfContentsComponent extends BaseComponent implements OnInit, O
subEntries: tempResult,
subEntriesType: sections && sections.length ? ToCEntryType.Section : ToCEntryType.FieldSet,
type: ToCEntryType.Section,
ordinal: item.ordinal
ordinal: item.ordinal,
visibilityRuleKey: item.id,
validityAbstractControl: this.formGroup.get('fieldSets').get(item.id)
}
}
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,
@ -349,22 +160,9 @@ export class TableOfContentsComponent extends BaseComponent implements OnInit, O
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
visibilityRuleKey: item.id,
validityAbstractControl: this.formGroup.get('fieldSets').get(item.id)
};
}
@ -404,15 +202,15 @@ export class TableOfContentsComponent extends BaseComponent implements OnInit, O
//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
ordinal: pageElement.ordinal,
visibilityRuleKey: pageElement.id,
validityAbstractControl: null
};
const sections = descriptionTemplate.definition.pages.find(x => x.id == pageElement.id)?.sections;
@ -432,45 +230,10 @@ export class TableOfContentsComponent extends BaseComponent implements OnInit, O
}
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);
}
}
onScrollStarted(entry: ToCEntry) {
this.isScrolling = true;
}
onScrollFinished(entry: ToCEntry) {
this.isScrolling = false;
}
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
@ -492,15 +255,27 @@ export class TableOfContentsComponent extends BaseComponent implements OnInit, O
return fieldsets;
}
public hasVisibleInvalidFields(): boolean {
if (!this.internalTable || !this.tocentries) {
return false;
}
return this.tocentries.some(e => this.internalTable.invalidChildsVisible(e));
public static _findTocEntryById(id: string, tocentries: ToCEntry[]): ToCEntry {
if (!tocentries || !tocentries.length) {
return null;
}
protected readonly console = console;
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;
}
}
export interface LinkToScroll {

View File

@ -5,12 +5,12 @@ import { TableOfContentsInternal } from './table-of-contents-internal/table-of-c
import { MatIconModule } from '@angular/material/icon';
import { VisibilityRulesService } from '@app/ui/description/editor/description-form/visibility-rules/visibility-rules.service';
import { TableOfContentsComponent } from './table-of-contents.component';
import { TableOfContentsValidationService } from './services/table-of-contents-validation-service';
import { TableOfContentsService } from './services/table-of-contents-service';
@NgModule({
imports: [CommonModule, RouterModule, MatIconModule],
declarations: [TableOfContentsComponent, TableOfContentsInternal],
exports: [TableOfContentsComponent],
providers: [VisibilityRulesService, TableOfContentsValidationService]
providers: [VisibilityRulesService, TableOfContentsService]
})
export class TableOfContentsModule { }