nm-edit-tree
    src/app/shared/widgets/select-tree/select-tree.component.ts
| selector | nm-edit-tree | 
            
| styleUrls | select-tree.component.scss | 
            
| templateUrl | ./select-tree.component.html | 
            
constructor(_widgetframeService: WidgetframeService, categoryService: ShopCategoryService, dialog: MatDialog, translateService: TranslateService, localStorageService: LocalStorageService, appdataStore: AppdataStore, cdr: ChangeDetectorRef, overlay: Overlay, viewContainerRef: ViewContainerRef)
                     | 
                ||||||||||||||||||||||||||||||
| 
                             
                                    Parameters :
                                     
                    
  | 
                
| Private buildTree | ||||||
                            
                        buildTree(paths: string[][])
                     | 
                ||||||
| 
                             
                                    Parameters :
                                     
                            
 
                                Returns :      
                                Node[]
                             | 
                
| clearSelection | 
clearSelection()
                     | 
                
| 
                             
                                Returns :      
                    void
                             | 
                
| Public closeTreeOverlay | 
                            
                        closeTreeOverlay()
                     | 
                
| 
                             
                                Returns :      
                    void
                             | 
                
| Public configureWidget | ||||||
                            
                        configureWidget(configuration: WidgetConfig)
                     | 
                ||||||
                            Decorators : WidgetConfigure
                         | 
                    ||||||
| 
                             
                                    Parameters :
                                     
                            
 
                                Returns :      
                                void
                             | 
                
| Private doReload | ||||
                            
                        doReload(withReset: )
                     | 
                ||||
| 
                             
                                    Parameters :
                                     
                            
 
                                Returns :      
                                void
                             | 
                
| Private emitSelection | 
                            
                        emitSelection()
                     | 
                
| 
                             
                                Returns :      
                    void
                             | 
                
| Private extractData | ||||
                            
                        extractData(response: )
                     | 
                ||||
| 
                             
                                    Parameters :
                                     
                            
 
                                Returns :      
                                any[]
                             | 
                
| forceLoad | 
forceLoad(root: any, tree: Node[])
                     | 
                
| 
                            
                             
                                Returns :      
                                Observable<any[]>
                             | 
                
| Private forceLoadChildren | 
                            
                        forceLoadChildren(node: any, tree: Node[])
                     | 
                
| 
                            
                             
                                Returns :      
                                Observable<any[]>
                             | 
                
| Private forceSelect | ||||||
                            
                        forceSelect(categoryValue: string)
                     | 
                ||||||
| 
                             
                                    Parameters :
                                     
                            
 
                                Returns :      
                                void
                             | 
                
| initCategoryFromInput | ||||||
initCategoryFromInput(categoryValues: literal type[])
                     | 
                ||||||
| 
                             
                                    Parameters :
                                     
                            
 
                                Returns :      
                                void
                             | 
                
| keyDownFunction | ||||
keyDownFunction(event: )
                     | 
                ||||
| 
                             
                                    Parameters :
                                     
                            
 
                                Returns :      
                                void
                             | 
                
| loadChildren | ||||
loadChildren(link: )
                     | 
                ||||
| 
                             
                                    Parameters :
                                     
                            
 
                                Returns :      
                                Observable<any>
                             | 
                
| Private loadRootCategories | 
                            
                        loadRootCategories(callback?: (nodes?: any[]) => void, tree: Node[])
                     | 
                
| 
                            
                             
                                Returns :      
                                void
                             | 
                
| ngAfterViewInit | 
ngAfterViewInit()
                     | 
                
| 
                             
                                Returns :      
                    void
                             | 
                
| onChange | 
onChange()
                     | 
                
| 
                             
                                Returns :      
                    void
                             | 
                
| onChipClick | ||||
onChipClick(event: )
                     | 
                ||||
| 
                             
                                    Parameters :
                                     
                            
 
                                Returns :      
                                boolean
                             | 
                
| onClickOnOverlayBackdrop | ||||
onClickOnOverlayBackdrop(event: )
                     | 
                ||||
| 
                             
                                    Parameters :
                                     
                            
 
                                Returns :      
                                void
                             | 
                
| Private onSelectionChange | 
                            
                        onSelectionChange()
                     | 
                
| 
                             
                                Returns :      
                    void
                             | 
                
| onSelectionEvent | ||||||
onSelectionEvent(event: SelectionEvent)
                     | 
                ||||||
| 
                             
                                    Parameters :
                                     
                            
 
                                Returns :      
                                void
                             | 
                
| removeFromSelection | ||||
removeFromSelection(event: )
                     | 
                ||||
| 
                             
                                    Parameters :
                                     
                            
 
                                Returns :      
                                boolean
                             | 
                
| Private setSelection | ||||||
                            
                        setSelection(input: string[] | string[][])
                     | 
                ||||||
| 
                             
                                    Parameters :
                                     
                            
 
                                Returns :      
                                void
                             | 
                
| toggleTree | 
toggleTree()
                     | 
                
| 
                             
                                Returns :      
                    void
                             | 
                
| transformData | ||||||||||||
transformData(data: any[], parentIdentifier: any)
                     | 
                ||||||||||||
| 
                             
                                    Parameters :
                                     
                            
 
                                Returns :      
                                any[]
                             | 
                
