{"version":3,"file":"app-102f8747.f584a1446b2fc034d810.bundle.js","mappings":"AAGA","sources":["webpack://lernplattform-admin-frontend/./src/resources/elements/formula-editor/formula-editor.html","webpack://lernplattform-admin-frontend/./src/resources/elements/formula-editor/formula-key-value-list/formula-key-value-list.html","webpack://lernplattform-admin-frontend/./src/resources/elements/formula-editor/formula-editor.ts","webpack://lernplattform-admin-frontend/./src/resources/elements/formula-editor/formula-key-value-list/formula-key-value-list.ts"],"sourcesContent":["// Module\nvar code = \"<template class=\\\"card formula-editor\\\"> <template if.to-view=\\\"existingFormula\\\"></template> <div class=\\\"card-header\\\"> <h1 class=\\\"card-title\\\" t=\\\"elements.formulaEditor.formulaEditor\\\"></h1> </div> <div class=\\\"formula-editor__content\\\"> <div class=\\\"formula-editor__main\\\"> <div class=\\\"formula-editor__result\\\"> <p class=\\\"card-title\\\" t=\\\"elements.formulaEditor.result\\\"></p> <div class=\\\"formula-editor__result-settings\\\"> <div class=\\\"form-group mr-6\\\"> <label class=\\\"form-label\\\" t=\\\"elements.formulaEditor.name\\\"></label> <input class=\\\"form-control\\\" attach-focus t=\\\"[placeholder]elements.formulaEditor.name\\\" value.to-view=\\\"resultName\\\" input.trigger=\\\"handleEditingResultName($event)\\\" type=\\\"text\\\"> </div> <div class=\\\"form-group mr-6\\\"> <label class=\\\"form-label\\\" t=\\\"elements.formulaEditor.arguments\\\"></label> <input class=\\\"form-control\\\" t=\\\"[placeholder]elements.formulaEditor.argumentsPlaceholder\\\" value.to-view=\\\"resultArguments\\\" input.trigger=\\\"handleEditingResultArguments($event)\\\" type=\\\"text\\\"> </div> <div class=\\\"form-group mr-6\\\"> <label class=\\\"form-label\\\" t=\\\"elements.formulaEditor.unitInBrackets\\\"></label> <input class=\\\"form-control\\\" t=\\\"[placeholder]elements.formulaEditor.unit\\\" value.to-view=\\\"resultUnit\\\" input.trigger=\\\"handleEditingResultUnit($event)\\\" type=\\\"text\\\"> </div> <div class=\\\"formula-editor__element formula-editor__element-equal\\\">=</div> </div> </div> <div ref=\\\"editorParentElement\\\" class=\\\"formula-editor__formula-editing ${ formulaIsValid ? 'formula-editor__formula-editing--is-valid' : ''}\\\"> <p class=\\\"card-title\\\" t=\\\"elements.formulaEditor.formula\\\"></p> <div ref=\\\"editorShadowElement\\\" class=\\\"formula-editor__editor-shadow\\\" style.to-view=\\\"{'width': `${editorWidth}px`}\\\"></div> <div class=\\\"formula-editor__editor\\\"> <div repeat.for=\\\"row of editorRows\\\" class=\\\"formula-editor__editor-row\\\"> <div repeat.for=\\\"element of row.elements\\\"> <drop-zone if.to-view=\\\"element.isContent\\\" dnd-active.to-view=\\\"dndActive\\\" context.to-view=\\\"dndContext\\\" config.to-view=\\\"element.dropzoneConfig\\\" sub-dropzone-calc-mode.to-view=\\\"SubDropzoneCalcMode.Horizontal\\\"> <draggable payload.to-view=\\\"{\\n                             existingPayloadId: element.sequenceIndex,\\n                           }\\\" dnd-active.to-view=\\\"dndActive\\\" context.to-view=\\\"dndContext\\\" drag-dom-element.to-view=\\\"draggedContentElement\\\"> <div ref=\\\"draggedContentElement\\\" class=\\\"formula-editor__element formula-editor__element--drag\\\"> <span>${element.innerHtml}</span> <span if.to-view=\\\"element.innerHtmlUnit\\\" class=\\\"formula-editor__element-unit\\\">&nbsp;${element.innerHtmlUnit}</span> </div> <div class=\\\"formula-editor__element ${ currentContentElementEditing.sequenceIndex === element.sequenceIndex ? 'formula-editor__element--editing' : '' } ${ element.sequenceIndex === dndDraggedId ? 'formula-editor__element--is-dragged' : '' }\\\" css=\\\"${element.styles}\\\" style.to-view=\\\"{'background-color': `${element.backgroundColorVar}`}\\\" click.delegate=\\\"handleEditContentElementStart(element.sequenceIndex)\\\"> <template if.to-view=\\\"element.sequenceIndex !== dndDraggedId\\\"> <span>${element.innerHtml}</span> <span if.to-view=\\\"element.innerHtmlUnit\\\" class=\\\"formula-editor__element-unit\\\">&nbsp;${element.innerHtmlUnit}</span> </template> </div> </draggable> </drop-zone> <drop-zone else class=\\\"formula-editor__spacer ${ !dndTargetDisplayRows[$parent.$index][$index].isTargeted && dndTargetDisplayRows[$parent.$index][$index].position === DndSequentialDropzonePosition.First ? 'formula-editor__spacer--first' : '' } ${ !dndTargetDisplayRows[$parent.$index][$index].isTargeted && dndTargetDisplayRows[$parent.$index][$index].position === DndSequentialDropzonePosition.Inbetween ? 'formula-editor__spacer--inbetween' : '' } ${ dndTargetDisplayRows[$parent.$index][$index].isTargeted && dndTargetDisplayRows[$parent.$index][$index].position === DndSequentialDropzonePosition.First ? 'formula-editor__spacer--first-targeted' : '' } ${ dndTargetDisplayRows[$parent.$index][$index].isTargeted && dndTargetDisplayRows[$parent.$index][$index].position === DndSequentialDropzonePosition.Inbetween ? 'formula-editor__spacer--inbetween-targeted' : '' } ${ dndTargetDisplayRows[$parent.$index][$index].isTargeted && dndTargetDisplayRows[$parent.$index][$index].position === DndSequentialDropzonePosition.Last ? 'formula-editor__spacer--last-targeted' : '' }\\\" css=\\\"${ dndTargetDisplayRows[$parent.$index][$index].position === DndSequentialDropzonePosition.Last ? element.styles : '' }\\\" dnd-active.to-view=\\\"dndActive\\\" context.to-view=\\\"dndContext\\\" config.to-view=\\\"element.dropzoneConfig\\\"> <div class=\\\"formula-editor__dnd-target ${ dndTargetDisplayRows[$parent.$index][$index].isTargeted ? 'formula-editor__dnd-target--targeted' : '' }\\\"></div> </drop-zone> </div> </div> </div> </div> <alert-message if.to-view=\\\"!isFormulaValid && formulaSequence.length\\\" message.to-view=\\\"{ tk: `elements.formulaEditor.invalidFormula` }\\\" message-style=\\\"alert-danger\\\" show-icon.to-view=\\\"true\\\"></alert-message> <div else class=\\\"formula-editor__invalid-message-placeholder\\\"></div> <div class=\\\"formula-editor__formula-display\\\"> <div ref=\\\"katexFormulaElement\\\" class=\\\"formula-editor__formula\\\"></div> </div> </div> <div if.to-view=\\\"contentElementEditMode === ContentElementEditMode.Inactive\\\" class=\\\"formula-editor__sidebar\\\"> <p class=\\\"card-title\\\" t=\\\"globals.add\\\"></p> <div class=\\\"formula-editor__available-elements-header\\\"> <p class=\\\"form-label\\\" t=\\\"elements.formulaEditor.elements\\\"></p> <info-icon t=\\\"[info-text]elements.formulaEditor.clickOrDragToAdd\\\" class=\\\"ml-2\\\"></info-icon> </div> <div class=\\\"formula-editor__available-operators\\\"> <draggable repeat.for=\\\"operatorType of availableOperatorTypes\\\" payload.to-view=\\\"{\\n                                     source: DndPayloadSource.New,\\n                                     createNewType: {\\n                                       elementType: FormulaElementType.Operator,\\n                                       operatorType: operatorType,\\n                                     },\\n                                   }\\\" drag-dom-element.to-view=\\\"draggedAvailableOperator\\\" dnd-active.to-view=\\\"dndActive\\\" context.to-view=\\\"dndContext\\\" click.delegate=\\\"handleAddElement(-1, FormulaElementType.Operator, operatorType)\\\"> <div ref=\\\"draggedAvailableOperator\\\" class=\\\"formula-editor__available-operator formula-editor__available-operator--drag\\\">${getOperatorAsString(operatorType)}</div> <div class=\\\"formula-editor__available-operator\\\">${getOperatorAsString(operatorType)}</div> </draggable> </div> <draggable payload.to-view=\\\"{\\n                                   source: DndPayloadSource.New,\\n                                   createNewType: {\\n                                     elementType: FormulaElementType.Variable,\\n                                   },\\n                                 }\\\" drag-dom-element.to-view=\\\"draggedAvailableVariable\\\" dnd-active.to-view=\\\"dndActive\\\" context.to-view=\\\"dndContext\\\" click.delegate=\\\"handleAddElement(-1, FormulaElementType.Variable, null)\\\"> <div ref=\\\"draggedAvailableVariable\\\" class=\\\"formula-editor__available-variable-value formula-editor__available-variable-value--drag\\\" t=\\\"elements.formulaEditor.variable\\\"></div> <div class=\\\"formula-editor__available-variable-value\\\" t=\\\"elements.formulaEditor.variable\\\"></div> </draggable> <draggable payload.to-view=\\\"{\\n                                   source: DndPayloadSource.New,\\n                                   createNewType: {\\n                                     elementType: FormulaElementType.Value,\\n                                   },\\n                                 }\\\" drag-dom-element.to-view=\\\"draggedAvailableValue\\\" dnd-active.to-view=\\\"dndActive\\\" context.to-view=\\\"dndContext\\\" click.delegate=\\\"handleAddElement(-1, FormulaElementType.Value, null)\\\"> <div ref=\\\"draggedAvailableValue\\\" class=\\\"formula-editor__available-variable-value formula-editor__available-variable-value--drag\\\" t=\\\"elements.formulaEditor.value\\\"></div> <div class=\\\"formula-editor__available-variable-value\\\" t=\\\"elements.formulaEditor.value\\\"></div> </draggable> <draggable payload.to-view=\\\"{\\n                                   source: DndPayloadSource.New,\\n                                   createNewType: {\\n                                     elementType: FormulaElementType.KeyValueList,\\n                                   },\\n                                 }\\\" drag-dom-element.to-view=\\\"draggedAvailableKeyValueList\\\" dnd-active.to-view=\\\"dndActive\\\" context.to-view=\\\"dndContext\\\" click.delegate=\\\"handleAddElement(-1, FormulaElementType.KeyValueList, null)\\\"> <div ref=\\\"draggedAvailableKeyValueList\\\" class=\\\"formula-editor__available-variable-value formula-editor__available-variable-value--drag\\\" t=\\\"elements.formulaEditor.keyValueList\\\"></div> <div class=\\\"formula-editor__available-variable-value\\\" t=\\\"elements.formulaEditor.keyValueList\\\"></div> </draggable> </div> <div else class=\\\"formula-editor__sidebar\\\"> <p class=\\\"card-title\\\" t=\\\"globals.edit\\\"></p> <div if.to-view=\\\"currentContentElementEditing.type === FormulaElementType.Variable\\n                      || currentContentElementEditing.type === FormulaElementType.KeyValueList\\\" class=\\\"form-group\\\"> <label class=\\\"form-label\\\" t=\\\"elements.formulaEditor.name\\\"></label> <input class=\\\"form-control\\\" attach-focus t=\\\"[placeholder]elements.formulaEditor.name\\\" value.to-view=\\\"currentContentElementEditingName\\\" input.trigger=\\\"handleContentElementEditingName($event)\\\" type=\\\"text\\\"> </div> <div if.to-view=\\\"currentContentElementEditing.type === FormulaElementType.Value\\\" class=\\\"form-group\\\"> <label class=\\\"form-label\\\" t=\\\"elements.formulaEditor.value\\\"></label> <input class=\\\"form-control ${ currentContentElementEditing.valueValid ? '' : 'is-invalid' }\\\" attach-focus t=\\\"[placeholder]elements.formulaEditor.value\\\" value.two-way=\\\"currentContentElementEditingValue\\\" type=\\\"text\\\"> <div if.to-view=\\\"!currentContentElementEditing.valueValid\\\" class=\\\"invalid-feedback\\\" t=\\\"elements.formulaEditor.invalidValue\\\"></div> </div> <div if.to-view=\\\"currentContentElementEditing.type === FormulaElementType.Variable\\n                      || currentContentElementEditing.type === FormulaElementType.Unit\\\" class=\\\"form-group\\\"> <label class=\\\"form-label\\\" t=\\\"elements.formulaEditor.unit\\\"></label> <input class=\\\"form-control\\\" t=\\\"[placeholder]elements.formulaEditor.unit\\\" value.to-view=\\\"currentContentElementEditingUnit\\\" input.trigger=\\\"handleContentElementEditingUnit($event)\\\" type=\\\"text\\\"> </div> <div if.to-view=\\\"currentContentElementEditing.type === FormulaElementType.Variable\\\" class=\\\"form-group\\\"> <label class=\\\"form-label\\\" t=\\\"elements.formulaEditor.minValue\\\"></label> <input class=\\\"form-control ${ currentContentElementEditingMinMaxValid ? '' : 'is-invalid' }\\\" t=\\\"[placeholder]elements.formulaEditor.value\\\" value.to-view=\\\"currentContentElementEditingMin\\\" input.trigger=\\\"handleCurrentContentElementEditingMinValueChanged($event)\\\" type=\\\"text\\\"> </div> <div if.to-view=\\\"currentContentElementEditing.type === FormulaElementType.Variable\\\" class=\\\"form-group formula-editor__max-value\\\"> <label class=\\\"form-label\\\" t=\\\"elements.formulaEditor.maxValue\\\"></label> <input class=\\\"form-control ${ currentContentElementEditingMinMaxValid ? '' : 'is-invalid' }\\\" t=\\\"[placeholder]elements.formulaEditor.value\\\" value.to-view=\\\"currentContentElementEditingMax\\\" input.trigger=\\\"handleCurrentContentElementEditingMaxValueChanged($event)\\\" type=\\\"text\\\"> </div> <formula-key-value-list if.to-view=\\\"currentContentElementEditing.type === FormulaElementType.KeyValueList\\\" class=\\\"formula-editor__key-value-list\\\" key-value-list.to-view=\\\"currentContentElementEditingKeyValueEntries\\\" key-value-list-updated.trigger=\\\"updateCurrentContentElementEditingKeyValueEntries($event)\\\" can-submit-updated.trigger=\\\"updateCurrentContentElementEditingCanSubmit($event)\\\"> </formula-key-value-list> <button if.to-view=\\\"currentContentElementEditing.type !== FormulaElementType.Operator\\\" class=\\\"btn btn-primary mb-4 ${ !contentElementEditCanSubmit || (currentContentElementEditing.type === FormulaElementType.Value && !currentContentElementEditing.valueValid) || (currentContentElementEditing.type === FormulaElementType.Variable && !currentContentElementEditingMinMaxValid) ? 'formula-editor__button--disabled' : '' }\\\" disabled.to-view=\\\"!contentElementEditCanSubmit\\n                                 || (currentContentElementEditing.type === FormulaElementType.Value && !currentContentElementEditing.valueValid)\\n                                 || (currentContentElementEditing.type === FormulaElementType.Variable && !currentContentElementEditingMinMaxValid)\\\" click.delegate=\\\"handleEditContentElementSave()\\\" t=\\\"globals.saveChange\\\"></button> <button class=\\\"btn btn-secondary mb-4\\\" click.delegate=\\\"handleEditContentElementCancel()\\\" t=\\\"globals.cancel\\\"></button> <button class=\\\"btn btn-outline-danger\\\" click.delegate=\\\"handleEditContentElementDelete()\\\" t=\\\"elements.formulaEditor.deleteElement\\\"></button> </div> </div> <div class=\\\"card-footer formula-editor__footer\\\"> <alert-message class=\\\"mb-0 ${ formulaCanSubmit ? 'formula-editor__alarm--hidden' : '' }\\\" message.to-view=\\\"{ tk: `elements.formulaEditor.formulaLengthError` }\\\" message-style=\\\"alert-danger\\\" show-icon.to-view=\\\"true\\\"></alert-message> <div> <button type=\\\"submit\\\" class=\\\"btn btn-primary mr-6 ${ !formulaCanSubmit || !contentElementEditCanSubmit || (currentContentElementEditing.type === FormulaElementType.Value && !currentContentElementEditing.valueValid) || (currentContentElementEditing.type === FormulaElementType.Variable && !currentContentElementEditingMinMaxValid) ? 'formula-editor__button--disabled' : '' }\\\" disabled.to-view=\\\"!formulaCanSubmit\\n                                 || !contentElementEditCanSubmit\\n                                 || (currentContentElementEditing.type === FormulaElementType.Value && !currentContentElementEditing.valueValid)\\n                                 || (currentContentElementEditing.type === FormulaElementType.Variable && !currentContentElementEditingMinMaxValid)\\\" click.delegate=\\\"handleSave()\\\" t.to-view=\\\"saveText\\\"></button> <button class=\\\"btn btn-secondary\\\" click.delegate=\\\"handleCancel()\\\" t=\\\"globals.cancel\\\"></button> </div> </div> </template> \";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"formula-key-value-list\\\"> <label class=\\\"form-label\\\" t=\\\"elements.formulaEditor.keyValueListEntriesHeader\\\"></label> <div class=\\\"formula-key-value-list__rows\\\" ref=\\\"listElement\\\"> <div repeat.for=\\\"entry of keyValueEntries\\\" class=\\\"formula-key-value-list__row mb-2\\\" mouseover.trigger=\\\"setHoveredEntryIndex($index)\\\" mouseout.trigger=\\\"resetHoveredEntryIndex()\\\"> <button-standard class=\\\"formula-key-value-list__icon-btn ${ hoveredEntryIndex === $index && $index > 0 ? '' : 'formula-key-value-list__icon-btn--hidden' }\\\" icon-name=\\\"System/Arrow - Up\\\" click.delegate=\\\"moveUp($index)\\\" btn-size=\\\"icon\\\" tooltip-tk=\\\"elements.formulaEditor.moveUp\\\" btn-color=\\\"secondary\\\"></button-standard> <input class=\\\"form-control ${ entry.showKeyInvalid && !entry.isKeyValid ? 'is-invalid' : '' }\\\" t=\\\"[placeholder]elements.formulaEditor.keyAsText\\\" value.to-view=\\\"entry.key\\\" input.delegate=\\\"updateKey($index, $event)\\\" type=\\\"text\\\"> <input class=\\\"form-control ${ entry.showValueInvalid && !entry.isValueValid ? 'is-invalid' : '' }\\\" t=\\\"[placeholder]elements.formulaEditor.value\\\" value.to-view=\\\"entry.valueAsString\\\" input.delegate=\\\"updateValue($index, $event)\\\" type=\\\"text\\\"> <button-standard class=\\\"formula-key-value-list__icon-btn ${ hoveredEntryIndex === $index ? '' : 'formula-key-value-list__icon-btn--hidden' }\\\" icon-name=\\\"Uce/Delete\\\" click.delegate=\\\"removeEntry($index)\\\" tooltip-tk=\\\"globals.delete\\\" btn-size=\\\"icon\\\" btn-color=\\\"secondary\\\"></button-standard> </div> </div> <button class=\\\"btn btn-secondary mb-4 mt-2 formula-key-value-list__button\\\" click.delegate=\\\"addEntry()\\\" t=\\\"elements.formulaEditor.addKeyValueEntry\\\"></button> </template> \";\n// Exports\nexport default code;","import { autoinject, bindable, computedFrom, observable } from 'aurelia-framework';\nimport { I18N } from 'aurelia-i18n';\nimport { selectState } from 'common/aurelia-decorators/select-state';\nimport { SubscriptionManager, SubscriptionManagerService } from 'common/aurelia-services/SubscriptionManagerService';\nimport { ContentEditingInsertModeSelector } from 'common/constants/content.editing.constants';\nimport {\n  FORMULA_KATEX_RENDER_OPTIONS,\n  FORMULA_NUMBER_VALIDATION_REGEX,\n  FORMULA_TEXT_VALIDATION_REGEX,\n  FORMULA_UNIT_COLOR,\n  FORMULA_REFERENCE_END_IDENTIFIER,\n  FORMULA_REFERENCE_START_IDENTIFIER,\n  FORMULA_VARIABLE_PREFIX,\n  FORMULA_KEY_VALUE_LIST_PREFIX,\n  FORMULA_MAX_LENGTH_DISPLAY,\n  FORMULA_MAX_LENGTH_LATEX,\n  FORMULA_MAX_LENGTH_FORMULA,\n  FORMULA_MAX_LENGTH_UNIQUE_VARIABLES,\n  FORMULA_MAX_LENGTH_UNIQUE_KEY_VALUE_LISTS,\n} from 'common/constants/formula.constants';\nimport {\n  FormulaElementType,\n  FormulaOperatorType,\n  IFormulaDto,\n  IFormulaElement,\n  IFormulaKeyValueList,\n  IFormulaOperator,\n  IFormulaValue,\n  IFormulaVariable,\n  IKeyValueEntry,\n  IUniqueFormulaItemBase,\n  IUpdateFormulaDto,\n} from 'common/dto/course/content/formula.interface';\nimport { blockFormulaSelectorCreator } from 'common/store/selectors/content.context.selectors';\nimport { ArrayUtils } from 'common/utils/ArrayUtils';\nimport { assertNotNullOrUndefined } from 'common/utils/assert';\nimport { DomUtils } from 'common/utils/DomUtils';\nimport { FormulaUtils } from 'common/utils/FormulaUtils';\nimport { FunctionUtils } from 'common/utils/FunctionUtils';\nimport { LogUtils } from 'common/utils/LogUtils';\nimport katex from 'katex';\nimport {\n  DndActivityEventData,\n  DndContext,\n  DndDropEventData,\n  DndInsertModeSequential,\n  DndManager,\n  DndManagerService,\n  DndPayloadUniversal,\n  DndSequentialDropzonePosition,\n  IDndDropzoneConfig,\n} from '../../../services/DndManagerService';\nimport { IContentEditorBlockEditorBlock } from '../../dialogs/content-editor-block-editor-dialog/content-editor-block-editor-dialog';\nimport { SubDropzoneCalcMode } from '../drag-and-drop/drop-zone';\nimport { CanSubmitUpdatedEvent, KeyValueListUpdatedEvent } from './formula-key-value-list/formula-key-value-list';\n\nexport interface IUniqueFormulaItem {\n  name:string;\n  occurrences: number[]; // referencing sequence indices of occurrences\n}\n\n\ninterface IFormulaDisplayElement extends IFormulaElement {\n  shadowElement: HTMLElement | null;\n  innerHtml: string | null;\n  innerHtmlUnit: string | null;\n}\n\nenum SpecialLatexElementType {\n  SpecialClose = 'special-close', // .. }\n  FractionStart = 'fraction-start', // .. \\frac{\n  Variable = 'variable',\n  Value = 'value',\n  KeyValueList = 'key-value-list',\n}\n\nexport type LatexElementType = FormulaOperatorType | SpecialLatexElementType;\n\nexport type LatexSequenceElement = {\n  type: LatexElementType;\n  latexBasicDisplayString: string;\n  latexUnitString?: string;\n  valueString?: string;\n};\n\n/**\n * collection of data used by view:\n * isContent .. either display a spacer (with target indicator) or a content element\n * backgroundColorVar .. bg col depends on element type and operator type\n * dropzoneConfig .. every element is also a dropzone\n * innerHtml .. content elements show this string\n * innerHtmlUnit .. additional unit string for display of variables and values\n * sequenceIndex .. used to identify current editing element and as dnd payload id\n * styles .. used to provide a predefined width\n */\ntype ElementInRow = {\n  isContent: boolean;\n  backgroundColorVar?: string;\n  dropzoneConfig: IDndDropzoneConfig;\n  innerHtml: string;\n  innerHtmlUnit: string | null;\n  sequenceIndex?: number;\n  styles?: string;\n};\n\ntype Row = {\n  index: number;\n  elements: ElementInRow[];\n};\n\nenum ContentElementEditMode {\n  Inactive = 'inactive',\n  Operator = 'operator',\n  Value = 'value',\n  Variable = 'variable',\n  KeyValueList = 'key-value-list',\n}\n\ntype CurrentContentElementEditing = {\n  sequenceIndex: number;\n  type: FormulaElementType;\n  attributesBackup: ContentElementAttributes;\n  valueValid: boolean;\n};\n\nenum LimitType {\n  Min = 'min',\n  Max = 'max',\n  Both = 'both',\n}\n\ntype ContentElementNumber = {\n  asValueString: string | null;\n  asDisplayString: string;\n  asNumber: number | null;\n};\n\ntype ContentElementAttributes = {\n  name: string | null;\n  value: ContentElementNumber;\n  min: ContentElementNumber;\n  max: ContentElementNumber;\n  keyValueEntries: IKeyValueEntry[] | null;\n  unit: string | null;\n};\n\ntype CheckContentElement = {\n  type: FormulaElementType;\n  operator: FormulaOperatorType | null;\n};\n\ntype DndTargetDisplayRow = Record<number, DndTargetDisplayInfo>;\n\ntype DndTargetDisplayInfo = {\n  isTargeted: boolean;\n  position: DndSequentialDropzonePosition;\n  targetedByAddresses: DndTargetAddress[];\n};\n\ntype DndTargetAddress = {\n  row: number;\n  element: number;\n};\n\nexport type DndFormulaElement = {\n  elementType: FormulaElementType;\n  operatorType?: FormulaOperatorType;\n  id?: number;\n};\n\nexport type EditFormulaOkEvent = CustomEvent<never>;\nexport type EditFormulaCancelEvent = CustomEvent<never>;\n\n@autoinject()\nexport class FormulaEditor {\n  @bindable() formulaId: number | null = null;\n  @bindable() courseId: number | null = null;\n  @bindable() contextIdentifier = '';\n  @bindable() block: IContentEditorBlockEditorBlock | null = null;\n  @selectState(FormulaEditor, blockFormulaSelectorCreator, ['contextIdentifier', 'formulaId']) private existingFormula?: IFormulaDto | null;\n  private editExisting = false;\n\n  @observable() private resultName = '';\n  @observable() private resultUnit = '';\n  @observable() private resultArguments = '';\n  private isFormulaValid = true;\n  private formulaCanSubmit = true;\n  private operators: Record<number, IFormulaOperator> = {};\n  private variables: Record<number, IFormulaVariable> = {};\n  private values: Record<number, IFormulaValue> = {};\n  private keyValueLists: Record<number, IFormulaKeyValueList> = {};\n  private nextOperatorId = 0;\n  private nextVariableId = 0;\n  private nextValueId = 0;\n  private nextKeyValueListId = 0;\n  private formulaSequence: IFormulaDisplayElement[] = [];\n  private uniqueVariables: IUniqueFormulaItem[] = [];\n  private uniqueKeyValueLists: IUniqueFormulaItem[] = [];\n  private latexSequence: LatexSequenceElement[] = [];\n  private variableAsText = '';\n  private valueAsText = '';\n  private keyAsText = '';\n  private keyValueListAsText = '';\n  private displayLatexString = ''; // for display only\n  private formulaLatexString = ''; // for calculation only\n\n  private subscriptionManager: SubscriptionManager;\n  private editorDialogElement: HTMLElement;\n  private editorParentElement: HTMLDivElement | null = null;\n  private editorShadowElement: HTMLDivElement | null = null;\n  private katexFormulaElement: HTMLDivElement | null = null;\n  private editorRowsBuffer: Row[] = [];\n  private editorRows: Row[] = []; // actual array carrying all display information\n  private editorWidth = 0;\n  private maxContentElementWidth = 0;\n  private dndTargetWidth = 4;\n  private defaultGap = 8;\n  private targetedGap = 8; // with width animation: 22; ... without: 8;\n\n  // edit existing content\n  private contentElementEditMode = ContentElementEditMode.Inactive;\n  private contentElementEditCanSubmit = true;\n  private currentContentElementEditingMinMaxValid = true;\n  private currentContentElementEditing: CurrentContentElementEditing | null = null;\n  private numberValidationRegex = new RegExp(FORMULA_NUMBER_VALIDATION_REGEX);\n  private textValidationRegex = new RegExp(FORMULA_TEXT_VALIDATION_REGEX);\n  private currentContentElementEditingKeyValueEntries: IKeyValueEntry[] = [];\n  @observable() private currentContentElementEditingName = '';\n  @observable() private currentContentElementEditingValue = '';\n  @observable() private currentContentElementEditingUnit = '';\n  @observable() private currentContentElementEditingMin = '';\n  @observable() private currentContentElementEditingMax = '';\n\n  // drag and drop specific\n  private dndActive = false;\n  private dndContext = DndContext.Formula;\n  private dndManager: DndManager<DndFormulaElement> | null = null;\n  private dndPayload: DndPayloadUniversal<DndFormulaElement> | null = null;\n  private dndDraggedId: number | null = null;\n  private dndTargetDisplayRows: DndTargetDisplayRow[] = [];\n  private dndTargetedList: DndTargetAddress[] = [];\n\n  // enums for view\n  private ContentElementEditMode = ContentElementEditMode;\n  private FormulaElementType = FormulaElementType;\n  private ContentEditingInsertModeSelector = ContentEditingInsertModeSelector;\n  private SubDropzoneCalcMode = SubDropzoneCalcMode;\n  private DndSequentialDropzonePosition = DndSequentialDropzonePosition;\n\n  constructor(\n    private dndManagerService: DndManagerService,\n    private i18n: I18N,\n    subscriptionManagerService: SubscriptionManagerService,\n    element: Element,\n  ) {\n    this.editorDialogElement = element as HTMLElement;\n    this.subscriptionManager = subscriptionManagerService.createSubscriptionManager();\n  }\n\n  protected attached(): void {\n    this.dndManager = this.dndManagerService.getDndManager<DndFormulaElement>(this.dndContext);\n    this.dndManager.subscribePayloadChanged(this, this.dndPayloadChangedHandler.bind(this));\n    this.dndManager.subscribeToDragActivity(this, this.dndDragActivityHandler.bind(this));\n    this.dndManager.subscribeDrop(this, this.onDrop.bind(this));\n\n    // prepare placeholder texts for empty variable names and empty values\n    this.variableAsText = this.i18n.tr('elements.formulaEditor.variable');\n    this.valueAsText = this.i18n.tr('elements.formulaEditor.value');\n    this.keyAsText = this.i18n.tr('elements.formulaEditor.keyAsText');\n    this.keyValueListAsText = this.i18n.tr('elements.formulaEditor.keyValueList');\n\n    assertNotNullOrUndefined(this.editorParentElement, 'editor parent element cannot be null');\n    this.subscriptionManager.subscribeToElementResize(this.editorParentElement, () => this.updateEditorView());\n\n    this.editExisting = this.formulaId != null;\n    if(this.editExisting) this.parseToView();\n  }\n\n  protected detached(): void {\n    this.dndManager?.disposeSubscriptions(this);\n    this.subscriptionManager.disposeSubscriptions();\n  }\n\n  public dndPayloadChangedHandler(payload: DndPayloadUniversal<DndFormulaElement> | null): void {\n    this.dndPayload = payload;\n    this.dndActive = payload != null;\n    this.dndDraggedId = this.dndPayload?.existingPayloadId ?? null;\n  }\n\n  public async dndDragActivityHandler(dndInfo: DndActivityEventData<IDndDropzoneConfig>): Promise<void> {\n    if(this.dndPayload == null) return;\n\n    // filter dropzone targetting activities directly neighbouring current payload (existingPayloadId .. sequenceIndex)\n    if(this.dndPayload.existingPayloadId != null\n      && (dndInfo.dropTarget.sequentialOrder === this.dndPayload.existingPayloadId\n        || (dndInfo.dropTarget.insertModeSequential === DndInsertModeSequential.After\n          && dndInfo.dropTarget.sequentialOrder === this.dndPayload.existingPayloadId - 1)\n        || (dndInfo.dropTarget.insertModeSequential === DndInsertModeSequential.Before\n          && dndInfo.dropTarget.sequentialOrder === this.dndPayload.existingPayloadId + 1))\n    ) {\n      return;\n    }\n\n    /*\n     * certain leave events should be delayed because the next enter event can set the same target.\n     * if theses leave events are not delayed the display will flicker.\n     * delay all leave events except those between calculated subdropzones by dropzone (... innerEvent = true)\n     * tests have shown 100ms can be too little, 120 worked fine so far.\n     */\n    if(!dndInfo.enterEvent && !dndInfo.dropTarget.innerEvent) await FunctionUtils.timeout(120);\n    this.calcDndTargetDisplays(dndInfo.dropTarget, dndInfo.enterEvent);\n  }\n\n  private resetDndTargetDisplays(): void {\n    for(const targeted of this.dndTargetedList) {\n      this.dndTargetDisplayRows[targeted.row][targeted.element].isTargeted = false;\n      this.dndTargetDisplayRows[targeted.row][targeted.element].targetedByAddresses = [];\n    }\n    this.dndTargetedList = [];\n    this.dndActive = false;\n    this.dndDraggedId = null;\n  }\n\n  /**\n   * in calcDndTargetDisplaySettings two types of target address lists are managed for two specific purposes\n   * this is required to handle multiple parallel targeted targets. it necessary to allow this to avoid flickering\n   * when the gap between a leave and enter event on different dropzones for the same target occurs.\n   * 1. dndTargetedList is a list of currently targeted targets, it is used for easy resetting at the end of dnd\n   * 2. each target has its own targetedByAddresses list. this allows preventing the reset of the targeted state\n   * for a delayed leave event if another enter event has already occurred.\n   */\n  private calcDndTargetDisplays(dndInfo: IDndDropzoneConfig, enter: boolean): void {\n    // prepare correct indizes\n    const sender = dndInfo.identifier;\n    const rowIndex = sender.superId;\n    let elementIndex = 0;\n    // non content element provides drag event, use its index for target display\n    if(dndInfo.sequentialPosition === DndSequentialDropzonePosition.Inbetween) {\n      elementIndex = sender.subId;\n    } else {\n      // content element provides drag event, find correct target display address\n      elementIndex = dndInfo.insertModeSequential === DndInsertModeSequential.Before\n        ? sender.subId - 1\n        : sender.subId + 1;\n    }\n\n    if(enter) {\n      this.dndTargetDisplayRows[rowIndex][elementIndex].isTargeted = true;\n      // only store new targeted infos if it is not alrey stored\n      if(!this.dndTargetedList.some(targeted => targeted.row === rowIndex && targeted.element === elementIndex)) {\n        this.dndTargetedList.push({\n          row: rowIndex,\n          element: elementIndex,\n        });\n      }\n      if(!this.dndTargetDisplayRows[rowIndex][elementIndex].targetedByAddresses\n        .some(address => address.row === rowIndex && address.element === sender.subId)) {\n        this.dndTargetDisplayRows[rowIndex][elementIndex].targetedByAddresses.push({\n          row: rowIndex,\n          element: sender.subId,\n        });\n      }\n    } else {\n      // remove targetedByAddress info\n      this.dndTargetDisplayRows[rowIndex][elementIndex].targetedByAddresses = this\n        .dndTargetDisplayRows[rowIndex][elementIndex].targetedByAddresses\n        .filter(address => !(address.row === rowIndex && address.element === sender.subId));\n      // reset targeted flag if last targetedByAddress has been removed\n      if(this.dndTargetDisplayRows[rowIndex][elementIndex].targetedByAddresses.length === 0) {\n        this.dndTargetDisplayRows[rowIndex][elementIndex].isTargeted = false;\n        this.dndTargetedList = this.dndTargetedList\n          .filter(targeted => !(targeted.row === rowIndex && targeted.element === elementIndex));\n      }\n    }\n  }\n\n  public async onDrop(dropInfo: DndDropEventData<DndFormulaElement>): Promise<void> {\n    this.resetDndTargetDisplays();\n\n    // cannot work without sequence index to insert at\n    if(dropInfo.dropTarget.sequentialOrder == null) return;\n    if(dropInfo.dndPayload?.existingPayloadId != null) {\n      // prevent dropping with existing payload index as target\n      if(dropInfo.dropTarget.sequentialOrder === dropInfo.dndPayload.existingPayloadId\n        || (dropInfo.dropTarget.insertModeSequential === DndInsertModeSequential.After\n          && dropInfo.dropTarget.sequentialOrder === dropInfo.dndPayload.existingPayloadId - 1)\n        || (dropInfo.dropTarget.insertModeSequential === DndInsertModeSequential.Before\n          && dropInfo.dropTarget.sequentialOrder === dropInfo.dndPayload.existingPayloadId + 1)\n      ) {\n        return;\n      }\n    }\n\n    const insertAtIndex = dropInfo.dropTarget.insertModeSequential === DndInsertModeSequential.Before\n      ? dropInfo.dropTarget.sequentialOrder : dropInfo.dropTarget.sequentialOrder + 1;\n\n    if(dropInfo.dndPayload?.createNewType != null) {\n      this.handleAddElement(\n        insertAtIndex,\n        dropInfo.dndPayload.createNewType.elementType,\n        dropInfo.dndPayload.createNewType.operatorType ?? null\n      );\n      // update reference for current content editing not required here. add new and editing is exclusive.\n    } else if(dropInfo.dndPayload?.existingPayloadId != null) {\n      const oldIndex = dropInfo.dndPayload.existingPayloadId;\n      const moveToFront = insertAtIndex < oldIndex ? 1 : 0;\n      const moveToBack = insertAtIndex > oldIndex ? 1 : 0;\n      this.formulaSequence.splice(insertAtIndex, 0, this.formulaSequence[oldIndex]);\n      this.formulaSequence.splice(oldIndex + 1 * moveToFront, 1);\n\n      if(this.currentContentElementEditing != null\n        && this.contentElementEditMode !== ContentElementEditMode.Inactive) {\n        // update current editing index required:\n        const { sequenceIndex: currentEditingIndex } = this.currentContentElementEditing;\n\n        if(currentEditingIndex === oldIndex) {\n          // current editing element has moved\n          this.currentContentElementEditing.sequenceIndex = insertAtIndex - 1 * moveToBack;\n        } else if(moveToFront && currentEditingIndex >= insertAtIndex) {\n          // moved to front and before current editing element\n          this.currentContentElementEditing.sequenceIndex += 1;\n        } else if(moveToBack && currentEditingIndex < insertAtIndex) {\n          // moved to back and after current editing element\n          this.currentContentElementEditing.sequenceIndex -= 1;\n        }\n      }\n\n      this.uniqueVariables = this\n        .updateUniqueItemsOnMove(this.uniqueVariables, oldIndex, insertAtIndex, moveToFront, moveToBack);\n      this.uniqueKeyValueLists = this\n        .updateUniqueItemsOnMove(this.uniqueKeyValueLists, oldIndex, insertAtIndex, moveToFront, moveToBack);\n      this.updateEditorView();\n      this.checkFormulaValidAndCreateLatexFormula();\n    }\n  }\n\n  @computedFrom('editExisting')\n  private get saveText(): string {\n    return this.editExisting ? 'globals.saveChanges' : 'elements.formulaEditor.insertFormula';\n  }\n\n  private parseToView(): void {\n    assertNotNullOrUndefined(this.existingFormula, 'Formula must exist to edit it!');\n    this.resultName = this.existingFormula.resultName;\n    this.resultUnit = this.existingFormula.resultUnit;\n    this.resultArguments = this.existingFormula.resultArguments;\n    if(!this.existingFormula.formulaString) return;\n\n    // extract fromulaStructure and from it all operators, variables and values\n    const formulaStructure = FormulaUtils.parseFormulaAndConvertShortToNormal(this.existingFormula?.formulaString);\n    if(!formulaStructure) return;\n\n    ({ record: this.operators, nextId: this.nextOperatorId } = FormulaUtils.convertElementsToRecordById<IFormulaOperator>(formulaStructure.operators));\n    ({ record: this.variables, nextId: this.nextVariableId } = FormulaUtils.convertElementsToRecordById<IFormulaVariable>(formulaStructure.variables));\n    ({ record: this.values, nextId: this.nextValueId } = FormulaUtils.convertElementsToRecordById<IFormulaValue>(formulaStructure.values));\n    ({ record: this.keyValueLists, nextId: this.nextKeyValueListId } = FormulaUtils.convertElementsToRecordById<IFormulaKeyValueList>(formulaStructure.keyValueLists));\n\n    this.formulaSequence = formulaStructure.sequence.map(sequenceElement => {\n      const { innerHtml, innerHtmlUnit } = this\n        .getInnerHtmlForExistingContentElement(sequenceElement.type, sequenceElement.id);\n      return {\n        type: sequenceElement.type,\n        id: sequenceElement.id,\n        shadowElement: this.createShadowElement(innerHtml, innerHtmlUnit),\n        innerHtml,\n        innerHtmlUnit,\n      };\n    });\n\n    this.uniqueVariables = this.extractUniqueItems(FormulaElementType.Variable, this.uniqueVariables, this.variables);\n    this.uniqueKeyValueLists = this.extractUniqueItems(FormulaElementType.KeyValueList, this.uniqueKeyValueLists, this.keyValueLists);\n    this.updateEditorView();\n    this.checkFormulaValidAndCreateLatexFormula();\n  }\n\n  private getInnerHtmlForExistingContentElement(\n    contentElementType: FormulaElementType,\n    contentElementId: number,\n  ): {\n    innerHtml: string;\n    innerHtmlUnit: string | null;\n  } {\n    let innerHtml = '';\n    let innerHtmlUnit: string | null = null;\n    switch(contentElementType) {\n      case FormulaElementType.Operator: {\n        const operatorType = this.operators[contentElementId].operator;\n        assertNotNullOrUndefined(operatorType, 'operatorType must be specified for adding new operator');\n        innerHtml = this.getOperatorAsString(operatorType, true);\n        break;\n      }\n\n      case FormulaElementType.Variable: {\n        innerHtml = this.variables[contentElementId].name !== ''\n          ? this.variables[contentElementId].name\n          : this.variableAsText;\n        innerHtmlUnit = this.variables[contentElementId].unit\n          ? `[${this.variables[contentElementId].unit}]`\n          : null;\n        break;\n      }\n\n      case FormulaElementType.Value: {\n        innerHtml = this.values[contentElementId].displayString !== ''\n          ? `${this.values[contentElementId].displayString}`\n          : this.valueAsText;\n        innerHtmlUnit = this.values[contentElementId].unit\n          ? `[${this.values[contentElementId].unit}]`\n          : null;\n        break;\n      }\n\n      case FormulaElementType.KeyValueList: {\n        innerHtml = this.keyValueLists[contentElementId].name !== ''\n          ? this.keyValueLists[contentElementId].name\n          : this.keyValueListAsText;\n        break;\n      }\n\n      default:\n        LogUtils.error(`FormulaEditor Error: Unexpected FormulaElementType: ${contentElementType} (while creating innerHtml)`);\n        break;\n    }\n    return { innerHtml, innerHtmlUnit };\n  }\n\n  private async handleSave(): Promise<void> {\n    assertNotNullOrUndefined(this.block, 'Block must be available to properly set formula id.');\n    this.block.formula = this.prepareFormulaDto();\n\n    DomUtils.fireEvent(this.editorDialogElement, {\n      name: 'edit-formula-ok',\n      bubbles: false,\n    });\n  }\n\n  private async handleCancel(): Promise<void> {\n    DomUtils.fireEvent(this.editorDialogElement, {\n      name: 'edit-formula-cancel',\n      bubbles: false,\n    });\n  }\n\n  private prepareFormulaDto(): IUpdateFormulaDto {\n    return {\n      isValid: this.isFormulaValid,\n      resultName: this.resultName,\n      resultUnit: this.resultUnit,\n      resultArguments: this.resultArguments,\n      formulaString: this.createFormulaString(),\n      displayString: this.displayLatexString,\n      latexFormula: this.formulaLatexString,\n      uniqueVariables: this.createUniqueVariablesString(),\n      uniqueKeyValueLists: this.createUniqueKeyValueListsString(),\n    };\n  }\n\n  private createFormulaString(): string {\n    return JSON.stringify({\n      sequence: this.formulaSequence.map(element => ({\n        type: element.type,\n        id: element.id,\n      })),\n      operators: Object.keys(this.operators).map(key => this.operators[Number(key)]),\n      variables: Object.keys(this.variables).map(key => FormulaUtils\n        .convertFormulaVariableNormalToShort(this.variables[Number(key)])),\n      values: Object.keys(this.values).map(key => this.values[Number(key)]),\n      keyValueLists: Object.keys(this.keyValueLists).map(key => ({\n        id: this.keyValueLists[Number(key)].id,\n        name: this.keyValueLists[Number(key)].name,\n        entries: FormulaUtils.convertKeyValueListEntriesNormalToShort(this.keyValueLists[Number(key)].entries),\n      })),\n    });\n  }\n\n  /**\n   * Will be used by formula block for displaying possible variable inputs.\n   * FormulaEditor extracts uniqueVariables from sequence itself.\n   */\n  private createUniqueVariablesString(): string {\n    return JSON.stringify(this.uniqueVariables.map(uniqueVariable => ({\n      name: uniqueVariable.name,\n      unit: this.variables[this.formulaSequence[uniqueVariable.occurrences[0]].id].unit,\n    })));\n  }\n\n  /**\n   * Will be used by formula block for displaying possible key-value-list inputs.\n   * FormulaEditor extracts uniqueKeyValueLists from sequence itself.\n   */\n  private createUniqueKeyValueListsString(): string {\n    return JSON.stringify(this.uniqueKeyValueLists.map(uniqueItem => ({\n      name: uniqueItem.name ?? '',\n      entries: this.keyValueLists[this.formulaSequence[uniqueItem.occurrences[0]].id].entries == null\n        ? []\n        : FormulaUtils.convertKeyValueListEntriesNormalToShort(\n          this.keyValueLists[this.formulaSequence[uniqueItem.occurrences[0]].id].entries),\n    })));\n  }\n\n  private checkFormulaValidAndCreateLatexFormula(): void {\n    this.checkFormulaValid();\n\n    if(this.isFormulaValid && this.formulaSequence.length) {\n      this.createAndRenderLatexFormula();\n    } else {\n      this.renderLatexFormula();\n    }\n\n    this.validateStringLengths();\n  }\n\n  private validateStringLengths(): void {\n    this.formulaCanSubmit = this.displayLatexString.length <= FORMULA_MAX_LENGTH_DISPLAY\n      && this.formulaLatexString.length <= FORMULA_MAX_LENGTH_LATEX\n      && this.createFormulaString().length <= FORMULA_MAX_LENGTH_FORMULA\n      && this.createUniqueVariablesString().length <= FORMULA_MAX_LENGTH_UNIQUE_VARIABLES\n      && this.createUniqueKeyValueListsString().length <= FORMULA_MAX_LENGTH_UNIQUE_KEY_VALUE_LISTS;\n  }\n\n  /**\n   * rules for valid formulas\n   *\n   * 1. must contain element(s)\n   * 2.1 every value must be != null (detailled number validation happens before it is stored in formula)\n   * 2.2 every variable must have a name !== ''\n   * 2.3 every keyValueList must have a name !== '' and at least one entry\n   * 3. single element formulas must be one of   Variable Value keyValueList\n   * the following rules apply to formulas with multiple elements:\n   * 4. must start with one of    Variable Value ( √(\n   * 5. must end with one of    Variable Value ) ^2\n   * 6. must satisfy neighbouring rules (detailled docu in function)\n   * 7. all opening and closing brackets must be pairs\n   */\n  private checkFormulaValid(): void {\n    // empty sequence is not valid\n    if(!this.formulaSequence.length) {\n      this.isFormulaValid = false;\n      return;\n    }\n\n    // every value must have a valid number and every variable a name\n    for(const element of this.formulaSequence) {\n      switch(element.type) {\n        case FormulaElementType.Value:\n          if(this.values[element.id].valueAsNumber == null) {\n            this.isFormulaValid = false;\n            return;\n          }\n          break;\n\n        case FormulaElementType.Variable:\n          if(this.variables[element.id].name === '') {\n            this.isFormulaValid = false;\n            return;\n          }\n          break;\n\n        case FormulaElementType.KeyValueList:\n          if(this.keyValueLists[element.id].name === '') {\n            this.isFormulaValid = false;\n            return;\n          }\n          break;\n      }\n    }\n\n    // a single element must be a variable or a constant\n    if(this.formulaSequence.length === 1) {\n      this.isFormulaValid = this.formulaSequence[0].type !== FormulaElementType.Operator;\n      return;\n    }\n\n    // formula must start with one of   Variable Value keyValueList ( √(\n    const groupStarters: CheckContentElement[] = [{\n      type: FormulaElementType.Operator,\n      operator: FormulaOperatorType.Root,\n    }, {\n      type: FormulaElementType.Operator,\n      operator: FormulaOperatorType.BracketOpen,\n    }, {\n      type: FormulaElementType.Variable,\n      operator: null,\n    }, {\n      type: FormulaElementType.Value,\n      operator: null,\n    }, {\n      type: FormulaElementType.KeyValueList,\n      operator: null,\n    }];\n    if(!this.isMemeberOfGroup(0, groupStarters)) {\n      this.isFormulaValid = false;\n      return;\n    }\n\n    // formula must end with one of    Variable Value keyValueList ) ^2\n    const groupB: CheckContentElement[] = [{\n      type: FormulaElementType.Operator,\n      operator: FormulaOperatorType.Squared,\n    }, {\n      type: FormulaElementType.Operator,\n      operator: FormulaOperatorType.BracketClose,\n    }, {\n      type: FormulaElementType.Variable,\n      operator: null,\n    }, {\n      type: FormulaElementType.Value,\n      operator: null,\n    }, {\n      type: FormulaElementType.KeyValueList,\n      operator: null,\n    }];\n    if(!this.isMemeberOfGroup(this.formulaSequence.length - 1, groupB)) {\n      this.isFormulaValid = false;\n      return;\n    }\n\n    /**\n     * sequence length > 1, check validity of neighbouring elements:\n     * there are two groups of content elements, whos members have a\n     * clear pattern which two groups of elements they are allowed to follow\n     * groups are:\n     * Group A:    + - ⋅ / ^ ^2 )                       which are allowed to follow    Group B:   ^2 ) Variable Value keyValueList\n     * Group C:    √( ( Variable Value keyValueList     which are allowed to follow    Group D:   + - ⋅ / ^ √( (\n     *\n     */\n    const groupA: CheckContentElement[] = [{\n      type: FormulaElementType.Operator,\n      operator: FormulaOperatorType.Add,\n    }, {\n      type: FormulaElementType.Operator,\n      operator: FormulaOperatorType.Subtract,\n    }, {\n      type: FormulaElementType.Operator,\n      operator: FormulaOperatorType.Multiply,\n    }, {\n      type: FormulaElementType.Operator,\n      operator: FormulaOperatorType.Divide,\n    }, {\n      type: FormulaElementType.Operator,\n      operator: FormulaOperatorType.Power,\n    }, {\n      type: FormulaElementType.Operator,\n      operator: FormulaOperatorType.Squared,\n    }, {\n      type: FormulaElementType.Operator,\n      operator: FormulaOperatorType.BracketClose,\n    }];\n\n    let i = 0;\n    while(i < this.formulaSequence.length - 1) {\n      // identify if current element at index i is a member of groupB or groupD\n      const currentElementIsMememberOfGroupB = this.isMemeberOfGroup(i, groupB);\n      const nextElementIsMemberOfGroupA = this.isMemeberOfGroup(i + 1, groupA);\n\n      if((nextElementIsMemberOfGroupA && !currentElementIsMememberOfGroupB)\n        || (!nextElementIsMemberOfGroupA && currentElementIsMememberOfGroupB)) {\n        this.isFormulaValid = false;\n        return;\n      }\n      i += 1;\n    }\n\n    // every opening bracket must be pared with a closing bracket\n    const groupOpeningBrackets: CheckContentElement[] = [{\n      type: FormulaElementType.Operator,\n      operator: FormulaOperatorType.Root,\n    }, {\n      type: FormulaElementType.Operator,\n      operator: FormulaOperatorType.BracketOpen,\n    }];\n    const groupClosingBracket: CheckContentElement[] = [{\n      type: FormulaElementType.Operator,\n      operator: FormulaOperatorType.BracketClose,\n    }];\n    let openCount = 0;\n    for(const [index] of this.formulaSequence.entries()) {\n      // no need for special handling of division \")/(\" because it actually does not affect the openCount\n      if(this.isMemeberOfGroup(index, groupOpeningBrackets)) {\n        openCount += 1;\n      } else if(this.isMemeberOfGroup(index, groupClosingBracket)) {\n        openCount -= 1;\n      }\n    }\n    if(openCount !== 0) {\n      this.isFormulaValid = false;\n      return;\n    }\n\n    // all validation passed -> formula is valid\n    this.isFormulaValid = true;\n  }\n\n  private isMemeberOfGroup(sequenceIndex: number, group: CheckContentElement[]): boolean {\n    // check if the type matches\n    let isMember = group.some(member => member.type === this.formulaSequence[sequenceIndex].type);\n    // in case it is an operator the operator type must match aswell\n    if(isMember && this.formulaSequence[sequenceIndex].type === FormulaElementType.Operator) {\n      const { operator } = this.operators[this.formulaSequence[sequenceIndex].id];\n      isMember = group.some(member => member.operator === operator);\n    }\n    return isMember;\n  }\n\n  /**\n   * converts the pre-checked formula sequence into a latex sequence.\n   * the latex sequence array is used to create the display version of the latex string.\n   * as well as the pure latex version used for calculation.\n   *\n   * sequence is processed multiple times to make each purpose easy to read, change and fix.\n   * the array lengths we are expecting for formulas are rather short, so performance shouldn't be an issue.\n   * multiple processing includes:\n   * * basic translation to latex syntax\n   * * replacing and adding special latex ending brackets } for root and power occurences\n   * * converting the division \"(..)/(..)\" into the according \\frac{..}{..} syntax\n   */\n  private createAndRenderLatexFormula(): void {\n    this.latexSequence = [];\n\n    // create a basic version of this.latexSequence which still needs cleanup afterwards\n    this.latexSequenceCreateBasicSequence();\n    // make sure exponents are wrapped inside { }, at this point closing of root is still a )\n    this.latexSequenceWrapExponents();\n    // find and convert root end brackets form ) to }\n    this.latexSequenceConvertRootEndBrackets();\n    // update all division start and end elements\n    this.latexSequenceUpdateDivisionStartAndEndElements();\n\n    this.displayLatexString = this.latexSequence\n      .reduce((previous, current) => {\n        let newString = '';\n        // ?{ ... } wrapping for variables AND for keyValueLists to allow later insertion of values for display\n        // also add k_ or v_ prefix to avoid namespace conflicts between the two\n        if(current.type === SpecialLatexElementType.Variable\n          || current.type === SpecialLatexElementType.KeyValueList) {\n          newString = `${previous}${FORMULA_REFERENCE_START_IDENTIFIER}${\n            current.type === SpecialLatexElementType.Variable\n              ? FORMULA_VARIABLE_PREFIX\n              : FORMULA_KEY_VALUE_LIST_PREFIX\n          }${FormulaUtils.escapeProblematicSpecialCharacters(\n            current.latexBasicDisplayString,\n          )}${FORMULA_REFERENCE_END_IDENTIFIER}`;\n        } else {\n          newString = `${previous}${current.latexBasicDisplayString}`;\n        }\n        if(current.latexUnitString) {\n          newString = `${newString}\\\\space\\\\textcolor{${FORMULA_UNIT_COLOR}}{[${FormulaUtils\n            .escapeProblematicSpecialCharacters(current.latexUnitString)}]}`;\n        }\n        return newString;\n      }, '');\n    this.renderLatexFormula(this.displayLatexString);\n\n    // prepare formula latex string for easy calculation in formula block component\n    this.formulaLatexString = this.latexSequence\n      .reduce((previous, current) => {\n        switch(current.type) {\n          case SpecialLatexElementType.Value:\n            return `${previous}${current.valueString}`;\n\n          case SpecialLatexElementType.Variable:\n            return `${previous}${FORMULA_VARIABLE_PREFIX}${FormulaUtils\n              .replaceSpaces(FormulaUtils\n                .replaceSpecialCharacters(current.latexBasicDisplayString))}`;\n\n          case SpecialLatexElementType.KeyValueList:\n            return `${previous}${FORMULA_KEY_VALUE_LIST_PREFIX}${FormulaUtils\n              .replaceSpaces(FormulaUtils\n                .replaceSpecialCharacters(current.latexBasicDisplayString))}`;\n\n          default:\n            return `${previous}${current.latexBasicDisplayString}`;\n        }\n      }, '');\n  }\n\n  /** prepends \"result =\" to a given formulaString and renders it */\n  private renderLatexFormula(formulaString?: string): void {\n\n    const displayString = FormulaUtils.generateDisplayString({\n      resultName: this.resultName,\n      resultArguments: this.resultArguments,\n      resultUnit: this.resultUnit,\n      formulaString,\n      resultAfterFormula: false,\n      removeReferenceIdentifiers: true,\n      convertSpaces: true,\n    });\n\n    assertNotNullOrUndefined(this.katexFormulaElement, 'Formula Display Element must be available!');\n    katex.render(displayString, this.katexFormulaElement, FORMULA_KATEX_RENDER_OPTIONS);\n  }\n\n  /** converts formulaSequence into a basic latexSequence */\n  private latexSequenceCreateBasicSequence(): void {\n    for(const sequenceElement of this.formulaSequence) {\n      switch(sequenceElement.type) {\n        case FormulaElementType.Operator:\n          this.latexSequence.push({\n            type: this.operators[sequenceElement.id].operator,\n            latexBasicDisplayString: this\n              .getOperatorAsLatexString(this.operators[sequenceElement.id].operator),\n          });\n          break;\n        case FormulaElementType.Variable:\n          this.latexSequence.push({\n            type: SpecialLatexElementType.Variable,\n            latexBasicDisplayString: this.variables[sequenceElement.id].name,\n            latexUnitString: this.variables[sequenceElement.id].unit,\n          });\n          break;\n        case FormulaElementType.Value:\n          this.latexSequence.push({\n            type: SpecialLatexElementType.Value,\n            latexBasicDisplayString: this.values[sequenceElement.id].displayString,\n            latexUnitString: this.values[sequenceElement.id].unit,\n            valueString: `${this.values[sequenceElement.id].valueAsNumber}`,\n          });\n          break;\n        case FormulaElementType.KeyValueList:\n          this.latexSequence.push({\n            type: SpecialLatexElementType.KeyValueList,\n            latexBasicDisplayString: this.keyValueLists[sequenceElement.id].name,\n          });\n          break;\n        default:\n          LogUtils.error(`FormulaEditor Error: Unexpected FormulaElementType: ${sequenceElement.type} (while creating basic latex sequence)`);\n          return;\n      }\n    }\n  }\n\n  /** wraps all exponents with curly brackets */\n  private latexSequenceWrapExponents(): void {\n    type OpenExponent = {\n      detectEndArmed: boolean;\n      openElementCount: number;\n    };\n    const openExponents: OpenExponent[] = [];\n    const addExponentEndBeforeIndices: number[] = [];\n    for(const [index, latexSequenceElement] of this.latexSequence.entries()) {\n      switch(latexSequenceElement.type) {\n        // start searching for end of exponent\n        case FormulaOperatorType.Power:\n          openExponents.push({\n            detectEndArmed: true,\n            openElementCount: 0,\n          });\n          break;\n        // wrap all variables and values as single element exponents in { }\n        case SpecialLatexElementType.Variable:\n        case SpecialLatexElementType.Value:\n        case SpecialLatexElementType.KeyValueList:\n          if(openExponents.length\n            && openExponents[openExponents.length - 1].detectEndArmed) {\n            openExponents.pop();\n            addExponentEndBeforeIndices.push(index + 1);\n          }\n          break;\n        // increase open element count within current exponent\n        case FormulaOperatorType.Root:\n        case FormulaOperatorType.BracketOpen:\n          if(openExponents.length) {\n            openExponents[openExponents.length - 1].detectEndArmed = false;\n            openExponents[openExponents.length - 1].openElementCount += 1;\n          }\n          break;\n        // reduce open element count within current exponent\n        case FormulaOperatorType.BracketClose:\n          if(openExponents.length) {\n            openExponents[openExponents.length - 1].openElementCount -= 1;\n            if(openExponents[openExponents.length - 1].openElementCount === 0) {\n              openExponents.pop();\n              addExponentEndBeforeIndices.push(index + 1);\n            }\n          }\n          break;\n      }\n    }\n    for(const [index, insertIndex] of addExponentEndBeforeIndices.entries()) {\n      this.latexSequence.splice(insertIndex + index, 0, {\n        type: SpecialLatexElementType.SpecialClose,\n        latexBasicDisplayString: '}',\n      });\n    }\n  }\n\n  /** replace ) with } for all roots */\n  private latexSequenceConvertRootEndBrackets(): void {\n    enum OpenElement {\n      Bracket = 'bracket',\n      Root = 'root',\n    }\n    const openElements: OpenElement[] = [];\n    for(const [index, latexSequenceElement] of this.latexSequence.entries()) {\n      switch(latexSequenceElement.type) {\n        case FormulaOperatorType.Root:\n          openElements.push(OpenElement.Root);\n          break;\n        case FormulaOperatorType.BracketOpen:\n          openElements.push(OpenElement.Bracket);\n          break;\n        case FormulaOperatorType.BracketClose:\n          if(!openElements.length)\n            LogUtils.error('FormulaEditor Error: Closing bracket must correspond to root or bracket open in order to create latex sequence');\n          if(openElements[openElements.length - 1] === OpenElement.Root) {\n            this.latexSequence[index].latexBasicDisplayString = '}';\n            this.latexSequence[index].type = SpecialLatexElementType.SpecialClose;\n          }\n          openElements.pop();\n          break;\n      }\n    }\n  }\n\n  /**\n   * replace all division start brackets ( with \\frac{\n   * replace all division end brackets ) with }\n   */\n  private latexSequenceUpdateDivisionStartAndEndElements(): void {\n    for(const [index, currentElement] of this.latexSequence.entries()) {\n      if(currentElement.type === FormulaOperatorType.Divide) {\n        // search for start of division and replace open bracket with \"\\frac{\" element\n        let closeCount = 1;\n        for(let i = index - 1; i >= 0; i--) {\n          switch(this.latexSequence[i].type) {\n            case FormulaOperatorType.BracketClose:\n              closeCount += 1;\n              break;\n            case FormulaOperatorType.BracketOpen:\n              closeCount -= 1;\n              break;\n          }\n          if(closeCount === 0) {\n            this.latexSequence[i].type = SpecialLatexElementType.FractionStart;\n            this.latexSequence[i].latexBasicDisplayString = '\\\\frac{';\n            break;\n          }\n        }\n        // search for end of division and replace close bracket with \"}\" element\n        let openCount = 1;\n        for(let i = index + 1; i < this.latexSequence.length; i++) {\n          switch(this.latexSequence[i].type) {\n            case FormulaOperatorType.BracketClose:\n              openCount -= 1;\n              break;\n            case FormulaOperatorType.BracketOpen:\n              openCount += 1;\n              break;\n          }\n          if(openCount === 0) {\n            this.latexSequence[i].type = SpecialLatexElementType.SpecialClose;\n            this.latexSequence[i].latexBasicDisplayString = '}';\n            break;\n          }\n        }\n      }\n    }\n  }\n\n  private getOperatorAsLatexString(type: FormulaOperatorType): string {\n    switch(type) {\n      case FormulaOperatorType.Add: return '+';\n      case FormulaOperatorType.BracketClose: return '\\\\right)';\n      case FormulaOperatorType.BracketOpen: return '\\\\left(';\n      case FormulaOperatorType.Divide: return '}{';\n      case FormulaOperatorType.Multiply: return '\\\\cdot ';\n      case FormulaOperatorType.Power: return '^{';\n      case FormulaOperatorType.Root: return '\\\\sqrt{';\n      case FormulaOperatorType.Squared: return '^2';\n      case FormulaOperatorType.Subtract: return '-';\n      default: return 'ERR';\n    }\n  }\n\n  /**\n   * some operators are displayed differently depending if they should belong to the\n   * available operators sidebar or if they are actually part of the formula\n   */\n  private getOperatorAsString(type: FormulaOperatorType, inFormula = false): string {\n    switch(type) {\n      case FormulaOperatorType.Add: return '+';\n      case FormulaOperatorType.BracketClose: return ')';\n      case FormulaOperatorType.BracketOpen: return '(';\n      case FormulaOperatorType.Divide: return inFormula ? ') / (' : '÷';\n      case FormulaOperatorType.Multiply: return '⋅';\n      case FormulaOperatorType.Power: return inFormula ? '^' : 'x”';\n      case FormulaOperatorType.Root: return '√(';\n      case FormulaOperatorType.Squared: return inFormula ? '^2' : 'x²';\n      case FormulaOperatorType.Subtract: return '-';\n      default: return 'ERR';\n    }\n  }\n\n  @computedFrom('FormulaOperatorType')\n  private get availableOperatorTypes(): FormulaOperatorType[] {\n    return [\n      FormulaOperatorType.Add,\n      FormulaOperatorType.Subtract,\n      FormulaOperatorType.Multiply,\n      FormulaOperatorType.Divide,\n      FormulaOperatorType.Squared,\n      FormulaOperatorType.BracketOpen,\n      FormulaOperatorType.BracketClose,\n      FormulaOperatorType.Root,\n      FormulaOperatorType.Power,\n    ];\n  }\n\n  /** if index -1 is provided as order it will be inserted as last element in sequence. */\n  private handleAddElement(\n    index: number,\n    elementType: FormulaElementType,\n    operatorType: FormulaOperatorType | null,\n  ): void {\n    let insertedAtIndex = index;\n    if(operatorType !== FormulaOperatorType.Divide) {\n      insertedAtIndex = this.prepareAndInsertNewElement(index, elementType, operatorType);\n    } else {\n      // a new division automatically comes with an opening and closing bracket\n      insertedAtIndex = this.prepareAndInsertNewElement(index, elementType, FormulaOperatorType.BracketOpen);\n      insertedAtIndex = this.prepareAndInsertNewElement(insertedAtIndex + 1, elementType, operatorType);\n      this.prepareAndInsertNewElement(insertedAtIndex + 1, elementType, FormulaOperatorType.BracketClose);\n    }\n\n    this.updateEditorView();\n    this.checkFormulaValidAndCreateLatexFormula();\n\n    // auto activate editing\n    if(elementType !== FormulaElementType.Operator) {\n      this.handleEditContentElementStart(insertedAtIndex);\n    }\n  }\n\n  /** if index -1 is provided as order it will be inserted as last element in sequence. */\n  private prepareAndInsertNewElement(\n    index: number,\n    elementType: FormulaElementType,\n    operatorType: FormulaOperatorType | null,\n  ): number {\n    const newId = this.addNewContentElement(elementType, operatorType);\n    const innerHtml = this.getInnerHtmlForContentType(elementType, operatorType);\n    const insertAtIndex = index === -1 ? this.formulaSequence.length : index;\n\n    this.formulaSequence.splice(insertAtIndex, 0, {\n      type: elementType,\n      id: newId,\n      shadowElement: this.createShadowElement(innerHtml),\n      innerHtml,\n      innerHtmlUnit: null,\n    });\n\n    this.uniqueVariables = this.updateUniqueItemOccurenceIndizesOnAdd(insertAtIndex, this.uniqueVariables);\n    this.uniqueKeyValueLists = this.updateUniqueItemOccurenceIndizesOnAdd(insertAtIndex, this.uniqueKeyValueLists);\n    return insertAtIndex;\n  }\n\n  /** shadowElements are used to pre-calc the width of each editor row */\n  private createShadowElement(innerHtml: string, innerHtmlUnit?: string | null): HTMLElement {\n    const shadowElement = document.createElement('div') as HTMLElement;\n    shadowElement.classList.add('formula-editor__element');\n    shadowElement.innerHTML = innerHtmlUnit\n      ? `${innerHtml} ${innerHtmlUnit}`\n      : `${innerHtml}`;\n    this.editorShadowElement?.appendChild(shadowElement);\n    return shadowElement;\n  }\n\n  /** provide default innerHtml for new content element */\n  private getInnerHtmlForContentType(\n    contentElementType: FormulaElementType,\n    operatorType: FormulaOperatorType | null,\n  ): string {\n    let innerHtml = '';\n    switch(contentElementType) {\n      case FormulaElementType.Operator:\n        assertNotNullOrUndefined(operatorType, 'operatorType must be specified for adding new operator');\n        innerHtml = this.getOperatorAsString(operatorType, true);\n        break;\n\n      case FormulaElementType.Variable:\n        innerHtml = this.variableAsText;\n        break;\n\n      case FormulaElementType.Value:\n        innerHtml = this.valueAsText;\n        break;\n\n      case FormulaElementType.KeyValueList:\n        innerHtml = this.keyValueListAsText;\n        break;\n\n      default:\n        LogUtils.error(`FormulaEditor Error: Unexpected FormulaElementType: ${contentElementType} (while retrieving innerHtml for contentType)`);\n        break;\n    }\n    return innerHtml;\n  }\n\n  private addNewContentElement(\n    contentElementType: FormulaElementType,\n    operatorType: FormulaOperatorType | null,\n  ): number {\n    let newId = 0;\n    switch(contentElementType) {\n      case FormulaElementType.Operator:\n        assertNotNullOrUndefined(operatorType, 'operatorType must be specified for adding new operator');\n        newId = this.nextOperatorId;\n        this.nextOperatorId += 1;\n        this.operators[newId] = {\n          id: newId,\n          operator: operatorType,\n        };\n        break;\n\n      case FormulaElementType.Variable:\n        newId = this.nextVariableId;\n        this.nextVariableId += 1;\n        this.variables[newId] = {\n          id: newId,\n          name: '',\n          unit: '',\n          minValueAsString: '',\n          minDisplayString: '',\n          minValueAsNumber: null,\n          maxValueAsString: '',\n          maxDisplayString: '',\n          maxValueAsNumber: null,\n        };\n        break;\n\n      case FormulaElementType.Value:\n        newId = this.nextValueId;\n        this.nextValueId += 1;\n        this.values[newId] = { id: newId, valueAsString: '', displayString: '', valueAsNumber: null, unit: '' };\n        break;\n\n      case FormulaElementType.KeyValueList:\n        newId = this.nextKeyValueListId;\n        this.nextKeyValueListId += 1;\n        this.keyValueLists[newId] = { id: newId, name: '', entries: [] };\n        break;\n\n      default:\n        LogUtils.error(`FormulaEditor Error: Unexpected FormulaElementType: ${contentElementType} (while adding new contentElement)`);\n        break;\n    }\n    return newId;\n  }\n\n  private handleEditContentElementStart(sequenceIndex: number | undefined | null): void {\n    if(sequenceIndex == null) return;\n    /**\n     * If ContentElementEditMode is currently active, but another element is clicked -\n     * abort previous element editing and start editing new element.\n     */\n    if(this.contentElementEditMode !== ContentElementEditMode.Inactive) {\n      // don't react to click on current editing element\n      if(sequenceIndex === this.currentContentElementEditing?.sequenceIndex) return;\n\n      assertNotNullOrUndefined(this.currentContentElementEditing, 'Need current content element editing to revert changes!');\n      const { sequenceIndex: lastEditingSequenceIndex } = this.currentContentElementEditing;\n      this.revertContentElementAttributeChanges(lastEditingSequenceIndex);\n      this.prepareAndUpdateEditorView(lastEditingSequenceIndex);\n    }\n    const attributesBackup = this.getContentElementAttributesAndSetEditMode(sequenceIndex);\n    this.currentContentElementEditingName = attributesBackup.name ?? '';\n    this.currentContentElementEditingUnit = attributesBackup.unit ?? '';\n    this.currentContentElementEditingValue = attributesBackup.value.asValueString != null ? `${attributesBackup.value.asValueString}` : '';\n    this.currentContentElementEditingMin = attributesBackup.min.asValueString != null ? `${attributesBackup.min.asValueString}` : '';\n    this.currentContentElementEditingMax = attributesBackup.max.asValueString != null ? `${attributesBackup.max.asValueString}` : '';\n    this.currentContentElementEditingKeyValueEntries = attributesBackup.keyValueEntries != null\n      ? [...attributesBackup.keyValueEntries.map(entry => ({ ...entry }))]\n      : [];\n    this.currentContentElementEditing = {\n      sequenceIndex,\n      type: this.formulaSequence[sequenceIndex].type,\n      attributesBackup,\n      valueValid: this.contentElementEditMode === ContentElementEditMode.Value\n        && (this.currentContentElementEditingValue === ''\n          || this.numberValidationRegex.test(this.currentContentElementEditingValue)),\n    };\n  }\n\n  private handleContentElementEditingName(event: InputEvent): void {\n    const targetElement = event.target as HTMLInputElement;\n    if(targetElement.value && !this.textValidationRegex.test(targetElement.value)) {\n      targetElement.value = this.currentContentElementEditingName;\n    } else {\n      this.currentContentElementEditingName = targetElement.value;\n    }\n  }\n\n  private handleContentElementEditingUnit(event: InputEvent): void {\n    const targetElement = event.target as HTMLInputElement;\n    if(!this.textValidationRegex.test(targetElement.value)) {\n      targetElement.value = this.currentContentElementEditingUnit;\n    } else {\n      this.currentContentElementEditingUnit = targetElement.value;\n    }\n  }\n\n  private handleEditingResultUnit(event: InputEvent): void {\n    const targetElement = event.target as HTMLInputElement;\n    if(!this.textValidationRegex.test(targetElement.value)) {\n      targetElement.value = this.resultUnit;\n    } else {\n      this.resultUnit = targetElement.value;\n    }\n  }\n\n  private handleEditingResultName(event: InputEvent): void {\n    const targetElement = event.target as HTMLInputElement;\n    if(!this.textValidationRegex.test(targetElement.value)) {\n      targetElement.value = this.resultName;\n    } else {\n      this.resultName = targetElement.value;\n    }\n  }\n\n  private handleEditingResultArguments(event: InputEvent): void {\n    const targetElement = event.target as HTMLInputElement;\n    if(!this.textValidationRegex.test(targetElement.value)) {\n      targetElement.value = this.resultArguments;\n    } else {\n      this.resultArguments = targetElement.value;\n    }\n  }\n\n  private updateCurrentContentElementEditingCanSubmit(event: CanSubmitUpdatedEvent): void {\n    this.contentElementEditCanSubmit = event.detail.canSubmit;\n  }\n\n  private updateCurrentContentElementEditingKeyValueEntries(event: KeyValueListUpdatedEvent): void {\n    if(this.currentContentElementEditing == null || !this.contentElementEditCanSubmit) return;\n\n    const { sequenceIndex } = this.currentContentElementEditing;\n    const updatedContentElementEditingKeyValueEntries = [...event.detail.entries.map(entry => ({ ...entry }))];\n\n    this.keyValueLists[this.formulaSequence[sequenceIndex].id].entries = updatedContentElementEditingKeyValueEntries;\n    const currentKeyValueListName = this.keyValueLists[this.formulaSequence[sequenceIndex].id].name;\n    this.updateEntriesOfUniqueKeyValueLists(currentKeyValueListName, updatedContentElementEditingKeyValueEntries);\n    this.validateStringLengths();\n  }\n\n  private handleCurrentContentElementEditingMinValueChanged(event: InputEvent): void {\n    if(this.currentContentElementEditing == null) return;\n\n    const targetElement = event.target as HTMLInputElement;\n    if(targetElement.value && !this.numberValidationRegex.test(targetElement.value)) {\n      targetElement.value = this.currentContentElementEditingMin;\n      return;\n    }\n\n    this.currentContentElementEditingMinMaxValid = this.validateMinMax(LimitType.Min, targetElement.value);\n    this.currentContentElementEditingMin = targetElement.value;\n    this.updateCurrentContentElementEditingMinOrMaxValue(\n      LimitType.Min,\n      this.currentContentElementEditingMin,\n    );\n  }\n\n  private handleCurrentContentElementEditingMaxValueChanged(event: InputEvent): void {\n    if(this.currentContentElementEditing == null) return;\n\n    const targetElement = event.target as HTMLInputElement;\n    if(targetElement.value && !this.numberValidationRegex.test(targetElement.value)) {\n      targetElement.value = this.currentContentElementEditingMax;\n      return;\n    }\n\n    this.currentContentElementEditingMinMaxValid = this.validateMinMax(LimitType.Max, targetElement.value);\n    this.currentContentElementEditingMax = targetElement.value;\n    this.updateCurrentContentElementEditingMinOrMaxValue(\n      LimitType.Max,\n      this.currentContentElementEditingMax,\n    );\n  }\n\n  private updateCurrentContentElementEditingMinOrMaxValue(\n    limitType: LimitType,\n    valueAsString: string,\n  ): void {\n    if(this.currentContentElementEditing == null || limitType === LimitType.Both) return;\n\n    const cleanedValueString = FormulaUtils.cleanNumberString(valueAsString, true);\n    const setValue: ContentElementNumber = {\n      asValueString: cleanedValueString,\n      asDisplayString: FormulaUtils.convertToDisplayString(cleanedValueString, FORMULA_NUMBER_VALIDATION_REGEX),\n      asNumber: FormulaUtils.convertStringToNumber(valueAsString, FORMULA_NUMBER_VALIDATION_REGEX) ?? null,\n    };\n\n    this.setMinMaxOfVariable(this.currentContentElementEditing.sequenceIndex, limitType, setValue);\n    this.updateMinMaxOfUniqueVariables(\n      this.currentContentElementEditingName,\n      limitType,\n      limitType === LimitType.Min ? setValue : null,\n      limitType === LimitType.Max ? setValue : null,\n    );\n\n    // Since Min/Max Limits don't affect formula validity or rendering the only necessary check is general formula length:\n    this.validateStringLengths();\n  }\n\n  private validateMinMax(limitType: LimitType, newValueAsString: string): boolean {\n    assertNotNullOrUndefined(this.currentContentElementEditing, 'Need current content element editing to validate min < max!');\n\n    // removing limit is allowed\n    if(newValueAsString === '') return true;\n    const newValue = FormulaUtils.convertStringToNumber(newValueAsString, FORMULA_NUMBER_VALIDATION_REGEX) ?? 0;\n\n    switch(limitType) {\n      case LimitType.Min: {\n        // any minimum is allowed if no maximum is defined:\n        if(this.currentContentElementEditingMax === '') return true;\n        const maxValue = this.variables[this.formulaSequence[this.currentContentElementEditing.sequenceIndex].id].maxValueAsNumber;\n        if(maxValue == null) return true;\n\n        return newValue < maxValue;\n      }\n      case LimitType.Max: {\n        // any maximum is allowed if no minimum is defined:\n        if(this.currentContentElementEditingMin === '') return true;\n        const minValue = this.variables[this.formulaSequence[this.currentContentElementEditing.sequenceIndex].id].minValueAsNumber;\n        if(minValue == null) return true;\n\n        return minValue < newValue;\n      }\n      default:\n        return false;\n    }\n  }\n\n  private revertContentElementAttributeChanges(sequenceIndex: number): void {\n    assertNotNullOrUndefined(this.currentContentElementEditing, 'Need current content element editing to revert changes!');\n    switch(this.currentContentElementEditing.type) {\n      case FormulaElementType.Operator:\n        break;\n\n      case FormulaElementType.Value: {\n        if(this.currentContentElementEditing.attributesBackup.unit) {\n          this.values[this.formulaSequence[sequenceIndex].id].unit = this.currentContentElementEditing.attributesBackup.unit;\n        }\n        if(this.currentContentElementEditing.attributesBackup.value.asValueString) {\n          this.values[this.formulaSequence[sequenceIndex].id].valueAsString = this.currentContentElementEditing.attributesBackup.value.asValueString;\n          this.values[this.formulaSequence[sequenceIndex].id].displayString = this.currentContentElementEditing.attributesBackup.value.asDisplayString;\n          this.values[this.formulaSequence[sequenceIndex].id].valueAsNumber = this.currentContentElementEditing.attributesBackup.value.asNumber;\n        }\n        break;\n      }\n\n      case FormulaElementType.Variable:\n        if(this.currentContentElementEditing.attributesBackup.unit) {\n          this.variables[this.formulaSequence[sequenceIndex].id].unit = this.currentContentElementEditing.attributesBackup.unit;\n        }\n        if(this.currentContentElementEditing.attributesBackup.name) {\n          this.variables[this.formulaSequence[sequenceIndex].id].name = this.currentContentElementEditing.attributesBackup.name;\n          this.uniqueVariables = this.updateUniqueItemsOnRename(\n            FormulaElementType.Variable, sequenceIndex, this.uniqueVariables, this.variables);\n          this.updateMinMaxOfUniqueVariables(\n            this.currentContentElementEditing.attributesBackup.name,\n            LimitType.Both,\n            this.currentContentElementEditing.attributesBackup.min,\n            this.currentContentElementEditing.attributesBackup.max,\n          );\n        }\n        if(this.currentContentElementEditing.attributesBackup.min.asValueString) {\n          this.variables[this.formulaSequence[sequenceIndex].id].minValueAsString = this.currentContentElementEditing.attributesBackup.min.asValueString;\n          this.variables[this.formulaSequence[sequenceIndex].id].minDisplayString = this.currentContentElementEditing.attributesBackup.min.asDisplayString;\n          this.variables[this.formulaSequence[sequenceIndex].id].minValueAsNumber = this.currentContentElementEditing.attributesBackup.min.asNumber;\n        }\n        if(this.currentContentElementEditing.attributesBackup.max.asValueString) {\n          this.variables[this.formulaSequence[sequenceIndex].id].maxValueAsString = this.currentContentElementEditing.attributesBackup.max.asValueString;\n          this.variables[this.formulaSequence[sequenceIndex].id].maxDisplayString = this.currentContentElementEditing.attributesBackup.max.asDisplayString;\n          this.variables[this.formulaSequence[sequenceIndex].id].maxValueAsNumber = this.currentContentElementEditing.attributesBackup.max.asNumber;\n        }\n        break;\n\n      case FormulaElementType.KeyValueList:\n        if(this.currentContentElementEditing.attributesBackup.name != null) {\n          this.keyValueLists[this.formulaSequence[sequenceIndex].id].name = this.currentContentElementEditing.attributesBackup.name;\n          this.uniqueKeyValueLists = this.updateUniqueItemsOnRename(\n            FormulaElementType.KeyValueList, sequenceIndex, this.uniqueKeyValueLists, this.keyValueLists);\n          this.updateEntriesOfUniqueKeyValueLists(\n            this.currentContentElementEditing.attributesBackup.name,\n            this.currentContentElementEditing.attributesBackup.keyValueEntries,\n          );\n        }\n        this.keyValueLists[this.formulaSequence[sequenceIndex].id].entries = this.currentContentElementEditing.attributesBackup.keyValueEntries  != null\n          ? [...this.currentContentElementEditing.attributesBackup.keyValueEntries.map(entry => ({ ...entry }))]\n          : [];\n        break;\n    }\n    this.currentContentElementEditing = null;\n    this.contentElementEditCanSubmit = true;\n    this.currentContentElementEditingMinMaxValid = true;\n  }\n\n  private getContentElementAttributesAndSetEditMode(sequenceIndex: number): ContentElementAttributes {\n    const emptyAttributes: ContentElementAttributes = {\n      name: null,\n      unit: null,\n      value: { asValueString: '', asDisplayString: '', asNumber: null },\n      min: { asValueString: '', asDisplayString: '', asNumber: null },\n      max: { asValueString: '', asDisplayString: '', asNumber: null },\n      keyValueEntries: null,\n    };\n\n    switch(this.formulaSequence[sequenceIndex].type) {\n      case FormulaElementType.Operator:\n        this.contentElementEditMode = ContentElementEditMode.Operator;\n        return emptyAttributes;\n      case FormulaElementType.Value:\n        this.contentElementEditMode = ContentElementEditMode.Value;\n        this.contentElementEditCanSubmit = true;\n        return {\n          ...emptyAttributes,\n          unit: this.values[this.formulaSequence[sequenceIndex].id].unit,\n          value: {\n            asValueString: this.values[this.formulaSequence[sequenceIndex].id].valueAsString,\n            asDisplayString: this.values[this.formulaSequence[sequenceIndex].id].displayString,\n            asNumber: this.values[this.formulaSequence[sequenceIndex].id].valueAsNumber,\n          },\n        };\n      case FormulaElementType.Variable:\n        this.contentElementEditMode = ContentElementEditMode.Variable;\n        this.contentElementEditCanSubmit = true;\n        this.currentContentElementEditingMinMaxValid = true;\n        return {\n          ...emptyAttributes,\n          name: this.variables[this.formulaSequence[sequenceIndex].id].name,\n          unit: this.variables[this.formulaSequence[sequenceIndex].id].unit,\n          min: {\n            asValueString: this.variables[this.formulaSequence[sequenceIndex].id].minValueAsString,\n            asDisplayString: this.variables[this.formulaSequence[sequenceIndex].id].minDisplayString,\n            asNumber: this.variables[this.formulaSequence[sequenceIndex].id].minValueAsNumber,\n          },\n          max: {\n            asValueString: this.variables[this.formulaSequence[sequenceIndex].id].maxValueAsString,\n            asDisplayString: this.variables[this.formulaSequence[sequenceIndex].id].maxDisplayString,\n            asNumber: this.variables[this.formulaSequence[sequenceIndex].id].maxValueAsNumber,\n          },\n        };\n      case FormulaElementType.KeyValueList:\n        this.contentElementEditMode = ContentElementEditMode.KeyValueList;\n        this.contentElementEditCanSubmit = false;\n        return {\n          ...emptyAttributes,\n          name: this.keyValueLists[this.formulaSequence[sequenceIndex].id].name,\n          keyValueEntries: [...this.keyValueLists[this.formulaSequence[sequenceIndex].id].entries.map(entry => ({ ...entry }))],\n        };\n\n      default:\n        return emptyAttributes;\n    }\n  }\n\n  private handleEditContentElementSave(): void {\n    this.deactivateContentElementEditing();\n    /**\n     * no storing in DB here! storing changes in DB only occurrs with dialog save!\n     * if that should change it would still only happen here if we are editing an existing formula\n     * storing a new formula while editing is not included in the concept now.\n     */\n  }\n\n  private handleEditContentElementCancel(): void {\n    assertNotNullOrUndefined(this.currentContentElementEditing, 'cannot cancel without current element');\n    const { sequenceIndex } = this.currentContentElementEditing;\n    this.revertContentElementAttributeChanges(sequenceIndex);\n    this.deactivateContentElementEditing();\n    this.prepareAndUpdateEditorView(sequenceIndex);\n  }\n\n  private handleEditContentElementDelete(): void {\n    assertNotNullOrUndefined(this.currentContentElementEditing, 'Need current content element editing to delete it!');\n    const { sequenceIndex } = this.currentContentElementEditing;\n\n    this.updateUniqueItemsOnDelete(sequenceIndex);\n    this.deleteCurrentContentElementEditing();\n    this.deactivateContentElementEditing();\n    this.checkFormulaValidAndCreateLatexFormula();\n    this.updateEditorView();\n    /**\n     * no storing in DB here! storing changes in DB only occurrs with dialog save!\n     * if that should change it would still only happen here if we are editing an existing formula\n     * storing a new formula while editing is not included in the concept now.\n     */\n  }\n\n  private deactivateContentElementEditing(): void {\n    this.contentElementEditCanSubmit = true;\n    this.currentContentElementEditingMinMaxValid = true;\n    this.currentContentElementEditing = null;\n    this.currentContentElementEditingName = '';\n    this.currentContentElementEditingUnit = '';\n    this.currentContentElementEditingValue = '';\n    this.currentContentElementEditingMin = '';\n    this.currentContentElementEditingMax = '';\n    this.currentContentElementEditingKeyValueEntries = [];\n    this.contentElementEditMode = ContentElementEditMode.Inactive;\n  }\n\n  private deleteCurrentContentElementEditing(): void {\n    assertNotNullOrUndefined(this.currentContentElementEditing, 'Need current content element editing to delete it!');\n    const { sequenceIndex } = this.currentContentElementEditing;\n\n    switch(this.currentContentElementEditing.type) {\n      case FormulaElementType.Operator:\n        delete this.operators[this.formulaSequence[sequenceIndex].id];\n        break;\n\n      case FormulaElementType.Value:\n        delete this.values[this.formulaSequence[sequenceIndex].id];\n        break;\n\n      case FormulaElementType.Variable:\n        delete this.variables[this.formulaSequence[sequenceIndex].id];\n        break;\n\n      case FormulaElementType.KeyValueList:\n        delete this.keyValueLists[this.formulaSequence[sequenceIndex].id];\n        break;\n    }\n    this.formulaSequence[this.currentContentElementEditing?.sequenceIndex].shadowElement?.remove();\n    this.formulaSequence.splice(this.currentContentElementEditing?.sequenceIndex, 1);\n  }\n\n  private currentContentElementEditingUnitChanged(): void {\n    if(this.currentContentElementEditing == null) return;\n    const { sequenceIndex } = this.currentContentElementEditing;\n    switch(this.currentContentElementEditing.type) {\n      case FormulaElementType.Value:\n        this.values[this.formulaSequence[sequenceIndex].id].unit = this.currentContentElementEditingUnit;\n        break;\n\n      case FormulaElementType.Variable:\n        this.variables[this.formulaSequence[sequenceIndex].id].unit = this.currentContentElementEditingUnit;\n        this.updateUnitsOfUniqueVariable(sequenceIndex);\n        break;\n    }\n\n    this.prepareAndUpdateEditorView(sequenceIndex);\n    this.checkCreateAndRenderLatexFormula();\n  }\n\n  private currentContentElementEditingValueChanged(): void {\n    if(this.currentContentElementEditing == null) return;\n    this.currentContentElementEditing.valueValid = this.currentContentElementEditingValue === ''\n      || this.numberValidationRegex.test(this.currentContentElementEditingValue);\n    const cleanedValueString = FormulaUtils.cleanNumberString(this\n      .currentContentElementEditingValue, this.currentContentElementEditing.valueValid);\n\n    const { sequenceIndex } = this.currentContentElementEditing;\n    this.values[this.formulaSequence[sequenceIndex].id].valueAsString = cleanedValueString;\n    this.values[this.formulaSequence[sequenceIndex].id].displayString = FormulaUtils\n      .convertToDisplayString(cleanedValueString, FORMULA_NUMBER_VALIDATION_REGEX);\n    this.values[this.formulaSequence[sequenceIndex].id].valueAsNumber = FormulaUtils\n      .convertStringToNumber(this.currentContentElementEditingValue, FORMULA_NUMBER_VALIDATION_REGEX) ?? null;\n\n    this.prepareAndUpdateEditorView(sequenceIndex);\n    this.checkFormulaValidAndCreateLatexFormula();\n  }\n\n  private currentContentElementEditingNameChanged(): void {\n    if(this.currentContentElementEditing == null) return;\n    const { sequenceIndex } = this.currentContentElementEditing;\n\n    switch(this.currentContentElementEditing.type) {\n      case FormulaElementType.Variable:\n        this.variables[this.formulaSequence[sequenceIndex].id].name = this.currentContentElementEditingName;\n        this.uniqueVariables = this.updateUniqueItemsOnRename(\n          FormulaElementType.Variable, sequenceIndex, this.uniqueVariables, this.variables);\n        break;\n\n      case FormulaElementType.KeyValueList:\n        this.keyValueLists[this.formulaSequence[sequenceIndex].id].name = this.currentContentElementEditingName;\n        this.uniqueKeyValueLists = this.updateUniqueItemsOnRename(\n          FormulaElementType.KeyValueList, sequenceIndex, this.uniqueKeyValueLists, this.keyValueLists);\n        break;\n    }\n\n    this.prepareAndUpdateEditorView(sequenceIndex);\n    this.checkFormulaValidAndCreateLatexFormula();\n  }\n\n  private updateEntriesOfUniqueKeyValueLists(\n    name: string,\n    updateWithEntries: IKeyValueEntry[] | null,\n  ): void {\n    const uniqueKeyValueList = this.uniqueKeyValueLists\n      .find(item => item.name === name);\n    if(!uniqueKeyValueList) return;\n    const setEntries: IKeyValueEntry[] = updateWithEntries != null ? [...updateWithEntries.map(entry => ({ ...entry }))] : [];\n    for(const occurrenceIndex of uniqueKeyValueList.occurrences) {\n      this.keyValueLists[this.formulaSequence[occurrenceIndex].id].entries = setEntries;\n    }\n  }\n\n  private updateMinMaxOfUniqueVariables(\n    name: string | null,\n    limitType: LimitType,\n    updateWithMinValue?: ContentElementNumber | null,\n    updateWithMaxValue?: ContentElementNumber | null,\n  ): void {\n    if(!name) return;\n    const uniqueVariable = this.uniqueVariables\n      .find(item => item.name === name);\n    if(!uniqueVariable) return;\n    const emptyValue: ContentElementNumber = {\n      asValueString: '',\n      asDisplayString: '',\n      asNumber: null,\n    };\n    const setMinValue: ContentElementNumber = updateWithMinValue != null ? {\n      asValueString: updateWithMinValue.asValueString,\n      asDisplayString: updateWithMinValue.asDisplayString,\n      asNumber: updateWithMinValue.asNumber,\n    } : emptyValue;\n    const setMaxValue: ContentElementNumber = updateWithMaxValue != null ? {\n      asValueString: updateWithMaxValue.asValueString,\n      asDisplayString: updateWithMaxValue.asDisplayString,\n      asNumber: updateWithMaxValue.asNumber,\n    } : emptyValue;\n    for(const occurrenceIndex of uniqueVariable.occurrences) {\n      if(limitType === LimitType.Min || limitType === LimitType.Both) {\n        this.setMinMaxOfVariable(occurrenceIndex, LimitType.Min, setMinValue);\n      }\n      if(limitType === LimitType.Max || limitType === LimitType.Both) {\n        this.setMinMaxOfVariable(occurrenceIndex, LimitType.Max, setMaxValue);\n      }\n    }\n  }\n\n  private setMinMaxOfVariable(\n    index: number,\n    limitType: LimitType,\n    setValue: ContentElementNumber\n  ): void {\n    if(limitType === LimitType.Min) {\n      this.variables[this.formulaSequence[index].id].minValueAsString = setValue.asValueString ?? '';\n      this.variables[this.formulaSequence[index].id].minDisplayString = setValue.asDisplayString;\n      this.variables[this.formulaSequence[index].id].minValueAsNumber = setValue.asNumber;\n    }\n    if(limitType === LimitType.Max) {\n      this.variables[this.formulaSequence[index].id].maxValueAsString = setValue.asValueString ?? '';\n      this.variables[this.formulaSequence[index].id].maxDisplayString = setValue.asDisplayString;\n      this.variables[this.formulaSequence[index].id].maxValueAsNumber = setValue.asNumber;\n    }\n  }\n\n  private resultNameChanged(): void {\n    this.checkCreateAndRenderLatexFormula();\n  }\n\n  private resultUnitChanged(): void {\n    this.checkCreateAndRenderLatexFormula();\n  }\n\n  private resultArgumentsChanged(): void {\n    this.checkCreateAndRenderLatexFormula();\n  }\n\n  private checkCreateAndRenderLatexFormula(): void {\n    if(!this.katexFormulaElement) return;\n    if(this.isFormulaValid && this.formulaSequence.length) {\n      this.createAndRenderLatexFormula();\n    } else {\n      this.renderLatexFormula();\n    }\n  }\n\n  private prepareAndUpdateEditorView(sequenceIndex: number): void {\n    this.prepareInnerHtml(sequenceIndex);\n    this.updateEditorView();\n  }\n\n  private prepareInnerHtml(sequenceIndex: number): void {\n    const { innerHtml, innerHtmlUnit } = this.getInnerHtmlForExistingContentElement(\n      this.formulaSequence[sequenceIndex].type,\n      this.formulaSequence[sequenceIndex].id,\n    );\n    this.formulaSequence[sequenceIndex].innerHtml = innerHtml;\n    this.formulaSequence[sequenceIndex].innerHtmlUnit = innerHtmlUnit;\n    assertNotNullOrUndefined(this.formulaSequence[sequenceIndex].shadowElement, 'Need shadow element for upcoming width calculation');\n    this.formulaSequence[sequenceIndex].shadowElement! // sadly typescript doesn't get the assertion above\n      .innerHTML = innerHtmlUnit == null ? innerHtml : `${innerHtml}&nbsp;${innerHtmlUnit}`;\n  }\n\n  private manageEditorWidth(): void {\n    assertNotNullOrUndefined(this.editorParentElement, 'Editor Parent Element must be available!');\n    this.editorWidth = this.editorParentElement.getBoundingClientRect().width;\n  }\n\n  private updateEditorView(): void {\n    // prepare for calculation\n    if(this.editorParentElement == null) return;\n\n    this.manageEditorWidth();\n    this.maxContentElementWidth = this.editorWidth - (this.dndTargetWidth + this.targetedGap);\n    this.editorRowsBuffer = [];\n    this.dndTargetDisplayRows = [];\n\n    if(this.formulaSequence.length) {\n      if(this.formulaSequence.length > 1) {\n        // checkAndCreateRow is recursive and will produce all required rows\n        this.checkAndCreateRow(0, 0, 2);\n      } else {\n        // only one element in sequence. it gets its own row.\n        this.addSingleElementRow(0, 0);\n      }\n    } else {\n      // empty sequence - setup one row with one spacer acting as empty formula dropzone.\n      this.prepareNewRow();\n      this.addElementToRow(0, false, 0, DndSequentialDropzonePosition.Last, this.editorWidth);\n    }\n\n    // let the calculated changes go live:\n    this.editorRows = this.editorRowsBuffer;\n  }\n\n  private prepareNewRow(): void {\n    this.editorRowsBuffer.push({ index: -1, elements: [] });\n    this.dndTargetDisplayRows.push({});\n  }\n\n  private checkAndCreateRow(\n    currentRowIndex: number,\n    startRowAtFormulaSequenceIndex: number,\n    checkCount: number,\n  ): void {\n    const lastCheckInSequence = startRowAtFormulaSequenceIndex + checkCount === this.formulaSequence.length;\n\n    // check if the requrested checkCount fits in one row\n    let accumulatedElementWidths = 0; // let !\n    for(let i = startRowAtFormulaSequenceIndex; i < startRowAtFormulaSequenceIndex + checkCount; i++) {\n      accumulatedElementWidths += this.getWidth(i);\n    }\n\n    /** without width animation */\n    const testWidth = accumulatedElementWidths + (checkCount * 2 + 1) * (this.targetedGap + this.dndTargetWidth);\n\n    /** with width animation */\n    // const testWidth = this.dndTargetWidth\n    //   + (checkCount - 2) * this.defaultGap // default gaps for non targgeted targets\n    //   + 4 * this.targetedGap // possibly 2 targeted targets at the same time, each gets 2 * this.targetedGap\n    //   + accumulatedElementWidths;\n\n    const checkCountFits = testWidth < this.editorWidth;\n\n    if(checkCountFits) {\n      // are we finished with the sequence? -> setup row!\n      if(lastCheckInSequence) {\n        const createContentElementCount = checkCount;\n        this.addMultiElementRow(currentRowIndex, startRowAtFormulaSequenceIndex, createContentElementCount);\n      } else {\n        // more elements available and more to check, continue ...\n        const nextCheckCount = checkCount + 1;\n        if(this.formulaSequence.length < startRowAtFormulaSequenceIndex + nextCheckCount) return;\n        // start next check for n + 1 elements in current row\n        this.checkAndCreateRow(currentRowIndex, startRowAtFormulaSequenceIndex, nextCheckCount);\n      }\n    } else {\n      // create elements that fit in this row:\n      const createContentElementCount = checkCount - 1;\n      if(checkCount === 2) {\n        this.addSingleElementRow(currentRowIndex, startRowAtFormulaSequenceIndex);\n      } else {\n        this.addMultiElementRow(currentRowIndex, startRowAtFormulaSequenceIndex, createContentElementCount);\n      }\n      const nextFormulaSequenceIndex = startRowAtFormulaSequenceIndex + createContentElementCount;\n      // is only one element left over? create it in its own row and be done:\n      if(lastCheckInSequence) {\n        this.addSingleElementRow(currentRowIndex + 1, nextFormulaSequenceIndex);\n        return;\n      }\n      // more elements to add, start a fresh check at the next reasonable index\n      const newCheckCount = 2;\n      if(this.formulaSequence.length < nextFormulaSequenceIndex + newCheckCount) return;\n      // start next check for two elements in next row\n      this.checkAndCreateRow(currentRowIndex + 1, nextFormulaSequenceIndex, newCheckCount);\n    }\n  }\n\n  private addMultiElementRow(\n    currentRowIndex: number,\n    startRowAtFormulaSequenceIndex: number,\n    createContentElementCount: number,\n  ): void {\n    // prepare row\n    this.prepareNewRow();\n    let widthUpToLastSpacer = 0;\n\n    // first spacer\n    this.addElementToRow(currentRowIndex, false, startRowAtFormulaSequenceIndex, DndSequentialDropzonePosition.First, 0);\n    // inbetween elements and spacers\n    for(let i = 0; i < createContentElementCount - 1; i++) {\n      // middle content elements\n      const formulaSequenceIndex = startRowAtFormulaSequenceIndex + i;\n      widthUpToLastSpacer += this.getWidth(formulaSequenceIndex);\n      this.addElementToRow(currentRowIndex, true, formulaSequenceIndex, null, null);\n      // inbetween spacer\n      widthUpToLastSpacer += this.defaultGap;\n      this.addElementToRow(currentRowIndex, false, formulaSequenceIndex + 1, DndSequentialDropzonePosition.Inbetween, this.defaultGap);\n    }\n    // last content element\n    const formulaSequenceIndex = startRowAtFormulaSequenceIndex + createContentElementCount - 1;\n    widthUpToLastSpacer += this.getWidth(formulaSequenceIndex);\n    this.addElementToRow(currentRowIndex, true, formulaSequenceIndex, null, null);\n    // final spacer\n    const lastSpacerWidth = this.editorWidth - widthUpToLastSpacer;\n    this.addElementToRow(currentRowIndex, false, formulaSequenceIndex + 1, DndSequentialDropzonePosition.Last, lastSpacerWidth);\n  }\n\n  private addSingleElementRow(currentRowIndex: number, sequenceIndex: number): void {\n    // prepare row\n    this.prepareNewRow();\n    // first spacer\n    this.addElementToRow(currentRowIndex, false, sequenceIndex, DndSequentialDropzonePosition.First, 0);\n    // content element\n    const limitedWidth = this.getWidth(sequenceIndex) > this.maxContentElementWidth\n      ? this.maxContentElementWidth : null;\n    this.addElementToRow(currentRowIndex, true, sequenceIndex, null, limitedWidth, true);\n    // final spacer\n    const lastSpacerWidth = this.editorWidth - this.getWidth(sequenceIndex);\n    this.addElementToRow(currentRowIndex, false, sequenceIndex + 1, DndSequentialDropzonePosition.Last, lastSpacerWidth);\n  }\n\n  /**\n   * provide sequenceIndex = null in case of non-content element\n   * provide position = null in case of content element, must provide for non-content elements\n   * provide width = null in case this function should read width itself\n   */\n  private addElementToRow(\n    rowIndex: number,\n    isContent: boolean,\n    sequenceIndex: number,\n    position: DndSequentialDropzonePosition | null,\n    width: number | null,\n    overrideWidth = false,\n  ): void {\n    // setup in case of contentElement\n    this.editorRowsBuffer[rowIndex].index += 1;\n    let usedWidth = width;\n    let innerHtml = '';\n    let innerHtmlUnit: string | null = null;\n    let backgroundColorVar = 'var(--color-formula-operator)';\n\n    if(isContent && sequenceIndex != null) {\n      innerHtml = this.formulaSequence[sequenceIndex].innerHtml ?? '';\n      ({ innerHtmlUnit } = this.formulaSequence[sequenceIndex]);\n      if(!overrideWidth) {\n        usedWidth = this.getWidth(sequenceIndex);\n      }\n      backgroundColorVar = this.getBackgroundColorVar(sequenceIndex);\n    }\n\n    this.editorRowsBuffer[rowIndex].elements.push({\n      isContent,\n      backgroundColorVar,\n      dropzoneConfig: {\n        identifier: {\n          superId: rowIndex,\n          subId: this.editorRowsBuffer[rowIndex].index,\n        },\n        insertModeSelector: ContentEditingInsertModeSelector.Sequential,\n        insertModeSequential: DndInsertModeSequential.Before,\n        sequentialOrder: sequenceIndex,\n        sequentialPosition: isContent ? undefined : DndSequentialDropzonePosition.Inbetween, // only required for non content element dropzones\n        cardinalInto: false,\n      },\n      innerHtml,\n      innerHtmlUnit,\n      sequenceIndex: isContent && sequenceIndex != null ? sequenceIndex : undefined,\n      styles: `width: ${usedWidth}px;`,\n    });\n\n    // initial setting for all dnd target displays\n    if(!isContent && position != null) {\n      this.dndTargetDisplayRows[rowIndex][this.editorRowsBuffer[rowIndex].index] = {\n        isTargeted: false,\n        position,\n        targetedByAddresses: [],\n      };\n    }\n  }\n\n  private getWidth(sequenceIndex: number): number {\n    const sequenceElement = this.formulaSequence[sequenceIndex];\n    let width = sequenceElement.shadowElement != null\n      ? sequenceElement.shadowElement?.getBoundingClientRect().width : 0;\n    /**\n     * must correct width if unit is set, because in view two spans are used\n     * and they have a wider width than the plain innerHtml of the shadowElement.\n     * the magic number seems to be 4.3 px.\n     */\n    if(width !== 0\n      && ((sequenceElement.type === FormulaElementType.Variable\n        && this.variables[sequenceElement.id].unit)\n        || (sequenceElement.type === FormulaElementType.Value\n          && this.values[sequenceElement.id].unit))) {\n      width += 4.3;\n    }\n    return width;\n  }\n\n  private getBackgroundColorVar(sequenceIndex: number): string {\n    const elementType = this.formulaSequence[sequenceIndex].type;\n    if(elementType !== FormulaElementType.Operator) {\n      return 'var(--color-field-background)';\n    }\n    const operatorType = this.operators[this.formulaSequence[sequenceIndex].id].operator;\n    switch(operatorType) {\n      case FormulaOperatorType.BracketOpen:\n        return this.isOperatorDivisionOpeningBracket(sequenceIndex)\n          ? 'var(--color-formula-operator)'\n          : 'var(--color-formula-bracket)';\n      case FormulaOperatorType.Root:\n        return 'var(--color-formula-root)';\n      case FormulaOperatorType.BracketClose:\n        return this.checkClosingBracketTypeAndGetColorVar(sequenceIndex);\n      default:\n        return 'var(--color-formula-operator)';\n    }\n  }\n\n  private checkClosingBracketTypeAndGetColorVar(sequenceIndex: number): string {\n    // starting with a closing bracket?\n    if(sequenceIndex === 0) return 'var(--color-formula-bracket)';\n\n    // search towards start of sequence and find matching bracket open/root open/division\n    let closeCount = 1;\n    for(let i = sequenceIndex - 1; i >= 0; i--) {\n      if(this.formulaSequence[i].type === FormulaElementType.Operator) {\n        const { operator } = this.operators[this.formulaSequence[i].id];\n        switch(operator) {\n          case FormulaOperatorType.BracketClose:\n            closeCount += 1;\n            break;\n          case FormulaOperatorType.BracketOpen:\n            if(closeCount === 1) return 'var(--color-formula-bracket)';\n            closeCount -= 1;\n            break;\n          case FormulaOperatorType.Root:\n            if(closeCount === 1) return 'var(--color-formula-root)';\n            closeCount -= 1;\n            break;\n          case FormulaOperatorType.Divide:\n            if(closeCount === 1) return 'var(--color-formula-operator)';\n            break;\n        }\n      }\n    }\n    return 'var(--color-formula-bracket)';\n  }\n\n  private isOperatorDivisionOpeningBracket(sequenceIndex: number): boolean {\n    // ending with an opening bracket?\n    if(sequenceIndex === this.formulaSequence.length - 1) return false;\n\n    // search towards end of sequence and check if a division corresponds to open bracket\n    let openCount = 1;\n    for(let i = sequenceIndex + 1; i < this.formulaSequence.length; i++) {\n      if(this.formulaSequence[i].type === FormulaElementType.Operator) {\n        const { operator } = this.operators[this.formulaSequence[i].id];\n        switch(operator) {\n          case FormulaOperatorType.BracketOpen:\n          case FormulaOperatorType.Root:\n            openCount += 1;\n            break;\n          case FormulaOperatorType.BracketClose:\n            if(openCount === 1) return false;\n            openCount -= 1;\n            break;\n          case FormulaOperatorType.Divide:\n            if(openCount === 1) return true;\n            openCount -= 1;\n            break;\n        }\n      }\n    }\n    return false;\n  }\n\n  private extractUniqueItems<T extends IUniqueFormulaItemBase>(\n    type: FormulaElementType,\n    uniqueItemArray: IUniqueFormulaItem[],\n    itemsToCheck: Record<number, T>,\n  ): IUniqueFormulaItem[] {\n    const newUniqueItemArray = [...uniqueItemArray.map(item => ({ ...item }))];\n    for(const [index, formulaElement] of this.formulaSequence.entries()) {\n      if(formulaElement.type === type) {\n        const candidate = itemsToCheck[formulaElement.id];\n        const uniqueIndex = newUniqueItemArray\n          .findIndex(uniqueItem => uniqueItem.name === candidate.name);\n        if(uniqueIndex === -1) {\n          newUniqueItemArray.push({\n            name: candidate.name,\n            occurrences: [index],\n          });\n        } else {\n          newUniqueItemArray[uniqueIndex].occurrences.push(index);\n        }\n      }\n    }\n    return newUniqueItemArray;\n  }\n\n  /**\n   * works for any uniqueItem array, e.g. variables, keyValueLists\n   */\n  private updateUniqueItemOccurenceIndizesOnAdd(\n    sequenceIndex: number,\n    uniqueItemArray: IUniqueFormulaItem[],\n  ): IUniqueFormulaItem[] {\n    // increase all refrences above sequence Index\n    return uniqueItemArray.map(uniqueItem => ({\n      name: uniqueItem.name,\n      occurrences: uniqueItem.occurrences.map(oldIndex => {\n        return oldIndex >= sequenceIndex ? oldIndex + 1 : oldIndex;\n      }),\n    }));\n    // on add no updating of unique items required, since empty strings are not relevant\n  }\n\n  /**\n   * potentially remove unique item from unique occurrences list and adjust\n   * all unique item lists indizes\n   */\n  private updateUniqueItemsOnDelete(\n    sequenceIndex: number,\n  ): void {\n    const formulaElement = this.formulaSequence[sequenceIndex];\n    if(formulaElement.type === FormulaElementType.Variable\n      || formulaElement.type === FormulaElementType.KeyValueList) {\n\n      let uniqueItemArray: IUniqueFormulaItem[] = [];\n      let itemsToCheck: Record<number, IUniqueFormulaItemBase> = {};\n      switch(formulaElement.type) {\n        case FormulaElementType.Variable:\n          uniqueItemArray = [...this.uniqueVariables.map(variable => ({ ...variable }))];\n          itemsToCheck = this.variables;\n          break;\n\n        case FormulaElementType.KeyValueList:\n          uniqueItemArray = [...this.uniqueKeyValueLists.map(list => ({ ...list }))];\n          itemsToCheck = this.keyValueLists;\n          break;\n      }\n\n      const itemToCheck = itemsToCheck[formulaElement.id];\n      if(itemToCheck.name !== '') {\n        const uniqueItemIndex = uniqueItemArray\n          .findIndex(uniqueVar => uniqueVar.name === itemToCheck.name);\n        if(uniqueItemIndex !== -1) {\n          if(uniqueItemArray[uniqueItemIndex].occurrences.length > 1) {\n            // remove occurrence of itemToCheck\n            const occurrenceIndex = uniqueItemArray[uniqueItemIndex].occurrences\n              .findIndex(occurrence => occurrence === sequenceIndex);\n            uniqueItemArray[uniqueItemIndex].occurrences.splice(occurrenceIndex, 1);\n          } else {\n            // remove unique itemToCheck\n            uniqueItemArray.splice(uniqueItemIndex, 1);\n          }\n        } else {\n          LogUtils.error(`FormulaEditor Error: trying to remove ${formulaElement.type} ${itemToCheck\n            .name} @sequenceIndex: ${sequenceIndex} from unique item list, but cannot find it there!`);\n        }\n      }\n\n      switch(formulaElement.type) {\n        case FormulaElementType.Variable:\n          this.uniqueVariables = uniqueItemArray;\n          break;\n\n        case FormulaElementType.KeyValueList:\n          this.uniqueKeyValueLists = uniqueItemArray;\n          break;\n      }\n    }\n\n    // decrease all refrences above sequence Index\n    this.uniqueVariables = this.uniqueVariables.map(uniqueItem => ({\n      name: uniqueItem.name,\n      occurrences: uniqueItem.occurrences.map(oldIndex => {\n        return oldIndex >= sequenceIndex ? oldIndex - 1 : oldIndex;\n      }),\n    }));\n    this.uniqueKeyValueLists = this.uniqueKeyValueLists.map(uniqueItem => ({\n      name: uniqueItem.name,\n      occurrences: uniqueItem.occurrences.map(oldIndex => {\n        return oldIndex >= sequenceIndex ? oldIndex - 1 : oldIndex;\n      }),\n    }));\n  }\n\n  /**\n * updates occurrence sequenceIndices according to changes in sequence.\n * oldIndex and newIndex are the according sequenceIndices of the move.\n */\n  private updateUniqueItemsOnMove(\n    uniqueItemArray: IUniqueFormulaItem[],\n    oldIndex: number,\n    newIndex: number,\n    moveToFront: number,\n    moveToBack: number,\n  ): IUniqueFormulaItem[] {\n    return uniqueItemArray.map(uniqueEntry => ({\n      name: uniqueEntry.name,\n      occurrences: uniqueEntry.occurrences.map(oldOccurrenceIndex => {\n        if(oldOccurrenceIndex === oldIndex) {\n          return newIndex - 1 * moveToBack;\n        } else if(moveToFront && oldOccurrenceIndex >= newIndex && oldOccurrenceIndex < oldIndex) {\n          return oldOccurrenceIndex + 1;\n        } else if(moveToBack && oldOccurrenceIndex < newIndex && oldOccurrenceIndex > oldIndex) {\n          return oldOccurrenceIndex - 1;\n        }\n        return oldOccurrenceIndex;\n      }),\n    }));\n  }\n\n  private updateUniqueItemsOnRename<T extends IUniqueFormulaItemBase>(\n    formulaElementType: FormulaElementType,\n    sequenceIndex: number,\n    uniqueItemArray: IUniqueFormulaItem[],\n    itemsToCheck: Record<number, T>,\n  ): IUniqueFormulaItem[] {\n    let newUniqueItemArray = [...uniqueItemArray.map(item => ({ ...item }))];\n    // check if variable at sequence index occurred in unique variables at all\n    const itemToCheck = itemsToCheck[this.formulaSequence[sequenceIndex].id];\n    const oldUniqueItemIndex = newUniqueItemArray.findIndex(uniqueVar => uniqueVar\n      .occurrences.some(occurrence => occurrence === sequenceIndex));\n    const newNameJoinsAnotherUniqueItemIndex = newUniqueItemArray\n      .findIndex(uniqueVar => (uniqueVar.name === itemToCheck.name));\n\n    if(oldUniqueItemIndex !== -1 && oldUniqueItemIndex === newNameJoinsAnotherUniqueItemIndex) {\n      return newUniqueItemArray;\n    }\n\n    if(oldUniqueItemIndex !== -1) {\n      const oldNameWasSingleOccurrence = newUniqueItemArray[oldUniqueItemIndex].occurrences.length === 1;\n\n      if(!oldNameWasSingleOccurrence) {\n        // old name had multiple occurrences -> remove occurrence\n        const oldOccurrenceIndex = newUniqueItemArray[oldUniqueItemIndex].occurrences\n          .findIndex(occurrence => occurrence === sequenceIndex);\n        newUniqueItemArray[oldUniqueItemIndex].occurrences.splice(oldOccurrenceIndex, 1);\n      }\n\n      if(newNameJoinsAnotherUniqueItemIndex !== -1) {\n        // another unique item already has the new name\n        newUniqueItemArray = this.addOccurrenceToPreExistingUniqueItem(\n          formulaElementType, newUniqueItemArray, newNameJoinsAnotherUniqueItemIndex, sequenceIndex);\n\n        // remove old unique item if necessary\n        if(oldNameWasSingleOccurrence) newUniqueItemArray.splice(oldUniqueItemIndex, 1);\n      } else {\n        // name is new\n        if(itemToCheck.name !== '') {\n          if(oldNameWasSingleOccurrence) {\n            // old name had a single occurrence -> just rename unique itemToCheck\n            newUniqueItemArray[oldUniqueItemIndex].name = itemToCheck.name;\n          } else {\n            // need a new unique itemToCheck\n            newUniqueItemArray.push({\n              name: itemToCheck.name,\n              occurrences: [sequenceIndex],\n            });\n          }\n        } else if(oldNameWasSingleOccurrence) {\n          // if new name is empty and the old one had only one occurrence remove unique itemToCheck\n          newUniqueItemArray.splice(oldUniqueItemIndex, 1);\n        }\n      }\n    } else if(itemToCheck.name !== '') {\n      // no previous record of itemToCheck, update existing or create new?\n      if(newNameJoinsAnotherUniqueItemIndex !== -1) {\n        // another unique item already has the new name\n        newUniqueItemArray = this.addOccurrenceToPreExistingUniqueItem(\n          formulaElementType, newUniqueItemArray, newNameJoinsAnotherUniqueItemIndex, sequenceIndex);\n      } else {\n        newUniqueItemArray.push({\n          name: itemToCheck.name,\n          occurrences: [sequenceIndex],\n        });\n      }\n    }\n    return newUniqueItemArray;\n  }\n\n  private addOccurrenceToPreExistingUniqueItem(\n    formulaElementType: FormulaElementType,\n    uniqueItemArray: IUniqueFormulaItem[],\n    newNameJoinsAnotherUniqueItemIndex: number,\n    sequenceIndex: number,\n  ): IUniqueFormulaItem[] {\n    const newUniqueItemArray = [...uniqueItemArray.map(item => ({ ...item }))];\n    // add occurrence\n    newUniqueItemArray[newNameJoinsAnotherUniqueItemIndex].occurrences.push(sequenceIndex);\n    const uniqueItem = newUniqueItemArray[newNameJoinsAnotherUniqueItemIndex];\n    const uniqueItemOtherSequenceIndex = uniqueItem.occurrences[uniqueItem.occurrences.length - 2];\n\n    switch(formulaElementType) {\n      case FormulaElementType.Variable:\n        {\n          // copy unit, min and max from pre-existing occurrences\n          const variableToCopy = { ...this.variables[this.formulaSequence[uniqueItemOtherSequenceIndex].id] };\n          // eslint-disable-next-line @typescript-eslint/no-unused-vars\n          const { id, name, ...valuesToCopy } = variableToCopy;\n\n          this.variables[this.formulaSequence[sequenceIndex].id] = {\n            id: this.variables[this.formulaSequence[sequenceIndex].id].id,\n            name: this.variables[this.formulaSequence[sequenceIndex].id].name,\n            ...valuesToCopy,\n          };\n\n          this.currentContentElementEditingUnit = valuesToCopy.unit; // could be written to wrongfully! in case of update = revert\n          this.currentContentElementEditingMin = valuesToCopy.minValueAsString;\n          this.currentContentElementEditingMax = valuesToCopy.maxValueAsString;\n        }\n        break;\n\n      case FormulaElementType.KeyValueList:\n        {\n          // copy keyValueEntries from pre-existing occurrences\n          const entriesToCopy = this.keyValueLists[this.formulaSequence[uniqueItemOtherSequenceIndex].id].entries;\n          this.keyValueLists[this.formulaSequence[sequenceIndex].id].entries = [...entriesToCopy.map(entry => ({ ...entry }))];\n          this.currentContentElementEditingKeyValueEntries = [...entriesToCopy.map(entry => ({ ...entry }))];\n        }\n        break;\n    }\n\n    return newUniqueItemArray;\n  }\n\n  /**\n   * unit of unique variable occurrence at \"sequenceIndex\" has been updated,\n   * now all other occurrences need their unit updated aswell.\n   */\n  private updateUnitsOfUniqueVariable(sequenceIndex: number): void {\n    const uniqueVariableIndex = this.uniqueVariables.findIndex(uniqueVar => uniqueVar\n      .occurrences.some(occurrence => occurrence === sequenceIndex));\n    if(uniqueVariableIndex !== -1) {\n      const unitToCopy = this.variables[this.formulaSequence[sequenceIndex].id].unit;\n      const otherOccurrences = this.uniqueVariables[uniqueVariableIndex]\n        .occurrences.filter(occurrence => occurrence !== sequenceIndex);\n      otherOccurrences.forEach(occurrence => {\n        this.variables[this.formulaSequence[occurrence].id].unit = unitToCopy;\n        this.prepareInnerHtml(occurrence);\n      });\n    }\n  }\n}\n\n","import { autoinject, bindable } from 'aurelia-framework';\nimport {\n  FORMULA_NUMBER_VALIDATION_REGEX,\n  FORMULA_TEXT_VALIDATION_REGEX,\n} from 'common/constants/formula.constants';\nimport { IKeyValueEntry } from 'common/dto/course/content/formula.interface';\nimport { ArrayUtils } from 'common/utils/ArrayUtils';\nimport { assertNotNullOrUndefined } from 'common/utils/assert';\nimport { DomUtils } from 'common/utils/DomUtils';\nimport { FormulaUtils } from 'common/utils/FormulaUtils';\nimport { LogUtils } from 'common/utils/LogUtils';\n\ninterface IKeyValuEntryWithDisplayData extends IKeyValueEntry {\n  isValueValid: boolean;\n  isKeyValid: boolean;\n  showValueInvalid: boolean;\n  showKeyInvalid: boolean;\n}\n\n/**\n * @event {KeyValueListUpdatedEvent} key-value-list-updated\n * @event {CanSubmitUpdatedEvent} can-submit-updated\n */\n@autoinject()\nexport class FormulaKeyValueList {\n  @bindable() keyValueList: IKeyValueEntry[] = [];\n\n  private keyValueEntries: IKeyValuEntryWithDisplayData[] = [];\n  private maxOrder = 0;\n  private keyValueListElement: HTMLElement;\n  private listElement?: HTMLElement;\n  private numberValidationRegex = new RegExp(FORMULA_NUMBER_VALIDATION_REGEX);\n  private textValidationRegex = new RegExp(FORMULA_TEXT_VALIDATION_REGEX);\n  private hoveredEntryIndex: number | null = null;\n  private canSubmit = false;\n\n  constructor(\n    element: Element,\n  ) {\n    this.keyValueListElement = element as HTMLElement;\n  }\n\n  private keyValueListChanged(): void {\n    this.keyValueEntries = ArrayUtils.immutableSort(\n      this.keyValueList,\n      (a, b) => a.order - b.order,\n    ).map(entry => ({\n      ...entry,\n      isValueValid: true,\n      isKeyValid: true,\n      showValueInvalid: false,\n      showKeyInvalid: false,\n    }));\n    this.maxOrder = this.keyValueEntries.length - 1;\n\n    this.ensureCanSubmit();\n  }\n\n  private setHoveredEntryIndex(index: number): void {\n    this.hoveredEntryIndex = index;\n  }\n\n  private resetHoveredEntryIndex(): void {\n    this.hoveredEntryIndex = null;\n  }\n\n  private addEntry(): void {\n    this.maxOrder += 1;\n    this.keyValueEntries.push({\n      order: this.maxOrder,\n      key: '',\n      valueAsString: '',\n      displayString: '',\n      valueAsNumber: null,\n      isValueValid: false,\n      isKeyValid: false,\n      showKeyInvalid: false,\n      showValueInvalid: false,\n    });\n\n    this.ensureCanSubmit();\n    this.updateList();\n\n    // scroll to bottom if necessary on add entry, timeout required to reach bottom\n    assertNotNullOrUndefined(this.listElement, 'forgot to ref listElement');\n    window.setTimeout(() => {\n      this.listElement?.scrollTo({\n        top: this.listElement.scrollHeight,\n        behavior: 'auto',\n      });\n    }, 80);\n  }\n\n  /**\n   * array index and order attribute must be in sync!\n   */\n  private removeEntry(index: number): void {\n    this.keyValueEntries.splice(index, 1);\n    this.keyValueEntries = this.keyValueEntries.map(entry => {\n      return {\n        ...entry,\n        order: entry.order > index ? entry.order - 1 : entry.order,\n      };\n    });\n    this.maxOrder -= 1;\n\n    this.ensureKeysValid();\n    this.ensureCanSubmit();\n    this.updateList();\n  }\n\n  /**\n   * increase priority of element at index/order (= decrease order)\n   */\n  private moveUp(index: number): void {\n    if(index < 1) {\n      LogUtils.error(`FormulaEditor Error: Trying invalid order change of keyValueList @index: ${index}`);\n      return;\n    }\n    this.keyValueEntries[index].order -= 1;\n    this.keyValueEntries[index - 1].order += 1;\n    this.keyValueEntries.sort((a, b) => a.order - b.order);\n    this.updateList();\n  }\n\n  private updateKey(entryIndex: number, event: Event): void {\n    const targetElement = event.target as HTMLInputElement;\n    const newKey = targetElement.value;\n\n    if(newKey && !this.textValidationRegex.test(newKey)) {\n      targetElement.value = this.keyValueEntries[entryIndex].key;\n      return;\n    }\n    this.keyValueEntries[entryIndex].key = newKey;\n\n    this.ensureKeysValid();\n    this.ensureCanSubmit();\n    this.updateList();\n  }\n\n  private updateValue(index: number, event: Event): void {\n    const { value } = event.target as HTMLInputElement;\n    const isValid = this.numberValidationRegex.test(value);\n    this.keyValueEntries[index].isValueValid = isValid;\n    this.keyValueEntries[index].showValueInvalid = !isValid;\n\n    if(isValid) {\n      const cleanedValueString = FormulaUtils.cleanNumberString(value, isValid);\n      this.keyValueEntries[index].valueAsString = cleanedValueString;\n      this.keyValueEntries[index].displayString = !isValid ? '' : FormulaUtils\n        .convertToDisplayString(cleanedValueString, FORMULA_NUMBER_VALIDATION_REGEX);\n      this.keyValueEntries[index].valueAsNumber = !isValid ? null : FormulaUtils\n        .convertStringToNumber(value, FORMULA_NUMBER_VALIDATION_REGEX) ?? null;\n      this.keyValueEntries[index].showValueInvalid = this.keyValueEntries[index].valueAsString !== '';\n    }\n\n    this.ensureCanSubmit();\n    this.updateList();\n  }\n\n  private updateList(): void {\n    if(!this.canSubmit) return;\n    DomUtils.fireEvent(this.keyValueListElement, {\n      name: 'key-value-list-updated',\n      detail: {\n        entries: this.keyValueEntries.map(entry => ({\n          order: entry.order,\n          key: entry.key,\n          valueAsString: entry.valueAsString,\n          displayString: entry.displayString,\n          valueAsNumber: entry.valueAsNumber,\n        })),\n      },\n      bubbles: false,\n    });\n  }\n\n  private updateCanSubmit(): void {\n    DomUtils.fireEvent(this.keyValueListElement, {\n      name: 'can-submit-updated',\n      detail: {\n        canSubmit: this.canSubmit,\n      },\n      bubbles: false,\n    });\n  }\n\n  private ensureKeysValid(): void {\n    const keyOccurrences: { [name: string]: number } = {};\n\n    for(const entry of this.keyValueEntries) {\n      if(entry.key === '') continue;\n      if(keyOccurrences[entry.key]) {\n        keyOccurrences[entry.key] += 1;\n      } else {\n        keyOccurrences[entry.key] = 1;\n      }\n    }\n\n    for(const entry of this.keyValueEntries) {\n      entry.isKeyValid = entry.key !== '' && keyOccurrences[entry.key] === 1;\n      entry.showKeyInvalid = entry.key !== '';\n    }\n  }\n\n  /**\n   * checking all keys and values are valid\n   * send out CanSubmitUpdatedEvent\n   */\n  private ensureCanSubmit(): void {\n    this.canSubmit = this.keyValueEntries.length > 0 && this.keyValueEntries\n      .every(entry => entry.isKeyValid && entry.isValueValid);\n    this.updateCanSubmit();\n  }\n}\n\nexport type KeyValueListUpdatedEvent = Event & { detail: { entries: IKeyValueEntry[] } };\nexport type CanSubmitUpdatedEvent = Event & { detail: { canSubmit: boolean } };\n"],"names":[],"sourceRoot":""}