{"version":3,"file":"app-a1a47a20.f584a1446b2fc034d810.bundle.js","mappings":"AAGA","sources":["webpack://lernplattform-admin-frontend/./src/resources/elements/media-share-create-form/media-share-create-form.html","webpack://lernplattform-admin-frontend/./src/resources/elements/media-share-table/media-share-table.html","webpack://lernplattform-admin-frontend/./src/resources/elements/media-share/media-share-edit-list-entry.html","webpack://lernplattform-admin-frontend/./src/resources/elements/mp-flipping-card-form/mp-flipping-card-form.html","webpack://lernplattform-admin-frontend/./src/resources/elements/mp-hot-spot-editor/mp-hot-spot-editor.html","webpack://lernplattform-admin-frontend/./src/resources/elements/mp-hot-spot-form/mp-hot-spot-form.html","webpack://lernplattform-admin-frontend/./src/resources/elements/mp-interactive-form/mp-interactive-form.html","webpack://lernplattform-admin-frontend/./src/resources/elements/media-share-create-form/media-share-create-form.ts","webpack://lernplattform-admin-frontend/./src/resources/elements/media-share-table/media-share-table.ts","webpack://lernplattform-admin-frontend/./src/resources/elements/media-share/index.ts","webpack://lernplattform-admin-frontend/./src/resources/elements/media-share/media-share-edit-list-entry.ts","webpack://lernplattform-admin-frontend/./src/resources/elements/mp-flipping-card-form/mp-flipping-card-form.ts","webpack://lernplattform-admin-frontend/./src/resources/elements/mp-hot-spot-editor/mp-hot-spot-editor.ts","webpack://lernplattform-admin-frontend/./src/resources/elements/mp-hot-spot-form/mp-hot-spot-form.ts","webpack://lernplattform-admin-frontend/./src/resources/elements/mp-interactive-form/mp-interactive-form.ts"],"sourcesContent":["// Module\nvar code = \"<template class=\\\"media-share-create-form form-group\\\"> <p class=\\\"text-muted my-5\\\" t=\\\"elements.mediaShareCreateForm.description\\\"></p> <div class=\\\"media-share-create-form__inputs ${ isProvider ? 'media-share-create-form__inputs--courses-only' : '' }\\\"> <div> <label class=\\\"form-label\\\" t=\\\"globals.selectCourse\\\"></label> <text-tags-widget table-columns.to-view=\\\"courseTableColumns\\\" table-data.to-view=\\\"foundCourses\\\" tags.to-view=\\\"selectedCourseTags\\\" tags-edited.trigger=\\\"handleCourseEntryRemoved($event)\\\" entry-selected.trigger=\\\"handleCourseEntrySelected($event)\\\" search-string-changed.trigger=\\\"handleCourseSearchStringChanged($event)\\\" fixed-height.to-view=\\\"true\\\" list-closed.trigger=\\\"validateCourse()\\\" is-invalid.to-view=\\\"!!errorTks.course\\\"></text-tags-widget> <div if.to-view=\\\"errorTks.course\\\" class=\\\"invalid-feedback media-share-create-form__error\\\" t=\\\"${errorTks.course}\\\"></div> </div> <div if.to-view=\\\"!isProvider\\\"> <label class=\\\"form-label\\\" t=\\\"globals.selectCourseGroup\\\"></label> <text-tags-widget table-columns.to-view=\\\"courseGroupTableColumns\\\" table-data.to-view=\\\"foundCourseGroups\\\" tags.to-view=\\\"selectedCourseGroupTags\\\" tags-edited.trigger=\\\"handleCourseGroupEntryRemoved($event)\\\" entry-selected.trigger=\\\"handleCourseGroupEntrySelected($event)\\\" search-string-changed.trigger=\\\"handleCourseGroupSearchStringChanged($event)\\\" fixed-height.to-view=\\\"true\\\" list-closed.trigger=\\\"validateCourseGroups()\\\" is-invalid.to-view=\\\"!!errorTks.courseGroups\\\"></text-tags-widget> <div if.to-view=\\\"errorTks.courseGroups\\\" class=\\\"invalid-feedback media-share-create-form__error\\\" t=\\\"${errorTks.courseGroups}\\\"></div> </div> <button-standard class=\\\"media-share-create-form__btn\\\" disabled.to-view=\\\"!canSave\\\" t=\\\"[btnText]elements.mediaShareCreateForm.addToCourse\\\" click.delegate=\\\"fireCreateShareEvent()\\\" icon-name=\\\"System/Tabler/Plus\\\"></button-standard> </div> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template> <custom-table columns.to-view=\\\"tableColumns\\\" extendable.to-view=\\\"!courseId\\\" table-id=\\\"media-shares-${courseId}-${courseGroupId}\\\" refresh-version.to-view=\\\"tableRefreshVersion\\\" get-shown-entries-data.call=\\\"fetchTableData(limit, offset)\\\"></custom-table>  </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"media-share-edit-list-entry ${ isProvider ? 'media-share-edit-list-entry--courses-only' : '' }\\\"> <div class=\\\"media-share-edit-list-entry__title-wrapper\\\"> <label class=\\\"form-label\\\" t=\\\"dialogs.mediaShareDialog.selectedCourse\\\"></label> <div class=\\\"media-share-edit-list-entry__title\\\">${course.title}</div> </div> <div if.to-view=\\\"!isProvider\\\" class=\\\"media-share-edit-list-entry__selection-wrapper\\\"> <label class=\\\"form-label\\\" t=\\\"globals.selectCourseGroup\\\"></label> <text-tags-widget table-columns.to-view=\\\"courseGroupsTableColumns\\\" table-data.to-view=\\\"filteredCourseGroups\\\" fixed-height.to-view=\\\"true\\\" tags.to-view=\\\"selectedCourseGroupTags\\\" tags-edited.trigger=\\\"handleCourseGroupTagsEdited($event)\\\" entry-selected.trigger=\\\"handleCourseGroupEntrySelected($event)\\\" search-string-changed.trigger=\\\"handleCourseGroupSearchStringChanged($event)\\\" list-closed.trigger=\\\"validateCourseGroups()\\\" is-invalid.to-view=\\\"!!courseGroupsErrorTk\\\"></text-tags-widget> <div class=\\\"invalid-feedback media-share-edit-list-entry__error\\\" t=\\\"${courseGroupsErrorTk}\\\"></div> </div> <button-standard class=\\\"media-share-edit-list-entry__btn\\\" btn-type=\\\"outline\\\" click.trigger=\\\"delete()\\\" icon-name=\\\"System/Tabler/Trash\\\" btn-color=\\\"danger\\\"></button-standard> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"card mp-flipping-card-form ${ block.id ? 'mp-flipping-card-form--full-size' : '' }\\\"> <div class=\\\"card-header\\\"> <h1 class=\\\"card-title\\\" t=\\\"pages.content-editor.block.mp-flipping-card\\\"></h1> <button-standard class=\\\"ml-auto\\\" click.trigger=\\\"handleCancelClick()\\\" icon-name=\\\"System/Tabler/X\\\" btn-type=\\\"outline\\\" btn-color=\\\"danger\\\"></button-standard> </div> <div if.to-view=\\\"!blockId\\\" class=\\\"card-body\\\"> <alert-message message.to-view=\\\"{ tk: `pages.content-editor.flippingCardWarning` }\\\" message-style=\\\"alert-warning\\\"></alert-message> </div> <div if.to-view=\\\"block.id\\\" class=\\\"card-body mp-flipping-card-form__body\\\"> <navigation-tabs class=\\\"mp-flipping-card-form__navbar\\\" tabs.to-view=\\\"navigationTabs\\\" fill-height.to-view=\\\"true\\\" disabled.to-view=\\\"loading\\\"></navigation-tabs> <div class=\\\"mp-flipping-card-form__content\\\"> <picture-select-form if.to-view=\\\"activeFlippingCardPage\\\" t=\\\"[title]globals.picture\\\" show-picture-title.to-view=\\\"false\\\" picture-id.two-way=\\\"activeFlippingCardPage.pictureId\\\" picture.to-view=\\\"activeFlippingCardPage.picture\\\" picture-url.to-view=\\\"activeFlippingCardPage.picture.url\\\"></picture-select-form> <div class=\\\"mp-flipping-card-form__text-editor-wrapper\\\"> <label class=\\\"form-label\\\" t=\\\"elements.flippingCardForm.content\\\"></label> <scroll-inset class=\\\"mp-flipping-card-form__text-editor-scrollbox ${ editorIsFocused ? 'mp-flipping-card-form__text-editor-scrollbox--focused' : ''}\\\" click.delegate=\\\"focusTextEditor()\\\" show-shadows.to-view=\\\"false\\\"> <text-editor if.to-view=\\\"activeFlippingCardPage\\\" view-model.ref=\\\"textEditor\\\" class=\\\"mp-flipping-card-form__text-editor\\\" is-focused.from-view=\\\"editorIsFocused\\\" editor-context.to-view=\\\"editorContext\\\" text.to-view=\\\"activeFlippingCardPage.text\\\" mode=\\\"system-text\\\" autofocus.to-view=\\\"true\\\" dispatch-null-if-empty.to-view=\\\"true\\\" save-on-exit.to-view=\\\"false\\\" rich-text-changed.trigger=\\\"handleRichTextChanged($event)\\\"></text-editor> </scroll-inset> <rich-text-editor-toolbar if.to-view=\\\"activeFlippingCardPage\\\" editor-context.to-view=\\\"editorContext\\\" mode=\\\"system-text\\\"></rich-text-editor-toolbar> </div> </div> </div> <div class=\\\"card-footer text-right\\\"> <button class=\\\"btn btn-primary\\\" disabled.to-view=\\\"!hasUnsavedChanges || loading\\\" click.delegate=\\\"handleSaveClick()\\\" t=\\\"globals.save\\\"></button> <button class=\\\"btn btn-secondary\\\" click.delegate=\\\"handleCancelClick()\\\" t=\\\"globals.cancel\\\"></button> </div> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"mp-hot-spot-editor\\\"> <aspect-ratio-container class=\\\"mp-hot-spot-editor__aspect-ratio-container\\\" mode=\\\"width-and-height-dependent\\\"> <template-preview-block if.to-view=\\\"!block.picture\\\" class=\\\"mp-hot-spot-editor__placeholder-picture\\\" block-type.to-view=\\\"BlockType.PICTURE\\\"></template-preview-block> <x-picture if.to-view=\\\"block.picture\\\" class=\\\"mp-hot-spot-editor__picture\\\" picture.to-view=\\\"block.picture\\\"></x-picture> <drop-zone ref=\\\"dropZoneDomElement\\\" class=\\\"mp-hot-spot-editor__drop-zone\\\" dnd-active.to-view=\\\"dndActive\\\" context.to-view=\\\"dndContext\\\" config.to-view=\\\"dropzoneConfig\\\" provide-dropzone-identifiers.to-view=\\\"false\\\"></drop-zone> <template repeat.for=\\\"hotSpot of block.hotSpots\\\"> <draggable class=\\\"mp-hot-spot-editor__btn ${ hotSpot.id === dndPayload.existingPayloadId ? 'mp-hot-spot-editor__btn--ghost' : '' }\\\" drag-dom-element.to-view=\\\"draggedHotSpotDomElement\\\" style.to-view=\\\"getHotSpotStyle(hotSpot)\\\" payload.to-view=\\\"{ existingPayloadId: hotSpot.id }\\\" dnd-active.to-view=\\\"dndActive\\\" context.to-view=\\\"dndContext\\\"> <safe-icon ref=\\\"draggedHotSpotDomElement\\\" class=\\\"mp-hot-spot-editor__dragged-icon\\\" aria-hidden=\\\"true\\\" icon-name=\\\"System/Tabler/Plus\\\"></safe-icon> <hot-spot-button selected.to-view=\\\"selectedHotSpot.id === hotSpot.id\\\" visited.to-view=\\\"true\\\" click.delegate=\\\"selectHotSpot(hotSpot)\\\"></hot-spot-button> <div class=\\\"mp-hot-spot-editor__order\\\">${hotSpot.order}</div> </draggable> </template> </aspect-ratio-container> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"card mp-hot-spot-form ${ blockId ? 'mp-hot-spot-form--full-size' : '' }\\\"> <div class=\\\"card-header\\\"> <h1 class=\\\"card-title\\\" t=\\\"pages.content-editor.block.mp-hot-spot\\\"></h1> <button-standard class=\\\"ml-auto\\\" click.trigger=\\\"handleCancelSaveBlock()\\\" icon-name=\\\"System/Tabler/X\\\" btn-type=\\\"outline\\\" btn-color=\\\"danger\\\"></button-standard> </div> <div if.to-view=\\\"!blockId\\\" class=\\\"card-body\\\"> <alert-message message.to-view=\\\"{ tk: `pages.content-editor.hotSpotWarning` }\\\" message-style=\\\"alert-warning\\\"></alert-message> </div> <div if.to-view=\\\"blockId\\\" class=\\\"card-body mp-hot-spot-form__body\\\"> <div class=\\\"mp-hot-spot-form__content\\\"> <mp-hot-spot-editor class=\\\"mp-hot-spot-form__editor\\\" block.to-view=\\\"block\\\" selected-hot-spot.to-view=\\\"selectedHotSpot\\\" selected.trigger=\\\"selectHotSpot($event.detail.hotSpot)\\\" update-position.trigger=\\\"updateHotSpotPosition($event)\\\"></mp-hot-spot-editor> </div> <div class=\\\"mp-hot-spot-form__sidebar\\\"> <button-standard class=\\\"mp-hot-spot-form__sidebar-btn\\\" click.delegate=\\\"createHotSpot()\\\" disabled.to-view=\\\"loading\\\" icon-name=\\\"System/Tabler/Plus\\\" t=\\\"[btn-text]elements.hotSpotForm.createHotSpot\\\" btn-fill-mode=\\\"fill-width\\\"></button-standard> <ul class=\\\"mp-hot-spot-form__list\\\"> <li repeat.for=\\\"hotSpot of hotSpots\\\" class=\\\"mp-hot-spot-form__entry ${ selectedHotSpot.id === hotSpot.id ? 'mp-hot-spot-form__entry--selected' : '' }\\\"> <div class=\\\"mp-hot-spot-form__entry-controls\\\"> <safe-icon class=\\\"mp-hot-spot-form__sort-btn\\\" use-as-button tabindex=\\\"${ !canOrderHotSpot(hotSpot.order, EOrderDirection.UP) ? '-1' : '0' }\\\" click.trigger=\\\"changeHotSpotOrder(hotSpot, EOrderDirection.UP)\\\" icon-name=\\\"System/Tabler/Triangle-Up\\\"></safe-icon> <safe-icon class=\\\"mp-hot-spot-form__sort-btn\\\" use-as-button tabindex=\\\"${ !canOrderHotSpot(hotSpot.order, EOrderDirection.DOWN) ? '-1' : '0' }\\\" click.trigger=\\\"changeHotSpotOrder(hotSpot, EOrderDirection.DOWN)\\\" icon-name=\\\"System/Tabler/Triangle-Down\\\"></safe-icon> </div> <div class=\\\"mp-hot-spot-form__entry-order\\\">${ loading ? ' ' : hotSpot.order }</div> <rich-text if.to-view=\\\"hotSpot.text.length\\\" class=\\\"mp-hot-spot-form__entry-text\\\" click.delegate=\\\"selectHotSpot(hotSpot)\\\" text.to-view=\\\"hotSpot.text\\\" context-identifier.to-view=\\\"contextIdentifier\\\" annotate.to-view=\\\"false\\\"></rich-text> <div else click.delegate=\\\"selectHotSpot(hotSpot)\\\" class=\\\"mp-hot-spot-form__entry-text d-flex align-items-center\\\" t=\\\"elements.hotSpotForm.placeholder\\\"></div> <button-standard class=\\\"mp-hot-spot-form__entry-btn mp-hot-spot-form__entry-btn--delete\\\" click.trigger=\\\"deleteHotSpot(hotSpot.id)\\\" disabled.to-view=\\\"loading\\\" icon-name=\\\"System/Tabler/Trash\\\" btn-type=\\\"uce\\\" btn-size=\\\"icon\\\" btn-color=\\\"danger\\\"></button-standard> </li> </ul> </div> <scroll-inset class=\\\"mp-hot-spot-form__text-editor-scrollbox\\\" click.delegate=\\\"focusTextEditor()\\\" show-shadows.to-view=\\\"false\\\"> <text-editor if.to-view=\\\"selectedHotSpot\\\" view-model.ref=\\\"textEditor\\\" class=\\\"mp-hot-spot-form__text-editor ${ editorIsFocused ? 'mp-hot-spot-form__text-editor--focused' : ''}\\\" is-focused.from-view=\\\"editorIsFocused\\\" editor-context.to-view=\\\"editorContext\\\" text.to-view=\\\"selectedHotSpot.text\\\" mode=\\\"system-text\\\" autofocus.to-view=\\\"true\\\" dispatch-null-if-empty.to-view=\\\"true\\\" save-on-exit.to-view=\\\"false\\\" rich-text-changed.trigger=\\\"handleRichTextChanged($event)\\\"></text-editor> </scroll-inset> <div class=\\\"mp-hot-spot-form__controls-wrapper\\\"> <div class=\\\"mp-hot-spot-form__controls\\\"> <div class=\\\"mp-hot-spot-form__picture-and-color-controls\\\"> <content-editor-color-select-preview color.to-view=\\\"block.stylePrimaryColor\\\" click.delegate=\\\"toggleColorSelect()\\\"></content-editor-color-select-preview> <content-editor-color-select if.to-view=\\\"showColorSelect\\\" class=\\\"mp-hot-spot-form__color-select\\\" current-color.to-view=\\\"block.stylePrimaryColor\\\" color-selected.trigger=\\\"handleColorSelected($event)\\\"></content-editor-color-select> <button-standard icon-name=\\\"Uce/Photo\\\" click.delegate=\\\"handleSelectImageClick()\\\" disabled.to-view=\\\"loading\\\" btn-size=\\\"icon\\\" tooltip-tk=\\\"pages.content-editor.tooltips.changeImage\\\" btn-color=\\\"secondary\\\"></button-standard> <button-standard if.to-view=\\\"block.picture != null\\\" icon-name=\\\"Uce/Crop\\\" click.delegate=\\\"handlePictureCrop()\\\" btn-size=\\\"icon\\\" tooltip-tk=\\\"pages.content-editor.tooltips.crop\\\" btn-color=\\\"secondary\\\"></button-standard> </div> <rich-text-editor-toolbar if.to-view=\\\"selectedHotSpot\\\" editor-context.to-view=\\\"editorContext\\\" mode=\\\"system-text\\\"></rich-text-editor-toolbar> </div> <button if.to-view=\\\"selectedHotSpot\\\" class=\\\"btn btn-primary\\\" disabled.to-view=\\\"loading\\\" click.delegate=\\\"updateHotSpotText()\\\" t=\\\"globals.save\\\"></button> </div> </div> <div if.to-view=\\\"!blockId\\\" class=\\\"card-footer text-right\\\"> <button type=\\\"submit\\\" class=\\\"btn btn-primary\\\" click.delegate=\\\"handleSaveBlock()\\\" t=\\\"globals.save\\\"></button> <button class=\\\"btn btn-secondary\\\" click.delegate=\\\"handleCancelSaveBlock()\\\" t=\\\"globals.cancel\\\"></button> </div> </template>\";\n// Exports\nexport default code;","// Module\nvar code = \"<template class=\\\"card\\\"> <div class=\\\"card-header\\\"> <h1 class=\\\"card-title\\\" t=\\\"elements.mpInteractiveForm.mpInteractiveTitle\\\"></h1> </div> <div class=\\\"card-body\\\"> <alert-message if.to-view=\\\"showPageConvertWarning\\\" message.to-view=\\\"{ tk: `pages.content-editor.mpInteractiveWarning` }\\\" message-style=\\\"alert-warning\\\"></alert-message> </div> <div class=\\\"card-footer text-right\\\"> <button type=\\\"submit\\\" class=\\\"btn btn-primary\\\" click.delegate=\\\"handleSaveBlock()\\\" t=\\\"globals.save\\\"></button> <button class=\\\"btn btn-secondary\\\" click.delegate=\\\"handleCancelSaveBlock()\\\" t=\\\"globals.cancel\\\"></button> </div> </template> \";\n// Exports\nexport default code;","import { autoinject, bindable, computedFrom } from 'aurelia-framework';\nimport { IMediaShareDetails } from 'common/dto/content/share/media.share.interface';\nimport { ICourseGroupResponseDto } from 'common/dto/course/course.group.interface';\nimport { ICourseEditResponseDto } from 'common/dto/course/management/course.management.interface';\nimport { ArrayUtils } from 'common/utils/ArrayUtils';\nimport { DomUtils } from 'common/utils/DomUtils';\nimport { assertNotNullOrUndefined } from 'common/utils/assert';\nimport { TenantApiService } from '../../../services/api/TenantApiService';\nimport { EntrySelectedEvent, ITextTagsWidgetTableColumn, SearchStringChangedEvent, TagsEditedEvent } from '../text-tags-widget/text-tags-widget';\n\n/**\n * @event {CreateShareEvent} create-share\n */\n@autoinject()\nexport class MediaShareCreateForm {\n  @bindable() public isProvider: boolean = false;\n  @bindable() public availableCourses: ICourseEditResponseDto[] = [];\n  private domElement: HTMLElement;\n\n  private selectedCourse: ICourseEditResponseDto | null = null;\n  private courseSearchString: string = '';\n  private courseTableColumns: ITextTagsWidgetTableColumn<ICourseEditResponseDto>[] = [{ transformer: e => e.title }];\n  private foundCourses: ICourseEditResponseDto[] = [];\n\n  private availableCourseGroups: ICourseGroupResponseDto<string>[] = [];\n  private courseGroupSearchString: string = '';\n  private selectedCourseGroups: ICourseGroupResponseDto<string>[] = [];\n  private courseGroupTableColumns: ITextTagsWidgetTableColumn<ICourseGroupResponseDto<string>>[] = [{ transformer: e => e.name }];\n  private foundCourseGroups: ICourseGroupResponseDto<string>[] = [];\n\n  private errorTks: IErrorTk = {\n    courseGroups: null,\n    course: null\n  };\n\n  constructor(\n    private tenantApiService: TenantApiService,\n    element: Element,\n  ) {\n    this.domElement = element as HTMLElement;\n  }\n\n  private availableCoursesChanged(): void {\n    this.updateCourseData();\n    this.fetchCourseGroups();\n  }\n\n  private async fetchCourseGroups(): Promise<void> {\n    if(this.isProvider) return;\n    const courseGroups = await this.tenantApiService.getAvailableCourseGroups(this.availableCourses.map(c => c.id));\n    this.availableCourseGroups = ArrayUtils.immutableSort(courseGroups, (a, b) => a.name.localeCompare(b.name));\n    this.updateCourseGroupData();\n  }\n\n  private validateCourse(): void {\n    this.errorTks.course = !this.selectedCourse ? 'utils.validation.noCourseSelectedError' : null;\n  }\n\n  private validateCourseGroups(): void {\n    this.errorTks.courseGroups = !this.selectedCourseGroups.length ? 'utils.validation.noCourseGroupsSelectedError' : null;\n  }\n\n  private handleCourseSearchStringChanged(event: SearchStringChangedEvent): void {\n    this.updateCourseData(event.detail.searchString.trim());\n  }\n\n  private handleCourseGroupSearchStringChanged(event: SearchStringChangedEvent): void {\n    this.updateCourseGroupData(event.detail.searchString.trim());\n  }\n\n  private handleCourseEntrySelected(event: EntrySelectedEvent<ICourseEditResponseDto>): void {\n    const selected = event.detail.entry;\n    this.selectedCourse = selected;\n    this.selectedCourseGroups = [];\n    this.updateCourseData();\n    this.validateCourse();\n  }\n\n  private handleCourseGroupEntrySelected(event: EntrySelectedEvent<ICourseGroupResponseDto<string>>): void {\n    const selected = event.detail.entry;\n    if(this.selectedCourseGroups.find(cG => cG.id === selected.id)) return;\n    this.selectedCourseGroups = [...this.selectedCourseGroups, selected];\n    this.updateCourseGroupData();\n    this.validateCourseGroups();\n  }\n\n  private handleCourseEntryRemoved(): void {\n    this.selectedCourse = null;\n    this.selectedCourseGroups = [];\n    this.updateCourseData();\n    this.validateCourse();\n  }\n\n  private handleCourseGroupEntryRemoved(event: TagsEditedEvent): void {\n    this.selectedCourseGroups = this.selectedCourseGroups.filter(cG => event.detail.tags.includes(cG.name));\n    this.updateCourseGroupData();\n    this.validateCourseGroups();\n  }\n\n  private updateCourseData(searchString?: string): void {\n    this.foundCourses = this.availableCourses.filter(\n      course => course.id !== this.selectedCourse?.id\n        && (searchString == null\n          || searchString.length === 0\n          || course.title.toLocaleLowerCase().includes(searchString.toLocaleLowerCase()))\n    );\n    this.updateCourseGroupData();\n  }\n\n  private updateCourseGroupData(searchString?: string): void {\n    const selectedCourseGroupIds = this.selectedCourseGroups.map(cG => cG.id);\n    this.foundCourseGroups = this.availableCourseGroups.filter(\n      cG => cG.course.id === this.selectedCourse?.id\n        && !selectedCourseGroupIds.includes(cG.id)\n        && (searchString == null\n          || searchString.length === 0\n          || cG.name.toLocaleLowerCase().includes(searchString.toLocaleLowerCase()))\n    );\n  }\n\n  @computedFrom('selectedCourse.id')\n  private get selectedCourseTags(): string[] {\n    return this.selectedCourse?.title ? [this.selectedCourse.title] : [];\n  }\n\n  @computedFrom('selectedCourseGroups.length')\n  private get selectedCourseGroupTags(): string[] {\n    return this.selectedCourseGroups.map(cG => cG.name);\n  }\n\n  @computedFrom('selectedCourse.id', 'selectedCourseGroups.length', 'isProvider')\n  private get canSave(): boolean {\n    return !!this.selectedCourse?.id && !!(this.isProvider || this.selectedCourseGroups.length);\n  }\n\n  private fireCreateShareEvent(): void {\n    assertNotNullOrUndefined(this.selectedCourse, 'selectedCourse cannot be null or undefined');\n    const limitToCourseGroupIds = this.isProvider ? undefined : this.selectedCourseGroups.map(cG => cG.id);\n\n    DomUtils.fireEvent<CreateShareEvent['detail']>(this.domElement, {\n      name: 'create-share',\n      bubbles: false,\n      detail: {\n        share: {\n          courseId: this.selectedCourse.id,\n          limitToCourseGroupIds\n        }\n      }\n    });\n    this.selectedCourse = null;\n    this.selectedCourseGroups = [];\n  }\n}\n\nexport type CreateShareEvent = Event & { detail: { share: IMediaShareDetails } };\n\ninterface IErrorTk {\n  courseGroups: string | null;\n  course: string | null;\n}","import { autoinject, bindable, computedFrom } from 'aurelia-framework';\nimport { StoreService } from 'common/aurelia-services/StoreService';\nimport { IMediaShareEntry } from 'common/dto/content/share/media.share.interface';\nimport { ColumnSortDirection, FilterOperationType } from 'common/dto/filter/filter.enum';\nimport { IFilterDto } from 'common/dto/filter/filter.interface';\nimport { ErrorType } from 'common/enum/errortype.enum';\nimport { RoleName } from 'common/enum/role.enums';\nimport { assertNotNullOrUndefined } from 'common/utils/assert';\nimport { CustomDialogService } from '../../../services/CustomDialogService';\nimport { ToastService } from '../../../services/ToastService';\nimport { MediaShareApiService } from '../../../services/api/MediaShareApiService';\nimport { IState } from '../../../store/state';\nimport { FilterUtils } from '../../../utils/FilterUtils';\nimport { MediaShareUtils } from '../../../utils/MediaShareUtils';\nimport { ActionsCell } from '../../elements/custom-table/actions-cell/actions-cell';\nimport { CustomTableCell } from '../../elements/custom-table/custom-table-cell/custom-table-cell';\nimport { VirtualFilterField, createPrepareServersideSortingFunction, prepareServersideFiltering } from '../../elements/custom-table/custom-table-helpers';\nimport { FilterSize, IColumnDefinition } from '../../elements/custom-table/custom-table-interfaces';\nimport { SharedToCell } from '../custom-table/shared-to-cell/shared-to-cell';\nimport { EOrderDirection, SortCell } from '../custom-table/sort-cell/sort-cell';\n\n@autoinject()\nexport class MediaShareTable {\n  @bindable() public courseId: number | null = null;\n  @bindable() public courseGroupId: number | null = null;\n  @bindable() public forTenantCoursesOnly: boolean = false;\n  @bindable() public forOverviewOnly: boolean = false;\n  @bindable() public tableRefreshVersion: number = 0;\n  private isProvider: boolean = false;\n\n  private filter: IFilterDto<IMediaShareEntry> = {\n    limit: 10,\n    offset: 0,\n    order: [{\n      field: 'order',\n      dir: ColumnSortDirection.ASC,\n    }],\n    filter: [],\n  };\n\n  @computedFrom('forTenantCoursesOnly', 'forOverviewOnly')\n  private get tableColumns(): Array<IColumnDefinition<CustomTableCell<IMediaShareEntry> | ActionsCell<IMediaShareEntry> | SharedToCell<IMediaShareEntry> | SortCell<IMediaShareEntry>>> {\n    return [\n      {\n        id: 'name',\n        nameTk: 'elements.mediaShareTable.title',\n        cellViewModel: CustomTableCell,\n        cellOptions: {\n          property: 'title'\n        },\n        filter: {\n          placeholderTk: 'globals.filter',\n          prepare: q => prepareServersideFiltering(this.filter, 'title', q),\n          size: FilterSize.LARGE\n        },\n        prepareSorting: createPrepareServersideSortingFunction(this.filter, 'title'),\n      },\n      ...(!this.forTenantCoursesOnly ? [{\n        id: 'shared',\n        nameTk: 'elements.mediaShareTable.sharedTo',\n        cellViewModel: SharedToCell,\n        cellOptions: {\n          showCourseGroupCount: () => !this.isProvider,\n          isFiltered: () => this.filter.filter?.some(f => (f.field as VirtualFilterField === 'course')) ?? false,\n        },\n        filter: {\n          placeholderTk: 'globals.filter',\n          prepare: (e: string) => prepareServersideFiltering(this.filter, 'course' as VirtualFilterField, e),\n          size: FilterSize.LARGE\n        },\n      }] : []),\n      ...(!this.forOverviewOnly && !this.forTenantCoursesOnly ? [{\n        id: 'order',\n        nameTk: 'elements.mediaShareTable.order',\n        cellViewModel: SortCell,\n        shrink: true,\n        visible: () => !!this.filter.filter?.some(f => ['courseId', 'courseGroupId'].includes(f.field)),\n        cellOptions: {\n          changeOrderCallback: (file: IMediaShareEntry, dir: EOrderDirection) => { this.changeMediaShareOrder(file, dir); }\n        },\n      }] : []),\n      {\n        id: 'actions',\n        nameTk: '',\n        shrink: true,\n        centered: true,\n        cellViewModel: ActionsCell,\n        visible: () => !this.forTenantCoursesOnly,\n        cellOptions: {\n          entries: [\n            {\n              labelTk: 'elements.mediaShareTable.editShares',\n              icon: 'fe-share',\n              clickCallback: (e: IMediaShareEntry) => { this.handleShareEntry(e); }\n            },\n            {\n              labelTk: 'elements.mediaShareTable.swapFile',\n              icon: 'fe-edit',\n              clickCallback: (e: IMediaShareEntry) => { this.handleMediaShareFileEdit(e); }\n            },\n            {\n              labelTk: 'elements.mediaShareTable.removeFile',\n              icon: 'fe-trash',\n              additionalClasses: 'text-danger',\n              clickCallback: (e: IMediaShareEntry) => { this.handleDeleteMediaShare(e); }\n            },\n          ]\n        }\n      }\n    ];\n  }\n\n  constructor(\n    private dialogService: CustomDialogService,\n    private mediaShareApiService: MediaShareApiService,\n    private storeService: StoreService<IState>,\n    private toastService: ToastService,\n  ) { }\n\n  protected attached(): void {\n    this.storeService.subscribeToDetailedChanges(this, [\n      state => state.currentRole?.role === RoleName.Provider,\n    ], (_state, isProvider) => {\n      this.isProvider = isProvider;\n      this.tableRefreshVersion++;\n    });\n  }\n\n  protected detached(): void {\n    this.storeService.disposeAllSubscriptions(this);\n  }\n\n  private courseIdChanged(): void {\n    this.tableRefreshVersion++;\n  }\n\n  private courseGroupIdChanged(): void {\n    this.tableRefreshVersion++;\n  }\n\n  private async handleShareEntry(file: IMediaShareEntry): Promise<void> {\n    const shareResult = await this.dialogService.showMediaShare({\n      mediaId: file.mediaId,\n      mediaType: file.mediaType,\n    });\n    if(!shareResult?.mediaId || !shareResult.mediaType) return;\n\n    await this.mediaShareApiService.setMediaShares({\n      mediaId: shareResult.mediaId,\n      mediaType: shareResult.mediaType,\n      shares: shareResult.shares\n    });\n    if(!MediaShareUtils.hasActiveShares(shareResult.shares)) {\n      this.toastService.warning('elements.mediaShareTable.noSharesOnEdit');\n    } else {\n      this.toastService.success('elements.mediaShareTable.mediaChanged');\n    }\n    this.tableRefreshVersion++;\n  }\n\n  private async handleDeleteMediaShare(share: IMediaShareEntry): Promise<void> {\n    const result = await this.dialogService.confirmDeletion('elements.mediaShareTable.deleteShares');\n    if(!result) return;\n\n    await this.mediaShareApiService.deleteShares(share.mediaType, share.mediaId);\n    this.tableRefreshVersion++;\n    this.toastService.genericDeleteSuccess('globals.share');\n  }\n\n  private async changeMediaShareOrder(share: IMediaShareEntry, dir: EOrderDirection): Promise<void> {\n    assertNotNullOrUndefined(share.id, 'share.id cannot be null or undefined');\n\n    const order = dir === EOrderDirection.UP ? share.order - 1 : share.order + 2;\n\n    await this.mediaShareApiService.changeOrder(share.id, { order });\n    this.tableRefreshVersion++;\n    this.toastService.success('elements.mediaShareTable.mediaChanged');\n  }\n\n  private async handleMediaShareFileEdit(file: IMediaShareEntry): Promise<void> {\n    const result = await this.dialogService.showMediaSelect({\n      showMediaTypeFilter: true,\n      preselectedMediaId: file.mediaId,\n      preselectedMediaType: file.mediaType,\n    });\n    if(!result || result?.value.id === file.mediaId) return;\n\n    if(!MediaShareUtils.isSupportedFileType(result.type)) {\n      this.toastService.error('utils.validation.unsupportedFileType');\n      return;\n    }\n\n    try {\n      await this.mediaShareApiService.changeMedia({\n        from: {\n          mediaId: file.mediaId,\n          mediaType: file.mediaType,\n        },\n        to: {\n          mediaId: result.value.id,\n          mediaType: result.type,\n        },\n      });\n      this.toastService.success('elements.mediaShareTable.mediaChanged');\n      this.tableRefreshVersion++;\n    } catch(e) {\n      if(e.message === ErrorType.SHARE_ALREADY_EXISTS) {\n        this.toastService.error('elements.mediaShareTable.shareExistsError');\n      } else {\n        this.toastService.error('elements.mediaShareTable.shareError');\n      }\n    }\n  }\n\n  private async fetchTableData(limit: number, offset: number): Promise<{ total: number; data: IMediaShareEntry[] }> {\n    this.filter.limit = limit;\n    this.filter.offset = offset;\n\n    if(this.courseId) {\n      this.filter = FilterUtils.removeDuplicateFilterFields(\n        this.filter,\n        {\n          field: 'courseId' as VirtualFilterField,\n          type: FilterOperationType.Eq,\n          value: this.courseId\n        }\n      );\n    }\n    if(this.courseGroupId) {\n      this.filter = FilterUtils.removeDuplicateFilterFields(\n        this.filter,\n        {\n          field: 'courseGroupId' as VirtualFilterField,\n          type: FilterOperationType.Eq,\n          value: this.courseGroupId\n        }\n      );\n    }\n    if(this.forTenantCoursesOnly) {\n      this.filter = FilterUtils.removeDuplicateFilterFields(\n        this.filter,\n        {\n          field: 'forTenantCoursesOnly' as VirtualFilterField,\n          type: FilterOperationType.Eq,\n          value: this.forTenantCoursesOnly\n        }\n      );\n    }\n    if(this.forOverviewOnly) {\n      this.filter = FilterUtils.removeDuplicateFilterFields(\n        this.filter,\n        {\n          field: 'forOverviewOnly' as VirtualFilterField,\n          type: FilterOperationType.Eq,\n          value: this.forOverviewOnly\n        }\n      );\n    }\n\n    return this.mediaShareApiService.getMyMediaShares(this.filter);\n  }\n}\n","import { FrameworkConfiguration, PLATFORM } from 'aurelia-framework';\n\nexport function configure(config: FrameworkConfiguration): void {\n  config.globalResources(PLATFORM.moduleName('resources/elements/media-share/media-share-edit-list-entry'));\n}\n","import { autoinject, bindable, computedFrom, observable } from 'aurelia-framework';\nimport { IMediaShareDetails } from 'common/dto/content/share/media.share.interface';\nimport { ICourseGroupResponseDto } from 'common/dto/course/course.group.interface';\nimport { ICourseEditResponseDto } from 'common/dto/course/management/course.management.interface';\nimport { ArrayUtils } from 'common/utils/ArrayUtils';\nimport { DomUtils } from 'common/utils/DomUtils';\nimport { TenantApiService } from '../../../services/api/TenantApiService';\nimport { EntrySelectedEvent, ITextTagsWidgetTableColumn, SearchStringChangedEvent, TagsEditedEvent } from '../text-tags-widget/text-tags-widget';\n\nexport type MediaShareEditListEntryDeleteEvent = Event & { detail: { course: ICourseEditResponseDto } };\nexport type MediaShareEditListEntryGroupsChangedEvent = Event & { detail: { course: ICourseEditResponseDto; limitToGroups: Array<ICourseGroupResponseDto<string>> | undefined } };\n\n/**\n * @event {MediaShareEditListEntryDeleteEvent} deleted\n * @event {MediaShareEditListEntryGroupsChangedEvent} groups-changed\n*/\n@autoinject()\nexport class MediaShareEditListEntry {\n  @bindable() public course: ICourseEditResponseDto | null = null;\n  @bindable() public mediaShareDetails: IMediaShareDetails | undefined;\n  @bindable() public isProvider: boolean = false;\n\n  @observable() private courseGroups: Array<ICourseGroupResponseDto<string>> = [];\n  @observable() private selectedCourseGroups: Array<ICourseGroupResponseDto<string>> = [];\n  @observable() private courseGroupSearchString: string = '';\n\n  private courseGroupsTableColumns: ITextTagsWidgetTableColumn<ICourseGroupResponseDto<string>>[] = [{ transformer: e => e.name }];\n  private courseGroupsErrorTk: string | null = null;\n\n  constructor(\n    private tenantApi: TenantApiService,\n    private element: Element,\n  ) { }\n\n\n  protected attached(): void {\n    this.fetchCourseGroups();\n  }\n\n  private courseChanged(): void {\n    if(!this.element.isConnected) return;\n    this.fetchCourseGroups();\n  }\n\n  private mediaShareDetailsChanged(): void {\n    this.injectMediaShareDetails();\n  }\n\n  private async fetchCourseGroups(): Promise<void> {\n    if(!this.course) return;\n    if(this.isProvider) {\n      this.courseGroups = [];\n      return;\n    }\n    const courseGroups = await this.tenantApi.getAvailableCourseGroups([this.course.id]);\n    this.courseGroups = courseGroups.sort((a, b) => a.name.localeCompare(b.name));\n    this.injectMediaShareDetails();\n  }\n\n  private injectMediaShareDetails(): void {\n    const { mediaShareDetails } = this;\n    if(!mediaShareDetails) return;\n    if(mediaShareDetails.limitToCourseGroupIds === undefined) return;\n    this.selectedCourseGroups = this.courseGroups.filter(cg => mediaShareDetails.limitToCourseGroupIds?.includes(cg.id));\n  }\n\n  @computedFrom('selectedCourseGroups')\n  private get selectedCourseGroupTags(): string[] {\n    return this.selectedCourseGroups.map(courseGroup => courseGroup.name);\n  }\n\n  @computedFrom('courseGroups', 'selectedCourseGroups', 'courseGroupSearchString')\n  private get filteredCourseGroups(): Array<ICourseGroupResponseDto<string>> {\n    let filtered = this.courseGroups.filter(cg => this.selectedCourseGroups.every(scg => scg.id !== cg.id));\n    const { courseGroupSearchString } = this;\n    if(courseGroupSearchString) {\n      filtered = filtered.filter(courseGroup =>\n        courseGroup.name.toLocaleLowerCase().includes(courseGroupSearchString.toLocaleLowerCase())\n      );\n    }\n    filtered = ArrayUtils.immutableSort(filtered, (a, b) => a.name.localeCompare(b.name));\n    return filtered;\n  }\n\n  private validateCourseGroups(): void {\n    this.courseGroupsErrorTk = !this.selectedCourseGroups.length ? 'utils.validation.noCourseGroupsSelectedError' : null;\n  }\n\n  private handleCourseGroupEntrySelected(event: EntrySelectedEvent<ICourseGroupResponseDto<string>>): void {\n    const selected = event.detail.entry;\n    if(!this.selectedCourseGroups.find(course => course.id === selected.id)) {\n      this.selectedCourseGroups = [...this.selectedCourseGroups, selected];\n    }\n    this.fireGroupsChangedEvent();\n    this.validateCourseGroups();\n  }\n\n  private handleCourseGroupTagsEdited(event: TagsEditedEvent): void {\n    this.selectedCourseGroups = this.selectedCourseGroups.filter(course => event.detail.tags.includes(course.name));\n    this.fireGroupsChangedEvent();\n    this.validateCourseGroups();\n  }\n\n  private async handleCourseGroupSearchStringChanged(event: SearchStringChangedEvent): Promise<void> {\n    this.courseGroupSearchString = event.detail.searchString;\n  }\n\n  private delete(): void {\n    if(!this.course) return;\n    DomUtils.fireEvent<MediaShareEditListEntryDeleteEvent['detail']>(this.element, {\n      bubbles: false,\n      name: 'deleted',\n      detail: {\n        course: this.course,\n      }\n    });\n  }\n\n  private fireGroupsChangedEvent(): void {\n    if(!this.course) return;\n    DomUtils.fireEvent<MediaShareEditListEntryGroupsChangedEvent['detail']>(this.element, {\n      bubbles: false,\n      name: 'groups-changed',\n      detail: {\n        course: this.course,\n        limitToGroups: !this.isProvider ? this.selectedCourseGroups : undefined\n      }\n    });\n  }\n}\n","import { autoinject, bindable, computedFrom } from 'aurelia-framework';\nimport { RichTextChangedEvent, TextEditor } from 'common/aurelia-components/text-editor/text-editor';\nimport { selectState } from 'common/aurelia-decorators/select-state';\nimport { StoreService } from 'common/aurelia-services/StoreService';\nimport { UniversalBlock } from 'common/dto/content-editor/content.editor.interface';\nimport { IMpFlippingCardPageDto } from 'common/dto/content/methodpool/flipping.card.page.interface';\nimport { ICourseBlockDto } from 'common/dto/course/content/block.interface';\nimport { BlockType } from 'common/dto/course/content/course.types';\nimport { IUpdateBlockDto } from 'common/dto/course/management/course.management.interface';\nimport * as ContentContextMutations from 'common/store/mutations/content.context.mutations';\nimport { blockByIdSelectorCreator } from 'common/store/selectors/content.context.selectors';\nimport { ArrayUtils } from 'common/utils/ArrayUtils';\nimport { DomUtils } from 'common/utils/DomUtils';\nimport { assertNotNullOrUndefined } from 'common/utils/assert';\nimport { ToastService } from '../../../services/ToastService';\nimport { ContentEditorApiService } from '../../../services/api/ContentEditorApiService';\nimport { MethodpoolApiService } from '../../../services/api/MethodpoolApiService';\nimport { IState } from '../../../store/state';\nimport { INavigationTab } from '../navigation-tabs/navigation-tabs';\n\nconst FlippingCardSide = {\n  FRONT: 1,\n  BACK: 2,\n};\n\n/**\n * @event {EditOkEvent} edit-ok\n * @event {EditCancelEvent} edit-cancel\n */\n@autoinject()\nexport class MpFlippingCardForm {\n  @bindable() public contextIdentifier: string | null = null;\n  @bindable() public blockId: number | null = null;\n  @selectState(MpFlippingCardForm, blockByIdSelectorCreator, ['contextIdentifier', 'blockId']) private block?: ICourseBlockDto | null;\n\n  private flippingCardPages: IMpFlippingCardPageDto[] = [];\n  private activeFlippingCardPage: IMpFlippingCardPageDto | null = null;\n  private editText: string = '';\n  private editorContext: string = 'mp-flipping-card-form';\n  private editorIsFocused: boolean = false;\n  private textEditor: TextEditor | null = null;\n\n  private activeTab: number = FlippingCardSide.FRONT;\n  private navigationTabs: INavigationTab[] = [];\n\n  private loading: boolean = false;\n  private BlockType = BlockType;\n  private domElement: HTMLElement;\n\n  constructor(\n    private methodpoolApiService: MethodpoolApiService,\n    private contentEditorApi: ContentEditorApiService,\n    private storeService: StoreService<IState>,\n    private toastService: ToastService,\n    element: Element,\n  ) {\n    this.domElement = element as HTMLElement;\n  }\n\n  private blockChanged(): void {\n    this.updateFlippingCards();\n  }\n\n  private async updateFlippingCards(): Promise<void> {\n    if(!this.block) return;\n    if(!this.block.flippingCardPages.length) {\n      const dto = { text: '', pictureId: null };\n      await this.methodpoolApiService.createFlippinCardPage(this.block.id, dto);\n      await this.methodpoolApiService.createFlippinCardPage(this.block.id, dto);\n      await this.updateBlock();\n    }\n    this.flippingCardPages = ArrayUtils.immutableSort(this.block.flippingCardPages, (a, b) => a.order - b.order);\n    this.selectActiveFlippingCard(this.activeFlippingCardPage?.order ?? FlippingCardSide.FRONT);\n  }\n\n  private selectActiveFlippingCard(side: number): void {\n    const activeFlippingCardPage = this.flippingCardPages.find(fCP => fCP.order === side);\n    if(activeFlippingCardPage != null) {\n      this.activeFlippingCardPage = {\n        id: activeFlippingCardPage.id,\n        order: activeFlippingCardPage.order,\n        picture: activeFlippingCardPage.picture,\n        pictureId: activeFlippingCardPage.pictureId,\n        text: activeFlippingCardPage.text,\n      };\n      this.editText = this.activeFlippingCardPage.text;\n    }\n    this.activeTab = side;\n    this.updateNavigationTabs();\n  }\n\n  private updateNavigationTabs(): void {\n    this.navigationTabs = [\n      {\n        title: { titleTk: 'elements.flippingCardForm.front' },\n        active: this.activeTab === FlippingCardSide.FRONT,\n        callback: async (): Promise<void> => {\n          await this.updateActiveFlippingCard();\n          this.selectActiveFlippingCard(FlippingCardSide.FRONT);\n        },\n      },\n      {\n        title: { titleTk: 'elements.flippingCardForm.back' },\n        active: this.activeTab === FlippingCardSide.BACK,\n        callback: async (): Promise<void> => {\n          await this.updateActiveFlippingCard();\n          this.selectActiveFlippingCard(FlippingCardSide.BACK);\n        },\n      }\n    ];\n  }\n\n  private async updateActiveFlippingCard(): Promise<void> {\n    assertNotNullOrUndefined(this.block?.id, 'block.id cannot be null or undefined');\n    if(!this.activeFlippingCardPage || !this.hasUnsavedChanges) return;\n\n    this.loading = true;\n    await this.methodpoolApiService.updateFlippinCardPage(this.block.id, this.activeFlippingCardPage.id, {\n      text: this.editText,\n      pictureId: this.activeFlippingCardPage.pictureId\n    });\n    await this.updateBlock();\n    this.loading = false;\n  }\n\n  private async updateBlock(updateBlock: IUpdateBlockDto = {}): Promise<void> {\n    assertNotNullOrUndefined(this.blockId, 'blockId cannot be null or undefined');\n    assertNotNullOrUndefined(this.contextIdentifier, 'contextIdentifier cannot be null or undefined');\n    const updatedBlock = await this.contentEditorApi.updateBlock<UniversalBlock>(this.blockId, updateBlock);\n    await this.storeService.dispatch(ContentContextMutations.updateBlock, this.contextIdentifier, updatedBlock);\n    this.toastService.success('globals.editSuccess');\n  }\n\n  private focusTextEditor(): void {\n    this.textEditor?.focus();\n  }\n\n  private handleRichTextChanged(event: RichTextChangedEvent): void {\n    const { value } = event.detail;\n    this.editText = value == null ? '' : JSON.stringify(value);\n  }\n\n  @computedFrom('block.flippingCardPages', 'activeFlippingCardPage.pictureId', 'editText')\n  private get hasUnsavedChanges(): boolean {\n    const oldFlippingCardPage = this.block?.flippingCardPages.find(fCP => fCP.order === this.activeFlippingCardPage?.order);\n    return oldFlippingCardPage?.pictureId !== this.activeFlippingCardPage?.pictureId\n      || this.editText !== oldFlippingCardPage?.text;\n  }\n\n  private handleSaveClick(): void {\n    if(this.activeFlippingCardPage) {\n      this.updateActiveFlippingCard();\n      return;\n    }\n    DomUtils.fireEvent(this.domElement, {\n      name: 'edit-ok',\n      bubbles: false,\n    });\n  }\n\n  private handleCancelClick(): void {\n    DomUtils.fireEvent(this.domElement, {\n      name: 'edit-cancel',\n      bubbles: false,\n    });\n  }\n}\n\nexport type EditOkEvent = CustomEvent<never>;\nexport type EditCancelEvent = CustomEvent<never>;\n","import { autoinject, bindable } from 'aurelia-framework';\nimport { SiteThemer, ThemeColorType } from 'common/aurelia-classes/SiteThemer';\nimport { MP_HOT_SPOT_BUTTON_SIZE, getHotSpotStyle } from 'common/aurelia-components/hot-spot-button/hot-spot-button';\nimport { ContentEditingInsertModeSelector } from 'common/constants/content.editing.constants';\nimport { FULL_HD } from 'common/constants/resolution.constants';\nimport { IMpHotSpotDto } from 'common/dto/content/methodpool/hot.spot.interface';\nimport { ICourseBlockDto } from 'common/dto/course/content/block.interface';\nimport { BlockType } from 'common/dto/course/content/course.types';\nimport { DomUtils } from 'common/utils/DomUtils';\nimport { MathUtils } from 'common/utils/MathUtils';\nimport {\n  DndActivityEventData,\n  DndContext,\n  DndDropEventData,\n  DndInsertModeSequential,\n  DndManager,\n  DndManagerService,\n  DndPayloadUniversal,\n  DndSubContext,\n  IDndDropzoneConfig\n} from '../../../services/DndManagerService';\n\n/**\n * @event {MpHotSpotEditorUpdatePositionEvent} update-position\n * @event {MpHotSpotEditorHotSpotSelectedEvent} selected\n */\n@autoinject()\nexport class MpHotSpotEditor {\n  @bindable() public selectedHotSpot: IMpHotSpotDto | null = null;\n  @bindable() public block: ICourseBlockDto | null = null;\n  private domElement: HTMLElement;\n  private BlockType = BlockType;\n\n  private dropZoneDomElement: HTMLElement | null = null;\n  private draggedHotSpotDomElement: HTMLElement | null = null;\n  private dndActive = false;\n  private dndContext: DndContext = DndContext.MpHotSpots;\n  private dndManager: DndManager<IMpHotSpotDto> | null = null;\n  private dndPayload: DndPayloadUniversal<IMpHotSpotDto> = null;\n  private dndLastEnteredInfo: IDndDropzoneConfig | null = null;\n  private dropzoneConfig: IDndDropzoneConfig = {\n    identifier: { superId: 0, subId: 0, },\n    insertModeSelector: ContentEditingInsertModeSelector.Plain,\n    subContext: DndSubContext.Content,\n  };\n\n  constructor(\n    private dndManagerService: DndManagerService,\n    private siteThemer: SiteThemer,\n    element: Element,\n  ) {\n    this.domElement = element as HTMLElement;\n  }\n\n  protected attached(): void {\n    this.dndManager = this.dndManagerService.getDndManager<IMpHotSpotDto>(this.dndContext);\n    this.dndManager.subscribePayloadChanged(this, this.dndPayloadChangedHandler.bind(this));\n    this.dndManager.subscribeToDragActivity(this, this.dndDragActivityHandler.bind(this));\n    this.dndManager.subscribeDrop(this, this.onDrop.bind(this));\n  }\n\n  protected detached(): void {\n    this.dndManager?.disposeSubscriptions(this);\n    this.dndLastEnteredInfo = null;\n  }\n\n  private getHotSpotStyle(hotSpot: IMpHotSpotDto): Record<string, string> {\n    const hotSpotColor = this.block?.stylePrimaryColor ?? this.siteThemer.getColor(ThemeColorType.Primary);\n\n    return getHotSpotStyle(hotSpot, hotSpotColor);\n  }\n\n  private selectHotSpot(hotSpot: IMpHotSpotDto): void {\n    DomUtils.fireEvent<MpHotSpotEditorHotSpotSelectedEvent['detail']>(this.domElement, {\n      name: 'selected',\n      detail: { hotSpot },\n      bubbles: false,\n    });\n  }\n\n  private fireUpdatePositionEvent(hotSpotId: number, posX: number, posY: number): void {\n    DomUtils.fireEvent<MpHotSpotEditorUpdatePositionEvent['detail']>(this.domElement, {\n      name: 'update-position',\n      detail: { hotSpotId, posX, posY },\n      bubbles: false,\n    });\n  }\n\n  private dndPayloadChangedHandler(payload: DndPayloadUniversal<IMpHotSpotDto>): void {\n    this.dndPayload = payload;\n    this.dndActive = payload != null;\n  }\n\n  private dndDragActivityHandler(dndInfo: DndActivityEventData<IDndDropzoneConfig>): void {\n    if(this.dndPayload == null) return;\n    if(dndInfo && this.dndLastEnteredInfo === null) this.dndLastEnteredInfo = dndInfo.dropTarget;\n    if(dndInfo.enterEvent && this.dndLastEnteredInfo) {\n      this.dndLastEnteredInfo = this.dndLastEnteredInfo !== dndInfo.dropTarget ? dndInfo.dropTarget : this.dndLastEnteredInfo;\n    }\n  }\n\n  private async onDrop(dropInfo: DndDropEventData<IMpHotSpotDto>): Promise<void> {\n    this.dndActive = false;\n    if(dropInfo.dndPayload == null\n      || dropInfo.dndPayload.existingPayloadId == null\n      || dropInfo.event == null\n      || dropInfo.dndPayload.existingPayloadId === dropInfo.dropTarget.identifier.superId) return;\n\n    const { x, y, width, height } = this.dropZoneDomElement?.getBoundingClientRect() ?? { x: 0, y: 0, width: 0, height: 0 };\n    const hotSpotRadius = MP_HOT_SPOT_BUTTON_SIZE / 2;\n    const posX = dropInfo.event.pageX - x - hotSpotRadius;\n    const posY = dropInfo.event.pageY - y - hotSpotRadius;\n\n    this.fireUpdatePositionEvent(\n      dropInfo.dndPayload.existingPayloadId,\n      MathUtils.clamp(MathUtils.roundToPrecision(posX / width * FULL_HD.width, 2), 0, (FULL_HD.width - MP_HOT_SPOT_BUTTON_SIZE)),\n      MathUtils.clamp(MathUtils.roundToPrecision(posY / height * FULL_HD.height, 2), 0, (FULL_HD.height - MP_HOT_SPOT_BUTTON_SIZE))\n    );\n  }\n\n  private getDropzoneConfig(\n    index: number,\n    insertMode: DndInsertModeSequential\n  ): IDndDropzoneConfig {\n    const config: IDndDropzoneConfig = {\n      identifier: {\n        superId: this.block?.hotSpots[index].id ?? -1,\n        subId: 0,\n      },\n      insertModeSelector: ContentEditingInsertModeSelector.Plain,\n      insertModeSequential: insertMode,\n      sequentialOrder: insertMode === DndInsertModeSequential.Before ? index + 1 : index + 2,\n    };\n    return config;\n  }\n}\n\nexport type MpHotSpotEditorUpdatePositionEvent = CustomEvent<{ hotSpotId: number; posX: number; posY: number }>;\nexport type MpHotSpotEditorHotSpotSelectedEvent = CustomEvent<{ hotSpot: IMpHotSpotDto }>;\n","import { autoinject, bindable } from 'aurelia-framework';\nimport { IImageCropperData } from 'common/aurelia-components/image-cropper/image-cropper';\nimport { RichTextChangedEvent, TextEditor } from 'common/aurelia-components/text-editor/text-editor';\nimport { selectState } from 'common/aurelia-decorators/select-state';\nimport { StoreService } from 'common/aurelia-services/StoreService';\nimport { FULL_HD } from 'common/constants/resolution.constants';\nimport { UniversalBlock } from 'common/dto/content-editor/content.editor.interface';\nimport { IMpHotSpotDto } from 'common/dto/content/methodpool/hot.spot.interface';\nimport { IPictureCropOptionsDto } from 'common/dto/content/pictures/picture.block.render.options.interface';\nimport { ICourseBlockDto } from 'common/dto/course/content/block.interface';\nimport { BlockType } from 'common/dto/course/content/course.types';\nimport { IUpdateBlockDto } from 'common/dto/course/management/course.management.interface';\nimport { GalleryMediaType } from 'common/enum/gallery.enum';\nimport * as ContentContextMutations from 'common/store/mutations/content.context.mutations';\nimport { blockByIdSelectorCreator } from 'common/store/selectors/content.context.selectors';\nimport { ArrayUtils } from 'common/utils/ArrayUtils';\nimport { DomUtils } from 'common/utils/DomUtils';\nimport { assertNotNullOrUndefined } from 'common/utils/assert';\nimport { CustomDialogService } from '../../../services/CustomDialogService';\nimport { ToastService } from '../../../services/ToastService';\nimport { ContentEditorApiService } from '../../../services/api/ContentEditorApiService';\nimport { MethodpoolApiService } from '../../../services/api/MethodpoolApiService';\nimport { IState } from '../../../store/state';\nimport { ColorSelectedEvent } from '../content-editor-color-select/content-editor-color-select';\nimport { MpHotSpotEditorUpdatePositionEvent } from '../mp-hot-spot-editor/mp-hot-spot-editor';\n\nenum EOrderDirection {\n  UP = 'UP',\n  DOWN = 'DOWN',\n}\n\n/**\n * @event {EditOkEvent} edit-ok\n * @event {EditCancelEvent} edit-cancel\n */\n@autoinject()\nexport class MpHotSpotForm {\n  @bindable() public contextIdentifier: string | null = null;\n  @bindable() public blockId: number | null = null;\n  @selectState(MpHotSpotForm, blockByIdSelectorCreator, ['contextIdentifier', 'blockId']) private block?: ICourseBlockDto | null;\n\n  private hotSpots: IMpHotSpotDto[] = [];\n  private selectedHotSpot: IMpHotSpotDto | null = null;\n\n  private editText: string = '';\n  private editorContext: string = 'mp-hot-spot-form';\n  private editorIsFocused: boolean = false;\n  private textEditor: TextEditor | null = null;\n\n  private showColorSelect: boolean = false;\n  private loading: boolean = false;\n  private BlockType = BlockType;\n  private EOrderDirection = EOrderDirection;\n  private domElement: HTMLElement;\n\n  constructor(\n    private methodpoolApiService: MethodpoolApiService,\n    private customDialogService: CustomDialogService,\n    private contentEditorApi: ContentEditorApiService,\n    private storeService: StoreService<IState>,\n    private toastService: ToastService,\n    element: Element,\n  ) {\n    this.domElement = element as HTMLElement;\n  }\n\n  private blockChanged(): void {\n    this.updateHotSpots();\n  }\n\n  private async handleSelectImageClick(): Promise<void> {\n    assertNotNullOrUndefined(this.blockId, 'blockId cannot be null or undefined');\n    assertNotNullOrUndefined(this.contextIdentifier, 'contextIdentifier cannot be null or undefined');\n\n    const result = await this.customDialogService.showMediaSelect({\n      selectedMediaType: GalleryMediaType.Picture\n    });\n    if(result?.type !== GalleryMediaType.Picture) return;\n    await this.updateBlock({ pictureId: result.value.id });\n  }\n\n  private async createHotSpot(): Promise<void> {\n    assertNotNullOrUndefined(this.blockId, 'blockId cannot be null or undefined');\n\n    this.loading = true;\n    try {\n      await this.updateHotSpotText();\n      this.selectedHotSpot = await this.methodpoolApiService.createHotSpot(this.blockId, {\n        text: '',\n        posY: FULL_HD.height / 2,\n        posX: FULL_HD.width / 2,\n        order: 0\n      });\n      await this.updateBlock();\n    } finally {\n      this.loading = false;\n    }\n  }\n\n  private async changeHotSpotOrder(hotSpot: IMpHotSpotDto, dir: EOrderDirection): Promise<void> {\n    assertNotNullOrUndefined(this.blockId, 'blockId cannot be null or undefined');\n\n    this.loading = true;\n    try {\n      const order = dir === EOrderDirection.UP ? hotSpot.order - 1 : hotSpot.order + 2;\n      await this.methodpoolApiService.changeHotSpotOrder(this.blockId, hotSpot.id, { order });\n      await this.updateBlock();\n    } finally {\n      this.loading = false;\n    }\n  }\n\n  private async updateHotSpotPosition(event: MpHotSpotEditorUpdatePositionEvent): Promise<void> {\n    assertNotNullOrUndefined(this.blockId, 'blockId cannot be null or undefined');\n    const { posX, posY, hotSpotId } = event.detail;\n\n    this.loading = true;\n    try {\n      await this.methodpoolApiService.updateHotSpot(this.blockId, hotSpotId, {\n        posX,\n        posY\n      });\n      await this.updateBlock();\n    } finally {\n      this.loading = false;\n    }\n  }\n\n  private async updateHotSpotText(): Promise<void> {\n    assertNotNullOrUndefined(this.blockId, 'blockId cannot be null or undefined');\n\n    if(!this.selectedHotSpot || this.editText === this.selectedHotSpot.text) return;\n    this.selectedHotSpot.text = this.editText;\n\n    this.loading = true;\n    try {\n      await this.methodpoolApiService.updateHotSpot(this.blockId, this.selectedHotSpot.id, {\n        text: this.editText,\n      });\n      await this.updateBlock();\n    } finally {\n      this.loading = false;\n    }\n  }\n\n  private async deleteHotSpot(hotSpotId: number): Promise<void> {\n    if(this.selectedHotSpot?.id === hotSpotId) {\n      this.selectedHotSpot = null;\n    }\n    this.loading = true;\n    try {\n      await this.methodpoolApiService.deleteHotSpot(hotSpotId);\n      await this.updateBlock();\n    } finally {\n      this.loading = false;\n    }\n  }\n\n  private async handlePictureCrop(): Promise<void> {\n    assertNotNullOrUndefined(this.contextIdentifier, 'contextIdentifier cannot be null or undefined');\n    assertNotNullOrUndefined(this.block?.picture?.url, 'block.picture.url cannot be null or undefined');\n    assertNotNullOrUndefined(this.block?.picture.width, 'block.picture.width cannot be null or undefined');\n    assertNotNullOrUndefined(this.block?.picture.height, 'block.picture.height cannot be null or undefined');\n\n    const initialData: IImageCropperData | undefined = this.block?.pictureBlockRenderOptions ? {\n      top: this.block.pictureBlockRenderOptions.top,\n      bottom: this.block.pictureBlockRenderOptions.bottom,\n      left: this.block.pictureBlockRenderOptions.left,\n      right: this.block.pictureBlockRenderOptions.right,\n      angle: this.block.pictureBlockRenderOptions.orientation,\n      flipped: this.block.pictureBlockRenderOptions.flipped\n    } : undefined;\n    const url = this.block?.originalPictureUrl ?? this.block?.picture.url;\n    const cropperData = await this.customDialogService.showCropImageDialog({ initialData, imageUrl: url, useWideScreenViewPort: true });\n    if(!cropperData) return;\n\n    const dto: IPictureCropOptionsDto = {\n      top: cropperData.top,\n      bottom: cropperData.bottom,\n      left: cropperData.left,\n      right: cropperData.right,\n      orientation: cropperData.angle,\n      flipped: cropperData.flipped\n    };\n    const updatedBlock = await this.contentEditorApi.updatePictureBlockRenderOptions<UniversalBlock>(this.block.id, dto);\n    await this.storeService.dispatch(ContentContextMutations.updateBlock, this.contextIdentifier, updatedBlock);\n    this.toastService.success('globals.editSuccess');\n  }\n\n  private async updateBlock(updateBlock: IUpdateBlockDto = {}): Promise<void> {\n    assertNotNullOrUndefined(this.blockId, 'blockId cannot be null or undefined');\n    assertNotNullOrUndefined(this.contextIdentifier, 'contextIdentifier cannot be null or undefined');\n    const updatedBlock = await this.contentEditorApi.updateBlock<UniversalBlock>(this.blockId, updateBlock);\n    await this.storeService.dispatch(ContentContextMutations.updateBlock, this.contextIdentifier, updatedBlock);\n    this.toastService.success('globals.editSuccess');\n  }\n\n  private focusTextEditor(): void {\n    this.textEditor?.focus();\n  }\n\n  private handleRichTextChanged(event: RichTextChangedEvent): void {\n    const { value } = event.detail;\n    this.editText = value == null ? '' : JSON.stringify(value);\n  }\n\n  private updateHotSpots(): void {\n    this.hotSpots = ArrayUtils.immutableSort(\n      this.block?.hotSpots ?? [],\n      (a, b) => a.order - b.order,\n    );\n  }\n\n  private canOrderHotSpot(order: number, dir: EOrderDirection): boolean {\n    if(dir === EOrderDirection.UP) return order > 1;\n    if(dir === EOrderDirection.DOWN) return order < (this.block?.hotSpots.length ?? 0);\n    return true;\n  }\n\n  private async selectHotSpot(hotSpot: IMpHotSpotDto): Promise<void> {\n    await this.updateHotSpotText();\n    this.selectedHotSpot = this.selectedHotSpot?.id === hotSpot.id ? null : hotSpot;\n    this.editText = this.selectedHotSpot?.text ?? '';\n  }\n\n  private toggleColorSelect(): void {\n    this.showColorSelect = !this.showColorSelect;\n  }\n\n  private async handleColorSelected(event: ColorSelectedEvent): Promise<void> {\n    await this.updateBlock({ stylePrimaryColor: event.detail.color });\n    this.showColorSelect = false;\n  }\n\n  private handleSaveBlock(): void {\n    DomUtils.fireEvent(this.domElement, {\n      name: 'edit-ok',\n      bubbles: false,\n    });\n  }\n\n  private handleCancelSaveBlock(): void {\n    DomUtils.fireEvent(this.domElement, {\n      name: 'edit-cancel',\n      bubbles: false,\n    });\n  }\n}\n\nexport type EditOkEvent = CustomEvent<never>;\nexport type EditCancelEvent = CustomEvent<never>;\n","import { autoinject, bindable } from 'aurelia-framework';\nimport { DomUtils } from 'common/utils/DomUtils';\nimport { IContentEditorBlockEditorBlock } from '../../dialogs/content-editor-block-editor-dialog/content-editor-block-editor-dialog';\n\n/**\n * @event {EditMpInteractiveOkEvent} edit-mp-interactive-ok\n * @event {EditMpInteractiveCancelEvent} edit-mp-interactive-cancel\n */\n@autoinject()\nexport class MpInteractiveForm {\n  @bindable() public block: IContentEditorBlockEditorBlock | null = null;\n  @bindable() public isIas?: boolean = false;\n  private domElement: HTMLElement;\n  private showPageConvertWarning: boolean = false;\n\n  constructor(\n    element: Element,\n  ) {\n    this.domElement = element as HTMLElement;\n  }\n\n  protected attached(): void {\n    this.showPageConvertWarning = !this.block?.text && !this.isIas;\n  }\n\n  private handleSaveBlock(): void {\n    DomUtils.fireEvent(this.domElement, {\n      name: 'edit-mp-interactive-ok',\n      bubbles: false,\n    });\n  }\n\n  private handleCancelSaveBlock(): void {\n    DomUtils.fireEvent(this.domElement, {\n      name: 'edit-mp-interactive-cancel',\n      bubbles: false,\n    });\n  }\n}\n\nexport type EditMpInteractiveOkEvent = CustomEvent<never>;\nexport type EditMpInteractiveCancelEvent = CustomEvent<never>;"],"names":[],"sourceRoot":""}