File

src/app/shared/widgets/dynamic-form/dynamic-form-component/dynamic-form-fields.component.ts

Extends

BaseConfiguration

Index

Properties

Properties

fields
fields: DynamicFormField[]
Type : DynamicFormField[]

Array of dynamic form fields to be added in the form.

import {
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  Component,
  OnDestroy,
  Input,
  Output,
  EventEmitter,
  ViewChild,
} from "@angular/core";
import { EMPTY, Observable, Subject } from "rxjs";
import { NgUnsubscribe } from "../../../ng-unsubscribe";
import { map, takeUntil, withLatestFrom } from "rxjs/operators";
import { WidgetframeService } from "../../widgetframe/widgetframe.service";
import { SelectItemsDialogComponent } from "../../../components/select-items-dialog/select-items-dialog.component";
import { CurrentLocaleService } from "../../../components/i18n/currentLocale.service";
import { DEFAULT_TEXT_EDITOR_CONFIG } from "../../../components/tiny-text-editor/tiny-text-editor.component";
import { BaseConfiguration } from "../../widgetframe/widgetframe.component";
import { DataValidation } from "../../../components/validation/validation.service";
import { ValidationError } from "../../../components/validation/validators";
import { TranslateService } from "@ngx-translate/core";
import { OwlDateTimeComponent, OwlDateTimeIntl } from "ng-pick-datetime";
import { DateAdapter } from "@angular/material/core";
import { getOrDefault } from "../../widget.configuration";
import {
  angularWidgetBridgeInput,
  stripPasteContent,
} from "../../../components/util/util.service";
import { PageDialogComponent } from "../../../components/dialog/page-dialog.component";
import { COMMA, ENTER } from "@angular/cdk/keycodes";
import { MatChipInputEvent } from "@angular/material/chips";
import { AppContext } from "../../../components/app-context/app.context";
import { Content, Selectors } from "../../../components/app-context/api";
import { filter } from "rxjs/operators";
import { ValidationService } from "../../../components/validation/validation.service";
import { DateTimeAdapter } from "ng-pick-datetime";
import { WidgetForPipe } from "../../../widgets/container/widget-for.pipe";
import { DialogService } from "../../../components/dialog";

declare var contextPath: string;

