@WidgetComponent

nm-edit-tree

File

src/app/shared/widgets/select-tree/select-tree.component.ts

Implements

AfterViewInit

Metadata

selector nm-edit-tree
styleUrls select-tree.component.scss
templateUrl ./select-tree.component.html

Index

Widget inputs
Widget outputs
Properties
Methods

Constructor

constructor(_widgetframeService: WidgetframeService, categoryService: ShopCategoryService, dialog: MatDialog, translateService: TranslateService, localStorageService: LocalStorageService, appdataStore: AppdataStore, cdr: ChangeDetectorRef, overlay: Overlay, viewContainerRef: ViewContainerRef)
Parameters :
Name Type Optional
_widgetframeService WidgetframeService no
categoryService ShopCategoryService no
dialog MatDialog no
translateService TranslateService no
localStorageService LocalStorageService no
appdataStore AppdataStore no
cdr ChangeDetectorRef no
overlay Overlay no
viewContainerRef ViewContainerRef no

Methods

Private buildTree
buildTree(paths: string[][])
Parameters :
Name Type Optional
paths string[][] no
Returns : Node[]
clearSelection
clearSelection()
Returns : void
Public closeTreeOverlay
closeTreeOverlay()
Returns : void
Public configureWidget
configureWidget(configuration: WidgetConfig)
Decorators : WidgetConfigure
Parameters :
Name Type Optional
configuration WidgetConfig no
Returns : void
Private doReload
doReload(withReset: )
Parameters :
Name Optional
withReset no
Returns : void
Private emitSelection
emitSelection()
Returns : void
Private extractData
extractData(response: )
Parameters :
Name Optional
response no
Returns : any[]
forceLoad
forceLoad(root: any, tree: Node[])
Parameters :
Name Type Optional Default value
root any no
tree Node[] no ([] = [])
Returns : Observable<any[]>
Private forceLoadChildren
forceLoadChildren(node: any, tree: Node[])
Parameters :
Name Type Optional
node any no
tree Node[] no
Returns : Observable<any[]>
Private forceSelect
forceSelect(categoryValue: string)
Parameters :
Name Type Optional
categoryValue string no
Returns : void
initCategoryFromInput
initCategoryFromInput(categoryValues: literal type[])
Parameters :
Name Type Optional
categoryValues literal type[] no
Returns : void
keyDownFunction
keyDownFunction(event: )
Parameters :
Name Optional
event no
Returns : void
loadChildren
loadChildren(link: )
Parameters :
Name Optional
link no
Returns : Observable<any>
Private loadRootCategories
loadRootCategories(callback?: (nodes?: any[]) => void, tree: Node[])
Parameters :
Name Type Optional Default value
callback function yes
tree Node[] no []
Returns : void
ngAfterViewInit
ngAfterViewInit()
Returns : void
onChange
onChange()
Returns : void
onChipClick
onChipClick(event: )
Parameters :
Name Optional
event no
Returns : boolean
onClickOnOverlayBackdrop
onClickOnOverlayBackdrop(event: )
Parameters :
Name Optional
event no
Returns : void
Private onSelectionChange
onSelectionChange()
Returns : void
onSelectionEvent
onSelectionEvent(event: SelectionEvent)
Parameters :
Name Type Optional
event SelectionEvent no
Returns : void
removeFromSelection
removeFromSelection(event: )
Parameters :
Name Optional
event no
Returns : boolean
Private setSelection
setSelection(input: string[] | string[][])
Parameters :
Name Type Optional
input string[] | string[][] no
Returns : void
toggleTree
toggleTree()
Returns : void
transformData
transformData(data: any[], parentIdentifier: any)
Parameters :
Name Type Optional Default value
data any[] no
parentIdentifier any no null
Returns : any[]
Private updateCategoryOutput
updateCategoryOutput(event: SelectionEvent)
Parameters :
Name Type Optional
event SelectionEvent no
Returns : void

Properties

