File

projects/netgrif-components-core/src/lib/task-content/task-content/abstract-task-content.component.ts

Implements

OnDestroy

Metadata

selector ncc-abstract-task-content

Index

Properties
Methods
Inputs
Outputs
Accessors

Constructor

Protected constructor(_fieldConverter: FieldConverterService, taskContentService: TaskContentService, _paperView: PaperViewService, _logger: LoggerService, _taskEventService: TaskEventService, asyncRenderingConfiguration: AsyncRenderingConfiguration)
Parameters :
Name Type Optional
_fieldConverter FieldConverterService No
taskContentService TaskContentService No
_paperView PaperViewService No
_logger LoggerService No
_taskEventService TaskEventService No
asyncRenderingConfiguration AsyncRenderingConfiguration No

Inputs

displayNoDataIcon
Type : boolean
Default value : true

Whether an icon should be displayed when the no data message is shown.

An icon is displayed by default

noDataIcon
Type : string

The icon that should be displayed when the task contains no data.

If a falsy value is provided the default icon is displayed

noDataText
Type : string

The translate text that should be displayed when the task contains no data.

If a falsy value is provided the default text is displayed

Outputs

taskEvent
Type : EventEmitter<TaskEventNotification>

Emits notifications about task events

Methods

Protected cloneAndFilterHidden
cloneAndFilterHidden(dataGroups: Array)

Creates a duplicate of the provided data group array and filters away any fields and data groups that are marked as hidden. Because of the duplication the filtering doesn't affect the original instances and they remain unchanged.

Parameters :
Name Type Optional Description
dataGroups Array<DataGroup> No

the data groups that should be filtered

Returns : Array<DataGroup>

the filtered duplicated data groups

Protected cloneDataGroups
cloneDataGroups(dataGroups: Array)

Creates a duplicate of the provided data group array.

Only the data groups are cloned, the fields are only copied as references.

Parameters :
Name Type Optional Description
dataGroups Array<DataGroup> No

the data groups that should be cloned

Returns : Array<DataGroup>

the duplicated data groups

Protected collapseGridEmptySpace
collapseGridEmptySpace(grid: Array>, layout: DataGroupLayout, subgrid: Subgrid)

Applies the empty row removal and field compacting rules based on the provided layout configuration.

The input grid is modified in place.

Parameters :
Name Type Optional Description
grid Array<Array<string>> No

the state of the grid that should be modified

layout DataGroupLayout No

configuration of the applied compacting rules

subgrid Subgrid No

the object that holds the context of the computed layouting unit

Returns : void
Protected compactFieldsUp
compactFieldsUp(grid: Array>, hideRows: DataGroupHideEmptyRows, subgrid: Subgrid)

Moves any element as far UP as it can go. Elements that were originally declared in the same row might end up in different rows. Resulting trailing empty rows are removed.

The input grid is modified in place.

Parameters :
Name Type Optional Description
grid Array<Array<string>> No

the state of the grid that should be modified

hideRows DataGroupHideEmptyRows No

configuration for empty row removal during the compacting process

subgrid Subgrid No

the object that holds the context of the computed layouting unit

Returns : void
Protected computeDefaultLayoutConfiguration
computeDefaultLayoutConfiguration()

If te task specifies no default value the global default value is returned.

Returns : void