@Component({
  selector: "nm-dynamic-form-fields-component",
  templateUrl: "./dynamic-form-fields.component.html",
  styleUrls: ["./dynamic-form-fields.component.scss"],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DynamicFormFieldsComponent implements OnDestroy {
  public unsubscribe = NgUnsubscribe.create();

  public editorConfig: any = DEFAULT_TEXT_EDITOR_CONFIG;

  public lookupOptions = {};
  public dialogSelectedItems = {};
  public value = {};
  public description = {};
  public passwordConfirmation = {};
  public sourceCodeValidations = {};

  public menuActions: { [key: string]: Observable<Content[]> } = {};
  public buttonActions: { [key: string]: Observable<Content[]> } = {};

  public resetDateValue = new Subject<any>();
  public showPassword: boolean = false;
  public separatorKeysCodes: number[] = [ENTER, COMMA];
  public currentLocale: any;

  /**
   * Sets dynamic form fields.
   */
  @Input()
  public fields: DynamicFormField[];

  /**
   * Sets dynamic form configurations.
   */
  @Input()
  public configuration: DynamicFormConfiguration;

  /**
   * Sets lookup options for lookup types.
   */
  @Input("lookupOptions")
  public set lookupOptionsInput(value) {
    angularWidgetBridgeInput(
      value,
      this.lookupOptionsInputChannel,
      this.unsubscribe
    );
  }

  /**
   * Resets field lookup options.
   */
  @Input("resetLookup")
  public set resetLookup(value) {
    angularWidgetBridgeInput(value, this.resetLookupChannel, this.unsubscribe);
  }

  /**
   * Sets field value.
   */
  @Input("valueSetter")
  public set valueSetter(value) {
    angularWidgetBridgeInput(value, this.valueSetterChannel, this.unsubscribe);
  }

  /**
   * Sets field label.
   */
  @Input("labelSetter")
  public set labelSetter(value) {
    angularWidgetBridgeInput(value, this.labelSetterChannel, this.unsubscribe);
  }

  /**
   * Enables\Disables field.
   */
  @Input("setDisabled")
  public set setDisabled(value) {
    angularWidgetBridgeInput(value, this.setDisabledChannel, this.unsubscribe);
  }

  /**
   * Sets dialog context for field used with page-dialog type fields.
   */
  @Input("dialogContext")
  public set dialogContext(value) {
    angularWidgetBridgeInput(
      value,
      this.dialogContextChannel,
      this.unsubscribe
    );
  }

  /**
   * Resets all form fields.
   */
  @Input("resetFormFields")
  public set resetFormFields(value) {
    angularWidgetBridgeInput(
      value,
      this.resetFormFieldsChannel,
      this.unsubscribe
    );
  }

  /**
   * Shows\Hides field.
   */
  @Input("setHidden")
  public set setHidden(value) {
    angularWidgetBridgeInput(value, this.setHiddenChannel, this.unsubscribe);
  }

  /**
   * Resets all form fields.
   */
  @Input("reset")
  public set reset(value) {
    angularWidgetBridgeInput(value, this.resetChannel, this.unsubscribe);
  }

  /**
   * Reloads field lookup options.
   */
  @Input("reloadLookup")
  public set reloadLookup(value) {
    angularWidgetBridgeInput(value, this.reloadLookupChannel, this.unsubscribe);
  }

  /**
   * Adds fields dynamically to the form.
   */
  @Input("addFields")
  public set addFields(value) {
    angularWidgetBridgeInput(value, this.addFieldsChannel, this.unsubscribe);
  }

  /**
   * Removes fields dynamically from the form.
   */
  @Input("removeFields")
  public set removeFields(value) {
    angularWidgetBridgeInput(value, this.removeFieldsChannel, this.unsubscribe);
  }

  /**
   * Removes all fields dynamically from the form.
   */
  @Input("removeAllFields")
  public set removeAllFields(value) {
    angularWidgetBridgeInput(
      value,
      this.removeAllFieldsChannel,
      this.unsubscribe
    );
  }

  /**
   * Triggers validation for form fields.
   */
  @Input("triggerValidation")
  public set triggerValidation(value) {
    angularWidgetBridgeInput(
      value,
      this.triggerValidationChannel,
      this.unsubscribe
    );
  }

  @ViewChild("datepicker", { static: false })
  datePicker: OwlDateTimeComponent<Date>;
  @ViewChild("datetimepicker", { static: false })
  dateTimePicker: OwlDateTimeComponent<any>;

  public lookupOptionsInputChannel = new Subject<any>();
  public resetLookupChannel = new Subject<string>();
  public reloadLookupChannel = new Subject<string>();
  public valueSetterChannel = new Subject<{ field: string; value: any }>();
  public labelSetterChannel = new Subject<{ field: string; value: any }>();
  public setDisabledChannel = new Subject<{ field: string; value: boolean }>();
  public dialogContextChannel = new Subject<{ field: string; context: any }>();
  public resetFormFieldsChannel = new Subject<any>();
  public setHiddenChannel = new Subject<{ field: string; value: boolean }>();
  public resetChannel = new Subject<any>();
  public addFieldsChannel = new Subject<DynamicFormField[]>();
  public removeFieldsChannel = new Subject<string[]>();
  public removeAllFieldsChannel = new Subject<any>();
  public triggerValidationChannel = new Subject();

  /**
   * Emits when any field value changed.
   */
  @Output("value")
  public valueOutputEmitter = new EventEmitter<any>();

  /**
   * Emits validation result when any field value changed.
   */
  @Output("valid")
  public validOutputEmitter = new EventEmitter<any>();

  /**
   * Emits when any field is clicked.
   */
  @Output("onClick")
  public clickOutputEmitter = new EventEmitter<any>();

  /**
   * Emits when enter is pressed.
   */
  @Output("onEnterPressed")
  public enterPressedOutputEmitter = new EventEmitter<any>();

  /**
   * Emits when initialization is finished for all fields and emits also before form destroy.
   */
  @Output("init")
  public initOutputEmitter = new EventEmitter<any>();

  /**
   * Emits the dropped value when using drag and drop featuer.
   */
  @Output("droppedValue")
  public droppedValueEmitter = new EventEmitter<any>();

  constructor(
    protected cdr: ChangeDetectorRef,
    protected widgetFrameService: WidgetframeService,
    protected currentLocaleService: CurrentLocaleService,
    protected dateAdapter: DateAdapter<Date>,
    protected owldateTimeAdapter: DateTimeAdapter<any>,
    protected translateService: TranslateService,
    protected owlDateTimeIntl: OwlDateTimeIntl,
    protected dialogService: DialogService,
    protected appContext: AppContext,
    protected validationService: ValidationService
  ) {}

  public ngOnInit() {
    if (!this.fields) {
      return;
    }

    this.initFields(this.fields);

    this.reloadLookupChannel
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((identifier) => {
        let field = this.fields.find((entry) => entry.field === identifier);
        if (!field) {
          field = this.fields.find((entry) => entry.type === "multi-toggler");
          if (field) {
            field = field.items.find((item) => item.field === identifier);
          }
          if (!field) {
            console.error(
              `[ReloadLookup] Cant find field with identifier ${identifier}`
            );
            return;
          }
        }
        this.loadLookupData(field);
      });

    this.addFieldsChannel
      .pipe(
        withLatestFrom(this.currentLocaleService.getCurrentLocale()),
        takeUntil(this.unsubscribe)
      )
      .subscribe((data) => {
        const fields = data[0];
        const locale = data[1];
        this.fields.push(...fields);
        this.initFields(fields);
        this.updateLocale(locale, fields);
        this.cdr.markForCheck();
      });

    this.valueSetterChannel
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((data) => {
        if (!this.value) {
          this.value = {};
        }

        const field = this.findField(data.field);
        if (!field) {
          return;
        }

        this.setFieldDescription(field, data.value);
        this.value[field.field] = data.value;
        this.onValueChange(field, false);
        this.cdr.markForCheck();
      });

    this.labelSetterChannel
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((data) => {
        const field = this.findField(data.field);
        if (!field) {
          return;
        }

        if (field.label != null) {
          field.label = data.value;
        }

        this.cdr.markForCheck();
      });

    this.setDisabledChannel
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((data) => {
        const field = this.findField(data.field);
        if (!field) {
          return;
        }

        field.disabled = data.value;
        this.cdr.markForCheck();
      });

    this.dialogContextChannel
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((data) => {
        const field = this.findField(data.field);
        if (!field) {
          return;
        }

        field.dialogContext = data.context;
        this.cdr.markForCheck();
      });

    this.resetFormFieldsChannel
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((data) => {
        this.fields.forEach((field) => {
          if (field.type === "category-selector") {
            field.includeSubCategories = false;
            this.cdr.markForCheck();
          }
        });
        Object.keys(this.value).forEach((field) => {
          this.value[field] = null;
          this.description[field] = null;
          this.cdr.markForCheck();
          this.resetDateValue.next(true);
        });
        this.dialogSelectedItems = {};
      });

    this.setHiddenChannel
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((data) => {
        const field = this.fields.find((entry) => entry.field === data.field);
        if (!field) {
          console.error("Unable to find field " + data.field);
          return;
        }
        field.hidden = data.value;

        this.cdr.markForCheck();
      });

    this.lookupOptionsInputChannel
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((data) => {
        this.setLookupOptions(data.field, data.value);
      });

    this.resetChannel.pipe(takeUntil(this.unsubscribe)).subscribe((reset) => {
      this.value = {};
      this.dialogSelectedItems = {};
      this.description = {};
      this.cdr.markForCheck();
    });

    this.resetLookupChannel
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((data) => {
        this.lookupOptions[data] = [];
        this.value[data] = null;
        this.cdr.markForCheck();
      });

    this.removeFieldsChannel
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((fields) => {
        this.fields = this.fields.filter((f) => fields.indexOf(f.field) === -1);
        fields.forEach((f) => delete this.value[f]);
        this.cdr.markForCheck();
      });

    this.removeAllFieldsChannel
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((data) => {
        this.fields = [];
        this.value = {};
        this.description = {};
        this.cdr.markForCheck();
      });

    this.triggerValidationChannel
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((data) => this.onTriggerValidation());

    this.currentLocaleService
      .getCurrentLocale()
      .pipe(takeUntil(this.unsubscribe))
      .subscribe((locale) => {
        this.updateLocale(locale, this.fields);
      });

    this.initOutputEmitter.next({ init: true });
  }

  private findField(identifier: string) {
    let field = this.fields.find((entry) => entry.field === identifier);
    if (!field) {
      field = this.fields.find((entry) => entry.type === "multi-toggler");
      if (field) {
        field = field.items.find((item) => item.field === identifier);
      }
      if (!field) {
        console.error(
          `Unable to find field with identifier ${identifier} `,
          this.fields
        );
        return;
      }
    }

    return field;
  }

  private initFields(fields: any[]) {
    this.fields.sort((a, b) => {
      if (!a.order) {
        a.order = 0;
      }
      if (!b.order) {
        b.order = 0;
      }
      return a.order - b.order;
    });

    fields.forEach((field) => {
      if (field.chipRemovable === undefined || field.chipRemovable === null) {
        field.chipRemovable = true;
      }

      if (field.clearable == null) {
        field.clearable = true;
      }

      if (field.type === "date") {
        if (!field.dateFormat) {
          field.dateFormat = "dd.mm.yyyy";
        }
      } else if (field.type === "text") {
        if (field.height) {
          //This will only work if all editors are supposed to have the same height..
          this.editorConfig = Object.assign(
            { height: field.height },
            this.editorConfig
          );
        }
      } else if (
        field.type === "lookup" ||
        field.type === "multi-lookup" ||
        field.type === "context"
      ) {
        this.loadLookupData(field);
      } else if (field.type === "category-selector") {
        if (field.publication) {
          this.value[field.field] = {
            publication: field.publication,
          };
        }
      } else if (field.type === "boolean-indeterminate") {
        this.value[field.field] = field.defaultValue;
      } else if (field.type === "password-confirmation") {
        if (field.hideConfirmationField === undefined) {
          field.hideConfirmationField = true;
        }
      } else if (field.type === "radio") {
        this.loadLookupData(field);
      } else if (field.type === "source-code-editor") {
        this.value[field.field] = field.defaultValue;
      }

      field.showHint = !field.hintOnFocus;

      this.setFieldDefaultValue(field);
      this.onValueChange(field, false);
    });

    this.currentLocaleService.getCurrentLocale().subscribe((locale) => {
      this.currentLocale = locale;
    });
  }

  private setFieldDefaultValue(field: DynamicFormField) {
    if (!field.defaultValue) {
      return;
    }

    this.value[field.field] = field.defaultValue;
  }

  private setFieldDescription(field: DynamicFormField, value: any) {
    if (!field) {
      return;
    }

    if (field.type === "user-right") {
      this.description[field.field] = value
        ? value.identifier + " (" + value.pimRef + ")"
        : null;
    } else if (field.type === "worklist") {
      this.description[field.field] = value ? value.name : null;
    } else if (field.type === "page-dialog") {
      this.description[field.field] = value
        ? value[field.dialogDescription]
        : null;
    }
  }

  private setLookupOptions(field, value) {
    this.lookupOptions[field] = value;
    this.cdr.markForCheck();
  }

  public getClass(field: DynamicFormField) {
    return String(field.field);
  }

  public getIdentifierField(field: DynamicFormField) {
    if (field.identifierField) {
      return field.identifierField;
    }
    return "identifier";
  }

  public getDescriptionField(field: DynamicFormField) {
    if (field.descriptionField) {
      return field.descriptionField;
    }
    return "description";
  }

  public loadLookupData(field: DynamicFormField) {
    if (field.lookupUrl) {
      this.widgetFrameService.getData(field.lookupUrl).subscribe((data) => {
        if (data._embedded) {
          if (data.type) {
            data = data._embedded[data.type];
          } else {
            const key = Object.keys(data._embedded)[0];
            data = data._embedded[key];
          }
        }
        if (field.lookupAllowEmpty) {
          const emptyEntry = {};
          emptyEntry[this.getIdentifierField(field)] = "";
          emptyEntry[this.getDescriptionField(field)] = "";
          data.unshift(emptyEntry);
        }
        this.setLookupOptions(field.field, data);
      });
    } else if (field.lookupOptions) {
      this.setLookupOptions(field.field, field.lookupOptions);
    }
  }

  ngOnDestroy(): void {
    this.initOutputEmitter.next({ init: false, value: this.value });
    this.unsubscribe.destroy();
  }

  onValueChange(field: DynamicFormField, isEdit: boolean = true) {
    this.onTriggerValidation();

    this.valueOutputEmitter.next({
      value: this.value,
      lastChange: field.field,
      isEdit: isEdit,
    });
  }

  onTriggerValidation() {
    const valid = this.validateFields();
    this.validOutputEmitter.next(valid);
  }

  onEditorValueChange(field: DynamicFormField, value: any) {
    this.value[field.field] = value.value;
    this.sourceCodeValidations[field.field] = value.isValid;
    this.onValueChange(field);
  }

  private validateFields(): boolean {
    this.validationService.validateDynamicFormFields(this.fields, this.value);

    let valid = true;
    for (const field of this.fields) {
      // the validation service updates the errors to field.errors
      // if field.errors is undefined or has no entries, the field is valid
      if (Array.isArray(field.errors) && field.errors.length > 0) {
        valid = false;
        break;
      }

      if (!field.required) {
        continue;
      }

      const value = this.value[field.field];

      if (
        value === undefined ||
        value === null ||
        value === "" ||
        value.length === 0
      ) {
        valid = false;
        break;
      }

      if (!this.validateLocalizedText(field)) {
        valid = false;
        break;
      }

      if (!this.validateCategorySelector(field)) {
        valid = false;
        break;
      }

      if (!this.validatePasswords(field)) {
        valid = false;
        break;
      }

      if (!this.validateSourceCodeEditor(field)) {
        valid = false;
        break;
      }
    }

    return valid;
  }

  private validateSourceCodeEditor(field) {
    if (field.type !== "source-code-editor") {
      return true;
    }
    return this.sourceCodeValidations[field.field];
  }

  private validateLocalizedText(field: DynamicFormField): boolean {
    if (!field.localizedText) {
      return true;
    }

    const value = this.value[field.field];

    let validLocalizedText = false;
    Object.keys(value).forEach((locale) => {
      if (locale && value[locale].trim()) {
        validLocalizedText = true;
      }
    });

    return validLocalizedText;
  }

  private validateCategorySelector(field: DynamicFormField): boolean {
    if (field.type !== "category-selector") {
      return true;
    }

    const value = this.value[field.field];

    if (!value.publication || !value.category || value.category.length === 0) {
      return false;
    }

    return true;
  }

  private validatePasswords(field: DynamicFormField): boolean {
    if (field.type !== "password") {
      return true;
    }

    const password = this.value[field.field];
    const confirmPassword = this.passwordConfirmation[field.field];

    if (!password || !confirmPassword) {
      return false;
    }

    return password === confirmPassword;
  }

  onClick(event, field: DynamicFormField) {
    event.stopPropagation();
    this.clickOutputEmitter.next(field);
  }

  onEnterPressed(event) {
    if (event.target) {
      // due to ngModelOptions: force blur event to trigger setting of asset-search´s formValue
      event.target.blur();
      this.enterPressedOutputEmitter.next(event.target.value);
    }
  }

  onKeyPressedDatePicker(event) {
    const key = event.key.toLowerCase();
    switch (key) {
      case "space":
      case "spacebar":
      case " ":
      case "enter":
      case "down":
      case "arrowdown":
        this.datePicker.open();
        this.onEnterPressed(event);
    }
  }

  onKeyPressedDateTimePicker(event) {
    const key = event.key.toLowerCase();
    switch (key) {
      case "space":
      case "spacebar":
      case " ":
      case "enter":
      case "down":
      case "arrowdown":
        this.dateTimePicker.open();
        this.onEnterPressed(event);
    }
  }

  public onExternalModelChange(field: DynamicFormField, value: any) {
    this.value[field.field] = value;
    this.onValueChange(field);
  }

  public openSelectDialog(field: DynamicFormField) {
    const dialogConfig = {
      minWidth: "900px",
      minHeight: "750px",
      data: {
        title: field.label,
        itemsUrl: field.lookupUrl,
        itemType: field.selectDialogItemType,
        itemTypeId: this.getIdentifierField(field),
        itemTypeDescription: this.getDescriptionField(field),
        additionalProperties: field.additionalProperties
          ? field.additionalProperties
          : [],
        preselectedItems: this.dialogSelectedItems[field.field],
        excludePreselected: false,
        allowNoSelection: true,
      },
    };

    let dialogRef = this.dialogService.open(
      SelectItemsDialogComponent,
      dialogConfig
    );

    dialogRef.afterClosed().subscribe((selectedItems) => {
      if (!selectedItems) {
        return;
      }
      if (selectedItems.length === 0) {
        this.value[field.field] = null;
        this.description[field.field] = null;
        this.dialogSelectedItems[field.field] = null;
      } else if (selectedItems.length > 0) {
        this.dialogSelectedItems[field.field] = selectedItems;
        let values = [];
        let descriptions = [];
        selectedItems.forEach((item) => {
          values.push(item[this.getIdentifierField(field)]);
          descriptions.push(item[this.getDescriptionField(field)]);
        });
        this.value[field.field] = values.toString();
        this.description[field.field] = descriptions.join(", ");
        this.onValueChange(field);
      }
      this.cdr.markForCheck();
    });
  }

  public openUserRightDialog(field: DynamicFormField) {
    const context = { userRightsUrl: field.url };
    this.pageDialog(field, "core", "select-userRight", context, "650px");
  }

  public openWorklistDialog(field: DynamicFormField) {
    this.pageDialog(field, "core", "select-worklist", null, "650px");
  }

  public openPageDialog(field: DynamicFormField) {
    this.pageDialog(
      field,
      field.dialogModule,
      field.dialogIdentifier,
      field.dialogContext,
      "900px"
    );
  }

  private pageDialog(
    field: DynamicFormField,
    module: string,
    identifier: string,
    context: any,
    minWidth: string
  ) {
    if (field.disabled) {
      return;
    }

    const dialogRef = this.dialogService.open(PageDialogComponent, {
      minWidth: minWidth,
      height: "700px",
      data: {
        title: field.label,
        acceptEnabled: true,
        customAcceptIcon: true,
        acceptIcon: "check",
        acceptText: "button.accept",
        cancelEnabled: true,
        cancelText: "button.cancel",
        module: module,
        identifier: identifier,
        context: context,
      },
    });

    dialogRef.afterClosed().subscribe((data) => {
      if (!data) {
        return;
      }

      this.setFieldDescription(field, data);
      this.value[field.field] = data;
      this.onValueChange(field);
      this.cdr.markForCheck();
    });
  }

  public clearField(field: DynamicFormField) {
    this.value[field.field] = null;
    this.description[field.field] = null;
    this.dialogSelectedItems[field.field] = null;

    this.onValueChange(field);
    this.cdr.markForCheck();
  }

  private updateLocale(locale: string, fields: DynamicFormField[]) {
    fields.forEach((field) => {
      if (field.translations) {
        const translations = field.translations;
        if (translations[locale]) {
          field.label = translations[locale];
        }
      }
      if (field.hintTranslations) {
        const translations = field.hintTranslations;
        if (translations[locale]) {
          field.hint = translations[locale];
        }
      }
    });
    this.dateAdapter.setLocale(locale);
    this.owldateTimeAdapter.setLocale(locale);
    this.owlDateTimeIntl.cancelBtnLabel =
      this.translateService.instant("button.cancel");
    this.owlDateTimeIntl.setBtnLabel =
      this.translateService.instant("button.accept");
  }

  public includeSubCategoriesValueChanged(field: DynamicFormField, value: any) {
    if (this.value[field.field]) {
      this.value[field.field].includeSubCategories = value;
    } else {
      this.value[field.field] = { includeSubCategories: value };
    }
    field.includeSubCategories = value;
  }

  public getCategoryUrl(field: DynamicFormField) {
    return getOrDefault(field.categoryUrl, "");
  }

  public booleanValueChanged(field: DynamicFormField) {
    switch (this.value[field.field]) {
      case true: {
        this.value[field.field] = false;
        break;
      }
      case false: {
        this.value[field.field] = null;
        break;
      }
      case undefined:
      case null: {
        this.value[field.field] = true;
        break;
      }
    }

    this.onValueChange(field);
  }

  addChip(event: MatChipInputEvent, field: DynamicFormField): void {
    const input = event.input;
    const value = event.value;

    if ((value || "").trim()) {
      if (this.value[field.field]) {
        this.value[field.field].push({
          identifier: value.trim(),
          description: value.trim(),
        });
      } else {
        this.value[field.field] = [
          { identifier: value.trim(), description: value.trim() },
        ];
      }

      this.onValueChange(field);
    }

    if (input) {
      input.value = "";
    }
  }

  removeChip(chip: any, field: DynamicFormField): void {
    if (!this.value[field.field]) {
      return;
    }

    const index = this.value[field.field].indexOf(chip);

    if (index >= 0) {
      this.value[field.field].splice(index, 1);
      this.onValueChange(field);
    }
  }

  public onItemDropped(ev, field) {
    const dropValueIdentifier = getOrDefault(
      field.dropValueIdentifier,
      "identifier"
    );
    const myValue = ev.dragData.source[dropValueIdentifier];
    this.droppedValueEmitter.emit(myValue);
    const cursorPosition = ev.owner.element.nativeElement.selectionStart;

    if (this.value[field.field]) {
      var firstHalf = this.value[field.field].substring(0, cursorPosition);
      if (firstHalf) {
        firstHalf += " ";
      }
      var secondHalf = this.value[field.field].substring(
        cursorPosition,
        this.value[field.field].length
      );
      if (secondHalf) {
        secondHalf = " " + secondHalf;
      }

      this.value[field.field] = firstHalf + myValue + secondHalf;
    } else {
      this.value[field.field] = myValue;
    }
    this.onValueChange(field);
  }

  public onFileUpload(field, uploadedFile: any) {
    if (uploadedFile) {
      this.value[field.field] = uploadedFile;
    } else {
      this.value[field.field] = null;
    }
    this.onValueChange(field);
  }

  public changePassword(field) {
    field.hideConfirmationField = !field.hideConfirmationField;
    field.required = !field.required;
    field.disabled = !field.disabled;
    if (field.hideConfirmationField) {
      this.value[field.field] = null;
      this.passwordConfirmation[field.field] = null;
    }
    this.onValueChange(field);
    this.clickOutputEmitter.next(field);
  }

  public getLocalizedValue(field: DynamicFormField): string {
    if (!field || this.value[field.field] == null) {
      return "";
    }

    if (field.localizedText) {
      return this.value[field.field][this.currentLocale];
    }
    return this.value[field.field];
  }

  private initializeMenuActions(field: DynamicFormField) {
    if (field.menuInteractions) {
      this.menuActions[field.field] = this.appContext.browserContext
        .subscribe(field.menuInteractions)
        .pipe(
          takeUntil(this.unsubscribe),
          filter((contents) => !!contents),
          map((contents) => {
            const interactions = contents
              .filter(
                (entry) =>
                  entry.showAsButton == null || entry.showAsButton === false
              )
              .sort((l, r) => (r.order || 0) - (l.order || 0));
            return interactions;
          })
        );
    } else {
      this.menuActions[field.field] = EMPTY;
    }
  }

  private initializeButtonActions(field: DynamicFormField) {
    if (field.menuInteractions) {
      this.buttonActions[field.field] = this.appContext.browserContext
        .subscribe(field.menuInteractions)
        .pipe(
          takeUntil(this.unsubscribe),
          filter((contents) => !!contents),
          map((contents) => {
            return contents
              .filter((entry) => entry?.showAsButton === true)
              .sort((l, r) => (r.order || 0) - (l.order || 0));
          })
        );
    } else {
      this.buttonActions[field.field] = EMPTY;
    }
  }

  public getMenuActions(field: DynamicFormField): Observable<Content[]> {
    if (this.menuActions[field.field]) {
      return this.menuActions[field.field];
    }

    if (field.menuInteractions) {
      this.initializeMenuActions(field);
    } else {
      this.menuActions[field.field] = EMPTY;
    }
    return this.menuActions[field.field];
  }

  public getButtonActions(field: DynamicFormField): Observable<Content[]> {
    if (this.buttonActions[field.field]) {
      return this.buttonActions[field.field];
    }

    if (field.menuInteractions) {
      this.initializeButtonActions(field);
    } else {
      this.buttonActions[field.field] = EMPTY;
    }

    return this.buttonActions[field.field];
  }

  public getInteractions(
    field: DynamicFormField
  ): { menu: Observable<Content[]>; buttons: Observable<Content[]> } | null {
    if (field.menuInteractions == null) {
      return null;
    }

    return {
      menu: this.getMenuActions(field),
      buttons: this.getButtonActions(field),
    };
  }

  onKeyPressed(event: KeyboardEvent, field) {
    if (field.tab && event.code !== "Tab") {
      event.preventDefault();
      return false;
    }
    return;
  }

  onFocusOut(field: DynamicFormField) {
    field.showHint = !field.hintOnFocus;
  }

  onFocusIn(field: DynamicFormField) {
    field.showHint = true;
  }

  getPreferredPosition(field: DynamicFormField) {
    return field.additionalProperties &&
      field.additionalProperties.preferredPosition
      ? field.additionalProperties.preferredPosition
      : ["right-below", "right", "bottom"];
  }

  resetDateTime(field, event) {
    event.stopPropagation();
    this.value[field.field] = null;
    this.onValueChange(field);
  }

  handlePaste(event: ClipboardEvent) {
    stripPasteContent(event);
  }
}

export interface DynamicFormConfiguration extends BaseConfiguration {
  /**
   * Array of dynamic form fields to be added in the form.
   */
  fields: DynamicFormField[];
}

export interface DynamicFormField {
  /**
   * Type of the field. supported types are: boolean, boolean-indeterminate, category-selector,
   * chip-list, date, date-time, file-upload, heading, lookup, multi-lookup,  number,
   * number-decimal, password, plain, select-dialog, text, textarea, toggler, multi-toggler, user-right, worklist, source-code-editor
   */
  type: string;

  /**
   * Unique field identifier.
   */
  field: string;

  /**
   * collection of all data for multi-components
   */
  items?: DynamicFormField[];

  /**
   * Localized key for field label
   */
  label: string;

  /**
   * URL to load lookup options from. used with these types: lookup, multi-lookup, select-dialog.
   * (See also lookupOptions)
   */
  lookupUrl?: string;

  /**
   * Array of static lookup options. used with these types: lookup, multi-lookup, select-dialog.
   * (See also lookupUrl)
   */
  lookupOptions?: any[];

  /**
   * Allows empty selection for lookup types. @default(false)
   */
  lookupAllowEmpty?: boolean;

  /**
   * Item type used for select-dialog type.
   */
  selectDialogItemType?: string;

  /**
   * Enables\Disables the field.
   */
  disabled?: boolean;

  /**
   * Makes the field required or optional.
   */
  required?: boolean;

  /**
   * Identifier field of the object. it is used with 'lookup' and 'multi-lookup' types
   * to define identifier field. @default(identifier)
   */
  identifierField?: string;

  /**
   * Description field of the object. it is used with 'lookup' and 'multi-lookup' types
   * to define description field. @default(description)
   */
  descriptionField?: string;

  /**
   * Field height. used with 'text' type.
   */
  height?: number;

  /**
   * Hides or shows the field.
   */
  hidden?: boolean;

  /**
   * Order of the field in the form. @default(0)
   */
  order?: number;

  /**
   * If not set the version select lookup will be shown,
   * otherwise the provided version will be used and the category select lookup only will be shown.
   * Used with type 'category-selector'.
   */
  publication?: string;

  /**
   * Allows multiple category selection. Used with type 'category-selector'. @default(false)
   */
  multiCategorySelect?: boolean;

  /**
   * URL used to load version from.
   * default is 'category-select-publications' for publications
   * and 'category-select-asset-trees' for asset-trees
   * Used with type 'category-selector'.
   */
  categoryUrl?: string;

  /**
   * Label translation in other locales.
   */
  translations?: any;

  /**
   * hint translation in other locales.
   */
  hintTranslations?: any;

  /**
   * Validations to be applied on field input.
   */
  validations?: DataValidation[];

  /**
   * Date format. used with 'date' type. @default(dd.mm.yyyy)
   */
  dateFormat?: string;

  /**
   * Shows\Hides include sub-categories checkbox. used with 'category-selector' type. @default(false)
   */
  showIncludeSubCategories?: boolean;

  /**
   * Includes sub-categories value. used with 'category-selector' type.
   */
  includeSubCategories?: boolean;

  /**
   * Applies channel types filter on versions. supported values are PURCHASE, SALES, ARCHIVE.
   * used with 'category-selector' type.
   */
  filterChannelTypes?: string[];

  /**
   * Array of additional properties to view in select dialog list. used with 'select-dialog' type.
   */
  additionalProperties?: any;

  /**
   * List folder type. used with 'worklist' type.
   */
  listfolderType?: string;

  /**
   * List folder data type. used with 'worklist' type.
   */
  dataType?: string;

  /**
   * Shows floating label for the field. @default(false)
   */
  floatingLabel?: boolean;

  /**
   * The target tab. used in dynamic forms with mutliple tabs.
   * supported tabs are localizedtext, listFolder, userRights
   */
  tab?: string;

  /**
   * This property shouldn't be added in configuration. It is only used internally.
   */
  value?: any;

  /**
   * Initial value of the field.
   */
  defaultValue?: any;

  /**
   * Makes the field Readonly or editable. @default(false)
   */
  readonly?: boolean;

  /**
   * Sets autosize property for type 'textarea'.
   */
  autosize?: boolean;

  /**
   * Shows clear selection button. used with 'lookup' and 'multi-lookup' types. @default(true)
   */
  clearable?: boolean;

  /**
   * Sets maximum characters length for text types.
   */
  maxLength?: number;

  /**
   * URL to load user rights. used with 'user-right' type.
   */
  url?: string;

  /**
   * Comma-separated list of supported file types. used with 'file-upload' type.
   */
  supportedFileTypes?: string;

  /**
   * File upload url. used with 'file-upload type.
   */
  fileUploadUrl?: string;

  /**
   * Sets field as localized text. @default(false)
   */
  localizedText?: boolean;

  /**
   * Identifiers of the interactions that are supposed to be loaded for the menu.
   */
  menuInteractions?: Selectors;

  /**
   * List of field validation errors.
   */
  errors?: ValidationError[];

  /**
   * Array of supported MIME types. used with 'file-upload' type.
   */
  supportedMimeTypes?: string[];

  /**
   * Makes chips removable in 'chip-list' type. @default(true)
   */
  chipRemovable?: boolean;

  /**
   * Shows\Hides edit password button. used with 'password' type. @default(false)
   */
  showEditpasswordButton?: boolean;

  /**
   * Hides\Shows second password confirmation field. used with 'password' type. @default(true)
   */
  hideConfirmationField?: boolean;

  /**
   * Localized key for password confirmation lable. used with 'password' type.
   */
  confirmationLabel?: string;

  /**
   * Auto focus field.
   */
  autofocus?: boolean;

  /**
   * Localized key for field hint.
   */
  hint?: string;

  /**
   * Shows hint on focus.
   */
  hintOnFocus?: boolean;

  /**
   * This property shouldn't be added in configuration. It is only used internally.
   */
  showHint?: boolean;

  /**
   * Identifier field that will be used to get the value while dropping it in the textarea field.
   */
  dropValueIdentifier?: string;

  /**
   * min rows for textarea field
   */
  autosizeMinRows?: string;

  /**
   * Source code programming language. It is used with type = 'source-code-editor'.
   */
  sourceCodeLanguage?: string;

  /**
   * Dialog module for page-dialog type.
   */
  dialogModule?: string;

  /**
   * Dialog identifier for page-dialog type.
   */
  dialogIdentifier?: string;

  /**
   * Dialog data for page-dialog type.
   */
  dialogContext?: any;

  /**
   * Dialog data property used to set the dialog description when closed for page-dialog type.
   */
  dialogDescription?: any;

  /**
   * Enable/Disable editor minimap for source-code-editor type.
   */
  enableMinimap?: boolean;

  /**
   * Triggers the display of Tiny editor implmentation in its onInit callback
   */
  showEditorOnInit?: boolean;
}

results matching ""

    No results matching ""