From 8940394aa5e0991a737e58ae49b79f8b3d1818ff Mon Sep 17 00:00:00 2001 From: Diamantis Tziotzios Date: Fri, 7 Jun 2024 16:06:27 +0300 Subject: [PATCH] changed description editor table of contents --- .../editor/description-editor.component.html | 3 +- .../editor/description-editor.component.ts | 167 +++----- .../description-form.component.ts | 1 - .../table-of-contents/models/toc-entry.ts | 11 +- .../services/table-of-contents-service.ts | 25 ++ .../table-of-contents-validation-service.ts | 13 - .../table-of-contents-internal.html | 37 +- .../table-of-contents-internal.ts | 202 +-------- .../table-of-contents.component.html | 12 - .../table-of-contents.component.ts | 383 ++++-------------- .../table-of-contents.module.ts | 4 +- 11 files changed, 207 insertions(+), 651 deletions(-) create mode 100644 dmp-frontend/src/app/ui/description/editor/table-of-contents/services/table-of-contents-service.ts delete mode 100644 dmp-frontend/src/app/ui/description/editor/table-of-contents/services/table-of-contents-validation-service.ts diff --git a/dmp-frontend/src/app/ui/description/editor/description-editor.component.html b/dmp-frontend/src/app/ui/description/editor/description-editor.component.html index 60d772458..a56b2e1c2 100644 --- a/dmp-frontend/src/app/ui/description/editor/description-editor.component.html +++ b/dmp-frontend/src/app/ui/description/editor/description-editor.component.html @@ -107,7 +107,7 @@
- +
@@ -163,7 +163,6 @@ [validationErrorModel]="editorModel.validationErrorModel" [isNew]="isNew || isCopy" [canReview]="canReview" - (fieldsetFocusChange)="fieldsetIdWithFocus = $event" > diff --git a/dmp-frontend/src/app/ui/description/editor/description-editor.component.ts b/dmp-frontend/src/app/ui/description/editor/description-editor.component.ts index fc88915f5..8ec811903 100644 --- a/dmp-frontend/src/app/ui/description/editor/description-editor.component.ts +++ b/dmp-frontend/src/app/ui/description/editor/description-editor.component.ts @@ -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 { if (result) { this.titleService.setTitle(result.label); - + result.dmp = this.item.dmp; result.dmpDescriptionTemplate = this.item.dmpDescriptionTemplate; - + const sectionId = this.item.dmpDescriptionTemplate.sectionId; result.dmpDescriptionTemplate = this.item.dmp.dmpDescriptionTemplates.find(x => x.sectionId == sectionId && x.descriptionTemplateGroupId == result.descriptionTemplate.groupId); - + this.prepareForm(result); this.changeDetectorRef.markForCheck(); } @@ -267,10 +266,9 @@ export class DescriptionEditorComponent extends BaseEditor { + 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 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 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 !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 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,35 +634,31 @@ export class DescriptionEditorComponent extends BaseEditor this.tocValidationService.validateForm()); } descriptionTemplateValueChanged(descriptionTemplateId: Guid) { if (descriptionTemplateId != null) { this.descriptionTemplateService.getSingle(descriptionTemplateId, DescriptionEditorResolver.descriptionTemplateLookupFields()).pipe(takeUntil(this._destroyed)) - .subscribe(descriptionTemplate => { + .subscribe(descriptionTemplate => { - this.initialTemplateId = descriptionTemplateId.toString(); - this.editorModel.properties = new DescriptionPropertyDefinitionEditorModel(this.editorModel.validationErrorModel).fromModel(null, descriptionTemplate, null); - this.formGroup.setControl('properties', this.editorModel.buildProperties(this.visibilityRulesService)); - this.item.descriptionTemplate = descriptionTemplate; + this.initialTemplateId = descriptionTemplateId.toString(); + this.editorModel.properties = new DescriptionPropertyDefinitionEditorModel(this.editorModel.validationErrorModel).fromModel(null, descriptionTemplate, null); + this.formGroup.setControl('properties', this.editorModel.buildProperties(this.visibilityRulesService)); + this.item.descriptionTemplate = descriptionTemplate; - const sectionId = this.item.dmpDescriptionTemplate.sectionId; - this.item.dmpDescriptionTemplate = this.item.dmp.dmpDescriptionTemplates.find(x => x.sectionId == sectionId && x.descriptionTemplateGroupId == descriptionTemplate.groupId); - this.formGroup.get('dmpDescriptionTemplateId').setValue(this.item.dmpDescriptionTemplate.id); - if (descriptionTemplate.definition) this.visibilityRulesService.setContext(this.item.descriptionTemplate.definition, this.formGroup.get('properties')); - if (descriptionTemplate.definition) this.pageToFieldSetMap = this.mapPageToFieldSet(this.item.descriptionTemplate); + const sectionId = this.item.dmpDescriptionTemplate.sectionId; + this.item.dmpDescriptionTemplate = this.item.dmp.dmpDescriptionTemplates.find(x => x.sectionId == sectionId && x.descriptionTemplateGroupId == descriptionTemplate.groupId); + this.formGroup.get('dmpDescriptionTemplateId').setValue(this.item.dmpDescriptionTemplate.id); + if (descriptionTemplate.definition) this.visibilityRulesService.setContext(this.item.descriptionTemplate.definition, this.formGroup.get('properties')); + if (descriptionTemplate.definition) this.pageToFieldSetMap = this.mapPageToFieldSet(this.item.descriptionTemplate); - this.registerFormListeners(); - }, - error => { - this.formGroup.get('descriptionTemplateId').setValue(this.initialTemplateId && this.initialTemplateId != '' ? this.initialTemplateId : null); - this.httpErrorHandlingService.handleBackedRequestError(error); - }); + this.registerFormListeners(); + }, + error => { + this.formGroup.get('descriptionTemplateId').setValue(this.initialTemplateId && this.initialTemplateId != '' ? this.initialTemplateId : null); + this.httpErrorHandlingService.handleBackedRequestError(error); + }); } } @@ -756,8 +719,8 @@ export class DescriptionEditorComponent extends BaseEditor = new EventEmitter(); - @Output() fieldsetFocusChange: EventEmitter = new EventEmitter(); constructor( public descriptionFormAnnotationService: DescriptionFormAnnotationService, diff --git a/dmp-frontend/src/app/ui/description/editor/table-of-contents/models/toc-entry.ts b/dmp-frontend/src/app/ui/description/editor/table-of-contents/models/toc-entry.ts index 353f72d7e..d99a56426 100644 --- a/dmp-frontend/src/app/ui/description/editor/table-of-contents/models/toc-entry.ts +++ b/dmp-frontend/src/app/ui/description/editor/table-of-contents/models/toc-entry.ts @@ -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; } \ No newline at end of file diff --git a/dmp-frontend/src/app/ui/description/editor/table-of-contents/services/table-of-contents-service.ts b/dmp-frontend/src/app/ui/description/editor/table-of-contents/services/table-of-contents-service.ts new file mode 100644 index 000000000..25356fd5a --- /dev/null +++ b/dmp-frontend/src/app/ui/description/editor/table-of-contents/services/table-of-contents-service.ts @@ -0,0 +1,25 @@ +import { EventEmitter, Injectable } from "@angular/core"; +import { Observable } from "rxjs"; + +@Injectable() +export class TableOfContentsService { + + private _nextClickedEventEmmiter: EventEmitter = new EventEmitter(); + private _previousClickedEventEmmiter: EventEmitter = new EventEmitter(); + + getNextClickedEventObservable(): Observable { + return this._nextClickedEventEmmiter.asObservable(); + } + + getPreviousEventObservable(): Observable { + return this._previousClickedEventEmmiter.asObservable(); + } + + nextClicked(): void { + this._nextClickedEventEmmiter.emit(); + } + + previousClicked(): void { + this._previousClickedEventEmmiter.emit(); + } +} diff --git a/dmp-frontend/src/app/ui/description/editor/table-of-contents/services/table-of-contents-validation-service.ts b/dmp-frontend/src/app/ui/description/editor/table-of-contents/services/table-of-contents-validation-service.ts deleted file mode 100644 index c90ed33ca..000000000 --- a/dmp-frontend/src/app/ui/description/editor/table-of-contents/services/table-of-contents-validation-service.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { EventEmitter, Injectable } from "@angular/core"; - -@Injectable() -export class TableOfContentsValidationService { - private _validateFormEvent: EventEmitter = new EventEmitter(); - get validateFormEvent(): EventEmitter { - return this._validateFormEvent; - } - - validateForm(): void { - this._validateFormEvent.emit(); - } -} diff --git a/dmp-frontend/src/app/ui/description/editor/table-of-contents/table-of-contents-internal/table-of-contents-internal.html b/dmp-frontend/src/app/ui/description/editor/table-of-contents/table-of-contents-internal/table-of-contents-internal.html index 6b7dafd9e..8331eeca8 100644 --- a/dmp-frontend/src/app/ui/description/editor/table-of-contents/table-of-contents-internal/table-of-contents-internal.html +++ b/dmp-frontend/src/app/ui/description/editor/table-of-contents/table-of-contents-internal/table-of-contents-internal.html @@ -1,56 +1,31 @@
- - - - - - + - + {{entry.numbering}}. {{entry.label}} - - - + + -
-
- - -
\ No newline at end of file diff --git a/dmp-frontend/src/app/ui/description/editor/table-of-contents/table-of-contents-internal/table-of-contents-internal.ts b/dmp-frontend/src/app/ui/description/editor/table-of-contents/table-of-contents-internal/table-of-contents-internal.ts index f257ee2c5..925ab96c9 100644 --- a/dmp-frontend/src/app/ui/description/editor/table-of-contents/table-of-contents-internal/table-of-contents-internal.ts +++ b/dmp-frontend/src/app/ui/description/editor/table-of-contents/table-of-contents-internal/table-of-contents-internal.ts @@ -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(); - @Output() scrollStarted = new EventEmitter(); - @Output() scrollFinished = new EventEmitter(); @Input() propertiesFormGroup: UntypedFormGroup; expandChildren: boolean[]; - tocEntryTypeEnum = ToCEntryType; @Input() showErrors: boolean = false; - @Input() hiddenEntries: string[] = []; @Input() visibilityRulesService: VisibilityRulesService; - @ViewChildren(TableOfContentsInternal) internalTables: QueryList; - @Input() parentId: string; - @Input() parentMap: Map = new Map(); - @Input() updatedMap: Map = new Map(); - tocEntriesStateSubscriptions: Subscription[] = []; - tocEntriesStateMap: Map = new Map(); + constructor() { super(); } - constructor(private tocValidationService: TableOfContentsValidationService) { } - 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): Map { - if (this.parentId == null) return parentMap; - - let updatedMap = new Map(); - - 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; + isTocEntryValid(entry: ToCEntry): boolean { + if (entry == null) { + return true; } - let tocEntryFound = tocentries.find(entry => entry.id === id); + let currentValidity = entry.validityAbstractControl?.valid ?? true; + if (!currentValidity) return currentValidity; + entry.subEntries?.forEach(subEntry => { + currentValidity = currentValidity && this.isTocEntryValid(subEntry); + if (!currentValidity) return currentValidity; + }); - 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; - // } - // } - return true; - }); - return hasError; - } - return false; + return currentValidity; } } diff --git a/dmp-frontend/src/app/ui/description/editor/table-of-contents/table-of-contents.component.html b/dmp-frontend/src/app/ui/description/editor/table-of-contents/table-of-contents.component.html index dd156e3bc..5501ffd8a 100644 --- a/dmp-frontend/src/app/ui/description/editor/table-of-contents/table-of-contents.component.html +++ b/dmp-frontend/src/app/ui/description/editor/table-of-contents/table-of-contents.component.html @@ -1,11 +1,3 @@ -
-
- - {{link.name}} - -
-
-
@@ -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)" > diff --git a/dmp-frontend/src/app/ui/description/editor/table-of-contents/table-of-contents.component.ts b/dmp-frontend/src/app/ui/description/editor/table-of-contents/table-of-contents.component.ts index 5674200e9..6d9d5aefd 100644 --- a/dmp-frontend/src/app/ui/description/editor/table-of-contents/table-of-contents.component.ts +++ b/dmp-frontend/src/app/ui/description/editor/table-of-contents/table-of-contents.component.ts @@ -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(); - @Output() currentLinks = new EventEmitter(); @Output() entrySelected = new EventEmitter(); - subscription: Subscription; - linksSubject: Subject = new Subject(); - - @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; constructor( - @Inject(DOCUMENT) private _document: Document, - // public visibilityRulesService: VisibilityRulesService + private tableOfContentsService: TableOfContentsService ) { super(); } @@ -84,223 +44,78 @@ 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); - }); - - // 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; - // }) - // } } + this.tableOfContentsService.getNextClickedEventObservable().pipe(takeUntil(this._destroyed)).subscribe(x => { + //TODO: implement function to find next element + // call onToCentrySelected() + }); + + 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; } - const options = { - root: null, - rootMargin: '20% 0px -30% 0px', - threshold: 0 - } - - this._intersectionObserver = new IntersectionObserver((entries, observer) => { - if (!this._actOnObservation) { - return; + new Observable(observer => { + const options = { + root: null, + rootMargin: '20% 0px -30% 0px', + threshold: [0, 0.2, 0.5, 1] } + this._intersectionObserver = new IntersectionObserver(entries => { + observer.next(entries); + }, options); - entries.forEach(ie => { - if (ie.isIntersecting) { + const fieldsetsEtries = this._getAllFieldSets(this.tocentries); + + fieldsetsEtries.forEach(e => { + if (e.type === ToCEntryType.FieldSet) { 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)); - } - } + const targetElement = document.getElementById(e.id); + this._intersectionObserver.observe(targetElement); } catch { + console.log('element not found'); } - - // 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) } - }) - }, options); + }); - const fieldsetsEtries = this._getAllFieldSets(this.tocentries); - - fieldsetsEtries.forEach(e => { - if (e.type === ToCEntryType.FieldSet) { - try { - const targetElement = document.getElementById(e.id); - this._intersectionObserver.observe(targetElement); - } catch { - console.log('element not found'); + 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); } } }); } - 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; @@ -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); - } + this.tocentrySelected = entry; + this.entrySelected.emit({ entry: entry, execute: execute }); } - 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; + public static _findTocEntryById(id: string, tocentries: ToCEntry[]): ToCEntry { + if (!tocentries || !tocentries.length) { + return null; } - return this.tocentries.some(e => this.internalTable.invalidChildsVisible(e)); - } - 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 { diff --git a/dmp-frontend/src/app/ui/description/editor/table-of-contents/table-of-contents.module.ts b/dmp-frontend/src/app/ui/description/editor/table-of-contents/table-of-contents.module.ts index 413bfde9a..dd3f6cb70 100644 --- a/dmp-frontend/src/app/ui/description/editor/table-of-contents/table-of-contents.module.ts +++ b/dmp-frontend/src/app/ui/description/editor/table-of-contents/table-of-contents.module.ts @@ -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 { }