| Private updateCategoryOutput | ||||||
                            
                        updateCategoryOutput(event: SelectionEvent)
                     | 
                ||||||
| 
                             
                                    Parameters :
                                     
                            
 
                                Returns :      
                                void
                             | 
                
| Public _id | 
                        _id:     
                     | 
                
                            Type :     string
                         | 
                    
                            Decorators : WidgetId
                         | 
                    
| Public appdata | 
                        appdata:     
                     | 
                
                            Type :     any
                         | 
                    
| Private clearChannel | 
                        clearChannel:     
                     | 
                
                            Type :     Subject<any>
                         | 
                    
                            Default value : new Subject<any>()
                         | 
                    
                            Decorators : WidgetInput
                         | 
                    
| Private clearTextSearch | 
                        clearTextSearch:     
                     | 
                
                            Type :     Subject<any>
                         | 
                    
                            Decorators : WidgetOutput
                         | 
                    
| Public configuration | 
                        configuration:     
                     | 
                
                            Type :     WidgetConfig
                         | 
                    
                            Decorators : WidgetConfiguration
                         | 
                    
| Public dataType | 
                        dataType:     
                     | 
                
                            Type :     string
                         | 
                    
| Static DEFAULT_OUTPUT_TYPE | 
                        DEFAULT_OUTPUT_TYPE:     
                     | 
                
                            Type :     string
                         | 
                    
                            Default value : "uri"
                         | 
                    
| Public descriptionField | 
                        descriptionField:     
                     | 
                
| Public dialog | 
                        dialog:     
                     | 
                
                            Type :     MatDialog
                         | 
                    
| Private disabledInput | 
                        disabledInput:     
                     | 
                
                            Default value : new Subject<boolean>()
                         | 
                    
                            Decorators : WidgetInput
                         | 
                    
| Public header | 
                        header:     
                     | 
                
                            Type :     string
                         | 
                    
| Public identifierField | 
                        identifierField:     
                     | 
                
| Public infotext | 
                        infotext:     
                     | 
                
                            Type :     string
                         | 
                    
| Public inputLink | 
                        inputLink:     
                     | 
                
                            Type :     string
                         | 
                    
| Public isOpen | 
                        isOpen:     
                     | 
                
                            Default value : false
                         | 
                    
| Private lastFocused | 
                        lastFocused:     
                     | 
                
| Public localStorageDescriptionEntry | 
                        localStorageDescriptionEntry:     
                     | 
                
                            Type :     LocalStorageEntry
                         | 
                    
| Public localstorageSelectedCategoryEntry | 
                        localstorageSelectedCategoryEntry:     
                     | 
                
                            Type :     LocalStorageEntry
                         | 
                    
| Public nodes | 
                        nodes:     
                     | 
                
                            Type :     any[]
                         | 
                    
| Public options | 
                        options:     
                     | 
                
                            Type :     any[]
                         | 
                    
                            Default value : []
                         | 
                    
| Public outputType | 
                        outputType:     
                     | 
                
                            Type :     string
                         | 
                    
| Private outputUri | 
                        outputUri:     
                     | 
                
                            Default value : new Subject<any>()
                         | 
                    
                            Decorators : WidgetOutput
                         | 
                    
| Private overlayRef | 
                        overlayRef:     
                     | 
                
                            Type :     OverlayRef
                         | 
                    
| Public payload | 
                        payload:     
                     | 
                
                            Default value : new Subject<any>()
                         | 
                    
                            Decorators : WidgetInput
                         | 
                    
| Private pendingRequests | 
                        pendingRequests:     
                     | 
                
                            Type :     number
                         | 
                    
                            Default value : 0
                         | 
                    
| Public placeholder | 
                        placeholder:     
                     | 
                
                            Type :     string
                         | 
                    
| Private position | 
                        position:     
                     | 
                
                            Type :     FlexibleConnectedPositionStrategy
                         | 
                    
| Private profile | 
                        profile:     
                     | 
                
                            Default value : new Subject<any>()
                         | 
                    
                            Decorators : WidgetOutput
                         | 
                    
| Private programaticExpansion | 
                        programaticExpansion:     
                     | 
                
                            Type :     boolean
                         | 
                    
                            Default value : false
                         | 
                    
| Public reloadChannel | 
                        reloadChannel:     
                     | 
                
                            Default value : new Subject<any>()
                         | 
                    
                            Decorators : WidgetInput
                         | 
                    
| Public resetChannel | 
                        resetChannel:     
                     | 
                
                            Type :     Subject<any>
                         | 
                    
                            Default value : new Subject<any>()
                         | 
                    
                            Decorators : WidgetInput
                         | 
                    
| Public rootCategoryLink | 
                        rootCategoryLink:     
                     | 
                
                            Type :     string
                         | 
                    
| Public rootSearchCategoryLink | 
                        rootSearchCategoryLink:     
                     | 
                
                            Type :     string
                         | 
                    
| Private rootUriChannel | 
                        rootUriChannel:     
                     | 
                
                            Default value : new ReplaySubject<any>(1)
                         | 
                    
                            Decorators : WidgetInput
                         | 
                    
| Public selectedValue | 
                        selectedValue:     
                     | 
                
                            Default value : new ReplaySubject<any>(1)
                         | 
                    
                            Decorators : WidgetOutput
                         | 
                    