the default alignment for the displayed task that can be overridden by fields individual property. If te task specifies no default value the [global default]{

Protected computeFlowLayout
computeFlowLayout(dataGroup: DataGroup, subgrid: Subgrid)

Computes the layout data for a single data group with flow layout. The resulting layout is saved into the input objects.

Parameters :
Name Type Optional Description
dataGroup DataGroup No

the data group that should be laid out into a grid using the flow algorithm

subgrid Subgrid No

the object that holds the context of the computed layouting unit

Returns : void
Protected computeGridLayout
computeGridLayout(dataGroup: DataGroup, subgrid: Subgrid)

Computes the layout data for a single data group with grid layout. The resulting layout is saved into the input objects.

Parameters :
Name Type Optional Description
dataGroup DataGroup No

the data group that should be laid out into a grid

subgrid Subgrid No

the object that holds the context of the computed layouting unit

Returns : void
Public computeLayoutData
computeLayoutData(dataGroups: Array)

Computes the layout data for the tasks grid layout and sets the result to the public properties of this class that are then bound to HTML.

Parameters :
Name Type Optional Description
dataGroups Array<DataGroup> No

the data groups that should be laid out

Returns : void
Protected computeLegacyLayout
computeLegacyLayout(dataGroup: DataGroup, subgrid: Subgrid)

Computes the layout data for a single data group with legacy layout. The resulting layout is saved into the input objects.

The legacy layout forces the number of columns to be 4 and logs a warning if this was not the case.

Parameters :
Name Type Optional Description
dataGroup DataGroup No

the data group that should be laid out into a grid using the legacy algorithm used in NAE versions < 4.0.0

subgrid Subgrid No

the object that holds the context of the computed layouting unit

Returns : void
Protected createComponentTaskRefDataGroup
createComponentTaskRefDataGroup(originalDataGroup: DataGroup, taskRef: TaskRefField, title?: string)

Creates a new data group that contains only the passed task ref. The information about nesting and parent task/case are preserved from the passed original data group.

Parameters :
Name Type Optional Description
originalDataGroup DataGroup No

source of the information about parent task/case

taskRef TaskRefField No

the task ref that will be added to the new data group

title string Yes

optional title for taskref

Returns : DataGroup
Protected createSubgridId
createSubgridId(dataGroup: DataGroup)
Parameters :
Name Type Optional
dataGroup DataGroup No
Returns : string
Protected flowFields
flowFields(dataGroup: DataGroup, subgrid: Subgrid, fieldWidth: number)

Lays out the fields from left to right, from top to bottom. If the width is greater than 1 and the block of flowed fields doesn't fit neatly into the columns (if the width of a single field is not a divisor of column count), then the entire block of fields is left aligned. The last row of fields is aligned to the left, center or right based on the data groups property. If the last row cannot be aligned to the exact center it is offset one grid tile to the left.

Parameters :
Name Type Optional Description
dataGroup DataGroup No

the data group that should be laid out into a grid

subgrid Subgrid No

the object that holds the context of the computed layouting unit

fieldWidth number No

the number of grid tiles, that should be occupied by each field

Returns : void
Protected getElementDimensions
getElementDimensions(grid: Array>, x: number, y: number, subgrid: Subgrid)

Determines the dimensions of the element in the grid with its top-left corner at the specified position

Parameters :
Name Type Optional Description
grid Array<Array<string>> No

the grid of elements

x number No

the X coordinate of the desired elements top-left corner

y number No

the Y coordinate of the desired elements top-left corner

subgrid Subgrid No

the object that holds the context of the computed layouting unit

Returns : literal type

the width and height of the specified element

Public getItemAlignment
getItemAlignment(item: DatafieldGridLayoutElement)

the layout configuration specified by the process

Parameters :
Name Type Optional Description
item DatafieldGridLayoutElement No

an object containing grid element information

Returns : string

the Angular flex layouts alignment property for individual data fields based on the layout configuration specified by the process

Public getNumberOfFormColumns
getNumberOfFormColumns()
Returns : number

the number of columns as specified by the tasks layout property, or 4 if no value is specified.

Protected hasRequiredComponentProperty
hasRequiredComponentProperty(component: DataRefComponent, propertyName: string, propertyValue: string)
Parameters :
Name Type Optional
component DataRefComponent No
propertyName string No
propertyValue string No
Returns : boolean
Protected initializeLocalFieldLayout
initializeLocalFieldLayout(dataGroups: Array)

Passes over all the fields in the provided data groups and if they are visible, initializes their local layout attribute.

Parameters :
Name Type Optional Description
dataGroups Array<DataGroup> No

the containers of the fields that should have their local layout initialized

Returns : void
Protected isAreaEmpty
isAreaEmpty(grid: Array>, x: number, y: number, width: number, height: number)

Determines whether the specified area in the grid contains no elements, or not

Parameters :
Name Type Optional Description
grid Array<Array<string>> No

the grid of elements

x number No

the X coordinate of the tested areas top-left corner

y number No

the Y coordinate of the tested areas top-left corner

width number No

the width of the tested area

height number No

the height of the tested area

Returns : boolean
Protected isLastRow
isLastRow(index: number, dataGroup: DataGroup, fieldsPerRow: number)

Note that if the last row contains enough elements to be completely filled this method returns false.

Parameters :
Name Type Optional Description
index number No

the index of the checked element within its data group

dataGroup DataGroup No

the checked data group

fieldsPerRow number No

the number of fields that is required to fill an entire row

Returns : boolean

whether the field at the given index is within the last row of elements of the provided data group. Note that if the last row contains enough elements to be completely filled this method returns false.

Public isPaperView
isPaperView()
Returns : any
Protected isTaskRef
isTaskRef(field: DataField<unknown>)
Parameters :
Name Type Optional
field DataField<unknown> No
Returns : boolean
Protected newGridRow
newGridRow(width: number, content: string)
Parameters :
Name Type Optional Default value Description
width number No

width of the row

content string No ''

the value that should be used to fill all elements in the row

Returns : Array<string>
ngOnDestroy
ngOnDestroy()
Returns : void
Protected occupySpace
occupySpace(grid: Array>, y: number, x: number, width: number, value: string, height: number, checkOccupants)

Fills the specified rectangular area with the specified value in the provided grid.

If the specified area contains values other than the empty string an error will be thrown. If a check is performed an error will be thrown if elements are found.

Parameters :
Name Type Optional Default value Description
grid Array<Array<string>> No

the grid that should be modified

y number No

the 0 based row index of the top-left corner of the filled area

x number No

the 0 based column index of the top-left corner of the filled area

width number No

the width of the filled area

value string No

the value that is set into every cell of the grid inside the specified area

height number No 1

the height of the filled area

checkOccupants No true

whether the filled area should be checked for other elements. If a check is performed an error will be thrown if elements are found.

Returns : void
Protected preprocessDashboardTaskRef
preprocessDashboardTaskRef(preprocessedDataGroups: Array, rawDataGroups: Array)

Identifies data groups that represent dashboard tiles, removes them from the data group array and passes them onto the task ref field instance.

Parameters :
Name Type Optional
preprocessedDataGroups Array<DataGroup> No
rawDataGroups Array<DataGroup> No
Returns : Array<DataGroup>

an array of data group objects that does not contain dashboard tiles referenced by the dashboard task ref

Protected preprocessDataGroups
preprocessDataGroups(dataGroups: Array)

Clones the content of the data groups to prevent unintentional memory accesses to source data. Rearranges the data groups to accommodate taskrefs. Filters out hidden and forbidden fields. Populates dashboard task ref fields with dashboard tile data.

Parameters :
Name Type Optional
dataGroups Array<DataGroup> No
Returns : Array<DataGroup>

the preprocessed data groups

Protected rearrangeDataGroups
rearrangeDataGroups(dataGroups: Array)

Clones the content of the data groups to prevent unintentional memory accesses to source data. Rearranges the data groups to accommodate taskrefs. Determines if the data groups contain a dashboard task ref field.

Parameters :
Name Type Optional
dataGroups Array<DataGroup> No

the preprocessed data groups with metadata

Protected removeEmptyRows
removeEmptyRows(grid: Array>, firstRow: number, lastRow)

Removes rows from the grid that only contain empty elements (are empty). The grid is modified in place.

If no value is provided, the grid will be checked from the first row.

If no value is provided the entire grid from the firstRow will be checked.

If a value that is smaller or equal to the firstRow is provided no checks will be preformed.

Parameters :
Name Type Optional Default value Description
grid Array<Array<string>> No

the grid that should have it's empty rows removed

firstRow number No 0

the 0 based index of the first row that should be checked. Use 0 to start from the beginning of the grid.

If no value is provided, the grid will be checked from the first row.

lastRow No Number.POSITIVE_INFINITY

the 0 based index of the row where the checking should end. The row with this index is not checked.

If no value is provided the entire grid from the firstRow will be checked.

If a value that is smaller or equal to the firstRow is provided no checks will be preformed.

Returns : void
Protected renderContent
renderContent(content: Array<Subgrid>)

Destroys the previous content and pushes the new content into the stream

Parameters :
Name Type Optional Default value Description
content Array<Subgrid> No []

the new content

Returns : void
Protected renderFields
renderFields(subgrids: Map<string | Subgrid>)
Parameters :
Name Type Optional
subgrids Map<string | Subgrid> No
Returns : void
Public resolveContentHeightCss
resolveContentHeightCss()

Computes component height if the rows attribute of task layout is set.

Returns : string
Public scrollingNeeded
scrollingNeeded()

Decides whether there is a fixed number of rows and it need-s scrolling

Returns : boolean
Protected shouldResolveTaskRefData
shouldResolveTaskRefData(dataRef: DataField<any>)
Parameters :
Name Type Optional
dataRef DataField<any> No
Returns : boolean
Protected splitDataGroupOnTaskRef
splitDataGroupOnTaskRef(dataGroup: DataGroup)

Sorts the input data group based on the Y coordinate of the fields and splits it into parts on the first task ref. If some fields appear before the first task ref they are extracted into a new startGroup. If some fields appear after the first task ref they are extracted into a new endGroup.

Parameters :
Name Type Optional
dataGroup DataGroup No
Returns : SplitDataGroup
Protected spreadFieldRenderingOverTime
spreadFieldRenderingOverTime(subgrids: Array<Subgrid>, iteration: number)
Parameters :
Name Type Optional Default value
subgrids Array<Subgrid> No
iteration number No 0
Returns : void
Protected trackByDatafields
trackByDatafields(index: number, element: DatafieldGridLayoutElement)
Parameters :
Name Type Optional
index number No
element DatafieldGridLayoutElement No
Returns : string

Properties

Protected _asyncRenderingConfig
Type : AsyncRenderingConfiguration
Protected _asyncRenderTimeout
Type : number
Protected _dataSource$
Type : BehaviorSubject<Array<Subgrid>>

The data fields that are currently displayed

Protected _defaultAlignment
Type : FieldAlignment
Protected _defaultCompactDirection
Type : DataGroupCompact
Protected _defaultHideEmptyRows
Type : DataGroupHideEmptyRows
Protected _defaultNumberOfCols
Type : number
Protected _existingSubgridIds
Type : Set<string>
Protected _subgridIdCounter
Type : IncrementingCounter
Protected _subTaskContent
Type : Subscription
Protected _subTaskEvent
Type : Subscription
Readonly DEFAULT_ASYNC_RENDERING_CONFIGURATION
Type : AsyncRenderingConfiguration
Default value : { batchSize: 4, batchDelay: 200, numberOfPlaceholders: 4, enableAsyncRenderingForNewFields: true, enableAsyncRenderingOnTaskExpand: true }
Readonly DEFAULT_COMPACT_DIRECTION
Default value : DataGroupCompact.NONE
Readonly DEFAULT_FIELD_ALIGNMENT
Default value : FieldAlignment.CENTER
Readonly DEFAULT_HIDE_EMPTY_ROWS
Default value : DataGroupHideEmptyRows.ALL
Readonly DEFAULT_LAYOUT_TYPE
Default value : DataGroupLayoutType.LEGACY
displayNoDataIcon
Default value : true
Decorators :
@Input()

Whether an icon should be displayed when the no data message is shown.

An icon is displayed by default

Public fieldTypeResource
Default value : FieldTypeResource

Exists to allow references to the enum in the HTML

hasDataToDisplay$
Type : Observable<boolean>

Emits true if there is at least one data field, that should be displayed. Emits false otherwise.

loading$
Type : LoadingEmitter

Indicates whether data is being loaded from backend, or if it is being processed.

noDataIcon
Type : string
Decorators :
@Input()

The icon that should be displayed when the task contains no data.

If a falsy value is provided the default icon is displayed

noDataText
Type : string
Decorators :
@Input()

The translate text that should be displayed when the task contains no data.

If a falsy value is provided the default text is displayed

Protected rowHeight
Type : number
Default value : 105

Defines the row height of one row in task content

Public taskContentService
Type : TaskContentService
taskEvent
Type : EventEmitter<TaskEventNotification>
Decorators :
@Output()

Emits notifications about task events

Public trackByFn
Default value : () => {...}
Public trackBySubgridFn
Default value : () => {...}

Accessors

taskId
gettaskId()
dataSource
getdataSource()
import {Component, EventEmitter, Inject, Input, OnDestroy, Optional, Output} from '@angular/core';
import {DatafieldGridLayoutElement} from '../model/datafield-grid-layout-element';
import {FieldConverterService} from '../services/field-converter.service';
import {TaskContentService} from '../services/task-content.service';
import {PaperViewService} from '../../navigation/quick-panel/components/paper-view.service';
import {LoggerService} from '../../logger/services/logger.service';
import {TaskEventNotification} from '../model/task-event-notification';
import {TaskEventService} from '../services/task-event.service';
import {DataGroup, DataGroupAlignment, ParentDataGroupInformation} from '../../resources/interface/data-groups';
import {TaskElementType} from '../model/task-content-element-type';
import {DataField} from '../../data-fields/models/abstract-data-field';
import {
    DataGroupCompact,
    DataGroupHideEmptyRows,
    DataGroupLayout,
    DataGroupLayoutType
} from '../../resources/interface/data-group-layout';
import {FieldAlignment} from '../../resources/interface/field-alignment';
import {FieldTypeResource} from '../model/field-type-resource';
import {LoadingEmitter} from '../../utility/loading-emitter';
import {BehaviorSubject, Observable, Subscription} from 'rxjs';
import {map} from 'rxjs/operators';
import {NAE_ASYNC_RENDERING_CONFIGURATION} from '../model/async-rendering-configuration-injection-token';
import {AsyncRenderingConfiguration} from '../model/async-rendering-configuration';
import {TaskRefDashboardConstants} from '../../data-fields/task-ref-field/model/task-ref-dashboard-constants';
import {TaskRefField} from '../../data-fields/task-ref-field/model/task-ref-field';
import {TaskRefComponents} from '../../data-fields/task-ref-field/model/task-ref-components';
import {SplitDataGroup} from '../model/split-data-group';
import {Subgrid} from '../model/subgrid';
import {IncrementingCounter} from '../../utility/incrementing-counter';
import {PreprocessedDataGroups} from '../model/preprocessed-data-groups';
import {TaskRefDashboardTile} from '../../data-fields/task-ref-field/model/task-ref-dashboard-tile';
import {Component as DataRefComponent} from '../../data-fields/models/component';

@Component({
    selector: 'ncc-abstract-task-content',
    template: ''
})
export abstract class AbstractTaskContentComponent implements OnDestroy {
    readonly DEFAULT_LAYOUT_TYPE = DataGroupLayoutType.LEGACY;
    readonly DEFAULT_FIELD_ALIGNMENT = FieldAlignment.CENTER;
    readonly DEFAULT_ASYNC_RENDERING_CONFIGURATION: AsyncRenderingConfiguration = {
        batchSize: 4,
        batchDelay: 200,
        numberOfPlaceholders: 4,
        enableAsyncRenderingForNewFields: true,
        enableAsyncRenderingOnTaskExpand: true
    };
    readonly DEFAULT_COMPACT_DIRECTION = DataGroupCompact.NONE;
    readonly DEFAULT_HIDE_EMPTY_ROWS = DataGroupHideEmptyRows.ALL;

    /**
     * Indicates whether data is being loaded from backend, or if it is being processed.
     */
    loading$: LoadingEmitter;
    /**
     * Emits `true` if there is at least one data field, that should be displayed. Emits `false` otherwise.
     */
    hasDataToDisplay$: Observable<boolean>;

    /**
     * Exists to allow references to the enum in the HTML
     */
    public fieldTypeResource = FieldTypeResource;

    /**
     * The translate text that should be displayed when the task contains no data.
     *
     * If a falsy value is provided the default text is displayed
     */
    @Input() noDataText: string;
    /**
     * The icon that should be displayed when the task contains no data.
     *
     * If a falsy value is provided the default icon is displayed
     */
    @Input() noDataIcon: string;
    /**
     * Whether an icon should be displayed when the no data message is shown.
     *
     * An icon is displayed by default
     */
    @Input() displayNoDataIcon = true;
    /**
     * Emits notifications about task events
     */
    @Output() taskEvent: EventEmitter<TaskEventNotification>;
    /**
     * The data fields that are currently displayed
     */
    protected _dataSource$: BehaviorSubject<Array<Subgrid>>;
    protected _subTaskContent: Subscription;
    protected _subTaskEvent: Subscription;
    protected _asyncRenderingConfig: AsyncRenderingConfiguration;
    protected _asyncRenderTimeout: number;

    protected _defaultAlignment: FieldAlignment;
    protected _defaultCompactDirection: DataGroupCompact;
    protected _defaultHideEmptyRows: DataGroupHideEmptyRows;
    protected _defaultNumberOfCols: number;

    protected _subgridIdCounter: IncrementingCounter;
    protected _existingSubgridIds: Set<string>;

    /**
     * Defines the row height of one row in task content
     */
    protected rowHeight = 105;

    protected constructor(protected _fieldConverter: FieldConverterService,
                          public taskContentService: TaskContentService,
                          protected _paperView: PaperViewService,
                          protected _logger: LoggerService,
                          @Optional() protected _taskEventService: TaskEventService = null,
                          @Optional() @Inject(NAE_ASYNC_RENDERING_CONFIGURATION)
                              asyncRenderingConfiguration: AsyncRenderingConfiguration = null) {
        this._asyncRenderingConfig = {...this.DEFAULT_ASYNC_RENDERING_CONFIGURATION};
        if (asyncRenderingConfiguration !== null) {
            Object.assign(this._asyncRenderingConfig, asyncRenderingConfiguration);
        }

        this.loading$ = new LoadingEmitter(true);
        this._dataSource$ = new BehaviorSubject<Array<Subgrid>>([]);
        this.hasDataToDisplay$ = this._dataSource$.pipe(map(data => {
            return data.length !== 0;
        }));

        this._subTaskContent = this.taskContentService.$shouldCreate.subscribe(data => {
            if (data.length !== 0) {
                this.computeDefaultLayoutConfiguration();
                this.computeLayoutData(data);
            } else {
                this.renderContent();
            }
            this.loading$.off();
        });
        if (this._taskEventService !== null) {
            this.taskEvent = new EventEmitter<TaskEventNotification>();
            this._subTaskEvent = _taskEventService.taskEventNotifications$.subscribe(event => {
                this.taskEvent.emit(event);
            });
        }
    }

    ngOnDestroy(): void {
        this.loading$.complete();
        this._dataSource$.complete();
        this._subTaskContent.unsubscribe();
        if (this.taskEvent) {
            this.taskEvent.complete();
        }
        if (this._subTaskEvent) {
            this._subTaskEvent.unsubscribe();
        }
        if (this._asyncRenderTimeout !== undefined) {
            window.clearTimeout(this._asyncRenderTimeout);
        }
    }

    public get taskId(): string {
        return this.taskContentService.task.stringId;
    }

    public get dataSource(): Array<Subgrid> {
        return this._dataSource$.getValue();
    }

    /**
     * @returns the number of columns as specified by the tasks layout property, or 4 if no value is specified.
     */
    public getNumberOfFormColumns(): number {
        if (!this.taskContentService.task
            || !this.taskContentService.task.layout
            || this.taskContentService.task.layout.cols === undefined) {
            return 4;
        } else {
            return this.taskContentService.task.layout.cols;
        }
    }

    public isPaperView() {
        return this._paperView.paperView;
    }

    /**
     * @param item an object containing grid element information
     * @returns the Angular flex layouts alignment property for individual data fields based on
     * the layout configuration specified by the process
     */
    public getItemAlignment(item: DatafieldGridLayoutElement): string {
        if (item.alignment) {
            return item.alignment;
        }

        const fieldAlignment = item.item?.localLayout?.alignment ?? this._defaultAlignment;

        let alignment;
        switch (fieldAlignment) {
            case FieldAlignment.TOP:
                alignment = 'start';
                break;
            case FieldAlignment.CENTER:
                alignment = 'center';
                break;
            case FieldAlignment.BOTTOM:
                alignment = 'end';
                break;
        }

        item.alignment = 'space-between ' + alignment;
        return item.alignment;
    }

    /**
     * Computes component height if the rows attribute of task layout is set.
     */
    public resolveContentHeightCss(): string {
        if (this.scrollingNeeded()) {
            return this.taskContentService.task.layout.rows * this.rowHeight + 'px';
        } else {
            return '100%';
        }
    }

    /**
     * Decides whether there is a fixed number of rows and it need-s scrolling
     */
    public scrollingNeeded(): boolean {
        return this.taskContentService.task
            && !!this.taskContentService.task.layout
            && !!this.taskContentService.task.layout.rows
            && this.taskContentService.task.layout.rows > 0;
    }

    /**
     * @returns the default alignment for the displayed task that can be overridden by fields individual property.
     * If te task specifies no default value the [global default]{@link AbstractTaskContentComponent#DEFAULT_FIELD_ALIGNMENT}
     * value is returned.
     */
    protected computeDefaultLayoutConfiguration() {
        this._defaultAlignment = this.taskContentService.task?.layout?.fieldAlignment ?? this.DEFAULT_FIELD_ALIGNMENT;
        this._defaultCompactDirection = this.taskContentService.task?.layout?.compactDirection ?? this.DEFAULT_COMPACT_DIRECTION;
        this._defaultHideEmptyRows = this.taskContentService.task?.layout?.hideEmptyRows ?? this.DEFAULT_HIDE_EMPTY_ROWS;
        this._defaultNumberOfCols = this.getNumberOfFormColumns();
    }

    /**
     * Computes the layout data for the tasks grid layout and sets the result to the public properties
     * of this class that are then bound to HTML.
     * @param dataGroups the data groups that should be laid out
     */
    public computeLayoutData(dataGroups: Array<DataGroup>) {
        if (!this.taskContentService.task) {
            this.renderContent();
            return;
        }

        this._subgridIdCounter = new IncrementingCounter();
        this._existingSubgridIds = new Set<string>();

        const result = new Map<string, Subgrid>();

        dataGroups = this.preprocessDataGroups(dataGroups);

        const defaultLayout = this.taskContentService.task.layout && this.taskContentService.task.layout.type
            ? this.taskContentService.task.layout.type
            : this.DEFAULT_LAYOUT_TYPE;

        dataGroups.forEach(group => {
            if (!group.layout) {
                group.layout = {rows: undefined, cols: undefined, type: defaultLayout};
            }
            if (!group.layout.type) {
                group.layout.type = defaultLayout;
            }

            const subgrid = new Subgrid(
                this.createSubgridId(group),
                group.layout.cols ?? this._defaultNumberOfCols,
                this._asyncRenderingConfig
            );

            if (group.title !== undefined) {
                const title = subgrid.addTitle(group);
                subgrid.addRow(this.newGridRow(subgrid.cols, title.gridAreaId));
            }

            switch (group.layout.type) {
                case DataGroupLayoutType.GRID:
                    this.computeGridLayout(group, subgrid);
                    break;
                case DataGroupLayoutType.FLOW:
                    this.computeFlowLayout(group, subgrid);
                    break;
                case DataGroupLayoutType.LEGACY:
                    this.computeLegacyLayout(group, subgrid);
                    break;
                default:
                    throw new Error(`Unknown task layout type '${this.taskContentService.task.layout.type}'`);
            }

            subgrid.finalize();
            result.set(subgrid.subgridId, subgrid);
        });

        this.renderFields(result);
    }

    protected renderFields(subgrids: Map<string, Subgrid>) {
        const subgridsArray = Array.from(subgrids.values());

        if (!this._asyncRenderingConfig.enableAsyncRenderingForNewFields
            && !(this._asyncRenderingConfig.enableAsyncRenderingOnTaskExpand && this.taskContentService.isExpanding)) {
            subgridsArray.forEach(subgrid => {
                subgrid.displayAllFields();
            });
            this.renderContent(subgridsArray);
            return;
        }

        if (!(this.taskContentService.isExpanding && this._asyncRenderingConfig.enableAsyncRenderingOnTaskExpand)) {
            this._dataSource$.value.forEach(oldSubgrid => {
                if (subgrids.has(oldSubgrid.subgridId)) {
                    subgrids.get(oldSubgrid.subgridId).determineKeptFields(oldSubgrid);
                }
            });
        }

        this.renderContent(subgridsArray);
        this.spreadFieldRenderingOverTime(subgridsArray);
    }

    protected spreadFieldRenderingOverTime(subgrids: Array<Subgrid>, iteration = 0) {
        this._asyncRenderTimeout = undefined;
        if (iteration < subgrids.length) {
            this._asyncRenderTimeout = window.setTimeout(() => {
                subgrids[iteration].renderContentOverTime(() => {
                    this.spreadFieldRenderingOverTime(subgrids, iteration + 1);
                }, iteration === 0);
            });
        }
    }

    /**
     * Clones the content of the data groups to prevent unintentional memory accesses to source data.
     * Rearranges the data groups to accommodate taskrefs. Filters out hidden and forbidden fields.
     * Populates dashboard task ref fields with dashboard tile data.
     * @param dataGroups
     * @returns the preprocessed data groups
     */
    protected preprocessDataGroups(dataGroups: Array<DataGroup>): Array<DataGroup> {
        const rearrangeResult = this.rearrangeDataGroups(dataGroups);
        let unfilteredDataGroups = rearrangeResult.dataGroups;

        if (rearrangeResult.containsDashboardTaskRef) {
            unfilteredDataGroups = this.preprocessDashboardTaskRef(rearrangeResult.dataGroups, dataGroups);
        }

        return this.cloneAndFilterHidden(unfilteredDataGroups);
    }

    /**
     * Clones the content of the data groups to prevent unintentional memory accesses to source data.
     * Rearranges the data groups to accommodate taskrefs.
     * Determines if the data groups contain a dashboard task ref field.
     * @param dataGroups
     * @returns the preprocessed data groups with metadata
     */
    protected rearrangeDataGroups(dataGroups: Array<DataGroup>): PreprocessedDataGroups {
        dataGroups = this.cloneDataGroups(dataGroups);
        this.initializeLocalFieldLayout(dataGroups);

        let containsDashboard = false;
        const result = [];
        for (let i = 0; i < dataGroups.length; i++) {
            const group = dataGroups[i];
            if (!group.fields.some(f => this.isTaskRef(f) && this.shouldResolveTaskRefData(f))) {
                result.push(group);
                continue;
            }
            const split = this.splitDataGroupOnTaskRef(group);
            if (split.startGroup !== undefined) {
                result.push(split.startGroup);
            }

            if (!!split.taskRef.component?.name) {
                result.push(this.createComponentTaskRefDataGroup(group, split.taskRef));
                containsDashboard = split.taskRef.component.name === TaskRefComponents.DASHBOARD;
            }

            if (split.taskRef.value.length === 0 || split.endGroup === undefined) {
                if (split.endGroup !== undefined) {
                    dataGroups.splice(i + 1, 0, split.endGroup);
                }
                continue;
            }

            const directChild = dataGroups[i + 1]; // the data group that immediately follows IS ALWAYS a direct child
            let firstNonDescendantIndex = i + 2;
            for (; firstNonDescendantIndex < dataGroups.length; firstNonDescendantIndex++) {
                const nextGroup = dataGroups[firstNonDescendantIndex];
                if (nextGroup.nestingLevel === undefined || nextGroup.nestingLevel < directChild.nestingLevel) {
                    // end of the block of nested data groups
                    break;
                }
                if (nextGroup.nestingLevel === directChild.nestingLevel
                    && (nextGroup.parentTaskRefId !== split.taskRef.stringId
                        || !split.taskRef.value.some(reffedTaskId => reffedTaskId === nextGroup.parentTaskId))) {
                    break;
                }
            }

            dataGroups.splice(firstNonDescendantIndex, 0, split.endGroup);
        }

        return {
            dataGroups: result,
            containsDashboardTaskRef: containsDashboard
        };
    }

    /**
     * Creates a duplicate of the provided data group array.
     *
     * Only the data groups are cloned, the fields are only copied as references.
     * @param dataGroups the data groups that should be cloned
     * @returns the duplicated data groups
     */
    protected cloneDataGroups(dataGroups: Array<DataGroup>): Array<DataGroup> {
        return dataGroups.map(group => {
            const g = {...group};
            g.fields = g.fields.map(field => field);
            return g;
        });

    }

    /**
     * Passes over all the fields in the provided data groups and if they are visible, initializes their local layout attribute.
     * @param dataGroups the containers of the fields that should have their local layout initialized
     */
    protected initializeLocalFieldLayout(dataGroups: Array<DataGroup>): void {
        for (const g of dataGroups) {
            for (const f of g.fields) {
                f.resetLocalLayout();
            }
        }
    }

    /**
     * Creates a duplicate of the provided data group array and filters away any fields and data groups that are marked as hidden.
     * Because of the duplication the filtering doesn't affect the original instances and they remain unchanged.
     * @param dataGroups the data groups that should be filtered
     * @returns the filtered duplicated data groups
     */
    protected cloneAndFilterHidden(dataGroups: Array<DataGroup>): Array<DataGroup> {
        const result = dataGroups.map(group => {
            const g = {...group};
            g.fields = g.fields.filter(field => !field.behavior.hidden && !field.behavior.forbidden).map(field => {
                field.resetLocalLayout();
                return field;
            });
            return g;
        });

        return result.filter(group => group.fields.length > 0);
    }

    protected isTaskRef(field: DataField<unknown>): boolean {
        return field instanceof TaskRefField;
    }

    /**
     * Sorts the input data group based on the Y coordinate of the fields and splits it into parts on the first task ref.
     * If some fields appear before the first task ref they are extracted into a new [startGroup]{@link SplitDataGroup#startGroup}.
     * If some fields appear after the first task ref they are extracted into a new [endGroup]{@link SplitDataGroup#endGroup}.
     * @param dataGroup
     * @protected
     */
    protected splitDataGroupOnTaskRef(dataGroup: DataGroup): SplitDataGroup {
        dataGroup.fields.sort((a, b) => a.localLayout.y - b.localLayout.y);
        const taskRefPosition = dataGroup.fields.findIndex(f => this.isTaskRef(f));
        const result: SplitDataGroup = {
            taskRef: dataGroup.fields[taskRefPosition] as TaskRefField
        };

        if (taskRefPosition !== 0) {
            result.startGroup = {
                title: dataGroup.title,
                alignment: dataGroup.alignment,
                layout: dataGroup.layout,
                stretch: dataGroup.stretch,
                fields: dataGroup.fields.slice(0, taskRefPosition),
            };
        }

        if (taskRefPosition !== dataGroup.fields.length - 1) {
            result.endGroup = {
                title: undefined,
                alignment: dataGroup.alignment,
                layout: dataGroup.layout,
                stretch: dataGroup.stretch,
                fields: dataGroup.fields.slice(taskRefPosition + 1),
            };
            result.endGroup.fields.forEach(f => {
                f.localLayout.y = f.localLayout.y - result.taskRef.localLayout.y - 1;
            });
        }

        return result;
    }

    /**
     * Creates a new data group that contains only the passed task ref.
     * The information about nesting and parent task/case are preserved from the passed original data group.
     * @param originalDataGroup source of the information about parent task/case
     * @param taskRef the task ref that will be added to the new data group
     * @param title optional title for taskref
     * @protected
     */
    protected createComponentTaskRefDataGroup(originalDataGroup: DataGroup, taskRef: TaskRefField, title?: string): DataGroup {
        return {
            fields: [taskRef],
            alignment: undefined,
            stretch: false,
            title: title,
            parentTaskId: originalDataGroup.parentTaskId,
            parentTransitionId: originalDataGroup.parentTransitionId,
            parentCaseId: originalDataGroup.parentCaseId,
            parentTaskRefId: originalDataGroup.parentTaskRefId,
            nestingLevel: originalDataGroup.nestingLevel,
            layout: originalDataGroup.layout
        }
    }

    /**
     * Identifies data groups that represent dashboard tiles,
     * removes them from the data group array and passes them onto the task ref field instance.
     * @param preprocessedDataGroups
     * @param rawDataGroups
     * @returns an array of data group objects that does not contain dashboard tiles referenced by the dashboard task ref
     * @protected
     */
    protected preprocessDashboardTaskRef(preprocessedDataGroups: Array<DataGroup>, rawDataGroups: Array<DataGroup>): Array<DataGroup> {
        // TODO support more than one dashboard task ref in a task
        let dashboardTaskRefField: TaskRefField;
        let dashboardParentInformation: ParentDataGroupInformation;
        for (const dg of preprocessedDataGroups) {
            for (const field of dg.fields) {
                if (this.isTaskRef(field) && field.component?.name === TaskRefComponents.DASHBOARD) {
                    dashboardTaskRefField = field as TaskRefField;
                    dashboardParentInformation = {
                        parentTaskId: dg.parentTaskId,
                        parentTransitionId: dg.parentTransitionId,
                        parentCaseId: dg.parentCaseId,
                        parentTaskRefId: dg.parentTaskRefId,
                        nestingLevel: dg.nestingLevel
                    }
                    break;
                }
            }
            if (dashboardTaskRefField !== undefined) {
                break;
            }
        }
        if (dashboardTaskRefField === undefined) {
            this._logger.error('preprocessDashboardTaskRef method was called on task content without dashboard task refs!');
            return preprocessedDataGroups;
        }

        const tiles = new Map<string, TaskRefDashboardTile>();

        // TODO resolve transitive tile content
        const nonDashboardDataGroups = [];
        for (const dg of preprocessedDataGroups) {
            if (dg.parentTaskRefId !== dashboardTaskRefField.stringId) {
                nonDashboardDataGroups.push(dg);
                continue;
            }
            if (tiles.has(dg.parentTaskId)) {
                const tile = tiles.get(dg.parentTaskId);
                tile.dataGroups.push(dg);
            } else {
                tiles.set(dg.parentTaskId, {dataGroups: [dg]});
            }
        }

        for (const rdg of rawDataGroups) {
            if (rdg.parentTaskId === dashboardParentInformation.parentTaskId
                && rdg.parentTransitionId === dashboardParentInformation.parentTransitionId
                && rdg.parentCaseId === dashboardParentInformation.parentCaseId
                && rdg.parentTaskRefId === dashboardParentInformation.parentTaskRefId
                && rdg.nestingLevel === dashboardParentInformation.nestingLevel) {
                for (const filed of rdg.fields) {
                    if (filed.stringId === TaskRefDashboardConstants.DASHBOARD_COLS) {
                        dashboardTaskRefField.dashboardCols = filed.value;
                    } else if (filed.stringId === TaskRefDashboardConstants.DASHBOARD_ROWS) {
                        dashboardTaskRefField.dashboardRows = filed.value;
                    }
                }
            }
        }

        dashboardTaskRefField.dashboardTiles = Array.from(tiles.values());
        return nonDashboardDataGroups;
    }

    /**
     * Computes the layout data for a single data group with grid layout. The resulting layout is saved into the input objects.
     * @param dataGroup the data group that should be laid out into a grid
     * @param subgrid the object that holds the context of the computed layouting unit
     */
    protected computeGridLayout(dataGroup: DataGroup, subgrid: Subgrid) {
        const localGrid: Array<Array<string>> = [];
        dataGroup.fields = [...dataGroup.fields].sort((f1 , f2) => f1.layout.y === f2.layout.y ? f1.layout.x - f2.layout.x : f1.layout.y - f2.layout.y)
        dataGroup.fields.forEach(dataField => {
            if (!dataField.localLayout
                || dataField.localLayout.x === undefined
                || dataField.localLayout.y === undefined
                || !dataField.localLayout.rows
                || !dataField.localLayout.cols) {
                throw new Error(
                    `You cannot use 'grid' layout without specifying the layout of the data fields (field ID: ${dataField.stringId})`);
            }

            while (localGrid.length < dataField.localLayout.y + dataField.localLayout.rows) {
                localGrid.push(this.newGridRow(subgrid.cols));
            }

            const fieldElement = subgrid.addField(dataField, this._fieldConverter.resolveType(dataField));
            this.occupySpace(localGrid, dataField.localLayout.y, dataField.localLayout.x,
                dataField.localLayout.cols, fieldElement.gridAreaId, dataField.localLayout.rows);
        });

        this.collapseGridEmptySpace(localGrid, dataGroup.layout, subgrid);

        localGrid.forEach(localGridRow => subgrid.addRow(localGridRow));
    }

    /**
     * Applies the empty row removal and field compacting rules based on the provided layout configuration.
     *
     * The input grid is modified in place.
     * @param grid the state of the grid that should be modified
     * @param layout configuration of the applied compacting rules
     * @param subgrid the object that holds the context of the computed layouting unit
     */
    protected collapseGridEmptySpace(grid: Array<Array<string>>, layout: DataGroupLayout, subgrid: Subgrid) {
        const hideRows = layout.hideEmptyRows ?? this._defaultHideEmptyRows;

        if (hideRows === DataGroupHideEmptyRows.ALL) {
            this.removeEmptyRows(grid);
        }

        switch (layout.compactDirection ?? this._defaultCompactDirection) {
            case DataGroupCompact.UP:
                this.compactFieldsUp(grid, hideRows, subgrid);
                break;
        }
    }

    /**
     * Moves any element as far UP as it can go. Elements that were originally declared in the same row might end up in different rows.
     * Resulting trailing empty rows are removed.
     *
     * The input grid is modified in place.
     * @param grid the state of the grid that should be modified
     * @param hideRows configuration for empty row removal during the compacting process
     * @param subgrid the object that holds the context of the computed layouting unit
     */
    protected compactFieldsUp(grid: Array<Array<string>>, hideRows: DataGroupHideEmptyRows, subgrid: Subgrid) {
        for (let rowIndex = 0; rowIndex < grid.length; rowIndex++) {
            const row = grid[rowIndex];

            for (let columnIndex = 0; columnIndex < row.length; columnIndex++) {
                if (row[columnIndex] !== '') {
                    continue;
                }

                let foundElement = false;
                let foundElementRowIndex = rowIndex + 1;
                for (; foundElementRowIndex < grid.length; foundElementRowIndex++) {
                    if (grid[foundElementRowIndex][columnIndex] !== '') {
                        foundElement = true;
                        break;
                    }
                }
                if (!foundElement) {
                    continue;
                }

                if (columnIndex > 0 && grid[foundElementRowIndex][columnIndex - 1] === grid[foundElementRowIndex][columnIndex]) {
                    continue;
                }

                const elementDimensions = this.getElementDimensions(grid, columnIndex, foundElementRowIndex, subgrid);

                if (this.isAreaEmpty(grid, columnIndex, rowIndex, elementDimensions.width, foundElementRowIndex - rowIndex)) {
                    const element = grid[foundElementRowIndex][columnIndex];
                    this.occupySpace(grid, foundElementRowIndex, columnIndex, elementDimensions.width, '', elementDimensions.height, false);
                    this.occupySpace(grid, rowIndex, columnIndex, elementDimensions.width, element, elementDimensions.height, false);

                    if (hideRows !== DataGroupHideEmptyRows.NONE) {
                        // we only check the rows potentially cleared by moving the grid element up
                        this.removeEmptyRows(grid, foundElementRowIndex, foundElementRowIndex + elementDimensions.height);
                    }
                }
            }
        }
    }

    /**
     * Removes rows from the grid that only contain empty elements (are empty). The grid is modified in place.
     * @param grid the grid that should have it's empty rows removed
     * @param firstRow the 0 based index of the first row that should be checked. Use 0 to start from the beginning of the grid.
     *
     * If no value is provided, the grid will be checked from the first row.
     *
     * @param lastRow the 0 based index of the row where the checking should end. The row with this index is not checked.
     *
     * If no value is provided the entire grid from the `firstRow` will be checked.
     *
     * If a value that is smaller or equal to the `firstRow` is provided no checks will be preformed.
     */
    protected removeEmptyRows(grid: Array<Array<string>>, firstRow = 0, lastRow = Number.POSITIVE_INFINITY) {
        let i = firstRow;
        while (i < grid.length && i < lastRow) {
            if (grid[i].every(element => element === '')) {
                grid.splice(i, 1);
            } else {
                i++;
            }
        }
    }

    /**
     * Determines the dimensions of the element in the grid with its top-left corner at the specified position
     * @param grid the grid of elements
     * @param x the X coordinate of the desired elements top-left corner
     * @param y the Y coordinate of the desired elements top-left corner
     * @param subgrid the object that holds the context of the computed layouting unit
     * @returns the width and height of the specified element
     */
    protected getElementDimensions(grid: Array<Array<string>>, x: number, y: number, subgrid: Subgrid): { width: number, height: number } {
        const element = grid[y][x];
        let width = 1;
        while (x + width < subgrid.cols && grid[y][x + width] === element) {
            width++;
        }
        let height = 1;
        while (y + height < grid.length && grid[y + height][x] === element) {
            height++;
        }
        return {width, height};
    }

    /**
     * Determines whether the specified area in the grid contains no elements, or not
     * @param grid the grid of elements
     * @param x the X coordinate of the tested areas top-left corner
     * @param y the Y coordinate of the tested areas top-left corner
     * @param width the width of the tested area
     * @param height the height of the tested area
     */
    protected isAreaEmpty(grid: Array<Array<string>>, x: number, y: number, width: number, height: number): boolean {
        for (let i = y; i < y + height; i++) {
            for (let j = x; j < x + width; j++) {
                if (grid[i][j] !== '') {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Computes the layout data for a single data group with flow layout. The resulting layout is saved into the input objects.
     * @param dataGroup the data group that should be laid out into a grid using the flow algorithm
     * @param subgrid the object that holds the context of the computed layouting unit
     */
    protected computeFlowLayout(dataGroup: DataGroup, subgrid: Subgrid) {
        this.flowFields(dataGroup, subgrid, 1);
    }

    /**
     * Computes the layout data for a single data group with legacy layout. The resulting layout is saved into the input objects.
     *
     * The legacy layout forces the number of columns to be 4 and logs a warning if this was not the case.
     * @param dataGroup the data group that should be laid out into a grid using the legacy algorithm used in NAE versions < 4.0.0
     * @param subgrid the object that holds the context of the computed layouting unit
     */
    protected computeLegacyLayout(dataGroup: DataGroup, subgrid: Subgrid) {
        if (subgrid.cols !== 4) {
            subgrid.cols = 4;
            this._logger.warn(`Task with id '${this.taskContentService.task.stringId}' has a data group with legacy layout with a `
                + `non-default number of columns. If you want to use a layout with different number of columns than 2 use a different `
                + `layout type instead.`);
        }

        this.flowFields(dataGroup, subgrid, 2);
    }

    /**
     * Lays out the fields from left to right, from top to bottom. If the width is greater than 1 and the block of flowed fields doesn't fit
     * neatly into the columns (if the width of a single field is not a divisor of column count), then the entire block of fields is
     * left aligned. The last row of fields is aligned to the left, center or right based on the data groups property.
     * If the last row cannot be aligned to the exact center it is offset one grid tile to the left.
     * @param dataGroup the data group that should be laid out into a grid
     * @param subgrid the object that holds the context of the computed layouting unit
     * @param fieldWidth the number of grid tiles, that should be occupied by each field
     */
    protected flowFields(dataGroup: DataGroup, subgrid: Subgrid, fieldWidth: number) {
        const fieldsPerRow = Math.floor(subgrid.cols / fieldWidth);
        const maxXPosition = fieldWidth * (fieldsPerRow - 1);

        let xPosition = 0;
        const localGrid: Array<Array<string>> = [];
        dataGroup.fields.forEach((dataField, dataFieldCount) => {
            const fieldElement = subgrid.addField(dataField, this._fieldConverter.resolveType(dataField));
            if (dataGroup.stretch) {
                subgrid.addRow(this.newGridRow(subgrid.cols, fieldElement.gridAreaId));
                return; // continue
            }
            // else
            if (xPosition === 0) {
                localGrid.push(this.newGridRow(subgrid.cols));
            }
            if (xPosition === 0 && this.isLastRow(dataFieldCount, dataGroup, fieldsPerRow)) {
                const fieldsInLastRow = dataGroup.fields.length % fieldsPerRow;
                const rowWidth = maxXPosition + fieldWidth;
                if (dataGroup.alignment === DataGroupAlignment.END) {
                    xPosition = rowWidth - fieldWidth * fieldsInLastRow;
                } else if (dataGroup.alignment === DataGroupAlignment.CENTER) {
                    xPosition = Math.floor((rowWidth - fieldsInLastRow * fieldWidth) / 2);
                }
            }
            this.occupySpace(localGrid, localGrid.length - 1, xPosition, fieldWidth, fieldElement.gridAreaId);

            xPosition += fieldWidth;
            if (xPosition > maxXPosition) {
                xPosition = 0;
            }
        });
        localGrid.forEach(row => subgrid.addRow(row));
    }

    /**
     * @param width width of the row
     * @param content the value that should be used to fill all elements in the row
     */
    protected newGridRow(width: number, content = ''): Array<string> {
        return Array(width).fill(content);
    }

    /**
     * Fills the specified rectangular area with the specified value in the provided grid.
     *
     * If the specified area contains values other than the empty string an error will be thrown.
     * @param grid the grid that should be modified
     * @param y the 0 based row index of the top-left corner of the filled area
     * @param x the 0 based column index of the top-left corner of the filled area
     * @param width the width of the filled area
     * @param value the value that is set into every cell of the grid inside the specified area
     * @param height the height of the filled area
     * @param checkOccupants whether the filled area should be checked for other elements.
     * If a check is performed an error will be thrown if elements are found.
     */
    protected occupySpace(grid: Array<Array<string>>, y: number, x: number, width: number,
                          value: string, height = 1, checkOccupants = true) {
        for (let j = y; j < y + height; j++) {
            for (let i = x; i < x + width; i++) {
                if (checkOccupants && grid[j][i] !== '') {
                    throw new Error(`Cannot place element ${value} into the grid layout, because it's space (x: ${i}, y: ${j})` +
                        ` is already occupied by another element (${grid[j][i]})`);
                }
                grid[j][i] = value;
            }
        }
    }

    /**
     * @param index the index of the checked element within its data group
     * @param dataGroup the checked data group
     * @param fieldsPerRow the number of fields that is required to fill an entire row
     * @returns whether the field at the given index is within the last row of elements of the provided data group.
     * Note that if the last row contains enough elements to be completely filled this method returns `false`.
     */
    protected isLastRow(index: number, dataGroup: DataGroup, fieldsPerRow: number): boolean {
        return index + fieldsPerRow >= dataGroup.fields.length;
    }

    /**
     * Destroys the previous content and pushes the new content into the stream
     * @param content the new content
     */
    protected renderContent(content: Array<Subgrid> = []): void {
        this._dataSource$.value.forEach(subgrid => subgrid.destroy());
        this._dataSource$.next(content);
    }

    protected createSubgridId(dataGroup: DataGroup): string {
        let idBase: string;
        if (dataGroup.parentTaskId !== undefined) {
            idBase = [dataGroup.parentTaskId, dataGroup.parentTaskRefId, dataGroup.nestingLevel].join('#');
        } else {
            idBase = 'root';
        }
        let id = idBase;
        while (this._existingSubgridIds.has(id)) {
            id = idBase + this._subgridIdCounter.next();
        }
        this._existingSubgridIds.add(id);
        return id;
    }

    public trackBySubgridFn = (index: number, subgrid: Subgrid) => subgrid.subgridId;

    public trackByFn = (index: number, element: DatafieldGridLayoutElement) => this.trackByDatafields(index, element);

    protected trackByDatafields(index: number, element: DatafieldGridLayoutElement) {
        switch (element.type) {
            case TaskElementType.BLANK:
                return element.gridAreaId + '-' + this.taskContentService.$shouldCreateCounter.getValue();
            case TaskElementType.LOADER:
                return element.gridAreaId + '-L-' + this.taskContentService.$shouldCreateCounter.getValue();
            case TaskElementType.DATA_GROUP_TITLE:
                return element.title + '-' + this.taskContentService.$shouldCreateCounter.getValue();
            default:
                return element.item.stringId + '-' + this.taskContentService.$shouldCreateCounter.getValue();
        }
    }

    protected shouldResolveTaskRefData(dataRef: DataField<any>): boolean {
        if (!!dataRef.component) {
            return this.hasRequiredComponentProperty(dataRef.component, "resolve_data", "true");
        }
        return true;
    }

    protected hasRequiredComponentProperty(component: DataRefComponent, propertyName: string, propertyValue: string): boolean {
        return  component?.properties != null
        && !!component?.properties[propertyName]
        && component?.properties[propertyName] === propertyValue;
    }
}
Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""