import { DOCUMENT } from '@angular/common'; import { Component, EventEmitter, Inject, OnInit, Output, Input } from '@angular/core'; import { BaseComponent } from '@common/base/base.component'; import { interval, Subject, Subscription } from 'rxjs'; import { distinctUntilChanged } from 'rxjs/operators'; import { type } from 'os'; import { SimpleChanges } from '@angular/core'; import { Foo, ToCEntry, ToCEntryType } from './table-of-contents-entry'; import { DragulaService } from 'ng2-dragula'; import { FormArray } from '@angular/forms'; import { MatSnackBar, MatSnackBarConfig } from '@angular/material'; 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: 'dataset-profile-table-of-contents', styleUrls: ['./table-of-contents.scss'], templateUrl: './table-of-contents.html' }) export class DatasetProfileTableOfContents extends BaseComponent implements OnInit { @Input() links: ToCEntry[]; container: string; headerSelectors = '.toc-page-header, .toc-section-header, .toc-compositeField-header'; @Output() stepFound = new EventEmitter(); @Output() currentLinks = new EventEmitter(); @Output() itemClick = new EventEmitter(); @Output() newEntry = new EventEmitter(); @Output() removeEntry = new EventEmitter(); @Output() createEntry = new EventEmitter(); @Output() dataNeedsRefresh = new EventEmitter(); @Input() itemSelected: ToCEntry; // @Input() set itemSelected(entry:ToCEntry){ // this._itemSelected = entry; // }; // get itemSelected():ToCEntry{ // return this._itemSelected; // } // private _itemSelected:ToCEntry; @Input() viewOnly: boolean; subscription: Subscription; linksSubject: Subject = new Subject(); @Input() isActive: boolean; show: boolean = false; isDragging: boolean = false; draggingItemId: string = null; tocEntryType = ToCEntryType; DRAGULA_ITEM_ID_PREFIX="table_item_id_"; ROOT_ID: string = "ROOT_ID";//no special meaning constructor( @Inject(DOCUMENT) private _document: Document, private dragulaService: DragulaService, private snackbar: MatSnackBar ) { super(); if(this.dragulaService.find('TABLEDRAG')){ this.dragulaService.destroy('TABLEDRAG'); } const dragula = this.dragulaService.createGroup('TABLEDRAG', {}); const drake = dragula.drake; drake.on('drop', (el, target, source,sibling)=>{ const elementId = (el.id as string).replace(this.DRAGULA_ITEM_ID_PREFIX,''); const targetId = target.id as string; const sourceId = source.id as string; if(!(elementId && targetId && sourceId)){ console.error('Elements not have an id'); this.dataNeedsRefresh.emit(); return; } const element:ToCEntry = this._findTocEntryById(elementId, this.links); const targetContainer:ToCEntry = this._findTocEntryById(targetId , this.links); const sourceContainer:ToCEntry = this._findTocEntryById(sourceId, this.links); if(!(element && (targetContainer ||((element.type===ToCEntryType.Page) && (targetId === this.ROOT_ID))) && (sourceContainer||((element.type===ToCEntryType.Page) && (sourceId === this.ROOT_ID))))){ console.info('Could not find elements'); this.dataNeedsRefresh.emit(); drake.cancel(true); return; } switch(element.type){ case ToCEntryType.FieldSet: if(targetContainer.type != this.tocEntryType.Section){ const message = 'Fieldset can only be child of Subsections'; console.error(message); this.notifyUser(message) this.dataNeedsRefresh.emit(); return; } //check if target container has no sections if((targetContainer.form.get('sections') as FormArray).length){ const message = 'Cannot have inputs and sections on the same level'; this.notifyUser(message); console.error(message); this.dataNeedsRefresh.emit(); return; } const fieldsetForm = element.form; const targetFieldsets = targetContainer.form.get('fieldSets') as FormArray; const sourceFieldsets = sourceContainer.form.get('fieldSets') as FormArray; if(!targetFieldsets){ console.error('Not target fieldsets container found'); this.dataNeedsRefresh.emit(); return; } let sourceOrdinal=-1; let idx = -1; sourceFieldsets.controls.forEach((elem,index)=>{ if(elem.get('id').value === elementId){ sourceOrdinal = elem.get('ordinal').value; idx = index } }); if(sourceOrdinal>=0 && idx>=0){ sourceFieldsets.removeAt(idx); sourceFieldsets.controls.forEach(control=>{ const ordinal = control.get('ordinal'); if(ordinal.value>= sourceOrdinal){ const updatedOrdinalVal = ordinal.value -1; ordinal.setValue(updatedOrdinalVal); } }); } let position:number = targetFieldsets.length; if(!sibling ||!sibling.id){ console.info('No sibling Id found'); }else{ const siblingId = (sibling.id as string).replace(this.DRAGULA_ITEM_ID_PREFIX,''); let siblingIndex = -1; targetFieldsets.controls.forEach((e,idx)=>{ if(e.get('id').value === siblingId){ siblingIndex = idx; position = e.get('ordinal').value; } }); if(siblingIndex>=0 && (position!=targetFieldsets.length)){ targetFieldsets.controls.filter(control=> control.get('ordinal').value >= position).forEach(control=>{ const ordinal = control.get('ordinal'); const updatedOrdinalVal = ordinal.value +1; ordinal.setValue(updatedOrdinalVal); }) } } fieldsetForm.get('ordinal').setValue(position); targetFieldsets.insert(position,fieldsetForm); this.dataNeedsRefresh.emit(); break; case ToCEntryType.Section: if(targetContainer.type == ToCEntryType.Section){ if((targetContainer.form.get('fieldSets')as FormArray).length){ const message = 'Cannot have inputs and sections on the same level'; this.notifyUser(message); console.info(message); this.dataNeedsRefresh.emit(); return; } const targetSections = targetContainer.form.get('sections') as FormArray; const elementSectionForm = element.form; const sourceSections = elementSectionForm.parent as FormArray; if(!(targetSections && sourceSections && elementSectionForm)){ console.info('Could not load sections'); this.dataNeedsRefresh.emit(); return; } let idx = -1; sourceSections.controls.forEach((section,i)=>{ if(section.get('id').value === elementId){ idx = i; } }); if(!(idx>=0)){ console.info('Could not find element in Parent container'); this.dataNeedsRefresh.emit(); return; } sourceSections.controls.filter(control=>control.get('ordinal').value >= elementSectionForm.get('ordinal').value).forEach(control=>{ const ordinal = control.get('ordinal'); const updatedOrdinalVal = ordinal.value -1; ordinal.setValue(updatedOrdinalVal); }); sourceSections.removeAt(idx); let targetOrdinal = targetSections.length; if(sibling && sibling.id){ const siblingId = sibling.id.replace(this.DRAGULA_ITEM_ID_PREFIX,''); targetSections.controls.forEach((section,i)=>{ if(section.get('id').value === siblingId){ targetOrdinal = section.get('ordinal').value; } }) if(targetOrdinal!=targetSections.length){ // section.get('ordinal').setValue(i+1); targetSections.controls.filter(control=> control.get('ordinal').value>=targetOrdinal).forEach(control=>{ const ordinal = control.get('ordinal'); const updatedOrdinalVal = ordinal.value+1; ordinal.setValue(updatedOrdinalVal); }); } }else{ console.info('no siblings found'); } elementSectionForm.get('ordinal').setValue(targetOrdinal); targetSections.insert(targetOrdinal, elementSectionForm); }else if(targetContainer.type === ToCEntryType.Page){ const pageId = targetContainer.form.get('id').value; const rootform = targetContainer.form.root; const sectionForm = element.form; const parentSections = sectionForm.parent as FormArray; let parentIndex = -1; parentSections.controls.forEach((section,i )=>{ if(section.get('id').value === elementId){ parentIndex = i } }) if(parentIndex<0){ console.info('could not locate section in parents array'); this.dataNeedsRefresh.emit(); return; } //update parent sections ordinal parentSections.controls.filter(section=>section.get('ordinal').value >= sectionForm.get('ordinal').value).forEach(section=>{ const ordinal = section.get('ordinal'); const updatedOrdinalVal = ordinal.value -1; ordinal.setValue(updatedOrdinalVal); }) parentSections.removeAt(parentIndex); let position = 0; if(targetContainer.subEntries){ position = targetContainer.subEntries.length; } //populate sections const targetSectionsArray = rootform.get('sections') as FormArray; if(sibling && sibling.id){ const siblingId= sibling.id.replace(this.DRAGULA_ITEM_ID_PREFIX, ''); let indx = -1; targetContainer.subEntries.forEach((e,i)=>{//TOOD TO CHECK IF ORDINAL AND INDEX IS THE SAME if(e.form.get('id').value === siblingId){ indx = i; position = e.form.get('ordinal').value; } }); if(indx>=0 && position !=targetContainer.subEntries.length) { // e.form.get('ordinal').setValue(i+1); targetContainer.subEntries.filter(e=>e.form.get('ordinal').value >= position).forEach(e=>{ const ordinal = e.form.get('ordinal'); const updatedOrdinalVal = ordinal.value +1; ordinal.setValue(updatedOrdinalVal); }); } }else{ console.info('No sibling found'); } sectionForm.get('ordinal').setValue(position); sectionForm.get('page').setValue(targetContainer.id); targetSectionsArray.push(sectionForm); }else{ const message = 'Drag not support to specific container'; this.notifyUser(message); console.info(message); this.dataNeedsRefresh.emit(); return; } this.dataNeedsRefresh.emit(); break; case ToCEntryType.Page: if(targetId != this.ROOT_ID){ const message = 'A page element can only be at top level'; this.notifyUser(message); console.info(message); this.dataNeedsRefresh.emit(); return; } const rootForm = element.form.root; if(!rootForm){ console.info('Could not find root!') this.dataNeedsRefresh.emit(); return; } const pages = rootForm.get('pages') as FormArray; const pageForm = element.form; let index = -1; pages.controls.forEach((page,i)=>{ if(page.get('id').value === elementId){ index =i; } }); if(index<0){ console.info('Could not locate page on pages'); this.dataNeedsRefresh.emit(); return; } //ordinality pages.controls.filter(page=> page.get('ordinal').value> pageForm.get('ordinal').value).forEach(page=>{ const ordinal = page.get('ordinal'); const ordinalVal = ordinal.value - 1; ordinal.setValue(ordinalVal); }); pages.removeAt(index); let targetPosition = pages.length; if(sibling){ const siblingId = sibling.id.replace(this.DRAGULA_ITEM_ID_PREFIX, ''); pages.controls.forEach((page,i)=>{ if(page.get('id').value === siblingId){ targetPosition = page.get('ordinal').value; } }); } pageForm.get('ordinal').setValue(targetPosition); //update ordinality pages.controls.filter(page=> page.get('ordinal').value>= targetPosition).forEach(page=>{ const ordinal = page.get('ordinal'); const ordinalVal = ordinal.value +1; ordinal.setValue(ordinalVal); }); pages.insert(targetPosition, pageForm); this.dataNeedsRefresh.emit(); break; default: console.error('Could not support moving objects for specific type of element'); this.dataNeedsRefresh.emit(); return; } }); drake.on('drag',(el,source)=>{ this.isDragging = true; this.draggingItemId = (el.id as string).replace(this.DRAGULA_ITEM_ID_PREFIX, ''); }); drake.on('over',(el, container, source)=>{ try { this.overcontainer = container.id; } catch (error) { this.overcontainer = null; } }); drake.on('dragend',(el)=>{ const draggingItem = this.draggingItemId; this.isDragging = false; this.draggingItemId = null; this.overcontainer = null; const entry = this._findTocEntryById(draggingItem, this.links); if(entry){ this.itemClicked(entry); } }); } overcontainer: string = null; private _findTocEntryById(id: string, tocentries: ToCEntry[]): ToCEntry{ if(!tocentries){ 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; } ngOnInit(): void { //emit value every 500ms // const source = interval(500); // this.subscription = source.subscribe(val => { // const headers = Array.from(this._document.querySelectorAll(this.headerSelectors)) as HTMLElement[]; // this.linksSubject.next(headers); // }); // if (!this.links || this.links.length === 0) { // this.linksSubject.asObservable() // .pipe(distinctUntilChanged((p: HTMLElement[], q: HTMLElement[]) => JSON.stringify(p) == JSON.stringify(q))) // .subscribe(headers => { // const links: Array = []; // if (headers.length) { // let page; // let section; // let show // for (const header of headers) { // let name; // let id; // if (header.classList.contains('toc-page-header')) { // deprecated after removing stepper // name = header.innerText.trim().replace(/^link/, ''); // id = header.id; // page = header.id.split('_')[1]; // section = undefined; // show = true; // } else if (header.classList.contains('toc-section-header')) { // name = header.childNodes[0].childNodes[0].childNodes[0].childNodes[0].childNodes[0].nodeValue.trim().replace(/^link/, ''); // id = header.id; // page = header.id.split('.')[1]; // section = header.id; // if (header.id.split('.')[4]) { show = false; } // else { show = true; } // } else if (header.classList.contains('toc-compositeField-header')) { // name = (header.childNodes[0]).nodeValue.trim().replace(/^link/, ''); // id = header.id; // // id = header.parentElement.parentElement.parentElement.id; // show = false; // } // const { top } = header.getBoundingClientRect(); // links.push({ // name, // id, // type: header.tagName.toLowerCase(), // top: top, // active: false, // page: page, // section: section, // show: show, // selected: false // }); // } // } // this.links = links; // // Initialize selected for button next on dataset wizard component editor // this.links.length > 0 ? this.links[0].selected = true : null; // }) // } } ngOnChanges(changes: SimpleChanges) { // if (!this.isActive && this.links && this.links.length > 0) { // this.links.forEach(link => { // link.selected = false; // }) // this.links[0].selected = true; // } } 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]; // } itemClicked(item: ToCEntry){ //leaf node this.itemClick.emit(item); } // propagateClickToParent(childIds:ToCEntry[], currentItem: ToCEntry){ // childIds.push(currentItem); // this.itemClick.emit(childIds); // } addNewEntry(tce: ToCEntry){ this.newEntry.emit(tce); } deleteEntry(currentLink: ToCEntry){ this.removeEntry.emit(currentLink); } createNewEntry(foo: Foo){ this.createEntry.emit(foo); } onDataNeedsRefresh(){ this.dataNeedsRefresh.emit(); } notifyUser(message:string){ this.snackbar.open(message, null, this._snackBarConfig); } private _snackBarConfig: MatSnackBarConfig = { duration:2000 } } export interface LinkToScroll { page: number; section: number; }