| Public selectedValueDescription | 
                        selectedValueDescription:     
                     | 
                
                            Default value : new ReplaySubject<any>(1)
                         | 
                    
                            Decorators : WidgetOutput
                         | 
                    
| Public selectedValueRootNode | 
                        selectedValueRootNode:     
                     | 
                
                            Default value : new Subject<any>()
                         | 
                    
                            Decorators : WidgetOutput
                         | 
                    
| Public selection | 
                        selection:     
                     | 
                
                            Type :     any[]
                         | 
                    
| Public selectionEmitter | 
                        selectionEmitter:     
                     | 
                
                            Default value : new Subject<Selection[]>()
                         | 
                    
                            Decorators : WidgetOutput
                         | 
                    
| Public selectionIdentifier | 
                        selectionIdentifier:     
                     | 
                
                            Default value : new ReplaySubject<string[]>(1)
                         | 
                    
                            Decorators : WidgetInput
                         | 
                    
| Public selectionPath | 
                        selectionPath:     
                     | 
                
                            Default value : new ReplaySubject<string[] | string[][]>(1)
                         | 
                    
                            Decorators : WidgetInput
                         | 
                    
| 
                         Input to select given node, based on its path  | 
                
| Public selectionRootNodes | 
                        selectionRootNodes:     
                     | 
                
                            Type :     any[]
                         | 
                    
| Public selectionString | 
                        selectionString:     
                     | 
                
| Public shouldLabelFloat | 
                        shouldLabelFloat:     
                     | 
                
                            Type :     boolean
                         | 
                    
                            Default value : false
                         | 
                    
| Private template | 
                        template:     
                     | 
                
                            Type :     TemplateRef<any>
                         | 
                    
                            Decorators : ViewChild
                         | 
                    
| Private templatePortal | 
                        templatePortal:     
                     | 
                
                            Type :     TemplatePortal<any>
                         | 
                    
| Public title | 
                        title:     
                     | 
                
                            Type :     string
                         | 
                    
| Public translateService | 
                        translateService:     
                     | 
                
                            Type :     TranslateService
                         | 
                    
| treeinput | 
                        treeinput:     
                     | 
                
                            Decorators : ViewChild
                         | 
                    
| treeSelector | 
                        treeSelector:     
                     | 
                
                            Type :     TreeSelectorComponent
                         | 
                    
                            Decorators : ViewChild
                         | 
                    
| Public triggerAsChipList | 
                        triggerAsChipList:     
                     | 
                
                            Type :     boolean
                         | 
                    
| triggerRef | 
                        triggerRef:     
                     | 
                
                            Type :     ElementRef
                         | 
                    
                            Decorators : ViewChild
                         | 
                    
| Private triggerSearch | 
                        triggerSearch:     
                     | 
                
                            Type :     Subject<any>
                         | 
                    
                            Default value : new Subject<any>()
                         | 
                    
                            Decorators : WidgetOutput
                         | 
                    
| Private unsubscribe | 
                        unsubscribe:     
                     | 
                
                            Default value : NgUnsubscribe.create()
                         | 
                    
| Public wikiLink | 
                        wikiLink:     
                     | 
                
                            Type :     string
                         | 
                    
| Public withHeader | 
                        withHeader:     
                     | 
                
                            Type :     boolean
                         | 
                    
| initialSelection | 
                        getinitialSelection()
                     | 
                
