{"version":3,"file":"app-3c440a5d.f584a1446b2fc034d810.bundle.js","mappings":"AAGA","sources":["webpack://lernplattform-admin-frontend/../common/aurelia-components/x-icon/safe-icon.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/x-icon/x-icon.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/x-img/x-img.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/x-picture/x-picture.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/x-spinner/x-spinner.html","webpack://lernplattform-admin-frontend/../common/aurelia-components/x-icon/SvgIdPrefixer.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/x-icon/index.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/x-icon/safe-icon.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/x-icon/x-icon.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/x-img/index.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/x-img/x-img.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/x-picture/index.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/x-picture/x-picture.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/x-spinner/index.ts","webpack://lernplattform-admin-frontend/../common/aurelia-components/x-spinner/x-spinner.ts"],"sourcesContent":["// Module\nvar code = \"<template class=\\\"safe-icon ${showIndicator ? `safe-icon--show-indicator` : ``}\\\"> <style>:host{display:inline-block;width:var(--safe-icon-size);height:var(--safe-icon-size);position:relative}:host>svg{position:absolute;top:0;right:0;bottom:0;left:0;pointer-events:none}.safe-icon__indicator{position:absolute;top:4px;right:0;width:calc(var(--safe-icon-size)/ 3);height:calc(var(--safe-icon-size)/ 3);border-radius:50%;background-color:var(--safe-icon-indicator-color,var(--color-dismiss-red))}:host(.safe-icon--show-indicator)::after{content:'';position:absolute;top:4px;right:0;width:calc(var(--safe-icon-size)/ 3);height:calc(var(--safe-icon-size)/ 3);border-radius:50%;background-color:var(--safe-icon-indicator-color,var(--color-dismiss-red))}</style> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"x-icon ${showIndicator ? `x-icon--indicator` : ``} ${getIconClass(iconType, iconName)}\\\" style.to-view=\\\"getIconStyle(size)\\\"> </template> \";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"${loading ? `x-img--loading` : ``} ${imgLoaded ? `x-img--img-loaded` : ``} ${placeholderRatio != null ? `x-img--placeholder-available` : ``}\\\"> <svg class=\\\"x-img__placeholder\\\" viewBox=\\\"0 0 ${placeholderRatio || 0} 1\\\"> <rect x=\\\"0\\\" y=\\\"0\\\" width=\\\"${placeholderRatio || 0}\\\" height=\\\"1\\\" class=\\\"x-img__placeholder-content\\\"></rect> </svg> <img src.to-view=\\\"imgSrc\\\" class=\\\"x-img__img\\\" load.trigger=\\\"handleImgLoad()\\\" error.trigger=\\\"handleImgError()\\\"> </template> \";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"x-picture x-picture--state-${state} ${placeholderRatio != null ? `x-picture--placeholder-available` : ``}\\\"> <svg class=\\\"x-picture__placeholder\\\" viewBox=\\\"0 0 ${placeholderRatio || 0} 1\\\"> <rect if.to-view=\\\"state === State.Loading\\\" x=\\\"0\\\" y=\\\"0\\\" width=\\\"${placeholderRatio || 0}\\\" height=\\\"1\\\" class=\\\"x-picture__placeholder-content\\\"></rect> </svg> <blurhash class=\\\"x-picture__blurhash\\\" hash.to-view=\\\"picture.blurHash\\\" ratio.to-view=\\\"placeholderRatio\\\"></blurhash> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"x-spinner x-spinner--${status}\\\"> <svg class=\\\"x-spinner__spinner\\\" viewBox=\\\"0 0 66 66\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\"> <circle repeat.for=\\\"circle of [background && 'background', 'foreground']\\\" if.to-view=\\\"circle\\\" class=\\\"x-spinner__circle x-spinner__circle--${circle}\\\" fill=\\\"none\\\" stroke-width.to-view=\\\"strokeWidth\\\" stroke-linecap=\\\"round\\\" cx=\\\"33\\\" cy=\\\"33\\\" r.to-view=\\\"33 - (strokeWidth / 2)\\\"></circle> </svg> <safe-icon if.to-view=\\\"status === 'success'\\\" class=\\\"x-spinner__icon\\\" icon-name=\\\"System/Tabler/Check\\\"></safe-icon> <safe-icon if.to-view=\\\"status === 'error'\\\" class=\\\"x-spinner__icon\\\" icon-name=\\\"System/Tabler/X\\\"></safe-icon> </template>\";\n// Exports\nexport default code;","/**\n * it's necessary to prefix the used ids in a svg file when inlining them, because they share a global scope, but we want the ids to be unique per svg element\n */\nexport class SvgIdPrefixer {\n  private static lastId = 0;\n\n  private linkAttributes = new Set(['xlink:href']);\n  private prefix: string;\n\n  constructor(prefix: string) {\n    this.prefix = `p${++SvgIdPrefixer.lastId}${this.sanitizePrefix(prefix)}`;\n  }\n\n  public prefixSvg(element: SVGSVGElement): void {\n    this.walkThroughSvgElements(element.childNodes, currentElement => {\n      this.prefixSvgElementId(currentElement);\n      this.prefixSvgElementAttributeIdReferences(currentElement);\n    });\n  }\n\n  private walkThroughSvgElements(nodes: NodeListOf<Node>, callback: (currentElement: SVGElement) => void): void {\n    for(let i = 0; i < nodes.length; i++) {\n      const node = nodes[i];\n\n      if(node instanceof SVGElement) {\n        callback(node);\n      }\n\n      this.walkThroughSvgElements(node.childNodes, callback);\n    }\n  }\n\n  // side-effects on the element parameter are expected here\n  /* eslint-disable no-param-reassign */\n  private prefixSvgElementId(element: SVGElement): void {\n    if(element.id) {\n      element.id = this.prefixId(element.id);\n    }\n  }\n  /* eslint-enable no-param-reassign */\n\n  private prefixSvgElementAttributeIdReferences(element: SVGElement): void {\n    for(let i = 0; i < element.attributes.length; i++) {\n      const attr = element.attributes.item(i);\n      if(!attr) {\n        continue;\n      }\n\n      // replace urls in the unquoted format (Chrome, FF)\n      attr.value = attr.value.replace(/url\\(#([^)]+)\\)/, (match, id: string) => {\n        return `url(#${this.prefixId(id)})`;\n      });\n\n      // replace urls in the quote format (Edge)\n      attr.value = attr.value.replace(/url\\(\"#([^\"]+)\"\\)/, (match, id: string) => {\n        return `url(\"#${this.prefixId(id)}\")`;\n      });\n\n      if(this.linkAttributes.has(attr.name)) {\n        attr.value = attr.value.replace(/#(.*)/, (match, id) => {\n          return `#${this.prefixId(id)}`;\n        });\n      }\n    }\n  }\n\n  private prefixId(id: string): string {\n    return `${this.prefix}-${id}`;\n  }\n\n  private sanitizePrefix(prefix: string): string {\n    const cleanedPrefix = prefix.replace(/[- ./]/g, '');\n    const splitCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';\n\n    let sanitizedPrefix = '';\n\n    for(let i = 0; i < cleanedPrefix.length; i++) {\n      const currentChar = cleanedPrefix[i];\n      if(splitCharacters.indexOf(currentChar) >= 0) {\n        sanitizedPrefix += `-${currentChar.toLocaleLowerCase()}`;\n      } else {\n        sanitizedPrefix += currentChar;\n      }\n    }\n\n    return sanitizedPrefix;\n  }\n}\n","import { PLATFORM, FrameworkConfiguration } from 'aurelia-framework';\n\nexport function configure(config: FrameworkConfiguration): void {\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/x-icon/x-icon'));\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/x-icon/safe-icon'));\n}\n","import { autoinject, bindable, PLATFORM, useShadowDOM, useView } from 'aurelia-framework';\nimport { SvgLoaderService } from 'common/aurelia-services/SvgLoaderService';\nimport { assertNotNullOrUndefined } from 'common/utils/assert';\nimport { iconNameToSvgString } from './icons';\n\n@autoinject()\n@useShadowDOM()\n@useView(PLATFORM.moduleName('./safe-icon.html'))\nexport class SafeIcon {\n  @bindable() public iconName: string | null = null;\n  @bindable() public showIndicator = false;\n  @bindable() public iconStyle: Record<string, unknown> | null = null;\n\n  private domElement: HTMLElement;\n  private customIconElement: SVGSVGElement | null = null;\n\n  constructor(\n    element: Element,\n    private svgLoaderService: SvgLoaderService,\n  ) {\n    this.domElement = element as HTMLElement;\n  }\n\n  private iconNameChanged(): void {\n    this.updateCustomIcon();\n  }\n\n  private iconStyleChanged(newValue?: Record<string, unknown> | null, oldValue?: Record<string, unknown> | null): void {\n    Object.keys(oldValue ?? {}).forEach(prop => this.customIconElement?.style.removeProperty(prop));\n    Object.keys(newValue ?? {}).forEach(prop => this.customIconElement?.style.setProperty(prop, String(newValue?.[prop] ?? '')));\n  }\n\n  private async updateCustomIcon(): Promise<void> {\n    assertNotNullOrUndefined(this.domElement.shadowRoot, 'expected safe-icon to have a shadowRoot');\n    const { iconName } = this;\n\n    if(iconName != null) {\n      const iconUrl = iconNameToSvgString[iconName];\n      assertNotNullOrUndefined(iconUrl, `no url found for ${iconName}`);\n\n      const element = await this.svgLoaderService.getPrefixedSvgElement(iconUrl);\n\n      // values could change while we wait for the icon to load, ignore the result if we loaded the wrong icon\n      if(iconName === this.iconName) {\n        this.removeCustomIconElement();\n        this.customIconElement = element;\n        this.domElement.shadowRoot.prepend(element);\n        this.iconStyleChanged(this.iconStyle);\n      }\n    } else {\n      this.removeCustomIconElement();\n    }\n  }\n\n  private removeCustomIconElement(): void {\n    if(this.customIconElement != null) {\n      this.domElement.shadowRoot?.removeChild(this.customIconElement);\n      this.customIconElement = null;\n    }\n  }\n}\n","import { autoinject, bindable, PLATFORM, useView } from 'aurelia-framework';\nimport { SvgLoaderService } from 'common/aurelia-services/SvgLoaderService';\nimport { assertNotNullOrUndefined } from 'common/utils/assert';\nimport { iconNameToSvgString } from './icons';\n\nexport enum IconSize {\n  unspecified = 'unspecified',\n  normal = 'normal',\n}\n\nexport enum IconType {\n  fas = 'fas',\n  fal = 'fal',\n  far = 'far',\n  custom = 'custom',\n}\n\n@autoinject()\n@useView(PLATFORM.moduleName('./x-icon.html'))\nexport class XIcon {\n\n  @bindable() public iconType: IconType | null = null;\n  /**\n   * name of the icon without the `fa-` prefix or the `.svg` extension if it is a custom icon\n   */\n  @bindable() public iconName: string | null = null;\n  @bindable() public size: IconSize = IconSize.normal;\n  @bindable() public showIndicator = false;\n\n  private domElement: HTMLElement;\n  private customIconElement: SVGSVGElement | null = null;\n\n  constructor(\n    element: Element,\n    private svgLoaderService: SvgLoaderService,\n  ) {\n    this.domElement = element as HTMLElement;\n  }\n\n  private iconTypeChanged(): void {\n    this.updateCustomIcon();\n  }\n\n  private iconNameChanged(): void {\n    this.updateCustomIcon();\n  }\n\n  private async updateCustomIcon(): Promise<void> {\n    const { iconType, iconName } = this;\n\n    if(this.iconType === IconType.custom && iconName) {\n      const iconUrl = iconNameToSvgString[iconName];\n      assertNotNullOrUndefined(iconUrl, `no url found for ${iconName}`);\n      const element = await this.svgLoaderService.getPrefixedSvgElement(iconUrl);\n\n      // values could change while we wait for the icon to load, ignore the result if we loaded the wrong icon\n      if(iconType === this.iconType && iconName === this.iconName) {\n        this.removeCustomIconElement();\n        this.customIconElement = element;\n        this.domElement.appendChild(element);\n      }\n    } else {\n      this.removeCustomIconElement();\n    }\n  }\n\n  private removeCustomIconElement(): void {\n    if(this.customIconElement) {\n      if(this.customIconElement.parentNode) {\n        this.customIconElement.parentNode.removeChild(this.customIconElement);\n      }\n\n      this.customIconElement = null;\n    }\n  }\n\n  private getIconClass(iconType: IconType | null, iconName: string | null): string {\n    if(iconType == null || iconName == null) {\n      return '';\n    }\n    const newIconName = iconType === IconType.custom ? '' : ` fa-${iconName}`;\n\n    return `${iconType}${newIconName}`;\n  }\n\n  private getIconStyle(size: IconSize): { [s: string]: any } {\n    switch(size) {\n      case IconSize.normal:\n        return {\n          height: '24px',\n          width: '24px',\n          'line-height': '24px',\n          'font-size': '24px',\n        };\n\n      case IconSize.unspecified:\n        return {};\n\n      default:\n        throw new Error(`unknown icon size \"${size}\"`);\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/x-img/x-img'));\n}\n","import { bindable, useView, PLATFORM } from 'aurelia-framework';\n\n/**\n * an img wrapper which prevents showing the old picture when setting a new src\n *\n * @cssvar --x-img-placeholder-color\n */\n@useView(PLATFORM.moduleName('./x-img.html'))\nexport class XImg {\n  @bindable()\n  public src: string | null = null;\n\n  /**\n   * if placeholderRatio is given, then a placeholder will be shown while the picture is being loaded\n   */\n  @bindable()\n  public placeholderRatio: number | null = null;\n\n  private imgSrc = '';\n  private loading = false;\n  private imgLoaded = false;\n\n  private srcChanged(): void {\n    const newImageSrc = this.src || '';\n\n    if(newImageSrc === this.imgSrc) {\n      return;\n    }\n\n    this.loading = true;\n    this.imgSrc = newImageSrc;\n  }\n\n  private handleImgLoad(): void {\n    this.loading = false;\n    this.imgLoaded = true;\n  }\n\n  private handleImgError(): void {\n    this.loading = false;\n  }\n}\n","import { PLATFORM, FrameworkConfiguration } from 'aurelia-framework';\r\n\r\nexport function configure(config: FrameworkConfiguration): void {\r\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/x-picture/x-picture'));\r\n}\r\n","import { autoinject, bindable, Disposable, PLATFORM, useView } from 'aurelia-framework';\r\nimport { RefCache } from 'common/aurelia-classes/RefCache';\r\nimport { IPictureDto } from 'common/dto/course/content/course.interface';\r\nimport { assertNotNullOrUndefined } from 'common/utils/assert';\r\nimport { HttpRequestUtils } from 'common/utils/HttpRequestUtils';\r\nimport { IdGeneratorService } from '../../aurelia-services/IdGeneratorService';\r\nimport { SvgIdPrefixer } from '../x-icon/SvgIdPrefixer';\r\n\r\n@autoinject\r\n@useView(PLATFORM.moduleName('./x-picture.html'))\r\nexport class XPicture {\r\n  private static svgDataCache = new RefCache<string, string>();\r\n  private static imageCache = new RefCache<string, HTMLImageElement>();\r\n\r\n  @bindable() public picture?: IPictureDto | null = null;\r\n  @bindable() public inlineSvg = false;\r\n\r\n  private State = XPictureState;\r\n\r\n  private state = XPictureState.Uninitialized;\r\n  private placeholderRatio: number | null = null;\r\n  private imageElement: Element | null = null;\r\n  private currentPictureUrl: string | null = null;\r\n  private currentWidth: number | null = null;\r\n  private currentHeight: number | null = null;\r\n  private cacheRef: Disposable | null = null;\r\n\r\n  constructor(\r\n    private domElement: Element,\r\n    private idGeneratorService: IdGeneratorService,\r\n  ) { }\r\n\r\n  protected detached(): void {\r\n    this.cacheRef?.dispose();\r\n  }\r\n\r\n  protected async pictureChanged(): Promise<void> {\r\n    if(this.currentPictureUrl === this.picture?.url\r\n      && this.currentWidth === this.picture.width\r\n      && this.currentHeight === this.picture.height) {\r\n      return;\r\n    }\r\n    this.refreshImage();\r\n  }\r\n\r\n  protected inlineSvgChanged(): void {\r\n    this.refreshImage();\r\n  }\r\n\r\n  private async refreshImage(): Promise<void> {\r\n    if(!this.picture?.url) return;\r\n\r\n    const { url, width, height } = this.picture;\r\n    this.currentPictureUrl = url;\r\n\r\n    this.placeholderRatio = width && height ? width / height : null;\r\n    this.currentWidth = width;\r\n    this.currentHeight = height;\r\n\r\n    if(this.inlineSvg && this.isSvg(url)) {\r\n      await this.appendImageElement(url, {\r\n        cache: XPicture.svgDataCache,\r\n        loadData: this.loadSvgData.bind(this),\r\n        createElement: this.createSvgElement.bind(this),\r\n      });\r\n    } else {\r\n      await this.appendImageElement(url, {\r\n        cache: XPicture.imageCache,\r\n        loadData: this.loadImage.bind(this),\r\n        createElement: imgElement => imgElement.cloneNode() as Element,\r\n      });\r\n    }\r\n  }\r\n\r\n  private async appendImageElement<T>(url: string, strategy: ImageRefreshStrategy<T>): Promise<void> {\r\n    this.removeImageElement();\r\n\r\n    let data = strategy.cache.get(url);\r\n    if(!data) {\r\n      this.state = XPictureState.Loading;\r\n      try {\r\n        data = await strategy.loadData(url);\r\n      } catch(e) {\r\n        this.state = XPictureState.Failed;\r\n        throw e;\r\n      }\r\n    }\r\n\r\n    if(url !== this.currentPictureUrl) return;\r\n\r\n    this.cacheRef = strategy.cache.setAndRef(url, data);\r\n    this.imageElement = strategy.createElement(data);\r\n    this.domElement.appendChild(this.imageElement);\r\n    this.state = XPictureState.Loaded;\r\n  }\r\n\r\n  private isSvg(url: string): boolean {\r\n    return url.includes('.svg');\r\n  }\r\n\r\n  private removeImageElement(): void {\r\n    this.cacheRef?.dispose();\r\n    this.imageElement?.remove();\r\n    this.imageElement = null;\r\n  }\r\n\r\n  private loadImage(url: string): Promise<HTMLImageElement> {\r\n    return new Promise<HTMLImageElement>((resolve, reject) => {\r\n      const img = new Image();\r\n      // flipped/mirrored classes are a hack to support flipping despite imgproxy not supporting it\r\n      img.setAttribute('class', `x-picture__img${url.endsWith('?flip') ? ' x-picture__img--flipped' : ''}${url.endsWith('?mirror') ? ' x-picture__img--mirrored' : ''}`);\r\n      img.src = url;\r\n      img.onload = () => resolve(img);\r\n      img.onerror = e => reject(e);\r\n    });\r\n  }\r\n\r\n  private createSvgElement(svgData: string): SVGSVGElement {\r\n    const wrapper = document.createElement('div');\r\n    wrapper.innerHTML = svgData;\r\n    assertNotNullOrUndefined(wrapper.firstElementChild, 'Invalid SVG');\r\n    const svgElement = wrapper.firstElementChild as SVGSVGElement;\r\n    svgElement.setAttribute('class', 'x-picture__inline-svg');\r\n    const svgWidth = svgElement.getAttribute('width');\r\n    const svgHeight = svgElement.getAttribute('height');\r\n    const svgViewBox = svgElement.getAttribute('viewBox');\r\n    if(!svgViewBox && svgWidth && svgHeight) {\r\n      svgElement.setAttribute('viewBox', `0 0 ${svgWidth} ${svgHeight}`);\r\n    }\r\n\r\n    const prefixer = new SvgIdPrefixer(`inlineSVG${this.idGeneratorService.getId()}`);\r\n    prefixer.prefixSvg(svgElement);\r\n    return svgElement;\r\n  }\r\n\r\n  private async loadSvgData(url: string): Promise<string> {\r\n    return HttpRequestUtils.get({ url });\r\n  }\r\n}\r\n\r\nenum XPictureState {\r\n  Uninitialized = 'initial',\r\n  Loading = 'loading',\r\n  Loaded = 'loaded',\r\n  Failed = 'failed',\r\n}\r\n\r\ntype ImageRefreshStrategy<T> = {\r\n  cache: RefCache<string, T>;\r\n  loadData: (url: string) => Promise<T>;\r\n  createElement: (data: T) => Element;\r\n};\r\n","import { PLATFORM, FrameworkConfiguration } from 'aurelia-framework';\n\nexport function configure(config: FrameworkConfiguration): void {\n  config.globalResources(PLATFORM.moduleName('common/aurelia-components/x-spinner/x-spinner'));\n}\n","import { autoinject, bindable, PLATFORM, useView } from 'aurelia-framework';\nimport { OperationStatus } from 'common/dto/other/operation.response';\n\n@useView(PLATFORM.moduleName('./x-spinner.html'))\n@autoinject()\nexport class XSpinner {\n  @bindable() background = false;\n  @bindable() strokeWidth = 6;\n  @bindable() status = OperationStatus.Running;\n}\n"],"names":[],"sourceRoot":""}