Public _id
_id: string
Type : string
Decorators : WidgetId
Public appdata
appdata: any
Type : any
Private clearChannel
clearChannel: Subject<any>
Type : Subject<any>
Default value : new Subject<any>()
Decorators : WidgetInput
Private clearTextSearch
clearTextSearch: Subject<any>
Type : Subject<any>
Decorators : WidgetOutput
Public configuration
configuration: WidgetConfig
Type : WidgetConfig
Decorators : WidgetConfiguration
Public dataType
dataType: string
Type : string
Static DEFAULT_OUTPUT_TYPE
DEFAULT_OUTPUT_TYPE: string
Type : string
Default value : "uri"
Public descriptionField
descriptionField:
Public dialog
dialog: MatDialog
Type : MatDialog
Private disabledInput
disabledInput:
Default value : new Subject<boolean>()
Decorators : WidgetInput
Public header
header: string
Type : string
Public identifierField
identifierField:
Public infotext
infotext: string
Type : string
Public inputLink
inputLink: string
Type : string
Public isOpen
isOpen:
Default value : false
Private lastFocused
lastFocused:
loadChildNodes
loadChildNodes:
Default value : (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([]); } }
Public loadChildrenOnFilter
loadChildrenOnFilter:
Default value : (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(); }
Public localStorageDescriptionEntry
localStorageDescriptionEntry: LocalStorageEntry
Type : LocalStorageEntry
Public localstorageSelectedCategoryEntry
localstorageSelectedCategoryEntry: LocalStorageEntry
Type : LocalStorageEntry
Public nodes
nodes: any[]
Type : any[]
Public options
options: any[]
Type : any[]
Default value : []
Public outputType
outputType: string
Type : string
Private outputUri
outputUri:
Default value : new Subject<any>()
Decorators : WidgetOutput
Private overlayRef
overlayRef: OverlayRef
Type : OverlayRef
Public payload
payload:
Default value : new Subject<any>()
Decorators : WidgetInput
Private pendingRequests
pendingRequests: number
Type : number
Default value : 0
Public placeholder
placeholder: string
Type : string
Private position
position: FlexibleConnectedPositionStrategy
Type : FlexibleConnectedPositionStrategy
Private profile
profile:
Default value : new Subject<any>()
Decorators : WidgetOutput
Private programaticExpansion
programaticExpansion: boolean
Type : boolean
Default value : false
Public reloadChannel
reloadChannel:
Default value : new Subject<any>()
Decorators : WidgetInput
Public resetChannel
resetChannel: Subject<any>
Type : Subject<any>
Default value : new Subject<any>()
Decorators : WidgetInput
Public rootCategoryLink
rootCategoryLink: string
Type : string
Public rootSearchCategoryLink
rootSearchCategoryLink: string
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: any[]
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: any[]
Type : any[]
Public selectionString
selectionString:
Public shouldLabelFloat
shouldLabelFloat: boolean
Type : boolean
Default value : false
Private template
template: TemplateRef<any>
Type : TemplateRef<any>
Decorators : ViewChild
Private templatePortal
templatePortal: TemplatePortal<any>
Type : TemplatePortal<any>
Public title
title: string
Type : string
Public translateService
translateService: TranslateService
Type : TranslateService
treeinput
treeinput:
Decorators : ViewChild
treeSelector
treeSelector: TreeSelectorComponent
Type : TreeSelectorComponent
Decorators : ViewChild
Public triggerAsChipList
triggerAsChipList: boolean
Type : boolean
triggerRef
triggerRef: ElementRef
Type : ElementRef
Decorators : ViewChild
Private triggerSearch
triggerSearch: Subject<any>
Type : Subject<any>
Default value : new Subject<any>()
Decorators : WidgetOutput
Private unsubscribe
unsubscribe:
Default value : NgUnsubscribe.create()
Public wikiLink
wikiLink: string
Type : string
Public withHeader
withHeader: boolean
Type : boolean

Accessors

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>
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""