import {
  concatAll,
  concatMap,
  flatMap,
  last,
  mergeMap,
  scan,
  switchMap,
  takeUntil,
} from "rxjs/operators";
import {
  AfterViewInit,
  ChangeDetectorRef,
  Component,
  ElementRef,
  EventEmitter,
  HostListener,
  Input,
  Output,
  TemplateRef,
  ViewChild,
  ViewContainerRef,
} from "@angular/core";
import { from, Observable, of, ReplaySubject, Subject } from "rxjs";
import { WidgetframeService } from "../widgetframe/widgetframe.service";
import { getOrDefault, WidgetConfig } from "../widget.configuration";
import {
  WidgetComponent,
  WidgetConfiguration,
  WidgetConfigure,
  WidgetId,
  WidgetInput,
  WidgetOutput,
} from "../widget.metadata";
import { NgUnsubscribe } from "../../ng-unsubscribe";
import { MatDialog } from "@angular/material/dialog";
import { TranslateService } from "@ngx-translate/core";
import { ShopCategory } from "../apps/my-shop-md/shop-category/shop-category";
import { ShopCategoryService } from "../apps/my-shop-md/shop-category/shop-category.service";
import { AppdataStore } from "../../components/appdata/appdata.store";
import {
  LocalStorageEntry,
  LocalStorageService,
} from "../../components/local-storage/local-storage.service";
import {
  DeletionMode,
  Scope,
} from "../../components/local-storage/local-storage-constants";
import { TreeSelectorComponent } from "../../components/tree-selector/tree-selector.component";
import {
  ConnectionPositionPair,
  FlexibleConnectedPositionStrategy,
  Overlay,
  OverlayRef,
} from "@angular/cdk/overlay";
import { SelectionEvent } from "../../components/tree-selector/tree-selector.api";
import * as uriTemplates_ from "uri-templates";
import { TemplatePortal } from "@angular/cdk/portal";
const uriTemplates = uriTemplates_;
const POSITIONS: { [key: string]: ConnectionPositionPair } = {
  "combo-bottom": {
    originX: "center",
    originY: "top",
    overlayX: "center",
    overlayY: "top",
    offsetX: 40,
  },
  "combo-top": {
    originX: "center",
    originY: "bottom",
    overlayX: "center",
    overlayY: "bottom",
    offsetX: 40,
  },
  bottom: {
    originX: "center",
    originY: "bottom",
    overlayX: "center",
    overlayY: "top",
  },
  top: {
    originX: "center",
    originY: "top",
    overlayX: "center",
    overlayY: "bottom",
  },
  left: {
    originX: "start",
    originY: "center",
    overlayX: "end",
    overlayY: "center",
  },
  right: {
    originX: "end",
    originY: "center",
    overlayX: "start",
    overlayY: "center",
  },
  "left-below": {
    originX: "start",
    originY: "bottom",
    overlayX: "end",
    overlayY: "top",
  },
  "right-below": {
    originX: "end",
    originY: "top",
    overlayX: "start",
    overlayY: "top",
  },
  "right-above": {
    originX: "end",
    originY: "bottom",
    overlayX: "start",
    overlayY: "bottom",
  },
  "left-above": {
    originX: "start",
    originY: "bottom",
    overlayX: "end",
    overlayY: "bottom",
  },
};
const DEFAULT_POSITIONS = ["right", "left-below", "right-below", "right-above"];
interface Selection {
  root: string;
  category: string;
  description: string;
  path: string[];
}
interface Node {
  identifier: string;
  children: Node[];
}
@WidgetComponent("nm-select-tree")
@Component({
  selector: "nm-edit-tree",
  templateUrl: "./select-tree.component.html",
  styleUrls: ["./select-tree.component.scss"],
})
export class SelectTreeWidgetComponent implements AfterViewInit {
  @WidgetConfiguration()
  public configuration: WidgetConfig;
  @WidgetOutput()
  public selectedValue = new ReplaySubject<any>(1);
  @WidgetOutput()
  public selectedValueDescription = new ReplaySubject<any>(1);
  @WidgetOutput()
  public selectedValueRootNode = new Subject<any>();
  @WidgetOutput()
  public selectionEmitter = new Subject<Selection[]>();
  @WidgetInput()
  public payload = new Subject<any>();
  @WidgetInput()
  public selectionIdentifier = new ReplaySubject<string[]>(1);
  /**
   * Input to select given node, based on its path
   */
  @WidgetInput()
  public selectionPath = new ReplaySubject<string[] | string[][]>(1);
  @WidgetInput("disabled")
  private disabledInput = new Subject<boolean>();
  @Input()
  public disabled: boolean = false;
  @Input()
  public filterable: boolean = true;
  @Output()
  public emitCategory = new EventEmitter<ShopCategory>();
  @Input()
  public preferredPosition = DEFAULT_POSITIONS;
  @WidgetOutput("profile")
  private profile = new Subject<any>();
  @WidgetInput("reload")
  public reloadChannel = new Subject<any>();
  @WidgetInput("reset")
  public resetChannel: Subject<any> = new Subject<any>();
  @WidgetInput("clearcategorysearch")
  private clearChannel: Subject<any> = new Subject<any>();
  @WidgetInput("rootUri")
  private rootUriChannel = new ReplaySubject<any>(1);
  @WidgetOutput("cleartextsearch")
  private clearTextSearch: Subject<any>;
  @WidgetOutput("triggerSearch")
  private triggerSearch: Subject<any> = new Subject<any>();
  @WidgetOutput("uri")
  private outputUri = new Subject<any>();
  @WidgetId()
  public _id: string;
  @ViewChild("menuTrigger")
  triggerRef: ElementRef;
  @ViewChild("treeinput")
  treeinput;
  @ViewChild("treeSelector")
  treeSelector: TreeSelectorComponent;
  public header: string;
  public withHeader: boolean;
  // The current amount of requests that are running to fetch children
  private pendingRequests = 0;
  public selection: any[];
  public selectionString;
  public placeholder: string;
  public shouldLabelFloat: boolean = false;
  public identifierField;
  public descriptionField;
  public options: any[] = [];
  private programaticExpansion: boolean = false;
  public inputLink: string;
  public rootCategoryLink: string;
  public rootSearchCategoryLink: string;
  public dataType: string;
  public localStorageDescriptionEntry: LocalStorageEntry;
  public localstorageSelectedCategoryEntry: LocalStorageEntry;
  public nodes: any[];
  public appdata: any;
  public outputType: string;
  public triggerAsChipList: boolean;
  public wikiLink: string;
  public infotext: string;
  public title: string;
  public static DEFAULT_OUTPUT_TYPE: string = "uri";
  private unsubscribe = NgUnsubscribe.create();
  public isOpen = false;
  @ViewChild("treeOverlay", { static: false })
  private template: TemplateRef<any>;
  private position: FlexibleConnectedPositionStrategy;
  private templatePortal: TemplatePortal<any>;
  private overlayRef: OverlayRef;
  public selectionRootNodes: any[];
  private lastFocused;
  constructor(
    private _widgetframeService: WidgetframeService,
    private categoryService: ShopCategoryService,
    public dialog: MatDialog,
    public translateService: TranslateService,
    private localStorageService: LocalStorageService,
    private appdataStore: AppdataStore,
    private cdr: ChangeDetectorRef,
    private overlay: Overlay,
    private viewContainerRef: ViewContainerRef
  ) {}
  keyDownFunction(event) {
    if (event.keyCode == 13 && !!this.selection) {
      this.triggerSearch.next(new Date());
    }
  }
  @HostListener("keydown", ["$event"])
  onKeyDown(event: KeyboardEvent) {
    const key = event.key.toLowerCase();
    if (!this.isOpen) {
      switch (key) {
        case "space":
        case "spacebar":
        case " ":
        case "enter":
          //case "down":
          //case "arrowdown":
          this.toggleTree();
      }
    }
    event.stopPropagation();
  }
  onChange() {
    // this.localStorageDescriptionEntry.value =  this.selection
    // this.selectedValue.next(this.selection);
  }
  ngAfterViewInit() {
    let preferredPosition = this.preferredPosition;
    if (this.preferredPosition == null) {
      preferredPosition = DEFAULT_POSITIONS;
    } else if (typeof this.preferredPosition === "string") {
      preferredPosition = [this.preferredPosition];
    }
    let positions = preferredPosition
      .map((key) => POSITIONS[key])
      .filter((pos) => !!pos);
    this.position = this.overlay
      .position()
      .flexibleConnectedTo(this.triggerRef)
      .withPositions(positions)
      .withPush(false)
      .withFlexibleDimensions(true);
    this.templatePortal = new TemplatePortal(
      this.template,
      this.viewContainerRef
    );
    this.overlayRef = this.overlay.create({
      width: "400px",
      height: "550px",
      panelClass: "tree-popup",
      positionStrategy: this.position,
      disposeOnNavigation: true,
      hasBackdrop: true,
      backdropClass: "cdk-overlay-transparent-backdrop",
    });
  }
  toggleTree() {
    this.lastFocused = document.activeElement;
    this.isOpen = !this.isOpen && this.nodes && this.nodes.length > 0;
    if (this.isOpen) {
      this.overlayRef.attach(this.templatePortal);
      this.overlayRef
        .backdropClick()
        .pipe(takeUntil(this.unsubscribe))
        .subscribe(() => {
          this.closeTreeOverlay();
        });
    } else {
      this.overlayRef.detach();
    }
    this.cdr.markForCheck();
  }
  get initialSelection(): any[] {
    return this.selection
      ? this.selection.map((selection) => selection.id)
      : [];
  }
  // using property syntax, to bind `this` correctly
  loadChildNodes = (parentNode: any, done: (children: any[]) => void) => {
    if (
      parentNode._links &&
      parentNode._links.children &&
      parentNode._links.children.href
    ) {
      this.categoryService
        .getCategories(parentNode._links.children.href)
        .pipe(takeUntil(this.unsubscribe))
        .subscribe(
          (data) => {
            let parentIdentifier = parentNode[this.identifierField];
            let children = this.transformData(
              this.extractData(data),
              parentIdentifier
            );
            done(children);
            this.cdr.detectChanges();
          },
          (err) => {
            console.error(err);
            done([]);
          }
        );
    } else {
      done([]);
    }
  };
  forceLoad(root: any, tree: Node[] = ([] = [])): Observable<any[]> {
    let rootNodes = this.transformData(this.extractData(root));
    // iterate all loaded nodes
    // and collapse the resulting arrays
    return from(rootNodes).pipe(
      flatMap((rootNode) => this.forceLoadChildren(rootNode, tree)),
      scan((l, r) => [...l, ...r]),
      last()
    );
  }
  private forceLoadChildren(node: any, tree: Node[]): Observable<any[]> {
    const treeNode = tree.find(
      (tn) => node[this.identifierField] === tn.identifier
    );
    const nodeObs = of([node]);
    // we use the tree representation of one or more given category paths
    // to lazily load the children for each level of the tree
    if (treeNode?.children?.length) {
      // load children of the current node
      const children = this.loadChildren(node._links.children.href).pipe(
        // map the loaded children to node objects and push each node through the resulting stream
        flatMap((data) =>
          from(
            this.transformData(
              this.extractData(data),
              node[this.identifierField]
            )
          )
        ),
        // then, for each loaded & mapped child node, recursively load their child nodes
        // and concatenate the resulting streams
        concatMap((childNode) =>
          this.forceLoadChildren(childNode, treeNode.children)
        )
      );
      return from([nodeObs, children]).pipe(concatAll());
    }
    return nodeObs;
  }
  loadChildren(link): Observable<any> {
    return this.categoryService.getCategories(link);
  }
  clearSelection() {
    this.selection = [];
    this.emitSelection();
    if (this.localStorageDescriptionEntry) {
      this.localStorageDescriptionEntry.clear();
    }
    if (this.localstorageSelectedCategoryEntry) {
      this.localstorageSelectedCategoryEntry.clear();
    }
  }
  onClickOnOverlayBackdrop(event) {
    this.closeTreeOverlay();
  }
  initCategoryFromInput(categoryValues: { id: string; description: string }[]) {
    if (categoryValues && categoryValues.length > 0) {
      this.selection = categoryValues;
      this.selectionString = categoryValues
        .map((value) => value.description)
        .join(" | ");
      if (this.localStorageDescriptionEntry) {
        this.localStorageDescriptionEntry.value = this.selectionString;
      }
      if (this.localstorageSelectedCategoryEntry) {
        this.localstorageSelectedCategoryEntry.value = JSON.stringify(
          this.selection
        );
      }
      this.selectedValueDescription.next(this.selectionString);
    } else if (this.localstorageSelectedCategoryEntry) {
      this.localstorageSelectedCategoryEntry.clear();
    }
    this.cdr.markForCheck();
  }
  onSelectionEvent(event: SelectionEvent) {
    // to reload the last selection when clicking on the input field again
    // we need to set "initial-selection" which immediately triggers a
    if (event.source === "initial") {
      return;
    }
    let nodes = event.newSelection;
    if (nodes.length === 0 && this.localstorageSelectedCategoryEntry) {
      this.localstorageSelectedCategoryEntry.clear();
    }
    let newSelection = nodes.map((node) => ({
      id: node[this.identifierField],
      description: node[this.descriptionField],
      path: this.treeSelector.pathForNode(node[this.identifierField]),
    }));
    this.selection = newSelection;
    this.onSelectionChange();
    if (!this.configuration.configuration.multi) {
      this.closeTreeOverlay();
    }
    if (this.localstorageSelectedCategoryEntry) {
      this.localstorageSelectedCategoryEntry.value = JSON.stringify(
        this.selection
      );
    }
    this.updateCategoryOutput(event);
  }
  public loadChildrenOnFilter = (query: string) => {
    if (this.rootSearchCategoryLink) {
      const uri = uriTemplates(this.rootSearchCategoryLink).fill({ query });
      return this._widgetframeService.getData(uri).pipe(
        switchMap((data) => {
          if (data.paths) {
            const obs = new Subject<any>();
            const tree = this.buildTree(data.paths);
            this.loadRootCategories((nodes) => {
              obs.next();
              obs.complete();
              this.cdr.markForCheck();
            }, tree);
            return obs;
          }
          return of();
        })
      );
    }
    return of();
  };
  private onSelectionChange() {
    this.emitSelection();
    this.cdr.detectChanges();
  }
  private updateCategoryOutput(event: SelectionEvent) {
    if (event.added.length <= 0) {
      return;
    }
    let category = event.added[0];
    this.emitCategory.emit(category);
    if (category._links && category._links[this.dataType] !== undefined) {
      if (this.outputType === SelectTreeWidgetComponent.DEFAULT_OUTPUT_TYPE) {
        this.outputUri.next(category._links[this.dataType].href);
      } else {
        var categories = [];
        for (var item of this.selection) {
          categories.push(item.id);
        }
        this.outputUri.next(categories);
      }
      this.profile.next("categorySearchResult");
    } else {
      this.outputUri.next(null);
    }
  }
  public closeTreeOverlay() {
    this.isOpen = false;
    if (this.overlayRef && this.overlayRef.hasAttached()) {
      this.overlayRef.detach();
    }
    this.cdr.markForCheck();
    this.lastFocused.focus();
  }
  private emitSelection() {
    this.selectionString = this.selection
      .map((entry) => entry.description)
      .join(" | ");
    this.selectedValueDescription.next(this.selectionString);
    if (this.localStorageDescriptionEntry) {
      this.localStorageDescriptionEntry.value = this.selectionString;
    }
    this.cdr.markForCheck();
    if (this.selection.length == 0) {
      this.selectionEmitter.next([]);
      this.selectedValueRootNode.next(null);
      return;
    } else {
      const selection: Selection[] = this.selection.map((sel) => {
        return {
          category: sel.id,
          path: sel.path,
          root: sel.path[0],
          description: sel.description,
        };
      });
      this.selectionEmitter.next(selection);
    }
    if (!this.configuration.configuration.multi) {
      if (this.selection.length === 0) {
        this.selectedValue.next(null);
        return;
      }
      if (this.selection.length > 2) {
        console.warn(
          "Select-tree is not in multi mode but multiple selected nodes where detected"
        );
      }
      let selection = this.selection[0];
      this.selectedValue.next(selection.id);
      this.selectedValueRootNode.next(selection.path[0]);
      return;
    }
    this.selectedValueRootNode.next(
      this.selection.map((entry) => entry.path[0])
    );
    this.selectedValue.next(this.selection.map((entry) => entry.id));
  }
  @WidgetConfigure()
  public configureWidget(configuration: WidgetConfig) {
    this.header = getOrDefault(
      this.configuration.configuration.header,
      "primary"
    );
    this.withHeader = getOrDefault(
      this.configuration.configuration.withHeader,
      false
    );
    this.identifierField = getOrDefault(
      this.configuration.configuration.identifierField,
      "identifier"
    );
    this.disabled = getOrDefault(
      this.configuration.configuration.disabled,
      false
    );
    this.descriptionField = getOrDefault(
      this.configuration.configuration.descriptionField,
      "description"
    );
    this.triggerAsChipList =
      configuration.configuration["triggerAsChipList"] !== undefined
        ? configuration.configuration["triggerAsChipList"]
        : false;
    let title = configuration.configuration["title"];
    this.placeholder = configuration.configuration["placeholder"];
    this.shouldLabelFloat = configuration.configuration["shouldLabelFloat"];
    this.preferredPosition = configuration.configuration["preferredPosition"];
    const descriptionEntryKey =
      configuration.configuration["local-storage-key"];
    if (descriptionEntryKey) {
      this.localStorageDescriptionEntry = this.localStorageService.getLocalStorageEntry(
        descriptionEntryKey,
        Scope.GLOBAL,
        DeletionMode.LOGIN
      );
      if (this.localStorageDescriptionEntry.exists()) {
        this.selectionString = this.localStorageDescriptionEntry.value;
      }
    }
    if (this.configuration.configuration["localstorage-category"]) {
      this.localstorageSelectedCategoryEntry = this.localStorageService.getLocalStorageEntry(
        this.configuration.configuration["localstorage-category"],
        Scope.GLOBAL,
        DeletionMode.LOGIN
      );
    }
    if (
      this.localstorageSelectedCategoryEntry &&
      this.localstorageSelectedCategoryEntry.exists()
    ) {
      const value = this.localstorageSelectedCategoryEntry.value;
      this.selection = JSON.parse(value);
      if (value && value[0] === "[") {
      } else {
        // Support legacy localstorage keys that have saved this value in non array format, they will have the entries saved as path
        this.selection = [value];
      }
      this.programaticExpansion = true;
    }
    this.selectedValue.next(this.selection);
    this.infotext = this.configuration.configuration["infoText"];
    this.wikiLink = this.configuration.configuration["wikiLink"];
    this.title = this.configuration.configuration["title"];
    if (this.configuration._links) {
      this.rootCategoryLink = this.configuration["_links"]["categories"][
        "href"
      ];
    }
    this.dataType = this.configuration.configuration["dataType"];
    this.disabledInput.pipe(takeUntil(this.unsubscribe)).subscribe((data) => {
      this.disabled = data;
    });
    this.selectionIdentifier
      .asObservable()
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((selectionIdentifier) => {
        if (this.treeSelector) {
          this.treeSelector.selectNodes(selectionIdentifier);
        } else {
          this.setSelection(selectionIdentifier);
        }
      });
    this.selectionPath.pipe(takeUntil(this.unsubscribe)).subscribe((input) => {
      if (input?.length) {
        if (this.treeSelector) {
          const paths: string[][] = <string[][]>(
            (Array.isArray(input[0]) ? input : [input])
          );
          for (let path of paths) {
            this.treeSelector.expand(path);
            this.treeSelector.selectNodes([path.length - 1]);
          }
        } else {
          this.setSelection(input);
        }
      } else {
        this.clearSelection();
      }
    });
    this.reloadChannel
      .asObservable()
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((reload) => {
        this.doReload(false);
      });
    this.rootUriChannel.pipe(takeUntil(this.unsubscribe)).subscribe((uri) => {
      this.rootCategoryLink = uri;
      this.loadRootCategories();
    });
    this.clearChannel
      .asObservable()
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((reset) => {
        this.doReload(true);
      });
    this.resetChannel
      .asObservable()
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((reset) => {
        this.clearSelection();
        this.doReload(true);
      });
    // rootcategorylink might be set later if this component is used embedded in category-select-control
    if (this.rootCategoryLink) {
      this.loadRootCategories();
    }
  }
  private setSelection(input: string[] | string[][]) {
    const paths = <string[][]>(Array.isArray(input[0]) ? input : [input]);
    const tree = this.buildTree(paths);
    const path = <string[]>input;
    //If we send a selection before the tree is loaded just set the selection string so the user gets a visual representation of his selection
    if (this.rootCategoryLink) {
      this.loadRootCategories((nodes) => {
        this.selection = paths
          .map((path) => {
            const category = nodes.find(
              (node) => node[this.identifierField] === path[path.length - 1]
            );
            if (category == null) {
              return null;
            }
            return {
              id: category[this.identifierField],
              description: category[this.descriptionField],
              path,
            };
          })
          .filter((selection) => !!selection);
        this.emitSelection();
      }, tree);
    } else {
      this.selectionString = path.join(" | ");
    }
  }
  private forceSelect(categoryValue: string) {
    this.selection = [categoryValue];
  }
  private loadRootCategories(
    callback?: (nodes: any[]) => void,
    tree: Node[] = []
  ) {
    if (this.rootCategoryLink == null) {
      this.nodes = [];
      callback(this.nodes);
      return;
    }
    this.categoryService
      .getRootCategories(this.rootCategoryLink)
      .pipe(mergeMap((root) => this.forceLoad(root, tree)))
      .subscribe(
        (data) => {
          this.nodes = data;
          if (callback) {
            callback(data);
          }
        },
        (err) => console.error(err)
      );
  }
  private doReload(withReset) {
    this.loadRootCategories(
      () => {
        if (withReset) {
          if (this.treeSelector) {
            this.treeSelector.collapseAll();
            this.treeSelector.clearSelection();
          }
        }
      },
      withReset ? [] : this.selection
    );
  }
  removeFromSelection(event) {
    this.selection = this.selection.filter(
      (value) => value.id !== event.owner.id
    );
    this.onSelectionChange();
    return false;
  }
  onChipClick(event) {
    event.stopPropagation();
    event.preventDefault();
    return false;
  }
  private extractData(response): any[] {
    if (response._embedded.categories) {
      return response._embedded.categories;
    }
    if (response.type) {
      return response._embedded[response.type];
    }
    return [];
  }
  transformData(data: any[], parentIdentifier: any = null): any[] {
    for (var item of data) {
      item.label = item[this.descriptionField];
      item.data = item[this.identifierField];
      item["parent-node"] = parentIdentifier;
      item.name = item.description;
      item.expandedIcon = "fa-folder-open";
      item.collapsedIcon = "fa-folder";
      item.allowDrag = false;
      if (item._links && item._links.children != undefined) {
        item.expandedIcon = "fa-folder-open";
        item.collapsedIcon = "fa-folder";
        item.leaf = false;
        item.hasChildren = true;
      } else {
        item.expandedIcon = "fa-folder-open";
        item.collapsedIcon = "fa-folder-o";
        item.leaf = true;
        item.hasChildren = false;
      }
    }
    return data;
  }
  // build a tree model of all given category paths
  // this allows us to lazy load the required paths,
  // but only load duplicate steps once
  // CATEGORY -> Cars -> VW -> X and CATEGORY -> Cars -> Ford -> Y
  // will load CATEGORY and Cars only once
  private buildTree(paths: string[][]): Node[] {
    const tree: Node[] = [];
    function findNode(identifier: string, tree: Node[]): Node | null {
      let found = tree.find((node) => node.identifier === identifier);
      if (found != null) {
        return found;
      }
      for (let node of tree) {
        if (!node.children?.length) {
          continue;
        }
        found = findNode(identifier, node.children);
        if (found != null) {
          return found;
        }
      }
      return null;
    }
    for (let path of paths) {
      let parentChildren: Node[] = tree;
      for (let identifier of path) {
        let node = findNode(identifier, tree);
        if (node == null) {
          node = {
            identifier,
            children: [],
          };
          parentChildren.push(node);
        }
        parentChildren = node.children;
      }
    }
    return tree;
  }
}
    <nm-widgetframe
  [header]="header"
  [configuration]="configuration"
  [infoTitle]="configuration.configuration.infoTitle"
  [widgetId]="_id"
  [infoText]="configuration.configuration.infoText"
  [infoPlacement]="'bottom'"
  [wikiLink]="configuration.configuration.wikiLink"
  [isCollapsible]="configuration.configuration.isCollapsible"
  *ngIf="withHeader; else content"
