{"version":3,"file":"app-b5b7fca7.f584a1446b2fc034d810.bundle.js","mappings":"AAGA","sources":["webpack://lernplattform-admin-frontend/../common/aurelia-components/common-full-screen-container/common-full-screen-container.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/conference-attendee/conference-attendee.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/conference-video-matrix/conference-video-matrix-settings-overlay/conference-video-matrix-settings-overlay.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/conference-video-matrix/conference-video-matrix.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/content-block/content-block-formula/content-block-formula.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/content-block/content-block-formula/content-formula-variable-input/content-formula-variable-input.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/common-full-screen-container/common-full-screen-container.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/common-full-screen-container/index.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/conference-attendee/conference-attendee.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/conference-attendee/index.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/conference-video-matrix/conference-video-matrix-settings-overlay/conference-video-matrix-settings-overlay.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/conference-video-matrix/conference-video-matrix.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/conference-video-matrix/index.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/content-block/content-block-formula/content-block-formula.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/content-block/content-block-formula/content-formula-variable-input/content-formula-variable-input.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/content-block/index.ts"],"sourcesContent":["// Module\nvar code = \"<template> <div ref=\\\"contentWrapper\\\" class=\\\"fullscreen-container__content-wrapper\\\" style.to-view=\\\"{display: shown ? 'block' : 'none', 'z-index': importance}\\\"> <slot></slot> </div> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"conference-attendee conference-attendee--mode-${mode}\\\"> <div class=\\\"conference-attendee__content-wrapper\\\"> <div show.to-view=\\\"mode === Mode.Video || mode === Mode.ContentShare\\\" class=\\\"conference-attendee__video-wrapper\\\"></div> <avatar-icon if.to-view=\\\"mode === Mode.Avatar\\\" class=\\\"conference-attendee__attendee-avatar\\\" avatar-svg-download-link.to-view=\\\"attendee.avatarSvgDownloadLink\\\" points.to-view=\\\"attendee.points\\\" level-mode=\\\"primary-color-indicator\\\" size=\\\"unspecified\\\"></avatar-icon> <div if.to-view=\\\"mode === Mode.TenantLogo\\\" class=\\\"conference-attendee__tenant-logo-wrapper\\\"> <img if.to-view=\\\"tenantLogoPictureUrl\\\" class=\\\"conference-attendee__tenant-logo\\\" src.to-view=\\\"tenantLogoPictureUrl\\\"> <span if.to-view=\\\"!tenantLogoPictureUrl\\\" class=\\\"conference-attendee__tenant-name\\\">${tenantName}</span> </div> <div class=\\\"conference-attendee__audio-input-indicator ${isActiveSpeaker ? `conference-attendee__audio-input-indicator--visible` : ``}\\\"> <x-icon icon-type=\\\"custom\\\" size=\\\"normal\\\" icon-name=\\\"Distinar/StudentMic\\\"></x-icon> </div> <video-nametag if.to-view=\\\"attendeeName\\\" name.to-view=\\\"attendeeName\\\" style.to-view=\\\"nameTagStyle\\\" position.to-view=\\\"nameTagPosition\\\"></video-nametag> </div> </template> \";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"conference-video-matrix-settings-overlay\\\"> <div class=\\\"conference-video-matrix-settings-overlay__content-wrapper\\\"> <button-lightswitch value.to-view=\\\"!autoResizeGrid\\\" click.trigger=\\\"toggleAutoResize($event)\\\" t=\\\"[label]common.conference-video-matrix.fixedGridSize\\\"></button-lightswitch> <select value.bind=\\\"currentValue\\\" stop-propagation change.trigger=\\\"valueChanged()\\\" disabled.to-view=\\\"autoResizeGrid\\\" aria-label=\\\"fixed grid item count\\\"> <option repeat.for=\\\"value of values\\\" model.bind=\\\"value\\\"> ${value} </option> </select> </div> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"conference-video-matrix\\\" style.to-view=\\\"{ '--conference-video-matrix-column-count': columnCount, '--conference-video-matrix-row-count': rowCount }\\\" mousemove.trigger=\\\"handleMousemove($event)\\\"> <aspect-ratio-container if.to-view=\\\"!showContentShare || contentShareAttendeeId == null\\\" class=\\\"conference-video-matrix__grid-ratio-container\\\" mode=\\\"width-and-height-dependent\\\" ratio.to-view=\\\"gridRatio\\\"> <div class=\\\"conference-video-matrix__video-grid\\\"> <conference-attendee repeat.for=\\\"attendeeId of conferenceAttendeeIds\\\" chime-attendee-id.to-view=\\\"attendeeId\\\" type=\\\"video-only\\\"></conference-attendee> <div repeat.for=\\\"placeholder of gridEmptyItemCount\\\" class=\\\"conference-video-matrix__empty-grid-item\\\"> <safe-icon class=\\\"conference-video-matrix__empty-grid-item-icon\\\" icon-name=\\\"Distinar/LecturerCamOff\\\"></safe-icon> </div> </div> </aspect-ratio-container> <div if.to-view=\\\"showContentShare && contentShareAttendeeId != null\\\" class=\\\"conference-video-matrix__content-share\\\"> <conference-attendee chime-attendee-id.to-view=\\\"contentShareAttendeeId\\\" type=\\\"content-share-only\\\" name-tag-position=\\\"top\\\"></conference-attendee> </div> <conference-video-matrix-settings-overlay show.to-view=\\\"(mouseMovedRecently || mouseInsideControlElement) && !contentShareAttendeeId\\\" data-control-element=\\\"data-control-element\\\"></conference-video-matrix-settings-overlay> <loading-overlay visible.to-view=\\\"contentShareAttendeeId == null && gridLoading\\\"></loading-overlay> </template> \";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"content-block-formula\\\"> <template if.to-view=\\\"formula\\\"></template> <div if.to-view=\\\"!formula.isValid\\\" class=\\\"content-block-formula__invalid\\\" t=\\\"common.course-block-formula.invalidFormula\\\"></div> <div show.to-view=\\\"formula.isValid\\\" ref=\\\"latexFormulaPlainDisplayElement\\\" class=\\\"content-block-formula__formula content-block-formula__formula--plain\\\"></div> <div show.to-view=\\\"formula.isValid\\\" ref=\\\"latexFormulaPluggedDisplayElement\\\" class=\\\"content-block-formula__formula content-block-formula__formula--plugged\\\"></div> <content-formula-variable-input show.to-view=\\\"formula.isValid\\\" repeat.for=\\\"variable of variables\\\" class=\\\"content-block-formula__variables\\\" variable.to-view=\\\"variable\\\" value-updated.trigger=\\\"handleVariableValueUpdated($event)\\\"></content-formula-variable-input> <div if.to-view=\\\"formula.isValid\\\" repeat.for=\\\"keyValueList of keyValueLists\\\" class=\\\"form-group content-block-formula__key-value-list\\\"> <label class=\\\"form-label content-block-formula__key-value-list-name\\\"> ${keyValueList.name} </label> <select class=\\\"custom-select content-block-formula__key-value-list-select\\\" change.trigger=\\\"handleKeyValueListEntrySelected($event, keyValueList.name)\\\" value.to-view=\\\"keyValueList.selectedEntry\\\"> <option repeat.for=\\\"entry of keyValueList.entries\\\" model.bind=\\\"entry\\\">${entry.key}</option> </select> </div> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"content-formula-variable-input\\\"> <div class=\\\"content-formula-variable-input__input-wrapper ${ errorMessage.length ? '' : 'content-formula-variable-input__input-wrapper--spaced'}\\\"> <label class=\\\"content-formula-variable-input__name\\\">${variable.name}</label> <input class=\\\"form-control content-formula-variable-input__input ${ isValid || value === '' ? '' : 'is-invalid' }\\\" type=\\\"text\\\" value.two-way=\\\"value\\\"> <span class=\\\"content-formula-variable-input__unit\\\">${variable.unit}</span> </div> <span if.to-view=\\\"errorMessage.length\\\" class=\\\"content-formula-variable-input__error\\\">${ errorMessage }</span> </template>\";\n// Exports\nexport default code;","import { autoinject, bindable, PLATFORM, useView } from 'aurelia-framework';\nimport { WebAnimationPlayer } from 'common/aurelia-classes/animationPlayer/WebAnimationPlayer';\nimport { SiteScrollLocker } from 'common/aurelia-classes/SiteScrollLocker';\nimport { FunctionUtils } from 'common/utils/FunctionUtils';\n\n/**\n * @slot default - put your fullscreen content in this slot\n */\n@autoinject()\n@useView(PLATFORM.moduleName('./common-full-screen-container.html'))\nexport class CommonFullScreenContainer {\n  /**\n   * is true when the content is fully attached\n   * use this to trigger animations\n   * readonly\n   */\n  @bindable() public contentReadyToRender = false;\n\n  @bindable() public animationPlayer: WebAnimationPlayer | null = null;\n\n  /**\n   * When many full-screen-containers are open at the same time, the ones with the higher importance value will show on top\n   * For same importance containers, the last one is shown on top\n   */\n  @bindable() public importance = 1;\n\n  private domElement: HTMLElement;\n  private contentWrapper: HTMLElement | null = null;\n  private shown = false;\n  private readyToRenderAnimationFrame: number | null = null;\n\n  constructor(element: Element) {\n    this.domElement = element as HTMLElement;\n  }\n\n  public async show(): Promise<void> {\n    return FunctionUtils.async(() => {\n      document.body.appendChild(this.getContentWrapper());\n      this.shown = true;\n      this.setContentReadyToRenderDelayed(true);\n      SiteScrollLocker.lockScrolling(this);\n\n      if(this.animationPlayer != null) {\n        const { animationPlayer } = this;\n\n        animationPlayer.addFinishForwardEventListener(this, () => {\n          animationPlayer.removeEventListenersByScope(this);\n        });\n        animationPlayer.forward();\n      }\n    });\n  }\n\n  public async hide(): Promise<void> {\n    return FunctionUtils.async(() => {\n      const animationFinished = (): void => {\n        this.shown = false;\n        SiteScrollLocker.unlockScrolling(this);\n        this.setContentReadyToRenderDelayed(false);\n      };\n\n      if(this.animationPlayer) {\n        const { animationPlayer } = this;\n        animationPlayer.addFinishBackwardEventListener(this, () => {\n          animationPlayer.removeEventListenersByScope(this);\n          animationFinished();\n        });\n        animationPlayer.backward();\n      } else {\n        animationFinished();\n      }\n    });\n  }\n\n  private detached(): void {\n    this.domElement.appendChild(this.getContentWrapper());\n    this.setContentReadyToRenderDelayed(false);\n    SiteScrollLocker.unlockScrolling(this);\n  }\n\n  private getContentWrapper(): HTMLElement {\n    if(!this.contentWrapper) {\n      throw new Error('contentWrapper is not available');\n    }\n\n    return this.contentWrapper;\n  }\n\n  private setContentReadyToRenderDelayed(contentReadyToRender: boolean): void {\n    this.clearReadyRenderTimeout();\n\n    if(contentReadyToRender) {\n      this.readyToRenderAnimationFrame = window.requestAnimationFrame(() => {\n        // Firefox needs a reflow trigger before we can assume that our content is ready to render\n        // without the reflow the content animation gets lost sometimes\n        // eslint-disable-next-line @typescript-eslint/no-unused-expressions\n        window.getComputedStyle(this.getContentWrapper()).opacity;\n        this.contentReadyToRender = true;\n      });\n    } else {\n      this.contentReadyToRender = false;\n    }\n  }\n\n  private clearReadyRenderTimeout(): void {\n    if(this.readyToRenderAnimationFrame != null) {\n      window.cancelAnimationFrame(this.readyToRenderAnimationFrame);\n      this.readyToRenderAnimationFrame = null;\n    }\n  }\n}\n","import { PLATFORM, FrameworkConfiguration } from 'aurelia-framework';\n\nexport function configure(config: FrameworkConfiguration): void {\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/common-full-screen-container/common-full-screen-container'));\n}\n","import { autoinject, bindable, computedFrom, PLATFORM, useView } from 'aurelia-framework';\nimport { StoreService, StoreSubscriptionManager } from 'common/aurelia-services/StoreService';\nimport { TenantThemeService } from 'common/aurelia-services/TenantThemeService';\nimport { VideoBindingService } from 'common/aurelia-services/VideoBindingService';\nimport {\n  selectAttendeeInfo,\n  selectContentShareTileId,\n  selectIsActiveSpeaker,\n  selectIsLecturerAttendeeId,\n  selectVideoTileId,\n} from 'common/store/selectors/conference.context.selectors';\nimport { ICommonState, IConferenceAttendeeInfo } from 'common/store/store.interface';\nimport { assertNotNullOrUndefined } from 'common/utils/assert';\nimport { NicknameUtils } from 'common/utils/NicknameUtils';\nimport * as ConferenceContextMutations from 'common/store/mutations/conference.context.mutations';\nimport { NameTagPosition, NameTagStyle } from '../video-nametag/video-nametag';\n\nenum Mode {\n  Avatar = 'avatar',\n  ContentShare = 'content-share',\n  TenantLogo = 'tenant-logo',\n  Video = 'video',\n}\n\nenum AttendeeType {\n  VideoOnly = 'video-only',\n  ContentShareOnly = 'content-share-only',\n  VideoOrContentShare = 'video-or-content-share',\n}\n\n@useView(PLATFORM.moduleName('./conference-attendee.html'))\n@autoinject()\nexport class ConferenceAttendee {\n  @bindable() public chimeAttendeeId: string | null = null;\n  @bindable() public nameTagStyle: NameTagStyle = NameTagStyle.Grid;\n  @bindable() public nameTagPosition: NameTagPosition = NameTagPosition.Bottom;\n  @bindable() public type = AttendeeType.VideoOrContentShare;\n\n  private Mode = Mode;\n  /**\n   * the current mode\n   */\n  private mode = Mode.Avatar;\n  /**\n   * the mode to use when there was no video or content-share to show\n   */\n  private placeholderMode = Mode.Avatar;\n\n  private domElement: HTMLElement;\n  private attendee: IConferenceAttendeeInfo | null = null;\n  private isActiveSpeaker = false;\n\n  private tenantLogoPictureUrl: string | null = null;\n  private tenantName: string | null = null;\n\n  private videoTileId: number | null = null;\n  private contentTileId: number | null = null;\n\n  private elementCount?: number;\n\n  private attachedAttendeeId: string | null = null;\n  private storeSubscriptionManager: StoreSubscriptionManager<ICommonState>;\n\n  constructor(\n    element: Element,\n    storeService: StoreService<ICommonState>,\n    private videoBindingService: VideoBindingService,\n    private tenantThemeService: TenantThemeService,\n  ) {\n    this.domElement = element as HTMLElement;\n    this.storeSubscriptionManager = storeService.createStoreSubscriptionManager();\n  }\n\n  protected async attached(): Promise<void> {\n    assertNotNullOrUndefined(this.chimeAttendeeId, 'chimeAttendeeId cannot be null or undefined');\n    const attendeeId = this.chimeAttendeeId;\n    this.attachedAttendeeId = attendeeId;\n\n    // TODO(ian): if we want to show content-share and video at the same time\n    // we have to increment the element count per type\n    await this.incrementElementCountForAttendeeId(attendeeId);\n    this.subscribeToState(attendeeId);\n  }\n\n  protected async detached(): Promise<void> {\n    assertNotNullOrUndefined(this.attachedAttendeeId, 'attachedAttendeeId cannot be null or undefined');\n    const attendeeId = this.attachedAttendeeId;\n\n    // we have to unsubscribe before we decrement the element count else we\n    // would get the update for this element as well\n    this.storeSubscriptionManager.unsubscribe();\n\n    await this.videoBindingService.unbindTileForAttendee(attendeeId);\n    await this.decrementElementCountForAttendeeId(attendeeId);\n  }\n\n  private subscribeToState(attendeeId: string): void {\n    this.storeSubscriptionManager.unsubscribe();\n\n    this.storeSubscriptionManager.subscribeToValueChange(\n      state => state.conferenceContext.elementCountByChimeAttendeeId[attendeeId] ?? 1,\n      (newCount, oldCount) => {\n        if(oldCount != null) {\n          const hasMultipleSameAttendees = oldCount === 1 && newCount > 1;\n          const hasOneAttendee = oldCount > 1 && newCount === 1;\n\n          if(hasMultipleSameAttendees) this.videoBindingService.unbindTileForAttendee(attendeeId);\n          if(hasOneAttendee) this.resolveBinding();\n        } else if(newCount === 1) {\n          this.resolveBinding();\n        }\n\n        this.elementCount = newCount;\n      },\n    );\n\n    this.storeSubscriptionManager.subscribeToDetailedChanges([\n      state => selectContentShareTileId({ state, attendeeId }),\n      state => selectVideoTileId({ state, attendeeId }),\n      state => selectIsLecturerAttendeeId({ state, attendeeId }),\n    ], (_state, contentTileId, videoTileId, isLecturer) => {\n      this.placeholderMode = isLecturer ? Mode.TenantLogo : Mode.Avatar;\n\n      if(this.elementCount == null\n        || this.elementCount > 1\n        || (\n          contentTileId == null\n          && videoTileId == null\n          && (\n            this.contentTileId != null\n          || this.videoTileId != null\n          )\n        )) {\n        this.mode = this.placeholderMode;\n      }\n\n      if(this.elementCount === 1) this.resolveBinding({ contentTileId, videoTileId });\n\n      this.contentTileId = contentTileId;\n      this.videoTileId = videoTileId;\n    });\n\n    this.storeSubscriptionManager.subscribeToDetailedChanges([\n      state => state.distinar.activeLecture?.tenantId ?? null,\n      state => selectIsLecturerAttendeeId({ state, attendeeId }),\n    ], (_state, tenantId, isLecturer) => {\n      if(tenantId != null && isLecturer) this.updateTenantLogo(tenantId);\n    });\n\n    this.storeSubscriptionManager.subscribeToDetailedChanges([\n      state => selectAttendeeInfo({ state, attendeeId }),\n    ], (_state, attendee) => {\n      this.attendee = attendee;\n    });\n\n    this.storeSubscriptionManager.subscribeToDetailedChanges([\n      state => selectIsActiveSpeaker({ state, attendeeId }),\n    ], (_state, isActiveSpeaker) => {\n      this.isActiveSpeaker = isActiveSpeaker;\n    });\n  }\n\n  private resolveBinding(tileIds?: { contentTileId: number | null; videoTileId: number | null }): void {\n    assertNotNullOrUndefined(this.chimeAttendeeId, 'chimeAttendeeId cannot be null or undefined');\n    const attendeeId = this.chimeAttendeeId;\n\n    if(this.type === AttendeeType.VideoOrContentShare) {\n      this.resolveBindingVideoOrContentShare(attendeeId, tileIds);\n    } else if(this.type === AttendeeType.ContentShareOnly) {\n      this.resolveBindingContentShareOnly(attendeeId, tileIds?.contentTileId);\n    } else if(this.type === AttendeeType.VideoOnly) {\n      this.resolveBindingVideoOnly(attendeeId, tileIds?.videoTileId);\n    }\n  }\n\n  private resolveBindingVideoOrContentShare(chimeAttendeeId: string, tileIds?: { contentTileId: number | null; videoTileId: number | null }): void {\n    const resolveCurrent = tileIds === undefined;\n\n    const hasNewContentTile = tileIds?.contentTileId != null;\n    const hasNewVideoTile = tileIds?.videoTileId != null;\n    const contentTileChanged = tileIds?.contentTileId !== this.contentTileId;\n    const videoTileChanged = tileIds?.videoTileId !== this.videoTileId;\n\n    const hasActiveContentShare = resolveCurrent && this.contentTileId != null || tileIds?.contentTileId != null && tileIds.contentTileId === this.contentTileId;\n    const contentTileWasRemoved = contentTileChanged && tileIds?.contentTileId === null;\n\n    if(\n      hasNewContentTile\n      && contentTileChanged\n    ) {\n      // has new content tile\n      this.videoBindingService.bindAttendeeTileToElement(chimeAttendeeId, tileIds.contentTileId!, this.videoWrapper);\n      this.mode = Mode.ContentShare;\n    } else if(\n      hasNewVideoTile\n      && (videoTileChanged || contentTileWasRemoved)\n      && !hasActiveContentShare\n    ) {\n      // has new video tile, or content share has ended\n      this.videoBindingService.bindAttendeeTileToElement(chimeAttendeeId, tileIds.videoTileId!, this.videoWrapper);\n      this.mode = Mode.Video;\n    } else if(\n      resolveCurrent\n      && (this.contentTileId != null || this.videoTileId != null)\n    ) {\n      // bind the current ids\n      const tileId = this.contentTileId ?? this.videoTileId!;\n\n      this.videoBindingService.bindAttendeeTileToElement(chimeAttendeeId, tileId, this.videoWrapper);\n      this.mode = this.contentTileId != null ? Mode.ContentShare : Mode.Video;\n    } else if(\n      (\n        resolveCurrent\n        && this.contentTileId == null\n        && this.videoTileId == null\n      ) || (\n        tileIds?.contentTileId === null\n        && tileIds.videoTileId === null\n      )\n    ) {\n      this.videoBindingService.unbindTileForAttendee(chimeAttendeeId);\n      this.mode = this.placeholderMode;\n    }\n  }\n\n  private resolveBindingContentShareOnly(chimeAttendeeId: string, contentTileId?: number | null): void {\n    const resolveCurrent = contentTileId === undefined;\n    const hasNewContentTile = contentTileId != null;\n    const contentTileChanged = contentTileId !== this.contentTileId;\n    const contentTileWasRemoved = contentTileChanged && contentTileId === null;\n\n    if(hasNewContentTile && contentTileChanged) {\n      // use new content tile\n      this.videoBindingService.bindAttendeeTileToElement(chimeAttendeeId, contentTileId, this.videoWrapper);\n      this.mode = Mode.ContentShare;\n    } else if(resolveCurrent && this.contentTileId != null) {\n      // reuse old content tile\n      this.videoBindingService.bindAttendeeTileToElement(chimeAttendeeId, this.contentTileId, this.videoWrapper);\n      this.mode = Mode.ContentShare;\n    } else if(contentTileWasRemoved) {\n      this.videoBindingService.unbindTileForAttendee(chimeAttendeeId);\n      this.mode = this.placeholderMode;\n    }\n  }\n\n  private resolveBindingVideoOnly(chimeAttendeeId: string, videoTileId?: number | null): void {\n    const resolveCurrent = videoTileId === undefined;\n    const hasNewVideoTile = videoTileId != null;\n    const videoTileChanged = videoTileId !== this.videoTileId;\n    const videoTileWasRemoved = videoTileChanged && videoTileId === null;\n\n    if(hasNewVideoTile && videoTileChanged) {\n      // use new video tile\n      this.videoBindingService.bindAttendeeTileToElement(chimeAttendeeId, videoTileId, this.videoWrapper);\n      this.mode = Mode.Video;\n    } else if(resolveCurrent && this.videoTileId != null) {\n      // reuse old video tile\n      this.videoBindingService.bindAttendeeTileToElement(chimeAttendeeId, this.videoTileId, this.videoWrapper);\n      this.mode = Mode.Video;\n    } else if(videoTileWasRemoved) {\n      this.videoBindingService.unbindTileForAttendee(chimeAttendeeId);\n      this.mode = this.placeholderMode;\n    }\n  }\n\n  @computedFrom('attendee.firstname', 'attendee.lastname', 'attendee.nickname')\n  private get attendeeName(): string | null {\n    if(this.attendee?.firstname != null && this.attendee.lastname != null) return NicknameUtils.getFullName(this.attendee);\n    return this.attendee?.nickname ?? null;\n  }\n\n  private get videoWrapper(): HTMLDivElement {\n    const el = this.domElement.querySelector('.conference-attendee__video-wrapper') as HTMLDivElement;\n    assertNotNullOrUndefined(el, 'couldn\\'t find videoWrapper');\n    return el;\n  }\n\n  private async updateTenantLogo(tenantId: number): Promise<void> {\n    const tenantLogo = await this.tenantThemeService.getTenantLogo(tenantId);\n    this.tenantLogoPictureUrl = tenantLogo.logoPictureUrl;\n    this.tenantName = tenantLogo.name;\n  }\n\n\n  private async incrementElementCountForAttendeeId(chimeAttendeeId: string): Promise<void> {\n    await this.storeSubscriptionManager.getStore()\n      .dispatch(ConferenceContextMutations.incrementElementCountForAttendeeId, chimeAttendeeId);\n  }\n\n  private async decrementElementCountForAttendeeId(chimeAttendeeId: string): Promise<void> {\n    await this.storeSubscriptionManager.getStore()\n      .dispatch(ConferenceContextMutations.decrementElementCountForAttendeeId, chimeAttendeeId);\n  }\n}\n","import { FrameworkConfiguration, PLATFORM } from 'aurelia-framework';\n\nexport function configure(config: FrameworkConfiguration): void {\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/conference-attendee/conference-attendee'));\n}\n","import { autoinject, PLATFORM, useView } from 'aurelia-framework';\nimport { StoreService } from 'common/aurelia-services/StoreService';\nimport * as ConferenceContextMutations from 'common/store/mutations/conference.context.mutations';\nimport { ICommonState } from 'common/store/store.interface';\nimport { ConferenceVideoMatrix } from '../conference-video-matrix';\n\n@useView(PLATFORM.moduleName('./conference-video-matrix-settings-overlay.html'))\n@autoinject()\nexport class ConferenceVideoMatrixSettingsOverlay {\n  private currentValue = 1;\n  private values = ConferenceVideoMatrix.ITEM_COUNTS;\n  /**\n   * if enabled the grid is automatically adapted to attendee count and container ratio\n   * (in portrait the grid could be 2x4 whereas in landscape it would be 4x2)\n   */\n  private autoResizeGrid = true;\n\n  constructor(\n    private storeService: StoreService<ICommonState>,\n  ) { }\n\n  protected attached(): void {\n    this.storeService.subscribeToDetailedChanges(this, [\n      state => state.conferenceContext.bindingContexts.matrix.itemCount,\n      state => state.conferenceContext.bindingContexts.matrix.autoResize,\n    ], (_state, itemCount, autoResize) => {\n      this.currentValue = itemCount;\n      this.autoResizeGrid = autoResize;\n    });\n  }\n\n  protected detached(): void {\n    this.storeService.disposeAllSubscriptions(this);\n  }\n\n  private valueChanged(): void {\n    this.storeService.dispatch(ConferenceContextMutations.setMatrixItemCount, this.currentValue);\n  }\n\n  private toggleAutoResize(event: Event): void {\n    event.stopPropagation();\n    this.storeService.dispatch(ConferenceContextMutations.setMatrixAutoResize, !this.autoResizeGrid);\n  }\n}","import { autoinject, bindable, computedFrom, PLATFORM, useView } from 'aurelia-framework';\nimport { StoreService, StoreSubscriptionManager } from 'common/aurelia-services/StoreService';\nimport { SubscriptionManager, SubscriptionManagerService } from 'common/aurelia-services/SubscriptionManagerService';\nimport { selectContentShareAttendeeId, selectContentShareHasTile, selectMatrixAttendeeIds } from 'common/store/selectors/conference.context.selectors';\nimport { ICommonState } from 'common/store/store.interface';\nimport * as ConferenceContextMutations from 'common/store/mutations/conference.context.mutations';\nimport { FunctionUtils } from 'common/utils/FunctionUtils';\nimport { assertNotNullOrUndefined } from 'common/utils/assert';\nimport { DomUtils } from 'common/utils/DomUtils';\n\n@autoinject()\n@useView(PLATFORM.moduleName('./conference-video-matrix.html'))\nexport class ConferenceVideoMatrix {\n  public static ITEM_COUNTS = [1, 2, 4, 6, 8, 9, 12, 15, 16, 20, 24, 25];\n  public static MOUSE_MOVED_RECENTLY_TIMEOUT = 1000;\n\n  @bindable() public tenantLogoPictureUrl: string | null = null;\n  @bindable() public tenantName: string | null = null;\n  @bindable() public showContentShare = false;\n\n  private matrixStoreSubscriptionManager: StoreSubscriptionManager<ICommonState>;\n  private contentShareStoreSubscriptionManager: StoreSubscriptionManager<ICommonState>;\n  private subscriptionManager: SubscriptionManager;\n\n  private contentShareAttendeeId: string | null = null;\n  private conferenceAttendeeIds: Array<string> = [];\n  private isContentShare = false;\n\n  private itemCount = 1;\n  private autoResizeGrid = false;\n  private allAttendeesCount = 0;\n\n  private columnCount = 1;\n  private rowCount = 1;\n\n  private domElement: HTMLElement;\n  private ownRatio: number | null = null;\n\n  private mouseMovedRecently = false;\n  private mouseInsideControlElement = false;\n  private debouncedResetMouseMoved = FunctionUtils.debounce(() => { this.mouseMovedRecently = false; }, ConferenceVideoMatrix.MOUSE_MOVED_RECENTLY_TIMEOUT);\n\n  constructor(\n    element: Element,\n    storeService: StoreService<ICommonState>,\n    subscriptionManagerService: SubscriptionManagerService,\n  ) {\n    this.domElement = element as HTMLElement;\n    this.contentShareStoreSubscriptionManager = storeService.createStoreSubscriptionManager();\n    this.matrixStoreSubscriptionManager = storeService.createStoreSubscriptionManager();\n    this.subscriptionManager = subscriptionManagerService.createSubscriptionManager();\n  }\n\n  protected attached(): void {\n    this.subscriptionManager.subscribeToElementResize(this.domElement, contentRect => {\n      if(contentRect.height !== 0) {\n        this.ownRatio = contentRect.width / contentRect.height;\n      } else {\n        this.ownRatio = contentRect.width;\n      }\n      this.updateGridRowsAndColumns();\n    });\n\n    if(this.showContentShare) this.setupContentShareSubscription();\n    this.setupMatrixSubscription();\n  }\n\n  protected detached(): void {\n    this.subscriptionManager.disposeSubscriptions();\n    this.disposeContentShareSubscription();\n    this.disposeMatrixSubscription();\n    this.debouncedResetMouseMoved.cancel();\n    this.mouseMovedRecently = false;\n  }\n\n  private handleMousemove(event: MouseEvent): boolean {\n    assertNotNullOrUndefined(event.target, 'event without a target in PresentationSlide.handleMousemove');\n    this.mouseInsideControlElement = this.elementIsInsideControlElement(event.target as HTMLElement);\n\n    this.mouseMovedRecently = true;\n    this.debouncedResetMouseMoved();\n    return true;\n  }\n\n  private elementIsInsideControlElement(element: HTMLElement): boolean {\n    return DomUtils.isInsideElementWithDataAttribute(element, 'controlElement');\n  }\n\n  @computedFrom('columnCount', 'rowCount')\n  private get gridRatio(): number {\n    return (this.columnCount * 4) / (this.rowCount * 3);\n  }\n\n  private showContentShareChanged(): void {\n    if(this.showContentShare && this.domElement.isConnected) {\n      this.setupContentShareSubscription();\n    } else {\n      this.disposeContentShareSubscription();\n    }\n  }\n\n  private setupContentShareSubscription(): void {\n    this.contentShareStoreSubscriptionManager.subscribeToDetailedChanges([\n      state => selectContentShareAttendeeId({ state }),\n      state => selectContentShareHasTile({ state }),\n    ], (_state, attendeeId, hasTile) => {\n      this.contentShareAttendeeId = hasTile ? attendeeId : null;\n      this.isContentShare = hasTile;\n    });\n  }\n\n  private disposeContentShareSubscription(): void {\n    this.contentShareStoreSubscriptionManager.unsubscribe();\n    this.contentShareAttendeeId = null;\n    this.isContentShare = false;\n  }\n\n  private setupMatrixSubscription(): void {\n    this.matrixStoreSubscriptionManager.subscribeToDetailedChanges([\n      state => selectMatrixAttendeeIds({ state }),\n    ], (_state, attendeeIds) => {\n      this.conferenceAttendeeIds = attendeeIds;\n    });\n\n    this.matrixStoreSubscriptionManager.subscribeToDetailedChanges([\n      state => state.conferenceContext.bindingContexts.matrix.itemCount,\n    ], (_state, itemCount) => {\n      this.itemCount = itemCount;\n      this.updateGridRowsAndColumns();\n    });\n\n    this.matrixStoreSubscriptionManager.subscribeToDetailedChanges([\n      state => state.conferenceContext.bindingContexts.matrix.allAttendeeIds.length,\n    ], (_state, allAttendeesCount) => {\n      this.allAttendeesCount = allAttendeesCount;\n      this.adaptGridToBestSize();\n    });\n\n    this.matrixStoreSubscriptionManager.subscribeToDetailedChanges([\n      state => state.conferenceContext.bindingContexts.matrix.autoResize,\n    ], (_state, autoResize) => {\n      this.autoResizeGrid = autoResize;\n      this.adaptGridToBestSize();\n    });\n  }\n\n  private updateGridRowsAndColumns(): void {\n    [this.columnCount, this.rowCount] = this.getRowAndColumnCount(this.itemCount, this.ownRatio);\n  }\n\n  private adaptGridToBestSize(): void {\n    if(this.autoResizeGrid) {\n      // automatically update item count to best match the attendee count\n      const bestItemCountMatch = ConferenceVideoMatrix.ITEM_COUNTS.find(count => count >= this.allAttendeesCount) ?? ConferenceVideoMatrix.ITEM_COUNTS[ConferenceVideoMatrix.ITEM_COUNTS.length -1];\n      void this.matrixStoreSubscriptionManager.getStore().dispatch(ConferenceContextMutations.setMatrixItemCount, bestItemCountMatch);\n    }\n  }\n\n  private disposeMatrixSubscription(): void {\n    this.matrixStoreSubscriptionManager.unsubscribe();\n    this.conferenceAttendeeIds = [];\n  }\n\n  @computedFrom('conferenceAttendeeIds', 'columnCount', 'rowCount')\n  private get gridEmptyItemCount(): number {\n    const itemCount = this.columnCount * this.rowCount;\n    if(this.conferenceAttendeeIds == null) return itemCount;\n    return itemCount - this.conferenceAttendeeIds.length;\n  }\n\n  @computedFrom('conferenceAttendeeIds', 'columnCount', 'rowCount')\n  private get gridLoading(): boolean {\n    return this.conferenceAttendeeIds == null\n      || this.columnCount * this.rowCount < this.conferenceAttendeeIds.length;\n  }\n\n  private getRowAndColumnCount(itemCount: number, containerRatio: number | null): [number, number] {\n    const combination = this.getRowColumnCombinationForItemCount(itemCount);\n    return this.getRowColumnBestMatchForContainerRatio(combination, containerRatio ?? 1);\n  }\n\n  private getRowColumnCombinationForItemCount(itemCount: number): [number, number] {\n    switch(itemCount) {\n      case 2: return [2, 1];\n      case 4: return [2, 2];\n      case 6: return [3, 2];\n      case 8: return [4, 2];\n      case 12: return [4, 3];\n      case 15: return [5, 3];\n      case 16: return [4, 4];\n      case 20: return [5, 4];\n      case 24: return [6, 4];\n      case 25: return [5, 5];\n      case 9: return [3, 3];\n      case 1:\n      default: return [1, 1];\n    }\n  }\n\n  private getRowColumnBestMatchForContainerRatio([x, y]: [number, number], containerRatio: number): [number, number] {\n    if(x === y || containerRatio === 1) return [x, y];\n\n    const containerIsHorizontal = containerRatio > 1;\n    if(containerIsHorizontal && x > y) return [x, y];\n    return [y, x];\n  }\n}\n","import { FrameworkConfiguration, PLATFORM } from 'aurelia-framework';\n\nexport function configure(config: FrameworkConfiguration): void {\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/conference-video-matrix/conference-video-matrix'));\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/conference-video-matrix/conference-video-matrix-settings-overlay/conference-video-matrix-settings-overlay'));\n}\n","import { FORMULA_REFERENCE_TYPE_PREFIX_LENGTH } from './../../../constants/formula.constants';\nimport { autoinject, bindable, PLATFORM, useView } from 'aurelia-framework';\nimport {\n  FORMULA_KATEX_RENDER_OPTIONS,\n  FORMULA_KEY_VALUE_LIST_PREFIX,\n  FORMULA_REFERENCE_END_IDENTIFIER,\n  FORMULA_REFERENCE_START_IDENTIFIER,\n  FORMULA_VARIABLE_PREFIX,\n} from 'common/constants/formula.constants';\nimport { ArrayUtils } from 'common/utils/ArrayUtils';\nimport { assertNotNullOrUndefined } from 'common/utils/assert';\nimport { FormulaUtils } from 'common/utils/FormulaUtils';\nimport { LogUtils } from 'common/utils/LogUtils';\nimport evaluatex from 'evaluatex/dist/evaluatex';\nimport katex from 'katex';\nimport {\n  IFormulaDto,\n  IFormulaKeyValueListShort,\n  IFormulaVariableShort,\n  IKeyValueEntry,\n} from '../../../dto/course/content/formula.interface';\nimport { IVariableValueUpdatedEventData } from './content-formula-variable-input/content-formula-variable-input';\n\ntype ReferenceableElement = {\n  name: string; // can include allowed special characters (ä, ö, ü and greek alphabet)\n  cleanName: string; // must not include any special characters\n  latexName: string; // cleanName without any \" \" -> replaced by unique strings\n};\n\nexport type VariableWithValue = ReferenceableElement & {\n  isValid: boolean;\n  unit: string;\n  valueAsNumber: number | null;\n  valueAsString: string;\n  minValue: number | null;\n  maxValue: number | null;\n  minValueDisplay: string;\n  maxValueDisplay: string;\n};\n\ntype KeyValueList = ReferenceableElement & {\n  id: number;\n  entries: IKeyValueEntry[];\n  selectedEntry: IKeyValueEntry | null;\n};\n\ntype InsertReferenceStringDto = {\n  baseString: string;\n  insertString: string;\n  startIndex: number;\n  endIndex: number;\n};\n\n/**\n * Shows two versions of the formula:\n * 1. Plain - the formula with result, variable and keyValueList names\n * 2. Plugged - plugged in all the replaceable element values that are defined, including\n * a result if possible\n *\n * Also shows an input row for each unique variable and keyValueList in the formula.\n */\n@useView(PLATFORM.moduleName('./content-block-formula.html'))\n@autoinject()\nexport class ContentBlockFormula {\n  @bindable() public formula: IFormulaDto | null = null;\n  private latexFormulaPlainDisplayElement: HTMLDivElement | null = null;\n  private latexFormulaPluggedDisplayElement: HTMLDivElement | null = null;\n  private variables: VariableWithValue[] = [];\n  private variableNameIndices: Record<string, number> = {};\n  private keyValueLists: KeyValueList[] = [];\n  private keyValueListIndices: Record<string, number> = {};\n\n  /**\n   * attached and formulaChanged are both required to trigger on all\n   * relevant updates of formula (create, modify, move from clipboard, show preview)\n   */\n  protected attached(): void {\n    this.reInitFormula();\n  }\n\n  private formulaChanged(): void {\n    this.reInitFormula();\n  }\n\n  private reInitFormula(): void {\n    if(!this.latexFormulaPlainDisplayElement ||\n      !this.latexFormulaPluggedDisplayElement ||\n      !this.formula) return;\n\n    const formulaStructure = FormulaUtils.parseFormulaAndConvertShortToNormal(this.formula.formulaString);\n    if(!formulaStructure) return;\n\n    const uniqueVariablesRecord = ArrayUtils.toRecord(\n      formulaStructure.variables,\n      value => value.name,\n      value => value,\n    );\n\n    this.variables = (JSON.parse(this.formula.uniqueVariables) as IFormulaVariableShort[]).map(variableShort => {\n      const variable = FormulaUtils.convertFormulaVariableShortToNormal(variableShort);\n      return {\n        isValid: false,\n        name: variable.name,\n        cleanName: FormulaUtils.replaceSpecialCharacters(variable.name),\n        latexName: FormulaUtils.replaceSpaces(FormulaUtils.replaceSpecialCharacters(variable.name)),\n        unit: variable.unit,\n        valueAsNumber: 1,\n        valueAsString: '',\n        minValue: uniqueVariablesRecord[variable.name].minValueAsNumber,\n        maxValue: uniqueVariablesRecord[variable.name].maxValueAsNumber,\n        minValueDisplay: uniqueVariablesRecord[variable.name].minDisplayString,\n        maxValueDisplay: uniqueVariablesRecord[variable.name].maxDisplayString,\n      };\n    });\n\n    this.variableNameIndices = ArrayUtils.toRecord(\n      this.variables,\n      variable => variable.name,\n      (variable, _, index) => index,\n    );\n\n    if(this.formula.uniqueKeyValueLists.length) {\n      this.keyValueLists = (JSON.parse(this.formula.uniqueKeyValueLists) as IFormulaKeyValueListShort[]).map(keyValueList => ({\n        id: keyValueList.id,\n        name: keyValueList.name,\n        cleanName: FormulaUtils.replaceSpecialCharacters(keyValueList.name),\n        latexName: FormulaUtils.replaceSpaces(FormulaUtils.replaceSpecialCharacters(keyValueList.name)),\n        entries: FormulaUtils.convertKeyValueListEntriesShortToNormal(keyValueList.entries),\n        selectedEntry: keyValueList.entries[0] ? FormulaUtils.convertKeyValueListEntryShortToNormal(keyValueList.entries[0]) : null,\n      }));\n      this.keyValueListIndices = ArrayUtils.toRecord(\n        this.keyValueLists,\n        keyValueList => keyValueList.name,\n        (keyValueList, _, index) => index,\n      );\n    }\n\n    this.initialRenderFormulas();\n  }\n\n  private initialRenderFormulas(): void {\n    assertNotNullOrUndefined(this.formula, 'Need a formula to display a plain formula.');\n    assertNotNullOrUndefined(this.latexFormulaPlainDisplayElement, 'Plain Formula Display Element must be available!');\n    assertNotNullOrUndefined(this.latexFormulaPluggedDisplayElement, 'Plugged Formula Display Element must be available!');\n\n    const displayString = FormulaUtils.generateDisplayString({\n      resultName: this.formula.resultName,\n      resultArguments: this.formula.resultArguments,\n      resultUnit: this.formula.resultUnit,\n      formulaString: this.formula.displayString,\n      resultAfterFormula: false,\n      removeReferenceIdentifiers: true,\n      convertSpaces: true,\n    });\n\n    katex.render(displayString, this.latexFormulaPlainDisplayElement, FORMULA_KATEX_RENDER_OPTIONS);\n\n    this.updateFormula();\n  }\n\n  private handleVariableValueUpdated(event: CustomEvent<IVariableValueUpdatedEventData>): void {\n    this.variables[this.variableNameIndices[event.detail.name]].isValid = event.detail.isValid;\n    if(event.detail.isValid) {\n      this.variables[this.variableNameIndices[event.detail.name]].valueAsNumber = event.detail.valueAsNumber;\n      this.variables[this.variableNameIndices[event.detail.name]].valueAsString = event.detail.valueAsString;\n    }\n    this.updateFormula();\n  }\n\n  private handleKeyValueListEntrySelected(event: Event, name: string): void {\n    const selectedKey = (event.target as HTMLSelectElement).value;\n    const indexOfCurrentKeyValueList = this.keyValueLists.findIndex(keyValueList => keyValueList.name === name);\n    if(indexOfCurrentKeyValueList === -1) return;\n\n    const selectedEntry = this.keyValueLists[indexOfCurrentKeyValueList].entries.find(entry => entry.key === selectedKey);\n    if(!selectedEntry) {\n      LogUtils.error(`FormulaBlock Error: Cannot find selected key \"${selectedKey}\" in formula ${JSON\n        .stringify(this.formula, null, 2)}`);\n      return;\n    }\n\n    this.keyValueLists[indexOfCurrentKeyValueList].selectedEntry = selectedEntry;\n    this.updateFormula();\n  }\n\n  private updateFormula(): void {\n    assertNotNullOrUndefined(this.formula, 'Need Formula for plugging variables and calculation');\n    assertNotNullOrUndefined(this.latexFormulaPluggedDisplayElement, 'Plain Formula Display Element must be available!');\n\n    let isFormulaValid = true;\n    let pluggedFormulaString = this.formula.displayString;\n    let itemIndex = 0;\n    do {\n      // for each occurrence of a variable: remove the variable identification syntax, and optional replace name with value\n      itemIndex = pluggedFormulaString.indexOf(FORMULA_REFERENCE_START_IDENTIFIER);\n      if(itemIndex !== -1) {\n        /**\n         * it is possible to have tilde characters which are stored as \\text{\\textasciitilde}\n         * this conflicts with the end identifier for replaceable elements }\n         * meaning the correct end index cannot be found using indexOf(\"}\").\n         * so we need to search for the desired occurrence of \"}\".\n         */\n        const itemEndIndex = this.findEndIndex(itemIndex + 1, pluggedFormulaString);\n        if(itemEndIndex === -1) {\n          isFormulaValid = false;\n          LogUtils.error(`FormulaBlock Error: Cannot find end of replaceable element starting at index ${itemIndex} of pluggedFormulaString ${pluggedFormulaString}`);\n          return;\n        }\n\n        const currentItemString = pluggedFormulaString.slice(\n          itemIndex + FORMULA_REFERENCE_START_IDENTIFIER.length,\n          itemEndIndex,\n        );\n        const currentIdentifier = currentItemString.slice(0, FORMULA_REFERENCE_TYPE_PREFIX_LENGTH);\n        const currentItemName = currentItemString.slice(FORMULA_REFERENCE_TYPE_PREFIX_LENGTH);\n        const searchableItemName = FormulaUtils.unescapeProblematicSpecialCharacters(currentItemName);\n\n        switch(currentIdentifier) {\n          case FORMULA_VARIABLE_PREFIX: {\n            const currentVar = this.variables[this.variableNameIndices[searchableItemName]];\n            if(!currentVar) {\n              isFormulaValid = false;\n              LogUtils.error(`FormulaBlock Error: Cannot find variable named \"${searchableItemName}\" in variables \\n\\n (at index: \"${this\n                .variableNameIndices[searchableItemName]}\") \\n\\n list: ${JSON.stringify(this\n                .variables, null, 2)}. \\n\\n This is the displayString: ${this\n                .formula.displayString} \\n\\n This is the variableNameIndices-Record: ${JSON.stringify(this\n                .variableNameIndices, null, 2)}`);\n              return;\n            }\n            pluggedFormulaString = this.insertReferenceString({\n              baseString: pluggedFormulaString,\n              insertString: currentVar.isValid ? currentVar.valueAsString : currentItemName,\n              startIndex: itemIndex,\n              endIndex: itemEndIndex,\n            });\n            if(!currentVar.isValid) isFormulaValid = false;\n            break;\n          }\n\n          case FORMULA_KEY_VALUE_LIST_PREFIX: {\n            const currentItem = this.keyValueLists[this.keyValueListIndices[searchableItemName]];\n            if(!currentItem) {\n              isFormulaValid = false;\n              LogUtils.error(`FormulaBlock Error: Cannot find keyValueList named \"${searchableItemName}\" in keyValueList \\n\\n (at index: \"${this\n                .keyValueListIndices[searchableItemName]}\") \\n\\n list: ${JSON.stringify(this\n                .keyValueLists, null, 2)}. \\n\\n This is the displayString: ${this\n                .formula.displayString} \\n\\n This is the keyValueListIndices-Record: ${JSON.stringify(this\n                .keyValueListIndices, null, 2)}`);\n              return;\n            }\n            pluggedFormulaString = this.insertReferenceString({\n              baseString: pluggedFormulaString,\n              insertString: currentItem.selectedEntry?.valueAsString ?? currentItemName,\n              startIndex: itemIndex,\n              endIndex: itemEndIndex,\n            });\n            if(!currentItem.selectedEntry?.valueAsString) isFormulaValid = false;\n            break;\n          }\n\n          default:\n            LogUtils.error(`FormulaBlock Error: Need Identifier for replacement formula element (found string: ${currentItemString}) in formula ${JSON\n              .stringify(this.formula, null, 2)}`);\n            return;\n        }\n      }\n    } while(itemIndex !== -1);\n\n    // calculate result if all variables are valid\n    let result = 1;\n    if(isFormulaValid) {\n      const variablesWithValues = ArrayUtils.toRecord(\n        this.variables,\n        variable => `${FORMULA_VARIABLE_PREFIX}${variable.latexName}`,\n        variable => variable.valueAsNumber ?? 1, // 1 for typechecking, null should never occurr here\n      );\n      const selectedKeyValues = ArrayUtils.toRecord(\n        this.keyValueLists,\n        keyValueList => `${FORMULA_KEY_VALUE_LIST_PREFIX}${keyValueList.latexName}`,\n        keyValueList => keyValueList.selectedEntry?.valueAsNumber ?? 1,  // 1 for typechecking, null should never occurr here\n      );\n      const lookupTable = { ...variablesWithValues, ...selectedKeyValues };\n\n      result = evaluatex(this.formula.latexFormula)(lookupTable);\n    }\n    const displayResult = isFormulaValid\n      ? `${result.toLocaleString('de-DE')}`\n      : this.formula.resultName;\n\n    // prepend result to plugged formula\n    pluggedFormulaString = FormulaUtils.generateDisplayString({\n      resultName: displayResult,\n      resultArguments: '', // this.formula.resultArguments,\n      resultUnit: this.formula.resultUnit,\n      formulaString: pluggedFormulaString,\n      resultAfterFormula: true,\n      removeReferenceIdentifiers: false,\n      convertSpaces: true,\n    });\n\n    katex.render(pluggedFormulaString, this.latexFormulaPluggedDisplayElement, FORMULA_KATEX_RENDER_OPTIONS);\n  }\n\n  private insertReferenceString(dto: InsertReferenceStringDto): string {\n    return `${dto.baseString.slice(0, dto.startIndex)}${dto.insertString}${dto.baseString\n      .slice(dto.endIndex + FORMULA_REFERENCE_END_IDENTIFIER.length)}`;\n  }\n\n  private findEndIndex(startIndex: number, stringToCheck: string): number {\n    let openBracketCount = 0;\n\n    for(let i = startIndex; i < stringToCheck.length; i++) {\n      const currentCharacter = stringToCheck[i];\n\n      if(currentCharacter === '{') {\n        openBracketCount++;\n      } else if(currentCharacter === '}') {\n        openBracketCount--;\n        if(openBracketCount === 0) {\n          return i;\n        }\n      }\n    }\n    return -1;\n  }\n}\n","import { autoinject, bindable, observable, PLATFORM, useView } from 'aurelia-framework';\nimport { FORMULA_NUMBER_VALIDATION_REGEX } from 'common/constants/formula.constants';\nimport { DomUtils } from 'common/utils/DomUtils';\nimport { FormulaUtils } from 'common/utils/FormulaUtils';\nimport { VariableWithValue } from '../content-block-formula';\nimport { I18N } from 'aurelia-i18n';\n\nexport interface IVariableValueUpdatedEventData {\n  name: string;\n  valueAsString: string;\n  valueAsNumber: number | null;\n  isValid: boolean;\n}\n\n@useView(PLATFORM.moduleName('./content-formula-variable-input.html'))\n@autoinject()\nexport class ContentFormulaVariableInput {\n  @bindable() variable: VariableWithValue | null = null;\n  @observable() private value: string = '';\n  private numberValidationRegex = new RegExp(FORMULA_NUMBER_VALIDATION_REGEX);\n  private isValid: boolean = true;\n  private variableInputElement: HTMLElement;\n  private errorMessage: string = '';\n\n  constructor(\n    element: Element,\n    private i18N: I18N,\n  ) {\n    this.variableInputElement = element as HTMLElement;\n  }\n\n  private valueChanged(): void {\n    if(this.value == null || this.variable == null) return;\n    this.isValid = !!this.value.length\n      && this.numberValidationRegex.test(this.value);\n\n    this.errorMessage = this.isValid || !this.value.length ? '' : this.i18N.tr('common.formula.errorNumber');\n\n    const cleanedValueString = FormulaUtils.cleanNumberString(this.value, this.isValid);\n    const valueAsString = FormulaUtils.convertToDisplayString(cleanedValueString, FORMULA_NUMBER_VALIDATION_REGEX);\n    const valueAsNumber = FormulaUtils.convertStringToNumber(cleanedValueString, FORMULA_NUMBER_VALIDATION_REGEX);\n\n    if(this.isValid && valueAsNumber != null) {\n      if(this.variable.minValue != null) {\n        this.isValid = valueAsNumber >= this.variable.minValue;\n        this.errorMessage = this.isValid\n          ? ''\n          : this.i18N.tr('common.formula.errorMin', { value: this.variable.minValueDisplay });\n      }\n      if(this.isValid && this.variable.maxValue != null) {\n        this.isValid = valueAsNumber <= this.variable.maxValue;\n        this.errorMessage = this.isValid\n          ? ''\n          : this.i18N.tr('common.formula.errorMax', { value: this.variable.maxValueDisplay });\n      }\n    }\n\n    this.valueUpdated(valueAsString, valueAsNumber);\n  }\n\n  private valueUpdated(valueAsString: string, valueAsNumber: number | null): void {\n    if(!this.variable) return;\n    DomUtils.fireEvent<IVariableValueUpdatedEventData>(this.variableInputElement, {\n      name: 'value-updated',\n      detail: {\n        name: this.variable.name,\n        valueAsString,\n        valueAsNumber,\n        isValid: this.isValid,\n      },\n      bubbles: false,\n    });\n  }\n}","import { PLATFORM, FrameworkConfiguration } from 'aurelia-framework';\n\nexport function configure(config: FrameworkConfiguration): void {\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/content-block/content-block-formula/content-block-formula'));\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/content-block/content-block-formula/content-formula-variable-input/content-formula-variable-input'));\n}\n"],"names":[],"sourceRoot":""}