{"version":3,"file":"app-96608726.f584a1446b2fc034d810.bundle.js","mappings":"AAGA","sources":["webpack://lernplattform-admin-frontend/./src/resources/elements/file-drop-zone/file-drop-zone.html","webpack://lernplattform-admin-frontend/./src/resources/elements/fill-in-the-blank-preview/fill-in-the-blank-preview.html","webpack://lernplattform-admin-frontend/./src/resources/elements/filtered-export-widget/filtered-export-widget.html","webpack://lernplattform-admin-frontend/./src/resources/elements/font-preloader/font-preloader.html","webpack://lernplattform-admin-frontend/./src/resources/elements/file-drop-zone/file-drop-zone.ts","webpack://lernplattform-admin-frontend/./src/resources/elements/fill-in-the-blank-preview/fill-in-the-blank-preview.ts","webpack://lernplattform-admin-frontend/./src/resources/elements/filtered-export-widget/filtered-export-widget.ts","webpack://lernplattform-admin-frontend/./src/resources/elements/font-preloader/font-preloader.ts"],"sourcesContent":["// Module\nvar code = \"<template class=\\\"file-drop-zone file-drop-zone--${dropZoneStyle} ${ hasMinHeight ? 'file-drop-zone--min-height' : '' }\\\"> <form action=\\\"/file-upload\\\" ref=\\\"dropzoneElement\\\" class=\\\"dropzone file-drop-zone__form\\\"> <div if.to-view=\\\"!processing\\\" class=\\\"file-drop-zone__description\\\"> <safe-icon if.to-view=\\\"showIcon\\\" class=\\\"file-drop-zone__description-icon\\\" icon-name=\\\"System/Upload${ dropZoneStyle === DropZoneStyle.Gallery ? `-Gallery` : `` }\\\"></safe-icon> <p class=\\\"file-drop-zone__description-text\\\" t=\\\"${ descriptionTextTk == null ? 'elements.fileDropZone.placeholder' : descriptionTextTk }\\\"></p> <p if.to-view=\\\"fileInfoText\\\" class=\\\"file-drop-zone__description-text\\\">${ fileInfoText }</p> </div> <div class=\\\"fallback\\\"> <input name=\\\"file\\\" type=\\\"file\\\" click.delegate=\\\"onFileInputClick($event)\\\" multiple=\\\"multiple\\\"/> </div> </form> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"fill-in-the-blank-preview\\\"> <div repeat.for=\\\"fitbText of fitbTexts\\\" class=\\\"fill-in-the-blank-preview__question\\\"> <label class=\\\"fill-in-the-blank-preview__question-label\\\">${fitbText}</label> <div class=\\\"fill-in-the-blank-preview__question-select-wrapper\\\"> <select class=\\\"fill-in-the-blank-preview__question-select\\\"> <option repeat.for=\\\"answer of getAnswers(question.answers[$index])\\\" class=\\\"${answer.isCorrect ? 'text-success' : 'text-danger'}\\\" selected.to-view=\\\"answer.isCorrect\\\" disabled.to-view=\\\"!answer.isCorrect\\\">${answer.text}</option> </select> <safe-icon class=\\\"fill-in-the-blank-preview__question-select-icon\\\" icon-name=\\\"System/Tabler/Chevron-Down\\\"> </safe-icon> </div> </div> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template> <dropdown-button offset.to-view=\\\"[-5, 6]\\\"> <button-standard slot=\\\"button\\\" btn-text.to-view=\\\"btnText\\\" icon-name=\\\"System/Tabler/Download\\\" btn-color=\\\"secondary\\\"></button-standard> <button class=\\\"dropdown-item pointer\\\" click.delegate=\\\"exportCsv(CSVType.TSV_UTF16BOM)\\\" t=\\\"globals.csvTypeExcel\\\"></button> <button class=\\\"dropdown-item pointer\\\" click.delegate=\\\"exportCsv(CSVType.CSV_UTF8)\\\" t=\\\"globals.csvTypeCsv\\\"></button> <div if.to-view=\\\"filter.filter.length > 0\\\" class=\\\"dropdown-divider\\\"></div> <div if.to-view=\\\"filter.filter.length > 0\\\" class=\\\"dropdown-item disabled\\\" t=\\\"pages.logs.export.filterNotice\\\" t-params.to-view=\\\"{ count: filter.filter.length }\\\"></div> </dropdown-button> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template> <div class=\\\"font_preload\\\" style=\\\"opacity:0\\\"> <span style=\\\"font-family:Inter;font-weight:600\\\"></span> <span style=\\\"font-family:Inter;font-style:italic\\\"></span> <span style=\\\"font-family:Inter;font-style:italic;font-weight:600\\\"></span> </div> </template> \";\n// Exports\nexport default code;","import { autoinject, bindable } from 'aurelia-framework';\nimport { connectTo } from 'aurelia-store';\nimport { ApiError } from 'common/aurelia-classes/Error';\nimport { HttpRequestHeaders } from 'common/constants/http-headers.constants';\nimport { OperationStatus } from 'common/dto/other/operation.response';\nimport { AppResponse, DefaultAppResponse, Failed } from 'common/types';\nimport { IUploadQuery } from 'common/utils/ApiQueryStringUtils';\nimport { DomUtils } from 'common/utils/DomUtils';\nimport type Dropzone from 'dropzone';\nimport type { DropzoneFile } from 'dropzone';\nimport { ExportImportService, MediaLibraryContext, TrackedExportImportOperation } from '../../../services/ExportImportService';\nimport { PageLoadingIndicatorService } from './../../../services/PageLoadingIndicatorService';\nimport { ToastService } from './../../../services/ToastService';\nimport { IState } from './../../../store/state';\n\nexport enum DropZoneStyle {\n  Background = 'background',\n  Regular = 'regular',\n  Gallery = 'gallery'\n}\n\nexport enum UploadMode {\n  /**\n   * upload a single file\n   */\n  Single = 'single',\n  /**\n   * upload multiple files in parallel\n   */\n  Multiple = 'multiple',\n}\n\nexport interface IFileDropZoneDelegate {\n  /**\n   * @param file\n   * @return {string} the upload url for the file\n   */\n  getFileUploadUrl(file: DropzoneFile): string;\n  /**\n   * the delegate is notified when files have been added to the dropzone.\n   * @param files\n   * @return {boolean} `true` if the added files should be processed immediately. When `false`\n   * is returned you have to call `FileDropZone.processQueue()` manually\n   */\n  didAddFiles?(files: Array<DropzoneFile>): boolean;\n}\n\n@connectTo()\n@autoinject()\nexport class FileDropZone {\n  @bindable() public dropZoneStyle: DropZoneStyle = DropZoneStyle.Regular;\n  @bindable() public url: string = '/';\n  @bindable() public uploadQuery: IUploadQuery<File>[] | null = null;\n  @bindable() public maxFiles: number = 1;\n  @bindable() public uploadCallback: (file: File, url: string) => Record<string, string> = () => ({});\n  @bindable() public errorCallback: null | ((message: ApiError) => void) = null;\n  @bindable() public successCallback: null | ((file: File) => void) = null;\n  @bindable() public addedFileCallback: null | ((file: File) => string) = null;\n  @bindable() public acceptedFiles: string | undefined = 'image/png, image/jpeg, image/svg+xml';\n  @bindable() public draggingInProgress = false;\n  @bindable() public disableClick = false;\n  @bindable() public descriptionTextTk: string | null = null;\n  @bindable() public fileInfoText: string | null = null;\n  @bindable() public hasMinHeight = true;\n  @bindable() public showIcon = true;\n  @bindable() public delegate?: IFileDropZoneDelegate;\n  @bindable() public uploadMode: UploadMode = UploadMode.Single;\n\n  /**\n   * To prevent \"Too many workers active\" error we limit the number of concurrent upload workers.\n   * Files will stay queued until running workers are finished.\n   */\n  private static MAX_CONCURRENT_UPLOAD_WORKERS = 3;\n\n  private state!: IState;\n  private UploadMode = UploadMode;\n  private DropZoneStyle = DropZoneStyle;\n  private dropzoneElement!: HTMLElement;\n  private processing = false;\n  private dropzoneInstance?: Dropzone;\n\n  /**\n   * Used to track (con)current upload workers\n   */\n  private uploadOperationIds: Set<number> = new Set();\n\n  constructor(\n    private element: Element,\n    private toastService: ToastService,\n    private indicatorService: PageLoadingIndicatorService,\n    private exportImportService: ExportImportService,\n  ) { }\n\n  /**\n   * processes the dropzone queue if there aren't to many workers running\n   */\n  public processQueue(): void {\n    if(this.uploadOperationIds.size < FileDropZone.MAX_CONCURRENT_UPLOAD_WORKERS) {\n      this.dropzoneInstance?.processQueue();\n    }\n  }\n\n  public removeAllFiles(): void {\n    this.dropzoneInstance?.removeAllFiles(true);\n  }\n\n  protected async attached(): Promise<void> {\n    this.exportImportService.subscribeToOperations(this, MediaLibraryContext, this.operationsDidUpdate.bind(this));\n    // eslint-disable-next-line @typescript-eslint/no-this-alias\n    const self = this;\n    const { csrfToken } = this.state;\n    if(!csrfToken) {\n      throw new Error('Cannot initialize dropzone without csrfToken');\n    }\n    const DropzoneImp = await import(\n      /* webpackChunkName: \"Dropzone\" */ 'dropzone'\n    );\n    this.dropzoneInstance = new DropzoneImp.default(this.dropzoneElement, {\n      url: self.url,\n      withCredentials: true,\n      acceptedFiles: this.acceptedFiles,\n      maxFiles: this.maxFiles,\n      clickable: !this.disableClick,\n      uploadMultiple: self.uploadMode === UploadMode.Multiple,\n      // queue should only be auto-processed in single file mode\n      autoProcessQueue: self.uploadMode === UploadMode.Single,\n      // default parallel uploads is 2 -> leads to many BE workers\n      parallelUploads: 10,\n      // the interface makes us believe we cannot do this, but we can\n      // eslint-disable-next-line\n      // @ts-ignore\n      paramName: () => self.uploadMode === UploadMode.Multiple\n        ? 'files[]'\n        : 'file',\n      init: function() {\n        const fileUrlMap: WeakMap<File, string> = new WeakMap();\n        // eslint-disable-next-line @typescript-eslint/no-this-alias\n        const currentDropzone = this;\n        currentDropzone.on('error', async (file, message: Failed<AppResponse<unknown>>) => {\n          const error = await ApiError.fromFailedAppResponse(message, 400);\n          self.onUploadError(error);\n          currentDropzone.removeFile(file);\n          self.indicatorService.stop();\n          self.processing = false;\n          self.draggingInProgress = false;\n          fileUrlMap.delete(file);\n        });\n        currentDropzone.on('success', (file, response) => {\n          self.onUploadSuccess(file, response);\n          currentDropzone.removeFile(file);\n          self.indicatorService.stop();\n          self.processing = false;\n          self.draggingInProgress = false;\n          fileUrlMap.delete(file);\n        });\n        currentDropzone.on('sending', (file, xhr, formData) => {\n          // url fallback to binding\n          const url = fileUrlMap.get(file) ?? self.delegate?.getFileUploadUrl(file) ?? self.url;\n          const uploadData = self.uploadCallback(file, url);\n          for(const [key, value] of Object.entries(uploadData)) {\n            formData.append(key, value);\n          }\n        });\n        currentDropzone.on('addedfiles', files => {\n          if(self.uploadMode === UploadMode.Single) return;\n\n          const processImmediately = self.delegate?.didAddFiles?.(files) ?? false;\n          if(processImmediately) self.processQueue();\n        });\n        currentDropzone.on('successmultiple', (_files: Array<DropzoneFile>, response: string | object) => {\n          // if the upload started a worker we should consider it still uploading until the worker has finished\n          if(typeof response === 'object' && 'operationId' in response && typeof (response as Record<'operationId', unknown>).operationId === 'number') {\n            const { operationId } = response as Record<'operationId', number>;\n\n            self.uploadOperationIds.add(operationId);\n          }\n\n          self.processQueue();\n        });\n        currentDropzone.on('errormultiple', () => {\n          self.processQueue();\n        });\n        currentDropzone.on('addedfile', file => {\n          const url = self.addedFileCallback?.(file) ?? self.url;\n          if(url != null) fileUrlMap.set(file, url);\n          currentDropzone.options.acceptedFiles = self.acceptedFiles;\n        });\n        currentDropzone.on('processing', function(file) {\n          self.processing = true;\n          let url = fileUrlMap.get(file) ?? self.delegate?.getFileUploadUrl(file);\n          if(!url) {\n            self.processing = false;\n            self.draggingInProgress = false;\n            currentDropzone.cancelUpload(file);\n            throw new Error('expected to have upload url for file');\n          }\n\n          if(self.uploadQuery) {\n            url = `${url}?${self.uploadQuery.map(query => `${query.queryKey}=${file[query.value]}`).join('&')}`;\n          }\n          currentDropzone.options.url = url;\n          self.indicatorService.start();\n        });\n        currentDropzone.on('dragenter', function() {\n          self.draggingInProgress = true;\n        });\n        currentDropzone.on('dragleave', function() {\n          self.draggingInProgress = false;\n        });\n      },\n      headers: {\n        [HttpRequestHeaders.X_CSRF_TOKEN]: csrfToken\n      }\n    });\n  }\n\n  private operationsDidUpdate(operations: Array<TrackedExportImportOperation>): void {\n    // filter finished operations from operation ids\n    const currentOperationIds = Array.from(this.uploadOperationIds);\n    for(const operationId of currentOperationIds) {\n      const operation = operations.find(op => op.id === operationId);\n      if(operation?.status === OperationStatus.Success || operation?.status === OperationStatus.Error) {\n        this.uploadOperationIds.delete(operationId);\n      }\n    }\n\n    this.processQueue();\n  }\n\n  protected detached(): void {\n    this.dropzoneInstance?.destroy();\n    this.exportImportService.disposeSubscriptions(this);\n  }\n\n  private uploadModeChanged(): void {\n    if(this.dropzoneInstance == null) return;\n\n    this.dropzoneInstance.options.uploadMultiple = this.uploadMode === UploadMode.Multiple;\n    this.dropzoneInstance.options.autoProcessQueue = this.uploadMode === UploadMode.Single;\n  }\n\n  private acceptedFilesChanged(): void {\n    if(this.dropzoneInstance == null) return;\n\n    if(this.dropzoneInstance.hiddenFileInput != null && this.acceptedFiles != null) {\n      this.dropzoneInstance.hiddenFileInput.setAttribute('accept', this.acceptedFiles);\n    }\n    this.dropzoneInstance.options.acceptedFiles = this.acceptedFiles;\n  }\n\n  // Ignore type warning as this comes from the dropzone library\n  // eslint-disable-next-line @typescript-eslint/ban-types\n  private onUploadSuccess(file: DropzoneFile, response: string | Object): void {\n    if(!this.successCallback) {\n      this.toastService.success('elements.fileDropZone.uploadSuccess');\n    }\n    this.successCallback?.(file);\n\n    DomUtils.fireEvent<IFileDropZoneUploadSuccessEventDetail<DefaultAppResponse>>(this.element, {\n      name: 'success',\n      detail: {\n        response: response as DefaultAppResponse,\n        file,\n      },\n      bubbles: true,\n    });\n  }\n\n  private onUploadError(error: ApiError): void {\n    if(!this.errorCallback) {\n      this.toastService.error('elements.fileDropZone.uploadError');\n    }\n    this.errorCallback?.(error);\n  }\n}\n\nexport interface IFileDropZoneUploadSuccessEventDetail<TResponse extends DefaultAppResponse> {\n  response: TResponse;\n  file: DropzoneFile;\n}\n\nexport type FileDropZoneUploadSuccessEvent<TResponse extends DefaultAppResponse> = CustomEvent<IFileDropZoneUploadSuccessEventDetail<TResponse>>;\n\n","import { IQuizQuestionDto } from 'common/dto/course/quiz/quiz.question.interface';\nimport { computedFrom, bindable } from 'aurelia-framework';\nimport { QuestionUtils } from 'common/utils/QuestionUtils';\nimport { IQuizAnswerDto } from 'common/dto/course/quiz/quiz.answer.interface';\n\nexport class FillInTheBlankPreview {\n  @bindable() public question!: IQuizQuestionDto;\n  private selectAnswers: ISelectAnswer[] | null = null;\n\n  @computedFrom('question')\n  private get fitbTexts(): string[] {\n    return QuestionUtils.parseFillInTheBlankQuestion(this.question.text);\n  }\n\n  private getAnswers(answer: IQuizAnswerDto): ISelectAnswer[] {\n    return [\n      { text: answer.text1, isCorrect: !!answer.isCorrect1 },\n      { text: answer.text2, isCorrect: !!answer.isCorrect2 },\n      { text: answer.text3, isCorrect: !!answer.isCorrect3 },\n      { text: answer.text4, isCorrect: !!answer.isCorrect4 },\n    ];\n  }\n}\n\ninterface ISelectAnswer {\n  text: string;\n  isCorrect: boolean;\n}","import { autoinject, bindable } from 'aurelia-framework';\nimport { IFilterDto } from 'common/dto/filter/filter.interface';\nimport { CSVType } from 'common/enum/csv.type.enum';\nimport { DomUtils } from 'common/utils/DomUtils';\n\n/**\n * @event {FilteredExportWidgetExportCsvEvent} export-csv\n */\n@autoinject()\nexport class FilteredExportWidget {\n  @bindable() public filter: IFilterDto | null = null;\n  @bindable() public btnText: string = '';\n  private CSVType = CSVType;\n\n  constructor(private element: Element) { }\n\n  public exportCsv(csvType: CSVType): void {\n    DomUtils.fireEvent<FilteredExportWidgetExportCsvEvent['detail']>(this.element, {\n      bubbles: true,\n      detail: {\n        csvType,\n      },\n      name: 'export-csv',\n    });\n  }\n}\n\nexport type FilteredExportWidgetExportCsvEvent = CustomEvent<{ csvType: CSVType }>;\n","// TODO KL-1842 Switch to a more reliable preloading method, as\n// this soft-preloading does not guarantee that a font is loaded at a specific execution time\nexport class FontPreloader {}\n"],"names":[],"sourceRoot":""}