{"version":3,"file":"app-4bc96a3c.f584a1446b2fc034d810.bundle.js","mappings":"AAAA","sources":["webpack://lernplattform-admin-frontend/../common/aurelia-attributes/attach-to.ts","webpack://lernplattform-admin-frontend/../common/aurelia-attributes/attached-emitter.ts","webpack://lernplattform-admin-frontend/../common/aurelia-attributes/class-list.ts","webpack://lernplattform-admin-frontend/../common/aurelia-attributes/href-ext.ts","webpack://lernplattform-admin-frontend/../common/aurelia-attributes/index.ts","webpack://lernplattform-admin-frontend/../common/aurelia-attributes/real-touch.ts","webpack://lernplattform-admin-frontend/../common/aurelia-attributes/stop-propagation.ts","webpack://lernplattform-admin-frontend/../common/aurelia-attributes/use-as-button.ts","webpack://lernplattform-admin-frontend/../common/aurelia-classes/AudioPlaybackNode.ts","webpack://lernplattform-admin-frontend/../common/aurelia-classes/BroadcastChannel.ts","webpack://lernplattform-admin-frontend/../common/aurelia-classes/CancellationToken.ts","webpack://lernplattform-admin-frontend/../common/aurelia-classes/CrossTabChannel.ts","webpack://lernplattform-admin-frontend/../common/aurelia-classes/DistinarWebSocketConnection.ts","webpack://lernplattform-admin-frontend/../common/aurelia-classes/Error.ts","webpack://lernplattform-admin-frontend/../common/aurelia-classes/EventEmitter.ts","webpack://lernplattform-admin-frontend/../common/aurelia-classes/FocusTrap.ts","webpack://lernplattform-admin-frontend/../common/aurelia-classes/RecordCache.ts","webpack://lernplattform-admin-frontend/../common/aurelia-classes/RefCache.ts","webpack://lernplattform-admin-frontend/../common/aurelia-classes/ResourcePreloader.ts","webpack://lernplattform-admin-frontend/../common/aurelia-classes/SiteScrollLocker.ts","webpack://lernplattform-admin-frontend/../common/aurelia-classes/SiteThemer.ts","webpack://lernplattform-admin-frontend/../common/aurelia-classes/StoreManager.ts","webpack://lernplattform-admin-frontend/../common/aurelia-classes/Timer.ts","webpack://lernplattform-admin-frontend/../common/aurelia-classes/UpdateTimer.ts","webpack://lernplattform-admin-frontend/../common/aurelia-classes/WebSocketConnection.ts","webpack://lernplattform-admin-frontend/../common/aurelia-classes/animationPlayer/FadeAnimationPlayer.ts","webpack://lernplattform-admin-frontend/../common/aurelia-classes/animationPlayer/WebAnimationPlayer.ts","webpack://lernplattform-admin-frontend/../common/aurelia-classes/index.ts"],"sourcesContent":["import { autoinject } from 'aurelia-framework';\n\n/**\n * Custom Attribute to create an element in one place but then dynamically move it around in the DOM.\n * \n * @param value a DOM element: attach to it; null: return to original position\n * \n * example:\n * ```html\n * <some-tag attach-to.to-view=\"targetElement\">this element moves itself into the referenced element</some-tag>\n * \n * <div>\n *   <div ref=\"targetElement\">in this example, it will land in here</div>\n * </div>\n * ```\n */\n@autoinject()\nexport class AttachToCustomAttribute {\n  private originalPositionAnchor = document.createComment('attach-to');\n  private value: HTMLElement | null = null;\n\n  constructor(\n    private element: Element,\n  ) { }\n\n  protected attached(): void {\n    this.attachToTarget();\n  }\n\n  protected detached(): void {\n    this.originalPositionAnchor.remove();\n    this.element.remove();\n  }\n\n  private valueChanged(): void {\n    if(!this.element.isConnected) return;\n    this.attachToTarget();\n  }\n\n  private attachToTarget(): void {\n    if(this.value) {\n      if(!this.originalPositionAnchor.isConnected) this.element.parentElement!.insertBefore(this.originalPositionAnchor, this.element);\n      this.value.append(this.element);\n    } else if(this.originalPositionAnchor.isConnected) {\n      this.originalPositionAnchor.replaceWith(this.element);\n    }\n  }\n}\n","import { autoinject, bindable } from 'aurelia-framework';\nimport { DomUtils } from 'common/utils/DomUtils';\n\nexport interface IAttachedEmitterEventDetail {\n  name: string;\n}\n\n@autoinject\nexport class AttachedEmitterCustomAttribute {\n  @bindable({ primaryProperty: true }) public name = '';\n\n  constructor(private element: Element) {\n  }\n\n  protected attached(): void {\n    DomUtils.fireEvent<IAttachedEmitterEventDetail>(this.element, {\n      name: 'attached',\n      detail: {\n        name: this.name,\n      },\n      bubbles: true,\n    });\n  }\n}\n","import { autoinject } from 'aurelia-framework';\nimport difference from 'lodash/difference';\n\n/**\n * Custom Attribute to fix aurelia class quirks when using multiple conditional classes\n */\n@autoinject()\nexport class ClassListCustomAttribute {\n  private currentClasses: Array<string> = [];\n\n  constructor(\n    private element: Element,\n  ) { }\n\n  protected attached(): void {\n    this.element.classList.add(...this.currentClasses);\n  }\n\n  protected detached(): void {\n    this.element.classList.remove(...this.currentClasses);\n  }\n\n  private valueChanged(newClasses: Array<string>, oldClasses?: Array<string>): void {\n    this.currentClasses = newClasses;\n    if(!this.element.isConnected) return;\n\n    const classesAdded = difference(newClasses, oldClasses ?? []);\n    const classesRemoved = difference(oldClasses ?? [], newClasses);\n\n    this.element.classList.remove(...classesRemoved);\n    this.element.classList.add(...classesAdded);\n  }\n}\n","import { Disposable, autoinject } from 'aurelia-framework';\nimport { SubscriptionManager, SubscriptionManagerService } from 'common/aurelia-services/SubscriptionManagerService';\nimport { AppUtils } from 'common/utils/AppUtils';\nimport { DomUtils } from 'common/utils/DomUtils';\n\n/**\n * used on <a></a> tags to open a link externally\n *\n * target=\"_blank\" is used for browsers and `AppUtils.openExternal` for Apps\n */\n@autoinject()\nexport class HrefExtCustomAttribute {\n  private value?: string = '';\n\n  private domElement: HTMLElement;\n  private subscriptionManager: SubscriptionManager;\n  private disposables: Disposable[] = [];\n\n  constructor(\n    element: Element,\n    subscriptionManagerService: SubscriptionManagerService,\n  ) {\n    this.domElement = element as HTMLElement;\n    this.subscriptionManager = subscriptionManagerService.createSubscriptionManager();\n  }\n\n  protected attached(): void {\n    this.valueChanged();\n  }\n\n  protected detached(): void {\n    this.uninstall();\n  }\n\n  private valueChanged(): void {\n    if(!this.domElement.isConnected) return;\n\n    this.uninstall();\n    if(!this.value) return;\n    this.install(this.value);\n  }\n\n  private install(link: string): void {\n    if(!AppUtils.isMobileApp) {\n      this.disposables.push(\n        DomUtils.applyAttributeModification(this.domElement, 'href', link, { force: false }),\n        DomUtils.applyAttributeModification(this.domElement, 'target', '_blank', { force: false }),\n      );\n    } else {\n      this.subscriptionManager.subscribeToDomEvent(this.domElement, 'click', () => AppUtils.openExternal(link));\n    }\n  }\n\n  private uninstall(): void {\n    this.disposables.forEach(d => d.dispose());\n    this.disposables = [];\n    this.subscriptionManager.disposeSubscriptions();\n  }\n}\n","import { PLATFORM } from 'aurelia-pal';\nimport { FrameworkConfiguration } from 'aurelia-framework';\n\nexport function configure(config: FrameworkConfiguration): void {\n  config.globalResources(PLATFORM.moduleName('common/aurelia-attributes/attach-to'));\n  config.globalResources(PLATFORM.moduleName('common/aurelia-attributes/attached-emitter'));\n  config.globalResources(PLATFORM.moduleName('common/aurelia-attributes/class-list'));\n  config.globalResources(PLATFORM.moduleName('common/aurelia-attributes/href-ext'));\n  config.globalResources(PLATFORM.moduleName('common/aurelia-attributes/real-touch'));\n  config.globalResources(PLATFORM.moduleName('common/aurelia-attributes/stop-propagation'));\n  config.globalResources(PLATFORM.moduleName('common/aurelia-attributes/use-as-button'));\n}\n","import { bindable, autoinject } from 'aurelia-framework';\nimport { SubscriptionManagerService, SubscriptionManager } from 'common/aurelia-services/SubscriptionManagerService';\n\n@autoinject()\nexport class RealTouchCustomAttribute {\n  @bindable() private onTouchCallback: ((event: TouchEvent) => void) | null = null;\n  private subscriptionManager: SubscriptionManager;\n  private domElement: HTMLElement;\n  private touchStartPosition = { x: 0, y: 0 };\n  private touchEndPosition = { x: 0, y: 0 };\n  private touchDistanceTolerance = 10;\n\n  constructor(element: Element, subscriptionManagerService: SubscriptionManagerService) {\n    this.subscriptionManager = subscriptionManagerService.createSubscriptionManager();\n    this.domElement = element as HTMLElement;\n  }\n\n  protected bind(): void {\n    this.subscriptionManager.subscribeToDomEvent(this.domElement, 'touchstart', event => {\n      const touchEvent = event as TouchEvent;\n      this.touchStartPosition = { x: touchEvent.touches[0].pageX, y: touchEvent.touches[0].pageY };\n    });\n\n    this.subscriptionManager.subscribeToDomEvent(this.domElement, 'touchend', event => {\n      const touchEvent = event as TouchEvent;\n      this.touchEndPosition = { x: touchEvent.changedTouches[0].pageX, y: touchEvent.changedTouches[0].pageY };\n\n      if(!this.isScroll() && this.onTouchCallback !== null) {\n        this.onTouchCallback(touchEvent);\n      }\n    });\n  }\n\n  protected unbind(): void {\n    this.subscriptionManager.disposeSubscriptions();\n  }\n\n  private isScroll(): boolean {\n    return Math.sqrt(\n      ((this.touchEndPosition.x - this.touchStartPosition.x) ** 2)\n      + ((this.touchEndPosition.y - this.touchStartPosition.y) ** 2),\n    ) > this.touchDistanceTolerance;\n  }\n}\n","import { autoinject } from 'aurelia-framework';\nimport { SubscriptionManagerService, SubscriptionManager } from 'common/aurelia-services/SubscriptionManagerService';\n\n@autoinject()\nexport class StopPropagationCustomAttribute {\n  private subscriptionManager: SubscriptionManager;\n  private domElement: HTMLElement;\n\n  constructor(\n    element: Element,\n    subscriptionManagerService: SubscriptionManagerService,\n  ) {\n    this.subscriptionManager = subscriptionManagerService.createSubscriptionManager();\n    this.domElement = element as HTMLElement;\n  }\n\n  protected bind(): void {\n    this.subscriptionManager.subscribeToDomEvent(this.domElement, 'click', event => {\n      event.stopPropagation();\n    });\n  }\n\n  protected unbind(): void {\n    this.subscriptionManager.disposeSubscriptions();\n  }\n}\n","import { autoinject, Disposable } from 'aurelia-framework';\nimport { SubscriptionManager, SubscriptionManagerService } from 'common/aurelia-services/SubscriptionManagerService';\nimport { KeyboardKey } from 'common/enum/keyboard.key.enum';\nimport { DomUtils } from 'common/utils/DomUtils';\n\n/**\n * fires click events if enter or space is being pressed and the element is focused\n * great for icons or anchors\n * may not work on route-href\n */\n@autoinject()\nexport class UseAsButtonCustomAttribute {\n  private value: boolean | '' = '';\n\n  private element: HTMLElement;\n  private subscriptionManager: SubscriptionManager;\n\n  private disposables: Disposable[] = [];\n\n  constructor(\n    element: Element,\n    subscriptionManagerService: SubscriptionManagerService,\n  ) {\n    this.element = element as HTMLElement;\n    this.subscriptionManager = subscriptionManagerService.createSubscriptionManager();\n  }\n\n  protected attached(): void {\n    this.valueChanged();\n  }\n\n  protected detached(): void {\n    this.subscriptionManager.disposeSubscriptions();\n  }\n\n  protected fireClickEvent(): void {\n    DomUtils.fireEvent(this.element, {\n      name: 'click',\n      bubbles: true,\n    });\n  }\n\n  protected valueChanged(): void {\n    if(!this.element.isConnected) return;\n\n    this.uninstall();\n    if(!this.getValue()) return;\n    this.install();\n  }\n\n  private install(): void {\n    this.disposables.push(\n      DomUtils.applyAttributeModification(this.element, 'tabindex', '0', { force: false }),\n      DomUtils.applyAttributeModification(this.element, 'role', 'button', { force: false }),\n      DomUtils.applyAttributeModification(this.element, 'href', 'javascript:void(0)', { force: true }),\n    );\n\n    this.subscriptionManager.subscribeToDomEvent(this.element, 'keyup', (e: Event) => {\n      const keyboardEvent = e as KeyboardEvent;\n      const keys = [KeyboardKey.Space];\n      // anchors already fire a clickish event on enter\n      if(this.element.tagName.toLowerCase() !== 'a') {\n        keys.push(KeyboardKey.Enter);\n      }\n      if('key' in keyboardEvent && keys.includes(keyboardEvent.key as KeyboardKey)) {\n        this.fireClickEvent();\n      }\n    });\n  }\n\n  private uninstall(): void {\n    this.disposables.forEach(d => d.dispose());\n    this.disposables = [];\n    this.subscriptionManager.disposeSubscriptions();\n  }\n\n  private getValue(): boolean {\n    // default is empty string, which should be true\n    if(this.value === '') return true;\n    return !!this.value;\n  }\n}\n","import { HttpRequestUtils } from 'common/utils/HttpRequestUtils';\n\nexport class AudioPlaybackNode {\n  private volumeNode: GainNode;\n  private bufferSourceNode: AudioBufferSourceNode | null = null;\n\n  private src: string | null = null;\n  private audioBuffer: AudioBuffer | null = null;\n\n  private endedCallback: (() => void) | null = null;\n\n  constructor(\n    private context: AudioContext,\n  ) {\n    this.volumeNode = context.createGain();\n    this.volumeNode.gain.value = 1;\n    this.volumeNode.connect(context.destination);\n  }\n\n  public dispose(): void {\n    this.destroyBufferSource();\n    this.volumeNode.disconnect();\n    this.endedCallback = null;\n  }\n\n  public async play(src: string, startTimeSeconds = 0): Promise<void> {\n    this.destroyBufferSource();\n\n    // load new audio buffer if src has changed\n    if(src !== this.src) {\n      this.src = src;\n      this.audioBuffer = null;\n      this.audioBuffer = await this.loadAudio(src);\n      if(src !== this.src) return;\n    }\n\n    // create a buffer source and connect to the volume node\n    const bufferSource = this.context.createBufferSource();\n    bufferSource.buffer = this.audioBuffer;\n    bufferSource.connect(this.volumeNode);\n\n    // set up started\n    bufferSource.start(this.context.currentTime, startTimeSeconds);\n    bufferSource.onended = () => this.endedCallback?.();\n\n    this.bufferSourceNode = bufferSource;\n  }\n\n  public stop(): void {\n    this.destroyBufferSource();\n  }\n\n  public setVolume(value: number): void {\n    this.volumeNode.gain.value = value;\n  }\n\n  public onEnded(cb: () => void): void {\n    this.endedCallback = cb;\n  }\n\n  private destroyBufferSource(): void {\n    if(!this.bufferSourceNode) return;\n\n    this.bufferSourceNode.onended = null;\n    this.bufferSourceNode.stop(this.context.currentTime);\n    this.bufferSourceNode.disconnect();\n    this.bufferSourceNode = null;\n  }\n\n  private async loadAudio(url: string): Promise<AudioBuffer> {\n    const arrayBuffer = await HttpRequestUtils.getArrayBuffer({ url });\n    return this.context.decodeAudioData(arrayBuffer);\n  }\n}","import { BroadcastChannel as Polyfill } from 'broadcast-channel';\n\n// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\nexport const BroadcastChannel = window.BroadcastChannel || Polyfill;\n","export class CancellationToken {\n  private _canceled = false;\n\n  constructor(\n    private onCancel?: () => void,\n  ) {}\n\n  public get canceled(): boolean {\n    return this._canceled;\n  }\n\n  public cancel(): void {\n    if(this.canceled) return;\n    this._canceled = true;\n    this.onCancel?.();\n  }\n}\n","import { Disposable } from 'aurelia-framework';\nimport { assertNotNullOrUndefined } from 'common/utils/assert';\nimport { BroadcastChannel } from './BroadcastChannel';\n\nexport class CrossTabChannel {\n  private channel: BroadcastChannel | null;\n  private subscribers = new Map<string, Array<CrossTabChannelCallback<ICrossTabMessage>>>();\n\n  constructor(name: string) {\n    this.channel = new BroadcastChannel(name);\n    this.channel.onmessage = this.handleMessage.bind(this);\n    this.channel.onmessageerror = this.handleMessageError.bind(this);\n  }\n\n  public close(): void {\n    this.channel?.close();\n    this.channel = null;\n    this.subscribers = new Map();\n  }\n\n  public emit<T extends ICrossTabMessage = DefaultCrossTabMessage>(config: CrossTabMessageConfig<T>): void {\n    assertNotNullOrUndefined(this.channel, 'channel was closed');\n    const message: ICrossTabMessage<T['type'], T['detail']> = {\n      detail: config.detail ?? undefined,\n      type: config.type,\n    };\n\n    this.channel.postMessage(message);\n    if(config.notifySelf) {\n      this.notifySubscribers(message);\n    }\n  }\n\n  public subscribe<T extends ICrossTabMessage>(type: T['type'], callback: CrossTabChannelCallback<T>): Disposable {\n    assertNotNullOrUndefined(this.channel, 'channel was closed');\n\n    const callbacks: CrossTabChannelCallback<ICrossTabMessage>[] = this.subscribers.get(type) ?? [];\n    if(!this.subscribers.has(type)) {\n      this.subscribers.set(type, callbacks);\n    }\n\n    callbacks.push(callback as CrossTabChannelCallback<ICrossTabMessage>);\n\n    return {\n      dispose: () => this.unsubscribe(type, callback),\n    };\n  }\n\n  private unsubscribe<T extends ICrossTabMessage = DefaultCrossTabMessage>(type: string, callback: CrossTabChannelCallback<T>): void {\n    const callbacks = this.subscribers.get(type);\n    if(!callbacks) return;\n\n    const index = callbacks.findIndex(cb => cb === callback);\n    if(index !== -1) {\n      callbacks.splice(index, 1);\n      if(callbacks.length === 0) {\n        this.subscribers.delete(type);\n      }\n    }\n  }\n\n  private handleMessage(event: MessageEvent): void {\n    const message = event.data as ICrossTabMessage;\n    // eslint-disable-next-line no-console\n    if(typeof message.type !== 'string') console.warn(`received illegal message (missing type): ${event.data}`);\n\n    this.notifySubscribers(message);\n  }\n\n  private notifySubscribers(message: ICrossTabMessage): void {\n    const typeSubscribers = this.subscribers.get(message.type);\n    if(typeSubscribers) {\n      typeSubscribers.forEach(cb => cb(message));\n    }\n  }\n\n  private handleMessageError(event: MessageEvent): void {\n    console.error(`Error receiving crosstab message: ${event}`);\n  }\n}\n\nexport interface ICrossTabMessage<TType extends string = string, TDetail = unknown> {\n  type: TType;\n  detail: TDetail;\n}\n\nexport type DefaultCrossTabMessage = ICrossTabMessage<string, never>;\n\nexport type CrossTabMessageConfig<T extends ICrossTabMessage<string, unknown> = DefaultCrossTabMessage> = {\n  type: T['type'];\n  notifySelf?: boolean;\n} & (T['detail'] extends never ? {\n  detail?: never;\n} : {\n  detail: T['detail'];\n});\n\nexport type CrossTabChannelCallback<T extends ICrossTabMessage = DefaultCrossTabMessage> = (message: T) => void;\n","import { ICommonState } from 'common/store/store.interface';\nimport { EmitMap, EmitData, EmitResult, EmitValueResolver, EventMap, WebSocketConnection } from './WebSocketConnection';\n\nexport class DistinarWebSocketConnection<TListenerMap extends EventMap, TEmitMap extends EmitMap, TState extends ICommonState> extends WebSocketConnection<TListenerMap, TEmitMap> {\n  constructor(\n    private stateGetter: () => Promise<TState>,\n    uri: string,\n    path: string,\n    reconnectionAttempts?: number,\n  ) {\n    super(uri, path, reconnectionAttempts);\n  }\n\n  public async emit<TEventName extends keyof TEmitMap & string>(\n    eventName: TEventName,\n    data: EmitData<TEmitMap, TEventName>,\n  ): Promise<void>;\n  public async emit<TEventName extends keyof TEmitMap & string>(\n    event: TEventName,\n    data: EmitData<TEmitMap, TEventName>,\n    valueResolver: EmitValueResolver<TEmitMap, TEventName>,\n  ): Promise<EmitResult<TEmitMap, TEventName>>;\n  public async emit<TEventName extends keyof TEmitMap & string>(\n    event: TEventName,\n    data: EmitData<TEmitMap, TEventName>,\n    valueResolver?: EmitValueResolver<TEmitMap, TEventName> | undefined,\n  ): Promise<EmitResult<TEmitMap, TEventName> | void> {\n    const { distinarConnectionData } = await this.stateGetter();\n    if(distinarConnectionData == null) return;\n\n    // we wrap the sent data here because the BE expects we submit a lectureId on every packet\n    // the data is unwrapped in BE again to provide the usual experience, where only data is passed to the gateway subscriber method\n    const pimpedData = {\n      connection: distinarConnectionData,\n      data,\n    };\n\n    if(valueResolver == null) {\n      return super.emit(event, pimpedData);\n    }\n    return super.emit(event, pimpedData, valueResolver);\n  }\n}","import { ErrorType } from 'common/enum/errortype.enum';\nimport { AppResponse, Failed } from 'common/types';\nimport { Socket } from 'socket.io-client';\n\nexport class ApplicationError extends Error {\n  constructor(msg: string, public details: any[] = []) {\n    super(msg);\n  }\n}\n\nexport class ApiError extends ApplicationError {\n  public status: number;\n  public errorType: ErrorType | null;\n\n  constructor(msg: string, public details: any[] = [], status: number) {\n    super(msg);\n    Object.setPrototypeOf(this, ApiError.prototype);\n    this.errorType = Object.values(ErrorType).some(eT => eT === this.message)\n      ? this.message as ErrorType\n      : null;\n\n    this.status = status;\n  }\n\n  public static async fromResponse(response: Response): Promise<ApiError> {\n    if(response.status < 503) {\n      const responseJson: Failed<AppResponse<unknown>> = (await response.json());\n      return ApiError.fromFailedAppResponse(responseJson, response.status);\n    }\n    return new ApiError(response.statusText, [], response.status);\n  }\n\n  public static async fromFailedAppResponse(response: Failed<AppResponse<unknown>>, status: number): Promise<ApiError> {\n    return new ApiError(response.error, [response.message, ...(response.details || [])], status);\n  }\n}\n\nexport class WebSocketError extends ApplicationError {\n  public static fromAppResponse(response: Failed<AppResponse<unknown>>): WebSocketError {\n    return new WebSocketError(response.error, [response.message, ...(response.details || [])]);\n  }\n\n  public static fromDisconnectReason(reason: Socket.DisconnectReason): WebSocketError {\n    return new WebSocketError(`connection was disconnected: ${reason}`);\n  }\n\n  public static fromError(error: Error): WebSocketError {\n    return new WebSocketError(error.message, [error]);\n  }\n\n  public static fromMessage(msg: string): WebSocketError {\n    return new WebSocketError(msg);\n  }\n}\n","type EventContext = string | symbol;\ntype EventCallback<TConfig, TKey extends keyof TConfig> = (payload: TConfig[TKey]) => void;\n\nconst DefaultEventContext = Symbol();\n\nexport class EventEmitter<TEventConfig extends Record<string | number | symbol, any>> {\n  private listeners: Array<IEventListener<TEventConfig>> = [];\n\n  public addEventListener<T extends keyof TEventConfig>(scope: unknown, eventName: T, callback: EventCallback<TEventConfig, T>): void {\n    this.addEventListenerInternal(scope, DefaultEventContext, eventName, callback);\n  }\n\n  public addEventListenerWithContext<T extends keyof TEventConfig>(scope: unknown, context: EventContext, eventName: T, callback: EventCallback<TEventConfig, T>): void {\n    this.addEventListenerInternal(scope, context, eventName, callback);\n  }\n\n  /**\n   * Subscribe to an event and wait for it to be triggered once\n   * Throws if the scope is unsubscribed while waiting\n   */\n  public waitforEvent<T extends keyof TEventConfig>(scope: unknown, eventName: T): Promise<TEventConfig[T]> {\n    return new Promise<TEventConfig[T]>((resolve, reject) => {\n      const callback = (result: TEventConfig[T]): void => {\n        resolve(result);\n        this.removeEventListenersByCallback(callback);\n      };\n\n      this.addEventListenerInternal(scope, DefaultEventContext, eventName, callback, () => reject(new Error('the event was unsubscribed')));\n    });\n  }\n\n  public removeEventListenersByCallback<T extends keyof TEventConfig>(callback: EventCallback<TEventConfig, T>): void {\n    this.removeEventListenerInternal(listener => listener.callback === callback);\n  }\n\n  public removeEventListenersByScope(scope: unknown): void {\n    this.removeEventListenerInternal(listener => listener.scope === scope);\n  }\n\n  public dispatch<T extends keyof TEventConfig>(eventName: T, payload: TEventConfig[T]): void {\n    for(const listener of this.listeners.slice()) {\n      if(listener.eventName === eventName) {\n        try {\n          listener.callback(payload);\n        } catch(e) {\n          console.error(`Failed to notify subscriber: ${e}`);\n        }\n      }\n    }\n  }\n\n  public dispatchScoped<T extends keyof TEventConfig>(scope: unknown, eventName: T, payload: TEventConfig[T]): void {\n    for(const listener of this.listeners.filter(l => l.scope === scope).slice()) {\n      if(listener.eventName === eventName) {\n        try {\n          listener.callback(payload);\n        } catch(e) {\n          console.error(`Failed to notify subscriber: ${e}`);\n        }\n      }\n    }\n  }\n\n  public dispatchForContext<T extends keyof TEventConfig>(context: EventContext, eventName: T, payload: TEventConfig[T]): void {\n    for(const listener of this.listeners.filter(l => l.context === context).slice()) {\n      if(listener.eventName === eventName) {\n        try {\n          listener.callback(payload);\n        } catch(e) {\n          console.error(`Failed to notify subscriber: ${e}`);\n        }\n      }\n    }\n  }\n\n  private addEventListenerInternal<T extends keyof TEventConfig>(\n    scope: any,\n    context: EventContext,\n    eventName: T,\n    callback: EventCallback<TEventConfig, T>,\n    onRemove?: () => void,\n  ): void {\n    this.listeners.push({\n      scope, context, eventName, callback, onRemove,\n    });\n  }\n\n  private removeEventListenerInternal(matcher: (item: IEventListener<TEventConfig, any>) => boolean): void {\n    this.listeners = this.listeners.reduce<Array<IEventListener<TEventConfig>>>((newListeners, listener) => {\n      if(matcher(listener)) {\n        try {\n          listener.onRemove?.();\n        } catch(e) {\n          console.error(`listener onRemove callback failed: ${e}`);\n        }\n      } else {\n        newListeners.push(listener);\n      }\n\n      return newListeners;\n    }, []);\n  }\n}\n\ninterface IEventListener<TEventConfig, TKey extends keyof TEventConfig = any> {\n  scope: any;\n  context: EventContext;\n  eventName: TKey;\n  callback: EventCallback<TEventConfig, TKey>;\n  onRemove?: () => void;\n}\n","import { Disposable } from 'aurelia-framework';\nimport { SubscriptionManagerService } from 'common/aurelia-services/SubscriptionManagerService';\n\nexport class LastIsActiveStack {\n  private items: Array<{ activate: ActivateFunction; disposable: Disposable | null }> = [];\n\n  public add(activate: ActivateFunction): Disposable {\n    this.disposeAllStackItems();\n    this.items.push({\n      activate,\n      disposable: activate(),\n    });\n\n    return {\n      dispose: () => {\n        this.disposeAllStackItems();\n        this.items = this.items.filter(item => item.activate !== activate);\n        this.activateLastItem();\n      },\n    };\n  }\n\n  private async disposeAllStackItems(): Promise<void> {\n    for(const item of this.items) {\n      item.disposable?.dispose();\n      item.disposable = null;\n    }\n  }\n\n  private activateLastItem(): void {\n    const item = this.items[this.items.length - 1];\n    if(item) {\n      item.disposable = item.activate();\n    }\n  }\n}\n\n/**\n * this class is used to trap the focus inside an element\n * if anything outside of the element gets focused, then the element itself will get focused automatically\n */\nexport class FocusTrap {\n  private static stack: LastIsActiveStack = new LastIsActiveStack();\n\n  private stackDisposable: Disposable | null = null;\n\n  constructor(\n    private element: HTMLElement,\n    private subscriptionManagerService: SubscriptionManagerService,\n  ) { }\n\n  public setup(): void {\n    this.stackDisposable = FocusTrap.stack.add(() => {\n      const subscriptionManager = this.subscriptionManagerService.createSubscriptionManager();\n      subscriptionManager.subscribeToDomEvent(document.body, 'focusin', this.handleFocusin.bind(this));\n\n      return {\n        dispose: () => {\n          subscriptionManager.disposeSubscriptions();\n        },\n      };\n    });\n  }\n\n  public destroy(): void {\n    this.stackDisposable?.dispose();\n  }\n\n  private handleFocusin(event: Event): void {\n    if(!this.eventTargetIsInside(event)) {\n      this.element.focus();\n    }\n  }\n\n  private eventTargetIsInside(event: Event): boolean {\n    if(event.target && event.target instanceof Node) {\n      return event.target === this.element || this.element.contains(event.target);\n    }\n\n    return false;\n  }\n}\n\nexport type ActivateFunction = () => Disposable;\n","import { ArrayUtils } from 'common/utils/ArrayUtils';\n\nexport class RecordCache<Key extends string | number | symbol, Value> {\n  private valuesByKey: Partial<Record<Key, Value>> = {};\n  private pendingRequests: Partial<Record<Key, Promise<void>>> = {};\n  private maxAgeMs: number | null;\n  private expirationTimestampsByKey: Partial<Record<Key, number>> = {};\n\n  constructor(config?: {\n    maxAgeMs?: number;\n  }) {\n    this.maxAgeMs = config?.maxAgeMs ?? null;\n  }\n\n  /**\n   * @returns true if the item was updated and not stored in cache\n   */\n  public async ensureInCache(\n    key: Key,\n    contentLoaderFunction: () => Promise<void>,\n    shouldUpdateFunction?: (value: Value | undefined) => Promise<boolean>,\n  ): Promise<boolean> {\n\n    // 'null' can be a valid key\n    const found = this.valuesByKey[key] !== undefined;\n    const expired = this.maxAgeMs != null && (this.expirationTimestampsByKey[key] ?? 0) < Date.now();\n    if(expired) delete this.expirationTimestampsByKey[key];\n\n    if(found && !expired) {\n      const shouldUpdate = await shouldUpdateFunction?.(this.valuesByKey[key]);\n      if(!shouldUpdate) {\n        return false;\n      }\n    }\n\n    if(this.pendingRequests[key] == null) {\n      // eslint-disable-next-line no-async-promise-executor\n      this.pendingRequests[key] = new Promise<void>(async (resolve, reject) => {\n        try {\n          await contentLoaderFunction();\n          delete this.pendingRequests[key];\n          resolve();\n        } catch(e) {\n          console.error(e);\n          delete this.pendingRequests[key];\n          reject(e);\n        }\n      });\n    }\n\n    await this.pendingRequests[key];\n    return true;\n  }\n\n  public invalidateEntry(key: Key): void {\n    delete this.valuesByKey[key];\n  }\n\n  public setState(valuesByKey: Partial<Record<Key, Value>>): void {\n    const { maxAgeMs } = this;\n\n    // update the expiration dates for all changed entries\n    if(maxAgeMs != null && valuesByKey !== this.valuesByKey) {\n      this.expirationTimestampsByKey = ArrayUtils.toRecord(\n        Object.keys(valuesByKey) as Key[],\n        key => key,\n        key => (this.valuesByKey[key] !== valuesByKey[key] || this.expirationTimestampsByKey[key] == null\n          ? Date.now() + maxAgeMs\n          : this.expirationTimestampsByKey[key]),\n      );\n    }\n\n    this.valuesByKey = valuesByKey;\n  }\n\n  public getValue(key: Key): Value | undefined {\n    return this.valuesByKey[key];\n  }\n}\n","import { Disposable } from 'aurelia-framework';\n\n/**\n * A reference tracking cache which expires an item X time (default 1 second) after the last ref has been removed\n * TODO: has currently only one usecase, and therefore probably flaws ;)\n */\nexport class RefCache<K, V> {\n  constructor(\n    private expirationMs = 1000,\n  ) { }\n\n  private items = new Map<K, RefCacheItem<V>>();\n\n  /**\n   * set a value and take a reference (disposable)\n   */\n  public setAndRef(key: K, value: V): Disposable {\n    const item = this.trySetAndRefExisting(key, value) ?? this.setAndRefNew(key, value);\n\n    let disposed = false;\n    return {\n      dispose: () => {\n        if(disposed) return;\n        disposed = true;\n        this.unref(key, item);\n      },\n    };\n  }\n\n  /**\n   * get a value (without touching references)\n   */\n  public get(key: K): V | undefined {\n    return this.items.get(key)?.value;\n  }\n\n  private trySetAndRefExisting(key: K, value: V): RefCacheItem<V> | null {\n    const existing = this.items.get(key);\n    if(existing) {\n      existing.value = value;\n      existing.refCount++;\n      window.clearTimeout(existing.expirationTimeout);\n      return existing;\n    }\n\n    return null;\n  }\n\n  private setAndRefNew(key: K, value: V): RefCacheItem<V> {\n    const item: RefCacheItem<V> = { value, refCount: 1 };\n    this.items.set(key, item);\n    return item;\n  }\n\n  private unref(key: K, item: RefCacheItem<V>): void {\n    item.refCount--;\n    if(item.refCount <= 0) {\n      item.expirationTimeout = window.setTimeout(() => {\n        if(this.items.get(key) === item) this.items.delete(key);\n      }, this.expirationMs);\n    }\n  }\n}\n\ntype RefCacheItem<V> = {\n  value: V;\n  refCount: number;\n  expirationTimeout?: number;\n};\n","import { assertNotNullOrUndefined } from 'common/utils/assert';\nimport { HttpRequestUtils } from 'common/utils/HttpRequestUtils';\n\nexport interface IPreloadProgress {\n  successCount: number;\n  failedCount: number;\n}\n\nexport enum ResourceType {\n  Audio = 'audio',\n  Image = 'image',\n}\n\nexport interface ILoadResourceOptions {\n  onProgress?: (progress: IPreloadProgress) => void;\n  onError?: (src: string, error: string) => void;\n}\n\nexport interface IResourcePreloaderConfig {\n  audioBaseUrl: string;\n}\n\nexport class ResourcePreloader {\n  private static audioBaseUrl: string | null;\n  private loadedResources: Array<ArrayBuffer | HTMLImageElement> = [];\n\n  public static configure(config: IResourcePreloaderConfig): void {\n    ResourcePreloader.audioBaseUrl = config.audioBaseUrl;\n  }\n\n  private static getAudioBaseUrl(): string {\n    assertNotNullOrUndefined(ResourcePreloader.audioBaseUrl, 'ResourcePreloader must be configured');\n    return ResourcePreloader.audioBaseUrl;\n  }\n\n  public async loadResources(type: ResourceType, src: string | string[], options: ILoadResourceOptions = {}): Promise<void> {\n    const srcArray = (Array.isArray(src) ? src : [src]);\n\n    let successCount = 0;\n    let failedCount = 0;\n\n    await Promise.all(srcArray.map(async item => {\n      try {\n        await this.loadResource(type, item);\n        successCount++;\n      } catch(e) {\n        failedCount++;\n\n        if(options.onError) {\n          options.onError(item, e.message || e);\n        }\n      }\n\n      if(options.onProgress) {\n        options.onProgress({ failedCount, successCount });\n      }\n    }));\n  }\n\n  public free(): void {\n    this.loadedResources = [];\n  }\n\n  private async loadResource(type: ResourceType, src: string): Promise<void> {\n    const resource = type === ResourceType.Audio\n      ? await HttpRequestUtils.getArrayBuffer({ url: `${ResourcePreloader.getAudioBaseUrl()}${src}` })\n      : await HttpRequestUtils.loadImage(src);\n\n    this.loadedResources.push(resource);\n  }\n}\n","/**\n * With this class you can prevent the body/html scrolling from multiple sources without having conflicts\n * Only the scrolling of the root element will be prevented.\n * useful for e.g. Dialogs so you don't accidentally scroll in the background\n */\nexport class SiteScrollLocker {\n  private static locks = new Set<unknown>();\n  private static locked = false;\n\n  /**\n   * @param scope - can be any reference, usually you will use `this`\n   */\n  public static lockScrolling(scope: unknown): void {\n    this.locks.add(scope);\n    this.updateScrollLock();\n  }\n\n  /**\n   * @param scope - can be any reference, usually you will use `this`\n   */\n  public static unlockScrolling(scope: unknown): void {\n    this.locks.delete(scope);\n    this.updateScrollLock();\n  }\n\n  private static updateScrollLock(): void {\n    if(this.locks.size > 0) {\n      document.documentElement.style.setProperty('--body-overflow', 'hidden');\n    } else {\n      document.documentElement.style.removeProperty('--body-overflow');\n    }\n  }\n}\n","import { ThemeColor } from 'common/enum/theme.enums';\nimport { AppUtils } from 'common/utils/AppUtils';\nimport { assertNotNullOrUndefined } from 'common/utils/assert';\n\n// adjustValue scale factor: 14\nexport const themeSubColors = [\n  { name: '900', adjustValue: -9 },\n  { name: '800', adjustValue: 5 },\n  { name: '700', adjustValue: 19 },\n  { name: '600', adjustValue: 33 },\n  { name: '500', adjustValue: 47 },\n  { name: '400', adjustValue: 61 },\n  { name: '300', adjustValue: 75 },\n  { name: '200', adjustValue: 89 },\n  { name: '100', adjustValue: 103 },\n  { name: '50', adjustValue: 117 },\n  { name: 'semi-transparent', adjustValue: 2, transparency: 0.25 },\n  { name: 'light-transparent', adjustValue: 2, transparency: 0.04 },\n  { name: '', adjustValue: 0 },\n];\n\nexport enum ThemeColorType {\n  Primary = 'primary',\n  Secondary1 = 'secondary1',\n  Secondary2 = 'secondary2',\n  Secondary3 = 'secondary3',\n  Testexam = 'testexam',\n  MultiCourse = 'multi-course',\n  PrimaryGradient = 'primary-gradient',\n  Secondary1Gradient = 'secondary1-gradient',\n  Secondary2Gradient = 'secondary2-gradient',\n  Secondary3Gradient = 'secondary3-gradient',\n  TestexamGradient = 'testexam-gradient',\n  MultiCourseGradient = 'multi-course-gradient',\n  PrimaryTransparent = 'primary-transparent',\n}\n\nexport class SiteThemer {\n  private themeColorToCssName = new Map([\n    [ThemeColor.ColdBlue, '--color-cold-blue'],\n    [ThemeColor.BrightOrange, '--color-bright-orange'],\n    [ThemeColor.FreshGreen, '--color-fresh-green'],\n    [ThemeColor.MellowPurple, '--color-mellow-purple'],\n    [ThemeColor.CandyBlue, '--color-candy-blue'],\n    [ThemeColor.CandyRed, '--color-candy-red'],\n    [ThemeColor.MuddyYellow, '--color-muddy-yellow'],\n    [ThemeColor.BerryPink, '--color-berry-pink'],\n    [ThemeColor.LeraGreen, '--color-lera-green'],\n    [ThemeColor.LeraGrey, '--color-lera-grey'],\n    [ThemeColor.FrpGreen, '--color-frp-green'],\n    [ThemeColor.FrpBlue, '--color-frp-blue'],\n  ]);\n\n  private themeColorToContrastColor = new Map([\n    [ThemeColor.ColdBlue, '#fff'],\n    [ThemeColor.BrightOrange, '#000'],\n    [ThemeColor.FreshGreen, '#000'],\n    [ThemeColor.MellowPurple, '#000'],\n    [ThemeColor.CandyBlue, '#000'],\n    [ThemeColor.CandyRed, '#000'],\n    [ThemeColor.MuddyYellow, '#000'],\n    [ThemeColor.BerryPink, '#000'],\n    [ThemeColor.LeraGreen, '#fff'],\n    [ThemeColor.LeraGrey, '#fff'],\n    [ThemeColor.FrpGreen, '#fff'],\n    [ThemeColor.FrpBlue, '#fff'],\n  ]);\n\n  private themeColorTypeToCssName = new Map([\n    [ThemeColorType.Primary, '--color-primary'],\n    [ThemeColorType.Secondary1, '--color-secondary1'],\n    [ThemeColorType.Secondary2, '--color-secondary2'],\n    [ThemeColorType.Secondary3, '--color-secondary3'],\n    [ThemeColorType.Testexam, '--color-primary-300'],\n    [ThemeColorType.MultiCourse, '--color-multi-course'],\n    [ThemeColorType.PrimaryGradient, '--color-primary-400'],\n    [ThemeColorType.Secondary1Gradient, '--color-secondary1-400'],\n    [ThemeColorType.Secondary2Gradient, '--color-secondary2-400'],\n    [ThemeColorType.Secondary3Gradient, '--color-secondary3-400'],\n    [ThemeColorType.TestexamGradient, '--color-primary-400'],\n    [ThemeColorType.MultiCourseGradient, '--color-multi-course-gradient'],\n    [ThemeColorType.PrimaryTransparent, '--color-primary-semi-transparent'],\n  ]);\n\n  constructor() {\n    // override multi-course theme for appFlavours\n    if(AppUtils.isLera) {\n      document.documentElement.style.setProperty('--color-multi-course', 'var(--color-multi-course-lera)');\n      document.documentElement.style.setProperty('--color-multi-course-gradient', 'var(--color-multi-course-gradient-lera)');\n      document.documentElement.style.setProperty('--color-multi-course-semi-transparent', 'var(--color-multi-course-semi-transparent-lera)');\n    } else if(AppUtils.isFrp) {\n      document.documentElement.style.setProperty('--color-multi-course', 'var(--color-multi-course-frp)');\n      document.documentElement.style.setProperty('--color-multi-course-gradient', 'var(--color-multi-course-gradient-frp)');\n      document.documentElement.style.setProperty('--color-multi-course-semi-transparent', 'var(--color-multi-course-semi-transparent-frp)');\n    }\n  }\n\n  public setColor(colorType: ThemeColorType, color: ThemeColor | string): void {\n    const colorTypeCssName = this.themeColorTypeToCssName.get(colorType) ?? 'custom';\n    const colorCssName = this.themeColorToCssName.get(color as ThemeColor) ?? 'custom';\n    let isCustomColor = false;\n\n    if(!Object.values(ThemeColor).includes(color as ThemeColor)) {\n      this.setCustomColor(colorTypeCssName, color);\n      isCustomColor = true;\n    }\n\n    themeSubColors.forEach(subColor => {\n      assertNotNullOrUndefined(colorCssName, `no colorCssName for \"${color}\" found`);\n      const colorTypeFullCssName = this.getSubColorNameForCssName(colorTypeCssName, subColor.name);\n      const colorFullCssName = this.getSubColorNameForCssName(colorCssName, subColor.name);\n      if(isCustomColor) {\n        const customColor = this.adjustColorBrightness(color, subColor.adjustValue, false, subColor.transparency);\n        document.documentElement.style.setProperty(colorTypeFullCssName, customColor);\n      } else {\n        document.documentElement.style.setProperty(colorTypeFullCssName, `var(${colorFullCssName})`);\n      }\n    });\n\n    // set contrast color\n    const contrastColor = this.getThemeColorContrast(color);\n    const colorTypeFullCssName = this.getSubColorNameForCssName(colorTypeCssName, 'contrast');\n    document.documentElement.style.setProperty(colorTypeFullCssName, contrastColor);\n  }\n\n  public getColor(colorType: ThemeColorType, subColor: string = ''): string {\n    const colorTypeCssName = this.themeColorTypeToCssName.get(colorType);\n    assertNotNullOrUndefined(colorTypeCssName, `cannot getColor, no colorTypeCssName for \"${colorType}\" found`);\n    return getComputedStyle(document.documentElement).getPropertyValue(this.getSubColorNameForCssName(colorTypeCssName, subColor));\n  }\n\n  public getThemeColor(themeColor: ThemeColor | string, subColor?: string): string {\n    const themeColorCssName = this.themeColorToCssName.get(themeColor as ThemeColor);\n    if(!themeColorCssName) {\n      return themeColor;\n    }\n\n    return getComputedStyle(document.documentElement).getPropertyValue(this.getSubColorNameForCssName(themeColorCssName, subColor ?? ''));\n  }\n\n  public getThemeColorContrast(themeColor: ThemeColor | string): string {\n    const themeColorCssName = this.themeColorToCssName.get(themeColor as ThemeColor);\n    if(!themeColorCssName) {\n      return this.adjustColorBrightness(themeColor, 255, true);\n    }\n    return this.themeColorToContrastColor.get(themeColor as ThemeColor) ?? '#000';\n  }\n\n  public adjustColorBrightness(color: string, brightnessAmount: number, flip: boolean, transparency?: number): string {\n    const R = parseInt(color.substring(1, 3), 16);\n    const G = parseInt(color.substring(3, 5), 16);\n    const B = parseInt(color.substring(5, 7), 16);\n\n    if(transparency != null) {\n      const [r, g, b] = color.match(/\\w\\w/g)!.map(x => parseInt(x, 16));\n      return `rgba(${r},${g},${b},${transparency})`;\n    }\n\n    const RR = this.adjustColorValue(R, brightnessAmount, flip);\n    const GG = this.adjustColorValue(G, brightnessAmount, flip);\n    const BB = this.adjustColorValue(B, brightnessAmount, flip);\n    return `#${RR}${GG}${BB}`;\n  }\n\n  private adjustColorValue(hex: number, brightness: number, flip: boolean): string {\n    // add or substract brightness value\n    let hexWithBright = hex + brightness > 255 && flip ? hex - brightness : hex + brightness;\n    // normalize value\n    if(hexWithBright > 255) hexWithBright = 255;\n    else if(flip) hexWithBright = Math.abs(hexWithBright);\n    else if(hexWithBright < 0) hexWithBright = 0;\n    // return RGB part\n    return hexWithBright.toString(16).length === 1 ? `0${hexWithBright.toString(16)}` : hexWithBright.toString(16);\n  }\n\n  private getSubColorNameForCssName(cssName: string, subColor: string): string {\n    if(subColor.length) {\n      return `${cssName}-${subColor}`;\n    }\n\n    return cssName;\n  }\n\n  private setCustomColor(colorTypeFullCssName: string, colorValue: string): void {\n    document.documentElement.style.setProperty(colorTypeFullCssName, colorValue);\n  }\n}\n","import { Container } from 'aurelia-dependency-injection';\nimport { Store } from 'aurelia-store';\nimport { first } from 'rxjs/operators';\n\nexport class StoreManager {\n  public static getStore<TState>(): Store<TState> {\n    return StoreManager.retrieveStore();\n  }\n\n  public static async getState<TState>(): Promise<TState> {\n    return StoreManager.getStore<TState>().state.pipe(first()).toPromise();\n  }\n\n  private static retrieveStore<TState>(): Store<TState> {\n    const hasResolver = Container.instance.hasResolver(Store);\n    if(!hasResolver) throw Error('Tried to retrieve Store before it was registered. Await main setup completion');\n\n    return Container.instance.get(Store) as Store<TState>;\n  }\n}\n","/**\n * Timer class for calculating time\n */\nexport class Timer {\n  /**\n   * performance.now() in milliseconds\n   */\n  public static nowMs(): number {\n    return this.toMs(this.nowPMs());\n  }\n\n  /**\n   * performance.now() in seconds\n   */\n  public static nowSeconds(): number {\n    return this.toSeconds(this.nowPMs());\n  }\n\n  /**\n   * convert precise milliseconds to floored milliseconds\n   */\n  public static toMs(pMs: number): number {\n    return Math.floor(pMs);\n  }\n\n  /**\n   * convert precise milliseconds to floored seconds\n   */\n  public static toSeconds(pMs: number): number {\n    return Math.floor(pMs / 1000);\n  }\n\n  /**\n   * performance.now() in precise milliseconds\n   */\n  private static nowPMs(): number {\n    return performance.now();\n  }\n\n  private static convertSnapshot(snap: ITimerSnapshot, converter: (pMs: number) => number): ITimerSnapshot {\n    return {\n      delta: converter(snap.delta),\n      elapsed: converter(snap.elapsed),\n    };\n  }\n\n  private startedPMs: number | null = null;\n  private lastPMs = 0;\n\n  public start(): this {\n    this.startedPMs = Timer.nowPMs();\n    this.lastPMs = this.startedPMs;\n    return this;\n  }\n\n  /**\n   * get time between this and the last time the delta (or snapshot) was requested in milliseconds\n   */\n  public getDeltaMs(): number {\n    return Timer.toMs(this.getDeltaPMs());\n  }\n\n  /**\n   * get time between this and the last time the delta (or snapshot) was requested in seconds\n   */\n  public getDeltaSeconds(): number {\n    return Timer.toSeconds(this.getDeltaSeconds());\n  }\n\n  /**\n   * get time between now and start in milliseconds\n   */\n  public getElapsedMs(): number {\n    return Timer.toMs(this.getElapsedPMs());\n  }\n\n  /**\n   * get time between now and start in seconds\n   */\n  public getElapsedSeconds(): number {\n    return Timer.toSeconds(this.getElapsedPMs());\n  }\n\n  /**\n   * get delta and elapsed in one (milliseconds)\n   */\n  public snapMs(): ITimerSnapshot {\n    return Timer.convertSnapshot(this.snapPMs(), Timer.toMs);\n  }\n\n  /**\n   * get delta and elapsed in one (seconds)\n   */\n  public snapSeconds(): ITimerSnapshot {\n    return Timer.convertSnapshot(this.snapPMs(), Timer.toSeconds);\n  }\n\n  private getElapsedPMs(): number {\n    return Timer.nowPMs() - this.assertAndGetStartedPMs();\n  }\n\n  private getDeltaPMs(): number {\n    this.assertAndGetStartedPMs();\n    return this.getAndUpdateDeltaPMs(Timer.nowPMs());\n  }\n\n  private snapPMs(): ITimerSnapshot {\n    const startedPMs = this.assertAndGetStartedPMs();\n    const nowPMs = Timer.nowPMs();\n    const deltaPMs = this.getAndUpdateDeltaPMs(nowPMs);\n\n    return {\n      delta: deltaPMs,\n      elapsed: nowPMs - startedPMs,\n    };\n  }\n\n  private assertAndGetStartedPMs(): number {\n    if(this.startedPMs === null) {\n      throw new Error('start() was not called');\n    }\n\n    return this.startedPMs;\n  }\n\n  private getAndUpdateDeltaPMs(nowPMs: number): number {\n    const deltaPMs = nowPMs - this.lastPMs;\n    this.lastPMs = nowPMs;\n    return deltaPMs;\n  }\n}\n\nexport interface ITimerSnapshot {\n  elapsed: number;\n  delta: number;\n}\n","import { ArrayUtils } from 'common/utils/ArrayUtils';\n\n/**\n * Used to execute multiple updates at the same time every 1000ms.\n * This can prevent weird timing Issues where Update A has been executed but Update B hasn't yet, but both are used in a calculation.\n *\n * An example for an use case is the course progress.\n * Since 2 independent mechanisms are tracking the users activity (the PageActivityTracker + CourseBlockVideo),\n * the course progress will fluctuate based on the timing where it gets calculated.\n * To prevent this, we track the activity in the same cycle (in `subscribe`)\n * and then recalculate the progress after the updates have been done (in `subscribeToAfterUpdate`)\n */\nexport class UpdateTimer {\n  private static instance: UpdateTimer | null = null;\n\n  private subscriptions: Array<ISubscription> = [];\n  private afterUpdateSubscriptions: Array<ISubscription> = [];\n  private callingSubscriptions = false;\n\n  private constructor() {\n    window.setInterval(() => {\n      if(!this.callingSubscriptions) {\n        this.callSubscriptions();\n      }\n    }, 1000);\n  }\n\n  public static getInstance(): UpdateTimer {\n    if(!UpdateTimer.instance) {\n      UpdateTimer.instance = new UpdateTimer();\n    }\n\n    return UpdateTimer.instance;\n  }\n\n  public subscribe(scope: unknown, callback: TimerCallback): void {\n    this.subscriptions.push({\n      scope,\n      callback,\n    });\n  }\n\n  public subscribeToAfterUpdate(scope: unknown, callback: TimerCallback): void {\n    this.afterUpdateSubscriptions.push({\n      scope,\n      callback,\n    });\n  }\n\n  public unsubscribe(scope: unknown): void {\n    this.subscriptions = ArrayUtils.remove(this.subscriptions, s => s.scope === scope);\n    this.afterUpdateSubscriptions = ArrayUtils.remove(this.afterUpdateSubscriptions, s => s.scope === scope);\n  }\n\n  /**\n   * returns true if some updates are pending\n   */\n  public isUpdating(): boolean {\n    return this.callingSubscriptions;\n  }\n\n  private async callSubscriptions(): Promise<void> {\n    this.callingSubscriptions = true;\n\n    await this.callCallbacks(() => this.subscriptions);\n    await this.callCallbacks(() => this.afterUpdateSubscriptions);\n\n    this.callingSubscriptions = false;\n  }\n\n  private async callCallbacks(subscriptionsGetter: () => Array<ISubscription>): Promise<void> {\n    const subscriptions = subscriptionsGetter();\n    for(const subscription of subscriptions) {\n      try {\n        /**\n         * since this async it can happen that some listener is already unsubscribed now\n         * this happens very rarely, but apparently it can\n         */\n        if(subscriptionsGetter().indexOf(subscription) >= 0) {\n          await subscription.callback();\n        }\n      } catch(e) {\n        console.error(e);\n      }\n    }\n  }\n}\n\nexport type TimerCallback = (() => void) | (() => Promise<void>);\n\ninterface ISubscription {\n  scope: any;\n  callback: TimerCallback;\n}\n","import { ReservedManagerEventName } from 'common/enum/websocket.enum';\nimport { ReservedEvents } from 'common/interfaces/websocket.interface';\nimport { AppResponse, Successful } from 'common/types';\nimport io, { ManagerOptions, Socket } from 'socket.io-client';\nimport { WebSocketError } from './Error';\n\n/**\n * An events map is an interface that maps event names to their value, which\n * represents the type of the `on` listener.\n */\nexport type EventMap = {\n  [event: string]: (...args: any[]) => void;\n};\n\n/**\n * Returns a union type containing all the keys of an event map.\n */\nexport type EventNames<Map extends EventMap> = keyof Map & string;\n\n/** The tuple type representing the parameters of an event listener */\nexport type EventParams<\n    Map extends EventMap,\n    Ev extends EventNames<Map>\n    > = Parameters<Map[Ev]>;\n\n/**\n * The event names that are either in ReservedEvents or in UserEvents\n */\nexport type ReservedOrUserEventNames<\n    UserEvents extends EventMap\n    > = EventNames<ReservedEvents> | EventNames<UserEvents>;\n\n/**\n * Type of a listener of a user event or a reserved event. If `Ev` is in\n * `ReservedEvents`, the reserved event listener is returned.\n */\nexport type ReservedOrUserListener<\n    UserEvents extends EventMap,\n    Ev extends ReservedOrUserEventNames<UserEvents>\n    > = FallbackToUntypedListener<\n    Ev extends EventNames<ReservedEvents>\n        ? ReservedEvents[Ev]\n        : Ev extends EventNames<UserEvents>\n        ? UserEvents[Ev]\n        : never\n    >;\n\n/**\n * Returns an untyped listener type if `T` is `never`; otherwise, returns `T`.\n *\n * This is a hack to mitigate https://github.com/socketio/socket.io/issues/3833.\n * Needed because of https://github.com/microsoft/TypeScript/issues/41778\n */\ntype FallbackToUntypedListener<T> = [T] extends [never]\n    ? (...args: any[]) => void | Promise<void>\n    : T;\n\nexport type EmitConfig<TData extends Partial<Record<string, any>> = Record<string, never>, TResponse = unknown, TResult = unknown> = {\n  data: TData;\n  response: TResponse;\n  result: TResult;\n};\n\nexport type EmitMap = { [key: string]: { data: Partial<Record<string, any>>; response: unknown; result: unknown } };\n\nexport type EmitData<TMap extends EmitMap, TKey extends string> = TMap[TKey]['data'];\nexport type EmitResponse<TMap extends EmitMap, TKey extends string> = AppResponse<TMap[TKey]['response']>;\nexport type EmitResult<TMap extends EmitMap, TKey extends string> = TMap[TKey]['result'];\nexport type EmitValueResolver<TMap extends EmitMap, TKey extends string> = (ack: Successful<EmitResponse<TMap, TKey>>) => EmitResult<TMap, TKey>;\n\n\nexport type EventListeners<TListenerMap extends EventMap> = Partial<{ [P in ReservedOrUserEventNames<TListenerMap>]: ReservedOrUserListener<TListenerMap, P> }>;\n\nexport class WebSocketConnection<TListenerMap extends EventMap, TEmitMap extends EmitMap> {\n  private socket: Socket;\n\n  constructor(\n    private uri: string,\n    private path: string,\n    private reconnectionAttempts?: number,\n  ) {\n    this.socket = io(this.uri, this.connectionOpts);\n  }\n\n  public get connected(): boolean {\n    return this.socket.connected;\n  }\n\n  public get disconnected(): boolean {\n    return this.socket.disconnected;\n  }\n\n  public connect(): void {\n    if(this.socket.connected) return;\n    this.socket.connect();\n  }\n\n  public disconnect(): void {\n    // Also cancel pending connections and reconnect timers during logout.\n    this.socket.disconnect();\n  }\n\n  public addEventListeners(listeners: EventListeners<TListenerMap>): void {\n    for(const [eventName, callback] of Object.entries(listeners)) {\n      if(Object.values<string>(ReservedManagerEventName).includes(eventName)) {\n        this.socket.io.on(eventName as ReservedManagerEventName, (...args: any[]) => callback(...args));\n      } else {\n        this.socket.on(eventName, (...args: any[]) => callback(...args));\n      }\n    }\n  }\n\n  public async emit<TEventName extends keyof TEmitMap & string>(\n    eventName: TEventName,\n    data: EmitData<TEmitMap, TEventName>,\n  ): Promise<void>;\n  public async emit<TEventName extends keyof TEmitMap & string>(\n    event: TEventName,\n    data: EmitData<TEmitMap, TEventName>,\n    valueResolver: EmitValueResolver<TEmitMap, TEventName>,\n  ): Promise<EmitResult<TEmitMap, TEventName>>;\n  public async emit<TEventName extends keyof TEmitMap & string>(\n    event: TEventName,\n    data: EmitData<TEmitMap, TEventName>,\n    valueResolver?: EmitValueResolver<TEmitMap, TEventName> | undefined,\n  ): Promise<EmitResult<TEmitMap, TEventName> | void> {\n    if(this.disconnected) this.connect();\n    // the BE expects a subscription handler param even if there is no Dto defined in the handler\n    // if none is transmitted it will try to parse the ACK-function with JSON.parse, so we set the default arg if none were provided to empty object\n\n    return new Promise((resolve, reject) => {\n      this.socket.emit(event, data, (ack: EmitResponse<TEmitMap, TEventName>) => {\n        try {\n          this.assertSuccess(ack);\n          resolve(valueResolver?.(ack));\n        } catch(e) {\n          reject(e);\n        }\n      });\n    });\n  }\n\n  private assertSuccess<T>(response: AppResponse<T>): asserts response is Successful<AppResponse<T>> {\n    if(!response.success) {\n      throw WebSocketError.fromAppResponse(response);\n    }\n  }\n\n  private get connectionOpts(): Partial<ManagerOptions> {\n    return {\n      transports: ['websocket'],\n      path: this.path,\n      autoConnect: false,\n      // needed for up to date cookies\n      forceNew: true,\n      reconnectionAttempts: this.reconnectionAttempts,\n    };\n  }\n}\n","import { WebAnimationPlayer } from './WebAnimationPlayer';\n\nexport class FadeAnimationPlayer extends WebAnimationPlayer {\n  private element: HTMLElement;\n  private options: IOptions;\n  private animation: Animation | null = null;\n\n  constructor(element: HTMLElement, options?: Partial<IOptions>) {\n    super();\n    this.element = element;\n    this.options = {\n      duration: 300,\n      ...options,\n    };\n\n    this.element.style.opacity = '0';\n  }\n\n  public forward(): void {\n    this.playAnimationsForward([\n      this.getOrCreateAnimation(),\n    ]);\n  }\n\n  public backward(): void {\n    this.playAnimationsBackward([\n      this.getOrCreateAnimation(),\n    ]);\n  }\n\n  private getOrCreateAnimation(): Animation {\n    if(!this.animation) {\n      this.animation = this.element.animate([\n        { opacity: 0 },\n        { opacity: 1 },\n      ], {\n        duration: this.options.duration,\n        fill: 'both',\n        easing: 'ease',\n      });\n    }\n\n    return this.animation;\n  }\n}\n\nexport interface IOptions {\n  duration: number;\n}\n","import { EventEmitter } from 'common/aurelia-classes/EventEmitter';\n\ninterface IEventEmitterConfig {\n  finishForward: undefined;\n  finishBackward: undefined;\n  finish: undefined;\n}\n\nexport abstract class WebAnimationPlayer {\n  protected eventEmitter = new EventEmitter<IEventEmitterConfig>();\n\n  public addFinishEventListener(scope: unknown, callback: () => void): void {\n    this.eventEmitter.addEventListener(scope, 'finish', callback);\n  }\n\n  public addFinishForwardEventListener(scope: unknown, callback: () => void): void {\n    this.eventEmitter.addEventListener(scope, 'finishForward', callback);\n  }\n\n  public addFinishBackwardEventListener(scope: unknown, callback: () => void): void {\n    this.eventEmitter.addEventListener(scope, 'finishBackward', callback);\n  }\n\n  public removeEventListenersByScope(scope: unknown): void {\n    this.eventEmitter.removeEventListenersByScope(scope);\n  }\n\n  /**\n   * play the animation in the forward direction\n   */\n  public abstract forward(): void;\n\n  /**\n   * play the animation in the backward direction\n   */\n  public abstract backward(): void;\n\n  protected playAnimationsForward(animations: Array<Animation>): void {\n    this.playAnimations(animations, 1, () => {\n      this.forwardFinished();\n    });\n  }\n\n  protected playAnimationsBackward(animations: Array<Animation>): void {\n    this.playAnimations(animations, -1, () => {\n      this.backwardFinished();\n    });\n  }\n\n  private playAnimations(animations: Array<Animation>, playbackRate: number, onFinish: () => void): void {\n    let isFirst = true;\n    for(const animation of animations) {\n      if(isFirst) {\n        animation.onfinish = onFinish;\n      }\n\n      animation.playbackRate = playbackRate;\n      animation.play();\n\n      isFirst = false;\n    }\n  }\n\n  private forwardFinished(): void {\n    this.eventEmitter.dispatch('finishForward', undefined);\n    this.eventEmitter.dispatch('finish', undefined);\n  }\n\n  private backwardFinished(): void {\n    this.eventEmitter.dispatch('finishBackward', undefined);\n    this.eventEmitter.dispatch('finish', undefined);\n  }\n}\n","import { FrameworkConfiguration } from 'aurelia-framework';\nimport { IResourcePreloaderConfig, ResourcePreloader } from 'common/aurelia-classes/ResourcePreloader';\nimport { ILocaleUtilsConfig, LocaleUtils } from 'common/utils/LocaleUtils';\n\nexport function configure(_: FrameworkConfiguration, opts: IAureliaClassesOptions): void {\n  LocaleUtils.configure(opts.localeUtilsConfig);\n  ResourcePreloader.configure(opts.resourcePreloaderConfig);\n}\n\nexport interface IAureliaClassesOptions {\n  localeUtilsConfig: ILocaleUtilsConfig;\n  resourcePreloaderConfig: IResourcePreloaderConfig;\n}\n"],"names":[],"sourceRoot":""}