>
  <div slot="title" class="nm-widgetframe__title">
    <span class="title-font"
      >{{ configuration.configuration.title | translate }}
    </span>
  </div>
  <div slot="content" class="nm-widgetframe__content">
    <ng-container *ngTemplateOutlet="content"></ng-container>
  </div>
</nm-widgetframe>
<ng-template #content>
  <div
    class="nm-select"
    [ngClass]="{ 'selection-as-chip-list': !disabled && triggerAsChipList }"
    #menuTrigger
  >
    <mat-form-field [ngClass]="{ virgin: !selection }">
      <igx-chips-area
        *ngIf="!disabled && triggerAsChipList"
        (click)="onChipClick($event)"
        class="select-trigger-chips"
      >
        <igx-chip
          *ngFor="let chip of selection"
          [id]="chip.id"
          [removable]="!disabled"
          (click)="onChipClick($event)"
          (remove)="removeFromSelection($event)"
        >
          <span>{{ chip.description }}</span>
        </igx-chip>
      </igx-chips-area>
      <input
        matInput
        #treeinput
        autocomplete="off"
        [disabled]="disabled"
        [(ngModel)]="selectionString"
        (keydown)="keyDownFunction($event)"
        (keydown.backspace)="clearSelection(); $event.stopPropagation()"
        cdkOverlayOrigin
        #menuTrigger
        (pointerdown)="!disabled ? toggleTree() : null"
        (input)="onChange()"
        [placeholder]="placeholder | translate"
        [name]="configuration.configuration['name']"
        [id]="configuration.configuration['name']"
      />
      <button
        mat-icon-button
        matSuffix
        aria-label="Clear"
        color="primary"
        *ngIf="!disabled && selectionString && !(selectionString == '')"
        (click)="clearSelection()"
        tabIndex="-1"
      >
        <mat-icon color="primary" class="fade-in">close</mat-icon>
      </button>
    </mat-form-field>
  </div>
  <ng-template #treeOverlay>
    <div class="nm-selectTree__overlay--wrapper">
      <button
        class="nm-selectTree__overlay--closeButton"
        color="primary"
        *ngIf="configuration.configuration.multi"
        mat-icon-button
        (click)="closeTreeOverlay()"
      >
        <mat-icon>done</mat-icon>
      </button>
      <nm-tree-selector
        #treeSelector
        [nodes]="nodes"
        [filterable]="filterable"
        [primary-key]="identifierField"
        [parent-key]="'parent-node'"
        [has-children-key]="'hasChildren'"
        [load-children]="loadChildNodes"
        [field]="'name'"
        [multi-selection]="configuration.configuration.multi"
        [height]="'100%'"
        [initial-selection]="initialSelection"
        [load-children-on-filter]="loadChildrenOnFilter"
        (onCloseTreeOverlayEvent)="closeTreeOverlay()"
        (onSelectionEvent)="onSelectionEvent($event)"
      >
      </nm-tree-selector>
    </div>
  </ng-template>
</ng-template>